@driftengine/core 4.3.0 → 4.4.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 (215) hide show
  1. package/CHANGELOG.json +261 -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/backend/createRenderer.d.ts +13 -1
  12. package/dist/render/backend/createRenderer.js +6 -5
  13. package/dist/render/backend/webgl2/renderer.d.ts +147 -1
  14. package/dist/render/backend/webgl2/renderer.js +402 -23
  15. package/dist/render/backend/webgpu/buffers.js +46 -8
  16. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  17. package/dist/render/backend/webgpu/depthPass.js +75 -14
  18. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  19. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  20. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  21. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  22. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  23. package/dist/render/backend/webgpu/flatPass.js +12 -5
  24. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  25. package/dist/render/backend/webgpu/particlePass.js +1 -0
  26. package/dist/render/backend/webgpu/postPass.d.ts +12 -2
  27. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  28. package/dist/render/backend/webgpu/probeBake.js +97 -7
  29. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  30. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  31. package/dist/render/backend/webgpu/renderer.d.ts +174 -11
  32. package/dist/render/backend/webgpu/renderer.js +675 -249
  33. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  34. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  35. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  36. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  37. package/dist/render/backend/webgpu/skyPass.js +5 -1
  38. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  39. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  40. package/dist/render/bloomChain.d.ts +6 -0
  41. package/dist/render/bloomChain.js +8 -0
  42. package/dist/render/clusteredLights.d.ts +19 -11
  43. package/dist/render/clusteredLights.js +96 -45
  44. package/dist/render/cutoutCaster.d.ts +23 -0
  45. package/dist/render/cutoutCaster.js +20 -0
  46. package/dist/render/driftLight/bake.d.ts +34 -0
  47. package/dist/render/driftLight/bake.js +95 -0
  48. package/dist/render/driftLight/falloff.d.ts +16 -0
  49. package/dist/render/driftLight/falloff.js +23 -0
  50. package/dist/render/driftLight/layout.d.ts +59 -0
  51. package/dist/render/driftLight/layout.js +138 -0
  52. package/dist/render/driftLight/lightField.d.ts +108 -0
  53. package/dist/render/driftLight/lightField.js +179 -0
  54. package/dist/render/driftLight/uniforms.d.ts +28 -0
  55. package/dist/render/driftLight/uniforms.js +41 -0
  56. package/dist/render/driftLight/visibility.d.ts +18 -0
  57. package/dist/render/driftLight/visibility.js +47 -0
  58. package/dist/render/emptyTexture.d.ts +6 -0
  59. package/dist/render/emptyTexture.js +19 -0
  60. package/dist/render/exposurePass.d.ts +48 -0
  61. package/dist/render/exposurePass.js +172 -0
  62. package/dist/render/flameLight.d.ts +71 -0
  63. package/dist/render/flameLight.js +63 -0
  64. package/dist/render/gi/bounceLights.d.ts +34 -0
  65. package/dist/render/gi/bounceLights.js +66 -0
  66. package/dist/render/gi/fieldReach.d.ts +12 -0
  67. package/dist/render/gi/fieldReach.js +62 -0
  68. package/dist/render/gi/globalField.d.ts +8 -0
  69. package/dist/render/gi/globalField.js +31 -0
  70. package/dist/render/gi/probeTrace.d.ts +3 -1
  71. package/dist/render/gi/probeTrace.js +3 -1
  72. package/dist/render/gpuCapability.js +19 -4
  73. package/dist/render/lightBudget.d.ts +11 -0
  74. package/dist/render/lightBudget.js +11 -0
  75. package/dist/render/pointLightSelection.d.ts +16 -0
  76. package/dist/render/pointLightSelection.js +178 -83
  77. package/dist/render/probeGrid.d.ts +11 -1
  78. package/dist/render/probeGrid.js +5 -2
  79. package/dist/render/probeSweeps.d.ts +45 -0
  80. package/dist/render/probeSweeps.js +77 -0
  81. package/dist/render/reflectionProbe.d.ts +43 -1
  82. package/dist/render/reflectionProbe.js +17 -3
  83. package/dist/render/renderQuality.d.ts +3 -1
  84. package/dist/render/rendererHarness.d.ts +9 -0
  85. package/dist/render/rendererHarness.js +18 -3
  86. package/dist/render/sceneTarget.d.ts +20 -0
  87. package/dist/render/sceneTarget.js +62 -1
  88. package/dist/render/shaders/agx.d.ts +31 -0
  89. package/dist/render/shaders/agx.js +81 -0
  90. package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
  91. package/dist/render/shaders/ambientOcclusion.js +120 -63
  92. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  93. package/dist/render/shaders/cutoutCoverage.js +40 -0
  94. package/dist/render/shaders/depth.d.ts +25 -0
  95. package/dist/render/shaders/depth.js +69 -3
  96. package/dist/render/shaders/exposure.d.ts +52 -0
  97. package/dist/render/shaders/exposure.js +124 -0
  98. package/dist/render/shaders/filmLook.d.ts +47 -0
  99. package/dist/render/shaders/filmLook.js +98 -0
  100. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  101. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  102. package/dist/render/shaders/flat/driftLight.js +75 -0
  103. package/dist/render/shaders/flat/index.js +5 -0
  104. package/dist/render/shaders/flat/lobes.js +13 -0
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +68 -9
  107. package/dist/render/shaders/flat/preamble.js +23 -5
  108. package/dist/render/shaders/flat/probeGrid.js +46 -3
  109. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
  111. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  112. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  113. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  115. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  116. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  117. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  118. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  119. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  120. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  121. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  123. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  124. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  125. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  126. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  127. package/dist/render/shaders/globalMedium.js +4 -0
  128. package/dist/render/shaders/localExposure.d.ts +35 -0
  129. package/dist/render/shaders/localExposure.js +138 -0
  130. package/dist/render/shaders/prefilter.d.ts +1 -1
  131. package/dist/render/shaders/prefilter.js +6 -0
  132. package/dist/render/shaders/rush.js +33 -5
  133. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  134. package/dist/render/shaders/tangentFrame.js +15 -3
  135. package/dist/render/shaders/temporalResolve.js +39 -1
  136. package/dist/render/shadowCasters.d.ts +5 -3
  137. package/dist/render/shadowMap.js +13 -0
  138. package/dist/render/surfaceTexture.d.ts +5 -0
  139. package/package.json +6 -6
  140. package/scripts/gpuCompute.mjs +24 -3
  141. package/scripts/png.mjs +23 -12
  142. package/src/cinematic/cameraPath.ts +178 -0
  143. package/src/environment/daylight.ts +152 -0
  144. package/src/geometry/meshConcat.ts +100 -0
  145. package/src/index.ts +32 -0
  146. package/src/render/backend/createRenderer.ts +22 -6
  147. package/src/render/backend/webgl2/renderer.ts +439 -20
  148. package/src/render/backend/webgpu/buffers.ts +51 -8
  149. package/src/render/backend/webgpu/depthPass.ts +78 -10
  150. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  151. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  152. package/src/render/backend/webgpu/flatPass.ts +10 -2
  153. package/src/render/backend/webgpu/particlePass.ts +9 -0
  154. package/src/render/backend/webgpu/probeBake.ts +143 -9
  155. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  156. package/src/render/backend/webgpu/renderer.ts +801 -270
  157. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  158. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  159. package/src/render/backend/webgpu/skyPass.ts +5 -1
  160. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  161. package/src/render/bloomChain.ts +9 -0
  162. package/src/render/clusteredLights.ts +108 -63
  163. package/src/render/cutoutCaster.ts +44 -0
  164. package/src/render/driftLight/bake.ts +125 -0
  165. package/src/render/driftLight/falloff.ts +26 -0
  166. package/src/render/driftLight/layout.ts +177 -0
  167. package/src/render/driftLight/lightField.ts +227 -0
  168. package/src/render/driftLight/uniforms.ts +61 -0
  169. package/src/render/driftLight/visibility.ts +58 -0
  170. package/src/render/emptyTexture.ts +41 -0
  171. package/src/render/exposurePass.ts +186 -0
  172. package/src/render/flameLight.ts +135 -0
  173. package/src/render/gi/bounceLights.ts +68 -0
  174. package/src/render/gi/fieldReach.ts +74 -0
  175. package/src/render/gi/globalField.ts +39 -0
  176. package/src/render/gi/probeTrace.ts +3 -1
  177. package/src/render/gpuCapability.ts +18 -5
  178. package/src/render/lightBudget.ts +12 -0
  179. package/src/render/pointLightSelection.ts +191 -79
  180. package/src/render/probeGrid.ts +13 -2
  181. package/src/render/probeSweeps.ts +80 -0
  182. package/src/render/reflectionProbe.ts +54 -2
  183. package/src/render/renderQuality.ts +3 -1
  184. package/src/render/rendererHarness.ts +25 -3
  185. package/src/render/sceneTarget.ts +75 -1
  186. package/src/render/shaders/ambientOcclusion.ts +125 -63
  187. package/src/render/shaders/cutoutCoverage.ts +43 -0
  188. package/src/render/shaders/depth.ts +80 -3
  189. package/src/render/shaders/exposure.ts +128 -0
  190. package/src/render/shaders/filmLook.ts +111 -0
  191. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  192. package/src/render/shaders/flat/driftLight.ts +75 -0
  193. package/src/render/shaders/flat/index.ts +5 -0
  194. package/src/render/shaders/flat/lobes.ts +13 -0
  195. package/src/render/shaders/flat/main.ts +68 -9
  196. package/src/render/shaders/flat/preamble.ts +23 -5
  197. package/src/render/shaders/flat/probeGrid.ts +46 -3
  198. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
  199. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  200. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  201. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  202. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  203. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  204. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  205. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  206. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  207. package/src/render/shaders/globalMedium.ts +4 -0
  208. package/src/render/shaders/localExposure.ts +154 -0
  209. package/src/render/shaders/prefilter.ts +6 -0
  210. package/src/render/shaders/rush.ts +33 -5
  211. package/src/render/shaders/tangentFrame.ts +15 -3
  212. package/src/render/shaders/temporalResolve.ts +39 -1
  213. package/src/render/shadowCasters.ts +5 -3
  214. package/src/render/shadowMap.ts +14 -0
  215. package/src/render/surfaceTexture.ts +5 -0
