@driftengine/core 4.11.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 (56) hide show
  1. package/CHANGELOG.json +36 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +2 -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 +45 -4
  9. package/dist/render/backend/webgl2/renderer.js +112 -19
  10. package/dist/render/backend/webgpu/renderer.d.ts +52 -1
  11. package/dist/render/backend/webgpu/renderer.js +237 -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/index.js +9 -2
  20. package/dist/render/shaders/flat/layered.d.ts +15 -7
  21. package/dist/render/shaders/flat/layered.js +114 -31
  22. package/dist/render/shaders/flat/main.d.ts +1 -1
  23. package/dist/render/shaders/flat/main.js +10 -3
  24. package/dist/render/shaders/flat/normalMap.d.ts +1 -1
  25. package/dist/render/shaders/flat/normalMap.js +11 -2
  26. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
  27. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
  28. package/dist/render/shaders/generated/flat.wgsl.d.ts +48 -48
  29. package/dist/render/shaders/generated/flat.wgsl.js +71 -71
  30. package/dist/render/surfaceLayers.d.ts +83 -15
  31. package/dist/render/surfaceLayers.js +69 -13
  32. package/dist/render/surfaceTexture.d.ts +13 -4
  33. package/dist/render/transmission.d.ts +21 -0
  34. package/dist/render/transmission.js +21 -0
  35. package/dist/ui/splash.d.ts +42 -2
  36. package/dist/ui/splash.js +78 -13
  37. package/package.json +6 -6
  38. package/src/index.ts +2 -0
  39. package/src/render/ambientOcclusionPass.ts +4 -1
  40. package/src/render/backend/api.ts +31 -0
  41. package/src/render/backend/webgl2/renderer.ts +130 -20
  42. package/src/render/backend/webgpu/renderer.ts +371 -139
  43. package/src/render/instances.ts +18 -1
  44. package/src/render/occlusionFade.ts +29 -0
  45. package/src/render/sceneTarget.ts +3 -0
  46. package/src/render/shaders/ambientOcclusion.ts +12 -1
  47. package/src/render/shaders/flat/index.ts +9 -2
  48. package/src/render/shaders/flat/layered.ts +114 -31
  49. package/src/render/shaders/flat/main.ts +10 -3
  50. package/src/render/shaders/flat/normalMap.ts +11 -2
  51. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
  52. package/src/render/shaders/generated/flat.wgsl.ts +71 -71
  53. package/src/render/surfaceLayers.ts +138 -19
  54. package/src/render/surfaceTexture.ts +13 -4
  55. package/src/render/transmission.ts +27 -0
  56. 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,8 +197,16 @@ 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';
