reze-engine 0.65.0 → 0.66.1

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 (151) hide show
  1. package/dist/engine.d.ts +156 -42
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +876 -186
  4. package/dist/graph/compile.d.ts.map +1 -1
  5. package/dist/graph/compile.js +5 -0
  6. package/dist/graph/presets/body.d.ts.map +1 -1
  7. package/dist/graph/presets/body.js +26 -24
  8. package/dist/graph/presets/cloth_rough.js +8 -8
  9. package/dist/graph/presets/cloth_smooth.js +8 -8
  10. package/dist/graph/presets/default.js +4 -4
  11. package/dist/graph/presets/eye.js +10 -10
  12. package/dist/graph/presets/face.d.ts.map +1 -1
  13. package/dist/graph/presets/face.js +28 -26
  14. package/dist/graph/presets/hair.js +6 -6
  15. package/dist/graph/presets/metal.js +8 -8
  16. package/dist/graph/presets/stockings.d.ts.map +1 -1
  17. package/dist/graph/presets/stockings.js +4 -11
  18. package/dist/graph/presets/unlit.d.ts.map +1 -1
  19. package/dist/graph/presets/unlit.js +9 -13
  20. package/dist/graph/registry.d.ts.map +1 -1
  21. package/dist/graph/registry.js +128 -157
  22. package/dist/graph/render-class.d.ts +5 -2
  23. package/dist/graph/render-class.d.ts.map +1 -1
  24. package/dist/graph/slots.d.ts.map +1 -1
  25. package/dist/graph/slots.js +5 -1
  26. package/dist/model.d.ts +14 -1
  27. package/dist/model.d.ts.map +1 -1
  28. package/dist/model.js +27 -1
  29. package/dist/pmx-loader.d.ts +4 -0
  30. package/dist/pmx-loader.d.ts.map +1 -1
  31. package/dist/pmx-loader.js +11 -2
  32. package/dist/shaders/anchor-table.d.ts +11 -0
  33. package/dist/shaders/anchor-table.d.ts.map +1 -1
  34. package/dist/shaders/anchor-table.js +9 -3
  35. package/dist/shaders/directives.d.ts +42 -5
  36. package/dist/shaders/directives.d.ts.map +1 -1
  37. package/dist/shaders/directives.js +51 -4
  38. package/dist/shaders/lights.d.ts +11 -7
  39. package/dist/shaders/lights.d.ts.map +1 -1
  40. package/dist/shaders/lights.js +315 -271
  41. package/dist/shaders/materials/common.d.ts.map +1 -1
  42. package/dist/shaders/materials/common.js +26 -3
  43. package/dist/shaders/materials/nodes.d.ts +1 -1
  44. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  45. package/dist/shaders/materials/nodes.js +191 -271
  46. package/dist/shaders/passes/composite.d.ts +1 -1
  47. package/dist/shaders/passes/composite.d.ts.map +1 -1
  48. package/dist/shaders/passes/composite.js +3 -5
  49. package/dist/shaders/passes/ground.js +1 -1
  50. package/dist/shaders/passes/particles.d.ts.map +1 -1
  51. package/dist/shaders/passes/particles.js +9 -3
  52. package/dist/shaders/passes/probe.d.ts +45 -0
  53. package/dist/shaders/passes/probe.d.ts.map +1 -0
  54. package/dist/shaders/passes/probe.js +230 -0
  55. package/dist/shaders/passes/scene-contract.d.ts +13 -0
  56. package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
  57. package/dist/shaders/passes/scene-contract.js +4 -0
  58. package/dist/shaders/scene-light-api.d.ts.map +1 -1
  59. package/dist/shaders/scene-light-api.js +1 -0
  60. package/dist/unity/globals.d.ts +7 -0
  61. package/dist/unity/globals.d.ts.map +1 -1
  62. package/dist/unity/globals.js +4 -3
  63. package/dist/unity/host.d.ts +3 -0
  64. package/dist/unity/host.d.ts.map +1 -1
  65. package/dist/unity/host.js +41 -7
  66. package/dist/unity/looks.d.ts +42 -0
  67. package/dist/unity/looks.d.ts.map +1 -1
  68. package/dist/unity/looks.js +131 -4
  69. package/dist/unity/stage.d.ts +17 -0
  70. package/dist/unity/stage.d.ts.map +1 -1
  71. package/dist/unity/stage.js +22 -5
  72. package/package.json +1 -1
  73. package/src/engine.ts +949 -215
  74. package/src/graph/compile.ts +4 -0
  75. package/src/graph/presets/body.ts +26 -24
  76. package/src/graph/presets/cloth_rough.ts +8 -8
  77. package/src/graph/presets/cloth_smooth.ts +8 -8
  78. package/src/graph/presets/default.ts +4 -4
  79. package/src/graph/presets/eye.ts +10 -10
  80. package/src/graph/presets/face.ts +28 -26
  81. package/src/graph/presets/hair.ts +6 -6
  82. package/src/graph/presets/metal.ts +8 -8
  83. package/src/graph/presets/stockings.ts +4 -11
  84. package/src/graph/presets/unlit.ts +9 -13
  85. package/src/graph/registry.ts +133 -158
  86. package/src/graph/render-class.ts +5 -2
  87. package/src/graph/slots.ts +5 -1
  88. package/src/model.ts +33 -1
  89. package/src/pmx-loader.ts +11 -2
  90. package/src/shaders/anchor-table.ts +15 -3
  91. package/src/shaders/directives.ts +88 -6
  92. package/src/shaders/lights.ts +464 -415
  93. package/src/shaders/materials/common.ts +26 -3
  94. package/src/shaders/materials/nodes.ts +191 -271
  95. package/src/shaders/passes/composite.ts +3 -5
  96. package/src/shaders/passes/ground.ts +1 -1
  97. package/src/shaders/passes/particles.ts +9 -3
  98. package/src/shaders/passes/probe.ts +240 -0
  99. package/src/shaders/passes/scene-contract.ts +17 -0
  100. package/src/shaders/scene-light-api.ts +1 -0
  101. package/src/unity/globals.ts +7 -3
  102. package/src/unity/host.ts +36 -6
  103. package/src/unity/looks.ts +161 -7
  104. package/src/unity/stage.ts +28 -5
  105. package/dist/physics-debug.d.ts +0 -30
  106. package/dist/physics-debug.d.ts.map +0 -1
  107. package/dist/physics-debug.js +0 -526
  108. package/dist/shaders/agx-lut.d.ts +0 -11
  109. package/dist/shaders/agx-lut.d.ts.map +0 -1
  110. package/dist/shaders/agx-lut.js +0 -29
  111. package/dist/shaders/dfg_lut.d.ts +0 -3
  112. package/dist/shaders/dfg_lut.d.ts.map +0 -1
  113. package/dist/shaders/dfg_lut.js +0 -129
  114. package/dist/shaders/ltc_mag_lut.d.ts +0 -3
  115. package/dist/shaders/ltc_mag_lut.d.ts.map +0 -1
  116. package/dist/shaders/ltc_mag_lut.js +0 -1033
  117. package/dist/shaders/materials/body.d.ts +0 -2
  118. package/dist/shaders/materials/body.d.ts.map +0 -1
  119. package/dist/shaders/materials/body.js +0 -95
  120. package/dist/shaders/materials/cloth_rough.d.ts +0 -2
  121. package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
  122. package/dist/shaders/materials/cloth_rough.js +0 -69
  123. package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
  124. package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
  125. package/dist/shaders/materials/cloth_smooth.js +0 -61
  126. package/dist/shaders/materials/default.d.ts +0 -2
  127. package/dist/shaders/materials/default.d.ts.map +0 -1
  128. package/dist/shaders/materials/default.js +0 -43
  129. package/dist/shaders/materials/eye.d.ts +0 -2
  130. package/dist/shaders/materials/eye.d.ts.map +0 -1
  131. package/dist/shaders/materials/eye.js +0 -60
  132. package/dist/shaders/materials/face.d.ts +0 -2
  133. package/dist/shaders/materials/face.d.ts.map +0 -1
  134. package/dist/shaders/materials/face.js +0 -95
  135. package/dist/shaders/materials/hair.d.ts +0 -2
  136. package/dist/shaders/materials/hair.d.ts.map +0 -1
  137. package/dist/shaders/materials/hair.js +0 -90
  138. package/dist/shaders/materials/metal.d.ts +0 -2
  139. package/dist/shaders/materials/metal.d.ts.map +0 -1
  140. package/dist/shaders/materials/metal.js +0 -77
  141. package/dist/shaders/materials/mmd_classic.d.ts +0 -2
  142. package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
  143. package/dist/shaders/materials/mmd_classic.js +0 -66
  144. package/dist/shaders/materials/stockings.d.ts +0 -2
  145. package/dist/shaders/materials/stockings.d.ts.map +0 -1
  146. package/dist/shaders/materials/stockings.js +0 -122
  147. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  148. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  149. package/dist/shaders/passes/physics-debug.js +0 -69
  150. package/src/shaders/dfg_lut.ts +0 -131
  151. package/src/shaders/ltc_mag_lut.ts +0 -1035
