|
| 1 | +import { |
| 2 | + CUSTOM_ELEMENTS_SCHEMA, |
| 3 | + ChangeDetectionStrategy, |
| 4 | + Component, |
| 5 | + DestroyRef, |
| 6 | + ElementRef, |
| 7 | + computed, |
| 8 | + effect, |
| 9 | + inject, |
| 10 | + input, |
| 11 | + viewChild, |
| 12 | +} from '@angular/core'; |
| 13 | +import { NgtArgs, NgtVector3, beforeRender, injectStore, pick, vector3 } from 'angular-three'; |
| 14 | +import { easing } from 'maath'; |
| 15 | +import { mergeInputs } from 'ngxtension/inject-inputs'; |
| 16 | +import { CopyPass, DepthOfFieldEffect, DepthPickingPass } from 'postprocessing'; |
| 17 | +import * as THREE from 'three'; |
| 18 | +import { NgtpEffectComposer } from '../effect-composer'; |
| 19 | + |
| 20 | +type DOFOptions = NonNullable<ConstructorParameters<typeof DepthOfFieldEffect>[1]>; |
| 21 | + |
| 22 | +export type AutofocusOptions = DOFOptions & { |
| 23 | + target?: NgtVector3; |
| 24 | + mouse?: boolean; |
| 25 | + debug?: number; |
| 26 | + manual?: boolean; |
| 27 | + smoothTime?: number; |
| 28 | +}; |
| 29 | + |
| 30 | +const defaultOptions: AutofocusOptions = { |
| 31 | + mouse: false, |
| 32 | + manual: false, |
| 33 | + smoothTime: 0.25, |
| 34 | +}; |
| 35 | + |
| 36 | +@Component({ |
| 37 | + selector: 'ngtp-autofocus', |
| 38 | + template: ` |
| 39 | + <ngt-primitive *args="[dofEffect()]" [dispose]="null" /> |
| 40 | + @if (debugSize(); as debugSize) { |
| 41 | + <ngt-mesh #hitpointMesh> |
| 42 | + <ngt-sphere-geometry *args="[debugSize, 16, 16]" /> |
| 43 | + <ngt-mesh-basic-material [color]="'#00ff00'" [opacity]="1" [transparent]="true" [depthWrite]="false" /> |
| 44 | + </ngt-mesh> |
| 45 | + <ngt-mesh #targetMesh> |
| 46 | + <ngt-sphere-geometry *args="[debugSize / 2, 16, 16]" /> |
| 47 | + <ngt-mesh-basic-material |
| 48 | + [color]="'#00ff00'" |
| 49 | + [opacity]="0.5" |
| 50 | + [transparent]="true" |
| 51 | + [depthWrite]="false" |
| 52 | + /> |
| 53 | + </ngt-mesh> |
| 54 | + } |
| 55 | + `, |
| 56 | + imports: [NgtArgs], |
| 57 | + schemas: [CUSTOM_ELEMENTS_SCHEMA], |
| 58 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 59 | +}) |
| 60 | +export class NgtpAutofocus { |
| 61 | + options = input(defaultOptions, { transform: mergeInputs(defaultOptions) }); |
| 62 | + |
| 63 | + private effectComposer = inject(NgtpEffectComposer); |
| 64 | + private store = injectStore(); |
| 65 | + |
| 66 | + private hitpoint = new THREE.Vector3(0, 0, 0); |
| 67 | + private ndc = new THREE.Vector3(0, 0, 0); |
| 68 | + |
| 69 | + private depthPickingPass = new DepthPickingPass(); |
| 70 | + private copyPass = new CopyPass(); |
| 71 | + |
| 72 | + debugSize = pick(this.options, 'debug'); |
| 73 | + |
| 74 | + private hitpointMeshRef = viewChild<ElementRef<THREE.Mesh>>('hitpointMesh'); |
| 75 | + private targetMeshRef = viewChild<ElementRef<THREE.Mesh>>('targetMesh'); |
| 76 | + |
| 77 | + private target = vector3(this.options, 'target', true); |
| 78 | + |
| 79 | + dofEffect = computed(() => { |
| 80 | + const [camera, options] = [this.effectComposer.camera(), this.options()]; |
| 81 | + const { target: _, mouse: __, debug: ___, manual: ____, smoothTime: _____, ...dofOptions } = options; |
| 82 | + const dof = new DepthOfFieldEffect(camera, dofOptions); |
| 83 | + dof.target = new THREE.Vector3().copy(this.hitpoint); |
| 84 | + return dof; |
| 85 | + }); |
| 86 | + |
| 87 | + constructor() { |
| 88 | + // add passes to composer |
| 89 | + effect((onCleanup) => { |
| 90 | + const composer = this.effectComposer.effectComposer(); |
| 91 | + if (!composer) return; |
| 92 | + |
| 93 | + composer.addPass(this.depthPickingPass); |
| 94 | + composer.addPass(this.copyPass); |
| 95 | + |
| 96 | + onCleanup(() => { |
| 97 | + composer.removePass(this.depthPickingPass); |
| 98 | + composer.removePass(this.copyPass); |
| 99 | + }); |
| 100 | + }); |
| 101 | + |
| 102 | + inject(DestroyRef).onDestroy(() => { |
| 103 | + this.depthPickingPass.dispose(); |
| 104 | + this.copyPass.dispose(); |
| 105 | + }); |
| 106 | + |
| 107 | + // cleanup dof effect |
| 108 | + effect((onCleanup) => { |
| 109 | + const dof = this.dofEffect(); |
| 110 | + onCleanup(() => dof.dispose()); |
| 111 | + }); |
| 112 | + |
| 113 | + beforeRender(({ delta }) => { |
| 114 | + const dof = this.dofEffect(); |
| 115 | + if (!dof?.target) return; |
| 116 | + |
| 117 | + const { mouse: followMouse, smoothTime, manual } = this.options(); |
| 118 | + if (manual) return; |
| 119 | + |
| 120 | + const target = this.target(); |
| 121 | + const camera = this.effectComposer.camera(); |
| 122 | + |
| 123 | + if (target) { |
| 124 | + this.hitpoint.copy(target); |
| 125 | + } else { |
| 126 | + const { x, y } = followMouse ? this.store.snapshot.pointer : { x: 0, y: 0 }; |
| 127 | + this.ndc.x = x; |
| 128 | + this.ndc.y = y; |
| 129 | + |
| 130 | + this.depthPickingPass.readDepth(this.ndc).then((depth) => { |
| 131 | + this.ndc.z = depth * 2.0 - 1.0; |
| 132 | + const hit = 1 - this.ndc.z > 0.0000001; |
| 133 | + if (hit) { |
| 134 | + const unprojected = this.ndc.clone().unproject(camera); |
| 135 | + this.hitpoint.copy(unprojected); |
| 136 | + } |
| 137 | + }); |
| 138 | + } |
| 139 | + |
| 140 | + if (smoothTime && smoothTime > 0 && delta > 0) { |
| 141 | + easing.damp3(dof.target, this.hitpoint, smoothTime, delta); |
| 142 | + } else { |
| 143 | + dof.target.copy(this.hitpoint); |
| 144 | + } |
| 145 | + |
| 146 | + const hitpointMesh = this.hitpointMeshRef()?.nativeElement; |
| 147 | + if (hitpointMesh) hitpointMesh.position.copy(this.hitpoint); |
| 148 | + |
| 149 | + const targetMesh = this.targetMeshRef()?.nativeElement; |
| 150 | + if (targetMesh) targetMesh.position.copy(dof.target); |
| 151 | + }); |
| 152 | + } |
| 153 | +} |
0 commit comments