@driftengine/core 3.61.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 (955) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/behavior/behaviorTree.d.ts +140 -0
  5. package/dist/behavior/behaviorTree.js +289 -0
  6. package/dist/build/shaderComments.d.ts +64 -0
  7. package/dist/build/shaderComments.js +0 -0
  8. package/dist/cinematic/player.d.ts +80 -0
  9. package/dist/cinematic/player.js +184 -0
  10. package/dist/cinematic/script.d.ts +63 -0
  11. package/dist/cinematic/script.js +141 -0
  12. package/dist/core/bootGate.d.ts +37 -0
  13. package/dist/core/bootGate.js +47 -0
  14. package/dist/core/loadTracker.d.ts +78 -0
  15. package/dist/core/loadTracker.js +106 -0
  16. package/dist/core/loop.d.ts +118 -0
  17. package/dist/core/loop.js +84 -0
  18. package/dist/core/messageQueue.d.ts +61 -0
  19. package/dist/core/messageQueue.js +104 -0
  20. package/dist/core/onceSet.d.ts +27 -0
  21. package/dist/core/onceSet.js +62 -0
  22. package/dist/core/preferences.d.ts +47 -0
  23. package/dist/core/preferences.js +140 -0
  24. package/dist/core/remoteSave.d.ts +154 -0
  25. package/dist/core/remoteSave.js +249 -0
  26. package/dist/core/rng.d.ts +64 -0
  27. package/dist/core/rng.js +72 -0
  28. package/dist/core/stepBudget.d.ts +91 -0
  29. package/dist/core/stepBudget.js +105 -0
  30. package/dist/core/storage.d.ts +52 -0
  31. package/dist/core/storage.js +85 -0
  32. package/dist/core/tickTrace.d.ts +72 -0
  33. package/dist/core/tickTrace.js +102 -0
  34. package/dist/core/timeCurve.d.ts +83 -0
  35. package/dist/core/timeCurve.js +107 -0
  36. package/dist/core/withDefaults.d.ts +21 -0
  37. package/dist/core/withDefaults.js +29 -0
  38. package/dist/dev/contactProbe.d.ts +41 -0
  39. package/dist/dev/contactProbe.js +58 -0
  40. package/dist/dev/fpsMeter.d.ts +45 -0
  41. package/dist/dev/fpsMeter.js +86 -0
  42. package/dist/environment/celestialClock.d.ts +141 -0
  43. package/dist/environment/celestialClock.js +262 -0
  44. package/dist/geometry/decal.d.ts +61 -0
  45. package/dist/geometry/decal.js +178 -0
  46. package/dist/geometry/filmPatch.d.ts +35 -0
  47. package/dist/geometry/filmPatch.js +112 -0
  48. package/dist/geometry/growable.d.ts +70 -0
  49. package/dist/geometry/growable.js +167 -0
  50. package/dist/geometry/lightVolume.d.ts +76 -0
  51. package/dist/geometry/lightVolume.js +65 -0
  52. package/dist/geometry/meshBuilder.d.ts +426 -0
  53. package/dist/geometry/meshBuilder.js +1184 -0
  54. package/dist/geometry/pixelFont.d.ts +59 -0
  55. package/dist/geometry/pixelFont.js +179 -0
  56. package/dist/geometry/ribbon.d.ts +101 -0
  57. package/dist/geometry/ribbon.js +546 -0
  58. package/dist/geometry/shellFill.d.ts +101 -0
  59. package/dist/geometry/shellFill.js +203 -0
  60. package/dist/geometry/spline.d.ts +162 -0
  61. package/dist/geometry/spline.js +527 -0
  62. package/dist/geometry/tangents.d.ts +30 -0
  63. package/dist/geometry/tangents.js +142 -0
  64. package/dist/geometry/textMesh.d.ts +51 -0
  65. package/dist/geometry/textMesh.js +57 -0
  66. package/dist/geometry/treeBuilder.d.ts +37 -0
  67. package/dist/geometry/treeBuilder.js +56 -0
  68. package/dist/host/display.d.ts +135 -0
  69. package/dist/host/display.js +114 -0
  70. package/dist/host/files.d.ts +25 -0
  71. package/dist/host/files.js +44 -0
  72. package/dist/host/lifecycle.d.ts +45 -0
  73. package/dist/host/lifecycle.js +28 -0
  74. package/dist/host/platformServices.d.ts +23 -0
  75. package/dist/host/platformServices.js +1 -0
  76. package/dist/host/screen.d.ts +49 -0
  77. package/dist/host/screen.js +90 -0
  78. package/dist/index.d.ts +289 -0
  79. package/dist/index.js +320 -0
  80. package/dist/input/actionMap.d.ts +181 -0
  81. package/dist/input/actionMap.js +314 -0
  82. package/dist/input/activeDevice.d.ts +55 -0
  83. package/dist/input/activeDevice.js +83 -0
  84. package/dist/input/gamepadIdentity.d.ts +43 -0
  85. package/dist/input/gamepadIdentity.js +108 -0
  86. package/dist/input/gamepadMapping.d.ts +56 -0
  87. package/dist/input/gamepadMapping.js +91 -0
  88. package/dist/input/input.d.ts +281 -0
  89. package/dist/input/input.js +703 -0
  90. package/dist/input/inputHarness.d.ts +42 -0
  91. package/dist/input/inputHarness.js +193 -0
  92. package/dist/input/rightGesture.d.ts +57 -0
  93. package/dist/input/rightGesture.js +49 -0
  94. package/dist/input/rumble.d.ts +71 -0
  95. package/dist/input/rumble.js +119 -0
  96. package/dist/input/touchControls.d.ts +138 -0
  97. package/dist/input/touchControls.js +352 -0
  98. package/dist/input/typingTarget.d.ts +6 -0
  99. package/dist/input/typingTarget.js +27 -0
  100. package/dist/math/bounds.d.ts +61 -0
  101. package/dist/math/bounds.js +121 -0
  102. package/dist/math/color.d.ts +8 -0
  103. package/dist/math/color.js +35 -0
  104. package/dist/math/exact.d.ts +66 -0
  105. package/dist/math/exact.js +262 -0
  106. package/dist/math/frustum.d.ts +44 -0
  107. package/dist/math/frustum.js +90 -0
  108. package/dist/math/intersect.d.ts +76 -0
  109. package/dist/math/intersect.js +185 -0
  110. package/dist/math/scalar.d.ts +51 -0
  111. package/dist/math/scalar.js +99 -0
  112. package/dist/math/spring.d.ts +59 -0
  113. package/dist/math/spring.js +88 -0
  114. package/dist/nav/navGraph.d.ts +71 -0
  115. package/dist/nav/navGraph.js +111 -0
  116. package/dist/nav/navPath.d.ts +93 -0
  117. package/dist/nav/navPath.js +162 -0
  118. package/dist/nav/navSearch.d.ts +74 -0
  119. package/dist/nav/navSearch.js +198 -0
  120. package/dist/physics/boxSurface.d.ts +48 -0
  121. package/dist/physics/boxSurface.js +73 -0
  122. package/dist/physics/colliderSurface.d.ts +46 -0
  123. package/dist/physics/colliderSurface.js +146 -0
  124. package/dist/physics/compositeSurface.d.ts +33 -0
  125. package/dist/physics/compositeSurface.js +108 -0
  126. package/dist/physics/heightSurface.d.ts +103 -0
  127. package/dist/physics/heightSurface.js +153 -0
  128. package/dist/physics/ribbonSurface.d.ts +239 -0
  129. package/dist/physics/ribbonSurface.js +522 -0
  130. package/dist/render/ambientOcclusionPass.d.ts +53 -0
  131. package/dist/render/ambientOcclusionPass.js +179 -0
  132. package/dist/render/areaLights.d.ts +116 -0
  133. package/dist/render/areaLights.js +91 -0
  134. package/dist/render/areaShadowSet.d.ts +184 -0
  135. package/dist/render/areaShadowSet.js +277 -0
  136. package/dist/render/atmosphere.d.ts +94 -0
  137. package/dist/render/atmosphere.js +91 -0
  138. package/dist/render/backend/api.d.ts +496 -0
  139. package/dist/render/backend/api.js +1 -0
  140. package/dist/render/backend/budget.d.ts +82 -0
  141. package/dist/render/backend/budget.js +99 -0
  142. package/dist/render/backend/createRenderer.d.ts +135 -0
  143. package/dist/render/backend/createRenderer.js +201 -0
  144. package/dist/render/backend/deadline.d.ts +24 -0
  145. package/dist/render/backend/deadline.js +38 -0
  146. package/dist/render/backend/probe.d.ts +30 -0
  147. package/dist/render/backend/probe.js +129 -0
  148. package/dist/render/backend/select.d.ts +70 -0
  149. package/dist/render/backend/select.js +249 -0
  150. package/dist/render/backend/timer.d.ts +50 -0
  151. package/dist/render/backend/timer.js +1 -0
  152. package/dist/render/backend/webgl2/instanced.d.ts +21 -0
  153. package/dist/render/backend/webgl2/instanced.js +42 -0
  154. package/dist/render/backend/webgl2/morphTexture.d.ts +21 -0
  155. package/dist/render/backend/webgl2/morphTexture.js +55 -0
  156. package/dist/render/backend/webgl2/renderer.d.ts +3027 -0
  157. package/dist/render/backend/webgl2/renderer.js +6041 -0
  158. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +32 -0
  159. package/dist/render/backend/webgl2/skinPaletteTexture.js +68 -0
  160. package/dist/render/backend/webgpu/boltPass.d.ts +42 -0
  161. package/dist/render/backend/webgpu/boltPass.js +160 -0
  162. package/dist/render/backend/webgpu/buffers.d.ts +159 -0
  163. package/dist/render/backend/webgpu/buffers.js +402 -0
  164. package/dist/render/backend/webgpu/causticsPass.d.ts +36 -0
  165. package/dist/render/backend/webgpu/causticsPass.js +138 -0
  166. package/dist/render/backend/webgpu/clusterBinner.d.ts +51 -0
  167. package/dist/render/backend/webgpu/clusterBinner.js +206 -0
  168. package/dist/render/backend/webgpu/depthPass.d.ts +114 -0
  169. package/dist/render/backend/webgpu/depthPass.js +228 -0
  170. package/dist/render/backend/webgpu/device.d.ts +52 -0
  171. package/dist/render/backend/webgpu/device.js +121 -0
  172. package/dist/render/backend/webgpu/drawCommand.d.ts +43 -0
  173. package/dist/render/backend/webgpu/drawCommand.js +54 -0
  174. package/dist/render/backend/webgpu/environmentTexels.d.ts +28 -0
  175. package/dist/render/backend/webgpu/environmentTexels.js +79 -0
  176. package/dist/render/backend/webgpu/filmPass.d.ts +29 -0
  177. package/dist/render/backend/webgpu/filmPass.js +97 -0
  178. package/dist/render/backend/webgpu/flatPass.d.ts +213 -0
  179. package/dist/render/backend/webgpu/flatPass.js +482 -0
  180. package/dist/render/backend/webgpu/flockPass.d.ts +34 -0
  181. package/dist/render/backend/webgpu/flockPass.js +129 -0
  182. package/dist/render/backend/webgpu/gpuTimestamps.d.ts +94 -0
  183. package/dist/render/backend/webgpu/gpuTimestamps.js +213 -0
  184. package/dist/render/backend/webgpu/insetPass.d.ts +19 -0
  185. package/dist/render/backend/webgpu/insetPass.js +108 -0
  186. package/dist/render/backend/webgpu/instanced.d.ts +27 -0
  187. package/dist/render/backend/webgpu/instanced.js +49 -0
  188. package/dist/render/backend/webgpu/lightVolumePass.d.ts +75 -0
  189. package/dist/render/backend/webgpu/lightVolumePass.js +154 -0
  190. package/dist/render/backend/webgpu/linePass.d.ts +90 -0
  191. package/dist/render/backend/webgpu/linePass.js +220 -0
  192. package/dist/render/backend/webgpu/morphTexture.d.ts +18 -0
  193. package/dist/render/backend/webgpu/morphTexture.js +51 -0
  194. package/dist/render/backend/webgpu/panelPass.d.ts +37 -0
  195. package/dist/render/backend/webgpu/panelPass.js +114 -0
  196. package/dist/render/backend/webgpu/particlePass.d.ts +89 -0
  197. package/dist/render/backend/webgpu/particlePass.js +280 -0
  198. package/dist/render/backend/webgpu/pipelineCache.d.ts +93 -0
  199. package/dist/render/backend/webgpu/pipelineCache.js +140 -0
  200. package/dist/render/backend/webgpu/plumePass.d.ts +69 -0
  201. package/dist/render/backend/webgpu/plumePass.js +213 -0
  202. package/dist/render/backend/webgpu/pointShadowPass.d.ts +88 -0
  203. package/dist/render/backend/webgpu/pointShadowPass.js +280 -0
  204. package/dist/render/backend/webgpu/postPass.d.ts +342 -0
  205. package/dist/render/backend/webgpu/postPass.js +366 -0
  206. package/dist/render/backend/webgpu/probePass.d.ts +108 -0
  207. package/dist/render/backend/webgpu/probePass.js +188 -0
  208. package/dist/render/backend/webgpu/reflectionPass.d.ts +29 -0
  209. package/dist/render/backend/webgpu/reflectionPass.js +87 -0
  210. package/dist/render/backend/webgpu/renderer.d.ts +3082 -0
  211. package/dist/render/backend/webgpu/renderer.js +10911 -0
  212. package/dist/render/backend/webgpu/scatterPass.d.ts +95 -0
  213. package/dist/render/backend/webgpu/scatterPass.js +266 -0
  214. package/dist/render/backend/webgpu/sdfTextPass.d.ts +86 -0
  215. package/dist/render/backend/webgpu/sdfTextPass.js +190 -0
  216. package/dist/render/backend/webgpu/shaderModules.d.ts +9 -0
  217. package/dist/render/backend/webgpu/shaderModules.js +45 -0
  218. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +23 -0
  219. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.js +208 -0
  220. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +67 -0
  221. package/dist/render/backend/webgpu/skinPaletteRing.js +97 -0
  222. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +17 -0
  223. package/dist/render/backend/webgpu/skinPaletteTexture.js +68 -0
  224. package/dist/render/backend/webgpu/skyPass.d.ts +102 -0
  225. package/dist/render/backend/webgpu/skyPass.js +70 -0
  226. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +41 -0
  227. package/dist/render/backend/webgpu/surfaceTexturePass.js +228 -0
  228. package/dist/render/backend/webgpu/textPass.d.ts +73 -0
  229. package/dist/render/backend/webgpu/textPass.js +190 -0
  230. package/dist/render/backend/webgpu/uniformRing.d.ts +133 -0
  231. package/dist/render/backend/webgpu/uniformRing.js +208 -0
  232. package/dist/render/backend/webgpu/waterPass.d.ts +73 -0
  233. package/dist/render/backend/webgpu/waterPass.js +160 -0
  234. package/dist/render/backend/webgpu/windStreakPass.d.ts +46 -0
  235. package/dist/render/backend/webgpu/windStreakPass.js +153 -0
  236. package/dist/render/billboard.d.ts +27 -0
  237. package/dist/render/billboard.js +79 -0
  238. package/dist/render/bloomChain.d.ts +66 -0
  239. package/dist/render/bloomChain.js +83 -0
  240. package/dist/render/bloomPass.d.ts +54 -0
  241. package/dist/render/bloomPass.js +219 -0
  242. package/dist/render/boltBatch.d.ts +39 -0
  243. package/dist/render/boltBatch.js +121 -0
  244. package/dist/render/boltPool.d.ts +135 -0
  245. package/dist/render/boltPool.js +259 -0
  246. package/dist/render/boom.d.ts +46 -0
  247. package/dist/render/boom.js +81 -0
  248. package/dist/render/camera.d.ts +119 -0
  249. package/dist/render/camera.js +304 -0
  250. package/dist/render/causticsRenderer.d.ts +63 -0
  251. package/dist/render/causticsRenderer.js +119 -0
  252. package/dist/render/cinematicCamera.d.ts +102 -0
  253. package/dist/render/cinematicCamera.js +227 -0
  254. package/dist/render/clusteredLights.d.ts +260 -0
  255. package/dist/render/clusteredLights.js +479 -0
  256. package/dist/render/colourGrade.d.ts +62 -0
  257. package/dist/render/colourGrade.js +91 -0
  258. package/dist/render/compute.d.ts +74 -0
  259. package/dist/render/compute.js +8 -0
  260. package/dist/render/contextLoss.d.ts +32 -0
  261. package/dist/render/contextLoss.js +46 -0
  262. package/dist/render/debugLines.d.ts +75 -0
  263. package/dist/render/debugLines.js +281 -0
  264. package/dist/render/decalPass.d.ts +34 -0
  265. package/dist/render/decalPass.js +103 -0
  266. package/dist/render/decalProjector.d.ts +140 -0
  267. package/dist/render/decalProjector.js +282 -0
  268. package/dist/render/decalQueue.d.ts +53 -0
  269. package/dist/render/decalQueue.js +87 -0
  270. package/dist/render/depthConvention.d.ts +314 -0
  271. package/dist/render/depthConvention.js +346 -0
  272. package/dist/render/drawingBuffer.d.ts +52 -0
  273. package/dist/render/drawingBuffer.js +67 -0
  274. package/dist/render/emptyTexture.d.ts +60 -0
  275. package/dist/render/emptyTexture.js +103 -0
  276. package/dist/render/envProbeArray.d.ts +85 -0
  277. package/dist/render/envProbeArray.js +164 -0
  278. package/dist/render/equirectToCube.d.ts +54 -0
  279. package/dist/render/equirectToCube.js +123 -0
  280. package/dist/render/flockGeometry.d.ts +23 -0
  281. package/dist/render/flockGeometry.js +43 -0
  282. package/dist/render/flockRenderer.d.ts +37 -0
  283. package/dist/render/flockRenderer.js +70 -0
  284. package/dist/render/frame/arena.d.ts +32 -0
  285. package/dist/render/frame/arena.js +65 -0
  286. package/dist/render/frame/index.d.ts +18 -0
  287. package/dist/render/frame/index.js +4 -0
  288. package/dist/render/frame/replay.d.ts +34 -0
  289. package/dist/render/frame/replay.js +48 -0
  290. package/dist/render/frame/resources.d.ts +25 -0
  291. package/dist/render/frame/resources.js +55 -0
  292. package/dist/render/frame/schedule.d.ts +37 -0
  293. package/dist/render/frame/schedule.js +61 -0
  294. package/dist/render/frameView.d.ts +19 -0
  295. package/dist/render/frameView.js +1 -0
  296. package/dist/render/gizmo.d.ts +236 -0
  297. package/dist/render/gizmo.js +727 -0
  298. package/dist/render/globalMedium.d.ts +124 -0
  299. package/dist/render/globalMedium.js +100 -0
  300. package/dist/render/globalMediumPass.d.ts +101 -0
  301. package/dist/render/globalMediumPass.js +250 -0
  302. package/dist/render/gpuCapability.d.ts +111 -0
  303. package/dist/render/gpuCapability.js +194 -0
  304. package/dist/render/gpuTimer.d.ts +87 -0
  305. package/dist/render/gpuTimer.js +145 -0
  306. package/dist/render/halfFloat.d.ts +4 -0
  307. package/dist/render/halfFloat.js +38 -0
  308. package/dist/render/handleRegistry.d.ts +54 -0
  309. package/dist/render/handleRegistry.js +84 -0
  310. package/dist/render/iesProfile.d.ts +64 -0
  311. package/dist/render/iesProfile.js +179 -0
  312. package/dist/render/imageTexels.d.ts +50 -0
  313. package/dist/render/imageTexels.js +91 -0
  314. package/dist/render/instancedMesh.d.ts +48 -0
  315. package/dist/render/instancedMesh.js +127 -0
  316. package/dist/render/instances.d.ts +69 -0
  317. package/dist/render/instances.js +52 -0
  318. package/dist/render/lightBudget.d.ts +397 -0
  319. package/dist/render/lightBudget.js +524 -0
  320. package/dist/render/lightMatrix.d.ts +3 -0
  321. package/dist/render/lightMatrix.js +45 -0
  322. package/dist/render/lightVolumeDraw.d.ts +132 -0
  323. package/dist/render/lightVolumeDraw.js +181 -0
  324. package/dist/render/lineBatch.d.ts +60 -0
  325. package/dist/render/lineBatch.js +160 -0
  326. package/dist/render/linePoints.d.ts +33 -0
  327. package/dist/render/linePoints.js +46 -0
  328. package/dist/render/livePointShadowSet.d.ts +72 -0
  329. package/dist/render/livePointShadowSet.js +258 -0
  330. package/dist/render/lod.d.ts +35 -0
  331. package/dist/render/lod.js +65 -0
  332. package/dist/render/mesh.d.ts +177 -0
  333. package/dist/render/mesh.js +466 -0
  334. package/dist/render/occlusion.d.ts +81 -0
  335. package/dist/render/occlusion.js +378 -0
  336. package/dist/render/oitPass.d.ts +67 -0
  337. package/dist/render/oitPass.js +213 -0
  338. package/dist/render/orderIndependent.d.ts +92 -0
  339. package/dist/render/orderIndependent.js +108 -0
  340. package/dist/render/particleBatch.d.ts +140 -0
  341. package/dist/render/particleBatch.js +186 -0
  342. package/dist/render/particleMaterial.d.ts +54 -0
  343. package/dist/render/particleMaterial.js +42 -0
  344. package/dist/render/particlePool.d.ts +124 -0
  345. package/dist/render/particlePool.js +182 -0
  346. package/dist/render/pass.d.ts +195 -0
  347. package/dist/render/pass.js +9 -0
  348. package/dist/render/passTarget.d.ts +86 -0
  349. package/dist/render/passTarget.js +121 -0
  350. package/dist/render/pickable.d.ts +31 -0
  351. package/dist/render/pickable.js +172 -0
  352. package/dist/render/planarReflection.d.ts +59 -0
  353. package/dist/render/planarReflection.js +194 -0
  354. package/dist/render/planarReflectionDraw.d.ts +31 -0
  355. package/dist/render/planarReflectionDraw.js +65 -0
  356. package/dist/render/plumeGeometry.d.ts +25 -0
  357. package/dist/render/plumeGeometry.js +61 -0
  358. package/dist/render/plumeMaterial.d.ts +31 -0
  359. package/dist/render/plumeMaterial.js +64 -0
  360. package/dist/render/plumeRenderer.d.ts +96 -0
  361. package/dist/render/plumeRenderer.js +185 -0
  362. package/dist/render/pointLightSelection.d.ts +175 -0
  363. package/dist/render/pointLightSelection.js +301 -0
  364. package/dist/render/pointShadowArray.d.ts +95 -0
  365. package/dist/render/pointShadowArray.js +219 -0
  366. package/dist/render/pointShadowBudget.d.ts +118 -0
  367. package/dist/render/pointShadowBudget.js +221 -0
  368. package/dist/render/pointShadowImage.d.ts +265 -0
  369. package/dist/render/pointShadowImage.js +339 -0
  370. package/dist/render/pointShadowMap.d.ts +93 -0
  371. package/dist/render/pointShadowMap.js +167 -0
  372. package/dist/render/pointShadowSystem.d.ts +195 -0
  373. package/dist/render/pointShadowSystem.js +336 -0
  374. package/dist/render/prefilterEnvMap.d.ts +124 -0
  375. package/dist/render/prefilterEnvMap.js +146 -0
  376. package/dist/render/prefilterPass.d.ts +44 -0
  377. package/dist/render/prefilterPass.js +98 -0
  378. package/dist/render/probeGrid.d.ts +122 -0
  379. package/dist/render/probeGrid.js +174 -0
  380. package/dist/render/rainField.d.ts +99 -0
  381. package/dist/render/rainField.js +168 -0
  382. package/dist/render/reflectionProbe.d.ts +188 -0
  383. package/dist/render/reflectionProbe.js +437 -0
  384. package/dist/render/renderQuality.d.ts +699 -0
  385. package/dist/render/renderQuality.js +293 -0
  386. package/dist/render/rendererHarness.d.ts +39 -0
  387. package/dist/render/rendererHarness.js +185 -0
  388. package/dist/render/resolutionGovernor.d.ts +107 -0
  389. package/dist/render/resolutionGovernor.js +144 -0
  390. package/dist/render/scatterDeform.d.ts +17 -0
  391. package/dist/render/scatterDeform.js +46 -0
  392. package/dist/render/sceneTarget.d.ts +288 -0
  393. package/dist/render/sceneTarget.js +856 -0
  394. package/dist/render/screenSpaceReflection.d.ts +233 -0
  395. package/dist/render/screenSpaceReflection.js +415 -0
  396. package/dist/render/sdfFont.d.ts +77 -0
  397. package/dist/render/sdfFont.js +86 -0
  398. package/dist/render/sdfTextLayout.d.ts +46 -0
  399. package/dist/render/sdfTextLayout.js +145 -0
  400. package/dist/render/sdfTextRenderer.d.ts +75 -0
  401. package/dist/render/sdfTextRenderer.js +194 -0
  402. package/dist/render/seaState.d.ts +25 -0
  403. package/dist/render/seaState.js +34 -0
  404. package/dist/render/segmentQuads.d.ts +29 -0
  405. package/dist/render/segmentQuads.js +75 -0
  406. package/dist/render/shader.d.ts +5 -0
  407. package/dist/render/shader.js +172 -0
  408. package/dist/render/shaders/ambientOcclusion.d.ts +45 -0
  409. package/dist/render/shaders/ambientOcclusion.js +352 -0
  410. package/dist/render/shaders/arcane.d.ts +19 -0
  411. package/dist/render/shaders/arcane.js +102 -0
  412. package/dist/render/shaders/bloom.d.ts +70 -0
  413. package/dist/render/shaders/bloom.js +227 -0
  414. package/dist/render/shaders/bolt.d.ts +17 -0
  415. package/dist/render/shaders/bolt.js +161 -0
  416. package/dist/render/shaders/caustics.d.ts +38 -0
  417. package/dist/render/shaders/caustics.js +167 -0
  418. package/dist/render/shaders/conditionals.d.ts +21 -0
  419. package/dist/render/shaders/conditionals.js +55 -0
  420. package/dist/render/shaders/decalProject.d.ts +25 -0
  421. package/dist/render/shaders/decalProject.js +125 -0
  422. package/dist/render/shaders/depth.d.ts +14 -0
  423. package/dist/render/shaders/depth.js +114 -0
  424. package/dist/render/shaders/depthOfField.d.ts +41 -0
  425. package/dist/render/shaders/depthOfField.js +139 -0
  426. package/dist/render/shaders/film.d.ts +15 -0
  427. package/dist/render/shaders/film.js +283 -0
  428. package/dist/render/shaders/fire.d.ts +12 -0
  429. package/dist/render/shaders/fire.js +178 -0
  430. package/dist/render/shaders/flat/directionalShadow.d.ts +1 -0
  431. package/dist/render/shaders/flat/directionalShadow.js +195 -0
  432. package/dist/render/shaders/flat/index.d.ts +135 -0
  433. package/dist/render/shaders/flat/index.js +387 -0
  434. package/dist/render/shaders/flat/lobes.d.ts +1 -0
  435. package/dist/render/shaders/flat/lobes.js +398 -0
  436. package/dist/render/shaders/flat/main.d.ts +2 -0
  437. package/dist/render/shaders/flat/main.js +1630 -0
  438. package/dist/render/shaders/flat/pointShadow.d.ts +1 -0
  439. package/dist/render/shaders/flat/pointShadow.js +322 -0
  440. package/dist/render/shaders/flat/preamble.d.ts +15 -0
  441. package/dist/render/shaders/flat/preamble.js +864 -0
  442. package/dist/render/shaders/flat/probeGrid.d.ts +21 -0
  443. package/dist/render/shaders/flat/probeGrid.js +151 -0
  444. package/dist/render/shaders/flat/surface.d.ts +2 -0
  445. package/dist/render/shaders/flat/surface.js +40 -0
  446. package/dist/render/shaders/flock.d.ts +11 -0
  447. package/dist/render/shaders/flock.js +102 -0
  448. package/dist/render/shaders/fog.d.ts +37 -0
  449. package/dist/render/shaders/fog.js +98 -0
  450. package/dist/render/shaders/fullscreen.d.ts +16 -0
  451. package/dist/render/shaders/fullscreen.js +27 -0
  452. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +65 -0
  453. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +71 -0
  454. package/dist/render/shaders/generated/arcane.wgsl.d.ts +90 -0
  455. package/dist/render/shaders/generated/arcane.wgsl.js +96 -0
  456. package/dist/render/shaders/generated/bloom.wgsl.d.ts +69 -0
  457. package/dist/render/shaders/generated/bloom.wgsl.js +75 -0
  458. package/dist/render/shaders/generated/bolt.wgsl.d.ts +118 -0
  459. package/dist/render/shaders/generated/bolt.wgsl.js +124 -0
  460. package/dist/render/shaders/generated/caustics.wgsl.d.ts +118 -0
  461. package/dist/render/shaders/generated/caustics.wgsl.js +124 -0
  462. package/dist/render/shaders/generated/decalProject.wgsl.d.ts +61 -0
  463. package/dist/render/shaders/generated/decalProject.wgsl.js +67 -0
  464. package/dist/render/shaders/generated/depth.wgsl.d.ts +131 -0
  465. package/dist/render/shaders/generated/depth.wgsl.js +137 -0
  466. package/dist/render/shaders/generated/film.wgsl.d.ts +134 -0
  467. package/dist/render/shaders/generated/film.wgsl.js +140 -0
  468. package/dist/render/shaders/generated/fire.wgsl.d.ts +128 -0
  469. package/dist/render/shaders/generated/fire.wgsl.js +134 -0
  470. package/dist/render/shaders/generated/flat.wgsl.d.ts +9163 -0
  471. package/dist/render/shaders/generated/flat.wgsl.js +9192 -0
  472. package/dist/render/shaders/generated/flock.wgsl.d.ts +73 -0
  473. package/dist/render/shaders/generated/flock.wgsl.js +79 -0
  474. package/dist/render/shaders/generated/fullscreen.wgsl.d.ts +12 -0
  475. package/dist/render/shaders/generated/fullscreen.wgsl.js +18 -0
  476. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +130 -0
  477. package/dist/render/shaders/generated/globalMedium.wgsl.js +136 -0
  478. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +230 -0
  479. package/dist/render/shaders/generated/lightVolume.wgsl.js +239 -0
  480. package/dist/render/shaders/generated/line.wgsl.d.ts +128 -0
  481. package/dist/render/shaders/generated/line.wgsl.js +134 -0
  482. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +51 -0
  483. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +57 -0
  484. package/dist/render/shaders/generated/oitResolve.wgsl.d.ts +23 -0
  485. package/dist/render/shaders/generated/oitResolve.wgsl.js +29 -0
  486. package/dist/render/shaders/generated/panel.wgsl.d.ts +48 -0
  487. package/dist/render/shaders/generated/panel.wgsl.js +54 -0
  488. package/dist/render/shaders/generated/particle.wgsl.d.ts +332 -0
  489. package/dist/render/shaders/generated/particle.wgsl.js +338 -0
  490. package/dist/render/shaders/generated/prefilter.wgsl.d.ts +61 -0
  491. package/dist/render/shaders/generated/prefilter.wgsl.js +67 -0
  492. package/dist/render/shaders/generated/rush.wgsl.d.ts +131 -0
  493. package/dist/render/shaders/generated/rush.wgsl.js +137 -0
  494. package/dist/render/shaders/generated/scatter.wgsl.d.ts +195 -0
  495. package/dist/render/shaders/generated/scatter.wgsl.js +201 -0
  496. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +69 -0
  497. package/dist/render/shaders/generated/sdfText.wgsl.js +75 -0
  498. package/dist/render/shaders/generated/sky.wgsl.d.ts +100 -0
  499. package/dist/render/shaders/generated/sky.wgsl.js +106 -0
  500. package/dist/render/shaders/generated/smoke.wgsl.d.ts +85 -0
  501. package/dist/render/shaders/generated/smoke.wgsl.js +91 -0
  502. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +18 -0
  503. package/dist/render/shaders/generated/ssrResolve.wgsl.js +24 -0
  504. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +86 -0
  505. package/dist/render/shaders/generated/ssrTrace.wgsl.js +92 -0
  506. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +46 -0
  507. package/dist/render/shaders/generated/temporalResolve.wgsl.js +52 -0
  508. package/dist/render/shaders/generated/text.wgsl.d.ts +98 -0
  509. package/dist/render/shaders/generated/text.wgsl.js +104 -0
  510. package/dist/render/shaders/generated/water.wgsl.d.ts +247 -0
  511. package/dist/render/shaders/generated/water.wgsl.js +253 -0
  512. package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +73 -0
  513. package/dist/render/shaders/generated/windStreaks.wgsl.js +79 -0
  514. package/dist/render/shaders/gerstner.d.ts +21 -0
  515. package/dist/render/shaders/gerstner.js +110 -0
  516. package/dist/render/shaders/globalMedium.d.ts +56 -0
  517. package/dist/render/shaders/globalMedium.js +393 -0
  518. package/dist/render/shaders/lightVolume.d.ts +61 -0
  519. package/dist/render/shaders/lightVolume.js +527 -0
  520. package/dist/render/shaders/line.d.ts +18 -0
  521. package/dist/render/shaders/line.js +122 -0
  522. package/dist/render/shaders/morph.d.ts +26 -0
  523. package/dist/render/shaders/morph.js +61 -0
  524. package/dist/render/shaders/octahedral.d.ts +76 -0
  525. package/dist/render/shaders/octahedral.js +161 -0
  526. package/dist/render/shaders/octahedralResolve.d.ts +25 -0
  527. package/dist/render/shaders/octahedralResolve.js +81 -0
  528. package/dist/render/shaders/oitResolve.d.ts +17 -0
  529. package/dist/render/shaders/oitResolve.js +42 -0
  530. package/dist/render/shaders/outputTransform.d.ts +37 -0
  531. package/dist/render/shaders/outputTransform.js +106 -0
  532. package/dist/render/shaders/panel.d.ts +17 -0
  533. package/dist/render/shaders/panel.js +50 -0
  534. package/dist/render/shaders/particle.d.ts +82 -0
  535. package/dist/render/shaders/particle.js +431 -0
  536. package/dist/render/shaders/prefilter.d.ts +37 -0
  537. package/dist/render/shaders/prefilter.js +276 -0
  538. package/dist/render/shaders/rush.d.ts +21 -0
  539. package/dist/render/shaders/rush.js +355 -0
  540. package/dist/render/shaders/scatter.d.ts +14 -0
  541. package/dist/render/shaders/scatter.js +206 -0
  542. package/dist/render/shaders/sdfText.d.ts +9 -0
  543. package/dist/render/shaders/sdfText.js +65 -0
  544. package/dist/render/shaders/skinning.d.ts +17 -0
  545. package/dist/render/shaders/skinning.js +59 -0
  546. package/dist/render/shaders/sky.d.ts +10 -0
  547. package/dist/render/shaders/sky.js +242 -0
  548. package/dist/render/shaders/smoke.d.ts +8 -0
  549. package/dist/render/shaders/smoke.js +88 -0
  550. package/dist/render/shaders/ssrResolve.d.ts +15 -0
  551. package/dist/render/shaders/ssrResolve.js +28 -0
  552. package/dist/render/shaders/ssrTrace.d.ts +22 -0
  553. package/dist/render/shaders/ssrTrace.js +230 -0
  554. package/dist/render/shaders/tangentFrame.d.ts +17 -0
  555. package/dist/render/shaders/tangentFrame.js +70 -0
  556. package/dist/render/shaders/temporalResolve.d.ts +1 -0
  557. package/dist/render/shaders/temporalResolve.js +150 -0
  558. package/dist/render/shaders/text.d.ts +17 -0
  559. package/dist/render/shaders/text.js +155 -0
  560. package/dist/render/shaders/vertexChannel.d.ts +39 -0
  561. package/dist/render/shaders/vertexChannel.js +69 -0
  562. package/dist/render/shaders/water.d.ts +18 -0
  563. package/dist/render/shaders/water.js +354 -0
  564. package/dist/render/shaders/windStreaks.d.ts +14 -0
  565. package/dist/render/shaders/windStreaks.js +129 -0
  566. package/dist/render/shadowCasters.d.ts +107 -0
  567. package/dist/render/shadowCasters.js +1 -0
  568. package/dist/render/shadowMap.d.ts +10 -0
  569. package/dist/render/shadowMap.js +80 -0
  570. package/dist/render/shotPlacement.d.ts +80 -0
  571. package/dist/render/shotPlacement.js +183 -0
  572. package/dist/render/skinPalette.d.ts +90 -0
  573. package/dist/render/skinPalette.js +136 -0
  574. package/dist/render/ssrPass.d.ts +59 -0
  575. package/dist/render/ssrPass.js +196 -0
  576. package/dist/render/surfaceSheet.d.ts +52 -0
  577. package/dist/render/surfaceSheet.js +99 -0
  578. package/dist/render/surfaceTexture.d.ts +197 -0
  579. package/dist/render/surfaceTexture.js +116 -0
  580. package/dist/render/temporalAa.d.ts +104 -0
  581. package/dist/render/temporalAa.js +194 -0
  582. package/dist/render/temporalPass.d.ts +54 -0
  583. package/dist/render/temporalPass.js +135 -0
  584. package/dist/render/textLayout.d.ts +88 -0
  585. package/dist/render/textLayout.js +201 -0
  586. package/dist/render/textRenderer.d.ts +32 -0
  587. package/dist/render/textRenderer.js +176 -0
  588. package/dist/render/thirdPersonCamera.d.ts +272 -0
  589. package/dist/render/thirdPersonCamera.js +479 -0
  590. package/dist/render/trample.d.ts +57 -0
  591. package/dist/render/trample.js +134 -0
  592. package/dist/render/translucentQueue.d.ts +76 -0
  593. package/dist/render/translucentQueue.js +90 -0
  594. package/dist/render/uniformVectorBudget.d.ts +105 -0
  595. package/dist/render/uniformVectorBudget.js +201 -0
  596. package/dist/render/uploadStep.d.ts +14 -0
  597. package/dist/render/uploadStep.js +14 -0
  598. package/dist/render/vertexDefaults.d.ts +48 -0
  599. package/dist/render/vertexDefaults.js +111 -0
  600. package/dist/render/visibility.d.ts +22 -0
  601. package/dist/render/visibility.js +47 -0
  602. package/dist/render/waterDraw.d.ts +57 -0
  603. package/dist/render/waterDraw.js +102 -0
  604. package/dist/render/waterGrid.d.ts +62 -0
  605. package/dist/render/waterGrid.js +151 -0
  606. package/dist/render/waterRenderer.d.ts +163 -0
  607. package/dist/render/waterRenderer.js +165 -0
  608. package/dist/render/wind.d.ts +45 -0
  609. package/dist/render/wind.js +32 -0
  610. package/dist/render/windField.d.ts +34 -0
  611. package/dist/render/windField.js +13 -0
  612. package/dist/render/windStreakDraw.d.ts +73 -0
  613. package/dist/render/windStreakDraw.js +86 -0
  614. package/dist/render/windStreakRenderer.d.ts +37 -0
  615. package/dist/render/windStreakRenderer.js +86 -0
  616. package/dist/scene/node.d.ts +128 -0
  617. package/dist/scene/node.js +319 -0
  618. package/dist/scene/visit.d.ts +38 -0
  619. package/dist/scene/visit.js +43 -0
  620. package/dist/ui/clipMime.d.ts +46 -0
  621. package/dist/ui/clipMime.js +88 -0
  622. package/dist/ui/exportTarget.d.ts +305 -0
  623. package/dist/ui/exportTarget.js +368 -0
  624. package/dist/ui/frameOverlay.d.ts +59 -0
  625. package/dist/ui/frameOverlay.js +76 -0
  626. package/dist/ui/framePacer.d.ts +142 -0
  627. package/dist/ui/framePacer.js +284 -0
  628. package/dist/ui/frameRecorder.d.ts +188 -0
  629. package/dist/ui/frameRecorder.js +221 -0
  630. package/dist/ui/fullscreen.d.ts +72 -0
  631. package/dist/ui/fullscreen.js +92 -0
  632. package/dist/ui/pixelCursor.d.ts +28 -0
  633. package/dist/ui/pixelCursor.js +66 -0
  634. package/dist/ui/splash.d.ts +86 -0
  635. package/dist/ui/splash.js +300 -0
  636. package/dist/ui/stillFrame.d.ts +22 -0
  637. package/dist/ui/stillFrame.js +40 -0
  638. package/package.json +60 -0
  639. package/src/behavior/behaviorTree.ts +346 -0
  640. package/src/build/shaderComments.ts +0 -0
  641. package/src/cinematic/player.ts +209 -0
  642. package/src/cinematic/script.ts +231 -0
  643. package/src/core/bootGate.ts +49 -0
  644. package/src/core/loadTracker.ts +144 -0
  645. package/src/core/loop.ts +211 -0
  646. package/src/core/messageQueue.ts +145 -0
  647. package/src/core/onceSet.ts +65 -0
  648. package/src/core/preferences.ts +177 -0
  649. package/src/core/remoteSave.ts +347 -0
  650. package/src/core/rng.ts +101 -0
  651. package/src/core/stepBudget.ts +136 -0
  652. package/src/core/storage.ts +114 -0
  653. package/src/core/tickTrace.ts +129 -0
  654. package/src/core/timeCurve.ts +175 -0
  655. package/src/core/withDefaults.ts +28 -0
  656. package/src/dev/contactProbe.ts +115 -0
  657. package/src/dev/fpsMeter.ts +101 -0
  658. package/src/environment/celestialClock.ts +385 -0
  659. package/src/geometry/decal.ts +260 -0
  660. package/src/geometry/filmPatch.ts +158 -0
  661. package/src/geometry/growable.ts +165 -0
  662. package/src/geometry/lightVolume.ts +161 -0
  663. package/src/geometry/meshBuilder.ts +1397 -0
  664. package/src/geometry/pixelFont.ts +189 -0
  665. package/src/geometry/ribbon.ts +790 -0
  666. package/src/geometry/shellFill.ts +265 -0
  667. package/src/geometry/spline.ts +659 -0
  668. package/src/geometry/tangents.ts +173 -0
  669. package/src/geometry/textMesh.ts +96 -0
  670. package/src/geometry/treeBuilder.ts +114 -0
  671. package/src/host/display.ts +180 -0
  672. package/src/host/files.ts +77 -0
  673. package/src/host/lifecycle.ts +63 -0
  674. package/src/host/platformServices.ts +23 -0
  675. package/src/host/screen.ts +134 -0
  676. package/src/index.ts +730 -0
  677. package/src/input/actionMap.ts +377 -0
  678. package/src/input/activeDevice.ts +86 -0
  679. package/src/input/gamepadIdentity.ts +131 -0
  680. package/src/input/gamepadMapping.ts +121 -0
  681. package/src/input/input.ts +872 -0
  682. package/src/input/inputHarness.ts +226 -0
  683. package/src/input/rightGesture.ts +76 -0
  684. package/src/input/rumble.ts +138 -0
  685. package/src/input/touchControls.ts +451 -0
  686. package/src/input/typingTarget.ts +26 -0
  687. package/src/math/bounds.ts +153 -0
  688. package/src/math/color.ts +37 -0
  689. package/src/math/exact.ts +274 -0
  690. package/src/math/frustum.ts +115 -0
  691. package/src/math/intersect.ts +228 -0
  692. package/src/math/scalar.ts +109 -0
  693. package/src/math/spring.ts +106 -0
  694. package/src/nav/navGraph.ts +168 -0
  695. package/src/nav/navPath.ts +217 -0
  696. package/src/nav/navSearch.ts +200 -0
  697. package/src/physics/boxSurface.ts +96 -0
  698. package/src/physics/colliderSurface.ts +213 -0
  699. package/src/physics/compositeSurface.ts +111 -0
  700. package/src/physics/heightSurface.ts +234 -0
  701. package/src/physics/ribbonSurface.ts +642 -0
  702. package/src/render/ambientOcclusionPass.ts +200 -0
  703. package/src/render/areaLights.ts +194 -0
  704. package/src/render/areaShadowSet.ts +405 -0
  705. package/src/render/atmosphere.ts +186 -0
  706. package/src/render/backend/api.ts +802 -0
  707. package/src/render/backend/budget.ts +139 -0
  708. package/src/render/backend/createRenderer.ts +343 -0
  709. package/src/render/backend/deadline.ts +37 -0
  710. package/src/render/backend/probe.ts +152 -0
  711. package/src/render/backend/select.ts +288 -0
  712. package/src/render/backend/timer.ts +59 -0
  713. package/src/render/backend/webgl2/instanced.ts +46 -0
  714. package/src/render/backend/webgl2/morphTexture.ts +58 -0
  715. package/src/render/backend/webgl2/renderer.ts +7484 -0
  716. package/src/render/backend/webgl2/skinPaletteTexture.ts +70 -0
  717. package/src/render/backend/webgpu/boltPass.ts +205 -0
  718. package/src/render/backend/webgpu/buffers.ts +553 -0
  719. package/src/render/backend/webgpu/causticsPass.ts +174 -0
  720. package/src/render/backend/webgpu/clusterBinner.ts +259 -0
  721. package/src/render/backend/webgpu/depthPass.ts +261 -0
  722. package/src/render/backend/webgpu/device.ts +148 -0
  723. package/src/render/backend/webgpu/drawCommand.ts +79 -0
  724. package/src/render/backend/webgpu/environmentTexels.ts +96 -0
  725. package/src/render/backend/webgpu/filmPass.ts +123 -0
  726. package/src/render/backend/webgpu/flatPass.ts +635 -0
  727. package/src/render/backend/webgpu/flockPass.ts +163 -0
  728. package/src/render/backend/webgpu/gpuTimestamps.ts +255 -0
  729. package/src/render/backend/webgpu/insetPass.ts +123 -0
  730. package/src/render/backend/webgpu/instanced.ts +55 -0
  731. package/src/render/backend/webgpu/lightVolumePass.ts +203 -0
  732. package/src/render/backend/webgpu/linePass.ts +270 -0
  733. package/src/render/backend/webgpu/morphTexture.ts +61 -0
  734. package/src/render/backend/webgpu/panelPass.ts +138 -0
  735. package/src/render/backend/webgpu/particlePass.ts +370 -0
  736. package/src/render/backend/webgpu/pipelineCache.ts +152 -0
  737. package/src/render/backend/webgpu/plumePass.ts +292 -0
  738. package/src/render/backend/webgpu/pointShadowPass.ts +368 -0
  739. package/src/render/backend/webgpu/postPass.ts +470 -0
  740. package/src/render/backend/webgpu/probePass.ts +218 -0
  741. package/src/render/backend/webgpu/reflectionPass.ts +137 -0
  742. package/src/render/backend/webgpu/renderer.ts +13004 -0
  743. package/src/render/backend/webgpu/scatterPass.ts +354 -0
  744. package/src/render/backend/webgpu/sdfTextPass.ts +220 -0
  745. package/src/render/backend/webgpu/shaderModules.ts +49 -0
  746. package/src/render/backend/webgpu/shaders/clusterBin.wgsl.ts +219 -0
  747. package/src/render/backend/webgpu/skinPaletteRing.ts +105 -0
  748. package/src/render/backend/webgpu/skinPaletteTexture.ts +79 -0
  749. package/src/render/backend/webgpu/skyPass.ts +85 -0
  750. package/src/render/backend/webgpu/surfaceTexturePass.ts +257 -0
  751. package/src/render/backend/webgpu/textPass.ts +224 -0
  752. package/src/render/backend/webgpu/uniformRing.ts +228 -0
  753. package/src/render/backend/webgpu/waterPass.ts +211 -0
  754. package/src/render/backend/webgpu/windStreakPass.ts +196 -0
  755. package/src/render/billboard.ts +108 -0
  756. package/src/render/bloomChain.ts +101 -0
  757. package/src/render/bloomPass.ts +256 -0
  758. package/src/render/boltBatch.ts +141 -0
  759. package/src/render/boltPool.ts +360 -0
  760. package/src/render/boom.ts +123 -0
  761. package/src/render/camera.ts +348 -0
  762. package/src/render/causticsRenderer.ts +171 -0
  763. package/src/render/cinematicCamera.ts +278 -0
  764. package/src/render/clusteredLights.ts +598 -0
  765. package/src/render/colourGrade.ts +109 -0
  766. package/src/render/compute.ts +83 -0
  767. package/src/render/contextLoss.ts +50 -0
  768. package/src/render/debugLines.ts +411 -0
  769. package/src/render/decalPass.ts +124 -0
  770. package/src/render/decalProjector.ts +390 -0
  771. package/src/render/decalQueue.ts +110 -0
  772. package/src/render/depthConvention.ts +383 -0
  773. package/src/render/drawingBuffer.ts +89 -0
  774. package/src/render/emptyTexture.ts +130 -0
  775. package/src/render/envProbeArray.ts +201 -0
  776. package/src/render/equirectToCube.ts +160 -0
  777. package/src/render/flockGeometry.ts +53 -0
  778. package/src/render/flockRenderer.ts +111 -0
  779. package/src/render/frame/arena.ts +93 -0
  780. package/src/render/frame/index.ts +29 -0
  781. package/src/render/frame/replay.ts +50 -0
  782. package/src/render/frame/resources.ts +61 -0
  783. package/src/render/frame/schedule.ts +85 -0
  784. package/src/render/frameView.ts +20 -0
  785. package/src/render/gizmo.ts +923 -0
  786. package/src/render/globalMedium.ts +154 -0
  787. package/src/render/globalMediumPass.ts +323 -0
  788. package/src/render/gpuCapability.ts +244 -0
  789. package/src/render/gpuTimer.ts +182 -0
  790. package/src/render/halfFloat.ts +39 -0
  791. package/src/render/handleRegistry.ts +96 -0
  792. package/src/render/iesProfile.ts +215 -0
  793. package/src/render/imageTexels.ts +109 -0
  794. package/src/render/instancedMesh.ts +187 -0
  795. package/src/render/instances.ts +92 -0
  796. package/src/render/lightBudget.ts +655 -0
  797. package/src/render/lightMatrix.ts +69 -0
  798. package/src/render/lightVolumeDraw.ts +216 -0
  799. package/src/render/lineBatch.ts +180 -0
  800. package/src/render/linePoints.ts +61 -0
  801. package/src/render/livePointShadowSet.ts +285 -0
  802. package/src/render/lod.ts +86 -0
  803. package/src/render/mesh.ts +536 -0
  804. package/src/render/occlusion.ts +406 -0
  805. package/src/render/oitPass.ts +230 -0
  806. package/src/render/orderIndependent.ts +136 -0
  807. package/src/render/particleBatch.ts +299 -0
  808. package/src/render/particleMaterial.ts +84 -0
  809. package/src/render/particlePool.ts +291 -0
  810. package/src/render/pass.ts +203 -0
  811. package/src/render/passTarget.ts +225 -0
  812. package/src/render/pickable.ts +222 -0
  813. package/src/render/planarReflection.ts +233 -0
  814. package/src/render/planarReflectionDraw.ts +87 -0
  815. package/src/render/plumeGeometry.ts +75 -0
  816. package/src/render/plumeMaterial.ts +99 -0
  817. package/src/render/plumeRenderer.ts +272 -0
  818. package/src/render/pointLightSelection.ts +447 -0
  819. package/src/render/pointShadowArray.ts +276 -0
  820. package/src/render/pointShadowBudget.ts +305 -0
  821. package/src/render/pointShadowImage.ts +470 -0
  822. package/src/render/pointShadowMap.ts +228 -0
  823. package/src/render/pointShadowSystem.ts +408 -0
  824. package/src/render/prefilterEnvMap.ts +162 -0
  825. package/src/render/prefilterPass.ts +115 -0
  826. package/src/render/probeGrid.ts +248 -0
  827. package/src/render/rainField.ts +240 -0
  828. package/src/render/reflectionProbe.ts +530 -0
  829. package/src/render/renderQuality.ts +1054 -0
  830. package/src/render/rendererHarness.ts +214 -0
  831. package/src/render/resolutionGovernor.ts +189 -0
  832. package/src/render/scatterDeform.ts +72 -0
  833. package/src/render/sceneTarget.ts +1032 -0
  834. package/src/render/screenSpaceReflection.ts +524 -0
  835. package/src/render/sdfFont.ts +157 -0
  836. package/src/render/sdfTextLayout.ts +206 -0
  837. package/src/render/sdfTextRenderer.ts +233 -0
  838. package/src/render/seaState.ts +55 -0
  839. package/src/render/segmentQuads.ts +92 -0
  840. package/src/render/shader.ts +190 -0
  841. package/src/render/shaders/ambientOcclusion.ts +354 -0
  842. package/src/render/shaders/arcane.ts +103 -0
  843. package/src/render/shaders/bloom.ts +232 -0
  844. package/src/render/shaders/bolt.ts +164 -0
  845. package/src/render/shaders/caustics.ts +169 -0
  846. package/src/render/shaders/conditionals.ts +58 -0
  847. package/src/render/shaders/decalProject.ts +127 -0
  848. package/src/render/shaders/depth.ts +131 -0
  849. package/src/render/shaders/depthOfField.ts +141 -0
  850. package/src/render/shaders/film.ts +285 -0
  851. package/src/render/shaders/fire.ts +180 -0
  852. package/src/render/shaders/flat/directionalShadow.ts +196 -0
  853. package/src/render/shaders/flat/index.ts +502 -0
  854. package/src/render/shaders/flat/lobes.ts +399 -0
  855. package/src/render/shaders/flat/main.ts +1631 -0
  856. package/src/render/shaders/flat/pointShadow.ts +323 -0
  857. package/src/render/shaders/flat/preamble.ts +875 -0
  858. package/src/render/shaders/flat/probeGrid.ts +152 -0
  859. package/src/render/shaders/flat/surface.ts +41 -0
  860. package/src/render/shaders/flock.ts +103 -0
  861. package/src/render/shaders/fog.ts +99 -0
  862. package/src/render/shaders/fullscreen.ts +28 -0
  863. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +74 -0
  864. package/src/render/shaders/generated/arcane.wgsl.ts +98 -0
  865. package/src/render/shaders/generated/bloom.wgsl.ts +79 -0
  866. package/src/render/shaders/generated/bolt.wgsl.ts +127 -0
  867. package/src/render/shaders/generated/caustics.wgsl.ts +127 -0
  868. package/src/render/shaders/generated/decalProject.wgsl.ts +69 -0
  869. package/src/render/shaders/generated/depth.wgsl.ts +142 -0
  870. package/src/render/shaders/generated/film.wgsl.ts +143 -0
  871. package/src/render/shaders/generated/fire.wgsl.ts +137 -0
  872. package/src/render/shaders/generated/flat.wgsl.ts +9195 -0
  873. package/src/render/shaders/generated/flock.wgsl.ts +82 -0
  874. package/src/render/shaders/generated/fullscreen.wgsl.ts +20 -0
  875. package/src/render/shaders/generated/globalMedium.wgsl.ts +139 -0
  876. package/src/render/shaders/generated/lightVolume.wgsl.ts +242 -0
  877. package/src/render/shaders/generated/line.wgsl.ts +137 -0
  878. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +60 -0
  879. package/src/render/shaders/generated/oitResolve.wgsl.ts +31 -0
  880. package/src/render/shaders/generated/panel.wgsl.ts +57 -0
  881. package/src/render/shaders/generated/particle.wgsl.ts +343 -0
  882. package/src/render/shaders/generated/prefilter.wgsl.ts +69 -0
  883. package/src/render/shaders/generated/rush.wgsl.ts +139 -0
  884. package/src/render/shaders/generated/scatter.wgsl.ts +205 -0
  885. package/src/render/shaders/generated/sdfText.wgsl.ts +78 -0
  886. package/src/render/shaders/generated/sky.wgsl.ts +109 -0
  887. package/src/render/shaders/generated/smoke.wgsl.ts +93 -0
  888. package/src/render/shaders/generated/ssrResolve.wgsl.ts +26 -0
  889. package/src/render/shaders/generated/ssrTrace.wgsl.ts +94 -0
  890. package/src/render/shaders/generated/temporalResolve.wgsl.ts +54 -0
  891. package/src/render/shaders/generated/text.wgsl.ts +107 -0
  892. package/src/render/shaders/generated/water.wgsl.ts +256 -0
  893. package/src/render/shaders/generated/windStreaks.wgsl.ts +82 -0
  894. package/src/render/shaders/gerstner.ts +110 -0
  895. package/src/render/shaders/globalMedium.ts +396 -0
  896. package/src/render/shaders/lightVolume.ts +546 -0
  897. package/src/render/shaders/line.ts +125 -0
  898. package/src/render/shaders/morph.ts +62 -0
  899. package/src/render/shaders/octahedral.ts +170 -0
  900. package/src/render/shaders/octahedralResolve.ts +83 -0
  901. package/src/render/shaders/oitResolve.ts +42 -0
  902. package/src/render/shaders/outputTransform.ts +107 -0
  903. package/src/render/shaders/panel.ts +51 -0
  904. package/src/render/shaders/particle.ts +438 -0
  905. package/src/render/shaders/prefilter.ts +278 -0
  906. package/src/render/shaders/rush.ts +357 -0
  907. package/src/render/shaders/scatter.ts +211 -0
  908. package/src/render/shaders/sdfText.ts +68 -0
  909. package/src/render/shaders/skinning.ts +59 -0
  910. package/src/render/shaders/sky.ts +243 -0
  911. package/src/render/shaders/smoke.ts +89 -0
  912. package/src/render/shaders/ssrResolve.ts +28 -0
  913. package/src/render/shaders/ssrTrace.ts +233 -0
  914. package/src/render/shaders/tangentFrame.ts +70 -0
  915. package/src/render/shaders/temporalResolve.ts +151 -0
  916. package/src/render/shaders/text.ts +156 -0
  917. package/src/render/shaders/vertexChannel.ts +71 -0
  918. package/src/render/shaders/water.ts +356 -0
  919. package/src/render/shaders/windStreaks.ts +130 -0
  920. package/src/render/shadowCasters.ts +124 -0
  921. package/src/render/shadowMap.ts +89 -0
  922. package/src/render/shotPlacement.ts +266 -0
  923. package/src/render/skinPalette.ts +157 -0
  924. package/src/render/ssrPass.ts +241 -0
  925. package/src/render/surfaceSheet.ts +158 -0
  926. package/src/render/surfaceTexture.ts +310 -0
  927. package/src/render/temporalAa.ts +221 -0
  928. package/src/render/temporalPass.ts +153 -0
  929. package/src/render/textLayout.ts +239 -0
  930. package/src/render/textRenderer.ts +201 -0
  931. package/src/render/thirdPersonCamera.ts +578 -0
  932. package/src/render/trample.ts +140 -0
  933. package/src/render/translucentQueue.ts +161 -0
  934. package/src/render/uniformVectorBudget.ts +246 -0
  935. package/src/render/uploadStep.ts +14 -0
  936. package/src/render/vertexDefaults.ts +115 -0
  937. package/src/render/visibility.ts +56 -0
  938. package/src/render/waterDraw.ts +167 -0
  939. package/src/render/waterGrid.ts +180 -0
  940. package/src/render/waterRenderer.ts +352 -0
  941. package/src/render/wind.ts +79 -0
  942. package/src/render/windField.ts +50 -0
  943. package/src/render/windStreakDraw.ts +131 -0
  944. package/src/render/windStreakRenderer.ts +130 -0
  945. package/src/scene/node.ts +336 -0
  946. package/src/scene/visit.ts +82 -0
  947. package/src/ui/clipMime.ts +109 -0
  948. package/src/ui/exportTarget.ts +491 -0
  949. package/src/ui/frameOverlay.ts +159 -0
  950. package/src/ui/framePacer.ts +351 -0
  951. package/src/ui/frameRecorder.ts +333 -0
  952. package/src/ui/fullscreen.ts +113 -0
  953. package/src/ui/pixelCursor.ts +99 -0
  954. package/src/ui/splash.ts +347 -0
  955. package/src/ui/stillFrame.ts +44 -0