@@ -636,11 +636,9 @@ const COMPOSITE_BODY = /* wgsl */ `
636
636
  // ── Film grain, on the SCENE ONLY ─────────────────────────────────────────
637
637
  //
638
638
  // Applied here, before the background is composited under, so it rides on what
639
- // the engine drew and nothing else. That placement is the whole point when the
640
- // background is footage: the plate came off a real sensor and already carries
641
- // its own grain, and a second helping over the top would grade the photograph
642
- // rather than match it. A clean CG figure on a grainy plate is one of the
643
- // loudest tells there is — the noise gives it away long before the geometry.
639
+ // the engine drew and nothing else: a backdrop photo or video already carries
640
+ // its own grain, and a second helping over the top would grade the picture
641
+ // rather than match it.
644
642
  //
645
643
  // Multiplicative and weighted toward the mid-tones, which is how film behaves:
646
644
  // little grain in the blacks, and the highlights clip it off.
@@ -145,7 +145,7 @@ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
145
145
  struct MirrorClip { plane: vec4f, on: f32, _p0: f32, _p1: f32, _p2: f32, };
146
146
  @group(0) @binding(14) var<uniform> clip: MirrorClip;
147
147
  ${WORLD_AMBIENT_WGSL}
148
- ${lightsApi(0, 6, "1u")}
148
+ ${lightsApi(0, 6, "1u", { binding: 15, sampler: "linearSampler" })}
149
149
 
150
150
  struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
151
151
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
@@ -388,9 +388,15 @@ ${src.orient ? ` // the author's own plane (particleOrient): a card on a surfac
388
388
  let o = particleOrient(p, ii);
