Skip to content

Commit e118791

Browse files
committed
feat(soba): add instances
1 parent 0a8a372 commit e118791

File tree

5 files changed

+295
-8
lines changed

5 files changed

+295
-8
lines changed

Diff for: libs/soba/performances/src/index.ts

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

Diff for: libs/soba/performances/src/instances/instances.ts

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { NgTemplateOutlet } from '@angular/common';
2+
import {
3+
Component,
4+
computed,
5+
ContentChild,
6+
CUSTOM_ELEMENTS_SCHEMA,
7+
effect,
8+
forwardRef,
9+
inject,
10+
Injector,
11+
Input,
12+
NgZone,
13+
signal,
14+
TemplateRef,
15+
untracked,
16+
} from '@angular/core';
17+
import {
18+
checkUpdate,
19+
createInjectionToken,
20+
extend,
21+
injectBeforeRender,
22+
injectNgtRef,
23+
is,
24+
NgtArgs,
25+
NgtRef,
26+
signalStore,
27+
type NgtGroup,
28+
type NgtInstancedMesh,
29+
} from 'angular-three';
30+
import { NgtsSobaContent } from 'angular-three-soba/utils';
31+
import * as THREE from 'three';
32+
import { InstancedMesh } from 'three';
33+
import { PositionMesh } from './position-mesh';
34+
35+
extend({ PositionMesh, InstancedMesh });
36+
37+
export type NgtsInstancesState = {
38+
range?: number;
39+
limit: number;
40+
frames: number;
41+
};
42+
43+
declare global {
44+
interface HTMLElementTagNameMap {
45+
/**
46+
* @extends ngt-group
47+
*/
48+
'ngt-position-mesh': PositionMesh & NgtGroup;
49+
/**
50+
* @extends ngt-instanced-mesh
51+
*/
52+
'ngts-instances': NgtsInstancesState & NgtInstancedMesh;
53+
}
54+
}
55+
56+
const parentMatrix = /*@__PURE__*/ new THREE.Matrix4();
57+
const instanceMatrix = /*@__PURE__*/ new THREE.Matrix4();
58+
const tempMatrix = /*@__PURE__*/ new THREE.Matrix4();
59+
const translation = /*@__PURE__*/ new THREE.Vector3();
60+
const rotation = /*@__PURE__*/ new THREE.Quaternion();
61+
const scale = /*@__PURE__*/ new THREE.Vector3();
62+
63+
export const [injectNgtsInstancesApi, provideNgtsInstancesApi] = createInjectionToken(
64+
(instances: NgtsInstances) => instances.api,
65+
{ isRoot: false, deps: [forwardRef(() => NgtsInstances)] },
66+
);
67+
68+
@Component({
69+
selector: 'ngts-instance',
70+
standalone: true,
71+
template: `
72+
<ngt-position-mesh
73+
[ref]="instanceRef"
74+
[instance]="instancesApi.getParent()"
75+
[instanceKey]="instanceRef"
76+
ngtCompound
77+
>
78+
<ng-content />
79+
</ngt-position-mesh>
80+
`,
81+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
82+
})
83+
export class NgtsInstance {
84+
@Input() instanceRef = injectNgtRef<PositionMesh>();
85+
86+
private zone = inject(NgZone);
87+
private injector = inject(Injector);
88+
instancesApi = injectNgtsInstancesApi();
89+
90+
ngOnInit() {
91+
effect(
92+
(onCleanup) => {
93+
const cleanup = this.zone.runOutsideAngular(() => this.instancesApi.subscribe(this.instanceRef));
94+
onCleanup(cleanup);
95+
},
96+
{ injector: this.injector },
97+
);
98+
}
99+
}
100+
101+
@Component({
102+
selector: 'ngts-instances',
103+
standalone: true,
104+
template: `
105+
<ngt-instanced-mesh
106+
[userData]="{ instances: meshes() }"
107+
[ref]="instancesRef"
108+
[matrixAutoUpdate]="false"
109+
[raycast]="nullRaycast"
110+
*args="[undefined, undefined, 0]"
111+
ngtCompound
112+
>
113+
<ngt-instanced-buffer-attribute
114+
attach="instanceMatrix"
115+
[count]="matrices().length / 16"
116+
[array]="matrices()"
117+
[itemSize]="16"
118+
[usage]="DynamicDrawUsage"
119+
/>
120+
<ngt-instanced-buffer-attribute
121+
attach="instanceColor"
122+
[count]="colors().length / 3"
123+
[array]="colors()"
124+
[itemSize]="3"
125+
[usage]="DynamicDrawUsage"
126+
/>
127+
<ng-container *ngTemplateOutlet="content" />
128+
</ngt-instanced-mesh>
129+
`,
130+
imports: [NgtArgs, NgTemplateOutlet],
131+
providers: [provideNgtsInstancesApi()],
132+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
133+
})
134+
export class NgtsInstances {
135+
DynamicDrawUsage = THREE.DynamicDrawUsage;
136+
nullRaycast = () => null;
137+
138+
private inputs = signalStore<NgtsInstancesState>({ limit: 1000, frames: Infinity });
139+
140+
@Input() instancesRef = injectNgtRef<InstancedMesh>();
141+
142+
@Input({ alias: 'range' }) set _range(range: number) {
143+
this.inputs.set({ range });
144+
}
145+
146+
@Input({ alias: 'limit' }) set _limit(limit: number) {
147+
this.inputs.set({ limit });
148+
}
149+
150+
@Input({ alias: 'frames' }) set _frames(frames: number) {
151+
this.inputs.set({ frames });
152+
}
153+
154+
@ContentChild(NgtsSobaContent, { static: true, read: TemplateRef }) content!: TemplateRef<unknown>;
155+
156+
private limit = this.inputs.select('limit');
157+
158+
private positionMeshes = signal<NgtRef<PositionMesh>[]>([]);
159+
meshes = this.positionMeshes.asReadonly();
160+
161+
matrices = computed(() => new Float32Array(this.limit() * 16));
162+
colors = computed(() => {
163+
const [limit, matrices] = [this.limit(), this.matrices()];
164+
for (let i = 0; i < limit; i++) {
165+
tempMatrix.identity().toArray(matrices, i * 16);
166+
}
167+
return new Float32Array([...Array.from({ length: limit * 3 }, () => 1)]);
168+
});
169+
170+
api = {
171+
getParent: () => this.instancesRef,
172+
subscribe: (meshRef: NgtRef<PositionMesh>) => {
173+
untracked(() => {
174+
this.positionMeshes.update((s) => [...s, meshRef]);
175+
});
176+
return () => {
177+
untracked(() => {
178+
this.positionMeshes.update((s) => s.filter((positionMesh) => positionMesh !== meshRef));
179+
});
180+
};
181+
},
182+
};
183+
184+
constructor() {
185+
this.checkUpdate();
186+
this.beforeRender();
187+
}
188+
189+
private checkUpdate() {
190+
effect(() => {
191+
const instancedMesh = this.instancesRef.nativeElement;
192+
if (!instancedMesh) return;
193+
checkUpdate(instancedMesh.instanceMatrix);
194+
});
195+
}
196+
197+
private beforeRender() {
198+
let count = 0;
199+
let updateRange = 0;
200+
injectBeforeRender(() => {
201+
const [{ frames, limit, range }, instancedMesh, instances, matrices, colors] = [
202+
this.inputs.get(),
203+
this.instancesRef.nativeElement,
204+
this.meshes(),
205+
this.matrices(),
206+
this.colors(),
207+
];
208+
if (frames === Infinity || count < frames) {
209+
instancedMesh.updateMatrix();
210+
instancedMesh.updateMatrixWorld();
211+
parentMatrix.copy(instancedMesh.matrixWorld).invert();
212+
213+
updateRange = Math.min(limit, range !== undefined ? range : limit, instances.length);
214+
instancedMesh.count = updateRange;
215+
instancedMesh.instanceMatrix.updateRange.count = updateRange * 16;
216+
if (instancedMesh.instanceColor) {
217+
instancedMesh.instanceColor.updateRange.count = updateRange * 3;
218+
}
219+
220+
for (let i = 0; i < instances.length; i++) {
221+
const instanceRef = instances[i];
222+
const instance = is.ref(instanceRef) ? instanceRef.nativeElement : instanceRef;
223+
// Multiply the inverse of the InstancedMesh world matrix or else
224+
// Instances will be double-transformed if <Instances> isn't at identity
225+
instance.matrixWorld.decompose(translation, rotation, scale);
226+
instanceMatrix.compose(translation, rotation, scale).premultiply(parentMatrix);
227+
instanceMatrix.toArray(matrices, i * 16);
228+
instancedMesh.instanceMatrix.needsUpdate = true;
229+
instance.color.toArray(colors, i * 3);
230+
checkUpdate(instancedMesh.instanceColor);
231+
}
232+
count++;
233+
}
234+
});
235+
}
236+
}
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
constructor() {
14+
super();
15+
this.color = new THREE.Color('white');
16+
this.instance = new ElementRef(undefined);
17+
this.instanceKey = new ElementRef(undefined);
18+
}
19+
20+
// This will allow the virtual instance have bounds
21+
get geometry() {
22+
return this.instance.nativeElement?.geometry;
23+
}
24+
25+
// And this will allow the virtual instance to receive events
26+
override raycast(raycaster: THREE.Raycaster, intersects: THREE.Intersection[]) {
27+
const parent = this.instance.nativeElement;
28+
if (!parent) return;
29+
if (!parent.geometry || !parent.material) return;
30+
_mesh.geometry = parent.geometry;
31+
const matrixWorld = parent.matrixWorld;
32+
const instanceId = parent.userData['instances'].indexOf(this.instanceKey);
33+
// If the instance wasn't found or exceeds the parents draw range, bail out
34+
if (instanceId === -1 || instanceId > parent.count) return;
35+
// calculate the world matrix for each instance
36+
parent.getMatrixAt(instanceId, _instanceLocalMatrix);
37+
_instanceWorldMatrix.multiplyMatrices(matrixWorld, _instanceLocalMatrix);
38+
// the mesh represents this single instance
39+
_mesh.matrixWorld = _instanceWorldMatrix;
40+
// raycast side according to instance material
41+
if (parent.material instanceof THREE.Material) _mesh.material.side = parent.material.side;
42+
else _mesh.material.side = parent.material[0].side;
43+
_mesh.raycast(raycaster, _instanceIntersects);
44+
// process the result of raycast
45+
for (let i = 0, l = _instanceIntersects.length; i < l; i++) {
46+
const intersect = _instanceIntersects[i];
47+
intersect.instanceId = instanceId;
48+
intersect.object = this;
49+
intersects.push(intersect);
50+
}
51+
_instanceIntersects.length = 0;
52+
}
53+
}

