@driftengine/core 4.4.2 → 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 (193) hide show
  1. package/CHANGELOG.json +46 -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 +79 -8
  9. package/dist/render/backend/webgl2/renderer.js +360 -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 +2 -12
  34. package/dist/render/backend/webgpu/renderer.d.ts +171 -54
  35. package/dist/render/backend/webgpu/renderer.js +944 -390
  36. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  37. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  38. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  39. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  40. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  41. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -0
  42. package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
  43. package/dist/render/glass.d.ts +45 -0
  44. package/dist/render/glass.js +39 -0
  45. package/dist/render/glassCasters.d.ts +40 -0
  46. package/dist/render/glassCasters.js +84 -0
  47. package/dist/render/glassShadow.d.ts +78 -0
  48. package/dist/render/glassShadow.js +125 -0
  49. package/dist/render/globalMediumPass.d.ts +2 -3
  50. package/dist/render/globalMediumPass.js +2 -8
  51. package/dist/render/gpudriven/lit.d.ts +12 -0
  52. package/dist/render/gpudriven/lit.js +23 -0
  53. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  54. package/dist/render/gpudriven/materialTable.js +26 -6
  55. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  56. package/dist/render/gpudriven/pipeline.js +4 -3
  57. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  58. package/dist/render/gpudriven/sunGlass.js +89 -0
  59. package/dist/render/lightBudget.d.ts +17 -5
  60. package/dist/render/lightBudget.js +17 -5
  61. package/dist/render/pointGlassTint.d.ts +59 -0
  62. package/dist/render/pointGlassTint.js +198 -0
  63. package/dist/render/pointShadowArray.d.ts +56 -3
  64. package/dist/render/pointShadowArray.js +127 -16
  65. package/dist/render/pointShadowMap.d.ts +23 -1
  66. package/dist/render/pointShadowMap.js +27 -1
  67. package/dist/render/recon/changeFrames.d.ts +15 -0
  68. package/dist/render/recon/changeFrames.js +21 -0
  69. package/dist/render/recon/lateRouting.d.ts +14 -0
  70. package/dist/render/recon/lateRouting.js +18 -0
  71. package/dist/render/recon/mover.d.ts +57 -0
  72. package/dist/render/recon/mover.js +86 -0
  73. package/dist/render/renderQuality.d.ts +35 -0
  74. package/dist/render/renderQuality.js +7 -0
  75. package/dist/render/rendererHarness.js +12 -0
  76. package/dist/render/sceneTarget.d.ts +7 -1
  77. package/dist/render/sceneTarget.js +58 -12
  78. package/dist/render/shaders/depth.d.ts +16 -1
  79. package/dist/render/shaders/depth.js +85 -9
  80. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  81. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  82. package/dist/render/shaders/flat/glassTint.js +25 -0
  83. package/dist/render/shaders/flat/index.d.ts +13 -0
  84. package/dist/render/shaders/flat/index.js +4 -0
  85. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  86. package/dist/render/shaders/flat/lampGlass.js +183 -0
  87. package/dist/render/shaders/flat/main.d.ts +1 -1
  88. package/dist/render/shaders/flat/main.js +115 -15
  89. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  90. package/dist/render/shaders/flat/pointShadow.js +2 -0
  91. package/dist/render/shaders/flat/preamble.js +21 -6
  92. package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
  93. package/dist/render/shaders/generated/depth.wgsl.js +227 -4
  94. package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
  95. package/dist/render/shaders/generated/flat.wgsl.js +376 -328
  96. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  97. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  98. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  99. package/dist/render/shaders/generated/lightVolume.wgsl.js +4 -14
  100. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  101. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  102. package/dist/render/shaders/globalMedium.js +6 -6
  103. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  104. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  105. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  106. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  107. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  108. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  109. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  110. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  111. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  112. package/dist/render/shaders/lightVolume.js +6 -6
  113. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  114. package/dist/render/shaders/octahedralResolve.js +48 -17
  115. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  116. package/dist/render/shaders/recon/lateDepth.wgsl.js +44 -0
  117. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  118. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  119. package/dist/render/shadowCasters.d.ts +20 -1
  120. package/dist/render/shadowCasters.js +15 -1
  121. package/dist/render/shadowMap.d.ts +54 -5
  122. package/dist/render/shadowMap.js +190 -36
  123. package/dist/render/sunGlassTint.d.ts +25 -0
  124. package/dist/render/sunGlassTint.js +79 -0
  125. package/dist/render/translucentQueue.d.ts +6 -0
  126. package/dist/render/translucentQueue.js +15 -1
  127. package/package.json +6 -6
  128. package/src/index.ts +10 -0
  129. package/src/render/backend/api.ts +15 -9
  130. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  131. package/src/render/backend/webgl2/renderer.ts +401 -108
  132. package/src/render/backend/webgpu/buffers.ts +50 -2
  133. package/src/render/backend/webgpu/depthPass.ts +2 -1
  134. package/src/render/backend/webgpu/flatPass.ts +22 -1
  135. package/src/render/backend/webgpu/glassTintPass.ts +244 -0
  136. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  137. package/src/render/backend/webgpu/instanced.ts +21 -1
  138. package/src/render/backend/webgpu/latePass.ts +152 -0
  139. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  140. package/src/render/backend/webgpu/motionPass.ts +535 -0
  141. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  142. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  143. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  144. package/src/render/backend/webgpu/renderer.ts +1108 -421
  145. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  146. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  147. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  148. package/src/render/backend/webgpu/surfaceTexturePass.ts +1 -1
  149. package/src/render/glass.ts +74 -0
  150. package/src/render/glassCasters.ts +164 -0
  151. package/src/render/glassShadow.ts +153 -0
  152. package/src/render/globalMediumPass.ts +4 -11
  153. package/src/render/gpudriven/lit.ts +31 -0
  154. package/src/render/gpudriven/materialTable.ts +28 -6
  155. package/src/render/gpudriven/pipeline.ts +4 -3
  156. package/src/render/gpudriven/sunGlass.ts +117 -0
  157. package/src/render/lightBudget.ts +19 -5
  158. package/src/render/pointGlassTint.ts +250 -0
  159. package/src/render/pointShadowArray.ts +188 -22
  160. package/src/render/pointShadowMap.ts +41 -0
  161. package/src/render/recon/changeFrames.ts +24 -0
  162. package/src/render/recon/lateRouting.ts +29 -0
  163. package/src/render/recon/mover.ts +118 -0
  164. package/src/render/renderQuality.ts +47 -0
  165. package/src/render/rendererHarness.ts +12 -0
  166. package/src/render/sceneTarget.ts +58 -11
  167. package/src/render/shaders/depth.ts +109 -9
  168. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  169. package/src/render/shaders/flat/glassTint.ts +25 -0
  170. package/src/render/shaders/flat/index.ts +17 -0
  171. package/src/render/shaders/flat/lampGlass.ts +185 -0
  172. package/src/render/shaders/flat/main.ts +115 -15
  173. package/src/render/shaders/flat/pointShadow.ts +2 -0
  174. package/src/render/shaders/flat/preamble.ts +21 -6
  175. package/src/render/shaders/generated/depth.wgsl.ts +234 -4
  176. package/src/render/shaders/generated/flat.wgsl.ts +376 -328
  177. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  178. package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
  179. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  180. package/src/render/shaders/globalMedium.ts +6 -6
  181. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  182. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  183. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  184. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  185. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  186. package/src/render/shaders/lightVolume.ts +6 -6
  187. package/src/render/shaders/octahedralResolve.ts +50 -17
  188. package/src/render/shaders/recon/lateDepth.wgsl.ts +47 -0
  189. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  190. package/src/render/shadowCasters.ts +28 -1
  191. package/src/render/shadowMap.ts +231 -37
  192. package/src/render/sunGlassTint.ts +86 -0
  193. package/src/render/translucentQueue.ts +20 -1
