@driftengine/core 4.6.1 → 4.7.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.
- package/CHANGELOG.json +58 -1
- package/README.md +166 -5
- package/dist/render/backend/budget.d.ts +6 -0
- package/dist/render/backend/budget.js +3 -0
- package/dist/render/backend/webgl2/renderer.d.ts +10 -13
- package/dist/render/backend/webgl2/renderer.js +21 -18
- package/dist/render/backend/webgpu/renderer.d.ts +30 -1
- package/dist/render/backend/webgpu/renderer.js +46 -42
- package/dist/render/pass.d.ts +4 -2
- package/dist/render/renderQuality.d.ts +39 -4
- package/dist/render/renderQuality.js +19 -0
- package/dist/render/rendererHarness.js +9 -1
- package/dist/world/freeze.d.ts +1 -1
- package/package.json +13 -9
- package/scripts/png.d.mts +1 -1
- package/scripts/png.mjs +99 -25
- package/src/render/backend/budget.ts +11 -1
- package/src/render/backend/webgl2/renderer.ts +21 -18
- package/src/render/backend/webgpu/renderer.ts +46 -43
- package/src/render/pass.ts +4 -2
- package/src/render/renderQuality.ts +59 -4
- package/src/render/rendererHarness.ts +8 -1
- package/src/world/freeze.ts +1 -1
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,61 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "4.7.1",
|
|
4
|
+
"date": "2026-10-02",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "added",
|
|
8
|
+
"short": "The manual: installation to a complete game, then every system in turn, sixty-nine chapters whose code is compiled from forty-six examples that switch live and run their behaviour in DriftScript.",
|
|
9
|
+
"text": "**The manual lives in `docs/manual/`**, one Markdown file a chapter and `manual.json` for their order: Getting started, from installation to a complete 3D game and a 2D one; Concepts; Rendering; Worlds; Simulation; Content, for models, the `.drft` container, sound and the command-line tools; and Interface, for input, cameras, sprites and menus. **Every code block is a region of a file under `examples/`**, which the typecheck compiles and `npm run manual:sync` copies in, so a sample cannot drift from code that runs. Forty-six examples run on WebGPU and WebGL2, their switches change the running scene, and where a scene has behaviour it lives in a `.drs` file that reloads in place. `docs/manual/uncovered.json` lists what has no chapter yet and can only shrink, and `undocumented.json` the exports with no comment, which can only shrink too. **`@driftengine/core`'s README is a package page**: what the engine is, a quickstart a test keeps equal to `examples/starter`, its systems linked to their chapters, and the other packages."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"kind": "added",
|
|
13
|
+
"short": "Scripts reach further: drift/audio ducks, fades and recalls the mix and reads the kick, drift/input reads a touch screen, and drift/chemistry asks whether a thing can be wet.",
|
|
14
|
+
"text": "**`drift/audio` turns the mix by name.** `duck`, `fade` and `recall` move a bus or a snapshot the host made, and answer `false` for a name nothing has; `pulse` reads the kick detector the host runs, `0` without one. `MixConsole.recall` returns whether the snapshot existed. **`drift/input` reads a touch screen** through a `Touch` handle beside `Actions`: `touchX`, `touchY`, `touchHeld`, `touchTap` and `touchSlide`, so a game whose controls are a script can be played on a phone. **`drift/chemistry` gains `wettable`**, so rain poured over every parcel passes over a nail instead of stopping at it."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"kind": "fixed",
|
|
18
|
+
"short": "A navigation mesh goes round a wall standing on the floor, and the regions a room is split into meet edge to edge, so a route crosses rooms it could not; the climb is a setting, maxStep.",
|
|
19
|
+
"text": "**`voxeliseWalkable` sees walls.** It sampled surfaces at cell centres, which never meet a vertical face, so a building on the ground read as a roof over open floor and a route went through it; a face too steep to stand on is now a solid in every cell it crosses, and erosion judges a neighbour at the span's own height. **Regions meet edge to edge**: each outline was simplified on its own, so a shared border came out as two lines that overlap nowhere and a portal vanished; a room with a pillar at the deviation the README recommends had no route between opposite corners. Border points are held across regions and each stretch is simplified once. `VoxeliseSettings.maxStep` takes the climb `buildRegions` takes."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"kind": "fixed",
|
|
23
|
+
"short": "Sprites and tiles drawn in a 2D world are the right way up, and a clipping node in an interface clips what its children draw and where the pointer reaches them.",
|
|
24
|
+
"text": "**A picture keeps its top up in a 2D world.** Every sprite and tile drawn through `worldToNdc` came out upside down, because the frame's top row was read at the sprite's corner, which in a world is its bottom; the vertex stage reads the affine's orientation now, and `scripts/sprite-check.mjs` draws the same rectangles in both spaces and asserts the frames match byte for byte. **`clip` is drawn and hit tested**: `drawUiTree` cuts every quad under a clipping node to what it leaves, an image's frame in proportion, and gives the content sink the visible rectangle; `uiHitTest` finds nothing under a clipping node outside its box. A scrolled list's rows drew wherever they had scrolled to, and took clicks there."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"kind": "fixed",
|
|
28
|
+
"short": "A script makes an entity from a prefab with ecs.instantiate and counts entities inside a system; splats hide behind what stands in front of them; a ragdoll whose root has no bone holds together.",
|
|
29
|
+
"text": "**`drift/prefab` could not be called**, since its one function is named `spawn`, a keyword; a script makes an entity from a prefab with `ecs.instantiate` now, and the module is refused by name. **`ecs.count`, `ecs.at` and `ecs.alive` work inside a system**, where they threw and the schedule skipped the system every tick. **Splats are occluded by the geometry in front of them on both backends**: WebGPU compared depth one way under reversed depth, and a contributed pass on WebGL2 was handed the identity in place of the depth remap. **A ragdoll built from a rig whose root has no bone holds together**: bones hanging from that root were jointed to nothing, so a humanoid fell as a torso and two loose legs. **Pale smoke is drawn**, because droplets now count toward how thick smoke is."
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "4.7.0",
|
|
35
|
+
"date": "2026-10-01",
|
|
36
|
+
"entries": [
|
|
37
|
+
{
|
|
38
|
+
"kind": "added",
|
|
39
|
+
"short": "Blender files bake directly: .blend from 2.79 to 5.x, compressed or not, with meshes, normals, materials, lights, cameras, shape keys and animation, and through your Blender where needed.",
|
|
40
|
+
"text": "**A `.blend` file is read directly, with nothing installed.** `readModel` and `npm run bake` take Blender's own format from 2.79 to 5.x, plain, gzip or zstd, because the reader learns every struct from the file's `DNA1` catalogue rather than from a list of Blender releases. It reads meshes in all three layouts Blender has stored them in (the legacy structs, 3.5's named attributes and 5.0's attribute storage), their normals by Blender's own rule — sharp edges and faces, auto smooth, custom split normals — with UVs, colour attributes and shape keys; Principled materials wired the way the glTF importer wires them, with their packed pictures; point, spot and sun lights, blackbody colour included; cameras; parents, collection instances and render visibility; and keyed object animation, layered actions and NLA strips included. It builds the same glTF the rest of the pipeline reads, so a `.blend` and a `.glb` reach the baker as the same thing, and the fixtures in `scripts/fixtures/blend/` hold it to what Blender's own exporter writes for the same file. **What only Blender can evaluate is refused by name** — a rig, a constraint, a modifier past the smoothing ones — as `BlendNeedsBlender`, never as half a scene; the baker then drives the user's own Blender through its bundled glTF exporter, `--via-blender` asks for that outright and `--direct` refuses it, and every import says which route it took."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"kind": "added",
|
|
44
|
+
"short": "materialChangesPerFrame: a scene that changes material more than 1,024 times a frame on WebGPU can now ask for a larger ring instead of losing the draws past it.",
|
|
45
|
+
"text": "**How many material changes a WebGPU frame holds is a quality option, `materialChangesPerFrame`.** A frame spends a slot of a uniform ring each time it changes material and skips every draw past the last slot, which is geometry missing rather than geometry drawn wrong — and 1,024 was a constant. A bought city of eleven hundred materials spends about nine hundred on a street, and a planar mirror of that street spends as many again, so its reflection came and went as the camera turned. The default stays 1,024; a scene that measures past it asks for more at construction and pays seven to thirteen kilobytes a slot, and `frameBudget`'s materials line reports the ceiling it got. WebGL2 keeps no ring and reads the option without effect."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"kind": "fixed",
|
|
49
|
+
"short": "The PNG reader in core's scripts reads palettes, one- to four-bit greys, sixteen-bit images and tRNS transparency, so a bake no longer drops those pictures and draws their surfaces white.",
|
|
50
|
+
"text": "**`@driftengine/core/scripts/png.mjs` reads every non-interlaced PNG.** It refused palettes (colour type 3), greys below eight bits and sixteen-bit images, and the baker and the native host read model pictures through it: measured on one bought city, 219 of its 2,447 pictures, every material wearing one baked with no colour map and drew white. Rows are now un-filtered as packed bytes and then widened to eight-bit samples — a palette to RGB, or RGBA where it carries `tRNS`; a grey or RGB `tRNS` key to an alpha of nought at that value; sixteen bits to the high byte; a grey below eight bits scaled to the full byte. An eight-bit image with no key still takes the path that copies nothing, so a screenshot decodes as it did. Interlacing is still refused by name."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"kind": "fixed",
|
|
54
|
+
"short": "A glTF material with no metallic texture and no metallicFactor is a full metal, as the specification says, and now reflects like one instead of reading as matte.",
|
|
55
|
+
"text": "**An untextured glTF metal that states no `metallicFactor` mirrors.** The specification's default for the factor is 1, and the reader took the material's reflectivity from the factor alone with a default of 0, so every such material — an exporter writes none when the value is the default — drew as a matte surface. The reflectivity of a material with no base colour texture now follows its metallic value, defaulted as the specification defaults it; a textured or vertex-coloured one is unchanged."
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
2
59
|
{
|
|
3
60
|
"version": "4.6.1",
|
|
4
61
|
"date": "2026-10-01",
|
|
@@ -2881,7 +2938,7 @@
|
|
|
2881
2938
|
},
|
|
2882
2939
|
{
|
|
2883
2940
|
"kind": "added",
|
|
2884
|
-
"text": "`KickDetector.level` is a low-end level a visual can actually follow. `energy`, the only one there was, is the raw sum of the two lowest bands with a gain on it: honest about what it says and useless on anything mastered loud, because it reaches its ceiling and stays there, so a consumer driving a light or a particle field from it draws a constant. Measured on a
|
|
2941
|
+
"text": "`KickDetector.level` is a low-end level a visual can actually follow. `energy`, the only one there was, is the raw sum of the two lowest bands with a gain on it: honest about what it says and useless on anything mastered loud, because it reaches its ceiling and stays there, so a consumer driving a light or a particle field from it draws a constant. Measured on a bass-heavy track through this detector, it read 1.000 in every frame sampled. The detector already computed the quantity that does move with the kick and threw it away after gating on it, which is the whitened signal: what is left once the bassline, the mud and the low mids are masked out. That is now readable, unscaled, because how far a consumer opens it up is a decision about its own picture rather than about the music. `energy` keeps its meaning and its arithmetic, and a production consumer reads only `pulse`, so nothing existing moves",
|
|
2885
2942
|
"short": "`KickDetector.level` exposes the whitened low end, which keeps its dynamics where `energy` saturates on a loud master."
|
|
2886
2943
|
},
|
|
2887
2944
|
{
|
package/README.md
CHANGED
|
@@ -1,9 +1,170 @@
|
|
|
1
1
|
# @driftengine/core
|
|
2
2
|
|
|
3
|
-
The runtime
|
|
3
|
+
The runtime of DriftEngine, a WebGPU game engine for the web and for native apps. It draws through
|
|
4
|
+
WebGPU where the browser has a device and through WebGL2 everywhere else, behind one API, and it
|
|
5
|
+
carries the loop, the renderer, cameras, input, the scene graph, geometry, text
|
|
6
|
+
and the whole of [`@driftengine/physics`](https://driftengine.dev/docs/simulation/rigid-bodies),
|
|
7
|
+
which it re-exports. The rest of the engine is separate packages you add when you need them.
|
|
4
8
|
|
|
5
|
-
**Cost: 699.0 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
|
|
6
|
-
|
|
9
|
+
**Cost: 699.0 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
|
|
10
|
+
which fails the build if the figure drifts by more than 3%.
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
The manual is at **[driftengine.dev/docs](https://driftengine.dev/docs)**, with an example running
|
|
13
|
+
beside every chapter, and the reference for every export at
|
|
14
|
+
[driftengine.dev/docs/api](https://driftengine.dev/docs/api).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install @driftengine/core
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
It needs a browser with WebGL2, which is every current one, and an ES module bundler such as Vite.
|
|
23
|
+
[Installation](https://driftengine.dev/docs/start/installation) covers the page, TypeScript and the
|
|
24
|
+
backends.
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
A lit cube turning on a fixed step, on a page with `<canvas id="stage">`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
Camera,
|
|
33
|
+
MeshBuilder,
|
|
34
|
+
SceneNode,
|
|
35
|
+
createEnvironment,
|
|
36
|
+
createRenderer,
|
|
37
|
+
startLoop,
|
|
38
|
+
} from '@driftengine/core';
|
|
39
|
+
|
|
40
|
+
const canvas = document.querySelector<HTMLCanvasElement>('#stage');
|
|
41
|
+
if (canvas === null) throw new Error('the page must carry <canvas id="stage">');
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* Asynchronous because it has to be: asking for a WebGPU adapter returns a promise, and there
|
|
45
|
+
* is no synchronous way to learn whether a usable device exists. The fall back to WebGL2 is
|
|
46
|
+
* automatic and silent, so `reason` is the only thing that says why. Print it: the first
|
|
47
|
+
* question asked about any picture is what drew it.
|
|
48
|
+
*/
|
|
49
|
+
const { renderer, backend, reason } = await createRenderer(canvas, {
|
|
50
|
+
maxDevicePixelRatio: 1.75,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const readout = document.querySelector('#backend');
|
|
54
|
+
if (readout !== null) readout.textContent = `${backend}: ${reason}`;
|
|
55
|
+
|
|
56
|
+
renderer.resize();
|
|
57
|
+
addEventListener('resize', () => renderer.resize());
|
|
58
|
+
|
|
59
|
+
/** Light, air and ground bounce. Chosen once, because these are allocation-sized decisions. */
|
|
60
|
+
const ENV = createEnvironment({
|
|
61
|
+
directionalDir: [0.4, 0.7, 0.35],
|
|
62
|
+
directionalColor: [1, 0.96, 0.88],
|
|
63
|
+
ambient: [0.2, 0.22, 0.28],
|
|
64
|
+
ambientGround: [0.08, 0.08, 0.1],
|
|
65
|
+
emissiveGain: 0,
|
|
66
|
+
nightFactor: 0,
|
|
67
|
+
fogColor: [0.16, 0.18, 0.24],
|
|
68
|
+
fogDensity: 0.004,
|
|
69
|
+
fogHeightFalloff: 0.03,
|
|
70
|
+
fogBaseY: 0,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* Colour is vertex data here, which is what lets a whole world be flat-shaded draw calls.
|
|
75
|
+
* `addBox` takes a centre and *half* extents, so this ground is 24 units across and the cube
|
|
76
|
+
* is two units on a side.
|
|
77
|
+
*/
|
|
78
|
+
const shapes = new MeshBuilder();
|
|
79
|
+
shapes.addBox([0, 1, 0], [1, 1, 1], [0.85, 0.45, 0.25]);
|
|
80
|
+
const cube = renderer.createMesh(shapes.build());
|
|
81
|
+
|
|
82
|
+
const slab = new MeshBuilder();
|
|
83
|
+
slab.addBox([0, -0.25, 0], [12, 0.25, 12], [0.3, 0.32, 0.36]);
|
|
84
|
+
const ground = renderer.createMesh(slab.build());
|
|
85
|
+
|
|
86
|
+
const spinner = new SceneNode();
|
|
87
|
+
spinner.setPosition(0, 1, 0);
|
|
88
|
+
const stillness = new SceneNode();
|
|
89
|
+
stillness.updateWorld();
|
|
90
|
+
|
|
91
|
+
const camera = new Camera();
|
|
92
|
+
camera.position[0] = 6;
|
|
93
|
+
camera.position[1] = 4.5;
|
|
94
|
+
camera.position[2] = 8;
|
|
95
|
+
camera.lookAt(0, 1, 0);
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* Two angles, because that is what `alpha` is for. The simulation advances in fixed steps and
|
|
99
|
+
* the display does not, so a frame almost never lands on a step boundary: drawing `spin`
|
|
100
|
+
* directly judders at any refresh rate that is not a multiple of the step. Interpolating
|
|
101
|
+
* between the last two states is the whole reason the loop hands `alpha` over.
|
|
102
|
+
*/
|
|
103
|
+
let spin = 0;
|
|
104
|
+
let previousSpin = 0;
|
|
105
|
+
|
|
106
|
+
startLoop({
|
|
107
|
+
simulate(dt) {
|
|
108
|
+
previousSpin = spin;
|
|
109
|
+
spin += dt * 0.8;
|
|
110
|
+
},
|
|
111
|
+
render(alpha) {
|
|
112
|
+
spinner.setRotationAxisAngle(0, 1, 0, previousSpin + (spin - previousSpin) * alpha);
|
|
113
|
+
spinner.updateWorld();
|
|
114
|
+
|
|
115
|
+
camera.updateMatrices(canvas.height > 0 ? canvas.width / canvas.height : 1);
|
|
116
|
+
|
|
117
|
+
renderer.beginFrame([0.05, 0.06, 0.09]);
|
|
118
|
+
renderer.bindMeshPass(camera, ENV);
|
|
119
|
+
renderer.drawMesh(ground, stillness.worldMatrix);
|
|
120
|
+
renderer.drawMesh(cube, spinner.worldMatrix);
|
|
121
|
+
renderer.endFrame();
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This is the engine's `examples/starter`, and a test keeps the two the same.
|
|
127
|
+
[Hello world](https://driftengine.dev/docs/start/hello-world) walks through it, and
|
|
128
|
+
[Your first game](https://driftengine.dev/docs/start/first-game) builds a complete one.
|
|
129
|
+
|
|
130
|
+
## What is in it
|
|
131
|
+
|
|
132
|
+
| | |
|
|
133
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| The frame | A fixed-step [loop](https://driftengine.dev/docs/start/the-loop) with interpolation, [two backends](https://driftengine.dev/docs/concepts/backends) behind one API, [quality profiles](https://driftengine.dev/docs/concepts/quality), and a [frame](https://driftengine.dev/docs/concepts/the-frame) whose order you can read. |
|
|
135
|
+
| Drawing | [Meshes](https://driftengine.dev/docs/rendering/meshes), [materials](https://driftengine.dev/docs/rendering/materials), [instancing](https://driftengine.dev/docs/rendering/instancing), [lights](https://driftengine.dev/docs/rendering/lights) and [shadows](https://driftengine.dev/docs/rendering/shadows), [the sky](https://driftengine.dev/docs/rendering/sky-and-atmosphere), [fog and weather](https://driftengine.dev/docs/rendering/fog-and-weather), [water](https://driftengine.dev/docs/rendering/water), [reflections](https://driftengine.dev/docs/rendering/reflections), [post-processing](https://driftengine.dev/docs/rendering/post-processing), [particles](https://driftengine.dev/docs/rendering/particles), [text](https://driftengine.dev/docs/rendering/text-and-overlays) and [passes of your own](https://driftengine.dev/docs/rendering/custom-passes). |
|
|
136
|
+
| Light at scale | [DriftLight](https://driftengine.dev/docs/rendering/driftlight) for hundreds of fixed lights, [DriftRay](https://driftengine.dev/docs/rendering/driftray) for bounced light traced every frame, [DriftTR](https://driftengine.dev/docs/rendering/drifttr) for temporal reconstruction, and the [GPU-driven pipeline](https://driftengine.dev/docs/rendering/gpu-driven). |
|
|
137
|
+
| Worlds | [Worlds bigger than a float](https://driftengine.dev/docs/worlds/large-worlds), streamed in cells, with [hierarchical detail](https://driftengine.dev/docs/worlds/hierarchical-detail) and [worlds built from a kit](https://driftengine.dev/docs/worlds/worlds-from-a-kit). |
|
|
138
|
+
| Physics | Deterministic [rigid bodies](https://driftengine.dev/docs/simulation/rigid-bodies), [joints](https://driftengine.dev/docs/simulation/joints), [queries](https://driftengine.dev/docs/simulation/queries), [characters](https://driftengine.dev/docs/simulation/characters), [vehicles](https://driftengine.dev/docs/simulation/vehicles), and [ragdolls and cloth](https://driftengine.dev/docs/simulation/ragdolls-and-cloth). |
|
|
139
|
+
| Play | [Input](https://driftengine.dev/docs/interface/input) from every device through one action map, [cameras that cut](https://driftengine.dev/docs/interface/cameras), [picking](https://driftengine.dev/docs/rendering/picking) and the [scene graph](https://driftengine.dev/docs/concepts/scene-graph). |
|
|
140
|
+
|
|
141
|
+
## The other packages
|
|
142
|
+
|
|
143
|
+
Each is added when a game reaches for it, and a game that never imports one carries none of it.
|
|
144
|
+
|
|
145
|
+
| Package | What it adds |
|
|
146
|
+
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
147
|
+
| `@driftengine/animation` | Skeletons, clips, blend trees and IK. [Guide](https://driftengine.dev/docs/simulation/animation) |
|
|
148
|
+
| `@driftengine/nav` | Navigation meshes and routes. [Guide](https://driftengine.dev/docs/simulation/navigation) |
|
|
149
|
+
| `@driftengine/entities` | An entity component system with prefabs and rewind. [Guide](https://driftengine.dev/docs/simulation/entities) |
|
|
150
|
+
| `@driftengine/chemistry` | Heat, burning, phase changes and smoke. [Guide](https://driftengine.dev/docs/simulation/chemistry) |
|
|
151
|
+
| `@driftengine/ai` | Behaviour trees and agents. [Guide](https://driftengine.dev/docs/simulation/agents) |
|
|
152
|
+
| `@driftengine/audio` | A mix of buses, placed sounds, rooms and rhythm. [Guide](https://driftengine.dev/docs/content/audio) |
|
|
153
|
+
| `@driftengine/assets` and `@driftengine/drft` | Model importers and the streaming `.drft` container. [Guide](https://driftengine.dev/docs/content/importing-models) |
|
|
154
|
+
| `@driftengine/terrain` | Terrain drawn as a clipmap. [Guide](https://driftengine.dev/docs/worlds/terrain) |
|
|
155
|
+
| `@driftengine/splats` | Gaussian splat captures among meshes. [Guide](https://driftengine.dev/docs/worlds/splats) |
|
|
156
|
+
| `@driftengine/texture` | Materials as programs decoded on the device. [Guide](https://driftengine.dev/docs/worlds/drifttexture) |
|
|
157
|
+
| `@driftengine/capture` | A video clip turned into a surface, a collider and entities. [Guide](https://driftengine.dev/docs/worlds/driftcapture) |
|
|
158
|
+
| `@driftengine/ui2d` | Sprites, tilemaps and interfaces. [Guide](https://driftengine.dev/docs/interface/sprites) |
|
|
159
|
+
| `@driftengine/script` | DriftScript, the engine's scripting language, with hot reload. [Reference](https://driftengine.dev/docs/api/script) |
|
|
160
|
+
| `@driftengine/network` | Rollback netcode. [Reference](https://driftengine.dev/docs/api/network) |
|
|
161
|
+
| `@driftengine/xr` | WebXR sessions. [Reference](https://driftengine.dev/docs/api/xr) |
|
|
162
|
+
| `@driftengine/media` | Recording and exporting clips. [Reference](https://driftengine.dev/docs/api/media) |
|
|
163
|
+
| `@driftengine/editor` and `@driftengine/tools` | The editor and in-game tools. [Reference](https://driftengine.dev/docs/api/editor) |
|
|
164
|
+
| `@driftengine/package` and `@driftengine/native-host` | Installable apps built from the same game. [Reference](https://driftengine.dev/docs/api/package) |
|
|
165
|
+
|
|
166
|
+
## Licence
|
|
167
|
+
|
|
168
|
+
Apache-2.0. Part of [DriftEngine](https://github.com/drftrun/driftengine); see
|
|
169
|
+
[`docs/ARCHITECTURE.md`](https://github.com/drftrun/driftengine/blob/main/docs/ARCHITECTURE.md) for
|
|
170
|
+
how the packages divide.
|
|
@@ -47,6 +47,12 @@ export interface BudgetCounter extends BudgetLine {
|
|
|
47
47
|
ask(): void;
|
|
48
48
|
/** One of them was refused, and the frame is marked. */
|
|
49
49
|
drop(): void;
|
|
50
|
+
/**
|
|
51
|
+
* The ceiling, where it is a construction-time option and not known when the line is declared.
|
|
52
|
+
* Called at construction, once; a line keeps the place it was declared in, which is the order a
|
|
53
|
+
* report reads in.
|
|
54
|
+
*/
|
|
55
|
+
limit(ceiling: number): void;
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
52
58
|
* Every ceiling a backend imposes, counted for the frame just drawn.
|
|
@@ -2316,17 +2316,14 @@ export declare class WebGL2Renderer implements RendererApi {
|
|
|
2316
2316
|
* `endPlanarReflection`; the next water draw samples the completed target.
|
|
2317
2317
|
*/
|
|
2318
2318
|
/**
|
|
2319
|
-
* **
|
|
2319
|
+
* **Two things sample the target: water, and a wet film.** It is allocated when `water` and
|
|
2320
|
+
* `waterReflections` are both on, or when `RenderQuality.planarReflections` asks for it in a
|
|
2321
|
+
* scene with no water, and `drawFilm` reads it through `FilmOptions.reflectionStrength` for a
|
|
2322
|
+
* film on the plane it was rendered for. This said the pass was water-only after both of those
|
|
2323
|
+
* had landed, which is how a stale limitation outlives its cause: it read as a decision.
|
|
2320
2324
|
*
|
|
2321
|
-
*
|
|
2322
|
-
*
|
|
2323
|
-
* with `drawFilm` patches over it, which is what a wet street is, cannot mirror the scene
|
|
2324
|
-
* into it however it calls this. Reported from outside after being attempted, which is the
|
|
2325
|
-
* cost of a name that promises more than the wiring does.
|
|
2326
|
-
*
|
|
2327
|
-
* What is available to a surface that is not water: `bakeReflectionProbe`, which is a room
|
|
2328
|
-
* rather than a mirror, and `drawFilm`'s sheen, which is iridescence rather than reflection.
|
|
2329
|
-
* Neither is a substitute for a mirrored pass and this comment is not pretending otherwise.
|
|
2325
|
+
* A surface on any other plane, or that is not a film, reflects a probe
|
|
2326
|
+
* (`bakeReflectionProbe`), which is a room rather than a mirror.
|
|
2330
2327
|
*/
|
|
2331
2328
|
beginPlanarReflection(source: Camera, planeY: number, clearColor: Vec3): Camera | null;
|
|
2332
2329
|
endPlanarReflection(): void;
|
|
@@ -3431,9 +3428,9 @@ export declare class WebGL2Renderer implements RendererApi {
|
|
|
3431
3428
|
* tarmac at night by bisection twice, at 0.7 and again at 0.24, and landed on **0.15 to
|
|
3432
3429
|
* 0.18**. Start there rather than repeating the search.
|
|
3433
3430
|
*
|
|
3434
|
-
*
|
|
3435
|
-
*
|
|
3436
|
-
*
|
|
3431
|
+
* The mirror is a separate dial: `FilmOptions.reflectionStrength` shows the planar reflection
|
|
3432
|
+
* a frame rendered for the film's plane, weighted by the viewing angle. See
|
|
3433
|
+
* `beginPlanarReflection` for when that target exists.
|
|
3437
3434
|
*/
|
|
3438
3435
|
drawFilm(mesh: Mesh, camera: Camera, timeSeconds: number, env: Environment, sheen: number, options?: FilmOptions): void;
|
|
3439
3436
|
/**
|
|
@@ -1348,14 +1348,20 @@ export class WebGL2Renderer {
|
|
|
1348
1348
|
*/
|
|
1349
1349
|
registerPass(definition) {
|
|
1350
1350
|
const handle = registerIn(this.passes, definition);
|
|
1351
|
-
/*
|
|
1352
|
-
|
|
1353
|
-
|
|
1351
|
+
/*
|
|
1352
|
+
* Both the same here: this backend runs the GLSL natively, so no generator has flipped
|
|
1353
|
+
* anything. **What they carry is the depth remap whenever this context draws reversed**, and
|
|
1354
|
+
* they were the identity until a splat capture was drawn among meshes on this backend: the
|
|
1355
|
+
* renderer's own draws go through `GL_DEPTH_REMAP` (`sceneMatrix`), a pass's projection did
|
|
1356
|
+
* not, so a pass wrote conventional depth into a reversed buffer and a splat behind a post was
|
|
1357
|
+
* drawn over it. Identity where clip control is missing and depth stays conventional.
|
|
1358
|
+
*/
|
|
1359
|
+
const correction = this.reversedDepth ? GL_DEPTH_REMAP : IDENTITY_CLIP;
|
|
1354
1360
|
this.passDevice ??= {
|
|
1355
1361
|
backend: 'webgl2',
|
|
1356
1362
|
gl: this.gl,
|
|
1357
|
-
clipCorrection:
|
|
1358
|
-
depthCorrection:
|
|
1363
|
+
clipCorrection: correction,
|
|
1364
|
+
depthCorrection: correction,
|
|
1359
1365
|
};
|
|
1360
1366
|
if (definition.prepare !== undefined)
|
|
1361
1367
|
this.preparingPasses++;
|
|
@@ -3588,17 +3594,14 @@ export class WebGL2Renderer {
|
|
|
3588
3594
|
* `endPlanarReflection`; the next water draw samples the completed target.
|
|
3589
3595
|
*/
|
|
3590
3596
|
/**
|
|
3591
|
-
* **
|
|
3592
|
-
*
|
|
3593
|
-
*
|
|
3594
|
-
*
|
|
3595
|
-
*
|
|
3596
|
-
* into it however it calls this. Reported from outside after being attempted, which is the
|
|
3597
|
-
* cost of a name that promises more than the wiring does.
|
|
3597
|
+
* **Two things sample the target: water, and a wet film.** It is allocated when `water` and
|
|
3598
|
+
* `waterReflections` are both on, or when `RenderQuality.planarReflections` asks for it in a
|
|
3599
|
+
* scene with no water, and `drawFilm` reads it through `FilmOptions.reflectionStrength` for a
|
|
3600
|
+
* film on the plane it was rendered for. This said the pass was water-only after both of those
|
|
3601
|
+
* had landed, which is how a stale limitation outlives its cause: it read as a decision.
|
|
3598
3602
|
*
|
|
3599
|
-
*
|
|
3600
|
-
*
|
|
3601
|
-
* Neither is a substitute for a mirrored pass and this comment is not pretending otherwise.
|
|
3603
|
+
* A surface on any other plane, or that is not a film, reflects a probe
|
|
3604
|
+
* (`bakeReflectionProbe`), which is a room rather than a mirror.
|
|
3602
3605
|
*/
|
|
3603
3606
|
/* Like `bakeReflectionProbe`, this re-enters the **mesh pass only**: anything a scene draws
|
|
3604
3607
|
after the world, particles, light volumes, the sky, is missing from the mirror unless it is
|
|
@@ -6922,9 +6925,9 @@ export class WebGL2Renderer {
|
|
|
6922
6925
|
* tarmac at night by bisection twice, at 0.7 and again at 0.24, and landed on **0.15 to
|
|
6923
6926
|
* 0.18**. Start there rather than repeating the search.
|
|
6924
6927
|
*
|
|
6925
|
-
*
|
|
6926
|
-
*
|
|
6927
|
-
*
|
|
6928
|
+
* The mirror is a separate dial: `FilmOptions.reflectionStrength` shows the planar reflection
|
|
6929
|
+
* a frame rendered for the film's plane, weighted by the viewing angle. See
|
|
6930
|
+
* `beginPlanarReflection` for when that target exists.
|
|
6928
6931
|
*/
|
|
6929
6932
|
drawFilm(mesh, camera, timeSeconds, env, sheen, options = {}) {
|
|
6930
6933
|
const { gl } = this;
|
|
@@ -242,6 +242,35 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
242
242
|
*/
|
|
243
243
|
private readonly budget;
|
|
244
244
|
private readonly drawBudget;
|
|
245
|
+
/**
|
|
246
|
+
* Distinct materials a frame may set: `materialChangesPerFrame`, 1,024 unless a consumer asks.
|
|
247
|
+
*
|
|
248
|
+
* A slot is spent per *change*, not per draw, so this counts how many times a scene switches
|
|
249
|
+
* material rather than how much it draws.
|
|
250
|
+
*
|
|
251
|
+
* **A thousand and twenty-four by default, and it is the expensive one of the two.** A slot is the fragment
|
|
252
|
+
* block — 3,376 to 6,656 bytes depending on which of the sixteen permutations is built — paid
|
|
253
|
+
* twice for staging and buffer, so **7,168 to 13,312 bytes a slot** against 1,540 for a draw.
|
|
254
|
+
* Raising it from 256 costs 5.25 to 9.75 MB.
|
|
255
|
+
*
|
|
256
|
+
* It is worth paying for how it *fails* rather than how it performs. Past the ring
|
|
257
|
+
* `materialSlotForDraw` returns null and **every draw that needed a new material is skipped** —
|
|
258
|
+
* a stretch of the world simply not drawn, which has been reported from a consumer as
|
|
259
|
+
* ground that appears and disappears as the camera moves. A budget that hopes to stay under a
|
|
260
|
+
* ceiling is a budget that ships this the first time somebody adds a model.
|
|
261
|
+
*
|
|
262
|
+
* **This comment said "silently reuses the last material" until 2026-09-02, and so did the
|
|
263
|
+
* warning, and neither was ever true.** The skip has been there since `9d90bf7` wrote both
|
|
264
|
+
* halves in one diff. It is recorded because the wrong failure was the stated argument for
|
|
265
|
+
* raising this number from 256, and because "wrong texture" and "no geometry" send a consumer
|
|
266
|
+
* looking in completely different places — one at materials, the other at culling, which is
|
|
267
|
+
* where the reporting game spent its weeks. `frameBudget` reports the count now, so the next
|
|
268
|
+
* consumer reads a number instead of a sentence.
|
|
269
|
+
*
|
|
270
|
+
* **What would make it wrong** is a scene switching material every draw: a thousand material
|
|
271
|
+
* changes is a thousand uniform copies, and a consumer reaching this has a batching problem no
|
|
272
|
+
* ring size answers — many copies of one mesh want one material and one instanced draw.
|
|
273
|
+
*/
|
|
245
274
|
private readonly materialBudget;
|
|
246
275
|
/**
|
|
247
276
|
* Native bind groups built this frame, which is the one cost a consumer could not see.
|
|
@@ -2277,7 +2306,7 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
2277
2306
|
* *unchanged*, so a run of entries sharing one material has no shorter spelling and the sink
|
|
2278
2307
|
* bound once per entry. On this backend that is a cliff rather than a slope: `setMaterial`
|
|
2279
2308
|
* dirties `materialSlot`, so a bind per entry is a **slot per draw** out of
|
|
2280
|
-
* `
|
|
2309
|
+
* `materialChangesPerFrame` — and past the ring `materialSlotForDraw` returns null and the draw
|
|
2281
2310
|
* is skipped, which is the world that appears and disappears that constant's own comment
|
|
2282
2311
|
* describes. A consumer measured 82 materials a model over two to four draws each, and a
|
|
2283
2312
|
* reflection replaying the frame shares the ring with the pass that recorded it.
|
|
@@ -139,7 +139,9 @@ import { LightField } from '../../driftLight/lightField.js';
|
|
|
139
139
|
import { WorldLightField } from '../../driftLight/worldLightField.js';
|
|
140
140
|
import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
|
|
141
141
|
/**
|
|
142
|
-
* How many draws one frame
|
|
142
|
+
* How many draws one frame is prepared for before anything grows: `drawsPerFrame`'s default, and
|
|
143
|
+
* the starting size of the arena and the command pool, which reallocate on demand. The two rings
|
|
144
|
+
* that cannot grow take `drawsPerFrame` itself.
|
|
143
145
|
*
|
|
144
146
|
* **Four thousand and ninety-six, and the number is an allocation rather than a limit.**
|
|
145
147
|
* `UniformRing.flush` uploads `used * slotSize`, so a frame that draws two hundred pays for two
|
|
@@ -194,36 +196,6 @@ const MAX_OVERLAYS = 64;
|
|
|
194
196
|
* and passes nobody has written yet; the cost is sixteen kilobytes.
|
|
195
197
|
*/
|
|
196
198
|
const MAX_SKY_DRAWS = 64;
|
|
197
|
-
/**
|
|
198
|
-
* Distinct materials a frame may set.
|
|
199
|
-
*
|
|
200
|
-
* A slot is spent per *change*, not per draw, so this counts how many times a scene switches
|
|
201
|
-
* material rather than how much it draws.
|
|
202
|
-
*
|
|
203
|
-
* **A thousand and twenty-four, and it is the expensive one of the two.** A slot is the fragment
|
|
204
|
-
* block — 3,376 to 6,656 bytes depending on which of the sixteen permutations is built — paid
|
|
205
|
-
* twice for staging and buffer, so **7,168 to 13,312 bytes a slot** against 1,540 for a draw.
|
|
206
|
-
* Raising it from 256 costs 5.25 to 9.75 MB.
|
|
207
|
-
*
|
|
208
|
-
* It is worth paying for how it *fails* rather than how it performs. Past the ring
|
|
209
|
-
* `materialSlotForDraw` returns null and **every draw that needed a new material is skipped** —
|
|
210
|
-
* a stretch of the world simply not drawn, which has been reported from a consumer as
|
|
211
|
-
* ground that appears and disappears as the camera moves. A budget that hopes to stay under a
|
|
212
|
-
* ceiling is a budget that ships this the first time somebody adds a model.
|
|
213
|
-
*
|
|
214
|
-
* **This comment said "silently reuses the last material" until 2026-09-02, and so did the
|
|
215
|
-
* warning, and neither was ever true.** The skip has been there since `9d90bf7` wrote both
|
|
216
|
-
* halves in one diff. It is recorded because the wrong failure was the stated argument for
|
|
217
|
-
* raising this number from 256, and because "wrong texture" and "no geometry" send a consumer
|
|
218
|
-
* looking in completely different places — one at materials, the other at culling, which is
|
|
219
|
-
* where the reporting game spent its weeks. `frameBudget` reports the count now, so the next
|
|
220
|
-
* consumer reads a number instead of a sentence.
|
|
221
|
-
*
|
|
222
|
-
* **What would make it wrong** is a scene switching material every draw: a thousand material
|
|
223
|
-
* changes is a thousand uniform copies, and a consumer reaching this has a batching problem no
|
|
224
|
-
* ring size answers — many copies of one mesh want one material and one instanced draw.
|
|
225
|
-
*/
|
|
226
|
-
const MAX_MATERIALS_PER_FRAME = 1024;
|
|
227
199
|
/**
|
|
228
200
|
* Whether the environment probe's permutation fits this device's per-stage binding limits.
|
|
229
201
|
*
|
|
@@ -992,8 +964,37 @@ export class WebGPURenderer {
|
|
|
992
964
|
* geometry come first.
|
|
993
965
|
*/
|
|
994
966
|
budget = new FrameBudget();
|
|
995
|
-
drawBudget = this.budget.line('draws',
|
|
996
|
-
|
|
967
|
+
drawBudget = this.budget.line('draws', null);
|
|
968
|
+
/**
|
|
969
|
+
* Distinct materials a frame may set: `materialChangesPerFrame`, 1,024 unless a consumer asks.
|
|
970
|
+
*
|
|
971
|
+
* A slot is spent per *change*, not per draw, so this counts how many times a scene switches
|
|
972
|
+
* material rather than how much it draws.
|
|
973
|
+
*
|
|
974
|
+
* **A thousand and twenty-four by default, and it is the expensive one of the two.** A slot is the fragment
|
|
975
|
+
* block — 3,376 to 6,656 bytes depending on which of the sixteen permutations is built — paid
|
|
976
|
+
* twice for staging and buffer, so **7,168 to 13,312 bytes a slot** against 1,540 for a draw.
|
|
977
|
+
* Raising it from 256 costs 5.25 to 9.75 MB.
|
|
978
|
+
*
|
|
979
|
+
* It is worth paying for how it *fails* rather than how it performs. Past the ring
|
|
980
|
+
* `materialSlotForDraw` returns null and **every draw that needed a new material is skipped** —
|
|
981
|
+
* a stretch of the world simply not drawn, which has been reported from a consumer as
|
|
982
|
+
* ground that appears and disappears as the camera moves. A budget that hopes to stay under a
|
|
983
|
+
* ceiling is a budget that ships this the first time somebody adds a model.
|
|
984
|
+
*
|
|
985
|
+
* **This comment said "silently reuses the last material" until 2026-09-02, and so did the
|
|
986
|
+
* warning, and neither was ever true.** The skip has been there since `9d90bf7` wrote both
|
|
987
|
+
* halves in one diff. It is recorded because the wrong failure was the stated argument for
|
|
988
|
+
* raising this number from 256, and because "wrong texture" and "no geometry" send a consumer
|
|
989
|
+
* looking in completely different places — one at materials, the other at culling, which is
|
|
990
|
+
* where the reporting game spent its weeks. `frameBudget` reports the count now, so the next
|
|
991
|
+
* consumer reads a number instead of a sentence.
|
|
992
|
+
*
|
|
993
|
+
* **What would make it wrong** is a scene switching material every draw: a thousand material
|
|
994
|
+
* changes is a thousand uniform copies, and a consumer reaching this has a batching problem no
|
|
995
|
+
* ring size answers — many copies of one mesh want one material and one instanced draw.
|
|
996
|
+
*/
|
|
997
|
+
materialBudget = this.budget.line('materials', null);
|
|
997
998
|
/**
|
|
998
999
|
* Native bind groups built this frame, which is the one cost a consumer could not see.
|
|
999
1000
|
*
|
|
@@ -1004,7 +1005,7 @@ export class WebGPURenderer {
|
|
|
1004
1005
|
* in draw order, precisely because the frame reported draws and material changes and never this.
|
|
1005
1006
|
*/
|
|
1006
1007
|
bindGroupBudget = this.budget.line('bind groups', null);
|
|
1007
|
-
shadowDrawBudget = this.budget.line('shadow draws',
|
|
1008
|
+
shadowDrawBudget = this.budget.line('shadow draws', null);
|
|
1008
1009
|
scatterDepthBudget = this.budget.line('scatter shadow draws', MAX_SCATTER_DEPTH_DRAWS);
|
|
1009
1010
|
waterBudget = this.budget.line('water bodies', MAX_WATER_BODIES_PER_FRAME);
|
|
1010
1011
|
lightVolumeBudget = this.budget.line('light volumes', MAX_LIGHT_VOLUMES_PER_FRAME);
|
|
@@ -4782,6 +4783,9 @@ export class WebGPURenderer {
|
|
|
4782
4783
|
}
|
|
4783
4784
|
: quality;
|
|
4784
4785
|
quality = this.quality;
|
|
4786
|
+
this.materialBudget.limit(quality.materialChangesPerFrame);
|
|
4787
|
+
this.drawBudget.limit(quality.drawsPerFrame);
|
|
4788
|
+
this.shadowDrawBudget.limit(quality.drawsPerFrame);
|
|
4785
4789
|
this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
|
|
4786
4790
|
this.exposure = quality.outputExposure;
|
|
4787
4791
|
this.flushSchedule =
|
|
@@ -4879,7 +4883,7 @@ export class WebGPURenderer {
|
|
|
4879
4883
|
this.skinnedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true);
|
|
4880
4884
|
this.morphedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, false, true);
|
|
4881
4885
|
this.bothBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true, true);
|
|
4882
|
-
this.perDraw = new UniformRing(device, FLAT_VERT_SIZE,
|
|
4886
|
+
this.perDraw = new UniformRing(device, FLAT_VERT_SIZE, quality.drawsPerFrame, USAGE_UNIFORM_DST, 'flat.vertRing', this.drawBudget);
|
|
4883
4887
|
this.perFrameStaging = new ArrayBuffer(this.fragment.uniformSize);
|
|
4884
4888
|
this.perFrameFloats = new Float32Array(this.perFrameStaging);
|
|
4885
4889
|
this.perFrameInts = new Int32Array(this.perFrameStaging);
|
|
@@ -4896,7 +4900,7 @@ export class WebGPURenderer {
|
|
|
4896
4900
|
* A slot per *material change* rather than per draw: `drawMesh` reuses the open slot until
|
|
4897
4901
|
* something dirties it, so a scene drawing a hundred meshes of one material spends one.
|
|
4898
4902
|
*/
|
|
4899
|
-
this.perFrame = new UniformRing(device, this.fragment.uniformSize,
|
|
4903
|
+
this.perFrame = new UniformRing(device, this.fragment.uniformSize, this.quality.materialChangesPerFrame, USAGE_UNIFORM_DST, 'flat.fragRing', this.materialBudget);
|
|
4900
4904
|
/*
|
|
4901
4905
|
* A one-pixel stand-in for the albedo binding. `uAlbedoEnabled` is zero for untextured
|
|
4902
4906
|
* geometry, which is nearly all of it, so the shader never samples this — but WebGPU
|
|
@@ -5488,7 +5492,7 @@ export class WebGPURenderer {
|
|
|
5488
5492
|
this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
|
|
5489
5493
|
this.shadowDraws = new UniformRing(device,
|
|
5490
5494
|
/* The cutout block is the plain one and two fields more, and a slot must hold either. */
|
|
5491
|
-
Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE),
|
|
5495
|
+
Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE), quality.drawsPerFrame, USAGE_UNIFORM_DST, 'shadow.drawRing', this.shadowDrawBudget);
|
|
5492
5496
|
const depthPassUniforms = device.createBuffer({
|
|
5493
5497
|
label: 'depth.passUniforms',
|
|
5494
5498
|
size: DEPTH_FRAG_SIZE,
|
|
@@ -6639,7 +6643,7 @@ export class WebGPURenderer {
|
|
|
6639
6643
|
if (slot === null) {
|
|
6640
6644
|
if (!this.warnedMaterialsFull) {
|
|
6641
6645
|
this.warnedMaterialsFull = true;
|
|
6642
|
-
console.warn(`WebGPU: more than ${
|
|
6646
|
+
console.warn(`WebGPU: more than ${this.quality.materialChangesPerFrame} material changes in a frame; the draws asking for the rest are skipped. renderer.frameBudget names the line and the count.`);
|
|
6643
6647
|
}
|
|
6644
6648
|
return null;
|
|
6645
6649
|
}
|
|
@@ -6832,7 +6836,7 @@ export class WebGPURenderer {
|
|
|
6832
6836
|
if (slot === null) {
|
|
6833
6837
|
if (!this.warnedFull) {
|
|
6834
6838
|
this.warnedFull = true;
|
|
6835
|
-
console.warn(`WebGPU: more than ${
|
|
6839
|
+
console.warn(`WebGPU: more than ${this.quality.drawsPerFrame} draws in a frame; the rest are skipped`);
|
|
6836
6840
|
}
|
|
6837
6841
|
return;
|
|
6838
6842
|
}
|
|
@@ -6975,7 +6979,7 @@ export class WebGPURenderer {
|
|
|
6975
6979
|
/*
|
|
6976
6980
|
* Guards the draw call only, not the restore below.
|
|
6977
6981
|
*
|
|
6978
|
-
* `materialSlotForDraw` returns null once `
|
|
6982
|
+
* `materialSlotForDraw` returns null once `materialChangesPerFrame` is exhausted, and an
|
|
6979
6983
|
* early `return` here used to skip the restore too — which left `uOpacity`,
|
|
6980
6984
|
* `uLightingEnabled` and `uFogEnabled` dirtied in `perFrameFloats`/`perFrameInts` for the
|
|
6981
6985
|
* rest of the frame, since that scratch block is cumulative state shared by every draw
|
|
@@ -7232,7 +7236,7 @@ export class WebGPURenderer {
|
|
|
7232
7236
|
if (slot === null) {
|
|
7233
7237
|
if (!this.warnedFull) {
|
|
7234
7238
|
this.warnedFull = true;
|
|
7235
|
-
console.warn(`WebGPU: more than ${
|
|
7239
|
+
console.warn(`WebGPU: more than ${this.quality.drawsPerFrame} draws in a frame; the rest are skipped`);
|
|
7236
7240
|
}
|
|
7237
7241
|
return;
|
|
7238
7242
|
}
|
|
@@ -7576,7 +7580,7 @@ export class WebGPURenderer {
|
|
|
7576
7580
|
* *unchanged*, so a run of entries sharing one material has no shorter spelling and the sink
|
|
7577
7581
|
* bound once per entry. On this backend that is a cliff rather than a slope: `setMaterial`
|
|
7578
7582
|
* dirties `materialSlot`, so a bind per entry is a **slot per draw** out of
|
|
7579
|
-
* `
|
|
7583
|
+
* `materialChangesPerFrame` — and past the ring `materialSlotForDraw` returns null and the draw
|
|
7580
7584
|
* is skipped, which is the world that appears and disappears that constant's own comment
|
|
7581
7585
|
* describes. A consumer measured 82 materials a model over two to four draws each, and a
|
|
7582
7586
|
* reflection replaying the frame shares the ring with the pass that recorded it.
|