@driftengine/core 4.3.0 → 4.4.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 (218) hide show
  1. package/CHANGELOG.json +282 -0
  2. package/README.md +1 -1
  3. package/dist/cinematic/cameraPath.d.ts +53 -0
  4. package/dist/cinematic/cameraPath.js +118 -0
  5. package/dist/environment/daylight.d.ts +77 -0
  6. package/dist/environment/daylight.js +77 -0
  7. package/dist/geometry/meshConcat.d.ts +9 -0
  8. package/dist/geometry/meshConcat.js +101 -0
  9. package/dist/index.d.ts +12 -2
  10. package/dist/index.js +14 -1
  11. package/dist/render/ambientOcclusionPass.js +5 -1
  12. package/dist/render/backend/createRenderer.d.ts +13 -1
  13. package/dist/render/backend/createRenderer.js +6 -5
  14. package/dist/render/backend/webgl2/renderer.d.ts +147 -1
  15. package/dist/render/backend/webgl2/renderer.js +402 -23
  16. package/dist/render/backend/webgpu/buffers.js +46 -8
  17. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  18. package/dist/render/backend/webgpu/depthPass.js +75 -14
  19. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  20. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  21. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  22. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  23. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  24. package/dist/render/backend/webgpu/flatPass.js +12 -5
  25. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  26. package/dist/render/backend/webgpu/particlePass.js +1 -0
  27. package/dist/render/backend/webgpu/postPass.d.ts +13 -3
  28. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  29. package/dist/render/backend/webgpu/probeBake.js +97 -7
  30. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  31. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  32. package/dist/render/backend/webgpu/renderer.d.ts +174 -11
  33. package/dist/render/backend/webgpu/renderer.js +682 -252
  34. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  35. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  36. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  37. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  38. package/dist/render/backend/webgpu/skyPass.js +5 -1
  39. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  40. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  41. package/dist/render/bloomChain.d.ts +6 -0
  42. package/dist/render/bloomChain.js +8 -0
  43. package/dist/render/clusteredLights.d.ts +19 -11
  44. package/dist/render/clusteredLights.js +96 -45
  45. package/dist/render/cutoutCaster.d.ts +23 -0
  46. package/dist/render/cutoutCaster.js +20 -0
  47. package/dist/render/driftLight/bake.d.ts +34 -0
  48. package/dist/render/driftLight/bake.js +95 -0
  49. package/dist/render/driftLight/falloff.d.ts +16 -0
  50. package/dist/render/driftLight/falloff.js +23 -0
  51. package/dist/render/driftLight/layout.d.ts +59 -0
  52. package/dist/render/driftLight/layout.js +138 -0
  53. package/dist/render/driftLight/lightField.d.ts +108 -0
  54. package/dist/render/driftLight/lightField.js +179 -0
  55. package/dist/render/driftLight/uniforms.d.ts +28 -0
  56. package/dist/render/driftLight/uniforms.js +41 -0
  57. package/dist/render/driftLight/visibility.d.ts +18 -0
  58. package/dist/render/driftLight/visibility.js +47 -0
  59. package/dist/render/emptyTexture.d.ts +6 -0
  60. package/dist/render/emptyTexture.js +19 -0
  61. package/dist/render/exposurePass.d.ts +48 -0
  62. package/dist/render/exposurePass.js +172 -0
  63. package/dist/render/flameLight.d.ts +71 -0
  64. package/dist/render/flameLight.js +63 -0
  65. package/dist/render/gi/bounceLights.d.ts +34 -0
  66. package/dist/render/gi/bounceLights.js +66 -0
  67. package/dist/render/gi/fieldReach.d.ts +12 -0
  68. package/dist/render/gi/fieldReach.js +62 -0
  69. package/dist/render/gi/globalField.d.ts +8 -0
  70. package/dist/render/gi/globalField.js +31 -0
  71. package/dist/render/gi/probeTrace.d.ts +3 -1
  72. package/dist/render/gi/probeTrace.js +3 -1
  73. package/dist/render/gpuCapability.js +19 -4
  74. package/dist/render/lightBudget.d.ts +11 -0
  75. package/dist/render/lightBudget.js +11 -0
  76. package/dist/render/pointLightSelection.d.ts +16 -0
  77. package/dist/render/pointLightSelection.js +178 -83
  78. package/dist/render/pointShadowBudget.d.ts +12 -2
  79. package/dist/render/pointShadowBudget.js +16 -10
  80. package/dist/render/probeGrid.d.ts +11 -1
  81. package/dist/render/probeGrid.js +5 -2
  82. package/dist/render/probeSweeps.d.ts +45 -0
  83. package/dist/render/probeSweeps.js +77 -0
  84. package/dist/render/reflectionProbe.d.ts +43 -1
  85. package/dist/render/reflectionProbe.js +17 -3
  86. package/dist/render/renderQuality.d.ts +3 -1
  87. package/dist/render/rendererHarness.d.ts +9 -0
  88. package/dist/render/rendererHarness.js +18 -3
  89. package/dist/render/sceneTarget.d.ts +20 -0
  90. package/dist/render/sceneTarget.js +62 -1
  91. package/dist/render/shaders/ambientOcclusion.d.ts +63 -6
  92. package/dist/render/shaders/ambientOcclusion.js +188 -71
  93. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  94. package/dist/render/shaders/cutoutCoverage.js +40 -0
  95. package/dist/render/shaders/depth.d.ts +25 -0
  96. package/dist/render/shaders/depth.js +69 -3
  97. package/dist/render/shaders/exposure.d.ts +52 -0
  98. package/dist/render/shaders/exposure.js +124 -0
  99. package/dist/render/shaders/filmLook.d.ts +47 -0
  100. package/dist/render/shaders/filmLook.js +98 -0
  101. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  102. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  103. package/dist/render/shaders/flat/driftLight.js +75 -0
  104. package/dist/render/shaders/flat/index.js +5 -0
  105. package/dist/render/shaders/flat/lobes.js +13 -0
  106. package/dist/render/shaders/flat/main.d.ts +1 -1
  107. package/dist/render/shaders/flat/main.js +68 -9
  108. package/dist/render/shaders/flat/preamble.js +23 -5
  109. package/dist/render/shaders/flat/probeGrid.js +46 -3
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
  111. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
  112. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  113. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  115. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  116. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  117. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  118. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  119. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  120. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  121. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  124. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  125. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  126. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  127. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  128. package/dist/render/shaders/globalMedium.js +4 -0
  129. package/dist/render/shaders/localExposure.d.ts +35 -0
  130. package/dist/render/shaders/localExposure.js +138 -0
  131. package/dist/render/shaders/prefilter.d.ts +1 -1
  132. package/dist/render/shaders/prefilter.js +6 -0
  133. package/dist/render/shaders/rush.js +33 -5
  134. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  135. package/dist/render/shaders/tangentFrame.js +15 -3
  136. package/dist/render/shaders/temporalResolve.js +39 -1
  137. package/dist/render/shadowCasters.d.ts +5 -3
  138. package/dist/render/shadowMap.js +13 -0
  139. package/dist/render/surfaceTexture.d.ts +5 -0
  140. package/package.json +6 -6
  141. package/scripts/gpuCompute.mjs +24 -3
  142. package/scripts/png.mjs +23 -12
  143. package/src/cinematic/cameraPath.ts +178 -0
  144. package/src/environment/daylight.ts +152 -0
  145. package/src/geometry/meshConcat.ts +100 -0
  146. package/src/index.ts +32 -0
  147. package/src/render/ambientOcclusionPass.ts +5 -1
  148. package/src/render/backend/createRenderer.ts +22 -6
  149. package/src/render/backend/webgl2/renderer.ts +439 -20
  150. package/src/render/backend/webgpu/buffers.ts +51 -8
  151. package/src/render/backend/webgpu/depthPass.ts +78 -10
  152. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  153. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  154. package/src/render/backend/webgpu/flatPass.ts +10 -2
  155. package/src/render/backend/webgpu/particlePass.ts +9 -0
  156. package/src/render/backend/webgpu/probeBake.ts +143 -9
  157. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  158. package/src/render/backend/webgpu/renderer.ts +808 -273
  159. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  160. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  161. package/src/render/backend/webgpu/skyPass.ts +5 -1
  162. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  163. package/src/render/bloomChain.ts +9 -0
  164. package/src/render/clusteredLights.ts +108 -63
  165. package/src/render/cutoutCaster.ts +44 -0
  166. package/src/render/driftLight/bake.ts +125 -0
  167. package/src/render/driftLight/falloff.ts +26 -0
  168. package/src/render/driftLight/layout.ts +177 -0
  169. package/src/render/driftLight/lightField.ts +227 -0
  170. package/src/render/driftLight/uniforms.ts +61 -0
  171. package/src/render/driftLight/visibility.ts +58 -0
  172. package/src/render/emptyTexture.ts +41 -0
  173. package/src/render/exposurePass.ts +186 -0
  174. package/src/render/flameLight.ts +135 -0
  175. package/src/render/gi/bounceLights.ts +68 -0
  176. package/src/render/gi/fieldReach.ts +74 -0
  177. package/src/render/gi/globalField.ts +39 -0
  178. package/src/render/gi/probeTrace.ts +3 -1
  179. package/src/render/gpuCapability.ts +18 -5
  180. package/src/render/lightBudget.ts +12 -0
  181. package/src/render/pointLightSelection.ts +191 -79
  182. package/src/render/pointShadowBudget.ts +16 -8
  183. package/src/render/probeGrid.ts +13 -2
  184. package/src/render/probeSweeps.ts +80 -0
  185. package/src/render/reflectionProbe.ts +54 -2
  186. package/src/render/renderQuality.ts +3 -1
  187. package/src/render/rendererHarness.ts +25 -3
  188. package/src/render/sceneTarget.ts +75 -1
  189. package/src/render/shaders/ambientOcclusion.ts +205 -71
  190. package/src/render/shaders/cutoutCoverage.ts +43 -0
  191. package/src/render/shaders/depth.ts +80 -3
  192. package/src/render/shaders/exposure.ts +128 -0
  193. package/src/render/shaders/filmLook.ts +111 -0
  194. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  195. package/src/render/shaders/flat/driftLight.ts +75 -0
  196. package/src/render/shaders/flat/index.ts +5 -0
  197. package/src/render/shaders/flat/lobes.ts +13 -0
  198. package/src/render/shaders/flat/main.ts +68 -9
  199. package/src/render/shaders/flat/preamble.ts +23 -5
  200. package/src/render/shaders/flat/probeGrid.ts +46 -3
  201. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
  202. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  203. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  204. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  205. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  206. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  207. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  208. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  209. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  210. package/src/render/shaders/globalMedium.ts +4 -0
  211. package/src/render/shaders/localExposure.ts +154 -0
  212. package/src/render/shaders/prefilter.ts +6 -0
  213. package/src/render/shaders/rush.ts +33 -5
  214. package/src/render/shaders/tangentFrame.ts +15 -3
  215. package/src/render/shaders/temporalResolve.ts +39 -1
  216. package/src/render/shadowCasters.ts +5 -3
  217. package/src/render/shadowMap.ts +14 -0
  218. package/src/render/surfaceTexture.ts +5 -0
