reze-engine 0.58.2 → 0.58.4
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 +20 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +159 -33
- package/dist/graph/slots.js +1 -1
- package/dist/graph/style-group.d.ts +7 -0
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +3 -0
- package/dist/shaders/passes/particles.d.ts +18 -1
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +30 -1
- package/package.json +1 -1
- package/src/engine.ts +166 -32
- package/src/graph/slots.ts +1 -1
- package/src/graph/style-group.ts +12 -1
- package/src/shaders/materials/common.ts +3 -0
- package/src/shaders/passes/particles.ts +40 -2
package/dist/engine.js
CHANGED
|
@@ -42,7 +42,7 @@ import { boneOverlay, boneMarkerPositions, buildOverlayShapes, jointOverlay, lin
|
|
|
42
42
|
import { BLOOM_BLIT_SHADER_WGSL, BLOOM_DOWNSAMPLE_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
|
|
43
43
|
import { AGX_LUT_GZ, AGX_LUT_SIZE } from "./shaders/agx-lut";
|
|
44
44
|
import { buildCompositeShader, EFFECT_SCENE_API, buildFieldShader, EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, } from "./shaders/passes/composite";
|
|
45
|
-
import { buildParticleComputeShader, buildParticleRenderShader, PARTICLE_LIGHT_BINDING, PARTICLE_POINTS_BINDING, particleEntryPoints, PARTICLE_STRIDE, } from "./shaders/passes/particles";
|
|
45
|
+
import { buildParticleComputeShader, buildParticleRenderShader, PARTICLE_LIGHT_BINDING, PARTICLE_POINTS_BINDING, particleEntryPoints, PARTICLE_INDIRECT_BINDING, PARTICLE_INDIRECT_BYTES, PARTICLE_INDIRECT_DRAW_OFFSET, PARTICLE_STRIDE, } from "./shaders/passes/particles";
|
|
46
46
|
import { MAX_EFFECT_POINTS, POINTS_FLOATS } from "./shaders/points-api";
|
|
47
47
|
import { bonesWithPrefix, pointsData, writeBonePoint } from "./effect-points";
|
|
48
48
|
import { SIM_FORMAT, GRID_MAX, buildSimShader, gridEntryPoint, } from "./shaders/passes/grid";
|
|
@@ -1163,7 +1163,9 @@ export class Engine {
|
|
|
1163
1163
|
// GPU texture can't be read back cheaply, and PMX diffuse alpha is usually 1.0
|
|
1164
1164
|
// even for see-through cloth: the translucency lives in the texture).
|
|
1165
1165
|
this.textureAlphaCache = new Map();
|
|
1166
|
-
|
|
1166
|
+
/** One per format: a render pipeline is bound to its target's format, and
|
|
1167
|
+
* material textures are sRGB while a group's data maps are not. */
|
|
1168
|
+
this.mipBlitPipelines = new Map();
|
|
1167
1169
|
this.mipBlitSampler = null;
|
|
1168
1170
|
this._nextDefaultModelId = 0;
|
|
1169
1171
|
// IK and physics enabled at engine level (same for all models)
|
|
@@ -3056,6 +3058,8 @@ export class Engine {
|
|
|
3056
3058
|
e.particles.computeBinds = b.computeBinds;
|
|
3057
3059
|
e.particles.renderBinds = b.renderBinds;
|
|
3058
3060
|
e.particles.mirrorRenderBinds = b.mirrorRenderBinds;
|
|
3061
|
+
if (e.particles.live && b.countBinds)
|
|
3062
|
+
e.particles.live.binds = b.countBinds;
|
|
3059
3063
|
}
|
|
3060
3064
|
if (e.grid)
|
|
3061
3065
|
e.grid.binds = [this.gridBindGroup(e.grid, 0), this.gridBindGroup(e.grid, 1)];
|
|
@@ -3581,6 +3585,7 @@ export class Engine {
|
|
|
3581
3585
|
particles?.buffer.destroy();
|
|
3582
3586
|
particles?.uniform.destroy();
|
|
3583
3587
|
particles?.points?.buffer.destroy();
|
|
3588
|
+
particles?.live?.indirect.destroy();
|
|
3584
3589
|
grid?.textures[0].destroy();
|
|
3585
3590
|
grid?.textures[1].destroy();
|
|
3586
3591
|
grid?.uniform.destroy();
|
|
@@ -3934,6 +3939,7 @@ export class Engine {
|
|
|
3934
3939
|
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
3935
3940
|
gridSize: grid?.size ?? 0,
|
|
3936
3941
|
cover: particleEntryPoints(wgsl).cover,
|
|
3942
|
+
live: particleEntryPoints(wgsl).count,
|
|
3937
3943
|
};
|
|
3938
3944
|
const prepass = src.blend === "cutout" && src.cover;
|
|
3939
3945
|
const points = d.points
|
|
@@ -3997,13 +4003,23 @@ export class Engine {
|
|
|
3997
4003
|
});
|
|
3998
4004
|
const uniformBytes = new ArrayBuffer(32);
|
|
3999
4005
|
const uniformView = { floats: new Float32Array(uniformBytes), uints: new Uint32Array(uniformBytes) };
|
|
4006
|
+
// The live count's arguments — written by rzCount, read by the step's
|
|
4007
|
+
// dispatch and the quads' draws. Zero until the first step, so a pool
|
|
4008
|
+
// drawn before it has stepped draws nothing rather than everything.
|
|
4009
|
+
const indirect = src.live
|
|
4010
|
+
? this.device.createBuffer({
|
|
4011
|
+
label: "particle indirect",
|
|
4012
|
+
size: PARTICLE_INDIRECT_BYTES,
|
|
4013
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT,
|
|
4014
|
+
})
|
|
4015
|
+
: null;
|
|
4000
4016
|
// Visibility is per LAYOUT, not shared: a read_write storage buffer may not be
|
|
4001
4017
|
// visible to the vertex stage at all (WebGPU forbids it — a vertex shader
|
|
4002
4018
|
// that could write memory has no defined ordering against the rasteriser).
|
|
4003
4019
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
4004
4020
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
4005
4021
|
// due to a previous error".
|
|
4006
|
-
const layoutFor = (storage, visibility, shadow) => this.device.createBindGroupLayout({
|
|
4022
|
+
const layoutFor = (storage, visibility, shadow, withIndirect) => this.device.createBindGroupLayout({
|
|
4007
4023
|
entries: [
|
|
4008
4024
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
4009
4025
|
{ binding: 1, visibility, buffer: { type: "uniform" } },
|
|
@@ -4037,13 +4053,21 @@ export class Engine {
|
|
|
4037
4053
|
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
4038
4054
|
]
|
|
4039
4055
|
: []),
|
|
4056
|
+
// The live count's arguments, for the COUNT kernel's layout alone. The
|
|
4057
|
+
// step's layout must not carry them: a bound group's entries count
|
|
4058
|
+
// toward a pass's usage whether the shader reads them or not, and a
|
|
4059
|
+
// buffer may not be writable storage and indirect arguments in one
|
|
4060
|
+
// pass.
|
|
4061
|
+
...(withIndirect
|
|
4062
|
+
? [{ binding: PARTICLE_INDIRECT_BINDING, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }]
|
|
4063
|
+
: []),
|
|
4040
4064
|
],
|
|
4041
4065
|
});
|
|
4042
4066
|
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
4043
4067
|
* alternates which texture is current, and rebuilding a single group every
|
|
4044
4068
|
* frame is waste for a change that only ever toggles between two known
|
|
4045
4069
|
* states. Both entries are the same view when there is no grid. */
|
|
4046
|
-
const bindFor = (layout, camera, shadow) => [0, 1].map((parity) => this.device.createBindGroup({
|
|
4070
|
+
const bindFor = (layout, camera, shadow, withIndirect) => [0, 1].map((parity) => this.device.createBindGroup({
|
|
4047
4071
|
layout,
|
|
4048
4072
|
entries: [
|
|
4049
4073
|
{ binding: 0, resource: { buffer } },
|
|
@@ -4066,10 +4090,12 @@ export class Engine {
|
|
|
4066
4090
|
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
4067
4091
|
]
|
|
4068
4092
|
: []),
|
|
4093
|
+
...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
|
|
4069
4094
|
],
|
|
4070
4095
|
}));
|
|
4071
|
-
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false);
|
|
4072
|
-
const
|
|
4096
|
+
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false, false);
|
|
4097
|
+
const countLayout = indirect ? layoutFor("storage", GPUShaderStage.COMPUTE, false, true) : null;
|
|
4098
|
+
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, true, false);
|
|
4073
4099
|
// Additive keeps the destination and adds to it, and leaves alpha alone, so
|
|
4074
4100
|
// a glow does not claim coverage it never occluded. The MASK sums with it —
|
|
4075
4101
|
// otherwise an additive effect could never reach the bloom gate. Both live
|
|
@@ -4082,11 +4108,23 @@ export class Engine {
|
|
|
4082
4108
|
const cutout = src.blend === "cutout";
|
|
4083
4109
|
this.device.pushErrorScope("validation");
|
|
4084
4110
|
try {
|
|
4111
|
+
const computePipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: [computeLayout] });
|
|
4085
4112
|
const compute = await this.device.createComputePipelineAsync({
|
|
4086
4113
|
label: "particle compute pipeline",
|
|
4087
|
-
layout:
|
|
4114
|
+
layout: computePipelineLayout,
|
|
4088
4115
|
compute: { module: computeModule, entryPoint: "main" },
|
|
4089
4116
|
});
|
|
4117
|
+
const live = indirect && countLayout
|
|
4118
|
+
? {
|
|
4119
|
+
pipeline: await this.device.createComputePipelineAsync({
|
|
4120
|
+
label: "particle count pipeline",
|
|
4121
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [countLayout] }),
|
|
4122
|
+
compute: { module: computeModule, entryPoint: "rzCount" },
|
|
4123
|
+
}),
|
|
4124
|
+
indirect,
|
|
4125
|
+
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
4126
|
+
}
|
|
4127
|
+
: null;
|
|
4090
4128
|
const renderLayoutObj = this.device.createPipelineLayout({ bindGroupLayouts: [renderLayout] });
|
|
4091
4129
|
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
4092
4130
|
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
@@ -4129,6 +4167,7 @@ export class Engine {
|
|
|
4129
4167
|
buffer.destroy();
|
|
4130
4168
|
uniform.destroy();
|
|
4131
4169
|
points?.buffer.destroy();
|
|
4170
|
+
indirect?.destroy();
|
|
4132
4171
|
return { ok: false, diagnostics: [scoped.message] };
|
|
4133
4172
|
}
|
|
4134
4173
|
return {
|
|
@@ -4144,18 +4183,20 @@ export class Engine {
|
|
|
4144
4183
|
counts: uniformView.uints,
|
|
4145
4184
|
compute,
|
|
4146
4185
|
computeLayout,
|
|
4147
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
4186
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false, false),
|
|
4148
4187
|
depth,
|
|
4149
4188
|
render,
|
|
4150
4189
|
renderLayout,
|
|
4151
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
4152
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
4190
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true, false),
|
|
4191
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true, false),
|
|
4153
4192
|
rebind: () => ({
|
|
4154
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
4155
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
4156
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
4193
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false, false),
|
|
4194
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true, false),
|
|
4195
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true, false),
|
|
4196
|
+
countBinds: countLayout ? bindFor(countLayout, this.cameraUniformBuffer, false, true) : null,
|
|
4157
4197
|
}),
|
|
4158
4198
|
points,
|
|
4199
|
+
live,
|
|
4159
4200
|
},
|
|
4160
4201
|
};
|
|
4161
4202
|
}
|
|
@@ -4164,6 +4205,7 @@ export class Engine {
|
|
|
4164
4205
|
buffer.destroy();
|
|
4165
4206
|
uniform.destroy();
|
|
4166
4207
|
points?.buffer.destroy();
|
|
4208
|
+
indirect?.destroy();
|
|
4167
4209
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] };
|
|
4168
4210
|
}
|
|
4169
4211
|
}
|
|
@@ -4349,10 +4391,24 @@ export class Engine {
|
|
|
4349
4391
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer);
|
|
4350
4392
|
if (p.points)
|
|
4351
4393
|
this.writePoints(p.points);
|
|
4394
|
+
const bind = p.computeBinds[e.grid?.parity ?? 0];
|
|
4395
|
+
if (p.live) {
|
|
4396
|
+
// The frame's live count first, in a pass of its own: a buffer may
|
|
4397
|
+
// not be written as storage and read as indirect arguments inside one
|
|
4398
|
+
// pass, and a pass is the synchronization scope.
|
|
4399
|
+
const count = encoder.beginComputePass({ label: "particle count" });
|
|
4400
|
+
count.setPipeline(p.live.pipeline);
|
|
4401
|
+
count.setBindGroup(0, p.live.binds[e.grid?.parity ?? 0]);
|
|
4402
|
+
count.dispatchWorkgroups(1);
|
|
4403
|
+
count.end();
|
|
4404
|
+
}
|
|
4352
4405
|
const cp = encoder.beginComputePass({ label: "particles" });
|
|
4353
4406
|
cp.setPipeline(p.compute);
|
|
4354
|
-
cp.setBindGroup(0,
|
|
4355
|
-
|
|
4407
|
+
cp.setBindGroup(0, bind);
|
|
4408
|
+
if (p.live)
|
|
4409
|
+
cp.dispatchWorkgroupsIndirect(p.live.indirect, 0);
|
|
4410
|
+
else
|
|
4411
|
+
cp.dispatchWorkgroups(Math.ceil(p.count / 64));
|
|
4356
4412
|
cp.end();
|
|
4357
4413
|
}
|
|
4358
4414
|
}
|
|
@@ -4397,14 +4453,21 @@ export class Engine {
|
|
|
4397
4453
|
continue;
|
|
4398
4454
|
const parity = e.grid?.parity ?? 0;
|
|
4399
4455
|
const bind = view === "mirror" ? p.mirrorRenderBinds[parity] : p.renderBinds[parity];
|
|
4456
|
+
// The live count's quads when the effect declared one, else the pool's.
|
|
4457
|
+
const draw = () => {
|
|
4458
|
+
if (p.live)
|
|
4459
|
+
pass.drawIndirect(p.live.indirect, PARTICLE_INDIRECT_DRAW_OFFSET);
|
|
4460
|
+
else
|
|
4461
|
+
pass.draw(6, p.count);
|
|
4462
|
+
};
|
|
4400
4463
|
if (p.depth) {
|
|
4401
4464
|
pass.setPipeline(p.depth);
|
|
4402
4465
|
pass.setBindGroup(0, bind);
|
|
4403
|
-
|
|
4466
|
+
draw();
|
|
4404
4467
|
}
|
|
4405
4468
|
pass.setPipeline(p.render);
|
|
4406
4469
|
pass.setBindGroup(0, bind);
|
|
4407
|
-
|
|
4470
|
+
draw();
|
|
4408
4471
|
}
|
|
4409
4472
|
}
|
|
4410
4473
|
/**
|
|
@@ -4771,6 +4834,7 @@ export class Engine {
|
|
|
4771
4834
|
e.particles?.buffer.destroy();
|
|
4772
4835
|
e.particles?.uniform.destroy();
|
|
4773
4836
|
e.particles?.points?.buffer.destroy();
|
|
4837
|
+
e.particles?.live?.indirect.destroy();
|
|
4774
4838
|
e.particles = null;
|
|
4775
4839
|
}
|
|
4776
4840
|
}
|
|
@@ -5816,6 +5880,11 @@ export class Engine {
|
|
|
5816
5880
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
5817
5881
|
});
|
|
5818
5882
|
this.device.queue.writeTexture({ texture: this.fallbackMaterialTexture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
|
|
5883
|
+
this.noFillBuffer = this.device.createBuffer({
|
|
5884
|
+
label: "model fill (none)",
|
|
5885
|
+
size: 16,
|
|
5886
|
+
usage: GPUBufferUsage.UNIFORM,
|
|
5887
|
+
});
|
|
5819
5888
|
// Generic shared-toon ramp: lit white down to a soft cool shadow tone with
|
|
5820
5889
|
// a tight terminator around the midpoint, approximating MMD's toon ramps.
|
|
5821
5890
|
const TOON_H = 64;
|
|
@@ -5922,6 +5991,8 @@ export class Engine {
|
|
|
5922
5991
|
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
5923
5992
|
buffer: { type: "read-only-storage" },
|
|
5924
5993
|
},
|
|
5994
|
+
// The model's own fill — see setModelFill.
|
|
5995
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
5925
5996
|
],
|
|
5926
5997
|
});
|
|
5927
5998
|
// group 2: per-material (textures + material uniforms) — bound per draw call.
|
|
@@ -8927,6 +8998,54 @@ export class Engine {
|
|
|
8927
8998
|
inst.model.setName(newName);
|
|
8928
8999
|
this.modelInstances.set(newName, inst);
|
|
8929
9000
|
}
|
|
9001
|
+
/**
|
|
9002
|
+
* Light one model with more than the world gives it: a FILL, linear RGB added
|
|
9003
|
+
* to its ambient everywhere on it. Null takes it away again.
|
|
9004
|
+
*
|
|
9005
|
+
* A game lights its stage and its characters apart. Aether Gazer's rooms are
|
|
9006
|
+
* lit by their lamps and a near-black ambient, while its characters take a
|
|
9007
|
+
* flat base light of their own — so a stage whose World is right for the room
|
|
9008
|
+
* leaves a face turned from the lamps in the dark. This is that second light,
|
|
9009
|
+
* for the models the host counts as cast.
|
|
9010
|
+
*/
|
|
9011
|
+
setModelFill(name, fill) {
|
|
9012
|
+
const inst = this.modelInstances.get(name);
|
|
9013
|
+
if (!inst || !this.device)
|
|
9014
|
+
return false;
|
|
9015
|
+
if (!fill) {
|
|
9016
|
+
if (!inst.fillBuffer)
|
|
9017
|
+
return true;
|
|
9018
|
+
const retired = inst.fillBuffer;
|
|
9019
|
+
inst.fillBuffer = null;
|
|
9020
|
+
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, null);
|
|
9021
|
+
this.bundlesDirty = true;
|
|
9022
|
+
// Retired once the GPU is done with it: an encoded frame may still name it.
|
|
9023
|
+
void this.device.queue.onSubmittedWorkDone().then(() => retired.destroy());
|
|
9024
|
+
return true;
|
|
9025
|
+
}
|
|
9026
|
+
if (!inst.fillBuffer) {
|
|
9027
|
+
inst.fillBuffer = this.device.createBuffer({
|
|
9028
|
+
label: `${name}: fill`,
|
|
9029
|
+
size: 16,
|
|
9030
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
9031
|
+
});
|
|
9032
|
+
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, inst.fillBuffer);
|
|
9033
|
+
this.bundlesDirty = true;
|
|
9034
|
+
}
|
|
9035
|
+
this.device.queue.writeBuffer(inst.fillBuffer, 0, new Float32Array([fill.x, fill.y, fill.z, 0]));
|
|
9036
|
+
return true;
|
|
9037
|
+
}
|
|
9038
|
+
/** The per-model group: its skinning matrices and its fill, or the zero stand-in. */
|
|
9039
|
+
perInstanceBindGroup(name, skinMatrixBuffer, fill) {
|
|
9040
|
+
return this.device.createBindGroup({
|
|
9041
|
+
label: `${name}: main per-instance bind group`,
|
|
9042
|
+
layout: this.mainPerInstanceBindGroupLayout,
|
|
9043
|
+
entries: [
|
|
9044
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
9045
|
+
{ binding: 1, resource: { buffer: fill ?? this.noFillBuffer } },
|
|
9046
|
+
],
|
|
9047
|
+
});
|
|
9048
|
+
}
|
|
8930
9049
|
removeModel(name) {
|
|
8931
9050
|
const inst = this.modelInstances.get(name);
|
|
8932
9051
|
if (!inst)
|
|
@@ -8958,6 +9077,7 @@ export class Engine {
|
|
|
8958
9077
|
for (const buf of inst.gpuBuffers) {
|
|
8959
9078
|
buf.destroy();
|
|
8960
9079
|
}
|
|
9080
|
+
inst.fillBuffer?.destroy();
|
|
8961
9081
|
// Per-group StyleUniforms buffers aren't in gpuBuffers (allocated post-load).
|
|
8962
9082
|
for (const install of inst.styleGroups.values())
|
|
8963
9083
|
this.destroyInstall(install);
|
|
@@ -9940,15 +10060,19 @@ export class Engine {
|
|
|
9940
10060
|
const src = wrapped ? entry.source : entry;
|
|
9941
10061
|
const width = Math.max(1, "naturalWidth" in src ? src.naturalWidth : src.width);
|
|
9942
10062
|
const height = Math.max(1, "naturalHeight" in src ? src.naturalHeight : src.height);
|
|
10063
|
+
const mipLevelCount = wrapped && entry.mipmaps ? Math.floor(Math.log2(Math.max(width, height))) + 1 : 1;
|
|
9943
10064
|
const tex = this.device.createTexture({
|
|
9944
10065
|
label: `group map: ${group.id}`,
|
|
9945
10066
|
size: [width, height],
|
|
10067
|
+
mipLevelCount,
|
|
9946
10068
|
// Colour maps decode to linear on sample, the way material textures do;
|
|
9947
10069
|
// data maps must not, or every threshold packed in their channels moves.
|
|
9948
10070
|
format: wrapped && entry.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
9949
10071
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
9950
10072
|
});
|
|
9951
10073
|
this.device.queue.copyExternalImageToTexture({ source: src }, { texture: tex, premultipliedAlpha: wrapped && entry.premultiplied === true }, [width, height]);
|
|
10074
|
+
if (mipLevelCount > 1)
|
|
10075
|
+
this.generateMipmaps(tex, mipLevelCount);
|
|
9952
10076
|
return tex;
|
|
9953
10077
|
});
|
|
9954
10078
|
}
|
|
@@ -11296,11 +11420,7 @@ export class Engine {
|
|
|
11296
11420
|
{ binding: 2, resource: this.materialSampler },
|
|
11297
11421
|
],
|
|
11298
11422
|
}));
|
|
11299
|
-
const mainPerInstanceBindGroup = this.
|
|
11300
|
-
label: `${name}: main per-instance bind group`,
|
|
11301
|
-
layout: this.mainPerInstanceBindGroupLayout,
|
|
11302
|
-
entries: [{ binding: 0, resource: { buffer: skinMatrixBuffer } }],
|
|
11303
|
-
});
|
|
11423
|
+
const mainPerInstanceBindGroup = this.perInstanceBindGroup(name, skinMatrixBuffer, null);
|
|
11304
11424
|
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
11305
11425
|
label: `${name}: pick per-instance bind group`,
|
|
11306
11426
|
layout: this.pickPerInstanceBindGroupLayout,
|
|
@@ -11333,6 +11453,7 @@ export class Engine {
|
|
|
11333
11453
|
shadowDrawCalls: [],
|
|
11334
11454
|
shadowBindGroups,
|
|
11335
11455
|
mainPerInstanceBindGroup,
|
|
11456
|
+
fillBuffer: null,
|
|
11336
11457
|
pickPerInstanceBindGroup,
|
|
11337
11458
|
pickDrawCalls: [],
|
|
11338
11459
|
isStage,
|
|
@@ -12218,31 +12339,35 @@ export class Engine {
|
|
|
12218
12339
|
// Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
|
|
12219
12340
|
// re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
|
|
12220
12341
|
generateMipmaps(texture, mipLevelCount) {
|
|
12221
|
-
if (!this.
|
|
12342
|
+
if (!this.mipBlitSampler) {
|
|
12222
12343
|
this.mipBlitSampler = this.device.createSampler({
|
|
12223
12344
|
magFilter: "linear",
|
|
12224
12345
|
minFilter: "linear",
|
|
12225
12346
|
addressModeU: "clamp-to-edge",
|
|
12226
12347
|
addressModeV: "clamp-to-edge",
|
|
12227
12348
|
});
|
|
12349
|
+
}
|
|
12350
|
+
let pipeline = this.mipBlitPipelines.get(texture.format);
|
|
12351
|
+
if (!pipeline) {
|
|
12228
12352
|
const module = this.device.createShaderModule({
|
|
12229
12353
|
label: "mipmap blit",
|
|
12230
12354
|
code: MIPMAP_BLIT_SHADER_WGSL,
|
|
12231
12355
|
});
|
|
12232
|
-
|
|
12233
|
-
label:
|
|
12356
|
+
pipeline = this.device.createRenderPipeline({
|
|
12357
|
+
label: `mipmap blit pipeline (${texture.format})`,
|
|
12234
12358
|
layout: "auto",
|
|
12235
12359
|
vertex: { module, entryPoint: "vs" },
|
|
12236
|
-
fragment: { module, entryPoint: "fs", targets: [{ format:
|
|
12360
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: texture.format }] },
|
|
12237
12361
|
primitive: { topology: "triangle-list" },
|
|
12238
12362
|
});
|
|
12363
|
+
this.mipBlitPipelines.set(texture.format, pipeline);
|
|
12239
12364
|
}
|
|
12240
12365
|
const encoder = this.device.createCommandEncoder({ label: "mipgen" });
|
|
12241
12366
|
for (let level = 1; level < mipLevelCount; level++) {
|
|
12242
12367
|
const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
|
|
12243
12368
|
const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
|
|
12244
12369
|
const bindGroup = this.device.createBindGroup({
|
|
12245
|
-
layout:
|
|
12370
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
12246
12371
|
entries: [
|
|
12247
12372
|
{ binding: 0, resource: srcView },
|
|
12248
12373
|
{ binding: 1, resource: this.mipBlitSampler },
|
|
@@ -12253,7 +12378,7 @@ export class Engine {
|
|
|
12253
12378
|
{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
12254
12379
|
],
|
|
12255
12380
|
});
|
|
12256
|
-
pass.setPipeline(
|
|
12381
|
+
pass.setPipeline(pipeline);
|
|
12257
12382
|
pass.setBindGroup(0, bindGroup);
|
|
12258
12383
|
pass.draw(3);
|
|
12259
12384
|
pass.end();
|
|
@@ -15078,12 +15203,13 @@ Engine.JIGGLE_DAMPING_SCALE = 0.5;
|
|
|
15078
15203
|
* prefix sum and readback a compacted draw list would need every frame.
|
|
15079
15204
|
*/
|
|
15080
15205
|
/** Ceiling for `#particles`. Past this an author is asking for a stall. */
|
|
15081
|
-
// Raised for the lawn:
|
|
15082
|
-
//
|
|
15206
|
+
// Raised for the lawn: an 80-unit field at 36 blades a unit wants seven
|
|
15207
|
+
// hundred thousand, and a turf one three million. A cutout pool's cost is
|
|
15083
15208
|
// bounded by the pixels it covers, not by the count — the step is one
|
|
15084
|
-
// compute invocation per slot, the pool buffer is 48 bytes each (
|
|
15085
|
-
// the cap
|
|
15086
|
-
|
|
15209
|
+
// compute invocation per slot, the pool buffer is 48 bytes each (96 MiB at
|
|
15210
|
+
// the cap, under the 128 MiB a storage binding may be without asking for
|
|
15211
|
+
// more), and a culled blade is a degenerate quad the rasteriser drops.
|
|
15212
|
+
Engine.MAX_PARTICLES = 2097152;
|
|
15087
15213
|
/**
|
|
15088
15214
|
* The field layer: user background/foreground mounts, ONE TARGET PAIR PER
|
|
15089
15215
|
* RESOLUTION. Index 0 is full, index 1 is half — coarsest last, so the
|
package/dist/graph/slots.js
CHANGED
|
@@ -121,7 +121,7 @@ ${gate}
|
|
|
121
121
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
122
122
|
// The world: flat colour, or the HDRI's irradiance at this normal — which
|
|
123
123
|
// is what makes a loaded sky actually light her instead of only backing her.
|
|
124
|
-
let amb = rzWorldAmbient(n);
|
|
124
|
+
let amb = rzWorldAmbient(n) + modelFill.rgb;
|
|
125
125
|
let shadow = sampleShadow(input.worldPos, n);
|
|
126
126
|
let tex_color = tex_s.rgb;
|
|
127
127
|
|
|
@@ -75,6 +75,13 @@ export type GroupImage = {
|
|
|
75
75
|
source: GroupImageSource;
|
|
76
76
|
srgb?: boolean;
|
|
77
77
|
premultiplied?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Give it a mip chain. For a map TILED across a surface — a relief map, a
|
|
80
|
+
* water's ripples — whose far tiles otherwise shrink to a pixel each and
|
|
81
|
+
* sample as noise that sparkles with every camera move. Off by default: a
|
|
82
|
+
* ramp read at a computed coordinate would blur at its own terminator.
|
|
83
|
+
*/
|
|
84
|
+
mipmaps?: boolean;
|
|
78
85
|
};
|
|
79
86
|
export type GroupDiagnostic = {
|
|
80
87
|
groupId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style-group.d.ts","sourceRoot":"","sources":["../../src/graph/style-group.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,UAAU,GAAG;IACvB,wFAAwF;IACxF,EAAE,EAAE,MAAM,CAAA;IACV;2CACuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,sFAAsF;IACtF,KAAK,EAAE,WAAW,CAAA;IAClB,uEAAuE;IACvE,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAA;IACjD;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAC,CAAA;CAC5E,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,gBAAgB,GAAG,iBAAiB,CAAA;AAEjF;;;;;;;;;;;;GAYG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG;
|
|
1
|
+
{"version":3,"file":"style-group.d.ts","sourceRoot":"","sources":["../../src/graph/style-group.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,UAAU,GAAG;IACvB,wFAAwF;IACxF,EAAE,EAAE,MAAM,CAAA;IACV;2CACuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,sFAAsF;IACtF,KAAK,EAAE,WAAW,CAAA;IAClB,uEAAuE;IACvE,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAA;IACjD;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAC,CAAA;CAC5E,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,gBAAgB,GAAG,iBAAiB,CAAA;AAEjF;;;;;;;;;;;;GAYG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,gBAAgB,CAAA;IACxB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAAA;AAEzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,sCAAsC;IACtC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,kFAAkF;IAClF,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,EAAE,SAAS,EAAE,CAAA;CAAE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,oBAAoB,QAsJhC,CAAC;AA8HF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
|
|
@@ -143,6 +143,9 @@ struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
|
143
143
|
@group(0) @binding(8) var worldEnvTexture: texture_2d<f32>;
|
|
144
144
|
// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
|
|
145
145
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
146
|
+
// Light this model receives beyond the world's — see Engine.setModelFill. Zero
|
|
147
|
+
// for a model nobody gave one, which leaves its ambient exactly the world's.
|
|
148
|
+
@group(1) @binding(1) var<uniform> modelFill: vec4f;
|
|
146
149
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
147
150
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
148
151
|
// Reserved for future sphere/toon graph nodes; graphs that don't read them get the
|
|
@@ -36,6 +36,15 @@ type ParticleSource = {
|
|
|
36
36
|
* where every stacked fragment is otherwise shaded in full; the prepass is
|
|
37
37
|
* the shape of that rejection, done by hand and cheaply. */
|
|
38
38
|
cover: boolean;
|
|
39
|
+
/** The effect defines `particleCount() -> u32`: how many of the pool are
|
|
40
|
+
* live this frame, as a function of its dials. The step is dispatched and
|
|
41
|
+
* the quads drawn for that many slots and no more, through indirect
|
|
42
|
+
* arguments a one-thread kernel writes first. A pool sized for its densest
|
|
43
|
+
* setting then costs its current setting: a lawn's spare 1.3M slots were
|
|
44
|
+
* read, written and issued as degenerate quads every frame, and that
|
|
45
|
+
* traffic alone was a third of the frame. The live slots are always the
|
|
46
|
+
* first N — an author whose live set is not a prefix has no use for this. */
|
|
47
|
+
live: boolean;
|
|
39
48
|
/** The side of this effect's grid, or 0 when it declared none. Particles READ
|
|
40
49
|
* the grid; the kernel that steps it lives in the grid pass. A blade of grass
|
|
41
50
|
* bending where a foot pressed is the whole case: the bend has to persist and
|
|
@@ -46,15 +55,23 @@ type ParticleSource = {
|
|
|
46
55
|
export declare const PARTICLE_LIGHT_BINDING = 10;
|
|
47
56
|
/** The effect's named points (`#points`), in both stages — see points-api.ts. */
|
|
48
57
|
export declare const PARTICLE_POINTS_BINDING = 15;
|
|
58
|
+
/** The indirect arguments a `particleCount` effect writes — compute stage only.
|
|
59
|
+
* Eight u32: the step's dispatch (groups, 1, 1, pad) then the quads' draw
|
|
60
|
+
* (6, count, 0, 0), so the draw reads at byte 16. */
|
|
61
|
+
export declare const PARTICLE_INDIRECT_BINDING = 16;
|
|
62
|
+
export declare const PARTICLE_INDIRECT_BYTES = 32;
|
|
63
|
+
export declare const PARTICLE_INDIRECT_DRAW_OFFSET = 16;
|
|
49
64
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
50
65
|
export declare const PARTICLE_STRIDE = 48;
|
|
51
66
|
/** Does the source define the particle contract? All three are required
|
|
52
|
-
* together; `cover` is optional and only means anything to a cutout
|
|
67
|
+
* together; `cover` is optional and only means anything to a cutout, and
|
|
68
|
+
* `count` is optional and bounds the frame's work — see particleCount. */
|
|
53
69
|
export declare function particleEntryPoints(wgsl: string): {
|
|
54
70
|
init: boolean;
|
|
55
71
|
step: boolean;
|
|
56
72
|
shade: boolean;
|
|
57
73
|
cover: boolean;
|
|
74
|
+
count: boolean;
|
|
58
75
|
};
|
|
59
76
|
/**
|
|
60
77
|
* Spawn, age, recycle.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA+BA,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,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,KAAK,CAAA;AACxC,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA+BA,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;;;;;;;kFAO8E;IAC9E,IAAI,EAAE,OAAO,CAAA;IACb;;;wEAGoE;IACpE,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,KAAK,CAAA;AACxC,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;;sDAEsD;AACtD,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAC3C,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC,eAAO,MAAM,6BAA6B,KAAK,CAAA;AAE/C,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC;;2EAE2E;AAC3E,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,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAQlI;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0ExF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA2LvF"}
|
|
@@ -46,6 +46,12 @@ fn _rzCastLive() -> i32 {
|
|
|
46
46
|
export const PARTICLE_LIGHT_BINDING = 10;
|
|
47
47
|
/** The effect's named points (`#points`), in both stages — see points-api.ts. */
|
|
48
48
|
export const PARTICLE_POINTS_BINDING = 15;
|
|
49
|
+
/** The indirect arguments a `particleCount` effect writes — compute stage only.
|
|
50
|
+
* Eight u32: the step's dispatch (groups, 1, 1, pad) then the quads' draw
|
|
51
|
+
* (6, count, 0, 0), so the draw reads at byte 16. */
|
|
52
|
+
export const PARTICLE_INDIRECT_BINDING = 16;
|
|
53
|
+
export const PARTICLE_INDIRECT_BYTES = 32;
|
|
54
|
+
export const PARTICLE_INDIRECT_DRAW_OFFSET = 16;
|
|
49
55
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
50
56
|
export const PARTICLE_STRIDE = 48;
|
|
51
57
|
const CAMERA_STRUCT = /* wgsl */ `
|
|
@@ -99,13 +105,15 @@ fn rzProject(p: vec3f) -> vec3f {
|
|
|
99
105
|
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
100
106
|
`;
|
|
101
107
|
/** Does the source define the particle contract? All three are required
|
|
102
|
-
* together; `cover` is optional and only means anything to a cutout
|
|
108
|
+
* together; `cover` is optional and only means anything to a cutout, and
|
|
109
|
+
* `count` is optional and bounds the frame's work — see particleCount. */
|
|
103
110
|
export function particleEntryPoints(wgsl) {
|
|
104
111
|
return {
|
|
105
112
|
init: /\bfn\s+particleInit\s*\(/.test(wgsl),
|
|
106
113
|
step: /\bfn\s+particleStep\s*\(/.test(wgsl),
|
|
107
114
|
shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
|
|
108
115
|
cover: /\bfn\s+particleCover\s*\(/.test(wgsl),
|
|
116
|
+
count: /\bfn\s+particleCount\s*\(/.test(wgsl),
|
|
109
117
|
};
|
|
110
118
|
}
|
|
111
119
|
/**
|
|
@@ -143,8 +151,29 @@ ${src.paramsDecl}
|
|
|
143
151
|
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
144
152
|
// The named points, for real: particleInit is where a flame is put on a wick.
|
|
145
153
|
pointsApi(true, 0, PARTICLE_POINTS_BINDING) +
|
|
154
|
+
(src.live
|
|
155
|
+
? `@group(0) @binding(${PARTICLE_INDIRECT_BINDING}) var<storage, read_write> _rzIndirect: array<u32, 8>;\n`
|
|
156
|
+
: "") +
|
|
146
157
|
"\n// ── user effect ──\n" +
|
|
147
158
|
src.wgsl +
|
|
159
|
+
(src.live
|
|
160
|
+
? /* wgsl */ `
|
|
161
|
+
// The frame's live count, from the author's dials, into the arguments the
|
|
162
|
+
// step dispatch and both draws read. One thread; runs before the step.
|
|
163
|
+
@compute @workgroup_size(1)
|
|
164
|
+
fn rzCount() {
|
|
165
|
+
let n = min(particleCount(), pu.count);
|
|
166
|
+
_rzIndirect[0] = (n + 63u) / 64u;
|
|
167
|
+
_rzIndirect[1] = 1u;
|
|
168
|
+
_rzIndirect[2] = 1u;
|
|
169
|
+
_rzIndirect[3] = 0u;
|
|
170
|
+
_rzIndirect[4] = 6u;
|
|
171
|
+
_rzIndirect[5] = n;
|
|
172
|
+
_rzIndirect[6] = 0u;
|
|
173
|
+
_rzIndirect[7] = 0u;
|
|
174
|
+
}
|
|
175
|
+
`
|
|
176
|
+
: "") +
|
|
148
177
|
/* wgsl */ `
|
|
149
178
|
@compute @workgroup_size(64)
|
|
150
179
|
fn main(@builtin(global_invocation_id) gid: vec3u) {
|