reze-engine 0.58.0 → 0.58.2
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 +30 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +149 -19
- 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 +16 -19
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +121 -88
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +3 -17
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +4 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +4 -0
- 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 +13 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +3 -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/dist/shaders/scene-light-api.d.ts +42 -0
- package/dist/shaders/scene-light-api.d.ts.map +1 -0
- package/dist/shaders/scene-light-api.js +144 -0
- package/dist/shaders/shadow-api.d.ts +29 -0
- package/dist/shaders/shadow-api.d.ts.map +1 -0
- package/dist/shaders/shadow-api.js +112 -0
- package/package.json +1 -1
- package/src/effect-points.ts +67 -0
- package/src/engine.ts +163 -18
- 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 +121 -88
- package/src/shaders/materials/common.ts +3 -17
- package/src/shaders/passes/composite.ts +4 -0
- package/src/shaders/passes/grid.ts +4 -0
- package/src/shaders/passes/particles.ts +14 -1
- package/src/shaders/passes/trails.ts +3 -1
- package/src/shaders/points-api.ts +66 -0
- package/src/shaders/scene-light-api.ts +148 -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"
|
|
@@ -113,9 +115,13 @@ import {
|
|
|
113
115
|
import {
|
|
114
116
|
buildParticleComputeShader,
|
|
115
117
|
buildParticleRenderShader,
|
|
118
|
+
PARTICLE_LIGHT_BINDING,
|
|
119
|
+
PARTICLE_POINTS_BINDING,
|
|
116
120
|
particleEntryPoints,
|
|
117
121
|
PARTICLE_STRIDE,
|
|
118
122
|
} from "./shaders/passes/particles"
|
|
123
|
+
import { MAX_EFFECT_POINTS, POINTS_FLOATS } from "./shaders/points-api"
|
|
124
|
+
import { bonesWithPrefix, pointsData, writeBonePoint } from "./effect-points"
|
|
119
125
|
import {
|
|
120
126
|
SIM_FORMAT,
|
|
121
127
|
GRID_MAX,
|
|
@@ -1446,6 +1452,10 @@ interface EffectParticles {
|
|
|
1446
1452
|
renderBinds: [GPUBindGroup, GPUBindGroup]
|
|
1447
1453
|
mirrorRenderBinds: [GPUBindGroup, GPUBindGroup]
|
|
1448
1454
|
}
|
|
1455
|
+
/** `#points`: the bone-name prefix and the buffer its matches are written
|
|
1456
|
+
* to each frame. Null when the effect declared none — it then reads the
|
|
1457
|
+
* engine's empty fallback. */
|
|
1458
|
+
points: { prefix: string; buffer: GPUBuffer; data: Float32Array } | null
|
|
1449
1459
|
}
|
|
1450
1460
|
|
|
1451
1461
|
/**
|
|
@@ -2497,9 +2507,20 @@ export class Engine {
|
|
|
2497
2507
|
* Allocated once at full size and zero-filled, so "no lights" is a count of
|
|
2498
2508
|
* zero rather than an absent binding. */
|
|
2499
2509
|
private lightsBuffer!: GPUBuffer
|
|
2500
|
-
|
|
2501
|
-
|
|
2510
|
+
/** What a particle effect with no `#points` reads at its points binding: a
|
|
2511
|
+
* count of zero. One buffer for all of them — nothing ever writes it. */
|
|
2512
|
+
private pointsFallback!: GPUBuffer
|
|
2513
|
+
/** Per model, per prefix, the bones `#points` matched. A rig's bones never
|
|
2514
|
+
* change after load, so the names are walked once rather than every frame. */
|
|
2515
|
+
private pointBones = new WeakMap<Model, Map<string, number[]>>()
|
|
2516
|
+
/** The CPU copy, from construction rather than init: a host sets a scene's
|
|
2517
|
+
* lamps as soon as its state exists, which can be before the device. */
|
|
2518
|
+
private lightsData = new Float32Array(LIGHTS_FLOATS)
|
|
2519
|
+
/** The same bytes as words — the grid's lamp bits are u32, not floats. */
|
|
2520
|
+
private lightsWords = new Uint32Array(this.lightsData.buffer)
|
|
2521
|
+
/** Just the header: the counts and the grid's placement. */
|
|
2502
2522
|
private lightHeader = new Float32Array(LIGHT_HEADER)
|
|
2523
|
+
private lightHeaderWords = new Uint32Array(this.lightHeader.buffer)
|
|
2503
2524
|
/** How many of the slots belong to the DOCUMENT. Effects get what follows. */
|
|
2504
2525
|
private docLightCount = 0
|
|
2505
2526
|
private castData!: Float32Array<ArrayBuffer>
|
|
@@ -4863,6 +4884,7 @@ export class Engine {
|
|
|
4863
4884
|
paramsBuffer?.destroy()
|
|
4864
4885
|
particles?.buffer.destroy()
|
|
4865
4886
|
particles?.uniform.destroy()
|
|
4887
|
+
particles?.points?.buffer.destroy()
|
|
4866
4888
|
grid?.textures[0].destroy()
|
|
4867
4889
|
grid?.textures[1].destroy()
|
|
4868
4890
|
grid?.uniform.destroy()
|
|
@@ -5247,6 +5269,17 @@ export class Engine {
|
|
|
5247
5269
|
cover: particleEntryPoints(wgsl).cover,
|
|
5248
5270
|
}
|
|
5249
5271
|
const prepass = src.blend === "cutout" && src.cover
|
|
5272
|
+
const points = d.points
|
|
5273
|
+
? {
|
|
5274
|
+
prefix: d.points,
|
|
5275
|
+
buffer: this.device.createBuffer({
|
|
5276
|
+
label: `points "${d.points}"`,
|
|
5277
|
+
size: POINTS_FLOATS * 4,
|
|
5278
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
5279
|
+
}),
|
|
5280
|
+
data: pointsData(),
|
|
5281
|
+
}
|
|
5282
|
+
: null
|
|
5250
5283
|
// Sparks want to spawn where a trail is, so the particle stages see the same
|
|
5251
5284
|
// cast buffer the trail draw reads.
|
|
5252
5285
|
const cast = {
|
|
@@ -5273,9 +5306,15 @@ export class Engine {
|
|
|
5273
5306
|
}
|
|
5274
5307
|
|
|
5275
5308
|
const computeModule = await compile(buildParticleComputeShader(src, cast), "particle compute")
|
|
5276
|
-
if (Array.isArray(computeModule))
|
|
5309
|
+
if (Array.isArray(computeModule)) {
|
|
5310
|
+
points?.buffer.destroy()
|
|
5311
|
+
return { ok: false, diagnostics: computeModule }
|
|
5312
|
+
}
|
|
5277
5313
|
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render")
|
|
5278
|
-
if (Array.isArray(renderModule))
|
|
5314
|
+
if (Array.isArray(renderModule)) {
|
|
5315
|
+
points?.buffer.destroy()
|
|
5316
|
+
return { ok: false, diagnostics: renderModule }
|
|
5317
|
+
}
|
|
5279
5318
|
|
|
5280
5319
|
// COPY_DST so a scheduled effect can empty its pool when its window starts.
|
|
5281
5320
|
const buffer = this.device.createBuffer({
|
|
@@ -5300,7 +5339,7 @@ export class Engine {
|
|
|
5300
5339
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
5301
5340
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
5302
5341
|
// due to a previous error".
|
|
5303
|
-
const layoutFor = (storage: GPUBufferBindingType, visibility: number) =>
|
|
5342
|
+
const layoutFor = (storage: GPUBufferBindingType, visibility: number, shadow: boolean) =>
|
|
5304
5343
|
this.device.createBindGroupLayout({
|
|
5305
5344
|
entries: [
|
|
5306
5345
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
@@ -5320,13 +5359,28 @@ export class Engine {
|
|
|
5320
5359
|
// author calls it or not. With no grid it is the 1x1 of zeroes.
|
|
5321
5360
|
{ binding: 8, visibility, texture: { sampleType: "float" as const } },
|
|
5322
5361
|
{ binding: 9, visibility, sampler: { type: "filtering" as const } },
|
|
5362
|
+
// The named points, ALWAYS, for the grid's reason: the accessor is
|
|
5363
|
+
// compiled in whether or not the effect declared #points.
|
|
5364
|
+
{ binding: PARTICLE_POINTS_BINDING, visibility, buffer: { type: "read-only-storage" as const } },
|
|
5365
|
+
// The scene's light, for rzShadow and rzWorldAmbient — the shading
|
|
5366
|
+
// stage only; the compute module compiles the stubs. See
|
|
5367
|
+
// scene-light-api.ts.
|
|
5368
|
+
...(shadow
|
|
5369
|
+
? [
|
|
5370
|
+
{ binding: PARTICLE_LIGHT_BINDING, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" as const } },
|
|
5371
|
+
{ binding: PARTICLE_LIGHT_BINDING + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" as const } },
|
|
5372
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" as const } },
|
|
5373
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" as const } },
|
|
5374
|
+
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" as const } },
|
|
5375
|
+
]
|
|
5376
|
+
: []),
|
|
5323
5377
|
],
|
|
5324
5378
|
})
|
|
5325
5379
|
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
5326
5380
|
* alternates which texture is current, and rebuilding a single group every
|
|
5327
5381
|
* frame is waste for a change that only ever toggles between two known
|
|
5328
5382
|
* states. Both entries are the same view when there is no grid. */
|
|
5329
|
-
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer) =>
|
|
5383
|
+
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer, shadow: boolean) =>
|
|
5330
5384
|
[0, 1].map((parity) =>
|
|
5331
5385
|
this.device.createBindGroup({
|
|
5332
5386
|
layout,
|
|
@@ -5341,12 +5395,22 @@ export class Engine {
|
|
|
5341
5395
|
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
5342
5396
|
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
5343
5397
|
{ binding: 9, resource: this.simSampler },
|
|
5398
|
+
{ binding: PARTICLE_POINTS_BINDING, resource: { buffer: points?.buffer ?? this.pointsFallback } },
|
|
5399
|
+
...(shadow
|
|
5400
|
+
? [
|
|
5401
|
+
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
5402
|
+
{ binding: PARTICLE_LIGHT_BINDING + 1, resource: { buffer: this.shadowLightVPBuffer } },
|
|
5403
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.shadowMapDepthViews[0] },
|
|
5404
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
5405
|
+
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
5406
|
+
]
|
|
5407
|
+
: []),
|
|
5344
5408
|
],
|
|
5345
5409
|
}),
|
|
5346
5410
|
) as [GPUBindGroup, GPUBindGroup]
|
|
5347
5411
|
|
|
5348
|
-
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE)
|
|
5349
|
-
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT)
|
|
5412
|
+
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false)
|
|
5413
|
+
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, true)
|
|
5350
5414
|
|
|
5351
5415
|
// Additive keeps the destination and adds to it, and leaves alpha alone, so
|
|
5352
5416
|
// a glow does not claim coverage it never occluded. The MASK sums with it —
|
|
@@ -5407,6 +5471,7 @@ export class Engine {
|
|
|
5407
5471
|
if (scoped) {
|
|
5408
5472
|
buffer.destroy()
|
|
5409
5473
|
uniform.destroy()
|
|
5474
|
+
points?.buffer.destroy()
|
|
5410
5475
|
return { ok: false, diagnostics: [scoped.message] }
|
|
5411
5476
|
}
|
|
5412
5477
|
return {
|
|
@@ -5422,23 +5487,25 @@ export class Engine {
|
|
|
5422
5487
|
counts: uniformView.uints,
|
|
5423
5488
|
compute,
|
|
5424
5489
|
computeLayout,
|
|
5425
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer),
|
|
5490
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
5426
5491
|
depth,
|
|
5427
5492
|
render,
|
|
5428
5493
|
renderLayout,
|
|
5429
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer),
|
|
5430
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer),
|
|
5494
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5495
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5431
5496
|
rebind: () => ({
|
|
5432
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer),
|
|
5433
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer),
|
|
5434
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer),
|
|
5497
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
5498
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5499
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5435
5500
|
}),
|
|
5501
|
+
points,
|
|
5436
5502
|
},
|
|
5437
5503
|
}
|
|
5438
5504
|
} catch (e) {
|
|
5439
5505
|
await this.device.popErrorScope()
|
|
5440
5506
|
buffer.destroy()
|
|
5441
5507
|
uniform.destroy()
|
|
5508
|
+
points?.buffer.destroy()
|
|
5442
5509
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] }
|
|
5443
5510
|
}
|
|
5444
5511
|
}
|
|
@@ -5569,6 +5636,7 @@ export class Engine {
|
|
|
5569
5636
|
this.device.queue.writeBuffer(e.lights.uniform, 0, e.lights.data.buffer as ArrayBuffer)
|
|
5570
5637
|
}
|
|
5571
5638
|
this.lightHeader[0] = Math.min(next, MAX_LIGHTS)
|
|
5639
|
+
this.lightHeader[1] = this.docLightCount
|
|
5572
5640
|
this.device.queue.writeBuffer(this.lightsBuffer, 0, this.lightHeader)
|
|
5573
5641
|
}
|
|
5574
5642
|
|
|
@@ -5629,6 +5697,7 @@ export class Engine {
|
|
|
5629
5697
|
p.counts[2] = p.count
|
|
5630
5698
|
p.counts[3] = this.particleFrame++
|
|
5631
5699
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer as ArrayBuffer)
|
|
5700
|
+
if (p.points) this.writePoints(p.points)
|
|
5632
5701
|
const cp = encoder.beginComputePass({ label: "particles" })
|
|
5633
5702
|
cp.setPipeline(p.compute)
|
|
5634
5703
|
cp.setBindGroup(0, p.computeBinds[e.grid?.parity ?? 0])
|
|
@@ -5637,6 +5706,35 @@ export class Engine {
|
|
|
5637
5706
|
}
|
|
5638
5707
|
}
|
|
5639
5708
|
|
|
5709
|
+
/**
|
|
5710
|
+
* Every bone matching an effect's `#points` prefix, on every visible model,
|
|
5711
|
+
* posed and placed this frame. Every frame rather than on load: a candle a
|
|
5712
|
+
* character carries moves with the hand, and a stage can be moved.
|
|
5713
|
+
*
|
|
5714
|
+
* The order is the models' order, then rig order — stable while nothing is
|
|
5715
|
+
* added or removed, so particle i stays on point i and a flame does not
|
|
5716
|
+
* jump to another wick between frames.
|
|
5717
|
+
*/
|
|
5718
|
+
private writePoints(points: { prefix: string; buffer: GPUBuffer; data: Float32Array }): void {
|
|
5719
|
+
let n = 0
|
|
5720
|
+
for (const inst of this.modelInstances.values()) {
|
|
5721
|
+
const m = inst.model
|
|
5722
|
+
if (!m.visible) continue
|
|
5723
|
+
let byPrefix = this.pointBones.get(m)
|
|
5724
|
+
if (!byPrefix) this.pointBones.set(m, (byPrefix = new Map()))
|
|
5725
|
+
let bones = byPrefix.get(points.prefix)
|
|
5726
|
+
const rig = m.getSkeleton().bones
|
|
5727
|
+
if (!bones) byPrefix.set(points.prefix, (bones = bonesWithPrefix(rig, points.prefix)))
|
|
5728
|
+
for (const i of bones) {
|
|
5729
|
+
if (n >= MAX_EFFECT_POINTS) break
|
|
5730
|
+
const world = m.getBoneWorldMatrixAt(i)
|
|
5731
|
+
if (world) writeBonePoint(points.data, n++, world, rig[i].tail, m)
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
points.data[0] = n
|
|
5735
|
+
this.device.queue.writeBuffer(points.buffer, 0, points.data.buffer as ArrayBuffer, 0, (4 + n * 8) * 4)
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5640
5738
|
/** Draw the pool. Inside the scene pass, so it is depth-tested and pre-bloom. */
|
|
5641
5739
|
private renderParticles(pass: GPURenderPassEncoder, view: "camera" | "mirror"): void {
|
|
5642
5740
|
for (const e of this.effects) {
|
|
@@ -6029,6 +6127,7 @@ export class Engine {
|
|
|
6029
6127
|
for (const e of this.effects) {
|
|
6030
6128
|
e.particles?.buffer.destroy()
|
|
6031
6129
|
e.particles?.uniform.destroy()
|
|
6130
|
+
e.particles?.points?.buffer.destroy()
|
|
6032
6131
|
e.particles = null
|
|
6033
6132
|
}
|
|
6034
6133
|
}
|
|
@@ -7387,8 +7486,10 @@ export class Engine {
|
|
|
7387
7486
|
},
|
|
7388
7487
|
})
|
|
7389
7488
|
|
|
7489
|
+
// The matrices, then one vec4: the caster sphere, which effects' rzShadow
|
|
7490
|
+
// tests before any tap. The materials and the ground bind the matrices only.
|
|
7390
7491
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
7391
|
-
size: 64 * SHADOW_CASCADES.length,
|
|
7492
|
+
size: 64 * SHADOW_CASCADES.length + 16,
|
|
7392
7493
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
7393
7494
|
})
|
|
7394
7495
|
this.shadowCascadeVPBuffers = SHADOW_CASCADES.map((_, i) =>
|
|
@@ -7464,15 +7565,21 @@ export class Engine {
|
|
|
7464
7565
|
|
|
7465
7566
|
// BEFORE the bind group below, which binds it. Full size from the start:
|
|
7466
7567
|
// every material pipeline binds this, so sizing it to the light count would
|
|
7467
|
-
// mean rebuilding bind groups whenever a scene gained a lamp.
|
|
7468
|
-
//
|
|
7469
|
-
|
|
7568
|
+
// mean rebuilding bind groups whenever a scene gained a lamp. Uploaded from
|
|
7569
|
+
// the CPU copy, which holds any lamps set before the device existed; the
|
|
7570
|
+
// header after it, from the one writer that owns it.
|
|
7470
7571
|
this.lightsBuffer = this.device.createBuffer({
|
|
7471
7572
|
label: "positional lights",
|
|
7472
7573
|
size: this.lightsData.byteLength,
|
|
7473
7574
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
7474
7575
|
})
|
|
7475
7576
|
this.device.queue.writeBuffer(this.lightsBuffer, 0, this.lightsData)
|
|
7577
|
+
this.allocateLightSlots()
|
|
7578
|
+
this.pointsFallback = this.device.createBuffer({
|
|
7579
|
+
label: "no points",
|
|
7580
|
+
size: 16,
|
|
7581
|
+
usage: GPUBufferUsage.STORAGE,
|
|
7582
|
+
})
|
|
7476
7583
|
// BEFORE the per-frame groups, which bind it. The composite needs this 1x1
|
|
7477
7584
|
// stand-in too, and it used to be created with the composite's own
|
|
7478
7585
|
// resources further down — far enough down that a material group built up
|
|
@@ -10023,6 +10130,20 @@ export class Engine {
|
|
|
10023
10130
|
this.lightsData[b + 14] = 0
|
|
10024
10131
|
this.lightsData[b + 15] = 0
|
|
10025
10132
|
}
|
|
10133
|
+
// The grid, from the records as STORED — normalised aim, clamped reach, the
|
|
10134
|
+
// cosines in f32 — so it indexes exactly what the shader will evaluate.
|
|
10135
|
+
const grid = buildLightGrid(
|
|
10136
|
+
list.map((_, i) => {
|
|
10137
|
+
const b = LIGHT_HEADER + i * LIGHT_STRIDE
|
|
10138
|
+
const d = this.lightsData
|
|
10139
|
+
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] }
|
|
10140
|
+
}),
|
|
10141
|
+
)
|
|
10142
|
+
this.lightHeader.set(grid.origin, 4)
|
|
10143
|
+
this.lightHeader[7] = 1 / grid.cell
|
|
10144
|
+
this.lightHeaderWords.set(grid.dims, 8)
|
|
10145
|
+
this.lightHeaderWords.set(grid.outside, 12)
|
|
10146
|
+
this.lightsWords.set(grid.cells, LIGHT_GRID_BASE)
|
|
10026
10147
|
// The CPU copy is written either way; only the upload needs a device. A
|
|
10027
10148
|
// scene whose lamps arrive before init keeps them, and init uploads them.
|
|
10028
10149
|
if (list.length && this.device && this.lightsBuffer) {
|
|
@@ -10033,6 +10154,13 @@ export class Engine {
|
|
|
10033
10154
|
LIGHT_HEADER * 4,
|
|
10034
10155
|
list.length * LIGHT_STRIDE * 4,
|
|
10035
10156
|
)
|
|
10157
|
+
this.device.queue.writeBuffer(
|
|
10158
|
+
this.lightsBuffer,
|
|
10159
|
+
LIGHT_GRID_BASE * 4,
|
|
10160
|
+
this.lightsData.buffer as ArrayBuffer,
|
|
10161
|
+
LIGHT_GRID_BASE * 4,
|
|
10162
|
+
grid.cells.byteLength,
|
|
10163
|
+
)
|
|
10036
10164
|
}
|
|
10037
10165
|
// The effects' bases move when the document's count does; the header —
|
|
10038
10166
|
// count included — is written there.
|
|
@@ -12380,6 +12508,22 @@ export class Engine {
|
|
|
12380
12508
|
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 40, gb.subarray(10, 11) as Float32Array<ArrayBuffer>)
|
|
12381
12509
|
}
|
|
12382
12510
|
|
|
12511
|
+
/** The caster sphere as last written after the cascade matrices; NaN so the
|
|
12512
|
+
* first frame always writes. */
|
|
12513
|
+
private shadowCastersWritten = new Float32Array([NaN, NaN, NaN, NaN])
|
|
12514
|
+
|
|
12515
|
+
/**
|
|
12516
|
+
* The same sphere, into the cascade block after its matrices — rzShadow's
|
|
12517
|
+
* copy. There whether or not a ground exists: a lawn is often the only floor.
|
|
12518
|
+
*/
|
|
12519
|
+
private writeShadowCasters(): void {
|
|
12520
|
+
const w = this.shadowCastersWritten
|
|
12521
|
+
const c = this.casterSphere
|
|
12522
|
+
if (w[0] === c[0] && w[1] === c[1] && w[2] === c[2] && w[3] === c[3]) return
|
|
12523
|
+
w.set(c)
|
|
12524
|
+
this.device.queue.writeBuffer(this.shadowLightVPBuffer, 64 * SHADOW_CASCADES.length, c as Float32Array<ArrayBuffer>)
|
|
12525
|
+
}
|
|
12526
|
+
|
|
12383
12527
|
/**
|
|
12384
12528
|
* Push this frame's caster sphere into the ground's uniform.
|
|
12385
12529
|
*
|
|
@@ -15376,6 +15520,7 @@ export class Engine {
|
|
|
15376
15520
|
if (hasModels) this.dispatchCull(encoder)
|
|
15377
15521
|
// After the cull, which is what recomputes the spheres it unions.
|
|
15378
15522
|
this.writeGroundCasterSphere()
|
|
15523
|
+
this.writeShadowCasters()
|
|
15379
15524
|
this.writeGroundDress()
|
|
15380
15525
|
|
|
15381
15526
|
// After the cull, because a rebuild there can reallocate the argument
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { LIGHT_GRID_CELLS, LIGHT_MASK_WORDS } from "./shaders/lights"
|
|
2
|
+
|
|
3
|
+
// Which document lamps can reach which part of the world, so a fragment walks
|
|
4
|
+
// only those.
|
|
5
|
+
//
|
|
6
|
+
// A uniform grid in WORLD space, built on the CPU whenever the document's lamps
|
|
7
|
+
// change. Each cell holds one bit per lamp — 128 lamps is one vec4u — set when
|
|
8
|
+
// the lamp's sphere, and its cone for a spot, can touch the cell. A fragment
|
|
9
|
+
// finds its cell from its world position and loops the set bits.
|
|
10
|
+
//
|
|
11
|
+
// WORLD SPACE rather than screen tiles, because the answer then belongs to the
|
|
12
|
+
// lamps and not to a camera: the mirror pass, an export at another resolution
|
|
13
|
+
// and a preview all read the same grid with no per-view work, and it is rebuilt
|
|
14
|
+
// only when a lamp moves, never per frame.
|
|
15
|
+
//
|
|
16
|
+
// THE BOX is where the lamps cluster — their positions padded by the median
|
|
17
|
+
// reach — not the union of every sphere. One stage spot reaching 300 units
|
|
18
|
+
// would otherwise stretch the box across the whole set, and a fixed cell budget
|
|
19
|
+
// spread over it leaves cells wider than the small lamps they are meant to
|
|
20
|
+
// separate. A lamp whose reach leaves the box also sets its bit in the OUTSIDE
|
|
21
|
+
// mask, which is what a fragment beyond the box reads, so nothing is lost at
|
|
22
|
+
// the edge; it is only coarser there.
|
|
23
|
+
//
|
|
24
|
+
// CONSERVATIVE, never exact: a bit may be set in a cell the lamp does not
|
|
25
|
+
// light, never missing from one it does. A missing bit is a hole in the light;
|
|
26
|
+
// a spare one is a few instructions.
|
|
27
|
+
|
|
28
|
+
/** One document lamp, as the grid sees it. */
|
|
29
|
+
export type GridLamp = {
|
|
30
|
+
x: number
|
|
31
|
+
y: number
|
|
32
|
+
z: number
|
|
33
|
+
radius: number
|
|
34
|
+
/** Unit aim, away from the lamp. Zero for a point lamp. */
|
|
35
|
+
ax: number
|
|
36
|
+
ay: number
|
|
37
|
+
az: number
|
|
38
|
+
/** Cosine of the half-angle. -1 for a point lamp, which covers everything. */
|
|
39
|
+
cosOuter: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type LightGrid = {
|
|
43
|
+
origin: [number, number, number]
|
|
44
|
+
/** Edge length of one cubic cell, world units. */
|
|
45
|
+
cell: number
|
|
46
|
+
dims: [number, number, number]
|
|
47
|
+
/** The lamps that reach past the box, for fragments outside it. */
|
|
48
|
+
outside: Uint32Array
|
|
49
|
+
/** LIGHT_MASK_WORDS words per cell, x fastest, then y, then z. */
|
|
50
|
+
cells: Uint32Array
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setBit(words: Uint32Array, base: number, lamp: number): void {
|
|
54
|
+
words[base + (lamp >> 5)] |= 1 << (lamp & 31)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildLightGrid(lamps: GridLamp[], maxCells = LIGHT_GRID_CELLS): LightGrid {
|
|
58
|
+
const outside = new Uint32Array(LIGHT_MASK_WORDS)
|
|
59
|
+
const live = lamps
|
|
60
|
+
.map((l, i) => ({ l, i }))
|
|
61
|
+
.filter(({ l }) => l.radius > 0 && [l.x, l.y, l.z, l.radius].every(Number.isFinite))
|
|
62
|
+
if (!live.length) return { origin: [0, 0, 0], cell: 1, dims: [0, 0, 0], outside, cells: new Uint32Array(0) }
|
|
63
|
+
|
|
64
|
+
const radii = live.map(({ l }) => l.radius).sort((a, b) => a - b)
|
|
65
|
+
const pad = radii[radii.length >> 1]
|
|
66
|
+
const min = [Infinity, Infinity, Infinity]
|
|
67
|
+
const max = [-Infinity, -Infinity, -Infinity]
|
|
68
|
+
for (const { l } of live) {
|
|
69
|
+
const p = [l.x, l.y, l.z]
|
|
70
|
+
for (let k = 0; k < 3; k++) {
|
|
71
|
+
min[k] = Math.min(min[k], p[k] - pad)
|
|
72
|
+
max[k] = Math.max(max[k], p[k] + pad)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const extent = [max[0] - min[0], max[1] - min[1], max[2] - min[2]]
|
|
76
|
+
let cell = Math.cbrt((extent[0] * extent[1] * extent[2]) / maxCells)
|
|
77
|
+
let dims: [number, number, number]
|
|
78
|
+
// ceil() rounds every axis up, so the first guess can overshoot the budget by
|
|
79
|
+
// a few percent; widen the cell until it fits.
|
|
80
|
+
for (;;) {
|
|
81
|
+
dims = [0, 1, 2].map((k) => Math.max(1, Math.ceil(extent[k] / cell))) as [number, number, number]
|
|
82
|
+
if (dims[0] * dims[1] * dims[2] <= maxCells) break
|
|
83
|
+
cell *= 1.02
|
|
84
|
+
}
|
|
85
|
+
const cells = new Uint32Array(dims[0] * dims[1] * dims[2] * LIGHT_MASK_WORDS)
|
|
86
|
+
// Half the cell's diagonal: the sphere a spot's cone is tested against.
|
|
87
|
+
const halfDiagonal = (cell * Math.sqrt(3)) / 2
|
|
88
|
+
|
|
89
|
+
for (const { l, i } of live) {
|
|
90
|
+
// A hair of slack on the reach: the shader finds its cell in f32 from a
|
|
91
|
+
// reciprocal, the builder in f64 from a division, and a fragment on a cell
|
|
92
|
+
// face may land in either neighbour.
|
|
93
|
+
const r = l.radius + cell * 1e-4
|
|
94
|
+
const p = [l.x, l.y, l.z]
|
|
95
|
+
const lo = [0, 0, 0]
|
|
96
|
+
const hi = [0, 0, 0]
|
|
97
|
+
let leaves = false
|
|
98
|
+
for (let k = 0; k < 3; k++) {
|
|
99
|
+
const a = Math.floor((p[k] - r - min[k]) / cell)
|
|
100
|
+
const b = Math.floor((p[k] + r - min[k]) / cell)
|
|
101
|
+
if (a < 0 || b >= dims[k]) leaves = true
|
|
102
|
+
lo[k] = Math.max(a, 0)
|
|
103
|
+
hi[k] = Math.min(b, dims[k] - 1)
|
|
104
|
+
}
|
|
105
|
+
if (leaves) setBit(outside, 0, i)
|
|
106
|
+
if (lo[0] > hi[0] || lo[1] > hi[1] || lo[2] > hi[2]) continue
|
|
107
|
+
|
|
108
|
+
const spot = l.cosOuter > -1
|
|
109
|
+
const outer = Math.acos(Math.min(Math.max(l.cosOuter, -1), 1))
|
|
110
|
+
for (let z = lo[2]; z <= hi[2]; z++) {
|
|
111
|
+
for (let y = lo[1]; y <= hi[1]; y++) {
|
|
112
|
+
for (let x = lo[0]; x <= hi[0]; x++) {
|
|
113
|
+
const b0 = [min[0] + x * cell, min[1] + y * cell, min[2] + z * cell]
|
|
114
|
+
// The sphere against the cell's box: the nearest point of the box.
|
|
115
|
+
let d2 = 0
|
|
116
|
+
for (let k = 0; k < 3; k++) {
|
|
117
|
+
const q = Math.min(Math.max(p[k], b0[k]), b0[k] + cell)
|
|
118
|
+
d2 += (q - p[k]) * (q - p[k])
|
|
119
|
+
}
|
|
120
|
+
if (d2 > r * r) continue
|
|
121
|
+
if (spot) {
|
|
122
|
+
// The cone against the cell's bounding sphere: the cell can be lit
|
|
123
|
+
// when the angle to its centre is within the cone's half-angle plus
|
|
124
|
+
// the angle that sphere subtends.
|
|
125
|
+
const v = [b0[0] + cell / 2 - p[0], b0[1] + cell / 2 - p[1], b0[2] + cell / 2 - p[2]]
|
|
126
|
+
const len = Math.hypot(v[0], v[1], v[2])
|
|
127
|
+
if (len > halfDiagonal) {
|
|
128
|
+
const limit = outer + Math.asin(halfDiagonal / len)
|
|
129
|
+
const cos = (v[0] * l.ax + v[1] * l.ay + v[2] * l.az) / len
|
|
130
|
+
if (limit < Math.PI && Math.acos(Math.min(Math.max(cos, -1), 1)) > limit) continue
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
setBit(cells, ((z * dims[1] + y) * dims[0] + x) * LIGHT_MASK_WORDS, i)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { origin: [min[0], min[1], min[2]], cell, dims, outside, cells }
|
|
139
|
+
}
|
package/src/model.ts
CHANGED
|
@@ -140,6 +140,11 @@ export interface Bone {
|
|
|
140
140
|
appendMove?: boolean
|
|
141
141
|
/** 軸制限: this bone rotates ONLY about this axis (the twist bones use it). */
|
|
142
142
|
fixedAxis?: [number, number, number]
|
|
143
|
+
/** Where the bone's tail sits at rest, as an offset from its head — the
|
|
144
|
+
* direction a PMX editor draws it pointing. PMX states it either as that
|
|
145
|
+
* offset or as another bone to point at; both arrive here as the offset.
|
|
146
|
+
* An effect reading named points takes it as the point's length and way. */
|
|
147
|
+
tail?: [number, number, number]
|
|
143
148
|
/** 変形階層: MMD poses whole layers in order, not bones in array order.
|
|
144
149
|
* Parsed but not yet honoured — distinct from Model's `deformOrder`, which is
|
|
145
150
|
* this engine's parent-before-child traversal. */
|
|
@@ -1096,6 +1101,13 @@ export class Model {
|
|
|
1096
1101
|
return this.runtimeSkeleton.worldMatrices[idx].values
|
|
1097
1102
|
}
|
|
1098
1103
|
|
|
1104
|
+
/** A bone's posed matrix by INDEX — model space, column-major, the live
|
|
1105
|
+
* array. For walking bones that share a name, which the name lookup
|
|
1106
|
+
* collapses to one. Null past the end. */
|
|
1107
|
+
getBoneWorldMatrixAt(index: number): Float32Array | null {
|
|
1108
|
+
return this.runtimeSkeleton.worldMatrices[index]?.values ?? null
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1099
1111
|
// World bone origin (world matrix col3); unknown name → null
|
|
1100
1112
|
getBoneWorldPosition(boneName: string): Vec3 | null {
|
|
1101
1113
|
const idx = this.runtimeSkeleton.nameIndex[boneName]
|
package/src/pmx-loader.ts
CHANGED
|
@@ -357,6 +357,8 @@ export class PmxLoader {
|
|
|
357
357
|
ikLinks?: IKLink[]
|
|
358
358
|
fixedAxis?: [number, number, number]
|
|
359
359
|
deformLayer?: number
|
|
360
|
+
tailOffset?: [number, number, number]
|
|
361
|
+
tailBone?: number
|
|
360
362
|
}
|
|
361
363
|
const abs: AbsBone[] = new Array(count)
|
|
362
364
|
// PMX 2.x bone flags (best-effort common masks)
|
|
@@ -378,14 +380,14 @@ export class PmxLoader {
|
|
|
378
380
|
const deformLayer = this.getInt32() // 変形階層
|
|
379
381
|
const flags = this.getUint16()
|
|
380
382
|
|
|
381
|
-
// Tail: bone index or offset vector3
|
|
383
|
+
// Tail: bone index or offset vector3. Kept either way — the bone it
|
|
384
|
+
// names is resolved to an offset once every head is known.
|
|
385
|
+
let tailOffset: [number, number, number] | undefined
|
|
386
|
+
let tailBone: number | undefined
|
|
382
387
|
if ((flags & FLAG_TAIL_IS_BONE) !== 0) {
|
|
383
|
-
this.getNonVertexIndex(this.boneIndexSize)
|
|
388
|
+
tailBone = this.getNonVertexIndex(this.boneIndexSize)
|
|
384
389
|
} else {
|
|
385
|
-
|
|
386
|
-
this.getFloat32()
|
|
387
|
-
this.getFloat32()
|
|
388
|
-
this.getFloat32()
|
|
390
|
+
tailOffset = [this.getFloat32(), this.getFloat32(), this.getFloat32()]
|
|
389
391
|
}
|
|
390
392
|
|
|
391
393
|
// Append transform (inherit/ratio)
|
|
@@ -475,10 +477,14 @@ export class PmxLoader {
|
|
|
475
477
|
ikIteration,
|
|
476
478
|
ikLimitAngle,
|
|
477
479
|
ikLinks,
|
|
480
|
+
tailOffset,
|
|
481
|
+
tailBone,
|
|
478
482
|
}
|
|
479
483
|
}
|
|
480
484
|
for (let i = 0; i < count; i++) {
|
|
481
485
|
const a = abs[i]
|
|
486
|
+
const to = a.tailBone !== undefined && a.tailBone >= 0 && a.tailBone < count ? abs[a.tailBone] : null
|
|
487
|
+
const tail: [number, number, number] | undefined = to ? [to.x - a.x, to.y - a.y, to.z - a.z] : a.tailOffset
|
|
482
488
|
const boneData: Bone = {
|
|
483
489
|
name: a.name,
|
|
484
490
|
parentIndex: a.parent,
|
|
@@ -497,6 +503,7 @@ export class PmxLoader {
|
|
|
497
503
|
ikIteration: a.ikIteration,
|
|
498
504
|
ikLimitAngle: a.ikLimitAngle,
|
|
499
505
|
ikLinks: a.ikLinks,
|
|
506
|
+
...(tail ? { tail } : {}),
|
|
500
507
|
}
|
|
501
508
|
bones.push(boneData)
|
|
502
509
|
}
|
|
@@ -23,8 +23,10 @@ export const EFFECT_SUBJECT_VEC4S = 4
|
|
|
23
23
|
*
|
|
24
24
|
* 8 was reachable, and quietly: two effects each wanting two hands, two feet and
|
|
25
25
|
* a head is ten, so the second one silently lost its ribbons to a diagnostic
|
|
26
|
-
* nobody was reading. 16
|
|
27
|
-
*
|
|
26
|
+
* nobody was reading. 16 held until an effect traced a HAND: an outline through
|
|
27
|
+
* every finger reads fifteen bones a hand, thirty for the pair, before any other
|
|
28
|
+
* effect in the scene asks for one. 48 is that and room beside it, for 393KB of
|
|
29
|
+
* storage buffer where 16 cost 131KB.
|
|
28
30
|
*
|
|
29
31
|
* The cost really is only that. The per-frame upload is bounded by the last
|
|
30
32
|
* TRAILED slot, not by this cap (see the writeBuffer in updateCastBuffer), so a
|
|
@@ -39,7 +41,7 @@ export const EFFECT_SUBJECT_VEC4S = 4
|
|
|
39
41
|
* scene's anchor count grows, which is a different feature from raising a number
|
|
40
42
|
* that was never load-bearing.
|
|
41
43
|
*/
|
|
42
|
-
export const EFFECT_ANCHORS =
|
|
44
|
+
export const EFFECT_ANCHORS = 48
|
|
43
45
|
/**
|
|
44
46
|
* Path samples kept per trailed anchor — 128 at the 60Hz sampling rate is a
|
|
45
47
|
* ~2.1 second ribbon.
|
|
@@ -57,6 +57,9 @@ export type EffectDirectives = {
|
|
|
57
57
|
particles: number
|
|
58
58
|
lights: number
|
|
59
59
|
grid: number
|
|
60
|
+
/** `#points <prefix>`: every bone on every model whose name starts with it,
|
|
61
|
+
* as rzPoint(i) in the particle stages. Null when the file declares none. */
|
|
62
|
+
points: string | null
|
|
60
63
|
bloom: boolean
|
|
61
64
|
/** This effect takes the cast apart — the host reads the timing. */
|
|
62
65
|
dissolve: boolean
|
|
@@ -111,6 +114,7 @@ const SPEC = {
|
|
|
111
114
|
particles: 1,
|
|
112
115
|
lights: 1,
|
|
113
116
|
grid: 1,
|
|
117
|
+
points: 1,
|
|
114
118
|
bloom: 0,
|
|
115
119
|
dissolve: 0,
|
|
116
120
|
mirror: 0,
|
|
@@ -168,6 +172,7 @@ export function parseDirectives(wgsl: string): DirectiveResult {
|
|
|
168
172
|
particles: 0,
|
|
169
173
|
lights: 0,
|
|
170
174
|
grid: 0,
|
|
175
|
+
points: null,
|
|
171
176
|
bloom: false,
|
|
172
177
|
dissolve: false,
|
|
173
178
|
ground: null,
|
|
@@ -285,6 +290,9 @@ export function parseDirectives(wgsl: string): DirectiveResult {
|
|
|
285
290
|
case "dissolve":
|
|
286
291
|
d.dissolve = true
|
|
287
292
|
return
|
|
293
|
+
case "points":
|
|
294
|
+
d.points = args[0]
|
|
295
|
+
return
|
|
288
296
|
case "mirror":
|
|
289
297
|
d.mirror = true
|
|
290
298
|
return
|