@driftengine/core 4.13.3 → 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 (153) hide show
  1. package/CHANGELOG.json +52 -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/clothSetPack.js +1 -0
  13. package/dist/render/backend/webgpu/clothSolvePack.d.ts +5 -2
  14. package/dist/render/backend/webgpu/clothSolvePack.js +2 -1
  15. package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
  16. package/dist/render/backend/webgpu/gpuClothSet.js +1 -1
  17. package/dist/render/backend/webgpu/gpuDrivenPass.js +20 -3
  18. package/dist/render/backend/webgpu/linePass.d.ts +1 -1
  19. package/dist/render/backend/webgpu/mediumFields.d.ts +17 -0
  20. package/dist/render/backend/webgpu/mediumFields.js +74 -0
  21. package/dist/render/backend/webgpu/plumePass.d.ts +3 -3
  22. package/dist/render/backend/webgpu/postPass.d.ts +7 -2
  23. package/dist/render/backend/webgpu/renderer.d.ts +13 -3
  24. package/dist/render/backend/webgpu/renderer.js +86 -150
  25. package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
  26. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
  27. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -5
  28. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  29. package/dist/render/environmentMixing.d.ts +18 -0
  30. package/dist/render/environmentMixing.js +40 -0
  31. package/dist/render/fog.d.ts +43 -1
  32. package/dist/render/fog.js +87 -22
  33. package/dist/render/globalMedium.d.ts +112 -10
  34. package/dist/render/globalMedium.js +96 -1
  35. package/dist/render/globalMediumPass.d.ts +7 -3
  36. package/dist/render/globalMediumPass.js +22 -1
  37. package/dist/render/hairLobes.d.ts +27 -2
  38. package/dist/render/hairLobes.js +57 -4
  39. package/dist/render/lightBudget.d.ts +12 -0
  40. package/dist/render/lightBudget.js +24 -0
  41. package/dist/render/sceneTarget.d.ts +5 -3
  42. package/dist/render/sceneTarget.js +14 -8
  43. package/dist/render/shaders/arcane.d.ts +1 -1
  44. package/dist/render/shaders/bolt.d.ts +1 -1
  45. package/dist/render/shaders/caustics.d.ts +1 -1
  46. package/dist/render/shaders/film.d.ts +1 -1
  47. package/dist/render/shaders/film.js +4 -1
  48. package/dist/render/shaders/fire.d.ts +1 -1
  49. package/dist/render/shaders/flat/anisotropic.d.ts +8 -1
  50. package/dist/render/shaders/flat/anisotropic.js +25 -3
  51. package/dist/render/shaders/flat/hair.js +77 -27
  52. package/dist/render/shaders/flat/main.d.ts +1 -1
  53. package/dist/render/shaders/flat/main.js +23 -3
  54. package/dist/render/shaders/flat/models.js +12 -5
  55. package/dist/render/shaders/flat/preamble.js +7 -0
  56. package/dist/render/shaders/fog.d.ts +4 -4
  57. package/dist/render/shaders/fog.js +81 -32
  58. package/dist/render/shaders/generated/arcane.wgsl.d.ts +29 -39
  59. package/dist/render/shaders/generated/arcane.wgsl.js +29 -39
  60. package/dist/render/shaders/generated/bolt.wgsl.d.ts +26 -36
  61. package/dist/render/shaders/generated/bolt.wgsl.js +26 -36
  62. package/dist/render/shaders/generated/caustics.wgsl.d.ts +25 -35
  63. package/dist/render/shaders/generated/caustics.wgsl.js +25 -35
  64. package/dist/render/shaders/generated/film.wgsl.d.ts +28 -38
  65. package/dist/render/shaders/generated/film.wgsl.js +28 -38
  66. package/dist/render/shaders/generated/fire.wgsl.d.ts +28 -38
  67. package/dist/render/shaders/generated/fire.wgsl.js +28 -38
  68. package/dist/render/shaders/generated/flat.wgsl.d.ts +1466 -1546
  69. package/dist/render/shaders/generated/flat.wgsl.js +1489 -1569
  70. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +98 -12
  71. package/dist/render/shaders/generated/globalMedium.wgsl.js +98 -12
  72. package/dist/render/shaders/generated/line.wgsl.d.ts +25 -35
  73. package/dist/render/shaders/generated/line.wgsl.js +25 -35
  74. package/dist/render/shaders/generated/particle.wgsl.d.ts +100 -140
  75. package/dist/render/shaders/generated/particle.wgsl.js +100 -140
  76. package/dist/render/shaders/generated/rush.wgsl.d.ts +20 -10
  77. package/dist/render/shaders/generated/rush.wgsl.js +20 -10
  78. package/dist/render/shaders/generated/scatter.wgsl.d.ts +25 -35
  79. package/dist/render/shaders/generated/scatter.wgsl.js +25 -35
  80. package/dist/render/shaders/generated/smoke.wgsl.d.ts +28 -38
  81. package/dist/render/shaders/generated/smoke.wgsl.js +28 -38
  82. package/dist/render/shaders/generated/water.wgsl.d.ts +29 -39
  83. package/dist/render/shaders/generated/water.wgsl.js +29 -39
  84. package/dist/render/shaders/globalMedium.js +114 -10
  85. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +2 -1
  86. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +10 -4
  87. package/dist/render/shaders/gpudriven/fog.wgsl.js +77 -19
  88. package/dist/render/shaders/gpudriven/frameBlock.wgsl.d.ts +1 -1
  89. package/dist/render/shaders/gpudriven/frameBlock.wgsl.js +12 -1
  90. package/dist/render/shaders/gpudriven/shade.wgsl.js +3 -1
  91. package/dist/render/shaders/line.d.ts +1 -1
  92. package/dist/render/shaders/line.js +4 -2
  93. package/dist/render/shaders/localExposure.d.ts +42 -1
  94. package/dist/render/shaders/localExposure.js +73 -6
  95. package/dist/render/shaders/particle.d.ts +3 -3
  96. package/dist/render/shaders/particle.js +10 -6
  97. package/dist/render/shaders/scatter.d.ts +1 -1
  98. package/dist/render/shaders/scatter.js +4 -2
  99. package/dist/render/shaders/smoke.d.ts +1 -1
  100. package/dist/render/shaders/water.d.ts +1 -1
  101. package/dist/render/shaders/water.js +2 -1
  102. package/dist/render/surfaceModel.d.ts +23 -4
  103. package/dist/render/surfaceModel.js +12 -2
  104. package/package.json +6 -6
  105. package/src/index.ts +12 -2
  106. package/src/render/anisotropicLobe.ts +20 -0
  107. package/src/render/atmosphere.ts +153 -20
  108. package/src/render/backend/webgl2/renderer.ts +63 -13
  109. package/src/render/backend/webgpu/clothSetPack.ts +1 -0
  110. package/src/render/backend/webgpu/clothSolvePack.ts +6 -2
  111. package/src/render/backend/webgpu/gpuClothSet.ts +1 -1
  112. package/src/render/backend/webgpu/gpuDrivenPass.ts +20 -3
  113. package/src/render/backend/webgpu/mediumFields.ts +87 -0
  114. package/src/render/backend/webgpu/renderer.ts +109 -157
  115. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -5
  116. package/src/render/environmentMixing.ts +48 -0
  117. package/src/render/fog.ts +125 -25
  118. package/src/render/globalMedium.ts +214 -11
  119. package/src/render/globalMediumPass.ts +29 -4
  120. package/src/render/hairLobes.ts +109 -3
  121. package/src/render/lightBudget.ts +32 -0
  122. package/src/render/sceneTarget.ts +15 -11
  123. package/src/render/shaders/film.ts +4 -1
  124. package/src/render/shaders/flat/anisotropic.ts +25 -3
  125. package/src/render/shaders/flat/hair.ts +77 -27
  126. package/src/render/shaders/flat/main.ts +23 -3
  127. package/src/render/shaders/flat/models.ts +12 -5
  128. package/src/render/shaders/flat/preamble.ts +7 -0
  129. package/src/render/shaders/fog.ts +81 -32
  130. package/src/render/shaders/generated/arcane.wgsl.ts +29 -39
  131. package/src/render/shaders/generated/bolt.wgsl.ts +26 -36
  132. package/src/render/shaders/generated/caustics.wgsl.ts +25 -35
  133. package/src/render/shaders/generated/film.wgsl.ts +28 -38
  134. package/src/render/shaders/generated/fire.wgsl.ts +28 -38
  135. package/src/render/shaders/generated/flat.wgsl.ts +1489 -1569
  136. package/src/render/shaders/generated/globalMedium.wgsl.ts +98 -12
  137. package/src/render/shaders/generated/line.wgsl.ts +25 -35
  138. package/src/render/shaders/generated/particle.wgsl.ts +100 -140
  139. package/src/render/shaders/generated/rush.wgsl.ts +20 -10
  140. package/src/render/shaders/generated/scatter.wgsl.ts +25 -35
  141. package/src/render/shaders/generated/smoke.wgsl.ts +28 -38
  142. package/src/render/shaders/generated/water.wgsl.ts +29 -39
  143. package/src/render/shaders/globalMedium.ts +114 -10
  144. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +2 -1
  145. package/src/render/shaders/gpudriven/fog.wgsl.ts +78 -19
  146. package/src/render/shaders/gpudriven/frameBlock.wgsl.ts +12 -1
  147. package/src/render/shaders/gpudriven/shade.wgsl.ts +3 -1
  148. package/src/render/shaders/line.ts +4 -2
  149. package/src/render/shaders/localExposure.ts +115 -6
  150. package/src/render/shaders/particle.ts +10 -6
  151. package/src/render/shaders/scatter.ts +4 -2
  152. package/src/render/shaders/water.ts +2 -1
  153. package/src/render/surfaceModel.ts +41 -6
