@driftengine/assets 4.5.0 → 4.6.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.
@@ -0,0 +1,333 @@
1
+ /**
2
+ * The regions of a streamed world as they arrive: each one's meshes at every level, its prop
3
+ * batches, its occluders and its collision, kept apart from the loader's parts and never merged.
4
+ *
5
+ * **Apart, because a region's levels are alternatives rather than pieces.** The loader merges its
6
+ * parts by material into one mesh a group, which is right for one model and wrong here twice over:
7
+ * it would weld a district's finest level to its coarsest, drawing both, and weld one district to
8
+ * the next, so none could be dropped when the eye leaves. So a region's meshes upload on the
9
+ * loader's clocked queue like any part and land here instead, one part a mesh, a list a level.
10
+ *
11
+ * **A prop's prototype arrives once and serves every region that places it.** A region naming a
12
+ * prototype already up gets its batch at the next budgeted build; one naming a prototype still to
13
+ * come waits for it. Either way the batch is built inside a frame's budget, never from the stream's
14
+ * callback, and culls instance by instance (`createInstanced`'s `cull`).
15
+ *
16
+ * **A level that arrives as assemblies is paged** (`MSHC`, `drftAssembly.ts`). A city's finest
17
+ * level is millions of triangles, more than any device keeps resident, so such a level is held as
18
+ * the copies the file carried — tens of kilobytes a district — and comes up only when the caller
19
+ * pages it in, expanded and uploaded on the same clocked queue; paged out, its meshes are freed and
20
+ * the copies stay. What it costs is a region that is not yet up when the eye arrives: the caller
21
+ * draws a coarser level until `resident` says the fine one is there, and pages ahead of the eye.
22
+ *
23
+ * Everything is in the loader's placement: a world loads with `{ fit: 'none' }`, where that is the
24
+ * file's own space.
25
+ */
26
+ import { createMeshInstances } from '@driftengine/core';
27
+ import type { MeshHandle, MeshInstances, RendererApi } from '@driftengine/core';
28
+ import type { DrftAssembly, DrftRegion, DrftRegionCollision, PieceLookup } from '@driftengine/drft';
29
+ import { expandAssembly } from '@driftengine/drft';
30
+ import type { MeshData } from '@driftengine/drft';
31
+ import type { DrftPart } from './loadProgress.ts';
32
+
33
+ /** How the loader placed the model: `p · scale + (x, y, z)`. */
34
+ export interface LoaderFit {
35
+ readonly scale: number;
36
+ readonly x: number;
37
+ readonly y: number;
38
+ readonly z: number;
39
+ }
40
+
41
+ export interface LoadedRegionLevel {
42
+ /** How far, in metres of the file, this level may stand from the real surface. */
43
+ readonly error: number;
44
+ /** One part a mesh, filled as they upload. */
45
+ readonly parts: DrftPart[];
46
+ /** Whether any of its meshes arrived as assemblies, kept small until paged in. */
47
+ paged: boolean;
48
+ /** Whether every mesh is up: always, for a level that is not paged. */
49
+ resident: boolean;
50
+ }
51
+
52
+ export interface LoadedRegion {
53
+ readonly id: number;
54
+ /** Min xyz then max xyz, placed. */
55
+ readonly bounds: Float32Array;
56
+ /** Finest first. `HlodSet.add` takes the errors, scaled by the fit. */
57
+ readonly levels: readonly LoadedRegionLevel[];
58
+ /** Its props, one instanced part a prototype, culled per instance. */
59
+ readonly batches: DrftPart[];
60
+ /**
61
+ * Which of the region's groups in the file each batch is, index for index. Batches are built as
62
+ * their prototypes arrive, not in the file's order, so a caller pairing something of its own with
63
+ * each group — a material, a blend — pairs through this, never by position.
64
+ */
65
+ readonly batchGroups: number[];
66
+ /** Six floats a box, placed, for `addOccluder`. */
67
+ readonly occluders: Float32Array;
68
+ /** What it collides as, placed, or null. */
69
+ readonly collision: DrftRegionCollision | null;
70
+ /** Meshes and batches still to come; 0 once every level and prop is up. */
71
+ pending: number;
72
+ }
73
+
74
+ /**
75
+ * Copies of a fitted prototype, each matrix conjugated by the fit: M becomes F·M·F⁻¹, the same turn
76
+ * and a translation of t − R·t + s·u for the fit's scale s and offset t and the copy's own
77
+ * translation u. The prototype already carries the fit, so the copies land where its geometry does
78
+ * and are not scaled twice.
79
+ */
80
+ export function fitCopies(transforms: Float32Array, fit: LoaderFit): MeshInstances {
81
+ const count = transforms.length / 16;
82
+ const data = createMeshInstances(count);
83
+ for (let k = 0; k < count; k++) {
84
+ const at = k * 16;
85
+ for (let i = 0; i < 12; i++) data.models[at + i] = transforms[at + i] as number;
86
+ /* R·t, with R the copy's own 3×3 read column-major. */
87
+ const rtx =
88
+ (transforms[at] as number) * fit.x +
89
+ (transforms[at + 4] as number) * fit.y +
90
+ (transforms[at + 8] as number) * fit.z;
91
+ const rty =
92
+ (transforms[at + 1] as number) * fit.x +
93
+ (transforms[at + 5] as number) * fit.y +
94
+ (transforms[at + 9] as number) * fit.z;
95
+ const rtz =
96
+ (transforms[at + 2] as number) * fit.x +
97
+ (transforms[at + 6] as number) * fit.y +
98
+ (transforms[at + 10] as number) * fit.z;
99
+ data.models[at + 12] = fit.x - rtx + fit.scale * (transforms[at + 12] as number);
100
+ data.models[at + 13] = fit.y - rty + fit.scale * (transforms[at + 13] as number);
101
+ data.models[at + 14] = fit.z - rtz + fit.scale * (transforms[at + 14] as number);
102
+ data.models[at + 15] = 1;
103
+ }
104
+ data.tints.fill(1);
105
+ data.count = count;
106
+ return data;
107
+ }
108
+
109
+ /** Points packed xyz, placed by the fit into a new array. */
110
+ function placed(points: ArrayLike<number>, fit: LoaderFit): Float32Array {
111
+ const out = new Float32Array(points.length);
112
+ for (let i = 0; i < points.length; i += 3) {
113
+ out[i] = (points[i] as number) * fit.scale + fit.x;
114
+ out[i + 1] = (points[i + 1] as number) * fit.scale + fit.y;
115
+ out[i + 2] = (points[i + 2] as number) * fit.scale + fit.z;
116
+ }
117
+ return out;
118
+ }
119
+
120
+ export class RegionStore {
121
+ readonly regions = new Map<number, LoadedRegion>();
122
+ /** Level meshes by ordinal: whose, and which level. */
123
+ private readonly levelOwners = new Map<number, { region: LoadedRegion; level: number }>();
124
+ /** Prototypes that have uploaded, by ordinal. */
125
+ private readonly prototypes = new Map<number, DrftPart>();
126
+ /** Groups whose prototype has not uploaded yet, by its ordinal. */
127
+ private readonly waiting = new Map<
128
+ number,
129
+ { region: LoadedRegion; transforms: Float32Array; group: number }[]
130
+ >();
131
+ private readonly jobs: {
132
+ region: LoadedRegion;
133
+ prototype: DrftPart;
134
+ transforms: Float32Array;
135
+ group: number;
136
+ }[] = [];
137
+ private readonly made: MeshHandle[] = [];
138
+ /** Assemblies held small, by ordinal, and each paged level's ordinals. */
139
+ private readonly compact = new Map<number, { assembly: DrftAssembly; piece: PieceLookup }>();
140
+ private readonly levelCompact = new Map<LoadedRegionLevel, number[]>();
141
+ /** Paged meshes asked for and not yet up, and those that are. */
142
+ private readonly requested = new Set<number>();
143
+ private readonly pagedParts = new Map<number, DrftPart>();
144
+
145
+ constructor(
146
+ private readonly renderer: RendererApi,
147
+ private readonly fit: () => LoaderFit | null,
148
+ ) {}
149
+
150
+ /** Whether a mesh is a region's, as a level or a prop, and so is not a part of the model. */
151
+ owns(ordinal: number): boolean {
152
+ return (
153
+ this.levelOwners.has(ordinal) || this.prototypes.has(ordinal) || this.waiting.has(ordinal)
154
+ );
155
+ }
156
+
157
+ /** Whether batches are queued. */
158
+ get busy(): boolean {
159
+ return this.jobs.length > 0;
160
+ }
161
+
162
+ /** A region's chunk has landed, ahead of the meshes it introduces. */
163
+ admit(region: DrftRegion): void {
164
+ const fit = this.fit() ?? { scale: 1, x: 0, y: 0, z: 0 };
165
+ const loaded: LoadedRegion = {
166
+ id: region.id,
167
+ bounds: new Float32Array([
168
+ ...placed([region.bounds[0], region.bounds[1], region.bounds[2]] as number[], fit),
169
+ ...placed([region.bounds[3], region.bounds[4], region.bounds[5]] as number[], fit),
170
+ ]),
171
+ levels: region.levels.map((level) => ({
172
+ error: level.error * fit.scale,
173
+ parts: [],
174
+ paged: false,
175
+ resident: true,
176
+ })),
177
+ batches: [],
178
+ batchGroups: [],
179
+ occluders: placed(region.occluders, fit),
180
+ collision:
181
+ region.collision === null
182
+ ? null
183
+ : {
184
+ positions: placed(region.collision.positions, fit),
185
+ indices: region.collision.indices,
186
+ },
187
+ pending: 0,
188
+ };
189
+ region.levels.forEach((level, index) => {
190
+ for (const mesh of level.meshes) {
191
+ this.levelOwners.set(mesh, { region: loaded, level: index });
192
+ loaded.pending++;
193
+ }
194
+ });
195
+ region.instances.forEach((group, index) => {
196
+ loaded.pending++;
197
+ const prototype = this.prototypes.get(group.mesh);
198
+ if (prototype !== undefined) {
199
+ this.jobs.push({ region: loaded, prototype, transforms: group.transforms, group: index });
200
+ return;
201
+ }
202
+ const list = this.waiting.get(group.mesh) ?? [];
203
+ list.push({ region: loaded, transforms: group.transforms, group: index });
204
+ this.waiting.set(group.mesh, list);
205
+ });
206
+ this.regions.set(region.id, loaded);
207
+ }
208
+
209
+ /** Whether a mesh is one of a region's levels. */
210
+ isLevelMesh(ordinal: number): boolean {
211
+ return this.levelOwners.has(ordinal);
212
+ }
213
+
214
+ /** A level's assembly has arrived: held small, its level paged and not yet up. */
215
+ hold(ordinal: number, assembly: DrftAssembly, piece: PieceLookup): void {
216
+ const owner = this.levelOwners.get(ordinal);
217
+ const level = owner?.region.levels[owner.level];
218
+ if (owner === undefined || level === undefined) return;
219
+ this.compact.set(ordinal, { assembly, piece });
220
+ const list = this.levelCompact.get(level) ?? [];
221
+ list.push(ordinal);
222
+ this.levelCompact.set(level, list);
223
+ level.paged = true;
224
+ level.resident = false;
225
+ owner.region.pending--;
226
+ }
227
+
228
+ /**
229
+ * Ask for a paged level's meshes, or free them. Coming up, returns each mesh still to upload with
230
+ * a way to expand it, for the loader's queue; going down, frees what is up and returns the
231
+ * ordinals released, so the loader can drop any still queued.
232
+ */
233
+ page(
234
+ id: number,
235
+ level: number,
236
+ resident: boolean,
237
+ ): { ordinal: number; expand: () => MeshData }[] {
238
+ const lv = this.regions.get(id)?.levels[level];
239
+ const ordinals = lv === undefined ? undefined : this.levelCompact.get(lv);
240
+ if (lv === undefined || ordinals === undefined) return [];
241
+ const out: { ordinal: number; expand: () => MeshData }[] = [];
242
+ for (const ordinal of ordinals) {
243
+ const held = this.compact.get(ordinal);
244
+ if (held === undefined) continue;
245
+ if (resident) {
246
+ if (this.pagedParts.has(ordinal) || this.requested.has(ordinal)) continue;
247
+ this.requested.add(ordinal);
248
+ out.push({ ordinal, expand: () => expandAssembly(held.assembly, held.piece) });
249
+ continue;
250
+ }
251
+ this.requested.delete(ordinal);
252
+ const part = this.pagedParts.get(ordinal);
253
+ out.push({ ordinal, expand: () => expandAssembly(held.assembly, held.piece) });
254
+ if (part === undefined) continue;
255
+ this.pagedParts.delete(ordinal);
256
+ this.release(part);
257
+ const at = lv.parts.indexOf(part);
258
+ if (at >= 0) lv.parts.splice(at, 1);
259
+ }
260
+ if (!resident) lv.resident = false;
261
+ return out;
262
+ }
263
+
264
+ private release(part: DrftPart): void {
265
+ const at = this.made.indexOf(part.mesh);
266
+ if (at >= 0) this.made.splice(at, 1);
267
+ this.renderer.disposeMesh(part.mesh);
268
+ }
269
+
270
+ /** A region's mesh has uploaded, as the part the loader would have made of it. */
271
+ arrived(ordinal: number, part: DrftPart): void {
272
+ if (this.compact.has(ordinal)) {
273
+ /* Paged out again while it was on its way up: nothing wants it. */
274
+ if (!this.requested.delete(ordinal)) {
275
+ this.renderer.disposeMesh(part.mesh);
276
+ return;
277
+ }
278
+ this.made.push(part.mesh);
279
+ this.pagedParts.set(ordinal, part);
280
+ const owner = this.levelOwners.get(ordinal);
281
+ const level = owner?.region.levels[owner.level];
282
+ if (level === undefined) return;
283
+ level.parts.push(part);
284
+ level.resident = (this.levelCompact.get(level) ?? []).every((o) => this.pagedParts.has(o));
285
+ return;
286
+ }
287
+ this.made.push(part.mesh);
288
+ const owner = this.levelOwners.get(ordinal);
289
+ if (owner !== undefined) {
290
+ owner.region.levels[owner.level]?.parts.push(part);
291
+ owner.region.pending--;
292
+ return;
293
+ }
294
+ this.prototypes.set(ordinal, part);
295
+ for (const group of this.waiting.get(ordinal) ?? []) {
296
+ this.jobs.push({
297
+ region: group.region,
298
+ prototype: part,
299
+ transforms: group.transforms,
300
+ group: group.group,
301
+ });
302
+ }
303
+ this.waiting.delete(ordinal);
304
+ }
305
+
306
+ /** Build one queued batch inside the caller's budget; false when none was queued. */
307
+ buildNext(): boolean {
308
+ const job = this.jobs.shift();
309
+ if (job === undefined) return false;
310
+ const data = fitCopies(job.transforms, this.fit() ?? { scale: 1, x: 0, y: 0, z: 0 });
311
+ try {
312
+ const batch = this.renderer.createInstanced(job.prototype.mesh, data.count, { cull: true });
313
+ this.renderer.uploadInstanced(batch, data);
314
+ job.region.batches.push({ ...job.prototype, reveal: 1, instances: { batch, data } });
315
+ job.region.batchGroups.push(job.group);
316
+ } catch (error) {
317
+ console.warn(`DrftLoader: region ${job.region.id}'s props would not upload`, error);
318
+ }
319
+ job.region.pending--;
320
+ return true;
321
+ }
322
+
323
+ dispose(): void {
324
+ for (const region of this.regions.values()) {
325
+ for (const part of region.batches) {
326
+ if (part.instances !== null) this.renderer.disposeInstanced(part.instances.batch);
327
+ }
328
+ }
329
+ for (const mesh of this.made) this.renderer.disposeMesh(mesh);
330
+ this.made.length = 0;
331
+ this.regions.clear();
332
+ }
333
+ }
package/src/repeats.ts CHANGED
@@ -35,6 +35,7 @@ const EQUAL = [
35
35
  'relief',
36
36
  'emissiveColor',
37
37
  'channel',
38
+ 'layers',
38
39
  ] as const;
39
40
 
40
41
  /**
package/src/weld.ts CHANGED
@@ -64,6 +64,12 @@ const ATTRIBUTES = {
64
64
  * tip merged into the trunk it grows from would stop moving.
65
65
  */
66
66
  channel: 4,
67
+ /*
68
+ * The texture-array layer. In the key for the same reason as the channel: two vertices at one
69
+ * position on two faces wearing different images are two vertices, and welding them would put one
70
+ * face on the other's picture.
71
+ */
72
+ layers: 1,
67
73
  } as const satisfies Record<PerVertexAttribute, number>;
68
74
 
69
75
  /**