@woosh/meep-engine 3.14.4 → 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 (89) 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/network/orchestrator/NetworkPeer.d.ts +56 -8
  6. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  7. package/src/engine/network/orchestrator/NetworkPeer.js +263 -67
  8. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
  9. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  10. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
  11. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +13 -7
  12. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  13. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +103 -138
  14. package/src/engine/network/replication/Replicator.d.ts +29 -6
  15. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  16. package/src/engine/network/replication/Replicator.js +265 -108
  17. package/src/engine/network/sim/ActionLog.d.ts +2 -0
  18. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  19. package/src/engine/network/sim/ActionLog.js +2 -0
  20. package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
  21. package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
  22. package/src/engine/network/sim/ActionRecordCursor.js +120 -0
  23. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  24. package/src/engine/network/sim/RewindEngine.js +13 -29
  25. package/src/engine/network/sim/SimActionExecutor.d.ts +2 -3
  26. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  27. package/src/engine/network/sim/SimActionExecutor.js +10 -11
  28. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  29. package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -1
  30. package/src/shade/playground/particle_system/particle_scene.js +38 -37
  31. package/src/shade/playground/skinned_mesh_soup/README.md +39 -18
  32. package/src/shade/playground/skinned_mesh_soup/index.html +9 -6
  33. package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -1
  34. package/src/shade/playground/skinned_mesh_soup/main.js +16 -13
  35. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +8 -2
  36. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
  37. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +23 -4
  38. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  39. package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
  40. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
  41. package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
  42. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
  43. package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
  44. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
  45. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
  46. package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
  47. package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
  48. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
  49. package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
  50. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
  51. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
  52. package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
  53. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
  54. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
  55. package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
  56. package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
  57. package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
  58. package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
  59. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
  60. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
  61. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
  62. package/src/shade/renderer/particles/DESIGN.md +5 -5
  63. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
  64. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
  65. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
  66. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
  67. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  68. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
  69. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
  70. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  71. package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
  72. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
  73. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
  74. package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
  75. package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
  76. package/src/shade/renderer/particles/particle_prototype.js +181 -180
  77. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
  78. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  79. package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
  80. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
  81. package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
  82. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
  83. package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
  84. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
  85. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
  86. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
  87. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
  88. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
  89. package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