@@ -1,6 +1,7 @@
1
1
  import { boundsOfPositions, createBounds } from '../../../math/bounds.ts';
2
2
  import type { Bounds } from '../../../math/bounds.ts';
3
3
  import { MorphTexture } from './morphTexture.ts';
4
+ import { validateMeshData } from '../../mesh.ts';
4
5
  import type { MeshData } from '../../mesh.ts';
5
6
  import { ABSENT_ATTRIBUTE } from '../../vertexDefaults.ts';
6
7
  import { UPLOAD_BYTES_PER_STEP } from '../../uploadStep.ts';
@@ -255,6 +256,11 @@ export function createGpuMeshIncremental(
255
256
  data: MeshData,
256
257
  dynamic = false,
257
258
  ): IncrementalGpuMesh {
259
+ /*
260
+ * The other backend's refusal, and the premise of the interleave below: every attribute holds
261
+ * exactly a vertex's worth for every vertex, so nothing reads past the end of its array.
262
+ */
263
+ validateMeshData(data);
258
264
  const bounds = boundsOfPositions(data.positions, createBounds());
259
265
  const vertexCount = data.positions.length / 3;
260
266
  const supplied = VERTEX_LAYOUT.filter(
@@ -434,14 +440,8 @@ function* uploadSteps(job: UploadJob): Generator<void, void, void> {
434
440
  let fieldOffset = 0;
435
441
  for (const attribute of supplied) {
436
442
  const source = data[attribute.name as keyof MeshData] as Float32Array;
437
- const width = attribute.components;
438
- for (let vertex = from; vertex < to; vertex++) {
439
- for (let component = 0; component < width; component++) {
440
- interleaved[vertex * step + fieldOffset + component] =
441
- source[vertex * width + component] ?? 0;
442
- }
443
- }
444
- fieldOffset += width;
443
+ interleaveField(interleaved, source, attribute.components, step, fieldOffset, from, to);
444
+ fieldOffset += attribute.components;
445
445
  }
446
446
  device.queue.writeBuffer(vertices, from * stride, interleaved, from * step, (to - from) * step);
447
447
  taken += 1;
@@ -462,6 +462,49 @@ function* uploadSteps(job: UploadJob): Generator<void, void, void> {
462
462
  job.progress.uploaded = true;
463
463
  }
464
464
 
465
+ /**
466
+ * One attribute of vertices `from` to `to` into its field of the interleaved rows.
467
+ *
468
+ * **Unrolled by width, because the general loop was most of a heavy load's upload.** A loop over
469
+ * components inside a loop over vertices, with the index recomputed from both, cost 1.5 s of main
470
+ * thread on a courtyard of five packs; a loop per width walking two cursors does the same writes in
471
+ * about a third of the time, measured on four million vertices in Node. Every width the layout
472
+ * carries is one, two, three or four, so the last arm is the one-float case and nothing else.
473
+ */
474
+ function interleaveField(
475
+ rows: Float32Array,
476
+ source: Float32Array,
477
+ width: number,
478
+ step: number,
479
+ fieldOffset: number,
480
+ from: number,
481
+ to: number,
482
+ ): void {
483
+ let write = from * step + fieldOffset;
484
+ const end = to * width;
485
+ if (width === 3) {
486
+ for (let read = from * 3; read < end; read += 3, write += step) {
487
+ rows[write] = source[read] as number;
488
+ rows[write + 1] = source[read + 1] as number;
489
+ rows[write + 2] = source[read + 2] as number;
490
+ }
491
+ } else if (width === 4) {
492
+ for (let read = from * 4; read < end; read += 4, write += step) {
493
+ rows[write] = source[read] as number;
494
+ rows[write + 1] = source[read + 1] as number;
495
+ rows[write + 2] = source[read + 2] as number;
496
+ rows[write + 3] = source[read + 3] as number;
497
+ }
498
+ } else if (width === 2) {
499
+ for (let read = from * 2; read < end; read += 2, write += step) {
500
+ rows[write] = source[read] as number;
501
+ rows[write + 1] = source[read + 1] as number;
502
+ }
503
+ } else {
504
+ for (let read = from; read < end; read++, write += step) rows[write] = source[read] as number;
505
+ }
506
+ }
507
+
465
508
  /** The handle, made where it can see only what it hands out. */
466
509
  function gpuMeshOf(parts: {
467
510
  readonly vertices: GPUBuffer;
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  DEPTH_BINDINGS,
3
+ DEPTH_CUTOUT_FRAG_WGSL,
4
+ DEPTH_CUTOUT_VERT_WGSL,
3
5
  DEPTH_FRAG_WGSL,
6
+ DEPTH_INSTANCED_CUTOUT_VERT_WGSL,
4
7
  DEPTH_INSTANCED_VERT_WGSL,
5
8
  DEPTH_SKINNED_VERT_WGSL,
6
9
  DEPTH_VERT_WGSL,
@@ -20,6 +23,15 @@ import { shaderModule } from './shaderModules.ts';
20
23
  export const DEPTH_VERT_FIELDS = DEPTH_BINDINGS.DEPTH_VERT.fields;
21
24
  export const DEPTH_VERT_SIZE = DEPTH_BINDINGS.DEPTH_VERT.uniformSize;
22
25
  export const DEPTH_FRAG_SIZE = DEPTH_BINDINGS.DEPTH_FRAG.uniformSize;
26
+ /*
27
+ * The cutout variants' vertex blocks. The rigid one is the plain block with `uUvScale` and
28
+ * `uAlphaCutout` after every other field, so a slot written for it is written with the plain
29
+ * offsets plus these two; the instanced one has no model and no wind, so its two sit earlier.
30
+ */
31
+ export const DEPTH_CUTOUT_VERT_FIELDS = DEPTH_BINDINGS.DEPTH_CUTOUT_VERT.fields;
32
+ export const DEPTH_CUTOUT_VERT_SIZE = DEPTH_BINDINGS.DEPTH_CUTOUT_VERT.uniformSize;
33
+ export const DEPTH_INSTANCED_CUTOUT_VERT_FIELDS = DEPTH_BINDINGS.DEPTH_INSTANCED_CUTOUT_VERT.fields;
34
+ const CUTOUT_MAP = DEPTH_BINDINGS.DEPTH_CUTOUT_FRAG.textures.uCutoutMap;
23
35
  /** Re-exported so the scatter depth pass can pair with the same fragment stage. */
24
36
  export { DEPTH_FRAG_WGSL };
25
37
  export const DEPTH_FRAG_BINDING = DEPTH_BINDINGS.DEPTH_FRAG.uniforms;
@@ -55,9 +67,15 @@ export function createDepthBindGroupLayout(
55
67
  * frame that skins nothing pays nothing at all.
56
68
  */
57
69
  skinned = false,
70
+ /**
71
+ * Whether this layout carries a cutout map, for a caster whose shape is in its texture's alpha.
72
+ * Separate for the palette's reason: the plain layout stays exactly what it was, and a frame with
73
+ * no cutout caster pays nothing.
74
+ */
75
+ cutout = false,
58
76
  ): GPUBindGroupLayout {
59
77
  return device.createBindGroupLayout({
60
- label: skinned ? 'depth.layout.skinned' : 'depth.layout',
78
+ label: skinned ? 'depth.layout.skinned' : cutout ? 'depth.layout.cutout' : 'depth.layout',
61
79
  entries: [
62
80
  /* The texture and no sampler: the shader reads the palette with `textureLoad`, an integer
63
81
  coordinate with nothing to filter, which is what `flatPass.ts` binds for the same
@@ -74,7 +92,11 @@ export function createDepthBindGroupLayout(
74
92
  {
75
93
  binding: VERT_BINDING,
76
94
  visibility: VISIBILITY_VERTEX,
77
- buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: DEPTH_VERT_SIZE },
95
+ buffer: {
96
+ type: 'uniform',
97
+ hasDynamicOffset: true,
98
+ minBindingSize: cutout ? DEPTH_CUTOUT_VERT_SIZE : DEPTH_VERT_SIZE,
99
+ },
78
100
  },
79
101
  {
80
102
  binding: FRAG_BINDING,
@@ -107,6 +129,22 @@ export function createDepthBindGroupLayout(
107
129
  visibility: VISIBILITY_FRAGMENT,
108
130
  sampler: { type: 'non-filtering' },
109
131
  },
132
+ /* A colour map sampled with its own filter and mips, so a leaf's edge in the map is the edge
133
+ a colour pass at that distance would draw. */
134
+ ...(cutout
135
+ ? [
136
+ {
137
+ binding: CUTOUT_MAP.texture,
138
+ visibility: VISIBILITY_FRAGMENT,
139
+ texture: { sampleType: 'float' as GPUTextureSampleType },
140
+ },
141
+ {
142
+ binding: CUTOUT_MAP.sampler,
143
+ visibility: VISIBILITY_FRAGMENT,
144
+ sampler: { type: 'filtering' as GPUSamplerBindingType },
145
+ },
146
+ ]
147
+ : []),
110
148
  ],
111
149
  });
112
150
  }
@@ -120,16 +158,35 @@ export function createDepthBindGroup(
120
158
  sampler: GPUSampler,
121
159
  /** The joint palette, for a layout built with `skinned`. Omit for the rigid layout. */
122
160
  palette: GPUTextureView | null = null,
161
+ /** The cutout map and its sampler, for a layout built with `cutout`. */
162
+ cutout: { readonly view: GPUTextureView; readonly sampler: GPUSampler } | null = null,
123
163
  ): GPUBindGroup {
124
164
  return device.createBindGroup({
125
- label: palette === null ? 'depth.bindGroup' : 'depth.bindGroup.skinned',
165
+ label:
166
+ palette !== null
167
+ ? 'depth.bindGroup.skinned'
168
+ : cutout !== null
169
+ ? 'depth.bindGroup.cutout'
170
+ : 'depth.bindGroup',
126
171
  layout,
127
172
  entries: [
128
- { binding: VERT_BINDING, resource: { buffer: perDraw, size: DEPTH_VERT_SIZE } },
173
+ {
174
+ binding: VERT_BINDING,
175
+ resource: {
176
+ buffer: perDraw,
177
+ size: cutout !== null ? DEPTH_CUTOUT_VERT_SIZE : DEPTH_VERT_SIZE,
178
+ },
179
+ },
129
180
  { binding: FRAG_BINDING, resource: { buffer: perPass, size: DEPTH_FRAG_SIZE } },
130
181
  { binding: PREVIOUS.texture, resource: previous },
131
182
  { binding: PREVIOUS.sampler, resource: sampler },
132
183
  ...(palette !== null ? [{ binding: DEPTH_PALETTE_BINDING.texture, resource: palette }] : []),
184
+ ...(cutout !== null
185
+ ? [
186
+ { binding: CUTOUT_MAP.texture, resource: cutout.view },
187
+ { binding: CUTOUT_MAP.sampler, resource: cutout.sampler },
188
+ ]
189
+ : []),
133
190
  ],
134
191
  });
135
192
  }
@@ -169,6 +226,8 @@ export function depthPipeline(
169
226
  * reclaims the two attribute locations the joint indices and weights occupy.
170
227
  */
171
228
  instanced = false,
229
+ /** Whether this pipeline casts a cutout: the variant that reads a map and discards by it. */
230
+ cutout = false,
172
231
  ): GPURenderPipeline {
173
232
  return cache.get(key, () => ({
174
233
  label: key,
@@ -176,17 +235,26 @@ export function depthPipeline(
176
235
  vertex: {
177
236
  module: shaderModule(
178
237
  device,
179
- instanced
180
- ? { label: 'depth.instanced.vert', code: DEPTH_INSTANCED_VERT_WGSL }
181
- : skinned
182
- ? { label: 'depth.skinned.vert', code: DEPTH_SKINNED_VERT_WGSL }
183
- : { label: 'depth.vert', code: DEPTH_VERT_WGSL },
238
+ cutout
239
+ ? instanced
240
+ ? { label: 'depth.instanced.cutout.vert', code: DEPTH_INSTANCED_CUTOUT_VERT_WGSL }
241
+ : { label: 'depth.cutout.vert', code: DEPTH_CUTOUT_VERT_WGSL }
242
+ : instanced
243
+ ? { label: 'depth.instanced.vert', code: DEPTH_INSTANCED_VERT_WGSL }
244
+ : skinned
245
+ ? { label: 'depth.skinned.vert', code: DEPTH_SKINNED_VERT_WGSL }
246
+ : { label: 'depth.vert', code: DEPTH_VERT_WGSL },
184
247
  ),
185
248
  entryPoint: 'main',
186
249
  buffers: vertexBufferLayouts(present, instanced),
187
250
  },
188
251
  fragment: {
189
- module: shaderModule(device, { label: 'depth.frag', code: DEPTH_FRAG_WGSL }),
252
+ module: shaderModule(
253
+ device,
254
+ cutout
255
+ ? { label: 'depth.cutout.frag', code: DEPTH_CUTOUT_FRAG_WGSL }
256
+ : { label: 'depth.frag', code: DEPTH_FRAG_WGSL },
257
+ ),
190
258
  entryPoint: 'main',
191
259
  targets: [],
192
260
  },
@@ -0,0 +1,234 @@
1
+ import {
2
+ EXPOSURE_ADAPT_FRAG_WGSL,
3
+ EXPOSURE_BINDINGS,
4
+ EXPOSURE_METER_FRAG_WGSL,
5
+ } from '../../shaders/generated/exposure.wgsl.ts';
6
+ import {
7
+ EXPOSURE_LOCAL_FRAG_WGSL,
8
+ LOCALEXPOSURE_BINDINGS,
9
+ } from '../../shaders/generated/localExposure.wgsl.ts';
10
+ import { METER_GRID, adaptBlend } from '../../shaders/exposure.ts';
11
+ import { LOCAL_GRID_WIDTH } from '../../shaders/localExposure.ts';
12
+ import { createPostStageLayout, postPipeline } from './postPass.ts';
13
+ import type { PipelineCache } from './pipelineCache.ts';
14
+
15
+ /**
16
+ * Eye adaptation on WebGPU: meter the finished scene into the frame's own encoder, and follow it
17
+ * with a held brightness the composite reads. See `shaders/exposure.ts` for the design.
18
+ *
19
+ * **One held texel the composite always binds, and a scratch the adaptation writes.** Reading and
20
+ * writing one texture in a pass is refused, and a pair that swapped roles would mean a composite
21
+ * bind group per role; so the adaptation writes the scratch from the held value and a copy puts it
22
+ * back, on the same encoder, before the composite reads it. Recorded into the frame's encoder
23
+ * rather than submitted apart, because a separate submission would run before the frame that
24
+ * draws the scene it is meant to measure.
25
+ *
26
+ * **And local exposure's grid, when that is asked for**, in the same encoder after the meter:
27
+ * `rg16float`, a sum and a count per tile per band, sampled filtered by the composite. See
28
+ * `localExposure.ts`.
29
+ */
30
+
31
+ const METER = EXPOSURE_BINDINGS.EXPOSURE_METER_FRAG.textures;
32
+ const ADAPT = EXPOSURE_BINDINGS.EXPOSURE_ADAPT_FRAG;
33
+ const LOCAL = LOCALEXPOSURE_BINDINGS.EXPOSURE_LOCAL_FRAG.textures;
34
+ const USAGE_RENDER = 0x10;
35
+ const USAGE_TEXTURE = 0x04;
36
+ const USAGE_COPY_SRC = 0x01;
37
+ const USAGE_COPY_DST = 0x02;
38
+ const USAGE_UNIFORM_DST = 0x40 | 0x08;
39
+ const VISIBILITY_FRAGMENT = 0x2;
40
+
41
+ export class ExposurePass {
42
+ private readonly meter: GPUTexture;
43
+ private readonly meterView: GPUTextureView;
44
+ private readonly held: GPUTexture;
45
+ /** The texel the composite binds, for its bind group. */
46
+ readonly heldView: GPUTextureView;
47
+ private readonly scratch: GPUTexture;
48
+ private readonly scratchView: GPUTextureView;
49
+ private readonly localGrid: GPUTexture;
50
+ /** Local exposure's grid, for the composite's bind group. */
51
+ readonly localView: GPUTextureView;
52
+ private readonly localLayout: GPUBindGroupLayout;
53
+ private localGroup: GPUBindGroup | null = null;
54
+ private localSource: GPUTextureView | null = null;
55
+ private readonly meterLayout: GPUBindGroupLayout;
56
+ private readonly adaptLayout: GPUBindGroupLayout;
57
+ private readonly adaptGroup: GPUBindGroup;
58
+ private readonly uniforms: GPUBuffer;
59
+ private readonly blend = new Float32Array(4);
60
+ private meterGroup: GPUBindGroup | null = null;
61
+ private meterSource: GPUTextureView | null = null;
62
+ /** Whether anything is held yet: the first frame, and the first after a cut, snaps. */
63
+ private holding = false;
64
+
65
+ constructor(
66
+ private readonly device: GPUDevice,
67
+ private readonly pipelines: PipelineCache,
68
+ private readonly sampler: GPUSampler,
69
+ ) {
70
+ const target = (label: string, size: number, usage: number): GPUTexture =>
71
+ device.createTexture({ label, size: [size, size], format: 'r16float', usage });
72
+ this.meter = target('exposure.meter', METER_GRID, USAGE_RENDER | USAGE_TEXTURE);
73
+ this.held = target('exposure.held', 1, USAGE_RENDER | USAGE_TEXTURE | USAGE_COPY_DST);
74
+ this.scratch = target('exposure.scratch', 1, USAGE_RENDER | USAGE_COPY_SRC);
75
+ this.localGrid = device.createTexture({
76
+ label: 'exposure.localGrid',
77
+ size: [LOCAL_GRID_WIDTH, METER_GRID],
78
+ format: 'rg16float',
79
+ usage: USAGE_RENDER | USAGE_TEXTURE,
80
+ });
81
+ this.localView = this.localGrid.createView();
82
+ this.meterView = this.meter.createView();
83
+ this.heldView = this.held.createView();
84
+ this.scratchView = this.scratch.createView();
85
+
86
+ /* The meter has no uniforms, so its layout is its one texture and sampler and nothing else. */
87
+ this.meterLayout = device.createBindGroupLayout({
88
+ label: 'exposure.meterLayout',
89
+ entries: [
90
+ { binding: METER.uScene.texture, visibility: VISIBILITY_FRAGMENT, texture: {} },
91
+ { binding: METER.uScene.sampler, visibility: VISIBILITY_FRAGMENT, sampler: {} },
92
+ ],
93
+ });
94
+ this.localLayout = device.createBindGroupLayout({
95
+ label: 'exposure.localLayout',
96
+ entries: [
97
+ { binding: LOCAL.uScene.texture, visibility: VISIBILITY_FRAGMENT, texture: {} },
98
+ { binding: LOCAL.uScene.sampler, visibility: VISIBILITY_FRAGMENT, sampler: {} },
99
+ ],
100
+ });
101
+ this.adaptLayout = createPostStageLayout(device, ADAPT.uniforms, ADAPT.uniformSize, [
102
+ { binding: ADAPT.textures.uMeter },
103
+ { binding: ADAPT.textures.uHeld },
104
+ ]);
105
+ this.uniforms = device.createBuffer({
106
+ label: 'exposure.uniforms',
107
+ size: ADAPT.uniformSize,
108
+ usage: USAGE_UNIFORM_DST,
109
+ });
110
+ this.adaptGroup = device.createBindGroup({
111
+ label: 'exposure.adaptGroup',
112
+ layout: this.adaptLayout,
113
+ entries: [
114
+ { binding: ADAPT.uniforms, resource: { buffer: this.uniforms, size: ADAPT.uniformSize } },
115
+ { binding: ADAPT.textures.uMeter.texture, resource: this.meterView },
116
+ { binding: ADAPT.textures.uMeter.sampler, resource: sampler },
117
+ { binding: ADAPT.textures.uHeld.texture, resource: this.heldView },
118
+ { binding: ADAPT.textures.uHeld.sampler, resource: sampler },
119
+ ],
120
+ });
121
+ }
122
+
123
+ /** Forget what is held, so the next frame meters afresh rather than easing from another shot. */
124
+ cut(): void {
125
+ this.holding = false;
126
+ }
127
+
128
+ /**
129
+ * Meter `scene` and move the held brightness toward it by `dtSec` of adaptation, and with `local`
130
+ * draw local exposure's grid too.
131
+ */
132
+ run(encoder: GPUCommandEncoder, scene: GPUTextureView, dtSec: number, local = false): void {
133
+ const { device } = this;
134
+ if (this.meterSource !== scene || this.meterGroup === null) {
135
+ this.meterGroup = device.createBindGroup({
136
+ label: 'exposure.meterGroup',
137
+ layout: this.meterLayout,
138
+ entries: [
139
+ { binding: METER.uScene.texture, resource: scene },
140
+ { binding: METER.uScene.sampler, resource: this.sampler },
141
+ ],
142
+ });
143
+ this.meterSource = scene;
144
+ }
145
+ this.blend[0] = adaptBlend(dtSec, !this.holding);
146
+ device.queue.writeBuffer(this.uniforms, 0, this.blend);
147
+
148
+ const meter = encoder.beginRenderPass({
149
+ label: 'exposure.meter',
150
+ colorAttachments: [
151
+ { view: this.meterView, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
152
+ ],
153
+ });
154
+ meter.setPipeline(
155
+ postPipeline(
156
+ this.pipelines,
157
+ device,
158
+ this.meterLayout,
159
+ 'exposure.meter',
160
+ EXPOSURE_METER_FRAG_WGSL,
161
+ 'r16float',
162
+ ),
163
+ );
164
+ meter.setBindGroup(0, this.meterGroup);
165
+ meter.draw(3);
166
+ meter.end();
167
+ if (local) this.drawLocal(encoder, scene);
168
+
169
+ const adapt = encoder.beginRenderPass({
170
+ label: 'exposure.adapt',
171
+ colorAttachments: [
172
+ { view: this.scratchView, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
173
+ ],
174
+ });
175
+ adapt.setPipeline(
176
+ postPipeline(
177
+ this.pipelines,
178
+ device,
179
+ this.adaptLayout,
180
+ 'exposure.adapt',
181
+ EXPOSURE_ADAPT_FRAG_WGSL,
182
+ 'r16float',
183
+ ),
184
+ );
185
+ adapt.setBindGroup(0, this.adaptGroup, [0]);
186
+ adapt.draw(3);
187
+ adapt.end();
188
+ encoder.copyTextureToTexture({ texture: this.scratch }, { texture: this.held }, [1, 1, 1]);
189
+ this.holding = true;
190
+ }
191
+
192
+ /** Local exposure's grid, from the same scene the meter read. */
193
+ private drawLocal(encoder: GPUCommandEncoder, scene: GPUTextureView): void {
194
+ const { device } = this;
195
+ if (this.localSource !== scene || this.localGroup === null) {
196
+ this.localGroup = device.createBindGroup({
197
+ label: 'exposure.localGroup',
198
+ layout: this.localLayout,
199
+ entries: [
200
+ { binding: LOCAL.uScene.texture, resource: scene },
201
+ { binding: LOCAL.uScene.sampler, resource: this.sampler },
202
+ ],
203
+ });
204
+ this.localSource = scene;
205
+ }
206
+ const pass = encoder.beginRenderPass({
207
+ label: 'exposure.local',
208
+ colorAttachments: [
209
+ { view: this.localView, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
210
+ ],
211
+ });
212
+ pass.setPipeline(
213
+ postPipeline(
214
+ this.pipelines,
215
+ device,
216
+ this.localLayout,
217
+ 'exposure.local',
218
+ EXPOSURE_LOCAL_FRAG_WGSL,
219
+ 'rg16float',
220
+ ),
221
+ );
222
+ pass.setBindGroup(0, this.localGroup);
223
+ pass.draw(3);
224
+ pass.end();
225
+ }
226
+
227
+ dispose(): void {
228
+ this.localGrid.destroy();
229
+ this.meter.destroy();
230
+ this.held.destroy();
231
+ this.scratch.destroy();
232
+ this.uniforms.destroy();
233
+ }
234
+ }
@@ -71,6 +71,16 @@ export const DEFAULT_FIELD_COMPOSE: FieldComposeOptions = {
71
71
  radius: 4,
72
72
  };
73
73
 
74
+ /**
75
+ * The most cascades a field is given to reach a probe grid, which is `cascadesForGrid`'s ceiling.
76
+ *
77
+ * Six from the 4 m default reach 128 m, and each is the same 49 cubed samples, about half a
78
+ * megabyte, so the bound is on memory rather than on distance. What would make it wrong is a scene
79
+ * whose probe grid is longer than about a hundred metres, whose far probes are then left untraced
80
+ * rather than traced against a field that is not there.
81
+ */
82
+ export const MAX_FIELD_CASCADES = 6;
83
+
74
84
  export interface FieldComposeOptions {
75
85
  /** Samples a side, every cascade. */
76
86
  readonly resolution: number;
@@ -147,6 +157,11 @@ export class FieldComposer {
147
157
  private timed = false;
148
158
  private composeMsValue = 0;
149
159
 
160
+ /** How many cascades this composer was built with. */
161
+ get cascadeCount(): number {
162
+ return this.options.cascades;
163
+ }
164
+
150
165
  constructor(device: GPUDevice, options: FieldComposeOptions) {
151
166
  if (!(Math.trunc(options.resolution) >= 2)) {
152
167
  throw new Error(
@@ -269,7 +269,9 @@ export function createFlatBindGroupLayout(
269
269
  * other kind against a `uint` texture, and the shader never samples it — every read is a
270
270
  * `texelFetch`, which is why the declaration shares a sampler rather than bringing its own.
271
271
  */
272
- const integer = texture.type === 'usampler2D';
272
+ const integer = texture.type === 'usampler2D' || texture.type === 'usampler3D';
273
+ /* DriftLight's brick index and atlas, the lit pass's only volumes. See `driftLight.ts`. */
274
+ const volume = texture.type === 'sampler3D' || texture.type === 'usampler3D';
273
275
  const isShadow =
274
276
  integer || (name !== 'uEnvironment' && (name.endsWith('ShadowMap') || cube || array));
275
277
  entries.push({
@@ -279,6 +281,7 @@ export function createFlatBindGroupLayout(
279
281
  sampleType: integer ? 'uint' : isShadow ? 'unfilterable-float' : 'float',
280
282
  ...(cube ? { viewDimension: 'cube' as const } : {}),
281
283
  ...(array ? { viewDimension: '2d-array' as const } : {}),
284
+ ...(volume ? { viewDimension: '3d' as const } : {}),
282
285
  },
283
286
  });
284
287
  /*
@@ -460,6 +463,8 @@ export function flatPipeline(
460
463
  instanced = false,
461
464
  /** Which order-independent buffer this pipeline writes, if any. The key must carry it. */
462
465
  oit: OitTarget = 'none',
466
+ /** Culls nothing, for a surface seen from both faces. The key must carry it: `|2s`. */
467
+ doubleSided = false,
463
468
  ): GPURenderPipeline {
464
469
  return cache.get(key, () =>
465
470
  flatDescriptor(
@@ -476,6 +481,7 @@ export function flatPipeline(
476
481
  depthLayer,
477
482
  instanced,
478
483
  oit,
484
+ doubleSided,
479
485
  ),
480
486
  );
481
487
  }
@@ -571,6 +577,8 @@ function flatDescriptor(
571
577
  * `flatFrag`'s permutation axis, which `scripts/wgsl.ts` measures at about 247 KB gzipped.
572
578
  */
573
579
  oit: OitTarget = 'none',
580
+ /** Culls nothing: a surface seen from both faces. */
581
+ doubleSided = false,
574
582
  ): GPURenderPipelineDescriptor {
575
583
  const offset = depthOffsetForLayer(depthLayer);
576
584
  return {
@@ -612,7 +620,7 @@ function flatDescriptor(
612
620
  : { format: cache.format }),
613
621
  ],
614
622
  },
615
- primitive: { topology: 'triangle-list', cullMode: 'back' },
623
+ primitive: { topology: 'triangle-list', cullMode: doubleSided ? 'none' : 'back' },
616
624
  multisample: { count: cache.sampleCount },
617
625
  /*
618
626
  * `less`, matching the WebGL2 path's `LESS` rather than `LEQUAL`, and for the reason
@@ -118,6 +118,14 @@ export interface GpuParticles {
118
118
  readonly fragInts: Int32Array;
119
119
  readonly fields: UniformFields;
120
120
  readonly bindGroup: GPUBindGroup;
121
+ /**
122
+ * The encoder this batch was last drawn into, by the renderer's count, or -1.
123
+ *
124
+ * **A second draw into the same encoder is refused**, because this batch's buffers are written
125
+ * at each draw and every write lands before the encoder runs: both draws would show the second.
126
+ * WebGL2 has no such limit, and a limit one backend has is said, not left to a wrong picture.
127
+ */
128
+ drawnInEncoder: number;
121
129
  dispose(): void;
122
130
  }
123
131
 
@@ -215,6 +223,7 @@ export function createGpuParticles(
215
223
  const fragStaging = new ArrayBuffer(fragSize);
216
224
 
217
225
  return {
226
+ drawnInEncoder: -1,
218
227
  corners: cornerBuffer,
219
228
  instances,
220
229
  indexBuffer,