فهرست منبع

VolumeMesh exploration.

Mr.doob 3 ماه پیش
والد
کامیت
887b8a8ad2

+ 58 - 0
examples/jsm/utils/GenerateSDFMaterial.js

@@ -0,0 +1,58 @@
+import { ShaderMaterial, Vector3 } from 'three';
+
+export class GenerateSDFMaterial extends ShaderMaterial {
+
+	constructor( params ) {
+
+		super( {
+			uniforms: {
+				bvh: { value: null },
+				matrix: { value: null },
+				zValue: { value: 0 }
+			},
+
+			vertexShader: /* glsl */`
+				varying vec2 vUv;
+				void main() {
+					vUv = uv;
+					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+				}
+			`,
+
+			fragmentShader: /* glsl */`
+				varying vec2 vUv;
+				uniform BVH bvh;
+				uniform mat4 matrix;
+				uniform float zValue;
+
+				void main() {
+					// calculate the point in space for this pixel
+					vec3 point = vec3( vUv.x - 0.5, vUv.y - 0.5, zValue - 0.5 );
+					point = ( matrix * vec4( point, 1.0 ) ).xyz;
+
+					// get the distance to the geometry
+					uvec4 faceIndices = uvec4( 0u );
+					vec3 faceNormal = vec3( 0.0, 0.0, 1.0 );
+					vec3 barycoord = vec3( 0.0 );
+					vec3 outPoint = vec3( 0.0 );
+					float dist = bvhClosestPointToPoint(
+						bvh, point.xyz, faceIndices, faceNormal, barycoord, outPoint
+					);
+
+					// if the triangle face normal and the point to triangle vector are pointing in the same direction
+					// then the point is in the negative half space of the triangle
+					vec3 toTriangle = normalize( outPoint - point );
+					if ( dot( faceNormal, toTriangle ) < 0.0 ) {
+						dist *= - 1.0;
+					}
+
+					gl_FragColor = vec4( dist );
+				}
+			`
+		} );
+
+		this.setValues( params );
+
+	}
+
+}

+ 238 - 0
examples/jsm/utils/JumpFloodSDFGenerator.js

