@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
@@ -125,6 +125,12 @@ export interface GpuMesh {
125
125
  * its own does.
126
126
  */
127
127
  readonly vertexStride: number;
128
+ /**
129
+ * Byte offsets of the joint indices and weights in the interleaved vertex, or null for a mesh
130
+ * that supplied neither. For a pass that skins through a shader of its own — the motion pass —
131
+ * which has to find them without the `present` map the generated pipelines are keyed by.
132
+ */
133
+ readonly skinOffsets: { readonly joints: number; readonly weights: number } | null;
128
134
  readonly indexBuffer: GPUBuffer;
129
135
  readonly indexCount: number;
130
136
  /**
@@ -192,7 +198,19 @@ export interface GpuMesh {
192
198
  * than every mesh paying it.
193
199
  */
194
200
  readonly update:
195
- ((device: GPUDevice, positions: Float32Array, normals?: Float32Array) => void) | null;
201
+ | ((
202
+ device: GPUDevice,
203
+ positions: Float32Array,
204
+ normals?: Float32Array,
205
+ previous?: GPUBuffer | null,
206
+ ) => void)
207
+ | null;
208
+ /**
209
+ * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's interleaved
210
+ * rows, made the first time a reconstructing renderer rewrites the mesh, and the frame of the
211
+ * last rewrite. Null on a mesh created without `{ dynamic: true }`, which never moves a vertex.
212
+ */
213
+ readonly motion: { previous: GPUBuffer | null; changed: number } | null;
196
214
  dispose(): void;
197
215
  }
198
216
 
@@ -377,6 +395,7 @@ export function createGpuMeshIncremental(
377
395
  progress,
378
396
  hasTangents: data.tangents !== undefined,
379
397
  isSkinned: data.joints !== undefined,
398
+ skinOffsets: skinOffsetsOf(supplied),
380
399
  hasChannel: data.channel !== undefined,
381
400
  morph:
382
401
  data.morphTargets === undefined || data.morphTargetCount === undefined
@@ -505,6 +524,22 @@ function interleaveField(
505
524
  }
506
525
  }
507
526
 
527
+ /**
528
+ * Where the joints and weights sit in a vertex of `supplied`, the attributes a mesh interleaves in
529
+ * layout order; null unless it supplied both.
530
+ */
531
+ function skinOffsetsOf(supplied: readonly VertexAttribute[]): GpuMesh['skinOffsets'] {
532
+ let floats = 0;
533
+ let joints = -1;
534
+ let weights = -1;
535
+ for (const attribute of supplied) {
536
+ if (attribute.name === 'joints') joints = floats * 4;
537
+ if (attribute.name === 'weights') weights = floats * 4;
538
+ floats += attribute.components;
539
+ }
540
+ return joints >= 0 && weights >= 0 ? { joints, weights } : null;
541
+ }
542
+
508
543
  /** The handle, made where it can see only what it hands out. */