@@ -83,6 +83,90 @@ fn motionUv(clip: vec4<f32>) -> vec2<f32> {
83
83
  return clip.xy / clip.w * 0.5 + vec2<f32>(0.5);
84
84
  }
85
85
 
86
+ /*
87
+ * The skinned stage: the joints this frame and last frame, read from two palette textures of the
88
+ * frame's ring. The same four weighted products in the same order as the scene's own skinning, so
89
+ * the depth this stage produces is the depth the scene wrote.
90
+ */
91
+ @group(1) @binding(0) var palette: texture_2d<f32>;
92
+ @group(1) @binding(1) var previousPalette: texture_2d<f32>;
93
+
94
+ fn jointOf(source: texture_2d<f32>, index: i32) -> mat4x4<f32> {
95
+ let x = index * 4;
96
+ return mat4x4<f32>(
97
+ textureLoad(source, vec2<i32>(x, 0), 0),
98
+ textureLoad(source, vec2<i32>(x + 1, 0), 0),
99
+ textureLoad(source, vec2<i32>(x + 2, 0), 0),
100
+ textureLoad(source, vec2<i32>(x + 3, 0), 0));
101
+ }
102
+
103
+ fn skinOf(source: texture_2d<f32>, joints: vec4<f32>, weights: vec4<f32>) -> mat4x4<f32> {
104
+ return jointOf(source, i32(joints.x)) * weights.x
105
+ + jointOf(source, i32(joints.y)) * weights.y
106
+ + jointOf(source, i32(joints.z)) * weights.z
107
+ + jointOf(source, i32(joints.w)) * weights.w;
108
+ }
109
+
110
+ @vertex
111
+ fn motionSkinnedVert(
112
+ @location(0) position: vec3<f32>,
113
+ @location(11) joints: vec4<f32>,
114
+ @location(12) weights: vec4<f32>,
115
+ ) -> Varyings {
116
+ let world = draw.model * (skinOf(palette, joints, weights) * vec4<f32>(position, 1.0));
117
+ let wasWorld =
118
+ draw.previousModel * (skinOf(previousPalette, joints, weights) * vec4<f32>(position, 1.0));
119
+ var out: Varyings;
120
+ out.clip = frame.raster * world;
121
+ out.now = frame.viewProj * world;
122
+ out.was = frame.previousViewProj * wasWorld;
123
+ return out;
124
+ }
125
+
126
+ /*
127
+ * The rewritten stage: a mesh whose vertices were rewritten this frame reads last frame's rows from
128
+ * a second buffer of the same stride, so each vertex carries both of its positions.
129
+ */
130
+ @vertex
131
+ fn motionDynamicVert(
132
+ @location(0) position: vec3<f32>,
133
+ @location(1) previousPosition: vec3<f32>,
134
+ ) -> Varyings {
135
+ let world = draw.model * vec4<f32>(position, 1.0);
136
+ let wasWorld = draw.previousModel * vec4<f32>(previousPosition, 1.0);
137
+ var out: Varyings;
138
+ out.clip = frame.raster * world;
139
+ out.now = frame.viewProj * world;
140
+ out.was = frame.previousViewProj * wasWorld;
141
+ return out;
142
+ }
143
+
144
+ /*
145
+ * The instanced stage: each instance's matrix this frame and last frame, read from the batch's
146
+ * placement and the copy of last frame's it kept. Slot for slot, so an instance is paired with
147
+ * itself; the per-draw matrices are not read.
148
+ */
149
+ @vertex
150
+ fn motionInstancedVert(
151
+ @location(0) position: vec3<f32>,
152
+ @location(2) m0: vec4<f32>,
153
+ @location(3) m1: vec4<f32>,
154
+ @location(4) m2: vec4<f32>,
155
+ @location(5) m3: vec4<f32>,
156
+ @location(6) p0: vec4<f32>,
157
+ @location(7) p1: vec4<f32>,
158
+ @location(8) p2: vec4<f32>,
159
+ @location(9) p3: vec4<f32>,
160
+ ) -> Varyings {
161
+ let world = mat4x4<f32>(m0, m1, m2, m3) * vec4<f32>(position, 1.0);
162
+ let wasWorld = mat4x4<f32>(p0, p1, p2, p3) * vec4<f32>(position, 1.0);
163
+ var out: Varyings;
164
+ out.clip = frame.raster * world;
165
+ out.now = frame.viewProj * world;
166
+ out.was = frame.previousViewProj * wasWorld;
167
+ return out;
168
+ }
169
+
86
170
  @fragment