203
211
  import {
204
212
  frameReflectionsRefused,
@@ -756,6 +764,12 @@ const MAX_OVERLAYS = 64;
756
764
  */
757
765
  const MAX_SKY_DRAWS = 64;
758
766
 
767
+ /** The draw a preparation asks pipelines for: every option at its default. See `prepareMesh`. */
768
+ const PREPARED_DRAW: TranslucentMeshOptions = {};
769
+ /** A preparation's morph states for a mesh with no targets, and for one with some. */
770
+ const NOT_MORPHED: readonly boolean[] = [false];
771
+ const MORPHED_OR_NOT: readonly boolean[] = [false, true];
772
+
759
773
  /**
760
774
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
761
775
  *
@@ -2347,6 +2361,8 @@ export class WebGPURenderer implements RendererApi {
2347
2361
 
2348
2362
  private readonly aoStaging = new ArrayBuffer(AO_FRAG_SIZE);
2349
2363
  private readonly aoFloats = new Float32Array(this.aoStaging);
2364
+ /** Where occlusion fades out with distance, `uFade`: none until a caller sets one. */
2365
+ private readonly aoFade = new Float32Array([-1, 0]);
2350
2366
  private aoBindGroup: GPUBindGroup | null = null;
2351
2367
  private readonly aoBlurLayout: GPUBindGroupLayout;
2352
2368
  /**
@@ -2999,7 +3015,13 @@ export class WebGPURenderer implements RendererApi {
2999
3015
  private readonly projection = new Float32Array(PROJECTION_FLOATS);
3000
3016
  /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
3001
3017
  private readonly layerFloats = new Float32Array(LAYER_FLOATS);
3018
+ /**
3019
+ * `uGlassTint` as the material leaves it, its light from behind and that light's colour, which a
3020
+ * pane overwrites for its own draw and `releaseSeeThrough` puts back. See `transmission.ts`.
3021
+ */
3022
+ private readonly transmission = new Float32Array([-1, -1, -1, 0]);
3002
3023
  private warnedLayersModelMap = false;
3024
+ private warnedLayersTriplanar = false;
3003
3025
  /** Its numbers, `uModelParams`, packed by `packModel`. */
3004
3026
  private readonly modelParams = new Float32Array(MODEL_PARAM_FLOATS);
3005
3027
  /**
@@ -3475,9 +3497,9 @@ export class WebGPURenderer implements RendererApi {
3475
3497
  const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
3476
3498
  const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
3477
3499
  const ownModelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
3478
- /* A layered material's mask goes where a model's map would: see `SurfaceMaterial.layers`. */
3500
+ /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
3479
3501
  const layers = this.layersOf(material);
3480
- const modelMap = ownModelMap ?? ((layers?.mask ?? null) as GpuSurfaceTexture | null);
3502
+ const modelMap = ownModelMap ?? layerMaskMap(layers);
3481
3503
  this.materialModel = material?.model ?? null;
3482
3504
  const reflectivity = material?.reflectivity;
3483
3505
  const environmentGain = material?.environmentGain;
@@ -3554,11 +3576,13 @@ export class WebGPURenderer implements RendererApi {
3554
3576
  /* The first physical highlight asked for is when the lit pipelines start carrying one. */
3555
3577
  if (material?.physicalSpecular === true) this.enableLit('PHYSICAL_SPECULAR');
3556
3578
  /* Maps placed by the world: the first material that asks is when lit pipelines carry it. */
3557
- packSurfaceProjection(material?.projection, this.projection);
3579
+ const projection = layeredProjection(material?.projection, layers);
3580
+ if (projection !== (material?.projection ?? null)) this.warnLayersTriplanar();
3581
+ packSurfaceProjection(projection, this.projection);
3558
3582
  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;
3583
+ /* How much light from behind it lets through, and its colour: transmission.ts. */
3584
+ resolveTransmission(material, this.transmission);
3585
+ f.set(this.transmission, this.materialField('uGlassTint'));
3562
3586
  if ((this.projection[0] as number) > 0) this.enableLit('WORLD_UVS');
3563
3587
  /* And its layers, the first time a material blends any: layered.ts. */
3564
3588
  packSurfaceLayers(layers, this.layerFloats);
@@ -6325,17 +6349,28 @@ export class WebGPURenderer implements RendererApi {
6325
6349
  ): SurfaceLayers<GpuSurfaceTexture> | null {
6326
6350
  const layers = material?.layers ?? null;
6327
6351
  if (layers === null || layers.repeats.length === 0) return null;
6328
- if ((material?.modelMap ?? null) === null) return layers;
6352
+ if (layersFitBeside(layers, material?.modelMap)) return layers;
6329
6353
  if (!this.warnedLayersModelMap) {
6330
6354
  this.warnedLayersModelMap = true;
6331
6355
  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.',
6356
+ "WebGPU: a material's layers are drawn as one: its mask is a map, read where a model's map " +
6357
+ "goes, and this material carries a `modelMap` too. A mask in the ORM array (`mask: 'orm'`) " +
6358
+ "or the vertex colour (`mask: 'vertex'`) leaves the slot free.",
6334
6359
  );
6335
6360
  }
6336
6361
  return null;
6337
6362
  }
6338
6363
 
6364
+ /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
6365
+ private warnLayersTriplanar(): void {
6366
+ if (this.warnedLayersTriplanar) return;
6367
+ this.warnedLayersTriplanar = true;
6368
+ console.warn(
6369
+ "WebGPU: a layered material's triplanar projection is laid on the horizontal plane: every " +
6370
+ "layer's maps on three planes would be three times the reads. See `SurfaceMaterial.layers`.",
6371
+ );
6372
+ }
6373
+
6339
6374
  /**
6340
6375
  * What the sun's moving layer is read through: the matrix its shadow pass was last drawn with,
6341
6376
  * where the lit pipelines carry one (`MOVING_SUN`); the environment's otherwise, which every caller
@@ -8937,7 +8972,8 @@ export class WebGPURenderer implements RendererApi {
8937
8972
  /* And no projection: maps follow the mesh's coordinates until a material asks otherwise. */
8938
8973
  f.fill(0, at('uWorldUv'), at('uWorldUv') + 4);
8939
8974
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
8940
- f[at('uGlassTint') + 3] = 0;
8975
+ resolveTransmission(null, this.transmission);
8976
+ f.set(this.transmission, at('uGlassTint'));
8941
8977
  /* Nor layers: one layer until a material asks for more. */
8942
8978
  f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
8943
8979
  f[at('uCutout')] = 0;
@@ -9732,6 +9768,225 @@ export class WebGPURenderer implements RendererApi {
9732
9768
  );
9733
9769
  }
