@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
@@ -63,8 +63,11 @@ 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';
70
+ import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
68
71
  import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
69
72
  import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
70
73
  import { validateClothBinding } from '../../clothBindingData.js';
@@ -228,6 +231,11 @@ const MAX_OVERLAYS = 64;
228
231
  * and passes nobody has written yet; the cost is sixteen kilobytes.
229
232
  */
230
233
  const MAX_SKY_DRAWS = 64;
234
+ /** The draw a preparation asks pipelines for: every option at its default. See `prepareMesh`. */
235
+ const PREPARED_DRAW = {};
236
+ /** A preparation's morph states for a mesh with no targets, and for one with some. */
237
+ const NOT_MORPHED = [false];
238
+ const MORPHED_OR_NOT = [false, true];
231
239
  /**
232
240
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
233
241
  *
@@ -1646,6 +1654,8 @@ export class WebGPURenderer {
1646
1654
  temporalHistoryUsable = false;
1647
1655
  aoStaging = new ArrayBuffer(AO_FRAG_SIZE);
1648
1656
  aoFloats = new Float32Array(this.aoStaging);
1657
+ /** Where occlusion fades out with distance, `uFade`: none until a caller sets one. */
1658
+ aoFade = new Float32Array([-1, 0]);
1649
1659
  aoBindGroup = null;
1650
1660
  aoBlurLayout;
1651
1661
  /**
@@ -2247,7 +2257,15 @@ export class WebGPURenderer {
2247
2257
  projection = new Float32Array(PROJECTION_FLOATS);
2248
2258
  /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
2249
2259
  layerFloats = new Float32Array(LAYER_FLOATS);
2260
+ /** `uLayerLooks`, the material's layer picks and looks packed by `packLayerLooks`. */
2261
+ lookFloats = new Float32Array(LOOK_FLOATS);
2262
+ /**
2263
+ * `uGlassTint` as the material leaves it, its light from behind and that light's colour, which a
2264
+ * pane overwrites for its own draw and `releaseSeeThrough` puts back. See `transmission.ts`.
2265
+ */
2266
+ transmission = new Float32Array([-1, -1, -1, 0]);
2250
2267
  warnedLayersModelMap = false;
2268
+ warnedLayersTriplanar = false;
2251
2269
  /** Its numbers, `uModelParams`, packed by `packModel`. */
2252
2270
  modelParams = new Float32Array(MODEL_PARAM_FLOATS);
2253
2271
  /**
@@ -2649,9 +2667,9 @@ export class WebGPURenderer {
2649
2667
  const orm = (material?.orm ?? null);
2650
2668
  const emissiveMap = (material?.emissive ?? null);
2651
2669
  const ownModelMap = (material?.modelMap ?? null);
2652
- /* A layered material's mask goes where a model's map would: see `SurfaceMaterial.layers`. */
2670
+ /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
2653
2671
  const layers = this.layersOf(material);
2654
- const modelMap = ownModelMap ?? (layers?.mask ?? null);
2672
+ const modelMap = ownModelMap ?? layerMaskMap(layers);
2655
2673
  this.materialModel = material?.model ?? null;
2656
2674
  const reflectivity = material?.reflectivity;
2657
2675
  const environmentGain = material?.environmentGain;
@@ -2715,11 +2733,14 @@ export class WebGPURenderer {
2715
2733
  if (material?.physicalSpecular === true)
2716
2734
  this.enableLit('PHYSICAL_SPECULAR');
2717
2735
  /* Maps placed by the world: the first material that asks is when lit pipelines carry it. */
2718
- packSurfaceProjection(material?.projection, this.projection);
2736
+ const projection = layeredProjection(material?.projection, layers);
2737
+ if (projection !== (material?.projection ?? null))
2738
+ this.warnLayersTriplanar();
2739
+ packSurfaceProjection(projection, this.projection);
2719
2740
  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;
2741
+ /* How much light from behind it lets through, and its colour: transmission.ts. */
2742
+ resolveTransmission(material, this.transmission);
2743
+ f.set(this.transmission, this.materialField('uGlassTint'));
2723
2744
  if (this.projection[0] > 0)
2724
2745
  this.enableLit('WORLD_UVS');
2725
2746
  /* And its layers, the first time a material blends any: layered.ts. */
@@ -2727,6 +2748,11 @@ export class WebGPURenderer {
2727
2748
  f.set(this.layerFloats, this.materialField('uLayers'));
2728
2749
  if (layers !== null)
2729
2750
  this.enableLit('LAYERED');
2751
+ /* And which layer of shared arrays each reads, with its look: layerLooks.ts. */
2752
+ packLayerLooks(layers, this.lookFloats);
2753
+ f.set(this.lookFloats, this.materialField('uLayerLooks'));
2754
+ if (asksLayerLooks(layers))
2755
+ this.enableLit('LAYER_LOOKS');
2730
2756
  f.set(this.modelParams, this.materialField('uModelParams'));
2731
2757
  });
