@driftengine/core 4.8.5 → 4.8.7

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 (214) hide show
  1. package/CHANGELOG.json +4392 -4250
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +12 -4
  4. package/dist/index.js +12 -2
  5. package/dist/render/ambientHarmonics.d.ts +46 -0
  6. package/dist/render/ambientHarmonics.js +80 -0
  7. package/dist/render/anisotropicLobe.d.ts +10 -0
  8. package/dist/render/anisotropicLobe.js +19 -0
  9. package/dist/render/areaLights.d.ts +54 -0
  10. package/dist/render/areaLights.js +55 -0
  11. package/dist/render/backend/api.d.ts +43 -2
  12. package/dist/render/backend/webgl2/instanced.d.ts +2 -0
  13. package/dist/render/backend/webgl2/instanced.js +11 -2
  14. package/dist/render/backend/webgl2/renderer.d.ts +120 -11
  15. package/dist/render/backend/webgl2/renderer.js +356 -53
  16. package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
  17. package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
  18. package/dist/render/backend/webgl2/skinScatter.d.ts +14 -0
  19. package/dist/render/backend/webgl2/skinScatter.js +88 -26
  20. package/dist/render/backend/webgpu/buffers.js +2 -1
  21. package/dist/render/backend/webgpu/clusterBinner.js +3 -2
  22. package/dist/render/backend/webgpu/flatPass.d.ts +7 -5
  23. package/dist/render/backend/webgpu/flatPass.js +15 -13
  24. package/dist/render/backend/webgpu/particlePass.d.ts +37 -4
  25. package/dist/render/backend/webgpu/particlePass.js +81 -12
  26. package/dist/render/backend/webgpu/postPass.d.ts +6 -1
  27. package/dist/render/backend/webgpu/postPass.js +7 -2
  28. package/dist/render/backend/webgpu/renderer.d.ts +93 -8
  29. package/dist/render/backend/webgpu/renderer.js +483 -93
  30. package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
  31. package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
  32. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  33. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.js +41 -1
  34. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +40 -28
  35. package/dist/render/backend/webgpu/skinPaletteRing.js +69 -32
  36. package/dist/render/backend/webgpu/skinScatterPass.d.ts +10 -2
  37. package/dist/render/backend/webgpu/skinScatterPass.js +59 -9
  38. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -1
  39. package/dist/render/backend/webgpu/surfaceTexturePass.js +45 -5
  40. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  41. package/dist/render/bloomChain.d.ts +44 -0
  42. package/dist/render/bloomChain.js +28 -0
  43. package/dist/render/bloomPass.d.ts +3 -1
  44. package/dist/render/bloomPass.js +16 -2
  45. package/dist/render/clusteredLights.d.ts +78 -6
  46. package/dist/render/clusteredLights.js +214 -13
  47. package/dist/render/driftLight/bake.d.ts +2 -0
  48. package/dist/render/driftLight/bake.js +1 -1
  49. package/dist/render/driftLight/falloff.d.ts +4 -2
  50. package/dist/render/driftLight/falloff.js +8 -2
  51. package/dist/render/fixtureAtlas.js +1 -1
  52. package/dist/render/gi/bounceLights.d.ts +3 -2
  53. package/dist/render/gi/bounceLights.js +8 -3
  54. package/dist/render/instanceCull.js +12 -0
  55. package/dist/render/instances.d.ts +47 -11
  56. package/dist/render/instances.js +24 -9
  57. package/dist/render/lightBudget.d.ts +38 -2
  58. package/dist/render/lightBudget.js +48 -5
  59. package/dist/render/lightmap.d.ts +116 -0
  60. package/dist/render/lightmap.js +140 -0
  61. package/dist/render/mesh.js +2 -1
  62. package/dist/render/particleBatch.d.ts +59 -3
  63. package/dist/render/particleBatch.js +53 -2
  64. package/dist/render/particleMaterial.d.ts +6 -1
  65. package/dist/render/particleMaterial.js +10 -2
  66. package/dist/render/particlePool.d.ts +32 -0
  67. package/dist/render/particlePool.js +9 -0
  68. package/dist/render/particleSort.d.ts +34 -0
  69. package/dist/render/particleSort.js +96 -0
  70. package/dist/render/pointLightSelection.d.ts +13 -0
  71. package/dist/render/pointLightSelection.js +3 -1
  72. package/dist/render/renderQuality.d.ts +8 -7
  73. package/dist/render/rendererHarness.js +3 -0
  74. package/dist/render/rgb9e5.d.ts +22 -0
  75. package/dist/render/rgb9e5.js +47 -0
  76. package/dist/render/sceneCapture.d.ts +29 -0
  77. package/dist/render/sceneCapture.js +36 -0
  78. package/dist/render/sceneTarget.d.ts +5 -0
  79. package/dist/render/sceneTarget.js +8 -1
  80. package/dist/render/screenSpaceReflection.d.ts +27 -1
  81. package/dist/render/screenSpaceReflection.js +9 -0
  82. package/dist/render/shaders/bloom.d.ts +8 -4
  83. package/dist/render/shaders/bloom.js +20 -3
  84. package/dist/render/shaders/bolt.d.ts +1 -1
  85. package/dist/render/shaders/bolt.js +7 -1
  86. package/dist/render/shaders/depth.js +11 -0
  87. package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
  88. package/dist/render/shaders/flat/anisotropic.js +37 -3
  89. package/dist/render/shaders/flat/areaLight.d.ts +10 -0
  90. package/dist/render/shaders/flat/areaLight.js +142 -0
  91. package/dist/render/shaders/flat/glassTint.d.ts +3 -2
  92. package/dist/render/shaders/flat/glassTint.js +3 -1
  93. package/dist/render/shaders/flat/index.d.ts +5 -0
  94. package/dist/render/shaders/flat/index.js +46 -6
  95. package/dist/render/shaders/flat/lobes.d.ts +6 -0
  96. package/dist/render/shaders/flat/lobes.js +49 -43
  97. package/dist/render/shaders/flat/main.d.ts +1 -1
  98. package/dist/render/shaders/flat/main.js +158 -121
  99. package/dist/render/shaders/flat/models.d.ts +21 -6
  100. package/dist/render/shaders/flat/models.js +161 -17
  101. package/dist/render/shaders/flat/normalMap.d.ts +10 -0
  102. package/dist/render/shaders/flat/normalMap.js +25 -0
  103. package/dist/render/shaders/flat/preamble.js +84 -40
  104. package/dist/render/shaders/flat/skin.js +6 -5
  105. package/dist/render/shaders/generated/bloom.wgsl.d.ts +13 -3
  106. package/dist/render/shaders/generated/bloom.wgsl.js +13 -3
  107. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  108. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  109. package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
  110. package/dist/render/shaders/generated/depth.wgsl.js +4 -4
  111. package/dist/render/shaders/generated/flat.wgsl.d.ts +1647 -2044
  112. package/dist/render/shaders/generated/flat.wgsl.js +1685 -2082
  113. package/dist/render/shaders/generated/particle.wgsl.d.ts +115 -1
  114. package/dist/render/shaders/generated/particle.wgsl.js +115 -1
  115. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +12 -2
  116. package/dist/render/shaders/generated/skinBlur.wgsl.js +12 -2
  117. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
  118. package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
  119. package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
  120. package/dist/render/shaders/generated/water.wgsl.js +23 -16
  121. package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
  122. package/dist/render/shaders/particle.d.ts +17 -1
  123. package/dist/render/shaders/particle.js +133 -1
  124. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/recon/motion.wgsl.js +13 -2
  126. package/dist/render/shaders/skinBlur.js +25 -3
  127. package/dist/render/shaders/ssrTrace.js +20 -1
  128. package/dist/render/shaders/water.d.ts +1 -1
  129. package/dist/render/shaders/water.js +12 -3
  130. package/dist/render/skinBlur.d.ts +8 -5
  131. package/dist/render/skinBlur.js +8 -5
  132. package/dist/render/skinScatter.d.ts +3 -1
  133. package/dist/render/skinScatter.js +3 -1
  134. package/dist/render/ssrPass.js +1 -0
  135. package/dist/render/surfaceModel.d.ts +49 -8
  136. package/dist/render/surfaceModel.js +54 -8
  137. package/dist/render/surfaceTexture.d.ts +38 -1
  138. package/dist/render/surfaceTexture.js +37 -2
  139. package/package.json +6 -6
  140. package/src/index.ts +26 -3
  141. package/src/render/ambientHarmonics.ts +92 -0
  142. package/src/render/anisotropicLobe.ts +29 -0
  143. package/src/render/areaLights.ts +97 -0
  144. package/src/render/backend/api.ts +61 -1
  145. package/src/render/backend/webgl2/instanced.ts +12 -2
  146. package/src/render/backend/webgl2/renderer.ts +470 -59
  147. package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
  148. package/src/render/backend/webgl2/skinScatter.ts +120 -43
  149. package/src/render/backend/webgpu/buffers.ts +2 -1
  150. package/src/render/backend/webgpu/clusterBinner.ts +3 -1
  151. package/src/render/backend/webgpu/flatPass.ts +18 -13
  152. package/src/render/backend/webgpu/particlePass.ts +119 -12
  153. package/src/render/backend/webgpu/postPass.ts +7 -2
  154. package/src/render/backend/webgpu/renderer.ts +589 -113
  155. package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
  156. package/src/render/backend/webgpu/shaders/clusterBin.wgsl.ts +41 -1
  157. package/src/render/backend/webgpu/skinPaletteRing.ts +65 -33
  158. package/src/render/backend/webgpu/skinScatterPass.ts +59 -9
  159. package/src/render/backend/webgpu/surfaceTexturePass.ts +57 -6
  160. package/src/render/bloomChain.ts +69 -0
  161. package/src/render/bloomPass.ts +26 -2
  162. package/src/render/clusteredLights.ts +259 -12
  163. package/src/render/driftLight/bake.ts +3 -1
  164. package/src/render/driftLight/falloff.ts +13 -2
  165. package/src/render/fixtureAtlas.ts +1 -1
  166. package/src/render/gi/bounceLights.ts +12 -3
  167. package/src/render/instanceCull.ts +9 -0
  168. package/src/render/instances.ts +61 -12
  169. package/src/render/lightBudget.ts +88 -4
  170. package/src/render/lightmap.ts +243 -0
  171. package/src/render/mesh.ts +2 -1
  172. package/src/render/particleBatch.ts +95 -4
  173. package/src/render/particleMaterial.ts +11 -1
  174. package/src/render/particlePool.ts +36 -0
  175. package/src/render/particleSort.ts +133 -0
  176. package/src/render/pointLightSelection.ts +20 -1
  177. package/src/render/renderQuality.ts +8 -7
  178. package/src/render/rendererHarness.ts +3 -0
  179. package/src/render/rgb9e5.ts +52 -0
  180. package/src/render/sceneCapture.ts +56 -0
  181. package/src/render/sceneTarget.ts +18 -1
  182. package/src/render/screenSpaceReflection.ts +33 -1
  183. package/src/render/shaders/bloom.ts +20 -3
  184. package/src/render/shaders/bolt.ts +7 -1
  185. package/src/render/shaders/depth.ts +11 -0
  186. package/src/render/shaders/flat/anisotropic.ts +37 -3
  187. package/src/render/shaders/flat/areaLight.ts +142 -0
  188. package/src/render/shaders/flat/glassTint.ts +9 -2
  189. package/src/render/shaders/flat/index.ts +51 -6
  190. package/src/render/shaders/flat/lobes.ts +50 -43
  191. package/src/render/shaders/flat/main.ts +158 -121
  192. package/src/render/shaders/flat/models.ts +170 -21
  193. package/src/render/shaders/flat/normalMap.ts +25 -0
  194. package/src/render/shaders/flat/preamble.ts +84 -40
  195. package/src/render/shaders/flat/skin.ts +6 -5
  196. package/src/render/shaders/generated/bloom.wgsl.ts +13 -3
  197. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  198. package/src/render/shaders/generated/depth.wgsl.ts +4 -4
  199. package/src/render/shaders/generated/flat.wgsl.ts +1685 -2082
  200. package/src/render/shaders/generated/particle.wgsl.ts +116 -1
  201. package/src/render/shaders/generated/skinBlur.wgsl.ts +12 -2
  202. package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
  203. package/src/render/shaders/generated/water.wgsl.ts +23 -16
  204. package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
  205. package/src/render/shaders/particle.ts +134 -1
  206. package/src/render/shaders/recon/motion.wgsl.ts +13 -2
  207. package/src/render/shaders/skinBlur.ts +25 -3
  208. package/src/render/shaders/ssrTrace.ts +20 -1
  209. package/src/render/shaders/water.ts +12 -3
  210. package/src/render/skinBlur.ts +8 -5
  211. package/src/render/skinScatter.ts +3 -1
  212. package/src/render/ssrPass.ts +1 -0
  213. package/src/render/surfaceModel.ts +82 -10
  214. package/src/render/surfaceTexture.ts +101 -3
