reze-engine 0.62.1 → 0.64.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.
Files changed (83) hide show
  1. package/dist/engine.d.ts +95 -57
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +466 -162
  4. package/dist/graph/compile.d.ts.map +1 -1
  5. package/dist/graph/compile.js +4 -1
  6. package/dist/graph/registry.d.ts +3 -0
  7. package/dist/graph/registry.d.ts.map +1 -1
  8. package/dist/graph/registry.js +133 -0
  9. package/dist/graph/schema.d.ts +12 -0
  10. package/dist/graph/schema.d.ts.map +1 -1
  11. package/dist/graph/slots.d.ts.map +1 -1
  12. package/dist/graph/slots.js +11 -20
  13. package/dist/index.d.ts +9 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/shaders/lights.d.ts +3 -2
  17. package/dist/shaders/lights.d.ts.map +1 -1
  18. package/dist/shaders/lights.js +16 -3
  19. package/dist/shaders/materials/common.d.ts.map +1 -1
  20. package/dist/shaders/materials/common.js +63 -56
  21. package/dist/shaders/materials/nodes.d.ts +1 -1
  22. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  23. package/dist/shaders/materials/nodes.js +210 -0
  24. package/dist/shaders/passes/composite.d.ts.map +1 -1
  25. package/dist/shaders/passes/composite.js +13 -1
  26. package/dist/shaders/passes/cull.d.ts +1 -1
  27. package/dist/shaders/passes/cull.d.ts.map +1 -1
  28. package/dist/shaders/passes/cull.js +7 -3
  29. package/dist/shaders/passes/ground.d.ts.map +1 -1
  30. package/dist/shaders/passes/ground.js +32 -55
  31. package/dist/shaders/passes/particles.d.ts +11 -1
  32. package/dist/shaders/passes/particles.d.ts.map +1 -1
  33. package/dist/shaders/passes/particles.js +9 -4
  34. package/dist/shaders/scene-light-api.d.ts +18 -10
  35. package/dist/shaders/scene-light-api.d.ts.map +1 -1
  36. package/dist/shaders/scene-light-api.js +58 -46
  37. package/dist/shadow-cascades.d.ts +47 -20
  38. package/dist/shadow-cascades.d.ts.map +1 -1
  39. package/dist/shadow-cascades.js +36 -13
  40. package/dist/unity/character.d.ts +59 -0
  41. package/dist/unity/character.d.ts.map +1 -0
  42. package/dist/unity/character.js +120 -0
  43. package/dist/unity/globals.d.ts +93 -0
  44. package/dist/unity/globals.d.ts.map +1 -0
  45. package/dist/unity/globals.js +340 -0
  46. package/dist/unity/host.d.ts +137 -0
  47. package/dist/unity/host.d.ts.map +1 -0
  48. package/dist/unity/host.js +489 -0
  49. package/dist/unity/looks.d.ts +88 -0
  50. package/dist/unity/looks.d.ts.map +1 -0
  51. package/dist/unity/looks.js +295 -0
  52. package/dist/unity/skin.d.ts +9 -0
  53. package/dist/unity/skin.d.ts.map +1 -0
  54. package/dist/unity/skin.js +117 -0
  55. package/dist/unity/stage.d.ts +148 -0
  56. package/dist/unity/stage.d.ts.map +1 -0
  57. package/dist/unity/stage.js +375 -0
  58. package/dist/unity/state.d.ts +22 -0
  59. package/dist/unity/state.d.ts.map +1 -0
  60. package/dist/unity/state.js +170 -0
  61. package/package.json +1 -1
  62. package/src/engine.ts +504 -176
  63. package/src/graph/compile.ts +3 -1
  64. package/src/graph/registry.ts +142 -0
  65. package/src/graph/schema.ts +9 -0
  66. package/src/graph/slots.ts +11 -20
  67. package/src/index.ts +11 -0
  68. package/src/shaders/lights.ts +16 -3
  69. package/src/shaders/materials/common.ts +63 -57
  70. package/src/shaders/materials/nodes.ts +210 -0
  71. package/src/shaders/passes/composite.ts +13 -1
  72. package/src/shaders/passes/cull.ts +7 -3
  73. package/src/shaders/passes/ground.ts +32 -55
  74. package/src/shaders/passes/particles.ts +19 -5
  75. package/src/shaders/scene-light-api.ts +59 -46
  76. package/src/shadow-cascades.ts +61 -32
  77. package/src/unity/character.ts +169 -0
  78. package/src/unity/globals.ts +443 -0
  79. package/src/unity/host.ts +587 -0
  80. package/src/unity/looks.ts +372 -0
  81. package/src/unity/skin.ts +128 -0
  82. package/src/unity/stage.ts +568 -0
  83. package/src/unity/state.ts +172 -0
@@ -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 near
7
- * one to the stretch around what the camera is looking at, the far one to the
8
- * whole of what it sees. Whatever is in view is in a cascade, and whatever
9
- * could throw a shadow onto it is in the cascade's depth range, which is fitted
10
- * to the scene's bounds along the light. A room forty metres across with its
11
- * window frames behind the camera shadows its floor as the game does; a lone
12
- * dancer on an empty floor keeps a crisp near map.
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 shape was two fixed boxes around the camera target, 64 and 256
15
- * units across, and a stage reached past them: the floor near the windows lay
16
- * outside every box and was drawn lit, the shadows stopping at the box's edge
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: the outer cascade CONTAINS
20
- * the inner one. The sampler falls from cascade 0 to 1 at the box edge, which
21
- * is only seamless if 1 covers where 0 ends, and the cull tests the OUTERMOST
22
- * frustum alone. Both hold because the outer slice is the whole frustum, of
23
- * which the inner slice is a part, and both take the same depth range.
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 map — sets the snap quantum. */
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
- export const SHADOW_CASCADES: readonly ShadowCascade[] = [{ mapSize: 4096 }, { mapSize: 2048 }]
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 near cascade reaches, in
34
- * world units: the dancer and the floor around her, at the near map's texel. */
35
- export const NEAR_REACH = 40
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 split = Math.min(farFit, Math.max(view.near + 8, view.focus + NEAR_REACH))
85
- return [
86
- [view.near, split],
87
- [view.near, farFit],
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
+ }