Skip to content

Commit ea65873

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
feat(soba): add instances
1 parent 06fa3ce commit ea65873

File tree

4 files changed

+275
-0
lines changed

4 files changed

+275
-0
lines changed

libs/soba/performance/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export * from './adaptive/adaptive-dpr';
22
export * from './adaptive/adaptive-events';
33
export * from './detailed/detailed';
4+
export * from './instances/instance';
5+
export * from './instances/instances';
46
export * from './stats/stats';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { CUSTOM_ELEMENTS_SCHEMA, Component, effect, inject } from '@angular/core';
2+
import { extend, injectNgtRef, requestAnimationInInjectionContext } from 'angular-three';
3+
import { NGTS_INSTANCES_API } from './instances';
4+
import { PositionMesh } from './position-mesh';
5+
6+
extend({ PositionMesh });
7+
8+
@Component({
9+
selector: 'ngts-instance',
10+
standalone: true,
11+
template: `
12+
<ngt-position-mesh
13+
ngtCompound
14+
[ref]="positionMeshRef"
15+
[instanceKey]="positionMeshRef"
16+
[instance]="instancesApi().getParent()"
17+
>
18+
<ng-content />
19+
</ngt-position-mesh>
20+
`,
21+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
22+
})
23+
export class NgtsInstance {
24+
protected readonly positionMeshRef = injectNgtRef<PositionMesh>();
25+
protected readonly instancesApi = inject(NGTS_INSTANCES_API);
26+
27+
constructor() {
28+
requestAnimationInInjectionContext(() => {
29+
effect((onCleanup) => {
30+
onCleanup(this.instancesApi().subscribe(this.positionMeshRef));
31+
});
32+
});
33+
}
34+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import {
2+
CUSTOM_ELEMENTS_SCHEMA,
3+
Component,
4+
ElementRef,
5+
InjectionToken,
6+
Input,
7+
Signal,
8+
computed,
9+
effect,
10+
signal,
11+
} from '@angular/core';
12+
import {
13+
NgtArgs,
14+
NgtSignalStore,
15+
checkUpdate,
16+
extend,
17+
injectBeforeRender,
18+
injectNgtRef,
19+
requestAnimationInInjectionContext,
20+
} from 'angular-three';
21+
import * as THREE from 'three';
22+
import { InstancedBufferAttribute, InstancedMesh } from 'three';
23+
import { PositionMesh } from './position-mesh';
24+
25+
extend({ InstancedMesh, InstancedBufferAttribute });
26+
27+
export interface NgtsInstancesState {
28+
range: number;
29+
limit: number;
30+
frames: number;
31+
}
32+
33+
export interface NgtsInstancesApi {
34+
getParent: () => ElementRef<InstancedMesh>;
35+
subscribe: (ref: ElementRef<PositionMesh>) => () => void;
36+
}
37+
38+
export const NGTS_INSTANCES_API = new InjectionToken<Signal<NgtsInstancesApi>>('NgtsInstances API');
39+
40+
@Component({
41+
selector: 'ngts-instances',
42+
standalone: true,
43+
template: `
44+
<ngt-instanced-mesh
45+
*args="[undefined, undefined, 0]"
46+
ngtCompound
47+
[ref]="instancesRef"
48+
[matrixAutoUpdate]="false"
49+
[raycast]="nullRaycast"
50+
[userData]="{ instances }"
51+
>
52+
<ngt-instanced-buffer-attribute
53+
attach="instanceMatrix"
54+
[count]="matrices().length / 16"
55+
[array]="matrices()"
56+
[itemSize]="16"
57+
[usage]="DynamicDrawUsage"
58+
/>
59+
<ngt-instanced-buffer-attribute
60+
attach="instanceColor"
61+
[count]="colors().length / 3"
62+
[array]="colors()"
63+
[itemSize]="3"
64+
[usage]="DynamicDrawUsage"
65+
/>
66+
67+
<ng-content />
68+
</ngt-instanced-mesh>
69+
`,
70+
imports: [NgtArgs],
71+
providers: [
72+
{ provide: NGTS_INSTANCES_API, useFactory: (instances: NgtsInstances) => instances.api, deps: [NgtsInstances] },
73+
],
74+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
75+
})
76+
export class NgtsInstances extends NgtSignalStore<NgtsInstancesState> {
77+
readonly nullRaycast = () => null;
78+
readonly DynamicDrawUsage = THREE.DynamicDrawUsage;
79+
80+
@Input() instancesRef = injectNgtRef<THREE.InstancedMesh>();
81+
82+
@Input({ required: true }) set range(range: number) {
83+
this.set({ range });
84+
}
85+
86+
@Input() set limit(limit: number) {
87+
this.set({ limit });
88+
}
89+
90+
@Input() set frames(frames: number) {
91+
this.set({ frames });
92+
}
93+
94+
readonly #parentMatrix = new THREE.Matrix4();
95+
readonly #instanceMatrix = new THREE.Matrix4();
96+
readonly #tempMatrix = new THREE.Matrix4();
97+
readonly #translation = new THREE.Vector3();
98+
readonly #rotation = new THREE.Quaternion();
99+
readonly #scale = new THREE.Vector3();
100+
101+
readonly #limit = this.select('limit');
102+
103+
readonly matrices = computed(() => {
104+
const limit = this.#limit();
105+
const mArray = new Float32Array(limit * 16);
106+
for (let i = 0; i < limit; i++) this.#tempMatrix.identity().toArray(mArray, i * 16);
107+
return mArray;
108+
});
109+
readonly colors = computed(() => {
110+
const limit = this.#limit();
111+
return new Float32Array([...new Array(limit * 3)].map(() => 1));
112+
});
113+
114+
readonly instances = signal<Array<ElementRef<PositionMesh>>>([]);
115+
116+
readonly api = computed(() => ({
117+
getParent: () => this.instancesRef,
118+
subscribe: (ref: ElementRef<PositionMesh>) => {
119+
this.instances.update((prev) => [...prev, ref]);
120+
return () =>
121+
this.instances.update((prev) =>
122+
prev.filter((instance) => instance.nativeElement !== ref.nativeElement)
123+
);
124+
},
125+
}));
126+
127+
constructor() {
128+
super({ limit: 1000, frames: Infinity });
129+
requestAnimationInInjectionContext(() => {
130+
this.#checkUpdate();
131+
this.#setBeforeRender();
132+
});
133+
}
134+
135+
#checkUpdate() {
136+
effect(() => {
137+
const instances = this.instancesRef.nativeElement;
138+
if (!instances) return;
139+
checkUpdate(instances.instanceMatrix);
140+
});
141+
}
142+
143+
#setBeforeRender() {
144+
let count = 0;
145+
let updateRange = 0;
146+
injectBeforeRender(() => {
147+
const instances = this.instancesRef.nativeElement;
148+
if (!instances) return;
149+
150+
const { frames, range, limit } = this.get();
151+
const meshes = this.instances();
152+
const colors = this.colors();
153+
const matrices = this.matrices();
154+
if (frames === Infinity || count < frames) {
155+
instances.updateMatrix();
156+
instances.updateMatrixWorld();
157+
this.#parentMatrix.copy(instances.matrixWorld).invert();
158+
159+
updateRange = Math.min(limit, range !== undefined ? range : limit, meshes.length);
160+
instances.count = updateRange;
161+
instances.instanceMatrix.updateRange.count = updateRange * 16;
162+
if (instances.instanceColor) {
163+
instances.instanceColor.updateRange.count = updateRange * 3;
164+
}
165+
166+
for (let i = 0; i < meshes.length; i++) {
167+
const instance = meshes[i].nativeElement;
168+
// Multiply the inverse of the InstancedMesh world matrix or else
169+
// Instances will be double-transformed if <Instances> isn't at identity
170+
instance.matrixWorld.decompose(this.#translation, this.#rotation, this.#scale);
171+
this.#instanceMatrix
172+
.compose(this.#translation, this.#rotation, this.#scale)
173+
.premultiply(this.#parentMatrix);
174+
this.#instanceMatrix.toArray(matrices, i * 16);
175+
instances.instanceMatrix.needsUpdate = true;
176+
instance.color.toArray(colors, i * 3);
177+
if (instances.instanceColor) {
178+
checkUpdate(instances.instanceColor);
179+
}
180+
}
181+
count++;
182+
}
183+
});
184+
}
185+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ElementRef } from '@angular/core';
2+
import * as THREE from 'three';
3+
4+
const _instanceLocalMatrix = /*@__PURE__*/ new THREE.Matrix4();
5+
const _instanceWorldMatrix = /*@__PURE__*/ new THREE.Matrix4();
6+
const _instanceIntersects: THREE.Intersection[] = /*@__PURE__*/ [];
7+
const _mesh = /*@__PURE__*/ new THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial>();
8+
9+
export class PositionMesh extends THREE.Group {
10+
color: THREE.Color;
11+
instance: ElementRef<THREE.InstancedMesh | undefined>;
12+
instanceKey: ElementRef<PositionMesh | undefined>;
13+
14+
constructor() {
15+
super();
16+
this.color = new THREE.Color('white');
17+
this.instance = new ElementRef(undefined);
18+
this.instanceKey = new ElementRef(undefined);
19+
}
20+
21+
// This will allow the virtual instance have bounds
22+
get geometry() {
23+
return this.instance.nativeElement?.geometry;
24+
}
25+
26+
// And this will allow the virtual instance to receive events
27+
override raycast(raycaster: THREE.Raycaster, intersects: THREE.Intersection[]) {
28+
const parent = this.instance.nativeElement;
29+
if (!parent) return;
30+
if (!parent.geometry || !parent.material) return;
31+
_mesh.geometry = parent.geometry;
32+
const matrixWorld = parent.matrixWorld;
33+
const instanceId = parent.userData['instances']().indexOf(this.instanceKey);
34+
// If the instance wasn't found or exceeds the parents draw range, bail out
35+
if (instanceId === -1 || instanceId > parent.count) return;
36+
// calculate the world matrix for each instance
37+
parent.getMatrixAt(instanceId, _instanceLocalMatrix);
38+
_instanceWorldMatrix.multiplyMatrices(matrixWorld, _instanceLocalMatrix);
39+
// the mesh represents this single instance
40+
_mesh.matrixWorld = _instanceWorldMatrix;
41+
// raycast side according to instance material
42+
if (parent.material instanceof THREE.Material) _mesh.material.side = parent.material.side;
43+
else _mesh.material.side = parent.material[0].side;
44+
_mesh.raycast(raycaster, _instanceIntersects);
45+
// process the result of raycast
46+
for (let i = 0, l = _instanceIntersects.length; i < l; i++) {
47+
const intersect = _instanceIntersects[i];
48+
intersect.instanceId = instanceId;
49+
intersect.object = this;
50+
intersects.push(intersect);
51+
}
52+
_instanceIntersects.length = 0;
53+
}
54+
}

0 commit comments

Comments
 (0)