@aethervtt/shard-particles 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/components.d.ts +13 -0
- package/dist/components.js +45 -0
- package/dist/effect.d.ts +85 -0
- package/dist/effect.js +333 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +56 -0
- package/dist/modules.d.ts +41 -0
- package/dist/modules.js +292 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +39 -0
- package/dist/preview-render.d.ts +4 -0
- package/dist/preview-render.js +68 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +11 -0
- package/dist/shaders.d.ts +15 -0
- package/dist/shaders.js +347 -0
- package/dist/sim.d.ts +139 -0
- package/dist/sim.js +962 -0
- package/dist/values.d.ts +33 -0
- package/dist/values.js +199 -0
- package/package.json +40 -0
package/dist/modules.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
curve,
|
|
3
|
+
evalCurve,
|
|
4
|
+
wgslFloat as f,
|
|
5
|
+
gradient,
|
|
6
|
+
invalid,
|
|
7
|
+
rand,
|
|
8
|
+
scalar,
|
|
9
|
+
wgslCurve,
|
|
10
|
+
wgslGradient
|
|
11
|
+
} from "./values.js";
|
|
12
|
+
const vec3 = (v) => `vec3f(${v.map(f).join(", ")})`;
|
|
13
|
+
const MODULES = {
|
|
14
|
+
gravity: {
|
|
15
|
+
name: "gravity",
|
|
16
|
+
description: "Constant acceleration.",
|
|
17
|
+
params: {
|
|
18
|
+
acceleration: { kind: "vec3", default: [0, -9.81, 0], description: "m/s\xB2, world axes." }
|
|
19
|
+
},
|
|
20
|
+
update: (p) => `vel += ${vec3(p.acceleration)} * dt;`,
|
|
21
|
+
cpu: (p, s, dt) => {
|
|
22
|
+
const a = p.acceleration;
|
|
23
|
+
for (let k = 0; k < 3; k++) s.vel[k] += a[k] * dt;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
drag: {
|
|
27
|
+
name: "drag",
|
|
28
|
+
description: "Slows particles exponentially: velocity \xD7 e^(\u2212coefficient \xB7 t).",
|
|
29
|
+
params: { coefficient: { kind: "number", default: 1, description: "Per second." } },
|
|
30
|
+
update: (p) => `vel *= exp(${f(-p.coefficient)} * dt);`,
|
|
31
|
+
cpu: (p, s, dt) => {
|
|
32
|
+
const k = Math.exp(-p.coefficient * dt);
|
|
33
|
+
for (let i = 0; i < 3; i++) s.vel[i] *= k;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"velocity-over-life": {
|
|
37
|
+
name: "velocity-over-life",
|
|
38
|
+
description: "An extra velocity, scaled by a curve over life (wind that dies down, a late lift).",
|
|
39
|
+
params: {
|
|
40
|
+
velocity: { kind: "vec3", default: [0, 1, 0], description: "m/s." },
|
|
41
|
+
curve: {
|
|
42
|
+
kind: "curve",
|
|
43
|
+
default: [
|
|
44
|
+
[0, 1],
|
|
45
|
+
[1, 1]
|
|
46
|
+
],
|
|
47
|
+
description: "Multiplier over life."
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
functions: (p, id) => wgslCurve(`${id}_curve`, p.curve),
|
|
51
|
+
update: (p, id) => `pos += ${vec3(p.velocity)} * ${id}_curve(age / life) * dt;`,
|
|
52
|
+
cpu: (p, s, dt) => {
|
|
53
|
+
const m = evalCurve(p.curve, s.age / s.life);
|
|
54
|
+
const v = p.velocity;
|
|
55
|
+
for (let k = 0; k < 3; k++) s.pos[k] += v[k] * m * dt;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"curl-noise": {
|
|
59
|
+
name: "curl-noise",
|
|
60
|
+
description: "Swirling, divergence-free turbulence (smoke, exhaust).",
|
|
61
|
+
params: {
|
|
62
|
+
strength: { kind: "number", default: 1, description: "m/s\xB2 at full swirl." },
|
|
63
|
+
frequency: { kind: "number", default: 1, description: "Swirls per meter." },
|
|
64
|
+
speed: {
|
|
65
|
+
kind: "number",
|
|
66
|
+
default: 0.5,
|
|
67
|
+
description: "How fast the field changes, per second."
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
update: (p) => {
|
|
71
|
+
const s = f(p.strength);
|
|
72
|
+
const q = f(p.frequency);
|
|
73
|
+
const w = f(p.speed);
|
|
74
|
+
return `{
|
|
75
|
+
let ph = time * ${w};
|
|
76
|
+
let a = -vec3f(cos(pos.z * ${q} + ph), cos(pos.x * ${q} + ph), cos(pos.y * ${q} + ph));
|
|
77
|
+
let b = -vec3f(cos((pos.x + pos.y) * ${q} * 1.7 - ph), cos((pos.y + pos.z) * ${q} * 1.7 - ph), cos((pos.z + pos.x) * ${q} * 1.7 - ph)).zxy;
|
|
78
|
+
vel += (a + 0.5 * b) * ${s} * dt;
|
|
79
|
+
}`;
|
|
80
|
+
},
|
|
81
|
+
cpu: (p, s, dt, time) => {
|
|
82
|
+
const q = p.frequency;
|
|
83
|
+
const ph = time * p.speed;
|
|
84
|
+
const [x, y, z] = [s.pos[0], s.pos[1], s.pos[2]];
|
|
85
|
+
const a = [-Math.cos(z * q + ph), -Math.cos(x * q + ph), -Math.cos(y * q + ph)];
|
|
86
|
+
const b0 = [
|
|
87
|
+
-Math.cos((x + y) * q * 1.7 - ph),
|
|
88
|
+
-Math.cos((y + z) * q * 1.7 - ph),
|
|
89
|
+
-Math.cos((z + x) * q * 1.7 - ph)
|
|
90
|
+
];
|
|
91
|
+
const b = [b0[2], b0[0], b0[1]];
|
|
92
|
+
for (let k = 0; k < 3; k++) s.vel[k] += (a[k] + 0.5 * b[k]) * p.strength * dt;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
attractor: {
|
|
96
|
+
name: "attractor",
|
|
97
|
+
description: "Pulls particles toward a point (negative strength pushes away), fading to zero at radius.",
|
|
98
|
+
params: {
|
|
99
|
+
position: {
|
|
100
|
+
kind: "vec3",
|
|
101
|
+
default: [0, 0, 0],
|
|
102
|
+
description: "In the effect's space (local to the emitter for local systems)."
|
|
103
|
+
},
|
|
104
|
+
strength: { kind: "number", default: 5, description: "m/s\xB2 at the center." },
|
|
105
|
+
radius: { kind: "number", default: 5, description: "m." }
|
|
106
|
+
},
|
|
107
|
+
update: (p) => `{
|
|
108
|
+
let d = ${vec3(p.position)} - pos;
|
|
109
|
+
let len = max(length(d), 1e-4);
|
|
110
|
+
vel += d / len * ${f(p.strength)} * clamp(1.0 - len / ${f(p.radius)}, 0.0, 1.0) * dt;
|
|
111
|
+
}`,
|
|
112
|
+
cpu: (p, s, dt) => {
|
|
113
|
+
const c = p.position;
|
|
114
|
+
const d = [c[0] - s.pos[0], c[1] - s.pos[1], c[2] - s.pos[2]];
|
|
115
|
+
const len = Math.max(Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]), 1e-4);
|
|
116
|
+
const k = p.strength * Math.min(1, Math.max(0, 1 - len / p.radius)) * dt / len;
|
|
117
|
+
for (let i = 0; i < 3; i++) s.vel[i] += d[i] * k;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
rotation: {
|
|
121
|
+
name: "rotation",
|
|
122
|
+
description: "Spins billboards: a speed per particle, picked from a range.",
|
|
123
|
+
params: { speed: { kind: "scalar", default: [-90, 90], description: "Degrees per second." } },
|
|
124
|
+
init: (p) => {
|
|
125
|
+
const [a, b] = p.speed;
|
|
126
|
+
return `rot_speed = radians(mix(${f(a)}, ${f(b)}, rand(seed, 20u)));`;
|
|
127
|
+
},
|
|
128
|
+
update: () => "rot += rot_speed * dt;",
|
|
129
|
+
cpuInit: (p, s) => {
|
|
130
|
+
const [a, b] = p.speed;
|
|
131
|
+
s.rotSpeed = (a + (b - a) * rand(s.seed, 20)) * Math.PI / 180;
|
|
132
|
+
},
|
|
133
|
+
cpu: (_, s, dt) => {
|
|
134
|
+
s.rot += s.rotSpeed * dt;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"color-over-life": {
|
|
138
|
+
name: "color-over-life",
|
|
139
|
+
description: "Multiplies the color (and alpha) by a gradient over life.",
|
|
140
|
+
params: { gradient: { kind: "gradient", description: "[[t, color, alpha?], \u2026]." } },
|
|
141
|
+
functions: (p, id) => wgslGradient(`${id}_gradient`, p.gradient),
|
|
142
|
+
render: (_, id) => `color *= ${id}_gradient(t);`
|
|
143
|
+
},
|
|
144
|
+
"size-over-life": {
|
|
145
|
+
name: "size-over-life",
|
|
146
|
+
description: "Multiplies the size by a curve over life.",
|
|
147
|
+
params: { curve: { kind: "curve", description: "[[t, multiplier], \u2026]." } },
|
|
148
|
+
functions: (p, id) => wgslCurve(`${id}_curve`, p.curve),
|
|
149
|
+
render: (_, id) => `size *= ${id}_curve(t);`
|
|
150
|
+
},
|
|
151
|
+
collision: {
|
|
152
|
+
name: "collision",
|
|
153
|
+
description: "Collides with what the camera sees (its depth buffer): bounce or die. GPU only; off screen, particles pass through. The CPU backend doesn't collide.",
|
|
154
|
+
params: {
|
|
155
|
+
mode: {
|
|
156
|
+
kind: "enum",
|
|
157
|
+
values: ["bounce", "die"],
|
|
158
|
+
default: "bounce",
|
|
159
|
+
description: "What a hit does."
|
|
160
|
+
},
|
|
161
|
+
restitution: {
|
|
162
|
+
kind: "number",
|
|
163
|
+
default: 0.5,
|
|
164
|
+
description: "Speed kept along the normal (0..1)."
|
|
165
|
+
},
|
|
166
|
+
friction: {
|
|
167
|
+
kind: "number",
|
|
168
|
+
default: 0.2,
|
|
169
|
+
description: "Speed lost along the surface (0..1)."
|
|
170
|
+
},
|
|
171
|
+
thickness: {
|
|
172
|
+
kind: "number",
|
|
173
|
+
default: 0.5,
|
|
174
|
+
description: "m: surfaces this far behind the depth buffer still count."
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
depth: true,
|
|
178
|
+
update: (p) => `{
|
|
179
|
+
let hit = depth_collision(pos, vel, ${f(p.thickness)});
|
|
180
|
+
if (hit.w > 0.0) {
|
|
181
|
+
${p.mode === "die" ? "age = life;" : `let n = hit.xyz;
|
|
182
|
+
let vn = dot(vel, n);
|
|
183
|
+
if (vn < 0.0) {
|
|
184
|
+
let vt = vel - n * vn;
|
|
185
|
+
vel = vt * ${f(1 - p.friction)} - n * vn * ${f(p.restitution)};
|
|
186
|
+
}`}
|
|
187
|
+
}
|
|
188
|
+
}`
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function validateModule(json, path, errors) {
|
|
192
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) {
|
|
193
|
+
errors.push(invalid(path, 'Expected a module: { "module": "<name>", \u2026parameters }'));
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
const entry = json;
|
|
197
|
+
const def = MODULES[entry.module];
|
|
198
|
+
if (!def) {
|
|
199
|
+
errors.push(
|
|
200
|
+
invalid(
|
|
201
|
+
`${path}/module`,
|
|
202
|
+
`Unknown module ${JSON.stringify(entry.module)}`,
|
|
203
|
+
`Modules: ${Object.keys(MODULES).join(", ")}.`
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
const out = { module: def.name };
|
|
209
|
+
for (const key of Object.keys(entry)) {
|
|
210
|
+
if (key !== "module" && !def.params[key]) {
|
|
211
|
+
errors.push(
|
|
212
|
+
invalid(
|
|
213
|
+
`${path}/${key}`,
|
|
214
|
+
`${def.name} has no parameter "${key}"`,
|
|
215
|
+
`Parameters: ${Object.keys(def.params).join(", ")}.`
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const [key, param] of Object.entries(def.params)) {
|
|
221
|
+
const v = entry[key];
|
|
222
|
+
const at = `${path}/${key}`;
|
|
223
|
+
if (v === void 0 && param.default === void 0) {
|
|
224
|
+
errors.push(invalid(at, `${def.name} needs "${key}"`, param.description));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const value = v ?? param.default;
|
|
228
|
+
switch (param.kind) {
|
|
229
|
+
case "number":
|
|
230
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
231
|
+
errors.push(invalid(at, "Expected a number"));
|
|
232
|
+
out[key] = value;
|
|
233
|
+
break;
|
|
234
|
+
case "scalar":
|
|
235
|
+
out[key] = scalar(value, at, errors, 0);
|
|
236
|
+
break;
|
|
237
|
+
case "vec3":
|
|
238
|
+
if (!Array.isArray(value) || value.length !== 3 || !value.every((n) => typeof n === "number")) {
|
|
239
|
+
errors.push(invalid(at, "Expected [x, y, z]"));
|
|
240
|
+
}
|
|
241
|
+
out[key] = value;
|
|
242
|
+
break;
|
|
243
|
+
case "curve":
|
|
244
|
+
out[key] = curve(value, at, errors);
|
|
245
|
+
break;
|
|
246
|
+
case "gradient":
|
|
247
|
+
out[key] = gradient(value, at, errors);
|
|
248
|
+
break;
|
|
249
|
+
case "enum":
|
|
250
|
+
if (!param.values.includes(value)) {
|
|
251
|
+
errors.push(invalid(at, `Expected one of: ${param.values.join(", ")}`));
|
|
252
|
+
}
|
|
253
|
+
out[key] = value;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
function moduleJsonSchema(def) {
|
|
260
|
+
const range = { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 };
|
|
261
|
+
const forms = {
|
|
262
|
+
number: { type: "number" },
|
|
263
|
+
scalar: { anyOf: [{ type: "number" }, range] },
|
|
264
|
+
vec3: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 },
|
|
265
|
+
curve: { anyOf: [{ type: "number" }, { type: "array", items: range }] },
|
|
266
|
+
gradient: { type: "array", items: { type: "array" } },
|
|
267
|
+
enum: {}
|
|
268
|
+
};
|
|
269
|
+
const properties = { module: { const: def.name } };
|
|
270
|
+
for (const [key, p] of Object.entries(def.params)) {
|
|
271
|
+
properties[key] = {
|
|
272
|
+
...p.kind === "enum" ? { enum: p.values } : forms[p.kind],
|
|
273
|
+
description: p.description,
|
|
274
|
+
...p.default !== void 0 ? { default: p.default } : {}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
type: "object",
|
|
279
|
+
description: def.description,
|
|
280
|
+
properties,
|
|
281
|
+
required: [
|
|
282
|
+
"module",
|
|
283
|
+
...Object.entries(def.params).filter(([, p]) => p.default === void 0).map(([k]) => k)
|
|
284
|
+
],
|
|
285
|
+
additionalProperties: false
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export {
|
|
289
|
+
MODULES,
|
|
290
|
+
moduleJsonSchema,
|
|
291
|
+
validateModule
|
|
292
|
+
};
|
package/dist/plugin.d.ts
ADDED
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Last } from "@aethervtt/shard-core";
|
|
2
|
+
import { Graph, RenderDescribers, RenderSet } from "@aethervtt/shard-render";
|
|
3
|
+
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
4
|
+
import { OriginShift } from "@aethervtt/shard-transform";
|
|
5
|
+
import * as componentsModule from "./components.js";
|
|
6
|
+
import * as effectModule from "./effect.js";
|
|
7
|
+
import { ParticleEffects } from "./effect.js";
|
|
8
|
+
import * as previewModule from "./preview.js";
|
|
9
|
+
import * as simModule from "./sim.js";
|
|
10
|
+
import {
|
|
11
|
+
describeParticles,
|
|
12
|
+
drawNode,
|
|
13
|
+
Particles,
|
|
14
|
+
prepareParticles,
|
|
15
|
+
shiftParticles,
|
|
16
|
+
simulateNode
|
|
17
|
+
} from "./sim.js";
|
|
18
|
+
const particlesPlugin = definePlugin({
|
|
19
|
+
name: "particles",
|
|
20
|
+
provides: [componentsModule, effectModule, previewModule, simModule],
|
|
21
|
+
dependencies: ["render/forward"],
|
|
22
|
+
build(app) {
|
|
23
|
+
app.world.initResource(ParticleEffects);
|
|
24
|
+
const store = app.world.initResource(Particles);
|
|
25
|
+
app.world.observe(OriginShift, ({ data }) => {
|
|
26
|
+
shiftParticles(store, data.offset[0], data.offset[1], data.offset[2]);
|
|
27
|
+
});
|
|
28
|
+
app.addSystems(Last, prepareParticles.inSet(RenderSet.Prepare));
|
|
29
|
+
},
|
|
30
|
+
ready(app) {
|
|
31
|
+
const graph = app.world.resource(Graph);
|
|
32
|
+
graph.addNode("particles/simulate", simulateNode());
|
|
33
|
+
graph.addNode("particles", drawNode());
|
|
34
|
+
app.world.initResource(RenderDescribers).set("particles", (w) => describeParticles(w));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
export {
|
|
38
|
+
particlesPlugin
|
|
39
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type PreviewImage } from '@aethervtt/shard-assets';
|
|
2
|
+
import type { World } from '@aethervtt/shard-core';
|
|
3
|
+
/** An effect after one second of seeded, fixed-step simulation, in a private world. */
|
|
4
|
+
export declare function renderEffectPreview(world: World, path: string, width: number, height: number): Promise<PreviewImage>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
Camera3d,
|
|
4
|
+
captureView,
|
|
5
|
+
Exposure,
|
|
6
|
+
forwardPlugin,
|
|
7
|
+
Gpu,
|
|
8
|
+
OffscreenTarget,
|
|
9
|
+
RenderTargets,
|
|
10
|
+
renderPlugin
|
|
11
|
+
} from "@aethervtt/shard-render";
|
|
12
|
+
import { App } from "@aethervtt/shard-runtime";
|
|
13
|
+
import { Textures } from "@aethervtt/shard-texture";
|
|
14
|
+
import { lookAt, Transform, TransformPlugin } from "@aethervtt/shard-transform";
|
|
15
|
+
import { ParticleSystem } from "./components.js";
|
|
16
|
+
import { ParticleEffect, ParticleEffects } from "./effect.js";
|
|
17
|
+
async function renderEffectPreview(world, path, width, height) {
|
|
18
|
+
const server = assetServer(world);
|
|
19
|
+
const artifact = await server.artifact(path);
|
|
20
|
+
const effect = ParticleEffect.fromJson(artifact.json, (p) => server.resolve(p));
|
|
21
|
+
const gpu = world.resource(Gpu);
|
|
22
|
+
const { particlesPlugin } = await import("./plugin.js");
|
|
23
|
+
const app = new App().addPlugin(
|
|
24
|
+
TransformPlugin,
|
|
25
|
+
renderPlugin({ gpu, windowView: false }),
|
|
26
|
+
forwardPlugin({ msaa: 1 }),
|
|
27
|
+
particlesPlugin
|
|
28
|
+
);
|
|
29
|
+
await app.init();
|
|
30
|
+
const w = app.world;
|
|
31
|
+
const textures = world.resource(Textures);
|
|
32
|
+
for (const e of effect.emitters) {
|
|
33
|
+
const guid = e.render.texture?.guid;
|
|
34
|
+
const t = guid ? textures.byGuid(guid) : void 0;
|
|
35
|
+
if (guid && t) w.resource(Textures).set(guid, t);
|
|
36
|
+
}
|
|
37
|
+
const target = new OffscreenTarget(gpu, { label: "particle-preview", width, height });
|
|
38
|
+
const targetRef = w.resource(RenderTargets).add(target, "particle-preview");
|
|
39
|
+
const ref = w.resource(ParticleEffects).add(effect);
|
|
40
|
+
w.spawn([ParticleSystem, { effect: ref, seed: 1 }], Transform);
|
|
41
|
+
const reach = Math.max(
|
|
42
|
+
1,
|
|
43
|
+
...effect.emitters.map(
|
|
44
|
+
(e) => e.bounds || e.shape.radius + e.init.speed[1] * e.init.lifetime[1]
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
const eye = [0, reach * 0.6, reach * 2.2];
|
|
48
|
+
const cam = w.spawn(
|
|
49
|
+
[Camera3d, { target: targetRef, fovY: 45, clearColor: [0.01, 0.01, 0.02, 1] }],
|
|
50
|
+
[Exposure, { ev100: 10 }],
|
|
51
|
+
[Transform, { translation: eye, rotation: lookAt(eye, [0, reach * 0.3, 0]) }]
|
|
52
|
+
);
|
|
53
|
+
try {
|
|
54
|
+
for (let i = 0; i < 60; i++) {
|
|
55
|
+
app.update(1 / 60);
|
|
56
|
+
await gpu.pipelines.whenIdle();
|
|
57
|
+
}
|
|
58
|
+
const shot = captureView(w, `camera:${cam}`);
|
|
59
|
+
app.update(1 / 60);
|
|
60
|
+
const image = await shot;
|
|
61
|
+
return { width: image.width, height: image.height, data: image.data };
|
|
62
|
+
} finally {
|
|
63
|
+
target.destroy();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
renderEffectPreview
|
|
68
|
+
};
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineAssetPreview } from "@aethervtt/shard-assets";
|
|
2
|
+
const particleEffectPreview = defineAssetPreview(
|
|
3
|
+
"ParticleEffect",
|
|
4
|
+
async (world, path, width, height) => {
|
|
5
|
+
const { renderEffectPreview } = await import("./preview-render.js");
|
|
6
|
+
return renderEffectPreview(world, path, width, height);
|
|
7
|
+
}
|
|
8
|
+
);
|
|
9
|
+
export {
|
|
10
|
+
particleEffectPreview
|
|
11
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EmitterDef } from './effect.js';
|
|
2
|
+
/** Floats per particle: pos + age, vel + life, size, rot, rot speed, seed, color. */
|
|
3
|
+
export declare const PARTICLE_FLOATS = 16;
|
|
4
|
+
/** Bytes of the simulation uniform (see `Sim` below). */
|
|
5
|
+
export declare const SIM_BYTES = 336;
|
|
6
|
+
/** Bytes of the draw uniform (see `Draw` below). */
|
|
7
|
+
export declare const DRAW_BYTES = 112;
|
|
8
|
+
/** A stable key for an emitter's shaders: its modules and render settings. */
|
|
9
|
+
export declare function emitterKey(def: EmitterDef): string;
|
|
10
|
+
/** The compute module of an emitter: spawn (ring allocation), update (modules inlined), count. */
|
|
11
|
+
export declare function simulationShader(def: EmitterDef): string;
|
|
12
|
+
/** The draw module of an emitter: billboards (or meshes) from the particle buffer. */
|
|
13
|
+
export declare function renderShader(def: EmitterDef): string;
|
|
14
|
+
/** Bitonic sort steps (alpha emitters): keys by view depth, far first; dead slots last. */
|
|
15
|
+
export declare const SORT_SHADER = "\nimport shard::view::view;\n\nstruct Particle {\n /** position, age */\n p0: vec4f,\n /** velocity, lifetime (0: never spawned) */\n p1: vec4f,\n /** size, rotation, rotation speed, seed bits */\n p2: vec4f,\n /** initial color (linear, alpha) */\n p3: vec4f,\n}\n\nstruct Step { j: u32, k: u32, n: u32, capacity: u32 }\n\n@group(0) @binding(10) var<storage, read> particles: array<Particle>;\n@group(0) @binding(11) var<storage, read_write> keys: array<f32>;\n@group(0) @binding(12) var<storage, read_write> order: array<u32>;\n@group(0) @binding(13) var<uniform> step: Step;\n@group(0) @binding(14) var<uniform> model: mat4x4f;\n\n@compute @workgroup_size(64)\nfn keys_pass(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n if (i >= step.n) { return; }\n order[i] = i;\n var key = 3.0e38;\n if (i < step.capacity) {\n let q = particles[i];\n if (q.p1.w > 0.0 && q.p0.w < q.p1.w) {\n let world = (model * vec4f(q.p0.xyz, 1.0)).xyz;\n key = (view.view * vec4f(world, 1.0)).z;\n }\n }\n keys[i] = key;\n}\n\n@compute @workgroup_size(64)\nfn sort_pass(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n let l = i ^ step.j;\n if (i >= step.n || l <= i) { return; }\n let ascending = (i & step.k) == 0u;\n let a = keys[i];\n let b = keys[l];\n if ((a > b) == ascending) {\n keys[i] = b;\n keys[l] = a;\n let t = order[i];\n order[i] = order[l];\n order[l] = t;\n }\n}";
|