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 0000000..0172e84
Binary files /dev/null and b/libs/angular-three-soba/.storybook/public/soba/dflat.glb differ
diff --git a/libs/angular-three-soba/abstractions/src/index.ts b/libs/angular-three-soba/abstractions/src/index.ts
index c4b6b94..0771f31 100644
--- a/libs/angular-three-soba/abstractions/src/index.ts
+++ b/libs/angular-three-soba/abstractions/src/index.ts
@@ -1,6 +1,7 @@
export * from './lib/billboard/billboard';
export * from './lib/catmull-rom-line/catmull-rom-line';
export * from './lib/cubic-bezier-line/cubic-bezier-line';
+export * from './lib/edges/edges';
export * from './lib/gizmo-helper/gizmo-helper';
export * from './lib/gizmo-helper/gizmo-viewcube/gizmo-viewcube';
export * from './lib/gizmo-helper/gizmo-viewport/gizmo-viewport';
diff --git a/libs/angular-three-soba/abstractions/src/lib/edges/edges.ts b/libs/angular-three-soba/abstractions/src/lib/edges/edges.ts
new file mode 100644
index 0000000..242b752
--- /dev/null
+++ b/libs/angular-three-soba/abstractions/src/lib/edges/edges.ts
@@ -0,0 +1,75 @@
+import { NgIf } from '@angular/common';
+import { Component, CUSTOM_ELEMENTS_SCHEMA, Input, OnInit } from '@angular/core';
+import { extend, injectNgtRef, NgtAnyRecord, NgtRxStore } from 'angular-three';
+import * as THREE from 'three';
+import { LineBasicMaterial, LineSegments } from 'three';
+
+extend({ LineSegments, LineBasicMaterial });
+
+@Component({
+ selector: 'ngts-edges',
+ standalone: true,
+ template: `
+
+
+
+
+
+
+
+
+ `,
+ 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;
+ }
+ });
+ }
+ }
+ );
+ }
+}