@@ -0,0 +1,238 @@
+import {
+	Vector2,
+	Vector3,
+	Matrix4,
+	Data3DTexture,
+	RGBAFormat,
+	FloatType,
+	LinearFilter,
+	ShaderMaterial,
+	WebGLRenderTarget
+} from 'three';
+import { FullScreenQuad } from 'three/addons/utils/FullScreenQuad.js';
+
+export class JumpFloodSDFGenerator {
+
+	constructor( renderer ) {
+
+		this.renderer = renderer;
+
+		this.jumpFloodInitMaterial = new ShaderMaterial( {
+			uniforms: {
+				bvh: { value: null },
+				matrix: { value: null },
+				zValue: { value: 0 }
+			},
+			vertexShader: /* glsl */`
+				varying vec2 vUv;
+				void main() {
+					vUv = uv;
+					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+				}
+			`,
+			fragmentShader: /* glsl */`
+				varying vec2 vUv;
+				uniform BVH bvh;
+				uniform mat4 matrix;
+				uniform float zValue;
+
+				void main() {
+					// calculate the point in space for this pixel
+					vec3 point = vec3( vUv.x - 0.5, vUv.y - 0.5, zValue - 0.5 );
+					point = ( matrix * vec4( point, 1.0 ) ).xyz;
+
+					// get the distance to the geometry
+					uvec4 faceIndices = uvec4( 0u );
+					vec3 faceNormal = vec3( 0.0, 0.0, 1.0 );
+					vec3 barycoord = vec3( 0.0 );
+					vec3 outPoint = vec3( 0.0 );
+					bvhClosestPointToPoint(
+						bvh, point.xyz, faceIndices, faceNormal, barycoord, outPoint
+					);
+
+					gl_FragColor = vec4( outPoint, 1.0 );
+				}
+			`
+		} );
+
+		this.jumpFloodMaterial = new ShaderMaterial( {
+			uniforms: {
+				map: { value: null },
+				stepSize: { value: 0 }
+			},
+			vertexShader: /* glsl */`
+				varying vec2 vUv;
+				void main() {
+					vUv = uv;
+					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+				}
+			`,
+			fragmentShader: /* glsl */`
+				varying vec2 vUv;
+				uniform sampler2D map;
+				uniform float stepSize;
+
+				void main() {
+					vec2 bestUv = vUv;
+					float bestDist = distance( texture2D( map, vUv ).xyz, gl_FragCoord.xyz );
+
+					for ( int i = -1; i <= 1; i ++ ) {
+						for ( int j = -1; j <= 1; j ++ ) {
+							if ( i == 0 && j == 0 ) continue;
+
+							vec2 uv = vUv + vec2( float( i ), float( j ) ) * stepSize;
+							float dist = distance( texture2D( map, uv ).xyz, gl_FragCoord.xyz );
+
+							if ( dist < bestDist ) {
+								bestDist = dist;
+								bestUv = uv;
+							}
+						}
+					}
+
+					gl_FragColor = texture2D( map, bestUv );
+				}
+			`
+		} );
+
+		this.distanceMaterial = new ShaderMaterial( {
+			uniforms: {
+				map: { value: null },
+				matrix: { value: null },
+				zValue: { value: 0 }
+			},
+			vertexShader: /* glsl */`
+				varying vec2 vUv;
+				void main() {
+					vUv = uv;
+					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+				}
+			`,
+			fragmentShader: /* glsl */`
+				varying vec2 vUv;
+				uniform sampler2D map;
+				uniform mat4 matrix;
+				uniform float zValue;
+
+				void main() {
+					vec3 point = vec3( vUv.x - 0.5, vUv.y - 0.5, zValue - 0.5 );
+					point = ( matrix * vec4( point, 1.0 ) ).xyz;
+
+					vec3 closestPoint = texture2D( map, vUv ).xyz;
+					float dist = distance( point, closestPoint );
+
+					gl_FragColor = vec4( dist, 0.0, 0.0, 1.0 );
+				}
+			`
+		} );
+
+	}
+
+	generate( sourceMesh, resolution = 64 ) {
+
+		const { renderer } = this;
+
+		const dim = resolution;
+		const geometry = sourceMesh.geometry;
+
+		// Ensure BVH is computed
+		if ( ! geometry.boundsTree ) {
+
+			throw new Error( 'Source mesh geometry must have a BVH. Call geometry.computeBoundsTree() first.' );
+
+		}
+
+		const bvh = geometry.boundsTree;
+
+		const matrix = new Matrix4();
+		const center = new Vector3();
+		const quat = new Quaternion();
+		const scale = new Vector3();
+
+		// Compute the bounding box of the geometry
+		if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
+
+		geometry.boundingBox.getCenter( center );
+		scale.subVectors( geometry.boundingBox.max, geometry.boundingBox.min );
+		matrix.compose( center, quat, scale );
+
+		// Create the render targets
+		const rt1 = new WebGLRenderTarget( dim, dim, {
+			format: RGBAFormat,
+			type: FloatType,
+			minFilter: LinearFilter,
+			magFilter: LinearFilter
+		} );
+
+		const rt2 = new WebGLRenderTarget( dim, dim, {
+			format: RGBAFormat,
+			type: FloatType,
+			minFilter: LinearFilter,
+			magFilter: LinearFilter
+		} );
+
+		// Create the 3D texture
+		const sdfTexture = new Data3DTexture( new Float32Array( dim * dim * dim * 4 ), dim, dim, dim );
+		sdfTexture.format = RGBAFormat;
+		sdfTexture.type = FloatType;
+		sdfTexture.minFilter = LinearFilter;
+		sdfTexture.magFilter = LinearFilter;
+
+		const fsQuad = new FullScreenQuad();
+
+		for ( let z = 0; z < dim; z ++ ) {
+
+			// Initialization pass
+			fsQuad.material = this.jumpFloodInitMaterial;
+			this.jumpFloodInitMaterial.uniforms.bvh.value = bvh;
+			this.jumpFloodInitMaterial.uniforms.matrix.value = matrix;
+			this.jumpFloodInitMaterial.uniforms.zValue.value = z / dim;
+			renderer.setRenderTarget( rt1 );
+			fsQuad.render( renderer );
+
+			// Jump flooding passes
+			let stepSize = dim / 2;
+			while ( stepSize >= 1 ) {
+
+				fsQuad.material = this.jumpFloodMaterial;
+				this.jumpFloodMaterial.uniforms.map.value = rt1.texture;
+				this.jumpFloodMaterial.uniforms.stepSize.value = stepSize / dim;
+				renderer.setRenderTarget( rt2 );
+				fsQuad.render( renderer );
+
+				// Swap render targets
+				const temp = rt1;
+				rt1 = rt2;
+				rt2 = temp;
+
+				stepSize /= 2;
+
+			}
+
+			// Distance calculation pass
+			fsQuad.material = this.distanceMaterial;
+			this.distanceMaterial.uniforms.map.value = rt1.texture;
+			this.distanceMaterial.uniforms.matrix.value = matrix;
+			this.distanceMaterial.uniforms.zValue.value = z / dim;
+			renderer.setRenderTarget( rt2 );
+			fsQuad.render( renderer );
+
+			// Read the data from the render target
+			const buffer = new Float32Array( dim * dim * 4 );
+			renderer.readRenderTargetPixels( rt2, 0, 0, dim, dim, buffer );
+
+			// Copy the data to the 3D texture
+			const offset = z * dim * dim * 4;
+			sdfTexture.image.data.set( buffer, offset );
+
+		}
+
+		fsQuad.dispose();
+		rt1.dispose();
+		rt2.dispose();
+
+		return sdfTexture;
+
+	}
+
+}

