|
| 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