reze-engine 0.63.0 → 0.65.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/dist/engine.d.ts +226 -96
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1187 -732
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts +3 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +133 -0
- package/dist/graph/schema.d.ts +12 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +15 -18
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +10 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +15 -1
- package/dist/outline-normals.d.ts +12 -0
- package/dist/outline-normals.d.ts.map +1 -0
- package/dist/outline-normals.js +100 -0
- package/dist/pmx-loader.d.ts +3 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +7 -2
- package/dist/shaders/lights.d.ts +3 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -3
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +65 -56
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +210 -0
- package/dist/shaders/passes/bloom.d.ts +4 -3
- package/dist/shaders/passes/bloom.d.ts.map +1 -1
- package/dist/shaders/passes/bloom.js +58 -87
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +122 -64
- package/dist/shaders/passes/cull.d.ts +1 -1
- package/dist/shaders/passes/cull.d.ts.map +1 -1
- package/dist/shaders/passes/cull.js +7 -3
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +32 -55
- package/dist/shaders/passes/mirror.d.ts +18 -12
- package/dist/shaders/passes/mirror.d.ts.map +1 -1
- package/dist/shaders/passes/mirror.js +59 -13
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +71 -14
- package/dist/shaders/passes/particles.d.ts +1 -1
- package/dist/shaders/passes/particles.js +1 -1
- package/dist/shaders/scene-light-api.d.ts +18 -10
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +58 -46
- package/dist/shadow-cascades.d.ts +47 -20
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +36 -13
- package/dist/unity/character.d.ts +59 -0
- package/dist/unity/character.d.ts.map +1 -0
- package/dist/unity/character.js +120 -0
- package/dist/unity/globals.d.ts +93 -0
- package/dist/unity/globals.d.ts.map +1 -0
- package/dist/unity/globals.js +340 -0
- package/dist/unity/host.d.ts +152 -0
- package/dist/unity/host.d.ts.map +1 -0
- package/dist/unity/host.js +529 -0
- package/dist/unity/looks.d.ts +93 -0
- package/dist/unity/looks.d.ts.map +1 -0
- package/dist/unity/looks.js +312 -0
- package/dist/unity/skin.d.ts +9 -0
- package/dist/unity/skin.d.ts.map +1 -0
- package/dist/unity/skin.js +120 -0
- package/dist/unity/stage.d.ts +155 -0
- package/dist/unity/stage.d.ts.map +1 -0
- package/dist/unity/stage.js +410 -0
- package/dist/unity/state.d.ts +22 -0
- package/dist/unity/state.d.ts.map +1 -0
- package/dist/unity/state.js +170 -0
- package/package.json +1 -1
- package/src/engine.ts +1535 -964
- package/src/graph/compile.ts +3 -1
- package/src/graph/registry.ts +142 -0
- package/src/graph/schema.ts +9 -0
- package/src/graph/slots.ts +15 -18
- package/src/index.ts +12 -0
- package/src/model.ts +18 -1
- package/src/outline-normals.ts +110 -0
- package/src/pmx-loader.ts +8 -2
- package/src/shaders/lights.ts +16 -3
- package/src/shaders/materials/common.ts +65 -57
- package/src/shaders/materials/nodes.ts +210 -0
- package/src/shaders/passes/bloom.ts +59 -87
- package/src/shaders/passes/composite.ts +122 -64
- package/src/shaders/passes/cull.ts +7 -3
- package/src/shaders/passes/ground.ts +32 -55
- package/src/shaders/passes/mirror.ts +60 -13
- package/src/shaders/passes/outline.ts +71 -14
- package/src/shaders/passes/particles.ts +1 -1
- package/src/shaders/scene-light-api.ts +59 -46
- package/src/shadow-cascades.ts +61 -32
- package/src/unity/character.ts +169 -0
- package/src/unity/globals.ts +443 -0
- package/src/unity/host.ts +630 -0
- package/src/unity/looks.ts +391 -0
- package/src/unity/skin.ts +131 -0
- package/src/unity/stage.ts +604 -0
- package/src/unity/state.ts +172 -0
- package/src/shaders/agx-lut.ts +0 -30
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SHADOW_CASCADES } from "../shadow-cascades"
|
|
1
|
+
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES } from "../shadow-cascades"
|
|
2
2
|
// The scene's own light, as an effect author sees it: the sun's shadow and the
|
|
3
3
|
// world's ambient.
|
|
4
4
|
//
|
|
@@ -51,31 +51,61 @@ fn rzWorldAmbientAvg() -> vec3f {
|
|
|
51
51
|
`
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* The sun's shadow atlas, as every reader looks it up: `<p>Locate(q)` finds
|
|
55
|
+
* world point q in the first cascade whose box holds it and answers
|
|
56
|
+
* (atlas uv, depth, cascade) — cascade -1 outside them all — and `<p>Taps(a)`
|
|
57
|
+
* is the 3×3 PCF at that place.
|
|
55
58
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
59
|
+
* ONE implementation for the materials, the ground and effects, so a blade of
|
|
60
|
+
* grass and her own skin go dark at the same edge. `vp` names the cascades'
|
|
61
|
+
* view-projection array in the module that includes it.
|
|
62
|
+
*
|
|
63
|
+
* The box test keeps a margin (SHADOW_MARGIN of the tile's half-width) so the
|
|
64
|
+
* whole filter stays inside the tile: a tap that crossed into the neighbouring
|
|
65
|
+
* tile would compare against another cascade's depths. The sampler is linear,
|
|
66
|
+
* so each tap is itself a 2×2 compare. Unrolled — Safari's Metal backend
|
|
67
|
+
* doesn't unroll nested shadow loops reliably.
|
|
60
68
|
*/
|
|
61
|
-
export const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
export const SHADOW_MARGIN = 0.98
|
|
70
|
+
|
|
71
|
+
export const sunShadowWgsl = (p: string, map: string, sampler: string, vp: string) => {
|
|
72
|
+
const n = SHADOW_CASCADES.length
|
|
73
|
+
const tiles = SHADOW_CASCADES.map((c) => `vec2f(${c.origin[0] / SHADOW_ATLAS_SIZE}, ${c.origin[1] / SHADOW_ATLAS_SIZE})`).join(", ")
|
|
74
|
+
const scale = SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE
|
|
75
|
+
return /* wgsl */ `
|
|
76
|
+
const ${p}Tiles = array<vec2f, ${n}>(${tiles});
|
|
77
|
+
|
|
78
|
+
fn ${p}Locate(q: vec3f) -> vec4f {
|
|
79
|
+
for (var i = 0u; i < ${n}u; i++) {
|
|
80
|
+
let c = ${vp}[i] * vec4f(q, 1.0);
|
|
81
|
+
let ndc = c.xyz / max(c.w, 1e-6);
|
|
82
|
+
if (all(abs(ndc.xy) < vec2f(${SHADOW_MARGIN})) && ndc.z > 0.0 && ndc.z < 1.0) {
|
|
83
|
+
let uv = ${p}Tiles[i] + vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5) * ${scale};
|
|
84
|
+
return vec4f(uv, ndc.z, f32(i));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return vec4f(0.0, 0.0, 0.0, -1.0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn ${p}Taps(a: vec4f) -> f32 {
|
|
91
|
+
let cmpZ = a.z - 0.001;
|
|
92
|
+
let ts = ${1 / SHADOW_ATLAS_SIZE};
|
|
93
|
+
let s00 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, -ts), cmpZ);
|
|
94
|
+
let s10 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(0.0, -ts), cmpZ);
|
|
95
|
+
let s20 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, -ts), cmpZ);
|
|
96
|
+
let s01 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, 0.0), cmpZ);
|
|
97
|
+
let s11 = textureSampleCompareLevel(${map}, ${sampler}, a.xy, cmpZ);
|
|
98
|
+
let s21 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, 0.0), cmpZ);
|
|
99
|
+
let s02 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, ts), cmpZ);
|
|
100
|
+
let s12 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(0.0, ts), cmpZ);
|
|
101
|
+
let s22 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, ts), cmpZ);
|
|
74
102
|
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
103
|
+
}
|
|
75
104
|
`
|
|
105
|
+
}
|
|
76
106
|
|
|
77
107
|
/** Bindings the real accessors take, from `binding` upward. */
|
|
78
|
-
export const SCENE_LIGHT_API_BINDINGS =
|
|
108
|
+
export const SCENE_LIGHT_API_BINDINGS = 4
|
|
79
109
|
|
|
80
110
|
/**
|
|
81
111
|
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
@@ -84,12 +114,11 @@ export const SCENE_LIGHT_API_BINDINGS = 5
|
|
|
84
114
|
* `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
|
|
85
115
|
* the materials' own ambient, so an effect's shade can match hers.
|
|
86
116
|
*
|
|
87
|
-
* With `on`, declares
|
|
117
|
+
* With `on`, declares four bindings from `binding`:
|
|
88
118
|
* +0 the light uniform (ambient, the sun — direction, and in .w the shadow
|
|
89
119
|
* switch — and the HDRI's irradiance)
|
|
90
120
|
* +1 the cascade view-projections, then the caster sphere
|
|
91
|
-
* +2 the
|
|
92
|
-
* +4 the comparison sampler
|
|
121
|
+
* +2 the sun's shadow atlas +3 the comparison sampler
|
|
93
122
|
*
|
|
94
123
|
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
95
124
|
* surface facing away from the sun, which is right for skin and wrong for a
|
|
@@ -114,17 +143,9 @@ struct _RzSceneLightU { ambientColor: vec4f, lights: array<_RzSceneLight, 4>, sh
|
|
|
114
143
|
struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
|
|
115
144
|
@group(${group}) @binding(${binding}) var<uniform> _rzLight: _RzSceneLightU;
|
|
116
145
|
@group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
|
|
117
|
-
@group(${group}) @binding(${binding + 2}) var
|
|
118
|
-
@group(${group}) @binding(${binding + 3}) var
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
|
|
122
|
-
${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
|
|
126
|
-
${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
|
|
127
|
-
}
|
|
146
|
+
@group(${group}) @binding(${binding + 2}) var _rzShadowAtlas: texture_depth_2d;
|
|
147
|
+
@group(${group}) @binding(${binding + 3}) var _rzShadowCmp: sampler_comparison;
|
|
148
|
+
${sunShadowWgsl("_rzSun", "_rzShadowAtlas", "_rzShadowCmp", "_rzShadowVP.viewProj")}
|
|
128
149
|
|
|
129
150
|
fn rzShadow(p: vec3f) -> f32 {
|
|
130
151
|
// The scene's one shadow switch, on the sun — see sampleShadow.
|
|
@@ -142,18 +163,10 @@ fn rzShadow(p: vec3f) -> f32 {
|
|
|
142
163
|
let perp = length(toCaster - toSun * along);
|
|
143
164
|
if (along <= -cs.w || perp > cs.w) { return 1.0; }
|
|
144
165
|
}
|
|
145
|
-
let
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return mix(1.0, _rzShadowNearTaps(n0), castAmt);
|
|
150
|
-
}
|
|
151
|
-
let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
|
|
152
|
-
let n1 = c1.xyz / max(c1.w, 1e-6);
|
|
153
|
-
if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
|
|
154
|
-
return mix(1.0, _rzShadowFarTaps(n1), castAmt);
|
|
155
|
-
}
|
|
156
|
-
return 1.0;
|
|
166
|
+
let a = _rzSunLocate(p + toSun * 0.08);
|
|
167
|
+
// Outside every cascade there is no occlusion information: lit.
|
|
168
|
+
if (a.w < 0.0) { return 1.0; }
|
|
169
|
+
return mix(1.0, _rzSunTaps(a), castAmt);
|
|
157
170
|
}
|
|
158
171
|
${worldAmbientWgsl("_rzLight")}`
|
|
159
172
|
}
|
package/src/shadow-cascades.ts
CHANGED
|
@@ -3,36 +3,54 @@ import { Mat4, Vec3 } from "./math"
|
|
|
3
3
|
/**
|
|
4
4
|
* THE SUN'S SHADOW FOLLOWS THE CAMERA, the way Blender's does.
|
|
5
5
|
*
|
|
6
|
-
* Each cascade is fitted every frame to a slice of the view frustum: the
|
|
7
|
-
*
|
|
8
|
-
* whole of what it sees
|
|
9
|
-
* could throw a shadow onto it is in the
|
|
10
|
-
* to the scene's bounds along the
|
|
11
|
-
*
|
|
12
|
-
* dancer on an empty floor keeps a
|
|
6
|
+
* Each cascade is fitted every frame to a slice of the view frustum: the
|
|
7
|
+
* innermost to the stretch around what the camera is looking at, the
|
|
8
|
+
* outermost to the whole of what it sees, two more between. Whatever is in
|
|
9
|
+
* view is in a cascade, and whatever could throw a shadow onto it is in the
|
|
10
|
+
* cascade's depth range, which is fitted to the scene's bounds along the
|
|
11
|
+
* light. A room forty metres across with its window frames behind the camera
|
|
12
|
+
* shadows its floor as the game does; a lone dancer on an empty floor keeps a
|
|
13
|
+
* crisp inner tile.
|
|
13
14
|
*
|
|
14
|
-
* The earlier
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* in a straight line.
|
|
15
|
+
* The earlier shapes were two fixed boxes around the camera target, then two
|
|
16
|
+
* fitted maps; the game's pipeline draws four cascades into one atlas, and so
|
|
17
|
+
* does this.
|
|
18
18
|
*
|
|
19
|
-
* INVARIANT the sampler and the cull both lean on:
|
|
20
|
-
*
|
|
21
|
-
* is only seamless if
|
|
22
|
-
* frustum alone. Both hold because
|
|
23
|
-
*
|
|
19
|
+
* INVARIANT the sampler and the cull both lean on: each cascade CONTAINS the
|
|
20
|
+
* ones inside it. The sampler takes the first box that holds a point, which
|
|
21
|
+
* is only seamless if the next covers where one ends, and the cull tests the
|
|
22
|
+
* OUTERMOST frustum alone. Both hold because every slice starts at the near
|
|
23
|
+
* plane and the outermost is the whole frustum, and all take the same depth
|
|
24
|
+
* range.
|
|
24
25
|
* tests/shadow-cascades.test.mjs pins it.
|
|
25
26
|
*/
|
|
26
27
|
export type ShadowCascade = {
|
|
27
|
-
/** Texels per side of this cascade's
|
|
28
|
+
/** Texels per side of this cascade's tile — sets the snap quantum. */
|
|
28
29
|
mapSize: number
|
|
30
|
+
/** Where the tile sits in the atlas, in texels from its top-left corner. */
|
|
31
|
+
origin: readonly [number, number]
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
/**
|
|
35
|
+
* ONE ATLAS, FOUR TILES, as the game's pipeline (and URP) lays its sun shadow
|
|
36
|
+
* out: one depth texture, cascade i in tile (i mod 2, i div 2). One texture
|
|
37
|
+
* means one binding for every reader — materials, ground, effects, particles —
|
|
38
|
+
* and room in the same scheme for the local lights' shadows later.
|
|
39
|
+
*/
|
|
40
|
+
export const SHADOW_ATLAS_SIZE = 4096
|
|
41
|
+
|
|
42
|
+
const TILE = SHADOW_ATLAS_SIZE / 2
|
|
43
|
+
|
|
44
|
+
export const SHADOW_CASCADES: readonly ShadowCascade[] = [0, 1, 2, 3].map((i) => ({
|
|
45
|
+
mapSize: TILE,
|
|
46
|
+
origin: [(i % 2) * TILE, Math.floor(i / 2) * TILE] as const,
|
|
47
|
+
}))
|
|
32
48
|
|
|
33
|
-
/** How far past the camera's point of interest the
|
|
34
|
-
* world units: the dancer and the floor around her
|
|
35
|
-
|
|
49
|
+
/** How far past the camera's point of interest the inner cascade reaches, in
|
|
50
|
+
* world units: the dancer and the floor around her. Half the old two-map
|
|
51
|
+
* reach, which keeps her texel where it was at half the tile; the three
|
|
52
|
+
* outer cascades carry the rest of the room. */
|
|
53
|
+
export const NEAR_REACH = 20
|
|
36
54
|
|
|
37
55
|
/** The view the cascades are fitted to: the camera's eye and basis (world
|
|
38
56
|
* space, left-handed, +Z forward as the projection is), its vertical field of
|
|
@@ -55,16 +73,18 @@ export type ShadowBounds = { min: [number, number, number]; max: [number, number
|
|
|
55
73
|
|
|
56
74
|
type XYZ = { x: number; y: number; z: number }
|
|
57
75
|
|
|
58
|
-
/**
|
|
59
|
-
* Where each cascade's slice of the view starts and ends, as distances along
|
|
60
|
-
* the view: [near, split] and [near, farFit]. The far end is where the scene
|
|
61
|
-
* ends, so an empty floor with a dancer on it keeps a short, sharp frustum
|
|
62
|
-
* rather than the camera's far plane.
|
|
63
|
-
*/
|
|
64
76
|
/** The deepest a cascade reaches along the view, in world units — the far
|
|
65
77
|
* plane's old cap, which is where every stage's shadows were fitted before. */
|
|
66
78
|
const SHADOW_FAR = 8000
|
|
67
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Where each cascade's slice of the view starts and ends, as distances along
|
|
82
|
+
* the view, inner to outer: every slice starts at the near plane and they
|
|
83
|
+
* nest, the innermost ending just past the point of interest and the
|
|
84
|
+
* outermost where the scene ends, the two between spaced evenly in ratio.
|
|
85
|
+
* Nested rather than abutting so the invariant above holds: the outermost
|
|
86
|
+
* box contains all of them, and the sampler takes the first that holds a point.
|
|
87
|
+
*/
|
|
68
88
|
export function cascadeSlices(view: ShadowView, bounds: ShadowBounds): [number, number][] {
|
|
69
89
|
let farFit = view.near + 200
|
|
70
90
|
if (bounds) {
|
|
@@ -81,11 +101,14 @@ export function cascadeSlices(view: ShadowView, bounds: ShadowBounds): [number,
|
|
|
81
101
|
// a kilometre out is in the scene's bounds, and fitting the far cascade to it
|
|
82
102
|
// would spread the stage's shadow map across the sky.
|
|
83
103
|
farFit = Math.min(view.far, SHADOW_FAR, Math.max(view.near + 1, farFit))
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
const first = Math.min(farFit, Math.max(view.near + 8, view.focus + NEAR_REACH))
|
|
105
|
+
const n = SHADOW_CASCADES.length
|
|
106
|
+
const out: [number, number][] = []
|
|
107
|
+
for (let i = 0; i < n; i++) {
|
|
108
|
+
const end = i === n - 1 ? farFit : first * Math.pow(farFit / first, i / (n - 1))
|
|
109
|
+
out.push([view.near, end])
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
89
112
|
}
|
|
90
113
|
|
|
91
114
|
/** The bounding sphere of a frustum slice: on the view axis, at the depth that
|
|
@@ -188,6 +211,12 @@ export function fitShadowVP(
|
|
|
188
211
|
return out
|
|
189
212
|
}
|
|
190
213
|
|
|
214
|
+
/** Every cascade's bounding sphere (centre, radius), inner to outer — the
|
|
215
|
+
* split spheres a game shader picks its cascade by (_CascadeShadowSplitSpheres). */
|
|
216
|
+
export function cascadeSpheres(view: ShadowView, bounds: ShadowBounds): { center: Vec3; radius: number }[] {
|
|
217
|
+
return cascadeSlices(view, bounds).map(([n, f]) => sliceSphere(view, n, f))
|
|
218
|
+
}
|
|
219
|
+
|
|
191
220
|
/** Every cascade's view-projection in a row, inner to outer. */
|
|
192
221
|
export function buildShadowCascades(view: ShadowView, sunDirection: XYZ, bounds: ShadowBounds, out: Float32Array): Float32Array {
|
|
193
222
|
const slices = cascadeSlices(view, bounds)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The game's per-character shading inputs, for a model wearing its character
|
|
2
|
+
// shaders.
|
|
3
|
+
//
|
|
4
|
+
// In the game a CharacterEffect component (P08.RenderPipeline, ported in
|
|
5
|
+
// ag-rip's charfx/CharacterEffect.cs from the decompiled GameAssembly.dll)
|
|
6
|
+
// writes one property block shared by all of a character's renderers: its own
|
|
7
|
+
// key light, expressed in a camera-facing frame (_LocalLightDir in
|
|
8
|
+
// sim_TowardMatrix, LightingFeatures.SetupTowardMatrix); its face frame for the
|
|
9
|
+
// SDF face shadow (_World2Face); its rim lights; its fills; and an ambient of
|
|
10
|
+
// its own from three colours (EnvironmentEffect). Nothing here is per scene:
|
|
11
|
+
// it is the character's look, carried by the character.
|
|
12
|
+
//
|
|
13
|
+
// An MMD model has no CharacterEffect. It takes a CharacterRig — the values a
|
|
14
|
+
// game character's prefab carries, exported with the look — and the frames
|
|
15
|
+
// come from its own bones: the face is the head bone turned to face the way
|
|
16
|
+
// the game's faces do (the game's face node rests aligned with the world,
|
|
17
|
+
// looking +Z; a PMX model looks −Z, so a half turn about Y).
|
|
18
|
+
|
|
19
|
+
import type { NativeValue } from "./host"
|
|
20
|
+
import { gameToEngine, invert4, mul4 } from "./globals"
|
|
21
|
+
|
|
22
|
+
/** A game character's CharacterEffect values, as its prefab serialises them. */
|
|
23
|
+
export type CharacterRig = {
|
|
24
|
+
localLightInclination: number
|
|
25
|
+
localLightAzimuth: number
|
|
26
|
+
localLightIntensity: number
|
|
27
|
+
localLightColor: [number, number, number, number]
|
|
28
|
+
faceReceiveShadow: boolean
|
|
29
|
+
disableFaceSDFShadow: boolean
|
|
30
|
+
emissionColor: [number, number, number, number]
|
|
31
|
+
fillOuter: [number, number, number, number]
|
|
32
|
+
fillInner: [number, number, number, number]
|
|
33
|
+
fillColor: [number, number, number, number]
|
|
34
|
+
fillRatio: number
|
|
35
|
+
rimlightColor: [number, number, number, number]
|
|
36
|
+
rimlightThreshold: number
|
|
37
|
+
rimlightFade: number
|
|
38
|
+
rimlightRange: number
|
|
39
|
+
rimlightInclination: number
|
|
40
|
+
rimlightAzimuth1: number
|
|
41
|
+
rimlightAzimuth2: number
|
|
42
|
+
characterLayer: number
|
|
43
|
+
/** EnvironmentEffect's trilight, sRGB as serialised. */
|
|
44
|
+
skyColor: [number, number, number]
|
|
45
|
+
equatorColor: [number, number, number]
|
|
46
|
+
groundColor: [number, number, number]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const srgbToLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4))
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Unity's ambient probe of a trilight (RenderSettings.ambientProbe in Trilight
|
|
53
|
+
* mode), measured in Unity 6 rather than derived: the colours are taken to
|
|
54
|
+
* linear, then
|
|
55
|
+
* c0 = (S + 4E + G) / 6 c1 = (√6 / 9)(S − G)
|
|
56
|
+
* c6 = (5/192)(2E − S − G) c8 = (5/64)(2E − S − G)
|
|
57
|
+
* and every other coefficient is zero — Unity's raw coefficients, which its
|
|
58
|
+
* shader constants pack as SHA = (c3, c1, c2, c0 − c6), SHB = (c4, c5, 3c6, c7),
|
|
59
|
+
* SHC = c8. Checked against the game's recorded block for (0.8, 0.5, 0.3).
|
|
60
|
+
*/
|
|
61
|
+
export function trilightProbe(
|
|
62
|
+
sky: [number, number, number],
|
|
63
|
+
equator: [number, number, number],
|
|
64
|
+
ground: [number, number, number],
|
|
65
|
+
): number[][] {
|
|
66
|
+
const out: number[][] = []
|
|
67
|
+
for (let k = 0; k < 3; k++) {
|
|
68
|
+
const S = srgbToLinear(sky[k]),
|
|
69
|
+
E = srgbToLinear(equator[k]),
|
|
70
|
+
G = srgbToLinear(ground[k])
|
|
71
|
+
const ring = 2 * E - S - G
|
|
72
|
+
out.push([(S + 4 * E + G) / 6, (Math.sqrt(6) / 9) * (S - G), 0, 0, 0, 0, (5 / 192) * ring, 0, (5 / 64) * ring])
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Unity's shader constants from raw coefficients (channel, index). */
|
|
78
|
+
export function probeConstants(c: number[][]): Record<string, number[]> {
|
|
79
|
+
const ch = ["r", "g", "b"]
|
|
80
|
+
const o: Record<string, number[]> = {}
|
|
81
|
+
for (let k = 0; k < 3; k++) {
|
|
82
|
+
const s = c[k]
|
|
83
|
+
o[`SHA${ch[k]}`] = [s[3], s[1], s[2], s[0] - s[6]]
|
|
84
|
+
o[`SHB${ch[k]}`] = [s[4], s[5], s[6] * 3, s[7]]
|
|
85
|
+
}
|
|
86
|
+
o.SHC = [c[0][8], c[1][8], c[2][8], 1]
|
|
87
|
+
return o
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* sim_TowardMatrix (LightingFeatures.SetupTowardMatrix): the frame the
|
|
92
|
+
* character light is expressed in — the camera's backward direction flattened
|
|
93
|
+
* to the ground, as columns (up × d, up, d). Taken immediately, as the DLC
|
|
94
|
+
* poster sets CameraExtension.shadowRotationImmediately.
|
|
95
|
+
*/
|
|
96
|
+
export function towardMatrix(cameraForward: [number, number, number]): Float32Array {
|
|
97
|
+
const x = -cameraForward[0],
|
|
98
|
+
z = -cameraForward[2]
|
|
99
|
+
const len = Math.hypot(x, z)
|
|
100
|
+
const d = len > 1e-5 ? [x / len, 0, z / len] : [0, 0, 0]
|
|
101
|
+
return Float32Array.from([d[2], 0, -d[0], 0, 0, 1, 0, 0, d[0], d[1], d[2], 0, 0, 0, 0, 1])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** CharacterGlobalParams.Execute: the outline's resolution allowance. */
|
|
105
|
+
export function outlineMaxOffsetMultiplier(pixelWidth: number): number {
|
|
106
|
+
return Math.max(pixelWidth / 768 - 1, 0)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The character's property block (CharacterEffect.LateUpdate with the default
|
|
111
|
+
* look: no dissolve, dither, interference), by name.
|
|
112
|
+
*
|
|
113
|
+
* `head` is the head bone's skinning matrix (its pose relative to the bind
|
|
114
|
+
* pose, engine units) and `headRest` its rest position; `scale` engine units
|
|
115
|
+
* per game unit.
|
|
116
|
+
*/
|
|
117
|
+
export function characterBlock(
|
|
118
|
+
rig: CharacterRig,
|
|
119
|
+
head: ArrayLike<number> | null,
|
|
120
|
+
headRest: [number, number, number],
|
|
121
|
+
scale: number,
|
|
122
|
+
): Record<string, NativeValue> {
|
|
123
|
+
const o: Record<string, NativeValue> = {}
|
|
124
|
+
// _World2Face: the face frame's world-to-local, in game units. The face is
|
|
125
|
+
// the head's pose applied to a frame at the head's rest position looking −Z.
|
|
126
|
+
const faceRest = Float32Array.from([-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, headRest[0], headRest[1], headRest[2], 1])
|
|
127
|
+
const face = head ? mul4(head, faceRest) : faceRest
|
|
128
|
+
o._World2Face = mul4(invert4(face), gameToEngine(scale))
|
|
129
|
+
o._ExtralEmissionColor = rig.emissionColor
|
|
130
|
+
let inner = rig.fillInner,
|
|
131
|
+
outer = rig.fillOuter
|
|
132
|
+
inner = [inner[0], inner[1], inner[2], inner[3] * rig.fillRatio]
|
|
133
|
+
outer = [outer[0], outer[1], outer[2], outer[3] * rig.fillRatio]
|
|
134
|
+
o._FillOuter = outer
|
|
135
|
+
o._FillInner = inner
|
|
136
|
+
o._FillClamp = 0
|
|
137
|
+
o._FillSoft = 0
|
|
138
|
+
o._DissolveFactor = 0
|
|
139
|
+
// inclination about the horizon, azimuth negated — float math as the game's
|
|
140
|
+
const incl = (rig.localLightInclination * Math.PI) / 180
|
|
141
|
+
const az = (-rig.localLightAzimuth * Math.PI) / 180
|
|
142
|
+
const c = Math.cos(incl)
|
|
143
|
+
o._LocalLightDir = [Math.sin(az) * -c, Math.sin(incl), Math.cos(az) * c, 0]
|
|
144
|
+
const k = rig.localLightIntensity
|
|
145
|
+
o._LocalLightColor = [rig.localLightColor[0] * k, rig.localLightColor[1] * k, rig.localLightColor[2] * k, rig.localLightColor[3] * k]
|
|
146
|
+
o._UseFaceReceiveShadow = rig.faceReceiveShadow ? 1 : 0
|
|
147
|
+
o._DisableFaceSDFShadow = rig.disableFaceSDFShadow ? 1 : 0
|
|
148
|
+
o._RimlightColor = rig.rimlightColor
|
|
149
|
+
o._RimlightThreshold = rig.rimlightThreshold
|
|
150
|
+
o._RimlightFade = rig.rimlightFade
|
|
151
|
+
o._RimlightRange = rig.rimlightRange
|
|
152
|
+
const zr = rig.rimlightInclination
|
|
153
|
+
const sr = Math.sqrt(1 - zr * zr)
|
|
154
|
+
const a1 = rig.rimlightAzimuth1 * Math.PI,
|
|
155
|
+
a2 = rig.rimlightAzimuth2 * Math.PI
|
|
156
|
+
o._RimlightDir1 = [Math.cos(a1) * sr, Math.sin(a1) * sr, zr, 0]
|
|
157
|
+
o._RimlightDir2 = [Math.cos(a2) * sr, Math.sin(a2) * sr, zr, 0]
|
|
158
|
+
o._ZOffset_Unit = rig.characterLayer
|
|
159
|
+
o._DitherAlpha = 1
|
|
160
|
+
// EnvironmentEffect: the character's own ambient, in both names it is read by
|
|
161
|
+
const sh = probeConstants(trilightProbe(rig.skyColor, rig.equatorColor, rig.groundColor))
|
|
162
|
+
for (const [n, v] of Object.entries(sh)) {
|
|
163
|
+
o[`unity_${n}`] = v
|
|
164
|
+
o[`_Replica_${n}`] = v
|
|
165
|
+
}
|
|
166
|
+
// CharacterEffect.OnEnable: every character renderer on rendering layer 0x40000001
|
|
167
|
+
o.unity_RenderingLayer = new Uint32Array([0x40000001, 0, 0, 0])
|
|
168
|
+
return o
|
|
169
|
+
}
|