@driftengine/core 4.5.0 → 4.6.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 (209) hide show
  1. package/CHANGELOG.json +62 -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 +249 -128
  33. package/dist/render/backend/webgpu/buffers.d.ts +17 -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/motionPass.js +2 -1
  48. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  49. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  50. package/dist/render/backend/webgpu/renderer.js +478 -214
  51. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  53. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  54. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  55. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  56. package/dist/render/driftLight/bake.d.ts +7 -0
  57. package/dist/render/driftLight/bake.js +61 -56
  58. package/dist/render/driftLight/denseField.d.ts +46 -0
  59. package/dist/render/driftLight/denseField.js +109 -0
  60. package/dist/render/driftLight/lightField.d.ts +10 -5
  61. package/dist/render/driftLight/lightField.js +25 -19
  62. package/dist/render/driftLight/presence.d.ts +50 -0
  63. package/dist/render/driftLight/presence.js +43 -0
  64. package/dist/render/driftLight/uniforms.d.ts +3 -3
  65. package/dist/render/driftLight/uniforms.js +5 -6
  66. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  67. package/dist/render/driftLight/worldLightField.js +66 -0
  68. package/dist/render/fixtureAtlas.d.ts +20 -0
  69. package/dist/render/fixtureAtlas.js +49 -0
  70. package/dist/render/instanceCull.d.ts +63 -0
  71. package/dist/render/instanceCull.js +160 -0
  72. package/dist/render/instances.d.ts +15 -0
  73. package/dist/render/lightBudget.d.ts +22 -33
  74. package/dist/render/lightBudget.js +24 -35
  75. package/dist/render/lightGrid.d.ts +42 -0
  76. package/dist/render/lightGrid.js +87 -0
  77. package/dist/render/mesh.d.ts +23 -24
  78. package/dist/render/mesh.js +56 -68
  79. package/dist/render/occlusion.d.ts +10 -0
  80. package/dist/render/occlusion.js +36 -7
  81. package/dist/render/pointLightSelection.d.ts +7 -0
  82. package/dist/render/pointLightSelection.js +29 -4
  83. package/dist/render/rainField.d.ts +13 -0
  84. package/dist/render/rainField.js +27 -5
  85. package/dist/render/recon/lateDepth.d.ts +11 -0
  86. package/dist/render/recon/lateDepth.js +70 -0
  87. package/dist/render/recon/resolve.d.ts +34 -4
  88. package/dist/render/recon/resolve.js +161 -20
  89. package/dist/render/rendererHarness.js +4 -0
  90. package/dist/render/sceneTarget.d.ts +2 -0
  91. package/dist/render/sceneTarget.js +2 -0
  92. package/dist/render/shaders/depth.js +12 -9
  93. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  94. package/dist/render/shaders/flat/driftLight.js +32 -7
  95. package/dist/render/shaders/flat/index.js +5 -3
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +75 -23
  98. package/dist/render/shaders/flat/preamble.js +75 -20
  99. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  100. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  101. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  102. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  103. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  104. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  106. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  107. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  108. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  109. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  110. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  114. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  115. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  116. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  117. package/dist/render/shaders/rush.js +11 -1
  118. package/dist/render/shaders/sdfText.d.ts +1 -1
  119. package/dist/render/shaders/sdfText.js +3 -2
  120. package/dist/render/shaders/text.d.ts +1 -1
  121. package/dist/render/shaders/text.js +18 -9
  122. package/dist/render/surfaceEffects.d.ts +83 -0
  123. package/dist/render/surfaceEffects.js +75 -0
  124. package/dist/render/surfaceScene.d.ts +8 -0
  125. package/dist/render/surfaceScene.js +8 -0
  126. package/dist/render/surfaceTexture.d.ts +18 -2
  127. package/dist/render/surfaceTexture.js +68 -15
  128. package/dist/render/temporalAa.d.ts +25 -6
  129. package/dist/render/temporalAa.js +45 -15
  130. package/dist/render/textureSource.d.ts +30 -0
  131. package/dist/render/textureSource.js +53 -0
  132. package/dist/render/vertexDefaults.js +2 -0
  133. package/dist/world/hlod.d.ts +104 -0
  134. package/dist/world/hlod.js +214 -0
  135. package/package.json +6 -6
  136. package/src/geometry/boxCsg.ts +186 -0
  137. package/src/geometry/csg.ts +384 -0
  138. package/src/geometry/meshBuilder.ts +15 -0
  139. package/src/geometry/meshConcat.ts +1 -0
  140. package/src/geometry/pixelFont.ts +2 -0
  141. package/src/geometry/solid.ts +189 -0
  142. package/src/geometry/solidFlat.ts +522 -0
  143. package/src/geometry/solidHarness.ts +83 -0
  144. package/src/geometry/solidNormals.ts +103 -0
  145. package/src/geometry/solidRound.ts +443 -0
  146. package/src/geometry/solidWriter.ts +58 -0
  147. package/src/index.ts +51 -1
  148. package/src/render/backend/api.ts +21 -2
  149. package/src/render/backend/drawFog.ts +31 -0
  150. package/src/render/backend/materialChanges.ts +9 -2
  151. package/src/render/backend/webgl2/instanced.ts +80 -14
  152. package/src/render/backend/webgl2/renderer.ts +311 -150
  153. package/src/render/backend/webgpu/buffers.ts +49 -3
  154. package/src/render/backend/webgpu/depthPass.ts +5 -1
  155. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  156. package/src/render/backend/webgpu/flatPass.ts +41 -11
  157. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  158. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  159. package/src/render/backend/webgpu/instanced.ts +21 -3
  160. package/src/render/backend/webgpu/latePass.ts +12 -3
  161. package/src/render/backend/webgpu/motionPass.ts +2 -1
  162. package/src/render/backend/webgpu/renderer.ts +556 -239
  163. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  164. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  165. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  166. package/src/render/driftLight/bake.ts +85 -62
  167. package/src/render/driftLight/denseField.ts +149 -0
  168. package/src/render/driftLight/lightField.ts +33 -18
  169. package/src/render/driftLight/presence.ts +69 -0
  170. package/src/render/driftLight/uniforms.ts +8 -9
  171. package/src/render/driftLight/worldLightField.ts +83 -0
  172. package/src/render/fixtureAtlas.ts +63 -0
  173. package/src/render/instanceCull.ts +205 -0
  174. package/src/render/instances.ts +16 -0
  175. package/src/render/lightBudget.ts +25 -36
  176. package/src/render/lightGrid.ts +130 -0
  177. package/src/render/mesh.ts +64 -72
  178. package/src/render/occlusion.ts +51 -7
  179. package/src/render/pointLightSelection.ts +72 -4
  180. package/src/render/rainField.ts +39 -5
  181. package/src/render/recon/lateDepth.ts +83 -0
  182. package/src/render/recon/resolve.ts +206 -20
  183. package/src/render/rendererHarness.ts +4 -0
  184. package/src/render/sceneTarget.ts +4 -0
  185. package/src/render/shaders/depth.ts +12 -9
  186. package/src/render/shaders/flat/driftLight.ts +32 -7
  187. package/src/render/shaders/flat/index.ts +5 -3
  188. package/src/render/shaders/flat/main.ts +75 -23
  189. package/src/render/shaders/flat/preamble.ts +75 -20
  190. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  191. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  192. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  193. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  194. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  195. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  196. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  197. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  198. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  199. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  200. package/src/render/shaders/rush.ts +11 -1
  201. package/src/render/shaders/sdfText.ts +3 -2
  202. package/src/render/shaders/text.ts +18 -9
  203. package/src/render/surfaceEffects.ts +132 -0
  204. package/src/render/surfaceScene.ts +16 -0
  205. package/src/render/surfaceTexture.ts +110 -19
  206. package/src/render/temporalAa.ts +78 -14
  207. package/src/render/textureSource.ts +72 -0
  208. package/src/render/vertexDefaults.ts +2 -0
  209. package/src/world/hlod.ts +290 -0
