Browse Source

VOXLoader: Added scene graph support. (#32488)

mrdoob 2 months ago
parent
commit
bf440d9f42
3 changed files with 413 additions and 42 deletions
  1. 4 14
      editor/js/Loader.js
  2. 404 16
      examples/jsm/loaders/VOXLoader.js
  3. 5 12
      examples/webgl_loader_vox.html

+ 4 - 14
editor/js/Loader.js

@@ -675,23 +675,13 @@ function Loader( editor ) {
 
 					const contents = event.target.result;
 
-					const { VOXLoader, VOXMesh } = await import( 'three/addons/loaders/VOXLoader.js' );
+					const { VOXLoader } = await import( 'three/addons/loaders/VOXLoader.js' );
 
-					const chunks = new VOXLoader().parse( contents );
+					const { scene } = new VOXLoader().parse( contents );
 
-					const group = new THREE.Group();
-					group.name = filename;
-
-					for ( let i = 0; i < chunks.length; i ++ ) {
-
-						const chunk = chunks[ i ];
+					scene.name = filename;
 
-						const mesh = new VOXMesh( chunk );
-						group.add( mesh );
-
-					}
-
-					editor.execute( new AddObjectCommand( editor, group ) );
+					editor.execute( new AddObjectCommand( editor, scene ) );
 
 				}, false );
 				reader.readAsArrayBuffer( file );

+ 404 - 16
examples/jsm/loaders/VOXLoader.js

