reze-engine 0.58.1 → 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 +22 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +99 -8
- 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 +2 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +6 -0
- 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 +111 -7
- 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 +6 -0
- 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,12 @@ import {
|
|
|
114
116
|
buildParticleComputeShader,
|
|
115
117
|
buildParticleRenderShader,
|
|
116
118
|
PARTICLE_LIGHT_BINDING,
|
|
119
|
+
PARTICLE_POINTS_BINDING,
|
|
117
120
|
particleEntryPoints,
|
|
118
121
|
PARTICLE_STRIDE,
|
|
119
122
|
} from "./shaders/passes/particles"
|
|
123
|
+
import { MAX_EFFECT_POINTS, POINTS_FLOATS } from "./shaders/points-api"
|
|
124
|
+
import { bonesWithPrefix, pointsData, writeBonePoint } from "./effect-points"
|
|
120
125
|
import {
|
|
121
126
|
SIM_FORMAT,
|
|
122
127
|
GRID_MAX,
|
|
@@ -1447,6 +1452,10 @@ interface EffectParticles {
|
|
|
1447
1452
|
renderBinds: [GPUBindGroup, GPUBindGroup]
|
|
1448
1453
|
mirrorRenderBinds: [GPUBindGroup, GPUBindGroup]
|
|
1449
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
|
|
1450
1459
|
}
|
|
1451
1460
|
|
|
1452
1461
|
/**
|
|
@@ -2498,9 +2507,20 @@ export class Engine {
|
|
|
2498
2507
|
* Allocated once at full size and zero-filled, so "no lights" is a count of
|
|
2499
2508
|
* zero rather than an absent binding. */
|
|
2500
2509
|
private lightsBuffer!: GPUBuffer
|
|
2501
|
-
|
|
2502
|
-
|
|
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. */
|
|
2503
2522
|
private lightHeader = new Float32Array(LIGHT_HEADER)
|
|
2523
|
+
private lightHeaderWords = new Uint32Array(this.lightHeader.buffer)
|
|
2504
2524
|
/** How many of the slots belong to the DOCUMENT. Effects get what follows. */
|
|
2505
2525
|
private docLightCount = 0
|
|
2506
2526
|
private castData!: Float32Array<ArrayBuffer>
|
|
@@ -4864,6 +4884,7 @@ export class Engine {
|
|
|
4864
4884
|
paramsBuffer?.destroy()
|
|
4865
4885
|
particles?.buffer.destroy()
|
|
4866
4886
|
particles?.uniform.destroy()
|
|
4887
|
+
particles?.points?.buffer.destroy()
|
|
4867
4888
|
grid?.textures[0].destroy()
|
|
4868
4889
|
grid?.textures[1].destroy()
|
|
4869
4890
|
grid?.uniform.destroy()
|
|
@@ -5248,6 +5269,17 @@ export class Engine {
|
|
|
5248
5269
|
cover: particleEntryPoints(wgsl).cover,
|
|
5249
5270
|
}
|
|
5250
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
|
|
5251
5283
|
// Sparks want to spawn where a trail is, so the particle stages see the same
|
|
5252
5284
|
// cast buffer the trail draw reads.
|
|
5253
5285
|
const cast = {
|
|
@@ -5274,9 +5306,15 @@ export class Engine {
|
|
|
5274
5306
|
}
|
|
5275
5307
|
|
|
5276
5308
|
const computeModule = await compile(buildParticleComputeShader(src, cast), "particle compute")
|
|
5277
|
-
if (Array.isArray(computeModule))
|
|
5309
|
+
if (Array.isArray(computeModule)) {
|
|
5310
|
+
points?.buffer.destroy()
|
|
5311
|
+
return { ok: false, diagnostics: computeModule }
|
|
5312
|
+
}
|
|
5278
5313
|
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render")
|
|
5279
|
-
if (Array.isArray(renderModule))
|
|
5314
|
+
if (Array.isArray(renderModule)) {
|
|
5315
|
+
points?.buffer.destroy()
|
|
5316
|
+
return { ok: false, diagnostics: renderModule }
|
|
5317
|
+
}
|
|
5280
5318
|
|
|
5281
5319
|
// COPY_DST so a scheduled effect can empty its pool when its window starts.
|
|
5282
5320
|
const buffer = this.device.createBuffer({
|
|
@@ -5321,6 +5359,9 @@ export class Engine {
|
|
|
5321
5359
|
// author calls it or not. With no grid it is the 1x1 of zeroes.
|
|
5322
5360
|
{ binding: 8, visibility, texture: { sampleType: "float" as const } },
|
|
5323
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 } },
|
|
5324
5365
|
// The scene's light, for rzShadow and rzWorldAmbient — the shading
|
|
5325
5366
|
// stage only; the compute module compiles the stubs. See
|
|
5326
5367
|
// scene-light-api.ts.
|
|
@@ -5354,6 +5395,7 @@ export class Engine {
|
|
|
5354
5395
|
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
5355
5396
|
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
5356
5397
|
{ binding: 9, resource: this.simSampler },
|
|
5398
|
+
{ binding: PARTICLE_POINTS_BINDING, resource: { buffer: points?.buffer ?? this.pointsFallback } },
|
|
5357
5399
|
...(shadow
|
|
5358
5400
|
? [
|
|
5359
5401
|
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
@@ -5429,6 +5471,7 @@ export class Engine {
|
|
|
5429
5471
|
if (scoped) {
|
|
5430
5472
|
buffer.destroy()
|
|
5431
5473
|
uniform.destroy()
|
|
5474
|
+
points?.buffer.destroy()
|
|
5432
5475
|
return { ok: false, diagnostics: [scoped.message] }
|
|
5433
5476
|
}
|
|
5434
5477
|
return {
|
|
@@ -5455,12 +5498,14 @@ export class Engine {
|
|
|
5455
5498
|
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5456
5499
|
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5457
5500
|
}),
|
|
5501
|
+
points,
|
|
5458
5502
|
},
|
|
5459
5503
|
}
|
|
5460
5504
|
} catch (e) {
|
|
5461
5505
|
await this.device.popErrorScope()
|
|
5462
5506
|
buffer.destroy()
|
|
5463
5507
|
uniform.destroy()
|
|
5508
|
+
points?.buffer.destroy()
|
|
5464
5509
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] }
|
|
5465
5510
|
}
|
|
5466
5511
|
}
|
|
@@ -5591,6 +5636,7 @@ export class Engine {
|
|
|
5591
5636
|
this.device.queue.writeBuffer(e.lights.uniform, 0, e.lights.data.buffer as ArrayBuffer)
|
|
5592
5637
|
}
|
|
5593
5638
|
this.lightHeader[0] = Math.min(next, MAX_LIGHTS)
|
|
5639
|
+
this.lightHeader[1] = this.docLightCount
|
|
5594
5640
|
this.device.queue.writeBuffer(this.lightsBuffer, 0, this.lightHeader)
|
|
5595
5641
|
}
|
|
5596
5642
|
|
|
@@ -5651,6 +5697,7 @@ export class Engine {
|
|
|
5651
5697
|
p.counts[2] = p.count
|
|
5652
5698
|
p.counts[3] = this.particleFrame++
|
|
5653
5699
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer as ArrayBuffer)
|
|
5700
|
+
if (p.points) this.writePoints(p.points)
|
|
5654
5701
|
const cp = encoder.beginComputePass({ label: "particles" })
|
|
5655
5702
|
cp.setPipeline(p.compute)
|
|
5656
5703
|
cp.setBindGroup(0, p.computeBinds[e.grid?.parity ?? 0])
|
|
@@ -5659,6 +5706,35 @@ export class Engine {
|
|
|
5659
5706
|
}
|
|
5660
5707
|
}
|
|
5661
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
|
+
|
|
5662
5738
|
/** Draw the pool. Inside the scene pass, so it is depth-tested and pre-bloom. */
|
|
5663
5739
|
private renderParticles(pass: GPURenderPassEncoder, view: "camera" | "mirror"): void {
|
|
5664
5740
|
for (const e of this.effects) {
|
|
@@ -6051,6 +6127,7 @@ export class Engine {
|
|
|
6051
6127
|
for (const e of this.effects) {
|
|
6052
6128
|
e.particles?.buffer.destroy()
|
|
6053
6129
|
e.particles?.uniform.destroy()
|
|
6130
|
+
e.particles?.points?.buffer.destroy()
|
|
6054
6131
|
e.particles = null
|
|
6055
6132
|
}
|
|
6056
6133
|
}
|
|
@@ -7488,15 +7565,21 @@ export class Engine {
|
|
|
7488
7565
|
|
|
7489
7566
|
// BEFORE the bind group below, which binds it. Full size from the start:
|
|
7490
7567
|
// 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
|
-
|
|
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.
|
|
7494
7571
|
this.lightsBuffer = this.device.createBuffer({
|
|
7495
7572
|
label: "positional lights",
|
|
7496
7573
|
size: this.lightsData.byteLength,
|
|
7497
7574
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
7498
7575
|
})
|
|
7499
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
|
+
})
|
|
7500
7583
|
// BEFORE the per-frame groups, which bind it. The composite needs this 1x1
|
|
7501
7584
|
// stand-in too, and it used to be created with the composite's own
|
|
7502
7585
|
// resources further down — far enough down that a material group built up
|
|
@@ -10047,6 +10130,20 @@ export class Engine {
|
|
|
10047
10130
|
this.lightsData[b + 14] = 0
|
|
10048
10131
|
this.lightsData[b + 15] = 0
|
|
10049
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)
|
|
10050
10147
|
// The CPU copy is written either way; only the upload needs a device. A
|
|
10051
10148
|
// scene whose lamps arrive before init keeps them, and init uploads them.
|
|
10052
10149
|
if (list.length && this.device && this.lightsBuffer) {
|
|
@@ -10057,6 +10154,13 @@ export class Engine {
|
|
|
10057
10154
|
LIGHT_HEADER * 4,
|
|
10058
10155
|
list.length * LIGHT_STRIDE * 4,
|
|
10059
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
|
+
)
|
|
10060
10164
|
}
|
|
10061
10165
|
// The effects' bases move when the document's count does; the header —
|
|
10062
10166
|
// count included — is written there.
|
|
@@ -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
|