@@ -34,12 +34,16 @@
34
34
  * fn reconMotion(x: i32, y: i32) -> vec4<f32> // uv, previous view depth, flag
35
35
  * fn reconHistory(x: i32, y: i32) -> vec4<f32> // output size, colour and gathered weight
36
36
  * fn reconHistoryBilinear(p: vec2<f32>) -> vec4<f32>
37
+ * fn reconFlicker(x: i32, y: i32) -> vec4<f32> // output size, last frame's flicker record
38
+ * fn reconSwing(x: i32, y: i32) -> f32 // output size, last frame's held depth swing
37
39
  * ```
38
40
  *
39
41
  * and the uniform `recon: ReconParams`, whose struct this module declares.
40
42
  */
41
43
  import { wgslSceneDepthToNdc } from '../../depthConvention.ts';
42
44
  import { PLANAR_FROM, PLANAR_TO } from '../../recon/disocclusion.ts';
45
+ import { DEPTH_SWING_WIDTH } from '../../recon/resolve.ts';
46
+ import { FLICKER_BLEND, FLICKER_REPEAT, STILL_FROM, STILL_TO } from '../../temporalAa.ts';
43
47
 
44
48
  /**
45
49
  * The parameters one resolve reads, as `ResolveFrame` minus its images.
@@ -66,11 +70,16 @@ struct ReconParams {
66
70
  motionScale: f32,
67
71
  normalFloor: f32,
68
72
  normalCeiling: f32,
73
+ periodStart: u32,
74
+ antiFlicker: u32,
75
+ flickerMemory: f32,
76
+ flickerWidth: f32,
77
+ reprojection: mat4x4<f32>,
69
78
  }
70
79
  `;
71
80
 
72
81
  /** Floats in `ReconParams`, which is what a caller sizes its uniform buffer by. */
73
- export const RECON_PARAM_FLOATS = 72;
82
+ export const RECON_PARAM_FLOATS = 92;
74
83
 
75
84
  /** `SAMPLE_SPREAD` and `CONFIDENT_WEIGHT`, which the reference exports and this must not restate. */
76
85
  export const RECON_SAMPLE_SPREAD = 0.47;
@@ -91,8 +100,8 @@ export const RECON_WORKGROUP = 8;
91
100
  export const RECON_HISTORY_FORMAT: GPUTextureFormat = 'rgba16float';
92
101
 