@@ -4,8 +4,10 @@ import {
 	Data3DTexture,
 	FileLoader,
 	Float32BufferAttribute,
+	Group,
 	Loader,
 	LinearFilter,
+	Matrix4,
 	Mesh,
 	MeshStandardMaterial,
 	NearestFilter,
@@ -13,21 +15,228 @@ import {
 	SRGBColorSpace
 } from 'three';
 
+// Helper function to read a STRING from the data view
+function readString( data, offset ) {
+
+	const size = data.getUint32( offset, true );
+	offset += 4;
+
+	let str = '';
+	for ( let i = 0; i < size; i ++ ) {
+
+		str += String.fromCharCode( data.getUint8( offset ++ ) );
+
+	}
+
+	return { value: str, size: 4 + size };
+
+}
+
+// Helper function to read a DICT from the data view
+function readDict( data, offset ) {
+
+	const dict = {};
+	const count = data.getUint32( offset, true );
+	offset += 4;
+
+	let totalSize = 4;
+
+	for ( let i = 0; i < count; i ++ ) {
+
+		const key = readString( data, offset );
+		offset += key.size;
+		totalSize += key.size;
+
+		const value = readString( data, offset );
+		offset += value.size;
+		totalSize += value.size;
+
+		dict[ key.value ] = value.value;
+
+	}
+
+	return { value: dict, size: totalSize };
+
+}
+
+// Helper function to decode ROTATION byte into a rotation matrix
+function decodeRotation( byte ) {
+
+	// The rotation is stored as a row-major 3x3 matrix encoded in a single byte
+	// Bits 0-1: index of the non-zero entry in the first row
+	// Bits 2-3: index of the non-zero entry in the second row
+	// Bit 4: sign of the first row entry (0 = positive, 1 = negative)
+	// Bit 5: sign of the second row entry
+	// Bit 6: sign of the third row entry
+	// The third row index is determined by the remaining column
+
+	const index1 = byte & 0x3;
+	const index2 = ( byte >> 2 ) & 0x3;
+	const sign1 = ( byte >> 4 ) & 0x1 ? - 1 : 1;
+	const sign2 = ( byte >> 5 ) & 0x1 ? - 1 : 1;
+	const sign3 = ( byte >> 6 ) & 0x1 ? - 1 : 1;
+
+	// Find the third row index (the one not used by row 0 or row 1)
+	const index3 = 3 - index1 - index2;
+
+	// Build the VOX rotation matrix (row-major 3x3)
+	// r[row][col] - each row has one non-zero entry
+	const r = [
+		[ 0, 0, 0 ],
+		[ 0, 0, 0 ],
+		[ 0, 0, 0 ]
+	];
+
+	r[ 0 ][ index1 ] = sign1;
+	r[ 1 ][ index2 ] = sign2;
+	r[ 2 ][ index3 ] = sign3;
+
+	// Convert from VOX coordinate system (Z-up) to Three.js (Y-up)
+	// VOX: X-right, Y-forward, Z-up
+	// Three.js: X-right, Y-up, Z-backward
+	// Transformation: x' = x, y' = z, z' = -y
+	//
+	// To convert rotation matrix R_vox to R_three:
+	// R_three = C * R_vox * C^-1
+	// where C converts VOX coords to Three.js coords
+
+	// Apply coordinate change: swap Y and Z, negate new Z
+	// This is equivalent to: C * R * C^-1
+	const m = new Matrix4();
+	m.set(
+		r[ 0 ][ 0 ], r[ 0 ][ 2 ], - r[ 0 ][ 1 ], 0,
+		r[ 2 ][ 0 ], r[ 2 ][ 2 ], - r[ 2 ][ 1 ], 0,
+		- r[ 1 ][ 0 ], - r[ 1 ][ 2 ], r[ 1 ][ 1 ], 0,
+		0, 0, 0, 1
+	);
+
+	return m;
+
+}
+
+// Apply VOX transform to a Three.js object
+function applyTransform( object, node ) {
+
+	if ( node.attributes._name ) {
+
+		object.name = node.attributes._name;
+
+	}
+
+	if ( node.frames.length > 0 ) {
+
+		const frame = node.frames[ 0 ];
+
+		if ( frame.rotation ) {
+
+			object.applyMatrix4( frame.rotation );
+
+		}
+
+		if ( frame.translation ) {
+
+			// VOX uses Z-up, Three.js uses Y-up
+			object.position.set(
+				frame.translation.x,
+				frame.translation.z,
+				- frame.translation.y
+			);
+
+		}
+
+	}
+
+}
+
+// Recursively build Three.js object graph from VOX nodes
+function buildObject( nodeId, nodes, chunks ) {
+
+	const node = nodes[ nodeId ];
+
+	if ( node.type === 'transform' ) {
+
+		const childNode = nodes[ node.childNodeId ];
+
+		// Check if this transform has actual transformation data
+		const frame = node.frames[ 0 ];
+		const hasTransform = frame && ( frame.rotation || frame.translation );
+
+		// Flatten: if child is a single-model shape, apply transform directly to mesh
+		if ( childNode.type === 'shape' && childNode.models.length === 1 ) {
+
+			const chunk = chunks[ childNode.models[ 0 ].modelId ];
+			const mesh = new VOXMesh( chunk );
+			applyTransform( mesh, node );
+			return mesh;
+
+		}
+
+		// If no transform, just return the child directly (avoid unnecessary group)
+		if ( ! hasTransform ) {
+
+			const child = buildObject( node.childNodeId, nodes, chunks );
+			if ( child && node.attributes._name ) child.name = node.attributes._name;
+			return child;
+
+		}
+
+		// Otherwise create a group
+		const group = new Group();
+		applyTransform( group, node );
+
+		const child = buildObject( node.childNodeId, nodes, chunks );
+		if ( child ) group.add( child );
+
+		return group;
+
+	} else if ( node.type === 'group' ) {
+
+		const group = new Group();
+
+		for ( const childId of node.childIds ) {
+
+			const child = buildObject( childId, nodes, chunks );
+			if ( child ) group.add( child );
+
+		}
+
+		return group;
+
+	} else if ( node.type === 'shape' ) {
+
+		// Shape reached directly (shouldn't happen in well-formed files, but handle it)
+		if ( node.models.length === 1 ) {
+
+			const chunk = chunks[ node.models[ 0 ].modelId ];
+			return new VOXMesh( chunk );
+
+		}
+
+		const group = new Group();
+
+		for ( const model of node.models ) {
+
+			const chunk = chunks[ model.modelId ];
+			group.add( new VOXMesh( chunk ) );
+
+		}
+
+		return group;
+
+	}
+
+	return null;
+
+}
+
 /**
  * A loader for the VOX format.
  *
  * ```js
  * const loader = new VOXLoader();
- * const chunks = await loader.loadAsync( 'models/vox/monu10.vox' );
- *
- * for ( let i = 0; i < chunks.length; i ++ ) {
+ * const result = await loader.loadAsync( 'models/vox/monu10.vox' );
  *
- * 	const chunk = chunks[ i ];
- * 	const mesh = new VOXMesh( chunk );
- * 	mesh.scale.setScalar( 0.0015 );
- * 	scene.add( mesh );
- *
- * }
+ * scene.add( result.scene.children[ 0 ] );
  * ```
  * @augments Loader
  * @three_import import { VOXLoader } from 'three/addons/loaders/VOXLoader.js';
@@ -39,7 +248,7 @@ class VOXLoader extends Loader {
 	 * to the `onLoad()` callback.
 	 *
 	 * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
-	 * @param {function(Array<Object>)} onLoad - Executed when the loading process has been finished.
+	 * @param {function(Object)} onLoad - Executed when the loading process has been finished.
 	 * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
 	 * @param {onErrorCallback} onError - Executed when errors occur.
 	 */
@@ -78,10 +287,10 @@ class VOXLoader extends Loader {
 	}
 
 	/**
-	 * Parses the given VOX data and returns the resulting chunks.
+	 * Parses the given VOX data and returns the result object.
 	 *
 	 * @param {ArrayBuffer} buffer - The raw VOX data as an array buffer.
-	 * @return {Array<Object>} The parsed chunks.
+	 * @return {Object} The parsed VOX data with properties: chunks, scene.
 	 */
 	parse( buffer ) {
 
@@ -144,6 +353,10 @@ class VOXLoader extends Loader {
 		let chunk;
 		const chunks = [];
 
+		// Extension data
+		const nodes = {};
+		let palette = DEFAULT_PALETTE;
+
 		while ( i < data.byteLength ) {
 
 			let id = '';
@@ -181,7 +394,7 @@ class VOXLoader extends Loader {
 
 			} else if ( id === 'RGBA' ) {
 
-				const palette = [ 0 ];
+				palette = [ 0 ];
 
 				for ( let j = 0; j < 256; j ++ ) {
 
@@ -191,17 +404,192 @@ class VOXLoader extends Loader {
 
 				chunk.palette = palette;
 
-			} else {
+			} else if ( id === 'nTRN' ) {
+
+				// Transform Node
+				const nodeId = data.getUint32( i, true ); i += 4;
+				const attributes = readDict( data, i );
+				i += attributes.size;
+
+				const childNodeId = data.getUint32( i, true ); i += 4;
+				i += 4; // reserved (-1)
+				const layerId = data.getInt32( i, true ); i += 4;
+				const numFrames = data.getUint32( i, true ); i += 4;
+
+				const frames = [];
+
+				for ( let f = 0; f < numFrames; f ++ ) {
+
+					const frameDict = readDict( data, i );
+					i += frameDict.size;
+
+					const frame = { rotation: null, translation: null };
+
+					if ( frameDict.value._r !== undefined ) {
+
+						frame.rotation = decodeRotation( parseInt( frameDict.value._r ) );
+
+					}
+
+					if ( frameDict.value._t !== undefined ) {
+
+						const parts = frameDict.value._t.split( ' ' ).map( Number );
+						frame.translation = { x: parts[ 0 ], y: parts[ 1 ], z: parts[ 2 ] };
+
+					}
+
+					frames.push( frame );
+
+				}
+
+				nodes[ nodeId ] = {
+					type: 'transform',
+					id: nodeId,
+					attributes: attributes.value,
+					childNodeId: childNodeId,
+					layerId: layerId,
+					frames: frames
+				};
+
+			} else if ( id === 'nGRP' ) {
+
+				// Group Node
+				const nodeId = data.getUint32( i, true ); i += 4;
+				const attributes = readDict( data, i );
+				i += attributes.size;
+
+				const numChildren = data.getUint32( i, true ); i += 4;
+				const childIds = [];
+
+				for ( let c = 0; c < numChildren; c ++ ) {
+
+					childIds.push( data.getUint32( i, true ) ); i += 4;
+
+				}
+
+				nodes[ nodeId ] = {
+					type: 'group',
+					id: nodeId,
+					attributes: attributes.value,
+					childIds: childIds
+				};
+
+			} else if ( id === 'nSHP' ) {
+
+				// Shape Node
+				const nodeId = data.getUint32( i, true ); i += 4;
+				const attributes = readDict( data, i );
+				i += attributes.size;
+
+				const numModels = data.getUint32( i, true ); i += 4;
+				const models = [];
+
+				for ( let m = 0; m < numModels; m ++ ) {
+
+					const modelId = data.getUint32( i, true ); i += 4;
+					const modelAttributes = readDict( data, i );
+					i += modelAttributes.size;
+
+					models.push( {
+						modelId: modelId,
+						attributes: modelAttributes.value
+					} );
+
+				}
 
-				// console.log( id, chunkSize, childChunks );
+				nodes[ nodeId ] = {
+					type: 'shape',
+					id: nodeId,
+					attributes: attributes.value,
+					models: models
+				};
+
+			} else {
 
+				// Skip unknown chunks
 				i += chunkSize;
 
 			}
 
 		}
 
-		return chunks;
+		// Apply palette to all chunks
+		for ( let c = 0; c < chunks.length; c ++ ) {
+
+			chunks[ c ].palette = palette;
+
+		}
+
+		// Build Three.js scene graph from nodes
+		let scene = null;
+
+		if ( Object.keys( nodes ).length > 0 ) {
+
+			scene = buildObject( 0, nodes, chunks );
+
+		}
+
+		// Build result object
+		const result = {
+			chunks: chunks,
+			scene: scene
+		};
+
+		// @deprecated, r182
+		// Proxy for backwards compatibility with array-like access
+		let warned = false;
+
+		return new Proxy( result, {
+
+			get( target, prop ) {
+
+				// Handle numeric indices
+				if ( typeof prop === 'string' && /^\d+$/.test( prop ) ) {
+
+					if ( ! warned ) {
+
+						console.warn( 'THREE.VOXLoader: Accessing result as an array is deprecated. Use result.chunks[] instead.' );
+						warned = true;
+
+					}
+
+					return target.chunks[ parseInt( prop ) ];
+
+				}
+
+				// Handle array properties/methods
+				if ( prop === 'length' ) {
+
+					if ( ! warned ) {
+
+						console.warn( 'THREE.VOXLoader: Accessing result as an array is deprecated. Use result.chunks instead.' );
+						warned = true;
+
+					}
+
+					return target.chunks.length;
+
+				}
+
+				// Handle iteration
+				if ( prop === Symbol.iterator ) {
+
+					if ( ! warned ) {
+
+						console.warn( 'THREE.VOXLoader: Iterating result as an array is deprecated. Use result.chunks instead.' );
+						warned = true;
+
+					}
+
+					return target.chunks[ Symbol.iterator ].bind( target.chunks );
+
+				}
+
+				return target[ prop ];
+
+			}
+
+		} );
 
 	}
 

+ 5 - 12
examples/webgl_loader_vox.html

@@ -54,19 +54,12 @@
 				scene.add( dirLight2 );
 
 				const loader = new VOXLoader();
-				loader.load( 'models/vox/monu10.vox', function ( chunks ) {
+				loader.load( 'models/vox/monu10.vox', function ( result ) {
 
-					for ( let i = 0; i < chunks.length; i ++ ) {
-
-						const chunk = chunks[ i ];
-
-						// displayPalette( chunk.palette );
-
-						const mesh = new VOXMesh( chunk );
-						mesh.scale.setScalar( 0.0015 );
-						scene.add( mesh );
-
-					}
+					const mesh = result.scene.children[ 0 ];
+					mesh.position.y = 0;
+					mesh.scale.setScalar( 0.0015 );
+					scene.add( mesh );
 
 				} );
 

粤ICP备19079148号