@gltf-transform/core
Version:
glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.
690 lines (539 loc) • 22.8 kB
text/typescript
import {
ComponentTypeToTypedArray,
GLB_BUFFER,
type mat4,
PropertyType,
type TypedArray,
type vec3,
type vec4,
} from '../constants.js';
import { Document } from '../document.js';
import type { Extension } from '../extension.js';
import type { JSONDocument } from '../json-document.js';
import { Accessor, AnimationSampler, Camera } from '../properties/index.js';
import type { GLTF } from '../types/gltf.js';
import { BufferUtils, FileUtils, type ILogger, ImageUtils, Logger, MathUtils } from '../utils/index.js';
import { ReaderContext } from './reader-context.js';
export interface ReaderOptions {
logger?: ILogger;
extensions: (typeof Extension)[];
dependencies: { [key: string]: unknown };
}
const DEFAULT_OPTIONS: ReaderOptions = {
logger: Logger.DEFAULT_INSTANCE,
extensions: [],
dependencies: {},
};
const SUPPORTED_PREREAD_TYPES = new Set<PropertyType>([
PropertyType.BUFFER,
PropertyType.TEXTURE,
PropertyType.MATERIAL,
PropertyType.MESH,
PropertyType.PRIMITIVE,
PropertyType.NODE,
PropertyType.SCENE,
]);
/** @internal */
export class GLTFReader {
public static read(jsonDoc: JSONDocument, _options: ReaderOptions = DEFAULT_OPTIONS): Document {
const options = { ...DEFAULT_OPTIONS, ..._options } as Required<ReaderOptions>;
const { json } = jsonDoc;
const document = new Document().setLogger(options.logger);
this.validate(jsonDoc, options);
/* Reader context. */
const context = new ReaderContext(jsonDoc);
/** Asset. */
const assetDef = json.asset;
const asset = document.getRoot().getAsset();
if (assetDef.copyright) asset.copyright = assetDef.copyright;
if (assetDef.extras) asset.extras = assetDef.extras;
if (json.extras !== undefined) {
document.getRoot().setExtras({ ...json.extras });
}
/** Extensions (1/2). */
const extensionsUsed = json.extensionsUsed || [];
const extensionsRequired = json.extensionsRequired || [];
options.extensions.sort((a, b) => (a.EXTENSION_NAME > b.EXTENSION_NAME ? 1 : -1));
for (const Extension of options.extensions) {
if (extensionsUsed.includes(Extension.EXTENSION_NAME)) {
// Create extension.
const extension = document
.createExtension(Extension as unknown as new (doc: Document) => Extension)
.setRequired(extensionsRequired.includes(Extension.EXTENSION_NAME));
// Warn on unsupported preread hooks.
const unsupportedHooks = extension.prereadTypes.filter((type) => !SUPPORTED_PREREAD_TYPES.has(type));
if (unsupportedHooks.length) {
options.logger.warn(
`Preread hooks for some types (${unsupportedHooks.join()}), requested by extension ` +
`${extension.extensionName}, are unsupported. Please file an issue or a PR.`,
);
}
// Install dependencies.
for (const key of extension.readDependencies) {
extension.install(key, options.dependencies[key]);
}
}
}
/** Buffers. */
const bufferDefs = json.buffers || [];
document
.getRoot()
.listExtensionsUsed()
.filter((extension) => extension.prereadTypes.includes(PropertyType.BUFFER))
.forEach((extension) => extension.preread(context, PropertyType.BUFFER));
context.buffers = bufferDefs.map((bufferDef) => {
const buffer = document.createBuffer(bufferDef.name);
if (bufferDef.extras) buffer.setExtras(bufferDef.extras);
if (bufferDef.uri && bufferDef.uri.indexOf('__') !== 0) {
buffer.setURI(bufferDef.uri);
}
return buffer;
});
/** Buffer views. */
const bufferViewDefs = json.bufferViews || [];
context.bufferViewBuffers = bufferViewDefs.map((bufferViewDef, index) => {
if (!context.bufferViews[index]) {
const bufferDef = jsonDoc.json.buffers![bufferViewDef.buffer];
const resource = bufferDef.uri ? jsonDoc.resources[bufferDef.uri] : jsonDoc.resources[GLB_BUFFER];
const byteOffset = bufferViewDef.byteOffset || 0;
context.bufferViews[index] = BufferUtils.toView(resource, byteOffset, bufferViewDef.byteLength);
}
return context.buffers[bufferViewDef.buffer];
});
/** Accessors. */
// Accessor .count and .componentType properties are inferred dynamically.
const accessorDefs = json.accessors || [];
context.accessors = accessorDefs.map((accessorDef) => {
const buffer = context.bufferViewBuffers[accessorDef.bufferView!];
const accessor = document.createAccessor(accessorDef.name, buffer).setType(accessorDef.type);
if (accessorDef.extras) accessor.setExtras(accessorDef.extras);
if (accessorDef.normalized !== undefined) {
accessor.setNormalized(accessorDef.normalized);
}
// Sparse accessors, KHR_draco_mesh_compression, and EXT_meshopt_compression.
if (accessorDef.bufferView === undefined) return accessor;
// NOTICE: We mark sparse accessors at the end of the I/O reading process. Consider an
// accessor to be 'sparse' if it (A) includes sparse value overrides, or (B) does not
// define .bufferView _and_ no extension provides that data.
accessor.setArray(getAccessorArray(accessorDef, context));
return accessor;
});
/** Textures. */
// glTF Transform's "Texture" properties correspond 1:1 with glTF "Image" properties, and
// with image files. The glTF file may contain more one texture per image, where images
// are reused with different sampler properties.
const imageDefs = json.images || [];
const textureDefs = json.textures || [];
document
.getRoot()
.listExtensionsUsed()
.filter((extension) => extension.prereadTypes.includes(PropertyType.TEXTURE))
.forEach((extension) => extension.preread(context, PropertyType.TEXTURE));
context.textures = imageDefs.map((imageDef) => {
const texture = document.createTexture(imageDef.name);
// glTF Image corresponds 1:1 with glTF Transform Texture. See `writer.ts`.
if (imageDef.extras) texture.setExtras(imageDef.extras);
if (imageDef.bufferView !== undefined) {
const bufferViewDef = json.bufferViews![imageDef.bufferView];
const bufferDef = jsonDoc.json.buffers![bufferViewDef.buffer];
const bufferData = bufferDef.uri ? jsonDoc.resources[bufferDef.uri] : jsonDoc.resources[GLB_BUFFER];
const byteOffset = bufferViewDef.byteOffset || 0;
const byteLength = bufferViewDef.byteLength;
const imageData = bufferData.slice(byteOffset, byteOffset + byteLength);
texture.setImage(imageData);
} else if (imageDef.uri !== undefined) {
texture.setImage(jsonDoc.resources[imageDef.uri]);
if (imageDef.uri.indexOf('__') !== 0) {
texture.setURI(imageDef.uri);
}
}
if (imageDef.mimeType !== undefined) {
texture.setMimeType(imageDef.mimeType);
} else if (imageDef.uri) {
const extension = FileUtils.extension(imageDef.uri);
texture.setMimeType(ImageUtils.extensionToMimeType(extension));
}
return texture;
});
/** Materials. */
document
.getRoot()
.listExtensionsUsed()
.filter((extension) => extension.prereadTypes.includes(PropertyType.MATERIAL))
.forEach((extension) => extension.preread(context, PropertyType.MATERIAL));
const materialDefs = json.materials || [];
context.materials = materialDefs.map((materialDef) => {
const material = document.createMaterial(materialDef.name);
if (materialDef.extras) material.setExtras(materialDef.extras);
// Program state & blending.
if (materialDef.alphaMode !== undefined) {
material.setAlphaMode(materialDef.alphaMode);
}
if (materialDef.alphaCutoff !== undefined) {
material.setAlphaCutoff(materialDef.alphaCutoff);
}
if (materialDef.doubleSided !== undefined) {
material.setDoubleSided(materialDef.doubleSided);
}
// Factors.
const pbrDef = materialDef.pbrMetallicRoughness || {};
if (pbrDef.baseColorFactor !== undefined) {
material.setBaseColorFactor(pbrDef.baseColorFactor as vec4);
}
if (materialDef.emissiveFactor !== undefined) {
material.setEmissiveFactor(materialDef.emissiveFactor as vec3);
}
if (pbrDef.metallicFactor !== undefined) {
material.setMetallicFactor(pbrDef.metallicFactor);
}
if (pbrDef.roughnessFactor !== undefined) {
material.setRoughnessFactor(pbrDef.roughnessFactor);
}
// Textures.
if (pbrDef.baseColorTexture !== undefined) {
const textureInfoDef = pbrDef.baseColorTexture;
const texture = context.textures[textureDefs[textureInfoDef.index].source!];
material.setBaseColorTexture(texture);
context.setTextureInfo(material.getBaseColorTextureInfo()!, textureInfoDef);
}
if (materialDef.emissiveTexture !== undefined) {
const textureInfoDef = materialDef.emissiveTexture;
const texture = context.textures[textureDefs[textureInfoDef.index].source!];
material.setEmissiveTexture(texture);
context.setTextureInfo(material.getEmissiveTextureInfo()!, textureInfoDef);
}
if (materialDef.normalTexture !== undefined) {
const textureInfoDef = materialDef.normalTexture;
const texture = context.textures[textureDefs[textureInfoDef.index].source!];
material.setNormalTexture(texture);
context.setTextureInfo(material.getNormalTextureInfo()!, textureInfoDef);
if (materialDef.normalTexture.scale !== undefined) {
material.setNormalScale(materialDef.normalTexture.scale);
}
}
if (materialDef.occlusionTexture !== undefined) {
const textureInfoDef = materialDef.occlusionTexture;
const texture = context.textures[textureDefs[textureInfoDef.index].source!];
material.setOcclusionTexture(texture);
context.setTextureInfo(material.getOcclusionTextureInfo()!, textureInfoDef);
if (materialDef.occlusionTexture.strength !== undefined) {
material.setOcclusionStrength(materialDef.occlusionTexture.strength);
}
}
if (pbrDef.metallicRoughnessTexture !== undefined) {
const textureInfoDef = pbrDef.metallicRoughnessTexture;
const texture = context.textures[textureDefs[textureInfoDef.index].source!];
material.setMetallicRoughnessTexture(texture);
context.setTextureInfo(material.getMetallicRoughnessTextureInfo()!, textureInfoDef);
}
return material;
});
/** Meshes. */
document
.getRoot()
.listExtensionsUsed()
.filter((extension) => extension.prereadTypes.includes(PropertyType.MESH))
.forEach((extension) => extension.preread(context, PropertyType.MESH));
const meshDefs = json.meshes || [];
document
.getRoot()
.listExtensionsUsed()
.filter((extension) => extension.prereadTypes.includes(PropertyType.PRIMITIVE))
.forEach((extension) => extension.preread(context, PropertyType.PRIMITIVE));
context.meshes = meshDefs.map((meshDef) => {
const mesh = document.createMesh(meshDef.name);
if (meshDef.extras) mesh.setExtras(meshDef.extras);
if (meshDef.weights !== undefined) {
mesh.setWeights(meshDef.weights);
}
const primitiveDefs = meshDef.primitives || [];
primitiveDefs.forEach((primitiveDef) => {
const primitive = document.createPrimitive();
if (primitiveDef.extras) primitive.setExtras(primitiveDef.extras);
if (primitiveDef.material !== undefined) {
primitive.setMaterial(context.materials[primitiveDef.material]);
}
if (primitiveDef.mode !== undefined) {
primitive.setMode(primitiveDef.mode);
}
for (const [semantic, index] of Object.entries(primitiveDef.attributes || {})) {
primitive.setAttribute(semantic, context.accessors[index]);
}
if (primitiveDef.indices !== undefined) {
primitive.setIndices(context.accessors[primitiveDef.indices]);
}
const targetNames: string[] = (meshDef.extras && (meshDef.extras.targetNames as string[])) || [];
const targetDefs = primitiveDef.targets || [];
targetDefs.forEach((targetDef, targetIndex) => {
const targetName = targetNames[targetIndex] || targetIndex.toString();
const target = document.createPrimitiveTarget(targetName);
for (const [semantic, accessorIndex] of Object.entries(targetDef)) {
target.setAttribute(semantic, context.accessors[accessorIndex]);
}
primitive.addTarget(target);
});
mesh.addPrimitive(primitive);
});
return mesh;
});
/** Cameras. */
const cameraDefs = json.cameras || [];
context.cameras = cameraDefs.map((cameraDef) => {
const camera = document.createCamera(cameraDef.name).setType(cameraDef.type);
if (cameraDef.extras) camera.setExtras(cameraDef.extras);
if (cameraDef.type === Camera.Type.PERSPECTIVE) {
const perspectiveDef = cameraDef.perspective!;
camera.setYFov(perspectiveDef.yfov);
camera.setZNear(perspectiveDef.znear);
if (perspectiveDef.zfar !== undefined) {
camera.setZFar(perspectiveDef.zfar);
}
if (perspectiveDef.aspectRatio !== undefined) {
camera.setAspectRatio(perspectiveDef.aspectRatio);
}
} else {
const orthoDef = cameraDef.orthographic!;
camera.setZNear(orthoDef.znear).setZFar(orthoDef.zfar).setXMag(orthoDef.xmag).setYMag(orthoDef.ymag);
}
return camera;
});
/** Nodes. */
const nodeDefs = json.nodes || [];
document
.getRoot()
.listExtensionsUsed()
.filter((extension) => extension.prereadTypes.includes(PropertyType.NODE))
.forEach((extension) => extension.preread(context, PropertyType.NODE));
context.nodes = nodeDefs.map((nodeDef) => {
const node = document.createNode(nodeDef.name);
if (nodeDef.extras) node.setExtras(nodeDef.extras);
if (nodeDef.translation !== undefined) {
node.setTranslation(nodeDef.translation as vec3);
}
if (nodeDef.rotation !== undefined) {
node.setRotation(nodeDef.rotation as vec4);
}
if (nodeDef.scale !== undefined) {
node.setScale(nodeDef.scale as vec3);
}
if (nodeDef.matrix !== undefined) {
const translation = [0, 0, 0] as vec3;
const rotation = [0, 0, 0, 1] as vec4;
const scale = [1, 1, 1] as vec3;
MathUtils.decompose(nodeDef.matrix as mat4, translation, rotation, scale);
node.setTranslation(translation);
node.setRotation(rotation);
node.setScale(scale);
}
if (nodeDef.weights !== undefined) {
node.setWeights(nodeDef.weights);
}
// Attachments (mesh, camera, skin) defined later in reading process.
return node;
});
/** Skins. */
const skinDefs = json.skins || [];
context.skins = skinDefs.map((skinDef) => {
const skin = document.createSkin(skinDef.name);
if (skinDef.extras) skin.setExtras(skinDef.extras);
if (skinDef.inverseBindMatrices !== undefined) {
skin.setInverseBindMatrices(context.accessors[skinDef.inverseBindMatrices]);
}
if (skinDef.skeleton !== undefined) {
skin.setSkeleton(context.nodes[skinDef.skeleton]);
}
for (const nodeIndex of skinDef.joints) {
skin.addJoint(context.nodes[nodeIndex]);
}
return skin;
});
/** Node attachments. */
nodeDefs.map((nodeDef, nodeIndex) => {
const node = context.nodes[nodeIndex];
const children = nodeDef.children || [];
children.forEach((childIndex) => node.addChild(context.nodes[childIndex]));
if (nodeDef.mesh !== undefined) node.setMesh(context.meshes[nodeDef.mesh]);
if (nodeDef.camera !== undefined) node.setCamera(context.cameras[nodeDef.camera]);
if (nodeDef.skin !== undefined) node.setSkin(context.skins[nodeDef.skin]);
});
/** Animations. */
const animationDefs = json.animations || [];
context.animations = animationDefs.map((animationDef) => {
const animation = document.createAnimation(animationDef.name);
if (animationDef.extras) animation.setExtras(animationDef.extras);
const samplerDefs = animationDef.samplers || [];
const samplers = samplerDefs.map((samplerDef) => {
const sampler = document
.createAnimationSampler()
.setInput(context.accessors[samplerDef.input])
.setOutput(context.accessors[samplerDef.output])
.setInterpolation(samplerDef.interpolation || AnimationSampler.Interpolation.LINEAR);
if (samplerDef.extras) sampler.setExtras(samplerDef.extras);
animation.addSampler(sampler);
return sampler;
});
const channels = animationDef.channels || [];
channels.forEach((channelDef) => {
const channel = document
.createAnimationChannel()
.setSampler(samplers[channelDef.sampler])
.setTargetPath(channelDef.target.path);
if (channelDef.target.node !== undefined) channel.setTargetNode(context.nodes[channelDef.target.node]);
if (channelDef.extras) channel.setExtras(channelDef.extras);
animation.addChannel(channel);
});
return animation;
});
/** Scenes. */
const sceneDefs = json.scenes || [];
document
.getRoot()
.listExtensionsUsed()
.filter((extension) => extension.prereadTypes.includes(PropertyType.SCENE))
.forEach((extension) => extension.preread(context, PropertyType.SCENE));
context.scenes = sceneDefs.map((sceneDef) => {
const scene = document.createScene(sceneDef.name);
if (sceneDef.extras) scene.setExtras(sceneDef.extras);
const children = sceneDef.nodes || [];
children.map((nodeIndex) => context.nodes[nodeIndex]).forEach((node) => scene.addChild(node));
return scene;
});
if (json.scene !== undefined) {
document.getRoot().setDefaultScene(context.scenes[json.scene]);
}
/** Extensions (2/2). */
document
.getRoot()
.listExtensionsUsed()
.forEach((extension) => extension.read(context));
/** Post-processing. */
// Consider an accessor to be 'sparse' if it (A) includes sparse value overrides,
// or (B) does not define .bufferView _and_ no extension provides that data. Case
// (B) represents a zero-filled accessor.
accessorDefs.forEach((accessorDef, index) => {
const accessor = context.accessors[index];
const hasSparseValues = !!accessorDef.sparse;
const isZeroFilled = !accessorDef.bufferView && !accessor.getArray();
if (hasSparseValues || isZeroFilled) {
accessor.setSparse(true).setArray(getSparseArray(accessorDef, context));
}
});
return document;
}
private static validate(jsonDoc: JSONDocument, options: Required<ReaderOptions>): void {
const json = jsonDoc.json;
if (json.asset.version !== '2.0') {
throw new Error(`Unsupported glTF version, "${json.asset.version}".`);
}
if (json.extensionsRequired) {
for (const extensionName of json.extensionsRequired) {
if (!options.extensions.find((extension) => extension.EXTENSION_NAME === extensionName)) {
throw new Error(`Missing required extension, "${extensionName}".`);
}
}
}
if (json.extensionsUsed) {
for (const extensionName of json.extensionsUsed) {
if (!options.extensions.find((extension) => extension.EXTENSION_NAME === extensionName)) {
options.logger.warn(`Missing optional extension, "${extensionName}".`);
}
}
}
}
}
/**
* Returns the contents of an interleaved accessor, as a typed array.
* @internal
*/
function getInterleavedArray(accessorDef: GLTF.IAccessor, context: ReaderContext): TypedArray {
const jsonDoc = context.jsonDoc;
const bufferView = context.bufferViews[accessorDef.bufferView!];
const bufferViewDef = jsonDoc.json.bufferViews![accessorDef.bufferView!];
const TypedArray = ComponentTypeToTypedArray[accessorDef.componentType];
const elementSize = Accessor.getElementSize(accessorDef.type);
const componentSize = TypedArray.BYTES_PER_ELEMENT;
const accessorByteOffset = accessorDef.byteOffset || 0;
const array = new TypedArray(accessorDef.count * elementSize);
const view = new DataView(bufferView.buffer, bufferView.byteOffset, bufferView.byteLength);
const byteStride = bufferViewDef.byteStride!;
for (let i = 0; i < accessorDef.count; i++) {
for (let j = 0; j < elementSize; j++) {
const byteOffset = accessorByteOffset + i * byteStride + j * componentSize;
let value: number;
switch (accessorDef.componentType) {
case Accessor.ComponentType.FLOAT:
value = view.getFloat32(byteOffset, true);
break;
case Accessor.ComponentType.UNSIGNED_INT:
value = view.getUint32(byteOffset, true);
break;
case Accessor.ComponentType.UNSIGNED_SHORT:
value = view.getUint16(byteOffset, true);
break;
case Accessor.ComponentType.UNSIGNED_BYTE:
value = view.getUint8(byteOffset);
break;
case Accessor.ComponentType.SHORT:
value = view.getInt16(byteOffset, true);
break;
case Accessor.ComponentType.BYTE:
value = view.getInt8(byteOffset);
break;
default:
throw new Error(`Unexpected componentType "${accessorDef.componentType}".`);
}
array[i * elementSize + j] = value;
}
}
return array;
}
/**
* Returns the contents of an accessor, as a typed array.
* @internal
*/
function getAccessorArray(accessorDef: GLTF.IAccessor, context: ReaderContext): TypedArray {
const jsonDoc = context.jsonDoc;
const bufferView = context.bufferViews[accessorDef.bufferView!];
const bufferViewDef = jsonDoc.json.bufferViews![accessorDef.bufferView!];
const TypedArray = ComponentTypeToTypedArray[accessorDef.componentType];
const elementSize = Accessor.getElementSize(accessorDef.type);
const componentSize = TypedArray.BYTES_PER_ELEMENT;
const elementStride = elementSize * componentSize;
// Interleaved buffer view.
if (bufferViewDef.byteStride !== undefined && bufferViewDef.byteStride !== elementStride) {
return getInterleavedArray(accessorDef, context);
}
const byteOffset = bufferView.byteOffset + (accessorDef.byteOffset || 0);
const byteLength = accessorDef.count * elementSize * componentSize;
// Might optimize this to avoid deep copy later, but it's useful for now and not a known
// bottleneck. See https://github.com/donmccurdy/glTF-Transform/issues/256.
return new TypedArray(bufferView.buffer.slice(byteOffset, byteOffset + byteLength));
}
/**
* Returns the contents of a sparse accessor, as a typed array.
* @internal
*/
function getSparseArray(accessorDef: GLTF.IAccessor, context: ReaderContext): TypedArray {
const TypedArray = ComponentTypeToTypedArray[accessorDef.componentType];
const elementSize = Accessor.getElementSize(accessorDef.type);
let array: TypedArray;
if (accessorDef.bufferView !== undefined) {
array = getAccessorArray(accessorDef, context);
} else {
array = new TypedArray(accessorDef.count * elementSize);
}
const sparseDef = accessorDef.sparse;
if (!sparseDef) return array; // Zero-filled accessor.
const count = sparseDef.count;
const indicesDef = { ...accessorDef, ...sparseDef.indices, count, type: 'SCALAR' };
const valuesDef = { ...accessorDef, ...sparseDef.values, count };
const indices = getAccessorArray(indicesDef as GLTF.IAccessor, context);
const values = getAccessorArray(valuesDef, context);
// Override indices given in the sparse data.
for (let i = 0; i < indicesDef.count; i++) {
for (let j = 0; j < elementSize; j++) {
array[indices[i] * elementSize + j] = values[i * elementSize + j];
}
}
return array;
}