@drawcall/lod 0.1.2 → 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.
Files changed (144) hide show
  1. package/README.md +10 -69
  2. package/dist/at.d.ts +2 -0
  3. package/dist/at.js +8 -0
  4. package/dist/at.js.map +1 -0
  5. package/dist/baked/codec.d.ts +6 -3
  6. package/dist/baked/codec.js +22 -16
  7. package/dist/baked/codec.js.map +1 -1
  8. package/dist/baked/index.d.ts +4 -0
  9. package/dist/baked/index.js +2 -2
  10. package/dist/baked/index.js.map +1 -1
  11. package/dist/baked/low.d.ts +2 -2
  12. package/dist/baked/low.js +4 -4
  13. package/dist/baked/low.js.map +1 -1
  14. package/dist/bounds.d.ts +12 -2
  15. package/dist/bounds.js +72 -10
  16. package/dist/bounds.js.map +1 -1
  17. package/dist/compile.d.ts +4 -0
  18. package/dist/compile.js +100 -0
  19. package/dist/compile.js.map +1 -0
  20. package/dist/deferred.d.ts +12 -0
  21. package/dist/deferred.js +27 -0
  22. package/dist/deferred.js.map +1 -0
  23. package/dist/dispose.d.ts +7 -3
  24. package/dist/dispose.js +33 -4
  25. package/dist/dispose.js.map +1 -1
  26. package/dist/fade.d.ts +5 -13
  27. package/dist/fade.js +62 -28
  28. package/dist/fade.js.map +1 -1
  29. package/dist/impostor/bake/index.d.ts +1 -0
  30. package/dist/impostor/bake/index.js +1 -0
  31. package/dist/impostor/bake/index.js.map +1 -1
  32. package/dist/impostor/bake/materials.js +11 -1
  33. package/dist/impostor/bake/materials.js.map +1 -1
  34. package/dist/impostor/codec.d.ts +2 -7
  35. package/dist/impostor/codec.js +16 -27
  36. package/dist/impostor/codec.js.map +1 -1
  37. package/dist/impostor/mesh/card.js +4 -1
  38. package/dist/impostor/mesh/card.js.map +1 -1
  39. package/dist/impostor/mesh/index.d.ts +6 -4
  40. package/dist/impostor/mesh/index.js +8 -10
  41. package/dist/impostor/mesh/index.js.map +1 -1
  42. package/dist/impostor/mesh/material.d.ts +7 -5
  43. package/dist/impostor/mesh/material.js +29 -26
  44. package/dist/impostor/mesh/material.js.map +1 -1
  45. package/dist/impostor/mesh/surface.d.ts +20 -16
  46. package/dist/impostor/mesh/surface.js +74 -55
  47. package/dist/impostor/mesh/surface.js.map +1 -1
  48. package/dist/impostor/octahedral.d.ts +1 -8
  49. package/dist/impostor/octahedral.js +3 -18
  50. package/dist/impostor/octahedral.js.map +1 -1
  51. package/dist/index.d.ts +7 -6
  52. package/dist/index.js +5 -4
  53. package/dist/index.js.map +1 -1
  54. package/dist/instanced/args.js +2 -2
  55. package/dist/instanced/args.js.map +1 -1
  56. package/dist/instanced/{bounds.d.ts → blocks.d.ts} +25 -9
  57. package/dist/instanced/{bounds.js → blocks.js} +76 -53
  58. package/dist/instanced/blocks.js.map +1 -0
  59. package/dist/instanced/dirty.d.ts +3 -0
  60. package/dist/instanced/dirty.js +40 -5
  61. package/dist/instanced/dirty.js.map +1 -1
  62. package/dist/instanced/index.d.ts +25 -51
  63. package/dist/instanced/index.js +189 -413
  64. package/dist/instanced/index.js.map +1 -1
  65. package/dist/instanced/meshes.d.ts +54 -13
  66. package/dist/instanced/meshes.js +178 -41
  67. package/dist/instanced/meshes.js.map +1 -1
  68. package/dist/instanced/parts.d.ts +14 -0
  69. package/dist/instanced/parts.js +79 -0
  70. package/dist/instanced/parts.js.map +1 -0
  71. package/dist/instanced/raycast.d.ts +13 -0
  72. package/dist/instanced/raycast.js +53 -0
  73. package/dist/instanced/raycast.js.map +1 -0
  74. package/dist/instanced/select.d.ts +17 -9
  75. package/dist/instanced/select.js +114 -63
  76. package/dist/instanced/select.js.map +1 -1
  77. package/dist/instanced/store.d.ts +31 -0
  78. package/dist/instanced/store.js +96 -0
  79. package/dist/instanced/store.js.map +1 -0
  80. package/dist/levels.d.ts +71 -48
  81. package/dist/levels.js +99 -74
  82. package/dist/levels.js.map +1 -1
  83. package/dist/loader/bundle.d.ts +25 -0
  84. package/dist/loader/bundle.js +241 -0
  85. package/dist/loader/bundle.js.map +1 -0
  86. package/dist/loader/cache.d.ts +15 -15
  87. package/dist/loader/cache.js +71 -33
  88. package/dist/loader/cache.js.map +1 -1
  89. package/dist/loader/provenance.d.ts +30 -0
  90. package/dist/loader/provenance.js +105 -0
  91. package/dist/loader/provenance.js.map +1 -0
  92. package/dist/loader/url.d.ts +13 -0
  93. package/dist/loader/url.js +31 -0
  94. package/dist/loader/url.js.map +1 -0
  95. package/dist/loader.d.ts +30 -74
  96. package/dist/loader.js +191 -186
  97. package/dist/loader.js.map +1 -1
  98. package/dist/lod.d.ts +20 -31
  99. package/dist/lod.js +105 -96
  100. package/dist/lod.js.map +1 -1
  101. package/dist/material.js +9 -4
  102. package/dist/material.js.map +1 -1
  103. package/dist/mesh/codec.d.ts +4 -0
  104. package/dist/mesh/codec.js +192 -0
  105. package/dist/mesh/codec.js.map +1 -0
  106. package/dist/queue.d.ts +2 -0
  107. package/dist/queue.js +33 -0
  108. package/dist/queue.js.map +1 -0
  109. package/dist/residency.d.ts +44 -0
  110. package/dist/residency.js +205 -0
  111. package/dist/residency.js.map +1 -0
  112. package/dist/simplify.js +5 -3
  113. package/dist/simplify.js.map +1 -1
  114. package/dist/source/{error.d.ts → deviation.d.ts} +3 -3
  115. package/dist/source/{error.js → deviation.js} +6 -6
  116. package/dist/source/deviation.js.map +1 -0
  117. package/dist/source/source.d.ts +1 -1
  118. package/dist/source/source.js +1 -1
  119. package/dist/three.d.ts +17 -5
  120. package/dist/three.js +24 -3
  121. package/dist/three.js.map +1 -1
  122. package/dist/tsl.d.ts +5 -5
  123. package/dist/tsl.js +12 -19
  124. package/dist/tsl.js.map +1 -1
  125. package/dist/view.d.ts +16 -1
  126. package/dist/view.js +57 -1
  127. package/dist/view.js.map +1 -1
  128. package/dist/wind/index.d.ts +2 -2
  129. package/dist/wind/index.js +31 -28
  130. package/dist/wind/index.js.map +1 -1
  131. package/dist/wind/sway.d.ts +2 -1
  132. package/package.json +9 -3
  133. package/skills/lod/SKILL.md +71 -109
  134. package/dist/impostor/cache.d.ts +0 -15
  135. package/dist/impostor/cache.js +0 -44
  136. package/dist/impostor/cache.js.map +0 -1
  137. package/dist/instanced/bounds.js.map +0 -1
  138. package/dist/lazy.d.ts +0 -64
  139. package/dist/lazy.js +0 -187
  140. package/dist/lazy.js.map +0 -1
  141. package/dist/loader/dispose.d.ts +0 -3
  142. package/dist/loader/dispose.js +0 -25
  143. package/dist/loader/dispose.js.map +0 -1
  144. package/dist/source/error.js.map +0 -1
