@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
@@ -42,6 +42,25 @@ export interface FogOptions {
42
42
  */
43
43
  readonly near: number;
44
44
  readonly far: number;
45
+ /** The medium's second layer: its extinction per metre at the camera's height. Absent, none. */
46
+ readonly density2?: number;
47
+ /** And its reciprocal scale height. */
48
+ readonly heightFalloff2?: number;
49
+ /**
50
+ * The least of a surface the haze must leave, `Atmosphere.fogMaxOpacity`'s complement. Absent or 0,
51
+ * no floor.
52
+ */
53
+ readonly floor?: number;
54
+ /** Metres past which the haze hides nothing. Absent or 0, none. */
55
+ readonly cutoff?: number;
56
+ /** The sun's glow in the haze: its colour. Absent or black, none. */
57
+ readonly glowColour?: Rgb;
58
+ /** The glow's lobe: the cosine of the angle from its light, to this power. */
59
+ readonly glowExponent?: number;
60
+ /** Surface → the glow's light, a unit vector. */
61
+ readonly glowDirection?: Rgb;
62
+ /** Metres past the haze's start before the glow begins. */
63
+ readonly glowStart?: number;
45
64
  }
46
65
  /** What distance converges toward: the day's haze, or the water column. */
47
66
  export declare function mediumColour(fog: FogOptions): [number, number, number];
@@ -50,6 +69,19 @@ export declare function mediumColour(fog: FogOptions): [number, number, number];
50
69
  * 0 is perfectly clear, 1 is the medium's own colour.
51
70
  */
52
71
  export declare function mediumFog(fog: FogOptions, dist: number, pointY: number): number;
72
+ /**
73
+ * The haze's optical depth from where it begins along the ray to a point at `pointY`, `dist` metres
74
+ * away: each layer's density at the eye carried over what is left of the ray, the second only where
75
+ * there is one. `mediumFog` and `mediumGlow` both integrate it.
76
+ */
77
+ export declare function hazeDepth(fog: FogOptions, dist: number, pointY: number): number;
78
+ /**
79
+ * The sun's light the haze scatters toward the eye along a ray to a point at `pointY`, `dist`
80
+ * metres away in the unit direction `toPoint`, into `out`: added after the fog, never mixed. A lobe
81
+ * of `cos^exponent` about the light, by as much of the ray past the glow's start as the haze hides.
82
+ * None in a linear ramp, past the cutoff, or under water.
83
+ */
84
+ export declare function mediumGlow(fog: FogOptions, dist: number, pointY: number, toPoint: ArrayLike<number>, out: [number, number, number]): [number, number, number];
53
85
  /** `FogOptions` with its fields writable, which is what a caller holds and refills a frame at a time. */
