|
| 1 | +// const addLight = (light: Object3D, effect: SelectiveBloomEffect) => light.layers.enable(effect.selection.layer) |
| 2 | +// const removeLight = (light: Object3D, effect: SelectiveBloomEffect) => light.layers.disable(effect.selection.layer) |
| 3 | + |
| 4 | +import { |
| 5 | + ChangeDetectionStrategy, |
| 6 | + Component, |
| 7 | + computed, |
| 8 | + CUSTOM_ELEMENTS_SCHEMA, |
| 9 | + effect, |
| 10 | + ElementRef, |
| 11 | + inject, |
| 12 | + input, |
| 13 | +} from '@angular/core'; |
| 14 | +import { injectStore, NgtArgs, NgtSelection, pick, resolveRef } from 'angular-three'; |
| 15 | +import { mergeInputs } from 'ngxtension/inject-inputs'; |
| 16 | +import { BlendFunction, BloomEffectOptions, SelectiveBloomEffect } from 'postprocessing'; |
| 17 | +import * as THREE from 'three'; |
| 18 | +import { NgtpEffectComposer } from '../effect-composer'; |
| 19 | + |
| 20 | +export type SelectiveBloomOptions = BloomEffectOptions & { |
| 21 | + selectionLayer: number; |
| 22 | + inverted: boolean; |
| 23 | + ignoreBackground: boolean; |
| 24 | +}; |
| 25 | + |
| 26 | +const defaultOptions: SelectiveBloomOptions = { |
| 27 | + selectionLayer: 10, |
| 28 | + inverted: false, |
| 29 | + ignoreBackground: false, |
| 30 | +}; |
| 31 | + |
| 32 | +@Component({ |
| 33 | + selector: 'ngtp-selective-bloom', |
| 34 | + template: ` |
| 35 | + <ngt-primitive *args="[effect()]" [dispose]="null" /> |
| 36 | + `, |
| 37 | + imports: [NgtArgs], |
| 38 | + schemas: [CUSTOM_ELEMENTS_SCHEMA], |
| 39 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 40 | +}) |
| 41 | +export class NgtpSelectiveBloom { |
| 42 | + lights = input.required<THREE.Object3D[] | ElementRef<THREE.Object3D | undefined>[]>(); |
| 43 | + selection = input< |
| 44 | + | THREE.Object3D |
| 45 | + | THREE.Object3D[] |
| 46 | + | ElementRef<THREE.Object3D | undefined> |
| 47 | + | ElementRef<THREE.Object3D | undefined>[] |
| 48 | + >([]); |
| 49 | + options = input(defaultOptions, { transform: mergeInputs(defaultOptions) }); |
| 50 | + |
| 51 | + private blendFunction = pick(this.options, 'blendFunction'); |
| 52 | + private luminanceThreshold = pick(this.options, 'luminanceThreshold'); |
| 53 | + private luminanceSmoothing = pick(this.options, 'luminanceSmoothing'); |
| 54 | + private mipmapBlur = pick(this.options, 'mipmapBlur'); |
| 55 | + private intensity = pick(this.options, 'intensity'); |
| 56 | + private radius = pick(this.options, 'radius'); |
| 57 | + private levels = pick(this.options, 'levels'); |
| 58 | + private kernelSize = pick(this.options, 'kernelSize'); |
| 59 | + private resolutionScale = pick(this.options, 'resolutionScale'); |
| 60 | + private width = pick(this.options, 'width'); |
| 61 | + private height = pick(this.options, 'height'); |
| 62 | + private resolutionX = pick(this.options, 'resolutionX'); |
| 63 | + private resolutionY = pick(this.options, 'resolutionY'); |
| 64 | + private inverted = pick(this.options, 'inverted'); |
| 65 | + private ignoreBackground = pick(this.options, 'ignoreBackground'); |
| 66 | + private selectionLayer = pick(this.options, 'selectionLayer'); |
| 67 | + |
| 68 | + private store = injectStore(); |
| 69 | + private effectComposer = inject(NgtpEffectComposer); |
| 70 | + private ngtSelection = inject(NgtSelection, { optional: true }); |
| 71 | + |
| 72 | + private resolvedLights = computed(() => this.lights().map((light) => resolveRef(light))); |
| 73 | + private resolvedSelected = computed(() => { |
| 74 | + const selection = this.selection(); |
| 75 | + if (!selection) return []; |
| 76 | + const array = Array.isArray(selection) ? selection : [selection]; |
| 77 | + return array.map((selected) => resolveRef(selected)); |
| 78 | + }); |
| 79 | + private resolvedNgtSelected = computed(() => { |
| 80 | + if (!this.ngtSelection || !this.ngtSelection.enabled) return []; |
| 81 | + return this.ngtSelection.selected().map((selected) => resolveRef(selected)); |
| 82 | + }); |
| 83 | + |
| 84 | + protected effect = computed(() => { |
| 85 | + const effect = new SelectiveBloomEffect(this.effectComposer.scene(), this.effectComposer.camera(), { |
| 86 | + blendFunction: this.blendFunction() || BlendFunction.ADD, |
| 87 | + luminanceThreshold: this.luminanceThreshold(), |
| 88 | + luminanceSmoothing: this.luminanceSmoothing(), |
| 89 | + mipmapBlur: this.mipmapBlur(), |
| 90 | + intensity: this.intensity(), |
| 91 | + radius: this.radius(), |
| 92 | + levels: this.levels(), |
| 93 | + kernelSize: this.kernelSize(), |
| 94 | + resolutionScale: this.resolutionScale(), |
| 95 | + width: this.width(), |
| 96 | + height: this.height(), |
| 97 | + resolutionX: this.resolutionX(), |
| 98 | + resolutionY: this.resolutionY(), |
| 99 | + }); |
| 100 | + |
| 101 | + effect.inverted = this.inverted(); |
| 102 | + effect.ignoreBackground = this.ignoreBackground(); |
| 103 | + |
| 104 | + return effect; |
| 105 | + }); |
| 106 | + |
| 107 | + constructor() { |
| 108 | + effect((onCleanup) => { |
| 109 | + // skip input selection altogether if NgtSelection is used |
| 110 | + if (this.ngtSelection) return; |
| 111 | + const selection = this.resolvedSelected(); |
| 112 | + if (!selection.length) return; |
| 113 | + |
| 114 | + const [effect, invalidate] = [this.effect(), this.store.invalidate(), this.selectionLayer()]; |
| 115 | + effect.selection.set(selection as THREE.Object3D[]); |
| 116 | + invalidate(); |
| 117 | + |
| 118 | + onCleanup(() => { |
| 119 | + effect.selection.clear(); |
| 120 | + invalidate(); |
| 121 | + }); |
| 122 | + }); |
| 123 | + |
| 124 | + effect(() => { |
| 125 | + const [selectionLayer, invalidate, effect] = [this.selectionLayer(), this.store.invalidate(), this.effect()]; |
| 126 | + effect.selection.layer = selectionLayer; |
| 127 | + invalidate(); |
| 128 | + }); |
| 129 | + |
| 130 | + effect((onCleanup) => { |
| 131 | + const lights = this.resolvedLights(); |
| 132 | + if (lights.length <= 0) return; |
| 133 | + |
| 134 | + const [effect, invalidate] = [this.effect(), this.store.invalidate(), this.selectionLayer()]; |
| 135 | + |
| 136 | + lights.forEach((light) => light && this.addLight(effect, light)); |
| 137 | + invalidate(); |
| 138 | + |
| 139 | + onCleanup(() => { |
| 140 | + lights.forEach((light) => light && this.removeLight(effect, light)); |
| 141 | + invalidate(); |
| 142 | + }); |
| 143 | + }); |
| 144 | + |
| 145 | + effect((onCleanup) => { |
| 146 | + const selected = this.resolvedNgtSelected(); |
| 147 | + if (!selected.length) return; |
| 148 | + |
| 149 | + const [effect, invalidate] = [this.effect(), this.store.invalidate(), this.selectionLayer()]; |
| 150 | + effect.selection.set(selected as THREE.Object3D[]); |
| 151 | + invalidate(); |
| 152 | + |
| 153 | + onCleanup(() => { |
| 154 | + effect.selection.clear(); |
| 155 | + invalidate(); |
| 156 | + }); |
| 157 | + }); |
| 158 | + } |
| 159 | + |
| 160 | + private addLight(effect: SelectiveBloomEffect, light: THREE.Object3D) { |
| 161 | + light.layers.enable(effect.selection.layer); |
| 162 | + } |
| 163 | + |
| 164 | + private removeLight(effect: SelectiveBloomEffect, light: THREE.Object3D) { |
| 165 | + light.layers.disable(effect.selection.layer); |
| 166 | + } |
| 167 | +} |
0 commit comments