@@ -1,5 +1,6 @@
1
1
  import { boundsOfPositions, createBounds } from '../../../math/bounds.js';
2
2
  import { MorphTexture } from './morphTexture.js';
3
+ import { validateMeshData } from '../../mesh.js';
3
4
  import { ABSENT_ATTRIBUTE } from '../../vertexDefaults.js';
4
5
  import { UPLOAD_BYTES_PER_STEP } from '../../uploadStep.js';
5
6
  import { INSTANCE_STRIDE } from '../../instances.js';
@@ -122,6 +123,11 @@ export function createGpuMesh(device, data, dynamic = false) {
122
123
  * bounds were not yet known would not be usable at once, which is the whole promise here.
123
124
  */
124
125
  export function createGpuMeshIncremental(device, data, dynamic = false) {
126
+ /*
127
+ * The other backend's refusal, and the premise of the interleave below: every attribute holds
128
+ * exactly a vertex's worth for every vertex, so nothing reads past the end of its array.
129
+ */
130
+ validateMeshData(data);
125
131
  const bounds = boundsOfPositions(data.positions, createBounds());
126
132
  const vertexCount = data.positions.length / 3;
127
133
  const supplied = VERTEX_LAYOUT.filter((attribute) => !attribute.optional || data[attribute.name] !== undefined);
@@ -264,14 +270,8 @@ function* uploadSteps(job) {
264
270
  let fieldOffset = 0;
265
271
  for (const attribute of supplied) {
266
272
  const source = data[attribute.name];
267
- const width = attribute.components;
268
- for (let vertex = from; vertex < to; vertex++) {
269
- for (let component = 0; component < width; component++) {
270
- interleaved[vertex * step + fieldOffset + component] =
271
- source[vertex * width + component] ?? 0;
272
- }
273
- }
274
- fieldOffset += width;
273
+ interleaveField(interleaved, source, attribute.components, step, fieldOffset, from, to);
274
+ fieldOffset += attribute.components;
275
275
  }
276
276
  device.queue.writeBuffer(vertices, from * stride, interleaved, from * step, (to - from) * step);
277
277
  taken += 1;
@@ -291,6 +291,44 @@ function* uploadSteps(job) {
291
291
  job.interleaved = null;
292
292
  job.progress.uploaded = true;
293
293
  }
294
+ /**
295
+ * One attribute of vertices `from` to `to` into its field of the interleaved rows.
296
+ *
297
+ * **Unrolled by width, because the general loop was most of a heavy load's upload.** A loop over
298
+ * components inside a loop over vertices, with the index recomputed from both, cost 1.5 s of main
299
+ * thread on a courtyard of five packs; a loop per width walking two cursors does the same writes in
300
+ * about a third of the time, measured on four million vertices in Node. Every width the layout
301
+ * carries is one, two, three or four, so the last arm is the one-float case and nothing else.
302
+ */
303
+ function interleaveField(rows, source, width, step, fieldOffset, from, to) {
304
+ let write = from * step + fieldOffset;
305
+ const end = to * width;
306
+ if (width === 3) {
307
+ for (let read = from * 3; read < end; read += 3, write += step) {
308
+ rows[write] = source[read];
309
+ rows[write + 1] = source[read + 1];
310
+ rows[write + 2] = source[read + 2];
311
+ }
312
+ }
313
+ else if (width === 4) {
314
+ for (let read = from * 4; read < end; read += 4, write += step) {
315
+ rows[write] = source[read];
316
+ rows[write + 1] = source[read + 1];
317
+ rows[write + 2] = source[read + 2];
318
+ rows[write + 3] = source[read + 3];
319
+ }
320
+ }
321
+ else if (width === 2) {
322
+ for (let read = from * 2; read < end; read += 2, write += step) {
323
+ rows[write] = source[read];
324
+ rows[write + 1] = source[read + 1];
325
+ }
326
+ }
327
+ else {
328
+ for (let read = from; read < end; read++, write += step)
329
+ rows[write] = source[read];
330
+ }
331
+ }
294
332
  /** The handle, made where it can see only what it hands out. */
295
333
  function gpuMeshOf(parts) {
296
334
  const { vertices, constants, indexBuffer, progress } = parts;
@@ -46,6 +46,71 @@ export declare const DEPTH_VERT_FIELDS: {
46
46
  };
47
47
  export declare const DEPTH_VERT_SIZE: 160;
48
48
  export declare const DEPTH_FRAG_SIZE: 16;
49
+ export declare const DEPTH_CUTOUT_VERT_FIELDS: {
50
+ readonly uLightViewProj: {
51
+ readonly offset: 0;
52
+ readonly size: 64;
53
+ readonly type: "mat4";
54
+ };
55
+ readonly uModel: {
56
+ readonly offset: 64;
57
+ readonly size: 64;
58
+ readonly type: "mat4";
59
+ };
60
+ readonly uWindDirection: {
61
+ readonly offset: 128;
62
+ readonly size: 8;
63
+ readonly type: "vec2";
64
+ };
65
+ readonly uWindSpeed: {
66
+ readonly offset: 136;
67
+ readonly size: 4;
68
+ readonly type: "float";
69
+ };
70
+ readonly uWindGust: {
71
+ readonly offset: 140;
72
+ readonly size: 4;
73
+ readonly type: "float";
74
+ };
75
+ readonly uWindTime: {
76
+ readonly offset: 144;
77
+ readonly size: 4;
78
+ readonly type: "float";
79
+ };
80
+ readonly uWindSpatialPhase: {
81
+ readonly offset: 152;
82
+ readonly size: 8;
83
+ readonly type: "vec2";
84
+ };
85
+ readonly uUvScale: {
86
+ readonly offset: 160;
87
+ readonly size: 8;
88
+ readonly type: "vec2";
89
+ };
90
+ readonly uAlphaCutout: {
91
+ readonly offset: 168;
92
+ readonly size: 4;
93
+ readonly type: "float";
94
+ };
95
+ };
96
+ export declare const DEPTH_CUTOUT_VERT_SIZE: 176;
97
+ export declare const DEPTH_INSTANCED_CUTOUT_VERT_FIELDS: {
98
+ readonly uLightViewProj: {
99
+ readonly offset: 0;
100
+ readonly size: 64;
101
+ readonly type: "mat4";
102
+ };
103
+ readonly uUvScale: {
104
+ readonly offset: 64;
105
+ readonly size: 8;
106
+ readonly type: "vec2";
107
+ };
108
+ readonly uAlphaCutout: {
109
+ readonly offset: 72;
110
+ readonly size: 4;
111
+ readonly type: "float";
112
+ };
113
+ };
49
114
  /** Re-exported so the scatter depth pass can pair with the same fragment stage. */
50
115
  export { DEPTH_FRAG_WGSL };
51
116
  export declare const DEPTH_FRAG_BINDING: 1;
@@ -78,10 +143,21 @@ export declare function createDepthBindGroupLayout(device: GPUDevice,
78
143
  * the rigid path is untouched: no extra binding, no one-pixel texture to keep alive, and a
79
144
  * frame that skins nothing pays nothing at all.
80
145
  */
81
- skinned?: boolean): GPUBindGroupLayout;
146
+ skinned?: boolean,
147
+ /**
148
+ * Whether this layout carries a cutout map, for a caster whose shape is in its texture's alpha.
149
+ * Separate for the palette's reason: the plain layout stays exactly what it was, and a frame with
150
+ * no cutout caster pays nothing.
151
+ */
152
+ cutout?: boolean): GPUBindGroupLayout;
82
153
  export declare function createDepthBindGroup(device: GPUDevice, layout: GPUBindGroupLayout, perDraw: GPUBuffer, perPass: GPUBuffer, previous: GPUTextureView, sampler: GPUSampler,
83
154
  /** The joint palette, for a layout built with `skinned`. Omit for the rigid layout. */
84
- palette?: GPUTextureView | null): GPUBindGroup;
155
+ palette?: GPUTextureView | null,
156
+ /** The cutout map and its sampler, for a layout built with `cutout`. */
157
+ cutout?: {
158
+ readonly view: GPUTextureView;
159
+ readonly sampler: GPUSampler;
160
+ } | null): GPUBindGroup;
85
161
  /**
86
162
  * The depth pipeline.
87
163
  *
@@ -111,4 +187,6 @@ skinned?: boolean,
111
187
  * Excludes `skinned`, and the layout is why rather than the shader: an instanced pipeline
112
188
  * reclaims the two attribute locations the joint indices and weights occupy.
113
189
  */
114
- instanced?: boolean): GPURenderPipeline;
190
+ instanced?: boolean,
191
+ /** Whether this pipeline casts a cutout: the variant that reads a map and discards by it. */
192
+ cutout?: boolean): GPURenderPipeline;
@@ -1,4 +1,4 @@
1
- import { DEPTH_BINDINGS, DEPTH_FRAG_WGSL, DEPTH_INSTANCED_VERT_WGSL, DEPTH_SKINNED_VERT_WGSL, DEPTH_VERT_WGSL, } from '../../shaders/generated/depth.wgsl.js';
1
+ import { DEPTH_BINDINGS, DEPTH_CUTOUT_FRAG_WGSL, DEPTH_CUTOUT_VERT_WGSL, DEPTH_FRAG_WGSL, DEPTH_INSTANCED_CUTOUT_VERT_WGSL, DEPTH_INSTANCED_VERT_WGSL, DEPTH_SKINNED_VERT_WGSL, DEPTH_VERT_WGSL, } from '../../shaders/generated/depth.wgsl.js';
2
2
  import { vertexBufferLayouts } from './buffers.js';