@@ -1,180 +1,181 @@
1
- import { EMITTER_BLEND, EMITTER_PROJECTION } from "./data/PARTICLE_EMITTER_LAYOUT.js";
2
- import { ParticleGraph } from "./graph/ParticleGraph.js";
3
- import { VM_BUILTIN, VM_RANDOM_MODE } from "./isa/ParticleVMISA.js";
4
- import { ParticleLayout } from "./layout/ParticleLayout.js";
5
- import { create_particle_effect } from "./runtime/create_particle_effect.js";
6
-
7
- /**
8
- * Worked examples of authoring particle effects with the node graph + wiring them into a
9
- * {@link GPUParticleSystem}. Demonstrates: distinct per-particle lifecycle (no built-in age the
10
- * effect declares it), AnimationCurve sampling over life, flipbook frames, random emission,
11
- * additive vs alpha blending, projection modes, soft depth and lighting toggles.
12
- *
13
- * `curves` is a `GPUAnimationManager` (or anything with `register_curve(AnimationCurve) -> handle`);
14
- * `size_curve` / `alpha_curve` are meep `AnimationCurve`s. Both are optional — passing `undefined`
15
- * yields effects that use constant size/alpha instead (curve handle 0, evaluated as 0 by the
16
- * disabled provider), which is handy for the headless tests.
17
- */
18
-
19
- /**
20
- * A fire effect: buoyant, turbulent, additive-blended camera-facing sprites whose size and opacity
21
- * follow curves over their lifetime and whose flipbook advances with age.
22
- *
23
- * @param {object} [options]
24
- * @param {number} [options.size_curve] curve handle for size(normalizedAge)
25
- * @param {number} [options.alpha_curve] curve handle for alpha(normalizedAge)
26
- * @param {number} [options.flipbook_frames]
27
- * @returns {{layout: ParticleLayout, effect: object, emitter: object}}
28
- */
29
- export function fire_effect({ size_curve, alpha_curve, flipbook_frames = 16 } = {}) {
30
- const layout = new ParticleLayout([
31
- { name: "position", components: 3 },
32
- { name: "velocity", components: 3 },
33
- { name: "age", components: 1 },
34
- { name: "lifetime", components: 1 },
35
- { name: "size", components: 1 },
36
- { name: "color", components: 4 },
37
- { name: "frame", components: 1 },
38
- ]);
39
-
40
- // --- SPAWN: seed each particle's state ---
41
- const spawn = new ParticleGraph();
42
- {
43
- // position = emitter position + a small disk of jitter on the ground
44
- const ep = spawn.add("builtin", { id: VM_BUILTIN.EMITTER_POSITION });
45
- const disk = spawn.add("random", { mode: VM_RANDOM_MODE.UNIT_DISK });
46
- const jitter = spawn.add("scale"); spawn.wire(jitter, "v", disk); spawn.wire(jitter, "s", [0.2]);
47
- const p0 = spawn.add("add"); spawn.wire(p0, "a", ep); spawn.wire(p0, "b", jitter);
48
- spawn.wire(spawn.add("setAttribute", { name: "position" }), "value", p0);
49
-
50
- // velocity = upward buoyancy + signed turbulence
51
- const up = spawn.add("const", { value: [0, 2.0, 0, 0] });
52
- const turb = spawn.add("random", { mode: VM_RANDOM_MODE.UNIFORM_SIGNED });
53
- const turbS = spawn.add("scale"); spawn.wire(turbS, "v", turb); spawn.wire(turbS, "s", [0.5]);
54
- const v0 = spawn.add("add"); spawn.wire(v0, "a", up); spawn.wire(v0, "b", turbS);
55
- spawn.wire(spawn.add("setAttribute", { name: "velocity" }), "value", v0);
56
-
57
- spawn.wire(spawn.add("setAttribute", { name: "age" }), "value", [0]);
58
- // lifetime 1.2..1.8s
59
- const lr = spawn.add("random", { mode: VM_RANDOM_MODE.UNIFORM });
60
- const ls = spawn.add("mad"); spawn.wire(ls, "a", lr); spawn.wire(ls, "b", [0.6]); spawn.wire(ls, "c", [1.2]);
61
- spawn.wire(spawn.add("setAttribute", { name: "lifetime" }), "value", ls);
62
-
63
- spawn.wire(spawn.add("setAttribute", { name: "size" }), "value", [0.4]);
64
- spawn.wire(spawn.add("setAttribute", { name: "color" }), "value", [1.0, 0.6, 0.2, 1.0]);
65
- spawn.wire(spawn.add("setAttribute", { name: "frame" }), "value", [0]);
66
- }
67
-
68
- // --- UPDATE: advance + destroy ---
69
- const update = new ParticleGraph();
70
- {
71
- const dt = update.add("builtin", { id: VM_BUILTIN.DELTA_TIME });
72
-
73
- // age += dt
74
- const age = update.add("attribute", { name: "age" });
75
- const newAge = update.add("add"); update.wire(newAge, "a", age); update.wire(newAge, "b", dt);
76
- update.wire(update.add("setAttribute", { name: "age" }), "value", newAge);
77
-
78
- // normalizedAge = age / lifetime
79
- const lifetime = update.add("attribute", { name: "lifetime" });
80
- const nAge = update.add("div"); update.wire(nAge, "a", newAge); update.wire(nAge, "b", lifetime);
81
-
82
- // kill when normalizedAge >= 1
83
- const dead = update.add("compare", { op: "ge" }); update.wire(dead, "a", nAge); update.wire(dead, "b", [1]);
84
- update.wire(update.add("kill"), "condition", dead);
85
-
86
- // buoyant rise + drift: velocity += (0, 1, 0)*dt ; position += velocity*dt
87
- const vel = update.add("attribute", { name: "velocity" });
88
- const buoy = update.add("const", { value: [0, 1.0, 0, 0] });
89
- const buoyDt = update.add("scale"); update.wire(buoyDt, "v", buoy); update.wire(buoyDt, "s", dt);
90
- const nVel = update.add("add"); update.wire(nVel, "a", vel); update.wire(nVel, "b", buoyDt);
91
- update.wire(update.add("setAttribute", { name: "velocity" }), "value", nVel);
92
- const pos = update.add("attribute", { name: "position" });
93
- const velDt = update.add("scale"); update.wire(velDt, "v", nVel); update.wire(velDt, "s", dt);
94
- const nPos = update.add("add"); update.wire(nPos, "a", pos); update.wire(nPos, "b", velDt);
95
- update.wire(update.add("setAttribute", { name: "position" }), "value", nPos);
96
-
97
- // size + alpha from curves over life (or constants if no curve provided)
98
- if (size_curve !== undefined) {
99
- const sc = update.add("curve", { handle: size_curve }); update.wire(sc, "t", nAge);
100
- update.wire(update.add("setAttribute", { name: "size" }), "value", sc);
101
- }
102
- const color = update.add("attribute", { name: "color" });
103
- if (alpha_curve !== undefined) {
104
- const ac = update.add("curve", { handle: alpha_curve }); update.wire(ac, "t", nAge);
105
- // color.rgb unchanged, color.a = ac : rebuild via select on a swizzle mask is overkill;
106
- // simplest: multiply color by (1,1,1,ac_normalized) approximate by scaling alpha only.
107
- const alphaVec = update.add("mul");
108
- update.wire(alphaVec, "a", color);
109
- update.wire(alphaVec, "b", ac); // broadcasts ac to all lanes (rgb also fades — a warm ember look)
110
- update.wire(update.add("setAttribute", { name: "color" }), "value", alphaVec);
111
- }
112
-
113
- // flipbook frame = floor(normalizedAge * frames)
114
- const framesN = update.add("const", { value: [flipbook_frames, 0, 0, 0] });
115
- const fscaled = update.add("mul"); update.wire(fscaled, "a", nAge); update.wire(fscaled, "b", framesN);
116
- const fframe = update.add("floor"); update.wire(fframe, "x", fscaled);
117
- update.wire(update.add("setAttribute", { name: "frame" }), "value", fframe);
118
- }
119
-
120
- const effect = create_particle_effect({ layout, spawn, update });
121
-
122
- const emitter = {
123
- effect,
124
- spawn_rate: 120,
125
- flags: { blend: EMITTER_BLEND.ADDITIVE, projection: EMITTER_PROJECTION.BILLBOARD },
126
- render: { position: "position", size: "size", color: "color", frame: "frame" },
127
- bounds: { center: [0, 1.5, 0], radius: 2.5 },
128
- // A 4x4 flipbook packed as the whole atlas here; a real atlas would use a sub-rect.
129
- atlas_region: [0, 0, 1, 1],
130
- flipbook: [4, 4],
131
- };
132
-
133
- return { layout, effect, emitter };
134
- }
135
-
136
- /**
137
- * A smoke effect: slow, expanding, alpha-blended, lit, soft-depth billboards. Same authoring
138
- * surface as fire; the differences are all data (blend/lighting/soft flags, rates, colours).
139
- * @returns {{layout: ParticleLayout, effect: object, emitter: object}}
140
- */
141
- export function smoke_effect() {
142
- const { layout, effect } = fire_effect({ flipbook_frames: 8 });
143
- const emitter = {
144
- effect,
145
- spawn_rate: 25,
146
- flags: { blend: EMITTER_BLEND.ALPHA, projection: EMITTER_PROJECTION.BILLBOARD, soft_depth: true, lighting: true, sort: true },
147
- render: { position: "position", size: "size", color: "color", frame: "frame" },
148
- bounds: { center: [0, 3, 0], radius: 5 },
149
- atlas_region: [0, 0, 1, 1],
150
- flipbook: [1, 1],
151
- };
152
- return { layout, effect, emitter };
153
- }
154
-
155
- /**
156
- * Register a fire + smoke plume on a particle system, optionally attaching the fire to a bone.
157
- *
158
- * The per-frame loop the integrator runs:
159
- * ```js
160
- * const spawn_requests = system.update(dt, { is_visible }); // CPU: bone transforms + spawn counts
161
- * system.uploadFrame(spawn_requests); // upload emitters (if dirty) + requests
162
- * graph_particles({ graph, gpu: system.gpuHandles(), frame }); // reset -> emit -> simulate -> indirect
163
- * // caller appends the depth sort + billboard render into its own graph
164
- * ```
165
- *
166
- * @param {import("./GPUParticleSystem.js").GPUParticleSystem} system
167
- * @param {object} [options]
168
- * @param {object} [options.bone] a Node3D to attach the fire emitter to
169
- * @returns {{fire: number, smoke: number}} emitter indices
170
- */
171
- export function register_campfire(system, { bone } = {}) {
172
- const fire = system.addEmitter(fire_effect().emitter);
173
- const smoke = system.addEmitter(smoke_effect().emitter);
174
- if (bone !== undefined) {
175
- system.attachToNode(fire, bone);
176
- system.attachToNode(smoke, bone);
177
- }
178
- system.buildEmitterData();
179
- return { fire, smoke };
180
- }
1
+ import { EMITTER_BLEND, EMITTER_PROJECTION } from "./data/PARTICLE_EMITTER_LAYOUT.js";
2
+ import { NodeGraph } from "../../../core/model/node-graph/NodeGraph.js";
3
+ import { particle_node, particle_wire } from "./graph/particle_graph_authoring.js";
4
+ import { VM_BUILTIN, VM_RANDOM_MODE } from "./isa/ParticleVMISA.js";
5
+ import { ParticleLayout } from "./layout/ParticleLayout.js";
6
+ import { create_particle_effect } from "./runtime/create_particle_effect.js";
7
+
8
+ /**
9
+ * Worked examples of authoring particle effects with the node graph + wiring them into a
10
+ * {@link GPUParticleSystem}. Demonstrates: distinct per-particle lifecycle (no built-in age the
11
+ * effect declares it), AnimationCurve sampling over life, flipbook frames, random emission,
12
+ * additive vs alpha blending, projection modes, soft depth and lighting toggles.
13
+ *
14
+ * `curves` is a `GPUAnimationManager` (or anything with `register_curve(AnimationCurve) -> handle`);
15
+ * `size_curve` / `alpha_curve` are meep `AnimationCurve`s. Both are optional passing `undefined`
16
+ * yields effects that use constant size/alpha instead (curve handle 0, evaluated as 0 by the
17
+ * disabled provider), which is handy for the headless tests.
18
+ */
19
+
20
+ /**
21
+ * A fire effect: buoyant, turbulent, additive-blended camera-facing sprites whose size and opacity
22
+ * follow curves over their lifetime and whose flipbook advances with age.
23
+ *
24
+ * @param {object} [options]
25
+ * @param {number} [options.size_curve] curve handle for size(normalizedAge)
26
+ * @param {number} [options.alpha_curve] curve handle for alpha(normalizedAge)
27
+ * @param {number} [options.flipbook_frames]
28
+ * @returns {{layout: ParticleLayout, effect: object, emitter: object}}
29
+ */
30
+ export function fire_effect({ size_curve, alpha_curve, flipbook_frames = 16 } = {}) {
31
+ const layout = new ParticleLayout([
32
+ { name: "position", components: 3 },
33
+ { name: "velocity", components: 3 },
34
+ { name: "age", components: 1 },
35
+ { name: "lifetime", components: 1 },
36
+ { name: "size", components: 1 },
37
+ { name: "color", components: 4 },
38
+ { name: "frame", components: 1 },
39
+ ]);
40
+
41
+ // --- SPAWN: seed each particle's state ---
42
+ const spawn = new NodeGraph();
43
+ {
44
+ // position = emitter position + a small disk of jitter on the ground
45
+ const ep = particle_node(spawn, "builtin", { id: VM_BUILTIN.EMITTER_POSITION });
46
+ const disk = particle_node(spawn, "random", { mode: VM_RANDOM_MODE.UNIT_DISK });
47
+ const jitter = particle_node(spawn, "scale"); particle_wire(spawn, jitter, "v", disk); particle_wire(spawn, jitter, "s", [0.2]);
48
+ const p0 = particle_node(spawn, "add"); particle_wire(spawn, p0, "a", ep); particle_wire(spawn, p0, "b", jitter);
49
+ particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "position" }), "value", p0);
50
+
51
+ // velocity = upward buoyancy + signed turbulence
52
+ const up = particle_node(spawn, "const", { value: [0, 2.0, 0, 0] });
53
+ const turb = particle_node(spawn, "random", { mode: VM_RANDOM_MODE.UNIFORM_SIGNED });
54
+ const turbS = particle_node(spawn, "scale"); particle_wire(spawn, turbS, "v", turb); particle_wire(spawn, turbS, "s", [0.5]);
55
+ const v0 = particle_node(spawn, "add"); particle_wire(spawn, v0, "a", up); particle_wire(spawn, v0, "b", turbS);
56
+ particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "velocity" }), "value", v0);
57
+
58
+ particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "age" }), "value", [0]);
59
+ // lifetime 1.2..1.8s
60
+ const lr = particle_node(spawn, "random", { mode: VM_RANDOM_MODE.UNIFORM });
61
+ const ls = particle_node(spawn, "mad"); particle_wire(spawn, ls, "a", lr); particle_wire(spawn, ls, "b", [0.6]); particle_wire(spawn, ls, "c", [1.2]);
62
+ particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "lifetime" }), "value", ls);
63
+
64
+ particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "size" }), "value", [0.4]);
65
+ particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "color" }), "value", [1.0, 0.6, 0.2, 1.0]);
66
+ particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "frame" }), "value", [0]);
67
+ }
68
+
69
+ // --- UPDATE: advance + destroy ---
70
+ const update = new NodeGraph();
71
+ {
72
+ const dt = particle_node(update, "builtin", { id: VM_BUILTIN.DELTA_TIME });
73
+
74
+ // age += dt
75
+ const age = particle_node(update, "attribute", { name: "age" });
76
+ const newAge = particle_node(update, "add"); particle_wire(update, newAge, "a", age); particle_wire(update, newAge, "b", dt);
77
+ particle_wire(update, particle_node(update, "setAttribute", { name: "age" }), "value", newAge);
78
+
79
+ // normalizedAge = age / lifetime
80
+ const lifetime = particle_node(update, "attribute", { name: "lifetime" });
81
+ const nAge = particle_node(update, "div"); particle_wire(update, nAge, "a", newAge); particle_wire(update, nAge, "b", lifetime);
82
+
83
+ // kill when normalizedAge >= 1
84
+ const dead = particle_node(update, "compare", { op: "ge" }); particle_wire(update, dead, "a", nAge); particle_wire(update, dead, "b", [1]);
85
+ particle_wire(update, particle_node(update, "kill"), "condition", dead);
86
+
87
+ // buoyant rise + drift: velocity += (0, 1, 0)*dt ; position += velocity*dt
88
+ const vel = particle_node(update, "attribute", { name: "velocity" });
89
+ const buoy = particle_node(update, "const", { value: [0, 1.0, 0, 0] });
90
+ const buoyDt = particle_node(update, "scale"); particle_wire(update, buoyDt, "v", buoy); particle_wire(update, buoyDt, "s", dt);
91
+ const nVel = particle_node(update, "add"); particle_wire(update, nVel, "a", vel); particle_wire(update, nVel, "b", buoyDt);
92
+ particle_wire(update, particle_node(update, "setAttribute", { name: "velocity" }), "value", nVel);
93
+ const pos = particle_node(update, "attribute", { name: "position" });
94
+ const velDt = particle_node(update, "scale"); particle_wire(update, velDt, "v", nVel); particle_wire(update, velDt, "s", dt);
95
+ const nPos = particle_node(update, "add"); particle_wire(update, nPos, "a", pos); particle_wire(update, nPos, "b", velDt);
96
+ particle_wire(update, particle_node(update, "setAttribute", { name: "position" }), "value", nPos);
97
+
98
+ // size + alpha from curves over life (or constants if no curve provided)
99
+ if (size_curve !== undefined) {
100
+ const sc = particle_node(update, "curve", { handle: size_curve }); particle_wire(update, sc, "t", nAge);
101
+ particle_wire(update, particle_node(update, "setAttribute", { name: "size" }), "value", sc);
102
+ }
103
+ const color = particle_node(update, "attribute", { name: "color" });
104
+ if (alpha_curve !== undefined) {
105
+ const ac = particle_node(update, "curve", { handle: alpha_curve }); particle_wire(update, ac, "t", nAge);
106
+ // color.rgb unchanged, color.a = ac : rebuild via select on a swizzle mask is overkill;
107
+ // simplest: multiply color by (1,1,1,ac_normalized) — approximate by scaling alpha only.
108
+ const alphaVec = particle_node(update, "mul");
109
+ particle_wire(update, alphaVec, "a", color);
110
+ particle_wire(update, alphaVec, "b", ac); // broadcasts ac to all lanes (rgb also fades — a warm ember look)
111
+ particle_wire(update, particle_node(update, "setAttribute", { name: "color" }), "value", alphaVec);
112
+ }
113
+
114
+ // flipbook frame = floor(normalizedAge * frames)
115
+ const framesN = particle_node(update, "const", { value: [flipbook_frames, 0, 0, 0] });
116
+ const fscaled = particle_node(update, "mul"); particle_wire(update, fscaled, "a", nAge); particle_wire(update, fscaled, "b", framesN);
117
+ const fframe = particle_node(update, "floor"); particle_wire(update, fframe, "x", fscaled);
118
+ particle_wire(update, particle_node(update, "setAttribute", { name: "frame" }), "value", fframe);
119
+ }
120
+
121
+ const effect = create_particle_effect({ layout, spawn, update });
122
+
123
+ const emitter = {
124
+ effect,
125
+ spawn_rate: 120,
126
+ flags: { blend: EMITTER_BLEND.ADDITIVE, projection: EMITTER_PROJECTION.BILLBOARD },
127
+ render: { position: "position", size: "size", color: "color", frame: "frame" },
128
+ bounds: { center: [0, 1.5, 0], radius: 2.5 },
129
+ // A 4x4 flipbook packed as the whole atlas here; a real atlas would use a sub-rect.
130
+ atlas_region: [0, 0, 1, 1],
131
+ flipbook: [4, 4],
132
+ };
133
+
134
+ return { layout, effect, emitter };
135
+ }
136
+
137
+ /**
138
+ * A smoke effect: slow, expanding, alpha-blended, lit, soft-depth billboards. Same authoring
139
+ * surface as fire; the differences are all data (blend/lighting/soft flags, rates, colours).
140
+ * @returns {{layout: ParticleLayout, effect: object, emitter: object}}
141
+ */
142
+ export function smoke_effect() {
143
+ const { layout, effect } = fire_effect({ flipbook_frames: 8 });
144
+ const emitter = {
145
+ effect,
146
+ spawn_rate: 25,
147
+ flags: { blend: EMITTER_BLEND.ALPHA, projection: EMITTER_PROJECTION.BILLBOARD, soft_depth: true, lighting: true, sort: true },
148
+ render: { position: "position", size: "size", color: "color", frame: "frame" },
149
+ bounds: { center: [0, 3, 0], radius: 5 },
150
+ atlas_region: [0, 0, 1, 1],
151
+ flipbook: [1, 1],
152
+ };
153
+ return { layout, effect, emitter };
154
+ }
155
+
156
+ /**
157
+ * Register a fire + smoke plume on a particle system, optionally attaching the fire to a bone.
158
+ *
159
+ * The per-frame loop the integrator runs:
160
+ * ```js
161
+ * const spawn_requests = system.update(dt, { is_visible }); // CPU: bone transforms + spawn counts
162
+ * system.uploadFrame(spawn_requests); // upload emitters (if dirty) + requests
163
+ * graph_particles({ graph, gpu: system.gpuHandles(), frame }); // reset -> emit -> simulate -> indirect
164
+ * // caller appends the depth sort + billboard render into its own graph
165
+ * ```
166
+ *
167
+ * @param {import("./GPUParticleSystem.js").GPUParticleSystem} system
168
+ * @param {object} [options]
169
+ * @param {object} [options.bone] a Node3D to attach the fire emitter to
170
+ * @returns {{fire: number, smoke: number}} emitter indices
171
+ */
172
+ export function register_campfire(system, { bone } = {}) {
173
+ const fire = system.addEmitter(fire_effect().emitter);
174
+ const smoke = system.addEmitter(smoke_effect().emitter);
175
+ if (bone !== undefined) {
176
+ system.attachToNode(fire, bone);
177
+ system.attachToNode(smoke, bone);
178
+ }
179
+ system.buildEmitterData();
180
+ return { fire, smoke };
181
+ }
@@ -7,14 +7,14 @@
7
7
  *
