@driftengine/core 4.4.2 → 4.6.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 (339) hide show
  1. package/CHANGELOG.json +97 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +28 -3
  23. package/dist/index.js +20 -1
  24. package/dist/render/backend/api.d.ts +34 -14
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
  30. package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
  31. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  32. package/dist/render/backend/webgl2/instanced.js +72 -15
  33. package/dist/render/backend/webgl2/renderer.d.ts +201 -16
  34. package/dist/render/backend/webgl2/renderer.js +596 -224
  35. package/dist/render/backend/webgpu/buffers.d.ts +29 -1
  36. package/dist/render/backend/webgpu/buffers.js +61 -4
  37. package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
  38. package/dist/render/backend/webgpu/depthPass.js +7 -2
  39. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  40. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  41. package/dist/render/backend/webgpu/flatPass.d.ts +6 -1
  42. package/dist/render/backend/webgpu/flatPass.js +57 -9
  43. package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
  44. package/dist/render/backend/webgpu/glassTintPass.js +206 -0
  45. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +60 -1
  46. package/dist/render/backend/webgpu/gpuDrivenPass.js +305 -5
  47. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  48. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  49. package/dist/render/backend/webgpu/instanced.d.ts +23 -2
  50. package/dist/render/backend/webgpu/instanced.js +41 -4
  51. package/dist/render/backend/webgpu/latePass.d.ts +30 -0
  52. package/dist/render/backend/webgpu/latePass.js +140 -0
  53. package/dist/render/backend/webgpu/lightVolumePass.js +5 -1
  54. package/dist/render/backend/webgpu/motionPass.d.ts +95 -0
  55. package/dist/render/backend/webgpu/motionPass.js +491 -0
  56. package/dist/render/backend/webgpu/pipelineCache.d.ts +10 -1
  57. package/dist/render/backend/webgpu/pipelineCache.js +11 -1
  58. package/dist/render/backend/webgpu/pointGlassTintPass.d.ts +32 -0
  59. package/dist/render/backend/webgpu/pointGlassTintPass.js +193 -0
  60. package/dist/render/backend/webgpu/pointShadowPass.d.ts +35 -6
  61. package/dist/render/backend/webgpu/pointShadowPass.js +105 -20
  62. package/dist/render/backend/webgpu/postPass.d.ts +3 -13
  63. package/dist/render/backend/webgpu/renderer.d.ts +271 -64
  64. package/dist/render/backend/webgpu/renderer.js +1395 -580
  65. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  66. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  67. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  68. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  69. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  70. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  71. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  72. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  73. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +22 -3
  74. package/dist/render/backend/webgpu/surfaceTexturePass.js +78 -39
  75. package/dist/render/driftLight/bake.d.ts +7 -0
  76. package/dist/render/driftLight/bake.js +61 -56
  77. package/dist/render/driftLight/denseField.d.ts +46 -0
  78. package/dist/render/driftLight/denseField.js +109 -0
  79. package/dist/render/driftLight/lightField.d.ts +10 -5
  80. package/dist/render/driftLight/lightField.js +25 -19
  81. package/dist/render/driftLight/presence.d.ts +50 -0
  82. package/dist/render/driftLight/presence.js +43 -0
  83. package/dist/render/driftLight/uniforms.d.ts +3 -3
  84. package/dist/render/driftLight/uniforms.js +5 -6
  85. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  86. package/dist/render/driftLight/worldLightField.js +66 -0
  87. package/dist/render/fixtureAtlas.d.ts +20 -0
  88. package/dist/render/fixtureAtlas.js +49 -0
  89. package/dist/render/glass.d.ts +45 -0
  90. package/dist/render/glass.js +39 -0
  91. package/dist/render/glassCasters.d.ts +40 -0
  92. package/dist/render/glassCasters.js +84 -0
  93. package/dist/render/glassShadow.d.ts +78 -0
  94. package/dist/render/glassShadow.js +125 -0
  95. package/dist/render/globalMediumPass.d.ts +2 -3
  96. package/dist/render/globalMediumPass.js +2 -8
  97. package/dist/render/gpudriven/lit.d.ts +12 -0
  98. package/dist/render/gpudriven/lit.js +23 -0
  99. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  100. package/dist/render/gpudriven/materialTable.js +26 -6
  101. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  102. package/dist/render/gpudriven/pipeline.js +4 -3
  103. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  104. package/dist/render/gpudriven/sunGlass.js +89 -0
  105. package/dist/render/instanceCull.d.ts +63 -0
  106. package/dist/render/instanceCull.js +160 -0
  107. package/dist/render/instances.d.ts +15 -0
  108. package/dist/render/lightBudget.d.ts +36 -35
  109. package/dist/render/lightBudget.js +38 -37
  110. package/dist/render/lightGrid.d.ts +42 -0
  111. package/dist/render/lightGrid.js +87 -0
  112. package/dist/render/mesh.d.ts +18 -24
  113. package/dist/render/mesh.js +51 -68
  114. package/dist/render/occlusion.d.ts +10 -0
  115. package/dist/render/occlusion.js +36 -7
  116. package/dist/render/pointGlassTint.d.ts +59 -0
  117. package/dist/render/pointGlassTint.js +198 -0
  118. package/dist/render/pointLightSelection.d.ts +7 -0
  119. package/dist/render/pointLightSelection.js +29 -4
  120. package/dist/render/pointShadowArray.d.ts +56 -3
  121. package/dist/render/pointShadowArray.js +127 -16
  122. package/dist/render/pointShadowMap.d.ts +23 -1
  123. package/dist/render/pointShadowMap.js +27 -1
  124. package/dist/render/rainField.d.ts +13 -0
  125. package/dist/render/rainField.js +27 -5
  126. package/dist/render/recon/changeFrames.d.ts +15 -0
  127. package/dist/render/recon/changeFrames.js +21 -0
  128. package/dist/render/recon/lateDepth.d.ts +11 -0
  129. package/dist/render/recon/lateDepth.js +70 -0
  130. package/dist/render/recon/lateRouting.d.ts +14 -0
  131. package/dist/render/recon/lateRouting.js +18 -0
  132. package/dist/render/recon/mover.d.ts +57 -0
  133. package/dist/render/recon/mover.js +86 -0
  134. package/dist/render/recon/resolve.d.ts +34 -4
  135. package/dist/render/recon/resolve.js +161 -20
  136. package/dist/render/renderQuality.d.ts +35 -0
  137. package/dist/render/renderQuality.js +7 -0
  138. package/dist/render/rendererHarness.js +16 -0
  139. package/dist/render/sceneTarget.d.ts +9 -1
  140. package/dist/render/sceneTarget.js +60 -12
  141. package/dist/render/shaders/depth.d.ts +16 -1
  142. package/dist/render/shaders/depth.js +94 -15
  143. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  144. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  145. package/dist/render/shaders/flat/driftLight.js +32 -7
  146. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  147. package/dist/render/shaders/flat/glassTint.js +25 -0
  148. package/dist/render/shaders/flat/index.d.ts +13 -0
  149. package/dist/render/shaders/flat/index.js +9 -3
  150. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  151. package/dist/render/shaders/flat/lampGlass.js +183 -0
  152. package/dist/render/shaders/flat/main.d.ts +1 -1
  153. package/dist/render/shaders/flat/main.js +190 -38
  154. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  155. package/dist/render/shaders/flat/pointShadow.js +2 -0
  156. package/dist/render/shaders/flat/preamble.js +96 -26
  157. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  158. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  159. package/dist/render/shaders/generated/depth.wgsl.d.ts +230 -7
  160. package/dist/render/shaders/generated/depth.wgsl.js +230 -7
  161. package/dist/render/shaders/generated/flat.wgsl.d.ts +1644 -1596
  162. package/dist/render/shaders/generated/flat.wgsl.js +1683 -1621
  163. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  164. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  165. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  166. package/dist/render/shaders/generated/lightVolume.wgsl.js +21 -17
  167. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  168. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  169. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  170. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  171. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  172. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  173. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  174. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  175. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  176. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  177. package/dist/render/shaders/globalMedium.js +6 -6
  178. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  179. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  180. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  181. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  182. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  183. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  184. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  185. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  186. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  187. package/dist/render/shaders/lightVolume.js +6 -6
  188. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  189. package/dist/render/shaders/octahedralResolve.js +48 -17
  190. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  191. package/dist/render/shaders/recon/lateDepth.wgsl.js +69 -0
  192. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  193. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  194. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  195. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  196. package/dist/render/shaders/rush.js +11 -1
  197. package/dist/render/shaders/sdfText.d.ts +1 -1
  198. package/dist/render/shaders/sdfText.js +3 -2
  199. package/dist/render/shaders/text.d.ts +1 -1
  200. package/dist/render/shaders/text.js +18 -9
  201. package/dist/render/shadowCasters.d.ts +20 -1
  202. package/dist/render/shadowCasters.js +15 -1
  203. package/dist/render/shadowMap.d.ts +54 -5
  204. package/dist/render/shadowMap.js +190 -36
  205. package/dist/render/sunGlassTint.d.ts +25 -0
  206. package/dist/render/sunGlassTint.js +79 -0
  207. package/dist/render/surfaceEffects.d.ts +83 -0
  208. package/dist/render/surfaceEffects.js +75 -0
  209. package/dist/render/surfaceScene.d.ts +8 -0
  210. package/dist/render/surfaceScene.js +8 -0
  211. package/dist/render/surfaceTexture.d.ts +18 -2
  212. package/dist/render/surfaceTexture.js +68 -15
  213. package/dist/render/temporalAa.d.ts +25 -6
  214. package/dist/render/temporalAa.js +45 -15
  215. package/dist/render/textureSource.d.ts +30 -0
  216. package/dist/render/textureSource.js +53 -0
  217. package/dist/render/translucentQueue.d.ts +6 -0
  218. package/dist/render/translucentQueue.js +15 -1
  219. package/dist/render/vertexDefaults.js +2 -0
  220. package/dist/world/hlod.d.ts +104 -0
  221. package/dist/world/hlod.js +214 -0
  222. package/package.json +6 -6
  223. package/src/geometry/boxCsg.ts +186 -0
  224. package/src/geometry/csg.ts +384 -0
  225. package/src/geometry/meshBuilder.ts +15 -0
  226. package/src/geometry/meshConcat.ts +1 -0
  227. package/src/geometry/pixelFont.ts +2 -0
  228. package/src/geometry/solid.ts +189 -0
  229. package/src/geometry/solidFlat.ts +522 -0
  230. package/src/geometry/solidHarness.ts +83 -0
  231. package/src/geometry/solidNormals.ts +103 -0
  232. package/src/geometry/solidRound.ts +443 -0
  233. package/src/geometry/solidWriter.ts +58 -0
  234. package/src/index.ts +61 -1
  235. package/src/render/backend/api.ts +36 -11
  236. package/src/render/backend/drawFog.ts +31 -0
  237. package/src/render/backend/materialChanges.ts +9 -2
  238. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  239. package/src/render/backend/webgl2/instanced.ts +80 -14
  240. package/src/render/backend/webgl2/renderer.ts +700 -250
  241. package/src/render/backend/webgpu/buffers.ts +92 -5
  242. package/src/render/backend/webgpu/depthPass.ts +7 -2
  243. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  244. package/src/render/backend/webgpu/flatPass.ts +62 -11
  245. package/src/render/backend/webgpu/glassTintPass.ts +245 -0
  246. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  247. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  248. package/src/render/backend/webgpu/instanced.ts +42 -4
  249. package/src/render/backend/webgpu/latePass.ts +161 -0
  250. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  251. package/src/render/backend/webgpu/motionPass.ts +535 -0
  252. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  253. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  254. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  255. package/src/render/backend/webgpu/renderer.ts +1671 -670
  256. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  257. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  258. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  259. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  260. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  261. package/src/render/backend/webgpu/surfaceTexturePass.ts +91 -51
  262. package/src/render/driftLight/bake.ts +85 -62
  263. package/src/render/driftLight/denseField.ts +149 -0
  264. package/src/render/driftLight/lightField.ts +33 -18
  265. package/src/render/driftLight/presence.ts +69 -0
  266. package/src/render/driftLight/uniforms.ts +8 -9
  267. package/src/render/driftLight/worldLightField.ts +83 -0
  268. package/src/render/fixtureAtlas.ts +63 -0
  269. package/src/render/glass.ts +74 -0
  270. package/src/render/glassCasters.ts +164 -0
  271. package/src/render/glassShadow.ts +153 -0
  272. package/src/render/globalMediumPass.ts +4 -11
  273. package/src/render/gpudriven/lit.ts +31 -0
  274. package/src/render/gpudriven/materialTable.ts +28 -6
  275. package/src/render/gpudriven/pipeline.ts +4 -3
  276. package/src/render/gpudriven/sunGlass.ts +117 -0
  277. package/src/render/instanceCull.ts +205 -0
  278. package/src/render/instances.ts +16 -0
  279. package/src/render/lightBudget.ts +41 -38
  280. package/src/render/lightGrid.ts +130 -0
  281. package/src/render/mesh.ts +59 -72
  282. package/src/render/occlusion.ts +51 -7
  283. package/src/render/pointGlassTint.ts +250 -0
  284. package/src/render/pointLightSelection.ts +72 -4
  285. package/src/render/pointShadowArray.ts +188 -22
  286. package/src/render/pointShadowMap.ts +41 -0
  287. package/src/render/rainField.ts +39 -5
  288. package/src/render/recon/changeFrames.ts +24 -0
  289. package/src/render/recon/lateDepth.ts +83 -0
  290. package/src/render/recon/lateRouting.ts +29 -0
  291. package/src/render/recon/mover.ts +118 -0
  292. package/src/render/recon/resolve.ts +206 -20
  293. package/src/render/renderQuality.ts +47 -0
  294. package/src/render/rendererHarness.ts +16 -0
  295. package/src/render/sceneTarget.ts +62 -11
  296. package/src/render/shaders/depth.ts +118 -15
  297. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  298. package/src/render/shaders/flat/driftLight.ts +32 -7
  299. package/src/render/shaders/flat/glassTint.ts +25 -0
  300. package/src/render/shaders/flat/index.ts +22 -3
  301. package/src/render/shaders/flat/lampGlass.ts +185 -0
  302. package/src/render/shaders/flat/main.ts +190 -38
  303. package/src/render/shaders/flat/pointShadow.ts +2 -0
  304. package/src/render/shaders/flat/preamble.ts +96 -26
  305. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  306. package/src/render/shaders/generated/depth.wgsl.ts +237 -7
  307. package/src/render/shaders/generated/flat.wgsl.ts +1685 -1621
  308. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  309. package/src/render/shaders/generated/lightVolume.wgsl.ts +23 -17
  310. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  311. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  312. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  313. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  314. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  315. package/src/render/shaders/globalMedium.ts +6 -6
  316. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  317. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  318. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  319. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  320. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  321. package/src/render/shaders/lightVolume.ts +6 -6
  322. package/src/render/shaders/octahedralResolve.ts +50 -17
  323. package/src/render/shaders/recon/lateDepth.wgsl.ts +72 -0
  324. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  325. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  326. package/src/render/shaders/rush.ts +11 -1
  327. package/src/render/shaders/sdfText.ts +3 -2
  328. package/src/render/shaders/text.ts +18 -9
  329. package/src/render/shadowCasters.ts +28 -1
  330. package/src/render/shadowMap.ts +231 -37
  331. package/src/render/sunGlassTint.ts +86 -0
  332. package/src/render/surfaceEffects.ts +132 -0
  333. package/src/render/surfaceScene.ts +16 -0
  334. package/src/render/surfaceTexture.ts +110 -19
  335. package/src/render/temporalAa.ts +78 -14
  336. package/src/render/textureSource.ts +72 -0
  337. package/src/render/translucentQueue.ts +20 -1
  338. package/src/render/vertexDefaults.ts +2 -0
  339. package/src/world/hlod.ts +290 -0
