@driftengine/core 4.4.1 → 4.5.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 (218) hide show
  1. package/CHANGELOG.json +72 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -1
  4. package/dist/index.js +7 -0
  5. package/dist/render/backend/api.d.ts +17 -11
  6. package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
  7. package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
  8. package/dist/render/backend/webgl2/renderer.d.ts +82 -8
  9. package/dist/render/backend/webgl2/renderer.js +373 -105
  10. package/dist/render/backend/webgpu/buffers.d.ts +19 -1
  11. package/dist/render/backend/webgpu/buffers.js +27 -1
  12. package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
  13. package/dist/render/backend/webgpu/depthPass.js +2 -1
  14. package/dist/render/backend/webgpu/flatPass.d.ts +2 -0
  15. package/dist/render/backend/webgpu/flatPass.js +20 -1
  16. package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
  17. package/dist/render/backend/webgpu/glassTintPass.js +205 -0
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +60 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +305 -5
  20. package/dist/render/backend/webgpu/instanced.d.ts +12 -1
  21. package/dist/render/backend/webgpu/instanced.js +20 -1
  22. package/dist/render/backend/webgpu/latePass.d.ts +26 -0
  23. package/dist/render/backend/webgpu/latePass.js +133 -0
  24. package/dist/render/backend/webgpu/lightVolumePass.js +5 -1
  25. package/dist/render/backend/webgpu/motionPass.d.ts +95 -0
  26. package/dist/render/backend/webgpu/motionPass.js +491 -0
  27. package/dist/render/backend/webgpu/pipelineCache.d.ts +10 -1
  28. package/dist/render/backend/webgpu/pipelineCache.js +11 -1
  29. package/dist/render/backend/webgpu/pointGlassTintPass.d.ts +32 -0
  30. package/dist/render/backend/webgpu/pointGlassTintPass.js +193 -0
  31. package/dist/render/backend/webgpu/pointShadowPass.d.ts +35 -6
  32. package/dist/render/backend/webgpu/pointShadowPass.js +105 -20
  33. package/dist/render/backend/webgpu/postPass.d.ts +21 -15
  34. package/dist/render/backend/webgpu/postPass.js +17 -4
  35. package/dist/render/backend/webgpu/renderer.d.ts +181 -54
  36. package/dist/render/backend/webgpu/renderer.js +1011 -394
  37. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  38. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  39. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  40. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  41. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  42. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -0
  43. package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
  44. package/dist/render/flameLight.js +15 -2
  45. package/dist/render/glass.d.ts +45 -0
  46. package/dist/render/glass.js +39 -0
  47. package/dist/render/glassCasters.d.ts +40 -0
  48. package/dist/render/glassCasters.js +84 -0
  49. package/dist/render/glassShadow.d.ts +78 -0
  50. package/dist/render/glassShadow.js +125 -0
  51. package/dist/render/globalMediumPass.d.ts +2 -3
  52. package/dist/render/globalMediumPass.js +2 -8
  53. package/dist/render/gpudriven/lit.d.ts +12 -0
  54. package/dist/render/gpudriven/lit.js +23 -0
  55. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  56. package/dist/render/gpudriven/materialTable.js +26 -6
  57. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  58. package/dist/render/gpudriven/pipeline.js +4 -3
  59. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  60. package/dist/render/gpudriven/sunGlass.js +89 -0
  61. package/dist/render/lightBudget.d.ts +17 -5
  62. package/dist/render/lightBudget.js +17 -5
  63. package/dist/render/pointGlassTint.d.ts +59 -0
  64. package/dist/render/pointGlassTint.js +198 -0
  65. package/dist/render/pointShadowArray.d.ts +56 -3
  66. package/dist/render/pointShadowArray.js +127 -16
  67. package/dist/render/pointShadowBudget.js +51 -41
  68. package/dist/render/pointShadowMap.d.ts +23 -1
  69. package/dist/render/pointShadowMap.js +27 -1
  70. package/dist/render/recon/changeFrames.d.ts +15 -0
  71. package/dist/render/recon/changeFrames.js +21 -0
  72. package/dist/render/recon/disocclusion.d.ts +36 -3
  73. package/dist/render/recon/disocclusion.js +56 -4
  74. package/dist/render/recon/lateRouting.d.ts +14 -0
  75. package/dist/render/recon/lateRouting.js +18 -0
  76. package/dist/render/recon/mover.d.ts +57 -0
  77. package/dist/render/recon/mover.js +86 -0
  78. package/dist/render/recon/resolve.js +95 -12
  79. package/dist/render/renderQuality.d.ts +35 -0
  80. package/dist/render/renderQuality.js +7 -0
  81. package/dist/render/rendererHarness.js +12 -0
  82. package/dist/render/sceneTarget.d.ts +9 -1
  83. package/dist/render/sceneTarget.js +59 -13
  84. package/dist/render/shaders/depth.d.ts +16 -1
  85. package/dist/render/shaders/depth.js +85 -9
  86. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  87. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  88. package/dist/render/shaders/flat/glassTint.js +25 -0
  89. package/dist/render/shaders/flat/index.d.ts +13 -0
  90. package/dist/render/shaders/flat/index.js +4 -0
  91. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  92. package/dist/render/shaders/flat/lampGlass.js +183 -0
  93. package/dist/render/shaders/flat/main.d.ts +1 -1
  94. package/dist/render/shaders/flat/main.js +115 -15
  95. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  96. package/dist/render/shaders/flat/pointShadow.js +2 -0
  97. package/dist/render/shaders/flat/preamble.js +21 -6
  98. package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
  99. package/dist/render/shaders/generated/depth.wgsl.js +227 -4
  100. package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
  101. package/dist/render/shaders/generated/flat.wgsl.js +376 -328
  102. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  103. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  104. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +4 -14
  106. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  107. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  108. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
  109. package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
  110. package/dist/render/shaders/globalMedium.js +6 -6
  111. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  112. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  113. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  114. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  115. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  116. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  117. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  118. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  119. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  120. package/dist/render/shaders/lightVolume.js +6 -6
  121. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  122. package/dist/render/shaders/octahedralResolve.js +48 -17
  123. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  124. package/dist/render/shaders/recon/lateDepth.wgsl.js +44 -0
  125. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  126. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  127. package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
  128. package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
  129. package/dist/render/shaders/temporalResolve.js +133 -5
  130. package/dist/render/shadowCasters.d.ts +20 -1
  131. package/dist/render/shadowCasters.js +15 -1
  132. package/dist/render/shadowMap.d.ts +54 -5
  133. package/dist/render/shadowMap.js +190 -36
  134. package/dist/render/sunGlassTint.d.ts +25 -0
  135. package/dist/render/sunGlassTint.js +79 -0
  136. package/dist/render/temporalAa.d.ts +145 -0
  137. package/dist/render/temporalAa.js +190 -0
  138. package/dist/render/temporalPass.d.ts +16 -14
  139. package/dist/render/temporalPass.js +59 -14
  140. package/dist/render/translucentQueue.d.ts +6 -0
  141. package/dist/render/translucentQueue.js +15 -1
  142. package/package.json +6 -6
  143. package/src/index.ts +10 -0
  144. package/src/render/backend/api.ts +15 -9
  145. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  146. package/src/render/backend/webgl2/renderer.ts +422 -109
  147. package/src/render/backend/webgpu/buffers.ts +50 -2
  148. package/src/render/backend/webgpu/depthPass.ts +2 -1
  149. package/src/render/backend/webgpu/flatPass.ts +22 -1
  150. package/src/render/backend/webgpu/glassTintPass.ts +244 -0
  151. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  152. package/src/render/backend/webgpu/instanced.ts +21 -1
  153. package/src/render/backend/webgpu/latePass.ts +152 -0
  154. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  155. package/src/render/backend/webgpu/motionPass.ts +535 -0
  156. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  157. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  158. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  159. package/src/render/backend/webgpu/postPass.ts +34 -4
  160. package/src/render/backend/webgpu/renderer.ts +1178 -425
  161. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  162. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  163. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  164. package/src/render/backend/webgpu/surfaceTexturePass.ts +1 -1
  165. package/src/render/flameLight.ts +15 -2
  166. package/src/render/glass.ts +74 -0
  167. package/src/render/glassCasters.ts +164 -0
  168. package/src/render/glassShadow.ts +153 -0
  169. package/src/render/globalMediumPass.ts +4 -11
  170. package/src/render/gpudriven/lit.ts +31 -0
  171. package/src/render/gpudriven/materialTable.ts +28 -6
  172. package/src/render/gpudriven/pipeline.ts +4 -3
  173. package/src/render/gpudriven/sunGlass.ts +117 -0
  174. package/src/render/lightBudget.ts +19 -5
  175. package/src/render/pointGlassTint.ts +250 -0
  176. package/src/render/pointShadowArray.ts +188 -22
  177. package/src/render/pointShadowBudget.ts +49 -39
  178. package/src/render/pointShadowMap.ts +41 -0
  179. package/src/render/recon/changeFrames.ts +24 -0
  180. package/src/render/recon/disocclusion.ts +54 -3
  181. package/src/render/recon/lateRouting.ts +29 -0
  182. package/src/render/recon/mover.ts +118 -0
  183. package/src/render/recon/resolve.ts +137 -10
  184. package/src/render/renderQuality.ts +47 -0
  185. package/src/render/rendererHarness.ts +12 -0
  186. package/src/render/sceneTarget.ts +66 -12
  187. package/src/render/shaders/depth.ts +109 -9
  188. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  189. package/src/render/shaders/flat/glassTint.ts +25 -0
  190. package/src/render/shaders/flat/index.ts +17 -0
  191. package/src/render/shaders/flat/lampGlass.ts +185 -0
  192. package/src/render/shaders/flat/main.ts +115 -15
  193. package/src/render/shaders/flat/pointShadow.ts +2 -0
  194. package/src/render/shaders/flat/preamble.ts +21 -6
  195. package/src/render/shaders/generated/depth.wgsl.ts +234 -4
  196. package/src/render/shaders/generated/flat.wgsl.ts +376 -328
  197. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  198. package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
  199. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  200. package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
  201. package/src/render/shaders/globalMedium.ts +6 -6
  202. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  203. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  204. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  205. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  206. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  207. package/src/render/shaders/lightVolume.ts +6 -6
  208. package/src/render/shaders/octahedralResolve.ts +50 -17
  209. package/src/render/shaders/recon/lateDepth.wgsl.ts +47 -0
  210. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  211. package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
  212. package/src/render/shaders/temporalResolve.ts +141 -5
  213. package/src/render/shadowCasters.ts +28 -1
  214. package/src/render/shadowMap.ts +231 -37
  215. package/src/render/sunGlassTint.ts +86 -0
  216. package/src/render/temporalAa.ts +227 -0
  217. package/src/render/temporalPass.ts +88 -12
  218. package/src/render/translucentQueue.ts +20 -1
