@driftengine/core 4.10.0 → 4.11.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 (165) hide show
  1. package/CHANGELOG.json +107 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +6 -5
  4. package/dist/index.js +2 -2
  5. package/dist/render/ambientOcclusionPass.d.ts +3 -2
  6. package/dist/render/ambientOcclusionPass.js +4 -2
  7. package/dist/render/backend/api.d.ts +21 -1
  8. package/dist/render/backend/webgl2/renderer.d.ts +141 -3
  9. package/dist/render/backend/webgl2/renderer.js +454 -42
  10. package/dist/render/backend/webgpu/buffers.d.ts +5 -0
  11. package/dist/render/backend/webgpu/buffers.js +98 -48
  12. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  13. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  14. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  15. package/dist/render/backend/webgpu/clothOps.js +78 -0
  16. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  17. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  18. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  19. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  20. package/dist/render/backend/webgpu/drawCommand.d.ts +2 -0
  21. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  22. package/dist/render/backend/webgpu/flatPass.d.ts +1 -1
  23. package/dist/render/backend/webgpu/flatPass.js +2 -1
  24. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  25. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  26. package/dist/render/backend/webgpu/latePass.js +1 -0
  27. package/dist/render/backend/webgpu/motionPass.js +15 -6
  28. package/dist/render/backend/webgpu/pipelineCache.d.ts +22 -1
  29. package/dist/render/backend/webgpu/pipelineCache.js +39 -1
  30. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  31. package/dist/render/backend/webgpu/renderer.d.ts +100 -1
  32. package/dist/render/backend/webgpu/renderer.js +423 -95
  33. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  34. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  35. package/dist/render/backend/webgpu/staticBundles.js +2 -1
  36. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  37. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  38. package/dist/render/globalMediumPass.d.ts +2 -0
  39. package/dist/render/globalMediumPass.js +1 -0
  40. package/dist/render/indexWidth.d.ts +17 -0
  41. package/dist/render/indexWidth.js +23 -0
  42. package/dist/render/instances.d.ts +13 -1
  43. package/dist/render/instances.js +10 -0
  44. package/dist/render/lightMatrix.d.ts +11 -0
  45. package/dist/render/lightMatrix.js +14 -0
  46. package/dist/render/mesh.d.ts +9 -0
  47. package/dist/render/mesh.js +38 -14
  48. package/dist/render/occlusionFade.d.ts +21 -0
  49. package/dist/render/occlusionFade.js +27 -0
  50. package/dist/render/renderQuality.d.ts +30 -0
  51. package/dist/render/renderQuality.js +5 -0
  52. package/dist/render/rendererHarness.d.ts +7 -0
  53. package/dist/render/rendererHarness.js +12 -0
  54. package/dist/render/sceneTarget.d.ts +2 -0
  55. package/dist/render/sceneTarget.js +1 -1
  56. package/dist/render/shader.d.ts +15 -0
  57. package/dist/render/shader.js +41 -0
  58. package/dist/render/shaders/ambientOcclusion.js +12 -1
  59. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  60. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  61. package/dist/render/shaders/flat/glassTint.js +3 -0
  62. package/dist/render/shaders/flat/index.d.ts +15 -0
  63. package/dist/render/shaders/flat/index.js +24 -4
  64. package/dist/render/shaders/flat/layered.d.ts +27 -0
  65. package/dist/render/shaders/flat/layered.js +198 -0
  66. package/dist/render/shaders/flat/main.d.ts +1 -1
  67. package/dist/render/shaders/flat/main.js +50 -16
  68. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  69. package/dist/render/shaders/flat/mapReads.js +38 -0
  70. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  71. package/dist/render/shaders/flat/movingSun.js +46 -0
  72. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  73. package/dist/render/shaders/flat/normalMap.js +23 -3
  74. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  75. package/dist/render/shaders/flat/overlay.js +41 -19
  76. package/dist/render/shaders/flat/preamble.js +23 -5
  77. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  78. package/dist/render/shaders/flat/worldUv.js +113 -0
  79. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
  80. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
  81. package/dist/render/shaders/generated/flat.wgsl.d.ts +768 -368
  82. package/dist/render/shaders/generated/flat.wgsl.js +791 -391
  83. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  84. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  85. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  86. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  87. package/dist/render/shaders/globalMedium.js +9 -1
  88. package/dist/render/shaders/lightVolume.js +9 -1
  89. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  90. package/dist/render/shaders/tangentFrame.js +16 -3
  91. package/dist/render/skinnedClothRun.d.ts +28 -0
  92. package/dist/render/skinnedClothRun.js +103 -35
  93. package/dist/render/surfaceLayers.d.ts +107 -0
  94. package/dist/render/surfaceLayers.js +82 -0
  95. package/dist/render/surfaceOverlay.d.ts +16 -3
  96. package/dist/render/surfaceOverlay.js +11 -6
  97. package/dist/render/surfaceProjection.d.ts +34 -0
  98. package/dist/render/surfaceProjection.js +30 -0
  99. package/dist/render/surfaceTexture.d.ts +39 -0
  100. package/dist/render/transmission.d.ts +21 -0
  101. package/dist/render/transmission.js +21 -0
  102. package/dist/render/vertexPacking.d.ts +48 -0
  103. package/dist/render/vertexPacking.js +68 -0
  104. package/dist/ui/splash.d.ts +42 -2
  105. package/dist/ui/splash.js +78 -13
  106. package/package.json +6 -6
  107. package/src/index.ts +6 -2
  108. package/src/render/ambientOcclusionPass.ts +4 -1
  109. package/src/render/backend/api.ts +31 -0
  110. package/src/render/backend/webgl2/renderer.ts +519 -56
  111. package/src/render/backend/webgpu/buffers.ts +153 -54
  112. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  113. package/src/render/backend/webgpu/clothOps.ts +86 -0
  114. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  115. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  116. package/src/render/backend/webgpu/drawCommand.ts +3 -0
  117. package/src/render/backend/webgpu/flatPass.ts +26 -28
  118. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  119. package/src/render/backend/webgpu/latePass.ts +1 -0
  120. package/src/render/backend/webgpu/motionPass.ts +18 -7
  121. package/src/render/backend/webgpu/pipelineCache.ts +46 -7
  122. package/src/render/backend/webgpu/renderer.ts +622 -164
  123. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  124. package/src/render/backend/webgpu/staticBundles.ts +2 -1
  125. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  126. package/src/render/globalMediumPass.ts +7 -0
  127. package/src/render/indexWidth.ts +27 -0
  128. package/src/render/instances.ts +18 -1
  129. package/src/render/lightMatrix.ts +15 -0
  130. package/src/render/mesh.ts +66 -14
  131. package/src/render/occlusionFade.ts +29 -0
  132. package/src/render/renderQuality.ts +38 -0
  133. package/src/render/rendererHarness.ts +15 -0
  134. package/src/render/sceneTarget.ts +3 -0
  135. package/src/render/shader.ts +59 -0
  136. package/src/render/shaders/ambientOcclusion.ts +12 -1
  137. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  138. package/src/render/shaders/flat/glassTint.ts +7 -1
  139. package/src/render/shaders/flat/index.ts +39 -4
  140. package/src/render/shaders/flat/layered.ts +198 -0
  141. package/src/render/shaders/flat/main.ts +50 -16
  142. package/src/render/shaders/flat/mapReads.ts +38 -0
  143. package/src/render/shaders/flat/movingSun.ts +46 -0
  144. package/src/render/shaders/flat/normalMap.ts +23 -3
  145. package/src/render/shaders/flat/overlay.ts +41 -19
  146. package/src/render/shaders/flat/preamble.ts +23 -5
  147. package/src/render/shaders/flat/worldUv.ts +113 -0
  148. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
  149. package/src/render/shaders/generated/flat.wgsl.ts +791 -391
  150. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  151. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  152. package/src/render/shaders/globalMedium.ts +9 -1
  153. package/src/render/shaders/lightVolume.ts +9 -1
  154. package/src/render/shaders/tangentFrame.ts +16 -3
  155. package/src/render/skinnedClothRun.ts +138 -38
  156. package/src/render/surfaceLayers.ts +176 -0
  157. package/src/render/surfaceOverlay.ts +24 -7
  158. package/src/render/surfaceProjection.ts +57 -0
  159. package/src/render/surfaceTexture.ts +39 -0
  160. package/src/render/transmission.ts +27 -0
  161. package/src/render/vertexPacking.ts +109 -0
  162. package/src/ui/splash.ts +83 -14
  163. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  164. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  165. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -46,9 +46,16 @@ export interface MeshInstances {
46
46
  * read by a draw whose material is a `lightmapModel` — instances of one mesh in one material sit
47
47
  * in different regions of one page, each applied before the material's own region. **They ride
48
48
  * the tint and the opacity**, which a lightmapped instance gives up (see `lightmap.ts`): where
49
- * these are given, those are not read. **A lightmapped batch needs them**, since a bake is
49
+ * these are given, the tints are not read. **A lightmapped batch needs them**, since a bake is
50
50
  * different light at each instance; one drawn without them reads its tints as its regions, and
51
51
  * either mismatch is said once on the console.
52
+ *
53
+ * **The opacity survives, in sixty-fourths**, so a lightmapped instance fades by
54
+ * `setDitherOpacity` as any other does: its transparency travels as whole steps of 2 added to the
55
+ * region's U offset, which is a place in a page and so under 1, and the vertex stage takes them
56
+ * back off (`LIGHTMAP_OPACITY_STEPS`). Sixty-four is the screen door's own resolution, an 8x8
57
+ * pattern. **What it gives up**: a fading instance's offset keeps 16 bits of fraction rather than
58
+ * 23, a sixteenth of a texel on a 4,096 page; an opaque one is written to the bit as before.
52
59
  */
53
60
  readonly lightmapRegions?: Float32Array;
54
61
  /**
@@ -98,6 +105,11 @@ export declare function createMeshInstances(capacity: number): MeshInstances;
98
105
  * as one four-float attribute, so the opacity cost no attribute and no byte.
99
106
  */
100
107
  export declare const INSTANCE_FLOATS = 20;
108
+ /**
109
+ * How finely a lightmapped instance's opacity travels in its region: transparency in steps of
110
+ * 1/64, each 2 on the U offset. See `MeshInstances.lightmapRegions`; the vertex stage reads it back.
111
+ */
112
+ export declare const LIGHTMAP_OPACITY_STEPS = 64;
101
113
  /** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
102
114
  export declare const INSTANCE_STRIDE: number;
103
115
  /**
@@ -26,6 +26,11 @@ export function createMeshInstances(capacity) {
26
26
  * as one four-float attribute, so the opacity cost no attribute and no byte.
27
27
  */
28
28
  export const INSTANCE_FLOATS = 20;
29
+ /**
30
+ * How finely a lightmapped instance's opacity travels in its region: transparency in steps of
31
+ * 1/64, each 2 on the U offset. See `MeshInstances.lightmapRegions`; the vertex stage reads it back.
32
+ */
33
+ export const LIGHTMAP_OPACITY_STEPS = 64;
29
34
  /** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
30
35
  export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
31
36
  /**
@@ -56,6 +61,11 @@ export function packInstances(instances, out) {
56
61
  /* The region in the tint's and the opacity's place. See `lightmapRegions`. */
57
62
  for (let c = 0; c < 4; c += 1)
58
63
  out[at + 16 + c] = lightmapRegions[i * 4 + c];
64
+ /* And the opacity's transparency in whole steps of 2 on the U offset, none where opaque. */
65
+ const alpha = Math.min(Math.max(alphas?.[i] ?? 1, 0), 1);
66
+ const steps = Math.round((1 - alpha) * LIGHTMAP_OPACITY_STEPS);
67
+ if (steps > 0)
68
+ out[at + 18] = out[at + 18] + 2 * steps;
59
69
  continue;
60
70
  }
