Add existing to tracked

This commit is contained in:
Jay
2026-08-11 09:53:42 -04:00
parent afe07f3055
commit ffd6e3d73c
8531 changed files with 4396230 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
"use strict";
const Cesium = require("cesium");
const ComponentDatatype = Cesium.ComponentDatatype;
module.exports = ArrayStorage;
const initialLength = 1024; // 2^10
const doublingThreshold = 33554432; // 2^25 (~134 MB for a Float32Array)
const fixedExpansionLength = 33554432; // 2^25 (~134 MB for a Float32Array)
/**
* Provides expandable typed array storage for geometry data. This is preferable to JS arrays which are
* stored with double precision. The resizing mechanism is similar to std::vector.
*
* @param {ComponentDatatype} componentDatatype The data type.
*
* @private
*/
function ArrayStorage(componentDatatype) {
this.componentDatatype = componentDatatype;
this.typedArray = ComponentDatatype.createTypedArray(componentDatatype, 0);
this.length = 0;
}
function resize(storage, length) {
const typedArray = ComponentDatatype.createTypedArray(
storage.componentDatatype,
length,
);
typedArray.set(storage.typedArray);
storage.typedArray = typedArray;
}
ArrayStorage.prototype.push = function (value) {
const length = this.length;
const typedArrayLength = this.typedArray.length;
if (length === 0) {
resize(this, initialLength);
} else if (length === typedArrayLength) {
if (length < doublingThreshold) {
resize(this, typedArrayLength * 2);
} else {
resize(this, typedArrayLength + fixedExpansionLength);
}
}
this.typedArray[this.length++] = value;
};
ArrayStorage.prototype.get = function (index) {
return this.typedArray[index];
};
const sizeOfUint16 = 2;
const sizeOfUint32 = 4;
const sizeOfFloat = 4;
ArrayStorage.prototype.toUint16Buffer = function () {
const length = this.length;
const typedArray = this.typedArray;
const paddedLength = length + (length % 2 === 0 ? 0 : 1); // Round to next multiple of 2
const buffer = Buffer.alloc(paddedLength * sizeOfUint16);
for (let i = 0; i < length; ++i) {
buffer.writeUInt16LE(typedArray[i], i * sizeOfUint16);
}
return buffer;
};
ArrayStorage.prototype.toUint32Buffer = function () {
const length = this.length;
const typedArray = this.typedArray;
const buffer = Buffer.alloc(length * sizeOfUint32);
for (let i = 0; i < length; ++i) {
buffer.writeUInt32LE(typedArray[i], i * sizeOfUint32);
}
return buffer;
};
ArrayStorage.prototype.toFloatBuffer = function () {
const length = this.length;
const typedArray = this.typedArray;
const buffer = Buffer.alloc(length * sizeOfFloat);
for (let i = 0; i < length; ++i) {
buffer.writeFloatLE(typedArray[i], i * sizeOfFloat);
}
return buffer;
};
ArrayStorage.prototype.getMinMax = function (components) {
const length = this.length;
const typedArray = this.typedArray;
const count = length / components;
const min = new Array(components).fill(Number.POSITIVE_INFINITY);
const max = new Array(components).fill(Number.NEGATIVE_INFINITY);
for (let i = 0; i < count; ++i) {
for (let j = 0; j < components; ++j) {
const index = i * components + j;
const value = typedArray[index];
min[j] = Math.min(min[j], value);
max[j] = Math.max(max[j], value);
}
}
return {
min: min,
max: max,
};
};
+19
View File
@@ -0,0 +1,19 @@
"use strict";
module.exports = Texture;
/**
* An object containing information about a texture.
*
* @private
*/
function Texture() {
this.transparent = false;
this.source = undefined;
this.name = undefined;
this.extension = undefined;
this.path = undefined;
this.pixels = undefined;
this.width = undefined;
this.height = undefined;
}
+712
View File
@@ -0,0 +1,712 @@
"use strict";
const FS_WRITE_MAX_LENGTH = 2147479552; // See https://github.com/nodejs/node/issues/35605
const BUFFER_MAX_LENGTH = require("buffer").constants.MAX_LENGTH;
const BUFFER_MAX_BYTE_LENGTH = Math.min(FS_WRITE_MAX_LENGTH, BUFFER_MAX_LENGTH);
const Cesium = require("cesium");
const getBufferPadded = require("./getBufferPadded");
const getDefaultMaterial = require("./loadMtl").getDefaultMaterial;
const Texture = require("./Texture");
const defaultValue = (a, b) => a ?? b;
const defined = Cesium.defined;
const WebGLConstants = Cesium.WebGLConstants;
module.exports = createGltf;
/**
* Create a glTF from obj data.
*
* @param {Object} objData An object containing an array of nodes containing geometry information and an array of materials.
* @param {Object} options The options object passed along from lib/obj2gltf.js
* @returns {Object} A glTF asset.
*
* @private
*/
function createGltf(objData, options) {
const nodes = objData.nodes;
let materials = objData.materials;
const name = objData.name;
// Split materials used by primitives with different types of attributes
materials = splitIncompatibleMaterials(nodes, materials, options);
const gltf = {
accessors: [],
asset: {},
buffers: [],
bufferViews: [],
extensionsUsed: [],
extensionsRequired: [],
images: [],
materials: [],
meshes: [],
nodes: [],
samplers: [],
scene: 0,
scenes: [],
textures: [],
};
gltf.asset = {
generator: "obj2gltf",
version: "2.0",
};
gltf.scenes.push({
nodes: [],
});
const bufferState = {
positionBuffers: [],
normalBuffers: [],
uvBuffers: [],
indexBuffers: [],
positionAccessors: [],
normalAccessors: [],
uvAccessors: [],
indexAccessors: [],
};
const uint32Indices = requiresUint32Indices(nodes);
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
const node = nodes[i];
const meshes = node.meshes;
const meshesLength = meshes.length;
if (meshesLength === 1) {
const meshIndex = addMesh(
gltf,
materials,
bufferState,
uint32Indices,
meshes[0],
options,
);
addNode(gltf, node.name, meshIndex, undefined);
} else {
// Add meshes as child nodes
const parentIndex = addNode(gltf, node.name);
for (let j = 0; j < meshesLength; ++j) {
const mesh = meshes[j];
const meshIndex = addMesh(
gltf,
materials,
bufferState,
uint32Indices,
mesh,
options,
);
addNode(gltf, mesh.name, meshIndex, parentIndex);
}
}
}
if (gltf.images.length > 0) {
gltf.samplers.push({
wrapS: WebGLConstants.REPEAT,
wrapT: WebGLConstants.REPEAT,
});
}
addBuffers(gltf, bufferState, name, options.separate);
if (options.specularGlossiness) {
gltf.extensionsUsed.push("KHR_materials_pbrSpecularGlossiness");
gltf.extensionsRequired.push("KHR_materials_pbrSpecularGlossiness");
}
if (options.unlit) {
gltf.extensionsUsed.push("KHR_materials_unlit");
gltf.extensionsRequired.push("KHR_materials_unlit");
}
return gltf;
}
function addCombinedBufferView(gltf, buffers, accessors, byteStride, target) {
const length = buffers.length;
if (length === 0) {
return;
}
const bufferViewIndex = gltf.bufferViews.length;
const previousBufferView = gltf.bufferViews[bufferViewIndex - 1];
const byteOffset = defined(previousBufferView)
? previousBufferView.byteOffset + previousBufferView.byteLength
: 0;
let byteLength = 0;
for (let i = 0; i < length; ++i) {
const accessor = gltf.accessors[accessors[i]];
accessor.bufferView = bufferViewIndex;
accessor.byteOffset = byteLength;
byteLength += buffers[i].length;
}
gltf.bufferViews.push({
name: `bufferView_${bufferViewIndex}`,
buffer: 0,
byteLength: byteLength,
byteOffset: byteOffset,
byteStride: byteStride,
target: target,
});
}
function addCombinedBuffers(gltf, bufferState, name) {
addCombinedBufferView(
gltf,
bufferState.positionBuffers,
bufferState.positionAccessors,
12,
WebGLConstants.ARRAY_BUFFER,
);
addCombinedBufferView(
gltf,
bufferState.normalBuffers,
bufferState.normalAccessors,
12,
WebGLConstants.ARRAY_BUFFER,
);
addCombinedBufferView(
gltf,
bufferState.uvBuffers,
bufferState.uvAccessors,
8,
WebGLConstants.ARRAY_BUFFER,
);
addCombinedBufferView(
gltf,
bufferState.indexBuffers,
bufferState.indexAccessors,
undefined,
WebGLConstants.ELEMENT_ARRAY_BUFFER,
);
let buffers = [];
buffers = buffers.concat(
bufferState.positionBuffers,
bufferState.normalBuffers,
bufferState.uvBuffers,
bufferState.indexBuffers,
);
const buffer = getBufferPadded(Buffer.concat(buffers));
gltf.buffers.push({
name: name,
byteLength: buffer.length,
extras: {
_obj2gltf: {
source: buffer,
},
},
});
}
function addSeparateBufferView(
gltf,
buffer,
accessor,
byteStride,
target,
name,
) {
const bufferIndex = gltf.buffers.length;
const bufferViewIndex = gltf.bufferViews.length;
gltf.buffers.push({
name: `${name}_${bufferIndex}`,
byteLength: buffer.length,
extras: {
_obj2gltf: {
source: buffer,
},
},
});
gltf.bufferViews.push({
buffer: bufferIndex,
byteLength: buffer.length,
byteOffset: 0,
byteStride: byteStride,
target: target,
});
gltf.accessors[accessor].bufferView = bufferViewIndex;
gltf.accessors[accessor].byteOffset = 0;
}
function addSeparateBufferViews(
gltf,
buffers,
accessors,
byteStride,
target,
name,
) {
const length = buffers.length;
for (let i = 0; i < length; ++i) {
addSeparateBufferView(
gltf,
buffers[i],
accessors[i],
byteStride,
target,
name,
);
}
}
function addSeparateBuffers(gltf, bufferState, name) {
addSeparateBufferViews(
gltf,
bufferState.positionBuffers,
bufferState.positionAccessors,
12,
WebGLConstants.ARRAY_BUFFER,
name,
);
addSeparateBufferViews(
gltf,
bufferState.normalBuffers,
bufferState.normalAccessors,
12,
WebGLConstants.ARRAY_BUFFER,
name,
);
addSeparateBufferViews(
gltf,
bufferState.uvBuffers,
bufferState.uvAccessors,
8,
WebGLConstants.ARRAY_BUFFER,
name,
);
addSeparateBufferViews(
gltf,
bufferState.indexBuffers,
bufferState.indexAccessors,
undefined,
WebGLConstants.ELEMENT_ARRAY_BUFFER,
name,
);
}
function addBuffers(gltf, bufferState, name, separate) {
const buffers = bufferState.positionBuffers.concat(
bufferState.normalBuffers,
bufferState.uvBuffers,
bufferState.indexBuffers,
);
const buffersLength = buffers.length;
let buffersByteLength = 0;
for (let i = 0; i < buffersLength; ++i) {
buffersByteLength += buffers[i].length;
}
if (separate && buffersByteLength > createGltf._getBufferMaxByteLength()) {
// Don't combine buffers if the combined buffer will exceed the Node limit.
addSeparateBuffers(gltf, bufferState, name);
} else {
addCombinedBuffers(gltf, bufferState, name);
}
}
function addTexture(gltf, texture) {
const imageName = texture.name;
const textureName = texture.name;
const imageIndex = gltf.images.length;
const textureIndex = gltf.textures.length;
gltf.images.push({
name: imageName,
extras: {
_obj2gltf: texture,
},
});
gltf.textures.push({
name: textureName,
sampler: 0,
source: imageIndex,
});
return textureIndex;
}
function getTexture(gltf, texture) {
let textureIndex;
const images = gltf.images;
const length = images.length;
for (let i = 0; i < length; ++i) {
if (images[i].extras._obj2gltf === texture) {
textureIndex = i;
break;
}
}
if (!defined(textureIndex)) {
textureIndex = addTexture(gltf, texture);
}
return {
index: textureIndex,
};
}
function cloneMaterial(material, removeTextures) {
if (typeof material !== "object") {
return material;
} else if (material instanceof Texture) {
if (removeTextures) {
return undefined;
}
return material;
} else if (Array.isArray(material)) {
const length = material.length;
const clonedArray = new Array(length);
for (let i = 0; i < length; ++i) {
clonedArray[i] = cloneMaterial(material[i], removeTextures);
}
return clonedArray;
}
const clonedObject = {};
for (const name in material) {
if (Object.prototype.hasOwnProperty.call(material, name)) {
clonedObject[name] = cloneMaterial(material[name], removeTextures);
}
}
return clonedObject;
}
function resolveTextures(gltf, material) {
for (const name in material) {
if (Object.prototype.hasOwnProperty.call(material, name)) {
const property = material[name];
if (property instanceof Texture) {
material[name] = getTexture(gltf, property);
} else if (!Array.isArray(property) && typeof property === "object") {
resolveTextures(gltf, property);
}
}
}
}
function addGltfMaterial(gltf, material, options) {
resolveTextures(gltf, material);
const materialIndex = gltf.materials.length;
if (options.unlit) {
if (!defined(material.extensions)) {
material.extensions = {};
}
material.extensions.KHR_materials_unlit = {};
}
gltf.materials.push(material);
return materialIndex;
}
function getMaterialByName(materials, materialName) {
const materialsLength = materials.length;
for (let i = 0; i < materialsLength; ++i) {
if (materials[i].name === materialName) {
return materials[i];
}
}
}
function getMaterialIndex(materials, materialName) {
const materialsLength = materials.length;
for (let i = 0; i < materialsLength; ++i) {
if (materials[i].name === materialName) {
return i;
}
}
}
function getOrCreateGltfMaterial(gltf, materials, materialName, options) {
const material = getMaterialByName(materials, materialName);
let materialIndex = getMaterialIndex(gltf.materials, materialName);
if (!defined(materialIndex)) {
materialIndex = addGltfMaterial(gltf, material, options);
}
return materialIndex;
}
function primitiveInfoMatch(a, b) {
return a.hasUvs === b.hasUvs && a.hasNormals === b.hasNormals;
}
function getSplitMaterialName(
originalMaterialName,
primitiveInfo,
primitiveInfoByMaterial,
) {
let splitMaterialName = originalMaterialName;
let suffix = 2;
while (defined(primitiveInfoByMaterial[splitMaterialName])) {
if (
primitiveInfoMatch(
primitiveInfo,
primitiveInfoByMaterial[splitMaterialName],
)
) {
break;
}
splitMaterialName = `${originalMaterialName}-${suffix++}`;
}
return splitMaterialName;
}
function splitIncompatibleMaterials(nodes, materials, options) {
const splitMaterials = [];
const primitiveInfoByMaterial = {};
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
const meshes = nodes[i].meshes;
const meshesLength = meshes.length;
for (let j = 0; j < meshesLength; ++j) {
const primitives = meshes[j].primitives;
const primitivesLength = primitives.length;
for (let k = 0; k < primitivesLength; ++k) {
const primitive = primitives[k];
const hasUvs = primitive.uvs.length > 0;
const hasNormals = primitive.normals.length > 0;
const primitiveInfo = {
hasUvs: hasUvs,
hasNormals: hasNormals,
};
const originalMaterialName = defaultValue(
primitive.material,
"default",
);
const splitMaterialName = getSplitMaterialName(
originalMaterialName,
primitiveInfo,
primitiveInfoByMaterial,
);
primitive.material = splitMaterialName;
primitiveInfoByMaterial[splitMaterialName] = primitiveInfo;
let splitMaterial = getMaterialByName(
splitMaterials,
splitMaterialName,
);
if (defined(splitMaterial)) {
continue;
}
const originalMaterial = getMaterialByName(
materials,
originalMaterialName,
);
if (defined(originalMaterial)) {
splitMaterial = cloneMaterial(originalMaterial, !hasUvs);
} else {
splitMaterial = getDefaultMaterial(options);
}
splitMaterial.name = splitMaterialName;
splitMaterials.push(splitMaterial);
}
}
}
return splitMaterials;
}
function addVertexAttribute(gltf, array, components, name) {
const count = array.length / components;
const minMax = array.getMinMax(components);
const type = components === 3 ? "VEC3" : "VEC2";
const accessor = {
name: name,
componentType: WebGLConstants.FLOAT,
count: count,
min: minMax.min,
max: minMax.max,
type: type,
};
const accessorIndex = gltf.accessors.length;
gltf.accessors.push(accessor);
return accessorIndex;
}
function addIndexArray(gltf, array, uint32Indices, name) {
const componentType = uint32Indices
? WebGLConstants.UNSIGNED_INT
: WebGLConstants.UNSIGNED_SHORT;
const count = array.length;
const minMax = array.getMinMax(1);
const accessor = {
name: name,
componentType: componentType,
count: count,
min: minMax.min,
max: minMax.max,
type: "SCALAR",
};
const accessorIndex = gltf.accessors.length;
gltf.accessors.push(accessor);
return accessorIndex;
}
function requiresUint32Indices(nodes) {
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
const meshes = nodes[i].meshes;
const meshesLength = meshes.length;
for (let j = 0; j < meshesLength; ++j) {
const primitives = meshes[j].primitives;
const primitivesLength = primitives.length;
for (let k = 0; k < primitivesLength; ++k) {
// Reserve the 65535 index for primitive restart
const vertexCount = primitives[k].positions.length / 3;
if (vertexCount > 65534) {
return true;
}
}
}
}
return false;
}
function addPrimitive(
gltf,
materials,
bufferState,
uint32Indices,
mesh,
primitive,
index,
options,
) {
const hasPositions = primitive.positions.length > 0;
const hasNormals = primitive.normals.length > 0;
const hasUVs = primitive.uvs.length > 0;
const attributes = {};
if (hasPositions) {
const accessorIndex = addVertexAttribute(
gltf,
primitive.positions,
3,
`${mesh.name}_${index}_positions`,
);
attributes.POSITION = accessorIndex;
bufferState.positionBuffers.push(primitive.positions.toFloatBuffer());
bufferState.positionAccessors.push(accessorIndex);
}
if (hasNormals) {
const accessorIndex = addVertexAttribute(
gltf,
primitive.normals,
3,
`${mesh.name}_${index}_normals`,
);
attributes.NORMAL = accessorIndex;
bufferState.normalBuffers.push(primitive.normals.toFloatBuffer());
bufferState.normalAccessors.push(accessorIndex);
}
if (hasUVs) {
const accessorIndex = addVertexAttribute(
gltf,
primitive.uvs,
2,
`${mesh.name}_${index}_texcoords`,
);
attributes.TEXCOORD_0 = accessorIndex;
bufferState.uvBuffers.push(primitive.uvs.toFloatBuffer());
bufferState.uvAccessors.push(accessorIndex);
}
const indexAccessorIndex = addIndexArray(
gltf,
primitive.indices,
uint32Indices,
`${mesh.name}_${index}_indices`,
);
const indexBuffer = uint32Indices
? primitive.indices.toUint32Buffer()
: primitive.indices.toUint16Buffer();
bufferState.indexBuffers.push(indexBuffer);
bufferState.indexAccessors.push(indexAccessorIndex);
// Unload resources
primitive.positions = undefined;
primitive.normals = undefined;
primitive.uvs = undefined;
primitive.indices = undefined;
const materialIndex = getOrCreateGltfMaterial(
gltf,
materials,
primitive.material,
options,
);
return {
attributes: attributes,
indices: indexAccessorIndex,
material: materialIndex,
mode: WebGLConstants.TRIANGLES,
};
}
function addMesh(gltf, materials, bufferState, uint32Indices, mesh, options) {
const gltfPrimitives = [];
const primitives = mesh.primitives;
const primitivesLength = primitives.length;
for (let i = 0; i < primitivesLength; ++i) {
gltfPrimitives.push(
addPrimitive(
gltf,
materials,
bufferState,
uint32Indices,
mesh,
primitives[i],
i,
options,
),
);
}
const gltfMesh = {
name: mesh.name,
primitives: gltfPrimitives,
};
const meshIndex = gltf.meshes.length;
gltf.meshes.push(gltfMesh);
return meshIndex;
}
function addNode(gltf, name, meshIndex, parentIndex) {
const node = {
name: name,
mesh: meshIndex,
};
const nodeIndex = gltf.nodes.length;
gltf.nodes.push(node);
if (defined(parentIndex)) {
const parentNode = gltf.nodes[parentIndex];
if (!defined(parentNode.children)) {
parentNode.children = [];
}
parentNode.children.push(nodeIndex);
} else {
gltf.scenes[gltf.scene].nodes.push(nodeIndex);
}
return nodeIndex;
}
// Exposed for testing
createGltf._getBufferMaxByteLength = function () {
return BUFFER_MAX_BYTE_LENGTH;
};
+22
View File
@@ -0,0 +1,22 @@
"use strict";
module.exports = getBufferPadded;
/**
* Pad the buffer to the next 4-byte boundary to ensure proper alignment for the section that follows.
*
* @param {Buffer} buffer The buffer.
* @returns {Buffer} The padded buffer.
*
* @private
*/
function getBufferPadded(buffer) {
const boundary = 4;
const byteLength = buffer.length;
const remainder = byteLength % boundary;
if (remainder === 0) {
return buffer;
}
const padding = remainder === 0 ? 0 : boundary - remainder;
const emptyBuffer = Buffer.alloc(padding);
return Buffer.concat([buffer, emptyBuffer]);
}
+29
View File
@@ -0,0 +1,29 @@
"use strict";
module.exports = getJsonBufferPadded;
/**
* Convert the JSON object to a padded buffer.
*
* Pad the JSON with extra whitespace to fit the next 4-byte boundary. This ensures proper alignment
* for the section that follows.
*
* @param {Object} [json] The JSON object.
* @returns {Buffer} The padded JSON buffer.
*
* @private
*/
function getJsonBufferPadded(json) {
let string = JSON.stringify(json);
const boundary = 4;
const byteLength = Buffer.byteLength(string);
const remainder = byteLength % boundary;
const padding = remainder === 0 ? 0 : boundary - remainder;
let whitespace = "";
for (let i = 0; i < padding; ++i) {
whitespace += " ";
}
string += whitespace;
return Buffer.from(string);
}
+61
View File
@@ -0,0 +1,61 @@
"use strict";
const Cesium = require("cesium");
const getJsonBufferPadded = require("./getJsonBufferPadded");
const defined = Cesium.defined;
module.exports = gltfToGlb;
/**
* Convert a glTF to binary glTF.
*
* The glTF is expected to have a single buffer and all embedded resources stored in bufferViews.
*
* @param {Object} gltf The glTF asset.
* @param {Buffer} binaryBuffer The binary buffer.
* @returns {Buffer} The glb buffer.
*
* @private
*/
function gltfToGlb(gltf, binaryBuffer) {
const buffer = gltf.buffers[0];
if (defined(buffer.uri)) {
binaryBuffer = Buffer.alloc(0);
}
// Create padded binary scene string
const jsonBuffer = getJsonBufferPadded(gltf);
// Allocate buffer (Global header) + (JSON chunk header) + (JSON chunk) + (Binary chunk header) + (Binary chunk)
const glbLength = 12 + 8 + jsonBuffer.length + 8 + binaryBuffer.length;
const glb = Buffer.alloc(glbLength);
// Write binary glTF header (magic, version, length)
let byteOffset = 0;
glb.writeUInt32LE(0x46546c67, byteOffset);
byteOffset += 4;
glb.writeUInt32LE(2, byteOffset);
byteOffset += 4;
glb.writeUInt32LE(glbLength, byteOffset);
byteOffset += 4;
// Write JSON Chunk header (length, type)
glb.writeUInt32LE(jsonBuffer.length, byteOffset);
byteOffset += 4;
glb.writeUInt32LE(0x4e4f534a, byteOffset); // JSON
byteOffset += 4;
// Write JSON Chunk
jsonBuffer.copy(glb, byteOffset);
byteOffset += jsonBuffer.length;
// Write Binary Chunk header (length, type)
glb.writeUInt32LE(binaryBuffer.length, byteOffset);
byteOffset += 4;
glb.writeUInt32LE(0x004e4942, byteOffset); // BIN
byteOffset += 4;
// Write Binary Chunk
binaryBuffer.copy(glb, byteOffset);
return glb;
}
+991
View File
@@ -0,0 +1,991 @@
"use strict";
const Cesium = require("cesium");
const path = require("path");
const Promise = require("bluebird");
const loadTexture = require("./loadTexture");
const outsideDirectory = require("./outsideDirectory");
const readLines = require("./readLines");
const Texture = require("./Texture");
const CesiumMath = Cesium.Math;
const clone = Cesium.clone;
const combine = Cesium.combine;
const defaultValue = (a, b) => a ?? b;
const defined = Cesium.defined;
module.exports = loadMtl;
/**
* Parse a .mtl file and load textures referenced within. Returns an array of glTF materials with Texture
* objects stored in the texture slots.
* <p>
* Packed PBR textures (like metallicRoughnessOcclusion and specularGlossiness) require all input textures to be decoded before hand.
* If a texture is of an unsupported format like .gif or .tga it can't be packed and a metallicRoughness texture will not be created.
* Similarly if a texture cannot be found it will be ignored and a default value will be used instead.
* </p>
*
* @param {String} mtlPath Path to the .mtl file.
* @param {Object} options The options object passed along from lib/obj2gltf.js
* @returns {Promise} A promise resolving to an array of glTF materials with Texture objects stored in the texture slots.
*
* @private
*/
function loadMtl(mtlPath, options) {
let material;
let values;
let value;
const mtlDirectory = path.dirname(mtlPath);
const materials = [];
const texturePromiseMap = {}; // Maps texture paths to load promises so that no texture is loaded twice
const texturePromises = [];
const overridingTextures = options.overridingTextures;
const overridingSpecularTexture = defaultValue(
overridingTextures.metallicRoughnessOcclusionTexture,
overridingTextures.specularGlossinessTexture,
);
const overridingSpecularShininessTexture = defaultValue(
overridingTextures.metallicRoughnessOcclusionTexture,
overridingTextures.specularGlossinessTexture,
);
const overridingAmbientTexture = defaultValue(
overridingTextures.metallicRoughnessOcclusionTexture,
overridingTextures.occlusionTexture,
);
const overridingNormalTexture = overridingTextures.normalTexture;
const overridingDiffuseTexture = overridingTextures.baseColorTexture;
const overridingEmissiveTexture = overridingTextures.emissiveTexture;
const overridingAlphaTexture = overridingTextures.alphaTexture;
// Textures that are packed into PBR textures need to be decoded first
const decodeOptions = {
decode: true,
};
const diffuseTextureOptions = {
checkTransparency: options.checkTransparency,
};
const ambientTextureOptions = defined(overridingAmbientTexture)
? undefined
: options.packOcclusion
? decodeOptions
: undefined;
const specularTextureOptions = defined(overridingSpecularTexture)
? undefined
: decodeOptions;
const specularShinessTextureOptions = defined(
overridingSpecularShininessTexture,
)
? undefined
: decodeOptions;
const emissiveTextureOptions = undefined;
const normalTextureOptions = undefined;
const alphaTextureOptions = {
decode: true,
};
function createMaterial(name) {
material = new Material();
material.name = name;
material.specularShininess = options.metallicRoughness ? 1.0 : 0.0;
material.specularTexture = overridingSpecularTexture;
material.specularShininessTexture = overridingSpecularShininessTexture;
material.diffuseTexture = overridingDiffuseTexture;
material.ambientTexture = overridingAmbientTexture;
material.normalTexture = overridingNormalTexture;
material.emissiveTexture = overridingEmissiveTexture;
material.alphaTexture = overridingAlphaTexture;
materials.push(material);
}
function normalizeTexturePath(texturePath, mtlDirectory) {
//Remove double quotes around the texture file if it exists
texturePath = texturePath.replace(/^"(.+)"$/, "$1");
// Removes texture options from texture name
// Assumes no spaces in texture name
const re = /-(bm|t|s|o|blendu|blendv|boost|mm|texres|clamp|imfchan|type)/;
if (re.test(texturePath)) {
texturePath = texturePath.split(/\s+/).pop();
}
texturePath = texturePath.replace(/\\/g, "/");
return path.normalize(path.resolve(mtlDirectory, texturePath));
}
function parseLine(line) {
line = line.trim();
if (/^newmtl/i.test(line)) {
const name = line.substring(7).trim();
createMaterial(name);
} else if (/^Ka /i.test(line)) {
values = line.substring(3).trim().split(" ");
material.ambientColor = [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
1.0,
];
} else if (/^Ke /i.test(line)) {
values = line.substring(3).trim().split(" ");
material.emissiveColor = [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
1.0,
];
} else if (/^Kd /i.test(line)) {
values = line.substring(3).trim().split(" ");
material.diffuseColor = [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
1.0,
];
} else if (/^Ks /i.test(line)) {
values = line.substring(3).trim().split(" ");
material.specularColor = [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
1.0,
];
} else if (/^Ns /i.test(line)) {
value = line.substring(3).trim();
material.specularShininess = parseFloat(value);
} else if (/^d /i.test(line)) {
value = line.substring(2).trim();
material.alpha = correctAlpha(parseFloat(value));
} else if (/^Tr /i.test(line)) {
value = line.substring(3).trim();
material.alpha = correctAlpha(1.0 - parseFloat(value));
} else if (/^map_Ka /i.test(line)) {
if (!defined(overridingAmbientTexture)) {
material.ambientTexture = normalizeTexturePath(
line.substring(7).trim(),
mtlDirectory,
);
}
} else if (/^map_Ke /i.test(line)) {
if (!defined(overridingEmissiveTexture)) {
material.emissiveTexture = normalizeTexturePath(
line.substring(7).trim(),
mtlDirectory,
);
}
} else if (/^map_Kd /i.test(line)) {
if (!defined(overridingDiffuseTexture)) {
material.diffuseTexture = normalizeTexturePath(
line.substring(7).trim(),
mtlDirectory,
);
}
} else if (/^map_Ks /i.test(line)) {
if (!defined(overridingSpecularTexture)) {
material.specularTexture = normalizeTexturePath(
line.substring(7).trim(),
mtlDirectory,
);
}
} else if (/^map_Ns /i.test(line)) {
if (!defined(overridingSpecularShininessTexture)) {
material.specularShininessTexture = normalizeTexturePath(
line.substring(7).trim(),
mtlDirectory,
);
}
} else if (/^map_Bump /i.test(line)) {
if (!defined(overridingNormalTexture)) {
material.normalTexture = normalizeTexturePath(
line.substring(9).trim(),
mtlDirectory,
);
}
} else if (/^map_d /i.test(line)) {
if (!defined(overridingAlphaTexture)) {
material.alphaTexture = normalizeTexturePath(
line.substring(6).trim(),
mtlDirectory,
);
}
}
}
function loadMaterialTextures(material) {
// If an alpha texture is present the diffuse texture needs to be decoded so they can be packed together
const diffuseAlphaTextureOptions = defined(material.alphaTexture)
? alphaTextureOptions
: diffuseTextureOptions;
if (material.diffuseTexture === material.ambientTexture) {
// OBJ models are often exported with the same texture in the diffuse and ambient slots but this is typically not desirable, particularly
// when saving with PBR materials where the ambient texture is treated as the occlusion texture.
material.ambientTexture = undefined;
}
const textureNames = [
"diffuseTexture",
"ambientTexture",
"emissiveTexture",
"specularTexture",
"specularShininessTexture",
"normalTexture",
"alphaTexture",
];
const textureOptions = [
diffuseAlphaTextureOptions,
ambientTextureOptions,
emissiveTextureOptions,
specularTextureOptions,
specularShinessTextureOptions,
normalTextureOptions,
alphaTextureOptions,
];
const sharedOptions = {};
textureNames.forEach(function (name, index) {
const texturePath = material[name];
const originalOptions = textureOptions[index];
if (defined(texturePath) && defined(originalOptions)) {
if (!defined(sharedOptions[texturePath])) {
sharedOptions[texturePath] = clone(originalOptions);
}
const options = sharedOptions[texturePath];
options.checkTransparency =
options.checkTransparency || originalOptions.checkTransparency;
options.decode = options.decode || originalOptions.decode;
options.keepSource =
options.keepSource ||
!originalOptions.decode ||
!originalOptions.checkTransparency;
}
});
textureNames.forEach(function (name) {
const texturePath = material[name];
if (defined(texturePath)) {
loadMaterialTexture(
material,
name,
sharedOptions[texturePath],
mtlDirectory,
texturePromiseMap,
texturePromises,
options,
);
}
});
}
return readLines(mtlPath, parseLine)
.then(function () {
const length = materials.length;
for (let i = 0; i < length; ++i) {
loadMaterialTextures(materials[i]);
}
return Promise.all(texturePromises);
})
.then(function () {
return convertMaterials(materials, options);
});
}
function correctAlpha(alpha) {
// An alpha of 0.0 usually implies a problem in the export, change to 1.0 instead
return alpha === 0.0 ? 1.0 : alpha;
}
function Material() {
this.name = undefined;
this.ambientColor = [0.0, 0.0, 0.0, 1.0]; // Ka
this.emissiveColor = [0.0, 0.0, 0.0, 1.0]; // Ke
this.diffuseColor = [0.5, 0.5, 0.5, 1.0]; // Kd
this.specularColor = [0.0, 0.0, 0.0, 1.0]; // Ks
this.specularShininess = 0.0; // Ns
this.alpha = 1.0; // d / Tr
this.ambientTexture = undefined; // map_Ka
this.emissiveTexture = undefined; // map_Ke
this.diffuseTexture = undefined; // map_Kd
this.specularTexture = undefined; // map_Ks
this.specularShininessTexture = undefined; // map_Ns
this.normalTexture = undefined; // map_Bump
this.alphaTexture = undefined; // map_d
}
loadMtl.getDefaultMaterial = function (options) {
return convertMaterial(new Material(), options);
};
// Exposed for testing
loadMtl._createMaterial = function (materialOptions, options) {
return convertMaterial(combine(materialOptions, new Material()), options);
};
function loadMaterialTexture(
material,
name,
textureOptions,
mtlDirectory,
texturePromiseMap,
texturePromises,
options,
) {
const texturePath = material[name];
if (!defined(texturePath)) {
return;
}
let texturePromise = texturePromiseMap[texturePath];
if (!defined(texturePromise)) {
const shallowPath = path.join(mtlDirectory, path.basename(texturePath));
if (options.secure && outsideDirectory(texturePath, mtlDirectory)) {
// Try looking for the texture in the same directory as the obj
options.logger(
"Texture file is outside of the mtl directory and the secure flag is true. Attempting to read the texture file from within the obj directory instead.",
);
texturePromise = loadTexture(shallowPath, textureOptions).catch(
function (error) {
options.logger(error.message);
options.logger(
`Could not read texture file at ${shallowPath}. This texture will be ignored`,
);
},
);
} else {
texturePromise = loadTexture(texturePath, textureOptions)
.catch(function (error) {
// Try looking for the texture in the same directory as the obj
options.logger(error.message);
options.logger(
`Could not read texture file at ${texturePath}. Attempting to read the texture file from within the obj directory instead.`,
);
return loadTexture(shallowPath, textureOptions);
})
.catch(function (error) {
options.logger(error.message);
options.logger(
`Could not read texture file at ${shallowPath}. This texture will be ignored.`,
);
});
}
texturePromiseMap[texturePath] = texturePromise;
}
texturePromises.push(
texturePromise.then(function (texture) {
material[name] = texture;
}),
);
}
function convertMaterial(material, options) {
if (options.specularGlossiness) {
return createSpecularGlossinessMaterial(material, options);
} else if (options.metallicRoughness) {
return createMetallicRoughnessMaterial(material, options);
}
// No material type specified, convert the material to metallic roughness
convertTraditionalToMetallicRoughness(material);
return createMetallicRoughnessMaterial(material, options);
}
function convertMaterials(materials, options) {
return materials.map(function (material) {
return convertMaterial(material, options);
});
}
function resizeChannel(
sourcePixels,
sourceWidth,
sourceHeight,
targetPixels,
targetWidth,
targetHeight,
) {
// Nearest neighbor sampling
const widthRatio = sourceWidth / targetWidth;
const heightRatio = sourceHeight / targetHeight;
for (let y = 0; y < targetHeight; ++y) {
for (let x = 0; x < targetWidth; ++x) {
const targetIndex = y * targetWidth + x;
const sourceY = Math.round(y * heightRatio);
const sourceX = Math.round(x * widthRatio);
const sourceIndex = sourceY * sourceWidth + sourceX;
const sourceValue = sourcePixels.readUInt8(sourceIndex);
targetPixels.writeUInt8(sourceValue, targetIndex);
}
}
return targetPixels;
}
let scratchResizeChannel;
function getTextureChannel(
texture,
index,
targetWidth,
targetHeight,
targetChannel,
) {
const pixels = texture.pixels; // RGBA
const sourceWidth = texture.width;
const sourceHeight = texture.height;
const sourcePixelsLength = sourceWidth * sourceHeight;
const targetPixelsLength = targetWidth * targetHeight;
// Allocate the scratchResizeChannel on demand if the texture needs to be resized
let sourceChannel = targetChannel;
if (sourcePixelsLength > targetPixelsLength) {
if (
!defined(scratchResizeChannel) ||
sourcePixelsLength > scratchResizeChannel.length
) {
scratchResizeChannel = Buffer.alloc(sourcePixelsLength);
}
sourceChannel = scratchResizeChannel;
}
for (let i = 0; i < sourcePixelsLength; ++i) {
const value = pixels.readUInt8(i * 4 + index);
sourceChannel.writeUInt8(value, i);
}
if (sourcePixelsLength > targetPixelsLength) {
resizeChannel(
sourceChannel,
sourceWidth,
sourceHeight,
targetChannel,
targetWidth,
targetHeight,
);
}
return targetChannel;
}
function writeChannel(pixels, channel, index) {
const pixelsLength = pixels.length / 4;
for (let i = 0; i < pixelsLength; ++i) {
const value = channel.readUInt8(i);
pixels.writeUInt8(value, i * 4 + index);
}
}
function getMinimumDimensions(textures, options) {
let width = Number.POSITIVE_INFINITY;
let height = Number.POSITIVE_INFINITY;
const length = textures.length;
for (let i = 0; i < length; ++i) {
const texture = textures[i];
width = Math.min(texture.width, width);
height = Math.min(texture.height, height);
}
for (let i = 0; i < length; ++i) {
const texture = textures[i];
if (texture.width !== width || texture.height !== height) {
options.logger(
`Texture ${texture.path} will be scaled from ${texture.width}x${texture.height} to ${width}x${height}.`,
);
}
}
return [width, height];
}
function isChannelSingleColor(buffer) {
const first = buffer.readUInt8(0);
const length = buffer.length;
for (let i = 1; i < length; ++i) {
if (buffer[i] !== first) {
return false;
}
}
return true;
}
function createDiffuseAlphaTexture(diffuseTexture, alphaTexture, options) {
const packDiffuse = defined(diffuseTexture);
const packAlpha = defined(alphaTexture);
if (!packDiffuse) {
return undefined;
}
if (!packAlpha) {
return diffuseTexture;
}
if (diffuseTexture === alphaTexture) {
return diffuseTexture;
}
if (!defined(diffuseTexture.pixels) || !defined(alphaTexture.pixels)) {
options.logger(
`Could not get decoded texture data for ${diffuseTexture.path} or ${alphaTexture.path}. The material will be created without an alpha texture.`,
);
return diffuseTexture;
}
const packedTextures = [diffuseTexture, alphaTexture];
const dimensions = getMinimumDimensions(packedTextures, options);
const width = dimensions[0];
const height = dimensions[1];
const pixelsLength = width * height;
const pixels = Buffer.alloc(pixelsLength * 4, 0xff); // Initialize with 4 channels
const scratchChannel = Buffer.alloc(pixelsLength);
// Write into the R, G, B channels
const redChannel = getTextureChannel(
diffuseTexture,
0,
width,
height,
scratchChannel,
);
writeChannel(pixels, redChannel, 0);
const greenChannel = getTextureChannel(
diffuseTexture,
1,
width,
height,
scratchChannel,
);
writeChannel(pixels, greenChannel, 1);
const blueChannel = getTextureChannel(
diffuseTexture,
2,
width,
height,
scratchChannel,
);
writeChannel(pixels, blueChannel, 2);
// First try reading the alpha component from the alpha channel, but if it is a single color read from the red channel instead.
let alphaChannel = getTextureChannel(
alphaTexture,
3,
width,
height,
scratchChannel,
);
if (isChannelSingleColor(alphaChannel)) {
alphaChannel = getTextureChannel(
alphaTexture,
0,
width,
height,
scratchChannel,
);
}
writeChannel(pixels, alphaChannel, 3);
const texture = new Texture();
texture.name = diffuseTexture.name;
texture.extension = ".png";
texture.pixels = pixels;
texture.width = width;
texture.height = height;
texture.transparent = true;
return texture;
}
function createMetallicRoughnessTexture(
metallicTexture,
roughnessTexture,
occlusionTexture,
options,
) {
if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture)) {
return metallicTexture;
}
const packMetallic = defined(metallicTexture);
const packRoughness = defined(roughnessTexture);
const packOcclusion = defined(occlusionTexture) && options.packOcclusion;
if (!packMetallic && !packRoughness) {
return undefined;
}
if (packMetallic && !defined(metallicTexture.pixels)) {
options.logger(
`Could not get decoded texture data for ${metallicTexture.path}. The material will be created without a metallicRoughness texture.`,
);
return undefined;
}
if (packRoughness && !defined(roughnessTexture.pixels)) {
options.logger(
`Could not get decoded texture data for ${roughnessTexture.path}. The material will be created without a metallicRoughness texture.`,
);
return undefined;
}
if (packOcclusion && !defined(occlusionTexture.pixels)) {
options.logger(
`Could not get decoded texture data for ${occlusionTexture.path}. The occlusion texture will not be packed in the metallicRoughness texture.`,
);
return undefined;
}
const packedTextures = [
metallicTexture,
roughnessTexture,
occlusionTexture,
].filter(function (texture) {
return defined(texture) && defined(texture.pixels);
});
const dimensions = getMinimumDimensions(packedTextures, options);
const width = dimensions[0];
const height = dimensions[1];
const pixelsLength = width * height;
const pixels = Buffer.alloc(pixelsLength * 4, 0xff); // Initialize with 4 channels, unused channels will be white
const scratchChannel = Buffer.alloc(pixelsLength);
if (packMetallic) {
// Write into the B channel
const metallicChannel = getTextureChannel(
metallicTexture,
0,
width,
height,
scratchChannel,
);
writeChannel(pixels, metallicChannel, 2);
}
if (packRoughness) {
// Write into the G channel
const roughnessChannel = getTextureChannel(
roughnessTexture,
0,
width,
height,
scratchChannel,
);
writeChannel(pixels, roughnessChannel, 1);
}
if (packOcclusion) {
// Write into the R channel
const occlusionChannel = getTextureChannel(
occlusionTexture,
0,
width,
height,
scratchChannel,
);
writeChannel(pixels, occlusionChannel, 0);
}
const length = packedTextures.length;
const names = new Array(length);
for (let i = 0; i < length; ++i) {
names[i] = packedTextures[i].name;
}
const name = names.join("_");
const texture = new Texture();
texture.name = name;
texture.extension = ".png";
texture.pixels = pixels;
texture.width = width;
texture.height = height;
return texture;
}
function createSpecularGlossinessTexture(
specularTexture,
glossinessTexture,
options,
) {
if (defined(options.overridingTextures.specularGlossinessTexture)) {
return specularTexture;
}
const packSpecular = defined(specularTexture);
const packGlossiness = defined(glossinessTexture);
if (!packSpecular && !packGlossiness) {
return undefined;
}
if (packSpecular && !defined(specularTexture.pixels)) {
options.logger(
`Could not get decoded texture data for ${specularTexture.path}. The material will be created without a specularGlossiness texture.`,
);
return undefined;
}
if (packGlossiness && !defined(glossinessTexture.pixels)) {
options.logger(
`Could not get decoded texture data for ${glossinessTexture.path}. The material will be created without a specularGlossiness texture.`,
);
return undefined;
}
const packedTextures = [specularTexture, glossinessTexture].filter(
function (texture) {
return defined(texture) && defined(texture.pixels);
},
);
const dimensions = getMinimumDimensions(packedTextures, options);
const width = dimensions[0];
const height = dimensions[1];
const pixelsLength = width * height;
const pixels = Buffer.alloc(pixelsLength * 4, 0xff); // Initialize with 4 channels, unused channels will be white
const scratchChannel = Buffer.alloc(pixelsLength);
if (packSpecular) {
// Write into the R, G, B channels
const redChannel = getTextureChannel(
specularTexture,
0,
width,
height,
scratchChannel,
);
writeChannel(pixels, redChannel, 0);
const greenChannel = getTextureChannel(
specularTexture,
1,
width,
height,
scratchChannel,
);
writeChannel(pixels, greenChannel, 1);
const blueChannel = getTextureChannel(
specularTexture,
2,
width,
height,
scratchChannel,
);
writeChannel(pixels, blueChannel, 2);
}
if (packGlossiness) {
// Write into the A channel
const glossinessChannel = getTextureChannel(
glossinessTexture,
0,
width,
height,
scratchChannel,
);
writeChannel(pixels, glossinessChannel, 3);
}
const length = packedTextures.length;
const names = new Array(length);
for (let i = 0; i < length; ++i) {
names[i] = packedTextures[i].name;
}
const name = names.join("_");
const texture = new Texture();
texture.name = name;
texture.extension = ".png";
texture.pixels = pixels;
texture.width = width;
texture.height = height;
return texture;
}
function createSpecularGlossinessMaterial(material, options) {
const emissiveTexture = material.emissiveTexture;
const normalTexture = material.normalTexture;
const occlusionTexture = material.ambientTexture;
const diffuseTexture = material.diffuseTexture;
const alphaTexture = material.alphaTexture;
const specularTexture = material.specularTexture;
const glossinessTexture = material.specularShininessTexture;
const specularGlossinessTexture = createSpecularGlossinessTexture(
specularTexture,
glossinessTexture,
options,
);
const diffuseAlphaTexture = createDiffuseAlphaTexture(
diffuseTexture,
alphaTexture,
options,
);
let emissiveFactor = material.emissiveColor.slice(0, 3);
let diffuseFactor = material.diffuseColor;
let specularFactor = material.specularColor.slice(0, 3);
let glossinessFactor = material.specularShininess;
if (defined(emissiveTexture)) {
emissiveFactor = [1.0, 1.0, 1.0];
}
if (defined(diffuseTexture)) {
diffuseFactor = [1.0, 1.0, 1.0, 1.0];
}
if (defined(specularTexture)) {
specularFactor = [1.0, 1.0, 1.0];
}
if (defined(glossinessTexture)) {
glossinessFactor = 1.0;
}
let transparent = false;
if (defined(alphaTexture)) {
transparent = true;
} else {
const alpha = material.alpha;
diffuseFactor[3] = alpha;
transparent = alpha < 1.0;
}
if (defined(diffuseTexture)) {
transparent = transparent || diffuseTexture.transparent;
}
const doubleSided = transparent || options.doubleSidedMaterial;
const alphaMode = transparent ? "BLEND" : "OPAQUE";
return {
name: material.name,
extensions: {
KHR_materials_pbrSpecularGlossiness: {
diffuseTexture: diffuseAlphaTexture,
specularGlossinessTexture: specularGlossinessTexture,
diffuseFactor: diffuseFactor,
specularFactor: specularFactor,
glossinessFactor: glossinessFactor,
},
},
emissiveTexture: emissiveTexture,
normalTexture: normalTexture,
occlusionTexture: occlusionTexture,
emissiveFactor: emissiveFactor,
alphaMode: alphaMode,
doubleSided: doubleSided,
};
}
function createMetallicRoughnessMaterial(material, options) {
const emissiveTexture = material.emissiveTexture;
const normalTexture = material.normalTexture;
let occlusionTexture = material.ambientTexture;
const baseColorTexture = material.diffuseTexture;
const alphaTexture = material.alphaTexture;
const metallicTexture = material.specularTexture;
const roughnessTexture = material.specularShininessTexture;
const metallicRoughnessTexture = createMetallicRoughnessTexture(
metallicTexture,
roughnessTexture,
occlusionTexture,
options,
);
const diffuseAlphaTexture = createDiffuseAlphaTexture(
baseColorTexture,
alphaTexture,
options,
);
if (options.packOcclusion) {
occlusionTexture = metallicRoughnessTexture;
}
let emissiveFactor = material.emissiveColor.slice(0, 3);
let baseColorFactor = material.diffuseColor;
let metallicFactor = material.specularColor[0];
let roughnessFactor = material.specularShininess;
if (defined(emissiveTexture)) {
emissiveFactor = [1.0, 1.0, 1.0];
}
if (defined(baseColorTexture)) {
baseColorFactor = [1.0, 1.0, 1.0, 1.0];
}
if (defined(metallicTexture)) {
metallicFactor = 1.0;
}
if (defined(roughnessTexture)) {
roughnessFactor = 1.0;
}
let transparent = false;
if (defined(alphaTexture)) {
transparent = true;
} else {
const alpha = material.alpha;
baseColorFactor[3] = alpha;
transparent = alpha < 1.0;
}
if (defined(baseColorTexture)) {
transparent = transparent || baseColorTexture.transparent;
}
const doubleSided = transparent || options.doubleSidedMaterial;
const alphaMode = transparent ? "BLEND" : "OPAQUE";
return {
name: material.name,
pbrMetallicRoughness: {
baseColorTexture: diffuseAlphaTexture,
metallicRoughnessTexture: metallicRoughnessTexture,
baseColorFactor: baseColorFactor,
metallicFactor: metallicFactor,
roughnessFactor: roughnessFactor,
},
emissiveTexture: emissiveTexture,
normalTexture: normalTexture,
occlusionTexture: occlusionTexture,
emissiveFactor: emissiveFactor,
alphaMode: alphaMode,
doubleSided: doubleSided,
};
}
function luminance(color) {
return color[0] * 0.2125 + color[1] * 0.7154 + color[2] * 0.0721;
}
function convertTraditionalToMetallicRoughness(material) {
// Translate the blinn-phong model to the pbr metallic-roughness model
// Roughness factor is a combination of specular intensity and shininess
// Metallic factor is 0.0
// Textures are not converted for now
const specularIntensity = luminance(material.specularColor);
// Transform from 0-1000 range to 0-1 range. Then invert.
let roughnessFactor = material.specularShininess;
roughnessFactor = roughnessFactor / 1000.0;
roughnessFactor = 1.0 - roughnessFactor;
roughnessFactor = CesiumMath.clamp(roughnessFactor, 0.0, 1.0);
// Low specular intensity values should produce a rough material even if shininess is high.
if (specularIntensity < 0.1) {
roughnessFactor *= 1.0 - specularIntensity;
}
const metallicFactor = 0.0;
material.specularColor = [
metallicFactor,
metallicFactor,
metallicFactor,
1.0,
];
material.specularShininess = roughnessFactor;
}
+757
View File
@@ -0,0 +1,757 @@
"use strict";
const Cesium = require("cesium");
const path = require("path");
const Promise = require("bluebird");
const ArrayStorage = require("./ArrayStorage");
const loadMtl = require("./loadMtl");
const outsideDirectory = require("./outsideDirectory");
const readLines = require("./readLines");
const Axis = Cesium.Axis;
const Cartesian3 = Cesium.Cartesian3;
const ComponentDatatype = Cesium.ComponentDatatype;
const CoplanarPolygonGeometryLibrary = Cesium.CoplanarPolygonGeometryLibrary;
const defaultValue = (a, b) => a ?? b;
const defined = Cesium.defined;
const PolygonPipeline = Cesium.PolygonPipeline;
const RuntimeError = Cesium.RuntimeError;
const WindingOrder = Cesium.WindingOrder;
const Matrix4 = Cesium.Matrix4;
module.exports = loadObj;
// Object name (o) -> node
// Group name (g) -> mesh
// Material name (usemtl) -> primitive
function Node() {
this.name = undefined;
this.meshes = [];
}
function Mesh() {
this.name = undefined;
this.primitives = [];
}
function Primitive() {
this.material = undefined;
this.indices = new ArrayStorage(ComponentDatatype.UNSIGNED_INT);
this.positions = new ArrayStorage(ComponentDatatype.FLOAT);
this.normals = new ArrayStorage(ComponentDatatype.FLOAT);
this.uvs = new ArrayStorage(ComponentDatatype.FLOAT);
}
// OBJ regex patterns are modified from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js)
const vertexPattern =
/v(\s+[\d|\.|\+|\-|e|E]+)(\s+[\d|\.|\+|\-|e|E]+)(\s+[\d|\.|\+|\-|e|E]+)/; // v float float float
const normalPattern =
/vn(\s+[\d|\.|\+|\-|e|E]+)(\s+[\d|\.|\+|\-|e|E]+)(\s+[\d|\.|\+|\-|e|E]+)/; // vn float float float
const uvPattern = /vt(\s+[\d|\.|\+|\-|e|E]+)(\s+[\d|\.|\+|\-|e|E]+)/; // vt float float
const facePattern = /(-?\d+)\/?(-?\d*)\/?(-?\d*)/g; // for any face format "f v", "f v/v", "f v//v", "f v/v/v"
const scratchCartesian = new Cartesian3();
/**
* Parse an obj file.
*
* @param {String} objPath Path to the obj file.
* @param {Object} options The options object passed along from lib/obj2gltf.js
* @returns {Promise} A promise resolving to the obj data, which includes an array of nodes containing geometry information and an array of materials.
*
* @private
*/
function loadObj(objPath, options) {
const axisTransform = getAxisTransform(
options.inputUpAxis,
options.outputUpAxis,
);
// Global store of vertex attributes listed in the obj file
let globalPositions = new ArrayStorage(ComponentDatatype.FLOAT);
let globalNormals = new ArrayStorage(ComponentDatatype.FLOAT);
let globalUvs = new ArrayStorage(ComponentDatatype.FLOAT);
// The current node, mesh, and primitive
let node;
let mesh;
let primitive;
let activeMaterial;
// All nodes seen in the obj
const nodes = [];
// Used to build the indices. The vertex cache is unique to each primitive.
let vertexCache = {};
const vertexCacheLimit = 1000000;
let vertexCacheCount = 0;
let vertexCount = 0;
// All mtl paths seen in the obj
let mtlPaths = [];
// Buffers for face data that spans multiple lines
let lineBuffer = "";
// Used for parsing face data
const faceVertices = [];
const facePositions = [];
const faceUvs = [];
const faceNormals = [];
function clearVertexCache() {
vertexCache = {};
vertexCacheCount = 0;
}
function getName(name) {
return name === "" ? undefined : name;
}
function addNode(name) {
node = new Node();
node.name = getName(name);
nodes.push(node);
addMesh();
}
function addMesh(name) {
mesh = new Mesh();
mesh.name = getName(name);
node.meshes.push(mesh);
addPrimitive();
}
function addPrimitive() {
primitive = new Primitive();
primitive.material = activeMaterial;
mesh.primitives.push(primitive);
// Clear the vertex cache for each new primitive
clearVertexCache();
vertexCount = 0;
}
function reusePrimitive(callback) {
const primitives = mesh.primitives;
const primitivesLength = primitives.length;
for (let i = 0; i < primitivesLength; ++i) {
if (primitives[i].material === activeMaterial) {
if (!defined(callback) || callback(primitives[i])) {
primitive = primitives[i];
clearVertexCache();
vertexCount = primitive.positions.length / 3;
return;
}
}
}
addPrimitive();
}
function useMaterial(name) {
activeMaterial = getName(name);
reusePrimitive();
}
function faceAndPrimitiveMatch(uvs, normals, primitive) {
const faceHasUvs = defined(uvs[0]);
const faceHasNormals = defined(normals[0]);
const primitiveHasUvs = primitive.uvs.length > 0;
const primitiveHasNormals = primitive.normals.length > 0;
return (
primitiveHasUvs === faceHasUvs && primitiveHasNormals === faceHasNormals
);
}
function checkPrimitive(uvs, normals) {
const firstFace = primitive.indices.length === 0;
if (!firstFace && !faceAndPrimitiveMatch(uvs, normals, primitive)) {
reusePrimitive(function (primitive) {
return faceAndPrimitiveMatch(uvs, normals, primitive);
});
}
}
function getIndexFromStart(index, attributeData, components) {
const i = parseInt(index);
if (i < 0) {
// Negative vertex indexes reference the vertices immediately above it
return attributeData.length / components + i;
}
return i - 1;
}
function correctAttributeIndices(
attributeIndices,
attributeData,
components,
) {
const length = attributeIndices.length;
for (let i = 0; i < length; ++i) {
if (attributeIndices[i].length === 0) {
attributeIndices[i] = undefined;
} else {
attributeIndices[i] = getIndexFromStart(
attributeIndices[i],
attributeData,
components,
);
}
}
}
function correctVertices(vertices, positions, uvs, normals) {
const length = vertices.length;
for (let i = 0; i < length; ++i) {
vertices[i] = `${defaultValue(positions[i], "")}/${defaultValue(
uvs[i],
"",
)}/${defaultValue(normals[i], "")}`;
}
}
function createVertex(p, u, n) {
// Positions
if (defined(p) && globalPositions.length > 0) {
if (p * 3 >= globalPositions.length) {
throw new RuntimeError(`Position index ${p} is out of bounds`);
}
const px = globalPositions.get(p * 3);
const py = globalPositions.get(p * 3 + 1);
const pz = globalPositions.get(p * 3 + 2);
primitive.positions.push(px);
primitive.positions.push(py);
primitive.positions.push(pz);
}
// Normals
if (defined(n) && globalNormals.length > 0) {
if (n * 3 >= globalNormals.length) {
throw new RuntimeError(`Normal index ${n} is out of bounds`);
}
const nx = globalNormals.get(n * 3);
const ny = globalNormals.get(n * 3 + 1);
const nz = globalNormals.get(n * 3 + 2);
primitive.normals.push(nx);
primitive.normals.push(ny);
primitive.normals.push(nz);
}
// UVs
if (defined(u) && globalUvs.length > 0) {
if (u * 2 >= globalUvs.length) {
throw new RuntimeError(`UV index ${u} is out of bounds`);
}
const ux = globalUvs.get(u * 2);
const uy = globalUvs.get(u * 2 + 1);
primitive.uvs.push(ux);
primitive.uvs.push(uy);
}
}
function addVertex(v, p, u, n) {
let index = vertexCache[v];
if (!defined(index)) {
index = vertexCount++;
vertexCache[v] = index;
createVertex(p, u, n);
// Prevent the vertex cache from growing too large. As a result of clearing the cache there
// may be some duplicate vertices.
vertexCacheCount++;
if (vertexCacheCount > vertexCacheLimit) {
clearVertexCache();
}
}
return index;
}
function getPosition(index, result) {
const px = globalPositions.get(index * 3);
const py = globalPositions.get(index * 3 + 1);
const pz = globalPositions.get(index * 3 + 2);
return Cartesian3.fromElements(px, py, pz, result);
}
function getNormal(index, result) {
const nx = globalNormals.get(index * 3);
const ny = globalNormals.get(index * 3 + 1);
const nz = globalNormals.get(index * 3 + 2);
return Cartesian3.fromElements(nx, ny, nz, result);
}
const scratch1 = new Cartesian3();
const scratch2 = new Cartesian3();
const scratch3 = new Cartesian3();
const scratch4 = new Cartesian3();
const scratch5 = new Cartesian3();
const scratchCenter = new Cartesian3();
const scratchAxis1 = new Cartesian3();
const scratchAxis2 = new Cartesian3();
const scratchNormal = new Cartesian3();
const scratchPositions = [
new Cartesian3(),
new Cartesian3(),
new Cartesian3(),
new Cartesian3(),
];
const scratchVertexIndices = [];
const scratchPoints = [];
function checkWindingCorrect(
positionIndex1,
positionIndex2,
positionIndex3,
normalIndex,
) {
if (!defined(normalIndex)) {
// If no face normal, we have to assume the winding is correct.
return true;
}
const normal = getNormal(normalIndex, scratchNormal);
const A = getPosition(positionIndex1, scratch1);
const B = getPosition(positionIndex2, scratch2);
const C = getPosition(positionIndex3, scratch3);
const BA = Cartesian3.subtract(B, A, scratch4);
const CA = Cartesian3.subtract(C, A, scratch5);
const cross = Cartesian3.cross(BA, CA, scratch3);
return Cartesian3.dot(normal, cross) >= 0;
}
function addTriangle(index1, index2, index3, correctWinding) {
if (correctWinding) {
primitive.indices.push(index1);
primitive.indices.push(index2);
primitive.indices.push(index3);
} else {
primitive.indices.push(index1);
primitive.indices.push(index3);
primitive.indices.push(index2);
}
}
function addFace(
vertices,
positions,
uvs,
normals,
triangleWindingOrderSanitization,
) {
correctAttributeIndices(positions, globalPositions, 3);
correctAttributeIndices(normals, globalNormals, 3);
correctAttributeIndices(uvs, globalUvs, 2);
correctVertices(vertices, positions, uvs, normals);
checkPrimitive(uvs, faceNormals);
if (vertices.length === 3) {
const isWindingCorrect =
!triangleWindingOrderSanitization ||
checkWindingCorrect(
positions[0],
positions[1],
positions[2],
normals[0],
);
const index1 = addVertex(vertices[0], positions[0], uvs[0], normals[0]);
const index2 = addVertex(vertices[1], positions[1], uvs[1], normals[1]);
const index3 = addVertex(vertices[2], positions[2], uvs[2], normals[2]);
addTriangle(index1, index2, index3, isWindingCorrect);
} else {
// Triangulate if the face is not a triangle
const points = scratchPoints;
const vertexIndices = scratchVertexIndices;
points.length = 0;
vertexIndices.length = 0;
for (let i = 0; i < vertices.length; ++i) {
const index = addVertex(vertices[i], positions[i], uvs[i], normals[i]);
vertexIndices.push(index);
if (i === scratchPositions.length) {
scratchPositions.push(new Cartesian3());
}
points.push(getPosition(positions[i], scratchPositions[i]));
}
const validGeometry =
CoplanarPolygonGeometryLibrary.computeProjectTo2DArguments(
points,
scratchCenter,
scratchAxis1,
scratchAxis2,
);
if (!validGeometry) {
return;
}
const projectPoints =
CoplanarPolygonGeometryLibrary.createProjectPointsTo2DFunction(
scratchCenter,
scratchAxis1,
scratchAxis2,
);
const points2D = projectPoints(points);
const indices = PolygonPipeline.triangulate(points2D);
const isWindingCorrect =
PolygonPipeline.computeWindingOrder2D(points2D) !==
WindingOrder.CLOCKWISE;
for (let i = 0; i < indices.length - 2; i += 3) {
addTriangle(
vertexIndices[indices[i]],
vertexIndices[indices[i + 1]],
vertexIndices[indices[i + 2]],
isWindingCorrect,
);
}
}
}
function parseLine(line) {
line = line.trim();
let result;
if (line.length === 0 || line.charAt(0) === "#") {
// Don't process empty lines or comments
} else if (/^o\s/i.test(line)) {
const objectName = line.substring(2).trim();
addNode(objectName);
} else if (/^g\s/i.test(line)) {
const groupName = line.substring(2).trim();
addMesh(groupName);
} else if (/^usemtl/i.test(line)) {
const materialName = line.substring(7).trim();
useMaterial(materialName);
} else if (/^mtllib/i.test(line)) {
const mtllibLine = line.substring(7).trim();
mtlPaths = mtlPaths.concat(getMtlPaths(mtllibLine));
} else if ((result = vertexPattern.exec(line)) !== null) {
const position = scratchCartesian;
position.x = parseFloat(result[1]);
position.y = parseFloat(result[2]);
position.z = parseFloat(result[3]);
if (defined(axisTransform)) {
Matrix4.multiplyByPoint(axisTransform, position, position);
}
globalPositions.push(position.x);
globalPositions.push(position.y);
globalPositions.push(position.z);
} else if ((result = normalPattern.exec(line)) !== null) {
const normal = Cartesian3.fromElements(
parseFloat(result[1]),
parseFloat(result[2]),
parseFloat(result[3]),
scratchNormal,
);
if (Cartesian3.equals(normal, Cartesian3.ZERO)) {
Cartesian3.clone(Cartesian3.UNIT_Z, normal);
} else {
Cartesian3.normalize(normal, normal);
}
if (defined(axisTransform)) {
Matrix4.multiplyByPointAsVector(axisTransform, normal, normal);
}
globalNormals.push(normal.x);
globalNormals.push(normal.y);
globalNormals.push(normal.z);
} else if ((result = uvPattern.exec(line)) !== null) {
globalUvs.push(parseFloat(result[1]));
globalUvs.push(1.0 - parseFloat(result[2])); // Flip y so 0.0 is the bottom of the image
} else {
// face line or invalid line
// Because face lines can contain n vertices, we use a line buffer in case the face data spans multiple lines.
// If there's a line continuation don't create face yet
if (line.slice(-1) === "\\") {
lineBuffer += line.substring(0, line.length - 1);
return;
}
lineBuffer += line;
if (lineBuffer.substring(0, 2) === "f ") {
while ((result = facePattern.exec(lineBuffer)) !== null) {
faceVertices.push(result[0]);
facePositions.push(result[1]);
faceUvs.push(result[2]);
faceNormals.push(result[3]);
}
if (faceVertices.length > 2) {
addFace(
faceVertices,
facePositions,
faceUvs,
faceNormals,
options.triangleWindingOrderSanitization,
);
}
faceVertices.length = 0;
facePositions.length = 0;
faceNormals.length = 0;
faceUvs.length = 0;
}
lineBuffer = "";
}
}
// Create a default node in case there are no o/g/usemtl lines in the obj
addNode();
// Parse the obj file
return readLines(objPath, parseLine).then(function () {
// Unload resources
globalPositions = undefined;
globalNormals = undefined;
globalUvs = undefined;
// Load materials and textures
return finishLoading(
nodes,
mtlPaths,
objPath,
defined(activeMaterial),
options,
);
});
}
function getMtlPaths(mtllibLine) {
// Handle paths with spaces. E.g. mtllib my material file.mtl
const mtlPaths = [];
//Remove double quotes around the mtl file if it exists
mtllibLine = mtllibLine.replace(/^"(.+)"$/, "$1");
const splits = mtllibLine.split(" ");
const length = splits.length;
let startIndex = 0;
for (let i = 0; i < length; ++i) {
if (path.extname(splits[i]) !== ".mtl") {
continue;
}
const mtlPath = splits.slice(startIndex, i + 1).join(" ");
mtlPaths.push(mtlPath);
startIndex = i + 1;
}
return mtlPaths;
}
function finishLoading(nodes, mtlPaths, objPath, usesMaterials, options) {
nodes = cleanNodes(nodes);
if (nodes.length === 0) {
throw new RuntimeError(`${objPath} does not have any geometry data`);
}
const name = path.basename(objPath, path.extname(objPath));
return loadMtls(mtlPaths, objPath, options).then(function (materials) {
if (materials.length > 0 && !usesMaterials) {
assignDefaultMaterial(nodes, materials, usesMaterials);
}
assignUnnamedMaterial(nodes, materials);
return {
nodes: nodes,
materials: materials,
name: name,
};
});
}
function normalizeMtlPath(mtlPath, objDirectory) {
mtlPath = mtlPath.replace(/\\/g, "/");
return path.normalize(path.resolve(objDirectory, mtlPath));
}
function loadMtls(mtlPaths, objPath, options) {
const objDirectory = path.dirname(objPath);
let materials = [];
// Remove duplicates
mtlPaths = mtlPaths.filter(function (value, index, self) {
return self.indexOf(value) === index;
});
return Promise.map(
mtlPaths,
function (mtlPath) {
mtlPath = normalizeMtlPath(mtlPath, objDirectory);
const shallowPath = path.join(objDirectory, path.basename(mtlPath));
if (options.secure && outsideDirectory(mtlPath, objDirectory)) {
// Try looking for the .mtl in the same directory as the obj
options.logger(
"The material file is outside of the obj directory and the secure flag is true. Attempting to read the material file from within the obj directory instead.",
);
return loadMtl(shallowPath, options)
.then(function (materialsInMtl) {
materials = materials.concat(materialsInMtl);
})
.catch(function (error) {
options.logger(error.message);
options.logger(
`Could not read material file at ${shallowPath}. Using default material instead.`,
);
});
}
return loadMtl(mtlPath, options)
.catch(function (error) {
// Try looking for the .mtl in the same directory as the obj
options.logger(error.message);
options.logger(
`Could not read material file at ${mtlPath}. Attempting to read the material file from within the obj directory instead.`,
);
return loadMtl(shallowPath, options);
})
.then(function (materialsInMtl) {
materials = materials.concat(materialsInMtl);
})
.catch(function (error) {
options.logger(error.message);
options.logger(
`Could not read material file at ${shallowPath}. Using default material instead.`,
);
});
},
{ concurrency: 10 },
).then(function () {
return materials;
});
}
function assignDefaultMaterial(nodes, materials) {
const defaultMaterial = materials[0].name;
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
const meshes = nodes[i].meshes;
const meshesLength = meshes.length;
for (let j = 0; j < meshesLength; ++j) {
const primitives = meshes[j].primitives;
const primitivesLength = primitives.length;
for (let k = 0; k < primitivesLength; ++k) {
const primitive = primitives[k];
primitive.material = defaultValue(primitive.material, defaultMaterial);
}
}
}
}
function assignUnnamedMaterial(nodes, materials) {
// If there is a material that doesn't have a name, assign that
// material to any primitives whose material is undefined.
const unnamedMaterial = materials.find(function (material) {
return material.name.length === 0;
});
if (!defined(unnamedMaterial)) {
return;
}
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
const meshes = nodes[i].meshes;
const meshesLength = meshes.length;
for (let j = 0; j < meshesLength; ++j) {
const primitives = meshes[j].primitives;
const primitivesLength = primitives.length;
for (let k = 0; k < primitivesLength; ++k) {
const primitive = primitives[k];
if (!defined(primitive.material)) {
primitive.material = unnamedMaterial.name;
}
}
}
}
}
function removeEmptyMeshes(meshes) {
return meshes.filter(function (mesh) {
// Remove empty primitives
mesh.primitives = mesh.primitives.filter(function (primitive) {
return primitive.indices.length > 0 && primitive.positions.length > 0;
});
// Valid meshes must have at least one primitive
return mesh.primitives.length > 0;
});
}
function meshesHaveNames(meshes) {
const meshesLength = meshes.length;
for (let i = 0; i < meshesLength; ++i) {
if (defined(meshes[i].name)) {
return true;
}
}
return false;
}
function removeEmptyNodes(nodes) {
const final = [];
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
const node = nodes[i];
const meshes = removeEmptyMeshes(node.meshes);
if (meshes.length === 0) {
continue;
}
node.meshes = meshes;
if (!defined(node.name) && meshesHaveNames(meshes)) {
// If the obj has groups (g) but not object groups (o) then convert meshes to nodes
const meshesLength = meshes.length;
for (let j = 0; j < meshesLength; ++j) {
const mesh = meshes[j];
const convertedNode = new Node();
convertedNode.name = mesh.name;
convertedNode.meshes = [mesh];
final.push(convertedNode);
}
} else {
final.push(node);
}
}
return final;
}
function setDefaultNames(items, defaultName, usedNames) {
const itemsLength = items.length;
for (let i = 0; i < itemsLength; ++i) {
const item = items[i];
let name = defaultValue(item.name, defaultName);
const occurrences = usedNames[name];
if (defined(occurrences)) {
usedNames[name]++;
name = `${name}_${occurrences}`;
} else {
usedNames[name] = 1;
}
item.name = name;
}
}
function setDefaults(nodes) {
const usedNames = {};
setDefaultNames(nodes, "Node", usedNames);
const nodesLength = nodes.length;
for (let i = 0; i < nodesLength; ++i) {
const node = nodes[i];
setDefaultNames(node.meshes, `${node.name}-Mesh`, usedNames);
}
}
function cleanNodes(nodes) {
nodes = removeEmptyNodes(nodes);
setDefaults(nodes);
return nodes;
}
function getAxisTransform(inputUpAxis, outputUpAxis) {
if (inputUpAxis === "X" && outputUpAxis === "Y") {
return Axis.X_UP_TO_Y_UP;
} else if (inputUpAxis === "X" && outputUpAxis === "Z") {
return Axis.X_UP_TO_Z_UP;
} else if (inputUpAxis === "Y" && outputUpAxis === "X") {
return Axis.Y_UP_TO_X_UP;
} else if (inputUpAxis === "Y" && outputUpAxis === "Z") {
return Axis.Y_UP_TO_Z_UP;
} else if (inputUpAxis === "Z" && outputUpAxis === "X") {
return Axis.Z_UP_TO_X_UP;
} else if (inputUpAxis === "Z" && outputUpAxis === "Y") {
return Axis.Z_UP_TO_Y_UP;
}
}
+133
View File
@@ -0,0 +1,133 @@
"use strict";
const Cesium = require("cesium");
const fsExtra = require("fs-extra");
const jpeg = require("jpeg-js");
const path = require("path");
const PNG = require("pngjs").PNG;
const Promise = require("bluebird");
const Texture = require("./Texture");
const defaultValue = (a, b) => a ?? b;
const defined = Cesium.defined;
module.exports = loadTexture;
/**
* Load a texture file.
*
* @param {String} texturePath Path to the texture file.
* @param {Object} [options] An object with the following properties:
* @param {Boolean} [options.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel.
* @param {Boolean} [options.decode=false] Whether to decode the texture.
* @param {Boolean} [options.keepSource=false] Whether to keep the source image contents in memory.
* @returns {Promise} A promise resolving to a Texture object.
*
* @private
*/
function loadTexture(texturePath, options) {
options = defaultValue(options, {});
options.checkTransparency = defaultValue(options.checkTransparency, false);
options.decode = defaultValue(options.decode, false);
options.keepSource = defaultValue(options.keepSource, false);
return fsExtra.readFile(texturePath).then(function (source) {
const name = path.basename(texturePath, path.extname(texturePath));
const extension = path.extname(texturePath).toLowerCase();
const texture = new Texture();
texture.source = source;
texture.name = name;
texture.extension = extension;
texture.path = texturePath;
let decodePromise;
if (extension === ".png") {
decodePromise = decodePng(texture, options);
} else if (extension === ".jpg" || extension === ".jpeg") {
decodePromise = decodeJpeg(texture, options);
}
if (defined(decodePromise)) {
return decodePromise.then(function () {
return texture;
});
}
return texture;
});
}
function hasTransparency(pixels) {
const pixelsLength = pixels.length / 4;
for (let i = 0; i < pixelsLength; ++i) {
if (pixels[i * 4 + 3] < 255) {
return true;
}
}
return false;
}
function getChannels(colorType) {
switch (colorType) {
case 0: // greyscale
return 1;
case 2: // RGB
return 3;
case 4: // greyscale + alpha
return 2;
case 6: // RGB + alpha
return 4;
default:
return 3;
}
}
function parsePng(data) {
return new Promise(function (resolve, reject) {
new PNG().parse(data, function (error, decodedResults) {
if (defined(error)) {
reject(error);
return;
}
resolve(decodedResults);
});
});
}
function decodePng(texture, options) {
// Color type is encoded in the 25th bit of the png
const source = texture.source;
const colorType = source[25];
const channels = getChannels(colorType);
const checkTransparency = channels === 4 && options.checkTransparency;
const decode = options.decode || checkTransparency;
if (decode) {
return parsePng(source).then(function (decodedResults) {
if (options.checkTransparency) {
texture.transparent = hasTransparency(decodedResults.data);
}
if (options.decode) {
texture.pixels = decodedResults.data;
texture.width = decodedResults.width;
texture.height = decodedResults.height;
if (!options.keepSource) {
texture.source = undefined; // Unload resources
}
}
});
}
}
function decodeJpeg(texture, options) {
if (options.decode) {
const source = texture.source;
const decodedResults = jpeg.decode(source);
texture.pixels = decodedResults.data;
texture.width = decodedResults.width;
texture.height = decodedResults.height;
if (!options.keepSource) {
texture.source = undefined; // Unload resources
}
}
}
+257
View File
@@ -0,0 +1,257 @@
"use strict";
const Cesium = require("cesium");
const fsExtra = require("fs-extra");
const path = require("path");
const createGltf = require("./createGltf");
const loadObj = require("./loadObj");
const writeGltf = require("./writeGltf");
const defaultValue = (a, b) => a ?? b;
const defined = Cesium.defined;
const DeveloperError = Cesium.DeveloperError;
module.exports = obj2gltf;
/**
* Converts an obj file to a glTF or glb.
*
* @param {String} objPath Path to the obj file.
* @param {Object} [options] An object with the following properties:
* @param {Boolean} [options.binary=false] Convert to binary glTF.
* @param {Boolean} [options.separate=false] Write out separate buffer files and textures instead of embedding them in the glTF.
* @param {Boolean} [options.separateTextures=false] Write out separate textures only.
* @param {Boolean} [options.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel.
* @param {Boolean} [options.secure=false] Prevent the converter from reading textures or mtl files outside of the input obj directory.
* @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of the metallic-roughness texture.
* @param {Boolean} [options.metallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.
* @param {Boolean} [options.specularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension.
* @param {Boolean} [options.unlit=false] The glTF will be saved with the KHR_materials_unlit extension.
* @param {Object} [options.overridingTextures] An object containing texture paths that override textures defined in the .mtl file. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material.
* @param {String} [options.overridingTextures.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material.
* @param {String} [options.overridingTextures.specularGlossinessTexture] Path to the specular-glossiness texture, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension.
* @param {String} [options.overridingTextures.occlusionTexture] Path to the occlusion texture. Ignored if metallicRoughnessOcclusionTexture is also set.
* @param {String} [options.overridingTextures.normalTexture] Path to the normal texture.
* @param {String} [options.overridingTextures.baseColorTexture] Path to the baseColor/diffuse texture.
* @param {String} [options.overridingTextures.emissiveTexture] Path to the emissive texture.
* @param {String} [options.overridingTextures.alphaTexture] Path to the alpha texture.
* @param {String} [options.inputUpAxis='Y'] Up axis of the obj. Choices are 'X', 'Y', and 'Z'.
* @param {String} [options.outputUpAxis='Y'] Up axis of the converted glTF. Choices are 'X', 'Y', and 'Z'.
* @param {String} [options.triangleWindingOrderSanitization=false] Apply triangle winding order sanitization.
* @param {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log.
* @param {Writer} [options.writer] A callback function that writes files that are saved as separate resources.
* @param {String} [options.outputDirectory] Output directory for writing separate resources when options.writer is not defined.
* @param {Boolean} [options.doubleSidedMaterial=false] Allows materials to be double sided.
* @return {Promise} A promise that resolves to the glTF JSON or glb buffer.
*/
function obj2gltf(objPath, options) {
const defaults = obj2gltf.defaults;
options = defaultValue(options, {});
options.binary = defaultValue(options.binary, defaults.binary);
options.separate = defaultValue(options.separate, defaults.separate);
options.separateTextures =
defaultValue(options.separateTextures, defaults.separateTextures) ||
options.separate;
options.checkTransparency = defaultValue(
options.checkTransparency,
defaults.checkTransparency,
);
options.doubleSidedMaterial = defaultValue(
options.doubleSidedMaterial,
defaults.doubleSidedMaterial,
);
options.secure = defaultValue(options.secure, defaults.secure);
options.packOcclusion = defaultValue(
options.packOcclusion,
defaults.packOcclusion,
);
options.metallicRoughness = defaultValue(
options.metallicRoughness,
defaults.metallicRoughness,
);
options.specularGlossiness = defaultValue(
options.specularGlossiness,
defaults.specularGlossiness,
);
options.unlit = defaultValue(options.unlit, defaults.unlit);
options.overridingTextures = defaultValue(
options.overridingTextures,
Cesium.Frozen.EMPTY_OBJECT,
);
options.logger = defaultValue(options.logger, getDefaultLogger());
options.writer = defaultValue(
options.writer,
getDefaultWriter(options.outputDirectory),
);
options.inputUpAxis = defaultValue(options.inputUpAxis, defaults.inputUpAxis);
options.outputUpAxis = defaultValue(
options.outputUpAxis,
defaults.outputUpAxis,
);
options.triangleWindingOrderSanitization = defaultValue(
options.triangleWindingOrderSanitization,
defaults.triangleWindingOrderSanitization,
);
if (!defined(objPath)) {
throw new DeveloperError("objPath is required");
}
if (options.separateTextures && !defined(options.writer)) {
throw new DeveloperError(
"Either options.writer or options.outputDirectory must be defined when writing separate resources.",
);
}
if (
options.metallicRoughness + options.specularGlossiness + options.unlit >
1
) {
throw new DeveloperError(
"Only one material type may be set from [metallicRoughness, specularGlossiness, unlit].",
);
}
if (
defined(options.overridingTextures.metallicRoughnessOcclusionTexture) &&
defined(options.overridingTextures.specularGlossinessTexture)
) {
throw new DeveloperError(
"metallicRoughnessOcclusionTexture and specularGlossinessTexture cannot both be defined.",
);
}
if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture)) {
options.metallicRoughness = true;
options.specularGlossiness = false;
options.packOcclusion = true;
}
if (defined(options.overridingTextures.specularGlossinessTexture)) {
options.metallicRoughness = false;
options.specularGlossiness = true;
}
return loadObj(objPath, options)
.then(function (objData) {
return createGltf(objData, options);
})
.then(function (gltf) {
return writeGltf(gltf, options);
});
}
function getDefaultLogger() {
return function (message) {
console.log(message);
};
}
function getDefaultWriter(outputDirectory) {
if (defined(outputDirectory)) {
return function (file, data) {
const outputFile = path.join(outputDirectory, file);
return fsExtra.outputFile(outputFile, data);
};
}
}
/**
* Default values that will be used when calling obj2gltf(options) unless specified in the options object.
*/
obj2gltf.defaults = {
/**
* Gets or sets whether the converter will return a glb.
* @type Boolean
* @default false
*/
binary: false,
/**
* Gets or sets whether to write out separate buffer and texture,
* shader files, and textures instead of embedding them in the glTF.
* @type Boolean
* @default false
*/
separate: false,
/**
* Gets or sets whether to write out separate textures only.
* @type Boolean
* @default false
*/
separateTextures: false,
/**
* Gets or sets whether the converter will do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel.
* @type Boolean
* @default false
*/
checkTransparency: false,
/**
* Gets and sets whether a material will be doubleSided or not
* @type Boolean
* @default false
*/
doubleSidedMaterial: false,
/**
* Gets or sets whether the source model can reference paths outside of its directory.
* @type Boolean
* @default false
*/
secure: false,
/**
* Gets or sets whether to pack the occlusion texture in the red channel of the metallic-roughness texture.
* @type Boolean
* @default false
*/
packOcclusion: false,
/**
* Gets or sets whether rhe values in the .mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.
* @type Boolean
* @default false
*/
metallicRoughness: false,
/**
* Gets or sets whether the values in the .mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension.
* @type Boolean
* @default false
*/
specularGlossiness: false,
/**
* Gets or sets whether the glTF will be saved with the KHR_materials_unlit extension.
* @type Boolean
* @default false
*/
unlit: false,
/**
* Gets or sets the up axis of the obj.
* @type String
* @default 'Y'
*/
inputUpAxis: "Y",
/**
* Gets or sets the up axis of the converted glTF.
* @type String
* @default 'Y'
*/
outputUpAxis: "Y",
/**
* Gets or sets whether triangle winding order sanitization will be applied.
* @type Boolean
* @default false
*/
windingOrderSanitization: false,
};
/**
* A callback function that logs messages.
* @callback Logger
*
* @param {String} message The message to log.
*/
/**
* A callback function that writes files that are saved as separate resources.
* @callback Writer
*
* @param {String} file The relative path of the file.
* @param {Buffer} data The file data to write.
* @returns {Promise} A promise that resolves when the file is written.
*/
+17
View File
@@ -0,0 +1,17 @@
"use strict";
const path = require("path");
module.exports = outsideDirectory;
/**
* Checks if a file is outside of a directory.
*
* @param {String} file Path to the file.
* @param {String} directory Path to the directory.
* @returns {Boolean} Whether the file is outside of the directory.
*
* @private
*/
function outsideDirectory(file, directory) {
return path.relative(directory, file).indexOf("..") === 0;
}
+40
View File
@@ -0,0 +1,40 @@
"use strict";
const fsExtra = require("fs-extra");
const Promise = require("bluebird");
const readline = require("readline");
const events = require("events");
module.exports = readLines;
/**
* Read a file line-by-line.
*
* @param {String} path Path to the file.
* @param {Function} callback Function to call when reading each line.
* @returns {Promise} A promise when the reader is finished.
*
* @private
*/
function readLines(path, callback) {
const stream = fsExtra.createReadStream(path);
return events.once(stream, "open").then(function () {
return new Promise(function (resolve, reject) {
stream.on("error", reject);
stream.on("end", resolve);
const lineReader = readline.createInterface({
input: stream,
});
const callbackWrapper = function (line) {
try {
callback(line);
} catch (error) {
reject(error);
}
};
lineReader.on("line", callbackWrapper);
});
});
}
+206
View File
@@ -0,0 +1,206 @@
"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;
}