reze-engine 0.57.6 → 0.58.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/engine.d.ts +23 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +187 -77
- package/dist/shaders/directives.d.ts +18 -1
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +22 -3
- package/dist/shaders/passes/particles.d.ts +18 -2
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +70 -4
- package/dist/shaders/passes/trails.d.ts +3 -0
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +5 -0
- package/package.json +1 -1
- package/src/engine.ts +226 -91
- package/src/shaders/directives.ts +37 -4
- package/src/shaders/passes/particles.ts +85 -5
- package/src/shaders/passes/trails.ts +8 -0
package/dist/engine.js
CHANGED
|
@@ -1305,6 +1305,10 @@ export class Engine {
|
|
|
1305
1305
|
/** The ground's uniform block, kept so the caster sphere can be refreshed in
|
|
1306
1306
|
* it every frame rather than rebuilding the buffer (addGround allocates). */
|
|
1307
1307
|
this.groundMaterialData = null;
|
|
1308
|
+
/** What addGround was given, so an effect's `#ground` can be lifted again
|
|
1309
|
+
* without rebuilding the floor. */
|
|
1310
|
+
this.groundBaseColor = new Vec3(1, 1, 1);
|
|
1311
|
+
this.groundBaseNoise = 0;
|
|
1308
1312
|
/** Set by the first getGpuTimings() call. See it for why asking is the switch. */
|
|
1309
1313
|
this.timestampsWanted = false;
|
|
1310
1314
|
this.groundMirrorPipeline = null;
|
|
@@ -3034,9 +3038,9 @@ export class Engine {
|
|
|
3034
3038
|
for (const e of this.effects) {
|
|
3035
3039
|
if (e.particles) {
|
|
3036
3040
|
const b = e.particles.rebind();
|
|
3037
|
-
e.particles.
|
|
3038
|
-
e.particles.
|
|
3039
|
-
e.particles.
|
|
3041
|
+
e.particles.computeBinds = b.computeBinds;
|
|
3042
|
+
e.particles.renderBinds = b.renderBinds;
|
|
3043
|
+
e.particles.mirrorRenderBinds = b.mirrorRenderBinds;
|
|
3040
3044
|
}
|
|
3041
3045
|
if (e.grid)
|
|
3042
3046
|
e.grid.binds = [this.gridBindGroup(e.grid, 0), this.gridBindGroup(e.grid, 1)];
|
|
@@ -3567,18 +3571,21 @@ export class Engine {
|
|
|
3567
3571
|
trails?.uniform.destroy();
|
|
3568
3572
|
return { ok: false, diagnostics, mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3569
3573
|
};
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
return abandon(built.diagnostics);
|
|
3574
|
-
particles = built.state;
|
|
3575
|
-
}
|
|
3574
|
+
// THE GRID FIRST. Particles and ribbons READ it — a blade of grass reads the
|
|
3575
|
+
// bend a foot left there — so the textures have to exist before their bind
|
|
3576
|
+
// groups are built. Nothing reads them back, so the grid depends on neither.
|
|
3576
3577
|
if (gridEntryPoint(wgsl)) {
|
|
3577
3578
|
const built = await this.buildSim(wgsl, d, anchors, alias, paramsFor);
|
|
3578
3579
|
if (!built.ok)
|
|
3579
3580
|
return abandon(built.diagnostics);
|
|
3580
3581
|
grid = built.state;
|
|
3581
3582
|
}
|
|
3583
|
+
if (wantsParticles) {
|
|
3584
|
+
const built = await this.buildParticles(wgsl, d, anchors, alias, paramsFor, grid);
|
|
3585
|
+
if (!built.ok)
|
|
3586
|
+
return abandon(built.diagnostics);
|
|
3587
|
+
particles = built.state;
|
|
3588
|
+
}
|
|
3582
3589
|
if (wantsTrails) {
|
|
3583
3590
|
// Only anchors that asked for `trail` have a path to draw; a ribbon on a
|
|
3584
3591
|
// bone recorded without one would read zeroes and paint a line to the origin.
|
|
@@ -3586,7 +3593,7 @@ export class Engine {
|
|
|
3586
3593
|
if (trailSlots === 0) {
|
|
3587
3594
|
return abandon(["a ribbon effect needs at least one #anchor <bone> trail"]);
|
|
3588
3595
|
}
|
|
3589
|
-
const built = await this.buildTrails(wgsl, d, anchors, alias, paramsFor);
|
|
3596
|
+
const built = await this.buildTrails(wgsl, d, anchors, alias, paramsFor, grid);
|
|
3590
3597
|
if (!built.ok)
|
|
3591
3598
|
return abandon(built.diagnostics);
|
|
3592
3599
|
trails = built.state;
|
|
@@ -3665,6 +3672,7 @@ export class Engine {
|
|
|
3665
3672
|
simStep: 0,
|
|
3666
3673
|
simReset: false,
|
|
3667
3674
|
dissolve: d.dissolve ? dissolveConstants(authored) : null,
|
|
3675
|
+
ground: d.ground ? { color: new Vec3(...d.ground.color), noise: d.ground.noise } : null,
|
|
3668
3676
|
// Its OWN resolution, no longer the scene's: an effect that never asked
|
|
3669
3677
|
// for full res is not promoted because a neighbour did.
|
|
3670
3678
|
// FULL RESOLUTION UNLESS TOLD OTHERWISE.
|
|
@@ -3895,11 +3903,23 @@ export class Engine {
|
|
|
3895
3903
|
alias,
|
|
3896
3904
|
/** The effect's declared dials. Spliced into both stages and bound at 7,
|
|
3897
3905
|
* the same binding the composite gives them. */
|
|
3898
|
-
params
|
|
3906
|
+
params,
|
|
3907
|
+
/** This effect's own grid, already built, or null when it declared none.
|
|
3908
|
+
* Read-only here: the pool samples it, the grid pass owns it. */
|
|
3909
|
+
grid) {
|
|
3899
3910
|
// No pragma means "some": an author who wrote the trio clearly wants
|
|
3900
3911
|
// particles, and failing over a missing comment would be pedantry.
|
|
3901
3912
|
const count = Math.min(d.particles || 1024, Engine.MAX_PARTICLES);
|
|
3902
|
-
const src = {
|
|
3913
|
+
const src = {
|
|
3914
|
+
wgsl,
|
|
3915
|
+
count,
|
|
3916
|
+
blend: d.particleBlend,
|
|
3917
|
+
bloom: d.bloom,
|
|
3918
|
+
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
3919
|
+
gridSize: grid?.size ?? 0,
|
|
3920
|
+
cover: particleEntryPoints(wgsl).cover,
|
|
3921
|
+
};
|
|
3922
|
+
const prepass = src.blend === "cutout" && src.cover;
|
|
3903
3923
|
// Sparks want to spawn where a trail is, so the particle stages see the same
|
|
3904
3924
|
// cast buffer the trail draw reads.
|
|
3905
3925
|
const cast = {
|
|
@@ -3966,9 +3986,18 @@ export class Engine {
|
|
|
3966
3986
|
// Only when the effect declared any: WGSL has no empty struct, so a
|
|
3967
3987
|
// param-less effect must not carry the decl OR the binding.
|
|
3968
3988
|
...(params.buffer ? [{ binding: 7, visibility, buffer: { type: "uniform" } }] : []),
|
|
3989
|
+
// The grid, ALWAYS — the accessor is compiled in unconditionally, and a
|
|
3990
|
+
// binding the shader declares must exist in the layout whether the
|
|
3991
|
+
// author calls it or not. With no grid it is the 1x1 of zeroes.
|
|
3992
|
+
{ binding: 8, visibility, texture: { sampleType: "float" } },
|
|
3993
|
+
{ binding: 9, visibility, sampler: { type: "filtering" } },
|
|
3969
3994
|
],
|
|
3970
3995
|
});
|
|
3971
|
-
|
|
3996
|
+
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
3997
|
+
* alternates which texture is current, and rebuilding a single group every
|
|
3998
|
+
* frame is waste for a change that only ever toggles between two known
|
|
3999
|
+
* states. Both entries are the same view when there is no grid. */
|
|
4000
|
+
const bindFor = (layout, camera) => [0, 1].map((parity) => this.device.createBindGroup({
|
|
3972
4001
|
layout,
|
|
3973
4002
|
entries: [
|
|
3974
4003
|
{ binding: 0, resource: { buffer } },
|
|
@@ -3979,15 +4008,22 @@ export class Engine {
|
|
|
3979
4008
|
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
3980
4009
|
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
3981
4010
|
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
4011
|
+
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
4012
|
+
{ binding: 9, resource: this.simSampler },
|
|
3982
4013
|
],
|
|
3983
|
-
});
|
|
4014
|
+
}));
|
|
3984
4015
|
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE);
|
|
3985
4016
|
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT);
|
|
3986
4017
|
// Additive keeps the destination and adds to it, and leaves alpha alone, so
|
|
3987
4018
|
// a glow does not claim coverage it never occluded. The MASK sums with it —
|
|
3988
4019
|
// otherwise an additive effect could never reach the bloom gate. Both live
|
|
3989
4020
|
// in scene-contract as the "particle-additive" class.
|
|
4021
|
+
//
|
|
4022
|
+
// Cutout blends exactly like alpha and differs only in depth and coverage,
|
|
4023
|
+
// below — so it shares alpha's targets rather than adding a class whose
|
|
4024
|
+
// blends would be a copy.
|
|
3990
4025
|
const targets = sceneTargetsFor(src.blend === "additive" ? "particle-additive" : "particle", this.sceneFormats);
|
|
4026
|
+
const cutout = src.blend === "cutout";
|
|
3991
4027
|
this.device.pushErrorScope("validation");
|
|
3992
4028
|
try {
|
|
3993
4029
|
const compute = await this.device.createComputePipelineAsync({
|
|
@@ -3995,15 +4031,41 @@ export class Engine {
|
|
|
3995
4031
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [computeLayout] }),
|
|
3996
4032
|
compute: { module: computeModule, entryPoint: "main" },
|
|
3997
4033
|
});
|
|
4034
|
+
const renderLayoutObj = this.device.createPipelineLayout({ bindGroupLayouts: [renderLayout] });
|
|
4035
|
+
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
4036
|
+
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
4037
|
+
// the shading pass below then tests EQUAL and writes no depth.
|
|
4038
|
+
const depth = prepass
|
|
4039
|
+
? await this.device.createRenderPipelineAsync({
|
|
4040
|
+
label: "particle depth prepass pipeline",
|
|
4041
|
+
layout: renderLayoutObj,
|
|
4042
|
+
vertex: { module: renderModule, entryPoint: "vs" },
|
|
4043
|
+
fragment: { module: renderModule, entryPoint: "fsDepth", targets: sceneTargetsFor("depth-prepass", this.sceneFormats) },
|
|
4044
|
+
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
4045
|
+
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
4046
|
+
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4047
|
+
})
|
|
4048
|
+
: null;
|
|
3998
4049
|
const render = await this.device.createRenderPipelineAsync({
|
|
3999
4050
|
label: "particle render pipeline",
|
|
4000
|
-
layout:
|
|
4051
|
+
layout: renderLayoutObj,
|
|
4001
4052
|
vertex: { module: renderModule, entryPoint: "vs" },
|
|
4002
4053
|
fragment: { module: renderModule, entryPoint: "fs", targets },
|
|
4003
4054
|
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
4004
4055
|
// Tested but not WRITTEN: particles are transparent, so writing depth
|
|
4005
4056
|
// would make whichever quad drew first occlude the ones behind it.
|
|
4006
|
-
|
|
4057
|
+
//
|
|
4058
|
+
// Except a CUTOUT, which is opaque where it is drawn at all. Its alpha
|
|
4059
|
+
// becomes per-sample coverage (in the shader — see FSOut.samples, the
|
|
4060
|
+
// HDR target has no alpha for the hardware path), so its edges stay
|
|
4061
|
+
// antialiased under MSAA while its depth is real — and a blade behind
|
|
4062
|
+
// another blade is then rejected at the depth test rather than shaded
|
|
4063
|
+
// and blended under it.
|
|
4064
|
+
// For a lawn stacked ten blades deep that is the difference between
|
|
4065
|
+
// shading ten layers and shading about three.
|
|
4066
|
+
depthStencil: prepass
|
|
4067
|
+
? { format: this.depthFormat, depthWriteEnabled: false, depthCompare: "equal" }
|
|
4068
|
+
: { format: this.depthFormat, depthWriteEnabled: cutout, depthCompare: this.depthAhead },
|
|
4007
4069
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4008
4070
|
});
|
|
4009
4071
|
const scoped = await this.device.popErrorScope();
|
|
@@ -4025,15 +4087,16 @@ export class Engine {
|
|
|
4025
4087
|
counts: uniformView.uints,
|
|
4026
4088
|
compute,
|
|
4027
4089
|
computeLayout,
|
|
4028
|
-
|
|
4090
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer),
|
|
4091
|
+
depth,
|
|
4029
4092
|
render,
|
|
4030
4093
|
renderLayout,
|
|
4031
|
-
|
|
4032
|
-
|
|
4094
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer),
|
|
4095
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer),
|
|
4033
4096
|
rebind: () => ({
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4097
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer),
|
|
4098
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer),
|
|
4099
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer),
|
|
4037
4100
|
}),
|
|
4038
4101
|
},
|
|
4039
4102
|
};
|
|
@@ -4226,7 +4289,7 @@ export class Engine {
|
|
|
4226
4289
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer);
|
|
4227
4290
|
const cp = encoder.beginComputePass({ label: "particles" });
|
|
4228
4291
|
cp.setPipeline(p.compute);
|
|
4229
|
-
cp.setBindGroup(0, p.
|
|
4292
|
+
cp.setBindGroup(0, p.computeBinds[e.grid?.parity ?? 0]);
|
|
4230
4293
|
cp.dispatchWorkgroups(Math.ceil(p.count / 64));
|
|
4231
4294
|
cp.end();
|
|
4232
4295
|
}
|
|
@@ -4237,8 +4300,15 @@ export class Engine {
|
|
|
4237
4300
|
const p = e.particles;
|
|
4238
4301
|
if (!p || e.weight === 0)
|
|
4239
4302
|
continue;
|
|
4303
|
+
const parity = e.grid?.parity ?? 0;
|
|
4304
|
+
const bind = view === "mirror" ? p.mirrorRenderBinds[parity] : p.renderBinds[parity];
|
|
4305
|
+
if (p.depth) {
|
|
4306
|
+
pass.setPipeline(p.depth);
|
|
4307
|
+
pass.setBindGroup(0, bind);
|
|
4308
|
+
pass.draw(6, p.count);
|
|
4309
|
+
}
|
|
4240
4310
|
pass.setPipeline(p.render);
|
|
4241
|
-
pass.setBindGroup(0,
|
|
4311
|
+
pass.setBindGroup(0, bind);
|
|
4242
4312
|
pass.draw(6, p.count);
|
|
4243
4313
|
}
|
|
4244
4314
|
}
|
|
@@ -4260,7 +4330,10 @@ export class Engine {
|
|
|
4260
4330
|
* describes the effect that is still on screen. */
|
|
4261
4331
|
alias,
|
|
4262
4332
|
/** The effect's declared dials, spliced and bound at 7 as everywhere else. */
|
|
4263
|
-
params
|
|
4333
|
+
params,
|
|
4334
|
+
/** This effect's own grid, already built, or null. Ribbons read it; the grid
|
|
4335
|
+
* pass owns it. */
|
|
4336
|
+
grid) {
|
|
4264
4337
|
// `slots` here is how many RIBBONS to draw — one per trailed anchor — which
|
|
4265
4338
|
// is a different number from the anchor ADDRESS SPACE the accessors index
|
|
4266
4339
|
// by. Conflating the two is what made a trail declared after a bare anchor
|
|
@@ -4270,7 +4343,15 @@ export class Engine {
|
|
|
4270
4343
|
// nothing before: ribbon i was read as anchor slot i.
|
|
4271
4344
|
const ribbonSlots = anchors.map((a, i) => (a.trail ? i : -1)).filter((i) => i >= 0);
|
|
4272
4345
|
const slots = ribbonSlots.length;
|
|
4273
|
-
const src = {
|
|
4346
|
+
const src = {
|
|
4347
|
+
wgsl,
|
|
4348
|
+
slots,
|
|
4349
|
+
ribbonSlots,
|
|
4350
|
+
blend: (d.particleBlend === "additive" ? "additive" : "alpha"),
|
|
4351
|
+
bloom: d.bloom,
|
|
4352
|
+
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
4353
|
+
gridSize: grid?.size ?? 0,
|
|
4354
|
+
};
|
|
4274
4355
|
const code = buildTrailShader(src, {
|
|
4275
4356
|
subjects: MAX_EFFECT_SUBJECTS,
|
|
4276
4357
|
samples: TRAIL_SAMPLES,
|
|
@@ -4318,6 +4399,13 @@ export class Engine {
|
|
|
4318
4399
|
...(params.buffer
|
|
4319
4400
|
? [{ binding: 7, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }]
|
|
4320
4401
|
: []),
|
|
4402
|
+
// The grid, always — compiled in whether the author calls it or not.
|
|
4403
|
+
{
|
|
4404
|
+
binding: 8,
|
|
4405
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
4406
|
+
texture: { sampleType: "float" },
|
|
4407
|
+
},
|
|
4408
|
+
{ binding: 9, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
4321
4409
|
],
|
|
4322
4410
|
});
|
|
4323
4411
|
// TWO targets, the scene pass's own: HDR colour and the aux (bloom mask,
|
|
@@ -4363,30 +4451,8 @@ export class Engine {
|
|
|
4363
4451
|
data: new Float32Array(4),
|
|
4364
4452
|
pipeline,
|
|
4365
4453
|
layout,
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
entries: [
|
|
4369
|
-
{ binding: 0, resource: { buffer: this.castBuffer } },
|
|
4370
|
-
{ binding: 1, resource: { buffer: uniform } },
|
|
4371
|
-
{ binding: 2, resource: { buffer: this.cameraUniformBuffer } },
|
|
4372
|
-
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4373
|
-
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4374
|
-
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4375
|
-
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
4376
|
-
],
|
|
4377
|
-
}),
|
|
4378
|
-
mirrorBind: this.device.createBindGroup({
|
|
4379
|
-
layout,
|
|
4380
|
-
entries: [
|
|
4381
|
-
{ binding: 0, resource: { buffer: this.castBuffer } },
|
|
4382
|
-
{ binding: 1, resource: { buffer: uniform } },
|
|
4383
|
-
{ binding: 2, resource: { buffer: this.mirrorCameraBuffer } },
|
|
4384
|
-
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4385
|
-
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4386
|
-
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4387
|
-
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
4388
|
-
],
|
|
4389
|
-
}),
|
|
4454
|
+
binds: this.trailBindGroups(layout, uniform, params.buffer, this.cameraUniformBuffer, grid),
|
|
4455
|
+
mirrorBinds: this.trailBindGroups(layout, uniform, params.buffer, this.mirrorCameraBuffer, grid),
|
|
4390
4456
|
},
|
|
4391
4457
|
};
|
|
4392
4458
|
}
|
|
@@ -4447,7 +4513,8 @@ export class Engine {
|
|
|
4447
4513
|
this.device.queue.writeBuffer(t.uniform, 0, t.data.buffer);
|
|
4448
4514
|
}
|
|
4449
4515
|
pass.setPipeline(t.pipeline);
|
|
4450
|
-
|
|
4516
|
+
const parity = e.grid?.parity ?? 0;
|
|
4517
|
+
pass.setBindGroup(0, view === "mirror" ? t.mirrorBinds[parity] : t.binds[parity]);
|
|
4451
4518
|
pass.draw(6, t.slots * live * (TRAIL_SAMPLES - 1) * TRAIL_SUBDIVISIONS);
|
|
4452
4519
|
}
|
|
4453
4520
|
}
|
|
@@ -4575,33 +4642,35 @@ export class Engine {
|
|
|
4575
4642
|
// one, so it has to be part of every group built against that layout —
|
|
4576
4643
|
// including this one. Omitting it here is not a missing uniform, it is a
|
|
4577
4644
|
// group WebGPU refuses outright, which takes the whole frame down.
|
|
4578
|
-
|
|
4579
|
-
t.
|
|
4580
|
-
layout: t.layout,
|
|
4581
|
-
entries: [
|
|
4582
|
-
{ binding: 0, resource: { buffer: this.castBuffer } },
|
|
4583
|
-
{ binding: 1, resource: { buffer: t.uniform } },
|
|
4584
|
-
{ binding: 2, resource: { buffer: this.cameraUniformBuffer } },
|
|
4585
|
-
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4586
|
-
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4587
|
-
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4588
|
-
...paramsEntry,
|
|
4589
|
-
],
|
|
4590
|
-
});
|
|
4591
|
-
t.mirrorBind = this.device.createBindGroup({
|
|
4592
|
-
layout: t.layout,
|
|
4593
|
-
entries: [
|
|
4594
|
-
{ binding: 0, resource: { buffer: this.castBuffer } },
|
|
4595
|
-
{ binding: 1, resource: { buffer: t.uniform } },
|
|
4596
|
-
{ binding: 2, resource: { buffer: this.mirrorCameraBuffer } },
|
|
4597
|
-
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4598
|
-
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4599
|
-
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4600
|
-
...paramsEntry,
|
|
4601
|
-
],
|
|
4602
|
-
});
|
|
4645
|
+
t.binds = this.trailBindGroups(t.layout, t.uniform, t.params, this.cameraUniformBuffer, e.grid);
|
|
4646
|
+
t.mirrorBinds = this.trailBindGroups(t.layout, t.uniform, t.params, this.mirrorCameraBuffer, e.grid);
|
|
4603
4647
|
}
|
|
4604
4648
|
}
|
|
4649
|
+
/**
|
|
4650
|
+
* A ribbon's bind group, ONE PER GRID PARITY.
|
|
4651
|
+
*
|
|
4652
|
+
* One author for both the build and the rebind, because they drifted once
|
|
4653
|
+
* already: the params binding is part of the LAYOUT whenever the effect
|
|
4654
|
+
* declared one, so it has to be part of every group built against that
|
|
4655
|
+
* layout, and a group missing it is not a missing uniform but one WebGPU
|
|
4656
|
+
* refuses outright, taking the whole frame down.
|
|
4657
|
+
*/
|
|
4658
|
+
trailBindGroups(layout, uniform, params, camera, grid) {
|
|
4659
|
+
return [0, 1].map((parity) => this.device.createBindGroup({
|
|
4660
|
+
layout,
|
|
4661
|
+
entries: [
|
|
4662
|
+
{ binding: 0, resource: { buffer: this.castBuffer } },
|
|
4663
|
+
{ binding: 1, resource: { buffer: uniform } },
|
|
4664
|
+
{ binding: 2, resource: { buffer: camera } },
|
|
4665
|
+
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4666
|
+
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4667
|
+
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4668
|
+
...(params ? [{ binding: EFFECT_PARAMS_BINDING, resource: { buffer: params } }] : []),
|
|
4669
|
+
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
4670
|
+
{ binding: 9, resource: this.simSampler },
|
|
4671
|
+
],
|
|
4672
|
+
}));
|
|
4673
|
+
}
|
|
4605
4674
|
releaseParticles() {
|
|
4606
4675
|
for (const e of this.effects) {
|
|
4607
4676
|
e.particles?.buffer.destroy();
|
|
@@ -10435,6 +10504,39 @@ export class Engine {
|
|
|
10435
10504
|
out[2] = cz;
|
|
10436
10505
|
out[3] = any ? r : 0;
|
|
10437
10506
|
}
|
|
10507
|
+
/**
|
|
10508
|
+
* Dress the floor in whatever the installed effects ask for.
|
|
10509
|
+
*
|
|
10510
|
+
* The LAST effect declaring `#ground` wins, faded by its own weight so a
|
|
10511
|
+
* scheduled lawn brings its soil in with it and takes it away again. Three
|
|
10512
|
+
* floats at the top of the ground block, written only when they change —
|
|
10513
|
+
* the same bargain writeGroundCasterSphere strikes, for the same reason.
|
|
10514
|
+
*/
|
|
10515
|
+
writeGroundDress() {
|
|
10516
|
+
const gb = this.groundMaterialData;
|
|
10517
|
+
if (!gb || !this.groundShadowMaterialBuffer)
|
|
10518
|
+
return;
|
|
10519
|
+
let want = this.groundBaseColor;
|
|
10520
|
+
let noise = this.groundBaseNoise;
|
|
10521
|
+
for (const e of this.effects) {
|
|
10522
|
+
if (!e.ground || e.weight <= 0)
|
|
10523
|
+
continue;
|
|
10524
|
+
const w = e.weight;
|
|
10525
|
+
want = new Vec3(want.x + (e.ground.color.x - want.x) * w, want.y + (e.ground.color.y - want.y) * w, want.z + (e.ground.color.z - want.z) * w);
|
|
10526
|
+
noise += (e.ground.noise - noise) * w;
|
|
10527
|
+
}
|
|
10528
|
+
if (gb[0] === want.x && gb[1] === want.y && gb[2] === want.z && gb[10] === noise)
|
|
10529
|
+
return;
|
|
10530
|
+
gb[0] = want.x;
|
|
10531
|
+
gb[1] = want.y;
|
|
10532
|
+
gb[2] = want.z;
|
|
10533
|
+
gb[10] = noise;
|
|
10534
|
+
// Two writes rather than one over the span: the seven floats between hold
|
|
10535
|
+
// the fades and the shadow strength, and this is not the place that owns
|
|
10536
|
+
// them.
|
|
10537
|
+
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 0, gb.subarray(0, 3));
|
|
10538
|
+
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 40, gb.subarray(10, 11));
|
|
10539
|
+
}
|
|
10438
10540
|
/**
|
|
10439
10541
|
* Push this frame's caster sphere into the ground's uniform.
|
|
10440
10542
|
*
|
|
@@ -11307,6 +11409,8 @@ export class Engine {
|
|
|
11307
11409
|
// keeping the uniform vec4-aligned.
|
|
11308
11410
|
const gb = new Float32Array(24);
|
|
11309
11411
|
this.groundMaterialData = gb;
|
|
11412
|
+
this.groundBaseColor = new Vec3(diffuseColor.x, diffuseColor.y, diffuseColor.z);
|
|
11413
|
+
this.groundBaseNoise = noiseStrength;
|
|
11310
11414
|
gb[0] = diffuseColor.x;
|
|
11311
11415
|
gb[1] = diffuseColor.y;
|
|
11312
11416
|
gb[2] = diffuseColor.z;
|
|
@@ -13006,6 +13110,7 @@ export class Engine {
|
|
|
13006
13110
|
this.dispatchCull(encoder);
|
|
13007
13111
|
// After the cull, which is what recomputes the spheres it unions.
|
|
13008
13112
|
this.writeGroundCasterSphere();
|
|
13113
|
+
this.writeGroundDress();
|
|
13009
13114
|
// After the cull, because a rebuild there can reallocate the argument
|
|
13010
13115
|
// buffers and a bundle captures the buffer it recorded against.
|
|
13011
13116
|
if (this.bundlesDirty)
|
|
@@ -14843,7 +14948,12 @@ Engine.JIGGLE_DAMPING_SCALE = 0.5;
|
|
|
14843
14948
|
* prefix sum and readback a compacted draw list would need every frame.
|
|
14844
14949
|
*/
|
|
14845
14950
|
/** Ceiling for `#particles`. Past this an author is asking for a stall. */
|
|
14846
|
-
|
|
14951
|
+
// Raised for the lawn: 65536 blades at real density is a 40-unit patch and
|
|
14952
|
+
// an 80-unit one wants four hundred thousand. A cutout pool's cost is
|
|
14953
|
+
// bounded by the pixels it covers, not by the count — the step is one
|
|
14954
|
+
// compute invocation per slot, the pool buffer is 48 bytes each (24 MB at
|
|
14955
|
+
// the cap), and a culled blade is a degenerate quad the rasteriser drops.
|
|
14956
|
+
Engine.MAX_PARTICLES = 524288;
|
|
14847
14957
|
/**
|
|
14848
14958
|
* The field layer: user background/foreground mounts, ONE TARGET PAIR PER
|
|
14849
14959
|
* RESOLUTION. Index 0 is full, index 1 is half — coarsest last, so the
|
|
@@ -20,13 +20,30 @@ export type EffectDirectives = {
|
|
|
20
20
|
/** The field layer composites additively rather than over. */
|
|
21
21
|
additiveLayer: boolean;
|
|
22
22
|
/** Particle blend, which is a different axis from the field layer's. */
|
|
23
|
-
|
|
23
|
+
/** `cutout` is alpha WITH DEPTH: the pipeline writes depth and turns the
|
|
24
|
+
* fragment's alpha into MSAA sample coverage, so a blade of grass behind
|
|
25
|
+
* another is rejected by the depth test instead of blended under it. What
|
|
26
|
+
* every real grass renderer does, and the only thing that bounds a dense
|
|
27
|
+
* lawn's cost by the pixels it covers rather than by how deep it stacks. */
|
|
28
|
+
particleBlend: "alpha" | "additive" | "cutout";
|
|
24
29
|
particles: number;
|
|
25
30
|
lights: number;
|
|
26
31
|
grid: number;
|
|
27
32
|
bloom: boolean;
|
|
28
33
|
/** This effect takes the cast apart — the host reads the timing. */
|
|
29
34
|
dissolve: boolean;
|
|
35
|
+
/** `#ground #rrggbb [noise]`: how this effect DRESSES THE FLOOR while it is
|
|
36
|
+
* installed — a colour as linear RGB, and the strength of the ground's own
|
|
37
|
+
* grain (0 flat, 1 strong), which is what keeps soil from being a swatch.
|
|
38
|
+
* A lawn wants earth under it, and the earth has to be the engine's own
|
|
39
|
+
* ground rather than a layer painted over the frame: the ground is geometry
|
|
40
|
+
* in the scene pass, so a shadow lands on it and it tone-maps with
|
|
41
|
+
* everything else. The scene's own floor returns when the effect goes.
|
|
42
|
+
* Null when the file declares none. */
|
|
43
|
+
ground: {
|
|
44
|
+
color: [number, number, number];
|
|
45
|
+
noise: number;
|
|
46
|
+
} | null;
|
|
30
47
|
/**
|
|
31
48
|
* This effect IS a mirror: a plane in the scene showing a true reflection of
|
|
32
49
|
* it, folded and re-rendered by the engine.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"directives.d.ts","sourceRoot":"","sources":["../../src/shaders/directives.ts"],"names":[],"mappings":"AA8BA,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;IAChC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACjD,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6EAA6E;IAC7E,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;IAC3C,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,0EAA0E;IAC1E,UAAU,EAAE,CAAC,GAAG,CAAC,CAAA;IACjB,8DAA8D;IAC9D,aAAa,EAAE,OAAO,CAAA;IACtB,wEAAwE;IACxE,aAAa,EAAE,OAAO,GAAG,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"directives.d.ts","sourceRoot":"","sources":["../../src/shaders/directives.ts"],"names":[],"mappings":"AA8BA,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;IAChC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACjD,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6EAA6E;IAC7E,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;IAC3C,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,0EAA0E;IAC1E,UAAU,EAAE,CAAC,GAAG,CAAC,CAAA;IACjB,8DAA8D;IAC9D,aAAa,EAAE,OAAO,CAAA;IACtB,wEAAwE;IACxE;;;;iFAI6E;IAC7E,aAAa,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAA;IAC9C,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAA;IACjB;;;;;;;4CAOwC;IACxC,MAAM,EAAE;QAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACjE;;;;;;;;;;;;;OAaG;IACH,MAAM,EAAE,OAAO,CAAA;IACf;;;;;;;;;;;;;OAaG;IACH,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAmBD;;;+BAG+B;AAC/B,eAAO,MAAM,cAAc,QAAmC,CAAA;AAG9D;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,QAA8B,CAAA;AAmBzD,MAAM,MAAM,eAAe,GAAG;IAAE,UAAU,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEhF,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CA2K7D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpD"}
|
|
@@ -41,6 +41,7 @@ const SPEC = {
|
|
|
41
41
|
dissolve: 0,
|
|
42
42
|
mirror: 0,
|
|
43
43
|
duration: 1,
|
|
44
|
+
ground: "rest",
|
|
44
45
|
};
|
|
45
46
|
/** A line that declares something, and what it declares. Exported because an
|
|
46
47
|
* editor highlights by the same rule the parser reads by — a highlighter with
|
|
@@ -89,6 +90,7 @@ export function parseDirectives(wgsl) {
|
|
|
89
90
|
grid: 0,
|
|
90
91
|
bloom: false,
|
|
91
92
|
dissolve: false,
|
|
93
|
+
ground: null,
|
|
92
94
|
mirror: false,
|
|
93
95
|
duration: 0,
|
|
94
96
|
};
|
|
@@ -173,15 +175,32 @@ export function parseDirectives(wgsl) {
|
|
|
173
175
|
d.additiveLayer = true;
|
|
174
176
|
return;
|
|
175
177
|
case "blend":
|
|
176
|
-
if (args[0] !== "additive") {
|
|
177
|
-
errors.push(`${at}: #blend takes "additive" — alpha is the default`);
|
|
178
|
+
if (args[0] !== "additive" && args[0] !== "cutout") {
|
|
179
|
+
errors.push(`${at}: #blend takes "additive" or "cutout" — alpha is the default`);
|
|
178
180
|
return;
|
|
179
181
|
}
|
|
180
|
-
d.particleBlend =
|
|
182
|
+
d.particleBlend = args[0];
|
|
181
183
|
return;
|
|
182
184
|
case "bloom":
|
|
183
185
|
d.bloom = true;
|
|
184
186
|
return;
|
|
187
|
+
case "ground": {
|
|
188
|
+
const m = /^#([0-9a-fA-F]{6})$/.exec(args[0] ?? "");
|
|
189
|
+
const noise = args[1] === undefined ? 0 : num(args[1]);
|
|
190
|
+
if (!m || args.length > 2 || noise === null) {
|
|
191
|
+
errors.push(`${at}: #ground takes a colour like #4b4026 and optionally a grain strength 0..1`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const v = parseInt(m[1], 16);
|
|
195
|
+
// sRGB in the file, linear in the uniform — the same transfer the
|
|
196
|
+
// ground's own colour goes through on the way in.
|
|
197
|
+
const lin = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
198
|
+
d.ground = {
|
|
199
|
+
color: [lin(((v >> 16) & 255) / 255), lin(((v >> 8) & 255) / 255), lin((v & 255) / 255)],
|
|
200
|
+
noise: Math.min(Math.max(noise, 0), 1),
|
|
201
|
+
};
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
185
204
|
case "dissolve":
|
|
186
205
|
d.dissolve = true;
|
|
187
206
|
return;
|
|
@@ -14,7 +14,9 @@ export type CastLayout = {
|
|
|
14
14
|
alias: number[];
|
|
15
15
|
};
|
|
16
16
|
/** How the author's quads combine with the scene. */
|
|
17
|
-
|
|
17
|
+
/** `cutout` shades exactly like `alpha`; the difference is entirely in the
|
|
18
|
+
* pipeline — depth write on, alpha-to-coverage on. See directives.ts. */
|
|
19
|
+
type ParticleBlend = "alpha" | "additive" | "cutout";
|
|
18
20
|
type ParticleSource = {
|
|
19
21
|
/** The author's WGSL verbatim. */
|
|
20
22
|
wgsl: string;
|
|
@@ -27,14 +29,28 @@ type ParticleSource = {
|
|
|
27
29
|
* rule reads the same dial the shading does, and rain's fall speed is used
|
|
28
30
|
* in the compute half. */
|
|
29
31
|
paramsDecl: string;
|
|
32
|
+
/** The effect defines `particleCover(p, uv) -> f32`: the shape alone, cheap.
|
|
33
|
+
* A cutout then draws TWICE — coverage to depth first, then the shading at
|
|
34
|
+
* depthCompare equal — so the lighting runs only for the visible layer. A
|
|
35
|
+
* discard forbids the hardware's own early rejection on a tile-based GPU,
|
|
36
|
+
* where every stacked fragment is otherwise shaded in full; the prepass is
|
|
37
|
+
* the shape of that rejection, done by hand and cheaply. */
|
|
38
|
+
cover: boolean;
|
|
39
|
+
/** The side of this effect's grid, or 0 when it declared none. Particles READ
|
|
40
|
+
* the grid; the kernel that steps it lives in the grid pass. A blade of grass
|
|
41
|
+
* bending where a foot pressed is the whole case: the bend has to persist and
|
|
42
|
+
* recover, which is memory, and memory is what the grid is for. */
|
|
43
|
+
gridSize: number;
|
|
30
44
|
};
|
|
31
45
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
32
46
|
export declare const PARTICLE_STRIDE = 48;
|
|
33
|
-
/** Does the source define the particle contract? All three are required
|
|
47
|
+
/** Does the source define the particle contract? All three are required
|
|
48
|
+
* together; `cover` is optional and only means anything to a cutout. */
|
|
34
49
|
export declare function particleEntryPoints(wgsl: string): {
|
|
35
50
|
init: boolean;
|
|
36
51
|
step: boolean;
|
|
37
52
|
shade: boolean;
|
|
53
|
+
cover: boolean;
|
|
38
54
|
};
|
|
39
55
|
/**
|
|
40
56
|
* Spawn, age, recycle.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA6BA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAkCD,qDAAqD;AACrD;0EAC0E;AAC1E,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEpD,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;+BAE2B;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;iEAK6D;IAC7D,KAAK,EAAE,OAAO,CAAA;IACd;;;wEAGoE;IACpE,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC;yEACyE;AACzE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAOlH;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAmDxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAuLvF"}
|