@driftengine/core 4.8.7 → 4.10.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 (263) hide show
  1. package/CHANGELOG.json +123 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +11 -3
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/atmosphere.d.ts +13 -0
  10. package/dist/render/atmosphere.js +9 -1
  11. package/dist/render/backend/api.d.ts +19 -4
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/select.js +11 -5
  15. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  16. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  17. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  18. package/dist/render/backend/webgl2/instanced.js +13 -1
  19. package/dist/render/backend/webgl2/renderer.d.ts +180 -4
  20. package/dist/render/backend/webgl2/renderer.js +550 -40
  21. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  22. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  23. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  24. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  25. package/dist/render/backend/webgpu/buffers.d.ts +19 -4
  26. package/dist/render/backend/webgpu/buffers.js +18 -11
  27. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  28. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  29. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  30. package/dist/render/backend/webgpu/depthPass.js +49 -14
  31. package/dist/render/backend/webgpu/device.d.ts +7 -1
  32. package/dist/render/backend/webgpu/device.js +31 -3
  33. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  34. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  35. package/dist/render/backend/webgpu/flatPass.d.ts +100 -42
  36. package/dist/render/backend/webgpu/flatPass.js +191 -43
  37. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  38. package/dist/render/backend/webgpu/instanced.js +24 -1
  39. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  40. package/dist/render/backend/webgpu/materialStore.js +265 -0
  41. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  42. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  43. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  44. package/dist/render/backend/webgpu/postPass.js +16 -2
  45. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  46. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  47. package/dist/render/backend/webgpu/renderer.d.ts +283 -37
  48. package/dist/render/backend/webgpu/renderer.js +1363 -202
  49. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  50. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  51. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  52. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  53. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  54. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  55. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  56. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  57. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  58. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  59. package/dist/render/bloomChain.d.ts +3 -3
  60. package/dist/render/boneAnimation.d.ts +92 -0
  61. package/dist/render/boneAnimation.js +133 -0
  62. package/dist/render/compressedSource.d.ts +33 -13
  63. package/dist/render/compressedSource.js +70 -25
  64. package/dist/render/displayRange.d.ts +31 -0
  65. package/dist/render/displayRange.js +47 -0
  66. package/dist/render/filmicCurve.d.ts +74 -0
  67. package/dist/render/filmicCurve.js +203 -0
  68. package/dist/render/fog.d.ts +4 -1
  69. package/dist/render/fog.js +12 -2
  70. package/dist/render/frame/resources.d.ts +2 -2
  71. package/dist/render/frame/resources.js +6 -0
  72. package/dist/render/frameReflections.d.ts +40 -0
  73. package/dist/render/frameReflections.js +48 -0
  74. package/dist/render/glCompressed.js +83 -0
  75. package/dist/render/instances.d.ts +26 -0
  76. package/dist/render/lightBudget.d.ts +10 -0
  77. package/dist/render/lightBudget.js +10 -0
  78. package/dist/render/lightmap.d.ts +1 -1
  79. package/dist/render/mesh.d.ts +5 -0
  80. package/dist/render/mesh.js +6 -0
  81. package/dist/render/pass.d.ts +49 -0
  82. package/dist/render/pointLightSelection.d.ts +4 -4
  83. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  84. package/dist/render/reflectionSurfaceTargets.js +99 -0
  85. package/dist/render/renderQuality.d.ts +39 -8
  86. package/dist/render/renderQuality.js +4 -0
  87. package/dist/render/sceneTarget.d.ts +5 -0
  88. package/dist/render/sceneTarget.js +24 -1
  89. package/dist/render/screenSpaceReflection.d.ts +7 -1
  90. package/dist/render/screenSpaceReflection.js +5 -0
  91. package/dist/render/shaders/arcane.d.ts +1 -1
  92. package/dist/render/shaders/bolt.d.ts +1 -1
  93. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  94. package/dist/render/shaders/boneAnimation.js +63 -0
  95. package/dist/render/shaders/caustics.d.ts +1 -1
  96. package/dist/render/shaders/depth.d.ts +7 -0
  97. package/dist/render/shaders/depth.js +35 -10
  98. package/dist/render/shaders/film.d.ts +1 -1
  99. package/dist/render/shaders/filmLook.js +2 -1
  100. package/dist/render/shaders/fire.d.ts +1 -1
  101. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  102. package/dist/render/shaders/flat/glassTint.js +2 -0
  103. package/dist/render/shaders/flat/index.d.ts +16 -0
  104. package/dist/render/shaders/flat/index.js +33 -5
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +43 -5
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  109. package/dist/render/shaders/flat/overlay.js +117 -0
  110. package/dist/render/shaders/flat/pointShadow.js +3 -3
  111. package/dist/render/shaders/flat/preamble.js +38 -24
  112. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  113. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  114. package/dist/render/shaders/fog.d.ts +1 -1
  115. package/dist/render/shaders/fog.js +9 -2
  116. package/dist/render/shaders/fringe.d.ts +29 -0
  117. package/dist/render/shaders/fringe.js +55 -0
  118. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  125. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  126. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  127. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  128. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  130. package/dist/render/shaders/generated/flat.wgsl.d.ts +3254 -2920
  131. package/dist/render/shaders/generated/flat.wgsl.js +3292 -2957
  132. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  135. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  136. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  137. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  138. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  139. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  140. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  141. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  142. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  143. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  144. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  145. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  146. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  147. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  148. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  149. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  150. package/dist/render/shaders/line.d.ts +1 -1
  151. package/dist/render/shaders/outputTransform.d.ts +7 -0
  152. package/dist/render/shaders/outputTransform.js +28 -14
  153. package/dist/render/shaders/particle.d.ts +3 -3
  154. package/dist/render/shaders/rush.js +54 -11
  155. package/dist/render/shaders/scatter.d.ts +1 -1
  156. package/dist/render/shaders/smoke.d.ts +1 -1
  157. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  158. package/dist/render/shaders/ssrResolve.js +51 -0
  159. package/dist/render/shaders/ssrTrace.js +26 -0
  160. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  161. package/dist/render/shaders/vertexChannel.js +26 -0
  162. package/dist/render/shaders/water.d.ts +1 -1
  163. package/dist/render/ssrPass.d.ts +16 -0
  164. package/dist/render/ssrPass.js +106 -1
  165. package/dist/render/staticDraws.d.ts +47 -0
  166. package/dist/render/staticDraws.js +43 -0
  167. package/dist/render/surfaceOverlay.d.ts +132 -0
  168. package/dist/render/surfaceOverlay.js +85 -0
  169. package/dist/render/surfaceTexture.d.ts +57 -7
  170. package/dist/render/surfaceTexture.js +125 -17
  171. package/dist/render/temporalAa.d.ts +14 -0
  172. package/dist/render/temporalAa.js +20 -0
  173. package/dist/render/vertexDefaults.d.ts +17 -0
  174. package/dist/render/vertexDefaults.js +24 -0
  175. package/package.json +6 -6
  176. package/src/index.ts +24 -2
  177. package/src/render/ambientHarmonics.ts +1 -1
  178. package/src/render/areaLights.ts +4 -4
  179. package/src/render/atmosphere.ts +22 -1
  180. package/src/render/backend/api.ts +22 -3
  181. package/src/render/backend/createRenderer.ts +18 -1
  182. package/src/render/backend/select.ts +10 -5
  183. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  184. package/src/render/backend/webgl2/instanced.ts +20 -1
  185. package/src/render/backend/webgl2/renderer.ts +672 -37
  186. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  187. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  188. package/src/render/backend/webgpu/buffers.ts +42 -16
  189. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  190. package/src/render/backend/webgpu/depthPass.ts +51 -10
  191. package/src/render/backend/webgpu/device.ts +42 -3
  192. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  193. package/src/render/backend/webgpu/flatPass.ts +250 -40
  194. package/src/render/backend/webgpu/instanced.ts +32 -1
  195. package/src/render/backend/webgpu/materialStore.ts +276 -0
  196. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  197. package/src/render/backend/webgpu/postPass.ts +18 -0
  198. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  199. package/src/render/backend/webgpu/renderer.ts +1655 -230
  200. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  201. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  202. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  203. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  204. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  205. package/src/render/bloomChain.ts +3 -3
  206. package/src/render/boneAnimation.ts +223 -0
  207. package/src/render/compressedSource.ts +121 -29
  208. package/src/render/displayRange.ts +61 -0
  209. package/src/render/filmicCurve.ts +229 -0
  210. package/src/render/fog.ts +19 -3
  211. package/src/render/frame/resources.ts +6 -0
  212. package/src/render/frameReflections.ts +72 -0
  213. package/src/render/glCompressed.ts +83 -0
  214. package/src/render/instances.ts +27 -0
  215. package/src/render/lightBudget.ts +11 -0
  216. package/src/render/lightmap.ts +1 -1
  217. package/src/render/mesh.ts +6 -0
  218. package/src/render/pass.ts +53 -1
  219. package/src/render/pointLightSelection.ts +4 -4
  220. package/src/render/reflectionSurfaceTargets.ts +105 -0
  221. package/src/render/renderQuality.ts +44 -8
  222. package/src/render/sceneTarget.ts +30 -2
  223. package/src/render/screenSpaceReflection.ts +7 -1
  224. package/src/render/shaders/boneAnimation.ts +63 -0
  225. package/src/render/shaders/depth.ts +45 -10
  226. package/src/render/shaders/filmLook.ts +2 -1
  227. package/src/render/shaders/flat/glassTint.ts +5 -1
  228. package/src/render/shaders/flat/index.ts +49 -5
  229. package/src/render/shaders/flat/main.ts +43 -5
  230. package/src/render/shaders/flat/models.ts +1 -1
  231. package/src/render/shaders/flat/overlay.ts +117 -0
  232. package/src/render/shaders/flat/pointShadow.ts +3 -3
  233. package/src/render/shaders/flat/preamble.ts +38 -24
  234. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  235. package/src/render/shaders/fog.ts +9 -2
  236. package/src/render/shaders/fringe.ts +59 -0
  237. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  238. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  239. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  240. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  241. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  242. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  243. package/src/render/shaders/generated/flat.wgsl.ts +3292 -2957
  244. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  245. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  246. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  247. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  248. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  249. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  250. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  251. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  252. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  253. package/src/render/shaders/outputTransform.ts +30 -14
  254. package/src/render/shaders/rush.ts +54 -11
  255. package/src/render/shaders/ssrResolve.ts +52 -0
  256. package/src/render/shaders/ssrTrace.ts +26 -0
  257. package/src/render/shaders/vertexChannel.ts +27 -0
  258. package/src/render/ssrPass.ts +138 -1
  259. package/src/render/staticDraws.ts +95 -0
  260. package/src/render/surfaceOverlay.ts +195 -0
  261. package/src/render/surfaceTexture.ts +161 -27
  262. package/src/render/temporalAa.ts +20 -0
  263. package/src/render/vertexDefaults.ts +28 -0
