Skip to content

Commit 89c1e5a

Browse files
committed
feat(soba/performances): add bvh
1 parent 2ad2b30 commit 89c1e5a

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

libs/soba/performances/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './lib/adaptive-dpr';
22
export * from './lib/adaptive-events';
3+
export * from './lib/bvh';
34
export * from './lib/detailed';
45
export * from './lib/instances/instances';
56
export * from './lib/points/points';

libs/soba/performances/src/lib/bvh.ts

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
CUSTOM_ELEMENTS_SCHEMA,
5+
effect,
6+
ElementRef,
7+
input,
8+
untracked,
9+
viewChild,
10+
} from '@angular/core';
11+
import { extend, injectStore, is, NgtThreeElements, omit, pick } from 'angular-three';
12+
import { mergeInputs } from 'ngxtension/inject-inputs';
13+
import * as THREE from 'three';
14+
import { Group } from 'three';
15+
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, SAH, SplitStrategy } from 'three-mesh-bvh';
16+
17+
export type NgtsBvhOptions = Partial<NgtThreeElements['ngt-group']> & {
18+
/**Enabled, default: true */
19+
enabled: boolean;
20+
/** Use .raycastFirst to retrieve hits which is generally faster, default: false */
21+
firstHitOnly: boolean;
22+
/** Split strategy, default: SAH (slowest to construct, fastest runtime, least memory) */
23+
strategy: SplitStrategy;
24+
/** Print out warnings encountered during tree construction, default: false */
25+
verbose: boolean;
26+
/** If true then the bounding box for the geometry is set once the BVH has been constructed, default: true */
27+
setBoundingBox: boolean;
28+
/** The maximum depth to allow the tree to build to, default: 40 */
29+
maxDepth: number;
30+
/** The number of triangles to aim for in a leaf node, default: 10 */
31+
maxLeafTris: number;
32+
33+
/** If false then an index buffer is created if it does not exist and is rearranged */
34+
/** to hold the bvh structure. If false then a separate buffer is created to store the */
35+
/** structure and the index buffer (or lack thereof) is retained. This can be used */
36+
/** when the existing index layout is important or groups are being used so a */
37+
/** single BVH hierarchy can be created to improve performance. */
38+
/** default: false */
39+
/** Note: This setting is experimental */
40+
indirect?: boolean;
41+
};
42+
43+
const defaultOptions: NgtsBvhOptions = {
44+
enabled: true,
45+
firstHitOnly: false,
46+
strategy: SAH,
47+
verbose: false,
48+
setBoundingBox: true,
49+
maxDepth: 40,
50+
maxLeafTris: 10,
51+
indirect: false,
52+
};
53+
54+
@Component({
55+
selector: 'ngts-bvh',
56+
template: `
57+
<ngt-group #group [parameters]="parameters()">
58+
<ng-content />
59+
</ngt-group>
60+
`,
61+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
62+
changeDetection: ChangeDetectionStrategy.OnPush,
63+
})
64+
export class NgtsBvh {
65+
options = input(defaultOptions, { transform: mergeInputs(defaultOptions) });
66+
protected parameters = omit(this.options, [
67+
'enabled',
68+
'firstHitOnly',
69+
'strategy',
70+
'verbose',
71+
'setBoundingBox',
72+
'maxDepth',
73+
'maxLeafTris',
74+
'indirect',
75+
]);
76+
77+
groupRef = viewChild.required<ElementRef<THREE.Group>>('group');
78+
79+
private store = injectStore();
80+
81+
private enabled = pick(this.options, 'enabled');
82+
private firstHitOnly = pick(this.options, 'firstHitOnly');
83+
private strategy = pick(this.options, 'strategy');
84+
private verbose = pick(this.options, 'verbose');
85+
private setBoundingBox = pick(this.options, 'setBoundingBox');
86+
private maxDepth = pick(this.options, 'maxDepth');
87+
private maxLeafTris = pick(this.options, 'maxLeafTris');
88+
private indirect = pick(this.options, 'indirect');
89+
90+
constructor() {
91+
extend({ Group });
92+
93+
effect((onCleanup) => {
94+
const enabled = this.enabled();
95+
if (!enabled) return;
96+
97+
// This can only safely work if the component is used once, but there is no alternative.
98+
// Hijacking the raycast method to do it for individual meshes is not an option as it would
99+
// cost too much memory ...
100+
const [firstHitOnly, strategy, verbose, setBoundingBox, maxDepth, maxLeafTris, indirect, group, raycaster] =
101+
[
102+
untracked(this.firstHitOnly),
103+
untracked(this.strategy),
104+
untracked(this.verbose),
105+
untracked(this.setBoundingBox),
106+
untracked(this.maxDepth),
107+
untracked(this.maxLeafTris),
108+
untracked(this.indirect),
109+
this.groupRef().nativeElement,
110+
this.store.snapshot.raycaster,
111+
];
112+
113+
const options = { strategy, verbose, setBoundingBox, maxDepth, maxLeafTris, indirect };
114+
raycaster.firstHitOnly = firstHitOnly;
115+
116+
group.traverse((child) => {
117+
if (
118+
is.three<THREE.Mesh>(child, 'isMesh') &&
119+
!child.geometry.boundsTree &&
120+
child.raycast === THREE.Mesh.prototype.raycast
121+
) {
122+
child.raycast = acceleratedRaycast;
123+
child.geometry.computeBoundsTree = computeBoundsTree;
124+
child.geometry.disposeBoundsTree = disposeBoundsTree;
125+
child.geometry.computeBoundsTree(options);
126+
}
127+
});
128+
129+
onCleanup(() => {
130+
delete raycaster.firstHitOnly;
131+
group.traverse((child) => {
132+
if (is.three<THREE.Mesh>(child, 'isMesh') && child.geometry.boundsTree) {
133+
child.geometry.disposeBoundsTree();
134+
child.raycast = THREE.Mesh.prototype.raycast;
135+
}
136+
});
137+
});
138+
});
139+
}
140+
}

0 commit comments

Comments
 (0)