@@ -1,54 +1,36 @@
1
- import { Box3, InstancedMesh, Matrix4, Object3D, Ray, Sphere, Vector3, } from "three/webgpu";
2
- import { measureLevels } from "../bounds.js";
3
- import { disposeTree } from "../dispose.js";
4
- import { drawOf, setDraw } from "../fade.js";
5
- import { placeInterior } from "../baked/interior/material.js";
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 { available, fallbacks, inView } from "../lazy.js";
10
- import { gpuRenderer, inShadowPass, isLazyImpostor, isImpostorMesh, isInstancedMesh, isLOD, isMesh, rejectSkinned, sameClass } from "../three.js";
11
- import { TYPICAL_HEIGHT, View, viewportHeight, watchViewport } from "../view.js";
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 { groupMaterial, indirect, placed, ranges } from "./meshes.js";
15
- import { Bounds, stretch } from "./bounds.js";
16
- import { Dirty } from "./dirty.js";
17
- import { ARGS_BYTES, Selection } from "./select.js";
18
- const IDENTITY = new Matrix4();
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
- * Selections due before the next draw of any InstancedLod: the first level mesh to draw
29
- * runs them all in one compute call (each call has a cost of its own: an encoder, a
30
- * pass, a submit). Each renderer runs those its own buffers are not current with.
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
- * Instance matrices are model transforms of the object, as for an InstancedMesh; set
44
- * them with `setMatrixAt`, and instance colours with `setColorAt`. An instance at scale 0
45
- * draws nothing. Its own transform (and its parents') should scale uniformly; instances
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
- * Levels are templates, read when it is built: their meshes are drawn instanced, not
49
- * added to the scene. They cannot contain skinned meshes, InstancedMeshes (other than an
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
- /** Per level, the meshes drawing it. */
64
- meshes = [];
65
- probes = [];
45
+ probes;
66
46
  selection = null;
67
- fallback = [];
68
- installed = new Set();
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
- /** Instance matrices, 16 floats each: the selection uploads them from here. */
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.matrices = new Float32Array(count * 16);
101
- this.spheres = new Float32Array(count * 4);
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 Bounds(this.matrices, this.spheres, this.sphere, this.bounds, draws, this.rest);
85
+ this.blocks = new Blocks(this.store.matrices, this.store.spheres, this.sphere, this.bounds, draws, this.rest);
109
86
  }
110
- build(objects, rebuilding = false) {
111
- const [source] = objects;
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
- if (!rebuilding) {
136
- objects.forEach((object, i) => this.entries.push(new Level(object, i, false, this.entries)));
137
- estimate(this.entries, 2 * own.radius);
138
- }
139
- validate(this.entries);
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
- const draws = parts.flat().map((part) => part.draw);
157
- const selection = this.selection ??
158
- new Selection(this.matrices, this.sphere, parts.map((found, i) => found.length > 0 || isLazyImpostor(at(objects, i))), draws, this.fallback);
159
- if (this.selection)
160
- selection.replace(draws, this.fallback);
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
- if (this.colors)
163
- this.useColors(selection, this.colors);
164
- let k = 0;
165
- parts.forEach((found, i) => {
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.boundLevels();
181
- if (!rebuilding)
182
- for (const object of objects)
183
- if (isLazyImpostor(object))
184
- object.retain();
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
- /** Install ready atlas templates atomically before selecting this view. */
188
- refresh() {
189
- if (this.installed.size === this.entries.length)
190
- return;
191
- let changed = false;
192
- for (const { object } of this.entries) {
193
- if (!isLazyImpostor(object) || this.installed.has(object))
194
- continue;
195
- object.materialize();
196
- changed ||= available(object);
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 (!changed)
152
+ if (this.disposed) {
153
+ abandon();
199
154
  return;
200
- if (this.selection)
201
- pending.delete(this.selection);
202
- this.probes.length = 0;
203
- this.envelope.makeEmpty();
204
- const draws = this.build(this.entries.map((entry) => entry.object), true);
205
- this.blocks = new Bounds(this.matrices, this.spheres, this.sphere, this.bounds, draws, this.rest);
206
- this.matrixChanges.all();
207
- this.measure();
208
- refreshWind(this);
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
- /** Bound what the level meshes draw, including template placement and impostor boxes. */
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.check(index);
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.check(index);
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.check(index);
288
- if (!this.colors) {
289
- this.colors = new Float32Array(this.count * 4).fill(1);
290
- if (this.selection)
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.check(index);
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.check(index);
318
- const { last } = this;
196
+ const { last, store } = this;
197
+ store.check(index);
319
198
  if (!last.selected)
320
199
  return null;
321
- this.measure();
200
+ store.measure(this.blocks);
322
201
  const offset = index * 4;
323
- const r = at(this.spheres, offset + 3);
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 x = this.spheres[offset];
329
- const y = this.spheres[offset + 1];
330
- const z = this.spheres[offset + 2];
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
- return this.fallback[selected] ?? selected;
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
- for (const mesh of this.meshes.flat()) {
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
- /** Measure only changed matrices; matrix and colour uploads have separate ranges. */
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.refresh();
391
- const { entries, selection } = this;
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
- Object.assign(last, { selected: true, scale, ortho: view.ortho, fades: view.fades, forced: this.forceLevel });
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
- if (this.installed.size < entries.length) {
408
- let demand = this.reaches;
409
- if (height === null) {
410
- // Every mesh must get a first chance to report its viewport. That conservative
411
- // visibility is not a reason to load every lazy tier: use the actual default
412
- // selection, including forced and empty levels, for initial atlas demand.
413
- demand = this.demand;
414
- this.blocks.select(entries, last, demand, true, this.matrixWorld);
415
- }
416
- for (let i = this.reaches.length - 1; i >= 0; i--) {
417
- const reach = at(this.reaches, i), object = at(entries, i).object;
418
- if (isLazyImpostor(object) && object.status === "idle" && inView(camera, this.matrixWorld, at(demand, i)))
419
- object.request();
420
- const target = this.fallback[i] ?? i;
421
- if (target !== i) {
422
- at(this.reaches, target).union(reach);
423
- reach.makeEmpty();
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
- if (this.showLevels())
431
- pending.add(selection);
279
+ meshes.schedule(meshes.show(camera, this.matrixWorld));
432
280
  }
433
- /** Only levels whose conservative bounds contain a possible selected instance. */
434
- showLevels() {
435
- let shown = false;
436
- this.meshes.forEach((meshes, level) => {
437
- const visible = !this.reaches[level]?.isEmpty();
438
- for (const mesh of meshes)
439
- mesh.visible = visible;
440
- shown ||= visible && meshes.length > 0;
441
- });
442
- return shown;
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
- ray.copy(raycaster.ray).applyMatrix4(inverse.copy(this.matrixWorld).invert());
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.matrices.set(source.matrices);
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 Bounds(this.matrices, this.spheres, this.sphere, this.bounds, draws, this.rest);
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.clearDraws();
508
- for (const { object } of this.entries)
509
- isLazyImpostor(object) ? object.release() : disposeTree(object);
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
- // An ImpostorMesh frees its own material and geometry; the other meshes' faded
513
- // materials go with the levels' originals.
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.meshes.length = 0;
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