From 80b2b10c962870b7b597fe1757a4f0b9bcb435ea Mon Sep 17 00:00:00 2001 From: Chau Tran Date: Sat, 4 Mar 2023 22:40:45 -0600 Subject: [PATCH] feat: mesh refarction but it's half working (maybe caustics issue) --- .../.storybook/public/soba/dflat.glb | Bin 0 -> 11468 bytes .../abstractions/src/index.ts | 1 + .../abstractions/src/lib/edges/edges.ts | 75 +++ libs/angular-three-soba/cameras/src/index.ts | 1 + .../cameras/src/lib/camera/camera-content.ts | 2 +- .../src/lib/cube-camera/cube-camera.ts | 111 +++++ .../angular-three-soba/materials/src/index.ts | 2 + .../mesh-refraction-material.ts | 120 ++++- .../mesh-transmission-material.ts | 210 +++++++++ .../misc/src/lib/fbo/fbo.ts | 22 +- libs/angular-three-soba/shaders/src/index.ts | 4 + .../caustics-material/caustics-material.ts | 130 ++++++ .../caustics-projection-material.ts | 35 ++ .../lib/discard-material/discard-material.ts | 7 + .../mesh-transmission-material.ts | 302 ++++++++++++ .../mesh-refraction-material.stories.ts | 170 +++++++ libs/angular-three-soba/staging/src/index.ts | 1 + .../progressive-light-map.ts | 8 +- .../staging/src/lib/caustics/caustics.ts | 431 ++++++++++++++++++ 19 files changed, 1610 insertions(+), 22 deletions(-) create mode 100644 libs/angular-three-soba/.storybook/public/soba/dflat.glb create mode 100644 libs/angular-three-soba/abstractions/src/lib/edges/edges.ts create mode 100644 libs/angular-three-soba/cameras/src/lib/cube-camera/cube-camera.ts create mode 100644 libs/angular-three-soba/materials/src/lib/mesh-transmission-material/mesh-transmission-material.ts create mode 100644 libs/angular-three-soba/shaders/src/lib/caustics-material/caustics-material.ts create mode 100644 libs/angular-three-soba/shaders/src/lib/caustics-projection-material/caustics-projection-material.ts create mode 100644 libs/angular-three-soba/shaders/src/lib/discard-material/discard-material.ts create mode 100644 libs/angular-three-soba/shaders/src/lib/mesh-transmission-material/mesh-transmission-material.ts create mode 100644 libs/angular-three-soba/src/shaders/mesh-refraction-material.stories.ts create mode 100644 libs/angular-three-soba/staging/src/lib/caustics/caustics.ts diff --git a/libs/angular-three-soba/.storybook/public/soba/dflat.glb b/libs/angular-three-soba/.storybook/public/soba/dflat.glb new file mode 100644 index 0000000000000000000000000000000000000000..0172e84e3fe07a85838c8e2b167a2548bac56d0a GIT binary patch literal 11468 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 + + + + + + + + `, + imports: [NgIf], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class NgtsEdges extends NgtRxStore implements OnInit { + @Input() edgesRef = injectNgtRef(); + + @Input() set threshold(threshold: number) { + this.set({ threshold }); + } + + @Input() set color(color: THREE.ColorRepresentation) { + this.set({ color }); + } + + @Input() set geometry(geometry: THREE.BufferGeometry) { + this.set({ geometry }); + } + + @Input() set userData(userData: NgtAnyRecord) { + this.set({ userData }); + } + + @Input() withChildren = false; + + readonly noop = () => null; + + override initialize(): void { + super.initialize(); + this.set({ + threshold: 15, + color: 'black', + userData: {}, + }); + } + + ngOnInit(): void { + this.setupGeometry(); + } + + private setupGeometry(): void { + this.hold(this.edgesRef.$, (segments) => { + const parent = segments.parent as THREE.Mesh; + if (parent) { + const geom = this.get('geometry') || parent.geometry; + const threshold = this.get('threshold'); + if (geom !== segments.userData['currentGeom'] || threshold !== segments.userData['currentThreshold']) { + segments.userData['currentGeom'] = geom; + segments.userData['currentThreshold'] = threshold; + segments.geometry = new THREE.EdgesGeometry(geom, threshold); + } + } + }); + } +} diff --git a/libs/angular-three-soba/cameras/src/index.ts b/libs/angular-three-soba/cameras/src/index.ts index 2d2d035..aca8cd3 100644 --- a/libs/angular-three-soba/cameras/src/index.ts +++ b/libs/angular-three-soba/cameras/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/camera/camera-content'; +export * from './lib/cube-camera/cube-camera'; export * from './lib/orthographic-camera/orthographic-camera'; export * from './lib/perspective-camera/perspective-camera'; diff --git a/libs/angular-three-soba/cameras/src/lib/camera/camera-content.ts b/libs/angular-three-soba/cameras/src/lib/camera/camera-content.ts index 5196a52..36877a1 100644 --- a/libs/angular-three-soba/cameras/src/lib/camera/camera-content.ts +++ b/libs/angular-three-soba/cameras/src/lib/camera/camera-content.ts @@ -9,7 +9,7 @@ export class NgtsCameraContent { static ngTemplateContextGuard( _: NgtsCameraContent, ctx: unknown - ): ctx is { target: THREE.WebGLRenderTarget; group?: THREE.Group } { + ): ctx is { fbo: THREE.WebGLRenderTarget; group?: THREE.Group } { return true; } } diff --git a/libs/angular-three-soba/cameras/src/lib/cube-camera/cube-camera.ts b/libs/angular-three-soba/cameras/src/lib/cube-camera/cube-camera.ts new file mode 100644 index 0000000..089a2ad --- /dev/null +++ b/libs/angular-three-soba/cameras/src/lib/cube-camera/cube-camera.ts @@ -0,0 +1,111 @@ +import { NgIf, NgTemplateOutlet } from '@angular/common'; +import { Component, ContentChild, CUSTOM_ELEMENTS_SCHEMA, ElementRef, inject, Input, ViewChild } from '@angular/core'; +import { extend, injectBeforeRender, injectNgtRef, NgtArgs, NgtRxStore, NgtStore } from 'angular-three'; +import { combineLatest, map } from 'rxjs'; +import * as THREE from 'three'; +import { CubeCamera, Group } from 'three'; +import { NgtsCameraContent } from '../camera/camera-content'; + +extend({ Group, CubeCamera }); + +@Component({ + selector: 'ngts-cube-camera', + standalone: true, + template: ` + + + + + + + `, + imports: [NgIf, NgTemplateOutlet, NgtArgs], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class NgtsCubeCamera extends NgtRxStore { + @ViewChild('group', { static: true }) groupRef!: ElementRef; + @ContentChild(NgtsCameraContent) cameraContent?: NgtsCameraContent; + + readonly cameraRef = injectNgtRef(); + + /** Number of frames to render, Infinity */ + @Input() set frames(frames: number) { + this.set({ frames }); + } + /** Resolution of the FBO, 256 */ + @Input() set resolution(resolution: number) { + this.set({ resolution }); + } + /** Camera near, 0.1 */ + @Input() set near(near: number) { + this.set({ near }); + } + /** Camera far, 1000 */ + @Input() set far(far: number) { + this.set({ far }); + } + /** Custom environment map that is temporarily set as the scenes background */ + @Input() set envMap(envMap: THREE.Texture) { + this.set({ envMap }); + } + /** Custom fog that is temporarily set as the scenes fog */ + @Input() set fog(fog: THREE.Fog | THREE.FogExp2) { + this.set({ fog }); + } + + private readonly store = inject(NgtStore); + + override initialize(): void { + super.initialize(); + this.set({ + frames: Infinity, + resolution: 256, + near: 0.1, + far: 1000, + }); + } + + constructor() { + super(); + this.connect( + 'fbo', + this.select('resolution').pipe( + map((resolution) => { + const fbo = new THREE.WebGLCubeRenderTarget(resolution); + fbo.texture.encoding = this.store.get('gl').outputEncoding; + fbo.texture.type = THREE.HalfFloatType; + return fbo; + }) + ) + ); + this.connect('cameraArgs', combineLatest([this.select('near'), this.select('far'), this.select('fbo')])); + + let count = 0; + let originalFog: THREE.Scene['fog']; + let originalBackground: THREE.Scene['background']; + injectBeforeRender(({ scene, gl }) => { + const { frames, envMap, fog } = this.get(); + if ( + envMap && + this.cameraRef.nativeElement && + this.groupRef.nativeElement && + (frames === Infinity || count < frames) + ) { + this.groupRef.nativeElement.visible = false; + originalFog = scene.fog; + originalBackground = scene.background; + scene.background = envMap || originalBackground; + scene.fog = fog || originalFog; + this.cameraRef.nativeElement.update(gl, scene); + scene.fog = originalFog; + scene.background = originalBackground; + this.groupRef.nativeElement.visible = true; + count++; + } + }); + } +} diff --git a/libs/angular-three-soba/materials/src/index.ts b/libs/angular-three-soba/materials/src/index.ts index ed732da..f3b40b7 100644 --- a/libs/angular-three-soba/materials/src/index.ts +++ b/libs/angular-three-soba/materials/src/index.ts @@ -1,3 +1,5 @@ export * from './lib/mesh-distort-material/mesh-distort-material'; export * from './lib/mesh-reflector-material/mesh-reflector-material'; +export * from './lib/mesh-refraction-material/mesh-refraction-material'; +export * from './lib/mesh-transmission-material/mesh-transmission-material'; export * from './lib/mesh-wobble-material/mesh-wobble-material'; diff --git a/libs/angular-three-soba/materials/src/lib/mesh-refraction-material/mesh-refraction-material.ts b/libs/angular-three-soba/materials/src/lib/mesh-refraction-material/mesh-refraction-material.ts index aee83d8..5d40d8f 100644 --- a/libs/angular-three-soba/materials/src/lib/mesh-refraction-material/mesh-refraction-material.ts +++ b/libs/angular-three-soba/materials/src/lib/mesh-refraction-material/mesh-refraction-material.ts @@ -1,21 +1,131 @@ import { NgIf } from '@angular/common'; -import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core'; -import { extend, injectNgtRef, NgtPush } from 'angular-three'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, Input, OnInit } from '@angular/core'; +import { extend, getLocalState, injectBeforeRender, injectNgtRef, NgtPush, NgtRxStore, NgtStore } from 'angular-three'; import { MeshRefractionMaterial } from 'angular-three-soba/shaders'; +import { combineLatest, map } from 'rxjs'; +import { MeshBVH, SAH } from 'three-mesh-bvh'; extend({ MeshRefractionMaterial }); +const isCubeTexture = (def: THREE.CubeTexture | THREE.Texture): def is THREE.CubeTexture => + def && (def as THREE.CubeTexture).isCubeTexture; + @Component({ selector: 'ngts-mesh-refraction-material', standalone: true, template: ` - + `, imports: [NgtPush, NgIf], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) -export class NgtsMeshRefractionMaterial { - @Input() materialRef = injectNgtRef(); +export class NgtsMeshRefractionMaterial extends NgtRxStore implements OnInit { + @Input() materialRef = injectNgtRef(); + /** Environment map */ + @Input() set envMap(envMap: THREE.CubeTexture | THREE.Texture) { + this.set({ envMap }); + } + /** Number of ray-cast bounces, it can be expensive to have too many, 2 */ + @Input() set bounces(bounces: number) { + this.set({ bounces }); + } + /** Refraction index, 2.4 */ + @Input() set ior(ior: number) { + this.set({ ior }); + } + /** Fresnel (strip light), 0 */ + @Input() set fresnel(fresnel: number) { + this.set({ fresnel }); + } + /** RGB shift intensity, can be expensive, 0 */ + @Input() set aberrationStrength(aberrationStrength: number) { + this.set({ aberrationStrength }); + } + /** Color, white */ + @Input() set color(color: THREE.ColorRepresentation) { + this.set({ color }); + } + /** If this is on it uses fewer ray casts for the RGB shift sacrificing physical accuracy, true */ + @Input() set fastChroma(fastChroma: boolean) { + this.set({ fastChroma }); + } + + readonly defines$ = this.select('defines'); + + private readonly store = inject(NgtStore); + + override initialize(): void { + super.initialize(); + this.set({ + aberrationStrength: 0, + fastChroma: true, + }); + } + + constructor() { + super(); + this.connect( + 'defines', + combineLatest([this.select('aberrationStrength'), this.select('fastChroma'), this.select('envMap')]).pipe( + map(([aberrationStrength, fastChroma, envMap]) => { + const temp = {} as { [key: string]: string }; + // Sampler2D and SamplerCube need different defines + const isCubeMap = isCubeTexture(envMap); + const w = (isCubeMap ? envMap.image[0]?.width : envMap.image.width) ?? 1024; + const cubeSize = w / 4; + const _lodMax = Math.floor(Math.log2(cubeSize)); + const _cubeSize = Math.pow(2, _lodMax); + const width = 3 * Math.max(_cubeSize, 16 * 7); + const height = 4 * _cubeSize; + if (isCubeMap) temp['ENVMAP_TYPE_CUBEM'] = ''; + temp['CUBEUV_TEXEL_WIDTH'] = `${1.0 / width}`; + temp['CUBEUV_TEXEL_HEIGHT'] = `${1.0 / height}`; + temp['CUBEUV_MAX_MIP'] = `${_lodMax}.0`; + // Add defines from chromatic aberration + if (aberrationStrength > 0) temp['CHROMATIC_ABERRATIONS'] = ''; + if (fastChroma) temp['FAST_CHROMA'] = ''; + return temp; + }) + ) + ); + this.connect('resolution', this.store.select('size').pipe(map((size) => [size.width, size.height]))); + + injectBeforeRender(({ camera }) => { + if (this.materialRef.nativeElement) { + (this.materialRef.nativeElement as any)!.viewMatrixInverse = camera.matrixWorld; + (this.materialRef.nativeElement as any)!.projectionMatrixInverse = camera.projectionMatrixInverse; + } + }); + } + + ngOnInit() { + this.setupGeometry(); + } + + private setupGeometry() { + this.hold(this.materialRef.$, (material) => { + const geometry = getLocalState(material).parent?.geometry; + if (geometry) { + (material as any).bvh.updateFrom( + new MeshBVH(geometry.toNonIndexed(), { lazyGeneration: false, strategy: SAH } as any) + ); + } + }); + } } diff --git a/libs/angular-three-soba/materials/src/lib/mesh-transmission-material/mesh-transmission-material.ts b/libs/angular-three-soba/materials/src/lib/mesh-transmission-material/mesh-transmission-material.ts new file mode 100644 index 0000000..a81314e --- /dev/null +++ b/libs/angular-three-soba/materials/src/lib/mesh-transmission-material/mesh-transmission-material.ts @@ -0,0 +1,210 @@ +import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core'; +import { + extend, + getLocalState, + injectBeforeRender, + injectNgtRef, + NgtAnyRecord, + NgtArgs, + NgtRxStore, +} from 'angular-three'; +import { injectNgtsFBO } from 'angular-three-soba/misc'; +import { DiscardMaterial, MeshTransmissionMaterial } from 'angular-three-soba/shaders'; +import { combineLatest, map } from 'rxjs'; +import * as THREE from 'three'; + +extend({ MeshTransmissionMaterial }); + +@Component({ + selector: 'ngts-mesh-transmission-material', + standalone: true, + template: ` + + `, + imports: [NgtArgs], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class NgtsMeshTranmissionMaterial extends NgtRxStore { + @Input() materialRef = injectNgtRef< + MeshTransmissionMaterial & { + time: number; + buffer?: THREE.Texture; + } + >(); + /** transmissionSampler, you can use the threejs transmission sampler texture that is + * generated once for all transmissive materials. The upside is that it can be faster if you + * use multiple MeshPhysical and Transmission materials, the downside is that transmissive materials + * using this can't see other transparent or transmissive objects, default: false */ + @Input() set transmissionSampler(transmissionSampler: boolean) { + this.set({ transmissionSampler }); + } + /** Render the backside of the material (more cost, better results), default: false */ + @Input() set backside(backside: boolean) { + this.set({ backside }); + } + /** Backside thickness (when backside is true), default: 0 */ + @Input() set backsideThickness(backsideThickness: number) { + this.set({ backsideThickness }); + } + /** Resolution of the local buffer, default: undefined (fullscreen) */ + @Input() set resolution(resolution: number) { + this.set({ resolution }); + } + /** Resolution of the local buffer for backfaces, default: undefined (fullscreen) */ + @Input() set backsideResolution(backsideResolution: number) { + this.set({ backsideResolution }); + } + /** Refraction samples, default: 10 */ + @Input() set samples(samples: number) { + this.set({ samples }); + } + /** Buffer scene background (can be a texture, a cubetexture or a color), default: null */ + @Input() set background(background: THREE.Texture | THREE.Color) { + this.set({ background }); + } + /* Transmission, default: 1 */ + @Input() set transmission(transmission: number) { + this.set({ transmission }); + } + /* Thickness (refraction), default: 0 */ + @Input() set thickness(thickness: number) { + this.set({ thickness }); + } + /* Roughness (blur), default: 0 */ + @Input() set roughness(roughness: number) { + this.set({ roughness }); + } + /* Chromatic aberration, default: 0.03 */ + @Input() set chromaticAberration(chromaticAberration: number) { + this.set({ chromaticAberration }); + } + /* Anisotropy, default: 0.1 */ + @Input() set anisotropy(anisotropy: number) { + this.set({ anisotropy }); + } + /* Distortion, default: 0 */ + @Input() set distortion(distortion: number) { + this.set({ distortion }); + } + /* Distortion scale, default: 0.5 */ + @Input() set distortionScale(distortionScale: number) { + this.set({ distortionScale }); + } + /* Temporal distortion (speed of movement), default: 0.0 */ + @Input() set temporalDistortion(temporalDistortion: number) { + this.set({ temporalDistortion }); + } + /** The scene rendered into a texture (use it to share a texture between materials), default: null */ + @Input() set buffer(buffer: THREE.Texture) { + this.set({ buffer }); + } + /** Internals */ + @Input() set time(time: number) { + this.set({ time }); + } + + readonly discardMaterial = new DiscardMaterial(); + readonly fboBackRef = injectNgtsFBO(() => + combineLatest([this.select('backsideResolution'), this.select('resolution')]).pipe( + map(([backsideResolution, resolution]) => backsideResolution || resolution) + ) + ); + readonly fboMainRef = injectNgtsFBO(() => this.select('resolution')); + + override initialize(): void { + super.initialize(); + this.set({ + transmissionSampler: false, + backside: false, + side: THREE.FrontSide, + transmission: 1, + thickness: 0, + backsideThickness: 0, + samples: 10, + roughness: 0, + anisotropy: 0.1, + chromaticAberration: 0.03, + distortion: 0, + distortionScale: 0.5, + temporalDistortion: 0.0, + buffer: null, + }); + } + + constructor() { + super(); + let oldBg: THREE.Scene['background']; + let oldTone: THREE.WebGLRenderer['toneMapping']; + let parent: THREE.Object3D; + + injectBeforeRender((state) => { + const { transmissionSampler, background, backside, backsideThickness, thickness, side } = this.get(); + + this.materialRef.nativeElement.time = state.clock.getElapsedTime(); + // Render only if the buffer matches the built-in and no transmission sampler is set + if ( + this.materialRef.nativeElement.buffer === this.fboMainRef.nativeElement.texture && + !transmissionSampler + ) { + parent = getLocalState(this.materialRef.nativeElement).parent as THREE.Object3D; + if (parent) { + // Save defaults + oldTone = state.gl.toneMapping; + oldBg = state.scene.background; + + // Switch off tonemapping lest it double tone maps + // Save the current background and set the HDR as the new BG + // Use discardmaterial, the parent will be invisible, but it's shadows will still be cast + state.gl.toneMapping = THREE.NoToneMapping; + if (background) state.scene.background = background; + (parent as NgtAnyRecord)['material'] = this.discardMaterial; + + if (backside) { + // Render into the backside buffer + state.gl.setRenderTarget(this.fboBackRef.nativeElement); + state.gl.render(state.scene, state.camera); + // And now prepare the material for the main render using the backside buffer + (parent as NgtAnyRecord)['material'] = this.materialRef.nativeElement; + (parent as NgtAnyRecord)['material'].buffer = this.fboBackRef.nativeElement.texture; + (parent as NgtAnyRecord)['material'].thickness = backsideThickness; + (parent as NgtAnyRecord)['material'].side = THREE.BackSide; + } + + // Render into the main buffer + state.gl.setRenderTarget(this.fboMainRef.nativeElement); + state.gl.render(state.scene, state.camera); + + (parent as NgtAnyRecord)['material'].thickness = thickness; + (parent as NgtAnyRecord)['material'].side = side; + (parent as NgtAnyRecord)['material'].buffer = this.fboMainRef.nativeElement.texture; + + // Set old state back + state.scene.background = oldBg; + state.gl.setRenderTarget(null); + (parent as NgtAnyRecord)['material'] = this.materialRef.nativeElement; + state.gl.toneMapping = oldTone; + } + } + }); + } + + ngOnInit() { + console.log(this.materialRef); + } +} diff --git a/libs/angular-three-soba/misc/src/lib/fbo/fbo.ts b/libs/angular-three-soba/misc/src/lib/fbo/fbo.ts index c4810af..a50a8ba 100644 --- a/libs/angular-three-soba/misc/src/lib/fbo/fbo.ts +++ b/libs/angular-three-soba/misc/src/lib/fbo/fbo.ts @@ -3,19 +3,21 @@ import { injectNgtDestroy, injectNgtRef, NgtStore, safeDetectChanges } from 'ang import { isObservable, Observable, of, takeUntil } from 'rxjs'; import * as THREE from 'three'; -interface FBOSettings extends THREE.WebGLRenderTargetOptions { - multisample?: T; +interface FBOSettings extends THREE.WebGLRenderTargetOptions { + /** Defines the count of MSAA samples. Can only be used with WebGL 2. Default: 0 */ samples?: number; + /** If set, the scene depth will be rendered into buffer.depthTexture. Default: false */ + depth?: boolean; } -export interface NgtsFBOParams { - width?: number | FBOSettings; +export interface NgtsFBOParams { + width?: number | FBOSettings; height?: number; - settings?: FBOSettings; + settings?: FBOSettings; } -export function injectNgtsFBO( - paramsFactory: (defaultParams: Partial>) => NgtsFBOParams | Observable> +export function injectNgtsFBO( + paramsFactory: (defaultParams: Partial) => NgtsFBOParams | Observable ) { const store = inject(NgtStore); const targetRef = injectNgtRef(); @@ -31,7 +33,7 @@ export function injectNgtsFBO( const _height = typeof height === 'number' ? height : size.height * viewport.dpr; const _settings = (typeof width === 'number' ? settings : (width as FBOSettings)) || {}; - const { samples, ...targetSettings } = _settings; + const { samples = 0, depth, ...targetSettings } = _settings; if (!targetRef.nativeElement) { const target = new THREE.WebGLRenderTarget(_width, _height, { @@ -41,7 +43,9 @@ export function injectNgtsFBO( type: THREE.HalfFloatType, ...targetSettings, }); - if (samples) target.samples = samples; + if (depth) target.depthTexture = new THREE.DepthTexture(_width, _height, THREE.FloatType); + + target.samples = samples; targetRef.nativeElement = target; } diff --git a/libs/angular-three-soba/shaders/src/index.ts b/libs/angular-three-soba/shaders/src/index.ts index 2edc73c..9f02c7f 100644 --- a/libs/angular-three-soba/shaders/src/index.ts +++ b/libs/angular-three-soba/shaders/src/index.ts @@ -1,8 +1,12 @@ export * from './lib/blur-pass/blur-pass'; +export * from './lib/caustics-material/caustics-material'; +export * from './lib/caustics-projection-material/caustics-projection-material'; export * from './lib/convolution-material/convolution-material'; +export * from './lib/discard-material/discard-material'; export * from './lib/mesh-distort-material/mesh-distort-material'; export * from './lib/mesh-reflector-material/mesh-reflector-material'; export * from './lib/mesh-refraction-material/mesh-refraction-material'; +export * from './lib/mesh-transmission-material/mesh-transmission-material'; export * from './lib/mesh-wobble-material/mesh-wobble-material'; export * from './lib/shader-material/shader-material'; export * from './lib/spot-light-material/spot-light-material'; diff --git a/libs/angular-three-soba/shaders/src/lib/caustics-material/caustics-material.ts b/libs/angular-three-soba/shaders/src/lib/caustics-material/caustics-material.ts new file mode 100644 index 0000000..254956f --- /dev/null +++ b/libs/angular-three-soba/shaders/src/lib/caustics-material/caustics-material.ts @@ -0,0 +1,130 @@ +import * as THREE from 'three'; +import { shaderMaterial } from '../shader-material/shader-material'; + +export const CausticsMaterial = shaderMaterial( + { + cameraMatrixWorld: new THREE.Matrix4(), + cameraProjectionMatrixInv: new THREE.Matrix4(), + normalTexture: null, + depthTexture: null, + lightDir: new THREE.Vector3(0, 1, 0), + lightPlaneNormal: new THREE.Vector3(0, 1, 0), + lightPlaneConstant: 0, + near: 0.1, + far: 100, + modelMatrix: new THREE.Matrix4(), + worldRadius: 1 / 40, + ior: 1.1, + bounces: 0, + resolution: 1024, + size: 10, + intensity: 0.5, + }, + /* glsl */ `varying vec2 vUv; +void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); +}`, + /* glsl */ `uniform mat4 cameraMatrixWorld; +uniform mat4 cameraProjectionMatrixInv; +uniform vec3 lightDir; +uniform vec3 lightPlaneNormal; +uniform float lightPlaneConstant; +uniform float near; +uniform float far; +uniform float time; +uniform float worldRadius; +uniform float resolution; +uniform float size; +uniform float intensity; +uniform float ior; +precision highp isampler2D; +precision highp usampler2D; +uniform sampler2D normalTexture; +uniform sampler2D depthTexture; +uniform float bounces; +varying vec2 vUv; +vec3 WorldPosFromDepth(float depth, vec2 coord) { + float z = depth * 2.0 - 1.0; + vec4 clipSpacePosition = vec4(coord * 2.0 - 1.0, z, 1.0); + vec4 viewSpacePosition = cameraProjectionMatrixInv * clipSpacePosition; + // Perspective division + viewSpacePosition /= viewSpacePosition.w; + vec4 worldSpacePosition = cameraMatrixWorld * viewSpacePosition; + return worldSpacePosition.xyz; +} +float sdPlane( vec3 p, vec3 n, float h ) { + // n must be normalized + return dot(p,n) + h; +} +float planeIntersect( vec3 ro, vec3 rd, vec4 p ) { + return -(dot(ro,p.xyz)+p.w)/dot(rd,p.xyz); +} +vec3 totalInternalReflection(vec3 ro, vec3 rd, vec3 pos, vec3 normal, float ior, out vec3 rayOrigin, out vec3 rayDirection) { + rayOrigin = ro; + rayDirection = rd; + rayDirection = refract(rayDirection, normal, 1.0 / ior); + rayOrigin = pos + rayDirection * 0.1; + return rayDirection; +} +void main() { + // Each sample consists of random offset in the x and y direction + float caustic = 0.0; + float causticTexelSize = (1.0 / resolution) * size * 2.0; + float texelsNeeded = worldRadius / causticTexelSize; + float sampleRadius = texelsNeeded / resolution; + float sum = 0.0; + if (texture2D(depthTexture, vUv).x == 1.0) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + vec2 offset1 = vec2(-0.5, -0.5);//vec2(rand() - 0.5, rand() - 0.5); + vec2 offset2 = vec2(-0.5, 0.5);//vec2(rand() - 0.5, rand() - 0.5); + vec2 offset3 = vec2(0.5, 0.5);//vec2(rand() - 0.5, rand() - 0.5); + vec2 offset4 = vec2(0.5, -0.5);//vec2(rand() - 0.5, rand() - 0.5); + vec2 uv1 = vUv + offset1 * sampleRadius; + vec2 uv2 = vUv + offset2 * sampleRadius; + vec2 uv3 = vUv + offset3 * sampleRadius; + vec2 uv4 = vUv + offset4 * sampleRadius; + vec3 normal1 = texture2D(normalTexture, uv1, -10.0).rgb * 2.0 - 1.0; + vec3 normal2 = texture2D(normalTexture, uv2, -10.0).rgb * 2.0 - 1.0; + vec3 normal3 = texture2D(normalTexture, uv3, -10.0).rgb * 2.0 - 1.0; + vec3 normal4 = texture2D(normalTexture, uv4, -10.0).rgb * 2.0 - 1.0; + float depth1 = texture2D(depthTexture, uv1, -10.0).x; + float depth2 = texture2D(depthTexture, uv2, -10.0).x; + float depth3 = texture2D(depthTexture, uv3, -10.0).x; + float depth4 = texture2D(depthTexture, uv4, -10.0).x; + // Sanity check the depths + if (depth1 == 1.0 || depth2 == 1.0 || depth3 == 1.0 || depth4 == 1.0) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + vec3 pos1 = WorldPosFromDepth(depth1, uv1); + vec3 pos2 = WorldPosFromDepth(depth2, uv2); + vec3 pos3 = WorldPosFromDepth(depth3, uv3); + vec3 pos4 = WorldPosFromDepth(depth4, uv4); + vec3 originPos1 = WorldPosFromDepth(0.0, uv1); + vec3 originPos2 = WorldPosFromDepth(0.0, uv2); + vec3 originPos3 = WorldPosFromDepth(0.0, uv3); + vec3 originPos4 = WorldPosFromDepth(0.0, uv4); + vec3 endPos1, endPos2, endPos3, endPos4; + vec3 endDir1, endDir2, endDir3, endDir4; + totalInternalReflection(originPos1, lightDir, pos1, normal1, ior, endPos1, endDir1); + totalInternalReflection(originPos2, lightDir, pos2, normal2, ior, endPos2, endDir2); + totalInternalReflection(originPos3, lightDir, pos3, normal3, ior, endPos3, endDir3); + totalInternalReflection(originPos4, lightDir, pos4, normal4, ior, endPos4, endDir4); + float lightPosArea = length(cross(originPos2 - originPos1, originPos3 - originPos1)) + length(cross(originPos3 - originPos1, originPos4 - originPos1)); + float t1 = planeIntersect(endPos1, endDir1, vec4(lightPlaneNormal, lightPlaneConstant)); + float t2 = planeIntersect(endPos2, endDir2, vec4(lightPlaneNormal, lightPlaneConstant)); + float t3 = planeIntersect(endPos3, endDir3, vec4(lightPlaneNormal, lightPlaneConstant)); + float t4 = planeIntersect(endPos4, endDir4, vec4(lightPlaneNormal, lightPlaneConstant)); + vec3 finalPos1 = endPos1 + endDir1 * t1; + vec3 finalPos2 = endPos2 + endDir2 * t2; + vec3 finalPos3 = endPos3 + endDir3 * t3; + vec3 finalPos4 = endPos4 + endDir4 * t4; + float finalArea = length(cross(finalPos2 - finalPos1, finalPos3 - finalPos1)) + length(cross(finalPos3 - finalPos1, finalPos4 - finalPos1)); + caustic += intensity * (lightPosArea / finalArea); + // Calculate the area of the triangle in light spaces + gl_FragColor = vec4(vec3(max(caustic, 0.0)), 1.0); +}` +); diff --git a/libs/angular-three-soba/shaders/src/lib/caustics-projection-material/caustics-projection-material.ts b/libs/angular-three-soba/shaders/src/lib/caustics-projection-material/caustics-projection-material.ts new file mode 100644 index 0000000..356fd7e --- /dev/null +++ b/libs/angular-three-soba/shaders/src/lib/caustics-projection-material/caustics-projection-material.ts @@ -0,0 +1,35 @@ +import * as THREE from 'three'; +import { shaderMaterial } from '../shader-material/shader-material'; + +export const CausticsProjectionMaterial = shaderMaterial( + { + causticsTexture: null, + causticsTextureB: null, + color: new THREE.Color(), + lightProjMatrix: new THREE.Matrix4(), + lightViewMatrix: new THREE.Matrix4(), + }, + /* glsl */ `varying vec3 vWorldPosition; +void main() { + gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.); + vec4 worldPosition = modelMatrix * vec4(position, 1.); + vWorldPosition = worldPosition.xyz; +}`, + /* glsl */ `varying vec3 vWorldPosition; +uniform vec3 color; +uniform sampler2D causticsTexture; +uniform sampler2D causticsTextureB; +uniform mat4 lightProjMatrix; +uniform mat4 lightViewMatrix; + void main() { + // Apply caustics + vec4 lightSpacePos = lightProjMatrix * lightViewMatrix * vec4(vWorldPosition, 1.0); + lightSpacePos.xyz /= lightSpacePos.w; + lightSpacePos.xyz = lightSpacePos.xyz * 0.5 + 0.5; + vec3 front = texture2D(causticsTexture, lightSpacePos.xy).rgb; + vec3 back = texture2D(causticsTextureB, lightSpacePos.xy).rgb; + gl_FragColor = vec4((front + back) * color, 1.0); + #include + #include +}` +); diff --git a/libs/angular-three-soba/shaders/src/lib/discard-material/discard-material.ts b/libs/angular-three-soba/shaders/src/lib/discard-material/discard-material.ts new file mode 100644 index 0000000..ecc23cb --- /dev/null +++ b/libs/angular-three-soba/shaders/src/lib/discard-material/discard-material.ts @@ -0,0 +1,7 @@ +import { shaderMaterial } from '../shader-material/shader-material'; + +export const DiscardMaterial = shaderMaterial( + {}, + 'void main() { }', + 'void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); discard; }' +); diff --git a/libs/angular-three-soba/shaders/src/lib/mesh-transmission-material/mesh-transmission-material.ts b/libs/angular-three-soba/shaders/src/lib/mesh-transmission-material/mesh-transmission-material.ts new file mode 100644 index 0000000..7341e71 --- /dev/null +++ b/libs/angular-three-soba/shaders/src/lib/mesh-transmission-material/mesh-transmission-material.ts @@ -0,0 +1,302 @@ +import { NgtAnyRecord } from 'angular-three'; +import * as THREE from 'three'; + +interface Uniform { + value: T; +} + +export class MeshTransmissionMaterial extends THREE.MeshPhysicalMaterial { + uniforms: { + chromaticAberration: Uniform; + transmission: Uniform; + transmissionMap: Uniform; + _transmission: Uniform; + thickness: Uniform; + roughness: Uniform; + thicknessMap: Uniform; + attenuationDistance: Uniform; + attenuationColor: Uniform; + anisotropy: Uniform; + time: Uniform; + distortion: Uniform; + distortionScale: Uniform; + temporalDistortion: Uniform; + buffer: Uniform; + }; + + constructor(samples = 6, transmissionSampler = false) { + super(); + + this.uniforms = { + chromaticAberration: { value: 0.05 }, + // Transmission must always be 0, unless transmissionSampler is being used + transmission: { value: 0 }, + // Instead a workaround is used, see below for reasons why + _transmission: { value: 1 }, + transmissionMap: { value: null }, + // Roughness is 1 in THREE.MeshPhysicalMaterial but it makes little sense in a transmission material + roughness: { value: 0 }, + thickness: { value: 0 }, + thicknessMap: { value: null }, + attenuationDistance: { value: Infinity }, + attenuationColor: { value: new THREE.Color('white') }, + anisotropy: { value: 0.1 }, + time: { value: 0 }, + distortion: { value: 0.0 }, + distortionScale: { value: 0.5 }, + temporalDistortion: { value: 0.0 }, + buffer: { value: null }, + }; + + this.onBeforeCompile = (shader: THREE.Shader & { defines: { [key: string]: string } }) => { + shader.uniforms = { + ...shader.uniforms, + ...this.uniforms, + }; + + // If the transmission sampler is active inject a flag + if (transmissionSampler) shader.defines['USE_SAMPLER'] = ''; + // Otherwise we do use use .transmission and must therefore force USE_TRANSMISSION + // because threejs won't inject it for us + else shader.defines['USE_TRANSMISSION'] = ''; + + // Head + shader.fragmentShader = + /*glsl*/ ` + uniform float chromaticAberration; + uniform float anisotropy; + uniform float time; + uniform float distortion; + uniform float distortionScale; + uniform float temporalDistortion; + uniform sampler2D buffer; + + vec3 random3(vec3 c) { + float j = 4096.0*sin(dot(c,vec3(17.0, 59.4, 15.0))); + vec3 r; + r.z = fract(512.0*j); + j *= .125; + r.x = fract(512.0*j); + j *= .125; + r.y = fract(512.0*j); + return r-0.5; + } + + float seed = 0.0; + uint hash( uint x ) { + x += ( x << 10u ); + x ^= ( x >> 6u ); + x += ( x << 3u ); + x ^= ( x >> 11u ); + x += ( x << 15u ); + return x; + } + + // Compound versions of the hashing algorithm I whipped together. + uint hash( uvec2 v ) { return hash( v.x ^ hash(v.y) ); } + uint hash( uvec3 v ) { return hash( v.x ^ hash(v.y) ^ hash(v.z) ); } + uint hash( uvec4 v ) { return hash( v.x ^ hash(v.y) ^ hash(v.z) ^ hash(v.w) ); } + + // Construct a float with half-open range [0:1] using low 23 bits. + // All zeroes yields 0.0, all ones yields the next smallest representable value below 1.0. + float floatConstruct( uint m ) { + const uint ieeeMantissa = 0x007FFFFFu; // binary32 mantissa bitmask + const uint ieeeOne = 0x3F800000u; // 1.0 in IEEE binary32 + m &= ieeeMantissa; // Keep only mantissa bits (fractional part) + m |= ieeeOne; // Add fractional part to 1.0 + float f = uintBitsToFloat( m ); // Range [1:2] + return f - 1.0; // Range [0:1] + } + + // Pseudo-random value in half-open range [0:1]. + float random( float x ) { return floatConstruct(hash(floatBitsToUint(x))); } + float random( vec2 v ) { return floatConstruct(hash(floatBitsToUint(v))); } + float random( vec3 v ) { return floatConstruct(hash(floatBitsToUint(v))); } + float random( vec4 v ) { return floatConstruct(hash(floatBitsToUint(v))); } + + float rand() { + float result = random(vec3(gl_FragCoord.xy, seed)); + seed += 1.0; + return result; + } + + const float F3 = 0.3333333; + const float G3 = 0.1666667; + + float snoise(vec3 p) { + vec3 s = floor(p + dot(p, vec3(F3))); + vec3 x = p - s + dot(s, vec3(G3)); + vec3 e = step(vec3(0.0), x - x.yzx); + vec3 i1 = e*(1.0 - e.zxy); + vec3 i2 = 1.0 - e.zxy*(1.0 - e); + vec3 x1 = x - i1 + G3; + vec3 x2 = x - i2 + 2.0*G3; + vec3 x3 = x - 1.0 + 3.0*G3; + vec4 w, d; + w.x = dot(x, x); + w.y = dot(x1, x1); + w.z = dot(x2, x2); + w.w = dot(x3, x3); + w = max(0.6 - w, 0.0); + d.x = dot(random3(s), x); + d.y = dot(random3(s + i1), x1); + d.z = dot(random3(s + i2), x2); + d.w = dot(random3(s + 1.0), x3); + w *= w; + w *= w; + d *= w; + return dot(d, vec4(52.0)); + } + + float snoiseFractal(vec3 m) { + return 0.5333333* snoise(m) + +0.2666667* snoise(2.0*m) + +0.1333333* snoise(4.0*m) + +0.0666667* snoise(8.0*m); + }\n` + shader.fragmentShader; + + // Remove transmission + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + /*glsl*/ ` + #ifdef USE_TRANSMISSION + // Transmission code is based on glTF-Sampler-Viewer + // https://github.com/KhronosGroup/glTF-Sample-Viewer + uniform float _transmission; + uniform float thickness; + uniform float attenuationDistance; + uniform vec3 attenuationColor; + #ifdef USE_TRANSMISSIONMAP + uniform sampler2D transmissionMap; + #endif + #ifdef USE_THICKNESSMAP + uniform sampler2D thicknessMap; + #endif + uniform vec2 transmissionSamplerSize; + uniform sampler2D transmissionSamplerMap; + uniform mat4 modelMatrix; + uniform mat4 projectionMatrix; + varying vec3 vWorldPosition; + vec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) { + // Direction of refracted light. + vec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior ); + // Compute rotation-independant scaling of the model matrix. + vec3 modelScale; + modelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) ); + modelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) ); + modelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) ); + // The thickness is specified in local space. + return normalize( refractionVector ) * thickness * modelScale; + } + float applyIorToRoughness( const in float roughness, const in float ior ) { + // Scale roughness with IOR so that an IOR of 1.0 results in no microfacet refraction and + // an IOR of 1.5 results in the default amount of microfacet refraction. + return roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 ); + } + vec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) { + float framebufferLod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior ); + #ifdef USE_SAMPLER + #ifdef texture2DLodEXT + return texture2DLodEXT(transmissionSamplerMap, fragCoord.xy, framebufferLod); + #else + return texture2D(transmissionSamplerMap, fragCoord.xy, framebufferLod); + #endif + #else + return texture2D(buffer, fragCoord.xy); + #endif + } + vec3 applyVolumeAttenuation( const in vec3 radiance, const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) { + if ( isinf( attenuationDistance ) ) { + // Attenuation distance is +∞, i.e. the transmitted color is not attenuated at all. + return radiance; + } else { + // Compute light attenuation using Beer's law. + vec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance; + vec3 transmittance = exp( - attenuationCoefficient * transmissionDistance ); // Beer's law + return transmittance * radiance; + } + } + vec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor, + const in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix, + const in mat4 viewMatrix, const in mat4 projMatrix, const in float ior, const in float thickness, + const in vec3 attenuationColor, const in float attenuationDistance ) { + vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix ); + vec3 refractedRayExit = position + transmissionRay; + // Project refracted vector on the framebuffer, while mapping to normalized device coordinates. + vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + // Sample framebuffer to get pixel the refracted ray hits. + vec4 transmittedLight = getTransmissionSample( refractionCoords, roughness, ior ); + vec3 attenuatedColor = applyVolumeAttenuation( transmittedLight.rgb, length( transmissionRay ), attenuationColor, attenuationDistance ); + // Get the specular component. + vec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness ); + return vec4( ( 1.0 - F ) * attenuatedColor * diffuseColor, transmittedLight.a ); + } + #endif\n` + ); + + // Add refraction + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + /*glsl*/ ` + // Improve the refraction to use the world pos + material.transmission = _transmission; + material.transmissionAlpha = 1.0; + material.thickness = thickness; + material.attenuationDistance = attenuationDistance; + material.attenuationColor = attenuationColor; + #ifdef USE_TRANSMISSIONMAP + material.transmission *= texture2D( transmissionMap, vUv ).r; + #endif + #ifdef USE_THICKNESSMAP + material.thickness *= texture2D( thicknessMap, vUv ).g; + #endif + + vec3 pos = vWorldPosition; + vec3 v = normalize( cameraPosition - pos ); + vec3 n = inverseTransformDirection( normal, viewMatrix ); + vec3 transmission = vec3(0.0); + float transmissionR, transmissionB, transmissionG; + float randomCoords = rand(); + float thickness_smear = thickness * max(pow(roughnessFactor, 0.33), anisotropy); + vec3 distortionNormal = vec3(0.0); + vec3 temporalOffset = vec3(time, -time, -time) * temporalDistortion; + if (distortion > 0.0) { + distortionNormal = distortion * vec3(snoiseFractal(vec3((pos * distortionScale + temporalOffset))), snoiseFractal(vec3(pos.zxy * distortionScale - temporalOffset)), snoiseFractal(vec3(pos.yxz * distortionScale + temporalOffset))); + } + for (float i = 0.0; i < ${samples}.0; i ++) { + vec3 sampleNorm = normalize(n + roughnessFactor * roughnessFactor * 2.0 * normalize(vec3(rand() - 0.5, rand() - 0.5, rand() - 0.5)) * pow(rand(), 0.33) + distortionNormal); + transmissionR = getIBLVolumeRefraction( + sampleNorm, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90, + pos, modelMatrix, viewMatrix, projectionMatrix, material.ior, material.thickness + thickness_smear * (i + randomCoords) / float(${samples}), + material.attenuationColor, material.attenuationDistance + ).r; + transmissionG = getIBLVolumeRefraction( + sampleNorm, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90, + pos, modelMatrix, viewMatrix, projectionMatrix, material.ior * (1.0 + chromaticAberration * (i + randomCoords) / float(${samples})) , material.thickness + thickness_smear * (i + randomCoords) / float(${samples}), + material.attenuationColor, material.attenuationDistance + ).g; + transmissionB = getIBLVolumeRefraction( + sampleNorm, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90, + pos, modelMatrix, viewMatrix, projectionMatrix, material.ior * (1.0 + 2.0 * chromaticAberration * (i + randomCoords) / float(${samples})), material.thickness + thickness_smear * (i + randomCoords) / float(${samples}), + material.attenuationColor, material.attenuationDistance + ).b; + transmission.r += transmissionR; + transmission.g += transmissionG; + transmission.b += transmissionB; + } + transmission /= ${samples}.0; + totalDiffuse = mix( totalDiffuse, transmission.rgb, material.transmission );\n` + ); + }; + + Object.keys(this.uniforms).forEach((name) => + Object.defineProperty(this, name, { + get: () => (this.uniforms as NgtAnyRecord)[name].value, + set: (v) => ((this.uniforms as NgtAnyRecord)[name].value = v), + }) + ); + } +} diff --git a/libs/angular-three-soba/src/shaders/mesh-refraction-material.stories.ts b/libs/angular-three-soba/src/shaders/mesh-refraction-material.stories.ts new file mode 100644 index 0000000..b0ab025 --- /dev/null +++ b/libs/angular-three-soba/src/shaders/mesh-refraction-material.stories.ts @@ -0,0 +1,170 @@ +import { NgIf } from '@angular/common'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { Meta, moduleMetadata, StoryFn } from '@storybook/angular'; +import { extend, injectNgtLoader, NgtArgs, NgtPush } from 'angular-three'; +import { NgtsCameraContent, NgtsCubeCamera } from 'angular-three-soba/cameras'; +import { NgtsOrbitControls } from 'angular-three-soba/controls'; +import { injectNgtsGLTFLoader } from 'angular-three-soba/loaders'; +import { NgtsMeshRefractionMaterial, NgtsMeshTranmissionMaterial } from 'angular-three-soba/materials'; +import { + NgtsAccumulativeShadows, + NgtsCaustics, + NgtsEnvironment, + NgtsRandomizedLights, +} from 'angular-three-soba/staging'; +import { map } from 'rxjs'; +import { AmbientLight, Color, Mesh, MeshStandardMaterial, PointLight, SphereGeometry, SpotLight } from 'three'; +import { RGBELoader } from 'three-stdlib'; +import { makeCanvasOptions, StorybookSetup } from '../setup-canvas'; + +extend({ Mesh, Color, AmbientLight, SpotLight, PointLight, SphereGeometry, MeshStandardMaterial }); + +@Component({ + selector: 'Diamond', + standalone: true, + template: ` + + + + + + + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [NgtsCubeCamera, NgtsCaustics, NgtsCameraContent, NgtsMeshRefractionMaterial, NgtPush, NgIf], +}) +class Diamond { + readonly cameraTexture$ = injectNgtLoader( + () => RGBELoader, + 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/aerodynamics_workshop_1k.hdr' + ); + readonly diamondGeometry$ = injectNgtsGLTFLoader('soba/dflat.glb').pipe( + map((gltf) => (gltf.nodes['Diamond_1_0'] as THREE.Mesh).geometry) + ); +} + +@Component({ + standalone: true, + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + imports: [ + Diamond, + NgtArgs, + NgtsMeshTranmissionMaterial, + NgtsAccumulativeShadows, + NgtsRandomizedLights, + NgtsEnvironment, + NgtsOrbitControls, + NgtsCaustics, + ], + schemas: [NO_ERRORS_SCHEMA], +}) +class DefaultMeshRefractionMaterialStory { + readonly Math = Math; +} + +export default { + title: 'Shaders/MeshRefractionMaterial', + decorators: [moduleMetadata({ imports: [StorybookSetup] })], +} as Meta; + +export const Default: StoryFn = () => ({ + props: { + story: DefaultMeshRefractionMaterialStory, + options: makeCanvasOptions({ + camera: { fov: 45, position: [-5, 0.5, 5] }, + compoundPrefixes: ['Diamond'], + }), + }, + template: ` + + `, +}); diff --git a/libs/angular-three-soba/staging/src/index.ts b/libs/angular-three-soba/staging/src/index.ts index b257943..db1f219 100644 --- a/libs/angular-three-soba/staging/src/index.ts +++ b/libs/angular-three-soba/staging/src/index.ts @@ -2,6 +2,7 @@ export { NgtsAccumulativeShadows } from './lib/accumulative-shadows/accumulative export { NgtsRandomizedLights } from './lib/accumulative-shadows/randomized-lights'; export * from './lib/bounds/bounds'; export * from './lib/camera-shake/camera-shake'; +export * from './lib/caustics/caustics'; export * from './lib/center/center'; export * from './lib/cloud/cloud'; export * from './lib/contact-shadows/contact-shadows'; diff --git a/libs/angular-three-soba/staging/src/lib/accumulative-shadows/progressive-light-map.ts b/libs/angular-three-soba/staging/src/lib/accumulative-shadows/progressive-light-map.ts index 9c257fe..1077fa1 100644 --- a/libs/angular-three-soba/staging/src/lib/accumulative-shadows/progressive-light-map.ts +++ b/libs/angular-three-soba/staging/src/lib/accumulative-shadows/progressive-light-map.ts @@ -1,4 +1,4 @@ -import { shaderMaterial } from 'angular-three-soba/shaders'; +import { DiscardMaterial } from 'angular-three-soba/shaders'; import * as THREE from 'three'; function isLight(object: any): object is THREE.Light { @@ -9,12 +9,6 @@ function isGeometry(object: any): object is THREE.Mesh { return !!object.geometry; } -const DiscardMaterial = shaderMaterial( - {}, - 'void main() { gl_Position = vec4((uv - 0.5) * 2.0, 1.0, 1.0); }', - 'void main() { discard; }' -); - export class ProgressiveLightMap { renderer: THREE.WebGLRenderer; res: number; diff --git a/libs/angular-three-soba/staging/src/lib/caustics/caustics.ts b/libs/angular-three-soba/staging/src/lib/caustics/caustics.ts new file mode 100644 index 0000000..ee5ca7b --- /dev/null +++ b/libs/angular-three-soba/staging/src/lib/caustics/caustics.ts @@ -0,0 +1,431 @@ +import { NgIf } from '@angular/common'; +import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, inject, Input, OnInit } from '@angular/core'; +import { extend, injectNgtRef, NgtRxStore, NgtStore } from 'angular-three'; +import { NgtsEdges } from 'angular-three-soba/abstractions'; +import { injectNgtsFBO } from 'angular-three-soba/misc'; +import { CausticsMaterial, CausticsProjectionMaterial } from 'angular-three-soba/shaders'; +import { combineLatest, map } from 'rxjs'; +import * as THREE from 'three'; +import { Group, LineBasicMaterial, Mesh, OrthographicCamera, PlaneGeometry, Scene } from 'three'; +import { FullScreenQuad } from 'three-stdlib'; + +extend({ Group, Scene, Mesh, PlaneGeometry, OrthographicCamera, CausticsProjectionMaterial, LineBasicMaterial }); + +const NORMALPROPS = { + depth: true, + minFilter: THREE.LinearFilter, + magFilter: THREE.LinearFilter, + encoding: THREE.LinearEncoding, + type: THREE.UnsignedByteType, +}; + +const CAUSTICPROPS = { + minFilter: THREE.LinearMipmapLinearFilter, + magFilter: THREE.LinearFilter, + encoding: THREE.LinearEncoding, + format: THREE.RGBAFormat, + type: THREE.FloatType, + generateMipmaps: true, +}; + +type CausticsMaterialType = THREE.ShaderMaterial & { + cameraMatrixWorld?: THREE.Matrix4; + cameraProjectionMatrixInv?: THREE.Matrix4; + lightPlaneNormal?: THREE.Vector3; + lightPlaneConstant?: number; + normalTexture?: THREE.Texture | null; + depthTexture?: THREE.Texture | null; + lightDir?: THREE.Vector3; + near?: number; + far?: number; + modelMatrix?: THREE.Matrix4; + worldRadius?: number; + ior?: number; + bounces?: number; + resolution?: number; + size?: number; + intensity?: number; +}; + +type CausticsProjectionMaterialType = THREE.MeshNormalMaterial & { + viewMatrix: { value?: THREE.Matrix4 }; + color?: THREE.Color; + causticsTexture?: THREE.Texture; + causticsTextureB?: THREE.Texture; + lightProjMatrix?: THREE.Matrix4; + lightViewMatrix?: THREE.Matrix4; +}; + +function createNormalMaterial(side = THREE.FrontSide) { + const viewMatrix = { value: new THREE.Matrix4() }; + return Object.assign(new THREE.MeshNormalMaterial({ side }), { + viewMatrix, + onBeforeCompile: (shader: any) => { + shader.uniforms.viewMatrix = viewMatrix; + shader.fragmentShader = + `vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz ); + }\n` + + shader.fragmentShader.replace( + '#include ', + `#include + normal = inverseTransformDirection( normal, viewMatrix );\n` + ); + }, + }); +} + +@Component({ + selector: 'ngts-caustics', + standalone: true, + template: ` + + + + + + + + + + + + + + + `, + imports: [NgIf, NgtsEdges], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class NgtsCaustics extends NgtRxStore implements OnInit { + readonly CustomBlending = THREE.CustomBlending; + readonly OneFactor = THREE.OneFactor; + readonly SrcAlphaFactor = THREE.SrcAlphaFactor; + readonly Math = Math; + + readonly planeRef = injectNgtRef(); + readonly sceneRef = injectNgtRef(); + readonly cameraRef = injectNgtRef(); + + @Input() causticsRef = injectNgtRef(); + + /** How many frames it will render, set it to Infinity for runtime, default: 1 */ + @Input() set frames(frames: number) { + this.set({ frames }); + } + /** Enables visual cues to help you stage your scene, default: false */ + @Input() set debug(debug: boolean) { + this.set({ debug }); + } + /** Will display caustics only and skip the models, default: false */ + @Input() set causticsOnly(causticsOnly: boolean) { + this.set({ causticsOnly }); + } + /** Will include back faces and enable the backsideIOR prop, default: false */ + @Input() set backside(backside: boolean) { + this.set({ backside }); + } + /** The IOR refraction index, default: 1.1 */ + @Input() set ior(ior: number) { + this.set({ ior }); + } + /** The IOR refraction index for back faces (only available when backside is enabled), default: 1.1 */ + @Input() set backsideIOR(backsideIOR: number) { + this.set({ backsideIOR }); + } + /** The texel size, default: 0.3125 */ + @Input() set worldRadius(worldRadius: number) { + this.set({ worldRadius }); + } + /** Intensity of the prjected caustics, default: 0.05 */ + @Input() set intensity(intensity: number) { + this.set({ intensity }); + } + /** Caustics color, default: white */ + @Input() set color(color: THREE.ColorRepresentation) { + this.set({ color }); + } + /** Buffer resolution, default: 2048 */ + @Input() set resolution(resolution: number) { + this.set({ resolution }); + } + /** Camera position, it will point towards the contents bounds center, default: [5, 5, 5] */ + @Input() set lightSource(lightSource: [x: number, y: number, z: number] | ElementRef) { + this.set({ lightSource }); + } + + readonly normalTargetFbo = injectNgtsFBO(() => + this.select('resolution').pipe( + map((resolution) => ({ width: resolution, height: resolution, settings: NORMALPROPS })) + ) + ); + + readonly normalTargetBFbo = injectNgtsFBO(() => + this.select('resolution').pipe( + map((resolution) => ({ width: resolution, height: resolution, settings: NORMALPROPS })) + ) + ); + + readonly causticsTargetFbo = injectNgtsFBO(() => + this.select('resolution').pipe( + map((resolution) => ({ width: resolution, height: resolution, settings: CAUSTICPROPS })) + ) + ); + + readonly causticsTargetBFbo = injectNgtsFBO(() => + this.select('resolution').pipe( + map((resolution) => ({ width: resolution, height: resolution, settings: CAUSTICPROPS })) + ) + ); + + private readonly store = inject(NgtStore); + private readonly cdr = inject(ChangeDetectorRef); + + override initialize(): void { + this.set({ + frames: 1, + ior: 1.1, + color: 'white', + causticsOnly: false, + backside: false, + backsideIOR: 1.1, + worldRadius: 0.3125, + intensity: 0.05, + resolution: 2024, + lightSource: [5, 5, 5], + }); + } + + ngOnInit() { + this.updateWorldMatrix(); + this.setBeforeRender(); + } + + private updateWorldMatrix() { + this.hold( + combineLatest([this.sceneRef.children$(), this.causticsRef.$, this.causticsRef.children$(), this.select()]), + () => { + if (this.causticsRef.nativeElement) { + this.causticsRef.nativeElement.updateWorldMatrix(false, true); + } + } + ); + } + + private setBeforeRender() { + const causticsMaterial = new CausticsMaterial() as CausticsMaterialType; + const causticsQuad = new FullScreenQuad(causticsMaterial); + + const normalMaterial = createNormalMaterial(); + const normalMaterialB = createNormalMaterial(THREE.BackSide); + + this.effect( + combineLatest([ + this.sceneRef.$, + this.sceneRef.children$('both'), + this.causticsRef.$, + this.cameraRef.$, + this.planeRef.$, + this.planeRef.children$('both'), + this.normalTargetFbo.$, + this.normalTargetBFbo.$, + this.causticsTargetFbo.$, + this.causticsTargetBFbo.$, + ]), + ([ + scene, + children, + caustics, + camera, + plane, + , + normalTarget, + normalTargetB, + causticsTarget, + causticsTargetB, + ]) => { + const v = new THREE.Vector3(); + const lpF = new THREE.Frustum(); + const lpM = new THREE.Matrix4(); + const lpP = new THREE.Plane(); + + const lightDir = new THREE.Vector3(); + const lightDirInv = new THREE.Vector3(); + const bounds = new THREE.Box3(); + const focusPos = new THREE.Vector3(); + + let count = 0; + + caustics.updateWorldMatrix(false, true); + + if (children.length > 1) { + return this.store.get('internal').subscribe(({ gl }) => { + const { + frames, + lightSource, + resolution, + worldRadius, + intensity, + backside, + backsideIOR, + ior, + causticsOnly, + debug, + } = this.get(); + + if (frames === Infinity || count++ < frames) { + if (Array.isArray(lightSource)) lightDir.fromArray(lightSource).normalize(); + else + lightDir.copy( + caustics.worldToLocal(lightSource.nativeElement.getWorldPosition(v)).normalize() + ); + + lightDirInv.copy(lightDir).multiplyScalar(-1); + + let boundsVertices: THREE.Vector3[] = []; + scene.parent?.matrixWorld.identity(); + bounds.setFromObject(scene, true); + boundsVertices.push(new THREE.Vector3(bounds.min.x, bounds.min.y, bounds.min.z)); + boundsVertices.push(new THREE.Vector3(bounds.min.x, bounds.min.y, bounds.max.z)); + boundsVertices.push(new THREE.Vector3(bounds.min.x, bounds.max.y, bounds.min.z)); + boundsVertices.push(new THREE.Vector3(bounds.min.x, bounds.max.y, bounds.max.z)); + boundsVertices.push(new THREE.Vector3(bounds.max.x, bounds.min.y, bounds.min.z)); + boundsVertices.push(new THREE.Vector3(bounds.max.x, bounds.min.y, bounds.max.z)); + boundsVertices.push(new THREE.Vector3(bounds.max.x, bounds.max.y, bounds.min.z)); + boundsVertices.push(new THREE.Vector3(bounds.max.x, bounds.max.y, bounds.max.z)); + + const worldVerts = boundsVertices.map((v) => v.clone()); + + bounds.getCenter(focusPos); + boundsVertices = boundsVertices.map((v) => v.clone().sub(focusPos)); + const lightPlane = lpP.set(lightDirInv, 0); + const projectedVerts = boundsVertices.map((v) => + lightPlane.projectPoint(v, new THREE.Vector3()) + ); + + const centralVert = projectedVerts + .reduce((a, b) => a.add(b), v.set(0, 0, 0)) + .divideScalar(projectedVerts.length); + const radius = projectedVerts + .map((v) => v.distanceTo(centralVert)) + .reduce((a, b) => Math.max(a, b)); + const dirLength = boundsVertices + .map((x) => x.dot(lightDir)) + .reduce((a, b) => Math.max(a, b)); + + // Shadows + camera.position.copy(lightDir.clone().multiplyScalar(dirLength).add(focusPos)); + camera.lookAt(scene.localToWorld(focusPos.clone())); + const dirMatrix = lpM.lookAt(camera.position, focusPos, v.set(0, 1, 0)); + camera.left = -radius; + camera.right = radius; + camera.top = radius; + camera.bottom = -radius; + const yOffset = v.set(0, radius, 0).applyMatrix4(dirMatrix); + const yTime = (camera.position.y + yOffset.y) / lightDir.y; + camera.near = 0.1; + camera.far = yTime; + camera.updateProjectionMatrix(); + camera.updateMatrixWorld(); + + // Now find size of ground plane + const groundProjectedCoords = worldVerts.map((v) => + v.add(lightDir.clone().multiplyScalar(-v.y / lightDir.y)) + ); + const centerPos = groundProjectedCoords + .reduce((a, b) => a.add(b), v.set(0, 0, 0)) + .divideScalar(groundProjectedCoords.length); + const maxSize = + 2 * + groundProjectedCoords + .map((v) => Math.hypot(v.x - centerPos.x, v.z - centerPos.z)) + .reduce((a, b) => Math.max(a, b)); + plane.scale.setScalar(maxSize); + plane.position.copy(centerPos); + + // if (debug) helper.current?.update(); + + // Inject uniforms + normalMaterialB.viewMatrix.value = normalMaterial.viewMatrix.value = + camera.matrixWorldInverse; + + const dirLightNearPlane = lpF.setFromProjectionMatrix( + lpM.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse) + ).planes[4]; + + causticsMaterial.cameraMatrixWorld = camera.matrixWorld; + causticsMaterial.cameraProjectionMatrixInv = camera.projectionMatrixInverse; + causticsMaterial.lightDir = lightDirInv; + + causticsMaterial.lightPlaneNormal = dirLightNearPlane.normal; + causticsMaterial.lightPlaneConstant = dirLightNearPlane.constant; + + causticsMaterial.near = camera.near; + causticsMaterial.far = camera.far; + causticsMaterial.resolution = resolution; + causticsMaterial.size = radius; + causticsMaterial.intensity = intensity; + causticsMaterial.worldRadius = worldRadius; + + // Switch the scene on + scene.visible = true; + + // Render front face normals + gl.setRenderTarget(normalTarget); + gl.clear(); + scene.overrideMaterial = normalMaterial; + gl.render(scene, camera); + + // Render back face normals, if enabled + gl.setRenderTarget(normalTargetB); + gl.clear(); + if (backside) { + scene.overrideMaterial = normalMaterialB; + gl.render(scene, camera); + } + + // Remove the override material + scene.overrideMaterial = null; + + // Render front face caustics + causticsMaterial.ior = ior; + // @ts-ignore + plane.material.lightProjMatrix = camera.projectionMatrix; + // @ts-ignore + plane.material.lightViewMatrix = camera.matrixWorldInverse; + causticsMaterial.normalTexture = normalTarget.texture; + causticsMaterial.depthTexture = normalTarget.depthTexture; + gl.setRenderTarget(causticsTarget); + gl.clear(); + causticsQuad.render(gl); + + // Render back face caustics, if enabled + causticsMaterial.ior = backsideIOR; + causticsMaterial.normalTexture = normalTargetB.texture; + causticsMaterial.depthTexture = normalTargetB.depthTexture; + gl.setRenderTarget(causticsTargetB); + gl.clear(); + if (backside) causticsQuad.render(gl); + + // Reset render target + gl.setRenderTarget(null); + + // Switch the scene off if caustics is all that's wanted + if (causticsOnly) scene.visible = false; + } + }); + } + } + ); + } +}