/* eslint-disable camelcase */ // This code is inspired by example code in the DRACO repository import type { Draco3D, DracoInt8Array, Encoder, Mesh, MeshBuilder, PointCloud, Metadata, MetadataBuilder, draco_GeometryAttribute_Type } from '../draco3d/draco3d-types'; import type {TypedArray} from '@loaders.gl/schema'; import type {DracoMesh} from './draco-types'; export type DracoBuildOptions = { pointcloud?: boolean; metadata?: {[key: string]: string}; attributesMetadata?: {}; log?: any; // draco encoding options speed?: [number, number]; method?: string; quantization?: {[attributeName: string]: number}; }; // Native Draco attribute names to GLTF attribute names. const GLTF_TO_DRACO_ATTRIBUTE_NAME_MAP = { POSITION: 'POSITION', NORMAL: 'NORMAL', COLOR_0: 'COLOR', TEXCOORD_0: 'TEX_COORD' }; const noop = () => {}; export default class DracoBuilder { draco: Draco3D; dracoEncoder: Encoder; dracoMeshBuilder: MeshBuilder; dracoMetadataBuilder: MetadataBuilder; log: any; // draco - the draco decoder, either import `draco3d` or load dynamically constructor(draco: Draco3D) { this.draco = draco; this.dracoEncoder = new this.draco.Encoder(); this.dracoMeshBuilder = new this.draco.MeshBuilder(); this.dracoMetadataBuilder = new this.draco.MetadataBuilder(); } destroy(): void { this.destroyEncodedObject(this.dracoMeshBuilder); this.destroyEncodedObject(this.dracoEncoder); this.destroyEncodedObject(this.dracoMetadataBuilder); // @ts-ignore this.dracoMeshBuilder = null; // @ts-ignore this.dracoEncoder = null; // @ts-ignore this.draco = null; } // TBD - when does this need to be called? destroyEncodedObject(object): void { if (object) { this.draco.destroy(object); } } /** * Encode mesh or point cloud * @param mesh =({}) * @param options */ encodeSync(mesh: DracoMesh, options: DracoBuildOptions = {}): ArrayBuffer { this.log = noop; // TODO this._setOptions(options); return options.pointcloud ? this._encodePointCloud(mesh, options) : this._encodeMesh(mesh, options); } // PRIVATE _getAttributesFromMesh(mesh: DracoMesh) { // TODO - Change the encodePointCloud interface instead? const attributes = {...mesh, ...mesh.attributes}; // Fold indices into the attributes if (mesh.indices) { attributes.indices = mesh.indices; } return attributes; } _encodePointCloud(pointcloud: DracoMesh, options: DracoBuildOptions): ArrayBuffer { const dracoPointCloud = new this.draco.PointCloud(); if (options.metadata) { this._addGeometryMetadata(dracoPointCloud, options.metadata); } const attributes = this._getAttributesFromMesh(pointcloud); // Build a `DracoPointCloud` from the input data this._createDracoPointCloud(dracoPointCloud, attributes, options); const dracoData = new this.draco.DracoInt8Array(); try { const encodedLen = this.dracoEncoder.EncodePointCloudToDracoBuffer( dracoPointCloud, false, dracoData ); if (!(encodedLen > 0)) { throw new Error('Draco encoding failed.'); } this.log(`DRACO encoded ${dracoPointCloud.num_points()} points with ${dracoPointCloud.num_attributes()} attributes into ${encodedLen} bytes`); return dracoInt8ArrayToArrayBuffer(dracoData); } finally { this.destroyEncodedObject(dracoData); this.destroyEncodedObject(dracoPointCloud); } } _encodeMesh(mesh: DracoMesh, options: DracoBuildOptions): ArrayBuffer { const dracoMesh = new this.draco.Mesh(); if (options.metadata) { this._addGeometryMetadata(dracoMesh, options.metadata); } const attributes = this._getAttributesFromMesh(mesh); // Build a `DracoMesh` from the input data this._createDracoMesh(dracoMesh, attributes, options); const dracoData = new this.draco.DracoInt8Array(); try { const encodedLen = this.dracoEncoder.EncodeMeshToDracoBuffer(dracoMesh, dracoData); if (encodedLen <= 0) { throw new Error('Draco encoding failed.'); } this.log(`DRACO encoded ${dracoMesh.num_points()} points with ${dracoMesh.num_attributes()} attributes into ${encodedLen} bytes`); return dracoInt8ArrayToArrayBuffer(dracoData); } finally { this.destroyEncodedObject(dracoData); this.destroyEncodedObject(dracoMesh); } } /** * Set encoding options. * @param {{speed?: any; method?: any; quantization?: any;}} options */ _setOptions(options: DracoBuildOptions): void { if ('speed' in options) { // @ts-ignore this.dracoEncoder.SetSpeedOptions(...options.speed); } if ('method' in options) { const dracoMethod = this.draco[options.method || 'MESH_SEQUENTIAL_ENCODING']; // assert(dracoMethod) this.dracoEncoder.SetEncodingMethod(dracoMethod); } if ('quantization' in options) { for (const attribute in options.quantization) { const bits = options.quantization[attribute]; const dracoPosition = this.draco[attribute]; this.dracoEncoder.SetAttributeQuantization(dracoPosition, bits); } } } /** * @param {Mesh} dracoMesh * @param {object} attributes * @returns {Mesh} */ _createDracoMesh(dracoMesh: Mesh, attributes, options: DracoBuildOptions): Mesh { const optionalMetadata = options.attributesMetadata || {}; try { const positions = this._getPositionAttribute(attributes); if (!positions) { throw new Error('positions'); } const vertexCount = positions.length / 3; for (let attributeName in attributes) { const attribute = attributes[attributeName]; attributeName = GLTF_TO_DRACO_ATTRIBUTE_NAME_MAP[attributeName] || attributeName; const uniqueId = this._addAttributeToMesh(dracoMesh, attributeName, attribute, vertexCount); if (uniqueId !== -1) { this._addAttributeMetadata(dracoMesh, uniqueId, { name: attributeName, ...(optionalMetadata[attributeName] || {}) }); } } } catch (error) { this.destroyEncodedObject(dracoMesh); throw error; } return dracoMesh; } /** * @param {} dracoPointCloud * @param {object} attributes */ _createDracoPointCloud( dracoPointCloud: PointCloud, attributes: object, options: DracoBuildOptions ): PointCloud { const optionalMetadata = options.attributesMetadata || {}; try { const positions = this._getPositionAttribute(attributes); if (!positions) { throw new Error('positions'); } const vertexCount = positions.length / 3; for (let attributeName in attributes) { const attribute = attributes[attributeName]; attributeName = GLTF_TO_DRACO_ATTRIBUTE_NAME_MAP[attributeName] || attributeName; const uniqueId = this._addAttributeToMesh( dracoPointCloud, attributeName, attribute, vertexCount ); if (uniqueId !== -1) { this._addAttributeMetadata(dracoPointCloud, uniqueId, { name: attributeName, ...(optionalMetadata[attributeName] || {}) }); } } } catch (error) { this.destroyEncodedObject(dracoPointCloud); throw error; } return dracoPointCloud; } /** * @param mesh * @param attributeName * @param attribute * @param vertexCount */ _addAttributeToMesh( mesh: PointCloud, attributeName: string, attribute: TypedArray, vertexCount: number ): number { if (!ArrayBuffer.isView(attribute)) { return -1; } const type = this._getDracoAttributeType(attributeName); // @ts-ignore TODO/fix types const size = attribute.length / vertexCount; if (type === 'indices') { // @ts-ignore TODO/fix types const numFaces = attribute.length / 3; this.log(`Adding attribute ${attributeName}, size ${numFaces}`); // @ts-ignore assumes mesh is a Mesh, not a point cloud this.dracoMeshBuilder.AddFacesToMesh(mesh, numFaces, attribute); return -1; } this.log(`Adding attribute ${attributeName}, size ${size}`); const builder = this.dracoMeshBuilder; const {buffer} = attribute; switch (attribute.constructor) { case Int8Array: return builder.AddInt8Attribute(mesh, type, vertexCount, size, new Int8Array(buffer)); case Int16Array: return builder.AddInt16Attribute(mesh, type, vertexCount, size, new Int16Array(buffer)); case Int32Array: return builder.AddInt32Attribute(mesh, type, vertexCount, size, new Int32Array(buffer)); case Uint8Array: case Uint8ClampedArray: return builder.AddUInt8Attribute(mesh, type, vertexCount, size, new Uint8Array(buffer)); case Uint16Array: return builder.AddUInt16Attribute(mesh, type, vertexCount, size, new Uint16Array(buffer)); case Uint32Array: return builder.AddUInt32Attribute(mesh, type, vertexCount, size, new Uint32Array(buffer)); case Float32Array: return builder.AddFloatAttribute(mesh, type, vertexCount, size, new Float32Array(buffer)); default: // eslint-disable-next-line no-console console.warn('Unsupported attribute type', attribute); return -1; } // case Float64Array: // Add attribute does not seem to be exposed // return builder.AddAttribute(mesh, type, vertexCount, size, new Float32Array(buffer)); } /** * DRACO can compress attributes of know type better * TODO - expose an attribute type map? * @param attributeName */ _getDracoAttributeType(attributeName: string): draco_GeometryAttribute_Type | 'indices' { switch (attributeName.toLowerCase()) { case 'indices': return 'indices'; case 'position': case 'positions': case 'vertices': return this.draco.POSITION; case 'normal': case 'normals': return this.draco.NORMAL; case 'color': case 'colors': return this.draco.COLOR; case 'texcoord': case 'texcoords': return this.draco.TEX_COORD; default: return this.draco.GENERIC; } } _getPositionAttribute(attributes) { for (const attributeName in attributes) { const attribute = attributes[attributeName]; const dracoType = this._getDracoAttributeType(attributeName); if (dracoType === this.draco.POSITION) { return attribute; } } return null; } /** * Add metadata for the geometry. * @param dracoGeometry - WASM Draco Object * @param metadata */ _addGeometryMetadata(dracoGeometry: PointCloud, metadata: {[key: string]: string}) { const dracoMetadata = new this.draco.Metadata(); this._populateDracoMetadata(dracoMetadata, metadata); this.dracoMeshBuilder.AddMetadata(dracoGeometry, dracoMetadata); } /** * Add metadata for an attribute to geometry. * @param dracoGeometry - WASM Draco Object * @param uniqueAttributeId * @param metadata */ _addAttributeMetadata( dracoGeometry: PointCloud, uniqueAttributeId: number, metadata: Map | {[key: string]: string} ) { // Note: Draco JS IDL doesn't seem to expose draco.AttributeMetadata, however it seems to // create such objects automatically from draco.Metadata object. const dracoAttributeMetadata = new this.draco.Metadata(); this._populateDracoMetadata(dracoAttributeMetadata, metadata); // Draco3d doc note: Directly add attribute metadata to geometry. // You can do this without explicitly adding |GeometryMetadata| to mesh. this.dracoMeshBuilder.SetMetadataForAttribute( dracoGeometry, uniqueAttributeId, dracoAttributeMetadata ); } /** * Add contents of object or map to a WASM Draco Metadata Object * @param dracoMetadata - WASM Draco Object * @param metadata */ _populateDracoMetadata( dracoMetadata: Metadata, metadata: Map | {[key: string]: string} ) { for (const [key, value] of getEntries(metadata)) { switch (typeof value) { case 'number': if (Math.trunc(value) === value) { this.dracoMetadataBuilder.AddIntEntry(dracoMetadata, key, value); } else { this.dracoMetadataBuilder.AddDoubleEntry(dracoMetadata, key, value); } break; case 'object': if (value instanceof Int32Array) { this.dracoMetadataBuilder.AddIntEntryArray(dracoMetadata, key, value, value.length); } break; case 'string': default: this.dracoMetadataBuilder.AddStringEntry(dracoMetadata, key, value); } } } } // HELPER FUNCTIONS /** * Copy encoded data to buffer * @param dracoData */ function dracoInt8ArrayToArrayBuffer(dracoData: DracoInt8Array) { const byteLength = dracoData.size(); const outputBuffer = new ArrayBuffer(byteLength); const outputData = new Int8Array(outputBuffer); for (let i = 0; i < byteLength; ++i) { outputData[i] = dracoData.GetValue(i); } return outputBuffer; } /** Enable iteration over either an object or a map */ function getEntries(container) { const hasEntriesFunc = container.entries && !container.hasOwnProperty('entries'); return hasEntriesFunc ? container.entries() : Object.entries(container); }