Skip to content

Commit 67207ee

Browse files
committed
cameras
1 parent 01d829e commit 67207ee

File tree

7 files changed

+367
-1
lines changed

7 files changed

+367
-1
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { NgTemplateOutlet } from '@angular/common';
2+
import {
3+
CUSTOM_ELEMENTS_SCHEMA,
4+
Component,
5+
ContentChild,
6+
DestroyRef,
7+
Directive,
8+
EmbeddedViewRef,
9+
Input,
10+
OnInit,
11+
Signal,
12+
TemplateRef,
13+
ViewChild,
14+
ViewContainerRef,
15+
computed,
16+
effect,
17+
inject,
18+
runInInjectionContext,
19+
untracked,
20+
type Injector,
21+
} from '@angular/core';
22+
import {
23+
NgtArgs,
24+
assertInjectionContext,
25+
extend,
26+
injectBeforeRender,
27+
injectNgtRef,
28+
injectNgtStore,
29+
signalStore,
30+
type NgtGroup,
31+
} from 'angular-three';
32+
import * as THREE from 'three';
33+
import { Group } from 'three';
34+
35+
export type NgtsCubeCameraState = {
36+
/** Resolution of the FBO, 256 */
37+
resolution: number;
38+
/** Camera near, 0.1 */
39+
near: number;
40+
/** Camera far, 1000 */
41+
far: number;
42+
/** Custom environment map that is temporarily set as the scenes background */
43+
envMap?: THREE.Texture;
44+
/** Custom fog that is temporarily set as the scenes fog */
45+
fog?: THREE.Fog | THREE.FogExp2;
46+
};
47+
48+
const defaultCubeCameraState = {
49+
resolution: 256,
50+
near: 0.1,
51+
far: 1000,
52+
} satisfies NgtsCubeCameraState;
53+
54+
export function injectNgtsCubeCamera(
55+
cubeCameraState: () => Partial<NgtsCubeCameraState>,
56+
{ injector }: { injector?: Injector } = {},
57+
) {
58+
injector = assertInjectionContext(injectNgtsCubeCamera, injector);
59+
return runInInjectionContext(injector, () => {
60+
const state = computed(() => {
61+
const cameraState = cubeCameraState();
62+
return { ...defaultCubeCameraState, ...cameraState };
63+
});
64+
65+
const store = injectNgtStore();
66+
67+
const gl = store.select('gl');
68+
const scene = store.select('scene');
69+
70+
const fbo = computed(() => {
71+
const renderTarget = new THREE.WebGLCubeRenderTarget(state().resolution);
72+
renderTarget.texture.type = THREE.HalfFloatType;
73+
return renderTarget;
74+
});
75+
76+
effect((onCleanup) => {
77+
const _fbo = fbo();
78+
onCleanup(() => _fbo.dispose());
79+
});
80+
81+
const camera = computed(() => {
82+
const { near, far } = state();
83+
return new THREE.CubeCamera(near, far, fbo());
84+
});
85+
86+
let originalFog: THREE.Scene['fog'];
87+
let originalBackground: THREE.Scene['background'];
88+
89+
const update = computed(() => {
90+
const _scene = scene();
91+
const _gl = gl();
92+
const _camera = camera();
93+
const { envMap, fog } = untracked(state);
94+
95+
return () => {
96+
originalFog = _scene.fog;
97+
originalBackground = _scene.background;
98+
_scene.background = envMap || originalBackground;
99+
_scene.fog = fog || originalFog;
100+
_camera.update(_gl, _scene);
101+
_scene.fog = originalFog;
102+
_scene.background = originalBackground;
103+
};
104+
});
105+
106+
return { fbo, camera, update };
107+
});
108+
}
109+
110+
extend({ Group });
111+
112+
export type NgtsCubeCameraComponentState = NgtsCubeCameraState & {
113+
frames: number;
114+
};
115+
116+
declare global {
117+
interface HTMLElementTagNameMap {
118+
/**
119+
* @extends ngt-group
120+
*/
121+
'ngts-cube-camera': NgtsCubeCameraState & NgtGroup & { frames?: number };
122+
}
123+
}
124+
125+
@Directive({ selector: 'ng-template[ngtsCubeCameraContent]', standalone: true })
126+
export class NgtsCubeCameraContent {
127+
static ngTemplateContextGuard(
128+
_: NgtsCubeCameraContent,
129+
ctx: unknown,
130+
): ctx is { texture: Signal<THREE.WebGLRenderTarget['texture']> } {
131+
return true;
132+
}
133+
}
134+
135+
@Component({
136+
selector: 'ngts-cube-camera',
137+
standalone: true,
138+
template: `
139+
<ngt-group ngtCompound>
140+
<ngt-primitive *args="[cubeCamera.camera()]" />
141+
<ngt-group [ref]="cameraRef">
142+
<ng-container #anchor />
143+
</ngt-group>
144+
</ngt-group>
145+
`,
146+
imports: [NgtArgs, NgTemplateOutlet],
147+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
148+
})
149+
export class NgtsCubeCamera implements OnInit {
150+
private inputs = signalStore<NgtsCubeCameraComponentState>({ frames: Infinity });
151+
152+
@Input() cameraRef = injectNgtRef<Group>();
153+
154+
@ContentChild(NgtsCubeCameraContent, { static: true, read: TemplateRef })
155+
cameraContent!: TemplateRef<{ texture: Signal<THREE.WebGLRenderTarget['texture']> }>;
156+
157+
@ViewChild('anchor', { static: true, read: ViewContainerRef })
158+
anchor!: ViewContainerRef;
159+
160+
/** Resolution of the FBO, 256 */
161+
@Input({ alias: 'resolution' }) set _resolution(resolution: number) {
162+
this.inputs.set({ resolution });
163+
}
164+
165+
/** Camera near, 0.1 */
166+
@Input({ alias: 'near' }) set _near(near: number) {
167+
this.inputs.set({ near });
168+
}
169+
170+
/** Camera far, 1000 */
171+
@Input({ alias: 'far' }) set _far(far: number) {
172+
this.inputs.set({ far });
173+
}
174+
175+
/** Custom environment map that is temporarily set as the scenes background */
176+
@Input({ alias: 'envMap' }) set _envMap(envMap: THREE.Texture) {
177+
this.inputs.set({ envMap });
178+
}
179+
180+
/** Custom fog that is temporarily set as the scenes fog */
181+
@Input({ alias: 'fog' }) set _fog(fog: THREE.Fog | THREE.FogExp2) {
182+
this.inputs.set({ fog });
183+
}
184+
185+
cubeCamera = injectNgtsCubeCamera(this.inputs.select());
186+
private texture = computed(() => this.cubeCamera.fbo().texture);
187+
private contentRef?: EmbeddedViewRef<unknown>;
188+
189+
constructor() {
190+
this.beforeRender();
191+
inject(DestroyRef).onDestroy(() => {
192+
this.contentRef?.destroy();
193+
});
194+
}
195+
196+
ngOnInit() {
197+
this.contentRef = this.anchor.createEmbeddedView(this.cameraContent, { texture: this.texture });
198+
}
199+
200+
private beforeRender() {
201+
let count = 0;
202+
injectBeforeRender(() => {
203+
const camera = this.cameraRef.nativeElement;
204+
if (!camera) return;
205+
const update = this.cubeCamera.update();
206+
const frames = this.inputs.get('frames');
207+
if (frames === Infinity || count < frames) {
208+
camera.visible = false;
209+
update();
210+
camera.visible = true;
211+
count++;
212+
}
213+
});
214+
}
215+
}

