Skip to content

Commit 03e21af

Browse files
committed
trail
1 parent 53152a4 commit 03e21af

File tree

8 files changed

+487
-7
lines changed

8 files changed

+487
-7
lines changed

libs/soba/misc/src/html/html.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ declare global {
9494
[ref]="occlusionMeshRef"
9595
>
9696
<ngt-plane-geometry *ngIf="geometry()" />
97-
<ngt-shader-material *ngIf="material()" [side]="DoubleSide" />
97+
<ngt-shader-material
98+
*ngIf="material()"
99+
[side]="DoubleSide"
100+
[vertexShader]="shaders().vertexShader"
101+
[fragmentShader]="shaders().fragmentShader"
102+
/>
98103
</ngt-mesh>
99104
</ngt-group>
100105
@@ -220,6 +225,8 @@ export class NgtsHtml {
220225

221226
occlusionMeshRef = injectNgtRef<Mesh>();
222227

228+
private transform = this.inputs.select('transform');
229+
223230
isRayCastOcclusion = computed(() => {
224231
const occlude = this.occlude();
225232
return (
@@ -234,10 +241,57 @@ export class NgtsHtml {
234241
material = this.inputs.select('material');
235242
scale = this.inputs.select('scale');
236243

244+
shaders = computed(() => {
245+
const transform = this.transform();
246+
return {
247+
vertexShader: !transform
248+
? /* glsl */ `
249+
/*
250+
This shader is from the THREE's SpriteMaterial.
251+
We need to turn the backing plane into a Sprite
252+
(make it always face the camera) if "transfrom"
253+
is false.
254+
*/
255+
#include <common>
256+
257+
void main() {
258+
vec2 center = vec2(0., 1.);
259+
float rotation = 0.0;
260+
261+
// This is somewhat arbitrary, but it seems to work well
262+
// Need to figure out how to derive this dynamically if it even matters
263+
float size = 0.03;
264+
265+
vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );
266+
vec2 scale;
267+
scale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );
268+
scale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );
269+
270+
bool isPerspective = isPerspectiveMatrix( projectionMatrix );
271+
if ( isPerspective ) scale *= - mvPosition.z;
272+
273+
vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale * size;
274+
vec2 rotatedPosition;
275+
rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;
276+
rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;
277+
mvPosition.xy += rotatedPosition;
278+
279+
gl_Position = projectionMatrix * mvPosition;
280+
}
281+
`
282+
: undefined,
283+
fragmentShader: /* glsl */ `
284+
void main() {
285+
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
286+
}
287+
`,
288+
};
289+
});
290+
237291
state = {
238292
zIndexRange: this.inputs.select('zIndexRange'),
239293
prepend: this.inputs.select('prepend'),
240-
transform: this.inputs.select('transform'),
294+
transform: this.transform,
241295
center: this.inputs.select('center'),
242296
fullscreen: this.inputs.select('fullscreen'),
243297
calculatePosition: this.inputs.select('calculatePosition'),

libs/soba/misc/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './html/html';
88
export * from './sampler/sampler';
99
export * from './shadow/shadow';
1010
export * from './stats-gl/stats-gl';
11+
export * from './trail/trail';

libs/soba/misc/src/trail/trail.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import {
2+
CUSTOM_ELEMENTS_SCHEMA,
3+
Component,
4+
Input,
5+
computed,
6+
effect,
7+
runInInjectionContext,
8+
type Injector,
9+
} from '@angular/core';
10+
import {
11+
NgtPortal,
12+
NgtPortalContent,
13+
NgtRef,
14+
assertInjectionContext,
15+
extend,
16+
injectBeforeRender,
17+
injectNgtRef,
18+
injectNgtStore,
19+
is,
20+
signalStore,
21+
} from 'angular-three';
22+
import { MeshLineGeometry, MeshLineMaterial } from 'meshline';
23+
import * as THREE from 'three';
24+
import { Group, Mesh } from 'three';
25+
26+
const shiftLeft = (collection: Float32Array, steps = 1): Float32Array => {
27+
collection.set(collection.subarray(steps));
28+
collection.fill(-Infinity, -steps);
29+
return collection;
30+
};
31+
32+
export type NgtsTrailSettings = {
33+
width: number;
34+
length: number;
35+
decay: number;
36+
/**
37+
* Wether to use the target's world or local positions
38+
*/
39+
local: boolean;
40+
// Min distance between previous and current points
41+
stride: number;
42+
// Number of frames to wait before next calculation
43+
interval: number;
44+
};
45+
46+
const defaultSettings: NgtsTrailSettings = {
47+
width: 0.2,
48+
length: 1,
49+
decay: 1,
50+
local: false,
51+
stride: 0,
52+
interval: 1,
53+
};
54+
55+
export function injectNgtsTrail(
56+
targetFactory: () => NgtRef<THREE.Object3D> | null,
57+
settingsFactory: () => Partial<NgtsTrailSettings>,
58+
{ injector }: { injector?: Injector } = {},
59+
) {
60+
injector = assertInjectionContext(injectNgtsTrail, injector);
61+
return runInInjectionContext(injector, () => {
62+
const points = injectNgtRef<Float32Array>();
63+
let frameCount = 0;
64+
65+
const prevPosition = new THREE.Vector3();
66+
const worldPosition = new THREE.Vector3();
67+
68+
const _target = computed(() => {
69+
const _target = targetFactory();
70+
if (is.ref(_target)) return _target.nativeElement;
71+
return _target;
72+
});
73+
74+
const _settings = computed(() => ({ ...defaultSettings, ...settingsFactory() }));
75+
const _length = computed(() => _settings().length);
76+
77+
effect(() => {
78+
const [target, length] = [_target(), _length()];
79+
if (target) {
80+
points.nativeElement = Float32Array.from({ length: length * 10 * 3 }, (_, i) =>
81+
target.position.getComponent(i % 3),
82+
);
83+
}
84+
});
85+
86+
injectBeforeRender(() => {
87+
const [target, _points, { local, decay, stride, interval }] = [
88+
_target(),
89+
points.nativeElement,
90+
_settings(),
91+
];
92+
if (!target) return;
93+
if (!_points) return;
94+
95+
if (frameCount === 0) {
96+
let newPosition: THREE.Vector3;
97+
if (local) {
98+
newPosition = target.position;
99+
} else {
100+
target.getWorldPosition(worldPosition);
101+
newPosition = worldPosition;
102+
}
103+
104+
const steps = 1 * decay;
105+
for (let i = 0; i < steps; i++) {
106+
if (newPosition.distanceTo(prevPosition) < stride) continue;
107+
108+
shiftLeft(_points, 3);
109+
_points.set(newPosition.toArray(), _points.length - 3);
110+
}
111+
prevPosition.copy(newPosition);
112+
}
113+
114+
frameCount++;
115+
frameCount = frameCount % interval;
116+
});
117+
118+
return points;
119+
});
120+
}
121+
122+
export type NgtsTrailState = {
123+
color: THREE.ColorRepresentation;
124+
attenuation: (width: number) => number;
125+
settings: NgtsTrailSettings;
126+
target?: NgtRef<THREE.Object3D>;
127+
};
128+
129+
declare global {
130+
interface HTMLElementTagNameMap {
131+
'ngts-trail': NgtsTrailState;
132+
}
133+
}
134+
135+
extend({ Group, Mesh });
136+
137+
@Component({
138+
selector: 'ngts-trail',
139+
standalone: true,
140+
template: `
141+
<ngt-group>
142+
<ngt-portal [container]="scene()" [autoRender]="false">
143+
<ng-template ngtPortalContent>
144+
<ngt-mesh [ref]="trailRef" [geometry]="geometry" [material]="material()" />
145+
</ng-template>
146+
</ngt-portal>
147+
<ngt-group [ref]="groupRef">
148+
<ng-content />
149+
</ngt-group>
150+
</ngt-group>
151+
`,
152+
imports: [NgtPortal, NgtPortalContent],
153+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
154+
})
155+
export class NgtsTrail {
156+
private inputs = signalStore<NgtsTrailState>({ settings: defaultSettings, color: 'hotpink' });
157+
158+
@Input() trailRef = injectNgtRef<Mesh>();
159+
160+
@Input({ alias: 'color' }) set _color(color: THREE.ColorRepresentation) {
161+
this.inputs.set({ color });
162+
}
163+
164+
@Input({ alias: 'attenuation' }) set _attenuation(attenuation: (width: number) => number) {
165+
this.inputs.set({ attenuation });
166+
}
167+
168+
@Input({ alias: 'target' }) set _target(target: NgtRef<THREE.Object3D>) {
169+
this.inputs.set({ target });
170+
}
171+
172+
@Input({ alias: 'settings' }) set _settings(settings: Partial<NgtsTrailSettings>) {
173+
this.inputs.set({ settings: { ...defaultSettings, ...settings } });
174+
}
175+
176+
groupRef = injectNgtRef<Group>();
177+
178+
private children = this.groupRef.children('both');
179+
180+
private store = injectNgtStore();
181+
private size = this.store.select('size');
182+
183+
private target = this.inputs.select('target');
184+
private settings = this.inputs.select('settings');
185+
private width = computed(() => this.settings().width);
186+
private color = this.inputs.select('color');
187+
private attenuation = this.inputs.select('attenuation');
188+
189+
private anchor = computed(() => {
190+
const target = this.target();
191+
if (target) return target;
192+
const group = this.groupRef.nativeElement;
193+
if (group) {
194+
return group.children.find((child) => child instanceof THREE.Object3D) || null;
195+
}
196+
return null;
197+
});
198+
199+
private points = injectNgtsTrail(this.anchor, this.settings);
200+
201+
scene = this.store.select('scene');
202+
geometry = new MeshLineGeometry();
203+
material = computed(() => {
204+
const [width, color, size] = [this.width(), this.color(), this.size()];
205+
206+
const m = new MeshLineMaterial({
207+
lineWidth: 0.1 * width,
208+
color,
209+
sizeAttenuation: 1,
210+
resolution: new THREE.Vector2(size.width, size.height),
211+
});
212+
213+
// TODO: understand this first
214+
// Get and apply first <meshLineMaterial /> from children
215+
// let matOverride: React.ReactElement | undefined;
216+
// if (children) {
217+
// if (Array.isArray(children)) {
218+
// matOverride = children.find((child: React.ReactNode) => {
219+
// const c = child as React.ReactElement;
220+
// return typeof c.type === 'string' && c.type === 'meshLineMaterial';
221+
// }) as React.ReactElement | undefined;
222+
// } else {
223+
// const c = children as React.ReactElement;
224+
// if (typeof c.type === 'string' && c.type === 'meshLineMaterial') {
225+
// matOverride = c;
226+
// }
227+
// }
228+
// }
229+
// if (typeof matOverride?.props === 'object') {
230+
// m.setValues(matOverride.props);
231+
// }
232+
233+
return m;
234+
});
235+
236+
constructor() {
237+
this.setMaterialSize();
238+
this.beforeRender();
239+
}
240+
241+
private setMaterialSize() {
242+
effect(() => {
243+
const [material, size] = [this.material(), this.size()];
244+
material.uniforms['resolution'].value.set(size.width, size.height);
245+
});
246+
}
247+
248+
private beforeRender() {
249+
injectBeforeRender(() => {
250+
const [points, attenuation] = [this.points.nativeElement, this.attenuation()];
251+
if (!points) return;
252+
this.geometry.setPoints(points, attenuation);
253+
});
254+
}
255+
}

libs/soba/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,18 @@
2929
"@angular/common": "^16.0.0",
3030
"@angular/core": "^16.0.0",
3131
"angular-three": "^2.0.0",
32-
"three": ">=0.148.0",
33-
"stats.js": "^0.17.0",
32+
"meshline": "^3.1.6",
3433
"stats-gl": "^1.0.0",
34+
"stats.js": "^0.17.0",
35+
"three": ">=0.148.0",
3536
"three-mesh-bvh": "^0.5.0 || ^0.6.0",
3637
"three-stdlib": "^2.0.0",
3738
"troika-three-text": "^0.47.0"
3839
},
3940
"dependencies": {
40-
"tslib": "^2.3.0",
4141
"@nx/devkit": "^16.0.0",
42-
"nx": "^16.0.0"
42+
"nx": "^16.0.0",
43+
"tslib": "^2.3.0"
4344
},
4445
"sideEffects": false
4546
}

0 commit comments

Comments
 (0)