3
3
  import { shaderModule } from './shaderModules.js';
4
4
  /**
@@ -11,6 +11,15 @@ import { shaderModule } from './shaderModules.js';
11
11
  export const DEPTH_VERT_FIELDS = DEPTH_BINDINGS.DEPTH_VERT.fields;
12
12
  export const DEPTH_VERT_SIZE = DEPTH_BINDINGS.DEPTH_VERT.uniformSize;
13
13
  export const DEPTH_FRAG_SIZE = DEPTH_BINDINGS.DEPTH_FRAG.uniformSize;
14
+ /*
15
+ * The cutout variants' vertex blocks. The rigid one is the plain block with `uUvScale` and
16
+ * `uAlphaCutout` after every other field, so a slot written for it is written with the plain
17
+ * offsets plus these two; the instanced one has no model and no wind, so its two sit earlier.
18
+ */
19
+ export const DEPTH_CUTOUT_VERT_FIELDS = DEPTH_BINDINGS.DEPTH_CUTOUT_VERT.fields;
20
+ export const DEPTH_CUTOUT_VERT_SIZE = DEPTH_BINDINGS.DEPTH_CUTOUT_VERT.uniformSize;
21
+ export const DEPTH_INSTANCED_CUTOUT_VERT_FIELDS = DEPTH_BINDINGS.DEPTH_INSTANCED_CUTOUT_VERT.fields;
22
+ const CUTOUT_MAP = DEPTH_BINDINGS.DEPTH_CUTOUT_FRAG.textures.uCutoutMap;
14
23
  /** Re-exported so the scatter depth pass can pair with the same fragment stage. */
