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
@@ -134,6 +134,13 @@ struct VertexOutput {
134
134
  /** The scene fog's two amounts, computed PER VERTEX as the game does — see
135
135
  * Engine.setSceneFog — and interpolated across the face. */
136
136
  @location(5) fog: vec2f,
137
+ /** The vertex colour, RGBA — white on a mesh that carries none. A PMX keeps
138
+ * it in its first additional UV (what a converted game stage writes there:
139
+ * a plant's occlusion rides in its alpha). The attribute node reads it. */
140
+ @location(6) vcolor: vec4f,
141
+ /** The second UV set — a PMX's second additional UV (xy), else the first
142
+ * UV again. The uv_map node's uv2. */
143
+ @location(7) uv2: vec2f,
137
144
  };
138
145
 
139
146
  /** One fog layer's amount at a vertex of view depth d and world height y. */
@@ -165,6 +172,18 @@ struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
165
172
  // The cast's own shadow map for a stage's floor — see _rzCastShadow.
166
173
  @group(0) @binding(10) var castShadowMap: texture_depth_2d;
167
174
 
175
+ /** The stage's reflection probe (Engine.captureReflectionProbe): where it was
176
+ * captured (xyz) and its strength (w); the box it projects onto, min with the
177
+ * on-flag in w, then max. Off (boxMin.w 0) the sky stands in. */
178
+ struct ProbeUniforms {
179
+ center: vec4f,
180
+ boxMin: vec4f,
181
+ boxMax: vec4f,
182
+ };
183
+ // A cube, GGX-prefiltered down its mips — see rzProbeSpecular in nodes.ts.
184
+ @group(0) @binding(11) var probeCube: texture_cube<f32>;
185
+ @group(0) @binding(12) var<uniform> probe: ProbeUniforms;
186
+
168
187
  /**
169
188
  * How much of the cast stands between this point and the stage's shadow
170
189
  * direction, 0–1 — setStageCastShadow. A 5×5 comparison filter at 1.5 texels:
@@ -186,7 +205,6 @@ fn _rzCastShadow(wp: vec3f) -> f32 {
186
205
  }
187
206
  return 1.0 - lit / 25.0;
188
207
  }
189
- // binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
190
208
  @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
191
209
  // What this object's lighting is, apart from the scene's, as the game's
192
210
  // pipeline gives each renderer its own:
@@ -303,9 +321,14 @@ const COMMON_VS_WGSL = /* wgsl */ `
303
321
  @location(1) normal: vec3f,
304
322
  @location(2) uv: vec2f,
305
323
  @location(3) joints0: vec4<u32>,
306
- @location(4) weights0: vec4<f32>
324
+ @location(4) weights0: vec4<f32>,
325
+ // The extra stream (Engine: the model's attribute buffer, slot 4).
326
+ @location(5) vcolor: vec4<f32>,
327
+ @location(6) uv2: vec2<f32>
307
328
  ) -> VertexOutput {
308
329
  var output: VertexOutput;
330
+ output.vcolor = vcolor;
331
+ output.uv2 = uv2;
309
332
  let pos4 = vec4f(position, 1.0);
310
333
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
311
334
  let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
@@ -507,4 +530,4 @@ fn rz_dissolve_threshold(restPos: vec3f) -> f32 {
507
530
  // The FSOut struct is NOT in here any more — see commonFsOutWgsl above. Every
508
531
  // consumer of this constant appends it immediately, which is where it was.
509
532
  export const COMMON_MATERIAL_PRELUDE_WGSL =
510
- COMMON_BINDINGS_WGSL + WORLD_AMBIENT_WGSL + lightsApi(0, 6, "objectLight.layers.x") + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL
533
+ COMMON_BINDINGS_WGSL + WORLD_AMBIENT_WGSL + lightsApi(0, 6, "objectLight.layers.x", { binding: 9, sampler: "diffuseSampler" }) + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL
@@ -1,17 +1,10 @@
1
- // Shared WGSL primitives for Blender-style NPR material nodes.
2
- // Every function here maps 1:1 to a Blender shader node type used in the preset JSONs.
1
+ // Shared WGSL primitives for the material graph's nodes: Unity's lighting
2
+ // (Lit, Lambert, the reflection probe) and the colour and vector maths a
3
+ // Shader Graph builds from.
3
4
  // Hand-ported material shaders concatenate this block before their own code.
4
5
 
5
6
  export const NODES_WGSL = /* wgsl */ `
6
7
 
7
- // Baked 64×64 rgba8unorm combined BRDF LUT — created once at engine init by dfg_lut.ts.
8
- // .rg = split-sum DFG (Karis: tint = f0·x + f90·y) → F_brdf_*_scatter
9
- // .ba = Heitz 2016 LTC magnitude (ltc_mag_ggx) → ltc_brdf_scale_from_lut
10
- // Paired with group(0) binding(2) diffuseSampler (linear filter). Sample once per
11
- // fragment via brdf_lut_sample() — callers feed .rg and the whole vec4 into the
12
- // helpers below, halving LUT taps on the default Principled path.
13
- @group(0) @binding(9) var brdfLut: texture_2d<f32>;
14
-
15
8
  // ─── RGB ↔ HSV ──────────────────────────────────────────────────────
16
9
 
