Skip to content

Instantly share code, notes, and snippets.

@donmccurdy
Last active September 25, 2023 13:49
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save donmccurdy/323c6363ac7ca8a7de6a3362d7fdddb4 to your computer and use it in GitHub Desktop.
Save donmccurdy/323c6363ac7ca8a7de6a3362d7fdddb4 to your computer and use it in GitHub Desktop.
THREE.GLTFLoader with Draco support in Node.js.
/**
* THREE.GLTFLoader with Draco support in Node.js.
*/
const fs = require('fs');
// All packages in 'three/examples/js' currently require access to the THREE
// variable in global scope. Work on supporting ES modules is in progress.
const THREE = global.THREE = require('three');
// GLTFLoader is more efficient with access to a TextDecoder instance, which is
// in the global namespace in the browser.
global.TextDecoder = require('util').TextDecoder;
// GLTFLoader will add itself to the THREE namespace. Work on supporting ES
// modules is in progress.
require('three/examples/js/loaders/GLTFLoader.js');
// Custom-built version of DRACOLoader, for Node.js.
const NodeDRACOLoader = require('./NodeDRACOLoader.js');
// GLTFLoader prefetches the decoder module, when Draco is needed, to speed up
// parsing later. This isn't necessary for our custom decoder, so set the
// method to a no-op.
THREE.DRACOLoader.getDecoderModule = () => {};
// Create a loader, and configure it with the custom Draco decoder.
const loader = new THREE.GLTFLoader();
loader.setDRACOLoader( new NodeDRACOLoader() );
// Read model from disk and parse it. If the model required external resources,
// we would need to import the 'node-xmlhttprequest' package and patch it into
// the global namespace. Unfortunately GLTFLoader cannot parse embedded textures
// in a Node.js context, due to reliance on browser APIs (URL.createObjectURL).
const content = fs.readFileSync( 'model.glb' );
loader.parse( trimBuffer( content ), '', ( gltf ) => {
console.log( gltf );
}, ( e ) => {
console.error( e );
} );
/**
* The Node.js filesystem API ('fs') returns a Buffer instance, which may be a
* view into a larger buffer. Because GLTFLoader parsing expects a raw
* ArrayBuffer, we make a trimmed copy of the original here.
*
* @param {Buffer} buffer
* @return {ArrayBuffer}
*/
function trimBuffer ( buffer ) {
const { byteOffset, byteLength } = buffer;
return buffer.buffer.slice( byteOffset, byteOffset + byteLength );
}
diff --git a/node_modules/three/examples/js/loaders/DRACOLoader.js b/NodeDRACOLoader.js
index 57767d7..cc46f2c 100644
--- a/node_modules/three/examples/js/loaders/DRACOLoader.js
+++ b/NodeDRACOLoader.js
@@ -14,10 +14,12 @@
//
'use strict';
+const decoder = require('draco3dgltf').createDecoderModule();
+
/**
* @param {THREE.LoadingManager} manager
*/
-THREE.DRACOLoader = function(manager) {
+module.exports = THREE.DRACOLoader = function(manager) {
this.timeLoaded = 0;
this.manager = manager || THREE.DefaultLoadingManager;
this.materials = null;
@@ -100,12 +102,8 @@ THREE.DRACOLoader.prototype = {
*/
decodeDracoFile: function(rawBuffer, callback, attributeUniqueIdMap,
attributeTypeMap) {
- var scope = this;
- THREE.DRACOLoader.getDecoderModule()
- .then( function ( module ) {
- scope.decodeDracoFileInternal( rawBuffer, module.decoder, callback,
- attributeUniqueIdMap, attributeTypeMap);
- });
+ this.decodeDracoFileInternal( rawBuffer, decoder, callback,
+ attributeUniqueIdMap, attributeTypeMap);
},
decodeDracoFileInternal: function(rawBuffer, dracoDecoder, callback,
@@ -238,7 +236,7 @@ THREE.DRACOLoader.prototype = {
}
var dracoGeometry;
var decodingStatus;
- var start_time = performance.now();
+ // var start_time = performance.now();
if (geometryType === dracoDecoder.TRIANGULAR_MESH) {
dracoGeometry = new dracoDecoder.Mesh();
decodingStatus = decoder.DecodeBufferToMesh(buffer, dracoGeometry);
@@ -256,7 +254,7 @@ THREE.DRACOLoader.prototype = {
throw new Error(errorMsg);
}
- var decode_end = performance.now();
+ // var decode_end = performance.now();
dracoDecoder.destroy(buffer);
/*
* Example on how to retrieve mesh and attributes.
@@ -381,21 +379,18 @@ THREE.DRACOLoader.prototype = {
dracoDecoder.destroy(decoder);
dracoDecoder.destroy(dracoGeometry);
- this.decode_time = decode_end - start_time;
- this.import_time = performance.now() - decode_end;
+ // this.decode_time = decode_end - start_time;
+ // this.import_time = performance.now() - decode_end;
- if (this.verbosity > 0) {
- console.log('Decode time: ' + this.decode_time);
- console.log('Import time: ' + this.import_time);
- }
+ // if (this.verbosity > 0) {
+ // console.log('Decode time: ' + this.decode_time);
+ // console.log('Import time: ' + this.import_time);
+ // }
return geometry;
},
isVersionSupported: function(version, callback) {
- THREE.DRACOLoader.getDecoderModule()
- .then( function ( module ) {
- callback( module.decoder.isVersionSupported( version ) );
- });
+ callback( decoder.isVersionSupported( version ) );
},
getAttributeOptions: function(attributeName) {
@@ -404,118 +399,3 @@ THREE.DRACOLoader.prototype = {
return this.attributeOptions[attributeName];
}
};
-
-THREE.DRACOLoader.decoderPath = './';
-THREE.DRACOLoader.decoderConfig = {};
-THREE.DRACOLoader.decoderModulePromise = null;
-
-/**
- * Sets the base path for decoder source files.
- * @param {string} path
- */
-THREE.DRACOLoader.setDecoderPath = function ( path ) {
- THREE.DRACOLoader.decoderPath = path;
-};
-
-/**
- * Sets decoder configuration and releases singleton decoder module. Module
- * will be recreated with the next decoding call.
- * @param {Object} config
- */
-THREE.DRACOLoader.setDecoderConfig = function ( config ) {
- var wasmBinary = THREE.DRACOLoader.decoderConfig.wasmBinary;
- THREE.DRACOLoader.decoderConfig = config || {};
- THREE.DRACOLoader.releaseDecoderModule();
-
- // Reuse WASM binary.
- if ( wasmBinary ) THREE.DRACOLoader.decoderConfig.wasmBinary = wasmBinary;
-};
-
-/**
- * Releases the singleton DracoDecoderModule instance. Module will be recreated
- * with the next decoding call.
- */
-THREE.DRACOLoader.releaseDecoderModule = function () {
- THREE.DRACOLoader.decoderModulePromise = null;
-};
-
-/**
- * Gets WebAssembly or asm.js singleton instance of DracoDecoderModule
- * after testing for browser support. Returns Promise that resolves when
- * module is available.
- * @return {Promise<{decoder: DracoDecoderModule}>}
- */
-THREE.DRACOLoader.getDecoderModule = function () {
- var scope = this;
- var path = THREE.DRACOLoader.decoderPath;
- var config = THREE.DRACOLoader.decoderConfig;
- var promise = THREE.DRACOLoader.decoderModulePromise;
-
- if ( promise ) return promise;
-
- // Load source files.
- if ( typeof DracoDecoderModule !== 'undefined' ) {
- // Loaded externally.
- promise = Promise.resolve();
- } else if ( typeof WebAssembly !== 'object' || config.type === 'js' ) {
- // Load with asm.js.
- promise = THREE.DRACOLoader._loadScript( path + 'draco_decoder.js' );
- } else {
- // Load with WebAssembly.
- config.wasmBinaryFile = path + 'draco_decoder.wasm';
- promise = THREE.DRACOLoader._loadScript( path + 'draco_wasm_wrapper.js' )
- .then( function () {
- return THREE.DRACOLoader._loadArrayBuffer( config.wasmBinaryFile );
- } )
- .then( function ( wasmBinary ) {
- config.wasmBinary = wasmBinary;
- } );
- }
-
- // Wait for source files, then create and return a decoder.
- promise = promise.then( function () {
- return new Promise( function ( resolve ) {
- config.onModuleLoaded = function ( decoder ) {
- scope.timeLoaded = performance.now();
- // Module is Promise-like. Wrap before resolving to avoid loop.
- resolve( { decoder: decoder } );
- };
- DracoDecoderModule( config );
- } );
- } );
-
- THREE.DRACOLoader.decoderModulePromise = promise;
- return promise;
-};
-
-/**
- * @param {string} src
- * @return {Promise}
- */
-THREE.DRACOLoader._loadScript = function ( src ) {
- var prevScript = document.getElementById( 'decoder_script' );
- if ( prevScript !== null ) {
- prevScript.parentNode.removeChild( prevScript );
- }
- var head = document.getElementsByTagName( 'head' )[ 0 ];
- var script = document.createElement( 'script' );
- script.id = 'decoder_script';
- script.type = 'text/javascript';
- script.src = src;
- return new Promise( function ( resolve ) {
- script.onload = resolve;
- head.appendChild( script );
- });
-};
-
-/**
- * @param {string} src
- * @return {Promise}
- */
-THREE.DRACOLoader._loadArrayBuffer = function ( src ) {
- var loader = new THREE.FileLoader();
- loader.setResponseType( 'arraybuffer' );
- return new Promise( function( resolve, reject ) {
- loader.load( src, resolve, undefined, reject );
- });
-};
// Copyright 2016 The Draco Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
'use strict';
const decoder = require('draco3dgltf').createDecoderModule();
/**
* @param {THREE.LoadingManager} manager
*/
module.exports = THREE.DRACOLoader = function(manager) {
this.timeLoaded = 0;
this.manager = manager || THREE.DefaultLoadingManager;
this.materials = null;
this.verbosity = 0;
this.attributeOptions = {};
this.drawMode = THREE.TrianglesDrawMode;
// Native Draco attribute type to Three.JS attribute type.
this.nativeAttributeMap = {
'position' : 'POSITION',
'normal' : 'NORMAL',
'color' : 'COLOR',
'uv' : 'TEX_COORD'
};
};
THREE.DRACOLoader.prototype = {
constructor: THREE.DRACOLoader,
load: function(url, onLoad, onProgress, onError) {
var scope = this;
var loader = new THREE.FileLoader(scope.manager);
loader.setPath(this.path);
loader.setResponseType('arraybuffer');
loader.load(url, function(blob) {
scope.decodeDracoFile(blob, onLoad);
}, onProgress, onError);
},
setPath: function(value) {
this.path = value;
return this;
},
setVerbosity: function(level) {
this.verbosity = level;
return this;
},
/**
* Sets desired mode for generated geometry indices.
* Can be either:
* THREE.TrianglesDrawMode
* THREE.TriangleStripDrawMode
*/
setDrawMode: function(drawMode) {
this.drawMode = drawMode;
return this;
},
/**
* Skips dequantization for a specific attribute.
* |attributeName| is the THREE.js name of the given attribute type.
* The only currently supported |attributeName| is 'position', more may be
* added in future.
*/
setSkipDequantization: function(attributeName, skip) {
var skipDequantization = true;
if (typeof skip !== 'undefined')
skipDequantization = skip;
this.getAttributeOptions(attributeName).skipDequantization =
skipDequantization;
return this;
},
/**
* Decompresses a Draco buffer. Names of attributes (for ID and type maps)
* must be one of the supported three.js types, including: position, color,
* normal, uv, uv2, skinIndex, skinWeight.
*
* @param {ArrayBuffer} rawBuffer
* @param {Function} callback
* @param {Object|undefined} attributeUniqueIdMap Provides a pre-defined ID
* for each attribute in the geometry to be decoded. If given,
* `attributeTypeMap` is required and `nativeAttributeMap` will be
* ignored.
* @param {Object|undefined} attributeTypeMap Provides a predefined data
* type (as a typed array constructor) for each attribute in the
* geometry to be decoded.
*/
decodeDracoFile: function(rawBuffer, callback, attributeUniqueIdMap,
attributeTypeMap) {
this.decodeDracoFileInternal( rawBuffer, decoder, callback,
attributeUniqueIdMap, attributeTypeMap);
},
decodeDracoFileInternal: function(rawBuffer, dracoDecoder, callback,
attributeUniqueIdMap, attributeTypeMap) {
/*
* Here is how to use Draco Javascript decoder and get the geometry.
*/
var buffer = new dracoDecoder.DecoderBuffer();
buffer.Init(new Int8Array(rawBuffer), rawBuffer.byteLength);
var decoder = new dracoDecoder.Decoder();
/*
* Determine what type is this file: mesh or point cloud.
*/
var geometryType = decoder.GetEncodedGeometryType(buffer);
if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
if (this.verbosity > 0) {
console.log('Loaded a mesh.');
}
} else if (geometryType == dracoDecoder.POINT_CLOUD) {
if (this.verbosity > 0) {
console.log('Loaded a point cloud.');
}
} else {
var errorMsg = 'THREE.DRACOLoader: Unknown geometry type.';
console.error(errorMsg);
throw new Error(errorMsg);
}
callback(this.convertDracoGeometryTo3JS(dracoDecoder, decoder,
geometryType, buffer, attributeUniqueIdMap, attributeTypeMap));
},
addAttributeToGeometry: function(dracoDecoder, decoder, dracoGeometry,
attributeName, attributeType, attribute,
geometry, geometryBuffer) {
if (attribute.ptr === 0) {
var errorMsg = 'THREE.DRACOLoader: No attribute ' + attributeName;
console.error(errorMsg);
throw new Error(errorMsg);
}
var numComponents = attribute.num_components();
var numPoints = dracoGeometry.num_points();
var numValues = numPoints * numComponents;
var attributeData;
var TypedBufferAttribute;
switch ( attributeType ) {
case Float32Array:
attributeData = new dracoDecoder.DracoFloat32Array();
decoder.GetAttributeFloatForAllPoints(
dracoGeometry, attribute, attributeData);
geometryBuffer[ attributeName ] = new Float32Array( numValues );
TypedBufferAttribute = THREE.Float32BufferAttribute;
break;
case Int8Array:
attributeData = new dracoDecoder.DracoInt8Array();
decoder.GetAttributeInt8ForAllPoints(
dracoGeometry, attribute, attributeData );
geometryBuffer[ attributeName ] = new Int8Array( numValues );
TypedBufferAttribute = THREE.Int8BufferAttribute;
break;
case Int16Array:
attributeData = new dracoDecoder.DracoInt16Array();
decoder.GetAttributeInt16ForAllPoints(
dracoGeometry, attribute, attributeData);
geometryBuffer[ attributeName ] = new Int16Array( numValues );
TypedBufferAttribute = THREE.Int16BufferAttribute;
break;
case Int32Array:
attributeData = new dracoDecoder.DracoInt32Array();
decoder.GetAttributeInt32ForAllPoints(
dracoGeometry, attribute, attributeData);
geometryBuffer[ attributeName ] = new Int32Array( numValues );
TypedBufferAttribute = THREE.Int32BufferAttribute;
break;
case Uint8Array:
attributeData = new dracoDecoder.DracoUInt8Array();
decoder.GetAttributeUInt8ForAllPoints(
dracoGeometry, attribute, attributeData);
geometryBuffer[ attributeName ] = new Uint8Array( numValues );
TypedBufferAttribute = THREE.Uint8BufferAttribute;
break;
case Uint16Array:
attributeData = new dracoDecoder.DracoUInt16Array();
decoder.GetAttributeUInt16ForAllPoints(
dracoGeometry, attribute, attributeData);
geometryBuffer[ attributeName ] = new Uint16Array( numValues );
TypedBufferAttribute = THREE.Uint16BufferAttribute;
break;
case Uint32Array:
attributeData = new dracoDecoder.DracoUInt32Array();
decoder.GetAttributeUInt32ForAllPoints(
dracoGeometry, attribute, attributeData);
geometryBuffer[ attributeName ] = new Uint32Array( numValues );
TypedBufferAttribute = THREE.Uint32BufferAttribute;
break;
default:
var errorMsg = 'THREE.DRACOLoader: Unexpected attribute type.';
console.error( errorMsg );
throw new Error( errorMsg );
}
// Copy data from decoder.
for (var i = 0; i < numValues; i++) {
geometryBuffer[attributeName][i] = attributeData.GetValue(i);
}
// Add attribute to THREEJS geometry for rendering.
geometry.addAttribute(attributeName,
new TypedBufferAttribute(geometryBuffer[attributeName],
numComponents));
dracoDecoder.destroy(attributeData);
},
convertDracoGeometryTo3JS: function(dracoDecoder, decoder, geometryType,
buffer, attributeUniqueIdMap,
attributeTypeMap) {
// TODO: Should not assume native Draco attribute IDs apply.
if (this.getAttributeOptions('position').skipDequantization === true) {
decoder.SkipAttributeTransform(dracoDecoder.POSITION);
}
var dracoGeometry;
var decodingStatus;
// var start_time = performance.now();
if (geometryType === dracoDecoder.TRIANGULAR_MESH) {
dracoGeometry = new dracoDecoder.Mesh();
decodingStatus = decoder.DecodeBufferToMesh(buffer, dracoGeometry);
} else {
dracoGeometry = new dracoDecoder.PointCloud();
decodingStatus =
decoder.DecodeBufferToPointCloud(buffer, dracoGeometry);
}
if (!decodingStatus.ok() || dracoGeometry.ptr == 0) {
var errorMsg = 'THREE.DRACOLoader: Decoding failed: ';
errorMsg += decodingStatus.error_msg();
console.error(errorMsg);
dracoDecoder.destroy(decoder);
dracoDecoder.destroy(dracoGeometry);
throw new Error(errorMsg);
}
// var decode_end = performance.now();
dracoDecoder.destroy(buffer);
/*
* Example on how to retrieve mesh and attributes.
*/
var numFaces;
if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
numFaces = dracoGeometry.num_faces();
if (this.verbosity > 0) {
console.log('Number of faces loaded: ' + numFaces.toString());
}
} else {
numFaces = 0;
}
var numPoints = dracoGeometry.num_points();
var numAttributes = dracoGeometry.num_attributes();
if (this.verbosity > 0) {
console.log('Number of points loaded: ' + numPoints.toString());
console.log('Number of attributes loaded: ' +
numAttributes.toString());
}
// Verify if there is position attribute.
// TODO: Should not assume native Draco attribute IDs apply.
var posAttId = decoder.GetAttributeId(dracoGeometry,
dracoDecoder.POSITION);
if (posAttId == -1) {
var errorMsg = 'THREE.DRACOLoader: No position attribute found.';
console.error(errorMsg);
dracoDecoder.destroy(decoder);
dracoDecoder.destroy(dracoGeometry);
throw new Error(errorMsg);
}
var posAttribute = decoder.GetAttribute(dracoGeometry, posAttId);
// Structure for converting to THREEJS geometry later.
var geometryBuffer = {};
// Import data to Three JS geometry.
var geometry = new THREE.BufferGeometry();
// Do not use both the native attribute map and a provided (e.g. glTF) map.
if ( attributeUniqueIdMap ) {
// Add attributes of user specified unique id. E.g. GLTF models.
for (var attributeName in attributeUniqueIdMap) {
var attributeType = attributeTypeMap[attributeName];
var attributeId = attributeUniqueIdMap[attributeName];
var attribute = decoder.GetAttributeByUniqueId(dracoGeometry,
attributeId);
this.addAttributeToGeometry(dracoDecoder, decoder, dracoGeometry,
attributeName, attributeType, attribute, geometry, geometryBuffer);
}
} else {
// Add native Draco attribute type to geometry.
for (var attributeName in this.nativeAttributeMap) {
var attId = decoder.GetAttributeId(dracoGeometry,
dracoDecoder[this.nativeAttributeMap[attributeName]]);
if (attId !== -1) {
if (this.verbosity > 0) {
console.log('Loaded ' + attributeName + ' attribute.');
}
var attribute = decoder.GetAttribute(dracoGeometry, attId);
this.addAttributeToGeometry(dracoDecoder, decoder, dracoGeometry,
attributeName, Float32Array, attribute, geometry, geometryBuffer);
}
}
}
// For mesh, we need to generate the faces.
if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
if (this.drawMode === THREE.TriangleStripDrawMode) {
var stripsArray = new dracoDecoder.DracoInt32Array();
var numStrips = decoder.GetTriangleStripsFromMesh(
dracoGeometry, stripsArray);
geometryBuffer.indices = new Uint32Array(stripsArray.size());
for (var i = 0; i < stripsArray.size(); ++i) {
geometryBuffer.indices[i] = stripsArray.GetValue(i);
}
dracoDecoder.destroy(stripsArray);
} else {
var numIndices = numFaces * 3;
geometryBuffer.indices = new Uint32Array(numIndices);
var ia = new dracoDecoder.DracoInt32Array();
for (var i = 0; i < numFaces; ++i) {
decoder.GetFaceFromMesh(dracoGeometry, i, ia);
var index = i * 3;
geometryBuffer.indices[index] = ia.GetValue(0);
geometryBuffer.indices[index + 1] = ia.GetValue(1);
geometryBuffer.indices[index + 2] = ia.GetValue(2);
}
dracoDecoder.destroy(ia);
}
}
geometry.drawMode = this.drawMode;
if (geometryType == dracoDecoder.TRIANGULAR_MESH) {
geometry.setIndex(new(geometryBuffer.indices.length > 65535 ?
THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute)
(geometryBuffer.indices, 1));
}
// TODO: Should not assume native Draco attribute IDs apply.
// TODO: Can other attribute types be quantized?
var posTransform = new dracoDecoder.AttributeQuantizationTransform();
if (posTransform.InitFromAttribute(posAttribute)) {
// Quantized attribute. Store the quantization parameters into the
// THREE.js attribute.
geometry.attributes['position'].isQuantized = true;
geometry.attributes['position'].maxRange = posTransform.range();
geometry.attributes['position'].numQuantizationBits =
posTransform.quantization_bits();
geometry.attributes['position'].minValues = new Float32Array(3);
for (var i = 0; i < 3; ++i) {
geometry.attributes['position'].minValues[i] =
posTransform.min_value(i);
}
}
dracoDecoder.destroy(posTransform);
dracoDecoder.destroy(decoder);
dracoDecoder.destroy(dracoGeometry);
// this.decode_time = decode_end - start_time;
// this.import_time = performance.now() - decode_end;
// if (this.verbosity > 0) {
// console.log('Decode time: ' + this.decode_time);
// console.log('Import time: ' + this.import_time);
// }
return geometry;
},
isVersionSupported: function(version, callback) {
callback( decoder.isVersionSupported( version ) );
},
getAttributeOptions: function(attributeName) {
if (typeof this.attributeOptions[attributeName] === 'undefined')
this.attributeOptions[attributeName] = {};
return this.attributeOptions[attributeName];
}
};
@makc
Copy link

makc commented Mar 5, 2023

If anyone is curious what 3js revision a DRACOLoader.js file 57767d7 is from, try running this command: git log --all -m --find-object 57767d7. From its output it seems like this file version was introduced in mrdoob/three.js#15084 (October 19, 2018)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment