Skip to content

Commit 2884002

Browse files
committed
contact shadows
1 parent 7452ffe commit 2884002

File tree

5 files changed

+331
-19
lines changed

5 files changed

+331
-19
lines changed

libs/soba/src/setup-canvas.ts

+19-18
Original file line numberDiff line numberDiff line change
@@ -141,43 +141,44 @@ export class StorybookSetup implements OnInit {
141141
@Input() options: CanvasOptions = defaultCanvasOptions;
142142
@Input() story!: Type<unknown>;
143143

144-
readonly #inputs = signal<Record<string, unknown>>({});
145-
@Input() set inputs(inputs: Record<string, unknown>) {
146-
this.#inputs.set(inputs);
144+
inputs = signal<Record<string, unknown>>({});
145+
@Input({ alias: 'inputs' }) set _inputs(inputs: Record<string, unknown>) {
146+
this.inputs.set(inputs);
147147
}
148148

149149
@ViewChild('anchor', { read: ViewContainerRef, static: true })
150150
anchor!: ViewContainerRef;
151151

152-
readonly #envInjector = inject(EnvironmentInjector);
152+
private envInjector = inject(EnvironmentInjector);
153153

154-
#ref?: ComponentRef<unknown>;
155-
#refEnvInjector?: EnvironmentInjector;
154+
private ref?: ComponentRef<unknown>;
155+
private refEnvInjector?: EnvironmentInjector;
156156

157157
constructor() {
158158
inject(DestroyRef).onDestroy(() => {
159-
this.#ref?.destroy();
160-
this.#refEnvInjector?.destroy();
159+
this.ref?.destroy();
160+
this.refEnvInjector?.destroy();
161161
});
162162
}
163163

164164
ngOnInit() {
165-
this.#refEnvInjector = createEnvironmentInjector(
165+
this.refEnvInjector = createEnvironmentInjector(
166166
[
167167
{ provide: CANVAS_OPTIONS, useValue: this.options },
168168
{ provide: STORY_COMPONENT, useValue: this.story },
169169
{ provide: STORY_COMPONENT_MIRROR, useValue: reflectComponentType(this.story) },
170-
{ provide: STORY_INPUTS, useValue: this.#inputs },
170+
{ provide: STORY_INPUTS, useValue: this.inputs },
171171
],
172-
this.#envInjector,
172+
this.envInjector,
173173
);
174-
this.#ref = this.anchor.createComponent(NgtCanvas, { environmentInjector: this.#refEnvInjector });
175-
this.#ref.setInput('shadows', true);
176-
this.#ref.setInput('performance', this.options.performance);
177-
this.#ref.setInput('camera', this.options.camera);
178-
this.#ref.setInput('compoundPrefixes', this.options.compoundPrefixes || []);
179-
this.#ref.setInput('sceneGraph', StorybookScene);
180-
safeDetectChanges(this.#ref.changeDetectorRef);
174+
this.ref = this.anchor.createComponent(NgtCanvas, { environmentInjector: this.refEnvInjector });
175+
this.ref.setInput('shadows', true);
176+
this.ref.setInput('performance', this.options.performance);
177+
this.ref.setInput('camera', this.options.camera);
178+
// this.ref.setInput('gl', { useLegacyLights: true });
179+
this.ref.setInput('compoundPrefixes', this.options.compoundPrefixes || []);
180+
this.ref.setInput('sceneGraph', StorybookScene);
181+
safeDetectChanges(this.ref.changeDetectorRef);
181182
}
182183
}
183184

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core';
2+
import { Meta } from '@storybook/angular';
3+
import { injectNgtStore, NgtArgs, type NgtBeforeRenderEvent } from 'angular-three';
4+
import { NgtsContactShadows } from 'angular-three-soba/staging';
5+
import * as THREE from 'three';
6+
import { makeDecorators, makeStoryFunction, makeStoryObject } from '../setup-canvas';
7+
8+
@Component({
9+
standalone: true,
10+
template: `
11+
<ngt-mesh [position]="[0, 2, 0]" (beforeRender)="onBeforeRender($event)">
12+
<ngt-sphere-geometry *args="[1, 32, 32]" />
13+
<ngt-mesh-toon-material #material color="#2a8aff" />
14+
</ngt-mesh>
15+
<ngts-contact-shadows
16+
[position]="[0, 0, 0]"
17+
[scale]="10"
18+
[far]="3"
19+
[blur]="3"
20+
[rotation]="[Math.PI / 2, 0, 0]"
21+
[color]="colorized ? material.color : 'black'"
22+
/>
23+
<ngt-mesh [position]="[0, -0.01, 0]" [rotation]="[-Math.PI / 2, 0, 0]">
24+
<ngt-plane-geometry *args="[10, 10]" />
25+
<ngt-mesh-toon-material />
26+
</ngt-mesh>
27+
`,
28+
imports: [NgtsContactShadows, NgtArgs],
29+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
30+
})
31+
class DefaultContactShadowsStory {
32+
@Input() colorized = false;
33+
readonly Math = Math;
34+
35+
store = injectNgtStore();
36+
37+
ngOnInit() {
38+
console.log(this.store, THREE);
39+
}
40+
41+
onBeforeRender({ state: { clock }, object: mesh }: NgtBeforeRenderEvent<THREE.Mesh>) {
42+
mesh.position.y = Math.sin(clock.getElapsedTime()) + 2;
43+
}
44+
}
45+
46+
export default {
47+
title: 'Staging/Contact Shadows',
48+
decorators: makeDecorators(),
49+
} as Meta;
50+
51+
export const Default = makeStoryFunction(DefaultContactShadowsStory);
52+
export const Colorized = makeStoryObject(DefaultContactShadowsStory, {
53+
argsOptions: { colorized: true },
54+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core';
2+
import {
3+
extend,
4+
injectBeforeRender,
5+
injectNgtRef,
6+
injectNgtStore,
7+
NgtArgs,
8+
signalStore,
9+
type NgtGroup,
10+
type NgtRenderState,
11+
} from 'angular-three';
12+
import * as THREE from 'three';
13+
import { Group, Mesh, MeshBasicMaterial, OrthographicCamera } from 'three';
14+
import { HorizontalBlurShader, VerticalBlurShader } from 'three-stdlib';
15+
16+
extend({ Group, Mesh, MeshBasicMaterial, OrthographicCamera });
17+
18+
export type NgtsContactShadowsState = {
19+
opacity: number;
20+
width: number;
21+
height: number;
22+
blur: number;
23+
far: number;
24+
smooth: boolean;
25+
resolution: number;
26+
frames: number;
27+
scale: number | [x: number, y: number];
28+
color: THREE.ColorRepresentation;
29+
depthWrite: boolean;
30+
renderOrder: number;
31+
};
32+
33+
declare global {
34+
interface HTMLElementTagNameMap {
35+
/**
36+
* @extends ngt-group
37+
*/
38+
'ngts-contact-shadows': NgtsContactShadowsState & NgtGroup;
39+
}
40+
}
41+
42+
@Component({
43+
selector: 'ngts-contact-shadows',
44+
standalone: true,
45+
template: `
46+
<ngt-group ngtCompound [ref]="contactShadowsRef" [rotation]="[Math.PI / 2, 0, 0]">
47+
<ngt-mesh
48+
[renderOrder]="renderOrder() ?? 0"
49+
[geometry]="contactShadows().planeGeometry"
50+
[scale]="[1, -1, 1]"
51+
[rotation]="[-Math.PI / 2, 0, 0]"
52+
>
53+
<ngt-mesh-basic-material
54+
[map]="contactShadows().renderTarget.texture"
55+
[transparent]="true"
56+
[opacity]="opacity() ?? 1"
57+
[depthWrite]="depthWrite() ?? false"
58+
/>
59+
</ngt-mesh>
60+
<ngt-orthographic-camera *args="cameraArgs()" [ref]="shadowCameraRef" />
61+
</ngt-group>
62+
`,
63+
imports: [NgtArgs],
64+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
65+
})
66+
export class NgtsContactShadows {
67+
private inputs = signalStore<NgtsContactShadowsState>({
68+
scale: 10,
69+
frames: Infinity,
70+
opacity: 1,
71+
width: 1,
72+
height: 1,
73+
blur: 1,
74+
far: 10,
75+
resolution: 512,
76+
smooth: true,
77+
color: '#000000',
78+
depthWrite: false,
79+
renderOrder: 0,
80+
});
81+
82+
@Input() contactShadowsRef = injectNgtRef<Group>();
83+
84+
@Input({ alias: 'opacity' }) set _opacity(opacity: number) {
85+
this.inputs.set({ opacity });
86+
}
87+
88+
@Input({ alias: 'width' }) set _width(width: number) {
89+
this.inputs.set({ width });
90+
}
91+
92+
@Input({ alias: 'height' }) set _height(height: number) {
93+
this.inputs.set({ height });
94+
}
95+
96+
@Input({ alias: 'blur' }) set _blur(blur: number) {
97+
this.inputs.set({ blur });
98+
}
99+
100+
@Input({ alias: 'far' }) set _far(far: number) {
101+
this.inputs.set({ far });
102+
}
103+
104+
@Input({ alias: 'smooth' }) set _smooth(smooth: boolean) {
105+
this.inputs.set({ smooth });
106+
}
107+
108+
@Input({ alias: 'resolution' }) set _resolution(resolution: number) {
109+
this.inputs.set({ resolution });
110+
}
111+
112+
@Input({ alias: 'frames' }) set _frames(frames: number) {
113+
this.inputs.set({ frames });
114+
}
115+
116+
@Input({ alias: 'scale' }) set _scale(scale: number | [x: number, y: number]) {
117+
this.inputs.set({ scale });
118+
}
119+
120+
@Input({ alias: 'color' }) set _color(color: THREE.ColorRepresentation) {
121+
this.inputs.set({ color });
122+
}
123+
124+
@Input({ alias: 'depthWrite' }) set _depthWrite(depthWrite: boolean) {
125+
this.inputs.set({ depthWrite });
126+
}
127+
128+
@Input({ alias: 'renderOrder' }) set _renderOrder(renderOrder: number) {
129+
this.inputs.set({ renderOrder });
130+
}
131+
132+
Math = Math;
133+
134+
private store = injectNgtStore();
135+
136+
shadowCameraRef = injectNgtRef<OrthographicCamera>();
137+
138+
private scale = this.inputs.select('scale');
139+
private width = this.inputs.select('width');
140+
private height = this.inputs.select('height');
141+
private far = this.inputs.select('far');
142+
private resolution = this.inputs.select('resolution');
143+
private color = this.inputs.select('color');
144+
145+
private scaledWidth = computed(() => {
146+
const scale = this.scale();
147+
return this.width() * (Array.isArray(scale) ? scale[0] : scale || 1);
148+
});
149+
private scaledHeight = computed(() => {
150+
const scale = this.scale();
151+
return this.height() * (Array.isArray(scale) ? scale[1] : scale || 1);
152+
});
153+
154+
renderOrder = this.inputs.select('renderOrder');
155+
opacity = this.inputs.select('opacity');
156+
depthWrite = this.inputs.select('depthWrite');
157+
158+
cameraArgs = computed(() => {
159+
const width = this.scaledWidth();
160+
const height = this.scaledHeight();
161+
return [-width / 2, width / 2, height / 2, -height / 2, 0, this.far()];
162+
});
163+
contactShadows = computed(() => {
164+
const color = this.color();
165+
const resolution = this.resolution();
166+
const renderTarget = new THREE.WebGLRenderTarget(resolution, resolution);
167+
const renderTargetBlur = new THREE.WebGLRenderTarget(resolution, resolution);
168+
renderTargetBlur.texture.generateMipmaps = renderTarget.texture.generateMipmaps = false;
169+
const planeGeometry = new THREE.PlaneGeometry(this.scaledWidth(), this.scaledHeight()).rotateX(Math.PI / 2);
170+
const blurPlane = new Mesh(planeGeometry);
171+
const depthMaterial = new THREE.MeshDepthMaterial();
172+
depthMaterial.depthTest = depthMaterial.depthWrite = false;
173+
depthMaterial.onBeforeCompile = (shader) => {
174+
shader.uniforms = {
175+
...shader.uniforms,
176+
ucolor: { value: new THREE.Color(color) },
177+
};
178+
shader.fragmentShader = shader.fragmentShader.replace(
179+
`void main() {`, //
180+
`uniform vec3 ucolor;
181+
void main() {
182+
`,
183+
);
184+
shader.fragmentShader = shader.fragmentShader.replace(
185+
'vec4( vec3( 1.0 - fragCoordZ ), opacity );',
186+
// Colorize the shadow, multiply by the falloff so that the center can remain darker
187+
'vec4( ucolor * fragCoordZ * 2.0, ( 1.0 - fragCoordZ ) * 1.0 );',
188+
);
189+
};
190+
191+
const horizontalBlurMaterial = new THREE.ShaderMaterial(HorizontalBlurShader);
192+
const verticalBlurMaterial = new THREE.ShaderMaterial(VerticalBlurShader);
193+
verticalBlurMaterial.depthTest = horizontalBlurMaterial.depthTest = false;
194+
195+
return {
196+
renderTarget,
197+
planeGeometry,
198+
depthMaterial,
199+
blurPlane,
200+
horizontalBlurMaterial,
201+
verticalBlurMaterial,
202+
renderTargetBlur,
203+
};
204+
});
205+
206+
constructor() {
207+
injectBeforeRender(this.beforeRender.bind(this, 0));
208+
}
209+
210+
private beforeRender(count: number, { scene, gl }: NgtRenderState) {
211+
const { frames = Infinity, blur = 1, smooth = true } = this.inputs.get();
212+
const { depthMaterial, renderTarget } = this.contactShadows();
213+
const shadowCamera = this.shadowCameraRef.nativeElement;
214+
if (shadowCamera && (frames === Infinity || count < frames)) {
215+
const initialBackground = scene.background;
216+
scene.background = null;
217+
const initialOverrideMaterial = scene.overrideMaterial;
218+
scene.overrideMaterial = depthMaterial;
219+
gl.setRenderTarget(renderTarget);
220+
gl.render(scene, shadowCamera);
221+
scene.overrideMaterial = initialOverrideMaterial;
222+
223+
this.blurShadows(blur);
224+
if (smooth) this.blurShadows(blur * 0.4);
225+
226+
gl.setRenderTarget(null);
227+
scene.background = initialBackground;
228+
count++;
229+
}
230+
}
231+
232+
private blurShadows(blur: number) {
233+
const { blurPlane, horizontalBlurMaterial, verticalBlurMaterial, renderTargetBlur, renderTarget } =
234+
this.contactShadows();
235+
const gl = this.store.get('gl');
236+
const shadowCamera = this.shadowCameraRef.nativeElement;
237+
238+
blurPlane.visible = true;
239+
240+
blurPlane.material = horizontalBlurMaterial;
241+
horizontalBlurMaterial.uniforms['tDiffuse'].value = renderTarget.texture;
242+
horizontalBlurMaterial.uniforms['h'].value = (blur * 1) / 256;
243+
244+
gl.setRenderTarget(renderTargetBlur);
245+
gl.render(blurPlane, shadowCamera);
246+
247+
blurPlane.material = verticalBlurMaterial;
248+
verticalBlurMaterial.uniforms['tDiffuse'].value = renderTargetBlur.texture;
249+
verticalBlurMaterial.uniforms['v'].value = (blur * 1) / 256;
250+
251+
gl.setRenderTarget(renderTarget);
252+
gl.render(blurPlane, shadowCamera);
253+
254+
blurPlane.visible = false;
255+
}
256+
}

libs/soba/staging/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './camera-shake/camera-shake';
22
export * from './center/center';
33
export * from './cloud/cloud';
4+
export * from './contact-shadows/contact-shadows';
45
export * from './float/float';
56
export * from './matcap-texture/matcap-texture';

tools/scripts/generate-soba-json.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const entryPoints = {
3030
controls: ['orbit-controls'],
3131
abstractions: ['billboard', 'text', 'grid', 'text-3d'],
3232
cameras: ['perspective-camera', 'orthographic-camera', 'cube-camera'],
33-
staging: ['center', 'float', 'camera-shake', 'cloud'],
33+
staging: ['center', 'float', 'camera-shake', 'cloud', 'contact-shadows'],
3434
};
3535

3636
const paths = [];

0 commit comments

Comments
 (0)