@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
@@ -350,9 +350,30 @@ export function towardCameraFade(direction: ReadonlyVec3, viewDirection: Readonl
350
350
  return Math.min(1, Math.max(0, (along + 1) / 2));
351
351
  }
352
352
 
353
+ /**
354
+ * What a `ReflectiveSurface` is made with: its box, which way the surfaces inside it face, and how
355
+ * much and how far they reflect. Every number but the box has a default; see each field.
356
+ */
353
357
  export interface ReflectiveSurfaceOptions extends DecalBox {
354
- /** How much of what the ray finds lands on the surface, 0 to 1. */
358
+ /**
359
+ * How much of what the ray finds lands on the surface, 0 to 1: one share over the box, or with
360
+ * `fresnel` the surface's reflectance head-on.
361
+ */
355
362
  readonly strength?: number;
363
+ /**
364
+ * Whether `strength` is the surface's reflectance head-on, F0, rather than one share over the
365
+ * box. Each pixel then returns `F0 · A + B` of what its ray finds, the split-sum environment BRDF
366
+ * at its own view and `roughness` — Karis's fit, the one the lit stage reflects its environment
367
+ * by — so a floor returns more toward the horizon than underfoot, and the share follows the
368
+ * camera as Unreal weighs its reflections. **False by default**, the one share as before.
369
+ *
370
+ * **What it gives up**: the pass has no material buffer, so the roughness and F0 are the box's,
371
+ * not each pixel's, and nothing occludes the reflection where the lit stage's ambient occlusion
372
+ * would. A floor of one material is what it is for.
373
+ */
374
+ readonly fresnel?: boolean;
375
+ /** The surface's roughness, 0 to 1, that `fresnel` weighs by. 0, a polished surface, if absent. */
376
+ readonly roughness?: number;
356
377
  /** How far a ray may travel, world metres. */
357
378
  readonly reachM?: number;
358
379
  /** How deep a depth sample is treated as being. See `ScreenSpaceMarch.thicknessM`. */
@@ -382,6 +403,9 @@ export class ReflectiveSurface {
382
403
  readonly tint = new Float32Array(3);
383
404
 
384
405
  strength: number;
406
+ /** See `ReflectiveSurfaceOptions.fresnel`. */
407
+ fresnel: boolean;
408
+ roughness: number;
385
409
  reachM: number;
386
410
  thicknessM: number;
387
411
  steps: number;
@@ -405,6 +429,8 @@ export class ReflectiveSurface {
405
429
  /* Half, because a mirror is the rare case: a wet floor, polished stone and a car bonnet all
406
430
  return a fraction of what lands on them, and a consumer who wants a mirror asks for one. */
407
431
  this.strength = options.strength ?? 0.5;
432
+ this.fresnel = options.fresnel ?? false;
433
+ this.roughness = options.roughness ?? 0;
408
434
  /* Eight metres covers a room and a street corner. Further costs steps rather than accuracy,
409
435
  since the step is the reach divided by them. */
410
436
  this.reachM = options.reachM ?? 8;
@@ -469,6 +495,8 @@ export interface RecordedReflection {
469
495
  readonly axis: Float32Array;
470
496
  readonly tint: Float32Array;
471
497
  strength: number;
498
+ fresnel: boolean;
499
+ roughness: number;
472
500
  reachM: number;
473
501
  thicknessM: number;
474
502
  steps: number;
@@ -482,6 +510,8 @@ function newRecord(): RecordedReflection {
482
510
  axis: new Float32Array(3),
483
511
  tint: new Float32Array(3),
484
512
  strength: 0.5,
513
+ fresnel: false,
514
+ roughness: 0,
485
515
  reachM: 8,
486
516
  thicknessM: 0.25,
487
517
  steps: 24,
@@ -537,6 +567,8 @@ export class ReflectionQueue {
537
567
  record.axis.set(surface.axis);
538
568
  record.tint.set(surface.tint);
539
569
  record.strength = Math.min(1, Math.max(0, surface.strength));
570
+ record.fresnel = surface.fresnel;
571
+ record.roughness = Math.min(1, Math.max(0, surface.roughness));
540
572
  record.reachM = Math.max(0, surface.reachM);
541
573
  record.thicknessM = Math.max(0, surface.thicknessM);
542
574
  record.steps = Math.min(32, Math.max(1, Math.round(surface.steps)));
@@ -27,7 +27,9 @@
27
27
  * 3. **Upsample**, additively back up the chain through a tent filter.
28
28
  *
29
29
  * Deliberately absent, because they were asked against by name: lens dirt, anamorphic streaks
30
- * and ghosting. One threshold, one radius, one strength.
30
+ * and ghosting. One threshold, one radius, one strength — and, since 4.8.6, how a colour comes in
31
+ * past the threshold and a tint for each octave, which is how another engine's stage is matched
32
+ * rather than how this one looks: see `BloomResponse` in `bloomChain.ts`.
31
33
  */
32
34
 
33
35
  /**
@@ -128,6 +130,11 @@ uniform sampler2D uSource;
128
130
  uniform vec2 uTexel;
129
131
  /** In scene units. 1.0 is "brighter than white", which is the useful place to start. */
130
132
  uniform float uThreshold;
133
+ /**
134
+ * Scene units over which a colour comes in past the threshold, or 0 to subtract the threshold
135
+ * instead. See \`BloomResponse.ramp\`.
136
+ */
137
+ uniform float uRamp;
131
138
 
132
139
  out vec4 fragColor;
133
140
 
@@ -144,6 +151,8 @@ ${BLOOM_BRIGHTNESS}
144
151
  vec3 fetch(vec2 uv) {
145
152
  vec3 c = min(textureLod(uSource, uv, 0.0).rgb, vec3(BLOOM_CEILING));
146
153
  float bright = brightness(c);
154
+ /* A ramp keeps a share of the colour, all of it a ramp's width past the line. */
155
+ if (uRamp > 0.0) return c * clamp((bright - uThreshold) / uRamp, 0.0, 1.0);
147
156
  return c * (max(bright - uThreshold, 0.0) / max(bright, 1e-4));
148
157
  }
149
158
 
@@ -194,7 +203,9 @@ ${BLOOM_BOXES}
194
203
  /**
195
204
  * Stage three: a 3x3 tent, added into the level above.
196
205
  *
197
- * **Additive rather than a mix, and each octave counts the same.** A lone bright pixel arrives
206
+ * **Additive rather than a mix, and each octave counts the same** unless a tint says otherwise:
207
+ * the level being added onto is multiplied by its own tint as it is, by the blend, and the deepest
208
+ * by `uScale` on the first draw. White everywhere is exactly the sum. A lone bright pixel arrives
198
209
  * at level n divided by four to the n, spread over four to the n pixels, so summing the levels
199
210
  * lifts its own pixel by about a third and lays a long shallow tail around it. That falloff is
200
211
  * what a real glare looks like; a single gaussian gives a disc with an end to it.
@@ -209,6 +220,12 @@ precision highp float;
209
220
  in vec2 vUv;
210
221
 
211
222
  uniform sampler2D uSource;
223
+ /**
224
+ * What the level being read is multiplied by: its tint, on the first draw up from the deepest
225
+ * level, and white on every other — each other level's tint is applied to the level below as it
226
+ * is added onto, by the blend. See \`BloomResponse.tints\`.
227
+ */
228
+ uniform vec3 uScale;
212
229
  /** How far the tent reaches, in UV. One number for the whole chain. */
213
230
  uniform float uRadius;
214
231
 
@@ -227,6 +244,6 @@ void main() {
227
244
  vec3 i = textureLod(uSource, vUv + vec2( r, -r), 0.0).rgb;
228
245
 
229
246
  vec3 sum = e * 4.0 + (b + d + f + h) * 2.0 + (a + c + g + i);
230
- fragColor = vec4(sum * 0.0625, 1.0);
247
+ fragColor = vec4(sum * 0.0625 * uScale, 1.0);
231
248
  }
232
249
  `;
@@ -107,7 +107,13 @@ void main() {
107
107
  * the glow. A single falloff gives either a hard line with no atmosphere or a
108
108
  * soft smudge with no filament.
109
109
  */
110
- float across = 1.0 - abs(vSide);
110
+ /*
111
+ * Clamped, because a multisampled target shades a pixel on the quad's edge at its centre, which
112
+ * may lie outside the quad: there 'vSide' runs past one, 'across' goes negative, and 'pow' of a
113
+ * negative number is NaN. One NaN pixel is a dark speck; bloom then spreads it into a white disc
114
+ * the size of the blur. Found by a fight drawing short arcs at 4 samples.
115
+ */
116
+ float across = clamp(1.0 - abs(vSide), 0.0, 1.0);
111
117
  float core = pow(across, 10.0);
112
118
  /*
113
119
  * A *wide* halo, deliberately. This exponent is the single biggest control over
@@ -101,6 +101,12 @@ void main() {
101
101
  #endif
102
102
  #if INSTANCED
103
103
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
104
+ /* The bottom row is the instance's texture cell, rebuilt as the row it stands for: see flat. */
105
+ vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
106
+ model[0].w = 0.0;
107
+ model[1].w = 0.0;
108
+ model[2].w = 0.0;
109
+ model[3].w = 1.0;
104
110
  #else
105
111
  mat4 model = uModel;
106
112
  #endif
@@ -125,7 +131,12 @@ void main() {
125
131
  vLightPosition = uLightViewProj * vec4(bent, world.w);
126
132
  gl_Position = vLightPosition;
127
133
  #if CUTOUT
134
+ #if INSTANCED
135
+ /* A cutout instance's shadow is cut by its own cell, as its picture is. */
136
+ vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
137
+ #else
128
138
  vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
139
+ #endif
129
140
  vAlphaCutout = uAlphaCutout;
130
141
  #endif
131
142
  #if GLASS
@@ -30,7 +30,16 @@ void anisotropicSurface() {
30
30
  direction.x * uModelParams[0].y - direction.y * uModelParams[0].z,
31
31
  direction.x * uModelParams[0].z + direction.y * uModelParams[0].y
32
32
  );
33
- aAlong = normalize(mTangent * turned.x + mBitangent * turned.y);
33
+ vec3 along = mTangent * turned.x + mBitangent * turned.y;
34
+ /*
35
+ * A mesh with neither tangents nor texture coordinates has no frame to take a direction from,
36
+ * and normalising the zero it gives was a NaN that drew the whole surface black. Any direction in
37
+ * the surface stands in, the one across the world's up, so the highlight is stretched somewhere.
38
+ */
39
+ if (dot(along, along) < 1e-12) {
40
+ along = cross(abs(mNormal.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0), mNormal);
41
+ }
42
+ aAlong = normalize(along);
34
43
  aAcross = cross(mNormal, aAlong);
35
44
  aStrength = strength;
36
45
  }
@@ -50,12 +59,37 @@ float anisotropicLobe(vec3 h, float sourceRadius, float dist) {
50
59
  return energy / max(d * d, 1e-8);
51
60
  }
52
61
 
62
+ /**
63
+ * The same stretch as GGX's own BRDF, \`π · D · Vis · N·L\`, for a surface whose highlight is
64
+ * physical: the distribution normalised, and Smith's height-correlated masking stretched with it
65
+ * (Heitz 2014, as Filament writes it). \`anisotropicPhysicalLobe\` in anisotropicLobe.ts.
66
+ */
67
+ float anisotropicPhysical(vec3 l, vec3 h, float ndl, float sourceRadius, float dist) {
68
+ float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);
69
+ float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;
70
+ float grow = sourceRadius / max(2.0 * dist, 1e-3);
71
+ float along = clamp(alongWidth + grow, alongWidth, 1.0);
72
+ float across = clamp(alpha + grow, alpha, 1.0);
73
+ float x = dot(aAlong, h) / along;
74
+ float y = dot(aAcross, h) / across;
75
+ float z = dot(mNormal, h);
76
+ float d = x * x + y * y + z * z;
77
+ float distribution = 1.0 / max(3.14159265 * along * across * d * d, 1e-8);
78
+ float ndv = max(dot(mNormal, mToEye), 1e-4);
79
+ float viewed = ndl * length(vec3(along * dot(aAlong, mToEye), across * dot(aAcross, mToEye), ndv));
80
+ float lit = ndv * length(vec3(along * dot(aAlong, l), across * dot(aAcross, l), ndl));
81
+ return 3.14159265 * distribution * (0.5 / max(viewed + lit, 1e-5)) * ndl;
82
+ }
83
+
53
84
  void anisotropicLight(vec3 l, float sourceRadius, float dist) {
54
85
  float ndl = max(dot(mNormal, l), 0.0);
55
86
  vec3 h = normalize(l + mToEye);
56
- vec3 tint = mix(mSpecColor, vec3(1.0), pow(1.0 - max(dot(mToEye, h), 0.0), 5.0) * mMetal);
87
+ vec3 tint = highlightTint(mSpecColor, max(dot(mToEye, h), 0.0), mMetal);
57
88
  mDiffuse = mAlbedo * ndl * (1.0 - mMetal);
58
- mSpecular = ndl > 0.0 ? tint * anisotropicLobe(h, sourceRadius, dist) : vec3(0.0);
89
+ float lobe = physicalSpecular()
90
+ ? anisotropicPhysical(l, h, ndl, sourceRadius, dist)
91
+ : anisotropicLobe(h, sourceRadius, dist);
92
+ mSpecular = ndl > 0.0 ? tint * lobe : vec3(0.0);
59
93
  }
60
94
 
61
95
  /* The environment's normal, bent toward the stretch by its strength and the surface's roughness. */
@@ -0,0 +1,142 @@
1
+ /**
2
+ * One rectangle's light on a fragment: its form factor, and what it adds once its occlusion is
3
+ * known. **The one statement of it**, called by the fixed loop over the first `maxAreaLights`
4
+ * rectangles and by the froxel table's for the rest, so the two arms shade a rectangle with one
5
+ * body rather than two transcriptions of it — the zero-pixel gate between them means something
6
+ * only if that is so.
7
+ *
8
+ * Depth zero and after the models, whose `modelLight` it calls; see `flatFrag`.
9
+ */
10
+ export const AREA_LIGHT_GLSL = /* glsl */ `
11
+ /**
12
+ * The rectangle's signed form factor at this fragment: its corners **wound so a surface on the
13
+ * emitting side sees them counter-clockwise**, which is what makes it positive there. The natural
14
+ * order — minus, plus, plus, minus — winds the other way, and every one-sided light then clamped to
15
+ * zero and the frame was black. The sign is the sidedness: a one-sided rectangle takes the positive
16
+ * part and a two-sided one the magnitude.
17
+ */
18
+ float areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {
19
+ vec3 c0 = centre - right * halfSize.x + up * halfSize.y;
20
+ vec3 c1 = centre + right * halfSize.x + up * halfSize.y;
21
+ vec3 c2 = centre + right * halfSize.x - up * halfSize.y;
22
+ vec3 c3 = centre - right * halfSize.x - up * halfSize.y;
23
+ return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);
24
+ }
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
+
72
+ /**
73
+ * What one rectangle of colour \`color\` and form factor \`form\` adds to the lamps' two sums, open
74
+ * and shadowed by \`occl\` and \`glass\`.
75
+ *
76
+ * **A model answers a rectangle as it answers a lamp at its centre**, carried at the rectangle's
77
+ * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by the
78
+ * form factor. An approximation, stated: a model's lobe is not integrated over the rectangle as the
79
+ * standard lobe is, so a glossy model under a large softbox shows a lobe from its centre rather than
80
+ * its shape.
81
+ *
82
+ * **The standard lobe is integrated over the rectangle**, rather than sampled at one point on it.
83
+ * What stood here once handed the closest point on the rectangle to \`sphereLobe\` and scaled it by
84
+ * \`form\`, the *diffuse* form factor. Measured against a brute-force integral by
85
+ * \`scripts/areaSpecular.mjs\`, that lost the reflection rather than blurring it: on polished metal
86
+ * facing a softbox it returned 0.000233 where the integral is 0.9207, and head-on on a rough surface
87
+ * it was 3.7 times too bright. Worst 287% either way; this is 40.1% where the light is what the
88
+ * surface reflects and 79.0% for one overhead. \`quadCoverage\` says what fraction of the specular
89
+ * lobe the rectangle covers and the environment BRDF says how much the surface returns, the split
90
+ * the probe path uses. **\`dfg.x + dfg.y\` and not \`envSpecularEnergy\`**, deliberately: the
91
+ * compensation returns the multiply-scattered share, and every direct lobe in this shader is
92
+ * single-scattering, so adding it here returns energy the light never had. Measured at 211%
93
+ * against 40.1%.
94
+ */
95
+ void areaLightAdd(
96
+ vec3 centre,
97
+ vec3 right,
98
+ vec3 up,
99
+ vec2 halfSize,
100
+ vec3 color,
101
+ float form,
102
+ float occl,
103
+ vec3 glass,
104
+ vec3 n,
105
+ vec3 albedo,
106
+ float metal,
107
+ vec3 specColor,
108
+ float roughness,
109
+ bool modelled,
110
+ inout vec3 lampOpen,
111
+ inout vec3 lampShadowed
112
+ ) {
113
+ if (modelled) {
114
+ vec3 areaL = normalize(centre - vWorldPos);
115
+ modelLight(areaL, 0.0, 0.0);
116
+ vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form) * color;
117
+ lampOpen += areaModel;
118
+ lampShadowed += areaModel * occl * glass;
119
+ return;
120
+ }
121
+ /* Held back with the point lamps', and shadowed by the same term it always was. */
122
+ vec3 areaDiffuse = albedo * color * form * (1.0 - metal);
123
+ lampOpen += areaDiffuse;
124
+ lampShadowed += areaDiffuse * occl * glass;
125
+
126
+ if (vSpecular > 0.0 || metal > 0.0) {
127
+ vec3 c0 = centre - right * halfSize.x + up * halfSize.y;
128
+ vec3 c1 = centre + right * halfSize.x + up * halfSize.y;
129
+ vec3 c2 = centre + right * halfSize.x - up * halfSize.y;
130
+ vec3 c3 = centre - right * halfSize.x - up * halfSize.y;
131
+ vec3 toEyeArea = normalize(uCameraPos - vWorldPos);
132
+ float coverage = quadCoverage(n, vWorldPos, toEyeArea, roughness, c0, c1, c2, c3);
133
+ if (coverage > 0.0) {
134
+ vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), roughness);
135
+ vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);
136
+ vec3 areaHighlight = areaSpec * color * coverage;
137
+ lampOpen += areaHighlight;
138
+ lampShadowed += areaHighlight * occl * glass;
139
+ }
140
+ }
141
+ }
142
+ `;
@@ -29,7 +29,12 @@ 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';
33
38
 
34
39
  /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
35
40
  export function noLitSwitches(): Record<LitSwitch, boolean> {
@@ -39,6 +44,7 @@ export function noLitSwitches(): Record<LitSwitch, boolean> {
39
44
  LIGHT_FIXTURES: false,
40
45
  SURFACE_EFFECTS: false,
41
46
  DRIFT_LIGHT: false,
47
+ PHYSICAL_SPECULAR: false,
42
48
  };
43
49
  }
44
50
 
@@ -61,7 +67,8 @@ export interface LitSwitches {
61
67
  * first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
62
68
  * volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
63
69
  * 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.
70
+ * registers, 4,641 and 96 with all four out. `PHYSICAL_SPECULAR` is a sixth, declared with the
71
+ * models so that theirs keep their ids (`modelsGlsl`).
65
72
  */
66
73
  export function litSwitchesGlsl(switches: LitSwitches): string {
67
74
  return /* glsl */ `
@@ -14,7 +14,9 @@ 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
+ import { AREA_LIGHT_GLSL } from './areaLight.ts';
19
+ import { NORMAL_MAP_GLSL } from './normalMap.ts';
18
20
  import { DRIFT_LIGHT_GLSL } from './driftLight.ts';
19
21
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.ts';
20
22
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.ts';
@@ -107,7 +109,17 @@ layout(location = 11) in vec4 aInstanceModel0;
107
109
  layout(location = 12) in vec4 aInstanceModel1;
108
110
  layout(location = 13) in vec4 aInstanceModel2;
109
111
  layout(location = 14) in vec4 aInstanceModel3;
110
- layout(location = 15) in vec3 aInstanceTint;
112
+ /**
113
+ * The instance's tint, and its opacity in w: \`MeshInstances.alphas\`, 1 where none was given — or,
114
+ * in a lightmapped batch's pipeline, its region of the page (\`MeshInstances.lightmapRegions\`).
115
+ */
116
+ layout(location = 15) in vec4 aInstanceTint;
117
+ /*
118
+ * On in the pipeline a lightmapped material draws an instanced batch with, where the lane above is
119
+ * a region and not a tint: the second coordinates leave scaled and offset by it, under a tint and an
120
+ * opacity of one. See lightmap.ts.
121
+ */
122
+ const bool LIGHTMAP_REGIONS = false; // wgsl:override
111
123
  #else
112
124
  uniform mat4 uModel;
113
125
  /**
@@ -233,7 +245,17 @@ void main() {
233
245
  #endif
234
246
  #if INSTANCED
235
247
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
236
- vec3 tint = aInstanceTint;
248
+ /*
249
+ * **The bottom row is the instance's texture cell** (\`MeshInstances.uvRegions\`), which an
250
+ * affine placement leaves free: read, and rebuilt as the \`[0, 0, 0, 1]\` it stands for.
251
+ */
252
+ vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
253
+ model[0].w = 0.0;
254
+ model[1].w = 0.0;
255
+ model[2].w = 0.0;
256
+ model[3].w = 1.0;
257
+ vec3 tint = aInstanceTint.rgb;
258
+ if (LIGHTMAP_REGIONS) tint = vec3(1.0);
237
259
  #else
238
260
  mat4 model = uModel;
239
261
  vec3 tint = uTint;
@@ -280,17 +302,31 @@ void main() {
280
302
  vColor = aColor * tint;
281
303
  vEmissive = aEmissive;
282
304
  vSpecular = aSpecular;
305
+ #if INSTANCED
306
+ /* The instance's cell first, then the material's scale and offset. */
307
+ vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
308
+ #else
283
309
  vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
310
+ #endif
284
311
  vEmissiveColor = aEmissiveColor;
285
312
  vRoughness = aRoughness;
286
313
  vGrain = aGrain;
287
314
  vRelief = aRelief;
288
315
  vTangent = vec4(worldTangent, aTangent.w);
289
316
  #if INSTANCED
290
- /* No channel on this path; the neutral values are what every mesh read before it existed. */
317
+ /*
318
+ * No channel on this path; the neutral values are what every mesh read before it existed — but
319
+ * for the opacity, which an instance carries of its own, 1 where none was given.
320
+ */
291
321
  vSkyDirect = 1.0;
292
- vAlpha = 1.0;
322
+ vAlpha = aInstanceTint.w;
293
323
  vThickness = 1.0;
324
+ if (LIGHTMAP_REGIONS) {
325
+ /* Out of the lane's \`-1 - uv\`, through the region, and back into it. See lightmap.ts. */
326
+ vGrain = -1.0 - ((-1.0 - aGrain) * aInstanceTint.x + aInstanceTint.z);
327
+ vRelief = -1.0 - ((-1.0 - aRelief) * aInstanceTint.y + aInstanceTint.w);
328
+ vAlpha = 1.0;
329
+ }
294
330
  #else
295
331
  vSkyDirect = aChannel.y;
296
332
  vAlpha = aChannel.z;
@@ -427,6 +463,11 @@ export interface FlatShaderOptions {
427
463
  readonly surfaceEffects?: boolean;
428
464
  /** Whether it reads a DriftLight volume. True when absent. See `litSwitchesGlsl`. */
429
465
  readonly driftLight?: boolean;
466
+ /**
467
+ * Whether it carries GGX's own highlight for a material that asks (`physicalSpecular`). True
468
+ * when absent. See `modelsGlsl`.
469
+ */
470
+ readonly physicalSpecular?: boolean;
430
471
  /**
431
472
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
432
473
  *
@@ -563,13 +604,17 @@ export function flatFrag(options: FlatShaderOptions): string {
563
604
  * and `main` is unconditional by construction.
564
605
  */
565
606
  TANGENT_FRAME_GLSL,
607
+ /* After the frame it calls, unconditional for the same reason. See normalMap.ts. */
608
+ NORMAL_MAP_GLSL,
566
609
  /* Unconditional for the same reason, and called only where a cutoff was asked for. */
567
610
  CUTOUT_COVERAGE_GLSL,
568
611
  CUTOUT_DITHER_GLSL,
569
612
  DRIFT_LIGHT_GLSL,
570
613
  SURFACE_EFFECTS_GLSL,
571
614
  /* Depth zero and after the tangent frame and the lobes it calls; see models.ts. */
572
- MODELS_GLSL,
615
+ modelsGlsl(options.physicalSpecular ?? true),
616
+ /* After the models, whose answer to a rectangle it asks for. See areaLight.ts. */
617
+ AREA_LIGHT_GLSL,
573
618
  MAIN_GLSL,
574
619
  ].join('\n'),
575
620
  {
@@ -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