Skip to content

Commit 199a722

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
feat(soba): migrate reflection
1 parent aee4943 commit 199a722

File tree

7 files changed

+959
-0
lines changed

7 files changed

+959
-0
lines changed

libs/soba/materials/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './mesh-distort-material/mesh-distort-material';
2+
export * from './mesh-reflector-material/mesh-reflector-material';
23
export * from './mesh-wobble-material/mesh-wobble-material';
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
import { NgIf } from '@angular/common';
2+
import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, inject, Input } from '@angular/core';
3+
import {
4+
extend,
5+
getLocalState,
6+
injectBeforeRender,
7+
injectNgtRef,
8+
NgtArgs,
9+
NgtRenderState,
10+
NgtSignalStore,
11+
NgtStore,
12+
} from 'angular-three';
13+
import { BlurPass, MeshReflectorMaterial } from 'angular-three-soba/shaders';
14+
import * as THREE from 'three';
15+
16+
extend({ MeshReflectorMaterial });
17+
18+
export interface NgtsMeshReflectorMaterialState {
19+
resolution: number;
20+
mixBlur: number;
21+
mixStrength: number;
22+
blur: [number, number] | number;
23+
mirror: number;
24+
minDepthThreshold: number;
25+
maxDepthThreshold: number;
26+
depthScale: number;
27+
depthToBlurRatioBias: number;
28+
distortionMap?: THREE.Texture;
29+
distortion: number;
30+
mixContrast: number;
31+
reflectorOffset: number;
32+
}
33+
34+
@Component({
35+
selector: 'ngts-mesh-reflector-material',
36+
standalone: true,
37+
template: `
38+
<ngt-mesh-reflector-material
39+
ngtCompound
40+
attach="material"
41+
*ngIf="defines()"
42+
[ref]="materialRef"
43+
[defines]="defines()"
44+
[mirror]="reflectorMirror()"
45+
[textureMatrix]="reflectorTextureMatrix()"
46+
[mixBlur]="reflectorMixBlur()"
47+
[tDiffuse]="reflectorTDiffuse()"
48+
[tDepth]="reflectorTDepth()"
49+
[tDiffuseBlur]="reflectorTDiffuseBlur()"
50+
[hasBlur]="reflectorHasBlur()"
51+
[mixStrength]="reflectorMixStrength()"
52+
[minDepthThreshold]="reflectorMinDepthThreshold()"
53+
[maxDepthThreshold]="reflectorMaxDepthThreshold()"
54+
[depthScale]="reflectorDepthScale()"
55+
[depthToBlurRatioBias]="reflectorDepthToBlurRatioBias()"
56+
[distortion]="reflectorDistortion()"
57+
[distortionMap]="reflectorDistortionMap()"
58+
[mixContrast]="reflectorMixContrast()"
59+
>
60+
<ng-content />
61+
</ngt-mesh-reflector-material>
62+
`,
63+
imports: [NgtArgs, NgIf],
64+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
65+
})
66+
export class NgtsMeshReflectorMaterial extends NgtSignalStore<NgtsMeshReflectorMaterialState> {
67+
@Input() materialRef = injectNgtRef<MeshReflectorMaterial>();
68+
69+
@Input() set resolution(resolution: number) {
70+
this.set({ resolution });
71+
}
72+
73+
@Input() set mixBlur(mixBlur: number) {
74+
this.set({ mixBlur });
75+
}
76+
77+
@Input() set mixStrength(mixStrength: number) {
78+
this.set({ mixStrength });
79+
}
80+
81+
@Input() set blur(blur: [number, number] | number) {
82+
this.set({ blur });
83+
}
84+
85+
@Input() set mirror(mirror: number) {
86+
this.set({ mirror });
87+
}
88+
89+
@Input() set minDepthThreshold(minDepthThreshold: number) {
90+
this.set({ minDepthThreshold });
91+
}
92+
93+
@Input() set maxDepthThreshold(maxDepthThreshold: number) {
94+
this.set({ maxDepthThreshold });
95+
}
96+
97+
@Input() set depthScale(depthScale: number) {
98+
this.set({ depthScale });
99+
}
100+
101+
@Input() set depthToBlurRatioBias(depthToBlurRatioBias: number) {
102+
this.set({ depthToBlurRatioBias });
103+
}
104+
105+
@Input() set distortionMap(distortionMap: THREE.Texture) {
106+
this.set({ distortionMap });
107+
}
108+
109+
@Input() set distortion(distortion: number) {
110+
this.set({ distortion });
111+
}
112+
113+
@Input() set mixContrast(mixContrast: number) {
114+
this.set({ mixContrast });
115+
}
116+
117+
@Input() set reflectorOffset(reflectorOffset: number) {
118+
this.set({ reflectorOffset });
119+
}
120+
121+
readonly reflectorProps = computed(() => this.#reflectorEntities().reflectorProps);
122+
readonly defines = computed(() => this.reflectorProps().defines);
123+
readonly reflectorMirror = computed(() => this.reflectorProps().mirror);
124+
readonly reflectorTextureMatrix = computed(() => this.reflectorProps().textureMatrix);
125+
readonly reflectorMixBlur = computed(() => this.reflectorProps().mixBlur);
126+
readonly reflectorTDiffuse = computed(() => this.reflectorProps().tDiffuse);
127+
readonly reflectorTDepth = computed(() => this.reflectorProps().tDepth);
128+
readonly reflectorTDiffuseBlur = computed(() => this.reflectorProps().tDiffuseBlur);
129+
readonly reflectorHasBlur = computed(() => this.reflectorProps().hasBlur);
130+
readonly reflectorMixStrength = computed(() => this.reflectorProps().mixStrength);
131+
readonly reflectorMinDepthThreshold = computed(() => this.reflectorProps().minDepthThreshold);
132+
readonly reflectorMaxDepthThreshold = computed(() => this.reflectorProps().maxDepthThreshold);
133+
readonly reflectorDepthScale = computed(() => this.reflectorProps().depthScale);
134+
readonly reflectorDepthToBlurRatioBias = computed(() => this.reflectorProps().depthToBlurRatioBias);
135+
readonly reflectorDistortion = computed(() => this.reflectorProps().distortion);
136+
readonly reflectorDistortionMap = computed(() => this.reflectorProps().distortionMap);
137+
readonly reflectorMixContrast = computed(() => this.reflectorProps().mixContrast);
138+
139+
readonly #store = inject(NgtStore);
140+
readonly #gl = this.#store.select('gl');
141+
142+
readonly #reflectorPlane = new THREE.Plane();
143+
readonly #normal = new THREE.Vector3();
144+
readonly #reflectorWorldPosition = new THREE.Vector3();
145+
readonly #cameraWorldPosition = new THREE.Vector3();
146+
readonly #rotationMatrix = new THREE.Matrix4();
147+
readonly #lookAtPosition = new THREE.Vector3(0, 0, -1);
148+
readonly #clipPlane = new THREE.Vector4();
149+
readonly #view = new THREE.Vector3();
150+
readonly #target = new THREE.Vector3();
151+
readonly #q = new THREE.Vector4();
152+
readonly #textureMatrix = new THREE.Matrix4();
153+
readonly #virtualCamera = new THREE.PerspectiveCamera();
154+
155+
readonly #blur = this.select('blur');
156+
readonly #resolution = this.select('resolution');
157+
readonly #mirror = this.select('mirror');
158+
readonly #mixBlur = this.select('mixBlur');
159+
readonly #mixStrength = this.select('mixStrength');
160+
readonly #minDepthThreshold = this.select('minDepthThreshold');
161+
readonly #maxDepthThreshold = this.select('maxDepthThreshold');
162+
readonly #depthScale = this.select('depthScale');
163+
readonly #depthToBlurRatioBias = this.select('depthToBlurRatioBias');
164+
readonly #distortion = this.select('distortion');
165+
readonly #distortionMap = this.select('distortionMap');
166+
readonly #mixContrast = this.select('mixContrast');
167+
168+
readonly #normalizedBlur = computed(() => {
169+
const blur = this.#blur();
170+
return Array.isArray(blur) ? blur : [blur, blur];
171+
});
172+
173+
readonly #hasBlur = computed(() => {
174+
const [x, y] = this.#normalizedBlur();
175+
return x + y > 0;
176+
});
177+
178+
readonly #reflectorEntities = computed(() => {
179+
const gl = this.#gl();
180+
const resolution = this.#resolution();
181+
const blur = this.#normalizedBlur();
182+
const minDepthThreshold = this.#minDepthThreshold();
183+
const maxDepthThreshold = this.#maxDepthThreshold();
184+
const depthScale = this.#depthScale();
185+
const depthToBlurRatioBias = this.#depthToBlurRatioBias();
186+
const mirror = this.#mirror();
187+
const mixBlur = this.#mixBlur();
188+
const mixStrength = this.#mixStrength();
189+
const mixContrast = this.#mixContrast();
190+
const distortion = this.#distortion();
191+
const distortionMap = this.#distortionMap();
192+
const hasBlur = this.#hasBlur();
193+
194+
const parameters = {
195+
minFilter: THREE.LinearFilter,
196+
magFilter: THREE.LinearFilter,
197+
encoding: gl.outputEncoding,
198+
type: THREE.HalfFloatType,
199+
};
200+
const fbo1 = new THREE.WebGLRenderTarget(resolution, resolution, parameters);
201+
fbo1.depthBuffer = true;
202+
fbo1.depthTexture = new THREE.DepthTexture(resolution, resolution);
203+
fbo1.depthTexture.format = THREE.DepthFormat;
204+
fbo1.depthTexture.type = THREE.UnsignedShortType;
205+
206+
const fbo2 = new THREE.WebGLRenderTarget(resolution, resolution, parameters);
207+
const blurPass = new BlurPass({
208+
gl,
209+
resolution,
210+
width: blur[0],
211+
height: blur[1],
212+
minDepthThreshold,
213+
maxDepthThreshold,
214+
depthScale,
215+
depthToBlurRatioBias,
216+
});
217+
const reflectorProps = {
218+
mirror,
219+
textureMatrix: this.#textureMatrix,
220+
mixBlur,
221+
tDiffuse: fbo1.texture,
222+
tDepth: fbo1.depthTexture,
223+
tDiffuseBlur: fbo2.texture,
224+
hasBlur,
225+
mixStrength,
226+
minDepthThreshold,
227+
maxDepthThreshold,
228+
depthScale,
229+
depthToBlurRatioBias,
230+
distortion,
231+
distortionMap,
232+
mixContrast,
233+
defines: {
234+
USE_BLUR: hasBlur ? '' : undefined,
235+
USE_DEPTH: depthScale > 0 ? '' : undefined,
236+
USE_DISTORTION: distortionMap ? '' : undefined,
237+
},
238+
};
239+
240+
return { fbo1, fbo2, blurPass, reflectorProps };
241+
});
242+
243+
constructor() {
244+
super({
245+
mixBlur: 0,
246+
mixStrength: 1,
247+
resolution: 256,
248+
blur: [0, 0],
249+
minDepthThreshold: 0.9,
250+
maxDepthThreshold: 1,
251+
depthScale: 0,
252+
depthToBlurRatioBias: 0.25,
253+
mirror: 0,
254+
distortion: 1,
255+
mixContrast: 1,
256+
reflectorOffset: 0,
257+
});
258+
259+
injectBeforeRender(this.#onBeforeRender.bind(this));
260+
}
261+
262+
#onBeforeRender(state: NgtRenderState) {
263+
if (!this.materialRef.nativeElement) return;
264+
const parent = getLocalState(this.materialRef.nativeElement).parent();
265+
if (!parent) return;
266+
267+
const { gl, scene } = state;
268+
const hasBlur = this.#hasBlur();
269+
const { fbo1, fbo2, blurPass } = this.#reflectorEntities();
270+
271+
if (fbo1 && fbo2 && blurPass) {
272+
parent.visible = false;
273+
const currentXrEnabled = gl.xr.enabled;
274+
const currentShadowAutoUpdate = gl.shadowMap.autoUpdate;
275+
this.#beforeRender(state);
276+
gl.xr.enabled = false;
277+
gl.shadowMap.autoUpdate = false;
278+
gl.setRenderTarget(fbo1);
279+
gl.state.buffers.depth.setMask(true);
280+
if (!gl.autoClear) gl.clear();
281+
gl.render(scene, this.#virtualCamera);
282+
if (hasBlur) blurPass.render(gl, fbo1, fbo2);
283+
gl.xr.enabled = currentXrEnabled;
284+
gl.shadowMap.autoUpdate = currentShadowAutoUpdate;
285+
parent.visible = true;
286+
gl.setRenderTarget(null);
287+
}
288+
}
289+
290+
#beforeRender(state: NgtRenderState) {
291+
const parent = getLocalState(this.materialRef.nativeElement).parent();
292+
if (!parent) return;
293+
294+
const { camera } = state;
295+
296+
this.#reflectorWorldPosition.setFromMatrixPosition(parent.matrixWorld);
297+
this.#cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);
298+
this.#rotationMatrix.extractRotation(parent.matrixWorld);
299+
this.#normal.set(0, 0, 1);
300+
this.#normal.applyMatrix4(this.#rotationMatrix);
301+
this.#reflectorWorldPosition.addScaledVector(this.#normal, this.get('reflectorOffset'));
302+
this.#view.subVectors(this.#reflectorWorldPosition, this.#cameraWorldPosition);
303+
// Avoid rendering when reflector is facing away
304+
if (this.#view.dot(this.#normal) > 0) return;
305+
this.#view.reflect(this.#normal).negate();
306+
this.#view.add(this.#reflectorWorldPosition);
307+
this.#rotationMatrix.extractRotation(camera.matrixWorld);
308+
this.#lookAtPosition.set(0, 0, -1);
309+
this.#lookAtPosition.applyMatrix4(this.#rotationMatrix);
310+
this.#lookAtPosition.add(this.#cameraWorldPosition);
311+
this.#target.subVectors(this.#reflectorWorldPosition, this.#lookAtPosition);
312+
this.#target.reflect(this.#normal).negate();
313+
this.#target.add(this.#reflectorWorldPosition);
314+
this.#virtualCamera.position.copy(this.#view);
315+
this.#virtualCamera.up.set(0, 1, 0);
316+
this.#virtualCamera.up.applyMatrix4(this.#rotationMatrix);
317+
this.#virtualCamera.up.reflect(this.#normal);
318+
this.#virtualCamera.lookAt(this.#target);
319+
this.#virtualCamera.far = camera.far; // Used in WebGLBackground
320+
this.#virtualCamera.updateMatrixWorld();
321+
this.#virtualCamera.projectionMatrix.copy(camera.projectionMatrix);
322+
// Update the texture matrix
323+
this.#textureMatrix.set(0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0);
324+
this.#textureMatrix.multiply(this.#virtualCamera.projectionMatrix);
325+
this.#textureMatrix.multiply(this.#virtualCamera.matrixWorldInverse);
326+
this.#textureMatrix.multiply(parent.matrixWorld);
327+
// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
328+
// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
329+
this.#reflectorPlane.setFromNormalAndCoplanarPoint(this.#normal, this.#reflectorWorldPosition);
330+
this.#reflectorPlane.applyMatrix4(this.#virtualCamera.matrixWorldInverse);
331+
this.#clipPlane.set(
332+
this.#reflectorPlane.normal.x,
333+
this.#reflectorPlane.normal.y,
334+
this.#reflectorPlane.normal.z,
335+
this.#reflectorPlane.constant
336+
);
337+
const projectionMatrix = this.#virtualCamera.projectionMatrix;
338+
this.#q.x = (Math.sign(this.#clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0];
339+
this.#q.y = (Math.sign(this.#clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5];
340+
this.#q.z = -1.0;
341+
this.#q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
342+
// Calculate the scaled plane vector
343+
this.#clipPlane.multiplyScalar(2.0 / this.#clipPlane.dot(this.#q));
344+
// Replacing the third row of the projection matrix
345+
projectionMatrix.elements[2] = this.#clipPlane.x;
346+
projectionMatrix.elements[6] = this.#clipPlane.y;
347+
projectionMatrix.elements[10] = this.#clipPlane.z + 1.0;
348+
projectionMatrix.elements[14] = this.#clipPlane.w;
349+
}
350+
}

0 commit comments

Comments
 (0)