@woosh/meep-engine 3.14.3 → 3.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
  3. package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
  4. package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
  5. package/src/engine/Engine.d.ts.map +1 -1
  6. package/src/engine/Engine.js +733 -726
  7. package/src/engine/network/NetworkSession.d.ts +24 -4
  8. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  9. package/src/engine/network/NetworkSession.js +28 -4
  10. package/src/engine/network/orchestrator/NetworkPeer.d.ts +96 -7
  11. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  12. package/src/engine/network/orchestrator/NetworkPeer.js +392 -68
  13. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
  14. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  15. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
  16. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +105 -13
  17. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  18. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +306 -59
  19. package/src/engine/network/replication/Replicator.d.ts +43 -7
  20. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  21. package/src/engine/network/replication/Replicator.js +309 -111
  22. package/src/engine/network/sim/ActionLog.d.ts +23 -3
  23. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  24. package/src/engine/network/sim/ActionLog.js +26 -3
  25. package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
  26. package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
  27. package/src/engine/network/sim/ActionRecordCursor.js +120 -0
  28. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  29. package/src/engine/network/sim/RewindEngine.js +13 -29
  30. package/src/engine/network/sim/SimAction.d.ts +15 -0
  31. package/src/engine/network/sim/SimAction.d.ts.map +1 -1
  32. package/src/engine/network/sim/SimAction.js +15 -0
  33. package/src/engine/network/sim/SimActionExecutor.d.ts +51 -2
  34. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  35. package/src/engine/network/sim/SimActionExecutor.js +74 -7
  36. package/src/engine/network/transport/Channel.d.ts +1 -1
  37. package/src/engine/network/transport/Channel.js +1 -1
  38. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  39. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  40. package/src/engine/simulation/Ticker.js +4 -1
  41. package/src/engine/ui/GUIEngine.d.ts.map +1 -1
  42. package/src/engine/ui/GUIEngine.js +6 -0
  43. package/src/shade/playground/particle_system/README.md +98 -0
  44. package/src/shade/playground/particle_system/index.html +124 -0
  45. package/src/shade/playground/particle_system/main.d.ts +2 -0
  46. package/src/shade/playground/particle_system/main.d.ts.map +1 -0
  47. package/src/shade/playground/particle_system/main.js +180 -0
  48. package/src/shade/playground/particle_system/particle_scene.d.ts +52 -0
  49. package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -0
  50. package/src/shade/playground/particle_system/particle_scene.js +553 -0
  51. package/src/shade/playground/skinned_mesh_soup/README.md +143 -0
  52. package/src/shade/playground/skinned_mesh_soup/index.html +202 -0
  53. package/src/shade/playground/skinned_mesh_soup/main.d.ts +7 -0
  54. package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -0
  55. package/src/shade/playground/skinned_mesh_soup/main.js +567 -0
  56. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +29 -0
  57. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -0
  58. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +428 -0
  59. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  60. package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
  61. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
  62. package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
  63. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
  64. package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
  65. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
  66. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
  67. package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
  68. package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
  69. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
  70. package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
  71. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
  72. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
  73. package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
  74. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
  75. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
  76. package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
  77. package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
  78. package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
  79. package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
  80. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
  81. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
  82. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
  83. package/src/shade/renderer/particles/DESIGN.md +5 -5
  84. package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +1 -1
  85. package/src/shade/renderer/particles/cull/chunk_particle_cull.js +3 -1
  86. package/src/shade/renderer/particles/data/chunk_particle_emitter.js +1 -1
  87. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
  88. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
  89. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
  90. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
  91. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  92. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
  93. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
  94. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  95. package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
  96. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
  97. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
  98. package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
  99. package/src/shade/renderer/particles/isa/InstructionStream.d.ts +89 -0
  100. package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -0
  101. package/src/shade/renderer/particles/isa/InstructionStream.js +162 -0
  102. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +1 -48
  103. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
  104. package/src/shade/renderer/particles/isa/ParticleAssembler.js +25 -114
  105. package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
  106. package/src/shade/renderer/particles/particle_prototype.js +181 -180
  107. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
  108. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  109. package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
  110. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +1 -1
  111. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +4 -2
  112. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +4 -4
  113. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
  114. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +7 -5
  115. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
  116. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +3 -1
  117. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  118. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +3 -1
  119. package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
  120. package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +5 -2
  121. package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
  122. package/src/shade/renderer/particles/shaders/shader_particle_render.js +9 -2
  123. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  124. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -1
  125. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
  126. package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
  127. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
  128. package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
  129. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
  130. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
  131. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
  132. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
  133. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
  134. package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
  135. package/src/shade/renderer/particles/prototypeParticleSystem.d.ts +0 -2
  136. package/src/shade/renderer/particles/prototypeParticleSystem.d.ts.map +0 -1
  137. package/src/shade/renderer/particles/prototypeParticleSystem.js +0 -448
