@driftengine/core 4.7.0 → 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 +32 -1
- package/README.md +166 -5
- package/dist/render/backend/webgl2/renderer.d.ts +10 -13
- package/dist/render/backend/webgl2/renderer.js +21 -18
- package/dist/render/pass.d.ts +4 -2
- package/dist/render/renderQuality.d.ts +4 -4
- package/dist/render/rendererHarness.js +9 -1
- package/dist/world/freeze.d.ts +1 -1
- package/package.json +13 -9
- package/src/render/backend/webgl2/renderer.ts +21 -18
- package/src/render/pass.ts +4 -2
- package/src/render/renderQuality.ts +4 -4
- package/src/render/rendererHarness.ts +8 -1
- package/src/world/freeze.ts +1 -1
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
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
|
+
},
|
|
2
33
|
{
|
|
3
34
|
"version": "4.7.0",
|
|
4
35
|
"date": "2026-10-01",
|
|
@@ -2907,7 +2938,7 @@
|
|
|
2907
2938
|
},
|
|
2908
2939
|
{
|
|
2909
2940
|
"kind": "added",
|
|
2910
|
-
"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",
|
|
2911
2942
|
"short": "`KickDetector.level` exposes the whitened low end, which keeps its dynamics where `energy` saturates on a loud master."
|
|
2912
2943
|
},
|
|
2913
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.
|
|
@@ -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;
|
package/dist/render/pass.d.ts
CHANGED
|
@@ -34,7 +34,8 @@ import type { FrameResource } from './frame/index.ts';
|
|
|
34
34
|
*/
|
|
35
35
|
export type PassDevice = {
|
|
36
36
|
/**
|
|
37
|
-
* Pre-multiply a projection by this **if your WGSL was generated from GLSL**.
|
|
37
|
+
* Pre-multiply a projection by this **if your WGSL was generated from GLSL**. On WebGL2 it is
|
|
38
|
+
* the depth remap while that context draws reversed depth, and the identity where it cannot.
|
|
38
39
|
*
|
|
39
40
|
* A contributed pass takes its camera from its caller, so it never sees the corrected matrix the
|
|
40
41
|
* renderer built for its own verbs. Without this it would have to copy four numbers — and a
|
|
@@ -55,7 +56,8 @@ export type PassDevice = {
|
|
|
55
56
|
*/
|
|
56
57
|
readonly clipCorrection: Float32Array;
|
|
57
58
|
/**
|
|
58
|
-
* Pre-multiply a projection by this **if you wrote your WGSL by hand**.
|
|
59
|
+
* Pre-multiply a projection by this **if you wrote your WGSL by hand**. On WebGL2 it is the same
|
|
60
|
+
* matrix as `clipCorrection`.
|
|
59
61
|
*
|
|
60
62
|
* The same matrix without the Y negation, because there is no generated negation to cancel.
|
|
61
63
|
* Taking `clipCorrection` in a hand-written shader mirrors the picture vertically — and mirrors
|
|
@@ -595,10 +595,10 @@ export interface RenderQuality {
|
|
|
595
595
|
* the output size with no upscaling, which is a temporal antialiaser with a better neighbourhood
|
|
596
596
|
* rule than `temporalAa`'s. A sentinel that collided with it would make that unaskable.
|
|
597
597
|
*
|
|
598
|
-
* The range is 1.3 to 2 and a value
|
|
599
|
-
* render saves less than a third of the fragment work and the resolve's own cost
|
|
600
|
-
* the render is a quarter of the output and no reconstruction holds an edge
|
|
601
|
-
* negative number is off, not a clamp to the bottom: it cannot mean "a little".
|
|
598
|
+
* The range is 1.3 to 2 and a positive value outside it is clamped to the end it is nearest.
|
|
599
|
+
* Below 1.3 the render saves less than a third of the fragment work and the resolve's own cost
|
|
600
|
+
* eats it; above 2 the render is a quarter of the output and no reconstruction holds an edge
|
|
601
|
+
* through that. A negative number is off, not a clamp to the bottom: it cannot mean "a little".
|
|
602
602
|
*
|
|
603
603
|
* **What it needs from you.** A `createMover()` per moving object, passed with that object's
|
|
604
604
|
* draws where `drawMesh` takes last frame's matrix — the renderer keeps last frame's model and,
|
|
@@ -123,7 +123,15 @@ export function recordingGl(options = {}) {
|
|
|
123
123
|
and a small answer is read as a device that cannot do the thing. */
|
|
124
124
|
return 4096;
|
|
125
125
|
},
|
|
126
|
-
getExtension: (name) =>
|
|
126
|
+
getExtension: (name) => {
|
|
127
|
+
if (!extensions.has(String(name)))
|
|
128
|
+
return null;
|
|
129
|
+
/* Clip control is called as well as asked for, so its object has to carry the call. */
|
|
130
|
+
if (name === 'EXT_clip_control') {
|
|
131
|
+
return { LOWER_LEFT_EXT: 0x8ca1, ZERO_TO_ONE_EXT: 0x935f, clipControlEXT: () => undefined };
|
|
132
|
+
}
|
|
133
|
+
return {};
|
|
134
|
+
},
|
|
127
135
|
getShaderParameter: () => true,
|
|
128
136
|
/*
|
|
129
137
|
* A shader remembers its own source, and a program remembers its fragment one, so a link can
|
package/dist/world/freeze.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* What freezing needs of a world.
|
|
24
24
|
*
|
|
25
25
|
* Taken as a parameter rather than imported, on `AGENTS.md`'s rule: core has no entity store and a
|
|
26
|
-
* consumer may be holding one of several.
|
|
26
|
+
* consumer may be holding one of several. Five methods, all of which any store can answer.
|
|
27
27
|
*/
|
|
28
28
|
export interface FreezableWorld {
|
|
29
29
|
/** Every entity whose home is this cell, live or not. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.7.
|
|
4
|
-
"description": "DriftEngine
|
|
3
|
+
"version": "4.7.1",
|
|
4
|
+
"description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.7.
|
|
33
|
-
"@driftengine/physics": "4.7.
|
|
32
|
+
"@driftengine/drft": "4.7.1",
|
|
33
|
+
"@driftengine/physics": "4.7.1",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"url": "git+https://github.com/drftrun/driftengine.git",
|
|
40
40
|
"directory": "packages/core"
|
|
41
41
|
},
|
|
42
|
-
"homepage": "https://
|
|
42
|
+
"homepage": "https://driftengine.dev/docs",
|
|
43
43
|
"bugs": "https://github.com/drftrun/driftengine/issues",
|
|
44
44
|
"keywords": [
|
|
45
45
|
"driftengine",
|
|
@@ -51,7 +51,11 @@
|
|
|
51
51
|
"webgl2",
|
|
52
52
|
"scene-graph",
|
|
53
53
|
"game-engine",
|
|
54
|
-
"game-loop"
|
|
54
|
+
"game-loop",
|
|
55
|
+
"gamedev",
|
|
56
|
+
"webgpu-game-engine",
|
|
57
|
+
"physics",
|
|
58
|
+
"rigid-body"
|
|
55
59
|
],
|
|
56
60
|
"engines": {
|
|
57
61
|
"node": ">=22.12.0"
|
|
@@ -63,8 +67,8 @@
|
|
|
63
67
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
64
68
|
},
|
|
65
69
|
"devDependencies": {
|
|
66
|
-
"@driftengine/nav": "4.7.
|
|
67
|
-
"@driftengine/network": "4.7.
|
|
68
|
-
"@driftengine/texture": "4.7.
|
|
70
|
+
"@driftengine/nav": "4.7.1",
|
|
71
|
+
"@driftengine/network": "4.7.1",
|
|
72
|
+
"@driftengine/texture": "4.7.1"
|
|
69
73
|
}
|
|
70
74
|
}
|
|
@@ -2093,14 +2093,20 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
2093
2093
|
*/
|
|
2094
2094
|
registerPass(definition: PassDefinition): PassHandle {
|
|
2095
2095
|
const handle = registerIn(this.passes, definition);
|
|
2096
|
-
/*
|
|
2097
|
-
|
|
2098
|
-
|
|
2096
|
+
/*
|
|
2097
|
+
* Both the same here: this backend runs the GLSL natively, so no generator has flipped
|
|
2098
|
+
* anything. **What they carry is the depth remap whenever this context draws reversed**, and
|
|
2099
|
+
* they were the identity until a splat capture was drawn among meshes on this backend: the
|
|
2100
|
+
* renderer's own draws go through `GL_DEPTH_REMAP` (`sceneMatrix`), a pass's projection did
|
|
2101
|
+
* not, so a pass wrote conventional depth into a reversed buffer and a splat behind a post was
|
|
2102
|
+
* drawn over it. Identity where clip control is missing and depth stays conventional.
|
|
2103
|
+
*/
|
|
2104
|
+
const correction = this.reversedDepth ? GL_DEPTH_REMAP : IDENTITY_CLIP;
|
|
2099
2105
|
this.passDevice ??= {
|
|
2100
2106
|
backend: 'webgl2',
|
|
2101
2107
|
gl: this.gl,
|
|
2102
|
-
clipCorrection:
|
|
2103
|
-
depthCorrection:
|
|
2108
|
+
clipCorrection: correction,
|
|
2109
|
+
depthCorrection: correction,
|
|
2104
2110
|
};
|
|
2105
2111
|
if (definition.prepare !== undefined) this.preparingPasses++;
|
|
2106
2112
|
definition.init?.(this.passDevice);
|
|
@@ -4746,17 +4752,14 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
4746
4752
|
* `endPlanarReflection`; the next water draw samples the completed target.
|
|
4747
4753
|
*/
|
|
4748
4754
|
/**
|
|
4749
|
-
* **
|
|
4750
|
-
*
|
|
4751
|
-
*
|
|
4752
|
-
*
|
|
4753
|
-
*
|
|
4754
|
-
* into it however it calls this. Reported from outside after being attempted, which is the
|
|
4755
|
-
* cost of a name that promises more than the wiring does.
|
|
4755
|
+
* **Two things sample the target: water, and a wet film.** It is allocated when `water` and
|
|
4756
|
+
* `waterReflections` are both on, or when `RenderQuality.planarReflections` asks for it in a
|
|
4757
|
+
* scene with no water, and `drawFilm` reads it through `FilmOptions.reflectionStrength` for a
|
|
4758
|
+
* film on the plane it was rendered for. This said the pass was water-only after both of those
|
|
4759
|
+
* had landed, which is how a stale limitation outlives its cause: it read as a decision.
|
|
4756
4760
|
*
|
|
4757
|
-
*
|
|
4758
|
-
*
|
|
4759
|
-
* Neither is a substitute for a mirrored pass and this comment is not pretending otherwise.
|
|
4761
|
+
* A surface on any other plane, or that is not a film, reflects a probe
|
|
4762
|
+
* (`bakeReflectionProbe`), which is a room rather than a mirror.
|
|
4760
4763
|
*/
|
|
4761
4764
|
/* Like `bakeReflectionProbe`, this re-enters the **mesh pass only**: anything a scene draws
|
|
4762
4765
|
after the world, particles, light volumes, the sky, is missing from the mirror unless it is
|
|
@@ -8447,9 +8450,9 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
8447
8450
|
* tarmac at night by bisection twice, at 0.7 and again at 0.24, and landed on **0.15 to
|
|
8448
8451
|
* 0.18**. Start there rather than repeating the search.
|
|
8449
8452
|
*
|
|
8450
|
-
*
|
|
8451
|
-
*
|
|
8452
|
-
*
|
|
8453
|
+
* The mirror is a separate dial: `FilmOptions.reflectionStrength` shows the planar reflection
|
|
8454
|
+
* a frame rendered for the film's plane, weighted by the viewing angle. See
|
|
8455
|
+
* `beginPlanarReflection` for when that target exists.
|
|
8453
8456
|
*/
|
|
8454
8457
|
drawFilm(
|
|
8455
8458
|
mesh: Mesh,
|
package/src/render/pass.ts
CHANGED
|
@@ -37,7 +37,8 @@ import type { FrameResource } from './frame/index.ts';
|
|
|
37
37
|
*/
|
|
38
38
|
export type PassDevice = {
|
|
39
39
|
/**
|
|
40
|
-
* Pre-multiply a projection by this **if your WGSL was generated from GLSL**.
|
|
40
|
+
* Pre-multiply a projection by this **if your WGSL was generated from GLSL**. On WebGL2 it is
|
|
41
|
+
* the depth remap while that context draws reversed depth, and the identity where it cannot.
|
|
41
42
|
*
|
|
42
43
|
* A contributed pass takes its camera from its caller, so it never sees the corrected matrix the
|
|
43
44
|
* renderer built for its own verbs. Without this it would have to copy four numbers — and a
|
|
@@ -58,7 +59,8 @@ export type PassDevice = {
|
|
|
58
59
|
*/
|
|
59
60
|
readonly clipCorrection: Float32Array;
|
|
60
61
|
/**
|
|
61
|
-
* Pre-multiply a projection by this **if you wrote your WGSL by hand**.
|
|
62
|
+
* Pre-multiply a projection by this **if you wrote your WGSL by hand**. On WebGL2 it is the same
|
|
63
|
+
* matrix as `clipCorrection`.
|
|
62
64
|
*
|
|
63
65
|
* The same matrix without the Y negation, because there is no generated negation to cancel.
|
|
64
66
|
* Taking `clipCorrection` in a hand-written shader mirrors the picture vertically — and mirrors
|
|
@@ -604,10 +604,10 @@ export interface RenderQuality {
|
|
|
604
604
|
* the output size with no upscaling, which is a temporal antialiaser with a better neighbourhood
|
|
605
605
|
* rule than `temporalAa`'s. A sentinel that collided with it would make that unaskable.
|
|
606
606
|
*
|
|
607
|
-
* The range is 1.3 to 2 and a value
|
|
608
|
-
* render saves less than a third of the fragment work and the resolve's own cost
|
|
609
|
-
* the render is a quarter of the output and no reconstruction holds an edge
|
|
610
|
-
* negative number is off, not a clamp to the bottom: it cannot mean "a little".
|
|
607
|
+
* The range is 1.3 to 2 and a positive value outside it is clamped to the end it is nearest.
|
|
608
|
+
* Below 1.3 the render saves less than a third of the fragment work and the resolve's own cost
|
|
609
|
+
* eats it; above 2 the render is a quarter of the output and no reconstruction holds an edge
|
|
610
|
+
* through that. A negative number is off, not a clamp to the bottom: it cannot mean "a little".
|
|
611
611
|
*
|
|
612
612
|
* **What it needs from you.** A `createMover()` per moving object, passed with that object's
|
|
613
613
|
* draws where `drawMesh` takes last frame's matrix — the renderer keeps last frame's model and,
|
|
@@ -160,7 +160,14 @@ export function recordingGl(
|
|
|
160
160
|
and a small answer is read as a device that cannot do the thing. */
|
|
161
161
|
return 4096;
|
|
162
162
|
},
|
|
163
|
-
getExtension: (name: unknown) =>
|
|
163
|
+
getExtension: (name: unknown) => {
|
|
164
|
+
if (!extensions.has(String(name))) return null;
|
|
165
|
+
/* Clip control is called as well as asked for, so its object has to carry the call. */
|
|
166
|
+
if (name === 'EXT_clip_control') {
|
|
167
|
+
return { LOWER_LEFT_EXT: 0x8ca1, ZERO_TO_ONE_EXT: 0x935f, clipControlEXT: () => undefined };
|
|
168
|
+
}
|
|
169
|
+
return {};
|
|
170
|
+
},
|
|
164
171
|
getShaderParameter: () => true,
|
|
165
172
|
/*
|
|
166
173
|
* A shader remembers its own source, and a program remembers its fragment one, so a link can
|
package/src/world/freeze.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* What freezing needs of a world.
|
|
25
25
|
*
|
|
26
26
|
* Taken as a parameter rather than imported, on `AGENTS.md`'s rule: core has no entity store and a
|
|
27
|
-
* consumer may be holding one of several.
|
|
27
|
+
* consumer may be holding one of several. Five methods, all of which any store can answer.
|
|
28
28
|
*/
|
|
29
29
|
export interface FreezableWorld {
|
|
30
30
|
/** Every entity whose home is this cell, live or not. */
|