Skip to content

Commit 883e0f2

Browse files
committed
fix(soba/performances): adjust tracking logic in bvh effect
1 parent 595ed2d commit 883e0f2

File tree

2 files changed

+216
-16
lines changed

2 files changed

+216
-16
lines changed

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

+44-16
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import {
22
ChangeDetectionStrategy,
33
Component,
44
CUSTOM_ELEMENTS_SCHEMA,
5+
DestroyRef,
56
effect,
67
ElementRef,
8+
inject,
79
input,
10+
signal,
811
untracked,
912
viewChild,
1013
} from '@angular/core';
@@ -14,7 +17,7 @@ import * as THREE from 'three';
1417
import { Group } from 'three';
1518
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, SAH, SplitStrategy } from 'three-mesh-bvh';
1619

17-
export type NgtsBvhOptions = Partial<NgtThreeElements['ngt-group']> & {
20+
export type NgtsBVHOptions = Partial<NgtThreeElements['ngt-group']> & {
1821
/**Enabled, default: true */
1922
enabled: boolean;
2023
/** Use .raycastFirst to retrieve hits which is generally faster, default: false */
@@ -40,7 +43,7 @@ export type NgtsBvhOptions = Partial<NgtThreeElements['ngt-group']> & {
4043
indirect?: boolean;
4144
};
4245

43-
const defaultOptions: NgtsBvhOptions = {
46+
const defaultOptions: NgtsBVHOptions = {
4447
enabled: true,
4548
firstHitOnly: false,
4649
strategy: SAH,
@@ -61,7 +64,7 @@ const defaultOptions: NgtsBvhOptions = {
6164
schemas: [CUSTOM_ELEMENTS_SCHEMA],
6265
changeDetection: ChangeDetectionStrategy.OnPush,
6366
})
64-
export class NgtsBvh {
67+
export class NgtsBVH {
6568
options = input(defaultOptions, { transform: mergeInputs(defaultOptions) });
6669
protected parameters = omit(this.options, [
6770
'enabled',
@@ -87,38 +90,57 @@ export class NgtsBvh {
8790
private maxLeafTris = pick(this.options, 'maxLeafTris');
8891
private indirect = pick(this.options, 'indirect');
8992

93+
private reset = signal(Math.random());
94+
private retryMap = new Map();
95+
private MAX_RETRIES = 3;
96+
9097
constructor() {
9198
extend({ Group });
9299

93100
effect((onCleanup) => {
94101
const enabled = this.enabled();
95102
if (!enabled) return;
96103

97-
// This can only safely work if the component is used once, but there is no alternative.
104+
const group = this.groupRef().nativeElement;
105+
106+
// track reset
107+
this.reset();
108+
98109
// Hijacking the raycast method to do it for individual meshes is not an option as it would
110+
// This can only safely work if the component is used once, but there is no alternative.
99111
// 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+
const [firstHitOnly, strategy, verbose, setBoundingBox, maxDepth, maxLeafTris, indirect, raycaster] = [
113+
untracked(this.firstHitOnly),
114+
untracked(this.strategy),
115+
untracked(this.verbose),
116+
untracked(this.setBoundingBox),
117+
untracked(this.maxDepth),
118+
untracked(this.maxLeafTris),
119+
untracked(this.indirect),
120+
this.store.snapshot.raycaster,
121+
];
112122

113123
const options = { strategy, verbose, setBoundingBox, maxDepth, maxLeafTris, indirect };
114124
raycaster.firstHitOnly = firstHitOnly;
115125

126+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
116127
group.traverse((child) => {
117128
if (
118129
is.three<THREE.Mesh>(child, 'isMesh') &&
119130
!child.geometry.boundsTree &&
120131
child.raycast === THREE.Mesh.prototype.raycast
121132
) {
133+
const geometry = child.geometry;
134+
const retryCount = this.retryMap.get(child) ?? 0;
135+
// retry 3 times
136+
if (!Object.keys(geometry.attributes).length && retryCount <= this.MAX_RETRIES) {
137+
this.retryMap.set(child, retryCount + 1);
138+
timeoutId = setTimeout(() => {
139+
this.reset.set(Math.random());
140+
});
141+
return;
142+
}
143+
122144
child.raycast = acceleratedRaycast;
123145
child.geometry.computeBoundsTree = computeBoundsTree;
124146
child.geometry.disposeBoundsTree = disposeBoundsTree;
@@ -127,6 +149,8 @@ export class NgtsBvh {
127149
});
128150

129151
onCleanup(() => {
152+
timeoutId && clearTimeout(timeoutId);
153+
130154
delete raycaster.firstHitOnly;
131155
group.traverse((child) => {
132156
if (is.three<THREE.Mesh>(child, 'isMesh') && child.geometry.boundsTree) {
@@ -136,5 +160,9 @@ export class NgtsBvh {
136160
});
137161
});
138162
});
163+
164+
inject(DestroyRef).onDestroy(() => {
165+
this.retryMap.clear();
166+
});
139167
}
140168
}
+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
CUSTOM_ELEMENTS_SCHEMA,
5+
effect,
6+
ElementRef,
7+
input,
8+
signal,
9+
viewChild,
10+
} from '@angular/core';
11+
import { Meta } from '@storybook/angular';
12+
import { injectBeforeRender, NgtArgs, NgtThreeElements } from 'angular-three';
13+
import { injectHelper } from 'angular-three-soba/abstractions';
14+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
15+
import { NgtsBVH } from 'angular-three-soba/performances';
16+
import * as THREE from 'three';
17+
import { MeshBVHHelper } from 'three-mesh-bvh';
18+
import { storyDecorators, storyFunction } from '../setup-canvas';
19+
20+
@Component({
21+
selector: 'torus-bvh',
22+
template: `
23+
<ngts-bvh>
24+
<ngt-mesh
25+
#mesh
26+
[position.z]="positionZ()"
27+
(pointerover)="color.set('#ffff00')"
28+
(pointerout)="color.set('#ff0000')"
29+
>
30+
<ngt-torus-knot-geometry *args="[1, 0.4, 250, 50]" />
31+
<ngt-mesh-basic-material [color]="color()" />
32+
</ngt-mesh>
33+
</ngts-bvh>
34+
`,
35+
imports: [NgtsBVH, NgtArgs],
36+
changeDetection: ChangeDetectionStrategy.OnPush,
37+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
38+
})
39+
class TorusBVH {
40+
positionZ = input(0);
41+
42+
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
43+
44+
protected color = signal('#ff0000');
45+
46+
constructor() {
47+
injectHelper(this.meshRef, () => MeshBVHHelper);
48+
}
49+
}
50+
51+
const pointDist = 5;
52+
const raycaster = new THREE.Raycaster();
53+
const origVec = new THREE.Vector3();
54+
const dirVec = new THREE.Vector3();
55+
56+
@Component({
57+
selector: 'raycast-obj',
58+
template: `
59+
<ngt-group #obj>
60+
<ngt-mesh #original>
61+
<ngt-sphere-geometry *args="[0.1, 20, 20]" />
62+
<ngt-mesh-basic-material />
63+
</ngt-mesh>
64+
<ngt-mesh #hit>
65+
<ngt-sphere-geometry *args="[0.1, 20, 20]" />
66+
<ngt-mesh-basic-material />
67+
</ngt-mesh>
68+
<ngt-mesh #cylinder>
69+
<ngt-cylinder-geometry *args="[0.01, 0.01]" />
70+
<ngt-mesh-basic-material transparent [opacity]="0.25" />
71+
</ngt-mesh>
72+
</ngt-group>
73+
`,
74+
imports: [NgtArgs],
75+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
76+
changeDetection: ChangeDetectionStrategy.OnPush,
77+
})
78+
class RaycastObj {
79+
group = input.required<NgtThreeElements['ngt-group']>();
80+
81+
private objRef = viewChild.required<ElementRef<THREE.Group>>('obj');
82+
private originalRef = viewChild.required<ElementRef<THREE.Mesh>>('original');
83+
private hitRef = viewChild.required<ElementRef<THREE.Mesh>>('hit');
84+
private cylinderRef = viewChild.required<ElementRef<THREE.Mesh>>('cylinder');
85+
86+
constructor() {
87+
effect(() => {
88+
const [obj, originalMesh, hitMesh] = [
89+
this.objRef().nativeElement,
90+
this.originalRef().nativeElement,
91+
this.hitRef().nativeElement,
92+
this.cylinderRef().nativeElement,
93+
];
94+
hitMesh.scale.multiplyScalar(0.5);
95+
originalMesh.position.set(pointDist, 0, 0);
96+
obj.rotation.x = Math.random() * 10;
97+
obj.rotation.y = Math.random() * 10;
98+
});
99+
100+
const xDir = Math.random() - 0.5;
101+
const yDir = Math.random() - 0.5;
102+
103+
injectBeforeRender(({ delta }) => {
104+
const [obj, originalMesh, hitMesh, cylinderMesh, group] = [
105+
this.objRef().nativeElement,
106+
this.originalRef().nativeElement,
107+
this.hitRef().nativeElement,
108+
this.cylinderRef().nativeElement,
109+
this.group(),
110+
];
111+
112+
obj.rotation.x += xDir * delta;
113+
obj.rotation.y += yDir * delta;
114+
115+
originalMesh.updateMatrixWorld();
116+
origVec.setFromMatrixPosition(originalMesh.matrixWorld);
117+
dirVec.copy(origVec).multiplyScalar(-1).normalize();
118+
119+
raycaster.set(origVec, dirVec);
120+
raycaster.firstHitOnly = true;
121+
const res = raycaster.intersectObject(group as THREE.Group, true);
122+
const length = res.length ? res[0].distance : pointDist;
123+
124+
hitMesh.position.set(pointDist - length, 0, 0);
125+
cylinderMesh.position.set(pointDist - length / 2, 0, 0);
126+
cylinderMesh.scale.set(1, length, 1);
127+
cylinderMesh.rotation.z = Math.PI / 2;
128+
});
129+
}
130+
}
131+
132+
@Component({
133+
selector: 'debug-raycast',
134+
template: `
135+
@for (i of amount; track $index) {
136+
<raycast-obj [group]="group()" />
137+
}
138+
`,
139+
imports: [RaycastObj],
140+
changeDetection: ChangeDetectionStrategy.OnPush,
141+
})
142+
class DebugRaycast {
143+
group = input.required<NgtThreeElements['ngt-group']>();
144+
protected amount = Array.from({ length: 80 }, (_, index) => index);
145+
}
146+
147+
@Component({
148+
template: `
149+
<ngt-group #group>
150+
<torus-bvh [positionZ]="-2" />
151+
<torus-bvh [positionZ]="0" />
152+
<torus-bvh [positionZ]="2" />
153+
</ngt-group>
154+
155+
<debug-raycast [group]="group" />
156+
<ngts-orbit-controls [options]="{ enablePan: false, zoomSpeed: 0.5 }" />
157+
`,
158+
imports: [TorusBVH, DebugRaycast, NgtsOrbitControls],
159+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
160+
changeDetection: ChangeDetectionStrategy.OnPush,
161+
host: { class: 'default-bvh-story' },
162+
})
163+
class DefaultBVHStory {}
164+
165+
export default {
166+
title: 'Performances/BVH',
167+
decorators: storyDecorators(),
168+
} as Meta;
169+
170+
export const Default = storyFunction(DefaultBVHStory, {
171+
controls: false,
172+
});

0 commit comments

Comments
 (0)