87
171
  fn motionFrag(in: Varyings) -> @location(0) vec4<f32> {
88
172
  /*
@@ -1,3 +1,4 @@
1
+ import { type GlassOptions, type ResolvedGlass } from './glass.ts';
1
2
  import type { ReadonlyMat4 } from 'gl-matrix';
2
3
  import type { InstanceData } from './instancedMesh.ts';
3
4
  import type { InstancedHandle, MeshHandle, ScatterHandle } from './backend/api.ts';
@@ -40,7 +41,25 @@ import type { SurfaceTextureHandle } from './backend/api.ts';
40
41
  * outwards, so a required parameter added in a minor stops every consumer's sink and every test
41
42
  * double compiling. A sink that ignores it behaves exactly as it did.
42
43
  */
43
- export type SceneCasterMaterial = SurfaceMaterial<SurfaceTextureHandle> | null;
44
+ export type SceneCasterMaterial = (SurfaceMaterial<SurfaceTextureHandle> & {
45
+ /**
46
+ * The caster is glass: it lets light through, so it writes no depth into any shadow map and a
47
+ * lamp behind it lights what is beyond. The colour replay into a mirror or a probe still draws
48
+ * it. With `RenderQuality.glassShadows` on it casts a coloured shadow instead: see
49
+ * `glassShadow.ts`.
50
+ */
51
+ readonly glass?: GlassOptions;
52
+ }) | null;
53
+ /**
54
+ * Whether a caster writes depth into a shadow map: everything but glass. One line in each depth
55
+ * sink, so the answer is the same in every shadow a backend draws.
56
+ */
57
+ export declare function castsDepth(material: SceneCasterMaterial | undefined): boolean;
58
+ /**
59
+ * The caster's glass, resolved into `out`, and whether it is glass at all — the rule `castsDepth`
60
+ * states from the other side, so a caster is exactly one of the two.
61
+ */
62
+ export declare function glassOf(material: SceneCasterMaterial | undefined, out: ResolvedGlass): boolean;
44
63
  export interface ShadowCasterSink {
45
64
  /** A rigid mesh at a model transform: the world, a prop, a static piece of scenery. */
46
65
  mesh(mesh: MeshHandle, model: ReadonlyMat4, material?: SceneCasterMaterial): void;
@@ -1 +1,15 @@
1
- export {};
1
+ import { resolveGlass } from './glass.js';
2
+ /**
3
+ * Whether a caster writes depth into a shadow map: everything but glass. One line in each depth
4
+ * sink, so the answer is the same in every shadow a backend draws.
5
+ */
6
+ export function castsDepth(material) {
7
+ return (material?.glass?.transmission ?? 0) <= 0;
8
+ }
9
+ /**
10
+ * The caster's glass, resolved into `out`, and whether it is glass at all — the rule `castsDepth`
11
+ * states from the other side, so a caster is exactly one of the two.
12
+ */
13
+ export function glassOf(material, out) {
14
+ return resolveGlass(material?.glass, out);
15
+ }
@@ -1,10 +1,59 @@
1
- export declare class ShadowMap {
1
+ /** Where each of the sun's maps lives in the array. The shaders read these same numbers. */
2
+ export declare const SUN_STATIC_LAYER = 0;
3
+ export declare const SUN_DYNAMIC_LAYER = 1;
4
+ /** Present only when more than one static depth layer is asked for. */
5
+ export declare const SUN_PEELED_LAYER = 2;
6
+ /**
7
+ * Where the nearest pane's depth lives once glass has cast: after the three maps, at fixed places,
8
+ * because a shader cannot ask an array how many layers it has here — naga refuses the third
9
+ * component of an arrayed size query. A profile with no peel grows past an unused third layer to
10
+ * reach them, which is one map's memory in a profile that already declined one.
11
+ */
12
+ export declare const SUN_GLASS_STATIC_LAYER = 3;
13
+ export declare const SUN_GLASS_MOVING_LAYER = 4;
14
+ /** The three passes a scene opens, by the names `beginShadowPass` takes. */
15
+ export type SunShadowLayer = 'static' | 'static-peel' | 'dynamic';
16
+ /** The array layer a pass draws into. */
17
+ export declare function sunLayerIndex(layer: SunShadowLayer): number;
18
+ export declare class SunShadowArray {
2
19
  readonly size: number;
3
- readonly texture: WebGLTexture;
20
+ /** Replaced once, when glass first casts: see `growForGlass`. */
21
+ private current;
22
+ private layerCount;
23
+ private glass;
4
24
  private readonly framebuffer;
5
- constructor(gl: WebGL2RenderingContext, size?: number);
6
- /** Bind as the render target and clear. Caller then draws the casters. */
7
- begin(gl: WebGL2RenderingContext): void;
25
+ /**
26
+ * **The peel is drawn here and copied into its layer when the pass ends.** It reads the static
27
+ * layer while it is drawn, and WebGL2 calls a texture both sampled and attached a feedback loop
28
+ * whatever layer each names — so drawing the peel into the array would draw nothing at all.
29
+ * What it costs is one more map's memory where a peel is asked for, and one blit per peel.
30
+ */
31
+ private readonly peelTexture;
32
+ private readonly peelFramebuffer;
33
+ private peeling;
34
+ constructor(gl: WebGL2RenderingContext, size?: number, peeled?: boolean);
35
+ get texture(): WebGLTexture;
36
+ get layers(): number;
37
+ /** Whether the two glass layers exist: from the first glass a sun pass is offered, for good. */
38
+ get hasGlass(): boolean;
39
+ /** The glass depth layer beside a map: fixed places after the three maps. */
40
+ glassLayer(which: 'static' | 'dynamic'): number;
41
+ /**
42
+ * Add the two glass layers, once, carrying every layer already held across.
43
+ *
44
+ * **Carried rather than re-baked**, because the sun's static map is the consumer's to bake and
45
+ * it may bake it once: the glass that grows the array arrives in the very pass that drew that
46
+ * map, so a copy is what keeps it. A depth blit per layer, same size and format, and the glass
47
+ * layers cleared to the far plane — nothing nearer than the sun until a pane is drawn there.
48
+ * Returns whether it grew, so the caller knows every bind of the old texture is stale.
49
+ */
50
+ growForGlass(gl: WebGL2RenderingContext): boolean;
51
+ /** Bind `layer` as the render target and clear it. Caller then draws the casters. */
52
+ begin(gl: WebGL2RenderingContext, layer: SunShadowLayer): void;
53
+ /** Bind a glass depth layer as the render target and clear it: where the nearest pane is. */
54
+ beginGlass(gl: WebGL2RenderingContext, which: 'static' | 'dynamic'): void;
55
+ private attach;
56
+ private clearAndRaster;
8
57
  end(gl: WebGL2RenderingContext): void;
9
58
  dispose(gl: WebGL2RenderingContext): void;
10
59
  }
@@ -1,43 +1,92 @@
1
1
  /**
2
- * Single-cascade directional shadow map: one depth texture, one extra draw of
3
- * the static world per update.
2
+ * The sun's shadow: one depth array whose layers are the static world, its movers, and the static
3
+ * world's second occluder (the peel), drawn one at a time and sampled together.
4
4
  *
5
- * Filtering belongs to the receiving material shader, so the depth target can
6
- * stay a single-sample texture shared by every mesh pass.
5
+ * **One array rather than three maps since 4.5.0**, for the reason the lamps' twelve cubemaps
6
+ * became one: a texture unit. The lit pass binds all three through one `sampler2DArray`, which is
7
+ * what paid for the two tints glass casts with. The layers share one size and one format, so
8
+ * nothing about how each is drawn or read moved — only where it lives.
7
9
  *
8
- * Note this is a depth *render target*, not an art texture: AGENTS.md's
9
- * "zero textures" rule is about the art direction (no image assets in the
10
- * payload), and nothing here adds a byte to it.
10
+ * Filtering belongs to the receiving material shader, so the depth target can stay a
11
+ * single-sample texture shared by every mesh pass. This is a depth *render target*, not an art
12
+ * texture: AGENTS.md's "zero textures" rule is about the payload, and nothing here adds to it.
11
13
  */
12
14
  import { SHADOW_DEPTH_CLEAR } from './depthConvention.js';
13
15
  import { DEFAULT_RENDER_QUALITY } from './renderQuality.js';
14
- export class ShadowMap {
16
+ /** Where each of the sun's maps lives in the array. The shaders read these same numbers. */
17
+ export const SUN_STATIC_LAYER = 0;
18
+ export const SUN_DYNAMIC_LAYER = 1;
19
+ /** Present only when more than one static depth layer is asked for. */
20
+ export const SUN_PEELED_LAYER = 2;
21
+ /**
22
+ * Where the nearest pane's depth lives once glass has cast: after the three maps, at fixed places,
23
+ * because a shader cannot ask an array how many layers it has here — naga refuses the third
24
+ * component of an arrayed size query. A profile with no peel grows past an unused third layer to
25
+ * reach them, which is one map's memory in a profile that already declined one.
26
+ */
27
+ export const SUN_GLASS_STATIC_LAYER = 3;
28
+ export const SUN_GLASS_MOVING_LAYER = 4;
29
+ /** The array layer a pass draws into. */
30
+ export function sunLayerIndex(layer) {
31
+ return layer === 'static'
32
+ ? SUN_STATIC_LAYER
33
+ : layer === 'dynamic'
34
+ ? SUN_DYNAMIC_LAYER
35
+ : SUN_PEELED_LAYER;
36
+ }
37
+ export class SunShadowArray {
15
38
  size;
16
- texture;
39
+ /** Replaced once, when glass first casts: see `growForGlass`. */
40
+ current;
41
+ layerCount;
42
+ glass = false;
17
43
  framebuffer;
18
- constructor(gl, size = DEFAULT_RENDER_QUALITY.directionalShadowMapSize) {
44
+ /**
45
+ * **The peel is drawn here and copied into its layer when the pass ends.** It reads the static
46
+ * layer while it is drawn, and WebGL2 calls a texture both sampled and attached a feedback loop
47
+ * whatever layer each names — so drawing the peel into the array would draw nothing at all.
48
+ * What it costs is one more map's memory where a peel is asked for, and one blit per peel.
49
+ */
50
+ peelTexture;
51
+ peelFramebuffer;
52
+ peeling = false;
53
+ constructor(gl, size = DEFAULT_RENDER_QUALITY.directionalShadowMapSize, peeled = false) {
19
54
  this.size = size;
20
- const texture = gl.createTexture();
21
- if (texture === null)
22
- throw new Error('ShadowMap: createTexture failed');
23
- this.texture = texture;
24
- gl.bindTexture(gl.TEXTURE_2D, texture);
25
- gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT24, size, size);
26
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
27
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
28
- // Clamp so anything outside the light frustum samples the border and is
29
- // treated as lit, rather than wrapping into a spurious shadow.
30
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
31
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
55
+ this.layerCount = peeled ? 3 : 2;
56
+ const texture = createSunTexture(gl, size, this.layerCount);
57
+ this.current = texture;
32
58
  const framebuffer = gl.createFramebuffer();
33
59
  if (framebuffer === null)
34
- throw new Error('ShadowMap: createFramebuffer failed');
60
+ throw new Error('SunShadowArray: createFramebuffer failed');
35
61
  this.framebuffer = framebuffer;
36
62
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
37
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0);
63
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, texture, 0, SUN_STATIC_LAYER);
38
64
  // Depth-only: without this WebGL expects a colour attachment.
39
65
  gl.drawBuffers([gl.NONE]);
40
66
  gl.readBuffer(gl.NONE);
67
+ if (peeled) {
68
+ const peelTexture = gl.createTexture();
69
+ const peelFramebuffer = gl.createFramebuffer();
70
+ if (peelTexture === null || peelFramebuffer === null) {
71
+ throw new Error('SunShadowArray: the peel target could not be created');
72
+ }
73
+ gl.bindTexture(gl.TEXTURE_2D, peelTexture);
74
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT24, size, size);
75
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
76
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
77
+ gl.bindFramebuffer(gl.FRAMEBUFFER, peelFramebuffer);
78
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, peelTexture, 0);
79
+ gl.drawBuffers([gl.NONE]);
80
+ gl.readBuffer(gl.NONE);
81
+ gl.bindTexture(gl.TEXTURE_2D, null);
82
+ this.peelTexture = peelTexture;
83
+ this.peelFramebuffer = peelFramebuffer;
84
+ }
85
+ else {
86
+ this.peelTexture = null;
87
+ this.peelFramebuffer = null;
88
+ }
89
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
41
90
  const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
42
91
  if (status !== gl.FRAMEBUFFER_COMPLETE) {
43
92
  /*
@@ -47,26 +96,102 @@ export class ShadowMap {
47
96
  */
48
97
  if (gl.isContextLost())
49
98
  return;
50
- throw new Error(`ShadowMap: framebuffer incomplete (0x${status.toString(16)})`);
99
+ throw new Error(`SunShadowArray: framebuffer incomplete (0x${status.toString(16)})`);
51
100
  }
52
101
  /*
53
- * **Cleared once, here, to the far plane, because nothing else says what the texture holds.**
54
- * WebGL zeroes it, and a zero compared with `LEQUAL` is an occluder at the light itself, so
55
- * a layer no pass has drawn put the whole world in shade. ANGLE on Vulkan hands the same
56
- * texture back reading 1, which is lit, and that is what every capture here ran on: a scene
57
- * with a second depth layer it never peeled lost its sun only under ANGLE's GL backend, the
58
- * one Chrome on Linux runs by default. The frame's clear value is borrowed and put back.
102
+ * **Every layer cleared once, here, to the far plane, because nothing else says what it
103
+ * holds.** WebGL zeroes the texture, and a zero compared with `LEQUAL` is an occluder at the
104
+ * light itself, so a layer no pass has drawn put the whole world in shade. ANGLE on Vulkan
105
+ * hands the same texture back reading 1, which is lit, and that is what every capture here
106
+ * ran on: a scene with a second depth layer it never peeled lost its sun only under ANGLE's
107
+ * GL backend, the one Chrome on Linux runs by default. The frame's clear value is put back.
59
108
  */
60
109
  const frameClear = gl.getParameter(gl.DEPTH_CLEAR_VALUE);
61
110
  gl.clearDepth(SHADOW_DEPTH_CLEAR);
62
- gl.clear(gl.DEPTH_BUFFER_BIT);
111
+ gl.viewport(0, 0, size, size);
112
+ for (let layer = 0; layer < this.layers; layer++) {
113
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, texture, 0, layer);
114
+ gl.clear(gl.DEPTH_BUFFER_BIT);
115
+ }
63
116
  gl.clearDepth(frameClear);
64
117
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
65
- gl.bindTexture(gl.TEXTURE_2D, null);
118
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
119
+ }
120
+ get texture() {
121
+ return this.current;
122
+ }
123
+ get layers() {
124
+ return this.layerCount;
125
+ }
126
+ /** Whether the two glass layers exist: from the first glass a sun pass is offered, for good. */
127
+ get hasGlass() {
128
+ return this.glass;
129
+ }
130
+ /** The glass depth layer beside a map: fixed places after the three maps. */
131
+ glassLayer(which) {
132
+ return which === 'static' ? SUN_GLASS_STATIC_LAYER : SUN_GLASS_MOVING_LAYER;
66
133
  }
