From 5e81d771b47d1d66080767c4bf7203ac0e8361e5 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:12:13 -0400 Subject: [PATCH 1/4] Refactor exportIndices --- .../serializers/src/glTF/2.0/glTFExporter.ts | 120 ++++++++---------- .../serializers/src/glTF/2.0/glTFUtilities.ts | 23 ++-- 2 files changed, 65 insertions(+), 78 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 95ec9ac21d6..276f5e1b97f 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -83,8 +83,8 @@ import { DataWriter } from "./dataWriter"; import { OpenPBRMaterial } from "core/Materials/PBR/openPbrMaterial"; class ExporterState { - // Babylon indices array, start, count, offset, flip -> glTF accessor index - private _indicesAccessorMap = new Map, Map>>>>(); + // Babylon indices array, start, count, flip -> glTF accessor index + private _indicesAccessorMap = new Map, Map>>>(); // Babylon buffer -> glTF buffer view private _vertexBufferViewMap = new Map(); @@ -115,36 +115,30 @@ class ExporterState { // Only used when convertToRightHanded is true. public readonly convertedToRightHandedBuffers = new Map(); - public getIndicesAccessor(indices: Nullable, start: number, count: number, offset: number, flip: boolean): number | undefined { - return this._indicesAccessorMap.get(indices)?.get(start)?.get(count)?.get(offset)?.get(flip); + public getIndicesAccessor(indices: Nullable, start: number, count: number, flip: boolean): number | undefined { + return this._indicesAccessorMap.get(indices)?.get(start)?.get(count)?.get(flip); } - public setIndicesAccessor(indices: Nullable, start: number, count: number, offset: number, flip: boolean, accessorIndex: number): void { + public setIndicesAccessor(indices: Nullable, start: number, count: number, flip: boolean, accessorIndex: number): void { let map1 = this._indicesAccessorMap.get(indices); if (!map1) { - map1 = new Map>>>(); + map1 = new Map>>(); this._indicesAccessorMap.set(indices, map1); } let map2 = map1.get(start); if (!map2) { - map2 = new Map>>(); + map2 = new Map>(); map1.set(start, map2); } let map3 = map2.get(count); if (!map3) { - map3 = new Map>(); + map3 = new Map(); map2.set(count, map3); } - let map4 = map3.get(offset); - if (!map4) { - map4 = new Map(); - map3.set(offset, map4); - } - - map4.set(flip, accessorIndex); + map3.set(flip, accessorIndex); } public pushExportedNode(node: Node) { @@ -1305,65 +1299,56 @@ export class GLTFExporter { is32Bits: boolean, start: number, count: number, - offset: number, fillMode: number, sideOrientation: number, state: ExporterState, primitive: IMeshPrimitive ): void { - let indicesToExport = indices; - - primitive.mode = GetPrimitiveMode(fillMode); - // Flip indices if triangle winding order is not CCW, as glTF is always CCW. - const flip = sideOrientation !== Material.CounterClockWiseSideOrientation && IsTriangleFillMode(fillMode); - if (flip) { - if (fillMode === Material.TriangleStripDrawMode || fillMode === Material.TriangleFanDrawMode) { - throw new Error("Triangle strip/fan fill mode is not implemented"); - } - - primitive.mode = GetPrimitiveMode(fillMode); + const needsFlip = sideOrientation !== Material.CounterClockWiseSideOrientation && IsTriangleFillMode(fillMode); - const newIndices = is32Bits ? new Uint32Array(count) : new Uint16Array(count); + if (needsFlip && (fillMode === Material.TriangleStripDrawMode || fillMode === Material.TriangleFanDrawMode)) { + throw new Error("Converting sideOrientation of triangle strip/fan fill modes is not implemented"); + } - if (indices) { - for (let i = 0; i + 2 < count; i += 3) { - newIndices[i] = indices[start + i] + offset; - newIndices[i + 1] = indices[start + i + 2] + offset; - newIndices[i + 2] = indices[start + i + 1] + offset; - } - } else { - for (let i = 0; i + 2 < count; i += 3) { - newIndices[i] = i; - newIndices[i + 1] = i + 2; - newIndices[i + 2] = i + 1; + let accessorIndex = state.getIndicesAccessor(indices, start, count, needsFlip); + if (accessorIndex === undefined) { + // Normalize and subset indices + let processedIndices = IndicesArrayToTypedArray(indices, start, count, is32Bits); + + // Flip indices, if needed + if (needsFlip) { + const newIndices = is32Bits ? new Uint32Array(count) : new Uint16Array(count); + + if (processedIndices) { + for (let i = 0; i + 2 < count; i += 3) { + newIndices[i] = processedIndices[i]; + newIndices[i + 1] = processedIndices[i + 2]; + newIndices[i + 2] = processedIndices[i + 1]; + } + } else { + for (let i = 0; i + 2 < count; i += 3) { + newIndices[i] = i; + newIndices[i + 1] = i + 2; + newIndices[i + 2] = i + 1; + } } - } - indicesToExport = newIndices; - } else if (indices && offset !== 0) { - const newIndices = is32Bits ? new Uint32Array(count) : new Uint16Array(count); - for (let i = 0; i < count; i++) { - newIndices[i] = indices[start + i] + offset; + processedIndices = newIndices; } - indicesToExport = newIndices; - } - - if (indicesToExport) { - let accessorIndex = state.getIndicesAccessor(indices, start, count, offset, flip); - if (accessorIndex === undefined) { - const bytes = IndicesArrayToTypedArray(indicesToExport, 0, count, is32Bits); - const bufferView = this._bufferManager.createBufferView(bytes); - + // Create accessor and buffer view + if (processedIndices) { + const bufferView = this._bufferManager.createBufferView(processedIndices); const componentType = is32Bits ? AccessorComponentType.UNSIGNED_INT : AccessorComponentType.UNSIGNED_SHORT; 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); + state.setIndicesAccessor(indices, start, count, needsFlip, accessorIndex); } - - primitive.indices = accessorIndex; } + + primitive.mode = GetPrimitiveMode(fillMode); + primitive.indices = accessorIndex; } private _exportVertexBuffer(vertexBuffer: VertexBuffer, babylonMaterial: Material, start: number, count: number, state: ExporterState, primitive: IMeshPrimitive): void { @@ -1417,7 +1402,11 @@ export class GLTFExporter { let materialIndex = this._materialMap.get(babylonMaterial); if (materialIndex === undefined) { const hasUVs = vertexBuffers && Object.keys(vertexBuffers).some((kind) => kind.startsWith("uv")); - babylonMaterial = babylonMaterial instanceof MultiMaterial ? babylonMaterial.subMaterials[subMesh.materialIndex]! : babylonMaterial; + + if (babylonMaterial instanceof MultiMaterial) { + babylonMaterial = babylonMaterial.subMaterials[subMesh.materialIndex]!; + } + if (babylonMaterial instanceof PBRBaseMaterial) { materialIndex = await this._materialExporter.exportPBRMaterialAsync(babylonMaterial, hasUVs); } else if (babylonMaterial instanceof StandardMaterial) { @@ -1458,18 +1447,16 @@ export class GLTFExporter { for (const subMesh of subMeshes) { const primitive: IMeshPrimitive = { attributes: {} }; + // Material const babylonMaterial = subMesh.getMaterial() || this._babylonScene.defaultMaterial; - if (isGreasedLineMesh) { const material: IMaterial = { name: babylonMaterial.name, }; - const babylonLinesMesh = babylonMesh; - const colorWhite = Color3.White(); - const alpha = babylonLinesMesh.material?.alpha ?? 1; - const color = babylonLinesMesh.greasedLineMaterial?.color ?? colorWhite; + const alpha = babylonMesh.material?.alpha ?? 1; + const color = babylonMesh.greasedLineMaterial?.color ?? colorWhite; if (!color.equalsWithEpsilon(colorWhite, Epsilon) || alpha < 1) { material.pbrMetallicRoughness = { baseColorFactor: [...color.asArray(), alpha], @@ -1484,11 +1471,9 @@ export class GLTFExporter { name: babylonMaterial.name, }; - const babylonLinesMesh = babylonMesh; - - if (!babylonLinesMesh.color.equalsWithEpsilon(Color3.White(), Epsilon) || babylonLinesMesh.alpha < 1) { + if (!babylonMesh.color.equalsWithEpsilon(Color3.White(), Epsilon) || babylonMesh.alpha < 1) { material.pbrMetallicRoughness = { - baseColorFactor: [...babylonLinesMesh.color.asArray(), babylonLinesMesh.alpha], + baseColorFactor: [...babylonMesh.color.asArray(), babylonMesh.alpha], }; } @@ -1514,7 +1499,6 @@ export class GLTFExporter { indices ? AreIndices32Bits(indices, subMesh.indexCount, subMesh.indexStart, subMesh.verticesStart) : subMesh.verticesCount > 65535, indices ? subMesh.indexStart : subMesh.verticesStart, indices ? subMesh.indexCount : subMesh.verticesCount, - -subMesh.verticesStart, fillMode, sideOrientation, state, diff --git a/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts b/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts index c2e1bb16fec..cd9945adf75 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts @@ -1,7 +1,7 @@ /* eslint-disable jsdoc/require-jsdoc */ import type { INode } from "babylonjs-gltf2interface"; import { AccessorType, MeshPrimitiveMode } from "babylonjs-gltf2interface"; -import type { FloatArray, DataArray, IndicesArray } from "core/types"; +import type { FloatArray, DataArray, IndicesArray, Nullable } 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"; @@ -325,25 +325,28 @@ export function IsChildCollapsible(babylonNode: ShadowLight | TargetCamera, pare } /** - * Converts an IndicesArray into either Uint32Array or Uint16Array, only copying if the data is number[]. + * Normalizes an IndicesArray into either a Uint32Array or Uint16Array at the specified count and offset. * @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; +export function IndicesArrayToTypedArray(indices: Nullable, start: number, count: number, is32Bits: boolean): Nullable { + if (!indices) { + return null; } - // If Int32Array, cast the indices (which are all positive) to Uint32Array - if (indices instanceof Int32Array) { - return new Uint32Array(indices.buffer, indices.byteOffset, indices.length); + // Convert to appropriate typed array + let typedIndices: Uint32Array | Uint16Array; + if (indices instanceof Uint32Array || indices instanceof Uint16Array) { + typedIndices = indices; + } else { + typedIndices = (is32Bits ? Uint32Array : Uint16Array).from(indices); } - const subarray = indices.slice(start, start + count); - return is32Bits ? new Uint32Array(subarray) : new Uint16Array(subarray); + // Apply subsetting if needed + return start !== 0 || count !== typedIndices.length ? typedIndices.subarray(start, start + count) : typedIndices; } export function DataArrayToUint8Array(data: DataArray): Uint8Array { From bd52a097621df777e18af6fa5a94a5d7e7785608 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:28:43 -0400 Subject: [PATCH 2/4] Avoid copying twice if flipped --- .../serializers/src/glTF/2.0/glTFExporter.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 276f5e1b97f..0d262457f1d 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -1313,28 +1313,30 @@ export class GLTFExporter { let accessorIndex = state.getIndicesAccessor(indices, start, count, needsFlip); if (accessorIndex === undefined) { - // Normalize and subset indices - let processedIndices = IndicesArrayToTypedArray(indices, start, count, is32Bits); + let processedIndices: Nullable = null; - // Flip indices, if needed if (needsFlip) { - const newIndices = is32Bits ? new Uint32Array(count) : new Uint16Array(count); + // Create new array with swapped second and third vertices of each triangle + processedIndices = is32Bits ? new Uint32Array(count) : new Uint16Array(count); - if (processedIndices) { + if (indices) { + // Use original indices with offset for (let i = 0; i + 2 < count; i += 3) { - newIndices[i] = processedIndices[i]; - newIndices[i + 1] = processedIndices[i + 2]; - newIndices[i + 2] = processedIndices[i + 1]; + processedIndices[i] = indices[start + i]; + processedIndices[i + 1] = indices[start + i + 2]; + processedIndices[i + 2] = indices[start + i + 1]; } } else { + // Unindexed geometry - generate sequential indices for (let i = 0; i + 2 < count; i += 3) { - newIndices[i] = i; - newIndices[i + 1] = i + 2; - newIndices[i + 2] = i + 1; + processedIndices[i] = i; + processedIndices[i + 1] = i + 2; + processedIndices[i + 2] = i + 1; } } - - processedIndices = newIndices; + } else { + // No flipping needed - create a subset of the original indices to avoid exporting shared buffers multiple times + processedIndices = IndicesArrayToTypedArray(indices, start, count, is32Bits); } // Create accessor and buffer view From 84fee3a3967a1fb9550f288fbe616958e5a1ae13 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:35:32 -0400 Subject: [PATCH 3/4] Clean up --- .../serializers/src/glTF/2.0/glTFExporter.ts | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 0d262457f1d..52dc440a911 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -1313,35 +1313,35 @@ export class GLTFExporter { let accessorIndex = state.getIndicesAccessor(indices, start, count, needsFlip); if (accessorIndex === undefined) { - let processedIndices: Nullable = null; + let indicesToExport: Nullable = null; if (needsFlip) { // Create new array with swapped second and third vertices of each triangle - processedIndices = is32Bits ? new Uint32Array(count) : new Uint16Array(count); + indicesToExport = is32Bits ? new Uint32Array(count) : new Uint16Array(count); if (indices) { // Use original indices with offset for (let i = 0; i + 2 < count; i += 3) { - processedIndices[i] = indices[start + i]; - processedIndices[i + 1] = indices[start + i + 2]; - processedIndices[i + 2] = indices[start + i + 1]; + indicesToExport[i] = indices[start + i]; + indicesToExport[i + 1] = indices[start + i + 2]; + indicesToExport[i + 2] = indices[start + i + 1]; } } else { // Unindexed geometry - generate sequential indices for (let i = 0; i + 2 < count; i += 3) { - processedIndices[i] = i; - processedIndices[i + 1] = i + 2; - processedIndices[i + 2] = i + 1; + indicesToExport[i] = i; + indicesToExport[i + 1] = i + 2; + indicesToExport[i + 2] = i + 1; } } } else { - // No flipping needed - create a subset of the original indices to avoid exporting shared buffers multiple times - processedIndices = IndicesArrayToTypedArray(indices, start, count, is32Bits); + // No flipping needed - normalize & create a subset of the indices to avoid exporting shared buffers multiple times + indicesToExport = IndicesArrayToTypedArray(indices, start, count, is32Bits); } // Create accessor and buffer view - if (processedIndices) { - const bufferView = this._bufferManager.createBufferView(processedIndices); + if (indicesToExport) { + const bufferView = this._bufferManager.createBufferView(indicesToExport); const componentType = is32Bits ? AccessorComponentType.UNSIGNED_INT : AccessorComponentType.UNSIGNED_SHORT; this._accessors.push(this._bufferManager.createAccessor(bufferView, AccessorType.SCALAR, componentType, count, 0)); accessorIndex = this._accessors.length - 1; @@ -1404,11 +1404,7 @@ export class GLTFExporter { let materialIndex = this._materialMap.get(babylonMaterial); if (materialIndex === undefined) { const hasUVs = vertexBuffers && Object.keys(vertexBuffers).some((kind) => kind.startsWith("uv")); - - if (babylonMaterial instanceof MultiMaterial) { - babylonMaterial = babylonMaterial.subMaterials[subMesh.materialIndex]!; - } - + babylonMaterial = babylonMaterial instanceof MultiMaterial ? babylonMaterial.subMaterials[subMesh.materialIndex]! : babylonMaterial; if (babylonMaterial instanceof PBRBaseMaterial) { materialIndex = await this._materialExporter.exportPBRMaterialAsync(babylonMaterial, hasUVs); } else if (babylonMaterial instanceof StandardMaterial) { @@ -1452,6 +1448,7 @@ export class GLTFExporter { // Material const babylonMaterial = subMesh.getMaterial() || this._babylonScene.defaultMaterial; if (isGreasedLineMesh) { + // Special case for GreasedLineMesh const material: IMaterial = { name: babylonMaterial.name, }; @@ -1482,7 +1479,6 @@ export class GLTFExporter { this._materials.push(material); primitive.material = this._materials.length - 1; } else { - // Material // eslint-disable-next-line no-await-in-loop await this._exportMaterialAsync(babylonMaterial, vertexBuffers, subMesh, primitive); } From 71f53841662f4f12ee16c45440fead275f0f9265 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:21:23 -0400 Subject: [PATCH 4/4] Subset before creating view --- .../serializers/src/glTF/2.0/glTFExporter.ts | 4 +-- .../serializers/src/glTF/2.0/glTFUtilities.ts | 30 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index 52dc440a911..efae4f37816 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -52,7 +52,7 @@ import { IsChildCollapsible, FloatsNeed16BitInteger, IsStandardVertexAttribute, - IndicesArrayToTypedArray, + IndicesArrayToTypedSubarray, GetVertexBufferInfo, CollapseChildIntoParent, Rotate180Y, @@ -1336,7 +1336,7 @@ export class GLTFExporter { } } else { // No flipping needed - normalize & create a subset of the indices to avoid exporting shared buffers multiple times - indicesToExport = IndicesArrayToTypedArray(indices, start, count, is32Bits); + indicesToExport = IndicesArrayToTypedSubarray(indices, start, count, is32Bits); } // Create accessor and buffer view diff --git a/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts b/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts index cd9945adf75..0d37ad7bf0d 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts @@ -325,28 +325,38 @@ export function IsChildCollapsible(babylonNode: ShadowLight | TargetCamera, pare } /** - * Normalizes an IndicesArray into either a Uint32Array or Uint16Array at the specified count and offset. + * Normalizes an IndicesArray into either a Uint32Array or Uint16Array, only copying if the data is number[] + * Note that a copy will be made only if the data was 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 + * @returns a Uint32Array or Uint16Array view at the specified count and offset * @internal */ -export function IndicesArrayToTypedArray(indices: Nullable, start: number, count: number, is32Bits: boolean): Nullable { +export function IndicesArrayToTypedSubarray(indices: Nullable, start: number, count: number, is32Bits: boolean): Nullable { if (!indices) { return null; } - // Convert to appropriate typed array - let typedIndices: Uint32Array | Uint16Array; - if (indices instanceof Uint32Array || indices instanceof Uint16Array) { - typedIndices = indices; + // Subset from the full indices array if needed + let processedIndices = indices; + if (start !== 0 || count !== indices.length) { + processedIndices = Array.isArray(indices) ? indices.slice(start, start + count) : indices.subarray(start, start + count); } else { - typedIndices = (is32Bits ? Uint32Array : Uint16Array).from(indices); + processedIndices = indices; } - // Apply subsetting if needed - return start !== 0 || count !== typedIndices.length ? typedIndices.subarray(start, start + count) : typedIndices; + // Cast Int32Array (which should all be positive) to Uint32Array + if (processedIndices instanceof Int32Array) { + return new Uint32Array(processedIndices.buffer, processedIndices.byteOffset, processedIndices.length); + } + + // Convert number[] to typed array + if (Array.isArray(processedIndices)) { + return is32Bits ? new Uint32Array(processedIndices) : new Uint16Array(processedIndices); + } + + return processedIndices; } export function DataArrayToUint8Array(data: DataArray): Uint8Array {