libs/soba/cameras/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export * from './cube-camera/cube-camera';
12
export * from './orthographic-camera/orthographic-camera';
23
export * from './perspective-camera/perspective-camera';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { CUSTOM_ELEMENTS_SCHEMA, Component, Input } from '@angular/core';
2+
import { NgtArgs, NgtBeforeRenderEvent } from 'angular-three';
3+
import { NgtsCubeCamera, NgtsCubeCameraContent } from 'angular-three-soba/cameras';
4+
import { makeDecorators, makeStoryObject } from '../setup-canvas';
5+
6+
@Component({
7+
selector: 'cube-camera-sphere',
8+
standalone: true,
9+
template: `
10+
<ngts-cube-camera [position]="position">
11+
<ngt-mesh *ngtsCubeCameraContent="''; texture as texture" (beforeRender)="onBeforeRender($event)">
12+
<ngt-sphere-geometry *args="[5, 64, 64]" />
13+
<ngt-mesh-standard-material [roughness]="0" [metalness]="1" [envMap]="texture()" />
14+
</ngt-mesh>
15+
</ngts-cube-camera>
16+
`,
17+
imports: [NgtsCubeCamera, NgtsCubeCameraContent, NgtArgs],
18+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
19+
})
20+
class Sphere {
21+
@Input() position = [0, 0, 0];
22+
@Input() offset = 0;
23+
24+
onBeforeRender({ object, state: { clock } }: NgtBeforeRenderEvent<THREE.Mesh>) {
25+
object.position.y = Math.sin(this.offset + clock.elapsedTime) * 5;
26+
}
27+
}
28+
29+
@Component({
30+
standalone: true,
31+
template: `
32+
<ngt-fog attach="fog" *args="['#f0f0f0', 100, 200]" />
33+
34+
<cube-camera-sphere [position]="[-10, 10, 0]" />
35+
<cube-camera-sphere [position]="[10, 9, 0]" [offset]="2000" />
36+
37+
<ngt-mesh>
38+
<ngt-box-geometry *args="[5, 5, 5]" [position]="[0, 2.5, 0]" />
39+
<ngt-mesh-basic-material color="hotpink" />
40+
</ngt-mesh>
41+
42+
<ngt-grid-helper *args="[100, 10]" />
43+
`,
44+
imports: [NgtArgs, Sphere],
45+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
46+
})
47+
class DefaultCubeCameraStory {}
48+
49+
export default {
50+
title: 'Camera/CubeCamera',
51+
decorators: makeDecorators(),
52+
};
53+
54+
export const Default = makeStoryObject(DefaultCubeCameraStory, {
55+
canvasOptions: { camera: { position: [0, 10, 40] } },
56+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NgFor } from '@angular/common';
2+
import { CUSTOM_ELEMENTS_SCHEMA, Component, TrackByFunction } from '@angular/core';
3+
import { NgtArgs } from 'angular-three';
4+
import { NgtsOrthographicCamera } from 'angular-three-soba/cameras';
5+
import { makeDecorators, makeStoryObject } from '../setup-canvas';
6+
import { positions, type Position } from './positions';
7+
8+
@Component({
9+
standalone: true,
10+
template: `
11+
<ngts-orthographic-camera [makeDefault]="true" [position]="[0, 0, 10]" [zoom]="40" />
12+
13+
<ngt-group [position]="[0, 0, -10]">
14+
<ngt-mesh *ngFor="let position of positions(); trackBy: trackBy" [position]="position.position">
15+
<ngt-icosahedron-geometry *args="[1, 1]" />
16+
<ngt-mesh-basic-material color="white" [wireframe]="true" />
17+
</ngt-mesh>
18+
</ngt-group>
19+
`,
20+
imports: [NgtsOrthographicCamera, NgFor, NgtArgs],
21+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
22+
})
23+
class DefaultOrthographicCameraStory {
24+
positions = positions;
25+
trackBy: TrackByFunction<Position> = (_, item) => item.id;
26+
}
27+
28+
export default {
29+
title: 'Camera/OrthographicCamera',
30+
decorators: makeDecorators(),
31+
};
32+
33+
export const Default = makeStoryObject(DefaultOrthographicCameraStory);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NgFor } from '@angular/common';
2+
import { CUSTOM_ELEMENTS_SCHEMA, Component, TrackByFunction } from '@angular/core';
3+
import { NgtArgs } from 'angular-three';
4+
import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';
5+
import { makeDecorators, makeStoryObject } from '../setup-canvas';
6+
import { positions, type Position } from './positions';
7+
8+
@Component({
9+
standalone: true,
10+
template: `
11+
<ngts-perspective-camera [makeDefault]="true" [position]="[0, 0, 10]" />
12+
13+
<ngt-group [position]="[0, 0, -10]">
14+
<ngt-mesh *ngFor="let position of positions(); trackBy: trackBy" [position]="position.position">
15+
<ngt-icosahedron-geometry *args="[1, 1]" />
16+
<ngt-mesh-basic-material color="white" [wireframe]="true" />
17+
</ngt-mesh>
18+
</ngt-group>
19+
`,
20+
imports: [NgtsPerspectiveCamera, NgFor, NgtArgs],
21+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
22+
})
23+
class DefaultPerspectiveCameraStory {
24+
positions = positions;
25+
trackBy: TrackByFunction<Position> = (_, item) => item.id;
26+
}
27+
28+
export default {
29+
title: 'Camera/PerspectiveCamera',
30+
decorators: makeDecorators(),
31+
};
32+
33+
export const Default = makeStoryObject(DefaultPerspectiveCameraStory);

