gltf-pipeline
Version:
Content pipeline tools for optimizing glTF assets.
189 lines (167 loc) • 6.27 kB
JavaScript
;
const Cesium = require("cesium");
const fsExtra = require("fs-extra");
const path = require("path");
const Promise = require("bluebird");
const { URL } = require("url");
const addPipelineExtras = require("./addPipelineExtras");
const dataUriToBuffer = require("./dataUriToBuffer");
const { fileURLToPath, pathToFileURL } = require("url");
const ForEach = require("./ForEach");
const defined = Cesium.defined;
const isDataUri = Cesium.isDataUri;
const RuntimeError = Cesium.RuntimeError;
module.exports = readResources;
/**
* Read data uris, buffer views, or files referenced by the glTF into buffers.
* The buffer data is placed into extras._pipeline.source for the corresponding object.
* This stage runs before updateVersion and handles both glTF 1.0 and glTF 2.0 assets.
*
* @param {object} gltf A javascript object containing a glTF asset.
* @param {object} [options] Object with the following properties:
* @param {string} [options.resourceDirectory] The path to look in when reading separate files.
* @returns {Promise} A promise that resolves to the glTF asset when all resources are read.
*
* @private
*/
function readResources(gltf, options) {
addPipelineExtras(gltf);
options = options ?? {};
// Make sure its an absolute path with a trailing separator
options.resourceDirectory = defined(options.resourceDirectory)
? path.resolve(options.resourceDirectory) + path.sep
: undefined;
const bufferPromises = [];
const resourcePromises = [];
ForEach.buffer(gltf, function (buffer) {
bufferPromises.push(readBuffer(gltf, buffer, options));
});
// Buffers need to be read first because images and shader may resolve to bufferViews
return Promise.all(bufferPromises)
.then(function () {
ForEach.shader(gltf, function (shader) {
resourcePromises.push(readShader(gltf, shader, options));
});
ForEach.image(gltf, function (image) {
resourcePromises.push(readImage(gltf, image, options));
});
return Promise.all(resourcePromises);
})
.then(function () {
return gltf;
});
}
function readBuffer(gltf, buffer, options) {
return readResource(gltf, buffer, false, options).then(function (data) {
if (defined(data)) {
buffer.extras._pipeline.source = data;
}
});
}
function readImage(gltf, image, options) {
return readResource(gltf, image, true, options).then(function (data) {
image.extras._pipeline.source = data;
});
}
function readShader(gltf, shader, options) {
return readResource(gltf, shader, true, options).then(function (data) {
shader.extras._pipeline.source = data.toString();
});
}
function readResource(gltf, object, saveResourceId, options) {
const uri = object.uri;
delete object.uri; // Don't hold onto the uri, its contents will be stored in extras._pipeline.source
// Source already exists if the gltf was converted from a glb
const source = object.extras._pipeline.source;
if (defined(source)) {
return Promise.resolve(Buffer.from(source));
}
// Handle reading buffer view from 1.0 glb model
const extensions = object.extensions;
if (defined(extensions)) {
const khrBinaryGltf = extensions.KHR_binary_glTF;
if (defined(khrBinaryGltf)) {
return Promise.resolve(
readBufferView(gltf, khrBinaryGltf.bufferView, object, saveResourceId),
);
}
}
if (defined(object.bufferView)) {
return Promise.resolve(
readBufferView(gltf, object.bufferView, object, saveResourceId),
);
}
if (!defined(uri)) {
return Promise.resolve(undefined);
}
if (isDataUri(uri)) {
return Promise.resolve(dataUriToBuffer(uri));
}
return readFile(object, uri, saveResourceId, options);
}
function readBufferView(gltf, bufferViewId, object, saveResourceId) {
if (saveResourceId) {
object.extras._pipeline.resourceId = bufferViewId;
}
const bufferView = gltf.bufferViews[bufferViewId];
const buffer = gltf.buffers[bufferView.buffer];
const source = buffer.extras._pipeline.source;
const byteOffset = bufferView.byteOffset ?? 0;
return source.slice(byteOffset, byteOffset + bufferView.byteLength);
}
function readFile(object, uri, saveResourceId, options) {
const resourceDirectory = options.resourceDirectory;
const hasResourceDirectory = defined(resourceDirectory);
const uriIsAbsolute = uri.startsWith("/") || uri.startsWith("file:///");
// Resolve the URL
let absoluteUrl;
// Since we treat this as a URI,
// file:///path/to/file, //path/to/file, and /path/to/file would all be considered absolute.
if (uriIsAbsolute) {
if (!options.allowAbsolute && hasResourceDirectory) {
return Promise.reject(
new RuntimeError(
"Absolute paths are not permitted; use the 'allowAbsolute' option to disable this error.",
),
);
}
if (!hasResourceDirectory) {
console.warn(
`No 'resourceDirectory' provided, so relative paths are impossible; forcing 'allowAbsolute' option to avoid breaking things.`,
);
}
}
let localUri = uri;
// new URL(`/`, undefined) throws an error because there's no protocol. Give it one so naked absolute paths
// survive the next step when `resourceDirectory` is not defined.
if (!resourceDirectory && localUri.startsWith("/")) {
localUri = new URL(localUri, "file:///");
}
try {
absoluteUrl = new URL(
localUri,
hasResourceDirectory ? pathToFileURL(resourceDirectory) : undefined,
);
} catch (error) {
return Promise.reject(
new RuntimeError(
"glTF model references separate files but no resourceDirectory is supplied",
),
);
}
// Generate file paths for the resource
const absolutePath = fileURLToPath(absoluteUrl);
const relativePath = hasResourceDirectory
? path.relative(resourceDirectory, absolutePath)
: path.basename(absolutePath);
if (!defined(object.name)) {
const extension = path.extname(relativePath);
object.name = path.basename(relativePath, extension);
}
if (saveResourceId) {
object.extras._pipeline.resourceId = absolutePath;
}
object.extras._pipeline.absolutePath = absolutePath;
object.extras._pipeline.relativePath = relativePath;
return fsExtra.readFile(absolutePath);
}