Diff for: libs/soba/performances/src/points/points.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class NgtsPointsInstances {
120120

121121
injector = inject(Injector);
122122

123-
api = computed(() => ({
123+
api = {
124124
getParent: () => this.pointsInput.pointsRef,
125125
subscribe: (pointRef: NgtRef<PositionPoint>) => {
126126
untracked(() => {
@@ -132,7 +132,7 @@ export class NgtsPointsInstances {
132132
});
133133
};
134134
},
135-
}));
135+
};
136136

137137
constructor() {
138138
this.checkUpdatePositionAttribute();
@@ -194,7 +194,7 @@ export class NgtsPointsInstances {
194194
standalone: true,
195195
template: `
196196
<ngt-position-point
197-
[instance]="pointsInstancesApi().getParent()"
197+
[instance]="pointsInstancesApi.getParent()"
198198
[ref]="pointRef"
199199
[instanceKey]="pointRef"
200200
ngtCompound
@@ -214,10 +214,7 @@ export class NgtsPoint implements OnInit {
214214
ngOnInit() {
215215
effect(
216216
(onCleanup) => {
217-
const cleanup = this.zone.runOutsideAngular(() => {
218-
const api = this.pointsInstancesApi();
219-
return api.subscribe(this.pointRef);
220-
});
217+
const cleanup = this.zone.runOutsideAngular(() => this.pointsInstancesApi.subscribe(this.pointRef));
221218
onCleanup(cleanup);
222219
},
223220
{ injector: this.injector },

Diff for: libs/soba/performances/src/points/position-point.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export class PositionPoint extends THREE.Group {
1010
size: number;
1111
color: THREE.Color;
1212
instance: ElementRef<THREE.Points | undefined>;
13-
instanceKey: ElementRef<any>;
13+
instanceKey: ElementRef<PositionPoint | undefined>;
1414

1515
constructor() {
1616
super();

0 commit comments

Comments
 (0)