389
389
  right = o[0];
390
390
  up = o[1];
391
- ` : ""} let s = sin(p.rot);
391
+ ` : ""}${/\bfn\s+particlePivot\s*\(/.test(src.wgsl) ? ` // the card's pivot (particlePivot, in half-size units of the quad): the
392
+ // corners are offset by it BEFORE the turn, so the card turns about its
393
+ // pivot rather than its centre — Unity's renderer pivot, a lighthouse beam
394
+ // whose card starts at the lamp instead of straddling it
395
+ let cp = c + particlePivot(p, ii);
396
+ ` : ` let cp = c;
397
+ `} let s = sin(p.rot);
392
398
  let k = cos(p.rot);
393
- var r = vec2f(c.x * k - c.y * s, c.x * s + c.y * k);
399
+ var r = vec2f(cp.x * k - cp.y * s, cp.x * s + cp.y * k);
394
400
  // Stretched along the direction of travel ON SCREEN — which is not the world
395
401
  // direction once the camera is off-axis. Rain falling straight down is nearly
396
402
  // a point when viewed from above and a long streak from the side, and taking
@@ -402,7 +408,7 @@ ${src.orient ? ` // the author's own plane (particleOrient): a card on a surfac
402
408
  let vlen = length(vec2f(vr, vu));
403
409
  if (vlen > 1e-5) {
404
410
  let d = vec2f(vr, vu) / vlen;
405
- r = vec2f(d.y, -d.x) * c.x + d * (c.y * p.stretch);
411
+ r = vec2f(d.y, -d.x) * cp.x + d * (cp.y * p.stretch);
406
412
  }
407
413
  }
408
414
  let world = p.pos + (right * r.x + up * r.y) * p.size;
@@ -0,0 +1,240 @@
1
+ // The reflection probe's GPU passes, and the sky's prefilter.
2
+ //
3
+ // A game does not reflect the sky alone: it bakes a cube of the stage itself
4
+ // from somewhere in the middle of it, prefilters it down its mips with the
5
+ // specular lobe, and every surface reads it box-projected onto the stage's
6
+ // bounds. The engine does the same once a stage has loaded (Engine:
7
+ // captureReflectionProbe): six renders of the stage into a 128² cube, the
8
+ // sky laid in wherever nothing was drawn, a mip chain, then a GGX convolution
9
+ // into the 64² cube the materials sample (rzProbeSpecular in nodes.ts).
10
+ //
11
+ // The world's sky gets the same convolution down its equirect mips, in place
12
+ // of the box average they were built from.
13
+
14
+ import { NODES_WGSL } from "../materials/nodes"
15
+ import { COMMON_MATERIAL_PRELUDE_WGSL, DISSOLVE_WGSL } from "../materials/common"
16
+
17
+ /** Face size of the capture, and of the cube the materials read. The game
18
+ * bakes 64²; the capture is twice that so the base level is antialiased. */
19
+ export const PROBE_CAPTURE_SIZE = 128
20
+ export const PROBE_SIZE = 64
21
+ /** 64² down to 1²: seven levels, the game's own count. */
22
+ export const PROBE_LEVELS = 7
23
+ export const PROBE_CAPTURE_LEVELS = 8
24
+
25
+ /**
26
+ * The perceptual roughness level m of the probe was convolved with, on
27
+ * Unity's probe curve: m = r·(1.7 − 0.7r)·6 (UNITY_SPECCUBE_LOD_STEPS), the
28
+ * convention the game's baked probes follow and its lookup (×8, clamped at 6)
29
+ * reads against. Level 0 is the unblurred capture.
30
+ */
31
+ export function probeLevelRoughness(level: number): number {
32
+ if (level <= 0) return 0
33
+ const m = Math.min(level, 6)
34
+ return Math.min((10.2 - Math.sqrt(Math.max(104.04 - 16.8 * m, 0))) / 8.4, 1)
35
+ }
36
+
37
+ /**
38
+ * The perceptual roughness level m of the sky's equirect chain was convolved
39
+ * with: the engine's own lookup is lod = sqrt(r)·(levels − 1), so level m is
40
+ * r = (m / (levels − 1))².
41
+ */
42
+ export function skyLevelRoughness(level: number, levels: number): number {
43
+ if (level <= 0 || levels <= 1) return 0
44
+ const t = level / (levels - 1)
45
+ return Math.min(t * t, 1)
46
+ }
47
+
48
+ /** A cube face's direction for a uv on it (uv.y down), in the convention the
49
+ * GPU samples a cube with — the same one the capture cameras render. */
50
+ const FACE_DIR_WGSL = /* wgsl */ `
51
+ fn rz_face_dir(face: u32, uv: vec2f) -> vec3f {
52
+ let s = uv.x * 2.0 - 1.0;
53
+ let t = uv.y * 2.0 - 1.0;
54
+ switch face {
55
+ case 0u: { return normalize(vec3f(1.0, -t, -s)); }
56
+ case 1u: { return normalize(vec3f(-1.0, -t, s)); }
57
+ case 2u: { return normalize(vec3f(s, 1.0, t)); }
58
+ case 3u: { return normalize(vec3f(s, -1.0, -t)); }
59
+ case 4u: { return normalize(vec3f(s, -t, 1.0)); }
60
+ default: { return normalize(vec3f(-s, -t, -1.0)); }
61
+ }
62
+ }
63
+
64
+ struct RzFullOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
65
+
66
+ @vertex fn vs_full(@builtin(vertex_index) i: u32) -> RzFullOut {
67
+ var o: RzFullOut;
68
+ let x = f32(i32(i / 2u) * 4 - 1);
69
+ let y = f32(i32(i % 2u) * 4 - 1);
70
+ o.pos = vec4f(x, y, 0.0, 1.0);
71
+ o.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);
72
+ return o;
73
+ }
74
+ `
75
+
76
+ /** GGX importance sampling, the filtered kind (Křivánek & Colbert): each
77
+ * sample reads the source at the mip whose texel matches its solid angle, so
78
+ * a few dozen taps integrate a wide lobe without the fireflies a point sample
79
+ * of a bright lamp would leave. */
80
+ const GGX_SAMPLING_WGSL = /* wgsl */ `
81
+ fn rz_hammersley(i: u32, n: u32) -> vec2f {
82
+ var b = (i << 16u) | (i >> 16u);
83
+ b = ((b & 0x55555555u) << 1u) | ((b & 0xAAAAAAAAu) >> 1u);
84
+ b = ((b & 0x33333333u) << 2u) | ((b & 0xCCCCCCCCu) >> 2u);
85
+ b = ((b & 0x0F0F0F0Fu) << 4u) | ((b & 0xF0F0F0F0u) >> 4u);
86
+ b = ((b & 0x00FF00FFu) << 8u) | ((b & 0xFF00FF00u) >> 8u);
87
+ return vec2f(f32(i) / f32(n), f32(b) * 2.3283064365386963e-10);
88
+ }
89
+
90
+ /** A half vector about N for the lobe at alpha = a (= perceptual r²). */
91
+ fn rz_ggx_half(xi: vec2f, N: vec3f, a: f32) -> vec3f {
92
+ let phi = 6.283185307 * xi.x;
93
+ let cosT = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));
94
+ let sinT = sqrt(max(1.0 - cosT * cosT, 0.0));
95
+ let h = vec3f(sinT * cos(phi), sinT * sin(phi), cosT);
96
+ let up = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 0.0, 1.0), abs(N.z) < 0.999);
97
+ let tx = normalize(cross(up, N));
98
+ let ty = cross(N, tx);
99
+ return normalize(tx * h.x + ty * h.y + N * h.z);
100
+ }
101
+
102
+ fn rz_ggx_d(nh: f32, a: f32) -> f32 {
103
+ let a2 = a * a;
104
+ let d = nh * nh * (a2 - 1.0) + 1.0;
105
+ return a2 / (3.141592653589793 * d * d);
106
+ }
107
+ `
108
+
109
+ /**
110
+ * The compose step: the stage as captured, with the world's sky laid in
111
+ * behind it wherever the capture's coverage says nothing was drawn — an open
112
+ * stage reflects its sky, an interior its walls. Built on the material
113
+ * prelude for rzWorldSpecularLod, which knows every kind of sky the scene may
114
+ * have (an HDRI, a gradient, one colour). Only the bindings it reads end up in
115
+ * its layout ("auto"): the light uniform, the sampler and the sky at group 0,
116
+ * and its own three past the material set's.
117
+ */
118
+ export const PROBE_COMPOSE_WGSL =
119
+ NODES_WGSL +
120
+ COMMON_MATERIAL_PRELUDE_WGSL +
121
+ DISSOLVE_WGSL +
122
+ FACE_DIR_WGSL +
123
+ /* wgsl */ `
124
+ struct RzProbeStep { face: u32, lod: f32, roughness: f32, srcSize: f32 };
125
+ @group(0) @binding(20) var rzCaptureColor: texture_2d_array<f32>;
126
+ @group(0) @binding(21) var rzCaptureAux: texture_2d_array<f32>;
127
+ @group(0) @binding(22) var<uniform> rzStep: RzProbeStep;
128
+
129
+ @fragment fn fs_compose(in: RzFullOut) -> @location(0) vec4f {
130
+ let size = textureDimensions(rzCaptureColor);
131
+ let px = vec2u(min(in.pos.xy, vec2f(size) - vec2f(1.0)));
132
+ let c = textureLoad(rzCaptureColor, px, rzStep.face, 0).rgb;
133
+ let cover = saturate(textureLoad(rzCaptureAux, px, rzStep.face, 0).g);
134
+ let sky = rzWorldSpecularLod(rz_face_dir(rzStep.face, in.uv), 0.0, 0.0);
135
+ return vec4f(c + sky * (1.0 - cover), 1.0);
136
+ }
137
+ `
138
+
139
+ /**
140
+ * The cube's own passes: a mip from the level above (mode 0 — one bilinear
141
+ * tap at the child texel's direction is the 2×2 average), and the GGX
142
+ * convolution into the probe the materials read (mode 1).
143
+ */
144
+ export const PROBE_FILTER_WGSL =
145
+ FACE_DIR_WGSL +
146
+ GGX_SAMPLING_WGSL +
147
+ /* wgsl */ `
148
+ struct RzProbeStep { face: u32, lod: f32, roughness: f32, srcSize: f32, mode: u32 };
149
+ @group(0) @binding(0) var rzSrc: texture_cube<f32>;
150
+ @group(0) @binding(1) var rzSamp: sampler;
151
+ @group(0) @binding(2) var<uniform> rzStep: RzProbeStep;
152
+
153
+ const RZ_PROBE_SAMPLES: u32 = 128u;
154
+
155
+ @fragment fn fs_filter(in: RzFullOut) -> @location(0) vec4f {
156
+ let N = rz_face_dir(rzStep.face, in.uv);
157
+ let r = rzStep.roughness;
158
+ if (rzStep.mode == 0u || r < 0.002) {
159
+ return vec4f(textureSampleLevel(rzSrc, rzSamp, N, rzStep.lod).rgb, 1.0);
160
+ }
161
+ let a = r * r;
162
+ let texelSolid = 4.0 * 3.141592653589793 / (6.0 * rzStep.srcSize * rzStep.srcSize);
163
+ let maxLod = f32(textureNumLevels(rzSrc) - 1u);
164
+ var sum = vec3f(0.0);
165
+ var weight = 0.0;
166
+ for (var i = 0u; i < RZ_PROBE_SAMPLES; i++) {
167
+ let H = rz_ggx_half(rz_hammersley(i, RZ_PROBE_SAMPLES), N, a);
168
+ let L = 2.0 * dot(N, H) * H - N;
169
+ let nl = dot(N, L);
170
+ if (nl <= 0.0) { continue; }
171
+ // N = V: the pdf over L is D·(N·H)/(4·V·H) = D/4.
172
+ let pdf = rz_ggx_d(saturate(dot(N, H)), a) * 0.25;
173
+ let sampleSolid = 1.0 / (f32(RZ_PROBE_SAMPLES) * pdf + 1e-6);
174
+ let lod = clamp(0.5 * log2(sampleSolid / texelSolid) + 1.0, 0.0, maxLod);
175
+ sum += textureSampleLevel(rzSrc, rzSamp, L, lod).rgb * nl;
176
+ weight += nl;
177
+ }
178
+ return vec4f(sum / max(weight, 1e-4), 1.0);
179
+ }
180
+ `
181
+
182
+ /**
183
+ * The sky's mips, convolved: level m of the equirect the materials read
184
+ * (rzWorldSpecularLod) becomes the GGX-filtered sky at the roughness that
185
+ * level stands for, sampled from the box-averaged chain the upload built.
186
+ * Directions follow the composite's dome: u = 0.5 + atan2(x, z)/2π,
187
+ * v = 0.5 − asin(y)/π.
188
+ */
189
+ export const SKY_PREFILTER_WGSL =
190
+ GGX_SAMPLING_WGSL +
191
+ /* wgsl */ `
192
+ struct RzFullOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
193
+
194
+ @vertex fn vs_full(@builtin(vertex_index) i: u32) -> RzFullOut {
195
+ var o: RzFullOut;
196
+ let x = f32(i32(i / 2u) * 4 - 1);
197
+ let y = f32(i32(i % 2u) * 4 - 1);
198
+ o.pos = vec4f(x, y, 0.0, 1.0);
199
+ o.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);
200
+ return o;
201
+ }
202
+
203
+ struct RzSkyStep { roughness: f32, srcWidth: f32, srcHeight: f32, spare: f32 };
204
+ @group(0) @binding(0) var rzSky: texture_2d<f32>;
205
+ @group(0) @binding(1) var rzSamp: sampler;
206
+ @group(0) @binding(2) var<uniform> rzStep: RzSkyStep;
207
+
208
+ const RZ_SKY_SAMPLES: u32 = 64u;
209
+
210
+ fn rz_sky_dir(uv: vec2f) -> vec3f {
211
+ let phi = (uv.x - 0.5) * 6.283185307;
212
+ let lat = (0.5 - uv.y) * 3.141592653589793;
213
+ return vec3f(cos(lat) * sin(phi), sin(lat), cos(lat) * cos(phi));
214
+ }
215
+
216
+ fn rz_sky_uv(d: vec3f) -> vec2f {
217
+ return vec2f(0.5 + atan2(d.x, d.z) * 0.15915494309, 0.5 - asin(clamp(d.y, -1.0, 1.0)) * 0.31830988618);
218
+ }
219
+
220
+ @fragment fn fs_sky(in: RzFullOut) -> @location(0) vec4f {
221
+ let N = rz_sky_dir(in.uv);
222
+ let a = rzStep.roughness * rzStep.roughness;
223
+ let texelSolid = 4.0 * 3.141592653589793 / (rzStep.srcWidth * rzStep.srcHeight);
224
+ let maxLod = f32(textureNumLevels(rzSky) - 1u);
225
+ var sum = vec3f(0.0);
226
+ var weight = 0.0;
227
+ for (var i = 0u; i < RZ_SKY_SAMPLES; i++) {
228
+ let H = rz_ggx_half(rz_hammersley(i, RZ_SKY_SAMPLES), N, a);
229
+ let L = 2.0 * dot(N, H) * H - N;
230
+ let nl = dot(N, L);
231
+ if (nl <= 0.0) { continue; }
232
+ let pdf = rz_ggx_d(saturate(dot(N, H)), a) * 0.25;
233
+ let sampleSolid = 1.0 / (f32(RZ_SKY_SAMPLES) * pdf + 1e-6);
234
+ let lod = clamp(0.5 * log2(sampleSolid / texelSolid) + 1.0, 0.0, maxLod);
235
+ sum += textureSampleLevel(rzSky, rzSamp, rz_sky_uv(L), lod).rgb * nl;
236
+ weight += nl;
237
+ }
238
+ return vec4f(sum / max(weight, 1e-4), 1.0);
239
+ }
240
+ `
@@ -98,6 +98,12 @@ type SceneRenderClass =
98
98
  * X309's night sky was. Out of WRITES_ID for the mirror's reason — a sheet of
99
99
  * light is not an object anything should be able to pick or outline. */
100
100
  | "material-additive"
101
+ /** A material whose colour arrives ALREADY weighted by its coverage — a
102
+ * game's glass (Blend One OneMinusSrcAlpha): the pane's diffuse fades with
103
+ * its alpha while its reflection does not, which a src-alpha blend cannot
104
+ * express. Out of WRITES_ID for the mirror's reason: a window is not what a
105
+ * click on the figure behind it means. */
106
+ | "material-premultiplied"
101
107
  /** Particles and ribbons in their default, non-additive mode. */
102
108
  | "particle"
103
109
  /** Particles declaring `#blend additive` — LIGHT rather than matter, so
