UNPKG

@gltf-transform/functions

Version:

Functions for common glTF modifications, written using the core API

325 lines (291 loc) 8.87 kB
import { Accessor, Document, ExtensionProperty, GLTF, ImageUtils, Texture, getBounds, PropertyType, } from '@gltf-transform/core'; import { getGLPrimitiveCount } from './utils.js'; import { KHR_DF_MODEL_ETC1S, KHR_DF_MODEL_UASTC, read as readKTX } from 'ktx-parse'; import { VertexCountMethod, getMeshVertexCount, getSceneVertexCount } from './get-vertex-count.js'; /** Inspects the contents of a glTF file and returns a JSON report. */ export function inspect(doc: Document): InspectReport { return { scenes: listScenes(doc), meshes: listMeshes(doc), materials: listMaterials(doc), textures: listTextures(doc), animations: listAnimations(doc), }; } /** List scenes. */ function listScenes(doc: Document): InspectPropertyReport<InspectSceneReport> { const scenes = doc .getRoot() .listScenes() .map((scene) => { const root = scene.listChildren()[0]; const sceneBounds = getBounds(scene); return { name: scene.getName(), rootName: root ? root.getName() : '', bboxMin: toPrecision(sceneBounds.min), bboxMax: toPrecision(sceneBounds.max), renderVertexCount: getSceneVertexCount(scene, VertexCountMethod.RENDER), uploadVertexCount: getSceneVertexCount(scene, VertexCountMethod.UPLOAD), uploadNaiveVertexCount: getSceneVertexCount(scene, VertexCountMethod.UPLOAD_NAIVE), }; }); return { properties: scenes }; } /** List meshes. */ function listMeshes(doc: Document): InspectPropertyReport<InspectMeshReport> { const meshes: InspectMeshReport[] = doc .getRoot() .listMeshes() .map((mesh) => { const instances = mesh.listParents().filter((parent) => parent.propertyType !== PropertyType.ROOT).length; let glPrimitives = 0; const semantics = new Set<string>(); const meshIndices = new Set<string>(); const meshAccessors: Set<Accessor> = new Set(); mesh.listPrimitives().forEach((prim) => { for (const semantic of prim.listSemantics()) { const attr = prim.getAttribute(semantic)!; semantics.add(semantic + ':' + accessorToTypeLabel(attr)); meshAccessors.add(attr); } for (const targ of prim.listTargets()) { targ.listAttributes().forEach((attr) => meshAccessors.add(attr)); } const indices = prim.getIndices(); if (indices) { meshIndices.add(accessorToTypeLabel(indices)); meshAccessors.add(indices); } glPrimitives += getGLPrimitiveCount(prim); }); let size = 0; Array.from(meshAccessors).forEach((a) => (size += a.getArray()!.byteLength)); const modes = mesh.listPrimitives().map((prim) => MeshPrimitiveModeLabels[prim.getMode()]); return { name: mesh.getName(), mode: Array.from(new Set(modes)), meshPrimitives: mesh.listPrimitives().length, glPrimitives: glPrimitives, vertices: getMeshVertexCount(mesh, VertexCountMethod.UPLOAD), indices: Array.from(meshIndices).sort(), attributes: Array.from(semantics).sort(), instances: instances, size: size, }; }); return { properties: meshes }; } /** List materials. */ function listMaterials(doc: Document): InspectPropertyReport<InspectMaterialReport> { const materials: InspectMaterialReport[] = doc .getRoot() .listMaterials() .map((material) => { const instances = material .listParents() .filter((parent) => parent.propertyType !== PropertyType.ROOT).length; // Find all texture slots attached to this material or its extensions. const extensions = new Set<ExtensionProperty>(material.listExtensions()); const slots = doc .getGraph() .listEdges() .filter((ref) => { const child = ref.getChild(); const parent = ref.getParent(); if (child instanceof Texture && parent === material) { return true; } if (child instanceof Texture && parent instanceof ExtensionProperty && extensions.has(parent)) { return true; } return false; }) .map((ref) => ref.getName()); return { name: material.getName(), instances, textures: slots, alphaMode: material.getAlphaMode(), doubleSided: material.getDoubleSided(), }; }); return { properties: materials }; } /** List textures. */ function listTextures(doc: Document): InspectPropertyReport<InspectTextureReport> { const textures: InspectTextureReport[] = doc .getRoot() .listTextures() .map((texture) => { const instances = texture .listParents() .filter((parent) => parent.propertyType !== PropertyType.ROOT).length; const slots = doc .getGraph() .listParentEdges(texture) .filter((edge) => edge.getParent().propertyType !== PropertyType.ROOT) .map((edge) => edge.getName()); const resolution = ImageUtils.getSize(texture.getImage()!, texture.getMimeType()); let compression = ''; if (texture.getMimeType() === 'image/ktx2') { const container = readKTX(texture.getImage()!); const dfd = container.dataFormatDescriptor[0]; if (dfd.colorModel === KHR_DF_MODEL_ETC1S) { compression = 'ETC1S'; } else if (dfd.colorModel === KHR_DF_MODEL_UASTC) { compression = 'UASTC'; } } return { name: texture.getName(), uri: texture.getURI(), slots: Array.from(new Set(slots)), instances, mimeType: texture.getMimeType(), compression, resolution: resolution ? resolution.join('x') : '', size: texture.getImage()!.byteLength, gpuSize: ImageUtils.getVRAMByteLength(texture.getImage()!, texture.getMimeType()), }; }); return { properties: textures }; } /** List animations. */ function listAnimations(doc: Document): InspectPropertyReport<InspectAnimationReport> { const animations: InspectAnimationReport[] = doc .getRoot() .listAnimations() .map((anim) => { let minTime = Infinity; let maxTime = -Infinity; anim.listSamplers().forEach((sampler) => { const input = sampler.getInput(); if (!input) return; minTime = Math.min(minTime, input.getMin([])[0]); maxTime = Math.max(maxTime, input.getMax([])[0]); }); let size = 0; let keyframes = 0; const accessors: Set<Accessor> = new Set(); anim.listSamplers().forEach((sampler) => { const input = sampler.getInput(); const output = sampler.getOutput(); if (!input) return; keyframes += input.getCount(); accessors.add(input); if (!output) return; accessors.add(output); }); Array.from(accessors).forEach((accessor) => { size += accessor.getArray()!.byteLength; }); return { name: anim.getName(), channels: anim.listChannels().length, samplers: anim.listSamplers().length, duration: Math.round((maxTime - minTime) * 1000) / 1000, keyframes: keyframes, size: size, }; }); return { properties: animations }; } export interface InspectReport { scenes: InspectPropertyReport<InspectSceneReport>; meshes: InspectPropertyReport<InspectMeshReport>; materials: InspectPropertyReport<InspectMaterialReport>; textures: InspectPropertyReport<InspectTextureReport>; animations: InspectPropertyReport<InspectAnimationReport>; } export interface InspectPropertyReport<T> { properties: T[]; errors?: string[]; warnings?: string[]; } export interface InspectSceneReport { name: string; rootName: string; bboxMin: number[]; bboxMax: number[]; renderVertexCount: number; uploadVertexCount: number; uploadNaiveVertexCount: number; } export interface InspectMeshReport { name: string; meshPrimitives: number; mode: string[]; vertices: number; glPrimitives: number; indices: string[]; attributes: string[]; instances: number; size: number; } export interface InspectMaterialReport { name: string; instances: number; textures: string[]; alphaMode: GLTF.MaterialAlphaMode; doubleSided: boolean; } export interface InspectTextureReport { name: string; uri: string; slots: string[]; instances: number; mimeType: string; resolution: string; compression: string; size: number; gpuSize: number | null; } export interface InspectAnimationReport { name: string; channels: number; samplers: number; keyframes: number; duration: number; size: number; } const MeshPrimitiveModeLabels = [ 'POINTS', 'LINES', 'LINE_LOOP', 'LINE_STRIP', 'TRIANGLES', 'TRIANGLE_STRIP', 'TRIANGLE_FAN', ]; const NumericTypeLabels: Record<string, string> = { Float32Array: 'f32', Uint32Array: 'u32', Uint16Array: 'u16', Uint8Array: 'u8', Int32Array: 'i32', Int16Array: 'i16', Int8Array: 'i8', }; /** Maps values in a vector to a finite precision. */ function toPrecision(v: number[]): number[] { for (let i = 0; i < v.length; i++) { if ((v[i] as number).toFixed) v[i] = Number(v[i].toFixed(5)); } return v; } function accessorToTypeLabel(accessor: Accessor): string { const array = accessor.getArray()!; const base = NumericTypeLabels[array.constructor.name] || '?'; const suffix = accessor.getNormalized() ? '_norm' : ''; return base + suffix; }