@@ -12,6 +12,44 @@ export interface UnderwaterAtmosphere {
12
12
  transitionDepth: number;
13
13
  }
14
14
 
15
+ /**
16
+ * A layer of haze: how thick it is where it is thickest, and how fast it thins above that.
17
+ *
18
+ * In natural units, as `fogDensity` is: a haze authored in base two, its surviving light
19
+ * `2^(−∫ρ)` and its density falling as `2^(−f·Δy)`, is the layer `density: ρ · (ln 2)²`,
20
+ * `heightFalloff: f · ln 2` here — the integral of base two's exponential carries one `ln 2` of its
21
+ * own, and the transmittance's base the other.
22
+ */
23
+ export interface FogLayer {
24
+ /** Extinction per metre at `baseY`. */
25
+ density: number;
26
+ /** Reciprocal scale height, per metre: 0 is a layer of even density. */
27
+ heightFalloff: number;
28
+ /** The height it is thickest at. */
29
+ baseY: number;
30
+ }
31
+
32
+ /**
33
+ * The sun's light scattered toward the eye by the haze: a glow in the sun's direction, as dust and
34
+ * droplets throw forward what they catch.
35
+ *
36
+ * Added to what the haze returns rather than mixed with it, by as much of the ray past `start` as
37
+ * the haze hides: a mountain range seen into a low sun burns gold at its distance, and the
38
+ * figures a few metres off do not. **What it gives up** is the light's own shape: the glow is a
39
+ * lobe about one direction, `cos^exponent` of the angle from it, not light scattered along the
40
+ * ray through the shadows the sun's map knows of — the global medium is that one.
41
+ */
42
+ export interface FogSunGlow {
43
+ /** The glow looking straight along `direction` through haze that hides everything. */
44
+ color: Vec3;
45
+ /** How tight the glow is about the sun: the cosine of the angle from it, to this power. */
46
+ exponent: number;
47
+ /** Metres past the haze's own start before the glow begins. Default 0. */
48
+ start?: number;
49
+ /** Surface → the light the glow surrounds. Absent, the environment's `directionalDir`. */
50
+ direction?: Vec3;
51
+ }
52
+
15
53
  /** Atmosphere fields shared by every visible render pass. */
