@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
@@ -63,8 +63,10 @@ import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTexture
63
63
  import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
64
64
  import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
65
65
  import { PROJECTION_FLOATS, packSurfaceProjection } from '../../surfaceProjection.js';
66
+ import { resolveTransmission } from '../../transmission.js';
67
+ import { resolveOcclusionFade } from '../../occlusionFade.js';
66
68
  import { orthographicDepthSpan } from '../../lightMatrix.js';
67
- import { LAYER_FLOATS, packSurfaceLayers } from '../../surfaceLayers.js';
69
+ import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
68
70
  import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
69
71
  import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
70
72
  import { validateClothBinding } from '../../clothBindingData.js';
@@ -228,6 +230,11 @@ const MAX_OVERLAYS = 64;
228
230
  * and passes nobody has written yet; the cost is sixteen kilobytes.
229
231
  */
230
232
  const MAX_SKY_DRAWS = 64;
233
+ /** The draw a preparation asks pipelines for: every option at its default. See `prepareMesh`. */
234
+ const PREPARED_DRAW = {};
235
+ /** A preparation's morph states for a mesh with no targets, and for one with some. */
236
+ const NOT_MORPHED = [false];
237
+ const MORPHED_OR_NOT = [false, true];
231
238
  /**
232
239
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
233
240
  *
@@ -1646,6 +1653,8 @@ export class WebGPURenderer {
1646
1653
  temporalHistoryUsable = false;
1647
1654
  aoStaging = new ArrayBuffer(AO_FRAG_SIZE);
1648
1655
  aoFloats = new Float32Array(this.aoStaging);
1656
+ /** Where occlusion fades out with distance, `uFade`: none until a caller sets one. */
1657
+ aoFade = new Float32Array([-1, 0]);
1649
1658
  aoBindGroup = null;
1650
1659
  aoBlurLayout;
1651
1660
  /**
@@ -2247,7 +2256,13 @@ export class WebGPURenderer {
2247
2256
  projection = new Float32Array(PROJECTION_FLOATS);
2248
2257
  /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
2249
2258
  layerFloats = new Float32Array(LAYER_FLOATS);
2259
+ /**
2260
+ * `uGlassTint` as the material leaves it, its light from behind and that light's colour, which a
2261
+ * pane overwrites for its own draw and `releaseSeeThrough` puts back. See `transmission.ts`.
2262
+ */
2263
+ transmission = new Float32Array([-1, -1, -1, 0]);
2250
2264
  warnedLayersModelMap = false;
2265
+ warnedLayersTriplanar = false;
2251
2266
  /** Its numbers, `uModelParams`, packed by `packModel`. */
2252
2267
  modelParams = new Float32Array(MODEL_PARAM_FLOATS);
2253
2268
  /**
@@ -2649,9 +2664,9 @@ export class WebGPURenderer {
2649
2664
  const orm = (material?.orm ?? null);
2650
2665
  const emissiveMap = (material?.emissive ?? null);
2651
2666
  const ownModelMap = (material?.modelMap ?? null);
2652
- /* A layered material's mask goes where a model's map would: see `SurfaceMaterial.layers`. */
2667
+ /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
2653
2668
  const layers = this.layersOf(material);
2654
- const modelMap = ownModelMap ?? (layers?.mask ?? null);
2669
+ const modelMap = ownModelMap ?? layerMaskMap(layers);
2655
2670
  this.materialModel = material?.model ?? null;
2656
2671
  const reflectivity = material?.reflectivity;
2657
2672
  const environmentGain = material?.environmentGain;
