@drawcall/lod 0.1.0 → 0.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 +10 -69
- package/dist/at.d.ts +2 -0
- package/dist/at.js +8 -0
- package/dist/at.js.map +1 -0
- package/dist/baked/codec.d.ts +6 -3
- package/dist/baked/codec.js +22 -16
- package/dist/baked/codec.js.map +1 -1
- package/dist/baked/coverage.d.ts +3 -0
- package/dist/baked/coverage.js +45 -0
- package/dist/baked/coverage.js.map +1 -0
- package/dist/baked/index.d.ts +4 -0
- package/dist/baked/index.js +3 -35
- package/dist/baked/index.js.map +1 -1
- package/dist/baked/low.d.ts +2 -2
- package/dist/baked/low.js +4 -4
- package/dist/baked/low.js.map +1 -1
- package/dist/bounds.d.ts +12 -2
- package/dist/bounds.js +72 -10
- package/dist/bounds.js.map +1 -1
- package/dist/compile.d.ts +4 -0
- package/dist/compile.js +100 -0
- package/dist/compile.js.map +1 -0
- package/dist/deferred.d.ts +12 -0
- package/dist/deferred.js +27 -0
- package/dist/deferred.js.map +1 -0
- package/dist/dispose.d.ts +7 -3
- package/dist/dispose.js +33 -4
- package/dist/dispose.js.map +1 -1
- package/dist/fade.d.ts +5 -13
- package/dist/fade.js +62 -28
- package/dist/fade.js.map +1 -1
- package/dist/impostor/bake/index.d.ts +1 -0
- package/dist/impostor/bake/index.js +1 -0
- package/dist/impostor/bake/index.js.map +1 -1
- package/dist/impostor/bake/materials.js +11 -1
- package/dist/impostor/bake/materials.js.map +1 -1
- package/dist/impostor/codec.d.ts +2 -7
- package/dist/impostor/codec.js +16 -27
- package/dist/impostor/codec.js.map +1 -1
- package/dist/impostor/mesh/card.js +4 -1
- package/dist/impostor/mesh/card.js.map +1 -1
- package/dist/impostor/mesh/index.d.ts +6 -4
- package/dist/impostor/mesh/index.js +8 -10
- package/dist/impostor/mesh/index.js.map +1 -1
- package/dist/impostor/mesh/material.d.ts +7 -5
- package/dist/impostor/mesh/material.js +29 -26
- package/dist/impostor/mesh/material.js.map +1 -1
- package/dist/impostor/mesh/surface.d.ts +20 -16
- package/dist/impostor/mesh/surface.js +74 -55
- package/dist/impostor/mesh/surface.js.map +1 -1
- package/dist/impostor/octahedral.d.ts +1 -8
- package/dist/impostor/octahedral.js +3 -18
- package/dist/impostor/octahedral.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/instanced/args.js +2 -2
- package/dist/instanced/args.js.map +1 -1
- package/dist/instanced/{bounds.d.ts → blocks.d.ts} +25 -9
- package/dist/instanced/{bounds.js → blocks.js} +76 -53
- package/dist/instanced/blocks.js.map +1 -0
- package/dist/instanced/dirty.d.ts +3 -0
- package/dist/instanced/dirty.js +40 -5
- package/dist/instanced/dirty.js.map +1 -1
- package/dist/instanced/index.d.ts +25 -51
- package/dist/instanced/index.js +189 -413
- package/dist/instanced/index.js.map +1 -1
- package/dist/instanced/meshes.d.ts +54 -13
- package/dist/instanced/meshes.js +178 -41
- package/dist/instanced/meshes.js.map +1 -1
- package/dist/instanced/parts.d.ts +14 -0
- package/dist/instanced/parts.js +79 -0
- package/dist/instanced/parts.js.map +1 -0
- package/dist/instanced/raycast.d.ts +13 -0
- package/dist/instanced/raycast.js +53 -0
- package/dist/instanced/raycast.js.map +1 -0
- package/dist/instanced/select.d.ts +17 -9
- package/dist/instanced/select.js +114 -63
- package/dist/instanced/select.js.map +1 -1
- package/dist/instanced/store.d.ts +31 -0
- package/dist/instanced/store.js +96 -0
- package/dist/instanced/store.js.map +1 -0
- package/dist/levels.d.ts +71 -48
- package/dist/levels.js +99 -74
- package/dist/levels.js.map +1 -1
- package/dist/loader/bundle.d.ts +25 -0
- package/dist/loader/bundle.js +241 -0
- package/dist/loader/bundle.js.map +1 -0
- package/dist/loader/cache.d.ts +15 -15
- package/dist/loader/cache.js +71 -33
- package/dist/loader/cache.js.map +1 -1
- package/dist/loader/provenance.d.ts +30 -0
- package/dist/loader/provenance.js +105 -0
- package/dist/loader/provenance.js.map +1 -0
- package/dist/loader/url.d.ts +13 -0
- package/dist/loader/url.js +31 -0
- package/dist/loader/url.js.map +1 -0
- package/dist/loader.d.ts +30 -74
- package/dist/loader.js +191 -186
- package/dist/loader.js.map +1 -1
- package/dist/lod.d.ts +20 -31
- package/dist/lod.js +105 -96
- package/dist/lod.js.map +1 -1
- package/dist/material.js +9 -4
- package/dist/material.js.map +1 -1
- package/dist/mesh/codec.d.ts +4 -0
- package/dist/mesh/codec.js +192 -0
- package/dist/mesh/codec.js.map +1 -0
- package/dist/queue.d.ts +2 -0
- package/dist/queue.js +33 -0
- package/dist/queue.js.map +1 -0
- package/dist/residency.d.ts +44 -0
- package/dist/residency.js +205 -0
- package/dist/residency.js.map +1 -0
- package/dist/simplify.js +5 -3
- package/dist/simplify.js.map +1 -1
- package/dist/source/{error.d.ts → deviation.d.ts} +3 -3
- package/dist/source/{error.js → deviation.js} +6 -6
- package/dist/source/deviation.js.map +1 -0
- package/dist/source/source.d.ts +1 -1
- package/dist/source/source.js +1 -1
- package/dist/three.d.ts +17 -5
- package/dist/three.js +24 -3
- package/dist/three.js.map +1 -1
- package/dist/tsl.d.ts +5 -5
- package/dist/tsl.js +12 -19
- package/dist/tsl.js.map +1 -1
- package/dist/view.d.ts +16 -1
- package/dist/view.js +57 -1
- package/dist/view.js.map +1 -1
- package/dist/wind/index.d.ts +2 -2
- package/dist/wind/index.js +31 -28
- package/dist/wind/index.js.map +1 -1
- package/dist/wind/sway.d.ts +2 -1
- package/package.json +9 -3
- package/skills/lod/SKILL.md +71 -109
- package/dist/impostor/cache.d.ts +0 -15
- package/dist/impostor/cache.js +0 -44
- package/dist/impostor/cache.js.map +0 -1
- package/dist/instanced/bounds.js.map +0 -1
- package/dist/lazy.d.ts +0 -64
- package/dist/lazy.js +0 -187
- package/dist/lazy.js.map +0 -1
- package/dist/loader/dispose.d.ts +0 -3
- package/dist/loader/dispose.js +0 -25
- package/dist/loader/dispose.js.map +0 -1
- package/dist/source/error.js.map +0 -1
package/dist/instanced/index.js
CHANGED
|
@@ -1,54 +1,36 @@
|
|
|
1
|
-
import { Box3,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { impostorInstances } from "../impostor/mesh/index.js";
|
|
7
|
-
import { Level, cloneLevels, copyThresholds, estimate, fadeProgress, levelAt, tintLevels, validate } from "../levels.js";
|
|
1
|
+
import { Box3, Matrix4, Object3D, Sphere, Vector3 } from "three/webgpu";
|
|
2
|
+
import { at } from "../at.js";
|
|
3
|
+
import { levelBox, measureLevels, prepareBounds } from "../bounds.js";
|
|
4
|
+
import { disposeCopies } from "../dispose.js";
|
|
5
|
+
import { Level, cloneLevels, estimate, fadeProgress, levelAt, tintLevels, validate } from "../levels.js";
|
|
8
6
|
import { Lod } from "../lod.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { TYPICAL_HEIGHT, View, viewportHeight,
|
|
7
|
+
import { Residency } from "../residency.js";
|
|
8
|
+
import { inShadowPass, sameClass } from "../three.js";
|
|
9
|
+
import { TYPICAL_HEIGHT, View, viewportHeight, watchContext, unwatchContext, inView } from "../view.js";
|
|
10
|
+
import { compileLevel } from "../compile.js";
|
|
12
11
|
import { WIND_BOUNDS } from "../wind/hooks.js";
|
|
13
12
|
import { copyWind, refreshWind } from "../wind/index.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
13
|
+
import { Meshes } from "./meshes.js";
|
|
14
|
+
import { parts } from "./parts.js";
|
|
15
|
+
import { Blocks } from "./blocks.js";
|
|
16
|
+
import { Probes } from "./raycast.js";
|
|
17
|
+
import { ARGS_BYTES } from "./args.js";
|
|
18
|
+
import { Selection } from "./select.js";
|
|
19
|
+
import { Store } from "./store.js";
|
|
19
20
|
const view = new View();
|
|
20
21
|
const inverse = new Matrix4();
|
|
21
|
-
const ray = new Ray();
|
|
22
|
-
const sphere = new Sphere();
|
|
23
|
-
const instance = new Matrix4();
|
|
24
|
-
const world = new Matrix4();
|
|
25
|
-
const box = new Box3();
|
|
26
|
-
const hits = [];
|
|
27
22
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
const pending = new Set();
|
|
33
|
-
/**
|
|
34
|
-
* `count` copies of one object with levels of detail, each switched by its own height on
|
|
35
|
-
* screen: `Lod` for instances. The GPU sorts the instances into levels once per view
|
|
36
|
-
* (see select.ts) and every mesh of a level draws the instances it holds in one indirect
|
|
37
|
-
* draw; an instance near a switch is in both neighbours and they split its pixels with
|
|
38
|
-
* the dither. Thresholds, `forceLevel`, `Lod.quality`, `Lod.debug` and shadows work as
|
|
39
|
-
* for `Lod`; nothing of an instance is drawn into a directional light's shadow map whose
|
|
40
|
-
* texels are wider than it. Each level is culled by the crowd's bounds, tightened for
|
|
41
|
-
* finer levels to the region near the camera where their instances can draw.
|
|
23
|
+
* Independently selected copies of one object, batched into indirect draws per view.
|
|
24
|
+
* Thresholds, fades, global settings and shadows follow Lod. Each level's bounds tighten
|
|
25
|
+
* to the region where its instances can draw. Instance matrices may scale arbitrarily;
|
|
26
|
+
* the crowd's own transform and its parents should scale uniformly. Scale 0 draws nothing.
|
|
42
27
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* may scale any way.
|
|
28
|
+
* Level templates are cloned, then their meshes are instanced. Skinned meshes,
|
|
29
|
+
* InstancedMeshes (except ImpostorMesh) and nested Lods are unsupported. For external
|
|
30
|
+
* detail, leave level 0 empty and draw the instances that `levelOf` reports there.
|
|
47
31
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* ImpostorMesh) or nested Lods. To build a hierarchy on top, leave level 0 empty (see
|
|
51
|
-
* `Lod`) and draw the detail of the instances `levelOf` reports at level 0 yourself.
|
|
32
|
+
* Requires WebGPU `indirect-first-instance`. Sequential cameras are supported; nested
|
|
33
|
+
* renders replace the outer selection, so exclude this crowd with camera layers.
|
|
52
34
|
*/
|
|
53
35
|
export class InstancedLod extends Object3D {
|
|
54
36
|
isLOD = true;
|
|
@@ -60,19 +42,15 @@ export class InstancedLod extends Object3D {
|
|
|
60
42
|
levels;
|
|
61
43
|
count;
|
|
62
44
|
entries = [];
|
|
63
|
-
|
|
64
|
-
meshes = [];
|
|
65
|
-
probes = [];
|
|
45
|
+
probes;
|
|
66
46
|
selection = null;
|
|
67
|
-
|
|
68
|
-
|
|
47
|
+
meshes = null;
|
|
48
|
+
residency = new Residency(this.entries, (level, object, context) => this.install(level, object, context), () => {
|
|
49
|
+
this.selection?.route(this.residency.fallback, true);
|
|
50
|
+
this.dispatchEvent({ type: "levelschange" });
|
|
51
|
+
}, (error) => this.dispatchEvent({ type: "levelserror", error }));
|
|
69
52
|
disposed = false;
|
|
70
|
-
|
|
71
|
-
matrices;
|
|
72
|
-
/** Instance colours, 4 floats each, once `setColorAt` was called. */
|
|
73
|
-
colors = null;
|
|
74
|
-
/** Each instance's bounding sphere, centre and radius, in the InstancedLod's space. */
|
|
75
|
-
spheres;
|
|
53
|
+
store;
|
|
76
54
|
/** Bounding sphere of the source in an instance's space. */
|
|
77
55
|
sphere = new Sphere();
|
|
78
56
|
/** Every placed level, kept separate from the source sphere used for LOD selection. */
|
|
@@ -86,10 +64,14 @@ export class InstancedLod extends Object3D {
|
|
|
86
64
|
demand = [];
|
|
87
65
|
/** Source-radius padding for the shadow caster cutoff; culling pads every level separately. */
|
|
88
66
|
padding = 0;
|
|
89
|
-
matrixChanges;
|
|
90
|
-
colorChanges;
|
|
91
67
|
blocks;
|
|
92
68
|
last = { selected: false, eye: new Vector3(), scale: 0, ortho: false, fades: false, forced: null };
|
|
69
|
+
get pending() {
|
|
70
|
+
return this.residency.pending;
|
|
71
|
+
}
|
|
72
|
+
get error() {
|
|
73
|
+
return this.residency.error;
|
|
74
|
+
}
|
|
93
75
|
/** `levels[0]` is the source. Empty only as the target of `copy`. */
|
|
94
76
|
constructor(levels = [], count = 0) {
|
|
95
77
|
super();
|
|
@@ -97,133 +79,91 @@ export class InstancedLod extends Object3D {
|
|
|
97
79
|
throw new Error(`InstancedLod: count must be an integer >= 0, got ${count}`);
|
|
98
80
|
this.levels = this.entries;
|
|
99
81
|
this.count = count;
|
|
100
|
-
this.
|
|
101
|
-
this.
|
|
102
|
-
for (let i = 0; i < count; i++)
|
|
103
|
-
IDENTITY.toArray(this.matrices, i * 16);
|
|
104
|
-
this.matrixChanges = new Dirty(count);
|
|
105
|
-
this.colorChanges = new Dirty(count);
|
|
106
|
-
this.matrixChanges.all();
|
|
82
|
+
this.store = new Store(count);
|
|
83
|
+
this.probes = new Probes(this.store.matrices, this.rest);
|
|
107
84
|
const draws = levels.length > 0 ? this.build(levels) : 0;
|
|
108
|
-
this.blocks = new
|
|
85
|
+
this.blocks = new Blocks(this.store.matrices, this.store.spheres, this.sphere, this.bounds, draws, this.rest);
|
|
109
86
|
}
|
|
110
|
-
build(
|
|
111
|
-
const
|
|
112
|
-
if (!source)
|
|
113
|
-
throw new Error("InstancedLod: needs at least the source level");
|
|
114
|
-
if (isLazyImpostor(source))
|
|
115
|
-
throw new Error("InstancedLod: the source level must be eager");
|
|
116
|
-
for (const object of objects) {
|
|
117
|
-
if (isLazyImpostor(object)) {
|
|
118
|
-
object.materialize();
|
|
119
|
-
}
|
|
120
|
-
if (available(object))
|
|
121
|
-
this.installed.add(object);
|
|
122
|
-
}
|
|
123
|
-
this.fallback = fallbacks(objects);
|
|
124
|
-
for (const object of objects) {
|
|
125
|
-
rejectSkinned(object, "InstancedLod");
|
|
126
|
-
object.traverse((node) => {
|
|
127
|
-
if (isLOD(node))
|
|
128
|
-
throw new Error("InstancedLod: levels cannot contain a Lod or InstancedLod; nest them with Lod");
|
|
129
|
-
if (isInstancedMesh(node) && !isImpostorMesh(node))
|
|
130
|
-
throw new Error(`InstancedLod: levels cannot contain an InstancedMesh ("${node.name}")`);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
const { own, placed } = measureLevels(objects, "InstancedLod");
|
|
87
|
+
build(inputs) {
|
|
88
|
+
const { own, placed } = measureLevels(inputs, "InstancedLod");
|
|
134
89
|
this.sphere.copy(placed);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Every template mesh, split per material group: an indirect draw has one range.
|
|
141
|
-
const parts = objects.map((object, level) => {
|
|
142
|
-
// A template's own transform places it in the instance's space, as a Lod's child.
|
|
143
|
-
object.updateWorldMatrix(true, true);
|
|
144
|
-
const outside = object.parent ? object.parent.matrixWorld.clone().invert() : new Matrix4();
|
|
145
|
-
const found = [];
|
|
146
|
-
object.traverse((node) => {
|
|
147
|
-
if (!isMesh(node))
|
|
148
|
-
return;
|
|
149
|
-
const placement = outside.clone().multiply(node.matrixWorld);
|
|
150
|
-
if (level === 0)
|
|
151
|
-
this.probes.push({ mesh: node, placement });
|
|
152
|
-
ranges(node).forEach((range, group) => found.push({ template: node, placement, group, draw: { level, ...range } }));
|
|
153
|
-
});
|
|
154
|
-
return found;
|
|
90
|
+
inputs.forEach((input, i) => {
|
|
91
|
+
this.entries.push(new Level(input, i, this.entries));
|
|
92
|
+
this.reaches.push(new Sphere());
|
|
93
|
+
this.demand.push(new Sphere());
|
|
94
|
+
this.envelope.union(levelBox(input));
|
|
155
95
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
96
|
+
estimate(this.entries, 2 * own.radius);
|
|
97
|
+
validate(this.entries);
|
|
98
|
+
this.residency.reset();
|
|
99
|
+
this.boundLevels();
|
|
100
|
+
const templates = this.entries.map(({ object }, i) => (object ? parts(object, i) : []));
|
|
101
|
+
this.probes.add(templates[0] ?? []);
|
|
102
|
+
const draws = templates.flatMap((parts) => parts.map((part) => part.draw));
|
|
103
|
+
const selection = new Selection(this.store.matrices, this.sphere, templates.map((parts, i) => parts.length > 0 || at(inputs, i).box?.isEmpty() === false), draws, this.residency.fallback);
|
|
161
104
|
this.selection = selection;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const level = at(this.entries, i);
|
|
167
|
-
this.reaches[i] ??= new Sphere();
|
|
168
|
-
this.demand[i] ??= new Sphere();
|
|
169
|
-
const previous = this.meshes[i];
|
|
170
|
-
if (previous?.length && previous.length !== found.length)
|
|
171
|
-
throw new Error("InstancedLod: an existing level's meshes changed");
|
|
172
|
-
this.meshes[i] = found.map((part, index) => {
|
|
173
|
-
const offset = k++ * ARGS_BYTES, mesh = previous?.[index];
|
|
174
|
-
if (!mesh)
|
|
175
|
-
return this.instance(level, part.template, part.placement, part.group, offset, selection);
|
|
176
|
-
this.bind(mesh, level, selection, offset);
|
|
177
|
-
return mesh;
|
|
178
|
-
});
|
|
105
|
+
this.store.attach(selection);
|
|
106
|
+
const meshes = new Meshes(selection, this.reaches, this.drawRadius, {
|
|
107
|
+
live: () => !this.disposed,
|
|
108
|
+
drawn: (camera, mesh) => this.residency.drawn(camera, mesh),
|
|
179
109
|
});
|
|
180
|
-
this.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
110
|
+
this.meshes = meshes;
|
|
111
|
+
let offset = 0;
|
|
112
|
+
templates.forEach((parts, i) => {
|
|
113
|
+
const made = meshes.make(at(this.entries, i), parts, offset);
|
|
114
|
+
offset += made.length * ARGS_BYTES;
|
|
115
|
+
meshes.byLevel[i] = made;
|
|
116
|
+
for (const mesh of made)
|
|
117
|
+
this.add(mesh);
|
|
118
|
+
});
|
|
119
|
+
if (draws.length === 0)
|
|
120
|
+
watchContext(this);
|
|
185
121
|
return draws.length;
|
|
186
122
|
}
|
|
187
|
-
/**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
object.
|
|
196
|
-
|
|
123
|
+
/** Compile only the arriving level; bounds and instance buffers stay fixed. */
|
|
124
|
+
async install(level, template, context) {
|
|
125
|
+
const { selection, meshes } = this;
|
|
126
|
+
if (!selection || !meshes)
|
|
127
|
+
throw new Error("InstancedLod: missing selection");
|
|
128
|
+
const staged = new Object3D();
|
|
129
|
+
const object = template.clone();
|
|
130
|
+
if (level.box)
|
|
131
|
+
prepareBounds(object, level.box);
|
|
132
|
+
const found = parts(object, level.index);
|
|
133
|
+
const offset = selection.append(found.map((part) => part.draw));
|
|
134
|
+
// A deferred level has no upfront mesh count, so its indirect draw table can relocate.
|
|
135
|
+
meshes.relocate();
|
|
136
|
+
const made = meshes.make(level, found, offset);
|
|
137
|
+
for (const mesh of made)
|
|
138
|
+
staged.add(mesh);
|
|
139
|
+
const abandon = () => {
|
|
140
|
+
for (const mesh of made)
|
|
141
|
+
meshes.free(mesh);
|
|
142
|
+
disposeCopies(object);
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
refreshWind(this, staged);
|
|
146
|
+
await compileLevel(staged, context);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
abandon();
|
|
150
|
+
throw error;
|
|
197
151
|
}
|
|
198
|
-
if (
|
|
152
|
+
if (this.disposed) {
|
|
153
|
+
abandon();
|
|
199
154
|
return;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.updateWorldMatrix(true, true);
|
|
210
|
-
this.dispatchEvent({ type: "levelschange" });
|
|
155
|
+
}
|
|
156
|
+
level.object = object;
|
|
157
|
+
if (made.length > 0)
|
|
158
|
+
unwatchContext(this);
|
|
159
|
+
if (level.index === 0)
|
|
160
|
+
this.probes.add(found);
|
|
161
|
+
meshes.byLevel[level.index] = made;
|
|
162
|
+
for (const mesh of made)
|
|
163
|
+
this.add(mesh);
|
|
211
164
|
}
|
|
212
|
-
/**
|
|
165
|
+
/** Metadata includes unloaded levels, so later arrivals cannot enlarge this envelope. */
|
|
213
166
|
boundLevels() {
|
|
214
|
-
for (const meshes of this.meshes) {
|
|
215
|
-
for (const mesh of meshes) {
|
|
216
|
-
if (!isImpostorMesh(mesh))
|
|
217
|
-
mesh.geometry.computeBoundingBox();
|
|
218
|
-
const bounds = mesh.geometry.boundingBox;
|
|
219
|
-
if (!bounds)
|
|
220
|
-
throw new Error("InstancedLod: a level mesh has no geometry bounds");
|
|
221
|
-
box.copy(bounds);
|
|
222
|
-
if (isImpostorMesh(mesh))
|
|
223
|
-
box.applyMatrix4(mesh.placement);
|
|
224
|
-
this.envelope.union(box);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
167
|
this.rest.copy(this.sphere);
|
|
228
168
|
if (this.envelope.isEmpty())
|
|
229
169
|
return;
|
|
@@ -231,117 +171,48 @@ export class InstancedLod extends Object3D {
|
|
|
231
171
|
const { min, max } = this.envelope;
|
|
232
172
|
this.rest.radius = Math.max(this.rest.radius, Math.hypot(Math.max(Math.abs(min.x - center.x), Math.abs(max.x - center.x)), Math.max(Math.abs(min.y - center.y), Math.abs(max.y - center.y)), Math.max(Math.abs(min.z - center.z), Math.abs(max.z - center.z))));
|
|
233
173
|
}
|
|
234
|
-
/** Material group `group` of `template` drawn for every instance in `level`'s slots, from indirect draw arguments at byte `offset`. */
|
|
235
|
-
instance(level, template, placement, group, offset, selection) {
|
|
236
|
-
// An impostor card is placed in the shader; any other mesh draws its geometry moved
|
|
237
|
-
// into the instance's space, so the instance matrix is all it needs.
|
|
238
|
-
const mesh = isImpostorMesh(template)
|
|
239
|
-
? impostorInstances(template, this.count, placement)
|
|
240
|
-
: new InstancedMesh(placed(template.geometry, placement), groupMaterial(template, group), this.count);
|
|
241
|
-
mesh.geometry = indirect(mesh.geometry, selection, offset);
|
|
242
|
-
this.bind(mesh, level, selection, offset);
|
|
243
|
-
if (!isImpostorMesh(template))
|
|
244
|
-
placeInterior(mesh, placement);
|
|
245
|
-
mesh.name = template.name;
|
|
246
|
-
mesh.castShadow = template.castShadow;
|
|
247
|
-
mesh.receiveShadow = template.receiveShadow;
|
|
248
|
-
this.reaches[level.index] ??= this.bounds.clone();
|
|
249
|
-
mesh.boundingSphere = this.reaches[level.index] ?? null;
|
|
250
|
-
const previous = mesh.onBeforeRender;
|
|
251
|
-
// Selected once per view, before the first level mesh draws: in the view, or in a
|
|
252
|
-
// shadow pass that comes first. The GPU runs it before anything drawn after it.
|
|
253
|
-
mesh.onBeforeRender = (renderer, scene, camera, geometry, material, group) => {
|
|
254
|
-
previous.call(mesh, renderer, scene, camera, geometry, material, group);
|
|
255
|
-
if (pending.size === 0)
|
|
256
|
-
return;
|
|
257
|
-
const gpu = gpuRenderer(renderer);
|
|
258
|
-
// Each level's draws start at its slots (see select.ts).
|
|
259
|
-
if (!gpu.hasFeature("indirect-first-instance"))
|
|
260
|
-
throw new Error("InstancedLod: needs the GPU feature indirect-first-instance, which this device lacks");
|
|
261
|
-
const passes = [...pending].flatMap((due) => due.take(gpu));
|
|
262
|
-
pending.clear();
|
|
263
|
-
if (passes.length > 0)
|
|
264
|
-
gpu.compute(passes);
|
|
265
|
-
};
|
|
266
|
-
watchViewport(mesh);
|
|
267
|
-
this.add(mesh);
|
|
268
|
-
return mesh;
|
|
269
|
-
}
|
|
270
|
-
bind(mesh, level, selection, offset) {
|
|
271
|
-
mesh.geometry.setIndirect(selection.args, selection.argsOffset + offset);
|
|
272
|
-
mesh.instanceMatrix = selection.slots.matrices;
|
|
273
|
-
mesh.instanceColor = selection.slots.colors;
|
|
274
|
-
setDraw(mesh, { kind: "instances", fades: selection.slots.fades, radius: this.drawRadius, tint: level.tint });
|
|
275
|
-
}
|
|
276
174
|
setMatrixAt(index, matrix) {
|
|
277
|
-
this.
|
|
278
|
-
matrix.toArray(this.matrices, index * 16);
|
|
279
|
-
this.matrixChanges.add(index);
|
|
175
|
+
this.store.setMatrix(index, matrix);
|
|
280
176
|
}
|
|
281
177
|
getMatrixAt(index, matrix) {
|
|
282
|
-
this.
|
|
283
|
-
return matrix.fromArray(this.matrices, index * 16);
|
|
178
|
+
return this.store.getMatrix(index, matrix);
|
|
284
179
|
}
|
|
285
180
|
/** Tint instance `index` by `color`, as InstancedMesh.setColorAt does, in every level. */
|
|
286
181
|
setColorAt(index, color) {
|
|
287
|
-
this.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
this.useColors(this.selection, this.colors);
|
|
292
|
-
}
|
|
293
|
-
color.toArray(this.colors, index * 4);
|
|
294
|
-
this.colorChanges.add(index);
|
|
182
|
+
const first = this.store.colors === null;
|
|
183
|
+
this.store.setColor(index, color);
|
|
184
|
+
if (first)
|
|
185
|
+
this.meshes?.recolor();
|
|
295
186
|
}
|
|
296
187
|
/** Instance `index`'s colour: white unless set. */
|
|
297
188
|
getColorAt(index, color) {
|
|
298
|
-
this.
|
|
299
|
-
return this.colors ? color.fromArray(this.colors, index * 4) : color.setRGB(1, 1, 1);
|
|
300
|
-
}
|
|
301
|
-
/** Carry `colors` with the instances from now on: every level's meshes draw them. */
|
|
302
|
-
useColors(selection, colors) {
|
|
303
|
-
selection.useColors(colors);
|
|
304
|
-
for (const mesh of this.meshes.flat()) {
|
|
305
|
-
mesh.instanceColor = selection.slots.colors;
|
|
306
|
-
// Instance colours change the shader.
|
|
307
|
-
for (const material of [mesh.material].flat())
|
|
308
|
-
material.needsUpdate = true;
|
|
309
|
-
}
|
|
310
|
-
this.colorChanges.all();
|
|
189
|
+
return this.store.getColor(index, color);
|
|
311
190
|
}
|
|
312
191
|
/**
|
|
313
192
|
* The finest level instance `index` drew in the last update, or null before the first
|
|
314
193
|
* (or at scale 0). Inside a fade the next coarser level draws too.
|
|
315
194
|
*/
|
|
316
195
|
levelOf(index) {
|
|
317
|
-
this
|
|
318
|
-
|
|
196
|
+
const { last, store } = this;
|
|
197
|
+
store.check(index);
|
|
319
198
|
if (!last.selected)
|
|
320
199
|
return null;
|
|
321
|
-
|
|
200
|
+
store.measure(this.blocks);
|
|
322
201
|
const offset = index * 4;
|
|
323
|
-
const r = at(
|
|
202
|
+
const r = at(store.spheres, offset + 3);
|
|
324
203
|
if (r === 0)
|
|
325
204
|
return null;
|
|
326
205
|
let height = 2 * r * last.scale;
|
|
327
206
|
if (!last.ortho) {
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
if (x === undefined || y === undefined || z === undefined)
|
|
332
|
-
throw new Error(`InstancedLod: missing sphere ${index}`);
|
|
333
|
-
const dx = x - last.eye.x;
|
|
334
|
-
const dy = y - last.eye.y;
|
|
335
|
-
const dz = z - last.eye.z;
|
|
207
|
+
const dx = at(store.spheres, offset) - last.eye.x;
|
|
208
|
+
const dy = at(store.spheres, offset + 1) - last.eye.y;
|
|
209
|
+
const dz = at(store.spheres, offset + 2) - last.eye.z;
|
|
336
210
|
height /= Math.max(Math.sqrt(dx * dx + dy * dy + dz * dz), 1e-6);
|
|
337
211
|
}
|
|
338
212
|
const level = levelAt(this.entries, height, last.forced);
|
|
339
213
|
const selected = last.fades && fadeProgress(this.entries, level, height) > 0 ? level.index - 1 : level.index;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
check(index) {
|
|
343
|
-
if (!Number.isInteger(index) || index < 0 || index >= this.count)
|
|
344
|
-
throw new Error(`InstancedLod: index ${index} is not in 0..${this.count - 1}`);
|
|
214
|
+
const ready = at(this.residency.fallback, selected);
|
|
215
|
+
return ready < 0 ? null : ready;
|
|
345
216
|
}
|
|
346
217
|
/** Source radius padded for the level shaders' shadow caster cutoff. */
|
|
347
218
|
get drawRadius() {
|
|
@@ -349,52 +220,23 @@ export class InstancedLod extends Object3D {
|
|
|
349
220
|
}
|
|
350
221
|
[WIND_BOUNDS](padding, slope, baseY) {
|
|
351
222
|
this.padding = padding / this.sphere.radius;
|
|
352
|
-
|
|
353
|
-
const draw = drawOf(mesh);
|
|
354
|
-
if (draw?.kind === "instances")
|
|
355
|
-
draw.radius = this.drawRadius;
|
|
356
|
-
}
|
|
223
|
+
this.meshes?.setRadius(this.drawRadius);
|
|
357
224
|
// The wind shape comes from the source, but taller/offset levels can sway farther.
|
|
358
225
|
const reach = slope * Math.max(Math.abs(this.envelope.min.y - baseY), Math.abs(this.envelope.max.y - baseY));
|
|
359
226
|
this.blocks.pad(this.rest.radius > 0 ? reach / this.rest.radius : 0);
|
|
360
227
|
this.blocks.refit();
|
|
361
228
|
}
|
|
362
|
-
/**
|
|
363
|
-
measure() {
|
|
364
|
-
if (this.matrixChanges.empty && this.colorChanges.empty)
|
|
365
|
-
return;
|
|
366
|
-
this.measureChanges();
|
|
367
|
-
}
|
|
368
|
-
// Keep the closure-bearing work out of the unchanged path: V8 can allocate a
|
|
369
|
-
// lexical context on entry even when a function returns before its callbacks.
|
|
370
|
-
measureChanges() {
|
|
371
|
-
const { selection } = this;
|
|
372
|
-
const matricesChanged = !this.matrixChanges.empty;
|
|
373
|
-
this.matrixChanges.drain((start, end) => {
|
|
374
|
-
this.blocks.measure(start, end);
|
|
375
|
-
if (selection)
|
|
376
|
-
upload(selection.matrices, start, end);
|
|
377
|
-
});
|
|
378
|
-
this.colorChanges.drain((start, end) => {
|
|
379
|
-
if (selection?.colors)
|
|
380
|
-
upload(selection.colors, start, end);
|
|
381
|
-
});
|
|
382
|
-
selection?.changed();
|
|
383
|
-
if (matricesChanged)
|
|
384
|
-
this.blocks.refit();
|
|
385
|
-
}
|
|
386
|
-
/** Sort the instances into levels for `camera`; three's renderer calls this itself while `autoUpdate`. */
|
|
229
|
+
/** Sort instances for `camera`; demand begins after its first drawing context exists. */
|
|
387
230
|
update(camera) {
|
|
388
231
|
if (this.disposed || inShadowPass(this))
|
|
389
232
|
return;
|
|
390
|
-
this
|
|
391
|
-
|
|
392
|
-
if (!selection)
|
|
233
|
+
const { entries, selection, meshes } = this;
|
|
234
|
+
if (!selection || !meshes)
|
|
393
235
|
return;
|
|
394
236
|
validate(entries);
|
|
395
237
|
if (this.forceLevel !== null && !entries[this.forceLevel])
|
|
396
238
|
throw new Error(`Lod: forceLevel ${this.forceLevel} is not a level`);
|
|
397
|
-
this.measure();
|
|
239
|
+
this.store.measure(this.blocks);
|
|
398
240
|
const height = viewportHeight(camera);
|
|
399
241
|
view.begin(camera, height ?? TYPICAL_HEIGHT, this.forceLevel);
|
|
400
242
|
tintLevels(entries, Lod.debug);
|
|
@@ -402,70 +244,62 @@ export class InstancedLod extends Object3D {
|
|
|
402
244
|
const scale = view.scale * Lod.quality * (view.ortho ? worldScale : 1);
|
|
403
245
|
const { last } = this;
|
|
404
246
|
last.eye.copy(view.eye).applyMatrix4(inverse.copy(this.matrixWorld).invert());
|
|
405
|
-
|
|
247
|
+
last.selected = true;
|
|
248
|
+
last.scale = scale;
|
|
249
|
+
last.ortho = view.ortho;
|
|
250
|
+
last.fades = view.fades;
|
|
251
|
+
last.forced = this.forceLevel;
|
|
406
252
|
const stable = this.blocks.select(entries, last, this.reaches, height !== null, this.matrixWorld);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
for (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
253
|
+
const { residency } = this;
|
|
254
|
+
const progress = residency.progress;
|
|
255
|
+
if (selection.progress.value !== progress) {
|
|
256
|
+
selection.progress.value = progress;
|
|
257
|
+
selection.changed();
|
|
258
|
+
}
|
|
259
|
+
const needed = [];
|
|
260
|
+
if (this.bounds.isEmpty()) {
|
|
261
|
+
// Reusable crowds can be populated and drawn in the same frame.
|
|
262
|
+
for (const level of entries)
|
|
263
|
+
if (level.object)
|
|
264
|
+
needed.push(level.index);
|
|
265
|
+
}
|
|
266
|
+
else if (residency.needsDemand(camera, meshes.byLevel)) {
|
|
267
|
+
this.blocks.demand(entries, last, this.demand, this.matrixWorld);
|
|
268
|
+
// Coarsest first, so its load starts first.
|
|
269
|
+
for (let i = this.demand.length - 1; i >= 0; i--)
|
|
270
|
+
if (inView(camera, this.matrixWorld, at(this.demand, i)))
|
|
271
|
+
needed.push(i);
|
|
272
|
+
this.reroute(progress);
|
|
426
273
|
}
|
|
274
|
+
residency.demand(camera, needed);
|
|
275
|
+
residency.update(camera, meshes.byLevel);
|
|
427
276
|
selection.aim(last.eye, scale, view.ortho, view.fades, this.forceLevel, entries, stable);
|
|
428
277
|
// Selected only when something may draw: an InstancedLod with every level hidden
|
|
429
278
|
// keeps its last selection.
|
|
430
|
-
|
|
431
|
-
pending.add(selection);
|
|
279
|
+
meshes.schedule(meshes.show(camera, this.matrixWorld));
|
|
432
280
|
}
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
281
|
+
/** Move each level's reach to the level that draws in its place: its fallback, and during a handover also its previous one. */
|
|
282
|
+
reroute(progress) {
|
|
283
|
+
const { fallback, previous } = this.residency;
|
|
284
|
+
for (let i = this.reaches.length - 1; i >= 0; i--) {
|
|
285
|
+
const reach = at(this.reaches, i);
|
|
286
|
+
const target = at(fallback, i);
|
|
287
|
+
const before = progress < 1 ? at(previous, i) : -1;
|
|
288
|
+
if (before >= 0 && before !== target)
|
|
289
|
+
at(this.reaches, before).union(reach);
|
|
290
|
+
if (target === i)
|
|
291
|
+
continue;
|
|
292
|
+
if (target >= 0)
|
|
293
|
+
at(this.reaches, target).union(reach);
|
|
294
|
+
reach.makeEmpty();
|
|
295
|
+
}
|
|
443
296
|
}
|
|
444
297
|
/**
|
|
445
298
|
* Hits every instance's source level, with `instanceId` set; `object` is this
|
|
446
299
|
* InstancedLod. Meshes of the source on layers the raycaster does not test are skipped.
|
|
447
300
|
*/
|
|
448
301
|
raycast(raycaster, intersects) {
|
|
449
|
-
|
|
450
|
-
for (let i = 0; i < this.count; i++) {
|
|
451
|
-
instance.fromArray(this.matrices, i * 16);
|
|
452
|
-
sphere.center.copy(this.rest.center).applyMatrix4(instance);
|
|
453
|
-
sphere.radius = this.rest.radius * stretch(instance);
|
|
454
|
-
if (!ray.intersectsSphere(sphere))
|
|
455
|
-
continue;
|
|
456
|
-
for (const { mesh, placement } of this.probes) {
|
|
457
|
-
if (!mesh.layers.test(raycaster.layers))
|
|
458
|
-
continue;
|
|
459
|
-
// The template's own raycast, as if it stood where this instance does.
|
|
460
|
-
const own = mesh.matrixWorld;
|
|
461
|
-
mesh.matrixWorld = world.multiplyMatrices(this.matrixWorld, instance).multiply(placement);
|
|
462
|
-
hits.length = 0;
|
|
463
|
-
mesh.raycast(raycaster, hits);
|
|
464
|
-
mesh.matrixWorld = own;
|
|
465
|
-
for (const hit of hits)
|
|
466
|
-
intersects.push(Object.assign(hit, { object: this, instanceId: i }));
|
|
467
|
-
}
|
|
468
|
-
}
|
|
302
|
+
this.probes.raycast(this, raycaster, intersects);
|
|
469
303
|
return false;
|
|
470
304
|
}
|
|
471
305
|
/**
|
|
@@ -476,14 +310,9 @@ export class InstancedLod extends Object3D {
|
|
|
476
310
|
if (this.entries.length > 0 || this.count !== source.count)
|
|
477
311
|
throw new Error("InstancedLod.copy: copy into an empty InstancedLod of the same count");
|
|
478
312
|
super.copy(source, false);
|
|
479
|
-
this.
|
|
480
|
-
this.colors = source.colors?.slice() ?? null;
|
|
481
|
-
this.matrixChanges.all();
|
|
482
|
-
if (this.colors)
|
|
483
|
-
this.colorChanges.all();
|
|
313
|
+
this.store.copy(source.store);
|
|
484
314
|
const draws = source.entries.length > 0 ? this.build(cloneLevels(source.entries)) : 0;
|
|
485
|
-
this.blocks = new
|
|
486
|
-
copyThresholds(source.entries, this.entries);
|
|
315
|
+
this.blocks = new Blocks(this.store.matrices, this.store.spheres, this.sphere, this.bounds, draws, this.rest);
|
|
487
316
|
this.forceLevel = source.forceLevel;
|
|
488
317
|
this.autoUpdate = source.autoUpdate;
|
|
489
318
|
copyWind(source, this);
|
|
@@ -496,78 +325,25 @@ export class InstancedLod extends Object3D {
|
|
|
496
325
|
throw new Error("InstancedLod.clone: a subclass must override clone");
|
|
497
326
|
return clone;
|
|
498
327
|
}
|
|
499
|
-
/**
|
|
500
|
-
* Frees every level, the source included, and the instanced meshes. Clones share
|
|
501
|
-
* geometry and textures, so dispose only once no clone draws any more.
|
|
502
|
-
*/
|
|
328
|
+
/** Release drawables and templates only after every owner sharing their geometry, materials and textures has stopped drawing. */
|
|
503
329
|
dispose() {
|
|
504
330
|
if (this.disposed)
|
|
505
331
|
return;
|
|
506
332
|
this.disposed = true;
|
|
507
|
-
this
|
|
508
|
-
|
|
509
|
-
|
|
333
|
+
unwatchContext(this);
|
|
334
|
+
this.residency.dispose(() => {
|
|
335
|
+
this.clearDraws();
|
|
336
|
+
for (const { object } of this.entries)
|
|
337
|
+
if (object)
|
|
338
|
+
disposeCopies(object);
|
|
339
|
+
});
|
|
510
340
|
}
|
|
511
341
|
clearDraws() {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
for (const mesh of this.meshes.flat()) {
|
|
515
|
-
this.remove(mesh);
|
|
516
|
-
mesh.dispose();
|
|
517
|
-
if (!isImpostorMesh(mesh))
|
|
518
|
-
mesh.geometry.dispose();
|
|
519
|
-
}
|
|
520
|
-
if (this.selection)
|
|
521
|
-
pending.delete(this.selection);
|
|
342
|
+
this.meshes?.dispose();
|
|
343
|
+
this.meshes = null;
|
|
522
344
|
this.selection?.dispose();
|
|
523
345
|
this.selection = null;
|
|
524
|
-
this.
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function at(values, i) {
|
|
528
|
-
const value = values[i];
|
|
529
|
-
if (value === undefined)
|
|
530
|
-
throw new Error(`InstancedLod: index ${i} out of range`);
|
|
531
|
-
return value;
|
|
532
|
-
}
|
|
533
|
-
/** Union pending uploads, including repeated edits while a crowd is not drawn. */
|
|
534
|
-
function upload(attribute, start, end) {
|
|
535
|
-
start *= attribute.itemSize;
|
|
536
|
-
end *= attribute.itemSize;
|
|
537
|
-
const ranges = attribute.updateRanges;
|
|
538
|
-
let low = 0;
|
|
539
|
-
let high = ranges.length;
|
|
540
|
-
while (low < high) {
|
|
541
|
-
const middle = (low + high) >>> 1;
|
|
542
|
-
const range = at(ranges, middle);
|
|
543
|
-
if (range.start + range.count < start)
|
|
544
|
-
low = middle + 1;
|
|
545
|
-
else
|
|
546
|
-
high = middle;
|
|
547
|
-
}
|
|
548
|
-
const first = ranges[low];
|
|
549
|
-
if (!first || first.start > end) {
|
|
550
|
-
const range = { start, count: end - start };
|
|
551
|
-
if (low === ranges.length)
|
|
552
|
-
ranges.push(range);
|
|
553
|
-
else
|
|
554
|
-
ranges.splice(low, 0, range);
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
end = Math.max(end, first.start + first.count);
|
|
558
|
-
first.start = Math.min(start, first.start);
|
|
559
|
-
let next = low + 1;
|
|
560
|
-
while (next < ranges.length) {
|
|
561
|
-
const range = at(ranges, next);
|
|
562
|
-
if (range.start > end)
|
|
563
|
-
break;
|
|
564
|
-
end = Math.max(end, range.start + range.count);
|
|
565
|
-
next++;
|
|
566
|
-
}
|
|
567
|
-
first.count = end - first.start;
|
|
568
|
-
if (next > low + 1)
|
|
569
|
-
ranges.splice(low + 1, next - low - 1);
|
|
346
|
+
this.store.attach(null);
|
|
570
347
|
}
|
|
571
|
-
attribute.needsUpdate = true;
|
|
572
348
|
}
|
|
573
349
|
//# sourceMappingURL=index.js.map
|