@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 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 phonk 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",
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 everything needs: renderer, loop, camera, input, geometry. Collision and dynamics live in [`@driftengine/physics`](../physics/README.md), which this package depends on and re-exports whole, so reaching for them through this barrel works as it always did.
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`, which fails if it drifts more than
6
- 3% — the number is a fact about the build rather than a claim in a document.
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
- Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
9
- for how the packages divide, and why some features are compiled into core on demand instead.
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
- * **This is a water feature, not a renderer one, and the name hides that.**
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
- * The target is allocated only when `water` *and* `waterReflections` are both on, and the
2322
- * only thing that samples it is `WaterRenderer`. So a scenario whose ground is a mesh slab
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
- * There is no mirror term here to raise instead. A surface that should reflect the scene
3435
- * rather than shimmer needs a planar pass, and `beginPlanarReflection` is water-only; see the
3436
- * comment on it. Splitting the two is an open request from outside and is not built.
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
- /* Identity: this backend's clip space is the one the projections are built for. */
1352
- /* Both are identity here: this backend runs the GLSL natively, so there is nothing to
1353
- correct and nothing a generator has flipped. */
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: IDENTITY_CLIP,
1358
- depthCorrection: IDENTITY_CLIP,
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
- * **This is a water feature, not a renderer one, and the name hides that.**
3592
- *
3593
- * The target is allocated only when `water` *and* `waterReflections` are both on, and the
3594
- * only thing that samples it is `WaterRenderer`. So a scenario whose ground is a mesh slab
3595
- * with `drawFilm` patches over it, which is what a wet street is, cannot mirror the scene
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
- * What is available to a surface that is not water: `bakeReflectionProbe`, which is a room
3600
- * rather than a mirror, and `drawFilm`'s sheen, which is iridescence rather than reflection.
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
- * There is no mirror term here to raise instead. A surface that should reflect the scene
6926
- * rather than shimmer needs a planar pass, and `beginPlanarReflection` is water-only; see the
6927
- * comment on it. Splitting the two is an open request from outside and is not built.
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;
@@ -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**. Identity on WebGL2.
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**. Identity on WebGL2.
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 inside it is clamped to the end it is nearest. Below 1.3 the
599
- * render saves less than a third of the fragment work and the resolve's own cost eats it; above 2
600
- * the render is a quarter of the output and no reconstruction holds an edge through that. A
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) => (extensions.has(String(name)) ? {} : null),
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
@@ -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. Six methods, all of which any store can answer.
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.0",
4
- "description": "DriftEngine reusable browser 3D runtime",
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.0",
33
- "@driftengine/physics": "4.7.0",
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://github.com/drftrun/driftengine#readme",
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.0",
67
- "@driftengine/network": "4.7.0",
68
- "@driftengine/texture": "4.7.0"
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
- /* Identity: this backend's clip space is the one the projections are built for. */
2097
- /* Both are identity here: this backend runs the GLSL natively, so there is nothing to
2098
- correct and nothing a generator has flipped. */
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: IDENTITY_CLIP,
2103
- depthCorrection: IDENTITY_CLIP,
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
- * **This is a water feature, not a renderer one, and the name hides that.**
4750
- *
4751
- * The target is allocated only when `water` *and* `waterReflections` are both on, and the
4752
- * only thing that samples it is `WaterRenderer`. So a scenario whose ground is a mesh slab
4753
- * with `drawFilm` patches over it, which is what a wet street is, cannot mirror the scene
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
- * What is available to a surface that is not water: `bakeReflectionProbe`, which is a room
4758
- * rather than a mirror, and `drawFilm`'s sheen, which is iridescence rather than reflection.
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
- * There is no mirror term here to raise instead. A surface that should reflect the scene
8451
- * rather than shimmer needs a planar pass, and `beginPlanarReflection` is water-only; see the
8452
- * comment on it. Splitting the two is an open request from outside and is not built.
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,
@@ -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**. Identity on WebGL2.
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**. Identity on WebGL2.
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 inside it is clamped to the end it is nearest. Below 1.3 the
608
- * render saves less than a third of the fragment work and the resolve's own cost eats it; above 2
609
- * the render is a quarter of the output and no reconstruction holds an edge through that. A
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) => (extensions.has(String(name)) ? {} : null),
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
@@ -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. Six methods, all of which any store can answer.
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. */