54
86
  export interface FogTarget {
55
87
  colour: [number, number, number];
@@ -62,6 +94,14 @@ export interface FogTarget {
62
94
  mode: number;
63
95
  near: number;
64
96
  far: number;
97
+ density2: number;
98
+ heightFalloff2: number;
99
+ floor: number;
100
+ cutoff: number;
101
+ glowColour: [number, number, number];
102
+ glowExponent: number;
103
+ glowDirection: [number, number, number];
104
+ glowStart: number;
65
105
  }
66
106
  /** A target for `atmosphereFog`, allocated once, outside the frame loop. */
67
107
  export declare function createFogTarget(): FogTarget;
@@ -79,4 +119,6 @@ export declare function createFogTarget(): FogTarget;
79
119
  * @param underwater The renderer's `underwaterAtmosphere` quality, which gates the second medium
80
120
  * on the forward path; pass the same.
81
121
  */
82
- export declare function atmosphereFog(atmosphere: Atmosphere, eyeY: number, underwater: boolean, out: FogTarget): FogTarget;
122
+ export declare function atmosphereFog(atmosphere: Atmosphere & {
123
+ readonly directionalDir?: ArrayLike<number>;
124
+ }, eyeY: number, underwater: boolean, out: FogTarget): FogTarget;
@@ -16,7 +16,7 @@
16
16
  * block that declares everything it uses can be pasted into any program; a function takes them as
17
17
  * one argument instead, and the backends keep their own packing.
18
18
  */
19
- import { resolveAtmosphere } from './atmosphere.js';
19
+ import { createResolvedAtmosphere, resolveAtmosphere, } from './atmosphere.js';
20
20
  /** Beer-Lambert through a haze layer that thins with height. `uFogMode` 0. */
21
21
  export const MEDIUM_FOG = 0;
22
22
  /** A ramp between two distances, which is a look rather than a medium. `uFogMode` 1. */
@@ -53,15 +53,37 @@ export function mediumFog(fog, dist, pointY) {
53
53
  * is perfectly clear, which Beer-Lambert can never be, and height falloff has no meaning here
54
54
  * because a ramp is defined on distance alone. `shaders/fog.ts` argues the whole of it.
55
55
  */
56
+ /* Past the cutoff the air hides nothing, in either mode; the water column keeps its own law. */
57
+ const cutoff = fog.cutoff ?? 0;
58
+ const cut = cutoff > 0 && dist > cutoff;
59
+ const floor = fog.floor ?? 0;
56
60
  if (fog.mode === LINEAR_FOG) {
57
61
  /* A consumer asking for a hard cut writes `far === near`; the guard is why that is a cut. */
58
62
  const span = Math.max(fog.far - fog.near, 1e-4);
59
- const ramp = Math.min(1, Math.max(0, (dist - fog.near) / span));
63
+ const ramp = cut ? 0 : Math.min(Math.min(1, Math.max(0, (dist - fog.near) / span)), 1 - floor);
60
64
  if (fog.underwaterFactor <= 0)
61
65
  return ramp;
62
66
  const wetLinear = fog.underwaterDensity * dist;
63
67
  return mix(ramp, 1 - Math.pow(2, -wetLinear * wetLinear * 1.442695), fog.underwaterFactor);
64
68
  }
69
+ /*
70
+ * Beer-Lambert through both layers, leaving at least `floor` of the surface: a floor on what
71
+ * survives rather than a ceiling on the depth, so the curve keeps its shape above it.
72
+ */
73
+ const air = cut ? 0 : 1 - Math.max(Math.exp(-hazeDepth(fog, dist, pointY)), floor);
74
+ if (fog.underwaterFactor <= 0)
75
+ return air;
76
+ /* Water is its own medium and keeps its own curve, so crossing the surface changes nothing
77
+ about how the water column reads. */
78
+ const wet = fog.underwaterDensity * dist;
79
+ return mix(air, 1 - Math.pow(2, -wet * wet * 1.442695), fog.underwaterFactor);
80
+ }
81
+ /**
82
+ * The haze's optical depth from where it begins along the ray to a point at `pointY`, `dist` metres
83
+ * away: each layer's density at the eye carried over what is left of the ray, the second only where
84
+ * there is one. `mediumFog` and `mediumGlow` both integrate it.
85
+ */
86
+ export function hazeDepth(fog, dist, pointY) {
65
87
  /*
66
88
  * **Where the medium begins along the ray**, `near` — the linear mode's slot, which this mode
67
89
  * never read before `Atmosphere.fogStart`. The ray is integrated from that point on: the density
@@ -77,14 +99,50 @@ export function mediumFog(fog, dist, pointY) {
77
99
  * `t = 0` the quotient is 0/0, and 1e-4 of a scale height is 8 mm.
78
100
  */
79
101
  const denom = Math.abs(rest) < 1e-4 ? 1e-4 : rest;
80
- const air = 1 -
81
- Math.exp((-fog.density * Math.exp(-t * before) * (dist - start) * (1 - Math.exp(-denom))) / denom);
82
- if (fog.underwaterFactor <= 0)
83
- return air;
84
- /* Water is its own medium and keeps its own curve, so crossing the surface changes nothing
85
- about how the water column reads. */
86
- const wet = fog.underwaterDensity * dist;
87
- return mix(air, 1 - Math.pow(2, -wet * wet * 1.442695), fog.underwaterFactor);
102
+ let depth = (fog.density * Math.exp(-t * before) * (dist - start) * (1 - Math.exp(-denom))) / denom;
103
+ const density2 = fog.density2 ?? 0;
104
+ if (density2 > 0) {
105
+ const t2 = (pointY - fog.eyeY) * (fog.heightFalloff2 ?? 0);
106
+ const rest2 = t2 * (1 - before);
107
+ const denom2 = Math.abs(rest2) < 1e-4 ? 1e-4 : rest2;
108
+ depth +=
109
+ (density2 * Math.exp(-t2 * before) * (dist - start) * (1 - Math.exp(-denom2))) / denom2;
110
+ }
111
+ return depth;
112
+ }
113
+ /**
114
+ * The sun's light the haze scatters toward the eye along a ray to a point at `pointY`, `dist`
115
+ * metres away in the unit direction `toPoint`, into `out`: added after the fog, never mixed. A lobe
116
+ * of `cos^exponent` about the light, by as much of the ray past the glow's start as the haze hides.
117
+ * None in a linear ramp, past the cutoff, or under water.
118
+ */
119
+ export function mediumGlow(fog, dist, pointY, toPoint, out) {
120
+ out[0] = 0;
121
+ out[1] = 0;
122
+ out[2] = 0;
123
+ const colour = fog.glowColour;
124
+ if (fog.mode === LINEAR_FOG || colour === undefined)
125
+ return out;
126
+ if (Math.max(colour[0], colour[1], colour[2]) <= 0)
127
+ return out;
128
+ const cutoff = fog.cutoff ?? 0;
129
+ if (cutoff > 0 && dist > cutoff)
130
+ return out;
131
+ const span = dist - Math.min(fog.near, dist);
132
+ if (span <= 0)
133
+ return out;
134
+ const perMetre = hazeDepth(fog, dist, pointY) / span;
135
+ const hidden = 1 - Math.exp(-perMetre * Math.max(span - (fog.glowStart ?? 0), 0));
136
+ const toward = fog.glowDirection ?? [0, 1, 0];
137
+ const cosine = toPoint[0] * toward[0] +
138
+ toPoint[1] * toward[1] +
139
+ toPoint[2] * toward[2];
140
+ const lobe = Math.pow(Math.min(1, Math.max(0, cosine)), fog.glowExponent ?? 1);
141
+ const share = lobe * hidden * (1 - fog.underwaterFactor);
142
+ out[0] = colour[0] * share;
143
+ out[1] = colour[1] * share;
144
+ out[2] = colour[2] * share;
145
+ return out;
88
146
  }
89
147
  /** A target for `atmosphereFog`, allocated once, outside the frame loop. */
90
148
  export function createFogTarget() {
@@ -99,21 +157,18 @@ export function createFogTarget() {
99
157
  mode: MEDIUM_FOG,
100
158
  near: 0,
101
159
  far: 1,
160
+ density2: 0,
161
+ heightFalloff2: 0,
162
+ floor: 0,
163
+ cutoff: 0,
164
+ glowColour: [0, 0, 0],
165
+ glowExponent: 1,
166
+ glowDirection: [0, 1, 0],
167
+ glowStart: 0,
102
168
  };
103
169
  }
104
170
  /** `resolveAtmosphere`'s own target, reused, because this runs a frame at a time. */
105
- const RESOLVED = {
106
- fogColor: new Float32Array(3),
107
- underwaterColor: new Float32Array(3),
108
- fogDensity: 0,
109
- fogHeightFalloff: 0,
110
- fogEyeY: 0,
111
- fogMode: 0,
112
- fogNear: 0,
113
- fogFar: 1,
114
- underwaterFogDensity: 0,
115
- underwaterFactor: 0,
116
- };
171
+ const RESOLVED = createResolvedAtmosphere();
117
172
  /**
118
173
  * The haze an `Environment` makes at a camera's height, as `GpuDrivenView.fog` takes it.
119
174
  *
@@ -144,5 +199,15 @@ export function atmosphereFog(atmosphere, eyeY, underwater, out) {
144
199
  out.mode = medium.fogMode;
145
200
  out.near = medium.fogNear;
146
201
  out.far = medium.fogFar;
202
+ out.density2 = medium.fogLayer2[0];
203
+ out.heightFalloff2 = medium.fogLayer2[1];
204
+ out.floor = medium.fogLayer2[2];
205
+ out.cutoff = medium.fogLayer2[3];
206
+ for (let c = 0; c < 3; c++) {
207
+ out.glowColour[c] = medium.fogGlow[c];
208
+ out.glowDirection[c] = medium.fogGlowDir[c];
209
+ }
210
+ out.glowExponent = medium.fogGlow[3];
211
+ out.glowStart = medium.fogGlowDir[3];
147
212
  return out;
148
213
  }
@@ -11,6 +11,10 @@
11
11
  * and this asks what the journey adds — which is every point along it the sun can see, and is the
12
12
  * only one of the two that can put the shape of a doorway on a floor.
13
13
  */
14
+ import type { Vec3 } from '../math/color.ts';
15
+ import type { FogLayer } from './atmosphere.ts';
16
+ import type { ResolvedPointLights } from './lightBudget.ts';
17
+ import type { ResolvedPointShadows } from './pointShadowImage.ts';
14
18
  /**
15
19
  * The most steps a march may take, which is the shader's own loop bound.
16
20
  *
@@ -38,6 +42,11 @@ export declare const MAX_GLOBAL_MEDIUM_STEPS = 64;
38
42
  * entitled to it and to the frame rate that comes with it.
39
43
  */
40
44
  export declare const WEAK_GPU_MEDIUM_STEPS = 16;
45
+ /**
46
+ * The most lamps the march lights the medium with, which is the lit stage's own per-pixel set:
47
+ * the lamps a frame shades with uniforms, each with the shadow it already has.
48
+ */
49
+ export declare const MAX_MEDIUM_LAMPS = 16;
41
50
  /**
42
51
  * How thick the air is, this frame.
43
52
  *
@@ -55,13 +64,15 @@ export interface GlobalMediumOptions {
55
64
  */
56
65
  readonly density: number;
57
66
  /**
58
- * How much of what the medium takes out of a ray comes back as light rather than as heat, 0 to 1.
67
+ * How much of what the medium takes out of a ray comes back as light rather than as heat, 0 to 1,
68
+ * or a colour, a share a channel.
59
69
  *
60
70
  * Near 1 for water droplets, which is fog, cloud and steam; lower for smoke, which is soot and
61
71
  * absorbs most of what it intercepts. It is the difference between a white fog that a torch
62
- * lights up and a black smoke that swallows the same torch.
72
+ * lights up and a black smoke that swallows the same torch. A colour is a medium that keeps more
73
+ * of one light than another: a faintly blue haze returns the sky's blue a little more readily.
63
74
  */
64
- readonly albedo: number;
75
+ readonly albedo: number | Vec3;
65
76
  /**
66
77
  * Henyey-Greenstein anisotropy, -1 back to 1 forward. 0 is isotropic.
67
78
  *
@@ -72,14 +83,58 @@ export interface GlobalMediumOptions {
72
83
  */
73
84
  readonly anisotropy: number;
74
85
  /**
75
- * Where the march stops, in metres. Beyond it the medium is not sampled and nothing is added.
86
+ * Where the medium ends, in metres: past it the air neither scatters nor absorbs.
76
87
  *
77
- * A bound on the *integral*, not on the fog: extinction is still integrated over the whole
78
- * distance a ray travels, so a far wall behind this bound is still obscured. What stops is the
79
- * search for light to scatter, which is what costs — and past a few extinction lengths there is
80
- * no transmittance left for a sample to contribute through.
88
+ * **This said extinction went on past it, and the march never did that**: it integrates from
89
+ * the eye to here or to the surface, whichever is nearer, and nothing after. Which is what a
90
+ * medium standing for the near part of the environment's haze wants: the haze takes the rest of
91
+ * the ray when its `fogStart` is this distance, and the two never lay one stretch of air twice.
92
+ * **What it gives up** is a medium of its own beyond the reach, which the haze has to be.
93
+ */
94
+ readonly maxDistance: number;
95
+ /**
96
+ * Reciprocal scale height, per metre: `density` is the extinction at `baseY`, thinning above it
97
+ * as the haze does (`Atmosphere.fogHeightFalloff`). Absent or 0, the medium is as thick
98
+ * everywhere, as it always was.
81
99
  */
100
+ readonly heightFalloff?: number;
101
+ /** The height `density` is measured at. Default 0. */
102
+ readonly baseY?: number;
103
+ /**
104
+ * A second layer, summed with the first, its own density at its own height thinning by its own
105
+ * falloff: `Atmosphere.fogLayer2`'s shape, so a medium standing for the near part of a two-layer
106
+ * haze can be that haze.
107
+ */
108
+ readonly layer2?: FogLayer | null;
109
+ /** How strongly the sun scatters in it, as a share of how strongly it lights a surface. Default 1. */
110
+ readonly sunScatter?: number;
111
+ /** The same for the sky's fill, which lights a shadowed medium from every side. Default 1. */
112
+ readonly skyScatter?: number;
113
+ /**
114
+ * The same for the lamps, each through its own shadow and times its own `lightMediumScatter`.
115
+ * **Default 0**: a medium was the sun's and the sky's until there was this, and a scene that
116
+ * never asks keeps that picture and that cost. The lamps are the lit stage's per-pixel set,
117
+ * shaped by their cones; a photometric profile and a cookie shape the pool on a surface and not
118
+ * the shaft in the air.
119
+ */
120
+ readonly lampScatter?: number;
121
+ }
122
+ /** A medium with every field filled in and clamped: what both backends' passes are handed. */
123
+ export interface ResolvedGlobalMedium {
124
+ readonly density: number;
125
+ /** A colour, three shares. */
126
+ readonly albedo: readonly [number, number, number];
127
+ readonly anisotropy: number;
82
128
  readonly maxDistance: number;
129
+ readonly heightFalloff: number;
130
+ readonly baseY: number;
131
+ /** The second layer's density at its own height, 0 for none, its falloff and that height. */
132
+ readonly density2: number;
133
+ readonly heightFalloff2: number;
134
+ readonly baseY2: number;
135
+ readonly sunScatter: number;
136
+ readonly skyScatter: number;
137
+ readonly lampScatter: number;
83
138
  }
