@driftengine/core 4.5.0 → 4.6.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 (207) hide show
  1. package/CHANGELOG.json +51 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +23 -2
  23. package/dist/index.js +13 -1
  24. package/dist/render/backend/api.d.ts +17 -3
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  30. package/dist/render/backend/webgl2/instanced.js +72 -15
  31. package/dist/render/backend/webgl2/renderer.d.ts +122 -8
  32. package/dist/render/backend/webgl2/renderer.js +238 -121
  33. package/dist/render/backend/webgpu/buffers.d.ts +10 -0
  34. package/dist/render/backend/webgpu/buffers.js +34 -3
  35. package/dist/render/backend/webgpu/depthPass.js +5 -1
  36. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  37. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  38. package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
  39. package/dist/render/backend/webgpu/flatPass.js +38 -9
  40. package/dist/render/backend/webgpu/glassTintPass.js +2 -1
  41. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  42. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  43. package/dist/render/backend/webgpu/instanced.d.ts +11 -1
  44. package/dist/render/backend/webgpu/instanced.js +21 -3
  45. package/dist/render/backend/webgpu/latePass.d.ts +6 -2
  46. package/dist/render/backend/webgpu/latePass.js +11 -4
  47. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  48. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  49. package/dist/render/backend/webgpu/renderer.js +462 -201
  50. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  51. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  53. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  54. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  55. package/dist/render/driftLight/bake.d.ts +7 -0
  56. package/dist/render/driftLight/bake.js +61 -56
  57. package/dist/render/driftLight/denseField.d.ts +46 -0
  58. package/dist/render/driftLight/denseField.js +109 -0
  59. package/dist/render/driftLight/lightField.d.ts +10 -5
  60. package/dist/render/driftLight/lightField.js +25 -19
  61. package/dist/render/driftLight/presence.d.ts +50 -0
  62. package/dist/render/driftLight/presence.js +43 -0
  63. package/dist/render/driftLight/uniforms.d.ts +3 -3
  64. package/dist/render/driftLight/uniforms.js +5 -6
  65. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  66. package/dist/render/driftLight/worldLightField.js +66 -0
  67. package/dist/render/fixtureAtlas.d.ts +20 -0
  68. package/dist/render/fixtureAtlas.js +49 -0
  69. package/dist/render/instanceCull.d.ts +63 -0
  70. package/dist/render/instanceCull.js +160 -0
  71. package/dist/render/instances.d.ts +15 -0
  72. package/dist/render/lightBudget.d.ts +22 -33
  73. package/dist/render/lightBudget.js +24 -35
  74. package/dist/render/lightGrid.d.ts +42 -0
  75. package/dist/render/lightGrid.js +87 -0
  76. package/dist/render/mesh.d.ts +18 -24
  77. package/dist/render/mesh.js +51 -68
  78. package/dist/render/occlusion.d.ts +10 -0
  79. package/dist/render/occlusion.js +36 -7
  80. package/dist/render/pointLightSelection.d.ts +7 -0
  81. package/dist/render/pointLightSelection.js +29 -4
  82. package/dist/render/rainField.d.ts +13 -0
  83. package/dist/render/rainField.js +27 -5
  84. package/dist/render/recon/lateDepth.d.ts +11 -0
  85. package/dist/render/recon/lateDepth.js +70 -0
  86. package/dist/render/recon/resolve.d.ts +34 -4
  87. package/dist/render/recon/resolve.js +161 -20
  88. package/dist/render/rendererHarness.js +4 -0
  89. package/dist/render/sceneTarget.d.ts +2 -0
  90. package/dist/render/sceneTarget.js +2 -0
  91. package/dist/render/shaders/depth.js +12 -9
  92. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  93. package/dist/render/shaders/flat/driftLight.js +32 -7
  94. package/dist/render/shaders/flat/index.js +5 -3
  95. package/dist/render/shaders/flat/main.d.ts +1 -1
  96. package/dist/render/shaders/flat/main.js +75 -23
  97. package/dist/render/shaders/flat/preamble.js +75 -20
  98. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  99. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  100. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  101. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  102. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  103. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  104. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  105. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  106. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  107. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  108. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  109. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  110. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  111. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  113. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  114. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  115. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  116. package/dist/render/shaders/rush.js +11 -1
  117. package/dist/render/shaders/sdfText.d.ts +1 -1
  118. package/dist/render/shaders/sdfText.js +3 -2
  119. package/dist/render/shaders/text.d.ts +1 -1
  120. package/dist/render/shaders/text.js +18 -9
  121. package/dist/render/surfaceEffects.d.ts +83 -0
  122. package/dist/render/surfaceEffects.js +75 -0
  123. package/dist/render/surfaceScene.d.ts +8 -0
  124. package/dist/render/surfaceScene.js +8 -0
  125. package/dist/render/surfaceTexture.d.ts +18 -2
  126. package/dist/render/surfaceTexture.js +68 -15
  127. package/dist/render/temporalAa.d.ts +25 -6
  128. package/dist/render/temporalAa.js +45 -15
  129. package/dist/render/textureSource.d.ts +30 -0
  130. package/dist/render/textureSource.js +53 -0
  131. package/dist/render/vertexDefaults.js +2 -0
  132. package/dist/world/hlod.d.ts +104 -0
  133. package/dist/world/hlod.js +214 -0
  134. package/package.json +6 -6
  135. package/src/geometry/boxCsg.ts +186 -0
  136. package/src/geometry/csg.ts +384 -0
  137. package/src/geometry/meshBuilder.ts +15 -0
  138. package/src/geometry/meshConcat.ts +1 -0
  139. package/src/geometry/pixelFont.ts +2 -0
  140. package/src/geometry/solid.ts +189 -0
  141. package/src/geometry/solidFlat.ts +522 -0
  142. package/src/geometry/solidHarness.ts +83 -0
  143. package/src/geometry/solidNormals.ts +103 -0
  144. package/src/geometry/solidRound.ts +443 -0
  145. package/src/geometry/solidWriter.ts +58 -0
  146. package/src/index.ts +51 -1
  147. package/src/render/backend/api.ts +21 -2
  148. package/src/render/backend/drawFog.ts +31 -0
  149. package/src/render/backend/materialChanges.ts +9 -2
  150. package/src/render/backend/webgl2/instanced.ts +80 -14
  151. package/src/render/backend/webgl2/renderer.ts +300 -143
  152. package/src/render/backend/webgpu/buffers.ts +42 -3
  153. package/src/render/backend/webgpu/depthPass.ts +5 -1
  154. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  155. package/src/render/backend/webgpu/flatPass.ts +41 -11
  156. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  157. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  158. package/src/render/backend/webgpu/instanced.ts +21 -3
  159. package/src/render/backend/webgpu/latePass.ts +12 -3
  160. package/src/render/backend/webgpu/renderer.ts +540 -226
  161. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  162. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  163. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  164. package/src/render/driftLight/bake.ts +85 -62
  165. package/src/render/driftLight/denseField.ts +149 -0
  166. package/src/render/driftLight/lightField.ts +33 -18
  167. package/src/render/driftLight/presence.ts +69 -0
  168. package/src/render/driftLight/uniforms.ts +8 -9
  169. package/src/render/driftLight/worldLightField.ts +83 -0
  170. package/src/render/fixtureAtlas.ts +63 -0
  171. package/src/render/instanceCull.ts +205 -0
  172. package/src/render/instances.ts +16 -0
  173. package/src/render/lightBudget.ts +25 -36
  174. package/src/render/lightGrid.ts +130 -0
  175. package/src/render/mesh.ts +59 -72
  176. package/src/render/occlusion.ts +51 -7
  177. package/src/render/pointLightSelection.ts +72 -4
  178. package/src/render/rainField.ts +39 -5
  179. package/src/render/recon/lateDepth.ts +83 -0
  180. package/src/render/recon/resolve.ts +206 -20
  181. package/src/render/rendererHarness.ts +4 -0
  182. package/src/render/sceneTarget.ts +4 -0
  183. package/src/render/shaders/depth.ts +12 -9
  184. package/src/render/shaders/flat/driftLight.ts +32 -7
  185. package/src/render/shaders/flat/index.ts +5 -3
  186. package/src/render/shaders/flat/main.ts +75 -23
  187. package/src/render/shaders/flat/preamble.ts +75 -20
  188. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  189. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  190. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  191. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  192. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  193. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  194. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  195. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  196. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  197. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  198. package/src/render/shaders/rush.ts +11 -1
  199. package/src/render/shaders/sdfText.ts +3 -2
  200. package/src/render/shaders/text.ts +18 -9
  201. package/src/render/surfaceEffects.ts +132 -0
  202. package/src/render/surfaceScene.ts +16 -0
  203. package/src/render/surfaceTexture.ts +110 -19
  204. package/src/render/temporalAa.ts +78 -14
  205. package/src/render/textureSource.ts +72 -0
  206. package/src/render/vertexDefaults.ts +2 -0
  207. package/src/world/hlod.ts +290 -0
