UNPKG

gltf-pipeline

Version:

Content pipeline tools for optimizing glTF assets.

482 lines (436 loc) 17.4 kB
"use strict"; const Cesium = require("cesium"); const draco3d = require("draco3d"); const hashObject = require("object-hash"); const Promise = require("bluebird"); 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 Cartesian3 = Cesium.Cartesian3; const Check = Cesium.Check; const clone = Cesium.clone; const ComponentDatatype = Cesium.ComponentDatatype; const defined = Cesium.defined; const RuntimeError = Cesium.RuntimeError; const WebGLConstants = Cesium.WebGLConstants; let encoderModulePromise; let decoderModulePromise; 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=11] 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=8] 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=10] 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=8] 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 {Promise} A promise that resolves to the glTF asset with compressed meshes. * * @private */ function compressDracoMeshes(gltf, options) { if (!defined(encoderModulePromise)) { // Prepare encoder for compressing meshes. encoderModulePromise = Promise.resolve(draco3d.createEncoderModule({})); decoderModulePromise = Promise.resolve(draco3d.createDecoderModule({})); } return encoderModulePromise.then(function (encoderModule) { return decoderModulePromise.then(function (decoderModule) { return compress(gltf, options, encoderModule, decoderModule); }); }); } function compress(gltf, options, encoderModule, decoderModule) { options = options ?? {}; const dracoOptions = options.dracoOptions ?? {}; const defaults = compressDracoMeshes.defaults; const compressionLevel = dracoOptions.compressionLevel ?? defaults.compressionLevel; const uncompressedFallback = dracoOptions.uncompressedFallback ?? defaults.uncompressedFallback; const unifiedQuantization = dracoOptions.unifiedQuantization ?? defaults.unifiedQuantization; const quantizationVolume = dracoOptions.quantizationVolume; const explicitQuantization = unifiedQuantization || defined(quantizationVolume); const quantizationBitsValues = getQuantizationBits(dracoOptions); checkRange("compressionLevel", compressionLevel, 0, 10); for (const attributeName in quantizationBitsValues) { if ( Object.prototype.hasOwnProperty.call( quantizationBitsValues, attributeName, ) ) { checkRange( `quantizationBitsValues[${attributeName}]`, quantizationBitsValues[attributeName], 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 = new Array(3).fill(Number.POSITIVE_INFINITY); const max = new Array(3).fill(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; } if (quantizationBitsValues[attributeName] > 0) { if (attributeName === "POSITION" && explicitQuantization) { encoder.SetAttributeExplicitQuantization( encoderModule.POSITION, quantizationBitsValues[attributeName], 3, positionOrigin, positionRange, ); } else { encoder.SetAttributeQuantization( attributeEnum, quantizationBitsValues[attributeName], ); } } }, ); const encodedDracoDataArray = new encoderModule.DracoInt8Array(); encoder.SetSpeedOptions(10 - compressionLevel, 10 - compressionLevel); // Compression level is 10 - speed. 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, quantizationBitsValues, decoderModule, ); 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, ["accessor", "bufferView", "buffer"]); 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, quantizationBitsValues, decoderModule, ) { 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 = primitive.extensions ?? {}; primitive.extensions.KHR_draco_mesh_compression = { bufferView: bufferViewId, attributes: attributeToId, }; gltf = replaceWithDecompressedPrimitive( gltf, primitive, dracoEncodedBuffer, uncompressedFallback, quantizationBitsValues, decoderModule, ); } 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 = 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); } function getQuantizationBits(dracoOptions) { const defaults = compressDracoMeshes.defaults; return { POSITION: dracoOptions.quantizePositionBits ?? defaults.quantizePositionBits, NORMAL: dracoOptions.quantizeNormalBits ?? defaults.quantizeNormalBits, TEXCOORD: dracoOptions.quantizeTexcoordBits ?? defaults.quantizeTexcoordBits, COLOR: dracoOptions.quantizeColorBits ?? defaults.quantizeColorBits, GENERIC: dracoOptions.quantizeGenericBits ?? defaults.quantizeGenericBits, }; } compressDracoMeshes.defaults = { compressionLevel: 7, quantizePositionBits: 11, quantizeNormalBits: 8, quantizeTexcoordBits: 10, quantizeColorBits: 8, quantizeSkinBits: 8, quantizeGenericBits: 8, uncompressedFallback: false, unifiedQuantization: false, };