@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
@@ -9,4 +9,4 @@
9
9
  */
10
10
  /** Shared billboard vertex stage: used by fire, smoke and any future plume. */
11
11
  export declare const PLUME_VERT = "#version 300 es\nlayout(location = 0) in vec3 aCenter;\nlayout(location = 1) in vec2 aCorner;\nlayout(location = 2) in vec2 aSize;\nlayout(location = 3) in float aSeed;\nlayout(location = 4) in float aBlade;\n\nuniform mat4 uViewProj;\nuniform vec3 uCameraPos;\nuniform float uTime;\n/** Amplitude of the size pulse; 0 keeps the quad a fixed size. */\nuniform float uSizePulse;\n/**\n * Horizontal wind, m/s. Plumes lean with it, increasingly toward their top \u2014\n * the base is anchored to whatever is burning, the crown is free. Sharing one\n * wind with the rest of the world is the point: smoke drifting one way while\n * foliage leans another is the clearest tell that a scene is a pile of effects\n * rather than a place.\n */\nuniform vec2 uWind;\n/**\n * Whole-batch offset. Plume centres live in a static buffer because braziers\n * never move; a batch that *does* move \u2014 a tornado \u2014 shifts by a uniform rather\n * than rewriting geometry every frame.\n */\nuniform vec3 uOrigin;\n/** How strongly this plume type answers the wind; smoke far more than flame. */\nuniform float uWindResponse;\n\nout vec2 vUv;\nout float vSeed;\nout float vDistance;\nout vec3 vWorldPos;\n\n/**\n * Plumes are not a constant size: a fire flares and dies back, a smoke column\n * swells and thins. Two out-of-phase sines, offset per plume, so the whole\n * effect breathes rather than only its interior pattern animating.\n */\nfloat sizePulse(float seed) {\n return 1.0 + uSizePulse * (\n sin(uTime * 1.7 + seed * 9.1) * 0.62 +\n sin(uTime * 3.3 + seed * 4.7) * 0.38\n );\n}\n\nvoid main() {\n // Height swells more than width, the way a real flame stretches upward.\n vec3 anchor = aCenter + uOrigin;\n\n /*\n * No billboarding at all: the blade's heading is fixed in the world.\n *\n * Each plume is drawn as a CROSS of two blades 90 degrees apart (see BLADES in\n * plumeRenderer), and aSeed advances by the golden ratio per plume, so the crosses\n * themselves are scattered by the golden angle rather than aligned into a grid.\n *\n * Nothing here reads the camera, and that is the fix. Anything that does - a full\n * billboard, or merely leaning edge-on cards toward the viewer - rotates geometry\n * as the camera moves, which is exactly what reads as flat: orbiting a fire showed\n * the same silhouette from every angle. A cross cannot track the viewer, so turning\n * around a fire genuinely brings one blade broadside while the other goes edge-on.\n * That is the parallax a volume has, and it is also why no blade needs to be turned\n * to stay visible - its partner is square-on whenever it is not.\n *\n * World up stays the vertical axis: a flame that tilts with the camera reads as a\n * decal.\n */\n vec3 up = vec3(0.0, 1.0, 0.0);\n float yaw = aSeed * 6.28318530718 + aBlade * 1.57079632679;\n vec3 right = vec3(cos(yaw), 0.0, sin(yaw));\n float pulse = sizePulse(aSeed);\n vec2 size = vec2(aSize.x * mix(1.0, pulse, 0.55), aSize.y * pulse);\n\n float rise = aCorner.y * 0.5 + 0.5;\n vec3 world = anchor\n + right * (aCorner.x * size.x)\n + up * (rise * size.y);\n\n // Quadratic in height: a column bends away from its anchor rather than\n // shearing uniformly, which would slide its base off the fire.\n world.xz += uWind * (uWindResponse * rise * rise * size.y);\n\n vUv = vec2(aCorner.x * 0.5 + 0.5, aCorner.y * 0.5 + 0.5);\n vSeed = aSeed;\n vDistance = distance(world, uCameraPos);\n vWorldPos = world;\n gl_Position = uViewProj * vec4(world, 1.0);\n}\n";