@@ -1,6 +1,7 @@
1
- /** A GPU-driven material as the shading pass reads it: five blocks, one of them integers. */
1
+ /** A GPU-driven material as the shading pass reads it: seven blocks, one of them integers. */
2
2
 
3
3
  import type { GpuDrivenMaterial } from '../backend/webgpu/gpuDrivenPass.ts';
4
+ import { resolveGlass, type ResolvedGlass } from '../glass.ts';
4
5
  import { DECODE_NO_PROGRAM } from '../shaders/gpudriven/decode.wgsl.ts';
5
6
  import type { GpuDrivenProgram } from './decodeTables.ts';
6
7
 
@@ -34,17 +35,18 @@ export interface GpuDrivenTextures {
34
35
 
35
36
  /**
36
37
  * Words a material entry: tint and emissive, the surface scalars, four program indices, the UV
37
- * scale and two strengths, the two ORM scales with the cutout and blend flags, and
38
- * the opacity with three spare.
38
+ * scale and two strengths, the two ORM scales with the cutout and blend flags, the opacity with the
39
+ * glass's transmission and frost and one spare, and the glass's tint with one spare.
39
40
  *
40
41
  * **It was eight until 2026-09-17**, and metalness took its last spare lane the same day, so the
41
42
  * programs and the scales needed three more blocks rather than squeezing into what was left. It
42
43
  * became twenty-four on 2026-09-18, when transparency arrived and there was nowhere left to put an
43
44
  * opacity: the cutout took lane 18 and the blend flag took 19, and lane 11 is a reserved program
44
45
  * slot rather than a gap. A sixteen-byte block is 1 kB across the whole table, against the 64 kB a
45
- * uniform buffer may hold.
46
+ * uniform buffer may hold. **Twenty-eight on 2026-09-29**, for glass: its transmission and frost
47
+ * took two of the opacity block's spares and its tint needed a block of its own.
46
48
  */
47
- export const GPU_DRIVEN_MATERIAL_FLOATS = 24;
49
+ export const GPU_DRIVEN_MATERIAL_FLOATS = 28;
48
50
 
49
51
  /**
50
52
  * Below this base-colour alpha the raster discards a fragment. Zero means no test.
@@ -74,6 +76,16 @@ export const MATERIAL_BLEND = 19;
74
76
  */
75
77
  export const MATERIAL_OPACITY = 20;
76
78
 
79
+ /**
80
+ * Glass (`glass.ts`): how much light passes, and how milky it is. Zero transmission is not glass,
81
+ * and a material that is glass is drawn in the blended half whatever `blend` says — a visibility
82
+ * buffer holds one surface a pixel, and glass is two.
83
+ */
84
+ export const MATERIAL_GLASS_TRANSMISSION = 21;
85
+ export const MATERIAL_GLASS_FROST = 22;
86
+ /** The colour light takes through the glass: three lanes, white where the material is not glass. */
87
+ export const MATERIAL_GLASS_TINT = 24;
88
+
77
89
  /**
78
90
  * Where a material's emissive program is: the fourth program lane, which the table reserved and
79
91
  * initialised to none before anything used it.
@@ -155,10 +167,20 @@ export function writeMaterialTable(
155
167
  float(m, 17, textures?.metallicScale ?? 1);
156
168
  /* Zero is off, so a material that says nothing about cutout writes the table it wrote before. */
157
169
  float(m, MATERIAL_ALPHA_CUTOFF, material.alphaCutoff ?? 0);
170
+ /* Glass A's clamps and its test of what is glass at all, so both pipelines agree on it. */
171
+ const glass = resolveGlass(material.glass, glassScratch);
158
172
  /* Zero is opaque, so a material that says nothing about blending stays in the opaque half. */
159
- float(m, MATERIAL_BLEND, material.blend === true ? 1 : 0);
173
+ float(m, MATERIAL_BLEND, material.blend === true || glass ? 1 : 0);
160
174
  /* One, so a material that asked to blend and said nothing else is as solid as an opaque one. */
161
175
  float(m, MATERIAL_OPACITY, material.opacity ?? 1);
176
+ float(m, MATERIAL_GLASS_TRANSMISSION, glassScratch.transmission);
177
+ float(m, MATERIAL_GLASS_FROST, glassScratch.frost);
178
+ float(m, MATERIAL_GLASS_TINT, glassScratch.tint[0]);
179
+ float(m, MATERIAL_GLASS_TINT + 1, glassScratch.tint[1]);
180
+ float(m, MATERIAL_GLASS_TINT + 2, glassScratch.tint[2]);
162
181
  });
