UNPKG

@gltf-transform/core

Version:

glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.

286 lines (246 loc) 10.2 kB
import { BufferViewUsage, Format, PropertyType } from '../constants.js'; import type { Document } from '../document.js'; import type { JSONDocument } from '../json-document.js'; import type { Accessor, Animation, Buffer, Camera, Material, Mesh, Node, Property, Scene, Skin, Texture, TextureInfo, } from '../properties/index.js'; import type { GLTF } from '../types/gltf.js'; import { type ILogger, ImageUtils } from '../utils/index.js'; import type { WriterOptions } from './writer.js'; type PropertyDef = GLTF.IScene | GLTF.INode | GLTF.IMaterial | GLTF.ISkin | GLTF.ITexture; enum BufferViewTarget { ARRAY_BUFFER = 34962, ELEMENT_ARRAY_BUFFER = 34963, } /** * Model class providing writing state to a {@link GLTFWriter} and its {@link Extension} * implementations. * * @hidden */ export class WriterContext { /** Explicit buffer view targets defined by glTF specification. */ public static readonly BufferViewTarget: typeof BufferViewTarget = BufferViewTarget; /** * Implicit buffer view usage, not required by glTF specification, but nonetheless useful for * proper grouping of accessors into buffer views. Additional usages are defined by extensions, * like `EXT_mesh_gpu_instancing`. */ public static readonly BufferViewUsage: typeof BufferViewUsage = BufferViewUsage; /** Maps usage type to buffer view target. Usages not mapped have undefined targets. */ public static readonly USAGE_TO_TARGET: { [key: string]: BufferViewTarget | undefined } = { [BufferViewUsage.ARRAY_BUFFER]: BufferViewTarget.ARRAY_BUFFER, [BufferViewUsage.ELEMENT_ARRAY_BUFFER]: BufferViewTarget.ELEMENT_ARRAY_BUFFER, }; public readonly accessorIndexMap: Map<Accessor, number> = new Map(); public readonly animationIndexMap: Map<Animation, number> = new Map(); public readonly bufferIndexMap: Map<Buffer, number> = new Map(); public readonly cameraIndexMap: Map<Camera, number> = new Map(); public readonly skinIndexMap: Map<Skin, number> = new Map(); public readonly materialIndexMap: Map<Material, number> = new Map(); public readonly meshIndexMap: Map<Mesh, number> = new Map(); public readonly nodeIndexMap: Map<Node, number> = new Map(); public readonly imageIndexMap: Map<Texture, number> = new Map(); public readonly textureDefIndexMap: Map<string, number> = new Map(); // textureDef JSON -> index public readonly textureInfoDefMap: Map<TextureInfo, GLTF.ITextureInfo> = new Map(); public readonly samplerDefIndexMap: Map<string, number> = new Map(); // samplerDef JSON -> index public readonly sceneIndexMap: Map<Scene, number> = new Map(); public readonly imageBufferViews: Uint8Array[] = []; public readonly otherBufferViews: Map<Buffer, Uint8Array[]> = new Map(); public readonly otherBufferViewsIndexMap: Map<Uint8Array, number> = new Map(); public readonly extensionData: { [key: string]: unknown } = {}; public bufferURIGenerator: UniqueURIGenerator<Buffer>; public imageURIGenerator: UniqueURIGenerator<Texture>; public logger: ILogger; private readonly _accessorUsageMap: Map<Accessor, BufferViewUsage | string> = new Map(); public readonly accessorUsageGroupedByParent: Set<string> = new Set(['ARRAY_BUFFER']); public readonly accessorParents: Map<Accessor, Property> = new Map(); constructor( private readonly _doc: Document, public readonly jsonDoc: JSONDocument, public readonly options: Required<WriterOptions>, ) { const root = _doc.getRoot(); const numBuffers = root.listBuffers().length; const numImages = root.listTextures().length; this.bufferURIGenerator = new UniqueURIGenerator(numBuffers > 1, () => options.basename || 'buffer'); this.imageURIGenerator = new UniqueURIGenerator( numImages > 1, (texture) => getSlot(_doc, texture) || options.basename || 'texture', ); this.logger = _doc.getLogger(); } /** * Creates a TextureInfo definition, and any Texture or Sampler definitions it requires. If * possible, Texture and Sampler definitions are shared. */ public createTextureInfoDef(texture: Texture, textureInfo: TextureInfo): GLTF.ITextureInfo { const samplerDef = { magFilter: textureInfo.getMagFilter() || undefined, minFilter: textureInfo.getMinFilter() || undefined, wrapS: textureInfo.getWrapS(), wrapT: textureInfo.getWrapT(), } as GLTF.ISampler; const samplerKey = JSON.stringify(samplerDef); if (!this.samplerDefIndexMap.has(samplerKey)) { this.samplerDefIndexMap.set(samplerKey, this.jsonDoc.json.samplers!.length); this.jsonDoc.json.samplers!.push(samplerDef); } const textureDef = { source: this.imageIndexMap.get(texture), sampler: this.samplerDefIndexMap.get(samplerKey), } as GLTF.ITexture; const textureKey = JSON.stringify(textureDef); if (!this.textureDefIndexMap.has(textureKey)) { this.textureDefIndexMap.set(textureKey, this.jsonDoc.json.textures!.length); this.jsonDoc.json.textures!.push(textureDef); } const textureInfoDef = { index: this.textureDefIndexMap.get(textureKey), } as GLTF.ITextureInfo; if (textureInfo.getTexCoord() !== 0) { textureInfoDef.texCoord = textureInfo.getTexCoord(); } if (Object.keys(textureInfo.getExtras()).length > 0) { textureInfoDef.extras = textureInfo.getExtras(); } this.textureInfoDefMap.set(textureInfo, textureInfoDef); return textureInfoDef; } public createPropertyDef(property: Property): PropertyDef { const def = {} as PropertyDef; if (property.getName()) { def.name = property.getName(); } if (Object.keys(property.getExtras()).length > 0) { def.extras = property.getExtras(); } return def; } public createAccessorDef(accessor: Accessor): GLTF.IAccessor { const accessorDef = this.createPropertyDef(accessor) as GLTF.IAccessor; accessorDef.type = accessor.getType(); accessorDef.componentType = accessor.getComponentType(); accessorDef.count = accessor.getCount(); const needsBounds = this._doc .getGraph() .listParentEdges(accessor) .some( (edge) => (edge.getName() === 'attributes' && edge.getAttributes().key === 'POSITION') || edge.getName() === 'input', ); if (needsBounds) { accessorDef.max = accessor.getMax([]).map(Math.fround); accessorDef.min = accessor.getMin([]).map(Math.fround); } if (accessor.getNormalized()) { accessorDef.normalized = accessor.getNormalized(); } return accessorDef; } public createImageData(imageDef: GLTF.IImage, data: Uint8Array, texture: Texture): void { if (this.options.format === Format.GLB) { this.imageBufferViews.push(data); imageDef.bufferView = this.jsonDoc.json.bufferViews!.length; this.jsonDoc.json.bufferViews!.push({ buffer: 0, byteOffset: -1, // determined while iterating buffers, in Writer.ts. byteLength: data.byteLength, }); } else { const extension = ImageUtils.mimeTypeToExtension(texture.getMimeType()); imageDef.uri = this.imageURIGenerator.createURI(texture, extension); this.assignResourceURI(imageDef.uri, data, false); } } public assignResourceURI(uri: string, data: Uint8Array, throwOnConflict: boolean): void { const resources = this.jsonDoc.resources; // https://github.com/KhronosGroup/glTF/issues/2446 if (!(uri in resources)) { resources[uri] = data; return; } if (data === resources[uri]) { this.logger.warn(`Duplicate resource URI, "${uri}".`); return; } const conflictMessage = `Resource URI "${uri}" already assigned to different data.`; if (!throwOnConflict) { this.logger.warn(conflictMessage); return; } throw new Error(conflictMessage); } /** * Returns implicit usage type of the given accessor, related to grouping accessors into * buffer views. Usage is a superset of buffer view target, including ARRAY_BUFFER and * ELEMENT_ARRAY_BUFFER, but also usages that do not match GPU buffer view targets such as * IBMs. Additional usages are defined by extensions, like `EXT_mesh_gpu_instancing`. */ public getAccessorUsage(accessor: Accessor): BufferViewUsage | string { const cachedUsage = this._accessorUsageMap.get(accessor); if (cachedUsage) return cachedUsage; if (accessor.getSparse()) return BufferViewUsage.SPARSE; for (const edge of this._doc.getGraph().listParentEdges(accessor)) { const { usage } = edge.getAttributes() as { usage: BufferViewUsage | undefined }; if (usage) return usage; if (edge.getParent().propertyType !== PropertyType.ROOT) { this.logger.warn(`Missing attribute ".usage" on edge, "${edge.getName()}".`); } } // Group accessors with no specified usage into a miscellaneous buffer view. return BufferViewUsage.OTHER; } /** * Sets usage for the given accessor. Some accessor types must be grouped into * buffer views with like accessors. This includes the specified buffer view "targets", but * also implicit usage like IBMs or instanced mesh attributes. If unspecified, an accessor * will be grouped with other accessors of unspecified usage. */ public addAccessorToUsageGroup(accessor: Accessor, usage: BufferViewUsage | string): this { const prevUsage = this._accessorUsageMap.get(accessor); if (prevUsage && prevUsage !== usage) { throw new Error(`Accessor with usage "${prevUsage}" cannot be reused as "${usage}".`); } this._accessorUsageMap.set(accessor, usage); return this; } } export class UniqueURIGenerator<T extends Texture | Buffer> { private counter = {} as Record<string, number>; constructor( private readonly multiple: boolean, private readonly basename: (t: T) => string, ) {} public createURI(object: T, extension: string): string { if (object.getURI()) { return object.getURI(); } else if (!this.multiple) { return `${this.basename(object)}.${extension}`; } else { const basename = this.basename(object); this.counter[basename] = this.counter[basename] || 1; return `${basename}_${this.counter[basename]++}.${extension}`; } } } /** Returns the first slot (by name) to which the texture is assigned. */ function getSlot(document: Document, texture: Texture): string { const edge = document .getGraph() .listParentEdges(texture) .find((edge) => edge.getParent() !== document.getRoot()); return edge ? edge.getName().replace(/texture$/i, '') : ''; }