@driftengine/core 4.10.0 → 4.11.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 (140) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -3
  4. package/dist/index.js +1 -1
  5. package/dist/render/backend/webgl2/renderer.d.ts +99 -2
  6. package/dist/render/backend/webgl2/renderer.js +361 -42
  7. package/dist/render/backend/webgpu/buffers.d.ts +5 -0
  8. package/dist/render/backend/webgpu/buffers.js +98 -48
  9. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  10. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  11. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  12. package/dist/render/backend/webgpu/clothOps.js +78 -0
  13. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  14. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  15. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  16. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  17. package/dist/render/backend/webgpu/drawCommand.d.ts +2 -0
  18. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  19. package/dist/render/backend/webgpu/flatPass.d.ts +1 -1
  20. package/dist/render/backend/webgpu/flatPass.js +2 -1
  21. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  22. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  23. package/dist/render/backend/webgpu/latePass.js +1 -0
  24. package/dist/render/backend/webgpu/motionPass.js +15 -6
  25. package/dist/render/backend/webgpu/pipelineCache.d.ts +22 -1
  26. package/dist/render/backend/webgpu/pipelineCache.js +39 -1
  27. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  28. package/dist/render/backend/webgpu/renderer.d.ts +48 -0
  29. package/dist/render/backend/webgpu/renderer.js +207 -27
  30. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  31. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  32. package/dist/render/backend/webgpu/staticBundles.js +2 -1
  33. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  34. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  35. package/dist/render/globalMediumPass.d.ts +2 -0
  36. package/dist/render/globalMediumPass.js +1 -0
  37. package/dist/render/indexWidth.d.ts +17 -0
  38. package/dist/render/indexWidth.js +23 -0
  39. package/dist/render/lightMatrix.d.ts +11 -0
  40. package/dist/render/lightMatrix.js +14 -0
  41. package/dist/render/mesh.d.ts +9 -0
  42. package/dist/render/mesh.js +38 -14
  43. package/dist/render/renderQuality.d.ts +30 -0
  44. package/dist/render/renderQuality.js +5 -0
  45. package/dist/render/rendererHarness.d.ts +7 -0
  46. package/dist/render/rendererHarness.js +12 -0
  47. package/dist/render/shader.d.ts +15 -0
  48. package/dist/render/shader.js +41 -0
  49. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  50. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  51. package/dist/render/shaders/flat/glassTint.js +3 -0
  52. package/dist/render/shaders/flat/index.d.ts +15 -0
  53. package/dist/render/shaders/flat/index.js +15 -2
  54. package/dist/render/shaders/flat/layered.d.ts +19 -0
  55. package/dist/render/shaders/flat/layered.js +115 -0
  56. package/dist/render/shaders/flat/main.d.ts +1 -1
  57. package/dist/render/shaders/flat/main.js +43 -16
  58. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  59. package/dist/render/shaders/flat/mapReads.js +38 -0
  60. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  61. package/dist/render/shaders/flat/movingSun.js +46 -0
  62. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  63. package/dist/render/shaders/flat/normalMap.js +14 -3
  64. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  65. package/dist/render/shaders/flat/overlay.js +41 -19
  66. package/dist/render/shaders/flat/preamble.js +23 -5
  67. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  68. package/dist/render/shaders/flat/worldUv.js +113 -0
  69. package/dist/render/shaders/generated/flat.wgsl.d.ts +768 -368
  70. package/dist/render/shaders/generated/flat.wgsl.js +791 -391
  71. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  72. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  73. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  74. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  75. package/dist/render/shaders/globalMedium.js +9 -1
  76. package/dist/render/shaders/lightVolume.js +9 -1
  77. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  78. package/dist/render/shaders/tangentFrame.js +16 -3
  79. package/dist/render/skinnedClothRun.d.ts +28 -0
  80. package/dist/render/skinnedClothRun.js +103 -35
  81. package/dist/render/surfaceLayers.d.ts +39 -0
  82. package/dist/render/surfaceLayers.js +26 -0
  83. package/dist/render/surfaceOverlay.d.ts +16 -3
  84. package/dist/render/surfaceOverlay.js +11 -6
  85. package/dist/render/surfaceProjection.d.ts +34 -0
  86. package/dist/render/surfaceProjection.js +30 -0
  87. package/dist/render/surfaceTexture.d.ts +30 -0
  88. package/dist/render/vertexPacking.d.ts +48 -0
  89. package/dist/render/vertexPacking.js +68 -0
  90. package/package.json +6 -6
  91. package/src/index.ts +4 -2
  92. package/src/render/backend/webgl2/renderer.ts +408 -55
  93. package/src/render/backend/webgpu/buffers.ts +153 -54
  94. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  95. package/src/render/backend/webgpu/clothOps.ts +86 -0
  96. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  97. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  98. package/src/render/backend/webgpu/drawCommand.ts +3 -0
  99. package/src/render/backend/webgpu/flatPass.ts +26 -28
  100. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  101. package/src/render/backend/webgpu/latePass.ts +1 -0
  102. package/src/render/backend/webgpu/motionPass.ts +18 -7
  103. package/src/render/backend/webgpu/pipelineCache.ts +46 -7
  104. package/src/render/backend/webgpu/renderer.ts +274 -48
  105. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  106. package/src/render/backend/webgpu/staticBundles.ts +2 -1
  107. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  108. package/src/render/globalMediumPass.ts +7 -0
  109. package/src/render/indexWidth.ts +27 -0
  110. package/src/render/lightMatrix.ts +15 -0
  111. package/src/render/mesh.ts +66 -14
  112. package/src/render/renderQuality.ts +38 -0
  113. package/src/render/rendererHarness.ts +15 -0
  114. package/src/render/shader.ts +59 -0
  115. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  116. package/src/render/shaders/flat/glassTint.ts +7 -1
  117. package/src/render/shaders/flat/index.ts +30 -2
  118. package/src/render/shaders/flat/layered.ts +115 -0
  119. package/src/render/shaders/flat/main.ts +43 -16
  120. package/src/render/shaders/flat/mapReads.ts +38 -0
  121. package/src/render/shaders/flat/movingSun.ts +46 -0
  122. package/src/render/shaders/flat/normalMap.ts +14 -3
  123. package/src/render/shaders/flat/overlay.ts +41 -19
  124. package/src/render/shaders/flat/preamble.ts +23 -5
  125. package/src/render/shaders/flat/worldUv.ts +113 -0
  126. package/src/render/shaders/generated/flat.wgsl.ts +791 -391
  127. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  128. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  129. package/src/render/shaders/globalMedium.ts +9 -1
  130. package/src/render/shaders/lightVolume.ts +9 -1
  131. package/src/render/shaders/tangentFrame.ts +16 -3
  132. package/src/render/skinnedClothRun.ts +138 -38
  133. package/src/render/surfaceLayers.ts +57 -0
  134. package/src/render/surfaceOverlay.ts +24 -7
  135. package/src/render/surfaceProjection.ts +57 -0
  136. package/src/render/surfaceTexture.ts +30 -0
  137. package/src/render/vertexPacking.ts +109 -0
  138. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  139. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  140. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -3,6 +3,13 @@