2732
2758
  /*
@@ -5078,15 +5104,24 @@ export class WebGPURenderer {
5078
5104
  const layers = material?.layers ?? null;
5079
5105
  if (layers === null || layers.repeats.length === 0)
5080
5106
  return null;
5081
- if ((material?.modelMap ?? null) === null)
5107
+ if (layersFitBeside(layers, material?.modelMap))
5082
5108
  return layers;
5083
5109
  if (!this.warnedLayersModelMap) {
5084
5110
  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.');
5111
+ console.warn("WebGPU: a material's layers are drawn as one: its mask is a map, read where a model's map " +
5112
+ "goes, and this material carries a `modelMap` too. A mask in the ORM array (`mask: 'orm'`) " +
5113
+ "or the vertex colour (`mask: 'vertex'`) leaves the slot free.");
5087
5114
  }
5088
5115
  return null;
5089
5116
  }
5117
+ /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
5118
+ warnLayersTriplanar() {
5119
+ if (this.warnedLayersTriplanar)
5120
+ return;
5121
+ this.warnedLayersTriplanar = true;
5122
+ console.warn("WebGPU: a layered material's triplanar projection is laid on the horizontal plane: every " +
5123
+ "layer's maps on three planes would be three times the reads. See `SurfaceMaterial.layers`.");
5124
+ }
5090
5125
  /**
5091
5126
  * What the sun's moving layer is read through: the matrix its shadow pass was last drawn with,
5092
5127
  * where the lit pipelines carry one (`MOVING_SUN`); the environment's otherwise, which every caller
@@ -7155,9 +7190,11 @@ export class WebGPURenderer {
7155
7190
  /* And no projection: maps follow the mesh's coordinates until a material asks otherwise. */
7156
7191
  f.fill(0, at('uWorldUv'), at('uWorldUv') + 4);
7157
7192
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
7158
- f[at('uGlassTint') + 3] = 0;
7193
+ resolveTransmission(null, this.transmission);
7194
+ f.set(this.transmission, at('uGlassTint'));
7159
7195
  /* Nor layers: one layer until a material asks for more. */
7160
7196
  f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
7197
+ f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
7161
7198
  f[at('uCutout')] = 0;
7162
7199
  f[at('uCutout') + 1] = 0;
7163
7200
  this.cutoutResolveStaged = 'hard';
@@ -7880,6 +7917,153 @@ export class WebGPURenderer {
7880
7917
  `TranslucentMeshOptions.tint` for why, and for the fade it was added for. */
7881
7918
  this.submitMesh(mesh, model, options.tint ?? null, Math.min(opacity, 1), true, options);
7882
7919
  }