libs/soba/src/cameras/positions.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { computed } from '@angular/core';
2+
3+
const NUM = 3;
4+
5+
export interface Position {
6+
id: string;
7+
position: [number, number, number];
8+
}
9+
10+
export const positions = computed(() => {
11+
const pos: Position[] = [];
12+
const half = (NUM - 1) / 2;
13+
14+
for (let x = 0; x < NUM; x++) {
15+
for (let y = 0; y < NUM; y++) {
16+
pos.push({
17+
id: `${x}-${y}`,
18+
position: [(x - half) * 4, (y - half) * 4, 0],
19+
});
20+
}
21+
}
22+
23+
return pos;
24+
});

libs/soba/src/setup-canvas.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
type Signal,
2222
type Type,
2323
} from '@angular/core';
24-
import type { Args } from '@storybook/angular';
24+
import { Decorator, moduleMetadata, type Args } from '@storybook/angular';
2525
import { NgtArgs, NgtCanvas, extend, safeDetectChanges, type NgtPerformance } from 'angular-three';
2626
import { NgtsOrbitControls } from 'angular-three-soba/controls';
2727
// import { NgtsLoader } from 'angular-three-soba/loaders';
@@ -273,3 +273,7 @@ export function select(defaultValue: string | string[], { multi, options }: { op
273273
export function turn(object: THREE.Object3D) {
274274
object.rotation.y += 0.01;
275275
}
276+
277+
export function makeDecorators(...decoratorFns: Decorator[]): Decorator[] {
278+
return [moduleMetadata({ imports: [StorybookSetup] }), ...decoratorFns];
279+
}

0 commit comments

Comments
 (0)