17
10
  fn rgb_to_hsv(rgb: vec3f) -> vec3f {
@@ -275,8 +268,23 @@ fn node_normal_map(color: vec3f, strength: f32, N: vec3f, worldPos: vec3f, uv: v
275
268
  let B = safe_normal(cross(N, T));
276
269
  // Tangent-space [0,1] → [-1,1], then into world.
277
270
  let t = color * 2.0 - vec3f(1.0);
278
- let mapped = safe_normal(T * t.x + B * t.y + N * t.z);
279
- return safe_normal(mix(N, mapped, clamp(strength, 0.0, 1.0)));
271
+ // Strength scales the tangent tilt (x, y), as Unity's _NormalScale and
272
+ // Blender's Normal Map strength both do — so it may exceed 1 and steepen a
273
+ // shallow map, which a lerp toward the map could never do.
274
+ let s = max(strength, 0.0);
275
+ return safe_normal(T * (t.x * s) + B * (t.y * s) + N * t.z);
276
+ }
277
+
278
+ /**
279
+ * The same, for a map packed the way game engines store normals (DXT5nm /
280
+ * BC5): X in alpha, Y in green, Z rebuilt from the two. Aether Gazer's stage
281
+ * normals are packed so (and its red channel is a constant 1). A converter
282
+ * normally swizzles these to a plain RGB map; this reads one that was not.
283
+ */
284
+ fn node_normal_map_packed(color: vec4f, strength: f32, N: vec3f, worldPos: vec3f, uv: vec2f) -> vec3f {
285
+ let xy = vec2f(color.a * color.r, color.g) * 2.0 - vec2f(1.0);
286
+ let z = sqrt(max(1.0 - dot(xy, xy), 0.0));
287
+ return node_normal_map(vec3f(xy, z) * 0.5 + vec3f(0.5), strength, N, worldPos, uv);
280
288
  }
281
289
 
282
290
  /** Vector Transform, world ↔ camera. Object space IS world here: a PMX model is
@@ -793,6 +801,15 @@ fn luminance_rec709_linear(c: vec3f) -> f32 {
793
801
  return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
794
802
  }
795
803
 
804
+ /** URP's LightingLambert for the main light, plus the ambient: the light a
805
+ * white surface facing n receives — light·max(N·L, 0)·shadow + ambient, in
806
+ * Unity's units. What a toon shader in Shader Graph ramps. .rgb is the light,
807
+ * .a its luminance (Unity's Luminance, Rec. 709). */
808
+ fn urp_lambert(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> vec4f {
809
+ let c = sun_rgb * (max(dot(n, l), 0.0) * shadow) + ambient_rgb;
810
+ return vec4f(c, luminance_rec709_linear(c));
811
+ }
812
+
796
813
  // ─── FRESNEL node ───────────────────────────────────────────────────
797
814
  // Schlick approximation matching Blender's Fresnel node
798
815
 
@@ -833,36 +850,6 @@ fn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {
833
850
  // Eevee captures lit diffuse: (albedo/π)*sun*N·L*shadow + ambient (linear). Albedo=1.
834
851
  // Matches default.ts direct term scale so VALTORGB thresholds from Blender JSON stay valid.
835
852
 
836
- /**
837
- * Shader → RGB on a white diffuse closure, as a COLOUR.
838
- *
839
- * Blender's ShaderToRGB hands back what the closure actually radiates, and that
840
- * is coloured: a warm sun against a cool ambient gives warm light and cool
841
- * shadow, which is most of what an NPR ramp is reading. Collapsing it to
842
- * luminance first — which this used to do — throws that away and leaves every
843
- * ramp working on grey.
844
- *
845
- * Feeding this into a scalar socket still yields a float, because Blender's own
846
- * implicit Color → Value conversion (BT.601, see color_to_value) does it at the
847
- * link. So a graph that wants the scalar gets Blender's scalar, and one that
848
- * wants the colour now gets that too.
849
- *
850
- * The maths matches 5.2's bxdf_diffuse_eval, which is saturate(dot(N,L))
851
- * integrated against a cosine distribution: radiance = E·N·L/π for the sun, plus
852
- * the world's own irradiance. EEVEE Next's extra transport — probe GI, virtual
853
- * shadow maps, ray-traced bounce — is machinery rather than a term, and is not
854
- * here.
855
- */
856
- fn shader_to_rgb_lit(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> vec3f {
857
- const PI_S: f32 = 3.141592653589793;
858
- let ndotl = max(dot(n, l), 0.0);
859
- return sun_rgb * (ndotl * shadow / PI_S) + ambient_rgb;
860
- }
861
-
862
- fn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> f32 {
863
- return luminance_rec709_linear(shader_to_rgb_lit(n, l, sun_rgb, ambient_rgb, shadow));
864
- }
865
-
866
853
  // ─── SUBSURFACE node ────────────────────────────────────────────────
867
854
  // Marks this fragment for the screen-space scattering pass (passes/subsurface.ts)
868
855
  // and passes its colour through. The strength lands in the aux target's .b via
@@ -1074,41 +1061,12 @@ fn normal_map(strength: f32, map_color: vec3f, normal: vec3f, tangent: vec3f, bi
1074
1061
  return normalize(mix(normal, perturbed, strength));
1075
1062
  }
1076
1063
 
1077
- // ─── EEVEE Principled BSDF primitives ───────────────────────────────
1078
- // Ports from Blender 3.6 source/blender/draw/engines/eevee/shaders/
1079
- // bsdf_common_lib.glsl + gpu_shader_material_principled.glsl.
1080
- // Usage pattern (see material shaders): direct spec = bsdf_ggx × sun × shadow
1081
- // (NL baked in, no F yet); ambient spec = probe_radiance; tint both with
1082
- // reflection_color = F_brdf_multi_scatter(f0, f90, split_sum) AFTER summing.
1083
-
1084
- const EEVEE_PI: f32 = 3.141592653589793;
1085
-
1086
- // Fused analytic GGX specular (direct lights). Returns BRDF × NL.
1087
- // 4·NL·NV is cancelled via G1_Smith reciprocal form — see bsdf_common_lib.glsl:115.
1088
- // Caller passes NL, NV (already computed for diffuse + brdf_lut_sample) so WebKit
1089
- // can reuse them instead of recomputing dot products across the function boundary.
1090
- fn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, NL_in: f32, NV_in: f32, roughness: f32) -> f32 {
1091
- let a = max(roughness, 1e-4);
1092
- let a2 = a * a;
1093
- let H = normalize(L + V);
1094
- let NH = max(dot(N, H), 1e-8);
1095
- let NL = max(NL_in, 1e-8);
1096
- let NV = max(NV_in, 1e-8);
1097
- // G1_Smith_GGX_opti reciprocal form — denominator piece only.
1098
- let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);
1099
- let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);
1100
- let G = G1L * G1V;
1101
- // D_ggx_opti = pi * denom² — reciprocal D × a².
1102
- let tmp = (NH * a2 - NH) * NH + 1.0;
1103
- let D_opti = EEVEE_PI * tmp * tmp;
1104
- return NL * a2 / (D_opti * G);
1105
- }
1064
+ // ─── URP's direct specular ──────────────────────────────────────────
1106
1065
 
1107
1066
  // URP's direct-light specular, as a game on URP (or a fork of it) shades its
1108
1067
  // lamps and sun: D·V·F folded into a2 / (d²·max(0.1, LoH²)·(4a + 2)), a the
1109
- // SQUARED roughness. Unity folds π into its light units, and a light that came
1110
- // from one reaches this engine with that π restored (see the app's stage
1111
- // loader), so it comes back out here. Returns BRDF × NL, like bsdf_ggx.
1068
+ // SQUARED roughness, exactly as URP writes it — this engine's lights are in
1069
+ // Unity's units (LIGHT UNITS). Returns BRDF × NL.
1112
1070
  fn bsdf_urp(N: vec3f, L: vec3f, V: vec3f, NL: f32, roughness: f32) -> f32 {
1113
1071
  let a = max(roughness * roughness, 6.1035156e-5);
1114
1072
  let a2 = a * a;
@@ -1116,94 +1074,10 @@ fn bsdf_urp(N: vec3f, L: vec3f, V: vec3f, NL: f32, roughness: f32) -> f32 {
1116
1074
  let NH = max(dot(N, H), 0.0);
1117
1075
  let LH = max(dot(L, H), 0.0);
1118
1076
  let d = NH * NH * (a2 - 1.0) + 1.00001;
1119
- return a2 / (d * d * max(0.1, LH * LH) * (a * 4.0 + 2.0)) * NL / EEVEE_PI;
1120
- }
1121
-
1122
- /** The direct-light lobe a principled closure asked for: 0 the engine's own,
1123
- * 1 URP's (principled's unity_direct). */
1124
- fn _rzDirectLobe(N: vec3f, L: vec3f, V: vec3f, NL: f32, NV: f32, roughness: f32, unity: f32) -> f32 {
1125
- if (unity > 0.5) { return bsdf_urp(N, L, V, NL, roughness); }
1126
- return bsdf_ggx(N, L, V, NL, NV, roughness);
1127
- }
1128
-
1129
- // Split-sum DFG LUT — Karis 2013 curve fit stand-in for the 64×64 baked LUT.
1130
- // Returns (lut.x, lut.y) in Blender convention: tint = f0·lut.x + f90·lut.y.
1131
- fn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {
1132
- let c0 = vec4f(-1.0, -0.0275, -0.572, 0.022);
1133
- let c1 = vec4f(1.0, 0.0425, 1.04, -0.04);
1134
- let r = roughness * c0 + c1;
1135
- let a004 = min(r.x * r.x, exp2(-9.28 * NV)) * r.x + r.y;
1136
- return vec2f(-1.04, 1.04) * a004 + r.zw;
1137
- }
1138
-
1139
- // Baked combined BRDF LUT — exact port of Blender bsdf_lut_frag.glsl packed with
1140
- // ltc_mag_ggx from eevee_lut.c. Single sample returns DFG (.rg) and LTC mag (.ba).
1141
- // Addressed as Blender's common_utiltex_lib.glsl:lut_coords:
1142
- // coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.
1143
- // Requires group(0) binding(9) brdfLut + binding(2) diffuseSampler in the host shader.
1144
- fn brdf_lut_sample(NV: f32, roughness: f32) -> vec4f {
1145
- let LUT_SIZE: f32 = 64.0;
1146
- var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));
1147
- uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;
1148
- return textureSampleLevel(brdfLut, diffuseSampler, uv, 0.0);
1149
- }
1150
-
1151
- fn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
1152
- return lut.y * f90 + lut.x * f0;
1153
- }
1154
-
1155
- // Fdez-Agüera 2019 multi-scatter compensation (EEVEE do_multiscatter=1).
1156
- fn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
1157
- let FssEss = lut.y * f90 + lut.x * f0;
1158
- let Ess = lut.x + lut.y;
1159
- let Ems = 1.0 - Ess;
1160
- let Favg = f0 + (1.0 - f0) / 21.0;
1161
- let Fms = FssEss * Favg / (1.0 - (1.0 - Ess) * Favg);
1162
- return FssEss + Fms * Ems;
1163
- }
1164
-
1165
- // EEVEE direct-specular energy compensation factor — closure_eval_glossy_lib.glsl:79-81:
1166
- // ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)
1167
- // Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum;
1168
- // direct radiance is rescaled so total-energy matches the split-sum LUT.
1169
- // Takes a pre-sampled vec4f from brdf_lut_sample() to share the fetch with
1170
- // F_brdf_multi_scatter on the same fragment.
1171
- fn ltc_brdf_scale_from_lut(lut: vec4f) -> f32 {
1172
- return (lut.z + lut.w) / max(lut.x + lut.y, 1e-6);
1173
- }
1174
-
1175
- // Luminance-normalized hue extraction — Blender tint_from_color (isolates hue+sat).
1176
- fn tint_from_color(color: vec3f) -> vec3f {
1177
- let lum = dot(color, vec3f(0.3, 0.6, 0.1));
1178
- return select(vec3f(1.0), color / lum, lum > 0.0);
1179
- }
1180
-
1181
- // ─── Principled sheen (gpu_shader_material_principled.glsl:8-14) ────
1182
- // Empirical NV-only curve that approximates grazing retroreflection on cloth/velvet.
1183
- // Scales the sheen layer's diffuse contribution; no sheen call site has sheen=0
1184
- // shortcut because the multiplier is tiny at normal view angles anyway.
1185
- fn principled_sheen(NV: f32) -> f32 {
1186
- let f = 1.0 - NV;
1187
- return f * f * f * 0.077 + f * 0.01 + 0.00026;
1188
- }
1189
-
1190
- // ─── Principled BSDF eval ───────────────────────────────────────────
1191
- // Shared EEVEE Principled path used by every material in the engine — metallic,
1192
- // dielectric, and sheen variants all fold into these ~15 lines via the struct
1193
- // fields. NPR materials still compute a separate toon/rim/warm stack on top and
1194
- // mix(npr_stack, eval_principled(...), fac); see body.ts / face.ts / etc.
1195
- //
1196
- // Field conventions:
1197
- // base — diffuse albedo. Mixed into f0 only when metallic > 0.
1198
- // metallic — 0 = dielectric (f0 from specular), 1 = pure metal (f0 = base).
1199
- // specular — Principled Specular input (0.5 default → f0 = 0.04). sqrt for f90.
1200
- // roughness — GGX roughness; drives BRDF LUT coord + bsdf_ggx.
1201
- // spec_clamp — EEVEE Light Clamp equivalent. Caps firefly spec from noise-bumped
1202
- // NDF aliasing (Blender hides this via TAA which we don't have).
1203
- // Pass 1e30 (effectively disabled) for materials that don't bump.
1204
- // sheen — 0 disables. Scales the sheen diffuse add; cloth/stockings use ~0.7.
1205
- // sheen_tint — 0 = white sheen, 1 = fully tinted by base. Multiplied by sheen,
1206
- // so value is don't-care when sheen=0.
1077
+ return a2 / (d * d * max(0.1, LH * LH) * (a * 4.0 + 2.0)) * NL;
1078
+ }
1079
+
1080
+
1207
1081
  // The PMX sphere map, applied the way MMD applies it: the texture is a
