@chromatic-coherence/generative-engine 1.0.3 → 1.2.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/README.md +51 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/objectives.d.ts +96 -0
- package/dist/objectives.js +174 -0
- package/dist/post.d.ts +13 -0
- package/dist/post.js +44 -4
- package/dist/shaders.d.ts +4 -0
- package/dist/shaders.js +82 -0
- package/dist/world.d.ts +25 -0
- package/dist/world.js +95 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,46 @@ w.start();
|
|
|
49
49
|
**The ink edition** (`theme: "light"`) still renders any recipe on paper instead of night —
|
|
50
50
|
lightness inverts, strokes blend normally, SSAO becomes pencil shading. Bloom stays dark-only.
|
|
51
51
|
|
|
52
|
+
## Objectives — scenes that know where they're going
|
|
53
|
+
|
|
54
|
+
A scene gets a **goal** and the engine drives motion toward it; a live **movement meter**
|
|
55
|
+
tells you whether the scene is moving or settled — and names what is moving.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createWorld, attachObjectives } from "@chromatic-coherence/generative-engine";
|
|
59
|
+
|
|
60
|
+
const w = createWorld({ canvas });
|
|
61
|
+
const obj = attachObjectives(w);
|
|
62
|
+
|
|
63
|
+
obj.define("reveal", {
|
|
64
|
+
morph: { group: "terrain", to: 1 }, // blend the terrain to its B shape
|
|
65
|
+
camera: { dist: 420, pitch: 0.62 }, // settle the camera in close
|
|
66
|
+
fade: { group: "veil", to: 0 }, // dissolve the veil
|
|
67
|
+
pace: 3, // seconds to ~95% of the way there
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
obj.onReading(r => {
|
|
71
|
+
// { name, progress, level, floor, moving, settled, driving: ["morph:terrain","camera",...] }
|
|
72
|
+
if (r.settled) console.log(`${r.name}: settled`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// later — the goal itself moves; motion continues from the current state, no snap,
|
|
76
|
+
// and the meter lights up again:
|
|
77
|
+
obj.retarget("reveal", { camera: { dist: 300 } });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Channels: `transform` (group rigid-body spec) · `morph` · `fade` · `camera` · `custom`
|
|
81
|
+
(an `apply(progress, dt)` escape hatch). `readings()` is the pull form of `onReading`.
|
|
82
|
+
|
|
83
|
+
**The meter's semantics** come from the objective-movement measurement work this engine's
|
|
84
|
+
org ran first as science: movement is a **level sustained above a settled floor**, not a
|
|
85
|
+
discrete event. `driving` names the elevated channels — movement you can name; `settled`
|
|
86
|
+
is the sustained fall to the floor. Options: `attachObjectives(w, { floor, settleAfter })`.
|
|
87
|
+
|
|
88
|
+
The layer rides the frame loop as a `grow()` hook and drives the World through its public
|
|
89
|
+
verbs only — the renderer is untouched, and everything composes with hand-written `grow()`
|
|
90
|
+
animation.
|
|
91
|
+
|
|
52
92
|
## Demo
|
|
53
93
|
|
|
54
94
|
`demo/index.html` — serve the package folder (`python -m http.server`) and open
|
|
@@ -62,9 +102,19 @@ engine → charts-3d → charts-2d is three imports of the same code. Browsers c
|
|
|
62
102
|
contexts per page: on teardown call `dispose(true)` to release the context immediately, and
|
|
63
103
|
mount a **fresh** canvas if you rebuild (a lost context is dead on its canvas).
|
|
64
104
|
|
|
105
|
+
## Instancing, depth of field, motion blur (v1.2)
|
|
106
|
+
|
|
107
|
+
- **`w.meshInstanced(geo, transforms, opts)`** — upload a mesh once, draw it at every
|
|
108
|
+
transform in a single instanced call, shadow pass included. A forest is one draw.
|
|
109
|
+
Instances ride their group's fade / morph / transform like any other geometry.
|
|
110
|
+
- **`w.dof = { on, focus, range, maxBlur }`** — depth of field: blur grows with distance
|
|
111
|
+
from the focus plane (world units), sharp subjects protected per-tap.
|
|
112
|
+
- **`w.motionBlur = { on, strength }`** — camera motion blur by depth reprojection into
|
|
113
|
+
the previous frame; streaks clamp to 4 % of the screen.
|
|
114
|
+
|
|
65
115
|
## Roadmap
|
|
66
116
|
|
|
67
|
-
Planar mirrors
|
|
117
|
+
Planar mirrors. The chart editions stay lean.
|
|
68
118
|
|
|
69
119
|
## Licence
|
|
70
120
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { V3, TransformSpec } from "./math.js";
|
|
2
|
+
import type { World } from "./world.js";
|
|
3
|
+
export type ObjectiveCamera = {
|
|
4
|
+
target?: V3;
|
|
5
|
+
dist?: number;
|
|
6
|
+
pitch?: number;
|
|
7
|
+
yaw?: number;
|
|
8
|
+
};
|
|
9
|
+
export type ObjectiveSpec = {
|
|
10
|
+
/** rigid-body goal for a named group — interpolated spec, applied via setTransform */
|
|
11
|
+
transform?: {
|
|
12
|
+
group: string;
|
|
13
|
+
to: TransformSpec;
|
|
14
|
+
from?: TransformSpec;
|
|
15
|
+
};
|
|
16
|
+
/** morph-weight goal for a named group (meshMorph geometry). Default from: 0. */
|
|
17
|
+
morph?: {
|
|
18
|
+
group: string;
|
|
19
|
+
to: number;
|
|
20
|
+
from?: number;
|
|
21
|
+
};
|
|
22
|
+
/** fade goal for a named group. Default from: 1. */
|
|
23
|
+
fade?: {
|
|
24
|
+
group: string;
|
|
25
|
+
to: number;
|
|
26
|
+
from?: number;
|
|
27
|
+
};
|
|
28
|
+
/** camera goal — absent fields keep the camera's current value */
|
|
29
|
+
camera?: ObjectiveCamera;
|
|
30
|
+
/** escape hatch — called every tick with eased progress 0..1 */
|
|
31
|
+
custom?: (progress: number, dt: number) => void;
|
|
32
|
+
/** seconds to ~95 % of the way there. Default 3. */
|
|
33
|
+
pace?: number;
|
|
34
|
+
};
|
|
35
|
+
/** One tick's reading for one objective — the movement meter's voice. */
|
|
36
|
+
export type MovementReading = {
|
|
37
|
+
name: string;
|
|
38
|
+
/** eased approach to the goal, 0..1 */
|
|
39
|
+
progress: number;
|
|
40
|
+
/** movement level this tick (≈ 1 at a fresh goal, decaying toward 0) */
|
|
41
|
+
level: number;
|
|
42
|
+
/** the settled floor the level is read against */
|
|
43
|
+
floor: number;
|
|
44
|
+
/** level is above the floor */
|
|
45
|
+
moving: boolean;
|
|
46
|
+
/** level has stayed at/below the floor for settleAfter seconds */
|
|
47
|
+
settled: boolean;
|
|
48
|
+
/** the channels currently elevated — the movement you can name */
|
|
49
|
+
driving: string[];
|
|
50
|
+
};
|
|
51
|
+
export type ObjectivesOpts = {
|
|
52
|
+
/** the settled floor (movement level units). Default 0.02. */
|
|
53
|
+
floor?: number;
|
|
54
|
+
/** seconds the level must hold at/below the floor to read settled. Default 0.6. */
|
|
55
|
+
settleAfter?: number;
|
|
56
|
+
};
|
|
57
|
+
/** Channel writes, injected — a World in production, plain recorders in tests. */
|
|
58
|
+
export type ChannelAppliers = {
|
|
59
|
+
transform(group: string, spec: TransformSpec): void;
|
|
60
|
+
morph(group: string, weight: number): void;
|
|
61
|
+
fade(group: string, alpha: number): void;
|
|
62
|
+
camera(c: ObjectiveCamera): void;
|
|
63
|
+
};
|
|
64
|
+
export declare class ObjectivesCore {
|
|
65
|
+
private apply;
|
|
66
|
+
private objs;
|
|
67
|
+
private listeners;
|
|
68
|
+
private floor;
|
|
69
|
+
private settleAfter;
|
|
70
|
+
constructor(apply: ChannelAppliers, opts?: ObjectivesOpts);
|
|
71
|
+
/** Define (or wholly replace) a named objective. The drive starts on the next tick. */
|
|
72
|
+
define(name: string, spec: ObjectiveSpec, camNow?: ObjectiveCamera): void;
|
|
73
|
+
/** Move a live objective's goal — progress restarts FROM THE CURRENT STATE (no snap)
|
|
74
|
+
* and the meter lights up again. Channels absent from the patch keep their goal. */
|
|
75
|
+
retarget(name: string, patch: Partial<ObjectiveSpec>, camNow?: ObjectiveCamera): void;
|
|
76
|
+
/** Forget an objective — its channels stay wherever they were last driven to. */
|
|
77
|
+
remove(name: string): void;
|
|
78
|
+
/** Subscribe to per-tick readings (one call per live objective per tick). */
|
|
79
|
+
onReading(cb: (r: MovementReading) => void): () => void;
|
|
80
|
+
/** Pull the latest readings — the same objects the callbacks saw. */
|
|
81
|
+
readings(): MovementReading[];
|
|
82
|
+
/** Advance every objective by dt seconds and apply the driven values.
|
|
83
|
+
* dt <= 0 (settle() stills) re-applies values without advancing the meter. */
|
|
84
|
+
tick(dt: number): void;
|
|
85
|
+
}
|
|
86
|
+
/** The user-facing facade returned by attachObjectives(). */
|
|
87
|
+
export type Objectives = {
|
|
88
|
+
define(name: string, spec: ObjectiveSpec): void;
|
|
89
|
+
retarget(name: string, patch: Partial<ObjectiveSpec>): void;
|
|
90
|
+
remove(name: string): void;
|
|
91
|
+
onReading(cb: (r: MovementReading) => void): () => void;
|
|
92
|
+
readings(): MovementReading[];
|
|
93
|
+
};
|
|
94
|
+
/** Attach the objectives layer to a World. Rides the frame loop as a grow() hook and
|
|
95
|
+
* drives the World through its public verbs — the renderer is untouched. */
|
|
96
|
+
export declare function attachObjectives(world: World, opts?: ObjectivesOpts): Objectives;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// generative-engine — objectives.ts: THE OBJECTIVES LAYER. A scene gets a goal and the
|
|
2
|
+
// engine drives motion toward it; a live movement meter reads whether the scene is
|
|
3
|
+
// moving or settled, and names what is moving.
|
|
4
|
+
//
|
|
5
|
+
// Two layers, deliberately split:
|
|
6
|
+
// ObjectivesCore — pure logic, no GL, no DOM. Channel writes go through injected
|
|
7
|
+
// appliers, so the core runs (and is tested) under bare node.
|
|
8
|
+
// attachObjectives(world) — the binder: wires the core to a World through its PUBLIC
|
|
9
|
+
// verbs only (grow / setTransform / morph / fade / cam). world.ts
|
|
10
|
+
// is untouched by this layer.
|
|
11
|
+
//
|
|
12
|
+
// The meter's lineage (documented, not mystified): it is the regime measure from the
|
|
13
|
+
// objective-movement work (science/walk/station-23) carried into engineering — movement
|
|
14
|
+
// is a LEVEL sustained above a settled FLOOR, not a discrete event. `driving` names the
|
|
15
|
+
// elevated channels — the movement you can name; `settled` is the sustained fall to the
|
|
16
|
+
// floor: the turn you cannot name is the stop.
|
|
17
|
+
import { v3, clamp } from "./math.js";
|
|
18
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
19
|
+
const lerpV3 = (a, b, t) => v3(lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t));
|
|
20
|
+
const asV3 = (s, d) => s == null ? v3(d, d, d) : typeof s === "number" ? v3(s, s, s) : s;
|
|
21
|
+
function lerpSpec(a, b, t) {
|
|
22
|
+
return {
|
|
23
|
+
translate: lerpV3(a.translate ?? v3(), b.translate ?? v3(), t),
|
|
24
|
+
rotateX: lerp(a.rotateX ?? 0, b.rotateX ?? 0, t),
|
|
25
|
+
rotateY: lerp(a.rotateY ?? 0, b.rotateY ?? 0, t),
|
|
26
|
+
rotateZ: lerp(a.rotateZ ?? 0, b.rotateZ ?? 0, t),
|
|
27
|
+
scale: lerpV3(asV3(a.scale, 1), asV3(b.scale, 1), t),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export class ObjectivesCore {
|
|
31
|
+
constructor(apply, opts = {}) {
|
|
32
|
+
this.apply = apply;
|
|
33
|
+
this.objs = new Map();
|
|
34
|
+
this.listeners = [];
|
|
35
|
+
this.floor = opts.floor ?? 0.02;
|
|
36
|
+
this.settleAfter = opts.settleAfter ?? 0.6;
|
|
37
|
+
}
|
|
38
|
+
/** Define (or wholly replace) a named objective. The drive starts on the next tick. */
|
|
39
|
+
define(name, spec, camNow) {
|
|
40
|
+
this.objs.set(name, {
|
|
41
|
+
name, spec,
|
|
42
|
+
k: 3 / Math.max(0.05, spec.pace ?? 3),
|
|
43
|
+
progress: 0, level: 1, atFloorFor: 0,
|
|
44
|
+
camFrom: spec.camera ? (camNow ?? {}) : null,
|
|
45
|
+
reading: { name, progress: 0, level: 1, floor: this.floor, moving: true, settled: false, driving: [] },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Move a live objective's goal — progress restarts FROM THE CURRENT STATE (no snap)
|
|
49
|
+
* and the meter lights up again. Channels absent from the patch keep their goal. */
|
|
50
|
+
retarget(name, patch, camNow) {
|
|
51
|
+
const o = this.objs.get(name);
|
|
52
|
+
if (!o)
|
|
53
|
+
throw new Error(`generative-engine objectives: retarget of unknown objective "${name}"`);
|
|
54
|
+
const s = o.spec, p = o.progress;
|
|
55
|
+
const next = { ...s, ...patch };
|
|
56
|
+
// continuity: each patched channel's new `from` is the value currently applied
|
|
57
|
+
if (patch.transform && s.transform)
|
|
58
|
+
next.transform = { ...patch.transform, from: lerpSpec(s.transform.from ?? {}, s.transform.to, p) };
|
|
59
|
+
if (patch.morph && s.morph)
|
|
60
|
+
next.morph = { ...patch.morph, from: lerp(s.morph.from ?? 0, s.morph.to, p) };
|
|
61
|
+
if (patch.fade && s.fade)
|
|
62
|
+
next.fade = { ...patch.fade, from: lerp(s.fade.from ?? 1, s.fade.to, p) };
|
|
63
|
+
const camFrom = patch.camera ? (camNow ?? o.camFrom ?? {}) : o.camFrom;
|
|
64
|
+
this.objs.set(name, {
|
|
65
|
+
name, spec: next,
|
|
66
|
+
k: 3 / Math.max(0.05, next.pace ?? 3),
|
|
67
|
+
progress: 0, level: 1, atFloorFor: 0,
|
|
68
|
+
camFrom,
|
|
69
|
+
reading: o.reading,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Forget an objective — its channels stay wherever they were last driven to. */
|
|
73
|
+
remove(name) { this.objs.delete(name); }
|
|
74
|
+
/** Subscribe to per-tick readings (one call per live objective per tick). */
|
|
75
|
+
onReading(cb) {
|
|
76
|
+
this.listeners.push(cb);
|
|
77
|
+
return () => { const i = this.listeners.indexOf(cb); if (i >= 0)
|
|
78
|
+
this.listeners.splice(i, 1); };
|
|
79
|
+
}
|
|
80
|
+
/** Pull the latest readings — the same objects the callbacks saw. */
|
|
81
|
+
readings() {
|
|
82
|
+
return [...this.objs.values()].map(o => o.reading);
|
|
83
|
+
}
|
|
84
|
+
/** Advance every objective by dt seconds and apply the driven values.
|
|
85
|
+
* dt <= 0 (settle() stills) re-applies values without advancing the meter. */
|
|
86
|
+
tick(dt) {
|
|
87
|
+
for (const o of this.objs.values()) {
|
|
88
|
+
if (dt > 0) {
|
|
89
|
+
o.progress += (1 - o.progress) * Math.min(1, dt * o.k);
|
|
90
|
+
// the level is the drive still outstanding — ≈1 at a fresh goal, easing to 0.
|
|
91
|
+
// A retarget resets progress, so the level re-elevates: movement is a REGIME.
|
|
92
|
+
o.level = 1 - o.progress;
|
|
93
|
+
if (o.level <= this.floor)
|
|
94
|
+
o.atFloorFor += dt;
|
|
95
|
+
else
|
|
96
|
+
o.atFloorFor = 0;
|
|
97
|
+
}
|
|
98
|
+
const p = o.progress, s = o.spec;
|
|
99
|
+
const driving = [];
|
|
100
|
+
const moving = o.level > this.floor;
|
|
101
|
+
if (s.transform) {
|
|
102
|
+
this.apply.transform(s.transform.group, lerpSpec(s.transform.from ?? {}, s.transform.to, p));
|
|
103
|
+
if (moving)
|
|
104
|
+
driving.push(`transform:${s.transform.group}`);
|
|
105
|
+
}
|
|
106
|
+
if (s.morph) {
|
|
107
|
+
this.apply.morph(s.morph.group, clamp(lerp(s.morph.from ?? 0, s.morph.to, p), 0, 1));
|
|
108
|
+
if (moving)
|
|
109
|
+
driving.push(`morph:${s.morph.group}`);
|
|
110
|
+
}
|
|
111
|
+
if (s.fade) {
|
|
112
|
+
this.apply.fade(s.fade.group, clamp(lerp(s.fade.from ?? 1, s.fade.to, p), 0, 1));
|
|
113
|
+
if (moving)
|
|
114
|
+
driving.push(`fade:${s.fade.group}`);
|
|
115
|
+
}
|
|
116
|
+
if (s.camera && o.camFrom) {
|
|
117
|
+
const c = s.camera, f = o.camFrom, out = {};
|
|
118
|
+
if (c.target)
|
|
119
|
+
out.target = lerpV3(f.target ?? c.target, c.target, p);
|
|
120
|
+
if (c.dist != null)
|
|
121
|
+
out.dist = lerp(f.dist ?? c.dist, c.dist, p);
|
|
122
|
+
if (c.pitch != null)
|
|
123
|
+
out.pitch = lerp(f.pitch ?? c.pitch, c.pitch, p);
|
|
124
|
+
if (c.yaw != null)
|
|
125
|
+
out.yaw = lerp(f.yaw ?? c.yaw, c.yaw, p);
|
|
126
|
+
this.apply.camera(out);
|
|
127
|
+
if (moving)
|
|
128
|
+
driving.push("camera");
|
|
129
|
+
}
|
|
130
|
+
if (s.custom) {
|
|
131
|
+
s.custom(p, Math.max(0, dt));
|
|
132
|
+
if (moving)
|
|
133
|
+
driving.push("custom");
|
|
134
|
+
}
|
|
135
|
+
o.reading = {
|
|
136
|
+
name: o.name, progress: p, level: o.level, floor: this.floor,
|
|
137
|
+
moving, settled: !moving && o.atFloorFor >= this.settleAfter, driving,
|
|
138
|
+
};
|
|
139
|
+
for (const cb of this.listeners)
|
|
140
|
+
cb(o.reading);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Attach the objectives layer to a World. Rides the frame loop as a grow() hook and
|
|
145
|
+
* drives the World through its public verbs — the renderer is untouched. */
|
|
146
|
+
export function attachObjectives(world, opts = {}) {
|
|
147
|
+
const core = new ObjectivesCore({
|
|
148
|
+
transform: (g, spec) => world.setTransform(g, spec),
|
|
149
|
+
morph: (g, w) => world.morph(g, w),
|
|
150
|
+
fade: (g, a) => world.fade(g, a),
|
|
151
|
+
camera: (c) => {
|
|
152
|
+
if (c.target)
|
|
153
|
+
world.cam.target = c.target;
|
|
154
|
+
if (c.dist != null)
|
|
155
|
+
world.cam.distT = c.dist;
|
|
156
|
+
if (c.pitch != null)
|
|
157
|
+
world.cam.pitch = c.pitch;
|
|
158
|
+
if (c.yaw != null)
|
|
159
|
+
world.cam.yawBase = c.yaw;
|
|
160
|
+
},
|
|
161
|
+
}, opts);
|
|
162
|
+
const camNow = () => ({
|
|
163
|
+
target: world.cam.target, dist: world.cam.distT,
|
|
164
|
+
pitch: world.cam.pitch, yaw: world.cam.yawBase,
|
|
165
|
+
});
|
|
166
|
+
world.grow((_t, dt) => core.tick(dt));
|
|
167
|
+
return {
|
|
168
|
+
define: (name, spec) => core.define(name, spec, camNow()),
|
|
169
|
+
retarget: (name, patch) => core.retarget(name, patch, camNow()),
|
|
170
|
+
remove: (name) => core.remove(name),
|
|
171
|
+
onReading: (cb) => core.onReading(cb),
|
|
172
|
+
readings: () => core.readings(),
|
|
173
|
+
};
|
|
174
|
+
}
|
package/dist/post.d.ts
CHANGED
|
@@ -23,6 +23,16 @@ export type PostParams = {
|
|
|
23
23
|
fxaa: boolean;
|
|
24
24
|
near: number;
|
|
25
25
|
far: number;
|
|
26
|
+
/** depth of field (W8): blur scaled by distance from the focus plane */
|
|
27
|
+
dofOn?: boolean;
|
|
28
|
+
dofFocus?: number;
|
|
29
|
+
dofRange?: number;
|
|
30
|
+
dofMaxBlur?: number;
|
|
31
|
+
/** camera motion blur (W8): depth reprojection into the previous frame */
|
|
32
|
+
moOn?: boolean;
|
|
33
|
+
moStrength?: number;
|
|
34
|
+
invVP?: Float32Array;
|
|
35
|
+
prevVP?: Float32Array;
|
|
26
36
|
};
|
|
27
37
|
export declare class PostChain {
|
|
28
38
|
readonly hdr: boolean;
|
|
@@ -33,6 +43,8 @@ export declare class PostChain {
|
|
|
33
43
|
private pAoBlur;
|
|
34
44
|
private pComposite;
|
|
35
45
|
private pFxaa;
|
|
46
|
+
private pDof;
|
|
47
|
+
private pMoblur;
|
|
36
48
|
private scene;
|
|
37
49
|
sceneDepth: WebGLTexture | null;
|
|
38
50
|
private bloomA;
|
|
@@ -40,6 +52,7 @@ export declare class PostChain {
|
|
|
40
52
|
private aoA;
|
|
41
53
|
private aoB;
|
|
42
54
|
private ldr;
|
|
55
|
+
private ldrB;
|
|
43
56
|
private W;
|
|
44
57
|
private H;
|
|
45
58
|
constructor(gl: WebGL2RenderingContext);
|
package/dist/post.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// texture; then bright-pass -> half-res separable gaussian (ping-pong) for
|
|
5
5
|
// bloom, depth-based half-res SSAO + blur, one composite pass (bloom add,
|
|
6
6
|
// ACES filmic, grade, vignette), and an optional FXAA pass last.
|
|
7
|
-
import { FST_VS, BRIGHT_FS, BLUR_FS, SSAO_FS, AOBLUR_FS, COMPOSITE_FS, FXAA_FS, } from "./shaders.js";
|
|
7
|
+
import { FST_VS, BRIGHT_FS, BLUR_FS, SSAO_FS, AOBLUR_FS, COMPOSITE_FS, FXAA_FS, DOF_FS, MOBLUR_FS, } from "./shaders.js";
|
|
8
8
|
export function makeProg(gl, vs, fs) {
|
|
9
9
|
const sh = (type, src) => {
|
|
10
10
|
const s = gl.createShader(type);
|
|
@@ -48,6 +48,7 @@ export class PostChain {
|
|
|
48
48
|
this.aoA = null;
|
|
49
49
|
this.aoB = null;
|
|
50
50
|
this.ldr = null;
|
|
51
|
+
this.ldrB = null;
|
|
51
52
|
this.W = 0;
|
|
52
53
|
this.H = 0;
|
|
53
54
|
this.gl = gl;
|
|
@@ -58,6 +59,8 @@ export class PostChain {
|
|
|
58
59
|
this.pAoBlur = new PInfo(gl, FST_VS, AOBLUR_FS);
|
|
59
60
|
this.pComposite = new PInfo(gl, FST_VS, COMPOSITE_FS);
|
|
60
61
|
this.pFxaa = new PInfo(gl, FST_VS, FXAA_FS);
|
|
62
|
+
this.pDof = new PInfo(gl, FST_VS, DOF_FS);
|
|
63
|
+
this.pMoblur = new PInfo(gl, FST_VS, MOBLUR_FS);
|
|
61
64
|
}
|
|
62
65
|
makeTarget(w, h, hdrFmt, filter) {
|
|
63
66
|
const gl = this.gl;
|
|
@@ -114,6 +117,7 @@ export class PostChain {
|
|
|
114
117
|
this.aoA = this.makeTarget(hw, hh, false, gl.LINEAR);
|
|
115
118
|
this.aoB = this.makeTarget(hw, hh, false, gl.LINEAR);
|
|
116
119
|
this.ldr = this.makeTarget(w, h, false, gl.LINEAR);
|
|
120
|
+
this.ldrB = this.makeTarget(w, h, false, gl.LINEAR);
|
|
117
121
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
118
122
|
}
|
|
119
123
|
/** Bind the scene target — the world's render pass draws into this. */
|
|
@@ -173,6 +177,9 @@ export class PostChain {
|
|
|
173
177
|
gl.uniform2f(this.pAoBlur.u("uPx"), 1 / this.aoA.w, 1 / this.aoA.h);
|
|
174
178
|
this.fst(this.pAoBlur, this.aoB);
|
|
175
179
|
}
|
|
180
|
+
// remaining LDR passes after composite — each renders src -> dst (null = the canvas)
|
|
181
|
+
const dofOn = !!o.dofOn, moOn = !!(o.moOn && o.invVP && o.prevVP);
|
|
182
|
+
const tail = (dofOn ? 1 : 0) + (moOn ? 1 : 0) + (o.fxaa ? 1 : 0);
|
|
176
183
|
this.pComposite.use();
|
|
177
184
|
this.bindTex(0, this.scene.tex);
|
|
178
185
|
this.bindTex(1, o.bloomOn ? this.bloomA.tex : null);
|
|
@@ -186,10 +193,42 @@ export class PostChain {
|
|
|
186
193
|
gl.uniform1f(this.pComposite.u("uGrade"), o.grade ? 1 : 0);
|
|
187
194
|
gl.uniform1f(this.pComposite.u("uVig"), o.vignette);
|
|
188
195
|
gl.uniform1f(this.pComposite.u("uInk"), o.ink ? 1 : 0);
|
|
189
|
-
|
|
196
|
+
let src = tail > 0 ? this.ldr : null;
|
|
197
|
+
this.fst(this.pComposite, src);
|
|
198
|
+
let left = tail;
|
|
199
|
+
const next = () => (--left > 0 ? (src === this.ldr ? this.ldrB : this.ldr) : null);
|
|
200
|
+
if (dofOn) {
|
|
201
|
+
const dst = next();
|
|
202
|
+
this.pDof.use();
|
|
203
|
+
this.bindTex(0, src.tex);
|
|
204
|
+
this.bindTex(1, this.sceneDepth);
|
|
205
|
+
gl.uniform1i(this.pDof.u("uTex"), 0);
|
|
206
|
+
gl.uniform1i(this.pDof.u("uDepth"), 1);
|
|
207
|
+
gl.uniform1f(this.pDof.u("uNear"), o.near);
|
|
208
|
+
gl.uniform1f(this.pDof.u("uFar"), o.far);
|
|
209
|
+
gl.uniform1f(this.pDof.u("uFocus"), o.dofFocus ?? 700);
|
|
210
|
+
gl.uniform1f(this.pDof.u("uRange"), o.dofRange ?? 420);
|
|
211
|
+
gl.uniform1f(this.pDof.u("uMaxBlur"), o.dofMaxBlur ?? 10);
|
|
212
|
+
gl.uniform2f(this.pDof.u("uPx"), 1 / this.W, 1 / this.H);
|
|
213
|
+
this.fst(this.pDof, dst);
|
|
214
|
+
src = dst;
|
|
215
|
+
}
|
|
216
|
+
if (moOn) {
|
|
217
|
+
const dst = next();
|
|
218
|
+
this.pMoblur.use();
|
|
219
|
+
this.bindTex(0, src.tex);
|
|
220
|
+
this.bindTex(1, this.sceneDepth);
|
|
221
|
+
gl.uniform1i(this.pMoblur.u("uTex"), 0);
|
|
222
|
+
gl.uniform1i(this.pMoblur.u("uDepth"), 1);
|
|
223
|
+
gl.uniformMatrix4fv(this.pMoblur.u("uInvVP"), false, o.invVP);
|
|
224
|
+
gl.uniformMatrix4fv(this.pMoblur.u("uPrevVP"), false, o.prevVP);
|
|
225
|
+
gl.uniform1f(this.pMoblur.u("uStrength"), o.moStrength ?? 0.6);
|
|
226
|
+
this.fst(this.pMoblur, dst);
|
|
227
|
+
src = dst;
|
|
228
|
+
}
|
|
190
229
|
if (o.fxaa) {
|
|
191
230
|
this.pFxaa.use();
|
|
192
|
-
this.bindTex(0,
|
|
231
|
+
this.bindTex(0, src.tex);
|
|
193
232
|
gl.uniform1i(this.pFxaa.u("uTex"), 0);
|
|
194
233
|
gl.uniform2f(this.pFxaa.u("uPx"), 1 / this.W, 1 / this.H);
|
|
195
234
|
this.fst(this.pFxaa, null);
|
|
@@ -205,9 +244,10 @@ export class PostChain {
|
|
|
205
244
|
this.drop(this.aoA);
|
|
206
245
|
this.drop(this.aoB);
|
|
207
246
|
this.drop(this.ldr);
|
|
247
|
+
this.drop(this.ldrB);
|
|
208
248
|
if (this.sceneDepth)
|
|
209
249
|
this.gl.deleteTexture(this.sceneDepth);
|
|
210
|
-
this.scene = this.bloomA = this.bloomB = this.aoA = this.aoB = this.ldr = null;
|
|
250
|
+
this.scene = this.bloomA = this.bloomB = this.aoA = this.aoB = this.ldr = this.ldrB = null;
|
|
211
251
|
this.sceneDepth = null;
|
|
212
252
|
}
|
|
213
253
|
}
|
package/dist/shaders.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const FACE_VS = "#version 300 es\nin vec3 aPos; in vec3 aNorm; in vec4 aCol; in float aBlood; in float aGrp;\nin vec3 aPos2; in vec3 aNorm2;\nuniform mat4 uVP; uniform mat4 uSVP;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nout vec3 vPos; out vec3 vNorm; out vec4 vCol; out float vZ; out vec4 vSh;\nout float vGA; out float vBlood;\nvoid main(){\n int g = int(aGrp + 0.5);\n float w = uMW[g];\n vec3 p = mix(aPos, aPos2, w), n = mix(aNorm, aNorm2, w);\n vec4 wp = uGM[g] * vec4(p, 1.0);\n vPos = wp.xyz; vNorm = uNM[g] * n; vCol = aCol; vGA = uGA[g]; vBlood = aBlood;\n vec4 cp = uVP * wp; vZ = cp.w; vSh = uSVP * wp; gl_Position = cp;\n}";
|
|
2
2
|
export declare const FACE_FS = "#version 300 es\nprecision highp float;\nin vec3 vPos; in vec3 vNorm; in vec4 vCol; in float vZ; in vec4 vSh;\nin float vGA; in float vBlood;\nuniform vec3 uLdir; uniform float uAmb; uniform float uDif; uniform float uSigned;\nuniform vec3 uLp[8]; uniform float uLi[8]; uniform float uLf[8]; uniform int uLn;\nuniform vec3 uCam; uniform float uFogK; uniform float uSpec; uniform vec3 uBg;\nuniform vec3 uSkyC; uniform vec3 uGroundC; uniform float uPulse;\nuniform float uShadowOn; uniform float uShadowSoft; uniform float uShadowBias;\nuniform lowp sampler2DShadow uShadow; uniform float uShadowPx;\nout vec4 fragColor;\nfloat h3(vec3 p){ return fract(sin(dot(p, vec3(12.9898, 78.233, 37.719))) * 43758.5453); }\nfloat vnoise(vec3 p){ vec3 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);\n return mix(mix(mix(h3(i), h3(i + vec3(1,0,0)), f.x), mix(h3(i + vec3(0,1,0)), h3(i + vec3(1,1,0)), f.x), f.y),\n mix(mix(h3(i + vec3(0,0,1)), h3(i + vec3(1,0,1)), f.x), mix(h3(i + vec3(0,1,1)), h3(i + vec3(1,1,1)), f.x), f.y), f.z); }\nfloat shadowFactor(){\n vec3 pr = vSh.xyz / vSh.w * 0.5 + 0.5;\n if (pr.x < 0.0 || pr.x > 1.0 || pr.y < 0.0 || pr.y > 1.0 || pr.z > 1.0) return 1.0;\n float ref = pr.z - uShadowBias;\n // 3x3 taps over a hardware-compared (LINEAR sampler2DShadow => free 2x2 PCF each) map\n float s = 0.0;\n for (int sy = -1; sy <= 1; sy++)\n for (int sx = -1; sx <= 1; sx++)\n s += texture(uShadow, vec3(pr.xy + vec2(float(sx), float(sy)) * uShadowPx * uShadowSoft, ref));\n s /= 9.0;\n return 0.35 + 0.65 * s;\n}\nvoid main(){\n vec3 n = normalize(vNorm);\n vec3 V = normalize(uCam - vPos);\n float skin = clamp(vBlood * 5.0, 0.0, 1.0);\n { // procedural micro-relief: perturb the shading normal by a fine noise gradient\n float bs = 5.5, e = 0.12, n0 = vnoise(vPos * bs);\n vec3 g = vec3(vnoise(vPos * bs + vec3(e, 0.0, 0.0)) - n0,\n vnoise(vPos * bs + vec3(0.0, e, 0.0)) - n0,\n vnoise(vPos * bs + vec3(0.0, 0.0, e)) - n0) / e;\n n = normalize(n - g * (0.10 + 0.30 * skin));\n }\n float d = dot(n, normalize(uLdir));\n float shade = uAmb + uDif * mix(abs(d), max(d, 0.0), uSigned);\n float spec = 0.0;\n float sh = uShadowOn > 0.5 ? shadowFactor() : 1.0;\n for (int i = 0; i < 8; i++) { if (i >= uLn) break;\n vec3 v = uLp[i] - vPos; float d2 = dot(v, v); vec3 L = v * inversesqrt(d2 + 1e-4);\n float lam = dot(n, L); lam = mix(abs(lam), max(lam, 0.0), uSigned);\n float fall = 1.0 / (1.0 + d2 / (uLf[i] * uLf[i]));\n float k = (i == 0) ? sh : 1.0;\n shade += uLi[i] * lam * fall * k;\n // GGX microfacet + Fresnel \u2014 roughness from the face's gloss (colour alpha slot)\n vec3 H = normalize(L + V);\n float nh = max(dot(n, H), 0.0);\n float rough = clamp(1.0 - vCol.a, 0.08, 1.0), a2 = rough * rough; a2 = a2 * a2;\n float dnm = nh * nh * (a2 - 1.0) + 1.0;\n float ggx = a2 / (3.14159 * dnm * dnm);\n float fres = 0.04 + 0.96 * pow(1.0 - max(dot(H, V), 0.0), 5.0);\n spec += uLi[i] * min(ggx, 6.0) * fres * fall * k * 0.16;\n }\n // coloured hemispheric ambient: cool from above, warm from below\n vec3 tint = mix(uGroundC, uSkyC, n.y * 0.5 + 0.5);\n vec3 lit = vCol.rgb * (uAmb * tint + vec3(shade - uAmb));\n // the perfusion term: blood near the surface changes how it meets the light\n float b = clamp(vBlood + uPulse * vBlood * 6.0, 0.0, 1.0);\n lit *= mix(vec3(1.0), vec3(1.07, 0.95, 0.94), b);\n lit += vec3(0.62, 0.06, 0.08) * (b * 0.20 * (1.0 - abs(d)));\n // pore noise, only where there is blood (skin, not cloth)\n float pore = vnoise(vPos * 2.4) * 0.6 + vnoise(vPos * 6.0) * 0.4;\n lit *= 1.0 + (pore - 0.5) * 0.09 * skin;\n // subsurface rim: grazing light bleeds through thin tissue, warm\n float fres = pow(1.0 - clamp(dot(n, V), 0.0, 1.0), 3.0);\n lit += vec3(0.55, 0.09, 0.10) * fres * skin * (0.28 + 0.5 * b);\n float fog = clamp(uFogK / vZ, 0.08, 1.0);\n vec3 outC = lit + vec3(1.0) * spec * uSpec * vCol.a;\n // fog and fades resolve toward the REAL background colour (the engine owns its ground)\n fragColor = vec4(mix(uBg, outC, fog * vGA), 1.0);\n}";
|
|
3
|
+
export declare const FACE_INST_VS = "#version 300 es\nin vec3 aPos; in vec3 aNorm; in vec4 aCol; in float aBlood; in float aGrp;\nin vec3 aPos2; in vec3 aNorm2;\nin vec4 aI0; in vec4 aI1; in vec4 aI2; in vec4 aI3;\nuniform mat4 uVP; uniform mat4 uSVP;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nout vec3 vPos; out vec3 vNorm; out vec4 vCol; out float vZ; out vec4 vSh;\nout float vGA; out float vBlood;\nvoid main(){\n int g = int(aGrp + 0.5);\n float w = uMW[g];\n mat4 inst = mat4(aI0, aI1, aI2, aI3);\n vec3 p = mix(aPos, aPos2, w), n = mix(aNorm, aNorm2, w);\n vec4 wp = uGM[g] * (inst * vec4(p, 1.0));\n vPos = wp.xyz; vNorm = uNM[g] * (mat3(inst) * n); vCol = aCol; vGA = uGA[g]; vBlood = aBlood;\n vec4 cp = uVP * wp; vZ = cp.w; vSh = uSVP * wp; gl_Position = cp;\n}";
|
|
4
|
+
export declare const DEPTH_INST_VS = "#version 300 es\nin vec3 aPos; in float aGrp; in vec3 aPos2;\nin vec4 aI0; in vec4 aI1; in vec4 aI2; in vec4 aI3;\nuniform mat4 uVP;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nvoid main(){\n int g = int(aGrp + 0.5);\n vec3 p = mix(aPos, aPos2, uMW[g]);\n gl_Position = uVP * (uGM[g] * (mat4(aI0, aI1, aI2, aI3) * vec4(p, 1.0)));\n}";
|
|
3
5
|
export declare const DEPTH_VS = "#version 300 es\nin vec3 aPos; in float aGrp; in vec3 aPos2;\nuniform mat4 uVP;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nvoid main(){\n int g = int(aGrp + 0.5);\n vec3 p = mix(aPos, aPos2, uMW[g]);\n gl_Position = uVP * (uGM[g] * vec4(p, 1.0));\n}";
|
|
4
6
|
export declare const DEPTH_FS = "#version 300 es\nprecision mediump float;\nvoid main(){}";
|
|
5
7
|
export declare const LINE_VS = "#version 300 es\nin vec3 aPos; in vec4 aCol; in float aGrp;\nuniform mat4 uVP;\nuniform mat4 uGM[8]; uniform mat3 uNM[8]; uniform float uMW[8]; uniform float uGA[8];\nout vec4 vCol; out float vZ; out float vGA;\nvoid main(){\n int g = int(aGrp + 0.5);\n vCol = aCol; vGA = uGA[g];\n vec4 p = uVP * (uGM[g] * vec4(aPos, 1.0)); vZ = p.w; gl_Position = p;\n}";
|
|
@@ -14,4 +16,6 @@ export declare const BLUR_FS = "#version 300 es\nprecision mediump float;\nin ve
|
|
|
14
16
|
export declare const SSAO_FS = "#version 300 es\nprecision highp float;\nin vec2 vUv; uniform sampler2D uDepth;\nuniform float uNear; uniform float uFar; uniform float uRadius; uniform vec2 uRes;\nout vec4 fragColor;\nfloat viewZ(vec2 uv){\n float d = texture(uDepth, uv).r * 2.0 - 1.0;\n return 2.0 * uNear * uFar / (uFar + uNear - d * (uFar - uNear));\n}\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }\nvoid main(){\n float z0 = viewZ(vUv);\n // screen-space sample radius shrinks with distance (a world-ish radius)\n float rad = uRadius / z0;\n float ang = hash(vUv * uRes) * 6.2831853;\n float occ = 0.0;\n for (int i = 0; i < 12; i++) {\n float fi = float(i);\n float a = ang + fi * 2.3999632; // golden-angle spiral\n float r = rad * (0.25 + 0.75 * fract(fi * 0.618034));\n vec2 uv = vUv + vec2(cos(a), sin(a)) * r;\n float dz = z0 - viewZ(uv); // positive => sample nearer camera => occluder\n occ += smoothstep(z0 * 0.002, z0 * 0.02, dz) * smoothstep(z0 * 0.35, 0.0, dz);\n }\n float ao = 1.0 - occ / 12.0;\n fragColor = vec4(vec3(ao), 1.0);\n}";
|
|
15
17
|
export declare const AOBLUR_FS = "#version 300 es\nprecision mediump float;\nin vec2 vUv; uniform sampler2D uTex; uniform vec2 uPx;\nout vec4 fragColor;\nvoid main(){\n float s = 0.0;\n for (int y = -1; y <= 2; y++)\n for (int x = -1; x <= 2; x++)\n s += texture(uTex, vUv + vec2(float(x) - 0.5, float(y) - 0.5) * uPx).r;\n fragColor = vec4(vec3(s / 16.0), 1.0);\n}";
|
|
16
18
|
export declare const COMPOSITE_FS = "#version 300 es\nprecision mediump float;\nin vec2 vUv;\nuniform sampler2D uScene; uniform sampler2D uBloom; uniform sampler2D uAO;\nuniform float uBloomStr; uniform float uAOStr;\nuniform float uFilm; uniform float uGrade; uniform float uVig; uniform float uInk;\nout vec4 fragColor;\nvoid main(){\n vec3 c = texture(uScene, vUv).rgb;\n c *= mix(1.0, texture(uAO, vUv).r, uAOStr);\n c += texture(uBloom, vUv).rgb * uBloomStr;\n if (uFilm > 0.5) { // ACES-ish filmic: highlights roll off instead of clipping\n c = (c * (2.51 * c + 0.03)) / (c * (2.43 * c + 0.59) + 0.14);\n }\n c = clamp(c, 0.0, 1.0);\n if (uGrade > 0.5) { // violet-cool shadows, warm highlights\n float luma = dot(c, vec3(0.299, 0.587, 0.114));\n c *= mix(vec3(0.85, 0.83, 1.07), vec3(1.07, 0.99, 0.92), smoothstep(0.12, 0.72, luma));\n c = clamp(c, 0.0, 1.0);\n }\n if (uVig > 0.0) { // cinematic vignette, in the composite now (off the overlay)\n vec2 q = vUv - vec2(0.5, 0.54);\n float r = smoothstep(0.35, 0.9, length(q * vec2(1.15, 1.0)));\n vec3 dk = uInk > 0.5 ? vec3(0.20, 0.19, 0.30) : vec3(0.024, 0.016, 0.055);\n c = mix(c, dk, uVig * r * (uInk > 0.5 ? 0.3 : 1.0));\n }\n fragColor = vec4(c, 1.0);\n}";
|
|
19
|
+
export declare const DOF_FS = "#version 300 es\nprecision highp float;\nin vec2 vUv; uniform sampler2D uTex; uniform sampler2D uDepth;\nuniform float uNear; uniform float uFar; uniform float uFocus; uniform float uRange;\nuniform float uMaxBlur; uniform vec2 uPx;\nout vec4 fragColor;\nfloat lin(float d){ return uNear * uFar / (uFar - d * (uFar - uNear)); }\nconst vec2 DISC[12] = vec2[](\n vec2(-0.326, -0.406), vec2(-0.840, -0.074), vec2(-0.696, 0.457), vec2(-0.203, 0.621),\n vec2(0.962, -0.195), vec2(0.473, -0.480), vec2(0.519, 0.767), vec2(0.185, -0.893),\n vec2(0.507, 0.064), vec2(0.896, 0.412), vec2(-0.322, -0.933), vec2(-0.792, -0.598));\nvoid main(){\n float z = lin(texture(uDepth, vUv).r);\n float coc = clamp(abs(z - uFocus) / max(1.0, uRange), 0.0, 1.0) * uMaxBlur;\n vec3 acc = texture(uTex, vUv).rgb;\n float wsum = 1.0;\n for (int i = 0; i < 12; i++) {\n vec2 off = DISC[i] * coc * uPx;\n // a sharp (in-focus) neighbour must not bleed into a blurred one: weight each tap\n // by ITS OWN CoC so foreground focus stays crisp against a soft background\n float zt = lin(texture(uDepth, vUv + off).r);\n float ct = clamp(abs(zt - uFocus) / max(1.0, uRange), 0.0, 1.0);\n float w = 0.25 + 0.75 * ct;\n acc += texture(uTex, vUv + off).rgb * w;\n wsum += w;\n }\n fragColor = vec4(acc / wsum, 1.0);\n}";
|
|
20
|
+
export declare const MOBLUR_FS = "#version 300 es\nprecision highp float;\nin vec2 vUv; uniform sampler2D uTex; uniform sampler2D uDepth;\nuniform mat4 uInvVP; uniform mat4 uPrevVP; uniform float uStrength;\nout vec4 fragColor;\nvoid main(){\n float d = texture(uDepth, vUv).r;\n vec4 ndc = vec4(vUv * 2.0 - 1.0, d * 2.0 - 1.0, 1.0);\n vec4 wp = uInvVP * ndc; wp /= wp.w;\n vec4 pc = uPrevVP * wp;\n vec2 prevUv = (pc.xy / pc.w) * 0.5 + 0.5;\n vec2 vel = (vUv - prevUv) * uStrength;\n float vlen = length(vel);\n if (vlen < 1e-5 || pc.w <= 0.0) { fragColor = vec4(texture(uTex, vUv).rgb, 1.0); return; }\n vel = vlen > 0.04 ? vel * (0.04 / vlen) : vel; // clamp the streak to 4% of the screen\n vec3 acc = vec3(0.0);\n for (int i = 0; i < 8; i++) {\n float t = (float(i) / 7.0) - 0.5;\n acc += texture(uTex, clamp(vUv + vel * t, vec2(0.001), vec2(0.999))).rgb;\n }\n fragColor = vec4(acc / 8.0, 1.0);\n}";
|
|
17
21
|
export declare const FXAA_FS = "#version 300 es\nprecision mediump float;\nin vec2 vUv; uniform sampler2D uTex; uniform vec2 uPx;\nout vec4 fragColor;\nfloat luma(vec3 c){ return dot(c, vec3(0.299, 0.587, 0.114)); }\nvoid main(){\n vec3 cM = texture(uTex, vUv).rgb;\n float lM = luma(cM);\n float lN = luma(texture(uTex, vUv + vec2(0.0, -uPx.y)).rgb);\n float lS = luma(texture(uTex, vUv + vec2(0.0, uPx.y)).rgb);\n float lW = luma(texture(uTex, vUv + vec2(-uPx.x, 0.0)).rgb);\n float lE = luma(texture(uTex, vUv + vec2(uPx.x, 0.0)).rgb);\n float lMin = min(lM, min(min(lN, lS), min(lW, lE)));\n float lMax = max(lM, max(max(lN, lS), max(lW, lE)));\n float range = lMax - lMin;\n if (range < max(0.0312, lMax * 0.125)) { fragColor = vec4(cM, 1.0); return; }\n vec2 dir = normalize(vec2((lE + lS) - (lW + lN), (lN + lE) - (lS + lW)) + 1e-6);\n vec3 a = texture(uTex, vUv + dir * uPx * 0.5).rgb;\n vec3 b = texture(uTex, vUv - dir * uPx * 0.5).rgb;\n fragColor = vec4(mix(cM, (a + b) * 0.5, 0.75), 1.0);\n}";
|
package/dist/shaders.js
CHANGED
|
@@ -97,6 +97,35 @@ void main(){
|
|
|
97
97
|
fragColor = vec4(mix(uBg, outC, fog * vGA), 1.0);
|
|
98
98
|
}`;
|
|
99
99
|
// depth-only pass (shadow map) — same morph + group transforms, no colour output
|
|
100
|
+
// instanced face VS (W8): the per-instance model matrix arrives as 4 vec4 attributes
|
|
101
|
+
// (divisor 1) and composes INSIDE the group transform — instances ride their group's
|
|
102
|
+
// fade / morph / transform like any other geometry. Normals use mat3(instance); with
|
|
103
|
+
// rotation + uniform scale that is exact (the normalize() in the FS absorbs scale).
|
|
104
|
+
export const FACE_INST_VS = `#version 300 es
|
|
105
|
+
in vec3 aPos; in vec3 aNorm; in vec4 aCol; in float aBlood; in float aGrp;
|
|
106
|
+
in vec3 aPos2; in vec3 aNorm2;
|
|
107
|
+
in vec4 aI0; in vec4 aI1; in vec4 aI2; in vec4 aI3;
|
|
108
|
+
uniform mat4 uVP; uniform mat4 uSVP;${GROUPS}
|
|
109
|
+
out vec3 vPos; out vec3 vNorm; out vec4 vCol; out float vZ; out vec4 vSh;
|
|
110
|
+
out float vGA; out float vBlood;
|
|
111
|
+
void main(){
|
|
112
|
+
int g = int(aGrp + 0.5);
|
|
113
|
+
float w = uMW[g];
|
|
114
|
+
mat4 inst = mat4(aI0, aI1, aI2, aI3);
|
|
115
|
+
vec3 p = mix(aPos, aPos2, w), n = mix(aNorm, aNorm2, w);
|
|
116
|
+
vec4 wp = uGM[g] * (inst * vec4(p, 1.0));
|
|
117
|
+
vPos = wp.xyz; vNorm = uNM[g] * (mat3(inst) * n); vCol = aCol; vGA = uGA[g]; vBlood = aBlood;
|
|
118
|
+
vec4 cp = uVP * wp; vZ = cp.w; vSh = uSVP * wp; gl_Position = cp;
|
|
119
|
+
}`;
|
|
120
|
+
export const DEPTH_INST_VS = `#version 300 es
|
|
121
|
+
in vec3 aPos; in float aGrp; in vec3 aPos2;
|
|
122
|
+
in vec4 aI0; in vec4 aI1; in vec4 aI2; in vec4 aI3;
|
|
123
|
+
uniform mat4 uVP;${GROUPS}
|
|
124
|
+
void main(){
|
|
125
|
+
int g = int(aGrp + 0.5);
|
|
126
|
+
vec3 p = mix(aPos, aPos2, uMW[g]);
|
|
127
|
+
gl_Position = uVP * (uGM[g] * (mat4(aI0, aI1, aI2, aI3) * vec4(p, 1.0)));
|
|
128
|
+
}`;
|
|
100
129
|
export const DEPTH_VS = `#version 300 es
|
|
101
130
|
in vec3 aPos; in float aGrp; in vec3 aPos2;
|
|
102
131
|
uniform mat4 uVP;${GROUPS}
|
|
@@ -283,6 +312,59 @@ void main(){
|
|
|
283
312
|
}
|
|
284
313
|
fragColor = vec4(c, 1.0);
|
|
285
314
|
}`;
|
|
315
|
+
// depth of field — CoC from linearized depth, 12-tap poisson disc gather on the LDR
|
|
316
|
+
export const DOF_FS = `#version 300 es
|
|
317
|
+
precision highp float;
|
|
318
|
+
in vec2 vUv; uniform sampler2D uTex; uniform sampler2D uDepth;
|
|
319
|
+
uniform float uNear; uniform float uFar; uniform float uFocus; uniform float uRange;
|
|
320
|
+
uniform float uMaxBlur; uniform vec2 uPx;
|
|
321
|
+
out vec4 fragColor;
|
|
322
|
+
float lin(float d){ return uNear * uFar / (uFar - d * (uFar - uNear)); }
|
|
323
|
+
const vec2 DISC[12] = vec2[](
|
|
324
|
+
vec2(-0.326, -0.406), vec2(-0.840, -0.074), vec2(-0.696, 0.457), vec2(-0.203, 0.621),
|
|
325
|
+
vec2(0.962, -0.195), vec2(0.473, -0.480), vec2(0.519, 0.767), vec2(0.185, -0.893),
|
|
326
|
+
vec2(0.507, 0.064), vec2(0.896, 0.412), vec2(-0.322, -0.933), vec2(-0.792, -0.598));
|
|
327
|
+
void main(){
|
|
328
|
+
float z = lin(texture(uDepth, vUv).r);
|
|
329
|
+
float coc = clamp(abs(z - uFocus) / max(1.0, uRange), 0.0, 1.0) * uMaxBlur;
|
|
330
|
+
vec3 acc = texture(uTex, vUv).rgb;
|
|
331
|
+
float wsum = 1.0;
|
|
332
|
+
for (int i = 0; i < 12; i++) {
|
|
333
|
+
vec2 off = DISC[i] * coc * uPx;
|
|
334
|
+
// a sharp (in-focus) neighbour must not bleed into a blurred one: weight each tap
|
|
335
|
+
// by ITS OWN CoC so foreground focus stays crisp against a soft background
|
|
336
|
+
float zt = lin(texture(uDepth, vUv + off).r);
|
|
337
|
+
float ct = clamp(abs(zt - uFocus) / max(1.0, uRange), 0.0, 1.0);
|
|
338
|
+
float w = 0.25 + 0.75 * ct;
|
|
339
|
+
acc += texture(uTex, vUv + off).rgb * w;
|
|
340
|
+
wsum += w;
|
|
341
|
+
}
|
|
342
|
+
fragColor = vec4(acc / wsum, 1.0);
|
|
343
|
+
}`;
|
|
344
|
+
// camera motion blur — reconstruct world position from depth, reproject into the
|
|
345
|
+
// previous frame's clip space, sample along the screen-space velocity
|
|
346
|
+
export const MOBLUR_FS = `#version 300 es
|
|
347
|
+
precision highp float;
|
|
348
|
+
in vec2 vUv; uniform sampler2D uTex; uniform sampler2D uDepth;
|
|
349
|
+
uniform mat4 uInvVP; uniform mat4 uPrevVP; uniform float uStrength;
|
|
350
|
+
out vec4 fragColor;
|
|
351
|
+
void main(){
|
|
352
|
+
float d = texture(uDepth, vUv).r;
|
|
353
|
+
vec4 ndc = vec4(vUv * 2.0 - 1.0, d * 2.0 - 1.0, 1.0);
|
|
354
|
+
vec4 wp = uInvVP * ndc; wp /= wp.w;
|
|
355
|
+
vec4 pc = uPrevVP * wp;
|
|
356
|
+
vec2 prevUv = (pc.xy / pc.w) * 0.5 + 0.5;
|
|
357
|
+
vec2 vel = (vUv - prevUv) * uStrength;
|
|
358
|
+
float vlen = length(vel);
|
|
359
|
+
if (vlen < 1e-5 || pc.w <= 0.0) { fragColor = vec4(texture(uTex, vUv).rgb, 1.0); return; }
|
|
360
|
+
vel = vlen > 0.04 ? vel * (0.04 / vlen) : vel; // clamp the streak to 4% of the screen
|
|
361
|
+
vec3 acc = vec3(0.0);
|
|
362
|
+
for (int i = 0; i < 8; i++) {
|
|
363
|
+
float t = (float(i) / 7.0) - 0.5;
|
|
364
|
+
acc += texture(uTex, clamp(vUv + vel * t, vec2(0.001), vec2(0.999))).rgb;
|
|
365
|
+
}
|
|
366
|
+
fragColor = vec4(acc / 8.0, 1.0);
|
|
367
|
+
}`;
|
|
286
368
|
// compact FXAA (luma-based, 5-tap edge blend) — runs last, on the LDR composite
|
|
287
369
|
export const FXAA_FS = `#version 300 es
|
|
288
370
|
precision mediump float;
|
package/dist/world.d.ts
CHANGED
|
@@ -88,6 +88,9 @@ export declare class World {
|
|
|
88
88
|
private pPt;
|
|
89
89
|
private pRib;
|
|
90
90
|
private pDepth;
|
|
91
|
+
private pFaceI;
|
|
92
|
+
private pDepthI;
|
|
93
|
+
private instanced;
|
|
91
94
|
private post;
|
|
92
95
|
private ink;
|
|
93
96
|
private bg;
|
|
@@ -171,6 +174,20 @@ export declare class World {
|
|
|
171
174
|
};
|
|
172
175
|
/** FXAA on the final composite */
|
|
173
176
|
fxaa: boolean;
|
|
177
|
+
/** depth of field (W8) — blur grows with distance from the focus plane (world units) */
|
|
178
|
+
dof: {
|
|
179
|
+
on: boolean;
|
|
180
|
+
focus: number;
|
|
181
|
+
range: number;
|
|
182
|
+
maxBlur: number;
|
|
183
|
+
};
|
|
184
|
+
/** camera motion blur (W8) — depth reprojection into the previous frame's view */
|
|
185
|
+
motionBlur: {
|
|
186
|
+
on: boolean;
|
|
187
|
+
strength: number;
|
|
188
|
+
};
|
|
189
|
+
private prevVP;
|
|
190
|
+
private prevVPValid;
|
|
174
191
|
private groupIdx;
|
|
175
192
|
private groupAlpha;
|
|
176
193
|
private groupTarget;
|
|
@@ -223,6 +240,10 @@ export declare class World {
|
|
|
223
240
|
private pushRibbon;
|
|
224
241
|
/** A static ribbon stroke — real pixel width, feathered edges, depth-tested. */
|
|
225
242
|
ribbon(pts: V3[], o: RibbonOpts): void;
|
|
243
|
+
/** W8 — GPU instancing: upload a Mesh ONCE and draw it at every given transform in a
|
|
244
|
+
* single instanced call (scene + shadow pass alike). A forest is one draw. Instances
|
|
245
|
+
* ride their group's fade / morph / transform like any other geometry. */
|
|
246
|
+
meshInstanced(geo: Mesh, instances: (M4 | TransformSpec)[], o: FaceOpts): void;
|
|
226
247
|
grow(fn: GrowFn): void;
|
|
227
248
|
drawLine(a: V3, b: V3, color: string, alpha: number, width?: number): void;
|
|
228
249
|
drawPath(pts: {
|
|
@@ -250,6 +271,10 @@ export declare class World {
|
|
|
250
271
|
private drawLines;
|
|
251
272
|
private drawPts;
|
|
252
273
|
private drawRibbons;
|
|
274
|
+
/** Instanced records under a given program (scene or depth pass). Divisors are reset
|
|
275
|
+
* after each draw — attribute divisor is global-per-index state and would otherwise
|
|
276
|
+
* poison the non-instanced draws that share those indices. */
|
|
277
|
+
private drawInstancedAll;
|
|
253
278
|
private renderFrame;
|
|
254
279
|
private frameFn;
|
|
255
280
|
private pausedByUser;
|
package/dist/world.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// WebGL2 renderer: HDR scene target + post chain (bloom, SSAO, composite, FXAA),
|
|
4
4
|
// hardware-PCF depth-texture shadows, per-group transforms + morph targets in the
|
|
5
5
|
// vertex stage, mesh + ribbon primitives. Zero third-party imports, on purpose.
|
|
6
|
-
import { v3, add, sub, scale, cross, norm, clamp, hsl, mIdent, mPerspective, mLookAt, mMul, mCompose, normalMat3, } from "./math.js";
|
|
7
|
-
import { FACE_VS, FACE_FS, DEPTH_VS, DEPTH_FS, LINE_VS, LINE_FS, PT_VS, PT_FS, RIBBON_VS, RIBBON_FS, } from "./shaders.js";
|
|
6
|
+
import { v3, add, sub, scale, cross, norm, clamp, hsl, mIdent, mPerspective, mLookAt, mMul, mCompose, mInvert, normalMat3, } from "./math.js";
|
|
7
|
+
import { FACE_VS, FACE_FS, DEPTH_VS, DEPTH_FS, LINE_VS, LINE_FS, PT_VS, PT_FS, RIBBON_VS, RIBBON_FS, FACE_INST_VS, DEPTH_INST_VS, } from "./shaders.js";
|
|
8
8
|
import { PInfo, PostChain } from "./post.js";
|
|
9
9
|
const FACE_STRIDE = 18; // pos3 norm3 rgb3 gloss1 blood1 grp1 pos2:3 norm2:3
|
|
10
10
|
const LINE_STRIDE = 8; // pos3 col4 grp1
|
|
@@ -33,6 +33,7 @@ export class World {
|
|
|
33
33
|
this.groupNM.set(normalMat3(mat), i * 9);
|
|
34
34
|
}
|
|
35
35
|
constructor(o) {
|
|
36
|
+
this.instanced = [];
|
|
36
37
|
this.ink = false;
|
|
37
38
|
this.bg = [0.024, 0.016, 0.055];
|
|
38
39
|
this.transparent = false;
|
|
@@ -84,6 +85,12 @@ export class World {
|
|
|
84
85
|
this.ssao = { on: true, strength: 0.55, radius: 22 };
|
|
85
86
|
/** FXAA on the final composite */
|
|
86
87
|
this.fxaa = true;
|
|
88
|
+
/** depth of field (W8) — blur grows with distance from the focus plane (world units) */
|
|
89
|
+
this.dof = { on: false, focus: 700, range: 420, maxBlur: 10 };
|
|
90
|
+
/** camera motion blur (W8) — depth reprojection into the previous frame's view */
|
|
91
|
+
this.motionBlur = { on: false, strength: 0.6 };
|
|
92
|
+
this.prevVP = mIdent();
|
|
93
|
+
this.prevVPValid = false;
|
|
87
94
|
// groups: fade alpha + morph weight + transform, resolved in the vertex stage
|
|
88
95
|
this.groupIdx = new Map([["default", 0]]);
|
|
89
96
|
this.groupAlpha = new Float32Array(8).fill(1);
|
|
@@ -156,6 +163,8 @@ export class World {
|
|
|
156
163
|
this.pPt = new PInfo(gl, PT_VS, PT_FS);
|
|
157
164
|
this.pRib = new PInfo(gl, RIBBON_VS, RIBBON_FS);
|
|
158
165
|
this.pDepth = new PInfo(gl, DEPTH_VS, DEPTH_FS);
|
|
166
|
+
this.pFaceI = new PInfo(gl, FACE_INST_VS, FACE_FS);
|
|
167
|
+
this.pDepthI = new PInfo(gl, DEPTH_INST_VS, DEPTH_FS);
|
|
159
168
|
this.post = (o.post ?? true) && !this.transparent ? new PostChain(gl) : null;
|
|
160
169
|
this.shadowsOn = !!o.shadows;
|
|
161
170
|
if (o.shadows && typeof o.shadows === "object") {
|
|
@@ -371,6 +380,28 @@ export class World {
|
|
|
371
380
|
}
|
|
372
381
|
/** A static ribbon stroke — real pixel width, feathered edges, depth-tested. */
|
|
373
382
|
ribbon(pts, o) { this.pushRibbon(this.sRib, pts, o); this.staticDirty = true; }
|
|
383
|
+
/** W8 — GPU instancing: upload a Mesh ONCE and draw it at every given transform in a
|
|
384
|
+
* single instanced call (scene + shadow pass alike). A forest is one draw. Instances
|
|
385
|
+
* ride their group's fade / morph / transform like any other geometry. */
|
|
386
|
+
meshInstanced(geo, instances, o) {
|
|
387
|
+
if (!instances.length)
|
|
388
|
+
return;
|
|
389
|
+
const gl = this.gl;
|
|
390
|
+
const store = [];
|
|
391
|
+
const P = geo.pos, N = geo.nrm;
|
|
392
|
+
for (let i = 0; i < P.length; i += 9) {
|
|
393
|
+
this.pushFace(store, v3(P[i], P[i + 1], P[i + 2]), v3(P[i + 3], P[i + 4], P[i + 5]), v3(P[i + 6], P[i + 7], P[i + 8]), o, v3(N[i], N[i + 1], N[i + 2]), v3(N[i + 3], N[i + 4], N[i + 5]), v3(N[i + 6], N[i + 7], N[i + 8]));
|
|
394
|
+
}
|
|
395
|
+
const mats = new Float32Array(instances.length * 16);
|
|
396
|
+
instances.forEach((m, i) => mats.set(m instanceof Float32Array ? m : mCompose(m), i * 16));
|
|
397
|
+
const vbuf = gl.createBuffer();
|
|
398
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
|
|
399
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(store), gl.STATIC_DRAW);
|
|
400
|
+
const ibuf = gl.createBuffer();
|
|
401
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, ibuf);
|
|
402
|
+
gl.bufferData(gl.ARRAY_BUFFER, mats, gl.STATIC_DRAW);
|
|
403
|
+
this.instanced.push({ vbuf, ibuf, verts: store.length / FACE_STRIDE, count: instances.length });
|
|
404
|
+
}
|
|
374
405
|
grow(fn) { this.hooks.push(fn); }
|
|
375
406
|
// ── the living verbs (gathered per frame) ──
|
|
376
407
|
drawLine(a, b, color, alpha, width = 1) {
|
|
@@ -563,11 +594,53 @@ export class World {
|
|
|
563
594
|
this.attrib(this.pRib, "aGrp", 1, RIB_STRIDE, 15);
|
|
564
595
|
gl.drawArrays(gl.TRIANGLES, 0, verts);
|
|
565
596
|
}
|
|
597
|
+
/** Instanced records under a given program (scene or depth pass). Divisors are reset
|
|
598
|
+
* after each draw — attribute divisor is global-per-index state and would otherwise
|
|
599
|
+
* poison the non-instanced draws that share those indices. */
|
|
600
|
+
drawInstancedAll(p, depthOnly) {
|
|
601
|
+
if (!this.instanced.length)
|
|
602
|
+
return;
|
|
603
|
+
const gl = this.gl;
|
|
604
|
+
for (const rec of this.instanced) {
|
|
605
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, rec.vbuf);
|
|
606
|
+
this.attrib(p, "aPos", 3, FACE_STRIDE, 0);
|
|
607
|
+
if (!depthOnly) {
|
|
608
|
+
this.attrib(p, "aNorm", 3, FACE_STRIDE, 3);
|
|
609
|
+
this.attrib(p, "aCol", 4, FACE_STRIDE, 6);
|
|
610
|
+
this.attrib(p, "aBlood", 1, FACE_STRIDE, 10);
|
|
611
|
+
}
|
|
612
|
+
this.attrib(p, "aGrp", 1, FACE_STRIDE, 11);
|
|
613
|
+
this.attrib(p, "aPos2", 3, FACE_STRIDE, 12);
|
|
614
|
+
if (!depthOnly)
|
|
615
|
+
this.attrib(p, "aNorm2", 3, FACE_STRIDE, 15);
|
|
616
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, rec.ibuf);
|
|
617
|
+
const locs = [];
|
|
618
|
+
for (let k = 0; k < 4; k++) {
|
|
619
|
+
const loc = gl.getAttribLocation(p.prog, "aI" + k);
|
|
620
|
+
if (loc < 0)
|
|
621
|
+
continue;
|
|
622
|
+
gl.enableVertexAttribArray(loc);
|
|
623
|
+
gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, 64, k * 16);
|
|
624
|
+
gl.vertexAttribDivisor(loc, 1);
|
|
625
|
+
locs.push(loc);
|
|
626
|
+
}
|
|
627
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, rec.verts, rec.count);
|
|
628
|
+
for (const loc of locs)
|
|
629
|
+
gl.vertexAttribDivisor(loc, 0);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
566
632
|
// ── the frame ──
|
|
567
633
|
renderFrame(t, dt) {
|
|
568
634
|
const gl = this.gl;
|
|
569
635
|
this.lastT = t;
|
|
636
|
+
if (this.motionBlur.on && this.prevVPValid)
|
|
637
|
+
this.prevVP = new Float32Array(this.vp);
|
|
570
638
|
this.setupCamera(t, dt);
|
|
639
|
+
if (this.motionBlur.on && !this.prevVPValid) {
|
|
640
|
+
// first blurred frame: previous = current, so velocity is zero (no identity streak)
|
|
641
|
+
this.prevVP = new Float32Array(this.vp);
|
|
642
|
+
this.prevVPValid = true;
|
|
643
|
+
}
|
|
571
644
|
this.fLight.length = 0;
|
|
572
645
|
this.fRib.length = 0;
|
|
573
646
|
this.dFaceA.length = 0;
|
|
@@ -640,6 +713,12 @@ export class World {
|
|
|
640
713
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.bFaceD);
|
|
641
714
|
this.depthDraw(this.bFaceD, this.dFaceA.length / FACE_STRIDE);
|
|
642
715
|
}
|
|
716
|
+
if (this.instanced.length) {
|
|
717
|
+
this.pDepthI.use();
|
|
718
|
+
gl.uniformMatrix4fv(this.pDepthI.u("uVP"), false, this.svp);
|
|
719
|
+
this.setGroups(this.pDepthI);
|
|
720
|
+
this.drawInstancedAll(this.pDepthI, true);
|
|
721
|
+
}
|
|
643
722
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
644
723
|
}
|
|
645
724
|
// the scene pass — into the HDR target when the post chain is on
|
|
@@ -664,6 +743,12 @@ export class World {
|
|
|
664
743
|
this.drawFaces(this.bFace, this.sFace.length / FACE_STRIDE, null);
|
|
665
744
|
if (this.dFaceA.length)
|
|
666
745
|
this.drawFaces(this.bFaceD, this.dFaceA.length / FACE_STRIDE, new Float32Array(this.dFaceA));
|
|
746
|
+
if (this.instanced.length) {
|
|
747
|
+
this.pFaceI.use();
|
|
748
|
+
this.setVP(this.pFaceI);
|
|
749
|
+
this.setLights(this.pFaceI, fogK);
|
|
750
|
+
this.drawInstancedAll(this.pFaceI, false);
|
|
751
|
+
}
|
|
667
752
|
// the light — additive, depth-TESTED but not written (ink: normal alpha blend)
|
|
668
753
|
gl.depthMask(false);
|
|
669
754
|
gl.enable(gl.BLEND);
|
|
@@ -701,6 +786,9 @@ export class World {
|
|
|
701
786
|
ssaoOn: this.ssao.on, ssaoStrength: this.ssao.strength, ssaoRadius: this.ssao.radius,
|
|
702
787
|
filmic: this.filmic && !this.ink, grade: this.grade, vignette: this.vignette, ink: this.ink,
|
|
703
788
|
fxaa: this.fxaa, near: this.zNear, far: this.zFar,
|
|
789
|
+
dofOn: this.dof.on, dofFocus: this.dof.focus, dofRange: this.dof.range, dofMaxBlur: this.dof.maxBlur,
|
|
790
|
+
moOn: this.motionBlur.on, moStrength: this.motionBlur.strength,
|
|
791
|
+
invVP: this.motionBlur.on ? mInvert(this.vp) : undefined, prevVP: this.prevVP,
|
|
704
792
|
});
|
|
705
793
|
}
|
|
706
794
|
// labels — 2D overlay, never occluded (the honest exception)
|
|
@@ -784,6 +872,11 @@ export class World {
|
|
|
784
872
|
c();
|
|
785
873
|
this.cleanup = [];
|
|
786
874
|
this.overlay.remove();
|
|
875
|
+
for (const rec of this.instanced) {
|
|
876
|
+
this.gl.deleteBuffer(rec.vbuf);
|
|
877
|
+
this.gl.deleteBuffer(rec.ibuf);
|
|
878
|
+
}
|
|
879
|
+
this.instanced = [];
|
|
787
880
|
this.post?.dispose();
|
|
788
881
|
if (releaseContext)
|
|
789
882
|
this.gl.getExtension("WEBGL_lose_context")?.loseContext();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chromatic-coherence/generative-engine",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "THE THIRD ENGINE — a zero-dependency WebGL2 graphics engine grown from generative-charts-3d: HDR + bloom + SSAO post chain, hardware-PCF shadow maps, a geometry stdlib, ribbon strokes, group transforms and morph targets. The charts API rides on top unchanged.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|