@@ -110,6 +116,13 @@ type SceneRenderClass =
110
116
  * whole, and an ordinary alpha-over aux so a ribbon's mask does not
111
117
  * saturate along every overlap. */
112
118
  | "trail"
119
+ /** Ribbons declaring `#blend over` — MATTER rather than light: laid over
120
+ * what is behind them by their own alpha, the way a game's trail material
121
+ * with Blend One OneMinusSrcAlpha draws. A dark band then darkens, which an
122
+ * additive ribbon can never do; it is how a deep-blue smoke tail reads as a
123
+ * tail at all. The fragment writes straight colour, so the colour blend is
124
+ * src-alpha like any other alpha-over class. */
125
+ | "trail-over"
113
126
  /** The transparent depth prepass: it exists to write DEPTH after the fabric's
114
127
  * colour blended, so an outline drawn later is occluded behind it. It must
115
128
  * therefore write no colour at all — the targets exist only to make the
@@ -183,6 +196,9 @@ const BLENDS: Record<Exclude<SceneRenderClass, "depth-prepass">, [GPUBlendState,
183
196
  // The same pair the additive particles use: light into the colour target, and
184
197
  // coverage that still accumulates so bloom and the composite can see it.
185
198
  "material-additive": [ADD_KEEP_ALPHA, ADD_BOTH],
199
+ // The colour premultiplied by the graph (game_pbr's glass); the aux is
200
+ // written straight, as every material's is.
201
+ "material-premultiplied": [PREMULTIPLIED_OVER, ALPHA_OVER],
186
202
  // PREMULTIPLIED, like the ground and for the same reason: what a mirror
187
203
  // writes is a sample of the HDR target, and that target already holds colour
188
204
  // premultiplied by its own alpha. Handed to the src-alpha blend it would be
@@ -201,6 +217,7 @@ const BLENDS: Record<Exclude<SceneRenderClass, "depth-prepass">, [GPUBlendState,
201
217
  particle: [PREMULTIPLIED_OVER, ALPHA_OVER],
202
218
  "particle-additive": [ADD_KEEP_ALPHA, ADD_BOTH],
203
219
  trail: [ADD_PREMULTIPLIED, ALPHA_OVER],
220
+ "trail-over": [ALPHA_OVER, ALPHA_OVER],
204
221
  }
205
222
 
206
223
  /**
@@ -131,6 +131,7 @@ export function sceneLightApi(on: boolean, group: number, binding: number): stri
131
131
  return /* wgsl */ `
132
132
  fn rzShadow(p: vec3f) -> f32 { return 1.0; }
133
133
  fn rzWorldAmbient(n: vec3f) -> vec3f { return vec3f(0.0); }
134
+ fn rzWorldAmbientAvg() -> vec3f { return vec3f(0.0); }
134
135
  `