93
102
  /**
94
- * The arithmetic, as text. Defines `reconResolvePixel(ox, oy) -> vec4<f32>` — the next history's
95
- * colour and the weight it has gathered — and `reconSharpenAt(ox, oy) -> vec3<f32>`, the picture
103
+ * The arithmetic, as text. Defines `reconResolvePixel(ox, oy) -> ReconResolved` — the next
104
+ * history's colour and the weight it has gathered, and the pixel's next flicker record — and `reconSharpenAt(ox, oy) -> vec3<f32>`, the picture
96
105
  * shown, which reads the history the first has already written.
97
106
  */
98
107
  export const RECON_RESOLVE_CORE_WGSL = /* wgsl */ `
@@ -100,6 +109,11 @@ const RECON_SAMPLE_SPREAD: f32 = ${RECON_SAMPLE_SPREAD};
100
109
  const RECON_CONFIDENT_WEIGHT: f32 = ${RECON_CONFIDENT_WEIGHT}.0;
101
110
  const RECON_PLANAR_FROM: f32 = ${PLANAR_FROM};
102
111
  const RECON_PLANAR_TO: f32 = ${PLANAR_TO};
112
+ const RECON_FLICKER_BLEND: f32 = ${FLICKER_BLEND};
113
+ const RECON_FLICKER_REPEAT: f32 = ${FLICKER_REPEAT};
114
+ const RECON_STILL_FROM: f32 = ${STILL_FROM};
115
+ const RECON_STILL_TO: f32 = ${STILL_TO};
116
+ const RECON_DEPTH_SWING_WIDTH: f32 = ${DEPTH_SWING_WIDTH}.0;
103
117
 
104
118
  fn reconClampTexel(value: i32, size: i32) -> i32 {
105
119
  return clamp(value, 0, size - 1);
@@ -467,6 +481,10 @@ fn reconSurfaceAt(previous: bool, x: i32, y: i32, centre: f32) -> ReconSurface {
467
481
  struct ReconHistory {
468
482
  weight: f32,
469
483
  colour: vec4<f32>,
484
+ /* How far the surface moved, in output pixels; read only where the weight is not zero. */
485
+ moved: f32,
486
+ /* How far, relative, last frame's depth missed the one expected there; -1 where none was held. */
487
+ missed: f32,
470
488
  }
471
489
 
472
490
  /*
@@ -474,10 +492,12 @@ struct ReconHistory {
474
492
  * behind last frame's eye or off its picture, and otherwise what it gathered, scaled by whether it
475
493
  * is the same surface.
476
494
  */
477
- fn reconHistoryWeight(u: f32, v: f32, dx: i32, dy: i32) -> ReconHistory {
495
+ fn reconHistoryWeight(u: f32, v: f32, dx: i32, dy: i32, swing: f32) -> ReconHistory {
478
496
  var out: ReconHistory;
479
497
  out.weight = 0.0;
480
498
  out.colour = vec4<f32>(0.0);
499
+ out.moved = 0.0;
500
+ out.missed = -1.0;
481
501
  if (recon.hasHistory == 0u) { return out; }
482
502
  let rw = f32(recon.renderSize.x);
483
503
  let rh = f32(recon.renderSize.y);
@@ -490,20 +510,24 @@ fn reconHistoryWeight(u: f32, v: f32, dx: i32, dy: i32) -> ReconHistory {
490
510
  mv = motion.y;
491
511
  expected = motion.z;
492
512
  } else {
493
- let here = reconWorldAt(false, dx, dy);
494
- if (!(here.depth > 0.0)) { return out; }
495
- let clip = recon.previousViewProj * vec4<f32>(here.position, 1.0);
513
+ /* Through the one reprojection matrix the renderer multiplied: see resolve.ts. */
514
+ let du = (f32(dx) + 0.5 - recon.jitter.x) / rw;
515
+ let dv = (f32(dy) + 0.5 - recon.jitter.y) / rh;
516
+ let ndc = vec4<f32>(du * 2.0 - 1.0, dv * 2.0 - 1.0, reconDepth(dx, dy), 1.0);
517
+ let m = recon.inverseViewProj;
518
+ let hw = m[0][3] * ndc.x + m[1][3] * ndc.y + m[2][3] * ndc.z + m[3][3];
519
+ if (!(hw > 1e-12)) { return out; }
520
+ let clip = recon.reprojection * ndc;
496
521
  /*
497
522
  * A surface behind last frame's eye has a w that is not positive: the disocclusion trusts no
498
523
  * such depth, so it needs no refusal of its own here.
499
524
  */
500
- expected = clip.w;
501
- let du = (f32(dx) + 0.5 - recon.jitter.x) / rw;
502
- let dv = (f32(dy) + 0.5 - recon.jitter.y) / rh;
503
- mu = (clip.x / expected) * 0.5 + 0.5 - du;
504
- mv = (clip.y / expected) * 0.5 + 0.5 - dv;
525
+ expected = clip.w / hw;
526
+ mu = (clip.x / clip.w) * 0.5 + 0.5 - du;
527
+ mv = (clip.y / clip.w) * 0.5 + 0.5 - dv;
505
528
  }
506
529
 
530
+ out.moved = length(vec2<f32>(mu * f32(recon.outputSize.x), mv * f32(recon.outputSize.y)));
507
531
  let pu = u + mu;
508
532
  let pv = v + mv;
509
533
  if (!(pu >= 0.0 && pu <= 1.0 && pv >= 0.0 && pv <= 1.0)) { return out; }
@@ -550,13 +574,41 @@ fn reconHistoryWeight(u: f32, v: f32, dx: i32, dy: i32) -> ReconHistory {
550
574
  normalDot = 1.0 - planar * (1.0 - dot(hereNormal.normal, thereNormal.normal));
551
575
  }
552
576
  }
553
- let trust = reconDisocclusion(expected, held, length(vec2<f32>(mu, mv)), normalDot, here.slope);
577
+ if (held > 0.0 && expected > 0.0) { out.missed = abs(held - expected) / expected; }
578
+ /* A still pixel's own held depth swing: see resolve.ts. */
579
+ var swung = 0.0;
580
+ if (swing > 0.0) {
581
+ swung = RECON_DEPTH_SWING_WIDTH * swing *
582
+ (1.0 - reconSmoothstep(RECON_STILL_FROM, RECON_STILL_TO, out.moved));
583
+ }
584
+ let trust = reconDisocclusion(
585
+ expected,
586
+ held,
587
+ length(vec2<f32>(mu, mv)),
588
+ normalDot,
589
+ here.slope + swung,
590
+ );
554
591
  out.weight = out.colour.w * trust;
555
592
  return out;
556
593
  }
557
594
 
558
- /* One output pixel: the history's next colour, before sharpening, and the weight it has gathered. */
559
- fn reconResolvePixel(ox: i32, oy: i32) -> vec4<f32> {
595
+ /* temporalAa.ts's repeats: whether two periods' means are the same, one to zero. */
596
+ fn reconRepeats(mean: f32, previous: f32) -> f32 {
597
+ let apart = abs(mean - previous) / max(max(mean, previous), 1.0 / 255.0);
598
+ return 1.0 - reconSmoothstep(RECON_FLICKER_REPEAT, 2.0 * RECON_FLICKER_REPEAT, apart);
599
+ }
600
+
601
+ struct ReconResolved {
602
+ /* The history's next colour, before sharpening, and the weight it has gathered. */
603
+ colour: vec4<f32>,
604
+ /* The pixel's next flicker record: spread, this period's sum, last period's mean, repeats. */
605
+ record: vec4<f32>,
606
+ /* And its next held depth swing. */
607
+ swing: f32,
608
+ }
609
+
610
+ /* One output pixel: the history's next colour and weight, and its next flicker record. */
611
+ fn reconResolvePixel(ox: i32, oy: i32) -> ReconResolved {
560
612
  let rw = i32(recon.renderSize.x);
561
613
  let rh = i32(recon.renderSize.y);
562
614
  let ow = i32(recon.outputSize.x);
@@ -604,18 +656,68 @@ fn reconResolvePixel(ox: i32, oy: i32) -> vec4<f32> {
604
656
  accum = accum / gathered;
605
657
 
606
658
  /* The history, and the weight it still deserves; a tenth of each frame is always new. */
607
- let cap = (1.0 - recon.alpha) / recon.alpha;
608
- let history = reconHistoryWeight(u, v, dx, dy);
659
+ var swing = 0.0;
660
+ if (recon.antiFlicker != 0u) { swing = reconSwing(ox, oy); }
661
+ let history = reconHistoryWeight(u, v, dx, dy, swing);
662
+ var cap = (1.0 - recon.alpha) / recon.alpha;
663
+ var low = box.low;
664
+ var high = box.high;
665
+ var out: ReconResolved;
666
+ out.record = vec4<f32>(0.0);
667
+ out.swing = 0.0;
668
+ /* The anti-flicker, operation for operation resolve.ts's: see temporalAa.ts. */
669
+ if (recon.antiFlicker != 0u) {
670
+ let seen = dot(accum, vec3<f32>(0.2126, 0.7152, 0.0722));
671
+ let memory = recon.flickerMemory;
672
+ let width = recon.flickerWidth;
673
+ var held = vec4<f32>(0.0, seen * memory, seen, 0.0);
674
+ if (history.weight > 0.0) { held = reconFlicker(ox, oy); }
675
+ var still = 0.0;
676
+ if (history.weight > 0.0) {
677
+ still = 1.0 - reconSmoothstep(RECON_STILL_FROM, RECON_STILL_TO, history.moved);
678
+ }
679
+ let apart = abs(seen - held.z);
680
+ let within = apart <= width * held.x + 1.0 / 255.0;
681
+ let proven = held.w > 0.75 && within;
682
+ var widen = 0.0;
683
+ let base = 1.0 - recon.alpha;
684
+ var keep = base;
685
+ if (proven) {
686
+ widen = width * held.x * still;
687
+ keep = base + (RECON_FLICKER_BLEND - base) * still;
688
+ }
689
+ low = low - vec3<f32>(widen);
690
+ high = high + vec3<f32>(widen);
691
+ cap = keep / (1.0 - keep);
692
+ var sum = held.y;
693
+ var mean = held.z;
694
+ var repeated = 0.0;
695
+ if (within) { repeated = held.w; }
696
+ if (recon.periodStart != 0u) {
697
+ if (reconRepeats(sum, mean) > 0.5) { repeated = min(1.0, repeated + 0.5); } else { repeated = 0.0; }
698
+ mean = sum;
699
+ sum = 0.0;
700
+ }
701
+ out.record = vec4<f32>(
702
+ held.x + (apart - held.x) * memory,
703
+ sum + seen * memory,
704
+ mean,
705
+ repeated * still,
706
+ );
707
+ /* The peak carried on, halving over a period, whatever the test said. */
708
+ if (history.missed >= 0.0) { out.swing = max(history.missed, swing * pow(0.5, memory)); }
709
+ }
609
710
  let kept = min(cap, history.weight);
610
711
 
611
712
  let total = kept + gathered;
612
713
  var mixed = accum;
613
714
  if (kept > 0.0) {
614
- let clipped = reconClipToBox(reconToYCoCg(history.colour.xyz), box.low, box.high);
715
+ let clipped = reconClipToBox(reconToYCoCg(history.colour.xyz), low, high);
615
716
  mixed = reconAccumulate(accum, reconFromYCoCg(clipped), 1.0, gathered / total);
616
717
  }
617
718
  let known = min(1.0, total / RECON_CONFIDENT_WEIGHT);
618
- return vec4<f32>(sample * (1.0 - known) + mixed * known, total);
719
+ out.colour = vec4<f32>(sample * (1.0 - known) + mixed * known, total);
720
+ return out;
619
721
  }
620
722
 
621
723
  /*
@@ -655,8 +757,9 @@ fn reconSharpenAt(ox: i32, oy: i32) -> vec3<f32> {
655
757
  * doing its own bilinear so the comparison is arithmetic rather than a tolerance on the device's
656
758
  * sampler.
657
759
  *
658
- * Two entry points over one set of bindings. `resolveMain` writes four floats a pixel — the next
659
- * history — and `sharpenMain` reads that history back at binding 4 and writes three.
760
+ * Two entry points over one set of bindings. `resolveMain` writes nine floats a pixel — the next
761
+ * history, the next flicker record and the next depth swing — and `sharpenMain` reads that history back at binding 4
762
+ * and writes three.
660
763
  */
661
764
  export function reconResolveParityWgsl(): string {
662
765
  return /* wgsl */ `
