reze-engine 0.58.1 → 0.58.3
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/effect-points.d.ts +30 -0
- package/dist/effect-points.d.ts.map +1 -0
- package/dist/effect-points.js +49 -0
- package/dist/engine.d.ts +25 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +199 -35
- package/dist/graph/style-group.d.ts +7 -0
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/light-grid.d.ts +25 -0
- package/dist/light-grid.d.ts.map +1 -0
- package/dist/light-grid.js +91 -0
- package/dist/model.d.ts +9 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +11 -6
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-layout.d.ts +5 -3
- package/dist/shaders/cast-layout.d.ts.map +1 -1
- package/dist/shaders/cast-layout.js +5 -3
- package/dist/shaders/directives.d.ts +3 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +5 -0
- package/dist/shaders/lights.d.ts +14 -9
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +118 -65
- package/dist/shaders/passes/composite.d.ts +1 -1
- 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 -1
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +36 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +2 -1
- package/dist/shaders/points-api.d.ts +11 -0
- package/dist/shaders/points-api.d.ts.map +1 -0
- package/dist/shaders/points-api.js +58 -0
- package/package.json +1 -1
- package/src/effect-points.ts +67 -0
- package/src/engine.ts +215 -35
- package/src/graph/style-group.ts +12 -1
- package/src/light-grid.ts +139 -0
- package/src/model.ts +12 -0
- package/src/pmx-loader.ts +13 -6
- package/src/shaders/cast-layout.ts +5 -3
- package/src/shaders/directives.ts +8 -0
- package/src/shaders/lights.ts +118 -65
- package/src/shaders/passes/composite.ts +2 -0
- package/src/shaders/passes/grid.ts +2 -0
- package/src/shaders/passes/particles.ts +46 -2
- package/src/shaders/passes/trails.ts +2 -1
- package/src/shaders/points-api.ts +66 -0
package/src/engine.ts
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
type SceneFormats,
|
|
60
60
|
} from "./shaders/passes/scene-contract"
|
|
61
61
|
import {
|
|
62
|
+
LIGHT_GRID_BASE,
|
|
62
63
|
LIGHT_HEADER,
|
|
63
64
|
LIGHT_STRIDE,
|
|
64
65
|
LIGHTS_FLOATS,
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
buildLightEmitShader,
|
|
67
68
|
hasLightEmit,
|
|
68
69
|
} from "./shaders/lights"
|
|
70
|
+
import { buildLightGrid } from "./light-grid"
|
|
69
71
|
import { groundShaderWgsl, GROUND_NOISE_BAKE_WGSL, GROUND_NOISE_SIZE } from "./shaders/passes/ground"
|
|
70
72
|
import { outlineShaderWgsl, RZ_OUTLINE_DISSOLVE_OFFSET } from "./shaders/passes/outline"
|
|
71
73
|
import { transparentDepthPrepassWgsl } from "./shaders/passes/depth-prepass"
|
|
@@ -114,9 +116,15 @@ import {
|
|
|
114
116
|
buildParticleComputeShader,
|
|
115
117
|
buildParticleRenderShader,
|
|
116
118
|
PARTICLE_LIGHT_BINDING,
|
|
119
|
+
PARTICLE_POINTS_BINDING,
|
|
117
120
|
particleEntryPoints,
|
|
121
|
+
PARTICLE_INDIRECT_BINDING,
|
|
122
|
+
PARTICLE_INDIRECT_BYTES,
|
|
123
|
+
PARTICLE_INDIRECT_DRAW_OFFSET,
|
|
118
124
|
PARTICLE_STRIDE,
|
|
119
125
|
} from "./shaders/passes/particles"
|
|
126
|
+
import { MAX_EFFECT_POINTS, POINTS_FLOATS } from "./shaders/points-api"
|
|
127
|
+
import { bonesWithPrefix, pointsData, writeBonePoint } from "./effect-points"
|
|
120
128
|
import {
|
|
121
129
|
SIM_FORMAT,
|
|
122
130
|
GRID_MAX,
|
|
@@ -1446,7 +1454,16 @@ interface EffectParticles {
|
|
|
1446
1454
|
computeBinds: [GPUBindGroup, GPUBindGroup]
|
|
1447
1455
|
renderBinds: [GPUBindGroup, GPUBindGroup]
|
|
1448
1456
|
mirrorRenderBinds: [GPUBindGroup, GPUBindGroup]
|
|
1449
|
-
|
|
1457
|
+
countBinds: [GPUBindGroup, GPUBindGroup] | null
|
|
1458
|
+
}
|
|
1459
|
+
/** `#points`: the bone-name prefix and the buffer its matches are written
|
|
1460
|
+
* to each frame. Null when the effect declared none — it then reads the
|
|
1461
|
+
* engine's empty fallback. */
|
|
1462
|
+
points: { prefix: string; buffer: GPUBuffer; data: Float32Array } | null
|
|
1463
|
+
/** `particleCount`: the kernel that writes the frame's live count and the
|
|
1464
|
+
* indirect arguments it writes them to. Null when the effect declared none —
|
|
1465
|
+
* the whole pool is then stepped and drawn. */
|
|
1466
|
+
live: { pipeline: GPUComputePipeline; indirect: GPUBuffer; binds: [GPUBindGroup, GPUBindGroup] } | null
|
|
1450
1467
|
}
|
|
1451
1468
|
|
|
1452
1469
|
/**
|
|
@@ -2076,12 +2093,13 @@ export class Engine {
|
|
|
2076
2093
|
* prefix sum and readback a compacted draw list would need every frame.
|
|
2077
2094
|
*/
|
|
2078
2095
|
/** Ceiling for `#particles`. Past this an author is asking for a stall. */
|
|
2079
|
-
// Raised for the lawn:
|
|
2080
|
-
//
|
|
2096
|
+
// Raised for the lawn: an 80-unit field at 36 blades a unit wants seven
|
|
2097
|
+
// hundred thousand, and a turf one three million. A cutout pool's cost is
|
|
2081
2098
|
// bounded by the pixels it covers, not by the count — the step is one
|
|
2082
|
-
// compute invocation per slot, the pool buffer is 48 bytes each (
|
|
2083
|
-
// the cap
|
|
2084
|
-
|
|
2099
|
+
// compute invocation per slot, the pool buffer is 48 bytes each (96 MiB at
|
|
2100
|
+
// the cap, under the 128 MiB a storage binding may be without asking for
|
|
2101
|
+
// more), and a culled blade is a degenerate quad the rasteriser drops.
|
|
2102
|
+
private static readonly MAX_PARTICLES = 2097152
|
|
2085
2103
|
private particleFrame = 0
|
|
2086
2104
|
/**
|
|
2087
2105
|
* The installed effect's persistent grid, or null when it declared none.
|
|
@@ -2498,9 +2516,20 @@ export class Engine {
|
|
|
2498
2516
|
* Allocated once at full size and zero-filled, so "no lights" is a count of
|
|
2499
2517
|
* zero rather than an absent binding. */
|
|
2500
2518
|
private lightsBuffer!: GPUBuffer
|
|
2501
|
-
|
|
2502
|
-
|
|
2519
|
+
/** What a particle effect with no `#points` reads at its points binding: a
|
|
2520
|
+
* count of zero. One buffer for all of them — nothing ever writes it. */
|
|
2521
|
+
private pointsFallback!: GPUBuffer
|
|
2522
|
+
/** Per model, per prefix, the bones `#points` matched. A rig's bones never
|
|
2523
|
+
* change after load, so the names are walked once rather than every frame. */
|
|
2524
|
+
private pointBones = new WeakMap<Model, Map<string, number[]>>()
|
|
2525
|
+
/** The CPU copy, from construction rather than init: a host sets a scene's
|
|
2526
|
+
* lamps as soon as its state exists, which can be before the device. */
|
|
2527
|
+
private lightsData = new Float32Array(LIGHTS_FLOATS)
|
|
2528
|
+
/** The same bytes as words — the grid's lamp bits are u32, not floats. */
|
|
2529
|
+
private lightsWords = new Uint32Array(this.lightsData.buffer)
|
|
2530
|
+
/** Just the header: the counts and the grid's placement. */
|
|
2503
2531
|
private lightHeader = new Float32Array(LIGHT_HEADER)
|
|
2532
|
+
private lightHeaderWords = new Uint32Array(this.lightHeader.buffer)
|
|
2504
2533
|
/** How many of the slots belong to the DOCUMENT. Effects get what follows. */
|
|
2505
2534
|
private docLightCount = 0
|
|
2506
2535
|
private castData!: Float32Array<ArrayBuffer>
|
|
@@ -2634,7 +2663,9 @@ export class Engine {
|
|
|
2634
2663
|
// GPU texture can't be read back cheaply, and PMX diffuse alpha is usually 1.0
|
|
2635
2664
|
// even for see-through cloth: the translucency lives in the texture).
|
|
2636
2665
|
private textureAlphaCache = new Map<string, { a: Uint8ClampedArray; w: number; h: number } | null>()
|
|
2637
|
-
|
|
2666
|
+
/** One per format: a render pipeline is bound to its target's format, and
|
|
2667
|
+
* material textures are sRGB while a group's data maps are not. */
|
|
2668
|
+
private mipBlitPipelines = new Map<GPUTextureFormat, GPURenderPipeline>()
|
|
2638
2669
|
private mipBlitSampler: GPUSampler | null = null
|
|
2639
2670
|
private _nextDefaultModelId = 0
|
|
2640
2671
|
|
|
@@ -4327,6 +4358,7 @@ export class Engine {
|
|
|
4327
4358
|
e.particles.computeBinds = b.computeBinds
|
|
4328
4359
|
e.particles.renderBinds = b.renderBinds
|
|
4329
4360
|
e.particles.mirrorRenderBinds = b.mirrorRenderBinds
|
|
4361
|
+
if (e.particles.live && b.countBinds) e.particles.live.binds = b.countBinds
|
|
4330
4362
|
}
|
|
4331
4363
|
if (e.grid) e.grid.binds = [this.gridBindGroup(e.grid, 0), this.gridBindGroup(e.grid, 1)]
|
|
4332
4364
|
if (e.lights) e.lights.bind = this.lightEmitBindGroup(e.lights.layout, e.lights.uniform, e.lights.params)
|
|
@@ -4864,6 +4896,8 @@ export class Engine {
|
|
|
4864
4896
|
paramsBuffer?.destroy()
|
|
4865
4897
|
particles?.buffer.destroy()
|
|
4866
4898
|
particles?.uniform.destroy()
|
|
4899
|
+
particles?.points?.buffer.destroy()
|
|
4900
|
+
particles?.live?.indirect.destroy()
|
|
4867
4901
|
grid?.textures[0].destroy()
|
|
4868
4902
|
grid?.textures[1].destroy()
|
|
4869
4903
|
grid?.uniform.destroy()
|
|
@@ -5246,8 +5280,20 @@ export class Engine {
|
|
|
5246
5280
|
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
5247
5281
|
gridSize: grid?.size ?? 0,
|
|
5248
5282
|
cover: particleEntryPoints(wgsl).cover,
|
|
5283
|
+
live: particleEntryPoints(wgsl).count,
|
|
5249
5284
|
}
|
|
5250
5285
|
const prepass = src.blend === "cutout" && src.cover
|
|
5286
|
+
const points = d.points
|
|
5287
|
+
? {
|
|
5288
|
+
prefix: d.points,
|
|
5289
|
+
buffer: this.device.createBuffer({
|
|
5290
|
+
label: `points "${d.points}"`,
|
|
5291
|
+
size: POINTS_FLOATS * 4,
|
|
5292
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
5293
|
+
}),
|
|
5294
|
+
data: pointsData(),
|
|
5295
|
+
}
|
|
5296
|
+
: null
|
|
5251
5297
|
// Sparks want to spawn where a trail is, so the particle stages see the same
|
|
5252
5298
|
// cast buffer the trail draw reads.
|
|
5253
5299
|
const cast = {
|
|
@@ -5274,9 +5320,15 @@ export class Engine {
|
|
|
5274
5320
|
}
|
|
5275
5321
|
|
|
5276
5322
|
const computeModule = await compile(buildParticleComputeShader(src, cast), "particle compute")
|
|
5277
|
-
if (Array.isArray(computeModule))
|
|
5323
|
+
if (Array.isArray(computeModule)) {
|
|
5324
|
+
points?.buffer.destroy()
|
|
5325
|
+
return { ok: false, diagnostics: computeModule }
|
|
5326
|
+
}
|
|
5278
5327
|
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render")
|
|
5279
|
-
if (Array.isArray(renderModule))
|
|
5328
|
+
if (Array.isArray(renderModule)) {
|
|
5329
|
+
points?.buffer.destroy()
|
|
5330
|
+
return { ok: false, diagnostics: renderModule }
|
|
5331
|
+
}
|
|
5280
5332
|
|
|
5281
5333
|
// COPY_DST so a scheduled effect can empty its pool when its window starts.
|
|
5282
5334
|
const buffer = this.device.createBuffer({
|
|
@@ -5294,6 +5346,16 @@ export class Engine {
|
|
|
5294
5346
|
})
|
|
5295
5347
|
const uniformBytes = new ArrayBuffer(32)
|
|
5296
5348
|
const uniformView = { floats: new Float32Array(uniformBytes), uints: new Uint32Array(uniformBytes) }
|
|
5349
|
+
// The live count's arguments — written by rzCount, read by the step's
|
|
5350
|
+
// dispatch and the quads' draws. Zero until the first step, so a pool
|
|
5351
|
+
// drawn before it has stepped draws nothing rather than everything.
|
|
5352
|
+
const indirect = src.live
|
|
5353
|
+
? this.device.createBuffer({
|
|
5354
|
+
label: "particle indirect",
|
|
5355
|
+
size: PARTICLE_INDIRECT_BYTES,
|
|
5356
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT,
|
|
5357
|
+
})
|
|
5358
|
+
: null
|
|
5297
5359
|
|
|
5298
5360
|
// Visibility is per LAYOUT, not shared: a read_write storage buffer may not be
|
|
5299
5361
|
// visible to the vertex stage at all (WebGPU forbids it — a vertex shader
|
|
@@ -5301,7 +5363,7 @@ export class Engine {
|
|
|
5301
5363
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
5302
5364
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
5303
5365
|
// due to a previous error".
|
|
5304
|
-
const layoutFor = (storage: GPUBufferBindingType, visibility: number, shadow: boolean) =>
|
|
5366
|
+
const layoutFor = (storage: GPUBufferBindingType, visibility: number, shadow: boolean, withIndirect: boolean) =>
|
|
5305
5367
|
this.device.createBindGroupLayout({
|
|
5306
5368
|
entries: [
|
|
5307
5369
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
@@ -5321,6 +5383,9 @@ export class Engine {
|
|
|
5321
5383
|
// author calls it or not. With no grid it is the 1x1 of zeroes.
|
|
5322
5384
|
{ binding: 8, visibility, texture: { sampleType: "float" as const } },
|
|
5323
5385
|
{ binding: 9, visibility, sampler: { type: "filtering" as const } },
|
|
5386
|
+
// The named points, ALWAYS, for the grid's reason: the accessor is
|
|
5387
|
+
// compiled in whether or not the effect declared #points.
|
|
5388
|
+
{ binding: PARTICLE_POINTS_BINDING, visibility, buffer: { type: "read-only-storage" as const } },
|
|
5324
5389
|
// The scene's light, for rzShadow and rzWorldAmbient — the shading
|
|
5325
5390
|
// stage only; the compute module compiles the stubs. See
|
|
5326
5391
|
// scene-light-api.ts.
|
|
@@ -5333,13 +5398,21 @@ export class Engine {
|
|
|
5333
5398
|
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" as const } },
|
|
5334
5399
|
]
|
|
5335
5400
|
: []),
|
|
5401
|
+
// The live count's arguments, for the COUNT kernel's layout alone. The
|
|
5402
|
+
// step's layout must not carry them: a bound group's entries count
|
|
5403
|
+
// toward a pass's usage whether the shader reads them or not, and a
|
|
5404
|
+
// buffer may not be writable storage and indirect arguments in one
|
|
5405
|
+
// pass.
|
|
5406
|
+
...(withIndirect
|
|
5407
|
+
? [{ binding: PARTICLE_INDIRECT_BINDING, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" as const } }]
|
|
5408
|
+
: []),
|
|
5336
5409
|
],
|
|
5337
5410
|
})
|
|
5338
5411
|
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
5339
5412
|
* alternates which texture is current, and rebuilding a single group every
|
|
5340
5413
|
* frame is waste for a change that only ever toggles between two known
|
|
5341
5414
|
* states. Both entries are the same view when there is no grid. */
|
|
5342
|
-
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer, shadow: boolean) =>
|
|
5415
|
+
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer, shadow: boolean, withIndirect: boolean) =>
|
|
5343
5416
|
[0, 1].map((parity) =>
|
|
5344
5417
|
this.device.createBindGroup({
|
|
5345
5418
|
layout,
|
|
@@ -5354,6 +5427,7 @@ export class Engine {
|
|
|
5354
5427
|
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
5355
5428
|
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
5356
5429
|
{ binding: 9, resource: this.simSampler },
|
|
5430
|
+
{ binding: PARTICLE_POINTS_BINDING, resource: { buffer: points?.buffer ?? this.pointsFallback } },
|
|
5357
5431
|
...(shadow
|
|
5358
5432
|
? [
|
|
5359
5433
|
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
@@ -5363,12 +5437,14 @@ export class Engine {
|
|
|
5363
5437
|
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
5364
5438
|
]
|
|
5365
5439
|
: []),
|
|
5440
|
+
...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
|
|
5366
5441
|
],
|
|
5367
5442
|
}),
|
|
5368
5443
|
) as [GPUBindGroup, GPUBindGroup]
|
|
5369
5444
|
|
|
5370
|
-
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false)
|
|
5371
|
-
const
|
|
5445
|
+
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false, false)
|
|
5446
|
+
const countLayout = indirect ? layoutFor("storage", GPUShaderStage.COMPUTE, false, true) : null
|
|
5447
|
+
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, true, false)
|
|
5372
5448
|
|
|
5373
5449
|
// Additive keeps the destination and adds to it, and leaves alpha alone, so
|
|
5374
5450
|
// a glow does not claim coverage it never occluded. The MASK sums with it —
|
|
@@ -5383,11 +5459,24 @@ export class Engine {
|
|
|
5383
5459
|
|
|
5384
5460
|
this.device.pushErrorScope("validation")
|
|
5385
5461
|
try {
|
|
5462
|
+
const computePipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: [computeLayout] })
|
|
5386
5463
|
const compute = await this.device.createComputePipelineAsync({
|
|
5387
5464
|
label: "particle compute pipeline",
|
|
5388
|
-
layout:
|
|
5465
|
+
layout: computePipelineLayout,
|
|
5389
5466
|
compute: { module: computeModule, entryPoint: "main" },
|
|
5390
5467
|
})
|
|
5468
|
+
const live =
|
|
5469
|
+
indirect && countLayout
|
|
5470
|
+
? {
|
|
5471
|
+
pipeline: await this.device.createComputePipelineAsync({
|
|
5472
|
+
label: "particle count pipeline",
|
|
5473
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [countLayout] }),
|
|
5474
|
+
compute: { module: computeModule, entryPoint: "rzCount" },
|
|
5475
|
+
}),
|
|
5476
|
+
indirect,
|
|
5477
|
+
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
5478
|
+
}
|
|
5479
|
+
: null
|
|
5391
5480
|
const renderLayoutObj = this.device.createPipelineLayout({ bindGroupLayouts: [renderLayout] })
|
|
5392
5481
|
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
5393
5482
|
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
@@ -5429,6 +5518,8 @@ export class Engine {
|
|
|
5429
5518
|
if (scoped) {
|
|
5430
5519
|
buffer.destroy()
|
|
5431
5520
|
uniform.destroy()
|
|
5521
|
+
points?.buffer.destroy()
|
|
5522
|
+
indirect?.destroy()
|
|
5432
5523
|
return { ok: false, diagnostics: [scoped.message] }
|
|
5433
5524
|
}
|
|
5434
5525
|
return {
|
|
@@ -5444,23 +5535,28 @@ export class Engine {
|
|
|
5444
5535
|
counts: uniformView.uints,
|
|
5445
5536
|
compute,
|
|
5446
5537
|
computeLayout,
|
|
5447
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
5538
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false, false),
|
|
5448
5539
|
depth,
|
|
5449
5540
|
render,
|
|
5450
5541
|
renderLayout,
|
|
5451
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5452
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5542
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true, false),
|
|
5543
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true, false),
|
|
5453
5544
|
rebind: () => ({
|
|
5454
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
5455
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5456
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5545
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false, false),
|
|
5546
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true, false),
|
|
5547
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true, false),
|
|
5548
|
+
countBinds: countLayout ? bindFor(countLayout, this.cameraUniformBuffer, false, true) : null,
|
|
5457
5549
|
}),
|
|
5550
|
+
points,
|
|
5551
|
+
live,
|
|
5458
5552
|
},
|
|
5459
5553
|
}
|
|
5460
5554
|
} catch (e) {
|
|
5461
5555
|
await this.device.popErrorScope()
|
|
5462
5556
|
buffer.destroy()
|
|
5463
5557
|
uniform.destroy()
|
|
5558
|
+
points?.buffer.destroy()
|
|
5559
|
+
indirect?.destroy()
|
|
5464
5560
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] }
|
|
5465
5561
|
}
|
|
5466
5562
|
}
|
|
@@ -5591,6 +5687,7 @@ export class Engine {
|
|
|
5591
5687
|
this.device.queue.writeBuffer(e.lights.uniform, 0, e.lights.data.buffer as ArrayBuffer)
|
|
5592
5688
|
}
|
|
5593
5689
|
this.lightHeader[0] = Math.min(next, MAX_LIGHTS)
|
|
5690
|
+
this.lightHeader[1] = this.docLightCount
|
|
5594
5691
|
this.device.queue.writeBuffer(this.lightsBuffer, 0, this.lightHeader)
|
|
5595
5692
|
}
|
|
5596
5693
|
|
|
@@ -5651,14 +5748,56 @@ export class Engine {
|
|
|
5651
5748
|
p.counts[2] = p.count
|
|
5652
5749
|
p.counts[3] = this.particleFrame++
|
|
5653
5750
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer as ArrayBuffer)
|
|
5751
|
+
if (p.points) this.writePoints(p.points)
|
|
5752
|
+
const bind = p.computeBinds[e.grid?.parity ?? 0]
|
|
5753
|
+
if (p.live) {
|
|
5754
|
+
// The frame's live count first, in a pass of its own: a buffer may
|
|
5755
|
+
// not be written as storage and read as indirect arguments inside one
|
|
5756
|
+
// pass, and a pass is the synchronization scope.
|
|
5757
|
+
const count = encoder.beginComputePass({ label: "particle count" })
|
|
5758
|
+
count.setPipeline(p.live.pipeline)
|
|
5759
|
+
count.setBindGroup(0, p.live.binds[e.grid?.parity ?? 0])
|
|
5760
|
+
count.dispatchWorkgroups(1)
|
|
5761
|
+
count.end()
|
|
5762
|
+
}
|
|
5654
5763
|
const cp = encoder.beginComputePass({ label: "particles" })
|
|
5655
5764
|
cp.setPipeline(p.compute)
|
|
5656
|
-
cp.setBindGroup(0,
|
|
5657
|
-
cp.
|
|
5765
|
+
cp.setBindGroup(0, bind)
|
|
5766
|
+
if (p.live) cp.dispatchWorkgroupsIndirect(p.live.indirect, 0)
|
|
5767
|
+
else cp.dispatchWorkgroups(Math.ceil(p.count / 64))
|
|
5658
5768
|
cp.end()
|
|
5659
5769
|
}
|
|
5660
5770
|
}
|
|
5661
5771
|
|
|
5772
|
+
/**
|
|
5773
|
+
* Every bone matching an effect's `#points` prefix, on every visible model,
|
|
5774
|
+
* posed and placed this frame. Every frame rather than on load: a candle a
|
|
5775
|
+
* character carries moves with the hand, and a stage can be moved.
|
|
5776
|
+
*
|
|
5777
|
+
* The order is the models' order, then rig order — stable while nothing is
|
|
5778
|
+
* added or removed, so particle i stays on point i and a flame does not
|
|
5779
|
+
* jump to another wick between frames.
|
|
5780
|
+
*/
|
|
5781
|
+
private writePoints(points: { prefix: string; buffer: GPUBuffer; data: Float32Array }): void {
|
|
5782
|
+
let n = 0
|
|
5783
|
+
for (const inst of this.modelInstances.values()) {
|
|
5784
|
+
const m = inst.model
|
|
5785
|
+
if (!m.visible) continue
|
|
5786
|
+
let byPrefix = this.pointBones.get(m)
|
|
5787
|
+
if (!byPrefix) this.pointBones.set(m, (byPrefix = new Map()))
|
|
5788
|
+
let bones = byPrefix.get(points.prefix)
|
|
5789
|
+
const rig = m.getSkeleton().bones
|
|
5790
|
+
if (!bones) byPrefix.set(points.prefix, (bones = bonesWithPrefix(rig, points.prefix)))
|
|
5791
|
+
for (const i of bones) {
|
|
5792
|
+
if (n >= MAX_EFFECT_POINTS) break
|
|
5793
|
+
const world = m.getBoneWorldMatrixAt(i)
|
|
5794
|
+
if (world) writeBonePoint(points.data, n++, world, rig[i].tail, m)
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
points.data[0] = n
|
|
5798
|
+
this.device.queue.writeBuffer(points.buffer, 0, points.data.buffer as ArrayBuffer, 0, (4 + n * 8) * 4)
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5662
5801
|
/** Draw the pool. Inside the scene pass, so it is depth-tested and pre-bloom. */
|
|
5663
5802
|
private renderParticles(pass: GPURenderPassEncoder, view: "camera" | "mirror"): void {
|
|
5664
5803
|
for (const e of this.effects) {
|
|
@@ -5666,14 +5805,19 @@ export class Engine {
|
|
|
5666
5805
|
if (!p || e.weight === 0) continue
|
|
5667
5806
|
const parity = e.grid?.parity ?? 0
|
|
5668
5807
|
const bind = view === "mirror" ? p.mirrorRenderBinds[parity] : p.renderBinds[parity]
|
|
5808
|
+
// The live count's quads when the effect declared one, else the pool's.
|
|
5809
|
+
const draw = () => {
|
|
5810
|
+
if (p.live) pass.drawIndirect(p.live.indirect, PARTICLE_INDIRECT_DRAW_OFFSET)
|
|
5811
|
+
else pass.draw(6, p.count)
|
|
5812
|
+
}
|
|
5669
5813
|
if (p.depth) {
|
|
5670
5814
|
pass.setPipeline(p.depth)
|
|
5671
5815
|
pass.setBindGroup(0, bind)
|
|
5672
|
-
|
|
5816
|
+
draw()
|
|
5673
5817
|
}
|
|
5674
5818
|
pass.setPipeline(p.render)
|
|
5675
5819
|
pass.setBindGroup(0, bind)
|
|
5676
|
-
|
|
5820
|
+
draw()
|
|
5677
5821
|
}
|
|
5678
5822
|
}
|
|
5679
5823
|
|
|
@@ -6051,6 +6195,8 @@ export class Engine {
|
|
|
6051
6195
|
for (const e of this.effects) {
|
|
6052
6196
|
e.particles?.buffer.destroy()
|
|
6053
6197
|
e.particles?.uniform.destroy()
|
|
6198
|
+
e.particles?.points?.buffer.destroy()
|
|
6199
|
+
e.particles?.live?.indirect.destroy()
|
|
6054
6200
|
e.particles = null
|
|
6055
6201
|
}
|
|
6056
6202
|
}
|
|
@@ -7488,15 +7634,21 @@ export class Engine {
|
|
|
7488
7634
|
|
|
7489
7635
|
// BEFORE the bind group below, which binds it. Full size from the start:
|
|
7490
7636
|
// every material pipeline binds this, so sizing it to the light count would
|
|
7491
|
-
// mean rebuilding bind groups whenever a scene gained a lamp.
|
|
7492
|
-
//
|
|
7493
|
-
|
|
7637
|
+
// mean rebuilding bind groups whenever a scene gained a lamp. Uploaded from
|
|
7638
|
+
// the CPU copy, which holds any lamps set before the device existed; the
|
|
7639
|
+
// header after it, from the one writer that owns it.
|
|
7494
7640
|
this.lightsBuffer = this.device.createBuffer({
|
|
7495
7641
|
label: "positional lights",
|
|
7496
7642
|
size: this.lightsData.byteLength,
|
|
7497
7643
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
7498
7644
|
})
|
|
7499
7645
|
this.device.queue.writeBuffer(this.lightsBuffer, 0, this.lightsData)
|
|
7646
|
+
this.allocateLightSlots()
|
|
7647
|
+
this.pointsFallback = this.device.createBuffer({
|
|
7648
|
+
label: "no points",
|
|
7649
|
+
size: 16,
|
|
7650
|
+
usage: GPUBufferUsage.STORAGE,
|
|
7651
|
+
})
|
|
7500
7652
|
// BEFORE the per-frame groups, which bind it. The composite needs this 1x1
|
|
7501
7653
|
// stand-in too, and it used to be created with the composite's own
|
|
7502
7654
|
// resources further down — far enough down that a material group built up
|
|
@@ -10047,6 +10199,20 @@ export class Engine {
|
|
|
10047
10199
|
this.lightsData[b + 14] = 0
|
|
10048
10200
|
this.lightsData[b + 15] = 0
|
|
10049
10201
|
}
|
|
10202
|
+
// The grid, from the records as STORED — normalised aim, clamped reach, the
|
|
10203
|
+
// cosines in f32 — so it indexes exactly what the shader will evaluate.
|
|
10204
|
+
const grid = buildLightGrid(
|
|
10205
|
+
list.map((_, i) => {
|
|
10206
|
+
const b = LIGHT_HEADER + i * LIGHT_STRIDE
|
|
10207
|
+
const d = this.lightsData
|
|
10208
|
+
return { x: d[b], y: d[b + 1], z: d[b + 2], radius: d[b + 3], ax: d[b + 8], ay: d[b + 9], az: d[b + 10], cosOuter: d[b + 11] }
|
|
10209
|
+
}),
|
|
10210
|
+
)
|
|
10211
|
+
this.lightHeader.set(grid.origin, 4)
|
|
10212
|
+
this.lightHeader[7] = 1 / grid.cell
|
|
10213
|
+
this.lightHeaderWords.set(grid.dims, 8)
|
|
10214
|
+
this.lightHeaderWords.set(grid.outside, 12)
|
|
10215
|
+
this.lightsWords.set(grid.cells, LIGHT_GRID_BASE)
|
|
10050
10216
|
// The CPU copy is written either way; only the upload needs a device. A
|
|
10051
10217
|
// scene whose lamps arrive before init keeps them, and init uploads them.
|
|
10052
10218
|
if (list.length && this.device && this.lightsBuffer) {
|
|
@@ -10057,6 +10223,13 @@ export class Engine {
|
|
|
10057
10223
|
LIGHT_HEADER * 4,
|
|
10058
10224
|
list.length * LIGHT_STRIDE * 4,
|
|
10059
10225
|
)
|
|
10226
|
+
this.device.queue.writeBuffer(
|
|
10227
|
+
this.lightsBuffer,
|
|
10228
|
+
LIGHT_GRID_BASE * 4,
|
|
10229
|
+
this.lightsData.buffer as ArrayBuffer,
|
|
10230
|
+
LIGHT_GRID_BASE * 4,
|
|
10231
|
+
grid.cells.byteLength,
|
|
10232
|
+
)
|
|
10060
10233
|
}
|
|
10061
10234
|
// The effects' bases move when the document's count does; the header —
|
|
10062
10235
|
// count included — is written there.
|
|
@@ -11621,9 +11794,11 @@ export class Engine {
|
|
|
11621
11794
|
const src = wrapped ? entry.source : entry
|
|
11622
11795
|
const width = Math.max(1, "naturalWidth" in src ? src.naturalWidth : src.width)
|
|
11623
11796
|
const height = Math.max(1, "naturalHeight" in src ? src.naturalHeight : src.height)
|
|
11797
|
+
const mipLevelCount = wrapped && entry.mipmaps ? Math.floor(Math.log2(Math.max(width, height))) + 1 : 1
|
|
11624
11798
|
const tex = this.device.createTexture({
|
|
11625
11799
|
label: `group map: ${group.id}`,
|
|
11626
11800
|
size: [width, height],
|
|
11801
|
+
mipLevelCount,
|
|
11627
11802
|
// Colour maps decode to linear on sample, the way material textures do;
|
|
11628
11803
|
// data maps must not, or every threshold packed in their channels moves.
|
|
11629
11804
|
format: wrapped && entry.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
@@ -11634,6 +11809,7 @@ export class Engine {
|
|
|
11634
11809
|
{ texture: tex, premultipliedAlpha: wrapped && entry.premultiplied === true },
|
|
11635
11810
|
[width, height],
|
|
11636
11811
|
)
|
|
11812
|
+
if (mipLevelCount > 1) this.generateMipmaps(tex, mipLevelCount)
|
|
11637
11813
|
return tex
|
|
11638
11814
|
})
|
|
11639
11815
|
}
|
|
@@ -14137,24 +14313,28 @@ export class Engine {
|
|
|
14137
14313
|
// Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
|
|
14138
14314
|
// re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
|
|
14139
14315
|
private generateMipmaps(texture: GPUTexture, mipLevelCount: number) {
|
|
14140
|
-
if (!this.
|
|
14316
|
+
if (!this.mipBlitSampler) {
|
|
14141
14317
|
this.mipBlitSampler = this.device.createSampler({
|
|
14142
14318
|
magFilter: "linear",
|
|
14143
14319
|
minFilter: "linear",
|
|
14144
14320
|
addressModeU: "clamp-to-edge",
|
|
14145
14321
|
addressModeV: "clamp-to-edge",
|
|
14146
14322
|
})
|
|
14323
|
+
}
|
|
14324
|
+
let pipeline = this.mipBlitPipelines.get(texture.format)
|
|
14325
|
+
if (!pipeline) {
|
|
14147
14326
|
const module = this.device.createShaderModule({
|
|
14148
14327
|
label: "mipmap blit",
|
|
14149
14328
|
code: MIPMAP_BLIT_SHADER_WGSL,
|
|
14150
14329
|
})
|
|
14151
|
-
|
|
14152
|
-
label:
|
|
14330
|
+
pipeline = this.device.createRenderPipeline({
|
|
14331
|
+
label: `mipmap blit pipeline (${texture.format})`,
|
|
14153
14332
|
layout: "auto",
|
|
14154
14333
|
vertex: { module, entryPoint: "vs" },
|
|
14155
|
-
fragment: { module, entryPoint: "fs", targets: [{ format:
|
|
14334
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: texture.format }] },
|
|
14156
14335
|
primitive: { topology: "triangle-list" },
|
|
14157
14336
|
})
|
|
14337
|
+
this.mipBlitPipelines.set(texture.format, pipeline)
|
|
14158
14338
|
}
|
|
14159
14339
|
|
|
14160
14340
|
const encoder = this.device.createCommandEncoder({ label: "mipgen" })
|
|
@@ -14162,7 +14342,7 @@ export class Engine {
|
|
|
14162
14342
|
const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 })
|
|
14163
14343
|
const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1 })
|
|
14164
14344
|
const bindGroup = this.device.createBindGroup({
|
|
14165
|
-
layout:
|
|
14345
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
14166
14346
|
entries: [
|
|
14167
14347
|
{ binding: 0, resource: srcView },
|
|
14168
14348
|
{ binding: 1, resource: this.mipBlitSampler },
|
|
@@ -14173,7 +14353,7 @@ export class Engine {
|
|
|
14173
14353
|
{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
14174
14354
|
],
|
|
14175
14355
|
})
|
|
14176
|
-
pass.setPipeline(
|
|
14356
|
+
pass.setPipeline(pipeline)
|
|
14177
14357
|
pass.setBindGroup(0, bindGroup)
|
|
14178
14358
|
pass.draw(3)
|
|
14179
14359
|
pass.end()
|
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
|
|