@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,553 @@
|
|
|
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 { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
|
|
6
|
+
import { CAMERA_UNIFORM_STRUCT } from "../../renderer/camera/CAMERA_UNIFORM_STRUCT.js";
|
|
7
|
+
import { PARTICLE_RECORD_WORD_COUNT } from "../../renderer/particles/ParticleConstants.js";
|
|
8
|
+
import { PARTICLE_COUNTER_COUNT } from "../../renderer/particles/data/PARTICLE_COUNTERS.js";
|
|
9
|
+
import { EMITTER_BLEND, EMITTER_PROJECTION } from "../../renderer/particles/data/PARTICLE_EMITTER_LAYOUT.js";
|
|
10
|
+
import { NodeGraph } from "../../../core/model/node-graph/NodeGraph.js";
|
|
11
|
+
import { particle_node, particle_wire } from "../../renderer/particles/graph/particle_graph_authoring.js";
|
|
12
|
+
import { VM_BUILTIN, VM_RANDOM_MODE } from "../../renderer/particles/isa/ParticleVMISA.js";
|
|
13
|
+
import { ParticleLayout } from "../../renderer/particles/layout/ParticleLayout.js";
|
|
14
|
+
import { GPUParticleSystem } from "../../renderer/particles/GPUParticleSystem.js";
|
|
15
|
+
import { create_particle_effect } from "../../renderer/particles/runtime/create_particle_effect.js";
|
|
16
|
+
import { fire_effect } from "../../renderer/particles/particle_prototype.js";
|
|
17
|
+
import {
|
|
18
|
+
PARTICLE_BILLBOARD_RESOURCES,
|
|
19
|
+
create_particle_billboard_pipeline,
|
|
20
|
+
} from "../../renderer/particles/shaders/shader_particle_render.js";
|
|
21
|
+
import { shader_particle_emit } from "../../renderer/particles/shaders/shader_particle_emit.js";
|
|
22
|
+
import {
|
|
23
|
+
shader_particle_build_indirect,
|
|
24
|
+
shader_particle_reset_counters,
|
|
25
|
+
} from "../../renderer/particles/shaders/shader_particle_finalize.js";
|
|
26
|
+
import {
|
|
27
|
+
PARTICLE_SIMULATE_WORKGROUP_SIZE,
|
|
28
|
+
shader_particle_simulate,
|
|
29
|
+
} from "../../renderer/particles/shaders/shader_particle_simulate.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The scene the particle playground draws, with every reference to the page removed: it takes a
|
|
33
|
+
* {@link GraphicsContext} and a colour target, and encodes one frame into a command context.
|
|
34
|
+
*
|
|
35
|
+
* Separate from `main.js` so the whole frame — every pass, binding, buffer usage and immediate
|
|
36
|
+
* block — can be encoded against {@link ../../device/mock/SoftwareGPUDevice.js} in a vitest spec
|
|
37
|
+
* (see `particle_scene.spec.js`). That is the only tier available to CI here; the mock validates
|
|
38
|
+
* the WebGPU API surface but executes no shaders, so what a spec can prove is that the frame is
|
|
39
|
+
* *legal*, not what it looks like. The picture is the browser's job.
|
|
40
|
+
*
|
|
41
|
+
* What the scene contains:
|
|
42
|
+
*
|
|
43
|
+
* - lit solid geometry (a ground plane + boxes) rendered into a reverse-Z depth buffer,
|
|
44
|
+
* - four emitters with distinct settings — an additive fire, an alpha soft-depth smoke column,
|
|
45
|
+
* additive velocity-stretched sparks, and a wide soft-depth dust drift,
|
|
46
|
+
* - a billboard pass that depth-tests against the scene, so particles are occluded by the boxes and
|
|
47
|
+
* soft-depth emitters fade as they approach surfaces.
|
|
48
|
+
*
|
|
49
|
+
* The loop is fully GPU-driven: the simulate dispatch runs via `dispatchWorkgroupsIndirect` and the
|
|
50
|
+
* billboard draw via `drawIndirect`, both from args the build-indirect pass writes on the GPU. No
|
|
51
|
+
* CPU readback participates in sizing anything — a stale CPU count would orphan alive-list entries
|
|
52
|
+
* when the population grows and double-free slots when it shrinks.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** Maximum simultaneous particles across every emitter — they share one pool. */
|
|
56
|
+
const PARTICLE_CAPACITY = 1 << 15; // 32k
|
|
57
|
+
|
|
58
|
+
/** Camera near plane. Also the numerator of the reverse-Z depth linearisation (see {@link write_camera}). */
|
|
59
|
+
const PARTICLE_NEAR = 0.2;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The largest immediate block any of the three compute passes declares (emit's 20-byte settings
|
|
63
|
+
* struct). Immediate data is an optional WebGPU capability, so callers check for it up front rather
|
|
64
|
+
* than letting pipeline creation fail with an error that names neither pass nor feature.
|
|
65
|
+
*/
|
|
66
|
+
export const PARTICLE_IMMEDIATE_BYTES = 20;
|
|
67
|
+
|
|
68
|
+
/** Bytes of stats {@link ParticleScene#encode_stats_copy} writes: 8 counters, then both arg buffers. */
|
|
69
|
+
export const PARTICLE_STATS_BYTES = 64;
|
|
70
|
+
|
|
71
|
+
const STATS_OFFSET_DISPATCH_ARGS = 32;
|
|
72
|
+
const STATS_OFFSET_DRAW_ARGS = 48;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {object} ParticleSceneStats
|
|
76
|
+
* @property {number} alive particles alive at the end of the captured frame
|
|
77
|
+
* @property {number} dead free slots left in the pool
|
|
78
|
+
* @property {number} alive_in the snapshot next frame's simulate reads as its input length
|
|
79
|
+
* @property {number[]} dispatch simulate's indirect dispatch args, `[x, y, z]` workgroups
|
|
80
|
+
* @property {number[]} draw the billboard draw's indirect args, `[vertices, instances]`
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build the scene against a live (or mock) device.
|
|
85
|
+
*
|
|
86
|
+
* @param {GraphicsContext} graphics
|
|
87
|
+
* @param {object} options
|
|
88
|
+
* @param {GPUTextureFormat} options.color_format format of the colour target frames are encoded into
|
|
89
|
+
* @param {number} [options.capacity] particle pool size
|
|
90
|
+
* @returns {object} the scene, see the returned object's own members
|
|
91
|
+
*/
|
|
92
|
+
export function create_particle_scene(graphics, { color_format, capacity = PARTICLE_CAPACITY }) {
|
|
93
|
+
const device = graphics.device;
|
|
94
|
+
|
|
95
|
+
// --- emitters: four, each with different settings -----------------------------------------
|
|
96
|
+
const system = new GPUParticleSystem(graphics, { capacity });
|
|
97
|
+
|
|
98
|
+
// fire — additive, billboard, at the base of the tall pillar
|
|
99
|
+
system.addEmitter({
|
|
100
|
+
...fire_effect().emitter,
|
|
101
|
+
spawn_rate: 500, flipbook: [1, 1], atlas_region: [0, 0, 1, 1],
|
|
102
|
+
flags: { blend: EMITTER_BLEND.ADDITIVE, projection: EMITTER_PROJECTION.BILLBOARD },
|
|
103
|
+
transform: translation(-1, 0, 0),
|
|
104
|
+
});
|
|
105
|
+
// smoke — alpha, soft-depth, rising column above the fire
|
|
106
|
+
system.addEmitter({
|
|
107
|
+
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] }),
|
|
108
|
+
spawn_rate: 60, atlas_region: [0, 0, 1, 1], flipbook: [1, 1],
|
|
109
|
+
flags: { blend: EMITTER_BLEND.ALPHA, projection: EMITTER_PROJECTION.BILLBOARD, soft_depth: true },
|
|
110
|
+
render: { position: "position", size: "size", color: "color" },
|
|
111
|
+
transform: translation(-1, 2.5, 0),
|
|
112
|
+
});
|
|
113
|
+
// sparks — additive, velocity-stretched, fast, short-lived, gravity, near the box
|
|
114
|
+
system.addEmitter({
|
|
115
|
+
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] }),
|
|
116
|
+
spawn_rate: 350, atlas_region: [0, 0, 1, 1], flipbook: [1, 1],
|
|
117
|
+
flags: { blend: EMITTER_BLEND.ADDITIVE, projection: EMITTER_PROJECTION.STRETCHED },
|
|
118
|
+
render: { position: "position", size: "size", color: "color", velocity: "velocity" },
|
|
119
|
+
transform: translation(2, 1.9, -1),
|
|
120
|
+
});
|
|
121
|
+
// dust — alpha, soft-depth, slow wide drift across the ground
|
|
122
|
+
system.addEmitter({
|
|
123
|
+
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] }),
|
|
124
|
+
spawn_rate: 120, atlas_region: [0, 0, 1, 1], flipbook: [1, 1],
|
|
125
|
+
flags: { blend: EMITTER_BLEND.ALPHA, projection: EMITTER_PROJECTION.BILLBOARD, soft_depth: true },
|
|
126
|
+
render: { position: "position", size: "size", color: "color" },
|
|
127
|
+
transform: translation(0, 0.3, 0),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const built = system.buildEmitterData();
|
|
131
|
+
|
|
132
|
+
// --- particle GPU buffers -----------------------------------------------------------------
|
|
133
|
+
const storage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
|
134
|
+
const pool = device.createBuffer({ label: "particles/pool", size: capacity * PARTICLE_RECORD_WORD_COUNT * 4, usage: storage });
|
|
135
|
+
const alive_a = device.createBuffer({ label: "particles/alive_a", size: capacity * 4, usage: storage });
|
|
136
|
+
const alive_b = device.createBuffer({ label: "particles/alive_b", size: capacity * 4, usage: storage });
|
|
137
|
+
const dead = device.createBuffer({ label: "particles/dead", size: capacity * 4, usage: storage });
|
|
138
|
+
const counters = device.createBuffer({ label: "particles/counters", size: PARTICLE_COUNTER_COUNT * 4, usage: storage });
|
|
139
|
+
const spawn_requests = device.createBuffer({ label: "particles/spawn_requests", size: capacity * 4, usage: storage });
|
|
140
|
+
const emitters = data_buffer(device, "particles/emitters", built.emitters, storage);
|
|
141
|
+
const program = data_buffer(device, "particles/program", built.program, storage);
|
|
142
|
+
const constants = data_buffer(device, "particles/constants", built.constants, storage);
|
|
143
|
+
|
|
144
|
+
// Indirect args, written on-GPU by the build-indirect pass. Zero-initialised (WebGPU spec), so
|
|
145
|
+
// the first frame's indirect dispatch/draw are no-ops until the first snapshot lands. COPY_SRC
|
|
146
|
+
// only so the stats copy can *display* them; nothing read back here sizes a pass.
|
|
147
|
+
const args_usage = GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_SRC;
|
|
148
|
+
const dispatch_args = device.createBuffer({ label: "particles/dispatch_args", size: 3 * 4, usage: args_usage });
|
|
149
|
+
const draw_args = device.createBuffer({ label: "particles/draw_args", size: 4 * 4, usage: args_usage });
|
|
150
|
+
|
|
151
|
+
const initial = system.initialFreeList();
|
|
152
|
+
device.queue.writeBuffer(dead, 0, initial.dead);
|
|
153
|
+
device.queue.writeBuffer(counters, 0, initial.counters);
|
|
154
|
+
|
|
155
|
+
// --- camera + geometry --------------------------------------------------------------------
|
|
156
|
+
const camera_buffer = device.createBuffer({ label: "particles/camera", size: CAMERA_UNIFORM_STRUCT.aligned_size, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
157
|
+
const vp_buffer = device.createBuffer({ label: "particles/vp", size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
158
|
+
|
|
159
|
+
const geometry = build_geometry();
|
|
160
|
+
const geometry_buffer = device.createBuffer({ label: "particles/geometry", size: geometry.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
161
|
+
device.queue.writeBuffer(geometry_buffer, 0, geometry);
|
|
162
|
+
const geometry_vertex_count = geometry.length / GEOMETRY_FLOATS_PER_VERTEX;
|
|
163
|
+
const { geometry_pipeline, geometry_bind_group } = make_geometry_pipeline(device, color_format, vp_buffer);
|
|
164
|
+
|
|
165
|
+
// atlas sprite — soft round falloff — and the filtering sampler the billboard shader declares
|
|
166
|
+
const atlas_view = make_sprite_texture(device).createView();
|
|
167
|
+
const atlas_sampler = device.createSampler({ magFilter: "linear", minFilter: "linear", addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge" });
|
|
168
|
+
|
|
169
|
+
// billboard pipeline: reverse-Z depth test (same compare as the geometry pass), no write;
|
|
170
|
+
// soft-depth reads the scene depth texture
|
|
171
|
+
const billboard_pipeline = create_particle_billboard_pipeline({
|
|
172
|
+
color_format,
|
|
173
|
+
depth_format: DEPTH_FORMAT,
|
|
174
|
+
depth_compare: "greater",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// reverse-Z depth buffer, recreated on resize
|
|
178
|
+
let depth_texture = null;
|
|
179
|
+
let depth_view = null;
|
|
180
|
+
let depth_width = 0;
|
|
181
|
+
let depth_height = 0;
|
|
182
|
+
|
|
183
|
+
function ensure_depth(width, height) {
|
|
184
|
+
if (depth_view !== null && depth_width === width && depth_height === height) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
depth_texture?.destroy();
|
|
189
|
+
depth_texture = device.createTexture({
|
|
190
|
+
label: "particles/scene_depth",
|
|
191
|
+
size: [width, height],
|
|
192
|
+
format: DEPTH_FORMAT,
|
|
193
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
194
|
+
});
|
|
195
|
+
depth_view = depth_texture.createView();
|
|
196
|
+
depth_width = width;
|
|
197
|
+
depth_height = height;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let alive_read = alive_a;
|
|
201
|
+
let alive_write = alive_b;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
system,
|
|
205
|
+
capacity,
|
|
206
|
+
|
|
207
|
+
/** Buffers a harness may inspect. Not for the frame loop, which never reads them back. */
|
|
208
|
+
buffers: { pool, alive_a, alive_b, dead, counters, spawn_requests, emitters, program, constants, dispatch_args, draw_args },
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Encode one whole frame: geometry, then emit + simulate + build-indirect, then the
|
|
212
|
+
* billboard draw. Does not submit — the caller finishes the context.
|
|
213
|
+
*
|
|
214
|
+
* @param {ShadeGPUCommandContext} cmd
|
|
215
|
+
* @param {object} frame
|
|
216
|
+
* @param {GPUTextureView} frame.color_view where the frame is drawn
|
|
217
|
+
* @param {number} frame.width colour target width, in texels
|
|
218
|
+
* @param {number} frame.height colour target height, in texels
|
|
219
|
+
* @param {number} frame.dt seconds since the previous frame
|
|
220
|
+
* @param {number} frame.time seconds since start, for the camera orbit and the VM's clock
|
|
221
|
+
*/
|
|
222
|
+
encode_frame(cmd, { color_view, width, height, dt, time }) {
|
|
223
|
+
ensure_depth(width, height);
|
|
224
|
+
write_camera(device, camera_buffer, vp_buffer, width / height, time);
|
|
225
|
+
|
|
226
|
+
const requests = system.update(dt);
|
|
227
|
+
if (requests.length > 0) {
|
|
228
|
+
device.queue.writeBuffer(spawn_requests, 0, requests);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- geometry pass: clear colour + reverse-Z depth (far = 0), lit, writes depth ---
|
|
232
|
+
const geometry_pass = cmd.beginRenderPass({
|
|
233
|
+
label: "particles/geometry",
|
|
234
|
+
colorAttachments: [{ view: color_view, loadOp: "clear", storeOp: "store", clearValue: { r: 0.03, g: 0.035, b: 0.05, a: 1 } }],
|
|
235
|
+
depthStencilAttachment: { view: depth_view, depthLoadOp: "clear", depthStoreOp: "store", depthClearValue: 0.0 },
|
|
236
|
+
});
|
|
237
|
+
geometry_pass.setPipeline(geometry_pipeline);
|
|
238
|
+
geometry_pass.setBindGroup(0, geometry_bind_group);
|
|
239
|
+
geometry_pass.setVertexBuffer(0, geometry_buffer);
|
|
240
|
+
geometry_pass.draw(geometry_vertex_count);
|
|
241
|
+
geometry_pass.end();
|
|
242
|
+
|
|
243
|
+
// --- emit + simulate (simulate is sized by LAST frame's GPU-written dispatch args) ---
|
|
244
|
+
shader_particle_reset_counters.dispatch({ encoder: cmd, bindings: { counters }, group_count_x: 1 });
|
|
245
|
+
|
|
246
|
+
if (requests.length > 0) {
|
|
247
|
+
shader_particle_emit.dispatchThreads({
|
|
248
|
+
encoder: cmd,
|
|
249
|
+
bindings: {
|
|
250
|
+
emitters, program, vm_constants: constants, pool, spawn_requests,
|
|
251
|
+
alive_out: alive_write, dead, counters,
|
|
252
|
+
settings: { u_dt: dt, u_time: time, u_frame: system.frame, u_count: requests.length, u_capacity: capacity },
|
|
253
|
+
},
|
|
254
|
+
thread_count_x: requests.length,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
shader_particle_simulate.dispatchIndirect({
|
|
259
|
+
encoder: cmd,
|
|
260
|
+
bindings: {
|
|
261
|
+
emitters, program, vm_constants: constants, pool,
|
|
262
|
+
alive_in: alive_read, alive_out: alive_write, dead, counters,
|
|
263
|
+
settings: { u_dt: dt, u_time: time, u_frame: system.frame },
|
|
264
|
+
},
|
|
265
|
+
command: dispatch_args,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Snapshot the final alive count (next frame's simulate input) + write this frame's draw args.
|
|
269
|
+
shader_particle_build_indirect.dispatch({
|
|
270
|
+
encoder: cmd,
|
|
271
|
+
bindings: { counters, dispatch_args, draw_args, u_workgroup_size: PARTICLE_SIMULATE_WORKGROUP_SIZE },
|
|
272
|
+
group_count_x: 1,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// --- billboard pass: load colour, depth-test against the scene (read-only), indirect draw ---
|
|
276
|
+
const bindings = PARTICLE_BILLBOARD_RESOURCES.generateBindings({
|
|
277
|
+
camera: camera_buffer, pool, sorted: alive_write, emitters,
|
|
278
|
+
atlas: atlas_view, atlas_sampler, scene_depth: depth_view,
|
|
279
|
+
}, cmd);
|
|
280
|
+
|
|
281
|
+
const billboard_pass = cmd.constructRenderPass({
|
|
282
|
+
pipeline: billboard_pipeline,
|
|
283
|
+
bindings,
|
|
284
|
+
colorAttachments: [{ view: color_view, loadOp: "load", storeOp: "store" }],
|
|
285
|
+
depthStencilAttachment: { view: depth_view, depthReadOnly: true },
|
|
286
|
+
});
|
|
287
|
+
billboard_pass.drawIndirect(draw_args, 0);
|
|
288
|
+
billboard_pass.end();
|
|
289
|
+
|
|
290
|
+
const previous = alive_read;
|
|
291
|
+
alive_read = alive_write;
|
|
292
|
+
alive_write = previous;
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Copy the counters and both indirect arg buffers into a {@link PARTICLE_STATS_BYTES}-byte
|
|
297
|
+
* MAP_READ destination, for display. Read back to *look at*: nothing here may size a pass.
|
|
298
|
+
*
|
|
299
|
+
* @param {ShadeGPUCommandContext} cmd
|
|
300
|
+
* @param {GPUBuffer} destination
|
|
301
|
+
*/
|
|
302
|
+
encode_stats_copy(cmd, destination) {
|
|
303
|
+
cmd.copyBufferToBuffer(counters, 0, destination, 0, PARTICLE_COUNTER_COUNT * 4);
|
|
304
|
+
cmd.copyBufferToBuffer(dispatch_args, 0, destination, STATS_OFFSET_DISPATCH_ARGS, 3 * 4);
|
|
305
|
+
cmd.copyBufferToBuffer(draw_args, 0, destination, STATS_OFFSET_DRAW_ARGS, 4 * 4);
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Interpret the bytes {@link encode_stats_copy} wrote.
|
|
310
|
+
*
|
|
311
|
+
* @param {ArrayBuffer} bytes
|
|
312
|
+
* @returns {ParticleSceneStats}
|
|
313
|
+
*/
|
|
314
|
+
read_stats(bytes) {
|
|
315
|
+
const words = new Uint32Array(bytes);
|
|
316
|
+
const dispatch_base = STATS_OFFSET_DISPATCH_ARGS / 4;
|
|
317
|
+
const draw_base = STATS_OFFSET_DRAW_ARGS / 4;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
alive: words[0],
|
|
321
|
+
dead: words[1],
|
|
322
|
+
alive_in: words[4],
|
|
323
|
+
dispatch: [words[dispatch_base], words[dispatch_base + 1], words[dispatch_base + 2]],
|
|
324
|
+
draw: [words[draw_base], words[draw_base + 1]],
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------------------------
|
|
331
|
+
// Effect authoring
|
|
332
|
+
// ---------------------------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* A parametric point-emission effect: emit with a base velocity + jitter from a disk, integrate
|
|
336
|
+
* under a vertical acceleration, and die at end of life. Only nAge.x is consumed (kill), so the
|
|
337
|
+
* NaN lanes from the scalar age/lifetime divide are harmless.
|
|
338
|
+
*/
|
|
339
|
+
function make_effect({ velocity, velocityJitter = 0, posJitter = 0, accel = 0, life = [1, 2], size = 1, color = [1, 1, 1, 1] }) {
|
|
340
|
+
const layout = new ParticleLayout([
|
|
341
|
+
{ name: "position", components: 3 }, { name: "velocity", components: 3 },
|
|
342
|
+
{ name: "age", components: 1 }, { name: "lifetime", components: 1 },
|
|
343
|
+
{ name: "size", components: 1 }, { name: "color", components: 4 },
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
const spawn = new NodeGraph();
|
|
347
|
+
let posN = particle_node(spawn, "builtin", { id: VM_BUILTIN.EMITTER_POSITION });
|
|
348
|
+
if (posJitter > 0) {
|
|
349
|
+
const d = particle_node(spawn, "random", { mode: VM_RANDOM_MODE.UNIT_DISK });
|
|
350
|
+
const j = particle_node(spawn, "scale"); particle_wire(spawn, j, "v", d); particle_wire(spawn, j, "s", [posJitter]);
|
|
351
|
+
const p = particle_node(spawn, "add"); particle_wire(spawn, p, "a", posN); particle_wire(spawn, p, "b", j); posN = p;
|
|
352
|
+
}
|
|
353
|
+
particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "position" }), "value", posN);
|
|
354
|
+
|
|
355
|
+
let velN = particle_node(spawn, "const", { value: [velocity[0], velocity[1], velocity[2], 0] });
|
|
356
|
+
if (velocityJitter > 0) {
|
|
357
|
+
const r = particle_node(spawn, "random", { mode: VM_RANDOM_MODE.UNIFORM_SIGNED });
|
|
358
|
+
const rs = particle_node(spawn, "scale"); particle_wire(spawn, rs, "v", r); particle_wire(spawn, rs, "s", [velocityJitter]);
|
|
359
|
+
const vv = particle_node(spawn, "add"); particle_wire(spawn, vv, "a", velN); particle_wire(spawn, vv, "b", rs); velN = vv;
|
|
360
|
+
}
|
|
361
|
+
particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "velocity" }), "value", velN);
|
|
362
|
+
particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "age" }), "value", [0]);
|
|
363
|
+
const lr = particle_node(spawn, "random", { mode: VM_RANDOM_MODE.UNIFORM });
|
|
364
|
+
const lm = particle_node(spawn, "mad"); particle_wire(spawn, lm, "a", lr); particle_wire(spawn, lm, "b", [life[1] - life[0]]); particle_wire(spawn, lm, "c", [life[0]]);
|
|
365
|
+
particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "lifetime" }), "value", lm);
|
|
366
|
+
particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "size" }), "value", [size]);
|
|
367
|
+
particle_wire(spawn, particle_node(spawn, "setAttribute", { name: "color" }), "value", color);
|
|
368
|
+
|
|
369
|
+
const update = new NodeGraph();
|
|
370
|
+
const dt = particle_node(update, "builtin", { id: VM_BUILTIN.DELTA_TIME });
|
|
371
|
+
const age = particle_node(update, "attribute", { name: "age" });
|
|
372
|
+
const nage = particle_node(update, "add"); particle_wire(update, nage, "a", age); particle_wire(update, nage, "b", dt);
|
|
373
|
+
particle_wire(update, particle_node(update, "setAttribute", { name: "age" }), "value", nage);
|
|
374
|
+
const lifeA = particle_node(update, "attribute", { name: "lifetime" });
|
|
375
|
+
const na = particle_node(update, "div"); particle_wire(update, na, "a", nage); particle_wire(update, na, "b", lifeA);
|
|
376
|
+
const dead = particle_node(update, "compare", { op: "ge" }); particle_wire(update, dead, "a", na); particle_wire(update, dead, "b", [1]);
|
|
377
|
+
particle_wire(update, particle_node(update, "kill"), "condition", dead);
|
|
378
|
+
let velN2 = particle_node(update, "attribute", { name: "velocity" });
|
|
379
|
+
if (accel !== 0) {
|
|
380
|
+
const g = particle_node(update, "const", { value: [0, accel, 0, 0] });
|
|
381
|
+
const gd = particle_node(update, "scale"); particle_wire(update, gd, "v", g); particle_wire(update, gd, "s", dt);
|
|
382
|
+
const nv = particle_node(update, "add"); particle_wire(update, nv, "a", velN2); particle_wire(update, nv, "b", gd);
|
|
383
|
+
particle_wire(update, particle_node(update, "setAttribute", { name: "velocity" }), "value", nv); velN2 = nv;
|
|
384
|
+
}
|
|
385
|
+
const pos = particle_node(update, "attribute", { name: "position" });
|
|
386
|
+
const pd = particle_node(update, "scale"); particle_wire(update, pd, "v", velN2); particle_wire(update, pd, "s", dt);
|
|
387
|
+
const np = particle_node(update, "add"); particle_wire(update, np, "a", pos); particle_wire(update, np, "b", pd);
|
|
388
|
+
particle_wire(update, particle_node(update, "setAttribute", { name: "position" }), "value", np);
|
|
389
|
+
|
|
390
|
+
return create_particle_effect({ layout, spawn, update });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---------------------------------------------------------------------------------------------
|
|
394
|
+
// Geometry + camera + helpers
|
|
395
|
+
// ---------------------------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
const DEPTH_FORMAT = "depth32float";
|
|
398
|
+
|
|
399
|
+
/** Interleaved `[x, y, z, nx, ny, nz]`. */
|
|
400
|
+
const GEOMETRY_FLOATS_PER_VERTEX = 6;
|
|
401
|
+
|
|
402
|
+
const GEOM_WGSL = /* wgsl */ `
|
|
403
|
+
@group(0) @binding(0) var<uniform> vp : mat4x4<f32>;
|
|
404
|
+
struct VOut { @builtin(position) clip : vec4<f32>, @location(0) normal : vec3<f32> }
|
|
405
|
+
@vertex fn vs(@location(0) pos : vec3<f32>, @location(1) normal : vec3<f32>) -> VOut {
|
|
406
|
+
var o : VOut;
|
|
407
|
+
o.clip = vp * vec4<f32>(pos, 1.0);
|
|
408
|
+
o.normal = normal;
|
|
409
|
+
return o;
|
|
410
|
+
}
|
|
411
|
+
@fragment fn fs(in : VOut) -> @location(0) vec4<f32> {
|
|
412
|
+
let N = normalize(in.normal);
|
|
413
|
+
let L = normalize(vec3<f32>(0.35, 1.0, 0.4));
|
|
414
|
+
let diffuse = max(dot(N, L), 0.0);
|
|
415
|
+
let albedo = select(vec3<f32>(0.30, 0.32, 0.36), vec3<f32>(0.22, 0.24, 0.28), N.y < 0.9);
|
|
416
|
+
let color = albedo * (0.18 + diffuse * 0.9);
|
|
417
|
+
return vec4<f32>(color, 1.0);
|
|
418
|
+
}
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
function make_geometry_pipeline(device, format, vp_buffer) {
|
|
422
|
+
const module = device.createShaderModule({ label: "particles/geometry", code: GEOM_WGSL });
|
|
423
|
+
const layout = device.createBindGroupLayout({
|
|
424
|
+
entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }],
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const geometry_pipeline = device.createRenderPipeline({
|
|
428
|
+
label: "particles/geometry",
|
|
429
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
430
|
+
vertex: {
|
|
431
|
+
module,
|
|
432
|
+
entryPoint: "vs",
|
|
433
|
+
buffers: [{
|
|
434
|
+
arrayStride: GEOMETRY_FLOATS_PER_VERTEX * 4,
|
|
435
|
+
attributes: [
|
|
436
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
437
|
+
{ shaderLocation: 1, offset: 12, format: "float32x3" },
|
|
438
|
+
],
|
|
439
|
+
}],
|
|
440
|
+
},
|
|
441
|
+
fragment: { module, entryPoint: "fs", targets: [{ format }] },
|
|
442
|
+
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
443
|
+
depthStencil: { format: DEPTH_FORMAT, depthWriteEnabled: true, depthCompare: "greater" }, // reverse-Z
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const geometry_bind_group = device.createBindGroup({ layout, entries: [{ binding: 0, resource: { buffer: vp_buffer } }] });
|
|
447
|
+
|
|
448
|
+
return { geometry_pipeline, geometry_bind_group };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Ground plane + a few boxes as an interleaved `[x,y,z, nx,ny,nz]` vertex array. */
|
|
452
|
+
function build_geometry() {
|
|
453
|
+
const v = [];
|
|
454
|
+
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]); };
|
|
455
|
+
const box = (cx, cy, cz, sx, sy, sz) => {
|
|
456
|
+
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;
|
|
457
|
+
quad([x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1], [0, 0, 1]);
|
|
458
|
+
quad([x1, y0, z0], [x0, y0, z0], [x0, y1, z0], [x1, y1, z0], [0, 0, -1]);
|
|
459
|
+
quad([x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x1, y1, z1], [1, 0, 0]);
|
|
460
|
+
quad([x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0], [-1, 0, 0]);
|
|
461
|
+
quad([x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0], [0, 1, 0]);
|
|
462
|
+
quad([x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1], [0, -1, 0]);
|
|
463
|
+
};
|
|
464
|
+
quad([-15, 0, -15], [15, 0, -15], [15, 0, 15], [-15, 0, 15], [0, 1, 0]); // ground
|
|
465
|
+
box(-1, 1.5, 0, 1, 3, 1); // tall pillar next to the fire
|
|
466
|
+
box(2, 0.9, -1, 1.6, 1.8, 1.6); // block near the sparks
|
|
467
|
+
box(0, 0.6, 3.5, 7, 1.2, 0.6); // low wall the dust drifts against
|
|
468
|
+
return new Float32Array(v);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Scratch for write_camera. One scene drives one camera, and the loop runs every frame, so these
|
|
472
|
+
// are reused rather than reallocated 60 times a second.
|
|
473
|
+
const camera_view = m4_allocate();
|
|
474
|
+
const camera_world = m4_allocate();
|
|
475
|
+
const camera_projection = m4_allocate();
|
|
476
|
+
const camera_view_projection = m4_allocate();
|
|
477
|
+
const camera_bytes = new Float32Array(CAMERA_UNIFORM_STRUCT.aligned_size / 4);
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Reverse-Z, infinite-far perspective (WebGPU 0..1 depth: near -> 1, far -> 0).
|
|
481
|
+
*
|
|
482
|
+
* @param {Float32Array} out column-major 4x4
|
|
483
|
+
* @param {number} fov_y vertical field of view, radians
|
|
484
|
+
* @param {number} aspect width / height
|
|
485
|
+
* @param {number} near near plane distance
|
|
486
|
+
* @returns {Float32Array} `out`
|
|
487
|
+
*/
|
|
488
|
+
function perspective_reverse_z(out, fov_y, aspect, near) {
|
|
489
|
+
const f = 1 / Math.tan(fov_y / 2);
|
|
490
|
+
|
|
491
|
+
out.fill(0);
|
|
492
|
+
out[0] = f / aspect;
|
|
493
|
+
out[5] = f;
|
|
494
|
+
out[11] = -1;
|
|
495
|
+
out[14] = near;
|
|
496
|
+
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Orbit the camera and upload both camera buffers: the engine's full camera struct for the
|
|
502
|
+
* billboard shader, and a bare view-projection for the geometry pass.
|
|
503
|
+
*/
|
|
504
|
+
function write_camera(device, camera_buffer, vp_buffer, aspect, t) {
|
|
505
|
+
const radius = 12, height = 5.5;
|
|
506
|
+
const eye = [Math.cos(t * 0.22) * radius, height, Math.sin(t * 0.22) * radius];
|
|
507
|
+
const center = [0, 1.6, 0];
|
|
508
|
+
|
|
509
|
+
const view = m4_look_at(camera_view, eye, center, [0, 1, 0]);
|
|
510
|
+
// A look-at basis is orthonormal plus a translation, so this never reports singular.
|
|
511
|
+
m4_invert(camera_world, view);
|
|
512
|
+
const world = camera_world;
|
|
513
|
+
const projection = perspective_reverse_z(camera_projection, (58 * Math.PI) / 180, aspect, PARTICLE_NEAR);
|
|
514
|
+
const view_projection = m4_multiply(camera_view_projection, projection, view);
|
|
515
|
+
|
|
516
|
+
const put = (field, m) => camera_bytes.set(m, CAMERA_UNIFORM_STRUCT.get(field).offset / 4);
|
|
517
|
+
put("transform", world);
|
|
518
|
+
put("view_matrix", view);
|
|
519
|
+
put("projection_matrix", projection);
|
|
520
|
+
put("view_projection_matrix", view_projection);
|
|
521
|
+
// Reverse-Z infinite-far linearisation: view_depth = A / (device_depth + B), with A=near, B=0.
|
|
522
|
+
camera_bytes.set([0, PARTICLE_NEAR, 0, 0], CAMERA_UNIFORM_STRUCT.get("device_depth_to_view_space").offset / 4);
|
|
523
|
+
|
|
524
|
+
device.queue.writeBuffer(camera_buffer, 0, camera_bytes);
|
|
525
|
+
device.queue.writeBuffer(vp_buffer, 0, view_projection);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function translation(x, y, z) {
|
|
529
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function data_buffer(device, label, data, usage) {
|
|
533
|
+
const buffer = device.createBuffer({ label, size: Math.max(data.byteLength, 4), usage });
|
|
534
|
+
device.queue.writeBuffer(buffer, 0, data);
|
|
535
|
+
return buffer;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** A 64x64 radial-falloff sprite (white, alpha falloff) for soft additive/alpha blobs. */
|
|
539
|
+
function make_sprite_texture(device) {
|
|
540
|
+
const N = 64;
|
|
541
|
+
const px = new Uint8Array(N * N * 4);
|
|
542
|
+
for (let y = 0; y < N; y++) {
|
|
543
|
+
for (let x = 0; x < N; x++) {
|
|
544
|
+
const dx = (x + 0.5) / N * 2 - 1, dy = (y + 0.5) / N * 2 - 1;
|
|
545
|
+
const a = Math.max(0, 1 - Math.hypot(dx, dy));
|
|
546
|
+
const i = (y * N + x) * 4;
|
|
547
|
+
px[i] = 255; px[i + 1] = 255; px[i + 2] = 255; px[i + 3] = Math.round(a * a * 255);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const tex = device.createTexture({ label: "particles/sprite", size: [N, N], format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
|
|
551
|
+
device.queue.writeTexture({ texture: tex }, px, { bytesPerRow: N * 4, rowsPerImage: N }, [N, N]);
|
|
552
|
+
return tex;
|
|
553
|
+
}
|