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(); - - } - -}