+ 209 - 0
examples/jsm/utils/RayMarchSDFMaterial.js

@@ -0,0 +1,209 @@
+import { MeshStandardMaterial, Matrix4, Vector3 } from 'three';
+
+export class RayMarchSDFMaterial extends MeshStandardMaterial {
+
+	constructor( params ) {
+
+		super( params );
+
+		this.uniforms = {
+			sdfTex: { value: null },
+			normalStep: { value: new Vector3() },
+			sdfNormalMatrix: { value: new Matrix4() },
+			surface: { value: 0 }
+		};
+
+		this.defines = {
+			MAX_STEPS: 500,
+			SURFACE_EPSILON: 0.001
+		};
+
+		this.onBeforeCompile = ( shader ) => {
+
+		// Add our custom uniforms
+		shader.uniforms.sdfTex = this.uniforms.sdfTex;
+		shader.uniforms.normalStep = this.uniforms.normalStep;
+		shader.uniforms.sdfNormalMatrix = this.uniforms.sdfNormalMatrix;
+		shader.uniforms.surface = this.uniforms.surface;			// Add our defines
+			shader.defines = shader.defines || {};
+			Object.assign( shader.defines, this.defines );
+
+		// Modify vertex shader to compute ray in local space
+		shader.vertexShader = shader.vertexShader.replace(
+			'#include <common>',
+			`#include <common>
+			varying vec3 vLocalPosition;
+			varying vec3 vLocalRayOrigin;`
+		);
+
+		shader.vertexShader = shader.vertexShader.replace(
+			'#include <worldpos_vertex>',
+			`#include <worldpos_vertex>
+			// Transform camera position to local space
+			vLocalRayOrigin = ( inverse( modelMatrix ) * vec4( cameraPosition, 1.0 ) ).xyz;
+			// Vertex position is already in local space
+			vLocalPosition = position;`
+		);			// Add custom uniforms and functions to fragment shader
+		shader.fragmentShader = shader.fragmentShader.replace(
+			'#include <common>',
+			`#include <common>
+			
+			uniform sampler3D sdfTex;
+			uniform vec3 normalStep;
+			uniform mat4 sdfNormalMatrix;
+			uniform float surface;
+			
+			varying vec3 vLocalPosition;
+			varying vec3 vLocalRayOrigin;
+
+			vec2 rayBoxDist( vec3 boundsMin, vec3 boundsMax, vec3 rayOrigin, vec3 rayDir ) {
+				vec3 t0 = ( boundsMin - rayOrigin ) / rayDir;
+				vec3 t1 = ( boundsMax - rayOrigin ) / rayDir;
+				vec3 tmin = min( t0, t1 );
+				vec3 tmax = max( t0, t1 );
+				float distA = max( max( tmin.x, tmin.y ), tmin.z );
+				float distB = min( tmax.x, min( tmax.y, tmax.z ) );
+				float distToBox = max( 0.0, distA );
+				float distInsideBox = max( 0.0, distB - distToBox );
+				return vec2( distToBox, distInsideBox );
+			}`
+		);			// Inject raymarching at the very start of main
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'void main() {',
+				`void main() {
+				// Raymarch from camera through the box in local space
+				vec3 rayOrigin = vLocalRayOrigin;
+				vec3 rayDirection = normalize( vLocalPosition - vLocalRayOrigin );
+				
+				// Find intersection with SDF bounds [-0.5, 0.5]
+				vec2 boxIntersectionInfo = rayBoxDist( vec3( - 0.5 ), vec3( 0.5 ), rayOrigin, rayDirection );
+				float distToBox = boxIntersectionInfo.x;
+				float distInsideBox = boxIntersectionInfo.y;
+				bool intersectsBox = distInsideBox > 0.0;
+				
+				if ( !intersectsBox ) {
+					discard;
+				}
+				
+				// Raymarch to find surface in SDF local space
+				bool intersectsSurface = false;
+				vec3 localPoint = rayOrigin + rayDirection * ( distToBox + 1e-5 );
+				
+				for ( int i = 0; i < MAX_STEPS; i ++ ) {
+					vec3 sdfUV = localPoint + vec3( 0.5 );
+					if ( sdfUV.x < 0.0 || sdfUV.x > 1.0 || sdfUV.y < 0.0 || sdfUV.y > 1.0 || sdfUV.z < 0.0 || sdfUV.z > 1.0 ) {
+						break;
+					}
+					float distanceToSurface = texture( sdfTex, sdfUV ).r - surface;
+					if ( abs( distanceToSurface ) < SURFACE_EPSILON ) {
+						intersectsSurface = true;
+						break;
+					}
+					localPoint += rayDirection * distanceToSurface * 0.5;
+				}					if ( !intersectsSurface ) {
+						discard;
+					}
+					
+					// Compute UV and normal from SDF
+					vec3 sdfUV = localPoint + vec3( 0.5 );
+					vec4 sdfData = texture( sdfTex, sdfUV );
+					vec2 sdfTexUv = sdfData.gb;
+					
+					// Compute gradient in SDF local space
+					float dx = texture( sdfTex, sdfUV + vec3( normalStep.x, 0.0, 0.0 ) ).r - texture( sdfTex, sdfUV - vec3( normalStep.x, 0.0, 0.0 ) ).r;
+					float dy = texture( sdfTex, sdfUV + vec3( 0.0, normalStep.y, 0.0 ) ).r - texture( sdfTex, sdfUV - vec3( 0.0, normalStep.y, 0.0 ) ).r;
+					float dz = texture( sdfTex, sdfUV + vec3( 0.0, 0.0, normalStep.z ) ).r - texture( sdfTex, sdfUV - vec3( 0.0, 0.0, normalStep.z ) ).r;
+					vec3 sdfNormalLocal = normalize( vec3( dx, dy, dz ) );
+					
+					// Transform normal from SDF local space to view space
+					vec3 sdfNormal = normalize( ( sdfNormalMatrix * vec4( sdfNormalLocal, 0.0 ) ).xyz );
+				`
+			);
+
+			// Replace UV sampling to use our computed UV
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'#include <map_fragment>',
+				`#ifdef USE_MAP
+					vec4 sampledDiffuseColor = texture2D( map, sdfTexUv );
+					#ifdef DECODE_VIDEO_TEXTURE
+						sampledDiffuseColor = vec4( mix( pow( sampledDiffuseColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), sampledDiffuseColor.rgb * 0.0773993808, vec3( lessThanEqual( sampledDiffuseColor.rgb, vec3( 0.04045 ) ) ) ), sampledDiffuseColor.w );
+					#endif
+					diffuseColor *= sampledDiffuseColor;
+				#endif`
+			);
+
+			// Replace normal mapping to use our computed UV and base normal
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'#include <normal_fragment_begin>',
+				`// Use the SDF normal (already in view space)
+				vec3 normal = sdfNormal;
+				vec3 nonPerturbedNormal = normal;
+				#ifdef FLAT_SHADED
+					normal = normalize( cross( dFdx( vViewPosition ), dFdy( vViewPosition ) ) );
+				#endif`
+			);
+
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'#include <normal_fragment_maps>',
+				`#ifdef USE_NORMALMAP
+					// Sample the normal map
+					vec3 mapN = texture2D( normalMap, sdfTexUv ).xyz * 2.0 - 1.0;
+					mapN.xy *= normalScale;
+					
+					// Create a tangent space from the SDF normal
+					// We need to construct tangent and bitangent vectors perpendicular to the normal
+					vec3 N = normalize( normal );
+					vec3 T = normalize( cross( N, vec3( 0.0, 1.0, 0.0 ) ) );
+					// If normal is too close to (0,1,0), use a different reference
+					if ( length( T ) < 0.1 ) {
+						T = normalize( cross( N, vec3( 1.0, 0.0, 0.0 ) ) );
+					}
+					vec3 B = normalize( cross( N, T ) );
+					
+					// Apply normal map in tangent space
+					normal = normalize( T * mapN.x + B * mapN.y + N * mapN.z );
+				#endif`
+			);
+
+			// Replace roughness/metalness sampling
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'#include <roughnessmap_fragment>',
+				`float roughnessFactor = roughness;
+				#ifdef USE_ROUGHNESSMAP
+					vec4 texelRoughness = texture2D( roughnessMap, sdfTexUv );
+					roughnessFactor *= texelRoughness.g;
+				#endif`
+			);
+
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'#include <metalnessmap_fragment>',
+				`float metalnessFactor = metalness;
+				#ifdef USE_METALNESSMAP
+					vec4 texelMetalness = texture2D( metalnessMap, sdfTexUv );
+					metalnessFactor *= texelMetalness.b;
+				#endif`
+			);
+
+			// Debug output
+			console.log( 'Shader compiled with defines:', shader.defines );
+
+			// Replace AO sampling
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'#include <aomap_fragment>',
+				`#ifdef USE_AOMAP
+					float ambientOcclusion = ( texture2D( aoMap, sdfTexUv ).r - 1.0 ) * aoMapIntensity + 1.0;
+					reflectedLight.indirectDiffuse *= ambientOcclusion;
+					#if defined( USE_ENVMAP ) && defined( STANDARD )
+						float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );
+						reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );
+					#endif
+				#endif`
+			);
+
+		};
+
+	}
+
+}
+
+