@@ -147,6 +147,12 @@ export const MAIN_GLSL = `void main() {
147
147
  float glassTransmission = uSeeThrough.z;
148
148
  float glassFrost = uSeeThrough.w;
149
149
 
150
+ /*
151
+ * The normal a refracting draw bends by: the shading normal, a normal map's tilt and every relief
152
+ * in it, as the lit block leaves it before turning a back face — the geometric normal where
153
+ * nothing tilts it, which is what refraction bent by before 4.8.6. See the refraction below.
154
+ */
155
+ vec3 shadingNormal = normalize(vNormal);
150
156
  if (uLightingEnabled != 0) {
151
157
  vec3 n = normalize(vNormal);
152
158
  /*
@@ -180,20 +186,7 @@ export const MAIN_GLSL = `void main() {
180
186
  * right: a mipped colour image, sampled under uniform control flow, exactly as uAlbedo is
181
187
  * below. The rule is about non-uniform branches, and this branch is on a uniform.
182
188
  */
183
- if (uNormalStrength > 0.0) {
184
- vec3 mapped = texture(uNormalMap, surfaceAt).xyz * 2.0 - 1.0;
185
- /*
186
- * z rebuilt where none is stored. A tangent-space normal points out of its surface, so a
187
- * stored z at or below zero is never a real one: it is a two-channel map, BC5, whose blue a
188
- * device samples as zero and which arrives here as -1. Every map baked from BC5 to PNG before
189
- * 4.8.4 carried the same zero. A select rather than a flag: no uniform to bind, no material
190
- * that has to say what format it holds, and continuous at the boundary, since a unit texel
191
- * lying flat has its z near zero either way. A texel with z above zero is left as it was.
192
- */
193
- mapped.z = mapped.z > 0.0 ? mapped.z : sqrt(max(0.0, 1.0 - dot(mapped.xy, mapped.xy)));
194
- mat3 tbn = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
195
- n = normalize(mix(n, normalize(tbn * mapped), uNormalStrength));
196
- }
189
+ if (uNormalStrength > 0.0) n = normalMapped(n, surfaceAt);
197
190
 
198
191
  /*
199
192
  * Microscopic relief: the surface's own texture, as a turn of the normal rather than a change
@@ -217,7 +210,8 @@ export const MAIN_GLSL = `void main() {
217
210
  * widening driven by fwidth was measured making that artefact three times worse, because that
218
211
  * derivative spikes at every hard crease. See docs/IMPROVEMENTS.md.
219
212
  */
220
- float reliefAmount = uRelief * vRelief;
213
+ /* Never below zero: a lightmapped mesh's coordinates ride this lane below it. See lightmap.ts. */
214
+ float reliefAmount = uRelief.x * max(vRelief, 0.0);
221
215
  /* Grown with the relief, before anything reads it. See the comment above: a normal that
222
216
  wanders cannot hold a highlight narrower than the wander. */
223
217
  /*
@@ -258,11 +252,11 @@ export const MAIN_GLSL = `void main() {
258
252
  * variance is still roughness — a road that faded to smooth at distance would go glossy
259
253
  * instead of noisy, which is a worse artefact wearing better clothes.
260
254
  */
261
- vec3 reliefPixel = fwidth(vWorldPos) * uReliefCycles;
255
+ vec3 reliefPixel = fwidth(vWorldPos) * uRelief.y;
262
256
  float reliefFootprint = max(max(reliefPixel.x, reliefPixel.y), reliefPixel.z);
263
257
  float reliefResolved = reliefAmount * (1.0 - smoothstep(0.5, 1.5, reliefFootprint));
264
258
  if (reliefResolved > 0.0) {
265
- vec3 at = vWorldPos * uReliefCycles;
259
+ vec3 at = vWorldPos * uRelief.y;
266
260
  /* One cell of the noise, which is one bump: the gradient over any smaller step is the
267
261
  interpolant's own slope rather than the shape's. */
268
262
  float here = grain(at);
@@ -359,6 +353,7 @@ export const MAIN_GLSL = `void main() {
359
353
  * Turning only the interpolated normal would leave a normal map's tilt pointing the wrong way
360
354
  * across the fold of every curtain.
361
355
  */
356
+ shadingNormal = n;
362
357
  if (backFace) n = -n;
363
358
  /*
364
359
  * A model's surface, gathered once, on a branch on the switches alone (models.ts) — and whether
@@ -368,6 +363,14 @@ export const MAIN_GLSL = `void main() {
368
363
  bool modelled = modelOn();
369
364
  bool modelledBehind = modelSeesBehind();
370
365
  if (modelled) modelSurface(n, albedo, surfaceRoughness, metal, surfaceAt);
366
+ /*
367
+ * Skin's colour half leaves here, with the colour its spread light is multiplied by, and a whole
368
+ * alpha a multisampled edge resolves to its coverage. A branch on the switches alone, so uniform.
369
+ */
370
+ if (modelAlbedoAlone()) {
371
+ outColor = vec4(albedo, 1.0);
372
+ return;
373
+ }
371
374
 
372
375
  /*
373
376
  * One dot product, used twice: ndl is the lit side and its negative is the night side below.
@@ -467,6 +470,8 @@ export const MAIN_GLSL = `void main() {
467
470
  irradiance = mix(irradiance, ambient, probeGridOutside(surfaceN, vWorldPos));
468
471
  ambient = mix(ambient, irradiance, uProbeGridAmbient * uEnvironmentEnabled);
469
472
  #endif
473
+ /* A draw's own ambient, where one is set, in place of the frame's: \`setAmbientSH\`. */
474
+ if (uAmbientSH[6].w > 0.5) ambient = ambientHarmonics(n);
470
475
  /*
471
476
  * A metal keeps its ambient and loses its direct diffuse.
472
477
  *
@@ -494,6 +499,8 @@ export const MAIN_GLSL = `void main() {
494
499
  } else {
495
500
  lit = albedo * (ambient + sunColor * direct * (1.0 - metal));
496
501
  }
502
+ /* A baked page's light, beside the sky's and the sun's: nothing but where a material has one. */
503
+ lit += modelBaked(n, albedo, metal);
497
504
 
498
505
  /*
499
506
  * A sun highlight, for the few surfaces that ask for one.
@@ -535,7 +542,7 @@ export const MAIN_GLSL = `void main() {
535
542
  * specularLobe's header argues for at length.
536
543
  */
537
544
  float sunVoH = max(dot(normalize(uCameraPos - vWorldPos), halfway), 0.0);
538
- vec3 sunSpec = mix(specColor, vec3(1.0), pow(1.0 - sunVoH, 5.0) * metal);
545
+ vec3 sunSpec = highlightTint(specColor, sunVoH, metal);
539
546
  /*
540
547
  * **Held back until after the environment blend, because that blend is a mix and would
541
548
  * otherwise erase this.**
@@ -554,8 +561,12 @@ export const MAIN_GLSL = `void main() {
554
561
  * Zero for the whole world that carries no specular attribute and no metalness, exactly as
555
562
  * before: \`sunSpec\` is \`vec3(vSpecular)\` at metal 0 and \`vSpecular\` defaults to 0.
556
563
  */
557
- vec3 sunHighlight =
558
- sunColor * specularLobe(max(dot(n, halfway), 0.0), surfaceRoughness) * sunSpec * sunShade * sunFacing;
564
+ /* GGX's own where the surface asks (\`physicalSpecular\`): the sun is a point, so nothing widens it. */
565
+ float sunNdh = max(dot(n, halfway), 0.0);
566
+ float sunLobe = physicalSpecular()
567
+ ? physicalLobe(sunNdh, ndl, dot(n, normalize(uCameraPos - vWorldPos)), surfaceRoughness, 0.0, 1.0)
568
+ : specularLobe(sunNdh, surfaceRoughness);
569
+ vec3 sunHighlight = sunColor * sunLobe * sunSpec * sunShade * sunFacing;
559
570
  if (modelled) sunHighlight = sunModelSpecular;
560
571
  /*
561
572
  * The dielectric's share goes in here, where it has always gone, so the environment blend
@@ -639,7 +650,7 @@ export const MAIN_GLSL = `void main() {
639
650
  */
640
651
  /* A model reflects its environment whatever the pass asked of the standard model: see
641
652
  modelReflectance. On the switches alone, so the derivatives below stay in uniform flow. */
642
- if (uReflectivity > 0.0 || uMaterialFlags.y != 0 || modelled) {
653
+ if (uEnvironmentDials.y > 0.0 || uMaterialFlags.y != 0 || modelled) {
643
654
  vec3 toEye = normalize(uCameraPos - vWorldPos);
644
655
  vec3 mirrored = reflect(-toEye, modelled ? modelReflectNormal(n) : n);
645
656
  vec3 environment = mix(uAmbientGround, uAmbient, mirrored.y * 0.5 + 0.5);
@@ -740,7 +751,7 @@ export const MAIN_GLSL = `void main() {
740
751
  float footprintRough = 0.5 * sqrt(min(swingRad, 1.0));
741
752
  float lobeRough = min(1.0, sqrt(surfaceRoughness * surfaceRoughness + footprintRough * footprintRough));
742
753
  float envLod = clamp(
743
- mix(boxLod, lobeRough * uEnvironmentMaxLod, uEnvironmentPrefiltered),
754
+ mix(boxLod, lobeRough * uEnvironmentMaxLod, uEnvironmentDials.x),
744
755
  0.0,
745
756
  uEnvironmentMaxLod
746
757
  );
@@ -776,7 +787,7 @@ export const MAIN_GLSL = `void main() {
776
787
  * \`mix(1, gain, 0)\` is exactly 1, so a dielectric is arithmetically untouched however the
777
788
  * caller sets this.
778
789
  */
779
- environment *= mix(1.0, uEnvironmentGain, metal);
790
+ environment *= mix(1.0, uEnvironmentDials.z, metal);
780
791
  float facing = 1.0 - max(dot(n, toEye), 0.0);
781
792
  /*
782
793
  * **A metal's Fresnel base is close to its albedo, not a dielectric's four percent.** Schlick
@@ -810,7 +821,7 @@ export const MAIN_GLSL = `void main() {
810
821
  * which is what the old \`max(uReflectivity, metal) * (1 - r)\` came to, and both renderers
811
822
  * clamp that uniform non-negative on upload.
812
823
  */
813
- float reflectAmount = max(uReflectivity * (1.0 - surfaceRoughness), metal);
824
+ float reflectAmount = max(uEnvironmentDials.y * (1.0 - surfaceRoughness), metal);
814
825
  /*
815
826
  * **With a prefiltered environment, how much a surface returns is an integral rather than a
816
827
  * curve somebody chose**, and this is where the comment above said the thinning should go
@@ -880,7 +891,7 @@ export const MAIN_GLSL = `void main() {
880
891
  */
881
892
  #if ENVIRONMENT_PROBE
882
893
  vec2 dfg = envBrdfApprox(ndv, surfaceRoughness);
883
- float integrated = clamp(envSpecularEnergy(f0, dfg) * max(uReflectivity, metal), 0.0, 1.0);
894
+ float integrated = clamp(envSpecularEnergy(f0, dfg) * max(uEnvironmentDials.y, metal), 0.0, 1.0);
884
895
  /*
885
896
  * **The split sum only where a prefiltered chain is what is being sampled.** It is the
886
897
  * correct share of a *prefiltered* environment; over a box chain it asserts an integral that
@@ -892,7 +903,7 @@ export const MAIN_GLSL = `void main() {
892
903
  float weight = mix(
893
904
  clamp(fresnel * reflectAmount, 0.0, 1.0),
894
905
  integrated,
895
- uEnvironmentEnabled * uEnvironmentPrefiltered
906
+ uEnvironmentEnabled * uEnvironmentDials.x
896
907
  );
897
908
  #else
898
909
  float weight = clamp(fresnel * reflectAmount, 0.0, 1.0);
@@ -1029,8 +1040,8 @@ export const MAIN_GLSL = `void main() {
1029
1040
  bool fixtureProfiles = false;
1030
1041
  bool fixtureShapes = false;
1031
1042
  if (LIGHT_FIXTURES) {
1032
- fixtureProfiles = uIesAtlasRows > 1.0;
1033
- fixtureShapes = uIesPlaneCount > 1.0 || uCookieTiles > 0.0;
1043
+ fixtureProfiles = uFixtureShape.x > 1.0;
1044
+ fixtureShapes = uFixtureShape.y > 1.0 || uFixtureShape.z > 0.0;
1034
1045
  }
1035
1046
 
1036
1047
  // Point lights. Inverse-square-ish falloff clipped to a finite radius, so a
@@ -1057,6 +1068,19 @@ export const MAIN_GLSL = `void main() {
1057
1068
  float iesProfile;
1058
1069
  vec3 iesAxis;
1059
1070
  float cookie;
1071
+ /* The light's lighting channels, a mask: 1 unless it names others. See \`uLightIesAxis\`. */
1072
+ float lightChannels = 1.0;
1073
+ /* The light's own falloff exponent, or 0 for the frame's falloff. See \`uLightCone\`. */
1074
+ float lightExponent = 0.0;
1075
+ /* A clustered rectangle's barn doors: their cosine and length, none at 0. See \`areaBarnDoors\`. */
1076
+ vec2 areaDoors = vec2(0.0);
1077
+ /*
1078
+ * **A rectangle in the froxel table** — one past the fixed arm's \`maxAreaLights\` — which
1079
+ * its record says with a negative radius, its reach negated. Its fields come out of the
1080
+ * fixture texels: sidedness, the right axis, the half extents and the up axis.
1081
+ */
1082
+ bool clusteredArea = false;
1083
+ float areaTwoSided = 0.0;
1060
1084
  if (clusteredArm) {
1061
1085
  if (slot >= clusterLights) break;
1062
1086
  /*
@@ -1075,6 +1099,8 @@ export const MAIN_GLSL = `void main() {
1075
1099
  lightPos = vec3(
1076
1100
  uintBitsToFloat(rec0.x), uintBitsToFloat(rec0.y), uintBitsToFloat(rec0.z));
1077
1101
  lightRadius = uintBitsToFloat(rec0.w);
1102
+ clusteredArea = lightRadius < 0.0;
1103
+ lightRadius = abs(lightRadius);
1078
1104
  /*
1079
1105
  * **Out of reach is decided on the first texel, before the rest of the record is read.**
1080
1106
  * A froxel holds every light whose sphere touches its box, so most of the lights a fragment
@@ -1098,8 +1124,8 @@ export const MAIN_GLSL = `void main() {
1098
1124
  lightWeight = sizeAndWeight.y;
1099
1125
  /*
1100
1126
  * **The fixture, read only for a light that has one.** What a plain light's record holds in
1101
- * the last three texels is exactly these values, so a light carrying no shadow slot, cone,
1102
- * profile or cookie shades the same whether they are read or assumed, and at heavy overlap
1127
+ * the last four texels is exactly these values, so a light carrying no shadow slot, cone,
1128
+ * profile, cookie, channels or falloff of its own shades the same whether they are read or assumed, and at heavy overlap
1103
1129
  * reading them was a third of the pass. A branch on a value from a texture around
1104
1130
  * \`texelFetch\` is not the 2026-08-07 case: an integer fetch takes no derivative.
1105
1131
  *
@@ -1119,7 +1145,9 @@ export const MAIN_GLSL = `void main() {
1119
1145
  uvec4 rec2 = clusterTexel(record + 2);
1120
1146
  uvec4 rec3 = clusterTexel(record + 3);
1121
1147
  uvec4 rec4 = clusterTexel(record + 4);
1122
- shadowSlot = int(uintBitsToFloat(rec2.x));
1148
+ uvec4 rec5 = clusterTexel(record + 5);
1149
+ shadowSlot = clusteredArea ? -1 : int(uintBitsToFloat(rec2.x));
1150
+ areaTwoSided = uintBitsToFloat(rec2.x);
1123
1151
  lightDir = vec3(
1124
1152
  uintBitsToFloat(rec2.y), uintBitsToFloat(rec2.z), uintBitsToFloat(rec2.w));
1125
1153
  lightCone = vec2(uintBitsToFloat(rec3.x), uintBitsToFloat(rec3.y));
@@ -1129,6 +1157,9 @@ export const MAIN_GLSL = `void main() {
1129
1157
  texel rather than being derived from the direction. */
1130
1158
  iesAxis = vec3(
1131
1159
  uintBitsToFloat(rec4.x), uintBitsToFloat(rec4.y), uintBitsToFloat(rec4.z));
1160
+ lightChannels = uintBitsToFloat(rec4.w);
1161
+ lightExponent = uintBitsToFloat(rec5.x);
1162
+ areaDoors = vec2(uintBitsToFloat(rec5.y), uintBitsToFloat(rec5.z));
1132
1163
  }
1133
1164
  }
1134
1165
  } else {
@@ -1141,11 +1172,48 @@ export const MAIN_GLSL = `void main() {
1141
1172
  lightWeight = uLightWeight[i];
1142
1173
  shadowSlot = i;
1143
1174
  lightDir = uLightDir[i];
1144
- lightCone = uLightCone[i];
1175
+ lightCone = uLightCone[i].xy;
1176
+ lightExponent = uLightCone[i].z;
1145
1177
  iesProfile = uLightIesProfile[i];
1146
- iesAxis = uLightIesAxis[i];
1178
+ iesAxis = uLightIesAxis[i].xyz;
1179
+ lightChannels = uLightIesAxis[i].w;
1147
1180
  cookie = uLightCookie[i];
1148
1181
  }
1182
+ /*
1183
+ * **A light shades a surface only where their channels meet**: the light's mask against the
1184
+ * material's. Both default to channel 1, so a scene naming none takes every light as before
1185
+ * — and **zero reads as that default on both sides**, so a pass that never writes the lane
1186
+ * loses no light: an unwritten uniform is zero, and zero here would be every lamp switched
1187
+ * off. A branch on a value read from a texture in the clustered arm, as the reach test above
1188
+ * is, and it skips only what the light would add; no derivative is taken past it.
1189
+ */
1190
+ int litChannels = int(lightChannels);
1191
+ int surfaceChannels = int(uCutout.w);
1192
+ if (((litChannels == 0 ? 1 : litChannels) & (surfaceChannels == 0 ? 1 : surfaceChannels)) == 0) {
1193
+ continue;
1194
+ }
1195
+ /*
1196
+ * **A clustered rectangle is shaded here and goes no further**: by the fixed loop's own body
1197
+ * (\`areaLight.ts\`), unshadowed — its occlusion is the fixed arm's, which is why the first
1198
+ * \`maxAreaLights\` stay there — and windowed to nothing at its reach, as a lamp is, so the
1199
+ * froxel it is binned into has no edge in the picture.
1200
+ */
1201
+ if (clusteredArea) {
1202
+ vec3 seenCentre = lightPos;
1203
+ vec2 seenHalf = lightCone;
1204
+ if (!areaBarnDoors(seenCentre, lightDir, iesAxis, seenHalf, areaDoors)) continue;
1205
+ float areaForm = areaSignedForm(n, seenCentre, lightDir, iesAxis, seenHalf);
1206
+ float areaWindow = clamp(1.0 - pow(length(lightPos - vWorldPos) / max(lightRadius, 1e-4), 4.0), 0.0, 1.0);
1207
+ areaForm = mix(max(0.0, areaForm), abs(areaForm), areaTwoSided);
1208
+ if (areaForm > 0.0) {
1209
+ areaLightAdd(
1210
+ seenCentre, lightDir, iesAxis, seenHalf, lightColor * (areaWindow * areaWindow), areaForm,
1211
+ 1.0, vec3(1.0), n, albedo, metal, specColor, surfaceRoughness, modelled,
1212
+ lampOpen, lampShadowed
1213
+ );
1214
+ }
1215
+ continue;
1216
+ }
1149
1217
  /* A DriftLight field's own light carries a negative weight, and is shaded exactly only for the
1150
1218
  share of this pixel the field does not stand in for. See \`driftLight.ts\`. */
1151
1219
  if (lightWeight < 0.0) lightWeight = -lightWeight * (1.0 - driftShare);
@@ -1190,7 +1258,16 @@ export const MAIN_GLSL = `void main() {
1190
1258
  * same way three.js windows its own, so the light still ends where it says it does.
1191
1259
  */
1192
1260
  float falloff;
1193
- if (uLightFalloff == 1) {
1261
+ /*
1262
+ * **A light may name its own falloff, and then it is Unreal's for a light without
1263
+ * inverse-square falloff**: \`(1 - (d/R)^2)^n\` with no distance term, so a fill authored to
1264
+ * light a whole hall across a hundred metres does, where the inverse square would have it
1265
+ * gone a few metres out. Zero is the frame's own rule below, which every light was before.
1266
+ */
1267
+ if (lightExponent > 0.0) {
1268
+ float reach = dist / max(lightRadius, 1e-4);
1269
+ falloff = pow(clamp(1.0 - reach * reach, 0.0, 1.0), lightExponent);
1270
+ } else if (uLightFalloff == 1) {
1194
1271
  float window = clamp(1.0 - pow(dist / max(lightRadius, 1e-4), 4.0), 0.0, 1.0);
1195
1272
  falloff = window * window / max(dist * dist, 0.01);
1196
1273
  } else {
@@ -1350,7 +1427,7 @@ export const MAIN_GLSL = `void main() {
1350
1427
  * and the result is selected afterwards with a \`mix\` on a compare — which is arithmetic
1351
1428
  * rather than control flow.
1352
1429
  *
1353
- * **And skipped outright where nothing loaded a profile**, behind \`uIesAtlasRows\`, which is
1430
+ * **And skipped outright where nothing loaded a profile**, behind \`uFixtureShape.x\`, which is
1354
1431
  * one while the atlas holds only its row of ones. A uniform is provably uniform control
1355
1432
  * flow, so this branch is not the one the rule forbids. It was paid on every scene until a
1356
1433
  * 4K frame of 320 lamps and no shadows measured it at 0.7 ms of the main pass, rising to
@@ -1393,12 +1470,12 @@ export const MAIN_GLSL = `void main() {
1393
1470
  if (iesRefLen > 1e-5) {
1394
1471
  iesRef /= iesRefLen;
1395
1472
  vec3 iesBinormal = cross(lightDir, iesRef);
1396
- if (uIesPlaneCount > 1.0) {
1473
+ if (uFixtureShape.y > 1.0) {
1397
1474
  /* \`atan\` answers -PI..PI and \`fract\` of a negative wraps forward, so this is 0..1
1398
1475
  across the whole turn without a branch on the sign. */
1399
1476
  iesTurns = fract(atan(dot(iesRay, iesBinormal), dot(iesRay, iesRef)) / TAU_IES);
1400
1477
  }
1401
- if (uCookieTiles > 0.0) {
1478
+ if (uFixtureShape.z > 0.0) {
1402
1479
  /*
1403
1480
  * **A perspective projection onto the plane one unit down the aim**, which is what a
1404
1481
  * fixture actually throws: the mask grows with distance rather than being pasted on
@@ -1445,15 +1522,19 @@ export const MAIN_GLSL = `void main() {
1445
1522
  */
1446
1523
  float photometric = 1.0;
1447
1524
  if (fixtureProfiles) {
1448
- float iesBase = iesRow * (uIesPlaneCount + 1.0);
1525
+ float iesBase = iesRow * (uFixtureShape.y + 1.0);
1449
1526
  /* The profiles are the atlas's bottom rows, below any cookies. */
1450
- float iesTop = fixtureSize.y - uIesAtlasRows;
1451
- float iesV = (iesTop + iesBase + iesTurns * uIesPlaneCount + 0.5) / fixtureSize.y;
1527
+ float iesTop = fixtureSize.y - uFixtureShape.x;
1528
+ float iesV = (iesTop + iesBase + iesTurns * uFixtureShape.y + 0.5) / fixtureSize.y;
1452
1529
  float iesGain = textureLod(uFixtureAtlas, vec2(iesU, iesV), 0.0).r;
1453
1530
  photometric = mix(1.0, iesGain, step(0.0, iesProfile));
1454
1531
  }
1455
1532
 
1456
- float shape = (uLightFalloff == 1 ? falloff : falloff * falloff) * coneFalloff * photometric;
1533
+ /* A light's own exponent already is its whole curve; the frame's smooth rule squares its own. */
1534
+ float shape =
1535
+ (lightExponent > 0.0 || uLightFalloff == 1 ? falloff : falloff * falloff) *
1536
+ coneFalloff *
1537
+ photometric;
1457
1538
  glassGlow += lightColor * backNdl * shape * lightWeight * shaded * lampGlass;
1458
1539
  if (ndl <= 0.0 && !modelledBehind) continue;
1459
1540
  /*
@@ -1530,16 +1611,15 @@ export const MAIN_GLSL = `void main() {
1530
1611
  if (vSpecular > 0.0 || metal > 0.0) {
1531
1612
  vec3 toEyeLamp = normalize(uCameraPos - vWorldPos);
1532
1613
  vec3 lampHalfway = normalize(toLight / max(dist, 1e-4) + toEyeLamp);
1533
- /* The same grazing whitening the sun's highlight takes, and zero at metal 0. */
1534
- vec3 lampSpec = mix(
1535
- specColor,
1536
- vec3(1.0),
1537
- pow(1.0 - max(dot(toEyeLamp, lampHalfway), 0.0), 5.0) * metal
1538
- );
1614
+ /* The same grazing whitening the sun's highlight takes, and zero at metal 0 for the look. */
1615
+ vec3 lampSpec = highlightTint(specColor, max(dot(toEyeLamp, lampHalfway), 0.0), metal);
1616
+ /* The look's lobe, or GGX's own where the surface asks for it: see \`physicalLobe\`. */
1617
+ float lampNdh = max(dot(n, lampHalfway), 0.0);
1618
+ float lampLobe = physicalSpecular()
1619
+ ? physicalLobe(lampNdh, ndl, dot(n, toEyeLamp), surfaceRoughness, lightSourceRadius, dist)
1620
+ : sphereLobe(lampNdh, surfaceRoughness, lightSourceRadius, dist);
1539
1621
  /* Held back with the diffuse half, and shadowed by the same term it always was. */
1540
- vec3 lampHighlight =
1541
- lightColor * sphereLobe(max(dot(n, lampHalfway), 0.0), surfaceRoughness, lightSourceRadius, dist)
1542
- * lampSpec * shape * lightWeight;
1622
+ vec3 lampHighlight = lightColor * lampLobe * lampSpec * shape * lightWeight;
1543
1623
  lampOpen += lampHighlight;
1544
1624
  lampShadowed += lampHighlight * shaded * lampGlass;
1545
1625
  }
@@ -1583,24 +1663,18 @@ export const MAIN_GLSL = `void main() {
1583
1663
  vec3 centre = uAreaLightPos[a];
1584
1664
  vec3 right = uAreaLightRight[a];
1585
1665
  vec3 up = uAreaLightUp[a];
1586
- vec2 halfSize = uAreaLightSize[a];
1587
-
1666
+ vec2 halfSize = uAreaLightSize[a].xy;
1588
1667
  /*
1589
- * **Wound so a surface on the emitting side sees them counter-clockwise**, which is what
1590
- * makes the form factor positive there. The natural order — minus, plus, plus, minus — winds
1591
- * the other way, and every one-sided light then clamped to zero and the frame was black.
1668
+ * **The part of the rectangle this fragment sees past its barn doors** (\`areaBarnDoors\`),
1669
+ * which its light and highlight come from. Its shadow keeps the whole rectangle's centre and
1670
+ * size below, because that is what its map was baked from.
1592
1671
  */
1593
- vec3 c0 = centre - right * halfSize.x + up * halfSize.y;
1594
- vec3 c1 = centre + right * halfSize.x + up * halfSize.y;
1595
- vec3 c2 = centre + right * halfSize.x - up * halfSize.y;
1596
- vec3 c3 = centre - right * halfSize.x - up * halfSize.y;
1672
+ vec3 seenCentre = centre;
1673
+ vec2 seenHalf = halfSize;
1674
+ if (!areaBarnDoors(seenCentre, right, up, seenHalf, uAreaLightSize[a].zw)) continue;
1597
1675
 
1598
- /*
1599
- * **The sign is the sidedness.** A one-sided rectangle takes the positive part and a
1600
- * two-sided one the magnitude — no facing test, because the sign already answers it exactly
1601
- * per fragment rather than once for the rectangle's centre.
1602
- */
1603
- float signedForm = quadFormFactor(n, vWorldPos, c0, c1, c2, c3);
1676
+ /* The form factor and its sign, which is the sidedness: see \`areaSignedForm\`. */
1677
+ float signedForm = areaSignedForm(n, seenCentre, right, up, seenHalf);
1604
1678
  float form = mix(max(0.0, signedForm), abs(signedForm), uAreaLightTwoSided[a]);
1605
1679
  /* The form factor is linear in the normal, so the one seen from behind is its negation. */
1606
1680
  float backForm = glassTransmission > 0.0
@@ -1715,55 +1789,11 @@ export const MAIN_GLSL = `void main() {
1715
1789
 
1716
1790
  glassGlow += uAreaLightColor[a] * backForm * areaOccl * areaGlass;
1717
1791
  if (form <= 0.0) continue;
1718
- /*
1719
- * A model answers a rectangle as it answers a lamp at its centre, carried at the rectangle's
1720
- * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by
1721
- * the form factor. **An approximation, stated**: a model's lobe is not integrated over the
1722
- * rectangle as the standard lobe is, so a glossy model under a large softbox shows a lobe
1723
- * from its centre rather than its shape.
1724
- */
1725
- if (modelled) {
1726
- vec3 areaL = normalize(centre - vWorldPos);
1727
- modelLight(areaL, 0.0, 0.0);
1728
- vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form)
1729
- * uAreaLightColor[a];
1730
- lampOpen += areaModel;
1731
- lampShadowed += areaModel * areaOccl * areaGlass;
1732
- continue;
1733
- }
1734
- /* Held back with the point lamps', and shadowed by the same term it always was. */
1735
- vec3 areaDiffuse = albedo * uAreaLightColor[a] * form * (1.0 - metal);
1736
- lampOpen += areaDiffuse;
1737
- lampShadowed += areaDiffuse * areaOccl * areaGlass;
1738
-
1739
- if (vSpecular > 0.0 || metal > 0.0) {
1740
- /*
1741
- * **The lobe integrated over the rectangle, rather than sampled at one point on it.**
1742
- *
1743
- * What stood here handed the closest point on the rectangle to \`sphereLobe\` and scaled it
1744
- * by \`form\`, the *diffuse* form factor. Measured against a brute-force integral by
1745
- * \`scripts/areaSpecular.mjs\`, that lost the reflection rather than blurring it: on
1746
- * polished metal facing a softbox it returned 0.000233 where the integral is 0.9207, and
1747
- * head-on on a rough surface it was 3.7 times too bright. Worst 287% either way; this is
1748
- * 40.1% where the light is what the surface reflects and 79.0% for one overhead.
1749
- *
1750
- * \`quadCoverage\` says what fraction of the specular lobe the rectangle covers and the
1751
- * environment BRDF says how much the surface returns, which is the same split the probe
1752
- * path uses. **\`dfg.x + dfg.y\` and not \`envSpecularEnergy\`**, deliberately and against
1753
- * the obvious argument: the compensation returns the multiply-scattered share, and every
1754
- * direct lobe in this shader is single-scattering, so adding it here returns energy the
1755
- * light never had. Measured at 211% against 40.1%.
1756
- */
1757
- vec3 toEyeArea = normalize(uCameraPos - vWorldPos);
1758
- float coverage = quadCoverage(n, vWorldPos, toEyeArea, surfaceRoughness, c0, c1, c2, c3);
1759
- if (coverage > 0.0) {
1760
- vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), surfaceRoughness);
1761
- vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);
1762
- vec3 areaHighlight = areaSpec * uAreaLightColor[a] * coverage;
1763
- lampOpen += areaHighlight;
1764
- lampShadowed += areaHighlight * areaOccl * areaGlass;
1765
- }
1766
- }
1792
+ /* The diffuse, the model's answer or the lobe integrated over the rectangle: see areaLight.ts. */
1793
+ areaLightAdd(
1794
+ seenCentre, right, up, seenHalf, uAreaLightColor[a], form, areaOccl, areaGlass,
1795
+ n, albedo, metal, specColor, surfaceRoughness, modelled, lampOpen, lampShadowed
1796
+ );
1767
1797
  }
1768
1798
 
1769
1799
  /*
@@ -1905,10 +1935,10 @@ export const MAIN_GLSL = `void main() {
1905
1935
  }
1906
1936
  /* Pulse, flicker and fade from the layer's effects; exactly 1 where it names none. */
1907
1937
  float fxGlow = fx4.z + fx5.x + fx5.w > 0.0 ? fxEmission(surfaceAt.z) : 1.0;
1908
- lit += emissiveTint * emissiveMapped * uEmissiveGain * vEmissive * uNightFactor
1938
+ lit += emissiveTint * emissiveMapped * uEmission.x * vEmissive * uEmission.y
1909
1939
  * mix(1.0, min(lightShade, sunShade), EMISSIVE_SHADOW_SHARE) * fxGlow;
1910
1940
  /* And a lit window's glow, which the scene's lit share already times: not gated on night. */
1911
- lit += windowGlow * uEmissiveGain;
1941
+ lit += windowGlow * uEmission.x;
1912
1942
 
1913
1943
  #if NIGHT_EMISSIVE
1914
1944
  /*
@@ -1941,9 +1971,9 @@ export const MAIN_GLSL = `void main() {
1941
1971
  // Arrival glow: a world-space box lit independently of the clock, so
1942
1972
  // finishing a run reads the same at noon and at midnight. Costs one compare
1943
1973
  // per fragment and no extra draw call — the box is a uniform, not geometry.
1944
- if (uHighlightGain > 0.0) {
1945
- vec3 inside = step(uHighlightMin, vWorldPos) * step(vWorldPos, uHighlightMax);
1946
- lit += albedo * uHighlightGain * inside.x * inside.y * inside.z;
1974
+ if (uHighlightMax.w > 0.0) {
1975
+ vec3 inside = step(uHighlightMin, vWorldPos) * step(vWorldPos, uHighlightMax.xyz);
1976
+ lit += albedo * uHighlightMax.w * inside.x * inside.y * inside.z;
1947
1977
  }
1948
1978
  }
1949
1979
 
@@ -2000,7 +2030,7 @@ export const MAIN_GLSL = `void main() {
2000
2030
  /*
2001
2031
  * **Refraction: the scene behind this surface, bent and absorbed.**
2002
2032
  *
2003
- * The offset is along the surface normal in screen space, which is the cheap approximation every
2033
+ * The offset is along the shading normal in screen space, which is the cheap approximation every
2004
2034
  * real-time renderer ships rather than a ray traced through two interfaces of a solid. What it
2005
2035
  * cannot do is show anything the frame had not already drawn, so a pane at the edge of the frame
2006
2036
  * refracts what is beside it -- and the clamp below is what keeps that from being worse than
@@ -2035,7 +2065,14 @@ export const MAIN_GLSL = `void main() {
2035
2065
  * draw uses.
2036
2066
  */
2037
2067
  if (uSeeThrough.x > 0.0 || glassTransmission > 0.0) {
2038
- vec3 refractN = normalize(vNormal);
2068
+ /*
2069
+ * **Bent by the shading normal, so a normal map shimmers what is seen through it** — a heat
2070
+ * haze, a distortion sphere, a rippled pane. Until 4.8.6 this was the geometric normal and a
2071
+ * mapped surface refracted as a smooth lens. An unlit draw shaded no normal, so it reads the
2072
+ * map here, on a branch on uniforms alone, which keeps its derivatives legal.
2073
+ */
2074
+ vec3 refractN = shadingNormal;
2075
+ if (uLightingEnabled == 0 && uNormalStrength > 0.0) refractN = normalMapped(refractN, surfaceAt);
2039
2076
  vec3 refractV = normalize(uCameraPos - vWorldPos);
2040
2077
  ivec2 snapSize = textureSize(uRefractScene, 0);
2041
2078
  vec2 screenUv = gl_FragCoord.xy / vec2(snapSize);
@@ -1,14 +1,29 @@
1
1
  import type { SurfaceModelKind } from '../../surfaceModel.ts';
2
- /** The models' whole chunk, at depth zero before `main`: see `flatFrag`. */
3
- export declare const MODELS_GLSL: string;
2
+ /**
3
+ * The models' whole chunk, at depth zero before `main`: see `flatFrag`. `physicalSpecular` is the
4
+ * \`PHYSICAL_SPECULAR\` lit switch's value, written in as \`litSwitchesGlsl\` writes the others.
5
+ */
6
+ export declare function modelsGlsl(physicalSpecular: boolean): string;
4
7
  /**
5
8
  * Which half of a skin a lit pipeline draws under `skinScattering: 'screen-space'`: the whole
6
- * surface (every other pipeline), the frame's half without its diffuse, or the diffuse alone into
7
- * the blur's target — that last one depth-tested for equality against what the frame's half wrote
8
- * and writing none, since it is the same surface drawn again. See `skinBlur.ts`.
9
+ * surface (every other pipeline), the frame's half without its diffuse, the diffuse's light alone
10
+ * into the blur's target, or the colour that light is multiplied by after the blur into a target
11
+ * of its own — those last two depth-tested for equality against what the frame's half wrote and
12
+ * writing none, since they are the same surface drawn again. See `skinBlur.ts`.
9
13
  */
10
- export type SkinHalf = 'whole' | 'scene' | 'diffuse';
14
+ export type SkinHalf = 'whole' | 'scene' | 'diffuse' | 'albedo';
15
+ /** The constant each half of a skin turns on. See `SkinHalf`. */
16
+ export declare const SKIN_HALF_SWITCH: {
17
+ readonly scene: 'SKIN_SCREEN';
18
+ readonly diffuse: 'SKIN_DIFFUSE';
19
+ readonly albedo: 'SKIN_ALBEDO';
20
+ };
11
21
  /** A skin lit stage with one half's switch on, WebGL2's way: see `SkinHalf`. */
12
22
  export declare function skinHalfBound(source: string, half: SkinHalf): string;
23
+ /**
24
+ * An instanced vertex stage whose tint lane is a lightmap region: the same source, the constant
25
+ * flipped — WebGL2's way. Only the instanced stage declares it.
26
+ */
27
+ export declare function lightmapRegionsBound(source: string): string;
13
28
  /** A lit stage with one model's switch on: the same source, the constant flipped — WebGL2's way. */
14
29
  export declare function modelBound(source: string, kind: SurfaceModelKind): string;