@driftengine/core 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.json +272 -0
  2. package/README.md +1 -1
  3. package/dist/cinematic/cameraPath.d.ts +53 -0
  4. package/dist/cinematic/cameraPath.js +118 -0
  5. package/dist/environment/daylight.d.ts +77 -0
  6. package/dist/environment/daylight.js +77 -0
  7. package/dist/geometry/meshConcat.d.ts +9 -0
  8. package/dist/geometry/meshConcat.js +101 -0
  9. package/dist/index.d.ts +12 -2
  10. package/dist/index.js +14 -1
  11. package/dist/render/backend/createRenderer.d.ts +13 -1
  12. package/dist/render/backend/createRenderer.js +6 -5
  13. package/dist/render/backend/webgl2/renderer.d.ts +166 -1
  14. package/dist/render/backend/webgl2/renderer.js +424 -23
  15. package/dist/render/backend/webgpu/buffers.js +46 -8
  16. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  17. package/dist/render/backend/webgpu/depthPass.js +75 -14
  18. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  19. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  20. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  21. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  22. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  23. package/dist/render/backend/webgpu/flatPass.js +12 -5
  24. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  25. package/dist/render/backend/webgpu/particlePass.js +1 -0
  26. package/dist/render/backend/webgpu/postPass.d.ts +12 -2
  27. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  28. package/dist/render/backend/webgpu/probeBake.js +97 -7
  29. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  30. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  31. package/dist/render/backend/webgpu/renderer.d.ts +176 -11
  32. package/dist/render/backend/webgpu/renderer.js +680 -250
  33. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  34. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  35. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  36. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  37. package/dist/render/backend/webgpu/skyPass.js +5 -1
  38. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  39. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  40. package/dist/render/bloomChain.d.ts +6 -0
  41. package/dist/render/bloomChain.js +8 -0
  42. package/dist/render/clusteredLights.d.ts +19 -11
  43. package/dist/render/clusteredLights.js +96 -45
  44. package/dist/render/cutoutCaster.d.ts +23 -0
  45. package/dist/render/cutoutCaster.js +20 -0
  46. package/dist/render/driftLight/bake.d.ts +34 -0
  47. package/dist/render/driftLight/bake.js +95 -0
  48. package/dist/render/driftLight/falloff.d.ts +16 -0
  49. package/dist/render/driftLight/falloff.js +23 -0
  50. package/dist/render/driftLight/layout.d.ts +59 -0
  51. package/dist/render/driftLight/layout.js +138 -0
  52. package/dist/render/driftLight/lightField.d.ts +108 -0
  53. package/dist/render/driftLight/lightField.js +179 -0
  54. package/dist/render/driftLight/uniforms.d.ts +28 -0
  55. package/dist/render/driftLight/uniforms.js +41 -0
  56. package/dist/render/driftLight/visibility.d.ts +18 -0
  57. package/dist/render/driftLight/visibility.js +47 -0
  58. package/dist/render/emptyTexture.d.ts +6 -0
  59. package/dist/render/emptyTexture.js +19 -0
  60. package/dist/render/exposurePass.d.ts +48 -0
  61. package/dist/render/exposurePass.js +172 -0
  62. package/dist/render/flameLight.d.ts +71 -0
  63. package/dist/render/flameLight.js +63 -0
  64. package/dist/render/gi/bounceLights.d.ts +34 -0
  65. package/dist/render/gi/bounceLights.js +66 -0
  66. package/dist/render/gi/fieldReach.d.ts +12 -0
  67. package/dist/render/gi/fieldReach.js +62 -0
  68. package/dist/render/gi/globalField.d.ts +8 -0
  69. package/dist/render/gi/globalField.js +31 -0
  70. package/dist/render/gi/probeTrace.d.ts +3 -1
  71. package/dist/render/gi/probeTrace.js +3 -1
  72. package/dist/render/gpuCapability.js +19 -4
  73. package/dist/render/lightBudget.d.ts +11 -0
  74. package/dist/render/lightBudget.js +11 -0
  75. package/dist/render/pointLightSelection.d.ts +16 -0
  76. package/dist/render/pointLightSelection.js +178 -83
  77. package/dist/render/probeGrid.d.ts +11 -1
  78. package/dist/render/probeGrid.js +5 -2
  79. package/dist/render/probeSweeps.d.ts +45 -0
  80. package/dist/render/probeSweeps.js +77 -0
  81. package/dist/render/reflectionProbe.d.ts +43 -1
  82. package/dist/render/reflectionProbe.js +17 -3
  83. package/dist/render/renderQuality.d.ts +3 -1
  84. package/dist/render/rendererHarness.d.ts +9 -0
  85. package/dist/render/rendererHarness.js +18 -3
  86. package/dist/render/sceneTarget.d.ts +20 -0
  87. package/dist/render/sceneTarget.js +62 -1
  88. package/dist/render/shaders/agx.d.ts +31 -0
  89. package/dist/render/shaders/agx.js +81 -0
  90. package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
  91. package/dist/render/shaders/ambientOcclusion.js +120 -63
  92. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  93. package/dist/render/shaders/cutoutCoverage.js +40 -0
  94. package/dist/render/shaders/depth.d.ts +25 -0
  95. package/dist/render/shaders/depth.js +69 -3
  96. package/dist/render/shaders/exposure.d.ts +52 -0
  97. package/dist/render/shaders/exposure.js +124 -0
  98. package/dist/render/shaders/filmLook.d.ts +47 -0
  99. package/dist/render/shaders/filmLook.js +98 -0
  100. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  101. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  102. package/dist/render/shaders/flat/driftLight.js +75 -0
  103. package/dist/render/shaders/flat/index.js +5 -0
  104. package/dist/render/shaders/flat/lobes.js +13 -0
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +68 -9
  107. package/dist/render/shaders/flat/preamble.js +23 -5
  108. package/dist/render/shaders/flat/probeGrid.js +46 -3
  109. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
  111. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  112. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  113. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  115. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  116. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  117. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  118. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  119. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  120. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  121. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  123. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  124. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  125. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  126. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  127. package/dist/render/shaders/globalMedium.js +4 -0
  128. package/dist/render/shaders/localExposure.d.ts +35 -0
  129. package/dist/render/shaders/localExposure.js +138 -0
  130. package/dist/render/shaders/prefilter.d.ts +1 -1
  131. package/dist/render/shaders/prefilter.js +6 -0
  132. package/dist/render/shaders/rush.js +33 -5
  133. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  134. package/dist/render/shaders/tangentFrame.js +15 -3
  135. package/dist/render/shaders/temporalResolve.js +39 -1
  136. package/dist/render/shadowCasters.d.ts +5 -3
  137. package/dist/render/shadowMap.js +13 -0
  138. package/dist/render/surfaceTexture.d.ts +5 -0
  139. package/package.json +6 -6
  140. package/scripts/gpuCompute.mjs +24 -3
  141. package/scripts/png.mjs +23 -12
  142. package/src/cinematic/cameraPath.ts +178 -0
  143. package/src/environment/daylight.ts +152 -0
  144. package/src/geometry/meshConcat.ts +100 -0
  145. package/src/index.ts +32 -0
  146. package/src/render/backend/createRenderer.ts +22 -6
  147. package/src/render/backend/webgl2/renderer.ts +462 -20
  148. package/src/render/backend/webgpu/buffers.ts +51 -8
  149. package/src/render/backend/webgpu/depthPass.ts +78 -10
  150. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  151. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  152. package/src/render/backend/webgpu/flatPass.ts +10 -2
  153. package/src/render/backend/webgpu/particlePass.ts +9 -0
  154. package/src/render/backend/webgpu/probeBake.ts +143 -9
  155. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  156. package/src/render/backend/webgpu/renderer.ts +807 -271
  157. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  158. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  159. package/src/render/backend/webgpu/skyPass.ts +5 -1
  160. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  161. package/src/render/bloomChain.ts +9 -0
  162. package/src/render/clusteredLights.ts +108 -63
  163. package/src/render/cutoutCaster.ts +44 -0
  164. package/src/render/driftLight/bake.ts +125 -0
  165. package/src/render/driftLight/falloff.ts +26 -0
  166. package/src/render/driftLight/layout.ts +177 -0
  167. package/src/render/driftLight/lightField.ts +227 -0
  168. package/src/render/driftLight/uniforms.ts +61 -0
  169. package/src/render/driftLight/visibility.ts +58 -0
  170. package/src/render/emptyTexture.ts +41 -0
  171. package/src/render/exposurePass.ts +186 -0
  172. package/src/render/flameLight.ts +135 -0
  173. package/src/render/gi/bounceLights.ts +68 -0
  174. package/src/render/gi/fieldReach.ts +74 -0
  175. package/src/render/gi/globalField.ts +39 -0
  176. package/src/render/gi/probeTrace.ts +3 -1
  177. package/src/render/gpuCapability.ts +18 -5
  178. package/src/render/lightBudget.ts +12 -0
  179. package/src/render/pointLightSelection.ts +191 -79
  180. package/src/render/probeGrid.ts +13 -2
  181. package/src/render/probeSweeps.ts +80 -0
  182. package/src/render/reflectionProbe.ts +54 -2
  183. package/src/render/renderQuality.ts +3 -1
  184. package/src/render/rendererHarness.ts +25 -3
  185. package/src/render/sceneTarget.ts +75 -1
  186. package/src/render/shaders/ambientOcclusion.ts +125 -63
  187. package/src/render/shaders/cutoutCoverage.ts +43 -0
  188. package/src/render/shaders/depth.ts +80 -3
  189. package/src/render/shaders/exposure.ts +128 -0
  190. package/src/render/shaders/filmLook.ts +111 -0
  191. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  192. package/src/render/shaders/flat/driftLight.ts +75 -0
  193. package/src/render/shaders/flat/index.ts +5 -0
  194. package/src/render/shaders/flat/lobes.ts +13 -0
  195. package/src/render/shaders/flat/main.ts +68 -9
  196. package/src/render/shaders/flat/preamble.ts +23 -5
  197. package/src/render/shaders/flat/probeGrid.ts +46 -3
  198. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
  199. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  200. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  201. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  202. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  203. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  204. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  205. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  206. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  207. package/src/render/shaders/globalMedium.ts +4 -0
  208. package/src/render/shaders/localExposure.ts +154 -0
  209. package/src/render/shaders/prefilter.ts +6 -0
  210. package/src/render/shaders/rush.ts +33 -5
  211. package/src/render/shaders/tangentFrame.ts +15 -3
  212. package/src/render/shaders/temporalResolve.ts +39 -1
  213. package/src/render/shadowCasters.ts +5 -3
  214. package/src/render/shadowMap.ts +14 -0
  215. package/src/render/surfaceTexture.ts +5 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * A probe grid's sweeps: which layer a bake writes, when a sweep is whole, and how far the shading
