From 26a71ef06f4655c31a5202293fe33af602f43948 Mon Sep 17 00:00:00 2001 From: mrdoob Date: Sat, 6 Dec 2025 12:50:02 +0900 Subject: [PATCH 1/3] VOXLoader: Replace VOXMesh and VOXData3DTexture classes with functions. (#32491) --- examples/jsm/loaders/VOXLoader.js | 394 ++++++++++++++------------ examples/webgl_loader_vox.html | 2 +- examples/webgl_volume_instancing.html | 4 +- 3 files changed, 209 insertions(+), 191 deletions(-) diff --git a/examples/jsm/loaders/VOXLoader.js b/examples/jsm/loaders/VOXLoader.js index eeede05a3eb468..5421e3ec096a4e 100644 --- a/examples/jsm/loaders/VOXLoader.js +++ b/examples/jsm/loaders/VOXLoader.js @@ -165,7 +165,7 @@ function buildObject( nodeId, nodes, chunks ) { if ( childNode.type === 'shape' && childNode.models.length === 1 ) { const chunk = chunks[ childNode.models[ 0 ].modelId ]; - const mesh = new VOXMesh( chunk ); + const mesh = buildMesh( chunk ); applyTransform( mesh, node ); return mesh; @@ -208,7 +208,7 @@ function buildObject( nodeId, nodes, chunks ) { if ( node.models.length === 1 ) { const chunk = chunks[ node.models[ 0 ].modelId ]; - return new VOXMesh( chunk ); + return buildMesh( chunk ); } @@ -217,7 +217,7 @@ function buildObject( nodeId, nodes, chunks ) { for ( const model of node.models ) { const chunk = chunks[ model.modelId ]; - group.add( new VOXMesh( chunk ) ); + group.add( buildMesh( chunk ) ); } @@ -596,231 +596,221 @@ class VOXLoader extends Loader { } /** - * A VOX mesh. + * Builds a mesh from a VOX chunk. * - * Instances of this class are created from the loaded chunks of {@link VOXLoader}. - * - * @augments Mesh + * @param {Object} chunk - A VOX chunk loaded via {@link VOXLoader}. + * @return {Mesh} The generated mesh. */ -class VOXMesh extends Mesh { - - /** - * Constructs a new VOX mesh. - * - * @param {Object} chunk - A VOX chunk loaded via {@link VOXLoader}. - */ - constructor( chunk ) { - - const data = chunk.data; - const size = chunk.size; - const palette = chunk.palette; - - const sx = size.x; - const sy = size.y; - const sz = size.z; +function buildMesh( chunk ) { - // Build volume with color indices + const data = chunk.data; + const size = chunk.size; + const palette = chunk.palette; - const volume = new Uint8Array( sx * sy * sz ); + const sx = size.x; + const sy = size.y; + const sz = size.z; - for ( let j = 0; j < data.length; j += 4 ) { + // Build volume with color indices - const x = data[ j + 0 ]; - const y = data[ j + 1 ]; - const z = data[ j + 2 ]; - const c = data[ j + 3 ]; + const volume = new Uint8Array( sx * sy * sz ); - volume[ x + y * sx + z * sx * sy ] = c; + for ( let j = 0; j < data.length; j += 4 ) { - } + const x = data[ j + 0 ]; + const y = data[ j + 1 ]; + const z = data[ j + 2 ]; + const c = data[ j + 3 ]; - // Greedy meshing + volume[ x + y * sx + z * sx * sy ] = c; - const vertices = []; - const indices = []; - const colors = []; + } - const _color = new Color(); - let hasColors = false; + // Greedy meshing - // Process each of the 6 face directions - // dims: the 3 axis sizes, d: which axis is normal to the face - const dims = [ sx, sy, sz ]; + const vertices = []; + const indices = []; + const colors = []; - for ( let d = 0; d < 3; d ++ ) { + const _color = new Color(); + let hasColors = false; - const u = ( d + 1 ) % 3; - const v = ( d + 2 ) % 3; + // Process each of the 6 face directions + // dims: the 3 axis sizes, d: which axis is normal to the face + const dims = [ sx, sy, sz ]; - const dimsD = dims[ d ]; - const dimsU = dims[ u ]; - const dimsV = dims[ v ]; + for ( let d = 0; d < 3; d ++ ) { - const q = [ 0, 0, 0 ]; - const mask = new Int16Array( dimsU * dimsV ); + const u = ( d + 1 ) % 3; + const v = ( d + 2 ) % 3; - q[ d ] = 1; + const dimsD = dims[ d ]; + const dimsU = dims[ u ]; + const dimsV = dims[ v ]; - // Sweep through slices - for ( let slice = 0; slice <= dimsD; slice ++ ) { + const q = [ 0, 0, 0 ]; + const mask = new Int16Array( dimsU * dimsV ); - // Build mask for this slice - let n = 0; + q[ d ] = 1; - for ( let vv = 0; vv < dimsV; vv ++ ) { + // Sweep through slices + for ( let slice = 0; slice <= dimsD; slice ++ ) { - for ( let uu = 0; uu < dimsU; uu ++ ) { + // Build mask for this slice + let n = 0; - const pos = [ 0, 0, 0 ]; - pos[ d ] = slice; - pos[ u ] = uu; - pos[ v ] = vv; + for ( let vv = 0; vv < dimsV; vv ++ ) { - const x0 = pos[ 0 ], y0 = pos[ 1 ], z0 = pos[ 2 ]; + for ( let uu = 0; uu < dimsU; uu ++ ) { - // Get voxel behind and in front of this face - const behind = ( slice > 0 ) ? volume[ ( x0 - q[ 0 ] ) + ( y0 - q[ 1 ] ) * sx + ( z0 - q[ 2 ] ) * sx * sy ] : 0; - const infront = ( slice < dimsD ) ? volume[ x0 + y0 * sx + z0 * sx * sy ] : 0; + const pos = [ 0, 0, 0 ]; + pos[ d ] = slice; + pos[ u ] = uu; + pos[ v ] = vv; - // Face exists if exactly one side is solid - if ( behind > 0 && infront === 0 ) { + const x0 = pos[ 0 ], y0 = pos[ 1 ], z0 = pos[ 2 ]; - mask[ n ] = behind; // positive face + // Get voxel behind and in front of this face + const behind = ( slice > 0 ) ? volume[ ( x0 - q[ 0 ] ) + ( y0 - q[ 1 ] ) * sx + ( z0 - q[ 2 ] ) * sx * sy ] : 0; + const infront = ( slice < dimsD ) ? volume[ x0 + y0 * sx + z0 * sx * sy ] : 0; - } else if ( infront > 0 && behind === 0 ) { + // Face exists if exactly one side is solid + if ( behind > 0 && infront === 0 ) { - mask[ n ] = - infront; // negative face + mask[ n ] = behind; // positive face - } else { + } else if ( infront > 0 && behind === 0 ) { - mask[ n ] = 0; + mask[ n ] = - infront; // negative face - } + } else { - n ++; + mask[ n ] = 0; } + n ++; + } - // Greedy merge mask into quads - n = 0; + } - for ( let vv = 0; vv < dimsV; vv ++ ) { + // Greedy merge mask into quads + n = 0; - for ( let uu = 0; uu < dimsU; ) { + for ( let vv = 0; vv < dimsV; vv ++ ) { - const c = mask[ n ]; + for ( let uu = 0; uu < dimsU; ) { - if ( c !== 0 ) { + const c = mask[ n ]; - // Find width - let w = 1; + if ( c !== 0 ) { - while ( uu + w < dimsU && mask[ n + w ] === c ) { + // Find width + let w = 1; - w ++; + while ( uu + w < dimsU && mask[ n + w ] === c ) { - } + w ++; - // Find height - let h = 1; - let done = false; + } - while ( vv + h < dimsV && ! done ) { + // Find height + let h = 1; + let done = false; - for ( let k = 0; k < w; k ++ ) { + while ( vv + h < dimsV && ! done ) { - if ( mask[ n + k + h * dimsU ] !== c ) { + for ( let k = 0; k < w; k ++ ) { - done = true; - break; + if ( mask[ n + k + h * dimsU ] !== c ) { - } + done = true; + break; } - if ( ! done ) h ++; - } - // Add quad - const pos = [ 0, 0, 0 ]; - pos[ d ] = slice; - pos[ u ] = uu; - pos[ v ] = vv; + if ( ! done ) h ++; - const du = [ 0, 0, 0 ]; - const dv = [ 0, 0, 0 ]; - du[ u ] = w; - dv[ v ] = h; + } - // Get color - const colorIndex = Math.abs( c ); - const hex = palette[ colorIndex ]; - const r = ( hex >> 0 & 0xff ) / 0xff; - const g = ( hex >> 8 & 0xff ) / 0xff; - const b = ( hex >> 16 & 0xff ) / 0xff; + // Add quad + const pos = [ 0, 0, 0 ]; + pos[ d ] = slice; + pos[ u ] = uu; + pos[ v ] = vv; - if ( r > 0 || g > 0 || b > 0 ) hasColors = true; + const du = [ 0, 0, 0 ]; + const dv = [ 0, 0, 0 ]; + du[ u ] = w; + dv[ v ] = h; - _color.setRGB( r, g, b, SRGBColorSpace ); + // Get color + const colorIndex = Math.abs( c ); + const hex = palette[ colorIndex ]; + const r = ( hex >> 0 & 0xff ) / 0xff; + const g = ( hex >> 8 & 0xff ) / 0xff; + const b = ( hex >> 16 & 0xff ) / 0xff; - // Convert VOX coords to Three.js coords (Y-up) - // VOX: X right, Y forward, Z up -> Three.js: X right, Y up, Z back - const toThree = ( p ) => [ - p[ 0 ] - sx / 2, - p[ 2 ] - sz / 2, - - p[ 1 ] + sy / 2 - ]; + if ( r > 0 || g > 0 || b > 0 ) hasColors = true; - const v0 = toThree( pos ); - const v1 = toThree( [ pos[ 0 ] + du[ 0 ], pos[ 1 ] + du[ 1 ], pos[ 2 ] + du[ 2 ] ] ); - const v2 = toThree( [ pos[ 0 ] + du[ 0 ] + dv[ 0 ], pos[ 1 ] + du[ 1 ] + dv[ 1 ], pos[ 2 ] + du[ 2 ] + dv[ 2 ] ] ); - const v3 = toThree( [ pos[ 0 ] + dv[ 0 ], pos[ 1 ] + dv[ 1 ], pos[ 2 ] + dv[ 2 ] ] ); + _color.setRGB( r, g, b, SRGBColorSpace ); - const idx = vertices.length / 3; + // Convert VOX coords to Three.js coords (Y-up) + // VOX: X right, Y forward, Z up -> Three.js: X right, Y up, Z back + const toThree = ( p ) => [ + p[ 0 ] - sx / 2, + p[ 2 ] - sz / 2, + - p[ 1 ] + sy / 2 + ]; - // Winding order depends on face direction - if ( c > 0 ) { + const v0 = toThree( pos ); + const v1 = toThree( [ pos[ 0 ] + du[ 0 ], pos[ 1 ] + du[ 1 ], pos[ 2 ] + du[ 2 ] ] ); + const v2 = toThree( [ pos[ 0 ] + du[ 0 ] + dv[ 0 ], pos[ 1 ] + du[ 1 ] + dv[ 1 ], pos[ 2 ] + du[ 2 ] + dv[ 2 ] ] ); + const v3 = toThree( [ pos[ 0 ] + dv[ 0 ], pos[ 1 ] + dv[ 1 ], pos[ 2 ] + dv[ 2 ] ] ); - vertices.push( ...v0, ...v1, ...v2, ...v3 ); - indices.push( idx, idx + 1, idx + 2, idx, idx + 2, idx + 3 ); + const idx = vertices.length / 3; - } else { + // Winding order depends on face direction + if ( c > 0 ) { - vertices.push( ...v0, ...v3, ...v2, ...v1 ); - indices.push( idx, idx + 1, idx + 2, idx, idx + 2, idx + 3 ); + vertices.push( ...v0, ...v1, ...v2, ...v3 ); + indices.push( idx, idx + 1, idx + 2, idx, idx + 2, idx + 3 ); - } + } else { + + vertices.push( ...v0, ...v3, ...v2, ...v1 ); + indices.push( idx, idx + 1, idx + 2, idx, idx + 2, idx + 3 ); - colors.push( - _color.r, _color.g, _color.b, - _color.r, _color.g, _color.b, - _color.r, _color.g, _color.b, - _color.r, _color.g, _color.b - ); + } - // Clear mask - for ( let hh = 0; hh < h; hh ++ ) { + colors.push( + _color.r, _color.g, _color.b, + _color.r, _color.g, _color.b, + _color.r, _color.g, _color.b, + _color.r, _color.g, _color.b + ); - for ( let ww = 0; ww < w; ww ++ ) { + // Clear mask + for ( let hh = 0; hh < h; hh ++ ) { - mask[ n + ww + hh * dimsU ] = 0; + for ( let ww = 0; ww < w; ww ++ ) { - } + mask[ n + ww + hh * dimsU ] = 0; } - uu += w; - n += w; + } - } else { + uu += w; + n += w; - uu ++; - n ++; + } else { - } + uu ++; + n ++; } @@ -830,72 +820,100 @@ class VOXMesh extends Mesh { } - const geometry = new BufferGeometry(); - geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); - geometry.setIndex( indices ); - geometry.computeVertexNormals(); - - const material = new MeshStandardMaterial(); + } - if ( hasColors ) { + const geometry = new BufferGeometry(); + geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); + geometry.setIndex( indices ); + geometry.computeVertexNormals(); - geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) ); - material.vertexColors = true; + const material = new MeshStandardMaterial(); - } + if ( hasColors ) { - super( geometry, material ); + geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) ); + material.vertexColors = true; } + return new Mesh( geometry, material ); + } /** - * A VOX 3D texture. - * - * Instances of this class are created from the loaded chunks of {@link VOXLoader}. + * Builds a 3D texture from a VOX chunk. * - * @augments Data3DTexture + * @param {Object} chunk - A VOX chunk loaded via {@link VOXLoader}. + * @return {Data3DTexture} The generated 3D texture. */ -class VOXData3DTexture extends Data3DTexture { +function buildData3DTexture( chunk ) { + + const data = chunk.data; + const size = chunk.size; + + const offsety = size.x; + const offsetz = size.x * size.y; + + const array = new Uint8Array( size.x * size.y * size.z ); + + for ( let j = 0; j < data.length; j += 4 ) { + + const x = data[ j + 0 ]; + const y = data[ j + 1 ]; + const z = data[ j + 2 ]; + + const index = x + ( y * offsety ) + ( z * offsetz ); + + array[ index ] = 255; + + } + + const texture = new Data3DTexture( array, size.x, size.y, size.z ); + + texture.format = RedFormat; + texture.minFilter = NearestFilter; + texture.magFilter = LinearFilter; + texture.unpackAlignment = 1; + texture.needsUpdate = true; + + return texture; + +} + +// @deprecated, r182 + +class VOXMesh extends Mesh { - /** - * Constructs a new VOX 3D texture. - * - * @param {Object} chunk - A VOX chunk loaded via {@link VOXLoader}. - */ constructor( chunk ) { - const data = chunk.data; - const size = chunk.size; + console.warn( 'VOXMesh has been deprecated. Use buildMesh() instead.' ); - const offsety = size.x; - const offsetz = size.x * size.y; + const mesh = buildMesh( chunk ); - const array = new Uint8Array( size.x * size.y * size.z ); + super( mesh.geometry, mesh.material ); + + } - for ( let j = 0; j < data.length; j += 4 ) { +} - const x = data[ j + 0 ]; - const y = data[ j + 1 ]; - const z = data[ j + 2 ]; +class VOXData3DTexture extends Data3DTexture { - const index = x + ( y * offsety ) + ( z * offsetz ); + constructor( chunk ) { - array[ index ] = 255; + console.warn( 'VOXData3DTexture has been deprecated. Use buildData3DTexture() instead.' ); - } + const texture = buildData3DTexture( chunk ); - super( array, size.x, size.y, size.z ); + super( texture.image.data, texture.image.width, texture.image.height, texture.image.depth ); - this.format = RedFormat; - this.minFilter = NearestFilter; - this.magFilter = LinearFilter; - this.unpackAlignment = 1; + this.format = texture.format; + this.minFilter = texture.minFilter; + this.magFilter = texture.magFilter; + this.unpackAlignment = texture.unpackAlignment; this.needsUpdate = true; } } -export { VOXLoader, VOXMesh, VOXData3DTexture }; +export { VOXLoader, buildMesh, buildData3DTexture, VOXMesh, VOXData3DTexture }; diff --git a/examples/webgl_loader_vox.html b/examples/webgl_loader_vox.html index f7f8a44b860583..dc2a43151b1a57 100644 --- a/examples/webgl_loader_vox.html +++ b/examples/webgl_loader_vox.html @@ -26,7 +26,7 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; - import { VOXLoader, VOXMesh } from 'three/addons/loaders/VOXLoader.js'; + import { VOXLoader } from 'three/addons/loaders/VOXLoader.js'; let camera, controls, scene, renderer; diff --git a/examples/webgl_volume_instancing.html b/examples/webgl_volume_instancing.html index 2807d525090a4b..78c0a04bfb49e5 100644 --- a/examples/webgl_volume_instancing.html +++ b/examples/webgl_volume_instancing.html @@ -24,7 +24,7 @@