84
139
  /**
85
140
  * What a renderer starts with: off, and everything else at a value that means "ordinary air".
@@ -97,7 +152,16 @@ export declare const DEFAULT_GLOBAL_MEDIUM: GlobalMediumOptions;
97
152
  * function by zero, which is a NaN that a half-float target spreads over the whole screen rather
98
153
  * than reporting; a `maxDistance` of zero is a march with no length that still costs its steps.
99
154
  */
100
- export declare function resolveGlobalMedium(options: Partial<GlobalMediumOptions>): GlobalMediumOptions;
155
+ export declare function resolveGlobalMedium(options: Partial<GlobalMediumOptions>): ResolvedGlobalMedium;
156
+ /** `setGlobalMedium`'s object form: a density, and any of the rest. */
157
+ export type GlobalMediumSettings = Partial<GlobalMediumOptions> & {
158
+ readonly density: number;
159
+ };
160
+ /**
161
+ * `setGlobalMedium`'s two forms, its four numbers or the object, read one way for both backends:
162
+ * a density that is not a number is none.
163
+ */
164
+ export declare function resolveGlobalMediumSettings(density: number | GlobalMediumSettings, albedo?: number | Vec3, anisotropy?: number, maxDistance?: number): ResolvedGlobalMedium;
101
165
  /**
102
166
  * Whether this frame draws the medium at all.
103
167
  *
@@ -105,7 +169,7 @@ export declare function resolveGlobalMedium(options: Partial<GlobalMediumOptions
105
169
  * `density: 0` is pixel-identical to the pass being absent is only true if neither backend runs a
106
170
  * composite at zero, and a second copy of this test is where that stops being true.
107
171
  */