163
182
  return buffer;
164
183
  }
184
+
185
+ /** One resolution every material's glass is read through. */
186
+ const glassScratch: ResolvedGlass = { transmission: 0, frost: 0, tint: [1, 1, 1] };
@@ -132,11 +132,11 @@ export const GPU_DRIVEN_PASSES = [
132
132
  'phaseTwoCull',
133
133
  'phaseTwoDraw',
134
134
  'blendCull',
135
- 'blendDraw',
136
135
  'visibilityCopy',
137
136
  'bin',
138
137
  'cursorCopy',
139
138
  'shade',
139
+ 'blendDraw',
140
140
  'blendResolve',
141
141
  ] as const;
142
142
 
@@ -230,10 +230,11 @@ function edgesFor(res: GpuDrivenResources): {
230
230
  /*
231
231
  * **It reads the depth the opaque half wrote and does not write it.** A pane behind a wall is
232
232
  * rejected at the wall; nothing here writes depth, so two panes do not hide each other and
233
- * there is something left to blend.
233
+ * there is something left to blend. **And it reads the shaded colour**, because glass shows the
234
+ * opaque picture behind it — which is why it is recorded after `shade` rather than before.
234
235
  */
235
236
  blendDraw: [
236
- [res.blendList, res.clusters, res.depth],
237
+ [res.blendList, res.clusters, res.depth, res.colour],
237
238
  [res.oitAccum, res.oitReveal],
238
239
  ],
239
240
  visibilityCopy: [[res.visibility], [res.visibilityCopy]],
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The second pipeline's sunlight through glass, as a reference: `glassTintAt` over the glass depth
3
+ * and tint maps the GPU-driven pass draws from the sun, for `shade.wgsl.ts`'s lookup to be read
4
+ * against.
5
+ *
6
+ * **The forward path's rule, not a second one** (`glassShadow.ts`, and `sunGlassLookup` in
7
+ * `flat/directionalShadow.ts`): the outline from taps at the base filter radius that count a pane
8
+ * only where the receiver is behind it, and the colour from spread taps read coarse, unmixed from
9
+ * the ground around the pane by its clarity and weighed by its share. Frost widens only the
10
+ * colour's taps, so the light a frosted pane passes is the light a clear one does.
11
+ *
12
+ * **What differs is where two numbers come from.** A compute invocation has no derivatives, so the
13
+ * map's scale in metres is the caller's (`uvPerMetre`, which an orthographic light matrix states
14
+ * outright); and a coarse level here is a box average over the spread's footprint, which is what a
15
+ * mip level approximates on the device. `scripts/gpu-parity.mjs` holds the device's per-tap
16
+ * arithmetic to `glassShadow.ts`; the sampling is what the glass page measures.
17
+ */
18
+ import { frostRadius, spreadTint, unmixTint, type GlassTexel } from '../glassShadow.ts';
19
+ import { SHADOW_DEPTH_BIAS, SHADOW_PCF_OFFSETS, SHADOW_TEXEL_SCALE } from './shadow.ts';
20
+
21
+ /** The two maps the sun's glass pass draws, square, row-major from v = 0. */
22
+ export interface SunGlassMaps {
23
+ readonly size: number;
24
+ /** The nearest pane's depth a texel, 1 where no pane was drawn. */
25
+ readonly glassDepth: Float32Array;
26
+ /** What the panes on a texel's ray let through, and their clarity: four floats a texel. */
27
+ readonly tint: Float32Array;
28
+ }
29
+
30
+ export interface SunGlassSettings {
31
+ /** Metres the light's depth range spans, which turns a depth difference into a distance. */
32
+ readonly depthSpan: number;
33
+ /** Map units a metre across the light, which turns a spread in metres into one on the map. */
34
+ readonly uvPerMetre: number;
35
+ readonly taps: number;
36
+ }
37
+
38
+ const centre: GlassTexel = { r: 1, g: 1, b: 1, clarity: 1 };
39
+ const coarse: GlassTexel = { r: 1, g: 1, b: 1, clarity: 1 };
40
+ const pane: GlassTexel = { r: 1, g: 1, b: 1, clarity: 1 };
41
+ const found: GlassTexel = { r: 0, g: 0, b: 0, clarity: 0 };
42
+
43
+ function index(maps: SunGlassMaps, u: number, v: number): number {
44
+ const x = Math.min(maps.size - 1, Math.max(0, Math.floor(u * maps.size)));
45
+ const y = Math.min(maps.size - 1, Math.max(0, Math.floor(v * maps.size)));
46
+ return y * maps.size + x;
47
+ }
48
+
49
+ /** The average of every texel within `radius` of (u, v): a mip level's footprint, in the large. */
50
+ function coarseAt(maps: SunGlassMaps, u: number, v: number, radius: number, out: GlassTexel): void {
51
+ const reach = radius * maps.size;
52
+ const x0 = Math.max(0, Math.floor(u * maps.size - reach));
53
+ const x1 = Math.min(maps.size - 1, Math.floor(u * maps.size + reach));
54
+ const y0 = Math.max(0, Math.floor(v * maps.size - reach));
55
+ const y1 = Math.min(maps.size - 1, Math.floor(v * maps.size + reach));
56
+ out.r = 0;
57
+ out.g = 0;
58
+ out.b = 0;
59
+ out.clarity = 0;
60
+ let n = 0;
61
+ for (let y = y0; y <= y1; y++) {
62
+ for (let x = x0; x <= x1; x++) {
63
+ const at = (y * maps.size + x) * 4;
64
+ out.r += maps.tint[at] as number;
65
+ out.g += maps.tint[at + 1] as number;
66
+ out.b += maps.tint[at + 2] as number;
67
+ out.clarity += maps.tint[at + 3] as number;
68
+ n++;
69
+ }
70
+ }
71
+ out.r /= n;
72
+ out.g /= n;
73
+ out.b /= n;
74
+ out.clarity /= n;
75
+ }
76
+
77
+ /** What the sun keeps through glass at a receiver at map position (u, v) and light depth `depth`. */
78
+ export function glassTintAt(
79
+ u: number,
80
+ v: number,
81
+ depth: number,
82
+ maps: SunGlassMaps,
83
+ settings: SunGlassSettings,
84
+ out: GlassTexel,
85
+ ): GlassTexel {
86
+ out.r = 1;
87
+ out.g = 1;
88
+ out.b = 1;
89
+ out.clarity = 1;
90
+ if (u < 0 || u > 1 || v < 0 || v > 1) return out;
91
+ const texel = SHADOW_TEXEL_SCALE / maps.size;
92
+ const bias = SHADOW_DEPTH_BIAS / settings.depthSpan;
93
+ const taps = Math.min(settings.taps, SHADOW_PCF_OFFSETS.length);
94
+
95
+ const at = index(maps, u, v);
96
+ centre.clarity = maps.tint[at * 4 + 3] as number;
97
+ const behind = Math.max(depth - (maps.glassDepth[at] as number), 0) * settings.depthSpan;
98
+ const radius = frostRadius(centre.clarity, behind * settings.uvPerMetre, texel);
99
+
100
+ let cover = 0;
101
+ found.r = 0;
102
+ found.g = 0;
103
+ found.b = 0;
104
+ let shares = 0;
105
+ for (let i = 0; i < taps; i++) {
106
+ const offset = SHADOW_PCF_OFFSETS[i] as readonly [number, number];
107
+ const nearest = maps.glassDepth[index(maps, u + offset[0] * texel, v + offset[1] * texel)];
108
+ if (depth > (nearest as number) + bias) cover++;
109
+ coarseAt(maps, u + offset[0] * radius, v + offset[1] * radius, radius, coarse);
110
+ const share = unmixTint(coarse, centre.clarity, pane);
111
+ found.r += pane.r * share;
112
+ found.g += pane.g * share;
113
+ found.b += pane.b * share;
114
+ shares += share;
115
+ }
116
+ return spreadTint(cover / Math.max(taps, 1), found, shares, out);
117
+ }
@@ -38,13 +38,14 @@ export const MAX_POINT_LIGHTS = 16;
38
38
  export const LIVE_POINT_SHADOW_MAPS = 2;