8
8
  * @param {object} params
9
9
  * @param {import("../layout/ParticleLayout.js").ParticleLayout} params.layout
10
- * @param {import("../graph/ParticleGraph.js").ParticleGraph} [params.spawn]
11
- * @param {import("../graph/ParticleGraph.js").ParticleGraph} [params.update]
10
+ * @param {NodeGraph} [params.spawn]
11
+ * @param {NodeGraph} [params.update]
12
12
  * @returns {{ layout: import("../layout/ParticleLayout.js").ParticleLayout, program: import("../isa/ParticleProgram.js").ParticleProgram }}
13
13
  */
14
14
  export function create_particle_effect({ layout, spawn, update }: {
15
15
  layout: import("../layout/ParticleLayout.js").ParticleLayout;
16
- spawn?: import("../graph/ParticleGraph.js").ParticleGraph;
17
- update?: import("../graph/ParticleGraph.js").ParticleGraph;
16
+ spawn?: NodeGraph;
17
+ update?: NodeGraph;
18
18
  }): {
19
19
  layout: import("../layout/ParticleLayout.js").ParticleLayout;
20
20
  program: import("../isa/ParticleProgram.js").ParticleProgram;
@@ -1 +1 @@
1
- {"version":3,"file":"create_particle_effect.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/runtime/create_particle_effect.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH;IALwE,MAAM,EAAnE,OAAO,6BAA6B,EAAE,cAAc;IACO,KAAK,GAAhE,OAAO,2BAA2B,EAAE,aAAa;IACU,MAAM,GAAjE,OAAO,2BAA2B,EAAE,aAAa;;YACrC,OAAO,6BAA6B,EAAE,cAAc;aAAW,OAAO,2BAA2B,EAAE,eAAe;EAKxI"}
1
+ {"version":3,"file":"create_particle_effect.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/runtime/create_particle_effect.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH;IALwE,MAAM,EAAnE,OAAO,6BAA6B,EAAE,cAAc;IACjC,KAAK;IACL,MAAM;;YACb,OAAO,6BAA6B,EAAE,cAAc;aAAW,OAAO,2BAA2B,EAAE,eAAe;EAKxI"}
@@ -1,19 +1,19 @@
1
- import { compile_particle_effect } from "../graph/compile_particle_graph.js";
2
-
3
- /**
4
- * Compile a particle effect: a per-particle attribute {@link ../layout/ParticleLayout.js} plus SPAWN
5
- * and UPDATE node graphs, lowered to VM bytecode.
6
- *
7
- * The returned value bundles the layout with the compiled program so the {@link ./EmitterRegistry.js}
8
- * can resolve render-attribute names to record offsets and pack the program into the arena.
9
- *
10
- * @param {object} params
11
- * @param {import("../layout/ParticleLayout.js").ParticleLayout} params.layout
12
- * @param {import("../graph/ParticleGraph.js").ParticleGraph} [params.spawn]
13
- * @param {import("../graph/ParticleGraph.js").ParticleGraph} [params.update]
14
- * @returns {{ layout: import("../layout/ParticleLayout.js").ParticleLayout, program: import("../isa/ParticleProgram.js").ParticleProgram }}
15
- */
16
- export function create_particle_effect({ layout, spawn, update }) {
17
- const program = compile_particle_effect({ spawn, update, layout });
18
- return { layout, program };
19
- }
1
+ import { compile_particle_effect } from "../graph/compile_particle_graph.js";
2
+
3
+ /**
4
+ * Compile a particle effect: a per-particle attribute {@link ../layout/ParticleLayout.js} plus SPAWN
5
+ * and UPDATE node graphs, lowered to VM bytecode.
6
+ *
7
+ * The returned value bundles the layout with the compiled program so the {@link ./EmitterRegistry.js}
8
+ * can resolve render-attribute names to record offsets and pack the program into the arena.
9
+ *
10
+ * @param {object} params
11
+ * @param {import("../layout/ParticleLayout.js").ParticleLayout} params.layout
12
+ * @param {NodeGraph} [params.spawn]
13
+ * @param {NodeGraph} [params.update]
14
+ * @returns {{ layout: import("../layout/ParticleLayout.js").ParticleLayout, program: import("../isa/ParticleProgram.js").ParticleProgram }}
15
+ */
16
+ export function create_particle_effect({ layout, spawn, update }) {
17
+ const program = compile_particle_effect({ spawn, update, layout });
18
+ return { layout, program };
19
+ }
@@ -33,7 +33,7 @@ import { VM_BUILTIN, VM_OP, VM_RANDOM_MODE } from "../isa/ParticleVMISA.js";
33
33
 
34
34
  const R = PARTICLE_VM_REGISTER_COUNT;
35
35
 
36
- const source = /* wgsl */ `
36
+ const source = /* language=wgsl */ `
37
37
  const PARTICLE_VM_REGISTER_COUNT : u32 = ${R}u;
38
38
  const PARTICLE_VM_MAX_INSTRUCTIONS : u32 = ${PARTICLE_VM_MAX_INSTRUCTIONS}u;
39
39
  const PARTICLE_VM_TWO_PI : f32 = 6.28318530717958647692;
@@ -28,6 +28,17 @@
28
28
  * see {@link chunk_skin_blend_mesh_local} for why it goes into each
29
29
  * matrix rather than onto the blended position.
30
30
  *
31
+ * **`transform_global` on a skinned mesh is the engine's, not the
32
+ * asset's.** It cancels out of the rendered position, so glTF declares
33
+ * it meaningless — and the one meaning it does have is the frame the
34
+ * clone's vertices are written in, which the fold above requires to be
35
+ * rigid against the skin. {@link GPUAnimationManager#register_skin}
36
+ * therefore overwrites it, by way of
37
+ * {@link skin_normalize_mesh_frame}, with the skin's own bind
38
+ * transform. Whatever the file put there is discarded, and anything
39
+ * reading this expecting the file's value is reading the wrong thing.
40
+ * See `renderer/animation/SKINNING_FRAME_PLAN.md`.
41
+ *
31
42
  * The mesh's **skin id** — a row in a GPU table — lives in
32
43
  * {@link GPUMeshSkinningContext} and nowhere else. The CPU-side
33
44
  * {@link Skin} it was registered with is a different thing, pure data
@@ -1 +1 @@
1
- {"version":3,"file":"SkinnedMesh.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/scene/SkinnedMesh.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH;IAEI;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAFU,OAAK,IAAI,CAEP;IAEZ;;OAEG;IACH,SAFa,WAAW,CAMvB;IAED;;OAEG;IACH,YAFW,WAAW,QAYrB;IA4CL;;;OAGG;IACH,wBAFU,OAAO,CAEkB;CANlC;qBAnJoB,WAAW"}
1
+ {"version":3,"file":"SkinnedMesh.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/scene/SkinnedMesh.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH;IAEI;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAFU,OAAK,IAAI,CAEP;IAEZ;;OAEG;IACH,SAFa,WAAW,CAMvB;IAED;;OAEG;IACH,YAFW,WAAW,QAYrB;IA4CL;;;OAGG;IACH,wBAFU,OAAO,CAEkB;CANlC;qBA9JoB,WAAW"}
@@ -31,6 +31,17 @@ import { Mesh } from "./Mesh.js";
31
31
  * see {@link chunk_skin_blend_mesh_local} for why it goes into each
32
32
  * matrix rather than onto the blended position.
33
33
  *
34
+ * **`transform_global` on a skinned mesh is the engine's, not the
35
+ * asset's.** It cancels out of the rendered position, so glTF declares
36
+ * it meaningless — and the one meaning it does have is the frame the
37
+ * clone's vertices are written in, which the fold above requires to be
38
+ * rigid against the skin. {@link GPUAnimationManager#register_skin}
39
+ * therefore overwrites it, by way of
40
+ * {@link skin_normalize_mesh_frame}, with the skin's own bind
41
+ * transform. Whatever the file put there is discarded, and anything
42
+ * reading this expecting the file's value is reading the wrong thing.
43
+ * See `renderer/animation/SKINNING_FRAME_PLAN.md`.
44
+ *
34
45
  * The mesh's **skin id** — a row in a GPU table — lives in
35
46
  * {@link GPUMeshSkinningContext} and nowhere else. The CPU-side
36
47
  * {@link Skin} it was registered with is a different thing, pure data
@@ -1,7 +1,37 @@
1
1
  /**
2
- * @see https://github.com/KosRud/DQ-skinning-for-Unity/blob/9d096e2db57677f0f67a823432f8b735ad622404/Code/DQ%20skinning/Shaders/Compute/DQ.cginc#L59
3
- * @see http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
4
- * Alternative Method by Christian
2
+ * A rigid transform as a dual quaternion: the rotation in the real part, the translation carried in
3
+ * the dual part as `0.5 · t · real`.
4
+ *
5
+ * The matrix's upper 3×3 is read as a rotation and its fourth column as a translation. Anything else
6
+ * in the matrix — a scale, a shear — has nowhere to go, and comes back as neither itself nor a
7
+ * warning: see {@link m4_rigidity_defect} for the number that says so and
8
+ * `renderer/animation/SKINNING_FRAME_PLAN.md` for the frame arrangement that keeps a skinning
9
+ * matrix rigid in the first place.
10
+ *
11
+ * ## Why the rotation goes through `quat_from_m3`
12
+ *
13
+ * This used to take all four quaternion components as `sqrt(max(0, 1 ± m00 ± m11 ± m22)) · 0.5` and
14
+ * recover their signs from the off-diagonal differences. That is exact in exact arithmetic and
15
+ * badly conditioned in floating point: near the identity three of those radicands are near zero, and
16
+ * a square root turns an input error of `e` into an output error of `sqrt(e)`. A matrix that is the
17
+ * identity to `1e-6` — which is what a skin's folded joint matrices are at bind pose — yielded
18
+ * quaternion components off by `9e-4`, an angular error of `1.8e-3` radians, and a millimetre and a
19
+ * half of vertex displacement on a 1.66 m character. Measured, on `Michelle.glb`.
20
+ *
21
+ * {@link chunk_quat_from_m3} is Shoemake's branch-picking form: it computes whichever component the
22
+ * matrix's diagonal says is largest, and derives the other three from it by division. Every branch
23
+ * divides by at least 2, so the components come out **linear** in the input error rather than as its
24
+ * square root — the same `1e-6` matrix now gives `2.5e-7`, three and a half orders better — and the
25
+ * near-180° case, where the trace form is ill-conditioned for the opposite reason, is a branch of its
26
+ * own. One `sqrt` and a reciprocal where there were four `sqrt`s.
27
+ *
28
+ * The normalize stays, on top of that. It is a no-op for the orthonormal input this is meant to be
29
+ * given; what it earns its rsqrt for is the input that is not — a degenerate 3×3 leaves
30
+ * `quat_from_m3` on a branch whose answer has length 0.5, and this is what keeps the documented
31
+ * postcondition, a unit real part, true anyway.
32
+ *
33
+ * @see https://gitlab.com/libeigen/eigen/-/blob/master/Eigen/src/Geometry/Quaternion.h#L813
34
+ * @type {CodeChunk}
5
35
  */
6
36
  export const chunk_dual_quat_from_m4: CodeChunk;
7
37
  import { CodeChunk } from "../../../../compiler/CodeChunk.js";
@@ -1 +1 @@
1
- {"version":3,"file":"chunk_dual_quat_from_m4.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,gDA2BC;0BApCyB,mCAAmC"}
1
+ {"version":3,"file":"chunk_dual_quat_from_m4.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,sCAFU,SAAS,CAoBlB;0BAzDyB,mCAAmC"}