@@ -1,448 +0,0 @@
1
- import { m4_allocate } from "../../../core/geom/3d/mat4/m4_allocate.js";
2
- import { m4_invert } from "../../../core/geom/3d/mat4/m4_invert.js";
3
- import { m4_look_at } from "../../../core/geom/3d/mat4/m4_look_at.js";
4
- import { m4_multiply } from "../../../core/geom/3d/mat4/m4_multiply.js";
5
- import { f32, u32 } from "../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
6
- import { GraphicsContext } from "../GraphicsContext.js";
7
- import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
8
- import { CAMERA_UNIFORM_STRUCT } from "../camera/CAMERA_UNIFORM_STRUCT.js";
9
- import { PARTICLE_RECORD_WORD_COUNT } from "./ParticleConstants.js";
10
- import { PARTICLE_COUNTER_COUNT } from "./data/PARTICLE_COUNTERS.js";
11
- import { EMITTER_BLEND, EMITTER_PROJECTION } from "./data/PARTICLE_EMITTER_LAYOUT.js";
12
- import { ParticleGraph } from "./graph/ParticleGraph.js";
13
- import { VM_BUILTIN, VM_RANDOM_MODE } from "./isa/ParticleVMISA.js";
14
- import { ParticleLayout } from "./layout/ParticleLayout.js";
15
- import { GPUParticleSystem } from "./GPUParticleSystem.js";
16
- import { create_particle_effect } from "./runtime/create_particle_effect.js";
17
- import { fire_effect } from "./particle_prototype.js";
18
- import {
19
- PARTICLE_BILLBOARD_RESOURCES,
20
- create_particle_billboard_pipeline,
21
- } from "./shaders/shader_particle_render.js";
22
- import { shader_particle_emit } from "./shaders/shader_particle_emit.js";
23
- import {
24
- shader_particle_build_indirect,
25
- shader_particle_reset_counters,
26
- } from "./shaders/shader_particle_finalize.js";
27
- import {
28
- PARTICLE_SIMULATE_WORKGROUP_SIZE,
29
- shader_particle_simulate,
30
- } from "./shaders/shader_particle_simulate.js";
31
-
32
- /**
33
- * Rich, runnable demonstration of the GPU particle system in a real scene:
34
- *
35
- * - lit solid geometry (a ground plane + boxes) rendered into a reverse-Z depth buffer,
36
- * - four emitters with distinct settings — an additive fire, an alpha soft-depth smoke column,
37
- * additive velocity-stretched sparks, and a wide soft-depth dust drift,
38
- * - the billboard pass depth-tests against the scene so particles are occluded by the boxes, and
39
- * soft-depth emitters fade as they approach surfaces.
40
- *
41
- * The loop is fully GPU-driven: the simulate dispatch runs via dispatchWorkgroupsIndirect and the
42
- * billboard draw via drawIndirect, both from args the build-indirect pass writes on the GPU. No CPU
43
- * readback participates in sizing anything — a stale CPU count would orphan alive-list entries when
44
- * the population grows and double-free slots when it shrinks.
45
- *
46
- * Everything runs against a live WebGPU device (no Renderer dependency): geometry pass, then the
47
- * real emit + simulate compute passes, then one instanced billboard draw over all particles (a
48
- * single premultiplied blend state expresses both additive and alpha). `index.html` points vite at
49
- * this module; run with `npm run start` (HTTPS) or `VITE_HTTP=1 npx vite` for headless browsers.
50
- */
51
-
52
- const CAPACITY = 1 << 15; // 32k particles
53
- const NEAR = 0.2;
54
-
55
- async function main() {
56
- document.body.style.cssText = "margin:0;background:#000;overflow:hidden";
57
- const canvas = document.createElement("canvas");
58
- document.body.appendChild(canvas);
59
-
60
- if (navigator.gpu === undefined) return show_message("WebGPU is not available in this browser.");
61
-
62
- const adapter = await navigator.gpu.requestAdapter();
63
- const device = await adapter.requestDevice();
64
- device.addEventListener?.("uncapturederror", (e) => console.error("WebGPU error:", e.error.message));
65
-
66
- const graphics = new GraphicsContext(device);
67
- const context = canvas.getContext("webgpu");
68
- const format = navigator.gpu.getPreferredCanvasFormat();
69
- context.configure({ device, format, alphaMode: "opaque" });
70
-
71
- // --- scene: four emitters, each with different settings -----------------------------------
72
- const system = new GPUParticleSystem(graphics, { capacity: CAPACITY });
73
-
74
- // fire — additive, billboard, at the base of the tall pillar
75
- system.addEmitter({
76
- ...fire_effect().emitter,
77
- spawn_rate: 500, flipbook: [1, 1], atlas_region: [0, 0, 1, 1],
78
- flags: { blend: EMITTER_BLEND.ADDITIVE, projection: EMITTER_PROJECTION.BILLBOARD },
79
- transform: translation(-1, 0, 0),
80
- });
81
- // smoke — alpha, soft-depth, rising column above the fire
82
- system.addEmitter({
83
- effect: make_effect({ velocity: [0, 1.3, 0], velocityJitter: 0.35, posJitter: 0.5, accel: 0.25, life: [3, 5], size: 1.4, color: [0.45, 0.45, 0.48, 0.28] }),
84
- spawn_rate: 60, atlas_region: [0, 0, 1, 1], flipbook: [1, 1],
85
- flags: { blend: EMITTER_BLEND.ALPHA, projection: EMITTER_PROJECTION.BILLBOARD, soft_depth: true },
86
- render: { position: "position", size: "size", color: "color" },
87
- transform: translation(-1, 2.5, 0),
88
- });
89
- // sparks — additive, velocity-stretched, fast, short-lived, gravity, near the box
90
- system.addEmitter({
91
- effect: make_effect({ velocity: [0, 5, 0], velocityJitter: 4.5, posJitter: 0.15, accel: -14, life: [0.3, 0.8], size: 0.09, color: [1.0, 0.8, 0.35, 1.0] }),
92
- spawn_rate: 350, atlas_region: [0, 0, 1, 1], flipbook: [1, 1],
93
- flags: { blend: EMITTER_BLEND.ADDITIVE, projection: EMITTER_PROJECTION.STRETCHED },
94
- render: { position: "position", size: "size", color: "color", velocity: "velocity" },
95
- transform: translation(2, 1.9, -1),
96
- });
97
- // dust — alpha, soft-depth, slow wide drift across the ground
98
- system.addEmitter({
99
- effect: make_effect({ velocity: [0.3, 0.15, 0.1], velocityJitter: 0.35, posJitter: 6, accel: 0, life: [4, 7], size: 0.7, color: [0.55, 0.5, 0.42, 0.16] }),
100
- spawn_rate: 120, atlas_region: [0, 0, 1, 1], flipbook: [1, 1],
101
- flags: { blend: EMITTER_BLEND.ALPHA, projection: EMITTER_PROJECTION.BILLBOARD, soft_depth: true },
102
- render: { position: "position", size: "size", color: "color" },
103
- transform: translation(0, 0.3, 0),
104
- });
105
- const built = system.buildEmitterData();
106
-
107
- // --- particle GPU buffers -----------------------------------------------------------------
108
- const S = () => GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
109
- const pool = device.createBuffer({ label: "particles/pool", size: CAPACITY * PARTICLE_RECORD_WORD_COUNT * 4, usage: S() });
110
- const aliveA = device.createBuffer({ label: "particles/alive_a", size: CAPACITY * 4, usage: S() });
111
- const aliveB = device.createBuffer({ label: "particles/alive_b", size: CAPACITY * 4, usage: S() });
112
- const dead = device.createBuffer({ label: "particles/dead", size: CAPACITY * 4, usage: S() });
113
- const counters = device.createBuffer({ label: "particles/counters", size: PARTICLE_COUNTER_COUNT * 4, usage: S() });
114
- const spawnRequests = device.createBuffer({ label: "particles/spawn_requests", size: CAPACITY * 4, usage: S() });
115
- const emitters = data_buffer(device, "particles/emitters", built.emitters, S());
116
- const program = data_buffer(device, "particles/program", built.program, S());
117
- const constants = data_buffer(device, "particles/constants", built.constants, S());
118
- // Debug instrumentation (counters readback + manual frame stepping) — opt in with ?debug.
119
- // Useful for verifying the loop numerically in a headless / hidden tab where rAF is suspended.
120
- const DEBUG = new URLSearchParams(location.search).has("debug");
121
-
122
- // Indirect args, written on-GPU by the build-indirect pass. Zero-initialised (WebGPU spec), so
123
- // the first frame's indirect dispatch/draw are no-ops until the first snapshot lands.
124
- const argsUsage = GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | (DEBUG ? GPUBufferUsage.COPY_SRC : 0);
125
- const dispatchArgs = device.createBuffer({ label: "particles/dispatch_args", size: 3 * 4, usage: argsUsage });
126
- const drawArgs = device.createBuffer({ label: "particles/draw_args", size: 4 * 4, usage: argsUsage });
127
- const initial = system.initialFreeList();
128
- device.queue.writeBuffer(dead, 0, initial.dead);
129
- device.queue.writeBuffer(counters, 0, initial.counters);
130
-
131
- // --- camera + geometry --------------------------------------------------------------------
132
- const cameraBuffer = device.createBuffer({ label: "particles/camera", size: CAMERA_UNIFORM_STRUCT.aligned_size, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
133
- const vpBuffer = device.createBuffer({ label: "vp", size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
134
-
135
- const geo = build_geometry();
136
- const geoBuffer = device.createBuffer({ label: "geometry", size: geo.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
137
- device.queue.writeBuffer(geoBuffer, 0, geo);
138
- const geoVertexCount = geo.length / 6;
139
- const { geoPipeline, geoBindGroup } = make_geometry_pipeline(device, format, vpBuffer);
140
-
141
- // atlas sprite + soft round falloff, filtering sampler
142
- const atlasView = make_sprite_texture(device).createView();
143
- const atlasSampler = device.createSampler({ magFilter: "linear", minFilter: "linear", addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge" });
144
-
145
- // reverse-Z depth buffer (recreated on resize)
146
- let depthTex = null, depthView = null, depthW = 0, depthH = 0;
147
- function ensure_depth(w, h) {
148
- if (depthView && depthW === w && depthH === h) return;
149
- depthTex?.destroy();
150
- depthTex = device.createTexture({ label: "scene_depth", size: [w, h], format: "depth32float", usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING });
151
- depthView = depthTex.createView();
152
- depthW = w; depthH = h;
153
- }
154
-
155
- // billboard pipeline: reverse-Z depth test (same compare as the geometry pass), no write;
156
- // soft-depth reads the scene depth texture
157
- const particlePipeline = create_particle_billboard_pipeline({ color_format: format, depth_format: "depth32float", depth_compare: "greater" });
158
-
159
- let aliveRead = aliveA, aliveWrite = aliveB;
160
- let last = performance.now();
161
-
162
- // ?debug: heartbeat + occasional counters/args readback exposed on window.__stats.
163
- const dbgBuf = DEBUG ? device.createBuffer({ label: "dbg", size: 64, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }) : null;
164
- let dbgPending = false;
165
- let frame_index = 0;
166
-
167
- function frame(now) {
168
- const dt = Math.min((now - last) / 1000, 0.05);
169
- last = now;
170
-
171
- const w = Math.max(1, window.innerWidth), h = Math.max(1, window.innerHeight);
172
- if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
173
- ensure_depth(w, h);
174
- write_camera(device, cameraBuffer, vpBuffer, w / h, now / 1000);
175
-
176
- const requests = system.update(dt);
177
- if (requests.length > 0) device.queue.writeBuffer(spawnRequests, 0, requests);
178
-
179
- const cmd = ShadeGPUCommandContext.create(graphics);
180
- const colorView = context.getCurrentTexture().createView();
181
-
182
- // --- geometry pass: clear colour + reverse-Z depth (far = 0), lit, writes depth ---
183
- const gpass = cmd.beginRenderPass({
184
- colorAttachments: [{ view: colorView, loadOp: "clear", storeOp: "store", clearValue: { r: 0.03, g: 0.035, b: 0.05, a: 1 } }],
185
- depthStencilAttachment: { view: depthView, depthLoadOp: "clear", depthStoreOp: "store", depthClearValue: 0.0 },
186
- });
187
- gpass.setPipeline(geoPipeline);
188
- gpass.setBindGroup(0, geoBindGroup);
189
- gpass.setVertexBuffer(0, geoBuffer);
190
- gpass.draw(geoVertexCount);
191
- gpass.end();
192
-
193
- // --- emit + simulate (simulate is sized by LAST frame's GPU-written dispatch args) ---
194
- shader_particle_reset_counters.dispatch({ encoder: cmd, bindings: { counters }, group_count_x: 1 });
195
- if (requests.length > 0) {
196
- shader_particle_emit.dispatchThreads({
197
- encoder: cmd,
198
- bindings: { emitters, program, vm_constants: constants, pool, spawn_requests: spawnRequests, alive_out: aliveWrite, dead, counters, settings: { u_dt: dt, u_time: now / 1000, u_frame: system.frame, u_count: requests.length, u_capacity: CAPACITY } },
199
- thread_count_x: requests.length,
200
- });
201
- }
202
- shader_particle_simulate.dispatchIndirect({
203
- encoder: cmd,
204
- bindings: { emitters, program, vm_constants: constants, pool, alive_in: aliveRead, alive_out: aliveWrite, dead, counters, settings: { u_dt: dt, u_time: now / 1000, u_frame: system.frame } },
205
- command: dispatchArgs,
206
- });
207
- // Snapshot the final alive count (next frame's simulate input) + write this frame's draw args.
208
- shader_particle_build_indirect.dispatch({
209
- encoder: cmd,
210
- bindings: { counters, dispatch_args: dispatchArgs, draw_args: drawArgs, u_workgroup_size: PARTICLE_SIMULATE_WORKGROUP_SIZE },
211
- group_count_x: 1,
212
- });
213
-
214
- // --- billboard pass: load colour, depth-test against the scene (read-only), exact indirect draw ---
215
- const bindings = PARTICLE_BILLBOARD_RESOURCES.generateBindings({
216
- camera: cameraBuffer, pool, sorted: aliveWrite, emitters,
217
- atlas: atlasView, atlas_sampler: atlasSampler, scene_depth: depthView,
218
- }, cmd);
219
- const ppass = cmd.constructRenderPass({
220
- pipeline: particlePipeline,
221
- bindings,
222
- colorAttachments: [{ view: colorView, loadOp: "load", storeOp: "store" }],
223
- depthStencilAttachment: { view: depthView, depthReadOnly: true },
224
- });
225
- ppass.drawIndirect(drawArgs, 0);
226
- ppass.end();
227
-
228
- // ?debug: copy counters + args for readback every ~30 frames (stats only; never used for sizing).
229
- const capture_stats = DEBUG && !dbgPending && frame_index % 30 === 0;
230
- if (capture_stats) {
231
- cmd.copyBufferToBuffer(counters, 0, dbgBuf, 0, PARTICLE_COUNTER_COUNT * 4);
232
- cmd.copyBufferToBuffer(dispatchArgs, 0, dbgBuf, 32, 12);
233
- cmd.copyBufferToBuffer(drawArgs, 0, dbgBuf, 48, 16);
234
- }
235
-
236
- cmd.finish();
237
-
238
- frame_index++;
239
- if (capture_stats) {
240
- dbgPending = true;
241
- dbgBuf.mapAsync(GPUMapMode.READ).then(() => {
242
- const v = new Uint32Array(dbgBuf.getMappedRange().slice(0));
243
- window.__stats = { f: frame_index, alive: v[0], dead: v[1], alive_in: v[4], dispatch: [v[8], v[9], v[10]], draw: [v[12], v[13]] };
244
- dbgBuf.unmap(); dbgPending = false;
245
- }).catch(() => { dbgPending = false; });
246
- }
247
-
248
- const t = aliveRead; aliveRead = aliveWrite; aliveWrite = t;
249
- requestAnimationFrame(frame);
250
- }
251
- if (DEBUG) {
252
- // Drive frames manually when the tab is hidden (rAF suspended) — headless verification.
253
- window.__step = (n = 1) => { for (let i = 0; i < n; i++) frame(performance.now()); };
254
- }
255
- requestAnimationFrame(frame);
256
- }
257
-
258
- // ---------------------------------------------------------------------------------------------
259
- // Effect authoring
260
- // ---------------------------------------------------------------------------------------------
261
-
262
- /**
263
- * A parametric point-emission effect: emit with a base velocity + jitter from a disk, integrate
264
- * under a vertical acceleration, and die at end of life. Only nAge.x is consumed (kill), so the
265
- * NaN lanes from the scalar age/lifetime divide are harmless.
266
- */
267
- function make_effect({ velocity, velocityJitter = 0, posJitter = 0, accel = 0, life = [1, 2], size = 1, color = [1, 1, 1, 1] }) {
268
- const layout = new ParticleLayout([
269
- { name: "position", components: 3 }, { name: "velocity", components: 3 },
270
- { name: "age", components: 1 }, { name: "lifetime", components: 1 },
271
- { name: "size", components: 1 }, { name: "color", components: 4 },
272
- ]);
273
-
274
- const spawn = new ParticleGraph();
275
- let posN = spawn.add("builtin", { id: VM_BUILTIN.EMITTER_POSITION });
276
- if (posJitter > 0) {
277
- const d = spawn.add("random", { mode: VM_RANDOM_MODE.UNIT_DISK });
278
- const j = spawn.add("scale"); spawn.wire(j, "v", d); spawn.wire(j, "s", [posJitter]);
279
- const p = spawn.add("add"); spawn.wire(p, "a", posN); spawn.wire(p, "b", j); posN = p;
280
- }
281
- spawn.wire(spawn.add("setAttribute", { name: "position" }), "value", posN);
282
-
283
- let velN = spawn.add("const", { value: [velocity[0], velocity[1], velocity[2], 0] });
284
- if (velocityJitter > 0) {
285
- const r = spawn.add("random", { mode: VM_RANDOM_MODE.UNIFORM_SIGNED });
286
- const rs = spawn.add("scale"); spawn.wire(rs, "v", r); spawn.wire(rs, "s", [velocityJitter]);
287
- const vv = spawn.add("add"); spawn.wire(vv, "a", velN); spawn.wire(vv, "b", rs); velN = vv;
288
- }
289
- spawn.wire(spawn.add("setAttribute", { name: "velocity" }), "value", velN);
290
- spawn.wire(spawn.add("setAttribute", { name: "age" }), "value", [0]);
291
- const lr = spawn.add("random", { mode: VM_RANDOM_MODE.UNIFORM });
292
- const lm = spawn.add("mad"); spawn.wire(lm, "a", lr); spawn.wire(lm, "b", [life[1] - life[0]]); spawn.wire(lm, "c", [life[0]]);
293
- spawn.wire(spawn.add("setAttribute", { name: "lifetime" }), "value", lm);
294
- spawn.wire(spawn.add("setAttribute", { name: "size" }), "value", [size]);
295
- spawn.wire(spawn.add("setAttribute", { name: "color" }), "value", color);
296
-
297
- const update = new ParticleGraph();
298
- const dt = update.add("builtin", { id: VM_BUILTIN.DELTA_TIME });
299
- const age = update.add("attribute", { name: "age" });
300
- const nage = update.add("add"); update.wire(nage, "a", age); update.wire(nage, "b", dt);
301
- update.wire(update.add("setAttribute", { name: "age" }), "value", nage);
302
- const lifeA = update.add("attribute", { name: "lifetime" });
303
- const na = update.add("div"); update.wire(na, "a", nage); update.wire(na, "b", lifeA);
304
- const dead = update.add("compare", { op: "ge" }); update.wire(dead, "a", na); update.wire(dead, "b", [1]);
305
- update.wire(update.add("kill"), "condition", dead);
306
- let velN2 = update.add("attribute", { name: "velocity" });
307
- if (accel !== 0) {
308
- const g = update.add("const", { value: [0, accel, 0, 0] });
309
- const gd = update.add("scale"); update.wire(gd, "v", g); update.wire(gd, "s", dt);
310
- const nv = update.add("add"); update.wire(nv, "a", velN2); update.wire(nv, "b", gd);
311
- update.wire(update.add("setAttribute", { name: "velocity" }), "value", nv); velN2 = nv;
312
- }
313
- const pos = update.add("attribute", { name: "position" });
314
- const pd = update.add("scale"); update.wire(pd, "v", velN2); update.wire(pd, "s", dt);
315
- const np = update.add("add"); update.wire(np, "a", pos); update.wire(np, "b", pd);
316
- update.wire(update.add("setAttribute", { name: "position" }), "value", np);
317
-
318
- return create_particle_effect({ layout, spawn, update });
319
- }
320
-
321
- // ---------------------------------------------------------------------------------------------
322
- // Geometry + camera + helpers
323
- // ---------------------------------------------------------------------------------------------
324
-
325
- const GEOM_WGSL = /* wgsl */ `
326
- @group(0) @binding(0) var<uniform> vp : mat4x4<f32>;
327
- struct VOut { @builtin(position) clip : vec4<f32>, @location(0) normal : vec3<f32> }
328
- @vertex fn vs(@location(0) pos : vec3<f32>, @location(1) normal : vec3<f32>) -> VOut {
329
- var o : VOut;
330
- o.clip = vp * vec4<f32>(pos, 1.0);
331
- o.normal = normal;
332
- return o;
333
- }
334
- @fragment fn fs(in : VOut) -> @location(0) vec4<f32> {
335
- let N = normalize(in.normal);
336
- let L = normalize(vec3<f32>(0.35, 1.0, 0.4));
337
- let diffuse = max(dot(N, L), 0.0);
338
- let albedo = select(vec3<f32>(0.30, 0.32, 0.36), vec3<f32>(0.22, 0.24, 0.28), N.y < 0.9);
339
- let color = albedo * (0.18 + diffuse * 0.9);
340
- return vec4<f32>(color, 1.0);
341
- }
342
- `;
343
-
344
- function make_geometry_pipeline(device, format, vpBuffer) {
345
- const module = device.createShaderModule({ code: GEOM_WGSL });
346
- const bgl = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }] });
347
- const geoPipeline = device.createRenderPipeline({
348
- layout: device.createPipelineLayout({ bindGroupLayouts: [bgl] }),
349
- vertex: { module, entryPoint: "vs", buffers: [{ arrayStride: 24, attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }, { shaderLocation: 1, offset: 12, format: "float32x3" }] }] },
350
- fragment: { module, entryPoint: "fs", targets: [{ format }] },
351
- primitive: { topology: "triangle-list", cullMode: "none" },
352
- depthStencil: { format: "depth32float", depthWriteEnabled: true, depthCompare: "greater" }, // reverse-Z
353
- });
354
- const geoBindGroup = device.createBindGroup({ layout: bgl, entries: [{ binding: 0, resource: { buffer: vpBuffer } }] });
355
- return { geoPipeline, geoBindGroup };
356
- }
357
-
358
- /** Ground plane + a few boxes as an interleaved [x,y,z, nx,ny,nz] vertex array. */
359
- function build_geometry() {
360
- const v = [];
361
- const quad = (a, b, c, d, n) => { for (const p of [a, b, c, a, c, d]) v.push(p[0], p[1], p[2], n[0], n[1], n[2]); };
362
- const box = (cx, cy, cz, sx, sy, sz) => {
363
- const x0 = cx - sx / 2, x1 = cx + sx / 2, y0 = cy - sy / 2, y1 = cy + sy / 2, z0 = cz - sz / 2, z1 = cz + sz / 2;
364
- quad([x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1], [0, 0, 1]);
365
- quad([x1, y0, z0], [x0, y0, z0], [x0, y1, z0], [x1, y1, z0], [0, 0, -1]);
366
- quad([x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x1, y1, z1], [1, 0, 0]);
367
- quad([x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0], [-1, 0, 0]);
368
- quad([x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0], [0, 1, 0]);
369
- quad([x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1], [0, -1, 0]);
370
- };
371
- quad([-15, 0, -15], [15, 0, -15], [15, 0, 15], [-15, 0, 15], [0, 1, 0]); // ground
372
- box(-1, 1.5, 0, 1, 3, 1); // tall pillar next to the fire
373
- box(2, 0.9, -1, 1.6, 1.8, 1.6); // block near the sparks
374
- box(0, 0.6, 3.5, 7, 1.2, 0.6); // low wall the dust drifts against
375
- return new Float32Array(v);
376
- }
377
-
378
- /** Reverse-Z, infinite-far perspective (WebGPU 0..1 depth: near -> 1, far -> 0). */
379
- function perspective_reverse_z(fovY, aspect, near) {
380
- const f = 1 / Math.tan(fovY / 2);
381
- return new Float32Array([
382
- f / aspect, 0, 0, 0,
383
- 0, f, 0, 0,
384
- 0, 0, 0, -1,
385
- 0, 0, near, 0,
386
- ]);
387
- }
388
-
389
- function write_camera(device, cameraBuffer, vpBuffer, aspect, t) {
390
- const radius = 12, height = 5.5;
391
- const eye = [Math.cos(t * 0.22) * radius, height, Math.sin(t * 0.22) * radius];
392
- const center = [0, 1.6, 0];
393
- const view = m4_look_at(m4_allocate(), eye, center, [0, 1, 0]);
394
-
395
- const world = m4_allocate();
396
- m4_invert(world, view);
397
-
398
- const proj = perspective_reverse_z((58 * Math.PI) / 180, aspect, NEAR);
399
-
400
- const vp = m4_multiply(m4_allocate(), proj, view);
401
-
402
- const data = new Float32Array(CAMERA_UNIFORM_STRUCT.aligned_size / 4);
403
- const put = (field, m) => data.set(m, CAMERA_UNIFORM_STRUCT.get(field).offset / 4);
404
- put("transform", world);
405
- put("view_matrix", view);
406
- put("projection_matrix", proj);
407
- put("view_projection_matrix", vp);
408
- // Reverse-Z infinite-far linearisation: view_depth = A / (device_depth + B), with A=near, B=0.
409
- data.set([0, NEAR, 0, 0], CAMERA_UNIFORM_STRUCT.get("device_depth_to_view_space").offset / 4);
410
- device.queue.writeBuffer(cameraBuffer, 0, data);
411
- device.queue.writeBuffer(vpBuffer, 0, vp);
412
- }
413
-
414
- function translation(x, y, z) {
415
- return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
416
- }
417
-
418
- function data_buffer(device, label, data, usage) {
419
- const buffer = device.createBuffer({ label, size: Math.max(data.byteLength, 4), usage });
420
- device.queue.writeBuffer(buffer, 0, data);
421
- return buffer;
422
- }
423
-
424
- /** A 64x64 radial-falloff sprite (white, alpha falloff) for soft additive/alpha blobs. */
425
- function make_sprite_texture(device) {
426
- const N = 64;
427
- const px = new Uint8Array(N * N * 4);
428
- for (let y = 0; y < N; y++) {
429
- for (let x = 0; x < N; x++) {
430
- const dx = (x + 0.5) / N * 2 - 1, dy = (y + 0.5) / N * 2 - 1;
431
- const a = Math.max(0, 1 - Math.hypot(dx, dy));
432
- const i = (y * N + x) * 4;
433
- px[i] = 255; px[i + 1] = 255; px[i + 2] = 255; px[i + 3] = Math.round(a * a * 255);
434
- }
435
- }
436
- const tex = device.createTexture({ label: "sprite", size: [N, N], format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
437
- device.queue.writeTexture({ texture: tex }, px, { bytesPerRow: N * 4, rowsPerImage: N }, [N, N]);
438
- return tex;
439
- }
440
-
441
- function show_message(text) {
442
- const d = document.createElement("div");
443
- d.textContent = text;
444
- d.style.cssText = "position:fixed;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-family:sans-serif;font-size:20px";
445
- document.body.appendChild(d);
446
- }
447
-
448
- main().catch((e) => { console.error(e); show_message("Particle prototype failed to start — see console. " + e.message); });