7920
+ /**
7921
+ * Compile off the main thread every pipeline a draw of `mesh` in `material` takes. **The other
7922
+ * backend's `prepareMesh` carries the contract**; here a draw's key is `meshShape`'s, so a prepared
7923
+ * key is exactly the one the draw asks for, and the compiles are started as a skipping draw starts
7924
+ * them (`PipelineCache.lit`) whatever `pipelineCompile` says, into the frame's own caches.
7925
+ */
7926
+ prepareMesh(mesh, material, options = {}) {
7927
+ if (this.surface.lost)
7928
+ return;
7929
+ this.preparing(material, (into, blend) => {
7930
+ const skinned = mesh.isSkinned;
7931
+ const eight = skinned && mesh.isSkinnedEight;
7932
+ for (const morphed of mesh.morph === null ? NOT_MORPHED : MORPHED_OR_NOT) {
7933
+ this.meshShape(mesh, blend, PREPARED_DRAW, skinned, morphed, eight, false);
7934
+ this.meshPipeline(blend, false, skinned, morphed, eight, false, into);
7935
+ }
7936
+ }, options);
7937
+ }
7938
+ /** `prepareMesh` for an instanced batch. See the other backend's `prepareInstanced`. */
7939
+ prepareInstanced(batch, material, options = {}) {
7940
+ if (this.surface.lost)
7941
+ return;
7942
+ this.preparing(material, (into, blend) => {
7943
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
7944
+ this.instancedPipeline(batch.mesh, batch.animation !== null, blend, false, PREPARED_DRAW, true, 0, coverage, into);
7945
+ }, options);
7946
+ }
7947
+ /**
7948
+ * `material` set, every cache a draw in it may build into passed to `start` with whether that draw
7949
+ * blends, compiles started rather than built (`PipelineCache.lit`), and then no material set and
7950
+ * each cache back to what `pipelineCompile` says.
7951
+ */
7952
+ preparing(material, start, options) {
7953
+ this.setMaterial(material);
7954
+ this.skipCompiling(true);
7955
+ try {
7956
+ start(this.pipelines, false);
7957
+ if (options.translucent === true) {
7958
+ start(this.pipelines, true);
7959
+ /* A blended draw in a reconstructing frame lands in the late pass, whose cache is its own. */
7960
+ if (this.latePipelines !== this.pipelines)
7961
+ start(this.latePipelines, true);
7962
+ }
7963
+ }
7964
+ finally {
7965
+ this.skipCompiling(this.quality.pipelineCompile === 'skip');
7966
+ this.setMaterial(null);
7967
+ }
7968
+ }
7969
+ /** What `meshShape` worked out for the mesh draw at hand. Reused, so a draw allocates no record. */
7970
+ drawShape = {
7971
+ base: '',
7972
+ key: '',
7973
+ depthWrite: true,
7974
+ layer: 0,
7975
+ adds: false,
7976
+ coverage: false,
7977
+ splits: false,
7978
+ modelKind: null,
7979
+ };
7980
+ /**
7981
+ * The key a mesh draw's pipeline is cached under, and the pipeline state that key encodes, under
7982
+ * the current material and these options, into `drawShape`. **The one statement of it**:
7983
+ * `submitMesh` draws by it and `prepareMesh` compiles by it, so the two cannot disagree about a key.
7984
+ */
7985
+ meshShape(mesh, blend, options, skinned, morphed, eight, clothed) {
7986
+ const modelKind = this.materialModel?.kind ?? null;
7987
+ const base = mesh.key ?? 'flat:s0:u0';
7988
+ const keyed = `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
7989
+ `${clothed ? '|cloth' : ''}`;
7990
+ /*
7991
+ * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
7992
+ * the key: two draws of one mesh differing only in these would otherwise share a cached
7993
+ * pipeline and the second would silently take the first one's depth behaviour.
7994
+ *
7995
+ * **Both suffixes are empty for the defaults**, which is what keeps the keys `createMesh`
7996
+ * warms — `fullKey` and `${'`'}${'$'}{fullKey}|blend${'`'}` — the ones an ordinary draw still asks for. A
7997
+ * caller that opts in pays one compile on its first draw, the same bargain the overlay
7998
+ * pipelines above already make.
7999
+ */
8000
+ const depthWrite = options.depthWrite ?? true;
8001
+ const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8002
+ /* Added rather than blended over: its own blend state, so its own pipeline. */
8003
+ const adds = blend && options.additive === true;
8004
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
8005
+ /* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
8006
+ const splits = modelKind === 'skin' && !blend && this.skinSplits();
8007
+ const key = `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
8008
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8009
+ /* The two order-independent buffers are two formats and two blend states, so a draw into
8010
+ one must never share a cached pipeline with the same mesh drawn into the other. */
8011
+ `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
8012
+ /* Culling is pipeline state here: a two-sided surface culls nothing. */
8013
+ `${this.materialDoubleSided ? '|2s' : ''}` +
8014
+ /* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
8015
+ `${coverage ? '|a2c' : ''}` +
8016
+ /* And the material's shading model, which is the fragment stage's own switch. */
8017
+ `${modelKind === null ? '' : `|m:${modelKind}`}` +
8018
+ /* And which half of a skin it draws, under the screen-space blur. */
8019
+ `${splits ? '|ss' : ''}`;
8020
+ const shape = this.drawShape;
8021
+ shape.base = base;
8022
+ shape.key = key;
8023
+ shape.depthWrite = depthWrite;
8024
+ shape.layer = layer;
8025
+ shape.adds = adds;
8026
+ shape.coverage = coverage;
8027
+ shape.splits = splits;
8028
+ shape.modelKind = modelKind;
8029
+ return shape;
8030
+ }
8031
+ /**
8032
+ * The pipeline `drawShape` names: the one built, one built here, or none where draws skip a
8033
+ * compile and this one has not landed (`PipelineCache.lit`) — and none for a split skin until all
8034
+ * of its halves have.
8035
+ */
8036
+ meshPipeline(blend, lands, skinned, morphed, eight, clothed,
8037
+ /** Which cache to build into: the one the draw's target reads, unless a preparation says. */
8038
+ into = null) {
8039
+ const { base, key, depthWrite, layer, adds, coverage, splits, modelKind } = this.drawShape;
8040
+ /*
8041
+ * **Asynchronous by default, synchronous if it has to be, and never absent.**
8042
+ *
8043
+ * `createMesh` compiles off the main thread, so a draw can legitimately arrive before the
8044
+ * driver has finished — a consumer that awaits `ready()` never sees this, and one that
8045
+ * draws immediately gets the old behaviour rather than an exception. Correctness cannot be
8046
+ * allowed to depend on which of those happened, so the miss builds the pipeline here and
8047
+ * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
8048
+ * thrown error in the middle of one.
8049
+ */
8050
+ const pipelines = into ?? (blend ? this.blendedPipelines(lands) : this.targetPipelines());
8051
+ let pipeline = pipelines.peek(key) ?? null;
8052
+ if (pipeline === null) {
8053
+ const present = this.meshPresent.get(base);
8054
+ if (present === undefined) {
8055
+ throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
8056
+ }
8057
+ 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');
8058
+ }
8059
+ /* A split skin draws its three halves or none of them. */
8060
+ if (pipeline !== null &&
8061
+ splits &&
8062
+ !this.skinHalvesReady(key, base, skinned, morphed, layer, eight, clothed)) {
8063
+ pipeline = null;
8064
+ }
8065
+ return pipeline;
8066
+ }
7883
8067
  /**
7884
8068
  * @param blend Which of the two entry points above this came through, and **not** something to
7885
8069
  * infer from `opacity`.
@@ -7999,7 +8183,6 @@ export class WebGPURenderer {
7999
8183
  const modelKind = this.materialModel?.kind ?? null;
8000
8184
  if (modelKind === 'eye')
8001
8185
  this.writeEyeAxis(model, this.skinPaletteSlot >= 0 && mesh.isSkinned);
8002
- const base = mesh.key ?? 'flat:s0:u0';
8003
8186
  /*
8004
8187
  * A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
8005
8188
  * key carries the flag so the two never share a cached pipeline. A rigged mesh whose caller
@@ -8014,38 +8197,6 @@ export class WebGPURenderer {
8014
8197
  /* A cloth set and a skinned draw: the pipeline built with CLOTH_BOUND on, warmed when the
8015
8198
  binding was made. See `createClothBinding`. */
8016
8199
  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
8200
  /*
8050
8201
  * The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
8051
8202
  * field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
@@ -8058,31 +8209,9 @@ export class WebGPURenderer {
8058
8209
  this.perDraw.writeInt(slot, fields.uMorphTargetCount.offset, mesh.morph.targetCount);
8059
8210
  this.perDraw.writeInt(slot, fields.uMorphTextureWidth.offset, mesh.morph.width);
8060
8211
  }
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
- }
8212
+ /* Its pipeline, by the key a prepared draw compiled too: see `meshShape`. */
8213
+ const { base, key, layer, depthWrite, splits } = this.meshShape(mesh, blend, options, skinned, morphed, eight, clothed);
8214
+ let pipeline = this.meshPipeline(blend, lands, skinned, morphed, eight, clothed);
8086
8215
  /*
8087
8216
  * Guards the draw call only, not the restore below.
8088
8217
  *
@@ -8388,6 +8517,8 @@ export class WebGPURenderer {
8388
8517
  const seeAt = this.materialField('uSeeThrough');
8389
8518
  this.perFrameFloats[seeAt] = 0;
8390
8519
  this.perFrameFloats[seeAt + 2] = 0;
8520
+ /* The material's light through, over the pane's tint: transmission.ts. */
8521
+ this.perFrameFloats.set(this.transmission, this.materialField('uGlassTint'));
8391
8522
  }
8392
8523
  /**
8393
8524
  * One draw, one material slot, one uniform slot, however many instances.
@@ -8401,6 +8532,41 @@ export class WebGPURenderer {
8401
8532
  * **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
8402
8533
  * is built, and a per-instance opacity: `opacity` here dims the whole batch.
8403
8534
  */
8535
+ /**
8536
+ * An instanced draw's pipeline under the current material and these options: the one built, one
8537
+ * built here, or none while it compiles (`PipelineCache.lit`), its key, base and model left in
8538
+ * `drawShape`. **The one statement of an instanced key**: `submitInstanced` draws by it and
8539
+ * `prepareInstanced` compiles by it.
8540
+ */
8541
+ instancedPipeline(mesh, animated, blend, lands, options, depthWrite, layer, coverage,
8542
+ /** Which cache to build into: the one the draw's target reads, unless a preparation says. */
8543
+ into = null) {
8544
+ /* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
8545
+ const modelKind = this.materialModel?.kind ?? null;
8546
+ const base = mesh.key ?? 'flat:s0:u0';
8547
+ /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
8548
+ a third vertex buffer and a different vertex module, and sharing a cache entry with the
8549
+ plain one would hand a draw the wrong layout. */
8550
+ const adds = blend && options.additive === true;
8551
+ const key = `${base}|inst${!animated ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
8552
+ `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8553
+ `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
8554
+ `${modelKind === null ? '' : `|m:${modelKind}`}`;
8555
+ const shape = this.drawShape;
8556
+ shape.base = base;
8557
+ shape.key = key;
8558
+ shape.modelKind = modelKind;
8559
+ const pipelines = into ?? (blend ? this.blendedPipelines(lands) : this.targetPipelines());
8560
+ let pipeline = pipelines.peek(key) ?? null;
8561
+ if (pipeline === null) {
8562
+ const present = this.meshPresent.get(base);
8563
+ if (present === undefined) {
8564
+ throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
8565
+ }
8566
+ 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);
8567
+ }
8568
+ return pipeline;
8569
+ }
8404
8570
  /**
8405
8571
  * The next of a batch's cull slots this frame, with its cull queued against the current view; null
8406
8572
  * when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
@@ -8504,26 +8670,9 @@ export class WebGPURenderer {
8504
8670
  const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
8505
8671
  if (own)
8506
8672
  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
- }
8673
+ /* Its pipeline, by the key a prepared batch compiled too: see `instancedPipeline`. */
8674
+ const pipeline = this.instancedPipeline(mesh, animation !== null, blend, lands, options, depthWrite, layer, coverage);
8675
+ const { base, key, modelKind } = this.drawShape;
8527
8676
  /* A batch whose pipeline is still compiling is left out, as a mesh is. */
