@driftengine/core 4.13.4 → 4.13.5

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 (143) hide show
  1. package/CHANGELOG.json +36 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +3 -2
  4. package/dist/render/anisotropicLobe.d.ts +5 -0
  5. package/dist/render/anisotropicLobe.js +13 -0
  6. package/dist/render/atmosphere.d.ts +79 -1
  7. package/dist/render/atmosphere.js +66 -19
  8. package/dist/render/backend/webgl2/renderer.d.ts +34 -2
  9. package/dist/render/backend/webgl2/renderer.js +44 -12
  10. package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
  11. package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
  12. package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
  13. package/dist/render/backend/webgpu/gpuDrivenPass.js +20 -3
  14. package/dist/render/backend/webgpu/linePass.d.ts +1 -1
  15. package/dist/render/backend/webgpu/mediumFields.d.ts +17 -0
  16. package/dist/render/backend/webgpu/mediumFields.js +74 -0
  17. package/dist/render/backend/webgpu/plumePass.d.ts +3 -3
  18. package/dist/render/backend/webgpu/postPass.d.ts +7 -2
  19. package/dist/render/backend/webgpu/renderer.d.ts +13 -3
  20. package/dist/render/backend/webgpu/renderer.js +86 -150
  21. package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
  22. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  23. package/dist/render/environmentMixing.d.ts +18 -0
  24. package/dist/render/environmentMixing.js +40 -0
  25. package/dist/render/fog.d.ts +43 -1
  26. package/dist/render/fog.js +87 -22
  27. package/dist/render/globalMedium.d.ts +112 -10
  28. package/dist/render/globalMedium.js +96 -1
  29. package/dist/render/globalMediumPass.d.ts +7 -3
  30. package/dist/render/globalMediumPass.js +22 -1
  31. package/dist/render/hairLobes.d.ts +5 -3
  32. package/dist/render/hairLobes.js +11 -7
  33. package/dist/render/lightBudget.d.ts +12 -0
  34. package/dist/render/lightBudget.js +24 -0
  35. package/dist/render/sceneTarget.d.ts +5 -3
  36. package/dist/render/sceneTarget.js +14 -8
  37. package/dist/render/shaders/arcane.d.ts +1 -1
  38. package/dist/render/shaders/bolt.d.ts +1 -1
  39. package/dist/render/shaders/caustics.d.ts +1 -1
  40. package/dist/render/shaders/film.d.ts +1 -1
  41. package/dist/render/shaders/film.js +4 -1
  42. package/dist/render/shaders/fire.d.ts +1 -1
  43. package/dist/render/shaders/flat/anisotropic.d.ts +8 -1
  44. package/dist/render/shaders/flat/anisotropic.js +25 -3
  45. package/dist/render/shaders/flat/hair.js +31 -7
  46. package/dist/render/shaders/flat/main.d.ts +1 -1
  47. package/dist/render/shaders/flat/main.js +20 -1
  48. package/dist/render/shaders/flat/models.js +1 -1
  49. package/dist/render/shaders/flat/preamble.js +7 -0
  50. package/dist/render/shaders/fog.d.ts +4 -4
  51. package/dist/render/shaders/fog.js +81 -32
  52. package/dist/render/shaders/generated/arcane.wgsl.d.ts +29 -39
  53. package/dist/render/shaders/generated/arcane.wgsl.js +29 -39
  54. package/dist/render/shaders/generated/bolt.wgsl.d.ts +26 -36
  55. package/dist/render/shaders/generated/bolt.wgsl.js +26 -36
  56. package/dist/render/shaders/generated/caustics.wgsl.d.ts +25 -35
  57. package/dist/render/shaders/generated/caustics.wgsl.js +25 -35
  58. package/dist/render/shaders/generated/film.wgsl.d.ts +28 -38
  59. package/dist/render/shaders/generated/film.wgsl.js +28 -38
  60. package/dist/render/shaders/generated/fire.wgsl.d.ts +28 -38
  61. package/dist/render/shaders/generated/fire.wgsl.js +28 -38
  62. package/dist/render/shaders/generated/flat.wgsl.d.ts +1466 -1546
  63. package/dist/render/shaders/generated/flat.wgsl.js +1489 -1569
  64. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +98 -12
  65. package/dist/render/shaders/generated/globalMedium.wgsl.js +98 -12
  66. package/dist/render/shaders/generated/line.wgsl.d.ts +25 -35
  67. package/dist/render/shaders/generated/line.wgsl.js +25 -35
  68. package/dist/render/shaders/generated/particle.wgsl.d.ts +100 -140
  69. package/dist/render/shaders/generated/particle.wgsl.js +100 -140
  70. package/dist/render/shaders/generated/rush.wgsl.d.ts +20 -10
  71. package/dist/render/shaders/generated/rush.wgsl.js +20 -10
  72. package/dist/render/shaders/generated/scatter.wgsl.d.ts +25 -35
  73. package/dist/render/shaders/generated/scatter.wgsl.js +25 -35
  74. package/dist/render/shaders/generated/smoke.wgsl.d.ts +28 -38
  75. package/dist/render/shaders/generated/smoke.wgsl.js +28 -38
  76. package/dist/render/shaders/generated/water.wgsl.d.ts +29 -39
  77. package/dist/render/shaders/generated/water.wgsl.js +29 -39
  78. package/dist/render/shaders/globalMedium.js +114 -10
  79. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +2 -1
  80. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +10 -4
  81. package/dist/render/shaders/gpudriven/fog.wgsl.js +77 -19
  82. package/dist/render/shaders/gpudriven/frameBlock.wgsl.d.ts +1 -1
  83. package/dist/render/shaders/gpudriven/frameBlock.wgsl.js +12 -1
  84. package/dist/render/shaders/gpudriven/shade.wgsl.js +3 -1
  85. package/dist/render/shaders/line.d.ts +1 -1
  86. package/dist/render/shaders/line.js +4 -2
  87. package/dist/render/shaders/localExposure.d.ts +42 -1
  88. package/dist/render/shaders/localExposure.js +73 -6
  89. package/dist/render/shaders/particle.d.ts +3 -3
  90. package/dist/render/shaders/particle.js +10 -6
  91. package/dist/render/shaders/scatter.d.ts +1 -1
  92. package/dist/render/shaders/scatter.js +4 -2
  93. package/dist/render/shaders/smoke.d.ts +1 -1
  94. package/dist/render/shaders/water.d.ts +1 -1
  95. package/dist/render/shaders/water.js +2 -1
  96. package/dist/render/surfaceModel.d.ts +23 -4
  97. package/dist/render/surfaceModel.js +12 -2
  98. package/package.json +6 -6
  99. package/src/index.ts +12 -2
  100. package/src/render/anisotropicLobe.ts +20 -0
  101. package/src/render/atmosphere.ts +153 -20
  102. package/src/render/backend/webgl2/renderer.ts +63 -13
  103. package/src/render/backend/webgpu/gpuDrivenPass.ts +20 -3
  104. package/src/render/backend/webgpu/mediumFields.ts +87 -0
  105. package/src/render/backend/webgpu/renderer.ts +109 -157
  106. package/src/render/environmentMixing.ts +48 -0
  107. package/src/render/fog.ts +125 -25
  108. package/src/render/globalMedium.ts +214 -11
  109. package/src/render/globalMediumPass.ts +29 -4
  110. package/src/render/hairLobes.ts +35 -4
  111. package/src/render/lightBudget.ts +32 -0
  112. package/src/render/sceneTarget.ts +15 -11
  113. package/src/render/shaders/film.ts +4 -1
  114. package/src/render/shaders/flat/anisotropic.ts +25 -3
  115. package/src/render/shaders/flat/hair.ts +31 -7
  116. package/src/render/shaders/flat/main.ts +20 -1
  117. package/src/render/shaders/flat/models.ts +1 -1
  118. package/src/render/shaders/flat/preamble.ts +7 -0
  119. package/src/render/shaders/fog.ts +81 -32
  120. package/src/render/shaders/generated/arcane.wgsl.ts +29 -39
  121. package/src/render/shaders/generated/bolt.wgsl.ts +26 -36
  122. package/src/render/shaders/generated/caustics.wgsl.ts +25 -35
  123. package/src/render/shaders/generated/film.wgsl.ts +28 -38
  124. package/src/render/shaders/generated/fire.wgsl.ts +28 -38
  125. package/src/render/shaders/generated/flat.wgsl.ts +1489 -1569
  126. package/src/render/shaders/generated/globalMedium.wgsl.ts +98 -12
  127. package/src/render/shaders/generated/line.wgsl.ts +25 -35
  128. package/src/render/shaders/generated/particle.wgsl.ts +100 -140
  129. package/src/render/shaders/generated/rush.wgsl.ts +20 -10
  130. package/src/render/shaders/generated/scatter.wgsl.ts +25 -35
  131. package/src/render/shaders/generated/smoke.wgsl.ts +28 -38
  132. package/src/render/shaders/generated/water.wgsl.ts +29 -39
  133. package/src/render/shaders/globalMedium.ts +114 -10
  134. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +2 -1
  135. package/src/render/shaders/gpudriven/fog.wgsl.ts +78 -19
  136. package/src/render/shaders/gpudriven/frameBlock.wgsl.ts +12 -1
  137. package/src/render/shaders/gpudriven/shade.wgsl.ts +3 -1
  138. package/src/render/shaders/line.ts +4 -2
  139. package/src/render/shaders/localExposure.ts +115 -6
  140. package/src/render/shaders/particle.ts +10 -6
  141. package/src/render/shaders/scatter.ts +4 -2
  142. package/src/render/shaders/water.ts +2 -1
  143. package/src/render/surfaceModel.ts +41 -6
