@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
@@ -36,6 +36,19 @@ export { FACE_COUNT, POINT_SHADOW_NEAR };
36
36
 
37
37
  const scratchViewProj = mat4.create();
38
38
 
39
+ /**
40
+ * What a bake asks of the renderer about glass, after each face's opaque depth.
41
+ *
42
+ * **Asked before it is drawn** because the answer decides whether the face binds a glass target at
43
+ * all: a face that offers no glass and holds none from a previous owner does no glass work.
44
+ */
45
+ export interface PointGlassDraw {
46
+ /** Whether the face just drawn offered any glass. */
47
+ offered(): boolean;
48
+ /** Draw that glass: its depth, or what it lets through lit from the bake origin. */
49
+ draw(pass: 'depth' | 'tint', x: number, y: number, z: number): void;
50
+ }
51
+
39
52
  export class PointShadowMap {
40
53
  readonly size: number;
41
54
  /**
@@ -163,6 +176,8 @@ export class PointShadowMap {
163
176
  near = POINT_SHADOW_NEAR,
164
177
  sourceRadius = DEFAULT_SOURCE_RADIUS,
165
178
  maxFaces = FACE_COUNT,
179
+ /** The face's glass, when glass shadows are on: `PointGlassDraw`. */
180
+ glass: PointGlassDraw | null = null,
166
181
  ): number {
167
182
  /*
168
183
  * What to render now, **and what to render it for**. A bake in flight owns the parameters
@@ -220,6 +235,7 @@ export class PointShadowMap {
220
235
  * next face's pass does care about.
221
236
  */
222
237
  array.resolveFace(gl, this.layer, face, plan.range, plan.near);
238
+ if (glass !== null) this.bakeGlassFace(gl, array, glass, face, plan);
223
239
  }
224
240
 
225
241
  gl.enable(gl.CULL_FACE);
