Skip to content
265 changes: 265 additions & 0 deletions examples/jsm/utils/InstancedVolume.js
Original file line number Diff line number Diff line change
@@ -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;

}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note that checking "insidedness" can only really reliably work if the source model is water tight - has no intersecting triangles, etc. Otherwise you might get an odd pos / neg pattern.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know!


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

}

}
57 changes: 57 additions & 0 deletions examples/jsm/utils/RenderSDFLayerMaterial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ShaderMaterial } from 'three';

export class RenderSDFLayerMaterial extends ShaderMaterial {

constructor( params ) {

super( {
uniforms: {
sdfTex: { value: null },
layer: { value: 0 },
},

vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,

fragmentShader: /* glsl */`
uniform sampler3D sdfTex;
uniform float layer;
varying vec2 vUv;

void main() {
vec4 data = texture( sdfTex, vec3( vUv, layer ) );

// Display three channels side by side
vec3 color;
if ( vUv.x < 0.33 ) {
// Left third: Distance (grayscale, normalized around 0)
float dist = data.r;
float normalized = dist * 0.5 + 0.5; // Map -1,1 to 0,1
color = vec3( normalized );
} else if ( vUv.x < 0.66 ) {
// Middle third: U channel (red, fractional part to handle >1 values)
float u = fract( data.g );
color = vec3( u, 0.0, 0.0 );
} else {
// Right third: V channel (green, fractional part to handle >1 values)
float v = fract( data.b );
color = vec3( 0.0, v, 0.0 );
}

gl_FragColor = vec4( color, 1.0 );

#include <colorspace_fragment>
}
`
} );

this.setValues( params );

}

}
Loading
Loading