@driftengine/core 4.8.6 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.json +132 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +7 -2
  4. package/dist/index.js +8 -1
  5. package/dist/render/anisotropicLobe.d.ts +10 -0
  6. package/dist/render/anisotropicLobe.js +19 -0
  7. package/dist/render/areaLights.d.ts +35 -0
  8. package/dist/render/areaLights.js +35 -0
  9. package/dist/render/atmosphere.d.ts +13 -0
  10. package/dist/render/atmosphere.js +9 -1
  11. package/dist/render/backend/api.d.ts +41 -3
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/webgl2/renderer.d.ts +140 -16
  15. package/dist/render/backend/webgl2/renderer.js +436 -44
  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/webgpu/device.d.ts +7 -1
  19. package/dist/render/backend/webgpu/device.js +31 -3
  20. package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
  21. package/dist/render/backend/webgpu/flatPass.js +58 -26
  22. package/dist/render/backend/webgpu/particlePass.js +2 -1
  23. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  24. package/dist/render/backend/webgpu/postPass.js +16 -2
  25. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  26. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  27. package/dist/render/backend/webgpu/renderer.d.ts +166 -3
  28. package/dist/render/backend/webgpu/renderer.js +738 -83
  29. package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
  30. package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
  31. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  32. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  33. package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
  34. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  35. package/dist/render/clusteredLights.d.ts +23 -5
  36. package/dist/render/clusteredLights.js +35 -8
  37. package/dist/render/displayRange.d.ts +31 -0
  38. package/dist/render/displayRange.js +47 -0
  39. package/dist/render/driftLight/bake.d.ts +2 -0
  40. package/dist/render/driftLight/bake.js +1 -1
  41. package/dist/render/driftLight/falloff.d.ts +4 -2
  42. package/dist/render/driftLight/falloff.js +8 -2
  43. package/dist/render/filmicCurve.d.ts +74 -0
  44. package/dist/render/filmicCurve.js +203 -0
  45. package/dist/render/fog.d.ts +4 -1
  46. package/dist/render/fog.js +12 -2
  47. package/dist/render/frame/resources.d.ts +2 -2
  48. package/dist/render/frame/resources.js +6 -0
  49. package/dist/render/frameReflections.d.ts +40 -0
  50. package/dist/render/frameReflections.js +48 -0
  51. package/dist/render/gi/bounceLights.d.ts +3 -2
  52. package/dist/render/gi/bounceLights.js +8 -3
  53. package/dist/render/instanceCull.js +5 -0
  54. package/dist/render/instances.d.ts +19 -5
  55. package/dist/render/instances.js +9 -3
  56. package/dist/render/lightBudget.d.ts +12 -1
  57. package/dist/render/lightBudget.js +23 -3
  58. package/dist/render/lightmap.d.ts +23 -9
  59. package/dist/render/lightmap.js +40 -18
  60. package/dist/render/particlePool.d.ts +21 -0
  61. package/dist/render/particlePool.js +7 -0
  62. package/dist/render/pass.d.ts +49 -0
  63. package/dist/render/pointLightSelection.d.ts +13 -0
  64. package/dist/render/pointLightSelection.js +3 -1
  65. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  66. package/dist/render/reflectionSurfaceTargets.js +99 -0
  67. package/dist/render/renderQuality.d.ts +47 -15
  68. package/dist/render/renderQuality.js +4 -0
  69. package/dist/render/rendererHarness.js +3 -0
  70. package/dist/render/rgb9e5.d.ts +22 -0
  71. package/dist/render/rgb9e5.js +47 -0
  72. package/dist/render/sceneCapture.d.ts +29 -0
  73. package/dist/render/sceneCapture.js +36 -0
  74. package/dist/render/sceneTarget.d.ts +5 -0
  75. package/dist/render/sceneTarget.js +24 -1
  76. package/dist/render/screenSpaceReflection.d.ts +32 -1
  77. package/dist/render/screenSpaceReflection.js +14 -0
  78. package/dist/render/shaders/arcane.d.ts +1 -1
  79. package/dist/render/shaders/bolt.d.ts +1 -1
  80. package/dist/render/shaders/bolt.js +7 -1
  81. package/dist/render/shaders/caustics.d.ts +1 -1
  82. package/dist/render/shaders/depth.js +11 -0
  83. package/dist/render/shaders/film.d.ts +1 -1
  84. package/dist/render/shaders/filmLook.js +2 -1
  85. package/dist/render/shaders/fire.d.ts +1 -1
  86. package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
  87. package/dist/render/shaders/flat/anisotropic.js +37 -3
  88. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  89. package/dist/render/shaders/flat/areaLight.js +46 -0
  90. package/dist/render/shaders/flat/glassTint.d.ts +3 -2
  91. package/dist/render/shaders/flat/glassTint.js +5 -1
  92. package/dist/render/shaders/flat/index.d.ts +15 -0
  93. package/dist/render/shaders/flat/index.js +22 -2
  94. package/dist/render/shaders/flat/lobes.d.ts +6 -0
  95. package/dist/render/shaders/flat/lobes.js +49 -43
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +85 -22
  98. package/dist/render/shaders/flat/models.d.ts +5 -2
  99. package/dist/render/shaders/flat/models.js +100 -16
  100. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  101. package/dist/render/shaders/flat/overlay.js +117 -0
  102. package/dist/render/shaders/flat/pointShadow.js +1 -1
  103. package/dist/render/shaders/flat/preamble.js +10 -3
  104. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  105. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  106. package/dist/render/shaders/fog.d.ts +1 -1
  107. package/dist/render/shaders/fog.js +9 -2
  108. package/dist/render/shaders/fringe.d.ts +29 -0
  109. package/dist/render/shaders/fringe.js +55 -0
  110. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  112. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  114. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  115. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  116. package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
  117. package/dist/render/shaders/generated/depth.wgsl.js +4 -4
  118. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/flat.wgsl.d.ts +192 -16
  123. package/dist/render/shaders/generated/flat.wgsl.js +230 -54
  124. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  126. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  127. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  128. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  129. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  130. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  131. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  132. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  135. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  136. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +32 -7
  137. package/dist/render/shaders/generated/ssrTrace.wgsl.js +32 -7
  138. package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
  139. package/dist/render/shaders/generated/water.wgsl.js +23 -16
  140. package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
  141. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  142. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  143. package/dist/render/shaders/line.d.ts +1 -1
  144. package/dist/render/shaders/particle.d.ts +3 -3
  145. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  146. package/dist/render/shaders/recon/motion.wgsl.js +13 -2
  147. package/dist/render/shaders/rush.js +54 -11
  148. package/dist/render/shaders/scatter.d.ts +1 -1
  149. package/dist/render/shaders/smoke.d.ts +1 -1
  150. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  151. package/dist/render/shaders/ssrResolve.js +51 -0
  152. package/dist/render/shaders/ssrTrace.js +46 -1
  153. package/dist/render/shaders/water.d.ts +1 -1
  154. package/dist/render/shaders/water.js +12 -3
  155. package/dist/render/skinScatter.d.ts +3 -1
  156. package/dist/render/skinScatter.js +3 -1
  157. package/dist/render/ssrPass.d.ts +16 -0
  158. package/dist/render/ssrPass.js +107 -1
  159. package/dist/render/surfaceModel.d.ts +29 -8
  160. package/dist/render/surfaceModel.js +33 -9
  161. package/dist/render/surfaceOverlay.d.ts +132 -0
  162. package/dist/render/surfaceOverlay.js +85 -0
  163. package/dist/render/surfaceTexture.d.ts +44 -1
  164. package/dist/render/surfaceTexture.js +96 -5
  165. package/dist/render/temporalAa.d.ts +14 -0
  166. package/dist/render/temporalAa.js +20 -0
  167. package/dist/render/vertexDefaults.d.ts +17 -0
  168. package/dist/render/vertexDefaults.js +24 -0
  169. package/package.json +6 -6
  170. package/src/index.ts +22 -2
  171. package/src/render/anisotropicLobe.ts +29 -0
  172. package/src/render/areaLights.ts +58 -0
  173. package/src/render/atmosphere.ts +22 -1
  174. package/src/render/backend/api.ts +58 -2
  175. package/src/render/backend/createRenderer.ts +18 -1
  176. package/src/render/backend/webgl2/renderer.ts +548 -50
  177. package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
  178. package/src/render/backend/webgpu/device.ts +42 -3
  179. package/src/render/backend/webgpu/flatPass.ts +63 -26
  180. package/src/render/backend/webgpu/particlePass.ts +2 -1
  181. package/src/render/backend/webgpu/postPass.ts +18 -0
  182. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  183. package/src/render/backend/webgpu/renderer.ts +927 -100
  184. package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
  185. package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
  186. package/src/render/clusteredLights.ts +41 -8
  187. package/src/render/displayRange.ts +61 -0
  188. package/src/render/driftLight/bake.ts +3 -1
  189. package/src/render/driftLight/falloff.ts +13 -2
  190. package/src/render/filmicCurve.ts +229 -0
  191. package/src/render/fog.ts +19 -3
  192. package/src/render/frame/resources.ts +6 -0
  193. package/src/render/frameReflections.ts +72 -0
  194. package/src/render/gi/bounceLights.ts +12 -3
  195. package/src/render/instanceCull.ts +4 -0
  196. package/src/render/instances.ts +26 -6
  197. package/src/render/lightBudget.ts +40 -2
  198. package/src/render/lightmap.ts +64 -23
  199. package/src/render/particlePool.ts +23 -0
  200. package/src/render/pass.ts +53 -1
  201. package/src/render/pointLightSelection.ts +20 -1
  202. package/src/render/reflectionSurfaceTargets.ts +105 -0
  203. package/src/render/renderQuality.ts +52 -15
  204. package/src/render/rendererHarness.ts +3 -0
  205. package/src/render/rgb9e5.ts +52 -0
  206. package/src/render/sceneCapture.ts +56 -0
  207. package/src/render/sceneTarget.ts +30 -2
  208. package/src/render/screenSpaceReflection.ts +38 -1
  209. package/src/render/shaders/bolt.ts +7 -1
  210. package/src/render/shaders/depth.ts +11 -0
  211. package/src/render/shaders/filmLook.ts +2 -1
  212. package/src/render/shaders/flat/anisotropic.ts +37 -3
  213. package/src/render/shaders/flat/areaLight.ts +46 -0
  214. package/src/render/shaders/flat/glassTint.ts +13 -2
  215. package/src/render/shaders/flat/index.ts +37 -2
  216. package/src/render/shaders/flat/lobes.ts +50 -43
  217. package/src/render/shaders/flat/main.ts +85 -22
  218. package/src/render/shaders/flat/models.ts +100 -16
  219. package/src/render/shaders/flat/overlay.ts +117 -0
  220. package/src/render/shaders/flat/pointShadow.ts +1 -1
  221. package/src/render/shaders/flat/preamble.ts +10 -3
  222. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  223. package/src/render/shaders/fog.ts +9 -2
  224. package/src/render/shaders/fringe.ts +59 -0
  225. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  226. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  227. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  228. package/src/render/shaders/generated/depth.wgsl.ts +4 -4
  229. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  230. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  231. package/src/render/shaders/generated/flat.wgsl.ts +230 -54
  232. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  233. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  234. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  235. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  236. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  237. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  238. package/src/render/shaders/generated/ssrTrace.wgsl.ts +32 -7
  239. package/src/render/shaders/generated/water.wgsl.ts +23 -16
  240. package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
  241. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  242. package/src/render/shaders/recon/motion.wgsl.ts +13 -2
  243. package/src/render/shaders/rush.ts +54 -11
  244. package/src/render/shaders/ssrResolve.ts +52 -0
  245. package/src/render/shaders/ssrTrace.ts +46 -1
  246. package/src/render/shaders/water.ts +12 -3
  247. package/src/render/skinScatter.ts +3 -1
  248. package/src/render/ssrPass.ts +139 -1
  249. package/src/render/surfaceModel.ts +47 -10
  250. package/src/render/surfaceOverlay.ts +195 -0
  251. package/src/render/surfaceTexture.ts +136 -9
  252. package/src/render/temporalAa.ts +20 -0
  253. package/src/render/vertexDefaults.ts +28 -0
