@driftengine/core 4.11.0 → 4.12.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 (77) hide show
  1. package/CHANGELOG.json +72 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.js +1 -1
  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 +61 -5
  9. package/dist/render/backend/webgl2/renderer.js +153 -21
  10. package/dist/render/backend/webgpu/renderer.d.ts +54 -1
  11. package/dist/render/backend/webgpu/renderer.js +246 -89
  12. package/dist/render/instances.d.ts +13 -1
  13. package/dist/render/instances.js +10 -0
  14. package/dist/render/occlusionFade.d.ts +21 -0
  15. package/dist/render/occlusionFade.js +27 -0
  16. package/dist/render/sceneTarget.d.ts +2 -0
  17. package/dist/render/sceneTarget.js +1 -1
  18. package/dist/render/shaders/ambientOcclusion.js +12 -1
  19. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  20. package/dist/render/shaders/flat/areaLight.js +1 -1
  21. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  22. package/dist/render/shaders/flat/glassTint.js +1 -0
  23. package/dist/render/shaders/flat/index.d.ts +5 -0
  24. package/dist/render/shaders/flat/index.js +10 -3
  25. package/dist/render/shaders/flat/layerLooks.d.ts +16 -0
  26. package/dist/render/shaders/flat/layerLooks.js +116 -0
  27. package/dist/render/shaders/flat/layered.d.ts +19 -9
  28. package/dist/render/shaders/flat/layered.js +169 -37
  29. package/dist/render/shaders/flat/main.d.ts +1 -1
  30. package/dist/render/shaders/flat/main.js +17 -5
  31. package/dist/render/shaders/flat/models.js +1 -1
  32. package/dist/render/shaders/flat/normalMap.d.ts +1 -1
  33. package/dist/render/shaders/flat/normalMap.js +11 -2
  34. package/dist/render/shaders/flat/preamble.js +6 -0
  35. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
  36. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
  37. package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -48
  38. package/dist/render/shaders/generated/flat.wgsl.js +215 -87
  39. package/dist/render/surfaceLayerLooks.d.ts +80 -0
  40. package/dist/render/surfaceLayerLooks.js +83 -0
  41. package/dist/render/surfaceLayers.d.ts +128 -19
  42. package/dist/render/surfaceLayers.js +73 -13
  43. package/dist/render/surfaceTexture.d.ts +13 -4
  44. package/dist/render/transmission.d.ts +21 -0
  45. package/dist/render/transmission.js +21 -0
  46. package/dist/ui/splash.d.ts +42 -2
  47. package/dist/ui/splash.js +78 -13
  48. package/package.json +6 -6
  49. package/scripts/browser.d.mts +5 -0
  50. package/scripts/browser.mjs +68 -14
  51. package/scripts/look.d.mts +81 -0
  52. package/scripts/look.mjs +380 -0
  53. package/src/index.ts +4 -0
  54. package/src/render/ambientOcclusionPass.ts +4 -1
  55. package/src/render/backend/api.ts +31 -0
  56. package/src/render/backend/webgl2/renderer.ts +170 -22
  57. package/src/render/backend/webgpu/renderer.ts +379 -139
  58. package/src/render/instances.ts +18 -1
  59. package/src/render/occlusionFade.ts +29 -0
  60. package/src/render/sceneTarget.ts +3 -0
  61. package/src/render/shaders/ambientOcclusion.ts +12 -1
  62. package/src/render/shaders/flat/areaLight.ts +1 -1
  63. package/src/render/shaders/flat/glassTint.ts +3 -1
  64. package/src/render/shaders/flat/index.ts +15 -3
  65. package/src/render/shaders/flat/layerLooks.ts +116 -0
  66. package/src/render/shaders/flat/layered.ts +170 -37
  67. package/src/render/shaders/flat/main.ts +17 -5
  68. package/src/render/shaders/flat/models.ts +1 -1
  69. package/src/render/shaders/flat/normalMap.ts +11 -2
  70. package/src/render/shaders/flat/preamble.ts +6 -0
  71. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
  72. package/src/render/shaders/generated/flat.wgsl.ts +215 -87
  73. package/src/render/surfaceLayerLooks.ts +155 -0
  74. package/src/render/surfaceLayers.ts +187 -23
  75. package/src/render/surfaceTexture.ts +13 -4
  76. package/src/render/transmission.ts +27 -0
  77. package/src/ui/splash.ts +83 -14
@@ -59,7 +59,7 @@ import type {
59
59
  SkyColors,
60
60
  TranslucentMeshOptions,
61
61
  } from '../webgl2/renderer.ts';
