@driftengine/core 4.3.0 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CHANGELOG.json +282 -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/ambientOcclusionPass.js +5 -1
  12. package/dist/render/backend/createRenderer.d.ts +13 -1
  13. package/dist/render/backend/createRenderer.js +6 -5
  14. package/dist/render/backend/webgl2/renderer.d.ts +147 -1
  15. package/dist/render/backend/webgl2/renderer.js +402 -23
  16. package/dist/render/backend/webgpu/buffers.js +46 -8
  17. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  18. package/dist/render/backend/webgpu/depthPass.js +75 -14
  19. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  20. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  21. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  22. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  23. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  24. package/dist/render/backend/webgpu/flatPass.js +12 -5
  25. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  26. package/dist/render/backend/webgpu/particlePass.js +1 -0
  27. package/dist/render/backend/webgpu/postPass.d.ts +13 -3
  28. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  29. package/dist/render/backend/webgpu/probeBake.js +97 -7
  30. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  31. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  32. package/dist/render/backend/webgpu/renderer.d.ts +174 -11
  33. package/dist/render/backend/webgpu/renderer.js +682 -252
  34. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  35. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  36. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  37. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  38. package/dist/render/backend/webgpu/skyPass.js +5 -1
  39. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  40. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  41. package/dist/render/bloomChain.d.ts +6 -0
  42. package/dist/render/bloomChain.js +8 -0
  43. package/dist/render/clusteredLights.d.ts +19 -11
  44. package/dist/render/clusteredLights.js +96 -45
  45. package/dist/render/cutoutCaster.d.ts +23 -0
  46. package/dist/render/cutoutCaster.js +20 -0
  47. package/dist/render/driftLight/bake.d.ts +34 -0
  48. package/dist/render/driftLight/bake.js +95 -0
  49. package/dist/render/driftLight/falloff.d.ts +16 -0
  50. package/dist/render/driftLight/falloff.js +23 -0
  51. package/dist/render/driftLight/layout.d.ts +59 -0
  52. package/dist/render/driftLight/layout.js +138 -0
  53. package/dist/render/driftLight/lightField.d.ts +108 -0
  54. package/dist/render/driftLight/lightField.js +179 -0
  55. package/dist/render/driftLight/uniforms.d.ts +28 -0
  56. package/dist/render/driftLight/uniforms.js +41 -0
  57. package/dist/render/driftLight/visibility.d.ts +18 -0
  58. package/dist/render/driftLight/visibility.js +47 -0
  59. package/dist/render/emptyTexture.d.ts +6 -0
  60. package/dist/render/emptyTexture.js +19 -0
  61. package/dist/render/exposurePass.d.ts +48 -0
  62. package/dist/render/exposurePass.js +172 -0
  63. package/dist/render/flameLight.d.ts +71 -0
  64. package/dist/render/flameLight.js +63 -0
  65. package/dist/render/gi/bounceLights.d.ts +34 -0
  66. package/dist/render/gi/bounceLights.js +66 -0
  67. package/dist/render/gi/fieldReach.d.ts +12 -0
  68. package/dist/render/gi/fieldReach.js +62 -0
  69. package/dist/render/gi/globalField.d.ts +8 -0
  70. package/dist/render/gi/globalField.js +31 -0
  71. package/dist/render/gi/probeTrace.d.ts +3 -1
  72. package/dist/render/gi/probeTrace.js +3 -1
  73. package/dist/render/gpuCapability.js +19 -4
  74. package/dist/render/lightBudget.d.ts +11 -0
  75. package/dist/render/lightBudget.js +11 -0
  76. package/dist/render/pointLightSelection.d.ts +16 -0
  77. package/dist/render/pointLightSelection.js +178 -83
  78. package/dist/render/pointShadowBudget.d.ts +12 -2
  79. package/dist/render/pointShadowBudget.js +16 -10
  80. package/dist/render/probeGrid.d.ts +11 -1
  81. package/dist/render/probeGrid.js +5 -2
  82. package/dist/render/probeSweeps.d.ts +45 -0
  83. package/dist/render/probeSweeps.js +77 -0
  84. package/dist/render/reflectionProbe.d.ts +43 -1
  85. package/dist/render/reflectionProbe.js +17 -3
  86. package/dist/render/renderQuality.d.ts +3 -1
  87. package/dist/render/rendererHarness.d.ts +9 -0
  88. package/dist/render/rendererHarness.js +18 -3
  89. package/dist/render/sceneTarget.d.ts +20 -0
  90. package/dist/render/sceneTarget.js +62 -1
  91. package/dist/render/shaders/ambientOcclusion.d.ts +63 -6
  92. package/dist/render/shaders/ambientOcclusion.js +188 -71
  93. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  94. package/dist/render/shaders/cutoutCoverage.js +40 -0
  95. package/dist/render/shaders/depth.d.ts +25 -0
  96. package/dist/render/shaders/depth.js +69 -3
  97. package/dist/render/shaders/exposure.d.ts +52 -0
  98. package/dist/render/shaders/exposure.js +124 -0
  99. package/dist/render/shaders/filmLook.d.ts +47 -0
  100. package/dist/render/shaders/filmLook.js +98 -0
  101. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  102. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  103. package/dist/render/shaders/flat/driftLight.js +75 -0
  104. package/dist/render/shaders/flat/index.js +5 -0
  105. package/dist/render/shaders/flat/lobes.js +13 -0
  106. package/dist/render/shaders/flat/main.d.ts +1 -1
  107. package/dist/render/shaders/flat/main.js +68 -9
  108. package/dist/render/shaders/flat/preamble.js +23 -5
  109. package/dist/render/shaders/flat/probeGrid.js +46 -3
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
  111. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
  112. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  113. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  115. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  116. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  117. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  118. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  119. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  120. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  121. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  124. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  125. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  126. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  127. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  128. package/dist/render/shaders/globalMedium.js +4 -0
  129. package/dist/render/shaders/localExposure.d.ts +35 -0
  130. package/dist/render/shaders/localExposure.js +138 -0
  131. package/dist/render/shaders/prefilter.d.ts +1 -1
  132. package/dist/render/shaders/prefilter.js +6 -0
  133. package/dist/render/shaders/rush.js +33 -5
  134. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  135. package/dist/render/shaders/tangentFrame.js +15 -3
  136. package/dist/render/shaders/temporalResolve.js +39 -1
  137. package/dist/render/shadowCasters.d.ts +5 -3
  138. package/dist/render/shadowMap.js +13 -0
  139. package/dist/render/surfaceTexture.d.ts +5 -0
  140. package/package.json +6 -6
  141. package/scripts/gpuCompute.mjs +24 -3
  142. package/scripts/png.mjs +23 -12
  143. package/src/cinematic/cameraPath.ts +178 -0
  144. package/src/environment/daylight.ts +152 -0
  145. package/src/geometry/meshConcat.ts +100 -0
  146. package/src/index.ts +32 -0
  147. package/src/render/ambientOcclusionPass.ts +5 -1
  148. package/src/render/backend/createRenderer.ts +22 -6
  149. package/src/render/backend/webgl2/renderer.ts +439 -20
  150. package/src/render/backend/webgpu/buffers.ts +51 -8
  151. package/src/render/backend/webgpu/depthPass.ts +78 -10
  152. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  153. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  154. package/src/render/backend/webgpu/flatPass.ts +10 -2
  155. package/src/render/backend/webgpu/particlePass.ts +9 -0
  156. package/src/render/backend/webgpu/probeBake.ts +143 -9
  157. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  158. package/src/render/backend/webgpu/renderer.ts +808 -273
  159. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  160. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  161. package/src/render/backend/webgpu/skyPass.ts +5 -1
  162. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  163. package/src/render/bloomChain.ts +9 -0
  164. package/src/render/clusteredLights.ts +108 -63
  165. package/src/render/cutoutCaster.ts +44 -0
  166. package/src/render/driftLight/bake.ts +125 -0
  167. package/src/render/driftLight/falloff.ts +26 -0
  168. package/src/render/driftLight/layout.ts +177 -0
  169. package/src/render/driftLight/lightField.ts +227 -0
  170. package/src/render/driftLight/uniforms.ts +61 -0
  171. package/src/render/driftLight/visibility.ts +58 -0
  172. package/src/render/emptyTexture.ts +41 -0
  173. package/src/render/exposurePass.ts +186 -0
  174. package/src/render/flameLight.ts +135 -0
  175. package/src/render/gi/bounceLights.ts +68 -0
  176. package/src/render/gi/fieldReach.ts +74 -0
  177. package/src/render/gi/globalField.ts +39 -0
  178. package/src/render/gi/probeTrace.ts +3 -1
  179. package/src/render/gpuCapability.ts +18 -5
  180. package/src/render/lightBudget.ts +12 -0
  181. package/src/render/pointLightSelection.ts +191 -79
  182. package/src/render/pointShadowBudget.ts +16 -8
  183. package/src/render/probeGrid.ts +13 -2
  184. package/src/render/probeSweeps.ts +80 -0
  185. package/src/render/reflectionProbe.ts +54 -2
  186. package/src/render/renderQuality.ts +3 -1
  187. package/src/render/rendererHarness.ts +25 -3
  188. package/src/render/sceneTarget.ts +75 -1
  189. package/src/render/shaders/ambientOcclusion.ts +205 -71
  190. package/src/render/shaders/cutoutCoverage.ts +43 -0
  191. package/src/render/shaders/depth.ts +80 -3
  192. package/src/render/shaders/exposure.ts +128 -0
  193. package/src/render/shaders/filmLook.ts +111 -0
  194. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  195. package/src/render/shaders/flat/driftLight.ts +75 -0
  196. package/src/render/shaders/flat/index.ts +5 -0
  197. package/src/render/shaders/flat/lobes.ts +13 -0
  198. package/src/render/shaders/flat/main.ts +68 -9
  199. package/src/render/shaders/flat/preamble.ts +23 -5
  200. package/src/render/shaders/flat/probeGrid.ts +46 -3
  201. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
  202. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  203. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  204. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  205. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  206. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  207. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  208. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  209. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  210. package/src/render/shaders/globalMedium.ts +4 -0
  211. package/src/render/shaders/localExposure.ts +154 -0
  212. package/src/render/shaders/prefilter.ts +6 -0
  213. package/src/render/shaders/rush.ts +33 -5
  214. package/src/render/shaders/tangentFrame.ts +15 -3
  215. package/src/render/shaders/temporalResolve.ts +39 -1
  216. package/src/render/shadowCasters.ts +5 -3
  217. package/src/render/shadowMap.ts +14 -0
  218. package/src/render/surfaceTexture.ts +5 -0
