@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
package/CHANGELOG.json CHANGED
@@ -1,4 +1,40 @@
1
1
  [
2
+ {
3
+ "version": "4.11.1",
4
+ "date": "2026-10-10",
5
+ "entries": [
6
+ {
7
+ "kind": "added",
8
+ "short": "`SurfaceMaterial.layers` takes its mask from the ORM array or the vertex colour, sums layers, places them by the world, lays one by facing up, and lays a normal under them.",
9
+ "text": "`layers` covers more of the ways a layered surface is blended. `mask: 'orm'` reads the mask from the ORM array's layer past the material's layers, which leaves the model's slot to a lightmap's page, so a lightmapped surface blends its layers; `mask: 'vertex'` takes the weights from the vertex colour, which then tints nothing. `blend: 'sum'` mixes the base toward each layer times its weight, by the weights' sum held to 1, besides laying each over the ones before it. Under a `projection` the layers are placed by the world, each at its own repeats a metre, and `addMask` adds a mask placed the same way to one layer's weight. `facing` weighs one layer by how much the surface faces up, and `meshNormal` lays a normal map read at the mesh's own coordinates under the layers' blended normal. A triplanar projection lays layers on the horizontal plane, and says so once. On `demo/dev/layerFamilies.html` a mask carried by the ORM array draws exactly as the same mask as a map, keeps its layers under a lightmap's page, and every mode draws the same on both backends."
10
+ },
11
+ {
12
+ "kind": "added",
13
+ "short": "`SurfaceMaterial.transmissionColor` names the colour light through a thin surface takes, where it was always the surface's own.",
14
+ "text": "`transmissionColor`, linear, is the colour the light from behind a thin surface (`diffuseTransmission`) takes through it, in place of the surface's own colour: a printed banner glowing a flat red behind its print rather than lit through in the print's colours. Absent, the surface's colour, as before. It rides the glass tint's colour lanes, which no opaque material reads, and a pane drawn between two draws of the material puts it back. On `demo/dev/thinLight.html?color=0,0.4,1` a red banner lit from behind went from 163, 24, 19 to 16, 74, 171 under the sun with no red let through, the same to the pixel on both backends."
15
+ },
16
+ {
17
+ "kind": "added",
18
+ "short": "`setAmbientOcclusionFade(distance, radius)` fades ambient occlusion out with distance, so a sky dome or a far range is not shaded in rings.",
19
+ "text": "Ambient occlusion is whole up to `distance` metres from the eye and gone `radius` metres past it, held until changed; a negative distance, the default, fades nothing. Far off, the depth buffer's steps are metres apart and the estimate shaded them in bands across a sky dome or a mountain range; past the fade those pixels are left open and skip the estimate's walk. Bound in DriftScript as `drift/render`'s `occlusionFade`. On `demo/dev/aoFade.html` a ridge 3 km off went from 66,267 banded pixels to none, while the feet of the near boxes darkened exactly as before, on both backends."
20
+ },
21
+ {
22
+ "kind": "added",
23
+ "short": "A lightmapped instance fades by `setDitherOpacity` too: its opacity rides its page region in sixty-fourths, where it had none.",
24
+ "text": "A lightmapped batch carries its page regions in the lane `MeshInstances.alphas` would take, so its instances could not fade. Their opacity now travels as whole steps added to the region's U offset, 64 of them, the screen door's own resolution, and the vertex stage takes them back off. An opaque instance's region is written to the bit as before; a fading one's offset keeps 16 bits of fraction, a sixteenth of a texel on a 4,096 page. On `demo/dev/lightmap.html?mode=tiles&fade=0.5` the faded tiles keep 50.0% of their pixels, each lit exactly as unfaded, and nothing else changes, on both backends."
25
+ },
26
+ {
27
+ "kind": "added",
28
+ "short": "`prepareMesh` and `prepareInstanced` compile a draw's pipelines in its material before its first frame, so a scene drawn under `pipelineCompile: 'skip'` shows whole.",
29
+ "text": "`prepareMesh(mesh, material)` and `prepareInstanced(batch, material)` compile, off the main thread, every pipeline a draw of that mesh or batch in that material takes that making it did not: a two-sided, cut-out or surface-model variant, a skin's halves under the screen-space blur, and the material's lit switches; `{ translucent: true }` adds the blended ones. `ready()` resolves once they have. Under `pipelineCompile: 'skip'` a draw whose pipeline is new is left out until it lands, so a figure brought on screen came without its skin and hair for a few frames; prepared, it is drawn whole in its first frame. On WebGPU a prepared key is the one the draw asks for, worked out by the same function; on WebGL2 a surface model's programs are what a draw can wait for, and are what it prepares."
30
+ },
31
+ {
32
+ "kind": "added",
33
+ "short": "`holdSplash(promise, { capMs })` keeps the engine's badge up until the game's own first screen has loaded, under a cap of its own.",
34
+ "text": "The badge left three seconds after the first frame, twenty at most, so a game whose first screen loads for longer was seen loading. `holdSplash` keeps it up until the promise settles, either way, as well as until the first frame; frames run behind it while the game loads, since a load may need them, and are held once it has settled for what is left of the minimum. `capMs` is the game's ceiling in place of the engine's twenty seconds, the longest of several holds, and the call answers whether there was a badge to hold. On `demo/dev/splashHold.html` a load settling at 6.2 s took the badge down at 6.7 s with 361 frames run under it, a rejected load the same, and an 8-second cap at 8.4 s."
35
+ }
36
+ ]
37
+ },
2
38
  {
3
39
  "version": "4.11.0",
4
40
  "date": "2026-10-10",
package/README.md CHANGED
@@ -6,7 +6,7 @@ WebGL2 everywhere else, behind one API, and carries the fixed-step loop, the ren
6
6
  input, the scene graph, geometry, text and the whole of `@driftengine/physics`, which it re-exports.
7
7
  Everything else is a separate package a game adds when it reaches for it.
8
8
 
9
- **Cost: 898.2 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
9
+ **Cost: 904.6 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
10
10
  which fails the build if the figure drifts by more than 3%.
11
11
 
12
12
  The manual is at **[driftengine.dev/docs](https://driftengine.dev/docs)**, with an example running
package/dist/index.d.ts CHANGED
@@ -155,7 +155,7 @@ export { BLOOM_LEVELS } from './render/bloomChain.ts';
155
155
  export type { BloomResponse } from './render/bloomChain.ts';
156
156
  export { probeDevice } from './render/backend/probe.ts';
157
157
  export type { ProbeVerdict } from './render/backend/probe.ts';
158
- export type { BoltHandle, CausticsHandle, ClothBindingHandle, ClothParticlesHandle, FlockHandle, IncrementalMeshHandle, LineHandle, MeshHandle, MeshOptions, RenderBackend, RendererApi, ParticleHandle, PlumeHandle, ScatterHandle, InstancedHandle, SdfTextHandle, SurfaceTextureHandle, TextHandle, WaterHandle, WindStreakHandle, } from './render/backend/api.ts';
158
+ export type { BoltHandle, CausticsHandle, ClothBindingHandle, ClothParticlesHandle, FlockHandle, IncrementalMeshHandle, LineHandle, MeshHandle, MeshOptions, PrepareOptions, RenderBackend, RendererApi, ParticleHandle, PlumeHandle, ScatterHandle, InstancedHandle, SdfTextHandle, SurfaceTextureHandle, TextHandle, WaterHandle, WindStreakHandle, } from './render/backend/api.ts';
159
159
  export type { ClothBindingData } from './render/clothBindingData.ts';
160
160
  export { anisotropicModel, eyeModel, hairModel, lightmapModel, skinModel, } from './render/surfaceModel.ts';
161
161
  export type { AnisotropicModel, EyeModel, HairModel, LightmapModel, SkinModel, SurfaceModel, SurfaceModelKind, } from './render/surfaceModel.ts';
@@ -344,7 +344,7 @@ export type { TouchControlsOptions } from './input/touchControls.ts';
344
344
  export { pixelCursor } from './ui/pixelCursor.ts';
345
345
  export { fullscreenWanted, requestFullscreenOnGesture } from './ui/fullscreen.ts';
346
346
  export type { FullscreenConditions } from './ui/fullscreen.ts';
347
- export { SPLASH_FADE_MS, SPLASH_HARD_CAP_MS, SPLASH_MIN_MS, forcedSplash, mountSplash, splashDecision, splashWanted, } from './ui/splash.ts';
347
+ export { SPLASH_FADE_MS, SPLASH_HARD_CAP_MS, SPLASH_MIN_MS, forcedSplash, holdSplash, mountSplash, splashDecision, splashWanted, } from './ui/splash.ts';
348
348
  export type { MountedSplash, SplashConditions, SplashDecision, SplashOptions, } from './ui/splash.ts';
349
349
  export { DEFAULT_CLIP_FPS, FrameRecorder, offerClip } from './ui/frameRecorder.ts';
350
350
  export { stillFrame } from './ui/stillFrame.ts';
package/dist/index.js CHANGED
@@ -439,7 +439,7 @@ export { fullscreenWanted, requestFullscreenOnGesture } from './ui/fullscreen.js
439
439
  * they are here for the consumer that declines the automatic one and places its own, and because
440
440
  * the two decisions behind it are worth reading before overriding either.
441
441
  */
442
- export { SPLASH_FADE_MS, SPLASH_HARD_CAP_MS, SPLASH_MIN_MS, forcedSplash, mountSplash, splashDecision, splashWanted, } from './ui/splash.js';
442
+ export { SPLASH_FADE_MS, SPLASH_HARD_CAP_MS, SPLASH_MIN_MS, forcedSplash, holdSplash, mountSplash, splashDecision, splashWanted, } from './ui/splash.js';
443
443
  export { DEFAULT_CLIP_FPS, FrameRecorder, offerClip } from './ui/frameRecorder.js';
444
444
  // Not on `FrameRecorder`, so a screenshot does not cost a clip pipeline. See the module.
445
445
  export { stillFrame } from './ui/stillFrame.js';
@@ -46,8 +46,9 @@ export declare class AmbientOcclusionPass {
46
46
  * by, or null if this pass could not run, which the caller must read as "no occlusion"
47
47
  * rather than as black.
48
48
  *
49
- * `invProjection` is column-major, as `gl-matrix` produces it.
49
+ * `invProjection` is column-major, as `gl-matrix` produces it. `fade` is `uFade`, where the
50
+ * occlusion fades out with distance (`occlusionFade.ts`).
50
51
  */
51
- run(depth: WebGLTexture, width: number, height: number, radius: number, projScale: Float32Array, invProjection: Float32Array): WebGLTexture | null;
52
+ run(depth: WebGLTexture, width: number, height: number, radius: number, projScale: Float32Array, invProjection: Float32Array, fade: Float32Array): WebGLTexture | null;
52
53
  dispose(): void;
53
54
  }
@@ -102,9 +102,10 @@ export class AmbientOcclusionPass {
102
102
  * by, or null if this pass could not run, which the caller must read as "no occlusion"
103
103
  * rather than as black.
104
104
  *
105
- * `invProjection` is column-major, as `gl-matrix` produces it.
105
+ * `invProjection` is column-major, as `gl-matrix` produces it. `fade` is `uFade`, where the
106
+ * occlusion fades out with distance (`occlusionFade.ts`).
106
107
  */
107
- run(depth, width, height, radius, projScale, invProjection) {
108
+ run(depth, width, height, radius, projScale, invProjection, fade) {
108
109
  if (!this.ensureSize(width, height))
109
110
  return null;
110
111
  const { gl } = this;
@@ -123,6 +124,7 @@ export class AmbientOcclusionPass {
123
124
  gl.uniform1i(this.estimateUniforms['uDepth'] ?? null, 0);
124
125
  gl.uniform2fv(this.estimateUniforms['uProjScale'] ?? null, projScale);
125
126
  gl.uniform1f(this.estimateUniforms['uRadius'] ?? null, radius);
127
+ gl.uniform2fv(this.estimateUniforms['uFade'] ?? null, fade);
126
128
  gl.uniformMatrix4fv(this.estimateUniforms['uInvProjection'] ?? null, false, invProjection);
127
129
  gl.drawArrays(gl.TRIANGLES, 0, 3);
128
130
  /*
@@ -177,6 +177,14 @@ export interface SurfaceTextureHandle {
177
177
  * `WebGLBuffer` in a private field, so no WebGPU object can satisfy it however identical its
178
178
  * shape. It is not meant to be read; it is meant to be enforced.
179
179
  */
180
+ /** Which of a draw's pipelines `prepareMesh` and `prepareInstanced` compile beside the opaque ones. */
181
+ export interface PrepareOptions {
182
+ /**
183
+ * The blended ones too, for a draw made with `drawTranslucentMesh` or `drawTranslucentInstanced`.
184
+ * Off by default: an opaque surface would pay a compile it never draws with.
185
+ */
186
+ readonly translucent?: boolean;
187
+ }
180
188
  /** What a mesh needs to be told at creation that its geometry cannot say. */
181
189
  export interface MeshOptions {
182
190
  /**
@@ -190,7 +198,7 @@ export interface MeshOptions {
190
198
  }
191
199
  export type RendererApi = Omit<{
192
200
  [K in keyof Renderer]: Renderer[K];
193
- }, 'gpuTimer' | 'createMesh' | 'createMeshIncremental' | 'updateMesh' | 'drawMesh' | 'disposeMesh' | 'drawTranslucentMesh' | 'drawLightVolume' | 'drawFilm' | 'createScatter' | 'drawScatter' | 'uploadScatter' | 'disposeScatter' | 'createInstanced' | 'uploadInstanced' | 'drawInstanced' | 'drawTranslucentInstanced' | 'disposeInstanced' | 'createWater' | 'drawWater' | 'disposeWater' | 'createPlumes' | 'drawPlumes' | 'disposePlumes' | 'setPlumeScale' | 'createCaustics' | 'drawCaustics' | 'disposeCaustics' | 'createParticles' | 'drawParticles' | 'drawDeviceParticles' | 'disposeParticles' | 'createWindStreaks' | 'drawWindStreaks' | 'disposeWindStreaks' | 'createFlock' | 'drawFlock' | 'disposeFlock' | 'createBolts' | 'drawBolts' | 'disposeBolts' | 'createLines' | 'drawLines' | 'disposeLines' | 'createText' | 'setText' | 'setPlate' | 'drawText' | 'textWidth' | 'disposeText' | 'createSdfText' | 'setSdfText' | 'drawSdfText' | 'disposeSdfText' | 'createSurfaceTexture' | 'createSurfaceTextureArray' | 'createLightmap' | 'createSceneCapture' | 'captureScene' | 'updateSurfaceTexture' | 'disposeSurfaceTexture' | 'setSurfaceTexture' | 'setMaterial' | 'setSurfaceOverlay' | 'createClothBinding' | 'createClothParticles' | 'updateClothParticles' | 'setCloth' | 'disposeClothBinding' | 'disposeClothParticles'> & {
201
+ }, 'gpuTimer' | 'createMesh' | 'createMeshIncremental' | 'updateMesh' | 'drawMesh' | 'disposeMesh' | 'drawTranslucentMesh' | 'drawLightVolume' | 'drawFilm' | 'createScatter' | 'drawScatter' | 'uploadScatter' | 'disposeScatter' | 'createInstanced' | 'uploadInstanced' | 'drawInstanced' | 'drawTranslucentInstanced' | 'disposeInstanced' | 'createWater' | 'drawWater' | 'disposeWater' | 'createPlumes' | 'drawPlumes' | 'disposePlumes' | 'setPlumeScale' | 'createCaustics' | 'drawCaustics' | 'disposeCaustics' | 'createParticles' | 'drawParticles' | 'drawDeviceParticles' | 'disposeParticles' | 'createWindStreaks' | 'drawWindStreaks' | 'disposeWindStreaks' | 'createFlock' | 'drawFlock' | 'disposeFlock' | 'createBolts' | 'drawBolts' | 'disposeBolts' | 'createLines' | 'drawLines' | 'disposeLines' | 'createText' | 'setText' | 'setPlate' | 'drawText' | 'textWidth' | 'disposeText' | 'createSdfText' | 'setSdfText' | 'drawSdfText' | 'disposeSdfText' | 'createSurfaceTexture' | 'createSurfaceTextureArray' | 'createLightmap' | 'createSceneCapture' | 'captureScene' | 'updateSurfaceTexture' | 'disposeSurfaceTexture' | 'setSurfaceTexture' | 'setMaterial' | 'prepareMesh' | 'prepareInstanced' | 'setSurfaceOverlay' | 'createClothBinding' | 'createClothParticles' | 'updateClothParticles' | 'setCloth' | 'disposeClothBinding' | 'disposeClothParticles'> & {
194
202
  /**
195
203
  * A mesh's cloth binding: which simulation triangle each of its vertices follows, where on it and
196
204
  * by how much (`ClothBindingData`). The mesh must be skinned — a binding blends each vertex
@@ -299,6 +307,18 @@ export type RendererApi = Omit<{
299
307
  * setter each — and `setSurfaceTexture` is now a wrapper over it.
300
308
  */
301
309
  setMaterial(material: SurfaceMaterial<SurfaceTextureHandle> | null): void;
310
+ /**
311
+ * Compile, off the main thread, every pipeline a draw of `mesh` in `material` takes that making
312
+ * the mesh did not — a two-sided, cut-out or shaded-by-a-model variant, a material's lit switches,
313
+ * a skin's halves — so the first frame that draws it waits on none; `ready()` resolves once they
314
+ * have. For `pipelineCompile: 'skip'` above all, where a draw whose pipeline is new is left out
315
+ * until it lands: a figure brought on screen whole rather than without its face for a few frames.
316
+ * A rigged mesh is prepared as drawn with a palette, and one with targets both with weights and
317
+ * without. Call it between frames: it sets `material` to read it and leaves none set after.
318
+ */
319
+ prepareMesh(mesh: MeshHandle, material: SurfaceMaterial<SurfaceTextureHandle> | null, options?: PrepareOptions): void;
320
+ /** `prepareMesh` for an instanced batch drawn in `material`, its own pipelines being its own. */
321
+ prepareInstanced(batch: InstancedHandle, material: SurfaceMaterial<SurfaceTextureHandle> | null, options?: PrepareOptions): void;
302
322
  /**
303
323
  * A 3D text object: one string, drawn as instanced glyph cubes over the scene.
304
324
  *
@@ -49,7 +49,7 @@ import type { RenderQuality, RenderQualityOptions } from '../../renderQuality.ts
49
49
  import { Mesh } from '../../mesh.ts';
50
50
  import type { IncrementalMesh } from '../../mesh.ts';
51
51
  import type { MeshData } from '../../mesh.ts';
52
- import type { MeshOptions } from '../api.ts';
52
+ import type { MeshOptions, PrepareOptions } from '../api.ts';
53
53
  import { SurfaceTexture } from '../../surfaceTexture.ts';
54
54
  import type { CompressedTextureFormat, CompressedTextureSource, SurfaceSource } from '../../compressedSource.ts';
55
55
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
@@ -1114,6 +1114,8 @@ export declare class WebGL2Renderer implements RendererApi {
1114
1114
  private readonly aoProjScale;
1115
1115
  /** Where this frame's jitter put the occlusion, in the composite's uv. See `uAoOffset`. */
1116
1116
  private readonly aoOffset;
1117
+ /** Where occlusion fades out with distance, `uFade`: none until a caller sets one. */
1118
+ private readonly aoFade;
1117
1119
  /**
1118
1120
  * Whether this frame has already been resolved to the canvas.
1119
1121
  *
@@ -1281,14 +1283,18 @@ export declare class WebGL2Renderer implements RendererApi {
1281
1283
  private warnedProjectionRoom;
1282
1284
  /** `uWorldUv`, the current material's projection packed. See `surfaceProjection.ts`. */
1283
1285
  private readonly projection;
1284
- /** The current material's `diffuseTransmission`, `uGlassTint.w`, 0 to 1. */
1285
- private diffuseTransmission;
1286
+ /**
1287
+ * `uGlassTint` as the current material leaves it: its light from behind and that light's colour,
1288
+ * which a pane overwrites for its own draw and `unbindSeeThrough` puts back. See `transmission.ts`.
1289
+ */
1290
+ private readonly transmission;
1286
1291
  /** `uLayers`, the current material's layers packed. See `surfaceLayers.ts`. */
1287
1292
  private readonly layerFloats;
1288
1293
  /** Whether the lit stage has room for layers' two vectors, and which refusals were said. */
1289
1294
  private layersRoom;
1290
1295
  private warnedLayersRoom;
1291
1296
  private warnedLayersModelMap;
1297
+ private warnedLayersTriplanar;
1292
1298
  private warnedOverlayRoom;
1293
1299
  private warnedOverlayBlocks;
1294
1300
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
@@ -1959,6 +1965,25 @@ export declare class WebGL2Renderer implements RendererApi {
1959
1965
  * `uScale`/`vScale` are repeats across the mesh's own UV range: geometry authored in
1960
1966
  * metres is textured at whatever density the material wants without rebuilding it.
1961
1967
  */
1968
+ /**
1969
+ * Compile, off the frame, every program a draw of `mesh` in `material` takes that making the mesh
1970
+ * did not, so the first frame that draws it waits on none: the program of the material's shading
1971
+ * model for each vertex variant the mesh can be drawn with (its rig, with a palette; its targets,
1972
+ * with weights and without), a skin's halves under the screen-space blur, and the lit switches
1973
+ * the material turns on. `ready()` resolves once they have.
1974
+ *
1975
+ * **For `pipelineCompile: 'skip'` above all**, where a draw whose program is new is left out until
1976
+ * it lands: a figure brought on screen whole rather than without its face and hair for the frames
1977
+ * those take. Where draws wait, the programs compile here, at the call, rather than in a frame.
1978
+ * Two-sided and cut-out surfaces are draw state on this backend and need nothing; on WebGPU they
1979
+ * are pipelines of their own, prepared the same way.
1980
+ *
1981
+ * **Call it between frames**: it sets `material` to read it and leaves no material set after, as
1982
+ * `setMaterial(null)` does. `options` is the other backend's; blended draws share a program here.
1983
+ */
1984
+ prepareMesh(mesh: Mesh, material: SurfaceMaterial | null, _options?: PrepareOptions): void;
1985
+ /** `prepareMesh` for an instanced batch: its own programs, and its model's for its variant. */
1986
+ prepareInstanced(batch: InstancedBatch, material: SurfaceMaterial | null, _options?: PrepareOptions): void;
1962
1987
  /**
1963
1988
  * Choose the joint palette the following `drawMesh` calls skin by, or null for none.
1964
1989
  *
@@ -2140,6 +2165,8 @@ export declare class WebGL2Renderer implements RendererApi {
2140
2165
  * no room for their two vectors each leave one layer, the last two said once each.
2141
2166
  */
2142
2167
  private layersOf;
2168
+ /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
2169
+ private warnLayersTriplanar;
2143
2170
  /** Whether the lit shader with `feature` compiled in fits this GPU's fragment uniform vectors. */
2144
2171
  private switchFits;
2145
2172
  /**
@@ -2236,7 +2263,8 @@ export declare class WebGL2Renderer implements RendererApi {
2236
2263
  *
2237
2264
  * What it gives up: a fade reads as grain while it lasts, which a temporal resolve smooths and a
2238
2265
  * plain frame does not; a blended draw that asks spends its opacity here and blends none of it;
2239
- * and a lightmapped batch carries regions where the opacities would be, so it has none to fade.
2266
+ * and a lightmapped batch's opacities ride its page regions, so they fade in steps of a
2267
+ * sixty-fourth, which is the pattern's own resolution (`MeshInstances.lightmapRegions`).
2240
2268
  */
2241
2269
  setDitherOpacity(on: boolean): void;
2242
2270
  /**
@@ -2404,6 +2432,8 @@ export declare class WebGL2Renderer implements RendererApi {
2404
2432
  * taking it mid-bake is how a probe's next face came to be attached to the scene's framebuffer.
2405
2433
  */
2406
2434
  private bindSeeThrough;
2435
+ /** `uGlassTint` as the current material leaves it. See `transmission`. */
2436
+ private writeTransmission;
2407
2437
  /** Put back what `bindSeeThrough` bound: the binding outlives its frame (the 2026-08-27 rule). */
2408
2438
  private unbindSeeThrough;
2409
2439
  disposeMesh(mesh: Mesh): void;
@@ -3293,6 +3323,17 @@ export declare class WebGL2Renderer implements RendererApi {
3293
3323
  * once rather than doing nothing. See `shaders/localExposure.ts`.
3294
3324
  */
3295
3325
  setLocalExposure(strength: number): void;
3326
+ /**
3327
+ * Where ambient occlusion fades out with distance: whole up to `distance` metres from the eye and
3328
+ * gone `radius` metres past it, so the far depth buffer's steps — a sky dome kilometres off, a
3329
+ * mountain range — are not shaded in rings, and those pixels skip the occlusion's sampling.
3330
+ *
3331
+ * A distance that is not a finite number at or above zero is no fade, which is the default, and
3332
+ * `Infinity` reads as "never fades"; a radius below zero is a cut at `distance`. Set before
3333
+ * `endFrame` and held until changed, like `setDepthOfField`. Nothing at all when
3334
+ * `ambientOcclusion` is 0. See `occlusionFade.ts` for what it gives up.
3335
+ */
3336
+ setAmbientOcclusionFade(distance: number, radius: number): void;
3296
3337
  /**
3297
3338
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
3298
3339
  * to take.
@@ -37,8 +37,10 @@ import { compileProgram, finishProgram, startProgram, uniformLocations } from '.
37
37
  import { countUniformVectors, FULL_LIGHT_BUDGET, nextLightBudget, planLightBudget, } from '../../uniformVectorBudget.js';
38
38
  import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
39
39
  import { PROJECTION_FLOATS, packSurfaceProjection, projects } from '../../surfaceProjection.js';
40
+ import { resolveTransmission } from '../../transmission.js';
41
+ import { resolveOcclusionFade } from '../../occlusionFade.js';
40
42
  import { orthographicDepthSpan } from '../../lightMatrix.js';
41
- import { LAYER_FLOATS, packSurfaceLayers } from '../../surfaceLayers.js';
43
+ import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
42
44
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.js';
43
45
  import { SceneTarget } from '../../sceneTarget.js';
44
46
  import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.js';
@@ -785,6 +787,8 @@ export class WebGL2Renderer {
785
787
  aoProjScale = new Float32Array(2);
786
788
  /** Where this frame's jitter put the occlusion, in the composite's uv. See `uAoOffset`. */
787
789
  aoOffset = new Float32Array(2);
790
+ /** Where occlusion fades out with distance, `uFade`: none until a caller sets one. */
791
+ aoFade = new Float32Array([-1, 0]);
788
792
  /**
789
793
  * Whether this frame has already been resolved to the canvas.
790
794
  *
@@ -952,14 +956,18 @@ export class WebGL2Renderer {
952
956
  warnedProjectionRoom = false;
953
957
  /** `uWorldUv`, the current material's projection packed. See `surfaceProjection.ts`. */
954
958
  projection = new Float32Array(PROJECTION_FLOATS);
955
- /** The current material's `diffuseTransmission`, `uGlassTint.w`, 0 to 1. */
956
- diffuseTransmission = 0;
959
+ /**
960
+ * `uGlassTint` as the current material leaves it: its light from behind and that light's colour,
961
+ * which a pane overwrites for its own draw and `unbindSeeThrough` puts back. See `transmission.ts`.
962
+ */
963
+ transmission = new Float32Array([-1, -1, -1, 0]);
957
964
  /** `uLayers`, the current material's layers packed. See `surfaceLayers.ts`. */
958
965
  layerFloats = new Float32Array(LAYER_FLOATS);
959
966
  /** Whether the lit stage has room for layers' two vectors, and which refusals were said. */
960
967
  layersRoom = null;
961
968
  warnedLayersRoom = false;
962
969
  warnedLayersModelMap = false;
970
+ warnedLayersTriplanar = false;
963
971
  warnedOverlayRoom = false;
964
972
  warnedOverlayBlocks = false;
965
973
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
@@ -2861,6 +2869,57 @@ export class WebGL2Renderer {
2861
2869
  * `uScale`/`vScale` are repeats across the mesh's own UV range: geometry authored in
2862
2870
  * metres is textured at whatever density the material wants without rebuilding it.
2863
2871
  */
2872
+ /**
2873
+ * Compile, off the frame, every program a draw of `mesh` in `material` takes that making the mesh
2874
+ * did not, so the first frame that draws it waits on none: the program of the material's shading
2875
+ * model for each vertex variant the mesh can be drawn with (its rig, with a palette; its targets,
2876
+ * with weights and without), a skin's halves under the screen-space blur, and the lit switches
2877
+ * the material turns on. `ready()` resolves once they have.
2878
+ *
2879
+ * **For `pipelineCompile: 'skip'` above all**, where a draw whose program is new is left out until
2880
+ * it lands: a figure brought on screen whole rather than without its face and hair for the frames
2881
+ * those take. Where draws wait, the programs compile here, at the call, rather than in a frame.
2882
+ * Two-sided and cut-out surfaces are draw state on this backend and need nothing; on WebGPU they
2883
+ * are pipelines of their own, prepared the same way.
2884
+ *
2885
+ * **Call it between frames**: it sets `material` to read it and leaves no material set after, as
2886
+ * `setMaterial(null)` does. `options` is the other backend's; blended draws share a program here.
2887
+ */
2888
+ prepareMesh(mesh, material, _options = {}) {
2889
+ if (this.contextLost)
2890
+ return;
2891
+ this.setMaterial(material);
2892
+ const kind = material?.model?.kind ?? null;
2893
+ if (kind !== null) {
2894
+ const eight = mesh.isSkinnedEight;
2895
+ const morphs = mesh.morph !== null;
2896
+ const plain = mesh.isSkinned ? (eight ? 'skinned8' : 'skinned') : 'plain';
2897
+ const morphed = mesh.isSkinned ? (eight ? 'both8' : 'both') : 'morphed';
2898
+ for (const variant of morphs ? [plain, morphed] : [plain]) {
2899
+ this.modelProgram(variant, kind);
2900
+ /* A skin under the screen-space blur draws three halves, each a program of its own. */
2901
+ if (kind === 'skin' && this.skinScatter !== null && this.sceneTarget !== null) {
2902
+ for (const half of SPLIT_SKIN_HALVES)
2903
+ this.modelProgram(variant, kind, half);
2904
+ }
2905
+ }
2906
+ }
2907
+ this.setMaterial(null);
2908
+ }
2909
+ /** `prepareMesh` for an instanced batch: its own programs, and its model's for its variant. */
2910
+ prepareInstanced(batch, material, _options = {}) {
2911
+ if (this.contextLost)
2912
+ return;
2913
+ this.setMaterial(material);
2914
+ const animated = batch.animation !== null;
2915
+ this.ensureInstancedProgram();
2916
+ if (animated)
2917
+ this.ensureAnimatedProgram();
2918
+ const kind = material?.model?.kind ?? null;
2919
+ if (kind !== null)
2920
+ this.modelProgram(animated ? 'animated' : 'instanced', kind);
2921
+ this.setMaterial(null);
2922
+ }
2864
2923
  /**
2865
2924
  * Choose the joint palette the following `drawMesh` calls skin by, or null for none.
2866
2925
  *
@@ -3197,9 +3256,9 @@ export class WebGL2Renderer {
3197
3256
  * and read neither — one decision for every program, as `setMaterial` says.
3198
3257
  */
3199
3258
  const modelMap = material?.modelMap ?? null;
3200
- /* A layered material's mask goes where a model's map would: see `SurfaceMaterial.layers`. */
3259
+ /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
3201
3260
  const layers = this.layersOf(material);
3202
- const boundMap = modelMap ?? layers?.mask ?? null;
3261
+ const boundMap = modelMap ?? layerMaskMap(layers);
3203
3262
  gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
3204
3263
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3205
3264
  if (boundMap !== null)
@@ -3217,11 +3276,13 @@ export class WebGL2Renderer {
3217
3276
  /* The draw's overlay, which a program born mid-pass must wear too: see `setSurfaceOverlay`. */
3218
3277
  gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
3219
3278
  gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
3220
- /* How much light from behind the surface lets through, in the glass tint's spare lane. */
3221
- this.diffuseTransmission = Math.min(Math.max(material?.diffuseTransmission ?? 0, 0), 1) || 0;
3222
- gl.uniform4f(u['uGlassTint'] ?? null, 1, 1, 1, this.diffuseTransmission);
3279
+ /* How much light from behind the surface lets through, and its colour: transmission.ts. */
3280
+ resolveTransmission(material, this.transmission);
3281
+ this.writeTransmission(u);
3223
3282
  /* Maps placed by the world, where the material asks and the lit stage has room: worldUv.ts. */
3224
- const projection = material?.projection ?? null;
3283
+ const projection = layeredProjection(material?.projection, layers);
3284
+ if (projection !== (material?.projection ?? null))
3285
+ this.warnLayersTriplanar();
3225
3286
  packSurfaceProjection(this.projectionFits(projection) ? projection : null, this.projection);
3226
3287
  if (this.projection[0] > 0)
3227
3288
  this.askLit('WORLD_UVS');
@@ -3455,14 +3516,15 @@ export class WebGL2Renderer {
3455
3516
  * no room for their two vectors each leave one layer, the last two said once each.
3456
3517
  */
3457
3518
  layersOf(material) {
3458
- const layers = material?.layers ?? null;
3519
+ const layers = (material?.layers ?? null);
3459
3520
  if (layers === null || layers.repeats.length === 0)
3460
3521
  return null;
3461
- if ((material?.modelMap ?? null) !== null) {
3522
+ if (!layersFitBeside(layers, material?.modelMap)) {
3462
3523
  if (!this.warnedLayersModelMap) {
3463
3524
  this.warnedLayersModelMap = true;
3464
- console.warn("Renderer: a material's layers are drawn as one: its mask is read where a model's map " +
3465
- 'goes, and this material carries a `modelMap` too.');
3525
+ console.warn("Renderer: a material's layers are drawn as one: its mask is a map, read where a " +
3526
+ "model's map goes, and this material carries a `modelMap` too. A mask in the ORM " +
3527
+ "array (`mask: 'orm'`) or the vertex colour (`mask: 'vertex'`) leaves the slot free.");
3466
3528
  }
3467
3529
  return null;
3468
3530
  }
@@ -3471,12 +3533,20 @@ export class WebGL2Renderer {
3471
3533
  return layers;
3472
3534
  if (!this.warnedLayersRoom) {
3473
3535
  this.warnedLayersRoom = true;
3474
- console.warn("Renderer: a material's layers are refused on this device. They take two fragment uniform " +
3536
+ console.warn("Renderer: a material's layers are refused on this device. They take four fragment uniform " +
3475
3537
  'vectors the lit shader does not have; the material is drawn as its base layer. A lower ' +
3476
3538
  '`maxLights` makes the room, or WebGPU has it.');
3477
3539
  }
3478
3540
  return null;
3479
3541
  }
3542
+ /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
3543
+ warnLayersTriplanar() {
3544
+ if (this.warnedLayersTriplanar)
3545
+ return;
3546
+ this.warnedLayersTriplanar = true;
3547
+ console.warn("Renderer: a layered material's triplanar projection is laid on the horizontal plane: every " +
3548
+ "layer's maps on three planes would be three times the reads. See `SurfaceMaterial.layers`.");
3549
+ }
3480
3550
  /** Whether the lit shader with `feature` compiled in fits this GPU's fragment uniform vectors. */
3481
3551
  switchFits(feature) {
3482
3552
  const asIf = { [feature]: true };
@@ -3635,7 +3705,8 @@ export class WebGL2Renderer {
3635
3705
  *
3636
3706
  * What it gives up: a fade reads as grain while it lasts, which a temporal resolve smooths and a
3637
3707
  * plain frame does not; a blended draw that asks spends its opacity here and blends none of it;
3638
- * and a lightmapped batch carries regions where the opacities would be, so it has none to fade.
3708
+ * and a lightmapped batch's opacities ride its page regions, so they fade in steps of a
3709
+ * sixty-fourth, which is the pattern's own resolution (`MeshInstances.lightmapRegions`).
3639
3710
  */
3640
3711
  setDitherOpacity(on) {
3641
3712
  if (this.contextLost)
@@ -4106,13 +4177,20 @@ export class WebGL2Renderer {
4106
4177
  const tint = options.refractTint ?? WHITE_TINT;
4107
4178
  gl.uniform4f(u['uRefractTint'] ?? null, tint[0] ?? 1, tint[1] ?? 1, tint[2] ?? 1, 1);
4108
4179
  /* The pane's colour; w is the material's light through from behind, which glass never reads. */
4109
- gl.uniform4f(u['uGlassTint'] ?? null, glass.tint[0] ?? 1, glass.tint[1] ?? 1, glass.tint[2] ?? 1, this.diffuseTransmission);
4180
+ gl.uniform4f(u['uGlassTint'] ?? null, glass.tint[0] ?? 1, glass.tint[1] ?? 1, glass.tint[2] ?? 1, this.transmission[3]);
4110
4181
  return true;
4111
4182
  }
4183
+ /** `uGlassTint` as the current material leaves it. See `transmission`. */
4184
+ writeTransmission(u) {
4185
+ const t = this.transmission;
4186
+ this.gl.uniform4f(u['uGlassTint'] ?? null, t[0] ?? -1, t[1] ?? -1, t[2] ?? -1, t[3] ?? 0);
4187
+ }
4112
4188
  /** Put back what `bindSeeThrough` bound: the binding outlives its frame (the 2026-08-27 rule). */
4113
4189
  unbindSeeThrough(u) {
4114
4190
  const { gl } = this;
4115
4191
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
4192
+ /* The material's light through, over the pane's tint: transmission.ts. */
4193
+ this.writeTransmission(u);
4116
4194
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
4117
4195
  /* The overlay's atlas, where one is held: it shares the unit. See `setSurfaceOverlay`. */
4118
4196
  gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture ?? this.emptyTexture2D);
@@ -6177,6 +6255,19 @@ export class WebGL2Renderer {
6177
6255
  }
6178
6256
  this.sceneTarget.setLocalExposure(strength);
6179
6257
  }
6258
+ /**
6259
+ * Where ambient occlusion fades out with distance: whole up to `distance` metres from the eye and
6260
+ * gone `radius` metres past it, so the far depth buffer's steps — a sky dome kilometres off, a
6261
+ * mountain range — are not shaded in rings, and those pixels skip the occlusion's sampling.
6262
+ *
6263
+ * A distance that is not a finite number at or above zero is no fade, which is the default, and
6264
+ * `Infinity` reads as "never fades"; a radius below zero is a cut at `distance`. Set before
6265
+ * `endFrame` and held until changed, like `setDepthOfField`. Nothing at all when
6266
+ * `ambientOcclusion` is 0. See `occlusionFade.ts` for what it gives up.
6267
+ */
6268
+ setAmbientOcclusionFade(distance, radius) {
6269
+ resolveOcclusionFade(distance, radius, this.aoFade);
6270
+ }
6180
6271
  /**
6181
6272
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
6182
6273
  * to take.
@@ -6804,6 +6895,7 @@ export class WebGL2Renderer {
6804
6895
  radius: this.quality.ambientOcclusionRadius,
6805
6896
  projScale: this.aoProjScale,
6806
6897
  invProjection: this.aoInvProjection,
6898
+ fade: this.aoFade,
6807
6899
  offset: this.aoOffset,
6808
6900
  };
6809
6901
  }
@@ -7256,7 +7348,7 @@ export class WebGL2Renderer {
7256
7348
  /* Nor a projection: maps follow the mesh's coordinates until a material asks otherwise. */
7257
7349
  this.projection.fill(0);
7258
7350
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
7259
- this.diffuseTransmission = 0;
7351
+ resolveTransmission(null, this.transmission);
7260
7352
  /* Nor layers: one layer until a material asks for more. */
7261
7353
  this.layerFloats.fill(0);
7262
7354
  this.overlayMaps = null;
@@ -7406,8 +7498,9 @@ export class WebGL2Renderer {
7406
7498
  `bindSeeThrough` for the four numbers in the one vector. */
7407
7499
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
7408
7500
  gl.uniform4f(u['uRefractTint'] ?? null, 1, 1, 1, 1);
7409
- /* White, and no light through from behind: no material at a pass's start. */
7410
- gl.uniform4f(u['uGlassTint'] ?? null, 1, 1, 1, this.diffuseTransmission);
7501
+ /* No light through from behind: no material at a pass's start. See `writeTransmission`. */
7502
+ const through = this.transmission;
7503
+ gl.uniform4f(u['uGlassTint'] ?? null, through[0] ?? -1, through[1] ?? -1, through[2] ?? -1, through[3] ?? 0);
7411
7504
  gl.uniform3fv(u['uDirectionalDir'] ?? null, env.directionalDir);
7412
7505
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
7413
7506
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
@@ -7,7 +7,7 @@ import type { PickHit, PickableSource } from '../../pickable.ts';
7
7
  import type { RendererApi } from '../api.ts';
8
8
  import { GpuTimestamps } from './gpuTimestamps.ts';
9
9
  import type { Environment, FilmOptions, InsetRect, LightVolumeDrawOptions, SkyColors, TranslucentMeshOptions } from '../webgl2/renderer.ts';
10
- import type { IncrementalMeshHandle, MeshOptions } from '../api.ts';
10
+ import type { IncrementalMeshHandle, MeshOptions, PrepareOptions } from '../api.ts';
11
11
  import type { LightmapPage } from '../../lightmap.ts';
12
12
  import { type BloomResponse } from '../../bloomChain.ts';
13
13
  import type { WaterBody } from '../../waterRenderer.ts';
@@ -851,6 +851,8 @@ export declare class WebGPURenderer implements RendererApi {
851
851
  private temporalHistoryUsable;
852
852
  private readonly aoStaging;
853
853
  private readonly aoFloats;
854
+ /** Where occlusion fades out with distance, `uFade`: none until a caller sets one. */
855
+ private readonly aoFade;
854
856
  private aoBindGroup;
855
857
  private readonly aoBlurLayout;
856
858
  /**
@@ -1187,7 +1189,13 @@ export declare class WebGPURenderer implements RendererApi {
1187
1189
  private readonly projection;
1188
1190
  /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
1189
1191
  private readonly layerFloats;
1192
+ /**
1193
+ * `uGlassTint` as the material leaves it, its light from behind and that light's colour, which a
1194
+ * pane overwrites for its own draw and `releaseSeeThrough` puts back. See `transmission.ts`.
1195
+ */
1196
+ private readonly transmission;
1190
1197
  private warnedLayersModelMap;
1198
+ private warnedLayersTriplanar;
1191
1199
  /** Its numbers, `uModelParams`, packed by `packModel`. */
1192
1200
  private readonly modelParams;
1193
1201
  /**
@@ -2093,6 +2101,8 @@ export declare class WebGPURenderer implements RendererApi {
2093
2101
  * said once, leave one layer.
2094
2102
  */
2095
2103
  private layersOf;
2104
+ /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
2105
+ private warnLayersTriplanar;
2096
2106
  /**
2097
2107
  * What the sun's moving layer is read through: the matrix its shadow pass was last drawn with,
2098
2108
  * where the lit pipelines carry one (`MOVING_SUN`); the environment's otherwise, which every caller
@@ -2478,6 +2488,35 @@ export declare class WebGPURenderer implements RendererApi {
2478
2488
  */
2479
2489
  private refuseOitMultisampled;
2480
2490
  drawTranslucentMesh(mesh: GpuMesh, model: ReadonlyMat4, opacity: number, options?: TranslucentMeshOptions): void;
2491
+ /**
2492
+ * Compile off the main thread every pipeline a draw of `mesh` in `material` takes. **The other
2493
+ * backend's `prepareMesh` carries the contract**; here a draw's key is `meshShape`'s, so a prepared
2494
+ * key is exactly the one the draw asks for, and the compiles are started as a skipping draw starts
2495
+ * them (`PipelineCache.lit`) whatever `pipelineCompile` says, into the frame's own caches.
2496
+ */
2497
+ prepareMesh(mesh: GpuMesh, material: SurfaceMaterial<GpuSurfaceTexture> | null, options?: PrepareOptions): void;
2498
+ /** `prepareMesh` for an instanced batch. See the other backend's `prepareInstanced`. */
2499
+ prepareInstanced(batch: GpuInstancedBatch, material: SurfaceMaterial<GpuSurfaceTexture> | null, options?: PrepareOptions): void;
2500
+ /**
2501
+ * `material` set, every cache a draw in it may build into passed to `start` with whether that draw
2502
+ * blends, compiles started rather than built (`PipelineCache.lit`), and then no material set and
2503
+ * each cache back to what `pipelineCompile` says.
2504
+ */
2505
+ private preparing;
2506
+ /** What `meshShape` worked out for the mesh draw at hand. Reused, so a draw allocates no record. */
2507
+ private readonly drawShape;
2508
+ /**
2509
+ * The key a mesh draw's pipeline is cached under, and the pipeline state that key encodes, under
2510
+ * the current material and these options, into `drawShape`. **The one statement of it**:
2511
+ * `submitMesh` draws by it and `prepareMesh` compiles by it, so the two cannot disagree about a key.
2512
+ */
2513
+ private meshShape;
2514
+ /**
2515
+ * The pipeline `drawShape` names: the one built, one built here, or none where draws skip a
2516
+ * compile and this one has not landed (`PipelineCache.lit`) — and none for a split skin until all
2517
+ * of its halves have.
2518
+ */
2519
+ private meshPipeline;
2481
2520
  /**
2482
2521
  * @param blend Which of the two entry points above this came through, and **not** something to
2483
2522
  * infer from `opacity`.
@@ -2568,6 +2607,13 @@ export declare class WebGPURenderer implements RendererApi {
2568
2607
  * **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
2569
2608
  * is built, and a per-instance opacity: `opacity` here dims the whole batch.
2570
2609
  */
2610
+ /**
2611
+ * An instanced draw's pipeline under the current material and these options: the one built, one
2612
+ * built here, or none while it compiles (`PipelineCache.lit`), its key, base and model left in
2613
+ * `drawShape`. **The one statement of an instanced key**: `submitInstanced` draws by it and
2614
+ * `prepareInstanced` compiles by it.
2615
+ */
2616
+ private instancedPipeline;
2571
2617
  /**
2572
2618
  * The next of a batch's cull slots this frame, with its cull queued against the current view; null
2573
2619
  * when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
@@ -3994,6 +4040,11 @@ export declare class WebGPURenderer implements RendererApi {
3994
4040
  setLocalExposure(strength: number): void;
3995
4041
  /** The exposure pass, built once; the composite's group holds placeholders until it exists. */
3996
4042
  private ensureExposurePass;
4043
+ /**
4044
+ * Where ambient occlusion fades out with distance. **The other backend's
4045
+ * `setAmbientOcclusionFade` carries the reasoning**; this is the same setter over the same state.
4046
+ */
4047
+ setAmbientOcclusionFade(distance: number, radius: number): void;
3997
4048
  /**
3998
4049
  * Where this frame's lens is focused, how deep the sharp zone is, and how much of the ceiling
3999
4050
  * to take. **The other backend's `setDepthOfField` carries the reasoning**, whole: the