@@ -668,6 +771,16 @@ export function reconResolveParityWgsl(): string {
668
771
  ${RECON_PARAMS_WGSL}
669
772
  @group(0) @binding(5) var<uniform> recon: ReconParams;
670
773
  @group(0) @binding(6) var<storage, read_write> output: array<f32>;
774
+ @group(0) @binding(7) var flickerTexture: texture_2d<f32>;
775
+ @group(0) @binding(8) var swingTexture: texture_2d<f32>;
776
+
777
+ fn reconFlicker(x: i32, y: i32) -> vec4<f32> {
778
+ return textureLoad(flickerTexture, vec2<i32>(x, y), 0);
779
+ }
780
+
781
+ fn reconSwing(x: i32, y: i32) -> f32 {
782
+ return textureLoad(swingTexture, vec2<i32>(x, y), 0).x;
783
+ }
671
784
 
672
785
  fn reconScene(x: i32, y: i32) -> vec3<f32> {
673
786
  return textureLoad(sceneTexture, vec2<i32>(x, y), 0).xyz;
@@ -725,11 +838,16 @@ ${RECON_RESOLVE_CORE_WGSL}
725
838
  fn resolveMain(@builtin(global_invocation_id) id: vec3<u32>) {
726
839
  if (id.x >= recon.outputSize.x || id.y >= recon.outputSize.y) { return; }
727
840
  let value = reconResolvePixel(i32(id.x), i32(id.y));
728
- let at = (id.y * recon.outputSize.x + id.x) * 4u;
729
- output[at] = value.x;
730
- output[at + 1u] = value.y;
731
- output[at + 2u] = value.z;
732
- output[at + 3u] = value.w;
841
+ let at = (id.y * recon.outputSize.x + id.x) * 9u;
842
+ output[at] = value.colour.x;
843
+ output[at + 1u] = value.colour.y;
844
+ output[at + 2u] = value.colour.z;
845
+ output[at + 3u] = value.colour.w;
846
+ output[at + 4u] = value.record.x;
847
+ output[at + 5u] = value.record.y;
848
+ output[at + 6u] = value.record.z;
849
+ output[at + 7u] = value.record.w;
850
+ output[at + 8u] = value.swing;
733
851
  }
734
852
 
735
853
  @compute @workgroup_size(${RECON_WORKGROUP}, ${RECON_WORKGROUP})
@@ -780,6 +898,12 @@ ${RECON_PARAMS_WGSL}
780
898
  @group(0) @binding(6) var<uniform> recon: ReconParams;
781
899
  @group(0) @binding(7) var resolved: texture_storage_2d<rgba16float, write>;
782
900
  /* Not "target": it is a reserved keyword in WGSL, as "meta" was in the cluster cull. */
901
+ /* Last frame's flicker records, and where this frame's go: a pair beside the histories. */
902
+ @group(0) @binding(8) var flickerTexture: texture_2d<f32>;
903
+ @group(0) @binding(9) var flickerOut: texture_storage_2d<rgba16float, write>;
904
+ /* And each pixel's held depth swing, a pair of its own. */
905
+ @group(0) @binding(10) var swingTexture: texture_2d<f32>;
906
+ @group(0) @binding(11) var swingOut: texture_storage_2d<r32float, write>;
783
907
 
784
908
  /* Rows flip here: the reference's row 0 is the bottom one and a texture's is the top. */
785
909
  fn reconRenderRow(y: i32) -> i32 {
@@ -812,6 +936,14 @@ fn reconHistory(x: i32, y: i32) -> vec4<f32> {
812
936
  return textureLoad(historyTexture, vec2<i32>(x, reconOutputRow(y)), 0);
813
937
  }
814
938
 
939
+ fn reconFlicker(x: i32, y: i32) -> vec4<f32> {
940
+ return textureLoad(flickerTexture, vec2<i32>(x, reconOutputRow(y)), 0);
941
+ }
942
+
943
+ fn reconSwing(x: i32, y: i32) -> f32 {
944
+ return textureLoad(swingTexture, vec2<i32>(x, reconOutputRow(y)), 0).x;
945
+ }
946
+
815
947
  /* A texel coordinate becomes the uv of its centre, with the row taken from the other end. */
816
948
  fn reconSceneBilinear(p: vec2<f32>) -> vec3<f32> {
817
949
  let size = vec2<f32>(f32(recon.renderSize.x), f32(recon.renderSize.y));
@@ -831,7 +963,9 @@ fn resolveMain(@builtin(global_invocation_id) id: vec3<u32>) {
831
963
  if (id.x >= recon.outputSize.x || id.y >= recon.outputSize.y) { return; }
832
964
  let y = i32(recon.outputSize.y) - 1 - i32(id.y);
833
965
  let value = reconResolvePixel(i32(id.x), y);
834
- textureStore(resolved, vec2<i32>(i32(id.x), i32(id.y)), value);
966
+ textureStore(resolved, vec2<i32>(i32(id.x), i32(id.y)), value.colour);
967
+ textureStore(flickerOut, vec2<i32>(i32(id.x), i32(id.y)), value.record);
968
+ textureStore(swingOut, vec2<i32>(i32(id.x), i32(id.y)), vec4<f32>(value.swing, 0.0, 0.0, 0.0));
835
969
  }
836
970
 
837
971
  @compute @workgroup_size(${RECON_WORKGROUP}, ${RECON_WORKGROUP})
@@ -104,6 +104,16 @@ uniform float uMotionMax;
104
104
  */
105
105
  uniform sampler2D uAo;
106
106
  uniform float uAoStrength;
107
+ /**
108
+ * Where this frame's jitter put the picture the occlusion was measured from, in this pass's uv.
109
+ *
110
+ * The occlusion is measured from the frame's own depth, which a temporal resolve or a
111
+ * reconstruction jittered, and it is applied here to a picture those have already taken the jitter
112
+ * out of — so read at the pixel's own uv it moved by the jitter every frame, and every crevice
113
+ * shimmered by up to half a render texel. Read where the jitter put it, it holds still. Zero for an
114
+ * unjittered frame.
115
+ */
116
+ uniform vec2 uAoOffset;
107
117
 
108
118
  /**
109
119
  * The bloom chain's top level, already thresholded, blurred and summed by its own pass.
@@ -341,7 +351,7 @@ void main() {
341
351
  * strength, so the parameter reads as "how much of this occlusion", and 0 is exactly the
342
352
  * frame that existed before the effect did.
343
353
  */
344
- float ao = mix(1.0, textureLod(uAo, vUv, 0.0).r, uAoStrength);
354
+ float ao = mix(1.0, textureLod(uAo, vUv + uAoOffset, 0.0).r, uAoStrength);
345
355
 
346
356
  /*
347
357
  * Distance from the centre, corrected so the falloff is a circle on screen rather than
@@ -30,7 +30,8 @@ precision highp float;
30
30
 
31
31
  in vec2 vUv;
32
32
 
33
- uniform sampler2D uAtlas;
33
+ /* An array of one: every surface texture is an array, and an atlas is read at layer 0. */
34
+ uniform mediump sampler2DArray uAtlas;
34
35
  uniform vec3 uColor;
35
36
  uniform float uOpacity;
36
37
  /* Texels the field spans, from the atlas metrics. Scales the antialiasing band. */
@@ -52,7 +53,7 @@ float median(vec3 rgb) {
52
53
  }
53
54
 
54
55
  void main() {
55
- vec3 sampled = textureLod(uAtlas, vUv, 0.0).rgb;
56
+ vec3 sampled = textureLod(uAtlas, vec3(vUv, 0.0), 0.0).rgb;
56
57
  float distance = median(sampled) - 0.5;
57
58
 
58
59
  /*
@@ -27,7 +27,7 @@ uniform vec2 uViewport;
27
27
  /** Where the string's baseline-left sits, in pixels from the top left. */
28
28
  uniform vec2 uOrigin;
29
29
  uniform float uCellSize;
30
- /** Distance from the virtual camera; larger flattens the perspective. */
30
+ /** How far each cell is seen from, head-on; larger flattens the perspective. */
31
31
  uniform float uDepth;
32
32
 
33
33
  /** 0 before a character has arrived, 1 once it has settled. */
@@ -105,14 +105,23 @@ void main() {
105
105
  vec2 cell = aCell * uCellSize;
106
106
  float bob = sin(uTime * 3.1 + aCharIndex * 0.7) * uBob;
107
107
 
108
- // Pixels, y down from the top-left corner of the viewport.
109
- vec2 screen = uOrigin + vec2(cell.x, -cell.y) + vec2(0.0, rise - bob);
110
- vec3 view = vec3(screen + local3.xy, -uDepth + local3.z);
111
-
112
- // A hand-built perspective divide. The near plane is the viewport itself, so
113
- // a cell at depth uDepth is exactly its pixel size on screen.
114
- float invDepth = uDepth / max(0.001, -view.z);
115
- vec2 ndc = ((view.xy * invDepth) / uViewport) * 2.0 - 1.0;
108
+ // The cell's centre in pixels, y down from the top-left corner of the viewport.
109
+ vec2 centre = uOrigin + vec2(cell.x, -cell.y) + vec2(0.0, rise - bob);
110
+
111
+ // A hand-built perspective divide about the cell's own centre, so every cube
112
+ // is seen head-on: its front face at depth zero is exactly its cell, and at
113
+ // rest its sides are behind it wherever on the screen it is drawn.
114
+ //
115
+ // It divided about the viewport's top-left corner until 2026-09-30, which
116
+ // shifted a front face by its distance from that corner times z / uDepth and
117
+ // showed the side it uncovered. Near the corner that was nothing; across the
118
+ // screen it widened every stroke by up to a pixel either side and closed the
119
+ // one-cell gaps a 5x7 face reads by. What this gives up is the extruded look
120
+ // text far from the corner had at rest; what would make it wrong is a caller
121
+ // wanting that look, which is then one offset for every cell, not a position.
122
+ float invDepth = uDepth / max(0.001, uDepth - local3.z);
123
+ vec2 screen = centre + local3.xy * invDepth;
124
+ vec2 ndc = (screen / uViewport) * 2.0 - 1.0;
116
125
  // Depth from the cube's own extrusion, mapped into a narrow slice of the
117
126
  // range so a message sorts against itself without ever reaching the scene.
118
127
  float depth = clamp(-local3.z / max(1.0, uCellSize * 2.0), -1.0, 1.0) * 0.4;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * What each layer of a surface array does beyond its picture: rooms behind its windows, windows that
3
+ * light by night, wear, animation, and whether rain wets it.
4
+ *
5
+ * **Per layer, because a city region is one mesh.** Its facades are told apart by the layer each
6
+ * vertex carries (`MeshData.layers`), so an effect's parameters cannot be a per-draw uniform — two facades of one
7
+ * draw would share them. They are a table beside the array, six texels a layer, read at the vertex's
8
+ * layer with `texelFetch`, and they bind whenever the array does.
9
+ *
10
+ * **Everything off is all zeros**, so a layer a scene names no effect for — and every layer of an
11
+ * array given no table — costs the shader one size query and nothing else.
12
+ *
13
+ * Layout, six RGBA texels a layer, half floats on the GPU (every whole number here is under 2,048,
14
+ * which a half holds exactly):
15
+ * - 0: window cells across and up, glass mask (1: glass where the albedo's alpha is 0), seed;
16
+ * - 1: room layer (0: none; the layer plus one), room depth (0 to 1 of a cell), share of rooms lit,
17
+ * window glow;
18
+ * - 2: window colour, then 1 where rain leaves the surface dry;
19
+ * - 3: dust, grime, streaks, the metres over which wear fades out with distance (0: never);
20
+ * - 4: scroll along u and v in repeats a second, pulse rate in hertz, pulse depth;
21
+ * - 5: flicker, flipbook frames, flipbook frames a second, the metres past which emission fades
22
+ * (0: never).
23
+ */
24
+
25
+ /** Texels a layer takes in the table. */
26
+ export const SURFACE_EFFECT_TEXELS = 6;
27
+
28
+ export interface SurfaceLayerEffect {
29
+ /**
30
+ * Windows across and up one repeat of the layer, for everything that works window by window:
31
+ * rooms, lit windows. With `glass`, a window is a hole in the albedo — glass where its alpha is 0,
32
+ * wall where it is 1 — which is how a facade is drawn, and the other way round would lose the
33
+ * wall's colour, which does not survive an upload at alpha 0.
34
+ */
35
+ readonly windows?: {
36
+ readonly cells: readonly [number, number];
37
+ readonly glass?: boolean;
38
+ /** Varies which windows light and which rooms show, between facades wearing one layer. */
39
+ readonly seed?: number;
40
+ /** How bright a lit window glows, 0 for none. */
41
+ readonly glow?: number;
42
+ /**
43
+ * The light a window shows where there is no room behind it. **A vertex that names an emissive
44
+ * colour overrides it** — the window's colour, or a tint on the room where there is one — which
45
+ * is how buildings sharing a layer each keep their own light: a window layer's vertex emissive
46
+ * is otherwise unused, so the colour there, strength folded in, is free to say it.
47
+ */
48
+ readonly colour?: readonly [number, number, number];
49
+ };
50
+ /** A room seen in parallax behind each window, drawn from another layer of the same array. */
51
+ readonly interior?: {
52
+ readonly roomLayer: number;
53
+ /** How deep a room is, as a share of a window's width. 0.5 unless stated. */
54
+ readonly depth?: number;
55
+ /** The share of rooms lit, 0 to 1. 1 unless stated. */
56
+ readonly lit?: number;
57
+ };
58
+ /** Procedural wear, each 0 to 1, fading out past `fade` metres (never, unless stated). */
59
+ readonly wear?: {
60
+ readonly dust?: number;
61
+ readonly grime?: number;
62
+ readonly streaks?: number;
63
+ readonly fade?: number;
64
+ };
65
+ /** Motion on the caller's clock (`Environment.surfaceTime`), so a held clock holds it. */
66
+ readonly animation?: {
67
+ /** Repeats a second along u and v. */
68
+ readonly scroll?: readonly [number, number];
69
+ /** Emission rising and falling: hertz, and how far it falls, 0 to 1. */
70
+ readonly pulse?: readonly [number, number];
71
+ /** Emission stuttering, 0 to 1. */
72
+ readonly flicker?: number;
73
+ /** Successive layers as frames: how many from this one, and how fast. */
74
+ readonly flipbook?: readonly [number, number];
75
+ /** Metres past which emission fades out, 0 for never. */
76
+ readonly fade?: number;
77
+ };
78
+ /** Rain leaves it dry: a covered walkway, an interior floor. */
79
+ readonly dry?: boolean;
80
+ }
81
+
82
+ /**
83
+ * The table for `effects`, one entry a layer (`undefined` for a layer with none), as floats: six
84
+ * RGBA texels a layer, a row a layer.
85
+ */
86
+ export function packSurfaceEffects(
87
+ effects: readonly (SurfaceLayerEffect | undefined)[],
88
+ ): Float32Array {
89
+ const out = new Float32Array(effects.length * SURFACE_EFFECT_TEXELS * 4);
90
+ effects.forEach((effect, layer) => {
91
+ if (effect === undefined) return;
92
+ const at = layer * SURFACE_EFFECT_TEXELS * 4;
93
+ const windows = effect.windows;
94
+ if (windows !== undefined) {
95
+ out[at] = windows.cells[0];
96
+ out[at + 1] = windows.cells[1];
97
+ out[at + 2] = windows.glass === true ? 1 : 0;
98
+ out[at + 3] = windows.seed ?? 0;
99
+ out[at + 7] = windows.glow ?? 0;
100
+ const colour = windows.colour ?? [1, 0.82, 0.55];
101
+ out[at + 8] = colour[0];
102
+ out[at + 9] = colour[1];
103
+ out[at + 10] = colour[2];
104
+ }
105
+ const interior = effect.interior;
106
+ if (interior !== undefined) {
107
+ out[at + 4] = interior.roomLayer + 1;
108
+ out[at + 5] = interior.depth ?? 0.5;
109
+ out[at + 6] = interior.lit ?? 1;
110
+ }
111
+ out[at + 11] = effect.dry === true ? 1 : 0;
112
+ const wear = effect.wear;
113
+ if (wear !== undefined) {
114
+ out[at + 12] = wear.dust ?? 0;
115
+ out[at + 13] = wear.grime ?? 0;
116
+ out[at + 14] = wear.streaks ?? 0;
117
+ out[at + 15] = wear.fade ?? 0;
118
+ }
119
+ const animation = effect.animation;
120
+ if (animation !== undefined) {
121
+ out[at + 16] = animation.scroll?.[0] ?? 0;
122
+ out[at + 17] = animation.scroll?.[1] ?? 0;
123
+ out[at + 18] = animation.pulse?.[0] ?? 0;
124
+ out[at + 19] = animation.pulse?.[1] ?? 0;
125
+ out[at + 20] = animation.flicker ?? 0;
126
+ out[at + 21] = animation.flipbook?.[0] ?? 0;
127
+ out[at + 22] = animation.flipbook?.[1] ?? 0;
128
+ out[at + 23] = animation.fade ?? 0;
129
+ }
130
+ });
131
+ return out;
132
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The scene's surface clock and weather, decided once for both backends: the four numbers the lit
3
+ * stage reads as `uSurfaceScene`. Each backend only uploads them, so the two cannot disagree about
4
+ * what a wetness of 1.4 means — it means 1, as a share of lit windows past one means all of them.
5
+ */
6
+ import type { Environment } from './backend/webgl2/renderer.ts';
7
+
8
+ const unit = (value: number | undefined): number => Math.min(1, Math.max(0, value ?? 0));
9
+
10
+ /** Fill `out`: seconds on the caller's clock, wetness, the lit share, the late share. */
11
+ export function resolveSurfaceScene(env: Environment, out: Float32Array): void {
12
+ out[0] = env.surfaceTime ?? 0;
13
+ out[1] = unit(env.wetness);
14
+ out[2] = unit(env.litWindows);
15
+ out[3] = unit(env.lateWindows);
16
+ }