108
- export declare function mediumActive(options: GlobalMediumOptions, steps: number): boolean;
172
+ export declare function mediumActive(options: Pick<GlobalMediumOptions, 'density'>, steps: number): boolean;
109
173
  /**
110
174
  * The march's target size.
111
175
  *
@@ -122,3 +186,41 @@ export declare function mediumTargetSize(width: number, height: number, halfReso
122
186
  width: number;
123
187
  height: number;
124
188
  };
189
+ /**
190
+ * The lamps as the march reads them: eight vectors a lamp, each array `MAX_MEDIUM_LAMPS` of them.
191
+ *
192
+ * **Decided here and bound twice**, on the split `resolvePointLights` draws: which lamps light the
193
+ * medium, how bright and through which maps is arithmetic both backends run, and a backend only
194
+ * uploads the arrays. A lamp's shadows are the lit stage's own, the static map and the live one,
195
+ * read where the frame left them, so the shaft in the air and the pool on the floor are cut by the
196
+ * same casters.
197
+ */
198
+ export interface ResolvedMediumLamps {
199
+ /** How many lamps light the medium: none while `lampScatter` is 0. */
200
+ count: number;
201
+ /** 1 for inverse-square, 0 for the smooth falloff: the frame's own rule. */
202
+ falloff: number;
203
+ /** Where each lamp is, and how far it reaches. */
204
+ readonly position: Float32Array;
205
+ /** Its colour, times its presence and its scatter, and its own falloff exponent (0 for none). */
206
+ readonly color: Float32Array;
207
+ /** The direction it shines, a spot's axis: `lightDirections`. */
208
+ readonly aim: Float32Array;
209
+ /** Its cone's two cosines, inner and outer, as the lit stage takes them. */
210
+ readonly cone: Float32Array;
211
+ /** Its static shadow: the layer (−1 for none), the near plane, and how present the map is. */
212
+ readonly shadow: Float32Array;
213
+ /** The static map's projection: its origin and its far plane. */
214
+ readonly shadowAt: Float32Array;
215
+ /** Its live shadow, by the same layout, and the live map's projection. */
216
+ readonly live: Float32Array;
217
+ readonly liveAt: Float32Array;
218
+ }
219
+ /** A target for `resolveMediumLamps`, made once outside the frame loop. */
220
+ export declare function createResolvedMediumLamps(): ResolvedMediumLamps;
221
+ /**
222
+ * The frame's lamps as the medium is lit by them, into `out`: the lit stage's resolved lamps and
223
+ * their resolved shadows, a lamp's scatter `lampScatter` times its own `lightMediumScatter` (1 where
224
+ * absent). Allocates nothing; a lamp past the count keeps whatever it held, which nothing reads.
225
+ */
226
+ export declare function resolveMediumLamps(medium: ResolvedGlobalMedium, lights: ResolvedPointLights, shadows: ResolvedPointShadows | null, scatter: Float32Array | undefined, out: ResolvedMediumLamps): ResolvedMediumLamps;
@@ -11,6 +11,7 @@
11
11
  * and this asks what the journey adds — which is every point along it the sun can see, and is the