1208
1082
  // VIEW-SPACE lighting mask, sampled by the camera-space normal rather than by
1209
1083
  // any UV the mesh carries, which is why it tracks the viewer and reads as a
@@ -1225,20 +1099,6 @@ fn pmx_sphere_map(base: vec3f, strength: f32, N: vec3f) -> vec3f {
1225
1099
  return base + sphere_rgb * amount;
1226
1100
  }
1227
1101
 
1228
- /**
1229
- * Principled v2 reflectance: IOR sets it, the level scales it.
1230
- *
1231
- * Blender 3.6 took Specular directly, where 0.5 meant f0 = 0.04. v2 computes
1232
- * f0 from IOR — ((ior-1)/(ior+1))² — and Specular IOR Level scales that, with
1233
- * 0.5 meaning "the IOR's own value". Returned in the 3.6 convention the BSDF
1234
- * below still speaks (f0 = 0.08 · specular), so the two agree at their defaults
1235
- * and v2 wins whenever a preset moves either socket.
1236
- */
1237
- fn principled_specular(ior: f32, level: f32) -> f32 {
1238
- let r = (ior - 1.0) / max(ior + 1.0, 1e-6);
1239
- return (r * r * 2.0 * max(level, 0.0)) / 0.08;
1240
- }
1241
-
1242
1102
  // ─── The scene's lamps, on a PBR closure ───────────────────────────