@@ -0,0 +1,1630 @@
1
+ /** The fragment entry point, which assembles everything above into a colour. */
2
+ export const MAIN_GLSL = `void main() {
3
+ if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;
4
+
5
+ /*
6
+ * Surface colour and its coverage, resolved before anything else — including before it is
7
+ * decided whether lighting runs at all. A thrown-away fragment should not pay for shading
8
+ * it will never contribute to, whichever of the two paths below ends up running, and the
9
+ * cutout is what a decal's edge needs regardless.
10
+ *
11
+ * An implicit-derivative texture() call rather than textureLod, and that is the
12
+ * documented exception the 2026-08-07 rule asks for rather than a lapse: the branch
13
+ * tests a *uniform*, so a compiler can prove it uniform and skip it wholesale, which
14
+ * is precisely what the point-light loop's per-iteration index test could not offer.
15
+ * The derivative is also the point here — this texture is mipmapped, and a tiled
16
+ * floor at a grazing angle without mip selection aliases and crawls.
17
+ *
18
+ * Sampled once into a local instead of at each use, so the untextured majority pays a
19
+ * single uniform test and the textured case pays a single fetch.
20
+ */
21
+ vec3 albedo = vColor;
22
+ /*
23
+ * How much of this fragment the surface actually covers.
24
+ *
25
+ * One for anything untextured, and the texture's own alpha for the rest. It reaches the
26
+ * output only where a draw is translucent — an opaque draw has blending off and the
27
+ * blend stage ignores alpha entirely — so this costs the opaque world nothing, and it
28
+ * is what lets a *decal* work: a stain, a poster, a sticker, whose image is transparent
29
+ * around the mark and partly transparent within it.
30
+ *
31
+ * Without it the cutout was the only thing a texture's alpha could say, so a translucent
32
+ * draw carrying a cutout texture painted its whole quad at the draw's opacity — a
33
+ * rectangle of haze with a picture in it, rather than a picture.
34
+ */
35
+ float coverage = 1.0;
36
+ if (uAlbedoEnabled != 0) {
37
+ vec4 texel = texture(uAlbedo, vUv);
38
+ // Discarded before anything else is computed: a thrown-away fragment should not pay
39
+ // for the lighting it will never contribute to.
40
+ if (texel.a < uAlbedoCutout) discard;
41
+ albedo *= texel.rgb;
42
+ coverage = texel.a;
43
+ }
44
+
45
+ /*
46
+ * Occlusion, roughness and metallic, resolved out here rather than inside the lighting branch.
47
+ *
48
+ * Occlusion applies to an unlit draw as much as to a lit one — uLightingEnabled at 0 is
49
+ * three.js's meshBasicMaterial and a baked crevice is still a crevice — so the three values have
50
+ * to exist on both sides of that branch.
51
+ *
52
+ * texture rather than textureLod, under a branch on a *uniform*, exactly as uAlbedo above: the
53
+ * compiler can prove this branch uniform and skip it wholesale, and the derivative is the point,
54
+ * because this image is mipmapped and a tiled floor at a grazing angle without mip selection
55
+ * aliases and crawls. See AGENTS.md, 2026-08-07, for the case where a branch is not uniform and
56
+ * this would be illegal.
57
+ *
58
+ * ormRoughness starts as the attribute and is overwritten rather than chosen by a ternary, so
59
+ * that a draw with no map bound reads exactly what it read before this existed, with nothing to
60
+ * get backwards. The map replaces the attribute rather than scaling it: an absent roughness
61
+ * attribute is 0.4277 and not 1, so scaling would have read glossy on every procedural mesh.
62
+ */
63
+ float ormOcclusion = 1.0;
64
+ float ormRoughness = vRoughness;
65
+ float metal = 0.0;
66
+ if (uOrmEnabled != 0) {
67
+ vec3 t = texture(uOrmMap, vUv).rgb;
68
+ ormOcclusion = mix(1.0, t.r, uOrmScale.r);
69
+ ormRoughness = clamp(t.g * uOrmScale.g, 0.0, 1.0);
70
+ metal = clamp(t.b * uOrmScale.b, 0.0, 1.0);
71
+ }
72
+
73
+ /*
74
+ * The unlit default: exactly the surface's own colour, times its own texture, and nothing
75
+ * else touching it — three.js calls this \`meshBasicMaterial\`. This is the value a draw with
76
+ * \`uLightingEnabled\` at 0 keeps to the end, and the value every lit draw starts from before
77
+ * the branch below builds a shaded result on top of it.
78
+ */
79
+ vec3 lit = albedo;
80
+
81
+ if (uLightingEnabled != 0) {
82
+ vec3 n = normalize(vNormal);
83
+ /*
84
+ * The authored normal, before either relief perturbs it.
85
+ *
86
+ * A map is the truth about which way the surface faces; uRelief invents structure from noise
87
+ * and uTextureRelief reads a height off the albedo's own luminance, and both exist because
88
+ * there was no way to author normals. So the map goes first and relief lays fine structure
89
+ * over whatever it produced — composing rather than replacing, because a map that switched
90
+ * relief off would silently discard a value the caller set.
91
+ *
92
+ * Gated on a uniform, which is what makes the derivatives inside tangentFrame legal at all —
93
+ * AGENTS.md 2026-08-07 — and what makes a scene with no normal map pay nothing.
94
+ *
95
+ * texture rather than textureLod, and this is the one place in this shader where that is
96
+ * right: a mipped colour image, sampled under uniform control flow, exactly as uAlbedo is
97
+ * below. The rule is about non-uniform branches, and this branch is on a uniform.
98
+ */
99
+ if (uNormalStrength > 0.0) {
100
+ vec3 mapped = texture(uNormalMap, vUv).xyz * 2.0 - 1.0;
101
+ mat3 tbn = tangentFrame(n, vWorldPos, vUv, vTangent, vHasTangents);
102
+ n = normalize(mix(n, normalize(tbn * mapped), uNormalStrength));
103
+ }
104
+
105
+ /*
106
+ * Microscopic relief: the surface's own texture, as a turn of the normal rather than a change
107
+ * of colour.
108
+ *
109
+ * **This is what makes a road read as asphalt instead of as a grey plane.** Real surfaces are
110
+ * covered in structure too small to model and too large to ignore, and what it does to a
111
+ * picture is change the *direction* light leaves each point, so a coarse surface catches a lamp
112
+ * on one side of every bump and shades on the other. Grain cannot do it: that varies how much
113
+ * light a point takes, which mottles a flat face and washes out at exactly the shallow angles
114
+ * where a road shows its texture most.
115
+ *
116
+ * The bumps are the gradient of the same value noise the grain term uses, sampled three times
117
+ * the shading point. That costs three extra noise evaluations and is gated on the surface
118
+ * having asked, so a world with no textured surfaces pays nothing.
119
+ *
120
+ * **The roughness is widened with it, and that is not optional.** A perturbed normal is a
121
+ * normal that varies fast, and a narrow highlight on a fast-varying normal is the sub-pixel
122
+ * sparkle this engine spent a release fixing. The amplitude here is *known* rather than
123
+ * measured from a screen-space derivative, which is what makes this the safe version: the same
124
+ * widening driven by fwidth was measured making that artefact three times worse, because that
125
+ * derivative spikes at every hard crease. See docs/IMPROVEMENTS.md.
126
+ */
127
+ float reliefAmount = uRelief * vRelief;
128
+ /* Grown with the relief, before anything reads it. See the comment above: a normal that
129
+ wanders cannot hold a highlight narrower than the wander. */
130
+ /*
131
+ * Grown with the relief *and* with the map, before anything reads it. A normal that wanders
132
+ * cannot hold a highlight narrower than the wander, and a strongly mapped surface wanders as
133
+ * surely as a procedurally relieved one — uTextureRelief already widens by its own amount for
134
+ * exactly this reason, and a map left out of it sparkles.
135
+ */
136
+ float surfaceRoughness = clamp(
137
+ ormRoughness + reliefAmount * RELIEF_ROUGHNESS + uNormalStrength * RELIEF_ROUGHNESS,
138
+ 0.0,
139
+ 1.0
140
+ );
141
+ /*
142
+ * How much of the noise one pixel covers, and the fade that follows from it.
143
+ *
144
+ * **Point-sampled procedural detail aliases the moment a pixel is wider than a bump**, and
145
+ * this had no defence against that at all. At the 60 cycles a metre a road asks for, a
146
+ * carriageway seen at a grazing angle puts tens of bumps inside one fragment and takes a
147
+ * single gradient from somewhere inside them; which one it lands on changes per pixel and
148
+ * per frame, so the surface reads as salt-and-pepper noise rather than as aggregate. It is
149
+ * worst exactly where a street scene spends most of its pixels: far away and near edge-on.
150
+ *
151
+ * The footprint is the world-space pixel size in noise cells, so the fade is stated in the
152
+ * units the artefact is actually in — a road fades at the distance a bump stops being
153
+ * resolvable, whatever the cycle count, the camera or the resolution.
154
+ *
155
+ * **Computed here rather than inside the branch below, and that is required rather than
156
+ * tidy.** \`fwidth\` is a derivative and \`vRelief\` is a varying, so a derivative taken under
157
+ * \`if (reliefAmount > 0.0)\` sits in non-uniform control flow: undefined in GLSL and refused
158
+ * outright by WGSL. \`shadowFactor\` in this file carries the long version of that rule and
159
+ * the shader that could not be translated until it was obeyed. \`uLightingEnabled\` above is
160
+ * a uniform, so this is legal where it stands.
161
+ *
162
+ * The cost is one \`fwidth\` on every lit fragment, including those carrying no relief.
163
+ * **What would make it wrong is reading this as the fix for sparkle**: it is not, and the
164
+ * roughness widening above deliberately keeps the *unfaded* amount. Unresolved normal
165
+ * variance is still roughness — a road that faded to smooth at distance would go glossy
166
+ * instead of noisy, which is a worse artefact wearing better clothes.
167
+ */
168
+ vec3 reliefPixel = fwidth(vWorldPos) * uReliefCycles;
169
+ float reliefFootprint = max(max(reliefPixel.x, reliefPixel.y), reliefPixel.z);
170
+ float reliefResolved = reliefAmount * (1.0 - smoothstep(0.5, 1.5, reliefFootprint));
171
+ if (reliefResolved > 0.0) {
172
+ vec3 at = vWorldPos * uReliefCycles;
173
+ /* One cell of the noise, which is one bump: the gradient over any smaller step is the
174
+ interpolant's own slope rather than the shape's. */
175
+ float here = grain(at);
176
+ float dx = grain(at + vec3(0.5, 0.0, 0.0)) - here;
177
+ float dy = grain(at + vec3(0.0, 0.5, 0.0)) - here;
178
+ float dz = grain(at + vec3(0.0, 0.0, 0.5)) - here;
179
+ vec3 slope = vec3(dx, dy, dz);
180
+ /* Only the part across the surface tilts it; pushing along the normal would move the point
181
+ rather than turn it, which a shading normal cannot express. */
182
+ slope -= n * dot(slope, n);
183
+ n = normalize(n - slope * reliefResolved * RELIEF_TILT);
184
+ }
185
+
186
+ /*
187
+ * And the relief the bound texture itself carries, as its own luminance read as a height.
188
+ *
189
+ * **Both conditions are uniforms, which is what makes the derivatives below legal.** A branch
190
+ * on a uniform is uniform control flow; \`shadowFactor\` above carries the long version of this
191
+ * rule and the shader that could not be translated until it was obeyed. The four \`texture\`
192
+ * calls are inside that same uniform branch, so the implicit level AGENTS.md's 2026-08-07
193
+ * rule is about is well defined here too.
194
+ *
195
+ * **A basis from the screen rather than from a tangent attribute.** Turning a gradient
196
+ * measured in UV into a tilt in world space needs to know which way U and V point on the
197
+ * surface, and \`MeshData\` carries no tangents. Deriving them per fragment from how world
198
+ * position and UV change across the quad needs nothing from the geometry, so this reaches
199
+ * every mesh in every consumer rather than only remade ones — and it is the construction
200
+ * three.js's own \`perturbNormalArb\` uses, so a surface ported from there is lit by the same
201
+ * arithmetic instead of by an approximation of it.
202
+ *
203
+ * \`det\` carries the basis's own scale, so the normal is built as \`abs(det) * n - slope\`
204
+ * and normalized: dividing the gradient by \`det\` instead would blow up on a fragment where
205
+ * the surface is edge-on and the quad's footprint collapses.
206
+ */
207
+ if (uAlbedoEnabled != 0 && uTextureRelief != 0.0) {
208
+ /*
209
+ * **A centred difference, not three.js's forward one, and that is what makes the two
210
+ * backends agree.** WebGL2's framebuffer counts y upward and WebGPU's counts it downward,
211
+ * so \`dFdy\` of anything points the opposite way on the two — which the construction below
212
+ * is built to survive, since \`sign(det)\` flips with it and cancels. A *forward* height
213
+ * difference breaks that cancellation, because \`h(uv + d) - h(uv)\` is not the negative of
214
+ * \`h(uv - d) - h(uv)\` anywhere the picture curves. Measured on the proof page: the y
215
+ * component of the perturbed normal disagreed between the backends by a mean of 18 of 255
216
+ * against 4 for the two components \`dFdx\` drives, with no dependence on pixel parity, so
217
+ * it was the direction rather than the precision. A centred difference is exactly
218
+ * antisymmetric, the cancellation holds, and it is the more accurate estimate besides: a
219
+ * forward difference reports the slope half a step away from the point it is used at.
220
+ */
221
+ vec2 duvdx = dFdx(vUv);
222
+ vec2 duvdy = dFdy(vUv);
223
+ /* Half, because a centred difference spans two steps and three.js's number means one. */
224
+ float reliefPerStep = uTextureRelief * 0.5;
225
+ float dHdx = (surfaceHeight(texture(uAlbedo, vUv + duvdx).rgb)
226
+ - surfaceHeight(texture(uAlbedo, vUv - duvdx).rgb)) * reliefPerStep;
227
+ float dHdy = (surfaceHeight(texture(uAlbedo, vUv + duvdy).rgb)
228
+ - surfaceHeight(texture(uAlbedo, vUv - duvdy).rgb)) * reliefPerStep;
229
+
230
+ vec3 dpdx = dFdx(vWorldPos);
231
+ vec3 dpdy = dFdy(vWorldPos);
232
+ vec3 acrossV = cross(dpdy, n);
233
+ vec3 acrossU = cross(n, dpdx);
234
+ float det = dot(dpdx, acrossV);
235
+ vec3 slope = sign(det) * (dHdx * acrossV + dHdy * acrossU);
236
+ /*
237
+ * **Capped at RELIEF_MAX_TILT, and that is not tuning.** This is *microscopic* relief:
238
+ * structure too small to model, standing on geometry that is still the real shape. Turned
239
+ * further than 45° it stops being that and starts lighting a face from behind its own
240
+ * surface, which no amount of texture detail is evidence for. And past that point the
241
+ * expression is near degenerate — \`slope\` has grown to the size of the term it is
242
+ * subtracted from, so the normalized result swings a long way for a small change in
243
+ * either, which is the fast-varying normal this engine already spent a release removing
244
+ * the sparkle from. The cap binds only where the picture is a cliff; at the amplitudes a
245
+ * photograph of a real surface produces, it never binds at all.
246
+ */
247
+ float lean = length(slope);
248
+ float limit = abs(det) * RELIEF_MAX_TILT;
249
+ if (lean > limit) slope *= limit / lean;
250
+ n = normalize(abs(det) * n - slope);
251
+ /* Widened for the same reason the procedural relief above widens it, and it is no more
252
+ optional here: a normal that wanders cannot hold a highlight narrower than the wander.
253
+ The amplitude is the caller's own stated one rather than a screen-space measurement,
254
+ which is the condition that made that widening safe there. */
255
+ surfaceRoughness = clamp(
256
+ surfaceRoughness + abs(uTextureRelief) * RELIEF_ROUGHNESS,
257
+ 0.0,
258
+ 1.0
259
+ );
260
+ }
261
+
262
+ /*
263
+ * One dot product, used twice: ndl is the lit side and its negative is the night side below.
264
+ * Shared rather than issued twice because it is the same number and one is cheaper, and not
265
+ * because it fixes anything. It was tried as a fix and measured: it does not. See the night
266
+ * term for what it does not fix.
267
+ */
268
+ float sunDot = dot(n, uDirectionalDir);
269
+ float ndl = max(sunDot, 0.0);
270
+ /*
271
+ * Kept as its own value rather than folded into direct, because the emissive
272
+ * term below needs it too. The sun and the moon are the only sources whose shadow
273
+ * used to stop at a glowing surface.
274
+ */
275
+ #if DIRECTIONAL_SHADOWS
276
+ float sunShade = shadowFactor(ndl);
277
+ #else
278
+ float sunShade = 1.0;
279
+ #endif
280
+ /*
281
+ * **How much sky this vertex can see, applied to the sun and to nothing else.**
282
+ *
283
+ * It multiplies sunShade rather than direct because the two are the same kind of quantity --
284
+ * occlusion of the one directional source -- and because direct is not the only thing that
285
+ * reads it: the sun's specular lobe below multiplies by sunShade on its own, so scaling
286
+ * direct alone would leave an enclosed face with no sunlight and a sun highlight anyway.
287
+ *
288
+ * **Ambient is deliberately untouched, and that is the defect this lane exists to fix.** The
289
+ * only per-vertex channel that reached shading before was colors, and the shader reads
290
+ * albedo = vColor before lit = albedo * (ambient + sun), so a sky factor carried in the
291
+ * vertex colour scales both terms: an enclosed face is darkened once for having no sky and
292
+ * again for the ambient it should still have received. A consumer hitting this raised their
293
+ * own floor constant to 0.45 to compensate and measured what it cost -- 55% of a chunk's
294
+ * vertices sat between 0.10 and 0.20 -- which is contrast they could not get back.
295
+ *
296
+ * 1.0 for every mesh that carries no channel, so this is a multiply by one on every scene
297
+ * that shipped before it.
298
+ */
299
+ sunShade *= vSkyDirect;
300
+ float direct = ndl * sunShade;
301
+
302
+ /*
303
+ * Hemispheric: sky above, ground below, mixed by which way the surface looks. An
304
+ * upward face takes the sky colour, a downward face the ground's, and a wall the
305
+ * average — which is what makes a floor and a ceiling in the same room read as
306
+ * different materials rather than the same one at different brightness.
307
+ */
308
+ vec3 ambient = mix(uAmbientGround, uAmbient, n.y * 0.5 + 0.5);
309
+ #if ENVIRONMENT_PROBE
310
+ /*
311
+ * The room's diffuse light, read from the probe array.
312
+ *
313
+ * **It replaces the hemispheric term where a grid exists, rather than lifting it.** What stood
314
+ * here until 2026-08-25 was a mix toward \`textureLod(uEnvironment, n, maxLod - 3)\`, guarded
315
+ * by a \`max\` so a *partial* probe could not darken a subject: a box-filtered level three
316
+ * below the coarsest is a sample of the room rather than an integral over it, and a plain mix
317
+ * toward one rendered a night courtyard black. What stood here after that was nine
318
+ * spherical-harmonic coefficients, which is the integral and needed a readback to produce.
319
+ *
320
+ * This is the same integral again, convolved into a level of the array at bake time and read
321
+ * with a fetch. **The clamp went with the fit that needed it**: a second-order projection can
322
+ * go slightly negative where the environment is strongly directional, and a cosine convolution
323
+ * of a non-negative environment cannot.
324
+ *
325
+ * \`uProbeGridAmbient\` is 0 when a caller declined the diffuse half, and the whole expression
326
+ * is multiplied away there — so a scene that passed \`irradiance: false\` keeps the gradient it
327
+ * set, which is the guarantee that option was added to give.
328
+ */
329
+ vec3 irradiance = gridIrradiance(n, vWorldPos);
330
+ ambient = mix(ambient, irradiance, uProbeGridAmbient * uEnvironmentEnabled);
331
+ #endif
332
+ /*
333
+ * A metal keeps its ambient and loses its direct diffuse.
334
+ *
335
+ * **Written as one term because the honest two cancel.** The spelling this stands for is
336
+ * albedo * (1 - metal) * (ambient + D * direct) + metal * albedo * ambient: a metal's reduced
337
+ * diffuse, plus the ambient floor that stops a rough one going black once its diffuse is gone
338
+ * and its reflection is scaled by (1 - roughness). Their two ambient contributions are
339
+ * (1 - metal) * ambient and metal * ambient, which sum to ambient exactly, so all of it
340
+ * reduces to this line.
341
+ *
342
+ * At metal 0 that is one multiply by 1.0 away from the expression it replaces, which is the
343
+ * smallest change available in a shader with a documented history of shared subexpressions
344
+ * rounding differently when their neighbours move. See the night-emissive term.
345
+ */
346
+ lit = albedo * (ambient + uDirectionalColor * direct * (1.0 - metal));
347
+
348
+ /*
349
+ * A sun highlight, for the few surfaces that ask for one.
350
+ *
351
+ * Blinn-Phong against the dominant source, which at night is the moon — so a polished
352
+ * prop catches whatever is in the sky rather than only the day. It takes the same
353
+ * sunShade the diffuse term takes, because a highlight that survives a shadow is
354
+ * the clearest possible tell that lighting is faked.
355
+ *
356
+ * vSpecular is zero for every mesh that did not supply the attribute — the vast
357
+ * majority — so for the rest of the world this is one multiply by a constant zero.
358
+ */
359
+ vec3 halfway = normalize(uDirectionalDir + normalize(uCameraPos - vWorldPos));
360
+ /*
361
+ * A metal's highlight takes its own colour; a dielectric's takes the light's.
362
+ *
363
+ * That is the single most visible thing metalness does, and it is why gold under a white lamp
364
+ * reads as gold rather than as beige plastic with a white dot on it. vSpecular is this
365
+ * engine's dielectric reflectance — a look control rather than an F0, as specularLobe's own
366
+ * header says at length — so the mix runs from it to the albedo rather than from 0.04.
367
+ *
368
+ * mix(x, y, 0) is exactly x, so at metal 0 this is vec3(vSpecular) and the line below is the
369
+ * one it replaces with the scalar broadened to three components.
370
+ */
371
+ vec3 specColor = mix(vec3(vSpecular), albedo, metal);
372
+ /*
373
+ * **A metal's highlight goes white at a grazing angle, and that is most of what reads as
374
+ * polished.**
375
+ *
376
+ * Schlick says reflectance climbs to 1 at the edge whatever the material is, so a red metal
377
+ * shows a red highlight face-on and a *white* one along every panel edge. Without this the
378
+ * highlight is albedo-tinted at every angle, and a dark red suit gets a dark red highlight
379
+ * that reads as matte paint however smooth the map says it is. The environment term below has
380
+ * carried its own Fresnel since it was written; the direct term never had one.
381
+ *
382
+ * Scaled by metal, so at metal 0 this is mix(specColor, white, 0) — specColor exactly, and the
383
+ * dielectric case is the expression it replaces. A dielectric's F0 is already so low that the
384
+ * engine's vSpecular look control stands in for the whole term, which is the reading
385
+ * specularLobe's header argues for at length.
386
+ */
387
+ float sunVoH = max(dot(normalize(uCameraPos - vWorldPos), halfway), 0.0);
388
+ vec3 sunSpec = mix(specColor, vec3(1.0), pow(1.0 - sunVoH, 5.0) * metal);
389
+ /*
390
+ * **Held back until after the environment blend, because that blend is a mix and would
391
+ * otherwise erase this.**
392
+ *
393
+ * The reflection below is written as \`mix(lit, environment, w)\`, so everything already in
394
+ * \`lit\` is scaled by \`1 - w\`. On a metal \`w\` reaches 1, and a highlight added here is
395
+ * multiplied by zero: the surface with the *most* reason to show a specular streak is the one
396
+ * guaranteed not to. That is the single most visible difference between a bought PBR model
397
+ * here and the same model in its own reference render, where every panel edge carries a white
398
+ * streak brighter than the paint under it.
399
+ *
400
+ * The lamps have always added their highlight after this block; the sun was the odd one out,
401
+ * and only because the block moved beneath it. Both are specular reflections of a source, and
402
+ * a reflection of the room cannot subtract one.
403
+ *
404
+ * Zero for the whole world that carries no specular attribute and no metalness, exactly as
405
+ * before: \`sunSpec\` is \`vec3(vSpecular)\` at metal 0 and \`vSpecular\` defaults to 0.
406
+ */
407
+ vec3 sunHighlight =
408
+ uDirectionalColor * specularLobe(max(dot(n, halfway), 0.0), surfaceRoughness) * sunSpec * sunShade;
409
+ /*
410
+ * The dielectric's share goes in here, where it has always gone, so the environment blend
411
+ * below dims it by exactly the reflectance it always dimmed it by. The metal's share is added
412
+ * after that blend instead — see the line that does it for why the two cannot be one.
413
+ */
414
+ lit += sunHighlight * (1.0 - metal);
415
+
416
+ /*
417
+ * Procedural grain on the surfaces that asked for it, so a face is not one flat colour.
418
+ *
419
+ * This is what "texture" means in an engine that has none: the variation is computed
420
+ * from world position rather than sampled, so it costs no upload, no memory and no
421
+ * sampler, and it cannot be seen to tile. Grain is the half of a realistic material
422
+ * that the colours alone cannot supply — a mineral surface is not uniform, and at
423
+ * these sizes the eye reads
424
+ * uniformity as plastic before it reads anything else.
425
+ *
426
+ * World-space rather than object-space, so the grain sits still in the world while a
427
+ * prop turns through it — which is how a solid looks, and what stops the pattern
428
+ * reading as something painted on and rotating with the object.
429
+ *
430
+ * Inside the branch on purpose. It is pure ALU with no texture fetch, so a
431
+ * non-uniform branch is safe here (AGENTS.md, 2026-08-07 — that rule is about
432
+ * implicit derivatives, which this has none of), and almost nothing in the world
433
+ * carries a grain attribute at all.
434
+ *
435
+ * The amount is what the surface itself declared, and the third answer to a question
436
+ * that was guessed twice. Gating on vSpecular meant "this is stone" only while stone
437
+ * was the only shiny thing in the world, so an imported car came out sanded. Weighting
438
+ * by vRoughness was closer and still a proxy: painted plaster is rough and has no
439
+ * grain, polished granite is smooth and has plenty, so a painted tower at roughness 0.55
440
+ * took 55% grain and read as marble. The two properties are independent and no weighting
441
+ * between them can be right. uGrain remains as the pass-level scale over whatever the
442
+ * geometry said, for an imported model that declares nothing.
443
+ */
444
+ if (vGrain > 0.0 && uGrain > 0.0) {
445
+ float grainMix = mix(
446
+ grain(vWorldPos * GRAIN_SCALE),
447
+ grain(vWorldPos * GRAIN_COARSE_SCALE),
448
+ GRAIN_COARSE_MIX
449
+ );
450
+ lit *= mix(1.0, mix(GRAIN_FLOOR, GRAIN_CEIL, grainMix), uGrain * vGrain);
451
+ }
452
+
453
+ /*
454
+ * Environment reflection, Fresnel weighted.
455
+ *
456
+ * The sky and ground colours the ambient term already carries, sampled along the mirror
457
+ * direction rather than the normal, so the reflection swings across a curved panel as the
458
+ * eye moves instead of sitting still on it. That movement is most of what reads as gloss.
459
+ *
460
+ * Fresnel because every dielectric reflects far more at a grazing angle than head on:
461
+ * about four percent facing the eye and nearly all of it at the edge, which is the bright
462
+ * rim along a wing and the reason a windscreen turns into a mirror from the side. Scaled
463
+ * by smoothness, since a rough surface scatters the same energy into no image at all.
464
+ *
465
+ * **Two sources, and which one is used is the caller's decision rather than a guess.** With a
466
+ * reflection probe baked, the mirror direction samples the room itself and a car body carries
467
+ * the panels above it and the walls around it. Without one, it samples the sky and ground
468
+ * colours the ambient term already holds: an approximation, stated as one, with no scene in
469
+ * it, which restores the tonal range a mirror surface has and cannot show anything.
470
+ */
471
+ /*
472
+ * **Or an ORM map, because a metal is nothing but its reflection.**
473
+ *
474
+ * This read uReflectivity alone, which is a per-pass control a consumer sets from the
475
+ * container's own scalar — and that scalar is 0 for exactly the materials that carry their
476
+ * metalness in a map, because a per-texel value cannot be expressed as a per-draw one. So the
477
+ * whole block was skipped for a fully metallic surface and max(uReflectivity, metal) inside it
478
+ * was never reached. A chromed subject came out near black with a baked probe sitting unread.
479
+ *
480
+ * **Gated on uOrmEnabled rather than on metal, and that is required rather than tidy.** The
481
+ * block takes fwidth of the mirror direction, and a derivative may only be taken under uniform
482
+ * control flow. uOrmEnabled is a uniform; metal is a texture fetch, so branching on it would
483
+ * put that derivative in non-uniform control flow and WGSL refuses the module outright. See
484
+ * AGENTS.md, 2026-08-07. The per-texel decision still happens, one line down, in the amount.
485
+ *
486
+ * At uOrmEnabled 0 this is uReflectivity > 0.0 exactly, so nothing that binds no map takes a
487
+ * branch it did not take before.
488
+ */
489
+ if (uReflectivity > 0.0 || uOrmEnabled != 0) {
490
+ vec3 toEye = normalize(uCameraPos - vWorldPos);
491
+ vec3 mirrored = reflect(-toEye, n);
492
+ vec3 environment = mix(uAmbientGround, uAmbient, mirrored.y * 0.5 + 0.5);
493
+ #if ENVIRONMENT_PROBE
494
+ /*
495
+ * Rougher surfaces read a blurrier level, which is what separates satin from lacquer, and
496
+ * **no level sharper than this pixel can resolve**, which is what stops a lacquered panel
497
+ * sparkling.
498
+ *
499
+ * Roughness alone answers only half the question. It says how blurred the *material* makes a
500
+ * reflection; it says nothing about how much of the cubemap lands inside one pixel. Where a
501
+ * panel creases or a shut line runs, the mirror direction swings a long way between
502
+ * neighbouring fragments, so at 0.025 for lacquered paint each of them reads a texel far from
503
+ * its neighbour's at the sharpest level there is. A street of lit windows then lands in some
504
+ * pixels and not the ones beside them: single bright dots tracing every crease, every gap and
505
+ * every silhouette, worst on dark paint at night, and gone the moment you move close enough
506
+ * for that curvature to span several pixels. **That is a sampling rate, not the model.**
507
+ *
508
+ * Two consumers reported it and it reproduces in demo/nightStreet.ts. It also looks exactly
509
+ * like anti-aliasing being off, and is not: multisampling resolves *coverage*, so it cannot
510
+ * touch a value that varies too fast inside one triangle.
511
+ *
512
+ * The footprint is fwidth of the mirror direction, turned into texels through the face size
513
+ * the chain implies: a face spans a right angle, so texels per radian is that size over π/2.
514
+ * The blurrier of the two levels wins. Nothing else about the term moves, deliberately —
515
+ * widening the *material's* roughness from the same derivative was tried and measured worse,
516
+ * because it also dims the reflection at exactly the creases it was meant to settle.
517
+ *
518
+ * Derivatives are well defined here: uReflectivity is a uniform, so this branch is uniform
519
+ * for every fragment in the draw. See AGENTS.md, 2026-08-07, for the case where it is not.
520
+ */
521
+ /*
522
+ * **Combined in roughness, not floored on a level — repaired 2026-08-25.**
523
+ *
524
+ * This turned the footprint into texels and took the blurrier of that level and the
525
+ * material's. Against the **box-filtered capture** that was right: a level was a smaller
526
+ * picture. Against the **GGX-prefiltered cube** a level is a *lobe convolved at roughness
527
+ * level/maxLod*, so the same floor stopped saying "sample more coarsely" and started saying
528
+ * "this surface is rough". Measured on a 512 probe with panels swinging about 0.03 rad a
529
+ * pixel: level 3.3, which under the new chain is an enforced **roughness 0.37 on every metal
530
+ * whatever its map says**. Reported from a consumer as metals reading opaque.
531
+ *
532
+ * So the footprint is the roughness whose lobe is as wide as the pixel, combined in
533
+ * quadrature — independent angular spreads add that way. A GGX lobe at roughness r has a
534
+ * characteristic half-angle of about r squared, so a footprint of \`swing\` radians is a
535
+ * roughness of \`sqrt(swing)\`.
536
+ *
537
+ * **Halved, because \`fwidth\` is an upper bound rather than the footprint.** It is
538
+ * \`abs(dFdx) + abs(dFdy)\`, a sum of both screen derivatives rather than the radius of the
539
+ * pixel's cone, and those derivatives are *coarse* on nearly every GPU: one pair per 2x2
540
+ * quad. Together they over-state the cone by about two, and this squares back into a
541
+ * roughness, so a half here is a quarter of the solid angle.
542
+ *
543
+ * **The reason this shape was refused before is gone.** The comment it replaces recorded
544
+ * that widening the material's roughness "was tried and measured worse, because it also dims
545
+ * the reflection at exactly the creases it was meant to settle" — true under the old weight,
546
+ * where \`reflectAmount\` carried a \`1 - roughness\` factor. \`envSpecularEnergy\` removed
547
+ * that: a metal's weight is exactly 1.0 at every roughness, so widening no longer dims.
548
+ *
549
+ * **It widens only the level this picks.** \`surfaceRoughness\` is untouched, because it also
550
+ * feeds the environment BRDF and the sun's own lobe, and a crease should not grow a wider
551
+ * *direct* highlight for spanning few pixels.
552
+ *
553
+ * **What it costs**: a surface still loses its mirror where it is genuinely under-sampled,
554
+ * which is the sparkle protection doing its job. **What would make it wrong** is a chain
555
+ * whose levels are not roughness — the capture's own box chain is exactly that.
556
+ */
557
+ /*
558
+ * **The map's edge, not \`exp2(uEnvironmentMaxLod)\`.** Those were one number while the
559
+ * environment was a cube whose chain ran to a single texel a face. The chain now stops one
560
+ * level below the cosine convolution, so the top level is 4 while the map is 256 across, and
561
+ * the old expression would say sixteen — which reads as every metal being under-sampled and
562
+ * forced to a roughness its material does not have.
563
+ */
564
+ float faceTexels = uEnvironmentEdge;
565
+ float swingRad = max(length(fwidth(mirrored)), 1e-6);
566
+ /*
567
+ * **Two chains, two ways to pick a level, chosen by \`uEnvironmentPrefiltered\`.**
568
+ *
569
+ * A *box* level is a smaller picture of the room, so the footprint belongs as a floor on the
570
+ * level: read coarsely enough that a pixel covers about a texel, and lose detail, nothing
571
+ * else. A *prefiltered* level is the room convolved at roughness level/maxLod, so the same
572
+ * floor stops saying "sample more coarsely" and starts saying "this surface is rough" —
573
+ * measured on a 512 probe with panels swinging 0.03 rad a pixel, an enforced roughness of
574
+ * 0.37 on every metal whatever its map says.
575
+ *
576
+ * So for the lobe chain the footprint is expressed as the roughness whose lobe is as wide as
577
+ * the pixel and combined in quadrature, independent angular spreads adding that way: a GGX
578
+ * lobe at roughness r has a characteristic half-angle of about r squared, so a footprint of
579
+ * \`swing\` radians is a roughness of \`sqrt(swing)\`. Halved, because \`fwidth\` is
580
+ * \`abs(dFdx) + abs(dFdy)\` over *coarse* derivatives — one pair per 2x2 quad — so it
581
+ * over-states the pixel's cone by about two, and this squares back into a roughness.
582
+ *
583
+ * **What it costs** is both expressions evaluated and one selected, which is a handful of
584
+ * multiply-adds against a uniform. **What would make it wrong** is a third kind of chain.
585
+ */
586
+ float boxLod = max(surfaceRoughness * uEnvironmentMaxLod, log2(max(swingRad * faceTexels * 0.6366, 1.0)));
587
+ float footprintRough = 0.5 * sqrt(min(swingRad, 1.0));
588
+ float lobeRough = min(1.0, sqrt(surfaceRoughness * surfaceRoughness + footprintRough * footprintRough));
589
+ float envLod = clamp(
590
+ mix(boxLod, lobeRough * uEnvironmentMaxLod, uEnvironmentPrefiltered),
591
+ 0.0,
592
+ uEnvironmentMaxLod
593
+ );
594
+ vec3 room = gridRadiance(mirrored, envLod, vWorldPos);
595
+ /*
596
+ * **The room wins outright where there is one, and it must.**
597
+ *
598
+ * This was briefly written as \`max(environment, room)\`, to stop a dim probe leaving a metal
599
+ * darker than the world around it. It does stop that, and it destroys the reflection doing
600
+ * it: the hemispheric term is a two-colour gradient with no image in it whatsoever, and a
601
+ * world's declared ambient is brighter than most of what a probe actually holds — so the max
602
+ * returns the *gradient* over nearly the whole sphere and a mirror shows a smooth wash. That
603
+ * is the same flat, matte surface this was trying to fix, arrived at from the other side.
604
+ *
605
+ * A floor is right for irradiance, where only the level matters, and wrong for a mirror,
606
+ * where the level is the least of what is being carried. Brightness belongs to
607
+ * \`uEnvironmentGain\`, which scales what the surface reflects and keeps its image.
608
+ */
609
+ environment = mix(environment, room, uEnvironmentEnabled);
610
+ #endif
611
+ /*
612
+ * Whatever the surface ends up reflecting, at the brightness the caller asked for — **and
613
+ * weighted by metalness, which is the only case the gain was ever an answer to.**
614
+ *
615
+ * A metal has no diffuse and no ambient behind its reflection: what it reflects is all it
616
+ * is, so when the probe under-reports the room the surface has no other way to be lit. A
617
+ * dielectric is in no such position. Its reflection sits on top of a diffuse term that is
618
+ * already carrying the world's light, and scaling it lifts a surface that was correct —
619
+ * measured on a car painted black, which came out washed grey with a milky haze over every
620
+ * panel, because \`fresnel * uReflectivity * (1 - roughness)\` reaches most of the way to 1 at
621
+ * a grazing angle and three times the room is what landed there.
622
+ *
623
+ * \`mix(1, gain, 0)\` is exactly 1, so a dielectric is arithmetically untouched however the
624
+ * caller sets this.
625
+ */
626
+ environment *= mix(1.0, uEnvironmentGain, metal);
627
+ float facing = 1.0 - max(dot(n, toEye), 0.0);
628
+ /*
629
+ * **A metal's Fresnel base is close to its albedo, not a dielectric's four percent.** Schlick
630
+ * with a fixed 0.04 is right for glass, plastic and paint and badly wrong for chrome: left
631
+ * there, a smooth metal seen head-on takes a four percent reflection and reads as dark paint.
632
+ *
633
+ * And a metal is nothing but its reflection, so it raises the amount as well as the base —
634
+ * uReflectivity is a per-pass control and a metal texel cannot be left mirroring nothing
635
+ * because the pass it happens to be drawn in did not ask for reflections.
636
+ *
637
+ * Both collapse at metal 0: mix(0.04, 1.0, 0) is 0.04, and max(uReflectivity, 0) is
638
+ * uReflectivity because both renderers clamp that uniform non-negative on upload.
639
+ */
640
+ float f0 = mix(0.04, 1.0, metal);
641
+ float fresnel = mix(f0, 1.0, facing * facing * facing * facing * facing);
642
+ /*
643
+ * **A rough metal is still a metal, and \`1 - roughness\` said otherwise.**
644
+ *
645
+ * The thinning belongs to the dielectric term, where it stands for a polish that scatters
646
+ * its reflection away into no image at all. A metal has no diffuse to fall back on: this is
647
+ * its entire response to light, and scaling it by \`1 - roughness\` throws away 40% of
648
+ * everything a surface at roughness 0.4 shows and replaces it with \`albedo * ambient\`,
649
+ * which is a dark constant. That is the "reads dark, reads like paint" report, arithmetically.
650
+ *
651
+ * It is also double counting. The chain the sample comes from *is* the roughness — a rougher
652
+ * surface reads a blurrier level a few lines above — so dimming it as well charges twice for
653
+ * one property. A GGX prefilter loses some energy with roughness and it is nothing like this
654
+ * steep; that belongs with the prefilter, when there is one.
655
+ *
656
+ * **Identical at metal 0**: \`max(uReflectivity * (1 - r), 0)\` is \`uReflectivity * (1 - r)\`,
657
+ * which is what the old \`max(uReflectivity, metal) * (1 - r)\` came to, and both renderers
658
+ * clamp that uniform non-negative on upload.
659
+ */
660
+ float reflectAmount = max(uReflectivity * (1.0 - surfaceRoughness), metal);
661
+ /*
662
+ * **With a prefiltered environment, how much a surface returns is an integral rather than a
663
+ * curve somebody chose**, and this is where the comment above said the thinning should go
664
+ * once a prefilter existed.
665
+ *
666
+ * \`envBrdfApprox\` is the split-sum's second half: the convolution decides *what* is
667
+ * reflected at a roughness and this decides *how much*. It contains the angular dependence,
668
+ * so it replaces \`fresnel\` as well as \`1 - roughness\` — keeping the separate Fresnel
669
+ * factor would apply it twice.
670
+ *
671
+ * **Gated on \`uEnvironmentEnabled\`, and the gate is an argument rather than a way of
672
+ * protecting a pixel count.** The split sum approximates an integral over a *prefiltered*
673
+ * environment. With no probe, \`environment\` is the two-colour sky-and-ground gradient,
674
+ * which was never integrated over anything and has no chain behind it, so applying a term
675
+ * that assumes one would assert an integral that does not exist. Every scene without a probe
676
+ * therefore keeps exactly the arithmetic it had.
677
+ *
678
+ * **That last clause used to read "which is every published scene", and it was wrong.**
679
+ * \`nightStreet\` bakes at 128 and \`showroom\` at 256, so two of the seven take this
680
+ * branch. They move no pixels — measured, 0 of 921,600 on each of the seven — but only
681
+ * because neither binds an ORM map, so both are dielectrics at \`f0\` 0.04 where this term
682
+ * is worth hundredths of a level. A claim about a gate is worth checking against the
683
+ * scenes rather than remembering, which is what \`docs/README.md\` says about every claim
684
+ * of this shape.
685
+ *
686
+ * **Measured rather than assumed, and it moved less than expected.** On the roughness ladder
687
+ * the whole change is at most 0.2 of 255, because a rough surface reads the coarsest levels
688
+ * of the chain and those are dark in a room with one bright wall. The term is nonetheless
689
+ * the right one: it is where a bright loaded environment will show, and it stops a fully
690
+ * rough dielectric reflecting exactly nothing, which \`1 - roughness\` asserted and which is
691
+ * false of every real surface.
692
+ */
693
+ float ndv = 1.0 - facing;
694
+ /*
695
+ * **\`envSpecularEnergy\` and not \`f0 * dfg.x + dfg.y\`, and the difference is a metal
696
+ * looking like paint.** The bare split sum integrates one scattering event and drops every
697
+ * ray that bounces twice inside the microsurface, so it returns \`1 - 0.55 * roughness\` for
698
+ * a surface at \`f0\` 1 — asserting that a perfect mirror absorbs over half the light once it
699
+ * is roughened. Reported from a consumer whose subject is forced fully metallic: measured
700
+ * losses of 22% at roughness 0.4, 33% at 0.6 and 44% at 0.8 against the term this replaced.
701
+ *
702
+ * The compensation returns it, and at \`f0\` 1 it returns *exactly* 1.0 at every roughness —
703
+ * which is the old \`fresnel * reflectAmount\` weight for a metal, to the bit. So this is a
704
+ * repair rather than a second look decision, and the arithmetic is in \`lobes.ts\`.
705
+ */
706
+ /*
707
+ * **Compiled out where there is no probe, and that is worth 4,762 gzipped bytes.**
708
+ *
709
+ * With \`ENVIRONMENT_PROBE\` off, \`prefiltered\` was a literal 0 and \`mix\` returned the
710
+ * left-hand side outright — so the fit, the compensation and the clamp were computed, and
711
+ * shipped, to be multiplied by nothing in eight of the sixteen permutations. \`ARCHITECTURE.md\`
712
+ * §1 is why that is not a rounding error: a permutation is larger than deflate's 32 KB window,
713
+ * so sixteen near-identical copies do not dedupe and every consumer downloads all of them.
714
+ *
715
+ * Measured on \`core-only\`, gzipped: **529,889 ungated, 525,127 gated**, against a floor of
716
+ * 524,402 before any of this. So the energy compensation costs **725 bytes** rather than the
717
+ * 5,487 it costs computed unconditionally — and the gate pays for more than it: \`dfg\` moved
718
+ * inside it too, so eight permutations stopped carrying \`envBrdfApprox\` as well, which they
719
+ * had been doing since the split sum landed. The picture is identical to the bit either way.
720
+ *
721
+ * The prediction written here first was 2,708. It was wrong, and it is replaced by the
722
+ * measurement rather than kept beside it.
723
+ *
724
+ * **What would make this wrong** is a second consumer of \`dfg\` outside this branch — a
725
+ * clearcoat lobe wanting the same fit. It would then belong above the \`#if\` again, and the
726
+ * bytes would be buying something.
727
+ */
728
+ #if ENVIRONMENT_PROBE
729
+ vec2 dfg = envBrdfApprox(ndv, surfaceRoughness);
730
+ float integrated = clamp(envSpecularEnergy(f0, dfg) * max(uReflectivity, metal), 0.0, 1.0);
731
+ /*
732
+ * **The split sum only where a prefiltered chain is what is being sampled.** It is the
733
+ * correct share of a *prefiltered* environment; over a box chain it asserts an integral that
734
+ * was never performed, and it flattens the Fresnel sweep that reads as gloss — measured, the
735
+ * grazing-to-head-on ratio falls from about 15 to 1 down to 5 to 1 at roughness 0.5, which
736
+ * was reported twice as a metal going opaque. With the prefilter off this is exactly the
737
+ * expression the engine carried before the lighting track.
738
+ */
739
+ float weight = mix(
740
+ clamp(fresnel * reflectAmount, 0.0, 1.0),
741
+ integrated,
742
+ uEnvironmentEnabled * uEnvironmentPrefiltered
743
+ );
744
+ #else
745
+ float weight = clamp(fresnel * reflectAmount, 0.0, 1.0);
746
+ #endif
747
+ lit = mix(
748
+ lit,
749
+ environment * mix(vec3(1.0), albedo, metal),
750
+ weight
751
+ );
752
+ }
753
+ /*
754
+ * The sun's highlight, on top of whatever the surface reflects rather than underneath it —
755
+ * **and only by as much as the surface is metal.**
756
+ *
757
+ * The metal half is the fix: the blend above reaches 1 on a metal, so a highlight added before
758
+ * it was multiplied by zero and the surface with the most reason to show a specular streak was
759
+ * the one guaranteed not to.
760
+ *
761
+ * The \`(1 - metal)\` half above is the part that must not move. A dielectric's blend weight is
762
+ * \`fresnel * uReflectivity * (1 - roughness)\`, which on polished paint or glass reaches most
763
+ * of the way to 1 at a grazing angle — and adding the highlight after the blend rather than
764
+ * into it takes a lobe that was correctly dimmed by the surface's own reflectance and puts it
765
+ * back at full strength. On a near-black glass lens carrying \`specular\` 1 that is a white
766
+ * blob where there was none. So the dielectric keeps the arithmetic it has always had, exactly:
767
+ * at metal 0 the line below adds nothing and the line above is the original expression.
768
+ */
769
+ lit += sunHighlight * metal;
770
+
771
+ /*
772
+ * Self-illuminated geometry. Additive and gated on the night factor, so it is
773
+ * exactly invisible by day and costs two ALU ops.
774
+ *
775
+ * Dimmed in shadow, but only partly, and the fraction is the whole argument. Strictly,
776
+ * emission is light leaving a surface and an occluder between that surface and the sun
777
+ * has no business touching it — which is what this used to do, adding the full term
778
+ * after the shadow. The result is that a shadow crossing the terrace darkens the deck
779
+ * and leaves every inlaid band at full brightness, so the bands stop belonging to the
780
+ * floor they are cut into. It gets reported as the shadow passing *under* the inlaid
781
+ * markings, as though it could not be cast over them at all.
782
+ *
783
+ * That is a real break in the picture, and the physical reading is not much
784
+ * of a defence, because none of these surfaces is a pure emitter: a lit inlay is worked
785
+ * stone with light coming *through* it, and the stone around the light is in shadow like
786
+ * everything else. So the shadow takes a share and leaves the rest. At 0.55 a band in
787
+ * shade is visibly in shade and still visibly lit, which is what it is.
788
+ */
789
+ /*
790
+ * Deferred to *after* the point lights, so their shadows can reach it. Addition
791
+ * commutes; the order is only about what is known by then.
792
+ */
793
+ float lightShade = 1.0;
794
+
795
+ /*
796
+ * Which froxel this fragment is in.
797
+ *
798
+ * Derived in view space rather than from \`gl_FragCoord\`, for the reason \`uView\`'s own
799
+ * comment gives: the two backends disagree about which way a framebuffer's y runs, and a tile
800
+ * row taken from a window coordinate would be mirrored on one of them.
801
+ *
802
+ * The arithmetic is \`sliceOfViewDepth\` and \`tileOf\` from \`clusteredLights.ts\`, in the
803
+ * same order and with the same clamps, because a fragment reading a froxel the binner filled
804
+ * differently is a fragment lit by somebody else's lights.
805
+ */
806
+ vec3 clusterView = (uView * vec4(vWorldPos, 1.0)).xyz;
807
+ float clusterDepth = -clusterView.z;
808
+ float clusterHalfH = max(clusterDepth, 1e-4) * uClusterFrustum.z;
809
+ float clusterHalfW = clusterHalfH * uClusterFrustum.w;
810
+ int clusterTileX = clamp(
811
+ int(floor((clusterView.x / clusterHalfW + 1.0) * 0.5 * float(CLUSTER_X))), 0, CLUSTER_X - 1);
812
+ int clusterTileY = clamp(
813
+ int(floor((clusterView.y / clusterHalfH + 1.0) * 0.5 * float(CLUSTER_Y))), 0, CLUSTER_Y - 1);
814
+ int clusterSlice = 0;
815
+ if (clusterDepth > uClusterFrustum.x) {
816
+ clusterSlice = clamp(
817
+ int(floor(log(clusterDepth / uClusterFrustum.x)
818
+ / log(uClusterFrustum.y / uClusterFrustum.x) * float(CLUSTER_Z))),
819
+ 0, CLUSTER_Z - 1);
820
+ }
821
+ int clusterBaseTexel = LIGHT_REGION_TEXELS
822
+ + (clusterTileX + clusterTileY * CLUSTER_X + clusterSlice * CLUSTER_X * CLUSTER_Y)
823
+ * CLUSTER_TEXELS;
824
+ int clusterLights = uClustered == 1 ? int(clusterTexel(clusterBaseTexel).x) : 0;
825
+
826
+ // Point lights. Inverse-square-ish falloff clipped to a finite radius, so a
827
+ // light can be culled without a visible seam.
828
+ for (int slot = 0; slot < LIGHT_LOOP_MAX; slot++) {
829
+ /*
830
+ * **The two arms differ in where a light's numbers come from and in nothing else.**
831
+ *
832
+ * Everything below reads \`lightPos\`, \`lightColor\`, \`lightRadius\`,
833
+ * \`lightSourceRadius\`, \`lightWeight\` and \`shadowSlot\`, so the clustered path is a
834
+ * different *source* for the same six values rather than a second copy of the shading. That
835
+ * is what makes the zero-pixel gate between the two arms mean something: if the body were
836
+ * duplicated, the gate would only be comparing two transcriptions.
837
+ */
838
+ int i;
839
+ vec3 lightPos;
840
+ float lightRadius;
841
+ vec3 lightColor;
842
+ float lightSourceRadius;
843
+ float lightWeight;
844
+ int shadowSlot;
845
+ vec3 lightDir;
846
+ vec2 lightCone;
847
+ float iesProfile;
848
+ vec3 iesAxis;
849
+ float cookie;
850
+ if (uClustered == 1) {
851
+ if (slot >= clusterLights) break;
852
+ /*
853
+ * Four indices to a texel. Selected with compares rather than by indexing the vector,
854
+ * because a dynamic component index is legal in GLSL ES 3.00 and is one more thing for a
855
+ * translator to disagree about on the one path that has no fallback beneath it.
856
+ */
857
+ uvec4 packedIndices = clusterTexel(clusterBaseTexel + 1 + (slot >> 2));
858
+ int component = slot & 3;
859
+ uint packed = component == 0
860
+ ? packedIndices.x
861
+ : component == 1 ? packedIndices.y : component == 2 ? packedIndices.z : packedIndices.w;
862
+ i = int(packed);
863
+ int record = i * LIGHT_TEXELS;
864
+ uvec4 rec0 = clusterTexel(record);
865
+ uvec4 rec1 = clusterTexel(record + 1);
866
+ uvec4 rec2 = clusterTexel(record + 2);
867
+ lightPos = vec3(
868
+ uintBitsToFloat(rec0.x), uintBitsToFloat(rec0.y), uintBitsToFloat(rec0.z));
869
+ lightRadius = uintBitsToFloat(rec0.w);
870
+ lightColor = vec3(
871
+ uintBitsToFloat(rec1.x), uintBitsToFloat(rec1.y), uintBitsToFloat(rec1.z));
872
+ lightSourceRadius = uintBitsToFloat(rec1.w);
873
+ lightWeight = uintBitsToFloat(rec2.x);
874
+ shadowSlot = int(uintBitsToFloat(rec2.y));
875
+ /*
876
+ * The fourth texel, which is the cone. See LIGHT_RECORD in clusteredLights.ts: the
877
+ * direction spans rec2.z, rec2.w and rec3.x, because the record is a run of floats rather
878
+ * than a struct and a vec3 does not respect texel boundaries.
879
+ */
880
+ uvec4 rec3 = clusterTexel(record + 3);
881
+ lightDir = vec3(
882
+ uintBitsToFloat(rec2.z), uintBitsToFloat(rec2.w), uintBitsToFloat(rec3.x));
883
+ lightCone = vec2(uintBitsToFloat(rec3.y), uintBitsToFloat(rec3.z));
884
+ iesProfile = uintBitsToFloat(rec3.w);
885
+ /* The fifth texel, which is the photometric azimuth. See LIGHT_RECORD in clusteredLights.ts
886
+ for why it costs a texel rather than being derived from the direction. */
887
+ uvec4 rec4 = clusterTexel(record + 4);
888
+ iesAxis = vec3(
889
+ uintBitsToFloat(rec4.x), uintBitsToFloat(rec4.y), uintBitsToFloat(rec4.z));
890
+ cookie = uintBitsToFloat(rec4.w);
891
+ } else {
892
+ i = slot;
893
+ if (i >= uLightCount) break;
894
+ lightPos = uLightPos[i];
895
+ lightRadius = uLightRadius[i];
896
+ lightColor = uLightColor[i];
897
+ lightSourceRadius = uLightSourceRadius[i];
898
+ lightWeight = uLightWeight[i];
899
+ shadowSlot = i;
900
+ lightDir = uLightDir[i];
901
+ lightCone = uLightCone[i];
902
+ iesProfile = uLightIesProfile[i];
903
+ iesAxis = uLightIesAxis[i];
904
+ cookie = uLightCookie[i];
905
+ }
906
+ /*
907
+ * **The cone, and a point light's is one that admits every direction.**
908
+ *
909
+ * \`lightCone\` is the cosine of the inner angle then of the outer, and a light that declared
910
+ * no cone carries -1 and -2 — so the dot product, which is at least -1 for every direction
911
+ * on the sphere, is always at or past the upper edge and the term is exactly 1. Not
912
+ * approximately: \`smoothstep\` returns its upper bound outright once the argument reaches
913
+ * it, with no arithmetic in between, which is what makes every scene with no spot light
914
+ * bit-identical to the build before spot lights existed.
915
+ *
916
+ * **Not written as a branch on whether the light is a spot.** In the clustered arm that
917
+ * would be a branch on a value read from a texture, which is not provably uniform control
918
+ * flow, and the shadow fetches below sit inside this same loop — the 2026-08-07 rule is
919
+ * about exactly that. Two multiply-adds are cheaper than the flattening a compiler would do
920
+ * anyway.
921
+ *
922
+ * **What it costs** is that the edge is a smoothstep in the cosine rather than in the angle,
923
+ * so the falloff is very slightly biased toward the outer edge. **What would make it wrong**
924
+ * is a cone narrow enough for that bias to be visible, which is a few degrees, and at that
925
+ * width the shadow map's resolution is the larger problem.
926
+ */
927
+ vec3 toLight = lightPos - vWorldPos;
928
+ float dist = length(toLight);
929
+ /* The direction the light travels, which is what a cone is measured against. */
930
+ float coneFalloff =
931
+ smoothstep(lightCone.y, lightCone.x, dot(-toLight / max(dist, 1e-4), lightDir));
932
+ /*
933
+ * Two ways for a lamp to fade, and the difference is a look rather than a detail.
934
+ *
935
+ * The default shapes the falloff to the radius: it reaches zero exactly at the edge, so
936
+ * a light can be culled with no seam, and it stays gentle near the source. That is the
937
+ * right behaviour for a world of large soft sources and it is what every world here is
938
+ * authored against.
939
+ *
940
+ * Inverse-square is what a physically-based renderer does, and it is *violent* close
941
+ * in: at a tenth of a metre it is a hundred times the value at one metre, so a lamp
942
+ * mounted a few centimetres under a ceiling floods it and the panel around it reads as
943
+ * lit rather than as a dark plate with a bright strip in it. Windowed by the radius the
944
+ * same way three.js windows its own, so the light still ends where it says it does.
945
+ */
946
+ float falloff;
947
+ if (uLightFalloff == 1) {
948
+ float window = clamp(1.0 - pow(dist / max(lightRadius, 1e-4), 4.0), 0.0, 1.0);
949
+ falloff = window * window / max(dist * dist, 0.01);
950
+ } else {
951
+ falloff = clamp(1.0 - dist / lightRadius, 0.0, 1.0);
952
+ }
953
+ if (falloff <= 0.0) continue;
954
+ float ndl = max(dot(n, toLight / max(dist, 1e-4)), 0.0);
955
+ if (ndl <= 0.0) continue;
956
+
957
+ /*
958
+ * Normal-offset bias.
959
+ *
960
+ * The deck is a single enormous flat plane, so the light grazes nearly all
961
+ * of it: one shadow texel covers a long stretch whose depth changes far
962
+ * faster than any depth bias can absorb. Pushing the *sample position*
963
+ * along the surface normal instead moves the receiver clear of its own
964
+ * occluder in the direction that actually matters, and it scales with how
965
+ * grazing the angle is. Without it the cubemap's face seams show as a
966
+ * diagonal cross and the whole plane bands.
967
+ */
968
+ #if POINT_SHADOWS
969
+ float grazing = 1.0 - ndl;
970
+ float normalOffset = (0.06 + dist * 0.05) * grazing * grazing + 0.02;
971
+ vec3 shadowFrom = (vWorldPos + n * normalOffset) - lightPos;
972
+
973
+ /*
974
+ * uPointShadowWeight is how present the cubemap is, and it is the difference
975
+ * between a shadow arriving and a shadow appearing. A pool slot changing hands
976
+ * throws its image away, so the map behind a light can go from absent to complete
977
+ * between two frames; sampling it at full strength the frame it lands is a shadow
978
+ * switching on under a lamp that never moved. It ramps instead.
979
+ */
980
+ float occl = 1.0;
981
+ /*
982
+ * One lookup, where there were ten unrolled else-if arms.
983
+ *
984
+ * GLSL ES cannot index a sampler array with a non-constant expression, so twelve cubemaps
985
+ * meant twelve declared samplers and a chain comparing this light against each slot's
986
+ * owner. One array texture makes the map a *layer* — an ordinary integer — so the light
987
+ * indexes the arrays directly and the chain is a compare against -1.
988
+ */
989
+ /*
990
+ * **Indexed by the shadow slot, not by the light.** Clustering lifts how many lights shade a
991
+ * fragment and not how many shadow maps exist — the pool is a memory budget — so a clustered
992
+ * light either holds one of these slots or casts none, and \`-1\` says which. \`clamp\` keeps
993
+ * the read in range for a light with no slot, whose \`layer\` is then forced negative anyway;
994
+ * indexing a uniform array with -1 is undefined and would be a picture rather than an error.
995
+ *
996
+ * **And in range at the other end, which a shrunken budget is what made reachable.** A
997
+ * clustered light carries its slot in the froxel record rather than in \`i\`, so a build with
998
+ * \`maxLights\` below \`MAX_LIGHTS_PER_CLUSTER\` can be handed a slot past the end of these
999
+ * arrays — undefined again, and again as a picture. Past the end is treated as *no shadow*
1000
+ * rather than clamped onto the last slot: a light wearing another light's occlusion is a
1001
+ * wrong picture, where a light that occludes nothing is the engine's own stated trade
1002
+ * everywhere else it runs out of room.
1003
+ */
1004
+ int shadowRead = clamp(shadowSlot, 0, MAX_LIGHTS - 1);
1005
+ int layer = shadowSlot >= 0 && shadowSlot < MAX_LIGHTS ? uPointShadowLayer[shadowRead] : -1;
1006
+ if (layer >= 0) {
1007
+ occl = mix(
1008
+ 1.0,
1009
+ pointShadow(
1010
+ uPointShadows,
1011
+ float(layer),
1012
+ shadowFrom,
1013
+ uPointShadowFar[shadowRead],
1014
+ uPointShadowNear[shadowRead],
1015
+ uPointShadowSize[shadowRead]
1016
+ ),
1017
+ uPointShadowWeight[shadowRead]
1018
+ );
1019
+ }
1020
+
1021
+ // A live map contains movers only. Multiplication composes their occlusion
1022
+ // with the immutable world map, while the weight makes ownership changes a
1023
+ // crossfade rather than switching a whole character shadow in one frame.
1024
+ float liveOccl = 1.0;
1025
+ float liveWeight = 0.0;
1026
+ int liveLayer =
1027
+ shadowSlot >= 0 && shadowSlot < MAX_LIGHTS ? uLivePointShadowLayer[shadowRead] : -1;
1028
+ if (liveLayer >= 0) {
1029
+ liveOccl = pointShadow(
1030
+ uPointShadows,
1031
+ float(liveLayer),
1032
+ shadowFrom,
1033
+ uLivePointShadowFar[shadowRead],
1034
+ uLivePointShadowNear[shadowRead],
1035
+ uLivePointShadowSize[shadowRead]
1036
+ );
1037
+ liveWeight = uLivePointShadowWeight[shadowRead];
1038
+ }
1039
+ occl *= mix(1.0, liveOccl, liveWeight);
1040
+ #else
1041
+ float occl = 1.0;
1042
+ #endif
1043
+
1044
+ // Blend occlusion out as the light fades, so a shadow can never be more
1045
+ // present than the light casting it — and slot changes at the edge of
1046
+ // range become invisible rather than a pop.
1047
+ float shaded = mix(1.0, occl, clamp(falloff * 2.0, 0.0, 1.0));
1048
+ /*
1049
+ * The cone multiplies the distance falloff rather than replacing it, which is what makes a
1050
+ * spot a point light with a direction rather than a second kind of light. It is exactly 1
1051
+ * for a light that declared no cone, so this line is arithmetically the one it replaces.
1052
+ */
1053
+ /*
1054
+ * **What a real fixture throws, rather than what a cone approximates.**
1055
+ *
1056
+ * An IES profile is a measured intensity by angle from the fixture's axis, so this is the
1057
+ * same angle the cone is measured at, read out of one row of the atlas. A light with no
1058
+ * profile carries a negative index and takes 1, which is the multiplicative identity and
1059
+ * costs a fetch nobody can avoid — see below.
1060
+ *
1061
+ * **The fetch is unconditional and \`textureLod\`, both deliberately.** This loop is not
1062
+ * provably uniform control flow: in the clustered arm the light index comes out of a
1063
+ * texture. A branch on the profile index would therefore be a non-uniform branch around a
1064
+ * sample, which is what the 2026-08-07 rule forbids and what lets a compiler flatten the
1065
+ * loop and cost every arm. So the row is clamped to a valid one, the sample always happens,
1066
+ * and the result is selected afterwards with a \`mix\` on a compare — which is arithmetic
1067
+ * rather than control flow.
1068
+ *
1069
+ * **What it costs** is one texture fetch per light per fragment on every scene, whether or
1070
+ * not anything loaded a profile. **What would make it wrong** is a scene where that fetch is
1071
+ * measurable against the shadow taps in the same loop, which would be a scene with many
1072
+ * lights and no shadows at all — at which point the honest answer is a permutation flag, and
1073
+ * \`ARCHITECTURE.md\` section 1 has the arithmetic for whether it earns one.
1074
+ */
1075
+ float iesRow = max(iesProfile, 0.0);
1076
+ /* The direction the fixture throws toward this fragment, which both angles are measured on. */
1077
+ vec3 iesRay = -toLight / max(dist, 1e-4);
1078
+ /* The angle from the fixture's axis, as a fraction of the 0-to-180 arc every row spans. */
1079
+ float iesAngle = acos(clamp(dot(iesRay, lightDir), -1.0, 1.0));
1080
+ float iesU = iesAngle / PI_IES;
1081
+ /*
1082
+ * **The azimuth about the fixture's own axis**, behind a branch on a uniform so a scene whose
1083
+ * profiles are all axially symmetric pays none of this arithmetic. A \`uniform float\` is
1084
+ * provably uniform control flow — no wavefront can diverge on it — so this is not the
1085
+ * 2026-08-07 case, which is about a branch a compiler *cannot* prove uniform.
1086
+ *
1087
+ * The reference is orthogonalised against the aim rather than assumed perpendicular to it,
1088
+ * so a caller may hand the fixture's own forward; one parallel to the aim leaves nothing to
1089
+ * project and stays on the first plane rather than producing a NaN.
1090
+ */
1091
+ float iesTurns = 0.0;
1092
+ vec3 cookieTint = vec3(1.0);
1093
+ /*
1094
+ * **The fixture's own frame, built once and shared by the azimuth and the cookie**, because
1095
+ * both are asking the same question about a fixture — which way is up — and both read
1096
+ * \`uLightIesAxis\` to answer it. Behind a branch on two uniforms, so a scene using neither
1097
+ * builds no frame at all.
1098
+ */
1099
+ if (uIesPlaneCount > 1.0 || uCookieTiles > 0.0) {
1100
+ vec3 iesRef = iesAxis - lightDir * dot(iesAxis, lightDir);
1101
+ float iesRefLen = length(iesRef);
1102
+ if (iesRefLen > 1e-5) {
1103
+ iesRef /= iesRefLen;
1104
+ vec3 iesBinormal = cross(lightDir, iesRef);
1105
+ if (uIesPlaneCount > 1.0) {
1106
+ /* \`atan\` answers -PI..PI and \`fract\` of a negative wraps forward, so this is 0..1
1107
+ across the whole turn without a branch on the sign. */
1108
+ iesTurns = fract(atan(dot(iesRay, iesBinormal), dot(iesRay, iesRef)) / TAU_IES);
1109
+ }
1110
+ if (uCookieTiles > 0.0) {
1111
+ /*
1112
+ * **A perspective projection onto the plane one unit down the aim**, which is what a
1113
+ * fixture actually throws: the mask grows with distance rather than being pasted on
1114
+ * at a fixed size.
1115
+ *
1116
+ * Scaled by the *outer cone's* tangent so a cookie fills whatever cone it is given —
1117
+ * a mask drawn once works in a narrow spot and a wide one. A light with no cone
1118
+ * carries a cosine of -2 and the \`max\` below turns that into zero rather than a NaN;
1119
+ * such a light also carries no cookie, so the result is discarded either way.
1120
+ */
1121
+ float cookieAlong = max(dot(iesRay, lightDir), 1e-4);
1122
+ float cosOuter = lightCone.y;
1123
+ float tanOuter = sqrt(max(1.0 - cosOuter * cosOuter, 0.0)) / max(cosOuter, 1e-3);
1124
+ vec2 fromAxis =
1125
+ vec2(dot(iesRay, iesRef), dot(iesRay, iesBinormal)) / cookieAlong;
1126
+ vec2 cookieUv = fromAxis / max(tanOuter, 1e-3) * 0.5 + 0.5;
1127
+ /*
1128
+ * Clamped inside the tile rather than wrapped: past the cone edge the cone term is
1129
+ * already zero, so what matters is only that the sample cannot reach the neighbouring
1130
+ * cookie — which a bilinear fetch at a seam otherwise does.
1131
+ */
1132
+ float cookieTile = max(cookie, 0.0);
1133
+ vec2 cookieAt = vec2(
1134
+ (cookieTile + clamp(cookieUv.x, COOKIE_INSET, 1.0 - COOKIE_INSET)) / uCookieTiles,
1135
+ clamp(cookieUv.y, COOKIE_INSET, 1.0 - COOKIE_INSET));
1136
+ /* Unconditional and \`textureLod\`, and selected with a \`mix\` afterwards: the light
1137
+ loop is not provably uniform control flow — the clustered arm reads its index from a
1138
+ texture — so a branch on the *light's* cookie index around a sample is what the
1139
+ 2026-08-07 rule forbids. */
1140
+ vec3 cookieSample = textureLod(uCookieAtlas, cookieAt, 0.0).rgb;
1141
+ cookieTint = mix(vec3(1.0), cookieSample, step(0.0, cookie));
1142
+ }
1143
+ }
1144
+ }
1145
+ /*
1146
+ * **One fetch, and the atlas's own linear filter does the blend between planes.**
1147
+ *
1148
+ * A profile occupies \`planes + 1\` rows with the last repeating the first, so a fractional
1149
+ * row inside a profile is exactly the blend wanted and the wrap from the last plane back to
1150
+ * the first is a blend into that repeat rather than a bleed into the next profile. The
1151
+ * version this replaced took two explicit fetches and a \`mix\`, and cost **18,315 gzipped
1152
+ * bytes on \`core-only\`** across the sixteen fragment permutations.
1153
+ */
1154
+ float iesBase = iesRow * (uIesPlaneCount + 1.0);
1155
+ float iesV = uIesAtlasRows <= 1.0
1156
+ ? 0.5
1157
+ : (iesBase + iesTurns * uIesPlaneCount + 0.5) / uIesAtlasRows;
1158
+ float iesGain = textureLod(uIesAtlas, vec2(iesU, iesV), 0.0).r;
1159
+ float photometric = mix(1.0, iesGain, step(0.0, iesProfile));
1160
+
1161
+ float shape = (uLightFalloff == 1 ? falloff : falloff * falloff) * coneFalloff * photometric;
1162
+ /*
1163
+ * The cookie tints rather than scales, because a mask may be coloured — a stained window is
1164
+ * the case that makes the difference — and a greyscale one is exactly a scale. Applied to
1165
+ * the light's colour rather than to \`shape\`, which is a scalar and could not carry it.
1166
+ */
1167
+ lightColor *= cookieTint;
1168
+ /*
1169
+ * **A metal has no diffuse term, and this lamp kept giving it one.**
1170
+ *
1171
+ * The sun's diffuse has been multiplied by \`(1 - metal)\` since metalness arrived. The lamps'
1172
+ * never was, and it is added *after* the environment blend, so it could not even be dimmed
1173
+ * by the reflection standing in front of it: every point light in a scene laid a full
1174
+ * Lambertian wash over a surface that physically reflects none of it, on top of everything
1175
+ * else the surface showed.
1176
+ *
1177
+ * **That wash is what a metal looked like.** A broad, soft, view-independent term is the
1178
+ * definition of matte, and in a room lit by its own fixtures it is most of the pixel — which
1179
+ * is why a chromed subject read as painted plastic in a lamp-lit hall, a garage and a night
1180
+ * street alike, and why raising what it reflected by four times moved almost nothing. The
1181
+ * reflection was not missing; it was being drowned.
1182
+ *
1183
+ * The light is not lost. What a metal does with a lamp arrives through the highlight below,
1184
+ * which is already metal-aware, and through the room it mirrors, which is where a lamp's
1185
+ * energy belongs on a metal.
1186
+ *
1187
+ * This was written once before and taken back out, because removing it alone made a metal
1188
+ * darker with nothing to replace what it lost. What replaces it is \`uEnvironmentGain\`: a
1189
+ * consumer whose rooms are darker than the surfaces standing in them can raise what those
1190
+ * surfaces reflect, rather than lighting a metal in a way metals are not lit.
1191
+ *
1192
+ * At metal 0 this is one multiply by 1.0 away from the expression it replaces.
1193
+ */
1194
+ lit += albedo * lightColor * ndl * shape * shaded * lightWeight * (1.0 - metal);
1195
+
1196
+ /*
1197
+ * A lamp reflected in a polished surface, which point lights could not do at all.
1198
+ *
1199
+ * The specular term used to answer only to the dominant directional source, so a world
1200
+ * with no sun in it had no highlights in it either. That is fine outdoors and wrong
1201
+ * everywhere else: a waxed floor under a strip light reflects the *strip*, and it is
1202
+ * most of what tells you the floor is polished rather than merely pale. An interior lit
1203
+ * entirely by its own fixtures had no way to say so.
1204
+ *
1205
+ * Gated on vSpecular, which almost nothing carries, so the rest of the world pays one
1206
+ * compare per light. Shares the sun's exponent and the same shadow and weight terms, so
1207
+ * a highlight cannot outlive the light casting it or survive a light fading out.
1208
+ */
1209
+ /*
1210
+ * The metal half is there because vSpecular defaults to 0 and every mesh meshBuilder builds
1211
+ * takes that default, so a metal gated on the attribute alone would take no highlight from
1212
+ * any lamp in the scene. The condition is the old one exactly when metal is 0, so nothing
1213
+ * that binds no map takes a different branch than it took before.
1214
+ */
1215
+ if (vSpecular > 0.0 || metal > 0.0) {
1216
+ vec3 toEyeLamp = normalize(uCameraPos - vWorldPos);
1217
+ vec3 lampHalfway = normalize(toLight / max(dist, 1e-4) + toEyeLamp);
1218
+ /* The same grazing whitening the sun's highlight takes, and zero at metal 0. */
1219
+ vec3 lampSpec = mix(
1220
+ specColor,
1221
+ vec3(1.0),
1222
+ pow(1.0 - max(dot(toEyeLamp, lampHalfway), 0.0), 5.0) * metal
1223
+ );
1224
+ lit += lightColor * sphereLobe(max(dot(n, lampHalfway), 0.0), surfaceRoughness, lightSourceRadius, dist)
1225
+ * lampSpec * shape * shaded * lightWeight;
1226
+ }
1227
+ /*
1228
+ * The most any light in range shadows this point, for the emissive term below —
1229
+ * weighted, because this one is not a colour multiply and would otherwise ignore
1230
+ * the ramp entirely. A light arriving at zero brightness would still switch a
1231
+ * shadow onto the marked line it is arriving over, which is precisely the surface
1232
+ * it was first watched happening on.
1233
+ */
1234
+ lightShade = min(lightShade, mix(1.0, shaded, lightWeight));
1235
+ }
1236
+
1237
+ /*
1238
+ * **Rectangular area lights: an exact diffuse integral and an approximate specular.**
1239
+ *
1240
+ * The diffuse half is the closed form of the cosine-weighted integral over a polygon, which is
1241
+ * what a linearly transformed cosine reduces to when its matrix is the identity — so this is
1242
+ * not an approximation of it, it *is* it. The specular half is a representative point handed to
1243
+ * \`sphereLobe\`, which widens a lobe by the source's subtended angle and renormalises its
1244
+ * energy; that is the approximate half, and it diverges from a fitted LTC at grazing angles
1245
+ * where the real lobe stretches along the view. Both are stated at their definitions.
1246
+ *
1247
+ * **A loop over a count rather than a permutation**, and the arithmetic is on record: thirty
1248
+ * lines of shared shader cost 6,215 gzipped bytes across the sixteen permutations this session
1249
+ * and fifty cost 10,517, while the fifth flag clustered lighting nearly took would have cost
1250
+ * 196,910. Compiled in unconditionally this is about a twentieth of what permuting it costs,
1251
+ * and a count of zero is what makes it free at runtime.
1252
+ *
1253
+ * **It occludes, as of 2026-08-28.** It used to not, and the note here said so: the pool is
1254
+ * octahedral and centred on a point, and a rectangle is not one. What settled it is that a
1255
+ * rectangle *does* have a point — its centre — and that the only thing separating an area
1256
+ * shadow from a point one is where the filter's width comes from. So the bake is a point
1257
+ * light's bake and the filter is an ellipse the size of the emitter; see \`areaShadow\` for
1258
+ * what that is exact about and what it approximates. Off unless a rectangle asks, because
1259
+ * every scene that already declared one was authored against a light that passed through
1260
+ * stone.
1261
+ */
1262
+ for (int a = 0; a < MAX_AREA_LIGHTS; a++) {
1263
+ if (a >= uAreaLightCount) break;
1264
+ vec3 centre = uAreaLightPos[a];
1265
+ vec3 right = uAreaLightRight[a];
1266
+ vec3 up = uAreaLightUp[a];
1267
+ vec2 halfSize = uAreaLightSize[a];
1268
+
1269
+ /*
1270
+ * **Wound so a surface on the emitting side sees them counter-clockwise**, which is what
1271
+ * makes the form factor positive there. The natural order — minus, plus, plus, minus — winds
1272
+ * the other way, and every one-sided light then clamped to zero and the frame was black.
1273
+ */
1274
+ vec3 c0 = centre - right * halfSize.x + up * halfSize.y;
1275
+ vec3 c1 = centre + right * halfSize.x + up * halfSize.y;
1276
+ vec3 c2 = centre + right * halfSize.x - up * halfSize.y;
1277
+ vec3 c3 = centre - right * halfSize.x - up * halfSize.y;
1278
+
1279
+ /*
1280
+ * **The sign is the sidedness.** A one-sided rectangle takes the positive part and a
1281
+ * two-sided one the magnitude — no facing test, because the sign already answers it exactly
1282
+ * per fragment rather than once for the rectangle's centre.
1283
+ */
1284
+ float signedForm = quadFormFactor(n, vWorldPos, c0, c1, c2, c3);
1285
+ float form = mix(max(0.0, signedForm), abs(signedForm), uAreaLightTwoSided[a]);
1286
+ if (form <= 0.0) continue;
1287
+
1288
+ /*
1289
+ * **How much of the rectangle this fragment can actually see past whatever is in the way.**
1290
+ *
1291
+ * One octahedral layer baked from the rectangle's *centre*, filtered by an ellipse whose axes
1292
+ * are the rectangle's own — see \`areaShadow\`, which is where the emitter's shape enters.
1293
+ * Multiplies both halves below, because a surface in shadow is in shadow for its highlight
1294
+ * too: occluding only the diffuse term leaves a specular streak hanging in mid-air where the
1295
+ * caster is, which is the shape the water renderer's own notes call light passing through
1296
+ * stone.
1297
+ *
1298
+ * **Two layers, multiplied.** The static one holds the world and is baked once for a fixture
1299
+ * that does not move; the live one holds movers only and is refreshed every frame. Their
1300
+ * product is the composition, exactly as the point lights above compose theirs — a person
1301
+ * walking under a lit awning is in the live layer and the awning's posts are in the static
1302
+ * one, and neither map has to know about the other.
1303
+ *
1304
+ * **Not folded into \`lightShade\`**, which is what dims a self-lit surface when a lamp's
1305
+ * shadow crosses it. That term needs a per-light "is this light arriving" weight so a light
1306
+ * at zero brightness cannot switch a shadow onto a glowing inlay, and the point lights have
1307
+ * one in their own range ramp. An area light's \`form\` is an irradiance fraction rather than
1308
+ * a presence: it is 1 for a rectangle filling the sky and a few thousandths for a bright
1309
+ * panel across a room, so using it directly barely dims anything, and any threshold over it
1310
+ * is a constant nobody has measured. \`docs/IMPROVEMENTS.md\` carries the row.
1311
+ */
1312
+ float areaOccl = 1.0;
1313
+ #if POINT_SHADOWS
1314
+ /*
1315
+ * The sample position is pushed along the surface normal before the lookup, and it scales
1316
+ * with how grazing the light is — the same cure and the same expression the point path uses a
1317
+ * few hundred lines up. Depth bias alone cannot fix a grazing receiver without peter-panning,
1318
+ * because one shadow texel then covers a stretch of floor whose depth changes faster than any
1319
+ * constant. The direction to the rectangle's *centre* is what "grazing" means here, which is
1320
+ * the same centre the map was baked from.
1321
+ */
1322
+ vec3 areaCentreVec = centre - vWorldPos;
1323
+ float areaCentreDist = length(areaCentreVec);
1324
+ float areaCentreNdl = areaCentreDist > 1e-4
1325
+ ? max(dot(n, areaCentreVec / areaCentreDist), 0.0)
1326
+ : 0.0;
1327
+ float areaGrazing = 1.0 - areaCentreNdl;
1328
+ float areaNormalOffset = (0.06 + areaCentreDist * 0.05) * areaGrazing * areaGrazing + 0.02;
1329
+ vec3 areaShadowFrom = (vWorldPos + n * areaNormalOffset) - centre;
1330
+
1331
+ /*
1332
+ * \`-1\` is a rectangle with no image: one that declined to cast, one that named no range,
1333
+ * or one whose first bake has not landed yet. A layer index rather than a flag, for the
1334
+ * reason the point path's is one — GLSL ES cannot index a sampler array with a non-constant
1335
+ * expression, and a layer is an ordinary integer.
1336
+ */
1337
+ int areaLayer = uAreaShadowLayer[a];
1338
+ if (areaLayer >= 0) {
1339
+ areaOccl = mix(
1340
+ 1.0,
1341
+ areaShadow(
1342
+ uPointShadows,
1343
+ float(areaLayer),
1344
+ areaShadowFrom,
1345
+ uAreaShadowFar[a],
1346
+ uAreaShadowNear[a],
1347
+ right,
1348
+ up,
1349
+ halfSize
1350
+ ),
1351
+ uAreaShadowWeight[a]
1352
+ );
1353
+ }
1354
+ int areaLiveLayer = uLiveAreaShadowLayer[a];
1355
+ if (areaLiveLayer >= 0) {
1356
+ areaOccl *= mix(
1357
+ 1.0,
1358
+ areaShadow(
1359
+ uPointShadows,
1360
+ float(areaLiveLayer),
1361
+ areaShadowFrom,
1362
+ uLiveAreaShadowFar[a],
1363
+ uLiveAreaShadowNear[a],
1364
+ right,
1365
+ up,
1366
+ halfSize
1367
+ ),
1368
+ uLiveAreaShadowWeight[a]
1369
+ );
1370
+ }
1371
+ #endif
1372
+
1373
+ lit += albedo * uAreaLightColor[a] * form * (1.0 - metal) * areaOccl;
1374
+
1375
+ if (vSpecular > 0.0 || metal > 0.0) {
1376
+ /*
1377
+ * **The lobe integrated over the rectangle, rather than sampled at one point on it.**
1378
+ *
1379
+ * What stood here handed the closest point on the rectangle to \`sphereLobe\` and scaled it
1380
+ * by \`form\`, the *diffuse* form factor. Measured against a brute-force integral by
1381
+ * \`scripts/areaSpecular.mjs\`, that lost the reflection rather than blurring it: on
1382
+ * polished metal facing a softbox it returned 0.000233 where the integral is 0.9207, and
1383
+ * head-on on a rough surface it was 3.7 times too bright. Worst 287% either way; this is
1384
+ * 40.1% where the light is what the surface reflects and 79.0% for one overhead.
1385
+ *
1386
+ * \`quadCoverage\` says what fraction of the specular lobe the rectangle covers and the
1387
+ * environment BRDF says how much the surface returns, which is the same split the probe
1388
+ * path uses. **\`dfg.x + dfg.y\` and not \`envSpecularEnergy\`**, deliberately and against
1389
+ * the obvious argument: the compensation returns the multiply-scattered share, and every
1390
+ * direct lobe in this shader is single-scattering, so adding it here returns energy the
1391
+ * light never had. Measured at 211% against 40.1%.
1392
+ */
1393
+ vec3 toEyeArea = normalize(uCameraPos - vWorldPos);
1394
+ float coverage = quadCoverage(n, vWorldPos, toEyeArea, surfaceRoughness, c0, c1, c2, c3);
1395
+ if (coverage > 0.0) {
1396
+ vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), surfaceRoughness);
1397
+ vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);
1398
+ lit += areaSpec * uAreaLightColor[a] * coverage * areaOccl;
1399
+ }
1400
+ }
1401
+ }
1402
+
1403
+ /*
1404
+ * Self-illuminated geometry, dimmed where a lamp's shadow crosses it.
1405
+ *
1406
+ * Emission used to be added straight after the sun term and touched by nothing, so a
1407
+ * shadow could not land on a glowing surface at all — a character standing on the
1408
+ * terrace threw a shadow across the deck and the inlaid bands under it stayed at full
1409
+ * brightness, as though the shadow passed beneath them. Reported as the shadow going
1410
+ * *under* the inlaid markings rather than over them — and, narrowed down, as a fault
1411
+ * in the point lights' shadows specifically: a character's or a prop's, with the sun's
1412
+ * apparently unaffected.
1413
+ *
1414
+ * The sun is not affected because emission is gated on uNightFactor and so does not
1415
+ * exist by day, when the sun's shadow is the one being cast. At night the lamps are the
1416
+ * light and their occlusion is per-light inside the loop above, which the emissive term
1417
+ * never saw. Hence lightShade: the most any lamp in range shadows this point.
1418
+ *
1419
+ * Only a share of it, because none of these surfaces is a pure emitter — a lit inlay is
1420
+ * worked stone with light coming through it, and the stone is shadowed like everything
1421
+ * else. Taking all of it would darken a lamp head because something crossed in front of
1422
+ * it, which is a worse picture than the one being fixed.
1423
+ */
1424
+ /*
1425
+ * The sun and the moon shadow a glowing surface too.
1426
+ *
1427
+ * lightShade fixed this for lamps and stopped there, on the reasoning that
1428
+ * emission is gated on uNightFactor and so does not exist by day, when the sun's
1429
+ * shadow is the one being cast. That is true at noon and false at every hour that
1430
+ * matters: at dusk the bands are already lit and the sun is at its longest, and at
1431
+ * night the moon is the directional source and casts exactly the same way.
1432
+ *
1433
+ * So an emissive deck — emissive 0.55 — took a lamp post's shadow from a lamp and
1434
+ * not from the sky, and a post that threw a long shadow across the grass by day
1435
+ * threw none across the deck at night. Reported as self-shadowing that was present
1436
+ * under the sun and gone at night.
1437
+ *
1438
+ * Same share as the lamps get, and for the same reason: these surfaces are worked
1439
+ * stone with light coming through, not pure emitters, so taking all of it would
1440
+ * black out a lamp head because something crossed in front of it.
1441
+ */
1442
+ /*
1443
+ * What a surface emits is not always what it is made of.
1444
+ *
1445
+ * A negative component means no colour was named, and the term falls back to the albedo,
1446
+ * which is this engine's original behaviour and right for anything glowing because it is
1447
+ * hot or lit from within. Where a colour is named it replaces the albedo outright rather
1448
+ * than tinting it, because the point is a glow whose hue is independent of the paint
1449
+ * underneath: a ceiling emitting a dull warm haze over a pale panel cannot be expressed
1450
+ * by scaling the panel, and scaling it gets the brightness right and the hue wrong.
1451
+ */
1452
+ vec3 emissiveTint = vEmissiveColor.r < 0.0 ? albedo : vEmissiveColor;
1453
+ /*
1454
+ * Where the surface glows, from an image, or everywhere if it binds none.
1455
+ *
1456
+ * **A multiplier of exactly 1 when no map is bound**, so this line collapses to the arithmetic
1457
+ * it replaces bit for bit — which is what the zero-pixel gate on the published scenes is
1458
+ * measuring, none of them binding one.
1459
+ *
1460
+ * **It modulates the emission rather than creating it**, which is glTF's rule: emitted colour
1461
+ * is the factor times the texture. A mesh whose \`vEmissive\` is 0 stays dark however bright the
1462
+ * image, because the whole term is still multiplied by it. That is stated here as well as at
1463
+ * \`SurfaceMaterial.emissive\`, because this is where somebody debugging a model that will not
1464
+ * glow ends up looking.
1465
+ *
1466
+ * The branch is on a uniform, so the \`texture\` call sits in uniform control flow and needs no
1467
+ * explicit level — the 2026-08-07 rule is satisfied by the gate rather than by a \`textureLod\`.
1468
+ */
1469
+ vec3 emissiveMapped = vec3(1.0);
1470
+ if (uEmissiveMapEnabled != 0) {
1471
+ emissiveMapped = texture(uEmissiveMap, vUv).rgb * uEmissiveScale;
1472
+ }
1473
+ lit += emissiveTint * emissiveMapped * uEmissiveGain * vEmissive * uNightFactor
1474
+ * mix(1.0, min(lightShade, sunShade), EMISSIVE_SHADOW_SHARE);
1475
+
1476
+ #if NIGHT_EMISSIVE
1477
+ /*
1478
+ * And the same emission again, weighted by how far this surface faces away from the sun.
1479
+ *
1480
+ * uDirectionalDir points from the surface toward the source, so its negative dot with the
1481
+ * normal is how far into its own night this fragment is: 0 anywhere the sun can see and 1 at
1482
+ * the antisolar point. Added rather than mixed, and outside uNightFactor, because a body that
1483
+ * turns has a night side at every hour of whatever clock the world is keeping.
1484
+ *
1485
+ * Not shadowed by lightShade: a city is not switched off by a mountain standing in front of
1486
+ * the sun, and the term is already zero everywhere the sun reaches.
1487
+ *
1488
+ * **It is not free when it is off, and that is measured rather than assumed.** Held on the
1489
+ * gilded chamber with uNightEmissive at 0, against the build before this term existed: 110
1490
+ * pixels of 750,080 move, in scattered clusters of two to four, mean delta 17. Isolated by
1491
+ * elimination: the uniform declared and never used moves 0 pixels, the branch present with an
1492
+ * empty body moves 1, which is that scene's own floor, and the arithmetic above moves 110. So
1493
+ * it is the added instructions perturbing the rounding of shared subexpressions, and what
1494
+ * lands on screen is a shadow comparison sitting exactly on its boundary flipping sides.
1495
+ * Neither result is more correct than the other. Sharing the dot product above was tried as a
1496
+ * repair and changed nothing.
1497
+ */
1498
+ if (uNightEmissive > 0.0) {
1499
+ /* The same emission, so the same map: a night side is not a different material. */
1500
+ lit += emissiveTint * emissiveMapped * uNightEmissive * vEmissive * max(-sunDot, 0.0);
1501
+ }
1502
+ #endif
1503
+
1504
+ // Arrival glow: a world-space box lit independently of the clock, so
1505
+ // finishing a run reads the same at noon and at midnight. Costs one compare
1506
+ // per fragment and no extra draw call — the box is a uniform, not geometry.
1507
+ if (uHighlightGain > 0.0) {
1508
+ vec3 inside = step(uHighlightMin, vWorldPos) * step(vWorldPos, uHighlightMax);
1509
+ lit += albedo * uHighlightGain * inside.x * inside.y * inside.z;
1510
+ }
1511
+ }
1512
+
1513
+ /*
1514
+ * The medium: fog and the underwater tint, both — see \`uFogEnabled\`'s own comment for why
1515
+ * the two are folded together and what a draw that skips them gives up. Every other pass in
1516
+ * the renderer binds \`uUnderwaterFactor\` unconditionally so that crossing the surface moves
1517
+ * the whole scene together; this is the one draw where that is the caller's choice.
1518
+ */
1519
+ /*
1520
+ * Baked occlusion, over everything this surface contributes.
1521
+ *
1522
+ * **Deliberately the same scope as the screen-space AO the composite pass applies**, which
1523
+ * multiplies the whole frame including direct light and emissive — see rush.ts. The narrower
1524
+ * physical reading, ambient and reflection only, was considered and not taken: consistency with
1525
+ * the effect already shipping won over it.
1526
+ *
1527
+ * **The stated cost of that is a double count with the shadow maps.** Where a baked crevice and
1528
+ * a cast shadow fall on the same fragment, both darken it, and the ambient-only reading would
1529
+ * not have. If that ever becomes visible enough to report, narrowing this is the fix.
1530
+ *
1531
+ * Outside the lighting branch, so an unlit draw — uLightingEnabled at 0, which is
1532
+ * meshBasicMaterial — is occluded too. Before the fog block, because fog is between the eye and
1533
+ * the surface, and darkening it would darken the air in front of a crevice rather than the
1534
+ * crevice.
1535
+ *
1536
+ * Exactly 1.0 with no map bound, so this is a multiply by one on every published scene.
1537
+ */
1538
+ lit *= ormOcclusion;
1539
+
1540
+ float fog = 0.0;
1541
+ if (uFogEnabled != 0) {
1542
+ // Water is a camera medium, not a property of individual objects.
1543
+ vec3 waterTransmission = vec3(0.42) + uUnderwaterColor * 2.0;
1544
+ lit = mix(lit, lit * waterTransmission, uUnderwaterFactor * 0.55);
1545
+ fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);
1546
+ }
1547
+
1548
+ vec3 shaded = applyOutputTransform(mix(lit, mediumColor(), fog));
1549
+
1550
+ /*
1551
+ * **Refraction: the scene behind this surface, bent and absorbed.**
1552
+ *
1553
+ * The offset is along the surface normal in screen space, which is the cheap approximation every
1554
+ * real-time renderer ships rather than a ray traced through two interfaces of a solid. What it
1555
+ * cannot do is show anything the frame had not already drawn, so a pane at the edge of the frame
1556
+ * refracts what is beside it -- and the clamp below is what keeps that from being worse than
1557
+ * that. Without it a sample walks off the edge and wraps, which reads as a tear rather than as
1558
+ * an approximation.
1559
+ *
1560
+ * **Beer-Lambert, with the caller stating what survives one metre** rather than an absorption
1561
+ * coefficient: pow(tint, d) is exp(d * log(tint)), so a tint of one is clear glass with no
1562
+ * special case and there is no log of zero at the API to trap on. What it costs is that a caller
1563
+ * holding coefficients from a reference converts them once.
1564
+ *
1565
+ * **The path length divides by the view angle, and that is the whole reason this is not a
1566
+ * tint.** A slab seen edge-on is more glass than the same slab seen face-on, which is what makes
1567
+ * a glass edge go green while its middle stays clear; a tint multiplied straight in gives the
1568
+ * same colour at every angle. The clamp is a floor on the grazing case rather than a tolerance:
1569
+ * dot reaches zero at the silhouette, and an unclamped divide takes the pixel to infinity and
1570
+ * then to a NaN, which takes the fragment with it. Twenty thicknesses is where it caps, so a
1571
+ * medium that should go opaque at a grazing angle stops getting darker there.
1572
+ *
1573
+ * **It replaces the surface's own shading rather than mixing with it, and the strength is the
1574
+ * offset alone.** Mixing by the strength was tried first and is wrong twice over: the offset and
1575
+ * the amount of background shown are different quantities, so one scalar cannot be both -- a
1576
+ * plausible offset of 0.025 UV then meant a 2.5% blend, and a pane of glass drew as flat paint
1577
+ * over the thing it was supposed to be showing. Found by this row's own check, where every bar
1578
+ * behind the pane vanished.
1579
+ *
1580
+ * What replacing costs is the surface's own highlight: glass here shows the scene behind it and
1581
+ * not a specular of its own, because by this line the shaded colour has the lighting, the fog and
1582
+ * the tone curve already folded into it and there is no specular left to keep. A caller wanting a
1583
+ * glint draws a second, non-refracting pass over the same geometry. What would change it is
1584
+ * splitting the specular out before the fog, which is a larger change to the one shader every
1585
+ * draw uses.
1586
+ */
1587
+ if (uRefractStrength > 0.0) {
1588
+ vec3 refractN = normalize(vNormal);
1589
+ vec3 refractV = normalize(uCameraPos - vWorldPos);
1590
+ vec2 screenUv = gl_FragCoord.xy / vec2(textureSize(uRefractScene, 0));
1591
+ vec2 bent = clamp(screenUv + refractN.xy * uRefractStrength, vec2(0.0), vec2(1.0));
1592
+ vec3 behind = texture(uRefractScene, bent).rgb;
1593
+ float pathLength = uRefractThickness * vThickness / max(dot(refractN, refractV), 0.05);
1594
+ shaded = behind * pow(uRefractTint, vec3(pathLength));
1595
+ }
1596
+ /*
1597
+ * The per-vertex alpha lane, folded in here rather than given an expression of its own so the
1598
+ * order-independent branch below inherits it through the same local. 1.0 when absent.
1599
+ *
1600
+ * **It did not need MeshData.colors widened to four floats**, which is how the consumer who
1601
+ * asked for it had costed the change -- widening colors moves every mesh producer, the
1602
+ * container layout and every baked asset. Alpha is a factor on the draw's opacity, not a
1603
+ * component of its albedo, so it rides a lane instead and nothing existing moves.
1604
+ */
1605
+ float alpha = uOpacity * coverage * vAlpha;
1606
+
1607
+ /*
1608
+ * **Order-independent transparency, when this draw is accumulating into it.**
1609
+ *
1610
+ * The weight falls off with distance so a near layer counts for more than a far one, and it is
1611
+ * clamped at both ends: without a ceiling a fragment on the near plane takes a weight large
1612
+ * enough to swamp the others inside a 16-bit target, and without a floor a distant one rounds to
1613
+ * nothing and vanishes rather than fading. The same arithmetic is \`oitWeight\` in
1614
+ * \`orderIndependent.ts\`, where it is asserted against numbers.
1615
+ *
1616
+ * The output is premultiplied and weighted, against a blend of \`(ONE, ONE)\`, so the target ends
1617
+ * up holding the weighted sum of colour and the weighted sum of alpha. The revealage pass draws
1618
+ * the same geometry with this at 0, where the branch below is not taken at all.
1619
+ */
1620
+ if (uOitWeighted != 0.0) {
1621
+ float z = distance(vWorldPos, uCameraPos) / 200.0;
1622
+ float falloff = 0.03 / (1e-5 + z * z * z * z);
1623
+ float w = alpha * clamp(falloff, 1e-2, 3e3);
1624
+ outColor = vec4(shaded * alpha * w, alpha * w);
1625
+ return;
1626
+ }
1627
+
1628
+ outColor = vec4(shaded, alpha);
1629
+ }
1630
+ `;