gltf-pipeline
Version:
Content pipeline tools for optimizing glTF assets.
369 lines (327 loc) • 17.6 kB
JavaScript
'use strict';
const Cesium = require('cesium');
const draco3d = require('draco3d');
const hashObject = require('object-hash');
const addBuffer = require('./addBuffer');
const addExtensionsRequired = require('./addExtensionsRequired');
const addExtensionsUsed = require('./addExtensionsUsed');
const addToArray = require('./addToArray');
const ForEach = require('./ForEach');
const numberOfComponentsForType = require('./numberOfComponentsForType');
const readAccessorPacked = require('./readAccessorPacked');
const removeUnusedElements = require('./removeUnusedElements');
const replaceWithDecompressedPrimitive = require('./replaceWithDecompressedPrimitive');
const splitPrimitives = require('./splitPrimitives');
const arrayFill = Cesium.arrayFill;
const Cartesian3 = Cesium.Cartesian3;
const Check = Cesium.Check;
const clone = Cesium.clone;
const ComponentDatatype = Cesium.ComponentDatatype;
const defaultValue = Cesium.defaultValue;
const defined = Cesium.defined;
const RuntimeError = Cesium.RuntimeError;
const WebGLConstants = Cesium.WebGLConstants;
// Prepare encoder for compressing meshes.
const encoderModule = draco3d.createEncoderModule({});
module.exports = compressDracoMeshes;
/**
* Compresses meshes using Draco compression in the glTF model.
*
* @param {Object} gltf A javascript object containing a glTF asset.
* @param {Object} options The same options object as {@link processGltf}
* @param {Object} options.dracoOptions Options defining Draco compression settings.
* @param {Number} [options.dracoOptions.compressionLevel=7] A value between 0 and 10 specifying the quality of the Draco compression. Higher values produce better quality compression but may take longer to decompress. A value of 0 will apply sequential encoding and preserve face order.
* @param {Number} [options.dracoOptions.quantizePositionBits=14] A value between 0 and 30 specifying the number of bits used for positions. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization.
* @param {Number} [options.dracoOptions.quantizeNormalBits=10] A value between 0 and 30 specifying the number of bits used for normals. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization.
* @param {Number} [options.dracoOptions.quantizeTexcoordBits=12] A value between 0 and 30 specifying the number of bits used for texture coordinates. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization.
* @param {Number} [options.dracoOptions.quantizeColorBits=8] A value between 0 and 30 specifying the number of bits used for color attributes. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization.
* @param {Number} [options.dracoOptions.quantizeGenericBits=12] A value between 0 and 30 specifying the number of bits used for skinning attributes (joint indices and joint weights) and custom attributes. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization.
* @param {Boolean} [options.dracoOptions.uncompressedFallback=false] If set, add uncompressed fallback versions of the compressed meshes.
* @param {Boolean} [options.dracoOptions.unifiedQuantization=false] Quantize positions, defined by the unified bounding box of all primitives. If not set, quantization is applied separately.
* @param {Object} [options.dracoOptions.quantizationVolume] An AxisAlignedBoundingBox defining the explicit quantization volume.
* @returns {Object} The glTF asset with compressed meshes.
*
* @private
*/
function compressDracoMeshes(gltf, options) {
options = defaultValue(options, {});
const dracoOptions = defaultValue(options.dracoOptions, {});
const defaults = compressDracoMeshes.defaults;
const compressionLevel = defaultValue(dracoOptions.compressionLevel, defaults.compressionLevel);
const quantizePositionBits = defaultValue(dracoOptions.quantizePositionBits, defaults.quantizePositionBits);
const quantizeNormalBits = defaultValue(dracoOptions.quantizeNormalBits, defaults.quantizeNormalBits);
const quantizeTexcoordBits = defaultValue(dracoOptions.quantizeTexcoordBits, defaults.quantizeTexcoordBits);
const quantizeColorBits = defaultValue(dracoOptions.quantizeColorBits, defaults.quantizeColorBits);
const quantizeGenericBits = defaultValue(dracoOptions.quantizeGenericBits, defaults.quantizeGenericBits);
const uncompressedFallback = defaultValue(dracoOptions.uncompressedFallback, defaults.uncompressedFallback);
const unifiedQuantization = defaultValue(dracoOptions.unifiedQuantization, defaults.unifiedQuantization);
const quantizationVolume = dracoOptions.quantizationVolume;
const explicitQuantization = unifiedQuantization || defined(quantizationVolume);
checkRange('compressionLevel', compressionLevel, 0, 10);
checkRange('quantizePositionBits', quantizePositionBits, 0, 30);
checkRange('quantizeNormalBits', quantizeNormalBits, 0, 30);
checkRange('quantizeTexcoordBits', quantizeTexcoordBits, 0, 30);
checkRange('quantizeColorBits', quantizeColorBits, 0, 30);
checkRange('quantizeGenericBits', quantizeGenericBits, 0, 30);
splitPrimitives(gltf);
const hashPrimitives = {};
let positionOrigin;
let positionRange;
if (defined(quantizationVolume)) {
positionOrigin = Cartesian3.pack(quantizationVolume.minimum, new Array(3));
positionRange = Cartesian3.maximumComponent(Cartesian3.subtract(quantizationVolume.maximum, quantizationVolume.minimum, new Cartesian3()));
} else if (unifiedQuantization) {
// Collect bounding box from all primitives. Currently works only for vec3 positions (XYZ).
const accessors = gltf.accessors;
const min = arrayFill(new Array(3), Number.POSITIVE_INFINITY);
const max = arrayFill(new Array(3), Number.NEGATIVE_INFINITY);
ForEach.accessorWithSemantic(gltf, 'POSITION', function(accessorId) {
const accessor = accessors[accessorId];
if (accessor.type !== 'VEC3') {
throw new RuntimeError('Could not perform unified quantization. Input contains position accessor with an unsupported number of components.');
}
const accessorMin = accessor.min;
const accessorMax = accessor.max;
for (let j = 0; j < 3; ++j) {
min[j] = Math.min(min[j], accessorMin[j]);
max[j] = Math.max(max[j], accessorMax[j]);
}
});
positionOrigin = min;
positionRange = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
}
let addedExtension = false;
ForEach.mesh(gltf, function(mesh) {
ForEach.meshPrimitive(mesh, function(primitive) {
if (defined(primitive.mode) && primitive.mode !== WebGLConstants.TRIANGLES) {
return;
}
if (!defined(primitive.indices)) {
addIndices(gltf, primitive);
}
addedExtension = true;
const primitiveGeometry = {
attributes: primitive.attributes,
indices: primitive.indices,
mode: primitive.mode
};
const hashValue = hashObject(primitiveGeometry);
if (defined(hashPrimitives[hashValue])) {
// Copy compressed primitive.
copyCompressedExtensionToPrimitive(primitive, hashPrimitives[hashValue]);
return;
}
hashPrimitives[hashValue] = primitive;
const encoder = new encoderModule.Encoder();
const meshBuilder = new encoderModule.MeshBuilder();
const mesh = new encoderModule.Mesh();
// First get the faces and add to geometry.
const indicesData = readAccessorPacked(gltf, gltf.accessors[primitive.indices]);
const indices = new Uint32Array(indicesData);
const numberOfFaces = indices.length / 3;
meshBuilder.AddFacesToMesh(mesh, numberOfFaces, indices);
// Add attributes to mesh.
const attributeToId = {};
ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) {
const accessor = gltf.accessors[accessorId];
const componentType = accessor.componentType;
const numberOfPoints = accessor.count;
const numberOfComponents = numberOfComponentsForType(accessor.type);
const packed = readAccessorPacked(gltf, accessor);
const addAttributeFunctionName = getAddAttributeFunctionName(componentType);
const data = ComponentDatatype.createTypedArray(componentType, packed);
let attributeName = semantic;
if (semantic.indexOf('_') > 0) { // Skip user-defined semantics prefixed with underscore
attributeName = attributeName.substring(0, semantic.indexOf('_'));
}
let attributeEnum;
if (attributeName === 'POSITION' || attributeName === 'NORMAL' || attributeName === 'COLOR') {
attributeEnum = encoderModule[attributeName];
} else if (attributeName === 'TEXCOORD') {
attributeEnum = encoderModule.TEX_COORD;
} else {
attributeEnum = encoderModule.GENERIC;
}
const attributeId = meshBuilder[addAttributeFunctionName](mesh, attributeEnum, numberOfPoints, numberOfComponents, data);
if (attributeId === -1) {
throw new RuntimeError('Error: Failed adding attribute ' + semantic);
} else {
attributeToId[semantic] = attributeId;
}
});
const encodedDracoDataArray = new encoderModule.DracoInt8Array();
encoder.SetSpeedOptions(10 - compressionLevel, 10 - compressionLevel); // Compression level is 10 - speed.
if (quantizePositionBits > 0) {
if (explicitQuantization) {
encoder.SetAttributeExplicitQuantization(encoderModule.POSITION, quantizePositionBits, 3, positionOrigin, positionRange);
} else {
encoder.SetAttributeQuantization(encoderModule.POSITION, quantizePositionBits);
}
}
if (quantizeNormalBits > 0) {
encoder.SetAttributeQuantization(encoderModule.NORMAL, quantizeNormalBits);
}
if (quantizeTexcoordBits > 0) {
encoder.SetAttributeQuantization(encoderModule.TEX_COORD, quantizeTexcoordBits);
}
if (quantizeColorBits > 0) {
encoder.SetAttributeQuantization(encoderModule.COLOR, quantizeColorBits);
}
if (quantizeGenericBits > 0) {
encoder.SetAttributeQuantization(encoderModule.GENERIC, quantizeGenericBits);
}
if (defined(primitive.targets)) {
// Set sequential encoding to preserve order of vertices.
encoder.SetEncodingMethod(encoderModule.MESH_SEQUENTIAL_ENCODING);
}
encoder.SetTrackEncodedProperties(true);
const encodedLength = encoder.EncodeMeshToDracoBuffer(mesh, encodedDracoDataArray);
if (encodedLength <= 0) {
throw new RuntimeError('Error: Draco encoding failed.');
}
const encodedData = Buffer.alloc(encodedLength);
for (let i = 0; i < encodedLength; ++i) {
encodedData[i] = encodedDracoDataArray.GetValue(i);
}
const dracoEncodedBuffer = {
buffer: encodedData,
numberOfPoints: encoder.GetNumberOfEncodedPoints(),
numberOfFaces: encoder.GetNumberOfEncodedFaces()
};
addCompressionExtensionToPrimitive(gltf, primitive, attributeToId, dracoEncodedBuffer, uncompressedFallback);
encoderModule.destroy(encodedDracoDataArray);
encoderModule.destroy(mesh);
encoderModule.destroy(meshBuilder);
encoderModule.destroy(encoder);
});
});
if (addedExtension) {
if (uncompressedFallback) {
addExtensionsUsed(gltf, 'KHR_draco_mesh_compression');
} else {
addExtensionsRequired(gltf, 'KHR_draco_mesh_compression');
}
removeUnusedElements(gltf);
if (uncompressedFallback) {
assignMergedBufferNames(gltf);
}
}
return gltf;
}
function addIndices(gltf, primitive) {
// Reserve the 65535 index for primitive restart
const length = gltf.accessors[primitive.attributes.POSITION].count;
const componentType = length < 65535 ? WebGLConstants.UNSIGNED_SHORT : WebGLConstants.UNSIGNED_INT;
const array = ComponentDatatype.createTypedArray(componentType, length);
for (let i = 0; i < length; ++i) {
array[i] = i;
}
const buffer = Buffer.from(array.buffer);
const bufferView = addBuffer(gltf, buffer);
const accessor = {
bufferView: bufferView,
byteOffset: 0,
componentType: componentType,
count: length,
type: 'SCALAR',
min: [0],
max: [length - 1]
};
primitive.indices = addToArray(gltf.accessors, accessor);
}
function addCompressionExtensionToPrimitive(gltf, primitive, attributeToId, dracoEncodedBuffer, uncompressedFallback) {
if (!uncompressedFallback) {
// Remove properties from accessors.
// Remove indices bufferView.
const indicesAccessor = clone(gltf.accessors[primitive.indices]);
delete indicesAccessor.bufferView;
delete indicesAccessor.byteOffset;
primitive.indices = addToArray(gltf.accessors, indicesAccessor);
// Remove attributes bufferViews.
ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) {
const attributeAccessor = clone(gltf.accessors[accessorId]);
delete attributeAccessor.bufferView;
delete attributeAccessor.byteOffset;
primitive.attributes[semantic] = addToArray(gltf.accessors, attributeAccessor);
});
}
const bufferViewId = addBuffer(gltf, dracoEncodedBuffer.buffer);
primitive.extensions = defaultValue(primitive.extensions, {});
primitive.extensions.KHR_draco_mesh_compression = {
bufferView: bufferViewId,
attributes: attributeToId
};
gltf = replaceWithDecompressedPrimitive(gltf, primitive, dracoEncodedBuffer, uncompressedFallback);
}
function copyCompressedExtensionToPrimitive(primitive, compressedPrimitive) {
ForEach.meshPrimitiveAttribute(compressedPrimitive, function(accessorId, semantic) {
primitive.attributes[semantic] = accessorId;
});
primitive.indices = compressedPrimitive.indices;
const dracoExtension = compressedPrimitive.extensions.KHR_draco_mesh_compression;
primitive.extensions = defaultValue(primitive.extensions, {});
primitive.extensions.KHR_draco_mesh_compression = {
bufferView: dracoExtension.bufferView,
attributes: dracoExtension.attributes
};
}
function assignBufferViewName(gltf, bufferViewId, name) {
const bufferView = gltf.bufferViews[bufferViewId];
const buffer = gltf.buffers[bufferView.buffer];
buffer.extras._pipeline.mergedBufferName = name;
}
function assignAccessorName(gltf, accessorId, name) {
const bufferViewId = gltf.accessors[accessorId].bufferView;
if (defined(bufferViewId)) {
assignBufferViewName(gltf, bufferViewId, name);
}
}
function assignMergedBufferNames(gltf) {
ForEach.accessorContainingVertexAttributeData(gltf, function(accessorId) {
assignAccessorName(gltf, accessorId, 'uncompressed');
});
ForEach.accessorContainingIndexData(gltf, function(accessorId) {
assignAccessorName(gltf, accessorId, 'uncompressed');
});
ForEach.mesh(gltf, function(mesh) {
ForEach.meshPrimitive(mesh, function(primitive) {
if (defined(primitive.extensions) &&
defined(primitive.extensions.KHR_draco_mesh_compression)) {
assignBufferViewName(gltf, primitive.extensions.KHR_draco_mesh_compression.bufferView, 'draco');
}
});
});
}
function getAddAttributeFunctionName(componentType) {
switch (componentType) {
case WebGLConstants.UNSIGNED_BYTE:
return 'AddUInt8Attribute';
case WebGLConstants.BYTE:
return 'AddInt8Attribute';
case WebGLConstants.UNSIGNED_SHORT:
return 'AddUInt16Attribute';
case WebGLConstants.SHORT:
return 'AddInt16Attribute';
case WebGLConstants.UNSIGNED_INT:
return 'AddUInt32Attribute';
case WebGLConstants.INT:
return 'AddInt32Attribute';
case WebGLConstants.FLOAT:
return 'AddFloatAttribute';
}
}
function checkRange(name, value, minimum, maximum) {
Check.typeOf.number.greaterThanOrEquals(name, value, minimum);
Check.typeOf.number.lessThanOrEquals(name, value, maximum);
}
compressDracoMeshes.defaults = {
compressionLevel: 7,
quantizePositionBits: 14,
quantizeNormalBits: 10,
quantizeTexcoordBits: 12,
quantizeColorBits: 8,
quantizeSkinBits: 12,
quantizeGenericBits: 12,
uncompressedFallback: false,
unifiedQuantization: false
};