@@ -1,6 +1,23 @@
1
1
  /** The fragment entry point, which assembles everything above into a colour. */
2
2
  export const MAIN_GLSL = `void main() {
3
- if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;
3
+ /*
4
+ * Where every material map is read: the texture coordinate, and the layer of the array the face
5
+ * wears, rounded here so an interpolated 2.9999 lands on layer 3 however the generated WGSL
6
+ * turns a float into an array index. A plain texture is an array of one, so layer 0.
7
+ */
8
+ vec3 surfaceAt = vec3(vUv.xy, floor(vUv.z + 0.5));
9
+ if (dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;
10
+ /*
11
+ * The layer's surface effects, zeros where the albedo array carries no table — see
12
+ * surfaceEffects.ts. Scroll and flipbook move surfaceAt before any map reads it, so every map of
13
+ * the material moves together. The gradients are taken here, outside every branch, for the room
14
+ * a window samples through textureGrad further down.
15
+ */
16
+ fxLoad(int(surfaceAt.z));
17
+ surfaceAt = fxAnimate(surfaceAt);
18
+ vec2 fxDx = dFdx(surfaceAt.xy);
19
+ vec2 fxDy = dFdy(surfaceAt.xy);
20
+ if (uWriteMode.y != 0.0 && !ditherKeeps(uWriteMode.y)) discard;
4
21
 
5
22
  /*
6
23
  * Surface colour and its coverage, resolved before anything else — including before it is
@@ -34,13 +51,13 @@ export const MAIN_GLSL = `void main() {
34
51
  */
35
52
  float coverage = 1.0;
36
53
  if (uAlbedoEnabled != 0) {
37
- vec4 texel = texture(uAlbedo, vUv);
54
+ vec4 texel = texture(uAlbedo, surfaceAt);
38
55
  // Discarded before anything else is computed: a thrown-away fragment should not pay
39
56
  // for the lighting it will never contribute to. The test credits alpha for the mip level
40
57
  // it samples, so a needle averaged thin down the chain is not lost; see cutoutCoverage.ts.
41
58
  // Behind a branch on a uniform, so a material with no cutoff pays nothing for it.
42
59
  if (uAlbedoCutout > 0.0) {
43
- float tested = cutoutAlpha(texel.a, vUv * vec2(textureSize(uAlbedo, 0)));
60
+ float tested = cutoutAlpha(texel.a, vUv.xy * vec2(textureSize(uAlbedo, 0).xy));
44
61
  if (tested < uAlbedoCutout) discard;
45
62
  }
46
63
  albedo *= texel.rgb;
@@ -69,7 +86,7 @@ export const MAIN_GLSL = `void main() {
69
86
  float ormRoughness = vRoughness;
70
87
  float metal = 0.0;
71
88
  if (uOrmEnabled != 0) {
72
- vec3 t = texture(uOrmMap, vUv).rgb;
89
+ vec3 t = texture(uOrmMap, surfaceAt).rgb;
73
90
  ormOcclusion = mix(1.0, t.r, uOrmScale.r);
74
91
  ormRoughness = clamp(t.g * uOrmScale.g, 0.0, 1.0);
75
92
  metal = clamp(t.b * uOrmScale.b, 0.0, 1.0);
@@ -81,7 +98,28 @@ export const MAIN_GLSL = `void main() {
81
98
  * \`uLightingEnabled\` at 0 keeps to the end, and the value every lit draw starts from before
82
99
  * the branch below builds a shaded result on top of it.
83
100
  */
101
+ /*
102
+ * Wear, rain and windows, on the colour and roughness everything below shades. Each is exactly
103
+ * nothing where its numbers are zero, which is every surface of a scene that names no effect.
104
+ */
105
+ fxNormal = normalize(vNormal);
106
+ fxDistance = distance(vWorldPos, uCameraPos);
107
+ vec4 weathered = fxWeather(vWorldPos, albedo, ormRoughness);
108
+ albedo = weathered.rgb;
109
+ ormRoughness = weathered.w;
110
+ vec3 windowGlow = vec3(0.0);
111
+ if (fx0.x > 0.0) {
112
+ vec4 window = fxWindows(surfaceAt, fxDx, fxDy, coverage);
113
+ albedo = mix(albedo, fxRoom * 0.35, window.w);
114
+ windowGlow = window.rgb;
115
+ }
116
+
84
117
  vec3 lit = albedo;
118
+ /* The light arriving at a glass pane from behind it, gathered in each light loop below and added
119
+ over the see-through at the end. Stays zero for anything that is not glass. See glass.ts. */
120
+ vec3 glassGlow = vec3(0.0);
121
+ float glassTransmission = uSeeThrough.z;
122
+ float glassFrost = uSeeThrough.w;
85
123
 
86
124
  if (uLightingEnabled != 0) {
87
125
  vec3 n = normalize(vNormal);
@@ -93,7 +131,13 @@ export const MAIN_GLSL = `void main() {
93
131
  * back of the sheet; what that gives up is a sliver at a curved silhouette, where a front face's
94
132
  * interpolated normal already leans away and is lit as the back.
95
133
  */
96
- bool backFace = uDoubleSided != 0 && dot(n, uCameraPos - vWorldPos) < 0.0;
134
+ /*
135
+ * **Glass is two-sided whatever its material says**: a pane is symmetric, so from behind the
136
+ * side the eye sees is the one lit, and the light on its far side is what glows through it.
137
+ * Taken from the stored normal alone, a pane seen from behind glowed with the light on the
138
+ * eye's side and not with the sun shining through it.
139
+ */
140
+ bool backFace = (uDoubleSided != 0 || glassTransmission > 0.0) && dot(n, uCameraPos - vWorldPos) < 0.0;
97
141
  /*
98
142
  * The authored normal, before either relief perturbs it.
99
143
  *
@@ -111,8 +155,8 @@ export const MAIN_GLSL = `void main() {
111
155
  * below. The rule is about non-uniform branches, and this branch is on a uniform.
112
156
  */
113
157
  if (uNormalStrength > 0.0) {
114
- vec3 mapped = texture(uNormalMap, vUv).xyz * 2.0 - 1.0;
115
- mat3 tbn = tangentFrame(n, vWorldPos, vUv, vTangent, vHasTangents);
158
+ vec3 mapped = texture(uNormalMap, surfaceAt).xyz * 2.0 - 1.0;
159
+ mat3 tbn = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
116
160
  n = normalize(mix(n, normalize(tbn * mapped), uNormalStrength));
117
161
  }
118
162
 
@@ -232,14 +276,14 @@ export const MAIN_GLSL = `void main() {
232
276
  * antisymmetric, the cancellation holds, and it is the more accurate estimate besides: a
233
277
  * forward difference reports the slope half a step away from the point it is used at.
234
278
  */
235
- vec2 duvdx = dFdx(vUv);
236
- vec2 duvdy = dFdy(vUv);
279
+ vec2 duvdx = dFdx(vUv.xy);
280
+ vec2 duvdy = dFdy(vUv.xy);
237
281
  /* Half, because a centred difference spans two steps and three.js's number means one. */
238
282
  float reliefPerStep = uTextureRelief * 0.5;
239
- float dHdx = (surfaceHeight(texture(uAlbedo, vUv + duvdx).rgb)
240
- - surfaceHeight(texture(uAlbedo, vUv - duvdx).rgb)) * reliefPerStep;
241
- float dHdy = (surfaceHeight(texture(uAlbedo, vUv + duvdy).rgb)
242
- - surfaceHeight(texture(uAlbedo, vUv - duvdy).rgb)) * reliefPerStep;
283
+ float dHdx = (surfaceHeight(texture(uAlbedo, vec3(vUv.xy + duvdx, surfaceAt.z)).rgb)
284
+ - surfaceHeight(texture(uAlbedo, vec3(vUv.xy - duvdx, surfaceAt.z)).rgb)) * reliefPerStep;
285
+ float dHdy = (surfaceHeight(texture(uAlbedo, vec3(vUv.xy + duvdy, surfaceAt.z)).rgb)
286
+ - surfaceHeight(texture(uAlbedo, vec3(vUv.xy - duvdy, surfaceAt.z)).rgb)) * reliefPerStep;
243
287
 
244
288
  vec3 dpdx = dFdx(vWorldPos);
245
289
  vec3 dpdy = dFdy(vWorldPos);
@@ -312,9 +356,13 @@ export const MAIN_GLSL = `void main() {
312
356
  float surfaceNdl = dot(surfaceN, uDirectionalDir);
313
357
  #if DIRECTIONAL_SHADOWS
314
358
  float sunShade = shadowFactor(max(surfaceNdl, 0.0));
359
+ /* What the sun keeps through any glass on its way here: see sunGlassLookup. */
360
+ vec3 sunGlass = sunGlassTint(max(surfaceNdl, 0.0));
315
361
  #else
316
362
  float sunShade = 1.0;
363
+ vec3 sunGlass = vec3(1.0);
317
364
  #endif
365
+ vec3 sunColor = uDirectionalColor * sunGlass;
318
366
  /*
319
367
  * **How much sky this vertex can see, applied to the sun and to nothing else.**
320
368
  *
@@ -342,6 +390,8 @@ export const MAIN_GLSL = `void main() {
342
390
  */
343
391
  float sunFacing = smoothstep(0.0, 0.02, surfaceNdl);
344
392
  float direct = ndl * sunShade * sunFacing;
393
+ /* Glass: the sun on the far side of a pane lights it through the glass. See glassGlow. */
394
+ if (glassTransmission > 0.0) glassGlow += sunColor * max(-surfaceNdl, 0.0) * sunShade;
345
395
 
346
396
  /*
347
397
  * Hemispheric: sky above, ground below, mixed by which way the surface looks. An
@@ -388,7 +438,7 @@ export const MAIN_GLSL = `void main() {
388
438
  * smallest change available in a shader with a documented history of shared subexpressions
389
439
  * rounding differently when their neighbours move. See the night-emissive term.
390
440
  */
391
- lit = albedo * (ambient + uDirectionalColor * direct * (1.0 - metal));
441
+ lit = albedo * (ambient + sunColor * direct * (1.0 - metal));
392
442
 
393
443
  /*
394
444
  * A sun highlight, for the few surfaces that ask for one.
@@ -450,7 +500,7 @@ export const MAIN_GLSL = `void main() {
450
500
  * before: \`sunSpec\` is \`vec3(vSpecular)\` at metal 0 and \`vSpecular\` defaults to 0.
451
501
  */
452
502
  vec3 sunHighlight =
453
- uDirectionalColor * specularLobe(max(dot(n, halfway), 0.0), surfaceRoughness) * sunSpec * sunShade * sunFacing;
503
+ sunColor * specularLobe(max(dot(n, halfway), 0.0), surfaceRoughness) * sunSpec * sunShade * sunFacing;
454
504
  /*
455
505
  * The dielectric's share goes in here, where it has always gone, so the environment blend
456
506
  * below dims it by exactly the reflectance it always dimmed it by. The metal's share is added
@@ -890,6 +940,9 @@ export const MAIN_GLSL = `void main() {
890
940
  * CLUSTER_TEXELS;
891
941
  int clusterLights = uClustered == 1 ? int(clusterTexel(clusterBaseTexel).x) : 0;
892
942
 
943
+ /* The fixture atlas's size, asked once: its cookie band and its profile rows are found from it. */
944
+ vec2 fixtureSize = vec2(textureSize(uFixtureAtlas, 0));
945
+
893
946
  // Point lights. Inverse-square-ish falloff clipped to a finite radius, so a
894
947
  // light can be culled without a visible seam.
895
948
  for (int slot = 0; slot < LIGHT_LOOP_MAX; slot++) {
@@ -1022,7 +1075,14 @@ export const MAIN_GLSL = `void main() {
1022
1075
  }
1023
1076
  if (falloff <= 0.0) continue;
1024
1077
  float ndl = max(dot(n, toLight / max(dist, 1e-4)), 0.0);
1025
- if (ndl <= 0.0) continue;
1078
+ /*
1079
+ * **A lamp behind a pane is not skipped when the surface is glass**: it lights the pane through
1080
+ * the glass, shaped and shadowed like any lamp, and is added to glassGlow once its shape is
1081
+ * known below. For everything else a lamp behind the surface is skipped here, as always.
1082
+ */
1083
+ float backNdl =
1084
+ glassTransmission > 0.0 ? max(-dot(n, toLight / max(dist, 1e-4)), 0.0) : 0.0;
1085
+ if (ndl <= 0.0 && backNdl <= 0.0) continue;
1026
1086
 
1027
1087
  /*
1028
1088
  * Normal-offset bias.
@@ -1075,7 +1135,17 @@ export const MAIN_GLSL = `void main() {
1075
1135
  */
1076
1136
  int shadowRead = clamp(shadowSlot, 0, MAX_LIGHTS - 1);
1077
1137
  int layer = shadowSlot >= 0 && shadowSlot < MAX_LIGHTS ? uPointShadowLayer[shadowRead] : -1;
1138
+ /* What the panes between this receiver and the lamp let through, per map: lampGlass.ts. */
1139
+ vec3 staticGlass = vec3(1.0);
1140
+ vec3 liveGlass = vec3(1.0);
1078
1141
  if (layer >= 0 && shadowWeight > POINT_SHADOW_MIN_WEIGHT) {
1142
+ staticGlass = pointGlassTint(
1143
+ float(layer),
1144
+ receiverPoint - uPointShadowProjection[shadowRead].xyz,
1145
+ uPointShadowProjection[shadowRead].w,
1146
+ uPointShadowNear[shadowRead],
1147
+ uPointShadowSize[shadowRead]
1148
+ );
1079
1149
  occl = mix(
1080
1150
  1.0,
1081
1151
  pointShadow(
@@ -1115,10 +1185,20 @@ export const MAIN_GLSL = `void main() {
1115
1185
  uLivePointShadowSize[shadowRead]
1116
1186
  );
1117
1187
  liveWeight = uLivePointShadowWeight[shadowRead];
1188
+ liveGlass = pointGlassTint(
1189
+ float(liveLayer),
1190
+ receiverPoint - uLivePointShadowProjection[shadowRead].xyz,
1191
+ uLivePointShadowProjection[shadowRead].w,
1192
+ uLivePointShadowNear[shadowRead],
1193
+ uLivePointShadowSize[shadowRead]
1194
+ );
1118
1195
  }
1119
1196
  occl *= mix(1.0, liveOccl, liveWeight);
1197
+ /* Each map's glass faded in by that map's presence, as its occlusion is. */
1198
+ vec3 lampGlass = mix(vec3(1.0), staticGlass, uPointShadowWeight[shadowRead]) * mix(vec3(1.0), liveGlass, liveWeight);
1120
1199
  #else
1121
1200
  float occl = 1.0;
1201
+ vec3 lampGlass = vec3(1.0);
1122
1202
  #endif
1123
1203
 
1124
1204
  // Blend occlusion out as the light fades, so a shadow can never be more
@@ -1157,7 +1237,9 @@ export const MAIN_GLSL = `void main() {
1157
1237
  vec3 iesRay = -toLight / max(dist, 1e-4);
1158
1238
  /* The angle from the fixture's axis, as a fraction of the 0-to-180 arc every row spans. */
1159
1239
  float iesAngle = acos(clamp(dot(iesRay, lightDir), -1.0, 1.0));
1160
- float iesU = iesAngle / PI_IES;
1240
+ /* Clamped to the row's first and last texel centres, which is what the edge clamp of a
1241
+ texture exactly one row wide gave; the atlas can be wider now, with cookies beside it. */
1242
+ float iesU = clamp(iesAngle / PI_IES * IES_WIDTH, 0.5, IES_WIDTH - 0.5) / fixtureSize.x;
1161
1243
  /*
1162
1244
  * **The azimuth about the fixture's own axis**, behind a branch on a uniform so a scene whose
1163
1245
  * profiles are all axially symmetric pays none of this arithmetic. A \`uniform float\` is
@@ -1211,13 +1293,14 @@ export const MAIN_GLSL = `void main() {
1211
1293
  */
1212
1294
  float cookieTile = max(cookie, 0.0);
1213
1295
  vec2 cookieAt = vec2(
1214
- (cookieTile + clamp(cookieUv.x, COOKIE_INSET, 1.0 - COOKIE_INSET)) / uCookieTiles,
1215
- clamp(cookieUv.y, COOKIE_INSET, 1.0 - COOKIE_INSET));
1296
+ (cookieTile + clamp(cookieUv.x, COOKIE_INSET, 1.0 - COOKIE_INSET)) * COOKIE_SPAN
1297
+ / fixtureSize.x,
1298
+ clamp(cookieUv.y, COOKIE_INSET, 1.0 - COOKIE_INSET) * COOKIE_SPAN / fixtureSize.y);
1216
1299
  /* Unconditional and \`textureLod\`, and selected with a \`mix\` afterwards: the light
1217
1300
  loop is not provably uniform control flow — the clustered arm reads its index from a
1218
1301
  texture — so a branch on the *light's* cookie index around a sample is what the
1219
1302
  2026-08-07 rule forbids. */
1220
- vec3 cookieSample = textureLod(uCookieAtlas, cookieAt, 0.0).rgb;
1303
+ vec3 cookieSample = textureLod(uFixtureAtlas, cookieAt, 0.0).rgb;
1221
1304
  cookieTint = mix(vec3(1.0), cookieSample, step(0.0, cookie));
1222
1305
  }
1223
1306
  }
@@ -1232,13 +1315,17 @@ export const MAIN_GLSL = `void main() {
1232
1315
  * bytes on \`core-only\`** across the sixteen fragment permutations.
1233
1316
  */
1234
1317
  float iesBase = iesRow * (uIesPlaneCount + 1.0);
1318
+ /* The profiles are the atlas's bottom rows, below any cookies. */
1319
+ float iesTop = fixtureSize.y - uIesAtlasRows;
1235
1320
  float iesV = uIesAtlasRows <= 1.0
1236
- ? 0.5
1237
- : (iesBase + iesTurns * uIesPlaneCount + 0.5) / uIesAtlasRows;
1238
- float iesGain = textureLod(uIesAtlas, vec2(iesU, iesV), 0.0).r;
1321
+ ? (iesTop + 0.5) / fixtureSize.y
1322
+ : (iesTop + iesBase + iesTurns * uIesPlaneCount + 0.5) / fixtureSize.y;
1323
+ float iesGain = textureLod(uFixtureAtlas, vec2(iesU, iesV), 0.0).r;
1239
1324
  float photometric = mix(1.0, iesGain, step(0.0, iesProfile));
1240
1325
 
1241
1326
  float shape = (uLightFalloff == 1 ? falloff : falloff * falloff) * coneFalloff * photometric;
1327
+ glassGlow += lightColor * backNdl * shape * lightWeight * shaded * lampGlass;
1328
+ if (ndl <= 0.0) continue;
1242
1329
  /*
1243
1330
  * The cookie tints rather than scales, because a mask may be coloured — a stained window is
1244
1331
  * the case that makes the difference — and a greyscale one is exactly a scale. Applied to
@@ -1277,7 +1364,7 @@ export const MAIN_GLSL = `void main() {
1277
1364
  */
1278
1365
  vec3 lampDiffuse = albedo * lightColor * ndl * shape * lightWeight * (1.0 - metal);
1279
1366
  lampOpen += lampDiffuse;
1280
- lampShadowed += lampDiffuse * shaded;
1367
+ lampShadowed += lampDiffuse * shaded * lampGlass;
1281
1368
 
1282
1369
  /*
1283
1370
  * A lamp reflected in a polished surface, which point lights could not do at all.
@@ -1312,7 +1399,7 @@ export const MAIN_GLSL = `void main() {
1312
1399
  lightColor * sphereLobe(max(dot(n, lampHalfway), 0.0), surfaceRoughness, lightSourceRadius, dist)
1313
1400
  * lampSpec * shape * lightWeight;
1314
1401
  lampOpen += lampHighlight;
1315
- lampShadowed += lampHighlight * shaded;
1402
+ lampShadowed += lampHighlight * shaded * lampGlass;
1316
1403
  }
1317
1404
  /*
1318
1405
  * The most any light in range shadows this point, for the emissive term below —
@@ -1373,7 +1460,11 @@ export const MAIN_GLSL = `void main() {
1373
1460
  */
1374
1461
  float signedForm = quadFormFactor(n, vWorldPos, c0, c1, c2, c3);
1375
1462
  float form = mix(max(0.0, signedForm), abs(signedForm), uAreaLightTwoSided[a]);
1376
- if (form <= 0.0) continue;
1463
+ /* The form factor is linear in the normal, so the one seen from behind is its negation. */
1464
+ float backForm = glassTransmission > 0.0
1465
+ ? mix(max(0.0, -signedForm), abs(signedForm), uAreaLightTwoSided[a])
1466
+ : 0.0;
1467
+ if (form <= 0.0 && backForm <= 0.0) continue;
1377
1468
 
1378
1469
  /*
1379
1470
  * **How much of the rectangle this fragment can actually see past whatever is in the way.**
@@ -1400,6 +1491,7 @@ export const MAIN_GLSL = `void main() {
1400
1491
  * is a constant nobody has measured. \`docs/IMPROVEMENTS.md\` carries the row.
1401
1492
  */
1402
1493
  float areaOccl = 1.0;
1494
+ vec3 areaGlass = vec3(1.0);
1403
1495
  #if POINT_SHADOWS
1404
1496
  /*
1405
1497
  * The sample position is pushed along the surface normal before the lookup, and it scales
@@ -1440,6 +1532,12 @@ export const MAIN_GLSL = `void main() {
1440
1532
  ),
1441
1533
  uAreaShadowWeight[a]
1442
1534
  );
1535
+ /* What the panes before the rectangle let through, faded in as its occlusion is. */
1536
+ areaGlass = mix(
1537
+ vec3(1.0),
1538
+ areaGlassTint(float(areaLayer), areaShadowFrom, uAreaShadowFar[a], uAreaShadowNear[a], right, up, halfSize),
1539
+ uAreaShadowWeight[a]
1540
+ );
1443
1541
  }
1444
1542
  int areaLiveLayer = uLiveAreaShadowLayer[a];
1445
1543
  if (areaLiveLayer >= 0) {
@@ -1457,13 +1555,28 @@ export const MAIN_GLSL = `void main() {
1457
1555
  ),
1458
1556
  uLiveAreaShadowWeight[a]
1459
1557
  );
1558
+ areaGlass *= mix(
1559
+ vec3(1.0),
1560
+ areaGlassTint(
1561
+ float(areaLiveLayer),
1562
+ areaShadowFrom,
1563
+ uLiveAreaShadowFar[a],
1564
+ uLiveAreaShadowNear[a],
1565
+ right,
1566
+ up,
1567
+ halfSize
1568
+ ),
1569
+ uLiveAreaShadowWeight[a]
1570
+ );
1460
1571
  }
1461
1572
  #endif
1462
1573
 
1574
+ glassGlow += uAreaLightColor[a] * backForm * areaOccl * areaGlass;
1575
+ if (form <= 0.0) continue;
1463
1576
  /* Held back with the point lamps', and shadowed by the same term it always was. */
1464
1577
  vec3 areaDiffuse = albedo * uAreaLightColor[a] * form * (1.0 - metal);
1465
1578
  lampOpen += areaDiffuse;
1466
- lampShadowed += areaDiffuse * areaOccl;
1579
+ lampShadowed += areaDiffuse * areaOccl * areaGlass;
1467
1580
 
1468
1581
  if (vSpecular > 0.0 || metal > 0.0) {
1469
1582
  /*
@@ -1490,7 +1603,7 @@ export const MAIN_GLSL = `void main() {
1490
1603
  vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);
1491
1604
  vec3 areaHighlight = areaSpec * uAreaLightColor[a] * coverage;
1492
1605
  lampOpen += areaHighlight;
1493
- lampShadowed += areaHighlight * areaOccl;
1606
+ lampShadowed += areaHighlight * areaOccl * areaGlass;
1494
1607
  }
1495
1608
  }
1496
1609
  }
@@ -1550,6 +1663,9 @@ export const MAIN_GLSL = `void main() {
1550
1663
  * \`driftLight.ts\`.
1551
1664
  */
1552
1665
  if (driftShare > 0.0) {
1666
+ if (glassTransmission > 0.0) {
1667
+ glassGlow += driftLightIrradiance(vWorldPos, -n) * (driftShare * uDriftLight.w);
1668
+ }
1553
1669
  lit += albedo * driftLightIrradiance(vWorldPos, n) * (driftShare * uDriftLight.w) * (1.0 - metal);
1554
1670
  }
1555
1671
 
@@ -1621,10 +1737,14 @@ export const MAIN_GLSL = `void main() {
1621
1737
  */
1622
1738
  vec3 emissiveMapped = vec3(1.0);
1623
1739
  if (uEmissiveMapEnabled != 0) {
1624
- emissiveMapped = texture(uEmissiveMap, vUv).rgb * uEmissiveScale;
1740
+ emissiveMapped = texture(uEmissiveMap, surfaceAt).rgb * uEmissiveScale;
1625
1741
  }
1742
+ /* Pulse, flicker and fade from the layer's effects; exactly 1 where it names none. */
1743
+ float fxGlow = fx4.z + fx5.x + fx5.w > 0.0 ? fxEmission(surfaceAt.z) : 1.0;
1626
1744
  lit += emissiveTint * emissiveMapped * uEmissiveGain * vEmissive * uNightFactor
1627
- * mix(1.0, min(lightShade, sunShade), EMISSIVE_SHADOW_SHARE);
1745
+ * mix(1.0, min(lightShade, sunShade), EMISSIVE_SHADOW_SHARE) * fxGlow;
1746
+ /* And a lit window's glow, which the scene's lit share already times: not gated on night. */
1747
+ lit += windowGlow * uEmissiveGain;
1628
1748
 
1629
1749
  #if NIGHT_EMISSIVE
1630
1750
  /*
@@ -1698,7 +1818,14 @@ export const MAIN_GLSL = `void main() {
1698
1818
  fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);
1699
1819
  }
1700
1820
 
1701
- vec3 shaded = applyOutputTransform(mix(lit, mediumColor(), fog));
1821
+ /*
1822
+ * A surface recedes into the medium; light added to the frame fades in it (\`uFogEnabled\` at 2,
1823
+ * \`drawFog.ts\`). What is behind an additive draw is already fogged, so mixing the draw toward
1824
+ * the medium's colour and adding it puts the haze into the frame twice — a glow by day drew as
1825
+ * a grey shape of itself, greyer the thicker the air.
1826
+ */
1827
+ vec3 shaded = applyOutputTransform(
1828
+ uFogEnabled == 2 ? lit * (1.0 - fog) : mix(lit, mediumColor(), fog));
1702
1829
 
1703
1830
  /*
1704
1831
  * **Refraction: the scene behind this surface, bent and absorbed.**
@@ -1737,14 +1864,39 @@ export const MAIN_GLSL = `void main() {
1737
1864
  * splitting the specular out before the fog, which is a larger change to the one shader every
1738
1865
  * draw uses.
1739
1866
  */
1740
- if (uRefractStrength > 0.0) {
1867
+ if (uSeeThrough.x > 0.0 || glassTransmission > 0.0) {
1741
1868
  vec3 refractN = normalize(vNormal);
1742
1869
  vec3 refractV = normalize(uCameraPos - vWorldPos);
1743
- vec2 screenUv = gl_FragCoord.xy / vec2(textureSize(uRefractScene, 0));
1744
- vec2 bent = clamp(screenUv + refractN.xy * uRefractStrength, vec2(0.0), vec2(1.0));
1745
- vec3 behind = texture(uRefractScene, bent).rgb;
1746
- float pathLength = uRefractThickness * vThickness / max(dot(refractN, refractV), 0.05);
1747
- shaded = behind * pow(uRefractTint, vec3(pathLength));
1870
+ ivec2 snapSize = textureSize(uRefractScene, 0);
1871
+ vec2 screenUv = gl_FragCoord.xy / vec2(snapSize);
1872
+ vec2 bent = clamp(screenUv + refractN.xy * uSeeThrough.x, vec2(0.0), vec2(1.0));
1873
+ /*
1874
+ * **An explicit level, and frost is what picks it** — the 2026-08-07 rule, and the reason a
1875
+ * frosted pane blurs rather than fades: the copy carries a mip chain on a frame that draws glass,
1876
+ * so a milkier pane reads a coarser level of it. Level 0 on a copy with one level is exactly what
1877
+ * \`texture()\` read here before, so plain refraction draws as it did.
1878
+ */
1879
+ /* Two short of the last level: a fully frosted pane still shows the large light and dark of what
1880
+ is behind it, where the chain's last level is one texel, the whole frame's average. */
1881
+ float levels = max(floor(log2(float(max(snapSize.x, snapSize.y)))) - 2.0, 0.0);
1882
+ vec3 behind = textureLod(uRefractScene, bent, glassFrost * levels).rgb;
1883
+ /* Unsigned: a pane passes the same light seen from either side, and so does its Fresnel. */
1884
+ float cosView = max(abs(dot(refractN, refractV)), 0.05);
1885
+ float pathLength = uSeeThrough.y * vThickness / cosView;
1886
+ vec3 seen = behind * pow(uRefractTint, vec3(pathLength));
1887
+ if (glassTransmission > 0.0) {
1888
+ /*
1889
+ * **Glass keeps its own surface and shows what is behind over it**, by how much it lets
1890
+ * through and how little it reflects at this angle — Schlick's Fresnel at 0.04, the arithmetic
1891
+ * \`glass.ts\` states. So a clear pane keeps its highlight, and a pane seen edge-on is a mirror.
1892
+ * The glow of the lights behind it is added last, by how frosted it is.
1893
+ */
1894
+ float fresnel = 0.04 + 0.96 * pow(1.0 - cosView, 5.0);
1895
+ shaded = mix(shaded, seen * uGlassTint, glassTransmission * (1.0 - fresnel)) +
1896
+ applyOutputTransform(glassGlow * uGlassTint * (glassFrost * glassTransmission) * (1.0 - fog));
1897
+ } else {
1898
+ shaded = seen;
1899
+ }
1748
1900
  }
1749
1901
  /*
1750
1902
  * The per-vertex alpha lane, folded in here rather than given an expression of its own so the
@@ -1770,7 +1922,7 @@ export const MAIN_GLSL = `void main() {
1770
1922
  * up holding the weighted sum of colour and the weighted sum of alpha. The revealage pass draws
1771
1923
  * the same geometry with this at 0, where the branch below is not taken at all.
1772
1924
  */
1773
- if (uOitWeighted != 0.0) {
1925
+ if (uWriteMode.x != 0.0) {
1774
1926
  float z = distance(vWorldPos, uCameraPos) / 200.0;
1775
1927
  float falloff = 0.03 / (1e-5 + z * z * z * z);
1776
1928
  float w = alpha * clamp(falloff, 1e-2, 3e3);
@@ -1 +1 @@
1
- export declare const POINTSHADOW_GLSL = "/**\n * How much of one tap's occlusion survives its own penumbra.\n *\n * **How much of a shadow survives, as well as how soft it is.** A penumbra does not merely blur\n * with distance from its caster, it *takes over*: far enough from the occluder, relative to how\n * big the source is, every point on the receiver can see part of the flame and there is no umbra\n * left at all. The shadow is gone, not blurry. Without it, two columns lit by a brazier threw\n * full-strength edges fifteen metres across the terrace, and the gap between them read as a\n * spotlight cone with straight sides rather than as a fire.\n *\n * **Per tap, and it was once for the whole filter, taken from the centre tap alone.** That is a\n * blocker search of one sample standing in for twelve, and it is right wherever the taps agree \u2014\n * inside an umbra they all find the same occluder, so this returns exactly what the shared value\n * did, and the umbra is unchanged to the bit. It is wrong exactly where they disagree, which is\n * the silhouette: there the centre ray *misses*, so the search reports no occluder, the spread is\n * zero and the fade is switched off \u2014 while the taps that do land on the caster then draw at full\n * strength. What that paints is a hard one-texel line around a shadow whose interior has\n * correctly faded to nothing.\n *\n * Reported as shadows \"faded out but NOT shadow outlines, generating long lines around the\n * world\", and on a brazier as a bare square outline lying on the ground with no shadow inside it.\n *\n * **What it costs** is two multiplies and a divide per occluded tap, against a tap that has\n * already paid for a texture fetch. **What would make it wrong** is a filter wide enough that one\n * tap's occluder is not the occluder a neighbouring tap is estimating for, which is the same\n * limit the shared value had and is what `MAX_FILTER_RADIUS` bounds.\n */\nfloat tapStrength(float receiverDistance, float occluderDistance, float sourceRadius) {\n float spread = max(receiverDistance - occluderDistance, 0.0) / max(occluderDistance, 0.05);\n float penumbra = clamp(sourceRadius * spread * PENUMBRA_FADE, 0.0, 1.0);\n return 1.0 - penumbra * penumbra;\n}\n\nfloat pointShadow(\n highp sampler2DArray maps,\n float layer,\n vec3 toFrag,\n float far,\n float near,\n float sourceRadius\n) {\n /*\n * The receiver's own distance, and it is the *radius* now rather than a cube face's local Z.\n *\n * That difference removes two things. Every tap of an angular disk shares this one number,\n * where a cube needed each tap's own face-local Z recomputed \u2014 offsets changed it, and\n * comparing them all against the centre ray drew repeated wedges on large receivers. And the\n * stored value converts with a multiply instead of the perspective divide, which used to\n * magnify whatever error the sample arrived with, worst near the far plane.\n */\n float dist = length(toFrag);\n\n /*\n * A receiver nearer than the near plane is not shadowed at all.\n *\n * The near plane already says \"nothing this close to the source is rendered into the\n * map\", and a caller raises it so a fixture does not shadow its own housing. That was\n * only ever applied to *casters*, so a surface inside the same distance still went\n * through the comparison \u2014 against a map that, by the caller's own instruction, holds\n * nothing about anything near it.\n *\n * Reported on a ceiling with recessed strip lights hung a hand's width below it: shadows\n * that \"go on and off\" as the camera moves. The seams that made it unmissable are gone;\n * the gate stays, because comparing against a map that holds nothing is still wrong.\n *\n * The gate is the caller's own number rather than a new constant. A consumer that wants\n * the surface right beside its lamp to take a shadow can lower it, which is the same\n * knob and the same meaning it has always had for the other side of the transaction.\n */\n if (dist <= near) return 1.0;\n\n // Modest depth bias only. The heavy lifting is done by offsetting the\n // sample position along the surface normal before this is called \u2014 see the\n // call site \u2014 which is the cure for grazing receivers that depth bias alone\n // cannot fix without peter-panning.\n float bias = 0.04 + dist * 0.015;\n\n /*\n * Penumbra from the emitter's actual size.\n *\n * A shadow's softness is not a constant: it is set by how large the source is\n * and how far the caster stands from the surface it falls on. Find the\n * occluder with a centre tap, then widen the filter by\n * sourceRadius * (receiver - occluder) / occluder \u2014 the real geometry of a\n * penumbra. A bulb stays crisp; a flame goes soft with distance, which is\n * what makes a fire read as a fire rather than a large lamp.\n *\n * textureLod(..., 0.0), not the implicit-derivative texture() call this replaced.\n * pointShadowArray.ts allocates the array with texStorage3D at one storage level,\n * NEAREST both filters \u2014 there is no mip to select, so the two calls fetch the\n * identical texel. Not a quality trade; the output is bit-for-bit the same.\n *\n * It matters because implicit derivatives are only well-defined under *uniform*\n * control flow, and this sits inside the light loop below. See AGENTS.md, 2026-08-07.\n */\n float centreStored = textureLod(maps, vec3(octEncode(toFrag), layer), 0.0).r;\n float occluderDistance = centreStored >= 0.9999 ? dist : centreStored * far;\n float spread = max(dist - occluderDistance, 0.0) / max(occluderDistance, 0.05);\n /*\n * Softness comes from *fading the shadow*, not from widening the filter.\n *\n * Four attempts went into widening it, and the fourth is what identified why\n * none of them worked. The decisive observation was that the light pool looked\n * right until a column entered it. With no occluder the penumbra estimate is zero,\n * the filter stays at its floor, and the light pool is clean. The instant a\n * column is found the filter widens \u2014 and a wide angular filter on a *cube*\n * map is what crossed face seams. The artefact was never the columns' shadows;\n * it was the search for their penumbra.\n *\n * The map has no seams now, so the cap could open. It has not, in this change:\n * the storage moved and nothing else did, and the widening is a picture decision\n * taken against a tree that already renders correctly.\n */\n float radius = clamp(sourceRadius * spread, 0.01, MAX_FILTER_RADIUS) + 0.012;\n\n /*\n * Taps are placed on a disk *perpendicular to the light ray*, turned by half a golden\n * angle on every other column of pixels.\n *\n * Offsetting the ray by fixed 3D vectors was the earlier approach and it has\n * two faults that only show on a wide filter. The offsets are not\n * perpendicular to the ray, so part of each one moves the sample along its own\n * depth instead of across the shadow; and twelve *fixed* taps at a large\n * radius align, so the penumbra breaks into hard straight bands following the\n * tap pattern \u2014 reported, on a brazier behind a rank of columns, as strange\n * straight lines across the light pool.\n *\n * **The cure for that was a per-fragment hash, and it is what made the penumbra\n * stipple.** Twelve taps are a coarse estimate of how much of the emitter this point can\n * see; turning them by an unrelated angle at every pixel makes each pixel's error\n * independent of its neighbour's, which is the definition of noise. Something has to\n * average that back out, and this engine's lit pass is the last thing to touch the frame \u2014\n * there is no blur and no temporal resolve behind it, so the noise reached the screen. Two\n * games reported the same coarse cross-hatch across a light pool.\n *\n * **So the turn comes from a two-pixel tile instead, and the pair is averaged afterwards.**\n * A pixel's own x parity picks one of two tap sets half a golden angle apart, and\n * `main` folds the pair together once the light loop is over \u2014 see the resolve there. The\n * pair's twenty-four taps are the filter; each pixel pays for twelve of them.\n *\n * **A *column* pair rather than a 2x2 square, and that was measured rather than assumed.**\n * Four turns on a square tile, resolved with both derivatives, came back at 3.88 against this\n * pair's 2.46 on the same build \u2014 because the second derivative hands the lower row of a quad\n * the upper row's difference on hardware that answers a coarse derivative, and with four turns\n * that is the wrong difference. Two turns keyed on the column alone make it the right one,\n * which is why the tile's shape and the resolve are one decision rather than two.\n *\n * **Neither half works alone**, which is the same pairing `ambientOcclusion.ts` describes\n * between its rotation tile and its blur. Measured on `demo/dev/pointshadow.html` at\n * `?exposure=6`, as mean absolute luminance difference to the right and down neighbour over\n * the mover's penumbra, on WebGL2: **4.60** for the per-fragment hash this replaces, **2.88**\n * for this tile with no resolve \u2014 and a one-pixel vertical comb in the picture, which is the\n * tile itself on screen \u2014 against **2.37** for the pair resolved. A frame with the shadow\n * switched off measures 0.81, and WebGPU answers 4.56 and 2.33 for the same two builds.\n *\n * **Unchanged by the move to an octahedral map, and that is the point.** The offsets are\n * angular perturbations of a direction, so every tap is still a direction and octEncode is\n * defined on all of them. Nothing offsets in UV, so the map's border needs no wrap\n * arithmetic and a tap that would have crossed a cube join lands on the adjacent texel.\n */\n vec3 fwd = normalize(toFrag);\n vec3 up = abs(fwd.y) < 0.9 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);\n vec3 tu = normalize(cross(up, fwd));\n vec3 tv = cross(fwd, tu);\n /*\n * Integer arithmetic, because this is a lattice: a shader compiler is free to implement\n * `mod(x, 2.0)` as a multiply by a reciprocal, and a GPU here answers 3 for `3.0 % 3.0`.\n * `gl_FragCoord.x` is the pixel's centre, so truncating it is the pixel's own column.\n */\n float angle = float(int(gl_FragCoord.x) & 1) * PCF_PAIR_TURN;\n float ca = cos(angle);\n float sa = sin(angle);\n\n float lit = 0.0;\n for (int i = 0; i < 12; i++) {\n if (i >= uShadowFilterTaps) break;\n vec2 disk = PCF_OFFSETS[i];\n vec2 turned = vec2(disk.x * ca - disk.y * sa, disk.x * sa + disk.y * ca);\n // Perpendicular to the ray, and *not* scaled by its length: the offsets are\n // an angular perturbation of a direction, which is what the encoding takes.\n vec3 sampleDir = toFrag + (tu * turned.x + tv * turned.y) * radius;\n float stored = textureLod(maps, vec3(octEncode(sampleDir), layer), 0.0).r;\n if (stored >= 0.9999) {\n // Beyond the far plane nothing was rendered: treat as lit.\n lit += 1.0;\n continue;\n }\n float storedDistance = stored * far;\n if (dist - bias > storedDistance) {\n // A point shadow cannot outlive the finite light that casts it, so its\n // occlusion fades smoothly before the light's far plane.\n float reach = shadowReach(max(dist - storedDistance, 0.0), far);\n lit += 1.0 - reach * tapStrength(dist, storedDistance, sourceRadius);\n } else {\n lit += 1.0;\n }\n }\n return lit / float(max(uShadowFilterTaps, 1));\n}\n\n/**\n * The same filter for a source that has a *shape* rather than a radius.\n *\n * **One map from the rectangle's centre, and the rectangle's own extent as the filter's shape.**\n * That is the whole difference from `pointShadow` above, and it is what makes the result read as\n * one wide emitter rather than as a lamp: a bulb's penumbra is round, so a row of nine of them\n * gives nine round shadows that pile up darker where they overlap, and a strip's penumbra is a\n * strip \u2014 soft along the cable, tight across it, one shadow with one soft edge.\n *\n * **The taps land on the emitter's silhouette as this fragment sees it**, which is the part a\n * scalar radius cannot express. The rectangle's two axes are projected onto the plane across the\n * light ray and each keeps its own width, so the sample pattern is an ellipse rather than a disk,\n * and a rectangle seen edge-on along one axis has no penumbra in that direction \u2014 which is correct\n * and is what a strip light actually does. Four dot products and two square roots more than the\n * round filter.\n *\n * **The fade is driven by the *narrower* projected half extent, and the reason is a strip.** A\n * penumbra does not only blur a shadow, it takes over: past the point where every part of the\n * receiver can see part of the emitter there is no umbra left. Driven by the wider extent, a\n * three-metre cable would delete its own shadow at a metre of caster clearance \u2014 but across the\n * cable the source is nearly a line, so an umbra band survives there and only the length is\n * smeared. `min` keeps that band; `max` would erase it.\n *\n * **What it costs** is a second copy of the tap loop, about 30 lines, compiled into the eight\n * permutations that carry point shadows. Folding the two into one loop was the alternative and was\n * not taken: the shared form has to multiply the disk offset by a width before scaling the axis\n * rather than after, which is a different rounding order on every tap of the *point* path \u2014 so the\n * saving is paid for by perturbing eight scenes that currently gate at zero pixels, in a change\n * about area lights. `docs/IMPROVEMENTS.md` carries the row.\n *\n * **What would make it wrong** is a rectangle much wider than its distance to the caster, where\n * different points on the emitter genuinely see different silhouettes and no single map from the\n * centre stands in for all of them \u2014 a ceiling-wide panel a hand's width above a table. The honest\n * answer there is several maps across the emitter, which is several bakes.\n */\nfloat areaShadow(\n highp sampler2DArray maps,\n float layer,\n vec3 toFrag,\n float far,\n float near,\n vec3 right,\n vec3 up,\n vec2 halfSize\n) {\n float dist = length(toFrag);\n /* The near plane says nothing this close to the emitter was rendered; see `pointShadow`. */\n if (dist <= near) return 1.0;\n\n float bias = 0.04 + dist * 0.015;\n\n /*\n * The blocker search, one tap along the centre ray, exactly as the round filter does it. Similar\n * triangles turn the two distances into how much wider than the emitter the penumbra is.\n *\n * `textureLod(..., 0.0)` rather than `texture()`: the array carries one storage level and\n * NEAREST both filters, so the two fetch the identical texel \u2014 and this sits inside the area\n * light loop, which is not provably uniform control flow. See AGENTS.md, 2026-08-07.\n */\n float centreStored = textureLod(maps, vec3(octEncode(toFrag), layer), 0.0).r;\n float occluderDistance = centreStored >= 0.9999 ? dist : centreStored * far;\n float spread = max(dist - occluderDistance, 0.0) / max(occluderDistance, 0.05);\n\n vec3 fwd = toFrag / dist;\n /*\n * Each axis with its along-the-ray component removed: the rectangle's outline flattened onto the\n * plane the taps move in. The lengths that come back are the foreshortening \u2014 1 for a rectangle\n * seen face-on, 0 for one seen exactly edge-on along that axis \u2014 so the two widths below carry\n * both the emitter's size and how much of it this fragment can see.\n */\n vec3 acrossX = right - fwd * dot(right, fwd);\n vec3 acrossY = up - fwd * dot(up, fwd);\n float seenX = length(acrossX);\n float seenY = length(acrossY);\n /*\n * A collapsed axis offsets nothing rather than offsetting along an arbitrary direction. Both\n * cannot collapse at once: `right` and `up` are perpendicular, so a ray parallel to one of\n * them is perpendicular to the other.\n */\n vec3 alongX = seenX > 1e-5 ? acrossX / seenX : vec3(0.0);\n vec3 alongY = seenY > 1e-5 ? acrossY / seenY : vec3(0.0);\n\n float halfX = halfSize.x * seenX;\n float halfY = halfSize.y * seenY;\n /* The same ceiling and the same floor the round filter uses, per axis. See MAX_FILTER_RADIUS. */\n float widthX = min(halfX * spread, MAX_FILTER_RADIUS) + 0.012;\n float widthY = min(halfY * spread, MAX_FILTER_RADIUS) + 0.012;\n\n /*\n * Turned inside the unit disk *before* the ellipse maps it, so the half-turn that separates\n * the two halves of a pixel pair survives the anisotropy. The same two-pixel tile the round\n * filter uses and for the same reasons \u2014 see `pointShadow`, which carries the measurement;\n * a rectangle's shadow stippled exactly as a lamp's did, because it is the same twelve taps.\n */\n float angle = float(int(gl_FragCoord.x) & 1) * PCF_PAIR_TURN;\n float ca = cos(angle);\n float sa = sin(angle);\n\n float lit = 0.0;\n for (int i = 0; i < 12; i++) {\n if (i >= uShadowFilterTaps) break;\n vec2 disk = PCF_OFFSETS[i];\n vec2 turned = vec2(disk.x * ca - disk.y * sa, disk.x * sa + disk.y * ca);\n vec3 sampleDir = toFrag + alongX * (turned.x * widthX) + alongY * (turned.y * widthY);\n float stored = textureLod(maps, vec3(octEncode(sampleDir), layer), 0.0).r;\n if (stored >= 0.9999) {\n /* Beyond the far plane nothing was rendered: treat as lit. */\n lit += 1.0;\n continue;\n }\n float storedDistance = stored * far;\n if (dist - bias > storedDistance) {\n /* An area light's occlusion cannot outlive the map that carries it either. */\n float reach = shadowReach(max(dist - storedDistance, 0.0), far);\n /* The narrower half extent, for the reason in the header: it is what keeps a strip's\n umbra. Per tap, for the reason `tapStrength` gives. */\n lit += 1.0 - reach * tapStrength(dist, storedDistance, min(halfX, halfY));\n } else {\n lit += 1.0;\n }\n }\n return lit / float(max(uShadowFilterTaps, 1));\n}\n#endif\n\nout vec4 outColor;\n\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n return linearToSrgb(c);\n}\n\n\n/**\n * A fixed Poisson disk avoids the grid-shaped edge left by square 3x3 PCF.\n * Directional and point shadows consume the same maximum filter-tap budget;\n * their texture sizes differ because one is a 2D projection of a frustum and one\n * is an octahedral projection of the whole sphere, not because one is a lower\n * quality tier.\n */\n#if DIRECTIONAL_SHADOWS\nconst vec2 DIRECTIONAL_PCF_OFFSETS[12] = vec2[12](\n vec2(-0.326, -0.406), vec2( 0.519, 0.767), vec2( 0.962, -0.195), vec2(-0.696, 0.457),\n vec2(-0.840, -0.074), vec2( 0.185, -0.893), vec2( 0.896, 0.412), vec2(-0.203, 0.621),\n vec2( 0.473, -0.480), vec2( 0.507, 0.064), vec2(-0.322, -0.933), vec2(-0.792, -0.598)\n);\n\n/** Anything outside the light frustum or beyond its far plane counts as lit. */";
1
+ export declare const POINTSHADOW_GLSL: string;
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { MAX_SHADOW_FILTER_TAPS } from '../../renderQuality.js';
16
16
  import { OUTPUT_TRANSFORM_GLSL } from '../outputTransform.js';
17
+ import { LAMP_GLASS_GLSL } from './lampGlass.js';
17
18
  export const POINTSHADOW_GLSL = `/**
18
19
  * How much of one tap's occlusion survives its own penumbra.
19
20
  *
@@ -337,6 +338,7 @@ float areaShadow(
337
338
  }
338
339
  return lit / float(max(uShadowFilterTaps, 1));
339
340
  }
341
+ ${LAMP_GLASS_GLSL}
340
342
  #endif
341
343
 
342
344
  out vec4 outColor;