@@ -2715,11 +2730,14 @@ export class WebGPURenderer {
2715
2730
  if (material?.physicalSpecular === true)
2716
2731
  this.enableLit('PHYSICAL_SPECULAR');
2717
2732
  /* Maps placed by the world: the first material that asks is when lit pipelines carry it. */
2718
- packSurfaceProjection(material?.projection, this.projection);
2733
+ const projection = layeredProjection(material?.projection, layers);
2734
+ if (projection !== (material?.projection ?? null))
2735
+ this.warnLayersTriplanar();
2736
+ packSurfaceProjection(projection, this.projection);
2719
2737
  f.set(this.projection, this.materialField('uWorldUv'));
2720
- /* How much light from behind it lets through, in the glass tint's spare lane. */
2721
- f[this.materialField('uGlassTint') + 3] =
2722
- Math.min(Math.max(material?.diffuseTransmission ?? 0, 0), 1) || 0;
2738
+ /* How much light from behind it lets through, and its colour: transmission.ts. */
2739
+ resolveTransmission(material, this.transmission);
2740
+ f.set(this.transmission, this.materialField('uGlassTint'));
2723
2741
  if (this.projection[0] > 0)
2724
2742
  this.enableLit('WORLD_UVS');
2725
2743
  /* And its layers, the first time a material blends any: layered.ts. */
@@ -5078,15 +5096,24 @@ export class WebGPURenderer {
5078
5096
  const layers = material?.layers ?? null;
5079
5097
  if (layers === null || layers.repeats.length === 0)
5080
5098
  return null;
5081
- if ((material?.modelMap ?? null) === null)
5099
+ if (layersFitBeside(layers, material?.modelMap))
5082
5100
  return layers;
5083
5101
  if (!this.warnedLayersModelMap) {
5084
5102
  this.warnedLayersModelMap = true;
5085
- console.warn("WebGPU: a material's layers are drawn as one: its mask is read where a model's map goes, " +
5086
- 'and this material carries a `modelMap` too.');
5103
+ console.warn("WebGPU: a material's layers are drawn as one: its mask is a map, read where a model's map " +
5104
+ "goes, and this material carries a `modelMap` too. A mask in the ORM array (`mask: 'orm'`) " +
5105
+ "or the vertex colour (`mask: 'vertex'`) leaves the slot free.");
5087
5106
  }
5088
5107
  return null;
5089
5108
  }
5109
+ /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
5110
+ warnLayersTriplanar() {
5111
+ if (this.warnedLayersTriplanar)
5112
+ return;
5113
+ this.warnedLayersTriplanar = true;
5114
+ console.warn("WebGPU: a layered material's triplanar projection is laid on the horizontal plane: every " +
5115
+ "layer's maps on three planes would be three times the reads. See `SurfaceMaterial.layers`.");
5116
+ }
5090
5117
  /**
5091
5118
  * What the sun's moving layer is read through: the matrix its shadow pass was last drawn with,
5092
5119
  * where the lit pipelines carry one (`MOVING_SUN`); the environment's otherwise, which every caller
@@ -7155,7 +7182,8 @@ export class WebGPURenderer {
7155
7182
  /* And no projection: maps follow the mesh's coordinates until a material asks otherwise. */
7156
7183
  f.fill(0, at('uWorldUv'), at('uWorldUv') + 4);
7157
7184
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
7158
- f[at('uGlassTint') + 3] = 0;
7185
+ resolveTransmission(null, this.transmission);
7186
+ f.set(this.transmission, at('uGlassTint'));
7159
7187
  /* Nor layers: one layer until a material asks for more. */
7160
7188
  f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
7161
7189
  f[at('uCutout')] = 0;
@@ -7880,6 +7908,153 @@ export class WebGPURenderer {
7880
7908
  `TranslucentMeshOptions.tint` for why, and for the fade it was added for. */
7881
7909
  this.submitMesh(mesh, model, options.tint ?? null, Math.min(opacity, 1), true, options);
7882
7910
  }
7911
+ /**
7912
+ * Compile off the main thread every pipeline a draw of `mesh` in `material` takes. **The other
7913
+ * backend's `prepareMesh` carries the contract**; here a draw's key is `meshShape`'s, so a prepared
7914
+ * key is exactly the one the draw asks for, and the compiles are started as a skipping draw starts
7915
+ * them (`PipelineCache.lit`) whatever `pipelineCompile` says, into the frame's own caches.
7916
+ */
7917
+ prepareMesh(mesh, material, options = {}) {
7918
+ if (this.surface.lost)
7919
+ return;
7920
+ this.preparing(material, (into, blend) => {
7921
+ const skinned = mesh.isSkinned;
7922
+ const eight = skinned && mesh.isSkinnedEight;
7923
+ for (const morphed of mesh.morph === null ? NOT_MORPHED : MORPHED_OR_NOT) {
7924
+ this.meshShape(mesh, blend, PREPARED_DRAW, skinned, morphed, eight, false);
7925
+ this.meshPipeline(blend, false, skinned, morphed, eight, false, into);
7926
+ }
7927
+ }, options);
7928
+ }
7929
+ /** `prepareMesh` for an instanced batch. See the other backend's `prepareInstanced`. */
7930
+ prepareInstanced(batch, material, options = {}) {
7931
+ if (this.surface.lost)
7932
+ return;
7933
+ this.preparing(material, (into, blend) => {
7934
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
7935
+ this.instancedPipeline(batch.mesh, batch.animation !== null, blend, false, PREPARED_DRAW, true, 0, coverage, into);
7936
+ }, options);
7937
+ }
7938
+ /**
7939
+ * `material` set, every cache a draw in it may build into passed to `start` with whether that draw
7940
+ * blends, compiles started rather than built (`PipelineCache.lit`), and then no material set and
7941
+ * each cache back to what `pipelineCompile` says.
7942
+ */
7943
+ preparing(material, start, options) {
7944
+ this.setMaterial(material);
7945
+ this.skipCompiling(true);
7946
+ try {
7947
+ start(this.pipelines, false);
7948
+ if (options.translucent === true) {
7949
+ start(this.pipelines, true);
7950
+ /* A blended draw in a reconstructing frame lands in the late pass, whose cache is its own. */
7951
+ if (this.latePipelines !== this.pipelines)
7952
+ start(this.latePipelines, true);
7953
+ }
7954
+ }
7955
+ finally {
7956
+ this.skipCompiling(this.quality.pipelineCompile === 'skip');
7957
+ this.setMaterial(null);
7958
+ }
7959
+ }
7960
+ /** What `meshShape` worked out for the mesh draw at hand. Reused, so a draw allocates no record. */
7961
+ drawShape = {
7962
+ base: '',
7963
+ key: '',
7964
+ depthWrite: true,
7965
+ layer: 0,
7966
+ adds: false,
7967
+ coverage: false,
7968
+ splits: false,
7969
+ modelKind: null,
7970
+ };
7971
+ /**
7972
+ * The key a mesh draw's pipeline is cached under, and the pipeline state that key encodes, under
7973
+ * the current material and these options, into `drawShape`. **The one statement of it**:
7974
+ * `submitMesh` draws by it and `prepareMesh` compiles by it, so the two cannot disagree about a key.
7975
+ */
7976
+ meshShape(mesh, blend, options, skinned, morphed, eight, clothed) {
7977
+ const modelKind = this.materialModel?.kind ?? null;
7978
+ const base = mesh.key ?? 'flat:s0:u0';
7979
+ const keyed = `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
7980
+ `${clothed ? '|cloth' : ''}`;
7981
+ /*
7982
+ * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
7983
+ * the key: two draws of one mesh differing only in these would otherwise share a cached
7984
+ * pipeline and the second would silently take the first one's depth behaviour.
7985
+ *
7986
+ * **Both suffixes are empty for the defaults**, which is what keeps the keys `createMesh`
7987
+ * warms — `fullKey` and `${'`'}${'$'}{fullKey}|blend${'`'}` — the ones an ordinary draw still asks for. A
7988
+ * caller that opts in pays one compile on its first draw, the same bargain the overlay
7989
+ * pipelines above already make.
7990
+ */
7991
+ const depthWrite = options.depthWrite ?? true;
7992
+ const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
7993
+ /* Added rather than blended over: its own blend state, so its own pipeline. */
7994
+ const adds = blend && options.additive === true;
7995
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
7996
+ /* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
7997
+ const splits = modelKind === 'skin' && !blend && this.skinSplits();
7998
+ const key = `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
7999
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8000
+ /* The two order-independent buffers are two formats and two blend states, so a draw into
8001
+ one must never share a cached pipeline with the same mesh drawn into the other. */
8002
+ `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
8003
+ /* Culling is pipeline state here: a two-sided surface culls nothing. */
8004
+ `${this.materialDoubleSided ? '|2s' : ''}` +
8005
+ /* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
8006
+ `${coverage ? '|a2c' : ''}` +
8007
+ /* And the material's shading model, which is the fragment stage's own switch. */
8008
+ `${modelKind === null ? '' : `|m:${modelKind}`}` +
8009
+ /* And which half of a skin it draws, under the screen-space blur. */
8010
+ `${splits ? '|ss' : ''}`;
8011
+ const shape = this.drawShape;
8012
+ shape.base = base;
8013
+ shape.key = key;
8014
+ shape.depthWrite = depthWrite;
8015
+ shape.layer = layer;
8016
+ shape.adds = adds;
8017
+ shape.coverage = coverage;
8018
+ shape.splits = splits;
8019
+ shape.modelKind = modelKind;
8020
+ return shape;
8021
+ }
8022
+ /**
8023
+ * The pipeline `drawShape` names: the one built, one built here, or none where draws skip a
8024
+ * compile and this one has not landed (`PipelineCache.lit`) — and none for a split skin until all
8025
+ * of its halves have.
8026
+ */
8027
+ meshPipeline(blend, lands, skinned, morphed, eight, clothed,
8028
+ /** Which cache to build into: the one the draw's target reads, unless a preparation says. */
8029
+ into = null) {
8030
+ const { base, key, depthWrite, layer, adds, coverage, splits, modelKind } = this.drawShape;
8031
+ /*
8032
+ * **Asynchronous by default, synchronous if it has to be, and never absent.**
8033
+ *
8034
+ * `createMesh` compiles off the main thread, so a draw can legitimately arrive before the
8035
+ * driver has finished — a consumer that awaits `ready()` never sees this, and one that
8036
+ * draws immediately gets the old behaviour rather than an exception. Correctness cannot be
8037
+ * allowed to depend on which of those happened, so the miss builds the pipeline here and
8038
+ * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
8039
+ * thrown error in the middle of one.
8040
+ */
8041
+ const pipelines = into ?? (blend ? this.blendedPipelines(lands) : this.targetPipelines());
8042
+ let pipeline = pipelines.peek(key) ?? null;
8043
+ if (pipeline === null) {
8044
+ const present = this.meshPresent.get(base);
8045
+ if (present === undefined) {
8046
+ throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
8047
+ }
8048
+ pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, adds ? 'additive' : blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided, coverage, eight, clothed, modelKind, splits ? 'scene' : 'whole');
8049
+ }
8050
+ /* A split skin draws its three halves or none of them. */
8051
+ if (pipeline !== null &&
8052
+ splits &&
8053
+ !this.skinHalvesReady(key, base, skinned, morphed, layer, eight, clothed)) {
8054
+ pipeline = null;
8055
+ }
8056
+ return pipeline;
8057
+ }
7883
8058
  /**
7884
8059
  * @param blend Which of the two entry points above this came through, and **not** something to
7885
8060
  * infer from `opacity`.
@@ -7999,7 +8174,6 @@ export class WebGPURenderer {
7999
8174
  const modelKind = this.materialModel?.kind ?? null;
8000
8175
  if (modelKind === 'eye')
8001
8176
  this.writeEyeAxis(model, this.skinPaletteSlot >= 0 && mesh.isSkinned);
8002
- const base = mesh.key ?? 'flat:s0:u0';
8003
8177
  /*
8004
8178
  * A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
8005
8179
  * key carries the flag so the two never share a cached pipeline. A rigged mesh whose caller
@@ -8014,38 +8188,6 @@ export class WebGPURenderer {
8014
8188
  /* A cloth set and a skinned draw: the pipeline built with CLOTH_BOUND on, warmed when the
8015
8189
  binding was made. See `createClothBinding`. */
8016
8190
  const clothed = skinned && this.cloth !== null;
8017
- const keyed = `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
8018
- `${clothed ? '|cloth' : ''}`;
8019
- /*
8020
- * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
8021
- * the key: two draws of one mesh differing only in these would otherwise share a cached
8022
- * pipeline and the second would silently take the first one's depth behaviour.
8023
- *
8024
- * **Both suffixes are empty for the defaults**, which is what keeps the keys `createMesh`
8025
- * warms — `fullKey` and `${'`'}${'$'}{fullKey}|blend${'`'}` — the ones an ordinary draw still asks for. A
8026
- * caller that opts in pays one compile on its first draw, the same bargain the overlay
8027
- * pipelines above already make.
8028
- */
8029
- const depthWrite = options.depthWrite ?? true;
8030
- const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8031
- /* Added rather than blended over: its own blend state, so its own pipeline. */
8032
- const adds = blend && options.additive === true;
8033
- const coverage = !blend && this.cutoutResolveStaged === 'coverage';
8034
- /* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
8035
- const splits = modelKind === 'skin' && !blend && this.skinSplits();
8036
- const key = `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
8037
- `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8038
- /* The two order-independent buffers are two formats and two blend states, so a draw into
8039
- one must never share a cached pipeline with the same mesh drawn into the other. */
8040
- `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
8041
- /* Culling is pipeline state here: a two-sided surface culls nothing. */
8042
- `${this.materialDoubleSided ? '|2s' : ''}` +
8043
- /* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
8044
- `${coverage ? '|a2c' : ''}` +
8045
- /* And the material's shading model, which is the fragment stage's own switch. */
8046
- `${modelKind === null ? '' : `|m:${modelKind}`}` +
8047
- /* And which half of a skin it draws, under the screen-space blur. */
8048
- `${splits ? '|ss' : ''}`;
8049
8191
  /*
8050
8192
  * The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
8051
8193
  * field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
@@ -8058,31 +8200,9 @@ export class WebGPURenderer {
8058
8200
  this.perDraw.writeInt(slot, fields.uMorphTargetCount.offset, mesh.morph.targetCount);
8059
8201
  this.perDraw.writeInt(slot, fields.uMorphTextureWidth.offset, mesh.morph.width);
8060
8202
  }
8061
- /*
8062
- * **Asynchronous by default, synchronous if it has to be, and never absent.**
8063
- *
8064
- * `createMesh` compiles off the main thread, so a draw can legitimately arrive before the
8065
- * driver has finished — a consumer that awaits `ready()` never sees this, and one that
8066
- * draws immediately gets the old behaviour rather than an exception. Correctness cannot be
8067
- * allowed to depend on which of those happened, so the miss builds the pipeline here and
8068
- * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
8069
- * thrown error in the middle of one.
8070
- */
8071
- const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
8072
- let pipeline = pipelines.peek(key) ?? null;
8073
- if (pipeline === null) {
8074
- const present = this.meshPresent.get(base);
8075
- if (present === undefined) {
8076
- throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
8077
- }
8078
- pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, adds ? 'additive' : blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided, coverage, eight, clothed, modelKind, splits ? 'scene' : 'whole');
8079
- }
8080
- /* A split skin draws its three halves or none of them. */
8081
- if (pipeline !== null &&
8082
- splits &&
8083
- !this.skinHalvesReady(key, base, skinned, morphed, layer, eight, clothed)) {
8084
- pipeline = null;
8085
- }
8203
+ /* Its pipeline, by the key a prepared draw compiled too: see `meshShape`. */
8204
+ const { base, key, layer, depthWrite, splits } = this.meshShape(mesh, blend, options, skinned, morphed, eight, clothed);
8205
+ let pipeline = this.meshPipeline(blend, lands, skinned, morphed, eight, clothed);
8086
8206
  /*
8087
8207
  * Guards the draw call only, not the restore below.
8088
8208
  *
@@ -8388,6 +8508,8 @@ export class WebGPURenderer {
8388
8508
  const seeAt = this.materialField('uSeeThrough');
8389
8509
  this.perFrameFloats[seeAt] = 0;
8390
8510
  this.perFrameFloats[seeAt + 2] = 0;
8511
+ /* The material's light through, over the pane's tint: transmission.ts. */
8512
+ this.perFrameFloats.set(this.transmission, this.materialField('uGlassTint'));
8391
8513
  }
8392
8514
  /**
8393
8515
  * One draw, one material slot, one uniform slot, however many instances.
@@ -8401,6 +8523,41 @@ export class WebGPURenderer {
8401
8523
  * **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
8402
8524
  * is built, and a per-instance opacity: `opacity` here dims the whole batch.
8403
8525
  */
8526
+ /**
8527
+ * An instanced draw's pipeline under the current material and these options: the one built, one
8528
+ * built here, or none while it compiles (`PipelineCache.lit`), its key, base and model left in
8529
+ * `drawShape`. **The one statement of an instanced key**: `submitInstanced` draws by it and
8530
+ * `prepareInstanced` compiles by it.
8531
+ */
8532
+ instancedPipeline(mesh, animated, blend, lands, options, depthWrite, layer, coverage,
8533
+ /** Which cache to build into: the one the draw's target reads, unless a preparation says. */
8534
+ into = null) {
8535
+ /* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
8536
+ const modelKind = this.materialModel?.kind ?? null;
8537
+ const base = mesh.key ?? 'flat:s0:u0';
8538
+ /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
8539
+ a third vertex buffer and a different vertex module, and sharing a cache entry with the
8540
+ plain one would hand a draw the wrong layout. */
8541
+ const adds = blend && options.additive === true;
8542
+ const key = `${base}|inst${!animated ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
8543
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8544
+ `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
8545
+ `${modelKind === null ? '' : `|m:${modelKind}`}`;
8546
+ const shape = this.drawShape;
8547
+ shape.base = base;
8548
+ shape.key = key;
8549
+ shape.modelKind = modelKind;
8550
+ const pipelines = into ?? (blend ? this.blendedPipelines(lands) : this.targetPipelines());
8551
+ let pipeline = pipelines.peek(key) ?? null;
8552
+ if (pipeline === null) {
8553
+ const present = this.meshPresent.get(base);
8554
+ if (present === undefined) {
8555
+ throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
8556
+ }
8557
+ pipeline = flatPipeline(pipelines, this.surface.device, !animated ? this.flatLayoutFor(false, false) : this.animatedBindGroupLayout, this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided, coverage, false, false, modelKind, 'whole', false, animated);
8558
+ }
8559
+ return pipeline;
8560
+ }
8404
8561
  /**
8405
8562
  * The next of a batch's cull slots this frame, with its cull queued against the current view; null
8406
8563
  * when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
@@ -8504,26 +8661,9 @@ export class WebGPURenderer {
8504
8661
  const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
8505
8662
  if (own)
8506
8663
  this.materials.dirty();
8507
- /* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
8508
- const modelKind = this.materialModel?.kind ?? null;
8509
- const base = mesh.key ?? 'flat:s0:u0';
8510
- /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
8511
- a third vertex buffer and a different vertex module, and sharing a cache entry with the
8512
- plain one would hand a draw the wrong layout. */
8513
- const adds = blend && options.additive === true;
8514
- const key = `${base}|inst${animation === null ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
8515
- `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8516
- `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
8517
- `${modelKind === null ? '' : `|m:${modelKind}`}`;
8518
- const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
8519
- let pipeline = pipelines.peek(key) ?? null;
8520
- if (pipeline === null) {
8521
- const present = this.meshPresent.get(base);
8522
- if (present === undefined) {
8523
- throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
8524
- }
8525
- pipeline = flatPipeline(pipelines, this.surface.device, animation === null ? this.flatLayoutFor(false, false) : this.animatedBindGroupLayout, this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided, coverage, false, false, modelKind, 'whole', false, animation !== null);
8526
- }
8664
+ /* Its pipeline, by the key a prepared batch compiled too: see `instancedPipeline`. */
8665
+ const pipeline = this.instancedPipeline(mesh, animation !== null, blend, lands, options, depthWrite, layer, coverage);
8666
+ const { base, key, modelKind } = this.drawShape;
8527
8667
  /* A batch whose pipeline is still compiling is left out, as a mesh is. */
8528
8668
  const material = pipeline === null ? null : this.materialSlotForDraw();
8529
8669
  if (pipeline !== null && material !== null) {
@@ -11581,6 +11721,7 @@ export class WebGPURenderer {
11581
11721
  const f = this.aoFloats;
11582
11722
  const at = (name) => this.postField(AO_FRAG_FIELDS, name);
11583
11723
  f[at('uRadius')] = this.quality.ambientOcclusionRadius;
11724
+ f.set(this.aoFade, at('uFade'));
11584
11725
  f.set(this.aoProjScale, at('uProjScale'));
11585
11726
  f.set(this.aoInvProjection, at('uInvProjection'));
11586
11727
  device.queue.writeBuffer(this.aoUniforms, 0, this.aoStaging);
@@ -14172,6 +14313,13 @@ export class WebGPURenderer {
14172
14313
  this.exposurePass = new ExposurePass(this.surface.device, this.pipelines, this.postSampler);
14173
14314
  this.rebuildRushBindGroup();
14174
14315
  }
14316
+ /**
14317
+ * Where ambient occlusion fades out with distance. **The other backend's
14318
+ * `setAmbientOcclusionFade` carries the reasoning**; this is the same setter over the same state.
14319
+ */
14320
+ setAmbientOcclusionFade(distance, radius) {
14321
+ resolveOcclusionFade(distance, radius, this.aoFade);
14322
+ }
14175
14323
  /**
14176
14324
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
14177
14325
  * to take. **The other backend's `setDepthOfField` carries the reasoning**, whole: the
@@ -46,9 +46,16 @@ export interface MeshInstances {
46
46
  * read by a draw whose material is a `lightmapModel` — instances of one mesh in one material sit
47
47
  * in different regions of one page, each applied before the material's own region. **They ride
48
48
  * the tint and the opacity**, which a lightmapped instance gives up (see `lightmap.ts`): where
49
- * these are given, those are not read. **A lightmapped batch needs them**, since a bake is
49
+ * these are given, the tints are not read. **A lightmapped batch needs them**, since a bake is
50
50
  * different light at each instance; one drawn without them reads its tints as its regions, and
51
51
  * either mismatch is said once on the console.
52
+ *
53
+ * **The opacity survives, in sixty-fourths**, so a lightmapped instance fades by
54
+ * `setDitherOpacity` as any other does: its transparency travels as whole steps of 2 added to the
55
+ * region's U offset, which is a place in a page and so under 1, and the vertex stage takes them
56
+ * back off (`LIGHTMAP_OPACITY_STEPS`). Sixty-four is the screen door's own resolution, an 8x8
57
+ * pattern. **What it gives up**: a fading instance's offset keeps 16 bits of fraction rather than
58
+ * 23, a sixteenth of a texel on a 4,096 page; an opaque one is written to the bit as before.
52
59
  */
53
60
  readonly lightmapRegions?: Float32Array;
54
61
  /**
@@ -98,6 +105,11 @@ export declare function createMeshInstances(capacity: number): MeshInstances;
98
105
  * as one four-float attribute, so the opacity cost no attribute and no byte.
99
106
  */
100
107
  export declare const INSTANCE_FLOATS = 20;
108
+ /**
109
+ * How finely a lightmapped instance's opacity travels in its region: transparency in steps of
110
+ * 1/64, each 2 on the U offset. See `MeshInstances.lightmapRegions`; the vertex stage reads it back.
111
+ */
112
+ export declare const LIGHTMAP_OPACITY_STEPS = 64;
101
113
  /** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
102
114
  export declare const INSTANCE_STRIDE: number;
103
115
  /**
@@ -26,6 +26,11 @@ export function createMeshInstances(capacity) {
26
26
  * as one four-float attribute, so the opacity cost no attribute and no byte.
27
27
  */
28
28
  export const INSTANCE_FLOATS = 20;
29
+ /**
30
+ * How finely a lightmapped instance's opacity travels in its region: transparency in steps of
31
+ * 1/64, each 2 on the U offset. See `MeshInstances.lightmapRegions`; the vertex stage reads it back.
32
+ */
33
+ export const LIGHTMAP_OPACITY_STEPS = 64;
29
34
  /** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
30
35
  export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
31
36
  /**
@@ -56,6 +61,11 @@ export function packInstances(instances, out) {
56
61
  /* The region in the tint's and the opacity's place. See `lightmapRegions`. */
57
62
  for (let c = 0; c < 4; c += 1)
58
63
  out[at + 16 + c] = lightmapRegions[i * 4 + c];
64
+ /* And the opacity's transparency in whole steps of 2 on the U offset, none where opaque. */
65
+ const alpha = Math.min(Math.max(alphas?.[i] ?? 1, 0), 1);
66
+ const steps = Math.round((1 - alpha) * LIGHTMAP_OPACITY_STEPS);
67
+ if (steps > 0)
68
+ out[at + 18] = out[at + 18] + 2 * steps;
59
69
  continue;
60
70
  }
61
71
  const t = i * 3;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Where ambient occlusion fades out with distance: `setAmbientOcclusionFade`'s two numbers as the
3
+ * estimate reads them, `uFade` in `shaders/ambientOcclusion.ts`.
4
+ *
5
+ * **Occlusion is whole up to `distance` metres from the eye and gone `radius` metres past it.** A
6
+ * screen-space estimate reaches as far as the depth does, and far off the depth buffer's steps are
7
+ * metres apart: a sky dome or a mountain range kilometres away is shaded in rings that follow
8
+ * those steps, and nothing there is in contact with anything the estimate could be right about.
9
+ * A pixel past the fade skips the estimate's walk, as the sky does, so it costs less as well.
10
+ *
11
+ * **What it gives up**: distant contact shade that is real — a doorway a hundred metres off — fades
12
+ * with the rest, and the distance is the eye's and not the subject's, so a long lens fades what it
13
+ * frames close up. What would make it wrong is a scene whose far depth is precise enough to shade,
14
+ * which is a reason to fade further, not to leave the fade off.
15
+ */
16
+ /**
17
+ * `distance` and `radius` into `out`, `[distance, radius]`. A distance that is not a finite number
18
+ * at or above zero is no fade, `[−1, 0]`, so `Infinity` reads as "never fades". A radius below zero
19
+ * or not a number is 0, which is a cut at `distance` rather than a ramp.
20
+ */
21
+ export declare function resolveOcclusionFade(distance: number, radius: number, out: Float32Array): void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Where ambient occlusion fades out with distance: `setAmbientOcclusionFade`'s two numbers as the
3
+ * estimate reads them, `uFade` in `shaders/ambientOcclusion.ts`.
4
+ *
5
+ * **Occlusion is whole up to `distance` metres from the eye and gone `radius` metres past it.** A
6
+ * screen-space estimate reaches as far as the depth does, and far off the depth buffer's steps are
7
+ * metres apart: a sky dome or a mountain range kilometres away is shaded in rings that follow
8
+ * those steps, and nothing there is in contact with anything the estimate could be right about.
9
+ * A pixel past the fade skips the estimate's walk, as the sky does, so it costs less as well.
10
+ *
11
+ * **What it gives up**: distant contact shade that is real — a doorway a hundred metres off — fades
12
+ * with the rest, and the distance is the eye's and not the subject's, so a long lens fades what it
13
+ * frames close up. What would make it wrong is a scene whose far depth is precise enough to shade,
14
+ * which is a reason to fade further, not to leave the fade off.
15
+ */
16
+ /** `uFade` for no fade: a distance below zero, which the estimate reads as "never". */
17
+ const NO_FADE = -1;
18
+ /**
19
+ * `distance` and `radius` into `out`, `[distance, radius]`. A distance that is not a finite number
20
+ * at or above zero is no fade, `[−1, 0]`, so `Infinity` reads as "never fades". A radius below zero
21
+ * or not a number is 0, which is a cut at `distance` rather than a ramp.
22
+ */
23
+ export function resolveOcclusionFade(distance, radius, out) {
24
+ const fades = Number.isFinite(distance) && distance >= 0;
25
+ out[0] = fades ? distance : NO_FADE;
26
+ out[1] = fades && Number.isFinite(radius) ? Math.max(radius, 0) : 0;
27
+ }
@@ -313,6 +313,8 @@ export declare class SceneTarget {
313
313
  /** The projection's x and y scales, which turn a world radius into a screen one. */
314
314
  readonly projScale: Float32Array;
315
315
  readonly invProjection: Float32Array;
316
+ /** Where it fades out with distance, `uFade`: see `occlusionFade.ts`. */
317
+ readonly fade: Float32Array;
316
318
  /** Where the frame's jitter put the occlusion, in uv: see the shader's `uAoOffset`. */
317
319
  readonly offset?: Float32Array;
318
320
  }, bloom?: {
@@ -771,7 +771,7 @@ export class SceneTarget {
771
771
  let aoTexture = null;
772
772
  if (ao !== undefined && !this.depthBlitRefused && this.depth !== null) {
773
773
  this.ao ??= new AmbientOcclusionPass(gl);
774
- aoTexture = this.ao.run(this.depth, this.width, this.height, ao.radius, ao.projScale, ao.invProjection);
774
+ aoTexture = this.ao.run(this.depth, this.width, this.height, ao.radius, ao.projScale, ao.invProjection, ao.fade);
775
775
  }
776
776
  /*
777
777
  * Bloom beside occlusion rather than after the composite, because the composite is what
@@ -75,6 +75,11 @@ uniform vec2 uProjScale;
75
75
  uniform mat4 uInvProjection;
76
76
  /** How far the sampling reaches, in metres of the world. */
77
77
  uniform float uRadius;
78
+ /**
79
+ * Where occlusion fades out: whole up to x metres from the eye, gone y metres past it, and no fade
80
+ * where x is below zero. See occlusionFade.ts.
81
+ */
82
+ uniform vec2 uFade;
78
83
 
79
84
  out float fragColor;
80
85
 
@@ -167,6 +172,12 @@ void main() {
167
172
  fragColor = 1.0;
168
173
  return;
169
174
  }
175
+ /* How far past the caller's fade this pixel is, and open with no walk where it is all the way. */
176
+ float faded = uFade.x < 0.0 ? 0.0 : clamp((length(p) - uFade.x) / max(uFade.y, 1e-4), 0.0, 1.0);
177
+ if (faded >= 1.0) {
178
+ fragColor = AO_STORE;
179
+ return;
180
+ }
170
181
 
171
182
  /*
172
183
  * At a silhouette, the quad derivative above is replaced by the one-sided difference that
@@ -307,7 +318,7 @@ void main() {
307
318
  visible += projectedLength * gtaoArc(h0, h1, normalAngle);
308
319
  }
309
320
 
310
- fragColor = clamp(visible / float(AO_SLICES) * AO_STORE, 0.0, 1.0);
321
+ fragColor = clamp(mix(visible / float(AO_SLICES), 1.0, faded) * AO_STORE, 0.0, 1.0);
311
322
  }
312
323
  `;
313
324
  /**
@@ -18,6 +18,7 @@ import { modelsGlsl } from './models.js';
18
18
  import { AREA_LIGHT_GLSL } from './areaLight.js';
19
19
  import { NORMAL_MAP_GLSL } from './normalMap.js';
20
20
  import { DRIFT_LIGHT_GLSL } from './driftLight.js';
21
+ import { LIGHTMAP_OPACITY_STEPS } from '../../instances.js';
21
22
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.js';
22
23
  import { overlayGlsl } from './overlay.js';
23
24
  import { reflectionSurfaceGlsl } from './reflectionSurface.js';
@@ -341,10 +342,16 @@ void main() {
341
342
  vAlpha = aInstanceTint.w;
342
343
  vThickness = 1.0;
343
344
  if (LIGHTMAP_REGIONS) {
345
+ /*
346
+ * The opacity's transparency rides the U offset as whole steps of 2, the offset itself under 1
347
+ * (\`MeshInstances.lightmapRegions\`): taken off, and none where the instance is opaque.
348
+ */
349
+ float steps = floor(aInstanceTint.z * 0.5);
350
+ float offsetU = aInstanceTint.z - 2.0 * steps;
344
351
  /* Out of the lane's \`-1 - uv\`, through the region, and back into it. See lightmap.ts. */
345
- vGrain = -1.0 - ((-1.0 - aGrain) * aInstanceTint.x + aInstanceTint.z);
352
+ vGrain = -1.0 - ((-1.0 - aGrain) * aInstanceTint.x + offsetU);
346
353
  vRelief = -1.0 - ((-1.0 - aRelief) * aInstanceTint.y + aInstanceTint.w);
347
- vAlpha = 1.0;
354
+ vAlpha = 1.0 - steps / ${LIGHTMAP_OPACITY_STEPS.toFixed(1)};
348
355
  }
349
356
  #else
350
357
  vSkyDirect = aChannel.y;