61
71
  const t = i * 3;
@@ -1,3 +1,14 @@
1
1
  import { mat4 } from 'gl-matrix';
2
2
  import type { Vec3 } from '../math/color.ts';
3
3
  export declare function computeLightMatrix(lightDir: Vec3, focusX: number, focusY: number, focusZ: number, radius: number, shadowMapSize: number, out: mat4): number;
4
+ /**
5
+ * The depth an orthographic light matrix spans, in metres: what `computeLightMatrix` returns, read
6
+ * back off any such matrix. Its depth row is the light's axis scaled by two over the span, so the
7
+ * span is two over that row's length — whatever focus, radius or bearing built it.
8
+ *
9
+ * **For a matrix the renderer is handed rather than one it built**: the sun's moving layer is read
10
+ * through the matrix its shadow pass was drawn with, and its depth tolerance and distance fade are
11
+ * in metres of that matrix. What would make this wrong is a perspective light, whose depth is not
12
+ * linear; the sun's never is.
13
+ */
14
+ export declare function orthographicDepthSpan(m: ArrayLike<number>): number;
@@ -43,3 +43,17 @@ export function computeLightMatrix(lightDir, focusX, focusY, focusZ, radius, sha
43
43
  // to express its residual depth tolerance in stable world-space metres.
44
44
  return radius * 6;
45
45
  }