15
24
  export { DEPTH_FRAG_WGSL };
16
25
  export const DEPTH_FRAG_BINDING = DEPTH_BINDINGS.DEPTH_FRAG.uniforms;
@@ -40,9 +49,15 @@ export function createDepthBindGroupLayout(device,
40
49
  * the rigid path is untouched: no extra binding, no one-pixel texture to keep alive, and a
41
50
  * frame that skins nothing pays nothing at all.
42
51
  */
43
- skinned = false) {
52
+ skinned = false,
53
+ /**
54
+ * Whether this layout carries a cutout map, for a caster whose shape is in its texture's alpha.
55
+ * Separate for the palette's reason: the plain layout stays exactly what it was, and a frame with
56
+ * no cutout caster pays nothing.
57
+ */
58
+ cutout = false) {
44
59
  return device.createBindGroupLayout({
45
- label: skinned ? 'depth.layout.skinned' : 'depth.layout',
60
+ label: skinned ? 'depth.layout.skinned' : cutout ? 'depth.layout.cutout' : 'depth.layout',
46
61
  entries: [
47
62
  /* The texture and no sampler: the shader reads the palette with `textureLoad`, an integer
48
63
  coordinate with nothing to filter, which is what `flatPass.ts` binds for the same
@@ -59,7 +74,11 @@ skinned = false) {
59
74
  {
60
75
  binding: VERT_BINDING,
61
76
  visibility: VISIBILITY_VERTEX,
62
- buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: DEPTH_VERT_SIZE },
77
+ buffer: {
78
+ type: 'uniform',
79
+ hasDynamicOffset: true,
80
+ minBindingSize: cutout ? DEPTH_CUTOUT_VERT_SIZE : DEPTH_VERT_SIZE,
81
+ },
63
82
  },
64
83
  {
65
84
  binding: FRAG_BINDING,
@@ -92,21 +111,55 @@ skinned = false) {
92
111
  visibility: VISIBILITY_FRAGMENT,
93
112
  sampler: { type: 'non-filtering' },
94
113
  },
114
+ /* A colour map sampled with its own filter and mips, so a leaf's edge in the map is the edge
115
+ a colour pass at that distance would draw. */
116
+ ...(cutout
117
+ ? [
118
+ {
119
+ binding: CUTOUT_MAP.texture,
120
+ visibility: VISIBILITY_FRAGMENT,
121
+ texture: { sampleType: 'float' },
122
+ },
123
+ {
124
+ binding: CUTOUT_MAP.sampler,
125
+ visibility: VISIBILITY_FRAGMENT,
126
+ sampler: { type: 'filtering' },
127
+ },
128
+ ]
129
+ : []),
95
130
  ],
96
131
  });
97
132
  }