39
39
 
40
40
  /**
41
- * Texture units the directional cascade binds: static, its depth peel, and dynamic.
41
+ * Texture units the sun's shadow binds: one, for its static, moving and peeled layers.
42
42
  *
43
- * Named because the shading pass's total unit count has to be compared against what a
44
- * device offers, and a budget assembled from three literals scattered across two files
45
- * is a budget nobody can check. See the guard in `Renderer`'s constructor.
43
+ * **It was three**, a `sampler2D` per map, until 4.5.0 folded them into one `sampler2DArray`
44
+ * (`shadowMap.ts`) the way the lamps' twelve cubemaps became one — which is where the two tints
45
+ * glass casts with found their units. Named because the shading pass's total has to be compared
46
+ * against what a device offers; see the guard in `Renderer`'s constructor.
46
47
  */
47
- export const DIRECTIONAL_SHADOW_UNITS = 3;
48
+ export const DIRECTIONAL_SHADOW_UNITS = 1;
48
49
 
49
50
  /**
50
51
  * Texture units every point light's shadow binds: one, for all of them.
@@ -230,6 +231,19 @@ export const REFRACT_SCENE_TEXTURE_UNIT = COOKIE_ATLAS_TEXTURE_UNIT + 1;
230
231
  export const DRIFT_LIGHT_INDEX_TEXTURE_UNIT = REFRACT_SCENE_TEXTURE_UNIT + 1;
231
232
  export const DRIFT_LIGHT_ATLAS_TEXTURE_UNIT = DRIFT_LIGHT_INDEX_TEXTURE_UNIT + 1;
232
233
 
234
+ /**
235
+ * What the sun keeps through glass (`sunGlassTint.ts`), the lit program's fourteenth fragment
236
+ * sampler: the unit the sun's three maps gave back when they became one array.
237
+ */
238
+ export const SUN_GLASS_TINT_TEXTURE_UNIT = DRIFT_LIGHT_ATLAS_TEXTURE_UNIT + 1;
239
+
240
+ /**
241
+ * What each lamp keeps through glass (`pointGlassTint.ts`), the lit program's fifteenth fragment
242
+ * sampler and the last one that leaves a sixteenth spare. Unit sixteen of the combined pool, which
243
+ * WebGL2 guarantees thirty-two of; what a stage may declare is the sixteen, and this is fifteen.
244
+ */
245
+ export const POINT_GLASS_TINT_TEXTURE_UNIT = SUN_GLASS_TINT_TEXTURE_UNIT + 1;
246
+
233
247
  /**
234
248
  * A cookie's tile, in texels a side.
235
249
  *
@@ -0,0 +1,250 @@
1
+ import { mat3 } from 'gl-matrix';
2
+
3
+ import { createEmptyTexture2D } from './emptyTexture.ts';
4
+ import { pointShadowFaceRotation } from './pointShadowImage.ts';
5
+ import { compileProgram, uniformLocations } from './shader.ts';
6
+ import {
7
+ OCTAHEDRAL_RESOLVE_TINT_FRAG,
8
+ OCTAHEDRAL_RESOLVE_VERT,
9
+ } from './shaders/octahedralResolve.ts';
10
+
11
+ /** Filled per resolved face, at module scope, because a bake runs inside the frame's budget. */
12
+ const faceRotation = mat3.create();
13
+ const CLEAR = new Float32Array([1, 1, 1, 1]);
14
+
15
+ /**
16
+ * What a lamp's light keeps through glass: an RGBA8 array of one layer a light, parallel to the
17
+ * point shadow array and indexed by the light, not by the interleaved depth layer. RGB is the
18
+ * product of every pane's `transmission × (1 − F) × tint`, A the product of their clarities
19
+ * (`glassShadow.ts`).
20
+ *
21
+ * **Drawn a face at a time, as the depth is**: into a face-sized colour scratch cleared to white,
22
+ * with a multiplying blend and no depth test, then resolved into the face's region of the layer by
23
+ * `OCTAHEDRAL_RESOLVE_TINT_FRAG`. One scratch for the whole engine, for the reason
24
+ * `PointShadowArray` gives for its own.
25
+ *
26
+ * **Which faces hold glass is remembered, six bits a layer**, so a face with no glass and none to
27
+ * forget costs nothing, and a face that inherited a previous owner's glass is cleared. **The chain
28
+ * frost reads is rebuilt once a round**, for the layers the round touched, through a 2D staging
29
+ * chain: `generateMipmap` on an array rebuilds every layer, which is the whole pool's texels for one
30
+ * lamp's glass. What that costs is one layer of memory with its chain — 5.6 MB at 1024, 1.4 MB at
31
+ * `'half'` — and a copy out and back per touched layer.
32
+ */
33
+ export class PointGlassTint {
34
+ readonly texture: WebGLTexture;
35
+ readonly edge: number;
36
+
37
+ private readonly layers: number;
38
+ private readonly levels: number;
39
+ private readonly faceSize: number;
40
+ private readonly scratch: WebGLTexture;
41
+ private readonly scratchFramebuffer: WebGLFramebuffer;
42
+ private readonly staging: WebGLTexture;
43
+ private readonly framebuffer: WebGLFramebuffer;
44
+ private readonly readFramebuffer: WebGLFramebuffer;
45
+ /** Parked on the resolve's unit while the scratch is a render target; see `beginFace`. */
46
+ private readonly parked: WebGLTexture;
47
+ private readonly program: WebGLProgram;
48
+ private readonly uniforms: Record<string, WebGLUniformLocation>;
49
+ private readonly vao: WebGLVertexArrayObject;
50
+ /** Six bits a layer: which faces' regions hold glass. */
51
+ private readonly heldFaces: Uint8Array;
52
+ /** Layers whose level 0 changed since the chain was last rebuilt. */
53
+ private readonly dirty: Uint8Array;
54
+
55
+ constructor(gl: WebGL2RenderingContext, faceSize: number, edge: number, layers: number) {
56
+ this.edge = edge;
57
+ this.layers = layers;
58
+ this.faceSize = faceSize;
59
+ this.levels = Math.floor(Math.log2(Math.max(1, edge))) + 1;
60
+ this.heldFaces = new Uint8Array(layers);
61
+ this.dirty = new Uint8Array(layers);
62
+
63
+ this.texture = colourTexture(gl, gl.TEXTURE_2D_ARRAY, this.levels, edge, layers);
64
+ this.scratch = colourTexture(gl, gl.TEXTURE_2D, 1, faceSize, 1);
65
+ this.staging = colourTexture(gl, gl.TEXTURE_2D, this.levels, edge, 1);
66
+ this.parked = createEmptyTexture2D(gl);
67
+ this.scratchFramebuffer = framebufferOf(gl);
68
+ this.framebuffer = framebufferOf(gl);
69
+ this.readFramebuffer = framebufferOf(gl);
70
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.scratchFramebuffer);
71
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.scratch, 0);
72
+
73
+ this.program = compileProgram(
74
+ gl,
75
+ OCTAHEDRAL_RESOLVE_VERT,
76
+ OCTAHEDRAL_RESOLVE_TINT_FRAG,
77
+ 'octahedralResolveTint',
78
+ );
79
+ this.uniforms = uniformLocations(gl, this.program, 'octahedralResolveTint');
80
+ const vao = gl.createVertexArray();
81
+ if (vao === null) throw new Error('PointGlassTint: createVertexArray failed');
82
+ this.vao = vao;
83
+
84
+ /* Every layer born clear: a light no pane has reached lets everything through. */
85
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
86
+ for (let layer = 0; layer < layers; layer++) {
87
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, this.texture, 0, layer);
88
+ gl.clearBufferfv(gl.COLOR, 0, CLEAR);
89
+ }
90
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
91
+ /* Once, for every layer: white all the way down. */
92
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
93
+ gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
94
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
95
+ }
96
+
97
+ /** Whether `face` of `layer` holds glass: a face that does must be cleared before it is reused. */
98
+ held(layer: number, face: number): boolean {
99
+ return (((this.heldFaces[layer] ?? 0) >> face) & 1) === 1;
100
+ }
101
+
102
+ /**
103
+ * Bind the colour scratch, clear it to white, and set the multiplying state.
104
+ *
105
+ * The previous resolve left the scratch bound for sampling on unit 0, and it is about to be the
106
+ * target: parked first, for the reason `PointShadowArray.beginFace` gives.
107
+ */
108
+ beginFace(gl: WebGL2RenderingContext): void {
109
+ gl.activeTexture(gl.TEXTURE0);
110
+ gl.bindTexture(gl.TEXTURE_2D, this.parked);
111
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.scratchFramebuffer);
112
+ gl.viewport(0, 0, this.faceSize, this.faceSize);
113
+ gl.clearBufferfv(gl.COLOR, 0, CLEAR);
114
+ gl.disable(gl.DEPTH_TEST);
115
+ gl.depthMask(false);
116
+ gl.enable(gl.BLEND);
117
+ gl.blendEquation(gl.FUNC_ADD);
118
+ gl.blendFuncSeparate(gl.DST_COLOR, gl.ZERO, gl.DST_ALPHA, gl.ZERO);
119
+ }
120
+
121
+ /**
122
+ * Copy the scratch into `face`'s region of `layer`, and remember whether it held glass.
123
+ *
124
+ * Leaves depth testing and writing on, which is what the next face's casters expect: the depth
125
+ * resolve that runs before them turns both on, and this one ran after it and turned them off.
126
+ */
127
+ resolveFace(gl: WebGL2RenderingContext, layer: number, face: number, drawn: boolean): void {
128
+ gl.disable(gl.BLEND);
129
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
130
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, this.texture, 0, layer);
131
+ gl.viewport(0, 0, this.edge, this.edge);
132
+ gl.useProgram(this.program);
133
+ gl.bindVertexArray(this.vao);
134
+ gl.activeTexture(gl.TEXTURE0);
135
+ gl.bindTexture(gl.TEXTURE_2D, this.scratch);
136
+ gl.uniform1i(this.uniforms['uFace'] ?? null, 0);
137
+ gl.uniform1i(this.uniforms['uFaceIndex'] ?? null, face);
138
+ gl.uniformMatrix3fv(
139
+ this.uniforms['uFaceRotation'] ?? null,
140
+ false,
141
+ pointShadowFaceRotation(face, faceRotation),
142
+ );
143
+ gl.uniform1f(this.uniforms['uEdge'] ?? null, this.edge);
144
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
145
+ gl.bindVertexArray(null);
146
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
147
+ gl.enable(gl.DEPTH_TEST);
148
+ gl.depthMask(true);
149
+
150
+ const bit = 1 << face;
151
+ const was = this.heldFaces[layer] ?? 0;
152
+ this.heldFaces[layer] = drawn ? was | bit : was & ~bit;
153
+ this.dirty[layer] = 1;
154
+ }
155
+
156
+ /** Rebuild the chain of every layer touched since the last call, one layer at a time. */
157
+ fillMips(gl: WebGL2RenderingContext): void {
158
+ for (let layer = 0; layer < this.layers; layer++) {
159
+ if (this.dirty[layer] !== 1) continue;
160
+ this.dirty[layer] = 0;
161
+ /* Level 0 out into the staging chain, which mipmaps as one 2D texture. */
162
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.readFramebuffer);
163
+ gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, this.texture, 0, layer);
164
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer);
165
+ gl.framebufferTexture2D(
166
+ gl.DRAW_FRAMEBUFFER,
167
+ gl.COLOR_ATTACHMENT0,
168
+ gl.TEXTURE_2D,
169
+ this.staging,
170
+ 0,
171
+ );
172
+ blit(gl, this.edge, this.edge);
173
+ gl.bindTexture(gl.TEXTURE_2D, this.staging);
174
+ gl.generateMipmap(gl.TEXTURE_2D);
175
+ gl.bindTexture(gl.TEXTURE_2D, null);
176
+ /* And every level after it back into the layer. */
177
+ for (let level = 1; level < this.levels; level++) {
178
+ const size = Math.max(1, this.edge >> level);
179
+ gl.framebufferTexture2D(
180
+ gl.READ_FRAMEBUFFER,
181
+ gl.COLOR_ATTACHMENT0,
182
+ gl.TEXTURE_2D,
183
+ this.staging,
184
+ level,
185
+ );
186
+ gl.framebufferTextureLayer(
187
+ gl.DRAW_FRAMEBUFFER,
188
+ gl.COLOR_ATTACHMENT0,
189
+ this.texture,
190
+ level,
191
+ layer,
192
+ );
193
+ blit(gl, size, size);
194
+ }
195
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
196
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
197
+ }
198
+ }
199
+
200
+ dispose(gl: WebGL2RenderingContext): void {
201
+ gl.deleteFramebuffer(this.scratchFramebuffer);
202
+ gl.deleteFramebuffer(this.framebuffer);
203
+ gl.deleteFramebuffer(this.readFramebuffer);
204
+ gl.deleteTexture(this.texture);
205
+ gl.deleteTexture(this.scratch);
206
+ gl.deleteTexture(this.staging);
207
+ gl.deleteTexture(this.parked);
208
+ gl.deleteProgram(this.program);
209
+ gl.deleteVertexArray(this.vao);
210
+ }
211
+ }
212
+
213
+ /** A colour texture with `levels` levels: nearest for a single level, trilinear for a chain. */
214
+ function colourTexture(
215
+ gl: WebGL2RenderingContext,
216
+ target: number,
217
+ levels: number,
218
+ edge: number,
219
+ layers: number,
220
+ ): WebGLTexture {
221
+ const texture = gl.createTexture();
222
+ if (texture === null) throw new Error('PointGlassTint: createTexture failed');
223
+ gl.bindTexture(target, texture);
224
+ if (target === gl.TEXTURE_2D_ARRAY) {
225
+ gl.texStorage3D(target, levels, gl.RGBA8, edge, edge, layers);
226
+ } else {
227
+ gl.texStorage2D(target, levels, gl.RGBA8, edge, edge);
228
+ }
229
+ gl.texParameteri(
230
+ target,
231
+ gl.TEXTURE_MIN_FILTER,
232
+ levels > 1 ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST,
233
+ );
234
+ gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, levels > 1 ? gl.LINEAR : gl.NEAREST);
235
+ gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
236
+ gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
237
+ gl.bindTexture(target, null);
238
+ return texture;
239
+ }
240
+
241
+ function framebufferOf(gl: WebGL2RenderingContext): WebGLFramebuffer {
242
+ const framebuffer = gl.createFramebuffer();
243
+ if (framebuffer === null) throw new Error('PointGlassTint: createFramebuffer failed');
244
+ return framebuffer;
245
+ }
246
+
247
+ /** The bound read framebuffer's colour into the bound draw framebuffer's, texel for texel. */
248
+ function blit(gl: WebGL2RenderingContext, width: number, height: number): void {
249
+ gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST);
250
+ }