@woosh/meep-engine 3.14.3 → 3.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
  3. package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
  4. package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
  5. package/src/engine/Engine.d.ts.map +1 -1
  6. package/src/engine/Engine.js +733 -726
  7. package/src/engine/network/NetworkSession.d.ts +24 -4
  8. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  9. package/src/engine/network/NetworkSession.js +28 -4
  10. package/src/engine/network/orchestrator/NetworkPeer.d.ts +96 -7
  11. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  12. package/src/engine/network/orchestrator/NetworkPeer.js +392 -68
  13. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
  14. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  15. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
  16. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +105 -13
  17. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  18. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +306 -59
  19. package/src/engine/network/replication/Replicator.d.ts +43 -7
  20. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  21. package/src/engine/network/replication/Replicator.js +309 -111
  22. package/src/engine/network/sim/ActionLog.d.ts +23 -3
  23. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  24. package/src/engine/network/sim/ActionLog.js +26 -3
  25. package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
  26. package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
  27. package/src/engine/network/sim/ActionRecordCursor.js +120 -0
  28. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  29. package/src/engine/network/sim/RewindEngine.js +13 -29
  30. package/src/engine/network/sim/SimAction.d.ts +15 -0
  31. package/src/engine/network/sim/SimAction.d.ts.map +1 -1
  32. package/src/engine/network/sim/SimAction.js +15 -0
  33. package/src/engine/network/sim/SimActionExecutor.d.ts +51 -2
  34. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  35. package/src/engine/network/sim/SimActionExecutor.js +74 -7
  36. package/src/engine/network/transport/Channel.d.ts +1 -1
  37. package/src/engine/network/transport/Channel.js +1 -1
  38. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  39. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  40. package/src/engine/simulation/Ticker.js +4 -1
  41. package/src/engine/ui/GUIEngine.d.ts.map +1 -1
  42. package/src/engine/ui/GUIEngine.js +6 -0
  43. package/src/shade/playground/particle_system/README.md +98 -0
  44. package/src/shade/playground/particle_system/index.html +124 -0
  45. package/src/shade/playground/particle_system/main.d.ts +2 -0
  46. package/src/shade/playground/particle_system/main.d.ts.map +1 -0
  47. package/src/shade/playground/particle_system/main.js +180 -0
  48. package/src/shade/playground/particle_system/particle_scene.d.ts +52 -0
  49. package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -0
  50. package/src/shade/playground/particle_system/particle_scene.js +553 -0
  51. package/src/shade/playground/skinned_mesh_soup/README.md +143 -0
  52. package/src/shade/playground/skinned_mesh_soup/index.html +202 -0
  53. package/src/shade/playground/skinned_mesh_soup/main.d.ts +7 -0
  54. package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -0
  55. package/src/shade/playground/skinned_mesh_soup/main.js +567 -0
  56. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +29 -0
  57. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -0
  58. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +428 -0
  59. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  60. package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
  61. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
  62. package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
  63. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
  64. package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
  65. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
  66. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
  67. package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
  68. package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
  69. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
  70. package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
  71. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
  72. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
  73. package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
  74. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
  75. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
  76. package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
  77. package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
  78. package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
  79. package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
  80. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
  81. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
  82. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
  83. package/src/shade/renderer/particles/DESIGN.md +5 -5
  84. package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +1 -1
  85. package/src/shade/renderer/particles/cull/chunk_particle_cull.js +3 -1
  86. package/src/shade/renderer/particles/data/chunk_particle_emitter.js +1 -1
  87. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
  88. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
  89. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
  90. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
  91. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  92. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
  93. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
  94. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  95. package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
  96. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
  97. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
  98. package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
  99. package/src/shade/renderer/particles/isa/InstructionStream.d.ts +89 -0
  100. package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -0
  101. package/src/shade/renderer/particles/isa/InstructionStream.js +162 -0
  102. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +1 -48
  103. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
  104. package/src/shade/renderer/particles/isa/ParticleAssembler.js +25 -114
  105. package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
  106. package/src/shade/renderer/particles/particle_prototype.js +181 -180
  107. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
  108. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  109. package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
  110. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +1 -1
  111. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +4 -2
  112. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +4 -4
  113. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
  114. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +7 -5
  115. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
  116. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +3 -1
  117. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  118. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +3 -1
  119. package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
  120. package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +5 -2
  121. package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
  122. package/src/shade/renderer/particles/shaders/shader_particle_render.js +9 -2
  123. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  124. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -1
  125. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
  126. package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
  127. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
  128. package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
  129. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
  130. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
  131. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
  132. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
  133. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
  134. package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
  135. package/src/shade/renderer/particles/prototypeParticleSystem.d.ts +0 -2
  136. package/src/shade/renderer/particles/prototypeParticleSystem.d.ts.map +0 -1
  137. package/src/shade/renderer/particles/prototypeParticleSystem.js +0 -448
