Skip to content

Commit cd67a8b

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
feat(soba): migrate spotlight
1 parent 64db4c2 commit cd67a8b

File tree

11 files changed

+822
-5
lines changed

11 files changed

+822
-5
lines changed

libs/soba/misc/src/depth-buffer/depth-buffer.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface NgtsDepthBufferParams {
99
}
1010

1111
export function injectNgtsDepthBuffer(
12-
paramsFactory: () => NgtsDepthBufferParams = () => ({ size: 256, frames: Infinity }),
12+
paramsFactory: () => Partial<NgtsDepthBufferParams> = () => ({}),
1313
{ injector }: { injector?: Injector } = {}
1414
) {
1515
injector = assertInjectionContext(injectNgtsDepthBuffer, injector);
@@ -22,8 +22,7 @@ export function injectNgtsDepthBuffer(
2222
const dpr = store.select('viewport', 'dpr');
2323

2424
const fboParams = computed(() => {
25-
const params = paramsFactory();
26-
25+
const params = { size: 256, frames: Infinity, ...paramsFactory() };
2726
const width = params.size || size().width * dpr();
2827
const height = params.size || size().height * dpr();
2928
const depthTexture = new THREE.DepthTexture(width, height);
@@ -36,7 +35,7 @@ export function injectNgtsDepthBuffer(
3635

3736
let count = 0;
3837
injectBeforeRender(({ gl, scene, camera }) => {
39-
const params = paramsFactory();
38+
const params = { size: 256, frames: Infinity, ...paramsFactory() };
4039
if ((params.frames === Infinity || count < params.frames) && fboRef.untracked) {
4140
gl.setRenderTarget(fboRef.untracked);
4241
gl.render(scene, camera);

libs/soba/shaders/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './sparkles-material/sparkles-material';
2+
export * from './spot-light-material/spot-light-material';
23
export * from './star-field-material/star-field-material';
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Color, ShaderMaterial, Vector2, Vector3 } from 'three';
2+
3+
export class SpotLightMaterial extends ShaderMaterial {
4+
constructor() {
5+
super({
6+
uniforms: {
7+
depth: { value: null },
8+
opacity: { value: 1 },
9+
attenuation: { value: 2.5 },
10+
anglePower: { value: 12 },
11+
spotPosition: { value: new Vector3(0, 0, 0) },
12+
lightColor: { value: new Color('white') },
13+
cameraNear: { value: 0 },
14+
cameraFar: { value: 1 },
15+
resolution: { value: new Vector2(0, 0) },
16+
},
17+
transparent: true,
18+
depthWrite: false,
19+
vertexShader: /* glsl */ `
20+
varying vec3 vNormal;
21+
varying vec3 vWorldPosition;
22+
varying float vViewZ;
23+
varying float vIntensity;
24+
uniform vec3 spotPosition;
25+
uniform float attenuation;
26+
27+
void main() {
28+
// compute intensity
29+
vNormal = normalize( normalMatrix * normal );
30+
vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
31+
vWorldPosition = worldPosition.xyz;
32+
vec4 viewPosition = viewMatrix * worldPosition;
33+
vViewZ = viewPosition.z;
34+
float intensity = distance(worldPosition.xyz, spotPosition) / attenuation;
35+
intensity = 1.0 - clamp(intensity, 0.0, 1.0);
36+
vIntensity = intensity;
37+
// set gl_Position
38+
gl_Position = projectionMatrix * viewPosition;
39+
40+
}`,
41+
fragmentShader: /* glsl */ `
42+
#include <packing>
43+
44+
varying vec3 vNormal;
45+
varying vec3 vWorldPosition;
46+
uniform vec3 lightColor;
47+
uniform vec3 spotPosition;
48+
uniform float attenuation;
49+
uniform float anglePower;
50+
uniform sampler2D depth;
51+
uniform vec2 resolution;
52+
uniform float cameraNear;
53+
uniform float cameraFar;
54+
varying float vViewZ;
55+
varying float vIntensity;
56+
uniform float opacity;
57+
58+
float readDepth( sampler2D depthSampler, vec2 coord ) {
59+
float fragCoordZ = texture2D( depthSampler, coord ).x;
60+
float viewZ = perspectiveDepthToViewZ(fragCoordZ, cameraNear, cameraFar);
61+
return viewZ;
62+
}
63+
64+
void main() {
65+
float d = 1.0;
66+
bool isSoft = resolution[0] > 0.0 && resolution[1] > 0.0;
67+
if (isSoft) {
68+
vec2 sUv = gl_FragCoord.xy / resolution;
69+
d = readDepth(depth, sUv);
70+
}
71+
float intensity = vIntensity;
72+
vec3 normal = vec3(vNormal.x, vNormal.y, abs(vNormal.z));
73+
float angleIntensity = pow( dot(normal, vec3(0.0, 0.0, 1.0)), anglePower );
74+
intensity *= angleIntensity;
75+
// fades when z is close to sampled depth, meaning the cone is intersecting existing geometry
76+
if (isSoft) {
77+
intensity *= smoothstep(0., 1., vViewZ - d);
78+
}
79+
gl_FragColor = vec4(lightColor, intensity * opacity);
80+
81+
#include <tonemapping_fragment>
82+
#include <encodings_fragment>
83+
}`,
84+
});
85+
}
86+
}

libs/soba/src/setup-canvas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,11 @@ export class StorybookSetup implements OnInit {
172172
this.#envInjector
173173
);
174174
this.#ref = this.anchor.createComponent(NgtCanvas, { environmentInjector: this.#refEnvInjector });
175-
this.#ref.setInput('sceneGraph', StorybookScene);
176175
this.#ref.setInput('shadows', true);
177176
this.#ref.setInput('performance', this.options.performance);
178177
this.#ref.setInput('camera', this.options.camera);
179178
this.#ref.setInput('compoundPrefixes', this.options.compoundPrefixes);
179+
this.#ref.setInput('sceneGraph', StorybookScene);
180180
safeDetectChanges(this.#ref.changeDetectorRef);
181181
}
182182
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { NgIf } from '@angular/common';
2+
import { Component, CUSTOM_ELEMENTS_SCHEMA, effect, Input, signal } from '@angular/core';
3+
import { Meta, moduleMetadata } from '@storybook/angular';
4+
import { checkUpdate, NgtArgs } from 'angular-three';
5+
import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';
6+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
7+
import { injectNgtsTextureLoader } from 'angular-three-soba/loaders';
8+
import { injectNgtsDepthBuffer } from 'angular-three-soba/misc';
9+
import { NgtsSpotLight, NgtsSpotLightShadow } from 'angular-three-soba/staging';
10+
import * as THREE from 'three';
11+
import { makeStoryObject, number, StorybookSetup } from '../setup-canvas';
12+
13+
injectNgtsTextureLoader.preload(() => '/soba/textures/other/leaves.jpg');
14+
15+
@Component({
16+
standalone: true,
17+
template: `
18+
<ngts-orbit-controls
19+
[makeDefault]="true"
20+
[autoRotate]="true"
21+
[autoRotateSpeed]="0.5"
22+
[minDistance]="2"
23+
[maxDistance]="10"
24+
/>
25+
<ngts-perspective-camera [near]="0.01" [far]="50" [position]="[1, 3, 1]" [makeDefault]="true" [fov]="60" />
26+
<!-- <ngts-environment preset="sunset" /> -->
27+
28+
<ngt-hemisphere-light *args="['#ffffbb', '#080820', 1]" />
29+
30+
<ngt-mesh *ngIf="textures() as textures" [rotation]="[-Math.PI / 2, 0, 0]" [receiveShadow]="true">
31+
<ngt-circle-geometry *args="[5, 64, 64]" />
32+
<ngt-mesh-standard-material
33+
[map]="textures.diffuse"
34+
[normalMap]="textures.normal"
35+
[roughnessMap]="textures.roughness"
36+
[aoMap]="textures.ao"
37+
[envMapIntensity]="0.2"
38+
/>
39+
</ngt-mesh>
40+
41+
<ngts-spot-light
42+
[distance]="20"
43+
[intensity]="5"
44+
[angle]="MathUtils.degToRad(45)"
45+
[color]="'#fadcb9'"
46+
[position]="[5, 7, -2]"
47+
[volumetric]="false"
48+
[debug]="debug"
49+
>
50+
<ngts-spot-light-shadow
51+
[scale]="4"
52+
[distance]="0.4"
53+
[width]="2048"
54+
[height]="2048"
55+
[map]="leaf()"
56+
[shader]="wind ? shader : null"
57+
/>
58+
</ngts-spot-light>
59+
`,
60+
imports: [
61+
NgtsSpotLight,
62+
NgtsSpotLightShadow,
63+
NgIf,
64+
NgtArgs,
65+
NgtsOrbitControls,
66+
NgtsPerspectiveCamera,
67+
// NgtsEnvironment,
68+
],
69+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
70+
})
71+
class ShadowsSpotLightStory {
72+
@Input() debug = false;
73+
@Input() wind = false;
74+
75+
readonly Math = Math;
76+
readonly MathUtils = THREE.MathUtils;
77+
78+
readonly shader = /* glsl */ `
79+
varying vec2 vUv;
80+
uniform sampler2D uShadowMap;
81+
uniform float uTime;
82+
void main() {
83+
// material.repeat.set(2.5) - Since repeat is a shader feature not texture
84+
// we need to implement it manually
85+
vec2 uv = mod(vUv, 0.4) * 2.5;
86+
// Fake wind distortion
87+
uv.x += sin(uv.y * 10.0 + uTime * 0.5) * 0.02;
88+
uv.y += sin(uv.x * 10.0 + uTime * 0.5) * 0.02;
89+
vec3 color = texture2D(uShadowMap, uv).xyz;
90+
gl_FragColor = vec4(color, 1.);
91+
}
92+
`;
93+
94+
readonly textures = injectNgtsTextureLoader(() => ({
95+
diffuse: '/soba/textures/grassy_cobble/grassy_cobblestone_diff_2k.jpg',
96+
normal: '/soba/textures/grassy_cobble/grassy_cobblestone_nor_gl_2k.jpg',
97+
roughness: '/soba/textures/grassy_cobble/grassy_cobblestone_rough_2k.jpg',
98+
ao: '/soba/textures/grassy_cobble/grassy_cobblestone_ao_2k.jpg',
99+
}));
100+
101+
readonly leaf = injectNgtsTextureLoader(() => '/soba/textures/other/leaves.jpg');
102+
103+
constructor() {
104+
effect(() => {
105+
const textures = this.textures();
106+
if (!textures) return;
107+
for (const texture of Object.values(textures)) {
108+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
109+
texture.repeat.set(2, 2);
110+
checkUpdate(texture);
111+
}
112+
});
113+
}
114+
}
115+
116+
@Component({
117+
standalone: true,
118+
template: `
119+
<ng-container *ngIf="depthBuffer.nativeElement">
120+
<ngts-spot-light
121+
[penumbra]="0.5"
122+
[depthBuffer]="depthBuffer.nativeElement"
123+
[position]="[3, 2, 0]"
124+
[intensity]="0.5"
125+
[angle]="0.5"
126+
[color]="'#ff005b'"
127+
[castShadow]="true"
128+
[debug]="debug"
129+
[volumetric]="volumetric"
130+
/>
131+
132+
<ngts-spot-light
133+
[penumbra]="0.5"
134+
[depthBuffer]="depthBuffer.nativeElement"
135+
[position]="[-3, 2, 0]"
136+
[intensity]="0.5"
137+
[angle]="0.5"
138+
[color]="'#0eec82'"
139+
[castShadow]="true"
140+
[debug]="debug"
141+
[volumetric]="volumetric"
142+
/>
143+
</ng-container>
144+
145+
<ngt-mesh [position]="[0, 0.5, 0]" [castShadow]="true">
146+
<ngt-box-geometry />
147+
<ngt-mesh-phong-material />
148+
</ngt-mesh>
149+
150+
<ngt-mesh [receiveShadow]="true" [rotation]="[-Math.PI / 2, 0, 0]">
151+
<ngt-plane-geometry *args="[100, 100]" />
152+
<ngt-mesh-phong-material />
153+
</ngt-mesh>
154+
`,
155+
imports: [NgtsSpotLight, NgIf, NgtArgs],
156+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
157+
})
158+
class DefaultSpotLightStory {
159+
readonly Math = Math;
160+
161+
readonly #size = signal(256);
162+
@Input() set size(size: number) {
163+
this.#size.set(size);
164+
}
165+
166+
@Input() debug = false;
167+
@Input() volumetric = true;
168+
169+
depthBuffer = injectNgtsDepthBuffer(() => ({ size: this.#size() }));
170+
}
171+
172+
export default {
173+
title: 'Staging/SpotLight',
174+
decorators: [moduleMetadata({ imports: [StorybookSetup] })],
175+
} as Meta;
176+
177+
export const Default = makeStoryObject(DefaultSpotLightStory, {
178+
canvasOptions: { lights: false },
179+
argsOptions: { size: number(256), debug: false, volumetric: true },
180+
});
181+
182+
// TODO: shadows doesn't seem to work due to some racing condition with map
183+
export const Shadows = makeStoryObject(ShadowsSpotLightStory, {
184+
canvasOptions: { lights: false },
185+
argsOptions: { wind: true, debug: false },
186+
});

libs/soba/staging/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ export * from './contact-shadows/contact-shadows';
44
export * from './float/float';
55
export * from './sky/sky';
66
export * from './sparkles/sparkles';
7+
export * from './spot-light/spot-light';
8+
export { NgtsSpotLightShadow } from './spot-light/spot-light-shadow-mesh';
79
export * from './stars/stars';
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Directive, Input } from '@angular/core';
2+
import { NgtSignalStore } from 'angular-three';
3+
4+
export interface NgtsSpotLightInputState {
5+
depthBuffer?: THREE.DepthTexture;
6+
angle: number;
7+
distance: number;
8+
attenuation: number;
9+
anglePower: number;
10+
radiusTop: number;
11+
radiusBottom: number;
12+
opacity: number;
13+
color: string | number;
14+
debug: boolean;
15+
}
16+
17+
@Directive()
18+
export abstract class NgtsSpotLightInput extends NgtSignalStore<NgtsSpotLightInputState> {
19+
@Input() set depthBuffer(depthBuffer: THREE.DepthTexture) {
20+
this.set({ depthBuffer });
21+
}
22+
23+
@Input() set angle(angle: number) {
24+
this.set({ angle });
25+
}
26+
27+
@Input() set distance(distance: number) {
28+
this.set({ distance });
29+
}
30+
31+
@Input() set attenuation(attenuation: number) {
32+
this.set({ attenuation });
33+
}
34+
35+
@Input() set anglePower(anglePower: number) {
36+
this.set({ anglePower });
37+
}
38+
39+
@Input() set radiusTop(radiusTop: number) {
40+
this.set({ radiusTop });
41+
}
42+
43+
@Input() set radiusBottom(radiusBottom: number) {
44+
this.set({ radiusBottom });
45+
}
46+
47+
@Input() set opacity(opacity: number) {
48+
this.set({ opacity });
49+
}
50+
51+
@Input() set color(color: string | number) {
52+
this.set({ color });
53+
}
54+
55+
@Input() set debug(debug: boolean) {
56+
this.set({ debug });
57+
}
58+
59+
readonly lightDebug = this.select('debug');
60+
readonly lightColor = this.select('color');
61+
readonly lightOpacity = this.select('opacity');
62+
readonly lightRadiusBottom = this.select('radiusBottom');
63+
readonly lightRadiusTop = this.select('radiusTop');
64+
readonly lightAnglePower = this.select('anglePower');
65+
readonly lightAttenuation = this.select('attenuation');
66+
readonly lightDistance = this.select('distance');
67+
readonly lightAngle = this.select('angle');
68+
readonly lightDepthBuffer = this.select('depthBuffer');
69+
}

0 commit comments

Comments
 (0)