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