+ 57 - 0
examples/jsm/utils/RenderSDFLayerMaterial.js

@@ -0,0 +1,57 @@
+import { ShaderMaterial } from 'three';
+
+export class RenderSDFLayerMaterial extends ShaderMaterial {
+
+	constructor( params ) {
+
+		super( {
+			uniforms: {
+				sdfTex: { value: null },
+				layer: { value: 0 },
+			},
+
+			vertexShader: /* glsl */`
+				varying vec2 vUv;
+				void main() {
+					vUv = uv;
+					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+				}
+			`,
+
+			fragmentShader: /* glsl */`
+				uniform sampler3D sdfTex;
+				uniform float layer;
+				varying vec2 vUv;
+
+				void main() {
+					vec4 data = texture( sdfTex, vec3( vUv, layer ) );
+					
+					// Display three channels side by side
+					vec3 color;
+					if ( vUv.x < 0.33 ) {
+						// Left third: Distance (grayscale, normalized around 0)
+						float dist = data.r;
+						float normalized = dist * 0.5 + 0.5; // Map -1,1 to 0,1
+						color = vec3( normalized );
+					} else if ( vUv.x < 0.66 ) {
+						// Middle third: U channel (red, fractional part to handle >1 values)
+						float u = fract( data.g );
+						color = vec3( u, 0.0, 0.0 );
+					} else {
+						// Right third: V channel (green, fractional part to handle >1 values)
+						float v = fract( data.b );
+						color = vec3( 0.0, v, 0.0 );
+					}
+					
+					gl_FragColor = vec4( color, 1.0 );
+
+					#include <colorspace_fragment>
+				}
+			`
+		} );
+
+		this.setValues( params );
+
+	}
+
+}

