@driftengine/core 4.5.0 → 4.6.1

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 (209) hide show
  1. package/CHANGELOG.json +62 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +23 -2
  23. package/dist/index.js +13 -1
  24. package/dist/render/backend/api.d.ts +17 -3
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  30. package/dist/render/backend/webgl2/instanced.js +72 -15
  31. package/dist/render/backend/webgl2/renderer.d.ts +122 -8
  32. package/dist/render/backend/webgl2/renderer.js +249 -128
  33. package/dist/render/backend/webgpu/buffers.d.ts +17 -0
  34. package/dist/render/backend/webgpu/buffers.js +34 -3
  35. package/dist/render/backend/webgpu/depthPass.js +5 -1
  36. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  37. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  38. package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
  39. package/dist/render/backend/webgpu/flatPass.js +38 -9
  40. package/dist/render/backend/webgpu/glassTintPass.js +2 -1
  41. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  42. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  43. package/dist/render/backend/webgpu/instanced.d.ts +11 -1
  44. package/dist/render/backend/webgpu/instanced.js +21 -3
  45. package/dist/render/backend/webgpu/latePass.d.ts +6 -2
  46. package/dist/render/backend/webgpu/latePass.js +11 -4
  47. package/dist/render/backend/webgpu/motionPass.js +2 -1
  48. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  49. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  50. package/dist/render/backend/webgpu/renderer.js +478 -214
  51. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  53. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  54. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  55. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  56. package/dist/render/driftLight/bake.d.ts +7 -0
  57. package/dist/render/driftLight/bake.js +61 -56
  58. package/dist/render/driftLight/denseField.d.ts +46 -0
  59. package/dist/render/driftLight/denseField.js +109 -0
  60. package/dist/render/driftLight/lightField.d.ts +10 -5
  61. package/dist/render/driftLight/lightField.js +25 -19
  62. package/dist/render/driftLight/presence.d.ts +50 -0
  63. package/dist/render/driftLight/presence.js +43 -0
  64. package/dist/render/driftLight/uniforms.d.ts +3 -3
  65. package/dist/render/driftLight/uniforms.js +5 -6
  66. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  67. package/dist/render/driftLight/worldLightField.js +66 -0
  68. package/dist/render/fixtureAtlas.d.ts +20 -0
  69. package/dist/render/fixtureAtlas.js +49 -0
  70. package/dist/render/instanceCull.d.ts +63 -0
  71. package/dist/render/instanceCull.js +160 -0
  72. package/dist/render/instances.d.ts +15 -0
  73. package/dist/render/lightBudget.d.ts +22 -33
  74. package/dist/render/lightBudget.js +24 -35
  75. package/dist/render/lightGrid.d.ts +42 -0
  76. package/dist/render/lightGrid.js +87 -0
  77. package/dist/render/mesh.d.ts +23 -24
  78. package/dist/render/mesh.js +56 -68
  79. package/dist/render/occlusion.d.ts +10 -0
  80. package/dist/render/occlusion.js +36 -7
  81. package/dist/render/pointLightSelection.d.ts +7 -0
  82. package/dist/render/pointLightSelection.js +29 -4
  83. package/dist/render/rainField.d.ts +13 -0
  84. package/dist/render/rainField.js +27 -5
  85. package/dist/render/recon/lateDepth.d.ts +11 -0
  86. package/dist/render/recon/lateDepth.js +70 -0
  87. package/dist/render/recon/resolve.d.ts +34 -4
  88. package/dist/render/recon/resolve.js +161 -20
  89. package/dist/render/rendererHarness.js +4 -0
  90. package/dist/render/sceneTarget.d.ts +2 -0
  91. package/dist/render/sceneTarget.js +2 -0
  92. package/dist/render/shaders/depth.js +12 -9
  93. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  94. package/dist/render/shaders/flat/driftLight.js +32 -7
  95. package/dist/render/shaders/flat/index.js +5 -3
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +75 -23
  98. package/dist/render/shaders/flat/preamble.js +75 -20
  99. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  100. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  101. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  102. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  103. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  104. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  106. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  107. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  108. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  109. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  110. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  114. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  115. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  116. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  117. package/dist/render/shaders/rush.js +11 -1
  118. package/dist/render/shaders/sdfText.d.ts +1 -1
  119. package/dist/render/shaders/sdfText.js +3 -2
  120. package/dist/render/shaders/text.d.ts +1 -1
  121. package/dist/render/shaders/text.js +18 -9
  122. package/dist/render/surfaceEffects.d.ts +83 -0
  123. package/dist/render/surfaceEffects.js +75 -0
  124. package/dist/render/surfaceScene.d.ts +8 -0
  125. package/dist/render/surfaceScene.js +8 -0
  126. package/dist/render/surfaceTexture.d.ts +18 -2
  127. package/dist/render/surfaceTexture.js +68 -15
  128. package/dist/render/temporalAa.d.ts +25 -6
  129. package/dist/render/temporalAa.js +45 -15
  130. package/dist/render/textureSource.d.ts +30 -0
  131. package/dist/render/textureSource.js +53 -0
  132. package/dist/render/vertexDefaults.js +2 -0
  133. package/dist/world/hlod.d.ts +104 -0
  134. package/dist/world/hlod.js +214 -0
  135. package/package.json +6 -6
  136. package/src/geometry/boxCsg.ts +186 -0
  137. package/src/geometry/csg.ts +384 -0
  138. package/src/geometry/meshBuilder.ts +15 -0
  139. package/src/geometry/meshConcat.ts +1 -0
  140. package/src/geometry/pixelFont.ts +2 -0
  141. package/src/geometry/solid.ts +189 -0
  142. package/src/geometry/solidFlat.ts +522 -0
  143. package/src/geometry/solidHarness.ts +83 -0
  144. package/src/geometry/solidNormals.ts +103 -0
  145. package/src/geometry/solidRound.ts +443 -0
  146. package/src/geometry/solidWriter.ts +58 -0
  147. package/src/index.ts +51 -1
  148. package/src/render/backend/api.ts +21 -2
  149. package/src/render/backend/drawFog.ts +31 -0
  150. package/src/render/backend/materialChanges.ts +9 -2
  151. package/src/render/backend/webgl2/instanced.ts +80 -14
  152. package/src/render/backend/webgl2/renderer.ts +311 -150
  153. package/src/render/backend/webgpu/buffers.ts +49 -3
  154. package/src/render/backend/webgpu/depthPass.ts +5 -1
  155. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  156. package/src/render/backend/webgpu/flatPass.ts +41 -11
  157. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  158. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  159. package/src/render/backend/webgpu/instanced.ts +21 -3
  160. package/src/render/backend/webgpu/latePass.ts +12 -3
  161. package/src/render/backend/webgpu/motionPass.ts +2 -1
  162. package/src/render/backend/webgpu/renderer.ts +556 -239
  163. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  164. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  165. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  166. package/src/render/driftLight/bake.ts +85 -62
  167. package/src/render/driftLight/denseField.ts +149 -0
  168. package/src/render/driftLight/lightField.ts +33 -18
  169. package/src/render/driftLight/presence.ts +69 -0
  170. package/src/render/driftLight/uniforms.ts +8 -9
  171. package/src/render/driftLight/worldLightField.ts +83 -0
  172. package/src/render/fixtureAtlas.ts +63 -0
  173. package/src/render/instanceCull.ts +205 -0
  174. package/src/render/instances.ts +16 -0
  175. package/src/render/lightBudget.ts +25 -36
  176. package/src/render/lightGrid.ts +130 -0
  177. package/src/render/mesh.ts +64 -72
  178. package/src/render/occlusion.ts +51 -7
  179. package/src/render/pointLightSelection.ts +72 -4
  180. package/src/render/rainField.ts +39 -5
  181. package/src/render/recon/lateDepth.ts +83 -0
  182. package/src/render/recon/resolve.ts +206 -20
  183. package/src/render/rendererHarness.ts +4 -0
  184. package/src/render/sceneTarget.ts +4 -0
  185. package/src/render/shaders/depth.ts +12 -9
  186. package/src/render/shaders/flat/driftLight.ts +32 -7
  187. package/src/render/shaders/flat/index.ts +5 -3
  188. package/src/render/shaders/flat/main.ts +75 -23
  189. package/src/render/shaders/flat/preamble.ts +75 -20
  190. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  191. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  192. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  193. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  194. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  195. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  196. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  197. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  198. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  199. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  200. package/src/render/shaders/rush.ts +11 -1
  201. package/src/render/shaders/sdfText.ts +3 -2
  202. package/src/render/shaders/text.ts +18 -9
  203. package/src/render/surfaceEffects.ts +132 -0
  204. package/src/render/surfaceScene.ts +16 -0
  205. package/src/render/surfaceTexture.ts +110 -19
  206. package/src/render/temporalAa.ts +78 -14
  207. package/src/render/textureSource.ts +72 -0
  208. package/src/render/vertexDefaults.ts +2 -0
  209. package/src/world/hlod.ts +290 -0