98
133
  export function createDepthBindGroup(device, layout, perDraw, perPass, previous, sampler,
99
134
  /** The joint palette, for a layout built with `skinned`. Omit for the rigid layout. */
100
- palette = null) {
135
+ palette = null,
136
+ /** The cutout map and its sampler, for a layout built with `cutout`. */
137
+ cutout = null) {
101
138
  return device.createBindGroup({
102
- label: palette === null ? 'depth.bindGroup' : 'depth.bindGroup.skinned',
139
+ label: palette !== null
140
+ ? 'depth.bindGroup.skinned'
141
+ : cutout !== null
142
+ ? 'depth.bindGroup.cutout'
143
+ : 'depth.bindGroup',
103
144
  layout,
104
145
  entries: [
105
- { binding: VERT_BINDING, resource: { buffer: perDraw, size: DEPTH_VERT_SIZE } },
146
+ {
147
+ binding: VERT_BINDING,
148
+ resource: {
149
+ buffer: perDraw,
150
+ size: cutout !== null ? DEPTH_CUTOUT_VERT_SIZE : DEPTH_VERT_SIZE,
151
+ },
152
+ },
106
153
  { binding: FRAG_BINDING, resource: { buffer: perPass, size: DEPTH_FRAG_SIZE } },
107
154
  { binding: PREVIOUS.texture, resource: previous },
108
155
  { binding: PREVIOUS.sampler, resource: sampler },
109
156
  ...(palette !== null ? [{ binding: DEPTH_PALETTE_BINDING.texture, resource: palette }] : []),
157
+ ...(cutout !== null
158
+ ? [
159
+ { binding: CUTOUT_MAP.texture, resource: cutout.view },
160
+ { binding: CUTOUT_MAP.sampler, resource: cutout.sampler },
161
+ ]
162
+ : []),
110
163
  ],
111
164
  });
112
165
  }
@@ -139,21 +192,29 @@ skinned = false,
139
192
  * Excludes `skinned`, and the layout is why rather than the shader: an instanced pipeline
140
193
  * reclaims the two attribute locations the joint indices and weights occupy.
141
194
  */