+ 251 - 0
examples/jsm/utils/VolumeMesh.js

@@ -0,0 +1,251 @@
+import { Mesh, BoxGeometry, Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three';
+import { RayMarchSDFMaterial } from './RayMarchSDFMaterial.js';
+
+export class VolumeMesh extends Mesh {
+
+	constructor( params = {} ) {
+
+		const geometry = new BoxGeometry( 1, 1, 1 );
+		const material = new RayMarchSDFMaterial( {
+			roughness: params.roughness !== undefined ? params.roughness : 1.0,
+			metalness: params.metalness !== undefined ? params.metalness : 1.0
+		} );
+
+		super( geometry, material );
+
+		this.resolution = params.resolution !== undefined ? params.resolution : 100;
+		this.margin = params.margin !== undefined ? params.margin : 0.05;
+		this.surface = params.surface !== undefined ? params.surface : 0.0;
+
+		this.sdfTexture = null;
+		this.inverseBoundsMatrix = new Matrix4();
+
+	}
+
+	async generate( sourceMesh ) {
+
+		const dim = this.resolution;
+		const geometry = sourceMesh.geometry;
+
+		// Ensure BVH is computed
+		if ( ! geometry.boundsTree ) {
+
+			throw new Error( 'Source mesh geometry must have a BVH. Call geometry.computeBoundsTree() first.' );
+
+		}
+
+		const bvh = geometry.boundsTree;
+
+		const matrix = new Matrix4();
+		const center = new Vector3();
+		const quat = new Quaternion();
+		const scale = new Vector3();
+
+		// Compute the bounding box of the geometry including the margin
+		if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
+
+		geometry.boundingBox.getCenter( center );
+		scale.subVectors( geometry.boundingBox.max, geometry.boundingBox.min );
+		scale.x += 2 * this.margin;
+		scale.y += 2 * this.margin;
+		scale.z += 2 * this.margin;
+		matrix.compose( center, quat, scale );
+		this.inverseBoundsMatrix.copy( matrix ).invert();
+
+		// Dispose of the existing SDF texture
+		if ( this.sdfTexture ) {
+
+			this.sdfTexture.dispose();
+
+		}
+
+		const pxWidth = 1 / dim;
+		const halfWidth = 0.5 * pxWidth;
+
+		console.log( `Generating ${dim}x${dim}x${dim} SDF texture...` );
+
+		// Create a new 3D data texture
+		this.sdfTexture = new Data3DTexture( new Float32Array( dim ** 3 * 4 ), dim, dim, dim );
+		this.sdfTexture.format = RGBAFormat;
+		this.sdfTexture.type = FloatType;
+		this.sdfTexture.minFilter = LinearFilter;
+		this.sdfTexture.magFilter = LinearFilter;
+
+		const point = new Vector3();
+		const ray = new Ray();
+		const target = {
+			point: new Vector3(),
+			distance: 0,
+			faceIndex: -1
+		};
+		const uvAttr = geometry.attributes.uv;
+
+		// Iterate over all pixels and check distance
+		for ( let x = 0; x < dim; x ++ ) {
+
+			if ( x % 10 === 0 ) {
+
+				console.log( `Processing slice ${x}/${dim}...` );
+
+			}
+
+			for ( let y = 0; y < dim; y ++ ) {
+
+				for ( let z = 0; z < dim; z ++ ) {
+
+					const index = ( x + dim * ( y + dim * z ) ) * 4;
+
+					// Adjust by half width of the pixel so we sample the pixel center
+					// and offset by half the box size
+					point.set(
+						halfWidth + x * pxWidth - 0.5,
+						halfWidth + y * pxWidth - 0.5,
+						halfWidth + z * pxWidth - 0.5,
+					).applyMatrix4( matrix );
+
+					// Get the distance to the geometry
+					bvh.closestPointToPoint( point, target );
+					const dist = target.distance;
+
+					// Check if the point is inside or outside by raycasting
+					// If we hit a back face then we're inside
+					let insideCount = 0;
+					const ray = new Ray( point );
+					const directions = [
+						new Vector3( 1, 0, 0 ),
+						new Vector3( -1, 0, 0 ),
+						new Vector3( 0, 1, 0 ),
+						new Vector3( 0, -1, 0 ),
+						new Vector3( 0, 0, 1 ),
+						new Vector3( 0, 0, -1 )
+					];
+
+					for( let i = 0; i < 6; i ++ ) {
+						ray.direction.copy( directions[ i ] );
+						const hit = bvh.raycastFirst( ray, DoubleSide );
+						if ( hit && hit.face.normal.dot( ray.direction ) > 0.0 ) {
+							insideCount ++;
+						}
+					}
+
+					const isInside = insideCount > 3;
+
+					// Set the distance in the texture data
+					this.sdfTexture.image.data[ index + 0 ] = isInside ? - dist : dist;
+
+					// Get UV from closest point
+					let u = 0, v = 0;
+
+					if ( uvAttr && target.faceIndex !== undefined ) {
+
+						const faceIndex = target.faceIndex;
+						const indexAttr = geometry.index;
+						const i0 = indexAttr.getX( faceIndex * 3 + 0 );
+						const i1 = indexAttr.getX( faceIndex * 3 + 1 );
+						const i2 = indexAttr.getX( faceIndex * 3 + 2 );
+
+						const v0 = new Vector3().fromBufferAttribute( geometry.attributes.position, i0 );
+						const v1 = new Vector3().fromBufferAttribute( geometry.attributes.position, i1 );
+						const v2 = new Vector3().fromBufferAttribute( geometry.attributes.position, i2 );
+
+						const barycoord = new Vector3();
+						Triangle.getBarycoord( target.point, v0, v1, v2, barycoord );
+
+						const uv0 = new Vector2().fromBufferAttribute( uvAttr, i0 );
+						const uv1 = new Vector2().fromBufferAttribute( uvAttr, i1 );
+						const uv2 = new Vector2().fromBufferAttribute( uvAttr, i2 );
+
+						u = uv0.x * barycoord.x + uv1.x * barycoord.y + uv2.x * barycoord.z;
+						v = uv0.y * barycoord.x + uv1.y * barycoord.y + uv2.y * barycoord.z;
+
+					}
+
+					// Store UV in G and B channels
+					this.sdfTexture.image.data[ index + 1 ] = u;
+					this.sdfTexture.image.data[ index + 2 ] = v;
+					this.sdfTexture.image.data[ index + 3 ] = 0; // Alpha unused
+
+				}
+
+			}
+
+		}
+
+		this.sdfTexture.needsUpdate = true;
+
+		console.log( 'SDF generation completed' );
+
+		// Copy textures from source mesh material if available
+		if ( sourceMesh.material ) {
+
+			const mat = sourceMesh.material;
+			if ( mat.map ) this.material.map = mat.map;
+			if ( mat.normalMap ) this.material.normalMap = mat.normalMap;
+			if ( mat.metalnessMap ) this.material.metalnessMap = mat.metalnessMap;
+			if ( mat.roughnessMap ) this.material.roughnessMap = mat.roughnessMap;
+			if ( mat.aoMap ) this.material.aoMap = mat.aoMap;
+			if ( mat.envMap ) this.material.envMap = mat.envMap;
+			this.material.needsUpdate = true;
+
+		}
+
+		// Set the mesh's scale to match SDF bounds
+		const sdfBoundsMatrix = this.inverseBoundsMatrix.clone().invert();
+		const boundsCenter = new Vector3();
+		const boundsQuat = new Quaternion();
+		const boundsScale = new Vector3();
+		sdfBoundsMatrix.decompose( boundsCenter, boundsQuat, boundsScale );
+
+		// Apply scale and position
+		this.scale.copy( boundsScale );
+		this.position.copy( boundsCenter );
+		this.updateMatrixWorld();
+
+	}
+
+	onBeforeRender( renderer, scene, camera ) {
+
+		if ( ! this.sdfTexture ) return;
+
+		// Update matrices
+		camera.updateMatrixWorld();
+		this.updateMatrixWorld();
+
+		const depth = 1 / this.resolution;
+
+		// Update custom uniforms
+		this.material.uniforms.sdfTex.value = this.sdfTexture;
+		this.material.uniforms.normalStep.value.set( depth, depth, depth );
+		this.material.uniforms.surface.value = this.surface;
+
+		// Automatically use scene.environment if available
+		if ( scene.environment && ! this.material.envMap ) {
+
+			this.material.envMap = scene.environment;
+			this.material.needsUpdate = true;
+
+		}
+
+		// Compute normal matrix: normalMatrix = transpose(inverse(modelViewMatrix))
+		// For transforming normals from local space to view space
+		const normalMatrix = new Matrix4();
+		normalMatrix.copy( this.modelViewMatrix ).invert().transpose();
+		this.material.uniforms.sdfNormalMatrix.value.copy( normalMatrix );
+
+	}
+
+	dispose() {
+
+		if ( this.sdfTexture ) {
+
+			this.sdfTexture.dispose();
+			this.sdfTexture = null;
+
+		}
+
+		this.geometry.dispose();
+		this.material.dispose();
+
+	}
+
+}

+ 334 - 0
examples/webgl_volume_mesh.html

@@ -0,0 +1,334 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - VolumeMesh</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - VolumeMesh<br/>
+			Generation time: <span id="output">-</span>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.7.8/build/index.module.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+			import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+			import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
+			import { VolumeMesh } from 'three/addons/utils/VolumeMesh.js';
+			import { RenderSDFLayerMaterial } from 'three/addons/utils/RenderSDFLayerMaterial.js';
+
+
+			// Add BVH extension to THREE.BufferGeometry
+			THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
+			THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
+			THREE.Mesh.prototype.raycast = acceleratedRaycast;
+
+			const params = {
+				resolution: 100,
+				margin: 0.05,
+				surface: 0.0,
+				regenerate: () => regenerateVolume(),
+				showMultiple: false,
+				showLayers: true,
+				layer: 0
+			};
+
+			let renderer, camera, scene, gui;
+			let outputContainer;
+			let sourceMesh, sourceMaterial;
+			let volumeMeshes = [];
+			let layerPass;
+
+			init();
+
+			async function init() {
+
+				outputContainer = document.getElementById( 'output' );
+
+				// renderer setup
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( render );
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				document.body.appendChild( renderer.domElement );
+
+				// scene setup
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x111111 );
+
+				// Setup environment map
+				const pmremGenerator = new THREE.PMREMGenerator( renderer );
+				const environment = new RoomEnvironment();
+				const envMapRT = pmremGenerator.fromScene( environment );
+				scene.environment = envMapRT.texture;
+				environment.dispose();
+				pmremGenerator.dispose();
+
+				// camera setup
+				camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
+				camera.position.set( 1, 1, 2 );
+				camera.far = 100;
+				camera.updateProjectionMatrix();
+
+				new OrbitControls( camera, renderer.domElement );
+
+				// screen pass to render a single layer of the 3d texture
+				layerPass = new FullScreenQuad( new RenderSDFLayerMaterial() );
+
+				// Load model
+				new GLTFLoader()
+					.load( 'models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', async ( gltf ) => {
+
+						const object = gltf.scene;
+						object.updateMatrixWorld( true );
+
+						// Get material from first mesh
+						object.traverse( c => {
+
+							if ( c.isMesh && c.material && ! sourceMaterial ) {
+
+								sourceMaterial = c.material;
+
+							}
+
+						} );
+
+						// Merge into single geometry
+						const geometries = [];
+						object.traverse( c => {
+
+							if ( c.geometry ) {
+
+								const cloned = c.geometry.clone();
+								cloned.applyMatrix4( c.matrixWorld );
+								geometries.push( cloned );
+
+							}
+
+						} );
+
+						const mergedGeometry = BufferGeometryUtils.mergeGeometries( geometries );
+						mergedGeometry.center();
+
+						// Compute BVH (required for VolumeMesh)
+						mergedGeometry.computeBoundsTree( { maxLeafTris: 1 } );
+
+						sourceMesh = new THREE.Mesh( mergedGeometry, sourceMaterial );
+
+						// Generate the first volume mesh
+						await regenerateVolume();
+
+					} );
+
+				// GUI
+				gui = new GUI();
+				gui.add( params, 'resolution', 32, 200, 1 ).name( 'Resolution' );
+				gui.add( params, 'margin', 0, 0.5 ).name( 'Margin' );
+				gui.add( params, 'surface', - 0.2, 0.5 ).name( 'Surface' ).onChange( () => {
+
+					volumeMeshes.forEach( v => v.surface = params.surface );
+
+				} );
+				gui.add( params, 'regenerate' ).name( 'Regenerate' );
+				gui.add( params, 'showMultiple' ).name( 'Show Multiple' ).onChange( ( value ) => {
+
+					if ( value ) {
+
+						createMultipleVolumes();
+
+					} else {
+
+						removeExtraVolumes();
+
+					}
+
+				} );
+				gui.add( params, 'showLayers' );
+				gui.add( params, 'layer', 0, params.resolution - 1, 1 );				window.addEventListener( 'resize', onResize );
+
+				window.addEventListener( 'resize', onResize );
+
+			}
+
+			async function regenerateVolume() {
+
+				if ( ! sourceMesh ) return;
+
+				const startTime = window.performance.now();
+
+				// Remove existing volume meshes
+				volumeMeshes.forEach( v => {
+
+					scene.remove( v );
+					v.dispose();
+
+				} );
+				volumeMeshes = [];
+
+				// Create new VolumeMesh - this is all you need!
+				const volume = new VolumeMesh( {
+					resolution: params.resolution,
+					margin: params.margin,
+					surface: params.surface,
+					roughness: 1.0,
+					metalness: 1.0
+				} );
+
+				// Generate the SDF from the source mesh
+				await volume.generate( sourceMesh );
+
+				// Add to scene
+				scene.add( volume );
+				volumeMeshes.push( volume );
+
+				const delta = window.performance.now() - startTime;
+				outputContainer.innerText = `${ delta.toFixed( 2 ) }ms`;
+				console.log( `VolumeMesh generated in ${delta.toFixed( 2 )}ms` );
+
+				// If showing multiple, create them
+				if ( params.showMultiple ) {
+
+					createMultipleVolumes();
+
+				}
+
+			}
+
+			async function createMultipleVolumes() {
+
+				if ( volumeMeshes.length === 0 || ! sourceMesh ) return;
+
+				// Position the first one
+				volumeMeshes[ 0 ].position.x = - 1.5;
+
+				// Create 2 more volumes
+				for ( let i = 1; i < 3; i ++ ) {
+
+					const volume = new VolumeMesh( {
+						resolution: params.resolution,
+						margin: params.margin,
+						surface: params.surface,
+						roughness: params.roughness,
+						metalness: params.metalness
+					} );
+
+					// Reuse the SDF texture from the first volume
+					volume.sdfTexture = volumeMeshes[ 0 ].sdfTexture;
+					volume.inverseBoundsMatrix.copy( volumeMeshes[ 0 ].inverseBoundsMatrix );
+
+					// Copy material properties
+					if ( sourceMesh.material ) {
+
+						const mat = sourceMesh.material;
+						if ( mat.map ) volume.material.map = mat.map;
+						if ( mat.normalMap ) volume.material.normalMap = mat.normalMap;
+						if ( mat.metalnessMap ) volume.material.metalnessMap = mat.metalnessMap;
+						if ( mat.roughnessMap ) volume.material.roughnessMap = mat.roughnessMap;
+						if ( mat.aoMap ) volume.material.aoMap = mat.aoMap;
+						volume.material.needsUpdate = true;
+
+					}
+
+					// Set scale and position
+					const sdfBoundsMatrix = volume.inverseBoundsMatrix.clone().invert();
+					const boundsCenter = new THREE.Vector3();
+					const boundsQuat = new THREE.Quaternion();
+					const boundsScale = new THREE.Vector3();
+					sdfBoundsMatrix.decompose( boundsCenter, boundsQuat, boundsScale );
+
+					volume.scale.copy( boundsScale );
+					volume.position.copy( boundsCenter );
+					volume.position.x = i * 1.5;
+					volume.updateMatrixWorld();
+
+					scene.add( volume );
+					volumeMeshes.push( volume );
+
+				}
+
+			}
+
+			function removeExtraVolumes() {
+
+				// Keep only the first volume
+				while ( volumeMeshes.length > 1 ) {
+
+					const v = volumeMeshes.pop();
+					scene.remove( v );
+					// Don't dispose the shared texture, only dispose materials
+					v.geometry.dispose();
+					v.material.dispose();
+
+				}
+
+				// Reset position of the first one
+				if ( volumeMeshes.length > 0 ) {
+
+					const sdfBoundsMatrix = volumeMeshes[ 0 ].inverseBoundsMatrix.clone().invert();
+					const boundsCenter = new THREE.Vector3();
+					const boundsQuat = new THREE.Quaternion();
+					const boundsScale = new THREE.Vector3();
+					sdfBoundsMatrix.decompose( boundsCenter, boundsQuat, boundsScale );
+
+					volumeMeshes[ 0 ].position.copy( boundsCenter );
+					volumeMeshes[ 0 ].updateMatrixWorld();
+
+				}
+
+			}
+
+			function onResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+				if ( params.showLayers && volumeMeshes.length > 0 ) {
+
+					const layerSize = 256;
+					renderer.setScissorTest( true );
+					renderer.setScissor( 0, window.innerHeight - layerSize, layerSize, layerSize );
+					renderer.setViewport( 0, window.innerHeight - layerSize, layerSize, layerSize );
+
+					layerPass.material.uniforms.sdfTex.value = volumeMeshes[ 0 ].sdfTexture;
+					layerPass.material.uniforms.layer.value = params.layer * ( 1 / params.resolution );
+
+					layerPass.render( renderer );
+
+					renderer.setScissorTest( false );
+					renderer.setViewport( 0, 0, window.innerWidth, window.innerHeight );
+
+				}
+
+			}
+
+		</script>
+
+	</body>
+</html>

粤ICP备19079148号