@threenative/core 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/dist/{audio-3vkjtiuo.d.ts → audio-CEAw0w5y.d.ts} +9 -9
- package/dist/game-DRt1Qhq3.d.ts +429 -0
- package/dist/hot.d.ts +17 -5
- package/dist/index.d.ts +95 -39
- package/dist/index.js +1841 -157
- package/dist/playtest.d.ts +26 -4
- package/dist/playtest.js +161 -23
- package/package.json +3 -2
- package/dist/game-DRPs3M7r.d.ts +0 -269
package/dist/index.js
CHANGED
|
@@ -1,9 +1,86 @@
|
|
|
1
|
-
import { AudioListener, Audio, PositionalAudio, Object3D,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { AnimationMixer, AudioListener, Audio, PositionalAudio, Object3D, Scene, OrthographicCamera, Sprite, Raycaster, Vector2, Mesh, CatmullRomCurve3, PerspectiveCamera, Plane, Vector3, Matrix4, Uint32BufferAttribute, Matrix3, Float32BufferAttribute, Texture } from 'three';
|
|
2
|
+
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
3
|
+
import { instancedArray, attribute, storage, vec4, positionLocal, normalLocal } from 'three/tsl';
|
|
4
|
+
import { StorageBufferAttribute, RenderPipeline } from 'three/webgpu';
|
|
5
|
+
import { MeshBVH } from 'three-mesh-bvh';
|
|
4
6
|
import { createStore } from 'zustand/vanilla';
|
|
5
7
|
|
|
6
|
-
// src/
|
|
8
|
+
// src/animation.ts
|
|
9
|
+
var AnimationPlayer = class {
|
|
10
|
+
mixer;
|
|
11
|
+
#actions = /* @__PURE__ */ new Map();
|
|
12
|
+
#current;
|
|
13
|
+
#advancedFrames = 0;
|
|
14
|
+
#fadeFrom;
|
|
15
|
+
#fadeElapsed = 0;
|
|
16
|
+
#fadeDuration = 0;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.mixer = new AnimationMixer(options.root);
|
|
19
|
+
for (const clip of options.clips) {
|
|
20
|
+
if (this.#actions.has(clip.name)) throw new Error(`Duplicate animation clip '${clip.name}'.`);
|
|
21
|
+
const action = this.mixer.clipAction(clip);
|
|
22
|
+
if (action === null)
|
|
23
|
+
throw new Error(`Animation clip '${clip.name}' could not create an action.`);
|
|
24
|
+
this.#actions.set(clip.name, action);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get current() {
|
|
28
|
+
return this.#current;
|
|
29
|
+
}
|
|
30
|
+
get advancedFrames() {
|
|
31
|
+
return this.#advancedFrames;
|
|
32
|
+
}
|
|
33
|
+
play(name, options = {}) {
|
|
34
|
+
const next = this.#actions.get(name);
|
|
35
|
+
if (next === void 0) throw new Error(`Unknown animation clip '${name}'.`);
|
|
36
|
+
if (this.#current === name) return;
|
|
37
|
+
const previous = this.#current === void 0 ? void 0 : this.#actions.get(this.#current);
|
|
38
|
+
const fade = Math.max(0, options.fade ?? 0);
|
|
39
|
+
for (const action of this.#actions.values()) {
|
|
40
|
+
if (action !== previous) action.setEffectiveWeight(0).stop();
|
|
41
|
+
}
|
|
42
|
+
previous?.setEffectiveWeight(1).play();
|
|
43
|
+
next.reset().setEffectiveWeight(fade > 0 && previous !== void 0 ? 0 : 1).play();
|
|
44
|
+
if (previous === void 0 || fade === 0) {
|
|
45
|
+
previous?.stop();
|
|
46
|
+
this.#fadeFrom = void 0;
|
|
47
|
+
} else {
|
|
48
|
+
previous.play();
|
|
49
|
+
this.#fadeFrom = previous;
|
|
50
|
+
this.#fadeElapsed = 0;
|
|
51
|
+
this.#fadeDuration = fade;
|
|
52
|
+
}
|
|
53
|
+
this.#current = name;
|
|
54
|
+
this.#advancedFrames = 0;
|
|
55
|
+
}
|
|
56
|
+
update(dt) {
|
|
57
|
+
if (!Number.isFinite(dt) || dt < 0)
|
|
58
|
+
throw new Error("AnimationPlayer.update requires a finite non-negative dt.");
|
|
59
|
+
const before = this.mixer.time;
|
|
60
|
+
this.mixer.update(dt);
|
|
61
|
+
if (this.#fadeFrom !== void 0) {
|
|
62
|
+
this.#fadeElapsed = Math.min(this.#fadeDuration, this.#fadeElapsed + dt);
|
|
63
|
+
const progress = this.#fadeDuration === 0 ? 1 : this.#fadeElapsed / this.#fadeDuration;
|
|
64
|
+
this.#fadeFrom.setEffectiveWeight(1 - progress);
|
|
65
|
+
this.#actions.get(this.#current ?? "")?.setEffectiveWeight(progress);
|
|
66
|
+
if (progress >= 1) {
|
|
67
|
+
this.#fadeFrom.stop();
|
|
68
|
+
this.#fadeFrom = void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (this.#current !== void 0 && this.mixer.time !== before) this.#advancedFrames += 1;
|
|
72
|
+
}
|
|
73
|
+
stop() {
|
|
74
|
+
this.mixer.stopAllAction();
|
|
75
|
+
this.#current = void 0;
|
|
76
|
+
this.#advancedFrames = 0;
|
|
77
|
+
this.#fadeFrom = void 0;
|
|
78
|
+
}
|
|
79
|
+
dispose() {
|
|
80
|
+
this.stop();
|
|
81
|
+
this.mixer.uncacheRoot(this.mixer.getRoot());
|
|
82
|
+
}
|
|
83
|
+
};
|
|
7
84
|
var buses = /* @__PURE__ */ new Set();
|
|
8
85
|
var AudioBus = class {
|
|
9
86
|
listener;
|
|
@@ -59,6 +136,8 @@ var AudioBus = class {
|
|
|
59
136
|
}
|
|
60
137
|
playAt(buffer, source, options = {}) {
|
|
61
138
|
assertBuffer(buffer);
|
|
139
|
+
if (typeof this.listener.context.createPanner !== "function")
|
|
140
|
+
throw new Error("AudioBus.playAt needs createPanner(); this runtime has none.");
|
|
62
141
|
const voice = new PositionalAudio(this.listener);
|
|
63
142
|
configureVoice(voice, options);
|
|
64
143
|
voice.setBuffer(buffer);
|
|
@@ -134,29 +213,116 @@ function assertBuffer(buffer) {
|
|
|
134
213
|
if (buffer === null || buffer === void 0)
|
|
135
214
|
throw new TypeError("AudioBus requires a non-null AudioBuffer.");
|
|
136
215
|
}
|
|
216
|
+
var CanvasLayer = class {
|
|
217
|
+
scene = new Scene();
|
|
218
|
+
camera = new OrthographicCamera();
|
|
219
|
+
/** Declares that this layer covers the framebuffer, allowing the world pass to be skipped. */
|
|
220
|
+
opaque = false;
|
|
221
|
+
#stopResize;
|
|
222
|
+
constructor(viewport) {
|
|
223
|
+
this.camera.position.z = 1;
|
|
224
|
+
this.#resize(viewport.size);
|
|
225
|
+
this.#stopResize = viewport.onResize(this.#resize);
|
|
226
|
+
}
|
|
227
|
+
dispose() {
|
|
228
|
+
this.#stopResize();
|
|
229
|
+
this.#stopResize = () => void 0;
|
|
230
|
+
this.scene.clear();
|
|
231
|
+
}
|
|
232
|
+
#resize = ({ height, width }) => {
|
|
233
|
+
this.camera.left = -width / 2;
|
|
234
|
+
this.camera.right = width / 2;
|
|
235
|
+
this.camera.top = height / 2;
|
|
236
|
+
this.camera.bottom = -height / 2;
|
|
237
|
+
this.camera.near = 0;
|
|
238
|
+
this.camera.far = 2;
|
|
239
|
+
this.camera.updateProjectionMatrix();
|
|
240
|
+
};
|
|
241
|
+
};
|
|
137
242
|
|
|
138
|
-
// src/
|
|
243
|
+
// src/random.ts
|
|
244
|
+
function createRandom(seed) {
|
|
245
|
+
if (seed !== void 0 && !Number.isFinite(seed)) throw new TypeError("seed must be finite.");
|
|
246
|
+
let state = seed === void 0 ? 0 : seed >>> 0;
|
|
247
|
+
const deterministic = seed !== void 0;
|
|
248
|
+
const random = (() => {
|
|
249
|
+
if (!deterministic) return Math.random();
|
|
250
|
+
state = Math.imul(1664525, state) + 1013904223 >>> 0;
|
|
251
|
+
return state / 4294967296;
|
|
252
|
+
});
|
|
253
|
+
random.range = (min, max) => {
|
|
254
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
255
|
+
throw new RangeError("range requires finite bounds with max greater than min.");
|
|
256
|
+
}
|
|
257
|
+
return min + random() * (max - min);
|
|
258
|
+
};
|
|
259
|
+
random.pick = (items) => {
|
|
260
|
+
if (items.length === 0) throw new RangeError("pick requires a non-empty list.");
|
|
261
|
+
return items[Math.floor(random() * items.length)];
|
|
262
|
+
};
|
|
263
|
+
Object.defineProperty(random, "state", {
|
|
264
|
+
get: () => {
|
|
265
|
+
if (!deterministic) throw new Error("state is unavailable on an unseeded random.");
|
|
266
|
+
return state;
|
|
267
|
+
},
|
|
268
|
+
set: (value) => {
|
|
269
|
+
if (!deterministic) throw new Error("state is unavailable on an unseeded random.");
|
|
270
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
271
|
+
throw new TypeError("state must be a finite integer.");
|
|
272
|
+
}
|
|
273
|
+
state = value >>> 0;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
return random;
|
|
277
|
+
}
|
|
139
278
|
function resolvePath(basePath, path) {
|
|
140
279
|
if (/^(?:[a-z]+:)?\/\//iu.test(path) || path.startsWith("data:")) return path;
|
|
141
280
|
if (basePath.length === 0) return path;
|
|
142
281
|
return `${basePath.replace(/\/$/u, "")}/${path.replace(/^\//u, "")}`;
|
|
143
282
|
}
|
|
144
283
|
function loadWith(loader, url) {
|
|
145
|
-
return new Promise((resolve, reject) =>
|
|
146
|
-
loader.load(url, resolve, void 0, reject);
|
|
147
|
-
});
|
|
284
|
+
return new Promise((resolve, reject) => loader.load(url, resolve, void 0, reject));
|
|
148
285
|
}
|
|
149
286
|
function createAssetLoader(options = {}) {
|
|
150
287
|
const basePath = options.basePath ?? "";
|
|
151
288
|
const cache = /* @__PURE__ */ new Map();
|
|
289
|
+
const disposed = {
|
|
290
|
+
geometries: /* @__PURE__ */ new WeakSet(),
|
|
291
|
+
surfaces: /* @__PURE__ */ new WeakSet(),
|
|
292
|
+
textures: /* @__PURE__ */ new WeakSet()
|
|
293
|
+
};
|
|
152
294
|
const cached = (kind, path, load) => {
|
|
153
295
|
const url = resolvePath(basePath, path);
|
|
154
296
|
const key = `${kind}:${url}`;
|
|
155
297
|
const existing = cache.get(key);
|
|
156
|
-
if (existing !== void 0) return existing;
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
298
|
+
if (existing !== void 0) return existing.promise;
|
|
299
|
+
const entry = {
|
|
300
|
+
disposed: false,
|
|
301
|
+
kind,
|
|
302
|
+
loaded: false,
|
|
303
|
+
promise: Promise.resolve().then(() => load(url)),
|
|
304
|
+
released: false
|
|
305
|
+
};
|
|
306
|
+
entry.promise = entry.promise.then((value) => {
|
|
307
|
+
entry.loaded = true;
|
|
308
|
+
entry.value = value;
|
|
309
|
+
if (entry.released) disposeEntry(entry, disposed);
|
|
310
|
+
return value;
|
|
311
|
+
});
|
|
312
|
+
entry.promise.catch(() => {
|
|
313
|
+
if (cache.get(key) === entry) cache.delete(key);
|
|
314
|
+
});
|
|
315
|
+
cache.set(key, entry);
|
|
316
|
+
return entry.promise;
|
|
317
|
+
};
|
|
318
|
+
const releaseEntry = (entry) => {
|
|
319
|
+
entry.released = true;
|
|
320
|
+
if (entry.loaded) disposeEntry(entry, disposed);
|
|
321
|
+
else
|
|
322
|
+
void entry.promise.then(
|
|
323
|
+
() => disposeEntry(entry, disposed),
|
|
324
|
+
() => void 0
|
|
325
|
+
);
|
|
160
326
|
};
|
|
161
327
|
return {
|
|
162
328
|
audio: (path) => cached("audio", path, async (url) => {
|
|
@@ -164,93 +330,1285 @@ function createAssetLoader(options = {}) {
|
|
|
164
330
|
const { AudioLoader: Loader } = await import('three');
|
|
165
331
|
return loadWith(new Loader(), url);
|
|
166
332
|
}),
|
|
167
|
-
clear: () =>
|
|
333
|
+
clear: () => {
|
|
334
|
+
const entries = [...cache.values()];
|
|
335
|
+
cache.clear();
|
|
336
|
+
entries.forEach(releaseEntry);
|
|
337
|
+
},
|
|
168
338
|
model: (path) => cached("model", path, async (url) => {
|
|
169
339
|
if (options.model !== void 0) return await options.model(url);
|
|
170
340
|
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
|
|
171
341
|
return await loadWith(new GLTFLoader(), url);
|
|
172
342
|
}),
|
|
173
|
-
release: (kind, path) =>
|
|
343
|
+
release: (kind, path) => {
|
|
344
|
+
const key = `${kind}:${resolvePath(basePath, path)}`;
|
|
345
|
+
const entry = cache.get(key);
|
|
346
|
+
if (entry === void 0) return false;
|
|
347
|
+
cache.delete(key);
|
|
348
|
+
releaseEntry(entry);
|
|
349
|
+
return true;
|
|
350
|
+
},
|
|
174
351
|
texture: (path) => cached("texture", path, async (url) => {
|
|
175
352
|
if (options.texture !== void 0) return options.texture(url);
|
|
353
|
+
if (typeof Image === "undefined" && typeof createImageBitmap === "function") {
|
|
354
|
+
const response = await fetch(url);
|
|
355
|
+
if (!response.ok) throw new Error(`Failed to load texture '${url}': ${response.status}.`);
|
|
356
|
+
const bitmap = await createImageBitmap(new Blob([await response.arrayBuffer()]));
|
|
357
|
+
return Object.assign(new Texture(bitmap), { needsUpdate: true });
|
|
358
|
+
}
|
|
176
359
|
const { TextureLoader: Loader } = await import('three');
|
|
177
360
|
return loadWith(new Loader(), url);
|
|
178
361
|
})
|
|
179
362
|
};
|
|
180
363
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
364
|
+
function disposeEntry(entry, disposed) {
|
|
365
|
+
if (entry.disposed || !entry.loaded) return;
|
|
366
|
+
entry.disposed = true;
|
|
367
|
+
if (entry.kind === "model") disposeModel(entry.value, disposed);
|
|
368
|
+
else if (entry.kind === "texture") disposeTexture(entry.value, disposed);
|
|
369
|
+
}
|
|
370
|
+
function disposeModel(value, disposed) {
|
|
371
|
+
for (const root of modelRoots(value)) {
|
|
372
|
+
root.traverse((object) => {
|
|
373
|
+
const renderable = object;
|
|
374
|
+
disposeResource(renderable.geometry, disposed.geometries);
|
|
375
|
+
disposeSurface(renderable["material"], disposed);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function modelRoots(value) {
|
|
380
|
+
if (value instanceof Object3D) return [value];
|
|
381
|
+
if (!isRecord(value)) return [];
|
|
382
|
+
const roots = [value.scene, ...Array.isArray(value.scenes) ? value.scenes : []];
|
|
383
|
+
return roots.filter((root) => root instanceof Object3D);
|
|
384
|
+
}
|
|
385
|
+
function disposeSurface(value, disposed) {
|
|
386
|
+
if (Array.isArray(value)) {
|
|
387
|
+
for (const surface of value) disposeSurface(surface, disposed);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!isRecord(value)) return;
|
|
391
|
+
if (disposed.surfaces.has(value)) return;
|
|
392
|
+
disposeSurfaceTextures(value, disposed, /* @__PURE__ */ new WeakSet());
|
|
393
|
+
disposeResource(value, disposed.surfaces);
|
|
394
|
+
}
|
|
395
|
+
function disposeSurfaceTextures(value, disposed, visited) {
|
|
396
|
+
if (isTexture(value)) {
|
|
397
|
+
disposeTexture(value, disposed);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (typeof value !== "object" || value === null || visited.has(value)) return;
|
|
401
|
+
visited.add(value);
|
|
402
|
+
if (Array.isArray(value)) {
|
|
403
|
+
for (const item of value) disposeSurfaceTextures(item, disposed, visited);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
for (const item of Object.values(value)) disposeSurfaceTextures(item, disposed, visited);
|
|
407
|
+
}
|
|
408
|
+
function disposeTexture(value, disposed) {
|
|
409
|
+
if (!isDisposable(value) || disposed.textures.has(value)) return;
|
|
410
|
+
disposed.textures.add(value);
|
|
411
|
+
value.dispose();
|
|
412
|
+
}
|
|
413
|
+
function disposeResource(value, disposed) {
|
|
414
|
+
if (!isDisposable(value) || disposed.has(value)) return;
|
|
415
|
+
disposed.add(value);
|
|
416
|
+
value.dispose();
|
|
417
|
+
}
|
|
418
|
+
function isDisposable(value) {
|
|
419
|
+
return typeof value === "object" && value !== null && typeof value.dispose === "function";
|
|
420
|
+
}
|
|
421
|
+
function isTexture(value) {
|
|
422
|
+
return value instanceof Texture || isRecord(value) && value.isTexture === true;
|
|
423
|
+
}
|
|
424
|
+
function isRecord(value) {
|
|
425
|
+
return typeof value === "object" && value !== null;
|
|
426
|
+
}
|
|
427
|
+
var CANONICAL_ATTRIBUTES = ["position", "normal", "uv", "color", "tnOwnerId"];
|
|
428
|
+
var OVERLAY_FLOOR = 12;
|
|
429
|
+
function emptySkippedCounts() {
|
|
430
|
+
return {
|
|
431
|
+
belowMeshFloor: 0,
|
|
432
|
+
cameraOverlay: 0,
|
|
433
|
+
layers: 0,
|
|
434
|
+
renderOrder: 0,
|
|
435
|
+
renderHook: 0,
|
|
436
|
+
transparency: 0,
|
|
437
|
+
sprite: 0,
|
|
438
|
+
points: 0,
|
|
439
|
+
missingPosition: 0,
|
|
440
|
+
missingGeometry: 0,
|
|
441
|
+
missingMaterial: 0
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function addSkipped(skipped, reason, count = 1) {
|
|
445
|
+
skipped[reason] += count;
|
|
446
|
+
}
|
|
447
|
+
function mergeSkippedCounts(target, source) {
|
|
448
|
+
for (const reason of Object.keys(target))
|
|
449
|
+
target[reason] += source[reason];
|
|
450
|
+
}
|
|
451
|
+
function bakeTransform(geometry, matrix, normalMatrix) {
|
|
452
|
+
const position = geometry.getAttribute("position");
|
|
453
|
+
const positions = position?.array;
|
|
454
|
+
if (positions !== void 0) {
|
|
455
|
+
const m = matrix.elements;
|
|
456
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
457
|
+
const x = positions[index];
|
|
458
|
+
const y = positions[index + 1];
|
|
459
|
+
const z = positions[index + 2];
|
|
460
|
+
const w = 1 / (m[3] * x + m[7] * y + m[11] * z + m[15] || 1);
|
|
461
|
+
positions[index] = (m[0] * x + m[4] * y + m[8] * z + m[12]) * w;
|
|
462
|
+
positions[index + 1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) * w;
|
|
463
|
+
positions[index + 2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) * w;
|
|
197
464
|
}
|
|
198
465
|
}
|
|
199
|
-
|
|
200
|
-
|
|
466
|
+
const normal = geometry.getAttribute("normal");
|
|
467
|
+
const normals = normal?.array;
|
|
468
|
+
if (normals !== void 0) {
|
|
469
|
+
const n = normalMatrix.elements;
|
|
470
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
471
|
+
const x = normals[index];
|
|
472
|
+
const y = normals[index + 1];
|
|
473
|
+
const z = normals[index + 2];
|
|
474
|
+
const nx = n[0] * x + n[3] * y + n[6] * z;
|
|
475
|
+
const ny = n[1] * x + n[4] * y + n[7] * z;
|
|
476
|
+
const nz = n[2] * x + n[5] * y + n[8] * z;
|
|
477
|
+
const length = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
478
|
+
normals[index] = nx / length;
|
|
479
|
+
normals[index + 1] = ny / length;
|
|
480
|
+
normals[index + 2] = nz / length;
|
|
481
|
+
}
|
|
201
482
|
}
|
|
202
|
-
|
|
203
|
-
|
|
483
|
+
}
|
|
484
|
+
function containsLight(object) {
|
|
485
|
+
let found = false;
|
|
486
|
+
object.traverse((child) => {
|
|
487
|
+
if (child.isLight === true) found = true;
|
|
488
|
+
});
|
|
489
|
+
return found;
|
|
490
|
+
}
|
|
491
|
+
function containsPreserved(object, preserved) {
|
|
492
|
+
let found = false;
|
|
493
|
+
object.traverse((child) => {
|
|
494
|
+
if (preserved.has(child)) found = true;
|
|
495
|
+
});
|
|
496
|
+
return found;
|
|
497
|
+
}
|
|
498
|
+
var objectKeys = /* @__PURE__ */ new WeakMap();
|
|
499
|
+
function objectKey(value) {
|
|
500
|
+
const cached = objectKeys.get(value);
|
|
501
|
+
if (cached !== void 0) return cached;
|
|
502
|
+
const isColor = value.isColor === true;
|
|
503
|
+
let key = isColor ? `rgb:${value.r},${value.g},${value.b}` : String(value.uuid ?? "obj");
|
|
504
|
+
const image = value.image;
|
|
505
|
+
const data = image?.data;
|
|
506
|
+
if (data !== void 0 && typeof data.length === "number") {
|
|
507
|
+
let hash = 2166136261;
|
|
508
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
509
|
+
hash ^= (data[index] ?? 0) & 255;
|
|
510
|
+
hash = Math.imul(hash, 16777619);
|
|
511
|
+
}
|
|
512
|
+
key = `tex:${image?.width}x${image?.height}:${data.length}:${(hash >>> 0).toString(36)}`;
|
|
513
|
+
for (const field of [
|
|
514
|
+
"wrapS",
|
|
515
|
+
"wrapT",
|
|
516
|
+
"magFilter",
|
|
517
|
+
"minFilter",
|
|
518
|
+
"format",
|
|
519
|
+
"type",
|
|
520
|
+
"colorSpace",
|
|
521
|
+
"flipY"
|
|
522
|
+
]) {
|
|
523
|
+
key += `:${String(value[field])}`;
|
|
524
|
+
}
|
|
204
525
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
526
|
+
objectKeys.set(value, key);
|
|
527
|
+
return key;
|
|
528
|
+
}
|
|
529
|
+
function colorOf(material) {
|
|
530
|
+
const color = material.color;
|
|
531
|
+
return color === void 0 || typeof color.r !== "number" ? void 0 : color;
|
|
532
|
+
}
|
|
533
|
+
var LOOK_FIELDS = [
|
|
534
|
+
"alphaHash",
|
|
535
|
+
"alphaMap",
|
|
536
|
+
"alphaTest",
|
|
537
|
+
"alphaToCoverage",
|
|
538
|
+
"aoMap",
|
|
539
|
+
"aoMapIntensity",
|
|
540
|
+
"attenuationColor",
|
|
541
|
+
"attenuationDistance",
|
|
542
|
+
"blendAlpha",
|
|
543
|
+
"blendDst",
|
|
544
|
+
"blendEquation",
|
|
545
|
+
"blendSrc",
|
|
546
|
+
"blending",
|
|
547
|
+
"bumpMap",
|
|
548
|
+
"bumpScale",
|
|
549
|
+
"clearcoat",
|
|
550
|
+
"clearcoatMap",
|
|
551
|
+
"clearcoatRoughness",
|
|
552
|
+
"clipIntersection",
|
|
553
|
+
"clipShadows",
|
|
554
|
+
"clippingPlanes",
|
|
555
|
+
"colorWrite",
|
|
556
|
+
"combine",
|
|
557
|
+
"depthFunc",
|
|
558
|
+
"depthTest",
|
|
559
|
+
"depthWrite",
|
|
560
|
+
"displacementBias",
|
|
561
|
+
"displacementMap",
|
|
562
|
+
"displacementScale",
|
|
563
|
+
"dithering",
|
|
564
|
+
"emissive",
|
|
565
|
+
"emissiveIntensity",
|
|
566
|
+
"emissiveMap",
|
|
567
|
+
"envMap",
|
|
568
|
+
"envMapIntensity",
|
|
569
|
+
"flatShading",
|
|
570
|
+
"fog",
|
|
571
|
+
"forceSinglePass",
|
|
572
|
+
"gradientMap",
|
|
573
|
+
"ior",
|
|
574
|
+
"iridescence",
|
|
575
|
+
"lightMap",
|
|
576
|
+
"lightMapIntensity",
|
|
577
|
+
"map",
|
|
578
|
+
"metalness",
|
|
579
|
+
"metalnessMap",
|
|
580
|
+
"normalMap",
|
|
581
|
+
"normalMapType",
|
|
582
|
+
"normalScale",
|
|
583
|
+
"opacity",
|
|
584
|
+
"polygonOffset",
|
|
585
|
+
"polygonOffsetFactor",
|
|
586
|
+
"polygonOffsetUnits",
|
|
587
|
+
"premultipliedAlpha",
|
|
588
|
+
"reflectivity",
|
|
589
|
+
"refractionRatio",
|
|
590
|
+
"roughness",
|
|
591
|
+
"roughnessMap",
|
|
592
|
+
"sheen",
|
|
593
|
+
"sheenColor",
|
|
594
|
+
"shadowSide",
|
|
595
|
+
"shininess",
|
|
596
|
+
"side",
|
|
597
|
+
"specular",
|
|
598
|
+
"specularMap",
|
|
599
|
+
"stencilWrite",
|
|
600
|
+
"toneMapped",
|
|
601
|
+
"transmission",
|
|
602
|
+
"transparent",
|
|
603
|
+
"vertexColors",
|
|
604
|
+
"visible",
|
|
605
|
+
"wireframe"
|
|
606
|
+
];
|
|
607
|
+
function materialSignature(material) {
|
|
608
|
+
const source = material;
|
|
609
|
+
const parts = [material.type];
|
|
610
|
+
for (const key of LOOK_FIELDS) {
|
|
611
|
+
const value = source[key];
|
|
612
|
+
if (value === void 0) continue;
|
|
613
|
+
if (value === null) {
|
|
614
|
+
parts.push(`${key}:none`);
|
|
615
|
+
continue;
|
|
213
616
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
617
|
+
parts.push(
|
|
618
|
+
typeof value === "object" ? `${key}:${objectKey(value)}` : `${key}:${String(value)}`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return parts.join("|");
|
|
622
|
+
}
|
|
623
|
+
function materialOf(object) {
|
|
624
|
+
const material = object.material;
|
|
625
|
+
if (material === void 0) return void 0;
|
|
626
|
+
return Array.isArray(material) ? material[0] : material;
|
|
627
|
+
}
|
|
628
|
+
function writeNormalMatrix(target, offset, source) {
|
|
629
|
+
const e = source.elements;
|
|
630
|
+
target[offset] = e[0] ?? 0;
|
|
631
|
+
target[offset + 1] = e[1] ?? 0;
|
|
632
|
+
target[offset + 2] = e[2] ?? 0;
|
|
633
|
+
target[offset + 3] = 0;
|
|
634
|
+
target[offset + 4] = e[3] ?? 0;
|
|
635
|
+
target[offset + 5] = e[4] ?? 0;
|
|
636
|
+
target[offset + 6] = e[5] ?? 0;
|
|
637
|
+
target[offset + 7] = 0;
|
|
638
|
+
target[offset + 8] = e[6] ?? 0;
|
|
639
|
+
target[offset + 9] = e[7] ?? 0;
|
|
640
|
+
target[offset + 10] = e[8] ?? 0;
|
|
641
|
+
target[offset + 11] = 0;
|
|
642
|
+
target[offset + 15] = 1;
|
|
643
|
+
}
|
|
644
|
+
function hasUniformScale(matrix) {
|
|
645
|
+
const e = matrix.elements;
|
|
646
|
+
const x = Math.hypot(e[0] ?? 0, e[1] ?? 0, e[2] ?? 0);
|
|
647
|
+
const y = Math.hypot(e[4] ?? 0, e[5] ?? 0, e[6] ?? 0);
|
|
648
|
+
const z = Math.hypot(e[8] ?? 0, e[9] ?? 0, e[10] ?? 0);
|
|
649
|
+
const largest = Math.max(x, y, z);
|
|
650
|
+
if (largest === 0) return true;
|
|
651
|
+
return Math.max(Math.abs(x - y), Math.abs(y - z), Math.abs(x - z)) / largest < 1e-4;
|
|
652
|
+
}
|
|
653
|
+
function writeNormals(target, part, scratch) {
|
|
654
|
+
const offset = part.index * 16;
|
|
655
|
+
const source = part.detachedLeaf ? part.object.matrix : part.object.matrixWorld;
|
|
656
|
+
if (part.uniformScale) {
|
|
657
|
+
const e = source.elements;
|
|
658
|
+
target[offset] = e[0] ?? 0;
|
|
659
|
+
target[offset + 1] = e[1] ?? 0;
|
|
660
|
+
target[offset + 2] = e[2] ?? 0;
|
|
661
|
+
target[offset + 4] = e[4] ?? 0;
|
|
662
|
+
target[offset + 5] = e[5] ?? 0;
|
|
663
|
+
target[offset + 6] = e[6] ?? 0;
|
|
664
|
+
target[offset + 8] = e[8] ?? 0;
|
|
665
|
+
target[offset + 9] = e[9] ?? 0;
|
|
666
|
+
target[offset + 10] = e[10] ?? 0;
|
|
667
|
+
target[offset + 15] = 1;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
writeNormalMatrix(target, offset, scratch.getNormalMatrix(source));
|
|
671
|
+
}
|
|
672
|
+
function sameMatrix(a, b) {
|
|
673
|
+
for (let index = 0; index < a.length; index += 1) if (a[index] !== b[index]) return false;
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
function transformSample(object) {
|
|
677
|
+
const { position, quaternion, scale } = object;
|
|
678
|
+
if (position === void 0 || quaternion === void 0 || scale === void 0) {
|
|
679
|
+
return Float64Array.from(object.matrix.elements);
|
|
680
|
+
}
|
|
681
|
+
return Float64Array.of(
|
|
682
|
+
position.x,
|
|
683
|
+
position.y,
|
|
684
|
+
position.z,
|
|
685
|
+
quaternion.x,
|
|
686
|
+
quaternion.y,
|
|
687
|
+
quaternion.z,
|
|
688
|
+
quaternion.w,
|
|
689
|
+
scale.x,
|
|
690
|
+
scale.y,
|
|
691
|
+
scale.z
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
function sharedAttributeNames(chunks) {
|
|
695
|
+
let shared;
|
|
696
|
+
for (const chunk of chunks) {
|
|
697
|
+
const names = Object.keys(chunk.attributes);
|
|
698
|
+
shared = shared === void 0 ? names : shared.filter((name) => names.includes(name));
|
|
699
|
+
}
|
|
700
|
+
return shared ?? [];
|
|
701
|
+
}
|
|
702
|
+
var SceneCollapse = class {
|
|
703
|
+
#scene;
|
|
704
|
+
#observeFrames;
|
|
705
|
+
#minMeshes;
|
|
706
|
+
#onReport;
|
|
707
|
+
#samples = /* @__PURE__ */ new Map();
|
|
708
|
+
#moving = /* @__PURE__ */ new Set();
|
|
709
|
+
#changes = /* @__PURE__ */ new Map();
|
|
710
|
+
#observed = 0;
|
|
711
|
+
#windowOpenedAt;
|
|
712
|
+
#sampleSpentMs = 0;
|
|
713
|
+
#mergeSpentMs = 0;
|
|
714
|
+
#copySpentMs = 0;
|
|
715
|
+
#transformSpentMs = 0;
|
|
716
|
+
#colorSpentMs = 0;
|
|
717
|
+
#prepSpentMs = 0;
|
|
718
|
+
#observeSpanMs = 0;
|
|
719
|
+
#meshCount = -1;
|
|
720
|
+
#reportedSmall = false;
|
|
721
|
+
#lastScan;
|
|
722
|
+
#lastSourceRenderables = 0;
|
|
723
|
+
#lastSourceMaterialIdentities = 0;
|
|
724
|
+
#refreshCount = 0;
|
|
725
|
+
#refreshLastMs = 0;
|
|
726
|
+
#refreshMaxMs = 0;
|
|
727
|
+
#refreshTotalMs = 0;
|
|
728
|
+
#report;
|
|
729
|
+
#bakeBudgetMs;
|
|
730
|
+
#measureTransformRefresh;
|
|
731
|
+
#steps;
|
|
732
|
+
#bakeProgress = 0;
|
|
733
|
+
#bakeSpentMs = 0;
|
|
734
|
+
#settled;
|
|
735
|
+
#markSettled = () => void 0;
|
|
736
|
+
#restore;
|
|
737
|
+
#update;
|
|
738
|
+
/**
|
|
739
|
+
* Roots the bake lifted out of the scene, each with the child count it had when it left.
|
|
740
|
+
*
|
|
741
|
+
* The pass consumes a hierarchy and settles, but the game keeps its references and is entitled
|
|
742
|
+
* to go on using them — a level that streams its far half in after the loading screen adds to
|
|
743
|
+
* the very group this pass detached. Without this watch that geometry lands in a subtree no
|
|
744
|
+
* renderer walks and never draws, with no error to notice: the map is simply missing a piece.
|
|
745
|
+
*/
|
|
746
|
+
#adoptable = [];
|
|
747
|
+
#adopted = 0;
|
|
748
|
+
constructor(scene, options = {}) {
|
|
749
|
+
const observeFrames = options.observeFrames ?? 8;
|
|
750
|
+
const minMeshes = options.minMeshes ?? 200;
|
|
751
|
+
const bakeBudgetMs = options.bakeBudgetMs ?? 100;
|
|
752
|
+
if (!Number.isInteger(observeFrames) || observeFrames < 1)
|
|
753
|
+
throw new Error("SceneCollapse.observeFrames must be a positive integer.");
|
|
754
|
+
if (!Number.isInteger(minMeshes) || minMeshes < 1)
|
|
755
|
+
throw new Error("SceneCollapse.minMeshes must be a positive integer.");
|
|
756
|
+
if (!(bakeBudgetMs > 0) || !Number.isFinite(bakeBudgetMs))
|
|
757
|
+
throw new Error("SceneCollapse.bakeBudgetMs must be a positive number.");
|
|
758
|
+
this.#scene = scene;
|
|
759
|
+
this.#observeFrames = observeFrames;
|
|
760
|
+
this.#minMeshes = minMeshes;
|
|
761
|
+
this.#bakeBudgetMs = bakeBudgetMs;
|
|
762
|
+
this.#measureTransformRefresh = options.measureTransformRefresh === true;
|
|
763
|
+
this.#settled = new Promise((resolve) => {
|
|
764
|
+
this.#markSettled = resolve;
|
|
765
|
+
});
|
|
766
|
+
const report = options.onReport ?? ((value) => {
|
|
767
|
+
console.info(`TN_SCENE_COLLAPSE:${JSON.stringify(value)}`);
|
|
768
|
+
});
|
|
769
|
+
this.#onReport = (value) => {
|
|
770
|
+
report(value);
|
|
771
|
+
this.#markSettled();
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Detached roots the pass has put back because the game added to them after the collapse.
|
|
776
|
+
*
|
|
777
|
+
* Non-zero means a game is still building into a hierarchy this pass consumed. That is
|
|
778
|
+
* supported, not a fault, but it is worth seeing: each adoption gives one root its per-frame
|
|
779
|
+
* traversal back.
|
|
780
|
+
*/
|
|
781
|
+
get adoptedRoots() {
|
|
782
|
+
return this.#adopted;
|
|
783
|
+
}
|
|
784
|
+
/** The pass has reached a verdict — collapsed or declined. */
|
|
785
|
+
get settled() {
|
|
786
|
+
return this.#report !== void 0;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* How far through its observation window the pass is, 0 to 1.
|
|
790
|
+
*
|
|
791
|
+
* Real progress, not a guess, for the only part of startup whose length is known in advance. The
|
|
792
|
+
* collapse itself is one blocking call and cannot report from inside; a bar driven by this
|
|
793
|
+
* fills, holds while the bake runs, and completes.
|
|
794
|
+
*/
|
|
795
|
+
get progress() {
|
|
796
|
+
if (this.#report !== void 0) return 1;
|
|
797
|
+
const observing = Math.min(1, this.#observed / this.#observeFrames);
|
|
798
|
+
return this.#steps === void 0 && this.#bakeProgress === 0 ? observing * 0.5 : 0.5 + this.#bakeProgress * 0.5;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Resolves once this pass has said something, whatever it said.
|
|
802
|
+
*
|
|
803
|
+
* A game needs this to know when it is safe to show the player the world. Without it the launch
|
|
804
|
+
* looks like the one the operator reported: a half-drawn map for several seconds, because each
|
|
805
|
+
* shader compiles the first time something using it is drawn, and then a multi-second stall when
|
|
806
|
+
* the collapse lands. Both costs are real and have to be paid somewhere; a game that can wait on
|
|
807
|
+
* this pays them behind a loading screen instead of in front of the player.
|
|
808
|
+
*
|
|
809
|
+
* It resolves on the *first* report, including the one that declines a scene too small to be
|
|
810
|
+
* worth collapsing — otherwise a small game would wait forever for a pass that is never going to
|
|
811
|
+
* run.
|
|
812
|
+
*/
|
|
813
|
+
whenSettled() {
|
|
814
|
+
return this.#settled;
|
|
815
|
+
}
|
|
816
|
+
get report() {
|
|
817
|
+
return this.#report;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Latest bounded diagnostics snapshot. `report` is the immutable outcome recorded when the pass
|
|
821
|
+
* applied/rejected/deferred; this getter is for per-frame transform-refresh timing after an
|
|
822
|
+
* applied collapse continues updating moving parts.
|
|
823
|
+
*/
|
|
824
|
+
get diagnostics() {
|
|
825
|
+
const report = this.#report;
|
|
826
|
+
if (report === void 0) return void 0;
|
|
827
|
+
return { ...report.diagnostics, timings: this.#timings() };
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Call once per rendered frame. Samples local matrices until the observation window closes, then
|
|
831
|
+
* collapses; afterwards it refreshes the moving parts' transforms.
|
|
832
|
+
*/
|
|
833
|
+
frame() {
|
|
834
|
+
if (this.#report !== void 0) {
|
|
835
|
+
this.#refreshTransforms();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (this.#meshCount < this.#minMeshes) {
|
|
839
|
+
this.#meshCount = this.#countMeshes();
|
|
840
|
+
if (this.#meshCount < this.#minMeshes) {
|
|
841
|
+
if (!this.#reportedSmall) {
|
|
842
|
+
this.#reportedSmall = true;
|
|
843
|
+
this.#onReport(
|
|
844
|
+
this.#makeReport({
|
|
845
|
+
collapsed: false,
|
|
846
|
+
reason: `fewer than ${this.#minMeshes} meshes so far; still watching`,
|
|
847
|
+
reasonCode: "belowMeshFloorStillWatching",
|
|
848
|
+
resultDrawCandidates: 0,
|
|
849
|
+
resultMaterialIdentities: 0,
|
|
850
|
+
skipped: {
|
|
851
|
+
...emptySkippedCounts(),
|
|
852
|
+
...this.#lastScan?.skipped ?? {},
|
|
853
|
+
belowMeshFloor: this.#meshCount
|
|
854
|
+
},
|
|
855
|
+
sourceMaterialIdentities: this.#lastSourceMaterialIdentities,
|
|
856
|
+
sourceMeshes: this.#meshCount,
|
|
857
|
+
sourceRenderables: this.#lastSourceRenderables || this.#meshCount,
|
|
858
|
+
status: "deferred"
|
|
859
|
+
})
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (this.#steps !== void 0) {
|
|
866
|
+
this.#pumpBake();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const now = () => globalThis.performance?.now() ?? 0;
|
|
870
|
+
this.#windowOpenedAt ??= now();
|
|
871
|
+
const sampledAt = now();
|
|
872
|
+
this.#sample();
|
|
873
|
+
this.#sampleSpentMs += now() - sampledAt;
|
|
874
|
+
this.#observed += 1;
|
|
875
|
+
if (this.#observed >= this.#observeFrames) {
|
|
876
|
+
this.#observeSpanMs = now() - this.#windowOpenedAt;
|
|
877
|
+
this.#steps = this.#collapseSteps();
|
|
878
|
+
this.#pumpBake();
|
|
224
879
|
}
|
|
225
|
-
this.#current = name;
|
|
226
|
-
this.#advancedFrames = 0;
|
|
227
880
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Spends this frame's budget on the bake.
|
|
883
|
+
*
|
|
884
|
+
* The budget is checked between meshes rather than enforced inside one, so a single enormous
|
|
885
|
+
* mesh can still overrun it — bounding that would mean splitting a geometry mid-transform, and
|
|
886
|
+
* a half-transformed geometry is a wrong picture rather than a slow one.
|
|
887
|
+
*/
|
|
888
|
+
#pumpBake() {
|
|
889
|
+
const steps = this.#steps;
|
|
890
|
+
if (steps === void 0) return;
|
|
891
|
+
const startedAt = globalThis.performance?.now() ?? 0;
|
|
892
|
+
const spend = () => {
|
|
893
|
+
this.#bakeSpentMs += (globalThis.performance?.now() ?? 0) - startedAt;
|
|
894
|
+
};
|
|
895
|
+
for (; ; ) {
|
|
896
|
+
const step = steps.next();
|
|
897
|
+
if (step.done === true) {
|
|
898
|
+
spend();
|
|
899
|
+
this.#steps = void 0;
|
|
900
|
+
this.#bakeProgress = 1;
|
|
901
|
+
if (this.#report === void 0) this.#bakeProgress = 0;
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
this.#bakeProgress = typeof step.value === "number" ? step.value : this.#bakeProgress;
|
|
905
|
+
if ((globalThis.performance?.now() ?? 0) - startedAt >= this.#bakeBudgetMs) {
|
|
906
|
+
spend();
|
|
907
|
+
return;
|
|
241
908
|
}
|
|
242
909
|
}
|
|
243
|
-
if (this.#current !== void 0 && this.mixer.time !== before) this.#advancedFrames += 1;
|
|
244
910
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.#
|
|
248
|
-
this.#
|
|
249
|
-
this.#
|
|
911
|
+
/** Put the original scene back. Used when a supposedly static object turns out to move. */
|
|
912
|
+
restore() {
|
|
913
|
+
this.#restore?.();
|
|
914
|
+
this.#restore = void 0;
|
|
915
|
+
this.#update = void 0;
|
|
916
|
+
this.#adoptable.length = 0;
|
|
917
|
+
}
|
|
918
|
+
#countMeshes() {
|
|
919
|
+
const scan = this.#scanSceneRenderables();
|
|
920
|
+
this.#lastScan = scan;
|
|
921
|
+
this.#lastSourceRenderables = scan.sourceRenderables;
|
|
922
|
+
this.#lastSourceMaterialIdentities = scan.materialIdentities.size;
|
|
923
|
+
return scan.meshes.length;
|
|
924
|
+
}
|
|
925
|
+
#skipReason(object) {
|
|
926
|
+
if (this.#excluded(object)) return "cameraOverlay";
|
|
927
|
+
if (object.layers !== void 0 && object.layers.mask !== 1) return "layers";
|
|
928
|
+
if ((object.renderOrder ?? 0) !== 0) return "renderOrder";
|
|
929
|
+
if (Object.hasOwn(object, "onBeforeRender") || Object.hasOwn(object, "onAfterRender"))
|
|
930
|
+
return "renderHook";
|
|
931
|
+
if (object.geometry === void 0) return "missingGeometry";
|
|
932
|
+
if (object.geometry.getAttribute("position") === void 0) return "missingPosition";
|
|
933
|
+
const material = materialOf(object);
|
|
934
|
+
if (material === void 0) return "missingMaterial";
|
|
935
|
+
if (material.transparent === true)
|
|
936
|
+
return "transparency";
|
|
937
|
+
return void 0;
|
|
250
938
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
939
|
+
#scanSceneRenderables() {
|
|
940
|
+
const skipped = emptySkippedCounts();
|
|
941
|
+
const meshes = [];
|
|
942
|
+
const preserved = /* @__PURE__ */ new Set();
|
|
943
|
+
const materialIdentities = /* @__PURE__ */ new Set();
|
|
944
|
+
let sourceRenderables = 0;
|
|
945
|
+
this.#scene.traverse((object) => {
|
|
946
|
+
if (object.isCamera === true) return;
|
|
947
|
+
if (object.isSprite === true) {
|
|
948
|
+
sourceRenderables += 1;
|
|
949
|
+
preserved.add(object);
|
|
950
|
+
addSkipped(skipped, "sprite");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (object.isPoints === true) {
|
|
954
|
+
sourceRenderables += 1;
|
|
955
|
+
preserved.add(object);
|
|
956
|
+
addSkipped(skipped, "points");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (object.isMesh !== true) return;
|
|
960
|
+
sourceRenderables += 1;
|
|
961
|
+
const material = materialOf(object);
|
|
962
|
+
if (material !== void 0) materialIdentities.add(material);
|
|
963
|
+
const reason = this.#skipReason(object);
|
|
964
|
+
if (reason !== void 0) {
|
|
965
|
+
preserved.add(object);
|
|
966
|
+
addSkipped(skipped, reason);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
meshes.push(object);
|
|
970
|
+
});
|
|
971
|
+
return { materialIdentities, meshes, preserved, skipped, sourceRenderables };
|
|
972
|
+
}
|
|
973
|
+
#sample() {
|
|
974
|
+
this.#scene.traverse((object) => {
|
|
975
|
+
if (object.isCamera === true) return;
|
|
976
|
+
const current = transformSample(object);
|
|
977
|
+
const previous = this.#samples.get(object);
|
|
978
|
+
if (previous === void 0) {
|
|
979
|
+
this.#samples.set(object, current);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (sameMatrix(previous, current)) return;
|
|
983
|
+
previous.set(current);
|
|
984
|
+
const changes = (this.#changes.get(object) ?? 0) + 1;
|
|
985
|
+
this.#changes.set(object, changes);
|
|
986
|
+
if (changes >= 2) this.#moving.add(object);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Folds each camera's own subtree — where every HUD and overlay lives — into one draw per
|
|
991
|
+
* material instance.
|
|
992
|
+
*
|
|
993
|
+
* The scene pass cannot touch these: it bakes world matrices, and a camera-parented mesh baked
|
|
994
|
+
* in world space strands itself wherever the camera happened to be standing. So it skips them,
|
|
995
|
+
* and on a real game that left the HUD as ~93 of ~110 draws, worth 11 ms of a 17 ms frame on a
|
|
996
|
+
* Pixel 8 — the whole gap between 57 fps and the budget, while the scene it did collapse cost
|
|
997
|
+
* 3.6 ms. This pass bakes in *camera* space instead and attaches the result to the camera, so
|
|
998
|
+
* the overlay rides along exactly as it did.
|
|
999
|
+
*
|
|
1000
|
+
* Three things a HUD does that a level does not, and each decides part of the design:
|
|
1001
|
+
*
|
|
1002
|
+
* - **Digits toggle `visible`.** A seven-segment counter hides segments rather than moving them,
|
|
1003
|
+
* and a 45-frame window will not see a clock tick. So nothing here is classified static:
|
|
1004
|
+
* every overlay mesh gets a transform slot, and an invisible one is pushed out of the frustum
|
|
1005
|
+
* the same way a hidden moving part is.
|
|
1006
|
+
* - **Hearts recolour their own material.** So groups are keyed by material *identity*, never
|
|
1007
|
+
* merged by look and never given baked vertex colours. The game keeps writing to the same
|
|
1008
|
+
* material object and the merged draw shows it.
|
|
1009
|
+
* - **The source meshes must keep updating.** They are parked on a render layer nothing draws
|
|
1010
|
+
* rather than detached, so Three.js still refreshes their world matrices and the game's own
|
|
1011
|
+
* `visible` writes still mean what they say.
|
|
1012
|
+
*/
|
|
1013
|
+
#collapseCameras() {
|
|
1014
|
+
let overlayMeshes = 0;
|
|
1015
|
+
let overlayDraws = 0;
|
|
1016
|
+
const skipped = emptySkippedCounts();
|
|
1017
|
+
const materialIdentities = /* @__PURE__ */ new Set();
|
|
1018
|
+
const cameras = [];
|
|
1019
|
+
this.#scene.traverse((object) => {
|
|
1020
|
+
if (object.isCamera === true) cameras.push(object);
|
|
1021
|
+
});
|
|
1022
|
+
for (const camera of cameras) {
|
|
1023
|
+
const meshes = [];
|
|
1024
|
+
camera.traverse((object) => {
|
|
1025
|
+
if (object === camera) return;
|
|
1026
|
+
if (object.isSprite === true) {
|
|
1027
|
+
addSkipped(skipped, "sprite");
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (object.isPoints === true) {
|
|
1031
|
+
addSkipped(skipped, "points");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (object.isMesh !== true) return;
|
|
1035
|
+
if (object.geometry?.getAttribute("position") === void 0) {
|
|
1036
|
+
addSkipped(skipped, "missingPosition");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (object.layers === void 0) {
|
|
1040
|
+
addSkipped(skipped, "layers");
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const material = materialOf(object);
|
|
1044
|
+
if (material !== void 0) materialIdentities.add(material);
|
|
1045
|
+
meshes.push(object);
|
|
1046
|
+
});
|
|
1047
|
+
if (meshes.length < OVERLAY_FLOOR) {
|
|
1048
|
+
addSkipped(skipped, "cameraOverlay", meshes.length);
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
camera.updateMatrixWorld(true);
|
|
1052
|
+
const cameraInverse = new Matrix4().copy(camera.matrixWorld).invert();
|
|
1053
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1054
|
+
const parts = [];
|
|
1055
|
+
let usable = true;
|
|
1056
|
+
for (const mesh of meshes) {
|
|
1057
|
+
const material = materialOf(mesh);
|
|
1058
|
+
const source = mesh.geometry;
|
|
1059
|
+
if (material === void 0 || source === void 0) {
|
|
1060
|
+
usable = false;
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
const index = parts.length;
|
|
1064
|
+
parts.push({ object: mesh, index });
|
|
1065
|
+
const geometry = source.index !== null ? source.toNonIndexed() : source.clone();
|
|
1066
|
+
for (const name of Object.keys(geometry.attributes)) {
|
|
1067
|
+
if (!CANONICAL_ATTRIBUTES.includes(name)) geometry.deleteAttribute(name);
|
|
1068
|
+
}
|
|
1069
|
+
if (geometry.getAttribute("normal") === void 0)
|
|
1070
|
+
geometry.computeVertexNormals();
|
|
1071
|
+
const count = geometry.getAttribute("position")?.count ?? 0;
|
|
1072
|
+
const ids = new Uint32Array(count);
|
|
1073
|
+
ids.fill(index);
|
|
1074
|
+
geometry.setAttribute("tnOwnerId", new Uint32BufferAttribute(ids, 1));
|
|
1075
|
+
const key = objectKey(material);
|
|
1076
|
+
const group = groups.get(key) ?? { material, chunks: [], renderOrder: 0 };
|
|
1077
|
+
group.renderOrder = Math.max(group.renderOrder, mesh.renderOrder ?? 0);
|
|
1078
|
+
group.chunks.push(geometry);
|
|
1079
|
+
groups.set(key, group);
|
|
1080
|
+
}
|
|
1081
|
+
if (!usable) continue;
|
|
1082
|
+
const slots = Math.max(1, parts.length);
|
|
1083
|
+
const transformData = new Float32Array(slots * 16);
|
|
1084
|
+
const normalData = new Float32Array(slots * 16);
|
|
1085
|
+
const transformAttribute = new StorageBufferAttribute(transformData, 16);
|
|
1086
|
+
const normalAttribute = new StorageBufferAttribute(normalData, 16);
|
|
1087
|
+
const ownerIdNode = attribute("tnOwnerId", "uint");
|
|
1088
|
+
const ownerMatrix = storage(transformAttribute, "mat4", slots).element(ownerIdNode);
|
|
1089
|
+
const ownerNormalMatrix = storage(normalAttribute, "mat4", slots).element(ownerIdNode);
|
|
1090
|
+
const added = [];
|
|
1091
|
+
const patched = [];
|
|
1092
|
+
let failed = false;
|
|
1093
|
+
for (const group of groups.values()) {
|
|
1094
|
+
const shared = sharedAttributeNames(group.chunks);
|
|
1095
|
+
if (!shared.includes("position") || !shared.includes("tnOwnerId")) {
|
|
1096
|
+
failed = true;
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
for (const chunk of group.chunks) {
|
|
1100
|
+
for (const name of Object.keys(chunk.attributes)) {
|
|
1101
|
+
if (!shared.includes(name)) chunk.deleteAttribute(name);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const overlayMergeStartedAt = globalThis.performance?.now() ?? 0;
|
|
1105
|
+
const geometry = mergeGeometries(group.chunks, false);
|
|
1106
|
+
this.#mergeSpentMs += (globalThis.performance?.now() ?? 0) - overlayMergeStartedAt;
|
|
1107
|
+
if (geometry === null) {
|
|
1108
|
+
failed = true;
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
const material = group.material;
|
|
1112
|
+
patched.push({
|
|
1113
|
+
material,
|
|
1114
|
+
position: material.positionNode,
|
|
1115
|
+
normal: material.normalNode
|
|
1116
|
+
});
|
|
1117
|
+
material.positionNode = ownerMatrix.mul(vec4(positionLocal, 1)).xyz;
|
|
1118
|
+
material.normalNode = ownerNormalMatrix.mul(vec4(normalLocal, 0)).xyz.normalize();
|
|
1119
|
+
const mesh = new Mesh(geometry, material);
|
|
1120
|
+
mesh.frustumCulled = false;
|
|
1121
|
+
mesh.renderOrder = group.renderOrder;
|
|
1122
|
+
camera.add(mesh);
|
|
1123
|
+
added.push(mesh);
|
|
1124
|
+
overlayDraws += 1;
|
|
1125
|
+
}
|
|
1126
|
+
if (failed) {
|
|
1127
|
+
for (const entry of patched) {
|
|
1128
|
+
entry.material.positionNode = entry.position;
|
|
1129
|
+
entry.material.normalNode = entry.normal;
|
|
1130
|
+
}
|
|
1131
|
+
for (const mesh of added) camera.remove(mesh);
|
|
1132
|
+
overlayDraws -= added.length;
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
const masks = /* @__PURE__ */ new Map();
|
|
1136
|
+
for (const part of parts) {
|
|
1137
|
+
const layers = part.object.layers;
|
|
1138
|
+
if (layers === void 0) continue;
|
|
1139
|
+
masks.set(part.object, layers.mask);
|
|
1140
|
+
layers.mask = 0;
|
|
1141
|
+
}
|
|
1142
|
+
overlayMeshes += parts.length;
|
|
1143
|
+
const normalMatrix = new Matrix3();
|
|
1144
|
+
const world = new Matrix4();
|
|
1145
|
+
const writeTransforms = () => {
|
|
1146
|
+
camera.updateMatrixWorld(true);
|
|
1147
|
+
cameraInverse.copy(camera.matrixWorld).invert();
|
|
1148
|
+
for (const part of parts) {
|
|
1149
|
+
world.copy(cameraInverse).multiply(part.object.matrixWorld);
|
|
1150
|
+
transformData.set(world.elements, part.index * 16);
|
|
1151
|
+
writeNormalMatrix(normalData, part.index * 16, normalMatrix.getNormalMatrix(world));
|
|
1152
|
+
if (!this.#drawn(part.object, camera)) {
|
|
1153
|
+
transformData[part.index * 16 + 12] = 1e9;
|
|
1154
|
+
transformData[part.index * 16 + 13] = 1e9;
|
|
1155
|
+
transformData[part.index * 16 + 14] = 1e9;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
transformAttribute.needsUpdate = true;
|
|
1159
|
+
normalAttribute.needsUpdate = true;
|
|
1160
|
+
};
|
|
1161
|
+
writeTransforms();
|
|
1162
|
+
const previousUpdate = this.#update;
|
|
1163
|
+
this.#update = () => {
|
|
1164
|
+
previousUpdate?.();
|
|
1165
|
+
writeTransforms();
|
|
1166
|
+
};
|
|
1167
|
+
const previousRestore = this.#restore;
|
|
1168
|
+
this.#restore = () => {
|
|
1169
|
+
for (const mesh of added) camera.remove(mesh);
|
|
1170
|
+
for (const entry of patched) {
|
|
1171
|
+
entry.material.positionNode = entry.position;
|
|
1172
|
+
entry.material.normalNode = entry.normal;
|
|
1173
|
+
}
|
|
1174
|
+
for (const [object, mask] of masks) {
|
|
1175
|
+
if (object.layers !== void 0) object.layers.mask = mask;
|
|
1176
|
+
}
|
|
1177
|
+
previousRestore?.();
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
return { materialIdentities, overlayMeshes, overlayDraws, skipped };
|
|
1181
|
+
}
|
|
1182
|
+
/** True when the game currently wants this overlay mesh on screen, ancestors included. */
|
|
1183
|
+
#drawn(object, camera) {
|
|
1184
|
+
for (let current = object; current !== null; current = current.parent) {
|
|
1185
|
+
if (current.visible !== true) return false;
|
|
1186
|
+
if (current === camera) break;
|
|
1187
|
+
}
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
#decline(reason, sourceMeshes, reasonCode) {
|
|
1191
|
+
this.#report = this.#makeReport({
|
|
1192
|
+
collapsed: false,
|
|
1193
|
+
reason,
|
|
1194
|
+
reasonCode,
|
|
1195
|
+
resultDrawCandidates: 0,
|
|
1196
|
+
resultMaterialIdentities: 0,
|
|
1197
|
+
skipped: this.#lastScan?.skipped ?? emptySkippedCounts(),
|
|
1198
|
+
sourceMaterialIdentities: this.#lastSourceMaterialIdentities,
|
|
1199
|
+
sourceMeshes,
|
|
1200
|
+
sourceRenderables: this.#lastSourceRenderables || sourceMeshes,
|
|
1201
|
+
status: "rejected"
|
|
1202
|
+
});
|
|
1203
|
+
this.#onReport(this.#report);
|
|
1204
|
+
}
|
|
1205
|
+
#makeReport(input) {
|
|
1206
|
+
return {
|
|
1207
|
+
schemaVersion: 1,
|
|
1208
|
+
status: input.status,
|
|
1209
|
+
reasonCode: input.reasonCode,
|
|
1210
|
+
collapsed: input.collapsed,
|
|
1211
|
+
...input.reason === void 0 ? {} : { reason: input.reason },
|
|
1212
|
+
sourceMeshes: input.sourceMeshes,
|
|
1213
|
+
mergedMeshes: input.resultDrawCandidates,
|
|
1214
|
+
movingParts: 0,
|
|
1215
|
+
diagnostics: {
|
|
1216
|
+
sourceRenderables: input.sourceRenderables,
|
|
1217
|
+
resultDrawCandidates: input.resultDrawCandidates,
|
|
1218
|
+
sourceMaterialIdentities: input.sourceMaterialIdentities,
|
|
1219
|
+
resultMaterialIdentities: input.resultMaterialIdentities,
|
|
1220
|
+
groups: { staticWorld: 0, movingOwners: 0, cameraOverlays: 0 },
|
|
1221
|
+
skipped: input.skipped,
|
|
1222
|
+
timings: this.#timings()
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
#timings() {
|
|
1227
|
+
return {
|
|
1228
|
+
bakeMs: this.#bakeSpentMs,
|
|
1229
|
+
observeMs: this.#observeSpanMs,
|
|
1230
|
+
mergeMs: this.#mergeSpentMs,
|
|
1231
|
+
copyMs: this.#copySpentMs,
|
|
1232
|
+
transformMs: this.#transformSpentMs,
|
|
1233
|
+
colorMs: this.#colorSpentMs,
|
|
1234
|
+
sampleMs: this.#sampleSpentMs,
|
|
1235
|
+
transformRefresh: {
|
|
1236
|
+
count: this.#refreshCount,
|
|
1237
|
+
lastMs: this.#refreshLastMs,
|
|
1238
|
+
maxMs: this.#refreshMaxMs,
|
|
1239
|
+
meanMs: this.#refreshCount === 0 ? 0 : this.#refreshTotalMs / this.#refreshCount
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Puts a detached root back in the scene once the game has added to it.
|
|
1245
|
+
*
|
|
1246
|
+
* The bake consumed what was there and removed each baked mesh from its parent, so a root that
|
|
1247
|
+
* comes back draws only what arrived after the collapse — the merged geometry keeps drawing the
|
|
1248
|
+
* rest and nothing is drawn twice. Re-attaching costs that root's traversal again, which is the
|
|
1249
|
+
* whole reason the pass detached it; paying it for a root the game demonstrably still builds
|
|
1250
|
+
* into is the correct trade, and a root nobody touches never pays.
|
|
1251
|
+
*
|
|
1252
|
+
* One integer compare per detached root per frame. There are tens of them, not thousands.
|
|
1253
|
+
*/
|
|
1254
|
+
#adoptGrownRoots() {
|
|
1255
|
+
if (this.#adoptable.length === 0) return;
|
|
1256
|
+
let remaining = 0;
|
|
1257
|
+
for (const entry of this.#adoptable) {
|
|
1258
|
+
if (entry.root.children.length === entry.children) {
|
|
1259
|
+
this.#adoptable[remaining] = entry;
|
|
1260
|
+
remaining += 1;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
this.#scene.add(entry.root);
|
|
1264
|
+
this.#adopted += 1;
|
|
1265
|
+
}
|
|
1266
|
+
this.#adoptable.length = remaining;
|
|
1267
|
+
}
|
|
1268
|
+
#refreshTransforms() {
|
|
1269
|
+
this.#adoptGrownRoots();
|
|
1270
|
+
if (this.#update === void 0) return;
|
|
1271
|
+
if (!this.#measureTransformRefresh) {
|
|
1272
|
+
this.#update();
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const startedAt = globalThis.performance?.now() ?? 0;
|
|
1276
|
+
this.#update();
|
|
1277
|
+
const elapsed = (globalThis.performance?.now() ?? 0) - startedAt;
|
|
1278
|
+
this.#refreshCount += 1;
|
|
1279
|
+
this.#refreshLastMs = elapsed;
|
|
1280
|
+
this.#refreshTotalMs += elapsed;
|
|
1281
|
+
this.#refreshMaxMs = Math.max(this.#refreshMaxMs, elapsed);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* A camera-parented subtree is not the scene pass's to bake: its world matrix would strand the
|
|
1285
|
+
* subtree wherever the camera happened to be standing. `#collapseCameras` folds it in camera
|
|
1286
|
+
* space instead, once this pass is done.
|
|
1287
|
+
*/
|
|
1288
|
+
#excluded(object) {
|
|
1289
|
+
for (let current = object; current !== null; current = current.parent) {
|
|
1290
|
+
if (current.isCamera === true) return true;
|
|
1291
|
+
}
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* The owner is the mesh's own parent whenever anything above it animates, not the nearest node
|
|
1296
|
+
* observed moving. Picking the nearest mover assumes every node between it and the mesh is
|
|
1297
|
+
* static, and a joint that happens to hold still during the observation window then freezes its
|
|
1298
|
+
* geometry for good — the fox's limbs glitching mid-stride. Uploading the parent's world matrix
|
|
1299
|
+
* carries the whole chain every frame and assumes nothing.
|
|
1300
|
+
*/
|
|
1301
|
+
/**
|
|
1302
|
+
* The nearest node at or above `object` that actually moves, which is the one whose transform the
|
|
1303
|
+
* baked draw has to follow. Returning `object.parent` instead looks equivalent on a rig — a mesh
|
|
1304
|
+
* hanging off an animated bone has that bone as its parent either way — but on a flat scene every
|
|
1305
|
+
* independently-moving sibling shares one parent, so they all resolved to it and were baked into a
|
|
1306
|
+
* single rigid group. Four thousand cubes moving on their own became one that did not.
|
|
1307
|
+
*/
|
|
1308
|
+
#ownerOf(object) {
|
|
1309
|
+
for (let current = object; current !== null; current = current.parent) {
|
|
1310
|
+
if (this.#moving.has(current)) return current;
|
|
1311
|
+
}
|
|
1312
|
+
return void 0;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* The collapse, as a generator that yields between meshes.
|
|
1316
|
+
*
|
|
1317
|
+
* Done in one call this took 3.6 s on a Pixel 8 — a freeze longer than the launch before it.
|
|
1318
|
+
* Yielding lets `frame()` spend a few milliseconds per frame on it instead, so a loading bar
|
|
1319
|
+
* keeps moving and nothing blocks. The merge at the end is not split: it was 138 ms of the
|
|
1320
|
+
* original 2,518 ms and splitting it would mean holding half-merged geometry across frames.
|
|
1321
|
+
*/
|
|
1322
|
+
*#collapseSteps() {
|
|
1323
|
+
this.#scene.updateMatrixWorld(true);
|
|
1324
|
+
const scan = this.#lastScan ?? this.#scanSceneRenderables();
|
|
1325
|
+
this.#lastScan = void 0;
|
|
1326
|
+
this.#lastSourceRenderables = scan.sourceRenderables;
|
|
1327
|
+
this.#lastSourceMaterialIdentities = scan.materialIdentities.size;
|
|
1328
|
+
const meshes = [...scan.meshes];
|
|
1329
|
+
if (meshes.length < this.#minMeshes) {
|
|
1330
|
+
this.#meshCount = 0;
|
|
1331
|
+
this.#observed = 0;
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const movingParts = [];
|
|
1335
|
+
const movingIndex = /* @__PURE__ */ new Map();
|
|
1336
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1337
|
+
const bakedFrom = [];
|
|
1338
|
+
const inverse = new Matrix4();
|
|
1339
|
+
const bakeMatrix = new Matrix4();
|
|
1340
|
+
const bakeNormalMatrix = new Matrix3();
|
|
1341
|
+
for (const [meshIndex, mesh] of meshes.entries()) {
|
|
1342
|
+
yield meshIndex / meshes.length;
|
|
1343
|
+
const material = materialOf(mesh);
|
|
1344
|
+
if (material === void 0) {
|
|
1345
|
+
this.#decline("a mesh has no material", meshes.length, "missingMaterial");
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const owner = this.#ownerOf(mesh);
|
|
1349
|
+
let ownerId;
|
|
1350
|
+
if (owner !== void 0) {
|
|
1351
|
+
ownerId = movingIndex.get(owner);
|
|
1352
|
+
if (ownerId === void 0) {
|
|
1353
|
+
ownerId = movingParts.length;
|
|
1354
|
+
movingIndex.set(owner, ownerId);
|
|
1355
|
+
movingParts.push({
|
|
1356
|
+
// Decided later, in `#update`'s setup: parts are still parented to the live scene at
|
|
1357
|
+
// this point and only leave it once the bake detaches them.
|
|
1358
|
+
detachedLeaf: false,
|
|
1359
|
+
index: ownerId,
|
|
1360
|
+
object: owner,
|
|
1361
|
+
uniformScale: hasUniformScale(owner.matrixWorld)
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const local = owner === void 0 ? mesh.matrixWorld : inverse.copy(owner.matrixWorld).invert().multiply(mesh.matrixWorld);
|
|
1366
|
+
const source = mesh.geometry;
|
|
1367
|
+
if (source === void 0) {
|
|
1368
|
+
this.#decline(
|
|
1369
|
+
"a mesh lost its geometry between the scan and the merge",
|
|
1370
|
+
meshes.length,
|
|
1371
|
+
"missingGeometry"
|
|
1372
|
+
);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const copyStartedAt = globalThis.performance?.now() ?? 0;
|
|
1376
|
+
const geometry = source.index !== null ? source.toNonIndexed() : source.clone();
|
|
1377
|
+
this.#copySpentMs += (globalThis.performance?.now() ?? 0) - copyStartedAt;
|
|
1378
|
+
for (const name of Object.keys(geometry.attributes)) {
|
|
1379
|
+
if (!CANONICAL_ATTRIBUTES.includes(name)) geometry.deleteAttribute(name);
|
|
1380
|
+
}
|
|
1381
|
+
if (geometry.getAttribute("normal") === void 0)
|
|
1382
|
+
geometry.computeVertexNormals();
|
|
1383
|
+
bakeMatrix.copy(local);
|
|
1384
|
+
const transformStartedAt = globalThis.performance?.now() ?? 0;
|
|
1385
|
+
bakeTransform(geometry, bakeMatrix, bakeNormalMatrix.getNormalMatrix(bakeMatrix));
|
|
1386
|
+
this.#transformSpentMs += (globalThis.performance?.now() ?? 0) - transformStartedAt;
|
|
1387
|
+
if (ownerId !== void 0) {
|
|
1388
|
+
const count2 = geometry.getAttribute("position")?.count ?? 0;
|
|
1389
|
+
const ids = new Uint32Array(count2);
|
|
1390
|
+
ids.fill(ownerId);
|
|
1391
|
+
geometry.setAttribute("tnOwnerId", new Uint32BufferAttribute(ids, 1));
|
|
1392
|
+
}
|
|
1393
|
+
material.transparent === true;
|
|
1394
|
+
const kind = owner === void 0 ? "static" : "moving";
|
|
1395
|
+
const color = colorOf(material);
|
|
1396
|
+
const count = geometry.getAttribute("position")?.count ?? 0;
|
|
1397
|
+
if (geometry.getAttribute("uv") === void 0)
|
|
1398
|
+
geometry.setAttribute("uv", new Float32BufferAttribute(new Float32Array(count * 2), 2));
|
|
1399
|
+
const rgb = color ?? { r: 1, g: 1, b: 1 };
|
|
1400
|
+
const painted = material.vertexColors === true ? geometry.getAttribute("color") : void 0;
|
|
1401
|
+
const paintedArray = painted?.array;
|
|
1402
|
+
const paintedStride = painted?.itemSize;
|
|
1403
|
+
const fastColors = paintedArray instanceof Float32Array && paintedStride !== void 0 && painted?.normalized !== true ? { source: paintedArray, stride: paintedStride } : void 0;
|
|
1404
|
+
const colorStartedAt = globalThis.performance?.now() ?? 0;
|
|
1405
|
+
const colors = new Float32Array(count * 3);
|
|
1406
|
+
if (painted === void 0) {
|
|
1407
|
+
for (let index = 0; index < count; index += 1) {
|
|
1408
|
+
colors[index * 3] = rgb.r;
|
|
1409
|
+
colors[index * 3 + 1] = rgb.g;
|
|
1410
|
+
colors[index * 3 + 2] = rgb.b;
|
|
1411
|
+
}
|
|
1412
|
+
} else if (fastColors !== void 0) {
|
|
1413
|
+
const { source: source2, stride } = fastColors;
|
|
1414
|
+
for (let index = 0; index < count; index += 1) {
|
|
1415
|
+
const at = index * stride;
|
|
1416
|
+
colors[index * 3] = rgb.r * (source2[at] ?? 0);
|
|
1417
|
+
colors[index * 3 + 1] = rgb.g * (source2[at + 1] ?? 0);
|
|
1418
|
+
colors[index * 3 + 2] = rgb.b * (source2[at + 2] ?? 0);
|
|
1419
|
+
}
|
|
1420
|
+
} else {
|
|
1421
|
+
for (let index = 0; index < count; index += 1) {
|
|
1422
|
+
colors[index * 3] = rgb.r * painted.getX(index);
|
|
1423
|
+
colors[index * 3 + 1] = rgb.g * painted.getY(index);
|
|
1424
|
+
colors[index * 3 + 2] = rgb.b * painted.getZ(index);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
geometry.setAttribute("color", new Float32BufferAttribute(colors, 3));
|
|
1428
|
+
this.#colorSpentMs += (globalThis.performance?.now() ?? 0) - colorStartedAt;
|
|
1429
|
+
const castShadow = mesh.castShadow === true;
|
|
1430
|
+
const receiveShadow = mesh.receiveShadow === true;
|
|
1431
|
+
const key = `${kind}|${materialSignature(material)}|cast:${castShadow}|receive:${receiveShadow}`;
|
|
1432
|
+
const group = groups.get(key) ?? {
|
|
1433
|
+
material,
|
|
1434
|
+
chunks: [],
|
|
1435
|
+
dynamic: owner !== void 0,
|
|
1436
|
+
castShadow,
|
|
1437
|
+
receiveShadow
|
|
1438
|
+
};
|
|
1439
|
+
group.chunks.push(geometry);
|
|
1440
|
+
groups.set(key, group);
|
|
1441
|
+
if (mesh.parent !== null) bakedFrom.push({ mesh, parent: mesh.parent });
|
|
1442
|
+
}
|
|
1443
|
+
const slots = Math.max(1, movingParts.length);
|
|
1444
|
+
const transformData = new Float32Array(slots * 16);
|
|
1445
|
+
const uniformNormals = movingParts.every((part) => part.uniformScale);
|
|
1446
|
+
const normalData = uniformNormals ? transformData : new Float32Array(slots * 16);
|
|
1447
|
+
const normalMatrix = new Matrix3();
|
|
1448
|
+
for (const part of movingParts) {
|
|
1449
|
+
transformData.set(part.object.matrixWorld.elements, part.index * 16);
|
|
1450
|
+
if (!uniformNormals) writeNormals(normalData, part, normalMatrix);
|
|
1451
|
+
}
|
|
1452
|
+
const transformAttribute = new StorageBufferAttribute(transformData, 16);
|
|
1453
|
+
const normalAttribute = uniformNormals ? transformAttribute : new StorageBufferAttribute(normalData, 16);
|
|
1454
|
+
const transformNode = storage(transformAttribute, "mat4", slots);
|
|
1455
|
+
const normalNodes = uniformNormals ? transformNode : storage(normalAttribute, "mat4", slots);
|
|
1456
|
+
const ownerIdNode = attribute("tnOwnerId", "uint");
|
|
1457
|
+
const ownerMatrix = transformNode.element(ownerIdNode);
|
|
1458
|
+
const ownerNormalMatrix = normalNodes.element(ownerIdNode);
|
|
1459
|
+
const added = [];
|
|
1460
|
+
let mergedMeshes = 0;
|
|
1461
|
+
for (const group of groups.values()) {
|
|
1462
|
+
const shared = sharedAttributeNames(group.chunks);
|
|
1463
|
+
if (!shared.includes("position")) {
|
|
1464
|
+
this.#decline(
|
|
1465
|
+
"a merge group disagrees on its position attribute",
|
|
1466
|
+
meshes.length,
|
|
1467
|
+
"mergeGroupMissingPosition"
|
|
1468
|
+
);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
for (const chunk of group.chunks) {
|
|
1472
|
+
for (const name of Object.keys(chunk.attributes)) {
|
|
1473
|
+
if (!shared.includes(name)) chunk.deleteAttribute(name);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const mergeStartedAt = globalThis.performance?.now() ?? 0;
|
|
1477
|
+
const geometry = mergeGeometries(group.chunks, false);
|
|
1478
|
+
this.#mergeSpentMs += (globalThis.performance?.now() ?? 0) - mergeStartedAt;
|
|
1479
|
+
if (geometry === null) {
|
|
1480
|
+
this.#decline("three.js refused to merge a group", meshes.length, "mergeRefused");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
let material = group.material;
|
|
1484
|
+
if (geometry.getAttribute("color") !== void 0) {
|
|
1485
|
+
material = material.clone();
|
|
1486
|
+
const tinted = material;
|
|
1487
|
+
tinted.vertexColors = true;
|
|
1488
|
+
tinted.color?.setRGB(1, 1, 1);
|
|
1489
|
+
}
|
|
1490
|
+
if (geometry.getAttribute("normal") === void 0) {
|
|
1491
|
+
this.#decline("a merge group lost its normals", meshes.length, "mergeLostNormals");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (group.dynamic && geometry.getAttribute("tnOwnerId") === void 0) {
|
|
1495
|
+
this.#decline(
|
|
1496
|
+
"a moving group lost its per-vertex transform id in the merge",
|
|
1497
|
+
meshes.length,
|
|
1498
|
+
"movingGroupLostOwnerId"
|
|
1499
|
+
);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (group.dynamic) {
|
|
1503
|
+
const node = material.clone();
|
|
1504
|
+
node.positionNode = ownerMatrix.mul(vec4(positionLocal, 1)).xyz;
|
|
1505
|
+
node.normalNode = ownerNormalMatrix.mul(vec4(normalLocal, 0)).xyz.normalize();
|
|
1506
|
+
material = node;
|
|
1507
|
+
}
|
|
1508
|
+
const mesh = new Mesh(geometry, material);
|
|
1509
|
+
mesh.frustumCulled = false;
|
|
1510
|
+
mesh.castShadow = group.castShadow;
|
|
1511
|
+
mesh.receiveShadow = group.receiveShadow;
|
|
1512
|
+
this.#scene.add(mesh);
|
|
1513
|
+
added.push(mesh);
|
|
1514
|
+
mergedMeshes += 1;
|
|
1515
|
+
}
|
|
1516
|
+
for (const entry of bakedFrom) entry.parent.remove(entry.mesh);
|
|
1517
|
+
const detached = [];
|
|
1518
|
+
for (const child of [...this.#scene.children]) {
|
|
1519
|
+
if (child.isCamera === true || added.includes(child) || scan.preserved.has(child)) continue;
|
|
1520
|
+
if (containsLight(child)) continue;
|
|
1521
|
+
if (containsPreserved(child, scan.preserved)) continue;
|
|
1522
|
+
detached.push(child);
|
|
1523
|
+
this.#scene.remove(child);
|
|
1524
|
+
this.#adoptable.push({ children: child.children.length, root: child });
|
|
1525
|
+
}
|
|
1526
|
+
const movingRoots = [];
|
|
1527
|
+
for (const part of movingParts) {
|
|
1528
|
+
let root = part.object;
|
|
1529
|
+
while (root.parent !== null) root = root.parent;
|
|
1530
|
+
if (!movingRoots.includes(root)) movingRoots.push(root);
|
|
1531
|
+
}
|
|
1532
|
+
for (const part of movingParts) {
|
|
1533
|
+
part.detachedLeaf = part.object.parent === null && part.object.children.length === 0 && typeof part.object.updateMatrix === "function";
|
|
1534
|
+
}
|
|
1535
|
+
const detachedLeaves = new Set(
|
|
1536
|
+
movingParts.filter((part) => part.detachedLeaf).map((part) => part.object)
|
|
1537
|
+
);
|
|
1538
|
+
const walkedRoots = movingRoots.filter((root) => !detachedLeaves.has(root));
|
|
1539
|
+
const leafParts = movingParts.filter((part) => part.detachedLeaf);
|
|
1540
|
+
const nestedParts = movingParts.filter((part) => !part.detachedLeaf);
|
|
1541
|
+
const hide = (offset) => {
|
|
1542
|
+
transformData[offset + 12] = 1e9;
|
|
1543
|
+
transformData[offset + 13] = 1e9;
|
|
1544
|
+
transformData[offset + 14] = 1e9;
|
|
1545
|
+
};
|
|
1546
|
+
this.#update = () => {
|
|
1547
|
+
for (const root of walkedRoots) root.updateMatrixWorld(true);
|
|
1548
|
+
for (const part of leafParts) {
|
|
1549
|
+
const object = part.object;
|
|
1550
|
+
object.updateMatrix?.();
|
|
1551
|
+
const offset = part.index * 16;
|
|
1552
|
+
transformData.set(object.matrix.elements, offset);
|
|
1553
|
+
if (!uniformNormals) writeNormals(normalData, part, normalMatrix);
|
|
1554
|
+
if (object.visible !== true) hide(offset);
|
|
1555
|
+
}
|
|
1556
|
+
for (const part of nestedParts) {
|
|
1557
|
+
const object = part.object;
|
|
1558
|
+
const offset = part.index * 16;
|
|
1559
|
+
transformData.set(object.matrixWorld.elements, offset);
|
|
1560
|
+
if (!uniformNormals) writeNormals(normalData, part, normalMatrix);
|
|
1561
|
+
if (object.visible !== true) hide(offset);
|
|
1562
|
+
}
|
|
1563
|
+
if (movingParts.length > 0) {
|
|
1564
|
+
transformAttribute.needsUpdate = true;
|
|
1565
|
+
if (!uniformNormals) normalAttribute.needsUpdate = true;
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
this.#restore = () => {
|
|
1569
|
+
for (const mesh of added) this.#scene.remove(mesh);
|
|
1570
|
+
for (const entry of bakedFrom) entry.parent.add(entry.mesh);
|
|
1571
|
+
for (const child of detached) this.#scene.add(child);
|
|
1572
|
+
};
|
|
1573
|
+
const overlay = this.#collapseCameras();
|
|
1574
|
+
const skipped = emptySkippedCounts();
|
|
1575
|
+
mergeSkippedCounts(skipped, scan.skipped);
|
|
1576
|
+
mergeSkippedCounts(skipped, overlay.skipped);
|
|
1577
|
+
const resultMaterialIdentities = /* @__PURE__ */ new Set();
|
|
1578
|
+
for (const group of groups.values()) resultMaterialIdentities.add(group.material);
|
|
1579
|
+
for (const material of overlay.materialIdentities) resultMaterialIdentities.add(material);
|
|
1580
|
+
this.#report = {
|
|
1581
|
+
schemaVersion: 1,
|
|
1582
|
+
status: "applied",
|
|
1583
|
+
reasonCode: "applied",
|
|
1584
|
+
collapsed: true,
|
|
1585
|
+
sourceMeshes: meshes.length,
|
|
1586
|
+
mergedMeshes,
|
|
1587
|
+
movingParts: movingParts.length,
|
|
1588
|
+
overlayMeshes: overlay.overlayMeshes,
|
|
1589
|
+
overlayDraws: overlay.overlayDraws,
|
|
1590
|
+
diagnostics: {
|
|
1591
|
+
sourceRenderables: scan.sourceRenderables,
|
|
1592
|
+
resultDrawCandidates: mergedMeshes + overlay.overlayDraws,
|
|
1593
|
+
sourceMaterialIdentities: scan.materialIdentities.size + overlay.materialIdentities.size,
|
|
1594
|
+
resultMaterialIdentities: resultMaterialIdentities.size,
|
|
1595
|
+
groups: {
|
|
1596
|
+
staticWorld: [...groups.values()].filter((group) => !group.dynamic).length,
|
|
1597
|
+
movingOwners: movingParts.length,
|
|
1598
|
+
cameraOverlays: overlay.overlayDraws
|
|
1599
|
+
},
|
|
1600
|
+
skipped,
|
|
1601
|
+
timings: this.#timings()
|
|
1602
|
+
},
|
|
1603
|
+
bakeMs: this.#bakeSpentMs,
|
|
1604
|
+
observeMs: this.#observeSpanMs,
|
|
1605
|
+
mergeMs: this.#mergeSpentMs,
|
|
1606
|
+
copyMs: this.#copySpentMs,
|
|
1607
|
+
transformMs: this.#transformSpentMs,
|
|
1608
|
+
colorMs: this.#colorSpentMs,
|
|
1609
|
+
sampleMs: this.#sampleSpentMs
|
|
1610
|
+
};
|
|
1611
|
+
this.#onReport(this.#report);
|
|
254
1612
|
}
|
|
255
1613
|
};
|
|
256
1614
|
|
|
@@ -275,6 +1633,8 @@ function disposeEntity(entity) {
|
|
|
275
1633
|
}
|
|
276
1634
|
var Registry = class {
|
|
277
1635
|
#named = /* @__PURE__ */ new Map();
|
|
1636
|
+
#pendingFree = /* @__PURE__ */ new Set();
|
|
1637
|
+
#iterating = false;
|
|
278
1638
|
add(name, entity) {
|
|
279
1639
|
if (this.#named.has(name)) throw new Error(`Entity "${name}" already registered.`);
|
|
280
1640
|
this.#named.set(name, entity);
|
|
@@ -284,28 +1644,64 @@ var Registry = class {
|
|
|
284
1644
|
return this.#named.get(name);
|
|
285
1645
|
}
|
|
286
1646
|
remove(name) {
|
|
1647
|
+
this.#assertNotIterating("remove");
|
|
1648
|
+
this.#pendingFree.delete(name);
|
|
287
1649
|
const entity = this.#named.get(name);
|
|
288
1650
|
this.#named.delete(name);
|
|
289
1651
|
if (entity !== void 0) disposeEntity(entity);
|
|
290
1652
|
}
|
|
1653
|
+
queueFree(target) {
|
|
1654
|
+
const name = typeof target === "string" ? target : this.#nameOf(target);
|
|
1655
|
+
if (name === void 0 || !this.#named.has(name))
|
|
1656
|
+
throw new Error("Cannot queue an entity that is not registered.");
|
|
1657
|
+
this.#pendingFree.add(name);
|
|
1658
|
+
}
|
|
1659
|
+
sweep() {
|
|
1660
|
+
this.#assertNotIterating("sweep");
|
|
1661
|
+
const pending = [...this.#pendingFree];
|
|
1662
|
+
this.#pendingFree.clear();
|
|
1663
|
+
for (const name of pending) {
|
|
1664
|
+
const entity = this.#named.get(name);
|
|
1665
|
+
if (entity === void 0) continue;
|
|
1666
|
+
this.#named.delete(name);
|
|
1667
|
+
disposeEntity(entity);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
291
1670
|
clear() {
|
|
1671
|
+
this.#assertNotIterating("clear");
|
|
1672
|
+
this.#pendingFree.clear();
|
|
292
1673
|
const entities = [...new Set(this.#named.values())];
|
|
293
1674
|
this.#named.clear();
|
|
294
1675
|
for (const entity of entities) disposeEntity(entity);
|
|
295
1676
|
}
|
|
296
1677
|
snapshot() {
|
|
297
1678
|
const result = {};
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
1679
|
+
this.#iterating = true;
|
|
1680
|
+
try {
|
|
1681
|
+
for (const [name, entity] of this.#named) {
|
|
1682
|
+
const debug = entity.debug;
|
|
1683
|
+
const fields = typeof debug === "function" ? debug.call(entity) : autoFields(entity);
|
|
1684
|
+
const tags = entity.tags;
|
|
1685
|
+
result[name] = {
|
|
1686
|
+
...fields,
|
|
1687
|
+
...Array.isArray(tags) && tags.every((tag) => typeof tag === "string") ? { tags: [...tags] } : {}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
} finally {
|
|
1691
|
+
this.#iterating = false;
|
|
306
1692
|
}
|
|
307
1693
|
return result;
|
|
308
1694
|
}
|
|
1695
|
+
#nameOf(entity) {
|
|
1696
|
+
for (const [name, registered] of this.#named) {
|
|
1697
|
+
if (registered === entity) return name;
|
|
1698
|
+
}
|
|
1699
|
+
return void 0;
|
|
1700
|
+
}
|
|
1701
|
+
#assertNotIterating(operation) {
|
|
1702
|
+
if (this.#iterating)
|
|
1703
|
+
throw new TypeError(`Registry.${operation}() cannot mutate during snapshot.`);
|
|
1704
|
+
}
|
|
309
1705
|
};
|
|
310
1706
|
var DEFAULT_BINDINGS = {
|
|
311
1707
|
move: {
|
|
@@ -326,8 +1722,8 @@ var InputMap = class {
|
|
|
326
1722
|
#target;
|
|
327
1723
|
#heldKeys = /* @__PURE__ */ new Set();
|
|
328
1724
|
#pointerPosition = new Vector2();
|
|
329
|
-
#pointerDown = false;
|
|
330
1725
|
#pointerButtons = 0;
|
|
1726
|
+
#pointers = /* @__PURE__ */ new Map();
|
|
331
1727
|
#gamepadAxes = [];
|
|
332
1728
|
#gamepadButtons = [];
|
|
333
1729
|
#previousPressed = /* @__PURE__ */ new Map();
|
|
@@ -346,9 +1742,10 @@ var InputMap = class {
|
|
|
346
1742
|
keys: this.#heldKeys,
|
|
347
1743
|
pointer: {
|
|
348
1744
|
buttons: this.#pointerButtons,
|
|
349
|
-
down:
|
|
1745
|
+
down: false,
|
|
350
1746
|
position: this.#pointerPosition
|
|
351
|
-
}
|
|
1747
|
+
},
|
|
1748
|
+
pointers: this.#pointers
|
|
352
1749
|
};
|
|
353
1750
|
this.#listen(this.#target, "keydown", (event2) => {
|
|
354
1751
|
const code = eventCode(event2);
|
|
@@ -359,14 +1756,16 @@ var InputMap = class {
|
|
|
359
1756
|
if (code !== void 0) this.#heldKeys.delete(code);
|
|
360
1757
|
});
|
|
361
1758
|
this.#listen(this.#pointerTarget, "pointerdown", (event2) => this.#pointerEvent(event2, true));
|
|
362
|
-
this.#listen(
|
|
363
|
-
this.#pointerTarget,
|
|
364
|
-
"pointermove",
|
|
365
|
-
(event2) => this.#pointerEvent(event2, this.#pointerDown)
|
|
366
|
-
);
|
|
1759
|
+
this.#listen(this.#pointerTarget, "pointermove", (event2) => this.#pointerEvent(event2));
|
|
367
1760
|
this.#listen(this.#pointerTarget, "pointerup", (event2) => this.#pointerEvent(event2, false));
|
|
1761
|
+
this.#listen(this.#pointerTarget, "pointercancel", (event2) => this.#pointerEvent(event2, false));
|
|
368
1762
|
this.#listen(this.#target, "blur", () => this.clear());
|
|
369
1763
|
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Returns a 2D action vector where +y is up. On a conventional XZ ground plane whose
|
|
1766
|
+
* forward direction is -z, map `vector.y` to `-z`. This differs from Godot's
|
|
1767
|
+
* `Input.get_vector`, where up is -y.
|
|
1768
|
+
*/
|
|
370
1769
|
vector(name) {
|
|
371
1770
|
const binding = this.#bindings[name];
|
|
372
1771
|
if (binding === void 0) return new Vector2();
|
|
@@ -385,7 +1784,7 @@ var InputMap = class {
|
|
|
385
1784
|
pressed(name) {
|
|
386
1785
|
const binding = this.#bindings[name];
|
|
387
1786
|
if (binding === void 0) return false;
|
|
388
|
-
return this.#isHeld(binding.down) || this.#isHeld(binding.left) || this.#isHeld(binding.right) || this.#isHeld(binding.up) || binding.pointer === true && this.#
|
|
1787
|
+
return this.#isHeld(binding.keys) || this.#isHeld(binding.down) || this.#isHeld(binding.left) || this.#isHeld(binding.right) || this.#isHeld(binding.up) || binding.pointer === true && this.#pointers.size > 0 || (binding.buttons?.some((button) => this.#gamepadButtons[button] === true) ?? false);
|
|
389
1788
|
}
|
|
390
1789
|
justPressed(name) {
|
|
391
1790
|
return this.#justPressed.has(name);
|
|
@@ -411,7 +1810,7 @@ var InputMap = class {
|
|
|
411
1810
|
}
|
|
412
1811
|
clear() {
|
|
413
1812
|
this.#heldKeys.clear();
|
|
414
|
-
this.#
|
|
1813
|
+
this.#pointers.clear();
|
|
415
1814
|
this.#pointerButtons = 0;
|
|
416
1815
|
this.#pointerPosition.set(0, 0);
|
|
417
1816
|
this.raw.pointer.buttons = 0;
|
|
@@ -435,21 +1834,44 @@ var InputMap = class {
|
|
|
435
1834
|
}
|
|
436
1835
|
#pointerEvent(event2, down) {
|
|
437
1836
|
const pointer = event2;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1837
|
+
const id = pointer.pointerId ?? 0;
|
|
1838
|
+
const buttons = pointer.buttons ?? 0;
|
|
1839
|
+
const x = pointer.clientX ?? 0;
|
|
1840
|
+
const y = pointer.clientY ?? 0;
|
|
1841
|
+
const tracked = this.#pointers.get(id);
|
|
1842
|
+
if (down === true) {
|
|
1843
|
+
if (tracked === void 0) {
|
|
1844
|
+
this.#pointers.set(id, { buttons, id, position: new Vector2(x, y) });
|
|
1845
|
+
} else {
|
|
1846
|
+
tracked.buttons = buttons;
|
|
1847
|
+
tracked.position.set(x, y);
|
|
1848
|
+
}
|
|
1849
|
+
} else if (down === false) {
|
|
1850
|
+
this.#pointers.delete(id);
|
|
1851
|
+
} else if (tracked !== void 0) {
|
|
1852
|
+
tracked.buttons = buttons;
|
|
1853
|
+
tracked.position.set(x, y);
|
|
1854
|
+
}
|
|
1855
|
+
const primary = this.#pointers.values().next().value;
|
|
1856
|
+
if (primary !== void 0) {
|
|
1857
|
+
this.#pointerButtons = primary.buttons;
|
|
1858
|
+
this.#pointerPosition.copy(primary.position);
|
|
1859
|
+
} else if (down !== false || tracked !== void 0) {
|
|
1860
|
+
this.#pointerButtons = buttons;
|
|
1861
|
+
this.#pointerPosition.set(x, y);
|
|
1862
|
+
}
|
|
441
1863
|
this.raw.pointer.buttons = this.#pointerButtons;
|
|
442
|
-
this.raw.pointer.down =
|
|
1864
|
+
this.raw.pointer.down = primary !== void 0;
|
|
443
1865
|
const capture = this.#pointerTarget;
|
|
444
|
-
if (
|
|
445
|
-
|
|
1866
|
+
if (pointer.isTrusted !== false) {
|
|
1867
|
+
if (down === true) capture.setPointerCapture?.(id);
|
|
1868
|
+
if (down === false) capture.releasePointerCapture?.(id);
|
|
1869
|
+
}
|
|
446
1870
|
}
|
|
447
1871
|
};
|
|
448
|
-
function input(bindings, target, pointerTarget, source) {
|
|
449
|
-
return new InputMap(bindings, target, pointerTarget, source);
|
|
450
|
-
}
|
|
451
1872
|
|
|
452
1873
|
// src/loop.ts
|
|
1874
|
+
var MAX_RENDER_PERFORMANCE_SAMPLES = 1024;
|
|
453
1875
|
var FixedStepLoop = class {
|
|
454
1876
|
step;
|
|
455
1877
|
maxSteps;
|
|
@@ -461,8 +1883,10 @@ var FixedStepLoop = class {
|
|
|
461
1883
|
#lastTime;
|
|
462
1884
|
#frameHandle;
|
|
463
1885
|
#running = false;
|
|
1886
|
+
#tick = 0;
|
|
464
1887
|
#fps = 0;
|
|
465
1888
|
#lastRenderTime;
|
|
1889
|
+
#renderPerformanceSamples = [];
|
|
466
1890
|
constructor(options) {
|
|
467
1891
|
this.step = options.step ?? 1 / 60;
|
|
468
1892
|
this.maxSteps = options.maxSteps ?? 5;
|
|
@@ -480,12 +1904,18 @@ var FixedStepLoop = class {
|
|
|
480
1904
|
get fps() {
|
|
481
1905
|
return this.#fps;
|
|
482
1906
|
}
|
|
1907
|
+
runtimeDiagnosticsSeries() {
|
|
1908
|
+
return this.#renderPerformanceSamples.map((sample) => ({ ...sample }));
|
|
1909
|
+
}
|
|
1910
|
+
tick = () => this.#tick;
|
|
483
1911
|
start(now = globalThis.performance?.now() ?? 0) {
|
|
484
1912
|
if (this.#running) return;
|
|
485
1913
|
this.#running = true;
|
|
486
1914
|
this.#lastTime = now;
|
|
487
1915
|
this.#lastRenderTime = void 0;
|
|
1916
|
+
this.#tick = 0;
|
|
488
1917
|
this.#fps = 0;
|
|
1918
|
+
this.#renderPerformanceSamples = [];
|
|
489
1919
|
this.#frameHandle = this.#requestFrame((time) => this.#frame(time));
|
|
490
1920
|
}
|
|
491
1921
|
stop() {
|
|
@@ -496,34 +1926,46 @@ var FixedStepLoop = class {
|
|
|
496
1926
|
this.#accumulator = 0;
|
|
497
1927
|
}
|
|
498
1928
|
stepFrame(now) {
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
this.#lastTime = now;
|
|
1929
|
+
const elapsed = Math.max(0, (now - (this.#lastTime ?? now)) / 1e3);
|
|
1930
|
+
this.#lastTime = Math.max(this.#lastTime ?? now, now);
|
|
502
1931
|
this.#accumulator += elapsed;
|
|
503
1932
|
let updates = 0;
|
|
504
1933
|
while (this.#accumulator + Number.EPSILON >= this.step && updates < this.maxSteps) {
|
|
505
1934
|
this.#onUpdate(this.step);
|
|
1935
|
+
this.#tick += 1;
|
|
506
1936
|
this.#accumulator -= this.step;
|
|
507
1937
|
updates += 1;
|
|
508
1938
|
}
|
|
509
1939
|
if (updates === this.maxSteps && this.#accumulator >= this.step) this.#accumulator = 0;
|
|
1940
|
+
let frameMs;
|
|
510
1941
|
if (Number.isFinite(now)) {
|
|
511
1942
|
if (this.#lastRenderTime !== void 0) {
|
|
512
|
-
|
|
1943
|
+
frameMs = now - this.#lastRenderTime;
|
|
513
1944
|
if (frameMs > 0) this.#fps += (1e3 / frameMs - this.#fps) * 0.1;
|
|
514
1945
|
}
|
|
515
1946
|
this.#lastRenderTime = now;
|
|
516
1947
|
}
|
|
517
|
-
this.#onRender();
|
|
1948
|
+
const metrics = this.#onRender();
|
|
1949
|
+
if (frameMs !== void 0 && frameMs > 0) {
|
|
1950
|
+
const sample = {
|
|
1951
|
+
frameMs,
|
|
1952
|
+
...metrics === void 0 || metrics.drawCalls === void 0 ? {} : { drawCalls: metrics.drawCalls },
|
|
1953
|
+
...metrics === void 0 || metrics.triangles === void 0 ? {} : { triangles: metrics.triangles }
|
|
1954
|
+
};
|
|
1955
|
+
this.#renderPerformanceSamples.push(sample);
|
|
1956
|
+
if (this.#renderPerformanceSamples.length > MAX_RENDER_PERFORMANCE_SAMPLES)
|
|
1957
|
+
this.#renderPerformanceSamples.shift();
|
|
1958
|
+
}
|
|
518
1959
|
return updates;
|
|
519
1960
|
}
|
|
520
1961
|
advance(ticks) {
|
|
521
1962
|
if (!this.#running) throw new Error("Cannot advance a stopped loop.");
|
|
522
1963
|
if (!Number.isInteger(ticks) || ticks <= 0)
|
|
523
1964
|
throw new Error("advance ticks must be a positive integer.");
|
|
1965
|
+
this.#lastTime = Number.POSITIVE_INFINITY;
|
|
524
1966
|
for (let index = 0; index < ticks; index += 1) {
|
|
525
|
-
this.#lastTime = (this.#lastTime ?? 0) + this.step * 1e3;
|
|
526
1967
|
this.#onUpdate(this.step);
|
|
1968
|
+
this.#tick += 1;
|
|
527
1969
|
}
|
|
528
1970
|
return ticks;
|
|
529
1971
|
}
|
|
@@ -604,42 +2046,70 @@ var GPUParticles3D = class extends Sprite {
|
|
|
604
2046
|
}
|
|
605
2047
|
#onRemoved = () => this.detach();
|
|
606
2048
|
};
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
2049
|
+
var ScenePicker = class {
|
|
2050
|
+
#camera;
|
|
2051
|
+
#pointer;
|
|
2052
|
+
#scene;
|
|
2053
|
+
#viewport;
|
|
2054
|
+
#raycaster = new Raycaster();
|
|
2055
|
+
#ndc = new Vector2();
|
|
2056
|
+
#hits = [];
|
|
2057
|
+
#trees = /* @__PURE__ */ new WeakMap();
|
|
2058
|
+
constructor(options) {
|
|
2059
|
+
this.#camera = options.camera;
|
|
2060
|
+
this.#pointer = options.pointer;
|
|
2061
|
+
this.#scene = options.scene;
|
|
2062
|
+
this.#viewport = options.viewport;
|
|
2063
|
+
this.#raycaster.firstHitOnly = true;
|
|
2064
|
+
}
|
|
2065
|
+
/** The closest hit under the screen point, or `undefined` when the ray hits nothing. */
|
|
2066
|
+
raycast(options = {}) {
|
|
2067
|
+
const screen = options.screen ?? this.#pointer();
|
|
2068
|
+
if (!Number.isFinite(screen.x) || !Number.isFinite(screen.y))
|
|
2069
|
+
throw new Error("ICtx.raycast screen point must be finite.");
|
|
2070
|
+
const { height, width } = this.#viewport.size;
|
|
2071
|
+
if (width <= 0 || height <= 0) throw new Error("ICtx.raycast needs a sized viewport.");
|
|
2072
|
+
this.#ndc.set(screen.x / width * 2 - 1, -(screen.y / height) * 2 + 1);
|
|
2073
|
+
this.#raycaster.setFromCamera(this.#ndc, this.#camera);
|
|
2074
|
+
const targets = options.targets ?? this.#scene;
|
|
2075
|
+
const roots = Array.isArray(targets) ? targets : [targets];
|
|
2076
|
+
this.#hits.length = 0;
|
|
2077
|
+
for (const root of roots) {
|
|
2078
|
+
root.updateMatrixWorld();
|
|
2079
|
+
this.#collect(root);
|
|
621
2080
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (
|
|
635
|
-
|
|
636
|
-
throw new TypeError("state must be a finite integer.");
|
|
637
|
-
}
|
|
638
|
-
state = value >>> 0;
|
|
2081
|
+
if (this.#hits.length === 0) return void 0;
|
|
2082
|
+
this.#hits.sort((first, second) => first.distance - second.distance);
|
|
2083
|
+
return this.#hits[0];
|
|
2084
|
+
}
|
|
2085
|
+
/** Drops every cached hierarchy. The next `raycast` rebuilds what it needs. */
|
|
2086
|
+
dispose() {
|
|
2087
|
+
this.#trees = /* @__PURE__ */ new WeakMap();
|
|
2088
|
+
this.#hits.length = 0;
|
|
2089
|
+
}
|
|
2090
|
+
#collect(object) {
|
|
2091
|
+
if (object.layers.test(this.#raycaster.layers)) {
|
|
2092
|
+
const tree = this.#treeFor(object);
|
|
2093
|
+
if (tree === void 0) object.raycast(this.#raycaster, this.#hits);
|
|
2094
|
+
else tree.raycastObject3D(object, this.#raycaster, this.#hits);
|
|
639
2095
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
2096
|
+
for (const child of object.children) this.#collect(child);
|
|
2097
|
+
}
|
|
2098
|
+
#treeFor(object) {
|
|
2099
|
+
if (!(object instanceof Mesh)) return void 0;
|
|
2100
|
+
const deformed = object;
|
|
2101
|
+
if (deformed.isSkinnedMesh === true || deformed.isInstancedMesh === true || deformed.isBatchedMesh === true || (object.morphTargetInfluences?.length ?? 0) > 0)
|
|
2102
|
+
return void 0;
|
|
2103
|
+
const geometry = object.geometry;
|
|
2104
|
+
const position = geometry.attributes.position;
|
|
2105
|
+
if (position === void 0 || position.count === 0) return void 0;
|
|
2106
|
+
const cached = this.#trees.get(geometry);
|
|
2107
|
+
if (cached !== void 0 && cached.version === position.version) return cached.tree;
|
|
2108
|
+
const tree = new MeshBVH(geometry);
|
|
2109
|
+
this.#trees.set(geometry, { tree, version: position.version });
|
|
2110
|
+
return tree;
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
643
2113
|
function readCanvasSize(canvas) {
|
|
644
2114
|
return [
|
|
645
2115
|
Math.max(1, canvas.clientWidth || globalThis.innerWidth || 1),
|
|
@@ -662,6 +2132,10 @@ function wrapRenderer(raw, kind) {
|
|
|
662
2132
|
domElement: raw.domElement,
|
|
663
2133
|
kind,
|
|
664
2134
|
raw,
|
|
2135
|
+
compileAsync: async (scene, camera) => {
|
|
2136
|
+
if (typeof raw.compileAsync !== "function") return;
|
|
2137
|
+
await raw.compileAsync(scene, camera);
|
|
2138
|
+
},
|
|
665
2139
|
compute: (node) => {
|
|
666
2140
|
if (kind !== "webgpu") throw new Error(`compute is unavailable on the ${kind} renderer.`);
|
|
667
2141
|
if (typeof raw.compute !== "function")
|
|
@@ -677,6 +2151,17 @@ function wrapRenderer(raw, kind) {
|
|
|
677
2151
|
if (outputPipeline === void 0) raw.render(scene, camera);
|
|
678
2152
|
else outputPipeline.render();
|
|
679
2153
|
},
|
|
2154
|
+
renderOverlay: (scene, camera) => {
|
|
2155
|
+
const hadOwnAutoClear = Object.hasOwn(raw, "autoClear");
|
|
2156
|
+
const autoClear = raw.autoClear;
|
|
2157
|
+
raw.autoClear = false;
|
|
2158
|
+
try {
|
|
2159
|
+
raw.render(scene, camera);
|
|
2160
|
+
} finally {
|
|
2161
|
+
if (hadOwnAutoClear) raw.autoClear = autoClear;
|
|
2162
|
+
else Reflect.deleteProperty(raw, "autoClear");
|
|
2163
|
+
}
|
|
2164
|
+
},
|
|
680
2165
|
setOutputNode: (node) => {
|
|
681
2166
|
if (kind !== "webgpu")
|
|
682
2167
|
throw new Error(`setOutputNode is unavailable on the ${kind} renderer.`);
|
|
@@ -689,16 +2174,23 @@ function wrapRenderer(raw, kind) {
|
|
|
689
2174
|
setSize: (width, height, updateStyle = false) => raw.setSize(width, height, updateStyle)
|
|
690
2175
|
};
|
|
691
2176
|
}
|
|
692
|
-
function addResizeHandling(renderer, source) {
|
|
2177
|
+
function addResizeHandling(renderer, source, resolutionScale) {
|
|
693
2178
|
const resize = () => {
|
|
694
2179
|
const [width, height] = source?.readSize(renderer.domElement) ?? readCanvasSize(renderer.domElement);
|
|
695
|
-
renderer.setSize(
|
|
2180
|
+
renderer.setSize(
|
|
2181
|
+
Math.max(1, Math.round(width * resolutionScale)),
|
|
2182
|
+
Math.max(1, Math.round(height * resolutionScale)),
|
|
2183
|
+
false
|
|
2184
|
+
);
|
|
696
2185
|
};
|
|
697
2186
|
resize();
|
|
698
2187
|
return source?.observeResize(renderer.domElement, resize) ?? observeCanvasResize(renderer.domElement, resize);
|
|
699
2188
|
}
|
|
700
2189
|
async function createRenderer(options = {}) {
|
|
701
2190
|
const source = options.source;
|
|
2191
|
+
const resolutionScale = options.resolutionScale ?? 1;
|
|
2192
|
+
if (!Number.isFinite(resolutionScale) || resolutionScale <= 0)
|
|
2193
|
+
throw new Error("renderer.resolutionScale must be finite and positive.");
|
|
702
2194
|
const canvas = options.canvas ?? source?.createCanvas() ?? document.createElement("canvas");
|
|
703
2195
|
const preferWebGPU = options.preferWebGPU ?? true;
|
|
704
2196
|
let renderer;
|
|
@@ -716,7 +2208,7 @@ async function createRenderer(options = {}) {
|
|
|
716
2208
|
const raw = options.webgl2Factory ? options.webgl2Factory(canvas) : new (await import('three')).WebGLRenderer({ canvas, antialias: true });
|
|
717
2209
|
renderer = wrapRenderer(raw, "webgl2");
|
|
718
2210
|
}
|
|
719
|
-
const stopResize = addResizeHandling(renderer, source);
|
|
2211
|
+
const stopResize = addResizeHandling(renderer, source, resolutionScale);
|
|
720
2212
|
const dispose = renderer.dispose;
|
|
721
2213
|
renderer.dispose = () => {
|
|
722
2214
|
stopResize();
|
|
@@ -826,8 +2318,10 @@ function assertDelay(value, label) {
|
|
|
826
2318
|
}
|
|
827
2319
|
function createGameStore(initial, intervalMs = 100) {
|
|
828
2320
|
const store = createStore(() => ({ ...initial }));
|
|
2321
|
+
const current = { value: store.getState() };
|
|
829
2322
|
let pending;
|
|
830
2323
|
let timer;
|
|
2324
|
+
store.subscribe((state) => Object.assign(current, { value: state }));
|
|
831
2325
|
const flush = () => {
|
|
832
2326
|
if (pending === void 0) return;
|
|
833
2327
|
const patch = pending;
|
|
@@ -835,9 +2329,11 @@ function createGameStore(initial, intervalMs = 100) {
|
|
|
835
2329
|
store.setState(patch);
|
|
836
2330
|
};
|
|
837
2331
|
const gameStore = store;
|
|
2332
|
+
gameStore.getState = () => current.value;
|
|
838
2333
|
gameStore.set = (patch) => {
|
|
839
|
-
const next = typeof patch === "function" ? patch(
|
|
2334
|
+
const next = typeof patch === "function" ? patch(current.value) : patch;
|
|
840
2335
|
pending = { ...pending, ...next };
|
|
2336
|
+
current.value = { ...current.value, ...next };
|
|
841
2337
|
};
|
|
842
2338
|
gameStore.flush = flush;
|
|
843
2339
|
gameStore.start = () => {
|
|
@@ -969,6 +2465,26 @@ function clearScene(scene, particles) {
|
|
|
969
2465
|
scene.environment = null;
|
|
970
2466
|
scene.fog = null;
|
|
971
2467
|
}
|
|
2468
|
+
function rendererPerformanceMetrics(raw) {
|
|
2469
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
2470
|
+
const info = raw.info;
|
|
2471
|
+
if (typeof info !== "object" || info === null) return {};
|
|
2472
|
+
const render = info.render;
|
|
2473
|
+
if (typeof render !== "object" || render === null) return {};
|
|
2474
|
+
const drawCalls = render.drawCalls;
|
|
2475
|
+
const calls = drawCalls ?? render.calls;
|
|
2476
|
+
const triangles = render.triangles;
|
|
2477
|
+
return {
|
|
2478
|
+
...typeof calls === "number" && Number.isFinite(calls) && calls >= 0 ? { drawCalls: calls } : {},
|
|
2479
|
+
...typeof triangles === "number" && Number.isFinite(triangles) && triangles >= 0 ? { triangles } : {}
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
function addRenderPerformanceMetrics(world, overlay) {
|
|
2483
|
+
return {
|
|
2484
|
+
...world.drawCalls === void 0 || overlay.drawCalls === void 0 ? {} : { drawCalls: world.drawCalls + overlay.drawCalls },
|
|
2485
|
+
...world.triangles === void 0 || overlay.triangles === void 0 ? {} : { triangles: world.triangles + overlay.triangles }
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
972
2488
|
function createCamera(config) {
|
|
973
2489
|
if (config === void 0) return new PerspectiveCamera(60, 1, 0.1, 2e3);
|
|
974
2490
|
validateCameraConfig(config);
|
|
@@ -990,10 +2506,12 @@ var GameImpl = class {
|
|
|
990
2506
|
#state;
|
|
991
2507
|
#initialState;
|
|
992
2508
|
#loop;
|
|
2509
|
+
#collapse;
|
|
993
2510
|
#cleanup = [];
|
|
994
2511
|
#particles = /* @__PURE__ */ new Set();
|
|
995
2512
|
#entities;
|
|
996
2513
|
#random;
|
|
2514
|
+
#picker;
|
|
997
2515
|
#scheduler;
|
|
998
2516
|
#activePlugins = [];
|
|
999
2517
|
#disposedPlugins = /* @__PURE__ */ new Set();
|
|
@@ -1043,6 +2561,7 @@ var GameImpl = class {
|
|
|
1043
2561
|
for (const plugin of this.#config.plugins ?? []) {
|
|
1044
2562
|
if (typeof plugin !== "function") plugin.sceneExit?.(ctx);
|
|
1045
2563
|
}
|
|
2564
|
+
this.#collapse?.restore();
|
|
1046
2565
|
clearScene(ctx.scene, this.#particles);
|
|
1047
2566
|
const scene = new SceneType();
|
|
1048
2567
|
this.#scene = scene;
|
|
@@ -1102,15 +2621,26 @@ var GameImpl = class {
|
|
|
1102
2621
|
const inputTarget = this.#config.inputTarget ?? (platform === void 0 ? typeof window === "undefined" ? canvas : window : platform.inputTarget ?? canvas);
|
|
1103
2622
|
this.#input = new InputMap(this.#config.input, inputTarget, canvas, platform?.input);
|
|
1104
2623
|
this.#state.start();
|
|
1105
|
-
const threeScene = new Scene
|
|
2624
|
+
const threeScene = new Scene();
|
|
1106
2625
|
const camera = createCamera(this.#config.camera);
|
|
1107
2626
|
const viewport = new Viewport({ camera, renderer: this.#renderer, source: platform?.viewport });
|
|
2627
|
+
const canvasLayer = new CanvasLayer(viewport);
|
|
1108
2628
|
this.#viewport = viewport;
|
|
1109
2629
|
const assets = createAssetLoader(this.#config.assets);
|
|
1110
2630
|
const entities = new Registry();
|
|
1111
2631
|
const random = createRandom(this.#config.seed);
|
|
1112
2632
|
const scheduler = new Scheduler();
|
|
2633
|
+
const input = this.#input;
|
|
2634
|
+
const picker = new ScenePicker({
|
|
2635
|
+
camera,
|
|
2636
|
+
pointer: () => input.raw.pointer.position,
|
|
2637
|
+
scene: threeScene,
|
|
2638
|
+
viewport
|
|
2639
|
+
});
|
|
2640
|
+
this.#picker = picker;
|
|
1113
2641
|
const loopState = {};
|
|
2642
|
+
const collapse = new SceneCollapse(threeScene);
|
|
2643
|
+
this.#collapse = collapse;
|
|
1114
2644
|
const ctx = {
|
|
1115
2645
|
add: (object) => {
|
|
1116
2646
|
threeScene.add(object);
|
|
@@ -1126,6 +2656,7 @@ var GameImpl = class {
|
|
|
1126
2656
|
assets,
|
|
1127
2657
|
after: (delay, callback) => scheduler.after(delay, callback),
|
|
1128
2658
|
camera,
|
|
2659
|
+
canvasLayer,
|
|
1129
2660
|
entities,
|
|
1130
2661
|
every: (callback) => scheduler.every(callback),
|
|
1131
2662
|
get fps() {
|
|
@@ -1135,6 +2666,17 @@ var GameImpl = class {
|
|
|
1135
2666
|
input: this.#input,
|
|
1136
2667
|
physics: void 0,
|
|
1137
2668
|
random,
|
|
2669
|
+
raycast: (options) => picker.raycast(options),
|
|
2670
|
+
startup: {
|
|
2671
|
+
get phase() {
|
|
2672
|
+
if (collapse.settled) return "ready";
|
|
2673
|
+
return collapse.progress >= 1 ? "collapsing" : "observing";
|
|
2674
|
+
},
|
|
2675
|
+
get progress() {
|
|
2676
|
+
return collapse.progress;
|
|
2677
|
+
},
|
|
2678
|
+
whenReady: () => collapse.whenSettled()
|
|
2679
|
+
},
|
|
1138
2680
|
renderer: this.#renderer,
|
|
1139
2681
|
viewport,
|
|
1140
2682
|
scene: threeScene,
|
|
@@ -1159,8 +2701,19 @@ var GameImpl = class {
|
|
|
1159
2701
|
particle.process(this.#renderer);
|
|
1160
2702
|
}
|
|
1161
2703
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
2704
|
+
this.#collapse?.frame();
|
|
2705
|
+
let worldMetrics;
|
|
2706
|
+
if (!canvasLayer.opaque) {
|
|
2707
|
+
renderer.render(threeScene, camera);
|
|
2708
|
+
this.#scene?.render(ctx);
|
|
2709
|
+
worldMetrics = rendererPerformanceMetrics(renderer.raw);
|
|
2710
|
+
}
|
|
2711
|
+
if (canvasLayer.scene.children.length > 0) {
|
|
2712
|
+
renderer.renderOverlay(canvasLayer.scene, canvasLayer.camera);
|
|
2713
|
+
const overlayMetrics = rendererPerformanceMetrics(renderer.raw);
|
|
2714
|
+
return worldMetrics === void 0 ? overlayMetrics : addRenderPerformanceMetrics(worldMetrics, overlayMetrics);
|
|
2715
|
+
}
|
|
2716
|
+
return worldMetrics ?? {};
|
|
1164
2717
|
},
|
|
1165
2718
|
onUpdate: (dt) => {
|
|
1166
2719
|
if (this.#paused) return;
|
|
@@ -1173,17 +2726,23 @@ var GameImpl = class {
|
|
|
1173
2726
|
else scene2?.update(ctx, dt);
|
|
1174
2727
|
if (this.#scene !== scene2 || this.#sceneFrame !== frame) return;
|
|
1175
2728
|
for (const plugin of this.#activePlugins) plugin.update?.(ctx, dt);
|
|
2729
|
+
this.#entities?.sweep();
|
|
1176
2730
|
},
|
|
1177
2731
|
step: this.#config.step
|
|
1178
2732
|
});
|
|
1179
2733
|
loopState.current = gameLoop;
|
|
1180
2734
|
this.#loop = gameLoop;
|
|
2735
|
+
const startGates = [];
|
|
1181
2736
|
const runtime = {
|
|
1182
2737
|
fixedStep: (ticks) => gameLoop.advance(ticks),
|
|
2738
|
+
holdStart: (gate) => startGates.push(gate),
|
|
2739
|
+
observations: createRuntimeObservations(),
|
|
2740
|
+
tick: gameLoop.tick,
|
|
1183
2741
|
random,
|
|
1184
2742
|
rapier: null,
|
|
1185
2743
|
seed: this.#config.seed ?? null,
|
|
1186
|
-
step: gameLoop.step
|
|
2744
|
+
step: gameLoop.step,
|
|
2745
|
+
runtimeDiagnosticsSeries: () => gameLoop.runtimeDiagnosticsSeries()
|
|
1187
2746
|
};
|
|
1188
2747
|
for (const plugin of this.#config.plugins ?? []) {
|
|
1189
2748
|
const cleanup = typeof plugin === "function" ? plugin(ctx) : await plugin.setup?.(ctx, runtime);
|
|
@@ -1210,6 +2769,18 @@ var GameImpl = class {
|
|
|
1210
2769
|
this.#teardown(ctx);
|
|
1211
2770
|
return;
|
|
1212
2771
|
}
|
|
2772
|
+
if (startGates.length > 0) {
|
|
2773
|
+
try {
|
|
2774
|
+
await Promise.all(startGates);
|
|
2775
|
+
} catch (error) {
|
|
2776
|
+
this.#teardown(ctx);
|
|
2777
|
+
throw error;
|
|
2778
|
+
}
|
|
2779
|
+
if (this.#aborted) {
|
|
2780
|
+
this.#teardown(ctx);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
1213
2784
|
this.#started = true;
|
|
1214
2785
|
gameLoop.start();
|
|
1215
2786
|
}
|
|
@@ -1244,6 +2815,7 @@ var GameImpl = class {
|
|
|
1244
2815
|
if (ctx !== void 0) clearScene(ctx.scene, this.#particles);
|
|
1245
2816
|
this.#input?.dispose();
|
|
1246
2817
|
this.#state.stop();
|
|
2818
|
+
ctx?.canvasLayer.dispose();
|
|
1247
2819
|
this.#viewport?.dispose();
|
|
1248
2820
|
const renderer = this.#renderer;
|
|
1249
2821
|
renderer?.dispose();
|
|
@@ -1258,15 +2830,126 @@ var GameImpl = class {
|
|
|
1258
2830
|
this.#ctx = void 0;
|
|
1259
2831
|
this.#loop = void 0;
|
|
1260
2832
|
this.#random = void 0;
|
|
2833
|
+
this.#picker?.dispose();
|
|
2834
|
+
this.#picker = void 0;
|
|
1261
2835
|
this.#scheduler = void 0;
|
|
1262
2836
|
this.#disposedPlugins.clear();
|
|
1263
2837
|
this.#paused = false;
|
|
1264
2838
|
this.#started = false;
|
|
2839
|
+
if ((ctx?.scene.children.length ?? 0) > 0)
|
|
2840
|
+
throw new Error("IGame teardown leaked scene objects.");
|
|
1265
2841
|
}
|
|
1266
2842
|
};
|
|
2843
|
+
function createRuntimeObservations() {
|
|
2844
|
+
const contributions = /* @__PURE__ */ new Set();
|
|
2845
|
+
return {
|
|
2846
|
+
contribute: (contribution) => {
|
|
2847
|
+
contributions.add(contribution);
|
|
2848
|
+
return () => contributions.delete(contribution);
|
|
2849
|
+
},
|
|
2850
|
+
contributions: () => [...contributions]
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
1267
2853
|
function defineGame(config) {
|
|
1268
2854
|
return new GameImpl(config);
|
|
1269
2855
|
}
|
|
2856
|
+
var CURVE_DIVISIONS = 128;
|
|
2857
|
+
var PathFollow3D = class {
|
|
2858
|
+
curve;
|
|
2859
|
+
loop;
|
|
2860
|
+
totalLength;
|
|
2861
|
+
#samples;
|
|
2862
|
+
#progress = 0;
|
|
2863
|
+
#speed;
|
|
2864
|
+
constructor(options) {
|
|
2865
|
+
if (options.points.length < 3) throw new Error("PathFollow3D requires at least three points.");
|
|
2866
|
+
if (options.speed !== void 0 && (!Number.isFinite(options.speed) || options.speed < 0))
|
|
2867
|
+
throw new Error("PathFollow3D speed must be a finite non-negative number.");
|
|
2868
|
+
const points = options.points.map((point) => {
|
|
2869
|
+
if (![point.x, point.y, point.z].every(Number.isFinite))
|
|
2870
|
+
throw new Error("PathFollow3D points must be finite.");
|
|
2871
|
+
return point.clone();
|
|
2872
|
+
});
|
|
2873
|
+
this.loop = options.loop ?? false;
|
|
2874
|
+
this.#speed = options.speed ?? 0;
|
|
2875
|
+
this.curve = new CatmullRomCurve3(points, this.loop, "centripetal", 0.5);
|
|
2876
|
+
this.totalLength = this.curve.getLengths(CURVE_DIVISIONS).at(-1) ?? 0;
|
|
2877
|
+
if (!(this.totalLength > 0)) throw new Error("PathFollow3D requires a positive-length path.");
|
|
2878
|
+
const spaced = this.curve.getSpacedPoints(CURVE_DIVISIONS);
|
|
2879
|
+
this.#samples = this.loop ? spaced.slice(0, CURVE_DIVISIONS) : spaced;
|
|
2880
|
+
}
|
|
2881
|
+
get completed() {
|
|
2882
|
+
return !this.loop && this.#progress >= this.totalLength;
|
|
2883
|
+
}
|
|
2884
|
+
get progress() {
|
|
2885
|
+
return this.#progress;
|
|
2886
|
+
}
|
|
2887
|
+
get speed() {
|
|
2888
|
+
return this.#speed;
|
|
2889
|
+
}
|
|
2890
|
+
set speed(value) {
|
|
2891
|
+
if (!Number.isFinite(value) || value < 0)
|
|
2892
|
+
throw new Error("PathFollow3D speed must be a finite non-negative number.");
|
|
2893
|
+
this.#speed = value;
|
|
2894
|
+
}
|
|
2895
|
+
advance(dt) {
|
|
2896
|
+
if (!Number.isFinite(dt) || dt < 0)
|
|
2897
|
+
throw new Error("PathFollow3D.advance requires a finite non-negative delta.");
|
|
2898
|
+
this.progressTo(this.#progress + this.#speed * dt);
|
|
2899
|
+
return this.sample();
|
|
2900
|
+
}
|
|
2901
|
+
progressTo(distance) {
|
|
2902
|
+
if (!Number.isFinite(distance) || distance < 0)
|
|
2903
|
+
throw new Error("PathFollow3D progress must be a finite non-negative number.");
|
|
2904
|
+
this.#progress = this.loop ? distance % this.totalLength : Math.min(distance, this.totalLength);
|
|
2905
|
+
return this;
|
|
2906
|
+
}
|
|
2907
|
+
sample(distance = this.#progress) {
|
|
2908
|
+
if (!Number.isFinite(distance) || distance < 0)
|
|
2909
|
+
throw new Error("PathFollow3D sample distance must be a finite non-negative number.");
|
|
2910
|
+
const progress = this.loop ? distance % this.totalLength : Math.min(distance, this.totalLength);
|
|
2911
|
+
const u = progress / this.totalLength;
|
|
2912
|
+
return {
|
|
2913
|
+
point: this.curve.getPointAt(u),
|
|
2914
|
+
progress,
|
|
2915
|
+
tangent: this.curve.getTangentAt(u).normalize()
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
pointAt(distance) {
|
|
2919
|
+
return this.sample(distance);
|
|
2920
|
+
}
|
|
2921
|
+
project(position) {
|
|
2922
|
+
if (![position.x, position.y, position.z].every(Number.isFinite))
|
|
2923
|
+
throw new Error("PathFollow3D projection position must be finite.");
|
|
2924
|
+
let nearest = Number.POSITIVE_INFINITY;
|
|
2925
|
+
let nearestIndex = 0;
|
|
2926
|
+
for (const [index, sample] of this.#samples.entries()) {
|
|
2927
|
+
const distance = sample.distanceToSquared(position);
|
|
2928
|
+
if (distance < nearest) {
|
|
2929
|
+
nearest = distance;
|
|
2930
|
+
nearestIndex = index;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
const last = this.#samples.length - 1;
|
|
2934
|
+
const segmentCount = this.loop ? this.#samples.length : last;
|
|
2935
|
+
const segment = Math.min(nearestIndex, Math.max(0, segmentCount - 1));
|
|
2936
|
+
const point = this.#samples[nearestIndex];
|
|
2937
|
+
if (point === void 0) throw new Error("PathFollow3D projection sample is missing.");
|
|
2938
|
+
const tangentStart = this.#samples[segment];
|
|
2939
|
+
if (tangentStart === void 0) throw new Error("PathFollow3D projection tangent is missing.");
|
|
2940
|
+
const nextIndex = this.loop ? (segment + 1) % this.#samples.length : Math.min(segment + 1, last);
|
|
2941
|
+
const next = this.#samples[nextIndex];
|
|
2942
|
+
if (next === void 0) throw new Error("PathFollow3D projection tangent is missing.");
|
|
2943
|
+
const distanceFromSample = nearestIndex / segmentCount * this.totalLength;
|
|
2944
|
+
return {
|
|
2945
|
+
distanceFromStart: distanceFromSample,
|
|
2946
|
+
lateralDistance: point.distanceTo(position),
|
|
2947
|
+
point: point.clone(),
|
|
2948
|
+
segment,
|
|
2949
|
+
tangent: next.clone().sub(tangentStart).normalize()
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
1270
2953
|
|
|
1271
2954
|
// ../playtest/dist/index.js
|
|
1272
2955
|
var PLAYTEST_CAPABILITY_REGISTRY = [
|
|
@@ -1285,10 +2968,11 @@ var PLAYTEST_CAPABILITY_REGISTRY = [
|
|
|
1285
2968
|
capability("runtime.audio", "Samples application-owned audio runtime state."),
|
|
1286
2969
|
capability("runtime.components", "Samples registered JSON-safe component state."),
|
|
1287
2970
|
capability("runtime.contacts", "Samples bounded collision and trigger contacts."),
|
|
1288
|
-
capability("runtime.diagnostics", "
|
|
2971
|
+
capability("runtime.diagnostics", "Captures application runtime errors."),
|
|
1289
2972
|
capability("runtime.events", "Drains bounded application event observations."),
|
|
1290
2973
|
capability("runtime.fixedStep", "Advances an application-owned deterministic tick."),
|
|
1291
2974
|
capability("runtime.physics", "Samples bounded application-owned physics observations."),
|
|
2975
|
+
capability("runtime.performance", "Samples bounded per-render frame cost and renderer counts."),
|
|
1292
2976
|
capability("runtime.resources", "Reads and writes registered JSON-safe application state."),
|
|
1293
2977
|
capability("runtime.state", "Samples application-owned state-machine state."),
|
|
1294
2978
|
capability("runtime.tags", "Samples bounded application-owned entity tags."),
|
|
@@ -1320,14 +3004,14 @@ function parseReplayRecording(value) {
|
|
|
1320
3004
|
fail("bad header");
|
|
1321
3005
|
const ticks = root.ticks;
|
|
1322
3006
|
if (!Number.isInteger(ticks) || ticks < 1) fail("ticks must be positive");
|
|
1323
|
-
const
|
|
1324
|
-
if (!Array.isArray(
|
|
3007
|
+
const input = root.input;
|
|
3008
|
+
if (!Array.isArray(input) || input.length === 0) fail("empty input", "TN_REPLAY_EMPTY");
|
|
1325
3009
|
const runtime = checked(root.runtime, ["agent", "core", "rapier", "step"]);
|
|
1326
3010
|
const step = runtime.step;
|
|
1327
3011
|
if (typeof runtime.agent !== "string" || !runtime.agent || typeof runtime.core !== "string" || !runtime.core || runtime.rapier !== null && typeof runtime.rapier !== "string" || !Number.isFinite(step) || step <= 0)
|
|
1328
3012
|
fail("runtime fingerprint is invalid");
|
|
1329
3013
|
let previousTick = -1;
|
|
1330
|
-
for (const rawSample of
|
|
3014
|
+
for (const rawSample of input) {
|
|
1331
3015
|
const sample = checked(rawSample, ["keys", "pointer", "tick"]);
|
|
1332
3016
|
const tick = sample.tick;
|
|
1333
3017
|
if (!Number.isInteger(tick) || tick <= previousTick || tick >= ticks) fail("bad tick");
|
|
@@ -1493,7 +3177,7 @@ function createReplayDriver(recording, target, pointerTarget = target) {
|
|
|
1493
3177
|
}
|
|
1494
3178
|
|
|
1495
3179
|
// src/scene.ts
|
|
1496
|
-
var
|
|
3180
|
+
var Scene2 = class {
|
|
1497
3181
|
static initialState = void 0;
|
|
1498
3182
|
load(_ctx) {
|
|
1499
3183
|
}
|
|
@@ -1511,4 +3195,4 @@ var Scene = class {
|
|
|
1511
3195
|
// src/index.ts
|
|
1512
3196
|
var version = "0.1.0";
|
|
1513
3197
|
|
|
1514
|
-
export { AnimationPlayer, AudioBus,
|
|
3198
|
+
export { AnimationPlayer, AudioBus, CanvasLayer, GPUParticles3D, PathFollow3D, Scene2 as Scene, ScenePicker, Scheduler, createRandom, createReplayDriver, defineGame, replay, version };
|