142
- instanced = false) {
195
+ instanced = false,
196
+ /** Whether this pipeline casts a cutout: the variant that reads a map and discards by it. */
197
+ cutout = false) {
143
198
  return cache.get(key, () => ({
144
199
  label: key,
145
200
  layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
146
201
  vertex: {
147
- module: shaderModule(device, instanced
148
- ? { label: 'depth.instanced.vert', code: DEPTH_INSTANCED_VERT_WGSL }
149
- : skinned
150
- ? { label: 'depth.skinned.vert', code: DEPTH_SKINNED_VERT_WGSL }
151
- : { label: 'depth.vert', code: DEPTH_VERT_WGSL }),
202
+ module: shaderModule(device, cutout
203
+ ? instanced
204
+ ? { label: 'depth.instanced.cutout.vert', code: DEPTH_INSTANCED_CUTOUT_VERT_WGSL }
205
+ : { label: 'depth.cutout.vert', code: DEPTH_CUTOUT_VERT_WGSL }
206
+ : instanced
207
+ ? { label: 'depth.instanced.vert', code: DEPTH_INSTANCED_VERT_WGSL }
208
+ : skinned
209
+ ? { label: 'depth.skinned.vert', code: DEPTH_SKINNED_VERT_WGSL }
210
+ : { label: 'depth.vert', code: DEPTH_VERT_WGSL }),
152
211
  entryPoint: 'main',
153
212
  buffers: vertexBufferLayouts(present, instanced),
154
213
  },
155
214
  fragment: {
156
- module: shaderModule(device, { label: 'depth.frag', code: DEPTH_FRAG_WGSL }),
215
+ module: shaderModule(device, cutout
216
+ ? { label: 'depth.cutout.frag', code: DEPTH_CUTOUT_FRAG_WGSL }
217
+ : { label: 'depth.frag', code: DEPTH_FRAG_WGSL }),
157
218
  entryPoint: 'main',
158
219
  targets: [],
159
220
  },
@@ -0,0 +1,39 @@
1
+ import type { PipelineCache } from './pipelineCache.ts';
2
+ export declare class ExposurePass {
3
+ private readonly device;
4
+ private readonly pipelines;
5
+ private readonly sampler;
6
+ private readonly meter;
7
+ private readonly meterView;
8
+ private readonly held;
9
+ /** The texel the composite binds, for its bind group. */
10
+ readonly heldView: GPUTextureView;
11
+ private readonly scratch;
12
+ private readonly scratchView;
13
+ private readonly localGrid;
14
+ /** Local exposure's grid, for the composite's bind group. */
15
+ readonly localView: GPUTextureView;
16
+ private readonly localLayout;
17
+ private localGroup;
18
+ private localSource;
19
+ private readonly meterLayout;
20
+ private readonly adaptLayout;
21
+ private readonly adaptGroup;
22
+ private readonly uniforms;
23
+ private readonly blend;
24
+ private meterGroup;
25
+ private meterSource;
26
+ /** Whether anything is held yet: the first frame, and the first after a cut, snaps. */
27
+ private holding;
28
+ constructor(device: GPUDevice, pipelines: PipelineCache, sampler: GPUSampler);
29
+ /** Forget what is held, so the next frame meters afresh rather than easing from another shot. */
30
+ cut(): void;
31
+ /**
32
+ * Meter `scene` and move the held brightness toward it by `dtSec` of adaptation, and with `local`
33
+ * draw local exposure's grid too.
34
+ */
35
+ run(encoder: GPUCommandEncoder, scene: GPUTextureView, dtSec: number, local?: boolean): void;
36
+ /** Local exposure's grid, from the same scene the meter read. */
37
+ private drawLocal;
38
+ dispose(): void;
39
+ }
@@ -0,0 +1,190 @@
1
+ import { EXPOSURE_ADAPT_FRAG_WGSL, EXPOSURE_BINDINGS, EXPOSURE_METER_FRAG_WGSL, } from '../../shaders/generated/exposure.wgsl.js';
2
+ import { EXPOSURE_LOCAL_FRAG_WGSL, LOCALEXPOSURE_BINDINGS, } from '../../shaders/generated/localExposure.wgsl.js';
3
+ import { METER_GRID, adaptBlend } from '../../shaders/exposure.js';
4
+ import { LOCAL_GRID_WIDTH } from '../../shaders/localExposure.js';
5
+ import { createPostStageLayout, postPipeline } from './postPass.js';
6
+ /**
7
+ * Eye adaptation on WebGPU: meter the finished scene into the frame's own encoder, and follow it
8
+ * with a held brightness the composite reads. See `shaders/exposure.ts` for the design.
9
+ *
10
+ * **One held texel the composite always binds, and a scratch the adaptation writes.** Reading and
11
+ * writing one texture in a pass is refused, and a pair that swapped roles would mean a composite
12
+ * bind group per role; so the adaptation writes the scratch from the held value and a copy puts it
13
+ * back, on the same encoder, before the composite reads it. Recorded into the frame's encoder
14
+ * rather than submitted apart, because a separate submission would run before the frame that
15
+ * draws the scene it is meant to measure.
16
+ *
17
+ * **And local exposure's grid, when that is asked for**, in the same encoder after the meter:
18
+ * `rg16float`, a sum and a count per tile per band, sampled filtered by the composite. See
19
+ * `localExposure.ts`.
20
+ */
21
+ const METER = EXPOSURE_BINDINGS.EXPOSURE_METER_FRAG.textures;
22
+ const ADAPT = EXPOSURE_BINDINGS.EXPOSURE_ADAPT_FRAG;
23
+ const LOCAL = LOCALEXPOSURE_BINDINGS.EXPOSURE_LOCAL_FRAG.textures;
24
+ const USAGE_RENDER = 0x10;
25
+ const USAGE_TEXTURE = 0x04;
26
+ const USAGE_COPY_SRC = 0x01;
27
+ const USAGE_COPY_DST = 0x02;
28
+ const USAGE_UNIFORM_DST = 0x40 | 0x08;
29
+ const VISIBILITY_FRAGMENT = 0x2;
30
+ export class ExposurePass {
31
+ device;
32
+ pipelines;
33
+ sampler;
34
+ meter;
35
+ meterView;
36
+ held;
37
+ /** The texel the composite binds, for its bind group. */
38
+ heldView;
39
+ scratch;
40
+ scratchView;
41
+ localGrid;
42
+ /** Local exposure's grid, for the composite's bind group. */
43
+ localView;
44
+ localLayout;
45
+ localGroup = null;
46
+ localSource = null;
47
+ meterLayout;
48
+ adaptLayout;
49
+ adaptGroup;
50
+ uniforms;
51
+ blend = new Float32Array(4);
52
+ meterGroup = null;
53
+ meterSource = null;
54
+ /** Whether anything is held yet: the first frame, and the first after a cut, snaps. */
55
+ holding = false;
56
+ constructor(device, pipelines, sampler) {
57
+ this.device = device;
58
+ this.pipelines = pipelines;
59
+ this.sampler = sampler;
60
+ const target = (label, size, usage) => device.createTexture({ label, size: [size, size], format: 'r16float', usage });
61
+ this.meter = target('exposure.meter', METER_GRID, USAGE_RENDER | USAGE_TEXTURE);
62
+ this.held = target('exposure.held', 1, USAGE_RENDER | USAGE_TEXTURE | USAGE_COPY_DST);
63
+ this.scratch = target('exposure.scratch', 1, USAGE_RENDER | USAGE_COPY_SRC);
64
+ this.localGrid = device.createTexture({
65
+ label: 'exposure.localGrid',
66
+ size: [LOCAL_GRID_WIDTH, METER_GRID],
67
+ format: 'rg16float',
68
+ usage: USAGE_RENDER | USAGE_TEXTURE,
69
+ });
70
+ this.localView = this.localGrid.createView();
71
+ this.meterView = this.meter.createView();
72
+ this.heldView = this.held.createView();
73
+ this.scratchView = this.scratch.createView();
74
+ /* The meter has no uniforms, so its layout is its one texture and sampler and nothing else. */
75
+ this.meterLayout = device.createBindGroupLayout({
76
+ label: 'exposure.meterLayout',
77
+ entries: [
78
+ { binding: METER.uScene.texture, visibility: VISIBILITY_FRAGMENT, texture: {} },
79
+ { binding: METER.uScene.sampler, visibility: VISIBILITY_FRAGMENT, sampler: {} },
80
+ ],
81
+ });
82
+ this.localLayout = device.createBindGroupLayout({
83
+ label: 'exposure.localLayout',
84
+ entries: [
85
+ { binding: LOCAL.uScene.texture, visibility: VISIBILITY_FRAGMENT, texture: {} },
86
+ { binding: LOCAL.uScene.sampler, visibility: VISIBILITY_FRAGMENT, sampler: {} },
87
+ ],
88
+ });
89
+ this.adaptLayout = createPostStageLayout(device, ADAPT.uniforms, ADAPT.uniformSize, [
90
+ { binding: ADAPT.textures.uMeter },
91
+ { binding: ADAPT.textures.uHeld },
92
+ ]);
93
+ this.uniforms = device.createBuffer({
94
+ label: 'exposure.uniforms',
95
+ size: ADAPT.uniformSize,
96
+ usage: USAGE_UNIFORM_DST,
97
+ });
98
+ this.adaptGroup = device.createBindGroup({
99
+ label: 'exposure.adaptGroup',
100
+ layout: this.adaptLayout,
101
+ entries: [
102
+ { binding: ADAPT.uniforms, resource: { buffer: this.uniforms, size: ADAPT.uniformSize } },
103
+ { binding: ADAPT.textures.uMeter.texture, resource: this.meterView },
104
+ { binding: ADAPT.textures.uMeter.sampler, resource: sampler },
105
+ { binding: ADAPT.textures.uHeld.texture, resource: this.heldView },
106
+ { binding: ADAPT.textures.uHeld.sampler, resource: sampler },
107
+ ],
108
+ });
109
+ }
110
+ /** Forget what is held, so the next frame meters afresh rather than easing from another shot. */
111
+ cut() {
112
+ this.holding = false;
113
+ }
114
+ /**
115
+ * Meter `scene` and move the held brightness toward it by `dtSec` of adaptation, and with `local`
116
+ * draw local exposure's grid too.
117
+ */
118
+ run(encoder, scene, dtSec, local = false) {
119
+ const { device } = this;
120
+ if (this.meterSource !== scene || this.meterGroup === null) {
121
+ this.meterGroup = device.createBindGroup({
122
+ label: 'exposure.meterGroup',
123
+ layout: this.meterLayout,
124
+ entries: [
125
+ { binding: METER.uScene.texture, resource: scene },
126
+ { binding: METER.uScene.sampler, resource: this.sampler },
127
+ ],
128
+ });
129
+ this.meterSource = scene;
130
+ }
131
+ this.blend[0] = adaptBlend(dtSec, !this.holding);
132
+ device.queue.writeBuffer(this.uniforms, 0, this.blend);
133
+ const meter = encoder.beginRenderPass({
134
+ label: 'exposure.meter',
135
+ colorAttachments: [
136
+ { view: this.meterView, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
137
+ ],
138
+ });
139
+ meter.setPipeline(postPipeline(this.pipelines, device, this.meterLayout, 'exposure.meter', EXPOSURE_METER_FRAG_WGSL, 'r16float'));
140
+ meter.setBindGroup(0, this.meterGroup);
141
+ meter.draw(3);
142
+ meter.end();
143
+ if (local)
144
+ this.drawLocal(encoder, scene);
145
+ const adapt = encoder.beginRenderPass({
146
+ label: 'exposure.adapt',
147
+ colorAttachments: [
148
+ { view: this.scratchView, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
149
+ ],
150
+ });
151
+ adapt.setPipeline(postPipeline(this.pipelines, device, this.adaptLayout, 'exposure.adapt', EXPOSURE_ADAPT_FRAG_WGSL, 'r16float'));
152
+ adapt.setBindGroup(0, this.adaptGroup, [0]);
153
+ adapt.draw(3);
154
+ adapt.end();
155
+ encoder.copyTextureToTexture({ texture: this.scratch }, { texture: this.held }, [1, 1, 1]);
156
+ this.holding = true;
157
+ }
158
+ /** Local exposure's grid, from the same scene the meter read. */
159
+ drawLocal(encoder, scene) {
160
+ const { device } = this;
161
+ if (this.localSource !== scene || this.localGroup === null) {
162
+ this.localGroup = device.createBindGroup({
163
+ label: 'exposure.localGroup',
164
+ layout: this.localLayout,
165
+ entries: [
166
+ { binding: LOCAL.uScene.texture, resource: scene },
167
+ { binding: LOCAL.uScene.sampler, resource: this.sampler },
168
+ ],
169
+ });
170
+ this.localSource = scene;
171
+ }
172
+ const pass = encoder.beginRenderPass({
173
+ label: 'exposure.local',
174
+ colorAttachments: [
175
+ { view: this.localView, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
176
+ ],
177
+ });
178
+ pass.setPipeline(postPipeline(this.pipelines, device, this.localLayout, 'exposure.local', EXPOSURE_LOCAL_FRAG_WGSL, 'rg16float'));
179
+ pass.setBindGroup(0, this.localGroup);
180
+ pass.draw(3);
181
+ pass.end();
182
+ }
183
+ dispose() {
184
+ this.localGrid.destroy();
185
+ this.meter.destroy();
186
+ this.held.destroy();
187
+ this.scratch.destroy();
188
+ this.uniforms.destroy();
189
+ }
190
+ }
@@ -39,6 +39,15 @@ export type ComposedField = PassDistanceField;
39
39
  * which the chain's probe level answers and never declines.
40
40
  */
41
41
  export declare const DEFAULT_FIELD_COMPOSE: FieldComposeOptions;
42
+ /**
43
+ * The most cascades a field is given to reach a probe grid, which is `cascadesForGrid`'s ceiling.
44
+ *
45
+ * Six from the 4 m default reach 128 m, and each is the same 49 cubed samples, about half a
46
+ * megabyte, so the bound is on memory rather than on distance. What would make it wrong is a scene
47
+ * whose probe grid is longer than about a hundred metres, whose far probes are then left untraced
48
+ * rather than traced against a field that is not there.
49
+ */
50
+ export declare const MAX_FIELD_CASCADES = 6;
42
51
  export interface FieldComposeOptions {
43
52
  /** Samples a side, every cascade. */
44
53
  readonly resolution: number;
@@ -106,6 +115,8 @@ export declare class FieldComposer {
106
115
  private queryMapping;
107
116
  private timed;
108
117
  private composeMsValue;
118
+ /** How many cascades this composer was built with. */
119
+ get cascadeCount(): number;
109
120
  constructor(device: GPUDevice, options: FieldComposeOptions);
110
121
  /** One bind group a cascade. Rebuilt whenever the source buffer is replaced by a larger one. */
111
122
  private rebuildGroups;
@@ -41,6 +41,15 @@ export const DEFAULT_FIELD_COMPOSE = {
41
41
  cascades: 3,
42
42
  radius: 4,
43
43
  };
44
+ /**
45
+ * The most cascades a field is given to reach a probe grid, which is `cascadesForGrid`'s ceiling.
46
+ *
47
+ * Six from the 4 m default reach 128 m, and each is the same 49 cubed samples, about half a
48
+ * megabyte, so the bound is on memory rather than on distance. What would make it wrong is a scene
49
+ * whose probe grid is longer than about a hundred metres, whose far probes are then left untraced
50
+ * rather than traced against a field that is not there.
51
+ */
52
+ export const MAX_FIELD_CASCADES = 6;
44
53
  export class FieldComposer {
45
54
  options;
46
55
  samples;
@@ -101,6 +110,10 @@ export class FieldComposer {
101
110
  queryMapping = false;
102
111
  timed = false;
103
112
  composeMsValue = 0;
113
+ /** How many cascades this composer was built with. */
114
+ get cascadeCount() {
115
+ return this.options.cascades;
116
+ }
104
117
  constructor(device, options) {
105
118
  if (!(Math.trunc(options.resolution) >= 2)) {
106
119
  throw new Error(`FieldComposer: resolution is ${String(options.resolution)}; the minimum is 2.`);
@@ -194,7 +194,9 @@ depthWrite?: boolean, depthLayer?: number,
194
194
  /** Whether placement and tint arrive per instance. See `FlatVertexOptions`. */
195
195
  instanced?: boolean,
196
196
  /** Which order-independent buffer this pipeline writes, if any. The key must carry it. */
197
- oit?: OitTarget): GPURenderPipeline;
197
+ oit?: OitTarget,
198
+ /** Culls nothing, for a surface seen from both faces. The key must carry it: `|2s`. */
199
+ doubleSided?: boolean): GPURenderPipeline;
198
200
  /**
199
201
  * The same pipeline, compiled off the main thread.
200
202
  *