509
544
  function gpuMeshOf(parts: {
510
545
  readonly vertices: GPUBuffer;
@@ -516,14 +551,17 @@ function gpuMeshOf(parts: {
516
551
  readonly progress: { readonly uploaded: boolean };
517
552
  readonly hasTangents: boolean;
518
553
  readonly isSkinned: boolean;
554
+ readonly skinOffsets: GpuMesh['skinOffsets'];
519
555
  readonly hasChannel: boolean;
520
556
  readonly morph: MorphTexture | null;
521
557
  readonly update: GpuMesh['update'];
522
558
  }): GpuMesh {
559
+ const motion = parts.update === null ? null : { previous: null as GPUBuffer | null, changed: -1 };
523
560
  const { vertices, constants, indexBuffer, progress } = parts;
524
561
  return {
525
562
  vertexBuffers: [vertices, constants],
526
563
  vertexStride: parts.stride,
564
+ skinOffsets: parts.skinOffsets,
527
565
  indexBuffer,
528
566
  indexCount: parts.indexCount,
529
567
  bounds: parts.bounds,
@@ -535,7 +573,9 @@ function gpuMeshOf(parts: {
535
573
  hasChannel: parts.hasChannel,
536
574
  morph: parts.morph,
537
575
  update: parts.update,
576
+ motion,
538
577
  dispose(): void {
578
+ motion?.previous?.destroy();
539
579
  vertices.destroy();
540
580
  constants.destroy();
541
581
  indexBuffer.destroy();
@@ -557,7 +597,12 @@ function dynamicUpdate(
557
597
  positionField: number,
558
598
  normalField: number,
559
599
  ): NonNullable<GpuMesh['update']> {
560
- return (target: GPUDevice, positions: Float32Array, normals?: Float32Array): void => {
600
+ return (
601
+ target: GPUDevice,
602
+ positions: Float32Array,
603
+ normals?: Float32Array,
604
+ previous: GPUBuffer | null = null,
605
+ ): void => {
561
606
  if (positions.length !== vertexCount * 3) {
562
607
  throw new Error(
563
608
  `this mesh has ${vertexCount} vertices and the update has ${positions.length / 3}. ` +
@@ -565,6 +610,9 @@ function dynamicUpdate(
565
610
  'every other attribute are sized against it.',
566
611
  );
567
612
  }
613
+ /* The rows as they stand are last frame's, handed over before a position of them changes.
614
+ `writeBuffer` copies at the call, so patching the array afterwards cannot reach this copy. */
615
+ if (previous !== null) target.queue.writeBuffer(previous, 0, interleaved);
568
616
  /* Everything that decides whether this is on screen starts from the bounds, so a cloth
569
617
  that blew sideways out of its original box would be culled while still visible. */
570
618
  boundsOfPositions(positions, bounds);
@@ -122,7 +122,8 @@ export function createDepthBindGroupLayout(
122
122
  {
123
123
  binding: PREVIOUS.texture,
124
124
  visibility: VISIBILITY_FRAGMENT,
125
- texture: { sampleType: 'unfilterable-float' },
125
+ /* An array since the sun's maps became one (`shadowMap.ts`): the peel reads its static layer. */
126
+ texture: { sampleType: 'unfilterable-float', viewDimension: '2d-array' },
126
127
  },
127
128
  {
128
129
  binding: PREVIOUS.sampler,
@@ -82,6 +82,8 @@ export interface StageBindings {
82
82
  >
83
83
  >;
84
84
  readonly textures: Readonly<Record<string, TextureBinding>>;
85
+ /** Each pipeline-overridable constant's id, by the GLSL name it was declared under. */
86
+ readonly overrides?: Readonly<Record<string, number>>;
85
87
  }
86
88
 
87
89
  /*
@@ -188,6 +190,20 @@ export function flatFragmentBindings(variant: FlatVariant): StageBindings {
188
190
  return found;
189
191
  }
190
192
 
193
+ /**
194
+ * The lit fragment stage's constants, set from the cache: the glass switch, keyed by its **id**.
195
+ *
196
+ * Not by its name, because an override declared with `@id` answers to the id alone — WebGPU's
197
+ * "pipeline-overridable constant identifier string" — and naga writes one on every specialisation
198
+ * constant. Keyed by name, Chrome refused every lit pipeline: *Pipeline overridable constant
199
+ * "GLASS_SHADOWS" not found*, and the frame drew nothing.
200
+ */
201
+ function flatFragmentConstants(variant: FlatVariant, cache: PipelineCache): Record<string, number> {
202
+ const id = flatFragmentBindings(variant).overrides?.['GLASS_SHADOWS'];
203
+ if (id === undefined) throw new Error(`flatPass: variant "${variant}" has no glass switch`);
204
+ return { [String(id)]: cache.glassShadows ? 1 : 0 };
205
+ }
206
+
191
207
  /** The depth format both backends compare with. */
192
208
  /* Re-exported so the twenty passes that already import it from here do not each learn a new
193
209
  module; `depthConvention.ts` is where the decision lives and why. */
@@ -272,8 +288,11 @@ export function createFlatBindGroupLayout(
272
288
  const integer = texture.type === 'usampler2D' || texture.type === 'usampler3D';
273
289
  /* DriftLight's brick index and atlas, the lit pass's only volumes. See `driftLight.ts`. */
274
290
  const volume = texture.type === 'sampler3D' || texture.type === 'usampler3D';
291
+ /* Glass tints are colour, mipmapped and filtered: a frosted pane is read at a coarser level. */
292
+ const tint = name.endsWith('GlassTints');
275
293
  const isShadow =
276
- integer || (name !== 'uEnvironment' && (name.endsWith('ShadowMap') || cube || array));
294
+ integer ||
295
+ (!tint && name !== 'uEnvironment' && (name.endsWith('ShadowMap') || cube || array));
277
296
  entries.push({
278
297
  binding: texture.texture,
279
298
  visibility: VISIBILITY_FRAGMENT,
@@ -603,6 +622,8 @@ function flatDescriptor(
603
622
  code: FLAT_FRAG_WGSL[variant] ?? '',
604
623
  }),
605
624
  entryPoint: 'main',
625
+ /* Every variant declares it, so every lit pipeline may set it: see `PipelineCache`. */
626
+ constants: flatFragmentConstants(variant, cache),
606
627
  targets: [
607
628
  oitTarget(oit) ??
608
629
  (translucent
@@ -0,0 +1,244 @@
1
+ /**
2
+ * What a pane lets through, drawn into a shadow's tint layer on this device: the layouts, the
3
+ * per-draw block, the bind groups and the pipelines for the five tint programs (`depth.ts`).
4
+ *
5
+ * **The vertex half is the depth pass's**, block for block — the tint's vertex stages are the depth
6
+ * ones with the world position handed on — so a draw's vertex slot comes from the shadow pass's own
7
+ * ring and only the pane's 32 bytes are this file's. **The fragment half multiplies**: an
8
+ * `rgba8unorm` target cleared to white, `dst × src` for colour and `dst-alpha × src` for clarity, and
9
+ * no depth, so every pane on a ray counts in any order. Built the first time glass casts, so a scene
10
+ * with no glass builds none of it.
11
+ */
12
+ import { DEPTH_BINDINGS } from '../../shaders/generated/depth.wgsl.ts';
13
+ import {
14
+ GLASS_TINT_CUTOUT_FRAG_WGSL,
15
+ GLASS_TINT_CUTOUT_VERT_WGSL,
16
+ GLASS_TINT_FRAG_WGSL,
17
+ GLASS_TINT_INSTANCED_CUTOUT_VERT_WGSL,
18
+ GLASS_TINT_INSTANCED_VERT_WGSL,
19
+ GLASS_TINT_SKINNED_VERT_WGSL,
20
+ GLASS_TINT_VERT_WGSL,
21
+ } from '../../shaders/generated/depth.wgsl.ts';
22
+ import type { ResolvedGlass } from '../../glass.ts';
23
+ import { vertexBufferLayouts } from './buffers.ts';
24
+ import type { PipelineCache } from './pipelineCache.ts';
25
+ import { shaderModule } from './shaderModules.ts';
26
+ import { UniformRing } from './uniformRing.ts';
27
+
28
+ const VISIBILITY_VERTEX = 0x1;
29
+ const VISIBILITY_FRAGMENT = 0x2;
30
+ const USAGE_UNIFORM_DST = 0x40 | 0x8; // UNIFORM | COPY_DST
31
+
32
+ const VERT = DEPTH_BINDINGS.GLASS_TINT_VERT;
33
+ const CUTOUT_VERT = DEPTH_BINDINGS.GLASS_TINT_CUTOUT_VERT;
34
+ const FRAG = DEPTH_BINDINGS.GLASS_TINT_FRAG;
35
+ const CUTOUT_MAP = DEPTH_BINDINGS.GLASS_TINT_CUTOUT_FRAG.textures.uCutoutMap;
36
+ const PALETTE = DEPTH_BINDINGS.GLASS_TINT_SKINNED_VERT.textures.uJointPalette;
37
+ const PANE = FRAG.fields.uGlassPane.offset;
38
+ const LIGHT = FRAG.fields.uGlassLight.offset;
39
+
40
+ /** Panes a frame can tint: a pane is a draw per shadow it reaches, and panes are few. */
41
+ const MAX_TINT_DRAWS = 512;
42
+
43
+ export type GlassTintKind = 'rigid' | 'instanced' | 'skinned' | 'cutout' | 'instancedCutout';
44
+
45
+ /** The multiplying state: colour times what passes, clarity times clarity, order-free. */
46
+ export const GLASS_TINT_BLEND: GPUBlendState = {
47
+ color: { operation: 'add', srcFactor: 'dst', dstFactor: 'zero' },
48
+ alpha: { operation: 'add', srcFactor: 'dst-alpha', dstFactor: 'zero' },
49
+ };
50
+
51
+ export class GpuGlassTint {
52
+ readonly frag: UniformRing;
53
+ private readonly layouts: Record<'plain' | 'cutout' | 'skinned', GPUBindGroupLayout>;
54
+ private plainGroup: GPUBindGroup | null = null;
55
+ private groupVertBuffer: GPUBuffer | null = null;
56
+ private groupFragBuffer: GPUBuffer | null = null;
57
+ private readonly cutoutGroups = new Map<GPUTextureView, GPUBindGroup>();
58
+ private readonly skinnedGroups = new Map<GPUTextureView, GPUBindGroup>();
59
+ private readonly device: GPUDevice;
60
+
61
+ constructor(device: GPUDevice) {
62
+ this.device = device;
63
+ this.frag = new UniformRing(
64
+ device,
65
+ FRAG.uniformSize,
66
+ MAX_TINT_DRAWS,
67
+ USAGE_UNIFORM_DST,
68
+ 'glassTint.ring',
69
+ );
70
+ const vert = (size: number): GPUBindGroupLayoutEntry => ({
71
+ binding: VERT.uniforms,
72
+ visibility: VISIBILITY_VERTEX,
73
+ buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: size },
74
+ });
75
+ const frag: GPUBindGroupLayoutEntry = {
76
+ binding: FRAG.uniforms,
77
+ visibility: VISIBILITY_FRAGMENT,
78
+ buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: FRAG.uniformSize },
79
+ };
80
+ this.layouts = {
81
+ plain: device.createBindGroupLayout({
82
+ label: 'glassTint.layout',
83
+ entries: [vert(VERT.uniformSize), frag],
84
+ }),
85
+ cutout: device.createBindGroupLayout({
86
+ label: 'glassTint.layout.cutout',
87
+ entries: [
88
+ vert(CUTOUT_VERT.uniformSize),
89
+ frag,
90
+ {
91
+ binding: CUTOUT_MAP.texture,
92
+ visibility: VISIBILITY_FRAGMENT,
93
+ texture: { sampleType: 'float' },
94
+ },
95
+ {
96
+ binding: CUTOUT_MAP.sampler,
97
+ visibility: VISIBILITY_FRAGMENT,
98
+ sampler: { type: 'filtering' },
99
+ },
100
+ ],
101
+ }),
102
+ skinned: device.createBindGroupLayout({
103
+ label: 'glassTint.layout.skinned',
104
+ entries: [
105
+ {
106
+ binding: PALETTE.texture,
107
+ visibility: VISIBILITY_VERTEX,
108
+ texture: { sampleType: 'unfilterable-float' },
109
+ },
110
+ vert(VERT.uniformSize),
111
+ frag,
112
+ ],
113
+ }),
114
+ };
115
+ }
116
+
117
+ /** One pane's block: what it lets through, how clear it is, and the light it is drawn for. */
118
+ writePane(slot: number, glass: ResolvedGlass, light: Float32Array): void {
119
+ const pane = paneScratch;
120
+ pane[0] = glass.transmission * glass.tint[0];
121
+ pane[1] = glass.transmission * glass.tint[1];
122
+ pane[2] = glass.transmission * glass.tint[2];
123
+ pane[3] = 1 - glass.frost;
124
+ this.frag.writeFloats(slot, PANE, pane);
125
+ this.frag.writeFloats(slot, LIGHT, light);
126
+ }
127
+
128
+ /** The plain group, rebuilt only when a ring has grown a new buffer under it. */
129
+ group(vertBuffer: GPUBuffer): GPUBindGroup {
130
+ if (
131
+ this.plainGroup === null ||
132
+ this.groupVertBuffer !== vertBuffer ||
133
+ this.groupFragBuffer !== this.frag.buffer
134
+ ) {
135
+ this.cutoutGroups.clear();
136
+ this.skinnedGroups.clear();
137
+ this.groupVertBuffer = vertBuffer;
138
+ this.groupFragBuffer = this.frag.buffer;
139
+ this.plainGroup = this.device.createBindGroup({
140
+ label: 'glassTint.group',
141
+ layout: this.layouts.plain,
142
+ entries: [
143
+ { binding: VERT.uniforms, resource: { buffer: vertBuffer, size: VERT.uniformSize } },
144
+ {
145
+ binding: FRAG.uniforms,
146
+ resource: { buffer: this.frag.buffer, size: FRAG.uniformSize },
147
+ },
148
+ ],
149
+ });
150
+ }
151
+ return this.plainGroup;
152
+ }
153
+
154
+ /** A cutout pane's group, one per map, built the first time that map casts colour. */
155
+ cutoutGroup(vertBuffer: GPUBuffer, view: GPUTextureView, sampler: GPUSampler): GPUBindGroup {
156
+ this.group(vertBuffer);
157
+ const held = this.cutoutGroups.get(view);
158
+ if (held !== undefined) return held;
159
+ const made = this.device.createBindGroup({
160
+ label: 'glassTint.group.cutout',
161
+ layout: this.layouts.cutout,
162
+ entries: [
163
+ {
164
+ binding: CUTOUT_VERT.uniforms,
165
+ resource: { buffer: vertBuffer, size: CUTOUT_VERT.uniformSize },
166
+ },
167
+ { binding: FRAG.uniforms, resource: { buffer: this.frag.buffer, size: FRAG.uniformSize } },
168
+ { binding: CUTOUT_MAP.texture, resource: view },
169
+ { binding: CUTOUT_MAP.sampler, resource: sampler },
170
+ ],
171
+ });
172
+ this.cutoutGroups.set(view, made);
173
+ return made;
174
+ }
175
+
176
+ /** A skinned pane's group, one per palette view. */
177
+ skinnedGroup(vertBuffer: GPUBuffer, palette: GPUTextureView): GPUBindGroup {
178
+ this.group(vertBuffer);
179
+ const held = this.skinnedGroups.get(palette);
180
+ if (held !== undefined) return held;
181
+ const made = this.device.createBindGroup({
182
+ label: 'glassTint.group.skinned',
183
+ layout: this.layouts.skinned,
184
+ entries: [
185
+ { binding: PALETTE.texture, resource: palette },
186
+ { binding: VERT.uniforms, resource: { buffer: vertBuffer, size: VERT.uniformSize } },
187
+ { binding: FRAG.uniforms, resource: { buffer: this.frag.buffer, size: FRAG.uniformSize } },
188
+ ],
189
+ });
190
+ this.skinnedGroups.set(palette, made);
191
+ return made;
192
+ }
193
+
194
+ /** The pipeline for one kind of caster, cull mode and vertex layout, cached by key. */
195
+ pipeline(
196
+ cache: PipelineCache,
197
+ kind: GlassTintKind,
198
+ key: string,
199
+ present: Readonly<Record<string, boolean>>,
200
+ cullMode: GPUCullMode,
201
+ ): GPURenderPipeline {
202
+ const cutout = kind === 'cutout' || kind === 'instancedCutout';
203
+ const instanced = kind === 'instanced' || kind === 'instancedCutout';
204
+ const layout =
205
+ kind === 'skinned' ? this.layouts.skinned : cutout ? this.layouts.cutout : this.layouts.plain;
206
+ const vertex =
207
+ kind === 'rigid'
208
+ ? GLASS_TINT_VERT_WGSL
209
+ : kind === 'instanced'
210
+ ? GLASS_TINT_INSTANCED_VERT_WGSL
211
+ : kind === 'skinned'
212
+ ? GLASS_TINT_SKINNED_VERT_WGSL
213
+ : kind === 'cutout'
214
+ ? GLASS_TINT_CUTOUT_VERT_WGSL
215
+ : GLASS_TINT_INSTANCED_CUTOUT_VERT_WGSL;
216
+ const label = `glass-tint|${kind}|${cullMode}|${key}`;
217
+ return cache.get(label, () => ({
218
+ label,
219
+ layout: this.device.createPipelineLayout({ bindGroupLayouts: [layout] }),
220
+ vertex: {
221
+ module: shaderModule(this.device, { label: `glassTint.${kind}.vert`, code: vertex }),
222
+ entryPoint: 'main',
223
+ buffers: vertexBufferLayouts(present, instanced),
224
+ },
225
+ fragment: {
226
+ module: shaderModule(this.device, {
227
+ label: cutout ? 'glassTint.cutout.frag' : 'glassTint.frag',
228
+ code: cutout ? GLASS_TINT_CUTOUT_FRAG_WGSL : GLASS_TINT_FRAG_WGSL,
229
+ }),
230
+ entryPoint: 'main',
231
+ targets: [{ format: 'rgba8unorm', blend: GLASS_TINT_BLEND }],
232
+ },
233
+ /* The depth pass's primitive state exactly — its winding included, for the reason
234
+ `depthPipeline` gives: this map is sampled, not presented. */
235
+ primitive: { topology: 'triangle-list', cullMode, frontFace: 'cw' },
236
+ }));
237
+ }
238
+
239
+ dispose(): void {
240
+ this.frag.dispose();
241
+ }
242
+ }
243
+
244
+ const paneScratch = new Float32Array(4);