46
+ /**
47
+ * The depth an orthographic light matrix spans, in metres: what `computeLightMatrix` returns, read
48
+ * back off any such matrix. Its depth row is the light's axis scaled by two over the span, so the
49
+ * span is two over that row's length — whatever focus, radius or bearing built it.
50
+ *
51
+ * **For a matrix the renderer is handed rather than one it built**: the sun's moving layer is read
52
+ * through the matrix its shadow pass was drawn with, and its depth tolerance and distance fade are
53
+ * in metres of that matrix. What would make this wrong is a perspective light, whose depth is not
54
+ * linear; the sun's never is.
55
+ */
56
+ export function orthographicDepthSpan(m) {
57
+ const length = Math.hypot(m[2], m[6], m[10]);
58
+ return length > 0 ? 2 / length : 1;
59
+ }
@@ -26,6 +26,8 @@ export declare class Mesh {
26
26
  * same frame.
27
27
  */
28
28
  readonly indexCount: number;
29
+ /** `UNSIGNED_SHORT` or `UNSIGNED_INT`, as `compactIndices` stored them: every draw says which. */
30
+ private readonly indexType;
29
31
  /**
30
32
  * The constant values this mesh's *absent* attributes are shaded with, re-applied on
31
33
  * every draw.
@@ -142,7 +144,14 @@ export declare class Mesh {
142
144
  * still visible, which is the kind of bug that only shows up at the edge of the frame.
143
145
  */
144
146
  update(gl: WebGL2RenderingContext, positions: Float32Array, normals?: Float32Array): void;
147
+ /**
148
+ * One attribute's buffer, as floats or in its sixteen-bit fixed point (`vertexPacking.ts`), which
149
+ * WebGL2 reads back through a normalised `SHORT` or `UNSIGNED_SHORT`: three lanes wide where
150
+ * WebGPU needs four, so a normal costs six bytes here.
151
+ */
145
152
  private attachAttribute;
153
+ /** A buffer, or the one value every vertex shares as a constant, as the packing says. */
154
+ private attachShared;
146
155
  /**
147
156
  * Give a bound buffer its storage, and its contents now or later.
148
157
  *
@@ -2,6 +2,8 @@
2
2
  import { MorphTexture } from './backend/webgl2/morphTexture.js';
3
3
  import { ABSENT_ATTRIBUTE } from './vertexDefaults.js';
4
4
  import { UPLOAD_BYTES_PER_STEP } from './uploadStep.js';
5
+ import { compactIndices } from './indexWidth.js';
6
+ import { fixedArray, sharedValue, vertexPackingOf, } from './vertexPacking.js';
5
7
  /*
6
8
  * `MeshData` and its validator live in `@driftengine/drft`, because the container's whole
7
9
  * job is to carry one and the format package must be readable by something that never
@@ -99,6 +101,8 @@ export class Mesh {
99
101
  * same frame.
100
102
  */
101
103
  indexCount;
104
+ /** `UNSIGNED_SHORT` or `UNSIGNED_INT`, as `compactIndices` stored them: every draw says which. */
105
+ indexType;
102
106
  /**
103
107
  * The constant values this mesh's *absent* attributes are shaded with, re-applied on
104
108
  * every draw.
@@ -207,12 +211,14 @@ export class Mesh {
207
211
  * that has been told a lie every frame is the shape of a stall nobody can find.
208
212
  */
209
213
  const usage = dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW;
214
+ /* How each attribute travels: sixteen-bit fixed point, or one value as a constant. */
215
+ const packing = vertexPackingOf(data, dynamic);
210
216
  const positions = this.attachAttribute(gl, ATTR_POSITION, data.positions, 3, usage);
211
- const normals = this.attachAttribute(gl, ATTR_NORMAL, data.normals, 3, usage);
217
+ const normals = this.attachAttribute(gl, ATTR_NORMAL, data.normals, 3, usage, packing.normals);
212
218
  if (dynamic)
213
219
  this.deformable = { positions, normals };
214
- this.attachAttribute(gl, ATTR_COLOR, data.colors, 3);
215
- this.attachAttribute(gl, ATTR_EMISSIVE, data.emissive, 1);
220
+ this.attachShared(gl, ATTR_COLOR, data.colors, 3, packing.colors);
221
+ this.attachShared(gl, ATTR_EMISSIVE, data.emissive, 1, packing.emissive);
216
222
  /*
217
223
  * Absent means "not shiny", supplied as a constant rather than as a buffer of
218
224
  * zeroes. `vertexAttrib1f` sets the value a *disabled* attribute reads, so this
@@ -289,7 +295,7 @@ export class Mesh {
289
295
  }
290
296
  else {
291
297
  this.attachAttribute(gl, ATTR_JOINTS, data.joints, 4);
292
- this.attachAttribute(gl, ATTR_WEIGHTS, data.weights, 4);
298
+ this.attachAttribute(gl, ATTR_WEIGHTS, data.weights, 4, gl.STATIC_DRAW, packing.weights);
293
299
  }
294
300
  /* The fifth to eighth influences, or zeros, which add nothing. See `isSkinnedEight`. */
295
301
  this.isSkinnedEight = this.isSkinned && data.joints2 !== undefined;
@@ -299,7 +305,7 @@ export class Mesh {
299
305
  }
300
306
  else {
301
307
  this.attachAttribute(gl, ATTR_JOINTS2, data.joints2, 4);
302
- this.attachAttribute(gl, ATTR_WEIGHTS2, data.weights2, 4);
308
+ this.attachAttribute(gl, ATTR_WEIGHTS2, data.weights2, 4, gl.STATIC_DRAW, packing.weights2);
303
309
  }
304
310
  this.hasTangents = data.tangents !== undefined;
305
311
  if (data.tangents === undefined) {
@@ -307,7 +313,7 @@ export class Mesh {
307
313
  this.constants.push({ location: ATTR_TANGENT, value: ABSENT_ATTRIBUTE['tangents'] });
308
314
  }
309
315
  else {
310
- this.attachAttribute(gl, ATTR_TANGENT, data.tangents, 4);
316
+ this.attachAttribute(gl, ATTR_TANGENT, data.tangents, 4, gl.STATIC_DRAW, packing.tangents);
311
317
  }
312
318
  this.hasGrain = data.grain !== undefined;
313
319
  if (data.grain === undefined) {
@@ -337,7 +343,9 @@ export class Mesh {
337
343
  this.buffers.push(indexBuffer);
338
344
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
339
345
  this.indexBuffer = indexBuffer;
340
- this.allocate(gl, gl.ELEMENT_ARRAY_BUFFER, indexBuffer, data.indices, gl.STATIC_DRAW);
346
+ const indices = compactIndices(data.indices, this.vertexCount);
347
+ this.indexType = indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT;
348
+ this.allocate(gl, gl.ELEMENT_ARRAY_BUFFER, indexBuffer, indices, gl.STATIC_DRAW);
341
349
  gl.bindVertexArray(null);
342
350
  }
343
351
  /** Caller is responsible for having the flat program + uniforms bound. */
@@ -354,7 +362,7 @@ export class Mesh {
354
362
  const value = constant.value;
355
363
  applyConstant(gl, constant.location, value);
356
364
  }
357
- gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_INT, 0);
365
+ gl.drawElements(gl.TRIANGLES, this.indexCount, this.indexType, 0);
358
366
  }
359
367
  /**
360
368
  * A vertex array for one instanced batch: every attribute this mesh binds from a buffer, its index
@@ -374,7 +382,7 @@ export class Mesh {
374
382
  for (const attribute of this.layout) {
375
383
  gl.bindBuffer(gl.ARRAY_BUFFER, attribute.buffer);
376
384
  gl.enableVertexAttribArray(attribute.location);
377
- gl.vertexAttribPointer(attribute.location, attribute.size, gl.FLOAT, false, 0, 0);
385
+ gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, 0, 0);
378
386
  }
379
387
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
380
388
  gl.bindBuffer(gl.ARRAY_BUFFER, instances);
@@ -399,7 +407,7 @@ export class Mesh {
399
407
  const value = constant.value;
400
408
  applyConstant(gl, constant.location, value);
401
409
  }
402
- gl.drawElementsInstanced(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_INT, 0, count);
410
+ gl.drawElementsInstanced(gl.TRIANGLES, this.indexCount, this.indexType, 0, count);
403
411
  }
404
412
  dispose(gl) {
405
413
  this.morph?.dispose(gl);
@@ -437,18 +445,34 @@ export class Mesh {
437
445
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, normals);
438
446
  }
439
447
  }
440
- attachAttribute(gl, location, data, size, usage = gl.STATIC_DRAW) {
448
+ /**
449
+ * One attribute's buffer, as floats or in its sixteen-bit fixed point (`vertexPacking.ts`), which
450
+ * WebGL2 reads back through a normalised `SHORT` or `UNSIGNED_SHORT`: three lanes wide where
451
+ * WebGPU needs four, so a normal costs six bytes here.
452
+ */
453
+ attachAttribute(gl, location, data, size, usage = gl.STATIC_DRAW, packing = 'float') {
441
454
  const buffer = gl.createBuffer();
442
455
  if (buffer === null)
443
456
  throw new Error('createBuffer failed');
444
457
  this.buffers.push(buffer);
445
458
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
446
- this.allocate(gl, gl.ARRAY_BUFFER, buffer, data, usage);
459
+ const fixed = packing === 'snorm16' || packing === 'unorm16';
460
+ this.allocate(gl, gl.ARRAY_BUFFER, buffer, fixed ? fixedArray(data, packing) : data, usage);
461
+ const type = packing === 'snorm16' ? gl.SHORT : packing === 'unorm16' ? gl.UNSIGNED_SHORT : gl.FLOAT;
447
462
  gl.enableVertexAttribArray(location);
448
- gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
449
- this.layout.push({ location, buffer, size });
463
+ gl.vertexAttribPointer(location, size, type, fixed, 0, 0);
464
+ this.layout.push({ location, buffer, size, type, normalized: fixed });
450
465
  return buffer;
451
466
  }
467
+ /** A buffer, or the one value every vertex shares as a constant, as the packing says. */
468
+ attachShared(gl, location, data, size, packing) {
469
+ if (packing !== 'constant') {
470
+ this.attachAttribute(gl, location, data, size, gl.STATIC_DRAW, packing);
471
+ return;
472
+ }
473
+ gl.disableVertexAttribArray(location);
474
+ this.constants.push({ location, value: sharedValue(data, size) });
475
+ }
452
476
  /**
453
477
  * Give a bound buffer its storage, and its contents now or later.
454
478
  *
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Where ambient occlusion fades out with distance: `setAmbientOcclusionFade`'s two numbers as the
3
+ * estimate reads them, `uFade` in `shaders/ambientOcclusion.ts`.
4
+ *
5
+ * **Occlusion is whole up to `distance` metres from the eye and gone `radius` metres past it.** A
6
+ * screen-space estimate reaches as far as the depth does, and far off the depth buffer's steps are
7
+ * metres apart: a sky dome or a mountain range kilometres away is shaded in rings that follow
8
+ * those steps, and nothing there is in contact with anything the estimate could be right about.
9
+ * A pixel past the fade skips the estimate's walk, as the sky does, so it costs less as well.
10
+ *
11
+ * **What it gives up**: distant contact shade that is real — a doorway a hundred metres off — fades
12
+ * with the rest, and the distance is the eye's and not the subject's, so a long lens fades what it
13
+ * frames close up. What would make it wrong is a scene whose far depth is precise enough to shade,
14
+ * which is a reason to fade further, not to leave the fade off.
15
+ */
16
+ /**
17
+ * `distance` and `radius` into `out`, `[distance, radius]`. A distance that is not a finite number
18
+ * at or above zero is no fade, `[−1, 0]`, so `Infinity` reads as "never fades". A radius below zero
19
+ * or not a number is 0, which is a cut at `distance` rather than a ramp.
20
+ */
21
+ export declare function resolveOcclusionFade(distance: number, radius: number, out: Float32Array): void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Where ambient occlusion fades out with distance: `setAmbientOcclusionFade`'s two numbers as the
3
+ * estimate reads them, `uFade` in `shaders/ambientOcclusion.ts`.
4
+ *
5
+ * **Occlusion is whole up to `distance` metres from the eye and gone `radius` metres past it.** A
6
+ * screen-space estimate reaches as far as the depth does, and far off the depth buffer's steps are
7
+ * metres apart: a sky dome or a mountain range kilometres away is shaded in rings that follow
8
+ * those steps, and nothing there is in contact with anything the estimate could be right about.
9
+ * A pixel past the fade skips the estimate's walk, as the sky does, so it costs less as well.
10
+ *
11
+ * **What it gives up**: distant contact shade that is real — a doorway a hundred metres off — fades
12
+ * with the rest, and the distance is the eye's and not the subject's, so a long lens fades what it
13
+ * frames close up. What would make it wrong is a scene whose far depth is precise enough to shade,
14
+ * which is a reason to fade further, not to leave the fade off.
15
+ */
16
+ /** `uFade` for no fade: a distance below zero, which the estimate reads as "never". */
17
+ const NO_FADE = -1;
18
+ /**
19
+ * `distance` and `radius` into `out`, `[distance, radius]`. A distance that is not a finite number
20
+ * at or above zero is no fade, `[−1, 0]`, so `Infinity` reads as "never fades". A radius below zero
21
+ * or not a number is 0, which is a cut at `distance` rather than a ramp.
22
+ */
23
+ export function resolveOcclusionFade(distance, radius, out) {
24
+ const fades = Number.isFinite(distance) && distance >= 0;
25
+ out[0] = fades ? distance : NO_FADE;
26
+ out[1] = fades && Number.isFinite(radius) ? Math.max(radius, 0) : 0;
27
+ }
@@ -42,6 +42,8 @@ export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder' | 'filmic';
42
42
  export type ShadowFilterTaps = 4 | 8 | 12;
43
43
  export type DirectionalShadowDepthLayers = 1 | 2;
44
44
  export type WaterReflectionFilterTaps = 1 | 5 | 9;
45
+ /** What a draw does when its pipeline has not compiled yet: see `RenderQuality.pipelineCompile`. */
46
+ export type PipelineCompile = 'wait' | 'skip';
45
47
  /** How light through glass is coloured: see `RenderQuality.glassShadows`. */
46
48
  export type GlassShadows = 'full' | 'half' | 'off';
47
49
  /** How light scattered beneath skin is drawn: see `RenderQuality.skinScattering`. */
@@ -210,6 +212,34 @@ export interface RenderQuality {
210
212
  * lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
211
213
  */
212
214
  readonly glassShadows: GlassShadows;
215
+ /**
216
+ * What a lit draw does when the pipeline it needs has not compiled: one first needed inside a
217
+ * frame, by a material's own state — two-sided, cut out, a surface model — or a draw state a
218
+ * mesh's creation did not warm.
219
+ *
220
+ * **`'wait'`, the default**: the pipeline is built where the draw asks for it, and on WebGPU the
221
+ * browser compiles it on its GPU process's main thread, holding the frame — 130 to 400 ms a
222
+ * pipeline was reported on a phone, for a material first drawn mid-scene. **`'skip'`** starts the
223
+ * compile off that thread and leaves the draw out until it lands, a few frames to a few dozen,
224
+ * and the draw then appears; nothing waits. Static draws always wait, since a recorded list keeps
225
+ * what it recorded, and a compile that fails is built where it is next asked for, so the device
226
+ * says why rather than the draw vanishing for good.
227
+ *
228
+ * **The way to have neither** is to draw what a scene will use once, behind a loading screen, and
229
+ * await `ready()`, which waits for every compile started — whichever this says. What would make
230
+ * `'skip'` wrong is a frame that must hold everything it was given, such as a capture of the first
231
+ * frame of a scene not awaited.
232
+ *
233
+ * **Measured on a desktop GPU** (`demo/dev/pipelineCompile.html`, eight new variants at once): on
234
+ * WebGPU the frame that met them took 700 to 3,700 ms waiting and 17 ms skipping, the draws
235
+ * landing 21 to 26 frames later. WebGL2 compiles its lit programs when it is made, all but a
236
+ * surface model's, which it compiles at the first draw that wants one: there `'skip'` asks the
237
+ * driver through `KHR_parallel_shader_compile` and waits where a context does not offer it — which
238
+ * ANGLE on Vulkan does not. Where it is offered, ANGLE on OpenGL, the longest frame went from 820 to
239
+ * 1,120 ms to 630 to 700 ms: the driver still finishes its own compile at the program's first
240
+ * draw, which nothing above it can move.
241
+ */
242
+ readonly pipelineCompile: PipelineCompile;
213
243
  /**
214
244
  * How light that scatters beneath skin is drawn, for a material whose model is `skinModel`.
215
245
  *
@@ -63,6 +63,7 @@ export const DEFAULT_RENDER_QUALITY = Object.freeze({
63
63
  lightVolumeSamples: 32,
64
64
  directionalShadowDepthLayers: 2,
65
65
  glassShadows: 'full',
66
+ pipelineCompile: 'wait',
66
67
  skinScattering: 'pre-integrated',
67
68
  directionalShadowMaxDistance: 6,
68
69
  directionalShadowMaxSlope: 3,
@@ -218,6 +219,7 @@ export function resolveRenderQuality(options = {}) {
218
219
  lightVolumeSamples: options.lightVolumeSamples ?? DEFAULT_RENDER_QUALITY.lightVolumeSamples,
219
220
  directionalShadowDepthLayers: options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
220
221
  glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
222
+ pipelineCompile: options.pipelineCompile ?? DEFAULT_RENDER_QUALITY.pipelineCompile,
221
223
  skinScattering: options.skinScattering ?? DEFAULT_RENDER_QUALITY.skinScattering,
222
224
  directionalShadowMaxDistance: options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
223
225
  directionalShadowMaxSlope: options.directionalShadowMaxSlope ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxSlope,
@@ -318,6 +320,9 @@ export function resolveRenderQuality(options = {}) {
318
320
  quality.glassShadows !== 'off') {
319
321
  throw new Error(`RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`);
320
322
  }
323
+ if (quality.pipelineCompile !== 'wait' && quality.pipelineCompile !== 'skip') {
324
+ throw new Error(`RenderQuality.pipelineCompile must be 'wait' or 'skip', got ${String(quality.pipelineCompile)}`);
325
+ }
321
326
  if (quality.skinScattering !== 'pre-integrated' && quality.skinScattering !== 'screen-space') {
322
327
  throw new Error(`RenderQuality.skinScattering must be 'pre-integrated' or 'screen-space', got ${String(quality.skinScattering)}`);
323
328
  }
@@ -38,6 +38,13 @@ export declare function recordingGl(options?: {
38
38
  * recording.
39
39
  */
40
40
  readonly uniforms?: readonly string[];
41
+ /**
42
+ * Whether a program started under `KHR_parallel_shader_compile` has finished, which the test
43
+ * flips: `COMPLETION_STATUS_KHR` answers `done`. Finished by default, as every program here is.
44
+ */
45
+ readonly compiling?: {
46
+ done: boolean;
47
+ };
41
48
  }): {
42
49
  gl: WebGL2RenderingContext;
43
50
  canvas: HTMLCanvasElement;
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { countUniformVectors } from './uniformVectorBudget.js';
19
19
  export function recordingGl(options = {}) {
20
+ const compiling = options.compiling ?? { done: true };
20
21
  const extensions = new Set(options.extensions ?? []);
21
22
  const uniforms = options.uniforms ?? [];
22
23
  /* What `clearDepth` last set, so a module that borrows the value can be seen to put it back. */
@@ -93,6 +94,13 @@ export function recordingGl(options = {}) {
93
94
  HALF_FLOAT: 0x140b,
94
95
  DEPTH_COMPONENT: 0x1902,
95
96
  UNSIGNED_INT: 0x1405,
97
+ /* Named so a mesh's index width and each attribute's type can be read back: the buffer its
98
+ indices fill, the type every draw of it names, and how each attribute is read. See
99
+ `mesh.test.ts`. */
100
+ UNSIGNED_SHORT: 0x1403,
101
+ SHORT: 0x1402,
102
+ FLOAT: 0x1406,
103
+ ELEMENT_ARRAY_BUFFER: 0x8893,
96
104
  LINEAR: 0x2601,
97
105
  COLOR_BUFFER_BIT: 0x00004000,
98
106
  /* Named so a test can tell a mesh that declared itself deforming from one that did not. */
@@ -137,6 +145,8 @@ export function recordingGl(options = {}) {
137
145
  if (name === 'EXT_clip_control') {
138
146
  return { LOWER_LEFT_EXT: 0x8ca1, ZERO_TO_ONE_EXT: 0x935f, clipControlEXT: () => undefined };
139
147
  }
148
+ if (name === 'KHR_parallel_shader_compile')
149
+ return { COMPLETION_STATUS_KHR: 0x91b1 };
140
150
  return {};
141
151
  },
142
152
  getShaderParameter: () => true,
@@ -166,6 +176,8 @@ export function recordingGl(options = {}) {
166
176
  /* Linked yes; zero active uniforms and attributes, which ends every reflection loop at once
167
177
  and is why this harness does not have to know a single uniform name. */
168
178
  getProgramParameter: (program, pname) => {
179
+ if (pname === 0x91b1)
180
+ return compiling.done;
169
181
  if (pname === constants.ACTIVE_UNIFORMS)
170
182
  return uniforms.length;
171
183
  if (pname === constants.ACTIVE_ATTRIBUTES)
@@ -313,6 +313,8 @@ export declare class SceneTarget {
313
313
  /** The projection's x and y scales, which turn a world radius into a screen one. */
314
314
  readonly projScale: Float32Array;
315
315
  readonly invProjection: Float32Array;
316
+ /** Where it fades out with distance, `uFade`: see `occlusionFade.ts`. */
317
+ readonly fade: Float32Array;
316
318
  /** Where the frame's jitter put the occlusion, in uv: see the shader's `uAoOffset`. */
317
319
  readonly offset?: Float32Array;
318
320
  }, bloom?: {
@@ -771,7 +771,7 @@ export class SceneTarget {
771
771
  let aoTexture = null;
772
772
  if (ao !== undefined && !this.depthBlitRefused && this.depth !== null) {
773
773
  this.ao ??= new AmbientOcclusionPass(gl);
774
- aoTexture = this.ao.run(this.depth, this.width, this.height, ao.radius, ao.projScale, ao.invProjection);
774
+ aoTexture = this.ao.run(this.depth, this.width, this.height, ao.radius, ao.projScale, ao.invProjection, ao.fade);
775
775
  }
776
776
  /*
777
777
  * Bloom beside occlusion rather than after the composite, because the composite is what
@@ -1,5 +1,20 @@
1
1
  /** Shader compilation with fail-fast diagnostics (init-time only, never per-frame). */
2
2
  export declare function compileProgram(gl: WebGL2RenderingContext, vertexSource: string, fragmentSource: string, label: string): WebGLProgram;
3
+ /**
4
+ * A program compiled and linked without asking whether that worked, since asking waits for the
5
+ * driver to finish: for a context offering `KHR_parallel_shader_compile`, whose
6
+ * `COMPLETION_STATUS_KHR` says when asking no longer waits. `finishProgram` asks then, and fails
7
+ * as loudly as `compileProgram` does; the stages are kept until it does, for their logs.
8
+ */
9
+ export interface StartedProgram {
10
+ readonly program: WebGLProgram;
11
+ readonly vertex: WebGLShader;
12
+ readonly fragment: WebGLShader;
13
+ readonly label: string;
14
+ }
15
+ export declare function startProgram(gl: WebGL2RenderingContext, vertexSource: string, fragmentSource: string, label: string): StartedProgram;
16
+ /** A started program, checked as `compileProgram` checks one: its stages, then its link. */
17
+ export declare function finishProgram(gl: WebGL2RenderingContext, started: StartedProgram): WebGLProgram;
3
18
  export declare function setUniformStrictMode(on: boolean): void;
4
19
  /** Locations of every active uniform, keyed by name. */
5
20
  export declare function uniformLocations(gl: WebGL2RenderingContext, program: WebGLProgram, label?: string): Record<string, WebGLUniformLocation>;
@@ -17,6 +17,47 @@ export function compileProgram(gl, vertexSource, fragmentSource, label) {
17
17
  }
18
18
  return program;
19
19
  }
20
+ export function startProgram(gl, vertexSource, fragmentSource, label) {
21
+ const stage = (type, source, name) => {
22
+ const shader = gl.createShader(type);
23
+ if (shader === null)
24
+ throw new Error(`[${label}.${name}] createShader failed`);
25
+ gl.shaderSource(shader, source);
26
+ gl.compileShader(shader);
27
+ return shader;
28
+ };
29
+ const vertex = stage(gl.VERTEX_SHADER, vertexSource, 'vert');
30
+ const fragment = stage(gl.FRAGMENT_SHADER, fragmentSource, 'frag');
31
+ const program = gl.createProgram();
32
+ if (program === null)
33
+ throw new Error(`[${label}] createProgram failed`);
34
+ gl.attachShader(program, vertex);
35
+ gl.attachShader(program, fragment);
36
+ gl.linkProgram(program);
37
+ return { program, vertex, fragment, label };
38
+ }
39
+ /** A started program, checked as `compileProgram` checks one: its stages, then its link. */
40
+ export function finishProgram(gl, started) {
41
+ const { program, vertex, fragment, label } = started;
42
+ const fail = (what, log) => {
43
+ gl.deleteShader(vertex);
44
+ gl.deleteShader(fragment);
45
+ gl.deleteProgram(program);
46
+ throw new Error(`[${label}${what}] ${log ?? 'no log'}`);
47
+ };
48
+ if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS)) {
49
+ fail('.vert compile failed:', gl.getShaderInfoLog(vertex));
50
+ }
51
+ if (!gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
52
+ fail('.frag compile failed:', gl.getShaderInfoLog(fragment));
53
+ }
54
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
55
+ fail(' link failed:', gl.getProgramInfoLog(program));
56
+ }
57
+ gl.deleteShader(vertex);
58
+ gl.deleteShader(fragment);
59
+ return program;
60
+ }
20
61
  /**
21
62
  * Whether a uniform lookup that cannot reach the GPU is reported.
22
63
  *
@@ -75,6 +75,11 @@ uniform vec2 uProjScale;
75
75
  uniform mat4 uInvProjection;
76
76
  /** How far the sampling reaches, in metres of the world. */
77
77
  uniform float uRadius;
78
+ /**
79
+ * Where occlusion fades out: whole up to x metres from the eye, gone y metres past it, and no fade
80
+ * where x is below zero. See occlusionFade.ts.
81
+ */
82
+ uniform vec2 uFade;
78
83
 
79
84
  out float fragColor;
80
85
 
@@ -167,6 +172,12 @@ void main() {
167
172
  fragColor = 1.0;
168
173
  return;
169
174
  }
175
+ /* How far past the caller's fade this pixel is, and open with no walk where it is all the way. */
176
+ float faded = uFade.x < 0.0 ? 0.0 : clamp((length(p) - uFade.x) / max(uFade.y, 1e-4), 0.0, 1.0);
177
+ if (faded >= 1.0) {
178
+ fragColor = AO_STORE;
179
+ return;
180
+ }
170
181
 
171
182
  /*
172
183
  * At a silhouette, the quad derivative above is replaced by the one-sided difference that
@@ -307,7 +318,7 @@ void main() {
307
318
  visible += projectedLength * gtaoArc(h0, h1, normalAngle);
308
319
  }
309
320
 
310
- fragColor = clamp(visible / float(AO_SLICES) * AO_STORE, 0.0, 1.0);
321
+ fragColor = clamp(mix(visible / float(AO_SLICES), 1.0, faded) * AO_STORE, 0.0, 1.0);
311
322
  }
312
323
  `;
313
324
  /**