@woosh/meep-engine 3.14.3 → 3.14.4

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