@@ -15,4 +15,4 @@
15
15
  * draws.
16
16
  */
17
17
  export declare const LINE_VERT = "#version 300 es\nlayout(location = 0) in vec3 aFrom;\nlayout(location = 1) in vec3 aTo;\n/** Along the segment (0 or 1), and which side of it (-1 or +1). */\nlayout(location = 2) in vec2 aCorner;\n\nuniform mat4 uViewProj;\nuniform mat4 uModel;\nuniform vec3 uCameraPos;\n/** Half-width in metres, and how much of a pixel a far line must still cover. */\nuniform float uWidth;\nuniform float uMinWidthPerMetre;\n\nout float vSide;\nout vec3 vWorldPos;\n\nvoid main() {\n /*\n * Through the model matrix before anything else, unlike an arc, whose endpoints are\n * world-space by nature. A line is usually attached to something that moves, and the\n * alternative is making every caller transform its own points on the CPU each frame.\n *\n * The width is deliberately *not* scaled by the model. A stroke's weight is a property\n * of how it is drawn rather than of the geometry it follows, so a card that shrinks\n * keeps the same line weight, which is what a reader expects and what a designer means.\n */\n vec3 from = (uModel * vec4(aFrom, 1.0)).xyz;\n vec3 to = (uModel * vec4(aTo, 1.0)).xyz;\n\n vec3 mid = mix(from, to, aCorner.x);\n vec3 dir = to - from;\n float len = length(dir);\n dir = len > 1e-6 ? dir / len : vec3(0.0, 1.0, 0.0);\n\n vec3 view = mid - uCameraPos;\n float dist = length(view);\n view = dist > 1e-6 ? view / dist : vec3(0.0, 0.0, 1.0);\n\n /*\n * Across the segment and across the line of sight. Degenerate exactly when the segment\n * points at the viewer, where any perpendicular is as good as another, and where the\n * segment covers almost no screen anyway.\n */\n vec3 across = cross(dir, view);\n float span = length(across);\n across = span > 1e-4 ? across / span : normalize(cross(dir, vec3(0.0, 1.0, 0.0)) + vec3(1e-3));\n\n /*\n * A floor on the world-space width that grows with distance, so a stroke never thins\n * below a pixel. Under a pixel a thin line does not get fainter, it *flickers*, because\n * the rasteriser catches it on some frames and not others.\n */\n float halfWidth = max(uWidth, dist * uMinWidthPerMetre);\n vec3 world = mid + across * aCorner.y * halfWidth;\n\n vSide = aCorner.y;\n vWorldPos = world;\n gl_Position = uViewProj * vec4(world, 1.0);\n}\n";
18
- export declare const LINE_FRAG = "#version 300 es\nprecision highp float;\n\nin float vSide;\nin vec3 vWorldPos;\n\nuniform vec3 uColor;\nuniform float uOpacity;\nuniform vec3 uCameraPos;\n/**\n * How far in from the edge the stroke fades, as a fraction of its half-width. Zero is a\n * clean edge, one is a stroke that is soft all the way to its middle.\n */\nuniform float uSoftness;\n\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n /*\n * Coverage across the ribbon: one down the middle, falling to zero at both edges.\n *\n * The band is at least one pixel wide *in screen space*, which is what lets one shader\n * draw a stroke of any width at any distance without either aliasing into a staircase or\n * blurring into a smear. A constant band is tuned for one width and is wrong at the rest,\n * which is the same argument `SDF_TEXT_FRAG` makes for the same reason.\n */\n float edge = 1.0 - abs(vSide);\n float band = max(fwidth(vSide), uSoftness);\n float coverage = smoothstep(0.0, band, edge);\n\n /*\n * Fogged like geometry, which is where this parts company with `bolt.ts`. That shader\n * includes the same helpers and never calls them, because an arc is light arriving\n * rather than a surface losing some of itself to the air on the way over. A drawn line\n * is a thing in the world and recedes like one.\n */\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n vec3 color = mix(uColor, mediumColor(), fog);\n outColor = vec4(applyOutputTransform(color), coverage * uOpacity);\n}\n";
18
+ export declare const LINE_FRAG = "#version 300 es\nprecision highp float;\n\nin float vSide;\nin vec3 vWorldPos;\n\nuniform vec3 uColor;\nuniform float uOpacity;\nuniform vec3 uCameraPos;\n/**\n * How far in from the edge the stroke fades, as a fraction of its half-width. Zero is a\n * clean edge, one is a stroke that is soft all the way to its middle.\n */\nuniform float uSoftness;\n\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n /*\n * Coverage across the ribbon: one down the middle, falling to zero at both edges.\n *\n * The band is at least one pixel wide *in screen space*, which is what lets one shader\n * draw a stroke of any width at any distance without either aliasing into a staircase or\n * blurring into a smear. A constant band is tuned for one width and is wrong at the rest,\n * which is the same argument `SDF_TEXT_FRAG` makes for the same reason.\n */\n float edge = 1.0 - abs(vSide);\n float band = max(fwidth(vSide), uSoftness);\n float coverage = smoothstep(0.0, band, edge);\n\n /*\n * Fogged like geometry, which is where this parts company with `bolt.ts`. That shader\n * includes the same helpers and never calls them, because an arc is light arriving\n * rather than a surface losing some of itself to the air on the way over. A drawn line\n * is a thing in the world and recedes like one.\n */\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n vec3 color = mix(uColor, mediumColor(), fog);\n outColor = vec4(applyOutputTransform(color), coverage * uOpacity);\n}\n";
@@ -48,3 +48,10 @@ export declare const HIGHLIGHT_SHOULDER_GLSL = "\nvec3 highlightShoulder(vec3 c)
48
48
  * a branch that costs one compare.
