forge-convert-utils
Version:
Tools for converting Autodesk Forge file formats.
765 lines (689 loc) • 32.5 kB
text/typescript
import * as path from 'path';
import crypto from 'crypto';
import * as fse from 'fs-extra';
import * as gltf from './schema';
import { isUndefined, isNullOrUndefined } from 'util';
import { ImagePlaceholder } from '../common/image-placeholders';
import * as IMF from '../common/intermediate-format';
const MaxBufferSize = 5 << 20;
const DefaultMaterial: gltf.MaterialPbrMetallicRoughness = {
pbrMetallicRoughness: {
baseColorFactor: [0.25, 0.25, 0.25, 1.0],
metallicFactor: 0.0,
roughnessFactor: 0.5
}
};
export interface IWriterOptions {
maxBufferSize?: number; /** Approx. size limit (in bytes) of binary buffers with mesh data (5 << 20 by default) */
ignoreMeshGeometry?: boolean; /** Don't output mesh geometry */
ignoreLineGeometry?: boolean; /** Don't output line geometry */
ignorePointGeometry?: boolean; /** Don't output point geometry */
deduplicate?: boolean; /** Find and remove mesh geometry duplicates (increases the processing time) */
skipUnusedUvs?: boolean; /** Skip unused tex coordinates. */
center?: boolean; /** Move the model to origin. */
log?: (msg: string) => void; /** Optional logging function. */
filter?: (dbid: number, fragid: number) => boolean;
}
function hasTextures(material: IMF.Material | null): boolean {
return !!(material?.maps?.diffuse);
}
interface IWriterStats {
materialsDeduplicated: number;
meshesDeduplicated: number;
accessorsDeduplicated: number;
bufferViewsDeduplicated: number;
}
/**
* Utility class for serializing parsed 3D content to local file system as glTF (2.0).
*/
export class Writer {
protected options: Required<IWriterOptions>;
protected baseDir: string;
protected manifest: gltf.GlTf;
protected bufferStream: fse.WriteStream | null;
protected bufferSize: number;
protected bufferViewCache = new Map<string, gltf.BufferView>(); // Cache of existing buffer views, indexed by hash of the binary data they point to
protected meshHashes = new Map<string, number>(); // List of hashes of existing gltf.Mesh objects, used for deduplication
protected bufferViewHashes = new Map<string, number>(); // List of hashes of existing gltf.BufferView objects, used for deduplication
protected accessorHashes = new Map<string, number>(); // List of hashes of existing gltf.Accessor objects, used for deduplication
protected pendingTasks: Promise<void>[] = [];
protected activeSvfMaterials: number[]; // List of SVF material IDs that are actually used during the glTF serialization (used to avoid serializing unused materials)
protected stats: IWriterStats = {
materialsDeduplicated: 0,
meshesDeduplicated: 0,
accessorsDeduplicated: 0,
bufferViewsDeduplicated: 0
};
/**
* Initializes the writer.
* @param {IWriterOptions} [options={}] Additional writer options.
*/
constructor(options: IWriterOptions = {}) {
this.options = {
maxBufferSize: isNullOrUndefined(options.maxBufferSize) ? MaxBufferSize : options.maxBufferSize,
ignoreMeshGeometry: !!options.ignoreMeshGeometry,
ignoreLineGeometry: !!options.ignoreLineGeometry,
ignorePointGeometry: !!options.ignorePointGeometry,
deduplicate: !!options.deduplicate,
skipUnusedUvs: !!options.skipUnusedUvs,
center: !!options.center,
log: (options && options.log) || function (msg: string) {},
filter: options && options.filter || ((dbid: number, fragid: number) => true)
};
// All these properties will be properly initialized in the 'reset' call
this.manifest = {} as gltf.GlTf;
this.bufferStream = null;
this.bufferSize = 0;
this.baseDir = '';
this.activeSvfMaterials = [];
}
/**
* Outputs scene into glTF.
* @async
* @param {IMF.IScene} imf Complete scene in intermediate, in-memory format.
* @param {string} outputDir Path to output folder.
*/
async write(imf: IMF.IScene, outputDir: string) {
this.reset(outputDir);
const scene = this.createScene(imf);
const scenes = this.manifest.scenes as gltf.Scene[];
scenes.push(scene);
if (this.bufferStream) {
const stream = this.bufferStream as fse.WriteStream;
this.pendingTasks.push(new Promise((resolve, reject) => {
stream.on('finish', resolve);
}));
this.bufferStream.close();
this.bufferStream = null;
this.bufferSize = 0;
}
await Promise.all(this.pendingTasks);
// Remove empty attributes textures or images to avoid errors in glTF validation
if (this.manifest.textures && this.manifest.textures.length === 0)
delete this.manifest.textures;
if (this.manifest.images && this.manifest.images.length === 0)
delete this.manifest.images;
const gltfPath = path.join(this.baseDir, 'output.gltf');
this.serializeManifest(this.manifest, gltfPath);
this.options.log(`Closing gltf output: done`);
this.options.log(`Stats: ${JSON.stringify(this.stats)}`);
await this.postprocess(imf, gltfPath);
}
protected reset(outputDir: string) {
this.baseDir = outputDir;
this.manifest = {
asset: {
version: '2.0',
generator: 'forge-convert-utils',
copyright: '2019 (c) Autodesk'
},
extensionsUsed: [
"KHR_texture_transform"
],
buffers: [],
bufferViews: [],
accessors: [],
meshes: [],
materials: [],
nodes: [],
scenes: [],
textures: [],
images: [],
scene: 0
};
this.bufferStream = null;
this.bufferSize = 0;
this.bufferViewCache.clear();
this.meshHashes = new Map<string, number>();
this.bufferViewHashes = new Map<string, number>();
this.accessorHashes = new Map<string, number>();
this.pendingTasks = [];
this.activeSvfMaterials = [];
this.stats = {
materialsDeduplicated: 0,
meshesDeduplicated: 0,
accessorsDeduplicated: 0,
bufferViewsDeduplicated: 0
};
}
protected async postprocess(imf: IMF.IScene, gltfPath: string) {}
protected serializeManifest(manifest: gltf.GlTf, outputPath: string) {
fse.writeFileSync(outputPath, JSON.stringify(manifest, null, 4));
}
protected createScene(imf: IMF.IScene): gltf.Scene {
fse.ensureDirSync(this.baseDir);
let scene: gltf.Scene = {
nodes: []
};
const manifestNodes = this.manifest.nodes as gltf.Node[];
const manifestMaterials = this.manifest.materials as gltf.MaterialPbrMetallicRoughness[];
const rootNode: gltf.Node = { children: [] }; // Root node with transform to glTF coordinate system
const xformNode: gltf.Node = { children: [] }; // Transform node with additional global transform (e.g., moving model to origin)
(scene.nodes as number[]).push(manifestNodes.push(rootNode) - 1);
(rootNode.children as number[]).push(manifestNodes.push(xformNode) - 1);
// Setup transformation to glTF coordinate system
const metadata = imf.getMetadata();
if (metadata['world up vector'] && metadata['world front vector'] && metadata['distance unit']) {
const up = metadata['world up vector'].XYZ;
const front = metadata['world front vector'].XYZ;
const distanceUnit = metadata['distance unit'].value;
if (up && front && distanceUnit) {
const left = [
up[1] * front[2] - up[2] * front[1],
up[2] * front[0] - up[0] * front[2],
up[0] * front[1] - up[1] * front[0]
];
if (left[0] * left[0] + left[1] * left[1] + left[2] * left[2] > 0.0) {
let scale = 1.0;
switch (distanceUnit) {
case 'centimeter':
case 'cm':
scale = 0.01;
break;
case 'millimeter':
case 'mm':
scale = 0.001;
break;
case 'foot':
case 'ft':
scale = 0.3048;
break;
case 'inch':
case 'in':
scale = 0.0254;
break;
default: // "meter" / "m"
scale = 1.0;
}
rootNode.matrix = [
left[0] * scale, up[0] * scale, front[0] * scale, 0,
left[1] * scale, up[1] * scale, front[1] * scale, 0,
left[2] * scale, up[2] * scale, front[2] * scale, 0,
0, 0, 0, 1
];
} else {
console.warn('Could not compute world matrix, leaving it as identity...');
}
}
}
// Setup translation to origin when enabled
if (metadata['world bounding box'] && this.options.center) {
const boundsMin = metadata['world bounding box'].minXYZ;
const boundsMax = metadata['world bounding box'].maxXYZ;
if (boundsMin && boundsMax) {
let translation = [
-0.5 * (boundsMin[0] + boundsMax[0]),
-0.5 * (boundsMin[1] + boundsMax[1]),
-0.5 * (boundsMin[2] + boundsMax[2])
];
xformNode.matrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
translation[0], translation[1], translation[2], 1
];
}
}
const nodeIndices = (xformNode.children as number[]);
this.options.log(`Writing scene nodes...`);
const { filter } = this.options;
for (let i = 0, len = imf.getNodeCount(); i < len; i++) {
const fragment = imf.getNode(i);
// Currently we only support flat lists of objects, no hierarchies
if (fragment.kind !== IMF.NodeKind.Object) {
continue;
}
if (!filter(fragment.dbid, i)) {
continue;
}
const material = imf.getMaterial(fragment.material);
// Only output UVs if there are any textures or if the user specifically asked not to skip unused UVs
const outputUvs = hasTextures(material) || !this.options.skipUnusedUvs;
const node = this.createNode(fragment, imf, outputUvs);
// Only output nodes that have a mesh
if (!isUndefined(node.mesh)) {
nodeIndices.push(manifestNodes.push(node) - 1);
}
}
this.options.log(`Writing materials...`);
if (this.options.deduplicate) {
const hashes: string[] = [];
const newMaterialIndices = new Uint16Array(imf.getMaterialCount());
for (const [i, activeMaterialID] of this.activeSvfMaterials.entries()) {
const material = imf.getMaterial(activeMaterialID);
const hash = this.computeMaterialHash(material);
const match = hashes.indexOf(hash);
if (match === -1) {
// If this is a first occurrence of the hash in the array, output a new material
newMaterialIndices[i] = manifestMaterials.length;
manifestMaterials.push(this.createMaterial(material, imf));
hashes.push(hash);
} else {
// Otherwise skip the material, and record an index to the first match below
this.options.log(`Skipping a duplicate material (hash: ${hash})`);
newMaterialIndices[i] = match;
this.stats.materialsDeduplicated++;
}
}
// Update material indices in all mesh primitives
for (const mesh of (this.manifest.meshes as gltf.Mesh[])) {
for (const primitive of mesh.primitives) {
if (!isUndefined(primitive.material)) {
primitive.material = newMaterialIndices[primitive.material];
}
}
}
} else {
for (const activeMaterialID of this.activeSvfMaterials) {
const material = imf.getMaterial(activeMaterialID);
const mat = this.createMaterial(material, imf);
manifestMaterials.push(mat);
}
}
this.options.log(`Writing scene: done`);
return scene;
}
protected createNode(fragment: IMF.IObjectNode, imf: IMF.IScene, outputUvs: boolean): gltf.Node {
let node: gltf.Node = {
name: fragment.dbid.toString()
};
if (fragment.transform) {
switch (fragment.transform.kind) {
case IMF.TransformKind.Matrix:
node.matrix = fragment.transform.elements;
break;
case IMF.TransformKind.Decomposed:
if (fragment.transform.scale) {
const s = fragment.transform.scale;
node.scale = [s.x, s.y, s.z];
}
if (fragment.transform.rotation) {
const r = fragment.transform.rotation;
node.rotation = [r.x, r.y, r.z, r.w];
}
if (fragment.transform.translation) {
const t = fragment.transform.translation;
node.translation = [t.x, t.y, t.z];
}
break;
}
}
const geometry = imf.getGeometry(fragment.geometry);
let mesh: gltf.Mesh | undefined = undefined;
switch (geometry.kind) {
case IMF.GeometryKind.Mesh:
mesh = this.createMeshGeometry(geometry, imf, outputUvs);
break;
case IMF.GeometryKind.Lines:
mesh = this.createLineGeometry(geometry, imf);
break;
case IMF.GeometryKind.Points:
mesh = this.createPointGeometry(geometry, imf);
break;
case IMF.GeometryKind.Empty:
console.warn('Could not find mesh for fragment', fragment);
break;
}
if (mesh && mesh.primitives.length > 0) {
let materialID = this.activeSvfMaterials.indexOf(fragment.material);
if (materialID === -1) {
materialID = this.activeSvfMaterials.length;
this.activeSvfMaterials.push(fragment.material);
}
for (const primitive of mesh.primitives) {
primitive.material = materialID;
}
node.mesh = this.addMesh(mesh);
}
return node;
}
protected addMesh(mesh: gltf.Mesh): number {
const meshes = this.manifest.meshes as gltf.Mesh[];
const hash = this.computeMeshHash(mesh);
const match = this.options.deduplicate ? this.meshHashes.get(hash) : undefined;
if (match !== undefined) {
this.options.log(`Skipping a duplicate mesh (${hash})`);
this.stats.meshesDeduplicated++;
return match;
} else {
if (this.options.deduplicate) {
this.meshHashes.set(hash, this.meshHashes.size);
}
return meshes.push(mesh) - 1;
}
}
protected createMeshGeometry(geometry: IMF.IMeshGeometry, imf: IMF.IScene, outputUvs: boolean): gltf.Mesh {
let mesh: gltf.Mesh = {
primitives: []
};
if (this.options.ignoreMeshGeometry) {
return mesh;
}
// Output index buffer
const indices = geometry.getIndices();
const indexBufferView = this.createBufferView(Buffer.from(indices.buffer, indices.byteOffset, indices.byteLength));
const indexBufferViewID = this.addBufferView(indexBufferView);
const indexAccessor = this.createAccessor(indexBufferViewID, 5123, indexBufferView.byteLength / 2, 'SCALAR');
const indexAccessorID = this.addAccessor(indexAccessor);
// Output vertex buffer
const vertices = geometry.getVertices();
const positionBounds = this.computeBoundsVec3(vertices); // Compute bounds manually, just in case
const positionBufferView = this.createBufferView(Buffer.from(vertices.buffer, vertices.byteOffset, vertices.byteLength));
const positionBufferViewID = this.addBufferView(positionBufferView);
const positionAccessor = this.createAccessor(positionBufferViewID, 5126, positionBufferView.byteLength / 4 / 3, 'VEC3', positionBounds.min, positionBounds.max/*[fragmesh.min.x, fragmesh.min.y, fragmesh.min.z], [fragmesh.max.x, fragmesh.max.y, fragmesh.max.z]*/);
const positionAccessorID = this.addAccessor(positionAccessor);
// Output normals buffer
let normalAccessorID: number | undefined = undefined;
const normals = geometry.getNormals();
if (normals) {
const normalBufferView = this.createBufferView(Buffer.from(normals.buffer, normals.byteOffset, normals.byteLength));
const normalBufferViewID = this.addBufferView(normalBufferView);
const normalAccessor = this.createAccessor(normalBufferViewID, 5126, normalBufferView.byteLength / 4 / 3, 'VEC3');
normalAccessorID = this.addAccessor(normalAccessor);
}
// Output color buffer
let colorAccessorID: number | undefined = undefined;
const colors = geometry.getColors();
if (colors) {
const colorBufferView = this.createBufferView(Buffer.from(colors.buffer, colors.byteOffset, colors.byteLength));
const colorBufferViewID = this.addBufferView(colorBufferView);
const colorAccessor = this.createAccessor(colorBufferViewID, 5126, colorBufferView.byteLength / 4 / 4, 'VEC4');
colorAccessorID = this.addAccessor(colorAccessor);
}
// Output UV buffers
let uvAccessorID: number | undefined = undefined;
if (geometry.getUvChannelCount() > 0 && outputUvs) {
const uvs = geometry.getUvs(0);
const uvBufferView = this.createBufferView(Buffer.from(uvs.buffer, uvs.byteOffset, uvs.byteLength));
const uvBufferViewID = this.addBufferView(uvBufferView);
const uvAccessor = this.createAccessor(uvBufferViewID, 5126, uvBufferView.byteLength / 4 / 2, 'VEC2');
uvAccessorID = this.addAccessor(uvAccessor);
}
mesh.primitives.push({
mode: 4,
attributes: {
POSITION: positionAccessorID,
},
indices: indexAccessorID
});
if (!isUndefined(normalAccessorID)) {
mesh.primitives[0].attributes.NORMAL = normalAccessorID;
}
if (!isUndefined(colorAccessorID)) {
mesh.primitives[0].attributes.COLOR_0 = colorAccessorID;
}
if (!isUndefined(uvAccessorID)) {
mesh.primitives[0].attributes.TEXCOORD_0 = uvAccessorID;
}
return mesh;
}
protected createLineGeometry(geometry: IMF.ILineGeometry, imf: IMF.IScene): gltf.Mesh {
let mesh: gltf.Mesh = {
primitives: []
};
if (this.options.ignoreLineGeometry) {
return mesh;
}
// Output index buffer
const indices = geometry.getIndices();
const indexBufferView = this.createBufferView(Buffer.from(indices.buffer, indices.byteOffset, indices.byteLength));
const indexBufferViewID = this.addBufferView(indexBufferView);
const indexAccessor = this.createAccessor(indexBufferViewID, 5123, indexBufferView.byteLength / 2, 'SCALAR');
const indexAccessorID = this.addAccessor(indexAccessor);
// Output vertex buffer
const vertices = geometry.getVertices();
const positionBounds = this.computeBoundsVec3(vertices);
const positionBufferView = this.createBufferView(Buffer.from(vertices.buffer, vertices.byteOffset, vertices.byteLength));
const positionBufferViewID = this.addBufferView(positionBufferView);
const positionAccessor = this.createAccessor(positionBufferViewID, 5126, positionBufferView.byteLength / 4 / 3, 'VEC3', positionBounds.min, positionBounds.max);
const positionAccessorID = this.addAccessor(positionAccessor);
// Output color buffer
let colorAccessorID: number | undefined = undefined;
const colors = geometry.getColors();
if (colors) {
const colorBufferView = this.createBufferView(Buffer.from(colors.buffer, colors.byteOffset, colors.byteLength));
const colorBufferViewID = this.addBufferView(colorBufferView);
const colorAccessor = this.createAccessor(colorBufferViewID, 5126, colorBufferView.byteLength / 4 / 3, 'VEC3');
colorAccessorID = this.addAccessor(colorAccessor);
}
mesh.primitives.push({
mode: 1, // LINES
attributes: {
POSITION: positionAccessorID
},
indices: indexAccessorID
});
if (!isUndefined(colorAccessorID)) {
mesh.primitives[0].attributes['COLOR_0'] = colorAccessorID;
}
return mesh;
}
protected createPointGeometry(geometry: IMF.IPointGeometry, imf: IMF.IScene): gltf.Mesh {
let mesh: gltf.Mesh = {
primitives: []
};
if (this.options.ignorePointGeometry) {
return mesh;
}
// Output vertex buffer
const vertices = geometry.getVertices();
const positionBounds = this.computeBoundsVec3(vertices);
const positionBufferView = this.createBufferView(Buffer.from(vertices.buffer, vertices.byteOffset, vertices.byteLength));
const positionBufferViewID = this.addBufferView(positionBufferView);
const positionAccessor = this.createAccessor(positionBufferViewID, 5126, positionBufferView.byteLength / 4 / 3, 'VEC3', positionBounds.min, positionBounds.max);
const positionAccessorID = this.addAccessor(positionAccessor);
// Output color buffer
let colorAccessorID: number | undefined = undefined;
const colors = geometry.getColors();
if (colors) {
const colorBufferView = this.createBufferView(Buffer.from(colors.buffer, colors.byteOffset, colors.byteLength));
const colorBufferViewID = this.addBufferView(colorBufferView);
const colorAccessor = this.createAccessor(colorBufferViewID, 5126, colorBufferView.byteLength / 4 / 3, 'VEC3');
colorAccessorID = this.addAccessor(colorAccessor);
}
mesh.primitives.push({
mode: 0, // POINTS
attributes: {
POSITION: positionAccessorID
}
});
if (!isUndefined(colorAccessorID)) {
mesh.primitives[0].attributes['COLOR_0'] = colorAccessorID;
}
return mesh;
}
protected addBufferView(bufferView: gltf.BufferView): number {
const bufferViews = this.manifest.bufferViews as gltf.BufferView[];
const hash = this.computeBufferViewHash(bufferView);
const match = this.options.deduplicate ? this.bufferViewHashes.get(hash) : undefined;
if (match !== undefined) {
this.options.log(`Skipping a duplicate buffer view (${hash})`);
this.stats.bufferViewsDeduplicated++;
return match;
} else {
if (this.options.deduplicate) {
this.bufferViewHashes.set(hash, this.bufferViewHashes.size);
}
return bufferViews.push(bufferView) - 1;
}
}
protected createBufferView(data: Buffer): gltf.BufferView {
const hash = this.computeBufferHash(data);
const cache = this.bufferViewCache.get(hash);
if (this.options.deduplicate && cache) {
this.options.log(`Skipping a duplicate buffer (${hash})`);
return cache;
}
const manifestBuffers = this.manifest.buffers as gltf.Buffer[];
// Prepare new writable stream if needed
if (this.bufferStream === null || this.bufferSize > this.options.maxBufferSize) {
if (this.bufferStream) {
const stream = this.bufferStream as fse.WriteStream;
this.pendingTasks.push(new Promise((resolve, reject) => {
stream.on('finish', resolve);
}));
this.bufferStream.close();
this.bufferStream = null;
this.bufferSize = 0;
}
const bufferUri = `${manifestBuffers.length}.bin`;
manifestBuffers.push({ uri: bufferUri, byteLength: 0 });
const bufferPath = path.join(this.baseDir, bufferUri);
this.bufferStream = fse.createWriteStream(bufferPath);
}
const bufferID = manifestBuffers.length - 1;
const buffer = manifestBuffers[bufferID];
this.bufferStream.write(data);
this.bufferSize += data.byteLength;
const bufferView = {
buffer: bufferID,
byteOffset: buffer.byteLength,
byteLength: data.byteLength
};
buffer.byteLength += bufferView.byteLength;
if (buffer.byteLength % 4 !== 0) {
// Pad to 4-byte multiples
const pad = 4 - buffer.byteLength % 4;
this.bufferStream.write(new Uint8Array(pad));
this.bufferSize += pad;
buffer.byteLength += pad;
}
if (this.options.deduplicate) {
this.bufferViewCache.set(hash, bufferView);
}
return bufferView;
}
protected addAccessor(accessor: gltf.Accessor): number {
const accessors = this.manifest.accessors as gltf.Accessor[];
const hash = this.computeAccessorHash(accessor);
const match = this.options.deduplicate ? this.accessorHashes.get(hash) : undefined;
if (match !== undefined) {
this.options.log(`Skipping a duplicate accessor (${hash})`);
this.stats.accessorsDeduplicated++;
return match;
} else {
if (this.options.deduplicate) {
this.accessorHashes.set(hash, this.accessorHashes.size);
}
return accessors.push(accessor) - 1;
}
}
protected createAccessor(bufferViewID: number, componentType: number, count: number, type: string, min?: number[], max?: number[]): gltf.Accessor {
const accessor: gltf.Accessor = {
bufferView: bufferViewID,
componentType: componentType,
count: count,
type: type
};
if (!isUndefined(min)) {
accessor.min = min.map(Math.fround);
}
if (!isUndefined(max)) {
accessor.max = max.map(Math.fround);
}
return accessor;
}
protected createMaterial(mat: IMF.Material | null, imf: IMF.IScene): gltf.MaterialPbrMetallicRoughness {
// console.log('writing material', mat)
if (!mat) {
return DefaultMaterial;
}
const diffuse = mat.diffuse;
let material: gltf.MaterialPbrMetallicRoughness = {
pbrMetallicRoughness: {
baseColorFactor: [diffuse.x, diffuse.y, diffuse.z, 1.0],
metallicFactor: mat.metallic,
roughnessFactor: (mat.roughness > 1.0) ? 1.0 : mat.roughness
}
};
if (!isUndefined(mat.opacity) && mat.opacity < 1.0 && material.pbrMetallicRoughness.baseColorFactor) {
material.alphaMode = 'BLEND';
material.pbrMetallicRoughness.baseColorFactor[3] = mat.opacity;
}
if (mat.maps) {
const manifestTextures = this.manifest.textures as gltf.Texture[];
if (mat.maps.diffuse) {
const textureID = manifestTextures.length;
manifestTextures.push(this.createTexture(mat.maps.diffuse, imf));
material.pbrMetallicRoughness.baseColorTexture = {
index: textureID,
texCoord: 0,
extensions: {
"KHR_texture_transform": {
scale: [mat.scale?.x, mat.scale?.y]
}
}
};
}
}
return material;
}
protected createTexture(uri: string, imf: IMF.IScene): gltf.Texture {
const manifestImages = this.manifest.images as gltf.Image[];
let imageID = manifestImages.findIndex(image => image.uri === uri);
if (imageID === -1) {
imageID = manifestImages.length;
const normalizedUri = uri.toLowerCase().split(/[\/\\]/).join(path.sep);
manifestImages.push({ uri: normalizedUri });
const filePath = path.join(this.baseDir, normalizedUri);
fse.ensureDirSync(path.dirname(filePath));
let imageData = imf.getImage(normalizedUri);
if (!imageData) {
// Default to a placeholder image based on the extension
switch (normalizedUri.substr(normalizedUri.lastIndexOf('.'))) {
case '.jpg':
case '.jpeg':
imageData = ImagePlaceholder.JPG;
break;
case '.png':
imageData = ImagePlaceholder.PNG;
break;
case '.bmp':
imageData = ImagePlaceholder.BMP;
break;
case '.gif':
imageData = ImagePlaceholder.GIF;
break;
default:
throw new Error(`Unsupported image format for ${normalizedUri}`);
}
}
fse.writeFileSync(filePath, imageData);
}
return { source: imageID };
}
protected computeMeshHash(mesh: gltf.Mesh): string {
return mesh.primitives.map(p => {
return `${p.mode || ''}/${p.material || ''}/${p.indices}/${p.attributes['POSITION'] || ''}/${p.attributes['NORMAL'] || ''}/${p.attributes['TEXCOORD_0'] || ''}/${p.attributes['COLOR_0'] || ''}`;
}).join('/');
}
protected computeBufferViewHash(bufferView: gltf.BufferView): string {
return `${bufferView.buffer}/${bufferView.byteLength}/${bufferView.byteOffset || ''}/${bufferView.byteStride || ''}`;
}
protected computeAccessorHash(accessor: gltf.Accessor): string {
return `${accessor.type}/${accessor.componentType}/${accessor.count}/${accessor.bufferView || 'X'}`;
}
protected computeBufferHash(buffer: Buffer): string {
const hash = crypto.createHash('md5');
hash.update(buffer);
return hash.digest('hex');
}
protected computeMaterialHash(material: IMF.IPhysicalMaterial | null): string {
if (!material) {
return 'null';
}
const hash = crypto.createHash('md5');
hash.update(JSON.stringify(material)); // TODO
return hash.digest('hex');
}
protected computeBoundsVec3(array: Float32Array): { min: number[], max: number[] } {
const min = [array[0], array[1], array[2]];
const max = [array[0], array[1], array[2]];
for (let i = 0; i < array.length; i += 3) {
min[0] = Math.min(min[0], array[i]); max[0] = Math.max(max[0], array[i]);
min[1] = Math.min(min[1], array[i + 1]); max[1] = Math.max(max[1], array[i + 1]);
min[2] = Math.min(min[2], array[i + 2]); max[2] = Math.max(max[2], array[i + 2]);
}
return { min, max };
}
}