@@ -53,6 +53,17 @@ export interface RainFieldOptions {
53
53
  streakSec?: number;
54
54
  /** The ground drops land on, and the roofs they do not fall through. */
55
55
  ground?: GroundSurface;
56
+ /**
57
+ * How near the viewer a streak may pass and still be drawn, metres. Default 1.
58
+ *
59
+ * **A drop that close is out of any focus a camera holds**, and drawn sharp it was a bar across
60
+ * the frame: a streak is a world-space width, so a 6 mm drop 30 cm from the eye drew fourteen
61
+ * pixels thick and as long as the frame, from a city seen from the air in the rain. A streak
62
+ * whose nearest point is inside this distance is not drawn; the drop keeps falling, as a
63
+ * sheltered one does. What it gives up is rain on a lens or a visor, which this field never
64
+ * drew as such anyway. What would make it wrong is a view that wants drops in macro: pass 0.
65
+ */
66
+ clearM?: number;
56
67
  }
57
68
 
58
69
  export class RainField {
@@ -97,6 +108,8 @@ export class RainField {
97
108
  */
98
109
  streakSec: number;
99
110
  private readonly ground: GroundSurface | null;
111
+ /** How near the viewer a streak may pass and still be drawn, squared. */
112
+ private readonly clear2: number;
100
113
  /** Drops not drawn this update because something was over them. */
101
114
  private sheltered = 0;
102
115
  /**
@@ -125,6 +138,8 @@ export class RainField {
125
138
  this.speedMps = options.speedMps ?? 9;
126
139
  this.streakSec = options.streakSec ?? 0.04;
127
140
  this.ground = options.ground ?? null;
141
+ const clear = options.clearM ?? 1;
142
+ this.clear2 = clear * clear;
128
143
  this.segments = createLineSegments(options.count);
129
144
  this.x = new Float32Array(options.count);
130
145
  this.y = new Float32Array(options.count);
@@ -212,15 +227,34 @@ export class RainField {
212
227
  continue;
213
228
  }
214
229
 
230
+ /* Where it was a shutter-time ago, which is the streak: down by its fall and back along the
231
+ wind, so a gust leans every drop at the same angle without anything trigonometric. */
232
+ const backX = -windX * this.streakSec;
233
+ const backY = this.speedMps * this.streakSec;
234
+ const backZ = -windZ * this.streakSec;
235
+ /* Too near the eye to be drawn (`clearM`): the streak's nearest point to the viewer. */
236
+ if (this.clear2 > 0) {
237
+ const ox = dropX - viewerX;
238
+ const oy = dropY - viewerY;
239
+ const oz = dropZ - viewerZ;
240
+ const length2 = backX * backX + backY * backY + backZ * backZ;
241
+ const along =
242
+ length2 > 0
243
+ ? Math.min(Math.max(-(ox * backX + oy * backY + oz * backZ) / length2, 0), 1)
244
+ : 0;
245
+ const nx = ox + backX * along;
246
+ const ny = oy + backY * along;
247
+ const nz = oz + backZ * along;
248
+ if (nx * nx + ny * ny + nz * nz < this.clear2) continue;
249
+ }
250
+
215
251
  const at = this.segments.count;
216
252
  this.segments.from[at * 3] = dropX;
217
253
  this.segments.from[at * 3 + 1] = dropY;
218
254
  this.segments.from[at * 3 + 2] = dropZ;
219
- /* Where it was a shutter-time ago, which is the streak: down by its fall and back along the
220
- wind, so a gust leans every drop at the same angle without anything trigonometric. */
221
- this.segments.to[at * 3] = dropX - windX * this.streakSec;
222
- this.segments.to[at * 3 + 1] = dropY + this.speedMps * this.streakSec;
223
- this.segments.to[at * 3 + 2] = dropZ - windZ * this.streakSec;
255
+ this.segments.to[at * 3] = dropX + backX;
256
+ this.segments.to[at * 3 + 1] = dropY + backY;
257
+ this.segments.to[at * 3 + 2] = dropZ + backZ;
224
258
  this.segments.count = at + 1;
225
259
  }
226
260
  this.primed = true;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The depth a reconstructing frame's late draws are tested against at an output pixel, taken from
3
+ * the render's jittered depth. `shaders/recon/lateDepth.wgsl.ts` is the same arithmetic on the device.
4
+ *
5
+ * **Each surface taken to the pixel, and the nearest of them.** A late draw — a glow, a pane, a
6
+ * caption — is drawn unjittered at the output size, and it has to meet the depth of what it lies on
7
+ * where *it* is. Each of the four render texels about the pixel, placed where the jitter put it,
8
+ * carries its surface on to the pixel's unjittered centre along that surface's own slope; the
9
+ * nearest of the four so carried stands, and a quarter of its slope is given back toward the far
10
+ * side so that a draw lying exactly on the surface is not decided by rounding. On a plane all four
11
+ * arrive at the plane's own depth at the pixel: exact, and the same whatever the jitter was.
12
+ *
13
+ * **A texel's slope is the smaller of its two differences along each axis, and none where they
14
+ * disagree in sign** — the minmod limiter. Beside an edge one difference is the jump, so it is the
15
+ * other, the surface's own, that is taken; a sliver one texel wide disagrees both ways and keeps its
16
+ * own depth, the nearest of the four as before. So a translucent draw still never comes through an
17
+ * opaque edge the render covered by more than the render's own texel.
18
+ *
19
+ * **Why this replaced the nearest of the four.** On a wall seen aslant the nearest of four neighbours
20
+ * stands up to a texel's step in front of the wall at the pixel, so a glow lying a few centimetres
21
+ * proud of it was judged behind the wall; and which neighbour was nearest moved with the jitter, so
22
+ * the glow was drawn one frame and not the next. Measured on a street of lit facades: every strip of
23
+ * glow on every tower flickered frame by frame, 12,415 pixels of one tower between two frames.
24
+ *
25
+ * What it gives up: a surface curving within a texel is carried on as a plane, which a quarter-step
26
+ * tolerance covers. What would make it wrong is depth that is not affine on a plane — a depth written
27
+ * from the fragment stage — which nothing drawn before the late pass writes.
28
+ */
29
+ import { REVERSED_DEPTH } from '../depthConvention.ts';
30
+
31
+ /** The share of a surface's slope given back toward the far side, for a draw lying on it. */
32
+ const TOLERANCE_SHARE = 0.25;
33
+
34
+ /** The smaller of two differences, or none where they disagree in sign. */
35
+ function minmod(a: number, b: number): number {
36
+ if (a * b <= 0) return 0;
37
+ return Math.abs(a) < Math.abs(b) ? a : b;
38
+ }
39
+
40
+ /**
41
+ * Where output pixel (`outX`, `outY`) — its centre, in pixels — falls among the render texels
42
+ * jittered by (`jitterX`, `jitterY`) render texels: into `out`, the cell's first texel and the
43
+ * fraction across it, `[x, y, fx, fy]`.
44
+ */
45
+ export function lateDepthCell(
46
+ outX: number,
47
+ outY: number,
48
+ renderWidth: number,
49
+ renderHeight: number,
50
+ outputWidth: number,
51
+ outputHeight: number,
52
+ jitterX: number,
53
+ jitterY: number,
54
+ out: Float64Array | Float32Array,
55
+ ): void {
56
+ /* A texel stands at its index plus a half less the jitter, as the resolve places it. */
57
+ const x = (outX * renderWidth) / outputWidth - 0.5 + jitterX;
58
+ const y = (outY * renderHeight) / outputHeight - 0.5 + jitterY;
59
+ out[0] = Math.floor(x);
60
+ out[1] = Math.floor(y);
61
+ out[2] = x - Math.floor(x);
62
+ out[3] = y - Math.floor(y);
63
+ }
64
+
65
+ /**
66
+ * The depth to test against, from `taps` — a 4x4 block of render texels, row-major, starting one
67
+ * texel up and left of the cell's first — at (`fx`, `fy`) across the cell.
68
+ */
69
+ export function lateDepthAt(taps: ArrayLike<number>, fx: number, fy: number): number {
70
+ const t = (x: number, y: number): number => taps[(y + 1) * 4 + x + 1] as number;
71
+ let depth = REVERSED_DEPTH ? -Infinity : Infinity;
72
+ for (let oy = 0; oy < 2; oy += 1) {
73
+ for (let ox = 0; ox < 2; ox += 1) {
74
+ const d = t(ox, oy);
75
+ const gx = minmod(t(ox + 1, oy) - d, d - t(ox - 1, oy));
76
+ const gy = minmod(t(ox, oy + 1) - d, d - t(ox, oy - 1));
77
+ const carried = d + gx * (fx - ox) + gy * (fy - oy);
78
+ const give = TOLERANCE_SHARE * Math.max(Math.abs(gx), Math.abs(gy));
79
+ depth = REVERSED_DEPTH ? Math.max(depth, carried - give) : Math.min(depth, carried + give);
80
+ }
81
+ }
82
+ return depth;
83
+ }
@@ -30,6 +30,29 @@
30
30
  * the render read bicubically fills the gap. The history keeps colour and weight; the picture shown
31
31
  * is it sharpened.
32
32
  *
33
+ * **And the temporal resolve's anti-flicker, where the caller carries its records** (`temporalAa.ts`
34
+ * says why it works and what it gives up). A thread finer than a render texel is caught in some
35
+ * phases of the jitter and missed in others, and in a phase that misses it the nine texels can all
36
+ * miss it: the box collapses onto what is behind, the history is clipped out, and a still picture
37
+ * flashes at every fine edge each period. A still camera over a still scene gathers every pixel the
38
+ * same way each period, so each pixel keeps a record of this frame's gathered luma over the period,
39
+ * and once two periods running have repeated its box widens by its own spread and it keeps more of
40
+ * its history. **The record stays at the pixel rather than following the reprojection**, because the
41
+ * proof is only ever for a pixel that has not moved; it starts over wherever the history did not
42
+ * survive, and is forgotten in proportion to motion.
43
+ *
44
+ * **And a still pixel whose depth has been seen to swing is not a disocclusion.** A rail finer than
45
+ * a texel in front of a wall is caught in some phases and missed in others, so last frame can hold
46
+ * the wall where this frame holds the rail with nothing moved — and the depth test threw the history
47
+ * away, leaving the record nothing to keep. So each pixel also keeps the largest relative miss of
48
+ * last frame's depth it has seen lately, kept whatever the test says, and while still its tolerance
49
+ * widens to `DEPTH_SWING_WIDTH` of it — the temporal resolve's answer to a crack of light round a
50
+ * door, as a held peak rather than its running mean: a rail caught in one phase of eighteen misses
51
+ * once a period, and a mean has worn that miss down to a thirtieth by the next. **What it gives up**
52
+ * is the depth test's word at such a pixel while still: a surface arriving there inside the swing
53
+ * keeps the history, and the neighbourhood clip, which the unproven sample of an arrival leaves
54
+ * unwidened, is what takes it out.
55
+ *
33
56
  * **Conventions.** Images are rows from `v = 0` upward. Depths are clip depths in the matrices'
