From 887b8a8ad2c735bc753b9eaa0f42bc0276777ac6 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Mon, 10 Nov 2025 22:31:05 +0900 Subject: [PATCH 01/12] VolumeMesh exploration. --- examples/jsm/utils/GenerateSDFMaterial.js | 58 ++++ examples/jsm/utils/JumpFloodSDFGenerator.js | 238 +++++++++++++ examples/jsm/utils/RayMarchSDFMaterial.js | 209 ++++++++++++ examples/jsm/utils/RenderSDFLayerMaterial.js | 57 ++++ examples/jsm/utils/VolumeMesh.js | 251 ++++++++++++++ examples/webgl_volume_mesh.html | 334 +++++++++++++++++++ 6 files changed, 1147 insertions(+) create mode 100644 examples/jsm/utils/GenerateSDFMaterial.js create mode 100644 examples/jsm/utils/JumpFloodSDFGenerator.js create mode 100644 examples/jsm/utils/RayMarchSDFMaterial.js create mode 100644 examples/jsm/utils/RenderSDFLayerMaterial.js create mode 100644 examples/jsm/utils/VolumeMesh.js create mode 100644 examples/webgl_volume_mesh.html diff --git a/examples/jsm/utils/GenerateSDFMaterial.js b/examples/jsm/utils/GenerateSDFMaterial.js new file mode 100644 index 00000000000000..c0c0bbf3d279cb --- /dev/null +++ b/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 ); + + } + +} diff --git a/examples/jsm/utils/JumpFloodSDFGenerator.js b/examples/jsm/utils/JumpFloodSDFGenerator.js new file mode 100644 index 00000000000000..321b12e571a681 --- /dev/null +++ b/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; + + } + +} diff --git a/examples/jsm/utils/RayMarchSDFMaterial.js b/examples/jsm/utils/RayMarchSDFMaterial.js new file mode 100644 index 00000000000000..61e16cdea65747 --- /dev/null +++ b/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 ', + `#include + varying vec3 vLocalPosition; + varying vec3 vLocalRayOrigin;` + ); + + shader.vertexShader = shader.vertexShader.replace( + '#include ', + `#include + // 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 ', + `#include + + 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 ', + `#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 ', + `// 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 ', + `#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 ', + `float roughnessFactor = roughness; + #ifdef USE_ROUGHNESSMAP + vec4 texelRoughness = texture2D( roughnessMap, sdfTexUv ); + roughnessFactor *= texelRoughness.g; + #endif` + ); + + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + `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 ', + `#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` + ); + + }; + + } + +} + + diff --git a/examples/jsm/utils/RenderSDFLayerMaterial.js b/examples/jsm/utils/RenderSDFLayerMaterial.js new file mode 100644 index 00000000000000..057b30ab2f0ef3 --- /dev/null +++ b/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 + } + ` + } ); + + this.setValues( params ); + + } + +} diff --git a/examples/jsm/utils/VolumeMesh.js b/examples/jsm/utils/VolumeMesh.js new file mode 100644 index 00000000000000..71734fb38ee4a5 --- /dev/null +++ b/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(); + + } + +} diff --git a/examples/webgl_volume_mesh.html b/examples/webgl_volume_mesh.html new file mode 100644 index 00000000000000..c8b7e7a56dec6c --- /dev/null +++ b/examples/webgl_volume_mesh.html @@ -0,0 +1,334 @@ + + + + three.js webgl - VolumeMesh + + + + + + +
+ three.js webgl - VolumeMesh
+ Generation time: - +
+ + + + + + + From 1107eb58d8c3922943cc63b39c8e3629cffc9e97 Mon Sep 17 00:00:00 2001 From: mrdoob Date: Mon, 10 Nov 2025 05:42:36 -0800 Subject: [PATCH 02/12] Potential fix for code scanning alert no. 3732: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- examples/jsm/utils/VolumeMesh.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/jsm/utils/VolumeMesh.js b/examples/jsm/utils/VolumeMesh.js index 71734fb38ee4a5..059a978959cfab 100644 --- a/examples/jsm/utils/VolumeMesh.js +++ b/examples/jsm/utils/VolumeMesh.js @@ -72,7 +72,6 @@ export class VolumeMesh extends Mesh { this.sdfTexture.magFilter = LinearFilter; const point = new Vector3(); - const ray = new Ray(); const target = { point: new Vector3(), distance: 0, From 4a0869aecba65e212a626e0b4cbb5f404947ac50 Mon Sep 17 00:00:00 2001 From: mrdoob Date: Mon, 10 Nov 2025 05:42:46 -0800 Subject: [PATCH 03/12] Potential fix for code scanning alert no. 3731: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- examples/jsm/utils/JumpFloodSDFGenerator.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/jsm/utils/JumpFloodSDFGenerator.js b/examples/jsm/utils/JumpFloodSDFGenerator.js index 321b12e571a681..dcf31b9f25199e 100644 --- a/examples/jsm/utils/JumpFloodSDFGenerator.js +++ b/examples/jsm/utils/JumpFloodSDFGenerator.js @@ -1,5 +1,4 @@ import { - Vector2, Vector3, Matrix4, Data3DTexture, From f14b2a5012c1ddd23946a51dcbcb2126aa330038 Mon Sep 17 00:00:00 2001 From: mrdoob Date: Mon, 10 Nov 2025 05:43:06 -0800 Subject: [PATCH 04/12] Potential fix for code scanning alert no. 3730: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- examples/jsm/utils/GenerateSDFMaterial.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/jsm/utils/GenerateSDFMaterial.js b/examples/jsm/utils/GenerateSDFMaterial.js index c0c0bbf3d279cb..246fc0feee339b 100644 --- a/examples/jsm/utils/GenerateSDFMaterial.js +++ b/examples/jsm/utils/GenerateSDFMaterial.js @@ -1,4 +1,4 @@ -import { ShaderMaterial, Vector3 } from 'three'; +import { ShaderMaterial } from 'three'; export class GenerateSDFMaterial extends ShaderMaterial { From b25836a0b4476f33b4d932d4491081280155027e Mon Sep 17 00:00:00 2001 From: mrdoob Date: Mon, 10 Nov 2025 05:54:08 -0800 Subject: [PATCH 05/12] Potential fix for code scanning alert no. 3733: Assignment to constant Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- examples/jsm/utils/JumpFloodSDFGenerator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/jsm/utils/JumpFloodSDFGenerator.js b/examples/jsm/utils/JumpFloodSDFGenerator.js index dcf31b9f25199e..a79b25f29a81b1 100644 --- a/examples/jsm/utils/JumpFloodSDFGenerator.js +++ b/examples/jsm/utils/JumpFloodSDFGenerator.js @@ -156,14 +156,14 @@ export class JumpFloodSDFGenerator { matrix.compose( center, quat, scale ); // Create the render targets - const rt1 = new WebGLRenderTarget( dim, dim, { + let rt1 = new WebGLRenderTarget( dim, dim, { format: RGBAFormat, type: FloatType, minFilter: LinearFilter, magFilter: LinearFilter } ); - const rt2 = new WebGLRenderTarget( dim, dim, { + let rt2 = new WebGLRenderTarget( dim, dim, { format: RGBAFormat, type: FloatType, minFilter: LinearFilter, From d14ea4670d1ef3d3cdf67ba376af11d3c52d2ff0 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Wed, 12 Nov 2025 16:27:05 +0900 Subject: [PATCH 06/12] Clean up. --- examples/jsm/utils/GenerateSDFMaterial.js | 58 ----- examples/jsm/utils/JumpFloodSDFGenerator.js | 237 -------------------- examples/jsm/utils/RayMarchSDFMaterial.js | 137 +++++------ examples/webgl_volume_mesh.html | 6 +- 4 files changed, 73 insertions(+), 365 deletions(-) delete mode 100644 examples/jsm/utils/GenerateSDFMaterial.js delete mode 100644 examples/jsm/utils/JumpFloodSDFGenerator.js diff --git a/examples/jsm/utils/GenerateSDFMaterial.js b/examples/jsm/utils/GenerateSDFMaterial.js deleted file mode 100644 index 246fc0feee339b..00000000000000 --- a/examples/jsm/utils/GenerateSDFMaterial.js +++ /dev/null @@ -1,58 +0,0 @@ -import { ShaderMaterial } 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 ); - - } - -} diff --git a/examples/jsm/utils/JumpFloodSDFGenerator.js b/examples/jsm/utils/JumpFloodSDFGenerator.js deleted file mode 100644 index a79b25f29a81b1..00000000000000 --- a/examples/jsm/utils/JumpFloodSDFGenerator.js +++ /dev/null @@ -1,237 +0,0 @@ -import { - 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 - let rt1 = new WebGLRenderTarget( dim, dim, { - format: RGBAFormat, - type: FloatType, - minFilter: LinearFilter, - magFilter: LinearFilter - } ); - - let 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; - - } - -} diff --git a/examples/jsm/utils/RayMarchSDFMaterial.js b/examples/jsm/utils/RayMarchSDFMaterial.js index 61e16cdea65747..e83ea82aad9004 100644 --- a/examples/jsm/utils/RayMarchSDFMaterial.js +++ b/examples/jsm/utils/RayMarchSDFMaterial.js @@ -20,54 +20,60 @@ export class RayMarchSDFMaterial extends MeshStandardMaterial { 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 + // 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 ', - `#include - varying vec3 vLocalPosition; - varying vec3 vLocalRayOrigin;` - ); - - shader.vertexShader = shader.vertexShader.replace( - '#include ', - `#include - // 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 ', - `#include - - 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 + // Modify vertex shader to compute ray in local space + shader.vertexShader = shader.vertexShader.replace( + '#include ', + `#include + varying vec3 vLocalPosition; + varying vec3 vLocalRayOrigin;` + ); + + shader.vertexShader = shader.vertexShader.replace( + '#include ', + `#include + // 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 ', + `#include + + 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() { @@ -100,23 +106,25 @@ export class RayMarchSDFMaterial extends MeshStandardMaterial { 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 ); + } + + 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 ); ` ); @@ -184,9 +192,6 @@ export class RayMarchSDFMaterial extends MeshStandardMaterial { #endif` ); - // Debug output - console.log( 'Shader compiled with defines:', shader.defines ); - // Replace AO sampling shader.fragmentShader = shader.fragmentShader.replace( '#include ', @@ -205,5 +210,3 @@ export class RayMarchSDFMaterial extends MeshStandardMaterial { } } - - diff --git a/examples/webgl_volume_mesh.html b/examples/webgl_volume_mesh.html index c8b7e7a56dec6c..02acee0496616e 100644 --- a/examples/webgl_volume_mesh.html +++ b/examples/webgl_volume_mesh.html @@ -164,7 +164,7 @@ } ); gui.add( params, 'showLayers' ); - gui.add( params, 'layer', 0, params.resolution - 1, 1 ); window.addEventListener( 'resize', onResize ); + gui.add( params, 'layer', 0, params.resolution - 1, 1 ); window.addEventListener( 'resize', onResize ); @@ -228,8 +228,8 @@ resolution: params.resolution, margin: params.margin, surface: params.surface, - roughness: params.roughness, - metalness: params.metalness + roughness: 1.0, + metalness: 1.0 } ); // Reuse the SDF texture from the first volume From c4c8657a4720b6a6ddce857ceee7726ead588054 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Wed, 12 Nov 2025 23:28:53 +0900 Subject: [PATCH 07/12] Renamed material class and reduced allocations. --- examples/jsm/utils/VolumeMesh.js | 59 ++++++++++-------- ...FMaterial.js => VolumeStandardMaterial.js} | 60 ++++++++++++------- examples/webgl_volume_mesh.html | 21 +++---- 3 files changed, 81 insertions(+), 59 deletions(-) rename examples/jsm/utils/{RayMarchSDFMaterial.js => VolumeStandardMaterial.js} (83%) diff --git a/examples/jsm/utils/VolumeMesh.js b/examples/jsm/utils/VolumeMesh.js index 059a978959cfab..0295064890e738 100644 --- a/examples/jsm/utils/VolumeMesh.js +++ b/examples/jsm/utils/VolumeMesh.js @@ -1,12 +1,12 @@ import { Mesh, BoxGeometry, Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three'; -import { RayMarchSDFMaterial } from './RayMarchSDFMaterial.js'; +import { VolumeStandardMaterial } from './VolumeStandardMaterial.js'; export class VolumeMesh extends Mesh { constructor( params = {} ) { const geometry = new BoxGeometry( 1, 1, 1 ); - const material = new RayMarchSDFMaterial( { + const material = new VolumeStandardMaterial( { roughness: params.roughness !== undefined ? params.roughness : 1.0, metalness: params.metalness !== undefined ? params.metalness : 1.0 } ); @@ -79,6 +79,24 @@ export class VolumeMesh extends Mesh { }; const uvAttr = geometry.attributes.uv; + // Reusable objects to avoid allocations in the loop + const ray = new Ray(); + 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 ) + ]; + const v0 = new Vector3(); + const v1 = new Vector3(); + const v2 = new Vector3(); + const barycoord = new Vector3(); + const uv0 = new Vector2(); + const uv1 = new Vector2(); + const uv2 = new Vector2(); + // Iterate over all pixels and check distance for ( let x = 0; x < dim; x ++ ) { @@ -109,22 +127,18 @@ export class VolumeMesh extends Mesh { // 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.origin.copy( point ); + + 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; @@ -143,16 +157,15 @@ export class VolumeMesh extends Mesh { 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 ); + v0.fromBufferAttribute( geometry.attributes.position, i0 ); + v1.fromBufferAttribute( geometry.attributes.position, i1 ); + v2.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 ); + uv0.fromBufferAttribute( uvAttr, i0 ); + uv1.fromBufferAttribute( uvAttr, i1 ); + uv2.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; @@ -225,12 +238,6 @@ export class VolumeMesh extends Mesh { } - // 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() { diff --git a/examples/jsm/utils/RayMarchSDFMaterial.js b/examples/jsm/utils/VolumeStandardMaterial.js similarity index 83% rename from examples/jsm/utils/RayMarchSDFMaterial.js rename to examples/jsm/utils/VolumeStandardMaterial.js index e83ea82aad9004..b88a571fb22043 100644 --- a/examples/jsm/utils/RayMarchSDFMaterial.js +++ b/examples/jsm/utils/VolumeStandardMaterial.js @@ -1,21 +1,22 @@ -import { MeshStandardMaterial, Matrix4, Vector3 } from 'three'; +import { MeshStandardMaterial, Vector3, BackSide } from 'three'; -export class RayMarchSDFMaterial extends MeshStandardMaterial { +export class VolumeStandardMaterial extends MeshStandardMaterial { constructor( params ) { super( params ); + this.side = BackSide; + 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 + MAX_STEPS: 50, + SURFACE_EPSILON: 0.0001 }; this.onBeforeCompile = ( shader ) => { @@ -23,7 +24,6 @@ export class RayMarchSDFMaterial extends MeshStandardMaterial { // 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 @@ -54,7 +54,9 @@ export class RayMarchSDFMaterial extends MeshStandardMaterial { uniform sampler3D sdfTex; uniform vec3 normalStep; - uniform mat4 sdfNormalMatrix; + uniform mat3 normalMatrix; + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; uniform float surface; varying vec3 vLocalPosition; @@ -77,41 +79,59 @@ export class RayMarchSDFMaterial extends MeshStandardMaterial { shader.fragmentShader = shader.fragmentShader.replace( 'void main() {', `void main() { - // Raymarch from camera through the box in local space + // Raymarch from entry point to back face (current fragment) 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 + + // Start from the entry point (or camera if inside) + distToBox = max( distToBox, 0.0 ); + + // Compute distance to back face (current fragment position) + float distToBackFace = length( vLocalPosition - rayOrigin ); + + // Raymarch from entry to back face to find surface in SDF bool intersectsSurface = false; - vec3 localPoint = rayOrigin + rayDirection * ( distToBox + 1e-5 ); - + vec3 localPoint = rayOrigin + rayDirection * distToBox; + float marchDist = distToBox; + for ( int i = 0; i < MAX_STEPS; i ++ ) { + + // Stop if we've reached the back face + if ( marchDist >= distToBackFace ) { + break; + } + 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; + + float stepSize = distanceToSurface * 0.5; + localPoint += rayDirection * stepSize; + marchDist += stepSize; } if ( !intersectsSurface ) { discard; } + // Write correct depth for the raymarched surface + vec4 viewPos = modelViewMatrix * vec4( localPoint, 1.0 ); + vec4 clipPos = projectionMatrix * viewPos; + float ndcDepth = clipPos.z / clipPos.w; + gl_FragDepth = ndcDepth * 0.5 + 0.5; + // Compute UV and normal from SDF vec3 sdfUV = localPoint + vec3( 0.5 ); vec4 sdfData = texture( sdfTex, sdfUV ); @@ -124,7 +144,7 @@ export class RayMarchSDFMaterial extends MeshStandardMaterial { 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 ); + vec3 sdfNormal = normalize( normalMatrix * sdfNormalLocal ); ` ); diff --git a/examples/webgl_volume_mesh.html b/examples/webgl_volume_mesh.html index 02acee0496616e..c42b74eac0f30e 100644 --- a/examples/webgl_volume_mesh.html +++ b/examples/webgl_volume_mesh.html @@ -219,10 +219,10 @@ if ( volumeMeshes.length === 0 || ! sourceMesh ) return; // Position the first one - volumeMeshes[ 0 ].position.x = - 1.5; + volumeMeshes[ 0 ].position.x = 0; // Create 2 more volumes - for ( let i = 1; i < 3; i ++ ) { + for ( let i = 1; i < 100; i ++ ) { const volume = new VolumeMesh( { resolution: params.resolution, @@ -249,17 +249,12 @@ } - // 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(); + volume.position.set( + ( Math.random() - 0.5 ) * 8, + ( Math.random() - 0.5 ) * 8, + ( Math.random() - 0.5 ) * 8 + ); + volume.rotation.set( Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI ); scene.add( volume ); volumeMeshes.push( volume ); From be186bed7b65e3c8d129b39a7b05af57c22078cc Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Wed, 12 Nov 2025 23:41:35 +0900 Subject: [PATCH 08/12] Speed up SDF generation. --- examples/jsm/utils/VolumeMesh.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/examples/jsm/utils/VolumeMesh.js b/examples/jsm/utils/VolumeMesh.js index 0295064890e738..f1ba5767f38c1c 100644 --- a/examples/jsm/utils/VolumeMesh.js +++ b/examples/jsm/utils/VolumeMesh.js @@ -75,7 +75,7 @@ export class VolumeMesh extends Mesh { const target = { point: new Vector3(), distance: 0, - faceIndex: -1 + faceIndex: - 1 }; const uvAttr = geometry.attributes.uv; @@ -125,23 +125,30 @@ export class VolumeMesh extends Mesh { 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; - ray.origin.copy( point ); + // Skip expensive raycasts for points far from surface (definitely outside) + let isInside = false; - for ( let i = 0; i < 6; i ++ ) { + if ( dist < this.margin ) { - ray.direction.copy( directions[ i ] ); - const hit = bvh.raycastFirst( ray, DoubleSide ); - if ( hit && hit.face.normal.dot( ray.direction ) > 0.0 ) { + // If we hit a back face then we're inside + let insideCount = 0; + ray.origin.copy( point ); - insideCount ++; + 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 ++; + + } } - } + isInside = insideCount > 3; - const isInside = insideCount > 3; + } // Set the distance in the texture data this.sdfTexture.image.data[ index + 0 ] = isInside ? - dist : dist; From c9ff3ae6d2dc4afbe701a942872d5b1d123cf190 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Wed, 12 Nov 2025 23:41:44 +0900 Subject: [PATCH 09/12] Clean up. --- examples/webgl_volume_mesh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/webgl_volume_mesh.html b/examples/webgl_volume_mesh.html index c42b74eac0f30e..7abee48b5d76dc 100644 --- a/examples/webgl_volume_mesh.html +++ b/examples/webgl_volume_mesh.html @@ -48,7 +48,7 @@ surface: 0.0, regenerate: () => regenerateVolume(), showMultiple: false, - showLayers: true, + showLayers: false, layer: 0 }; From f6258efa15a9c47ebac985c7c01a0342aafbac08 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Thu, 13 Nov 2025 01:29:11 +0900 Subject: [PATCH 10/12] Renamed to Volume and added InstancedVolume. --- examples/jsm/utils/InstancedVolume.js | 265 ++++++++++++++++++ .../jsm/utils/{VolumeMesh.js => Volume.js} | 2 +- examples/jsm/utils/VolumeStandardMaterial.js | 23 +- examples/webgl_volume_mesh.html | 194 +++++++++---- 4 files changed, 429 insertions(+), 55 deletions(-) create mode 100644 examples/jsm/utils/InstancedVolume.js rename examples/jsm/utils/{VolumeMesh.js => Volume.js} (99%) diff --git a/examples/jsm/utils/InstancedVolume.js b/examples/jsm/utils/InstancedVolume.js new file mode 100644 index 00000000000000..3f6146654080ea --- /dev/null +++ b/examples/jsm/utils/InstancedVolume.js @@ -0,0 +1,265 @@ +import { InstancedMesh, BoxGeometry, Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three'; +import { VolumeStandardMaterial } from './VolumeStandardMaterial.js'; + +export class InstancedVolume extends InstancedMesh { + + constructor( count, params = {} ) { + + const geometry = new BoxGeometry( 1, 1, 1 ); + const material = new VolumeStandardMaterial( { + roughness: params.roughness !== undefined ? params.roughness : 1.0, + metalness: params.metalness !== undefined ? params.metalness : 1.0 + } ); + + super( geometry, material, count ); + + 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 target = { + point: new Vector3(), + distance: 0, + faceIndex: - 1 + }; + const uvAttr = geometry.attributes.uv; + + // Reusable objects to avoid allocations in the loop + const ray = new Ray(); + 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 ) + ]; + const v0 = new Vector3(); + const v1 = new Vector3(); + const v2 = new Vector3(); + const barycoord = new Vector3(); + const uv0 = new Vector2(); + const uv1 = new Vector2(); + const uv2 = new Vector2(); + + // 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 + // Skip expensive raycasts for points far from surface (definitely outside) + let isInside = false; + + if ( dist < this.margin ) { + + // If we hit a back face then we're inside + let insideCount = 0; + ray.origin.copy( point ); + + 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 ++; + + } + + } + + 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 ); + + v0.fromBufferAttribute( geometry.attributes.position, i0 ); + v1.fromBufferAttribute( geometry.attributes.position, i1 ); + v2.fromBufferAttribute( geometry.attributes.position, i2 ); + + Triangle.getBarycoord( target.point, v0, v1, v2, barycoord ); + + uv0.fromBufferAttribute( uvAttr, i0 ); + uv1.fromBufferAttribute( uvAttr, i1 ); + uv2.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 ); + + // For instanced mesh, we set the base scale + // Individual instances can be positioned using setMatrixAt + this.scale.copy( boundsScale ); + this.position.copy( boundsCenter ); + this.updateMatrix(); + + } + + 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; + + } + + } + + dispose() { + + if ( this.sdfTexture ) { + + this.sdfTexture.dispose(); + this.sdfTexture = null; + + } + + this.geometry.dispose(); + this.material.dispose(); + + } + +} diff --git a/examples/jsm/utils/VolumeMesh.js b/examples/jsm/utils/Volume.js similarity index 99% rename from examples/jsm/utils/VolumeMesh.js rename to examples/jsm/utils/Volume.js index f1ba5767f38c1c..031f1c4385b7b1 100644 --- a/examples/jsm/utils/VolumeMesh.js +++ b/examples/jsm/utils/Volume.js @@ -1,7 +1,7 @@ import { Mesh, BoxGeometry, Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three'; import { VolumeStandardMaterial } from './VolumeStandardMaterial.js'; -export class VolumeMesh extends Mesh { +export class Volume extends Mesh { constructor( params = {} ) { diff --git a/examples/jsm/utils/VolumeStandardMaterial.js b/examples/jsm/utils/VolumeStandardMaterial.js index b88a571fb22043..771493b05cb24a 100644 --- a/examples/jsm/utils/VolumeStandardMaterial.js +++ b/examples/jsm/utils/VolumeStandardMaterial.js @@ -35,14 +35,21 @@ export class VolumeStandardMaterial extends MeshStandardMaterial { '#include ', `#include varying vec3 vLocalPosition; - varying vec3 vLocalRayOrigin;` + varying vec3 vLocalRayOrigin; + varying mat4 vInstanceMatrix;` ); shader.vertexShader = shader.vertexShader.replace( '#include ', `#include - // Transform camera position to local space - vLocalRayOrigin = ( inverse( modelMatrix ) * vec4( cameraPosition, 1.0 ) ).xyz; + // Get the instance matrix (identity for non-instanced meshes) + #ifdef USE_INSTANCING + vInstanceMatrix = instanceMatrix; + #else + vInstanceMatrix = mat4( 1.0 ); + #endif + // Transform camera position to local space (accounting for instance transform) + vLocalRayOrigin = ( inverse( modelMatrix * vInstanceMatrix ) * vec4( cameraPosition, 1.0 ) ).xyz; // Vertex position is already in local space vLocalPosition = position;` ); @@ -61,6 +68,7 @@ export class VolumeStandardMaterial extends MeshStandardMaterial { varying vec3 vLocalPosition; varying vec3 vLocalRayOrigin; + varying mat4 vInstanceMatrix; vec2 rayBoxDist( vec3 boundsMin, vec3 boundsMax, vec3 rayOrigin, vec3 rayDir ) { vec3 t0 = ( boundsMin - rayOrigin ) / rayDir; @@ -126,8 +134,8 @@ export class VolumeStandardMaterial extends MeshStandardMaterial { discard; } - // Write correct depth for the raymarched surface - vec4 viewPos = modelViewMatrix * vec4( localPoint, 1.0 ); + // Write correct depth for the raymarched surface (accounting for instance transform) + vec4 viewPos = modelViewMatrix * vInstanceMatrix * vec4( localPoint, 1.0 ); vec4 clipPos = projectionMatrix * viewPos; float ndcDepth = clipPos.z / clipPos.w; gl_FragDepth = ndcDepth * 0.5 + 0.5; @@ -143,8 +151,9 @@ export class VolumeStandardMaterial extends MeshStandardMaterial { 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( normalMatrix * sdfNormalLocal ); + // Transform normal from SDF local space to view space (accounting for instance transform) + mat3 instanceNormalMatrix = mat3( transpose( inverse( vInstanceMatrix ) ) ); + vec3 sdfNormal = normalize( normalMatrix * instanceNormalMatrix * sdfNormalLocal ); ` ); diff --git a/examples/webgl_volume_mesh.html b/examples/webgl_volume_mesh.html index 7abee48b5d76dc..22246e9b8529d4 100644 --- a/examples/webgl_volume_mesh.html +++ b/examples/webgl_volume_mesh.html @@ -1,7 +1,7 @@ - three.js webgl - VolumeMesh + three.js webgl - MeshVolume @@ -9,7 +9,7 @@
- three.js webgl - VolumeMesh
+ three.js webgl - MeshVolume
Generation time: -
@@ -33,7 +33,8 @@ 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 { Volume } from 'three/addons/utils/Volume.js'; + import { InstancedVolume } from 'three/addons/utils/InstancedVolume.js'; import { RenderSDFLayerMaterial } from 'three/addons/utils/RenderSDFLayerMaterial.js'; @@ -43,7 +44,7 @@ THREE.Mesh.prototype.raycast = acceleratedRaycast; const params = { - resolution: 100, + resolution: 64, margin: 0.05, surface: 0.0, regenerate: () => regenerateVolume(), @@ -56,7 +57,9 @@ let outputContainer; let sourceMesh, sourceMaterial; let volumeMeshes = []; + let instancedVolumeMesh; let layerPass; + let pointLight, blueLight; init(); @@ -69,7 +72,7 @@ renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setAnimationLoop( render ); - renderer.toneMapping = THREE.NeutralToneMapping; + renderer.toneMapping = THREE.ACESFilmicToneMapping; document.body.appendChild( renderer.domElement ); // scene setup @@ -80,15 +83,75 @@ const pmremGenerator = new THREE.PMREMGenerator( renderer ); const environment = new RoomEnvironment(); const envMapRT = pmremGenerator.fromScene( environment ); - scene.environment = envMapRT.texture; + // scene.environment = envMapRT.texture; + scene.environmentIntensity = 0.2; environment.dispose(); pmremGenerator.dispose(); + // Helper function to create radial gradient texture (grayscale) + function createRadialGradientTexture() { + + const canvas = document.createElement( 'canvas' ); + canvas.width = 128; + canvas.height = 128; + const context = canvas.getContext( '2d' ); + const gradient = context.createRadialGradient( 64, 64, 0, 64, 64, 64 ); + + // HDR-like center with exponential falloff for realistic glow + gradient.addColorStop( 0, 'rgba(255, 255, 255, 1.0)' ); + gradient.addColorStop( 0.15, 'rgba(255, 255, 255, 0.8)' ); + gradient.addColorStop( 0.35, 'rgba(255, 255, 255, 0.4)' ); + gradient.addColorStop( 0.6, 'rgba(128, 128, 128, 0.15)' ); + gradient.addColorStop( 1, 'rgba(0, 0, 0, 0)' ); + + context.fillStyle = gradient; + context.fillRect( 0, 0, 128, 128 ); + return new THREE.CanvasTexture( canvas ); + + } + + const gradientTexture = createRadialGradientTexture(); + + // Add point light + pointLight = new THREE.PointLight( 0xffffffbb, 20, 20 ); + pointLight.position.set( 2, 2, 2 ); + const whiteSprite = new THREE.Sprite( + new THREE.SpriteMaterial( { + map: gradientTexture, + color: pointLight.color, + blending: THREE.AdditiveBlending, + depthWrite: false + } ) + ); + whiteSprite.scale.setScalar( 0.25 ); + pointLight.add( whiteSprite ); + scene.add( pointLight ); + + // Add blue point light + blueLight = new THREE.PointLight( 0x00ffcc, 20, 20 ); + blueLight.position.set( - 2, 2, - 2 ); + const blueSprite = new THREE.Sprite( + new THREE.SpriteMaterial( { + map: gradientTexture, + color: blueLight.color, + blending: THREE.AdditiveBlending, + depthWrite: false + } ) + ); + blueSprite.scale.setScalar( 0.25 ); + blueLight.add( blueSprite ); + scene.add( blueLight ); + + const dirLight = new THREE.DirectionalLight( 0xffffff, 1.0 ); + dirLight.position.set( 5, 10, 7.5 ); + scene.add( dirLight ); + // 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(); + scene.add( camera ); new OrbitControls( camera, renderer.domElement ); @@ -147,6 +210,7 @@ gui.add( params, 'surface', - 0.2, 0.5 ).name( 'Surface' ).onChange( () => { volumeMeshes.forEach( v => v.surface = params.surface ); + if ( instancedVolumeMesh ) instancedVolumeMesh.surface = params.surface; } ); gui.add( params, 'regenerate' ).name( 'Regenerate' ); @@ -185,8 +249,17 @@ } ); volumeMeshes = []; - // Create new VolumeMesh - this is all you need! - const volume = new VolumeMesh( { + // Remove instanced mesh if it exists + if ( instancedVolumeMesh ) { + + scene.remove( instancedVolumeMesh ); + instancedVolumeMesh.dispose(); + instancedVolumeMesh = null; + + } + + // Create new Volume - this is all you need! + const volume = new Volume( { resolution: params.resolution, margin: params.margin, surface: params.surface, @@ -218,67 +291,82 @@ if ( volumeMeshes.length === 0 || ! sourceMesh ) return; - // Position the first one - volumeMeshes[ 0 ].position.x = 0; + // Remove the single volume mesh + scene.remove( volumeMeshes[ 0 ] ); + + // Create instanced volume mesh + const count = 1000; + instancedVolumeMesh = new InstancedVolume( count, { + resolution: params.resolution, + margin: params.margin, + surface: params.surface, + roughness: 1.0, + metalness: 1.0 + } ); + + // Reuse the SDF texture from the first volume + instancedVolumeMesh.sdfTexture = volumeMeshes[ 0 ].sdfTexture; + instancedVolumeMesh.inverseBoundsMatrix.copy( volumeMeshes[ 0 ].inverseBoundsMatrix ); - // Create 2 more volumes - for ( let i = 1; i < 100; i ++ ) { + // Copy material properties + if ( sourceMesh.material ) { - const volume = new VolumeMesh( { - resolution: params.resolution, - margin: params.margin, - surface: params.surface, - roughness: 1.0, - metalness: 1.0 - } ); + const mat = sourceMesh.material; + if ( mat.map ) instancedVolumeMesh.material.map = mat.map; + if ( mat.normalMap ) instancedVolumeMesh.material.normalMap = mat.normalMap; + if ( mat.metalnessMap ) instancedVolumeMesh.material.metalnessMap = mat.metalnessMap; + if ( mat.roughnessMap ) instancedVolumeMesh.material.roughnessMap = mat.roughnessMap; + if ( mat.aoMap ) instancedVolumeMesh.material.aoMap = mat.aoMap; + instancedVolumeMesh.material.needsUpdate = true; - // 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 ) { + // Set up instance matrices + const transform = new THREE.Object3D(); - 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; + for ( let i = 0; i < count; i ++ ) { - } + transform.position.set( + ( Math.random() - 0.5 ) * 18, + ( Math.random() - 0.5 ) * 18, + ( Math.random() - 0.5 ) * 18 + ); - volume.position.set( - ( Math.random() - 0.5 ) * 8, - ( Math.random() - 0.5 ) * 8, - ( Math.random() - 0.5 ) * 8 + transform.rotation.set( + Math.random() * Math.PI, + Math.random() * Math.PI, + Math.random() * Math.PI ); - volume.rotation.set( Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI ); - scene.add( volume ); - volumeMeshes.push( volume ); + transform.updateMatrix(); + instancedVolumeMesh.setMatrixAt( i, transform.matrix ); } + instancedVolumeMesh.instanceMatrix.needsUpdate = true; + + scene.add( instancedVolumeMesh ); + } function removeExtraVolumes() { - // Keep only the first volume - while ( volumeMeshes.length > 1 ) { + // Remove instanced mesh if it exists + if ( instancedVolumeMesh ) { - const v = volumeMeshes.pop(); - scene.remove( v ); - // Don't dispose the shared texture, only dispose materials - v.geometry.dispose(); - v.material.dispose(); + scene.remove( instancedVolumeMesh ); + // Don't dispose the shared texture + instancedVolumeMesh.geometry.dispose(); + instancedVolumeMesh.material.dispose(); + instancedVolumeMesh = null; } - // Reset position of the first one + // Add back the single volume mesh if ( volumeMeshes.length > 0 ) { + scene.add( volumeMeshes[ 0 ] ); + const sdfBoundsMatrix = volumeMeshes[ 0 ].inverseBoundsMatrix.clone().invert(); const boundsCenter = new THREE.Vector3(); const boundsQuat = new THREE.Quaternion(); @@ -302,6 +390,18 @@ function render() { + // Animate point light in a circle + const time = Date.now() * 0.001; + const radius = 2; + pointLight.position.x = Math.cos( time ) * radius; + pointLight.position.z = Math.sin( time ) * radius; + pointLight.position.y = Math.sin( time * 0.5 ) * radius; + + // Animate blue light in a different pattern + blueLight.position.x = Math.sin( time * 1.3 ) * radius; + blueLight.position.z = Math.cos( time * 1.3 ) * radius; + blueLight.position.y = Math.cos( time * 0.7 ) * radius; + renderer.render( scene, camera ); if ( params.showLayers && volumeMeshes.length > 0 ) { From 2b325b42048ecee8443392c1ce5561dcc9ad93fb Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Thu, 13 Nov 2025 16:55:44 +0900 Subject: [PATCH 11/12] Clean up. --- examples/jsm/utils/InstancedVolume.js | 169 +------------------------ examples/jsm/utils/Volume.js | 169 +------------------------ examples/jsm/utils/VolumeGenerator.js | 174 ++++++++++++++++++++++++++ examples/webgl_volume_mesh.html | 8 +- 4 files changed, 192 insertions(+), 328 deletions(-) create mode 100644 examples/jsm/utils/VolumeGenerator.js diff --git a/examples/jsm/utils/InstancedVolume.js b/examples/jsm/utils/InstancedVolume.js index 3f6146654080ea..a2c0b188eec001 100644 --- a/examples/jsm/utils/InstancedVolume.js +++ b/examples/jsm/utils/InstancedVolume.js @@ -1,5 +1,6 @@ -import { InstancedMesh, BoxGeometry, Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three'; +import { InstancedMesh, BoxGeometry, Matrix4, Vector3, Quaternion } from 'three'; import { VolumeStandardMaterial } from './VolumeStandardMaterial.js'; +import { VolumeGenerator } from './VolumeGenerator.js'; export class InstancedVolume extends InstancedMesh { @@ -24,34 +25,6 @@ export class InstancedVolume extends InstancedMesh { 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 ) { @@ -59,140 +32,10 @@ export class InstancedVolume extends InstancedMesh { } - 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 target = { - point: new Vector3(), - distance: 0, - faceIndex: - 1 - }; - const uvAttr = geometry.attributes.uv; - - // Reusable objects to avoid allocations in the loop - const ray = new Ray(); - 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 ) - ]; - const v0 = new Vector3(); - const v1 = new Vector3(); - const v2 = new Vector3(); - const barycoord = new Vector3(); - const uv0 = new Vector2(); - const uv1 = new Vector2(); - const uv2 = new Vector2(); - - // 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 - // Skip expensive raycasts for points far from surface (definitely outside) - let isInside = false; - - if ( dist < this.margin ) { - - // If we hit a back face then we're inside - let insideCount = 0; - ray.origin.copy( point ); - - 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 ++; - - } - - } - - 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 ); - - v0.fromBufferAttribute( geometry.attributes.position, i0 ); - v1.fromBufferAttribute( geometry.attributes.position, i1 ); - v2.fromBufferAttribute( geometry.attributes.position, i2 ); - - Triangle.getBarycoord( target.point, v0, v1, v2, barycoord ); - - uv0.fromBufferAttribute( uvAttr, i0 ); - uv1.fromBufferAttribute( uvAttr, i1 ); - uv2.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' ); + // Generate the SDF using the shared generator + const result = await VolumeGenerator.generateSDF( sourceMesh, this.resolution, this.margin ); + this.sdfTexture = result.sdfTexture; + this.inverseBoundsMatrix = result.inverseBoundsMatrix; // Copy textures from source mesh material if available if ( sourceMesh.material ) { diff --git a/examples/jsm/utils/Volume.js b/examples/jsm/utils/Volume.js index 031f1c4385b7b1..d97a0e95415c38 100644 --- a/examples/jsm/utils/Volume.js +++ b/examples/jsm/utils/Volume.js @@ -1,5 +1,6 @@ -import { Mesh, BoxGeometry, Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three'; +import { Mesh, BoxGeometry, Matrix4, Vector3, Quaternion } from 'three'; import { VolumeStandardMaterial } from './VolumeStandardMaterial.js'; +import { VolumeGenerator } from './VolumeGenerator.js'; export class Volume extends Mesh { @@ -24,34 +25,6 @@ export class Volume extends Mesh { 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 ) { @@ -59,140 +32,10 @@ export class Volume extends Mesh { } - 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 target = { - point: new Vector3(), - distance: 0, - faceIndex: - 1 - }; - const uvAttr = geometry.attributes.uv; - - // Reusable objects to avoid allocations in the loop - const ray = new Ray(); - 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 ) - ]; - const v0 = new Vector3(); - const v1 = new Vector3(); - const v2 = new Vector3(); - const barycoord = new Vector3(); - const uv0 = new Vector2(); - const uv1 = new Vector2(); - const uv2 = new Vector2(); - - // 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 - // Skip expensive raycasts for points far from surface (definitely outside) - let isInside = false; - - if ( dist < this.margin ) { - - // If we hit a back face then we're inside - let insideCount = 0; - ray.origin.copy( point ); - - 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 ++; - - } - - } - - 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 ); - - v0.fromBufferAttribute( geometry.attributes.position, i0 ); - v1.fromBufferAttribute( geometry.attributes.position, i1 ); - v2.fromBufferAttribute( geometry.attributes.position, i2 ); - - Triangle.getBarycoord( target.point, v0, v1, v2, barycoord ); - - uv0.fromBufferAttribute( uvAttr, i0 ); - uv1.fromBufferAttribute( uvAttr, i1 ); - uv2.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' ); + // Generate the SDF using the shared generator + const result = await VolumeGenerator.generateSDF( sourceMesh, this.resolution, this.margin ); + this.sdfTexture = result.sdfTexture; + this.inverseBoundsMatrix = result.inverseBoundsMatrix; // Copy textures from source mesh material if available if ( sourceMesh.material ) { diff --git a/examples/jsm/utils/VolumeGenerator.js b/examples/jsm/utils/VolumeGenerator.js new file mode 100644 index 00000000000000..a626555b53fd54 --- /dev/null +++ b/examples/jsm/utils/VolumeGenerator.js @@ -0,0 +1,174 @@ +import { Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three'; + +export class VolumeGenerator { + + static async generateSDF( sourceMesh, resolution, margin ) { + + 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 including the margin + if ( ! geometry.boundingBox ) geometry.computeBoundingBox(); + + geometry.boundingBox.getCenter( center ); + scale.subVectors( geometry.boundingBox.max, geometry.boundingBox.min ); + scale.x += 2 * margin; + scale.y += 2 * margin; + scale.z += 2 * margin; + matrix.compose( center, quat, scale ); + const inverseBoundsMatrix = new Matrix4().copy( matrix ).invert(); + + 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 + const sdfTexture = new Data3DTexture( new Float32Array( dim ** 3 * 4 ), dim, dim, dim ); + sdfTexture.format = RGBAFormat; + sdfTexture.type = FloatType; + sdfTexture.minFilter = LinearFilter; + sdfTexture.magFilter = LinearFilter; + + const point = new Vector3(); + const target = { + point: new Vector3(), + distance: 0, + faceIndex: - 1 + }; + const uvAttr = geometry.attributes.uv; + + // Reusable objects to avoid allocations in the loop + const ray = new Ray(); + 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 ) + ]; + const v0 = new Vector3(); + const v1 = new Vector3(); + const v2 = new Vector3(); + const barycoord = new Vector3(); + const uv0 = new Vector2(); + const uv1 = new Vector2(); + const uv2 = new Vector2(); + + // 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 + // Skip expensive raycasts for points far from surface (definitely outside) + let isInside = false; + + if ( dist < margin ) { + + // If we hit a back face then we're inside + let insideCount = 0; + ray.origin.copy( point ); + + 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 ++; + + } + + } + + isInside = insideCount > 3; + + } + + // Set the distance in the texture data + 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 ); + + v0.fromBufferAttribute( geometry.attributes.position, i0 ); + v1.fromBufferAttribute( geometry.attributes.position, i1 ); + v2.fromBufferAttribute( geometry.attributes.position, i2 ); + + Triangle.getBarycoord( target.point, v0, v1, v2, barycoord ); + + uv0.fromBufferAttribute( uvAttr, i0 ); + uv1.fromBufferAttribute( uvAttr, i1 ); + uv2.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 + sdfTexture.image.data[ index + 1 ] = u; + sdfTexture.image.data[ index + 2 ] = v; + sdfTexture.image.data[ index + 3 ] = 0; // Alpha unused + + } + + } + + } + + sdfTexture.needsUpdate = true; + + console.log( 'SDF generation completed' ); + + return { sdfTexture, inverseBoundsMatrix }; + + } + +} diff --git a/examples/webgl_volume_mesh.html b/examples/webgl_volume_mesh.html index 22246e9b8529d4..3d89d01be38eb9 100644 --- a/examples/webgl_volume_mesh.html +++ b/examples/webgl_volume_mesh.html @@ -59,7 +59,7 @@ let volumeMeshes = []; let instancedVolumeMesh; let layerPass; - let pointLight, blueLight; + // let pointLight, blueLight; init(); @@ -83,11 +83,12 @@ const pmremGenerator = new THREE.PMREMGenerator( renderer ); const environment = new RoomEnvironment(); const envMapRT = pmremGenerator.fromScene( environment ); - // scene.environment = envMapRT.texture; + scene.environment = envMapRT.texture; scene.environmentIntensity = 0.2; environment.dispose(); pmremGenerator.dispose(); + /* // Helper function to create radial gradient texture (grayscale) function createRadialGradientTexture() { @@ -145,6 +146,7 @@ const dirLight = new THREE.DirectionalLight( 0xffffff, 1.0 ); dirLight.position.set( 5, 10, 7.5 ); scene.add( dirLight ); + */ // camera setup camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 ); @@ -390,6 +392,7 @@ function render() { + /* // Animate point light in a circle const time = Date.now() * 0.001; const radius = 2; @@ -401,6 +404,7 @@ blueLight.position.x = Math.sin( time * 1.3 ) * radius; blueLight.position.z = Math.cos( time * 1.3 ) * radius; blueLight.position.y = Math.cos( time * 0.7 ) * radius; + */ renderer.render( scene, camera ); From 83a6172961adbdcbed3ae3fb71df7e31b0bdb616 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Thu, 13 Nov 2025 16:56:36 +0900 Subject: [PATCH 12/12] Clean up. --- examples/webgl_volume_mesh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/webgl_volume_mesh.html b/examples/webgl_volume_mesh.html index 3d89d01be38eb9..99a3cd85dc5cf9 100644 --- a/examples/webgl_volume_mesh.html +++ b/examples/webgl_volume_mesh.html @@ -44,7 +44,7 @@ THREE.Mesh.prototype.raycast = acceleratedRaycast; const params = { - resolution: 64, + resolution: 100, margin: 0.05, surface: 0.0, regenerate: () => regenerateVolume(),