reze-engine 0.58.1 → 0.58.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/effect-points.d.ts +30 -0
- package/dist/effect-points.d.ts.map +1 -0
- package/dist/effect-points.js +49 -0
- package/dist/engine.d.ts +25 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +199 -35
- package/dist/graph/style-group.d.ts +7 -0
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/light-grid.d.ts +25 -0
- package/dist/light-grid.d.ts.map +1 -0
- package/dist/light-grid.js +91 -0
- package/dist/model.d.ts +9 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +11 -6
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-layout.d.ts +5 -3
- package/dist/shaders/cast-layout.d.ts.map +1 -1
- package/dist/shaders/cast-layout.js +5 -3
- package/dist/shaders/directives.d.ts +3 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +5 -0
- package/dist/shaders/lights.d.ts +14 -9
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +118 -65
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +2 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +2 -0
- package/dist/shaders/passes/particles.d.ts +20 -1
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +36 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +2 -1
- package/dist/shaders/points-api.d.ts +11 -0
- package/dist/shaders/points-api.d.ts.map +1 -0
- package/dist/shaders/points-api.js +58 -0
- package/package.json +1 -1
- package/src/effect-points.ts +67 -0
- package/src/engine.ts +215 -35
- package/src/graph/style-group.ts +12 -1
- package/src/light-grid.ts +139 -0
- package/src/model.ts +12 -0
- package/src/pmx-loader.ts +13 -6
- package/src/shaders/cast-layout.ts +5 -3
- package/src/shaders/directives.ts +8 -0
- package/src/shaders/lights.ts +118 -65
- package/src/shaders/passes/composite.ts +2 -0
- package/src/shaders/passes/grid.ts +2 -0
- package/src/shaders/passes/particles.ts +46 -2
- package/src/shaders/passes/trails.ts +2 -1
- package/src/shaders/points-api.ts +66 -0
|
@@ -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
|
package/src/shaders/lights.ts
CHANGED
|
@@ -11,18 +11,27 @@ import { subjectMaskApi } from "./cast-api"
|
|
|
11
11
|
// renderer that bolted a second key light onto a toon shader has shipped. A
|
|
12
12
|
// light here brightens; it does not restate the shading.
|
|
13
13
|
//
|
|
14
|
-
// So there are no per-light shadows
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// So there are no per-light shadows and no area lights. A fragment walks the
|
|
15
|
+
// DOCUMENT's lamps through a world-space grid (see light-grid.ts): its cell
|
|
16
|
+
// names the lamps that can reach it, so a stage rig of fifty costs each pixel
|
|
17
|
+
// the handful standing near it. The lamps an EFFECT emits are placed by a
|
|
18
|
+
// compute pass every frame, which the grid never sees, and are walked in full.
|
|
17
19
|
//
|
|
18
|
-
// LAYOUT
|
|
19
|
-
//
|
|
20
|
+
// LAYOUT, in 32-bit words, read by the shader as vec4u. A 16-word header:
|
|
21
|
+
//
|
|
22
|
+
// [0] light count (f32) [1] how many are the document's (f32)
|
|
23
|
+
// [4..6] grid origin (f32) [7] 1 / cell edge (f32)
|
|
24
|
+
// [8..10] grid dims (u32) [12..15] the OUTSIDE mask (u32 bits)
|
|
25
|
+
//
|
|
26
|
+
// then MAX_LIGHTS records of 16 floats — four vec4s:
|
|
20
27
|
//
|
|
21
28
|
// [0..2] position, world space [3] radius
|
|
22
29
|
// [4..6] colour PREMULTIPLIED by intensity [7] type
|
|
23
30
|
// [8..10] aim, unit, pointing away from the light [11] cos of the outer angle
|
|
24
31
|
// [12] cos of the inner angle [13..15] spare
|
|
25
32
|
//
|
|
33
|
+
// then the grid: LIGHT_MASK_WORDS words of lamp bits per cell.
|
|
34
|
+
//
|
|
26
35
|
// Colour carries intensity because nothing reads them apart: every use is the
|
|
27
36
|
// product, and storing two numbers that are only ever multiplied is two numbers
|
|
28
37
|
// that can disagree.
|
|
@@ -39,10 +48,11 @@ import { lyricsApi } from "./lyrics-api"
|
|
|
39
48
|
import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api"
|
|
40
49
|
import { idApi } from "./id-api"
|
|
41
50
|
import { sceneLightApi, worldAmbientWgsl } from "./scene-light-api"
|
|
51
|
+
import { pointsApi } from "./points-api"
|
|
42
52
|
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
45
|
-
export const LIGHT_HEADER =
|
|
53
|
+
/** Words before the first record: the counts, the grid's placement and the
|
|
54
|
+
* outside mask, four vec4s — see the layout above. */
|
|
55
|
+
export const LIGHT_HEADER = 16
|
|
46
56
|
/** Floats per light — see the layout above. */
|
|
47
57
|
export const LIGHT_STRIDE = 16
|
|
48
58
|
/**
|
|
@@ -56,15 +66,20 @@ export const LIGHT_STRIDE = 16
|
|
|
56
66
|
* interior brings a hundred or more, and the ceilings of sixteen and then
|
|
57
67
|
* forty-eight each cut a real scene in half. Past this the extras are dropped.
|
|
58
68
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* size affordable at all; clustering is what replaces the linear walk when a
|
|
63
|
-
* scene wants every one of them close enough to matter.
|
|
69
|
+
* A fragment pays for the document lamps its grid cell names, so the cost
|
|
70
|
+
* follows how many reach a place rather than how many the scene holds. It is
|
|
71
|
+
* also why the cap is 128: one vec4u of bits per cell.
|
|
64
72
|
*/
|
|
65
73
|
export const MAX_LIGHTS = 128
|
|
66
|
-
/**
|
|
67
|
-
export const
|
|
74
|
+
/** Words of lamp bits per grid cell — one bit per lamp the cap allows. */
|
|
75
|
+
export const LIGHT_MASK_WORDS = MAX_LIGHTS / 32
|
|
76
|
+
/** The grid's cell budget. 32k cells over a stage a couple of hundred units
|
|
77
|
+
* across is a cell of about four — finer than the lamps it separates. */
|
|
78
|
+
export const LIGHT_GRID_CELLS = 32768
|
|
79
|
+
/** Where the grid starts, in words. */
|
|
80
|
+
export const LIGHT_GRID_BASE = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE
|
|
81
|
+
/** Words in the whole buffer: header, records, grid. */
|
|
82
|
+
export const LIGHTS_FLOATS = LIGHT_GRID_BASE + LIGHT_GRID_CELLS * LIGHT_MASK_WORDS
|
|
68
83
|
|
|
69
84
|
/**
|
|
70
85
|
* The RzLight struct, declared in EVERY module a user's source is spliced into.
|
|
@@ -164,7 +179,7 @@ ${viewportApi("viewU[6].w")}
|
|
|
164
179
|
${trailSlotsApi(cast.trailCount)}
|
|
165
180
|
// The id accessors, stubbed: this module cannot read an attachment the
|
|
166
181
|
// scene pass writes. See id-api.ts — the author's whole file compiles here.
|
|
167
|
-
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0)}
|
|
182
|
+
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) + pointsApi(false)}
|
|
168
183
|
// The dials the author declared, if any. A lamp is exactly the thing someone
|
|
169
184
|
// retunes — its colour and its reach — so an emitter reads params like every
|
|
170
185
|
// other mount rather than being the one place a #param resolves to nothing.
|
|
@@ -229,51 +244,55 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
229
244
|
|
|
230
245
|
/** The rz*Light accessors, with the buffer declared at the given binding. */
|
|
231
246
|
export function lightsApi(group: number, binding: number): string {
|
|
247
|
+
const R = LIGHT_HEADER / 4
|
|
248
|
+
const S = LIGHT_STRIDE / 4
|
|
232
249
|
return /* wgsl */ `
|
|
233
|
-
|
|
250
|
+
// vec4u, so a record is four loads and a cell's lamp bits are one. Floats come
|
|
251
|
+
// out through bitcast; the bits must NOT pass through f32 on the way, where a
|
|
252
|
+
// mask that happens to spell a NaN is not guaranteed to survive a load.
|
|
253
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzLights: array<vec4u>;
|
|
234
254
|
|
|
235
255
|
const RZ_MAX_LIGHTS: u32 = ${MAX_LIGHTS}u;
|
|
236
256
|
|
|
237
257
|
/** How many positional lights the scene has. Zero is the ordinary case. */
|
|
238
|
-
fn rzLightCount() -> u32 { return min(u32(_rzLights[0]), RZ_MAX_LIGHTS); }
|
|
258
|
+
fn rzLightCount() -> u32 { return min(u32(bitcast<f32>(_rzLights[0].x)), RZ_MAX_LIGHTS); }
|
|
259
|
+
|
|
260
|
+
/** How many of them the document placed — the ones the grid indexes. */
|
|
261
|
+
fn _rzLightDocCount() -> u32 { return min(u32(bitcast<f32>(_rzLights[0].y)), rzLightCount()); }
|
|
262
|
+
|
|
263
|
+
/** One vec4 of light i's record. */
|
|
264
|
+
fn _rzLightVec(i: u32, k: u32) -> vec4f { return bitcast<vec4f>(_rzLights[${R}u + i * ${S}u + k]); }
|
|
239
265
|
|
|
240
266
|
/** Light i's world position. */
|
|
241
|
-
fn rzLightPos(i: u32) -> vec3f {
|
|
242
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u;
|
|
243
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
244
|
-
}
|
|
267
|
+
fn rzLightPos(i: u32) -> vec3f { return _rzLightVec(i, 0u).xyz; }
|
|
245
268
|
|
|
246
269
|
/** How far light i reaches. Its falloff is zero AT this distance, not merely
|
|
247
|
-
* small,
|
|
248
|
-
fn rzLightRadius(i: u32) -> f32 { return
|
|
270
|
+
* small, which is the bound the grid is built from. */
|
|
271
|
+
fn rzLightRadius(i: u32) -> f32 { return _rzLightVec(i, 0u).w; }
|
|
249
272
|
|
|
250
273
|
/** Light i's colour, already multiplied by its intensity. */
|
|
251
|
-
fn rzLightColor(i: u32) -> vec3f {
|
|
252
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 4u;
|
|
253
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
254
|
-
}
|
|
274
|
+
fn rzLightColor(i: u32) -> vec3f { return _rzLightVec(i, 1u).xyz; }
|
|
255
275
|
|
|
256
276
|
/** Where light i points, away from itself. The zero vector for a point light. */
|
|
257
|
-
fn rzLightAim(i: u32) -> vec3f {
|
|
258
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 8u;
|
|
259
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
260
|
-
}
|
|
277
|
+
fn rzLightAim(i: u32) -> vec3f { return _rzLightVec(i, 2u).xyz; }
|
|
261
278
|
|
|
262
279
|
/** The cosines a spot fades between: x its outer edge, y its inner one. A point
|
|
263
280
|
* light stores (-1, -1), which saturates the cone term to 1. */
|
|
264
|
-
fn rzLightCone(i: u32) -> vec2f {
|
|
265
|
-
|
|
266
|
-
|
|
281
|
+
fn rzLightCone(i: u32) -> vec2f { return vec2f(_rzLightVec(i, 2u).w, _rzLightVec(i, 3u).x); }
|
|
282
|
+
|
|
283
|
+
/** The document lamps that can reach p: its grid cell's bits, or the outside
|
|
284
|
+
* mask beyond the grid. Written so a NaN position fails the inside test. */
|
|
285
|
+
fn _rzLightCellMask(p: vec3f) -> vec4u {
|
|
286
|
+
let g = bitcast<vec4f>(_rzLights[1]);
|
|
287
|
+
let dims = _rzLights[2].xyz;
|
|
288
|
+
let c = floor((p - g.xyz) * g.w);
|
|
289
|
+
if (!(all(c >= vec3f(0.0)) && all(c < vec3f(dims)))) { return _rzLights[3]; }
|
|
290
|
+
let ci = vec3u(c);
|
|
291
|
+
return _rzLights[${LIGHT_GRID_BASE / 4}u + (ci.z * dims.y + ci.y) * dims.x + ci.x];
|
|
267
292
|
}
|
|
268
293
|
|
|
269
294
|
/**
|
|
270
|
-
*
|
|
271
|
-
* finished colour. Multiply by whatever the surface's albedo is.
|
|
272
|
-
*
|
|
273
|
-
* WITH NO LIGHTS THIS RETURNS EXACTLY ZERO and the loop never runs, so a scene
|
|
274
|
-
* that declares none is arithmetically identical to one compiled before lights
|
|
275
|
-
* existed. That is the property the whole feature is gated on: adding this to
|
|
276
|
-
* every material must cost nothing until someone asks for a light.
|
|
295
|
+
* One light's contribution at a surface point.
|
|
277
296
|
*
|
|
278
297
|
* FALLOFF IS RELATIVE TO THE RADIUS, and deliberately not physical.
|
|
279
298
|
*
|
|
@@ -287,33 +306,67 @@ fn rzLightCone(i: u32) -> vec2f {
|
|
|
287
306
|
* zero, and the curve between them is the same shape whatever the scene's
|
|
288
307
|
* scale. Both dials now mean what they say, which for a composer beats being
|
|
289
308
|
* right about photons. (1 - t²)² — smooth at both ends, exactly 0 at the
|
|
290
|
-
* radius, so the bound
|
|
309
|
+
* radius, so the bound the grid is built from is real.
|
|
310
|
+
*/
|
|
311
|
+
fn _rzLightOne(i: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
312
|
+
let pr = _rzLightVec(i, 0u);
|
|
313
|
+
let d = pr.xyz - p;
|
|
314
|
+
let dist = length(d);
|
|
315
|
+
// Out of reach before anything else is computed. The grid is conservative —
|
|
316
|
+
// a cell's bit means the lamp CAN reach part of it — so this still runs.
|
|
317
|
+
if (dist >= pr.w) { return vec3f(0.0); }
|
|
318
|
+
let toLight = d / max(dist, 1e-4);
|
|
319
|
+
// Facing the light, and nothing behind it. No wrap or half-lambert: this
|
|
320
|
+
// layer adds light, and a wrapped term would lift the shadow side, which is
|
|
321
|
+
// the ramp's business and not this one's.
|
|
322
|
+
let ndl = max(dot(n, toLight), 0.0);
|
|
323
|
+
if (ndl <= 0.0) { return vec3f(0.0); }
|
|
324
|
+
let t = clamp(dist / max(pr.w, 1e-4), 0.0, 1.0);
|
|
325
|
+
let falloff = 1.0 - t * t;
|
|
326
|
+
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
327
|
+
// the outer one, squared for the same soft edge the falloff has. A point
|
|
328
|
+
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
329
|
+
// dot product and no branch.
|
|
330
|
+
let cone = rzLightCone(i);
|
|
331
|
+
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
332
|
+
return rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The lamps named by one word of a cell's bits, lowest first. */
|
|
336
|
+
fn _rzLightWord(bits0: u32, base: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
337
|
+
var acc = vec3f(0.0);
|
|
338
|
+
var bits = bits0;
|
|
339
|
+
loop {
|
|
340
|
+
if (bits == 0u) { break; }
|
|
341
|
+
let i = base + firstTrailingBit(bits);
|
|
342
|
+
bits = bits & (bits - 1u);
|
|
343
|
+
acc = acc + _rzLightOne(i, p, n);
|
|
344
|
+
}
|
|
345
|
+
return acc;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Every positional light's contribution at a surface point, as light — not as a
|
|
350
|
+
* finished colour. Multiply by whatever the surface's albedo is.
|
|
351
|
+
*
|
|
352
|
+
* WITH NO LIGHTS THIS RETURNS EXACTLY ZERO and neither walk runs, so a scene
|
|
353
|
+
* that declares none is arithmetically identical to one compiled before lights
|
|
354
|
+
* existed. That is the property the whole feature is gated on: adding this to
|
|
355
|
+
* every material must cost nothing until someone asks for a light.
|
|
291
356
|
*/
|
|
292
357
|
fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
|
|
293
358
|
var acc = vec3f(0.0);
|
|
294
359
|
let count = rzLightCount();
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// the ramp's business and not this one's.
|
|
306
|
-
let ndl = max(dot(n, toLight), 0.0);
|
|
307
|
-
if (ndl <= 0.0) { continue; }
|
|
308
|
-
let t = clamp(dist / max(rzLightRadius(i), 1e-4), 0.0, 1.0);
|
|
309
|
-
let falloff = 1.0 - t * t;
|
|
310
|
-
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
311
|
-
// the outer one, squared for the same soft edge the falloff has. A point
|
|
312
|
-
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
313
|
-
// dot product and no branch.
|
|
314
|
-
let cone = rzLightCone(i);
|
|
315
|
-
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
316
|
-
acc = acc + rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
360
|
+
let docs = _rzLightDocCount();
|
|
361
|
+
// The document's lamps: only those the grid says can reach this cell.
|
|
362
|
+
if (docs > 0u) {
|
|
363
|
+
let m = _rzLightCellMask(p);
|
|
364
|
+
acc = acc + _rzLightWord(m.x, 0u, p, n) + _rzLightWord(m.y, 32u, p, n) +
|
|
365
|
+
_rzLightWord(m.z, 64u, p, n) + _rzLightWord(m.w, 96u, p, n);
|
|
366
|
+
}
|
|
367
|
+
// The effects' lamps, which move every frame on the GPU: all of them.
|
|
368
|
+
for (var i = docs; i < count; i = i + 1u) {
|
|
369
|
+
acc = acc + _rzLightOne(i, p, n);
|
|
317
370
|
}
|
|
318
371
|
return acc;
|
|
319
372
|
}
|
|
@@ -7,6 +7,7 @@ import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLE
|
|
|
7
7
|
import { audioApi } from "../audio-api"
|
|
8
8
|
import { idApi } from "../id-api"
|
|
9
9
|
import { sceneLightApi } from "../scene-light-api"
|
|
10
|
+
import { pointsApi } from "../points-api"
|
|
10
11
|
import { castDistanceApi, CAST_FIELD_DIV } from "./cast-distance"
|
|
11
12
|
import { lyricsApi, lyricsTextApi } from "../lyrics-api"
|
|
12
13
|
import { midiApi } from "../midi-api"
|
|
@@ -724,6 +725,7 @@ export function buildFieldShader(effect: CompositeEffectSource): string {
|
|
|
724
725
|
trailSlotsApi(effect.trailCount) +
|
|
725
726
|
idApi(effect.ids === true, 0, 23) +
|
|
726
727
|
sceneLightApi(false, 0, 0) +
|
|
728
|
+
pointsApi(false) +
|
|
727
729
|
// Distance to the cast, in SCREEN pixels: the field is half-res, so a field
|
|
728
730
|
// texel is CAST_FIELD_DIV of them and the accessor scales on the way out.
|
|
729
731
|
// An author writes the width they mean and never learns how it is built.
|
|
@@ -9,6 +9,7 @@ import { type CastLayout } from "./particles"
|
|
|
9
9
|
import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
10
10
|
import { idApi } from "../id-api"
|
|
11
11
|
import { sceneLightApi } from "../scene-light-api"
|
|
12
|
+
import { pointsApi } from "../points-api"
|
|
12
13
|
|
|
13
14
|
// A persistent grid an effect can step and read: the one thing an effect could
|
|
14
15
|
// not have before, which is MEMORY.
|
|
@@ -170,6 +171,7 @@ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
|
170
171
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
171
172
|
idApi(false, 0, 0) +
|
|
172
173
|
sceneLightApi(false, 0, 0) +
|
|
174
|
+
pointsApi(false) +
|
|
173
175
|
// Inlined rather than imported from cast-distance, and NOT for tidiness.
|
|
174
176
|
//
|
|
175
177
|
// This module and composite.ts import each other — grid takes
|