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/src/engine.ts
CHANGED
|
@@ -118,6 +118,9 @@ import {
|
|
|
118
118
|
PARTICLE_LIGHT_BINDING,
|
|
119
119
|
PARTICLE_POINTS_BINDING,
|
|
120
120
|
particleEntryPoints,
|
|
121
|
+
PARTICLE_INDIRECT_BINDING,
|
|
122
|
+
PARTICLE_INDIRECT_BYTES,
|
|
123
|
+
PARTICLE_INDIRECT_DRAW_OFFSET,
|
|
121
124
|
PARTICLE_STRIDE,
|
|
122
125
|
} from "./shaders/passes/particles"
|
|
123
126
|
import { MAX_EFFECT_POINTS, POINTS_FLOATS } from "./shaders/points-api"
|
|
@@ -863,6 +866,8 @@ interface ModelInstance {
|
|
|
863
866
|
shadowDrawCalls: DrawCall[]
|
|
864
867
|
shadowBindGroups: GPUBindGroup[]
|
|
865
868
|
mainPerInstanceBindGroup: GPUBindGroup
|
|
869
|
+
/** Its own fill, when setModelFill gave it one. */
|
|
870
|
+
fillBuffer: GPUBuffer | null
|
|
866
871
|
pickPerInstanceBindGroup: GPUBindGroup
|
|
867
872
|
pickDrawCalls: PickDrawCall[]
|
|
868
873
|
/** Environment geometry added via addStage — no physics, no IK, and it
|
|
@@ -1451,11 +1456,16 @@ interface EffectParticles {
|
|
|
1451
1456
|
computeBinds: [GPUBindGroup, GPUBindGroup]
|
|
1452
1457
|
renderBinds: [GPUBindGroup, GPUBindGroup]
|
|
1453
1458
|
mirrorRenderBinds: [GPUBindGroup, GPUBindGroup]
|
|
1459
|
+
countBinds: [GPUBindGroup, GPUBindGroup] | null
|
|
1454
1460
|
}
|
|
1455
1461
|
/** `#points`: the bone-name prefix and the buffer its matches are written
|
|
1456
1462
|
* to each frame. Null when the effect declared none — it then reads the
|
|
1457
1463
|
* engine's empty fallback. */
|
|
1458
1464
|
points: { prefix: string; buffer: GPUBuffer; data: Float32Array } | null
|
|
1465
|
+
/** `particleCount`: the kernel that writes the frame's live count and the
|
|
1466
|
+
* indirect arguments it writes them to. Null when the effect declared none —
|
|
1467
|
+
* the whole pool is then stepped and drawn. */
|
|
1468
|
+
live: { pipeline: GPUComputePipeline; indirect: GPUBuffer; binds: [GPUBindGroup, GPUBindGroup] } | null
|
|
1459
1469
|
}
|
|
1460
1470
|
|
|
1461
1471
|
/**
|
|
@@ -1950,6 +1960,8 @@ export class Engine {
|
|
|
1950
1960
|
} | null = null
|
|
1951
1961
|
private mainPerFrameBindGroupLayout!: GPUBindGroupLayout
|
|
1952
1962
|
private mainPerInstanceBindGroupLayout!: GPUBindGroupLayout
|
|
1963
|
+
/** What a model with no fill of its own binds: zero light. */
|
|
1964
|
+
private noFillBuffer!: GPUBuffer
|
|
1953
1965
|
private mainPerMaterialBindGroupLayout!: GPUBindGroupLayout
|
|
1954
1966
|
private outlinePerFrameBindGroupLayout!: GPUBindGroupLayout
|
|
1955
1967
|
private outlinePerMaterialBindGroupLayout!: GPUBindGroupLayout
|
|
@@ -2085,12 +2097,13 @@ export class Engine {
|
|
|
2085
2097
|
* prefix sum and readback a compacted draw list would need every frame.
|
|
2086
2098
|
*/
|
|
2087
2099
|
/** Ceiling for `#particles`. Past this an author is asking for a stall. */
|
|
2088
|
-
// Raised for the lawn:
|
|
2089
|
-
//
|
|
2100
|
+
// Raised for the lawn: an 80-unit field at 36 blades a unit wants seven
|
|
2101
|
+
// hundred thousand, and a turf one three million. A cutout pool's cost is
|
|
2090
2102
|
// bounded by the pixels it covers, not by the count — the step is one
|
|
2091
|
-
// compute invocation per slot, the pool buffer is 48 bytes each (
|
|
2092
|
-
// the cap
|
|
2093
|
-
|
|
2103
|
+
// compute invocation per slot, the pool buffer is 48 bytes each (96 MiB at
|
|
2104
|
+
// the cap, under the 128 MiB a storage binding may be without asking for
|
|
2105
|
+
// more), and a culled blade is a degenerate quad the rasteriser drops.
|
|
2106
|
+
private static readonly MAX_PARTICLES = 2097152
|
|
2094
2107
|
private particleFrame = 0
|
|
2095
2108
|
/**
|
|
2096
2109
|
* The installed effect's persistent grid, or null when it declared none.
|
|
@@ -2654,7 +2667,9 @@ export class Engine {
|
|
|
2654
2667
|
// GPU texture can't be read back cheaply, and PMX diffuse alpha is usually 1.0
|
|
2655
2668
|
// even for see-through cloth: the translucency lives in the texture).
|
|
2656
2669
|
private textureAlphaCache = new Map<string, { a: Uint8ClampedArray; w: number; h: number } | null>()
|
|
2657
|
-
|
|
2670
|
+
/** One per format: a render pipeline is bound to its target's format, and
|
|
2671
|
+
* material textures are sRGB while a group's data maps are not. */
|
|
2672
|
+
private mipBlitPipelines = new Map<GPUTextureFormat, GPURenderPipeline>()
|
|
2658
2673
|
private mipBlitSampler: GPUSampler | null = null
|
|
2659
2674
|
private _nextDefaultModelId = 0
|
|
2660
2675
|
|
|
@@ -4347,6 +4362,7 @@ export class Engine {
|
|
|
4347
4362
|
e.particles.computeBinds = b.computeBinds
|
|
4348
4363
|
e.particles.renderBinds = b.renderBinds
|
|
4349
4364
|
e.particles.mirrorRenderBinds = b.mirrorRenderBinds
|
|
4365
|
+
if (e.particles.live && b.countBinds) e.particles.live.binds = b.countBinds
|
|
4350
4366
|
}
|
|
4351
4367
|
if (e.grid) e.grid.binds = [this.gridBindGroup(e.grid, 0), this.gridBindGroup(e.grid, 1)]
|
|
4352
4368
|
if (e.lights) e.lights.bind = this.lightEmitBindGroup(e.lights.layout, e.lights.uniform, e.lights.params)
|
|
@@ -4885,6 +4901,7 @@ export class Engine {
|
|
|
4885
4901
|
particles?.buffer.destroy()
|
|
4886
4902
|
particles?.uniform.destroy()
|
|
4887
4903
|
particles?.points?.buffer.destroy()
|
|
4904
|
+
particles?.live?.indirect.destroy()
|
|
4888
4905
|
grid?.textures[0].destroy()
|
|
4889
4906
|
grid?.textures[1].destroy()
|
|
4890
4907
|
grid?.uniform.destroy()
|
|
@@ -5267,6 +5284,7 @@ export class Engine {
|
|
|
5267
5284
|
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
5268
5285
|
gridSize: grid?.size ?? 0,
|
|
5269
5286
|
cover: particleEntryPoints(wgsl).cover,
|
|
5287
|
+
live: particleEntryPoints(wgsl).count,
|
|
5270
5288
|
}
|
|
5271
5289
|
const prepass = src.blend === "cutout" && src.cover
|
|
5272
5290
|
const points = d.points
|
|
@@ -5332,6 +5350,16 @@ export class Engine {
|
|
|
5332
5350
|
})
|
|
5333
5351
|
const uniformBytes = new ArrayBuffer(32)
|
|
5334
5352
|
const uniformView = { floats: new Float32Array(uniformBytes), uints: new Uint32Array(uniformBytes) }
|
|
5353
|
+
// The live count's arguments — written by rzCount, read by the step's
|
|
5354
|
+
// dispatch and the quads' draws. Zero until the first step, so a pool
|
|
5355
|
+
// drawn before it has stepped draws nothing rather than everything.
|
|
5356
|
+
const indirect = src.live
|
|
5357
|
+
? this.device.createBuffer({
|
|
5358
|
+
label: "particle indirect",
|
|
5359
|
+
size: PARTICLE_INDIRECT_BYTES,
|
|
5360
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT,
|
|
5361
|
+
})
|
|
5362
|
+
: null
|
|
5335
5363
|
|
|
5336
5364
|
// Visibility is per LAYOUT, not shared: a read_write storage buffer may not be
|
|
5337
5365
|
// visible to the vertex stage at all (WebGPU forbids it — a vertex shader
|
|
@@ -5339,7 +5367,7 @@ export class Engine {
|
|
|
5339
5367
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
5340
5368
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
5341
5369
|
// due to a previous error".
|
|
5342
|
-
const layoutFor = (storage: GPUBufferBindingType, visibility: number, shadow: boolean) =>
|
|
5370
|
+
const layoutFor = (storage: GPUBufferBindingType, visibility: number, shadow: boolean, withIndirect: boolean) =>
|
|
5343
5371
|
this.device.createBindGroupLayout({
|
|
5344
5372
|
entries: [
|
|
5345
5373
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
@@ -5374,13 +5402,21 @@ export class Engine {
|
|
|
5374
5402
|
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" as const } },
|
|
5375
5403
|
]
|
|
5376
5404
|
: []),
|
|
5405
|
+
// The live count's arguments, for the COUNT kernel's layout alone. The
|
|
5406
|
+
// step's layout must not carry them: a bound group's entries count
|
|
5407
|
+
// toward a pass's usage whether the shader reads them or not, and a
|
|
5408
|
+
// buffer may not be writable storage and indirect arguments in one
|
|
5409
|
+
// pass.
|
|
5410
|
+
...(withIndirect
|
|
5411
|
+
? [{ binding: PARTICLE_INDIRECT_BINDING, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" as const } }]
|
|
5412
|
+
: []),
|
|
5377
5413
|
],
|
|
5378
5414
|
})
|
|
5379
5415
|
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
5380
5416
|
* alternates which texture is current, and rebuilding a single group every
|
|
5381
5417
|
* frame is waste for a change that only ever toggles between two known
|
|
5382
5418
|
* states. Both entries are the same view when there is no grid. */
|
|
5383
|
-
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer, shadow: boolean) =>
|
|
5419
|
+
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer, shadow: boolean, withIndirect: boolean) =>
|
|
5384
5420
|
[0, 1].map((parity) =>
|
|
5385
5421
|
this.device.createBindGroup({
|
|
5386
5422
|
layout,
|
|
@@ -5405,12 +5441,14 @@ export class Engine {
|
|
|
5405
5441
|
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
5406
5442
|
]
|
|
5407
5443
|
: []),
|
|
5444
|
+
...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
|
|
5408
5445
|
],
|
|
5409
5446
|
}),
|
|
5410
5447
|
) as [GPUBindGroup, GPUBindGroup]
|
|
5411
5448
|
|
|
5412
|
-
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false)
|
|
5413
|
-
const
|
|
5449
|
+
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false, false)
|
|
5450
|
+
const countLayout = indirect ? layoutFor("storage", GPUShaderStage.COMPUTE, false, true) : null
|
|
5451
|
+
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, true, false)
|
|
5414
5452
|
|
|
5415
5453
|
// Additive keeps the destination and adds to it, and leaves alpha alone, so
|
|
5416
5454
|
// a glow does not claim coverage it never occluded. The MASK sums with it —
|
|
@@ -5425,11 +5463,24 @@ export class Engine {
|
|
|
5425
5463
|
|
|
5426
5464
|
this.device.pushErrorScope("validation")
|
|
5427
5465
|
try {
|
|
5466
|
+
const computePipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: [computeLayout] })
|
|
5428
5467
|
const compute = await this.device.createComputePipelineAsync({
|
|
5429
5468
|
label: "particle compute pipeline",
|
|
5430
|
-
layout:
|
|
5469
|
+
layout: computePipelineLayout,
|
|
5431
5470
|
compute: { module: computeModule, entryPoint: "main" },
|
|
5432
5471
|
})
|
|
5472
|
+
const live =
|
|
5473
|
+
indirect && countLayout
|
|
5474
|
+
? {
|
|
5475
|
+
pipeline: await this.device.createComputePipelineAsync({
|
|
5476
|
+
label: "particle count pipeline",
|
|
5477
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [countLayout] }),
|
|
5478
|
+
compute: { module: computeModule, entryPoint: "rzCount" },
|
|
5479
|
+
}),
|
|
5480
|
+
indirect,
|
|
5481
|
+
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
5482
|
+
}
|
|
5483
|
+
: null
|
|
5433
5484
|
const renderLayoutObj = this.device.createPipelineLayout({ bindGroupLayouts: [renderLayout] })
|
|
5434
5485
|
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
5435
5486
|
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
@@ -5472,6 +5523,7 @@ export class Engine {
|
|
|
5472
5523
|
buffer.destroy()
|
|
5473
5524
|
uniform.destroy()
|
|
5474
5525
|
points?.buffer.destroy()
|
|
5526
|
+
indirect?.destroy()
|
|
5475
5527
|
return { ok: false, diagnostics: [scoped.message] }
|
|
5476
5528
|
}
|
|
5477
5529
|
return {
|
|
@@ -5487,18 +5539,20 @@ export class Engine {
|
|
|
5487
5539
|
counts: uniformView.uints,
|
|
5488
5540
|
compute,
|
|
5489
5541
|
computeLayout,
|
|
5490
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
5542
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false, false),
|
|
5491
5543
|
depth,
|
|
5492
5544
|
render,
|
|
5493
5545
|
renderLayout,
|
|
5494
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5495
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5546
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true, false),
|
|
5547
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true, false),
|
|
5496
5548
|
rebind: () => ({
|
|
5497
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
5498
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5499
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5549
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false, false),
|
|
5550
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true, false),
|
|
5551
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true, false),
|
|
5552
|
+
countBinds: countLayout ? bindFor(countLayout, this.cameraUniformBuffer, false, true) : null,
|
|
5500
5553
|
}),
|
|
5501
5554
|
points,
|
|
5555
|
+
live,
|
|
5502
5556
|
},
|
|
5503
5557
|
}
|
|
5504
5558
|
} catch (e) {
|
|
@@ -5506,6 +5560,7 @@ export class Engine {
|
|
|
5506
5560
|
buffer.destroy()
|
|
5507
5561
|
uniform.destroy()
|
|
5508
5562
|
points?.buffer.destroy()
|
|
5563
|
+
indirect?.destroy()
|
|
5509
5564
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] }
|
|
5510
5565
|
}
|
|
5511
5566
|
}
|
|
@@ -5698,10 +5753,22 @@ export class Engine {
|
|
|
5698
5753
|
p.counts[3] = this.particleFrame++
|
|
5699
5754
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer as ArrayBuffer)
|
|
5700
5755
|
if (p.points) this.writePoints(p.points)
|
|
5756
|
+
const bind = p.computeBinds[e.grid?.parity ?? 0]
|
|
5757
|
+
if (p.live) {
|
|
5758
|
+
// The frame's live count first, in a pass of its own: a buffer may
|
|
5759
|
+
// not be written as storage and read as indirect arguments inside one
|
|
5760
|
+
// pass, and a pass is the synchronization scope.
|
|
5761
|
+
const count = encoder.beginComputePass({ label: "particle count" })
|
|
5762
|
+
count.setPipeline(p.live.pipeline)
|
|
5763
|
+
count.setBindGroup(0, p.live.binds[e.grid?.parity ?? 0])
|
|
5764
|
+
count.dispatchWorkgroups(1)
|
|
5765
|
+
count.end()
|
|
5766
|
+
}
|
|
5701
5767
|
const cp = encoder.beginComputePass({ label: "particles" })
|
|
5702
5768
|
cp.setPipeline(p.compute)
|
|
5703
|
-
cp.setBindGroup(0,
|
|
5704
|
-
cp.
|
|
5769
|
+
cp.setBindGroup(0, bind)
|
|
5770
|
+
if (p.live) cp.dispatchWorkgroupsIndirect(p.live.indirect, 0)
|
|
5771
|
+
else cp.dispatchWorkgroups(Math.ceil(p.count / 64))
|
|
5705
5772
|
cp.end()
|
|
5706
5773
|
}
|
|
5707
5774
|
}
|
|
@@ -5742,14 +5809,19 @@ export class Engine {
|
|
|
5742
5809
|
if (!p || e.weight === 0) continue
|
|
5743
5810
|
const parity = e.grid?.parity ?? 0
|
|
5744
5811
|
const bind = view === "mirror" ? p.mirrorRenderBinds[parity] : p.renderBinds[parity]
|
|
5812
|
+
// The live count's quads when the effect declared one, else the pool's.
|
|
5813
|
+
const draw = () => {
|
|
5814
|
+
if (p.live) pass.drawIndirect(p.live.indirect, PARTICLE_INDIRECT_DRAW_OFFSET)
|
|
5815
|
+
else pass.draw(6, p.count)
|
|
5816
|
+
}
|
|
5745
5817
|
if (p.depth) {
|
|
5746
5818
|
pass.setPipeline(p.depth)
|
|
5747
5819
|
pass.setBindGroup(0, bind)
|
|
5748
|
-
|
|
5820
|
+
draw()
|
|
5749
5821
|
}
|
|
5750
5822
|
pass.setPipeline(p.render)
|
|
5751
5823
|
pass.setBindGroup(0, bind)
|
|
5752
|
-
|
|
5824
|
+
draw()
|
|
5753
5825
|
}
|
|
5754
5826
|
}
|
|
5755
5827
|
|
|
@@ -6128,6 +6200,7 @@ export class Engine {
|
|
|
6128
6200
|
e.particles?.buffer.destroy()
|
|
6129
6201
|
e.particles?.uniform.destroy()
|
|
6130
6202
|
e.particles?.points?.buffer.destroy()
|
|
6203
|
+
e.particles?.live?.indirect.destroy()
|
|
6131
6204
|
e.particles = null
|
|
6132
6205
|
}
|
|
6133
6206
|
}
|
|
@@ -7233,6 +7306,12 @@ export class Engine {
|
|
|
7233
7306
|
[1, 1],
|
|
7234
7307
|
)
|
|
7235
7308
|
|
|
7309
|
+
this.noFillBuffer = this.device.createBuffer({
|
|
7310
|
+
label: "model fill (none)",
|
|
7311
|
+
size: 16,
|
|
7312
|
+
usage: GPUBufferUsage.UNIFORM,
|
|
7313
|
+
})
|
|
7314
|
+
|
|
7236
7315
|
// Generic shared-toon ramp: lit white down to a soft cool shadow tone with
|
|
7237
7316
|
// a tight terminator around the midpoint, approximating MMD's toon ramps.
|
|
7238
7317
|
const TOON_H = 64
|
|
@@ -7343,6 +7422,8 @@ export class Engine {
|
|
|
7343
7422
|
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
7344
7423
|
buffer: { type: "read-only-storage" },
|
|
7345
7424
|
},
|
|
7425
|
+
// The model's own fill — see setModelFill.
|
|
7426
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
7346
7427
|
],
|
|
7347
7428
|
})
|
|
7348
7429
|
// group 2: per-material (textures + material uniforms) — bound per draw call.
|
|
@@ -10661,6 +10742,54 @@ export class Engine {
|
|
|
10661
10742
|
this.modelInstances.set(newName, inst)
|
|
10662
10743
|
}
|
|
10663
10744
|
|
|
10745
|
+
/**
|
|
10746
|
+
* Light one model with more than the world gives it: a FILL, linear RGB added
|
|
10747
|
+
* to its ambient everywhere on it. Null takes it away again.
|
|
10748
|
+
*
|
|
10749
|
+
* A game lights its stage and its characters apart. Aether Gazer's rooms are
|
|
10750
|
+
* lit by their lamps and a near-black ambient, while its characters take a
|
|
10751
|
+
* flat base light of their own — so a stage whose World is right for the room
|
|
10752
|
+
* leaves a face turned from the lamps in the dark. This is that second light,
|
|
10753
|
+
* for the models the host counts as cast.
|
|
10754
|
+
*/
|
|
10755
|
+
setModelFill(name: string, fill: Vec3 | null): boolean {
|
|
10756
|
+
const inst = this.modelInstances.get(name)
|
|
10757
|
+
if (!inst || !this.device) return false
|
|
10758
|
+
if (!fill) {
|
|
10759
|
+
if (!inst.fillBuffer) return true
|
|
10760
|
+
const retired = inst.fillBuffer
|
|
10761
|
+
inst.fillBuffer = null
|
|
10762
|
+
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, null)
|
|
10763
|
+
this.bundlesDirty = true
|
|
10764
|
+
// Retired once the GPU is done with it: an encoded frame may still name it.
|
|
10765
|
+
void this.device.queue.onSubmittedWorkDone().then(() => retired.destroy())
|
|
10766
|
+
return true
|
|
10767
|
+
}
|
|
10768
|
+
if (!inst.fillBuffer) {
|
|
10769
|
+
inst.fillBuffer = this.device.createBuffer({
|
|
10770
|
+
label: `${name}: fill`,
|
|
10771
|
+
size: 16,
|
|
10772
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
10773
|
+
})
|
|
10774
|
+
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, inst.fillBuffer)
|
|
10775
|
+
this.bundlesDirty = true
|
|
10776
|
+
}
|
|
10777
|
+
this.device.queue.writeBuffer(inst.fillBuffer, 0, new Float32Array([fill.x, fill.y, fill.z, 0]))
|
|
10778
|
+
return true
|
|
10779
|
+
}
|
|
10780
|
+
|
|
10781
|
+
/** The per-model group: its skinning matrices and its fill, or the zero stand-in. */
|
|
10782
|
+
private perInstanceBindGroup(name: string, skinMatrixBuffer: GPUBuffer, fill: GPUBuffer | null): GPUBindGroup {
|
|
10783
|
+
return this.device.createBindGroup({
|
|
10784
|
+
label: `${name}: main per-instance bind group`,
|
|
10785
|
+
layout: this.mainPerInstanceBindGroupLayout,
|
|
10786
|
+
entries: [
|
|
10787
|
+
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
10788
|
+
{ binding: 1, resource: { buffer: fill ?? this.noFillBuffer } },
|
|
10789
|
+
],
|
|
10790
|
+
})
|
|
10791
|
+
}
|
|
10792
|
+
|
|
10664
10793
|
removeModel(name: string): void {
|
|
10665
10794
|
const inst = this.modelInstances.get(name)
|
|
10666
10795
|
if (!inst) return
|
|
@@ -10690,6 +10819,7 @@ export class Engine {
|
|
|
10690
10819
|
for (const buf of inst.gpuBuffers) {
|
|
10691
10820
|
buf.destroy()
|
|
10692
10821
|
}
|
|
10822
|
+
inst.fillBuffer?.destroy()
|
|
10693
10823
|
// Per-group StyleUniforms buffers aren't in gpuBuffers (allocated post-load).
|
|
10694
10824
|
for (const install of inst.styleGroups.values()) this.destroyInstall(install)
|
|
10695
10825
|
this.modelInstances.delete(name)
|
|
@@ -11725,9 +11855,11 @@ export class Engine {
|
|
|
11725
11855
|
const src = wrapped ? entry.source : entry
|
|
11726
11856
|
const width = Math.max(1, "naturalWidth" in src ? src.naturalWidth : src.width)
|
|
11727
11857
|
const height = Math.max(1, "naturalHeight" in src ? src.naturalHeight : src.height)
|
|
11858
|
+
const mipLevelCount = wrapped && entry.mipmaps ? Math.floor(Math.log2(Math.max(width, height))) + 1 : 1
|
|
11728
11859
|
const tex = this.device.createTexture({
|
|
11729
11860
|
label: `group map: ${group.id}`,
|
|
11730
11861
|
size: [width, height],
|
|
11862
|
+
mipLevelCount,
|
|
11731
11863
|
// Colour maps decode to linear on sample, the way material textures do;
|
|
11732
11864
|
// data maps must not, or every threshold packed in their channels moves.
|
|
11733
11865
|
format: wrapped && entry.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
@@ -11738,6 +11870,7 @@ export class Engine {
|
|
|
11738
11870
|
{ texture: tex, premultipliedAlpha: wrapped && entry.premultiplied === true },
|
|
11739
11871
|
[width, height],
|
|
11740
11872
|
)
|
|
11873
|
+
if (mipLevelCount > 1) this.generateMipmaps(tex, mipLevelCount)
|
|
11741
11874
|
return tex
|
|
11742
11875
|
})
|
|
11743
11876
|
}
|
|
@@ -13192,11 +13325,7 @@ export class Engine {
|
|
|
13192
13325
|
}),
|
|
13193
13326
|
)
|
|
13194
13327
|
|
|
13195
|
-
const mainPerInstanceBindGroup = this.
|
|
13196
|
-
label: `${name}: main per-instance bind group`,
|
|
13197
|
-
layout: this.mainPerInstanceBindGroupLayout,
|
|
13198
|
-
entries: [{ binding: 0, resource: { buffer: skinMatrixBuffer } }],
|
|
13199
|
-
})
|
|
13328
|
+
const mainPerInstanceBindGroup = this.perInstanceBindGroup(name, skinMatrixBuffer, null)
|
|
13200
13329
|
|
|
13201
13330
|
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
13202
13331
|
label: `${name}: pick per-instance bind group`,
|
|
@@ -13235,6 +13364,7 @@ export class Engine {
|
|
|
13235
13364
|
shadowDrawCalls: [],
|
|
13236
13365
|
shadowBindGroups,
|
|
13237
13366
|
mainPerInstanceBindGroup,
|
|
13367
|
+
fillBuffer: null,
|
|
13238
13368
|
pickPerInstanceBindGroup,
|
|
13239
13369
|
pickDrawCalls: [],
|
|
13240
13370
|
isStage,
|
|
@@ -14241,24 +14371,28 @@ export class Engine {
|
|
|
14241
14371
|
// Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
|
|
14242
14372
|
// re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
|
|
14243
14373
|
private generateMipmaps(texture: GPUTexture, mipLevelCount: number) {
|
|
14244
|
-
if (!this.
|
|
14374
|
+
if (!this.mipBlitSampler) {
|
|
14245
14375
|
this.mipBlitSampler = this.device.createSampler({
|
|
14246
14376
|
magFilter: "linear",
|
|
14247
14377
|
minFilter: "linear",
|
|
14248
14378
|
addressModeU: "clamp-to-edge",
|
|
14249
14379
|
addressModeV: "clamp-to-edge",
|
|
14250
14380
|
})
|
|
14381
|
+
}
|
|
14382
|
+
let pipeline = this.mipBlitPipelines.get(texture.format)
|
|
14383
|
+
if (!pipeline) {
|
|
14251
14384
|
const module = this.device.createShaderModule({
|
|
14252
14385
|
label: "mipmap blit",
|
|
14253
14386
|
code: MIPMAP_BLIT_SHADER_WGSL,
|
|
14254
14387
|
})
|
|
14255
|
-
|
|
14256
|
-
label:
|
|
14388
|
+
pipeline = this.device.createRenderPipeline({
|
|
14389
|
+
label: `mipmap blit pipeline (${texture.format})`,
|
|
14257
14390
|
layout: "auto",
|
|
14258
14391
|
vertex: { module, entryPoint: "vs" },
|
|
14259
|
-
fragment: { module, entryPoint: "fs", targets: [{ format:
|
|
14392
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: texture.format }] },
|
|
14260
14393
|
primitive: { topology: "triangle-list" },
|
|
14261
14394
|
})
|
|
14395
|
+
this.mipBlitPipelines.set(texture.format, pipeline)
|
|
14262
14396
|
}
|
|
14263
14397
|
|
|
14264
14398
|
const encoder = this.device.createCommandEncoder({ label: "mipgen" })
|
|
@@ -14266,7 +14400,7 @@ export class Engine {
|
|
|
14266
14400
|
const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 })
|
|
14267
14401
|
const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1 })
|
|
14268
14402
|
const bindGroup = this.device.createBindGroup({
|
|
14269
|
-
layout:
|
|
14403
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
14270
14404
|
entries: [
|
|
14271
14405
|
{ binding: 0, resource: srcView },
|
|
14272
14406
|
{ binding: 1, resource: this.mipBlitSampler },
|
|
@@ -14277,7 +14411,7 @@ export class Engine {
|
|
|
14277
14411
|
{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
14278
14412
|
],
|
|
14279
14413
|
})
|
|
14280
|
-
pass.setPipeline(
|
|
14414
|
+
pass.setPipeline(pipeline)
|
|
14281
14415
|
pass.setBindGroup(0, bindGroup)
|
|
14282
14416
|
pass.draw(3)
|
|
14283
14417
|
pass.end()
|
package/src/graph/slots.ts
CHANGED
|
@@ -133,7 +133,7 @@ ${gate}
|
|
|
133
133
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
134
134
|
// The world: flat colour, or the HDRI's irradiance at this normal — which
|
|
135
135
|
// is what makes a loaded sky actually light her instead of only backing her.
|
|
136
|
-
let amb = rzWorldAmbient(n);
|
|
136
|
+
let amb = rzWorldAmbient(n) + modelFill.rgb;
|
|
137
137
|
let shadow = sampleShadow(input.worldPos, n);
|
|
138
138
|
let tex_color = tex_s.rgb;
|
|
139
139
|
|
package/src/graph/style-group.ts
CHANGED
|
@@ -80,7 +80,18 @@ export type GroupImageSource = ImageBitmap | HTMLImageElement | HTMLCanvasElemen
|
|
|
80
80
|
* converts to the destination's tagged alpha mode, so a premultiplied ImageBitmap
|
|
81
81
|
* copied into an untagged texture is un-premultiplied again on the way in.
|
|
82
82
|
*/
|
|
83
|
-
export type GroupImage = {
|
|
83
|
+
export type GroupImage = {
|
|
84
|
+
source: GroupImageSource
|
|
85
|
+
srgb?: boolean
|
|
86
|
+
premultiplied?: boolean
|
|
87
|
+
/**
|
|
88
|
+
* Give it a mip chain. For a map TILED across a surface — a relief map, a
|
|
89
|
+
* water's ripples — whose far tiles otherwise shrink to a pixel each and
|
|
90
|
+
* sample as noise that sparkles with every camera move. Off by default: a
|
|
91
|
+
* ramp read at a computed coordinate would blur at its own terminator.
|
|
92
|
+
*/
|
|
93
|
+
mipmaps?: boolean
|
|
94
|
+
}
|
|
84
95
|
|
|
85
96
|
export type GroupDiagnostic = { groupId: string; diagnostics: Diagnostic[]; ok: boolean }
|
|
86
97
|
|
|
@@ -146,6 +146,9 @@ struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
|
146
146
|
@group(0) @binding(8) var worldEnvTexture: texture_2d<f32>;
|
|
147
147
|
// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
|
|
148
148
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
149
|
+
// Light this model receives beyond the world's — see Engine.setModelFill. Zero
|
|
150
|
+
// for a model nobody gave one, which leaves its ambient exactly the world's.
|
|
151
|
+
@group(1) @binding(1) var<uniform> modelFill: vec4f;
|
|
149
152
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
150
153
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
151
154
|
// Reserved for future sphere/toon graph nodes; graphs that don't read them get the
|
|
@@ -101,6 +101,15 @@ type ParticleSource = {
|
|
|
101
101
|
* where every stacked fragment is otherwise shaded in full; the prepass is
|
|
102
102
|
* the shape of that rejection, done by hand and cheaply. */
|
|
103
103
|
cover: boolean
|
|
104
|
+
/** The effect defines `particleCount() -> u32`: how many of the pool are
|
|
105
|
+
* live this frame, as a function of its dials. The step is dispatched and
|
|
106
|
+
* the quads drawn for that many slots and no more, through indirect
|
|
107
|
+
* arguments a one-thread kernel writes first. A pool sized for its densest
|
|
108
|
+
* setting then costs its current setting: a lawn's spare 1.3M slots were
|
|
109
|
+
* read, written and issued as degenerate quads every frame, and that
|
|
110
|
+
* traffic alone was a third of the frame. The live slots are always the
|
|
111
|
+
* first N — an author whose live set is not a prefix has no use for this. */
|
|
112
|
+
live: boolean
|
|
104
113
|
/** The side of this effect's grid, or 0 when it declared none. Particles READ
|
|
105
114
|
* the grid; the kernel that steps it lives in the grid pass. A blade of grass
|
|
106
115
|
* bending where a foot pressed is the whole case: the bend has to persist and
|
|
@@ -112,6 +121,12 @@ type ParticleSource = {
|
|
|
112
121
|
export const PARTICLE_LIGHT_BINDING = 10
|
|
113
122
|
/** The effect's named points (`#points`), in both stages — see points-api.ts. */
|
|
114
123
|
export const PARTICLE_POINTS_BINDING = 15
|
|
124
|
+
/** The indirect arguments a `particleCount` effect writes — compute stage only.
|
|
125
|
+
* Eight u32: the step's dispatch (groups, 1, 1, pad) then the quads' draw
|
|
126
|
+
* (6, count, 0, 0), so the draw reads at byte 16. */
|
|
127
|
+
export const PARTICLE_INDIRECT_BINDING = 16
|
|
128
|
+
export const PARTICLE_INDIRECT_BYTES = 32
|
|
129
|
+
export const PARTICLE_INDIRECT_DRAW_OFFSET = 16
|
|
115
130
|
|
|
116
131
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
117
132
|
export const PARTICLE_STRIDE = 48
|
|
@@ -171,13 +186,15 @@ fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
|
171
186
|
`
|
|
172
187
|
|
|
173
188
|
/** Does the source define the particle contract? All three are required
|
|
174
|
-
* together; `cover` is optional and only means anything to a cutout
|
|
175
|
-
|
|
189
|
+
* together; `cover` is optional and only means anything to a cutout, and
|
|
190
|
+
* `count` is optional and bounds the frame's work — see particleCount. */
|
|
191
|
+
export function particleEntryPoints(wgsl: string): { init: boolean; step: boolean; shade: boolean; cover: boolean; count: boolean } {
|
|
176
192
|
return {
|
|
177
193
|
init: /\bfn\s+particleInit\s*\(/.test(wgsl),
|
|
178
194
|
step: /\bfn\s+particleStep\s*\(/.test(wgsl),
|
|
179
195
|
shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
|
|
180
196
|
cover: /\bfn\s+particleCover\s*\(/.test(wgsl),
|
|
197
|
+
count: /\bfn\s+particleCount\s*\(/.test(wgsl),
|
|
181
198
|
}
|
|
182
199
|
}
|
|
183
200
|
|
|
@@ -217,8 +234,29 @@ ${src.paramsDecl}
|
|
|
217
234
|
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
218
235
|
// The named points, for real: particleInit is where a flame is put on a wick.
|
|
219
236
|
pointsApi(true, 0, PARTICLE_POINTS_BINDING) +
|
|
237
|
+
(src.live
|
|
238
|
+
? `@group(0) @binding(${PARTICLE_INDIRECT_BINDING}) var<storage, read_write> _rzIndirect: array<u32, 8>;\n`
|
|
239
|
+
: "") +
|
|
220
240
|
"\n// ── user effect ──\n" +
|
|
221
241
|
src.wgsl +
|
|
242
|
+
(src.live
|
|
243
|
+
? /* wgsl */ `
|
|
244
|
+
// The frame's live count, from the author's dials, into the arguments the
|
|
245
|
+
// step dispatch and both draws read. One thread; runs before the step.
|
|
246
|
+
@compute @workgroup_size(1)
|
|
247
|
+
fn rzCount() {
|
|
248
|
+
let n = min(particleCount(), pu.count);
|
|
249
|
+
_rzIndirect[0] = (n + 63u) / 64u;
|
|
250
|
+
_rzIndirect[1] = 1u;
|
|
251
|
+
_rzIndirect[2] = 1u;
|
|
252
|
+
_rzIndirect[3] = 0u;
|
|
253
|
+
_rzIndirect[4] = 6u;
|
|
254
|
+
_rzIndirect[5] = n;
|
|
255
|
+
_rzIndirect[6] = 0u;
|
|
256
|
+
_rzIndirect[7] = 0u;
|
|
257
|
+
}
|
|
258
|
+
`
|
|
259
|
+
: "") +
|
|
222
260
|
/* wgsl */ `
|
|
223
261
|
@compute @workgroup_size(64)
|
|
224
262
|
fn main(@builtin(global_invocation_id) gid: vec3u) {
|