3
3
  import { MorphTexture } from './backend/webgl2/morphTexture.ts';
4
4
  import { ABSENT_ATTRIBUTE } from './vertexDefaults.ts';
5
5
  import { UPLOAD_BYTES_PER_STEP } from './uploadStep.ts';
6
+ import { compactIndices } from './indexWidth.ts';
7
+ import {
8
+ fixedArray,
9
+ sharedValue,
10
+ vertexPackingOf,
11
+ type AttributePacking,
12
+ } from './vertexPacking.ts';
6
13
 
7
14
  /*
8
15
  * `MeshData` and its validator live in `@driftengine/drft`, because the container's whole
@@ -92,7 +99,14 @@ export class Mesh {
92
99
  private readonly vao: WebGLVertexArrayObject;
93
100
  private readonly buffers: WebGLBuffer[] = [];
94
101
  /** Every attribute bound from a buffer, so a batch can build a vertex array of its own. */
95
- private readonly layout: { location: number; buffer: WebGLBuffer; size: number }[] = [];
102
+ private readonly layout: {
103
+ location: number;
104
+ buffer: WebGLBuffer;
105
+ size: number;
106
+ /** `FLOAT`, or a sixteen-bit type read normalised: `vertexPacking.ts`. */
107
+ type: number;
108
+ normalized: boolean;
109
+ }[] = [];
96
110
  private indexBuffer: WebGLBuffer | null = null;
97
111
  /**
98
112
  * The position and normal buffers, kept so `update` can rewrite them.
@@ -110,6 +124,8 @@ export class Mesh {
110
124
  * same frame.
111
125
  */
112
126
  readonly indexCount: number;
127
+ /** `UNSIGNED_SHORT` or `UNSIGNED_INT`, as `compactIndices` stored them: every draw says which. */
128
+ private readonly indexType: number;
113
129
  /**
114
130
  * The constant values this mesh's *absent* attributes are shaded with, re-applied on
115
131
  * every draw.
@@ -226,11 +242,13 @@ export class Mesh {
226
242
  * that has been told a lie every frame is the shape of a stall nobody can find.
227
243
  */
228
244
  const usage = dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW;
245
+ /* How each attribute travels: sixteen-bit fixed point, or one value as a constant. */
246
+ const packing = vertexPackingOf(data, dynamic);
229
247
  const positions = this.attachAttribute(gl, ATTR_POSITION, data.positions, 3, usage);
230
- const normals = this.attachAttribute(gl, ATTR_NORMAL, data.normals, 3, usage);
248
+ const normals = this.attachAttribute(gl, ATTR_NORMAL, data.normals, 3, usage, packing.normals);
231
249
  if (dynamic) this.deformable = { positions, normals };
232
- this.attachAttribute(gl, ATTR_COLOR, data.colors, 3);
233
- this.attachAttribute(gl, ATTR_EMISSIVE, data.emissive, 1);
250
+ this.attachShared(gl, ATTR_COLOR, data.colors, 3, packing.colors);
251
+ this.attachShared(gl, ATTR_EMISSIVE, data.emissive, 1, packing.emissive);
234
252
  /*
235
253
  * Absent means "not shiny", supplied as a constant rather than as a buffer of
236
254
  * zeroes. `vertexAttrib1f` sets the value a *disabled* attribute reads, so this
@@ -304,7 +322,7 @@ export class Mesh {
304
322
  this.constants.push({ location: ATTR_WEIGHTS, value: ABSENT_ATTRIBUTE['weights'] });
305
323
  } else {
306
324
  this.attachAttribute(gl, ATTR_JOINTS, data.joints, 4);
307
- this.attachAttribute(gl, ATTR_WEIGHTS, data.weights, 4);
325
+ this.attachAttribute(gl, ATTR_WEIGHTS, data.weights, 4, gl.STATIC_DRAW, packing.weights);
308
326
  }
309
327
  /* The fifth to eighth influences, or zeros, which add nothing. See `isSkinnedEight`. */
