@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
@@ -92,6 +92,11 @@ import {
92
92
  gpuDrivenSupported,
93
93
  recordGpuDrivenStages,
94
94
  } from '../../gpudriven/pipeline.ts';
95
+ import { resolveGlass, type GlassOptions, type ResolvedGlass } from '../../glass.ts';
96
+ import type { GlassShadows } from '../../renderQuality.ts';
97
+ import { GLASS_TINT_BLEND } from './glassTintPass.ts';
98
+ import { SnapshotMips } from './snapshotMips.ts';
99
+ import type { MipPipelines } from './surfaceTexturePass.ts';
95
100
  import type { GpuDrivenFrameShape, GpuDrivenPassName } from '../../gpudriven/pipeline.ts';
96
101
  import { createDeps, resetDeps } from '../../frame/deps.ts';
97
102
  import { createGraphPasses, createGraphScratch, scheduleGraph } from '../../frame/graphSchedule.ts';
@@ -369,6 +374,14 @@ export interface GpuDrivenMaterial {
369
374
  * blend and said nothing else gets — visible, and therefore findable.
370
375
  */
371
376
  readonly opacity?: number;
377
+ /**
378
+ * Glass: how much light passes, how milky it is, and the colour it takes — `GlassOptions`, as the
379
+ * forward path's `drawTranslucentMesh` takes it. Absent, or letting nothing through, is not glass.
380
+ *
381
+ * **A glass material is drawn in the blended half whatever `blend` says**, because a visibility
382
+ * buffer holds one surface a pixel and glass is the pane and what is behind it.
383
+ */
384
+ readonly glass?: GlassOptions;
372
385
  }
373
386
 
374
387
  /** What the pass needs told once a frame, before `beginFrame`. */
@@ -473,6 +486,12 @@ export interface GpuDrivenShadowOptions {
473
486
  * texels, and reaches toward the sun as far as the scene does.
474
487
  */
475
488
  readonly followRadius?: number;
489
+ /**
490
+ * What the sun keeps through glass: `RenderQualityOptions.glassShadows`, which a contributed pass
491
+ * is not handed and so takes here. `'full'` by default, as there; `'half'` halves the tint and
492
+ * `'off'` draws a glass material's shadow as nothing, which is what it cast before.
493
+ */
494
+ readonly glass?: GlassShadows;
476
495
  }
477
496
 