12
- export declare const FIRE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nout vec4 outColor;\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\n/** Rising turbulence: octaves scrolled upward at different rates. */\nfloat turbulence(vec2 p, float t) {\n float v = valueNoise(p * 3.0 + vec2(0.0, -t * 1.9)) * 0.55;\n if (uNoiseOctaves == 1) return v / 0.55;\n v += valueNoise(p * 6.5 + vec2(t * 0.3, -t * 3.1)) * 0.28;\n if (uNoiseOctaves == 2) return v / 0.83;\n return v + valueNoise(p * 13.0 + vec2(-t * 0.2, -t * 4.6)) * 0.17;\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n vec2 uv = vUv;\n float t = uTime + vSeed * 37.0;\n\n // Taper toward the tip and pinch the sides, so the quad never shows.\n float sides = 1.0 - abs(uv.x - 0.5) * 2.0;\n float taper = sides * (1.0 - uv.y * 0.72);\n if (taper <= 0.0) discard;\n\n // Sway: the column leans and wanders instead of standing to attention.\n float sway = (valueNoise(vec2(t * 0.55, vSeed * 11.0)) - 0.5) * 0.34 * uv.y;\n vec2 p = vec2(uv.x + sway, uv.y);\n\n float n = turbulence(p, t);\n // Flame body: noise must beat a threshold that rises with height, which is\n // what makes the tip break into separate licks.\n float body = n * taper * 1.9 - uv.y * 0.55;\n if (body <= 0.02) discard;\n\n // Colour ramp: white-hot core through yellow and orange to a dark red edge.\n float heat = clamp(body * 1.5, 0.0, 1.0);\n vec3 col = mix(vec3(0.62, 0.06, 0.02), vec3(1.0, 0.45, 0.06), smoothstep(0.0, 0.45, heat));\n col = mix(col, vec3(1.0, 0.85, 0.35), smoothstep(0.45, 0.75, heat));\n col = mix(col, vec3(1.0, 0.98, 0.88), smoothstep(0.78, 1.0, heat));\n\n float alpha = clamp(body * 2.2, 0.0, 1.0) * (1.0 - smoothstep(0.72, 1.0, uv.y));\n // Additive light must lose alpha as it scatters, or mixing its colour alone\n // still makes a distant flame glow through fog and through water.\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, mediumColor(), fog);\n alpha *= 1.0 - fog;\n\n outColor = vec4(col, alpha);\n}\n";
12
+ export declare const FIRE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nout vec4 outColor;\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\n/** Rising turbulence: octaves scrolled upward at different rates. */\nfloat turbulence(vec2 p, float t) {\n float v = valueNoise(p * 3.0 + vec2(0.0, -t * 1.9)) * 0.55;\n if (uNoiseOctaves == 1) return v / 0.55;\n v += valueNoise(p * 6.5 + vec2(t * 0.3, -t * 3.1)) * 0.28;\n if (uNoiseOctaves == 2) return v / 0.83;\n return v + valueNoise(p * 13.0 + vec2(-t * 0.2, -t * 4.6)) * 0.17;\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n vec2 uv = vUv;\n float t = uTime + vSeed * 37.0;\n\n // Taper toward the tip and pinch the sides, so the quad never shows.\n float sides = 1.0 - abs(uv.x - 0.5) * 2.0;\n float taper = sides * (1.0 - uv.y * 0.72);\n if (taper <= 0.0) discard;\n\n // Sway: the column leans and wanders instead of standing to attention.\n float sway = (valueNoise(vec2(t * 0.55, vSeed * 11.0)) - 0.5) * 0.34 * uv.y;\n vec2 p = vec2(uv.x + sway, uv.y);\n\n float n = turbulence(p, t);\n // Flame body: noise must beat a threshold that rises with height, which is\n // what makes the tip break into separate licks.\n float body = n * taper * 1.9 - uv.y * 0.55;\n if (body <= 0.02) discard;\n\n // Colour ramp: white-hot core through yellow and orange to a dark red edge.\n float heat = clamp(body * 1.5, 0.0, 1.0);\n vec3 col = mix(vec3(0.62, 0.06, 0.02), vec3(1.0, 0.45, 0.06), smoothstep(0.0, 0.45, heat));\n col = mix(col, vec3(1.0, 0.85, 0.35), smoothstep(0.45, 0.75, heat));\n col = mix(col, vec3(1.0, 0.98, 0.88), smoothstep(0.78, 1.0, heat));\n\n float alpha = clamp(body * 2.2, 0.0, 1.0) * (1.0 - smoothstep(0.72, 1.0, uv.y));\n // Additive light must lose alpha as it scatters, or mixing its colour alone\n // still makes a distant flame glow through fog and through water.\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, mediumColor(), fog);\n alpha *= 1.0 - fog;\n\n outColor = vec4(col, alpha);\n}\n";
@@ -12,4 +12,4 @@
12
12
  * Lamps widen both of the lobe's widths by the source's angle, as `sphereLobe` widens the standard
13
13
  * lobe, with the energy kept; at strength 0 that is `sphereLobe` exactly.
14
14
  */
15
- export declare const ANISOTROPIC_GLSL = "\n/* The stretch's frame and strength, gathered once with the surface. */\nvec3 aAlong;\nvec3 aAcross;\nfloat aStrength;\n\nvoid anisotropicSurface() {\n vec2 direction = vec2(1.0, 0.0);\n float strength = uModelParams[0].x;\n if (uModelParams[1].w > 0.0) {\n vec2 mapped = mMap.rg * 2.0 - 1.0;\n direction = dot(mapped, mapped) > 1e-8 ? normalize(mapped) : direction;\n strength *= mMap.b;\n }\n vec2 turned = vec2(\n direction.x * uModelParams[0].y - direction.y * uModelParams[0].z,\n direction.x * uModelParams[0].z + direction.y * uModelParams[0].y\n );\n aAlong = normalize(mTangent * turned.x + mBitangent * turned.y);\n aAcross = cross(mNormal, aAlong);\n aStrength = strength;\n}\n\n/** The stretched lobe at a half vector, 1 at the mirror direction: anisotropicLobe.ts. */\nfloat anisotropicLobe(vec3 h, float sourceRadius, float dist) {\n float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);\n float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;\n float grow = sourceRadius / max(2.0 * dist, 1e-3);\n float alongWide = clamp(alongWidth + grow, alongWidth, 1.0);\n float acrossWide = clamp(alpha + grow, alpha, 1.0);\n float energy = (alongWidth / alongWide) * (alpha / acrossWide);\n float x = dot(aAlong, h) / alongWide;\n float y = dot(aAcross, h) / acrossWide;\n float z = dot(mNormal, h);\n float d = x * x + y * y + z * z;\n return energy / max(d * d, 1e-8);\n}\n\nvoid anisotropicLight(vec3 l, float sourceRadius, float dist) {\n float ndl = max(dot(mNormal, l), 0.0);\n vec3 h = normalize(l + mToEye);\n vec3 tint = mix(mSpecColor, vec3(1.0), pow(1.0 - max(dot(mToEye, h), 0.0), 5.0) * mMetal);\n mDiffuse = mAlbedo * ndl * (1.0 - mMetal);\n mSpecular = ndl > 0.0 ? tint * anisotropicLobe(h, sourceRadius, dist) : vec3(0.0);\n}\n\n/* The environment's normal, bent toward the stretch by its strength and the surface's roughness. */\nvec3 anisotropicReflectNormal(vec3 n) {\n vec3 across = cross(aAcross, mToEye);\n vec3 bent = cross(across, aAcross);\n float bend = aStrength * clamp(5.0 * mRoughness, 0.0, 1.0);\n return normalize(mix(n, bent, bend));\n}\n";
15
+ export declare const ANISOTROPIC_GLSL = "\n/* The stretch's frame and strength, gathered once with the surface. */\nvec3 aAlong;\nvec3 aAcross;\nfloat aStrength;\n\nvoid anisotropicSurface() {\n vec2 direction = vec2(1.0, 0.0);\n float strength = uModelParams[0].x;\n if (uModelParams[1].w > 0.0) {\n vec2 mapped = mMap.rg * 2.0 - 1.0;\n direction = dot(mapped, mapped) > 1e-8 ? normalize(mapped) : direction;\n strength *= mMap.b;\n }\n vec2 turned = vec2(\n direction.x * uModelParams[0].y - direction.y * uModelParams[0].z,\n direction.x * uModelParams[0].z + direction.y * uModelParams[0].y\n );\n vec3 along = mTangent * turned.x + mBitangent * turned.y;\n /*\n * A mesh with neither tangents nor texture coordinates has no frame to take a direction from,\n * and normalising the zero it gives was a NaN that drew the whole surface black. Any direction in\n * the surface stands in, the one across the world's up, so the highlight is stretched somewhere.\n */\n if (dot(along, along) < 1e-12) {\n along = cross(abs(mNormal.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0), mNormal);\n }\n aAlong = normalize(along);\n aAcross = cross(mNormal, aAlong);\n aStrength = strength;\n}\n\n/** The stretched lobe at a half vector, 1 at the mirror direction: anisotropicLobe.ts. */\nfloat anisotropicLobe(vec3 h, float sourceRadius, float dist) {\n float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);\n float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;\n float grow = sourceRadius / max(2.0 * dist, 1e-3);\n float alongWide = clamp(alongWidth + grow, alongWidth, 1.0);\n float acrossWide = clamp(alpha + grow, alpha, 1.0);\n float energy = (alongWidth / alongWide) * (alpha / acrossWide);\n float x = dot(aAlong, h) / alongWide;\n float y = dot(aAcross, h) / acrossWide;\n float z = dot(mNormal, h);\n float d = x * x + y * y + z * z;\n return energy / max(d * d, 1e-8);\n}\n\n/**\n * The same stretch as GGX's own BRDF, `\u03C0 \u00B7 D \u00B7 Vis \u00B7 N\u00B7L`, for a surface whose highlight is\n * physical: the distribution normalised, and Smith's height-correlated masking stretched with it\n * (Heitz 2014, as Filament writes it). `anisotropicPhysicalLobe` in anisotropicLobe.ts.\n */\nfloat anisotropicPhysical(vec3 l, vec3 h, float ndl, float sourceRadius, float dist) {\n float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);\n float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;\n float grow = sourceRadius / max(2.0 * dist, 1e-3);\n float along = clamp(alongWidth + grow, alongWidth, 1.0);\n float across = clamp(alpha + grow, alpha, 1.0);\n float x = dot(aAlong, h) / along;\n float y = dot(aAcross, h) / across;\n float z = dot(mNormal, h);\n float d = x * x + y * y + z * z;\n float distribution = 1.0 / max(3.14159265 * along * across * d * d, 1e-8);\n float ndv = max(dot(mNormal, mToEye), 1e-4);\n float viewed = ndl * length(vec3(along * dot(aAlong, mToEye), across * dot(aAcross, mToEye), ndv));\n float lit = ndv * length(vec3(along * dot(aAlong, l), across * dot(aAcross, l), ndl));\n return 3.14159265 * distribution * (0.5 / max(viewed + lit, 1e-5)) * ndl;\n}\n\nvoid anisotropicLight(vec3 l, float sourceRadius, float dist) {\n float ndl = max(dot(mNormal, l), 0.0);\n vec3 h = normalize(l + mToEye);\n vec3 tint = highlightTint(mSpecColor, max(dot(mToEye, h), 0.0), mMetal);\n mDiffuse = mAlbedo * ndl * (1.0 - mMetal);\n float lobe = physicalSpecular()\n ? anisotropicPhysical(l, h, ndl, sourceRadius, dist)\n : anisotropicLobe(h, sourceRadius, dist);\n mSpecular = ndl > 0.0 ? tint * lobe : vec3(0.0);\n}\n\n/* The environment's normal, bent toward the stretch by its strength and the surface's roughness. */\nvec3 anisotropicReflectNormal(vec3 n) {\n vec3 across = cross(aAcross, mToEye);\n vec3 bent = cross(across, aAcross);\n float bend = aStrength * clamp(5.0 * mRoughness, 0.0, 1.0);\n return normalize(mix(n, bent, bend));\n}\n";
@@ -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. */
@@ -7,4 +7,4 @@
7
7
  *
8
8
  * Depth zero and after the models, whose `modelLight` it calls; see `flatFrag`.
9
9
  */
10
- export declare const AREA_LIGHT_GLSL = "\n/**\n * The rectangle's signed form factor at this fragment: its corners **wound so a surface on the\n * emitting side sees them counter-clockwise**, which is what makes it positive there. The natural\n * order \u2014 minus, plus, plus, minus \u2014 winds the other way, and every one-sided light then clamped to\n * zero and the frame was black. The sign is the sidedness: a one-sided rectangle takes the positive\n * part and a two-sided one the magnitude.\n */\nfloat areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);\n}\n\n/**\n * What one rectangle of colour `color` and form factor `form` adds to the lamps' two sums, open\n * and shadowed by `occl` and `glass`.\n *\n * **A model answers a rectangle as it answers a lamp at its centre**, carried at the rectangle's\n * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by the\n * form factor. An approximation, stated: a model's lobe is not integrated over the rectangle as the\n * standard lobe is, so a glossy model under a large softbox shows a lobe from its centre rather than\n * its shape.\n *\n * **The standard lobe is integrated over the rectangle**, rather than sampled at one point on it.\n * What stood here once handed the closest point on the rectangle to `sphereLobe` and scaled it by\n * `form`, the *diffuse* form factor. Measured against a brute-force integral by\n * `scripts/areaSpecular.mjs`, that lost the reflection rather than blurring it: on polished metal\n * facing a softbox it returned 0.000233 where the integral is 0.9207, and head-on on a rough surface\n * it was 3.7 times too bright. Worst 287% either way; this is 40.1% where the light is what the\n * surface reflects and 79.0% for one overhead. `quadCoverage` says what fraction of the specular\n * lobe the rectangle covers and the environment BRDF says how much the surface returns, the split\n * the probe path uses. **`dfg.x + dfg.y` and not `envSpecularEnergy`**, deliberately: the\n * compensation returns the multiply-scattered share, and every direct lobe in this shader is\n * single-scattering, so adding it here returns energy the light never had. Measured at 211%\n * against 40.1%.\n */\nvoid areaLightAdd(\n vec3 centre,\n vec3 right,\n vec3 up,\n vec2 halfSize,\n vec3 color,\n float form,\n float occl,\n vec3 glass,\n vec3 n,\n vec3 albedo,\n float metal,\n vec3 specColor,\n float roughness,\n bool modelled,\n inout vec3 lampOpen,\n inout vec3 lampShadowed\n) {\n if (modelled) {\n vec3 areaL = normalize(centre - vWorldPos);\n modelLight(areaL, 0.0, 0.0);\n vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form) * color;\n lampOpen += areaModel;\n lampShadowed += areaModel * occl * glass;\n return;\n }\n /* Held back with the point lamps', and shadowed by the same term it always was. */\n vec3 areaDiffuse = albedo * color * form * (1.0 - metal);\n lampOpen += areaDiffuse;\n lampShadowed += areaDiffuse * occl * glass;\n\n if (vSpecular > 0.0 || metal > 0.0) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n vec3 toEyeArea = normalize(uCameraPos - vWorldPos);\n float coverage = quadCoverage(n, vWorldPos, toEyeArea, roughness, c0, c1, c2, c3);\n if (coverage > 0.0) {\n vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), roughness);\n vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);\n vec3 areaHighlight = areaSpec * color * coverage;\n lampOpen += areaHighlight;\n lampShadowed += areaHighlight * occl * glass;\n }\n }\n}\n";
10
+ export declare const AREA_LIGHT_GLSL = "\n/**\n * The rectangle's signed form factor at this fragment: its corners **wound so a surface on the\n * emitting side sees them counter-clockwise**, which is what makes it positive there. The natural\n * order \u2014 minus, plus, plus, minus \u2014 winds the other way, and every one-sided light then clamped to\n * zero and the frame was black. The sign is the sidedness: a one-sided rectangle takes the positive\n * part and a two-sided one the magnitude.\n */\nfloat areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);\n}\n\n/**\n * How far along one of a rectangle's axes a fragment still sees it past the barn door on that axis's\n * positive edge: `p` the fragment's place along the axis, `height` its height above the\n * rectangle's plane, `halfSize` the rectangle's half extent, and the door's tip `tipOut` beyond\n * the edge and `tipUp` above the plane. Points of the rectangle past the answer are hidden.\n *\n * Above the tip, the tip's shadow on the plane as seen from the fragment is the limit. Below it, a\n * fragment beside the door and outside its line is hidden from all of it, and one inside the door's\n * reach is hidden from none of it.\n */\nfloat barnDoorLimit(float p, float height, float halfSize, float tipOut, float tipUp) {\n float tipAt = halfSize + tipOut;\n if (height > tipUp) return (height * tipAt - tipUp * p) / (height - tipUp);\n if (p > halfSize && height * tipOut < tipUp * (p - halfSize)) return -1e9;\n return 1e9;\n}\n\n/**\n * The part of a rectangle a fragment sees past its barn doors, written into `centre` and\n * `halfSize` as the smaller rectangle that is left, or false where the doors hide all of it.\n * `doors` is the cosine of their angle from the normal and their length; a length of 0 is no doors.\n * Each axis is clipped by its own two doors, which is what makes this four projections and a min.\n */\nbool areaBarnDoors(inout vec3 centre, vec3 right, vec3 up, inout vec2 halfSize, vec2 doors) {\n if (doors.y <= 0.0) return true;\n vec3 toFragment = vWorldPos - centre;\n float height = dot(toFragment, cross(right, up));\n /* Behind the emitting side: nothing to clip, and the form factor already answers it. */\n if (height <= 0.0) return true;\n float tipOut = doors.y * sqrt(max(1.0 - doors.x * doors.x, 0.0));\n float tipUp = doors.y * doors.x;\n vec2 p = vec2(dot(toFragment, right), dot(toFragment, up));\n vec2 hi = vec2(\n min(halfSize.x, barnDoorLimit(p.x, height, halfSize.x, tipOut, tipUp)),\n min(halfSize.y, barnDoorLimit(p.y, height, halfSize.y, tipOut, tipUp))\n );\n vec2 lo = vec2(\n max(-halfSize.x, -barnDoorLimit(-p.x, height, halfSize.x, tipOut, tipUp)),\n max(-halfSize.y, -barnDoorLimit(-p.y, height, halfSize.y, tipOut, tipUp))\n );\n if (hi.x <= lo.x || hi.y <= lo.y) return false;\n centre += right * (0.5 * (lo.x + hi.x)) + up * (0.5 * (lo.y + hi.y));\n halfSize = 0.5 * (hi - lo);\n return true;\n}\n\n/**\n * What one rectangle of colour `color` and form factor `form` adds to the lamps' two sums, open\n * and shadowed by `occl` and `glass`.\n *\n * **A model answers a rectangle as it answers a lamp at its centre**, carried at the rectangle's\n * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by the\n * form factor. An approximation, stated: a model's lobe is not integrated over the rectangle as the\n * standard lobe is, so a glossy model under a large softbox shows a lobe from its centre rather than\n * its shape.\n *\n * **The standard lobe is integrated over the rectangle**, rather than sampled at one point on it.\n * What stood here once handed the closest point on the rectangle to `sphereLobe` and scaled it by\n * `form`, the *diffuse* form factor. Measured against a brute-force integral by\n * `scripts/areaSpecular.mjs`, that lost the reflection rather than blurring it: on polished metal\n * facing a softbox it returned 0.000233 where the integral is 0.9207, and head-on on a rough surface\n * it was 3.7 times too bright. Worst 287% either way; this is 40.1% where the light is what the\n * surface reflects and 79.0% for one overhead. `quadCoverage` says what fraction of the specular\n * lobe the rectangle covers and the environment BRDF says how much the surface returns, the split\n * the probe path uses. **`dfg.x + dfg.y` and not `envSpecularEnergy`**, deliberately: the\n * compensation returns the multiply-scattered share, and every direct lobe in this shader is\n * single-scattering, so adding it here returns energy the light never had. Measured at 211%\n * against 40.1%.\n */\nvoid areaLightAdd(\n vec3 centre,\n vec3 right,\n vec3 up,\n vec2 halfSize,\n vec3 color,\n float form,\n float occl,\n vec3 glass,\n vec3 n,\n vec3 albedo,\n float metal,\n vec3 specColor,\n float roughness,\n bool modelled,\n inout vec3 lampOpen,\n inout vec3 lampShadowed\n) {\n if (modelled) {\n vec3 areaL = normalize(centre - vWorldPos);\n modelLight(areaL, 0.0, 0.0);\n vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form) * color;\n lampOpen += areaModel;\n lampShadowed += areaModel * occl * glass;\n return;\n }\n /* Held back with the point lamps', and shadowed by the same term it always was. */\n vec3 areaDiffuse = albedo * color * form * (1.0 - metal);\n lampOpen += areaDiffuse;\n lampShadowed += areaDiffuse * occl * glass;\n\n if (vSpecular > 0.0 || metal > 0.0) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n vec3 toEyeArea = normalize(uCameraPos - vWorldPos);\n float coverage = quadCoverage(n, vWorldPos, toEyeArea, roughness, c0, c1, c2, c3);\n if (coverage > 0.0) {\n vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), roughness);\n vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);\n vec3 areaHighlight = areaSpec * color * coverage;\n lampOpen += areaHighlight;\n lampShadowed += areaHighlight * occl * glass;\n }\n }\n}\n";
@@ -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\`.
@@ -8,7 +8,7 @@ export declare function glassTintGlsl(glassShadows: boolean): string;
8
8
  * The lit stage's switches, by the name of the constant each is: the decision both backends make
9
9
  * about which optional code a lit program carries. See `litSwitchesGlsl`.
10
10
  */
11
- export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT';
11
+ export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT' | 'PHYSICAL_SPECULAR' | 'SURFACE_OVERLAY' | 'REFLECTION_MAPS';
12
12
  /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
13
13
  export declare function noLitSwitches(): Record<LitSwitch, boolean>;
14
14
  /** Which of the lit stage's optional features a build reads. See `litSwitchesGlsl`. */
@@ -29,6 +29,7 @@ export interface LitSwitches {
29
29
  * first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
30
30
  * volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
31
31
  * Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
32
- * registers, 4,641 and 96 with all four out.
32
+ * registers, 4,641 and 96 with all four out. `PHYSICAL_SPECULAR` is a sixth, declared with the
33
+ * models so that theirs keep their ids (`modelsGlsl`).
33
34
  */
34
35
  export declare function litSwitchesGlsl(switches: LitSwitches): string;
@@ -31,6 +31,9 @@ export function noLitSwitches() {
31
31
  LIGHT_FIXTURES: false,
32
32
  SURFACE_EFFECTS: false,
33
33
  DRIFT_LIGHT: false,
34
+ PHYSICAL_SPECULAR: false,
35
+ SURFACE_OVERLAY: false,
36
+ REFLECTION_MAPS: false,
34
37
  };
35
38
  }
36
39
  /**
@@ -44,7 +47,8 @@ export function noLitSwitches() {
44
47
  * first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
45
48
  * volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
46
49
  * Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
47
- * registers, 4,641 and 96 with all four out.
50
+ * registers, 4,641 and 96 with all four out. `PHYSICAL_SPECULAR` is a sixth, declared with the
51
+ * models so that theirs keep their ids (`modelsGlsl`).
48
52
  */
49
53
  export function litSwitchesGlsl(switches) {
50
54
  return /* glsl */ `
@@ -104,6 +104,21 @@ export interface FlatShaderOptions {
104
104
  readonly surfaceEffects?: boolean;
105
105
  /** Whether it reads a DriftLight volume. True when absent. See `litSwitchesGlsl`. */
106
106
  readonly driftLight?: boolean;
107
+ /**
108
+ * Whether it carries GGX's own highlight for a material that asks (`physicalSpecular`). True
109
+ * when absent. See `modelsGlsl`.
110
+ */
111
+ readonly physicalSpecular?: boolean;
112
+ /**
113
+ * Whether it carries a draw's surface overlay, `uOverlay` and all (`setSurfaceOverlay`). True
114
+ * when absent. See `overlayGlsl`.
115
+ */
116
+ readonly surfaceOverlay?: boolean;
117
+ /**
118
+ * Whether the frame's own draws write the reflection maps beside their colour: WebGL2's way to
119
+ * the frame's materials' reflections. True when absent. See `reflectionSurfaceGlsl`.
120
+ */
121
+ readonly reflectionMaps?: boolean;
107
122
  /**
108
123
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
109
124
  *
@@ -14,11 +14,13 @@ import { CLOTH_BINDING_GLSL } from '../clothBinding.js';
14
14
  import { SKINNING_GLSL } from '../skinning.js';
15
15
  import { SURFACE_GLSL } from './surface.js';
16
16
  import { MAIN_GLSL } from './main.js';
17
- import { MODELS_GLSL } from './models.js';
17
+ import { modelsGlsl } from './models.js';
18
18
  import { AREA_LIGHT_GLSL } from './areaLight.js';
19
19
  import { NORMAL_MAP_GLSL } from './normalMap.js';
20
20
  import { DRIFT_LIGHT_GLSL } from './driftLight.js';
21
21
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.js';
22
+ import { overlayGlsl } from './overlay.js';
23
+ import { reflectionSurfaceGlsl } from './reflectionSurface.js';
22
24
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.js';
23
25
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.js';
24
26
  /**
@@ -244,6 +246,15 @@ void main() {
244
246
  #endif
245
247
  #if INSTANCED
246
248
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
249
+ /*
250
+ * **The bottom row is the instance's texture cell** (\`MeshInstances.uvRegions\`), which an
251
+ * affine placement leaves free: read, and rebuilt as the \`[0, 0, 0, 1]\` it stands for.
252
+ */
253
+ vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
254
+ model[0].w = 0.0;
255
+ model[1].w = 0.0;
256
+ model[2].w = 0.0;
257
+ model[3].w = 1.0;
247
258
  vec3 tint = aInstanceTint.rgb;
248
259
  if (LIGHTMAP_REGIONS) tint = vec3(1.0);
249
260
  #else
@@ -292,7 +303,12 @@ void main() {
292
303
  vColor = aColor * tint;
293
304
  vEmissive = aEmissive;
294
305
  vSpecular = aSpecular;
306
+ #if INSTANCED
307
+ /* The instance's cell first, then the material's scale and offset. */
308
+ vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
309
+ #else
295
310
  vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
311
+ #endif
296
312
  vEmissiveColor = aEmissiveColor;
297
313
  vRoughness = aRoughness;
298
314
  vGrain = aGrain;
@@ -456,7 +472,11 @@ export function flatFrag(options) {
456
472
  DRIFT_LIGHT_GLSL,
457
473
  SURFACE_EFFECTS_GLSL,
458
474
  /* Depth zero and after the tangent frame and the lobes it calls; see models.ts. */
459
- MODELS_GLSL,
475
+ modelsGlsl(options.physicalSpecular ?? true),
476
+ /* After the models, so its switch takes the next id; depth zero, as main calls it. */
477
+ overlayGlsl(options.surfaceOverlay ?? true),
478
+ /* After the overlay, so its switch takes the next id. See reflectionSurface.ts. */
479
+ reflectionSurfaceGlsl(options.reflectionMaps ?? true),
460
480
  /* After the models, whose answer to a rectangle it asks for. See areaLight.ts. */
461
481
  AREA_LIGHT_GLSL,
462
482
  MAIN_GLSL,
@@ -25,4 +25,10 @@ export declare const EMISSIVE_SHADOW_SHARE = 0.55;
25
25
  * deliberately not this.
26
26
  */
27
27
  export declare const MIN_LOBE_ALPHA = 0.01;
28
+ /**
29
+ * Karis's fit to the split-sum environment BRDF, as GLSL: the lit stage's, and the screen-space
30
+ * reflection's when a surface asks for its Fresnel (`ReflectiveSurface.fresnel`). One text, so the
31
+ * two cannot drift.
32
+ */
33
+ export declare const ENV_BRDF_GLSL = "/**\n * The other half of the split-sum approximation: what fraction of an environment a surface returns.\n *\n * The convolution decides *what* a surface reflects at a given roughness; this decides *how much*.\n * Before it the amount was fresnel times one-minus-roughness, which is a curve somebody chose\n * standing in for an integral.\n *\n * Karis's analytic fit to the environment BRDF, so there is **no lookup texture and no texture\n * unit**: a few multiply-adds against roughness and the view angle. x scales the surface's own\n * reflectance at normal incidence and y is the additive grazing term, so a smooth dielectric at\n * f0 0.04 returns about 0.046 head-on and approaches 1 at the edge, which is what a dielectric\n * does. At roughness 1 it returns about 0.016 rather than the zero one-minus-roughness gave, which\n * is the substantive difference: a fully rough dielectric does reflect its surroundings.\n *\n * **It replaces the Fresnel factor as well as the thinning**, because the fit already contains the\n * angular dependence; multiplying by a separate Fresnel term would apply it twice.\n *\n * **How much this is worth was measured, and it is worth less than it looks on a dark room.**\n * On demo/dev/ibl.html?ladder=1 the whole ladder moved by at most 0.2 of 255 when the old term was\n * replaced. That is not the term failing: at high roughness the surface reads the coarsest levels\n * of the chain, and in a room of five dark walls and one bright one those levels are dark, so a\n * weight three times larger against a near-zero radiance is still near zero. It is worth much more\n * against a bright environment, which is what a loaded sky is and what no captured room here is.\n *\n * **A warning about reading that page, paid for once.** The ladder falls about five-fold from\n * mirror to rough, and that reads as energy being lost. It is not: a mirror concentrates the one\n * bright wall into a sharp image and a rough surface averages it against five dark ones, so the\n * falloff is the room's own content. The normalised falloff also happens to track one-minus-\n * roughness closely, which made the old term look like the cause \u2014 and removing that term entirely\n * changed nothing, which is what settled it. Perturb before believing a match.\n *\n * **What it costs** is a fit rather than the integral: the published error is around one per cent\n * of the tabulated term, far below what an eight-bit frame can show. **What would make it wrong**\n * is a material model this shader does not have \u2014 a clearcoat or a sheen lobe carries its own BRDF\n * and its own integral, and this fit is Trowbridge-Reitz with Smith masking and nothing else.\n */\nvec2 envBrdfApprox(float ndv, float roughness) {\n const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);\n const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);\n vec4 r = roughness * c0 + c1;\n float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;\n return vec2(-1.04, 1.04) * a004 + r.zw;\n}";
28
34
  export declare const LOBES_GLSL: string;
@@ -27,6 +27,54 @@ export const EMISSIVE_SHADOW_SHARE = 0.55;
27
27
  * deliberately not this.
28
28
  */
29
29
  export const MIN_LOBE_ALPHA = 0.01;
30
+ /**
31
+ * Karis's fit to the split-sum environment BRDF, as GLSL: the lit stage's, and the screen-space
32
+ * reflection's when a surface asks for its Fresnel (`ReflectiveSurface.fresnel`). One text, so the
33
+ * two cannot drift.
34
+ */
35
+ export const ENV_BRDF_GLSL = `/**
36
+ * The other half of the split-sum approximation: what fraction of an environment a surface returns.
37
+ *
38
+ * The convolution decides *what* a surface reflects at a given roughness; this decides *how much*.
39
+ * Before it the amount was fresnel times one-minus-roughness, which is a curve somebody chose
40
+ * standing in for an integral.
41
+ *
42
+ * Karis's analytic fit to the environment BRDF, so there is **no lookup texture and no texture
43
+ * unit**: a few multiply-adds against roughness and the view angle. x scales the surface's own
44
+ * reflectance at normal incidence and y is the additive grazing term, so a smooth dielectric at
45
+ * f0 0.04 returns about 0.046 head-on and approaches 1 at the edge, which is what a dielectric
46
+ * does. At roughness 1 it returns about 0.016 rather than the zero one-minus-roughness gave, which
47
+ * is the substantive difference: a fully rough dielectric does reflect its surroundings.
48
+ *
49
+ * **It replaces the Fresnel factor as well as the thinning**, because the fit already contains the
50
+ * angular dependence; multiplying by a separate Fresnel term would apply it twice.
51
+ *
52
+ * **How much this is worth was measured, and it is worth less than it looks on a dark room.**
53
+ * On demo/dev/ibl.html?ladder=1 the whole ladder moved by at most 0.2 of 255 when the old term was
54
+ * replaced. That is not the term failing: at high roughness the surface reads the coarsest levels
55
+ * of the chain, and in a room of five dark walls and one bright one those levels are dark, so a
56
+ * weight three times larger against a near-zero radiance is still near zero. It is worth much more
57
+ * against a bright environment, which is what a loaded sky is and what no captured room here is.
58
+ *
59
+ * **A warning about reading that page, paid for once.** The ladder falls about five-fold from
60
+ * mirror to rough, and that reads as energy being lost. It is not: a mirror concentrates the one
61
+ * bright wall into a sharp image and a rough surface averages it against five dark ones, so the
62
+ * falloff is the room's own content. The normalised falloff also happens to track one-minus-
63
+ * roughness closely, which made the old term look like the cause — and removing that term entirely
64
+ * changed nothing, which is what settled it. Perturb before believing a match.
65
+ *
66
+ * **What it costs** is a fit rather than the integral: the published error is around one per cent
67
+ * of the tabulated term, far below what an eight-bit frame can show. **What would make it wrong**
68
+ * is a material model this shader does not have — a clearcoat or a sheen lobe carries its own BRDF
69
+ * and its own integral, and this fit is Trowbridge-Reitz with Smith masking and nothing else.
70
+ */
71
+ vec2 envBrdfApprox(float ndv, float roughness) {
72
+ const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
73
+ const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
74
+ vec4 r = roughness * c0 + c1;
75
+ float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;
76
+ return vec2(-1.04, 1.04) * a004 + r.zw;
77
+ }`;
30
78
  export const LOBES_GLSL = `float shadowReach(float distance, float maxDistance) {
31
79
  return 1.0 - smoothstep(
32
80
  maxDistance * ${POINT_SHADOW_FADE_START.toFixed(2)},
@@ -153,49 +201,7 @@ float sphereLobe(float ndh, float roughness, float sourceRadius, float dist) {
153
201
  return energy * (w2 * w2) / max(d * d, 1e-8);
154
202
  }
155
203
 
156
- /**
157
- * The other half of the split-sum approximation: what fraction of an environment a surface returns.
158
- *
159
- * The convolution decides *what* a surface reflects at a given roughness; this decides *how much*.
160
- * Before it the amount was fresnel times one-minus-roughness, which is a curve somebody chose
161
- * standing in for an integral.
162
- *
163
- * Karis's analytic fit to the environment BRDF, so there is **no lookup texture and no texture
164
- * unit**: a few multiply-adds against roughness and the view angle. x scales the surface's own
165
- * reflectance at normal incidence and y is the additive grazing term, so a smooth dielectric at
166
- * f0 0.04 returns about 0.046 head-on and approaches 1 at the edge, which is what a dielectric
167
- * does. At roughness 1 it returns about 0.016 rather than the zero one-minus-roughness gave, which
168
- * is the substantive difference: a fully rough dielectric does reflect its surroundings.
169
- *
170
- * **It replaces the Fresnel factor as well as the thinning**, because the fit already contains the
171
- * angular dependence; multiplying by a separate Fresnel term would apply it twice.
172
- *
173
- * **How much this is worth was measured, and it is worth less than it looks on a dark room.**
174
- * On demo/dev/ibl.html?ladder=1 the whole ladder moved by at most 0.2 of 255 when the old term was
175
- * replaced. That is not the term failing: at high roughness the surface reads the coarsest levels
176
- * of the chain, and in a room of five dark walls and one bright one those levels are dark, so a
177
- * weight three times larger against a near-zero radiance is still near zero. It is worth much more
178
- * against a bright environment, which is what a loaded sky is and what no captured room here is.
179
- *
180
- * **A warning about reading that page, paid for once.** The ladder falls about five-fold from
181
- * mirror to rough, and that reads as energy being lost. It is not: a mirror concentrates the one
182
- * bright wall into a sharp image and a rough surface averages it against five dark ones, so the
183
- * falloff is the room's own content. The normalised falloff also happens to track one-minus-
184
- * roughness closely, which made the old term look like the cause — and removing that term entirely
185
- * changed nothing, which is what settled it. Perturb before believing a match.
186
- *
187
- * **What it costs** is a fit rather than the integral: the published error is around one per cent
188
- * of the tabulated term, far below what an eight-bit frame can show. **What would make it wrong**
189
- * is a material model this shader does not have — a clearcoat or a sheen lobe carries its own BRDF
190
- * and its own integral, and this fit is Trowbridge-Reitz with Smith masking and nothing else.
191
- */
192
- vec2 envBrdfApprox(float ndv, float roughness) {
193
- const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
194
- const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
195
- vec4 r = roughness * c0 + c1;
196
- float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;
197
- return vec2(-1.04, 1.04) * a004 + r.zw;
198
- }
204
+ ${ENV_BRDF_GLSL}
199
205
 
200
206
  /**
201
207
  * How much of a prefiltered environment a surface returns, with the light that bounces more than