310
328
  this.isSkinnedEight = this.isSkinned && data.joints2 !== undefined;
@@ -313,7 +331,7 @@ export class Mesh {
313
331
  this.constants.push({ location: ATTR_WEIGHTS2, value: ABSENT_ATTRIBUTE['weights2'] });
314
332
  } else {
315
333
  this.attachAttribute(gl, ATTR_JOINTS2, data.joints2, 4);
316
- this.attachAttribute(gl, ATTR_WEIGHTS2, data.weights2, 4);
334
+ this.attachAttribute(gl, ATTR_WEIGHTS2, data.weights2, 4, gl.STATIC_DRAW, packing.weights2);
317
335
  }
318
336
 
319
337
  this.hasTangents = data.tangents !== undefined;
@@ -321,7 +339,7 @@ export class Mesh {
321
339
  gl.disableVertexAttribArray(ATTR_TANGENT);
322
340
  this.constants.push({ location: ATTR_TANGENT, value: ABSENT_ATTRIBUTE['tangents'] });
323
341
  } else {
324
- this.attachAttribute(gl, ATTR_TANGENT, data.tangents, 4);
342
+ this.attachAttribute(gl, ATTR_TANGENT, data.tangents, 4, gl.STATIC_DRAW, packing.tangents);
325
343
  }
326
344
 
327
345
  this.hasGrain = data.grain !== undefined;
@@ -351,7 +369,9 @@ export class Mesh {
351
369
  this.buffers.push(indexBuffer);
352
370
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
353
371
  this.indexBuffer = indexBuffer;
354
- this.allocate(gl, gl.ELEMENT_ARRAY_BUFFER, indexBuffer, data.indices, gl.STATIC_DRAW);
372
+ const indices = compactIndices(data.indices, this.vertexCount);
373
+ this.indexType = indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT;
374
+ this.allocate(gl, gl.ELEMENT_ARRAY_BUFFER, indexBuffer, indices, gl.STATIC_DRAW);
355
375
 
356
376
  gl.bindVertexArray(null);
357
377
  }
@@ -370,7 +390,7 @@ export class Mesh {
370
390
  const value = constant.value;
371
391
  applyConstant(gl, constant.location, value);
372
392
  }
373
- gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_INT, 0);
393
+ gl.drawElements(gl.TRIANGLES, this.indexCount, this.indexType, 0);
374
394
  }
375
395
 
376
396
  /**
@@ -394,7 +414,14 @@ export class Mesh {
394
414
  for (const attribute of this.layout) {
395
415
  gl.bindBuffer(gl.ARRAY_BUFFER, attribute.buffer);
396
416
  gl.enableVertexAttribArray(attribute.location);
397
- gl.vertexAttribPointer(attribute.location, attribute.size, gl.FLOAT, false, 0, 0);
417
+ gl.vertexAttribPointer(
418
+ attribute.location,
419
+ attribute.size,
420
+ attribute.type,
421
+ attribute.normalized,
422
+ 0,
423
+ 0,
424
+ );
398
425
  }
399
426
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
400
427
  gl.bindBuffer(gl.ARRAY_BUFFER, instances);
@@ -424,7 +451,7 @@ export class Mesh {
424
451
  const value = constant.value;
425
452
  applyConstant(gl, constant.location, value);
426
453
  }
427
- gl.drawElementsInstanced(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_INT, 0, count);
454
+ gl.drawElementsInstanced(gl.TRIANGLES, this.indexCount, this.indexType, 0, count);
428
455
  }
429
456
 
430
457
  dispose(gl: WebGL2RenderingContext): void {
@@ -468,24 +495,49 @@ export class Mesh {
468
495
  }
469
496
  }
470
497
 
498
+ /**
499
+ * One attribute's buffer, as floats or in its sixteen-bit fixed point (`vertexPacking.ts`), which
500
+ * WebGL2 reads back through a normalised `SHORT` or `UNSIGNED_SHORT`: three lanes wide where
501
+ * WebGPU needs four, so a normal costs six bytes here.
502
+ */
471
503
  private attachAttribute(
472
504
  gl: WebGL2RenderingContext,
473
505
  location: number,
474
506
  data: Float32Array,
475
507
  size: number,
476
508
  usage: number = gl.STATIC_DRAW,
509
+ packing: AttributePacking = 'float',
477
510
  ): WebGLBuffer {
478
511
  const buffer = gl.createBuffer();
479
512
  if (buffer === null) throw new Error('createBuffer failed');
480
513
  this.buffers.push(buffer);
481
514
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
482
- this.allocate(gl, gl.ARRAY_BUFFER, buffer, data, usage);
515
+ const fixed = packing === 'snorm16' || packing === 'unorm16';
516
+ this.allocate(gl, gl.ARRAY_BUFFER, buffer, fixed ? fixedArray(data, packing) : data, usage);
517
+ const type =
518
+ packing === 'snorm16' ? gl.SHORT : packing === 'unorm16' ? gl.UNSIGNED_SHORT : gl.FLOAT;
483
519
  gl.enableVertexAttribArray(location);
484
- gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
485
- this.layout.push({ location, buffer, size });
520
+ gl.vertexAttribPointer(location, size, type, fixed, 0, 0);
521
+ this.layout.push({ location, buffer, size, type, normalized: fixed });
486
522
  return buffer;
487
523
  }