@@ -23,6 +23,52 @@ float areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {
23
23
  return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);
24
24
  }
25
25
 
26
+ /**
27
+ * How far along one of a rectangle's axes a fragment still sees it past the barn door on that axis's
28
+ * positive edge: \`p\` the fragment's place along the axis, \`height\` its height above the
29
+ * rectangle's plane, \`halfSize\` the rectangle's half extent, and the door's tip \`tipOut\` beyond
30
+ * the edge and \`tipUp\` above the plane. Points of the rectangle past the answer are hidden.
31
+ *
32
+ * Above the tip, the tip's shadow on the plane as seen from the fragment is the limit. Below it, a
33
+ * fragment beside the door and outside its line is hidden from all of it, and one inside the door's
34
+ * reach is hidden from none of it.
35
+ */
36
+ float barnDoorLimit(float p, float height, float halfSize, float tipOut, float tipUp) {
37
+ float tipAt = halfSize + tipOut;
38
+ if (height > tipUp) return (height * tipAt - tipUp * p) / (height - tipUp);
39
+ if (p > halfSize && height * tipOut < tipUp * (p - halfSize)) return -1e9;
40
+ return 1e9;
41
+ }
42
+
43
+ /**
44
+ * The part of a rectangle a fragment sees past its barn doors, written into \`centre\` and
45
+ * \`halfSize\` as the smaller rectangle that is left, or false where the doors hide all of it.
46
+ * \`doors\` is the cosine of their angle from the normal and their length; a length of 0 is no doors.
47
+ * Each axis is clipped by its own two doors, which is what makes this four projections and a min.
48
+ */
49
+ bool areaBarnDoors(inout vec3 centre, vec3 right, vec3 up, inout vec2 halfSize, vec2 doors) {
50
+ if (doors.y <= 0.0) return true;
51
+ vec3 toFragment = vWorldPos - centre;
52
+ float height = dot(toFragment, cross(right, up));
53
+ /* Behind the emitting side: nothing to clip, and the form factor already answers it. */
54
+ if (height <= 0.0) return true;
55
+ float tipOut = doors.y * sqrt(max(1.0 - doors.x * doors.x, 0.0));
56
+ float tipUp = doors.y * doors.x;
57
+ vec2 p = vec2(dot(toFragment, right), dot(toFragment, up));
58
+ vec2 hi = vec2(
59
+ min(halfSize.x, barnDoorLimit(p.x, height, halfSize.x, tipOut, tipUp)),
60
+ min(halfSize.y, barnDoorLimit(p.y, height, halfSize.y, tipOut, tipUp))
61
+ );
62
+ vec2 lo = vec2(
63
+ max(-halfSize.x, -barnDoorLimit(-p.x, height, halfSize.x, tipOut, tipUp)),
64
+ max(-halfSize.y, -barnDoorLimit(-p.y, height, halfSize.y, tipOut, tipUp))
65
+ );
66
+ if (hi.x <= lo.x || hi.y <= lo.y) return false;
67
+ centre += right * (0.5 * (lo.x + hi.x)) + up * (0.5 * (lo.y + hi.y));
68
+ halfSize = 0.5 * (hi - lo);
69
+ return true;
70
+ }
71
+
26
72
  /**
27
73
  * What one rectangle of colour \`color\` and form factor \`form\` adds to the lamps' two sums, open
28
74
  * and shadowed by \`occl\` and \`glass\`.
@@ -29,7 +29,14 @@ vec4 glassUnmix(vec4 mip, float centreClarity) {
29
29
  * about which optional code a lit program carries. See `litSwitchesGlsl`.
30
30
  */