49
49
  */
50
50
  export declare const OUTPUT_TRANSFORM_GLSL = "\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n";
51
+ /**
52
+ * The same, for the lit stage, whose two uniforms live in its material block: a draw that opts out
53
+ * of the grade sets them, so they change with the material and not with the pass. See
54
+ * `hoistUniformBlock` in `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs
55
+ * the same program it always has.
56
+ */
57
+ export declare const LIT_OUTPUT_TRANSFORM_GLSL = "\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform; // wgsl:material\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure; // wgsl:material\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n";
@@ -46,20 +46,8 @@ vec3 highlightShoulder(vec3 c) {
46
46
  return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);
47
47
  }
48
48
  `;
49
- /**
50
- * `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
51
- *
52
- * Declares its own two uniforms, so including it is the whole of what a fragment stage has to
53
- * do besides calling it. Both are ints and floats rather than defines because the choice is a
54
- * caller's, per draw, and a permutation per output transform would double a pipeline count for
55
- * a branch that costs one compare.
56
- */
57
- export const OUTPUT_TRANSFORM_GLSL = `
58
- /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
59
- uniform int uOutputTransform;
60
- /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
61
- uniform float uOutputExposure;
62
-
49
+ /** The fit and `applyOutputTransform`, below whichever declaration of the two uniforms. */
50
+ const OUTPUT_TRANSFORM_BODY_GLSL = `
63
51
  /*
64
52
  * ACES: the reference fit, not the cheap one.
65
53
  *
@@ -126,3 +114,29 @@ vec3 applyOutputTransform(vec3 c) {
126
114
  return linearToSrgb(c);
127
115
  }
128
116
  `;
117
+ /**
118
+ * `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
119
+ *
120
+ * Declares its own two uniforms, so including it is the whole of what a fragment stage has to
121
+ * do besides calling it. Both are ints and floats rather than defines because the choice is a
122
+ * caller's, per draw, and a permutation per output transform would double a pipeline count for
123
+ * a branch that costs one compare.
124
+ */
125
+ export const OUTPUT_TRANSFORM_GLSL = `
126
+ /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
127
+ uniform int uOutputTransform;
128
+ /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
129
+ uniform float uOutputExposure;
130
+ ${OUTPUT_TRANSFORM_BODY_GLSL}`;
131
+ /**
132
+ * The same, for the lit stage, whose two uniforms live in its material block: a draw that opts out
133
+ * of the grade sets them, so they change with the material and not with the pass. See
134
+ * `hoistUniformBlock` in `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs
135
+ * the same program it always has.
136
+ */
137
+ export const LIT_OUTPUT_TRANSFORM_GLSL = `
138
+ /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
139
+ uniform int uOutputTransform; // wgsl:material
140
+ /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
141
+ uniform float uOutputExposure; // wgsl:material
142
+ ${OUTPUT_TRANSFORM_BODY_GLSL}`;
@@ -47,7 +47,7 @@ export declare const PARTICLE_VERT = "#version 300 es\n/** Per-vertex: the quad
47
47
  * and unlit smoke in a world lit by fires is the tell that gives an effect away
48
48
  * fastest. A puff catching a flame's colour is most of what makes it belong.
49
49
  */
50
- export declare const PARTICLE_SMOKE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform float uTime;\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\nuniform vec3 uCameraPos;\nuniform int uNoiseOctaves;\n/** How strongly the noise eats into the puff. 0 is a plain soft ball. */\nuniform float uErosion;\n\n#define MAX_LIGHTS 16\nuniform int uLightCount;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightRadius[MAX_LIGHTS];\n// How present each light is, 0 to 1. See pointLightSelection.ts.\nuniform float uLightWeight[MAX_LIGHTS];\n\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\nfloat billow(vec2 p, float t, int octaves) {\n float v = valueNoise(p * 2.4 + vec2(0.0, -t * 0.5)) * 0.62;\n if (octaves == 1) return v / 0.62;\n v += valueNoise(p * 5.1 + vec2(t * 0.15, -t * 0.8)) * 0.38;\n return v / 1.0;\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n /*\n * A round, soft body first. 'smoothstep' rather than a hard disc: the edge is\n * the entire difference between a puff and a sprite, and a cut-off circle reads\n * as a decal however good the noise inside it is.\n */\n float r = length(vUv);\n float body = 1.0 - smoothstep(0.35, 1.0, r);\n if (body <= 0.0) discard;\n\n /*\n * Then eaten into by noise that drifts and coarsens as the puff ages, so it\n * dissipates by *breaking up* rather than by uniformly thinning. Uniform\n * thinning is what a fading sprite does, and it is what the old cube did.\n */\n float t = uTime * 0.55 + vSeed * 7.31;\n float n = billow(vUv * (0.9 + vAge * 1.4) + vec2(vSeed * 3.7, 0.0), t, uNoiseOctaves);\n float erode = mix(1.0, n, uErosion * (0.35 + vAge * 0.65));\n float mask = clamp(body * erode, 0.0, 1.0);\n // Feather the last of it out rather than letting a noise threshold flicker.\n mask *= smoothstep(0.0, 0.25, mask);\n if (mask <= 0.002) discard;\n\n vec3 n3 = normalize(vNormal);\n /*\n * Wrapped diffuse. Smoke transmits as well as reflects, so the terminator on a\n * puff sits well past 90\u00B0 \u2014 a hard 'max(dot, 0)' gives a lump with a black side,\n * which is exactly the \"coloured box\" failure the fidelity rules name.\n */\n float ndl = dot(n3, uDirectionalDir) * 0.5 + 0.5;\n vec3 lit = uAmbient + uDirectionalColor * ndl;\n\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uLightCount) break;\n vec3 toLight = uLightPos[i] - vWorldPos;\n float dist = length(toLight);\n float radius = uLightRadius[i];\n if (radius <= 0.0 || dist >= radius) continue;\n float falloff = 1.0 - dist / radius;\n float wrapped = dot(n3, toLight / max(dist, 1e-4)) * 0.5 + 0.5;\n lit += uLightColor[i] * falloff * falloff * wrapped * uLightWeight[i];\n }\n\n vec3 shaded = vColor * lit;\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n outColor = vec4(applyOutputTransform(mix(shaded, mediumColor(), fog)), mask * vAlpha);\n}\n";
50
+ export declare const PARTICLE_SMOKE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform float uTime;\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\nuniform vec3 uCameraPos;\nuniform int uNoiseOctaves;\n/** How strongly the noise eats into the puff. 0 is a plain soft ball. */\nuniform float uErosion;\n\n#define MAX_LIGHTS 16\nuniform int uLightCount;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightRadius[MAX_LIGHTS];\n// How present each light is, 0 to 1. See pointLightSelection.ts.\nuniform float uLightWeight[MAX_LIGHTS];\n\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\nfloat billow(vec2 p, float t, int octaves) {\n float v = valueNoise(p * 2.4 + vec2(0.0, -t * 0.5)) * 0.62;\n if (octaves == 1) return v / 0.62;\n v += valueNoise(p * 5.1 + vec2(t * 0.15, -t * 0.8)) * 0.38;\n return v / 1.0;\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n /*\n * A round, soft body first. 'smoothstep' rather than a hard disc: the edge is\n * the entire difference between a puff and a sprite, and a cut-off circle reads\n * as a decal however good the noise inside it is.\n */\n float r = length(vUv);\n float body = 1.0 - smoothstep(0.35, 1.0, r);\n if (body <= 0.0) discard;\n\n /*\n * Then eaten into by noise that drifts and coarsens as the puff ages, so it\n * dissipates by *breaking up* rather than by uniformly thinning. Uniform\n * thinning is what a fading sprite does, and it is what the old cube did.\n */\n float t = uTime * 0.55 + vSeed * 7.31;\n float n = billow(vUv * (0.9 + vAge * 1.4) + vec2(vSeed * 3.7, 0.0), t, uNoiseOctaves);\n float erode = mix(1.0, n, uErosion * (0.35 + vAge * 0.65));\n float mask = clamp(body * erode, 0.0, 1.0);\n // Feather the last of it out rather than letting a noise threshold flicker.\n mask *= smoothstep(0.0, 0.25, mask);\n if (mask <= 0.002) discard;\n\n vec3 n3 = normalize(vNormal);\n /*\n * Wrapped diffuse. Smoke transmits as well as reflects, so the terminator on a\n * puff sits well past 90\u00B0 \u2014 a hard 'max(dot, 0)' gives a lump with a black side,\n * which is exactly the \"coloured box\" failure the fidelity rules name.\n */\n float ndl = dot(n3, uDirectionalDir) * 0.5 + 0.5;\n vec3 lit = uAmbient + uDirectionalColor * ndl;\n\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uLightCount) break;\n vec3 toLight = uLightPos[i] - vWorldPos;\n float dist = length(toLight);\n float radius = uLightRadius[i];\n if (radius <= 0.0 || dist >= radius) continue;\n float falloff = 1.0 - dist / radius;\n float wrapped = dot(n3, toLight / max(dist, 1e-4)) * 0.5 + 0.5;\n lit += uLightColor[i] * falloff * falloff * wrapped * uLightWeight[i];\n }\n\n vec3 shaded = vColor * lit;\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n outColor = vec4(applyOutputTransform(mix(shaded, mediumColor(), fog)), mask * vAlpha);\n}\n";
51
51
  /**
52
52
  * Sparks and embers: additive, hot in the middle, and stretched along travel.
53
53
  *
@@ -55,7 +55,7 @@ export declare const PARTICLE_SMOKE_FRAG = "#version 300 es\nprecision highp flo
55
55
  * is what turned the first version into a grey cube at night. The core is allowed
56
56
  * past white so it reads as incandescent rather than as a pale dot.
57
57
  */
