"use strict"; const Cesium = require("cesium"); const mime = require("mime"); const PNG = require("pngjs").PNG; const Promise = require("bluebird"); const getBufferPadded = require("./getBufferPadded"); const gltfToGlb = require("./gltfToGlb"); const defined = Cesium.defined; const RuntimeError = Cesium.RuntimeError; module.exports = writeGltf; /** * Write glTF resources as embedded data uris or external files. * * @param {Object} gltf The glTF asset. * @param {Object} options The options object passed along from lib/obj2gltf.js * @returns {Promise} A promise that resolves to the glTF JSON or glb buffer. * * @private */ function writeGltf(gltf, options) { return encodeTextures(gltf).then(function () { const binary = options.binary; const separate = options.separate; const separateTextures = options.separateTextures; const promises = []; if (separateTextures) { promises.push(writeSeparateTextures(gltf, options)); } else { writeEmbeddedTextures(gltf); } if (separate) { promises.push(writeSeparateBuffers(gltf, options)); } else if (!binary) { writeEmbeddedBuffer(gltf); } const binaryBuffer = gltf.buffers[0].extras._obj2gltf.source; return Promise.all(promises).then(function () { deleteExtras(gltf); removeEmpty(gltf); if (binary) { return gltfToGlb(gltf, binaryBuffer); } return gltf; }); }); } function encodePng(texture) { // Constants defined by pngjs const rgbColorType = 2; const rgbaColorType = 6; const png = new PNG({ width: texture.width, height: texture.height, colorType: texture.transparent ? rgbaColorType : rgbColorType, inputColorType: rgbaColorType, inputHasAlpha: true, }); png.data = texture.pixels; return new Promise(function (resolve, reject) { const chunks = []; const stream = png.pack(); stream.on("data", function (chunk) { chunks.push(chunk); }); stream.on("end", function () { resolve(Buffer.concat(chunks)); }); stream.on("error", reject); }); } function encodeTexture(texture) { if ( !defined(texture.source) && defined(texture.pixels) && texture.extension === ".png" ) { return encodePng(texture).then(function (encoded) { texture.source = encoded; }); } } function encodeTextures(gltf) { // Dynamically generated PBR textures need to be encoded to png prior to being saved const encodePromises = []; const images = gltf.images; const length = images.length; for (let i = 0; i < length; ++i) { encodePromises.push(encodeTexture(images[i].extras._obj2gltf)); } return Promise.all(encodePromises); } function deleteExtras(gltf) { const buffers = gltf.buffers; const buffersLength = buffers.length; for (let i = 0; i < buffersLength; ++i) { delete buffers[i].extras; } const images = gltf.images; const imagesLength = images.length; for (let i = 0; i < imagesLength; ++i) { delete images[i].extras; } } function removeEmpty(json) { Object.keys(json).forEach(function (key) { if ( !defined(json[key]) || (Array.isArray(json[key]) && json[key].length === 0) ) { delete json[key]; // Delete values that are undefined or [] } else if (typeof json[key] === "object") { removeEmpty(json[key]); } }); } function writeSeparateBuffers(gltf, options) { const buffers = gltf.buffers; return Promise.map( buffers, function (buffer) { const source = buffer.extras._obj2gltf.source; const bufferUri = buffer.name + ".bin"; buffer.uri = bufferUri; return options.writer(bufferUri, source); }, { concurrency: 10 } ); } function writeSeparateTextures(gltf, options) { const images = gltf.images; return Promise.map( images, function (image) { const texture = image.extras._obj2gltf; const imageUri = image.name + texture.extension; image.uri = imageUri; return options.writer(imageUri, texture.source); }, { concurrency: 10 } ); } function writeEmbeddedBuffer(gltf) { const buffer = gltf.buffers[0]; const source = buffer.extras._obj2gltf.source; // Buffers larger than ~192MB cannot be base64 encoded due to a NodeJS limitation. Source: https://github.com/nodejs/node/issues/4266 if (source.length > 201326580) { throw new RuntimeError( "Buffer is too large to embed in the glTF. Use the --separate flag instead." ); } buffer.uri = "data:application/octet-stream;base64," + source.toString("base64"); } function writeEmbeddedTextures(gltf) { const buffer = gltf.buffers[0]; const bufferExtras = buffer.extras._obj2gltf; const bufferSource = bufferExtras.source; const images = gltf.images; const imagesLength = images.length; const sources = [bufferSource]; let byteOffset = bufferSource.length; for (let i = 0; i < imagesLength; ++i) { const image = images[i]; const texture = image.extras._obj2gltf; const textureSource = texture.source; const textureByteLength = textureSource.length; image.mimeType = mime.getType(texture.extension); image.bufferView = gltf.bufferViews.length; gltf.bufferViews.push({ buffer: 0, byteOffset: byteOffset, byteLength: textureByteLength, }); byteOffset += textureByteLength; sources.push(textureSource); } const source = getBufferPadded(Buffer.concat(sources)); bufferExtras.source = source; buffer.byteLength = source.length; }