@ifc-lite/renderer 3.0.0 → 3.1.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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mesh-entity-hosting.d.ts +14 -0
- package/dist/mesh-entity-hosting.d.ts.map +1 -0
- package/dist/mesh-entity-hosting.js +25 -0
- package/dist/mesh-entity-hosting.js.map +1 -0
- package/dist/scene-batch-upload.d.ts +3 -1
- package/dist/scene-batch-upload.d.ts.map +1 -1
- package/dist/scene-batch-upload.js +9 -1
- package/dist/scene-batch-upload.js.map +1 -1
- package/dist/scene-derived-batches.d.ts +84 -0
- package/dist/scene-derived-batches.d.ts.map +1 -0
- package/dist/scene-derived-batches.js +73 -0
- package/dist/scene-derived-batches.js.map +1 -0
- package/dist/scene.d.ts +21 -22
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +69 -78
- package/dist/scene.js.map +1 -1
- package/package.json +3 -3
package/dist/scene.js
CHANGED
|
@@ -10,6 +10,7 @@ import { prepareSceneAuthoredOwner } from './scene-authored-owner.js';
|
|
|
10
10
|
import { interleaveTexturedVertices } from './textured-vertices.js';
|
|
11
11
|
import { RgbaTexturePool } from './rgba-texture-pool.js';
|
|
12
12
|
import { splitMeshForStreaming } from './scene-stream-split.js';
|
|
13
|
+
import { hostsOtherEntities } from './mesh-entity-hosting.js';
|
|
13
14
|
import { BATCH_CONSTANTS } from './constants.js';
|
|
14
15
|
import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, rayIntersectsBox, } from './scene-raycaster.js';
|
|
15
16
|
import { selectBoundingBoxesInRect } from './scene-rect-select.js';
|
|
@@ -17,6 +18,7 @@ import { splitMeshDataForBufferLimit, cachedWorldAabb, worldAabbFromPieces, dest
|
|
|
17
18
|
import { sumResidentGpuBytes } from './render-stats.js';
|
|
18
19
|
import { composeInstancedOverrideColor } from './instanced-override-color.js';
|
|
19
20
|
import { bucketBaseKeyFor } from './chunk-grid.js';
|
|
21
|
+
import { inheritedQuantization, groupOverridePieces, cloneOverrides, overlaysInvalidatedBy } from './scene-derived-batches.js';
|
|
20
22
|
import { VisibilityEpochTracker } from './visibility-epoch.js';
|
|
21
23
|
import { isEntityVisible } from './entity-visibility.js';
|
|
22
24
|
import { planInstancedGhosting } from './instanced-ghost-plan.js';
|
|
@@ -279,6 +281,7 @@ export class Scene {
|
|
|
279
281
|
// Overlay batches render on top using depthCompare 'equal', so they only
|
|
280
282
|
// paint where original geometry already wrote depth. Clearing is instant.
|
|
281
283
|
overrideBatches = [];
|
|
284
|
+
overlaySources = new Map(); // overridden piece -> bucket key its overlay inherited from (#4832)
|
|
282
285
|
// Defensively-typed: the renderer is the sole writer (via setColorOverrides),
|
|
283
286
|
// external readers go through getColorOverrides() and get a ReadonlyMap.
|
|
284
287
|
colorOverrides = null;
|
|
@@ -458,28 +461,20 @@ export class Scene {
|
|
|
458
461
|
* Enable 12-byte lattice-quantized batch vertices (issue #1682 phase 6).
|
|
459
462
|
* ONLY call after the renderer verified its quantized pipeline variants
|
|
460
463
|
* exist (see Renderer.enableQuantizedBatches) — quantized buffers are
|
|
461
|
-
* undrawable without them. Applies to batches built from now on
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
464
|
+
* undrawable without them. Applies to batches built from now on: bucket
|
|
465
|
+
* batches and fragments quantize onto the SAME 2^-10 lattice when their
|
|
466
|
+
* extent fits the u16 range (else f32); partial + override batches INHERIT
|
|
467
|
+
* their source batch's decision (#4832, `scene-derived-batches.ts`), so
|
|
468
|
+
* depth-equal overlay matching and cross-batch coincidence stay bit-exact.
|
|
466
469
|
*/
|
|
467
470
|
setQuantizedBatches(enabled) {
|
|
468
471
|
this.quantizedBatchesEnabled = enabled;
|
|
469
472
|
}
|
|
470
|
-
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* fell back to f32, e.g. >64m extent, must NOT snap). Falls back to the
|
|
474
|
-
* global flag when the mesh isn't bucketed (mid-stream hydration).
|
|
475
|
-
*/
|
|
473
|
+
/** Whether THIS mesh's source batch renders quantized — drives the hydrated-mesh
|
|
474
|
+
* lattice snap in createMeshFromData (an f32 batch must NOT snap). Same rule
|
|
475
|
+
* as overlay/partial batches; global flag while unbucketed. */
|
|
476
476
|
isMeshQuantized(meshData) {
|
|
477
|
-
|
|
478
|
-
return false;
|
|
479
|
-
const bucket = this.meshDataBucket.get(meshData);
|
|
480
|
-
if (bucket?.batchedMesh)
|
|
481
|
-
return bucket.batchedMesh.quantized !== undefined;
|
|
482
|
-
return true;
|
|
477
|
+
return inheritedQuantization(this.quantizedBatchesEnabled, this.meshDataBucket.get(meshData)?.batchedMesh) !== 'off';
|
|
483
478
|
}
|
|
484
479
|
/** Set (or clear) the HOST budget in bytes for bucket CPU geometry. */
|
|
485
480
|
setHostResidencyBudget(bytes) {
|
|
@@ -1127,8 +1122,10 @@ export class Scene {
|
|
|
1127
1122
|
rebuildPendingBatches(device, pipeline) {
|
|
1128
1123
|
if (this.pendingBatchKeys.size === 0)
|
|
1129
1124
|
return;
|
|
1125
|
+
const rebuilt = [];
|
|
1130
1126
|
for (const key of this.pendingBatchKeys) {
|
|
1131
1127
|
const bucket = this.buckets.get(key);
|
|
1128
|
+
const previousQuantized = bucket?.batchedMesh?.quantized !== undefined;
|
|
1132
1129
|
// Destroy old GPU batch if it exists
|
|
1133
1130
|
if (bucket?.batchedMesh) {
|
|
1134
1131
|
// Slot keys embed the batch id and the replacement gets a fresh one, so
|
|
@@ -1148,6 +1145,7 @@ export class Scene {
|
|
|
1148
1145
|
const color = bucket.meshData[0].color;
|
|
1149
1146
|
const batchedMesh = this.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
|
|
1150
1147
|
bucket.batchedMesh = batchedMesh;
|
|
1148
|
+
rebuilt.push({ bucket, previousQuantized });
|
|
1151
1149
|
}
|
|
1152
1150
|
// Rebuild the flat render array from all buckets (148 max batches — not perf critical)
|
|
1153
1151
|
this.batchedMeshes = [];
|
|
@@ -1157,6 +1155,10 @@ export class Scene {
|
|
|
1157
1155
|
}
|
|
1158
1156
|
}
|
|
1159
1157
|
this.pendingBatchKeys.clear();
|
|
1158
|
+
// Overlays follow their depth writers only when one actually changed under
|
|
1159
|
+
// them (flip / bucket move, #4832); finalize re-applies once itself.
|
|
1160
|
+
if (!this.finalizeInProgress && overlaysInvalidatedBy(rebuilt, this.overlaySources))
|
|
1161
|
+
this.reapplyColorOverrides(device, pipeline);
|
|
1160
1162
|
}
|
|
1161
1163
|
/**
|
|
1162
1164
|
* Check if there are pending batch rebuilds
|
|
@@ -1177,8 +1179,8 @@ export class Scene {
|
|
|
1177
1179
|
* raycast bounds. This is the proper removal path.
|
|
1178
1180
|
*
|
|
1179
1181
|
* Notes:
|
|
1180
|
-
* - For color-merged meshes (
|
|
1181
|
-
* a single MeshData
|
|
1182
|
+
* - For color-merged meshes (`entityIds` naming other entities,
|
|
1183
|
+
* `hostsOtherEntities`) a single MeshData hosts many entities. We do NOT
|
|
1182
1184
|
* drop the whole mesh in that case — that would also remove
|
|
1183
1185
|
* the other entities — but we DO clear the bbox + meshDataMap
|
|
1184
1186
|
* for the requested expressId, so picking and selection stop
|
|
@@ -1207,10 +1209,9 @@ export class Scene {
|
|
|
1207
1209
|
// caller's bulk-count contract.
|
|
1208
1210
|
let removedDedicated = false;
|
|
1209
1211
|
for (const meshData of meshDataList) {
|
|
1210
|
-
// Color-merged path: shared mesh,
|
|
1211
|
-
if (meshData
|
|
1212
|
+
// Color-merged path (entityIds naming OTHER entities): keep the shared mesh, drop our entry.
|
|
1213
|
+
if (hostsOtherEntities(meshData))
|
|
1212
1214
|
continue;
|
|
1213
|
-
}
|
|
1214
1215
|
removedDedicated = true;
|
|
1215
1216
|
// Dedicated mesh — drop from its bucket and decrement the
|
|
1216
1217
|
// bucket's vertexBytes counter so subsequent
|
|
@@ -1943,6 +1944,7 @@ export class Scene {
|
|
|
1943
1944
|
if (!fragmentSet.has(batch))
|
|
1944
1945
|
destroyGpuResources(batch);
|
|
1945
1946
|
}
|
|
1947
|
+
this.reapplyColorOverrides(device, pipeline);
|
|
1946
1948
|
}
|
|
1947
1949
|
/**
|
|
1948
1950
|
* Time-sliced version of finalizeStreaming.
|
|
@@ -2062,6 +2064,7 @@ export class Scene {
|
|
|
2062
2064
|
destroyGpuResources(batch);
|
|
2063
2065
|
}
|
|
2064
2066
|
scene.finalizeInProgress = false;
|
|
2067
|
+
scene.reapplyColorOverrides(device, pipeline); // never throws: the swap above is committed
|
|
2065
2068
|
resolve();
|
|
2066
2069
|
}
|
|
2067
2070
|
catch (err) {
|
|
@@ -2323,8 +2326,9 @@ export class Scene {
|
|
|
2323
2326
|
* @param bucketKey - Optional unique key for this batch. When omitted the
|
|
2324
2327
|
* base color key is used (fine for overlay / partial batches that don't
|
|
2325
2328
|
* participate in the main buckets map).
|
|
2329
|
+
* @param quantization - 'auto' for bucket batches; derived batches pass `inheritedQuantization(source)` (#4832).
|
|
2326
2330
|
*/
|
|
2327
|
-
createBatchedMesh(meshes, color, device, pipeline, bucketKey) {
|
|
2331
|
+
createBatchedMesh(meshes, color, device, pipeline, bucketKey, quantization = this.quantizedBatchesEnabled ? 'auto' : 'off') {
|
|
2328
2332
|
// Keep main's model-local frame and translation registration while staging
|
|
2329
2333
|
// every GPU allocation before publishing Scene state.
|
|
2330
2334
|
const modelIndex = meshes[0]?.modelIndex ?? 0;
|
|
@@ -2332,7 +2336,7 @@ export class Scene {
|
|
|
2332
2336
|
const result = createSceneBatch(meshes, color, device, pipeline, {
|
|
2333
2337
|
id: this.nextBatchId, colorKey: bucketKey ?? this.colorKey(color),
|
|
2334
2338
|
origin: this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex),
|
|
2335
|
-
quantized:
|
|
2339
|
+
quantized: quantization, lod: this.lodBuildsEnabled,
|
|
2336
2340
|
}, bucketKey);
|
|
2337
2341
|
this.nextBatchId++;
|
|
2338
2342
|
if (!this.sharedFrameOrigins.has(modelIndex) && result.origin) {
|
|
@@ -2500,32 +2504,27 @@ export class Scene {
|
|
|
2500
2504
|
this.partialBatchCache.delete(currentCacheKey);
|
|
2501
2505
|
}
|
|
2502
2506
|
}
|
|
2503
|
-
//
|
|
2504
|
-
//
|
|
2505
|
-
//
|
|
2506
|
-
//
|
|
2507
|
-
|
|
2508
|
-
// belongs to this batch when BOTH its cell and colour match).
|
|
2507
|
+
// `colorKey` is the bucket key: match each piece's OWNING bucket exactly.
|
|
2508
|
+
// Matching the base key (suffix "#N" stripped) pulled sibling overflow
|
|
2509
|
+
// buckets' pieces in — drawn twice, wrong quantization inherited (#4832).
|
|
2510
|
+
// Only a batch with no bucket (a streaming fragment) uses the base key.
|
|
2511
|
+
const bucket = this.buckets.get(colorKey);
|
|
2509
2512
|
const baseKey = this.baseColorKey(colorKey);
|
|
2510
2513
|
const visibleMeshData = [];
|
|
2511
2514
|
for (const expressId of visibleIds) {
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
for (const piece of pieces) {
|
|
2516
|
-
// Only include pieces that match this batch's cell + color
|
|
2517
|
-
if (this.bucketBaseKey(piece) === baseKey) {
|
|
2518
|
-
visibleMeshData.push(piece);
|
|
2519
|
-
}
|
|
2515
|
+
for (const piece of this.meshDataMap.get(expressId) ?? []) {
|
|
2516
|
+
if (bucket ? this.meshDataBucket.get(piece) === bucket : this.bucketBaseKey(piece) === baseKey) {
|
|
2517
|
+
visibleMeshData.push(piece);
|
|
2520
2518
|
}
|
|
2521
2519
|
}
|
|
2522
2520
|
}
|
|
2523
2521
|
if (visibleMeshData.length === 0) {
|
|
2524
2522
|
return undefined;
|
|
2525
2523
|
}
|
|
2526
|
-
//
|
|
2524
|
+
// Drawn INSTEAD of its source batch, so it inherits the source's f32/quantized
|
|
2525
|
+
// decision (#4832): the overlay built from the same source must match its depth.
|
|
2527
2526
|
const color = visibleMeshData[0].color;
|
|
2528
|
-
const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline);
|
|
2527
|
+
const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline, undefined, inheritedQuantization(this.quantizedBatchesEnabled, bucket?.batchedMesh));
|
|
2529
2528
|
// Cache it
|
|
2530
2529
|
this.partialBatchCache.set(cacheKey, partialBatch);
|
|
2531
2530
|
this.partialBatchCacheKeys.set(sourceBatchKey, cacheKey);
|
|
@@ -2540,12 +2539,10 @@ export class Scene {
|
|
|
2540
2539
|
// override colors. They are rendered on top of existing depth via the overlay
|
|
2541
2540
|
// pipeline (depthCompare 'equal'), so hidden entities never leak through.
|
|
2542
2541
|
/**
|
|
2543
|
-
* Set color overrides for lens coloring.
|
|
2544
|
-
*
|
|
2545
|
-
*
|
|
2546
|
-
*
|
|
2547
|
-
* Applies the same buffer-size splitting as regular batches to prevent
|
|
2548
|
-
* GPU buffer overflow on large models.
|
|
2542
|
+
* Set color overrides for lens / chart / IDS / compare / 4D coloring. Overlay
|
|
2543
|
+
* batches are grouped by SOURCE bucket + override color and inherit the
|
|
2544
|
+
* source batch's quantization (#4832, `scene-derived-batches.ts`). Original
|
|
2545
|
+
* batches are NEVER modified — clearing is instant.
|
|
2549
2546
|
*/
|
|
2550
2547
|
setColorOverrides(overrides, device, pipeline) {
|
|
2551
2548
|
// Destroy previous overlay batches
|
|
@@ -2564,44 +2561,37 @@ export class Scene {
|
|
|
2564
2561
|
this.setInstancedColorOverrides(null);
|
|
2565
2562
|
return;
|
|
2566
2563
|
}
|
|
2567
|
-
// Defensive copy
|
|
2568
|
-
// without aliasing the renderer's pipeline-routing state. Tuples are
|
|
2569
|
-
// frozen by the readonly type — we don't deep-clone the inner arrays
|
|
2570
|
-
// because they're treated as immutable by every consumer.
|
|
2564
|
+
// Defensive copy: callers may mutate/reuse `overrides`; the tuples are treated as immutable by every consumer.
|
|
2571
2565
|
this.colorOverrides = new Map(overrides);
|
|
2572
|
-
//
|
|
2573
|
-
// bytes in place (no separate overlay pass — the instanced records carry the
|
|
2574
|
-
// override colour directly). No-op when no instanced data is loaded.
|
|
2566
|
+
// Instanced occurrences carry the override colour in their records (no overlay pass); no-op without instanced data.
|
|
2575
2567
|
this.setInstancedColorOverrides(overrides);
|
|
2576
|
-
//
|
|
2577
|
-
|
|
2578
|
-
const colorGroups = new Map();
|
|
2579
|
-
for (const [expressId, color] of overrides) {
|
|
2580
|
-
for (const piece of this.meshDataMap.get(expressId) ?? []) {
|
|
2581
|
-
const key = `${piece.modelIndex ?? 0}:${this.colorKey(color)}`;
|
|
2582
|
-
let group = colorGroups.get(key);
|
|
2583
|
-
if (!group) {
|
|
2584
|
-
group = { color, meshData: [] };
|
|
2585
|
-
colorGroups.set(key, group);
|
|
2586
|
-
}
|
|
2587
|
-
group.meshData.push(piece);
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
// Build overlay batches per override color, splitting if buffers would exceed GPU limit
|
|
2568
|
+
// Bucket keys carry cell + colour + model: an overlay batch is a subset of ONE base batch, in one model.
|
|
2569
|
+
const colorGroups = groupOverridePieces(overrides, (id) => this.meshDataMap.get(id), (piece) => this.meshDataBucket.get(piece), (piece) => this.bucketBaseKey(piece), (c) => this.colorKey(c));
|
|
2591
2570
|
const maxBufferSize = this.getMaxBufferSize(device);
|
|
2592
|
-
for (const
|
|
2593
|
-
if (
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
this.overrideBatches.push(
|
|
2571
|
+
for (const { color, meshData, sourceBatch, sourceKey } of colorGroups.values()) {
|
|
2572
|
+
if (sourceKey !== null)
|
|
2573
|
+
for (const piece of meshData)
|
|
2574
|
+
this.overlaySources.set(piece, sourceKey);
|
|
2575
|
+
const quantization = inheritedQuantization(this.quantizedBatchesEnabled, sourceBatch);
|
|
2576
|
+
for (const chunk of this.splitMeshDataForBufferLimit(meshData, maxBufferSize)) {
|
|
2577
|
+
this.overrideBatches.push(this.createBatchedMesh(chunk, color, device, pipeline, undefined, quantization));
|
|
2599
2578
|
}
|
|
2600
2579
|
}
|
|
2601
2580
|
}
|
|
2602
|
-
/**
|
|
2603
|
-
*
|
|
2604
|
-
*/
|
|
2581
|
+
/** Rebuild the installed overlays against the batches that write depth NOW
|
|
2582
|
+
* (#4832). Best-effort — a committed finalize/rebuild must not be undone by
|
|
2583
|
+
* an overlay allocation failure, so that is reported, not thrown. */
|
|
2584
|
+
reapplyColorOverrides(device, pipeline) {
|
|
2585
|
+
if (!this.colorOverrides)
|
|
2586
|
+
return;
|
|
2587
|
+
try {
|
|
2588
|
+
this.setColorOverrides(cloneOverrides(this.colorOverrides), device, pipeline);
|
|
2589
|
+
}
|
|
2590
|
+
catch (err) {
|
|
2591
|
+
console.warn('[Scene] overlay rebuild after finalize failed — colours may be missing until the next repaint', err);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
/** Clear all color overrides — instant, no batch rebuild needed. */
|
|
2605
2595
|
clearColorOverrides() {
|
|
2606
2596
|
this.destroyOverrideBatches();
|
|
2607
2597
|
this.colorOverrideGeneration++;
|
|
@@ -2644,6 +2634,7 @@ export class Scene {
|
|
|
2644
2634
|
for (const batch of this.overrideBatches)
|
|
2645
2635
|
destroyGpuResources(batch);
|
|
2646
2636
|
this.overrideBatches = [];
|
|
2637
|
+
this.overlaySources.clear();
|
|
2647
2638
|
}
|
|
2648
2639
|
/**
|
|
2649
2640
|
* Clear scene
|