58
- export declare const PARTICLE_SPARK_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform float uTime;\nuniform vec3 uCameraPos;\n/** How far past white the centre may go. */\nuniform float uCoreGain;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\nfloat billow(vec2 p, float t, int octaves) {\n float v = valueNoise(p * 2.4 + vec2(0.0, -t * 0.5)) * 0.62;\n if (octaves == 1) return v / 0.62;\n v += valueNoise(p * 5.1 + vec2(t * 0.15, -t * 0.8)) * 0.38;\n return v / 1.0;\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n float r = length(vUv);\n if (r > 1.0) discard;\n\n /*\n * Two falloffs again \u2014 a tight core and a broad halo \u2014 because one gives either\n * a hard dot or a soft smudge. The halo is what makes a shower of embers read\n * as glowing air rather than as a scatter of points.\n */\n float core = pow(1.0 - clamp(r, 0.0, 1.0), 6.0);\n float halo = pow(1.0 - clamp(r, 0.0, 1.0), 1.6);\n\n /*\n * Embers flicker as they tumble and cool. Per-particle phase from the seed, so\n * a shower crackles instead of pulsing as one body.\n */\n float flick = 0.7 + 0.3 * valueNoise(vec2(uTime * 9.0 + vSeed * 13.0, vSeed));\n\n // Cooling: the last of a spark's life is dimmer as well as smaller.\n float cool = 1.0 - vAge * vAge;\n float energy = (halo * 0.55 + core * uCoreGain) * flick * cool * vAlpha;\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n energy *= 1.0 - fog;\n /*\n * Alpha 1, energy in the colour \u2014 see the note in 'bolt.ts'. Additive blending is\n * 'rgb * a', so carrying the energy in both channels squares it and quietly halves\n * every emissive effect in the game.\n */\n outColor = vec4(applyOutputTransform(vColor * energy), 1.0);\n}\n";
58
+ export declare const PARTICLE_SPARK_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform float uTime;\nuniform vec3 uCameraPos;\n/** How far past white the centre may go. */\nuniform float uCoreGain;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\nfloat billow(vec2 p, float t, int octaves) {\n float v = valueNoise(p * 2.4 + vec2(0.0, -t * 0.5)) * 0.62;\n if (octaves == 1) return v / 0.62;\n v += valueNoise(p * 5.1 + vec2(t * 0.15, -t * 0.8)) * 0.38;\n return v / 1.0;\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n float r = length(vUv);\n if (r > 1.0) discard;\n\n /*\n * Two falloffs again \u2014 a tight core and a broad halo \u2014 because one gives either\n * a hard dot or a soft smudge. The halo is what makes a shower of embers read\n * as glowing air rather than as a scatter of points.\n */\n float core = pow(1.0 - clamp(r, 0.0, 1.0), 6.0);\n float halo = pow(1.0 - clamp(r, 0.0, 1.0), 1.6);\n\n /*\n * Embers flicker as they tumble and cool. Per-particle phase from the seed, so\n * a shower crackles instead of pulsing as one body.\n */\n float flick = 0.7 + 0.3 * valueNoise(vec2(uTime * 9.0 + vSeed * 13.0, vSeed));\n\n // Cooling: the last of a spark's life is dimmer as well as smaller.\n float cool = 1.0 - vAge * vAge;\n float energy = (halo * 0.55 + core * uCoreGain) * flick * cool * vAlpha;\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n energy *= 1.0 - fog;\n /*\n * Alpha 1, energy in the colour \u2014 see the note in 'bolt.ts'. Additive blending is\n * 'rgb * a', so carrying the energy in both channels squares it and quietly halves\n * every emissive effect in the game.\n */\n outColor = vec4(applyOutputTransform(vColor * energy), 1.0);\n}\n";
59
59
  /**
60
60
  * Dust motes and drifting glints: unlit, alpha blended, and fogged only when the caller asks.
61
61
  *
@@ -79,7 +79,7 @@ export declare const PARTICLE_SPARK_FRAG = "#version 300 es\nprecision highp flo
79
79
  * option and stay unconditionally fogged, matching every particle drawn before this material
80
80
  * existed; see `ParticleBatchOptions.fog`.
81
81
  */
82
- export declare const PARTICLE_MOTE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform vec3 uCameraPos;\n/**\n * Whether the fragment stage mixes toward the medium's own colour at all. 0 \u2014 what a batch\n * gets by leaving ParticleBatchOptions.fog unset \u2014 leaves vColor untouched by distance, which\n * is what a caller reaching for this material over 'spark'/'smoke' is usually asking for: an\n * unlit point drawn exactly as its own colour says. 1 mixes toward mediumColor() by\n * mediumFog(), the same medium every other pass in the frame recedes into.\n */\nuniform int uFogEnabled;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n /*\n * One soft, round falloff \u2014 no erosion, no core past white, because this material draws\n * neither a body being eaten by noise nor a light source. Squared rather than linear so the\n * edge feathers instead of ramping, which is what keeps a field of these reading as dust\n * rather than as a scatter of hard-edged discs.\n */\n float r = length(vUv);\n float body = 1.0 - smoothstep(0.4, 1.0, r);\n if (body <= 0.0) discard;\n float alpha = body * body * vAlpha;\n if (alpha <= 0.002) discard;\n\n vec3 color = vColor;\n if (uFogEnabled != 0) {\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n color = mix(color, mediumColor(), fog);\n }\n /*\n * A real alpha blend (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) rather than spark's energy-in-colour\n * trick: this point genuinely occludes what little is behind it instead of adding light to it.\n */\n outColor = vec4(applyOutputTransform(color), alpha);\n}\n";
82
+ export declare const PARTICLE_MOTE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform vec3 uCameraPos;\n/**\n * Whether the fragment stage mixes toward the medium's own colour at all. 0 \u2014 what a batch\n * gets by leaving ParticleBatchOptions.fog unset \u2014 leaves vColor untouched by distance, which\n * is what a caller reaching for this material over 'spark'/'smoke' is usually asking for: an\n * unlit point drawn exactly as its own colour says. 1 mixes toward mediumColor() by\n * mediumFog(), the same medium every other pass in the frame recedes into.\n */\nuniform int uFogEnabled;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n /*\n * One soft, round falloff \u2014 no erosion, no core past white, because this material draws\n * neither a body being eaten by noise nor a light source. Squared rather than linear so the\n * edge feathers instead of ramping, which is what keeps a field of these reading as dust\n * rather than as a scatter of hard-edged discs.\n */\n float r = length(vUv);\n float body = 1.0 - smoothstep(0.4, 1.0, r);\n if (body <= 0.0) discard;\n float alpha = body * body * vAlpha;\n if (alpha <= 0.002) discard;\n\n vec3 color = vColor;\n if (uFogEnabled != 0) {\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n color = mix(color, mediumColor(), fog);\n }\n /*\n * A real alpha blend (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) rather than spark's energy-in-colour\n * trick: this point genuinely occludes what little is behind it instead of adding light to it.\n */\n outColor = vec4(applyOutputTransform(color), alpha);\n}\n";
83
83
  /**
84
84
  * A textured sprite: an image a consumer supplies, a cell of it at a time, at the colour and opacity
85
85
  * each particle carries — what another engine's sprite renderer draws, and the material the three
@@ -25,6 +25,8 @@ import { AUTO_EXPOSURE_GLSL } from './exposure.js';
25
25
  import { LOCAL_EXPOSURE_GLSL } from './localExposure.js';
26
26
  import { FILM_LOOK_GLSL } from './filmLook.js';
27
27
  import { HIGHLIGHT_SHOULDER_GLSL } from './outputTransform.js';
28
+ import { FILMIC_GLSL } from '../filmicCurve.js';
29
+ import { FRINGE_GLSL } from './fringe.js';
28
30
  export const RUSH_FRAG = `#version 300 es
29
31
  precision highp float;
30
32
  /*
@@ -62,6 +64,18 @@ uniform float uReach;
62
64
  */