67
- /** Bind as the render target and clear. Caller then draws the casters. */
68
- begin(gl) {
134
+ /**
135
+ * Add the two glass layers, once, carrying every layer already held across.
136
+ *
137
+ * **Carried rather than re-baked**, because the sun's static map is the consumer's to bake and
138
+ * it may bake it once: the glass that grows the array arrives in the very pass that drew that
139
+ * map, so a copy is what keeps it. A depth blit per layer, same size and format, and the glass
140
+ * layers cleared to the far plane — nothing nearer than the sun until a pane is drawn there.
141
+ * Returns whether it grew, so the caller knows every bind of the old texture is stale.
142
+ */
143
+ growForGlass(gl) {
144
+ if (this.glass)
145
+ return false;
146
+ const old = this.current;
147
+ const oldLayers = this.layerCount;
148
+ const layers = SUN_GLASS_MOVING_LAYER + 1;
149
+ const texture = createSunTexture(gl, this.size, layers);
150
+ const read = gl.createFramebuffer();
151
+ if (read === null)
152
+ throw new Error('SunShadowArray: createFramebuffer failed');
153
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, read);
154
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer);
155
+ for (let layer = 0; layer < oldLayers; layer++) {
156
+ gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, old, 0, layer);
157
+ gl.framebufferTextureLayer(gl.DRAW_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, texture, 0, layer);
158
+ gl.blitFramebuffer(0, 0, this.size, this.size, 0, 0, this.size, this.size, gl.DEPTH_BUFFER_BIT, gl.NEAREST);
159
+ }
160
+ for (let layer = oldLayers; layer < layers; layer++) {
161
+ gl.framebufferTextureLayer(gl.DRAW_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, texture, 0, layer);
162
+ gl.clearBufferfv(gl.DEPTH, 0, FAR_PLANE);
163
+ }
164
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
165
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
166
+ gl.deleteFramebuffer(read);
167
+ gl.deleteTexture(old);
168
+ this.current = texture;
169
+ this.layerCount = layers;
170
+ this.glass = true;
171
+ return true;
172
+ }
173
+ /** Bind `layer` as the render target and clear it. Caller then draws the casters. */
174
+ begin(gl, layer) {
175
+ this.peeling = layer === 'static-peel' && this.peelFramebuffer !== null;
176
+ if (this.peeling) {
177
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.peelFramebuffer);
178
+ }
179
+ else {
180
+ this.attach(gl, sunLayerIndex(layer));
181
+ }
182
+ this.clearAndRaster(gl);
183
+ }
184
+ /** Bind a glass depth layer as the render target and clear it: where the nearest pane is. */
185
+ beginGlass(gl, which) {
186
+ this.peeling = false;
187
+ this.attach(gl, this.glassLayer(which));
188
+ this.clearAndRaster(gl);
189
+ }
190
+ attach(gl, layer) {
69
191
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
192
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, this.current, 0, layer);
193
+ }
194
+ clearAndRaster(gl) {
70
195
  gl.viewport(0, 0, this.size, this.size);
71
196
  gl.clear(gl.DEPTH_BUFFER_BIT);
72
197
  /*
@@ -84,10 +209,39 @@ export class ShadowMap {
84
209
  end(gl) {
85
210
  gl.disable(gl.POLYGON_OFFSET_FILL);
86
211
  gl.cullFace(gl.BACK);
212
+ if (this.peeling) {
213
+ /* The peel into its layer: same size, same format, so a nearest depth blit is a copy. */
214
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.peelFramebuffer);
215
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer);
216
+ gl.framebufferTextureLayer(gl.DRAW_FRAMEBUFFER, gl.DEPTH_ATTACHMENT, this.current, 0, SUN_PEELED_LAYER);
217
+ gl.blitFramebuffer(0, 0, this.size, this.size, 0, 0, this.size, this.size, gl.DEPTH_BUFFER_BIT, gl.NEAREST);
218
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
219
+ this.peeling = false;
220
+ }
87
221
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
88
222
  }