12
12
  * only one of the two that can put the shape of a doorway on a floor.
13
13
  */
14
+ import { MAX_POINT_LIGHTS } from './lightBudget.js';
14
15
  /**
15
16
  * The most steps a march may take, which is the shader's own loop bound.
16
17
  *
@@ -38,6 +39,11 @@ export const MAX_GLOBAL_MEDIUM_STEPS = 64;
38
39
  * entitled to it and to the frame rate that comes with it.
39
40
  */
40
41
  export const WEAK_GPU_MEDIUM_STEPS = 16;
42
+ /**
43
+ * The most lamps the march lights the medium with, which is the lit stage's own per-pixel set:
44
+ * the lamps a frame shades with uniforms, each with the shadow it already has.
45
+ */
46
+ export const MAX_MEDIUM_LAMPS = MAX_POINT_LIGHTS;
41
47
  /**
42
48
  * What a renderer starts with: off, and everything else at a value that means "ordinary air".
43
49
  *
@@ -61,14 +67,47 @@ export const DEFAULT_GLOBAL_MEDIUM = {
61
67
  */
62
68
  export function resolveGlobalMedium(options) {
63
69
  const d = DEFAULT_GLOBAL_MEDIUM;
70
+ const albedo = options.albedo ?? d.albedo;
71
+ const share = (value) => Math.min(1, Math.max(0, Number.isFinite(value) ? value : 0));
72
+ const layer2 = options.layer2 ?? null;
73
+ const density2 = layer2 === null ? 0 : Math.max(0, layer2.density);
74
+ const finite = (value, fallback) => value !== undefined && Number.isFinite(value) ? value : fallback;
64
75
  return {
65
76
  density: Math.max(0, options.density ?? d.density),
66
- albedo: Math.min(1, Math.max(0, options.albedo ?? d.albedo)),
77
+ albedo: typeof albedo === 'number'
78
+ ? [share(albedo), share(albedo), share(albedo)]
79
+ : [share(albedo[0]), share(albedo[1]), share(albedo[2])],
67
80
  /* Short of the poles, where `(1 - g^2)` over `(1 + g^2 - 2g)^1.5` is 0/0. */
68
81
  anisotropy: Math.min(0.95, Math.max(-0.95, options.anisotropy ?? d.anisotropy)),
69
82
  maxDistance: Math.max(1e-3, options.maxDistance ?? d.maxDistance),
83
+ heightFalloff: finite(options.heightFalloff, 0),
84
+ baseY: finite(options.baseY, 0),
85
+ density2,
86
+ heightFalloff2: layer2 === null ? 0 : finite(layer2.heightFalloff, 0),
87
+ baseY2: layer2 === null ? 0 : finite(layer2.baseY, 0),
88
+ sunScatter: Math.max(0, finite(options.sunScatter, 1)),
89
+ skyScatter: Math.max(0, finite(options.skyScatter, 1)),
90
+ lampScatter: Math.max(0, finite(options.lampScatter, 0)),
70
91
  };
71
92
  }