1243
1103
  //
1244
1104
  // A lamp reaches a material as rzLightsDiffuse, added outside the graph and
@@ -1248,7 +1108,7 @@ fn principled_specular(ior: f32, level: f32) -> f32 {
1248
1108
  // REFLECTS a lamp instead of having it painted on — without this a ported
1249
1109
  // stage is a photograph of itself, lit but never shining.
1250
1110
  //
1251
- // Same reach as _rzLightOne: the inverse square past RZ_LAMP_NEAR, the
1111
+ // Same reach as _rzLightOne: the inverse square past the lamp's bulb, the
1252
1112
  // (1 − (d/R)⁴)² window that makes the radius real, the squared cone. N·L is
1253
1113
  // inside bsdf_ggx. Declared here for bsdf_ggx and resolved against the lights
1254
1114
  // API the material prelude brings; the ground pass takes that API without
@@ -1263,7 +1123,7 @@ fn principled_specular(ior: f32, level: f32) -> f32 {
1263
1123
  // second pass over the grid. See lights.ts.
1264
1124
  struct _RzLampPair { d: vec3f, s: vec3f };
1265
1125
 
1266
- fn _rzLampOne(i: u32, p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32, unity: f32) -> _RzLampPair {
1126
+ fn _rzLampOne(i: u32, p: vec3f, n: vec3f, v: vec3f, roughness: f32) -> _RzLampPair {
1267
1127
  var out = _RzLampPair(vec3f(0.0), vec3f(0.0));
1268
1128
  if (!_rzLightReaches(i)) { return out; }
1269
1129
  let pr = _rzLightVec(i, 0u);
@@ -1277,140 +1137,200 @@ fn _rzLampOne(i: u32, p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32, un
1277
1137
  let t = clamp(dist / max(pr.w, 1e-4), 0.0, 1.0);
1278
1138
  let t2 = t * t;
1279
1139
  let window = 1.0 - t2 * t2;
1280
- let falloff = window * window / max(dist * dist, RZ_LAMP_NEAR * RZ_LAMP_NEAR);
1140
+ let bulb = rzLightNear(i);
1141
+ let falloff = window * window / max(dist * dist, bulb * bulb);
1281
1142
  let cone = rzLightCone(i);
1282
1143
  let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
1283
- let c = rzLightColor(i);
1144
+ let c = rzLightColor(i) * rzLightCookie(i, toLight);
1284
1145
  if (ndlD > 0.0) { out.d = c * (ndlD * falloff * aim * aim); }
1285
- if (ndl > 0.0) { out.s = c * (_rzDirectLobe(n, toLight, v, ndl, ndv, roughness, unity) * falloff * aim * aim); }
1146
+ if (ndl > 0.0) { out.s = c * (bsdf_urp(n, toLight, v, ndl, roughness) * falloff * aim * aim); }
1286
1147
  return out;
1287
1148
  }
1288
1149
 
1289
- fn _rzLampWord(bits0: u32, base: u32, p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32, unity: f32) -> _RzLampPair {
1150
+ fn _rzLampWord(bits0: u32, base: u32, p: vec3f, n: vec3f, v: vec3f, roughness: f32) -> _RzLampPair {
1290
1151
  var acc = _RzLampPair(vec3f(0.0), vec3f(0.0));
1291
1152
  var bits = bits0;
1292
1153
  loop {
1293
1154
  if (bits == 0u) { break; }
1294
1155
  let i = base + firstTrailingBit(bits);
1295
1156
  bits = bits & (bits - 1u);
1296
- let one = _rzLampOne(i, p, n, v, ndv, roughness, unity);
1157
+ let one = _rzLampOne(i, p, n, v, roughness);
1297
1158
  acc.d = acc.d + one.d;
1298
1159
  acc.s = acc.s + one.s;
1299
1160
  }
1300
1161
  return acc;
1301
1162
  }
1302
1163
 
1303
- fn rzLampsSpecular(p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32, unity: f32) -> vec3f {
1164
+ fn rzLampsSpecular(p: vec3f, n: vec3f, v: vec3f, roughness: f32) -> vec3f {
1304
1165
  var d = vec3f(0.0);
1305
1166
  var s = vec3f(0.0);
1306
1167
  let count = rzLightCount();
1307
1168
  let docs = _rzLightDocCount();
1308
1169
  if (docs > 0u) {
1309
1170
  let m = _rzLightCellMask(p);
1310
- let wx = _rzLampWord(m.x, 0u, p, n, v, ndv, roughness, unity);
1311
- let wy = _rzLampWord(m.y, 32u, p, n, v, ndv, roughness, unity);
1312
- let wz = _rzLampWord(m.z, 64u, p, n, v, ndv, roughness, unity);
1313
- let ww = _rzLampWord(m.w, 96u, p, n, v, ndv, roughness, unity);
1314
- d = d + wx.d + wy.d + wz.d + ww.d;
1315
- s = s + wx.s + wy.s + wz.s + ww.s;
1171
+ let w0 = _rzLampWord(m.lo.x, 0u, p, n, v, roughness);
1172
+ let w1 = _rzLampWord(m.lo.y, 32u, p, n, v, roughness);
1173
+ let w2 = _rzLampWord(m.lo.z, 64u, p, n, v, roughness);
1174
+ let w3 = _rzLampWord(m.lo.w, 96u, p, n, v, roughness);
1175
+ let w4 = _rzLampWord(m.hi.x, 128u, p, n, v, roughness);
1176
+ let w5 = _rzLampWord(m.hi.y, 160u, p, n, v, roughness);
1177
+ let w6 = _rzLampWord(m.hi.z, 192u, p, n, v, roughness);
1178
+ let w7 = _rzLampWord(m.hi.w, 224u, p, n, v, roughness);
1179
+ d = d + w0.d + w1.d + w2.d + w3.d + w4.d + w5.d + w6.d + w7.d;
1180
+ s = s + w0.s + w1.s + w2.s + w3.s + w4.s + w5.s + w6.s + w7.s;
1316
1181
  }
1317
1182
  for (var i = docs; i < count; i = i + 1u) {
1318
- let one = _rzLampOne(i, p, n, v, ndv, roughness, unity);
1183
+ let one = _rzLampOne(i, p, n, v, roughness);
1319
1184
  d = d + one.d;
1320
1185
  s = s + one.s;
1321
1186
  }
1322
1187
  // The first closure's walk speaks for the fragment; a second principled in
1323
1188
  // the same graph shares p and the prelude normal, so its answer is the same.
1324
1189
  if (!_rzLampDiffuseSet) {
1325
- _rzLampDiffuse = d * (1.0 / 3.141592653589793);
1190
+ _rzLampDiffuse = d;
1326
1191
  _rzLampDiffuseSet = true;
1327
1192
  }
1328
1193
  return s;
1329
1194
  }
1330
1195
 
1331
- struct PrincipledIn {
1332
- base: vec3f,
1333
- metallic: f32,
1334
- specular: f32,
1335
- roughness: f32,
1336
- spec_clamp: f32,
1337
- sheen: f32,
1338
- sheen_tint: f32,
1339
- reflection_lod: f32,
1340
- unity_direct: f32,
1341
- };
1342
-
1343
- fn eval_principled(
1344
- p: PrincipledIn,
1345
- N: vec3f, L: vec3f, V: vec3f,
1346
- sun_rgb: vec3f, amb_rgb: vec3f, shadow: f32, wp: vec3f
1347
- ) -> vec3f {
1348
- let NL = max(dot(N, L), 0.0);
1349
- let NV = max(dot(N, V), 1e-4);
1196
+ // ─── Lit: URP's PBR, term for term ─────────────
1197
+ //
1198
+ // What Unity's Lit shader (URP's UniversalFragmentPBR) computes, so a
1199
+ // surface authored with its inputs looks the way it does in Unity: stage,
1200
+ // props, scenery and weather. Light units are Unity's (see LIGHT UNITS in
1201
+ // lights.ts) — no 1/π anywhere.
1202
+
1203
+ /** URP's EnvironmentBRDFSpecular: the reflection's reflectance —
1204
+ * 1/(α² + 1)·mix(f0, grazing, (1 − N·V)⁴), grazing = saturate(s + reflectivity). */
1205
+ fn urp_env_brdf(f0: vec3f, roughness: f32, smoothness: f32, reflectivity: f32, NV: f32) -> vec3f {
1206
+ let a = max(roughness * roughness, 0.0078125);
1207
+ let grazing = saturate(smoothness + reflectivity);
1208
+ let f = pow(1.0 - NV, 4.0);
1209
+ return (1.0 / (a * a + 1.0)) * mix(f0, vec3f(grazing), f);
1210
+ }
1211
+
1212
+ /**
1213
+ * A box projection: where a ray from p along d leaves the box, seen from the
1214
+ * probe's capture point. What turns one cube captured at the middle of a room
1215
+ * into a reflection that lines up with that room from anywhere inside it —
1216
+ * the floor near the bar reflects the bar, not whatever was opposite the
1217
+ * capture point. A point outside the box (or a box of zero size) keeps the
1218
+ * plain direction.
1219
+ */
1220
+ fn rz_box_project(d: vec3f, p: vec3f, bmin: vec3f, bmax: vec3f, c: vec3f) -> vec3f {
1221
+ let dd = select(d, vec3f(1e-5), abs(d) < vec3f(1e-5));
1222
+ let far = max((bmax - p) / dd, (bmin - p) / dd);
1223
+ let t = min(min(far.x, far.y), far.z);
1224
+ let inside = all(p > bmin) && all(p < bmax);
1225
+ return select(d, normalize(p + d * t - c), inside && t > 0.0);
1226
+ }
1227
+
1228
+ /**
1229
+ * The scene's reflection: the stage's own reflection probe where the engine
1230
+ * captured one (Engine reflection probe — rendered from the stage's middle,
1231
+ * GGX-prefiltered, box-projected to the stage's bounds), else the world's sky.
1232
+ *
1233
+ * ROUGHNESS PICKS THE MIP ON UNITY'S CURVE, r·(1.7 − 0.7r)·6 on a cube of
1234
+ * seven levels (64² down to 1²) — URP's PerceptualRoughnessToMipmapLevel with
1235
+ * UNITY_SPECCUBE_LOD_STEPS 6, which the prefilter that built the levels assumed.
1236
+ */
1237
+ fn rzProbeSpecular(dir: vec3f, wp: vec3f, roughness: f32) -> vec3f {
1238
+ let r = clamp(roughness, 0.0, 1.0);
1239
+ if (probe.boxMin.w < 0.5) { return rzWorldSpecularLod(dir, r, 1.0); }
1240
+ let levels = f32(textureNumLevels(probeCube) - 1u);
1241
+ let lod = min(r * (1.7 - 0.7 * r) * 6.0, levels);
1242
+ let d = rz_box_project(normalize(dir), wp, probe.boxMin.xyz, probe.boxMax.xyz, probe.center.xyz);
1243
+ return textureSampleLevel(probeCube, diffuseSampler, d, lod).rgb * probe.center.w;
1244
+ }
1350
1245
 
1351
- // f0/f90 per gpu_shader_material_principled.glsl. specular_tint=0 is assumed
1352
- // (all presets in this engine use the default white dielectric tint).
1353
- let dielectric_f0 = vec3f(0.08 * p.specular);
1354
- let f0 = mix(dielectric_f0, p.base, p.metallic);
1355
- let f90 = mix(f0, vec3f(1.0), sqrt(p.specular));
1356
-
1357
- // Single LUT tap feeds both F_brdf_multi_scatter (split-sum DFG) and
1358
- // ltc_brdf_scale_from_lut (LTC mag in .ba). See nodes.ts brdf_lut_sample.
1359
- let lut = brdf_lut_sample(NV, p.roughness);
1360
- let reflection_color = F_brdf_multi_scatter(f0, f90, lut.xy);
1361
-
1362
- // Direct glossy — bsdf_ggx already includes NL; no F applied here (tinted after
1363
- // accum with reflection_color). ltc_brdf_scale rescales direct to match the
1364
- // split-sum indirect path, matching EEVEE closure_eval_glossy_lib behavior.
1365
- // The sun, and every lamp that reaches this point. One clamp over the pair:
1366
- // a candle a hand's width from a polished floor is a firefly otherwise.
1367
- // URP's lobe carries its own normalisation, so the LTC rescale that matches
1368
- // EEVEE's direct light to its split-sum indirect does not apply to it.
1369
- let direct_scale = select(ltc_brdf_scale_from_lut(lut), 1.0, p.unity_direct > 0.5);
1370
- let spec_direct_raw = (_rzDirectLobe(N, L, V, NL, NV, p.roughness, p.unity_direct) * sun_rgb * shadow
1371
- + rzLampsSpecular(wp, N, V, NV, p.roughness, p.unity_direct))
1372
- * direct_scale;
1373
- let spec_direct = min(spec_direct_raw, vec3f(p.spec_clamp));
1374
- // Indirect specular reads the world ALONG THE REFLECTION, at a roughness-
1375
- // picked level — EEVEE's probe_evaluate_world_spec, where the diffuse half
1376
- // below is probe_evaluate_world_diff along the normal. Taking the diffuse
1377
- // answer for both gave every surface the same flat wash whatever its
1378
- // roughness, so a chrome rail mirrored what a matte wall did and a stage's
1379
- // metal only ever lost its diffuse. A uniform sky gives the same number
1380
- // either way, so this moves direction, not energy.
1381
- let spec_indirect = rzWorldSpecularLod(reflect(-V, N), p.roughness, p.reflection_lod);
1382
- // URP tints a lamp's highlight by f0 alone — its D·V·F already folds the
1383
- // Fresnel in — where the split-sum reflectance grows toward grazing; applied
1384
- // to the direct term it made a far glossy floor several times brighter than
1385
- // the game draws it. Reflections keep the split sum either way.
1386
- let direct_tint = select(reflection_color, f0, p.unity_direct > 0.5);
1387
- let spec_radiance = spec_direct * direct_tint + spec_indirect * reflection_color;
1388
-
1389
- // Sheen add — when p.sheen=0 the whole term collapses, leaving diffuse_color=base.
1390
- let base_tint = tint_from_color(p.base);
1391
- let sheen_color = mix(vec3f(1.0), base_tint, p.sheen_tint);
1392
- let diffuse_color = p.base + p.sheen * sheen_color * principled_sheen(NV);
1393
-
1394
- // diffuse_weight = (1-metallic). Indirect diffuse uses amb (L_w) with no π factor
1395
- // (probe_evaluate_world_diff returns SH-projected radiance, not cosine-convolved).
1396
- let diffuse_weight = 1.0 - p.metallic;
1397
- var diffuse_radiance = diffuse_color * (sun_rgb * NL * shadow / EEVEE_PI + amb_rgb) * diffuse_weight;
1398
- // The lamps' diffuse, in Unity mode, as URP shades it: THIS surface's albedo
1399
- // — its tint and its maps, not the raw texture the epilogue would use —
1400
- // times 0.96·(1 − metal). X340's floor wears a 0.588 tint the epilogue never
1401
- // saw, and every lamp on it landed 1.7× too bright.
1402
- if (p.unity_direct > 0.5 && !_rzLampDiffuseTaken) {
1403
- diffuse_radiance += p.base * (0.96 * (1.0 - p.metallic)) * rzLightsDiffuseOnce(wp, N);
1246
+ /**
1247
+ * URP's Lit, whole: the sun and every lamp, the ambient, the reflection and
1248
+ * the emission, as UniversalFragmentPBR adds them.
1249
+ *
1250
+ * diffuse = base·(1 − f)·(1 − m) specular = mix(f, base, m), f = 0.08·spec
1251
+ * (spec 0.5 is URP's own dielectric 0.04 — Unreal's and HDRP's Specular)
1252
+ * direct = (diffuse + specular·DirectBRDFSpecular)·light·N·L·shadow
1253
+ * indirect = (diffuse·SH(n) + probe(R, mip(1 − s))·EnvironmentBRDF)·occlusion
1254
+ * out = direct + indirect + emission
1255
+ *
1256
+ * Alpha below 1 is Transparent with Preserve Specular Lighting (URP's
1257
+ * _ALPHAPREMULTIPLY_ON): the diffuse is weighted by alpha, reflection and
1258
+ * highlights are not, and the coverage is alpha·0.96(1 − m) + reflectivity —
1259
+ * a window keeps its reflections at any transparency. Pair it with a group
1260
+ * blending "premultiplied". The result is .rgb and that coverage is .a.
1261
+ *
1262
+ * As the surface (the graph's output), the lamps' diffuse is
1263
+ * shaded here by THIS surface's albedo and the epilogue adds none; as a layer
1264
+ * of a larger look it leaves them to the epilogue. The cast's shadow on a
1265
+ * stage multiplies the lit part.
1266
+ */
1267
+ fn urp_lit(base: vec3f, metallic: f32, smoothness: f32, spec: f32, occlusion: f32, emission: vec3f, alpha: f32,
1268
+ N: vec3f, L: vec3f, V: vec3f, sun_rgb: vec3f, amb_rgb: vec3f, shadow: f32, wp: vec3f,
1269
+ surface: bool) -> vec4f {
1270
+ let m = clamp(metallic, 0.0, 1.0);
1271
+ let sm = clamp(smoothness, 0.0, 1.0);
1272
+ let r = 1.0 - sm;
1273
+ let a = clamp(alpha, 0.0, 1.0);
1274
+ let NL = saturate(dot(N, L));
1275
+ let NV = max(dot(N, V), 1e-4);
1276
+ let f = 0.08 * clamp(spec, 0.0, 1.0);
1277
+ let oneMinusReflectivity = (1.0 - f) * (1.0 - m);
1278
+ let reflectivity = 1.0 - oneMinusReflectivity;
1279
+ let diffuse = base * oneMinusReflectivity * a;
1280
+ let specular = mix(vec3f(f), base, m);
1281
+ // The lamps first: their walk caches the diffuse layer it passes over.
1282
+ let lampSpec = rzLampsSpecular(wp, N, V, r);
1283
+ var lampDiff = vec3f(0.0);
1284
+ if (surface) {
1285
+ lampDiff = rzLightsDiffuseOnce(wp, N);
1404
1286
  _rzLampDiffuseTaken = true;
1405
1287
  }
1288
+ let ao = clamp(occlusion, 0.0, 1.0);
1289
+ let direct = diffuse * (sun_rgb * (NL * shadow) + lampDiff)
1290
+ + specular * (bsdf_urp(N, L, V, NL, r) * sun_rgb * shadow + lampSpec);
1291
+ let indirect = diffuse * amb_rgb * ao
1292
+ + rzProbeSpecular(reflect(-V, N), wp, r) * ao * urp_env_brdf(specular, r, sm, reflectivity, NV);
1293
+ var lit = direct + indirect;
1294
+ lit = lit * mix(vec3f(1.0), light.castShadow.rgb, _rzCastShadow(wp) * light.castShadow.w);
1295
+ return vec4f(lit + max(emission, vec3f(0.0)), a * oneMinusReflectivity + reflectivity);
1296
+ }
1406
1297
 
1407
- // The cast's shadow on a stage (Unity mode): the game lays its character
1408
- // shadow over the whole lit surface as a multiply toward the shadow colour.
1409
- var lit = diffuse_radiance + spec_radiance;
1410
- if (p.unity_direct > 0.5) {
1411
- lit = lit * mix(vec3f(1.0), light.castShadow.rgb, _rzCastShadow(wp) * light.castShadow.w);
1412
- }
1413
- return lit;
1298
+ /**
1299
+ * A packed property map, read the game's way: R metal, G perceptual
1300
+ * roughness (answered as Unity's smoothness, 1 − r), B occlusion, A emission
1301
+ * mask, each remapped through the material's own range — v = min +
1302
+ * (max − min)·tex for the first three, and
1303
+ * e = max((A − min)/(max − min), 0) for the glow, so max is the alpha that
1304
+ * means "full glow" and anything brighter overdrives. A min equal to its max
1305
+ * turns the glow off (the game's default, 1 and 1).
1306
+ *
1307
+ * A slot with no map in it (the 1×1 stand-in) answers with the constants
1308
+ * instead, which is how one look serves a set that brought maps and a model
1309
+ * that did not. Returns (metal, smoothness, occlusion, emission).
1310
+ */
1311
+ fn rz_property_map(t: vec4f, bound: bool, consts: vec4f, mn: vec4f, mx: vec4f) -> vec4f {
1312
+ if (!bound) { return consts; }
1313
+ let mro = mn.xyz + (mx.xyz - mn.xyz) * t.xyz;
1314
+ let span = mx.w - mn.w;
1315
+ let e = select(0.0, max((t.w - mn.w) / span, 0.0), abs(span) > 1e-6);
1316
+ return vec4f(mro.x, 1.0 - mro.y, mro.z, e);
1317
+ }
1318
+
1319
+ /** The highlights alone: the sun's and every lamp's specular on a GGX lobe
1320
+ * (URP's, α = r²), untinted — light, to be multiplied by whatever the
1321
+ * surface's reflectance is. What a water or a glass shader adds as its glint
1322
+ * on top of a reflection it computes itself. */
1323
+ fn rz_glossy_direct(N: vec3f, L: vec3f, V: vec3f, sun_rgb: vec3f, shadow: f32, wp: vec3f, roughness: f32) -> vec3f {
1324
+ let r = clamp(roughness, 0.0, 1.0);
1325
+ let NL = saturate(dot(N, L));
1326
+ return bsdf_urp(N, L, V, NL, r) * sun_rgb * shadow + rzLampsSpecular(wp, N, V, r);
1414
1327
  }
1415
1328
 
1329
+ /** Does group slot k hold a real image, rather than the 1×1 stand-in a
1330
+ * missing map is bound as? */
1331
+ fn rz_group_bound0() -> bool { return textureDimensions(groupTexture0).x > 1u; }
1332
+ fn rz_group_bound1() -> bool { return textureDimensions(groupTexture1).x > 1u; }
1333
+ fn rz_group_bound2() -> bool { return textureDimensions(groupTexture2).x > 1u; }
1334
+ fn rz_group_bound3() -> bool { return textureDimensions(groupTexture3).x > 1u; }
1335
+
1416
1336
  `;