62
- import type { IncrementalMeshHandle, MeshOptions } from '../api.ts';
62
+ import type { IncrementalMeshHandle, MeshOptions, PrepareOptions } from '../api.ts';
63
63
  import { resolveAtmosphere, type ResolvedAtmosphere } from '../../atmosphere.ts';
64
64
  import { AMBIENT_SH_FLOATS, packAmbientSH } from '../../ambientHarmonics.ts';
65
65
  import {
@@ -197,9 +197,18 @@ import {
197
197
  type SurfaceOverlay,
198
198
  } from '../../surfaceOverlay.ts';
199
199
  import { PROJECTION_FLOATS, packSurfaceProjection } from '../../surfaceProjection.ts';
200
+ import { resolveTransmission } from '../../transmission.ts';
201
+ import { resolveOcclusionFade } from '../../occlusionFade.ts';
200
202
  import { orthographicDepthSpan } from '../../lightMatrix.ts';
201
- import { LAYER_FLOATS, packSurfaceLayers } from '../../surfaceLayers.ts';
203
+ import {
204
+ LAYER_FLOATS,
205
+ layerMaskMap,
206
+ layeredProjection,
207
+ layersFitBeside,
208
+ packSurfaceLayers,
209
+ } from '../../surfaceLayers.ts';
202
210
  import type { SurfaceLayers } from '../../surfaceLayers.ts';
211
+ import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.ts';
203
212
  import {
204
213
  frameReflectionsRefused,
205
214
  resolveFrameReflections,
@@ -756,6 +765,12 @@ const MAX_OVERLAYS = 64;
756
765
  */
757
766
  const MAX_SKY_DRAWS = 64;
758
767
 
768
+ /** The draw a preparation asks pipelines for: every option at its default. See `prepareMesh`. */
769
+ const PREPARED_DRAW: TranslucentMeshOptions = {};
770
+ /** A preparation's morph states for a mesh with no targets, and for one with some. */
771
+ const NOT_MORPHED: readonly boolean[] = [false];
772
+ const MORPHED_OR_NOT: readonly boolean[] = [false, true];
773
+
759
774
  /**
760
775
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
761
776
  *
@@ -2347,6 +2362,8 @@ export class WebGPURenderer implements RendererApi {
2347
2362
 
2348
2363
  private readonly aoStaging = new ArrayBuffer(AO_FRAG_SIZE);
2349
2364
  private readonly aoFloats = new Float32Array(this.aoStaging);
2365
+ /** Where occlusion fades out with distance, `uFade`: none until a caller sets one. */
2366
+ private readonly aoFade = new Float32Array([-1, 0]);
2350
2367
  private aoBindGroup: GPUBindGroup | null = null;
2351
2368
  private readonly aoBlurLayout: GPUBindGroupLayout;
2352
2369
  /**
@@ -2999,7 +3016,15 @@ export class WebGPURenderer implements RendererApi {
2999
3016
  private readonly projection = new Float32Array(PROJECTION_FLOATS);
3000
3017
  /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
3001
3018
  private readonly layerFloats = new Float32Array(LAYER_FLOATS);
3019
+ /** `uLayerLooks`, the material's layer picks and looks packed by `packLayerLooks`. */
3020
+ private readonly lookFloats = new Float32Array(LOOK_FLOATS);
3021
+ /**
3022
+ * `uGlassTint` as the material leaves it, its light from behind and that light's colour, which a
3023
+ * pane overwrites for its own draw and `releaseSeeThrough` puts back. See `transmission.ts`.
3024
+ */
3025
+ private readonly transmission = new Float32Array([-1, -1, -1, 0]);
3002
3026
  private warnedLayersModelMap = false;
3027
+ private warnedLayersTriplanar = false;
3003
3028
  /** Its numbers, `uModelParams`, packed by `packModel`. */
3004
3029
  private readonly modelParams = new Float32Array(MODEL_PARAM_FLOATS);
3005
3030
  /**
@@ -3475,9 +3500,9 @@ export class WebGPURenderer implements RendererApi {
3475
3500
  const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
3476
3501
  const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
3477
3502
  const ownModelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
3478
- /* A layered material's mask goes where a model's map would: see `SurfaceMaterial.layers`. */
3503
+ /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
3479
3504
  const layers = this.layersOf(material);
3480
- const modelMap = ownModelMap ?? ((layers?.mask ?? null) as GpuSurfaceTexture | null);
3505
+ const modelMap = ownModelMap ?? layerMaskMap(layers);
3481
3506
  this.materialModel = material?.model ?? null;
3482
3507
  const reflectivity = material?.reflectivity;
3483
3508
  const environmentGain = material?.environmentGain;
@@ -3554,16 +3579,22 @@ export class WebGPURenderer implements RendererApi {
3554
3579
  /* The first physical highlight asked for is when the lit pipelines start carrying one. */
3555
3580
  if (material?.physicalSpecular === true) this.enableLit('PHYSICAL_SPECULAR');
3556
3581
  /* Maps placed by the world: the first material that asks is when lit pipelines carry it. */
3557
- packSurfaceProjection(material?.projection, this.projection);
3582
+ const projection = layeredProjection(material?.projection, layers);
3583
+ if (projection !== (material?.projection ?? null)) this.warnLayersTriplanar();
3584
+ packSurfaceProjection(projection, this.projection);
3558
3585
  f.set(this.projection, this.materialField('uWorldUv'));
3559
- /* How much light from behind it lets through, in the glass tint's spare lane. */
3560
- f[this.materialField('uGlassTint') + 3] =
3561
- Math.min(Math.max(material?.diffuseTransmission ?? 0, 0), 1) || 0;
3586
+ /* How much light from behind it lets through, and its colour: transmission.ts. */
3587
+ resolveTransmission(material, this.transmission);
3588
+ f.set(this.transmission, this.materialField('uGlassTint'));
3562
3589
  if ((this.projection[0] as number) > 0) this.enableLit('WORLD_UVS');
3563
3590
  /* And its layers, the first time a material blends any: layered.ts. */
3564
3591
  packSurfaceLayers(layers, this.layerFloats);
3565
3592
  f.set(this.layerFloats, this.materialField('uLayers'));
3566
3593
  if (layers !== null) this.enableLit('LAYERED');
3594
+ /* And which layer of shared arrays each reads, with its look: layerLooks.ts. */
3595
+ packLayerLooks(layers, this.lookFloats);
3596
+ f.set(this.lookFloats, this.materialField('uLayerLooks'));
3597
+ if (asksLayerLooks(layers)) this.enableLit('LAYER_LOOKS');
3567
3598
  f.set(this.modelParams, this.materialField('uModelParams'));
3568
3599
  });
3569
3600
  /*
@@ -6325,17 +6356,28 @@ export class WebGPURenderer implements RendererApi {
6325
6356
  ): SurfaceLayers<GpuSurfaceTexture> | null {
6326
6357
  const layers = material?.layers ?? null;
6327
6358
  if (layers === null || layers.repeats.length === 0) return null;
6328
- if ((material?.modelMap ?? null) === null) return layers;
6359
+ if (layersFitBeside(layers, material?.modelMap)) return layers;
6329
6360
  if (!this.warnedLayersModelMap) {
6330
6361
  this.warnedLayersModelMap = true;
6331
6362
  console.warn(
6332
- "WebGPU: a material's layers are drawn as one: its mask is read where a model's map goes, " +
6333
- 'and this material carries a `modelMap` too.',
6363
+ "WebGPU: a material's layers are drawn as one: its mask is a map, read where a model's map " +
6364
+ "goes, and this material carries a `modelMap` too. A mask in the ORM array (`mask: 'orm'`) " +
6365
+ "or the vertex colour (`mask: 'vertex'`) leaves the slot free.",
6334
6366
  );
6335
6367
  }
6336
6368
  return null;
6337
6369
  }
6338
6370
 
6371
+ /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
6372
+ private warnLayersTriplanar(): void {
6373
+ if (this.warnedLayersTriplanar) return;
6374
+ this.warnedLayersTriplanar = true;
6375
+ console.warn(
6376
+ "WebGPU: a layered material's triplanar projection is laid on the horizontal plane: every " +
6377
+ "layer's maps on three planes would be three times the reads. See `SurfaceMaterial.layers`.",
6378
+ );
6379
+ }
6380
+
6339
6381
  /**
6340
6382
  * What the sun's moving layer is read through: the matrix its shadow pass was last drawn with,
6341
6383
  * where the lit pipelines carry one (`MOVING_SUN`); the environment's otherwise, which every caller
@@ -8937,9 +8979,11 @@ export class WebGPURenderer implements RendererApi {
8937
8979
  /* And no projection: maps follow the mesh's coordinates until a material asks otherwise. */
8938
8980
  f.fill(0, at('uWorldUv'), at('uWorldUv') + 4);
8939
8981
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
8940
- f[at('uGlassTint') + 3] = 0;
8982
+ resolveTransmission(null, this.transmission);
8983
+ f.set(this.transmission, at('uGlassTint'));
8941
8984
  /* Nor layers: one layer until a material asks for more. */
8942
8985
  f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
8986
+ f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
8943
8987
  f[at('uCutout')] = 0;
8944
8988
  f[at('uCutout') + 1] = 0;
8945
8989
  this.cutoutResolveStaged = 'hard';
@@ -9732,6 +9776,225 @@ export class WebGPURenderer implements RendererApi {
9732
9776
  );
9733
9777
  }