@@ -0,0 +1,567 @@
1
+ import Vector3 from "../../../core/geom/Vector3.js";
2
+ import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
3
+ import ConcurrentExecutor from "../../../core/process/executor/ConcurrentExecutor.js";
4
+ import { AssetManager } from "../../../engine/asset/AssetManager.js";
5
+ import { GameAssetType } from "../../../engine/asset/GameAssetType.js";
6
+ import { ArrayBufferLoader } from "../../../engine/asset/loaders/ArrayBufferLoader.js";
7
+ import {
8
+ GLTFSceneBundleAssetLoader
9
+ } from "../../../engine/asset/loaders/GLTFSceneBundleAssetLoader.js";
10
+ import { ImageBitmapAssetLoader } from "../../../engine/asset/loaders/image/ImageBitmapAssetLoader.js";
11
+ import { load_model_scene_bundle } from "../../../engine/asset/load_model_scene_bundle.js";
12
+ import { instantiate_scene_bundle } from "../../../engine/graphics3/instantiate_scene_bundle.js";
13
+ import { AnimationClipFlags } from "../../renderer/animation/AnimationClipFlags.js";
14
+ import { camera_frame_sphere } from "../../renderer/camera/camera_frame_sphere.js";
15
+ import { OrbitalCameraController } from "../../renderer/camera/OrbitalCameraController.js";
16
+ import { PerspectiveCamera } from "../../renderer/camera/PerspectiveCamera.js";
17
+ import { create_frame_loop } from "../../renderer/create_frame_loop.js";
18
+ import { make_sunlight } from "../../renderer/light/make_sunlight.js";
19
+ import { Renderer } from "../../renderer/Renderer.js";
20
+ import { Node3D } from "../../renderer/scene/Node3D.js";
21
+ import { Scene } from "../../renderer/scene/Scene.js";
22
+ import { load_default_environment } from "../load_default_environment.js";
23
+
24
+ /**
25
+ * A skinned character, loaded the way the game loads one, with the pieces that could be making soup
26
+ * put on switches.
27
+ *
28
+ * ## Why this page exists next to `skinned_blas_refit`
29
+ *
30
+ * That page renders the same character and answers a sharp question about the BLAS — does the tree
31
+ * follow the mesh — with `verify()`. It answers that question *numerically*, by holding the tree's
32
+ * root box against the bounds chain's box over the same deformed vertices, and both of those are
33
+ * derived from the clone's vertex region. **Vertices that are wrong in the same way agree with each
34
+ * other perfectly.** So a refit can be correct, `verify()` green, every internal node exactly its
35
+ * children's union — and the picture still soup, because the thing the tree faithfully bounds is
36
+ * itself wrong.
37
+ *
38
+ * This page is that other question, and it is built to bisect it rather than to look good:
39
+ *
40
+ * - **`?skin=off`** — no skin registered. A {@link SkinnedMesh} still gets a vertex clone (see
41
+ * {@link GPUMeshSkinningContext#obtain_geometry_index}), but nothing ever deforms it, so what is
42
+ * drawn is the memcpy of the source: the rest pose, under the mesh node's own transform. For a
43
+ * Mixamo character that transform carries the file's centimetre scale and its up-axis rotation,
44
+ * so a *correct* result here is a 1.7 cm figure lying on its side. Soup here is the geometry
45
+ * upload — the meshlet copy or its address patch — and has nothing to do with skinning.
46
+ * - **`?skin=bind`** — the skin registered, no clip. The skinning pass runs every frame over a pose
47
+ * that never changes, and the answer it should write is one the CPU knows exactly. `diagnose()`
48
+ * in the console reads the clone's vertices back and says which of them are wrong and in what
49
+ * pattern — see {@link verify_clone_vertices}. Soup here is the skinning machinery.
50
+ * - **`?skin=animated`** — the dance, as `skinned_blas_refit` runs it. Soup only here is the pose:
51
+ * the joint matrices, their range, or the order the skin's joints were resolved in.
52
+ *
53
+ * ## What the bisect found, and what was done
54
+ *
55
+ * `chunk_skin_blend_mesh_local` folds `inverse(mesh.transform_global)` into each joint matrix and
56
+ * converts the product to a dual quaternion, which can only carry a rigid motion. Michelle's
57
+ * positions and inverse-bind matrices are authored in the already-scaled space, so
58
+ * `joint.global × inverse_bind` is the *identity* and the fold was a bare 100×.
59
+ * `dual_quat_from_m4` reads `sqrt(max(0, 1 ± m00 ± m11 ± m22))` over terms of `1 + 100·(…)`, clamps
60
+ * whichever go negative and normalizes the rest, so the rotation it extracted was wrong — and wrong
61
+ * differently per joint, which is what tore the mesh apart. `?skin=bind` was the right character at
62
+ * 1/100 size (one shared matrix, one shared error) and `?skin=animated` was soup.
63
+ *
64
+ * The frame is now the engine's rather than the asset's: `register_skin` puts each skinned mesh on
65
+ * its skin's own bind transform, so the fold is the identity at bind and a rigid displacement under
66
+ * a clip, for every shape a glTF file arrives in. See `renderer/animation/SKINNING_FRAME_PLAN.md`.
67
+ *
68
+ * All of which was established without a device — `__soup_blend_probe.spec.js` runs the shader's own
69
+ * blend over this character's own matrices, and the emulator tier in
70
+ * `chunk_skin_blend_mesh_local.spec.js` runs the real WGSL over both shapes. This page is where that
71
+ * is confirmed against pixels, which by then is a formality.
72
+ *
73
+ * ## Why through the asset framework
74
+ *
75
+ * `skinned_blas_refit` calls `load_gltf` directly and parses the file once per character.
76
+ * {@link GLTFSceneBundleAssetLoader} is what the game and the editor register, and it hands **the
77
+ * same bundle** to every caller — so a second character is not a second parse, it is
78
+ * {@link instantiate_scene_bundle}: its own node tree, its own skins bound to that tree's joints,
79
+ * its own clips retargeted onto it, sharing the geometry and the inverse-bind matrices. That is a
80
+ * materially different path into the renderer, and running the repro on it means a fix found here
81
+ * is a fix for the engine rather than for one prototype page.
82
+ *
83
+ * Runs under the dev server (`npm run dev`), which serves `test_assets/`.
84
+ *
85
+ * @author Alex Goldring
86
+ * @copyright Company Named Limited (c) 2026
87
+ */
88
+
89
+ /**
90
+ * Served by the dev server out of `test_assets/`, which is gitignored and unpublished. See
91
+ * `test_assets/README.md`.
92
+ *
93
+ * @type {string}
94
+ */
95
+ const CHARACTER_URL = '/test_assets/Michelle.glb';
96
+
97
+ /**
98
+ * How far apart characters stand, in character heights.
99
+ *
100
+ * @type {number}
101
+ */
102
+ const SPACING = 1.1;
103
+
104
+ /**
105
+ * How the skin is driven. The page's whole point is that these are three different questions.
106
+ *
107
+ * @enum {string}
108
+ */
109
+ const SkinMode = {
110
+ /** no skin registered — the clone is never deformed */
111
+ Off: 'off',
112
+ /** skin registered, no clip — the pose the CPU can predict */
113
+ Bind: 'bind',
114
+ /** skin registered, the file's dance bound and playing */
115
+ Animated: 'animated',
116
+ };
117
+
118
+ /**
119
+ * The manager a model load needs, registered the way the game's graphics profile registers it.
120
+ *
121
+ * Not {@link make_playground_asset_manager}: that one is the two-loader subset `load_gltf` needs
122
+ * when a page calls it by hand. Going through {@link load_model_scene_bundle} means the *model* is
123
+ * an asset too, which is the thing this page wants to be on.
124
+ *
125
+ * @returns {Promise<AssetManager>}
126
+ */
127
+ async function make_asset_manager() {
128
+ const manager = new AssetManager({
129
+ context: {},
130
+ executor: new ConcurrentExecutor(),
131
+ });
132
+
133
+ // One instance for both glTF types, as `addModelLoaders` does — the loader holds no per-type
134
+ // state, and the two types are `.glb` and `.gltf` telling themselves apart.
135
+ const models = new GLTFSceneBundleAssetLoader();
136
+
137
+ await manager.registerLoader(GameAssetType.ModelGLTF, models);
138
+ await manager.registerLoader(GameAssetType.ModelGLTF_JSON, models);
139
+
140
+ // the container's `.bin` buffers, and its images — `load_gltf` asks for both back through the
141
+ // manager rather than fetching them itself
142
+ await manager.registerLoader(GameAssetType.ArrayBuffer, new ArrayBufferLoader());
143
+ await manager.registerLoader(GameAssetType.ImageBitmap, new ImageBitmapAssetLoader());
144
+
145
+ manager.startup();
146
+
147
+ return manager;
148
+ }
149
+
150
+ /**
151
+ * Where the `index`-th of `count` characters stands: a square-ish grid centred on the origin.
152
+ *
153
+ * @param {number} index
154
+ * @param {number} count
155
+ * @param {number} spacing
156
+ * @returns {{x: number, z: number}}
157
+ */
158
+ function grid_position(index, count, spacing) {
159
+ const columns = Math.ceil(Math.sqrt(count));
160
+ const rows = Math.ceil(count / columns);
161
+
162
+ return {
163
+ x: ((index % columns) - (columns - 1) / 2) * spacing,
164
+ z: (Math.floor(index / columns) - (rows - 1) / 2) * spacing,
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Stand one instantiated character at `(x, 0, z)`.
170
+ *
171
+ * The offset goes on a fresh parent above the model's own roots, and on that parent's **local**
172
+ * transform. Both halves of that matter and neither is obvious; `place_character` in
173
+ * `skinned_blas_refit` carries the full argument, which is worth reading once. The short form: a
174
+ * skinned mesh's own node transform cancels exactly out of the vertex the renderer draws, so a
175
+ * skinned character is placed by its *joints* and only an ancestor the joints actually sit under
176
+ * moves them — and `transform_global` is derived, so a placement written there is discarded by the
177
+ * next walk that recomputes it.
178
+ *
179
+ * @param {Scene} scene
180
+ * @param {Node3D[]} roots the instance's roots, not yet parented
181
+ * @param {number} x
182
+ * @param {number} z
183
+ * @returns {Node3D} the placement node
184
+ */
185
+ function place_roots(scene, roots, x, z) {
186
+ const placement = new Node3D();
187
+
188
+ placement.name = `placement(${x}, ${z})`;
189
+
190
+ // `Transform64` keeps its matrix by hand: the write is only real after `updateMatrix`.
191
+ placement.transform_local.setTranslation(x, 0, z);
192
+ placement.transform_local.updateMatrix();
193
+
194
+ placement.addChildren(roots);
195
+
196
+ placement.updateMatrices();
197
+
198
+ // `updateMatrices` walks pre-order and a glTF character's joint root is the mesh node's
199
+ // sibling, so the bounds a SkinnedMesh refreshes mid-walk read joints the walk has not reached.
200
+ // This second pass reads a settled skeleton.
201
+ placement.traverse(node => {
202
+ if (node.isMesh === true) {
203
+ node.updateBoundsBasic();
204
+ }
205
+ });
206
+
207
+ scene.add(placement);
208
+
209
+ return placement;
210
+ }
211
+
212
+ /**
213
+ * Every {@link SkinnedMesh} an instance's skins deform.
214
+ *
215
+ * Off the skins rather than by walking the roots: a skin already lists exactly the meshes that bind
216
+ * to it, which is what `register_skin` uses.
217
+ *
218
+ * @param {SceneBundleInstance} instance
219
+ * @returns {SkinnedMesh[]}
220
+ */
221
+ function skinned_meshes_of(instance) {
222
+ const meshes = new Set();
223
+
224
+ for (const skin of instance.skins) {
225
+ for (const mesh of skin.meshes) {
226
+ meshes.add(mesh);
227
+ }
228
+ }
229
+
230
+ return [...meshes];
231
+ }
232
+
233
+ /**
234
+ * The world-space box the renderer culls and traces this mesh by — the `meshes` table row, which
235
+ * the skinning pass's bounds-refresh chain rewrites every frame from the vertices it just moved.
236
+ *
237
+ * Not `mesh.bounding_box`: that is the CPU box of the *source* geometry under the mesh transform,
238
+ * and for a character whose node carries a centimetre scale it is a box a couple of centimetres
239
+ * across. Framing a camera on that puts the camera inside the character.
240
+ *
241
+ * @param {GPUSceneContext} scene_context
242
+ * @param {Mesh} mesh
243
+ * @returns {Promise<{low: Vector3, high: Vector3}>}
244
+ */
245
+ async function world_bounds_of(scene_context, mesh) {
246
+ const row = scene_context.mesh_row_of(mesh);
247
+
248
+ const record = await scene_context.scene_database.debug_table_read_element('meshes', row);
249
+
250
+ const box = record.bounding_box;
251
+
252
+ return {
253
+ low: new Vector3(box[0], box[1], box[2]),
254
+ high: new Vector3(box[3], box[4], box[5]),
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Stand the page up.
260
+ *
261
+ * @returns {Promise<void>}
262
+ */
263
+ export async function start() {
264
+
265
+ const element = id => document.getElementById(id);
266
+
267
+ const status_element = element('status');
268
+ const stats_element = element('stats');
269
+
270
+ /**
271
+ * @param {string} text
272
+ * @param {string} [kind]
273
+ */
274
+ function set_status(text, kind = '') {
275
+ status_element.textContent = text;
276
+ status_element.className = kind;
277
+ }
278
+
279
+ const canvas = element('view');
280
+
281
+ const context = canvas.getContext('webgpu');
282
+
283
+ if (context === null) {
284
+ set_status(
285
+ 'no WebGPU context — this browser has none, or the page is not a secure context',
286
+ 'fail'
287
+ );
288
+ return;
289
+ }
290
+
291
+ // ---- what the url asks for ------------------------------------------------------------------
292
+
293
+ const parameters = new URLSearchParams(location.search);
294
+
295
+ const count = Math.min(64, Math.max(1, Number.parseInt(parameters.get('count'), 10) || 1));
296
+
297
+ const skin_mode = Object.values(SkinMode).includes(parameters.get('skin'))
298
+ ? parameters.get('skin')
299
+ : SkinMode.Animated;
300
+
301
+ const tracing = parameters.get('trace') === '1';
302
+
303
+ element('character_count').value = String(count);
304
+ element('skin_mode').value = skin_mode;
305
+ element('path_tracing').checked = tracing;
306
+
307
+ // A Scene has no teardown, so changing any of these reloads the page rather than emptying one
308
+ // in place. Every switch is in the url for the same reason a repro is a url: it can be sent.
309
+ function reload_with(changes) {
310
+ const next = new URLSearchParams(location.search);
311
+
312
+ for (const [key, value] of Object.entries(changes)) {
313
+ next.set(key, value);
314
+ }
315
+
316
+ location.search = next.toString();
317
+ }
318
+
319
+ element('respawn').addEventListener('click', () => reload_with({
320
+ count: element('character_count').value,
321
+ skin: element('skin_mode').value,
322
+ }));
323
+
324
+ element('skin_mode').addEventListener('change', () => reload_with({
325
+ skin: element('skin_mode').value,
326
+ }));
327
+
328
+ // ---- the renderer ---------------------------------------------------------------------------
329
+
330
+ const renderer = new Renderer();
331
+
332
+ set_status('acquiring a device…');
333
+
334
+ await renderer.initialize({ context });
335
+
336
+ if (!renderer.graphics.device.features.has('subgroups')) {
337
+ set_status('device has no `subgroups`, which this engine takes as a baseline', 'warn');
338
+ }
339
+
340
+ const camera = new PerspectiveCamera();
341
+
342
+ camera.near = 0.01;
343
+ camera.far = 200;
344
+ camera.fov = 50 * DEG_TO_RAD;
345
+ camera.aspect = renderer.aspect_ratio;
346
+
347
+ const scene = new Scene();
348
+
349
+ // document.body rather than the canvas: the controller starts a keyboard device, and only a
350
+ // focusable element receives key events — body carries tabindex for exactly that.
351
+ const controller = new OrbitalCameraController(camera, document.body);
352
+
353
+ // Rasterized by default. It is the simpler of the two readings — it reads the deformed vertices
354
+ // directly, with no tree in the way — so it is the one to establish the answer on first.
355
+ renderer.feature_path_tracing_enabled = tracing;
356
+ renderer.feature_taa_enabled = false;
357
+
358
+ element('path_tracing').addEventListener('change', event => {
359
+ renderer.feature_path_tracing_enabled = event.target.checked;
360
+ renderer.indicate_view_change();
361
+ });
362
+
363
+ globalThis.renderer = renderer;
364
+ globalThis.scene = scene;
365
+ globalThis.camera = camera;
366
+ globalThis.controller = controller;
367
+
368
+ // ---- the cast -------------------------------------------------------------------------------
369
+
370
+ set_status(`loading ${CHARACTER_URL} …`);
371
+
372
+ const scene_context = renderer.scenes.obtain(scene);
373
+
374
+ /** @type {SkinnedMesh[]} */
375
+ const characters = [];
376
+
377
+ try {
378
+ const asset_manager = await make_asset_manager();
379
+
380
+ // One load, whatever the count: the asset hands out the model itself, and a placement's
381
+ // private copy is `instantiate_scene_bundle`.
382
+ const bundle = await load_model_scene_bundle(asset_manager, CHARACTER_URL);
383
+
384
+ const animations = scene_context.animation_manager;
385
+
386
+ for (let i = 0; i < count; i++) {
387
+ const instance = instantiate_scene_bundle(bundle);
388
+
389
+ const meshes = skinned_meshes_of(instance);
390
+
391
+ if (meshes.length === 0) {
392
+ throw new Error(`${CHARACTER_URL} has no skinned mesh`);
393
+ }
394
+
395
+ const { x, z } = grid_position(i, count, SPACING);
396
+
397
+ place_roots(scene, instance.roots, x, z);
398
+
399
+ if (skin_mode !== SkinMode.Off) {
400
+ for (const skin of instance.skins) {
401
+ animations.register_skin(skin);
402
+ }
403
+ }
404
+
405
+ if (skin_mode === SkinMode.Animated) {
406
+ // One clip, looping, at full weight. The pose accumulator sums every *bound* clip
407
+ // whatever its weight, so binding the T-pose alongside the dance would blend the
408
+ // two — only what should play is registered.
409
+ const clip = instance.clips.find(candidate => /samba|dance/i.test(candidate.name ?? ''))
410
+ ?? instance.clips[0];
411
+
412
+ if (clip !== undefined) {
413
+ const clip_id = animations.register_clip(clip);
414
+
415
+ animations.set_playback_weight(clip_id, 1);
416
+ animations.set_flags(clip_id, AnimationClipFlags.Loop);
417
+ animations.start(clip_id);
418
+ }
419
+ }
420
+
421
+ characters.push(...meshes);
422
+ }
423
+ } catch (error) {
424
+ set_status(
425
+ `could not load the character (${error.message}). Put it under test_assets/ — `
426
+ + `see test_assets/README.md.`,
427
+ 'fail'
428
+ );
429
+ console.error(error);
430
+ return;
431
+ }
432
+
433
+ if (!scene.lights.elements.some(light => light.isDirectionalLight)) {
434
+ // a glTF character carries no light of its own
435
+ scene.lights.add(make_sunlight({ intensity: 6 }));
436
+ }
437
+
438
+ // ---- frames enough for the box to be worth framing on ----------------------------------------
439
+
440
+ set_status('first frames — uploading clones and their trees …');
441
+
442
+ for (let i = 0; i < 8; i++) {
443
+ renderer.render(camera, scene, 1 / 60);
444
+
445
+ await renderer.graphics.device.queue.onSubmittedWorkDone();
446
+ }
447
+
448
+ let low = null;
449
+ let high = null;
450
+
451
+ for (const character of characters) {
452
+ const bounds = await world_bounds_of(scene_context, character);
453
+
454
+ low = low === null ? bounds.low : new Vector3(
455
+ Math.min(low.x, bounds.low.x), Math.min(low.y, bounds.low.y), Math.min(low.z, bounds.low.z)
456
+ );
457
+
458
+ high = high === null ? bounds.high : new Vector3(
459
+ Math.max(high.x, bounds.high.x), Math.max(high.y, bounds.high.y), Math.max(high.z, bounds.high.z)
460
+ );
461
+ }
462
+
463
+ const span = new Vector3(high.x - low.x, high.y - low.y, high.z - low.z);
464
+
465
+ const radius = Math.hypot(span.x, span.y, span.z) * 0.5;
466
+
467
+ camera_frame_sphere(
468
+ controller,
469
+ radius,
470
+ new Vector3((low.x + high.x) / 2, (low.y + high.y) / 2, (low.z + high.z) / 2)
471
+ );
472
+
473
+ controller.movement_speed_scale = radius * 0.2;
474
+
475
+ camera.near = radius * 0.01;
476
+ camera.far = radius * 400;
477
+
478
+ console.log(
479
+ `published box: low=(${low.x.toFixed(4)}, ${low.y.toFixed(4)}, ${low.z.toFixed(4)}) `
480
+ + `high=(${high.x.toFixed(4)}, ${high.y.toFixed(4)}, ${high.z.toFixed(4)}) — `
481
+ + `span ${span.x.toFixed(4)} × ${span.y.toFixed(4)} × ${span.z.toFixed(4)} m`
482
+ );
483
+
484
+ load_default_environment(scene).then(applied => {
485
+ if (applied) {
486
+ renderer.indicate_view_change();
487
+ }
488
+ });
489
+
490
+ // the controller's keyboard device listens on body, and only a focused element gets key events
491
+ document.body.focus();
492
+
493
+ globalThis.characters = characters;
494
+
495
+ // ---- the console half ------------------------------------------------------------------------
496
+
497
+ let animating = skin_mode === SkinMode.Animated;
498
+
499
+ /**
500
+ * The clone's deformed vertices, against the answer the CPU knows for the bind pose.
501
+ *
502
+ * Only meaningful under `?skin=bind`: with a clip playing there is no CPU-side answer to compare
503
+ * against, and with no skin registered there is nothing deforming the clone. It runs in the
504
+ * other two modes anyway rather than refusing — under `?skin=off` a pass says the clone's copy
505
+ * and address patch are right, which is worth knowing on its own.
506
+ */
507
+ globalThis.diagnose = async () => {
508
+ const { verify_clone_vertices } = await import('./verify_clone_vertices.js');
509
+
510
+ const was_animating = animating;
511
+
512
+ animating = false;
513
+
514
+ try {
515
+ return await verify_clone_vertices({ renderer, scene, mesh: characters[0] });
516
+ } finally {
517
+ animating = was_animating;
518
+ }
519
+ };
520
+
521
+ set_status(
522
+ `loaded — ${count} × ${CHARACTER_URL}, skin=${skin_mode}. `
523
+ + `\`await diagnose()\` in the console.`,
524
+ 'pass'
525
+ );
526
+
527
+ renderer.indicate_view_change();
528
+
529
+ // ---- frame loop --------------------------------------------------------------------------
530
+
531
+ element('animation').disabled = skin_mode !== SkinMode.Animated;
532
+
533
+ if (skin_mode !== SkinMode.Animated) {
534
+ // there is no clip to freeze; saying so beats a live-looking button that does nothing
535
+ element('animation').textContent = 'No clip playing';
536
+ }
537
+
538
+ element('animation').addEventListener('click', () => {
539
+ animating = !animating;
540
+
541
+ element('animation').textContent = animating ? 'Freeze pose' : 'Resume dance';
542
+
543
+ renderer.path_tracer.clear_history = true;
544
+ renderer.indicate_view_change();
545
+ });
546
+
547
+ const FRAME_RATE_LIMIT = 60;
548
+
549
+ create_frame_loop(time_delta_s => {
550
+ const width = window.innerWidth;
551
+ const height = window.innerHeight;
552
+
553
+ renderer.resize(width, height);
554
+ renderer.pixel_ratio = window.devicePixelRatio;
555
+ camera.aspect = width / height;
556
+
557
+ controller.update();
558
+
559
+ // A frozen pose is a still scene, which is the only condition under which the tracer's
560
+ // accumulator converges — so time stops rather than the frame loop stopping.
561
+ renderer.render(camera, scene, animating ? time_delta_s : 0);
562
+
563
+ stats_element.textContent =
564
+ `${count} × skin=${skin_mode}`
565
+ + `${renderer.feature_path_tracing_enabled ? ' · path traced' : ' · rasterized'}`;
566
+ }, FRAME_RATE_LIMIT);
567
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The clone's deformed vertices, against `inverse(mesh.global) × rest`.
3
+ *
4
+ * Reads the GPU's own answer to "where does this geometry id's data live" rather than reconstructing
5
+ * it from the host records: the geometry metadata row, then the clone's meshlet metadata rows, then
6
+ * the data those rows address. That is the same chain the rasterizer and the intersector walk, so a
7
+ * clone whose *addressing* is wrong is caught here rather than being read around.
8
+ *
9
+ * @param {object} options
10
+ * @param {Renderer} options.renderer
11
+ * @param {Scene} options.scene
12
+ * @param {SkinnedMesh} options.mesh
13
+ * @param {number} [options.tolerance] in mesh-local units. Since `register_skin` puts the mesh on
14
+ * the skin's bind transform, mesh-local *is* world scale — a metre is a metre here, and at
15
+ * bind the clone should hold the source's own positions. `1e-4` is f32 headroom over a
16
+ * chain — a matrix product, a quaternion conversion and a screw motion, all at single
17
+ * precision — against a smallest real violation of `1.6`, so there are four orders between
18
+ * the two. It was `5e-3` while `dual_quat_from_m4` amplified a near-identity matrix's error
19
+ * by its square root; `SKINNING_FRAME_PLAN.md` §9 took that out, and the same measurement on
20
+ * the CPU now reads zero at bind.
21
+ * @returns {Promise<object>}
22
+ */
23
+ export function verify_clone_vertices({ renderer, scene, mesh, tolerance }: {
24
+ renderer: Renderer;
25
+ scene: Scene;
26
+ mesh: SkinnedMesh;
27
+ tolerance?: number;
28
+ }): Promise<object>;
29
+ //# sourceMappingURL=verify_clone_vertices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify_clone_vertices.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js"],"names":[],"mappings":"AAyJA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IAb6B,QAAQ;IACX,KAAK;IACC,IAAI;IACR,SAAS,GAA1B,MAAM;IAQJ,QAAQ,MAAM,CAAC,CA8P3B"}