@aethervtt/shard-particles 0.0.1 → 0.2.0
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/dist/plugin.js +10 -2
- package/dist/preview-render.js +1 -0
- package/dist/shaders.js +2 -2
- package/dist/sim.d.ts +15 -5
- package/dist/sim.js +172 -56
- package/package.json +10 -10
package/dist/plugin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Last } from "@aethervtt/shard-core";
|
|
2
|
-
import { Graph, RenderDescribers, RenderSet } from "@aethervtt/shard-render";
|
|
2
|
+
import { addRenderFeatures, Graph, RenderDescribers, RenderSet } from "@aethervtt/shard-render";
|
|
3
3
|
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
4
4
|
import { OriginShift } from "@aethervtt/shard-transform";
|
|
5
5
|
import * as componentsModule from "./components.js";
|
|
@@ -29,7 +29,15 @@ const particlesPlugin = definePlugin({
|
|
|
29
29
|
},
|
|
30
30
|
ready(app) {
|
|
31
31
|
const graph = app.world.resource(Graph);
|
|
32
|
-
|
|
32
|
+
addRenderFeatures(app.world, {
|
|
33
|
+
name: "particles",
|
|
34
|
+
description: "Particle simulation, depth sort and drawing.",
|
|
35
|
+
nodes: ["particles/simulate", "particles"],
|
|
36
|
+
baseline: {
|
|
37
|
+
strategy: "CPU simulation and depth sort for every system; particles in data textures"
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
graph.addNode("particles/simulate", simulateNode(app.world));
|
|
33
41
|
graph.addNode("particles", drawNode());
|
|
34
42
|
app.world.initResource(RenderDescribers).set("particles", (w) => describeParticles(w));
|
|
35
43
|
}
|
package/dist/preview-render.js
CHANGED
package/dist/shaders.js
CHANGED
|
@@ -208,9 +208,9 @@ struct Draw {
|
|
|
208
208
|
mode: vec4u,
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
@group(1) @binding(0) var<storage, read> particles: array<Particle>;
|
|
211
|
+
@data @group(1) @binding(0) var<storage, read> particles: array<Particle>;
|
|
212
212
|
@group(1) @binding(1) var<uniform> draw: Draw;
|
|
213
|
-
@group(1) @binding(2) var<storage, read> order: array<u32>;
|
|
213
|
+
@data @group(1) @binding(2) var<storage, read> order: array<u32>;
|
|
214
214
|
@group(2) @binding(0) var sprite: texture_2d<f32>;
|
|
215
215
|
@group(2) @binding(1) var sprite_sampler: sampler;
|
|
216
216
|
@group(2) @binding(2) var scene_depth: texture_depth_2d;
|
package/dist/sim.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
2
|
import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
|
|
3
|
-
import { type CameraData, type NodeDescriptor } from '@aethervtt/shard-render';
|
|
3
|
+
import { type CameraData, DataStore, type NodeDescriptor } from '@aethervtt/shard-render';
|
|
4
4
|
import { type EmitterDef, type ParticleEffect } from './effect.js';
|
|
5
5
|
/** Alpha emitters up to this capacity sort by depth; larger ones draw unsorted. */
|
|
6
6
|
export declare const MAX_SORTED = 65536;
|
|
@@ -9,9 +9,14 @@ export interface EmitterState {
|
|
|
9
9
|
def: EmitterDef;
|
|
10
10
|
key: string;
|
|
11
11
|
capacity: number;
|
|
12
|
-
|
|
12
|
+
/** Storage on the full tier, a data texture on baseline (0064). */
|
|
13
|
+
particles: DataStore;
|
|
13
14
|
/** CPU backend: the particles, uploaded whole each frame. */
|
|
14
15
|
cpu: Float32Array | undefined;
|
|
16
|
+
/** `cpu` as u32 (seeds). */
|
|
17
|
+
cpuBits: Uint32Array | undefined;
|
|
18
|
+
/** CPU backend, sorted emitters: order-preserving key bits and slot, packed per particle. */
|
|
19
|
+
cpuSort: Float64Array | undefined;
|
|
15
20
|
sim: GpuBuffer;
|
|
16
21
|
draw: GpuBuffer;
|
|
17
22
|
counter: GPUBuffer;
|
|
@@ -29,10 +34,12 @@ export interface EmitterState {
|
|
|
29
34
|
backlog: number;
|
|
30
35
|
/** Alive particles from the last readback (a frame or two late). */
|
|
31
36
|
alive: number;
|
|
37
|
+
/** System time of the last spawn: its particles live until lifetime max after it. */
|
|
38
|
+
lastSpawn: number;
|
|
32
39
|
/** Sorting (alpha, small enough): keys, order, and per-step uniforms. */
|
|
33
40
|
sort: {
|
|
34
41
|
keys: GPUBuffer;
|
|
35
|
-
order:
|
|
42
|
+
order: DataStore;
|
|
36
43
|
steps: GPUBuffer;
|
|
37
44
|
count: number;
|
|
38
45
|
n: number;
|
|
@@ -56,6 +63,8 @@ export interface SystemState {
|
|
|
56
63
|
seed: number;
|
|
57
64
|
local: boolean;
|
|
58
65
|
cpu: boolean;
|
|
66
|
+
/** The CPU backend because the device runs the baseline tier (0064), not by choice. */
|
|
67
|
+
forced: boolean;
|
|
59
68
|
model: Float32Array;
|
|
60
69
|
/** Bounding sphere (world) of the system, for culling. */
|
|
61
70
|
center: Float32Array;
|
|
@@ -103,12 +112,12 @@ interface Caches {
|
|
|
103
112
|
}>;
|
|
104
113
|
white?: GPUTexture;
|
|
105
114
|
noDepth?: GPUTexture;
|
|
106
|
-
noOrder?:
|
|
115
|
+
noOrder?: DataStore;
|
|
107
116
|
sampler?: GPUSampler;
|
|
108
117
|
}
|
|
109
118
|
export declare const ParticleCaches: import("@aethervtt/shard-core").ResourceDef<Caches>;
|
|
110
119
|
/** Spawn, update, and (alpha emitters) depth sort, for the primary camera's view. */
|
|
111
|
-
export declare function simulateNode(): NodeDescriptor;
|
|
120
|
+
export declare function simulateNode(world: World): NodeDescriptor;
|
|
112
121
|
/** Particles over the resolved HDR image, depth-tested against the scene without writing depth. */
|
|
113
122
|
export declare function drawNode(): NodeDescriptor;
|
|
114
123
|
/** Reads an emitter's particle buffer (tests: determinism, CPU vs GPU). */
|
|
@@ -125,6 +134,7 @@ export declare function describeParticles(world: World): {
|
|
|
125
134
|
systems: {
|
|
126
135
|
entity: number;
|
|
127
136
|
backend: string;
|
|
137
|
+
backendReason?: string | undefined;
|
|
128
138
|
emitters: {
|
|
129
139
|
name: string;
|
|
130
140
|
alive: number;
|
package/dist/sim.js
CHANGED
|
@@ -8,15 +8,20 @@ import { GpuBuffer } from "@aethervtt/shard-gpu";
|
|
|
8
8
|
import {
|
|
9
9
|
ComputedVisibility,
|
|
10
10
|
cameraOf,
|
|
11
|
+
clearUnsupported,
|
|
12
|
+
DataStore,
|
|
13
|
+
dataEntry,
|
|
14
|
+
depthReadEntry,
|
|
11
15
|
ForwardStateResource,
|
|
12
16
|
Gpu,
|
|
13
17
|
GpuAssetsResource,
|
|
14
18
|
Meshes,
|
|
15
19
|
RenderPhase,
|
|
20
|
+
reportUnsupported,
|
|
16
21
|
Shaders,
|
|
17
22
|
Views
|
|
18
23
|
} from "@aethervtt/shard-render";
|
|
19
|
-
import { Time } from "@aethervtt/shard-runtime";
|
|
24
|
+
import { FrameDemand, Time } from "@aethervtt/shard-runtime";
|
|
20
25
|
import { Textures } from "@aethervtt/shard-texture";
|
|
21
26
|
import { GlobalTransform } from "@aethervtt/shard-transform";
|
|
22
27
|
import { ParticleEmitterOverrides, ParticleSystem } from "./components.js";
|
|
@@ -69,8 +74,24 @@ const cpuParticle = {
|
|
|
69
74
|
rotSpeed: 0,
|
|
70
75
|
seed: 0
|
|
71
76
|
};
|
|
77
|
+
function randomDirection(seed, k0, out) {
|
|
78
|
+
const z = rand(seed, k0) * 2 - 1;
|
|
79
|
+
const phi = 6.2831853 * rand(seed, k0 + 1);
|
|
80
|
+
const r = Math.sqrt(Math.max(1 - z * z, 0));
|
|
81
|
+
out[0] = r * Math.cos(phi);
|
|
82
|
+
out[1] = z;
|
|
83
|
+
out[2] = r * Math.sin(phi);
|
|
84
|
+
}
|
|
85
|
+
function mixRange(r, t) {
|
|
86
|
+
return r[0] + (r[1] - r[0]) * t;
|
|
87
|
+
}
|
|
88
|
+
const spawnDirection = new Float32Array(3);
|
|
89
|
+
const cpuSteps = [];
|
|
90
|
+
const cpuStepParams = [];
|
|
72
91
|
function simulateCpu(e, sys, dt) {
|
|
73
92
|
const d = e.cpu;
|
|
93
|
+
if (!e.cpuBits || e.cpuBits.buffer !== d.buffer) e.cpuBits = new Uint32Array(d.buffer);
|
|
94
|
+
const u = e.cpuBits;
|
|
74
95
|
const def = e.def;
|
|
75
96
|
const s = def.shape;
|
|
76
97
|
const init = def.init;
|
|
@@ -84,17 +105,14 @@ function simulateCpu(e, sys, dt) {
|
|
|
84
105
|
let dx = 0;
|
|
85
106
|
let dy = 1;
|
|
86
107
|
let dz = 0;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
if (s.type === "point") dir(6);
|
|
96
|
-
else if (s.type === "sphere") {
|
|
97
|
-
dir(6);
|
|
108
|
+
if (s.type === "point" || s.type === "sphere") {
|
|
109
|
+
randomDirection(seed, 6, spawnDirection);
|
|
110
|
+
dx = spawnDirection[0];
|
|
111
|
+
dy = spawnDirection[1];
|
|
112
|
+
dz = spawnDirection[2];
|
|
113
|
+
}
|
|
114
|
+
if (s.type === "point") {
|
|
115
|
+
} else if (s.type === "sphere") {
|
|
98
116
|
const r = s.radius * rand(seed, 9) ** 0.3333333;
|
|
99
117
|
px = dx * r;
|
|
100
118
|
py = dy * r;
|
|
@@ -113,8 +131,7 @@ function simulateCpu(e, sys, dt) {
|
|
|
113
131
|
py = (rand(seed, 7) - 0.5) * s.size[1];
|
|
114
132
|
pz = (rand(seed, 8) - 0.5) * s.size[2];
|
|
115
133
|
}
|
|
116
|
-
const
|
|
117
|
-
const speed = mix(init.speed, rand(seed, 1));
|
|
134
|
+
const speed = mixRange(init.speed, rand(seed, 1));
|
|
118
135
|
const p = cpuParticle;
|
|
119
136
|
p.pos[0] = px;
|
|
120
137
|
p.pos[1] = py;
|
|
@@ -122,7 +139,7 @@ function simulateCpu(e, sys, dt) {
|
|
|
122
139
|
p.vel[0] = dx * speed;
|
|
123
140
|
p.vel[1] = dy * speed;
|
|
124
141
|
p.vel[2] = dz * speed;
|
|
125
|
-
p.life = Math.max(
|
|
142
|
+
p.life = Math.max(mixRange(init.lifetime, rand(seed, 2)), 1e-3);
|
|
126
143
|
p.seed = seed;
|
|
127
144
|
p.rotSpeed = 0;
|
|
128
145
|
for (const m of def.update) MODULES[m.module].cpuInit?.(m, p);
|
|
@@ -144,15 +161,23 @@ function simulateCpu(e, sys, dt) {
|
|
|
144
161
|
d[o + 3] = 0;
|
|
145
162
|
d.set(p.vel, o + 4);
|
|
146
163
|
d[o + 7] = p.life;
|
|
147
|
-
d[o + 8] =
|
|
148
|
-
d[o + 9] =
|
|
164
|
+
d[o + 8] = mixRange(init.size, rand(seed, 3));
|
|
165
|
+
d[o + 9] = mixRange(init.rotation, rand(seed, 4)) * Math.PI / 180;
|
|
149
166
|
d[o + 10] = p.rotSpeed;
|
|
150
|
-
|
|
167
|
+
u[o + 11] = seed;
|
|
151
168
|
const c = rand(seed, 5);
|
|
152
169
|
for (let a = 0; a < 4; a++)
|
|
153
170
|
d[o + 12 + a] = init.color[0][a] + (init.color[1][a] - init.color[0][a]) * c;
|
|
154
171
|
}
|
|
155
|
-
|
|
172
|
+
let steps = 0;
|
|
173
|
+
for (const m of def.update) {
|
|
174
|
+
const step = MODULES[m.module].cpu;
|
|
175
|
+
if (!step) continue;
|
|
176
|
+
cpuSteps[steps] = step;
|
|
177
|
+
cpuStepParams[steps] = m;
|
|
178
|
+
steps++;
|
|
179
|
+
}
|
|
180
|
+
const time = sys.time;
|
|
156
181
|
let alive = 0;
|
|
157
182
|
for (let i = 0; i < e.capacity; i++) {
|
|
158
183
|
const o = i * PARTICLE_FLOATS;
|
|
@@ -172,10 +197,16 @@ function simulateCpu(e, sys, dt) {
|
|
|
172
197
|
p.rot = d[o + 9];
|
|
173
198
|
p.rotSpeed = d[o + 10];
|
|
174
199
|
p.seed = u[o + 11];
|
|
175
|
-
for (
|
|
176
|
-
|
|
200
|
+
for (let k = 0; k < steps; k++) cpuSteps[k](cpuStepParams[k], p, dt, time);
|
|
201
|
+
const vel = p.vel;
|
|
202
|
+
const pos = p.pos;
|
|
203
|
+
d[o] = pos[0] + vel[0] * dt;
|
|
204
|
+
d[o + 1] = pos[1] + vel[1] * dt;
|
|
205
|
+
d[o + 2] = pos[2] + vel[2] * dt;
|
|
177
206
|
d[o + 3] = p.age;
|
|
178
|
-
d
|
|
207
|
+
d[o + 4] = vel[0];
|
|
208
|
+
d[o + 5] = vel[1];
|
|
209
|
+
d[o + 6] = vel[2];
|
|
179
210
|
d[o + 9] = p.rot;
|
|
180
211
|
if (p.age < life) alive++;
|
|
181
212
|
}
|
|
@@ -198,12 +229,14 @@ function createEmitter(gpu, entity, def, cpu, previous) {
|
|
|
198
229
|
def,
|
|
199
230
|
key: emitterKey(def),
|
|
200
231
|
capacity: def.capacity,
|
|
201
|
-
particles: keep ? previous.particles : new
|
|
232
|
+
particles: keep ? previous.particles : new DataStore(gpu, {
|
|
202
233
|
label,
|
|
203
|
-
usage: GPUBufferUsage.
|
|
234
|
+
usage: GPUBufferUsage.COPY_SRC,
|
|
204
235
|
size: def.capacity * PARTICLE_BYTES
|
|
205
236
|
}),
|
|
206
237
|
cpu: cpu ? keep && previous.cpu ? previous.cpu : new Float32Array(def.capacity * PARTICLE_FLOATS) : void 0,
|
|
238
|
+
cpuBits: void 0,
|
|
239
|
+
cpuSort: cpu && sortable ? new Float64Array(def.capacity) : void 0,
|
|
207
240
|
sim: previous?.sim ?? new GpuBuffer(gpu, { label: `${label}/sim`, usage: GPUBufferUsage.UNIFORM, size: SIM_BYTES }),
|
|
208
241
|
draw: previous?.draw ?? new GpuBuffer(gpu, {
|
|
209
242
|
label: `${label}/draw`,
|
|
@@ -229,17 +262,14 @@ function createEmitter(gpu, entity, def, cpu, previous) {
|
|
|
229
262
|
spawnCount: 0,
|
|
230
263
|
backlog: 0,
|
|
231
264
|
alive: keep ? previous.alive : 0,
|
|
265
|
+
lastSpawn: keep ? previous.lastSpawn : Number.NEGATIVE_INFINITY,
|
|
232
266
|
sort: sortable ? {
|
|
233
267
|
keys: gpu.device.createBuffer({
|
|
234
268
|
label: `${label}/keys`,
|
|
235
269
|
size: n * 4,
|
|
236
270
|
usage: GPUBufferUsage.STORAGE
|
|
237
271
|
}),
|
|
238
|
-
order: gpu
|
|
239
|
-
label: `${label}/order`,
|
|
240
|
-
size: n * 4,
|
|
241
|
-
usage: GPUBufferUsage.STORAGE
|
|
242
|
-
}),
|
|
272
|
+
order: new DataStore(gpu, { label: `${label}/order`, size: n * 4 }),
|
|
243
273
|
steps: gpu.device.createBuffer({
|
|
244
274
|
label: `${label}/steps`,
|
|
245
275
|
size: (steps + 1) * 256,
|
|
@@ -306,6 +336,7 @@ const prepareParticles = defineSystem({
|
|
|
306
336
|
store.primary = primary?.name;
|
|
307
337
|
store.primaryCamera = primary ? cameraOf(primary) : void 0;
|
|
308
338
|
const cam = store.primaryCamera;
|
|
339
|
+
let live = false;
|
|
309
340
|
for (const table of q.tables) {
|
|
310
341
|
const effectRefs = table.column(ParticleSystem, "effect");
|
|
311
342
|
const playing = table.column(ParticleSystem, "playing");
|
|
@@ -320,7 +351,8 @@ const prepareParticles = defineSystem({
|
|
|
320
351
|
const entity = table.entities[i];
|
|
321
352
|
const effect = effects.get(effectRefs[i]);
|
|
322
353
|
if (!effect || !vis[i]) continue;
|
|
323
|
-
const
|
|
354
|
+
const forced = gpu.tier === "baseline" && backends[i] !== 1;
|
|
355
|
+
const cpu = backends[i] === 1 || forced;
|
|
324
356
|
let sys = store.systems.get(entity);
|
|
325
357
|
if (!sys || sys.effect !== effect || sys.version !== effect.version || sys.cpu !== cpu) {
|
|
326
358
|
const old = sys;
|
|
@@ -342,6 +374,7 @@ const prepareParticles = defineSystem({
|
|
|
342
374
|
seed: seeds[i],
|
|
343
375
|
local: spaces[i] === 1,
|
|
344
376
|
cpu,
|
|
377
|
+
forced,
|
|
345
378
|
model: new Float32Array(16),
|
|
346
379
|
center: new Float32Array(3),
|
|
347
380
|
radius: reach(effect),
|
|
@@ -398,12 +431,16 @@ const prepareParticles = defineSystem({
|
|
|
398
431
|
e.backlog + (e.dt > 0 ? spawnCount(e, sys.time, e.dt, rate) : 0)
|
|
399
432
|
);
|
|
400
433
|
e.backlog = e.spawnCount;
|
|
434
|
+
if (e.spawnCount > 0) e.lastSpawn = sys.time;
|
|
435
|
+
if (dt > 0 && (rate > 0 || burstPending(e) || sys.time - e.lastSpawn <= e.def.init.lifetime[1]))
|
|
436
|
+
live = true;
|
|
401
437
|
writeUniforms(e, sys, cam);
|
|
402
438
|
if (sys.cpu && e.cpu) {
|
|
403
439
|
simulateCpu(e, sys, e.dt);
|
|
404
440
|
e.particles.write(e.cpu);
|
|
405
441
|
e.spawned += e.spawnCount;
|
|
406
442
|
e.backlog = 0;
|
|
443
|
+
if (e.sort && e.cpuSort && cam) sortCpu(e, sys, cam.view);
|
|
407
444
|
}
|
|
408
445
|
}
|
|
409
446
|
}
|
|
@@ -411,8 +448,90 @@ const prepareParticles = defineSystem({
|
|
|
411
448
|
for (const [entity, sys] of store.systems) {
|
|
412
449
|
if (sys.seen !== store.frame) store.systems.delete(entity);
|
|
413
450
|
}
|
|
451
|
+
world.tryResource(FrameDemand)?.set("particles", live);
|
|
452
|
+
if (gpu.tier === "baseline") reportCpuOnly(world, store);
|
|
414
453
|
}
|
|
415
454
|
});
|
|
455
|
+
const keyFloat = new Float32Array(1);
|
|
456
|
+
const keyBits = new Uint32Array(keyFloat.buffer);
|
|
457
|
+
function sortCpu(e, sys, view) {
|
|
458
|
+
const d = e.cpu;
|
|
459
|
+
const packed = e.cpuSort;
|
|
460
|
+
const m = sys.model;
|
|
461
|
+
for (let i = 0; i < e.capacity; i++) {
|
|
462
|
+
const o = i * PARTICLE_FLOATS;
|
|
463
|
+
let key = 3e38;
|
|
464
|
+
if (d[o + 7] > 0 && d[o + 3] < d[o + 7]) {
|
|
465
|
+
let x = d[o];
|
|
466
|
+
let y = d[o + 1];
|
|
467
|
+
let z = d[o + 2];
|
|
468
|
+
if (sys.local) {
|
|
469
|
+
const wx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
470
|
+
const wy = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
471
|
+
const wz = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
472
|
+
x = wx;
|
|
473
|
+
y = wy;
|
|
474
|
+
z = wz;
|
|
475
|
+
}
|
|
476
|
+
key = view[2] * x + view[6] * y + view[10] * z + view[14];
|
|
477
|
+
}
|
|
478
|
+
keyFloat[0] = key;
|
|
479
|
+
const bits = keyBits[0];
|
|
480
|
+
const ordered = bits & 2147483648 ? ~bits >>> 0 : (bits | 2147483648) >>> 0;
|
|
481
|
+
packed[i] = ordered * 65536 + i;
|
|
482
|
+
}
|
|
483
|
+
packed.sort();
|
|
484
|
+
const order = sortScratch(e.capacity);
|
|
485
|
+
for (let i = 0; i < e.capacity; i++) order[i] = packed[i] % 65536;
|
|
486
|
+
e.sort.order.write(order, 0, 0, e.capacity);
|
|
487
|
+
}
|
|
488
|
+
let orderScratch = new Uint32Array(1024);
|
|
489
|
+
function sortScratch(n) {
|
|
490
|
+
if (orderScratch.length < n) orderScratch = new Uint32Array(n);
|
|
491
|
+
return orderScratch;
|
|
492
|
+
}
|
|
493
|
+
const reportedModules = /* @__PURE__ */ new WeakMap();
|
|
494
|
+
const usedModules = /* @__PURE__ */ new Set();
|
|
495
|
+
function reportCpuOnly(world, store) {
|
|
496
|
+
let reported = reportedModules.get(world);
|
|
497
|
+
if (!reported) {
|
|
498
|
+
reported = /* @__PURE__ */ new Set();
|
|
499
|
+
reportedModules.set(world, reported);
|
|
500
|
+
}
|
|
501
|
+
usedModules.clear();
|
|
502
|
+
for (const sys of store.systems.values()) {
|
|
503
|
+
if (!sys.forced) continue;
|
|
504
|
+
for (const e of sys.emitters) {
|
|
505
|
+
for (const m of e.def.update) {
|
|
506
|
+
const def = MODULES[m.module];
|
|
507
|
+
if (def.update && !def.cpu) usedModules.add(m.module);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
for (const name of usedModules) {
|
|
512
|
+
if (reported.has(name)) continue;
|
|
513
|
+
reported.add(name);
|
|
514
|
+
reportUnsupported(
|
|
515
|
+
world,
|
|
516
|
+
`particles/${name}`,
|
|
517
|
+
`The particle module "${name}" runs on the GPU backend only; the baseline tier runs particles on the CPU, without it`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
for (const name of reported) {
|
|
521
|
+
if (usedModules.has(name)) continue;
|
|
522
|
+
reported.delete(name);
|
|
523
|
+
clearUnsupported(world, `particles/${name}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function burstPending(e) {
|
|
527
|
+
const bursts = e.def.spawn.bursts;
|
|
528
|
+
for (let i = 0; i < bursts.length; i++) {
|
|
529
|
+
const b = bursts[i];
|
|
530
|
+
const fired = e.fired[i] ?? 0;
|
|
531
|
+
if ((fired === 0 || b.interval > 0) && (b.cycles === 0 || fired < b.cycles)) return true;
|
|
532
|
+
}
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
416
535
|
function sphereVisible(planes, c, r) {
|
|
417
536
|
for (let p = 0; p < 6; p++) {
|
|
418
537
|
const d = planes[p * 4] * c[0] + planes[p * 4 + 1] * c[1] + planes[p * 4 + 2] * c[2] + planes[p * 4 + 3];
|
|
@@ -511,9 +630,9 @@ function caches(world, gpu) {
|
|
|
511
630
|
data: gpu.layouts.bindGroupLayout({
|
|
512
631
|
label: "particles/data",
|
|
513
632
|
entries: [
|
|
514
|
-
|
|
633
|
+
dataEntry(gpu, 0, V),
|
|
515
634
|
{ binding: 1, visibility: V | F, buffer: { type: "uniform" } },
|
|
516
|
-
|
|
635
|
+
dataEntry(gpu, 2, V)
|
|
517
636
|
]
|
|
518
637
|
}),
|
|
519
638
|
texture: gpu.layouts.bindGroupLayout({
|
|
@@ -521,7 +640,7 @@ function caches(world, gpu) {
|
|
|
521
640
|
entries: [
|
|
522
641
|
{ binding: 0, visibility: F, texture: { sampleType: "float" } },
|
|
523
642
|
{ binding: 1, visibility: F, sampler: { type: "filtering" } },
|
|
524
|
-
|
|
643
|
+
depthReadEntry(gpu, 2, F)
|
|
525
644
|
]
|
|
526
645
|
}),
|
|
527
646
|
view: gpu.layouts.bindGroupLayout({
|
|
@@ -549,11 +668,7 @@ function caches(world, gpu) {
|
|
|
549
668
|
format: "depth32float",
|
|
550
669
|
usage: GPUTextureUsage.TEXTURE_BINDING
|
|
551
670
|
});
|
|
552
|
-
c.noOrder = gpu
|
|
553
|
-
label: "particles/no-order",
|
|
554
|
-
size: 16,
|
|
555
|
-
usage: GPUBufferUsage.STORAGE
|
|
556
|
-
});
|
|
671
|
+
c.noOrder = new DataStore(gpu, { label: "particles/no-order", size: 16 });
|
|
557
672
|
c.sampler = gpu.device.createSampler({
|
|
558
673
|
label: "particles",
|
|
559
674
|
magFilter: "linear",
|
|
@@ -628,11 +743,12 @@ function sortPipelines(ctx, c) {
|
|
|
628
743
|
c.sort = { keys, sort };
|
|
629
744
|
return c.sort;
|
|
630
745
|
}
|
|
631
|
-
function simulateNode() {
|
|
746
|
+
function simulateNode(world) {
|
|
632
747
|
return {
|
|
633
748
|
kind: "raw",
|
|
634
749
|
phase: RenderPhase.Resolve + 10,
|
|
635
|
-
|
|
750
|
+
// Compute is the full tier's: on baseline every system runs the CPU backend in prepare.
|
|
751
|
+
enabled: (view) => cameraOf(view) !== void 0 && world.resource(Gpu).tier === "full",
|
|
636
752
|
reads: ["depth"],
|
|
637
753
|
sideEffects: true,
|
|
638
754
|
run: (ctx) => {
|
|
@@ -656,10 +772,10 @@ function simulateNode() {
|
|
|
656
772
|
gpu,
|
|
657
773
|
c,
|
|
658
774
|
`sim/${sys.entity}/${e.def.name}`,
|
|
659
|
-
`${idOf(e.particles.
|
|
775
|
+
`${idOf(e.particles.gpuBuffer)}/${idOf(e.sim.buffer)}/${idOf(depth)}`,
|
|
660
776
|
c.layouts.sim,
|
|
661
777
|
() => [
|
|
662
|
-
{ binding: 0, resource: { buffer: e.particles.
|
|
778
|
+
{ binding: 0, resource: { buffer: e.particles.gpuBuffer } },
|
|
663
779
|
{ binding: 1, resource: { buffer: e.sim.buffer } },
|
|
664
780
|
{ binding: 2, resource: depth.createView() },
|
|
665
781
|
{ binding: 3, resource: { buffer: e.counter } }
|
|
@@ -714,13 +830,13 @@ function sortEmitter(ctx, c, pass, sys, e, viewBuffer) {
|
|
|
714
830
|
gpu,
|
|
715
831
|
c,
|
|
716
832
|
`sort/${sys.entity}/${e.def.name}/${i}`,
|
|
717
|
-
`${idOf(e.particles.
|
|
833
|
+
`${idOf(e.particles.gpuBuffer)}/${idOf(viewBuffer)}/${idOf(s.keys)}`,
|
|
718
834
|
c.layouts.sort,
|
|
719
835
|
() => [
|
|
720
836
|
{ binding: 0, resource: { buffer: viewBuffer } },
|
|
721
|
-
{ binding: 10, resource: { buffer: e.particles.
|
|
837
|
+
{ binding: 10, resource: { buffer: e.particles.gpuBuffer } },
|
|
722
838
|
{ binding: 11, resource: { buffer: s.keys } },
|
|
723
|
-
{ binding: 12, resource: { buffer: s.order } },
|
|
839
|
+
{ binding: 12, resource: { buffer: s.order.gpuBuffer } },
|
|
724
840
|
{ binding: 13, resource: { buffer: s.steps, offset: i * 256, size: 16 } },
|
|
725
841
|
// The draw uniform starts with the model matrix: particles in local space sort in world.
|
|
726
842
|
{
|
|
@@ -738,18 +854,16 @@ function sortEmitter(ctx, c, pass, sys, e, viewBuffer) {
|
|
|
738
854
|
pass.dispatchWorkgroups(Math.ceil(s.n / 64));
|
|
739
855
|
}
|
|
740
856
|
}
|
|
741
|
-
let identityBuf;
|
|
742
857
|
function identityBuffer(gpu, _c) {
|
|
743
|
-
|
|
744
|
-
const buffer =
|
|
858
|
+
return gpu.shared("particles/identity", (device) => {
|
|
859
|
+
const buffer = device.createBuffer({
|
|
745
860
|
label: "particles/identity",
|
|
746
861
|
size: 64,
|
|
747
862
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
748
863
|
});
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
return identityBuf.buffer;
|
|
864
|
+
device.queue.writeBuffer(buffer, 0, identity);
|
|
865
|
+
return buffer;
|
|
866
|
+
});
|
|
753
867
|
}
|
|
754
868
|
const ADDITIVE = {
|
|
755
869
|
color: { srcFactor: "one", dstFactor: "one", operation: "add" },
|
|
@@ -852,12 +966,12 @@ function drawNode() {
|
|
|
852
966
|
gpu,
|
|
853
967
|
c,
|
|
854
968
|
`data/${sys.entity}/${e.def.name}`,
|
|
855
|
-
`${idOf(e.particles
|
|
969
|
+
`${idOf(e.particles)}:${e.particles.version}/${idOf(e.draw.buffer)}/${idOf(order)}:${order.version}`,
|
|
856
970
|
c.layouts.data,
|
|
857
971
|
() => [
|
|
858
|
-
{ binding: 0, resource:
|
|
972
|
+
{ binding: 0, resource: e.particles.resource() },
|
|
859
973
|
{ binding: 1, resource: { buffer: e.draw.buffer } },
|
|
860
|
-
{ binding: 2, resource:
|
|
974
|
+
{ binding: 2, resource: order.resource() }
|
|
861
975
|
]
|
|
862
976
|
)
|
|
863
977
|
);
|
|
@@ -894,13 +1008,14 @@ function drawNode() {
|
|
|
894
1008
|
};
|
|
895
1009
|
}
|
|
896
1010
|
async function readParticles(gpu, e) {
|
|
1011
|
+
if (e.particles.textured) return e.cpu.slice(0, e.capacity * PARTICLE_FLOATS);
|
|
897
1012
|
const size = e.capacity * PARTICLE_BYTES;
|
|
898
1013
|
const staging = gpu.device.createBuffer({
|
|
899
1014
|
size,
|
|
900
1015
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
901
1016
|
});
|
|
902
1017
|
const encoder = gpu.device.createCommandEncoder();
|
|
903
|
-
encoder.copyBufferToBuffer(e.particles.
|
|
1018
|
+
encoder.copyBufferToBuffer(e.particles.gpuBuffer, 0, staging, 0, size);
|
|
904
1019
|
gpu.device.queue.submit([encoder.finish()]);
|
|
905
1020
|
await staging.mapAsync(GPUMapMode.READ);
|
|
906
1021
|
const out = new Float32Array(staging.getMappedRange().slice(0));
|
|
@@ -936,6 +1051,7 @@ function describeParticles(world) {
|
|
|
936
1051
|
systems: [...store.systems.values()].map((sys) => ({
|
|
937
1052
|
entity: sys.entity,
|
|
938
1053
|
backend: sys.cpu ? "cpu" : "gpu",
|
|
1054
|
+
...sys.forced ? { backendReason: "the baseline tier has no compute" } : {},
|
|
939
1055
|
emitters: sys.emitters.map((e) => ({
|
|
940
1056
|
name: e.def.name,
|
|
941
1057
|
alive: e.alive,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aethervtt/shard-particles",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@aethervtt/shard-assets": "0.0
|
|
20
|
-
"@aethervtt/shard-core": "0.0
|
|
21
|
-
"@aethervtt/shard-gpu": "0.0
|
|
22
|
-
"@aethervtt/shard-mesh": "0.0
|
|
23
|
-
"@aethervtt/shard-render": "0.0
|
|
24
|
-
"@aethervtt/shard-runtime": "0.0
|
|
25
|
-
"@aethervtt/shard-texture": "0.0
|
|
26
|
-
"@aethervtt/shard-transform": "0.0
|
|
19
|
+
"@aethervtt/shard-assets": "0.2.0",
|
|
20
|
+
"@aethervtt/shard-core": "0.2.0",
|
|
21
|
+
"@aethervtt/shard-gpu": "0.2.0",
|
|
22
|
+
"@aethervtt/shard-mesh": "0.2.0",
|
|
23
|
+
"@aethervtt/shard-render": "0.2.0",
|
|
24
|
+
"@aethervtt/shard-runtime": "0.2.0",
|
|
25
|
+
"@aethervtt/shard-texture": "0.2.0",
|
|
26
|
+
"@aethervtt/shard-transform": "0.2.0"
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
"provenance": true
|
|
36
36
|
},
|
|
37
37
|
"shard": {
|
|
38
|
-
"buildSha": "
|
|
38
|
+
"buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
|
|
39
39
|
}
|
|
40
40
|
}
|