Skip to content

Commit 92a5d50

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
feat(soba): migrate contact shadows
1 parent e943869 commit 92a5d50

File tree

3 files changed

+302
-0
lines changed

3 files changed

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

libs/soba/staging/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './center/center';
22
export * from './cloud/cloud';
3+
export * from './contact-shadows/contact-shadows';
34
export * from './float/float';

0 commit comments

Comments
 (0)