89
223
  dispose(gl) {
90
224
  gl.deleteFramebuffer(this.framebuffer);
91
- gl.deleteTexture(this.texture);
225
+ gl.deleteTexture(this.current);
226
+ if (this.peelFramebuffer !== null)
227
+ gl.deleteFramebuffer(this.peelFramebuffer);
228
+ if (this.peelTexture !== null)
229
+ gl.deleteTexture(this.peelTexture);
92
230
  }
93
231
  }
232
+ /** The far plane, as `clearBufferfv` takes it: a layer no pane has reached is clear of glass. */
233
+ const FAR_PLANE = new Float32Array([SHADOW_DEPTH_CLEAR]);
234
+ function createSunTexture(gl, size, layers) {
235
+ const texture = gl.createTexture();
236
+ if (texture === null)
237
+ throw new Error('SunShadowArray: createTexture failed');
238
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
239
+ gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.DEPTH_COMPONENT24, size, size, layers);
240
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
241
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
242
+ // Clamp so anything outside the light frustum samples the border and is
243
+ // treated as lit, rather than wrapping into a spurious shadow.
244
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
245
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
246
+ return texture;
247
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What the sun's light keeps of itself through glass: an RGBA8 array of two layers, static and
3
+ * moving, beside the sun's depth array. RGB is the product of every pane's
4
+ * `transmission × (1 − F) × tint`, A the product of their clarities (`glassShadow.ts`).
5
+ *
6
+ * **Cleared to white and drawn with a multiplying blend and no depth test**, so every pane on a ray
7
+ * contributes and the order they come in cannot matter. **Mipmapped and filtered**, unlike the
8
+ * depths beside it: a frosted pane's colour is read at the level its spread asks for, so a wide
9
+ * spread is smooth rather than a scatter of taps — the chain is rebuilt after every draw of a layer.
10
+ * `'half'` makes it half the depth map's size and nothing else changes: the light matrix is the
11
+ * same, so a texel is a texel of the same place, only larger.
12
+ */
13
+ export declare class SunGlassTint {
14
+ readonly size: number;
15
+ readonly texture: WebGLTexture;
16
+ private readonly framebuffer;
17
+ constructor(gl: WebGL2RenderingContext, size: number);
18
+ /** Bind `layer` (0 static, 1 moving), clear it to white, and set the multiplying state. */
19
+ begin(gl: WebGL2RenderingContext, layer: number): void;
20
+ /** Put back what `begin` changed, and rebuild the mip chain the frost reads. */
21
+ end(gl: WebGL2RenderingContext): void;
22
+ /** Every level from level 0, both layers: bound on the active unit and handed back. */
23
+ private fillMips;
24
+ dispose(gl: WebGL2RenderingContext): void;
25
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * What the sun's light keeps of itself through glass: an RGBA8 array of two layers, static and
3
+ * moving, beside the sun's depth array. RGB is the product of every pane's
4
+ * `transmission × (1 − F) × tint`, A the product of their clarities (`glassShadow.ts`).
5
+ *
6
+ * **Cleared to white and drawn with a multiplying blend and no depth test**, so every pane on a ray
7
+ * contributes and the order they come in cannot matter. **Mipmapped and filtered**, unlike the
8
+ * depths beside it: a frosted pane's colour is read at the level its spread asks for, so a wide
9
+ * spread is smooth rather than a scatter of taps — the chain is rebuilt after every draw of a layer.
10
+ * `'half'` makes it half the depth map's size and nothing else changes: the light matrix is the
11
+ * same, so a texel is a texel of the same place, only larger.
12
+ */
13
+ export class SunGlassTint {
14
+ size;
15
+ texture;
16
+ framebuffer;
17
+ constructor(gl, size) {
18
+ this.size = size;
19
+ const texture = gl.createTexture();
20
+ const framebuffer = gl.createFramebuffer();
21
+ if (texture === null || framebuffer === null) {
22
+ throw new Error('SunGlassTint: the tint target could not be created');
23
+ }
24
+ this.texture = texture;
25
+ this.framebuffer = framebuffer;
26
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
27
+ gl.texStorage3D(gl.TEXTURE_2D_ARRAY, mipLevels(size), gl.RGBA8, size, size, 2);
28
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
29
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
30
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
31
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
32
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
33
+ /* Both layers born clear: a layer no pane has reached lets everything through. */
34
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
35
+ for (let layer = 0; layer < 2; layer++) {
36
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, texture, 0, layer);
37
+ gl.clearBufferfv(gl.COLOR, 0, CLEAR);
38
+ }
39
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
40
+ this.fillMips(gl);
41
+ }
42
+ /** Bind `layer` (0 static, 1 moving), clear it to white, and set the multiplying state. */
43
+ begin(gl, layer) {
44
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
45
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, this.texture, 0, layer);
46
+ gl.viewport(0, 0, this.size, this.size);
47
+ gl.clearBufferfv(gl.COLOR, 0, CLEAR);
48
+ gl.disable(gl.DEPTH_TEST);
49
+ gl.depthMask(false);
50
+ gl.disable(gl.POLYGON_OFFSET_FILL);
51
+ gl.enable(gl.BLEND);
52
+ gl.blendEquation(gl.FUNC_ADD);
53
+ gl.blendFuncSeparate(gl.DST_COLOR, gl.ZERO, gl.DST_ALPHA, gl.ZERO);
54
+ }
55
+ /** Put back what `begin` changed, and rebuild the mip chain the frost reads. */
56
+ end(gl) {
57
+ gl.disable(gl.BLEND);
58
+ gl.depthMask(true);
59
+ gl.enable(gl.DEPTH_TEST);
60
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
61
+ this.fillMips(gl);
62
+ }
63
+ /** Every level from level 0, both layers: bound on the active unit and handed back. */
64
+ fillMips(gl) {
65
+ const previous = gl.getParameter(gl.TEXTURE_BINDING_2D_ARRAY);
66
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
67
+ gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
68
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, previous);
69
+ }
70
+ dispose(gl) {
71
+ gl.deleteFramebuffer(this.framebuffer);
72
+ gl.deleteTexture(this.texture);
73
+ }
74
+ }
75
+ const CLEAR = new Float32Array([1, 1, 1, 1]);
76
+ /** A full chain down to one texel. */
77
+ function mipLevels(size) {
78
+ return Math.floor(Math.log2(Math.max(1, size))) + 1;
79
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ReadonlyMat4 } from 'gl-matrix';
2
2
  import type { TranslucentMeshOptions } from './backend/webgl2/renderer.ts';