3
+ * has moved from the last whole sweep toward the newest.
4
+ *
5
+ * **A grid re-baked a probe a frame changes in steps.** Each probe holds the light of the moment it
6
+ * was baked until its turn comes round again, a sweep later, and then jumps to the light of that
7
+ * moment. Under a moving sun the jump is a pass of light across the scene once a sweep: reported as
8
+ * "a pass every second or two", which at sixty-four probes a frame at 60 Hz is exactly one sweep.
9
+ *
10
+ * **So a crossfading grid keeps three sets of layers**: the last whole sweep, the newest whole
11
+ * sweep, and the one being baked. The shading blends the first toward the second by how much of the
12
+ * third has been baked, so it reaches the newest sweep at the moment the next one is whole, and the
13
+ * light moves a sixty-fourth of a sweep a frame rather than a probe's worth at once. The set being
14
+ * written is never one the shading reads.
15
+ *
16
+ * What it gives up is three times the layers, and a sweep of lag: the light shown is a blend of what
17
+ * the grid saw one and two sweeps ago. What would make it wrong is a light that must follow a change
18
+ * faster than a sweep, which a burst of bakes serves, since the blend follows the bakes rather than
19
+ * the clock. A grid that does not crossfade is one set, ready once every probe is baked, as before.
20
+ *
21
+ * Backend-neutral, per the rule that a decision lives in one place and only the binding is per
22
+ * backend: both renderers ask this where to write and what to upload.
23
+ */
24
+ export class ProbeSweeps {
25
+ /** Sets of layers the array holds: three for a crossfading grid, one otherwise. */
26
+ sets;
27
+ layers;
28
+ /** Which probes the sweep being baked has reached, and how many. */
29
+ reached;
30
+ reachedCount = 0;
31
+ from = 0;
32
+ to = 0;
33
+ write = 0;
34
+ /** Whether a whole sweep has been rotated in. */
35
+ whole = false;
36
+ constructor(layers, crossfade) {
37
+ this.layers = Math.max(1, Math.trunc(layers));
38
+ this.sets = crossfade ? 3 : 1;
39
+ this.reached = new Uint8Array(this.layers);
40
+ }
41
+ /** The array layer a bake of probe `layer` writes. */
42
+ writeLayer(layer) {
43
+ return this.write * this.layers + layer;
44
+ }
45
+ /** Record that probe `layer` was baked into the set being written; a whole sweep rotates. */
46
+ baked(layer) {
47
+ if (layer < 0 || layer >= this.layers || this.reached[layer] === 1)
48
+ return;
49
+ this.reached[layer] = 1;
50
+ this.reachedCount++;
51
+ if (this.reachedCount < this.layers)
52
+ return;
53
+ this.reached.fill(0);
54
+ this.reachedCount = 0;
55
+ if (this.sets === 1) {
56
+ this.whole = true;
57
+ return;
58
+ }
59
+ /* The newest is shown from now on; the first whole sweep is shown alone, blending into itself. */
60
+ this.from = this.whole ? this.to : this.write;
61
+ this.to = this.write;
62
+ this.whole = true;
63
+ /* The set neither end of the blend holds. */
64
+ this.write = this.from === this.to ? (this.to + 1) % 3 : 3 - this.from - this.to;
65
+ }
66
+ /** Whether the shading may read the grid: once a sweep is whole. */
67
+ get ready() {
68
+ return this.whole;
69
+ }
70
+ /** The layer offsets of the two sets shown and the blend between them, into `out`. */
71
+ uniforms(out) {
72
+ out[0] = this.from * this.layers;
73
+ out[1] = this.to * this.layers;
74
+ out[2] = this.sets === 1 ? 0 : this.reachedCount / this.layers;
75
+ return out;
76
+ }
77
+ }
@@ -59,7 +59,46 @@ export interface ProbeBakeOptions {
59
59
  * nothing gets the frame it already had.
60
60
  */
61
61
  readonly irradiance?: boolean;
62
+ /**
63
+ * Whether what the probe sees is lit by the grid's last bake, so each bake adds a bounce.
64
+ *
65
+ * Defaults to **false**, which is what every bake did before this existed. Then everything a
66
+ * probe sees is lit by the sun and by the hemispheric ambient, so the grid holds one bounce. An
67
+ * enclosed space is mostly lit at the second bounce and later. A courtyard's arcades are the
68
+ * case this was built for: one bounce came out about two stops under a reference render.
69
+ *
70
+ * With it on, the surfaces in each face take their ambient from the grid instead. Round-robin
71
+ * baking then converges on the space's full interreflection, about a bounce each time the grid
72
+ * is swept, and a scene holding one moment bakes the grid two or three times. It costs no pass
73
+ * and no memory. The faces are drawn into their own cube and only the resolve writes the array,
74
+ * so the array is never read and written in one pass.
75
+ *
76
+ * **Nothing changes until every probe has been baked once**, because until then the grid is not
77
+ * readable; the first sweep lights by the ambient as before. What it gives up: light that leaks
78
+ * through a wall into a probe is bounced again, so a leak grows with each sweep. What would make
79
+ * it wrong is a space with near-white walls, where a sweep adds almost as much as the last and
80
+ * convergence takes many sweeps.
81
+ */
82
+ readonly bounce?: boolean;
83
+ /**
84
+ * Which of the six faces this call draws, as `[first, count]`: all six when absent.
85
+ *
86
+ * **A bake is six draws of the scene, and a scene of millions of triangles makes it the most
87
+ * expensive thing in a frame.** Measured on a bought courtyard of eleven million: 6.4 ms of a
88
+ * 14 ms frame, on the frames it ran, which is a frame rate no display can hold steady. Spread over
89
+ * calls, each draws only its faces into the capture, and the convolution into the layer, which
90
+ * reads all six, runs when the sixth is drawn. Faces in the order a cube numbers them,
91
+ * `+X, -X, +Y, -Y, +Z, -Z`.
92
+ *
93
+ * **The caller finishes one probe before starting the next**, because the capture the faces are
94
+ * drawn into is one for the whole grid: faces of two probes interleaved make a cube of neither.
95
+ * What it gives up is a probe whose faces were drawn a few frames apart, under a light that moved
96
+ * a little between them.
97
+ */
98
+ readonly faces?: readonly [first: number, count: number];
62
99
  }
