|
| 1 | +import { NgIf } from '@angular/common'; |
| 2 | +import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, effect, inject, Input } from '@angular/core'; |
| 3 | +import { extend, getLocalState, injectBeforeRender, injectNgtRef, NgtSignalStore, NgtStore } from 'angular-three'; |
| 4 | +import { MeshRefractionMaterial } from 'angular-three-soba/shaders'; |
| 5 | +import { MeshBVH, SAH } from 'three-mesh-bvh'; |
| 6 | + |
| 7 | +extend({ MeshRefractionMaterial }); |
| 8 | + |
| 9 | +export interface NgtsMeshRefractionMaterialState { |
| 10 | + /** Environment map */ |
| 11 | + envMap: THREE.CubeTexture | THREE.Texture; |
| 12 | + /** Number of ray-cast bounces, it can be expensive to have too many, 2 */ |
| 13 | + bounces: number; |
| 14 | + /** Refraction index, 2.4 */ |
| 15 | + ior: number; |
| 16 | + /** Fresnel (strip light), 0 */ |
| 17 | + fresnel: number; |
| 18 | + /** RGB shift intensity, can be expensive, 0 */ |
| 19 | + aberrationStrength: number; |
| 20 | + /** Color, white */ |
| 21 | + color: THREE.ColorRepresentation; |
| 22 | + /** If this is on it uses fewer ray casts for the RGB shift sacrificing physical accuracy, true */ |
| 23 | + fastChroma: boolean; |
| 24 | +} |
| 25 | + |
| 26 | +const isCubeTexture = (def: THREE.CubeTexture | THREE.Texture): def is THREE.CubeTexture => |
| 27 | + def && (def as THREE.CubeTexture).isCubeTexture; |
| 28 | + |
| 29 | +@Component({ |
| 30 | + selector: 'ngts-mesh-refraction-material', |
| 31 | + standalone: true, |
| 32 | + template: ` |
| 33 | + <ngt-mesh-refraction-material |
| 34 | + *ngIf="defines() as defines" |
| 35 | + [ref]="materialRef" |
| 36 | + [defines]="defines" |
| 37 | + [resolution]="resolution()" |
| 38 | + [aberrationStrength]="refractionAberrationStrength()" |
| 39 | + [envMap]="refractionEnvMap()" |
| 40 | + [bounces]="refractionBounces()" |
| 41 | + [ior]="refractionIor()" |
| 42 | + [fresnel]="refractionFresnel()" |
| 43 | + [color]="refractionColor()" |
| 44 | + [fastChroma]="refractionFastChroma()" |
| 45 | + ngtCompound |
| 46 | + attach="material" |
| 47 | + > |
| 48 | + <ng-content /> |
| 49 | + </ngt-mesh-refraction-material> |
| 50 | + `, |
| 51 | + imports: [NgIf], |
| 52 | + schemas: [CUSTOM_ELEMENTS_SCHEMA], |
| 53 | +}) |
| 54 | +export class NgtsMeshRefractionMaterial extends NgtSignalStore<NgtsMeshRefractionMaterialState> { |
| 55 | + @Input() materialRef = injectNgtRef<InstanceType<typeof MeshRefractionMaterial>>(); |
| 56 | + /** Environment map */ |
| 57 | + @Input({ required: true }) set envMap(envMap: THREE.CubeTexture | THREE.Texture) { |
| 58 | + this.set({ envMap }); |
| 59 | + } |
| 60 | + /** Number of ray-cast bounces, it can be expensive to have too many, 2 */ |
| 61 | + @Input() set bounces(bounces: number) { |
| 62 | + this.set({ bounces }); |
| 63 | + } |
| 64 | + /** Refraction index, 2.4 */ |
| 65 | + @Input() set ior(ior: number) { |
| 66 | + this.set({ ior }); |
| 67 | + } |
| 68 | + /** Fresnel (strip light), 0 */ |
| 69 | + @Input() set fresnel(fresnel: number) { |
| 70 | + this.set({ fresnel }); |
| 71 | + } |
| 72 | + /** RGB shift intensity, can be expensive, 0 */ |
| 73 | + @Input() set aberrationStrength(aberrationStrength: number) { |
| 74 | + this.set({ aberrationStrength }); |
| 75 | + } |
| 76 | + /** Color, white */ |
| 77 | + @Input() set color(color: THREE.ColorRepresentation) { |
| 78 | + this.set({ color }); |
| 79 | + } |
| 80 | + /** If this is on it uses fewer ray casts for the RGB shift sacrificing physical accuracy, true */ |
| 81 | + @Input() set fastChroma(fastChroma: boolean) { |
| 82 | + this.set({ fastChroma }); |
| 83 | + } |
| 84 | + |
| 85 | + readonly refractionEnvMap = this.select('envMap'); |
| 86 | + readonly refractionBounces = this.select('bounces'); |
| 87 | + readonly refractionIor = this.select('ior'); |
| 88 | + readonly refractionFresnel = this.select('fresnel'); |
| 89 | + readonly refractionAberrationStrength = this.select('aberrationStrength'); |
| 90 | + readonly refractionColor = this.select('color'); |
| 91 | + readonly refractionFastChroma = this.select('fastChroma'); |
| 92 | + |
| 93 | + readonly #store = inject(NgtStore); |
| 94 | + readonly #size = this.#store.select('size'); |
| 95 | + |
| 96 | + readonly #envMap = this.select('envMap'); |
| 97 | + |
| 98 | + readonly defines = computed(() => { |
| 99 | + const envMap = this.#envMap(); |
| 100 | + if (!envMap) return null; |
| 101 | + |
| 102 | + const aberrationStrength = this.refractionAberrationStrength(); |
| 103 | + const fastChroma = this.refractionFastChroma(); |
| 104 | + |
| 105 | + const temp = {} as { [key: string]: string }; |
| 106 | + // Sampler2D and SamplerCube need different defines |
| 107 | + const isCubeMap = isCubeTexture(envMap); |
| 108 | + const w = (isCubeMap ? envMap.image[0]?.width : envMap.image.width) ?? 1024; |
| 109 | + const cubeSize = w / 4; |
| 110 | + const _lodMax = Math.floor(Math.log2(cubeSize)); |
| 111 | + const _cubeSize = Math.pow(2, _lodMax); |
| 112 | + const width = 3 * Math.max(_cubeSize, 16 * 7); |
| 113 | + const height = 4 * _cubeSize; |
| 114 | + if (isCubeMap) temp['ENVMAP_TYPE_CUBEM'] = ''; |
| 115 | + temp['CUBEUV_TEXEL_WIDTH'] = `${1.0 / width}`; |
| 116 | + temp['CUBEUV_TEXEL_HEIGHT'] = `${1.0 / height}`; |
| 117 | + temp['CUBEUV_MAX_MIP'] = `${_lodMax}.0`; |
| 118 | + // Add defines from chromatic aberration |
| 119 | + if (aberrationStrength > 0) temp['CHROMATIC_ABERRATIONS'] = ''; |
| 120 | + if (fastChroma) temp['FAST_CHROMA'] = ''; |
| 121 | + return temp; |
| 122 | + }); |
| 123 | + readonly resolution = computed(() => [this.#size().width, this.#size().height]); |
| 124 | + |
| 125 | + constructor() { |
| 126 | + super({ aberrationStrength: 0, fastChroma: true }); |
| 127 | + injectBeforeRender(({ camera }) => { |
| 128 | + if (this.materialRef.nativeElement) { |
| 129 | + (this.materialRef.nativeElement as any)!.viewMatrixInverse = camera.matrixWorld; |
| 130 | + (this.materialRef.nativeElement as any)!.projectionMatrixInverse = camera.projectionMatrixInverse; |
| 131 | + } |
| 132 | + }); |
| 133 | + this.#setupGeometry(); |
| 134 | + } |
| 135 | + |
| 136 | + #setupGeometry() { |
| 137 | + effect(() => { |
| 138 | + const material = this.materialRef.nativeElement; |
| 139 | + if (!material) return; |
| 140 | + const geometry = getLocalState(material).parent()?.geometry; |
| 141 | + if (geometry) { |
| 142 | + (material as any).bvh.updateFrom( |
| 143 | + new MeshBVH(geometry.toNonIndexed(), { lazyGeneration: false, strategy: SAH } as any) |
| 144 | + ); |
| 145 | + } |
| 146 | + }); |
| 147 | + } |
| 148 | +} |
0 commit comments