8528
8677
  const material = pipeline === null ? null : this.materialSlotForDraw();
8529
8678
  if (pipeline !== null && material !== null) {
@@ -11581,6 +11730,7 @@ export class WebGPURenderer {
11581
11730
  const f = this.aoFloats;
11582
11731
  const at = (name) => this.postField(AO_FRAG_FIELDS, name);
11583
11732
  f[at('uRadius')] = this.quality.ambientOcclusionRadius;
11733
+ f.set(this.aoFade, at('uFade'));
11584
11734
  f.set(this.aoProjScale, at('uProjScale'));
11585
11735
  f.set(this.aoInvProjection, at('uInvProjection'));
11586
11736
  device.queue.writeBuffer(this.aoUniforms, 0, this.aoStaging);
@@ -14172,6 +14322,13 @@ export class WebGPURenderer {
14172
14322
  this.exposurePass = new ExposurePass(this.surface.device, this.pipelines, this.postSampler);
14173
14323
  this.rebuildRushBindGroup();
14174
14324
  }
14325
+ /**
14326
+ * Where ambient occlusion fades out with distance. **The other backend's
14327
+ * `setAmbientOcclusionFade` carries the reasoning**; this is the same setter over the same state.
14328
+ */
14329
+ setAmbientOcclusionFade(distance, radius) {
14330
+ resolveOcclusionFade(distance, radius, this.aoFade);
14331
+ }
14175
14332
  /**
14176
14333
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
14177
14334
  * 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
  /**