reze-engine 0.55.4 → 0.55.7
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/camera.d.ts +10 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +14 -2
- package/dist/engine.d.ts +27 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +145 -29
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/shaders/lights.d.ts +1 -1
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +5 -1
- package/dist/shaders/passes/grid.d.ts +1 -1
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +4 -1
- package/dist/shaders/passes/particles.d.ts +4 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +2 -0
- package/dist/shaders/passes/trails.d.ts +2 -0
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +1 -0
- package/package.json +1 -1
- package/src/camera.ts +12 -2
- package/src/engine.ts +180 -27
- package/src/index.ts +14 -0
- package/src/shaders/lights.ts +5 -0
- package/src/shaders/passes/grid.ts +4 -1
- package/src/shaders/passes/particles.ts +6 -0
- package/src/shaders/passes/trails.ts +3 -0
package/src/engine.ts
CHANGED
|
@@ -1271,6 +1271,26 @@ function vertexMorphReach(model: Model): number {
|
|
|
1271
1271
|
const TEXTURE_EXTENSION_GUESSES = [".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds", ".spa", ".sph"]
|
|
1272
1272
|
|
|
1273
1273
|
/** One effect's GPU particle pool. Per effect: each declares its own count. */
|
|
1274
|
+
/**
|
|
1275
|
+
* The binding number an effect's params take in each pass.
|
|
1276
|
+
*
|
|
1277
|
+
* 7 in the composite, the particle stages and the ribbon pass, which all stop
|
|
1278
|
+
* below it. The GRID reaches 8 on its own, so it takes the next one up rather
|
|
1279
|
+
* than everything else moving to accommodate one mount.
|
|
1280
|
+
*/
|
|
1281
|
+
const EFFECT_PARAMS_BINDING = 7
|
|
1282
|
+
const EFFECT_PARAMS_BINDING_GRID = 9
|
|
1283
|
+
|
|
1284
|
+
/** How a mount receives the effect's declared dials: a generator for the struct
|
|
1285
|
+
* at the binding that mount has free, and the buffer behind it. Null buffer
|
|
1286
|
+
* means the effect declared none, and then neither the decl nor the binding is
|
|
1287
|
+
* emitted — WGSL has no empty struct, and a bound buffer nothing reads is a
|
|
1288
|
+
* layout mismatch. */
|
|
1289
|
+
type EffectParamsBinding = {
|
|
1290
|
+
wgsl: (binding: number) => string
|
|
1291
|
+
buffer: GPUBuffer | null
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1274
1294
|
interface EffectParticles {
|
|
1275
1295
|
count: number
|
|
1276
1296
|
buffer: GPUBuffer
|
|
@@ -1313,6 +1333,9 @@ interface EffectGrid {
|
|
|
1313
1333
|
data: Float32Array
|
|
1314
1334
|
parity: number
|
|
1315
1335
|
frame: number
|
|
1336
|
+
/** The effect's declared dials, or null. Held HERE as well as on the instance
|
|
1337
|
+
* because the rebind path rebuilds this bind group from the grid alone. */
|
|
1338
|
+
params: GPUBuffer | null
|
|
1316
1339
|
}
|
|
1317
1340
|
|
|
1318
1341
|
/**
|
|
@@ -1470,6 +1493,9 @@ interface EffectInstance {
|
|
|
1470
1493
|
/** How many slots it asked for. Its base is assigned by the engine and can
|
|
1471
1494
|
* move, which is why it travels in the uniform rather than the shader. */
|
|
1472
1495
|
count: number
|
|
1496
|
+
/** The effect's declared parameters, or null when it declares none. Kept
|
|
1497
|
+
* for the same reason `layout` is: `bind` is rebuilt on a buffer swap. */
|
|
1498
|
+
params: GPUBuffer | null
|
|
1473
1499
|
} | null
|
|
1474
1500
|
/** The field mount's pipeline and its two parity bind groups, or null when the
|
|
1475
1501
|
* effect declares neither background nor foreground. */
|
|
@@ -3253,7 +3279,13 @@ export class Engine {
|
|
|
3253
3279
|
* fail where it was written, it fails at the next submit.
|
|
3254
3280
|
*/
|
|
3255
3281
|
private gridBindGroup(
|
|
3256
|
-
g: {
|
|
3282
|
+
g: {
|
|
3283
|
+
layout: GPUBindGroupLayout
|
|
3284
|
+
uniform: GPUBuffer
|
|
3285
|
+
read: [GPUTextureView, GPUTextureView]
|
|
3286
|
+
textures: [GPUTexture, GPUTexture]
|
|
3287
|
+
params: GPUBuffer | null
|
|
3288
|
+
},
|
|
3257
3289
|
i: number,
|
|
3258
3290
|
): GPUBindGroup {
|
|
3259
3291
|
return this.device.createBindGroup({
|
|
@@ -3268,6 +3300,9 @@ export class Engine {
|
|
|
3268
3300
|
{ binding: 6, resource: { buffer: this.compositeUniformBuffer } },
|
|
3269
3301
|
{ binding: 7, resource: { buffer: this.midiBuffer } },
|
|
3270
3302
|
{ binding: 8, resource: { buffer: this.lyricsBuffer } },
|
|
3303
|
+
// The grid is the one pass whose own bindings reach 8, so its params sit
|
|
3304
|
+
// above them rather than everything else shifting for one mount.
|
|
3305
|
+
...(g.params ? [{ binding: EFFECT_PARAMS_BINDING_GRID, resource: { buffer: g.params } }] : []),
|
|
3271
3306
|
],
|
|
3272
3307
|
})
|
|
3273
3308
|
}
|
|
@@ -3304,7 +3339,7 @@ export class Engine {
|
|
|
3304
3339
|
e.particles.mirrorRenderBind = b.mirrorRenderBind
|
|
3305
3340
|
}
|
|
3306
3341
|
if (e.grid) e.grid.binds = [this.gridBindGroup(e.grid, 0), this.gridBindGroup(e.grid, 1)]
|
|
3307
|
-
if (e.lights) e.lights.bind = this.lightEmitBindGroup(e.lights.layout, e.lights.uniform)
|
|
3342
|
+
if (e.lights) e.lights.bind = this.lightEmitBindGroup(e.lights.layout, e.lights.uniform, e.lights.params)
|
|
3308
3343
|
}
|
|
3309
3344
|
}
|
|
3310
3345
|
|
|
@@ -3610,9 +3645,19 @@ export class Engine {
|
|
|
3610
3645
|
paramsData[slot.offset + 2] = value.z
|
|
3611
3646
|
}
|
|
3612
3647
|
}
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3648
|
+
/**
|
|
3649
|
+
* The params block, at whatever binding the pass asking has free.
|
|
3650
|
+
*
|
|
3651
|
+
* A single hardcoded slot cannot serve every mount — the grid's layout
|
|
3652
|
+
* already reaches binding 8 — so the struct is generated per pass and each
|
|
3653
|
+
* one states the number it can spare. Empty when nothing is declared: WGSL
|
|
3654
|
+
* has no empty struct, and a binding nothing reads is a layout mismatch.
|
|
3655
|
+
*/
|
|
3656
|
+
const paramsWgsl = (binding: number) =>
|
|
3657
|
+
entries.length
|
|
3658
|
+
? `struct EffectParams {\n${fields.join("\n")}\n}\n@group(0) @binding(${binding}) var<uniform> params: EffectParams;\n`
|
|
3659
|
+
: ""
|
|
3660
|
+
const paramsDecl = paramsWgsl(EFFECT_PARAMS_BINDING)
|
|
3616
3661
|
|
|
3617
3662
|
// ── Compile with validation captured, not thrown at the console. Line
|
|
3618
3663
|
// numbers in diagnostics are rebased to the USER's source.
|
|
@@ -3720,7 +3765,32 @@ export class Engine {
|
|
|
3720
3765
|
let grid: EffectGrid | null = null
|
|
3721
3766
|
let trails: EffectTrails | null = null
|
|
3722
3767
|
|
|
3768
|
+
// PARAMS ARE NOT A FIELD-MOUNT FEATURE, and used to be one by accident.
|
|
3769
|
+
//
|
|
3770
|
+
// `paramsDecl` was spliced into the composite alone, so `#param` worked for
|
|
3771
|
+
// background/foreground and produced "unresolved value 'params'" for every
|
|
3772
|
+
// particle, grid and ribbon effect — which is most of the ones anyone wants
|
|
3773
|
+
// a dial on. Rain's fall speed is the whole example.
|
|
3774
|
+
//
|
|
3775
|
+
// The buffer is created HERE rather than beside the instance it ends up on,
|
|
3776
|
+
// because the mount builders below need to bind it and they run first. That
|
|
3777
|
+
// makes `abandon` its owner: a mount that fails to compile must not leak it.
|
|
3778
|
+
let paramsBuffer: GPUBuffer | null = null
|
|
3779
|
+
if (entries.length) {
|
|
3780
|
+
paramsBuffer = this.device.createBuffer({
|
|
3781
|
+
label: "effect params",
|
|
3782
|
+
size: paramsData.byteLength,
|
|
3783
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
3784
|
+
})
|
|
3785
|
+
this.device.queue.writeBuffer(paramsBuffer, 0, paramsData)
|
|
3786
|
+
}
|
|
3787
|
+
/** What every mount splices and binds: a generator for the struct decl at
|
|
3788
|
+
* the binding that mount has free, and the buffer behind it. The buffer is
|
|
3789
|
+
* null when nothing is declared, and then no binding is added at all. */
|
|
3790
|
+
const paramsFor: EffectParamsBinding = { wgsl: paramsWgsl, buffer: paramsBuffer }
|
|
3791
|
+
|
|
3723
3792
|
const abandon = (diagnostics: string[]): EffectResult => {
|
|
3793
|
+
paramsBuffer?.destroy()
|
|
3724
3794
|
particles?.buffer.destroy()
|
|
3725
3795
|
particles?.uniform.destroy()
|
|
3726
3796
|
grid?.textures[0].destroy()
|
|
@@ -3730,12 +3800,12 @@ export class Engine {
|
|
|
3730
3800
|
return { ok: false, diagnostics, mounts, params: d.params, duration: d.duration }
|
|
3731
3801
|
}
|
|
3732
3802
|
if (wantsParticles) {
|
|
3733
|
-
const built = await this.buildParticles(wgsl, d, anchors, alias)
|
|
3803
|
+
const built = await this.buildParticles(wgsl, d, anchors, alias, paramsFor)
|
|
3734
3804
|
if (!built.ok) return abandon(built.diagnostics)
|
|
3735
3805
|
particles = built.state
|
|
3736
3806
|
}
|
|
3737
3807
|
if (gridEntryPoint(wgsl)) {
|
|
3738
|
-
const built = await this.buildSim(wgsl, d, anchors, alias)
|
|
3808
|
+
const built = await this.buildSim(wgsl, d, anchors, alias, paramsFor)
|
|
3739
3809
|
if (!built.ok) return abandon(built.diagnostics)
|
|
3740
3810
|
grid = built.state
|
|
3741
3811
|
}
|
|
@@ -3744,9 +3814,9 @@ export class Engine {
|
|
|
3744
3814
|
// bone recorded without one would read zeroes and paint a line to the origin.
|
|
3745
3815
|
const trailSlots = anchors.filter((a) => a.trail).length
|
|
3746
3816
|
if (trailSlots === 0) {
|
|
3747
|
-
return
|
|
3817
|
+
return abandon(["a ribbon effect needs at least one #anchor <bone> trail"])
|
|
3748
3818
|
}
|
|
3749
|
-
const built = await this.buildTrails(wgsl, d, anchors, alias)
|
|
3819
|
+
const built = await this.buildTrails(wgsl, d, anchors, alias, paramsFor)
|
|
3750
3820
|
if (!built.ok) return abandon(built.diagnostics)
|
|
3751
3821
|
trails = built.state
|
|
3752
3822
|
}
|
|
@@ -3769,7 +3839,7 @@ export class Engine {
|
|
|
3769
3839
|
])
|
|
3770
3840
|
}
|
|
3771
3841
|
if (declaredLights > 0) {
|
|
3772
|
-
const built = await this.buildLightEmit(wgsl, declaredLights, alias, anchors)
|
|
3842
|
+
const built = await this.buildLightEmit(wgsl, declaredLights, alias, anchors, paramsFor)
|
|
3773
3843
|
if (!built.ok) return abandon(built.diagnostics)
|
|
3774
3844
|
lights = built.state
|
|
3775
3845
|
}
|
|
@@ -3783,15 +3853,6 @@ export class Engine {
|
|
|
3783
3853
|
})
|
|
3784
3854
|
: null
|
|
3785
3855
|
|
|
3786
|
-
let paramsBuffer: GPUBuffer | null = null
|
|
3787
|
-
if (entries.length) {
|
|
3788
|
-
paramsBuffer = this.device.createBuffer({
|
|
3789
|
-
label: "effect params",
|
|
3790
|
-
size: paramsData.byteLength,
|
|
3791
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
3792
|
-
})
|
|
3793
|
-
this.device.queue.writeBuffer(paramsBuffer, 0, paramsData)
|
|
3794
|
-
}
|
|
3795
3856
|
const instance: EffectInstance = {
|
|
3796
3857
|
wgsl,
|
|
3797
3858
|
paramDecls: d.params,
|
|
@@ -4041,11 +4102,14 @@ export class Engine {
|
|
|
4041
4102
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
4042
4103
|
* describes the effect that is still on screen. */
|
|
4043
4104
|
alias: number[],
|
|
4105
|
+
/** The effect's declared dials. Spliced into both stages and bound at 7,
|
|
4106
|
+
* the same binding the composite gives them. */
|
|
4107
|
+
params: EffectParamsBinding,
|
|
4044
4108
|
): Promise<{ ok: true; state: EffectParticles } | { ok: false; diagnostics: string[] }> {
|
|
4045
4109
|
// No pragma means "some": an author who wrote the trio clearly wants
|
|
4046
4110
|
// particles, and failing over a missing comment would be pedantry.
|
|
4047
4111
|
const count = Math.min(d.particles || 1024, Engine.MAX_PARTICLES)
|
|
4048
|
-
const src = { wgsl, count, blend: d.particleBlend, bloom: d.bloom }
|
|
4112
|
+
const src = { wgsl, count, blend: d.particleBlend, bloom: d.bloom, paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING) }
|
|
4049
4113
|
// Sparks want to spawn where a trail is, so the particle stages see the same
|
|
4050
4114
|
// cast buffer the trail draw reads.
|
|
4051
4115
|
const cast = {
|
|
@@ -4110,6 +4174,9 @@ export class Engine {
|
|
|
4110
4174
|
// rule and a background read the same instant.
|
|
4111
4175
|
{ binding: 5, visibility, buffer: { type: "read-only-storage" } },
|
|
4112
4176
|
{ binding: 6, visibility, buffer: { type: "read-only-storage" } },
|
|
4177
|
+
// Only when the effect declared any: WGSL has no empty struct, so a
|
|
4178
|
+
// param-less effect must not carry the decl OR the binding.
|
|
4179
|
+
...(params.buffer ? [{ binding: 7, visibility, buffer: { type: "uniform" as const } }] : []),
|
|
4113
4180
|
],
|
|
4114
4181
|
})
|
|
4115
4182
|
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer) =>
|
|
@@ -4123,6 +4190,7 @@ export class Engine {
|
|
|
4123
4190
|
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4124
4191
|
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4125
4192
|
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4193
|
+
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
4126
4194
|
],
|
|
4127
4195
|
})
|
|
4128
4196
|
|
|
@@ -4212,6 +4280,7 @@ export class Engine {
|
|
|
4212
4280
|
count: number,
|
|
4213
4281
|
alias: number[],
|
|
4214
4282
|
anchors: { bone: string; trail: boolean }[],
|
|
4283
|
+
params: EffectParamsBinding,
|
|
4215
4284
|
): Promise<{ ok: true; state: NonNullable<EffectInstance["lights"]> } | { ok: false; diagnostics: string[] }> {
|
|
4216
4285
|
const layout = this.device.createBindGroupLayout({
|
|
4217
4286
|
label: "light emit layout",
|
|
@@ -4223,13 +4292,21 @@ export class Engine {
|
|
|
4223
4292
|
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4224
4293
|
{ binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4225
4294
|
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4295
|
+
...(params.buffer
|
|
4296
|
+
? [{ binding: EFFECT_PARAMS_BINDING, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" as const } }]
|
|
4297
|
+
: []),
|
|
4226
4298
|
],
|
|
4227
4299
|
})
|
|
4228
4300
|
const module = this.device.createShaderModule({
|
|
4229
4301
|
label: "light emit",
|
|
4230
4302
|
// The same scene API and the same per-effect anchor alias its drawing
|
|
4231
4303
|
// half gets, so a lamp reads the cast exactly as the beam that paints it.
|
|
4232
|
-
code: buildLightEmitShader(
|
|
4304
|
+
code: buildLightEmitShader(
|
|
4305
|
+
wgsl,
|
|
4306
|
+
EFFECT_SCENE_API + anchorAliasWgsl(alias),
|
|
4307
|
+
{ trailCount: anchors.filter((a) => a.trail).length },
|
|
4308
|
+
params.wgsl(EFFECT_PARAMS_BINDING),
|
|
4309
|
+
),
|
|
4233
4310
|
})
|
|
4234
4311
|
const info = await module.getCompilationInfo()
|
|
4235
4312
|
const diagnostics = info.messages.filter((m) => m.type === "error").map((m) => `${m.lineNum}:${m.linePos} ${m.message}`)
|
|
@@ -4251,15 +4328,15 @@ export class Engine {
|
|
|
4251
4328
|
uniform.destroy()
|
|
4252
4329
|
return { ok: false, diagnostics: [scoped.message] }
|
|
4253
4330
|
}
|
|
4254
|
-
const bind = this.lightEmitBindGroup(layout, uniform)
|
|
4331
|
+
const bind = this.lightEmitBindGroup(layout, uniform, params.buffer)
|
|
4255
4332
|
// The layout travels with the state so the emitter can be rebound when a
|
|
4256
4333
|
// shared buffer under it is replaced — see rebindSharedBuffers.
|
|
4257
|
-
return { ok: true, state: { pipeline, bind, layout, uniform, data, count } }
|
|
4334
|
+
return { ok: true, state: { pipeline, bind, layout, uniform, data, count, params: params.buffer } }
|
|
4258
4335
|
}
|
|
4259
4336
|
|
|
4260
4337
|
/** The light emitter's bind group. One author, for the reason gridBindGroup
|
|
4261
4338
|
* gives: it is built once and rebuilt on every shared-buffer swap. */
|
|
4262
|
-
private lightEmitBindGroup(layout: GPUBindGroupLayout, uniform: GPUBuffer): GPUBindGroup {
|
|
4339
|
+
private lightEmitBindGroup(layout: GPUBindGroupLayout, uniform: GPUBuffer, params: GPUBuffer | null): GPUBindGroup {
|
|
4263
4340
|
return this.device.createBindGroup({
|
|
4264
4341
|
label: "light emit bind",
|
|
4265
4342
|
layout,
|
|
@@ -4271,6 +4348,7 @@ export class Engine {
|
|
|
4271
4348
|
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4272
4349
|
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4273
4350
|
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4351
|
+
...(params ? [{ binding: EFFECT_PARAMS_BINDING, resource: { buffer: params } }] : []),
|
|
4274
4352
|
],
|
|
4275
4353
|
})
|
|
4276
4354
|
}
|
|
@@ -4380,6 +4458,8 @@ export class Engine {
|
|
|
4380
4458
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
4381
4459
|
* describes the effect that is still on screen. */
|
|
4382
4460
|
alias: number[],
|
|
4461
|
+
/** The effect's declared dials, spliced and bound at 7 as everywhere else. */
|
|
4462
|
+
params: EffectParamsBinding,
|
|
4383
4463
|
): Promise<{ ok: true; state: EffectTrails } | { ok: false; diagnostics: string[] }> {
|
|
4384
4464
|
// `slots` here is how many RIBBONS to draw — one per trailed anchor — which
|
|
4385
4465
|
// is a different number from the anchor ADDRESS SPACE the accessors index
|
|
@@ -4390,7 +4470,7 @@ export class Engine {
|
|
|
4390
4470
|
// nothing before: ribbon i was read as anchor slot i.
|
|
4391
4471
|
const ribbonSlots = anchors.map((a, i) => (a.trail ? i : -1)).filter((i) => i >= 0)
|
|
4392
4472
|
const slots = ribbonSlots.length
|
|
4393
|
-
const src = { wgsl, slots, ribbonSlots, blend: d.particleBlend, bloom: d.bloom }
|
|
4473
|
+
const src = { wgsl, slots, ribbonSlots, blend: d.particleBlend, bloom: d.bloom, paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING) }
|
|
4394
4474
|
const code = buildTrailShader(src, {
|
|
4395
4475
|
subjects: MAX_EFFECT_SUBJECTS,
|
|
4396
4476
|
samples: TRAIL_SAMPLES,
|
|
@@ -4434,6 +4514,9 @@ export class Engine {
|
|
|
4434
4514
|
{ binding: 5, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
4435
4515
|
// The lyrics, for rzLyric*.
|
|
4436
4516
|
{ binding: 6, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
4517
|
+
...(params.buffer
|
|
4518
|
+
? [{ binding: 7, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" as const } }]
|
|
4519
|
+
: []),
|
|
4437
4520
|
],
|
|
4438
4521
|
})
|
|
4439
4522
|
// TWO targets, the scene pass's own: HDR colour and the aux (bloom mask,
|
|
@@ -4486,6 +4569,7 @@ export class Engine {
|
|
|
4486
4569
|
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4487
4570
|
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4488
4571
|
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4572
|
+
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
4489
4573
|
],
|
|
4490
4574
|
}),
|
|
4491
4575
|
mirrorBind: this.device.createBindGroup({
|
|
@@ -4497,6 +4581,7 @@ export class Engine {
|
|
|
4497
4581
|
{ binding: 4, resource: { buffer: this.audioBuffer } },
|
|
4498
4582
|
{ binding: 5, resource: { buffer: this.midiBuffer } },
|
|
4499
4583
|
{ binding: 6, resource: { buffer: this.lyricsBuffer } },
|
|
4584
|
+
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
4500
4585
|
],
|
|
4501
4586
|
}),
|
|
4502
4587
|
},
|
|
@@ -4694,6 +4779,8 @@ export class Engine {
|
|
|
4694
4779
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
4695
4780
|
* describes the effect that is still on screen. */
|
|
4696
4781
|
alias: number[],
|
|
4782
|
+
/** The effect's declared dials, spliced and bound above the grid's own. */
|
|
4783
|
+
params: EffectParamsBinding,
|
|
4697
4784
|
): Promise<{ ok: true; state: EffectGrid } | { ok: false; diagnostics: string[] }> {
|
|
4698
4785
|
const size = Math.min(d.grid || 256, GRID_MAX)
|
|
4699
4786
|
const cast = {
|
|
@@ -4705,7 +4792,7 @@ export class Engine {
|
|
|
4705
4792
|
trailCount: anchors.filter((x) => x.trail).length,
|
|
4706
4793
|
alias,
|
|
4707
4794
|
}
|
|
4708
|
-
const code = buildSimShader(wgsl, size, cast)
|
|
4795
|
+
const code = buildSimShader(wgsl, size, cast, params.wgsl(EFFECT_PARAMS_BINDING_GRID))
|
|
4709
4796
|
const offset = code.slice(0, code.indexOf(wgsl)).split("\n").length - 1
|
|
4710
4797
|
this.device.pushErrorScope("validation")
|
|
4711
4798
|
const module = this.device.createShaderModule({ label: "grid step", code })
|
|
@@ -4733,6 +4820,9 @@ export class Engine {
|
|
|
4733
4820
|
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
4734
4821
|
{ binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4735
4822
|
{ binding: 8, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4823
|
+
...(params.buffer
|
|
4824
|
+
? [{ binding: EFFECT_PARAMS_BINDING_GRID, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" as const } }]
|
|
4825
|
+
: []),
|
|
4736
4826
|
],
|
|
4737
4827
|
})
|
|
4738
4828
|
|
|
@@ -4766,7 +4856,7 @@ export class Engine {
|
|
|
4766
4856
|
return { ok: false, diagnostics: [scoped.message] }
|
|
4767
4857
|
}
|
|
4768
4858
|
// One per parity: binds[i] READS textures[i] and WRITES the other.
|
|
4769
|
-
const bindFor = (i: number) => this.gridBindGroup({ layout, uniform, read, textures }, i)
|
|
4859
|
+
const bindFor = (i: number) => this.gridBindGroup({ layout, uniform, read, textures, params: params.buffer }, i)
|
|
4770
4860
|
return {
|
|
4771
4861
|
ok: true,
|
|
4772
4862
|
state: {
|
|
@@ -4780,6 +4870,7 @@ export class Engine {
|
|
|
4780
4870
|
data: new Float32Array(4),
|
|
4781
4871
|
parity: 0,
|
|
4782
4872
|
frame: 0,
|
|
4873
|
+
params: params.buffer,
|
|
4783
4874
|
},
|
|
4784
4875
|
}
|
|
4785
4876
|
} catch (e) {
|
|
@@ -7604,17 +7695,23 @@ export class Engine {
|
|
|
7604
7695
|
/** Bind camera orbit center to a model's bone (Souls-style follow cam). Pass null to unbind. */
|
|
7605
7696
|
setCameraTarget(model: Model | null, boneName: string, offset?: Vec3): void
|
|
7606
7697
|
setCameraTarget(modelOrVec: Model | Vec3 | null, boneName?: string, offset?: Vec3): void {
|
|
7698
|
+
// panLocked tracks the BINDING, on every path that changes it — a target
|
|
7699
|
+
// set to a fixed point is pannable again, and forgetting it here is how the
|
|
7700
|
+
// pan stays dead after the follow was turned off. See Camera.panLocked.
|
|
7607
7701
|
if (modelOrVec === null) {
|
|
7702
|
+
this.camera.panLocked = false
|
|
7608
7703
|
this.cameraTargetModel = null
|
|
7609
7704
|
return
|
|
7610
7705
|
}
|
|
7611
7706
|
if ("x" in modelOrVec && "y" in modelOrVec && "z" in modelOrVec) {
|
|
7707
|
+
this.camera.panLocked = false
|
|
7612
7708
|
this.cameraTargetModel = null
|
|
7613
7709
|
this.camera.target.x = modelOrVec.x
|
|
7614
7710
|
this.camera.target.y = modelOrVec.y
|
|
7615
7711
|
this.camera.target.z = modelOrVec.z
|
|
7616
7712
|
return
|
|
7617
7713
|
}
|
|
7714
|
+
this.camera.panLocked = true
|
|
7618
7715
|
this.cameraTargetModel = modelOrVec
|
|
7619
7716
|
this.cameraTargetBoneName = boneName ?? ""
|
|
7620
7717
|
this.cameraTargetOffset.x = offset?.x ?? 0
|
|
@@ -7624,6 +7721,9 @@ export class Engine {
|
|
|
7624
7721
|
|
|
7625
7722
|
/** Souls-style follow cam: orbit center tracks a model bone each frame. Shorthand for setCameraTarget(model, boneName, offset). */
|
|
7626
7723
|
setCameraFollow(model: Model | null, boneName?: string, offset?: Vec3, smoothing?: number): void {
|
|
7724
|
+
// Panning sets the very thing the follow overwrites each frame, so it is
|
|
7725
|
+
// refused for as long as the shot is riding a bone. See Camera.panLocked.
|
|
7726
|
+
this.camera.panLocked = model !== null
|
|
7627
7727
|
if (model === null) {
|
|
7628
7728
|
this.cameraTargetModel = null
|
|
7629
7729
|
return
|
|
@@ -9397,6 +9497,59 @@ export class Engine {
|
|
|
9397
9497
|
else inst.hiddenMaterials.add(materialName)
|
|
9398
9498
|
}
|
|
9399
9499
|
|
|
9500
|
+
/**
|
|
9501
|
+
* Push a colour/shading edit straight into a material's own uniform buffer —
|
|
9502
|
+
* the same block createMaterialUniformBuffer wrote at load, offset for
|
|
9503
|
+
* offset. A single write per call, on the fields that actually moved, so
|
|
9504
|
+
* dragging one slider does not touch the other eleven.
|
|
9505
|
+
*
|
|
9506
|
+
* UNGROUPED materials only. A grouped material renders through its style
|
|
9507
|
+
* group's own compiled graph (setupPipelines' neutral/DEFAULT_GRAPH path is
|
|
9508
|
+
* what reads this buffer, and a grouped material never runs it) — the call
|
|
9509
|
+
* still writes the bytes, they are simply never sampled, which would look
|
|
9510
|
+
* like the edit silently failing. Callers check groupsByModel first and this
|
|
9511
|
+
* quietly no-ops rather than assume that check was made, since a document
|
|
9512
|
+
* edit landing to the WRONG channel (the group's own colour input) would be
|
|
9513
|
+
* a worse failure than one that does nothing.
|
|
9514
|
+
*
|
|
9515
|
+
* Structural fields — anything that changes which draw bucket a material is
|
|
9516
|
+
* in, or which texture it binds — are NOT here: alpha crossing the 1.0
|
|
9517
|
+
* opaque/transparent line, edge on/off, a texture swap, all need the draw
|
|
9518
|
+
* list or the bind group rebuilt, not a uniform write. Those get their own
|
|
9519
|
+
* call when something needs them.
|
|
9520
|
+
*/
|
|
9521
|
+
setMaterialUniforms(
|
|
9522
|
+
modelName: string,
|
|
9523
|
+
materialName: string,
|
|
9524
|
+
patch: {
|
|
9525
|
+
diffuse?: readonly [number, number, number, number]
|
|
9526
|
+
specular?: readonly [number, number, number]
|
|
9527
|
+
specularPower?: number
|
|
9528
|
+
ambient?: readonly [number, number, number]
|
|
9529
|
+
},
|
|
9530
|
+
): boolean {
|
|
9531
|
+
const inst = this.modelInstances.get(modelName)
|
|
9532
|
+
if (!inst) return false
|
|
9533
|
+
const materials = inst.model.getMaterials()
|
|
9534
|
+
const index = materials.findIndex((m) => m.name === materialName)
|
|
9535
|
+
if (index < 0) return false
|
|
9536
|
+
const buffer = inst.materialUniformBuffers[index]
|
|
9537
|
+
if (!buffer) return false
|
|
9538
|
+
if (patch.diffuse) {
|
|
9539
|
+
this.device.queue.writeBuffer(buffer, 0, new Float32Array(patch.diffuse))
|
|
9540
|
+
}
|
|
9541
|
+
if (patch.ambient) {
|
|
9542
|
+
this.device.queue.writeBuffer(buffer, 16, new Float32Array(patch.ambient))
|
|
9543
|
+
}
|
|
9544
|
+
if (patch.specularPower !== undefined) {
|
|
9545
|
+
this.device.queue.writeBuffer(buffer, 28, new Float32Array([patch.specularPower]))
|
|
9546
|
+
}
|
|
9547
|
+
if (patch.specular) {
|
|
9548
|
+
this.device.queue.writeBuffer(buffer, 32, new Float32Array(patch.specular))
|
|
9549
|
+
}
|
|
9550
|
+
return true
|
|
9551
|
+
}
|
|
9552
|
+
|
|
9400
9553
|
isMaterialVisible(modelName: string, materialName: string): boolean {
|
|
9401
9554
|
const inst = this.modelInstances.get(modelName)
|
|
9402
9555
|
return inst ? !inst.hiddenMaterials.has(materialName) : false
|
package/src/index.ts
CHANGED
|
@@ -184,3 +184,17 @@ export {
|
|
|
184
184
|
type RGBA,
|
|
185
185
|
type Vec3Tuple,
|
|
186
186
|
} from "./overlay"
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The WGSL an effect's own source is compiled against — the hashes, the noise,
|
|
190
|
+
* the falloff, and the Particle struct the particle trio is written in terms of.
|
|
191
|
+
*
|
|
192
|
+
* Exported because reze-design renders LIBRARY PREVIEWS by hosting an effect's
|
|
193
|
+
* source itself, and a preview that reimplements this drifts from the renderer
|
|
194
|
+
* it is previewing. `rzHash13` meaning one thing on a card and another in the
|
|
195
|
+
* scene is worse than no card at all — and a preview missing them entirely just
|
|
196
|
+
* fails to compile, which is a blank card with nothing said anywhere.
|
|
197
|
+
*
|
|
198
|
+
* Pure strings, no GPU: importing them costs a host nothing.
|
|
199
|
+
*/
|
|
200
|
+
export { EFFECT_MATH_API, PARTICLE_STRUCT_WGSL } from "./shaders/passes/hosted-api"
|
package/src/shaders/lights.ts
CHANGED
|
@@ -127,6 +127,7 @@ export function buildLightEmitShader(
|
|
|
127
127
|
wgsl: string,
|
|
128
128
|
sceneApi: string,
|
|
129
129
|
cast: { trailCount: number },
|
|
130
|
+
paramsDecl = "",
|
|
130
131
|
): string {
|
|
131
132
|
return /* wgsl */ `
|
|
132
133
|
// read_write HERE and read-only in the material shaders. Different passes, so
|
|
@@ -164,6 +165,10 @@ ${trailSlotsApi(cast.trailCount)}
|
|
|
164
165
|
// The id accessors, stubbed: this module cannot read an attachment the
|
|
165
166
|
// scene pass writes. See id-api.ts — the author's whole file compiles here.
|
|
166
167
|
${idApi(false, 0, 0) + castDistanceStub()}
|
|
168
|
+
// The dials the author declared, if any. A lamp is exactly the thing someone
|
|
169
|
+
// retunes — its colour and its reach — so an emitter reads params like every
|
|
170
|
+
// other mount rather than being the one place a #param resolves to nothing.
|
|
171
|
+
${paramsDecl}
|
|
167
172
|
${wgsl}
|
|
168
173
|
|
|
169
174
|
@compute @workgroup_size(64)
|
|
@@ -107,9 +107,12 @@ struct SimU {
|
|
|
107
107
|
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
108
108
|
* what turns smoke into a staircase.
|
|
109
109
|
*/
|
|
110
|
-
export function buildSimShader(wgsl: string, size: number, cast: CastLayout): string {
|
|
110
|
+
export function buildSimShader(wgsl: string, size: number, cast: CastLayout, paramsDecl = ""): string {
|
|
111
111
|
return (
|
|
112
112
|
SIM_UNIFORMS +
|
|
113
|
+
// The effect's declared dials, ahead of everything that reads them. Empty
|
|
114
|
+
// when it declared none.
|
|
115
|
+
paramsDecl +
|
|
113
116
|
/* wgsl */ `
|
|
114
117
|
@group(0) @binding(0) var<uniform> su: SimU;
|
|
115
118
|
@group(0) @binding(1) var _rzGridPrevTex: texture_2d<f32>;
|
|
@@ -83,6 +83,10 @@ type ParticleSource = {
|
|
|
83
83
|
blend: ParticleBlend
|
|
84
84
|
/** Feed the bloom pyramid. Off by default — rain should not glow. */
|
|
85
85
|
bloom: boolean
|
|
86
|
+
/** The effect's `#param` block, or empty. Spliced into BOTH stages: a spawn
|
|
87
|
+
* rule reads the same dial the shading does, and rain's fall speed is used
|
|
88
|
+
* in the compute half. */
|
|
89
|
+
paramsDecl: string
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
@@ -165,6 +169,7 @@ export function buildParticleComputeShader(src: ParticleSource, cast: CastLayout
|
|
|
165
169
|
CAMERA_STRUCT +
|
|
166
170
|
PARTICLE_UNIFORMS +
|
|
167
171
|
`
|
|
172
|
+
${src.paramsDecl}
|
|
168
173
|
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
|
|
169
174
|
@group(0) @binding(1) var<uniform> pu: ParticleU;
|
|
170
175
|
@group(0) @binding(2) var<uniform> cam: CameraU;
|
|
@@ -227,6 +232,7 @@ override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};\n` +
|
|
|
227
232
|
CAMERA_STRUCT +
|
|
228
233
|
PARTICLE_UNIFORMS +
|
|
229
234
|
`
|
|
235
|
+
${src.paramsDecl}
|
|
230
236
|
@group(0) @binding(0) var<storage, read> particles: array<Particle>;
|
|
231
237
|
@group(0) @binding(1) var<uniform> pu: ParticleU;
|
|
232
238
|
@group(0) @binding(2) var<uniform> cam: CameraU;
|
|
@@ -33,6 +33,8 @@ type TrailSource = {
|
|
|
33
33
|
/** For each ribbon, the LOCAL anchor slot it belongs to. Identity when every
|
|
34
34
|
* anchor is trailed; otherwise it skips the untrailed ones. */
|
|
35
35
|
ribbonSlots: number[]
|
|
36
|
+
/** The effect's `#param` block, or empty. */
|
|
37
|
+
paramsDecl: string
|
|
36
38
|
/** Additive, like most glowing ribbons, or straight alpha. */
|
|
37
39
|
blend: "alpha" | "additive"
|
|
38
40
|
bloom: boolean
|
|
@@ -138,6 +140,7 @@ struct TrailU {
|
|
|
138
140
|
weight: f32,
|
|
139
141
|
_pad2: f32,
|
|
140
142
|
}
|
|
143
|
+
${src.paramsDecl}
|
|
141
144
|
@group(0) @binding(0) var<storage, read> _rzCast: array<vec4f>;
|
|
142
145
|
@group(0) @binding(1) var<uniform> tu: TrailU;
|
|
143
146
|
@group(0) @binding(2) var<uniform> cam: CameraU;
|