three-loaders
Version:
This is a wrapper around threejs loaders to import it from any threejs application
599 lines (552 loc) • 18.7 kB
JavaScript
// 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.
//
;
var THREE = require("three");
/**
* @param {THREE.LoadingManager} manager
*/
var 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"
};
};
DRACOLoader.prototype = {
constructor: 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
) {
var scope = this;
DRACOLoader.getDecoderModule().then(function(module) {
scope.decodeDracoFileInternal(
rawBuffer,
module.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) {
DRACOLoader.getDecoderModule().then(function(module) {
callback(module.decoder.isVersionSupported(version));
});
},
getAttributeOptions: function(attributeName) {
if (typeof this.attributeOptions[attributeName] === "undefined")
this.attributeOptions[attributeName] = {};
return this.attributeOptions[attributeName];
}
};
DRACOLoader.decoderPath = "./";
DRACOLoader.decoderConfig = {};
DRACOLoader.decoderModulePromise = null;
/**
* Sets the base path for decoder source files.
* @param {string} path
*/
DRACOLoader.setDecoderPath = function(path) {
DRACOLoader.decoderPath = path;
};
/**
* Sets decoder configuration and releases singleton decoder module. Module
* will be recreated with the next decoding call.
* @param {Object} config
*/
DRACOLoader.setDecoderConfig = function(config) {
var wasmBinary = DRACOLoader.decoderConfig.wasmBinary;
DRACOLoader.decoderConfig = config || {};
DRACOLoader.releaseDecoderModule();
// Reuse WASM binary.
if (wasmBinary) DRACOLoader.decoderConfig.wasmBinary = wasmBinary;
};
/**
* Releases the singleton DracoDecoderModule instance. Module will be recreated
* with the next decoding call.
*/
DRACOLoader.releaseDecoderModule = function() {
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}>}
*/
DRACOLoader.getDecoderModule = function() {
var scope = this;
var path = DRACOLoader.decoderPath;
var config = DRACOLoader.decoderConfig;
var promise = 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 = DRACOLoader._loadScript(path + "draco_decoder.js");
} else {
// Load with WebAssembly.
config.wasmBinaryFile = path + "draco_decoder.wasm";
promise = DRACOLoader._loadScript(path + "draco_wasm_wrapper.js")
.then(function() {
return 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);
});
});
DRACOLoader.decoderModulePromise = promise;
return promise;
};
/**
* @param {string} src
* @return {Promise}
*/
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}
*/
DRACOLoader._loadArrayBuffer = function(src) {
var loader = new THREE.FileLoader();
loader.setResponseType("arraybuffer");
return new Promise(function(resolve, reject) {
loader.load(src, resolve, undefined, reject);
});
};
module.exports = exports.default = DRACOLoader;