93
+ /**
94
+ * `setGlobalMedium`'s two forms, its four numbers or the object, read one way for both backends:
95
+ * a density that is not a number is none.
96
+ */
97
+ export function resolveGlobalMediumSettings(density, albedo, anisotropy, maxDistance) {
98
+ if (typeof density === 'object') {
99
+ return resolveGlobalMedium({
100
+ ...density,
101
+ density: Number.isFinite(density.density) ? density.density : 0,
102
+ });
103
+ }
104
+ return resolveGlobalMedium({
105
+ density: Number.isFinite(density) ? density : 0,
106
+ albedo,
107
+ anisotropy,
108
+ maxDistance,
109
+ });
110
+ }
72
111
  /**
73
112
  * Whether this frame draws the medium at all.
74
113
  *
@@ -98,3 +137,59 @@ export function mediumTargetSize(width, height, halfResolution) {
98
137
  height: Math.max(1, Math.ceil(Math.max(1, height) / divisor)),
99
138
  };
100
139
  }
140
+ /** A target for `resolveMediumLamps`, made once outside the frame loop. */
141
+ export function createResolvedMediumLamps() {
142
+ const vectors = () => new Float32Array(MAX_MEDIUM_LAMPS * 4);
143
+ return {
144
+ count: 0,
145
+ falloff: 0,
146
+ position: vectors(),
147
+ color: vectors(),
148
+ aim: vectors(),
149
+ cone: vectors(),
150
+ shadow: vectors(),
151
+ shadowAt: vectors(),
152
+ live: vectors(),
153
+ liveAt: vectors(),
154
+ };
155
+ }
156
+ /**
157
+ * The frame's lamps as the medium is lit by them, into `out`: the lit stage's resolved lamps and
158
+ * their resolved shadows, a lamp's scatter `lampScatter` times its own `lightMediumScatter` (1 where
159
+ * absent). Allocates nothing; a lamp past the count keeps whatever it held, which nothing reads.
160
+ */
161
+ export function resolveMediumLamps(medium, lights, shadows, scatter, out) {
162
+ const count = medium.lampScatter > 0 ? Math.min(lights.count, MAX_MEDIUM_LAMPS) : 0;
163
+ out.count = count;
164
+ out.falloff = lights.falloff;
165
+ const own = scatter !== undefined && scatter.length >= count ? scatter : undefined;
166
+ for (let i = 0; i < count; i++) {
167
+ const at = i * 4;
168
+ const strength = medium.lampScatter * Math.max(0, own?.[i] ?? 1) * lights.weights[i];
169
+ for (let c = 0; c < 3; c++) {
170
+ out.position[at + c] = lights.positions[i * 3 + c];
171
+ out.color[at + c] = lights.colors[i * 3 + c] * strength;
172
+ out.aim[at + c] = lights.directions[i * 3 + c];
173
+ }
174
+ out.position[at + 3] = lights.radii[i];
175
+ out.color[at + 3] = lights.conesAndFalloffs[at + 2];
176
+ out.aim[at + 3] = 0;
177
+ out.cone[at] = lights.conesAndFalloffs[at];
178
+ out.cone[at + 1] = lights.conesAndFalloffs[at + 1];
179
+ out.cone[at + 2] = 0;
180
+ out.cone[at + 3] = 0;
181
+ out.shadow[at] = shadows === null ? -1 : shadows.layers[i];
182
+ out.shadow[at + 1] = shadows === null ? 0 : shadows.near[i];
183
+ out.shadow[at + 2] = shadows === null ? 0 : shadows.presence[i];
184
+ out.shadow[at + 3] = 0;
185
+ out.live[at] = shadows === null ? -1 : shadows.liveLayers[i];
186
+ out.live[at + 1] = shadows === null ? 0 : shadows.liveNear[i];
187
+ out.live[at + 2] = shadows === null ? 0 : shadows.liveWeights[i];
188
+ out.live[at + 3] = 0;
189
+ for (let k = 0; k < 4; k++) {
190
+ out.shadowAt[at + k] = shadows === null ? 1 : shadows.projections[at + k];
191
+ out.liveAt[at + k] = shadows === null ? 1 : shadows.liveProjections[at + k];
192
+ }
193
+ }
194
+ return out;
195
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ReadonlyMat4 } from 'gl-matrix';
2
- import type { GlobalMediumOptions } from './globalMedium.ts';
3
- /** What the march needs about the light, which is the sun and the sky and nothing else. */
2
+ import type { ResolvedGlobalMedium, ResolvedMediumLamps } from './globalMedium.ts';
3
+ /** What the march needs about the light: the sun, the sky, and the lamps that scatter. */
4
4
  export interface MediumLight {
5
5
  /** Surface → sun, normalised. `Environment.directionalDir` unchanged. */
6
6
  readonly sunDir: ArrayLike<number>;
@@ -16,6 +16,10 @@ export interface MediumLight {
16
16
  /** The sun's static, moving and peeled maps, as one array. See `shadowMap.ts`. */
17
17
  readonly sunShadows: WebGLTexture;
18
18
  readonly peeledEnabled: boolean;
19
+ /** The lamps that scatter, as `resolveMediumLamps` lays them out: none while `lampScatter` is 0. */
20
+ readonly lamps: ResolvedMediumLamps;
21
+ /** The lamps' shadow array, the lit stage's; the placeholder where there is none. */
22
+ readonly pointShadows: WebGLTexture;
19
23
  }
20
24
  /**
21
25
  * The global participating medium on WebGL2: a half-res march, then a depth-aware composite.
@@ -90,7 +94,7 @@ export declare class GlobalMediumPass {
90
94
  * `DEPTH_01_TO_CLIP` carries a stored depth back into the clip space the first was built in,
91
95
  * and pairing either with a clip-corrected matrix applies the remap twice.
92
96
  */
93
- marchMedium(depth: WebGLTexture, frameWidth: number, frameHeight: number, half: boolean, steps: number, options: GlobalMediumOptions, viewProjection: ReadonlyMat4, projection: ReadonlyMat4, eye: ArrayLike<number>, light: MediumLight): boolean;
97
+ marchMedium(depth: WebGLTexture, frameWidth: number, frameHeight: number, half: boolean, steps: number, options: ResolvedGlobalMedium, viewProjection: ReadonlyMat4, projection: ReadonlyMat4, eye: ArrayLike<number>, light: MediumLight): boolean;
94
98
  /**
95
99
  * Composite the march over whatever framebuffer is bound, upsampled depth-aware.
96
100
  *
@@ -161,13 +161,19 @@ export class GlobalMediumPass {
161
161
  gl.activeTexture(gl.TEXTURE1);
162
162
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, light.sunShadows);
163
163
  gl.uniform1i(u['uSunShadows'] ?? null, 1);
164
+ gl.activeTexture(gl.TEXTURE2);
165
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, light.pointShadows);
166
+ gl.uniform1i(u['uPointShadows'] ?? null, 2);
164
167
  gl.uniformMatrix4fv(u['uDepthToWorld'] ?? null, false, this.depthToWorld);
165
168
  gl.uniform3f(u['uCameraPos'] ?? null, eye[0] ?? 0, eye[1] ?? 0, eye[2] ?? 0);
166
169
  gl.uniform3f(u['uSunDir'] ?? null, light.sunDir[0] ?? 0, light.sunDir[1] ?? 1, light.sunDir[2] ?? 0);
167
170
  gl.uniform3f(u['uSunColor'] ?? null, light.sunColor[0] ?? 0, light.sunColor[1] ?? 0, light.sunColor[2] ?? 0);
168
171
  gl.uniform3f(u['uAmbient'] ?? null, light.ambient[0] ?? 0, light.ambient[1] ?? 0, light.ambient[2] ?? 0);
169
172
  gl.uniform1f(u['uDensity'] ?? null, options.density);
170
- gl.uniform1f(u['uAlbedo'] ?? null, options.albedo);
173
+ gl.uniform2f(u['uHeight'] ?? null, options.heightFalloff, options.baseY);
174
+ gl.uniform3f(u['uLayer2'] ?? null, options.density2, options.heightFalloff2, options.baseY2);
175
+ gl.uniform3f(u['uAlbedo'] ?? null, options.albedo[0], options.albedo[1], options.albedo[2]);
176
+ gl.uniform2f(u['uScatter'] ?? null, options.sunScatter, options.skyScatter);
171
177
  gl.uniform1f(u['uAnisotropy'] ?? null, options.anisotropy);
172
178
  gl.uniform1f(u['uMaxDistance'] ?? null, options.maxDistance);
173
179
  gl.uniform1i(u['uSteps'] ?? null, steps);
@@ -175,6 +181,20 @@ export class GlobalMediumPass {
175
181
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, light.lightViewProj);
176
182
  gl.uniformMatrix4fv(u['uMovingLightViewProj'] ?? null, false, light.movingLightViewProj);
177
183
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, light.peeledEnabled ? 1 : 0);
184
+ const lamps = light.lamps;
185
+ gl.uniform1i(u['uLampCount'] ?? null, lamps.count);
186
+ gl.uniform1i(u['uLampFalloff'] ?? null, lamps.falloff);
187
+ /* As `name[0]`, the only spelling an array uniform answers to: see `bindPointShadows`. */
188
+ if (lamps.count > 0) {
189
+ gl.uniform4fv(u['uLampPosition[0]'] ?? null, lamps.position);
190
+ gl.uniform4fv(u['uLampColor[0]'] ?? null, lamps.color);
191
+ gl.uniform4fv(u['uLampAim[0]'] ?? null, lamps.aim);
192
+ gl.uniform4fv(u['uLampCone[0]'] ?? null, lamps.cone);
193
+ gl.uniform4fv(u['uLampShadow[0]'] ?? null, lamps.shadow);
194
+ gl.uniform4fv(u['uLampShadowAt[0]'] ?? null, lamps.shadowAt);
195
+ gl.uniform4fv(u['uLampLive[0]'] ?? null, lamps.live);
196
+ gl.uniform4fv(u['uLampLiveAt[0]'] ?? null, lamps.liveAt);
197
+ }
178
198
  gl.drawArrays(gl.TRIANGLES, 0, 3);
179
199
  gl.bindVertexArray(null);
180
200
  /* Released down to unit 0, for the reason `SsrPass` records: a binding outlives the frame that
@@ -182,6 +202,7 @@ export class GlobalMediumPass {
182
202
  for (const unit of [gl.TEXTURE3, gl.TEXTURE2, gl.TEXTURE1, gl.TEXTURE0]) {
183
203
  gl.activeTexture(unit);
184
204
  gl.bindTexture(gl.TEXTURE_2D, null);
205
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
185
206
  }
186
207
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
187
208
  return true;
@@ -46,6 +46,16 @@ export declare const HAIR_MIN_BETA = 0.05;
46
46
  * and its integral 4π times that. Over π: **1.33**.
47
47
  */
48
48
  export declare const HAIR_SCATTER_SPHERE = 1.33;
49
+ /**
50
+ * How far every lobe is widened for light from every side, in radians: the angle a source would
51
+ * subtend to widen them as much, `sourceRadius / 2·dist` in `hairLight`'s terms.
52
+ *
53
+ * **0.2, the real-time form's own value**, which stands one direction in for the sphere: a lobe as
54
+ * narrow as a lamp's would put the whole sky's light into one highlight wherever the eye's
55
+ * direction happens to meet it. What it gives up is the sky's shape: a sun low in a clear sky and
56
+ * an even overcast light hair through the same widened lobes.
57
+ */
58
+ export declare const HAIR_AREA = 0.2;
49
59
  /** A longitudinal lobe: a normalised Gaussian of width `beta` at `x` from its centre. */
50
60
  export declare function hairLongitudinal(beta: number, x: number): number;
51
61
  /** Schlick's Fresnel for the strand at a cosine, clamped to the unit interval. */
@@ -61,10 +71,25 @@ export declare const HAIR_OUT: {
61
71
  /**
62
72
  * One light on a strand, in lamp units and unshadowed, into `out` (`HAIR_OUT`'s layout: R is grey,
63
73
  * the rest colours). `tangent`, `toLight` and `toEye` are unit vectors but for a tangent that could
64
- * not be made one, which comes in as zeros; `fibre` is the base colour.
74
+ * not be made one, which comes in as zeros; `fibre` is the base colour; `specular` is the strand's,
75
+ * the model map's red, which scales the reflected lobe by twice it, so 0.5 draws the lobe as the
76
+ * fit gives it and 0 is a strand that reflects nothing off its surface.
65
77
  *
66
78
  * **A lamp with a size widens every lobe by the angle it subtends**, `sourceRadius / 2·dist` as
67
79
  * `sphereLobe` takes it, and the lobes are normalised Gaussians, so a wider one carries the same
68
80
  * light rather than more. The sun passes zeros.
69
81
  */
70
- export declare function hairLight(tangent: ArrayLike<number>, toLight: ArrayLike<number>, toEye: ArrayLike<number>, fibre: Vec3, roughness: number, shift: number, scatter: number, backlit: number, sourceRadius: number, dist: number, out: Float64Array): void;
82
+ export declare function hairLight(tangent: ArrayLike<number>, toLight: ArrayLike<number>, toEye: ArrayLike<number>, fibre: Vec3, roughness: number, shift: number, specular: number, scatter: number, backlit: number, sourceRadius: number, dist: number, out: Float64Array): void;
83
+ /**
84
+ * The share of light from every side a strand returns, a colour into `out`, with the one
85
+ * direction's lobes it is made of into `lobes` (`HAIR_OUT`'s layout). `normal` is the surface's,
86
+ * which stands in for the flattened eye where the eye runs along the strand and there is none.
87
+ *
88
+ * **One direction for the sphere**: the eye flattened across the strand, `V − T(V·T)`, unshadowed,
89
+ * with no light from behind and so no transmitted lobe, every lobe widened by `HAIR_AREA`. Twice
90
+ * the sum, and at most all of what arrives. **Why twice**: the scatter's half is then
91
+ * `HAIR_SCATTER_SPHERE`, its own answer to an even sky — along the flattened eye its shape is
92
+ * `mix(1/2, 1, 0.33)`, 0.665, which doubled is that constant's 1.33 — so the lobes ride on a
93
+ * scatter the sphere's quadrature already holds to, and R and TRT are what the direction adds.
94
+ */
95
+ export declare function hairIndirect(tangent: ArrayLike<number>, toEye: ArrayLike<number>, normal: ArrayLike<number>, fibre: Vec3, roughness: number, shift: number, specular: number, scatter: number, lobes: Float64Array, out: Float64Array): void;