488
524
 
525
+ /** A buffer, or the one value every vertex shares as a constant, as the packing says. */
526
+ private attachShared(
527
+ gl: WebGL2RenderingContext,
528
+ location: number,
529
+ data: Float32Array,
530
+ size: number,
531
+ packing: AttributePacking,
532
+ ): void {
533
+ if (packing !== 'constant') {
534
+ this.attachAttribute(gl, location, data, size, gl.STATIC_DRAW, packing);
535
+ return;
536
+ }
537
+ gl.disableVertexAttribArray(location);
538
+ this.constants.push({ location, value: sharedValue(data, size) });
539
+ }
540
+
489
541
  /**
490
542
  * Give a bound buffer its storage, and its contents now or later.
491
543
  *
@@ -50,6 +50,9 @@ export type ShadowFilterTaps = 4 | 8 | 12;
50
50
  export type DirectionalShadowDepthLayers = 1 | 2;
51
51
  export type WaterReflectionFilterTaps = 1 | 5 | 9;
52
52
 
53
+ /** What a draw does when its pipeline has not compiled yet: see `RenderQuality.pipelineCompile`. */
54
+ export type PipelineCompile = 'wait' | 'skip';
55
+
53
56
  /** How light through glass is coloured: see `RenderQuality.glassShadows`. */
54
57
  export type GlassShadows = 'full' | 'half' | 'off';
55
58
 
@@ -220,6 +223,34 @@ export interface RenderQuality {
220
223
  * lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
221
224
  */
222
225
  readonly glassShadows: GlassShadows;
226
+ /**
227
+ * What a lit draw does when the pipeline it needs has not compiled: one first needed inside a
228
+ * frame, by a material's own state — two-sided, cut out, a surface model — or a draw state a
229
+ * mesh's creation did not warm.
230
+ *
231
+ * **`'wait'`, the default**: the pipeline is built where the draw asks for it, and on WebGPU the
232
+ * browser compiles it on its GPU process's main thread, holding the frame — 130 to 400 ms a
233
+ * pipeline was reported on a phone, for a material first drawn mid-scene. **`'skip'`** starts the
234
+ * compile off that thread and leaves the draw out until it lands, a few frames to a few dozen,
235
+ * and the draw then appears; nothing waits. Static draws always wait, since a recorded list keeps
236
+ * what it recorded, and a compile that fails is built where it is next asked for, so the device
237
+ * says why rather than the draw vanishing for good.
238
+ *
239
+ * **The way to have neither** is to draw what a scene will use once, behind a loading screen, and
240
+ * await `ready()`, which waits for every compile started — whichever this says. What would make
241
+ * `'skip'` wrong is a frame that must hold everything it was given, such as a capture of the first
242
+ * frame of a scene not awaited.
243
+ *
244
+ * **Measured on a desktop GPU** (`demo/dev/pipelineCompile.html`, eight new variants at once): on
245
+ * WebGPU the frame that met them took 700 to 3,700 ms waiting and 17 ms skipping, the draws
246
+ * landing 21 to 26 frames later. WebGL2 compiles its lit programs when it is made, all but a
247
+ * surface model's, which it compiles at the first draw that wants one: there `'skip'` asks the
248
+ * driver through `KHR_parallel_shader_compile` and waits where a context does not offer it — which
249
+ * ANGLE on Vulkan does not. Where it is offered, ANGLE on OpenGL, the longest frame went from 820 to
250
+ * 1,120 ms to 630 to 700 ms: the driver still finishes its own compile at the program's first
251
+ * draw, which nothing above it can move.
252
+ */
253
+ readonly pipelineCompile: PipelineCompile;
223
254
  /**
224
255
  * How light that scatters beneath skin is drawn, for a material whose model is `skinModel`.
225
256
  *
@@ -973,6 +1004,7 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
973
1004
  lightVolumeSamples: 32,
974
1005
  directionalShadowDepthLayers: 2,
975
1006
  glassShadows: 'full',
1007
+ pipelineCompile: 'wait',
976
1008
  skinScattering: 'pre-integrated',
977
1009
  directionalShadowMaxDistance: 6,
978
1010
  directionalShadowMaxSlope: 3,
@@ -1140,6 +1172,7 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1140
1172
  directionalShadowDepthLayers:
1141
1173
  options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
1142
1174
  glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
1175
+ pipelineCompile: options.pipelineCompile ?? DEFAULT_RENDER_QUALITY.pipelineCompile,
1143
1176
  skinScattering: options.skinScattering ?? DEFAULT_RENDER_QUALITY.skinScattering,
1144
1177
  directionalShadowMaxDistance:
1145
1178
  options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
@@ -1298,6 +1331,11 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1298
1331
  `RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`,
1299
1332
  );
1300
1333
  }
1334
+ if (quality.pipelineCompile !== 'wait' && quality.pipelineCompile !== 'skip') {
1335
+ throw new Error(
1336
+ `RenderQuality.pipelineCompile must be 'wait' or 'skip', got ${String(quality.pipelineCompile)}`,
1337
+ );
1338
+ }
1301
1339
  if (quality.skinScattering !== 'pre-integrated' && quality.skinScattering !== 'screen-space') {
1302
1340
  throw new Error(
1303
1341
  `RenderQuality.skinScattering must be 'pre-integrated' or 'screen-space', got ${String(quality.skinScattering)}`,
@@ -58,8 +58,14 @@ export function recordingGl(
58
58
  * recording.
59
59
  */
60
60
  readonly uniforms?: readonly string[];
61
+ /**
62
+ * Whether a program started under `KHR_parallel_shader_compile` has finished, which the test
63
+ * flips: `COMPLETION_STATUS_KHR` answers `done`. Finished by default, as every program here is.
64
+ */
65
+ readonly compiling?: { done: boolean };
61
66
  } = {},