135
136
  }
136
137
  const n = SHADOW_CASCADES.length
@@ -71,6 +71,9 @@ export type UnityFrameInput = {
71
71
  /** The direction the main light travels, unit; null for none. */
72
72
  sunDirection: [number, number, number] | null
73
73
  sunColor: [number, number, number]
74
+ /** A game stage's own main light as its pipeline packed it (recorded), which
75
+ * its shaders get in place of sunColor; null for the scene sun's. */
76
+ sunUnity?: { color: number[]; simColor: number[]; simColorNoInt: number[] } | null
74
77
  sunShadow: number
75
78
  lights: UnityLight[]
76
79
  /** Ambient as the engine's folded SH9 (27 floats, ibl.ts), or null for flat. */
@@ -257,12 +260,13 @@ export function unityFrameGlobals(f: UnityFrameInput): Record<string, NativeValu
257
260
  if (f.sunDirection) {
258
261
  const d = gameDir(f.sunDirection)
259
262
  g.SimMainLightDir = [-d[0], -d[1], -d[2], 0]
260
- g.SimMainLightColor = [...f.sunColor, 1]
261
- g.SimMainLightColorNoInt = [...f.sunColor, 1]
263
+ const u = f.sunUnity
264
+ g.SimMainLightColor = u ? u.simColor.slice(0, 4) : [...f.sunColor, 1]
265
+ g.SimMainLightColorNoInt = u ? u.simColorNoInt.slice(0, 4) : [...f.sunColor, 1]
262
266
  // As the game's pipeline leaves it (recorded): the way the light travels.
263
267
  g._MainLightPosition = [d[0], d[1], d[2], 0]
264
268
  g.sim_ShadowLightDirection = [-d[0], -d[1], -d[2], 0]
265
- g._MainLightColor = [...f.sunColor, 1]
269
+ g._MainLightColor = u ? u.color.slice(0, 4) : [...f.sunColor, 1]
266
270
  } else {
267
271
  g.SimMainLightDir = [0, 0, 0, 0]
268
272
  g.SimMainLightColor = [0, 0, 0, 0]
package/src/unity/host.ts CHANGED
@@ -54,6 +54,21 @@ export type NativeValue = number | ArrayLike<number> | ArrayLike<ArrayLike<numbe
54
54
  /** Name -> value; the first source that has a name wins. */
55
55
  export type ValueSource = Record<string, NativeValue | undefined>
56
56
 
57
+ /** Whether two values carry the same numbers (and the same integer-ness). */
58
+ export function sameValue(a: NativeValue, b: NativeValue): boolean {
59
+ if (typeof a === "number" || typeof b === "number") return a === b
60
+ if (a.length !== b.length) return false
61
+ if (a instanceof Uint32Array !== b instanceof Uint32Array) return false
62
+ for (let i = 0; i < a.length; i++) {
63
+ const x = a[i] as number | ArrayLike<number>
64
+ const y = b[i] as number | ArrayLike<number>
65
+ if (typeof x === "number" || typeof y === "number") {
66
+ if (x !== y) return false
67
+ } else if (!sameValue(x, y)) return false
68
+ }
69
+ return true
70
+ }
71
+
57
72
  /** One vertex stream a draw binds, by the semantic a shader input names. */
58
73
  export type NativeStream = {
59
74
  buffer: GPUBuffer
@@ -87,14 +102,25 @@ export function wrapFragment(frag: string, ids: boolean): string {
87
102
  const at = frag.lastIndexOf("@fragment")
88
103
  if (at < 0) throw new Error("native shader: no fragment entry point")
89
104
  const head = frag.slice(at)
90
- const sig = /->\s*@location\(0\)\s*vec4<f32>\s*\{/
91
- if (!sig.test(head)) throw new Error("native shader: the fragment entry does not return one vec4 target")
105
+ // A target narrower than four channels (a float3 SV_Target) leaves the rest
106
+ // unwritten: widened here with zeros and an alpha of one.
107
+ const sig = /->\s*@location\(0\)\s*(vec4<f32>|vec3<f32>|vec2<f32>|f32)\s*\{/
108
+ const kind = sig.exec(head)?.[1]
109
+ if (!kind) throw new Error("native shader: the fragment entry does not return one float target")
92
110
  let body = head.replace(sig, "-> RzNativeOut {")
93
111
  const ret = /return\s+(_e\d+)\s*;\s*\}\s*$/
94
112
  const m = ret.exec(body)
95
113
  if (!m) throw new Error("native shader: the fragment entry's return is not the translator's")
96
- const v = m[1]
97
- body = body.replace(ret, `return RzNativeOut(${v}, vec4f(1.0, 1.0, 0.0, ${v}.a)${ids ? ", vec2u(0u)" : ""});\n}\n`)
114
+ const e = m[1]
115
+ const v =
116
+ kind === "vec4<f32>"
117
+ ? e
118
+ : kind === "vec3<f32>"
119
+ ? `vec4f(${e}, 1.0)`
120
+ : kind === "vec2<f32>"
121
+ ? `vec4f(${e}, 0.0, 1.0)`
122
+ : `vec4f(${e}, 0.0, 0.0, 1.0)`
123
+ body = body.replace(ret, `let rzColor = ${v};\n return RzNativeOut(rzColor, vec4f(1.0, 1.0, 0.0, rzColor.a)${ids ? ", vec2u(0u)" : ""});\n}\n`)
98
124
  const struct = `struct RzNativeOut {\n @location(0) color: vec4f,\n @location(1) mask: vec4f,\n${ids ? " @location(2) id: vec2u,\n" : ""}};\n`
99
125
  // After the module's directives (diagnostic, enable): WGSL wants them first.
100
126
  const pre = frag.slice(0, at)
@@ -190,6 +216,7 @@ export class NativeHost {
190
216
  white: GPUTextureView
191
217
  black: GPUTextureView
192
218
  blackCube: GPUTextureView
219
+ emptyVolume: GPUTextureView
193
220
  depth: GPUTextureView
194
221
  comparison: GPUSampler
195
222
  },
@@ -598,14 +625,17 @@ export class NativeHost {
598
625
  if (!samplers) return this.sampler
599
626
  const bare = name.replace(/^sampler_?/, "")
600
627
  const tex = info.bindings[bare] ? bare : info.bindings[`_${bare}`] ? `_${bare}` : bare
601
- return samplers[tex] ?? this.sampler
628
+ return samplers[tex] ?? samplers[tex.replace(/_+$/, "")] ?? this.sampler
602
629
  }
603
630
 
604
631
  private textureView(name: string, type: string, textures: Record<string, GPUTextureView | undefined>): GPUTextureView {
605
- const v = textures[name]
632
+ // naga renames a binding that ends in a digit with a trailing "_", as it
633
+ // does a member (lookupFrom): _AlbedoTex_1 is declared _AlbedoTex_1_
634
+ const v = textures[name] ?? (name.endsWith("_") ? textures[name.replace(/_+$/, "")] : undefined)
606
635
  if (v) return v
607
636
  if (/texture_depth/.test(type)) return this.fallback.depth
608
637
  if (/texture_cube/.test(type)) return this.fallback.blackCube
638
+ if (/texture_3d/.test(type)) return this.fallback.emptyVolume
609
639
  return this.fallback.white
610
640
  }
611
641