16
54
  export interface Atmosphere {
17
55
  fogColor: Vec3;
@@ -60,6 +98,31 @@ export interface Atmosphere {
60
98
  * between the camera and the start, which a real medium has; that is the point of it.
61
99
  */
62
100
  fogStart?: number;
101
+ /**
102
+ * The medium only: a second layer of haze, summed with the first along every ray, each its own
103
+ * density, height and falloff. Absent, one layer.
104
+ *
105
+ * Two layers are what a valley with a ground mist under a high summer haze is: one thin and
106
+ * tall, one dense and hugging the floor. One layer must pick one of the two scale heights, and
107
+ * either gets the other's half of the picture wrong.
108
+ */
109
+ fogLayer2?: FogLayer | null;
110
+ /**
111
+ * The most of a surface the haze may hide, 0 to 1. Absent or 1, all of it.
112
+ *
113
+ * Below 1 a far mountain keeps that much of itself however much air lies between, which is how a
114
+ * scene keeps a skyline readable behind a haze that should hide everything nearer the ground.
115
+ * **What it gives up** is physics: no medium has a floor on what survives it.
116
+ */
117
+ fogMaxOpacity?: number;
118
+ /**
119
+ * Metres from the camera past which the haze hides nothing: a surface further away, the sky
120
+ * included, is drawn clear. Absent or 0, none. For a sky or a backdrop that brings its own
121
+ * haze painted in, which the medium would otherwise lay a second time.
122
+ */
123
+ fogCutoff?: number;
124
+ /** The medium only: the sun's light scattered toward the eye by the haze. Absent, none. */
125
+ fogSunGlow?: FogSunGlow | null;
63
126
  underwater: UnderwaterAtmosphere | null;
64
127
  }
65
128
 
@@ -89,6 +152,16 @@ export function fogDensityAtEye(atmosphere: Atmosphere, cameraY: number): number
89
152
  */
90
153
  export interface ResolvedAtmosphere {
91
154
  readonly fogColor: Float32Array;
155
+ /**
156
+ * Four floats: the second layer's extinction per metre at the camera's height and its falloff,
157
+ * then the least of a surface the haze must leave, `fogMaxOpacity`'s complement, and the cutoff
158
+ * distance. **Every lane is none at zero**, so a pass that never wrote them draws as before.
159
+ */
160
+ readonly fogLayer2: Float32Array;
161
+ /** Four floats: the sun glow's colour, black for none, and its exponent. */
162
+ readonly fogGlow: Float32Array;
163
+ /** Four floats: surface → the glow's light, and how far past the haze's start the glow begins. */
164
+ readonly fogGlowDir: Float32Array;
92
165
  fogDensity: number;
93
166
  fogHeightFalloff: number;
94
167
  fogEyeY: number;
@@ -109,7 +182,7 @@ export interface ResolvedAtmosphere {
109
182
  * it; nothing here allocates and nothing is shared between them.
110
183
  */
111
184
  export function resolveAtmosphere(
112
- atmosphere: Atmosphere,
185
+ atmosphere: Atmosphere & { readonly directionalDir?: ArrayLike<number> },
113
186
  cameraY: number,
114
187
  underwaterEnabled: boolean,
115
188
  out: ResolvedAtmosphere,
@@ -163,22 +236,77 @@ export function resolveAtmosphere(
163
236
  out.underwaterColor.set(underwater === null ? atmosphere.fogColor : underwater.color);
164
237
  out.underwaterFogDensity = underwater === null ? atmosphere.fogDensity : underwater.fogDensity;
165
238
  out.underwaterFactor = factor;
239
+ resolveHazeExtras(atmosphere, cameraY, out);
166
240
  return out;
167
241
  }
168
242
 
243
+ /**
244
+ * The second layer, the haze's limits and the sun's glow, into `out`: the layer and the glow only
245
+ * in the medium, since a linear ramp has neither a density nor a ray to integrate, and every slot
246
+ * written whatever is asked, so nothing a shader reads keeps what an earlier frame left in it.
247
+ */
248
+ function resolveHazeExtras(
249
+ atmosphere: Atmosphere & { readonly directionalDir?: ArrayLike<number> },
250
+ cameraY: number,
251
+ out: ResolvedAtmosphere,
252
+ ): void {
253
+ const medium = atmosphere.fogMode !== 'linear';
254
+ const layer = medium ? (atmosphere.fogLayer2 ?? null) : null;
255
+ const density = layer === null ? 0 : Math.max(0, layer.density);
256
+ /* The second layer at the camera's height, as `fogDensityAtEye` puts the first. */
257
+ out.fogLayer2[0] =
258
+ density > 0 && layer !== null
259
+ ? density * Math.exp(-(cameraY - layer.baseY) * layer.heightFalloff)
260
+ : 0;
261
+ out.fogLayer2[1] = density > 0 && layer !== null ? layer.heightFalloff : 0;
262
+ const most = atmosphere.fogMaxOpacity;
263
+ out.fogLayer2[2] =
264
+ most !== undefined && Number.isFinite(most) ? 1 - Math.min(1, Math.max(0, most)) : 0;
265
+ const cutoff = atmosphere.fogCutoff;
266
+ out.fogLayer2[3] = cutoff !== undefined && Number.isFinite(cutoff) && cutoff > 0 ? cutoff : 0;
267
+
268
+ const glow = medium ? (atmosphere.fogSunGlow ?? null) : null;
269
+ const toward = glow?.direction ?? atmosphere.directionalDir;
270
+ const x = (toward?.[0] as number | undefined) ?? 0;
271
+ const y = (toward?.[1] as number | undefined) ?? 1;
272
+ const z = (toward?.[2] as number | undefined) ?? 0;
273
+ const length = Math.hypot(x, y, z);
274
+ const lit = glow !== null && length > 1e-6;
275
+ for (let c = 0; c < 3; c++) {
276
+ out.fogGlow[c] = lit && glow !== null ? Math.max(0, glow.color[c] as number) : 0;
277
+ }
278
+ /* Above zero, so the shader's pow never meets 0 to the 0: a lobe that flat is the whole sky. */
279
+ out.fogGlow[3] = lit && glow !== null ? Math.max(1e-3, glow.exponent) : 1;
280
+ out.fogGlowDir[0] = lit ? x / length : 0;
281
+ out.fogGlowDir[1] = lit ? y / length : 1;
282
+ out.fogGlowDir[2] = lit ? z / length : 0;
283
+ out.fogGlowDir[3] = lit && glow !== null ? Math.max(0, glow.start ?? 0) : 0;
284
+ }
285
+
286
+ /**
287
+ * A target for `resolveAtmosphere`, made once outside the frame loop: one factory, so a field added
288
+ * to the resolved medium is a field every holder of one gets rather than one each must remember.
289
+ */
290
+ export function createResolvedAtmosphere(): ResolvedAtmosphere {
291
+ return {
292
+ fogColor: new Float32Array(3),
293
+ fogLayer2: new Float32Array(4),
294
+ fogGlow: new Float32Array(4),
295
+ fogGlowDir: new Float32Array(4),
296
+ underwaterColor: new Float32Array(3),
297
+ fogDensity: 0,
298
+ fogHeightFalloff: 0,
299
+ fogEyeY: 0,
300
+ fogMode: 0,
301
+ fogNear: 0,
302
+ fogFar: 1,
303
+ underwaterFogDensity: 0,
304
+ underwaterFactor: 0,
305
+ };
306
+ }
307
+
169
308
  /** One per GL binder, refilled per call. See `resolveAtmosphere` on why it is not allocated. */
170
- const glResolved: ResolvedAtmosphere = {
171
- fogColor: new Float32Array(3),
172
- underwaterColor: new Float32Array(3),
173
- fogDensity: 0,
174
- fogHeightFalloff: 0,
175
- fogEyeY: 0,
176
- fogMode: 0,
177
- fogNear: 0,
178
- fogFar: 1,
179
- underwaterFogDensity: 0,
180
- underwaterFactor: 0,
181
- };
309
+ const glResolved: ResolvedAtmosphere = createResolvedAtmosphere();
182
310
 
183
311
  /**
184
312
  * Bind the same camera medium to any shader that declares atmosphere uniforms.
@@ -195,13 +323,18 @@ export function bindAtmosphere(
195
323
  const m = resolveAtmosphere(atmosphere, cameraY, underwaterEnabled, glResolved);
196
324
 
197
325
  gl.uniform3fv(uniforms['uFogColor'] ?? null, m.fogColor);
198
- gl.uniform1f(uniforms['uFogDensity'] ?? null, m.fogDensity);
199
- gl.uniform1f(uniforms['uFogHeightFalloff'] ?? null, m.fogHeightFalloff);
200
- gl.uniform1f(uniforms['uFogEyeY'] ?? null, m.fogEyeY);
201
- gl.uniform1i(uniforms['uFogMode'] ?? null, m.fogMode);
202
- gl.uniform1f(uniforms['uFogNear'] ?? null, m.fogNear);
203
- gl.uniform1f(uniforms['uFogFar'] ?? null, m.fogFar);
326
+ /* Packed as `shaders/fog.ts` declares them, four to a row: see `uFogMedium` there for why. */
327
+ gl.uniform4f(
328
+ uniforms['uFogMedium'] ?? null,
329
+ m.fogDensity,
330
+ m.fogHeightFalloff,
331
+ m.fogEyeY,
332
+ m.underwaterFogDensity,
333
+ );
334
+ gl.uniform4f(uniforms['uFogRange'] ?? null, m.fogNear, m.fogFar, m.fogMode, 0);
204
335
  gl.uniform3fv(uniforms['uUnderwaterColor'] ?? null, m.underwaterColor);
205
- gl.uniform1f(uniforms['uUnderwaterFogDensity'] ?? null, m.underwaterFogDensity);
206
336
  gl.uniform1f(uniforms['uUnderwaterFactor'] ?? null, m.underwaterFactor);
337
+ gl.uniform4fv(uniforms['uFogLayer2'] ?? null, m.fogLayer2);
338
+ gl.uniform4fv(uniforms['uFogGlow'] ?? null, m.fogGlow);
339
+ gl.uniform4fv(uniforms['uFogGlowDir'] ?? null, m.fogGlowDir);
207
340
  }
@@ -42,14 +42,18 @@ import {
42
42
  resolveFrameReflections,
43
43
  type ResolvedFrameReflections,
44
44
  } from '../../frameReflections.ts';
45
+ import { noEnvironmentMixing, resolveEnvironmentMixing } from '../../environmentMixing.ts';
45
46
  import { GlobalMediumPass } from '../../globalMediumPass.ts';
46
47
  import {
47
48
  DEFAULT_GLOBAL_MEDIUM,
48
49
  WEAK_GPU_MEDIUM_STEPS,
50
+ createResolvedMediumLamps,
49
51
  mediumActive,
50
52
  resolveGlobalMedium,
53
+ resolveGlobalMediumSettings,
54
+ resolveMediumLamps,
51
55
  } from '../../globalMedium.ts';
52
- import type { GlobalMediumOptions } from '../../globalMedium.ts';
56
+ import type { GlobalMediumSettings, ResolvedGlobalMedium } from '../../globalMedium.ts';
53
57
  import { REFLECTION_EDGE_FADE, ReflectionQueue } from '../../screenSpaceReflection.ts';
54
58
  import type { ReflectiveSurface } from '../../screenSpaceReflection.ts';
55
59
  import { DecalQueue } from '../../decalQueue.ts';
@@ -86,6 +90,7 @@ import type {
86
90
  PassRegistry,
87
91
  } from '../../pass.ts';
88
92
  import type { Vec3 } from '../../../math/color.ts';
93
+ import type { LocalExposureOptions } from '../../shaders/localExposure.ts';
89
94
  import { clamp } from '../../../math/scalar.ts';
90
95
  import { attachContextLoss } from '../../contextLoss.ts';
91
96
  import { isWeakGpuFamily, webgl2RendererName } from '../../gpuCapability.ts';
@@ -177,6 +182,8 @@ import {
177
182
  MODEL_MAP_TEXTURE_UNIT,
178
183
  MORPH_DELTA_TEXTURE_UNIT,
179
184
  SKIN_PALETTE_TEXTURE_UNIT,
185
+ createResolvedPointLights,
186
+ resolvePointLights,
180
187
  } from '../../lightBudget.ts';
181
188
  import {
182
189
  MODEL_PARAM_FLOATS,
@@ -528,6 +535,12 @@ export interface Environment extends Atmosphere {
528
535
  * every light. See `PointLightSource.falloffExponent`.
529
536
  */
530
537
  lightFalloffExponents?: Float32Array;
538
+ /**
539
+ * One per active light: how strongly it scatters in the global medium, a share of how strongly it
540
+ * lights a surface, beside `setGlobalMedium`'s `lampScatter`. Optional: absent is 1 for every
541
+ * light. See `PointLightSet.lightMediumScatter`.
542
+ */
543
+ lightMediumScatter?: Float32Array;
531
544
  /** One per active light: a row of the photometric atlas, or −1 for none. */
532
545
  lightIesProfiles: Float32Array;
533
546
  /**
@@ -1551,6 +1564,8 @@ export class WebGL2Renderer implements RendererApi {
1551
1564
  * vector on the device, so written whole, and kept here so a setter moving one keeps the others.
1552
1565
  */
1553
1566
  private readonly environmentDials = new Float32Array(3);
1567
+ /** `uEnvironmentMixing` as it stands: see `setEnvironmentMixing`. None until a draw asks. */
1568
+ private readonly environmentMixing = noEnvironmentMixing(new Float32Array(4));
1554
1569
  /** `uEmission` as it stands: the emissive gain and the night factor. */
1555
1570
  private readonly emission = new Float32Array([1, 0]);
1556
1571
  /** The ramp and each level's tint, as `resolveBloomResponse` writes them; the subtraction and white. */
@@ -4880,6 +4895,31 @@ export class WebGL2Renderer implements RendererApi {
4880
4895
  this.writeEnvironmentDials();
4881
4896
  }
4882
4897
 
4898
+ /**
4899
+ * How much of the light where the surfaces drawn next stand their reflection is brought to, for
4900
+ * every surface, metal or not: what each reflects of the environment times
4901
+ * `lerp(1, weight, smoothstep(roughnessLow, roughnessHigh, roughness))`. 1 is the default and
4902
+ * identity; held until changed, and put back at `bindMeshPass`.
4903
+ *
4904
+ * **Why there is a weight at all**: a probe is captured at one place and seen from many, so a
4905
+ * figure standing where the indirect light is twice what the probe saw reflects the probe at the
4906
+ * probe's brightness and reads as dull against everything lit around it. `weight` is that
4907
+ * ratio, the place's indirect light over the probe's own average, and a caller that knows both
4908
+ * passes it; above 1 brightens, below dims. A mirror keeps the capture as it is, because its
4909
+ * image is the point, and a rough surface, whose reflection is an average anyway, takes its
4910
+ * place's light: the roughnesses say where between, 0.1 and 0.3 by default.
4911
+ *
4912
+ * Unlike `setEnvironmentGain`, which reaches metals only, and `setSurfaceReflectivity`, which
4913
+ * stops at 1. **What it gives up** is a reflection that stays the probe's image on a rough
4914
+ * surface: past the upper roughness it is the probe scaled to the light, not the place relit.
4915
+ */
4916
+ setEnvironmentMixing(weight: number, roughnessLow = 0.1, roughnessHigh = 0.3): void {
4917
+ if (this.contextLost) return;
4918
+ resolveEnvironmentMixing(weight, roughnessLow, roughnessHigh, this.environmentMixing);
4919
+ this.materials.dirty();
4920
+ this.writeFlatVector('uEnvironmentMixing', this.environmentMixing);
4921
+ }
4922
+
4883
4923
  /**
4884
4924
  * Kept as a no-op that says so once, because it was a stand-in for a term that now exists.
4885
4925
  *
@@ -8098,12 +8138,12 @@ export class WebGL2Renderer implements RendererApi {
8098
8138
  * two regions of one brightness band differ. Needs `screenEffects` and `hdrScene`, and says so
8099
8139
  * once rather than doing nothing. See `shaders/localExposure.ts`.
8100
8140
  */
8101
- setLocalExposure(strength: number): void {
8141
+ setLocalExposure(setting: number | LocalExposureOptions): void {
8102
8142
  if (this.sceneTarget === null || !this.quality.hdrScene) {
8103
- this.warnLookWithoutComposite(strength);
8143
+ this.warnLookWithoutComposite(typeof setting === 'number' ? setting : 1);
8104
8144
  return;
8105
8145
  }
8106
- this.sceneTarget.setLocalExposure(strength);
8146
+ this.sceneTarget.setLocalExposure(setting);
8107
8147
  }
8108
8148
 
8109
8149
  /**
@@ -8214,21 +8254,19 @@ export class WebGL2Renderer implements RendererApi {
8214
8254
  * of the two that can put the shape of a doorway on a floor.
8215
8255
  */
8216
8256
  setGlobalMedium(
8217
- density: number,
8218
- albedo?: number,
8257
+ density: number | GlobalMediumSettings,
8258
+ albedo?: number | Vec3,
8219
8259
  anisotropy?: number,
8220
8260
  maxDistance?: number,
8221
8261
  ): void {
8222
- this.mediumOptions = resolveGlobalMedium({
8223
- density: Number.isFinite(density) ? density : 0,
8224
- albedo,
8225
- anisotropy,
8226
- maxDistance,
8227
- });
8262
+ this.mediumOptions = resolveGlobalMediumSettings(density, albedo, anisotropy, maxDistance);
8228
8263
  }
8229
8264
 
8230
8265
  /** This frame's medium. Off until a caller says otherwise. See `setGlobalMedium`. */
8231
- private mediumOptions: GlobalMediumOptions = DEFAULT_GLOBAL_MEDIUM;
8266
+ private mediumOptions: ResolvedGlobalMedium = resolveGlobalMedium(DEFAULT_GLOBAL_MEDIUM);
8267
+ /** The lamps the medium is lit by, and the lit stage's lamps they come from, refilled a frame. */
8268
+ private readonly mediumLamps = createResolvedMediumLamps();
8269
+ private readonly mediumLights = createResolvedPointLights();
8232
8270
  /** Built on the first frame that asks for a medium, and never on one that does not. */
8233
8271
  private medium: GlobalMediumPass | null = null;
8234
8272
 
@@ -9081,6 +9119,15 @@ export class WebGL2Renderer implements RendererApi {
9081
9119
  so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
9082
9120
  sunShadows: this.sunShadows?.texture ?? this.emptyTexture2DArray,
9083
9121
  peeledEnabled: this.peelFilled,
9122
+ /* The lit stage's lamps and the shadows it resolved for them, this frame's. */
9123
+ lamps: resolveMediumLamps(
9124
+ this.mediumOptions,
9125
+ resolvePointLights(env, this.quality.pointLightFalloff, this.mediumLights),
9126
+ this.pointShadows === null ? null : this.resolvedPointShadows,
9127
+ env?.lightMediumScatter,
9128
+ this.mediumLamps,
9129
+ ),
9130
+ pointShadows: this.pointShadowArray?.texture ?? this.emptyTexture2DArray,
9084
9131
  },
9085
9132
  );
9086
9133
  if (marched) {
@@ -9356,6 +9403,8 @@ export class WebGL2Renderer implements RendererApi {
9356
9403
  this.materialEnvironmentGain = null;
9357
9404
  this.environmentDials[1] = 0;
9358
9405
  this.environmentDials[2] = 1;
9406
+ /* And no mixing, for the same reason. See `setEnvironmentMixing`. */
9407
+ noEnvironmentMixing(this.environmentMixing);
9359
9408
  this.emission[0] = env.emissiveGain;
9360
9409
  this.emission[1] = env.nightFactor;
9361
9410
  /* The moving layer's own matrix, the first time a frame draws that layer with one. */
@@ -9612,6 +9661,7 @@ export class WebGL2Renderer implements RendererApi {
9612
9661
  * See `setSurfaceReflectivity` and `setEnvironmentGain`.
9613
9662
  */
9614
9663
  gl.uniform3fv(u['uEnvironmentDials'] ?? null, this.environmentDials);
9664
+ gl.uniform4fv(u['uEnvironmentMixing'] ?? null, this.environmentMixing);
9615
9665
  /* And no relief unless a caller asks, for the same reason. */
9616
9666
  gl.uniform2f(u['uRelief'] ?? null, 0, 60);
9617
9667
  /* And no relief read off a surface texture either, for the same reason. */
@@ -34,6 +34,7 @@ const SECTIONS: readonly (readonly [number, number])[] = [
34
34
  [7, 3],
35
35
  [10, 3],
36
36
  [13, 16],
37
+ [29, 1],
37
38
  ];
38
39
  /** Where the influences start, and where the garment's index sits, in particle counts. */
39
40
  const INFLUENCES = 13;
@@ -20,7 +20,7 @@ import { batchesOf } from '@driftengine/physics';
20
20
  import type { ClothBatches, SkinnedClothSetup } from '@driftengine/physics';
21
21
 
22
22
  /** Floats of statics a particle, across its sections. */
23
- export const CLOTH_STATIC_FLOATS = 29;
23
+ export const CLOTH_STATIC_FLOATS = 30;
24
24
 
25
25
  /** Bit flags of the limits a set-up has: a max distance, a backstop, a frontstop. */
26
26
  export const CLOTH_LIMIT_MAX = 1;
@@ -29,7 +29,10 @@ export const CLOTH_LIMIT_FRONT = 4;
29
29
 
30
30
  export interface PackedCloth {
31
31
  readonly count: number;
32
- /** inverse mass, max distance, backstop ×2, frontstop ×2, thickness, rest ×3, normal ×3, influences ×16. */
32
+ /**
33
+ * inverse mass, max distance, backstop ×2, frontstop ×2, thickness, rest ×3, normal ×3,
34
+ * influences ×16, pull weight.
35
+ */
33
36
  readonly statics: Float32Array;
34
37
  /** Distance records (a, b, rest, compliance), bending (four ids, rest, compliance), tethers (particle, anchor, length). */
35
38
  readonly constraints: Uint32Array;
@@ -77,6 +80,7 @@ export function packCloth(setup: SkinnedClothSetup): PackedCloth {
77
80
  statics[at + i * 2 + 1] = real ? radius : 0;
78
81
  }
79
82
  statics[6 * n + i] = limits.thickness?.[i] ?? 0;
83
+ statics[29 * n + i] = setup.pullWeights?.[i] ?? 1;
80
84
  for (let k = 0; k < 3; k++) {
81
85
  statics[7 * n + i * 3 + k] = setup.positions[i * 3 + k] as number;
82
86
  statics[10 * n + i * 3 + k] = setup.normals?.[i * 3 + k] ?? (k === 2 ? 1 : 0);
@@ -338,7 +338,7 @@ export class GpuClothSet {
338
338
  u[base + 32] = garment.range.colliderBase;
339
339
  u[base + 33] = garment.range.colliders;
340
340
  u[base + 34] = garment.particles.width;
341
- f[base + 35] = 1 / (1 + p.pull * h * h);
341
+ f[base + 35] = p.pull * h * h;
342
342
  f[base + 36] = exactExp(p.damping * h);
343
343
  f[base + 37] = exactExp(p.drag * h);
344
344
  f[base + 38] = p.maxDistanceScale;
@@ -182,11 +182,11 @@ export const VERTEX_FLOATS = GPU_DRIVEN_VERTEX_FLOATS;
182
182
  /**
183
183
  * Floats the shading pass's frame block holds. Matches `FRAME_BLOCK_WGSL`'s `Frame`.
184
184
  *
185
- * Two matrices and fifteen vectors: the camera's, the light's, then the eye, the light's direction
185
+ * Two matrices and eighteen vectors: the camera's, the light's, then the eye, the light's direction
186
186
  * and colour, the sky, the ground, the target's size, the shadow's settings in two, the probe's in
187
- * two, the medium's four, and the visibility buffer's row.
187
+ * two, the medium's four, the visibility buffer's row, and the medium's last three after it.
188
188
  */
189
- const FRAME_FLOATS = 92;
189
+ const FRAME_FLOATS = 104;
190
190
 
191
191
  /** Where each block starts in it, so the writer and the shader cannot drift apart. */
192
192
  const FRAME_LIGHT_VIEW_PROJ = 16;
@@ -205,6 +205,9 @@ const FRAME_FOG_UNDERWATER = 76;
205
205
  const FRAME_FOG_HEIGHT = 80;
206
206
  const FRAME_FOG_RANGE = 84;
207
207
  const FRAME_PITCH = 88;
208
+ const FRAME_FOG_LIMITS = 92;
209
+ const FRAME_FOG_GLOW = 96;
210
+ const FRAME_FOG_GLOW_DIR = 100;
208
211
 
209
212
  /**
210
213
  * Pixels the internal width is rounded up to.
@@ -2693,6 +2696,20 @@ export class GpuDrivenPass implements PassDefinition {
2693
2696
  params[FRAME_FOG_RANGE + 1] = fog?.far ?? 0;
2694
2697
  params[FRAME_FOG_RANGE + 2] = fog?.mode ?? MEDIUM_FOG;
2695
2698
  params[FRAME_FOG_RANGE + 3] = fog?.underwaterDensity ?? 0;
2699
+ /* The second layer, the floor, the cutoff and the glow: every one none at zero, as a view
2700
+ written before they existed asks. */
2701
+ params[FRAME_FOG_HEIGHT + 2] = fog?.density2 ?? 0;
2702
+ params[FRAME_FOG_HEIGHT + 3] = fog?.heightFalloff2 ?? 0;
2703
+ params[FRAME_FOG_LIMITS] = fog?.floor ?? 0;
2704
+ params[FRAME_FOG_LIMITS + 1] = fog?.cutoff ?? 0;
2705
+ params[FRAME_FOG_GLOW] = fog?.glowColour?.[0] ?? 0;
2706
+ params[FRAME_FOG_GLOW + 1] = fog?.glowColour?.[1] ?? 0;
2707
+ params[FRAME_FOG_GLOW + 2] = fog?.glowColour?.[2] ?? 0;
2708
+ params[FRAME_FOG_GLOW + 3] = fog?.glowExponent ?? 1;
2709
+ params[FRAME_FOG_GLOW_DIR] = fog?.glowDirection?.[0] ?? 0;
2710
+ params[FRAME_FOG_GLOW_DIR + 1] = fog?.glowDirection?.[1] ?? 1;
2711
+ params[FRAME_FOG_GLOW_DIR + 2] = fog?.glowDirection?.[2] ?? 0;
2712
+ params[FRAME_FOG_GLOW_DIR + 3] = fog?.glowStart ?? 0;
2696
2713
  /* The scene's own decision about whether its grid supplies the ambient, as it is on the
2697
2714
  forward path: `ProbeBakeOptions.irradiance` reaches here as `PassEnvironment.irradiance`. */
2698
2715
  params[FRAME_ENVIRONMENT_MIX] = room !== null && room.irradiance ? 1 : 0;
@@ -0,0 +1,87 @@
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
+
15
+ /** A field's byte offset in the block, or -1 where the shader compiled it out. */
16
+ function placed(fields: UniformFields, name: string): number {
17
+ return fields[name]?.offset ?? -1;
18
+ }
19
+
20
+ /** Into a block staged as floats, indexed in four-byte words. */
21
+ export function writeMediumFields(
22
+ fields: UniformFields,
23
+ f: Float32Array,
24
+ m: ResolvedAtmosphere,
25
+ ): void {
26
+ let at = placed(fields, 'uFogColor');
27
+ if (at >= 0) f.set(m.fogColor, at / 4);
28
+ at = placed(fields, 'uFogMedium');
29
+ if (at >= 0) {
30
+ f[at / 4] = m.fogDensity;
31
+ f[at / 4 + 1] = m.fogHeightFalloff;
32
+ f[at / 4 + 2] = m.fogEyeY;
33
+ f[at / 4 + 3] = m.underwaterFogDensity;
34
+ }
35
+ at = placed(fields, 'uFogRange');
36
+ if (at >= 0) {
37
+ f[at / 4] = m.fogNear;
38
+ f[at / 4 + 1] = m.fogFar;
39
+ f[at / 4 + 2] = m.fogMode;
40
+ f[at / 4 + 3] = 0;
41
+ }
42
+ at = placed(fields, 'uUnderwaterColor');
43
+ if (at >= 0) f.set(m.underwaterColor, at / 4);
44
+ at = placed(fields, 'uUnderwaterFactor');
45
+ if (at >= 0) f[at / 4] = m.underwaterFactor;
46
+ at = placed(fields, 'uFogLayer2');
47
+ if (at >= 0) f.set(m.fogLayer2, at / 4);
48
+ at = placed(fields, 'uFogGlow');
49
+ if (at >= 0) f.set(m.fogGlow, at / 4);
50
+ at = placed(fields, 'uFogGlowDir');
51
+ if (at >= 0) f.set(m.fogGlowDir, at / 4);
52
+ }
53
+
54
+ /** Into one slot of a ring, at the fields' byte offsets. */
55
+ export function writeMediumSlot(
56
+ ring: UniformRing,
57
+ slot: number,
58
+ fields: UniformFields,
59
+ m: ResolvedAtmosphere,
60
+ ): void {
61
+ let at = placed(fields, 'uFogColor');
62
+ if (at >= 0) ring.writeFloats(slot, at, m.fogColor);
63
+ at = placed(fields, 'uFogMedium');
64
+ if (at >= 0) {
65
+ ring.writeFloat(slot, at, m.fogDensity);
66
+ ring.writeFloat(slot, at + 4, m.fogHeightFalloff);
67
+ ring.writeFloat(slot, at + 8, m.fogEyeY);
68
+ ring.writeFloat(slot, at + 12, m.underwaterFogDensity);
69
+ }
70
+ at = placed(fields, 'uFogRange');
71
+ if (at >= 0) {
72
+ ring.writeFloat(slot, at, m.fogNear);
73
+ ring.writeFloat(slot, at + 4, m.fogFar);
74
+ ring.writeFloat(slot, at + 8, m.fogMode);
75
+ ring.writeFloat(slot, at + 12, 0);
76
+ }
77
+ at = placed(fields, 'uUnderwaterColor');
78
+ if (at >= 0) ring.writeFloats(slot, at, m.underwaterColor);
79
+ at = placed(fields, 'uUnderwaterFactor');
80
+ if (at >= 0) ring.writeFloat(slot, at, m.underwaterFactor);
81
+ at = placed(fields, 'uFogLayer2');
82
+ if (at >= 0) ring.writeFloats(slot, at, m.fogLayer2);
83
+ at = placed(fields, 'uFogGlow');
84
+ if (at >= 0) ring.writeFloats(slot, at, m.fogGlow);
85
+ at = placed(fields, 'uFogGlowDir');
86
+ if (at >= 0) ring.writeFloats(slot, at, m.fogGlowDir);
87
+ }