diff --git a/example/aoBake.html b/example/aoBake.html
new file mode 100644
index 000000000..2b6fc5159
--- /dev/null
+++ b/example/aoBake.html
@@ -0,0 +1,54 @@
+
+
+ Baked Path Traced Ambient Occlusion + Thickness
+
+
+
+
+
+
+ LOADING
+
+
+
+
diff --git a/example/aoBake.js b/example/aoBake.js
new file mode 100644
index 000000000..8e07937a8
--- /dev/null
+++ b/example/aoBake.js
@@ -0,0 +1,259 @@
+import {
+ WebGLRenderer,
+ PerspectiveCamera,
+ Scene,
+ PointLight,
+ AmbientLight,
+ WebGLRenderTarget,
+ FloatType,
+ LinearSRGBColorSpace,
+ RGBAFormat,
+ Group,
+ Box3,
+ Sphere,
+ MeshPhysicalMaterial,
+ EquirectangularReflectionMapping,
+ MeshBasicMaterial
+} from 'three';
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
+import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
+import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
+import Stats from 'three/examples/jsm/libs/stats.module.js';
+import { UVGenerator } from '../src/utils/UVGenerator.js';
+import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
+import { AOThicknessMapGenerator } from '../src/utils/AOThicknessMapGenerator.js';
+
+const ENV_URL = 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/master/hdri/aristea_wreck_puresky_2k.hdr';
+
+let renderer, camera, scene, stats;
+let statusEl, totalSamples = 0;
+let aoGenerator, aoTexture, gui, aoMaterial;
+let background;
+let quad;
+
+const params = {
+ transmission: false,
+ displayMap: false,
+};
+
+const AO_THICKNESS_TEXTURE_SIZE = 1024;
+const MAX_SAMPLES = 1000;
+
+init();
+
+async function init() {
+
+ // initialize renderer
+ renderer = new WebGLRenderer( { antialias: true } );
+ renderer.setClearColor( 0x111111 );
+ document.body.appendChild( renderer.domElement );
+
+ camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 200 );
+ camera.position.set( - 4, 2, 3 );
+
+ scene = new Scene();
+ scene.backgroundRotation.set( 0, 0.75, 0 );
+ scene.backgroundBlurriness = 0.1;
+
+ const light1 = new PointLight( 0xaaaaaa, 20, 100 );
+ light1.position.set( 3, 3, 3 );
+
+ const light2 = new PointLight( 0xaaaaaa, 20, 100 );
+ light2.position.set( - 3, - 3, - 3 );
+
+ const ambientLight = new AmbientLight( 0xffffff, 2.75 );
+ scene.add( ambientLight );
+
+ new OrbitControls( camera, renderer.domElement );
+ statusEl = document.getElementById( 'status' );
+
+ // const url = 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/models/stanford-bunny/bunny.glb';
+ const url = 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/FlightHelmet/glTF/FlightHelmet.gltf';
+
+ // init ao texture
+ const aoTarget = new WebGLRenderTarget( AO_THICKNESS_TEXTURE_SIZE, AO_THICKNESS_TEXTURE_SIZE, {
+ type: FloatType,
+ colorSpace: LinearSRGBColorSpace,
+ generateMipmaps: true,
+ format: RGBAFormat,
+ } );
+ aoTexture = aoTarget.texture;
+ aoTexture.channel = 2;
+
+ // init ao generator
+ aoGenerator = new AOThicknessMapGenerator( renderer );
+ aoGenerator.samples = MAX_SAMPLES;
+ aoGenerator.channel = 2;
+ aoGenerator.aoRadius = 2;
+ aoGenerator.thicknessRadius = 0.5;
+
+ // gltf material
+ aoMaterial = new MeshPhysicalMaterial( {
+ aoMap: aoTexture,
+ thicknessMap: aoTexture,
+ thickness: 1,
+ attenuationColor: 0xfaeef2,
+ attenuationDistance: 0.5,
+ } );
+
+ // quad for rendering texture result
+ quad = new FullScreenQuad( new MeshBasicMaterial( {
+ map: aoTexture,
+ } ) );
+
+ // uv generator
+ const uvGenerator = new UVGenerator();
+ uvGenerator.channel = 2;
+
+ const envPromise = new RGBELoader()
+ .loadAsync( ENV_URL )
+ .then( tex => {
+
+ tex.mapping = EquirectangularReflectionMapping;
+ background = tex;
+
+ } );
+
+ const geometriesToBake = [];
+ const gltfPromise = new GLTFLoader()
+ .setMeshoptDecoder( MeshoptDecoder )
+ .loadAsync( url )
+ .then( async gltf => {
+
+ const group = new Group();
+
+ // scale the scene to a reasonable size
+ const box = new Box3();
+ box.setFromObject( gltf.scene );
+
+ const sphere = new Sphere();
+ box.getBoundingSphere( sphere );
+
+ gltf.scene.scale.setScalar( 2.5 / sphere.radius );
+ gltf.scene.position.y = - 0.5 * ( box.max.y - box.min.y ) * 2.5 / sphere.radius;
+ gltf.scene.updateMatrixWorld();
+ group.add( gltf.scene );
+
+ group.traverse( c => {
+
+ if ( c.isMesh ) {
+
+ geometriesToBake.push( c.geometry );
+
+ c.material = aoMaterial;
+
+ }
+
+ } );
+
+ scene.add( group );
+
+ } );
+
+ // wait for promises
+ await Promise.all( [ gltfPromise, envPromise, uvGenerator.init() ] );
+
+ document.getElementById( 'loading' ).remove();
+
+ uvGenerator.generate( geometriesToBake, ( item, percentage ) => {
+
+ if ( percentage % 10 === 0 ) {
+
+ console.log( `UV Generation: ${ percentage } % of ${ item }` );
+
+ }
+
+ } );
+
+ aoGenerator.startGeneration( geometriesToBake, aoTarget );
+
+ onResize();
+ window.addEventListener( 'resize', onResize );
+
+ gui = new GUI();
+ gui.add( params, 'transmission' );
+ gui.add( params, 'displayMap' );
+
+ stats = new Stats();
+ document.body.appendChild( stats.domElement );
+
+ animate();
+
+}
+
+function onResize() {
+
+ const w = window.innerWidth;
+ const h = window.innerHeight;
+
+ renderer.setSize( w, h );
+ renderer.setPixelRatio( window.devicePixelRatio );
+ camera.aspect = w / h;
+ camera.updateProjectionMatrix();
+
+}
+
+function animate() {
+
+ stats.update();
+
+ requestAnimationFrame( animate );
+
+ if ( aoGenerator ) {
+
+ if ( aoGenerator.generateSample() ) {
+
+ totalSamples += aoGenerator.samplesPerUpdate;
+
+ } else {
+
+ aoGenerator = null;
+
+ }
+
+ }
+
+ if ( params.transmission ) {
+
+ aoMaterial.transmission = 1;
+ aoMaterial.color.copy( aoMaterial.attenuationColor );
+ aoMaterial.color.r *= 0.75;
+ aoMaterial.color.g *= 0.5;
+ aoMaterial.color.b *= 0.5;
+ aoMaterial.roughness = 0.25;
+
+ scene.background = background;
+
+ } else {
+
+ aoMaterial.transmission = 0;
+ aoMaterial.color.set( 0xffffff );
+ aoMaterial.roughness = 1;
+
+ scene.background = null;
+
+ }
+
+ renderer.setRenderTarget( null );
+
+ if ( params.displayMap ) {
+
+ aoTexture.channel = 0;
+ quad.render( renderer );
+
+ } else {
+
+ aoTexture.channel = 2;
+ renderer.render( scene, camera );
+
+ }
+
+ if ( aoGenerator ) {
+
+ statusEl.innerText = `Samples: ${ totalSamples } of ${ MAX_SAMPLES }`;
+
+ }
+
+}
diff --git a/example/aoRender.js b/example/aoRender.js
index 1cce00e2b..0adbbd876 100644
--- a/example/aoRender.js
+++ b/example/aoRender.js
@@ -5,7 +5,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { PathTracingSceneGenerator } from '../src/core/PathTracingSceneGenerator.js';
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
-import { AmbientOcclusionMaterial } from '../src/materials/surface/AmbientOcclusionMaterial.js';
+import { AOThicknessMaterial, AOThicknessMode } from '../src/materials/surface/AOThicknessMaterial.js';
import Stats from 'three/examples/jsm/libs/stats.module.js';
import { MeshBVHUniformStruct } from 'three-mesh-bvh';
import * as MikkTSpace from 'three/examples/jsm/libs/mikktspace.module.js';
@@ -136,8 +136,8 @@ async function init() {
const normalMap = c.material.normalMap;
if ( ! materialMap.has( normalMap ) ) {
- const material = new AmbientOcclusionMaterial( {
-
+ const material = new AOThicknessMaterial( {
+ mode: AOThicknessMode.AO_ONLY,
bvh: bvhUniform,
normalScale: c.material.normalScale,
normalMap,
diff --git a/package-lock.json b/package-lock.json
index c204ba5f7..46970ee36 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,9 +31,9 @@
"yargs": "^17.5.1"
},
"peerDependencies": {
+ "@gfodor/xatlas-web": "^1.3.1",
"three": ">=0.151.0",
- "three-mesh-bvh": ">=0.7.4",
- "xatlas-web": "^0.1.0"
+ "three-mesh-bvh": ">=0.7.4"
}
},
"node_modules/@babel/code-frame": {
@@ -194,6 +194,12 @@
}
}
},
+ "node_modules/@gfodor/xatlas-web": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@gfodor/xatlas-web/-/xatlas-web-1.3.1.tgz",
+ "integrity": "sha512-T4U5oPlRWQDpQN9shPgFKV/d3GqXhTCUV5L3NOi2l3Zvo8yvWrghLpd/eyKA7ZAV5jpBq6bCmbTQGiySFqAMzA==",
+ "peer": true
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
@@ -5826,12 +5832,6 @@
}
}
},
- "node_modules/xatlas-web": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/xatlas-web/-/xatlas-web-0.1.0.tgz",
- "integrity": "sha512-PprVfuXbaIskxLTLBUQRaWfgSy9xUQqAMIRooOw0P6NYqwgh6T0voeer6+Z5M7AFt5SGXUybuww/uDGs1yw8vQ==",
- "peer": true
- },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -6010,6 +6010,12 @@
}
}
},
+ "@gfodor/xatlas-web": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@gfodor/xatlas-web/-/xatlas-web-1.3.1.tgz",
+ "integrity": "sha512-T4U5oPlRWQDpQN9shPgFKV/d3GqXhTCUV5L3NOi2l3Zvo8yvWrghLpd/eyKA7ZAV5jpBq6bCmbTQGiySFqAMzA==",
+ "peer": true
+ },
"@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
@@ -9799,12 +9805,6 @@
"dev": true,
"requires": {}
},
- "xatlas-web": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/xatlas-web/-/xatlas-web-0.1.0.tgz",
- "integrity": "sha512-PprVfuXbaIskxLTLBUQRaWfgSy9xUQqAMIRooOw0P6NYqwgh6T0voeer6+Z5M7AFt5SGXUybuww/uDGs1yw8vQ==",
- "peer": true
- },
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 294534ce5..0c5dc705b 100644
--- a/package.json
+++ b/package.json
@@ -52,9 +52,9 @@
"yargs": "^17.5.1"
},
"peerDependencies": {
+ "@gfodor/xatlas-web": "^1.3.1",
"three": ">=0.151.0",
- "three-mesh-bvh": ">=0.7.4",
- "xatlas-web": "^0.1.0"
+ "three-mesh-bvh": ">=0.7.4"
},
"scripts": {
"start": "cd example && parcel serve ./*.html --dist-dir ./dev-bundle/ --no-cache --no-hmr",
diff --git a/src/index.d.ts b/src/index.d.ts
index 26c119e95..9c1ddf28f 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -273,3 +273,34 @@ export class FogVolumeMaterial extends MeshStandardMaterial {
density: number;
}
+
+// utils
+
+export class AOThicknessMapGenerator {
+
+ constructor( renderer: WebGLRenderer );
+
+ startGeneration( geometries: Array, renderTarget: WebGLRenderTarget ): void;
+
+ generateSample(): boolean;
+
+ channel: number;
+ samples: number;
+ flood: boolean;
+ mode: number;
+ aoRadius: number;
+ thicknessRadius: number;
+
+}
+
+export class UVGenerator {
+
+ constructor();
+
+ init(): Promise;
+
+ generate( geometries: Array, onProgress?: ( progress: number ) => void ): Promise;
+
+ channel: number;
+
+}
diff --git a/src/index.js b/src/index.js
index a317c9603..fd380dc3b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -14,6 +14,8 @@ export * from './textures/GradientEquirectTexture.js';
// utils
export * from './utils/BlurredEnvMapGenerator.js';
+export * from './utils/AOThicknessMapGenerator.js';
+export * from './utils/UVGenerator.js';
// materials
export * from './materials/fullscreen/DenoiseMaterial.js';
diff --git a/src/materials/surface/AOThicknessMaterial.js b/src/materials/surface/AOThicknessMaterial.js
new file mode 100644
index 000000000..aa09029cd
--- /dev/null
+++ b/src/materials/surface/AOThicknessMaterial.js
@@ -0,0 +1,332 @@
+import { TangentSpaceNormalMap, Vector2, Matrix4 } from 'three'; import { MaterialBase } from '../MaterialBase.js';
+import { MeshBVHUniformStruct, BVHShaderGLSL } from 'three-mesh-bvh';
+import { MATERIAL_PIXELS } from '../../uniforms/MaterialsTexture.js';
+
+import * as StructsGLSL from '../../shader/structs/index.js';
+import * as SamplingGLSL from '../../shader/sampling/index.js';
+import * as RandomGLSL from '../../shader/rand/index.js';
+import * as CommonGLSL from '../../shader/common/index.js';
+
+export const AOThicknessMode = {
+ AO_AND_THICKNESS: 1,
+ AO_ONLY: 2
+};
+
+export class AOThicknessMaterial extends MaterialBase {
+
+ get normalMap() {
+
+ return this.uniforms.normalMap.value;
+
+ }
+
+ set normalMap( v ) {
+
+ this.uniforms.normalMap.value = v;
+ this.setDefine( 'USE_NORMALMAP', v ? null : '' );
+
+ }
+
+ get normalMapType() {
+
+ return TangentSpaceNormalMap;
+
+ }
+
+ set normalMapType( v ) {
+
+ if ( v !== TangentSpaceNormalMap ) {
+
+ throw new Error( 'AOThicknessMaterial: Only tangent space normal map are supported' );
+
+ }
+
+ }
+
+ set samples( v ) {
+
+ this.setDefine( 'SAMPLES', Number( v ) || 1 );
+
+ }
+
+ get samples() {
+
+ return this.defines.SAMPLES;
+
+ }
+
+ constructor( parameters ) {
+
+ super( {
+ defines: {
+ SAMPLES: 1,
+ MATERIAL_PIXELS: MATERIAL_PIXELS
+ },
+
+ uniforms: {
+ bvh: { value: new MeshBVHUniformStruct() },
+ aoRadius: { value: 1.0 },
+ thicknessRadius: { value: 1.0 },
+ seed: { value: 0 },
+ resolution: { value: new Vector2() },
+
+ normalMap: { value: null },
+ normalScale: { value: new Vector2( 1, 1 ) },
+
+ // Used for baking
+ uvToTriangleMap: { value: null },
+ objectModelMatrix: { value: new Matrix4() },
+ },
+
+ vertexShader: /* glsl */`
+
+ #if ! defined( USE_UV_TRIANGLE_MAP )
+
+ varying vec3 vNorm;
+ varying vec3 vPos;
+
+ #if defined( USE_NORMALMAP ) && defined( USE_TANGENT )
+
+ varying vec4 vTan;
+
+ #endif
+
+ #endif
+
+ varying vec2 vUv;
+
+ void main() {
+
+ vec4 mvPosition = vec4( position, 1.0 );
+ mvPosition = modelViewMatrix * mvPosition;
+ gl_Position = projectionMatrix * mvPosition;
+
+ #if defined( USE_UV_TRIANGLE_MAP )
+
+ vUv = uv;
+
+ #else
+
+ mat3 modelNormalMatrix = transpose( inverse( mat3( modelMatrix ) ) );
+ vNorm = normalize( modelNormalMatrix * normal );
+ vPos = ( modelMatrix * vec4( position, 1.0 ) ).xyz;
+
+ #if defined( USE_NORMALMAP ) && defined( USE_TANGENT )
+
+ vUv = uv;
+ vTan = tangent;
+
+ #endif
+
+ #endif
+
+ }
+
+ `,
+
+ fragmentShader: /* glsl */`
+ #define RAY_OFFSET 1e-4
+
+ precision highp isampler2D;
+ precision highp usampler2D;
+ precision highp sampler2DArray;
+ precision highp sampler2D;
+ #include
+ #include
+
+ // bvh
+ ${ BVHShaderGLSL.common_functions }
+ ${ BVHShaderGLSL.bvh_struct_definitions }
+ ${ BVHShaderGLSL.bvh_ray_functions }
+
+ // uniform structs
+ ${ StructsGLSL.material_struct }
+
+ // random
+ ${ RandomGLSL.pcg_functions }
+ #define rand(v) pcgRand()
+ #define rand2(v) pcgRand2()
+ #define rand3(v) pcgRand3()
+ #define rand4(v) pcgRand4()
+
+ // common
+ ${ SamplingGLSL.shape_sampling_functions }
+ ${ CommonGLSL.util_functions }
+
+ uniform BVH bvh;
+ uniform sampler2D uvToTriangleMap;
+ uniform int seed;
+ uniform float aoRadius;
+ uniform float thicknessRadius;
+
+ uniform vec2 resolution;
+
+ #if defined( USE_UV_TRIANGLE_MAP )
+
+ uniform mat4 objectModelMatrix;
+ varying vec2 vUv;
+
+ #else
+
+ varying vec3 vNorm;
+ varying vec3 vPos;
+
+ #if defined( USE_NORMALMAP ) && defined( USE_TANGENT )
+
+ uniform sampler2D normalMap;
+ uniform vec2 normalScale;
+ varying vec2 vUv;
+ varying vec4 vTan;
+
+ #endif
+
+ #endif
+
+
+ float accumulateOcclusion(vec3 pos, vec3 normal, float radius) {
+ float accumulated = 0.0;
+ vec3 faceNormal;
+
+ // find the max component to scale the offset to account for floating point error
+ vec3 absPoint = abs( pos );
+ float maxPoint = max( absPoint.x, max( absPoint.y, absPoint.z ) );
+
+ #if defined( USE_UV_TRIANGLE_MAP )
+
+ faceNormal = normal;
+
+ #else
+
+ vec3 fdx = vec3( dFdx( pos.x ), dFdx( pos.y ), dFdx( pos.z ) );
+ vec3 fdy = vec3( dFdy( pos.x ), dFdy( pos.y ), dFdy( pos.z ) );
+ faceNormal = normalize( cross( fdx, fdy ) );
+
+ #if defined( USE_NORMALMAP ) && defined( USE_TANGENT )
+
+ // some provided tangents can be malformed (0, 0, 0) causing the normal to be degenerate
+ // resulting in NaNs and slow path tracing.
+ if ( length( vTan.xyz ) > 0.0 ) {
+
+ vec2 uv = vUv;
+ vec3 tangent = normalize( vTan.xyz );
+ vec3 bitangent = normalize( cross( normal, tangent ) * vTan.w );
+ mat3 vTBN = mat3( tangent, bitangent, normal );
+
+ vec3 texNormal = texture2D( normalMap, uv ).xyz * 2.0 - 1.0;
+ texNormal.xy *= normalScale;
+ normal = vTBN * texNormal;
+
+ }
+
+ #endif
+ #endif
+
+ normal *= gl_FrontFacing ? 1.0 : - 1.0;
+
+ vec3 rayOrigin = pos + faceNormal * ( maxPoint + 1.0 ) * RAY_OFFSET;
+ for ( int i = 0; i < SAMPLES; i ++ ) {
+
+ // sample the cosine weighted hemisphere and discard the sample if it's below
+ // the geometric surface
+ vec3 rayDirection = sampleHemisphere( normalize( normal ), pcgRand4().xy );
+
+ // check if we hit the mesh and its within the specified radius
+ float side = 1.0;
+ float dist = 0.0;
+ vec3 barycoord = vec3( 0.0 );
+ vec3 outNormal = vec3( 0.0 );
+ uvec4 faceIndices = uvec4( 0u );
+
+ // if the ray is above the geometry surface, and it doesn't hit another surface within the specified radius then
+ // we consider it lit
+ if (
+ dot( rayDirection, faceNormal ) > 0.0 &&
+ (
+ ! bvhIntersectFirstHit( bvh, rayOrigin, rayDirection, faceIndices, outNormal, barycoord, side, dist ) ||
+ dist >= radius
+ )
+ ) {
+
+ accumulated += 1.0;
+
+ }
+
+ }
+
+ return accumulated;
+ }
+
+ vec2 getUVTexelCoord(vec2 uv, vec2 texelSize, int offset) {
+ return uv + (vec2(offset, 0) * texelSize);
+ }
+
+ void main() {
+
+ float accumulated = 0.0;
+ rng_initialize( gl_FragCoord.xy, seed );
+ vec3 pos;
+ vec3 norm;
+
+ #if defined( USE_UV_TRIANGLE_MAP )
+ mat3 modelNormalMatrix = transpose( inverse( mat3( objectModelMatrix ) ) );
+ ivec2 posUv = ivec2(vUv * resolution - (0.5 / resolution));
+ ivec2 normUv = posUv;
+ normUv.y += int(resolution.y);
+
+ vec4 posTexel = texelFetch( uvToTriangleMap, posUv, 0);
+ pos = posTexel.rgb;
+ norm = texelFetch( uvToTriangleMap, normUv, 0 ).rgb;
+
+ pos = (objectModelMatrix * vec4( pos, 1.0 )).xyz;
+ norm = normalize( modelNormalMatrix * norm);
+ gl_FragColor.a = posTexel.a;
+
+ #else
+
+ pos = vPos;
+ norm = vNorm;
+ gl_FragColor.a = 1.0;
+
+
+ #endif
+
+ float ao = accumulateOcclusion( pos, norm, aoRadius );
+
+ #if defined ( GENERATE_THICKNESS )
+
+ float thickness = accumulateOcclusion( pos, - norm, thicknessRadius );
+
+ gl_FragColor.r = ao / float( SAMPLES );
+ gl_FragColor.g = thickness / float( SAMPLES );
+ gl_FragColor.b = 0.0;
+
+ #else
+
+ gl_FragColor.rgb = vec3( ao / float( SAMPLES ) );
+
+ #endif
+ }
+
+ `
+
+ } );
+
+ if ( parameters.uvToTriangleMap ) {
+
+ this.setDefine( 'USE_UV_TRIANGLE_MAP', '' );
+
+ }
+
+ if ( parameters.mode !== AOThicknessMode.AO_ONLY ) {
+
+ this.setDefine( 'GENERATE_THICKNESS', '' );
+
+ }
+
+ delete parameters.mode;
+
+ this.setValues( parameters );
+
+ }
+
+}
diff --git a/src/materials/surface/AmbientOcclusionMaterial.js b/src/materials/surface/AmbientOcclusionMaterial.js
deleted file mode 100644
index 64f1bb55d..000000000
--- a/src/materials/surface/AmbientOcclusionMaterial.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import { TangentSpaceNormalMap, Vector2 } from 'three';
-import { MaterialBase } from '../MaterialBase.js';
-import { MeshBVHUniformStruct, BVHShaderGLSL } from 'three-mesh-bvh';
-
-import * as StructsGLSL from '../../shader/structs/index.js';
-import * as SamplingGLSL from '../../shader/sampling/index.js';
-import * as RandomGLSL from '../../shader/rand/index.js';
-
-export class AmbientOcclusionMaterial extends MaterialBase {
-
- get normalMap() {
-
- return this.uniforms.normalMap.value;
-
- }
-
- set normalMap( v ) {
-
- this.uniforms.normalMap.value = v;
- this.setDefine( 'USE_NORMALMAP', v ? null : '' );
-
- }
-
- get normalMapType() {
-
- return TangentSpaceNormalMap;
-
- }
-
- set normalMapType( v ) {
-
- if ( v !== TangentSpaceNormalMap ) {
-
- throw new Error( 'AmbientOcclusionMaterial: Only tangent space normal map are supported' );
-
- }
-
- }
-
- constructor( parameters ) {
-
- super( {
-
- defines: {
- SAMPLES: 10,
- },
-
- uniforms: {
- bvh: { value: new MeshBVHUniformStruct() },
- radius: { value: 1.0 },
- seed: { value: 0 },
-
- normalMap: { value: null },
- normalScale: { value: new Vector2( 1, 1 ) },
- },
-
- vertexShader: /* glsl */`
-
- varying vec3 vNorm;
- varying vec3 vPos;
-
- #if defined( USE_NORMALMAP ) && defined( USE_TANGENT )
-
- varying vec2 vUv;
- varying vec4 vTan;
-
- #endif
-
- void main() {
-
- vec4 mvPosition = vec4( position, 1.0 );
- mvPosition = modelViewMatrix * mvPosition;
- gl_Position = projectionMatrix * mvPosition;
-
- mat3 modelNormalMatrix = transpose( inverse( mat3( modelMatrix ) ) );
- vNorm = normalize( modelNormalMatrix * normal );
- vPos = ( modelMatrix * vec4( position, 1.0 ) ).xyz;
-
- #if defined( USE_NORMALMAP ) && defined( USE_TANGENT )
-
- vUv = uv;
- vTan = tangent;
-
- #endif
-
- }
-
- `,
-
- fragmentShader: /* glsl */`
- #define RAY_OFFSET 1e-4
-
- precision highp isampler2D;
- precision highp usampler2D;
- precision highp sampler2DArray;
- #include
- #include
-
- // bvh
- ${ BVHShaderGLSL.common_functions }
- ${ BVHShaderGLSL.bvh_struct_definitions }
- ${ BVHShaderGLSL.bvh_ray_functions }
-
- // uniform structs
- ${ StructsGLSL.material_struct }
-
- // rand
- ${ RandomGLSL.pcg_functions }
-
- // common
- ${ SamplingGLSL.shape_sampling_functions }
-
- uniform BVH bvh;
- uniform int seed;
- uniform float radius;
-
- varying vec3 vNorm;
- varying vec3 vPos;
-
- #if defined(USE_NORMALMAP) && defined(USE_TANGENT)
-
- uniform sampler2D normalMap;
- uniform vec2 normalScale;
- varying vec2 vUv;
- varying vec4 vTan;
-
- #endif
-
- void main() {
-
- rng_initialize( gl_FragCoord.xy, seed );
-
- // compute the flat face surface normal
- vec3 fdx = vec3( dFdx( vPos.x ), dFdx( vPos.y ), dFdx( vPos.z ) );
- vec3 fdy = vec3( dFdy( vPos.x ), dFdy( vPos.y ), dFdy( vPos.z ) );
- vec3 faceNormal = normalize( cross( fdx, fdy ) );
-
- // find the max component to scale the offset to account for floating point error
- vec3 absPoint = abs( vPos );
- float maxPoint = max( absPoint.x, max( absPoint.y, absPoint.z ) );
- vec3 normal = vNorm;
-
- #if defined( USE_NORMALMAP ) && defined( USE_TANGENT )
-
- // some provided tangents can be malformed (0, 0, 0) causing the normal to be degenerate
- // resulting in NaNs and slow path tracing.
- if ( length( vTan.xyz ) > 0.0 ) {
-
- vec2 uv = vUv;
- vec3 tangent = normalize( vTan.xyz );
- vec3 bitangent = normalize( cross( normal, tangent ) * vTan.w );
- mat3 vTBN = mat3( tangent, bitangent, normal );
-
- vec3 texNormal = texture2D( normalMap, uv ).xyz * 2.0 - 1.0;
- texNormal.xy *= normalScale;
- normal = vTBN * texNormal;
-
- }
-
- #endif
-
- normal *= gl_FrontFacing ? 1.0 : - 1.0;
-
- vec3 rayOrigin = vPos + faceNormal * ( maxPoint + 1.0 ) * RAY_OFFSET;
- float accumulated = 0.0;
- for ( int i = 0; i < SAMPLES; i ++ ) {
-
- // sample the cosine weighted hemisphere and discard the sample if it's below
- // the geometric surface
- vec3 rayDirection = sampleHemisphere( normalize( normal ), pcgRand4().xy );
-
- // check if we hit the mesh and its within the specified radius
- float side = 1.0;
- float dist = 0.0;
- vec3 barycoord = vec3( 0.0 );
- vec3 outNormal = vec3( 0.0 );
- uvec4 faceIndices = uvec4( 0u );
-
- // if the ray is above the geometry surface, and it doesn't hit another surface within the specified radius then
- // we consider it lit
- if (
- dot( rayDirection, faceNormal ) > 0.0 &&
- (
- ! bvhIntersectFirstHit( bvh, rayOrigin, rayDirection, faceIndices, outNormal, barycoord, side, dist ) ||
- dist >= radius
- )
- ) {
-
- accumulated += 1.0;
-
- }
-
- }
-
- gl_FragColor.rgb = vec3( accumulated / float( SAMPLES ) );
- gl_FragColor.a = 1.0;
-
- }
-
- `
-
- } );
-
- this.setValues( parameters );
-
- }
-
-}
diff --git a/src/utils/AOThicknessMapGenerator.js b/src/utils/AOThicknessMapGenerator.js
new file mode 100644
index 000000000..3de3b6171
--- /dev/null
+++ b/src/utils/AOThicknessMapGenerator.js
@@ -0,0 +1,227 @@
+import * as THREE from 'three';
+import { MeshBVHUniformStruct } from 'three-mesh-bvh';
+import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
+import { UVTriangleDataTextureGenerator } from './UVTriangleDataTextureGenerator.js';
+import { AOThicknessMode, AOThicknessMaterial } from '../materials/surface/AOThicknessMaterial.js';
+import { MipFlooder } from './MipFlooder.js';
+import { UVEdgeExpander } from './UVEdgeExpander.js';
+import { PathTracingSceneGenerator } from '../core/PathTracingSceneGenerator.js';
+
+export class AOThicknessMapGenerator {
+
+ constructor( renderer ) {
+
+ this._renderer = renderer;
+ this.channel = 1;
+ this.samples = 100;
+ this.flood = true;
+ this.mode = AOThicknessMode.AO_AND_THICKNESS;
+ this.aoRadius = 1.0;
+ this.thicknessRadius = 1.0;
+ this.samplesPerUpdate = 1;
+
+ this._generator = null;
+ this._isGenerating = false;
+
+ }
+
+ startGeneration( geometries, renderTarget ) {
+
+ if ( this._isGenerating ) {
+
+ throw new Error( 'Generation already in progress' );
+
+ }
+
+ this._isGenerating = true;
+ this._generator = this._generateIterator( geometries, renderTarget );
+
+ }
+
+ generateSample() {
+
+ if ( ! this._isGenerating || ! this._generator ) {
+
+ return false;
+
+ }
+
+ const renderer = this._renderer;
+ const originalRenderTarget = renderer.getRenderTarget();
+ const originalRenderAutoClear = renderer.autoClear;
+ const originalAutoClear = renderer.autoClear;
+ const originalPixelRatio = renderer.getPixelRatio();
+ const originalClearColor = new THREE.Color();
+ renderer.getClearColor( originalClearColor );
+
+ try {
+
+ const result = this._generator.next();
+
+ if ( result.done ) {
+
+ this._isGenerating = false;
+ this._generator = null;
+ return false;
+
+ }
+
+ return true;
+
+ } finally {
+
+ renderer.setClearColor( originalClearColor );
+ renderer.setRenderTarget( originalRenderTarget );
+ renderer.autoClear = originalRenderAutoClear;
+ renderer.setPixelRatio( originalPixelRatio );
+ renderer.autoClear = originalAutoClear;
+
+ }
+
+ }
+
+ *_generateIterator( geometries, renderTarget ) {
+
+ if ( geometries.constructor !== Array ) {
+
+ geometries = [ geometries ];
+
+ }
+
+ const { _renderer: renderer, channel, samples, flood, mode, thicknessRadius, aoRadius } = this;
+
+ renderer.setClearColor( 0x000000, 0 );
+
+ renderer.autoClear = false;
+ renderer.setPixelRatio( 1 );
+
+ const aoRenderTarget = new THREE.WebGLRenderTarget( renderTarget.width, renderTarget.height, {
+ type: THREE.FloatType,
+ colorSpace: renderTarget.colorSpace
+ } );
+
+ renderer.setRenderTarget( renderTarget );
+ renderer.clear();
+
+ const copyQuad = new FullScreenQuad( new THREE.MeshBasicMaterial( { transparent: true } ) );
+
+ // scale the scene to a reasonable size
+ const scene = new THREE.Scene();
+ const group = new THREE.Group();
+
+ for ( const geometry of geometries ) {
+
+ group.add( new THREE.Mesh( geometry, new THREE.MeshBasicMaterial() ) );
+
+ }
+
+ scene.add( group );
+
+ const box = new THREE.Box3();
+ box.setFromObject( scene );
+
+ const sphere = new THREE.Sphere();
+ box.getBoundingSphere( sphere );
+
+ scene.scale.setScalar( 2.5 / sphere.radius );
+ scene.position.y = - 0.25 * ( box.max.y - box.min.y ) * 2.5 / sphere.radius;
+ scene.updateMatrixWorld();
+
+ const { bvh } = new PathTracingSceneGenerator( group ).generate();
+
+ const uvTriangleDataTextureGenerator = new UVTriangleDataTextureGenerator( renderer );
+ uvTriangleDataTextureGenerator.channel = channel;
+
+ const uvTriangleRenderTarget = new THREE.WebGLRenderTarget( renderTarget.width, renderTarget.height * 2, {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType
+ } );
+
+ uvTriangleDataTextureGenerator.generateTexture( geometries, uvTriangleRenderTarget );
+ const uvToTriangleMap = uvTriangleRenderTarget.texture;
+
+ const bvhUniform = new MeshBVHUniformStruct();
+ bvhUniform.updateFrom( bvh );
+
+ const material = new AOThicknessMaterial( {
+ mode,
+ bvh: bvhUniform,
+ objectModelMatrix: scene.matrixWorld, // new THREE.Matrix4(),
+ uvToTriangleMap: uvToTriangleMap,
+ resolution: new THREE.Vector2( renderTarget.width, renderTarget.height ),
+ thicknessRadius,
+ aoRadius,
+ samples: this.samplesPerUpdate,
+ } );
+
+ const aoQuad = new FullScreenQuad( material );
+ copyQuad.material.map = aoRenderTarget.texture;
+
+ let steps = 1;
+
+ for ( let i = 0; i < samples; i += this.samplesPerUpdate ) {
+
+ renderer.setPixelRatio( 1 );
+ renderer.setRenderTarget( aoRenderTarget );
+ renderer.setClearColor( 0x000000, 0 );
+ renderer.autoClear = false;
+ renderer.clear();
+
+ material.seed ++;
+ aoQuad.render( renderer );
+ renderer.setRenderTarget( renderTarget );
+ copyQuad.material.map.needsUpdate = true;
+ copyQuad.material.opacity = 1 / steps;
+ copyQuad.render( renderer );
+ steps ++;
+
+ yield;
+
+ }
+
+ uvTriangleRenderTarget.dispose();
+ material.dispose();
+ aoQuad.dispose();
+ aoRenderTarget.dispose();
+
+ if ( flood ) {
+
+ const expandTarget = new THREE.WebGLRenderTarget( renderTarget.width, renderTarget.height, {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType
+ } );
+
+ const floodTarget = new THREE.WebGLRenderTarget( renderTarget.width, renderTarget.height, {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType
+ } );
+
+ const mipFlooder = new MipFlooder( renderer );
+ const edgeExpander = new UVEdgeExpander( renderer );
+
+ edgeExpander.expand( renderTarget.texture, expandTarget );
+ mipFlooder.floodFill( expandTarget.texture, floodTarget );
+
+ renderer.setRenderTarget( renderTarget );
+ copyQuad.material.map = floodTarget.texture;
+ copyQuad.material.opacity = 1;
+ copyQuad.render( renderer );
+
+ expandTarget.dispose();
+ floodTarget.dispose();
+ mipFlooder.dispose();
+ edgeExpander.dispose();
+
+ }
+
+ copyQuad.dispose();
+
+ }
+
+}
diff --git a/src/utils/MipFlooder.js b/src/utils/MipFlooder.js
new file mode 100644
index 000000000..529879ed1
--- /dev/null
+++ b/src/utils/MipFlooder.js
@@ -0,0 +1,185 @@
+import * as THREE from 'three';
+import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
+
+// Algorithm: https://www.artstation.com/blogs/se_carri/XOBq/the-god-of-war-texture-optimization-algorithm-mip-flooding
+export class MipFlooder {
+
+ constructor( renderer ) {
+
+ this._renderer = renderer;
+ this._createShaders();
+
+ }
+
+ _createShaders() {
+
+ this._downsampleShader = new THREE.ShaderMaterial( {
+ uniforms: {
+ tDiffuse: { value: null },
+ convertToSRGB: { value: false },
+ mipLevel: { value: 0 }
+ },
+ vertexShader: `
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+ }
+ `,
+ fragmentShader: `
+ precision highp sampler2D;
+ uniform sampler2D tDiffuse;
+ uniform bool convertToSRGB;
+ uniform int mipLevel;
+ varying vec2 vUv;
+
+ void main() {
+ vec4 texel = texture2D(tDiffuse, vUv);
+ gl_FragColor = texel;
+ }
+ `
+ } );
+
+ this._compositeShader = new THREE.ShaderMaterial( {
+ uniforms: {
+ tLower: { value: null },
+ tHigher: { value: null }
+ },
+ vertexShader: `
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+ }
+ `,
+ fragmentShader: `
+ precision highp sampler2D;
+ uniform sampler2D tLower;
+ uniform sampler2D tHigher;
+ varying vec2 vUv;
+
+ void main() {
+ vec4 lowerColor = texture2D(tLower, vUv);
+ vec4 higherColor = texture2D(tHigher, vUv);
+
+ gl_FragColor.rgba = higherColor.a * higherColor.rgba + (1.0 - higherColor.a) * lowerColor.rgba;
+
+ }
+ `
+ } );
+
+ }
+
+ floodFill( texture, renderTarget ) {
+
+ const fsQuad = new FullScreenQuad();
+ const { _renderer: renderer, _downsampleShader: downsampleShader, _compositeShader: compositeShader } = this;
+
+ const clearColor = new THREE.Color();
+
+ renderer.getClearColor( clearColor );
+ renderer.setClearColor( 0x000000, 0 );
+ const originalRenderTarget = renderer.getRenderTarget();
+
+ // Create render targets
+ const mipLevels = Math.log2( Math.max( texture.image.width, texture.image.height ) );
+ const renderTargets = [];
+ for ( let i = 0; i <= mipLevels; i ++ ) {
+
+ const size = Math.pow( 2, mipLevels - i );
+ renderTargets.push( new THREE.WebGLRenderTarget( size, size, {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType,
+ colorSpace: texture.colorSpace
+ } ) );
+
+ }
+
+ // Downscale pass
+ downsampleShader.uniforms.tDiffuse.value = texture;
+
+ for ( let i = 0; i < renderTargets.length; i ++ ) {
+
+ renderer.setRenderTarget( renderTargets[ i ] );
+
+ fsQuad.material = downsampleShader;
+ fsQuad.render( renderer );
+
+ renderer.setRenderTarget( null );
+
+ }
+
+ // Create two additional render targets for the composite pass
+ const compositeTargets = [
+ new THREE.WebGLRenderTarget( texture.image.width, texture.image.height, {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType
+ } ),
+ new THREE.WebGLRenderTarget( texture.image.width, texture.image.height, {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType
+ } )
+ ];
+
+ let sourceIndex = 0;
+ let targetIndex = 1;
+
+ // Initial composite from the smallest mip level
+ compositeShader.uniforms.tLower.value = renderTargets[ renderTargets.length - 1 ].texture;
+ compositeShader.uniforms.tHigher.value = renderTargets[ renderTargets.length - 2 ].texture;
+ renderer.setRenderTarget( compositeTargets[ targetIndex ] );
+ fsQuad.material = compositeShader;
+ fsQuad.render( renderer );
+
+ // Continue compositing for the rest of the mip levels
+ for ( let i = renderTargets.length - 2; i >= 0; i -- ) {
+
+ const t = sourceIndex;
+ sourceIndex = targetIndex;
+ targetIndex = t;
+
+ compositeShader.uniforms.tLower.value = compositeTargets[ sourceIndex ].texture;
+ compositeShader.uniforms.tHigher.value = renderTargets[ i ].texture;
+ renderer.setRenderTarget( i === 0 ? renderTarget : compositeTargets[ targetIndex ] );
+ fsQuad.render( renderer );
+
+ }
+
+
+ renderer.setClearColor( clearColor );
+ renderer.setRenderTarget( originalRenderTarget );
+
+ const finalTexture = compositeTargets[ targetIndex ].texture;
+
+ for ( let i = 0; i < renderTargets.length; i ++ ) {
+
+ renderTargets[ i ].dispose();
+
+ }
+
+ for ( let i = 0; i < compositeTargets.length; i ++ ) {
+
+ compositeTargets[ i ].dispose();
+
+ }
+
+ fsQuad.dispose();
+
+ return finalTexture;
+
+ }
+
+ dispose() {
+
+ this._downsampleShader.dispose();
+ this._compositeShader.dispose();
+
+ }
+
+}
diff --git a/src/utils/UVEdgeExpander.js b/src/utils/UVEdgeExpander.js
new file mode 100644
index 000000000..774e6e843
--- /dev/null
+++ b/src/utils/UVEdgeExpander.js
@@ -0,0 +1,153 @@
+import * as THREE from 'three';
+import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
+
+// Given a UV map, extends the edges of islands by a specified number of pixels to avoid seams.
+export class UVEdgeExpander {
+
+ constructor( renderer ) {
+
+ this._renderer = renderer;
+
+ this._shader = new THREE.ShaderMaterial( {
+ uniforms: {
+ inputTexture: { value: null },
+ texelSize: { value: new THREE.Vector2() }
+ },
+ vertexShader: `
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+ }
+ `,
+ fragmentShader: `
+ uniform sampler2D inputTexture;
+ uniform vec2 texelSize;
+
+ float rgbSum(vec3 color) {
+ return color.r + color.g + color.b;
+ }
+
+ void main() {
+ vec2 uv = gl_FragCoord.xy * texelSize;
+ vec4 center = texture2D(inputTexture, uv);
+
+ // If the center pixel is already opaque, keep it as is
+ if (center.a > 0.0) {
+ gl_FragColor = center;
+ return;
+ }
+
+ gl_FragColor.a = 1.0;
+
+ // Check neighboring pixels
+ vec4 left = texelFetch(inputTexture, ivec2(gl_FragCoord.xy) + ivec2(-1, 0), 0);
+ vec4 right = texelFetch(inputTexture, ivec2(gl_FragCoord.xy) + ivec2(1, 0), 0);
+ vec4 top = texelFetch(inputTexture, ivec2(gl_FragCoord.xy) + ivec2(0, 1), 0);
+ vec4 bottom = texelFetch(inputTexture, ivec2(gl_FragCoord.xy) + ivec2(0, -1), 0);
+
+ vec4 topLeft = texelFetch(inputTexture, ivec2(gl_FragCoord.xy) + ivec2(-1, 1), 0);
+ vec4 topRight = texelFetch(inputTexture, ivec2(gl_FragCoord.xy) + ivec2(1, 1), 0);
+ vec4 bottomLeft = texelFetch(inputTexture, ivec2(gl_FragCoord.xy) + ivec2(-1, -1), 0);
+ vec4 bottomRight = texelFetch(inputTexture, ivec2(gl_FragCoord.xy) + ivec2(1, -1), 0);
+
+ // Find the maximum alpha value among neighbors
+ float maxAlpha = max(max(max(left.a, right.a), max(top.a, bottom.a)),
+ max(max(topLeft.a, topRight.a), max(bottomLeft.a, bottomRight.a)));
+
+ // If we have an opaque neighbor, choose the color with the highest alpha and RGB sum
+ if (maxAlpha > 0.0) {
+ vec4 result = vec4(0.0);
+ float maxRGBSum = -1.0;
+
+ vec4 neighbors[8] = vec4[8](left, right, top, bottom, topLeft, topRight, bottomLeft, bottomRight);
+
+ for (int i = 0; i < 8; i++) {
+ if (neighbors[i].a == maxAlpha) {
+ float currentRGBSum = rgbSum(neighbors[i].rgb);
+ if (currentRGBSum > maxRGBSum) {
+ maxRGBSum = currentRGBSum;
+ result = neighbors[i];
+ }
+ }
+ }
+
+ gl_FragColor.rgb = result.rgb;
+ } else {
+ // If no opaque neighbors, keep the pixel transparent
+ gl_FragColor = center;
+ }
+ }`
+ } );
+ this._fsQuad = new FullScreenQuad( this._shader );
+
+ }
+
+ expand( texture, renderTarget, iterations = 1 ) {
+
+ const width = texture.image.width;
+ const height = texture.image.height;
+ const { _shader: shader, _renderer: renderer, _fsQuad: fsQuad } = this;
+
+ const originalClearColor = new THREE.Color();
+
+ renderer.getClearColor( originalClearColor );
+ renderer.setClearColor( 0x000000, 0 );
+ const originalRenderTarget = renderer.getRenderTarget();
+
+ let renderTargetA = new THREE.WebGLRenderTarget( width, height, {
+ minFilter: THREE.LinearFilter,
+ magFilter: THREE.LinearFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType
+ } );
+ let renderTargetB = renderTargetA.clone();
+
+ shader.uniforms.texelSize.value.set( 1 / width, 1 / height );
+
+ // Initial render
+ shader.uniforms.inputTexture.value = texture;
+ renderer.setRenderTarget( iterations == 1 ? renderTarget : renderTargetA );
+ fsQuad.render( renderer );
+
+ // Iterative expansion
+ for ( let i = 0; i < iterations - 1; i ++ ) {
+
+ shader.uniforms.inputTexture.value = renderTargetA.texture;
+
+ if ( i === iterations - 2 ) {
+
+ renderer.setRenderTarget( renderTarget );
+
+ } else {
+
+ renderer.setRenderTarget( renderTargetB );
+
+ }
+
+
+ fsQuad.render( renderer );
+
+ // Swap render targets
+ [ renderTargetA, renderTargetB ] = [ renderTargetB, renderTargetA ];
+
+ }
+
+
+ renderTargetA.dispose();
+ renderTargetB.dispose();
+
+ renderer.setClearColor( originalClearColor );
+ renderer.setRenderTarget( originalRenderTarget );
+
+ }
+
+
+ dispose() {
+
+ this._shader.dispose();
+ this._fsQuad.dispose();
+
+ }
+
+}
diff --git a/src/utils/UVGenerator.js b/src/utils/UVGenerator.js
new file mode 100644
index 000000000..30226b46b
--- /dev/null
+++ b/src/utils/UVGenerator.js
@@ -0,0 +1,191 @@
+// https://github.com/mozilla/Spoke/commit/9701d647020e09d584885bd457eb225e9995c12f
+import XAtlas from '@gfodor/xatlas-web';
+import { Float32BufferAttribute, Uint32BufferAttribute } from 'three';
+
+const AddMeshStatus = {
+ Success: 0,
+ Error: 1,
+ IndexOutOfRange: 2,
+ InvalidIndexCount: 3
+};
+
+export class UVGenerator {
+
+ constructor( wasmPath = new URL( '../../node_modules/@gfodor/xatlas-web/dist/xatlas-web.wasm', import.meta.url ) ) {
+
+ this._module = null;
+ this._wasmPath = wasmPath;
+
+ this.channel = 1;
+
+ }
+
+ async init() {
+
+ if ( this._module ) return;
+
+ const wasmurl = new URL( this._wasmPath, import.meta.url );
+ this._module = XAtlas( {
+
+ locateFile( path ) {
+
+ if ( path.endsWith( '.wasm' ) ) {
+
+ return wasmurl.toString();
+
+ }
+
+ return path;
+
+ }
+
+ } );
+
+ this._xatlas = await this._module;
+
+ }
+
+ generate( geometries, onProgress = null ) {
+
+ if ( geometries.constructor !== Array ) {
+
+ geometries = [ geometries ];
+
+ }
+
+ const xatlas = this._xatlas;
+ xatlas.createAtlas( );
+
+ const meshInfos = [];
+
+ for ( let i = 0; i < geometries.length; i ++ ) {
+
+ const geometry = geometries[ i ];
+
+ const originalVertexCount = geometry.attributes.position.count;
+ const originalIndexCount = geometry.index.count;
+
+ const meshInfo = xatlas.createMesh( originalVertexCount, originalIndexCount, true, true );
+ xatlas.HEAPU32.set( geometry.index.array, meshInfo.indexOffset / Uint32Array.BYTES_PER_ELEMENT );
+ xatlas.HEAPF32.set( geometry.attributes.position.array, meshInfo.positionOffset / Float32Array.BYTES_PER_ELEMENT );
+ xatlas.HEAPF32.set( geometry.attributes.normal.array, meshInfo.normalOffset / Float32Array.BYTES_PER_ELEMENT );
+
+ if ( geometry.attributes.uv ) {
+
+ xatlas.HEAPF32.set( geometry.attributes.uv.array, meshInfo.uvOffset / Float32Array.BYTES_PER_ELEMENT );
+
+ } else {
+
+ const vertCount = geometry.attributes.position.count;
+ geometry.setAttribute( 'uv', new Float32BufferAttribute( new Float32Array( vertCount * 2 ), 2, false ) );
+
+ }
+
+ const statusCode = xatlas.addMesh();
+ if ( statusCode !== AddMeshStatus.Success ) {
+
+ throw new Error( `UVGenerator: Error adding mesh. Status code ${ statusCode }` );
+
+ }
+
+ meshInfos.push( meshInfo );
+
+ }
+
+ const params = { padding: 2 };
+ if ( onProgress ) {
+
+ params.onProgress = onProgress;
+
+ }
+
+ xatlas.generateAtlas( params );
+
+ for ( let i = 0; i < geometries.length; i ++ ) {
+
+ const geometry = geometries[ i ];
+ const meshInfo = meshInfos[ i ];
+
+ const meshData = xatlas.getMeshData( meshInfo.meshId );
+ const oldPositionArray = geometry.attributes.position.array;
+ const oldNormalArray = geometry.attributes.normal.array;
+ const oldUvArray = geometry.attributes.uv.array;
+ const oldColorArray = geometry.attributes.color ? geometry.attributes.color.array : null;
+
+ const newPositionArray = new Float32Array( meshData.newVertexCount * 3 );
+ const newNormalArray = new Float32Array( meshData.newVertexCount * 3 );
+ const newColorArray = oldColorArray ? new Float32Array( meshData.newVertexCount * 3 ) : null;
+
+ let newUvArray = null, newUv2Array = null;
+ const useSecondChannel = this.channel === 2;
+
+ if ( useSecondChannel ) {
+
+ newUvArray = new Float32Array( meshData.newVertexCount * 2 );
+ newUv2Array = new Float32Array( xatlas.HEAPF32.buffer, meshData.uvOffset, meshData.newVertexCount * 2 );
+
+ } else {
+
+ newUvArray = new Float32Array( xatlas.HEAPF32.buffer, meshData.uvOffset, meshData.newVertexCount * 2 );
+
+ }
+
+ const newIndexArray = new Uint32Array( xatlas.HEAPU32.buffer, meshData.indexOffset, meshData.newIndexCount );
+ const originalIndexArray = new Uint32Array(
+ xatlas.HEAPU32.buffer,
+ meshData.originalIndexOffset,
+ meshData.newVertexCount
+ );
+
+ for ( let i = 0; i < meshData.newVertexCount; i ++ ) {
+
+ const originalIndex = originalIndexArray[ i ];
+ newPositionArray[ i * 3 ] = oldPositionArray[ originalIndex * 3 ];
+ newPositionArray[ i * 3 + 1 ] = oldPositionArray[ originalIndex * 3 + 1 ];
+ newPositionArray[ i * 3 + 2 ] = oldPositionArray[ originalIndex * 3 + 2 ];
+ newNormalArray[ i * 3 ] = oldNormalArray[ originalIndex * 3 ];
+ newNormalArray[ i * 3 + 1 ] = oldNormalArray[ originalIndex * 3 + 1 ];
+ newNormalArray[ i * 3 + 2 ] = oldNormalArray[ originalIndex * 3 + 2 ];
+
+ if ( oldColorArray ) {
+
+ newColorArray[ i * 3 ] = oldColorArray[ originalIndex * 3 ];
+ newColorArray[ i * 3 + 1 ] = oldColorArray[ originalIndex * 3 + 1 ];
+ newColorArray[ i * 3 + 2 ] = oldColorArray[ originalIndex * 3 + 2 ];
+
+ }
+
+ if ( useSecondChannel ) {
+
+ newUvArray[ i * 2 ] = oldUvArray[ originalIndex * 2 ];
+ newUvArray[ i * 2 + 1 ] = oldUvArray[ originalIndex * 2 + 1 ];
+
+ }
+
+ }
+
+ geometry.setAttribute( 'position', new Float32BufferAttribute( newPositionArray, 3 ) );
+ geometry.setAttribute( 'normal', new Float32BufferAttribute( newNormalArray, 3 ) );
+ geometry.setAttribute( 'uv', new Float32BufferAttribute( newUvArray, 2 ) );
+
+ if ( newColorArray ) {
+
+ geometry.setAttribute( 'color', new Float32BufferAttribute( newColorArray, 3 ) );
+
+ }
+
+ if ( newUv2Array ) {
+
+ geometry.setAttribute( 'uv2', new Float32BufferAttribute( newUv2Array, 2 ) );
+
+ }
+
+ geometry.setIndex( new Uint32BufferAttribute( newIndexArray, 1 ) );
+
+ }
+
+ xatlas.destroyAtlas();
+
+ }
+
+}
diff --git a/src/utils/UVTriangleDataTextureGenerator.js b/src/utils/UVTriangleDataTextureGenerator.js
new file mode 100644
index 000000000..a016d6e98
--- /dev/null
+++ b/src/utils/UVTriangleDataTextureGenerator.js
@@ -0,0 +1,226 @@
+import * as THREE from 'three';
+import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
+
+// Generates a texture in UV space that encodes triangle positions and normals, used for baking.
+export class UVTriangleDataTextureGenerator {
+
+ constructor( renderer ) {
+
+ this._renderer = renderer;
+ this._createShaders();
+ this.channel = 1;
+
+ }
+
+ _createShaders() {
+
+ this._vertexShader = `
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ attribute vec3 originalPosition;
+ attribute vec3 originalNormal;
+
+ void main() {
+ vPosition = originalPosition;
+ vNormal = originalNormal;
+ gl_Position = vec4(position.xy, 0.0, 1.0);
+ }
+ `;
+
+ this._positionFragmentShader = `
+ varying vec3 vPosition;
+
+ void main() {
+ gl_FragColor.rgb = vPosition;
+ gl_FragColor.a = 1.0;
+ }
+ `;
+
+ this._normalFragmentShader = `
+ varying vec3 vNormal;
+
+ void main() {
+ gl_FragColor = vec4(normalize(vNormal), 1.0);
+ }
+ `;
+
+ }
+
+ generateTexture( geometries, renderTarget ) {
+
+ const resolution = renderTarget.width;
+
+ const { _vertexShader: vertexShader, _positionFragmentShader: positionFragmentShader, _normalFragmentShader: normalFragmentShader, _renderer: renderer } = this;
+
+ if ( geometries.constructor !== Array ) {
+
+ geometries = [ geometries ];
+
+ }
+
+ const positionTarget = new THREE.WebGLRenderTarget( resolution, resolution, {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType,
+ colorSpace: THREE.LinearSRGBColorSpace,
+ } );
+
+ const normalTarget = new THREE.WebGLRenderTarget( resolution, resolution, {
+ minFilter: THREE.NearestFilter,
+ magFilter: THREE.NearestFilter,
+ format: THREE.RGBAFormat,
+ type: THREE.FloatType,
+ colorSpace: THREE.LinearSRGBColorSpace,
+ } );
+
+ const positionMaterial = new THREE.ShaderMaterial( {
+ vertexShader: vertexShader,
+ fragmentShader: positionFragmentShader,
+ side: THREE.DoubleSide
+ } );
+
+ const normalMaterial = new THREE.ShaderMaterial( {
+ vertexShader: vertexShader,
+ fragmentShader: normalFragmentShader,
+ side: THREE.DoubleSide
+ } );
+
+ const originalRenderTarget = renderer.getRenderTarget();
+ const originalViewport = renderer.getViewport( new THREE.Vector4() );
+ const originalClearColor = new THREE.Color();
+ const originalAutoClear = renderer.autoClear;
+
+ renderer.getClearColor( originalClearColor );
+ renderer.setClearColor( 0x000000, 0 );
+
+ renderer.setRenderTarget( positionTarget );
+ renderer.clear();
+
+ renderer.setRenderTarget( normalTarget );
+ renderer.clear();
+
+ renderer.autoClear = false;
+
+ for ( const geometry of geometries ) {
+
+ const uvGeometry = this._createUVGeometry( geometry );
+ const mesh = new THREE.Mesh( uvGeometry );
+
+ // Render positions
+ mesh.material = positionMaterial;
+ renderer.setRenderTarget( positionTarget );
+ renderer.setViewport( 0, 0, resolution, resolution );
+ renderer.render( mesh, new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ) );
+
+ // Render normals
+ mesh.material = normalMaterial;
+ renderer.setRenderTarget( normalTarget );
+ renderer.setViewport( 0, 0, resolution, resolution );
+ renderer.render( mesh, new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ) );
+
+ uvGeometry.dispose();
+
+ }
+
+ this._combineTextures( positionTarget.texture, normalTarget.texture, renderTarget );
+
+ // Reset renderer state
+ renderer.autoClear = originalAutoClear;
+ renderer.setClearColor( originalClearColor );
+
+ renderer.setRenderTarget( originalRenderTarget );
+ renderer.setViewport( originalViewport );
+
+ // Clean up
+ positionTarget.dispose();
+ normalTarget.dispose();
+ positionMaterial.dispose();
+ normalMaterial.dispose();
+
+ }
+
+ _createUVGeometry( originalGeometry ) {
+
+ const uvGeometry = new THREE.BufferGeometry();
+
+ const positions = [];
+ const originalPositions = [];
+ const originalNormals = [];
+
+ const positionAttribute = originalGeometry.attributes.position;
+ const normalAttribute = originalGeometry.attributes.normal;
+ const uvAttribute = originalGeometry.attributes[ this.channel === 2 ? 'uv2' : 'uv' ];
+
+ for ( let i = 0; i < positionAttribute.count; i ++ ) {
+
+ const u = uvAttribute.getX( i );
+ const v = uvAttribute.getY( i );
+
+ positions.push( u * 2 - 1, v * 2 - 1, - 1 );
+
+ originalPositions.push(
+ positionAttribute.getX( i ),
+ positionAttribute.getY( i ),
+ positionAttribute.getZ( i )
+ );
+
+ originalNormals.push(
+ normalAttribute.getX( i ),
+ normalAttribute.getY( i ),
+ normalAttribute.getZ( i )
+ );
+
+ }
+
+ uvGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
+ uvGeometry.setAttribute( 'originalPosition', new THREE.Float32BufferAttribute( originalPositions, 3 ) );
+ uvGeometry.setAttribute( 'originalNormal', new THREE.Float32BufferAttribute( originalNormals, 3 ) );
+
+ if ( originalGeometry.index ) {
+
+ uvGeometry.setIndex( originalGeometry.index );
+
+ }
+
+ return uvGeometry;
+
+ }
+
+ _combineTextures( positionTexture, normalTexture, renderTarget ) {
+
+ const combineShader = new THREE.ShaderMaterial( {
+ uniforms: {
+ positionTexture: { value: positionTexture },
+ normalTexture: { value: normalTexture },
+ },
+ vertexShader: `
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ gl_Position = vec4(position.xy, 0.0, 1.0);
+ }
+ `,
+ fragmentShader: `
+ uniform sampler2D positionTexture;
+ uniform sampler2D normalTexture;
+ varying vec2 vUv;
+
+ void main() {
+ if (vUv.y < 0.5) {
+ gl_FragColor = texture2D(positionTexture, vUv * vec2(1.0, 2.0));
+ } else {
+ gl_FragColor = texture2D(normalTexture, vUv * vec2(1.0, 2.0) - vec2(0.0, 1.0));
+ }
+ }
+ `
+ } );
+
+ const fsQuad = new FullScreenQuad( combineShader );
+ this._renderer.setRenderTarget( renderTarget );
+ fsQuad.render( this._renderer );
+ fsQuad.dispose();
+
+ }
+
+}
diff --git a/src/utils/UVUnwrapper.js b/src/utils/UVUnwrapper.js
deleted file mode 100644
index fad4e4dac..000000000
--- a/src/utils/UVUnwrapper.js
+++ /dev/null
@@ -1,101 +0,0 @@
-// https://github.com/mozilla/Spoke/commit/9701d647020e09d584885bd457eb225e9995c12f
-import XAtlas from 'xatlas-web';
-import { BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute } from 'three';
-
-export class UVUnwrapper {
-
- constructor() {
-
- this._module = null;
-
- }
-
- async load() {
-
- const wasmurl = new URL( '../../node_modules/xatlas-web/dist/xatlas-web.wasm', import.meta.url );
- this._module = XAtlas( {
-
- locateFile( path ) {
-
- if ( path.endsWith( '.wasm' ) ) {
-
- return wasmurl.toString();
-
- }
-
- return path;
-
- }
-
- } );
- return this._module.ready;
-
- }
-
- generate( geometry ) {
-
- const xatlas = this._module;
- const originalVertexCount = geometry.attributes.position.count;
- const originalIndexCount = geometry.index.count;
-
- xatlas.createAtlas();
-
- const meshInfo = xatlas.createMesh( originalVertexCount, originalIndexCount, true, true );
- xatlas.HEAPU16.set( geometry.index.array, meshInfo.indexOffset / Uint16Array.BYTES_PER_ELEMENT );
- xatlas.HEAPF32.set( geometry.attributes.position.array, meshInfo.positionOffset / Float32Array.BYTES_PER_ELEMENT );
- xatlas.HEAPF32.set( geometry.attributes.normal.array, meshInfo.normalOffset / Float32Array.BYTES_PER_ELEMENT );
- xatlas.HEAPF32.set( geometry.attributes.uv.array, meshInfo.uvOffset / Float32Array.BYTES_PER_ELEMENT );
-
- const statusCode = xatlas.addMesh();
- if ( statusCode !== AddMeshStatus.Success ) {
-
- throw new Error( `UVUnwrapper: Error adding mesh. Status code ${ statusCode }` );
-
- }
-
- xatlas.generateAtlas();
-
- const meshData = xatlas.getMeshData( meshInfo.meshId );
- const oldPositionArray = geometry.attributes.position.array;
- const oldNormalArray = geometry.attributes.normal.array;
- const oldUvArray = geometry.attributes.uv.array;
-
- const newPositionArray = new Float32Array( meshData.newVertexCount * 3 );
- const newNormalArray = new Float32Array( meshData.newVertexCount * 3 );
- const newUvArray = new Float32Array( meshData.newVertexCount * 2 );
- const newUv2Array = new Float32Array( xatlas.HEAPF32.buffer, meshData.uvOffset, meshData.newVertexCount * 2 );
- const newIndexArray = new Uint32Array( xatlas.HEAPU32.buffer, meshData.indexOffset, meshData.newIndexCount );
- const originalIndexArray = new Uint32Array(
- xatlas.HEAPU32.buffer,
- meshData.originalIndexOffset,
- meshData.newVertexCount
- );
-
- for ( let i = 0; i < meshData.newVertexCount; i ++ ) {
-
- const originalIndex = originalIndexArray[ i ];
- newPositionArray[ i * 3 ] = oldPositionArray[ originalIndex * 3 ];
- newPositionArray[ i * 3 + 1 ] = oldPositionArray[ originalIndex * 3 + 1 ];
- newPositionArray[ i * 3 + 2 ] = oldPositionArray[ originalIndex * 3 + 2 ];
- newNormalArray[ i * 3 ] = oldNormalArray[ originalIndex * 3 ];
- newNormalArray[ i * 3 + 1 ] = oldNormalArray[ originalIndex * 3 + 1 ];
- newNormalArray[ i * 3 + 2 ] = oldNormalArray[ originalIndex * 3 + 2 ];
- newUvArray[ i * 2 ] = oldUvArray[ originalIndex * 2 ];
- newUvArray[ i * 2 + 1 ] = oldUvArray[ originalIndex * 2 + 1 ];
-
- }
-
- const newGeometry = new BufferGeometry();
- newGeometry.addAttribute( 'position', new Float32BufferAttribute( newPositionArray, 3 ) );
- newGeometry.addAttribute( 'normal', new Float32BufferAttribute( newNormalArray, 3 ) );
- newGeometry.addAttribute( 'uv', new Float32BufferAttribute( newUvArray, 2 ) );
- newGeometry.addAttribute( 'uv2', new Float32BufferAttribute( newUv2Array, 2 ) );
- newGeometry.setIndex( new Uint32BufferAttribute( newIndexArray, 1 ) );
-
- mesh.geometry = newGeometry;
-
- xatlas.destroyAtlas();
-
- }
-
-}