@ifc-lite/renderer 1.49.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +37 -27
- package/dist/camera-animation.js.map +1 -1
- package/dist/camera-controls.d.ts +26 -15
- package/dist/camera-controls.d.ts.map +1 -1
- package/dist/camera-controls.js +32 -18
- package/dist/camera-controls.js.map +1 -1
- package/dist/camera-framing.d.ts +60 -21
- package/dist/camera-framing.d.ts.map +1 -1
- package/dist/camera-framing.js +168 -131
- package/dist/camera-framing.js.map +1 -1
- package/dist/camera-preset-view.d.ts +2 -2
- package/dist/camera-preset-view.d.ts.map +1 -1
- package/dist/camera-preset-view.js +52 -42
- package/dist/camera-preset-view.js.map +1 -1
- package/dist/camera.d.ts +50 -6
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +113 -13
- package/dist/camera.js.map +1 -1
- package/dist/device.d.ts +11 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +71 -5
- package/dist/device.js.map +1 -1
- package/dist/federation-registry.d.ts.map +1 -1
- package/dist/federation-registry.js +19 -2
- package/dist/federation-registry.js.map +1 -1
- package/dist/frame-timing-cpu.d.ts +31 -0
- package/dist/frame-timing-cpu.d.ts.map +1 -0
- package/dist/frame-timing-cpu.js +25 -0
- package/dist/frame-timing-cpu.js.map +1 -0
- package/dist/frame-timing-gpu.d.ts +144 -0
- package/dist/frame-timing-gpu.d.ts.map +1 -0
- package/dist/frame-timing-gpu.js +176 -0
- package/dist/frame-timing-gpu.js.map +1 -0
- package/dist/frame-timing-stats.d.ts +100 -0
- package/dist/frame-timing-stats.d.ts.map +1 -0
- package/dist/frame-timing-stats.js +116 -0
- package/dist/frame-timing-stats.js.map +1 -0
- package/dist/frame-timing.d.ts +102 -0
- package/dist/frame-timing.d.ts.map +1 -0
- package/dist/frame-timing.js +112 -0
- package/dist/frame-timing.js.map +1 -0
- package/dist/index.d.ts +77 -59
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +272 -73
- package/dist/index.js.map +1 -1
- package/dist/instanced-override-color.d.ts +17 -0
- package/dist/instanced-override-color.d.ts.map +1 -0
- package/dist/instanced-override-color.js +22 -0
- package/dist/instanced-override-color.js.map +1 -0
- package/dist/instanced-render.d.ts +10 -0
- package/dist/instanced-render.d.ts.map +1 -1
- package/dist/instanced-render.js +13 -3
- package/dist/instanced-render.js.map +1 -1
- package/dist/lod-simplify.d.ts +0 -9
- package/dist/lod-simplify.d.ts.map +1 -1
- package/dist/lod-simplify.js +45 -7
- package/dist/lod-simplify.js.map +1 -1
- package/dist/pick-resolve.d.ts +125 -0
- package/dist/pick-resolve.d.ts.map +1 -0
- package/dist/pick-resolve.js +127 -0
- package/dist/pick-resolve.js.map +1 -0
- package/dist/picker.d.ts.map +1 -1
- package/dist/picker.js +20 -46
- package/dist/picker.js.map +1 -1
- package/dist/picking-manager.d.ts.map +1 -1
- package/dist/picking-manager.js +13 -1
- package/dist/picking-manager.js.map +1 -1
- package/dist/pipeline.d.ts +16 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +89 -3
- package/dist/pipeline.js.map +1 -1
- package/dist/pointcloud/point-cloud-spatial-index.d.ts +81 -9
- package/dist/pointcloud/point-cloud-spatial-index.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-spatial-index.js +140 -29
- package/dist/pointcloud/point-cloud-spatial-index.js.map +1 -1
- package/dist/raycast-engine.d.ts +4 -4
- package/dist/raycast-engine.d.ts.map +1 -1
- package/dist/raycast-engine.js +10 -3
- package/dist/raycast-engine.js.map +1 -1
- package/dist/renderer-overlays.d.ts +3 -21
- package/dist/renderer-overlays.d.ts.map +1 -1
- package/dist/renderer-overlays.js +57 -85
- package/dist/renderer-overlays.js.map +1 -1
- package/dist/renderer-symbolic-overlays.d.ts +29 -2
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
- package/dist/renderer-symbolic-overlays.js +61 -16
- package/dist/renderer-symbolic-overlays.js.map +1 -1
- package/dist/scene-contents.d.ts +132 -0
- package/dist/scene-contents.d.ts.map +1 -0
- package/dist/scene-contents.js +5 -0
- package/dist/scene-contents.js.map +1 -0
- package/dist/scene-raycaster.d.ts +14 -8
- package/dist/scene-raycaster.d.ts.map +1 -1
- package/dist/scene-raycaster.js +6 -2
- package/dist/scene-raycaster.js.map +1 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +63 -12
- package/dist/scene.js.map +1 -1
- package/dist/section-2d-overlay.d.ts +52 -63
- package/dist/section-2d-overlay.d.ts.map +1 -1
- package/dist/section-2d-overlay.js +75 -133
- package/dist/section-2d-overlay.js.map +1 -1
- package/dist/section-plane.d.ts +11 -0
- package/dist/section-plane.d.ts.map +1 -1
- package/dist/section-plane.js.map +1 -1
- package/dist/shaders/main.wgsl.d.ts +1 -1
- package/dist/shaders/main.wgsl.d.ts.map +1 -1
- package/dist/shaders/main.wgsl.js +87 -4
- package/dist/shaders/main.wgsl.js.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.d.ts +2 -2
- package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.js +2 -2
- package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
- package/dist/shaders/shadow.wgsl.d.ts +31 -0
- package/dist/shaders/shadow.wgsl.d.ts.map +1 -0
- package/dist/shaders/shadow.wgsl.js +141 -0
- package/dist/shaders/shadow.wgsl.js.map +1 -0
- package/dist/shadow-light-matrix.d.ts +106 -0
- package/dist/shadow-light-matrix.d.ts.map +1 -0
- package/dist/shadow-light-matrix.js +185 -0
- package/dist/shadow-light-matrix.js.map +1 -0
- package/dist/shadow-occluders.d.ts +106 -0
- package/dist/shadow-occluders.d.ts.map +1 -0
- package/dist/shadow-occluders.js +156 -0
- package/dist/shadow-occluders.js.map +1 -0
- package/dist/shadow-pass.d.ts +132 -0
- package/dist/shadow-pass.d.ts.map +1 -0
- package/dist/shadow-pass.js +342 -0
- package/dist/shadow-pass.js.map +1 -0
- package/dist/snap-detector.d.ts +10 -0
- package/dist/snap-detector.d.ts.map +1 -1
- package/dist/snap-detector.js.map +1 -1
- package/dist/symbolic-overlay-pipelines.d.ts +34 -0
- package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
- package/dist/symbolic-overlay-pipelines.js +31 -20
- package/dist/symbolic-overlay-pipelines.js.map +1 -1
- package/dist/types.d.ts +45 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export { WebGPUDevice } from './device.js';
|
|
|
8
8
|
export { RenderPipeline } from './pipeline.js';
|
|
9
9
|
export { Camera } from './camera.js';
|
|
10
10
|
export { pickFitPolicy } from './camera-fit-policy.js';
|
|
11
|
-
|
|
11
|
+
// `Scene` is NOT exported: an exported class republishes every method `SceneContents` above just froze.
|
|
12
12
|
export { Picker } from './picker.js';
|
|
13
13
|
export { MathUtils } from './math.js';
|
|
14
14
|
// The orthonormal camera basis `MathUtils.lookAt` renders through, exposed so
|
|
@@ -19,7 +19,7 @@ export { MathUtils } from './math.js';
|
|
|
19
19
|
// is the same situation from outside it).
|
|
20
20
|
export { viewBasis } from './math.js';
|
|
21
21
|
export { SectionPlaneRenderer } from './section-plane.js';
|
|
22
|
-
|
|
22
|
+
// `Section2DOverlayRenderer` is NOT exported: `Renderer.setLineOverlay` and the section cap drive it from inside. Its types below stay published.
|
|
23
23
|
// IfcAnnotation overlay pipelines (3D world-space). Self-contained — caller
|
|
24
24
|
// passes a GPUDevice + presentation format and invokes `.render(pass, viewProj)`
|
|
25
25
|
// from inside an RGBA-blended pass. See packages/renderer/src/symbolic-overlay-pipelines.ts.
|
|
@@ -30,14 +30,13 @@ export { SymbolicTextAtlas } from './symbolic-text-atlas.js';
|
|
|
30
30
|
// holds the styling primitives shared with the store and UI.
|
|
31
31
|
export { DEFAULT_CAP_STYLE, HATCH_PATTERN_IDS } from './section-cap-style.js';
|
|
32
32
|
export { planeBasis, nearestCardinalAxis } from './section-plane-basis.js';
|
|
33
|
+
export { LINE_OVERLAY_CHANNELS } from './section-2d-overlay.js';
|
|
33
34
|
export { Raycaster } from './raycaster.js';
|
|
34
35
|
export { SnapDetector, SnapType } from './snap-detector.js';
|
|
35
36
|
export { BVH } from './bvh.js';
|
|
36
37
|
export { FederationRegistry, federationRegistry } from './federation-registry.js';
|
|
37
38
|
export * from './types.js';
|
|
38
39
|
export { resolveEnvironment, deriveSkyGradient, packEnvironmentUniforms, ENVIRONMENT_UNIFORM_SIZE, } from './environment.js';
|
|
39
|
-
// Extracted manager classes
|
|
40
|
-
export { PickingManager } from './picking-manager.js';
|
|
41
40
|
export { resolveContributionThresholdPx, projectedAabbRadiusPx } from './contribution-cull.js';
|
|
42
41
|
export { chunkCellKey, bucketBaseKeyFor, DEFAULT_CHUNK_CELL_SIZE } from './chunk-grid.js';
|
|
43
42
|
export { selectEvictions, MIN_EVICTION_AGE_FRAMES } from './residency.js';
|
|
@@ -52,6 +51,15 @@ export { isEntityVisible } from './entity-visibility.js';
|
|
|
52
51
|
// through its own glTF pipeline rather than this one (#2591).
|
|
53
52
|
export { DEFAULT_GHOST_ALPHA, OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
|
|
54
53
|
export { VisibilityEpochTracker } from './visibility-epoch.js';
|
|
54
|
+
// Frame/pass GPU timing (issue #2670 perf-verdict gate). Opt-in and NOT wired
|
|
55
|
+
// into `Renderer` by default — a caller constructs `GpuFrameTimingRecorder`
|
|
56
|
+
// itself and attaches its `timestampWrites` to the passes it wants measured;
|
|
57
|
+
// see `frame-timing-gpu.ts`'s module doc for the usage pattern, and
|
|
58
|
+
// `decideTimingMode` for choosing GPU queries vs. the CPU fallback vs. off.
|
|
59
|
+
export { decideTimingMode, passDurationsMs, frameTotalMs, aggregateFrameTimings } from './frame-timing.js';
|
|
60
|
+
export { computeDurationStats, nsToMs, isNegativeDelta } from './frame-timing-stats.js';
|
|
61
|
+
export { GpuFrameTimingRecorder, hasTimestampQueryFeature } from './frame-timing-gpu.js';
|
|
62
|
+
export { createCpuFrameTicker } from './frame-timing-cpu.js';
|
|
55
63
|
export { RaycastEngine } from './raycast-engine.js';
|
|
56
64
|
export { PointPicker, decodePickSample } from './point-picker.js';
|
|
57
65
|
// Point cloud rendering (Phase 0: IFCx inline; Phase 1+: streaming LAS/LAZ)
|
|
@@ -62,6 +70,7 @@ import { RenderPipeline } from './pipeline.js';
|
|
|
62
70
|
import { Camera } from './camera.js';
|
|
63
71
|
import { Scene } from './scene.js';
|
|
64
72
|
import { Picker } from './picker.js';
|
|
73
|
+
import { reportableItemId } from './pick-resolve.js';
|
|
65
74
|
import { MathUtils, viewBasis } from './math.js';
|
|
66
75
|
import { FrustumUtils } from '@ifc-lite/spatial';
|
|
67
76
|
import { VisualEnhancementResolver } from './visual-enhancement.js';
|
|
@@ -81,6 +90,9 @@ import { EdlPass } from './edl-pass.js';
|
|
|
81
90
|
import { SkyPass } from './sky-pass.js';
|
|
82
91
|
import { skyShaderSource } from './shaders/sky.wgsl.js';
|
|
83
92
|
import { resolveEnvironment } from './environment.js';
|
|
93
|
+
import { ShadowPass, resolveShadowMapResolution } from './shadow-pass.js';
|
|
94
|
+
import { fitSunLightMatrix, cameraFrustumFocusCorners } from './shadow-light-matrix.js';
|
|
95
|
+
import { collectShadowOccluders, classifyBatchVisibility, DEFAULT_MIN_CAST_ALPHA } from './shadow-occluders.js';
|
|
84
96
|
import { shouldRouteMeshTransparent, shouldRouteBatchTransparent, splitVisibleIdsByPromotion, DEFAULT_GHOST_ALPHA } from './overlay-routing.js';
|
|
85
97
|
import { colorSaltByte, packEntityLane } from './scene-geometry.js';
|
|
86
98
|
import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
|
|
@@ -181,6 +193,14 @@ export class Renderer {
|
|
|
181
193
|
// Procedural sky background — created lazily on the first frame that
|
|
182
194
|
// enables it (most sessions never do).
|
|
183
195
|
skyPass = null;
|
|
196
|
+
// Sun shadow-map depth pre-pass (#2670, Phase 2) — created lazily on the
|
|
197
|
+
// first frame that enables `RenderOptions.sunShadows`. Off by default.
|
|
198
|
+
shadowPass = null;
|
|
199
|
+
// Whether the shadow map was wired into the environment bind group last
|
|
200
|
+
// frame — lets a toggle-off write `enabled = 0` and drop the depth view
|
|
201
|
+
// exactly once instead of every frame.
|
|
202
|
+
shadowsWired = false;
|
|
203
|
+
shadowScratch = new Float32Array(24); // lightViewProj(16) + params(4) + params2(4)
|
|
184
204
|
edlOptions = {
|
|
185
205
|
enabled: false,
|
|
186
206
|
strength: 1,
|
|
@@ -1107,6 +1127,82 @@ export class Renderer {
|
|
|
1107
1127
|
setModelBounds(bounds) {
|
|
1108
1128
|
this.modelBoundsTracker.set(bounds);
|
|
1109
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* The batched occluders the sun shadow pass should cast, filtered to the same
|
|
1132
|
+
* visibility the colour pass draws (#2670, Phase 2b). Without filtering every
|
|
1133
|
+
* batch casts. With hide/isolate active a fully-hidden batch is dropped and a
|
|
1134
|
+
* partially-hidden OPAQUE batch casts only its visible subset via the SAME
|
|
1135
|
+
* cached partial sub-batch the colour pass renders (shared cache key
|
|
1136
|
+
* `${colorKey}:${id}` + `_partialBatchEpoch`), so no extra clone memory and no
|
|
1137
|
+
* phantom shadow from an individually-hidden element in a shared batch.
|
|
1138
|
+
*
|
|
1139
|
+
* Transparent (glass-like) partially-hidden parents are left to the collector's
|
|
1140
|
+
* material-alpha filter — they don't cast at all, so building a visible subset
|
|
1141
|
+
* for them would waste a clone (and diverge from the colour pass's promotion
|
|
1142
|
+
* split keys). Fully-visible transparent batches pass through unchanged and the
|
|
1143
|
+
* collector drops them.
|
|
1144
|
+
*/
|
|
1145
|
+
shadowOccluderBatches(options, device, hasVisibilityFiltering) {
|
|
1146
|
+
const all = this.scene.getBatchedMeshes();
|
|
1147
|
+
if (!hasVisibilityFiltering) {
|
|
1148
|
+
for (const batch of all)
|
|
1149
|
+
this.noteShadowOccluderResidency(batch);
|
|
1150
|
+
return all;
|
|
1151
|
+
}
|
|
1152
|
+
const pipeline = this.pipeline;
|
|
1153
|
+
const out = [];
|
|
1154
|
+
for (const batch of all) {
|
|
1155
|
+
const vis = classifyBatchVisibility(batch.expressIds, options.hiddenIds, options.isolatedIds);
|
|
1156
|
+
if (vis.kind === 'none')
|
|
1157
|
+
continue; // fully hidden → does not cast
|
|
1158
|
+
if (vis.kind === 'all') {
|
|
1159
|
+
this.noteShadowOccluderResidency(batch);
|
|
1160
|
+
out.push(batch); // fully visible → its own buffers
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
// Partially hidden. Transparent parents don't cast (collector's alpha
|
|
1164
|
+
// filter) — skip rather than build a wasted, divergent-key clone.
|
|
1165
|
+
if (batch.color[3] < DEFAULT_MIN_CAST_ALPHA)
|
|
1166
|
+
continue;
|
|
1167
|
+
// The partial sub-batch is built from the PARENT's CPU meshData, so a
|
|
1168
|
+
// cold parent yields nothing until it is restored. Queue that restore
|
|
1169
|
+
// here too: the colour pass only queues it for parents inside its own
|
|
1170
|
+
// frustum, but an up-sun occluder behind the camera still has to cast.
|
|
1171
|
+
this.noteShadowOccluderResidency(batch);
|
|
1172
|
+
// Opaque partial: reuse the colour pass's cached sub-batch. The key is
|
|
1173
|
+
// visibility-content-independent; `_partialBatchEpoch` invalidates it on
|
|
1174
|
+
// any hide/isolate or override change, so the clone is always current.
|
|
1175
|
+
// Without a pipeline the renderer isn't drawing, so skip (the shadow
|
|
1176
|
+
// pass won't run either); casting the whole parent would be wrong.
|
|
1177
|
+
if (!pipeline)
|
|
1178
|
+
continue;
|
|
1179
|
+
const sub = this.scene.getOrCreatePartialBatch(`${batch.colorKey}:${batch.id}`, batch.colorKey, vis.visibleIds, device, pipeline, this._partialBatchEpoch);
|
|
1180
|
+
// A cold parent yields an empty partial (its residency restore is queued
|
|
1181
|
+
// above); skip this frame — the collector drops zero-index draws anyway,
|
|
1182
|
+
// and the subset casts once resident.
|
|
1183
|
+
if (sub && sub.indexCount > 0)
|
|
1184
|
+
out.push(sub);
|
|
1185
|
+
}
|
|
1186
|
+
return out;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Keep a shadow occluder batch resident so it does not thin out silently on
|
|
1190
|
+
* large models under the GPU residency budget (#2670 review). The depth pass
|
|
1191
|
+
* reads these batches' buffers, but that read did not count as usage, so an
|
|
1192
|
+
* up-sun occluder outside the colour frustum aged into an eviction candidate.
|
|
1193
|
+
* Transparent batches never cast (the collector's alpha filter), so their
|
|
1194
|
+
* residency is irrelevant here.
|
|
1195
|
+
*/
|
|
1196
|
+
noteShadowOccluderResidency(batch) {
|
|
1197
|
+
if (batch.color[3] < DEFAULT_MIN_CAST_ALPHA)
|
|
1198
|
+
return;
|
|
1199
|
+
if (batch.gpuResident === false) {
|
|
1200
|
+
this.scene.requestBatchResidency(batch);
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
this.scene.recordBatchDrawn(batch);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1110
1206
|
/**
|
|
1111
1207
|
* Create a GPU Mesh from MeshData (lazy creation for selection highlighting)
|
|
1112
1208
|
* This is called on-demand when a mesh is selected, avoiding 2x buffer creation during streaming
|
|
@@ -1187,6 +1283,11 @@ export class Renderer {
|
|
|
1187
1283
|
this.scene.addMesh({
|
|
1188
1284
|
expressId: meshData.expressId,
|
|
1189
1285
|
modelIndex: meshData.modelIndex, // Preserve modelIndex for multi-model selection
|
|
1286
|
+
// Source item, so a pick can report it (#2985) — via the shared rule,
|
|
1287
|
+
// so this and the CPU raycaster cannot answer one click two ways.
|
|
1288
|
+
// In-tree callers pre-split merged pieces (Scene.getMeshDataPieces
|
|
1289
|
+
// drops the field incidentally); this is public, so it owns the rule.
|
|
1290
|
+
geometryItemId: reportableItemId(meshData, meshData.expressId),
|
|
1190
1291
|
vertexBuffer,
|
|
1191
1292
|
indexBuffer,
|
|
1192
1293
|
indexCount: meshData.indices.length,
|
|
@@ -1783,6 +1884,137 @@ export class Renderer {
|
|
|
1783
1884
|
}
|
|
1784
1885
|
// Now record draw commands
|
|
1785
1886
|
const encoder = device.createCommandEncoder();
|
|
1887
|
+
// Sun shadow-map pass (#2670, Phase 2). Off unless the caller opts
|
|
1888
|
+
// in; when off the hot path pays only this check and (once) an
|
|
1889
|
+
// enabled=0 reset on toggle-off. Runs BEFORE the colour pass (its
|
|
1890
|
+
// own complete depth-only sub-pass on the same encoder); the colour
|
|
1891
|
+
// pass then samples the map via the environment bind group. Every
|
|
1892
|
+
// geometry path both casts (collectShadowOccluders) and receives
|
|
1893
|
+
// (the shared main-family shader), so no part of the model silently
|
|
1894
|
+
// stops shadowing.
|
|
1895
|
+
const shadowOpts = options.sunShadows;
|
|
1896
|
+
let shadowsThisFrame = false;
|
|
1897
|
+
if (shadowOpts?.enabled) {
|
|
1898
|
+
const bounds = this.getModelBounds();
|
|
1899
|
+
if (bounds) {
|
|
1900
|
+
// `resolution === 0` (or unset) means Auto: pick from the
|
|
1901
|
+
// device's texture limit. A manual value is clamped to that
|
|
1902
|
+
// limit so a 4096 request can't fail createTexture on a
|
|
1903
|
+
// 2048-max device (#2670 review).
|
|
1904
|
+
const resolution = resolveShadowMapResolution(shadowOpts.resolution, device.limits.maxTextureDimension2D);
|
|
1905
|
+
if (!this.shadowPass) {
|
|
1906
|
+
this.shadowPass = new ShadowPass(device, resolution);
|
|
1907
|
+
}
|
|
1908
|
+
else {
|
|
1909
|
+
this.shadowPass.setResolution(resolution);
|
|
1910
|
+
}
|
|
1911
|
+
const boundsMin = [bounds.min.x, bounds.min.y, bounds.min.z];
|
|
1912
|
+
const boundsMax = [bounds.max.x, bounds.max.y, bounds.max.z];
|
|
1913
|
+
// Lateral shadow fit. AT REST: fit to the camera frustum
|
|
1914
|
+
// clipped to the model (maintainer #1) so a small building on
|
|
1915
|
+
// a large site keeps sharp shadows instead of spending the
|
|
1916
|
+
// map on distant terrain. DURING INTERACTION: fall back to a
|
|
1917
|
+
// whole-bounds fit — it is camera-INDEPENDENT, so orbiting or
|
|
1918
|
+
// scroll-zooming can't make the focus box breathe and drop
|
|
1919
|
+
// receivers off the map edge (which read as chunks of shadow
|
|
1920
|
+
// vanishing, #2670 follow-up). Depth always spans the whole
|
|
1921
|
+
// model so up-sun occluders keep casting.
|
|
1922
|
+
let focusCorners;
|
|
1923
|
+
if (!interacting) {
|
|
1924
|
+
const camEye = this.camera.getPosition();
|
|
1925
|
+
const camBasisFit = viewBasis(camEye, this.camera.getTarget(), this.camera.getUp());
|
|
1926
|
+
focusCorners = cameraFrustumFocusCorners({
|
|
1927
|
+
eye: camEye,
|
|
1928
|
+
forward: camBasisFit.forward,
|
|
1929
|
+
right: camBasisFit.right,
|
|
1930
|
+
up: camBasisFit.up,
|
|
1931
|
+
fovY: this.camera.getFOV(),
|
|
1932
|
+
aspect: this.canvas.height > 0 ? this.canvas.width / this.canvas.height : 1,
|
|
1933
|
+
ortho: this.camera.getProjectionMode() === 'orthographic',
|
|
1934
|
+
orthoHalfHeight: this.camera.getOrthoSize(),
|
|
1935
|
+
boundsMin,
|
|
1936
|
+
boundsMax,
|
|
1937
|
+
}) ?? undefined;
|
|
1938
|
+
}
|
|
1939
|
+
const sun = resolveEnvironment(options.environment).sunDirection;
|
|
1940
|
+
const fit = fitSunLightMatrix({ sunDirection: sun, boundsMin, boundsMax, focusCorners });
|
|
1941
|
+
const occluders = collectShadowOccluders({
|
|
1942
|
+
// Cast from the same visibility the colour pass draws: a
|
|
1943
|
+
// fully-hidden batch is dropped and a partially-hidden one
|
|
1944
|
+
// casts only its visible subset (the same cached partial
|
|
1945
|
+
// sub-batch the colour pass renders), so an
|
|
1946
|
+
// individually-hidden element in a shared batch stops
|
|
1947
|
+
// casting instead of throwing a phantom shadow.
|
|
1948
|
+
batches: this.shadowOccluderBatches(options, device, hasVisibilityFiltering),
|
|
1949
|
+
instanced: this.scene.getInstancedTemplates(),
|
|
1950
|
+
textured: this.scene.getTexturedMeshes(),
|
|
1951
|
+
// Individual meshes cast too (Renderer.addMesh() /
|
|
1952
|
+
// no-batch fallback); the collector skips hydrated
|
|
1953
|
+
// selection copies so batched scenes don't double-cast.
|
|
1954
|
+
meshes: this.scene.getMeshes(),
|
|
1955
|
+
}, { hiddenIds: options.hiddenIds, isolatedIds: options.isolatedIds ?? undefined });
|
|
1956
|
+
// Cast the same cut the colour pass draws: geometry a
|
|
1957
|
+
// section plane / crop box removed from view must stop
|
|
1958
|
+
// casting too, or the sliced-off roof keeps shadowing the
|
|
1959
|
+
// floor it no longer covers. `sectionPlaneData` already
|
|
1960
|
+
// folds in the terrain clip, so that is covered as well.
|
|
1961
|
+
this.shadowPass.render(encoder, fit.lightViewProj, occluders, {
|
|
1962
|
+
section: sectionPlaneData?.enabled
|
|
1963
|
+
? {
|
|
1964
|
+
normal: sectionPlaneData.normal,
|
|
1965
|
+
distance: sectionPlaneData.distance,
|
|
1966
|
+
flipped: options.sectionPlane?.flipped === true,
|
|
1967
|
+
}
|
|
1968
|
+
: null,
|
|
1969
|
+
box: options.clipBox?.enabled
|
|
1970
|
+
? { min: options.clipBox.min, max: options.clipBox.max }
|
|
1971
|
+
: null,
|
|
1972
|
+
});
|
|
1973
|
+
// Shadow uniform: light matrix + sampling params. The kernel
|
|
1974
|
+
// width follows the sun's angular size (physical, ~0.53°
|
|
1975
|
+
// like Blender's Sun lamp). Bias scales with the kernel: a
|
|
1976
|
+
// wider penumbra samples farther, so the normal offset must
|
|
1977
|
+
// grow with it or the kernel edge self-shadows (the hardware
|
|
1978
|
+
// slope bias in ShadowPass handles the grazing-angle case).
|
|
1979
|
+
const texelWorld = (2 * fit.orthoHalfWidth) / resolution;
|
|
1980
|
+
const sunAngleDeg = shadowOpts.sunAngleDeg ?? 0.53;
|
|
1981
|
+
const pcfRadius = Math.min(Math.max(sunAngleDeg * 3.0, 0.75), 8.0);
|
|
1982
|
+
const normalBias = texelWorld * (2.0 + pcfRadius);
|
|
1983
|
+
const s = this.shadowScratch;
|
|
1984
|
+
s.set(fit.lightViewProj.m, 0);
|
|
1985
|
+
s[16] = 1 / resolution; // texelSize
|
|
1986
|
+
s[17] = 1; // enabled
|
|
1987
|
+
s[18] = normalBias;
|
|
1988
|
+
s[19] = pcfRadius;
|
|
1989
|
+
s[20] = 0.0006; // depthBias (reverse-Z clip units)
|
|
1990
|
+
s[21] = 0;
|
|
1991
|
+
s[22] = 0;
|
|
1992
|
+
s[23] = 0;
|
|
1993
|
+
this.pipeline.updateShadowUniform(s);
|
|
1994
|
+
this.pipeline.setShadowDepthView(this.shadowPass.getDepthTextureView());
|
|
1995
|
+
this.shadowsWired = true;
|
|
1996
|
+
shadowsThisFrame = true;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (!shadowsThisFrame && (this.shadowsWired || this.shadowPass)) {
|
|
2000
|
+
// Toggle-off: disable sampling, release the depth view, and free
|
|
2001
|
+
// the shadow pass itself so its depth texture (16.8 MB at 2048,
|
|
2002
|
+
// 67 MB at 4096) is returned to the driver for the rest of the
|
|
2003
|
+
// session instead of lingering unused. Re-enabling reconstructs
|
|
2004
|
+
// it lazily on the next shadowed frame (see the `!this.shadowPass`
|
|
2005
|
+
// guard above). The `|| this.shadowPass` arm also covers a pass
|
|
2006
|
+
// that was allocated but never wired — an occluder-prep throw
|
|
2007
|
+
// between `new ShadowPass` and `shadowsWired = true` would
|
|
2008
|
+
// otherwise leak its texture until Renderer.destroy() (Greptile
|
|
2009
|
+
// #3053). The uniform-unwire below is a no-op when never wired.
|
|
2010
|
+
// #2670 review.
|
|
2011
|
+
this.shadowScratch[17] = 0;
|
|
2012
|
+
this.pipeline.updateShadowUniform(this.shadowScratch);
|
|
2013
|
+
this.pipeline.setShadowDepthView(null);
|
|
2014
|
+
this.shadowsWired = false;
|
|
2015
|
+
this.shadowPass?.destroy();
|
|
2016
|
+
this.shadowPass = null;
|
|
2017
|
+
}
|
|
1786
2018
|
// Set up MSAA rendering if enabled
|
|
1787
2019
|
const msaaView = this.pipeline.getMultisampleTextureView();
|
|
1788
2020
|
const useMSAA = msaaView !== null && this.pipeline.getSampleCount() > 1;
|
|
@@ -2815,18 +3047,6 @@ export class Renderer {
|
|
|
2815
3047
|
invalidateBVHCache() {
|
|
2816
3048
|
this.raycastEngine.invalidateBVHCache();
|
|
2817
3049
|
}
|
|
2818
|
-
/**
|
|
2819
|
-
* Get the raycaster instance (for advanced usage)
|
|
2820
|
-
*/
|
|
2821
|
-
getRaycaster() {
|
|
2822
|
-
return this.raycastEngine.getRaycaster();
|
|
2823
|
-
}
|
|
2824
|
-
/**
|
|
2825
|
-
* Get the snap detector instance (for advanced usage)
|
|
2826
|
-
*/
|
|
2827
|
-
getSnapDetector() {
|
|
2828
|
-
return this.raycastEngine.getSnapDetector();
|
|
2829
|
-
}
|
|
2830
3050
|
/**
|
|
2831
3051
|
* Clear all caches (call when geometry changes)
|
|
2832
3052
|
*/
|
|
@@ -2886,6 +3106,7 @@ export class Renderer {
|
|
|
2886
3106
|
getCamera() {
|
|
2887
3107
|
return this.camera;
|
|
2888
3108
|
}
|
|
3109
|
+
/** MEASURED external surface, not the 4400-line `Scene`: widening `SceneContents` is a published-API decision, not a detail — see `scene-contents.ts`. */
|
|
2889
3110
|
getScene() {
|
|
2890
3111
|
return this.scene;
|
|
2891
3112
|
}
|
|
@@ -2929,63 +3150,32 @@ export class Renderer {
|
|
|
2929
3150
|
this.overlays.setOverlayLineColor(color);
|
|
2930
3151
|
}
|
|
2931
3152
|
/**
|
|
2932
|
-
*
|
|
2933
|
-
* overlay. Each segment is `[x1, y1, z1, x2, y2, z2]` in world space.
|
|
2934
|
-
* The overlay is drawn regardless of whether a section plane is active.
|
|
2935
|
-
* Pass an empty Float32Array to clear.
|
|
2936
|
-
*/
|
|
2937
|
-
uploadAnnotationLines3D(vertices) {
|
|
2938
|
-
this.overlays.uploadAnnotationLines3D(vertices);
|
|
2939
|
-
}
|
|
2940
|
-
/**
|
|
2941
|
-
* Clear the standalone annotation line overlay.
|
|
2942
|
-
*/
|
|
2943
|
-
clearAnnotationLines3D() {
|
|
2944
|
-
this.overlays.clearAnnotationLines3D();
|
|
2945
|
-
}
|
|
2946
|
-
/**
|
|
2947
|
-
* Upload IfcAlignment centerline segments as a flat [x,y,z,x,y,z,...]
|
|
2948
|
-
* line-list in world space. Rendered as thin lines (not a ribbon mesh)
|
|
2949
|
-
* to match IfcGrid / IfcAnnotation. Pass an empty Float32Array to clear.
|
|
2950
|
-
*/
|
|
2951
|
-
uploadAlignmentLines3D(vertices) {
|
|
2952
|
-
this.overlays.uploadAlignmentLines3D(vertices);
|
|
2953
|
-
}
|
|
2954
|
-
/** Clear the alignment centerline overlay. */
|
|
2955
|
-
clearAlignmentLines3D() {
|
|
2956
|
-
this.overlays.clearAlignmentLines3D();
|
|
2957
|
-
}
|
|
2958
|
-
/**
|
|
2959
|
-
* Upload structural-grid (IfcGridAxis) segments as a flat [x,y,z,x,y,z,...]
|
|
2960
|
-
* line-list in world space (issue #967). Rendered as thin lines, mirroring
|
|
2961
|
-
* the alignment overlay. Pass an empty Float32Array to clear.
|
|
3153
|
+
* Set one standalone 3D line overlay, or clear it by passing `null`.
|
|
2962
3154
|
*
|
|
2963
|
-
*
|
|
2964
|
-
*
|
|
2965
|
-
*
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
*
|
|
2976
|
-
*
|
|
2977
|
-
* the
|
|
2978
|
-
* `
|
|
2979
|
-
*
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
3155
|
+
* `vertices` is a flat world-space line-list — `[x1,y1,z1, x2,y2,z2, …]`,
|
|
3156
|
+
* one segment per six floats. The vertices are already lifted to world
|
|
3157
|
+
* space, so these overlays draw whether or not a section plane is active.
|
|
3158
|
+
* A trailing partial segment is dropped rather than rejecting the array.
|
|
3159
|
+
* An empty array clears too, but `null` skips building the pipelines.
|
|
3160
|
+
*
|
|
3161
|
+
* Every channel is an independent buffer with its own visibility, so
|
|
3162
|
+
* setting one leaves the other three untouched. All four share the colour
|
|
3163
|
+
* set by {@link setOverlayLineColor}; label colour is per-text via
|
|
3164
|
+
* `SymbolicTextInput.color` on `uploadAnnotationTexts3D`.
|
|
3165
|
+
*
|
|
3166
|
+
* The channels differ in exactly one way — whether they grow the scene
|
|
3167
|
+
* bounds. `annotation` (#653) and `alignment` DO: a file holding only them
|
|
3168
|
+
* has no IfcProduct meshes to frame, so Home / fit-to-view would have
|
|
3169
|
+
* nothing to aim at and the near/far range would clip the lines away.
|
|
3170
|
+
* `grid` (IfcGridAxis, #967) and `dxf` (the DXF reference layer, #2043) do
|
|
3171
|
+
* NOT: they are reference layers that routinely extend past the model
|
|
3172
|
+
* envelope, so growing the bounds would reframe the camera whenever one
|
|
3173
|
+
* was ticked on. The rule is "does this content DEFINE the model's
|
|
3174
|
+
* extent", NOT "is it behind a visibility toggle" — annotations sit behind
|
|
3175
|
+
* `ifcAnnotationsVisible` too.
|
|
2982
3176
|
*/
|
|
2983
|
-
|
|
2984
|
-
this.overlays.
|
|
2985
|
-
}
|
|
2986
|
-
/** Clear the 3D DXF reference-layer overlay. */
|
|
2987
|
-
clearDxfLines3D() {
|
|
2988
|
-
this.overlays.clearDxfLines3D();
|
|
3177
|
+
setLineOverlay(channel, vertices) {
|
|
3178
|
+
this.overlays.setLineOverlay(channel, vertices);
|
|
2989
3179
|
}
|
|
2990
3180
|
/**
|
|
2991
3181
|
* Show (or clear) the clash-overlap box: the wireframe AABB of a focused
|
|
@@ -3039,7 +3229,10 @@ export class Renderer {
|
|
|
3039
3229
|
return this.overlays.hasSection2DOverlay();
|
|
3040
3230
|
}
|
|
3041
3231
|
/**
|
|
3042
|
-
* Get render pipeline (for batching)
|
|
3232
|
+
* Get render pipeline (for batching). DELIBERATELY NOT NARROWED, unlike
|
|
3233
|
+
* `getScene()`: the measurement found ZERO external
|
|
3234
|
+
* `RenderPipeline` members — all 12 call sites pass the handle straight
|
|
3235
|
+
* back into a `SceneContents` upload method typed for the real class.
|
|
3043
3236
|
*/
|
|
3044
3237
|
getPipeline() {
|
|
3045
3238
|
return this.pipeline;
|
|
@@ -3081,7 +3274,11 @@ export class Renderer {
|
|
|
3081
3274
|
*
|
|
3082
3275
|
* Returning null instead routes into the `if (!device) return` check that
|
|
3083
3276
|
* every call site already has, so a lost device degrades to "stop
|
|
3084
|
-
* uploading" rather than an uncaught throw.
|
|
3277
|
+
* uploading" rather than an uncaught throw.
|
|
3278
|
+
*
|
|
3279
|
+
* DELIBERATELY NOT NARROWED, unlike `getScene()`: the one external
|
|
3280
|
+
* `GPUDevice` member measured is `queue`; every other call site
|
|
3281
|
+
* hands the device back to a `SceneContents` method. See `isDeviceLost()` /
|
|
3085
3282
|
* `onDeviceLost()` for the recovery contract.
|
|
3086
3283
|
*/
|
|
3087
3284
|
getGPUDevice() {
|
|
@@ -3182,6 +3379,8 @@ export class Renderer {
|
|
|
3182
3379
|
this.edlPass = null;
|
|
3183
3380
|
this.skyPass?.destroy();
|
|
3184
3381
|
this.skyPass = null;
|
|
3382
|
+
this.shadowPass?.destroy();
|
|
3383
|
+
this.shadowPass = null;
|
|
3185
3384
|
// Section-plane gizmo, 2D section overlay and the symbolic annotation
|
|
3186
3385
|
// pipelines — see RendererOverlays.destroy().
|
|
3187
3386
|
this.overlays.destroy();
|