@@ -46,14 +46,16 @@ export function sameGrid(a, b) {
46
46
  if (a.spacing[axis] !== b.spacing[axis])
47
47
  return false;
48
48
  }
49
- return true;
49
+ return a.crossfade === b.crossfade;
50
50
  }
51
51
  export class ProbeGrid {
52
52
  origin;
53
53
  spacing;
54
54
  counts;
55
- /** Probes in total, which is also the array's layer count. */
55
+ /** Probes in total: the array's layer count, or a third of it for a crossfading grid. */
56
56
  layers;
57
+ /** Whether sweeps blend into one another. See `ProbeGridOptions.crossfade`. */
58
+ crossfade;
57
59
  /** The reciprocal of the spacing, because the shader multiplies and so does everything here. */
58
60
  invSpacing;
59
61
  constructor(options) {
@@ -91,6 +93,7 @@ export class ProbeGrid {
91
93
  this.invSpacing = [1 / this.spacing[0], 1 / this.spacing[1], 1 / this.spacing[2]];
92
94
  this.counts = counts;
93
95
  this.layers = layers;
96
+ this.crossfade = options.crossfade === true;
94
97
  }
95
98
  /**
96
99
  * The layer a probe at whole lattice coordinates occupies.
@@ -0,0 +1,45 @@
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 declare class ProbeSweeps {
25
+ /** Sets of layers the array holds: three for a crossfading grid, one otherwise. */
26
+ readonly sets: 1 | 3;
27
+ private readonly layers;
28
+ /** Which probes the sweep being baked has reached, and how many. */
29
+ private readonly reached;
30
+ private reachedCount;
31
+ private from;
32
+ private to;
33
+ private write;
34
+ /** Whether a whole sweep has been rotated in. */
35
+ private whole;
36
+ constructor(layers: number, crossfade: boolean);
37
+ /** The array layer a bake of probe `layer` writes. */
38
+ writeLayer(layer: number): number;
39
+ /** Record that probe `layer` was baked into the set being written; a whole sweep rotates. */
40
+ baked(layer: number): void;
41
+ /** Whether the shading may read the grid: once a sweep is whole. */
42
+ get ready(): boolean;
43
+ /** The layer offsets of the two sets shown and the blend between them, into `out`. */
44
+ uniforms(out: Float32Array): Float32Array;
45
+ }
@@ -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;
@@ -24,13 +24,23 @@
24
24
  * wherever the depth test cuts the window short, as a dotted line along every edge.
25
25
  */
26
26
  /**
27
- * The estimate. Writes one channel: 1 is open sky, 0 is fully enclosed.
27
+ * The estimate. Writes one channel at `AO_STORE`: that is open sky, 0 is fully enclosed, and the
28
+ * second axis of the blur decodes it.
28
29
  *
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.
30
+ * **Horizons, not neighbours: ground-truth ambient occlusion (Jimenez et al. 2016).** Each pixel
31
+ * walks two lines across the screen, both ways, and keeps the highest thing it can see along each
32
+ * half: the horizon. The sky left between the two horizons of a line, weighted by the cosine to the
33
+ * surface's own normal, is that line's visibility in closed form, and two lines a pixel with the
34
+ * rotation tile below turning them is the hemisphere. It replaced a sum over twelve neighbours that
35
+ * counted anything within the radius as a blocker, however thin: cloth hanging a hand in front of a
36
+ * column darkened the column behind it, which read as a halo round every curtain in a courtyard.
37
+ *
38
+ * **What stops the halo is two rules about the horizon**, both the paper's. A sample fades out of
39
+ * the horizon as it approaches the radius, so a far wall does not shade a floor; and once the walk
40
+ * has passed the highest sample, the horizon sinks back toward what it sees next, so a thin thing
41
+ * in front raises it only while the walk is behind that thing. What that gives up is the shade under
42
+ * a thick object seen edge on, which reads as a thin one; the thickness is a constant because depth
43
+ * alone cannot say how deep anything is.
34
44
  */
35
45
  export declare const AO_FRAG: string;
36
46
  /**
@@ -43,3 +53,50 @@ export declare const AO_FRAG: string;
43
53
  * added to remove.
44
54
  */
45
55
  export declare const AO_BLUR_FRAG: string;
56
+ /**
57
+ * The estimate's closed form, written out once more so a test can pin it to hand-derived values:
58
+ * the cosine-weighted sky between horizon angles `h0 < 0 < h1` for a normal at `normal`, all in
59
+ * radians from the eye within one line's plane.
60
+ */
61
+ export declare function gtaoArc(h0: number, h1: number, normal: number): number;
62
+ /** One step of a horizon: up to a higher sample, or a fifth of the way down toward a lower one. */
63
+ export declare function nextHorizon(horizon: number, cosine: number): number;
64
+ /** The shader's thickness heuristic. See `AO_THIN` inside `AO_FRAG`. */
65
+ export declare const AO_THIN = 0.2;
66
+ /**
67
+ * What the estimate is multiplied by before it is stored, and divided by once the blur is done.
68
+ *
69
+ * **Half, because an unblurred estimate is allowed past one.** A pixel's two lines see a plane
70
+ * turned away from the eye from two directions, and the closed form credits one of them more than
71
+ * the whole sky and the other less: it is the average over every turn that is exactly one, and the
72
+ * average is what the blur is for. Stored as it came, an eight-bit target clipped every turn above
73
+ * one before the blur could average it, and every surface seen edge on came back a few percent
74
+ * shaded with nothing on it. A line reaches π/2 at most, so half fits the whole range.
75
+ *
76
+ * What it gives up is one bit of the intermediate: steps of 1/128 rather than 1/256 until the
77
+ * second axis decodes, which the eight taps a pixel averages carry below what a frame can show.
78
+ */
79
+ export declare const AO_STORE = 0.5;
80
+ /** How far a neighbour may sit from this pixel's surface, as a fraction of its distance. */
81
+ export declare const AO_BLUR_DEPTH_TOLERANCE = 0.02;
82
+ /**
83
+ * The blur's weight for a neighbour `offset` pixels along the axis, from view depths: this pixel's
84
+ * own, the two either side of it, and the neighbour's. The shader's lines written out again.
85
+ *
86
+ * **The neighbour is compared with this pixel's plane, not with this pixel's depth.** Comparing
87
+ * depths refused every neighbour of a surface seen edge on: a floor forty metres out changes its
88
+ * depth by four percent a row, twice the tolerance, so the pass down the frame kept the centre alone
89
+ * and left the rotation tile's rows standing — four-pixel stripes across every ramp and far floor,
90
+ * reported as strips on a track. On a plane 1/z is affine in the screen, so one step of it predicts
91
+ * the plane's depth at every tap exactly, and a tap is refused only for leaving the plane.
92
+ *
93
+ * The step is taken from whichever neighbour is closer to the centre in 1/z. At a silhouette that is
94
+ * the one on this pixel's own side, so the far surface cannot bend the prediction toward it; on a
95
+ * plane the two agree to a quantisation step, which moves a weight by nothing a frame shows. What it
96
+ * gives up is a crease: past the fold between a floor and a wall the prediction runs on along the
97
+ * floor, so the wall's taps fade out over the tolerance rather than being kept.
98
+ *
99
+ * **Written here rather than in the shader**, because a comment inside the GLSL is part of the string
100
+ * a consumer ships.
101
+ */
102
+ export declare function blurWeight(centre: number, before: number, after: number, neighbour: number, offset: number): number;