@@ -237,5 +253,30 @@ export class PointShadowMap {
237
253
  * needing disposal is exactly the shape of a leak nobody notices. The array is disposed by the
238
254
  * renderer that built it.
239
255
  */
256
+ /**
257
+ * The face's glass, twice: where the nearest pane is, then what the panes let through.
258
+ *
259
+ * A face that offers no glass still clears a region a previous owner's glass is in — the depth to
260
+ * the far plane and the tint to white, from scratches cleared and not drawn into — because a pool
261
+ * slot changes hands with its layer.
262
+ */
263
+ private bakeGlassFace(
264
+ gl: WebGL2RenderingContext,
265
+ array: PointShadowTarget,
266
+ glass: PointGlassDraw,
267
+ face: number,
268
+ plan: FaceRange,
269
+ ): void {
270
+ if (!array.hasGlass) return;
271
+ const offered = glass.offered();
272
+ if (!offered && !array.heldGlass(this.layer, face)) return;
273
+ array.beginGlassFace(gl);
274
+ if (offered) glass.draw('depth', plan.x, plan.y, plan.z);
275
+ array.resolveGlassFace(gl, this.layer, face, plan.range, plan.near);
276
+ array.beginTintFace(gl);
277
+ if (offered) glass.draw('tint', plan.x, plan.y, plan.z);
278
+ array.resolveTintFace(gl, this.layer, face, offered);
279
+ }
280
+
240
281
  dispose(_gl: WebGL2RenderingContext): void {}
241
282
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Which frame a change to a dynamic mesh or an instanced batch belongs to.
3
+ *
4
+ * **A consumer may change geometry in its simulation step, before `beginFrame`.** That change is
5
+ * what the *next* frame draws, so it is that frame's — counting it against the frame last begun
6
+ * would say the geometry changed a frame early and then did not change at all, which is a motion
7
+ * vector on the wrong frame and none on the right one.
8
+ *
9
+ * Only the **first** change in a frame keeps the state before it: a second rewrite in the same
10
+ * frame must not replace "where it was last frame" with "where it was a moment ago".
11
+ */
12
+
13
+ export function frameOfChange(serial: number, inFrame: boolean): number {
14
+ return inFrame ? serial : serial + 1;
15
+ }
16
+
17
+ export function keepsPrevious(lastChange: number, frame: number): boolean {
18
+ return lastChange !== frame;
19
+ }
20
+
21
+ /** Slots `0 … n-1` pair with last frame's; a slot past last frame's count is new and has no past. */
22
+ export function pairedInstances(previousCount: number, count: number): number {
23
+ return Math.min(previousCount, count);
24
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Two decisions about one draw in a reconstructing frame, and why they are the same set.
3
+ *
4
+ * A blended draw goes **late** — after the reconstruction, at output resolution, unjittered — and a
5
+ * mover's motion is **tracked**, only for draws that land in the picture being reconstructed. A
6
+ * planar mirror and a probe bake are other pictures drawn through the same verbs, and the overlay
7
+ * after `endFrame` is past the picture altogether; routing any of them late, or counting a mirrored
8
+ * draw as a mover's second draw of the frame, would be wrong in the way that reads as a ghost.
9
+ *
10
+ * Plain booleans rather than a state object, so the per-draw call allocates nothing.
11
+ */
12
+ export function routesLate(
13
+ reconstructing: boolean,
14
+ mirror: boolean,
15
+ probe: boolean,
16
+ presented: boolean,
17
+ ): boolean {
18
+ return reconstructing && !mirror && !probe && !presented;
19
+ }
20
+
21
+ /** The same set as `routesLate`, named for the other reader. */
22
+ export function tracksMotion(
23
+ reconstructing: boolean,
24
+ mirror: boolean,
25
+ probe: boolean,
26
+ presented: boolean,
27
+ ): boolean {
28
+ return routesLate(reconstructing, mirror, probe, presented);
29
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Which object a draw is, so a reconstruction can find where that object was last frame.
3
+ *
4
+ * **The engine cannot infer this and does not try.** `drawMesh` is immediate: a mesh drawn three
5
+ * times is three objects, and nothing in the call says which of last frame's three each one is. A
6
+ * consumer that makes one `Mover` per moving object and passes it with every draw of that object
7
+ * says exactly that, and the renderer keeps the rest — last frame's model and, for a skinned
8
+ * draw, last frame's palette.
9
+ *
10
+ * **Plain data, advanced by the renderer.** No renderer is needed to make one, so a consumer holds
11
+ * it beside the object it belongs to. Only a reconstructing WebGPU renderer reads or writes it; it
12
+ * costs nothing anywhere else. The fields past `kind` are the renderer's and a caller leaves them
13
+ * alone.
14
+ *
15
+ * **What would make this wrong** is a consumer whose objects are rebuilt every frame with no state
16
+ * of their own to hold a mover in; the identity then has to live in whatever *does* persist.
17
+ */
18
+
19
+ export const MOVER_KIND = 'drift.mover' as const;
20
+
21
+ export interface Mover {
22
+ readonly kind: typeof MOVER_KIND;
23
+ /** The renderer that last drew it, from `claimRendererId`; -1 before its first draw. */
24
+ renderer: number;
25
+ /** That renderer's frame serial at the last draw. */
26
+ frame: number;
27
+ readonly model: Float32Array;
28
+ readonly previousModel: Float32Array;
29
+ /**
30
+ * This frame's palette, exactly `paletteLength` floats long once a skinned draw has set it.
31
+ * Swapped with the previous one each frame and reallocated only when the rig changes.
32
+ */
33
+ palette: Float32Array;
34
+ previousPalette: Float32Array;
35
+ paletteLength: number;
36
+ previousPaletteLength: number;
37
+ }
38
+
39
+ /** Nothing to reproject from: first draw, a missed frame, another renderer, or a changed rig. */
40
+ export const MOVER_NONE = 0;
41
+ /** Drawn on the frame before by this renderer: `previousModel` and `previousPalette` are real. */
42
+ export const MOVER_MOTION = 1;
43
+ /** Drawn already this frame. The second draw is a different object and the caller's error. */
44
+ export const MOVER_TWICE = 2;
45
+ export type MoverVerdict = typeof MOVER_NONE | typeof MOVER_MOTION | typeof MOVER_TWICE;
46
+
47
+ let nextRendererId = 1;
48
+
49
+ /** A number no other renderer in this page holds, so a mover can tell renderers apart. */
50
+ export function claimRendererId(): number {
51
+ const id = nextRendererId;
52
+ nextRendererId += 1;
53
+ return id;
54
+ }
55
+
56
+ export function createMover(): Mover {
57
+ return {
58
+ kind: MOVER_KIND,
59
+ renderer: -1,
60
+ frame: -2,
61
+ model: new Float32Array(16),
62
+ previousModel: new Float32Array(16),
63
+ palette: new Float32Array(0),
64
+ previousPalette: new Float32Array(0),
65
+ paletteLength: 0,
66
+ previousPaletteLength: 0,
67
+ };
68
+ }
69
+
70
+ export function isMover(value: unknown): value is Mover {
71
+ return (
72
+ typeof value === 'object' && value !== null && (value as { kind?: unknown }).kind === MOVER_KIND
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Advance a mover to this draw, and say whether last frame's state is usable.
78
+ *
79
+ * **A second draw in one frame changes nothing**, so the first draw's motion stands; the caller
80
+ * warns. Otherwise what the mover held becomes its previous and this draw becomes its current —
81
+ * the palettes by swapping two arrays, so a steady rig allocates nothing after its first frame.
82
+ *
83
+ * **A changed joint count is no motion**: skinning a vertex with a palette of a different rig is
84
+ * not an old pose of this one.
85
+ */
86
+ export function advanceMover(
87
+ mover: Mover,
88
+ renderer: number,
89
+ frame: number,
90
+ model: ArrayLike<number>,
91
+ palette: Float32Array | null,
92
+ ): MoverVerdict {
93
+ if (mover.renderer === renderer && mover.frame === frame) return MOVER_TWICE;
94
+ const drawnLast = mover.renderer === renderer && mover.frame === frame - 1;
95
+
96
+ mover.previousModel.set(mover.model);
97
+ const spare = mover.previousPalette;
98
+ mover.previousPalette = mover.palette;
99
+ mover.palette = spare;
100
+ mover.previousPaletteLength = mover.paletteLength;
101
+
102
+ mover.model.set(model);
103
+ if (palette === null) {
104
+ mover.paletteLength = 0;
105
+ } else {
106
+ /* Exactly the rig's length, because the array is uploaded as it stands; reallocated only when
107
+ the rig changes, which a steady character never does. */
108
+ if (mover.palette.length !== palette.length) mover.palette = new Float32Array(palette.length);
109
+ mover.palette.set(palette);
110
+ mover.paletteLength = palette.length;
111
+ }
112
+ mover.renderer = renderer;
113
+ mover.frame = frame;
114
+
115
+ if (!drawnLast) return MOVER_NONE;
116
+ if (mover.paletteLength !== mover.previousPaletteLength) return MOVER_NONE;
117
+ return MOVER_MOTION;
118
+ }
@@ -37,6 +37,9 @@ export type ShadowFilterTaps = 4 | 8 | 12;
37
37
  export type DirectionalShadowDepthLayers = 1 | 2;
38
38
  export type WaterReflectionFilterTaps = 1 | 5 | 9;
39
39
 
40
+ /** How light through glass is coloured: see `RenderQuality.glassShadows`. */
41
+ export type GlassShadows = 'full' | 'half' | 'off';
42
+
40
43
  export interface RenderQuality {
41
44
  /**
42
45
  * The transfer curve applied at the end of every shaded pass.
@@ -149,6 +152,25 @@ export interface RenderQuality {
149
152
  readonly lightVolumeSamples: number;
150
153
  /** Independently composited static sun depths; two preserves common overlaps. */
151
154
  readonly directionalShadowDepthLayers: DirectionalShadowDepthLayers;
155
+ /**
156
+ * Whether light through glass takes the glass's colour, and at what size.
157
+ *
158
+ * **`'full'`, the default**: every shadow a glass caster reaches gains a layer holding the depth of
159
+ * the nearest pane and a tint layer holding what the panes let through, at the size of the depth
160
+ * map beside it — 8 bytes a texel, so 32 MB for a 2048 sun map and 8 MB for a 1024 lamp layer.
161
+ * **`'half'`** halves the tint layers only (the glass depth must match its array): 20 MB and 5 MB,
162
+ * and a coloured patch softer than an opaque shadow's edge. **`'off'`** is glass before this
163
+ * existed: a pane casts nothing at all.
164
+ *
165
+ * Nothing is allocated until a glass caster first reaches a shadow, so a scene with no glass pays
166
+ * no memory and no pass whatever this says. **The lit shader is the exception**: `'full'` and
167
+ * `'half'` compile the glass lookup into it, and a lookup that finds no glass and returns at its
168
+ * first line still costs the lit pass its registers — measured on the courtyard at night, 0.27 ms
169
+ * at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K. `'off'` compiles none of it, so a world
170
+ * that never casts through glass is that much faster for saying so. What would make `'full'` wrong
171
+ * is a phone budget with a lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
172
+ */
173
+ readonly glassShadows: GlassShadows;
152
174
  /** Horizontal world-space reach over which a directional shadow dissolves. */
153
175
  readonly directionalShadowMaxDistance: number;
154
176
  /** Maximum horizontal projection per vertical metre before low-angle shadows vanish. */
@@ -556,6 +578,20 @@ export interface RenderQuality {
556
578
  * the render is a quarter of the output and no reconstruction holds an edge through that. A
557
579
  * negative number is off, not a clamp to the bottom: it cannot mean "a little".
558
580
  *
581
+ * **What it needs from you.** A `createMover()` per moving object, passed with that object's
582
+ * draws where `drawMesh` takes last frame's matrix — the renderer keeps last frame's model and,
583
+ * for a skinned draw, last frame's pose. Instanced batches and meshes rewritten with
584
+ * `updateMesh` need nothing: the batch and the mesh are their own identity. And `cameraCut()` at
585
+ * every cut, because nothing detects one: without it a cut blends the previous shot into the
586
+ * next. Blended draws — translucent meshes, particles, plumes, film, bolts, lines, world text —
587
+ * are drawn after the upscale at the output size and need nothing either; light volumes stay in
588
+ * the reconstructed picture, being a march that keeps its history.
589
+ *
590
+ * **What it cannot give back** is the resolution it draws at: an edge that never moves is softer
591
+ * than a native frame's at any ratio, because two thirds of the pixels at 1.5 are a guess made
592
+ * sharp. Measured on `demo/dev/ghost.html` against a native frame at four samples, still edges
593
+ * err by about 8 levels where a native frame at one sample errs by 3.
594
+ *
559
595
  * `?recon=` is how it gets A/B'd in one reload.
560
596
  */
561
597
  readonly reconstruction: number;
@@ -838,6 +874,7 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
838
874
  shadowFilterTaps: MAX_SHADOW_FILTER_TAPS,
839
875
  lightVolumeSamples: 32,
840
876
  directionalShadowDepthLayers: 2,
877
+ glassShadows: 'full',
841
878
  directionalShadowMaxDistance: 6,
842
879
  directionalShadowMaxSlope: 3,
843
880
  pointShadowRebakeDistance: 0.001,
@@ -998,6 +1035,7 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
998
1035
  lightVolumeSamples: options.lightVolumeSamples ?? DEFAULT_RENDER_QUALITY.lightVolumeSamples,
999
1036
  directionalShadowDepthLayers:
1000
1037
  options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
1038
+ glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
1001
1039
  directionalShadowMaxDistance:
1002
1040
  options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
1003
1041
  directionalShadowMaxSlope:
@@ -1128,6 +1166,15 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1128
1166
  `RenderQuality.directionalShadowDepthLayers must be 1 or 2, got ${quality.directionalShadowDepthLayers}`,
1129
1167
  );
1130
1168
  }
1169
+ if (
1170
+ quality.glassShadows !== 'full' &&
1171
+ quality.glassShadows !== 'half' &&
1172
+ quality.glassShadows !== 'off'
1173
+ ) {
1174
+ throw new Error(
1175
+ `RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`,
1176
+ );
1177
+ }
1131
1178
  if (quality.plumeNoiseOctaves > 3) {
1132
1179
  throw new Error(
1133
1180
  `RenderQuality.plumeNoiseOctaves must be <= 3, got ${quality.plumeNoiseOctaves}`,
@@ -129,6 +129,18 @@ export function recordingGl(
129
129
  COLOR_BUFFER_BIT: 0x00004000,
130
130
  /* Named so a test can tell a mesh that declared itself deforming from one that did not. */
131
131
  ARRAY_BUFFER: 0x8892,
132
+ /* Named so a blend a pass sets can be read back as numbers: glass multiplies what it lets
133
+ through, and a test that cannot tell DST_COLOR from a recorder cannot tell it multiplies. */
134
+ ZERO: 0,
135
+ DST_ALPHA: 0x0304,
136
+ DST_COLOR: 0x0306,
137
+ FUNC_ADD: 0x8006,
138
+ COLOR: 0x1800,
139
+ DEPTH: 0x1801,
140
+ /* Named so a copy can be read as from-and-to: a blit's two framebuffers are the whole claim. */
141
+ READ_FRAMEBUFFER: 0x8ca8,
142
+ DRAW_FRAMEBUFFER: 0x8ca9,
143
+ BLEND: 0x0be2,
132
144
  STATIC_DRAW: 0x88e4,
133
145
  DYNAMIC_DRAW: 0x88e8,
134
146
  };
@@ -82,6 +82,8 @@ export class SceneTarget {
82
82
  private colorCopyFramebuffer: WebGLFramebuffer | null = null;
83
83
  /** Whether this frame has filled it. One snapshot serves every refracting draw in a frame. */
84
84
  private colorCopyFilled = false;
85
+ /** Whether this frame's copy has its mip chain, which frosted glass reads. See `snapshotColor`. */
86
+ private colorCopyMips = false;
85
87
  /**
86
88
  * The colour grade, as a 3D texture on unit 4, and the table it was built from.
87
89
  *
@@ -275,12 +277,24 @@ export class SceneTarget {
275
277
  */
276
278
  if (this.colorCopy === null) this.colorCopy = gl.createTexture();
277
279
  gl.bindTexture(gl.TEXTURE_2D, this.colorCopy);
278
- if (this.floatColor) {
279
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
280
- } else {
281
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
280
+ /*
281
+ * **Every level, so glass can read a blurrier one.** Frosted glass reads the copy at a level its
282
+ * frost picks, and a texture sampled through a mipmapping filter with a level missing is
283
+ * incomplete and reads black — so the chain is allocated here, once per size, and filled only on
284
+ * a frame that draws glass (`snapshotColor`). Plain refraction reads level 0, as it always has.
285
+ */
286
+ const levels = Math.floor(Math.log2(Math.max(width, height))) + 1;
287
+ for (let level = 0; level < levels; level += 1) {
288
+ const w = Math.max(1, width >> level);
289
+ const h = Math.max(1, height >> level);
290
+ if (this.floatColor) {
291
+ gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA16F, w, h, 0, gl.RGBA, gl.HALF_FLOAT, null);
292
+ } else {
293
+ gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
294
+ }
282
295
  }
283
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
296
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, levels - 1);
297
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
284
298
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
285
299
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
286
300
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
@@ -419,6 +433,7 @@ export class SceneTarget {
419
433
  /* Last frame's snapshot is last frame's scene. See `snapshotDepth`. */
420
434
  this.depthCopyFilled = false;
421
435
  this.colorCopyFilled = false;
436
+ this.colorCopyMips = false;
422
437
  this.bind();
423
438
  }
424
439
 
@@ -516,12 +531,21 @@ export class SceneTarget {
516
531
  * Null where the copy could not be made, and a caller must read that as "do not refract" rather
517
532
  * than as a black texture — refracting against black paints every pane the colour of a hole.
518
533
  */
519
- snapshotColor(afresh = false): WebGLTexture | null {
534
+ snapshotColor(
535
+ afresh = false,
536
+ /** Fill the copy's mip chain as well, once a frame: what frosted glass reads. */
537
+ mips = false,
538
+ ): WebGLTexture | null {
520
539
  if (this.colorCopy === null || this.colorCopyFramebuffer === null) return null;
521
- if (this.colorCopyFilled && !afresh) return this.colorCopy;
540
+ if (this.colorCopyFilled && !afresh) {
541
+ if (mips) this.fillColorCopyMips();
542
+ return this.colorCopy;
543
+ }
522
544
  const { gl } = this;
523
545
  const source = this.msaaFramebuffer ?? this.framebuffer;
524
546
  if (source === null) return null;
547
+ const drawing = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
548
+ const reading = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
525
549
 
526
550
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source);
527
551
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.colorCopyFramebuffer);
@@ -546,13 +570,33 @@ export class SceneTarget {
546
570
  gl.NEAREST,
547
571
  );
548
572
  const refused = gl.getError() !== gl.NO_ERROR;
549
- /* Back to what the caller was drawing into, whichever that was. */
550
- gl.bindFramebuffer(gl.FRAMEBUFFER, source);
573
+ /*
574
+ * **Back to what the caller was drawing into, whichever that was** — read before the copy and
575
+ * handed back, never assumed. It was the scene's framebuffer, assumed, and a pane drawn into a
576
+ * reflection probe's face then left the scene's bound: the probe attached its next face to it,
577
+ * a face-sized colour beside a screen-sized depth, and every draw after refused as incomplete.
578
+ */
579
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawing);
580
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, reading);
551
581
  if (refused) return null;
552
582
  this.colorCopyFilled = true;
583
+ this.colorCopyMips = false;
584
+ if (mips) this.fillColorCopyMips();
553
585
  return this.colorCopy;
554
586
  }
555
587
 
588
+ /** The copy's mip chain from its level 0, once per copy. */
589
+ private fillColorCopyMips(): void {
590
+ if (this.colorCopyMips || this.colorCopy === null) return;
591
+ const { gl } = this;
592
+ /* Bound on whichever unit is active and handed back, so the caller's texture there survives. */
593
+ const previous = gl.getParameter(gl.TEXTURE_BINDING_2D) as WebGLTexture | null;
594
+ gl.bindTexture(gl.TEXTURE_2D, this.colorCopy);
595
+ gl.generateMipmap(gl.TEXTURE_2D);
596
+ gl.bindTexture(gl.TEXTURE_2D, previous);
597
+ this.colorCopyMips = true;
598
+ }
599
+
556
600
  snapshotDepth(
557
601
  /**
558
602
  * Take the copy again even though one has already been taken this frame.
@@ -573,6 +617,8 @@ export class SceneTarget {
573
617
  const { gl } = this;
574
618
  const source = this.msaaFramebuffer ?? this.framebuffer;
575
619
  if (source === null) return null;
620
+ const drawing = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
621
+ const reading = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
576
622
 
577
623
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source);
578
624
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.depthCopyFramebuffer);
@@ -595,8 +641,9 @@ export class SceneTarget {
595
641
  gl.NEAREST,
596
642
  );
597
643
  const refused = gl.getError() !== gl.NO_ERROR;
598
- /* Back to what the caller was drawing into, whichever that was. */
599
- gl.bindFramebuffer(gl.FRAMEBUFFER, source);
644
+ /* Back to what the caller was drawing into, read rather than assumed: see `snapshotColor`. */
645
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawing);
646
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, reading);
600
647
  if (refused) return null;
601
648
  this.depthCopyFilled = true;
602
649
  return this.depthCopy;
@@ -1,4 +1,5 @@
1
1
  import { resolveConditionals } from './conditionals.ts';
2
+ import { SUN_STATIC_LAYER } from '../shadowMap.ts';
2
3
  import { CUTOUT_COVERAGE_GLSL } from './cutoutCoverage.ts';
3
4
  import { SKINNING_GLSL } from './skinning.ts';
4
5
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from './vertexChannel.ts';
@@ -73,6 +74,11 @@ out vec4 vLightPosition;
73
74
  out vec2 vUv;
74
75
  flat out float vAlphaCutout;
75
76
  #endif
77
+ #if GLASS
78
+ /* Where on the pane this is, for the tint's own normal and its angle to the light. Last, so the
79
+ outputs before it keep the locations the plain variants give them. */
80
+ out vec3 vGlassWorld;
81
+ #endif
76
82
 
77
83
  void main() {
78
84
  #if SKINNED
@@ -100,20 +106,23 @@ void main() {
100
106
  vUv = aUv * uUvScale;
101
107
  vAlphaCutout = uAlphaCutout;
102
108
  #endif
109
+ #if GLASS
110
+ vGlassWorld = bent;
111
+ #endif
103
112
  }
104
113
  `;
105
114
 
106
115
  /** The rigid variant: the world, a prop, a mesh with no rig behind it. */
107
116
  export const DEPTH_VERT = resolveConditionals(
108
117
  DEPTH_VERT_SOURCE,
109
- { SKINNED: false, INSTANCED: false, CUTOUT: false },
118
+ { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: false },
110
119
  'depth',
111
120
  );
112
121
 
113
122
  /** The skinned variant, which reads a joint palette and moves the vertex by it. */
114
123
  export const DEPTH_SKINNED_VERT = resolveConditionals(
115
124
  DEPTH_VERT_SOURCE,
116
- { SKINNED: true, INSTANCED: false, CUTOUT: false },
125
+ { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: false },
117
126
  'depth-skinned',
118
127
  );
119
128
 
@@ -127,7 +136,7 @@ export const DEPTH_SKINNED_VERT = resolveConditionals(
127
136
  */
128
137
  export const DEPTH_INSTANCED_VERT = resolveConditionals(
129
138
  DEPTH_VERT_SOURCE,
130
- { SKINNED: false, INSTANCED: true, CUTOUT: false },
139
+ { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: false },
131
140
  'depth-instanced',
132
141
  );
133
142
 
@@ -143,13 +152,13 @@ export const DEPTH_INSTANCED_VERT = resolveConditionals(
143
152
  */
144
153
  export const DEPTH_CUTOUT_VERT = resolveConditionals(
145
154
  DEPTH_VERT_SOURCE,
146
- { SKINNED: false, INSTANCED: false, CUTOUT: true },
155
+ { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: false },
147
156
  'depth-cutout',
148
157
  );
149
158
 
150
159
  export const DEPTH_INSTANCED_CUTOUT_VERT = resolveConditionals(
151
160
  DEPTH_VERT_SOURCE,
152
- { SKINNED: false, INSTANCED: true, CUTOUT: true },
161
+ { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: false },
153
162
  'depth-instanced-cutout',
154
163
  );
155
164
 
@@ -157,14 +166,14 @@ export const DEPTH_FRAG = `#version 300 es
157
166
  precision highp float;
158
167
  in vec4 vLightPosition;
159
168
 
160
- uniform highp sampler2D uPreviousShadowMap;
169
+ uniform highp sampler2DArray uPreviousShadowMap;
161
170
  uniform int uPeelShadowLayer;
162
171
 
163
172
  void main() {
164
173
  if (uPeelShadowLayer != 0) {
165
174
  vec3 p = vLightPosition.xyz / vLightPosition.w;
166
175
  vec2 uv = p.xy * 0.5 + 0.5;
167
- float previousDepth = texture(uPreviousShadowMap, uv).r;
176
+ float previousDepth = texture(uPreviousShadowMap, vec3(uv, ${SUN_STATIC_LAYER}.0)).r;
168
177
  // Discard the already-recorded surface and everything in front of it. The
169
178
  // ordinary depth test then stores the next independently fading occluder.
170
179
  if (gl_FragCoord.z <= previousDepth + 0.00001) discard;
@@ -190,7 +199,7 @@ in vec4 vLightPosition;
190
199
  in vec2 vUv;
191
200
  flat in float vAlphaCutout;
192
201
 
193
- uniform highp sampler2D uPreviousShadowMap;
202
+ uniform highp sampler2DArray uPreviousShadowMap;
194
203
  uniform int uPeelShadowLayer;
195
204
  uniform highp sampler2D uCutoutMap;
196
205
  ${CUTOUT_COVERAGE_GLSL}
@@ -200,9 +209,100 @@ void main() {
200
209
  if (uPeelShadowLayer != 0) {
201
210
  vec3 p = vLightPosition.xyz / vLightPosition.w;
202
211
  vec2 uv = p.xy * 0.5 + 0.5;
203
- float previousDepth = textureLod(uPreviousShadowMap, uv, 0.0).r;
212
+ float previousDepth = textureLod(uPreviousShadowMap, vec3(uv, ${SUN_STATIC_LAYER}.0), 0.0).r;
204
213
  if (gl_FragCoord.z <= previousDepth + 0.00001) discard;
205
214
  }
206
215
  if (alpha < vAlphaCutout) discard;
207
216
  }
208
217
  `;
218
+
219
+ /**
220
+ * The glass tint's vertex stages: every way a caster is drawn, with the world position handed on.
221
+ *
222
+ * **The same five as the depth pass, and for the same reason**: a pane must cast its colour from
223
+ * exactly where it casts its depth, deformed and placed the same way, or the coloured patch parts
224
+ * from the pane's own shadow edge. The only addition is `vGlassWorld`.
225
+ */
226
+ export const GLASS_TINT_VERT = resolveConditionals(
227
+ DEPTH_VERT_SOURCE,
228
+ { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: true },
229
+ 'glass-tint',
230
+ );
231
+ export const GLASS_TINT_SKINNED_VERT = resolveConditionals(
232
+ DEPTH_VERT_SOURCE,
233
+ { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: true },
234
+ 'glass-tint-skinned',
235
+ );
236
+ export const GLASS_TINT_INSTANCED_VERT = resolveConditionals(
237
+ DEPTH_VERT_SOURCE,
238
+ { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: true },
239
+ 'glass-tint-instanced',
240
+ );
241
+ export const GLASS_TINT_CUTOUT_VERT = resolveConditionals(
242
+ DEPTH_VERT_SOURCE,
243
+ { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: true },
244
+ 'glass-tint-cutout',
245
+ );
246
+ export const GLASS_TINT_INSTANCED_CUTOUT_VERT = resolveConditionals(
247
+ DEPTH_VERT_SOURCE,
248
+ { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: true },
249
+ 'glass-tint-instanced-cutout',
250
+ );
251
+
252
+ /**
253
+ * What a pane lets through: its transmission and colour, less what it reflects toward the light,
254
+ * and how clear it is — `glassShadow.ts`'s `paneTexel`, stated for the device. Drawn with a
255
+ * multiplying blend and no depth test, so every pane on a ray contributes whatever order they
256
+ * come in.
257
+ *
258
+ * **The pane's normal is its triangle's**, from the derivatives of its world position, first and in
259
+ * uniform control flow: a pane is flat, a sign does not matter to a cosine taken absolutely, and it
260
+ * spares the five vertex stages a normal they would carry for this alone. What it gives up is a
261
+ * curved pane, whose Fresnel steps per triangle; what would change it is glass that is mostly bulbs.
262
+ */
263
+ const GLASS_TINT_BODY = /* glsl */ `
264
+ vec3 n = normalize(cross(dFdx(vGlassWorld), dFdy(vGlassWorld)));
265
+ vec3 toLight = uGlassLight.w > 0.5 ? normalize(uGlassLight.xyz - vGlassWorld) : uGlassLight.xyz;
266
+ float cosLight = abs(dot(n, toLight));
267
+ float reflected = 0.04 + 0.96 * pow(1.0 - cosLight, 5.0);
268
+ outTint = vec4(uGlassPane.rgb * (1.0 - reflected), uGlassPane.a);
269
+ `;
270
+
271
+ export const GLASS_TINT_FRAG = `#version 300 es
272
+ precision highp float;
273
+ in vec4 vLightPosition;
274
+ in vec3 vGlassWorld;
275
+
276
+ /* rgb: transmission times tint; a: clarity, one less the frost. */
277
+ uniform vec4 uGlassPane;
278
+ /* xyz: the light's position (w 1) or the direction toward it (w 0). */
279
+ uniform vec4 uGlassLight;
280
+
281
+ out vec4 outTint;
282
+
283
+ void main() {
284
+ ${GLASS_TINT_BODY}
285
+ }
286
+ `;
287
+
288
+ /** A cut-out pane: its colour lands only where its depth does, by the depth cutout's own test. */
289
+ export const GLASS_TINT_CUTOUT_FRAG = `#version 300 es
290
+ precision highp float;
291
+ in vec4 vLightPosition;
292
+ in vec2 vUv;
293
+ flat in float vAlphaCutout;
294
+ in vec3 vGlassWorld;
295
+
296
+ uniform vec4 uGlassPane;
297
+ uniform vec4 uGlassLight;
298
+ uniform highp sampler2D uCutoutMap;
299
+ ${CUTOUT_COVERAGE_GLSL}
300
+ out vec4 outTint;
301
+
302
+ void main() {
303
+ /* First, in uniform control flow, as the depth cutout samples it. */
304
+ float alpha = cutoutAlpha(texture(uCutoutMap, vUv).a, vUv * vec2(textureSize(uCutoutMap, 0)));
305
+ ${GLASS_TINT_BODY}
306
+ if (alpha < vAlphaCutout) discard;
307
+ }
308
+ `;