34
57
  * convention, OpenGL's here — the nearest is the smallest — and the renderer converts its reversed
35
58
  * buffer before a sample reaches this arithmetic, as the temporal resolve already does.
@@ -44,6 +67,16 @@ import {
44
67
  } from './disocclusion.ts';
45
68
  import type { DisocclusionParams } from './disocclusion.ts';
46
69
  import { sampleBilinear, sampleHistoryBicubic5 } from './reproject.ts';
70
+ import {
71
+ TEMPORAL_FLICKER,
72
+ flickerBlend,
73
+ flickerRule,
74
+ flickerWidening,
75
+ freshFlicker,
76
+ nextFlicker,
77
+ stillness,
78
+ } from '../temporalAa.ts';
79
+ import type { Flicker, FlickerRule } from '../temporalAa.ts';
47
80
 
48
81
  /** What a reconstruction is asked for. */
49
82
  export interface ReconQuality {
@@ -140,6 +173,12 @@ export interface ResolveFrame {
140
173
  readonly inverseViewProj: ArrayLike<number>;
141
174
  /** Last frame's view-projection and its inverse, unjittered. */
142
175
  readonly previousViewProj: ArrayLike<number>;
176
+ /**
177
+ * Last frame's view-projection times this frame's inverse: where a texel's clip position was
178
+ * last frame. Derived from the two when absent; a caller holding its matrices at a lower precision
179
+ * than it multiplies at passes its own product, as the renderer does.
180
+ */
181
+ readonly reprojection?: ArrayLike<number>;
143
182
  readonly previousInverseViewProj: ArrayLike<number>;
144
183
  readonly eye: ArrayLike<number>;
145
184
  readonly previousEye: ArrayLike<number>;
@@ -148,6 +187,22 @@ export interface ResolveFrame {
148
187
  readonly previousJitter: ArrayLike<number>;
149
188
  readonly quality: ReconQuality;
150
189
  readonly disocclusion: DisocclusionParams;
190
+ /**
191
+ * Last frame's flicker records, output size, four floats a pixel — the spread, this period's
192
+ * running sum, the last period's mean, and how many periods running repeated, as `Flicker` holds
193
+ * them — or absent for the resolve without its anti-flicker.
194
+ */
195
+ readonly flicker?: ArrayLike<number> | null;
196
+ /**
197
+ * Last frame's depth swings, output size, one float a pixel: the largest miss, relative to the
198
+ * depth, of last frame's depth against the one this pixel's surface should have had, seen lately
199
+ * — held, and halving over a period. Carried with `flicker` and read only with it.
200
+ */
201
+ readonly depthSwing?: ArrayLike<number> | null;
202
+ /** Whether this frame samples the first phase of the jitter's period. */
203
+ readonly periodStart?: boolean;
204
+ /** The jitter's period: the reconstruction's own phase count, not the temporal resolve's. */
205
+ readonly flickerPhases?: number;
151
206
  }
152
207
 
153
208
  /**
@@ -170,6 +225,13 @@ export function reconstructionWeight(dx: number, dy: number): number {
170
225
  */
171
226
  export const CONFIDENT_WEIGHT = 1;
172
227
 
228
+ /**
229
+ * How many of its held depth swings a still pixel's depth test tolerates. The peak halves over a
230
+ * period, so the frame before the next once-a-period miss holds just over half of it: tolerating
231
+ * twice the peak passes that miss outright, and three leaves the same room again.
232
+ */
233
+ export const DEPTH_SWING_WIDTH = 3;
234
+
173
235
  const SAMPLE = new Float32Array(3);
174
236
  const GATHERED = new Float64Array(3);
175
237
  const HISTORY = new Float32Array(4);
@@ -185,6 +247,36 @@ const UP = new Float64Array(3);
185
247
  const NORMAL = new Float64Array(3);
186
248
  const PREVIOUS_NORMAL = new Float64Array(3);
187
249
  const CLIP = new Float64Array(4);
250
+ /** The last frame's reprojection, derived once a frame rather than once a pixel. */
251
+ const REPROJECTION = new Float64Array(16);
252
+ let reprojected: ResolveFrame | null = null;
253
+
254
+ /** `frame.reprojection`, or the product of its two matrices, multiplied once per frame. */
255
+ function reprojectionOf(frame: ResolveFrame): ArrayLike<number> {
256
+ if (frame.reprojection !== undefined) return frame.reprojection;
257
+ if (reprojected !== frame) {
258
+ reprojected = frame;
259
+ const a = frame.previousViewProj;
260
+ const b = frame.inverseViewProj;
261
+ for (let c = 0; c < 4; c += 1) {
262
+ for (let r = 0; r < 4; r += 1) {
263
+ let sum = 0;
264
+ for (let k = 0; k < 4; k += 1) sum += (a[k * 4 + r] as number) * (b[c * 4 + k] as number);
265
+ REPROJECTION[c * 4 + r] = sum;
266
+ }
267
+ }
268
+ }
269
+ return REPROJECTION;
270
+ }
271
+ const HELD: Flicker = { spread: 0, sum: 0, mean: 0, repeated: 0 };
272
+ const CARRIED: Flicker = { spread: 0, sum: 0, mean: 0, repeated: 0 };
273
+ /** How far, in output pixels, the last `historyWeight` found its surface had moved. */
274
+ let moved = 0;
275
+ /** How far, relative, last frame's depth missed the one expected there; -1 where nothing was held. */
276
+ let missed = -1;
277
+ /** The rule for the last period asked for, kept: one is made per period, not per pixel. */
278
+ let rulePhases = 0;
279
+ let rule: FlickerRule = TEMPORAL_FLICKER;
188
280
 
189
281
  function clampTexel(value: number, size: number): number {
190
282
  return value < 0 ? 0 : value >= size ? size - 1 : value;
@@ -296,7 +388,15 @@ function normalAt(
296
388
  * was behind last frame's eye or off its picture, and otherwise what it gathered, scaled by whether it
297
389
  * is the same surface. Reads `HISTORY`; writes the history's colour there.
298
390
  */
299
- function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy: number): number {
391
+ function historyWeight(
392
+ frame: ResolveFrame,
393
+ u: number,
394
+ v: number,
395
+ dx: number,
396
+ dy: number,
397
+ swing: number,
398
+ ): number {
399
+ missed = -1;
300
400
  if (!frame.hasHistory) return 0;
301
401
  const rw = frame.renderWidth;
302
402
  const rh = frame.renderHeight;
@@ -309,28 +409,46 @@ function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy
309
409
  mv = frame.motion[flagAt + 1] as number;
310
410
  expected = frame.motion[flagAt + 2] as number;
311
411
  } else {
312
- if (worldAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy, WORLD) <= 0) {
313
- return 0;
314
- }
315
- const m = frame.previousViewProj;
412
+ /*
413
+ * **Through one matrix, last frame's view-projection times this frame's inverse**, rather than
414
+ * out to a world position and back. The two are the same arithmetic in exact numbers; in single
415
+ * precision the round trip is not the identity — a near plane a quarter of a metre from a far one
416
+ * five kilometres off — and a still camera measured a few hundredths of a pixel of motion growing
417
+ * out from the middle of the frame, which crept every still history and kept every pixel from
418
+ * the anti-flicker's stillness. The product is taken where the precision is (`reprojection`),
419
+ * and for a still camera it is the identity.
420
+ */
421
+ const du = (dx + 0.5 - (frame.jitter[0] as number)) / rw;
422
+ const dv = (dy + 0.5 - (frame.jitter[1] as number)) / rh;
423
+ const x = du * 2 - 1;
424
+ const y = dv * 2 - 1;
425
+ const z = frame.depth[dy * rw + dx] as number;
426
+ const inverse = frame.inverseViewProj;
427
+ /* The homogeneous w of the world position: none, or behind the eye, is no surface. */
428
+ const hw =
429
+ (inverse[3] as number) * x +
430
+ (inverse[7] as number) * y +
431
+ (inverse[11] as number) * z +
432
+ (inverse[15] as number);
433
+ if (!(hw > 1e-12)) return 0;
434
+ const m = reprojectionOf(frame);
316
435
  for (let r = 0; r < 4; r += 1) {
317
436
  CLIP[r] =
318
- (m[r] as number) * (WORLD[0] as number) +
319
- (m[4 + r] as number) * (WORLD[1] as number) +
320
- (m[8 + r] as number) * (WORLD[2] as number) +
437
+ (m[r] as number) * x +
438
+ (m[4 + r] as number) * y +
439
+ (m[8 + r] as number) * z +
321
440
  (m[12 + r] as number);
322
441
  }
323
442
  /*
324
443
  * A surface behind last frame's eye has a w that is not positive: the disocclusion trusts no
325
444
  * such depth, so it needs no refusal of its own here.
326
445
  */
327
- expected = CLIP[3] as number;
328
- const du = (dx + 0.5 - (frame.jitter[0] as number)) / rw;
329
- const dv = (dy + 0.5 - (frame.jitter[1] as number)) / rh;
330
- mu = ((CLIP[0] as number) / expected) * 0.5 + 0.5 - du;
331
- mv = ((CLIP[1] as number) / expected) * 0.5 + 0.5 - dv;
446
+ expected = (CLIP[3] as number) / hw;
447
+ mu = ((CLIP[0] as number) / (CLIP[3] as number)) * 0.5 + 0.5 - du;
448
+ mv = ((CLIP[1] as number) / (CLIP[3] as number)) * 0.5 + 0.5 - dv;
332
449
  }
333
450
 
451
+ moved = Math.hypot(mu * frame.outputWidth, mv * frame.outputHeight);
334
452
  const pu = u + mu;
335
453
  const pv = v + mv;
336
454
  if (!(pu >= 0 && pu <= 1 && pv >= 0 && pv <= 1)) return 0;
@@ -440,22 +558,32 @@ function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy
440
558
  (NORMAL[2] as number) * (PREVIOUS_NORMAL[2] as number);
441
559
  normalDot = 1 - planar * (1 - cosine);
442
560
  }
561
+ if (held > 0 && expected > 0) missed = Math.abs(held - expected) / expected;
562
+ /* A still pixel's own depth swing, as wide as the anti-flicker's box: see the header. */
563
+ const swung = swing > 0 ? DEPTH_SWING_WIDTH * swing * stillness(moved) : 0;
443
564
  const trust = disocclusionWeight(
444
565
  expected,
445
566
  held,
446
567
  Math.hypot(mu, mv),
447
568
  normalDot,
448
569
  frame.disocclusion,
449
- slope,
570
+ slope + swung,
450
571
  );
451
572
  return (HISTORY[3] as number) * trust;
452
573
  }
453
574
 
454
575
  /**
455
576
  * One output pixel: the history's next colour, before sharpening, and the weight it has gathered —
456
- * four values into `out`.
577
+ * four values into `out` — and, where the frame carries records, the pixel's next one into `record`.
457
578
  */
458
- export function resolvePixel(frame: ResolveFrame, ox: number, oy: number, out: Float32Array): void {
579
+ export function resolvePixel(
580
+ frame: ResolveFrame,
581
+ ox: number,
582
+ oy: number,
583
+ out: Float32Array,
584
+ record?: Float32Array,
585
+ swingOut?: Float32Array,
586
+ ): void {
459
587
  const rw = frame.renderWidth;
460
588
  const rh = frame.renderHeight;
461
589
  const ow = frame.outputWidth;
@@ -508,8 +636,58 @@ export function resolvePixel(frame: ResolveFrame, ox: number, oy: number, out: F
508
636
  for (let c = 0; c < 3; c += 1) GATHERED[c] = (GATHERED[c] as number) / gathered;
509
637
 
510
638
  /* 3 and 4. The history, and the weight it still deserves. */
511
- const cap = (1 - frame.quality.alpha) / frame.quality.alpha;
512
- const kept = Math.min(cap, historyWeight(frame, u, v, dx, dy));
639
+ const alpha = frame.quality.alpha;
640
+ const records = frame.flicker;
641
+ const anti = records !== undefined && records !== null;
642
+ if (anti) {
643
+ const phases = frame.flickerPhases ?? 0;
644
+ if (phases !== rulePhases) {
645
+ rulePhases = phases;
646
+ rule = phases > 0 ? flickerRule(phases) : TEMPORAL_FLICKER;
647
+ }
648
+ }
649
+ const swings = frame.depthSwing;
650
+ const swing =
651
+ anti && swings !== undefined && swings !== null ? (swings[oy * ow + ox] as number) : 0;
652
+ const weight = historyWeight(frame, u, v, dx, dy, swing);
653
+ let cap = (1 - alpha) / alpha;
654
+ /* The anti-flicker: a proven pixel's box widened by its spread, and more of its history kept. */
655
+ if (anti) {
656
+ const sample =
657
+ (GATHERED[0] as number) * 0.2126 +
658
+ (GATHERED[1] as number) * 0.7152 +
659
+ (GATHERED[2] as number) * 0.0722;
660
+ const at = (oy * ow + ox) * 4;
661
+ if (weight > 0) {
662
+ HELD.spread = records[at] as number;
663
+ HELD.sum = records[at + 1] as number;
664
+ HELD.mean = records[at + 2] as number;
665
+ HELD.repeated = records[at + 3] as number;
666
+ } else {
667
+ freshFlicker(HELD, sample, rule);
668
+ }
669
+ const still = weight > 0 ? stillness(moved) : 0;
670
+ const widen = flickerWidening(HELD, sample, still, rule);
671
+ for (let c = 0; c < 3; c += 1) {
672
+ LOW[c] = (LOW[c] as number) - widen;
673
+ HIGH[c] = (HIGH[c] as number) + widen;
674
+ }
675
+ const keep = flickerBlend(1 - alpha, HELD, sample, still, rule);
676
+ cap = keep / (1 - keep);
677
+ nextFlicker(CARRIED, HELD, sample, still, frame.periodStart === true, rule);
678
+ if (record !== undefined) {
679
+ record[0] = CARRIED.spread;
680
+ record[1] = CARRIED.sum;
681
+ record[2] = CARRIED.mean;
682
+ record[3] = CARRIED.repeated;
683
+ }
684
+ /* The peak carried on, halving over a period, whatever the test said; none where nothing was
685
+ held to miss. */
686
+ if (swingOut !== undefined) {
687
+ swingOut[0] = missed >= 0 ? Math.max(missed, swing * 0.5 ** rule.memory) : 0;
688
+ }
689
+ }
690
+ const kept = Math.min(cap, weight);
513
691
 
514
692
  /* 5. Clipped, mixed by weight, and the gaps filled from the render. */
515
693
  const total = kept + gathered;
@@ -529,6 +707,8 @@ export function resolvePixel(frame: ResolveFrame, ox: number, oy: number, out: F
529
707
  }
530
708
 
531
709
  const TAP = new Float32Array(4);
710
+ const RECORD = new Float32Array(4);
711
+ const SWING = new Float32Array(1);
532
712
  const CENTRE = new Float32Array(4);
533
713
  const AROUND = new Float32Array(12);
534
714
  const SHOWN = new Float32Array(3);
@@ -541,19 +721,25 @@ const OFFSETS: readonly (readonly [number, number])[] = [
541
721
 
542
722
  /**
543
723
  * The whole frame: the next history into `history`, four floats a pixel, and the picture shown into
544
- * `shown`, three — the history sharpened against the four direct neighbours it has just been given.
724
+ * `shown`, three — the history sharpened against the four direct neighbours it has just been given —
725
+ * and, where the frame carries records, the next ones into `records`, four floats a pixel, and the
726
+ * next depth swings into `swings`, one.
545
727
  */
546
728
  export function resolveFrame(
547
729
  frame: ResolveFrame,
548
730
  history: Float32Array,
549
731
  shown: Float32Array,
732
+ records?: Float32Array,
733
+ swings?: Float32Array,
550
734
  ): void {
551
735
  const ow = frame.outputWidth;
552
736
  const oh = frame.outputHeight;
553
737
  for (let y = 0; y < oh; y += 1) {
554
738
  for (let x = 0; x < ow; x += 1) {
555
- resolvePixel(frame, x, y, TAP);
739
+ resolvePixel(frame, x, y, TAP, RECORD, SWING);
556
740
  history.set(TAP, (y * ow + x) * 4);
741
+ if (records !== undefined) records.set(RECORD, (y * ow + x) * 4);
742
+ if (swings !== undefined) swings[y * ow + x] = SWING[0] as number;
557
743
  }
558
744
  }
559
745
  for (let y = 0; y < oh; y += 1) {
@@ -72,6 +72,10 @@ export function recordingGl(
72
72
  const programFragments = new WeakMap<object, string>();
73
73
  const calls: { name: string; args: unknown[] }[] = [];
74
74
  const constants: Record<string, number> = {
75
+ /* The canvas's size, as a real context reports its drawing buffer; a function here made every
76
+ jitter and every per-pixel offset divided by it a NaN. */
77
+ drawingBufferWidth: 640,
78
+ drawingBufferHeight: 480,
75
79
  ACTIVE_UNIFORMS: 0x8b86,
76
80
  ACTIVE_ATTRIBUTES: 0x8b89,
77
81
  COMPILE_STATUS: 0x8b81,
@@ -755,6 +755,8 @@ export class SceneTarget {
755
755
  /** The projection's x and y scales, which turn a world radius into a screen one. */
756
756
  readonly projScale: Float32Array;
757
757
  readonly invProjection: Float32Array;
758
+ /** Where the frame's jitter put the occlusion, in uv: see the shader's `uAoOffset`. */
759
+ readonly offset?: Float32Array;
758
760
  },
759
761
  bloom?: {
760
762
  readonly strength: number;
@@ -1023,6 +1025,8 @@ export class SceneTarget {
1023
1025
  this.uniforms['uAoStrength'] ?? null,
1024
1026
  aoTexture === null ? 0 : (ao?.strength ?? 0),
1025
1027
  );
1028
+ const offset = ao?.offset;
1029
+ gl.uniform2f(this.uniforms['uAoOffset'] ?? null, offset?.[0] ?? 0, offset?.[1] ?? 0);
1026
1030
 
1027
1031
  /* Bloom on its own unit, and the placeholder for the same reason as the one above: a
1028
1032
  driver may fetch a texture's descriptor before it evaluates the branch that skips the
@@ -64,14 +64,15 @@ ${CHANNEL_BEND}
64
64
  * uniforms declared after every other, so no field of the plain variant moves and a binder written
65
65
  * against it stays right for this one; and two outputs after vLightPosition, which keeps location 0.
66
66
  */
67
- layout(location = 5) in vec2 aUv;
67
+ layout(location = 5) in vec3 aUv;
68
68
  uniform vec2 uUvScale;
69
69
  uniform float uAlphaCutout;
70
70
  #endif
71
71
 
72
72
  out vec4 vLightPosition;
73
73
  #if CUTOUT
74
- out vec2 vUv;
74
+ /* The texture coordinate and, in z, the texture-array layer the face wears. */
75
+ out vec3 vUv;
75
76
  flat out float vAlphaCutout;
76
77
  #endif
77
78
  #if GLASS
@@ -103,7 +104,7 @@ void main() {
103
104
  vLightPosition = uLightViewProj * vec4(bent, world.w);
104
105
  gl_Position = vLightPosition;
105
106
  #if CUTOUT
106
- vUv = aUv * uUvScale;
107
+ vUv = vec3(aUv.xy * uUvScale, aUv.z);
107
108
  vAlphaCutout = uAlphaCutout;
108
109
  #endif
109
110
  #if GLASS
@@ -196,16 +197,17 @@ void main() {
196
197
  export const DEPTH_CUTOUT_FRAG = `#version 300 es
197
198
  precision highp float;
198
199
  in vec4 vLightPosition;
199
- in vec2 vUv;
200
+ in vec3 vUv;
200
201
  flat in float vAlphaCutout;
201
202
 
202
203
  uniform highp sampler2DArray uPreviousShadowMap;
203
204
  uniform int uPeelShadowLayer;
204
- uniform highp sampler2D uCutoutMap;
205
+ uniform highp sampler2DArray uCutoutMap;
205
206
  ${CUTOUT_COVERAGE_GLSL}
206
207
  void main() {
207
208
  /* Credited for its mip level as the surface's own test is, so a leaf casts the shape it draws. */
208
- float alpha = cutoutAlpha(texture(uCutoutMap, vUv).a, vUv * vec2(textureSize(uCutoutMap, 0)));
209
+ vec3 at = vec3(vUv.xy, floor(vUv.z + 0.5));
210
+ float alpha = cutoutAlpha(texture(uCutoutMap, at).a, vUv.xy * vec2(textureSize(uCutoutMap, 0).xy));
209
211
  if (uPeelShadowLayer != 0) {
210
212
  vec3 p = vLightPosition.xyz / vLightPosition.w;
211
213
  vec2 uv = p.xy * 0.5 + 0.5;
@@ -289,19 +291,20 @@ ${GLASS_TINT_BODY}
289
291
  export const GLASS_TINT_CUTOUT_FRAG = `#version 300 es
290
292
  precision highp float;
291
293
  in vec4 vLightPosition;
292
- in vec2 vUv;
294
+ in vec3 vUv;
293
295
  flat in float vAlphaCutout;
294
296
  in vec3 vGlassWorld;
295
297
 
296
298
  uniform vec4 uGlassPane;
297
299
  uniform vec4 uGlassLight;
298
- uniform highp sampler2D uCutoutMap;
300
+ uniform highp sampler2DArray uCutoutMap;
299
301
  ${CUTOUT_COVERAGE_GLSL}
300
302
  out vec4 outTint;
301
303
 
302
304
  void main() {
303
305
  /* First, in uniform control flow, as the depth cutout samples it. */
304
- float alpha = cutoutAlpha(texture(uCutoutMap, vUv).a, vUv * vec2(textureSize(uCutoutMap, 0)));
306
+ vec3 at = vec3(vUv.xy, floor(vUv.z + 0.5));
307
+ float alpha = cutoutAlpha(texture(uCutoutMap, at).a, vUv.xy * vec2(textureSize(uCutoutMap, 0).xy));
305
308
  ${GLASS_TINT_BODY}
306
309
  if (alpha < vAlphaCutout) discard;
307
310
  }
@@ -29,7 +29,7 @@
29
29
  export const DRIFT_LIGHT_GLSL = /* glsl */ `
30
30
  /** x how much of the summed light is in the frame (0 off), y the radius, z the band, w the scale. */
31
31
  uniform vec4 uDriftLight;
32
- /** xyz the first brick's first sample; w metres between samples, a third of a brick. */
32
+ /** xyz the first sample; w metres between samples — a third of a brick, or negated for a dense volume. */
33
33
  uniform vec4 uDriftLightOrigin;
34
34
  uniform highp usampler3D uDriftLightIndex; // wgsl:share shadow
35
35
  uniform highp sampler3D uDriftLightAtlas;
@@ -41,9 +41,38 @@ float driftLightShare(vec3 world) {
41
41
  return smoothstep(uDriftLight.y - uDriftLight.z, uDriftLight.y, away) * uDriftLight.x;
42
42
  }
43
43
 
44
- /** The summed light arriving on a surface at \`world\` facing \`n\`. */
44
+ /**
45
+ * A sample's light and direction turned into the light on a surface facing \`n\`: Lambert for a
46
+ * single light, a quarter for light arriving from everywhere (see \`bake.ts\`). One statement for
47
+ * both kinds of volume.
48
+ */
49
+ vec3 driftLightFacing(vec4 light, vec4 toward, vec3 n) {
50
+ vec3 arriving = light.rgb / light.a;
51
+ vec3 from = toward.xyz / light.a;
52
+ float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;
53
+ return arriving * facing;
54
+ }
55
+
56
+ /**
57
+ * The same from a dense volume: one sample a texel, the light in the lower half of y and its
58
+ * direction in the upper. A lookup kept inside the samples never filters across the halves.
59
+ */
60
+ vec3 driftLightDense(vec3 world, vec3 n, float spacing) {
61
+ ivec3 size = textureSize(uDriftLightAtlas, 0);
62
+ vec3 dims = vec3(float(size.x), float(size.y / 2), float(size.z));
63
+ vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / spacing;
64
+ if (any(lessThan(at, vec3(0.0))) || any(greaterThan(at, dims - 1.0))) return vec3(0.0);
65
+ vec3 uvw = (at + 0.5) / vec3(dims.x, dims.y * 2.0, dims.z);
66
+ vec4 light = textureLod(uDriftLightAtlas, uvw, 0.0);
67
+ if (light.a < 1e-3) return vec3(0.0);
68
+ vec4 toward = textureLod(uDriftLightAtlas, uvw + vec3(0.0, 0.5, 0.0), 0.0);
69
+ return driftLightFacing(light, toward, n);
70
+ }
71
+
72
+ /** The summed light arriving on a surface at \`world\` facing \`n\`. A negative spacing is dense. */
45
73
  vec3 driftLightIrradiance(vec3 world, vec3 n) {
46
74
  float spacing = uDriftLightOrigin.w;
75
+ if (spacing < 0.0) return driftLightDense(world, n, -spacing);
47
76
  vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / (3.0 * spacing);
48
77
  ivec3 cell = ivec3(floor(at));
49
78
  if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, textureSize(uDriftLightIndex, 0)))) {
@@ -66,10 +95,6 @@ vec3 driftLightIrradiance(vec3 world, vec3 n) {
66
95
  vec4 light = textureLod(uDriftLightAtlas, (corner + inside) * texel, 0.0);
67
96
  if (light.a < 1e-3) return vec3(0.0);
68
97
  vec4 toward = textureLod(uDriftLightAtlas, (corner + vec3(4.0, 0.0, 0.0) + inside) * texel, 0.0);
69
- vec3 arriving = light.rgb / light.a;
70
- vec3 from = toward.xyz / light.a;
71
- /* Lambert for a single light, a quarter for light arriving from everywhere: see \`bake.ts\`. */
72
- float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;
73
- return arriving * facing;
98
+ return driftLightFacing(light, toward, n);
74
99
  }
75
100
  `;