478
497
  interface Level {
@@ -675,6 +694,30 @@ export class GpuDrivenPass implements PassDefinition {
675
694
  private latentClamp: GPUSampler | null = null;
676
695
  private latentRepeat: GPUSampler | null = null;
677
696
  private shadowView: GPUTextureView | null = null;
697
+ /** The nearest pane's depth from the sun: the shadow texture's second layer, where glass casts. */
698
+ private glassDepthView: GPUTextureView | null = null;
699
+ /** What the panes let through, as the sun sees them, with the chain frost reads. */
700
+ private glassTintTexture: GPUTexture | null = null;
701
+ private glassTintTarget: GPUTextureView | null = null;
702
+ private glassTintSampled: GPUTextureView | null = null;
703
+ private glassTintMips: SnapshotMips | null = null;
704
+ /** One white texel, bound where no glass casts, which the lookup reads as none. */
705
+ private glassTintNone: GPUTexture | null = null;
706
+ private readonly glassMode: GlassShadows;
707
+ /** Whether the sun's glass pass runs at all: a glass material, and glass shadows not off. */
708
+ private readonly castsGlass: boolean;
709
+ private glassList: GPUBuffer | null = null;
710
+ private glassArgs: GPUBuffer | null = null;
711
+ private glassLodBuffer: GPUBuffer | null = null;
712
+ private readonly lodParamsGlass = new Float32Array(8);
713
+ private shadowCullSettingsGlass: GPUBuffer | null = null;
714
+ private glassLodGroup: GPUBindGroup | null = null;
715
+ private glassCullGroup: GPUBindGroup | null = null;
716
+ private glassCompactGroup: GPUBindGroup | null = null;
717
+ private glassDepthPipeline: GPURenderPipeline | null = null;
718
+ private glassDepthGroup: GPUBindGroup | null = null;
719
+ private glassTintPipeline: GPURenderPipeline | null = null;
720
+ private glassTintGroup: GPUBindGroup | null = null;
678
721
  /**
679
722
  * What is bound as the probe where the frame has none.
680
723
  *
@@ -790,7 +833,26 @@ export class GpuDrivenPass implements PassDefinition {
790
833
  private visibilityTexture: GPUTexture | null = null;
791
834
  private depthTexture: GPUTexture | null = null;
792
835
  private colourTexture: GPUTexture | null = null;
836
+ /** Level 0: what the shading writes, the resolve lays over and the blit presents. */
793
837
  private colourView: GPUTextureView | null = null;
838
+ /** Every level, for glass to read at the level its frost picks. Level 0 alone with no glass. */
839
+ private colourSampledView: GPUTextureView | null = null;
840
+ /** The chain glass reads, filled from the shaded picture each frame; null in a scene with none. */
841
+ private colourMips: SnapshotMips | null = null;
842
+ /** Whether any material is glass, which is what earns the colour its chain: see `buildTargets`. */
843
+ private readonly hasGlass: boolean;
844
+ /** The pipelines the chain draws with, one a format. */
845
+ private readonly mipPipelineCache = new Map<string, GPURenderPipeline>();
846
+ private readonly mipPipelines: MipPipelines = {
847
+ get: (key, describe) => {
848
+ const held = this.mipPipelineCache.get(key);
849
+ if (held !== undefined) return held;
850
+ const made = (this.device as GPUDevice).createRenderPipeline(describe());
851
+ this.mipPipelineCache.set(key, made);
852
+ return made;
853
+ },
854
+ };
855
+ private glassSampler: GPUSampler | null = null;
794
856
  private visibility: GPUBuffer | null = null;
795
857
  private hzb: GPUBuffer | null = null;
796
858
  private levelStart: GPUBuffer | null = null;
@@ -921,7 +983,10 @@ export class GpuDrivenPass implements PassDefinition {
921
983
  this.decode = packDecodeTables(this.programs.programs);
922
984
  this.scene = scene;
923
985
  this.materials = materials;
986
+ this.hasGlass = materials.some((material) => resolveGlass(material.glass, glassProbe));
924
987
  this.shadowSize = shadow.mapSize ?? DEFAULT_RENDER_QUALITY.directionalShadowMapSize;
988
+ this.glassMode = shadow.glass ?? DEFAULT_RENDER_QUALITY.glassShadows;
989
+ this.castsGlass = this.hasGlass && this.glassMode !== 'off';
925
990
  this.shadowMaxDistance =
926
991
  shadow.maxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance;
927
992
  this.shadowMaxSlope = shadow.maxSlope ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxSlope;
@@ -1163,6 +1228,10 @@ export class GpuDrivenPass implements PassDefinition {
1163
1228
  this.shadowArgs = this.storage(device, 'draw shadow', shadowArgs, INDIRECT | COPY_SRC);
1164
1229
  this.shadowArgsTwo = this.storage(device, 'draw shadow two', shadowArgs, INDIRECT | COPY_SRC);
1165
1230
  this.shadowSelected = this.empty(device, 'shadow selected', count * 4);
1231
+ /* The sun's glass: its own list and arguments, over the light's selection once the map is done. */
1232
+ this.glassList = this.empty(device, 'shadow glass list', count * 4);
1233
+ this.glassArgs = this.storage(device, 'draw shadow glass', shadowArgs, INDIRECT | COPY_SRC);
1234
+ this.glassLodBuffer = this.empty(device, 'lod params glass', 8 * 4);
1166
1235
  this.shadowKeep = this.empty(device, 'shadow keep', count * 4);
1167
1236
  this.shadowPlanes = this.empty(device, 'shadow planes', FRUSTUM_FLOATS * 4);
1168
1237
  this.shadowHistories = [
@@ -1198,6 +1267,12 @@ export class GpuDrivenPass implements PassDefinition {
1198
1267
  'cull settings, shadow two',
1199
1268
  this.shadowCullValues,
1200
1269
  );
1270
+ /* The glass pass's: no pyramid and no cones, since a pane casts from either side. */
1271
+ this.shadowCullSettingsGlass = this.storage(
1272
+ device,
1273
+ 'cull settings, shadow glass',
1274
+ this.shadowCullValues,
1275
+ );
1201
1276
  this.shadowCompactSettings = this.storage(
1202
1277
  device,
1203
1278
  'phase shadow',
@@ -1219,11 +1294,61 @@ export class GpuDrivenPass implements PassDefinition {
1219
1294
  */
1220
1295
  this.shadowTexture = device.createTexture({
1221
1296
  label: 'gpu-driven shadow',
1222
- size: { width: this.shadowSize, height: this.shadowSize },
1297
+ /* A second layer for the nearest pane where a material is glass, and none where none is. */
1298
+ size: {
1299
+ width: this.shadowSize,
1300
+ height: this.shadowSize,
1301
+ depthOrArrayLayers: this.castsGlass ? 2 : 1,
1302
+ },
1223
1303
  format: SHADOW_FORMAT,
1224
1304
  usage: RENDER_ATTACHMENT | TEXTURE_BINDING,
1225
1305
  });
1226
- this.shadowView = this.shadowTexture.createView();
1306
+ const layer = (at: number): GPUTextureView =>
1307
+ (this.shadowTexture as GPUTexture).createView({
1308
+ dimension: '2d',
1309
+ baseArrayLayer: at,
1310
+ arrayLayerCount: 1,
1311
+ });
1312
+ this.shadowView = layer(0);
1313
+ this.glassDepthView = this.castsGlass ? layer(1) : null;
1314
+ const tintSize =
1315
+ this.glassMode === 'half' ? Math.max(1, this.shadowSize >> 1) : this.shadowSize;
1316
+ this.glassTintTexture = this.castsGlass
1317
+ ? device.createTexture({
1318
+ label: 'gpu-driven glass tint',
1319
+ size: { width: tintSize, height: tintSize },
1320
+ format: 'rgba8unorm',
1321
+ mipLevelCount: Math.floor(Math.log2(tintSize)) + 1,
1322
+ usage: RENDER_ATTACHMENT | TEXTURE_BINDING,
1323
+ })
1324
+ : null;
1325
+ if (this.glassTintTexture !== null) {
1326
+ this.glassTintTarget = this.glassTintTexture.createView({
1327
+ baseMipLevel: 0,
1328
+ mipLevelCount: 1,
1329
+ });
1330
+ this.glassTintSampled = this.glassTintTexture.createView();
1331
+ this.glassTintMips = new SnapshotMips(
1332
+ device,
1333
+ this.mipPipelines,
1334
+ this.glassTintTexture,
1335
+ 'rgba8unorm',
1336
+ null,
1337
+ 'gpu-driven glass tint mips',
1338
+ );
1339
+ }
1340
+ this.glassTintNone = device.createTexture({
1341
+ label: 'gpu-driven no glass',
1342
+ size: { width: 1, height: 1 },
1343
+ format: 'rgba8unorm',
1344
+ usage: TEXTURE_BINDING | TEXTURE_COPY_DST,
1345
+ });
1346
+ device.queue.writeTexture(
1347
+ { texture: this.glassTintNone },
1348
+ new Uint8Array([255, 255, 255, 255]),
1349
+ { bytesPerRow: 4 },
1350
+ { width: 1, height: 1 },
1351
+ );
1227
1352
 
1228
1353
  this.emptyEnvironment = device.createTexture({
1229
1354
  label: 'gpu-driven no environment',
@@ -1538,6 +1663,37 @@ export class GpuDrivenPass implements PassDefinition {
1538
1663
  });
1539
1664
  this.shadowGroup = shadowRasterFor(this.shadowList as GPUBuffer);
1540
1665
  this.shadowGroupTwo = shadowRasterFor(this.shadowListTwo as GPUBuffer);
1666
+ /*
1667
+ * **The nearest pane, from the sun: the same raster, culling nothing**, since a pane casts from
1668
+ * either side; into the shadow texture's second layer. Built only where glass casts.
1669
+ */
1670
+ if (this.castsGlass) {
1671
+ this.glassDepthPipeline = device.createRenderPipeline({
1672
+ label: 'gpu-driven shadow glass',
1673
+ layout: 'auto',
1674
+ vertex: { module: raster, entryPoint: 'vertexMain' },
1675
+ primitive: { topology: 'triangle-list', cullMode: 'none', frontFace: 'cw' },
1676
+ depthStencil: {
1677
+ format: SHADOW_FORMAT,
1678
+ depthWriteEnabled: true,
1679
+ depthCompare: 'less',
1680
+ depthBias: 4,
1681
+ depthBiasSlopeScale: 1.1,
1682
+ },
1683
+ });
1684
+ this.glassDepthGroup = device.createBindGroup({
1685
+ layout: this.glassDepthPipeline.getBindGroupLayout(0),
1686
+ entries: [
1687
+ { binding: 0, resource: { buffer: this.shadowMatrix as GPUBuffer } },
1688
+ { binding: 1, resource: { buffer: this.glassList as GPUBuffer } },
1689
+ { binding: 2, resource: { buffer: this.clusterMeta as GPUBuffer } },
1690
+ { binding: 3, resource: { buffer: this.indices as GPUBuffer } },
1691
+ { binding: 4, resource: { buffer: this.vertices as GPUBuffer } },
1692
+ { binding: 5, resource: { buffer: this.transformBuffer as GPUBuffer } },
1693
+ { binding: 6, resource: { buffer: this.materialOf as GPUBuffer } },
1694
+ ],
1695
+ });
1696
+ }
1541
1697
 
1542
1698
  /*
1543
1699
  * **The transparent half's raster: one indirect draw into two targets at once.**
@@ -1604,6 +1760,38 @@ export class GpuDrivenPass implements PassDefinition {
1604
1760
  },
1605
1761
  });
1606
1762
  this.watchBlendPipeline(device);
1763
+ /*
1764
+ * **What a pane lets through, from the sun**: the blended raster's vertex stage handed the
1765
+ * light's matrix and the glass list, and \`glassTintFrag\` for the colour — white target, a
1766
+ * multiplying blend, no depth, so every pane on a ray counts in any order.
1767
+ */
1768
+ if (this.castsGlass) {
1769
+ this.glassTintPipeline = device.createRenderPipeline({
1770
+ label: 'gpu-driven shadow tint',
1771
+ layout: 'auto',
1772
+ vertex: { module: blend, entryPoint: 'blendVert' },
1773
+ fragment: {
1774
+ module: blend,
1775
+ entryPoint: 'glassTintFrag',
1776
+ targets: [{ format: 'rgba8unorm', blend: GLASS_TINT_BLEND }],
1777
+ },
1778
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
1779
+ });
1780
+ this.glassTintGroup = device.createBindGroup({
1781
+ layout: this.glassTintPipeline.getBindGroupLayout(0),
1782
+ entries: [
1783
+ { binding: 0, resource: { buffer: this.shadowMatrix as GPUBuffer } },
1784
+ { binding: 1, resource: { buffer: this.glassList as GPUBuffer } },
1785
+ { binding: 2, resource: { buffer: this.clusterMeta as GPUBuffer } },
1786
+ { binding: 3, resource: { buffer: this.indices as GPUBuffer } },
1787
+ { binding: 4, resource: { buffer: this.vertices as GPUBuffer } },
1788
+ { binding: 5, resource: { buffer: this.transformBuffer as GPUBuffer } },
1789
+ { binding: 6, resource: { buffer: this.materialOf as GPUBuffer } },
1790
+ { binding: 7, resource: { buffer: this.materialTable as GPUBuffer } },
1791
+ { binding: 8, resource: { buffer: this.frameBuffer as GPUBuffer } },
1792
+ ],
1793
+ });
1794
+ }
1607
1795
 
1608
1796
  /*
1609
1797
  * **Premultiplied over, where `renderer.ts`'s composite is a lerp**, and `blendRaster.wgsl.ts`
@@ -1796,6 +1984,39 @@ export class GpuDrivenPass implements PassDefinition {
1796
1984
  shadowCullFor(this.shadowCullSettings as GPUBuffer),
1797
1985
  shadowCullFor(this.shadowCullSettingsTwo as GPUBuffer),
1798
1986
  ];
1987
+ /*
1988
+ * **The sun's glass cut: the light's selection again, of the glass alone**, once the map is
1989
+ * drawn and those buffers are free. The camera's level of detail, as the map's; every pane in
1990
+ * the light's frustum kept, as the blended half keeps every pane — so compacted as phase two
1991
+ * against a history of zeros, exactly as that half is.
1992
+ */
1993
+ if (this.castsGlass) {
1994
+ this.glassLodGroup = device.createBindGroup({
1995
+ layout: (this.lodPipeline as GPUComputePipeline).getBindGroupLayout(0),
1996
+ entries: [
1997
+ { binding: 0, resource: { buffer: this.glassLodBuffer as GPUBuffer } },
1998
+ { binding: 1, resource: { buffer: this.clusterLod as GPUBuffer } },
1999
+ { binding: 2, resource: { buffer: this.shadowSelected as GPUBuffer } },
2000
+ { binding: 3, resource: { buffer: this.clusterMeta as GPUBuffer } },
2001
+ { binding: 4, resource: { buffer: this.instanceNone as GPUBuffer } },
2002
+ { binding: 5, resource: { buffer: this.materialOf as GPUBuffer } },
2003
+ { binding: 6, resource: { buffer: this.materialTable as GPUBuffer } },
2004
+ ],
2005
+ });
2006
+ this.glassCullGroup = shadowCullFor(this.shadowCullSettingsGlass as GPUBuffer);
2007
+ this.glassCompactGroup = device.createBindGroup({
2008
+ layout: (this.compactPipeline as GPUComputePipeline).getBindGroupLayout(0),
2009
+ entries: [
2010
+ { binding: 0, resource: { buffer: this.blendCompactSettings as GPUBuffer } },
2011
+ { binding: 1, resource: { buffer: this.shadowSelected as GPUBuffer } },
2012
+ { binding: 2, resource: { buffer: this.shadowKeep as GPUBuffer } },
2013
+ { binding: 3, resource: { buffer: this.zeroHistory as GPUBuffer } },
2014
+ { binding: 4, resource: { buffer: this.glassList as GPUBuffer } },
2015
+ { binding: 5, resource: { buffer: this.historySink as GPUBuffer } },
2016
+ { binding: 6, resource: { buffer: this.glassArgs as GPUBuffer } },
2017
+ ],
2018
+ });
2019
+ }
1799
2020
  this.shadowSeedGroup = device.createBindGroup({
1800
2021
  layout: (this.shadowSeedPipeline as GPUComputePipeline).getBindGroupLayout(0),
1801
2022
  entries: [
@@ -1998,11 +2219,29 @@ export class GpuDrivenPass implements PassDefinition {
1998
2219
  label: 'gpu-driven colour',
1999
2220
  size: { width: stride, height },
2000
2221
  format: 'rgba16float',
2222
+ /*
2223
+ * **A chain only where a material is glass**, which a frosted pane reads a coarser level of,
2224
+ * as the forward path's refraction copy does. A scene with no glass allocates exactly the one
2225
+ * level it always did.
2226
+ */
2227
+ mipLevelCount: this.hasGlass ? Math.floor(Math.log2(Math.max(stride, height))) + 1 : 1,
2001
2228
  /* `RENDER_ATTACHMENT` only so a clear-only render pass can empty it each frame: a storage
2002
2229
  texture has no clear, and the alternative is a dispatch over every pixel. */
2003
2230
  usage: STORAGE_BINDING | TEXTURE_BINDING | RENDER_ATTACHMENT,
2004
2231
  });
2005
- this.colourView = this.colourTexture.createView();
2232
+ /* A storage binding takes one level, so everything that writes or presents reads level 0. */
2233
+ this.colourView = this.colourTexture.createView({ baseMipLevel: 0, mipLevelCount: 1 });
2234
+ this.colourSampledView = this.colourTexture.createView();
2235
+ this.colourMips = this.hasGlass
2236
+ ? new SnapshotMips(
2237
+ device,
2238
+ this.mipPipelines,
2239
+ this.colourTexture,
2240
+ 'rgba16float',
2241
+ null,
2242
+ 'gpu-driven colour mips',
2243
+ )
2244
+ : null;
2006
2245
  this.oitAccum = device.createTexture({
2007
2246
  label: 'gpu-driven oit accum',
2008
2247
  size: { width: stride, height },
@@ -2158,13 +2397,23 @@ export class GpuDrivenPass implements PassDefinition {
2158
2397
  { binding: 16, resource: this.latentRepeat as GPUSampler },
2159
2398
  { binding: 17, resource: { buffer: this.decodeNodes as GPUBuffer } },
2160
2399
  { binding: 18, resource: { buffer: this.decodeWeights as GPUBuffer } },
2400
+ /* The sun's glass, or the opaque map and a white texel where none casts. */
2401
+ {
2402
+ binding: 19,
2403
+ resource: this.glassDepthView ?? (this.shadowView as GPUTextureView),
2404
+ },
2405
+ {
2406
+ binding: 20,
2407
+ resource: this.glassTintSampled ?? (this.glassTintNone as GPUTexture).createView(),
2408
+ },
2409
+ { binding: 21, resource: this.glassSamplerFor(device) },
2161
2410
  ],
2162
2411
  });
2163
2412
 
2164
2413
  this.blitGroup = device.createBindGroup({
2165
2414
  layout: (this.blitPipeline as GPURenderPipeline).getBindGroupLayout(0),
2166
2415
  entries: [
2167
- { binding: 0, resource: (this.colourTexture as GPUTexture).createView() },
2416
+ { binding: 0, resource: this.colourView as GPUTextureView },
2168
2417
  /* The depth the raster wrote, sampled so the blit can hand it to the frame. */
2169
2418
  ...(this.presentDepth
2170
2419
  ? [
@@ -2209,6 +2458,12 @@ export class GpuDrivenPass implements PassDefinition {
2209
2458
  { binding: 13, resource: this.latentRepeat as GPUSampler },
2210
2459
  { binding: 14, resource: { buffer: this.decodeNodes as GPUBuffer } },
2211
2460
  { binding: 15, resource: { buffer: this.decodeWeights as GPUBuffer } },
2461
+ /* What the opaque half drew, for glass to show: `blendRaster.wgsl.ts`. */
2462
+ { binding: 16, resource: this.colourSampledView as GPUTextureView },
2463
+ {
2464
+ binding: 17,
2465
+ resource: this.glassSamplerFor(device),
2466
+ },
2212
2467
  ],
2213
2468
  });
2214
2469
 
@@ -2467,6 +2722,16 @@ export class GpuDrivenPass implements PassDefinition {
2467
2722
  queue.writeBuffer(this.shadowCullSettings as GPUBuffer, 0, this.shadowCullValues);
2468
2723
  this.shadowCullValues[3] = this.occlusion ? 1 : 0;
2469
2724
  queue.writeBuffer(this.shadowCullSettingsTwo as GPUBuffer, 0, this.shadowCullValues);
2725
+ if (this.castsGlass) {
2726
+ /* No pyramid and no cones, and the cones put back for the next frame's first write. */
2727
+ this.shadowCullValues[3] = 0;
2728
+ this.shadowCullValues[CULL_CONES] = 0;
2729
+ queue.writeBuffer(this.shadowCullSettingsGlass as GPUBuffer, 0, this.shadowCullValues);
2730
+ this.shadowCullValues[CULL_CONES] = 1;
2731
+ this.lodParamsGlass.set(this.lodParamsBlend);
2732
+ this.lodParamsGlass[7] = 1;
2733
+ queue.writeBuffer(this.glassLodBuffer as GPUBuffer, 0, this.lodParamsGlass);
2734
+ }
2470
2735
  queue.writeBuffer(this.planesBuffer as GPUBuffer, 0, this.planes);
2471
2736
  queue.writeBuffer(this.frameBuffer as GPUBuffer, 0, params);
2472
2737
  queue.writeBuffer(this.lodBuffer as GPUBuffer, 0, this.lodParams);
@@ -2524,7 +2789,9 @@ export class GpuDrivenPass implements PassDefinition {
2524
2789
  * one: the table is written when the pass is built. A scene with nothing blended schedules the
2525
2790
  * three transparent stages away entirely and encodes the frame it encoded before they existed.
2526
2791
  */
2527
- this.shape.blended = this.materials.some((material) => material.blend === true);
2792
+ /* Glass is the blended half's whatever its flag says: a visibility buffer holds one surface. */
2793
+ this.shape.blended =
2794
+ this.hasGlass || this.materials.some((material) => material.blend === true);
2528
2795
 
2529
2796
  /* `instanceCount` is the one word the compaction touches; the other three never change. */
2530
2797
  encoder.clearBuffer(argsOne, 4, 4);
@@ -2707,6 +2974,8 @@ export class GpuDrivenPass implements PassDefinition {
2707
2974
  */
2708
2975
  if (!this.blendUsable || this.blendPipeline === null || this.blendRasterGroup === null)
2709
2976
  return;
2977
+ /* The shaded picture's chain, which glass reads: after `shade`, before the panes. */
2978
+ this.colourMips?.record(encoder);
2710
2979
  const pass = encoder.beginRenderPass({
2711
2980
  label: 'gpu-driven blend draw',
2712
2981
  colorAttachments: [
@@ -3033,6 +3302,59 @@ export class GpuDrivenPass implements PassDefinition {
3033
3302
  second.end();
3034
3303
  this.drawShadowHalf(encoder, 1, writes);
3035
3304
  this.shadowFlipped = !this.shadowFlipped;
3305
+ if (this.castsGlass && this.blendUsable) this.drawShadowGlass(encoder, groups);
3306
+ }
3307
+
3308
+ /**
3309
+ * The sun's glass, after its opaque map: the glass cut from the light, then the panes drawn
3310
+ * twice — where the nearest one is, and what they let through, multiplied — and the tint's
3311
+ * chain filled for frost to read. The forward path's `flushSunGlass`, in this pipeline's terms.
3312
+ */
3313
+ private drawShadowGlass(encoder: GPUCommandEncoder, groups: number): void {
3314
+ encoder.clearBuffer(this.glassArgs as GPUBuffer, 4, 4);
3315
+ const cut = encoder.beginComputePass({ label: 'gpu-driven shadow glass cut' });
3316
+ cut.setPipeline(this.lodPipeline as GPUComputePipeline);
3317
+ cut.setBindGroup(0, this.glassLodGroup as GPUBindGroup);
3318
+ cut.dispatchWorkgroups(groups);
3319
+ cut.setPipeline(this.cullPipeline as GPUComputePipeline);
3320
+ cut.setBindGroup(0, this.glassCullGroup as GPUBindGroup);
3321
+ cut.dispatchWorkgroups(groups);
3322
+ cut.setPipeline(this.compactPipeline as GPUComputePipeline);
3323
+ cut.setBindGroup(0, this.glassCompactGroup as GPUBindGroup);
3324
+ cut.dispatchWorkgroups(groups);
3325
+ cut.end();
3326
+
3327
+ const depth = encoder.beginRenderPass({
3328
+ label: 'gpu-driven shadow glass',
3329
+ colorAttachments: [],
3330
+ depthStencilAttachment: {
3331
+ view: this.glassDepthView as GPUTextureView,
3332
+ depthClearValue: SHADOW_DEPTH_CLEAR,
3333
+ depthLoadOp: 'clear',
3334
+ depthStoreOp: 'store',
3335
+ },
3336
+ });
3337
+ depth.setPipeline(this.glassDepthPipeline as GPURenderPipeline);
3338
+ depth.setBindGroup(0, this.glassDepthGroup as GPUBindGroup);
3339
+ depth.drawIndirect(this.glassArgs as GPUBuffer, 0);
3340
+ depth.end();
3341
+
3342
+ const tint = encoder.beginRenderPass({
3343
+ label: 'gpu-driven shadow tint',
3344
+ colorAttachments: [
3345
+ {
3346
+ view: this.glassTintTarget as GPUTextureView,
3347
+ clearValue: { r: 1, g: 1, b: 1, a: 1 },
3348
+ loadOp: 'clear',
3349
+ storeOp: 'store',
3350
+ },
3351
+ ],
3352
+ });
3353
+ tint.setPipeline(this.glassTintPipeline as GPURenderPipeline);
3354
+ tint.setBindGroup(0, this.glassTintGroup as GPUBindGroup);
3355
+ tint.drawIndirect(this.glassArgs as GPUBuffer, 0);
3356
+ tint.end();
3357
+ this.glassTintMips?.record(encoder);
3036
3358
  }
3037
3359
 
3038
3360
  /** One half of the map: the first clears it, the second draws over it and closes the stage's time. */
@@ -3061,6 +3383,19 @@ export class GpuDrivenPass implements PassDefinition {
3061
3383
  pass.end();
3062
3384
  }
3063
3385
 
3386
+ /** The linear sampler glass reads the colour's and the tint's chains through, made once. */
3387
+ private glassSamplerFor(device: GPUDevice): GPUSampler {
3388
+ this.glassSampler ??= device.createSampler({
3389
+ label: 'gpu-driven glass',
3390
+ magFilter: 'linear',
3391
+ minFilter: 'linear',
3392
+ mipmapFilter: 'linear',
3393
+ addressModeU: 'clamp-to-edge',
3394
+ addressModeV: 'clamp-to-edge',
3395
+ });
3396
+ return this.glassSampler;
3397
+ }
3398
+
3064
3399
  private drawPhase(encoder: GPUCommandEncoder, phase: 0 | 1, clear: boolean): void {
3065
3400
  const pass = encoder.beginRenderPass({
3066
3401
  label: `gpu-driven phase ${phase + 1}`,
@@ -3118,6 +3453,8 @@ export class GpuDrivenPass implements PassDefinition {
3118
3453
  if (device.backend !== 'webgpu') return;
3119
3454
  this.disposeTargets();
3120
3455
  this.shadowTexture?.destroy();
3456
+ this.glassTintTexture?.destroy();
3457
+ this.glassTintNone?.destroy();
3121
3458
  this.emptyEnvironment?.destroy();
3122
3459
  this.latents?.destroy();
3123
3460
  this.queries?.destroy();
@@ -3156,6 +3493,10 @@ export class GpuDrivenPass implements PassDefinition {
3156
3493
  this.shadowListTwo,
3157
3494
  this.shadowSelected,
3158
3495
  this.shadowKeep,
3496
+ this.glassList,
3497
+ this.glassArgs,
3498
+ this.glassLodBuffer,
3499
+ this.shadowCullSettingsGlass,
3159
3500
  this.shadowPlanes,
3160
3501
  this.shadowCullSettings,
3161
3502
  this.shadowCullSettingsTwo,
@@ -3200,3 +3541,6 @@ function multiply(out: Float32Array, a: ArrayLike<number>, b: ArrayLike<number>)
3200
3541
  }
3201
3542
  }
3202
3543
  }
3544
+
3545
+ /** Where `hasGlass` reads each material's glass through; only the answer is kept. */
3546
+ const glassProbe: ResolvedGlass = { transmission: 0, frost: 0, tint: [1, 1, 1] };
@@ -36,20 +36,40 @@ export class GpuInstancedBatch {
36
36
  });
37
37
  }
38
38
 
39
+ /** Last frame's packed slots, for a reconstruction; made on first use by the renderer. */
40
+ previousBuffer: GPUBuffer | null = null;
41
+ /** How many slots the upload before the last one held, and how many the last one holds. */
42
+ previousCount = 0;
43
+ liveCount = 0;
44
+ /** The frame of the last upload. See `changeFrames.ts`. */
45
+ changed = -1;
46
+
39
47
  /**
40
48
  * Pack and push `data.count` instances, in one write.
41
49
  *
42
50
  * Only the live prefix, for the reason `uploadScatter` gives: the rest is not drawn and
43
51
  * uploading it would be per-frame waste with nothing reading it.
52
+ *
53
+ * **Given `previous`, the placement still in the staging array — the last upload, which is last
54
+ * frame's — is written there first**, so a reconstruction can pair each slot with where it was.
55
+ * `writeBuffer` copies at the call, so packing afterwards cannot reach that copy.
44
56
  */
45
- upload(queue: GPUQueue, data: MeshInstances): void {
57
+ upload(queue: GPUQueue, data: MeshInstances, previous: GPUBuffer | null = null): void {
46
58
  const count = Math.min(data.count, this.capacity);
59
+ if (previous !== null) {
60
+ if (this.liveCount > 0) {
61
+ queue.writeBuffer(previous, 0, this.staging, 0, this.liveCount * INSTANCE_FLOATS);
62
+ }
63
+ this.previousCount = this.liveCount;
64
+ }
65
+ this.liveCount = count;
47
66
  if (count === 0) return;
48
67
  packInstances(data, this.staging);
49
68
  queue.writeBuffer(this.buffer, 0, this.staging, 0, count * INSTANCE_FLOATS);
50
69
  }
51
70
 
52
71
  dispose(): void {
72
+ this.previousBuffer?.destroy();
53
73
  this.buffer.destroy();
54
74
  }
55
75
  }