@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.
- package/package.json +1 -1
- package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +733 -726
- package/src/engine/network/NetworkSession.d.ts +24 -4
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +28 -4
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +96 -7
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +392 -68
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +105 -13
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +306 -59
- package/src/engine/network/replication/Replicator.d.ts +43 -7
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +309 -111
- package/src/engine/network/sim/ActionLog.d.ts +23 -3
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +26 -3
- package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
- package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
- package/src/engine/network/sim/ActionRecordCursor.js +120 -0
- package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
- package/src/engine/network/sim/RewindEngine.js +13 -29
- package/src/engine/network/sim/SimAction.d.ts +15 -0
- package/src/engine/network/sim/SimAction.d.ts.map +1 -1
- package/src/engine/network/sim/SimAction.js +15 -0
- package/src/engine/network/sim/SimActionExecutor.d.ts +51 -2
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +74 -7
- package/src/engine/network/transport/Channel.d.ts +1 -1
- package/src/engine/network/transport/Channel.js +1 -1
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +4 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +6 -0
- package/src/shade/playground/particle_system/README.md +98 -0
- package/src/shade/playground/particle_system/index.html +124 -0
- package/src/shade/playground/particle_system/main.d.ts +2 -0
- package/src/shade/playground/particle_system/main.d.ts.map +1 -0
- package/src/shade/playground/particle_system/main.js +180 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts +52 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -0
- package/src/shade/playground/particle_system/particle_scene.js +553 -0
- package/src/shade/playground/skinned_mesh_soup/README.md +143 -0
- package/src/shade/playground/skinned_mesh_soup/index.html +202 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts +7 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/main.js +567 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +29 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +428 -0
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
- package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
- package/src/shade/renderer/particles/DESIGN.md +5 -5
- package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +1 -1
- package/src/shade/renderer/particles/cull/chunk_particle_cull.js +3 -1
- package/src/shade/renderer/particles/data/chunk_particle_emitter.js +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts +89 -0
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -0
- package/src/shade/renderer/particles/isa/InstructionStream.js +162 -0
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +1 -48
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/ParticleAssembler.js +25 -114
- package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
- package/src/shade/renderer/particles/particle_prototype.js +181 -180
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
- package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +4 -2
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +4 -4
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +7 -5
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +5 -2
- package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_render.js +9 -2
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -1
- package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
- package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
- package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts +0 -2
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts.map +0 -1
- 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"}
|