100
+ /** The faces a bake draws, `[first, end)`, from `ProbeBakeOptions.faces`. All six when absent. */
101
+ export declare function probeFaceRange(options: ProbeBakeOptions | undefined): [number, number];
63
102
  export declare class ReflectionProbe {
64
103
  /**
65
104
  * The captured cube, box-filtered, which the convolution reads and the projection reads.
@@ -188,8 +227,11 @@ export declare class ReflectionProbe {
188
227
  * Returns whether the bake happened. A false means the allocation was refused and the caller
189
228
  * should carry on without a probe, which is a picture with a gradient in its paint rather
190
229
  * than a room.
230
+ *
231
+ * `first` and `end` draw only those faces, and the chain the roughness reads is built when the
232
+ * sixth is drawn. See `ProbeBakeOptions.faces`.
191
233
  */
192
- bake(gl: WebGL2RenderingContext, origin: Vec3, clearColor: Vec3, drawFace: (camera: Camera) => void): boolean;
234
+ bake(gl: WebGL2RenderingContext, origin: Vec3, clearColor: Vec3, drawFace: (camera: Camera) => void, first?: number, end?: number): boolean;
193
235
  /**
194
236
  * Read six faces back off the GPU, at one mip level, for the irradiance projection.
195
237
  *
@@ -34,6 +34,15 @@ export function cubeFaceProjection(out, viewProj) {
34
34
  }
35
35
  return out;
36
36
  }
37
+ /** The faces a bake draws, `[first, end)`, from `ProbeBakeOptions.faces`. All six when absent. */
38
+ export function probeFaceRange(options) {
39
+ const asked = options?.faces;
40
+ if (asked === undefined)
41
+ return [0, 6];
42
+ const first = Math.min(Math.max(Math.trunc(asked[0]), 0), 5);
43
+ const count = Math.max(Math.trunc(asked[1]), 1);
44
+ return [first, Math.min(first + count, 6)];
45
+ }
37
46
  /**
38
47
  * The six directions of a cubemap, as the yaw and pitch a `Camera` aims with.
39
48
  *
@@ -336,8 +345,11 @@ export class ReflectionProbe {
336
345
  * Returns whether the bake happened. A false means the allocation was refused and the caller
337
346
  * should carry on without a probe, which is a picture with a gradient in its paint rather
338
347
  * than a room.
348
+ *
349
+ * `first` and `end` draw only those faces, and the chain the roughness reads is built when the
350
+ * sixth is drawn. See `ProbeBakeOptions.faces`.
339
351
  */
340
- bake(gl, origin, clearColor, drawFace) {
352
+ bake(gl, origin, clearColor, drawFace, first = 0, end = FACES.length) {
341
353
  if (this.unusable)
342
354
  return false;
343
355
  const camera = this.camera;
@@ -351,12 +363,12 @@ export class ReflectionProbe {
351
363
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
352
364
  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depth);
353
365
  gl.viewport(0, 0, this.size, this.size);
354
- for (let face = 0; face < FACES.length; face++) {
366
+ for (let face = first; face < end; face++) {
355
367
  const aim = FACES[face];
356
368
  if (aim === undefined)
357
369
  continue;
358
370
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, this.texture, 0);
359
- if (face === 0 && gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
371
+ if (face === first && gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
360
372
  this.unusable = true;
361
373
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
362
374
  console.warn('ReflectionProbe: this driver will not render to a cubemap face, so reflective ' +
@@ -376,6 +388,8 @@ export class ReflectionProbe {
376
388
  drawFace(camera);
377
389
  }
378
390
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
391
+ if (end < FACES.length)
392
+ return true;
379
393
  /* The blur the roughness reads. Once per bake, not once per frame. */
380
394
  gl.bindTexture(gl.TEXTURE_CUBE_MAP, this.texture);
381
395
  gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
@@ -552,7 +552,9 @@ export interface RenderQuality {
552
552
  */
553
553
  readonly reconstruction: number;
554
554
  /**
555
- * Light a scene from what bounces, rather than from what was baked. **Off by default.**
555
+ * Light a scene from what bounces, rather than from what was baked: **DriftRay**, this engine's
556
+ * ray tracing, rays marched through the declared distance fields in GPU compute rather than on
557
+ * the dedicated ray-tracing units WebGPU does not expose. **Off by default.**
556
558
  *
557
559
  * On, the probe grid the shading already samples is filled by tracing `render/gi/`'s three-level
558
560
  * chain — the world distance field, and behind it the probes themselves, which is what makes the
@@ -29,6 +29,15 @@ export declare function recordingGl(options?: {
29
29
  * `Infinity` by default, so a link only ever fails where a test asked for it.
30
30
  */
31
31
  readonly refuseLinkAbove?: number;
32
+ /**
33
+ * Uniforms every program reports as active, each located by a handle carrying its name.
34
+ *
35
+ * **None by default**, which ends every reflection loop at once and leaves every upload aimed
36
+ * at `null`. A test about what a frame *uploads* names the uniform here and then finds the
37
+ * calls whose location says that name, rather than guessing at an upload's position in the
38
+ * recording.
39
+ */
40
+ readonly uniforms?: readonly string[];
32
41
  }): {
33
42
  gl: WebGL2RenderingContext;
34
43
  canvas: HTMLCanvasElement;
@@ -18,6 +18,9 @@
18
18
  import { countUniformVectors } from './uniformVectorBudget.js';
19
19
  export function recordingGl(options = {}) {
20
20
  const extensions = new Set(options.extensions ?? []);
21
+ const uniforms = options.uniforms ?? [];
22
+ /* What `clearDepth` last set, so a module that borrows the value can be seen to put it back. */
23
+ let depthClear = 1;
21
24
  const reportedVectors = options.fragmentUniformVectors ?? 1024;
22
25
  const enforcedVectors = options.refuseLinkAbove ?? Number.POSITIVE_INFINITY;
23
26
  /** Fragment sources by shader handle, so a refusal can be decided from what was compiled. */
@@ -37,6 +40,7 @@ export function recordingGl(options = {}) {
37
40
  MAX_TEXTURE_IMAGE_UNITS: 0x8872,
38
41
  MAX_VERTEX_UNIFORM_VECTORS: 0x8dfb,
39
42
  MAX_FRAGMENT_UNIFORM_VECTORS: 0x8dfd,
43
+ DEPTH_CLEAR_VALUE: 0x0b73,
40
44
  /*
41
45
  * Enums a test needs to compare against, rather than merely to pass through.
42
46
  *
@@ -97,6 +101,8 @@ export function recordingGl(options = {}) {
97
101
  return reportedVectors;
98
102
  if (pname === constants.MAX_VERTEX_UNIFORM_VECTORS)
99
103
  return 1024;
104
+ if (pname === constants.DEPTH_CLEAR_VALUE)
105
+ return depthClear;
100
106
  /* Generous by default: every remaining limit is a ceiling something is checked against,
101
107
  and a small answer is read as a device that cannot do the thing. */
102
108
  return 4096;
@@ -129,7 +135,9 @@ export function recordingGl(options = {}) {
129
135
  /* Linked yes; zero active uniforms and attributes, which ends every reflection loop at once
130
136
  and is why this harness does not have to know a single uniform name. */
131
137
  getProgramParameter: (program, pname) => {
132
- if (pname === constants.ACTIVE_UNIFORMS || pname === constants.ACTIVE_ATTRIBUTES)
138
+ if (pname === constants.ACTIVE_UNIFORMS)
139
+ return uniforms.length;
140
+ if (pname === constants.ACTIVE_ATTRIBUTES)
133
141
  return 0;
134
142
  if (pname !== constants.LINK_STATUS)
135
143
  return true;
@@ -140,9 +148,16 @@ export function recordingGl(options = {}) {
140
148
  const source = programFragments.get(program);
141
149
  return source === undefined || countUniformVectors(source) <= enforcedVectors;
142
150
  },
143
- getActiveUniform: () => ({ name: '', size: 1, type: 0 }),
151
+ getActiveUniform: (_program, index) => ({
152
+ name: uniforms[Number(index)] ?? '',
153
+ size: 1,
154
+ type: 0,
155
+ }),
144
156
  getActiveAttrib: () => ({ name: '', size: 1, type: 0 }),
145
- getUniformLocation: () => ({}),
157
+ getUniformLocation: (_program, name) => ({ name }),
158
+ clearDepth: (value) => {
159
+ depthClear = Number(value);
160
+ },
146
161
  getAttribLocation: () => -1,
147
162
  checkFramebufferStatus: () => constants.FRAMEBUFFER_COMPLETE,
148
163
  getError: () => constants.NO_ERROR,
@@ -85,6 +85,16 @@ export declare class SceneTarget {
85
85
  private gradeSource;
86
86
  private gradeSize;
87
87
  private gradeStrength;
88
+ /** The lens and the print, held like the grade. See `filmLook.ts`. */
89
+ private vignette;
90
+ private grain;
91
+ private grainSeed;
92
+ /** Eye adaptation: how far, and this frame's time step. See `Renderer.setAutoExposure`. */
93
+ private autoExposure;
94
+ private autoExposureDt;
95
+ /** How far each region is brought toward the frame. See `Renderer.setLocalExposure`. */
96
+ private localExposure;
97
+ private exposure;
88
98
  private width;
89
99
  private height;
90
100
  /**
@@ -244,6 +254,16 @@ export declare class SceneTarget {
244
254
  * `null`, or a strength at or below zero, is exactly the frame that existed before the effect
245
255
  * did: the shader's branch is on a uniform, so no fetch happens at all.
246
256
  */
257
+ /** The lens's corner falloff, held until changed. See `Renderer.setVignette`. */
258
+ setVignette(strength: number): void;
259
+ /** The print's grain and this frame's seed, held until changed. See `Renderer.setFilmGrain`. */
260
+ setFilmGrain(strength: number, seed: number): void;
261
+ /** How far the eye adapts, and the time since the last frame. See `Renderer.setAutoExposure`. */
262
+ setAutoExposure(strength: number, dtSec: number): void;
263
+ /** How far each region is brought toward the frame. See `Renderer.setLocalExposure`. */
264
+ setLocalExposure(strength: number): void;
265
+ /** A new shot: the eye meters it afresh rather than easing in from the last one. */
266
+ cutExposure(): void;
247
267
  setColourGrade(lut: ColourGradeLut | null, strength: number): void;
248
268
  /**
249
269
  * Put a table on the device, replacing whatever was there.
@@ -1,5 +1,9 @@
1
1
  import { glSceneDepthFormat } from './depthConvention.js';
2
+ import { clampGrain, clampVignette, grainSeed } from './shaders/filmLook.js';
2
3
  import { AmbientOcclusionPass } from './ambientOcclusionPass.js';
4
+ import { ExposurePass } from './exposurePass.js';
5
+ import { clampAutoExposure } from './shaders/exposure.js';
6
+ import { clampLocalExposure } from './shaders/localExposure.js';
3
7
  import { BloomPass } from './bloomPass.js';
4
8
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from './colourGrade.js';
5
9
  import { compileProgram, uniformLocations } from './shader.js';
@@ -85,6 +89,16 @@ export class SceneTarget {
85
89
  gradeSource = null;
86
90
  gradeSize = GRADE_PLACEHOLDER_SIZE;
87
91
  gradeStrength = 0;
92
+ /** The lens and the print, held like the grade. See `filmLook.ts`. */
93
+ vignette = 0;
94
+ grain = 0;
95
+ grainSeed = 0;
96
+ /** Eye adaptation: how far, and this frame's time step. See `Renderer.setAutoExposure`. */
97
+ autoExposure = 0;
98
+ autoExposureDt = 0;
99
+ /** How far each region is brought toward the frame. See `Renderer.setLocalExposure`. */
100
+ localExposure = 0;
101
+ exposure = null;
88
102
  width = 0;
89
103
  height = 0;
90
104
  /**
@@ -520,6 +534,28 @@ export class SceneTarget {
520
534
  * `null`, or a strength at or below zero, is exactly the frame that existed before the effect
521
535
  * did: the shader's branch is on a uniform, so no fetch happens at all.
522
536
  */
537
+ /** The lens's corner falloff, held until changed. See `Renderer.setVignette`. */
538
+ setVignette(strength) {
539
+ this.vignette = clampVignette(strength);
540
+ }
541
+ /** The print's grain and this frame's seed, held until changed. See `Renderer.setFilmGrain`. */
542
+ setFilmGrain(strength, seed) {
543
+ this.grain = clampGrain(strength);
544
+ this.grainSeed = grainSeed(seed);
545
+ }
546
+ /** How far the eye adapts, and the time since the last frame. See `Renderer.setAutoExposure`. */
547
+ setAutoExposure(strength, dtSec) {
548
+ this.autoExposure = clampAutoExposure(strength);
549
+ this.autoExposureDt = dtSec;
550
+ }
551
+ /** How far each region is brought toward the frame. See `Renderer.setLocalExposure`. */
552
+ setLocalExposure(strength) {
553
+ this.localExposure = clampLocalExposure(strength);
554
+ }
555
+ /** A new shot: the eye meters it afresh rather than easing in from the last one. */
556
+ cutExposure() {
557
+ this.exposure?.cut();
558
+ }
523
559
  setColourGrade(lut, strength) {
524
560
  this.gradeStrength = lut === null ? 0 : Math.max(0, Math.min(1, strength));
525
561
  if (lut === null || lut === this.gradeSource)
@@ -682,6 +718,16 @@ export class SceneTarget {
682
718
  this.bloom ??= new BloomPass(gl);
683
719
  bloomTexture = this.bloom.run(scene, this.width, this.height, bloom.threshold);
684
720
  }
721
+ /*
722
+ * Eye adaptation, metered from the same resolved scene bloom reads, and after it for the same
723
+ * reason: the composite consumes it. Off at zero strength, where nothing is allocated or drawn.
724
+ */
725
+ let heldExposure = null;
726
+ const local = this.localExposure > 0;
727
+ if ((this.autoExposure > 0 || local) && scene !== null) {
728
+ this.exposure ??= new ExposurePass(gl);
729
+ heldExposure = this.exposure.run(scene, this.autoExposureDt, local);
730
+ }
685
731
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
686
732
  gl.viewport(0, 0, this.width, this.height);
687
733
  gl.disable(gl.DEPTH_TEST);
@@ -767,6 +813,18 @@ export class SceneTarget {
767
813
  gl.uniform1i(this.uniforms['uGradeLut'] ?? null, 4);
768
814
  gl.uniform1f(this.uniforms['uGradeStrength'] ?? null, this.gradeStrength);
769
815
  gl.uniform1f(this.uniforms['uGradeSize'] ?? null, this.gradeSize);
816
+ /* The held brightness on unit 5, and the scene as the placeholder where there is none, for
817
+ the reason the occlusion's placeholder gives. Zero strength is the whole of the off path. */
818
+ gl.activeTexture(gl.TEXTURE5);
819
+ gl.bindTexture(gl.TEXTURE_2D, heldExposure ?? scene);
820
+ gl.uniform1i(this.uniforms['uExposureHeld'] ?? null, 5);
821
+ gl.uniform1f(this.uniforms['uAutoExposure'] ?? null, heldExposure === null ? 0 : this.autoExposure);
822
+ /* Local exposure's grid on unit 6, with the same placeholder and the same off path. */
823
+ const localGrid = local && heldExposure !== null ? (this.exposure?.localTexture ?? null) : null;
824
+ gl.activeTexture(gl.TEXTURE6);
825
+ gl.bindTexture(gl.TEXTURE_2D, localGrid ?? scene);
826
+ gl.uniform1i(this.uniforms['uExposureLocal'] ?? null, 6);
827
+ gl.uniform1f(this.uniforms['uLocalExposure'] ?? null, localGrid === null ? 0 : this.localExposure);
770
828
  gl.activeTexture(gl.TEXTURE0);
771
829
  /* No unit and no texture: a flat colour, not a sample. Uploaded unconditionally — two
772
830
  floats and a triplet cost nothing next to the triangle this pass already draws — and it
@@ -774,6 +832,8 @@ export class SceneTarget {
774
832
  mix it does not need. */
775
833
  gl.uniform3fv(this.uniforms['uVeilColor'] ?? null, veilColor);
776
834
  gl.uniform1f(this.uniforms['uVeilAlpha'] ?? null, veilAlpha);
835
+ gl.uniform1f(this.uniforms['uVignette'] ?? null, this.vignette);
836
+ gl.uniform2f(this.uniforms['uGrain'] ?? null, this.grain, this.grainSeed);
777
837
  gl.bindVertexArray(this.vao);
778
838
  gl.drawArrays(gl.TRIANGLES, 0, 3);
779
839
  gl.bindVertexArray(null);
@@ -788,7 +848,7 @@ export class SceneTarget {
788
848
  * own framebuffers on the next frame, and that is how this was found: 120 pixels of a frame
789
849
  * that should have been identical, and a warning per draw explaining why.
790
850
  */
791
- for (let unit = 0; unit <= 4; unit++) {
851
+ for (let unit = 0; unit <= 5; unit++) {
792
852
  gl.activeTexture(gl.TEXTURE0 + unit);
793
853
  gl.bindTexture(gl.TEXTURE_2D, null);
794
854
  }
@@ -815,6 +875,7 @@ export class SceneTarget {
815
875
  dispose() {
816
876
  const { gl } = this;
817
877
  this.ao?.dispose();
878
+ this.exposure?.dispose();
818
879
  this.ao = null;
819
880
  this.bloom?.dispose();
820
881
  this.bloom = null;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * AgX: a tone curve whose shade stays open, for scenes lit mostly by bounce.
3
+ *
4
+ * **What ACES does to a courtyard.** The reference RRT and ODT are graded for a dark cinema, and
5
+ * their toe is steep: a surface at 1% of white leaves the curve at 4 of 255. A room lit by light
6
+ * bounced off its own walls lives in exactly that range, so its arcades read black beside renders
7
+ * of the same model, and the grime in its stone reads as ink. Measured on a bought courtyard: 39%
8
+ * of the frame below 16 of 255, against about 5% in the model maker's own renders.
9
+ *
10
+ * **AgX encodes the scene logarithmically and draws one sigmoid through it**, Troy Sobotka's
11
+ * construction as Blender, Filament and three.js ship it. The same 1% leaves at 18 of 255, middle
12
+ * grey lands at half the display range, and a highlight desaturates toward white instead of
13
+ * clipping to a hue. What it gives up is ACES's punch in the midtones: the same scene reads
14
+ * flatter, which a grade adds back as a look where ACES bakes it into the conversion. What would
15
+ * make it the wrong choice is a world graded against three.js's or Unreal's ACES, which is the
16
+ * match `aces` exists for.
17
+ *
18
+ * The constants are the Filament and three.js ones: Rec. 2020 primaries, the inset and outset
19
+ * matrices, an encoding from 2^-10 to 2^6.5 times middle grey, and the iolite polynomial fit of
20
+ * the default contrast curve, whose mean squared error against Blender's is 3.7e-6.
21
+ */
22
+ /**
23
+ * `agxFilmic(vec3)`: linear sRGB in, linear sRGB out, clamped to [0, 1]. Reads `uOutputExposure`,
24
+ * which the including source declares.
25
+ */
26
+ export declare const AGX_GLSL = "\nvec3 agxContrast(vec3 x) {\n vec3 x2 = x * x;\n vec3 x4 = x2 * x2;\n return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;\n}\n\nvec3 agxFilmic(vec3 x) {\n x *= uOutputExposure;\n // Columns, as GLSL reads them: linear sRGB to linear Rec. 2020, then the inset.\n const mat3 SRGB_TO_REC2020 = mat3(\n 0.6274, 0.0691, 0.0164,\n 0.3293, 0.9195, 0.0880,\n 0.0433, 0.0113, 0.8956\n );\n const mat3 AGX_INSET = mat3(\n 0.856627153315983, 0.137318972929847, 0.11189821299995,\n 0.0951212405381588, 0.761241990602591, 0.0767994186031903,\n 0.0482516061458583, 0.101439036467562, 0.811302368396859\n );\n const mat3 AGX_OUTSET = mat3(\n 1.1271005818144368, -0.1413297634984383, -0.14132976349843826,\n -0.11060664309660323, 1.157823702216272, -0.11060664309660294,\n -0.016493938717834573, -0.016493938717834257, 1.2519364065950405\n );\n const mat3 REC2020_TO_SRGB = mat3(\n 1.6605, -0.1246, -0.0182,\n -0.5876, 1.1329, -0.1006,\n -0.0728, -0.0083, 1.1187\n );\n vec3 c = AGX_INSET * (SRGB_TO_REC2020 * x);\n /* log2 of middle grey times 2^-10 and 2^6.5: -12.47393 and 4.026069, 16.5 stops apart. */\n c = clamp((log2(max(c, vec3(1e-10))) + 12.47393) / 16.5, 0.0, 1.0);\n c = AGX_OUTSET * agxContrast(c);\n c = pow(max(c, vec3(0.0)), vec3(2.2));\n return clamp(REC2020_TO_SRGB * c, 0.0, 1.0);\n}\n";
27
+ /**
28
+ * The curve for a neutral grey, as the shader computes it: every matrix above keeps grey grey, so
29
+ * one channel is the whole of it. For a test to pin to hand-derived values.
30
+ */
31
+ export declare function agxGrey(linear: number, exposure?: number): number;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * AgX: a tone curve whose shade stays open, for scenes lit mostly by bounce.
3
+ *
4
+ * **What ACES does to a courtyard.** The reference RRT and ODT are graded for a dark cinema, and
5
+ * their toe is steep: a surface at 1% of white leaves the curve at 4 of 255. A room lit by light
6
+ * bounced off its own walls lives in exactly that range, so its arcades read black beside renders
7
+ * of the same model, and the grime in its stone reads as ink. Measured on a bought courtyard: 39%
8
+ * of the frame below 16 of 255, against about 5% in the model maker's own renders.
9
+ *
10
+ * **AgX encodes the scene logarithmically and draws one sigmoid through it**, Troy Sobotka's
11
+ * construction as Blender, Filament and three.js ship it. The same 1% leaves at 18 of 255, middle
12
+ * grey lands at half the display range, and a highlight desaturates toward white instead of
13
+ * clipping to a hue. What it gives up is ACES's punch in the midtones: the same scene reads
14
+ * flatter, which a grade adds back as a look where ACES bakes it into the conversion. What would
15
+ * make it the wrong choice is a world graded against three.js's or Unreal's ACES, which is the
16
+ * match `aces` exists for.
17
+ *
18
+ * The constants are the Filament and three.js ones: Rec. 2020 primaries, the inset and outset
19
+ * matrices, an encoding from 2^-10 to 2^6.5 times middle grey, and the iolite polynomial fit of
20
+ * the default contrast curve, whose mean squared error against Blender's is 3.7e-6.
21
+ */
22
+ /**
23
+ * `agxFilmic(vec3)`: linear sRGB in, linear sRGB out, clamped to [0, 1]. Reads `uOutputExposure`,
24
+ * which the including source declares.
25
+ */
26
+ export const AGX_GLSL = `
27
+ vec3 agxContrast(vec3 x) {
28
+ vec3 x2 = x * x;
29
+ vec3 x4 = x2 * x2;
30
+ return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
31
+ }
32
+
33
+ vec3 agxFilmic(vec3 x) {
34
+ x *= uOutputExposure;
35
+ // Columns, as GLSL reads them: linear sRGB to linear Rec. 2020, then the inset.
36
+ const mat3 SRGB_TO_REC2020 = mat3(
37
+ 0.6274, 0.0691, 0.0164,
38
+ 0.3293, 0.9195, 0.0880,
39
+ 0.0433, 0.0113, 0.8956
40
+ );
41
+ const mat3 AGX_INSET = mat3(
42
+ 0.856627153315983, 0.137318972929847, 0.11189821299995,
43
+ 0.0951212405381588, 0.761241990602591, 0.0767994186031903,
44
+ 0.0482516061458583, 0.101439036467562, 0.811302368396859
45
+ );
46
+ const mat3 AGX_OUTSET = mat3(
47
+ 1.1271005818144368, -0.1413297634984383, -0.14132976349843826,
48
+ -0.11060664309660323, 1.157823702216272, -0.11060664309660294,
49
+ -0.016493938717834573, -0.016493938717834257, 1.2519364065950405
50
+ );
51
+ const mat3 REC2020_TO_SRGB = mat3(
52
+ 1.6605, -0.1246, -0.0182,
53
+ -0.5876, 1.1329, -0.1006,
54
+ -0.0728, -0.0083, 1.1187
55
+ );
56
+ vec3 c = AGX_INSET * (SRGB_TO_REC2020 * x);
57
+ /* log2 of middle grey times 2^-10 and 2^6.5: -12.47393 and 4.026069, 16.5 stops apart. */
58
+ c = clamp((log2(max(c, vec3(1e-10))) + 12.47393) / 16.5, 0.0, 1.0);
59
+ c = AGX_OUTSET * agxContrast(c);
60
+ c = pow(max(c, vec3(0.0)), vec3(2.2));
61
+ return clamp(REC2020_TO_SRGB * c, 0.0, 1.0);
62
+ }
63
+ `;
64
+ /**
65
+ * The curve for a neutral grey, as the shader computes it: every matrix above keeps grey grey, so
66
+ * one channel is the whole of it. For a test to pin to hand-derived values.
67
+ */
68
+ export function agxGrey(linear, exposure = 1) {
69
+ const x = Math.max(linear * exposure, 1e-10);
70
+ const t = Math.min(Math.max((Math.log2(x) + 12.47393) / 16.5, 0), 1);
71
+ const t2 = t * t;
72
+ const t4 = t2 * t2;
73
+ const s = 15.5 * t4 * t2 -
74
+ 40.14 * t4 * t +
75
+ 31.96 * t4 -
76
+ 6.868 * t2 * t +
77
+ 0.4298 * t2 +
78
+ 0.1191 * t -
79
+ 0.00232;
80
+ return Math.min(Math.max(Math.max(s, 0) ** 2.2, 0), 1);
81
+ }
@@ -26,11 +26,20 @@
26
26
  /**
27
27
  * The estimate. Writes one channel: 1 is open sky, 0 is fully enclosed.
28
28
  *
29
- * The estimator is the Alchemy/scalable-AO one — for each neighbour, the vector v from this
30
- * surface to it, weighted max(0, v·n) / (v·v). Two properties earn it its place over a
31
- * hemisphere of offset points: nothing has to be oriented, so there is no tangent frame and
32
- * no rotation matrix per pixel, and the 1 / v·v term falls away on its own, so a neighbour
33
- * further off contributes less without a second range test doing it by hand.
29
+ * **Horizons, not neighbours: ground-truth ambient occlusion (Jimenez et al. 2016).** Each pixel
30
+ * walks two lines across the screen, both ways, and keeps the highest thing it can see along each
31
+ * half: the horizon. The sky left between the two horizons of a line, weighted by the cosine to the
32
+ * surface's own normal, is that line's visibility in closed form, and two lines a pixel with the
33
+ * rotation tile below turning them is the hemisphere. It replaced a sum over twelve neighbours that
34
+ * counted anything within the radius as a blocker, however thin: cloth hanging a hand in front of a
35
+ * column darkened the column behind it, which read as a halo round every curtain in a courtyard.
36
+ *
37
+ * **What stops the halo is two rules about the horizon**, both the paper's. A sample fades out of
38
+ * the horizon as it approaches the radius, so a far wall does not shade a floor; and once the walk
39
+ * has passed the highest sample, the horizon sinks back toward what it sees next, so a thin thing
40
+ * in front raises it only while the walk is behind that thing. What that gives up is the shade under
41
+ * a thick object seen edge on, which reads as a thin one; the thickness is a constant because depth
42
+ * alone cannot say how deep anything is.
34
43
  */
35
44
  export declare const AO_FRAG: string;
36
45
  /**
@@ -43,3 +52,13 @@ export declare const AO_FRAG: string;
43
52
  * added to remove.
44
53
  */
45
54
  export declare const AO_BLUR_FRAG: string;
55
+ /**
56
+ * The estimate's closed form, written out once more so a test can pin it to hand-derived values:
57
+ * the cosine-weighted sky between horizon angles `h0 < 0 < h1` for a normal at `normal`, all in
58
+ * radians from the eye within one line's plane.
59
+ */
60
+ export declare function gtaoArc(h0: number, h1: number, normal: number): number;
61
+ /** One step of a horizon: up to a higher sample, or a fifth of the way down toward a lower one. */
62
+ export declare function nextHorizon(horizon: number, cosine: number): number;
63
+ /** The shader's thickness heuristic. See `AO_THIN` inside `AO_FRAG`. */
64
+ export declare const AO_THIN = 0.2;