package/CHANGELOG.json CHANGED
@@ -1,4 +1,40 @@
1
1
  [
2
+ {
3
+ "version": "4.13.5",
4
+ "date": "2026-10-12",
5
+ "entries": [
6
+ {
7
+ "kind": "added",
8
+ "short": "The haze takes a second layer, a most it may hide, a distance past which it hides nothing, and the sun glowing in it: `fogLayer2`, `fogMaxOpacity`, `fogCutoff` and `fogSunGlow`.",
9
+ "text": "The environment's haze takes four more fields, in every pass that fogs and in the GPU-driven pipeline. `fogLayer2` is a second layer, its own density at its own height thinning by its own falloff, summed with the first along every ray: a ground mist under a high summer haze, where one layer had to pick one of the two scale heights. `fogMaxOpacity` is the most of a surface the haze may hide, so a skyline keeps that much of itself however much air lies in front of it. `fogCutoff` is a distance past which the haze hides nothing, the sky included, for a backdrop that brings its own haze painted in. `fogSunGlow` is the sun's light the haze scatters toward the eye: a colour looking straight at the sun through haze that hides everything, falling off as the cosine of the angle from it to an exponent, beginning a distance past where the haze begins, and added after the fog rather than mixed in, so a far ridge seen into a low sun burns gold. Every new lane reads as none at zero, so a pass a binder never wrote them into draws the haze it drew before. A haze authored in base two converts to the engine's natural units as density `ρ · (ln 2)²` and falloff `f · ln 2`, which the manual states. The haze's eight scalar uniforms are packed into two vectors, which paid for the three new ones and freed two more of the lit stage's rows on a phone's 256. Measured on `demo/dev/fogStart.html`, eight white patches at known distances: each option reads its hand-derived value to a level on WebGPU and on WebGL2 over Vulkan and over OpenGL, and the GPU-driven pipeline's fog agrees with the reference in `fog.ts` to 7.8e-5 over 102 glowing channels. Every published scene draws what 4.13.4 drew, to the pixel on WebGPU."
10
+ },
11
+ {
12
+ "kind": "added",
13
+ "short": "The global medium thins with height in two layers, takes a coloured albedo, and scatters the sun, the sky and the lamps at strengths of their own, each lamp through its own shadows.",
14
+ "text": "`setGlobalMedium` takes an object beside its four numbers, with what the numbers cannot say: `heightFalloff` and `baseY`, so the medium thins with height as the environment's haze does, and `layer2`, a second layer, integrated over each step of the march rather than sampled; `albedo` as a colour, a share a channel; `sunScatter` and `skyScatter`, how strongly the sun and the sky's fill scatter, 1 as each lights a surface; and `lampScatter`, the lamps, 0 by default so a scene that never asks keeps its picture and its cost. A lamp is the lit stage's own, shaped by its falloff and its cone and cut by its own static and live shadow maps, so a lamp behind a post throws the post's shadow into the air, and `lightMediumScatter` in the environment scales each lamp's own. A medium standing for the near part of the environment's haze hands the rest of the ray to that haze by setting `fogStart` to its `maxDistance`: the reach is where the medium ends, which the documentation had wrongly said extinction went on past and the march never did. Measured: with the sun and the sky at 0 no pixel rises, a red albedo raises no green or blue, a falloff that empties the room draws the frame without the pass to the pixel, and turning a lamp's maps off brightens 12,722 pixels of air behind the post on `demo/dev/pointshadow.html`, where with the lamp out of the medium it moves 2,928, on WebGPU and WebGL2 alike. `scripts/medium-check.mjs` had gone red on three claims when 4.13.0 made the screen encoding the default, because its unshadowed runs clipped to white; it reads the linear output now, and all six claims hold on both backends with the numbers the build before this one gives."
15
+ },
16
+ {
17
+ "kind": "added",
18
+ "short": "Local exposure takes a shape: each region turned about middle grey at the frame’s exposure, a share kept above it and another below, and a pixel’s own detail kept apart.",
19
+ "text": "`setLocalExposure` takes a shape beside its one number: `{ highlightContrast, shadowContrast, detail, blurredBlend, middleGreyBias, range }`. Each region is turned about middle grey at the frame's exposure, the auto exposure's gain and the output exposure together, rather than about the frame's metered brightness: it keeps `highlightContrast` of its stops from grey where it is brighter and `shadowContrast` where it is darker, and each pixel keeps `detail` of its own stops from its region, so above 1 sharpens the texture a curve would flatten. `blurredBlend` leans a region's level toward its tile's mean, 0.4 as the engine has held it, `middleGreyBias` adds stops to grey, and `range` is the furthest a region moves. The one number is the shape it always was, both shares one less the strength about the held level, and it draws what it drew to the bit, measured on both backends. On `demo/dev/localExposure.html`, a sunlit wall half pale and half dark: keeping half the stops each side brings the clipped pale half down 0.79 of a stop, enough for its stripes to show again, and the dark half up 0.82, and a detail of 1.5 widens the stripes by half while the halves hold."
20
+ },
21
+ {
22
+ "kind": "added",
23
+ "short": "Every surface's reflection can be brought to the light where it stands, by a weight mixed in by roughness: a mirror keeps the capture, a rough surface takes its place's light.",
24
+ "text": "`setEnvironmentMixing(weight, roughnessLow, roughnessHigh)` scales what every surface reflects of the environment, metal or not, by `lerp(1, weight, smoothstep(roughnessLow, roughnessHigh, roughness))`, held until changed and put back at `bindMeshPass`. A probe is captured at one place and seen from many, so a figure standing where the indirect light is twice what the probe saw reflected the probe too dimly against everything lit around it, and `setEnvironmentGain` reached metals only while `setSurfaceReflectivity` stopped at 1. The weight is the place's indirect light over the probe's own average brightness, above 1 or below, and the roughnesses are where it is mixed in, 0.1 and 0.3 by default, so a mirror keeps its image. Measured on the models page: a weight of 1 is the frame without it to the pixel, a weight of 2 brightens 60,314 pixels of the row's reflections, and the backends agree to 7 pixels. It costs the lit stage one uniform row."
25
+ },
26
+ {
27
+ "kind": "added",
28
+ "short": "Hair reads each strand's specular and direction texel by texel: the model map's red scales the white highlight, and a layer of its own can lean strands out of the card.",
29
+ "text": "The hair model reads two things a texel that it took once a material or from the mesh. The model map's red is the strand's specular: the reflected lobe is scaled by twice it, in the lamps' lobes and in the light from every side alike, so 0.5 is the highlight as drawn and 0 a root that reflects nothing; a hair model map whose red was left at 0 now draws no white highlight, and 128 keeps the old look. And `hairModel({ directionOffset })` names a layer of the model map, that many past the texel's own, holding each strand's direction in the surface's tangent frame, packed as a normal map packs a normal: the strand runs along it rather than along the tangent, so a card's strands lean out of its plane each its own way. Measured on the models page's strands: no map draws what it drew, a red of 0 and of 1 move 83,451 and 82,138 pixels, strands leaned 30 degrees move 118,342, and the backends agree to 31 pixels."
30
+ },
31
+ {
32
+ "kind": "added",
33
+ "short": "A brushed surface's highlight can be lifted where it is bright, its strength texel by texel: `anisotropicModel({ lift })`, scaled by the model map's alpha.",
34
+ "text": "`anisotropicModel({ lift })` raises the anisotropic distribution where it is bright, `D + min(3, D^1.5) · lift`, the shape a brushed finish that reads as polished along its streak is authored with, applied as a factor of the distribution at the half vector so the peak-normalised lobe and the physical one lift alike. Where a model map is bound its alpha scales the lift texel by texel, so a map storing a tenth of each region's lift takes `lift: 10`. 0, the default, draws what it drew; on the models page's brushed discs a lift of 3 moves 55,287 pixels of their streaks and the backends agree to 11."
35
+ }
36
+ ]
37
+ },
2
38
  {
3
39
  "version": "4.13.4",
4
40
  "date": "2026-10-12",
package/README.md CHANGED
@@ -6,7 +6,7 @@ WebGL2 everywhere else, behind one API, and carries the fixed-step loop, the ren
6
6
  input, the scene graph, geometry, text and the whole of `@driftengine/physics`, which it re-exports.
7
7
  Everything else is a separate package a game adds when it reaches for it.
8
8
 
9
- **Cost: 930.5 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
9
+ **Cost: 951.0 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
10
10
  which fails the build if the figure drifts by more than 3%.
11
11
 
12
12
  The manual is at **[driftengine.dev/docs](https://driftengine.dev/docs)**, with an example running
package/dist/index.d.ts CHANGED
@@ -178,7 +178,7 @@ export type { FrameTimer } from './render/backend/timer.ts';
178
178
  export type { SceneCasterMaterial, SceneCasters, ShadowCasterCloth, ShadowCasterSink, ShadowCasters, } from './render/shadowCasters.ts';
179
179
  export type { StaticDrawsHandle } from './render/staticDraws.ts';
180
180
  export type { BoneAnimationClip, BoneAnimationHandle } from './render/boneAnimation.ts';
181
- export type { Atmosphere, UnderwaterAtmosphere } from './render/atmosphere.ts';
181
+ export type { Atmosphere, FogLayer, FogSunGlow, UnderwaterAtmosphere, } from './render/atmosphere.ts';
182
182
  export { createImageTexelDecoder } from './render/imageTexels.ts';
183
183
  export type { ImageTexelDecoder, ImageTexels } from './render/imageTexels.ts';
184
184
  export { DEFAULT_RENDER_QUALITY, resolveRenderQuality } from './render/renderQuality.ts';
@@ -190,7 +190,8 @@ export { DEFAULT_RENDER_QUALITY, resolveRenderQuality } from './render/renderQua
190
190
  * is the type the setter takes and the two bounds a settings screen has to know about.
191
191
  */
192
192
  export { DEFAULT_GLOBAL_MEDIUM, MAX_GLOBAL_MEDIUM_STEPS, WEAK_GPU_MEDIUM_STEPS, resolveGlobalMedium, } from './render/globalMedium.ts';
193
- export type { GlobalMediumOptions } from './render/globalMedium.ts';
193
+ export type { GlobalMediumOptions, GlobalMediumSettings, ResolvedGlobalMedium, } from './render/globalMedium.ts';
194
+ export type { LocalExposureOptions } from './render/shaders/localExposure.ts';
194
195
  export { equirectToCubeFaces } from './render/equirectToCube.ts';
195
196
  export type { EquirectSource } from './render/equirectToCube.ts';
196
197
  export { IRRADIANCE_EDGE, PREFILTER_SAMPLE_COUNTS, ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor, roughnessForLevel, } from './render/prefilterEnvMap.ts';
@@ -4,6 +4,11 @@ export declare function anisotropicAlphas(roughness: number, strength: number, o
4
4
  * The lobe at a half vector whose components along the direction, across it and along the normal
5
5
  * are `th`, `bh` and `nh`. 1 at the mirror direction.
6
6
  */
7
+ /**
8
+ * How much the lift brightens the stretched lobe at a half vector: `(D + min(3, D^1.5) · k) / D`,
9
+ * with D the distribution there, `1 / (π αT αB d²)`, as the shader's `anisotropicLift` has it.
10
+ */
11
+ export declare function anisotropicLiftFactor(th: number, bh: number, nh: number, alphaT: number, alphaB: number, lift: number): number;
7
12
  export declare function anisotropicLobe(th: number, bh: number, nh: number, alphaT: number, alphaB: number): number;
8
13
  /** Three components in the lobe's frame: along the direction, across it, along the normal. */
9
14
  export type FrameVector = readonly [number, number, number];
@@ -25,6 +25,19 @@ export function anisotropicAlphas(roughness, strength, out) {
25
25
  * The lobe at a half vector whose components along the direction, across it and along the normal
26
26
  * are `th`, `bh` and `nh`. 1 at the mirror direction.
27
27
  */
28
+ /**
29
+ * How much the lift brightens the stretched lobe at a half vector: `(D + min(3, D^1.5) · k) / D`,
30
+ * with D the distribution there, `1 / (π αT αB d²)`, as the shader's `anisotropicLift` has it.
31
+ */
32
+ export function anisotropicLiftFactor(th, bh, nh, alphaT, alphaB, lift) {
33
+ if (lift <= 0)
34
+ return 1;
35
+ const x = th / alphaT;
36
+ const y = bh / alphaB;
37
+ const d = x * x + y * y + nh * nh;
38
+ const distribution = 1 / Math.max(Math.PI * alphaT * alphaB * d * d, 1e-8);
39
+ return 1 + lift * Math.min(3 / distribution, Math.sqrt(distribution));
40
+ }
28
41
  export function anisotropicLobe(th, bh, nh, alphaT, alphaB) {
29
42
  const x = th / alphaT;
30
43
  const y = bh / alphaB;
@@ -10,6 +10,42 @@ export interface UnderwaterAtmosphere {
10
10
  /** Half-width of the smooth air↔water transition around the surface. */
11
11
  transitionDepth: number;
12
12
  }
13
+ /**
14
+ * A layer of haze: how thick it is where it is thickest, and how fast it thins above that.
15
+ *
16
+ * In natural units, as `fogDensity` is: a haze authored in base two, its surviving light
17
+ * `2^(−∫ρ)` and its density falling as `2^(−f·Δy)`, is the layer `density: ρ · (ln 2)²`,
18
+ * `heightFalloff: f · ln 2` here — the integral of base two's exponential carries one `ln 2` of its
19
+ * own, and the transmittance's base the other.
20
+ */
21
+ export interface FogLayer {
22
+ /** Extinction per metre at `baseY`. */
23
+ density: number;
24
+ /** Reciprocal scale height, per metre: 0 is a layer of even density. */
25
+ heightFalloff: number;
26
+ /** The height it is thickest at. */
27
+ baseY: number;
28
+ }
29
+ /**
30
+ * The sun's light scattered toward the eye by the haze: a glow in the sun's direction, as dust and
31
+ * droplets throw forward what they catch.
32
+ *
33
+ * Added to what the haze returns rather than mixed with it, by as much of the ray past `start` as
34
+ * the haze hides: a mountain range seen into a low sun burns gold at its distance, and the
35
+ * figures a few metres off do not. **What it gives up** is the light's own shape: the glow is a
36
+ * lobe about one direction, `cos^exponent` of the angle from it, not light scattered along the
37
+ * ray through the shadows the sun's map knows of — the global medium is that one.
38
+ */
39
+ export interface FogSunGlow {
40
+ /** The glow looking straight along `direction` through haze that hides everything. */
41
+ color: Vec3;
42
+ /** How tight the glow is about the sun: the cosine of the angle from it, to this power. */
43
+ exponent: number;
44
+ /** Metres past the haze's own start before the glow begins. Default 0. */
45
+ start?: number;
46
+ /** Surface → the light the glow surrounds. Absent, the environment's `directionalDir`. */
47
+ direction?: Vec3;
48
+ }
13
49
  /** Atmosphere fields shared by every visible render pass. */
14
50
  export interface Atmosphere {
15
51
  fogColor: Vec3;
@@ -58,6 +94,31 @@ export interface Atmosphere {
58
94
  * between the camera and the start, which a real medium has; that is the point of it.
59
95
  */
60
96
  fogStart?: number;
97
+ /**
98
+ * The medium only: a second layer of haze, summed with the first along every ray, each its own
99
+ * density, height and falloff. Absent, one layer.
100
+ *
101
+ * Two layers are what a valley with a ground mist under a high summer haze is: one thin and
102
+ * tall, one dense and hugging the floor. One layer must pick one of the two scale heights, and
103
+ * either gets the other's half of the picture wrong.
104
+ */
105
+ fogLayer2?: FogLayer | null;
106
+ /**
107
+ * The most of a surface the haze may hide, 0 to 1. Absent or 1, all of it.
108
+ *
109
+ * Below 1 a far mountain keeps that much of itself however much air lies between, which is how a
110
+ * scene keeps a skyline readable behind a haze that should hide everything nearer the ground.
111
+ * **What it gives up** is physics: no medium has a floor on what survives it.
112
+ */
113
+ fogMaxOpacity?: number;
114
+ /**
115
+ * Metres from the camera past which the haze hides nothing: a surface further away, the sky
116
+ * included, is drawn clear. Absent or 0, none. For a sky or a backdrop that brings its own
117
+ * haze painted in, which the medium would otherwise lay a second time.
118
+ */
119
+ fogCutoff?: number;
120
+ /** The medium only: the sun's light scattered toward the eye by the haze. Absent, none. */
121
+ fogSunGlow?: FogSunGlow | null;
61
122
  underwater: UnderwaterAtmosphere | null;
62
123
  }
63
124
  /**
@@ -80,6 +141,16 @@ export declare function fogDensityAtEye(atmosphere: Atmosphere, cameraY: number)
80
141
  */
81
142
  export interface ResolvedAtmosphere {
82
143
  readonly fogColor: Float32Array;
144
+ /**
145
+ * Four floats: the second layer's extinction per metre at the camera's height and its falloff,
146
+ * then the least of a surface the haze must leave, `fogMaxOpacity`'s complement, and the cutoff
147
+ * distance. **Every lane is none at zero**, so a pass that never wrote them draws as before.
148
+ */
149
+ readonly fogLayer2: Float32Array;
150
+ /** Four floats: the sun glow's colour, black for none, and its exponent. */
151
+ readonly fogGlow: Float32Array;
152
+ /** Four floats: surface → the glow's light, and how far past the haze's start the glow begins. */
153
+ readonly fogGlowDir: Float32Array;
83
154
  fogDensity: number;
84
155
  fogHeightFalloff: number;
85
156
  fogEyeY: number;
@@ -98,7 +169,14 @@ export interface ResolvedAtmosphere {
98
169
  * allows no allocation in one. Each backend keeps a single `ResolvedAtmosphere` and refills
99
170
  * it; nothing here allocates and nothing is shared between them.
100
171
  */
101
- export declare function resolveAtmosphere(atmosphere: Atmosphere, cameraY: number, underwaterEnabled: boolean, out: ResolvedAtmosphere): ResolvedAtmosphere;
172
+ export declare function resolveAtmosphere(atmosphere: Atmosphere & {
173
+ readonly directionalDir?: ArrayLike<number>;
174
+ }, cameraY: number, underwaterEnabled: boolean, out: ResolvedAtmosphere): ResolvedAtmosphere;
175
+ /**
176
+ * A target for `resolveAtmosphere`, made once outside the frame loop: one factory, so a field added
177
+ * to the resolved medium is a field every holder of one gets rather than one each must remember.
178
+ */
179
+ export declare function createResolvedAtmosphere(): ResolvedAtmosphere;
102
180
  /**
103
181
  * Bind the same camera medium to any shader that declares atmosphere uniforms.
104
182
  * Missing optional uniforms are harmless WebGL null locations, so mesh, sky,
@@ -64,21 +64,69 @@ export function resolveAtmosphere(atmosphere, cameraY, underwaterEnabled, out) {
64
64
  out.underwaterColor.set(underwater === null ? atmosphere.fogColor : underwater.color);
65
65
  out.underwaterFogDensity = underwater === null ? atmosphere.fogDensity : underwater.fogDensity;
66
66
  out.underwaterFactor = factor;
67
+ resolveHazeExtras(atmosphere, cameraY, out);
67
68
  return out;
68
69
  }
70
+ /**
71
+ * The second layer, the haze's limits and the sun's glow, into `out`: the layer and the glow only
72
+ * in the medium, since a linear ramp has neither a density nor a ray to integrate, and every slot
73
+ * written whatever is asked, so nothing a shader reads keeps what an earlier frame left in it.
74
+ */
75
+ function resolveHazeExtras(atmosphere, cameraY, out) {
76
+ const medium = atmosphere.fogMode !== 'linear';
77
+ const layer = medium ? (atmosphere.fogLayer2 ?? null) : null;
78
+ const density = layer === null ? 0 : Math.max(0, layer.density);
79
+ /* The second layer at the camera's height, as `fogDensityAtEye` puts the first. */
80
+ out.fogLayer2[0] =
81
+ density > 0 && layer !== null
82
+ ? density * Math.exp(-(cameraY - layer.baseY) * layer.heightFalloff)
83
+ : 0;
84
+ out.fogLayer2[1] = density > 0 && layer !== null ? layer.heightFalloff : 0;
85
+ const most = atmosphere.fogMaxOpacity;
86
+ out.fogLayer2[2] =
87
+ most !== undefined && Number.isFinite(most) ? 1 - Math.min(1, Math.max(0, most)) : 0;
88
+ const cutoff = atmosphere.fogCutoff;
89
+ out.fogLayer2[3] = cutoff !== undefined && Number.isFinite(cutoff) && cutoff > 0 ? cutoff : 0;
90
+ const glow = medium ? (atmosphere.fogSunGlow ?? null) : null;
91
+ const toward = glow?.direction ?? atmosphere.directionalDir;
92
+ const x = toward?.[0] ?? 0;
93
+ const y = toward?.[1] ?? 1;
94
+ const z = toward?.[2] ?? 0;
95
+ const length = Math.hypot(x, y, z);
96
+ const lit = glow !== null && length > 1e-6;
97
+ for (let c = 0; c < 3; c++) {
98
+ out.fogGlow[c] = lit && glow !== null ? Math.max(0, glow.color[c]) : 0;
99
+ }
100
+ /* Above zero, so the shader's pow never meets 0 to the 0: a lobe that flat is the whole sky. */
101
+ out.fogGlow[3] = lit && glow !== null ? Math.max(1e-3, glow.exponent) : 1;
102
+ out.fogGlowDir[0] = lit ? x / length : 0;
103
+ out.fogGlowDir[1] = lit ? y / length : 1;
104
+ out.fogGlowDir[2] = lit ? z / length : 0;
105
+ out.fogGlowDir[3] = lit && glow !== null ? Math.max(0, glow.start ?? 0) : 0;
106
+ }
107
+ /**
108
+ * A target for `resolveAtmosphere`, made once outside the frame loop: one factory, so a field added
109
+ * to the resolved medium is a field every holder of one gets rather than one each must remember.
110
+ */
111
+ export function createResolvedAtmosphere() {
112
+ return {
113
+ fogColor: new Float32Array(3),
114
+ fogLayer2: new Float32Array(4),
115
+ fogGlow: new Float32Array(4),
116
+ fogGlowDir: new Float32Array(4),
117
+ underwaterColor: new Float32Array(3),
118
+ fogDensity: 0,
119
+ fogHeightFalloff: 0,
120
+ fogEyeY: 0,
121
+ fogMode: 0,
122
+ fogNear: 0,
123
+ fogFar: 1,
124
+ underwaterFogDensity: 0,
125
+ underwaterFactor: 0,
126
+ };
127
+ }
69
128
  /** One per GL binder, refilled per call. See `resolveAtmosphere` on why it is not allocated. */
70
- const glResolved = {
71
- fogColor: new Float32Array(3),
72
- underwaterColor: new Float32Array(3),
73
- fogDensity: 0,
74
- fogHeightFalloff: 0,
75
- fogEyeY: 0,
76
- fogMode: 0,
77
- fogNear: 0,
78
- fogFar: 1,
79
- underwaterFogDensity: 0,
80
- underwaterFactor: 0,
81
- };
129
+ const glResolved = createResolvedAtmosphere();
82
130
  /**
83
131
  * Bind the same camera medium to any shader that declares atmosphere uniforms.
84
132
  * Missing optional uniforms are harmless WebGL null locations, so mesh, sky,
@@ -87,13 +135,12 @@ const glResolved = {
87
135
  export function bindAtmosphere(gl, uniforms, atmosphere, cameraY, underwaterEnabled) {
88
136
  const m = resolveAtmosphere(atmosphere, cameraY, underwaterEnabled, glResolved);
89
137
  gl.uniform3fv(uniforms['uFogColor'] ?? null, m.fogColor);
90
- gl.uniform1f(uniforms['uFogDensity'] ?? null, m.fogDensity);
91
- gl.uniform1f(uniforms['uFogHeightFalloff'] ?? null, m.fogHeightFalloff);
92
- gl.uniform1f(uniforms['uFogEyeY'] ?? null, m.fogEyeY);
93
- gl.uniform1i(uniforms['uFogMode'] ?? null, m.fogMode);
94
- gl.uniform1f(uniforms['uFogNear'] ?? null, m.fogNear);
95
- gl.uniform1f(uniforms['uFogFar'] ?? null, m.fogFar);
138
+ /* Packed as `shaders/fog.ts` declares them, four to a row: see `uFogMedium` there for why. */
139
+ gl.uniform4f(uniforms['uFogMedium'] ?? null, m.fogDensity, m.fogHeightFalloff, m.fogEyeY, m.underwaterFogDensity);
140
+ gl.uniform4f(uniforms['uFogRange'] ?? null, m.fogNear, m.fogFar, m.fogMode, 0);
96
141
  gl.uniform3fv(uniforms['uUnderwaterColor'] ?? null, m.underwaterColor);
97
- gl.uniform1f(uniforms['uUnderwaterFogDensity'] ?? null, m.underwaterFogDensity);
98
142
  gl.uniform1f(uniforms['uUnderwaterFactor'] ?? null, m.underwaterFactor);
143
+ gl.uniform4fv(uniforms['uFogLayer2'] ?? null, m.fogLayer2);
144
+ gl.uniform4fv(uniforms['uFogGlow'] ?? null, m.fogGlow);
145
+ gl.uniform4fv(uniforms['uFogGlowDir'] ?? null, m.fogGlowDir);
99
146
  }
@@ -4,6 +4,7 @@ import type { GlassOptions } from '../../glass.ts';
4
4
  import type { ReadonlyMat4 } from 'gl-matrix';
5
5
  import type { Camera } from '../../camera.ts';
6
6
  import type { PickHit, PickableSource } from '../../pickable.ts';
7
+ import type { GlobalMediumSettings } from '../../globalMedium.ts';
7
8
  import type { ReflectiveSurface } from '../../screenSpaceReflection.ts';
8
9
  import type { DecalProjector } from '../../decalProjector.ts';
9
10
  import type { FieldSource } from '../../gi/globalField.ts';
@@ -11,6 +12,7 @@ import type { Bounds } from '../../../math/bounds.ts';
11
12
  import type { ComputeDefinition, ComputeHandle } from '../../compute.ts';
12
13
  import type { PassDefinition, PassHandle } from '../../pass.ts';
13
14
  import type { Vec3 } from '../../../math/color.ts';
15
+ import type { LocalExposureOptions } from '../../shaders/localExposure.ts';
14
16
  import { GpuTimer } from '../../gpuTimer.ts';
15
17
  import { type SurfaceOverlay } from '../../surfaceOverlay.ts';
16
18
  import type { ColourGradeLut } from '../../colourGrade.ts';
@@ -210,6 +212,12 @@ export interface Environment extends Atmosphere {
210
212
  * every light. See `PointLightSource.falloffExponent`.
211
213
  */
212
214
  lightFalloffExponents?: Float32Array;
215
+ /**
216
+ * One per active light: how strongly it scatters in the global medium, a share of how strongly it
217
+ * lights a surface, beside `setGlobalMedium`'s `lampScatter`. Optional: absent is 1 for every
218
+ * light. See `PointLightSet.lightMediumScatter`.
219
+ */
220
+ lightMediumScatter?: Float32Array;
213
221
  /** One per active light: a row of the photometric atlas, or −1 for none. */
214
222
  lightIesProfiles: Float32Array;
215
223
  /**
@@ -1041,6 +1049,8 @@ export declare class WebGL2Renderer implements RendererApi {
1041
1049
  * vector on the device, so written whole, and kept here so a setter moving one keeps the others.
1042
1050
  */
1043
1051
  private readonly environmentDials;
1052
+ /** `uEnvironmentMixing` as it stands: see `setEnvironmentMixing`. None until a draw asks. */
1053
+ private readonly environmentMixing;
1044
1054
  /** `uEmission` as it stands: the emissive gain and the night factor. */
1045
1055
  private readonly emission;
1046
1056
  /** The ramp and each level's tint, as `resolveBloomResponse` writes them; the subtraction and white. */
@@ -2232,6 +2242,25 @@ export declare class WebGL2Renderer implements RendererApi {
2232
2242
  * legitimate as one above. Held non-negative, because a negative environment is not a darker one.
2233
2243
  */
2234
2244
  setEnvironmentGain(gain: number): void;
2245
+ /**
2246
+ * How much of the light where the surfaces drawn next stand their reflection is brought to, for
2247
+ * every surface, metal or not: what each reflects of the environment times
2248
+ * `lerp(1, weight, smoothstep(roughnessLow, roughnessHigh, roughness))`. 1 is the default and
2249
+ * identity; held until changed, and put back at `bindMeshPass`.
2250
+ *
2251
+ * **Why there is a weight at all**: a probe is captured at one place and seen from many, so a
2252
+ * figure standing where the indirect light is twice what the probe saw reflects the probe at the
2253
+ * probe's brightness and reads as dull against everything lit around it. `weight` is that
2254
+ * ratio, the place's indirect light over the probe's own average, and a caller that knows both
2255
+ * passes it; above 1 brightens, below dims. A mirror keeps the capture as it is, because its
2256
+ * image is the point, and a rough surface, whose reflection is an average anyway, takes its
2257
+ * place's light: the roughnesses say where between, 0.1 and 0.3 by default.
2258
+ *
2259
+ * Unlike `setEnvironmentGain`, which reaches metals only, and `setSurfaceReflectivity`, which
2260
+ * stops at 1. **What it gives up** is a reflection that stays the probe's image on a rough
2261
+ * surface: past the upper roughness it is the probe scaled to the light, not the place relit.
2262
+ */
2263
+ setEnvironmentMixing(weight: number, roughnessLow?: number, roughnessHigh?: number): void;
2235
2264
  /**
2236
2265
  * Kept as a no-op that says so once, because it was a stand-in for a term that now exists.
2237
2266
  *
@@ -3383,7 +3412,7 @@ export declare class WebGL2Renderer implements RendererApi {
3383
3412
  * two regions of one brightness band differ. Needs `screenEffects` and `hdrScene`, and says so
3384
3413
  * once rather than doing nothing. See `shaders/localExposure.ts`.
3385
3414
  */
3386
- setLocalExposure(strength: number): void;
3415
+ setLocalExposure(setting: number | LocalExposureOptions): void;
3387
3416
  /**
3388
3417
  * Where ambient occlusion fades out with distance: whole up to `distance` metres from the eye and
3389
3418
  * gone `radius` metres past it, so the far depth buffer's steps — a sky dome kilometres off, a
@@ -3477,9 +3506,12 @@ export declare class WebGL2Renderer implements RendererApi {
3477
3506
  * this is everything between the camera and whatever the frame already drew, and it is the one
3478
3507
  * of the two that can put the shape of a doorway on a floor.
3479
3508
  */
3480
- setGlobalMedium(density: number, albedo?: number, anisotropy?: number, maxDistance?: number): void;
3509
+ setGlobalMedium(density: number | GlobalMediumSettings, albedo?: number | Vec3, anisotropy?: number, maxDistance?: number): void;
3481
3510
  /** This frame's medium. Off until a caller says otherwise. See `setGlobalMedium`. */
3482
3511
  private mediumOptions;
3512
+ /** The lamps the medium is lit by, and the lit stage's lamps they come from, refilled a frame. */
3513
+ private readonly mediumLamps;
3514
+ private readonly mediumLights;
3483
3515
  /** Built on the first frame that asks for a medium, and never on one that does not. */
3484
3516
  private medium;
3485
3517
  /**
@@ -15,8 +15,9 @@ import { DecalPass } from '../../decalPass.js';
15
15
  import { SsrPass } from '../../ssrPass.js';
16
16
  import { ReflectionSurfaceTargets } from '../../reflectionSurfaceTargets.js';
17
17
  import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
18
+ import { noEnvironmentMixing, resolveEnvironmentMixing } from '../../environmentMixing.js';
18
19
  import { GlobalMediumPass } from '../../globalMediumPass.js';
19
- import { DEFAULT_GLOBAL_MEDIUM, WEAK_GPU_MEDIUM_STEPS, mediumActive, resolveGlobalMedium, } from '../../globalMedium.js';
20
+ import { DEFAULT_GLOBAL_MEDIUM, WEAK_GPU_MEDIUM_STEPS, createResolvedMediumLamps, mediumActive, resolveGlobalMedium, resolveGlobalMediumSettings, resolveMediumLamps, } from '../../globalMedium.js';
20
21
  import { REFLECTION_EDGE_FADE, ReflectionQueue } from '../../screenSpaceReflection.js';
21
22
  import { DecalQueue } from '../../decalQueue.js';
22
23
  import { OitPass } from '../../oitPass.js';
@@ -54,7 +55,7 @@ import { WorldLightField } from '../../driftLight/worldLightField.js';
54
55
  import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
55
56
  import { flatFrag, flatVert } from '../../shaders/flat/index.js';
56
57
  import { skinEight } from '../../shaders/skinning.js';
57
- import { MODEL_MAP_TEXTURE_UNIT, MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT, } from '../../lightBudget.js';
58
+ import { MODEL_MAP_TEXTURE_UNIT, MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT, createResolvedPointLights, resolvePointLights, } from '../../lightBudget.js';
58
59
  import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
59
60
  import { lightmapRegionsBound, modelBound, skinHalfBound } from '../../shaders/flat/models.js';
60
61
  import { lightmapTexels, warnRegionsWithoutPage, warnUnpagedLightmap, withLightmapUvs, } from '../../lightmap.js';
@@ -718,6 +719,8 @@ export class WebGL2Renderer {
718
719
  * vector on the device, so written whole, and kept here so a setter moving one keeps the others.
719
720
  */
720
721
  environmentDials = new Float32Array(3);
722
+ /** `uEnvironmentMixing` as it stands: see `setEnvironmentMixing`. None until a draw asks. */
723
+ environmentMixing = noEnvironmentMixing(new Float32Array(4));
721
724
  /** `uEmission` as it stands: the emissive gain and the night factor. */
722
725
  emission = new Float32Array([1, 0]);
723
726
  /** The ramp and each level's tint, as `resolveBloomResponse` writes them; the subtraction and white. */
@@ -3691,6 +3694,31 @@ export class WebGL2Renderer {
3691
3694
  this.passEnvironmentGain = Math.max(0, gain);
3692
3695
  this.writeEnvironmentDials();
3693
3696
  }
3697
+ /**
3698
+ * How much of the light where the surfaces drawn next stand their reflection is brought to, for
3699
+ * every surface, metal or not: what each reflects of the environment times
3700
+ * `lerp(1, weight, smoothstep(roughnessLow, roughnessHigh, roughness))`. 1 is the default and
3701
+ * identity; held until changed, and put back at `bindMeshPass`.
3702
+ *
3703
+ * **Why there is a weight at all**: a probe is captured at one place and seen from many, so a
3704
+ * figure standing where the indirect light is twice what the probe saw reflects the probe at the
3705
+ * probe's brightness and reads as dull against everything lit around it. `weight` is that
3706
+ * ratio, the place's indirect light over the probe's own average, and a caller that knows both
3707
+ * passes it; above 1 brightens, below dims. A mirror keeps the capture as it is, because its
3708
+ * image is the point, and a rough surface, whose reflection is an average anyway, takes its
3709
+ * place's light: the roughnesses say where between, 0.1 and 0.3 by default.
3710
+ *
3711
+ * Unlike `setEnvironmentGain`, which reaches metals only, and `setSurfaceReflectivity`, which
3712
+ * stops at 1. **What it gives up** is a reflection that stays the probe's image on a rough
3713
+ * surface: past the upper roughness it is the probe scaled to the light, not the place relit.
3714
+ */
3715
+ setEnvironmentMixing(weight, roughnessLow = 0.1, roughnessHigh = 0.3) {
3716
+ if (this.contextLost)
3717
+ return;
3718
+ resolveEnvironmentMixing(weight, roughnessLow, roughnessHigh, this.environmentMixing);
3719
+ this.materials.dirty();
3720
+ this.writeFlatVector('uEnvironmentMixing', this.environmentMixing);
3721
+ }
3694
3722
  /**
3695
3723
  * Kept as a no-op that says so once, because it was a stand-in for a term that now exists.
3696
3724
  *
@@ -6395,12 +6423,12 @@ export class WebGL2Renderer {
6395
6423
  * two regions of one brightness band differ. Needs `screenEffects` and `hdrScene`, and says so
6396
6424
  * once rather than doing nothing. See `shaders/localExposure.ts`.
6397
6425
  */
6398
- setLocalExposure(strength) {
6426
+ setLocalExposure(setting) {
6399
6427
  if (this.sceneTarget === null || !this.quality.hdrScene) {
6400
- this.warnLookWithoutComposite(strength);
6428
+ this.warnLookWithoutComposite(typeof setting === 'number' ? setting : 1);
6401
6429
  return;
6402
6430
  }
6403
- this.sceneTarget.setLocalExposure(strength);
6431
+ this.sceneTarget.setLocalExposure(setting);
6404
6432
  }
6405
6433
  /**
6406
6434
  * Where ambient occlusion fades out with distance: whole up to `distance` metres from the eye and
@@ -6508,15 +6536,13 @@ export class WebGL2Renderer {
6508
6536
  * of the two that can put the shape of a doorway on a floor.
6509
6537
  */
6510
6538
  setGlobalMedium(density, albedo, anisotropy, maxDistance) {
6511
- this.mediumOptions = resolveGlobalMedium({
6512
- density: Number.isFinite(density) ? density : 0,
6513
- albedo,
6514
- anisotropy,
6515
- maxDistance,
6516
- });
6539
+ this.mediumOptions = resolveGlobalMediumSettings(density, albedo, anisotropy, maxDistance);
6517
6540
  }
6518
6541
  /** This frame's medium. Off until a caller says otherwise. See `setGlobalMedium`. */
6519
- mediumOptions = DEFAULT_GLOBAL_MEDIUM;
6542
+ mediumOptions = resolveGlobalMedium(DEFAULT_GLOBAL_MEDIUM);
6543
+ /** The lamps the medium is lit by, and the lit stage's lamps they come from, refilled a frame. */
6544
+ mediumLamps = createResolvedMediumLamps();
6545
+ mediumLights = createResolvedPointLights();
6520
6546
  /** Built on the first frame that asks for a medium, and never on one that does not. */
6521
6547
  medium = null;
6522
6548
  /**
@@ -7265,6 +7291,9 @@ export class WebGL2Renderer {
7265
7291
  so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
7266
7292
  sunShadows: this.sunShadows?.texture ?? this.emptyTexture2DArray,
7267
7293
  peeledEnabled: this.peelFilled,
7294
+ /* The lit stage's lamps and the shadows it resolved for them, this frame's. */
7295
+ lamps: resolveMediumLamps(this.mediumOptions, resolvePointLights(env, this.quality.pointLightFalloff, this.mediumLights), this.pointShadows === null ? null : this.resolvedPointShadows, env?.lightMediumScatter, this.mediumLamps),
7296
+ pointShadows: this.pointShadowArray?.texture ?? this.emptyTexture2DArray,
7268
7297
  });
7269
7298
  if (marched) {
7270
7299
  /* The march finished on the default framebuffer, which is not where the frame is being
@@ -7512,6 +7541,8 @@ export class WebGL2Renderer {
7512
7541
  this.materialEnvironmentGain = null;
7513
7542
  this.environmentDials[1] = 0;
7514
7543
  this.environmentDials[2] = 1;
7544
+ /* And no mixing, for the same reason. See `setEnvironmentMixing`. */
7545
+ noEnvironmentMixing(this.environmentMixing);
7515
7546
  this.emission[0] = env.emissiveGain;
7516
7547
  this.emission[1] = env.nightFactor;
7517
7548
  /* The moving layer's own matrix, the first time a frame draws that layer with one. */
@@ -7731,6 +7762,7 @@ export class WebGL2Renderer {
7731
7762
  * See `setSurfaceReflectivity` and `setEnvironmentGain`.
7732
7763
  */
7733
7764
  gl.uniform3fv(u['uEnvironmentDials'] ?? null, this.environmentDials);
7765
+ gl.uniform4fv(u['uEnvironmentMixing'] ?? null, this.environmentMixing);
7734
7766
  /* And no relief unless a caller asks, for the same reason. */
7735
7767
  gl.uniform2f(u['uRelief'] ?? null, 0, 60);
7736
7768
  /* And no relief read off a surface texture either, for the same reason. */
@@ -11,7 +11,7 @@ import type { PipelineCache } from './pipelineCache.ts';
11
11
  export declare const BOLT_VERT_FIELDS: UniformFields;
12
12
  export declare const BOLT_VERT_SIZE: 96;
13
13
  export declare const BOLT_FRAG_FIELDS: UniformFields;
14
- export declare const BOLT_FRAG_SIZE: 144;
14
+ export declare const BOLT_FRAG_SIZE: 192;
15
15
  /** A batch of arcs on the device, behind the surface's opaque handle. */
16
16
  export interface GpuBolts {
17
17
  readonly fromBuffer: GPUBuffer;
@@ -10,7 +10,7 @@ import type { PipelineCache } from './pipelineCache.ts';
10
10
  export declare const CAUSTICS_VERT_FIELDS: UniformFields;
11
11
  export declare const CAUSTICS_VERT_SIZE: 64;
12
12
  export declare const CAUSTICS_FRAG_FIELDS: UniformFields;
13
- export declare const CAUSTICS_FRAG_SIZE: 160;
13
+ export declare const CAUSTICS_FRAG_SIZE: 208;
14
14
  /** A lit surface on the device, behind the API's opaque handle. */
15
15
  export interface GpuCaustics {
16
16
  readonly vertexBuffers: readonly GPUBuffer[];
@@ -14,7 +14,7 @@ import type { PipelineCache } from './pipelineCache.ts';
14
14
  export declare const FILM_VERT_FIELDS: UniformFields;
15
15
  export declare const FILM_VERT_SIZE: 128;
16
16
  export declare const FILM_FRAG_FIELDS: UniformFields;
17
- export declare const FILM_FRAG_SIZE: 160;
17
+ export declare const FILM_FRAG_SIZE: 208;
18
18
  export declare function createFilmBindGroupLayout(device: GPUDevice): GPUBindGroupLayout;
19
19
  /** Bound once against both rings; a dynamic offset per film. See `createPanelBindGroup`. */
20
20
  export declare function createFilmBindGroup(device: GPUDevice, layout: GPUBindGroupLayout, vertices: GPUBuffer, fragments: GPUBuffer, reflection: GPUTextureView, sampler: GPUSampler): GPUBindGroup;
@@ -123,11 +123,11 @@ export const VERTEX_FLOATS = GPU_DRIVEN_VERTEX_FLOATS;
123
123
  /**
124
124
  * Floats the shading pass's frame block holds. Matches `FRAME_BLOCK_WGSL`'s `Frame`.
125
125
  *
126
- * Two matrices and fifteen vectors: the camera's, the light's, then the eye, the light's direction
126
+ * Two matrices and eighteen vectors: the camera's, the light's, then the eye, the light's direction
127
127
  * and colour, the sky, the ground, the target's size, the shadow's settings in two, the probe's in
128
- * two, the medium's four, and the visibility buffer's row.
128
+ * two, the medium's four, the visibility buffer's row, and the medium's last three after it.
129
129
  */
130
- const FRAME_FLOATS = 92;
130
+ const FRAME_FLOATS = 104;
131
131
  /** Where each block starts in it, so the writer and the shader cannot drift apart. */
132
132
  const FRAME_LIGHT_VIEW_PROJ = 16;
133
133
  const FRAME_EYE = 32;
@@ -145,6 +145,9 @@ const FRAME_FOG_UNDERWATER = 76;
145
145
  const FRAME_FOG_HEIGHT = 80;
146
146
  const FRAME_FOG_RANGE = 84;
147
147
  const FRAME_PITCH = 88;
148
+ const FRAME_FOG_LIMITS = 92;
149
+ const FRAME_FOG_GLOW = 96;
150
+ const FRAME_FOG_GLOW_DIR = 100;
148
151
  /**
149
152
  * Pixels the internal width is rounded up to.
150
153
  *
@@ -2168,6 +2171,20 @@ export class GpuDrivenPass {
2168
2171
  params[FRAME_FOG_RANGE + 1] = fog?.far ?? 0;
2169
2172
  params[FRAME_FOG_RANGE + 2] = fog?.mode ?? MEDIUM_FOG;
2170
2173
  params[FRAME_FOG_RANGE + 3] = fog?.underwaterDensity ?? 0;
2174
+ /* The second layer, the floor, the cutoff and the glow: every one none at zero, as a view
2175
+ written before they existed asks. */
2176
+ params[FRAME_FOG_HEIGHT + 2] = fog?.density2 ?? 0;
2177
+ params[FRAME_FOG_HEIGHT + 3] = fog?.heightFalloff2 ?? 0;
2178
+ params[FRAME_FOG_LIMITS] = fog?.floor ?? 0;
2179
+ params[FRAME_FOG_LIMITS + 1] = fog?.cutoff ?? 0;
2180
+ params[FRAME_FOG_GLOW] = fog?.glowColour?.[0] ?? 0;
2181
+ params[FRAME_FOG_GLOW + 1] = fog?.glowColour?.[1] ?? 0;
2182
+ params[FRAME_FOG_GLOW + 2] = fog?.glowColour?.[2] ?? 0;
2183
+ params[FRAME_FOG_GLOW + 3] = fog?.glowExponent ?? 1;
2184
+ params[FRAME_FOG_GLOW_DIR] = fog?.glowDirection?.[0] ?? 0;
2185
+ params[FRAME_FOG_GLOW_DIR + 1] = fog?.glowDirection?.[1] ?? 1;
2186
+ params[FRAME_FOG_GLOW_DIR + 2] = fog?.glowDirection?.[2] ?? 0;
2187
+ params[FRAME_FOG_GLOW_DIR + 3] = fog?.glowStart ?? 0;
2171
2188
  /* The scene's own decision about whether its grid supplies the ambient, as it is on the
2172
2189
  forward path: `ProbeBakeOptions.irradiance` reaches here as `PassEnvironment.irradiance`. */
2173
2190
  params[FRAME_ENVIRONMENT_MIX] = room !== null && room.irradiance ? 1 : 0;
@@ -29,7 +29,7 @@ import type { PipelineCache } from './pipelineCache.ts';
29
29
  export declare const LINE_VERT_FIELDS: UniformFields;
30
30
  export declare const LINE_VERT_SIZE: 160;
31
31
  export declare const LINE_FRAG_FIELDS: UniformFields;
32
- export declare const LINE_FRAG_SIZE: 112;
32
+ export declare const LINE_FRAG_SIZE: 160;
33
33
  /** A polyline batch on the device, behind the surface's opaque handle. */
34
34
  export interface GpuLines {
35
35
  readonly fromBuffer: GPUBuffer;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The medium's fields, every uniform `shaders/fog.ts` declares, written into a pass's block by name
3
+ * from one resolved atmosphere.
4
+ *
5
+ * **One writer for every pass that fogs.** Ten passes wrote the same ten fields by hand, so a field
6
+ * the medium grows was ten places to remember and a pass that missed one read zero: the fourth
7
+ * point of the rule `AGENTS.md` dates 2026-08-13. A field a pass's shader never reads is compiled
8
+ * out of its block, so each is written only where the block holds it. An absent field is not an
9
+ * offset of zero, which is the first field of the block.
10
+ */
11
+ import type { ResolvedAtmosphere } from '../../atmosphere.ts';
12
+ import type { UniformFields } from './scatterPass.ts';
13
+ import type { UniformRing } from './uniformRing.ts';
14
+ /** Into a block staged as floats, indexed in four-byte words. */
15
+ export declare function writeMediumFields(fields: UniformFields, f: Float32Array, m: ResolvedAtmosphere): void;
16
+ /** Into one slot of a ring, at the fields' byte offsets. */
17
+ export declare function writeMediumSlot(ring: UniformRing, slot: number, fields: UniformFields, m: ResolvedAtmosphere): void;