@driftengine/core 4.12.0 → 4.13.1

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 (207) hide show
  1. package/CHANGELOG.json +77 -0
  2. package/README.md +18 -9
  3. package/dist/geometry/meshBuilder.d.ts +10 -0
  4. package/dist/geometry/meshBuilder.js +11 -2
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +3 -2
  7. package/dist/math/color.d.ts +25 -0
  8. package/dist/math/color.js +37 -0
  9. package/dist/render/backend/webgl2/renderer.d.ts +17 -4
  10. package/dist/render/backend/webgl2/renderer.js +82 -47
  11. package/dist/render/backend/webgl2/skinScatter.d.ts +3 -1
  12. package/dist/render/backend/webgl2/skinScatter.js +5 -1
  13. package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
  14. package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
  15. package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
  16. package/dist/render/backend/webgpu/flockPass.d.ts +1 -1
  17. package/dist/render/backend/webgpu/giFieldPass.d.ts +3 -1
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +10 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +28 -11
  20. package/dist/render/backend/webgpu/panelPass.d.ts +1 -1
  21. package/dist/render/backend/webgpu/postPass.d.ts +2 -2
  22. package/dist/render/backend/webgpu/renderer.d.ts +19 -8
  23. package/dist/render/backend/webgpu/renderer.js +94 -38
  24. package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
  25. package/dist/render/backend/webgpu/skinScatterPass.d.ts +5 -1
  26. package/dist/render/backend/webgpu/skinScatterPass.js +9 -3
  27. package/dist/render/backend/webgpu/skyPass.d.ts +11 -1
  28. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  29. package/dist/render/backend/webgpu/windStreakPass.d.ts +1 -1
  30. package/dist/render/causticsRenderer.d.ts +3 -1
  31. package/dist/render/causticsRenderer.js +5 -1
  32. package/dist/render/decalPass.d.ts +3 -1
  33. package/dist/render/decalPass.js +4 -1
  34. package/dist/render/flockRenderer.d.ts +1 -1
  35. package/dist/render/flockRenderer.js +4 -1
  36. package/dist/render/globalMediumPass.d.ts +3 -1
  37. package/dist/render/globalMediumPass.js +5 -1
  38. package/dist/render/gradeColor.d.ts +21 -0
  39. package/dist/render/gradeColor.js +64 -0
  40. package/dist/render/lightMatrix.d.ts +13 -0
  41. package/dist/render/lightMatrix.js +17 -0
  42. package/dist/render/passGrade.d.ts +13 -0
  43. package/dist/render/passGrade.js +47 -0
  44. package/dist/render/plumeRenderer.d.ts +1 -1
  45. package/dist/render/plumeRenderer.js +4 -1
  46. package/dist/render/renderQuality.d.ts +57 -18
  47. package/dist/render/renderQuality.js +3 -3
  48. package/dist/render/sceneTarget.d.ts +2 -0
  49. package/dist/render/sceneTarget.js +1 -0
  50. package/dist/render/sdfTextRenderer.d.ts +1 -1
  51. package/dist/render/sdfTextRenderer.js +1 -1
  52. package/dist/render/shaders/arcane.d.ts +1 -1
  53. package/dist/render/shaders/arcane.js +5 -1
  54. package/dist/render/shaders/bolt.d.ts +1 -1
  55. package/dist/render/shaders/bolt.js +4 -1
  56. package/dist/render/shaders/caustics.d.ts +1 -1
  57. package/dist/render/shaders/caustics.js +4 -1
  58. package/dist/render/shaders/decalProject.js +12 -2
  59. package/dist/render/shaders/film.d.ts +1 -1
  60. package/dist/render/shaders/film.js +22 -9
  61. package/dist/render/shaders/fire.d.ts +1 -1
  62. package/dist/render/shaders/fire.js +4 -1
  63. package/dist/render/shaders/flat/layerLooks.js +11 -19
  64. package/dist/render/shaders/flat/layered.js +33 -18
  65. package/dist/render/shaders/flock.d.ts +1 -1
  66. package/dist/render/shaders/flock.js +4 -1
  67. package/dist/render/shaders/generated/arcane.wgsl.d.ts +11 -1
  68. package/dist/render/shaders/generated/arcane.wgsl.js +11 -1
  69. package/dist/render/shaders/generated/bolt.wgsl.d.ts +12 -2
  70. package/dist/render/shaders/generated/bolt.wgsl.js +12 -2
  71. package/dist/render/shaders/generated/caustics.wgsl.d.ts +12 -2
  72. package/dist/render/shaders/generated/caustics.wgsl.js +12 -2
  73. package/dist/render/shaders/generated/decalProject.wgsl.d.ts +6 -1
  74. package/dist/render/shaders/generated/decalProject.wgsl.js +6 -1
  75. package/dist/render/shaders/generated/film.wgsl.d.ts +12 -2
  76. package/dist/render/shaders/generated/film.wgsl.js +12 -2
  77. package/dist/render/shaders/generated/fire.wgsl.d.ts +11 -1
  78. package/dist/render/shaders/generated/fire.wgsl.js +11 -1
  79. package/dist/render/shaders/generated/flat.wgsl.js +23 -23
  80. package/dist/render/shaders/generated/flock.wgsl.d.ts +12 -2
  81. package/dist/render/shaders/generated/flock.wgsl.js +12 -2
  82. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +12 -2
  83. package/dist/render/shaders/generated/globalMedium.wgsl.js +12 -2
  84. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +36 -16
  85. package/dist/render/shaders/generated/lightVolume.wgsl.js +39 -19
  86. package/dist/render/shaders/generated/panel.wgsl.d.ts +12 -2
  87. package/dist/render/shaders/generated/panel.wgsl.js +12 -2
  88. package/dist/render/shaders/generated/rush.wgsl.d.ts +31 -26
  89. package/dist/render/shaders/generated/rush.wgsl.js +31 -26
  90. package/dist/render/shaders/generated/scatter.wgsl.d.ts +12 -2
  91. package/dist/render/shaders/generated/scatter.wgsl.js +12 -2
  92. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +11 -1
  93. package/dist/render/shaders/generated/skinBlur.wgsl.js +11 -1
  94. package/dist/render/shaders/generated/sky.wgsl.d.ts +12 -2
  95. package/dist/render/shaders/generated/sky.wgsl.js +12 -2
  96. package/dist/render/shaders/generated/smoke.wgsl.d.ts +11 -1
  97. package/dist/render/shaders/generated/smoke.wgsl.js +11 -1
  98. package/dist/render/shaders/generated/text.wgsl.d.ts +11 -1
  99. package/dist/render/shaders/generated/text.wgsl.js +11 -1
  100. package/dist/render/shaders/generated/water.wgsl.d.ts +12 -2
  101. package/dist/render/shaders/generated/water.wgsl.js +12 -2
  102. package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +12 -2
  103. package/dist/render/shaders/generated/windStreaks.wgsl.js +12 -2
  104. package/dist/render/shaders/globalMedium.js +14 -1
  105. package/dist/render/shaders/gpudriven/present.wgsl.d.ts +15 -6
  106. package/dist/render/shaders/gpudriven/present.wgsl.js +71 -8
  107. package/dist/render/shaders/lightVolume.js +4 -1
  108. package/dist/render/shaders/line.d.ts +1 -1
  109. package/dist/render/shaders/outputTransform.d.ts +8 -2
  110. package/dist/render/shaders/outputTransform.js +18 -11
  111. package/dist/render/shaders/panel.d.ts +1 -1
  112. package/dist/render/shaders/panel.js +5 -1
  113. package/dist/render/shaders/particle.d.ts +3 -3
  114. package/dist/render/shaders/rush.js +8 -0
  115. package/dist/render/shaders/scatter.d.ts +1 -1
  116. package/dist/render/shaders/scatter.js +4 -1
  117. package/dist/render/shaders/sdfText.d.ts +1 -1
  118. package/dist/render/shaders/skinBlur.js +5 -1
  119. package/dist/render/shaders/sky.d.ts +1 -1
  120. package/dist/render/shaders/sky.js +5 -1
  121. package/dist/render/shaders/smoke.d.ts +1 -1
  122. package/dist/render/shaders/smoke.js +4 -1
  123. package/dist/render/shaders/text.d.ts +1 -1
  124. package/dist/render/shaders/text.js +4 -1
  125. package/dist/render/shaders/water.d.ts +1 -1
  126. package/dist/render/shaders/water.js +12 -2
  127. package/dist/render/shaders/windStreaks.d.ts +1 -1
  128. package/dist/render/shaders/windStreaks.js +4 -1
  129. package/dist/render/surfaceLayerLooks.d.ts +16 -4
  130. package/dist/render/surfaceLayerLooks.js +33 -14
  131. package/dist/render/surfaceLayers.d.ts +2 -2
  132. package/dist/render/textRenderer.d.ts +3 -1
  133. package/dist/render/textRenderer.js +7 -1
  134. package/dist/render/vertexDefaults.d.ts +1 -1
  135. package/dist/render/vertexDefaults.js +1 -1
  136. package/dist/render/waterRenderer.d.ts +1 -1
  137. package/dist/render/waterRenderer.js +4 -1
  138. package/dist/render/windStreakRenderer.d.ts +3 -1
  139. package/dist/render/windStreakRenderer.js +5 -1
  140. package/package.json +6 -6
  141. package/src/geometry/meshBuilder.ts +21 -2
  142. package/src/index.ts +11 -2
  143. package/src/math/color.ts +39 -0
  144. package/src/render/backend/webgl2/renderer.ts +134 -44
  145. package/src/render/backend/webgl2/skinScatter.ts +5 -0
  146. package/src/render/backend/webgpu/giFieldPass.ts +3 -1
  147. package/src/render/backend/webgpu/gpuDrivenPass.ts +29 -13
  148. package/src/render/backend/webgpu/renderer.ts +104 -37
  149. package/src/render/backend/webgpu/skinScatterPass.ts +17 -1
  150. package/src/render/causticsRenderer.ts +5 -0
  151. package/src/render/decalPass.ts +3 -0
  152. package/src/render/flockRenderer.ts +5 -0
  153. package/src/render/globalMediumPass.ts +10 -1
  154. package/src/render/gradeColor.ts +90 -0
  155. package/src/render/lightMatrix.ts +32 -0
  156. package/src/render/passGrade.ts +58 -0
  157. package/src/render/plumeRenderer.ts +5 -0
  158. package/src/render/renderQuality.ts +60 -21
  159. package/src/render/sceneTarget.ts +3 -0
  160. package/src/render/sdfTextRenderer.ts +1 -1
  161. package/src/render/shaders/arcane.ts +5 -1
  162. package/src/render/shaders/bolt.ts +4 -1
  163. package/src/render/shaders/caustics.ts +4 -1
  164. package/src/render/shaders/decalProject.ts +12 -2
  165. package/src/render/shaders/film.ts +22 -9
  166. package/src/render/shaders/fire.ts +4 -1
  167. package/src/render/shaders/flat/layerLooks.ts +11 -19
  168. package/src/render/shaders/flat/layered.ts +33 -18
  169. package/src/render/shaders/flock.ts +4 -1
  170. package/src/render/shaders/generated/arcane.wgsl.ts +11 -1
  171. package/src/render/shaders/generated/bolt.wgsl.ts +12 -2
  172. package/src/render/shaders/generated/caustics.wgsl.ts +12 -2
  173. package/src/render/shaders/generated/decalProject.wgsl.ts +6 -1
  174. package/src/render/shaders/generated/film.wgsl.ts +12 -2
  175. package/src/render/shaders/generated/fire.wgsl.ts +11 -1
  176. package/src/render/shaders/generated/flat.wgsl.ts +23 -23
  177. package/src/render/shaders/generated/flock.wgsl.ts +12 -2
  178. package/src/render/shaders/generated/globalMedium.wgsl.ts +12 -2
  179. package/src/render/shaders/generated/lightVolume.wgsl.ts +39 -19
  180. package/src/render/shaders/generated/panel.wgsl.ts +12 -2
  181. package/src/render/shaders/generated/rush.wgsl.ts +31 -26
  182. package/src/render/shaders/generated/scatter.wgsl.ts +12 -2
  183. package/src/render/shaders/generated/skinBlur.wgsl.ts +11 -1
  184. package/src/render/shaders/generated/sky.wgsl.ts +12 -2
  185. package/src/render/shaders/generated/smoke.wgsl.ts +11 -1
  186. package/src/render/shaders/generated/text.wgsl.ts +11 -1
  187. package/src/render/shaders/generated/water.wgsl.ts +12 -2
  188. package/src/render/shaders/generated/windStreaks.wgsl.ts +12 -2
  189. package/src/render/shaders/globalMedium.ts +14 -1
  190. package/src/render/shaders/gpudriven/present.wgsl.ts +72 -8
  191. package/src/render/shaders/lightVolume.ts +4 -1
  192. package/src/render/shaders/outputTransform.ts +19 -11
  193. package/src/render/shaders/panel.ts +5 -1
  194. package/src/render/shaders/rush.ts +8 -0
  195. package/src/render/shaders/scatter.ts +4 -1
  196. package/src/render/shaders/skinBlur.ts +5 -1
  197. package/src/render/shaders/sky.ts +5 -1
  198. package/src/render/shaders/smoke.ts +4 -1
  199. package/src/render/shaders/text.ts +4 -1
  200. package/src/render/shaders/water.ts +12 -2
  201. package/src/render/shaders/windStreaks.ts +4 -1
  202. package/src/render/surfaceLayerLooks.ts +49 -18
  203. package/src/render/surfaceLayers.ts +2 -2
  204. package/src/render/textRenderer.ts +6 -0
  205. package/src/render/vertexDefaults.ts +1 -1
  206. package/src/render/waterRenderer.ts +5 -0
  207. package/src/render/windStreakRenderer.ts +5 -0
