@pascal-app/viewer 1.0.6 → 1.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/README.md +54 -21
- package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
- package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/daylight-atmosphere.js +315 -0
- package/dist/components/viewer/drone-controller.d.ts +15 -0
- package/dist/components/viewer/drone-controller.d.ts.map +1 -0
- package/dist/components/viewer/drone-controller.js +194 -0
- package/dist/components/viewer/glb-interactive.d.ts +4 -18
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +32 -268
- package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
- package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene-motion.js +57 -0
- package/dist/components/viewer/glb-scene.d.ts +5 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +290 -49
- package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
- package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/glb-selection-callout.js +54 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
- package/dist/components/viewer/index.d.ts +5 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +38 -10
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +192 -5
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +150 -94
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- package/dist/components/viewer/scene-environment.js +4 -5
- package/dist/components/viewer/selection-callout.d.ts +51 -0
- package/dist/components/viewer/selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/selection-callout.js +197 -0
- package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
- package/dist/components/viewer/use-daylight-rooms.js +278 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +26 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -8
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/ground.d.ts +21 -0
- package/dist/lib/daylight/ground.d.ts.map +1 -0
- package/dist/lib/daylight/ground.js +53 -0
- package/dist/lib/daylight/location.d.ts +32 -0
- package/dist/lib/daylight/location.d.ts.map +1 -0
- package/dist/lib/daylight/location.js +74 -0
- package/dist/lib/daylight/look.d.ts +33 -0
- package/dist/lib/daylight/look.d.ts.map +1 -0
- package/dist/lib/daylight/look.js +13 -0
- package/dist/lib/daylight/material-hooks.d.ts +2 -0
- package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
- package/dist/lib/daylight/material-hooks.js +150 -0
- package/dist/lib/daylight/room-context.d.ts +12 -0
- package/dist/lib/daylight/room-context.d.ts.map +1 -0
- package/dist/lib/daylight/room-context.js +54 -0
- package/dist/lib/daylight/room-map.d.ts +124 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +630 -0
- package/dist/lib/daylight/room-scene.d.ts +56 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +391 -0
- package/dist/lib/daylight/rooms.d.ts +292 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1268 -0
- package/dist/lib/daylight/runtime.d.ts +33 -0
- package/dist/lib/daylight/runtime.d.ts.map +1 -0
- package/dist/lib/daylight/runtime.js +42 -0
- package/dist/lib/daylight/sky.d.ts +26 -0
- package/dist/lib/daylight/sky.d.ts.map +1 -0
- package/dist/lib/daylight/sky.js +217 -0
- package/dist/lib/daylight/solar.d.ts +46 -0
- package/dist/lib/daylight/solar.d.ts.map +1 -0
- package/dist/lib/daylight/solar.js +156 -0
- package/dist/lib/device-tier.d.ts +29 -0
- package/dist/lib/device-tier.d.ts.map +1 -0
- package/dist/lib/device-tier.js +71 -0
- package/dist/lib/dispose-object3d.d.ts.map +1 -1
- package/dist/lib/dispose-object3d.js +4 -2
- package/dist/lib/drone-flight.d.ts +44 -0
- package/dist/lib/drone-flight.d.ts.map +1 -0
- package/dist/lib/drone-flight.js +121 -0
- package/dist/lib/editor-highlight.d.ts +16 -0
- package/dist/lib/editor-highlight.d.ts.map +1 -0
- package/dist/lib/editor-highlight.js +33 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -1
- package/dist/lib/foundation-materials.js +2 -4
- package/dist/lib/glb-merge.d.ts +23 -0
- package/dist/lib/glb-merge.d.ts.map +1 -0
- package/dist/lib/glb-merge.js +184 -0
- package/dist/lib/hardware-geometry.d.ts +21 -0
- package/dist/lib/hardware-geometry.d.ts.map +1 -0
- package/dist/lib/hardware-geometry.js +283 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- package/dist/lib/layer-pass.d.ts +2 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layer-pass.js +20 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +50 -23
- package/dist/lib/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +5 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +8 -0
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -1
- package/dist/lib/occlusion-culler.d.ts +34 -0
- package/dist/lib/occlusion-culler.d.ts.map +1 -0
- package/dist/lib/occlusion-culler.js +698 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- package/dist/lib/post-processing-resources.d.ts +4 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/post-processing-resources.js +5 -0
- package/dist/lib/readback-rows.d.ts +14 -0
- package/dist/lib/readback-rows.d.ts.map +1 -0
- package/dist/lib/readback-rows.js +31 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -1
- package/dist/lib/reveal-pose.js +3 -0
- package/dist/lib/scene-matrices.d.ts +12 -0
- package/dist/lib/scene-matrices.d.ts.map +1 -0
- package/dist/lib/scene-matrices.js +287 -0
- package/dist/lib/scene-pass-mrt.d.ts +5 -2
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
- package/dist/lib/scene-pass-mrt.js +7 -2
- package/dist/lib/scene-themes.d.ts +0 -2
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +0 -2
- package/dist/lib/scene-visibility.d.ts +2 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +12 -3
- package/dist/lib/site-surfaces.d.ts +1 -8
- package/dist/lib/site-surfaces.d.ts.map +1 -1
- package/dist/lib/site-surfaces.js +0 -19
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +24 -40
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +13 -10
- package/dist/lib/underground-ambiance.d.ts +0 -2
- package/dist/lib/underground-ambiance.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.js +0 -2
- package/dist/lib/viewer-config.d.ts +21 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +21 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/store/use-item-light-pool.d.ts +3 -6
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +12 -0
- package/dist/store/use-viewer.d.ts +62 -59
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +106 -19
- package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +11 -1
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +501 -145
- package/dist/systems/door/leaf-profile.d.ts +11 -0
- package/dist/systems/door/leaf-profile.d.ts.map +1 -0
- package/dist/systems/door/leaf-profile.js +140 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/interactive/callout-actions.d.ts +72 -0
- package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
- package/dist/systems/interactive/callout-actions.js +212 -0
- package/dist/systems/interactive/interactive-system.d.ts +5 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +44 -121
- package/dist/systems/interactive/opening-settled.d.ts +16 -0
- package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
- package/dist/systems/interactive/opening-settled.js +53 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +22 -214
- package/dist/systems/item-light/pooled-lights.d.ts +70 -0
- package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
- package/dist/systems/item-light/pooled-lights.js +449 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -1
- package/dist/systems/roof/roof-material-slots.d.ts +14 -0
- package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
- package/dist/systems/roof/roof-material-slots.js +13 -0
- package/dist/systems/roof/roof-materials.d.ts +25 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +85 -26
- package/dist/systems/roof/roof-system.d.ts +34 -1
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +582 -168
- package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
- package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-casing-geometry.js +102 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
- package/dist/systems/wall/opening-cutout-geometry.js +7 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +28 -35
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +86 -25
- package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
- package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
- package/dist/systems/wall/wall-t-junctions.js +149 -0
- package/dist/systems/window/window-animation-system.d.ts +3 -12
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +37 -122
- package/dist/systems/window/window-pose.d.ts +31 -0
- package/dist/systems/window/window-pose.d.ts.map +1 -0
- package/dist/systems/window/window-pose.js +152 -0
- package/dist/systems/window/window-roller-shutter.d.ts +8 -0
- package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
- package/dist/systems/window/window-roller-shutter.js +64 -0
- package/dist/systems/window/window-system.d.ts +2 -11
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +470 -303
- package/package.json +6 -6
- package/dist/systems/interactive/control-widget.d.ts +0 -10
- package/dist/systems/interactive/control-widget.d.ts.map +0 -1
- package/dist/systems/interactive/control-widget.js +0 -35
- package/dist/systems/interactive/procedural-controls.d.ts +0 -13
- package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
- package/dist/systems/interactive/procedural-controls.js +0 -17
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { BATCHED_LAYER, SCENE_LAYER } from './layers';
|
|
3
|
+
import { timeSpan } from './perf-tracks';
|
|
4
|
+
import { isSetAsideForRender } from './scene-matrices';
|
|
5
|
+
import { heldReasons } from './scene-visibility';
|
|
6
|
+
/**
|
|
7
|
+
* CPU occlusion culling for baked artifacts. Each time the eye moves, the building's big opaque
|
|
8
|
+
* surfaces (walls, slabs, roofs, ceilings) are rasterized into a small depth image from the
|
|
9
|
+
* camera; every drawn mesh whose box lies wholly behind that image is left out of the color
|
|
10
|
+
* passes for that render. A level's floor then hides the furniture, walls and windows of the
|
|
11
|
+
* levels under it, and a facade hides the rooms behind it, for a few milliseconds of CPU
|
|
12
|
+
* instead of hundreds of draw encodes.
|
|
13
|
+
*
|
|
14
|
+
* The result is exact for the frame it is computed in (no readback, no one-frame lag, so
|
|
15
|
+
* nothing pops in late), and conservative: the occluder image is eroded by a pixel so a gap
|
|
16
|
+
* narrower than a pixel never hides what shows through it, a box is rasterized a little larger
|
|
17
|
+
* than it is, and a box that crosses the near plane is always drawn. Culled meshes keep casting
|
|
18
|
+
* shadows, so the sun's cached map never changes because of the eye.
|
|
19
|
+
*
|
|
20
|
+
* Only renders through the camera it was computed for are culled: the sun's shadow map and
|
|
21
|
+
* captures from other cameras see everything.
|
|
22
|
+
*/
|
|
23
|
+
const OCCLUDER_KINDS = new Set(['wall', 'slab', 'roof', 'roof-segment', 'ceiling', 'column']);
|
|
24
|
+
const WIDTH = 256;
|
|
25
|
+
/** An occluder must be this much nearer than a box (m) to hide it: slabs and walls are thin. */
|
|
26
|
+
const DEPTH_MARGIN = 0.02;
|
|
27
|
+
/** Mesh lists are re-read this often (s), for meshes added or rebuilt after the culler starts. */
|
|
28
|
+
const REFRESH_INTERVAL = 1;
|
|
29
|
+
/** With a still eye, the scene is checked for changes this often (s): doors open, levels move. */
|
|
30
|
+
const IDLE_INTERVAL = 0.1;
|
|
31
|
+
/** How far outside its edges (px) a box's face still counts as covering a pixel. */
|
|
32
|
+
const BOX_DILATION = 0.75;
|
|
33
|
+
/** Rounding allowance (px) at an occluder triangle's edges. */
|
|
34
|
+
const EDGE_SLACK = 1e-4;
|
|
35
|
+
/** Occluder triangles smaller than this (m²) are trim and moulding: not worth rasterizing. */
|
|
36
|
+
const MIN_TRIANGLE_AREA = 0.01;
|
|
37
|
+
/** Occluders narrower than this on screen (px of the depth image) hide next to nothing. */
|
|
38
|
+
const MIN_OCCLUDER_RADIUS = 1.5;
|
|
39
|
+
/** The 12 triangles of a box, as corner indices (bit 0: x, bit 1: y, bit 2: z), wound outward. */
|
|
40
|
+
const BOX_TRIANGLES = [
|
|
41
|
+
0, 2, 3, 0, 3, 1, 4, 5, 7, 4, 7, 6, 0, 1, 5, 0, 5, 4, 2, 6, 7, 2, 7, 3, 0, 4, 6, 0, 6, 2, 1, 3, 7,
|
|
42
|
+
1, 7, 5,
|
|
43
|
+
];
|
|
44
|
+
export function createOcclusionCuller(root) {
|
|
45
|
+
let occluders = [];
|
|
46
|
+
let occludees = [];
|
|
47
|
+
let listedAt = Number.NEGATIVE_INFINITY;
|
|
48
|
+
const shapes = new WeakMap();
|
|
49
|
+
const culled = new Set();
|
|
50
|
+
const stats = { occluders: 0, triangles: 0, tested: 0, culled: 0, ms: 0 };
|
|
51
|
+
let width = WIDTH;
|
|
52
|
+
let height = 0;
|
|
53
|
+
/** Nearest occluder depth key per pixel (−∞: nothing), then the eroded distances tested. */
|
|
54
|
+
let depth = new Float32Array(0);
|
|
55
|
+
let eroded = new Float32Array(0);
|
|
56
|
+
/** Farthest eroded distance per 2ⁿ×2ⁿ block, n = 1, 2, … */
|
|
57
|
+
let pyramid = [];
|
|
58
|
+
/** An occluder's vertices in view space, then on screen (x, y, depth key). */
|
|
59
|
+
let scratch = new Float32Array(0);
|
|
60
|
+
let screen = new Float64Array(0);
|
|
61
|
+
const view = new THREE.Matrix4();
|
|
62
|
+
const modelView = new THREE.Matrix4();
|
|
63
|
+
const viewProjection = new THREE.Matrix4();
|
|
64
|
+
const frustum = new THREE.Frustum();
|
|
65
|
+
const sphere = new THREE.Sphere();
|
|
66
|
+
let perspective = true;
|
|
67
|
+
let near = 0.1;
|
|
68
|
+
let p = [];
|
|
69
|
+
/** Screen x, y and depth key for up to 4 clipped vertices, and for a box's 8 corners. */
|
|
70
|
+
const polygon = new Float64Array(12);
|
|
71
|
+
const corners = new Float64Array(24);
|
|
72
|
+
/** Set by the box test's rasterizer when a box pixel is not hidden. */
|
|
73
|
+
let seen = false;
|
|
74
|
+
let lastSignature = Number.NaN;
|
|
75
|
+
/**
|
|
76
|
+
* Whether an object and its ancestors are visible, memoized for one pass. A subtree set aside
|
|
77
|
+
* for this render (`renderWithSceneMatrices`: nothing in it draws itself, e.g. merged
|
|
78
|
+
* originals) still counts as shown: its walls and slabs still occlude.
|
|
79
|
+
*/
|
|
80
|
+
const shownCache = new Map();
|
|
81
|
+
const shown = (object) => {
|
|
82
|
+
let value = shownCache.get(object);
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
value =
|
|
85
|
+
(object.visible || isSetAsideForRender(object)) &&
|
|
86
|
+
(object.parent ? shown(object.parent) : true);
|
|
87
|
+
shownCache.set(object, value);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Whether the camera draws `mesh` now. An occluder may be a merged original: its batch draws
|
|
93
|
+
* it, unless the level display holds it back (solo's shadow-only, isolation).
|
|
94
|
+
*/
|
|
95
|
+
const drawn = (mesh, occluder) => {
|
|
96
|
+
if (!shown(mesh))
|
|
97
|
+
return false;
|
|
98
|
+
if (mesh.layers.isEnabled(SCENE_LAYER))
|
|
99
|
+
return true;
|
|
100
|
+
if (!occluder || !mesh.layers.isEnabled(BATCHED_LAYER))
|
|
101
|
+
return false;
|
|
102
|
+
const reasons = heldReasons(mesh);
|
|
103
|
+
if (!reasons)
|
|
104
|
+
return true;
|
|
105
|
+
for (const reason of reasons)
|
|
106
|
+
if (reason !== 'batched' && reason !== 'wall-batched')
|
|
107
|
+
return false;
|
|
108
|
+
return true;
|
|
109
|
+
};
|
|
110
|
+
function list() {
|
|
111
|
+
occluders = [];
|
|
112
|
+
occludees = [];
|
|
113
|
+
root.traverse((object) => {
|
|
114
|
+
const mesh = object;
|
|
115
|
+
if (!mesh.isMesh || mesh.isSkinnedMesh)
|
|
116
|
+
return;
|
|
117
|
+
const occluder = !mesh.userData.glbBatch && OCCLUDER_KINDS.has(kindOf(mesh) ?? '') && opaque(mesh);
|
|
118
|
+
if (occluder) {
|
|
119
|
+
const geometry = mesh.geometry;
|
|
120
|
+
let shape = shapes.get(geometry);
|
|
121
|
+
if (shape === undefined) {
|
|
122
|
+
shape = occluderShape(geometry);
|
|
123
|
+
shapes.set(geometry, shape);
|
|
124
|
+
}
|
|
125
|
+
if (shape) {
|
|
126
|
+
if (!geometry.boundingSphere)
|
|
127
|
+
geometry.computeBoundingSphere();
|
|
128
|
+
occluders.push({
|
|
129
|
+
mesh,
|
|
130
|
+
...shape,
|
|
131
|
+
doubleSided: mesh.material.side !== THREE.FrontSide,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// A merged original is drawn by its batch, never itself.
|
|
136
|
+
if (!occluder && mesh.layers.isEnabled(BATCHED_LAYER) && !mesh.layers.isEnabled(SCENE_LAYER))
|
|
137
|
+
return;
|
|
138
|
+
if (!mesh.geometry.boundingBox)
|
|
139
|
+
mesh.geometry.computeBoundingBox();
|
|
140
|
+
if (!mesh.geometry.boundingSphere)
|
|
141
|
+
mesh.geometry.computeBoundingSphere();
|
|
142
|
+
occludees.push(mesh);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function resize(camera) {
|
|
146
|
+
const h = Math.max(32, Math.min(WIDTH, Math.round(WIDTH / aspectOf(camera))));
|
|
147
|
+
if (h === height)
|
|
148
|
+
return;
|
|
149
|
+
width = WIDTH;
|
|
150
|
+
height = h;
|
|
151
|
+
depth = new Float32Array(width * height);
|
|
152
|
+
eroded = new Float32Array(width * height);
|
|
153
|
+
pyramid = [];
|
|
154
|
+
let w = width;
|
|
155
|
+
let hh = height;
|
|
156
|
+
while (w > 1 || hh > 1) {
|
|
157
|
+
w = Math.max(1, Math.ceil(w / 2));
|
|
158
|
+
hh = Math.max(1, Math.ceil(hh / 2));
|
|
159
|
+
pyramid.push({ w, h: hh, data: new Float32Array(w * hh) });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function cull(camera) {
|
|
163
|
+
const start = performance.now();
|
|
164
|
+
if (start / 1000 - listedAt >= REFRESH_INTERVAL) {
|
|
165
|
+
list();
|
|
166
|
+
listedAt = start / 1000;
|
|
167
|
+
}
|
|
168
|
+
resize(camera);
|
|
169
|
+
culled.clear();
|
|
170
|
+
perspective = camera.isPerspectiveCamera === true;
|
|
171
|
+
near = Math.max(1e-3, camera.near ?? 0.1);
|
|
172
|
+
view.copy(camera.matrixWorldInverse);
|
|
173
|
+
viewProjection.multiplyMatrices(camera.projectionMatrix, view);
|
|
174
|
+
frustum.setFromProjectionMatrix(viewProjection, camera.coordinateSystem);
|
|
175
|
+
p = camera.projectionMatrix.elements;
|
|
176
|
+
// 1. Occluders → nearest depth key per pixel.
|
|
177
|
+
shownCache.clear();
|
|
178
|
+
depth.fill(Number.NEGATIVE_INFINITY);
|
|
179
|
+
let occluderCount = 0;
|
|
180
|
+
let triangles = 0;
|
|
181
|
+
// Pixels per metre at unit distance (perspective) or anywhere (orthographic).
|
|
182
|
+
const pixelsPerMetre = (p[5] * height) / 2;
|
|
183
|
+
for (const occluder of occluders) {
|
|
184
|
+
const mesh = occluder.mesh;
|
|
185
|
+
if (!drawn(mesh, true))
|
|
186
|
+
continue;
|
|
187
|
+
sphere.copy(mesh.geometry.boundingSphere).applyMatrix4(mesh.matrixWorld);
|
|
188
|
+
if (!frustum.intersectsSphere(sphere))
|
|
189
|
+
continue;
|
|
190
|
+
sphere.center.applyMatrix4(view);
|
|
191
|
+
const distance = Math.max(near, -sphere.center.z - sphere.radius);
|
|
192
|
+
const radius = (sphere.radius * pixelsPerMetre) / (perspective ? distance : 1);
|
|
193
|
+
if (radius < MIN_OCCLUDER_RADIUS)
|
|
194
|
+
continue;
|
|
195
|
+
occluderCount++;
|
|
196
|
+
modelView.multiplyMatrices(view, mesh.matrixWorld);
|
|
197
|
+
const m = modelView.elements;
|
|
198
|
+
const positions = occluder.positions;
|
|
199
|
+
const vertexCount = positions.length / 3;
|
|
200
|
+
if (scratch.length < vertexCount * 3) {
|
|
201
|
+
scratch = new Float32Array(vertexCount * 6);
|
|
202
|
+
screen = new Float64Array(vertexCount * 6);
|
|
203
|
+
}
|
|
204
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
205
|
+
const x = positions[i * 3];
|
|
206
|
+
const y = positions[i * 3 + 1];
|
|
207
|
+
const z = positions[i * 3 + 2];
|
|
208
|
+
const vx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
209
|
+
const vy = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
210
|
+
const vz = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
211
|
+
scratch[i * 3] = vx;
|
|
212
|
+
scratch[i * 3 + 1] = vy;
|
|
213
|
+
scratch[i * 3 + 2] = vz;
|
|
214
|
+
if (-vz >= near)
|
|
215
|
+
project(vx, vy, vz, screen, i * 3);
|
|
216
|
+
}
|
|
217
|
+
const list = occluder.triangles;
|
|
218
|
+
// A mirrored transform flips which side faces the camera.
|
|
219
|
+
const cullSide = occluder.doubleSided ? 0 : modelView.determinant() < 0 ? -1 : 1;
|
|
220
|
+
for (let t = 0; t < list.length; t += 3) {
|
|
221
|
+
const a = list[t] * 3;
|
|
222
|
+
const b = list[t + 1] * 3;
|
|
223
|
+
const c = list[t + 2] * 3;
|
|
224
|
+
triangles++;
|
|
225
|
+
if (-scratch[a + 2] >= near && -scratch[b + 2] >= near && -scratch[c + 2] >= near)
|
|
226
|
+
fill(screen, a, b, c, cullSide);
|
|
227
|
+
else
|
|
228
|
+
occluderTriangle(a, b, c, cullSide);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// 2. Keys → distances, eroded by a pixel (each pixel takes its farthest neighbour), then the
|
|
232
|
+
// max pyramid.
|
|
233
|
+
for (let i = 0; i < depth.length; i++) {
|
|
234
|
+
const key = depth[i];
|
|
235
|
+
depth[i] =
|
|
236
|
+
key === Number.NEGATIVE_INFINITY ? Number.POSITIVE_INFINITY : perspective ? 1 / key : -key;
|
|
237
|
+
}
|
|
238
|
+
for (let y = 0; y < height; y++) {
|
|
239
|
+
const up = Math.max(0, y - 1) * width;
|
|
240
|
+
const row = y * width;
|
|
241
|
+
const down = Math.min(height - 1, y + 1) * width;
|
|
242
|
+
for (let x = 0; x < width; x++) {
|
|
243
|
+
const l = Math.max(0, x - 1);
|
|
244
|
+
const r = Math.min(width - 1, x + 1);
|
|
245
|
+
eroded[row + x] = Math.max(depth[up + l], depth[up + x], depth[up + r], depth[row + l], depth[row + x], depth[row + r], depth[down + l], depth[down + x], depth[down + r]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let source = {
|
|
249
|
+
w: width,
|
|
250
|
+
h: height,
|
|
251
|
+
data: eroded,
|
|
252
|
+
};
|
|
253
|
+
for (const level of pyramid) {
|
|
254
|
+
for (let y = 0; y < level.h; y++) {
|
|
255
|
+
const y0 = y * 2 * source.w;
|
|
256
|
+
const y1 = Math.min(source.h - 1, y * 2 + 1) * source.w;
|
|
257
|
+
for (let x = 0; x < level.w; x++) {
|
|
258
|
+
const x0 = x * 2;
|
|
259
|
+
const x1 = Math.min(source.w - 1, x0 + 1);
|
|
260
|
+
level.data[y * level.w + x] = Math.max(source.data[y0 + x0], source.data[y0 + x1], source.data[y1 + x0], source.data[y1 + x1]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
source = level;
|
|
264
|
+
}
|
|
265
|
+
// 3. Each drawn mesh's box against the image.
|
|
266
|
+
let tested = 0;
|
|
267
|
+
if (occluderCount > 0) {
|
|
268
|
+
for (const mesh of occludees) {
|
|
269
|
+
if (!drawn(mesh, false))
|
|
270
|
+
continue;
|
|
271
|
+
sphere.copy(mesh.geometry.boundingSphere).applyMatrix4(mesh.matrixWorld);
|
|
272
|
+
if (!frustum.intersectsSphere(sphere))
|
|
273
|
+
continue;
|
|
274
|
+
tested++;
|
|
275
|
+
if (boxHidden(mesh))
|
|
276
|
+
culled.add(mesh);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
stats.occluders = occluderCount;
|
|
280
|
+
stats.triangles = triangles;
|
|
281
|
+
stats.tested = tested;
|
|
282
|
+
stats.culled = culled.size;
|
|
283
|
+
stats.ms = performance.now() - start;
|
|
284
|
+
return culled;
|
|
285
|
+
}
|
|
286
|
+
/** View-space point → screen x, y (pixels, y down) and a screen-linear depth key. */
|
|
287
|
+
function project(vx, vy, vz, out, o) {
|
|
288
|
+
const cx = p[0] * vx + p[4] * vy + p[8] * vz + p[12];
|
|
289
|
+
const cy = p[1] * vx + p[5] * vy + p[9] * vz + p[13];
|
|
290
|
+
const cw = p[3] * vx + p[7] * vy + p[11] * vz + p[15];
|
|
291
|
+
out[o] = ((cx / cw) * 0.5 + 0.5) * width;
|
|
292
|
+
out[o + 1] = (0.5 - (cy / cw) * 0.5) * height;
|
|
293
|
+
// Bigger is nearer: 1/distance (linear across the screen in perspective), or −distance.
|
|
294
|
+
out[o + 2] = perspective ? 1 / -vz : vz;
|
|
295
|
+
}
|
|
296
|
+
/** Clips one occluder triangle (view space, in `scratch`) at the near plane and rasterizes it. */
|
|
297
|
+
function occluderTriangle(ia, ib, ic, cullSide) {
|
|
298
|
+
const da = -scratch[ia + 2] - near;
|
|
299
|
+
const db = -scratch[ib + 2] - near;
|
|
300
|
+
const dc = -scratch[ic + 2] - near;
|
|
301
|
+
if (da < 0 && db < 0 && dc < 0)
|
|
302
|
+
return;
|
|
303
|
+
let n = 0;
|
|
304
|
+
for (let k = 0; k < 3; k++) {
|
|
305
|
+
const i0 = k === 0 ? ia : k === 1 ? ib : ic;
|
|
306
|
+
const i1 = k === 0 ? ib : k === 1 ? ic : ia;
|
|
307
|
+
const d0 = k === 0 ? da : k === 1 ? db : dc;
|
|
308
|
+
const d1 = k === 0 ? db : k === 1 ? dc : da;
|
|
309
|
+
if (d0 >= 0) {
|
|
310
|
+
project(scratch[i0], scratch[i0 + 1], scratch[i0 + 2], polygon, n * 3);
|
|
311
|
+
n++;
|
|
312
|
+
}
|
|
313
|
+
if (d0 >= 0 !== d1 >= 0) {
|
|
314
|
+
const t = d0 / (d0 - d1);
|
|
315
|
+
project(scratch[i0] + (scratch[i1] - scratch[i0]) * t, scratch[i0 + 1] + (scratch[i1 + 1] - scratch[i0 + 1]) * t, scratch[i0 + 2] + (scratch[i1 + 2] - scratch[i0 + 2]) * t, polygon, n * 3);
|
|
316
|
+
n++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (let k = 1; k + 1 < n; k++)
|
|
320
|
+
fill(polygon, 0, k * 3, (k + 1) * 3, cullSide);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Rasterizes an occluder's screen triangle: pixel centres inside it keep the nearer depth key.
|
|
324
|
+
* Row by row over the triangle's span, the key stepped across it.
|
|
325
|
+
* `cullSide`: 0 draws both sides, 1 skips back faces, -1 front faces (mirrored).
|
|
326
|
+
*/
|
|
327
|
+
function fill(v, a, b, c, cullSide) {
|
|
328
|
+
const ax = v[a];
|
|
329
|
+
const ay = v[a + 1];
|
|
330
|
+
const bx = v[b];
|
|
331
|
+
const by = v[b + 1];
|
|
332
|
+
const cx = v[c];
|
|
333
|
+
const cy = v[c + 1];
|
|
334
|
+
const area = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
335
|
+
if (area === 0 || !Number.isFinite(area))
|
|
336
|
+
return;
|
|
337
|
+
// Screen y points down, so a front (counter-clockwise) face has a negative area here.
|
|
338
|
+
if (cullSide !== 0 && area > 0 === cullSide > 0)
|
|
339
|
+
return;
|
|
340
|
+
const minX = Math.max(0, Math.round(Math.min(ax, bx, cx)));
|
|
341
|
+
const maxX = Math.min(width - 1, Math.round(Math.max(ax, bx, cx)) - 1);
|
|
342
|
+
const minY = Math.max(0, Math.round(Math.min(ay, by, cy)));
|
|
343
|
+
const maxY = Math.min(height - 1, Math.round(Math.max(ay, by, cy)) - 1);
|
|
344
|
+
if (minX > maxX || minY > maxY)
|
|
345
|
+
return;
|
|
346
|
+
const sign = area > 0 ? 1 : -1;
|
|
347
|
+
const e0x = (by - cy) * sign;
|
|
348
|
+
const e0y = (cx - bx) * sign;
|
|
349
|
+
const e1x = (cy - ay) * sign;
|
|
350
|
+
const e1y = (ax - cx) * sign;
|
|
351
|
+
const e2x = (ay - by) * sign;
|
|
352
|
+
const e2y = (bx - ax) * sign;
|
|
353
|
+
const inv = 1 / (area * sign);
|
|
354
|
+
const ka = v[a + 2];
|
|
355
|
+
const kb = v[b + 2];
|
|
356
|
+
const kc = v[c + 2];
|
|
357
|
+
const step = (e0x * ka + e1x * kb + e2x * kc) * inv;
|
|
358
|
+
const px = minX + 0.5;
|
|
359
|
+
for (let y = minY; y <= maxY; y++) {
|
|
360
|
+
const py = y + 0.5;
|
|
361
|
+
const w0 = e0x * (px - bx) + e0y * (py - by);
|
|
362
|
+
const w1 = e1x * (px - cx) + e1y * (py - cy);
|
|
363
|
+
const w2 = e2x * (px - ax) + e2y * (py - ay);
|
|
364
|
+
// The run of pixel centres where all three edge functions are non-negative, give or take
|
|
365
|
+
// rounding: a centre on the edge two triangles share must not be left out by both.
|
|
366
|
+
let lo = 0;
|
|
367
|
+
let hi = maxX - minX;
|
|
368
|
+
if (e0x > 0)
|
|
369
|
+
lo = Math.max(lo, Math.ceil(-w0 / e0x - EDGE_SLACK));
|
|
370
|
+
else if (e0x < 0)
|
|
371
|
+
hi = Math.min(hi, Math.floor(w0 / -e0x + EDGE_SLACK));
|
|
372
|
+
else if (w0 < 0)
|
|
373
|
+
continue;
|
|
374
|
+
if (e1x > 0)
|
|
375
|
+
lo = Math.max(lo, Math.ceil(-w1 / e1x - EDGE_SLACK));
|
|
376
|
+
else if (e1x < 0)
|
|
377
|
+
hi = Math.min(hi, Math.floor(w1 / -e1x + EDGE_SLACK));
|
|
378
|
+
else if (w1 < 0)
|
|
379
|
+
continue;
|
|
380
|
+
if (e2x > 0)
|
|
381
|
+
lo = Math.max(lo, Math.ceil(-w2 / e2x - EDGE_SLACK));
|
|
382
|
+
else if (e2x < 0)
|
|
383
|
+
hi = Math.min(hi, Math.floor(w2 / -e2x + EDGE_SLACK));
|
|
384
|
+
else if (w2 < 0)
|
|
385
|
+
continue;
|
|
386
|
+
if (lo > hi)
|
|
387
|
+
continue;
|
|
388
|
+
let key = (w0 * ka + w1 * kb + w2 * kc) * inv + step * lo;
|
|
389
|
+
const row = y * width + minX;
|
|
390
|
+
for (let x = lo; x <= hi; x++) {
|
|
391
|
+
if (key > depth[row + x])
|
|
392
|
+
depth[row + x] = key;
|
|
393
|
+
key += step;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Tests a box's screen triangle: pixels within {@link BOX_DILATION} of it are checked against
|
|
399
|
+
* the eroded image, and `seen` is set at the first one not hidden.
|
|
400
|
+
* `cullSide`: 1 skips back faces, -1 front faces (mirrored).
|
|
401
|
+
*/
|
|
402
|
+
function raster(v, a, b, c, cullSide, test) {
|
|
403
|
+
const ax = v[a];
|
|
404
|
+
const ay = v[a + 1];
|
|
405
|
+
const bx = v[b];
|
|
406
|
+
const by = v[b + 1];
|
|
407
|
+
const cx = v[c];
|
|
408
|
+
const cy = v[c + 1];
|
|
409
|
+
const area = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
410
|
+
if (area === 0 || !Number.isFinite(area))
|
|
411
|
+
return;
|
|
412
|
+
// Screen y points down, so a front (counter-clockwise) face has a negative area here.
|
|
413
|
+
if (cullSide !== 0 && area > 0 === cullSide > 0)
|
|
414
|
+
return;
|
|
415
|
+
const pad = test ? BOX_DILATION + 1 : 0;
|
|
416
|
+
const minX = Math.max(0, Math.floor(Math.min(ax, bx, cx) - pad));
|
|
417
|
+
const maxX = Math.min(width - 1, Math.ceil(Math.max(ax, bx, cx) + pad));
|
|
418
|
+
const minY = Math.max(0, Math.floor(Math.min(ay, by, cy) - pad));
|
|
419
|
+
const maxY = Math.min(height - 1, Math.ceil(Math.max(ay, by, cy) + pad));
|
|
420
|
+
if (minX > maxX || minY > maxY)
|
|
421
|
+
return;
|
|
422
|
+
const sign = area > 0 ? 1 : -1;
|
|
423
|
+
// Edge functions, positive inside and summing to |area|: each is its vertex's weight.
|
|
424
|
+
const e0x = (by - cy) * sign;
|
|
425
|
+
const e0y = (cx - bx) * sign;
|
|
426
|
+
const e1x = (cy - ay) * sign;
|
|
427
|
+
const e1y = (ax - cx) * sign;
|
|
428
|
+
const e2x = (ay - by) * sign;
|
|
429
|
+
const e2y = (bx - ax) * sign;
|
|
430
|
+
// How far outside each edge a tested pixel may lie, in edge-function units.
|
|
431
|
+
const s0 = test ? -BOX_DILATION * Math.hypot(e0x, e0y) : 0;
|
|
432
|
+
const s1 = test ? -BOX_DILATION * Math.hypot(e1x, e1y) : 0;
|
|
433
|
+
const s2 = test ? -BOX_DILATION * Math.hypot(e2x, e2y) : 0;
|
|
434
|
+
const inv = 1 / (area * sign);
|
|
435
|
+
const ka = v[a + 2];
|
|
436
|
+
const kb = v[b + 2];
|
|
437
|
+
const kc = v[c + 2];
|
|
438
|
+
// A dilated pixel lies off the face: give it the face's nearest depth, not an extrapolation.
|
|
439
|
+
const kNear = Math.max(ka, kb, kc);
|
|
440
|
+
for (let y = minY; y <= maxY; y++) {
|
|
441
|
+
const py = y + 0.5;
|
|
442
|
+
const row = y * width;
|
|
443
|
+
for (let x = minX; x <= maxX; x++) {
|
|
444
|
+
const px = x + 0.5;
|
|
445
|
+
const w0 = e0x * (px - bx) + e0y * (py - by);
|
|
446
|
+
const w1 = e1x * (px - cx) + e1y * (py - cy);
|
|
447
|
+
const w2 = e2x * (px - ax) + e2y * (py - ay);
|
|
448
|
+
if (w0 < s0 || w1 < s1 || w2 < s2)
|
|
449
|
+
continue;
|
|
450
|
+
const inside = w0 >= 0 && w1 >= 0 && w2 >= 0;
|
|
451
|
+
const key = inside ? (w0 * ka + w1 * kb + w2 * kc) * inv : kNear;
|
|
452
|
+
const d = perspective ? 1 / key : -key;
|
|
453
|
+
if (test && !(eroded[row + x] < d - DEPTH_MARGIN)) {
|
|
454
|
+
seen = true;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function boxHidden(mesh) {
|
|
461
|
+
const box = mesh.geometry.boundingBox;
|
|
462
|
+
const e = mesh.matrixWorld.elements;
|
|
463
|
+
const v = view.elements;
|
|
464
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
465
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
466
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
467
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
468
|
+
let nearest = Number.POSITIVE_INFINITY;
|
|
469
|
+
for (let k = 0; k < 8; k++) {
|
|
470
|
+
const x = k & 1 ? box.max.x : box.min.x;
|
|
471
|
+
const y = k & 2 ? box.max.y : box.min.y;
|
|
472
|
+
const z = k & 4 ? box.max.z : box.min.z;
|
|
473
|
+
const wx = e[0] * x + e[4] * y + e[8] * z + e[12];
|
|
474
|
+
const wy = e[1] * x + e[5] * y + e[9] * z + e[13];
|
|
475
|
+
const wz = e[2] * x + e[6] * y + e[10] * z + e[14];
|
|
476
|
+
const vx = v[0] * wx + v[4] * wy + v[8] * wz + v[12];
|
|
477
|
+
const vy = v[1] * wx + v[5] * wy + v[9] * wz + v[13];
|
|
478
|
+
const vz = v[2] * wx + v[6] * wy + v[10] * wz + v[14];
|
|
479
|
+
if (-vz < near)
|
|
480
|
+
return false;
|
|
481
|
+
project(vx, vy, vz, corners, k * 3);
|
|
482
|
+
const sx = corners[k * 3];
|
|
483
|
+
const sy = corners[k * 3 + 1];
|
|
484
|
+
if (sx < minX)
|
|
485
|
+
minX = sx;
|
|
486
|
+
if (sx > maxX)
|
|
487
|
+
maxX = sx;
|
|
488
|
+
if (sy < minY)
|
|
489
|
+
minY = sy;
|
|
490
|
+
if (sy > maxY)
|
|
491
|
+
maxY = sy;
|
|
492
|
+
if (-vz < nearest)
|
|
493
|
+
nearest = -vz;
|
|
494
|
+
}
|
|
495
|
+
// Wholly off screen: the frustum test already drops it.
|
|
496
|
+
if (maxX < 0 || maxY < 0 || minX >= width || minY >= height)
|
|
497
|
+
return false;
|
|
498
|
+
const x0 = Math.max(0, Math.floor(minX) - 1);
|
|
499
|
+
const y0 = Math.max(0, Math.floor(minY) - 1);
|
|
500
|
+
const x1 = Math.min(width - 1, Math.ceil(maxX) + 1);
|
|
501
|
+
const y1 = Math.min(height - 1, Math.ceil(maxY) + 1);
|
|
502
|
+
// Quick: the farthest occluder over the box's screen rectangle is nearer than its nearest
|
|
503
|
+
// corner. Read from the pyramid level where the rectangle spans a few texels.
|
|
504
|
+
const span = Math.max(x1 - x0 + 1, y1 - y0 + 1);
|
|
505
|
+
const level = Math.min(pyramid.length, Math.max(0, Math.ceil(Math.log2(span / 4))));
|
|
506
|
+
const data = level === 0 ? eroded : pyramid[level - 1].data;
|
|
507
|
+
const lw = level === 0 ? width : pyramid[level - 1].w;
|
|
508
|
+
const limit = nearest - DEPTH_MARGIN;
|
|
509
|
+
let quick = true;
|
|
510
|
+
for (let y = y0 >> level; quick && y <= y1 >> level; y++) {
|
|
511
|
+
for (let x = x0 >> level; x <= x1 >> level; x++) {
|
|
512
|
+
if (!(data[y * lw + x] < limit)) {
|
|
513
|
+
quick = false;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (quick)
|
|
519
|
+
return true;
|
|
520
|
+
// Exact: the box's faces, pixel by pixel, each against the occluder in front of it.
|
|
521
|
+
// Faces turned away lie behind ones that face the eye, so only those are drawn.
|
|
522
|
+
seen = false;
|
|
523
|
+
const mirrored = mesh.matrixWorld.determinant() < 0;
|
|
524
|
+
for (let t = 0; t < BOX_TRIANGLES.length && !seen; t += 3) {
|
|
525
|
+
raster(corners, BOX_TRIANGLES[t] * 3, BOX_TRIANGLES[t + 1] * 3, BOX_TRIANGLES[t + 2] * 3, mirrored ? -1 : 1, true);
|
|
526
|
+
}
|
|
527
|
+
return !seen;
|
|
528
|
+
}
|
|
529
|
+
/** A number that changes when any listed mesh moves or is shown or hidden. */
|
|
530
|
+
function signature() {
|
|
531
|
+
shownCache.clear();
|
|
532
|
+
let h = 0;
|
|
533
|
+
for (let i = 0; i < occludees.length; i++) {
|
|
534
|
+
const mesh = occludees[i];
|
|
535
|
+
if (!drawn(mesh, true))
|
|
536
|
+
continue;
|
|
537
|
+
const e = mesh.matrixWorld.elements;
|
|
538
|
+
h +=
|
|
539
|
+
((i % 89) + 1) *
|
|
540
|
+
(e[0] + e[2] * 3 + e[5] * 5 + e[10] * 7 + e[12] * 11 + e[13] * 13 + e[14] * 17 + 0.5);
|
|
541
|
+
}
|
|
542
|
+
return h;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
cull,
|
|
546
|
+
changed() {
|
|
547
|
+
const next = signature();
|
|
548
|
+
const changed = next !== lastSignature;
|
|
549
|
+
lastSignature = next;
|
|
550
|
+
return changed;
|
|
551
|
+
},
|
|
552
|
+
stats,
|
|
553
|
+
dispose() {
|
|
554
|
+
occluders = [];
|
|
555
|
+
occludees = [];
|
|
556
|
+
culled.clear();
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Culls `culler`'s meshes from the renders of `scene` through the camera `getCamera` returns:
|
|
562
|
+
* recomputed when that camera moves or the scene changes, and answered through the drawing
|
|
563
|
+
* pipeline's own culling test while such a render collects its draws. Nothing about the meshes
|
|
564
|
+
* changes, so other cameras (the sun's shadow, captures) see them all.
|
|
565
|
+
*/
|
|
566
|
+
export function attachOcclusionCuller({ scene, pipeline, culler, getCamera, }) {
|
|
567
|
+
const previousBefore = scene.onBeforeRender;
|
|
568
|
+
const previousAfter = scene.onAfterRender;
|
|
569
|
+
const ownCulled = Object.hasOwn(pipeline, 'isCulled');
|
|
570
|
+
const previousCulled = pipeline.isCulled;
|
|
571
|
+
const pose = new Float32Array(POSE_SIZE);
|
|
572
|
+
const computedPose = new Float32Array(POSE_SIZE);
|
|
573
|
+
let computedFor = null;
|
|
574
|
+
let checkedAt = Number.NEGATIVE_INFINITY;
|
|
575
|
+
let moving = true;
|
|
576
|
+
let culled = new Set();
|
|
577
|
+
let active = null;
|
|
578
|
+
scene.onBeforeRender = function (...args) {
|
|
579
|
+
previousBefore.apply(this, args);
|
|
580
|
+
const camera = args[2];
|
|
581
|
+
if (active || camera !== getCamera())
|
|
582
|
+
return;
|
|
583
|
+
const now = performance.now() / 1000;
|
|
584
|
+
// The pose and lens, not the projection matrix: temporal AA jitters that every frame.
|
|
585
|
+
readPose(camera, pose);
|
|
586
|
+
let stale = computedFor !== camera || !samePose(pose, computedPose);
|
|
587
|
+
if (stale)
|
|
588
|
+
moving = true;
|
|
589
|
+
else if (now - checkedAt >= IDLE_INTERVAL) {
|
|
590
|
+
// Once the eye settles, the scene is watched instead: the first check takes its measure
|
|
591
|
+
// (and culls once more, for what changed while the eye moved), later ones compare.
|
|
592
|
+
checkedAt = now;
|
|
593
|
+
stale = culler.changed() || moving;
|
|
594
|
+
moving = false;
|
|
595
|
+
}
|
|
596
|
+
if (stale) {
|
|
597
|
+
culled = timeSpan('occlusion-cull', () => culler.cull(camera));
|
|
598
|
+
computedFor = camera;
|
|
599
|
+
checkedAt = now;
|
|
600
|
+
computedPose.set(pose);
|
|
601
|
+
}
|
|
602
|
+
active = camera;
|
|
603
|
+
};
|
|
604
|
+
scene.onAfterRender = function (...args) {
|
|
605
|
+
if (active === args[2])
|
|
606
|
+
active = null;
|
|
607
|
+
previousAfter.apply(this, args);
|
|
608
|
+
};
|
|
609
|
+
pipeline.isCulled = function (object, camera) {
|
|
610
|
+
if (camera === active && culled.has(object))
|
|
611
|
+
return true;
|
|
612
|
+
return previousCulled.call(this, object, camera);
|
|
613
|
+
};
|
|
614
|
+
return () => {
|
|
615
|
+
scene.onBeforeRender = previousBefore;
|
|
616
|
+
scene.onAfterRender = previousAfter;
|
|
617
|
+
if (ownCulled)
|
|
618
|
+
pipeline.isCulled = previousCulled;
|
|
619
|
+
else
|
|
620
|
+
delete pipeline.isCulled;
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const POSE_SIZE = 23;
|
|
624
|
+
/** The camera's world matrix and lens settings. */
|
|
625
|
+
function readPose(camera, out) {
|
|
626
|
+
out.set(camera.matrixWorld.elements);
|
|
627
|
+
const c = camera;
|
|
628
|
+
out[16] = c.isPerspectiveCamera ? c.fov : c.left;
|
|
629
|
+
out[17] = c.isPerspectiveCamera ? c.aspect : c.right;
|
|
630
|
+
out[18] = c.isPerspectiveCamera ? 0 : c.top;
|
|
631
|
+
out[19] = c.isPerspectiveCamera ? 0 : c.bottom;
|
|
632
|
+
out[20] = c.near;
|
|
633
|
+
out[21] = c.far;
|
|
634
|
+
out[22] = c.zoom;
|
|
635
|
+
}
|
|
636
|
+
function samePose(a, b) {
|
|
637
|
+
for (let i = 0; i < a.length; i++)
|
|
638
|
+
if (a[i] !== b[i])
|
|
639
|
+
return false;
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
/** An occluder's vertices and the triangles of it big enough to rasterize. */
|
|
643
|
+
function occluderShape(geometry) {
|
|
644
|
+
const attribute = geometry.getAttribute('position');
|
|
645
|
+
if (!attribute)
|
|
646
|
+
return null;
|
|
647
|
+
const positions = new Float32Array(attribute.count * 3);
|
|
648
|
+
for (let i = 0; i < attribute.count; i++) {
|
|
649
|
+
positions[i * 3] = attribute.getX(i);
|
|
650
|
+
positions[i * 3 + 1] = attribute.getY(i);
|
|
651
|
+
positions[i * 3 + 2] = attribute.getZ(i);
|
|
652
|
+
}
|
|
653
|
+
const index = geometry.index;
|
|
654
|
+
const count = index ? index.count : attribute.count;
|
|
655
|
+
const kept = [];
|
|
656
|
+
const a = new THREE.Vector3();
|
|
657
|
+
const b = new THREE.Vector3();
|
|
658
|
+
const c = new THREE.Vector3();
|
|
659
|
+
for (let t = 0; t + 2 < count; t += 3) {
|
|
660
|
+
const ia = index ? index.getX(t) : t;
|
|
661
|
+
const ib = index ? index.getX(t + 1) : t + 1;
|
|
662
|
+
const ic = index ? index.getX(t + 2) : t + 2;
|
|
663
|
+
a.fromArray(positions, ia * 3);
|
|
664
|
+
b.fromArray(positions, ib * 3).sub(a);
|
|
665
|
+
c.fromArray(positions, ic * 3).sub(a);
|
|
666
|
+
if (b.cross(c).length() / 2 >= MIN_TRIANGLE_AREA)
|
|
667
|
+
kept.push(ia, ib, ic);
|
|
668
|
+
}
|
|
669
|
+
return kept.length ? { positions, triangles: new Uint32Array(kept) } : null;
|
|
670
|
+
}
|
|
671
|
+
function kindOf(mesh) {
|
|
672
|
+
for (let x = mesh; x; x = x.parent) {
|
|
673
|
+
const extras = x.userData;
|
|
674
|
+
if (extras.pascalId && extras.kind !== 'level')
|
|
675
|
+
return extras.kind;
|
|
676
|
+
}
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
function opaque(mesh) {
|
|
680
|
+
const materials = [mesh.material, mesh.userData.__bakedMaterial];
|
|
681
|
+
for (const material of materials) {
|
|
682
|
+
if (!material || Array.isArray(material))
|
|
683
|
+
continue;
|
|
684
|
+
const m = material;
|
|
685
|
+
if (m.transparent || m.opacity < 1 || m.transmission > 0 || m.alphaTest > 0)
|
|
686
|
+
return false;
|
|
687
|
+
if (m.userData.isGlass)
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
return mesh.userData.slotId !== 'glass';
|
|
691
|
+
}
|
|
692
|
+
function aspectOf(camera) {
|
|
693
|
+
const perspective = camera;
|
|
694
|
+
if (perspective.isPerspectiveCamera)
|
|
695
|
+
return perspective.aspect;
|
|
696
|
+
const ortho = camera;
|
|
697
|
+
return Math.abs((ortho.right - ortho.left) / (ortho.top - ortho.bottom)) || 1;
|
|
698
|
+
}
|