62
67
  ) {
68
+ const compiling = options.compiling ?? { done: true };
63
69
  const extensions = new Set(options.extensions ?? []);
64
70
  const uniforms = options.uniforms ?? [];
65
71
  /* What `clearDepth` last set, so a module that borrows the value can be seen to put it back. */
@@ -136,6 +142,13 @@ export function recordingGl(
136
142
  HALF_FLOAT: 0x140b,
137
143
  DEPTH_COMPONENT: 0x1902,
138
144
  UNSIGNED_INT: 0x1405,
145
+ /* Named so a mesh's index width and each attribute's type can be read back: the buffer its
146
+ indices fill, the type every draw of it names, and how each attribute is read. See
147
+ `mesh.test.ts`. */
148
+ UNSIGNED_SHORT: 0x1403,
149
+ SHORT: 0x1402,
150
+ FLOAT: 0x1406,
151
+ ELEMENT_ARRAY_BUFFER: 0x8893,
139
152
  LINEAR: 0x2601,
140
153
  COLOR_BUFFER_BIT: 0x00004000,
141
154
  /* Named so a test can tell a mesh that declared itself deforming from one that did not. */
@@ -173,6 +186,7 @@ export function recordingGl(
173
186
  if (name === 'EXT_clip_control') {
174
187
  return { LOWER_LEFT_EXT: 0x8ca1, ZERO_TO_ONE_EXT: 0x935f, clipControlEXT: () => undefined };
175
188
  }
189
+ if (name === 'KHR_parallel_shader_compile') return { COMPLETION_STATUS_KHR: 0x91b1 };
176
190
  return {};
177
191
  },
178
192
  getShaderParameter: () => true,
@@ -200,6 +214,7 @@ export function recordingGl(
200
214
  /* Linked yes; zero active uniforms and attributes, which ends every reflection loop at once
201
215
  and is why this harness does not have to know a single uniform name. */
202
216
  getProgramParameter: (program: unknown, pname: unknown) => {
217
+ if (pname === 0x91b1) return compiling.done;
203
218
  if (pname === constants.ACTIVE_UNIFORMS) return uniforms.length;
204
219
  if (pname === constants.ACTIVE_ATTRIBUTES) return 0;
205
220
  if (pname !== constants.LINK_STATUS) return true;
@@ -26,6 +26,65 @@ export function compileProgram(
26
26
  return program;
27
27
  }
28
28
 
29
+ /**
30
+ * A program compiled and linked without asking whether that worked, since asking waits for the
31
+ * driver to finish: for a context offering `KHR_parallel_shader_compile`, whose
32
+ * `COMPLETION_STATUS_KHR` says when asking no longer waits. `finishProgram` asks then, and fails
33
+ * as loudly as `compileProgram` does; the stages are kept until it does, for their logs.
34
+ */
35
+ export interface StartedProgram {
36
+ readonly program: WebGLProgram;
37
+ readonly vertex: WebGLShader;
38
+ readonly fragment: WebGLShader;
39
+ readonly label: string;
40
+ }
41
+
42
+ export function startProgram(
43
+ gl: WebGL2RenderingContext,
44
+ vertexSource: string,
45
+ fragmentSource: string,
46
+ label: string,
47
+ ): StartedProgram {
48
+ const stage = (type: number, source: string, name: string): WebGLShader => {
49
+ const shader = gl.createShader(type);
50
+ if (shader === null) throw new Error(`[${label}.${name}] createShader failed`);
51
+ gl.shaderSource(shader, source);
52
+ gl.compileShader(shader);
53
+ return shader;
54
+ };
55
+ const vertex = stage(gl.VERTEX_SHADER, vertexSource, 'vert');
56
+ const fragment = stage(gl.FRAGMENT_SHADER, fragmentSource, 'frag');
57
+ const program = gl.createProgram();
58
+ if (program === null) throw new Error(`[${label}] createProgram failed`);
59
+ gl.attachShader(program, vertex);
60
+ gl.attachShader(program, fragment);
61
+ gl.linkProgram(program);
62
+ return { program, vertex, fragment, label };
63
+ }
64
+
65
+ /** A started program, checked as `compileProgram` checks one: its stages, then its link. */
66
+ export function finishProgram(gl: WebGL2RenderingContext, started: StartedProgram): WebGLProgram {
67
+ const { program, vertex, fragment, label } = started;
68
+ const fail = (what: string, log: string | null): never => {
69
+ gl.deleteShader(vertex);
70
+ gl.deleteShader(fragment);
71
+ gl.deleteProgram(program);
72
+ throw new Error(`[${label}${what}] ${log ?? 'no log'}`);
73
+ };
74
+ if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS)) {
75
+ fail('.vert compile failed:', gl.getShaderInfoLog(vertex));
76
+ }
77
+ if (!gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
78
+ fail('.frag compile failed:', gl.getShaderInfoLog(fragment));
79
+ }
80
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
81
+ fail(' link failed:', gl.getProgramInfoLog(program));
82
+ }
83
+ gl.deleteShader(vertex);
84
+ gl.deleteShader(fragment);
85
+ return program;
86
+ }
87
+
29
88
  /**
30
89
  * Whether a uniform lookup that cannot reach the GPU is reported.
31
90
  *
@@ -36,13 +36,56 @@ export const DIRECTIONALSHADOW_GLSL = `vec2 receiverPlaneDepthGradient(vec3 p) {
36
36
  );
37
37
  }
38
38
 
39
- float directionalVisibility(float receiverDepth, float compareDepth, float storedDepth) {
39
+ /* One stored depth against a receiver, in a map spanning \`span\` metres of depth. */
40
+ float directionalVisibilityAt(float receiverDepth, float compareDepth, float storedDepth, float span) {
40
41
  if (compareDepth <= storedDepth) return 1.0;
41
- float rayDistance = max(receiverDepth - storedDepth, 0.0) * uShadowDepthSpan;
42
+ float rayDistance = max(receiverDepth - storedDepth, 0.0) * span;
42
43
  float groundDistance = rayDistance * length(uDirectionalDir.xz);
43
44
  return 1.0 - shadowReach(groundDistance, uShadowMaxDistance);
44
45
  }
45
46
 
47
+ float directionalVisibility(float receiverDepth, float compareDepth, float storedDepth) {
48
+ return directionalVisibilityAt(receiverDepth, compareDepth, storedDepth, uShadowDepthSpan);
49
+ }
50
+
51
+ /* How far inside its square a receiver stands in a layer's map: 1 well inside, 0 at its border. */
52
+ float sunMapFade(vec3 p) {
53
+ if (p.z > 1.0 || p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) return 0.0;
54
+ vec2 fromCentre = abs(p.xy - 0.5) * 2.0;
55
+ return (1.0 - smoothstep(0.72, 0.98, max(fromCentre.x, fromCentre.y))) *
56
+ (1.0 - smoothstep(0.90, 1.0, p.z));
57
+ }
58
+
59
+ /*
60
+ * The moving layer at its own place, where it has a matrix of its own (movingSun.ts): the coarse
61
+ * test of shadowFactor, and its filter, each against the moving map's own depth span, and lit by
62
+ * how far inside the moving map's square the receiver stands rather than cut at its border.
63
+ */
64
+ float movingSunCoarse(vec3 pm, float span) {
65
+ float inside = sunMapFade(pm);
66
+ if (inside <= 0.0) return 1.0;
67
+ float stored = textureLod(uSunShadows, vec3(pm.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
68
+ return mix(1.0, directionalVisibilityAt(pm.z, pm.z - 1.0 / span, stored, span), inside);
69
+ }
70
+
71
+ float movingSunFiltered(vec3 pm, vec2 gradient, float span) {
72
+ float inside = sunMapFade(pm);
73
+ if (inside <= 0.0) return 1.0;
74
+ float texel = 1.35 / uShadowMapSize;
75
+ float slopeLimit = 9.1 / uShadowMapSize;
76
+ float lit = 0.0;
77
+ for (int i = 0; i < ${MAX_SHADOW_FILTER_TAPS}; i++) {
78
+ if (i >= uShadowFilterTaps) break;
79
+ vec2 sampleUv = pm.xy + DIRECTIONAL_PCF_OFFSETS[i] * texel;
80
+ vec2 texelCentre = (floor(sampleUv * uShadowMapSize) + 0.5) / uShadowMapSize;
81
+ float slopeOffset = clamp(dot(gradient, texelCentre - pm.xy), -slopeLimit, slopeLimit);
82
+ float tapReceiverDepth = pm.z + slopeOffset;
83
+ float stored = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
84
+ lit += directionalVisibilityAt(tapReceiverDepth, tapReceiverDepth - 0.14 / span, stored, span);
85
+ }
86
+ return mix(1.0, lit / float(max(uShadowFilterTaps, 1)), inside);
87
+ }
88
+
46
89
  float shadowFactor(float directionalNdl) {
47
90
  if (uShadowStrength <= 0.0) return 1.0;
48
91
 
@@ -72,6 +115,14 @@ float shadowFactor(float directionalNdl) {
72
115
  vec3 p = vLightPos.xyz / vLightPos.w;
73
116
  p = p * 0.5 + 0.5;
74
117
  vec2 depthGradient = receiverPlaneDepthGradient(p);
118
+ /* The moving layer's own place and plane where it has a matrix of its own (movingSun.ts), taken
119
+ here beside the static layer's for the same reason: above every varying-dependent return. */
120
+ vec3 pm = p;
121
+ vec2 movingGradient = depthGradient;
122
+ if (MOVING_SUN) {
123
+ pm = movingSunAt();
124
+ movingGradient = receiverPlaneDepthGradient(pm);
125
+ }
75
126
 
76
127
  // Near a surface's own light terminator its direct-light term is already
77
128
  // approaching zero, while the orthographic projection of that surface
@@ -136,7 +187,8 @@ float shadowFactor(float directionalNdl) {
136
187
  float coarseCompare = p.z - 1.0 / uShadowDepthSpan;
137
188
  float coarse =
138
189
  directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_STATIC_LAYER}.0), 0.0).r) *
139
- directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r);
190
+ (MOVING_SUN ? movingSunCoarse(pm, movingSunSpan()) :
191
+ directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_DYNAMIC_LAYER}.0), 0.0).r));
140
192
  if (uPeeledShadowEnabled != 0) {
141
193
  coarse *= directionalVisibility(p.z, coarseCompare, textureLod(uSunShadows, vec3(p.xy, ${SUN_PEELED_LAYER}.0), 0.0).r);
142
194
  }