9734
9770
 
9771
+ /**
9772
+ * Compile off the main thread every pipeline a draw of `mesh` in `material` takes. **The other
9773
+ * backend's `prepareMesh` carries the contract**; here a draw's key is `meshShape`'s, so a prepared
9774
+ * key is exactly the one the draw asks for, and the compiles are started as a skipping draw starts
9775
+ * them (`PipelineCache.lit`) whatever `pipelineCompile` says, into the frame's own caches.
9776
+ */
9777
+ prepareMesh(
9778
+ mesh: GpuMesh,
9779
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
9780
+ options: PrepareOptions = {},
9781
+ ): void {
9782
+ if (this.surface.lost) return;
9783
+ this.preparing(
9784
+ material,
9785
+ (into, blend) => {
9786
+ const skinned = mesh.isSkinned;
9787
+ const eight = skinned && mesh.isSkinnedEight;
9788
+ for (const morphed of mesh.morph === null ? NOT_MORPHED : MORPHED_OR_NOT) {
9789
+ this.meshShape(mesh, blend, PREPARED_DRAW, skinned, morphed, eight, false);
9790
+ this.meshPipeline(blend, false, skinned, morphed, eight, false, into);
9791
+ }
9792
+ },
9793
+ options,
9794
+ );
9795
+ }
9796
+
9797
+ /** `prepareMesh` for an instanced batch. See the other backend's `prepareInstanced`. */
9798
+ prepareInstanced(
9799
+ batch: GpuInstancedBatch,
9800
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
9801
+ options: PrepareOptions = {},
9802
+ ): void {
9803
+ if (this.surface.lost) return;
9804
+ this.preparing(
9805
+ material,
9806
+ (into, blend) => {
9807
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
9808
+ this.instancedPipeline(
9809
+ batch.mesh,
9810
+ batch.animation !== null,
9811
+ blend,
9812
+ false,
9813
+ PREPARED_DRAW,
9814
+ true,
9815
+ 0,
9816
+ coverage,
9817
+ into,
9818
+ );
9819
+ },
9820
+ options,
9821
+ );
9822
+ }
9823
+
9824
+ /**
9825
+ * `material` set, every cache a draw in it may build into passed to `start` with whether that draw
9826
+ * blends, compiles started rather than built (`PipelineCache.lit`), and then no material set and
9827
+ * each cache back to what `pipelineCompile` says.
9828
+ */
9829
+ private preparing(
9830
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
9831
+ start: (into: PipelineCache, blend: boolean) => void,
9832
+ options: PrepareOptions,
9833
+ ): void {
9834
+ this.setMaterial(material);
9835
+ this.skipCompiling(true);
9836
+ try {
9837
+ start(this.pipelines, false);
9838
+ if (options.translucent === true) {
9839
+ start(this.pipelines, true);
9840
+ /* A blended draw in a reconstructing frame lands in the late pass, whose cache is its own. */
9841
+ if (this.latePipelines !== this.pipelines) start(this.latePipelines, true);
9842
+ }
9843
+ } finally {
9844
+ this.skipCompiling(this.quality.pipelineCompile === 'skip');
9845
+ this.setMaterial(null);
9846
+ }
9847
+ }
9848
+
9849
+ /** What `meshShape` worked out for the mesh draw at hand. Reused, so a draw allocates no record. */
9850
+ private readonly drawShape = {
9851
+ base: '',
9852
+ key: '',
9853
+ depthWrite: true,
9854
+ layer: 0,
9855
+ adds: false,
9856
+ coverage: false,
9857
+ splits: false,
9858
+ modelKind: null as SurfaceModelKind | null,
9859
+ };
9860
+
9861
+ /**
9862
+ * The key a mesh draw's pipeline is cached under, and the pipeline state that key encodes, under
9863
+ * the current material and these options, into `drawShape`. **The one statement of it**:
9864
+ * `submitMesh` draws by it and `prepareMesh` compiles by it, so the two cannot disagree about a key.
9865
+ */
9866
+ private meshShape(
9867
+ mesh: GpuMesh,
9868
+ blend: boolean,
9869
+ options: TranslucentMeshOptions,
9870
+ skinned: boolean,
9871
+ morphed: boolean,
9872
+ eight: boolean,
9873
+ clothed: boolean,
9874
+ ): WebGPURenderer['drawShape'] {
9875
+ const modelKind = this.materialModel?.kind ?? null;
9876
+ const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
9877
+ const keyed =
9878
+ `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
9879
+ `${clothed ? '|cloth' : ''}`;
9880
+ /*
9881
+ * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
9882
+ * the key: two draws of one mesh differing only in these would otherwise share a cached
9883
+ * pipeline and the second would silently take the first one's depth behaviour.
9884
+ *
9885
+ * **Both suffixes are empty for the defaults**, which is what keeps the keys `createMesh`
9886
+ * warms — `fullKey` and `${'`'}${'$'}{fullKey}|blend${'`'}` — the ones an ordinary draw still asks for. A
9887
+ * caller that opts in pays one compile on its first draw, the same bargain the overlay
9888
+ * pipelines above already make.
9889
+ */
9890
+ const depthWrite = options.depthWrite ?? true;
9891
+ const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
9892
+ /* Added rather than blended over: its own blend state, so its own pipeline. */
9893
+ const adds = blend && options.additive === true;
9894
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
9895
+ /* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
9896
+ const splits = modelKind === 'skin' && !blend && this.skinSplits();
9897
+ const key =
9898
+ `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
9899
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
9900
+ /* The two order-independent buffers are two formats and two blend states, so a draw into
9901
+ one must never share a cached pipeline with the same mesh drawn into the other. */
9902
+ `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
9903
+ /* Culling is pipeline state here: a two-sided surface culls nothing. */
9904
+ `${this.materialDoubleSided ? '|2s' : ''}` +
9905
+ /* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
9906
+ `${coverage ? '|a2c' : ''}` +
9907
+ /* And the material's shading model, which is the fragment stage's own switch. */
9908
+ `${modelKind === null ? '' : `|m:${modelKind}`}` +
9909
+ /* And which half of a skin it draws, under the screen-space blur. */
9910
+ `${splits ? '|ss' : ''}`;
9911
+
9912
+ const shape = this.drawShape;
9913
+ shape.base = base;
9914
+ shape.key = key;
9915
+ shape.depthWrite = depthWrite;
9916
+ shape.layer = layer;
9917
+ shape.adds = adds;
9918
+ shape.coverage = coverage;
9919
+ shape.splits = splits;
9920
+ shape.modelKind = modelKind;
9921
+ return shape;
9922
+ }
9923
+
9924
+ /**
9925
+ * The pipeline `drawShape` names: the one built, one built here, or none where draws skip a
9926
+ * compile and this one has not landed (`PipelineCache.lit`) — and none for a split skin until all
9927
+ * of its halves have.
9928
+ */
9929
+ private meshPipeline(
9930
+ blend: boolean,
9931
+ lands: boolean,
9932
+ skinned: boolean,
9933
+ morphed: boolean,
9934
+ eight: boolean,
9935
+ clothed: boolean,
9936
+ /** Which cache to build into: the one the draw's target reads, unless a preparation says. */
9937
+ into: PipelineCache | null = null,
9938
+ ): GPURenderPipeline | null {
9939
+ const { base, key, depthWrite, layer, adds, coverage, splits, modelKind } = this.drawShape;
9940
+ /*
9941
+ * **Asynchronous by default, synchronous if it has to be, and never absent.**
9942
+ *
9943
+ * `createMesh` compiles off the main thread, so a draw can legitimately arrive before the
9944
+ * driver has finished — a consumer that awaits `ready()` never sees this, and one that
9945
+ * draws immediately gets the old behaviour rather than an exception. Correctness cannot be
9946
+ * allowed to depend on which of those happened, so the miss builds the pipeline here and
9947
+ * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
9948
+ * thrown error in the middle of one.
9949
+ */
9950
+ const pipelines = into ?? (blend ? this.blendedPipelines(lands) : this.targetPipelines());
9951
+ let pipeline: GPURenderPipeline | null = pipelines.peek(key) ?? null;
9952
+ if (pipeline === null) {
9953
+ const present = this.meshPresent.get(base);
9954
+ if (present === undefined) {
9955
+ throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
9956
+ }
9957
+ pipeline = flatPipeline(
9958
+ pipelines,
9959
+ this.surface.device,
9960
+ this.flatLayoutFor(skinned, morphed),
9961
+ this.variant,
9962
+ key,
9963
+ present,
9964
+ adds ? 'additive' : blend,
9965
+ skinned,
9966
+ morphed,
9967
+ depthWrite,
9968
+ layer,
9969
+ false,
9970
+ this.oitMode,
9971
+ this.materialDoubleSided,
9972
+ coverage,
9973
+ eight,
9974
+ clothed,
9975
+ modelKind,
9976
+ splits ? 'scene' : 'whole',
9977
+ );
9978
+ }
9979
+ /* A split skin draws its three halves or none of them. */
9980
+ if (
9981
+ pipeline !== null &&
9982
+ splits &&
9983
+ !this.skinHalvesReady(key, base, skinned, morphed, layer, eight, clothed)
9984
+ ) {
9985
+ pipeline = null;
9986
+ }
9987
+ return pipeline;
9988
+ }
9989
+
9735
9990
  /**
9736
9991
  * @param blend Which of the two entry points above this came through, and **not** something to
9737
9992
  * infer from `opacity`.
@@ -9857,7 +10112,6 @@ export class WebGPURenderer implements RendererApi {
9857
10112
  /* An eye's axis is the draw's, written into the open material. See `eyeAxisInWorld`. */
9858
10113
  const modelKind = this.materialModel?.kind ?? null;
9859
10114
  if (modelKind === 'eye') this.writeEyeAxis(model, this.skinPaletteSlot >= 0 && mesh.isSkinned);
9860
- const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
9861
10115
  /*
9862
10116
  * A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
9863
10117
  * key carries the flag so the two never share a cached pipeline. A rigged mesh whose caller
@@ -9872,41 +10126,6 @@ export class WebGPURenderer implements RendererApi {
9872
10126
  /* A cloth set and a skinned draw: the pipeline built with CLOTH_BOUND on, warmed when the
9873
10127
  binding was made. See `createClothBinding`. */
9874
10128
  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
10129
  /*
9911
10130
  * The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
9912
10131
  * field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
@@ -9919,53 +10138,17 @@ export class WebGPURenderer implements RendererApi {
9919
10138
  this.perDraw.writeInt(slot, fields.uMorphTargetCount.offset, mesh.morph.targetCount);
9920
10139
  this.perDraw.writeInt(slot, fields.uMorphTextureWidth.offset, mesh.morph.width);
9921
10140
  }
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
- }
10141
+ /* Its pipeline, by the key a prepared draw compiled too: see `meshShape`. */
10142
+ const { base, key, layer, depthWrite, splits } = this.meshShape(
10143
+ mesh,
10144
+ blend,
10145
+ options,
10146
+ skinned,
10147
+ morphed,
10148
+ eight,
10149
+ clothed,
10150
+ );
10151
+ let pipeline = this.meshPipeline(blend, lands, skinned, morphed, eight, clothed);
9969
10152
 
9970
10153
  /*
9971
10154
  * Guards the draw call only, not the restore below.
@@ -10356,6 +10539,8 @@ export class WebGPURenderer implements RendererApi {
10356
10539
  const seeAt = this.materialField('uSeeThrough');
10357
10540
  this.perFrameFloats[seeAt] = 0;
10358
10541
  this.perFrameFloats[seeAt + 2] = 0;
10542
+ /* The material's light through, over the pane's tint: transmission.ts. */
10543
+ this.perFrameFloats.set(this.transmission, this.materialField('uGlassTint'));
10359
10544
  }
10360
10545
 
10361
10546
  /**
@@ -10370,6 +10555,76 @@ export class WebGPURenderer implements RendererApi {
10370
10555
  * **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
10371
10556
  * is built, and a per-instance opacity: `opacity` here dims the whole batch.
10372
10557
  */
10558
+ /**
10559
+ * An instanced draw's pipeline under the current material and these options: the one built, one
10560
+ * built here, or none while it compiles (`PipelineCache.lit`), its key, base and model left in
10561
+ * `drawShape`. **The one statement of an instanced key**: `submitInstanced` draws by it and
10562
+ * `prepareInstanced` compiles by it.
10563
+ */
10564
+ private instancedPipeline(
10565
+ mesh: GpuMesh,
10566
+ animated: boolean,
10567
+ blend: boolean,
10568
+ lands: boolean,
10569
+ options: TranslucentMeshOptions,
10570
+ depthWrite: boolean,
10571
+ layer: number,
10572
+ coverage: boolean,
10573
+ /** Which cache to build into: the one the draw's target reads, unless a preparation says. */
10574
+ into: PipelineCache | null = null,
10575
+ ): GPURenderPipeline | null {
10576
+ /* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
10577
+ const modelKind = this.materialModel?.kind ?? null;
10578
+ const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
10579
+ /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
10580
+ a third vertex buffer and a different vertex module, and sharing a cache entry with the
10581
+ plain one would hand a draw the wrong layout. */
10582
+ const adds = blend && options.additive === true;
10583
+ const key =
10584
+ `${base}|inst${!animated ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
10585
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
10586
+ `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
10587
+ `${modelKind === null ? '' : `|m:${modelKind}`}`;
10588
+
10589
+ const shape = this.drawShape;
10590
+ shape.base = base;
10591
+ shape.key = key;
10592
+ shape.modelKind = modelKind;
10593
+ const pipelines = into ?? (blend ? this.blendedPipelines(lands) : this.targetPipelines());
10594
+ let pipeline: GPURenderPipeline | null = pipelines.peek(key) ?? null;
10595
+ if (pipeline === null) {
10596
+ const present = this.meshPresent.get(base);
10597
+ if (present === undefined) {
10598
+ throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
10599
+ }
10600
+ pipeline = flatPipeline(
10601
+ pipelines,
10602
+ this.surface.device,
10603
+ !animated ? this.flatLayoutFor(false, false) : this.animatedBindGroupLayout,
10604
+ this.variant,
10605
+ key,
10606
+ present,
10607
+ adds ? 'additive' : blend,
10608
+ false,
10609
+ false,
10610
+ depthWrite,
10611
+ layer,
10612
+ true,
10613
+ 'none',
10614
+ this.materialDoubleSided,
10615
+ coverage,
10616
+ false,
10617
+ false,
10618
+ modelKind,
10619
+ 'whole',
10620
+ false,
10621
+ animated,
10622
+ );
10623
+ }
10624
+
10625
+ return pipeline;
10626
+ }
10627
+
10373
10628
  /**
10374
10629
  * The next of a batch's cull slots this frame, with its cull queued against the current view; null
10375
10630
  * when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
@@ -10488,50 +10743,18 @@ export class WebGPURenderer implements RendererApi {
10488
10743
  const own =
10489
10744
  ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
10490
10745
  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
- }
10746
+ /* Its pipeline, by the key a prepared batch compiled too: see `instancedPipeline`. */
10747
+ const pipeline = this.instancedPipeline(
10748
+ mesh,
10749
+ animation !== null,
10750
+ blend,
10751
+ lands,
10752
+ options,
10753
+ depthWrite,
10754
+ layer,
10755
+ coverage,
10756
+ );
10757
+ const { base, key, modelKind } = this.drawShape;
10535
10758
 
10536
10759
  /* A batch whose pipeline is still compiling is left out, as a mesh is. */
10537
10760
  const material = pipeline === null ? null : this.materialSlotForDraw();
@@ -14106,6 +14329,7 @@ export class WebGPURenderer implements RendererApi {
14106
14329
  const f = this.aoFloats;
14107
14330
  const at = (name: string): number => this.postField(AO_FRAG_FIELDS, name);
14108
14331
  f[at('uRadius')] = this.quality.ambientOcclusionRadius;
14332
+ f.set(this.aoFade, at('uFade'));
14109
14333
  f.set(this.aoProjScale, at('uProjScale'));
14110
14334
  f.set(this.aoInvProjection, at('uInvProjection'));
14111
14335
  device.queue.writeBuffer(this.aoUniforms, 0, this.aoStaging);
@@ -17007,6 +17231,14 @@ export class WebGPURenderer implements RendererApi {
17007
17231
  this.rebuildRushBindGroup();
17008
17232
  }
17009
17233
 
17234
+ /**
17235
+ * Where ambient occlusion fades out with distance. **The other backend's
17236
+ * `setAmbientOcclusionFade` carries the reasoning**; this is the same setter over the same state.
17237
+ */
17238
+ setAmbientOcclusionFade(distance: number, radius: number): void {
17239
+ resolveOcclusionFade(distance, radius, this.aoFade);
17240
+ }
17241
+
17010
17242
  /**
17011
17243
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
17012
17244
  * to take. **The other backend's `setDepthOfField` carries the reasoning**, whole: the