63
65
  uniform int uOutputTransform;
64
66
  uniform float uOutputExposure;
67
+ /**
68
+ * The \`filmic\` curve's eight constants, solved once a curve by \`resolveFilmicCurve\`: (slope, black
69
+ * clip, white clip, toe scale) and (shoulder scale, toe match, straight match, shoulder match).
70
+ * Read only where \`uOutputTransform\` is 4.
71
+ */
72
+ uniform vec4 uFilmA;
73
+ uniform vec4 uFilmB;
74
+ /**
75
+ * The display's peak over paper white on a high dynamic range canvas, and 1 everywhere else: a
76
+ * value of 1 is the display's paper white, and only above 1 does anything here let light past it.
77
+ */
78
+ uniform float uDisplayHeadroom;
65
79
 
66
80
  /**
67
81
  * The frame's depth, and the matrix that carries a pixel back to where it was last frame.
@@ -89,6 +103,14 @@ uniform mat4 uReprojection;
89
103
  uniform float uMotionStrength;
90
104
  /** A ceiling on the smear, in UV, so a fast turn softens the frame rather than erasing it. */
91
105
  uniform float uMotionMax;
106
+ /**
107
+ * Each drawn object's own motion, where the frame keeps it: the reconstruction's motion target,
108
+ * whose texel is where a mover's surface was minus where it is, in this pass's uv, and a flag in the
109
+ * fourth channel above one half where a draw wrote it. \`uObjectMotion\` is 1 where it is bound and
110
+ * drawn this frame and 0 otherwise, when a placeholder is bound and the camera's motion alone blurs.
111
+ */
112
+ uniform sampler2D uMotion;
113
+ uniform float uObjectMotion;
92
114
 
93
115
  /**
94
116
  * Ambient occlusion, already estimated and blurred by its own pass. 1 is open, 0 is enclosed.
@@ -242,10 +264,12 @@ vec3 acesFilmic(vec3 x) {
242
264
  }
243
265
 
244
266
  ${HIGHLIGHT_SHOULDER_GLSL}
267
+ ${FILMIC_GLSL}
245
268
  vec3 grade(vec3 c) {
246
269
  if (uOutputTransform == 0) return c;
247
270
  if (uOutputTransform == 2) c = acesFilmic(c);
248
271
  if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);
272
+ if (uOutputTransform == 4) c = filmicCurve(c * uOutputExposure);
249
273
  return linearToSrgb(c);
250
274
  }
251
275
 
@@ -258,9 +282,16 @@ vec3 withBloom(vec3 c) {
258
282
  /** The grade, sampled after the curve and before the veil. See uGradeLut's own comment. */
259
283
  vec3 applyColourGrade(vec3 c) {
260
284
  if (uGradeStrength <= 0.0) return c;
261
- vec3 clamped = clamp(c, 0.0, 1.0);
285
+ /*
286
+ * **A table graded for paper white grades the colour and keeps the light above it** on a high
287
+ * range display: the pixel is brought under white by its brightest channel, graded, and taken
288
+ * back up by the same factor, so a lamp keeps its brightness and takes the look's colour. At a
289
+ * headroom of 1 the factor is exactly 1 and this is the grade it always was.
290
+ */
291
+ float over = uDisplayHeadroom > 1.0 ? max(max(c.r, max(c.g, c.b)), 1.0) : 1.0;
292
+ vec3 clamped = clamp(c / over, 0.0, 1.0);
262
293
  vec3 uvw = (clamped * (uGradeSize - 1.0) + 0.5) / uGradeSize;
263
- return mix(c, textureLod(uGradeLut, uvw, 0.0).rgb, uGradeStrength);
294
+ return mix(c, textureLod(uGradeLut, uvw, 0.0).rgb * over, uGradeStrength);
264
295
  }
265
296
 
266
297
  /** The veil, mixed in last of everything this pass does. See uVeilAlpha's own comment. */