@@ -103,6 +103,28 @@ export const VERTEX_LAYOUT: readonly VertexAttribute[] = [
103
103
  ];
104
104
 
105
105
  /** Geometry on the device, with the buffers bound in layout order. */
106
+ /**
107
+ * The texture coordinates of a mesh that names texture-array layers: (u, v, layer), three floats.
108
+ *
109
+ * **Three only where layers exist.** Every location is spent, so the layer rides in this attribute,
110
+ * and widening it for every textured mesh would add four bytes to each of its vertices for nothing.
111
+ * A mesh without layers keeps two, and the shader's `vec3` input reads z as 0 — WebGPU fills a
112
+ * component the format does not carry with 0 (and w with 1) — which is layer 0. The pipeline key
113
+ * carries `:layers`, so the two vertex layouts never share a pipeline.
114
+ */
115
+ export const LAYERED_UVS: VertexAttribute = {
116
+ name: 'uvs',
117
+ shaderLocation: 5,
118
+ format: 'float32x3',
119
+ components: 3,
120
+ optional: true,
121
+ };
122
+
123
+ /** The attribute as this mesh lays it out: the UV field widens where the mesh names layers. */
124
+ function laidOut(attribute: VertexAttribute, layered: boolean): VertexAttribute {
125
+ return layered && attribute.name === 'uvs' ? LAYERED_UVS : attribute;
126
+ }
127
+
106
128
  export interface GpuMesh {
107
129
  /**
108
130
  * The buffers a draw binds, in the order `vertexBufferLayouts` describes them.
@@ -132,6 +154,13 @@ export interface GpuMesh {
132
154
  */
133
155
  readonly skinOffsets: { readonly joints: number; readonly weights: number } | null;
134
156
  readonly indexBuffer: GPUBuffer;
157
+ /**
158
+ * How many indices the mesh draws. **Zero is a mesh with nothing to draw, and no pass draws it**:
159
+ * a level can hold none of some kind of geometry, and a draw of zero indices does nothing but
160
+ * earn a warning from the device in every pass that issues it. Every place a mesh enters a pass
161
+ * checks this beside `complete` or its vertex buffers, and WebGL2 skips the same meshes, so the
162
+ * two backends count the same draws.
163
+ */
135
164
  readonly indexCount: number;
136
165
  /**
137
166
  * How big this mesh is, in its own space.
@@ -281,9 +310,10 @@ export function createGpuMeshIncremental(
281
310
  validateMeshData(data);
282
311
  const bounds = boundsOfPositions(data.positions, createBounds());
283
312
  const vertexCount = data.positions.length / 3;
313
+ const layered = data.layers !== undefined;
284
314
  const supplied = VERTEX_LAYOUT.filter(
285
315
  (attribute) => !attribute.optional || data[attribute.name as keyof MeshData] !== undefined,
286
- );
316
+ ).map((attribute) => laidOut(attribute, layered));
287
317
 
288
318
  /* Interleaved: one stride holding every attribute the mesh actually supplied. */
289
319
  const stride = supplied.reduce((sum, attribute) => sum + attribute.components * 4, 0);
@@ -459,7 +489,21 @@ function* uploadSteps(job: UploadJob): Generator<void, void, void> {
459
489
  let fieldOffset = 0;
460
490
  for (const attribute of supplied) {
461
491
  const source = data[attribute.name as keyof MeshData] as Float32Array;
462
- interleaveField(interleaved, source, attribute.components, step, fieldOffset, from, to);
492
+ if (attribute === LAYERED_UVS) {
493
+ /* (u, v) from the coordinates and the layer after them, one field of three. */
494
+ interleaveField(interleaved, source, 2, step, fieldOffset, from, to);
495
+ interleaveField(
496
+ interleaved,
497
+ data.layers as Float32Array,
498
+ 1,
499
+ step,
500
+ fieldOffset + 2,
501
+ from,
502
+ to,
503
+ );
504
+ } else {
505
+ interleaveField(interleaved, source, attribute.components, step, fieldOffset, from, to);
506
+ }
463
507
  fieldOffset += attribute.components;
464
508
  }
465
509
  device.queue.writeBuffer(vertices, from * stride, interleaved, from * step, (to - from) * step);
@@ -678,7 +722,9 @@ export function vertexBufferLayouts(
678
722
  const constants: GPUVertexAttribute[] = [];
679
723
  let offset = 0;
680
724
 
681
- VERTEX_LAYOUT.forEach((attribute, index) => {
725
+ const layered = present['layers'] === true;
726
+ VERTEX_LAYOUT.forEach((declared, index) => {
727
+ const attribute = laidOut(declared, layered);
682
728
  /*
683
729
  * **The instanced pipeline reclaims the two skinning locations, and it is allowed to because
684
730
  * it cannot skin.** Locations 11 and 12 are the joint indices and weights; the INSTANCED
@@ -137,7 +137,11 @@ export function createDepthBindGroupLayout(
137
137
  {
138
138
  binding: CUTOUT_MAP.texture,
139
139
  visibility: VISIBILITY_FRAGMENT,
140
- texture: { sampleType: 'float' as GPUTextureSampleType },
140
+ /* A surface texture, and every surface texture is a `2d-array` view. */
141
+ texture: {
142
+ sampleType: 'float' as GPUTextureSampleType,
143
+ viewDimension: '2d-array' as GPUTextureViewDimension,
144
+ },
141
145
  },
142
146
  {
143
147
  binding: CUTOUT_MAP.sampler,
@@ -28,6 +28,11 @@ export interface DrawCommand {
28
28
  indexed: boolean;
29
29
  count: number;
30
30
  instances: number;
31
+ /**
32
+ * A `drawIndexedIndirect` record to draw from instead of `count` and `instances`: an instance
33
+ * cull's survivors, counted on the device. Null for every other draw.
34
+ */
35
+ indirect: GPUBuffer | null;
31
36
  }
32
37
 
33
38
  export interface CommandPool {
@@ -50,6 +55,7 @@ function emptyCommand(): DrawCommand {
50
55
  indexed: false,
51
56
  count: 0,
52
57
  instances: 1,
58
+ indirect: null,
53
59
  };
54
60
  }
55
61
 
@@ -8,6 +8,14 @@ import { vertexBufferLayouts } from './buffers.ts';
8
8
  import type { PipelineCache } from './pipelineCache.ts';
9
9
  import { shaderModule } from './shaderModules.ts';
10
10
 
11
+ /** The material maps the flat pass reads: `2d-array` colour, filtered, not shadow maps. */
12
+ const SURFACE_MAPS: ReadonlySet<string> = new Set([
13
+ 'uAlbedo',
14
+ 'uNormalMap',
15
+ 'uOrmMap',
16
+ 'uEmissiveMap',
17
+ ]);
18
+
11
19
  /**
12
20
  * The flat pipeline: the shader that draws the world.
13
21
  *
@@ -290,9 +298,20 @@ export function createFlatBindGroupLayout(
290
298
  const volume = texture.type === 'sampler3D' || texture.type === 'usampler3D';
291
299
  /* Glass tints are colour, mipmapped and filtered: a frosted pane is read at a coarser level. */
292
300
  const tint = name.endsWith('GlassTints');
301
+ /*
302
+ * **The four material maps are arrays too, and they are colour.** Every surface texture is a
303
+ * `2d-array` view since texture arrays landed, so the rule below — an array is a shadow map —
304
+ * would declare albedo unfilterable and bind a non-filtering sampler to it, which reads as
305
+ * every textured surface going nearest-neighbour and losing its mip chain. Named, as
306
+ * `uEnvironment` is, because a pattern wide enough to catch them catches the shadow arrays.
307
+ */
308
+ const surface = SURFACE_MAPS.has(name);
293
309
  const isShadow =
294
310
  integer ||
295
- (!tint && name !== 'uEnvironment' && (name.endsWith('ShadowMap') || cube || array));
311
+ (!tint &&
312
+ !surface &&
313
+ name !== 'uEnvironment' &&
314
+ (name.endsWith('ShadowMap') || cube || array));
296
315
  entries.push({
297
316
  binding: texture.texture,
298
317
  visibility: VISIBILITY_FRAGMENT,
@@ -462,8 +481,11 @@ export function flatPipeline(
462
481
  * `depthMask` alone and only turns blending on. A translucent part of a loaded model is a
463
482
  * pane of glass in a solid object, not a particle, and it has to occlude what is behind it
464
483
  * for the rest of the model to sort correctly.
484
+ *
485
+ * `'additive'` adds instead: colour times alpha onto the target, its alpha kept. The key must
486
+ * carry it (`|add`).
465
487
  */
466
- translucent = false,
488
+ translucent: boolean | 'additive' = false,
467
489
  /** Whether this pipeline reads a joint palette. See `FlatVertexOptions`. */
468
490
  skinned = false,
469
491
  /** Whether this pipeline reads morph deltas. See `FlatVertexOptions`. */
@@ -576,7 +598,7 @@ function flatDescriptor(
576
598
  variant: FlatVariant,
577
599
  key: string,
578
600
  present: Readonly<Record<string, boolean>>,
579
- translucent: boolean,
601
+ translucent: boolean | 'additive',
580
602
  /** Whether this pipeline reads a joint palette. See `FlatVertexOptions`. */
581
603
  skinned: boolean,
582
604
  /** Whether this pipeline reads morph deltas. See `FlatVertexOptions`. */
@@ -626,19 +648,27 @@ function flatDescriptor(
626
648
  constants: flatFragmentConstants(variant, cache),
627
649
  targets: [
628
650
  oitTarget(oit) ??
629
- (translucent
651
+ (translucent === 'additive'
630
652
  ? {
631
653
  format: cache.format,
632
654
  blend: {
633
- color: {
634
- srcFactor: 'src-alpha',
635
- dstFactor: 'one-minus-src-alpha',
636
- operation: 'add',
637
- },
638
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
655
+ color: { srcFactor: 'src-alpha', dstFactor: 'one', operation: 'add' },
656
+ alpha: { srcFactor: 'zero', dstFactor: 'one', operation: 'add' },
639
657
  },
640
658
  }
641
- : { format: cache.format }),
659
+ : translucent
660
+ ? {
661
+ format: cache.format,
662
+ blend: {
663
+ color: {
664
+ srcFactor: 'src-alpha',
665
+ dstFactor: 'one-minus-src-alpha',
666
+ operation: 'add',
667
+ },
668
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
669
+ },
670
+ }
671
+ : { format: cache.format }),
642
672
  ],
643
673
  },
644
674
  primitive: { topology: 'triangle-list', cullMode: doubleSided ? 'none' : 'back' },
@@ -90,7 +90,8 @@ export class GpuGlassTint {
90
90
  {
91
91
  binding: CUTOUT_MAP.texture,
92
92
  visibility: VISIBILITY_FRAGMENT,
93
- texture: { sampleType: 'float' },
93
+ /* A surface texture, and every surface texture is a `2d-array` view. */
94
+ texture: { sampleType: 'float', viewDimension: '2d-array' },
94
95
  },
95
96
  {
96
97
  binding: CUTOUT_MAP.sampler,
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Instance culls on the device: a batch's instances tested against a view and compacted into an
3
+ * indirect draw, many batches in one compute submit.
4
+ *
5
+ * **Queued during the frame, dispatched in one submit from `flushRings`.** Every submit that can carry
6
+ * a culled draw — the frame, a mirror, a probe face — calls `flushRings` first, so the dispatch that
7
+ * fills a slot is always on the queue ahead of the pass that draws from it. The parameters and the
8
+ * zeroed instance count are written with `queue.writeBuffer` when the job is queued, which the queue
9
+ * orders ahead of both submits.
10
+ *
11
+ * **A slot is one (batch, view) pair in one frame.** A batch drawn from the camera, a mirror and a
12
+ * probe face needs three sets of survivors, so a batch holds up to `MAX_CULL_SLOTS`, taken in order
13
+ * each frame; past that the renderer draws every instance, which is right and merely slower. Each
14
+ * slot owns its buffers and a bind group built once, so queueing allocates nothing.
15
+ */
16
+ import { INSTANCE_STRIDE } from '../../instances.ts';
17
+ import { INSTANCE_CULL_WGSL, INSTANCE_CULL_WORKGROUP } from './shaders/instanceCull.wgsl.ts';
18
+ import { shaderModule } from './shaderModules.ts';
19
+
20
+ /** Views a batch can be culled for in one frame. See the header. */
21
+ export const MAX_CULL_SLOTS = 4;
22
+
23
+ /** Jobs one frame may queue before the rest draw uncut. A ring size, not an opinion. */
24
+ const MAX_JOBS = 4096;
25
+
26
+ /** Bytes of `Params`: six planes, the mesh's sphere, and the instance count padded to 16. */
27
+ const PARAMS_BYTES = 6 * 16 + 16 + 16;
28
+
29
+ const USAGE = {
30
+ VERTEX: 0x0020,
31
+ UNIFORM: 0x0040,
32
+ STORAGE: 0x0080,
33
+ INDIRECT: 0x0100,
34
+ COPY_DST: 0x0008,
35
+ } as const;
36
+
37
+ /** One set of survivors: the compacted instances, the indirect record, and what the cull reads. */
38
+ export interface CullSlot {
39
+ readonly instances: GPUBuffer;
40
+ readonly args: GPUBuffer;
41
+ readonly params: GPUBuffer;
42
+ readonly group: GPUBindGroup;
43
+ /** How many instances the last queued job tested, which sizes its dispatch. */
44
+ count: number;
45
+ }
46
+
47
+ export class InstanceCullPass {
48
+ private readonly pipeline: GPUComputePipeline;
49
+ private readonly layout: GPUBindGroupLayout;
50
+ private readonly jobs: (CullSlot | null)[] = new Array<CullSlot | null>(MAX_JOBS).fill(null);
51
+ private queued = 0;
52
+ private readonly paramsStaging = new ArrayBuffer(PARAMS_BYTES);
53
+ private readonly paramFloats = new Float32Array(this.paramsStaging);
54
+ private readonly paramWords = new Uint32Array(this.paramsStaging);
55
+ private readonly argsStaging = new Uint32Array(5);
56
+
57
+ constructor(private readonly device: GPUDevice) {
58
+ this.layout = device.createBindGroupLayout({
59
+ label: 'instances.cull',
60
+ entries: [
61
+ { binding: 0, visibility: 0x4, buffer: { type: 'uniform' } },
62
+ { binding: 1, visibility: 0x4, buffer: { type: 'read-only-storage' } },
63
+ { binding: 2, visibility: 0x4, buffer: { type: 'storage' } },
64
+ { binding: 3, visibility: 0x4, buffer: { type: 'storage' } },
65
+ ],
66
+ });
67
+ this.pipeline = device.createComputePipeline({
68
+ label: 'instances.cull',
69
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.layout] }),
70
+ compute: {
71
+ module: shaderModule(device, { label: 'instances.cull', code: INSTANCE_CULL_WGSL }),
72
+ entryPoint: 'main',
73
+ },
74
+ });
75
+ }
76
+
77
+ /** A slot reading `source`, a batch's full instance buffer of `capacity` instances. */
78
+ createSlot(source: GPUBuffer, capacity: number, label: string): CullSlot {
79
+ const size = Math.max(1, capacity) * INSTANCE_STRIDE;
80
+ const instances = this.device.createBuffer({
81
+ label: `${label}.kept`,
82
+ size,
83
+ usage: USAGE.VERTEX | USAGE.STORAGE,
84
+ });
85
+ const args = this.device.createBuffer({
86
+ label: `${label}.args`,
87
+ size: 20,
88
+ usage: USAGE.INDIRECT | USAGE.STORAGE | USAGE.COPY_DST,
89
+ });
90
+ const params = this.device.createBuffer({
91
+ label: `${label}.params`,
92
+ size: PARAMS_BYTES,
93
+ usage: USAGE.UNIFORM | USAGE.COPY_DST,
94
+ });
95
+ const group = this.device.createBindGroup({
96
+ label: `${label}.cull`,
97
+ layout: this.layout,
98
+ entries: [
99
+ { binding: 0, resource: { buffer: params } },
100
+ { binding: 1, resource: { buffer: source, size } },
101
+ { binding: 2, resource: { buffer: instances } },
102
+ { binding: 3, resource: { buffer: args } },
103
+ ],
104
+ });
105
+ return { instances, args, params, group, count: 0 };
106
+ }
107
+
108
+ /**
109
+ * Queue a cull of `count` instances against six `planes`, the mesh's local sphere being
110
+ * (centre, radius). Returns false when the frame's job ring is full, and the caller draws uncut.
111
+ */
112
+ queue(
113
+ slot: CullSlot,
114
+ planes: Float32Array,
115
+ centre: ArrayLike<number>,
116
+ radius: number,
117
+ count: number,
118
+ indexCount: number,
119
+ ): boolean {
120
+ if (this.queued >= MAX_JOBS) return false;
121
+ const floats = this.paramFloats;
122
+ for (let i = 0; i < 24; i++) floats[i] = planes[i] as number;
123
+ floats[24] = centre[0] ?? 0;
124
+ floats[25] = centre[1] ?? 0;
125
+ floats[26] = centre[2] ?? 0;
126
+ floats[27] = radius;
127
+ this.paramWords[28] = count;
128
+ this.device.queue.writeBuffer(slot.params, 0, this.paramsStaging);
129
+ const args = this.argsStaging;
130
+ args[0] = indexCount;
131
+ args[1] = 0;
132
+ args[2] = 0;
133
+ args[3] = 0;
134
+ args[4] = 0;
135
+ this.device.queue.writeBuffer(slot.args, 0, args);
136
+ slot.count = count;
137
+ this.jobs[this.queued++] = slot;
138
+ return true;
139
+ }
140
+
141
+ /** Dispatch everything queued, in one compute pass and one submit. Nothing queued is nothing done. */
142
+ flush(): void {
143
+ if (this.queued === 0) return;
144
+ const encoder = this.device.createCommandEncoder({ label: 'instances.cull' });
145
+ const pass = encoder.beginComputePass({ label: 'instances.cull' });
146
+ pass.setPipeline(this.pipeline);
147
+ for (let i = 0; i < this.queued; i++) {
148
+ const slot = this.jobs[i];
149
+ if (slot === null || slot === undefined) continue;
150
+ pass.setBindGroup(0, slot.group);
151
+ pass.dispatchWorkgroups(Math.ceil(slot.count / INSTANCE_CULL_WORKGROUP));
152
+ this.jobs[i] = null;
153
+ }
154
+ pass.end();
155
+ this.device.queue.submit([encoder.finish()]);
156
+ this.queued = 0;
157
+ }
158
+ }
159
+
160
+ /** Every buffer a slot owns, destroyed with its batch. */
161
+ export function destroySlot(slot: CullSlot): void {
162
+ slot.instances.destroy();
163
+ slot.args.destroy();
164
+ slot.params.destroy();
165
+ }
@@ -1,9 +1,14 @@
1
+ import { instancesBox } from '../../instanceCull.ts';
1
2
  import { INSTANCE_FLOATS, INSTANCE_STRIDE, packInstances } from '../../instances.ts';
3
+ import { destroySlot } from './instanceCullPass.ts';
4
+ import type { CullSlot } from './instanceCullPass.ts';
2
5
  import type { MeshInstances } from '../../instances.ts';
3
6
  import type { GpuMesh } from './buffers.ts';
4
7
 
5
8
  /** VERTEX | COPY_DST, spelled the way every other pass here spells it. */
6
9
  const USAGE_VERTEX = 0x0020 | 0x0008;
10
+ /** A culling batch's source is read by the cull as storage too. */
11
+ const USAGE_STORAGE = 0x0080;
7
12
 
8
13
  /**
9
14
  * One mesh's per-instance buffer on the device.
@@ -21,9 +26,20 @@ export class GpuInstancedBatch {
21
26
  readonly capacity: number;
22
27
  private readonly staging: Float32Array;
23
28
 
24
- constructor(device: GPUDevice, mesh: GpuMesh, capacity: number, label: string) {
29
+ /** Whether the camera draw culls this batch. See `InstancedOptions.cull`. */
30
+ readonly cull: boolean;
31
+ /** The world box around every live instance, fitted at upload. See `instancesBox`. */
32
+ readonly box = new Float32Array([1, 1, 1, -1, -1, -1]);
33
+ /** One set of survivors per view drawn this frame. See `InstanceCullPass`. */
34
+ readonly slots: CullSlot[] = [];
35
+ /** The frame the slots were last taken in, and how many were taken. */
36
+ slotFrame = -1;
37
+ slotsTaken = 0;
38
+
39
+ constructor(device: GPUDevice, mesh: GpuMesh, capacity: number, label: string, cull = false) {
25
40
  this.mesh = mesh;
26
41
  this.capacity = capacity;
42
+ this.cull = cull;
27
43
  this.staging = new Float32Array(capacity * INSTANCE_FLOATS);
28
44
  this.buffer = device.createBuffer({
29
45
  /*
@@ -31,8 +47,8 @@ export class GpuInstancedBatch {
31
47
  * naming the resource it was about, and an unlabelled buffer names nothing.
32
48
  */
33
49
  label,
34
- size: capacity * INSTANCE_STRIDE,
35
- usage: USAGE_VERTEX,
50
+ size: Math.max(1, capacity) * INSTANCE_STRIDE,
51
+ usage: USAGE_VERTEX | (cull ? USAGE_STORAGE : 0),
36
52
  });
37
53
  }
38
54
 
@@ -55,6 +71,7 @@ export class GpuInstancedBatch {
55
71
  * `writeBuffer` copies at the call, so packing afterwards cannot reach that copy.
56
72
  */
57
73
  upload(queue: GPUQueue, data: MeshInstances, previous: GPUBuffer | null = null): void {
74
+ if (this.cull) instancesBox(data, this.mesh.bounds, this.box);
58
75
  const count = Math.min(data.count, this.capacity);
59
76
  if (previous !== null) {
60
77
  if (this.liveCount > 0) {
@@ -71,5 +88,6 @@ export class GpuInstancedBatch {
71
88
  dispose(): void {
72
89
  this.previousBuffer?.destroy();
73
90
  this.buffer.destroy();
91
+ for (const slot of this.slots) destroySlot(slot);
74
92
  }
75
93
  }
@@ -25,7 +25,8 @@ export class LatePass {
25
25
  private readonly layout: GPUBindGroupLayout;
26
26
  private readonly pipeline: GPURenderPipeline;
27
27
  private readonly sizes: GPUBuffer;
28
- private readonly sizeStaging = new Float32Array(4);
28
+ /** The render size and the output size, then the frame's jitter in render texels. */
29
+ private readonly sizeStaging = new Float32Array(8);
29
30
  private group: GPUBindGroup | null = null;
30
31
  private groupSource: GPUTextureView | null = null;
31
32
 
@@ -49,7 +50,7 @@ export class LatePass {
49
50
  });
50
51
  this.sizes = device.createBuffer({
51
52
  label: 'recon.lateDepth.sizes',
52
- size: 16,
53
+ size: 32,
53
54
  usage: 0x40 | 0x8, // UNIFORM | COPY_DST
54
55
  });
55
56
  }
@@ -69,7 +70,10 @@ export class LatePass {
69
70
  return this.depthTextureView as GPUTextureView;
70
71
  }
71
72
 
72
- /** Fill the output-size depth from `source`, the render's single-sample depth. */
73
+ /**
74
+ * Fill the output-size depth from `source`, the render's single-sample depth, drawn jittered by
75
+ * (`jitterX`, `jitterY`) render texels — which the fill takes back out (`recon/lateDepth.ts`).
76
+ */
73
77
  upscale(
74
78
  encoder: GPUCommandEncoder,
75
79
  source: GPUTextureView,
@@ -77,12 +81,16 @@ export class LatePass {
77
81
  renderHeight: number,
78
82
  outputWidth: number,
79
83
  outputHeight: number,
84
+ jitterX: number,
85
+ jitterY: number,
80
86
  ): void {
81
87
  const target = this.depthView(outputWidth, outputHeight);
82
88
  this.sizeStaging[0] = renderWidth;
83
89
  this.sizeStaging[1] = renderHeight;
84
90
  this.sizeStaging[2] = outputWidth;
85
91
  this.sizeStaging[3] = outputHeight;
92
+ this.sizeStaging[4] = jitterX;
93
+ this.sizeStaging[5] = jitterY;
86
94
  this.device.queue.writeBuffer(this.sizes, 0, this.sizeStaging);
87
95
  if (this.group === null || this.groupSource !== source) {
88
96
  this.group = this.device.createBindGroup({
@@ -120,6 +128,7 @@ export class LatePass {
120
128
  command.indexBuffer = null;
121
129
  command.indexed = false;
122
130
  command.instances = 1;
131
+ command.indirect = null;
123
132
  return command;
124
133
  }
125
134
 
@@ -509,7 +509,8 @@ export class MotionPass {
509
509
  } else {
510
510
  pipeline = this.pipelineFor(mesh.vertexStride);
511
511
  }
512
- if (pipeline === null) continue;
512
+ /* A mesh with nothing in it draws nothing: see `GpuMesh.indexCount`. */
513
+ if (pipeline === null || mesh.indexCount === 0) continue;
513
514
  if (pipeline !== bound) {
514
515
  pass.setPipeline(pipeline);
515
516
  bound = pipeline;