From d8e5280da7d2e311e4d21b9e9970ee2d4afb5ef1 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:44:51 -0500 Subject: [PATCH 01/32] Factor encodeMesh API for glTF --- .../Compression/dracoCompressionWorker.ts | 4 +- .../src/Meshes/Compression/dracoEncoder.ts | 50 ++++++++++++------- .../Meshes/Compression/dracoEncoder.types.ts | 4 +- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/dev/core/src/Meshes/Compression/dracoCompressionWorker.ts b/packages/dev/core/src/Meshes/Compression/dracoCompressionWorker.ts index 8f7a48edb24..7d455042453 100644 --- a/packages/dev/core/src/Meshes/Compression/dracoCompressionWorker.ts +++ b/packages/dev/core/src/Meshes/Compression/dracoCompressionWorker.ts @@ -34,7 +34,7 @@ export function EncodeMesh( const attributeIDs: Record = {}; // Babylon kind -> Draco unique id // Double-check that at least a position attribute is provided - const positionAttribute = attributes.find((a) => a.babylonAttribute === "position"); + const positionAttribute = attributes.find((a) => a.dracoAttribute === "POSITION"); if (!positionAttribute) { throw new Error("Position attribute is required for Draco encoding"); } @@ -61,7 +61,7 @@ export function EncodeMesh( // Add the attributes for (const attribute of attributes) { const verticesCount = attribute.data.length / attribute.size; - attributeIDs[attribute.babylonAttribute] = meshBuilder.AddFloatAttribute(mesh, encoderModule[attribute.dracoAttribute], verticesCount, attribute.size, attribute.data); + attributeIDs[attribute.attribute] = meshBuilder.AddFloatAttribute(mesh, encoderModule[attribute.dracoAttribute], verticesCount, attribute.size, attribute.data); if (options.quantizationBits && options.quantizationBits[attribute.dracoAttribute]) { encoder.SetAttributeQuantization(encoderModule[attribute.dracoAttribute], options.quantizationBits[attribute.dracoAttribute]); } diff --git a/packages/dev/core/src/Meshes/Compression/dracoEncoder.ts b/packages/dev/core/src/Meshes/Compression/dracoEncoder.ts index 4cf834b432f..a6d891e01b4 100644 --- a/packages/dev/core/src/Meshes/Compression/dracoEncoder.ts +++ b/packages/dev/core/src/Meshes/Compression/dracoEncoder.ts @@ -73,7 +73,7 @@ function PrepareAttributesForDraco(input: Mesh | Geometry, excludedAttributes?: if (!(data instanceof Float32Array)) { data = Float32Array.from(data!); } - attributes.push({ babylonAttribute: kind, dracoAttribute: GetDracoAttributeName(kind), size: input.getVertexBuffer(kind)!.getSize(), data: data }); + attributes.push({ attribute: kind, dracoAttribute: GetDracoAttributeName(kind), size: input.getVertexBuffer(kind)!.getSize(), data: data }); } return attributes; @@ -202,26 +202,14 @@ export class DracoEncoder extends DracoCodec { } /** - * Encodes a mesh or geometry into a Draco-encoded mesh data. - * @param input the mesh or geometry to encode - * @param options options for the encoding - * @returns a promise that resolves to the newly-encoded data + * @internal */ - public async encodeMeshAsync(input: Mesh | Geometry, options?: IDracoEncoderOptions): Promise> { - const verticesCount = input.getTotalVertices(); - if (verticesCount == 0) { - throw new Error("Cannot compress geometry with Draco. There are no vertices."); - } - - // Prepare parameters for encoding + public async _encodeAsync( + attributes: Array, + indices: Nullable, + options?: IDracoEncoderOptions + ): Promise> { const mergedOptions = options ? deepMerge(DefaultEncoderOptions, options) : DefaultEncoderOptions; - if (input instanceof Mesh && input.morphTargetManager && mergedOptions.method === "MESH_EDGEBREAKER_ENCODING") { - Logger.Warn("Cannot use Draco EDGEBREAKER method with morph targets. Falling back to SEQUENTIAL method."); - mergedOptions.method = "MESH_SEQUENTIAL_ENCODING"; - } - - let indices = PrepareIndicesForDraco(input); - const attributes = PrepareAttributesForDraco(input, mergedOptions.excludedAttributes); if (this._workerPoolPromise) { const workerPool = await this._workerPoolPromise; @@ -269,4 +257,28 @@ export class DracoEncoder extends DracoCodec { throw new Error("Draco encoder module is not available"); } + + /** + * Encodes a mesh or geometry into a Draco-encoded mesh data. + * @param input the mesh or geometry to encode + * @param options options for the encoding + * @returns a promise that resolves to the newly-encoded data + */ + public async encodeMeshAsync(input: Mesh | Geometry, options?: IDracoEncoderOptions): Promise> { + const verticesCount = input.getTotalVertices(); + if (verticesCount == 0) { + throw new Error("Cannot compress geometry with Draco. There are no vertices."); + } + + // Prepare parameters for encoding + if (input instanceof Mesh && input.morphTargetManager && options?.method === "MESH_EDGEBREAKER_ENCODING") { + Logger.Warn("Cannot use Draco EDGEBREAKER method with morph targets. Falling back to SEQUENTIAL method."); + options.method = "MESH_SEQUENTIAL_ENCODING"; + } + + const indices = PrepareIndicesForDraco(input); + const attributes = PrepareAttributesForDraco(input, options?.excludedAttributes); + + return this._encodeAsync(attributes, indices, options); + } } diff --git a/packages/dev/core/src/Meshes/Compression/dracoEncoder.types.ts b/packages/dev/core/src/Meshes/Compression/dracoEncoder.types.ts index 410959c0c37..31ccc2f5d8a 100644 --- a/packages/dev/core/src/Meshes/Compression/dracoEncoder.types.ts +++ b/packages/dev/core/src/Meshes/Compression/dracoEncoder.types.ts @@ -42,9 +42,9 @@ export interface IDracoEncoderOptions { */ export interface IDracoAttributeData { /** - * The Babylon kind of the attribute. + * The kind of the attribute. */ - babylonAttribute: string; + attribute: string; /** * The Draco kind to use for the attribute. */ From 78627425d2e3610b8e59e6bbbfd9a46df53aabf9 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:02:54 -0500 Subject: [PATCH 02/32] Add KHR_draco_mesh_compression and centralize buffer management in separate class --- packages/dev/core/src/types.ts | 2 + .../2.0/Extensions/EXT_mesh_gpu_instancing.ts | 59 ++-- .../Extensions/KHR_draco_mesh_compression.ts | 166 ++++++++++ .../src/glTF/2.0/Extensions/index.ts | 1 + .../serializers/src/glTF/2.0/dataWriter.ts | 299 +++++++++++++----- .../serializers/src/glTF/2.0/glTFAnimation.ts | 71 +---- .../serializers/src/glTF/2.0/glTFExporter.ts | 257 ++++++--------- .../src/glTF/2.0/glTFExporterExtension.ts | 14 +- .../src/glTF/2.0/glTFMaterialExporter.ts | 43 ++- .../src/glTF/2.0/glTFMorphTargetsUtilities.ts | 48 ++- .../src/glTF/2.0/glTFSerializer.ts | 14 +- .../serializers/src/glTF/2.0/glTFUtilities.ts | 89 ++---- 12 files changed, 629 insertions(+), 434 deletions(-) create mode 100644 packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts diff --git a/packages/dev/core/src/types.ts b/packages/dev/core/src/types.ts index 9e089be6998..44ab40ca98b 100644 --- a/packages/dev/core/src/types.ts +++ b/packages/dev/core/src/types.ts @@ -136,6 +136,8 @@ export type Tuple = _Tuple; export type FloatArray = number[] | Float32Array; /** Alias type for number array or Float32Array or Int32Array or Uint32Array or Uint16Array */ export type IndicesArray = number[] | Int32Array | Uint32Array | Uint16Array; +/** Union of all TypedArrays up to 32 bits */ +export type TypedArray = Float32Array | Uint32Array | Uint16Array | Uint8Array | Uint8ClampedArray | Int32Array | Int16Array | Int8Array; /** * Alias for types that can be used by a Buffer or VertexBuffer. diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts index 31a23974752..8b87b85c048 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts @@ -1,4 +1,4 @@ -import type { IBufferView, IAccessor, INode, IEXTMeshGpuInstancing } from "babylonjs-gltf2interface"; +import type { INode, IEXTMeshGpuInstancing } from "babylonjs-gltf2interface"; import { AccessorType, AccessorComponentType } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; import type { DataWriter } from "../dataWriter"; @@ -8,7 +8,6 @@ import type { Node } from "core/node"; import { Mesh } from "core/Meshes/mesh"; import "core/Meshes/thinInstanceMesh"; import { TmpVectors, Quaternion, Vector3 } from "core/Maths/math.vector"; -import { VertexBuffer } from "core/Buffers/buffer"; import { ConvertToRightHandedPosition, ConvertToRightHandedRotation } from "../glTFUtilities"; const NAME = "EXT_mesh_gpu_instancing"; @@ -49,7 +48,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { * @param babylonNode the corresponding babylon node * @param nodeMap map from babylon node id to node index * @param convertToRightHanded true if we need to convert data from left hand to right hand system. - * @param dataWriter binary writer + * @param dataManager binary writer * @returns nullable promise, resolves with the node */ public postExportNodeAsync( @@ -58,7 +57,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { babylonNode: Node, nodeMap: Map, convertToRightHanded: boolean, - dataWriter: DataWriter + dataManager: DataWriter ): Promise> { return new Promise((resolve) => { if (node && babylonNode instanceof Mesh) { @@ -117,18 +116,24 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { translationBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, - dataWriter, + dataManager, AccessorComponentType.FLOAT ); } // do we need to write ROTATION ? if (hasAnyInstanceWorldRotation) { const componentType = AccessorComponentType.FLOAT; // we decided to stay on FLOAT for now see https://github.com/BabylonJS/Babylon.js/pull/12495 - extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, dataWriter, componentType); + extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, dataManager, componentType); } // do we need to write SCALE ? if (hasAnyInstanceWorldScale) { - extension.attributes["SCALE"] = this._buildAccessor(scaleBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, dataWriter, AccessorComponentType.FLOAT); + extension.attributes["SCALE"] = this._buildAccessor( + scaleBuffer, + AccessorType.VEC3, + babylonNode.thinInstanceCount, + dataManager, + AccessorComponentType.FLOAT + ); } /* eslint-enable @typescript-eslint/naming-convention*/ @@ -140,46 +145,50 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { }); } - private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, binaryWriter: DataWriter, componentType: AccessorComponentType): number { + private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, dataManager: DataWriter, componentType: AccessorComponentType): number { // write the buffer - const bufferOffset = binaryWriter.byteOffset; + let data; switch (componentType) { case AccessorComponentType.FLOAT: { + data = new Float32Array(buffer.length); for (let i = 0; i != buffer.length; i++) { - binaryWriter.writeFloat32(buffer[i]); + data[i] = buffer[i]; } break; } case AccessorComponentType.BYTE: { + data = new Int8Array(buffer.length); for (let i = 0; i != buffer.length; i++) { - binaryWriter.writeInt8(buffer[i] * 127); + data[i] = buffer[i] * 127; } break; } case AccessorComponentType.SHORT: { + data = new Int16Array(buffer.length); for (let i = 0; i != buffer.length; i++) { - binaryWriter.writeInt16(buffer[i] * 32767); + data[i] = buffer[i] * 32767; } - break; } + default: { + throw new Error(`Unsupported componentType ${componentType}`); + } } // build the buffer view - const bv: IBufferView = { buffer: 0, byteOffset: bufferOffset, byteLength: buffer.length * VertexBuffer.GetTypeByteLength(componentType) }; - const bufferViewIndex = this._exporter._bufferViews.length; - this._exporter._bufferViews.push(bv); + const bv = dataManager.createBufferView(data); // finally build the accessor - const accessorIndex = this._exporter._accessors.length; - const accessor: IAccessor = { - bufferView: bufferViewIndex, - componentType: componentType, - count: count, - type: type, - normalized: componentType == AccessorComponentType.BYTE || componentType == AccessorComponentType.SHORT, - }; + const accessor = dataManager.createAccessor( + bv, + type, + componentType, + count, + undefined, + undefined, + componentType == AccessorComponentType.BYTE || componentType == AccessorComponentType.SHORT + ); this._exporter._accessors.push(accessor); - return accessorIndex; + return this._exporter._accessors.length - 1; } } diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts new file mode 100644 index 00000000000..d000a932731 --- /dev/null +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -0,0 +1,166 @@ +import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; +import { GLTFExporter } from "../glTFExporter"; +import { MeshPrimitiveMode } from "babylonjs-gltf2interface"; +import type { IAccessor, IBufferView, IKHRDracoMeshCompression, IMeshPrimitive } from "babylonjs-gltf2interface"; +import type { DataWriter } from "../dataWriter"; +import { DracoEncoder } from "core/Meshes/Compression/dracoEncoder"; +import { GetFloatData, GetTypeByteLength } from "core/Buffers/bufferUtils"; +import { GetAccessorElementCount } from "../glTFUtilities"; +import type { DracoAttributeName, IDracoAttributeData, IDracoEncoderOptions } from "core/Meshes/Compression/dracoEncoder.types"; +import { Logger } from "core/Misc/logger"; +import type { Nullable } from "core/types"; + +const NAME = "KHR_draco_mesh_compression"; + +function getDracoAttributeName(glTFName: string): DracoAttributeName { + if (glTFName === "POSITION") { + return "POSITION"; + } else if (glTFName === "NORMAL") { + return "NORMAL"; + } else if (glTFName.startsWith("COLOR")) { + return "COLOR"; + } else if (glTFName.startsWith("TEXCOORD")) { + return "TEX_COORD"; + } + return "GENERIC"; +} + +/** + * [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_draco_mesh_compression/README.md) + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { + /** Name of this extension */ + public readonly name = NAME; + + /** Defines whether this extension is enabled */ + public enabled; + + /** KHR_draco_mesh_compression is required, as uncompressed fallback data is not yet implemented. */ + public required = true; + + /** BufferViews used for Draco data, which may be eligible for removal after Draco encoding */ + private _bufferViewsUsed: Set = new Set(); + + /** Accessors that were replaced with Draco data, which may be eligible for removal after Draco encoding */ + private _accessorsUsed: Set = new Set(); + + private _wasUsed = false; + + /** @internal */ + public get wasUsed() { + return this._wasUsed; + } + + /** @internal */ + constructor(exporter: GLTFExporter) { + this.enabled = exporter.options.useDracoCompression && DracoEncoder.DefaultAvailable; + } + + /** @internal */ + public dispose() { + this._wasUsed = false; + DracoEncoder.ResetDefault(); + } + + /** @internal */ + public onExporting(): void { + this.dispose(); + } + + /** @internal */ + public async postExportMeshPrimitiveAsync(primitive: IMeshPrimitive, dataManager: DataWriter, accessors: IAccessor[]): Promise { + if (!this.enabled) { + return primitive; + } + + if (primitive.mode !== MeshPrimitiveMode.TRIANGLES && primitive.mode !== MeshPrimitiveMode.TRIANGLE_STRIP) { + Logger.Warn("Cannot compress primitive with mode " + primitive.mode + "."); + return primitive; + } + + // Prepare indices for Draco encoding + let indices: Nullable = null; + if (primitive.indices !== undefined) { + const accessor = accessors[primitive.indices]; + const bufferView = dataManager.getBufferView(accessor); + this._bufferViewsUsed.add(bufferView); + this._accessorsUsed.add(accessor); + // Per exportIndices, indices must be either Uint16Array or Uint32Array + indices = dataManager.getData(bufferView) as Uint32Array | Uint16Array; + } + + // Prepare attributes for Draco encoding + const attributes: IDracoAttributeData[] = []; + for (const [name, accessorIndex] of Object.entries(primitive.attributes)) { + const accessor = accessors[accessorIndex]; + const bufferView = dataManager.getBufferView(accessor); + this._bufferViewsUsed.add(bufferView); + this._accessorsUsed.add(accessor); + const data = dataManager.getData(bufferView); + + const size = GetAccessorElementCount(accessor.type); + // TODO: In future, find a way to preserve original data type to avoid copying in some cases + const floatData = GetFloatData( + data, + size, + accessor.componentType, + accessor.byteOffset || 0, + bufferView.byteStride || GetTypeByteLength(accessor.componentType) * size, + accessor.normalized || false, + accessor.count + ) as Float32Array; // Because data is a TypedArray, GetFloatData will return a Float32Array + + attributes.push({ attribute: name, dracoAttribute: getDracoAttributeName(name), size: GetAccessorElementCount(accessor.type), data: floatData }); + } + + // Use sequential encoding to preserve vertex order for cases like morph targets + const options: IDracoEncoderOptions = { + method: primitive.targets ? "MESH_SEQUENTIAL_ENCODING" : "MESH_EDGEBREAKER_ENCODING", + }; + + const encodedData = await DracoEncoder.Default._encodeAsync(attributes, indices, options); + if (!encodedData) { + return primitive; // Draco encoding failed + } + + const dracoInfo: IKHRDracoMeshCompression = { + bufferView: -1, // bufferView will be set to a real index later, when we write the binary and decide bufferView ordering + attributes: encodedData.attributeIDs, + }; + const bufferView = dataManager.createBufferView(encodedData.data); + dataManager.setBufferView(dracoInfo, bufferView); + + primitive.extensions ||= {}; + primitive.extensions[NAME] = dracoInfo; + + this._wasUsed = true; + + return primitive; // TODO: Why return this? No need. + } + + /** @internal */ + public async preGenerateBinaryAsync(dataManager: DataWriter): Promise { + if (!this.enabled) { + return; + } + + // Cull obsolete bufferViews that are no longer needed, as they were replaced with Draco data + for (const bufferView of this._bufferViewsUsed) { + const references = dataManager.getProperties(bufferView); + + const bufferViewOnlyUsedByDraco = references.every((object) => { + return this._accessorsUsed.has(object as IAccessor); // has() can handle any object, but TS doesn't know that + }); + + if (bufferViewOnlyUsedByDraco) { + dataManager.removeBufferView(bufferView); + } + } + + this._bufferViewsUsed.clear(); + this._accessorsUsed.clear(); + } +} + +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_draco_mesh_compression(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/index.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/index.ts index 57c28d537f0..b6faee955af 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/index.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/index.ts @@ -1,4 +1,5 @@ export * from "./EXT_mesh_gpu_instancing"; +export * from "./KHR_draco_mesh_compression"; export * from "./KHR_lights_punctual"; export * from "./KHR_materials_anisotropy"; export * from "./KHR_materials_clearcoat"; diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts index a37b9b81dc5..a4b88935a3a 100644 --- a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -1,80 +1,219 @@ -/* eslint-disable babylonjs/available */ - -/** @internal */ -export class DataWriter { - private _data: Uint8Array; - private _dataView: DataView; - private _byteOffset: number; - - public constructor(byteLength: number) { - this._data = new Uint8Array(byteLength); - this._dataView = new DataView(this._data.buffer); - this._byteOffset = 0; - } - - public get byteOffset(): number { - return this._byteOffset; - } - - public getOutputData(): Uint8Array { - return new Uint8Array(this._data.buffer, 0, this._byteOffset); - } - - public writeUInt8(value: number): void { - this._checkGrowBuffer(1); - this._dataView.setUint8(this._byteOffset, value); - this._byteOffset++; - } - - public writeInt8(value: number): void { - this._checkGrowBuffer(1); - this._dataView.setInt8(this._byteOffset, value); - this._byteOffset++; - } - - public writeInt16(entry: number): void { - this._checkGrowBuffer(2); - this._dataView.setInt16(this._byteOffset, entry, true); - this._byteOffset += 2; - } - - public writeUInt16(value: number): void { - this._checkGrowBuffer(2); - this._dataView.setUint16(this._byteOffset, value, true); - this._byteOffset += 2; - } - - public writeUInt32(entry: number): void { - this._checkGrowBuffer(4); - this._dataView.setUint32(this._byteOffset, entry, true); - this._byteOffset += 4; - } - - public writeFloat32(value: number): void { - this._checkGrowBuffer(4); - this._dataView.setFloat32(this._byteOffset, value, true); - this._byteOffset += 4; - } - - public writeUint8Array(value: Uint8Array): void { - this._checkGrowBuffer(value.byteLength); - this._data.set(value, this._byteOffset); - this._byteOffset += value.byteLength; - } - - public writeUint16Array(value: Uint16Array): void { - this._checkGrowBuffer(value.byteLength); - this._data.set(value, this._byteOffset); - this._byteOffset += value.byteLength; - } - - private _checkGrowBuffer(byteLength: number): void { - const newByteLength = this.byteOffset + byteLength; - if (newByteLength > this._data.byteLength) { - const newData = new Uint8Array(newByteLength * 2); - newData.set(this._data); - this._data = newData; - this._dataView = new DataView(this._data.buffer); - } - } -} +import type { TypedArray } from "core/types"; +import type { AccessorComponentType, AccessorType, IAccessor, IBufferView } from "babylonjs-gltf2interface"; + +type IPropertyWithBufferView = { + bufferView?: number; + [key: string]: any; +}; + +/** + * Utility class to centralize the management of binary data, bufferViews, and the objects that reference them. + * TODO: Rename to BufferManager..? + * @internal + */ +export class DataWriter { + // BufferView -> data + private _bufferViewToData: Map = new Map(); + + // BufferView -> glTF objects + private _bufferViewToProperties: Map = new Map(); + + // Accessor -> bufferView + private _accessorToBufferView: Map = new Map(); + + /** + * Generates a binary buffer from the stored bufferViews. Also creates in the bufferViews list. + * @param bufferViews The list of bufferViews to be populated while writing the binary + * @returns The binary buffer + */ + public generateBinary(bufferViews: IBufferView[]): Uint8Array { + // Allocate the ArrayBuffer + let totalByteLength = 0; + for (const bufferView of this._bufferViewToData.keys()) { + bufferView.byteOffset = totalByteLength; + totalByteLength += bufferView.byteLength; + } + const buffer = new ArrayBuffer(totalByteLength); + const dataView = new DataView(buffer); // To write in little endian + + // Fill in the bufferViews list and missing bufferView index references while writing the binary + let byteOffset = 0; + for (const [bufferView, data] of this._bufferViewToData.entries()) { + bufferView.byteOffset = byteOffset; + bufferViews.push(bufferView); + + const bufferViewIndex = bufferViews.length - 1; + const properties = this._bufferViewToProperties.get(bufferView)!; + for (const object of properties) { + object.bufferView = bufferViewIndex; + } + + const type = data.constructor.name; + for (let i = 0; i < data.length; i++) { + const value = data[i]; + switch (type) { + case "Int8Array": + dataView.setInt8(byteOffset, value); + byteOffset += 1; + break; + case "Uint8Array": + dataView.setUint8(byteOffset, value); + byteOffset += 1; + break; + case "Int16Array": + dataView.setInt16(byteOffset, value, true); + byteOffset += 2; + break; + case "Uint16Array": + dataView.setUint16(byteOffset, value, true); + byteOffset += 2; + break; + case "Int32Array": + dataView.setInt32(byteOffset, value, true); + byteOffset += 4; + break; + case "Uint32Array": + dataView.setUint32(byteOffset, value, true); + byteOffset += 4; + break; + case "Float32Array": + dataView.setFloat32(byteOffset, value, true); + byteOffset += 4; + break; + case "Float64Array": + dataView.setFloat64(byteOffset, value, true); + byteOffset += 8; + break; + default: + throw new Error("Unsupported TypedArray type: " + type); + } + } + + this._bufferViewToData.delete(bufferView); + } + + return new Uint8Array(buffer, 0, byteOffset); + } + + /** + * Creates a buffer view based on the supplied arguments + * @param data a TypedArray to create the bufferView for + * @param byteStride byte distance between consecutive elements + * @returns bufferView for glTF + */ + public createBufferView(data: TypedArray, byteStride?: number): IBufferView { + const bufferView: IBufferView = { + buffer: 0, + byteOffset: undefined, // byteOffset will be set later, when we write the binary and decide bufferView ordering + byteLength: data.byteLength, + byteStride: byteStride, + }; + this._bufferViewToData.set(bufferView, data); + return bufferView; + } + + /** + * Creates an accessor based on the supplied arguments and assigns it to the bufferView + * @param bufferView The glTF bufferView referenced by this accessor + * @param type The type of the accessor + * @param componentType The datatype of components in the attribute + * @param count The number of attributes referenced by this accessor + * @param byteOffset The offset relative to the start of the bufferView in bytes + * @param minMax Minimum and maximum value of each component in this attribute + * @param normalized Specifies whether integer data values are normalized before usage + * @returns accessor for glTF + */ + public createAccessor( + bufferView: IBufferView, + type: AccessorType, + componentType: AccessorComponentType, + count: number, + byteOffset?: number, + minMax?: { min: number[]; max: number[] }, + normalized?: boolean + ): IAccessor { + if (!this._bufferViewToData.has(bufferView)) { + throw new Error(`BufferView ${bufferView} not found in DataWriter.`); + } + + const accessor: IAccessor = { + bufferView: undefined, // bufferView will be set to a real index later, once we write the binary and decide bufferView ordering + componentType: componentType, + count: count, + type: type, + min: minMax?.min, + max: minMax?.max, + normalized: normalized, + byteOffset: byteOffset, + }; + this.setBufferView(accessor, bufferView); + this._accessorToBufferView.set(accessor, bufferView); + return accessor; + } + + /** + * Assigns a bufferView to a glTF object + * @param object The glTF object + * @param bufferView The bufferView to assign + */ + public setBufferView(object: IPropertyWithBufferView, bufferView: IBufferView) { + if (!this._bufferViewToData.has(bufferView)) { + throw new Error(`BufferView ${bufferView} not found in DataWriter.`); + } + const properties = this._bufferViewToProperties.get(bufferView) ?? []; + properties.push(object); + this._bufferViewToProperties.set(bufferView, properties); + } + + public getBufferView(accessor: IAccessor): IBufferView { + const bufferView = this._accessorToBufferView.get(accessor); + if (!bufferView) { + throw new Error(`Accessor ${accessor} not found in DataWriter.`); + } + return bufferView; + } + + public getProperties(bufferView: IBufferView): IPropertyWithBufferView[] { + return this._bufferViewToProperties.get(bufferView) ?? []; + } + + public getData(bufferView: IBufferView): TypedArray { + const data = this._bufferViewToData.get(bufferView); + if (!data) { + throw new Error(`BufferView ${bufferView} not found in DataWriter.`); + } + return data; + } + + /** + * Removes buffer view from the binary. + * Warning: This will also remove the bufferView info from all object that reference it. + * @param bufferView the bufferView to remove + */ + public removeBufferView(bufferView: IBufferView): void { + if (!this._bufferViewToData.has(bufferView)) { + throw new Error(`BufferView ${bufferView} not found in DataWriter.`); + } + + const properties = this._bufferViewToProperties.get(bufferView); + if (properties) { + for (const object of properties) { + if (object.bufferView) { + delete object.bufferView; + } + } + } + + this._bufferViewToData.delete(bufferView); + this._bufferViewToProperties.delete(bufferView); + this._accessorToBufferView.forEach((bv, accessor) => { + if (bv === bufferView) { + // Additionally, remove byteOffset from accessor referencing this bufferView + if (accessor.byteOffset) { + delete accessor.byteOffset; + } + this._accessorToBufferView.delete(accessor); + } + }); + } +} diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index 84e6afe8714..92dde8156de 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -16,14 +16,7 @@ import { AnimationKeyInterpolation } from "core/Animations/animationKey"; import { Camera } from "core/Cameras/camera"; import { Light } from "core/Lights/light"; import type { DataWriter } from "./dataWriter"; -import { - CreateAccessor, - CreateBufferView, - GetAccessorElementCount, - ConvertToRightHandedPosition, - ConvertCameraRotationToGLTF, - ConvertToRightHandedRotation, -} from "./glTFUtilities"; +import { GetAccessorElementCount, ConvertToRightHandedPosition, ConvertCameraRotationToGLTF, ConvertToRightHandedRotation } from "./glTFUtilities"; /** * @internal @@ -572,7 +565,6 @@ export class _GLTFAnimation { let accessor: IAccessor; let keyframeAccessorIndex: number; let dataAccessorIndex: number; - let outputLength: number; let animationSampler: IAnimationSampler; let animationChannel: IAnimationChannel; @@ -598,51 +590,37 @@ export class _GLTFAnimation { const nodeIndex = nodeMap.get(babylonTransformNode); - // Creates buffer view and accessor for key frames. - let byteLength = animationData.inputs.length * 4; - const offset = binaryWriter.byteOffset; - bufferView = CreateBufferView(0, offset, byteLength); - bufferViews.push(bufferView); - animationData.inputs.forEach(function (input) { - binaryWriter.writeFloat32(input); - }); - - accessor = CreateAccessor(bufferViews.length - 1, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, null, { + // Create buffer view and accessor for key frames. + let data = new Float32Array(animationData.inputs); + bufferView = binaryWriter.createBufferView(data); + accessor = binaryWriter.createAccessor(bufferView, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, undefined, { min: [animationData.inputsMin], max: [animationData.inputsMax], }); - accessors.push(accessor); keyframeAccessorIndex = accessors.length - 1; - // create bufferview and accessor for keyed values. - outputLength = animationData.outputs.length; - byteLength = GetAccessorElementCount(dataAccessorType) * 4 * animationData.outputs.length; - - // check for in and out tangents - bufferView = CreateBufferView(0, binaryWriter.byteOffset, byteLength); - bufferViews.push(bufferView); - + // Convert keyed values into a flat array for writing. const rotationQuaternion = new Quaternion(); const eulerVec3 = new Vector3(); const position = new Vector3(); const isCamera = babylonTransformNode instanceof Camera; - animationData.outputs.forEach(function (output) { + data = new Float32Array(animationData.outputs.length * GetAccessorElementCount(dataAccessorType)); + animationData.outputs.forEach(function (output: number[], index: number) { if (convertToRightHanded) { switch (animationChannelTargetPath) { case AnimationChannelTargetPath.TRANSLATION: Vector3.FromArrayToRef(output, 0, position); ConvertToRightHandedPosition(position); - binaryWriter.writeFloat32(position.x); - binaryWriter.writeFloat32(position.y); - binaryWriter.writeFloat32(position.z); + position.toArray(output); break; case AnimationChannelTargetPath.ROTATION: if (output.length === 4) { Quaternion.FromArrayToRef(output, 0, rotationQuaternion); } else { + // TODO: should be impossible to get here, but just in case Vector3.FromArrayToRef(output, 0, eulerVec3); Quaternion.FromEulerVectorToRef(eulerVec3, rotationQuaternion); } @@ -655,17 +633,7 @@ export class _GLTFAnimation { } } - binaryWriter.writeFloat32(rotationQuaternion.x); - binaryWriter.writeFloat32(rotationQuaternion.y); - binaryWriter.writeFloat32(rotationQuaternion.z); - binaryWriter.writeFloat32(rotationQuaternion.w); - - break; - - default: - output.forEach(function (entry) { - binaryWriter.writeFloat32(entry); - }); + rotationQuaternion.toArray(output); break; } } else { @@ -674,29 +642,24 @@ export class _GLTFAnimation { if (output.length === 4) { Quaternion.FromArrayToRef(output, 0, rotationQuaternion); } else { + // TODO: should be impossible to get here, but just in case Vector3.FromArrayToRef(output, 0, eulerVec3); Quaternion.FromEulerVectorToRef(eulerVec3, rotationQuaternion); } if (isCamera) { ConvertCameraRotationToGLTF(rotationQuaternion); } - binaryWriter.writeFloat32(rotationQuaternion.x); - binaryWriter.writeFloat32(rotationQuaternion.y); - binaryWriter.writeFloat32(rotationQuaternion.z); - binaryWriter.writeFloat32(rotationQuaternion.w); - - break; - default: - output.forEach(function (entry) { - binaryWriter.writeFloat32(entry); - }); + rotationQuaternion.toArray(output); break; } } + data.set(output, index * GetAccessorElementCount(dataAccessorType)); }); - accessor = CreateAccessor(bufferViews.length - 1, dataAccessorType, AccessorComponentType.FLOAT, outputLength, null); + // Create buffer view and accessor for keyed values. + bufferView = binaryWriter.createBufferView(data); + accessor = binaryWriter.createAccessor(bufferView, dataAccessorType, AccessorComponentType.FLOAT, animationData.outputs.length); accessors.push(accessor); dataAccessorIndex = accessors.length - 1; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index ee82adb580c..7d3a03e61ae 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -42,14 +42,11 @@ import { GLTFData } from "./glTFData"; import { ConvertToRightHandedPosition, ConvertToRightHandedRotation, - CreateAccessor, - CreateBufferView, DataArrayToUint8Array, GetAccessorType, GetAttributeType, GetMinMax, GetPrimitiveMode, - IndicesArrayToUint8Array, IsNoopNode, IsTriangleFillMode, IsParentAddedByImporter, @@ -57,6 +54,7 @@ import { RotateNode180Y, FloatsNeed16BitInteger, IsStandardVertexAttribute, + IndicesArrayToTypedArray, } from "./glTFUtilities"; import { DataWriter } from "./dataWriter"; import { Camera } from "core/Cameras/camera"; @@ -77,13 +75,13 @@ class ExporterState { // Babylon indices array, start, count, offset, flip -> glTF accessor index private _indicesAccessorMap = new Map, Map>>>>(); - // Babylon buffer -> glTF buffer view index - private _vertexBufferViewMap = new Map(); + // Babylon buffer -> glTF buffer view + private _vertexBufferViewMap = new Map(); // Babylon vertex buffer, start, count -> glTF accessor index private _vertexAccessorMap = new Map>>(); - private _remappedBufferView = new Map>(); + private _remappedBufferView = new Map>(); private _meshMorphTargetMap = new Map(); @@ -148,20 +146,20 @@ class ExporterState { return this._exportedNodes; } - public getVertexBufferView(buffer: Buffer): number | undefined { + public getVertexBufferView(buffer: Buffer): IBufferView | undefined { return this._vertexBufferViewMap.get(buffer); } - public setVertexBufferView(buffer: Buffer, bufferViewIndex: number): void { - this._vertexBufferViewMap.set(buffer, bufferViewIndex); + public setVertexBufferView(buffer: Buffer, bufferView: IBufferView): void { + this._vertexBufferViewMap.set(buffer, bufferView); } - public setRemappedBufferView(buffer: Buffer, vertexBuffer: VertexBuffer, bufferViewIndex: number) { - this._remappedBufferView.set(buffer, new Map()); - this._remappedBufferView.get(buffer)!.set(vertexBuffer, bufferViewIndex); + public setRemappedBufferView(buffer: Buffer, vertexBuffer: VertexBuffer, bufferView: IBufferView) { + this._remappedBufferView.set(buffer, new Map()); + this._remappedBufferView.get(buffer)!.set(vertexBuffer, bufferView); } - public getRemappedBufferView(buffer: Buffer, vertexBuffer: VertexBuffer): number | undefined { + public getRemappedBufferView(buffer: Buffer, vertexBuffer: VertexBuffer): IBufferView | undefined { return this._remappedBufferView.get(buffer)?.get(vertexBuffer); } @@ -235,7 +233,6 @@ export class GLTFExporter { public readonly _babylonScene: Scene; public readonly _imageData: { [fileName: string]: { data: ArrayBuffer; mimeType: ImageMimeType } } = {}; - private readonly _orderedImageData: Array<{ data: ArrayBuffer; mimeType: ImageMimeType }> = []; /** * Baked animation sample rate @@ -244,11 +241,13 @@ export class GLTFExporter { private readonly _options: Required; + public readonly _shouldUseGlb: boolean; + public readonly _materialExporter = new GLTFMaterialExporter(this); private readonly _extensions: { [name: string]: IGLTFExporterExtensionV2 } = {}; - private readonly _dataWriter = new DataWriter(4); + public readonly _dataManager = new DataWriter(); private readonly _shouldExportNodeMap = new Map(); @@ -300,17 +299,17 @@ export class GLTFExporter { return this._applyExtensions(babylonTexture, (extension, node) => extension.preExportTextureAsync && extension.preExportTextureAsync(context, node, mimeType)); } - public _extensionsPostExportMeshPrimitiveAsync(context: string, meshPrimitive: IMeshPrimitive, babylonSubMesh: SubMesh): Promise> { + public _extensionsPostExportMeshPrimitiveAsync(primitive: IMeshPrimitive): Promise> { return this._applyExtensions( - meshPrimitive, - (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(context, node, babylonSubMesh) + primitive, + (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(node, this._dataManager, this._accessors) ); } public _extensionsPostExportNodeAsync(context: string, node: INode, babylonNode: Node, nodeMap: Map, convertToRightHanded: boolean): Promise> { return this._applyExtensions( node, - (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap, convertToRightHanded, this._dataWriter) + (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap, convertToRightHanded, this._dataManager) ); } @@ -342,6 +341,16 @@ export class GLTFExporter { } } + public async _extensionsPreGenerateBinaryAsync(): Promise { + for (const name of GLTFExporter._ExtensionNames) { + const extension = this._extensions[name]; + + if (extension.preGenerateBinaryAsync) { + extension.preGenerateBinaryAsync(this._dataManager); + } + } + } + private _forEachExtensions(action: (extension: IGLTFExporterExtensionV2) => void): void { for (const name of GLTFExporter._ExtensionNames) { const extension = this._extensions[name]; @@ -381,12 +390,13 @@ export class GLTFExporter { } } - public constructor(babylonScene: Nullable = EngineStore.LastCreatedScene, options?: IExportOptions) { + public constructor(babylonScene: Nullable = EngineStore.LastCreatedScene, shouldUseGlb: boolean, options?: IExportOptions) { if (!babylonScene) { throw new Error("No scene available to export"); } this._babylonScene = babylonScene; + this._shouldUseGlb = shouldUseGlb; this._options = { shouldExportNode: () => true, @@ -397,6 +407,7 @@ export class GLTFExporter { exportUnusedUVs: false, removeNoopRootNodes: true, includeCoordinateSystemConversionNodes: false, + useDracoCompression: false, ...options, }; @@ -437,12 +448,8 @@ export class GLTFExporter { return true; } - private _generateJSON(shouldUseGlb: boolean, bufferByteLength: number, fileName?: string, prettyPrint?: boolean): string { + private _generateJSON(bufferByteLength: number, fileName?: string, prettyPrint?: boolean): string { const buffer: IBuffer = { byteLength: bufferByteLength }; - let imageName: string; - let imageData: { data: ArrayBuffer; mimeType: ImageMimeType }; - let bufferView: IBufferView; - let byteOffset: number = bufferByteLength; if (buffer.byteLength) { this._glTF.buffers = [buffer]; @@ -482,43 +489,28 @@ export class GLTFExporter { this._glTF.skins = this._skins; } if (this._images && this._images.length) { - if (!shouldUseGlb) { - this._glTF.images = this._images; - } else { - this._glTF.images = []; - - this._images.forEach((image) => { - if (image.uri) { - imageData = this._imageData[image.uri]; - this._orderedImageData.push(imageData); - bufferView = CreateBufferView(0, byteOffset, imageData.data.byteLength, undefined); - byteOffset += imageData.data.byteLength; - this._bufferViews.push(bufferView); - image.bufferView = this._bufferViews.length - 1; - image.name = imageName; - image.mimeType = imageData.mimeType; - image.uri = undefined; - this._glTF.images!.push(image); - } - }); - - // Replace uri with bufferview and mime type for glb - buffer.byteLength = byteOffset; - } + this._glTF.images = this._images; } - if (!shouldUseGlb) { + if (!this._shouldUseGlb) { buffer.uri = fileName + ".bin"; } return prettyPrint ? JSON.stringify(this._glTF, null, 2) : JSON.stringify(this._glTF); } + public async generateAsync(glTFPrefix: string): Promise { + if (this._shouldUseGlb) { + return this.generateGLBAsync(glTFPrefix); + } + return this.generateGLTFAsync(glTFPrefix); + } + public async generateGLTFAsync(glTFPrefix: string): Promise { const binaryBuffer = await this._generateBinaryAsync(); this._extensionsOnExporting(); - const jsonText = this._generateJSON(false, binaryBuffer.byteLength, glTFPrefix, true); + const jsonText = this._generateJSON(binaryBuffer.byteLength, glTFPrefix, true); const bin = new Blob([binaryBuffer], { type: "application/octet-stream" }); const glTFFileName = glTFPrefix + ".gltf"; @@ -540,7 +532,8 @@ export class GLTFExporter { private async _generateBinaryAsync(): Promise { await this._exportSceneAsync(); - return this._dataWriter.getOutputData(); + await this._extensionsPreGenerateBinaryAsync(); + return this._dataManager.generateBinary(this._bufferViews); } /** @@ -559,27 +552,22 @@ export class GLTFExporter { const binaryBuffer = await this._generateBinaryAsync(); this._extensionsOnExporting(); - const jsonText = this._generateJSON(true, binaryBuffer.byteLength); + const jsonText = this._generateJSON(binaryBuffer.byteLength); const glbFileName = glTFPrefix + ".glb"; const headerLength = 12; const chunkLengthPrefix = 8; let jsonLength = jsonText.length; let encodedJsonText; - let imageByteLength = 0; // make use of TextEncoder when available if (typeof TextEncoder !== "undefined") { const encoder = new TextEncoder(); encodedJsonText = encoder.encode(jsonText); jsonLength = encodedJsonText.length; } - for (let i = 0; i < this._orderedImageData.length; ++i) { - imageByteLength += this._orderedImageData[i].data.byteLength; - } const jsonPadding = this._getPadding(jsonLength); const binPadding = this._getPadding(binaryBuffer.byteLength); - const imagePadding = this._getPadding(imageByteLength); - const byteLength = headerLength + 2 * chunkLengthPrefix + jsonLength + jsonPadding + binaryBuffer.byteLength + binPadding + imageByteLength + imagePadding; + const byteLength = headerLength + 2 * chunkLengthPrefix + jsonLength + jsonPadding + binaryBuffer.byteLength + binPadding; // header const headerBuffer = new ArrayBuffer(headerLength); @@ -621,7 +609,7 @@ export class GLTFExporter { // binary chunk const binaryChunkBuffer = new ArrayBuffer(chunkLengthPrefix); const binaryChunkBufferView = new DataView(binaryChunkBuffer); - binaryChunkBufferView.setUint32(0, binaryBuffer.byteLength + binPadding + imageByteLength + imagePadding, true); + binaryChunkBufferView.setUint32(0, binaryBuffer.byteLength + binPadding, true); binaryChunkBufferView.setUint32(4, 0x004e4942, true); // binary padding @@ -631,23 +619,7 @@ export class GLTFExporter { binPaddingView[i] = 0; } - const imagePaddingBuffer = new ArrayBuffer(imagePadding); - const imagePaddingView = new Uint8Array(imagePaddingBuffer); - for (let i = 0; i < imagePadding; ++i) { - imagePaddingView[i] = 0; - } - - const glbData = [headerBuffer, jsonChunkBuffer, binaryChunkBuffer, binaryBuffer]; - - // binary data - for (let i = 0; i < this._orderedImageData.length; ++i) { - glbData.push(this._orderedImageData[i].data); - } - - glbData.push(binPaddingBuffer); - - glbData.push(imagePaddingBuffer); - + const glbData = [headerBuffer, jsonChunkBuffer, binaryChunkBuffer, binaryBuffer, binPaddingBuffer]; const glbFile = new Blob(glbData, { type: "application/octet-stream" }); const container = new GLTFData(); @@ -818,21 +790,19 @@ export class GLTFExporter { // Only create skeleton if it has at least one joint and is used by a mesh. if (skin.joints.length > 0 && skinedNodes !== undefined) { - // create buffer view for inverse bind matrices + // Put IBM data into TypedArraybuffer view const byteStride = 64; // 4 x 4 matrix of 32 bit float const byteLength = inverseBindMatrices.length * byteStride; - const bufferViewOffset = this._dataWriter.byteOffset; - const bufferView = CreateBufferView(0, bufferViewOffset, byteLength, undefined); - this._bufferViews.push(bufferView); - const bufferViewIndex = this._bufferViews.length - 1; - const bindMatrixAccessor = CreateAccessor(bufferViewIndex, AccessorType.MAT4, AccessorComponentType.FLOAT, inverseBindMatrices.length, null, null); - const inverseBindAccessorIndex = this._accessors.push(bindMatrixAccessor) - 1; - skin.inverseBindMatrices = inverseBindAccessorIndex; - inverseBindMatrices.forEach((mat) => { - mat.m.forEach((cell: number) => { - this._dataWriter.writeFloat32(cell); + const inverseBindMatricesData = new Float32Array(byteLength / 4); + inverseBindMatrices.forEach((mat: Matrix, index: number) => { + mat.m.forEach((cell: number, cellIndex: number) => { + inverseBindMatricesData[index * 16 + cellIndex] = cell; }); }); + // Create buffer view and accessor + const bufferView = this._dataManager.createBufferView(inverseBindMatricesData, byteStride); + this._accessors.push(this._dataManager.createAccessor(bufferView, AccessorType.MAT4, AccessorComponentType.FLOAT, inverseBindMatrices.length)); + skin.inverseBindMatrices = this._accessors.length - 1; this._skins.push(skin); for (const skinedNode of skinedNodes) { @@ -895,7 +865,7 @@ export class GLTFExporter { this._babylonScene, this._animations, this._nodeMap, - this._dataWriter, + this._dataManager, this._bufferViews, this._accessors, this._animationSampleRate, @@ -1086,14 +1056,13 @@ export class GLTFExporter { state.convertedToRightHandedBuffers.set(buffer, bytes); } - const byteOffset = this._dataWriter.byteOffset; - this._dataWriter.writeUint8Array(bytes); - this._bufferViews.push(CreateBufferView(0, byteOffset, bytes.length, byteStride)); - state.setVertexBufferView(buffer, this._bufferViews.length - 1); + // Create buffer view, but defer accessor creation for later. Instead, track it via ExporterState. + const bufferView = this._dataManager.createBufferView(bytes, byteStride); + state.setVertexBufferView(buffer, bufferView); const floatMatricesIndices = new Map(); - // If buffers are of type MatricesWeightsKind and have float values, we need to create a new buffer instead. + // If buffers are of type MatricesIndicesKind and have float values, we need to create a new buffer instead. for (const vertexBuffer of vertexBuffers) { switch (vertexBuffer.getKind()) { case VertexBuffer.MatricesIndicesKind: @@ -1125,24 +1094,13 @@ export class GLTFExporter { continue; } - const byteOffset = this._dataWriter.byteOffset; - if (FloatsNeed16BitInteger(array)) { - const newArray = new Uint16Array(array.length); - for (let index = 0; index < array.length; index++) { - newArray[index] = array[index]; - } - this._dataWriter.writeUint16Array(newArray); - this._bufferViews.push(CreateBufferView(0, byteOffset, newArray.byteLength, 4 * 2)); - } else { - const newArray = new Uint8Array(array.length); - for (let index = 0; index < array.length; index++) { - newArray[index] = array[index]; - } - this._dataWriter.writeUint8Array(newArray); - this._bufferViews.push(CreateBufferView(0, byteOffset, newArray.byteLength, 4)); + const is16Bit = FloatsNeed16BitInteger(array); + const newArray = new (is16Bit ? Uint16Array : Uint8Array)(array.length); + for (let index = 0; index < array.length; index++) { + newArray[index] = array[index]; } - - state.setRemappedBufferView(buffer, vertexBuffer, this._bufferViews.length - 1); + const bufferView = this._dataManager.createBufferView(newArray, 4 * (is16Bit ? 2 : 1)); + state.setRemappedBufferView(buffer, vertexBuffer, bufferView); } } @@ -1155,7 +1113,7 @@ export class GLTFExporter { continue; } - const glTFMorphTarget = BuildMorphTargetBuffers(morphTarget, meshes[0], this._dataWriter, this._bufferViews, this._accessors, state.convertToRightHanded); + const glTFMorphTarget = BuildMorphTargetBuffers(morphTarget, meshes[0], this._dataManager, this._bufferViews, this._accessors, state.convertToRightHanded); for (const mesh of meshes) { state.bindMorphDataToMesh(mesh, glTFMorphTarget); @@ -1201,7 +1159,7 @@ export class GLTFExporter { idleGLTFAnimations, this._nodeMap, this._nodes, - this._dataWriter, + this._dataManager, this._bufferViews, this._accessors, this._animationSampleRate, @@ -1215,7 +1173,7 @@ export class GLTFExporter { idleGLTFAnimations, this._nodeMap, this._nodes, - this._dataWriter, + this._dataManager, this._bufferViews, this._accessors, this._animationSampleRate, @@ -1380,14 +1338,11 @@ export class GLTFExporter { if (indicesToExport) { let accessorIndex = state.getIndicesAccessor(indices, start, count, offset, flip); if (accessorIndex === undefined) { - const bufferViewByteOffset = this._dataWriter.byteOffset; - const bytes = IndicesArrayToUint8Array(indicesToExport, start, count, is32Bits); - this._dataWriter.writeUint8Array(bytes); - this._bufferViews.push(CreateBufferView(0, bufferViewByteOffset, bytes.length)); - const bufferViewIndex = this._bufferViews.length - 1; + const bytes = IndicesArrayToTypedArray(indicesToExport, start, count, is32Bits); + const bufferView = this._dataManager.createBufferView(bytes); const componentType = is32Bits ? AccessorComponentType.UNSIGNED_INT : AccessorComponentType.UNSIGNED_SHORT; - this._accessors.push(CreateAccessor(bufferViewIndex, AccessorType.SCALAR, componentType, count, 0)); + this._accessors.push(this._dataManager.createAccessor(bufferView, AccessorType.SCALAR, componentType, count, 0)); accessorIndex = this._accessors.length - 1; state.setIndicesAccessor(indices, start, count, offset, flip, accessorIndex); } @@ -1414,40 +1369,33 @@ export class GLTFExporter { if (accessorIndex === undefined) { // Get min/max from converted or original data. const data = state.convertedToRightHandedBuffers.get(vertexBuffer._buffer) || vertexBuffer._buffer.getData()!; - const minMax = kind === VertexBuffer.PositionKind ? GetMinMax(data, vertexBuffer, start, count) : null; - - if ((kind === VertexBuffer.MatricesIndicesKind || kind === VertexBuffer.MatricesIndicesExtraKind) && vertexBuffer.type === VertexBuffer.FLOAT) { - const bufferViewIndex = state.getRemappedBufferView(vertexBuffer._buffer, vertexBuffer); - if (bufferViewIndex !== undefined) { - const byteOffset = vertexBuffer.byteOffset + start * vertexBuffer.byteStride; - this._accessors.push( - CreateAccessor(bufferViewIndex, GetAccessorType(kind, state.hasVertexColorAlpha(vertexBuffer)), VertexBuffer.UNSIGNED_BYTE, count, byteOffset, minMax) - ); - accessorIndex = this._accessors.length - 1; - state.setVertexAccessor(vertexBuffer, start, count, accessorIndex); - primitive.attributes[GetAttributeType(kind)] = accessorIndex; - } - } else { - const bufferViewIndex = state.getVertexBufferView(vertexBuffer._buffer)!; - const byteOffset = vertexBuffer.byteOffset + start * vertexBuffer.byteStride; - this._accessors.push( - CreateAccessor( - bufferViewIndex, - GetAccessorType(kind, state.hasVertexColorAlpha(vertexBuffer)), - vertexBuffer.type, - count, - byteOffset, - minMax, - vertexBuffer.normalized // TODO: Find other places where this is needed. - ) - ); - accessorIndex = this._accessors.length - 1; - state.setVertexAccessor(vertexBuffer, start, count, accessorIndex); - primitive.attributes[GetAttributeType(kind)] = accessorIndex; - } - } else { - primitive.attributes[GetAttributeType(kind)] = accessorIndex; + const minMax = kind === VertexBuffer.PositionKind ? GetMinMax(data, vertexBuffer, start, count) : undefined; + + // For the remapped buffer views we created for float matrices indices, make sure to use their updated information. + const isFloatMatricesIndices = + (kind === VertexBuffer.MatricesIndicesKind || kind === VertexBuffer.MatricesIndicesExtraKind) && vertexBuffer.type === VertexBuffer.FLOAT; + + const vertexBufferType = isFloatMatricesIndices ? VertexBuffer.UNSIGNED_BYTE : vertexBuffer.type; + const vertexBufferNormalized = isFloatMatricesIndices ? undefined : vertexBuffer.normalized; + const bufferView = isFloatMatricesIndices ? state.getRemappedBufferView(vertexBuffer._buffer, vertexBuffer)! : state.getVertexBufferView(vertexBuffer._buffer)!; + + const byteOffset = vertexBuffer.byteOffset + start * vertexBuffer.byteStride; + this._accessors.push( + this._dataManager.createAccessor( + bufferView, + GetAccessorType(kind, state.hasVertexColorAlpha(vertexBuffer)), + vertexBufferType, + count, + byteOffset, + minMax, + vertexBufferNormalized // TODO: Find other places where this is needed. + ) + ); + accessorIndex = this._accessors.length - 1; + state.setVertexAccessor(vertexBuffer, start, count, accessorIndex); } + + primitive.attributes[GetAttributeType(kind)] = accessorIndex; } private async _exportMaterialAsync(babylonMaterial: Material, vertexBuffers: { [kind: string]: VertexBuffer }, subMesh: SubMesh, primitive: IMeshPrimitive): Promise { @@ -1531,14 +1479,15 @@ export class GLTFExporter { this._exportVertexBuffer(vertexBuffer, babylonMaterial, subMesh.verticesStart, subMesh.verticesCount, state, primitive); } - mesh.primitives.push(primitive); - if (morphTargets) { primitive.targets = []; for (const gltfMorphTarget of morphTargets) { primitive.targets.push(gltfMorphTarget.attributes); } } + + mesh.primitives.push(primitive); + await this._extensionsPostExportMeshPrimitiveAsync(primitive); } } diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts index 91a0b8e6513..939de5bb26d 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts @@ -1,9 +1,8 @@ -import type { ImageMimeType, IMeshPrimitive, INode, IMaterial, ITextureInfo } from "babylonjs-gltf2interface"; +import type { ImageMimeType, IMeshPrimitive, INode, IMaterial, ITextureInfo, IAccessor } from "babylonjs-gltf2interface"; import type { Node } from "core/node"; import type { Nullable } from "core/types"; import type { Texture } from "core/Materials/Textures/texture"; -import type { SubMesh } from "core/Meshes/subMesh"; import type { IDisposable } from "core/scene"; import type { IGLTFExporterExtension } from "../glTFFileExporter"; @@ -39,12 +38,9 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo /** * Define this method to modify the default behavior when exporting a mesh primitive - * @param context The context when loading the asset - * @param meshPrimitive glTF mesh primitive - * @param babylonSubMesh Babylon submesh * @returns nullable IMeshPrimitive promise */ - postExportMeshPrimitiveAsync?(context: string, meshPrimitive: IMeshPrimitive, babylonSubMesh: SubMesh): Promise; + postExportMeshPrimitiveAsync?(primitive: IMeshPrimitive, dataManager: DataWriter, accessors: IAccessor[]): Promise; /** * Define this method to modify the default behavior when exporting a node @@ -80,6 +76,12 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo */ postExportMaterialAdditionalTextures?(context: string, node: IMaterial, babylonMaterial: Material): BaseTexture[]; + /** + * todo + * @returns todo + */ + preGenerateBinaryAsync?(dataManager: DataWriter): Promise; + /** Gets a boolean indicating that this extension was used */ wasUsed: boolean; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts index 459f516fd16..954f153beda 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts @@ -1,6 +1,6 @@ /* eslint-disable babylonjs/available */ -import type { ITextureInfo, IMaterial, IMaterialPbrMetallicRoughness, IMaterialOcclusionTextureInfo, ISampler } from "babylonjs-gltf2interface"; +import type { ITextureInfo, IMaterial, IMaterialPbrMetallicRoughness, IMaterialOcclusionTextureInfo, ISampler, IImage } from "babylonjs-gltf2interface"; import { ImageMimeType, MaterialAlphaMode, TextureMagFilter, TextureMinFilter, TextureWrapMode } from "babylonjs-gltf2interface"; import type { Nullable } from "core/types"; @@ -960,25 +960,34 @@ export class GLTFMaterialExporter { } private _exportImage(name: string, mimeType: ImageMimeType, data: ArrayBuffer): number { - const imageData = this._exporter._imageData; + const images = this._exporter._images; - const baseName = name.replace(/\.\/|\/|\.\\|\\/g, "_"); - const extension = GetFileExtensionFromMimeType(mimeType); - let fileName = baseName + extension; - if (fileName in imageData) { - fileName = `${baseName}_${Tools.RandomId()}${extension}`; - } + let image: IImage; + if (this._exporter._shouldUseGlb) { + image = { + name: name, + mimeType: mimeType, + bufferView: undefined, // Will be updated later by DataManager + }; + const bufferView = this._exporter._dataManager.createBufferView(new Uint8Array(data)); + this._exporter._dataManager.setBufferView(image, bufferView); + } else { + // Build a unique URI + const baseName = name.replace(/\.\/|\/|\.\\|\\/g, "_"); + const extension = GetFileExtensionFromMimeType(mimeType); + let fileName = baseName + extension; + if (images.some((image) => image.uri === fileName)) { + fileName = `${baseName}_${Tools.RandomId()}${extension}`; + } - imageData[fileName] = { - data: data, - mimeType: mimeType, - }; + image = { + name: name, + uri: fileName, + }; + this._exporter._imageData[fileName] = { data: data, mimeType: mimeType }; + } - const images = this._exporter._images; - images.push({ - name: name, - uri: fileName, - }); + images.push(image); return images.length - 1; } diff --git a/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts b/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts index 69257f1cb31..fab79e3c133 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts @@ -3,7 +3,7 @@ import { AccessorComponentType, AccessorType } from "babylonjs-gltf2interface"; import type { MorphTarget } from "core/Morph/morphTarget"; import type { DataWriter } from "./dataWriter"; -import { CreateAccessor, CreateBufferView, NormalizeTangent } from "./glTFUtilities"; +import { NormalizeTangent } from "./glTFUtilities"; import type { Mesh } from "core/Meshes/mesh"; import { VertexBuffer } from "core/Buffers/buffer"; import { Vector3 } from "core/Maths/math.vector"; @@ -38,18 +38,16 @@ export function BuildMorphTargetBuffers( const difference = Vector3.Zero(); let vertexStart = 0; let vertexCount = 0; - let byteOffset = 0; - let bufferViewIndex = 0; if (morphTarget.hasPositions) { const morphPositions = morphTarget.getPositions()!; const originalPositions = mesh.getVerticesData(VertexBuffer.PositionKind, undefined, undefined, true); if (originalPositions) { + const positionData = new Float32Array(originalPositions.length); const min = [Infinity, Infinity, Infinity]; const max = [-Infinity, -Infinity, -Infinity]; vertexCount = originalPositions.length / 3; - byteOffset = dataWriter.byteOffset; vertexStart = 0; for (let i = vertexStart; i < vertexCount; ++i) { const originalPosition = Vector3.FromArray(originalPositions, i * 3); @@ -66,14 +64,14 @@ export function BuildMorphTargetBuffers( min[2] = Math.min(min[2], difference.z); max[2] = Math.max(max[2], difference.z); - dataWriter.writeFloat32(difference.x); - dataWriter.writeFloat32(difference.y); - dataWriter.writeFloat32(difference.z); + positionData[i * 3] = difference.x; + positionData[i * 3 + 1] = difference.y; + positionData[i * 3 + 2] = difference.z; } - bufferViews.push(CreateBufferView(0, byteOffset, morphPositions.length * floatSize, floatSize * 3)); - bufferViewIndex = bufferViews.length - 1; - accessors.push(CreateAccessor(bufferViewIndex, AccessorType.VEC3, AccessorComponentType.FLOAT, morphPositions.length / 3, 0, { min, max })); + const bufferView = dataWriter.createBufferView(positionData, floatSize * 3); + const accessor = dataWriter.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, morphPositions.length / 3, 0, { min, max }); + accessors.push(accessor); result.attributes["POSITION"] = accessors.length - 1; } else { Tools.Warn(`Morph target positions for mesh ${mesh.name} were not exported. Mesh does not have position vertex data`); @@ -85,21 +83,22 @@ export function BuildMorphTargetBuffers( const originalNormals = mesh.getVerticesData(VertexBuffer.NormalKind, undefined, undefined, true); if (originalNormals) { + const normalData = new Float32Array(originalNormals.length); vertexCount = originalNormals.length / 3; - byteOffset = dataWriter.byteOffset; vertexStart = 0; for (let i = vertexStart; i < vertexCount; ++i) { const originalNormal = Vector3.FromArray(originalNormals, i * 3).normalize(); const morphNormal = Vector3.FromArray(morphNormals, i * 3).normalize(); morphNormal.subtractToRef(originalNormal, difference); - dataWriter.writeFloat32(difference.x * flipX); - dataWriter.writeFloat32(difference.y); - dataWriter.writeFloat32(difference.z); + + normalData[i * 3] = difference.x * flipX; + normalData[i * 3 + 1] = difference.y; + normalData[i * 3 + 2] = difference.z; } - bufferViews.push(CreateBufferView(0, byteOffset, morphNormals.length * floatSize, floatSize * 3)); - bufferViewIndex = bufferViews.length - 1; - accessors.push(CreateAccessor(bufferViewIndex, AccessorType.VEC3, AccessorComponentType.FLOAT, morphNormals.length / 3, 0)); + const bufferView = dataWriter.createBufferView(normalData, floatSize * 3); + const accessor = dataWriter.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, morphNormals.length / 3, 0); + accessors.push(accessor); result.attributes["NORMAL"] = accessors.length - 1; } else { Tools.Warn(`Morph target normals for mesh ${mesh.name} were not exported. Mesh does not have normals vertex data`); @@ -112,8 +111,8 @@ export function BuildMorphTargetBuffers( if (originalTangents) { vertexCount = originalTangents.length / 4; + const tangentData = new Float32Array(vertexCount * 3); vertexStart = 0; - byteOffset = dataWriter.byteOffset; for (let i = vertexStart; i < vertexCount; ++i) { // Only read the x, y, z components and ignore w const originalTangent = Vector3.FromArray(originalTangents, i * 4); @@ -124,14 +123,13 @@ export function BuildMorphTargetBuffers( NormalizeTangent(morphTangent); morphTangent.subtractToRef(originalTangent, difference); - dataWriter.writeFloat32(difference.x * flipX); - dataWriter.writeFloat32(difference.y); - dataWriter.writeFloat32(difference.z); + tangentData[i * 3] = difference.x * flipX; + tangentData[i * 3 + 1] = difference.y; + tangentData[i * 3 + 2] = difference.z; } - - bufferViews.push(CreateBufferView(0, byteOffset, vertexCount * floatSize * 3, floatSize * 3)); - bufferViewIndex = bufferViews.length - 1; - accessors.push(CreateAccessor(bufferViewIndex, AccessorType.VEC3, AccessorComponentType.FLOAT, vertexCount, 0)); + const bufferView = dataWriter.createBufferView(tangentData, floatSize * 3); + const accessor = dataWriter.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, vertexCount, 0); + accessors.push(accessor); result.attributes["TANGENT"] = accessors.length - 1; } else { Tools.Warn(`Morph target tangents for mesh ${mesh.name} were not exported. Mesh does not have tangents vertex data`); diff --git a/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts b/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts index 1787b779fa3..784c19650b2 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts @@ -54,6 +54,12 @@ export interface IExportOptions { * @deprecated Please use removeNoopRootNodes instead */ includeCoordinateSystemConversionNodes?: boolean; + + /** + * Indicates if mesh data should be compressed using Draco to reduce binary file size. Defaults to false. + * If used, `extensionsRequired` property of the exported file will include "KHR_draco_mesh_compression". + */ + useDracoCompression?: boolean; } /** @@ -72,8 +78,8 @@ export class GLTF2Export { await scene.whenReadyAsync(); } - const exporter = new GLTFExporter(scene, options); - const data = await exporter.generateGLTFAsync(fileName.replace(/\.[^/.]+$/, "")); + const exporter = new GLTFExporter(scene, false, options); + const data = await exporter.generateAsync(fileName.replace(/\.[^/.]+$/, "")); exporter.dispose(); return data; @@ -91,8 +97,8 @@ export class GLTF2Export { await scene.whenReadyAsync(); } - const exporter = new GLTFExporter(scene, options); - const data = await exporter.generateGLBAsync(fileName.replace(/\.[^/.]+$/, "")); + const exporter = new GLTFExporter(scene, true, options); + const data = await exporter.generateAsync(fileName.replace(/\.[^/.]+$/, "")); exporter.dispose(); return data; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts b/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts index 06dd9faa756..0ada854f637 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts @@ -1,9 +1,8 @@ /* eslint-disable jsdoc/require-jsdoc */ -import type { IBufferView, AccessorComponentType, IAccessor, INode } from "babylonjs-gltf2interface"; +import type { INode } from "babylonjs-gltf2interface"; import { AccessorType, MeshPrimitiveMode } from "babylonjs-gltf2interface"; - -import type { FloatArray, DataArray, IndicesArray, Nullable } from "core/types"; +import type { FloatArray, DataArray, IndicesArray } from "core/types"; import type { Vector4 } from "core/Maths/math.vector"; import { Quaternion, TmpVectors, Matrix, Vector3 } from "core/Maths/math.vector"; import { VertexBuffer } from "core/Buffers/buffer"; @@ -25,66 +24,6 @@ const epsilon = 1e-6; const defaultTranslation = Vector3.Zero(); const defaultScale = Vector3.One(); -/** - * Creates a buffer view based on the supplied arguments - * @param bufferIndex index value of the specified buffer - * @param byteOffset byte offset value - * @param byteLength byte length of the bufferView - * @param byteStride byte distance between conequential elements - * @returns bufferView for glTF - */ -export function CreateBufferView(bufferIndex: number, byteOffset: number, byteLength: number, byteStride?: number): IBufferView { - const bufferview: IBufferView = { buffer: bufferIndex, byteLength: byteLength }; - - if (byteOffset) { - bufferview.byteOffset = byteOffset; - } - - if (byteStride) { - bufferview.byteStride = byteStride; - } - - return bufferview; -} - -/** - * Creates an accessor based on the supplied arguments - * @param bufferViewIndex The index of the bufferview referenced by this accessor - * @param type The type of the accessor - * @param componentType The datatype of components in the attribute - * @param count The number of attributes referenced by this accessor - * @param byteOffset The offset relative to the start of the bufferView in bytes - * @param minMax Minimum and maximum value of each component in this attribute - * @param normalized Specifies whether integer data values are normalized before usage - * @returns accessor for glTF - */ -export function CreateAccessor( - bufferViewIndex: number, - type: AccessorType, - componentType: AccessorComponentType, - count: number, - byteOffset: Nullable, - minMax: Nullable<{ min: number[]; max: number[] }> = null, - normalized?: boolean -): IAccessor { - const accessor: IAccessor = { bufferView: bufferViewIndex, componentType: componentType, count: count, type: type }; - - if (minMax != null) { - accessor.min = minMax.min; - accessor.max = minMax.max; - } - - if (normalized) { - accessor.normalized = normalized; - } - - if (byteOffset != null) { - accessor.byteOffset = byteOffset; - } - - return accessor; -} - export function GetAccessorElementCount(accessorType: AccessorType): number { switch (accessorType) { case AccessorType.MAT2: @@ -355,14 +294,26 @@ export function IsNoopNode(node: Node, useRightHandedSystem: boolean): boolean { return true; } -export function IndicesArrayToUint8Array(indices: IndicesArray, start: number, count: number, is32Bits: boolean): Uint8Array { - if (indices instanceof Array) { - const subarray = indices.slice(start, start + count); - indices = is32Bits ? new Uint32Array(subarray) : new Uint16Array(subarray); - return new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength); +/** + * Converts an IndicesArray into either Uint32Array or Uint16Array, only copying if the data is number[]. + * @param indices input array to be converted + * @param start starting index to copy from + * @param count number of indices to copy + * @returns a Uint32Array or Uint16Array + * @internal + */ +export function IndicesArrayToTypedArray(indices: IndicesArray, start: number, count: number, is32Bits: boolean): Uint32Array | Uint16Array { + if (indices instanceof Uint16Array || indices instanceof Uint32Array) { + return indices; + } + + // If Int32Array, cast the indices (which are all positive) to Uint32Array + if (indices instanceof Int32Array) { + return new Uint32Array(indices.buffer, indices.byteOffset, indices.length); } - return ArrayBuffer.isView(indices) ? new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength) : new Uint8Array(indices); + const subarray = indices.slice(start, start + count); + return is32Bits ? new Uint32Array(subarray) : new Uint16Array(subarray); } export function DataArrayToUint8Array(data: DataArray): Uint8Array { From f6c6d14a91d3f2435f3aa04930ec1311ef1f9a53 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:54:17 -0500 Subject: [PATCH 03/32] Rename class to BufferManager --- .../2.0/Extensions/EXT_mesh_gpu_instancing.ts | 18 +++++----- .../Extensions/KHR_draco_mesh_compression.ts | 22 ++++++------ .../2.0/{dataWriter.ts => bufferManager.ts} | 8 ++--- .../serializers/src/glTF/2.0/glTFAnimation.ts | 32 ++++++++--------- .../serializers/src/glTF/2.0/glTFExporter.ts | 34 +++++++++---------- .../src/glTF/2.0/glTFExporterExtension.ts | 8 ++--- .../src/glTF/2.0/glTFMaterialExporter.ts | 6 ++-- .../src/glTF/2.0/glTFMorphTargetsUtilities.ts | 16 ++++----- 8 files changed, 71 insertions(+), 73 deletions(-) rename packages/dev/serializers/src/glTF/2.0/{dataWriter.ts => bufferManager.ts} (98%) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts index 8b87b85c048..21f6398db4b 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts @@ -1,7 +1,7 @@ import type { INode, IEXTMeshGpuInstancing } from "babylonjs-gltf2interface"; import { AccessorType, AccessorComponentType } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import type { DataWriter } from "../dataWriter"; +import type { BufferManager } from "../bufferManager"; import { GLTFExporter } from "../glTFExporter"; import type { Nullable } from "core/types"; import type { Node } from "core/node"; @@ -48,7 +48,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { * @param babylonNode the corresponding babylon node * @param nodeMap map from babylon node id to node index * @param convertToRightHanded true if we need to convert data from left hand to right hand system. - * @param dataManager binary writer + * @param bufferManager binary writer * @returns nullable promise, resolves with the node */ public postExportNodeAsync( @@ -57,7 +57,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { babylonNode: Node, nodeMap: Map, convertToRightHanded: boolean, - dataManager: DataWriter + bufferManager: BufferManager ): Promise> { return new Promise((resolve) => { if (node && babylonNode instanceof Mesh) { @@ -116,14 +116,14 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { translationBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, - dataManager, + bufferManager, AccessorComponentType.FLOAT ); } // do we need to write ROTATION ? if (hasAnyInstanceWorldRotation) { const componentType = AccessorComponentType.FLOAT; // we decided to stay on FLOAT for now see https://github.com/BabylonJS/Babylon.js/pull/12495 - extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, dataManager, componentType); + extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, bufferManager, componentType); } // do we need to write SCALE ? if (hasAnyInstanceWorldScale) { @@ -131,7 +131,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { scaleBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, - dataManager, + bufferManager, AccessorComponentType.FLOAT ); } @@ -145,7 +145,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { }); } - private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, dataManager: DataWriter, componentType: AccessorComponentType): number { + private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, bufferManager: BufferManager, componentType: AccessorComponentType): number { // write the buffer let data; switch (componentType) { @@ -175,10 +175,10 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { } } // build the buffer view - const bv = dataManager.createBufferView(data); + const bv = bufferManager.createBufferView(data); // finally build the accessor - const accessor = dataManager.createAccessor( + const accessor = bufferManager.createAccessor( bv, type, componentType, diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index d000a932731..066e87eea6e 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -2,7 +2,7 @@ import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; import { GLTFExporter } from "../glTFExporter"; import { MeshPrimitiveMode } from "babylonjs-gltf2interface"; import type { IAccessor, IBufferView, IKHRDracoMeshCompression, IMeshPrimitive } from "babylonjs-gltf2interface"; -import type { DataWriter } from "../dataWriter"; +import type { BufferManager } from "../bufferManager"; import { DracoEncoder } from "core/Meshes/Compression/dracoEncoder"; import { GetFloatData, GetTypeByteLength } from "core/Buffers/bufferUtils"; import { GetAccessorElementCount } from "../glTFUtilities"; @@ -69,7 +69,7 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { } /** @internal */ - public async postExportMeshPrimitiveAsync(primitive: IMeshPrimitive, dataManager: DataWriter, accessors: IAccessor[]): Promise { + public async postExportMeshPrimitiveAsync(primitive: IMeshPrimitive, bufferManager: BufferManager, accessors: IAccessor[]): Promise { if (!this.enabled) { return primitive; } @@ -83,21 +83,21 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { let indices: Nullable = null; if (primitive.indices !== undefined) { const accessor = accessors[primitive.indices]; - const bufferView = dataManager.getBufferView(accessor); + const bufferView = bufferManager.getBufferView(accessor); this._bufferViewsUsed.add(bufferView); this._accessorsUsed.add(accessor); // Per exportIndices, indices must be either Uint16Array or Uint32Array - indices = dataManager.getData(bufferView) as Uint32Array | Uint16Array; + indices = bufferManager.getData(bufferView) as Uint32Array | Uint16Array; } // Prepare attributes for Draco encoding const attributes: IDracoAttributeData[] = []; for (const [name, accessorIndex] of Object.entries(primitive.attributes)) { const accessor = accessors[accessorIndex]; - const bufferView = dataManager.getBufferView(accessor); + const bufferView = bufferManager.getBufferView(accessor); this._bufferViewsUsed.add(bufferView); this._accessorsUsed.add(accessor); - const data = dataManager.getData(bufferView); + const data = bufferManager.getData(bufferView); const size = GetAccessorElementCount(accessor.type); // TODO: In future, find a way to preserve original data type to avoid copying in some cases @@ -128,8 +128,8 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { bufferView: -1, // bufferView will be set to a real index later, when we write the binary and decide bufferView ordering attributes: encodedData.attributeIDs, }; - const bufferView = dataManager.createBufferView(encodedData.data); - dataManager.setBufferView(dracoInfo, bufferView); + const bufferView = bufferManager.createBufferView(encodedData.data); + bufferManager.setBufferView(dracoInfo, bufferView); primitive.extensions ||= {}; primitive.extensions[NAME] = dracoInfo; @@ -140,21 +140,21 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { } /** @internal */ - public async preGenerateBinaryAsync(dataManager: DataWriter): Promise { + public async preGenerateBinaryAsync(bufferManager: BufferManager): Promise { if (!this.enabled) { return; } // Cull obsolete bufferViews that are no longer needed, as they were replaced with Draco data for (const bufferView of this._bufferViewsUsed) { - const references = dataManager.getProperties(bufferView); + const references = bufferManager.getProperties(bufferView); const bufferViewOnlyUsedByDraco = references.every((object) => { return this._accessorsUsed.has(object as IAccessor); // has() can handle any object, but TS doesn't know that }); if (bufferViewOnlyUsedByDraco) { - dataManager.removeBufferView(bufferView); + bufferManager.removeBufferView(bufferView); } } diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts similarity index 98% rename from packages/dev/serializers/src/glTF/2.0/dataWriter.ts rename to packages/dev/serializers/src/glTF/2.0/bufferManager.ts index a4b88935a3a..5167205a87a 100644 --- a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts +++ b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts @@ -1,17 +1,15 @@ import type { TypedArray } from "core/types"; import type { AccessorComponentType, AccessorType, IAccessor, IBufferView } from "babylonjs-gltf2interface"; -type IPropertyWithBufferView = { +interface IPropertyWithBufferView { bufferView?: number; - [key: string]: any; -}; +} /** * Utility class to centralize the management of binary data, bufferViews, and the objects that reference them. - * TODO: Rename to BufferManager..? * @internal */ -export class DataWriter { +export class BufferManager { // BufferView -> data private _bufferViewToData: Map = new Map(); diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index 92dde8156de..f859fdd0ac1 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -15,7 +15,7 @@ import { AnimationKeyInterpolation } from "core/Animations/animationKey"; import { Camera } from "core/Cameras/camera"; import { Light } from "core/Lights/light"; -import type { DataWriter } from "./dataWriter"; +import type { BufferManager } from "./bufferManager"; import { GetAccessorElementCount, ConvertToRightHandedPosition, ConvertCameraRotationToGLTF, ConvertToRightHandedRotation } from "./glTFUtilities"; /** @@ -224,7 +224,7 @@ export class _GLTFAnimation { * @param idleGLTFAnimations * @param nodeMap * @param nodes - * @param binaryWriter + * @param bufferManager * @param bufferViews * @param accessors * @param animationSampleRate @@ -235,7 +235,7 @@ export class _GLTFAnimation { idleGLTFAnimations: IAnimation[], nodeMap: Map, nodes: INode[], - binaryWriter: DataWriter, + bufferManager: BufferManager, bufferViews: IBufferView[], accessors: IAccessor[], animationSampleRate: number, @@ -264,7 +264,7 @@ export class _GLTFAnimation { animationInfo.dataAccessorType, animationInfo.animationChannelTargetPath, nodeMap, - binaryWriter, + bufferManager, bufferViews, accessors, animationInfo.useQuaternion, @@ -288,7 +288,7 @@ export class _GLTFAnimation { * @param idleGLTFAnimations * @param nodeMap * @param nodes - * @param binaryWriter + * @param bufferManager * @param bufferViews * @param accessors * @param animationSampleRate @@ -299,7 +299,7 @@ export class _GLTFAnimation { idleGLTFAnimations: IAnimation[], nodeMap: Map, nodes: INode[], - binaryWriter: DataWriter, + bufferManager: BufferManager, bufferViews: IBufferView[], accessors: IAccessor[], animationSampleRate: number, @@ -353,7 +353,7 @@ export class _GLTFAnimation { animationInfo.dataAccessorType, animationInfo.animationChannelTargetPath, nodeMap, - binaryWriter, + bufferManager, bufferViews, accessors, animationInfo.useQuaternion, @@ -378,7 +378,7 @@ export class _GLTFAnimation { * @param glTFAnimations * @param nodeMap * @param nodes - * @param binaryWriter + * @param bufferManager * @param bufferViews * @param accessors * @param animationSampleRate @@ -387,7 +387,7 @@ export class _GLTFAnimation { babylonScene: Scene, glTFAnimations: IAnimation[], nodeMap: Map, - binaryWriter: DataWriter, + bufferManager: BufferManager, bufferViews: IBufferView[], accessors: IAccessor[], animationSampleRate: number, @@ -430,7 +430,7 @@ export class _GLTFAnimation { animationInfo.dataAccessorType, animationInfo.animationChannelTargetPath, nodeMap, - binaryWriter, + bufferManager, bufferViews, accessors, animationInfo.useQuaternion, @@ -527,7 +527,7 @@ export class _GLTFAnimation { animationInfo.dataAccessorType, animationInfo.animationChannelTargetPath, nodeMap, - binaryWriter, + bufferManager, bufferViews, accessors, animationInfo.useQuaternion, @@ -552,7 +552,7 @@ export class _GLTFAnimation { dataAccessorType: AccessorType, animationChannelTargetPath: AnimationChannelTargetPath, nodeMap: Map, - binaryWriter: DataWriter, + bufferManager: BufferManager, bufferViews: IBufferView[], accessors: IAccessor[], useQuaternion: boolean, @@ -592,8 +592,8 @@ export class _GLTFAnimation { // Create buffer view and accessor for key frames. let data = new Float32Array(animationData.inputs); - bufferView = binaryWriter.createBufferView(data); - accessor = binaryWriter.createAccessor(bufferView, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, undefined, { + bufferView = bufferManager.createBufferView(data); + accessor = bufferManager.createAccessor(bufferView, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, undefined, { min: [animationData.inputsMin], max: [animationData.inputsMax], }); @@ -658,8 +658,8 @@ export class _GLTFAnimation { }); // Create buffer view and accessor for keyed values. - bufferView = binaryWriter.createBufferView(data); - accessor = binaryWriter.createAccessor(bufferView, dataAccessorType, AccessorComponentType.FLOAT, animationData.outputs.length); + bufferView = bufferManager.createBufferView(data); + accessor = bufferManager.createAccessor(bufferView, dataAccessorType, AccessorComponentType.FLOAT, animationData.outputs.length); accessors.push(accessor); dataAccessorIndex = accessors.length - 1; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 7d3a03e61ae..708601fdebf 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -56,7 +56,7 @@ import { IsStandardVertexAttribute, IndicesArrayToTypedArray, } from "./glTFUtilities"; -import { DataWriter } from "./dataWriter"; +import { BufferManager } from "./bufferManager"; import { Camera } from "core/Cameras/camera"; import { MultiMaterial } from "core/Materials/multiMaterial"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; @@ -247,7 +247,7 @@ export class GLTFExporter { private readonly _extensions: { [name: string]: IGLTFExporterExtensionV2 } = {}; - public readonly _dataManager = new DataWriter(); + public readonly _bufferManager = new BufferManager(); private readonly _shouldExportNodeMap = new Map(); @@ -302,14 +302,14 @@ export class GLTFExporter { public _extensionsPostExportMeshPrimitiveAsync(primitive: IMeshPrimitive): Promise> { return this._applyExtensions( primitive, - (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(node, this._dataManager, this._accessors) + (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(node, this._bufferManager, this._accessors) ); } public _extensionsPostExportNodeAsync(context: string, node: INode, babylonNode: Node, nodeMap: Map, convertToRightHanded: boolean): Promise> { return this._applyExtensions( node, - (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap, convertToRightHanded, this._dataManager) + (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap, convertToRightHanded, this._bufferManager) ); } @@ -346,7 +346,7 @@ export class GLTFExporter { const extension = this._extensions[name]; if (extension.preGenerateBinaryAsync) { - extension.preGenerateBinaryAsync(this._dataManager); + extension.preGenerateBinaryAsync(this._bufferManager); } } } @@ -533,7 +533,7 @@ export class GLTFExporter { private async _generateBinaryAsync(): Promise { await this._exportSceneAsync(); await this._extensionsPreGenerateBinaryAsync(); - return this._dataManager.generateBinary(this._bufferViews); + return this._bufferManager.generateBinary(this._bufferViews); } /** @@ -800,8 +800,8 @@ export class GLTFExporter { }); }); // Create buffer view and accessor - const bufferView = this._dataManager.createBufferView(inverseBindMatricesData, byteStride); - this._accessors.push(this._dataManager.createAccessor(bufferView, AccessorType.MAT4, AccessorComponentType.FLOAT, inverseBindMatrices.length)); + const bufferView = this._bufferManager.createBufferView(inverseBindMatricesData, byteStride); + this._accessors.push(this._bufferManager.createAccessor(bufferView, AccessorType.MAT4, AccessorComponentType.FLOAT, inverseBindMatrices.length)); skin.inverseBindMatrices = this._accessors.length - 1; this._skins.push(skin); @@ -865,7 +865,7 @@ export class GLTFExporter { this._babylonScene, this._animations, this._nodeMap, - this._dataManager, + this._bufferManager, this._bufferViews, this._accessors, this._animationSampleRate, @@ -1057,7 +1057,7 @@ export class GLTFExporter { } // Create buffer view, but defer accessor creation for later. Instead, track it via ExporterState. - const bufferView = this._dataManager.createBufferView(bytes, byteStride); + const bufferView = this._bufferManager.createBufferView(bytes, byteStride); state.setVertexBufferView(buffer, bufferView); const floatMatricesIndices = new Map(); @@ -1099,7 +1099,7 @@ export class GLTFExporter { for (let index = 0; index < array.length; index++) { newArray[index] = array[index]; } - const bufferView = this._dataManager.createBufferView(newArray, 4 * (is16Bit ? 2 : 1)); + const bufferView = this._bufferManager.createBufferView(newArray, 4 * (is16Bit ? 2 : 1)); state.setRemappedBufferView(buffer, vertexBuffer, bufferView); } } @@ -1113,7 +1113,7 @@ export class GLTFExporter { continue; } - const glTFMorphTarget = BuildMorphTargetBuffers(morphTarget, meshes[0], this._dataManager, this._bufferViews, this._accessors, state.convertToRightHanded); + const glTFMorphTarget = BuildMorphTargetBuffers(morphTarget, meshes[0], this._bufferManager, this._bufferViews, this._accessors, state.convertToRightHanded); for (const mesh of meshes) { state.bindMorphDataToMesh(mesh, glTFMorphTarget); @@ -1159,7 +1159,7 @@ export class GLTFExporter { idleGLTFAnimations, this._nodeMap, this._nodes, - this._dataManager, + this._bufferManager, this._bufferViews, this._accessors, this._animationSampleRate, @@ -1173,7 +1173,7 @@ export class GLTFExporter { idleGLTFAnimations, this._nodeMap, this._nodes, - this._dataManager, + this._bufferManager, this._bufferViews, this._accessors, this._animationSampleRate, @@ -1339,10 +1339,10 @@ export class GLTFExporter { let accessorIndex = state.getIndicesAccessor(indices, start, count, offset, flip); if (accessorIndex === undefined) { const bytes = IndicesArrayToTypedArray(indicesToExport, start, count, is32Bits); - const bufferView = this._dataManager.createBufferView(bytes); + const bufferView = this._bufferManager.createBufferView(bytes); const componentType = is32Bits ? AccessorComponentType.UNSIGNED_INT : AccessorComponentType.UNSIGNED_SHORT; - this._accessors.push(this._dataManager.createAccessor(bufferView, AccessorType.SCALAR, componentType, count, 0)); + this._accessors.push(this._bufferManager.createAccessor(bufferView, AccessorType.SCALAR, componentType, count, 0)); accessorIndex = this._accessors.length - 1; state.setIndicesAccessor(indices, start, count, offset, flip, accessorIndex); } @@ -1381,7 +1381,7 @@ export class GLTFExporter { const byteOffset = vertexBuffer.byteOffset + start * vertexBuffer.byteStride; this._accessors.push( - this._dataManager.createAccessor( + this._bufferManager.createAccessor( bufferView, GetAccessorType(kind, state.hasVertexColorAlpha(vertexBuffer)), vertexBufferType, diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts index 939de5bb26d..0e4d08e9d58 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts @@ -8,7 +8,7 @@ import type { IDisposable } from "core/scene"; import type { IGLTFExporterExtension } from "../glTFFileExporter"; import type { Material } from "core/Materials/material"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; -import type { DataWriter } from "./dataWriter"; +import type { BufferManager } from "./bufferManager"; /** @internal */ // eslint-disable-next-line no-var, @typescript-eslint/naming-convention @@ -40,7 +40,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo * Define this method to modify the default behavior when exporting a mesh primitive * @returns nullable IMeshPrimitive promise */ - postExportMeshPrimitiveAsync?(primitive: IMeshPrimitive, dataManager: DataWriter, accessors: IAccessor[]): Promise; + postExportMeshPrimitiveAsync?(primitive: IMeshPrimitive, bufferManager: BufferManager, accessors: IAccessor[]): Promise; /** * Define this method to modify the default behavior when exporting a node @@ -57,7 +57,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo babylonNode: Node, nodeMap: Map, convertToRightHanded: boolean, - dataWriter: DataWriter + bufferManager: BufferManager ): Promise>; /** @@ -80,7 +80,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo * todo * @returns todo */ - preGenerateBinaryAsync?(dataManager: DataWriter): Promise; + preGenerateBinaryAsync?(bufferManager: BufferManager): Promise; /** Gets a boolean indicating that this extension was used */ wasUsed: boolean; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts index 954f153beda..29071c6044d 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts @@ -967,10 +967,10 @@ export class GLTFMaterialExporter { image = { name: name, mimeType: mimeType, - bufferView: undefined, // Will be updated later by DataManager + bufferView: undefined, // Will be updated later by BufferManager }; - const bufferView = this._exporter._dataManager.createBufferView(new Uint8Array(data)); - this._exporter._dataManager.setBufferView(image, bufferView); + const bufferView = this._exporter._bufferManager.createBufferView(new Uint8Array(data)); + this._exporter._bufferManager.setBufferView(image, bufferView); } else { // Build a unique URI const baseName = name.replace(/\.\/|\/|\.\\|\\/g, "_"); diff --git a/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts b/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts index fab79e3c133..083afdd82cf 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts @@ -1,7 +1,7 @@ import type { IBufferView, IAccessor } from "babylonjs-gltf2interface"; import { AccessorComponentType, AccessorType } from "babylonjs-gltf2interface"; import type { MorphTarget } from "core/Morph/morphTarget"; -import type { DataWriter } from "./dataWriter"; +import type { BufferManager } from "./bufferManager"; import { NormalizeTangent } from "./glTFUtilities"; import type { Mesh } from "core/Meshes/mesh"; @@ -22,7 +22,7 @@ export interface IMorphTargetData { export function BuildMorphTargetBuffers( morphTarget: MorphTarget, mesh: Mesh, - dataWriter: DataWriter, + bufferManager: BufferManager, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHanded: boolean @@ -69,8 +69,8 @@ export function BuildMorphTargetBuffers( positionData[i * 3 + 2] = difference.z; } - const bufferView = dataWriter.createBufferView(positionData, floatSize * 3); - const accessor = dataWriter.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, morphPositions.length / 3, 0, { min, max }); + const bufferView = bufferManager.createBufferView(positionData, floatSize * 3); + const accessor = bufferManager.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, morphPositions.length / 3, 0, { min, max }); accessors.push(accessor); result.attributes["POSITION"] = accessors.length - 1; } else { @@ -96,8 +96,8 @@ export function BuildMorphTargetBuffers( normalData[i * 3 + 2] = difference.z; } - const bufferView = dataWriter.createBufferView(normalData, floatSize * 3); - const accessor = dataWriter.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, morphNormals.length / 3, 0); + const bufferView = bufferManager.createBufferView(normalData, floatSize * 3); + const accessor = bufferManager.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, morphNormals.length / 3, 0); accessors.push(accessor); result.attributes["NORMAL"] = accessors.length - 1; } else { @@ -127,8 +127,8 @@ export function BuildMorphTargetBuffers( tangentData[i * 3 + 1] = difference.y; tangentData[i * 3 + 2] = difference.z; } - const bufferView = dataWriter.createBufferView(tangentData, floatSize * 3); - const accessor = dataWriter.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, vertexCount, 0); + const bufferView = bufferManager.createBufferView(tangentData, floatSize * 3); + const accessor = bufferManager.createAccessor(bufferView, AccessorType.VEC3, AccessorComponentType.FLOAT, vertexCount, 0); accessors.push(accessor); result.attributes["TANGENT"] = accessors.length - 1; } else { From 1474308649f55837f60efb601eee5174c57c6bb7 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:43:53 -0500 Subject: [PATCH 04/32] Remove unnecessary async + return value from postExportMeshPrimitive; track encode promises in extension --- .../Extensions/KHR_draco_mesh_compression.ts | 45 +++++++++++-------- .../serializers/src/glTF/2.0/glTFExporter.ts | 17 ++++--- .../src/glTF/2.0/glTFExporterExtension.ts | 2 +- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index 066e87eea6e..d1150a3dfbc 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -45,6 +45,9 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { /** Accessors that were replaced with Draco data, which may be eligible for removal after Draco encoding */ private _accessorsUsed: Set = new Set(); + /** Promise pool for Draco encoding work */ + private _encodePromises: Promise[] = []; + private _wasUsed = false; /** @internal */ @@ -69,14 +72,14 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { } /** @internal */ - public async postExportMeshPrimitiveAsync(primitive: IMeshPrimitive, bufferManager: BufferManager, accessors: IAccessor[]): Promise { + public postExportMeshPrimitive(primitive: IMeshPrimitive, bufferManager: BufferManager, accessors: IAccessor[]): void { if (!this.enabled) { - return primitive; + return; } if (primitive.mode !== MeshPrimitiveMode.TRIANGLES && primitive.mode !== MeshPrimitiveMode.TRIANGLE_STRIP) { Logger.Warn("Cannot compress primitive with mode " + primitive.mode + "."); - return primitive; + return; } // Prepare indices for Draco encoding @@ -119,24 +122,26 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { method: primitive.targets ? "MESH_SEQUENTIAL_ENCODING" : "MESH_EDGEBREAKER_ENCODING", }; - const encodedData = await DracoEncoder.Default._encodeAsync(attributes, indices, options); - if (!encodedData) { - return primitive; // Draco encoding failed - } - - const dracoInfo: IKHRDracoMeshCompression = { - bufferView: -1, // bufferView will be set to a real index later, when we write the binary and decide bufferView ordering - attributes: encodedData.attributeIDs, - }; - const bufferView = bufferManager.createBufferView(encodedData.data); - bufferManager.setBufferView(dracoInfo, bufferView); - - primitive.extensions ||= {}; - primitive.extensions[NAME] = dracoInfo; + this._encodePromises.push( + DracoEncoder.Default._encodeAsync(attributes, indices, options).then((encodedData) => { + if (!encodedData) { + Logger.Warn("Draco encoding failed for primitive."); + return; + } + + const dracoInfo: IKHRDracoMeshCompression = { + bufferView: -1, // bufferView will be set to a real index later, when we write the binary and decide bufferView ordering + attributes: encodedData.attributeIDs, + }; + const bufferView = bufferManager.createBufferView(encodedData.data); + bufferManager.setBufferView(dracoInfo, bufferView); + + primitive.extensions ||= {}; + primitive.extensions[NAME] = dracoInfo; + }) + ); this._wasUsed = true; - - return primitive; // TODO: Why return this? No need. } /** @internal */ @@ -145,6 +150,8 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { return; } + await Promise.all(this._encodePromises); + // Cull obsolete bufferViews that are no longer needed, as they were replaced with Draco data for (const bufferView of this._bufferViewsUsed) { const references = bufferManager.getProperties(bufferView); diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 708601fdebf..e6ff38c1f6e 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -299,11 +299,14 @@ export class GLTFExporter { return this._applyExtensions(babylonTexture, (extension, node) => extension.preExportTextureAsync && extension.preExportTextureAsync(context, node, mimeType)); } - public _extensionsPostExportMeshPrimitiveAsync(primitive: IMeshPrimitive): Promise> { - return this._applyExtensions( - primitive, - (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(node, this._bufferManager, this._accessors) - ); + public _extensionsPostExportMeshPrimitive(primitive: IMeshPrimitive): void { + for (const name of GLTFExporter._ExtensionNames) { + const extension = this._extensions[name]; + + if (extension.postExportMeshPrimitive) { + extension.postExportMeshPrimitive(primitive, this._bufferManager, this._accessors); + } + } } public _extensionsPostExportNodeAsync(context: string, node: INode, babylonNode: Node, nodeMap: Map, convertToRightHanded: boolean): Promise> { @@ -346,7 +349,7 @@ export class GLTFExporter { const extension = this._extensions[name]; if (extension.preGenerateBinaryAsync) { - extension.preGenerateBinaryAsync(this._bufferManager); + await extension.preGenerateBinaryAsync(this._bufferManager); } } } @@ -1487,7 +1490,7 @@ export class GLTFExporter { } mesh.primitives.push(primitive); - await this._extensionsPostExportMeshPrimitiveAsync(primitive); + this._extensionsPostExportMeshPrimitive(primitive); } } diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts index 0e4d08e9d58..eb42d3581ac 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts @@ -40,7 +40,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo * Define this method to modify the default behavior when exporting a mesh primitive * @returns nullable IMeshPrimitive promise */ - postExportMeshPrimitiveAsync?(primitive: IMeshPrimitive, bufferManager: BufferManager, accessors: IAccessor[]): Promise; + postExportMeshPrimitive?(primitive: IMeshPrimitive, bufferManager: BufferManager, accessors: IAccessor[]): void; /** * Define this method to modify the default behavior when exporting a node From a06a53671f9f3d42c2579e2f7dc33dd4a0ce28ac Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:22:42 -0500 Subject: [PATCH 05/32] Use DataWriter in BufferManager --- .../serializers/src/glTF/2.0/bufferManager.ts | 79 ++++----------- .../serializers/src/glTF/2.0/dataWriter.ts | 95 +++++++++++++++++++ 2 files changed, 113 insertions(+), 61 deletions(-) create mode 100644 packages/dev/serializers/src/glTF/2.0/dataWriter.ts diff --git a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts index 5167205a87a..58695269b51 100644 --- a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts +++ b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts @@ -1,5 +1,6 @@ import type { TypedArray } from "core/types"; import type { AccessorComponentType, AccessorType, IAccessor, IBufferView } from "babylonjs-gltf2interface"; +import { DataWriter } from "./dataWriter"; interface IPropertyWithBufferView { bufferView?: number; @@ -25,72 +26,30 @@ export class BufferManager { * @returns The binary buffer */ public generateBinary(bufferViews: IBufferView[]): Uint8Array { - // Allocate the ArrayBuffer + // Construct a DataWriter with the total byte length to prevent resizing let totalByteLength = 0; - for (const bufferView of this._bufferViewToData.keys()) { - bufferView.byteOffset = totalByteLength; - totalByteLength += bufferView.byteLength; - } - const buffer = new ArrayBuffer(totalByteLength); - const dataView = new DataView(buffer); // To write in little endian + this._bufferViewToData.forEach((data) => { + totalByteLength += data.byteLength; + }); + const dataWriter = new DataWriter(totalByteLength); // Fill in the bufferViews list and missing bufferView index references while writing the binary - let byteOffset = 0; - for (const [bufferView, data] of this._bufferViewToData.entries()) { - bufferView.byteOffset = byteOffset; + this._bufferViewToData.forEach((data, bufferView) => { + bufferView.byteOffset = dataWriter.byteOffset; bufferViews.push(bufferView); const bufferViewIndex = bufferViews.length - 1; - const properties = this._bufferViewToProperties.get(bufferView)!; + const properties = this.getProperties(bufferView); for (const object of properties) { object.bufferView = bufferViewIndex; } - const type = data.constructor.name; - for (let i = 0; i < data.length; i++) { - const value = data[i]; - switch (type) { - case "Int8Array": - dataView.setInt8(byteOffset, value); - byteOffset += 1; - break; - case "Uint8Array": - dataView.setUint8(byteOffset, value); - byteOffset += 1; - break; - case "Int16Array": - dataView.setInt16(byteOffset, value, true); - byteOffset += 2; - break; - case "Uint16Array": - dataView.setUint16(byteOffset, value, true); - byteOffset += 2; - break; - case "Int32Array": - dataView.setInt32(byteOffset, value, true); - byteOffset += 4; - break; - case "Uint32Array": - dataView.setUint32(byteOffset, value, true); - byteOffset += 4; - break; - case "Float32Array": - dataView.setFloat32(byteOffset, value, true); - byteOffset += 4; - break; - case "Float64Array": - dataView.setFloat64(byteOffset, value, true); - byteOffset += 8; - break; - default: - throw new Error("Unsupported TypedArray type: " + type); - } - } + dataWriter.writeTypedArray(data); - this._bufferViewToData.delete(bufferView); - } + this._bufferViewToData.delete(bufferView); // Try to free up memory ASAP + }); - return new Uint8Array(buffer, 0, byteOffset); + return dataWriter.getOutputData(); } /** @@ -193,12 +152,10 @@ export class BufferManager { throw new Error(`BufferView ${bufferView} not found in DataWriter.`); } - const properties = this._bufferViewToProperties.get(bufferView); - if (properties) { - for (const object of properties) { - if (object.bufferView) { - delete object.bufferView; - } + const properties = this.getProperties(bufferView); + for (const object of properties) { + if (object.bufferView !== undefined) { + delete object.bufferView; } } @@ -207,7 +164,7 @@ export class BufferManager { this._accessorToBufferView.forEach((bv, accessor) => { if (bv === bufferView) { // Additionally, remove byteOffset from accessor referencing this bufferView - if (accessor.byteOffset) { + if (accessor.byteOffset !== undefined) { delete accessor.byteOffset; } this._accessorToBufferView.delete(accessor); diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts new file mode 100644 index 00000000000..46abbd0e841 --- /dev/null +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable babylonjs/available */ +import type { TypedArray } from "core/types"; + +/** @internal */ +export class DataWriter { + private _data: Uint8Array; + private _dataView: DataView; + private _byteOffset: number; + + private _typedArrayToWriteMethod: Record = { + Int8Array: this.writeInt8.bind(this), + Uint8Array: this.writeUInt8.bind(this), + Uint8ClampedArray: this.writeUInt8.bind(this), + Int16Array: this.writeInt16.bind(this), + Uint16Array: this.writeUInt16.bind(this), + Int32Array: this.writeInt32.bind(this), + Uint32Array: this.writeUInt32.bind(this), + Float32Array: this.writeFloat32.bind(this), + }; + + public constructor(byteLength: number) { + this._data = new Uint8Array(byteLength); + this._dataView = new DataView(this._data.buffer); + this._byteOffset = 0; + } + + public get byteOffset(): number { + return this._byteOffset; + } + + public getOutputData(): Uint8Array { + return new Uint8Array(this._data.buffer, 0, this._byteOffset); + } + + public writeTypedArray(value: TypedArray): void { + this._checkGrowBuffer(value.byteLength); + const setMethod = this._typedArrayToWriteMethod[value.constructor.name]; + for (let i = 0; i < value.length; i++) { + setMethod(value[i]); + } + } + + public writeUInt8(value: number): void { + this._checkGrowBuffer(1); + this._dataView.setUint8(this._byteOffset, value); + this._byteOffset++; + } + + public writeInt8(value: number): void { + this._checkGrowBuffer(1); + this._dataView.setInt8(this._byteOffset, value); + this._byteOffset++; + } + + public writeInt16(value: number): void { + this._checkGrowBuffer(2); + this._dataView.setInt16(this._byteOffset, value, true); + this._byteOffset += 2; + } + + public writeUInt16(value: number): void { + this._checkGrowBuffer(2); + this._dataView.setUint16(this._byteOffset, value, true); + this._byteOffset += 2; + } + + public writeInt32(value: number): void { + this._checkGrowBuffer(4); + this._dataView.setInt32(this._byteOffset, value, true); + this._byteOffset += 4; + } + + public writeUInt32(value: number): void { + this._checkGrowBuffer(4); + this._dataView.setUint32(this._byteOffset, value, true); + this._byteOffset += 4; + } + + public writeFloat32(value: number): void { + this._checkGrowBuffer(4); + this._dataView.setFloat32(this._byteOffset, value, true); + this._byteOffset += 4; + } + + private _checkGrowBuffer(byteLength: number): void { + const newByteLength = this.byteOffset + byteLength; + if (newByteLength > this._data.byteLength) { + const newData = new Uint8Array(newByteLength * 2); + newData.set(this._data); + this._data = newData; + this._dataView = new DataView(this._data.buffer); + } + } +} From 32dab1bc1d37ec1658aadbd9b4ddd9c0efecdaab Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:24:26 -0500 Subject: [PATCH 06/32] Unrelated: avoid exporting buffers not associated with standard vertex attributes --- packages/dev/serializers/src/glTF/2.0/glTFExporter.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index e6ff38c1f6e..e02cb4030fa 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -911,6 +911,9 @@ export class GLTFExporter { const vertexBuffers = babylonNode.geometry.getVertexBuffers(); if (vertexBuffers) { for (const kind in vertexBuffers) { + if (!IsStandardVertexAttribute(kind)) { + continue; + } const vertexBuffer = vertexBuffers[kind]; state.setHasVertexColorAlpha(vertexBuffer, babylonNode.hasVertexAlpha); const buffer = vertexBuffer._buffer; From e877b1259fb26e2ae0aea717c1fee6de26290521 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:49:30 -0500 Subject: [PATCH 07/32] Don't use Set iterator to fix UMD build --- .../src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index d1150a3dfbc..fa65583d8b4 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -153,17 +153,15 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { await Promise.all(this._encodePromises); // Cull obsolete bufferViews that are no longer needed, as they were replaced with Draco data - for (const bufferView of this._bufferViewsUsed) { + this._bufferViewsUsed.forEach((bufferView) => { const references = bufferManager.getProperties(bufferView); - const bufferViewOnlyUsedByDraco = references.every((object) => { return this._accessorsUsed.has(object as IAccessor); // has() can handle any object, but TS doesn't know that }); - if (bufferViewOnlyUsedByDraco) { bufferManager.removeBufferView(bufferView); } - } + }); this._bufferViewsUsed.clear(); this._accessorsUsed.clear(); From 1324e6819be447ea55a7a78bc16d717450681fbb Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Wed, 15 Jan 2025 23:42:16 -0500 Subject: [PATCH 08/32] Maintain buffer alignment --- .../dev/serializers/src/glTF/2.0/bufferManager.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts index 58695269b51..372ca005517 100644 --- a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts +++ b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts @@ -6,6 +6,12 @@ interface IPropertyWithBufferView { bufferView?: number; } +function getHighestByteAlignment(byteLength: number): number { + if (byteLength % 4 === 0) return 4; + if (byteLength % 2 === 0) return 2; + return 1; +} + /** * Utility class to centralize the management of binary data, bufferViews, and the objects that reference them. * @internal @@ -33,8 +39,11 @@ export class BufferManager { }); const dataWriter = new DataWriter(totalByteLength); + // Order the bufferViews in descending order of their alignment requirements + const orderedBufferViews = [...this._bufferViewToData.keys()].sort((a, b) => getHighestByteAlignment(b.byteLength) - getHighestByteAlignment(a.byteLength)); + // Fill in the bufferViews list and missing bufferView index references while writing the binary - this._bufferViewToData.forEach((data, bufferView) => { + for (const bufferView of orderedBufferViews) { bufferView.byteOffset = dataWriter.byteOffset; bufferViews.push(bufferView); @@ -44,10 +53,10 @@ export class BufferManager { object.bufferView = bufferViewIndex; } - dataWriter.writeTypedArray(data); + dataWriter.writeTypedArray(this._bufferViewToData.get(bufferView)!); this._bufferViewToData.delete(bufferView); // Try to free up memory ASAP - }); + } return dataWriter.getOutputData(); } From dca39d73f16e39424718dae38ec17ed965018f31 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:59:35 -0500 Subject: [PATCH 09/32] Fix SKIN_IBM_ACCESSOR_WITH_BYTESTRIDE (why not) --- packages/dev/serializers/src/glTF/2.0/glTFExporter.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index e02cb4030fa..ff0c548ba3a 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -794,8 +794,7 @@ export class GLTFExporter { // Only create skeleton if it has at least one joint and is used by a mesh. if (skin.joints.length > 0 && skinedNodes !== undefined) { // Put IBM data into TypedArraybuffer view - const byteStride = 64; // 4 x 4 matrix of 32 bit float - const byteLength = inverseBindMatrices.length * byteStride; + const byteLength = inverseBindMatrices.length * 64; // Always a 4 x 4 matrix of 32 bit float const inverseBindMatricesData = new Float32Array(byteLength / 4); inverseBindMatrices.forEach((mat: Matrix, index: number) => { mat.m.forEach((cell: number, cellIndex: number) => { @@ -803,7 +802,7 @@ export class GLTFExporter { }); }); // Create buffer view and accessor - const bufferView = this._bufferManager.createBufferView(inverseBindMatricesData, byteStride); + const bufferView = this._bufferManager.createBufferView(inverseBindMatricesData); this._accessors.push(this._bufferManager.createAccessor(bufferView, AccessorType.MAT4, AccessorComponentType.FLOAT, inverseBindMatrices.length)); skin.inverseBindMatrices = this._accessors.length - 1; From f9e2df2683b444d579aaf482d5954d09bdb2d10f Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 16 Jan 2025 01:28:52 -0500 Subject: [PATCH 10/32] Don't cull buffers of primitives whose encoding failed --- .../Extensions/KHR_draco_mesh_compression.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index fa65583d8b4..9cd02208c74 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -82,15 +82,20 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { return; } + // Collect bufferViews and accessors used by this primitive + const primitiveBufferViews: IBufferView[] = []; + const primitiveAccessors: IAccessor[] = []; + // Prepare indices for Draco encoding let indices: Nullable = null; if (primitive.indices !== undefined) { const accessor = accessors[primitive.indices]; const bufferView = bufferManager.getBufferView(accessor); - this._bufferViewsUsed.add(bufferView); - this._accessorsUsed.add(accessor); // Per exportIndices, indices must be either Uint16Array or Uint32Array indices = bufferManager.getData(bufferView) as Uint32Array | Uint16Array; + + primitiveBufferViews.push(bufferView); + primitiveAccessors.push(accessor); } // Prepare attributes for Draco encoding @@ -98,8 +103,6 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { for (const [name, accessorIndex] of Object.entries(primitive.attributes)) { const accessor = accessors[accessorIndex]; const bufferView = bufferManager.getBufferView(accessor); - this._bufferViewsUsed.add(bufferView); - this._accessorsUsed.add(accessor); const data = bufferManager.getData(bufferView); const size = GetAccessorElementCount(accessor.type); @@ -115,6 +118,9 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { ) as Float32Array; // Because data is a TypedArray, GetFloatData will return a Float32Array attributes.push({ attribute: name, dracoAttribute: getDracoAttributeName(name), size: GetAccessorElementCount(accessor.type), data: floatData }); + + primitiveBufferViews.push(bufferView); + primitiveAccessors.push(accessor); } // Use sequential encoding to preserve vertex order for cases like morph targets @@ -136,6 +142,13 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { const bufferView = bufferManager.createBufferView(encodedData.data); bufferManager.setBufferView(dracoInfo, bufferView); + for (const bufferView of primitiveBufferViews) { + this._bufferViewsUsed.add(bufferView); + } + for (const accessor of primitiveAccessors) { + this._accessorsUsed.add(accessor); + } + primitive.extensions ||= {}; primitive.extensions[NAME] = dracoInfo; }) From 7ebb7534b67bbef2a854c07b2d24cb3d3531169b Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 16 Jan 2025 02:01:56 -0500 Subject: [PATCH 11/32] Add roundtrip vis test --- .../glTFSerializerKhrDracoMeshCompression.png | Bin 0 -> 29545 bytes .../tools/tests/test/visualization/config.json | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKhrDracoMeshCompression.png diff --git a/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKhrDracoMeshCompression.png b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKhrDracoMeshCompression.png new file mode 100644 index 0000000000000000000000000000000000000000..18fab33c6fe8cbcba48cdaab4299ce816b0decd4 GIT binary patch literal 29545 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 literal 0 HcmV?d00001 diff --git a/packages/tools/tests/test/visualization/config.json b/packages/tools/tests/test/visualization/config.json index cea94073030..43f30acf09d 100644 --- a/packages/tools/tests/test/visualization/config.json +++ b/packages/tools/tests/test/visualization/config.json @@ -939,6 +939,11 @@ "referenceImage": "gltfSerializerMorphTargetAnimationGroup.png", "excludedEngines": ["webgl1"] }, + { + "title": "GLTF Serializer KHR draco mesh compression", + "playgroundId": "#F8BF8N", + "referenceImage": "glTFSerializerKhrDracoMeshCompression.png" + }, { "title": "GLTF Serializer KHR materials clearcoat", "playgroundId": "#9N6CLU#23", From 731b21116145acd5f48f6804166fc1479d31fc38 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 16 Jan 2025 03:20:29 -0500 Subject: [PATCH 12/32] Clean up --- .../2.0/Extensions/EXT_mesh_gpu_instancing.ts | 2 +- .../Extensions/KHR_draco_mesh_compression.ts | 28 +++---- .../serializers/src/glTF/2.0/bufferManager.ts | 76 +++++++++---------- .../serializers/src/glTF/2.0/glTFAnimation.ts | 39 +++++++--- .../serializers/src/glTF/2.0/glTFExporter.ts | 33 ++++---- .../src/glTF/2.0/glTFExporterExtension.ts | 11 ++- .../src/glTF/2.0/glTFMaterialExporter.ts | 2 +- .../src/glTF/2.0/glTFSerializer.ts | 12 +-- 8 files changed, 105 insertions(+), 98 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts index 21f6398db4b..96367f2795e 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts @@ -147,7 +147,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, bufferManager: BufferManager, componentType: AccessorComponentType): number { // write the buffer - let data; + let data: Float32Array | Int8Array | Int16Array; switch (componentType) { case AccessorComponentType.FLOAT: { data = new Float32Array(buffer.length); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index 9cd02208c74..9b1b8b0de33 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -61,15 +61,7 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { } /** @internal */ - public dispose() { - this._wasUsed = false; - DracoEncoder.ResetDefault(); - } - - /** @internal */ - public onExporting(): void { - this.dispose(); - } + public dispose() {} /** @internal */ public postExportMeshPrimitive(primitive: IMeshPrimitive, bufferManager: BufferManager, accessors: IAccessor[]): void { @@ -106,7 +98,7 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { const data = bufferManager.getData(bufferView); const size = GetAccessorElementCount(accessor.type); - // TODO: In future, find a way to preserve original data type to avoid copying in some cases + // TODO: In future, find a way to preserve original data type to curb unnecessary copies const floatData = GetFloatData( data, size, @@ -128,8 +120,8 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { method: primitive.targets ? "MESH_SEQUENTIAL_ENCODING" : "MESH_EDGEBREAKER_ENCODING", }; - this._encodePromises.push( - DracoEncoder.Default._encodeAsync(attributes, indices, options).then((encodedData) => { + const promise = DracoEncoder.Default._encodeAsync(attributes, indices, options) + .then((encodedData) => { if (!encodedData) { Logger.Warn("Draco encoding failed for primitive."); return; @@ -152,7 +144,11 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { primitive.extensions ||= {}; primitive.extensions[NAME] = dracoInfo; }) - ); + .catch((error) => { + Logger.Warn("Draco encoding failed for primitive: " + error); + }); + + this._encodePromises.push(promise); this._wasUsed = true; } @@ -165,13 +161,13 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { await Promise.all(this._encodePromises); - // Cull obsolete bufferViews that are no longer needed, as they were replaced with Draco data + // Cull obsolete bufferViews that were replaced with Draco data this._bufferViewsUsed.forEach((bufferView) => { const references = bufferManager.getProperties(bufferView); - const bufferViewOnlyUsedByDraco = references.every((object) => { + const onlyUsedByEncodedPrimitives = references.every((object) => { return this._accessorsUsed.has(object as IAccessor); // has() can handle any object, but TS doesn't know that }); - if (bufferViewOnlyUsedByDraco) { + if (onlyUsedByEncodedPrimitives) { bufferManager.removeBufferView(bufferView); } }); diff --git a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts index 372ca005517..91322fba6c1 100644 --- a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts +++ b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts @@ -17,13 +17,19 @@ function getHighestByteAlignment(byteLength: number): number { * @internal */ export class BufferManager { - // BufferView -> data + /** + * Maps a bufferView to its data + */ private _bufferViewToData: Map = new Map(); - // BufferView -> glTF objects + /** + * Maps a bufferView to glTF objects that reference it via a "bufferView" property (e.g. accessors, images) + */ private _bufferViewToProperties: Map = new Map(); - // Accessor -> bufferView + /** + * Maps an accessor to its bufferView + */ private _accessorToBufferView: Map = new Map(); /** @@ -98,10 +104,7 @@ export class BufferManager { minMax?: { min: number[]; max: number[] }, normalized?: boolean ): IAccessor { - if (!this._bufferViewToData.has(bufferView)) { - throw new Error(`BufferView ${bufferView} not found in DataWriter.`); - } - + this._verifyBufferView(bufferView); const accessor: IAccessor = { bufferView: undefined, // bufferView will be set to a real index later, once we write the binary and decide bufferView ordering componentType: componentType, @@ -118,49 +121,21 @@ export class BufferManager { } /** - * Assigns a bufferView to a glTF object + * Assigns a bufferView to a glTF object that references it * @param object The glTF object * @param bufferView The bufferView to assign */ public setBufferView(object: IPropertyWithBufferView, bufferView: IBufferView) { - if (!this._bufferViewToData.has(bufferView)) { - throw new Error(`BufferView ${bufferView} not found in DataWriter.`); - } - const properties = this._bufferViewToProperties.get(bufferView) ?? []; + this._verifyBufferView(bufferView); + const properties = this.getProperties(bufferView); properties.push(object); - this._bufferViewToProperties.set(bufferView, properties); - } - - public getBufferView(accessor: IAccessor): IBufferView { - const bufferView = this._accessorToBufferView.get(accessor); - if (!bufferView) { - throw new Error(`Accessor ${accessor} not found in DataWriter.`); - } - return bufferView; - } - - public getProperties(bufferView: IBufferView): IPropertyWithBufferView[] { - return this._bufferViewToProperties.get(bufferView) ?? []; - } - - public getData(bufferView: IBufferView): TypedArray { - const data = this._bufferViewToData.get(bufferView); - if (!data) { - throw new Error(`BufferView ${bufferView} not found in DataWriter.`); - } - return data; } /** - * Removes buffer view from the binary. - * Warning: This will also remove the bufferView info from all object that reference it. + * Removes buffer view from the binary data, as well as from all its known references * @param bufferView the bufferView to remove */ public removeBufferView(bufferView: IBufferView): void { - if (!this._bufferViewToData.has(bufferView)) { - throw new Error(`BufferView ${bufferView} not found in DataWriter.`); - } - const properties = this.getProperties(bufferView); for (const object of properties) { if (object.bufferView !== undefined) { @@ -180,4 +155,27 @@ export class BufferManager { } }); } + + public getBufferView(accessor: IAccessor): IBufferView { + const bufferView = this._accessorToBufferView.get(accessor); + this._verifyBufferView(bufferView); + return bufferView!; + } + + public getProperties(bufferView: IBufferView): IPropertyWithBufferView[] { + this._verifyBufferView(bufferView); + this._bufferViewToProperties.set(bufferView, this._bufferViewToProperties.get(bufferView) ?? []); + return this._bufferViewToProperties.get(bufferView)!; + } + + public getData(bufferView: IBufferView): TypedArray { + this._verifyBufferView(bufferView); + return this._bufferViewToData.get(bufferView)!; + } + + private _verifyBufferView(bufferView?: IBufferView): void { + if (bufferView === undefined || !this._bufferViewToData.has(bufferView)) { + throw new Error(`BufferView ${bufferView} not found in BufferManager.`); + } + } } diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index f859fdd0ac1..d9e5b7cad32 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -17,6 +17,7 @@ import { Camera } from "core/Cameras/camera"; import { Light } from "core/Lights/light"; import type { BufferManager } from "./bufferManager"; import { GetAccessorElementCount, ConvertToRightHandedPosition, ConvertCameraRotationToGLTF, ConvertToRightHandedRotation } from "./glTFUtilities"; +import { DataWriter } from "./dataWriter"; /** * @internal @@ -591,7 +592,7 @@ export class _GLTFAnimation { const nodeIndex = nodeMap.get(babylonTransformNode); // Create buffer view and accessor for key frames. - let data = new Float32Array(animationData.inputs); + const data = new Float32Array(animationData.inputs); bufferView = bufferManager.createBufferView(data); accessor = bufferManager.createAccessor(bufferView, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, undefined, { min: [animationData.inputsMin], @@ -600,27 +601,29 @@ export class _GLTFAnimation { accessors.push(accessor); keyframeAccessorIndex = accessors.length - 1; - // Convert keyed values into a flat array for writing. + // Perform conversions on keyed values while also building their buffer. const rotationQuaternion = new Quaternion(); const eulerVec3 = new Vector3(); const position = new Vector3(); const isCamera = babylonTransformNode instanceof Camera; - data = new Float32Array(animationData.outputs.length * GetAccessorElementCount(dataAccessorType)); - animationData.outputs.forEach(function (output: number[], index: number) { + const dataWriter = new DataWriter(animationData.outputs.length * GetAccessorElementCount(dataAccessorType)); + animationData.outputs.forEach(function (output: number[]) { if (convertToRightHanded) { switch (animationChannelTargetPath) { case AnimationChannelTargetPath.TRANSLATION: Vector3.FromArrayToRef(output, 0, position); ConvertToRightHandedPosition(position); - position.toArray(output); + + dataWriter.writeFloat32(position.x); + dataWriter.writeFloat32(position.y); + dataWriter.writeFloat32(position.z); break; case AnimationChannelTargetPath.ROTATION: if (output.length === 4) { Quaternion.FromArrayToRef(output, 0, rotationQuaternion); } else { - // TODO: should be impossible to get here, but just in case Vector3.FromArrayToRef(output, 0, eulerVec3); Quaternion.FromEulerVectorToRef(eulerVec3, rotationQuaternion); } @@ -633,7 +636,15 @@ export class _GLTFAnimation { } } - rotationQuaternion.toArray(output); + dataWriter.writeFloat32(rotationQuaternion.x); + dataWriter.writeFloat32(rotationQuaternion.y); + dataWriter.writeFloat32(rotationQuaternion.z); + dataWriter.writeFloat32(rotationQuaternion.w); + break; + default: + output.forEach((entry) => { + dataWriter.writeFloat32(entry); + }); break; } } else { @@ -642,7 +653,6 @@ export class _GLTFAnimation { if (output.length === 4) { Quaternion.FromArrayToRef(output, 0, rotationQuaternion); } else { - // TODO: should be impossible to get here, but just in case Vector3.FromArrayToRef(output, 0, eulerVec3); Quaternion.FromEulerVectorToRef(eulerVec3, rotationQuaternion); } @@ -650,15 +660,22 @@ export class _GLTFAnimation { ConvertCameraRotationToGLTF(rotationQuaternion); } - rotationQuaternion.toArray(output); + dataWriter.writeFloat32(rotationQuaternion.x); + dataWriter.writeFloat32(rotationQuaternion.y); + dataWriter.writeFloat32(rotationQuaternion.z); + dataWriter.writeFloat32(rotationQuaternion.w); + break; + default: + output.forEach((entry) => { + dataWriter.writeFloat32(entry); + }); break; } } - data.set(output, index * GetAccessorElementCount(dataAccessorType)); }); // Create buffer view and accessor for keyed values. - bufferView = bufferManager.createBufferView(data); + bufferView = bufferManager.createBufferView(dataWriter.getOutputData()); accessor = bufferManager.createAccessor(bufferView, dataAccessorType, AccessorComponentType.FLOAT, animationData.outputs.length); accessors.push(accessor); dataAccessorIndex = accessors.length - 1; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index ff0c548ba3a..7fd688c34c6 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -241,7 +241,7 @@ export class GLTFExporter { private readonly _options: Required; - public readonly _shouldUseGlb: boolean; + public _shouldUseGlb: boolean = false; public readonly _materialExporter = new GLTFMaterialExporter(this); @@ -299,16 +299,6 @@ export class GLTFExporter { return this._applyExtensions(babylonTexture, (extension, node) => extension.preExportTextureAsync && extension.preExportTextureAsync(context, node, mimeType)); } - public _extensionsPostExportMeshPrimitive(primitive: IMeshPrimitive): void { - for (const name of GLTFExporter._ExtensionNames) { - const extension = this._extensions[name]; - - if (extension.postExportMeshPrimitive) { - extension.postExportMeshPrimitive(primitive, this._bufferManager, this._accessors); - } - } - } - public _extensionsPostExportNodeAsync(context: string, node: INode, babylonNode: Node, nodeMap: Map, convertToRightHanded: boolean): Promise> { return this._applyExtensions( node, @@ -344,6 +334,16 @@ export class GLTFExporter { } } + public _extensionsPostExportMeshPrimitive(primitive: IMeshPrimitive): void { + for (const name of GLTFExporter._ExtensionNames) { + const extension = this._extensions[name]; + + if (extension.postExportMeshPrimitive) { + extension.postExportMeshPrimitive(primitive, this._bufferManager, this._accessors); + } + } + } + public async _extensionsPreGenerateBinaryAsync(): Promise { for (const name of GLTFExporter._ExtensionNames) { const extension = this._extensions[name]; @@ -393,13 +393,12 @@ export class GLTFExporter { } } - public constructor(babylonScene: Nullable = EngineStore.LastCreatedScene, shouldUseGlb: boolean, options?: IExportOptions) { + public constructor(babylonScene: Nullable = EngineStore.LastCreatedScene, options?: IExportOptions) { if (!babylonScene) { throw new Error("No scene available to export"); } this._babylonScene = babylonScene; - this._shouldUseGlb = shouldUseGlb; this._options = { shouldExportNode: () => true, @@ -502,13 +501,6 @@ export class GLTFExporter { return prettyPrint ? JSON.stringify(this._glTF, null, 2) : JSON.stringify(this._glTF); } - public async generateAsync(glTFPrefix: string): Promise { - if (this._shouldUseGlb) { - return this.generateGLBAsync(glTFPrefix); - } - return this.generateGLTFAsync(glTFPrefix); - } - public async generateGLTFAsync(glTFPrefix: string): Promise { const binaryBuffer = await this._generateBinaryAsync(); @@ -552,6 +544,7 @@ export class GLTFExporter { } public async generateGLBAsync(glTFPrefix: string): Promise { + this._shouldUseGlb = true; const binaryBuffer = await this._generateBinaryAsync(); this._extensionsOnExporting(); diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts index eb42d3581ac..b76c8b102b3 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts @@ -37,8 +37,10 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo postExportTexture?(context: string, textureInfo: ITextureInfo, babylonTexture: BaseTexture): void; /** - * Define this method to modify the default behavior when exporting a mesh primitive - * @returns nullable IMeshPrimitive promise + * Define this method to get notified when a primitive is created + * @param primitive glTF mesh primitive + * @param bufferManager Buffer manager + * @param accessors List of glTF accessors */ postExportMeshPrimitive?(primitive: IMeshPrimitive, bufferManager: BufferManager, accessors: IAccessor[]): void; @@ -49,6 +51,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo * @param babylonNode BabylonJS node * @param nodeMap Current node mapping of babylon node to glTF node index. Useful for combining nodes together. * @param convertToRightHanded Flag indicating whether to convert values to right-handed + * @param bufferManager Buffer manager * @returns nullable INode promise */ postExportNodeAsync?( @@ -77,8 +80,8 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo postExportMaterialAdditionalTextures?(context: string, node: IMaterial, babylonMaterial: Material): BaseTexture[]; /** - * todo - * @returns todo + * Define this method to modify the glTF buffer data before it is finalized and written + * @param bufferManager Buffer manager */ preGenerateBinaryAsync?(bufferManager: BufferManager): Promise; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts index 29071c6044d..b113337a506 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts @@ -984,7 +984,7 @@ export class GLTFMaterialExporter { name: name, uri: fileName, }; - this._exporter._imageData[fileName] = { data: data, mimeType: mimeType }; + this._exporter._imageData[fileName] = { data: data, mimeType: mimeType }; // Save image data to be written to file later } images.push(image); diff --git a/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts b/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts index 784c19650b2..de6bd3f0a91 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts @@ -56,8 +56,8 @@ export interface IExportOptions { includeCoordinateSystemConversionNodes?: boolean; /** - * Indicates if mesh data should be compressed using Draco to reduce binary file size. Defaults to false. - * If used, `extensionsRequired` property of the exported file will include "KHR_draco_mesh_compression". + * Indicates if geometries should be compressed with Draco. Defaults to false. + * NOTE: This may take a while if exporting many meshes. */ useDracoCompression?: boolean; } @@ -78,8 +78,8 @@ export class GLTF2Export { await scene.whenReadyAsync(); } - const exporter = new GLTFExporter(scene, false, options); - const data = await exporter.generateAsync(fileName.replace(/\.[^/.]+$/, "")); + const exporter = new GLTFExporter(scene, options); + const data = await exporter.generateGLTFAsync(fileName.replace(/\.[^/.]+$/, "")); exporter.dispose(); return data; @@ -97,8 +97,8 @@ export class GLTF2Export { await scene.whenReadyAsync(); } - const exporter = new GLTFExporter(scene, true, options); - const data = await exporter.generateAsync(fileName.replace(/\.[^/.]+$/, "")); + const exporter = new GLTFExporter(scene, options); + const data = await exporter.generateGLBAsync(fileName.replace(/\.[^/.]+$/, "")); exporter.dispose(); return data; From 53f27ad610abe44b4080226fc5451165f99a81db Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:40:58 -0500 Subject: [PATCH 13/32] Clean up --- .../src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts | 2 +- packages/dev/serializers/src/glTF/2.0/bufferManager.ts | 2 +- packages/dev/serializers/src/glTF/2.0/dataWriter.ts | 8 ++++---- packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts index 96367f2795e..b0ad384fe9c 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts @@ -48,7 +48,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { * @param babylonNode the corresponding babylon node * @param nodeMap map from babylon node id to node index * @param convertToRightHanded true if we need to convert data from left hand to right hand system. - * @param bufferManager binary writer + * @param bufferManager buffer manager * @returns nullable promise, resolves with the node */ public postExportNodeAsync( diff --git a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts index 91322fba6c1..b8557fde21a 100644 --- a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts +++ b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts @@ -33,7 +33,7 @@ export class BufferManager { private _accessorToBufferView: Map = new Map(); /** - * Generates a binary buffer from the stored bufferViews. Also creates in the bufferViews list. + * Generates a binary buffer from the stored bufferViews. Also populates the bufferViews list. * @param bufferViews The list of bufferViews to be populated while writing the binary * @returns The binary buffer */ diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts index 46abbd0e841..191f40c534f 100644 --- a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -53,9 +53,9 @@ export class DataWriter { this._byteOffset++; } - public writeInt16(value: number): void { + public writeInt16(entry: number): void { this._checkGrowBuffer(2); - this._dataView.setInt16(this._byteOffset, value, true); + this._dataView.setInt16(this._byteOffset, entry, true); this._byteOffset += 2; } @@ -65,9 +65,9 @@ export class DataWriter { this._byteOffset += 2; } - public writeInt32(value: number): void { + public writeInt32(entry: number): void { this._checkGrowBuffer(4); - this._dataView.setInt32(this._byteOffset, value, true); + this._dataView.setInt32(this._byteOffset, entry, true); this._byteOffset += 4; } diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index d9e5b7cad32..5f0b62db059 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -642,7 +642,7 @@ export class _GLTFAnimation { dataWriter.writeFloat32(rotationQuaternion.w); break; default: - output.forEach((entry) => { + output.forEach(function (entry) { dataWriter.writeFloat32(entry); }); break; @@ -666,7 +666,7 @@ export class _GLTFAnimation { dataWriter.writeFloat32(rotationQuaternion.w); break; default: - output.forEach((entry) => { + output.forEach(function (entry) { dataWriter.writeFloat32(entry); }); break; From 3e91566ed6de43751d6a6d134653c45078f4b757 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:20:17 -0500 Subject: [PATCH 14/32] Fix iterator UMD issue --- packages/dev/serializers/src/glTF/2.0/bufferManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts index b8557fde21a..00451dfca20 100644 --- a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts +++ b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts @@ -46,7 +46,7 @@ export class BufferManager { const dataWriter = new DataWriter(totalByteLength); // Order the bufferViews in descending order of their alignment requirements - const orderedBufferViews = [...this._bufferViewToData.keys()].sort((a, b) => getHighestByteAlignment(b.byteLength) - getHighestByteAlignment(a.byteLength)); + const orderedBufferViews = Array.from(this._bufferViewToData.keys()).sort((a, b) => getHighestByteAlignment(b.byteLength) - getHighestByteAlignment(a.byteLength)); // Fill in the bufferViews list and missing bufferView index references while writing the binary for (const bufferView of orderedBufferViews) { From 6e342d95bb50e57b6129171823734bc6d726cd7f Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:01:59 -0500 Subject: [PATCH 15/32] Unrelated: prevent NaN in tangents and normals --- packages/dev/serializers/src/glTF/2.0/glTFExporter.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 22741db165a..9a0539e60de 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -988,10 +988,13 @@ export class GLTFExporter { case VertexBuffer.NormalKind: case VertexBuffer.TangentKind: { EnumerateFloatValues(bytes, byteOffset, byteStride, size, type, maxTotalVertices * size, normalized, (values) => { - const invLength = 1 / Math.sqrt(values[0] * values[0] + values[1] * values[1] + values[2] * values[2]); - values[0] *= invLength; - values[1] *= invLength; - values[2] *= invLength; + const length = Math.sqrt(values[0] * values[0] + values[1] * values[1] + values[2] * values[2]); + if (length > 0) { + const invLength = 1 / length; + values[0] *= invLength; + values[1] *= invLength; + values[2] *= invLength; + } }); break; } From 7bde3e42be218e1cf17dfb4773333931a37c25a5 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:24:01 -0500 Subject: [PATCH 16/32] Update TypedArray type --- packages/dev/core/src/types.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/types.ts b/packages/dev/core/src/types.ts index 44ab40ca98b..571f6204b55 100644 --- a/packages/dev/core/src/types.ts +++ b/packages/dev/core/src/types.ts @@ -136,8 +136,22 @@ export type Tuple = _Tuple; export type FloatArray = number[] | Float32Array; /** Alias type for number array or Float32Array or Int32Array or Uint32Array or Uint16Array */ export type IndicesArray = number[] | Int32Array | Uint32Array | Uint16Array; -/** Union of all TypedArrays up to 32 bits */ -export type TypedArray = Float32Array | Uint32Array | Uint16Array | Uint8Array | Uint8ClampedArray | Int32Array | Int16Array | Int8Array; + +/** + * Alias type for all TypedArrays + */ +export type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array; /** * Alias for types that can be used by a Buffer or VertexBuffer. From 5b55b61f237b983a6a8c3b4ecb5a293394b63155 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:27:03 -0500 Subject: [PATCH 17/32] Log errors --- .../src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index 9b1b8b0de33..a12e0b80095 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -123,7 +123,7 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { const promise = DracoEncoder.Default._encodeAsync(attributes, indices, options) .then((encodedData) => { if (!encodedData) { - Logger.Warn("Draco encoding failed for primitive."); + Logger.Error("Draco encoding failed for primitive."); return; } @@ -145,7 +145,7 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { primitive.extensions[NAME] = dracoInfo; }) .catch((error) => { - Logger.Warn("Draco encoding failed for primitive: " + error); + Logger.Error("Draco encoding failed for primitive: " + error); }); this._encodePromises.push(promise); From d5d5479a81a72811e02355dadfe2e776afef155d Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:28:26 -0500 Subject: [PATCH 18/32] Rename to kind, dracoName --- .../core/src/Meshes/Compression/dracoCompressionWorker.ts | 8 ++++---- packages/dev/core/src/Meshes/Compression/dracoEncoder.ts | 2 +- .../dev/core/src/Meshes/Compression/dracoEncoder.types.ts | 6 +++--- .../src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/dev/core/src/Meshes/Compression/dracoCompressionWorker.ts b/packages/dev/core/src/Meshes/Compression/dracoCompressionWorker.ts index 316a82ff94a..4f224b42c68 100644 --- a/packages/dev/core/src/Meshes/Compression/dracoCompressionWorker.ts +++ b/packages/dev/core/src/Meshes/Compression/dracoCompressionWorker.ts @@ -34,7 +34,7 @@ export function EncodeMesh( const attributeIDs: Record = {}; // Babylon kind -> Draco unique id // Double-check that at least a position attribute is provided - const positionAttribute = attributes.find((a) => a.dracoAttribute === "POSITION"); + const positionAttribute = attributes.find((a) => a.dracoName === "POSITION"); if (!positionAttribute) { throw new Error("Position attribute is required for Draco encoding"); } @@ -61,9 +61,9 @@ export function EncodeMesh( // Add the attributes for (const attribute of attributes) { const verticesCount = attribute.data.length / attribute.size; - attributeIDs[attribute.attribute] = meshBuilder.AddFloatAttribute(mesh, encoderModule[attribute.dracoAttribute], verticesCount, attribute.size, attribute.data); - if (options.quantizationBits && options.quantizationBits[attribute.dracoAttribute]) { - encoder.SetAttributeQuantization(encoderModule[attribute.dracoAttribute], options.quantizationBits[attribute.dracoAttribute]); + attributeIDs[attribute.kind] = meshBuilder.AddFloatAttribute(mesh, encoderModule[attribute.dracoName], verticesCount, attribute.size, attribute.data); + if (options.quantizationBits && options.quantizationBits[attribute.dracoName]) { + encoder.SetAttributeQuantization(encoderModule[attribute.dracoName], options.quantizationBits[attribute.dracoName]); } } diff --git a/packages/dev/core/src/Meshes/Compression/dracoEncoder.ts b/packages/dev/core/src/Meshes/Compression/dracoEncoder.ts index a6d891e01b4..4b2de765907 100644 --- a/packages/dev/core/src/Meshes/Compression/dracoEncoder.ts +++ b/packages/dev/core/src/Meshes/Compression/dracoEncoder.ts @@ -73,7 +73,7 @@ function PrepareAttributesForDraco(input: Mesh | Geometry, excludedAttributes?: if (!(data instanceof Float32Array)) { data = Float32Array.from(data!); } - attributes.push({ attribute: kind, dracoAttribute: GetDracoAttributeName(kind), size: input.getVertexBuffer(kind)!.getSize(), data: data }); + attributes.push({ kind: kind, dracoName: GetDracoAttributeName(kind), size: input.getVertexBuffer(kind)!.getSize(), data: data }); } return attributes; diff --git a/packages/dev/core/src/Meshes/Compression/dracoEncoder.types.ts b/packages/dev/core/src/Meshes/Compression/dracoEncoder.types.ts index 31ccc2f5d8a..9100827efe0 100644 --- a/packages/dev/core/src/Meshes/Compression/dracoEncoder.types.ts +++ b/packages/dev/core/src/Meshes/Compression/dracoEncoder.types.ts @@ -44,11 +44,11 @@ export interface IDracoAttributeData { /** * The kind of the attribute. */ - attribute: string; + kind: string; /** - * The Draco kind to use for the attribute. + * The Draco name for the kind of the attribute. */ - dracoAttribute: DracoAttributeName; + dracoName: DracoAttributeName; /** * The size of the attribute. */ diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index a12e0b80095..87f68757f01 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -109,7 +109,7 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { accessor.count ) as Float32Array; // Because data is a TypedArray, GetFloatData will return a Float32Array - attributes.push({ attribute: name, dracoAttribute: getDracoAttributeName(name), size: GetAccessorElementCount(accessor.type), data: floatData }); + attributes.push({ kind: name, dracoName: getDracoAttributeName(name), size: GetAccessorElementCount(accessor.type), data: floatData }); primitiveBufferViews.push(bufferView); primitiveAccessors.push(accessor); From cc88fa2c82f24b102bdfefe7a72612668c17aa3c Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:39:41 -0500 Subject: [PATCH 19/32] Remove unused short and byte handling --- .../2.0/Extensions/EXT_mesh_gpu_instancing.ts | 62 +++---------------- 1 file changed, 7 insertions(+), 55 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts index b0ad384fe9c..99a9f57bf22 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts @@ -112,28 +112,16 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { // do we need to write TRANSLATION ? if (hasAnyInstanceWorldTranslation) { - extension.attributes["TRANSLATION"] = this._buildAccessor( - translationBuffer, - AccessorType.VEC3, - babylonNode.thinInstanceCount, - bufferManager, - AccessorComponentType.FLOAT - ); + extension.attributes["TRANSLATION"] = this._buildAccessor(translationBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, bufferManager); } // do we need to write ROTATION ? if (hasAnyInstanceWorldRotation) { - const componentType = AccessorComponentType.FLOAT; // we decided to stay on FLOAT for now see https://github.com/BabylonJS/Babylon.js/pull/12495 - extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, bufferManager, componentType); + // we decided to stay on FLOAT for now see https://github.com/BabylonJS/Babylon.js/pull/12495 + extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, bufferManager); } // do we need to write SCALE ? if (hasAnyInstanceWorldScale) { - extension.attributes["SCALE"] = this._buildAccessor( - scaleBuffer, - AccessorType.VEC3, - babylonNode.thinInstanceCount, - bufferManager, - AccessorComponentType.FLOAT - ); + extension.attributes["SCALE"] = this._buildAccessor(scaleBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, bufferManager); } /* eslint-enable @typescript-eslint/naming-convention*/ @@ -145,48 +133,12 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { }); } - private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, bufferManager: BufferManager, componentType: AccessorComponentType): number { - // write the buffer - let data: Float32Array | Int8Array | Int16Array; - switch (componentType) { - case AccessorComponentType.FLOAT: { - data = new Float32Array(buffer.length); - for (let i = 0; i != buffer.length; i++) { - data[i] = buffer[i]; - } - break; - } - case AccessorComponentType.BYTE: { - data = new Int8Array(buffer.length); - for (let i = 0; i != buffer.length; i++) { - data[i] = buffer[i] * 127; - } - break; - } - case AccessorComponentType.SHORT: { - data = new Int16Array(buffer.length); - for (let i = 0; i != buffer.length; i++) { - data[i] = buffer[i] * 32767; - } - break; - } - default: { - throw new Error(`Unsupported componentType ${componentType}`); - } - } + private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, bufferManager: BufferManager): number { // build the buffer view - const bv = bufferManager.createBufferView(data); + const bv = bufferManager.createBufferView(buffer); // finally build the accessor - const accessor = bufferManager.createAccessor( - bv, - type, - componentType, - count, - undefined, - undefined, - componentType == AccessorComponentType.BYTE || componentType == AccessorComponentType.SHORT - ); + const accessor = bufferManager.createAccessor(bv, type, AccessorComponentType.FLOAT, count); this._exporter._accessors.push(accessor); return this._exporter._accessors.length - 1; } From dcb9ce89d14fd9a01418176f6f485194df3845f1 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:00:04 -0500 Subject: [PATCH 20/32] Use .set for IBM --- packages/dev/serializers/src/glTF/2.0/glTFExporter.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 9a0539e60de..c96b978392d 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -791,9 +791,7 @@ export class GLTFExporter { const byteLength = inverseBindMatrices.length * 64; // Always a 4 x 4 matrix of 32 bit float const inverseBindMatricesData = new Float32Array(byteLength / 4); inverseBindMatrices.forEach((mat: Matrix, index: number) => { - mat.m.forEach((cell: number, cellIndex: number) => { - inverseBindMatricesData[index * 16 + cellIndex] = cell; - }); + inverseBindMatricesData.set(mat.m, index * 16); }); // Create buffer view and accessor const bufferView = this._bufferManager.createBufferView(inverseBindMatricesData); From a04f0c22630de908b4ade9db8eb593a84b347ba4 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:21:14 -0500 Subject: [PATCH 21/32] Rename to meshCompressionMethod to prepare for meshopt --- .../Extensions/KHR_draco_mesh_compression.ts | 2 +- .../serializers/src/glTF/2.0/glTFExporter.ts | 2 +- .../src/glTF/2.0/glTFSerializer.ts | 10 +++++++--- .../glTFSerializerKhrDracoMeshCompression.png | Bin 29545 -> 65913 bytes .../tests/test/visualization/config.json | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index 87f68757f01..14a90378ef0 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -57,7 +57,7 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { /** @internal */ constructor(exporter: GLTFExporter) { - this.enabled = exporter.options.useDracoCompression && DracoEncoder.DefaultAvailable; + this.enabled = exporter.options.meshCompressionMethod === "Draco" && DracoEncoder.DefaultAvailable; } /** @internal */ diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index c96b978392d..fa8d7b995df 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -410,7 +410,7 @@ export class GLTFExporter { exportUnusedUVs: false, removeNoopRootNodes: true, includeCoordinateSystemConversionNodes: false, - useDracoCompression: false, + meshCompressionMethod: "None", ...options, }; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts b/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts index de6bd3f0a91..775b71e4677 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts @@ -4,6 +4,11 @@ import type { Animation } from "core/Animations/animation"; import type { GLTFData } from "./glTFData"; import { GLTFExporter } from "./glTFExporter"; +/** + * Mesh compression methods. + */ +export type MeshCompressionMethod = "None" | "Draco"; + /** * Holds a collection of exporter options and parameters */ @@ -56,10 +61,9 @@ export interface IExportOptions { includeCoordinateSystemConversionNodes?: boolean; /** - * Indicates if geometries should be compressed with Draco. Defaults to false. - * NOTE: This may take a while if exporting many meshes. + * Indicates what compression method to apply to mesh data. */ - useDracoCompression?: boolean; + meshCompressionMethod?: MeshCompressionMethod; } /** diff --git a/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKhrDracoMeshCompression.png b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKhrDracoMeshCompression.png index 18fab33c6fe8cbcba48cdaab4299ce816b0decd4..e32ebaf716dd80f1733bc5d0209ca71097f19cb2 100644 GIT binary patch literal 65913 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 literal 29545 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 diff --git a/packages/tools/tests/test/visualization/config.json b/packages/tools/tests/test/visualization/config.json index e199f63be7d..2a7251e249b 100644 --- a/packages/tools/tests/test/visualization/config.json +++ b/packages/tools/tests/test/visualization/config.json @@ -941,7 +941,7 @@ }, { "title": "GLTF Serializer KHR draco mesh compression", - "playgroundId": "#F8BF8N", + "playgroundId": "#F8BF8N#3", "referenceImage": "glTFSerializerKhrDracoMeshCompression.png" }, { From 272641557f4bb1d4adeef86630053fe61d6846e1 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:29:23 -0500 Subject: [PATCH 22/32] Rename to getPropertiesWithBufferView --- .../Extensions/KHR_draco_mesh_compression.ts | 2 +- .../serializers/src/glTF/2.0/bufferManager.ts | 8 ++++---- .../dev/serializers/src/glTF/2.0/dataWriter.ts | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index 14a90378ef0..e13da3bbe84 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -163,7 +163,7 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { // Cull obsolete bufferViews that were replaced with Draco data this._bufferViewsUsed.forEach((bufferView) => { - const references = bufferManager.getProperties(bufferView); + const references = bufferManager.getPropertiesWithBufferView(bufferView); const onlyUsedByEncodedPrimitives = references.every((object) => { return this._accessorsUsed.has(object as IAccessor); // has() can handle any object, but TS doesn't know that }); diff --git a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts index 00451dfca20..f907703f4e2 100644 --- a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts +++ b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts @@ -54,7 +54,7 @@ export class BufferManager { bufferViews.push(bufferView); const bufferViewIndex = bufferViews.length - 1; - const properties = this.getProperties(bufferView); + const properties = this.getPropertiesWithBufferView(bufferView); for (const object of properties) { object.bufferView = bufferViewIndex; } @@ -127,7 +127,7 @@ export class BufferManager { */ public setBufferView(object: IPropertyWithBufferView, bufferView: IBufferView) { this._verifyBufferView(bufferView); - const properties = this.getProperties(bufferView); + const properties = this.getPropertiesWithBufferView(bufferView); properties.push(object); } @@ -136,7 +136,7 @@ export class BufferManager { * @param bufferView the bufferView to remove */ public removeBufferView(bufferView: IBufferView): void { - const properties = this.getProperties(bufferView); + const properties = this.getPropertiesWithBufferView(bufferView); for (const object of properties) { if (object.bufferView !== undefined) { delete object.bufferView; @@ -162,7 +162,7 @@ export class BufferManager { return bufferView!; } - public getProperties(bufferView: IBufferView): IPropertyWithBufferView[] { + public getPropertiesWithBufferView(bufferView: IBufferView): IPropertyWithBufferView[] { this._verifyBufferView(bufferView); this._bufferViewToProperties.set(bufferView, this._bufferViewToProperties.get(bufferView) ?? []); return this._bufferViewToProperties.get(bufferView)!; diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts index 191f40c534f..f9d4ef1e025 100644 --- a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -16,9 +16,17 @@ export class DataWriter { Uint16Array: this.writeUInt16.bind(this), Int32Array: this.writeInt32.bind(this), Uint32Array: this.writeUInt32.bind(this), - Float32Array: this.writeFloat32.bind(this), + Float32Array: this.writeFloat32.bind(this), // update to dataview api (dataview, value) }; + public writeTypedArray(value: TypedArray): void { + this._checkGrowBuffer(value.byteLength); + const setMethod = this._typedArrayToWriteMethod[value.constructor.name]; // use just constructor + for (let i = 0; i < value.length; i++) { + setMethod(value[i]); + } + } + public constructor(byteLength: number) { this._data = new Uint8Array(byteLength); this._dataView = new DataView(this._data.buffer); @@ -33,14 +41,6 @@ export class DataWriter { return new Uint8Array(this._data.buffer, 0, this._byteOffset); } - public writeTypedArray(value: TypedArray): void { - this._checkGrowBuffer(value.byteLength); - const setMethod = this._typedArrayToWriteMethod[value.constructor.name]; - for (let i = 0; i < value.length; i++) { - setMethod(value[i]); - } - } - public writeUInt8(value: number): void { this._checkGrowBuffer(1); this._dataView.setUint8(this._byteOffset, value); From 03a3e93efbb57d442accab97ed0b10337637ba3b Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:32:43 -0500 Subject: [PATCH 23/32] Update comment --- .../src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts index e13da3bbe84..247567a513a 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_draco_mesh_compression.ts @@ -98,7 +98,8 @@ export class KHR_draco_mesh_compression implements IGLTFExporterExtensionV2 { const data = bufferManager.getData(bufferView); const size = GetAccessorElementCount(accessor.type); - // TODO: In future, find a way to preserve original data type to curb unnecessary copies + // TODO: Implement a way to preserve original data type, as Draco can handle more than just floats + // TODO: Add flag in DracoEncoder API to prevent copying data (a second time) to transferable buffer const floatData = GetFloatData( data, size, From 6953490cdaa479a11da2fc48e7b47933d5aa2f8a Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:41:17 -0500 Subject: [PATCH 24/32] Use Float32Array for animation output writing --- .../serializers/src/glTF/2.0/glTFAnimation.ts | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index 5f0b62db059..996b4d58207 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -592,7 +592,7 @@ export class _GLTFAnimation { const nodeIndex = nodeMap.get(babylonTransformNode); // Create buffer view and accessor for key frames. - const data = new Float32Array(animationData.inputs); + let data = new Float32Array(animationData.inputs); bufferView = bufferManager.createBufferView(data); accessor = bufferManager.createAccessor(bufferView, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, undefined, { min: [animationData.inputsMin], @@ -607,17 +607,17 @@ export class _GLTFAnimation { const position = new Vector3(); const isCamera = babylonTransformNode instanceof Camera; - const dataWriter = new DataWriter(animationData.outputs.length * GetAccessorElementCount(dataAccessorType)); - animationData.outputs.forEach(function (output: number[]) { + data = new Float32Array(animationData.outputs.length * GetAccessorElementCount(dataAccessorType)); + animationData.outputs.forEach(function (output: number[], index: number) { if (convertToRightHanded) { switch (animationChannelTargetPath) { case AnimationChannelTargetPath.TRANSLATION: Vector3.FromArrayToRef(output, 0, position); ConvertToRightHandedPosition(position); - dataWriter.writeFloat32(position.x); - dataWriter.writeFloat32(position.y); - dataWriter.writeFloat32(position.z); + data[index] = position.x; + data[index + 1] = position.y; + data[index + 2] = position.z; break; case AnimationChannelTargetPath.ROTATION: @@ -636,15 +636,13 @@ export class _GLTFAnimation { } } - dataWriter.writeFloat32(rotationQuaternion.x); - dataWriter.writeFloat32(rotationQuaternion.y); - dataWriter.writeFloat32(rotationQuaternion.z); - dataWriter.writeFloat32(rotationQuaternion.w); + data[index] = rotationQuaternion.x; + data[index + 1] = rotationQuaternion.y; + data[index + 2] = rotationQuaternion.z; + data[index + 3] = rotationQuaternion.w; break; default: - output.forEach(function (entry) { - dataWriter.writeFloat32(entry); - }); + data.set(output, index); break; } } else { @@ -660,22 +658,20 @@ export class _GLTFAnimation { ConvertCameraRotationToGLTF(rotationQuaternion); } - dataWriter.writeFloat32(rotationQuaternion.x); - dataWriter.writeFloat32(rotationQuaternion.y); - dataWriter.writeFloat32(rotationQuaternion.z); - dataWriter.writeFloat32(rotationQuaternion.w); + data[index] = rotationQuaternion.x; + data[index + 1] = rotationQuaternion.y; + data[index + 2] = rotationQuaternion.z; + data[index + 3] = rotationQuaternion.w; break; default: - output.forEach(function (entry) { - dataWriter.writeFloat32(entry); - }); + data.set(output, index); break; } } }); // Create buffer view and accessor for keyed values. - bufferView = bufferManager.createBufferView(dataWriter.getOutputData()); + bufferView = bufferManager.createBufferView(data); accessor = bufferManager.createAccessor(bufferView, dataAccessorType, AccessorComponentType.FLOAT, animationData.outputs.length); accessors.push(accessor); dataAccessorIndex = accessors.length - 1; From 70d1870ae37d5d4118526fd2e3e649dcc8b3bd37 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:42:13 -0500 Subject: [PATCH 25/32] Remove unused DataWriter import --- packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index 996b4d58207..42b5266ffea 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -17,7 +17,6 @@ import { Camera } from "core/Cameras/camera"; import { Light } from "core/Lights/light"; import type { BufferManager } from "./bufferManager"; import { GetAccessorElementCount, ConvertToRightHandedPosition, ConvertCameraRotationToGLTF, ConvertToRightHandedRotation } from "./glTFUtilities"; -import { DataWriter } from "./dataWriter"; /** * @internal From 6a9db8980a9e02175285eca611c1a7ec85c0a369 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:09:31 -0500 Subject: [PATCH 26/32] Update typedArrayToWriteMethod --- .../serializers/src/glTF/2.0/dataWriter.ts | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts index f9d4ef1e025..7c753a9ae33 100644 --- a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -2,28 +2,31 @@ /* eslint-disable babylonjs/available */ import type { TypedArray } from "core/types"; +const TypedArrayToWriteMethod = new Map void>([ + [Int8Array, (d, b, v) => d.setInt8(b, v)], + [Uint8Array, (dv, bo, v) => dv.setUint8(bo, v)], + [Uint8ClampedArray, (dv, bo, v) => dv.setUint8(bo, v)], + [Int16Array, (dv, bo, v) => dv.setInt16(bo, v, true)], + [Uint16Array, (dv, bo, v) => dv.setUint16(bo, v, true)], + [Int32Array, (dv, bo, v) => dv.setInt32(bo, v, true)], + [Uint32Array, (dv, bo, v) => dv.setUint32(bo, v, true)], + [Float32Array, (dv, bo, v) => dv.setFloat32(bo, v, true)], +]); + /** @internal */ export class DataWriter { private _data: Uint8Array; private _dataView: DataView; private _byteOffset: number; - private _typedArrayToWriteMethod: Record = { - Int8Array: this.writeInt8.bind(this), - Uint8Array: this.writeUInt8.bind(this), - Uint8ClampedArray: this.writeUInt8.bind(this), - Int16Array: this.writeInt16.bind(this), - Uint16Array: this.writeUInt16.bind(this), - Int32Array: this.writeInt32.bind(this), - Uint32Array: this.writeUInt32.bind(this), - Float32Array: this.writeFloat32.bind(this), // update to dataview api (dataview, value) - }; - public writeTypedArray(value: TypedArray): void { this._checkGrowBuffer(value.byteLength); - const setMethod = this._typedArrayToWriteMethod[value.constructor.name]; // use just constructor + const setMethod = TypedArrayToWriteMethod.get(value.constructor); + if (!setMethod) { + throw new Error("writeTypedArray: Unsupported type: " + value.constructor.name); + } for (let i = 0; i < value.length; i++) { - setMethod(value[i]); + setMethod(this._dataView, this._byteOffset, value[i] as number); } } From c4dc52a15ffb465827620930835eda898a1bc23a Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:45:12 -0500 Subject: [PATCH 27/32] Missing byteOffset update! --- packages/dev/serializers/src/glTF/2.0/dataWriter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts index 7c753a9ae33..be63cd21370 100644 --- a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -27,6 +27,7 @@ export class DataWriter { } for (let i = 0; i < value.length; i++) { setMethod(this._dataView, this._byteOffset, value[i] as number); + this._byteOffset += value.BYTES_PER_ELEMENT; } } From a40d768fac629b175ec018619ffb947336c5d088 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:03:29 -0500 Subject: [PATCH 28/32] Fix update to animation output writing --- .../serializers/src/glTF/2.0/glTFAnimation.ts | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index 42b5266ffea..76807e9db9e 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -606,23 +606,22 @@ export class _GLTFAnimation { const position = new Vector3(); const isCamera = babylonTransformNode instanceof Camera; - data = new Float32Array(animationData.outputs.length * GetAccessorElementCount(dataAccessorType)); + const elementCount = GetAccessorElementCount(dataAccessorType); + data = new Float32Array(animationData.outputs.length * elementCount); animationData.outputs.forEach(function (output: number[], index: number) { + let outputToWrite: number[] = output; if (convertToRightHanded) { switch (animationChannelTargetPath) { case AnimationChannelTargetPath.TRANSLATION: Vector3.FromArrayToRef(output, 0, position); ConvertToRightHandedPosition(position); - - data[index] = position.x; - data[index + 1] = position.y; - data[index + 2] = position.z; + position.toArray(outputToWrite); break; - case AnimationChannelTargetPath.ROTATION: if (output.length === 4) { Quaternion.FromArrayToRef(output, 0, rotationQuaternion); } else { + outputToWrite = new Array(4); // Will need 4, not 3, for a quaternion Vector3.FromArrayToRef(output, 0, eulerVec3); Quaternion.FromEulerVectorToRef(eulerVec3, rotationQuaternion); } @@ -635,13 +634,7 @@ export class _GLTFAnimation { } } - data[index] = rotationQuaternion.x; - data[index + 1] = rotationQuaternion.y; - data[index + 2] = rotationQuaternion.z; - data[index + 3] = rotationQuaternion.w; - break; - default: - data.set(output, index); + rotationQuaternion.toArray(outputToWrite); break; } } else { @@ -650,23 +643,20 @@ export class _GLTFAnimation { if (output.length === 4) { Quaternion.FromArrayToRef(output, 0, rotationQuaternion); } else { + outputToWrite = new Array(4); // Will need 4, not 3, for a quaternion Vector3.FromArrayToRef(output, 0, eulerVec3); Quaternion.FromEulerVectorToRef(eulerVec3, rotationQuaternion); } + if (isCamera) { ConvertCameraRotationToGLTF(rotationQuaternion); } - data[index] = rotationQuaternion.x; - data[index + 1] = rotationQuaternion.y; - data[index + 2] = rotationQuaternion.z; - data[index + 3] = rotationQuaternion.w; - break; - default: - data.set(output, index); + rotationQuaternion.toArray(outputToWrite); break; } } + data.set(outputToWrite, index * elementCount); }); // Create buffer view and accessor for keyed values. From 5f63f10251aa3f59b6d633a1514aac9291e3771f Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:48:31 -0500 Subject: [PATCH 29/32] Add float64 support for consistency --- packages/dev/serializers/src/glTF/2.0/dataWriter.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts index be63cd21370..60b90fc2d53 100644 --- a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -11,6 +11,7 @@ const TypedArrayToWriteMethod = new Map dv.setInt32(bo, v, true)], [Uint32Array, (dv, bo, v) => dv.setUint32(bo, v, true)], [Float32Array, (dv, bo, v) => dv.setFloat32(bo, v, true)], + [Float64Array, (dv, bo, v) => dv.setFloat64(bo, v, true)], ]); /** @internal */ @@ -87,6 +88,12 @@ export class DataWriter { this._byteOffset += 4; } + public writeFloat64(value: number): void { + this._checkGrowBuffer(8); + this._dataView.setFloat64(this._byteOffset, value, true); + this._byteOffset += 8; + } + private _checkGrowBuffer(byteLength: number): void { const newByteLength = this.byteOffset + byteLength; if (newByteLength > this._data.byteLength) { From 1a914697e5894627006a5ddcd51d12c0cb3eb87a Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:49:58 -0500 Subject: [PATCH 30/32] Exclude BigInt TypedArrays in param type --- packages/dev/serializers/src/glTF/2.0/dataWriter.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts index 60b90fc2d53..153afd40b81 100644 --- a/packages/dev/serializers/src/glTF/2.0/dataWriter.ts +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -20,12 +20,9 @@ export class DataWriter { private _dataView: DataView; private _byteOffset: number; - public writeTypedArray(value: TypedArray): void { + public writeTypedArray(value: Exclude): void { this._checkGrowBuffer(value.byteLength); - const setMethod = TypedArrayToWriteMethod.get(value.constructor); - if (!setMethod) { - throw new Error("writeTypedArray: Unsupported type: " + value.constructor.name); - } + const setMethod = TypedArrayToWriteMethod.get(value.constructor)!; for (let i = 0; i < value.length; i++) { setMethod(this._dataView, this._byteOffset, value[i] as number); this._byteOffset += value.BYTES_PER_ELEMENT; From 428c47b8d5d827d4c3e0ed72b135d94f990159a9 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:51:51 -0500 Subject: [PATCH 31/32] Rename vars --- packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index 76807e9db9e..254684604b9 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -591,8 +591,8 @@ export class _GLTFAnimation { const nodeIndex = nodeMap.get(babylonTransformNode); // Create buffer view and accessor for key frames. - let data = new Float32Array(animationData.inputs); - bufferView = bufferManager.createBufferView(data); + const inputData = new Float32Array(animationData.inputs); + bufferView = bufferManager.createBufferView(inputData); accessor = bufferManager.createAccessor(bufferView, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, undefined, { min: [animationData.inputsMin], max: [animationData.inputsMax], @@ -607,7 +607,7 @@ export class _GLTFAnimation { const isCamera = babylonTransformNode instanceof Camera; const elementCount = GetAccessorElementCount(dataAccessorType); - data = new Float32Array(animationData.outputs.length * elementCount); + const outputData = new Float32Array(animationData.outputs.length * elementCount); animationData.outputs.forEach(function (output: number[], index: number) { let outputToWrite: number[] = output; if (convertToRightHanded) { @@ -656,11 +656,11 @@ export class _GLTFAnimation { break; } } - data.set(outputToWrite, index * elementCount); + outputData.set(outputToWrite, index * elementCount); }); // Create buffer view and accessor for keyed values. - bufferView = bufferManager.createBufferView(data); + bufferView = bufferManager.createBufferView(outputData); accessor = bufferManager.createAccessor(bufferView, dataAccessorType, AccessorComponentType.FLOAT, animationData.outputs.length); accessors.push(accessor); dataAccessorIndex = accessors.length - 1; From ab91e05d7b28e8245c7837c024b053ddaee1c34a Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:21:46 -0500 Subject: [PATCH 32/32] Update bufferManager data type --- packages/dev/serializers/src/glTF/2.0/bufferManager.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts index f907703f4e2..438bd78c7c9 100644 --- a/packages/dev/serializers/src/glTF/2.0/bufferManager.ts +++ b/packages/dev/serializers/src/glTF/2.0/bufferManager.ts @@ -2,6 +2,8 @@ import type { TypedArray } from "core/types"; import type { AccessorComponentType, AccessorType, IAccessor, IBufferView } from "babylonjs-gltf2interface"; import { DataWriter } from "./dataWriter"; +type TypedArrayForglTF = Exclude; + interface IPropertyWithBufferView { bufferView?: number; } @@ -20,7 +22,7 @@ export class BufferManager { /** * Maps a bufferView to its data */ - private _bufferViewToData: Map = new Map(); + private _bufferViewToData: Map = new Map(); /** * Maps a bufferView to glTF objects that reference it via a "bufferView" property (e.g. accessors, images) @@ -73,7 +75,7 @@ export class BufferManager { * @param byteStride byte distance between consecutive elements * @returns bufferView for glTF */ - public createBufferView(data: TypedArray, byteStride?: number): IBufferView { + public createBufferView(data: TypedArrayForglTF, byteStride?: number): IBufferView { const bufferView: IBufferView = { buffer: 0, byteOffset: undefined, // byteOffset will be set later, when we write the binary and decide bufferView ordering @@ -168,7 +170,7 @@ export class BufferManager { return this._bufferViewToProperties.get(bufferView)!; } - public getData(bufferView: IBufferView): TypedArray { + public getData(bufferView: IBufferView): TypedArrayForglTF { this._verifyBufferView(bufferView); return this._bufferViewToData.get(bufferView)!; }