9734
9778
 
9779
+ /**
9780
+ * Compile off the main thread every pipeline a draw of `mesh` in `material` takes. **The other
9781
+ * backend's `prepareMesh` carries the contract**; here a draw's key is `meshShape`'s, so a prepared
9782
+ * key is exactly the one the draw asks for, and the compiles are started as a skipping draw starts
9783
+ * them (`PipelineCache.lit`) whatever `pipelineCompile` says, into the frame's own caches.
9784
+ */
9785
+ prepareMesh(
9786
+ mesh: GpuMesh,
9787
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
9788
+ options: PrepareOptions = {},
9789
+ ): void {
9790
+ if (this.surface.lost) return;
9791
+ this.preparing(
9792
+ material,
9793
+ (into, blend) => {
9794
+ const skinned = mesh.isSkinned;
9795
+ const eight = skinned && mesh.isSkinnedEight;
9796
+ for (const morphed of mesh.morph === null ? NOT_MORPHED : MORPHED_OR_NOT) {
9797
+ this.meshShape(mesh, blend, PREPARED_DRAW, skinned, morphed, eight, false);
9798
+ this.meshPipeline(blend, false, skinned, morphed, eight, false, into);
9799
+ }
9800
+ },
9801
+ options,
9802
+ );
9803
+ }
9804
+
9805
+ /** `prepareMesh` for an instanced batch. See the other backend's `prepareInstanced`. */
9806
+ prepareInstanced(
9807
+ batch: GpuInstancedBatch,
9808
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
9809
+ options: PrepareOptions = {},
9810
+ ): void {
9811
+ if (this.surface.lost) return;
9812
+ this.preparing(
9813
+ material,
9814
+ (into, blend) => {
9815
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
9816
+ this.instancedPipeline(
9817
+ batch.mesh,
9818
+ batch.animation !== null,
9819
+ blend,
9820
+ false,
9821
+ PREPARED_DRAW,
9822
+ true,
9823
+ 0,
9824
+ coverage,
9825
+ into,
9826
+ );
9827
+ },
9828
+ options,
9829
+ );
9830
+ }
9831
+
9832
+ /**
9833
+ * `material` set, every cache a draw in it may build into passed to `start` with whether that draw
9834
+ * blends, compiles started rather than built (`PipelineCache.lit`), and then no material set and
9835
+ * each cache back to what `pipelineCompile` says.
9836
+ */
9837
+ private preparing(
9838
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
9839
+ start: (into: PipelineCache, blend: boolean) => void,
9840
+ options: PrepareOptions,
9841
+ ): void {
9842
+ this.setMaterial(material);
9843
+ this.skipCompiling(true);
9844
+ try {
9845
+ start(this.pipelines, false);
9846
+ if (options.translucent === true) {
9847
+ start(this.pipelines, true);
9848
+ /* A blended draw in a reconstructing frame lands in the late pass, whose cache is its own. */
9849
+ if (this.latePipelines !== this.pipelines) start(this.latePipelines, true);
9850
+ }
9851
+ } finally {
9852
+ this.skipCompiling(this.quality.pipelineCompile === 'skip');
9853
+ this.setMaterial(null);
9854
+ }
9855
+ }
9856
+
9857
+ /** What `meshShape` worked out for the mesh draw at hand. Reused, so a draw allocates no record. */
9858
+ private readonly drawShape = {
9859
+ base: '',
9860
+ key: '',
9861
+ depthWrite: true,
9862
+ layer: 0,
9863
+ adds: false,
9864
+ coverage: false,
9865
+ splits: false,
9866
+ modelKind: null as SurfaceModelKind | null,
9867
+ };
9868
+
9869
+ /**
9870
+ * The key a mesh draw's pipeline is cached under, and the pipeline state that key encodes, under
9871
+ * the current material and these options, into `drawShape`. **The one statement of it**:
9872
+ * `submitMesh` draws by it and `prepareMesh` compiles by it, so the two cannot disagree about a key.
9873
+ */
9874
+ private meshShape(
9875
+ mesh: GpuMesh,
9876
+ blend: boolean,
9877
+ options: TranslucentMeshOptions,
9878
+ skinned: boolean,
9879
+ morphed: boolean,
9880
+ eight: boolean,
9881
+ clothed: boolean,
9882
+ ): WebGPURenderer['drawShape'] {
9883
+ const modelKind = this.materialModel?.kind ?? null;
9884
+ const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
9885
+ const keyed =
9886
+ `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
9887
+ `${clothed ? '|cloth' : ''}`;
9888
+ /*
9889
+ * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
9890
+ * the key: two draws of one mesh differing only in these would otherwise share a cached
9891
+ * pipeline and the second would silently take the first one's depth behaviour.
9892
+ *
9893
+ * **Both suffixes are empty for the defaults**, which is what keeps the keys `createMesh`
9894
+ * warms — `fullKey` and `${'`'}${'$'}{fullKey}|blend${'`'}` — the ones an ordinary draw still asks for. A
9895
+ * caller that opts in pays one compile on its first draw, the same bargain the overlay
9896
+ * pipelines above already make.
9897
+ */
9898
+ const depthWrite = options.depthWrite ?? true;
9899
+ const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
9900
+ /* Added rather than blended over: its own blend state, so its own pipeline. */
9901
+ const adds = blend && options.additive === true;
9902
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
9903
+ /* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
9904
+ const splits = modelKind === 'skin' && !blend && this.skinSplits();
9905
+ const key =
9906
+ `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
9907
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
9908
+ /* The two order-independent buffers are two formats and two blend states, so a draw into
9909
+ one must never share a cached pipeline with the same mesh drawn into the other. */
9910
+ `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
9911
+ /* Culling is pipeline state here: a two-sided surface culls nothing. */
9912
+ `${this.materialDoubleSided ? '|2s' : ''}` +
9913
+ /* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
9914
+ `${coverage ? '|a2c' : ''}` +
9915
+ /* And the material's shading model, which is the fragment stage's own switch. */
9916
+ `${modelKind === null ? '' : `|m:${modelKind}`}` +
9917
+ /* And which half of a skin it draws, under the screen-space blur. */
9918
+ `${splits ? '|ss' : ''}`;
9919
+
9920
+ const shape = this.drawShape;
9921
+ shape.base = base;
9922
+ shape.key = key;
9923
+ shape.depthWrite = depthWrite;
9924
+ shape.layer = layer;
9925
+ shape.adds = adds;
9926
+ shape.coverage = coverage;
9927
+ shape.splits = splits;
9928
+ shape.modelKind = modelKind;
9929
+ return shape;
9930
+ }
9931
+
9932
+ /**
9933
+ * The pipeline `drawShape` names: the one built, one built here, or none where draws skip a
9934
+ * compile and this one has not landed (`PipelineCache.lit`) — and none for a split skin until all
9935
+ * of its halves have.
9936
+ */
9937
+ private meshPipeline(
9938
+ blend: boolean,
9939
+ lands: boolean,
9940
+ skinned: boolean,
9941
+ morphed: boolean,
9942
+ eight: boolean,
9943
+ clothed: boolean,
9944
+ /** Which cache to build into: the one the draw's target reads, unless a preparation says. */
9945
+ into: PipelineCache | null = null,
9946
+ ): GPURenderPipeline | null {
9947
+ const { base, key, depthWrite, layer, adds, coverage, splits, modelKind } = this.drawShape;
9948
+ /*
9949
+ * **Asynchronous by default, synchronous if it has to be, and never absent.**
9950
+ *
9951
+ * `createMesh` compiles off the main thread, so a draw can legitimately arrive before the
9952
+ * driver has finished — a consumer that awaits `ready()` never sees this, and one that
9953
+ * draws immediately gets the old behaviour rather than an exception. Correctness cannot be
9954
+ * allowed to depend on which of those happened, so the miss builds the pipeline here and
9955
+ * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
9956
+ * thrown error in the middle of one.
9957
+ */
9958
+ const pipelines = into ?? (blend ? this.blendedPipelines(lands) : this.targetPipelines());
9959
+ let pipeline: GPURenderPipeline | null = pipelines.peek(key) ?? null;
9960
+ if (pipeline === null) {
9961
+ const present = this.meshPresent.get(base);
9962
+ if (present === undefined) {
9963
+ throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
9964
+ }
9965
+ pipeline = flatPipeline(
9966
+ pipelines,
9967
+ this.surface.device,
9968
+ this.flatLayoutFor(skinned, morphed),
9969
+ this.variant,
9970
+ key,
9971
+ present,
9972
+ adds ? 'additive' : blend,
9973
+ skinned,
9974
+ morphed,
9975
+ depthWrite,
9976
+ layer,
9977
+ false,
9978
+ this.oitMode,
9979
+ this.materialDoubleSided,
9980
+ coverage,
9981
+ eight,
9982
+ clothed,
9983
+ modelKind,
9984
+ splits ? 'scene' : 'whole',
9985
+ );
9986
+ }
9987
+ /* A split skin draws its three halves or none of them. */
9988
+ if (
9989
+ pipeline !== null &&
9990
+ splits &&
9991
+ !this.skinHalvesReady(key, base, skinned, morphed, layer, eight, clothed)
9992
+ ) {
9993
+ pipeline = null;
9994
+ }
9995
+ return pipeline;
9996
+ }
9997
+
9735
9998
  /**
9736
9999
  * @param blend Which of the two entry points above this came through, and **not** something to
9737
10000
  * infer from `opacity`.
@@ -9857,7 +10120,6 @@ export class WebGPURenderer implements RendererApi {
9857
10120
  /* An eye's axis is the draw's, written into the open material. See `eyeAxisInWorld`. */
9858
10121
  const modelKind = this.materialModel?.kind ?? null;
9859
10122
  if (modelKind === 'eye') this.writeEyeAxis(model, this.skinPaletteSlot >= 0 && mesh.isSkinned);
9860
- const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
9861
10123
  /*
9862
10124
  * A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
9863
10125
  * key carries the flag so the two never share a cached pipeline. A rigged mesh whose caller
@@ -9872,41 +10134,6 @@ export class WebGPURenderer implements RendererApi {
9872
10134
  /* A cloth set and a skinned draw: the pipeline built with CLOTH_BOUND on, warmed when the
9873
10135
  binding was made. See `createClothBinding`. */
9874
10136
  const clothed = skinned && this.cloth !== null;
9875
- const keyed =
9876
- `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
9877
- `${clothed ? '|cloth' : ''}`;
9878
- /*
9879
- * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
9880
- * the key: two draws of one mesh differing only in these would otherwise share a cached
9881
- * pipeline and the second would silently take the first one's depth behaviour.
9882
- *
9883
- * **Both suffixes are empty for the defaults**, which is what keeps the keys `createMesh`
9884
- * warms — `fullKey` and `${'`'}${'$'}{fullKey}|blend${'`'}` — the ones an ordinary draw still asks for. A
9885
- * caller that opts in pays one compile on its first draw, the same bargain the overlay
9886
- * pipelines above already make.
9887
- */
9888
- const depthWrite = options.depthWrite ?? true;
9889
- const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
9890
- /* Added rather than blended over: its own blend state, so its own pipeline. */
9891
- const adds = blend && options.additive === true;
9892
- const coverage = !blend && this.cutoutResolveStaged === 'coverage';
9893
- /* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
9894
- const splits = modelKind === 'skin' && !blend && this.skinSplits();
9895
- const key =
9896
- `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
9897
- `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
9898
- /* The two order-independent buffers are two formats and two blend states, so a draw into
9899
- one must never share a cached pipeline with the same mesh drawn into the other. */
9900
- `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
9901
- /* Culling is pipeline state here: a two-sided surface culls nothing. */
9902
- `${this.materialDoubleSided ? '|2s' : ''}` +
9903
- /* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
9904
- `${coverage ? '|a2c' : ''}` +
9905
- /* And the material's shading model, which is the fragment stage's own switch. */
9906
- `${modelKind === null ? '' : `|m:${modelKind}`}` +
9907
- /* And which half of a skin it draws, under the screen-space blur. */
9908
- `${splits ? '|ss' : ''}`;
9909
-
9910
10137
  /*
9911
10138
  * The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
9912
10139
  * field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
@@ -9919,53 +10146,17 @@ export class WebGPURenderer implements RendererApi {
9919
10146
  this.perDraw.writeInt(slot, fields.uMorphTargetCount.offset, mesh.morph.targetCount);
9920
10147
  this.perDraw.writeInt(slot, fields.uMorphTextureWidth.offset, mesh.morph.width);
9921
10148
  }
9922
- /*
9923
- * **Asynchronous by default, synchronous if it has to be, and never absent.**
9924
- *
9925
- * `createMesh` compiles off the main thread, so a draw can legitimately arrive before the
9926
- * driver has finished — a consumer that awaits `ready()` never sees this, and one that
9927
- * draws immediately gets the old behaviour rather than an exception. Correctness cannot be
9928
- * allowed to depend on which of those happened, so the miss builds the pipeline here and
9929
- * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
9930
- * thrown error in the middle of one.
9931
- */
9932
- const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
9933
- let pipeline: GPURenderPipeline | null = pipelines.peek(key) ?? null;
9934
- if (pipeline === null) {
9935
- const present = this.meshPresent.get(base);
9936
- if (present === undefined) {
9937
- throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
9938
- }
9939
- pipeline = flatPipeline(
9940
- pipelines,
9941
- this.surface.device,
9942
- this.flatLayoutFor(skinned, morphed),
9943
- this.variant,
9944
- key,
9945
- present,
9946
- adds ? 'additive' : blend,
9947
- skinned,
9948
- morphed,
9949
- depthWrite,
9950
- layer,
9951
- false,
9952
- this.oitMode,
9953
- this.materialDoubleSided,
9954
- coverage,
9955
- eight,
9956
- clothed,
9957
- modelKind,
9958
- splits ? 'scene' : 'whole',
9959
- );
9960
- }
9961
- /* A split skin draws its three halves or none of them. */
9962
- if (
9963
- pipeline !== null &&
9964
- splits &&
9965
- !this.skinHalvesReady(key, base, skinned, morphed, layer, eight, clothed)
9966
- ) {
9967
- pipeline = null;
9968
- }
10149
+ /* Its pipeline, by the key a prepared draw compiled too: see `meshShape`. */
10150
+ const { base, key, layer, depthWrite, splits } = this.meshShape(
10151
+ mesh,
10152
+ blend,
10153
+ options,
10154
+ skinned,
10155
+ morphed,
10156
+ eight,
10157
+ clothed,
10158
+ );
10159
+ let pipeline = this.meshPipeline(blend, lands, skinned, morphed, eight, clothed);
9969
10160
 
9970
10161
  /*
9971
10162
  * Guards the draw call only, not the restore below.
@@ -10356,6 +10547,8 @@ export class WebGPURenderer implements RendererApi {
10356
10547
  const seeAt = this.materialField('uSeeThrough');
10357
10548
  this.perFrameFloats[seeAt] = 0;
10358
10549
  this.perFrameFloats[seeAt + 2] = 0;
10550
+ /* The material's light through, over the pane's tint: transmission.ts. */
10551
+ this.perFrameFloats.set(this.transmission, this.materialField('uGlassTint'));
10359
10552
  }
10360
10553
 
10361
10554
  /**
@@ -10370,6 +10563,76 @@ export class WebGPURenderer implements RendererApi {
10370
10563
  * **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
10371
10564
  * is built, and a per-instance opacity: `opacity` here dims the whole batch.
10372
10565
  */
10566
+ /**
10567
+ * An instanced draw's pipeline under the current material and these options: the one built, one
10568
+ * built here, or none while it compiles (`PipelineCache.lit`), its key, base and model left in
10569
+ * `drawShape`. **The one statement of an instanced key**: `submitInstanced` draws by it and
10570
+ * `prepareInstanced` compiles by it.
10571
+ */
10572
+ private instancedPipeline(
10573
+ mesh: GpuMesh,
10574
+ animated: boolean,
10575
+ blend: boolean,
10576
+ lands: boolean,
10577
+ options: TranslucentMeshOptions,
10578
+ depthWrite: boolean,
10579
+ layer: number,
10580
+ coverage: boolean,
10581
+ /** Which cache to build into: the one the draw's target reads, unless a preparation says. */
10582
+ into: PipelineCache | null = null,
10583
+ ): GPURenderPipeline | null {
10584
+ /* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
10585
+ const modelKind = this.materialModel?.kind ?? null;
10586
+ const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
10587
+ /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
10588
+ a third vertex buffer and a different vertex module, and sharing a cache entry with the
10589
+ plain one would hand a draw the wrong layout. */
10590
+ const adds = blend && options.additive === true;
10591
+ const key =
10592
+ `${base}|inst${!animated ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
10593
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
10594
+ `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
10595
+ `${modelKind === null ? '' : `|m:${modelKind}`}`;
10596
+
10597
+ const shape = this.drawShape;
10598
+ shape.base = base;
10599
+ shape.key = key;
10600
+ shape.modelKind = modelKind;
10601
+ const pipelines = into ?? (blend ? this.blendedPipelines(lands) : this.targetPipelines());
10602
+ let pipeline: GPURenderPipeline | null = pipelines.peek(key) ?? null;
10603
+ if (pipeline === null) {
10604
+ const present = this.meshPresent.get(base);
10605
+ if (present === undefined) {
10606
+ throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
10607
+ }
10608
+ pipeline = flatPipeline(
10609
+ pipelines,
10610
+ this.surface.device,
10611
+ !animated ? this.flatLayoutFor(false, false) : this.animatedBindGroupLayout,
10612
+ this.variant,
10613
+ key,
10614
+ present,
10615
+ adds ? 'additive' : blend,
10616
+ false,
10617
+ false,
10618
+ depthWrite,
10619
+ layer,
10620
+ true,
10621
+ 'none',
10622
+ this.materialDoubleSided,
10623
+ coverage,
10624
+ false,
10625
+ false,
10626
+ modelKind,
10627
+ 'whole',
10628
+ false,
10629
+ animated,
10630
+ );
10631
+ }
10632
+
10633
+ return pipeline;
10634
+ }
10635
+
10373
10636
  /**
10374
10637
  * The next of a batch's cull slots this frame, with its cull queued against the current view; null
10375
10638
  * when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
@@ -10488,50 +10751,18 @@ export class WebGPURenderer implements RendererApi {
10488
10751
  const own =
10489
10752
  ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
10490
10753
  if (own) this.materials.dirty();
10491
- /* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
10492
- const modelKind = this.materialModel?.kind ?? null;
10493
- const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
10494
- /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
10495
- a third vertex buffer and a different vertex module, and sharing a cache entry with the
10496
- plain one would hand a draw the wrong layout. */
10497
- const adds = blend && options.additive === true;
10498
- const key =
10499
- `${base}|inst${animation === null ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
10500
- `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
10501
- `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
10502
- `${modelKind === null ? '' : `|m:${modelKind}`}`;
10503
-
10504
- const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
10505
- let pipeline: GPURenderPipeline | null = pipelines.peek(key) ?? null;
10506
- if (pipeline === null) {
10507
- const present = this.meshPresent.get(base);
10508
- if (present === undefined) {
10509
- throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
10510
- }
10511
- pipeline = flatPipeline(
10512
- pipelines,
10513
- this.surface.device,
10514
- animation === null ? this.flatLayoutFor(false, false) : this.animatedBindGroupLayout,
10515
- this.variant,
10516
- key,
10517
- present,
10518
- adds ? 'additive' : blend,
10519
- false,
10520
- false,
10521
- depthWrite,
10522
- layer,
10523
- true,
10524
- 'none',
10525
- this.materialDoubleSided,
10526
- coverage,
10527
- false,
10528
- false,
10529
- modelKind,
10530
- 'whole',
10531
- false,
10532
- animation !== null,
10533
- );
10534
- }
10754
+ /* Its pipeline, by the key a prepared batch compiled too: see `instancedPipeline`. */
10755
+ const pipeline = this.instancedPipeline(
10756
+ mesh,
10757
+ animation !== null,
10758
+ blend,
10759
+ lands,
10760
+ options,
10761
+ depthWrite,
10762
+ layer,
10763
+ coverage,
10764
+ );
10765
+ const { base, key, modelKind } = this.drawShape;
10535
10766
 
10536
10767
  /* A batch whose pipeline is still compiling is left out, as a mesh is. */
10537
10768
  const material = pipeline === null ? null : this.materialSlotForDraw();
@@ -14106,6 +14337,7 @@ export class WebGPURenderer implements RendererApi {
14106
14337
  const f = this.aoFloats;
14107
14338
  const at = (name: string): number => this.postField(AO_FRAG_FIELDS, name);
14108
14339
  f[at('uRadius')] = this.quality.ambientOcclusionRadius;
14340
+ f.set(this.aoFade, at('uFade'));
14109
14341
  f.set(this.aoProjScale, at('uProjScale'));
14110
14342
  f.set(this.aoInvProjection, at('uInvProjection'));
14111
14343
  device.queue.writeBuffer(this.aoUniforms, 0, this.aoStaging);
@@ -17007,6 +17239,14 @@ export class WebGPURenderer implements RendererApi {
17007
17239
  this.rebuildRushBindGroup();
17008
17240
  }
17009
17241
 
17242
+ /**
17243
+ * Where ambient occlusion fades out with distance. **The other backend's
17244
+ * `setAmbientOcclusionFade` carries the reasoning**; this is the same setter over the same state.
17245
+ */
17246
+ setAmbientOcclusionFade(distance: number, radius: number): void {
17247
+ resolveOcclusionFade(distance, radius, this.aoFade);
17248
+ }
17249
+
17010
17250
  /**
17011
17251
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
17012
17252
  * to take. **The other backend's `setDepthOfField` carries the reasoning**, whole: the