@@ -200,7 +252,6 @@ float shadowFactor(float directionalNdl) {
200
252
  * there is no mip to select and level zero is the only thing \`texture\` could have read.
201
253
  */
202
254
  float staticDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_STATIC_LAYER}.0), 0.0).r;
203
- float dynamicDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
204
255
  staticLit += directionalVisibility(tapReceiverDepth, compare, staticDepth);
205
256
  if (uPeeledShadowEnabled != 0) {
206
257
  float peeledDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_PEELED_LAYER}.0), 0.0).r;
@@ -208,12 +259,18 @@ float shadowFactor(float directionalNdl) {
208
259
  } else {
209
260
  peeledLit += 1.0;
210
261
  }
211
- dynamicLit += directionalVisibility(tapReceiverDepth, compare, dynamicDepth);
262
+ /* The moving layer here where it shares the static one's place; at its own below otherwise.
263
+ Branched on the switch itself, never its negation: see overridableConstants in transform.mjs. */
264
+ if (MOVING_SUN) {
265
+ } else {
266
+ float dynamicDepth = textureLod(uSunShadows, vec3(sampleUv, ${SUN_DYNAMIC_LAYER}.0), 0.0).r;
267
+ dynamicLit += directionalVisibility(tapReceiverDepth, compare, dynamicDepth);
268
+ }
212
269
  }
213
270
  float tapCount = float(max(uShadowFilterTaps, 1));
214
271
  staticLit /= tapCount;
215
272
  peeledLit /= tapCount;
216
- dynamicLit /= tapCount;
273
+ dynamicLit = MOVING_SUN ? movingSunFiltered(pm, movingGradient, movingSunSpan()) : dynamicLit / tapCount;
217
274
 
218
275
  // Independent layers preserve every recorded fact at an overlap: each
219
276
  // caster keeps its own distance fade, and their transmissions compose
@@ -234,11 +291,11 @@ float shadowFactor(float directionalNdl) {
234
291
  * behind the receiver lending it colour within the spread, which only a receiver between two panes
235
292
  * meets. One function for both layers, because two copies were most of the lit shaders' growth.
236
293
  */
237
- vec3 sunGlassLayer(vec3 p, float paneLayer, float tintLayer, float bias, float texel, float uvPerMetre) {
294
+ vec3 sunGlassLayer(vec3 p, float paneLayer, float tintLayer, float bias, float texel, float uvPerMetre, float span) {
238
295
  float tintSize = float(textureSize(uSunGlassTints, 0).x);
239
296
  float clarity = textureLod(uSunGlassTints, vec3(p.xy, tintLayer), 0.0).a;
240
297
  float behind =
241
- max(p.z - textureLod(uSunShadows, vec3(p.xy, paneLayer), 0.0).r, 0.0) * uShadowDepthSpan;
298
+ max(p.z - textureLod(uSunShadows, vec3(p.xy, paneLayer), 0.0).r, 0.0) * span;
242
299
  float radius = clamp(
243
300
  (1.0 - clarity) * ${FROST_SPREAD.toFixed(6)} * behind * uvPerMetre,
244
301
  texel,
@@ -278,6 +335,13 @@ vec3 sunGlassLookup(float directionalNdl) {
278
335
  /* Map units per metre along the receiver, from derivatives taken first in uniform control flow,
279
336
  so a frost spread stated in metres lands the same whatever size of light matrix a consumer built. */
280
337
  float uvPerMetre = length(dFdx(p.xy)) / max(length(dFdx(vWorldPos)), 1e-6);
338
+ /* The moving glass at its own place where its layer has a matrix of its own: movingSun.ts. */
339
+ vec3 pm = p;
340
+ float movingPerMetre = uvPerMetre;
341
+ if (MOVING_SUN) {
342
+ pm = movingSunAt();
343
+ movingPerMetre = length(dFdx(pm.xy)) / max(length(dFdx(vWorldPos)), 1e-6);
344
+ }
281
345
  if (uShadowStrength <= 0.0 || textureSize(uSunGlassTints, 0).x <= 1) return vec3(1.0);
282
346
  if (directionalNdl <= 0.0) return vec3(1.0);
283
347
  if (p.z > 1.0 || p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) return vec3(1.0);
@@ -295,8 +359,18 @@ vec3 sunGlassLookup(float directionalNdl) {
295
359
 
296
360
  float bias = 0.14 / uShadowDepthSpan;
297
361
  float texel = 1.35 / uShadowMapSize;
298
- vec3 staticTint = sunGlassLayer(p, ${SUN_GLASS_STATIC_LAYER}.0, 0.0, bias, texel, uvPerMetre);
299
- vec3 movingTint = sunGlassLayer(p, ${SUN_GLASS_MOVING_LAYER}.0, 1.0, bias, texel, uvPerMetre);
362
+ vec3 staticTint = sunGlassLayer(p, ${SUN_GLASS_STATIC_LAYER}.0, 0.0, bias, texel, uvPerMetre, uShadowDepthSpan);
363
+ vec3 movingTint = MOVING_SUN && sunMapFade(pm) <= 0.0
364
+ ? vec3(1.0)
365
+ : sunGlassLayer(
366
+ pm,
367
+ ${SUN_GLASS_MOVING_LAYER}.0,
368
+ 1.0,
369
+ MOVING_SUN ? 0.14 / movingSunSpan() : bias,
370
+ texel,
371
+ movingPerMetre,
372
+ MOVING_SUN ? movingSunSpan() : uShadowDepthSpan
373
+ );
300
374
  return mix(vec3(1.0), staticTint * movingTint, strength);
301
375
  }
302
376
 
@@ -36,7 +36,10 @@ export type LitSwitch =
36
36
  | 'DRIFT_LIGHT'
37
37
  | 'PHYSICAL_SPECULAR'
38
38
  | 'SURFACE_OVERLAY'
39
- | 'REFLECTION_MAPS';
39
+ | 'REFLECTION_MAPS'
40
+ | 'WORLD_UVS'
41
+ | 'MOVING_SUN'
42
+ | 'LAYERED';
40
43
 
41
44
  /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
42
45
  export function noLitSwitches(): Record<LitSwitch, boolean> {
@@ -49,6 +52,9 @@ export function noLitSwitches(): Record<LitSwitch, boolean> {
49
52
  PHYSICAL_SPECULAR: false,
50
53
  SURFACE_OVERLAY: false,
51
54
  REFLECTION_MAPS: false,
55
+ WORLD_UVS: false,
56
+ MOVING_SUN: false,
57
+ LAYERED: false,
52
58
  };
53
59
  }
54
60
 
@@ -21,6 +21,10 @@ import { DRIFT_LIGHT_GLSL } from './driftLight.ts';
21
21
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.ts';
22
22
  import { overlayGlsl } from './overlay.ts';
23
23
  import { reflectionSurfaceGlsl } from './reflectionSurface.ts';
24
+ import { worldUvGlsl } from './worldUv.ts';
25
+ import { movingSunGlsl } from './movingSun.ts';
26
+ import { layeredGlsl } from './layered.ts';
27
+ import { MAP_READS_GLSL } from './mapReads.ts';
24
28
  import { CHANNEL_BEND, LIT_CHANNEL_ATTRIBUTE } from '../vertexChannel.ts';
25
29
  import { BONE_ANIMATION_GLSL } from '../boneAnimation.ts';
26
30
  import { FULL_LIGHT_BUDGET, type LightBudget } from '../../uniformVectorBudget.ts';
@@ -508,6 +512,21 @@ export interface FlatShaderOptions {
508
512
  * the frame's materials' reflections. True when absent. See `reflectionSurfaceGlsl`.
509
513
  */
510
514
  readonly reflectionMaps?: boolean;
515
+ /**
516
+ * Whether it places a material's maps by the world, `uWorldUv` and all (`SurfaceMaterial
517
+ * .projection`). True when absent. See `worldUvGlsl`.
518
+ */
519
+ readonly worldUvs?: boolean;
520
+ /**
521
+ * Whether it reads the sun's moving layer through a matrix of its own, \`uMovingLightViewProj\`
522
+ * and all. True when absent. See \`movingSunGlsl\`.
523
+ */
524
+ readonly movingSun?: boolean;
525
+ /**
526
+ * Whether it blends a material's layers by a mask, \`uLayers\` and all (\`SurfaceMaterial.layers\`).
527
+ * True when absent. See \`layeredGlsl\`.
528
+ */
529
+ readonly layered?: boolean;
511
530
  /**
512
531
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
513
532
  *
@@ -619,6 +638,8 @@ export function flatFrag(options: FlatShaderOptions): string {
619
638
  surfaceEffects: options.surfaceEffects ?? true,
620
639
  driftLight: options.driftLight ?? true,
621
640
  }),
641
+ /* Depth zero and ahead of the sun's lookup, which calls it; its switch takes the next id. */
642
+ movingSunGlsl(options.movingSun ?? true),
622
643
  LOBES_GLSL,
623
644
  POINTSHADOW_GLSL,
624
645
  DIRECTIONALSHADOW_GLSL,
@@ -644,8 +665,6 @@ export function flatFrag(options: FlatShaderOptions): string {
644
665
  * and `main` is unconditional by construction.
645
666
  */
646
667
  TANGENT_FRAME_GLSL,
647
- /* After the frame it calls, unconditional for the same reason. See normalMap.ts. */
648
- NORMAL_MAP_GLSL,
649
668
  /* Unconditional for the same reason, and called only where a cutoff was asked for. */
650
669
  CUTOUT_COVERAGE_GLSL,
651
670
  CUTOUT_DITHER_GLSL,
@@ -657,6 +676,15 @@ export function flatFrag(options: FlatShaderOptions): string {
657
676
  overlayGlsl(options.surfaceOverlay ?? true),
658
677
  /* After the overlay, so its switch takes the next id. See reflectionSurface.ts. */
659
678
  reflectionSurfaceGlsl(options.reflectionMaps ?? true),
679
+ /* After the reflection maps, so its switch takes the next id; main calls it. worldUv.ts. */
680
+ worldUvGlsl(options.worldUvs ?? true),
681
+ /* After the projection, so its switch takes the next id; the normal map below calls it. */
682
+ layeredGlsl(options.layered ?? true),
683
+ /* Where each map is read, after both of the ways it can be read otherwise. See mapReads.ts. */
684
+ MAP_READS_GLSL,
685
+ /* After the frame and the projection it calls, unconditional for the same reason as the
686
+ frame. See normalMap.ts. */
687
+ NORMAL_MAP_GLSL,
660
688
  /* After the models, whose answer to a rectangle it asks for. See areaLight.ts. */
661
689
  AREA_LIGHT_GLSL,
662
690
  MAIN_GLSL,