reze-engine 0.42.3 → 0.50.0
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/README.md +40 -410
- package/dist/camera.d.ts +3 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +33 -8
- package/dist/engine.d.ts +924 -29
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4043 -278
- package/dist/graph/registry.d.ts +2 -2
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +1 -1
- package/dist/graph/slots.d.ts +0 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +37 -9
- package/dist/hdr.d.ts +18 -0
- package/dist/hdr.d.ts.map +1 -0
- package/dist/hdr.js +162 -0
- package/dist/ibl.d.ts +19 -0
- package/dist/ibl.d.ts.map +1 -0
- package/dist/ibl.js +113 -0
- package/dist/ik-solver.d.ts +2 -1
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/math.d.ts +20 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +23 -16
- package/dist/midi-loader.d.ts +10 -0
- package/dist/midi-loader.d.ts.map +1 -0
- package/dist/midi-loader.js +247 -0
- package/dist/model.d.ts +2 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +29 -0
- package/dist/param-track.d.ts +48 -0
- package/dist/param-track.d.ts.map +1 -0
- package/dist/param-track.js +80 -0
- package/dist/physics/types.d.ts.map +1 -1
- package/dist/physics/types.js +3 -0
- package/dist/reflection.d.ts +27 -0
- package/dist/reflection.d.ts.map +1 -0
- package/dist/reflection.js +93 -0
- package/dist/shaders/anchor-table.d.ts +56 -0
- package/dist/shaders/anchor-table.d.ts.map +1 -0
- package/dist/shaders/anchor-table.js +128 -0
- package/dist/shaders/audio-api.d.ts +3 -0
- package/dist/shaders/audio-api.d.ts.map +1 -0
- package/dist/shaders/audio-api.js +81 -0
- package/dist/shaders/cast-api.d.ts +2 -0
- package/dist/shaders/cast-api.d.ts.map +1 -0
- package/dist/shaders/cast-api.js +121 -0
- package/dist/shaders/cast-layout.d.ts +21 -0
- package/dist/shaders/cast-layout.d.ts.map +1 -0
- package/dist/shaders/cast-layout.js +20 -0
- package/dist/shaders/lights.d.ts +79 -0
- package/dist/shaders/lights.d.ts.map +1 -0
- package/dist/shaders/lights.js +269 -0
- package/dist/shaders/lyrics-api.d.ts +39 -0
- package/dist/shaders/lyrics-api.d.ts.map +1 -0
- package/dist/shaders/lyrics-api.js +187 -0
- package/dist/shaders/materials/common.d.ts +2 -4
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +87 -35
- package/dist/shaders/midi-api.d.ts +10 -0
- package/dist/shaders/midi-api.d.ts.map +1 -0
- package/dist/shaders/midi-api.js +114 -0
- package/dist/shaders/passes/composite.d.ts +31 -15
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +225 -135
- package/dist/shaders/passes/cull.d.ts +2 -0
- package/dist/shaders/passes/cull.d.ts.map +1 -0
- package/dist/shaders/passes/cull.js +138 -0
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/grid.d.ts +31 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -0
- package/dist/shaders/passes/grid.js +169 -0
- package/dist/shaders/passes/ground.d.ts +13 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +170 -25
- package/dist/shaders/passes/hosted-api.d.ts +57 -0
- package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
- package/dist/shaders/passes/hosted-api.js +166 -0
- package/dist/shaders/passes/id-debug.d.ts +28 -0
- package/dist/shaders/passes/id-debug.d.ts.map +1 -0
- package/dist/shaders/passes/id-debug.js +74 -0
- package/dist/shaders/passes/particles.d.ts +66 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -0
- package/dist/shaders/passes/particles.js +279 -0
- package/dist/shaders/passes/scene-contract.d.ts +128 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
- package/dist/shaders/passes/scene-contract.js +207 -0
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/passes/trails.d.ts +59 -0
- package/dist/shaders/passes/trails.d.ts.map +1 -0
- package/dist/shaders/passes/trails.js +340 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/dist/shadow-cascades.d.ts +45 -0
- package/dist/shadow-cascades.d.ts.map +1 -0
- package/dist/shadow-cascades.js +70 -0
- package/dist/vmd-loader.d.ts +3 -2
- package/dist/vmd-loader.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/camera.ts +31 -8
- package/src/engine.ts +4535 -296
- package/src/graph/registry.ts +2 -2
- package/src/graph/slots.ts +37 -9
- package/src/hdr.ts +156 -0
- package/src/ibl.ts +115 -0
- package/src/ik-solver.ts +1 -1
- package/src/index.ts +12 -0
- package/src/math.ts +23 -17
- package/src/midi-loader.ts +246 -0
- package/src/model.ts +31 -3
- package/src/param-track.ts +83 -0
- package/src/physics/types.ts +4 -1
- package/src/reflection.ts +94 -0
- package/src/shaders/anchor-table.ts +147 -0
- package/src/shaders/audio-api.ts +82 -0
- package/src/shaders/cast-api.ts +123 -0
- package/src/shaders/cast-layout.ts +20 -0
- package/src/shaders/lights.ts +280 -0
- package/src/shaders/lyrics-api.ts +202 -0
- package/src/shaders/materials/common.ts +89 -35
- package/src/shaders/midi-api.ts +116 -0
- package/src/shaders/passes/composite.ts +244 -136
- package/src/shaders/passes/cull.ts +139 -0
- package/src/shaders/passes/grid.ts +178 -0
- package/src/shaders/passes/ground.ts +172 -25
- package/src/shaders/passes/hosted-api.ts +171 -0
- package/src/shaders/passes/id-debug.ts +75 -0
- package/src/shaders/passes/particles.ts +340 -0
- package/src/shaders/passes/scene-contract.ts +266 -0
- package/src/shaders/passes/trails.ts +390 -0
- package/src/shadow-cascades.ts +97 -0
- package/src/vmd-loader.ts +2 -2
package/src/graph/registry.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { SocketValue } from "./schema"
|
|
|
8
8
|
|
|
9
9
|
export type SockT = "float" | "color" | "vector" | "vec4"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
type InputSpec = {
|
|
12
12
|
type: SockT
|
|
13
13
|
default?: SocketValue
|
|
14
14
|
/** Socket is meaningless as a literal (e.g. the color being processed) — must be linked. */
|
|
@@ -32,7 +32,7 @@ export type NodeSpec = {
|
|
|
32
32
|
// Deterministic: same graph JSON → byte-identical WGSL. String(x) is JS shortest
|
|
33
33
|
// round-trip, so full-precision Blender constants (0.15000000596046448) survive.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function fmtFloat(x: number): string {
|
|
36
36
|
if (!Number.isFinite(x)) throw new Error(`non-finite literal: ${x}`)
|
|
37
37
|
const s = String(x)
|
|
38
38
|
return /[.e]/.test(s) ? s : s + ".0"
|
package/src/graph/slots.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// See docs/style-groups-spec.md §5, §7.
|
|
9
9
|
|
|
10
10
|
import { NODES_WGSL } from "../shaders/materials/nodes"
|
|
11
|
-
import { COMMON_MATERIAL_PRELUDE_WGSL } from "../shaders/materials/common"
|
|
11
|
+
import { COMMON_MATERIAL_PRELUDE_WGSL, commonFsOutWgsl } from "../shaders/materials/common"
|
|
12
|
+
import { sceneIdWriteWgsl } from "../shaders/passes/scene-contract"
|
|
12
13
|
import type { AlphaMode, RenderClass } from "./render-class"
|
|
13
14
|
|
|
14
15
|
// ── Module-scope declarations ──
|
|
@@ -58,7 +59,7 @@ const EYE_REAR_GATE = `
|
|
|
58
59
|
|
|
59
60
|
// ── Adjust-tier uniforms — fixed 16-vec4f block (256 B) shared by the single material
|
|
60
61
|
// bind group layout; non-graph draws bind a zero buffer. ──
|
|
61
|
-
|
|
62
|
+
const STYLE_UNIFORMS_WGSL = `struct StyleUniforms { p: array<vec4f, 16> };
|
|
62
63
|
@group(2) @binding(4) var<uniform> style: StyleUniforms;
|
|
63
64
|
|
|
64
65
|
`
|
|
@@ -96,7 +97,9 @@ ${discard}
|
|
|
96
97
|
${gate}
|
|
97
98
|
let l = -light.lights[0].direction.xyz;
|
|
98
99
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
99
|
-
|
|
100
|
+
// The world: flat colour, or the HDRI's irradiance at this normal — which
|
|
101
|
+
// is what makes a loaded sky actually light her instead of only backing her.
|
|
102
|
+
let amb = rzWorldAmbient(n);
|
|
100
103
|
let shadow = sampleShadow(input.worldPos, n);
|
|
101
104
|
let tex_color = tex_s.rgb;
|
|
102
105
|
|
|
@@ -108,20 +111,41 @@ ${gate}
|
|
|
108
111
|
// pass when IS_OVER_EYES is compiled true.
|
|
109
112
|
function epilogue(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
110
113
|
const alphaBase = alphaMode === "hashed" ? "1.0" : "alpha"
|
|
114
|
+
// Empty while ids are off, so the epilogue is exactly what it was. The values
|
|
115
|
+
// ride in the per-draw material uniform (see MaterialUniforms), which is what
|
|
116
|
+
// keeps this working through the indirect-draw path.
|
|
117
|
+
const ID_WRITE = sceneIdWriteWgsl("out", "u32(material.materialId)", "u32(material.objectId)")
|
|
118
|
+
// The positional lights, ADDED to whatever the graph decided and tinted by
|
|
119
|
+
// the surface's own texture colour — a red lamp on a blue dress must not wash
|
|
120
|
+
// it toward white. Deliberately outside the graph: this layer does not
|
|
121
|
+
// re-ramp, because two ramped terminators crossing a cheek read as plastic.
|
|
122
|
+
//
|
|
123
|
+
// With no lights rzLightsDiffuse returns exactly zero and its loop never
|
|
124
|
+
// runs, so every scene that declares none renders bit-for-bit as it did
|
|
125
|
+
// before lights existed. That is the property this whole feature is gated on.
|
|
126
|
+
// Modulated by `albedo`, a named local that TODAY is exactly tex_color — the
|
|
127
|
+
// raw texture. That is an approximation with a known failure: a graph that
|
|
128
|
+
// recolours a surface is lit by a colour it no longer shows. The local exists
|
|
129
|
+
// so the fix has an address — when graphs gain an optional albedo output
|
|
130
|
+
// (material-track era), it lands here and every light is corrected at once,
|
|
131
|
+
// instead of a hunt through the epilogue's string templates.
|
|
132
|
+
const LIT = ` + rzLightsDiffuse(input.worldPos, n) * albedo`
|
|
133
|
+
const ALBEDO = ` let albedo = tex_color;
|
|
134
|
+
`
|
|
111
135
|
if (renderClass === "hair") {
|
|
112
|
-
return
|
|
136
|
+
return `${ALBEDO} var outAlpha = ${alphaBase};
|
|
113
137
|
if (IS_OVER_EYES) { outAlpha = ${alphaBase} * 0.25; }
|
|
114
138
|
|
|
115
139
|
var out: FSOut;
|
|
116
|
-
out.color = vec4f(final_color, outAlpha);
|
|
140
|
+
out.color = vec4f(final_color${LIT}, outAlpha);
|
|
117
141
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
118
|
-
return out;
|
|
142
|
+
${ID_WRITE} return out;
|
|
119
143
|
`
|
|
120
144
|
}
|
|
121
|
-
return
|
|
122
|
-
out.color = vec4f(final_color, ${alphaBase});
|
|
145
|
+
return `${ALBEDO} var out: FSOut;
|
|
146
|
+
out.color = vec4f(final_color${LIT}, ${alphaBase});
|
|
123
147
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
124
|
-
return out;
|
|
148
|
+
${ID_WRITE} return out;
|
|
125
149
|
`
|
|
126
150
|
}
|
|
127
151
|
|
|
@@ -139,6 +163,10 @@ export function assembleModule(
|
|
|
139
163
|
return (
|
|
140
164
|
NODES_WGSL +
|
|
141
165
|
COMMON_MATERIAL_PRELUDE_WGSL +
|
|
166
|
+
// Exactly where it sat inside the prelude constant. It moved out because
|
|
167
|
+
// the id attachment is probed at init and this module is assembled per
|
|
168
|
+
// compile, which is late enough to know the answer.
|
|
169
|
+
commonFsOutWgsl() +
|
|
142
170
|
(includeStyleUniforms ? STYLE_UNIFORMS_WGSL : "") +
|
|
143
171
|
decls(renderClass, alphaMode) +
|
|
144
172
|
prelude(renderClass, alphaMode) +
|
package/src/hdr.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Radiance .hdr (RGBE) — the HDRI format Blender worlds ship in.
|
|
2
|
+
//
|
|
3
|
+
// Pure and dependency-free like everything else here: the whole format is a
|
|
4
|
+
// text header, a resolution line, and scanlines of shared-exponent RGBE
|
|
5
|
+
// pixels, so a parser is a hundred lines and a WASM decoder would be absurd.
|
|
6
|
+
// The output is scene-linear RGB — the space the view transform expects —
|
|
7
|
+
// which is the entire point: an 8-bit sky is display-space wallpaper, this is
|
|
8
|
+
// radiance the film can actually roll off.
|
|
9
|
+
|
|
10
|
+
export type HdrImage = {
|
|
11
|
+
width: number
|
|
12
|
+
height: number
|
|
13
|
+
/** RGBA, scene-linear, alpha 1 — padded for direct rgba16float upload. */
|
|
14
|
+
data: Float32Array
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a Radiance .hdr file. Throws with a reason on anything malformed —
|
|
19
|
+
* the caller shows it as an upload error, not a blank sky.
|
|
20
|
+
*/
|
|
21
|
+
export function parseHDR(buffer: ArrayBuffer): HdrImage {
|
|
22
|
+
const bytes = new Uint8Array(buffer)
|
|
23
|
+
let pos = 0
|
|
24
|
+
const readLine = (): string => {
|
|
25
|
+
let end = pos
|
|
26
|
+
while (end < bytes.length && bytes[end] !== 0x0a) end++
|
|
27
|
+
const line = String.fromCharCode(...bytes.subarray(pos, end))
|
|
28
|
+
pos = end + 1
|
|
29
|
+
return line
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const magic = readLine()
|
|
33
|
+
if (!magic.startsWith("#?RADIANCE") && !magic.startsWith("#?RGBE")) {
|
|
34
|
+
throw new Error("not a Radiance .hdr file (missing #?RADIANCE header)")
|
|
35
|
+
}
|
|
36
|
+
let formatOk = false
|
|
37
|
+
for (;;) {
|
|
38
|
+
const line = readLine()
|
|
39
|
+
if (line === "") break
|
|
40
|
+
if (line.startsWith("FORMAT=")) {
|
|
41
|
+
if (line !== "FORMAT=32-bit_rle_rgbe") throw new Error(`unsupported .hdr format: ${line.slice(7)}`)
|
|
42
|
+
formatOk = true
|
|
43
|
+
}
|
|
44
|
+
if (pos >= bytes.length) throw new Error("truncated .hdr header")
|
|
45
|
+
}
|
|
46
|
+
if (!formatOk) throw new Error(".hdr header has no FORMAT line")
|
|
47
|
+
|
|
48
|
+
// Only the universal orientation. Rotated/flipped variants exist in theory;
|
|
49
|
+
// no tool in this pipeline writes them, and silently mis-orienting a sky is
|
|
50
|
+
// worse than refusing one.
|
|
51
|
+
const res = readLine()
|
|
52
|
+
const m = /^-Y (\d+) \+X (\d+)$/.exec(res)
|
|
53
|
+
if (!m) throw new Error(`unsupported .hdr orientation: "${res}"`)
|
|
54
|
+
const height = parseInt(m[1], 10)
|
|
55
|
+
const width = parseInt(m[2], 10)
|
|
56
|
+
|
|
57
|
+
const data = new Float32Array(width * height * 4)
|
|
58
|
+
const rgbe = new Uint8Array(width * 4)
|
|
59
|
+
|
|
60
|
+
for (let y = 0; y < height; y++) {
|
|
61
|
+
if (pos + 4 > bytes.length) throw new Error(`truncated at scanline ${y}`)
|
|
62
|
+
const isNewRle = bytes[pos] === 2 && bytes[pos + 1] === 2 && ((bytes[pos + 2] << 8) | bytes[pos + 3]) === width
|
|
63
|
+
if (isNewRle && width >= 8 && width < 32768) {
|
|
64
|
+
pos += 4
|
|
65
|
+
// Four planes, each RLE'd independently: count>128 is a run, else literals.
|
|
66
|
+
for (let c = 0; c < 4; c++) {
|
|
67
|
+
let x = 0
|
|
68
|
+
while (x < width) {
|
|
69
|
+
if (pos >= bytes.length) throw new Error(`truncated RLE at scanline ${y}`)
|
|
70
|
+
let count = bytes[pos++]
|
|
71
|
+
if (count > 128) {
|
|
72
|
+
count -= 128
|
|
73
|
+
const v = bytes[pos++]
|
|
74
|
+
if (x + count > width) throw new Error(`RLE run overflows scanline ${y}`)
|
|
75
|
+
for (let i = 0; i < count; i++) rgbe[(x + i) * 4 + c] = v
|
|
76
|
+
} else {
|
|
77
|
+
if (x + count > width) throw new Error(`RLE literals overflow scanline ${y}`)
|
|
78
|
+
for (let i = 0; i < count; i++) rgbe[(x + i) * 4 + c] = bytes[pos++]
|
|
79
|
+
}
|
|
80
|
+
x += count
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
// Flat scanline (small widths, or files that never bothered with RLE).
|
|
85
|
+
// The old-style (1,1,1,n) repeat marker is part of this path.
|
|
86
|
+
let x = 0
|
|
87
|
+
while (x < width) {
|
|
88
|
+
if (pos + 4 > bytes.length) throw new Error(`truncated at scanline ${y}`)
|
|
89
|
+
const r = bytes[pos]
|
|
90
|
+
const g = bytes[pos + 1]
|
|
91
|
+
const b = bytes[pos + 2]
|
|
92
|
+
const e = bytes[pos + 3]
|
|
93
|
+
pos += 4
|
|
94
|
+
if (r === 1 && g === 1 && b === 1 && x > 0) {
|
|
95
|
+
// Old-style RLE: repeat the previous pixel e times (shift ignored —
|
|
96
|
+
// scanlines this parser accepts are under 2^15 wide).
|
|
97
|
+
const px = (x - 1) * 4
|
|
98
|
+
for (let i = 0; i < e && x < width; i++, x++) {
|
|
99
|
+
rgbe[x * 4] = rgbe[px]
|
|
100
|
+
rgbe[x * 4 + 1] = rgbe[px + 1]
|
|
101
|
+
rgbe[x * 4 + 2] = rgbe[px + 2]
|
|
102
|
+
rgbe[x * 4 + 3] = rgbe[px + 3]
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
rgbe[x * 4] = r
|
|
106
|
+
rgbe[x * 4 + 1] = g
|
|
107
|
+
rgbe[x * 4 + 2] = b
|
|
108
|
+
rgbe[x * 4 + 3] = e
|
|
109
|
+
x++
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// RGBE → linear: a shared exponent over three 8-bit mantissas.
|
|
114
|
+
const row = y * width * 4
|
|
115
|
+
for (let x = 0; x < width; x++) {
|
|
116
|
+
const e = rgbe[x * 4 + 3]
|
|
117
|
+
const scale = e === 0 ? 0 : Math.pow(2, e - 136) // 2^(e-128) / 256
|
|
118
|
+
data[row + x * 4] = rgbe[x * 4] * scale
|
|
119
|
+
data[row + x * 4 + 1] = rgbe[x * 4 + 1] * scale
|
|
120
|
+
data[row + x * 4 + 2] = rgbe[x * 4 + 2] * scale
|
|
121
|
+
data[row + x * 4 + 3] = 1
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { width, height, data }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Pack float32 to IEEE half, the format rgba16float uploads want. Clamps to
|
|
129
|
+
* the half range rather than producing Inf — a 70000-nit sun texel should
|
|
130
|
+
* read as "the brightest representable", not poison the filter.
|
|
131
|
+
*/
|
|
132
|
+
export function packHalf(src: Float32Array): Uint16Array<ArrayBuffer> {
|
|
133
|
+
const out = new Uint16Array(new ArrayBuffer(src.length * 2))
|
|
134
|
+
const f32 = new Float32Array(1)
|
|
135
|
+
const u32 = new Uint32Array(f32.buffer)
|
|
136
|
+
for (let i = 0; i < src.length; i++) {
|
|
137
|
+
f32[0] = src[i]
|
|
138
|
+
const x = u32[0]
|
|
139
|
+
const sign = (x >>> 16) & 0x8000
|
|
140
|
+
let exp = (x >>> 23) & 0xff
|
|
141
|
+
const frac = x & 0x7fffff
|
|
142
|
+
let h: number
|
|
143
|
+
if (exp === 0xff) {
|
|
144
|
+
h = sign | 0x7bff // NaN/Inf → max finite, by the clamp policy above
|
|
145
|
+
} else if (exp > 142) {
|
|
146
|
+
h = sign | 0x7bff // > 65504 → max finite
|
|
147
|
+
} else if (exp < 113) {
|
|
148
|
+
// Subnormal or zero in half precision.
|
|
149
|
+
h = exp < 103 ? sign : sign | ((frac | 0x800000) >> (126 - exp))
|
|
150
|
+
} else {
|
|
151
|
+
h = sign | ((exp - 112) << 10) | (frac >> 13)
|
|
152
|
+
}
|
|
153
|
+
out[i] = h
|
|
154
|
+
}
|
|
155
|
+
return out
|
|
156
|
+
}
|
package/src/ibl.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Diffuse irradiance from an HDRI, as 9 spherical-harmonic coefficients — the
|
|
2
|
+
// Ramamoorthi–Hanrahan formulation every engine uses, because integrating the
|
|
3
|
+
// whole sky per fragment is absurd and an SH2 fit of irradiance is within 1%
|
|
4
|
+
// for natural skies.
|
|
5
|
+
//
|
|
6
|
+
// Pure and headlessly testable (the shadow-cascades precedent). The output is
|
|
7
|
+
// PRE-FOLDED: all basis and convolution constants are baked into the nine RGB
|
|
8
|
+
// vectors here, so the shader is a plain polynomial in the normal — and the
|
|
9
|
+
// whole thing is normalised so a uniform sky of radiance 1 lights every
|
|
10
|
+
// surface with exactly 1, which is what makes an HDRI world a drop-in for the
|
|
11
|
+
// flat world colour it replaces.
|
|
12
|
+
//
|
|
13
|
+
// Direction convention matches the composite's equirect sampling exactly
|
|
14
|
+
// (LH, +Z forward: u = 0.5 + atan2(x, z)/2π, v = 0.5 − asin(y)/π), because a
|
|
15
|
+
// sky that lights the character from anywhere but where it is drawn would be
|
|
16
|
+
// worse than no IBL at all.
|
|
17
|
+
|
|
18
|
+
import type { HdrImage } from "./hdr"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Project an equirect HDRI to folded irradiance SH.
|
|
22
|
+
*
|
|
23
|
+
* Returns 9 RGB triplets (27 floats): [A, By, Bz, Bx, Cxy, Cyz, Cz2, Cxz, Cx2y2]
|
|
24
|
+
* for E(n) = A + By·y + Bz·z + Bx·x + Cxy·xy + Cyz·yz + Cz2·(3z²−1) + Cxz·xz + Cx2y2·(x²−y²).
|
|
25
|
+
*
|
|
26
|
+
* `stride` subsamples the image — irradiance is the lowest-frequency thing an
|
|
27
|
+
* HDRI carries, so every 4th texel of a 2K is already overkill.
|
|
28
|
+
*/
|
|
29
|
+
export function projectIrradianceSH(img: HdrImage, stride = 4): Float32Array {
|
|
30
|
+
const { width: w, height: h, data } = img
|
|
31
|
+
// Accumulated radiance moments L_lm, RGB each.
|
|
32
|
+
const L = new Float64Array(9 * 3)
|
|
33
|
+
const dPhi = (2 * Math.PI) / w
|
|
34
|
+
const dTheta = Math.PI / h
|
|
35
|
+
for (let y = 0; y < h; y += stride) {
|
|
36
|
+
const v = (y + 0.5) / h
|
|
37
|
+
const ny = Math.cos(Math.PI * v)
|
|
38
|
+
const r = Math.sin(Math.PI * v)
|
|
39
|
+
// Solid angle of one texel row-slice, times the stride² it stands in for.
|
|
40
|
+
const dw = r * dPhi * dTheta * stride * stride
|
|
41
|
+
if (dw <= 0) continue
|
|
42
|
+
for (let x = 0; x < w; x += stride) {
|
|
43
|
+
const u = (x + 0.5) / w
|
|
44
|
+
const phi = (u - 0.5) * 2 * Math.PI
|
|
45
|
+
const nx = r * Math.sin(phi)
|
|
46
|
+
const nz = r * Math.cos(phi)
|
|
47
|
+
const i = (y * w + x) * 4
|
|
48
|
+
// Real SH basis, evaluated once and weighted by the solid angle.
|
|
49
|
+
const b0 = 0.282095 * dw
|
|
50
|
+
const b1 = 0.488603 * ny * dw
|
|
51
|
+
const b2 = 0.488603 * nz * dw
|
|
52
|
+
const b3 = 0.488603 * nx * dw
|
|
53
|
+
const b4 = 1.092548 * nx * ny * dw
|
|
54
|
+
const b5 = 1.092548 * ny * nz * dw
|
|
55
|
+
const b6 = 0.315392 * (3 * nz * nz - 1) * dw
|
|
56
|
+
const b7 = 1.092548 * nx * nz * dw
|
|
57
|
+
const b8 = 0.546274 * (nx * nx - ny * ny) * dw
|
|
58
|
+
for (let c = 0; c < 3; c++) {
|
|
59
|
+
const rad = data[i + c]
|
|
60
|
+
L[0 * 3 + c] += rad * b0
|
|
61
|
+
L[1 * 3 + c] += rad * b1
|
|
62
|
+
L[2 * 3 + c] += rad * b2
|
|
63
|
+
L[3 * 3 + c] += rad * b3
|
|
64
|
+
L[4 * 3 + c] += rad * b4
|
|
65
|
+
L[5 * 3 + c] += rad * b5
|
|
66
|
+
L[6 * 3 + c] += rad * b6
|
|
67
|
+
L[7 * 3 + c] += rad * b7
|
|
68
|
+
L[8 * 3 + c] += rad * b8
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Irradiance convolution (Ramamoorthi's c1..c5), then /π for the Lambert
|
|
73
|
+
// convention — this is the normalisation that makes uniform-1 → exactly 1.
|
|
74
|
+
const c1 = 0.429043
|
|
75
|
+
const c2 = 0.511664
|
|
76
|
+
const c3 = 0.743125
|
|
77
|
+
const c4 = 0.886227
|
|
78
|
+
const c5 = 0.247708
|
|
79
|
+
const out = new Float32Array(27)
|
|
80
|
+
for (let c = 0; c < 3; c++) {
|
|
81
|
+
const l00 = L[0 * 3 + c]
|
|
82
|
+
const l1m1 = L[1 * 3 + c]
|
|
83
|
+
const l10 = L[2 * 3 + c]
|
|
84
|
+
const l11 = L[3 * 3 + c]
|
|
85
|
+
const l2m2 = L[4 * 3 + c]
|
|
86
|
+
const l2m1 = L[5 * 3 + c]
|
|
87
|
+
const l20 = L[6 * 3 + c]
|
|
88
|
+
const l21 = L[7 * 3 + c]
|
|
89
|
+
const l22 = L[8 * 3 + c]
|
|
90
|
+
// c3·L20·z² is refolded onto the (3z²−1) basis: the z²-free remainder
|
|
91
|
+
// joins the constant term, so the shader's polynomial needs no z² row.
|
|
92
|
+
out[0 * 3 + c] = (c4 * l00 - c5 * l20 + (c3 / 3) * l20) / Math.PI // A
|
|
93
|
+
out[1 * 3 + c] = (2 * c2 * l1m1) / Math.PI // By
|
|
94
|
+
out[2 * 3 + c] = (2 * c2 * l10) / Math.PI // Bz
|
|
95
|
+
out[3 * 3 + c] = (2 * c2 * l11) / Math.PI // Bx
|
|
96
|
+
out[4 * 3 + c] = (2 * c1 * l2m2) / Math.PI // Cxy
|
|
97
|
+
out[5 * 3 + c] = (2 * c1 * l2m1) / Math.PI // Cyz
|
|
98
|
+
out[6 * 3 + c] = ((c3 / 3) * l20) / Math.PI // Cz2 · (3z²−1)
|
|
99
|
+
out[7 * 3 + c] = (2 * c1 * l21) / Math.PI // Cxz
|
|
100
|
+
out[8 * 3 + c] = (2 * c1 * l22) / Math.PI // Cx2y2
|
|
101
|
+
}
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Evaluate the folded coefficients at a normal — the shader's polynomial, in
|
|
106
|
+
* JS, so tests and any future CPU consumer share one definition. */
|
|
107
|
+
export function evalIrradianceSH(sh: Float32Array, n: { x: number; y: number; z: number }): [number, number, number] {
|
|
108
|
+
const { x, y, z } = n
|
|
109
|
+
const basis = [1, y, z, x, x * y, y * z, 3 * z * z - 1, x * z, x * x - y * y]
|
|
110
|
+
const out: [number, number, number] = [0, 0, 0]
|
|
111
|
+
for (let b = 0; b < 9; b++) {
|
|
112
|
+
for (let c = 0; c < 3; c++) out[c] += sh[b * 3 + c] * basis[b]
|
|
113
|
+
}
|
|
114
|
+
return out
|
|
115
|
+
}
|
package/src/ik-solver.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Mat4, Quat, Vec3, type EulerOrder } from "./math"
|
|
|
4
4
|
import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
|
|
5
5
|
|
|
6
6
|
// Callback type for updating world matrix (provided by model to handle append transformations)
|
|
7
|
-
|
|
7
|
+
type UpdateWorldMatrixFn = (boneIndex: number, applyIK: boolean) => void
|
|
8
8
|
|
|
9
9
|
const enum InternalEulerRotationOrder {
|
|
10
10
|
YXZ = 0,
|
package/src/index.ts
CHANGED
|
@@ -19,8 +19,20 @@ export {
|
|
|
19
19
|
type GizmoDragKind,
|
|
20
20
|
type EffectParamValue,
|
|
21
21
|
type EffectResult,
|
|
22
|
+
type CullDiagnostics,
|
|
23
|
+
type MidiNote,
|
|
22
24
|
} from "./engine"
|
|
23
25
|
export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
|
|
26
|
+
export { parseMidi } from "./midi-loader"
|
|
27
|
+
// Radiance .hdr, for HDRI worlds — the host fetches the file and hands the
|
|
28
|
+
// parsed image to setBackdropEquirect.
|
|
29
|
+
export { parseHDR, type HdrImage } from "./hdr"
|
|
30
|
+
// Lyrics timing (.lrc) — the host parses the file and hands the lines to setLyrics.
|
|
31
|
+
export { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, parseLRC, type LyricLine, type LyricRect } from "./shaders/lyrics-api"
|
|
32
|
+
// A material parameter over time. The sampler is exported too: a host that
|
|
33
|
+
// wants to draw a track, or scrub one, should read the same curve the engine
|
|
34
|
+
// plays rather than reimplementing it a second time.
|
|
35
|
+
export { sampleParamTrack, type ParamKey, type ParamValue } from "./param-track"
|
|
24
36
|
export {
|
|
25
37
|
compileGraph,
|
|
26
38
|
validateGraph,
|
package/src/math.ts
CHANGED
|
@@ -684,16 +684,33 @@ export class Mat4 {
|
|
|
684
684
|
return new Mat4(new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]))
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
687
|
+
/**
|
|
688
|
+
* Perspective for a LEFT-HANDED system (+Z forward), mapping z ∈ [near, far]
|
|
689
|
+
* onto clip z ∈ [0, w] — WebGPU's range, and D3D's.
|
|
690
|
+
*
|
|
691
|
+
* It used to write the OpenGL matrix, which maps onto [-w, w]. That is not a
|
|
692
|
+
* style difference: WebGPU clips everything with z < 0, so half the mapping was
|
|
693
|
+
* being thrown away and the near plane silently sat at TWICE the requested
|
|
694
|
+
* near. With the camera's own floor of 0.5 that put the real clip at 1.0 units
|
|
695
|
+
* — and since most materials draw with no face culling, a close-up did not fade
|
|
696
|
+
* the front of a head, it cut it open and showed the inside of the skull.
|
|
697
|
+
* Reported from the wild as 穿模 on imported camera work, which is exactly
|
|
698
|
+
* where it would show: a wide-angle shot is composed by moving the camera in.
|
|
699
|
+
*
|
|
700
|
+
* REVERSED-Z is this same function with near and far passed the other way
|
|
701
|
+
* round: it maps near → 1 and far → 0, which is worth doing on a float depth
|
|
702
|
+
* buffer because 1/z crowds precision at the near plane and float crowds it at
|
|
703
|
+
* zero, and the two cancel. On a UNORM buffer it is pointless — the precision
|
|
704
|
+
* curve is identical, only mirrored — so the caller decides, and decides on
|
|
705
|
+
* whether it got a float depth format.
|
|
706
|
+
*/
|
|
690
707
|
static perspectiveInto(out: Float32Array, fov: number, aspect: number, near: number, far: number): void {
|
|
691
708
|
const f = 1.0 / Math.tan(fov / 2)
|
|
692
|
-
const rangeInv = 1.0 / (far - near)
|
|
709
|
+
const rangeInv = 1.0 / (far - near)
|
|
693
710
|
out[0] = f / aspect; out[1] = 0; out[2] = 0; out[3] = 0
|
|
694
711
|
out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0
|
|
695
|
-
out[8] = 0; out[9] = 0; out[10] =
|
|
696
|
-
out[12] = 0; out[13] = 0; out[14] = -near * far * rangeInv
|
|
712
|
+
out[8] = 0; out[9] = 0; out[10] = far * rangeInv; out[11] = 1 // Z+ forward (LH)
|
|
713
|
+
out[12] = 0; out[13] = 0; out[14] = -near * far * rangeInv; out[15] = 0
|
|
697
714
|
}
|
|
698
715
|
|
|
699
716
|
static perspective(fov: number, aspect: number, near: number, far: number): Mat4 {
|
|
@@ -1193,17 +1210,6 @@ export const scratchMat4Values: Float32Array[] = [
|
|
|
1193
1210
|
new Float32Array(16),
|
|
1194
1211
|
]
|
|
1195
1212
|
|
|
1196
|
-
export const scratchVec3: Vec3[] = [
|
|
1197
|
-
new Vec3(0, 0, 0),
|
|
1198
|
-
new Vec3(0, 0, 0),
|
|
1199
|
-
new Vec3(0, 0, 0),
|
|
1200
|
-
new Vec3(0, 0, 0),
|
|
1201
|
-
new Vec3(0, 0, 0),
|
|
1202
|
-
new Vec3(0, 0, 0),
|
|
1203
|
-
new Vec3(0, 0, 0),
|
|
1204
|
-
new Vec3(0, 0, 0),
|
|
1205
|
-
]
|
|
1206
|
-
|
|
1207
1213
|
export const scratchQuat: Quat[] = [
|
|
1208
1214
|
new Quat(0, 0, 0, 1),
|
|
1209
1215
|
new Quat(0, 0, 0, 1),
|