@@ -35,4 +35,4 @@
35
35
  * ordinary one, and it wants the sheet laid just clear of the tiles.
36
36
  */
37
37
  export declare const CAUSTICS_VERT = "#version 300 es\nlayout(location = 0) in vec3 aRest;\nlayout(location = 1) in vec2 aLocal;\n/** x: resting height of the water below. y: spare. */\nlayout(location = 2) in vec2 aParams;\n\nuniform mat4 uViewProj;\n\nout vec3 vWorldPos;\nout vec2 vLocal;\nout vec2 vParams;\n\nvoid main() {\n vWorldPos = aRest;\n vLocal = aLocal;\n vParams = aParams;\n gl_Position = uViewProj * vec4(aRest, 1.0);\n}\n";
38
- export declare const CAUSTICS_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorldPos;\nin vec2 vLocal;\nin vec2 vParams;\n\nuniform vec3 uCameraPos;\nuniform vec2 uWindDir;\nuniform float uWaveGain;\nuniform float uTime;\n/** Toward the dominant source, as everywhere else in the engine. */\nuniform vec3 uLightDir;\n/** Colour of the light the water is reflecting. */\nuniform vec3 uTint;\nuniform float uStrength;\n/** How far from the water, on either side, the effect has died out entirely. */\nuniform float uMaxDrop;\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\nconst int GERSTNER_COUNT = 4;\n/*\n * direction.xy, steepness, wavelength. Amplitude is steepness/k, so these sum to\n * roughly a 1m swell \u2014 enough to read as a living surface, low enough that crests\n * never wash over an island whose deck sits ~1.8m above sea level.\n */\nconst vec4 GERSTNER_WAVES[4] = vec4[4](\n vec4( 1.0, 0.0, 0.115, 34.0),\n vec4( 0.6, 0.8, 0.100, 18.0),\n vec4(-0.7, 0.7, 0.080, 9.0),\n vec4( 0.2, -0.98, 0.060, 5.0)\n);\n\n/*\n * Steer each wave toward the wind, but only partway. A real sea is a dominant\n * swell plus cross-chop; rotating all four onto one bearing gives corduroy,\n * which reads as a material rather than as water.\n */\nvec2 gerstnerDirection(int i, vec2 windDir) {\n return normalize(mix(normalize(GERSTNER_WAVES[i].xy), windDir, 0.62));\n}\n\nfloat gerstnerWavenumber(int i) {\n return 6.28318530718 / GERSTNER_WAVES[i].w;\n}\n\n/** Deep-water phase in radians at a point on the resting plane. */\nfloat gerstnerPhase(float k, vec2 dir, vec2 p, float time) {\n float c = sqrt(9.81 / k); // deep-water phase speed\n return k * (dot(dir, p) - c * time);\n}\n\nstruct GerstnerSurface {\n /** Displacement from the resting plane \u2014 horizontal as well as vertical. */\n vec3 offset;\n vec3 normal;\n /** Height in units of steepness; positive near a crest. */\n float crest;\n};\n\nGerstnerSurface gerstnerSurface(vec2 p, float time, vec2 windDir, float gain) {\n GerstnerSurface s;\n s.offset = vec3(0.0);\n s.normal = vec3(0.0, 1.0, 0.0);\n s.crest = 0.0;\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n float a = steepness / k; // amplitude from steepness\n\n s.offset.x += dir.x * a * cos(f);\n s.offset.y += a * sin(f);\n s.offset.z += dir.y * a * cos(f);\n\n // Analytic normal: the partial derivatives of the displacement above.\n s.normal.x -= dir.x * steepness * cos(f);\n s.normal.z -= dir.y * steepness * cos(f);\n s.normal.y -= steepness * sin(f);\n\n s.crest += sin(f) * steepness;\n }\n return s;\n}\n\n/**\n * Second derivatives of the height field: (d2h/dx2, d2h/dz2, d2h/dxdz).\n *\n * How the surface *bends*, which is what decides where reflected light\n * converges. Analytic from the same sum, so it cannot disagree with the shape\n * the vertex stage displaced.\n */\nvec3 gerstnerCurvature(vec2 p, float time, vec2 windDir, float gain) {\n vec3 h = vec3(0.0);\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n // Second derivative of (steepness/k)*sin(f) along the wave's own bearing.\n float second = -steepness * k * sin(f);\n h.x += second * dir.x * dir.x;\n h.y += second * dir.y * dir.y;\n h.z += second * dir.x * dir.y;\n }\n return h;\n}\n\n\n/**\n * Shallowest the source may be and still be treated as a source.\n *\n * The slant path goes as 1/sin(elevation), so a light on the horizon would send\n * the sampled water position kilometres away and the pattern would smear into\n * noise. Clamping keeps a sunset dramatic instead of broken.\n */\nconst float MIN_ELEVATION = 0.16;\n\nvoid main() {\n float waterY = vParams.x;\n float drop = vWorldPos.y - waterY;\n\n /*\n * Everything is masked rather than branched on, and fwidth() below is why:\n * screen-space derivatives are undefined in non-uniform control flow, so a\n * discard above them would make the antialiasing of the caustic lines a\n * driver-dependent guess. Nothing is saved by branching either \u2014 a masked\n * fragment adds zero under additive blending.\n *\n * Either side of the surface is lit, and the distance from it is what matters\n * rather than the direction. This began as a soffit-only effect \u2014 light thrown\n * *up* onto the underside of a bridge \u2014 on the reasoning that below the water\n * there is nothing to light. That is only true when the water is a ceiling to\n * you. A pool floor, a flooded corridor, a tank with a lamp over it: the water\n * focuses light onto the bottom exactly as it does onto a soffit, and it is by\n * far the more common of the two.\n *\n * Only the surface's own thin band is excluded, where the sheet and the water\n * are coplanar and the pattern would be meaningless.\n *\n * Too far from it and the net has spread into nothing. And the sheet dissolves\n * at its own boundary, so a lit surface does not end in a lit rectangle.\n */\n float distanceFromWater = abs(drop);\n float clear = step(0.02, distanceFromWater);\n float height = 1.0 - smoothstep(uMaxDrop * 0.45, uMaxDrop, distanceFromWater);\n float rim = min(min(vLocal.x, 1.0 - vLocal.x), min(vLocal.y, 1.0 - vLocal.y));\n float edge = smoothstep(0.0, 0.12, rim);\n float reach = clear * height * edge;\n\n /*\n * Which water lights *this* point. For a flat surface the reflected ray leaves\n * along the mirrored light direction, so walking back from the ceiling gives\n * the water position and the path length in one step. This is why the net\n * slides along the ground as the sun moves rather than sitting under itself.\n */\n float elevation = max(uLightDir.y, MIN_ELEVATION);\n float slant = 1.0 / elevation;\n float d = drop * slant;\n vec2 water = vWorldPos.xz + uLightDir.xz * d;\n\n vec3 h = gerstnerCurvature(water, uTime, uWindDir, uWaveGain);\n float a = 1.0 - 2.0 * d * h.x;\n float b = 1.0 - 2.0 * d * h.y;\n float c = 2.0 * d * h.z;\n float det = a * b - c * c;\n\n /*\n * The bright net is where the determinant crosses zero, so its width is a\n * gradient question, not a constant. Widening the core by the screen-space\n * derivative antialiases it for free \u2014 without that, a line thinner than a\n * pixel strobes as the camera moves, which is the usual reason an effect like\n * this ends up looking like noise.\n */\n float soft = max(0.05, fwidth(det) * 1.6);\n float lines = soft / max(abs(det), soft);\n /*\n * Between the lines, the faint brightening of merely convergent water \u2014 and it has to\n * stay faint. At 0.45 it lit the *whole* sheet, and since the sheet hangs a few\n * centimetres under the soffit, the result read as a pale plate floating below the\n * bridge rather than as light on its underside \u2014 reported as being able to see through\n * the bridge ceiling, which looked glassy and transparent. The plate was the sheet.\n *\n * A tighter threshold and a fifth of the weight: the net is bright, everything between\n * it is almost nothing, and a soffit with no caustics on it is simply a soffit.\n */\n float sheen = clamp(1.0 / max(abs(det), 0.18) - 0.85, 0.0, 1.0);\n float caustic = clamp(lines * 0.9 + sheen * 0.09, 0.0, 1.6);\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n\n // Additive: this is light arriving at a surface that is already shaded.\n outColor = vec4(uTint * (caustic * uStrength * reach * (1.0 - fog)), 1.0);\n}\n";
38
+ export declare const CAUSTICS_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorldPos;\nin vec2 vLocal;\nin vec2 vParams;\n\nuniform vec3 uCameraPos;\nuniform vec2 uWindDir;\nuniform float uWaveGain;\nuniform float uTime;\n/** Toward the dominant source, as everywhere else in the engine. */\nuniform vec3 uLightDir;\n/** Colour of the light the water is reflecting. */\nuniform vec3 uTint;\nuniform float uStrength;\n/** How far from the water, on either side, the effect has died out entirely. */\nuniform float uMaxDrop;\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/*\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\n\nconst int GERSTNER_COUNT = 4;\n/*\n * direction.xy, steepness, wavelength. Amplitude is steepness/k, so these sum to\n * roughly a 1m swell \u2014 enough to read as a living surface, low enough that crests\n * never wash over an island whose deck sits ~1.8m above sea level.\n */\nconst vec4 GERSTNER_WAVES[4] = vec4[4](\n vec4( 1.0, 0.0, 0.115, 34.0),\n vec4( 0.6, 0.8, 0.100, 18.0),\n vec4(-0.7, 0.7, 0.080, 9.0),\n vec4( 0.2, -0.98, 0.060, 5.0)\n);\n\n/*\n * Steer each wave toward the wind, but only partway. A real sea is a dominant\n * swell plus cross-chop; rotating all four onto one bearing gives corduroy,\n * which reads as a material rather than as water.\n */\nvec2 gerstnerDirection(int i, vec2 windDir) {\n return normalize(mix(normalize(GERSTNER_WAVES[i].xy), windDir, 0.62));\n}\n\nfloat gerstnerWavenumber(int i) {\n return 6.28318530718 / GERSTNER_WAVES[i].w;\n}\n\n/** Deep-water phase in radians at a point on the resting plane. */\nfloat gerstnerPhase(float k, vec2 dir, vec2 p, float time) {\n float c = sqrt(9.81 / k); // deep-water phase speed\n return k * (dot(dir, p) - c * time);\n}\n\nstruct GerstnerSurface {\n /** Displacement from the resting plane \u2014 horizontal as well as vertical. */\n vec3 offset;\n vec3 normal;\n /** Height in units of steepness; positive near a crest. */\n float crest;\n};\n\nGerstnerSurface gerstnerSurface(vec2 p, float time, vec2 windDir, float gain) {\n GerstnerSurface s;\n s.offset = vec3(0.0);\n s.normal = vec3(0.0, 1.0, 0.0);\n s.crest = 0.0;\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n float a = steepness / k; // amplitude from steepness\n\n s.offset.x += dir.x * a * cos(f);\n s.offset.y += a * sin(f);\n s.offset.z += dir.y * a * cos(f);\n\n // Analytic normal: the partial derivatives of the displacement above.\n s.normal.x -= dir.x * steepness * cos(f);\n s.normal.z -= dir.y * steepness * cos(f);\n s.normal.y -= steepness * sin(f);\n\n s.crest += sin(f) * steepness;\n }\n return s;\n}\n\n/**\n * Second derivatives of the height field: (d2h/dx2, d2h/dz2, d2h/dxdz).\n *\n * How the surface *bends*, which is what decides where reflected light\n * converges. Analytic from the same sum, so it cannot disagree with the shape\n * the vertex stage displaced.\n */\nvec3 gerstnerCurvature(vec2 p, float time, vec2 windDir, float gain) {\n vec3 h = vec3(0.0);\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n // Second derivative of (steepness/k)*sin(f) along the wave's own bearing.\n float second = -steepness * k * sin(f);\n h.x += second * dir.x * dir.x;\n h.y += second * dir.y * dir.y;\n h.z += second * dir.x * dir.y;\n }\n return h;\n}\n\n\n/**\n * Shallowest the source may be and still be treated as a source.\n *\n * The slant path goes as 1/sin(elevation), so a light on the horizon would send\n * the sampled water position kilometres away and the pattern would smear into\n * noise. Clamping keeps a sunset dramatic instead of broken.\n */\nconst float MIN_ELEVATION = 0.16;\n\nvoid main() {\n float waterY = vParams.x;\n float drop = vWorldPos.y - waterY;\n\n /*\n * Everything is masked rather than branched on, and fwidth() below is why:\n * screen-space derivatives are undefined in non-uniform control flow, so a\n * discard above them would make the antialiasing of the caustic lines a\n * driver-dependent guess. Nothing is saved by branching either \u2014 a masked\n * fragment adds zero under additive blending.\n *\n * Either side of the surface is lit, and the distance from it is what matters\n * rather than the direction. This began as a soffit-only effect \u2014 light thrown\n * *up* onto the underside of a bridge \u2014 on the reasoning that below the water\n * there is nothing to light. That is only true when the water is a ceiling to\n * you. A pool floor, a flooded corridor, a tank with a lamp over it: the water\n * focuses light onto the bottom exactly as it does onto a soffit, and it is by\n * far the more common of the two.\n *\n * Only the surface's own thin band is excluded, where the sheet and the water\n * are coplanar and the pattern would be meaningless.\n *\n * Too far from it and the net has spread into nothing. And the sheet dissolves\n * at its own boundary, so a lit surface does not end in a lit rectangle.\n */\n float distanceFromWater = abs(drop);\n float clear = step(0.02, distanceFromWater);\n float height = 1.0 - smoothstep(uMaxDrop * 0.45, uMaxDrop, distanceFromWater);\n float rim = min(min(vLocal.x, 1.0 - vLocal.x), min(vLocal.y, 1.0 - vLocal.y));\n float edge = smoothstep(0.0, 0.12, rim);\n float reach = clear * height * edge;\n\n /*\n * Which water lights *this* point. For a flat surface the reflected ray leaves\n * along the mirrored light direction, so walking back from the ceiling gives\n * the water position and the path length in one step. This is why the net\n * slides along the ground as the sun moves rather than sitting under itself.\n */\n float elevation = max(uLightDir.y, MIN_ELEVATION);\n float slant = 1.0 / elevation;\n float d = drop * slant;\n vec2 water = vWorldPos.xz + uLightDir.xz * d;\n\n vec3 h = gerstnerCurvature(water, uTime, uWindDir, uWaveGain);\n float a = 1.0 - 2.0 * d * h.x;\n float b = 1.0 - 2.0 * d * h.y;\n float c = 2.0 * d * h.z;\n float det = a * b - c * c;\n\n /*\n * The bright net is where the determinant crosses zero, so its width is a\n * gradient question, not a constant. Widening the core by the screen-space\n * derivative antialiases it for free \u2014 without that, a line thinner than a\n * pixel strobes as the camera moves, which is the usual reason an effect like\n * this ends up looking like noise.\n */\n float soft = max(0.05, fwidth(det) * 1.6);\n float lines = soft / max(abs(det), soft);\n /*\n * Between the lines, the faint brightening of merely convergent water \u2014 and it has to\n * stay faint. At 0.45 it lit the *whole* sheet, and since the sheet hangs a few\n * centimetres under the soffit, the result read as a pale plate floating below the\n * bridge rather than as light on its underside \u2014 reported as being able to see through\n * the bridge ceiling, which looked glassy and transparent. The plate was the sheet.\n *\n * A tighter threshold and a fifth of the weight: the net is bright, everything between\n * it is almost nothing, and a soffit with no caustics on it is simply a soffit.\n */\n float sheen = clamp(1.0 / max(abs(det), 0.18) - 0.85, 0.0, 1.0);\n float caustic = clamp(lines * 0.9 + sheen * 0.09, 0.0, 1.6);\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n\n // Additive: this is light arriving at a surface that is already shaded.\n /* Graded where nothing after this will grade it, as every pass is, light added included. */\n outColor = vec4(applyOutputTransform(uTint * (caustic * uStrength * reach * (1.0 - fog))), 1.0);\n}\n";
@@ -1,5 +1,6 @@
1
1
  import { GERSTNER_GLSL } from './gerstner.js';
2
2
  import { FOG_GLSL } from './fog.js';
3
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
3
4
  /**
4
5
  * Light off water, projected onto what covers it.
5
6
  *
@@ -76,6 +77,7 @@ uniform float uMaxDrop;
76
77
  ${FOG_GLSL}
77
78
 
78
79
  out vec4 outColor;
80
+ ${OUTPUT_TRANSFORM_GLSL}
79
81
 
80
82
  ${GERSTNER_GLSL}
81
83
 
@@ -162,6 +164,7 @@ void main() {
162
164
  float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);
163
165
 
164
166
  // Additive: this is light arriving at a surface that is already shaded.
165
- outColor = vec4(uTint * (caustic * uStrength * reach * (1.0 - fog)), 1.0);
167
+ /* Graded where nothing after this will grade it, as every pass is, light added included. */
168
+ outColor = vec4(applyOutputTransform(uTint * (caustic * uStrength * reach * (1.0 - fog))), 1.0);
166
169
  }
167
170
  `;
@@ -23,6 +23,7 @@
23
23
  * the rule `AGENTS.md` states for `texture()` in a branch, arriving through the other door.
24
24
  */
25
25
  import { glslIsFarDepth } from '../depthConvention.js';
26
+ import { LINEAR_TO_SRGB_GLSL } from './outputTransform.js';
26
27
  export const DECAL_PROJECT_FRAG = `#version 300 es
27
28
  precision highp float;
28
29
 
@@ -61,7 +62,9 @@ uniform float uDecalOpacity;
61
62
  uniform float uDecalFacingCos;
62
63
  /** How much of the mark's radius is edge. */
63
64
  uniform float uDecalSoftness;
64
-
65
+ /** How the frame under the marks was graded, 0 where the resolve grades it. See renderQuality.ts. */
66
+ uniform int uOutputTransform;
67
+ ${LINEAR_TO_SRGB_GLSL}
65
68
  out vec4 fragColor;
66
69
 
67
70
  void main() {
@@ -120,6 +123,13 @@ void main() {
120
123
  mark *= ${glslIsFarDepth('stored')} ? 0.0 : 1.0;
121
124
 
122
125
  /* White is the identity of a multiply, so a pixel the projector missed changes nothing. */
123
- fragColor = vec4(mix(vec3(1.0), uDecalColor, mark * uDecalOpacity), 1.0);
126
+ vec3 multiply = mix(vec3(1.0), uDecalColor, mark * uDecalOpacity);
127
+ /*
128
+ * **The multiply as an encoded pixel takes it**, where the frame was graded before this: the
129
+ * receiver is a display value, and darkening its light by a factor is darkening the display value
130
+ * by that factor encoded — exact through the sRGB encode, whose power carries a product through,
131
+ * and close through a curve, which bends it. A frame graded at the resolve multiplies linear light.
132
+ */
133
+ fragColor = vec4(mix(multiply, linearToSrgb(multiply), float(uOutputTransform != 0)), 1.0);
124
134
  }
125
135
  `;
@@ -12,4 +12,4 @@
12
12
  * rainbow is generated rather than sampled.
13
13
  */
14
14
  export declare const FILM_VERT = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec3 aPosition;\nlayout(location = 1) in vec3 aNormal;\nlayout(location = 2) in vec3 aColor;\n/*\n * Coverage, on the attribute the mesh format calls emissive.\n *\n * A film has no use for self-illumination, and it very much needs an edge that\n * dissolves: the first oil was flat squares, and a hard straight edge is what made\n * it read as a decal dropped on the world rather than a puddle lying in it. One\n * float per vertex, 1 in the middle of a slick and 0 at its rim.\n */\nlayout(location = 3) in float aCoverage;\n\nuniform mat4 uViewProj;\n/* The mirrored camera's projection, so a film can sample the planar target the way water does. */\nuniform mat4 uReflectionViewProj;\n\nout vec3 vWorld;\nout vec3 vNormal;\nout vec3 vTint;\nout vec4 vReflectionClip;\nout float vCoverage;\n\nvoid main() {\n vWorld = aPosition;\n vNormal = normalize(aNormal);\n vTint = aColor;\n vCoverage = aCoverage;\n vReflectionClip = uReflectionViewProj * vec4(aPosition, 1.0);\n gl_Position = uViewProj * vec4(aPosition, 1.0);\n}\n";
15
- export declare const FILM_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorld;\nin vec3 vNormal;\nin vec3 vTint;\nin vec4 vReflectionClip;\n/*\n * The scene mirrored across the plane this film lies on, and how much of it to show.\n *\n * **Sheen is iridescence and this is the image**, which is the split a consumer asked for after\n * finding that raising sheen to make a road wetter only made it oilier. A wet street at night is\n * defined by everything above it appearing twice, and no amount of interference pattern is that.\n *\n * The projection is the water shader's, unchanged and deliberately so: reproducing it outside the\n * engine is the piece most likely to be subtly wrong and least likely to be noticed, since a\n * mirror off by a small skew looks like a wet road until somebody puts a straight line across it.\n */\nuniform sampler2D uReflectionMap;\nuniform int uReflectionEnabled;\nuniform float uReflectionStrength;\nin float vCoverage;\n\nuniform vec3 uCameraPos;\nuniform float uTime;\n/*\n * The mirrored pass clips at the water plane, and it does so **in the fragment\n * shader** rather than with gl_ClipDistance.\n *\n * That is not a style choice. Clip distances in WebGL2 need\n * EXT_clip_cull_distance both enabled on the context and declared with an\n * #extension line, and without the declaration the shader fails to *compile* \u2014\n * which, since programs are built when the renderer is constructed, takes the\n * whole boot with it and leaves a black screen. This shader had never been\n * compiled when it was written, so the mistake sat harmlessly in the file until\n * the day it was wired to something. Discarding is what flat.ts does, it needs\n * no extension, and a film is a thin translucent layer where the cost of a\n * discarded fragment is nothing.\n */\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\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/** How strong the sheen is. Zero leaves a plain dark film. */\nuniform float uSheen;\n\n/**\n * How rough the film's own surface is, 0 to 1, and how coarse that roughness is.\n *\n * **A film had exactly one finish before this: mirror.** That is right for standing water and\n * wrong for the thing this pass is most used for, because a wet road is not a mirror \u2014 it is\n * water lying *in* aggregate, and the stone underneath breaks the reflection into a smear. With\n * no way to say so, a patch of film over a textured road met it at a hard rim: mirror on one\n * side, stone on the other, with nothing able to cross it. Feathering the coverage cannot fix\n * that, and it was tried \u2014 the alpha was never what the eye was reading.\n *\n * So the reflection is displaced by a noise field rather than blurred. **Displaced, because a\n * blur is the wrong phenomenon**: a rough surface does not soften what it reflects, it scatters\n * it, and a lamp on wet tarmac smears into a streak rather than becoming a soft disc. It is also\n * what a blur cannot afford here \u2014 the mirror has no mip chain (see the `textureLod` note\n * below), so softening it honestly would cost several taps where this costs none.\n *\n * `uFilmRoughnessCycles` is bumps to the metre, and it is the same quantity\n * `Renderer.setSurfaceRelief` takes. **Matching it to the surface underneath is the point**:\n * given the same field at the same scale, the water is broken up by the aggregate that the dry\n * road beside it is showing, and the two stop being different materials.\n *\n * Zero is the mirror this pass has always been, exactly, so nothing that does not ask for\n * roughness renders differently.\n */\nuniform float uFilmRoughness;\nuniform float uFilmRoughnessCycles;\n\nout vec4 fragColor;\n\n/** A hue wheel, without a texture. Standard 6-segment ramp. */\nvec3 hue(float h) {\n h = fract(h) * 6.0;\n vec3 c = clamp(vec3(abs(h - 3.0) - 1.0, 2.0 - abs(h - 2.0), 2.0 - abs(h - 4.0)), 0.0, 1.0);\n return c;\n}\n\n/**\n * The same value noise `flat.ts` builds its relief from, and deliberately the same.\n *\n * Copied rather than shared because these two shaders have no common include beyond the fog, and\n * a film that used a *different* field would defeat the whole purpose of taking a cycle count:\n * the water would be broken up by one pattern and the road by another, which is two materials\n * again with extra steps.\n */\nfloat filmHash(vec3 p) {\n p = fract(p * 0.1031);\n p += dot(p, p.zyx + 31.32);\n return fract((p.x + p.y) * p.z);\n}\n\nfloat filmNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n vec3 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(\n mix(filmHash(i + vec3(0.0, 0.0, 0.0)), filmHash(i + vec3(1.0, 0.0, 0.0)), u.x),\n mix(filmHash(i + vec3(0.0, 1.0, 0.0)), filmHash(i + vec3(1.0, 1.0, 0.0)), u.x),\n u.y\n ),\n mix(\n mix(filmHash(i + vec3(0.0, 0.0, 1.0)), filmHash(i + vec3(1.0, 0.0, 1.0)), u.x),\n mix(filmHash(i + vec3(0.0, 1.0, 1.0)), filmHash(i + vec3(1.0, 1.0, 1.0)), u.x),\n u.y\n ),\n u.z\n );\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorld, 1.0), uClipPlane) < 0.0) discard;\n\n vec3 view = normalize(uCameraPos - vWorld);\n float facing = max(dot(normalize(vNormal), view), 0.0);\n /*\n * Fresnel-ish: dark looking straight down at it, bright at a grazing angle.\n * This is the whole effect \u2014 it is what makes a wet patch look wet rather\n * than looking like a patch of dark paint.\n */\n float grazing = pow(1.0 - facing, 3.0);\n\n /*\n * A slow travelling interference, so the sheen crawls rather than sitting\n * still. Two frequencies that do not divide each other, or the pattern\n * repeats visibly across a long slick.\n */\n float ripple =\n sin(vWorld.x * 0.73 + vWorld.z * 0.41 + uTime * 0.55) *\n cos(vWorld.z * 0.91 - vWorld.x * 0.29 - uTime * 0.37);\n\n vec3 sheen = hue(grazing * 1.35 + ripple * 0.14 + 0.55);\n /*\n * Dark underneath, so the film always reads as *less* than the surface it lies on even where\n * the sheen is strongest \u2014 but not so dark that it disappears.\n *\n * A slick that has just launched somebody has to be findable afterwards. It was rounded,\n * feathered and iridescent \u2014 and at a fifth of an opaque tint on a pale deck at dusk,\n * invisible. A hazard you cannot find is not a hazard, it is a trap.\n *\n * So the base is darker and the sheen has a floor: even looking straight down, where the\n * Fresnel term is nearly nothing, a slick is a distinctly wet patch rather than a faint one.\n */\n vec3 base = vTint * 0.9;\n vec3 color = base + sheen * uSheen * (0.22 + grazing * 0.75);\n\n /*\n * The mirrored scene, under the sheen rather than instead of it.\n *\n * Screen-space from the mirrored clip position, exactly as the water pass does it, with the\n * same fade as the sample leaves the target: a reflection that stopped dead at the edge of its\n * own buffer would draw a line across the road where nothing is.\n *\n * Weighted by the grazing term as well as by strength, because that is what a reflection does:\n * a wet surface underfoot shows almost nothing and the same surface down the street shows the\n * whole scene. Added before the fog so the image recedes with everything else in the frame.\n */\n if (uReflectionEnabled != 0 && uReflectionStrength > 0.0 && vReflectionClip.w > 0.0) {\n vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;\n vec2 border = min(reflectionUv, 1.0 - reflectionUv);\n float valid = smoothstep(0.0, 0.025, min(border.x, border.y));\n /*\n * An explicit level, not an implicit one, and the branch above is the whole reason.\n *\n * An implicit derivative inside control flow that is not provably uniform is what lets a\n * compiler flatten the branch and pay for every arm \u2014 AGENTS.md, 2026-08-07 \u2014 and WGSL\n * does not merely warn: the module fails to compile with \"textureSample must only be\n * called from uniform control flow\", which invalidates the pipeline, which invalidates\n * the command buffer, and the frame still presents. The fifth time on this branch. The\n * mirror has no mip chain, so level 0 is the only level there is; the water shader\n * reaches the same conclusion for the same lookup.\n */\n /*\n * The aggregate under the water, as a displacement of what the water shows.\n *\n * The gradient of the same field the road's relief uses, taken across the surface and used\n * to push the lookup. **Stretched along the reflection rather than isotropic**, and that is\n * the half that makes it read as a road: a reflection on a rough horizontal surface smears\n * *towards the viewer*, because a bump tilts the ray in the plane it is already travelling\n * in. Displacing in a circle gives frosted glass instead, which is a different wet thing.\n *\n * Scaled by the grazing term for the same reason the mirror is: looking straight down at a\n * puddle the reflection is compressed into almost nothing and a displacement of it would be\n * enormous, so the scatter has to fall off exactly where the image does.\n */\n if (uFilmRoughness > 0.0) {\n vec3 at = vWorld * uFilmRoughnessCycles;\n float here = filmNoise(at);\n vec2 slope = vec2(\n filmNoise(at + vec3(0.5, 0.0, 0.0)) - here,\n filmNoise(at + vec3(0.0, 0.0, 0.5)) - here\n );\n /* Along the view, which on a horizontal film is where the image runs. */\n vec2 along = normalize(vec2(view.x, view.z) + vec2(1e-5, 0.0));\n vec2 smear = along * dot(slope, along) * 3.0 + slope * 0.35;\n reflectionUv += smear * uFilmRoughness * 0.016 * (0.2 + grazing * 0.8);\n /* Re-fade at the border: a displaced sample can leave the target the original did not. */\n vec2 movedBorder = min(reflectionUv, 1.0 - reflectionUv);\n valid = min(valid, smoothstep(0.0, 0.025, min(movedBorder.x, movedBorder.y)));\n }\n vec3 mirrored = textureLod(uReflectionMap, clamp(reflectionUv, 0.0, 1.0), 0.0).rgb;\n color = mix(color, mirrored, valid * uReflectionStrength * (0.25 + grazing * 0.75));\n }\n\n // The same fog every other pass applies \u2014 and now literally the same code, which\n // it was not: this ran its own curve while claiming otherwise, so a slick receded\n // at a different rate than the deck it was lying on.\n float fog = mediumFog(length(uCameraPos - vWorld), vWorld.y);\n color = mix(color, mediumColor(), fog);\n\n /*\n * Alpha from coverage, squared.\n *\n * Squared because a linear ramp across the rim still reads as a visible band \u2014\n * the eye finds the constant-gradient edge. Squaring pushes the falloff outward\n * so the slick has a soft shoulder and no perceptible boundary. The sheen fades\n * with it, which is right: the thinnest part of a film is the least iridescent.\n */\n /*\n * Alpha from coverage, and the rim is what is squared \u2014 not the whole patch.\n *\n * Squaring the coverage outright made the *middle* translucent too, which is most of why a\n * slick could not be found. The shoulder still needs to be soft, so the square is mixed in\n * near the edge only: solid through the body, dissolving at the rim.\n */\n float coverage = clamp(vCoverage, 0.0, 1.0);\n float alpha = mix(coverage * coverage, coverage, 0.65) * 0.92;\n fragColor = vec4(color, alpha);\n}\n";
15
+ export declare const FILM_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorld;\nin vec3 vNormal;\nin vec3 vTint;\nin vec4 vReflectionClip;\n/*\n * The scene mirrored across the plane this film lies on, and how much of it to show.\n *\n * **Sheen is iridescence and this is the image**, which is the split a consumer asked for after\n * finding that raising sheen to make a road wetter only made it oilier. A wet street at night is\n * defined by everything above it appearing twice, and no amount of interference pattern is that.\n *\n * The projection is the water shader's, unchanged and deliberately so: reproducing it outside the\n * engine is the piece most likely to be subtly wrong and least likely to be noticed, since a\n * mirror off by a small skew looks like a wet road until somebody puts a straight line across it.\n */\nuniform sampler2D uReflectionMap;\nuniform int uReflectionEnabled;\nuniform float uReflectionStrength;\nin float vCoverage;\n\nuniform vec3 uCameraPos;\nuniform float uTime;\n/*\n * The mirrored pass clips at the water plane, and it does so **in the fragment\n * shader** rather than with gl_ClipDistance.\n *\n * That is not a style choice. Clip distances in WebGL2 need\n * EXT_clip_cull_distance both enabled on the context and declared with an\n * #extension line, and without the declaration the shader fails to *compile* \u2014\n * which, since programs are built when the renderer is constructed, takes the\n * whole boot with it and leaves a black screen. This shader had never been\n * compiled when it was written, so the mistake sat harmlessly in the file until\n * the day it was wired to something. Discarding is what flat.ts does, it needs\n * no extension, and a film is a thin translucent layer where the cost of a\n * discarded fragment is nothing.\n */\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\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/** How strong the sheen is. Zero leaves a plain dark film. */\nuniform float uSheen;\n\n/**\n * How rough the film's own surface is, 0 to 1, and how coarse that roughness is.\n *\n * **A film had exactly one finish before this: mirror.** That is right for standing water and\n * wrong for the thing this pass is most used for, because a wet road is not a mirror \u2014 it is\n * water lying *in* aggregate, and the stone underneath breaks the reflection into a smear. With\n * no way to say so, a patch of film over a textured road met it at a hard rim: mirror on one\n * side, stone on the other, with nothing able to cross it. Feathering the coverage cannot fix\n * that, and it was tried \u2014 the alpha was never what the eye was reading.\n *\n * So the reflection is displaced by a noise field rather than blurred. **Displaced, because a\n * blur is the wrong phenomenon**: a rough surface does not soften what it reflects, it scatters\n * it, and a lamp on wet tarmac smears into a streak rather than becoming a soft disc. It is also\n * what a blur cannot afford here \u2014 the mirror has no mip chain (see the `textureLod` note\n * below), so softening it honestly would cost several taps where this costs none.\n *\n * `uFilmRoughnessCycles` is bumps to the metre, and it is the same quantity\n * `Renderer.setSurfaceRelief` takes. **Matching it to the surface underneath is the point**:\n * given the same field at the same scale, the water is broken up by the aggregate that the dry\n * road beside it is showing, and the two stop being different materials.\n *\n * Zero is the mirror this pass has always been, exactly, so nothing that does not ask for\n * roughness renders differently.\n */\nuniform float uFilmRoughness;\nuniform float uFilmRoughnessCycles;\n\nout vec4 fragColor;\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/*\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\n/** A hue wheel, without a texture. Standard 6-segment ramp. */\nvec3 hue(float h) {\n h = fract(h) * 6.0;\n vec3 c = clamp(vec3(abs(h - 3.0) - 1.0, 2.0 - abs(h - 2.0), 2.0 - abs(h - 4.0)), 0.0, 1.0);\n return c;\n}\n\n/**\n * The same value noise `flat.ts` builds its relief from, and deliberately the same.\n *\n * Copied rather than shared because these two shaders have no common include beyond the fog, and\n * a film that used a *different* field would defeat the whole purpose of taking a cycle count:\n * the water would be broken up by one pattern and the road by another, which is two materials\n * again with extra steps.\n */\nfloat filmHash(vec3 p) {\n p = fract(p * 0.1031);\n p += dot(p, p.zyx + 31.32);\n return fract((p.x + p.y) * p.z);\n}\n\nfloat filmNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n vec3 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(\n mix(filmHash(i + vec3(0.0, 0.0, 0.0)), filmHash(i + vec3(1.0, 0.0, 0.0)), u.x),\n mix(filmHash(i + vec3(0.0, 1.0, 0.0)), filmHash(i + vec3(1.0, 1.0, 0.0)), u.x),\n u.y\n ),\n mix(\n mix(filmHash(i + vec3(0.0, 0.0, 1.0)), filmHash(i + vec3(1.0, 0.0, 1.0)), u.x),\n mix(filmHash(i + vec3(0.0, 1.0, 1.0)), filmHash(i + vec3(1.0, 1.0, 1.0)), u.x),\n u.y\n ),\n u.z\n );\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorld, 1.0), uClipPlane) < 0.0) discard;\n\n vec3 view = normalize(uCameraPos - vWorld);\n float facing = max(dot(normalize(vNormal), view), 0.0);\n /*\n * Fresnel-ish: dark looking straight down at it, bright at a grazing angle.\n * This is the whole effect \u2014 it is what makes a wet patch look wet rather\n * than looking like a patch of dark paint.\n */\n float grazing = pow(1.0 - facing, 3.0);\n\n /*\n * A slow travelling interference, so the sheen crawls rather than sitting\n * still. Two frequencies that do not divide each other, or the pattern\n * repeats visibly across a long slick.\n */\n float ripple =\n sin(vWorld.x * 0.73 + vWorld.z * 0.41 + uTime * 0.55) *\n cos(vWorld.z * 0.91 - vWorld.x * 0.29 - uTime * 0.37);\n\n vec3 sheen = hue(grazing * 1.35 + ripple * 0.14 + 0.55);\n /*\n * Dark underneath, so the film always reads as *less* than the surface it lies on even where\n * the sheen is strongest \u2014 but not so dark that it disappears.\n *\n * A slick that has just launched somebody has to be findable afterwards. It was rounded,\n * feathered and iridescent \u2014 and at a fifth of an opaque tint on a pale deck at dusk,\n * invisible. A hazard you cannot find is not a hazard, it is a trap.\n *\n * So the base is darker and the sheen has a floor: even looking straight down, where the\n * Fresnel term is nearly nothing, a slick is a distinctly wet patch rather than a faint one.\n */\n vec3 base = vTint * 0.9;\n vec3 color = base + sheen * uSheen * (0.22 + grazing * 0.75);\n\n /*\n * **Graded before the mirror is mixed in**, as every pass grades what it draws when nothing after\n * it will, because the mirror's own passes graded what it holds. The fog that used to follow the\n * mirror distributes over the mix, so the film's own colour is fogged and graded here, and what\n * the mirror shows is fogged toward the same air, graded, where it is mixed in below.\n */\n // The same fog every other pass applies \u2014 and now literally the same code, which\n // it was not: this ran its own curve while claiming otherwise, so a slick receded\n // at a different rate than the deck it was lying on.\n float fog = mediumFog(length(uCameraPos - vWorld), vWorld.y);\n vec3 gradedAir = applyOutputTransform(mediumColor());\n vec3 graded = applyOutputTransform(mix(color, mediumColor(), fog));\n\n /*\n * The mirrored scene, under the sheen rather than instead of it.\n *\n * Screen-space from the mirrored clip position, exactly as the water pass does it, with the\n * same fade as the sample leaves the target: a reflection that stopped dead at the edge of its\n * own buffer would draw a line across the road where nothing is.\n *\n * Weighted by the grazing term as well as by strength, because that is what a reflection does:\n * a wet surface underfoot shows almost nothing and the same surface down the street shows the\n * whole scene. Fogged as the film is, so the image recedes with everything else in the frame.\n */\n if (uReflectionEnabled != 0 && uReflectionStrength > 0.0 && vReflectionClip.w > 0.0) {\n vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;\n vec2 border = min(reflectionUv, 1.0 - reflectionUv);\n float valid = smoothstep(0.0, 0.025, min(border.x, border.y));\n /*\n * An explicit level, not an implicit one, and the branch above is the whole reason.\n *\n * An implicit derivative inside control flow that is not provably uniform is what lets a\n * compiler flatten the branch and pay for every arm \u2014 AGENTS.md, 2026-08-07 \u2014 and WGSL\n * does not merely warn: the module fails to compile with \"textureSample must only be\n * called from uniform control flow\", which invalidates the pipeline, which invalidates\n * the command buffer, and the frame still presents. The fifth time on this branch. The\n * mirror has no mip chain, so level 0 is the only level there is; the water shader\n * reaches the same conclusion for the same lookup.\n */\n /*\n * The aggregate under the water, as a displacement of what the water shows.\n *\n * The gradient of the same field the road's relief uses, taken across the surface and used\n * to push the lookup. **Stretched along the reflection rather than isotropic**, and that is\n * the half that makes it read as a road: a reflection on a rough horizontal surface smears\n * *towards the viewer*, because a bump tilts the ray in the plane it is already travelling\n * in. Displacing in a circle gives frosted glass instead, which is a different wet thing.\n *\n * Scaled by the grazing term for the same reason the mirror is: looking straight down at a\n * puddle the reflection is compressed into almost nothing and a displacement of it would be\n * enormous, so the scatter has to fall off exactly where the image does.\n */\n if (uFilmRoughness > 0.0) {\n vec3 at = vWorld * uFilmRoughnessCycles;\n float here = filmNoise(at);\n vec2 slope = vec2(\n filmNoise(at + vec3(0.5, 0.0, 0.0)) - here,\n filmNoise(at + vec3(0.0, 0.0, 0.5)) - here\n );\n /* Along the view, which on a horizontal film is where the image runs. */\n vec2 along = normalize(vec2(view.x, view.z) + vec2(1e-5, 0.0));\n vec2 smear = along * dot(slope, along) * 3.0 + slope * 0.35;\n reflectionUv += smear * uFilmRoughness * 0.016 * (0.2 + grazing * 0.8);\n /* Re-fade at the border: a displaced sample can leave the target the original did not. */\n vec2 movedBorder = min(reflectionUv, 1.0 - reflectionUv);\n valid = min(valid, smoothstep(0.0, 0.025, min(movedBorder.x, movedBorder.y)));\n }\n vec3 mirrored = textureLod(uReflectionMap, clamp(reflectionUv, 0.0, 1.0), 0.0).rgb;\n graded = mix(\n graded,\n mix(mirrored, gradedAir, fog),\n valid * uReflectionStrength * (0.25 + grazing * 0.75)\n );\n }\n\n /*\n * Alpha from coverage, squared.\n *\n * Squared because a linear ramp across the rim still reads as a visible band \u2014\n * the eye finds the constant-gradient edge. Squaring pushes the falloff outward\n * so the slick has a soft shoulder and no perceptible boundary. The sheen fades\n * with it, which is right: the thinnest part of a film is the least iridescent.\n */\n /*\n * Alpha from coverage, and the rim is what is squared \u2014 not the whole patch.\n *\n * Squaring the coverage outright made the *middle* translucent too, which is most of why a\n * slick could not be found. The shoulder still needs to be soft, so the square is mixed in\n * near the edge only: solid through the body, dissolving at the rim.\n */\n float coverage = clamp(vCoverage, 0.0, 1.0);\n float alpha = mix(coverage * coverage, coverage, 0.65) * 0.92;\n fragColor = vec4(graded, alpha);\n}\n";
@@ -1,4 +1,5 @@
1
1
  import { FOG_GLSL } from './fog.js';
2
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
2
3
  /**
3
4
  * A thin, wet, iridescent film lying on a surface.
4
5
  *
@@ -120,6 +121,7 @@ uniform float uFilmRoughness;
120
121
  uniform float uFilmRoughnessCycles;
121
122
 
122
123
  out vec4 fragColor;
124
+ ${OUTPUT_TRANSFORM_GLSL}
123
125
 
124
126
  /** A hue wheel, without a texture. Standard 6-segment ramp. */
125
127
  vec3 hue(float h) {
@@ -197,6 +199,19 @@ void main() {
197
199
  vec3 base = vTint * 0.9;
198
200
  vec3 color = base + sheen * uSheen * (0.22 + grazing * 0.75);
199
201
 
202
+ /*
203
+ * **Graded before the mirror is mixed in**, as every pass grades what it draws when nothing after
204
+ * it will, because the mirror's own passes graded what it holds. The fog that used to follow the
205
+ * mirror distributes over the mix, so the film's own colour is fogged and graded here, and what
206
+ * the mirror shows is fogged toward the same air, graded, where it is mixed in below.
207
+ */
208
+ // The same fog every other pass applies — and now literally the same code, which
209
+ // it was not: this ran its own curve while claiming otherwise, so a slick receded
210
+ // at a different rate than the deck it was lying on.
211
+ float fog = mediumFog(length(uCameraPos - vWorld), vWorld.y);
212
+ vec3 gradedAir = applyOutputTransform(mediumColor());
213
+ vec3 graded = applyOutputTransform(mix(color, mediumColor(), fog));
214
+
200
215
  /*
201
216
  * The mirrored scene, under the sheen rather than instead of it.
202
217
  *
@@ -206,7 +221,7 @@ void main() {
206
221
  *
207
222
  * Weighted by the grazing term as well as by strength, because that is what a reflection does:
208
223
  * a wet surface underfoot shows almost nothing and the same surface down the street shows the
209
- * whole scene. Added before the fog so the image recedes with everything else in the frame.
224
+ * whole scene. Fogged as the film is, so the image recedes with everything else in the frame.
210
225
  */
211
226
  if (uReflectionEnabled != 0 && uReflectionStrength > 0.0 && vReflectionClip.w > 0.0) {
212
227
  vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;
@@ -252,15 +267,13 @@ void main() {
252
267
  valid = min(valid, smoothstep(0.0, 0.025, min(movedBorder.x, movedBorder.y)));
253
268
  }
254
269
  vec3 mirrored = textureLod(uReflectionMap, clamp(reflectionUv, 0.0, 1.0), 0.0).rgb;
255
- color = mix(color, mirrored, valid * uReflectionStrength * (0.25 + grazing * 0.75));
270
+ graded = mix(
271
+ graded,
272
+ mix(mirrored, gradedAir, fog),
273
+ valid * uReflectionStrength * (0.25 + grazing * 0.75)
274
+ );
256
275
  }
257
276
 
258
- // The same fog every other pass applies — and now literally the same code, which
259
- // it was not: this ran its own curve while claiming otherwise, so a slick receded
260
- // at a different rate than the deck it was lying on.
261
- float fog = mediumFog(length(uCameraPos - vWorld), vWorld.y);
262
- color = mix(color, mediumColor(), fog);
263
-
264
277
  /*
265
278
  * Alpha from coverage, squared.
266
279
  *
@@ -278,6 +291,6 @@ void main() {
278
291
  */
279
292
  float coverage = clamp(vCoverage, 0.0, 1.0);
280
293
  float alpha = mix(coverage * coverage, coverage, 0.65) * 0.92;
281
- fragColor = vec4(color, alpha);
294
+ fragColor = vec4(graded, alpha);
282
295
  }
283
296
  `;
@@ -9,4 +9,4 @@
9
9
  */
10
10
  /** Shared billboard vertex stage: used by fire, smoke and any future plume. */
11
11
  export declare const PLUME_VERT = "#version 300 es\nlayout(location = 0) in vec3 aCenter;\nlayout(location = 1) in vec2 aCorner;\nlayout(location = 2) in vec2 aSize;\nlayout(location = 3) in float aSeed;\nlayout(location = 4) in float aBlade;\n\nuniform mat4 uViewProj;\nuniform vec3 uCameraPos;\nuniform float uTime;\n/** Amplitude of the size pulse; 0 keeps the quad a fixed size. */\nuniform float uSizePulse;\n/**\n * Horizontal wind, m/s. Plumes lean with it, increasingly toward their top \u2014\n * the base is anchored to whatever is burning, the crown is free. Sharing one\n * wind with the rest of the world is the point: smoke drifting one way while\n * foliage leans another is the clearest tell that a scene is a pile of effects\n * rather than a place.\n */\nuniform vec2 uWind;\n/**\n * Whole-batch offset. Plume centres live in a static buffer because braziers\n * never move; a batch that *does* move \u2014 a tornado \u2014 shifts by a uniform rather\n * than rewriting geometry every frame.\n */\nuniform vec3 uOrigin;\n/** How strongly this plume type answers the wind; smoke far more than flame. */\nuniform float uWindResponse;\n\nout vec2 vUv;\nout float vSeed;\nout float vDistance;\nout vec3 vWorldPos;\n\n/**\n * Plumes are not a constant size: a fire flares and dies back, a smoke column\n * swells and thins. Two out-of-phase sines, offset per plume, so the whole\n * effect breathes rather than only its interior pattern animating.\n */\nfloat sizePulse(float seed) {\n return 1.0 + uSizePulse * (\n sin(uTime * 1.7 + seed * 9.1) * 0.62 +\n sin(uTime * 3.3 + seed * 4.7) * 0.38\n );\n}\n\nvoid main() {\n // Height swells more than width, the way a real flame stretches upward.\n vec3 anchor = aCenter + uOrigin;\n\n /*\n * No billboarding at all: the blade's heading is fixed in the world.\n *\n * Each plume is drawn as a CROSS of two blades 90 degrees apart (see BLADES in\n * plumeRenderer), and aSeed advances by the golden ratio per plume, so the crosses\n * themselves are scattered by the golden angle rather than aligned into a grid.\n *\n * Nothing here reads the camera, and that is the fix. Anything that does - a full\n * billboard, or merely leaning edge-on cards toward the viewer - rotates geometry\n * as the camera moves, which is exactly what reads as flat: orbiting a fire showed\n * the same silhouette from every angle. A cross cannot track the viewer, so turning\n * around a fire genuinely brings one blade broadside while the other goes edge-on.\n * That is the parallax a volume has, and it is also why no blade needs to be turned\n * to stay visible - its partner is square-on whenever it is not.\n *\n * World up stays the vertical axis: a flame that tilts with the camera reads as a\n * decal.\n */\n vec3 up = vec3(0.0, 1.0, 0.0);\n float yaw = aSeed * 6.28318530718 + aBlade * 1.57079632679;\n vec3 right = vec3(cos(yaw), 0.0, sin(yaw));\n float pulse = sizePulse(aSeed);\n vec2 size = vec2(aSize.x * mix(1.0, pulse, 0.55), aSize.y * pulse);\n\n float rise = aCorner.y * 0.5 + 0.5;\n vec3 world = anchor\n + right * (aCorner.x * size.x)\n + up * (rise * size.y);\n\n // Quadratic in height: a column bends away from its anchor rather than\n // shearing uniformly, which would slide its base off the fire.\n world.xz += uWind * (uWindResponse * rise * rise * size.y);\n\n vUv = vec2(aCorner.x * 0.5 + 0.5, aCorner.y * 0.5 + 0.5);\n vSeed = aSeed;\n vDistance = distance(world, uCameraPos);\n vWorldPos = world;\n gl_Position = uViewProj * vec4(world, 1.0);\n}\n";
12
- export declare const FIRE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nout vec4 outColor;\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\n/** Rising turbulence: octaves scrolled upward at different rates. */\nfloat turbulence(vec2 p, float t) {\n float v = valueNoise(p * 3.0 + vec2(0.0, -t * 1.9)) * 0.55;\n if (uNoiseOctaves == 1) return v / 0.55;\n v += valueNoise(p * 6.5 + vec2(t * 0.3, -t * 3.1)) * 0.28;\n if (uNoiseOctaves == 2) return v / 0.83;\n return v + valueNoise(p * 13.0 + vec2(-t * 0.2, -t * 4.6)) * 0.17;\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n vec2 uv = vUv;\n float t = uTime + vSeed * 37.0;\n\n // Taper toward the tip and pinch the sides, so the quad never shows.\n float sides = 1.0 - abs(uv.x - 0.5) * 2.0;\n float taper = sides * (1.0 - uv.y * 0.72);\n if (taper <= 0.0) discard;\n\n // Sway: the column leans and wanders instead of standing to attention.\n float sway = (valueNoise(vec2(t * 0.55, vSeed * 11.0)) - 0.5) * 0.34 * uv.y;\n vec2 p = vec2(uv.x + sway, uv.y);\n\n float n = turbulence(p, t);\n // Flame body: noise must beat a threshold that rises with height, which is\n // what makes the tip break into separate licks.\n float body = n * taper * 1.9 - uv.y * 0.55;\n if (body <= 0.02) discard;\n\n // Colour ramp: white-hot core through yellow and orange to a dark red edge.\n float heat = clamp(body * 1.5, 0.0, 1.0);\n vec3 col = mix(vec3(0.62, 0.06, 0.02), vec3(1.0, 0.45, 0.06), smoothstep(0.0, 0.45, heat));\n col = mix(col, vec3(1.0, 0.85, 0.35), smoothstep(0.45, 0.75, heat));\n col = mix(col, vec3(1.0, 0.98, 0.88), smoothstep(0.78, 1.0, heat));\n\n float alpha = clamp(body * 2.2, 0.0, 1.0) * (1.0 - smoothstep(0.72, 1.0, uv.y));\n // Additive light must lose alpha as it scatters, or mixing its colour alone\n // still makes a distant flame glow through fog and through water.\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, mediumColor(), fog);\n alpha *= 1.0 - fog;\n\n outColor = vec4(col, alpha);\n}\n";
12
+ export declare const FIRE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nout vec4 outColor;\n\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/*\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\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\n/** Rising turbulence: octaves scrolled upward at different rates. */\nfloat turbulence(vec2 p, float t) {\n float v = valueNoise(p * 3.0 + vec2(0.0, -t * 1.9)) * 0.55;\n if (uNoiseOctaves == 1) return v / 0.55;\n v += valueNoise(p * 6.5 + vec2(t * 0.3, -t * 3.1)) * 0.28;\n if (uNoiseOctaves == 2) return v / 0.83;\n return v + valueNoise(p * 13.0 + vec2(-t * 0.2, -t * 4.6)) * 0.17;\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n vec2 uv = vUv;\n float t = uTime + vSeed * 37.0;\n\n // Taper toward the tip and pinch the sides, so the quad never shows.\n float sides = 1.0 - abs(uv.x - 0.5) * 2.0;\n float taper = sides * (1.0 - uv.y * 0.72);\n if (taper <= 0.0) discard;\n\n // Sway: the column leans and wanders instead of standing to attention.\n float sway = (valueNoise(vec2(t * 0.55, vSeed * 11.0)) - 0.5) * 0.34 * uv.y;\n vec2 p = vec2(uv.x + sway, uv.y);\n\n float n = turbulence(p, t);\n // Flame body: noise must beat a threshold that rises with height, which is\n // what makes the tip break into separate licks.\n float body = n * taper * 1.9 - uv.y * 0.55;\n if (body <= 0.02) discard;\n\n // Colour ramp: white-hot core through yellow and orange to a dark red edge.\n float heat = clamp(body * 1.5, 0.0, 1.0);\n vec3 col = mix(vec3(0.62, 0.06, 0.02), vec3(1.0, 0.45, 0.06), smoothstep(0.0, 0.45, heat));\n col = mix(col, vec3(1.0, 0.85, 0.35), smoothstep(0.45, 0.75, heat));\n col = mix(col, vec3(1.0, 0.98, 0.88), smoothstep(0.78, 1.0, heat));\n\n float alpha = clamp(body * 2.2, 0.0, 1.0) * (1.0 - smoothstep(0.72, 1.0, uv.y));\n // Additive light must lose alpha as it scatters, or mixing its colour alone\n // still makes a distant flame glow through fog and through water.\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, mediumColor(), fog);\n alpha *= 1.0 - fog;\n\n /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */\n outColor = vec4(applyOutputTransform(col), alpha);\n}\n";
@@ -1,4 +1,5 @@
1
1
  import { FOG_GLSL } from './fog.js';
2
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
2
3
  /**
3
4
  * Procedural flames: camera-facing quads whose shape and colour come entirely
4
5
  * from animated noise. No sprite sheets, no image assets — and no static
@@ -114,6 +115,7 @@ uniform vec4 uClipPlane;
114
115
  uniform int uClipEnabled;
115
116
 
116
117
  out vec4 outColor;
118
+ ${OUTPUT_TRANSFORM_GLSL}
117
119
 
118
120
  float hash21(vec2 p) {
119
121
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
@@ -173,6 +175,7 @@ void main() {
173
175
  col = mix(col, mediumColor(), fog);
174
176
  alpha *= 1.0 - fog;
175
177
 
176
- outColor = vec4(col, alpha);
178
+ /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
179
+ outColor = vec4(applyOutputTransform(col), alpha);
177
180
  }
178
181
  `;
@@ -22,10 +22,9 @@ float layerExtras() { return uLayers[1].y; }
22
22
  vec3 layerTint(int i) { return vec3(1.0); }
23
23
  vec2 layerRanges(int i, vec2 roughMetal) { return roughMetal; }
24
24
  vec3 layerStrengthen(int i, vec3 texel) { return texel; }
25
- float layerSpecular(int i, float own) { return own; }
25
+ float layerNamedSpecular(int i) { return -1.0; }
26
26
  float layerRepeatDown(int i, float across) { return across; }
27
- float layerOcclusionStrength() { return 0.0; }
28
- bool layerOcclusionAmbient() { return false; }
27
+ vec2 layerOcclusionStrengths() { return vec2(0.0); }
29
28
  vec2 layerOcclusionRange() { return vec2(0.0, 1.0); }
30
29
  `;
31
30
  }
@@ -35,7 +34,7 @@ const bool LAYER_LOOKS = true; // wgsl:override
35
34
  /*
36
35
  * Fifteen vectors, as surfaceLayerLooks.ts packs them: the array layer each of the five reads, base
37
36
  * first (floats 0 to 4); where the maps beyond the layers start (5), the mesh occlusion's strength
38
- * (6) and whether it darkens the ambient light rather than the colour (7); then two vectors a
37
+ * on the colour, from 0 up (6), and on the ambient light (7); then two vectors a
39
38
  * layer, base first: its tint (x y z) and normal strength (w), then its roughness range (x to y)
40
39
  * and metalness range (z to w); then each layer's specular, -1 for the surface's own (48 to 52), its
41
40
  * repeat down (53 to 57), and the occlusion's range (58 to 59).
@@ -83,13 +82,10 @@ vec3 layerStrengthen(int i, vec3 texel) {
83
82
  return texel;
84
83
  }
85
84
 
86
- /* The layer's specular, or the surface's own where it names none. */
87
- float layerSpecular(int i, float own) {
88
- if (LAYER_LOOKS) {
89
- float named = layerLook(48 + i);
90
- return named < 0.0 ? own : named;
91
- }
92
- return own;
85
+ /* The specular the layer names, or -1 where it names none and the surface's own stands. */
86
+ float layerNamedSpecular(int i) {
87
+ if (LAYER_LOOKS) return layerLook(48 + i);
88
+ return -1.0;
93
89
  }
94
90
 
95
91
  /* The layer's repeat down, which is its repeat across unless it gave two. */
@@ -98,14 +94,10 @@ float layerRepeatDown(int i, float across) {
98
94
  return across;
99
95
  }
100
96
 
101
- float layerOcclusionStrength() {
102
- if (LAYER_LOOKS) return uLayerLooks[1].z;
103
- return 0.0;
104
- }
105
-
106
- bool layerOcclusionAmbient() {
107
- if (LAYER_LOOKS) return uLayerLooks[1].w > 0.5;
108
- return false;
97
+ /* The mesh occlusion's strength on the colour (x), from 0 up, and on the ambient light (y). */
98
+ vec2 layerOcclusionStrengths() {
99
+ if (LAYER_LOOKS) return uLayerLooks[1].zw;
100
+ return vec2(0.0);
109
101
  }
110
102
 
111
103
  vec2 layerOcclusionRange() {
@@ -60,8 +60,12 @@ ${layerLooksGlsl(looks)}
60
60
  /* Each layer's share of this point, the base's and layers 1 to 4's: worked out once, in layeredBegin. */
61
61
  float lShareBase;
62
62
  vec4 lShare;
63
- /* What an occlusion read at the mesh's coordinates leaves of this point, 1 where none: layeredBegin. */
64
- float lMeshOcclusion;
63
+ /*
64
+ * What an occlusion read at the mesh's coordinates leaves of this point's colour and of its ambient
65
+ * light, 1 where none: layeredBegin.
66
+ */
67
+ float lColorOcclusion;
68
+ float lAmbientOcclusion;
65
69
 
66
70
  bool layered() {
67
71
  return uLayers[1].y > 0.5;
@@ -102,15 +106,18 @@ vec4 layerPick(float layer) {
102
106
  void layeredBegin() {
103
107
  /*
104
108
  * The occlusion at the mesh's coordinates, once: the ORM array's layer past the layers and past
105
- * any mask and added mask it carries, spread over its range and taken at its strength.
109
+ * any mask and added mask it carries, spread over its range and taken at each strength. The
110
+ * colour's may pass 1, darkening faster than the occlusion does, and is held at black.
106
111
  */
107
- lMeshOcclusion = 1.0;
108
- float strength = layerOcclusionStrength();
109
- if (strength > 0.0) {
112
+ lColorOcclusion = 1.0;
113
+ lAmbientOcclusion = 1.0;
114
+ vec2 strength = layerOcclusionStrengths();
115
+ if (strength.x > 0.0 || strength.y > 0.0) {
110
116
  float at = layerExtras() + (abs(uLayers[1].w - 1.0) < 0.5 ? 1.0 : 0.0) + (uLayers[2].z > 0.5 ? 1.0 : 0.0);
111
117
  vec2 range = layerOcclusionRange();
112
- float occlusion = mix(range.x, range.y, texture(uOrmMap, vec3(vUv.xy, at)).r);
113
- lMeshOcclusion = mix(1.0, occlusion, strength);
118
+ float bare = 1.0 - mix(range.x, range.y, texture(uOrmMap, vec3(vUv.xy, at)).r);
119
+ lColorOcclusion = max(0.0, 1.0 - strength.x * bare);
120
+ lAmbientOcclusion = 1.0 - strength.y * bare;
114
121
  }
115
122
  vec4 m = layerWeights();
116
123
  if (uLayers[2].z > 0.5) {
@@ -157,7 +164,7 @@ vec3 layeredColor(vec3 color) {
157
164
 
158
165
  /*
159
166
  * Colour by the shares, each layer's tinted, and the base layer's coverage, which a cutout reads;
160
- * then darkened by the occlusion at the mesh's coordinates, where it is the colour that takes it.
167
+ * then darkened by the occlusion at the mesh's coordinates, at the colour's strength.
161
168
  */
162
169
  vec4 layeredAlbedo() {
163
170
  vec4 sum = vec4(0.0);
@@ -167,28 +174,36 @@ vec4 layeredAlbedo() {
167
174
  sum.rgb += t.rgb * layerTint(i) * layerShare(i);
168
175
  if (i == 0) sum.a = t.a;
169
176
  }
170
- if (!layerOcclusionAmbient()) sum.rgb *= lMeshOcclusion;
177
+ sum.rgb *= lColorOcclusion;
171
178
  return sum;
172
179
  }
173
180
 
174
- /* What the ambient light keeps: the occlusion at the mesh's coordinates, where it takes it. */
181
+ /* What the ambient light keeps: the occlusion at the mesh's coordinates, at the ambient's strength. */
175
182
  float layeredAmbient() {
176
- return layerOcclusionAmbient() ? lMeshOcclusion : 1.0;
183
+ return lAmbientOcclusion;
177
184
  }
178
185
 
179
186
  /*
180
- * The specular by the shares, each layer's own or the surface's where it names none. The surface's
181
- * own, untouched, where no material has asked for looks: shares summing to one in floating point
182
- * are not quite one, and a layered material that names nothing must draw exactly as before.
187
+ * The specular by the shares: each layer's named one at its share, and the surface's own over the
188
+ * layers naming none, once at most. Under a summed blend the shares add past 1 where the weights
189
+ * do, and a surface whose layers leave the specular to it reflects its own, not its own times that
190
+ * sum; under a blend laid over, the shares never pass 1 and this is the sum it always was. What it
191
+ * gives up: named speculars still add as their colours do, so a summed blend that names them
192
+ * reflects their sum. The surface's own, untouched, where no material has asked for looks: shares
193
+ * summing to one in floating point are not quite one, and a layered material that names nothing
194
+ * must draw exactly as before.
183
195
  */
184
196
  float layeredSpecular(float own) {
185
197
  if (LAYER_LOOKS) {
186
- float sum = 0.0;
198
+ float named = 0.0;
199
+ float unnamed = 0.0;
187
200
  for (int i = 0; i < 5; i++) {
188
201
  if (float(i) >= uLayers[1].y) break;
189
- sum += layerSpecular(i, own) * layerShare(i);
202
+ float specular = layerNamedSpecular(i);
203
+ if (specular < 0.0) unnamed += layerShare(i);
204
+ else named += specular * layerShare(i);
190
205
  }
191
- return sum;
206
+ return named + own * min(unnamed, 1.0);
192
207
  }
193
208
  return own;
194
209
  }
@@ -8,4 +8,4 @@
8
8
  * behaviour.
9
9
  */
10
10
  export declare const FLOCK_VERT = "#version 300 es\nlayout(location = 0) in vec2 aCorner;\nlayout(location = 1) in float aWing;\nlayout(location = 2) in float aIndex;\n\nuniform mat4 uViewProj;\nuniform vec3 uCenter;\nuniform float uRadius;\nuniform float uHeight;\nuniform float uSpeed;\nuniform float uTime;\nuniform float uCount;\nuniform float uScale;\n/** Horizontal wind, m/s. Birds hold station against it rather than riding it. */\nuniform vec2 uWind;\n\nout float vShade;\n\nvoid main() {\n // Each bird gets its own orbit: radius, height and rate all vary with index,\n // so the flock drifts apart and re-gathers instead of turning as one rigid\n // ring \u2014 which is what a shared angle alone produces.\n float id = aIndex;\n /*\n * Hashed from id + 1, and that +1 is a bug fix rather than a flourish.\n *\n * fract(sin(x) * k) has a fixed point at zero: sin(0) is 0, so bird 0 came out with spin\n * and lift of exactly 0 while every other bird got a scattered value. Zero is not a\n * middling result here, it is the floor of all three ranges at once \u2014 the innermost\n * orbit of the whole flock, the lowest height and the slowest wingbeat. Measured on the\n * storm scene: bird 0 orbited at 8.25 m while birds 1 to 33 spread over 8.34 to 15.72 m.\n * One bird in every flock, in every scene, permanently unlike the rest.\n */\n float seed = id + 1.0;\n float spin = fract(sin(seed * 12.9898) * 43758.5453);\n float lift = fract(sin(seed * 78.233) * 24634.6345);\n\n float radius = uRadius * (0.55 + spin * 0.5);\n float rate = uSpeed * (0.8 + lift * 0.45) / max(radius, 0.001);\n float angle = uTime * rate + id * 6.2831 / max(uCount, 1.0);\n\n vec3 heading = vec3(-sin(angle), 0.0, cos(angle));\n\n /*\n * Birds are not leaves. They hold station against a wind rather than being\n * carried off by it, so the wind does not translate them \u2014 it costs them\n * ground speed when they turn into it and gives it back downwind, and it\n * pushes the whole circuit slightly to leeward. The read is that they are\n * working, which is the only reason a bird is interesting at this distance.\n */\n float into = -dot(normalize(heading.xz + vec2(1e-5)), uWind) * 0.06;\n float driftAngle = angle + into;\n\n vec3 centre = uCenter + vec3(\n cos(driftAngle) * radius + uWind.x * 1.1,\n uHeight * (0.7 + lift * 0.6) + sin(uTime * 0.6 + id) * 1.4,\n sin(driftAngle) * radius + uWind.y * 1.1\n );\n\n // Wings beat about the bird's own travel direction, so the flap axis follows\n // the turn rather than staying locked to the world.\n vec3 forward = vec3(-sin(driftAngle), 0.0, cos(driftAngle));\n vec3 side = normalize(cross(forward, vec3(0.0, 1.0, 0.0)));\n\n // Harder into the wind: the beat rate rises with how much of the heading\n // opposes it, which is what makes the effort legible without any simulation.\n float effort = 1.0 + max(into, 0.0) * 22.0;\n float beat = sin(uTime * (9.0 + spin * 4.0) * effort + id) * 0.5 + 0.5;\n // aWing is 0 at the body and 1 at the tip, so only the tip rises.\n float fold = aWing * beat * 0.55;\n\n vec3 offset = side * (aCorner.x * uScale)\n + forward * (aCorner.y * uScale * 0.6)\n + vec3(0.0, fold * uScale, 0.0);\n\n vec3 world = centre + offset;\n // Silhouette only: a bird at this distance is a shape against the sky, and\n // shading it properly would just make it grey.\n vShade = 0.55 + beat * 0.25;\n gl_Position = uViewProj * vec4(world, 1.0);\n}\n";
11
- export declare const FLOCK_FRAG = "#version 300 es\nprecision highp float;\n\nin float vShade;\nuniform vec3 uTint;\nout vec4 outColor;\n\nvoid main() {\n outColor = vec4(uTint * vShade, 1.0);\n}\n";
11
+ export declare const FLOCK_FRAG = "#version 300 es\nprecision highp float;\n\nin float vShade;\nuniform vec3 uTint;\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/*\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 /* Graded where nothing after this will grade it, as every pass is. */\n outColor = vec4(applyOutputTransform(uTint * vShade), 1.0);\n}\n";
@@ -1,3 +1,4 @@
1
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
1
2
  /**
2
3
  * Birds: two triangles each, circling on a path computed entirely in the vertex
3
4
  * shader.
@@ -95,8 +96,10 @@ precision highp float;
95
96
  in float vShade;
96
97
  uniform vec3 uTint;
97
98
  out vec4 outColor;
99
+ ${OUTPUT_TRANSFORM_GLSL}
98
100
 
99
101
  void main() {
100
- outColor = vec4(uTint * vShade, 1.0);
102
+ /* Graded where nothing after this will grade it, as every pass is. */
103
+ outColor = vec4(applyOutputTransform(uTint * vShade), 1.0);
101
104
  }
102
105
  `;
@@ -1,4 +1,4 @@
1
- export declare const ARCANE_FRAG_WGSL = "struct Uniforms {\n uTime: f32,\n uFogColor: vec3<f32>,\n uFogDensity: f32,\n uFogHeightFalloff: f32,\n uFogEyeY: f32,\n uUnderwaterColor: vec3<f32>,\n uUnderwaterFogDensity: f32,\n uUnderwaterFactor: f32,\n uFogMode: i32,\n uFogNear: f32,\n uFogFar: f32,\n uNoiseOctaves: i32,\n uClipPlane: vec4<f32>,\n uClipEnabled: i32,\n uTint: vec3<f32>,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\nvar<private> vWorldPos_1: vec3<f32>;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vSeed_1: f32;\nvar<private> vDistance_1: f32;\nvar<private> outColor: vec4<f32>;\n\nfn mediumColor_u0028_() -> vec3<f32> {\n let _e59 = unnamed.uFogColor;\n let _e61 = unnamed.uUnderwaterColor;\n let _e63 = unnamed.uUnderwaterFactor;\n return mix(_e59, _e61, vec3(_e63));\n}\n\nfn mediumFog_u0028_f1_u003b_f1_u003b(dist: ptr<function, f32>, pointY: ptr<function, f32>) -> f32 {\n var span: f32;\n var ramp: f32;\n var wetLinear: f32;\n var start: f32;\n var before: f32;\n var local: f32;\n var t: f32;\n var rest: f32;\n var denom: f32;\n var air: f32;\n var wet: f32;\n\n let _e72 = unnamed.uFogMode;\n if (_e72 == 1i) {\n let _e75 = unnamed.uFogFar;\n let _e77 = unnamed.uFogNear;\n span = max((_e75 - _e77), 0.0001f);\n let _e80 = (*dist);\n let _e82 = unnamed.uFogNear;\n let _e84 = span;\n ramp = clamp(((_e80 - _e82) / _e84), 0f, 1f);\n let _e88 = unnamed.uUnderwaterFactor;\n if (_e88 <= 0f) {\n let _e90 = ramp;\n return _e90;\n }\n let _e92 = unnamed.uUnderwaterFogDensity;\n let _e93 = (*dist);\n wetLinear = (_e92 * _e93);\n let _e95 = ramp;\n let _e96 = wetLinear;\n let _e98 = wetLinear;\n let _e104 = unnamed.uUnderwaterFactor;\n return mix(_e95, (1f - exp2(((-(_e96) * _e98) * 1.442695f))), _e104);\n }\n let _e107 = unnamed.uFogNear;\n let _e108 = (*dist);\n start = min(_e107, _e108);\n let _e110 = (*dist);\n if (_e110 > 0f) {\n let _e112 = start;\n let _e113 = (*dist);\n local = (_e112 / _e113);\n } else {\n local = 0f;\n }\n let _e115 = local;\n before = _e115;\n let _e116 = (*pointY);\n let _e118 = unnamed.uFogEyeY;\n let _e121 = unnamed.uFogHeightFalloff;\n t = ((_e116 - _e118) * _e121);\n let _e123 = t;\n let _e124 = before;\n rest = (_e123 * (1f - _e124));\n let _e127 = rest;\n let _e130 = rest;\n denom = select(_e130, 0.0001f, (abs(_e127) < 0.0001f));\n let _e133 = unnamed.uFogDensity;\n let _e135 = t;\n let _e137 = before;\n let _e141 = (*dist);\n let _e142 = start;\n let _e145 = denom;\n let _e150 = denom;\n air = (1f - exp(((((-(_e133) * exp((-(_e135) * _e137))) * (_e141 - _e142)) * (1f - exp(-(_e145)))) / _e150)));\n let _e155 = unnamed.uUnderwaterFactor;\n if (_e155 <= 0f) {\n let _e157 = air;\n return _e157;\n }\n let _e159 = unnamed.uUnderwaterFogDensity;\n let _e160 = (*dist);\n wet = (_e159 * _e160);\n let _e162 = air;\n let _e163 = wet;\n let _e165 = wet;\n let _e171 = unnamed.uUnderwaterFactor;\n return mix(_e162, (1f - exp2(((-(_e163) * _e165) * 1.442695f))), _e171);\n}\n\nfn hash21_u0028_vf2_u003b(p: ptr<function, vec2<f32>>) -> f32 {\n let _e59 = (*p);\n return fract((sin(dot(_e59, vec2<f32>(127.1f, 311.7f))) * 43758.547f));\n}\n\nfn valueNoise_u0028_vf2_u003b(p_1: ptr<function, vec2<f32>>) -> f32 {\n var i: vec2<f32>;\n var f: vec2<f32>;\n var u: vec2<f32>;\n var param: vec2<f32>;\n var param_1: vec2<f32>;\n var param_2: vec2<f32>;\n var param_3: vec2<f32>;\n\n let _e66 = (*p_1);\n i = floor(_e66);\n let _e68 = (*p_1);\n f = fract(_e68);\n let _e70 = f;\n let _e71 = f;\n let _e73 = f;\n u = ((_e70 * _e71) * (vec2(3f) - (_e73 * 2f)));\n let _e78 = i;\n param = _e78;\n let _e79 = hash21_u0028_vf2_u003b((&param));\n let _e80 = i;\n param_1 = (_e80 + vec2<f32>(1f, 0f));\n let _e82 = hash21_u0028_vf2_u003b((&param_1));\n let _e84 = u[0u];\n let _e86 = i;\n param_2 = (_e86 + vec2<f32>(0f, 1f));\n let _e88 = hash21_u0028_vf2_u003b((&param_2));\n let _e89 = i;\n param_3 = (_e89 + vec2<f32>(1f, 1f));\n let _e91 = hash21_u0028_vf2_u003b((&param_3));\n let _e93 = u[0u];\n let _e96 = u[1u];\n return mix(mix(_e79, _e82, _e84), mix(_e88, _e91, _e93), _e96);\n}\n\nfn main_1() {\n var radius: f32;\n var d: vec2<f32>;\n var angle: f32;\n var t_1: f32;\n var inner: f32;\n var param_4: vec2<f32>;\n var outer: f32;\n var param_5: vec2<f32>;\n var swirl: f32;\n var param_6: vec2<f32>;\n var core: f32;\n var corona: f32;\n var body: f32;\n var col: vec3<f32>;\n var alpha: f32;\n var fog: f32;\n var param_7: f32;\n var param_8: f32;\n var phi_266_: bool;\n\n let _e77 = unnamed.uClipEnabled;\n let _e78 = (_e77 != 0i);\n phi_266_ = _e78;\n if _e78 {\n let _e79 = vWorldPos_1;\n let _e85 = unnamed.uClipPlane;\n phi_266_ = (dot(vec4<f32>(_e79.x, _e79.y, _e79.z, 1f), _e85) < 0f);\n }\n let _e89 = phi_266_;\n if _e89 {\n discard;\n }\n let _e90 = vUv_1;\n radius = (length((_e90 - vec2(0.5f))) * 2f);\n let _e95 = radius;\n if (_e95 >= 1f) {\n discard;\n }\n let _e97 = vUv_1;\n d = (_e97 - vec2(0.5f));\n let _e101 = d[1u];\n let _e103 = d[0u];\n angle = atan2(_e101, _e103);\n let _e106 = unnamed.uTime;\n let _e107 = vSeed_1;\n t_1 = (_e106 + (_e107 * 37f));\n let _e110 = angle;\n let _e112 = t_1;\n let _e115 = radius;\n param_4 = vec2<f32>(((_e110 * 1.9f) + (_e112 * 0.55f)), (_e115 * 4.2f));\n let _e118 = valueNoise_u0028_vf2_u003b((&param_4));\n inner = _e118;\n let _e119 = angle;\n let _e121 = t_1;\n let _e124 = radius;\n param_5 = vec2<f32>(((_e119 * 3.1f) - (_e121 * 0.34f)), (_e124 * 2.6f));\n let _e127 = valueNoise_u0028_vf2_u003b((&param_5));\n outer = _e127;\n let _e128 = inner;\n let _e130 = outer;\n swirl = ((_e128 * 0.62f) + (_e130 * 0.38f));\n let _e134 = unnamed.uNoiseOctaves;\n if (_e134 > 1i) {\n let _e136 = swirl;\n let _e137 = swirl;\n let _e138 = angle;\n let _e140 = t_1;\n let _e143 = radius;\n param_6 = vec2<f32>(((_e138 * 6.3f) + (_e140 * 0.9f)), (_e143 * 8f));\n let _e146 = valueNoise_u0028_vf2_u003b((&param_6));\n swirl = mix(_e136, ((_e137 * _e146) * 2f), 0.35f);\n }\n let _e150 = radius;\n core = (1f - smoothstep(0f, 0.28f, _e150));\n let _e153 = radius;\n let _e156 = swirl;\n corona = ((1f - smoothstep(0.18f, 1f, _e153)) * (0.35f + (_e156 * 0.65f)));\n let _e160 = core;\n let _e162 = corona;\n body = ((_e160 * 0.9f) + _e162);\n let _e164 = body;\n if (_e164 <= 0.02f) {\n discard;\n }\n let _e167 = unnamed.uTint;\n let _e170 = unnamed.uTint;\n let _e171 = body;\n col = mix((_e167 * 0.35f), _e170, vec3(smoothstep(0f, 0.55f, _e171)));\n let _e175 = col;\n let _e176 = body;\n col = mix(_e175, vec3<f32>(1f, 1f, 1f), vec3(smoothstep(0.82f, 1.4f, _e176)));\n let _e181 = unnamed.uTint;\n let _e182 = col;\n col = (_e182 * _e181);\n let _e184 = body;\n alpha = clamp((_e184 * 1.6f), 0f, 1f);\n let _e187 = vDistance_1;\n param_7 = _e187;\n let _e189 = vWorldPos_1[1u];\n param_8 = _e189;\n let _e190 = mediumFog_u0028_f1_u003b_f1_u003b((&param_7), (&param_8));\n fog = _e190;\n let _e191 = col;\n let _e192 = mediumColor_u0028_();\n let _e193 = fog;\n col = mix(_e191, _e192, vec3(_e193));\n let _e196 = fog;\n let _e198 = alpha;\n alpha = (_e198 * (1f - _e196));\n let _e200 = col;\n let _e201 = alpha;\n outColor = vec4<f32>(_e200.x, _e200.y, _e200.z, _e201);\n return;\n}\n\n@fragment \nfn main(@location(3) vWorldPos: vec3<f32>, @location(0) vUv: vec2<f32>, @location(1) vSeed: f32, @location(2) vDistance: f32) -> @location(0) vec4<f32> {\n vWorldPos_1 = vWorldPos;\n vUv_1 = vUv;\n vSeed_1 = vSeed;\n vDistance_1 = vDistance;\n main_1();\n let _e9 = outColor;\n return _e9;\n}\n";
1
+ export declare const ARCANE_FRAG_WGSL = "struct Uniforms {\n uTime: f32,\n uFogColor: vec3<f32>,\n uFogDensity: f32,\n uFogHeightFalloff: f32,\n uFogEyeY: f32,\n uUnderwaterColor: vec3<f32>,\n uUnderwaterFogDensity: f32,\n uUnderwaterFactor: f32,\n uFogMode: i32,\n uFogNear: f32,\n uFogFar: f32,\n uNoiseOctaves: i32,\n uClipPlane: vec4<f32>,\n uClipEnabled: i32,\n uOutputTransform: i32,\n uOutputExposure: f32,\n uTint: vec3<f32>,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\nvar<private> vWorldPos_1: vec3<f32>;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vSeed_1: f32;\nvar<private> vDistance_1: f32;\nvar<private> outColor: vec4<f32>;\n\nfn linearToSrgb_u0028_vf3_u003b(c: ptr<function, vec3<f32>>) -> vec3<f32> {\n var low: vec3<f32>;\n var high: vec3<f32>;\n\n let _e105 = (*c);\n low = (_e105 * 12.92f);\n let _e107 = (*c);\n high = ((pow(max(_e107, vec3<f32>(0f, 0f, 0f)), vec3<f32>(0.41666666f, 0.41666666f, 0.41666666f)) * 1.055f) - vec3(0.055f));\n let _e113 = high;\n let _e114 = low;\n let _e115 = (*c);\n return mix(_e113, _e114, step(_e115, vec3<f32>(0.0031308f, 0.0031308f, 0.0031308f)));\n}\n\nfn highlightShoulder_u0028_vf3_u003b(c_1: ptr<function, vec3<f32>>) -> vec3<f32> {\n var m: f32;\n var e: f32;\n\n let _e106 = (*c_1)[0u];\n let _e108 = (*c_1)[1u];\n let _e110 = (*c_1)[2u];\n m = max(_e106, max(_e108, _e110));\n let _e113 = m;\n if (_e113 <= 0.8f) {\n let _e115 = (*c_1);\n return _e115;\n }\n let _e116 = m;\n e = (_e116 - 0.8f);\n let _e118 = (*c_1);\n let _e119 = e;\n let _e121 = e;\n let _e125 = m;\n return (_e118 * ((0.8f + ((0.2f * _e119) / (_e121 + 0.2f))) / _e125));\n}\n\nfn rrtAndOdtFit_u0028_vf3_u003b(v: ptr<function, vec3<f32>>) -> vec3<f32> {\n var a: vec3<f32>;\n var b: vec3<f32>;\n\n let _e105 = (*v);\n let _e106 = (*v);\n a = ((_e105 * (_e106 + vec3(0.0245786f))) - vec3(0.000090537f));\n let _e112 = (*v);\n let _e113 = (*v);\n b = ((_e112 * ((_e113 * 0.983729f) + vec3(0.432951f))) + vec3(0.238081f));\n let _e120 = a;\n let _e121 = b;\n return (_e120 / _e121);\n}\n\nfn acesFilmic_u0028_vf3_u003b(x: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param: vec3<f32>;\n\n let _e105 = unnamed.uOutputExposure;\n let _e106 = (*x);\n (*x) = (_e106 * _e105);\n let _e108 = (*x);\n param = (mat3x3<f32>(vec3<f32>(0.59719f, 0.076f, 0.0284f), vec3<f32>(0.35458f, 0.90834f, 0.13383f), vec3<f32>(0.04823f, 0.01566f, 0.83777f)) * _e108);\n let _e110 = rrtAndOdtFit_u0028_vf3_u003b((&param));\n return clamp((mat3x3<f32>(vec3<f32>(1.60475f, -0.10208f, -0.00327f), vec3<f32>(-0.53108f, 1.10813f, -0.07276f), vec3<f32>(-0.07367f, -0.00605f, 1.07602f)) * _e110), vec3(0f), vec3(1f));\n}\n\nfn applyOutputTransform_u0028_vf3_u003b(c_2: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param_1: vec3<f32>;\n var param_2: vec3<f32>;\n var param_3: vec3<f32>;\n\n let _e107 = unnamed.uOutputTransform;\n if (_e107 == 0i) {\n let _e109 = (*c_2);\n return _e109;\n }\n let _e111 = unnamed.uOutputTransform;\n if (_e111 == 2i) {\n let _e113 = (*c_2);\n param_1 = _e113;\n let _e114 = acesFilmic_u0028_vf3_u003b((&param_1));\n (*c_2) = _e114;\n }\n let _e116 = unnamed.uOutputTransform;\n if (_e116 == 3i) {\n let _e118 = (*c_2);\n let _e120 = unnamed.uOutputExposure;\n param_2 = (_e118 * _e120);\n let _e122 = highlightShoulder_u0028_vf3_u003b((&param_2));\n (*c_2) = _e122;\n }\n let _e123 = (*c_2);\n param_3 = _e123;\n let _e124 = linearToSrgb_u0028_vf3_u003b((&param_3));\n return _e124;\n}\n\nfn mediumColor_u0028_() -> vec3<f32> {\n let _e103 = unnamed.uFogColor;\n let _e105 = unnamed.uUnderwaterColor;\n let _e107 = unnamed.uUnderwaterFactor;\n return mix(_e103, _e105, vec3(_e107));\n}\n\nfn mediumFog_u0028_f1_u003b_f1_u003b(dist: ptr<function, f32>, pointY: ptr<function, f32>) -> f32 {\n var span: f32;\n var ramp: f32;\n var wetLinear: f32;\n var start: f32;\n var before: f32;\n var local: f32;\n var t: f32;\n var rest: f32;\n var denom: f32;\n var air: f32;\n var wet: f32;\n\n let _e116 = unnamed.uFogMode;\n if (_e116 == 1i) {\n let _e119 = unnamed.uFogFar;\n let _e121 = unnamed.uFogNear;\n span = max((_e119 - _e121), 0.0001f);\n let _e124 = (*dist);\n let _e126 = unnamed.uFogNear;\n let _e128 = span;\n ramp = clamp(((_e124 - _e126) / _e128), 0f, 1f);\n let _e132 = unnamed.uUnderwaterFactor;\n if (_e132 <= 0f) {\n let _e134 = ramp;\n return _e134;\n }\n let _e136 = unnamed.uUnderwaterFogDensity;\n let _e137 = (*dist);\n wetLinear = (_e136 * _e137);\n let _e139 = ramp;\n let _e140 = wetLinear;\n let _e142 = wetLinear;\n let _e148 = unnamed.uUnderwaterFactor;\n return mix(_e139, (1f - exp2(((-(_e140) * _e142) * 1.442695f))), _e148);\n }\n let _e151 = unnamed.uFogNear;\n let _e152 = (*dist);\n start = min(_e151, _e152);\n let _e154 = (*dist);\n if (_e154 > 0f) {\n let _e156 = start;\n let _e157 = (*dist);\n local = (_e156 / _e157);\n } else {\n local = 0f;\n }\n let _e159 = local;\n before = _e159;\n let _e160 = (*pointY);\n let _e162 = unnamed.uFogEyeY;\n let _e165 = unnamed.uFogHeightFalloff;\n t = ((_e160 - _e162) * _e165);\n let _e167 = t;\n let _e168 = before;\n rest = (_e167 * (1f - _e168));\n let _e171 = rest;\n let _e174 = rest;\n denom = select(_e174, 0.0001f, (abs(_e171) < 0.0001f));\n let _e177 = unnamed.uFogDensity;\n let _e179 = t;\n let _e181 = before;\n let _e185 = (*dist);\n let _e186 = start;\n let _e189 = denom;\n let _e194 = denom;\n air = (1f - exp(((((-(_e177) * exp((-(_e179) * _e181))) * (_e185 - _e186)) * (1f - exp(-(_e189)))) / _e194)));\n let _e199 = unnamed.uUnderwaterFactor;\n if (_e199 <= 0f) {\n let _e201 = air;\n return _e201;\n }\n let _e203 = unnamed.uUnderwaterFogDensity;\n let _e204 = (*dist);\n wet = (_e203 * _e204);\n let _e206 = air;\n let _e207 = wet;\n let _e209 = wet;\n let _e215 = unnamed.uUnderwaterFactor;\n return mix(_e206, (1f - exp2(((-(_e207) * _e209) * 1.442695f))), _e215);\n}\n\nfn hash21_u0028_vf2_u003b(p: ptr<function, vec2<f32>>) -> f32 {\n let _e103 = (*p);\n return fract((sin(dot(_e103, vec2<f32>(127.1f, 311.7f))) * 43758.547f));\n}\n\nfn valueNoise_u0028_vf2_u003b(p_1: ptr<function, vec2<f32>>) -> f32 {\n var i: vec2<f32>;\n var f: vec2<f32>;\n var u: vec2<f32>;\n var param_4: vec2<f32>;\n var param_5: vec2<f32>;\n var param_6: vec2<f32>;\n var param_7: vec2<f32>;\n\n let _e110 = (*p_1);\n i = floor(_e110);\n let _e112 = (*p_1);\n f = fract(_e112);\n let _e114 = f;\n let _e115 = f;\n let _e117 = f;\n u = ((_e114 * _e115) * (vec2(3f) - (_e117 * 2f)));\n let _e122 = i;\n param_4 = _e122;\n let _e123 = hash21_u0028_vf2_u003b((&param_4));\n let _e124 = i;\n param_5 = (_e124 + vec2<f32>(1f, 0f));\n let _e126 = hash21_u0028_vf2_u003b((&param_5));\n let _e128 = u[0u];\n let _e130 = i;\n param_6 = (_e130 + vec2<f32>(0f, 1f));\n let _e132 = hash21_u0028_vf2_u003b((&param_6));\n let _e133 = i;\n param_7 = (_e133 + vec2<f32>(1f, 1f));\n let _e135 = hash21_u0028_vf2_u003b((&param_7));\n let _e137 = u[0u];\n let _e140 = u[1u];\n return mix(mix(_e123, _e126, _e128), mix(_e132, _e135, _e137), _e140);\n}\n\nfn main_1() {\n var radius: f32;\n var d: vec2<f32>;\n var angle: f32;\n var t_1: f32;\n var inner: f32;\n var param_8: vec2<f32>;\n var outer: f32;\n var param_9: vec2<f32>;\n var swirl: f32;\n var param_10: vec2<f32>;\n var core: f32;\n var corona: f32;\n var body: f32;\n var col: vec3<f32>;\n var alpha: f32;\n var fog: f32;\n var param_11: f32;\n var param_12: f32;\n var param_13: vec3<f32>;\n var phi_442_: bool;\n\n let _e122 = unnamed.uClipEnabled;\n let _e123 = (_e122 != 0i);\n phi_442_ = _e123;\n if _e123 {\n let _e124 = vWorldPos_1;\n let _e130 = unnamed.uClipPlane;\n phi_442_ = (dot(vec4<f32>(_e124.x, _e124.y, _e124.z, 1f), _e130) < 0f);\n }\n let _e134 = phi_442_;\n if _e134 {\n discard;\n }\n let _e135 = vUv_1;\n radius = (length((_e135 - vec2(0.5f))) * 2f);\n let _e140 = radius;\n if (_e140 >= 1f) {\n discard;\n }\n let _e142 = vUv_1;\n d = (_e142 - vec2(0.5f));\n let _e146 = d[1u];\n let _e148 = d[0u];\n angle = atan2(_e146, _e148);\n let _e151 = unnamed.uTime;\n let _e152 = vSeed_1;\n t_1 = (_e151 + (_e152 * 37f));\n let _e155 = angle;\n let _e157 = t_1;\n let _e160 = radius;\n param_8 = vec2<f32>(((_e155 * 1.9f) + (_e157 * 0.55f)), (_e160 * 4.2f));\n let _e163 = valueNoise_u0028_vf2_u003b((&param_8));\n inner = _e163;\n let _e164 = angle;\n let _e166 = t_1;\n let _e169 = radius;\n param_9 = vec2<f32>(((_e164 * 3.1f) - (_e166 * 0.34f)), (_e169 * 2.6f));\n let _e172 = valueNoise_u0028_vf2_u003b((&param_9));\n outer = _e172;\n let _e173 = inner;\n let _e175 = outer;\n swirl = ((_e173 * 0.62f) + (_e175 * 0.38f));\n let _e179 = unnamed.uNoiseOctaves;\n if (_e179 > 1i) {\n let _e181 = swirl;\n let _e182 = swirl;\n let _e183 = angle;\n let _e185 = t_1;\n let _e188 = radius;\n param_10 = vec2<f32>(((_e183 * 6.3f) + (_e185 * 0.9f)), (_e188 * 8f));\n let _e191 = valueNoise_u0028_vf2_u003b((&param_10));\n swirl = mix(_e181, ((_e182 * _e191) * 2f), 0.35f);\n }\n let _e195 = radius;\n core = (1f - smoothstep(0f, 0.28f, _e195));\n let _e198 = radius;\n let _e201 = swirl;\n corona = ((1f - smoothstep(0.18f, 1f, _e198)) * (0.35f + (_e201 * 0.65f)));\n let _e205 = core;\n let _e207 = corona;\n body = ((_e205 * 0.9f) + _e207);\n let _e209 = body;\n if (_e209 <= 0.02f) {\n discard;\n }\n let _e212 = unnamed.uTint;\n let _e215 = unnamed.uTint;\n let _e216 = body;\n col = mix((_e212 * 0.35f), _e215, vec3(smoothstep(0f, 0.55f, _e216)));\n let _e220 = col;\n let _e221 = body;\n col = mix(_e220, vec3<f32>(1f, 1f, 1f), vec3(smoothstep(0.82f, 1.4f, _e221)));\n let _e226 = unnamed.uTint;\n let _e227 = col;\n col = (_e227 * _e226);\n let _e229 = body;\n alpha = clamp((_e229 * 1.6f), 0f, 1f);\n let _e232 = vDistance_1;\n param_11 = _e232;\n let _e234 = vWorldPos_1[1u];\n param_12 = _e234;\n let _e235 = mediumFog_u0028_f1_u003b_f1_u003b((&param_11), (&param_12));\n fog = _e235;\n let _e236 = col;\n let _e237 = mediumColor_u0028_();\n let _e238 = fog;\n col = mix(_e236, _e237, vec3(_e238));\n let _e241 = fog;\n let _e243 = alpha;\n alpha = (_e243 * (1f - _e241));\n let _e245 = col;\n param_13 = _e245;\n let _e246 = applyOutputTransform_u0028_vf3_u003b((&param_13));\n let _e247 = alpha;\n outColor = vec4<f32>(_e246.x, _e246.y, _e246.z, _e247);\n return;\n}\n\n@fragment \nfn main(@location(3) vWorldPos: vec3<f32>, @location(0) vUv: vec2<f32>, @location(1) vSeed: f32, @location(2) vDistance: f32) -> @location(0) vec4<f32> {\n vWorldPos_1 = vWorldPos;\n vUv_1 = vUv;\n vSeed_1 = vSeed;\n vDistance_1 = vDistance;\n main_1();\n let _e9 = outColor;\n return _e9;\n}\n";
2
2
  /**
3
3
  * What the transform assigned, so the renderer binds the same numbers.
4
4
  *
@@ -79,6 +79,16 @@ export declare const ARCANE_BINDINGS: {
79
79
  readonly size: 4;
80
80
  readonly type: "int";
81
81
  };
82
+ readonly uOutputTransform: {
83
+ readonly offset: 116;
84
+ readonly size: 4;
85
+ readonly type: "int";
86
+ };
87
+ readonly uOutputExposure: {
88
+ readonly offset: 120;
89
+ readonly size: 4;
90
+ readonly type: "float";
91
+ };
82
92
  readonly uTint: {
83
93
  readonly offset: 128;
84
94
  readonly size: 12;