reze-engine 0.57.6 → 0.58.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/dist/engine.d.ts +31 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +231 -82
- 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/lights.d.ts +2 -10
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +4 -24
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +3 -17
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +2 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +2 -0
- package/dist/shaders/passes/particles.d.ts +20 -2
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +77 -5
- 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 +7 -1
- package/dist/shaders/scene-light-api.d.ts +42 -0
- package/dist/shaders/scene-light-api.d.ts.map +1 -0
- package/dist/shaders/scene-light-api.js +144 -0
- package/dist/shaders/shadow-api.d.ts +29 -0
- package/dist/shaders/shadow-api.d.ts.map +1 -0
- package/dist/shaders/shadow-api.js +112 -0
- package/package.json +1 -1
- package/src/engine.ts +272 -96
- package/src/shaders/directives.ts +37 -4
- package/src/shaders/lights.ts +4 -24
- package/src/shaders/materials/common.ts +3 -17
- package/src/shaders/passes/composite.ts +2 -0
- package/src/shaders/passes/grid.ts +2 -0
- package/src/shaders/passes/particles.ts +93 -6
- package/src/shaders/passes/trails.ts +10 -1
- package/src/shaders/scene-light-api.ts +148 -0
package/dist/engine.js
CHANGED
|
@@ -41,7 +41,7 @@ import { boneOverlay, boneMarkerPositions, buildOverlayShapes, jointOverlay, lin
|
|
|
41
41
|
import { BLOOM_BLIT_SHADER_WGSL, BLOOM_DOWNSAMPLE_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
|
|
42
42
|
import { AGX_LUT_GZ, AGX_LUT_SIZE } from "./shaders/agx-lut";
|
|
43
43
|
import { buildCompositeShader, EFFECT_SCENE_API, buildFieldShader, EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, } from "./shaders/passes/composite";
|
|
44
|
-
import { buildParticleComputeShader, buildParticleRenderShader, particleEntryPoints, PARTICLE_STRIDE, } from "./shaders/passes/particles";
|
|
44
|
+
import { buildParticleComputeShader, buildParticleRenderShader, PARTICLE_LIGHT_BINDING, particleEntryPoints, PARTICLE_STRIDE, } from "./shaders/passes/particles";
|
|
45
45
|
import { SIM_FORMAT, GRID_MAX, buildSimShader, gridEntryPoint, } from "./shaders/passes/grid";
|
|
46
46
|
import { buildCastResolveShader, buildCastSeedShader, buildCastStepShader, castDistanceUsed, CAST_COVERAGE_FORMAT, CAST_DIST_FORMAT, CAST_FIELD_DIV, CAST_SEED_FORMAT, } from "./shaders/passes/cast-distance";
|
|
47
47
|
import { buildTrailShader, trailEntryPoints, TRAIL_SUBDIVISIONS } from "./shaders/passes/trails";
|
|
@@ -1305,6 +1305,13 @@ 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;
|
|
1312
|
+
/** The caster sphere as last written after the cascade matrices; NaN so the
|
|
1313
|
+
* first frame always writes. */
|
|
1314
|
+
this.shadowCastersWritten = new Float32Array([NaN, NaN, NaN, NaN]);
|
|
1308
1315
|
/** Set by the first getGpuTimings() call. See it for why asking is the switch. */
|
|
1309
1316
|
this.timestampsWanted = false;
|
|
1310
1317
|
this.groundMirrorPipeline = null;
|
|
@@ -3034,9 +3041,9 @@ export class Engine {
|
|
|
3034
3041
|
for (const e of this.effects) {
|
|
3035
3042
|
if (e.particles) {
|
|
3036
3043
|
const b = e.particles.rebind();
|
|
3037
|
-
e.particles.
|
|
3038
|
-
e.particles.
|
|
3039
|
-
e.particles.
|
|
3044
|
+
e.particles.computeBinds = b.computeBinds;
|
|
3045
|
+
e.particles.renderBinds = b.renderBinds;
|
|
3046
|
+
e.particles.mirrorRenderBinds = b.mirrorRenderBinds;
|
|
3040
3047
|
}
|
|
3041
3048
|
if (e.grid)
|
|
3042
3049
|
e.grid.binds = [this.gridBindGroup(e.grid, 0), this.gridBindGroup(e.grid, 1)];
|
|
@@ -3567,18 +3574,21 @@ export class Engine {
|
|
|
3567
3574
|
trails?.uniform.destroy();
|
|
3568
3575
|
return { ok: false, diagnostics, mounts, params: d.params, duration: d.duration, readsCast: false };
|
|
3569
3576
|
};
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
return abandon(built.diagnostics);
|
|
3574
|
-
particles = built.state;
|
|
3575
|
-
}
|
|
3577
|
+
// THE GRID FIRST. Particles and ribbons READ it — a blade of grass reads the
|
|
3578
|
+
// bend a foot left there — so the textures have to exist before their bind
|
|
3579
|
+
// groups are built. Nothing reads them back, so the grid depends on neither.
|
|
3576
3580
|
if (gridEntryPoint(wgsl)) {
|
|
3577
3581
|
const built = await this.buildSim(wgsl, d, anchors, alias, paramsFor);
|
|
3578
3582
|
if (!built.ok)
|
|
3579
3583
|
return abandon(built.diagnostics);
|
|
3580
3584
|
grid = built.state;
|
|
3581
3585
|
}
|
|
3586
|
+
if (wantsParticles) {
|
|
3587
|
+
const built = await this.buildParticles(wgsl, d, anchors, alias, paramsFor, grid);
|
|
3588
|
+
if (!built.ok)
|
|
3589
|
+
return abandon(built.diagnostics);
|
|
3590
|
+
particles = built.state;
|
|
3591
|
+
}
|
|
3582
3592
|
if (wantsTrails) {
|
|
3583
3593
|
// Only anchors that asked for `trail` have a path to draw; a ribbon on a
|
|
3584
3594
|
// bone recorded without one would read zeroes and paint a line to the origin.
|
|
@@ -3586,7 +3596,7 @@ export class Engine {
|
|
|
3586
3596
|
if (trailSlots === 0) {
|
|
3587
3597
|
return abandon(["a ribbon effect needs at least one #anchor <bone> trail"]);
|
|
3588
3598
|
}
|
|
3589
|
-
const built = await this.buildTrails(wgsl, d, anchors, alias, paramsFor);
|
|
3599
|
+
const built = await this.buildTrails(wgsl, d, anchors, alias, paramsFor, grid);
|
|
3590
3600
|
if (!built.ok)
|
|
3591
3601
|
return abandon(built.diagnostics);
|
|
3592
3602
|
trails = built.state;
|
|
@@ -3665,6 +3675,7 @@ export class Engine {
|
|
|
3665
3675
|
simStep: 0,
|
|
3666
3676
|
simReset: false,
|
|
3667
3677
|
dissolve: d.dissolve ? dissolveConstants(authored) : null,
|
|
3678
|
+
ground: d.ground ? { color: new Vec3(...d.ground.color), noise: d.ground.noise } : null,
|
|
3668
3679
|
// Its OWN resolution, no longer the scene's: an effect that never asked
|
|
3669
3680
|
// for full res is not promoted because a neighbour did.
|
|
3670
3681
|
// FULL RESOLUTION UNLESS TOLD OTHERWISE.
|
|
@@ -3895,11 +3906,23 @@ export class Engine {
|
|
|
3895
3906
|
alias,
|
|
3896
3907
|
/** The effect's declared dials. Spliced into both stages and bound at 7,
|
|
3897
3908
|
* the same binding the composite gives them. */
|
|
3898
|
-
params
|
|
3909
|
+
params,
|
|
3910
|
+
/** This effect's own grid, already built, or null when it declared none.
|
|
3911
|
+
* Read-only here: the pool samples it, the grid pass owns it. */
|
|
3912
|
+
grid) {
|
|
3899
3913
|
// No pragma means "some": an author who wrote the trio clearly wants
|
|
3900
3914
|
// particles, and failing over a missing comment would be pedantry.
|
|
3901
3915
|
const count = Math.min(d.particles || 1024, Engine.MAX_PARTICLES);
|
|
3902
|
-
const src = {
|
|
3916
|
+
const src = {
|
|
3917
|
+
wgsl,
|
|
3918
|
+
count,
|
|
3919
|
+
blend: d.particleBlend,
|
|
3920
|
+
bloom: d.bloom,
|
|
3921
|
+
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
3922
|
+
gridSize: grid?.size ?? 0,
|
|
3923
|
+
cover: particleEntryPoints(wgsl).cover,
|
|
3924
|
+
};
|
|
3925
|
+
const prepass = src.blend === "cutout" && src.cover;
|
|
3903
3926
|
// Sparks want to spawn where a trail is, so the particle stages see the same
|
|
3904
3927
|
// cast buffer the trail draw reads.
|
|
3905
3928
|
const cast = {
|
|
@@ -3952,7 +3975,7 @@ export class Engine {
|
|
|
3952
3975
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
3953
3976
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
3954
3977
|
// due to a previous error".
|
|
3955
|
-
const layoutFor = (storage, visibility) => this.device.createBindGroupLayout({
|
|
3978
|
+
const layoutFor = (storage, visibility, shadow) => this.device.createBindGroupLayout({
|
|
3956
3979
|
entries: [
|
|
3957
3980
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
3958
3981
|
{ binding: 1, visibility, buffer: { type: "uniform" } },
|
|
@@ -3966,9 +3989,30 @@ export class Engine {
|
|
|
3966
3989
|
// Only when the effect declared any: WGSL has no empty struct, so a
|
|
3967
3990
|
// param-less effect must not carry the decl OR the binding.
|
|
3968
3991
|
...(params.buffer ? [{ binding: 7, visibility, buffer: { type: "uniform" } }] : []),
|
|
3992
|
+
// The grid, ALWAYS — the accessor is compiled in unconditionally, and a
|
|
3993
|
+
// binding the shader declares must exist in the layout whether the
|
|
3994
|
+
// author calls it or not. With no grid it is the 1x1 of zeroes.
|
|
3995
|
+
{ binding: 8, visibility, texture: { sampleType: "float" } },
|
|
3996
|
+
{ binding: 9, visibility, sampler: { type: "filtering" } },
|
|
3997
|
+
// The scene's light, for rzShadow and rzWorldAmbient — the shading
|
|
3998
|
+
// stage only; the compute module compiles the stubs. See
|
|
3999
|
+
// scene-light-api.ts.
|
|
4000
|
+
...(shadow
|
|
4001
|
+
? [
|
|
4002
|
+
{ binding: PARTICLE_LIGHT_BINDING, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
4003
|
+
{ binding: PARTICLE_LIGHT_BINDING + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
4004
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
4005
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
4006
|
+
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
4007
|
+
]
|
|
4008
|
+
: []),
|
|
3969
4009
|
],
|
|
3970
4010
|
});
|
|
3971
|
-
|
|
4011
|
+
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
4012
|
+
* alternates which texture is current, and rebuilding a single group every
|
|
4013
|
+
* frame is waste for a change that only ever toggles between two known
|
|
4014
|
+
* states. Both entries are the same view when there is no grid. */
|
|
4015
|
+
const bindFor = (layout, camera, shadow) => [0, 1].map((parity) => this.device.createBindGroup({
|
|
3972
4016
|
layout,
|
|
3973
4017
|
entries: [
|
|
3974
4018
|
{ binding: 0, resource: { buffer } },
|
|
@@ -3979,15 +4023,31 @@ export class Engine {
|
|
|
3979
4023
|
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
3980
4024
|
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
3981
4025
|
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
4026
|
+
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
4027
|
+
{ binding: 9, resource: this.simSampler },
|
|
4028
|
+
...(shadow
|
|
4029
|
+
? [
|
|
4030
|
+
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
4031
|
+
{ binding: PARTICLE_LIGHT_BINDING + 1, resource: { buffer: this.shadowLightVPBuffer } },
|
|
4032
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.shadowMapDepthViews[0] },
|
|
4033
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
4034
|
+
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
4035
|
+
]
|
|
4036
|
+
: []),
|
|
3982
4037
|
],
|
|
3983
|
-
});
|
|
3984
|
-
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE);
|
|
3985
|
-
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT);
|
|
4038
|
+
}));
|
|
4039
|
+
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false);
|
|
4040
|
+
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, true);
|
|
3986
4041
|
// Additive keeps the destination and adds to it, and leaves alpha alone, so
|
|
3987
4042
|
// a glow does not claim coverage it never occluded. The MASK sums with it —
|
|
3988
4043
|
// otherwise an additive effect could never reach the bloom gate. Both live
|
|
3989
4044
|
// in scene-contract as the "particle-additive" class.
|
|
4045
|
+
//
|
|
4046
|
+
// Cutout blends exactly like alpha and differs only in depth and coverage,
|
|
4047
|
+
// below — so it shares alpha's targets rather than adding a class whose
|
|
4048
|
+
// blends would be a copy.
|
|
3990
4049
|
const targets = sceneTargetsFor(src.blend === "additive" ? "particle-additive" : "particle", this.sceneFormats);
|
|
4050
|
+
const cutout = src.blend === "cutout";
|
|
3991
4051
|
this.device.pushErrorScope("validation");
|
|
3992
4052
|
try {
|
|
3993
4053
|
const compute = await this.device.createComputePipelineAsync({
|
|
@@ -3995,15 +4055,41 @@ export class Engine {
|
|
|
3995
4055
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [computeLayout] }),
|
|
3996
4056
|
compute: { module: computeModule, entryPoint: "main" },
|
|
3997
4057
|
});
|
|
4058
|
+
const renderLayoutObj = this.device.createPipelineLayout({ bindGroupLayouts: [renderLayout] });
|
|
4059
|
+
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
4060
|
+
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
4061
|
+
// the shading pass below then tests EQUAL and writes no depth.
|
|
4062
|
+
const depth = prepass
|
|
4063
|
+
? await this.device.createRenderPipelineAsync({
|
|
4064
|
+
label: "particle depth prepass pipeline",
|
|
4065
|
+
layout: renderLayoutObj,
|
|
4066
|
+
vertex: { module: renderModule, entryPoint: "vs" },
|
|
4067
|
+
fragment: { module: renderModule, entryPoint: "fsDepth", targets: sceneTargetsFor("depth-prepass", this.sceneFormats) },
|
|
4068
|
+
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
4069
|
+
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
4070
|
+
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4071
|
+
})
|
|
4072
|
+
: null;
|
|
3998
4073
|
const render = await this.device.createRenderPipelineAsync({
|
|
3999
4074
|
label: "particle render pipeline",
|
|
4000
|
-
layout:
|
|
4075
|
+
layout: renderLayoutObj,
|
|
4001
4076
|
vertex: { module: renderModule, entryPoint: "vs" },
|
|
4002
4077
|
fragment: { module: renderModule, entryPoint: "fs", targets },
|
|
4003
4078
|
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
4004
4079
|
// Tested but not WRITTEN: particles are transparent, so writing depth
|
|
4005
4080
|
// would make whichever quad drew first occlude the ones behind it.
|
|
4006
|
-
|
|
4081
|
+
//
|
|
4082
|
+
// Except a CUTOUT, which is opaque where it is drawn at all. Its alpha
|
|
4083
|
+
// becomes per-sample coverage (in the shader — see FSOut.samples, the
|
|
4084
|
+
// HDR target has no alpha for the hardware path), so its edges stay
|
|
4085
|
+
// antialiased under MSAA while its depth is real — and a blade behind
|
|
4086
|
+
// another blade is then rejected at the depth test rather than shaded
|
|
4087
|
+
// and blended under it.
|
|
4088
|
+
// For a lawn stacked ten blades deep that is the difference between
|
|
4089
|
+
// shading ten layers and shading about three.
|
|
4090
|
+
depthStencil: prepass
|
|
4091
|
+
? { format: this.depthFormat, depthWriteEnabled: false, depthCompare: "equal" }
|
|
4092
|
+
: { format: this.depthFormat, depthWriteEnabled: cutout, depthCompare: this.depthAhead },
|
|
4007
4093
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
4008
4094
|
});
|
|
4009
4095
|
const scoped = await this.device.popErrorScope();
|
|
@@ -4025,15 +4111,16 @@ export class Engine {
|
|
|
4025
4111
|
counts: uniformView.uints,
|
|
4026
4112
|
compute,
|
|
4027
4113
|
computeLayout,
|
|
4028
|
-
|
|
4114
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
4115
|
+
depth,
|
|
4029
4116
|
render,
|
|
4030
4117
|
renderLayout,
|
|
4031
|
-
|
|
4032
|
-
|
|
4118
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
4119
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
4033
4120
|
rebind: () => ({
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4121
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
4122
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
4123
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
4037
4124
|
}),
|
|
4038
4125
|
},
|
|
4039
4126
|
};
|
|
@@ -4226,7 +4313,7 @@ export class Engine {
|
|
|
4226
4313
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer);
|
|
4227
4314
|
const cp = encoder.beginComputePass({ label: "particles" });
|
|
4228
4315
|
cp.setPipeline(p.compute);
|
|
4229
|
-
cp.setBindGroup(0, p.
|
|
4316
|
+
cp.setBindGroup(0, p.computeBinds[e.grid?.parity ?? 0]);
|
|
4230
4317
|
cp.dispatchWorkgroups(Math.ceil(p.count / 64));
|
|
4231
4318
|
cp.end();
|
|
4232
4319
|
}
|
|
@@ -4237,8 +4324,15 @@ export class Engine {
|
|
|
4237
4324
|
const p = e.particles;
|
|
4238
4325
|
if (!p || e.weight === 0)
|
|
4239
4326
|
continue;
|
|
4327
|
+
const parity = e.grid?.parity ?? 0;
|
|
4328
|
+
const bind = view === "mirror" ? p.mirrorRenderBinds[parity] : p.renderBinds[parity];
|
|
4329
|
+
if (p.depth) {
|
|
4330
|
+
pass.setPipeline(p.depth);
|
|
4331
|
+
pass.setBindGroup(0, bind);
|
|
4332
|
+
pass.draw(6, p.count);
|
|
4333
|
+
}
|
|
4240
4334
|
pass.setPipeline(p.render);
|
|
4241
|
-
pass.setBindGroup(0,
|
|
4335
|
+
pass.setBindGroup(0, bind);
|
|
4242
4336
|
pass.draw(6, p.count);
|
|
4243
4337
|
}
|
|
4244
4338
|
}
|
|
@@ -4260,7 +4354,10 @@ export class Engine {
|
|
|
4260
4354
|
* describes the effect that is still on screen. */
|
|
4261
4355
|
alias,
|
|
4262
4356
|
/** The effect's declared dials, spliced and bound at 7 as everywhere else. */
|
|
4263
|
-
params
|
|
4357
|
+
params,
|
|
4358
|
+
/** This effect's own grid, already built, or null. Ribbons read it; the grid
|
|
4359
|
+
* pass owns it. */
|
|
4360
|
+
grid) {
|
|
4264
4361
|
// `slots` here is how many RIBBONS to draw — one per trailed anchor — which
|
|
4265
4362
|
// is a different number from the anchor ADDRESS SPACE the accessors index
|
|
4266
4363
|
// by. Conflating the two is what made a trail declared after a bare anchor
|
|
@@ -4270,7 +4367,15 @@ export class Engine {
|
|
|
4270
4367
|
// nothing before: ribbon i was read as anchor slot i.
|
|
4271
4368
|
const ribbonSlots = anchors.map((a, i) => (a.trail ? i : -1)).filter((i) => i >= 0);
|
|
4272
4369
|
const slots = ribbonSlots.length;
|
|
4273
|
-
const src = {
|
|
4370
|
+
const src = {
|
|
4371
|
+
wgsl,
|
|
4372
|
+
slots,
|
|
4373
|
+
ribbonSlots,
|
|
4374
|
+
blend: (d.particleBlend === "additive" ? "additive" : "alpha"),
|
|
4375
|
+
bloom: d.bloom,
|
|
4376
|
+
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
4377
|
+
gridSize: grid?.size ?? 0,
|
|
4378
|
+
};
|
|
4274
4379
|
const code = buildTrailShader(src, {
|
|
4275
4380
|
subjects: MAX_EFFECT_SUBJECTS,
|
|
4276
4381
|
samples: TRAIL_SAMPLES,
|
|
@@ -4318,6 +4423,13 @@ export class Engine {
|
|
|
4318
4423
|
...(params.buffer
|
|
4319
4424
|
? [{ binding: 7, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }]
|
|
4320
4425
|
: []),
|
|
4426
|
+
// The grid, always — compiled in whether the author calls it or not.
|
|
4427
|
+
{
|
|
4428
|
+
binding: 8,
|
|
4429
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
4430
|
+
texture: { sampleType: "float" },
|
|
4431
|
+
},
|
|
4432
|
+
{ binding: 9, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
4321
4433
|
],
|
|
4322
4434
|
});
|
|
4323
4435
|
// TWO targets, the scene pass's own: HDR colour and the aux (bloom mask,
|
|
@@ -4363,30 +4475,8 @@ export class Engine {
|
|
|
4363
4475
|
data: new Float32Array(4),
|
|
4364
4476
|
pipeline,
|
|
4365
4477
|
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
|
-
}),
|
|
4478
|
+
binds: this.trailBindGroups(layout, uniform, params.buffer, this.cameraUniformBuffer, grid),
|
|
4479
|
+
mirrorBinds: this.trailBindGroups(layout, uniform, params.buffer, this.mirrorCameraBuffer, grid),
|
|
4390
4480
|
},
|
|
4391
4481
|
};
|
|
4392
4482
|
}
|
|
@@ -4447,7 +4537,8 @@ export class Engine {
|
|
|
4447
4537
|
this.device.queue.writeBuffer(t.uniform, 0, t.data.buffer);
|
|
4448
4538
|
}
|
|
4449
4539
|
pass.setPipeline(t.pipeline);
|
|
4450
|
-
|
|
4540
|
+
const parity = e.grid?.parity ?? 0;
|
|
4541
|
+
pass.setBindGroup(0, view === "mirror" ? t.mirrorBinds[parity] : t.binds[parity]);
|
|
4451
4542
|
pass.draw(6, t.slots * live * (TRAIL_SAMPLES - 1) * TRAIL_SUBDIVISIONS);
|
|
4452
4543
|
}
|
|
4453
4544
|
}
|
|
@@ -4575,33 +4666,35 @@ export class Engine {
|
|
|
4575
4666
|
// one, so it has to be part of every group built against that layout —
|
|
4576
4667
|
// including this one. Omitting it here is not a missing uniform, it is a
|
|
4577
4668
|
// 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
|
-
});
|
|
4669
|
+
t.binds = this.trailBindGroups(t.layout, t.uniform, t.params, this.cameraUniformBuffer, e.grid);
|
|
4670
|
+
t.mirrorBinds = this.trailBindGroups(t.layout, t.uniform, t.params, this.mirrorCameraBuffer, e.grid);
|
|
4603
4671
|
}
|
|
4604
4672
|
}
|
|
4673
|
+
/**
|
|
4674
|
+
* A ribbon's bind group, ONE PER GRID PARITY.
|
|
4675
|
+
*
|
|
4676
|
+
* One author for both the build and the rebind, because they drifted once
|
|
4677
|
+
* already: the params binding is part of the LAYOUT whenever the effect
|
|
4678
|
+
* declared one, so it has to be part of every group built against that
|
|
4679
|
+
* layout, and a group missing it is not a missing uniform but one WebGPU
|
|
4680
|
+
* refuses outright, taking the whole frame down.
|
|
4681
|
+
*/
|
|
4682
|
+
trailBindGroups(layout, uniform, params, camera, grid) {
|
|
4683
|
+
return [0, 1].map((parity) => this.device.createBindGroup({
|
|
4684
|
+
layout,
|
|
4685
|
+
entries: [
|
|
4686
|
+
{ binding: 0, resource: { buffer: this.castBuffer } },
|
|
4687
|
+
{ binding: 1, resource: { buffer: uniform } },
|
|
4688
|
+
{ binding: 2, resource: { buffer: camera } },
|
|
4689
|
+
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4690
|
+
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4691
|
+
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4692
|
+
...(params ? [{ binding: EFFECT_PARAMS_BINDING, resource: { buffer: params } }] : []),
|
|
4693
|
+
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
4694
|
+
{ binding: 9, resource: this.simSampler },
|
|
4695
|
+
],
|
|
4696
|
+
}));
|
|
4697
|
+
}
|
|
4605
4698
|
releaseParticles() {
|
|
4606
4699
|
for (const e of this.effects) {
|
|
4607
4700
|
e.particles?.buffer.destroy();
|
|
@@ -5896,8 +5989,10 @@ export class Engine {
|
|
|
5896
5989
|
targets: sceneTargetsFor("depth-prepass", this.sceneFormats),
|
|
5897
5990
|
},
|
|
5898
5991
|
});
|
|
5992
|
+
// The matrices, then one vec4: the caster sphere, which effects' rzShadow
|
|
5993
|
+
// tests before any tap. The materials and the ground bind the matrices only.
|
|
5899
5994
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
5900
|
-
size: 64 * SHADOW_CASCADES.length,
|
|
5995
|
+
size: 64 * SHADOW_CASCADES.length + 16,
|
|
5901
5996
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
5902
5997
|
});
|
|
5903
5998
|
this.shadowCascadeVPBuffers = SHADOW_CASCADES.map((_, i) => this.device.createBuffer({
|
|
@@ -10435,6 +10530,51 @@ export class Engine {
|
|
|
10435
10530
|
out[2] = cz;
|
|
10436
10531
|
out[3] = any ? r : 0;
|
|
10437
10532
|
}
|
|
10533
|
+
/**
|
|
10534
|
+
* Dress the floor in whatever the installed effects ask for.
|
|
10535
|
+
*
|
|
10536
|
+
* The LAST effect declaring `#ground` wins, faded by its own weight so a
|
|
10537
|
+
* scheduled lawn brings its soil in with it and takes it away again. Three
|
|
10538
|
+
* floats at the top of the ground block, written only when they change —
|
|
10539
|
+
* the same bargain writeGroundCasterSphere strikes, for the same reason.
|
|
10540
|
+
*/
|
|
10541
|
+
writeGroundDress() {
|
|
10542
|
+
const gb = this.groundMaterialData;
|
|
10543
|
+
if (!gb || !this.groundShadowMaterialBuffer)
|
|
10544
|
+
return;
|
|
10545
|
+
let want = this.groundBaseColor;
|
|
10546
|
+
let noise = this.groundBaseNoise;
|
|
10547
|
+
for (const e of this.effects) {
|
|
10548
|
+
if (!e.ground || e.weight <= 0)
|
|
10549
|
+
continue;
|
|
10550
|
+
const w = e.weight;
|
|
10551
|
+
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);
|
|
10552
|
+
noise += (e.ground.noise - noise) * w;
|
|
10553
|
+
}
|
|
10554
|
+
if (gb[0] === want.x && gb[1] === want.y && gb[2] === want.z && gb[10] === noise)
|
|
10555
|
+
return;
|
|
10556
|
+
gb[0] = want.x;
|
|
10557
|
+
gb[1] = want.y;
|
|
10558
|
+
gb[2] = want.z;
|
|
10559
|
+
gb[10] = noise;
|
|
10560
|
+
// Two writes rather than one over the span: the seven floats between hold
|
|
10561
|
+
// the fades and the shadow strength, and this is not the place that owns
|
|
10562
|
+
// them.
|
|
10563
|
+
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 0, gb.subarray(0, 3));
|
|
10564
|
+
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 40, gb.subarray(10, 11));
|
|
10565
|
+
}
|
|
10566
|
+
/**
|
|
10567
|
+
* The same sphere, into the cascade block after its matrices — rzShadow's
|
|
10568
|
+
* copy. There whether or not a ground exists: a lawn is often the only floor.
|
|
10569
|
+
*/
|
|
10570
|
+
writeShadowCasters() {
|
|
10571
|
+
const w = this.shadowCastersWritten;
|
|
10572
|
+
const c = this.casterSphere;
|
|
10573
|
+
if (w[0] === c[0] && w[1] === c[1] && w[2] === c[2] && w[3] === c[3])
|
|
10574
|
+
return;
|
|
10575
|
+
w.set(c);
|
|
10576
|
+
this.device.queue.writeBuffer(this.shadowLightVPBuffer, 64 * SHADOW_CASCADES.length, c);
|
|
10577
|
+
}
|
|
10438
10578
|
/**
|
|
10439
10579
|
* Push this frame's caster sphere into the ground's uniform.
|
|
10440
10580
|
*
|
|
@@ -11307,6 +11447,8 @@ export class Engine {
|
|
|
11307
11447
|
// keeping the uniform vec4-aligned.
|
|
11308
11448
|
const gb = new Float32Array(24);
|
|
11309
11449
|
this.groundMaterialData = gb;
|
|
11450
|
+
this.groundBaseColor = new Vec3(diffuseColor.x, diffuseColor.y, diffuseColor.z);
|
|
11451
|
+
this.groundBaseNoise = noiseStrength;
|
|
11310
11452
|
gb[0] = diffuseColor.x;
|
|
11311
11453
|
gb[1] = diffuseColor.y;
|
|
11312
11454
|
gb[2] = diffuseColor.z;
|
|
@@ -13006,6 +13148,8 @@ export class Engine {
|
|
|
13006
13148
|
this.dispatchCull(encoder);
|
|
13007
13149
|
// After the cull, which is what recomputes the spheres it unions.
|
|
13008
13150
|
this.writeGroundCasterSphere();
|
|
13151
|
+
this.writeShadowCasters();
|
|
13152
|
+
this.writeGroundDress();
|
|
13009
13153
|
// After the cull, because a rebuild there can reallocate the argument
|
|
13010
13154
|
// buffers and a bundle captures the buffer it recorded against.
|
|
13011
13155
|
if (this.bundlesDirty)
|
|
@@ -14843,7 +14987,12 @@ Engine.JIGGLE_DAMPING_SCALE = 0.5;
|
|
|
14843
14987
|
* prefix sum and readback a compacted draw list would need every frame.
|
|
14844
14988
|
*/
|
|
14845
14989
|
/** Ceiling for `#particles`. Past this an author is asking for a stall. */
|
|
14846
|
-
|
|
14990
|
+
// Raised for the lawn: 65536 blades at real density is a 40-unit patch and
|
|
14991
|
+
// an 80-unit one wants four hundred thousand. A cutout pool's cost is
|
|
14992
|
+
// bounded by the pixels it covers, not by the count — the step is one
|
|
14993
|
+
// compute invocation per slot, the pool buffer is 48 bytes each (24 MB at
|
|
14994
|
+
// the cap), and a culled blade is a degenerate quad the rasteriser drops.
|
|
14995
|
+
Engine.MAX_PARTICLES = 524288;
|
|
14847
14996
|
/**
|
|
14848
14997
|
* The field layer: user background/foreground mounts, ONE TARGET PAIR PER
|
|
14849
14998
|
* 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;
|
package/dist/shaders/lights.d.ts
CHANGED
|
@@ -33,16 +33,8 @@ export declare const LIGHTS_FLOATS: number;
|
|
|
33
33
|
* write, which is the same trap the grid's step-only half documents.
|
|
34
34
|
*/
|
|
35
35
|
export declare const RZ_LIGHT_STRUCT_WGSL = "\n/** What an effect returns for one of its lights. */\nstruct RzLight {\n pos: vec3f,\n color: vec3f,\n intensity: f32,\n radius: f32,\n}\n";
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
39
|
-
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
40
|
-
*
|
|
41
|
-
* One string included by every module that declares LightUniforms with the sh
|
|
42
|
-
* block — the hosted-api lesson: a helper defined in some modules and not
|
|
43
|
-
* others is a compile error waiting for the first file that crosses them.
|
|
44
|
-
*/
|
|
45
|
-
export declare const WORLD_AMBIENT_WGSL = "\nfn rzWorldAmbient(n: vec3f) -> vec3f {\n if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }\n let x = n.x;\n let y = n.y;\n let z = n.z;\n let c = light.sh[0].xyz\n + light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x\n + light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)\n + light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)\n + light.sh[8].xyz * (x * x - y * y);\n return max(c, vec3f(0.0));\n}\n";
|
|
36
|
+
/** rzWorldAmbient against the materials' and the ground's `light` uniform. */
|
|
37
|
+
export declare const WORLD_AMBIENT_WGSL: string;
|
|
46
38
|
/** Does this source define the emit mount? */
|
|
47
39
|
export declare function hasLightEmit(wgsl: string): boolean;
|
|
48
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AA0CA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,kCAAkC;AAClC,eAAO,MAAM,aAAa,QAA2C,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,qJAQhC,CAAA;AAED,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,QAA4B,CAAA;AAE3D,8CAA8C;AAC9C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,EAC5B,UAAU,SAAK,GACd,MAAM,CAoGR;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA0FhE"}
|