31
31
  export type LitSwitch =
32
- 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT';
32
+ | 'GLASS_SHADOWS'
33
+ | 'CLUSTERED_LIGHTS'
34
+ | 'LIGHT_FIXTURES'
35
+ | 'SURFACE_EFFECTS'
36
+ | 'DRIFT_LIGHT'
37
+ | 'PHYSICAL_SPECULAR'
38
+ | 'SURFACE_OVERLAY'
39
+ | 'REFLECTION_MAPS';
33
40
 
34
41
  /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
35
42
  export function noLitSwitches(): Record<LitSwitch, boolean> {
@@ -39,6 +46,9 @@ export function noLitSwitches(): Record<LitSwitch, boolean> {
39
46
  LIGHT_FIXTURES: false,
40
47
  SURFACE_EFFECTS: false,
41
48
  DRIFT_LIGHT: false,
49
+ PHYSICAL_SPECULAR: false,
50
+ SURFACE_OVERLAY: false,
51
+ REFLECTION_MAPS: false,
42
52
  };
43
53
  }
44
54
 
@@ -61,7 +71,8 @@ export interface LitSwitches {
61
71
  * first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
62
72
  * volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
63
73
  * Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
64
- * registers, 4,641 and 96 with all four out.
74
+ * registers, 4,641 and 96 with all four out. `PHYSICAL_SPECULAR` is a sixth, declared with the
75
+ * models so that theirs keep their ids (`modelsGlsl`).
65
76
  */
66
77
  export function litSwitchesGlsl(switches: LitSwitches): string {
67
78
  return /* glsl */ `
@@ -14,11 +14,13 @@ import { CLOTH_BINDING_GLSL } from '../clothBinding.ts';
14
14
  import { SKINNING_GLSL } from '../skinning.ts';
15
15
  import { SURFACE_GLSL } from './surface.ts';
16
16
  import { MAIN_GLSL } from './main.ts';
17
- import { MODELS_GLSL } from './models.ts';
17
+ import { modelsGlsl } from './models.ts';
18
18
  import { AREA_LIGHT_GLSL } from './areaLight.ts';
19
19
  import { NORMAL_MAP_GLSL } from './normalMap.ts';
20
20
  import { DRIFT_LIGHT_GLSL } from './driftLight.ts';
21
21
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.ts';
22
+ import { overlayGlsl } from './overlay.ts';
23
+ import { reflectionSurfaceGlsl } from './reflectionSurface.ts';
22
24
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.ts';
23
25
  import { FULL_LIGHT_BUDGET, type LightBudget } from '../../uniformVectorBudget.ts';
24
26
 
@@ -245,6 +247,15 @@ void main() {
245
247
  #endif
246
248
  #if INSTANCED
247
249
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
250
+ /*
251
+ * **The bottom row is the instance's texture cell** (\`MeshInstances.uvRegions\`), which an
252
+ * affine placement leaves free: read, and rebuilt as the \`[0, 0, 0, 1]\` it stands for.
253
+ */
254
+ vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
255
+ model[0].w = 0.0;
256
+ model[1].w = 0.0;
257
+ model[2].w = 0.0;
258
+ model[3].w = 1.0;
248
259
  vec3 tint = aInstanceTint.rgb;
249
260
  if (LIGHTMAP_REGIONS) tint = vec3(1.0);
250
261
  #else
@@ -293,7 +304,12 @@ void main() {
293
304
  vColor = aColor * tint;
294
305
  vEmissive = aEmissive;
295
306
  vSpecular = aSpecular;
307
+ #if INSTANCED
308
+ /* The instance's cell first, then the material's scale and offset. */
309
+ vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
310
+ #else
296
311
  vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
312
+ #endif
297
313
  vEmissiveColor = aEmissiveColor;
298
314
  vRoughness = aRoughness;
299
315
  vGrain = aGrain;
@@ -449,6 +465,21 @@ export interface FlatShaderOptions {
449
465
  readonly surfaceEffects?: boolean;
450
466
  /** Whether it reads a DriftLight volume. True when absent. See `litSwitchesGlsl`. */
451
467
  readonly driftLight?: boolean;
468
+ /**
469
+ * Whether it carries GGX's own highlight for a material that asks (`physicalSpecular`). True
470
+ * when absent. See `modelsGlsl`.
471
+ */
472
+ readonly physicalSpecular?: boolean;
473
+ /**
474
+ * Whether it carries a draw's surface overlay, `uOverlay` and all (`setSurfaceOverlay`). True
475
+ * when absent. See `overlayGlsl`.
476
+ */
477
+ readonly surfaceOverlay?: boolean;
478
+ /**
479
+ * Whether the frame's own draws write the reflection maps beside their colour: WebGL2's way to
480
+ * the frame's materials' reflections. True when absent. See `reflectionSurfaceGlsl`.
481
+ */
482
+ readonly reflectionMaps?: boolean;
452
483
  /**
453
484
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
454
485
  *
@@ -593,7 +624,11 @@ export function flatFrag(options: FlatShaderOptions): string {
593
624
  DRIFT_LIGHT_GLSL,
594
625
  SURFACE_EFFECTS_GLSL,
595
626
  /* Depth zero and after the tangent frame and the lobes it calls; see models.ts. */
596
- MODELS_GLSL,
627
+ modelsGlsl(options.physicalSpecular ?? true),
628
+ /* After the models, so its switch takes the next id; depth zero, as main calls it. */
629
+ overlayGlsl(options.surfaceOverlay ?? true),
630
+ /* After the overlay, so its switch takes the next id. See reflectionSurface.ts. */
631
+ reflectionSurfaceGlsl(options.reflectionMaps ?? true),
597
632
  /* After the models, whose answer to a rectangle it asks for. See areaLight.ts. */
598
633
  AREA_LIGHT_GLSL,
599
634
  MAIN_GLSL,
@@ -30,6 +30,55 @@ export const EMISSIVE_SHADOW_SHARE = 0.55;
30
30
  */
31
31
  export const MIN_LOBE_ALPHA = 0.01;
32
32
 
33
+ /**
34
+ * Karis's fit to the split-sum environment BRDF, as GLSL: the lit stage's, and the screen-space
35
+ * reflection's when a surface asks for its Fresnel (`ReflectiveSurface.fresnel`). One text, so the
36
+ * two cannot drift.
37
+ */
38
+ export const ENV_BRDF_GLSL = `/**
39
+ * The other half of the split-sum approximation: what fraction of an environment a surface returns.
40
+ *
41
+ * The convolution decides *what* a surface reflects at a given roughness; this decides *how much*.
42
+ * Before it the amount was fresnel times one-minus-roughness, which is a curve somebody chose
43
+ * standing in for an integral.
44
+ *
45
+ * Karis's analytic fit to the environment BRDF, so there is **no lookup texture and no texture
46
+ * unit**: a few multiply-adds against roughness and the view angle. x scales the surface's own
47
+ * reflectance at normal incidence and y is the additive grazing term, so a smooth dielectric at
48
+ * f0 0.04 returns about 0.046 head-on and approaches 1 at the edge, which is what a dielectric
49
+ * does. At roughness 1 it returns about 0.016 rather than the zero one-minus-roughness gave, which
50
+ * is the substantive difference: a fully rough dielectric does reflect its surroundings.
51
+ *
52
+ * **It replaces the Fresnel factor as well as the thinning**, because the fit already contains the
53
+ * angular dependence; multiplying by a separate Fresnel term would apply it twice.
54
+ *
55
+ * **How much this is worth was measured, and it is worth less than it looks on a dark room.**
56
+ * On demo/dev/ibl.html?ladder=1 the whole ladder moved by at most 0.2 of 255 when the old term was
57
+ * replaced. That is not the term failing: at high roughness the surface reads the coarsest levels
58
+ * of the chain, and in a room of five dark walls and one bright one those levels are dark, so a
59
+ * weight three times larger against a near-zero radiance is still near zero. It is worth much more
60
+ * against a bright environment, which is what a loaded sky is and what no captured room here is.
61
+ *
62
+ * **A warning about reading that page, paid for once.** The ladder falls about five-fold from
63
+ * mirror to rough, and that reads as energy being lost. It is not: a mirror concentrates the one
64
+ * bright wall into a sharp image and a rough surface averages it against five dark ones, so the
65
+ * falloff is the room's own content. The normalised falloff also happens to track one-minus-
66
+ * roughness closely, which made the old term look like the cause — and removing that term entirely
67
+ * changed nothing, which is what settled it. Perturb before believing a match.
68
+ *
69
+ * **What it costs** is a fit rather than the integral: the published error is around one per cent
70
+ * of the tabulated term, far below what an eight-bit frame can show. **What would make it wrong**
71
+ * is a material model this shader does not have — a clearcoat or a sheen lobe carries its own BRDF
72
+ * and its own integral, and this fit is Trowbridge-Reitz with Smith masking and nothing else.
73
+ */
74
+ vec2 envBrdfApprox(float ndv, float roughness) {
75
+ const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
76
+ const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
77
+ vec4 r = roughness * c0 + c1;
78
+ float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;
79
+ return vec2(-1.04, 1.04) * a004 + r.zw;
80
+ }`;
81
+
33
82
  export const LOBES_GLSL = `float shadowReach(float distance, float maxDistance) {
34
83
  return 1.0 - smoothstep(
35
84
  maxDistance * ${POINT_SHADOW_FADE_START.toFixed(2)},
@@ -156,49 +205,7 @@ float sphereLobe(float ndh, float roughness, float sourceRadius, float dist) {
156
205
  return energy * (w2 * w2) / max(d * d, 1e-8);
157
206
  }
158
207
 
159
- /**
160
- * The other half of the split-sum approximation: what fraction of an environment a surface returns.
161
- *
162
- * The convolution decides *what* a surface reflects at a given roughness; this decides *how much*.
163
- * Before it the amount was fresnel times one-minus-roughness, which is a curve somebody chose
164
- * standing in for an integral.
165
- *
166
- * Karis's analytic fit to the environment BRDF, so there is **no lookup texture and no texture
167
- * unit**: a few multiply-adds against roughness and the view angle. x scales the surface's own
168
- * reflectance at normal incidence and y is the additive grazing term, so a smooth dielectric at
169
- * f0 0.04 returns about 0.046 head-on and approaches 1 at the edge, which is what a dielectric
170
- * does. At roughness 1 it returns about 0.016 rather than the zero one-minus-roughness gave, which
171
- * is the substantive difference: a fully rough dielectric does reflect its surroundings.
172
- *
173
- * **It replaces the Fresnel factor as well as the thinning**, because the fit already contains the
174
- * angular dependence; multiplying by a separate Fresnel term would apply it twice.
175
- *
176
- * **How much this is worth was measured, and it is worth less than it looks on a dark room.**
177
- * On demo/dev/ibl.html?ladder=1 the whole ladder moved by at most 0.2 of 255 when the old term was
178
- * replaced. That is not the term failing: at high roughness the surface reads the coarsest levels
179
- * of the chain, and in a room of five dark walls and one bright one those levels are dark, so a
180
- * weight three times larger against a near-zero radiance is still near zero. It is worth much more
181
- * against a bright environment, which is what a loaded sky is and what no captured room here is.
182
- *
183
- * **A warning about reading that page, paid for once.** The ladder falls about five-fold from
184
- * mirror to rough, and that reads as energy being lost. It is not: a mirror concentrates the one
185
- * bright wall into a sharp image and a rough surface averages it against five dark ones, so the
186
- * falloff is the room's own content. The normalised falloff also happens to track one-minus-
187
- * roughness closely, which made the old term look like the cause — and removing that term entirely
188
- * changed nothing, which is what settled it. Perturb before believing a match.
189
- *
190
- * **What it costs** is a fit rather than the integral: the published error is around one per cent
191
- * of the tabulated term, far below what an eight-bit frame can show. **What would make it wrong**
192
- * is a material model this shader does not have — a clearcoat or a sheen lobe carries its own BRDF
193
- * and its own integral, and this fit is Trowbridge-Reitz with Smith masking and nothing else.
194
- */
195
- vec2 envBrdfApprox(float ndv, float roughness) {
196
- const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
197
- const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
198
- vec4 r = roughness * c0 + c1;
199
- float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;
200
- return vec2(-1.04, 1.04) * a004 + r.zw;
201
- }
208
+ ${ENV_BRDF_GLSL}
202
209
 
203
210
  /**
204
211
  * How much of a prefiltered environment a surface returns, with the light that bounces more than
@@ -26,6 +26,10 @@ export const MAIN_GLSL = `void main() {
26
26
  vec2 fxDx = dFdx(surfaceAt.xy);
27
27
  vec2 fxDy = dFdy(surfaceAt.xy);
28
28
  if (uWriteMode.y != 0.0 && !ditherKeeps(uWriteMode.y)) discard;
29
+ /* A draw's dissolve cuts the surface away before anything is shaded: overlay.ts. */
30
+ if (SURFACE_OVERLAY) {
31
+ if (overlayCuts(surfaceAt.xy)) discard;
32
+ }
29
33
 
30
34
  /*
31
35
  * Surface colour and its coverage, resolved before anything else — including before it is
@@ -140,6 +144,11 @@ export const MAIN_GLSL = `void main() {
140
144
  }
141
145
 
142
146
  vec3 lit = albedo;
147
+ /* How much of the environment the frame shows here, and how a found reflection would land:
148
+ reflectionSurface.ts. None where nothing is reflected. */
149
+ vec3 reflectedProbe = vec3(0.0);
150
+ vec3 reflectedTint = vec3(0.0);
151
+ float reflectedRoughness = 1.0;
143
152
  /* Skin's diffuse, held before emission for the screen-space blur's own pass: models.ts. */
144
153
  vec3 diffuseAlone = vec3(0.0);
145
154
  /* The light arriving at a glass pane from behind it, gathered in each light loop below and added
@@ -187,7 +196,10 @@ export const MAIN_GLSL = `void main() {
187
196
  * right: a mipped colour image, sampled under uniform control flow, exactly as uAlbedo is
188
197
  * below. The rule is about non-uniform branches, and this branch is on a uniform.
189
198
  */
199
+ vec3 unmapped = n;
190
200
  if (uNormalStrength > 0.0) n = normalMapped(n, surfaceAt);
201
+ /* A draw's wrinkles, by region, over whatever the map made: overlay.ts. */
202
+ if (SURFACE_OVERLAY) n = overlayWrinkle(n, unmapped, surfaceAt.xy);
191
203
 
192
204
  /*
193
205
  * Microscopic relief: the surface's own texture, as a turn of the normal rather than a change
@@ -543,7 +555,7 @@ export const MAIN_GLSL = `void main() {
543
555
  * specularLobe's header argues for at length.
544
556
  */
545
557
  float sunVoH = max(dot(normalize(uCameraPos - vWorldPos), halfway), 0.0);
546
- vec3 sunSpec = mix(specColor, vec3(1.0), pow(1.0 - sunVoH, 5.0) * metal);
558
+ vec3 sunSpec = highlightTint(specColor, sunVoH, metal);
547
559
  /*
548
560
  * **Held back until after the environment blend, because that blend is a mix and would
549
561
  * otherwise erase this.**
@@ -562,8 +574,12 @@ export const MAIN_GLSL = `void main() {
562
574
  * Zero for the whole world that carries no specular attribute and no metalness, exactly as
563
575
  * before: \`sunSpec\` is \`vec3(vSpecular)\` at metal 0 and \`vSpecular\` defaults to 0.
564
576
  */
565
- vec3 sunHighlight =
566
- sunColor * specularLobe(max(dot(n, halfway), 0.0), surfaceRoughness) * sunSpec * sunShade * sunFacing;
577
+ /* GGX's own where the surface asks (\`physicalSpecular\`): the sun is a point, so nothing widens it. */
578
+ float sunNdh = max(dot(n, halfway), 0.0);
579
+ float sunLobe = physicalSpecular()
580
+ ? physicalLobe(sunNdh, ndl, dot(n, normalize(uCameraPos - vWorldPos)), surfaceRoughness, 0.0, 1.0)
581
+ : specularLobe(sunNdh, surfaceRoughness);
582
+ vec3 sunHighlight = sunColor * sunLobe * sunSpec * sunShade * sunFacing;
567
583
  if (modelled) sunHighlight = sunModelSpecular;
568
584
  /*
569
585
  * The dielectric's share goes in here, where it has always gone, so the environment blend
@@ -912,6 +928,15 @@ export const MAIN_GLSL = `void main() {
912
928
  environment * mix(vec3(1.0), albedo, metal),
913
929
  weight
914
930
  );
931
+ reflectedTint = mix(vec3(1.0), albedo, metal) * weight;
932
+ reflectedProbe = environment * reflectedTint;
933
+ reflectedRoughness = surfaceRoughness;
934
+ }
935
+ /* The reflection pass's surface half leaves here, before the lamps: reflectionSurface.ts. */
936
+ if (REFLECTION_SURFACE) {
937
+ reflectionSurfaceOut(reflectedProbe, reflectedTint, reflectedRoughness, ormOcclusion,
938
+ reflectionAir());
939
+ return;
915
940
  }
916
941
  /*
917
942
  * The sun's highlight, on top of whatever the surface reflects rather than underneath it —
@@ -1067,6 +1092,10 @@ export const MAIN_GLSL = `void main() {
1067
1092
  float cookie;
1068
1093
  /* The light's lighting channels, a mask: 1 unless it names others. See \`uLightIesAxis\`. */
1069
1094
  float lightChannels = 1.0;
1095
+ /* The light's own falloff exponent, or 0 for the frame's falloff. See \`uLightCone\`. */
1096
+ float lightExponent = 0.0;
1097
+ /* A clustered rectangle's barn doors: their cosine and length, none at 0. See \`areaBarnDoors\`. */
1098
+ vec2 areaDoors = vec2(0.0);
1070
1099
  /*
1071
1100
  * **A rectangle in the froxel table** — one past the fixed arm's \`maxAreaLights\` — which
1072
1101
  * its record says with a negative radius, its reach negated. Its fields come out of the
@@ -1117,8 +1146,8 @@ export const MAIN_GLSL = `void main() {
1117
1146
  lightWeight = sizeAndWeight.y;
1118
1147
  /*
1119
1148
  * **The fixture, read only for a light that has one.** What a plain light's record holds in
1120
- * the last three texels is exactly these values, so a light carrying no shadow slot, cone,
1121
- * profile or cookie shades the same whether they are read or assumed, and at heavy overlap
1149
+ * the last four texels is exactly these values, so a light carrying no shadow slot, cone,
1150
+ * profile, cookie, channels or falloff of its own shades the same whether they are read or assumed, and at heavy overlap
1122
1151
  * reading them was a third of the pass. A branch on a value from a texture around
1123
1152
  * \`texelFetch\` is not the 2026-08-07 case: an integer fetch takes no derivative.
1124
1153
  *
@@ -1138,6 +1167,7 @@ export const MAIN_GLSL = `void main() {
1138
1167
  uvec4 rec2 = clusterTexel(record + 2);
1139
1168
  uvec4 rec3 = clusterTexel(record + 3);
1140
1169
  uvec4 rec4 = clusterTexel(record + 4);
1170
+ uvec4 rec5 = clusterTexel(record + 5);
1141
1171
  shadowSlot = clusteredArea ? -1 : int(uintBitsToFloat(rec2.x));
1142
1172
  areaTwoSided = uintBitsToFloat(rec2.x);
1143
1173
  lightDir = vec3(
@@ -1150,6 +1180,8 @@ export const MAIN_GLSL = `void main() {
1150
1180
  iesAxis = vec3(
1151
1181
  uintBitsToFloat(rec4.x), uintBitsToFloat(rec4.y), uintBitsToFloat(rec4.z));
1152
1182
  lightChannels = uintBitsToFloat(rec4.w);
1183
+ lightExponent = uintBitsToFloat(rec5.x);
1184
+ areaDoors = vec2(uintBitsToFloat(rec5.y), uintBitsToFloat(rec5.z));
1153
1185
  }
1154
1186
  }
1155
1187
  } else {
@@ -1162,7 +1194,8 @@ export const MAIN_GLSL = `void main() {
1162
1194
  lightWeight = uLightWeight[i];
1163
1195
  shadowSlot = i;
1164
1196
  lightDir = uLightDir[i];
1165
- lightCone = uLightCone[i];
1197
+ lightCone = uLightCone[i].xy;
1198
+ lightExponent = uLightCone[i].z;
1166
1199
  iesProfile = uLightIesProfile[i];
1167
1200
  iesAxis = uLightIesAxis[i].xyz;
1168
1201
  lightChannels = uLightIesAxis[i].w;
@@ -1188,12 +1221,15 @@ export const MAIN_GLSL = `void main() {
1188
1221
  * froxel it is binned into has no edge in the picture.
1189
1222
  */
1190
1223
  if (clusteredArea) {
1191
- float areaForm = areaSignedForm(n, lightPos, lightDir, iesAxis, lightCone);
1224
+ vec3 seenCentre = lightPos;
1225
+ vec2 seenHalf = lightCone;
1226
+ if (!areaBarnDoors(seenCentre, lightDir, iesAxis, seenHalf, areaDoors)) continue;
1227
+ float areaForm = areaSignedForm(n, seenCentre, lightDir, iesAxis, seenHalf);
1192
1228
  float areaWindow = clamp(1.0 - pow(length(lightPos - vWorldPos) / max(lightRadius, 1e-4), 4.0), 0.0, 1.0);
1193
1229
  areaForm = mix(max(0.0, areaForm), abs(areaForm), areaTwoSided);
1194
1230
  if (areaForm > 0.0) {
1195
1231
  areaLightAdd(
1196
- lightPos, lightDir, iesAxis, lightCone, lightColor * (areaWindow * areaWindow), areaForm,
1232
+ seenCentre, lightDir, iesAxis, seenHalf, lightColor * (areaWindow * areaWindow), areaForm,
1197
1233
  1.0, vec3(1.0), n, albedo, metal, specColor, surfaceRoughness, modelled,
1198
1234
  lampOpen, lampShadowed
1199
1235
  );
@@ -1244,7 +1280,16 @@ export const MAIN_GLSL = `void main() {
1244
1280
  * same way three.js windows its own, so the light still ends where it says it does.
1245
1281
  */
1246
1282
  float falloff;
1247
- if (uLightFalloff == 1) {
1283
+ /*
1284
+ * **A light may name its own falloff, and then it is Unreal's for a light without
1285
+ * inverse-square falloff**: \`(1 - (d/R)^2)^n\` with no distance term, so a fill authored to
1286
+ * light a whole hall across a hundred metres does, where the inverse square would have it
1287
+ * gone a few metres out. Zero is the frame's own rule below, which every light was before.
1288
+ */
1289
+ if (lightExponent > 0.0) {
1290
+ float reach = dist / max(lightRadius, 1e-4);
1291
+ falloff = pow(clamp(1.0 - reach * reach, 0.0, 1.0), lightExponent);
1292
+ } else if (uLightFalloff == 1) {
1248
1293
  float window = clamp(1.0 - pow(dist / max(lightRadius, 1e-4), 4.0), 0.0, 1.0);
1249
1294
  falloff = window * window / max(dist * dist, 0.01);
1250
1295
  } else {
@@ -1507,7 +1552,11 @@ export const MAIN_GLSL = `void main() {
1507
1552
  photometric = mix(1.0, iesGain, step(0.0, iesProfile));
1508
1553
  }
1509
1554
 
1510
- float shape = (uLightFalloff == 1 ? falloff : falloff * falloff) * coneFalloff * photometric;
1555
+ /* A light's own exponent already is its whole curve; the frame's smooth rule squares its own. */
1556
+ float shape =
1557
+ (lightExponent > 0.0 || uLightFalloff == 1 ? falloff : falloff * falloff) *
1558
+ coneFalloff *
1559
+ photometric;
1511
1560
  glassGlow += lightColor * backNdl * shape * lightWeight * shaded * lampGlass;
1512
1561
  if (ndl <= 0.0 && !modelledBehind) continue;
1513
1562
  /*
@@ -1584,16 +1633,15 @@ export const MAIN_GLSL = `void main() {
1584
1633
  if (vSpecular > 0.0 || metal > 0.0) {
1585
1634
  vec3 toEyeLamp = normalize(uCameraPos - vWorldPos);
1586
1635
  vec3 lampHalfway = normalize(toLight / max(dist, 1e-4) + toEyeLamp);
1587
- /* The same grazing whitening the sun's highlight takes, and zero at metal 0. */
1588
- vec3 lampSpec = mix(
1589
- specColor,
1590
- vec3(1.0),
1591
- pow(1.0 - max(dot(toEyeLamp, lampHalfway), 0.0), 5.0) * metal
1592
- );
1636
+ /* The same grazing whitening the sun's highlight takes, and zero at metal 0 for the look. */
1637
+ vec3 lampSpec = highlightTint(specColor, max(dot(toEyeLamp, lampHalfway), 0.0), metal);
1638
+ /* The look's lobe, or GGX's own where the surface asks for it: see \`physicalLobe\`. */
1639
+ float lampNdh = max(dot(n, lampHalfway), 0.0);
1640
+ float lampLobe = physicalSpecular()
1641
+ ? physicalLobe(lampNdh, ndl, dot(n, toEyeLamp), surfaceRoughness, lightSourceRadius, dist)
1642
+ : sphereLobe(lampNdh, surfaceRoughness, lightSourceRadius, dist);
1593
1643
  /* Held back with the diffuse half, and shadowed by the same term it always was. */
1594
- vec3 lampHighlight =
1595
- lightColor * sphereLobe(max(dot(n, lampHalfway), 0.0), surfaceRoughness, lightSourceRadius, dist)
1596
- * lampSpec * shape * lightWeight;
1644
+ vec3 lampHighlight = lightColor * lampLobe * lampSpec * shape * lightWeight;
1597
1645
  lampOpen += lampHighlight;
1598
1646
  lampShadowed += lampHighlight * shaded * lampGlass;
1599
1647
  }
@@ -1637,10 +1685,18 @@ export const MAIN_GLSL = `void main() {
1637
1685
  vec3 centre = uAreaLightPos[a];
1638
1686
  vec3 right = uAreaLightRight[a];
1639
1687
  vec3 up = uAreaLightUp[a];
1640
- vec2 halfSize = uAreaLightSize[a];
1688
+ vec2 halfSize = uAreaLightSize[a].xy;
1689
+ /*
1690
+ * **The part of the rectangle this fragment sees past its barn doors** (\`areaBarnDoors\`),
1691
+ * which its light and highlight come from. Its shadow keeps the whole rectangle's centre and
1692
+ * size below, because that is what its map was baked from.
1693
+ */
1694
+ vec3 seenCentre = centre;
1695
+ vec2 seenHalf = halfSize;
1696
+ if (!areaBarnDoors(seenCentre, right, up, seenHalf, uAreaLightSize[a].zw)) continue;
1641
1697
 
1642
1698
  /* The form factor and its sign, which is the sidedness: see \`areaSignedForm\`. */
1643
- float signedForm = areaSignedForm(n, centre, right, up, halfSize);
1699
+ float signedForm = areaSignedForm(n, seenCentre, right, up, seenHalf);
1644
1700
  float form = mix(max(0.0, signedForm), abs(signedForm), uAreaLightTwoSided[a]);
1645
1701
  /* The form factor is linear in the normal, so the one seen from behind is its negation. */
1646
1702
  float backForm = glassTransmission > 0.0
@@ -1757,7 +1813,7 @@ export const MAIN_GLSL = `void main() {
1757
1813
  if (form <= 0.0) continue;
1758
1814
  /* The diffuse, the model's answer or the lobe integrated over the rectangle: see areaLight.ts. */
1759
1815
  areaLightAdd(
1760
- centre, right, up, halfSize, uAreaLightColor[a], form, areaOccl, areaGlass,
1816
+ seenCentre, right, up, seenHalf, uAreaLightColor[a], form, areaOccl, areaGlass,
1761
1817
  n, albedo, metal, specColor, surfaceRoughness, modelled, lampOpen, lampShadowed
1762
1818
  );
1763
1819
  }
@@ -1905,6 +1961,9 @@ export const MAIN_GLSL = `void main() {
1905
1961
  * mix(1.0, min(lightShade, sunShade), EMISSIVE_SHADOW_SHARE) * fxGlow;
1906
1962
  /* And a lit window's glow, which the scene's lit share already times: not gated on night. */
1907
1963
  lit += windowGlow * uEmission.x;
1964
+ /* And a draw's overlay: its rim, its dissolve's band and the colour laid over. Not gated on the
1965
+ night, as emission is: a glow the surface wears rather than light it gives off. overlay.ts. */
1966
+ if (SURFACE_OVERLAY) lit += overlayGlow(surfaceAt.xy, n);
1908
1967
 
1909
1968
  #if NIGHT_EMISSIVE
1910
1969
  /*
@@ -2103,6 +2162,10 @@ export const MAIN_GLSL = `void main() {
2103
2162
  return;
2104
2163
  }
2105
2164
 
2165
+ /* WebGL2's frame writes the reflection maps itself, with the fog already reckoned above. */
2166
+ if (REFLECTION_MAPS) {
2167
+ reflectionSurfaceOut(reflectedProbe, reflectedTint, reflectedRoughness, ormOcclusion, fog);
2168
+ }
2106
2169
  outColor = vec4(shaded, alpha);
2107
2170
  }
2108
2171
  `;