@driftengine/core 4.7.3 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +67 -0
- package/README.md +42 -1
- package/dist/geometry/meshBuilder.js +3 -2
- package/dist/math/exact.js +3 -3
- package/dist/render/mesh.js +0 -2
- package/dist/render/plumeRenderer.js +0 -29
- package/dist/scene/node.js +1 -1
- package/package.json +6 -6
- package/src/geometry/meshBuilder.ts +3 -2
- package/src/math/exact.ts +3 -3
- package/src/render/backend/webgpu/linePass.ts +0 -1
- package/src/render/mesh.ts +0 -2
- package/src/render/plumeRenderer.ts +0 -30
- package/src/scene/node.ts +1 -1
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,71 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "4.8.0",
|
|
4
|
+
"date": "2026-10-03",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "changed",
|
|
8
|
+
"short": "A drift/* answer that is one of a few things is an enum a script matches on: a contact, a phase of matter, a chemistry event, a tree's or a save's status, the editor's modes, a hand and its joints.",
|
|
9
|
+
"text": "Twelve enums are declared by the engine for DriftScript 1.15.0's host enums: `TreeStatus`, `MatterPhase`, `ChemistryEvent`, `ContactKind`, `SaveStatus`, `GizmoMode`, `GizmoSpace`, `GizmoHandle`, `EditorMode`, `FieldKind`, `Hand` and `HandJoint`. `behavior.tick`, `step` and `status`, `chemistry.phase` and `eventKind`, `physics.contactKind`, `persistence.saveStatus` and the editor's `gizmoMode`, `space`, `hovered`, `mode` and `fieldKind` answer a variant where they answered a number or a string, and `drift/xr` takes a `Hand` and a `HandJoint` where it took `\"right\"` and `\"index-finger-tip\"`. A script that compared one of these against a number or a string no longer compiles, which is the point: those comparisons were checked by nothing, and a misspelt joint read zero. Each variant list is tested against the engine constants it translates. `contactKind`, `eventKind` and `fieldKind` refuse an index past what exists, where they answered a stale entry or nothing."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"kind": "fixed",
|
|
13
|
+
"short": "An optional component field reads and writes as an option from a script, in a query loop and through a handle: a cleared one reads none, and writing one no longer stores NaN.",
|
|
14
|
+
"text": "**`driftscript` moves to 1.16.0.** A loop read only an optional field's value column and an assignment put the option object into it, which stored `NaN` marked present; through a handle the read answered a bare number. The compiler now reads and writes the presence column beside the value, and `drift/ecs`'s `read` answers `undefined` for an absent optional field where it answered zero, which the compiler turns into `none`. The entities example's frogs keep their target as an `Entity?`, where they kept a flag beside a plain `Entity` because of this."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"kind": "fixed",
|
|
18
|
+
"short": "A lockstep session whose player holds a control on the first tick stays in step: the ticks before anyone can publish an input are neutral on every peer, where each peer guessed its own.",
|
|
19
|
+
"text": "No peer can publish an input for the ticks before its first tick plus the input delay, so every peer guessed them and nothing ever corrected the guess. A guess repeats the newest input the log holds, and for a peer's own participant that was the input it had just submitted for a later tick, which the other peer cannot know. So a key held when the match began split the two worlds, and the first fingerprint halted the session. `LockstepSession` settles those ticks as neutral for every participant on its first `submit`."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"kind": "fixed",
|
|
23
|
+
"short": "WebSocketTransport takes a browser WebSocket as it is typed: the socket shape asked for any buffer, which a browser's send does not accept, so the package's own example did not compile.",
|
|
24
|
+
"text": "`SocketLike.send` and `DataChannelLike.send` take a view of an `ArrayBuffer`, which is what a browser's `WebSocket` and `RTCDataChannel` accept, and both transports copy a message on shared memory off it before sending, since a browser refuses to send one."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"kind": "added",
|
|
28
|
+
"short": "runSchedule takes a failure reporter: every system failure, every tick, with the system's name, the tick and the error, where it logged once per system and nothing else could see it.",
|
|
29
|
+
"text": "`runSchedule(world, schedule, tick, report)`. Without a reporter a failing system is still skipped and logged once per system per world, as before. With one, nothing is logged and the reporter decides: count failures, show them in an overlay, send them to telemetry, or throw to make a failure fatal while developing, which stops the schedule at that system."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"kind": "changed",
|
|
33
|
+
"short": "A system's component access follows its calls into other files, so DS0291 stops calling a correct declaration unused and an undeclared write is a compile error, not a refusal at runtime.",
|
|
34
|
+
"text": "From DriftScript 1.16.0. Access was inferred from a module's own functions, so a helper imported from another file touched nothing as far as a system was concerned: a declaration of what it wrote drew `DS0291`, and leaving it out drew no `DS0288` and left the write out of the metadata the engine builds its schedule from. And from 1.15.0, `==` compares what two values hold, so an enum variant still equals itself after a hot reload; it compared objects by identity, and a rule written `if round.phase != Phase.Playing` returned forever after the first save."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"kind": "added",
|
|
38
|
+
"short": "The manual gains a DriftScript section and a networking chapter: what a script is for, setting one up, patterns, everything it can reach, testing, and two peers in one match over a lossy link.",
|
|
39
|
+
"text": "**DriftScript in a game** says what belongs in a script and what stays in TypeScript, **Setting up scripts** covers the build, binding, services and hot reload, **Patterns** names the shapes the examples share, **What a script can reach** lists every module, function and enum from the capability file, and **Determinism, testing and shipping** covers `@deterministic`, testing a script in Node and what reaches the bundle. The starter and the first game keep their rules in `.drs` files, the starter carries the Vite config and declaration a copied project needs and builds without a warning, and the first game's round is tested in Node. **Networking and rollback** is built on the new netplay example: two peers in one page over an impaired loopback, with the link, the redundancy and the input delay as live switches."
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"version": "4.7.4",
|
|
45
|
+
"date": "2026-10-02",
|
|
46
|
+
"entries": [
|
|
47
|
+
{
|
|
48
|
+
"kind": "fixed",
|
|
49
|
+
"short": "A component's declared field values fill every field an entity is not given: an add with some fields, a prefab that leaves one out, a script's instantiate, a save from before the field.",
|
|
50
|
+
"text": "**`driftscript` moves to 1.14.0**, whose compiler now carries the value each component field was declared with (`seed: u32 = 7`). Before, the value was parsed and dropped, so every entity started at zero and nothing said so. A component type has `defaults`, and `defineComponent(schema, defaults)` refuses one that names no field or does not fit its column. The store fills an omitted field from them, so `world.add` with some of the fields, a prefab that leaves one out and a script's `ecs.instantiate` all start at the declared values; a `null` given for an optional field is still its absence. `deserializeWorld` gives a field the saved scene does not have its declared value, so a save loads across an added field with what the component says. DriftScript 1.14.0 also type-checks a component's defaults, which nothing did before, and refuses one that is computed (DS0275)."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"kind": "added",
|
|
54
|
+
"short": "The manual gains XR, and a Systems section that opens with saves and preferences, each chapter built on an example that runs live and keeps its behaviour in DriftScript.",
|
|
55
|
+
"text": "**XR** covers asking a browser what it can present, entering a session, and reading the head, the controllers and a tracked hand, from TypeScript and from `drift/xr`, through the simulated runtime the package tests against. It says plainly that the renderer cannot draw into a headset yet, and the xr README's sample, which called `renderer.setViewport` and `renderer.draw`, now stops where the renderer does. **Saves and preferences** covers the synchronous store and why it is synchronous, preferences checked field by field, `RemoteSaveStore` over a backend that answers later, `serializeWorld` and `deserializeWorld` with a save read back across an added field, and `drift/persistence`, around a garden bed that saves to a server the page can make slow or take offline."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"kind": "fixed",
|
|
59
|
+
"short": "Four examples behave on any machine: the vehicle starts clear of the cones, readouts fit a small frame, chemistry holds 60 fps, and DriftLight starts smoothly on a slow CPU.",
|
|
60
|
+
"text": "The vehicle spawned overlapping a cone and came to rest on it with two wheels in the air; it starts on the ramp's line. A readout is sized to the frame it is in, so the manual's 768 by 432 frames no longer cut lines off, and it can keep clear of a panel. Chemistry stepped a quarter second sixty times a second and drew 3 fps once smoke filled the air; it steps a sixteenth, about four times the clock, at 60 fps, and `stage.run` takes loop options so a slow frame catches up two ticks at most. DriftLight baked 64 bricks a frame and ran at 1 to 8 fps for its first ten seconds on a CPU three times slower; it bakes for four milliseconds a frame and stays above 18. Under that throttling 44 of the 46 examples hold 57 to 60 fps, and chemistry 16."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"kind": "changed",
|
|
64
|
+
"short": "The repository lints without a warning: oxlint's 170 are fixed, one of them a test that divided its fallback where it meant to check a length.",
|
|
65
|
+
"text": "Unused imports, constants, functions, variables and parameters are gone, products by zero are written as the terms that remain, and optional chains in tests stay optional to the end. Inside the native host, the window, the document and the canvas take their `on<type>` attributes from a base class that declares them, in place of an interface merged into each class, and fdlibm's constants are spelled with the digits a double holds, each parsing to the same value."
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
2
69
|
{
|
|
3
70
|
"version": "4.7.3",
|
|
4
71
|
"date": "2026-10-02",
|
package/README.md
CHANGED
|
@@ -36,6 +36,9 @@ import {
|
|
|
36
36
|
createRenderer,
|
|
37
37
|
startLoop,
|
|
38
38
|
} from '@driftengine/core';
|
|
39
|
+
import { bindModule } from '@driftengine/script';
|
|
40
|
+
import { loadModule, patchModule } from 'driftscript';
|
|
41
|
+
import * as spinScript from './spin.drs';
|
|
39
42
|
|
|
40
43
|
const canvas = document.querySelector<HTMLCanvasElement>('#stage');
|
|
41
44
|
if (canvas === null) throw new Error('the page must carry <canvas id="stage">');
|
|
@@ -94,6 +97,22 @@ camera.position[1] = 4.5;
|
|
|
94
97
|
camera.position[2] = 8;
|
|
95
98
|
camera.lookAt(0, 1, 0);
|
|
96
99
|
|
|
100
|
+
/*
|
|
101
|
+
* How fast the cube turns is a rule, and rules live in DriftScript: `spin.drs`. The bundler compiles
|
|
102
|
+
* the file, `loadModule` makes a module of it, and `bindModule` gives it the engine capabilities it
|
|
103
|
+
* imports. Saving the file while the page is open replaces its function in place.
|
|
104
|
+
*/
|
|
105
|
+
const spinModule = loadModule(spinScript as Record<string, unknown>);
|
|
106
|
+
const bound = bindModule(spinModule, {});
|
|
107
|
+
if (!bound.bound) throw new Error(bound.reason);
|
|
108
|
+
const rules = spinModule.exports as unknown as { spinRate(time: number): number };
|
|
109
|
+
|
|
110
|
+
if (import.meta.hot) {
|
|
111
|
+
import.meta.hot.accept('./spin.drs', (next) => {
|
|
112
|
+
if (next !== undefined) patchModule(spinModule, next as Record<string, unknown>);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
97
116
|
/*
|
|
98
117
|
* Two angles, because that is what `alpha` is for. The simulation advances in fixed steps and
|
|
99
118
|
* the display does not, so a frame almost never lands on a step boundary: drawing `spin`
|
|
@@ -102,11 +121,13 @@ camera.lookAt(0, 1, 0);
|
|
|
102
121
|
*/
|
|
103
122
|
let spin = 0;
|
|
104
123
|
let previousSpin = 0;
|
|
124
|
+
let time = 0;
|
|
105
125
|
|
|
106
126
|
startLoop({
|
|
107
127
|
simulate(dt) {
|
|
108
128
|
previousSpin = spin;
|
|
109
|
-
|
|
129
|
+
time += dt;
|
|
130
|
+
spin += dt * rules.spinRate(time);
|
|
110
131
|
},
|
|
111
132
|
render(alpha) {
|
|
112
133
|
spinner.setRotationAxisAngle(0, 1, 0, previousSpin + (spin - previousSpin) * alpha);
|
|
@@ -123,6 +144,26 @@ startLoop({
|
|
|
123
144
|
});
|
|
124
145
|
```
|
|
125
146
|
|
|
147
|
+
How fast the cube turns is a rule, so it lives in DriftScript, in `spin.drs` beside the page:
|
|
148
|
+
|
|
149
|
+
```drs
|
|
150
|
+
// How the cube turns. The page owns the cube; this file only says how fast it goes.
|
|
151
|
+
//
|
|
152
|
+
// Save it while the page is open and the cube takes the new rate without the page reloading. Try a
|
|
153
|
+
// steady 2, a cube that stops every few seconds, or one that turns the other way.
|
|
154
|
+
|
|
155
|
+
import { sin } from "std/math"
|
|
156
|
+
|
|
157
|
+
// Radians a second: a steady turn that quickens and eases every few seconds.
|
|
158
|
+
fn spinRate(time: f32) -> f32 {
|
|
159
|
+
return 0.8 + 0.4 * math.sin(time * 0.5)
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The bundler compiles it with DriftScript's Vite plugin, set up as in the starter's `vite.config.ts`;
|
|
164
|
+
[Installation](https://driftengine.dev/docs/start/installation#driftscript) has the two packages and
|
|
165
|
+
the config.
|
|
166
|
+
|
|
126
167
|
This is the engine's `examples/starter`, and a test keeps the two the same.
|
|
127
168
|
[Hello world](https://driftengine.dev/docs/start/hello-world) walks through it, and
|
|
128
169
|
[Your first game](https://driftengine.dev/docs/start/first-game) builds a complete one.
|
|
@@ -812,8 +812,9 @@ export class MeshBuilder {
|
|
|
812
812
|
// later one is transported from it, so the choice never shows.
|
|
813
813
|
const ax = Math.abs(tx) < 0.9 ? 1 : 0;
|
|
814
814
|
const ay = Math.abs(tx) < 0.9 ? 0 : 1;
|
|
815
|
-
|
|
816
|
-
|
|
815
|
+
// (ax, ay, 0) x tangent, with the terms the zero removes left out.
|
|
816
|
+
ux = ay * tz;
|
|
817
|
+
uy = -ax * tz;
|
|
817
818
|
uz = ax * ty - ay * tx;
|
|
818
819
|
seeded = true;
|
|
819
820
|
}
|
package/dist/math/exact.js
CHANGED
|
@@ -66,14 +66,14 @@ const HALF_PI = 1.5707963267948966;
|
|
|
66
66
|
const PI = 3.141592653589793;
|
|
67
67
|
/* fdlibm's `__kernel_sin` coefficients. */
|
|
68
68
|
const S1 = -1.6666666666666632e-1;
|
|
69
|
-
const S2 = 8.
|
|
69
|
+
const S2 = 8.33333333332249e-3;
|
|
70
70
|
const S3 = -1.984126982985795e-4;
|
|
71
71
|
const S4 = 2.7557313707070068e-6;
|
|
72
72
|
const S5 = -2.5050760253406863e-8;
|
|
73
73
|
const S6 = 1.5896909952115501e-10;
|
|
74
74
|
/* fdlibm's `__kernel_cos` coefficients. */
|
|
75
75
|
const C1 = 4.1666666666666602e-2;
|
|
76
|
-
const C2 = -1.
|
|
76
|
+
const C2 = -1.388888888887411e-3;
|
|
77
77
|
const C3 = 2.480158728947673e-5;
|
|
78
78
|
const C4 = -2.7557314351390663e-7;
|
|
79
79
|
const C5 = 2.0875723212981748e-9;
|
|
@@ -273,7 +273,7 @@ export function exactLog(x) {
|
|
|
273
273
|
/* |f| < 2^-20: two terms of the series are the whole answer. */
|
|
274
274
|
if (f === 0)
|
|
275
275
|
return k === 0 ? 0 : k * LN2_HI + k * LN2_LO;
|
|
276
|
-
const r = f * f * (0.5 - 0.
|
|
276
|
+
const r = f * f * (0.5 - 0.3333333333333333 * f);
|
|
277
277
|
return k === 0 ? f - r : k * LN2_HI - (r - k * LN2_LO - f);
|
|
278
278
|
}
|
|
279
279
|
const s = f / (2 + f);
|
package/dist/render/mesh.js
CHANGED
|
@@ -23,8 +23,6 @@ import { validateMeshData } from '@driftengine/drft';
|
|
|
23
23
|
* well. Both halves are fixed — `flat.ts` normalises the lobe, and this is the width that
|
|
24
24
|
* actually matches.
|
|
25
25
|
*/
|
|
26
|
-
/* The engine's fixed lobe, stated once in `vertexDefaults.ts` so both backends read one number. */
|
|
27
|
-
const DEFAULT_ROUGHNESS = ABSENT_ATTRIBUTE['roughness']?.[0] ?? 0.4277;
|
|
28
26
|
/** Attribute locations are fixed engine-wide (`layout(location = n)` in GLSL). */
|
|
29
27
|
const ATTR_POSITION = 0;
|
|
30
28
|
const ATTR_NORMAL = 1;
|
|
@@ -3,37 +3,8 @@ import { compileProgram, uniformLocations } from './shader.js';
|
|
|
3
3
|
import { bindAtmosphere } from './atmosphere.js';
|
|
4
4
|
import { plumeShaders } from './plumeMaterial.js';
|
|
5
5
|
import { buildPlumeGeometry } from './plumeGeometry.js';
|
|
6
|
-
/**
|
|
7
|
-
* Blades in each plume's cross, and the vertices that costs.
|
|
8
|
-
*
|
|
9
|
-
* **Two, crossed at 90°, instead of one card turned to face the viewer.** A single
|
|
10
|
-
* billboard has no three-dimensional structure to see: orbiting a fire showed the
|
|
11
|
-
* identical silhouette from every angle, because every card shared one camera-derived
|
|
12
|
-
* axis and the whole plume swivelled as a sheet. Reported twice: the fire did not read
|
|
13
|
-
* as three-dimensional, it read as something that moved with the camera — and after a
|
|
14
|
-
* first attempt that only *leaned* edge-on cards toward the viewer, it still did,
|
|
15
|
-
* correctly, because leaning still rotates whichever cards are edge-on, so the ensemble
|
|
16
|
-
* still tracks the viewer.
|
|
17
|
-
*
|
|
18
|
-
* A cross cannot track anything: both blades are fixed in world space, so turning
|
|
19
|
-
* around a fire genuinely brings one broadside as the other goes edge-on, which is
|
|
20
|
-
* the parallax a volume has. It also removes the reason billboarding existed — a
|
|
21
|
-
* blade going edge-on no longer vanishes, because its partner is square-on at that
|
|
22
|
-
* exact moment.
|
|
23
|
-
*
|
|
24
|
-
* Two rather than three because the plumes blend additively and are drawn in
|
|
25
|
-
* quantity: the third blade costs 50% more geometry for a difference the eye does not
|
|
26
|
-
* separate at the sizes these are drawn.
|
|
27
|
-
*/
|
|
28
|
-
const BLADES = 2;
|
|
29
6
|
/* The shared count, so both backends step a plume's vertices the same way. */
|
|
30
7
|
const VERTS_PER_PLUME = PLUME_VERTS;
|
|
31
|
-
const CORNERS = [
|
|
32
|
-
[-1, -1],
|
|
33
|
-
[1, -1],
|
|
34
|
-
[1, 1],
|
|
35
|
-
[-1, 1],
|
|
36
|
-
];
|
|
37
8
|
export class PlumeRenderer {
|
|
38
9
|
program;
|
|
39
10
|
uniforms;
|
package/dist/scene/node.js
CHANGED
|
@@ -122,7 +122,7 @@ export class SceneNode {
|
|
|
122
122
|
attachChild(child) {
|
|
123
123
|
if (child === this)
|
|
124
124
|
throw new Error('SceneNode: a node cannot be its own child (cycle)');
|
|
125
|
-
for (let at = this; at !== null; at = at.parent) {
|
|
125
|
+
for (let at = this.parent; at !== null; at = at.parent) {
|
|
126
126
|
if (at === child)
|
|
127
127
|
throw new Error('SceneNode: that would make a cycle');
|
|
128
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
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",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.
|
|
33
|
-
"@driftengine/physics": "4.
|
|
32
|
+
"@driftengine/drft": "4.8.0",
|
|
33
|
+
"@driftengine/physics": "4.8.0",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@driftengine/nav": "4.
|
|
71
|
-
"@driftengine/network": "4.
|
|
72
|
-
"@driftengine/texture": "4.
|
|
70
|
+
"@driftengine/nav": "4.8.0",
|
|
71
|
+
"@driftengine/network": "4.8.0",
|
|
72
|
+
"@driftengine/texture": "4.8.0"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -969,8 +969,9 @@ export class MeshBuilder {
|
|
|
969
969
|
// later one is transported from it, so the choice never shows.
|
|
970
970
|
const ax = Math.abs(tx) < 0.9 ? 1 : 0;
|
|
971
971
|
const ay = Math.abs(tx) < 0.9 ? 0 : 1;
|
|
972
|
-
|
|
973
|
-
|
|
972
|
+
// (ax, ay, 0) x tangent, with the terms the zero removes left out.
|
|
973
|
+
ux = ay * tz;
|
|
974
|
+
uy = -ax * tz;
|
|
974
975
|
uz = ax * ty - ay * tx;
|
|
975
976
|
seeded = true;
|
|
976
977
|
}
|
package/src/math/exact.ts
CHANGED
|
@@ -69,7 +69,7 @@ const PI = 3.141592653589793;
|
|
|
69
69
|
|
|
70
70
|
/* fdlibm's `__kernel_sin` coefficients. */
|
|
71
71
|
const S1 = -1.6666666666666632e-1;
|
|
72
|
-
const S2 = 8.
|
|
72
|
+
const S2 = 8.33333333332249e-3;
|
|
73
73
|
const S3 = -1.984126982985795e-4;
|
|
74
74
|
const S4 = 2.7557313707070068e-6;
|
|
75
75
|
const S5 = -2.5050760253406863e-8;
|
|
@@ -77,7 +77,7 @@ const S6 = 1.5896909952115501e-10;
|
|
|
77
77
|
|
|
78
78
|
/* fdlibm's `__kernel_cos` coefficients. */
|
|
79
79
|
const C1 = 4.1666666666666602e-2;
|
|
80
|
-
const C2 = -1.
|
|
80
|
+
const C2 = -1.388888888887411e-3;
|
|
81
81
|
const C3 = 2.480158728947673e-5;
|
|
82
82
|
const C4 = -2.7557314351390663e-7;
|
|
83
83
|
const C5 = 2.0875723212981748e-9;
|
|
@@ -286,7 +286,7 @@ export function exactLog(x: number): number {
|
|
|
286
286
|
if ((0x000fffff & (2 + hx)) < 3) {
|
|
287
287
|
/* |f| < 2^-20: two terms of the series are the whole answer. */
|
|
288
288
|
if (f === 0) return k === 0 ? 0 : k * LN2_HI + k * LN2_LO;
|
|
289
|
-
const r = f * f * (0.5 - 0.
|
|
289
|
+
const r = f * f * (0.5 - 0.3333333333333333 * f);
|
|
290
290
|
return k === 0 ? f - r : k * LN2_HI - (r - k * LN2_LO - f);
|
|
291
291
|
}
|
|
292
292
|
const s = f / (2 + f);
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
LINE_VERT_WGSL,
|
|
6
6
|
} from '../../shaders/generated/line.wgsl.ts';
|
|
7
7
|
import { buildSegmentQuads } from '../../segmentQuads.ts';
|
|
8
|
-
import type { LineSegments } from '../../linePoints.ts';
|
|
9
8
|
import type { UniformFields } from './scatterPass.ts';
|
|
10
9
|
import { DEPTH_FORMAT } from './flatPass.ts';
|
|
11
10
|
import type { PipelineCache } from './pipelineCache.ts';
|
package/src/render/mesh.ts
CHANGED
|
@@ -30,8 +30,6 @@ import { validateMeshData } from '@driftengine/drft';
|
|
|
30
30
|
* well. Both halves are fixed — `flat.ts` normalises the lobe, and this is the width that
|
|
31
31
|
* actually matches.
|
|
32
32
|
*/
|
|
33
|
-
/* The engine's fixed lobe, stated once in `vertexDefaults.ts` so both backends read one number. */
|
|
34
|
-
const DEFAULT_ROUGHNESS = ABSENT_ATTRIBUTE['roughness']?.[0] ?? 0.4277;
|
|
35
33
|
|
|
36
34
|
/** Attribute locations are fixed engine-wide (`layout(location = n)` in GLSL). */
|
|
37
35
|
const ATTR_POSITION = 0;
|
|
@@ -62,39 +62,9 @@ export interface PlumeOptions {
|
|
|
62
62
|
tint?: Vec3;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
* Blades in each plume's cross, and the vertices that costs.
|
|
67
|
-
*
|
|
68
|
-
* **Two, crossed at 90°, instead of one card turned to face the viewer.** A single
|
|
69
|
-
* billboard has no three-dimensional structure to see: orbiting a fire showed the
|
|
70
|
-
* identical silhouette from every angle, because every card shared one camera-derived
|
|
71
|
-
* axis and the whole plume swivelled as a sheet. Reported twice: the fire did not read
|
|
72
|
-
* as three-dimensional, it read as something that moved with the camera — and after a
|
|
73
|
-
* first attempt that only *leaned* edge-on cards toward the viewer, it still did,
|
|
74
|
-
* correctly, because leaning still rotates whichever cards are edge-on, so the ensemble
|
|
75
|
-
* still tracks the viewer.
|
|
76
|
-
*
|
|
77
|
-
* A cross cannot track anything: both blades are fixed in world space, so turning
|
|
78
|
-
* around a fire genuinely brings one broadside as the other goes edge-on, which is
|
|
79
|
-
* the parallax a volume has. It also removes the reason billboarding existed — a
|
|
80
|
-
* blade going edge-on no longer vanishes, because its partner is square-on at that
|
|
81
|
-
* exact moment.
|
|
82
|
-
*
|
|
83
|
-
* Two rather than three because the plumes blend additively and are drawn in
|
|
84
|
-
* quantity: the third blade costs 50% more geometry for a difference the eye does not
|
|
85
|
-
* separate at the sizes these are drawn.
|
|
86
|
-
*/
|
|
87
|
-
const BLADES = 2;
|
|
88
65
|
/* The shared count, so both backends step a plume's vertices the same way. */
|
|
89
66
|
const VERTS_PER_PLUME = PLUME_VERTS;
|
|
90
67
|
|
|
91
|
-
const CORNERS = [
|
|
92
|
-
[-1, -1],
|
|
93
|
-
[1, -1],
|
|
94
|
-
[1, 1],
|
|
95
|
-
[-1, 1],
|
|
96
|
-
] as const;
|
|
97
|
-
|
|
98
68
|
export class PlumeRenderer {
|
|
99
69
|
private readonly program: WebGLProgram;
|
|
100
70
|
private readonly uniforms: Record<string, WebGLUniformLocation>;
|
package/src/scene/node.ts
CHANGED
|
@@ -140,7 +140,7 @@ export class SceneNode {
|
|
|
140
140
|
*/
|
|
141
141
|
attachChild(child: SceneNode): void {
|
|
142
142
|
if (child === this) throw new Error('SceneNode: a node cannot be its own child (cycle)');
|
|
143
|
-
for (let at
|
|
143
|
+
for (let at = this.parent; at !== null; at = at.parent) {
|
|
144
144
|
if (at === child) throw new Error('SceneNode: that would make a cycle');
|
|
145
145
|
}
|
|
146
146
|
if (child.parent === this) return;
|