3
+ import type { ResolvedGlass } from './glass.ts';
3
4
  /**
4
5
  * The frame's translucent draws, held so they can be submitted more than once.
5
6
  *
@@ -46,11 +47,14 @@ export interface TranslucentDraw {
46
47
  /** What survives one metre of the medium. Null where the draw named none; otherwise a copy. */
47
48
  refractTint: Float32Array | null;
48
49
  thicknessM: number;
50
+ /** Glass, resolved, or null for a draw that is not glass. See `glass.ts`. */
51
+ glass: ResolvedGlass | null;
49
52
  }
50
53
  export declare class TranslucentQueue {
51
54
  private readonly pool;
52
55
  private count;
53
56
  private refracting;
57
+ private glassing;
54
58
  /** How many draws this frame has recorded. */
55
59
  get length(): number;
56
60
  /**
@@ -58,6 +62,8 @@ export declare class TranslucentQueue {
58
62
  * frame behind it first, and does not copy it for a set that never reads it.
59
63
  */
60
64
  get refracts(): boolean;
65
+ /** Whether anything recorded this frame is glass, so the copy it reads needs its mip chain. */
66
+ get glassy(): boolean;
61
67
  /** How many records are pooled, which a test uses to assert a steady scene stops allocating. */
62
68
  get capacity(): number;
63
69
  /**
@@ -1,3 +1,4 @@
1
+ import { resolveGlass } from './glass.js';
1
2
  function newRecord() {
2
3
  return {
3
4
  mesh: null,
@@ -14,12 +15,15 @@ function newRecord() {
14
15
  refractTint: null,
15
16
  thicknessM: 0,
16
17
  refractTintStore: new Float32Array(3),
18
+ glass: null,
19
+ glassStore: { transmission: 0, frost: 0, tint: [1, 1, 1] },
17
20
  };
18
21
  }
19
22
  export class TranslucentQueue {
20
23
  pool = [];
21
24
  count = 0;
22
25
  refracting = 0;
26
+ glassing = 0;
23
27
  /** How many draws this frame has recorded. */
24
28
  get length() {
25
29
  return this.count;
@@ -31,6 +35,10 @@ export class TranslucentQueue {
31
35
  get refracts() {
32
36
  return this.refracting > 0;
33
37
  }
38
+ /** Whether anything recorded this frame is glass, so the copy it reads needs its mip chain. */
39
+ get glassy() {
40
+ return this.glassing > 0;
41
+ }
34
42
  /** How many records are pooled, which a test uses to assert a steady scene stops allocating. */
35
43
  get capacity() {
36
44
  return this.pool.length;
@@ -65,7 +73,12 @@ export class TranslucentQueue {
65
73
  record.tint = record.tintStore;
66
74
  }
67
75
  record.refraction = options.refraction ?? 0;
68
- if (record.refraction > 0)
76
+ /* Glass reads the copy too, whether or not it bends: see `glass.ts`. */
77
+ const glassy = resolveGlass(options.glass, record.glassStore);
78
+ record.glass = glassy ? record.glassStore : null;
79
+ if (glassy)
80
+ this.glassing++;
81
+ if (record.refraction > 0 || glassy)
69
82
  this.refracting++;
70
83
  record.thicknessM = options.thicknessM ?? 0;
71
84
  const refractTint = options.refractTint ?? null;
@@ -97,5 +110,6 @@ export class TranslucentQueue {
97
110
  reset() {
98
111
  this.count = 0;
99
112
  this.refracting = 0;
113
+ this.glassing = 0;
100
114
  }
101
115
  }