@@ -290,14 +321,24 @@ vec3 withVeil(vec3 c) {
290
321
  * correctly, and a camera turning past a horizon *should* smear it.
291
322
  */
292
323
  vec3 cameraBlur(vec3 scene) {
293
- float depth = textureLod(uDepth, vUv, 0.0).r;
294
- vec4 clip = vec4(vUv * 2.0 - 1.0, ${glslSceneDepthToNdc('depth')}, 1.0);
295
- vec4 previous = uReprojection * clip;
296
- /* Behind the previous eye: there is no last-frame position to smear toward. */
297
- if (previous.w <= 0.0) return scene;
298
-
299
- vec2 wasUv = (previous.xy / previous.w) * 0.5 + 0.5;
300
- vec2 velocity = (vUv - wasUv) * uMotionStrength;
324
+ /*
325
+ * **A drawn object's own motion where it has one**: a kick's leg or a thrown body smears along
326
+ * its path while the stage standing still behind it does not, which the camera's motion alone
327
+ * cannot tell apart. Read unconditionally, the placeholder where there is no target.
328
+ */
329
+ vec4 moved = textureLod(uMotion, vUv, 0.0);
330
+ vec2 velocity;
331
+ if (uObjectMotion > 0.5 && moved.a > 0.5) {
332
+ velocity = -moved.xy * uMotionStrength;
333
+ } else {
334
+ float depth = textureLod(uDepth, vUv, 0.0).r;
335
+ vec4 clip = vec4(vUv * 2.0 - 1.0, ${glslSceneDepthToNdc('depth')}, 1.0);
336
+ vec4 previous = uReprojection * clip;
337
+ /* Behind the previous eye: there is no last-frame position to smear toward. */
338
+ if (previous.w <= 0.0) return scene;
339
+ vec2 wasUv = (previous.xy / previous.w) * 0.5 + 0.5;
340
+ velocity = (vUv - wasUv) * uMotionStrength;
341
+ }
301
342
 
302
343
  float distance = length(velocity);
303
344
  if (distance < 1e-4) return scene;
@@ -335,9 +376,11 @@ vec3 finish(vec3 light) {
335
376
  return withGrain(withVeil(applyColourGrade(grade(lensed))), gl_FragCoord.xy);
336
377
  }
337
378
 
379
+ ${FRINGE_GLSL}
338
380
  void main() {
339
381
  vec4 sampled = textureLod(uScene, vUv, 0.0);
340
- vec3 scene = sampled.rgb;
382
+ /* The lens's colour fringe first, on the scene as it arrived. See shaders/fringe.ts. */
383
+ vec3 scene = withFringe(sampled.rgb);
341
384
  if (uMotionStrength > 0.0) scene = cameraBlur(scene);
342
385
  /*
343
386
  * Defocus after the camera smear and before everything else, because a lens is the last thing
@@ -11,4 +11,4 @@ export declare const SCATTER_VERT = "#version 300 es\nlayout(location = 0) in ve
11
11
  * program is not obliged to read every attribute a VAO offers.
12
12
  */
13
13
  export declare const SCATTER_DEPTH_VERT = "#version 300 es\nlayout(location = 0) in vec3 aPosition;\nlayout(location = 3) in vec3 aInstancePos;\nlayout(location = 4) in vec2 aScaleYaw;\nlayout(location = 6) in vec3 aWindResponse;\n\nuniform mat4 uLightViewProj;\n\nuniform vec2 uWindDirection;\nuniform float uWindSpeed;\nuniform float uWindGust;\nuniform float uWindTime;\n/* Spatial frequency of the travelling gust, per metre. */\nuniform vec2 uWindSpatialPhase;\n/*\n * Where something heavy has just been: xyz world position, w how pressed, 0 unused.\n *\n * Uniforms rather than a texture or a re-upload, because this is the hottest shader\n * in the game and the effect is a metre wide. Eight of them is about two seconds of\n * trail at running pace. See TrampleField.\n */\nuniform vec4 uTrample[8];\n/* How far a press reaches, metres, and how much of a plant it takes away. */\nuniform float uTrampleRadius;\nuniform float uTrampleDepth;\n\n/* Yaw as (sin, cos). Computed once per vertex and shared by position and normal. */\nvec2 scatterYaw(float yaw) {\n return vec2(sin(yaw), cos(yaw));\n}\n\n/* The instance's local offset from its root, after scale, yaw, wind and trampling. */\nvec3 scatterOffset(\n vec3 position,\n vec3 instancePos,\n vec2 scaleYaw,\n vec3 windResponse,\n vec2 yawTrig\n) {\n float s = yawTrig.x;\n float c = yawTrig.y;\n\n vec3 local = position * scaleYaw.x;\n vec3 rotated = vec3(local.x * c + local.z * s, local.y, -local.x * s + local.z * c);\n\n /*\n * Wind phase comes from world position, so neighbouring plants are never in\n * lockstep and a gust visibly travels across a field instead of the whole\n * field pulsing at once. Direction and strength come from the shared wind \u2014\n * this adds spatial variation, never a second wind.\n *\n * Amplitude rises with the square of height, so the base stays planted and\n * only the tip travels. A linear falloff slides the whole plant sideways,\n * which reads as the ground moving.\n */\n float phase = dot(instancePos.xz, uWindSpatialPhase) + uWindTime + windResponse.z;\n float rooted = clamp(rotated.y * windResponse.x, 0.0, 1.0);\n float bend = (uWindSpeed + uWindGust * windResponse.y) * sin(phase) * rooted * rooted;\n rotated.xz += uWindDirection * bend;\n\n /*\n * Trampling: pressed toward the ground and pushed away from whatever pressed it.\n *\n * The strongest press wins rather than the sum. Two overlapping presses adding up\n * would drive a blade through the deck, and a character crossing their own trail is\n * exactly when that would happen.\n *\n * Direction comes from the *press*, so a plant leans away from where the foot was\n * instead of all of them lying the same way, which is what makes it read as\n * something having gone through rather than as a second wind.\n */\n float pressed = 0.0;\n vec2 away = vec2(0.0);\n for (int i = 0; i < 8; i++) {\n vec4 p = uTrample[i];\n if (p.w <= 0.0) continue;\n vec3 offset = instancePos - p.xyz;\n float dist = length(offset);\n if (dist >= uTrampleRadius) continue;\n float fade = 1.0 - dist / uTrampleRadius;\n float strength = p.w * fade * fade;\n if (strength <= pressed) continue;\n pressed = strength;\n // Degenerate at the exact centre, where any direction is as good as another.\n away = dist > 1e-4 ? offset.xz / dist : vec2(0.0);\n }\n if (pressed > 0.0) {\n float take = pressed * uTrampleDepth;\n // Height goes first \u2014 a flattened plant is shorter, and the fold outward is\n // what makes the loss of height read as bending rather than as sinking.\n rotated.y *= 1.0 - take;\n rotated.xz += away * take * rotated.y * 1.4;\n }\n\n return rotated;\n}\n\nout vec4 vLightPosition;\n\nvoid main() {\n vec3 rotated = scatterOffset(\n aPosition,\n aInstancePos,\n aScaleYaw,\n aWindResponse,\n scatterYaw(aScaleYaw.y)\n );\n vLightPosition = uLightViewProj * vec4(aInstancePos + rotated, 1.0);\n gl_Position = vLightPosition;\n}\n";
14
- export declare const SCATTER_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vColor;\nin vec3 vNormal;\nin vec3 vWorldPos;\n\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\nuniform vec3 uCameraPos;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\nvoid main() {\n vec3 n = normalize(vNormal);\n /*\n * Two-sided. A blade of grass is a flat sliver seen from both faces, and\n * lighting its back face as though it faced away leaves half of every field\n * black \u2014 the cheapest and most obvious foliage tell there is.\n */\n float ndl = abs(dot(n, uDirectionalDir));\n vec3 lit = vColor * (uAmbient + uDirectionalColor * ndl);\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n\n outColor = vec4(mix(lit, mediumColor(), fog), 1.0);\n}\n";
14
+ export declare const SCATTER_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vColor;\nin vec3 vNormal;\nin vec3 vWorldPos;\n\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\nuniform vec3 uCameraPos;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\nvoid main() {\n vec3 n = normalize(vNormal);\n /*\n * Two-sided. A blade of grass is a flat sliver seen from both faces, and\n * lighting its back face as though it faced away leaves half of every field\n * black \u2014 the cheapest and most obvious foliage tell there is.\n */\n float ndl = abs(dot(n, uDirectionalDir));\n vec3 lit = vColor * (uAmbient + uDirectionalColor * ndl);\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n\n outColor = vec4(mix(lit, mediumColor(), fog), 1.0);\n}\n";
@@ -5,4 +5,4 @@
5
5
  * Alpha-blended rather than additive: smoke occludes what is behind it, and
6
6
  * adding it would make it glow instead of darken.
7
7
  */
8
- export declare const SMOKE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nout vec4 outColor;\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\nfloat billow(vec2 p, float t) {\n float v = valueNoise(p * 2.2 + vec2(0.0, -t * 0.55)) * 0.55;\n if (uNoiseOctaves == 1) return v / 0.55;\n v += valueNoise(p * 4.7 + vec2(t * 0.12, -t * 0.9)) * 0.29;\n if (uNoiseOctaves == 2) return v / 0.84;\n return v + valueNoise(p * 9.3 + vec2(-t * 0.09, -t * 1.4)) * 0.16;\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n float t = uTime + vSeed * 53.0;\n float height = vUv.y;\n\n // Widens as it climbs: sample a compressed space low down, a stretched one up\n // top. Narrow at the bottom is what lets it sit inside the flame's tip.\n float spread = mix(0.5, 1.0, height);\n float centred = (vUv.x - 0.5) / spread + 0.5;\n\n // Soft horizontal falloff rather than a hard cut \u2014 a clipped edge here shows\n // the quad and is the single most obvious way smoke reads as fake.\n float sides = smoothstep(0.0, 0.55, 1.0 - clamp(abs(centred - 0.5) * 2.0, 0.0, 1.0));\n if (sides <= 0.001) discard;\n\n // Leans away over its height instead of rising ruler-straight.\n float drift = (valueNoise(vec2(t * 0.21, vSeed * 7.0)) - 0.5) * 0.6 * height * height;\n float n = billow(vec2(centred + drift, height), t);\n\n float density = n * sides * 1.35 - height * 0.88;\n if (density <= 0.02) discard;\n\n // Colour tells the story of the plume: ember-lit where it leaves the flame,\n // cooling through soot, then dispersing into the fog. Starting at flat grey\n // is what made it look like a separate object parked above the fire.\n vec3 col = mix(vec3(0.90, 0.42, 0.16), vec3(0.11, 0.10, 0.10), smoothstep(0.0, 0.34, height));\n vec3 atmosphereColor = mediumColor();\n col = mix(col, atmosphereColor, smoothstep(0.30, 0.95, height));\n\n // Fade in at the base so it emerges out of the flame, and out at the top so\n // it dissipates rather than ending.\n float emerge = smoothstep(0.0, 0.30, height);\n float dissipate = 1.0 - smoothstep(0.22, 0.75, height);\n\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, atmosphereColor, fog);\n float alpha = clamp(density * 0.95, 0.0, 1.0) * emerge * dissipate * 0.13;\n outColor = vec4(col, alpha * (1.0 - fog));\n}\n";
8
+ export declare const SMOKE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nout vec4 outColor;\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\nfloat billow(vec2 p, float t) {\n float v = valueNoise(p * 2.2 + vec2(0.0, -t * 0.55)) * 0.55;\n if (uNoiseOctaves == 1) return v / 0.55;\n v += valueNoise(p * 4.7 + vec2(t * 0.12, -t * 0.9)) * 0.29;\n if (uNoiseOctaves == 2) return v / 0.84;\n return v + valueNoise(p * 9.3 + vec2(-t * 0.09, -t * 1.4)) * 0.16;\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n float t = uTime + vSeed * 53.0;\n float height = vUv.y;\n\n // Widens as it climbs: sample a compressed space low down, a stretched one up\n // top. Narrow at the bottom is what lets it sit inside the flame's tip.\n float spread = mix(0.5, 1.0, height);\n float centred = (vUv.x - 0.5) / spread + 0.5;\n\n // Soft horizontal falloff rather than a hard cut \u2014 a clipped edge here shows\n // the quad and is the single most obvious way smoke reads as fake.\n float sides = smoothstep(0.0, 0.55, 1.0 - clamp(abs(centred - 0.5) * 2.0, 0.0, 1.0));\n if (sides <= 0.001) discard;\n\n // Leans away over its height instead of rising ruler-straight.\n float drift = (valueNoise(vec2(t * 0.21, vSeed * 7.0)) - 0.5) * 0.6 * height * height;\n float n = billow(vec2(centred + drift, height), t);\n\n float density = n * sides * 1.35 - height * 0.88;\n if (density <= 0.02) discard;\n\n // Colour tells the story of the plume: ember-lit where it leaves the flame,\n // cooling through soot, then dispersing into the fog. Starting at flat grey\n // is what made it look like a separate object parked above the fire.\n vec3 col = mix(vec3(0.90, 0.42, 0.16), vec3(0.11, 0.10, 0.10), smoothstep(0.0, 0.34, height));\n vec3 atmosphereColor = mediumColor();\n col = mix(col, atmosphereColor, smoothstep(0.30, 0.95, height));\n\n // Fade in at the base so it emerges out of the flame, and out at the top so\n // it dissipates rather than ending.\n float emerge = smoothstep(0.0, 0.30, height);\n float dissipate = 1.0 - smoothstep(0.22, 0.75, height);\n\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, atmosphereColor, fog);\n float alpha = clamp(density * 0.95, 0.0, 1.0) * emerge * dissipate * 0.13;\n outColor = vec4(col, alpha * (1.0 - fog));\n}\n";
@@ -13,3 +13,20 @@
13
13
  * The vertex stage is `FULLSCREEN_VERT`, shared with the occlusion pass, its blur and the rush.
14
14
  */
15
15
  export declare const SSR_RESOLVE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\n\n/** Premultiplied reflection in rgb, coverage in a. Cleared to zero: nothing found is nothing. */\nuniform sampler2D uSsrReflection;\n\nout vec4 fragColor;\n\nvoid main() {\n fragColor = textureLod(uSsrReflection, vUv, 0.0);\n}\n";
16
+ /**
17
+ * The resolve for a trace of the frame's materials: **the probe's share of each pixel swapped for
18
+ * what the ray found**, by the weight the lit stage reflected with.
19
+ *
20
+ * The frame holds `environment · tint · weight` of the probe (`flat/reflectionSurface.ts`), and a
21
+ * reflection found in the frame belongs there with the same tint and weight. So the frame gains the
22
+ * found colour times the tint, less the probe's share, both scaled by how far the hit is believed —
23
+ * nothing where nothing was found, and the probe kept wherever the ray left the frame. Added, against
24
+ * a blend of `(one, one)` on the colour and the scene's alpha kept, onto a half-float scene, where a
25
+ * negative addend is the subtraction it reads as.
26
+ *
27
+ * **Blurred by the surface's roughness**, twelve taps on two rings whose radius is the roughness
28
+ * times `uSsrBlur` texels: the trace reads the scene at one point, which is a mirror, and a satin
29
+ * panel reflects a smear. What it gives up is a lobe's true shape and its stretch toward a grazing
30
+ * view; what it keeps is that a rougher panel reflects a softer picture, and a mirror a sharp one.
31
+ */
32
+ export declare const SSR_MATERIAL_RESOLVE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\n\n/** The found colour times how far it is believed in rgb, the belief in a. */\nuniform sampler2D uSsrReflection;\n/** The environment as the frame shows it, the roughness in a. */\nuniform sampler2D uSsrProbeMap;\n/** What a found reflection is multiplied by to land where the environment did. */\nuniform sampler2D uSsrTintMap;\n/** x, y: one texel of the trace; z: the blur's radius at roughness 1, in texels. */\nuniform vec3 uSsrBlur;\n\nout vec4 fragColor;\n\nvoid main() {\n vec4 probe = textureLod(uSsrProbeMap, vUv, 0.0);\n vec3 tint = textureLod(uSsrTintMap, vUv, 0.0).rgb;\n vec4 traced = textureLod(uSsrReflection, vUv, 0.0);\n float radius = probe.a * uSsrBlur.z;\n if (radius >= 1.0) {\n vec4 sum = traced;\n for (int i = 0; i < 12; i++) {\n /* Six taps a ring, sixty degrees apart, the inner ring turned half a step. */\n float angle = float(i % 6) * 1.0471976 + (i < 6 ? 0.0 : 0.5235988);\n float reach = i < 6 ? radius : radius * 0.5;\n vec2 offset = vec2(cos(angle), sin(angle)) * reach * uSsrBlur.xy;\n sum += textureLod(uSsrReflection, vUv + offset, 0.0);\n }\n traced = sum / 13.0;\n }\n fragColor = vec4(traced.rgb * tint - traced.a * probe.rgb, 0.0);\n}\n";
@@ -26,3 +26,54 @@ void main() {
26
26
  fragColor = textureLod(uSsrReflection, vUv, 0.0);
27
27
  }
28
28
  `;
29
+ /**
30
+ * The resolve for a trace of the frame's materials: **the probe's share of each pixel swapped for
31
+ * what the ray found**, by the weight the lit stage reflected with.
32
+ *
33
+ * The frame holds `environment · tint · weight` of the probe (`flat/reflectionSurface.ts`), and a
34
+ * reflection found in the frame belongs there with the same tint and weight. So the frame gains the
35
+ * found colour times the tint, less the probe's share, both scaled by how far the hit is believed —
36
+ * nothing where nothing was found, and the probe kept wherever the ray left the frame. Added, against
37
+ * a blend of `(one, one)` on the colour and the scene's alpha kept, onto a half-float scene, where a
38
+ * negative addend is the subtraction it reads as.
39
+ *
40
+ * **Blurred by the surface's roughness**, twelve taps on two rings whose radius is the roughness
41
+ * times `uSsrBlur` texels: the trace reads the scene at one point, which is a mirror, and a satin
42
+ * panel reflects a smear. What it gives up is a lobe's true shape and its stretch toward a grazing
43
+ * view; what it keeps is that a rougher panel reflects a softer picture, and a mirror a sharp one.
44
+ */
45
+ export const SSR_MATERIAL_RESOLVE_FRAG = `#version 300 es
46
+ precision highp float;
47
+
48
+ in vec2 vUv;
49
+
50
+ /** The found colour times how far it is believed in rgb, the belief in a. */
51
+ uniform sampler2D uSsrReflection;
52
+ /** The environment as the frame shows it, the roughness in a. */
53
+ uniform sampler2D uSsrProbeMap;
54
+ /** What a found reflection is multiplied by to land where the environment did. */
55
+ uniform sampler2D uSsrTintMap;
56
+ /** x, y: one texel of the trace; z: the blur's radius at roughness 1, in texels. */
57
+ uniform vec3 uSsrBlur;
58
+
59
+ out vec4 fragColor;
60
+
61
+ void main() {
62
+ vec4 probe = textureLod(uSsrProbeMap, vUv, 0.0);
63
+ vec3 tint = textureLod(uSsrTintMap, vUv, 0.0).rgb;
64
+ vec4 traced = textureLod(uSsrReflection, vUv, 0.0);
65
+ float radius = probe.a * uSsrBlur.z;
66
+ if (radius >= 1.0) {
67
+ vec4 sum = traced;
68
+ for (int i = 0; i < 12; i++) {
69
+ /* Six taps a ring, sixty degrees apart, the inner ring turned half a step. */
70
+ float angle = float(i % 6) * 1.0471976 + (i < 6 ? 0.0 : 0.5235988);
71
+ float reach = i < 6 ? radius : radius * 0.5;
72
+ vec2 offset = vec2(cos(angle), sin(angle)) * reach * uSsrBlur.xy;
73
+ sum += textureLod(uSsrReflection, vUv + offset, 0.0);
74
+ }
75
+ traced = sum / 13.0;
76
+ }
77
+ fragColor = vec4(traced.rgb * tint - traced.a * probe.rgb, 0.0);
78
+ }
79
+ `;
@@ -73,6 +73,18 @@ uniform float uSsrThickness;
73
73
  uniform float uSsrSteps;
74
74
  /** How much of the frame each side is fade, as a fraction. */
75
75
  uniform float uSsrEdgeFade;
76
+ /**
77
+ * **1 where the trace is the frame's materials rather than a box's**: every pixel the lit stage
78
+ * left a reflection for reflects, by its own material, and the box, its axis and its strength are
79
+ * not read. See \`flat/reflectionSurface.ts\` for the two maps it is handed.
80
+ */
81
+ uniform float uSsrMaterial;
82
+ /** The roughness past which a pixel's reflection is the probes' alone, faded over the last quarter. */
83
+ uniform float uSsrMaxRoughness;
84
+ /** The environment as the frame shows it, and the surface's roughness in its alpha. */
85
+ uniform sampler2D uSsrProbeMap;
86
+ /** What a found reflection is multiplied by to land where the environment did. */
87
+ uniform sampler2D uSsrTintMap;
76
88
 
77
89
  out vec4 fragColor;
78
90
 
@@ -150,6 +162,20 @@ void main() {
150
162
  returned = clamp(uSsrStrength * dfg.x + dfg.y, 0.0, 1.0);
151
163
  }
152
164
  mask *= returned;
165
+ /*
166
+ * **Or the pixel's own material**, where the trace is the frame's: reflective wherever the lit
167
+ * stage left a reflection to swap (any tint above zero), and the probes' alone past the
168
+ * roughness ceiling. How much it returns is the tint, which the resolve multiplies by, so the
169
+ * mask here is only whether to march and how much to trust a hit.
170
+ */
171
+ if (uSsrMaterial > 0.5) {
172
+ vec3 tint = textureLod(uSsrTintMap, vUv, 0.0).rgb;
173
+ float roughness = textureLod(uSsrProbeMap, vUv, 0.0).a;
174
+ float reflective = max(max(tint.r, tint.g), tint.b) > 0.0 ? 1.0 : 0.0;
175
+ float ceiling = max(uSsrMaxRoughness, 1.0e-4);
176
+ mask = reflective * (1.0 - smoothstep(ceiling * 0.75, ceiling, roughness));
177
+ mask *= ${glslIsFarDepth('stored')} ? 0.0 : 1.0;
178
+ }
153
179
 
154
180
  vec3 found = vec3(0.0);
155
181
  float weight = 0.0;
@@ -18,6 +18,13 @@
18
18
  * 4.1 KB across five, so a `#if` here would buy nothing measurable and cost a variant axis.
19
19
  */
20
20
  export declare const CHANNEL_ATTRIBUTE = "\nlayout(location = 13) in vec4 aChannel;\n\n/*\n * The frame's wind, already converted by `resolveScatterDeform` \u2014 a normalised direction, metres\n * of bend, a gust amplitude and a scaled clock.\n *\n * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A\n * second conversion here would be a second wind however identical its inputs, and the failure is\n * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the\n * grass beneath it leans another, with no single element looking wrong. See \"One wind, sampled\n * once\" in AGENTS.md.\n */\nuniform vec2 uWindDirection;\nuniform float uWindSpeed;\nuniform float uWindGust;\nuniform float uWindTime;\n/* Spatial frequency of the travelling gust, per metre. */\nuniform vec2 uWindSpatialPhase;\n";
21
+ /**
22
+ * The same, for the lit stage, whose wind lives in its view block: the frame's and not the draw's,
23
+ * so a draw's own block does not change when the wind does. See `hoistUniformBlock` in
24
+ * `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs the same program it always
25
+ * has; the depth stage keeps the unmarked chunk, having no view block.
26
+ */
27
+ export declare const LIT_CHANNEL_ATTRIBUTE = "\nlayout(location = 13) in vec4 aChannel;\n\n/*\n * The frame's wind, already converted by `resolveScatterDeform` \u2014 a normalised direction, metres\n * of bend, a gust amplitude and a scaled clock.\n *\n * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A\n * second conversion here would be a second wind however identical its inputs, and the failure is\n * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the\n * grass beneath it leans another, with no single element looking wrong. See \"One wind, sampled\n * once\" in AGENTS.md.\n */\nuniform vec2 uWindDirection; // wgsl:view\nuniform float uWindSpeed; // wgsl:view\nuniform float uWindGust; // wgsl:view\nuniform float uWindTime; // wgsl:view\n/* Spatial frequency of the travelling gust, per metre. */\nuniform vec2 uWindSpatialPhase; // wgsl:view\n";
21
28
  /**
22
29
  * Where the wind puts a vertex, given how much of it that vertex takes.
23
30
  *