diff --git a/packages/dev/core/src/Buffers/buffer.ts b/packages/dev/core/src/Buffers/buffer.ts index 0d3c2ae82c9..1824f997d0f 100644 --- a/packages/dev/core/src/Buffers/buffer.ts +++ b/packages/dev/core/src/Buffers/buffer.ts @@ -4,6 +4,7 @@ import { DataBuffer } from "./dataBuffer"; import type { Mesh } from "../Meshes/mesh"; import { Logger } from "../Misc/logger"; import { Constants } from "../Engines/constants"; +import { EnumerateFloatValues, GetFloatData, GetTypeByteLength } from "./bufferUtils"; /** * Class used to store data that will be store in GPU memory @@ -566,7 +567,7 @@ export class VertexBuffer { this.type = type; } - const typeByteLength = VertexBuffer.GetTypeByteLength(this.type); + const typeByteLength = GetTypeByteLength(this.type); if (useBytes) { this._size = size || (stride ? stride / typeByteLength : VertexBuffer.DeduceStride(kind)); @@ -641,7 +642,7 @@ export class VertexBuffer { return null; } - return VertexBuffer.GetFloatData(data, this._size, this.type, this.byteOffset, this.byteStride, this.normalized, totalVertices, forceCopy); + return GetFloatData(data, this._size, this.type, this.byteOffset, this.byteStride, this.normalized, totalVertices, forceCopy); } /** @@ -667,7 +668,7 @@ export class VertexBuffer { * @deprecated Please use byteStride instead. */ public getStrideSize(): number { - return this.byteStride / VertexBuffer.GetTypeByteLength(this.type); + return this.byteStride / GetTypeByteLength(this.type); } /** @@ -676,7 +677,7 @@ export class VertexBuffer { * @deprecated Please use byteOffset instead. */ public getOffset(): number { - return this.byteOffset / VertexBuffer.GetTypeByteLength(this.type); + return this.byteOffset / GetTypeByteLength(this.type); } /** @@ -685,7 +686,7 @@ export class VertexBuffer { * @returns the number of components */ public getSize(sizeInBytes = false): number { - return sizeInBytes ? this._size * VertexBuffer.GetTypeByteLength(this.type) : this._size; + return sizeInBytes ? this._size * GetTypeByteLength(this.type) : this._size; } /** @@ -754,7 +755,11 @@ export class VertexBuffer { * @param callback the callback function called for each value */ public forEach(count: number, callback: (value: number, index: number) => void): void { - VertexBuffer.ForEach(this._buffer.getData()!, this.byteOffset, this.byteStride, this._size, this.type, count, this.normalized, callback); + EnumerateFloatValues(this._buffer.getData()!, this.byteOffset, this.byteStride, this._size, this.type, count, this.normalized, (values, index) => { + for (let i = 0; i < this._size; i++) { + callback(values[i], index + i); + } + }); } /** @internal */ @@ -879,22 +884,10 @@ export class VertexBuffer { * Gets the byte length of the given type. * @param type the type * @returns the number of bytes + * @deprecated Use `getTypeByteLength` from `bufferUtils` instead */ public static GetTypeByteLength(type: number): number { - switch (type) { - case VertexBuffer.BYTE: - case VertexBuffer.UNSIGNED_BYTE: - return 1; - case VertexBuffer.SHORT: - case VertexBuffer.UNSIGNED_SHORT: - return 2; - case VertexBuffer.INT: - case VertexBuffer.UNSIGNED_INT: - case VertexBuffer.FLOAT: - return 4; - default: - throw new Error(`Invalid type '${type}'`); - } + return GetTypeByteLength(type); } /** @@ -907,6 +900,7 @@ export class VertexBuffer { * @param count the number of values to enumerate * @param normalized whether the data is normalized * @param callback the callback function called for each value + * @deprecated Use `EnumerateFloatValues` from `bufferUtils` instead */ public static ForEach( data: DataArray, @@ -918,73 +912,11 @@ export class VertexBuffer { normalized: boolean, callback: (value: number, index: number) => void ): void { - if (data instanceof Array) { - let offset = byteOffset / 4; - const stride = byteStride / 4; - for (let index = 0; index < count; index += componentCount) { - for (let componentIndex = 0; componentIndex < componentCount; componentIndex++) { - callback(data[offset + componentIndex], index + componentIndex); - } - offset += stride; - } - } else { - const dataView = data instanceof ArrayBuffer ? new DataView(data) : new DataView(data.buffer, data.byteOffset, data.byteLength); - const componentByteLength = VertexBuffer.GetTypeByteLength(componentType); - for (let index = 0; index < count; index += componentCount) { - let componentByteOffset = byteOffset; - for (let componentIndex = 0; componentIndex < componentCount; componentIndex++) { - const value = VertexBuffer._GetFloatValue(dataView, componentType, componentByteOffset, normalized); - callback(value, index + componentIndex); - componentByteOffset += componentByteLength; - } - byteOffset += byteStride; - } - } - } - - private static _GetFloatValue(dataView: DataView, type: number, byteOffset: number, normalized: boolean): number { - switch (type) { - case VertexBuffer.BYTE: { - let value = dataView.getInt8(byteOffset); - if (normalized) { - value = Math.max(value / 127, -1); - } - return value; - } - case VertexBuffer.UNSIGNED_BYTE: { - let value = dataView.getUint8(byteOffset); - if (normalized) { - value = value / 255; - } - return value; - } - case VertexBuffer.SHORT: { - let value = dataView.getInt16(byteOffset, true); - if (normalized) { - value = Math.max(value / 32767, -1); - } - return value; - } - case VertexBuffer.UNSIGNED_SHORT: { - let value = dataView.getUint16(byteOffset, true); - if (normalized) { - value = value / 65535; - } - return value; - } - case VertexBuffer.INT: { - return dataView.getInt32(byteOffset, true); - } - case VertexBuffer.UNSIGNED_INT: { - return dataView.getUint32(byteOffset, true); - } - case VertexBuffer.FLOAT: { - return dataView.getFloat32(byteOffset, true); - } - default: { - throw new Error(`Invalid component type ${type}`); + EnumerateFloatValues(data, byteOffset, byteStride, componentCount, componentType, count, normalized, (values, index) => { + for (let componentIndex = 0; componentIndex < componentCount; componentIndex++) { + callback(values[componentIndex], index + componentIndex); } - } + }); } /** @@ -998,6 +930,7 @@ export class VertexBuffer { * @param totalVertices number of vertices in the buffer to take into account * @param forceCopy defines a boolean indicating that the returned array must be cloned upon returning it * @returns a float array containing vertex data + * @deprecated Use `GetFloatData` from `bufferUtils` instead */ public static GetFloatData( data: DataArray, @@ -1009,43 +942,6 @@ export class VertexBuffer { totalVertices: number, forceCopy?: boolean ): FloatArray { - const tightlyPackedByteStride = size * VertexBuffer.GetTypeByteLength(type); - const count = totalVertices * size; - - if (type !== VertexBuffer.FLOAT || byteStride !== tightlyPackedByteStride) { - const copy = new Float32Array(count); - VertexBuffer.ForEach(data, byteOffset, byteStride, size, type, count, normalized, (value, index) => (copy[index] = value)); - return copy; - } - - if (!(data instanceof Array || data instanceof Float32Array) || byteOffset !== 0 || data.length !== count) { - if (data instanceof Array) { - const offset = byteOffset / 4; - return data.slice(offset, offset + count); - } else if (data instanceof ArrayBuffer) { - return new Float32Array(data, byteOffset, count); - } else { - const offset = data.byteOffset + byteOffset; - if ((offset & 3) !== 0) { - Logger.Warn("Float array must be aligned to 4-bytes border"); - forceCopy = true; - } - - if (forceCopy) { - const result = new Uint8Array(count * Float32Array.BYTES_PER_ELEMENT); - const source = new Uint8Array(data.buffer, offset, result.length); - result.set(source); - return new Float32Array(result.buffer); - } else { - return new Float32Array(data.buffer, offset, count); - } - } - } - - if (forceCopy) { - return data.slice(); - } - - return data; + return GetFloatData(data, size, type, byteOffset, byteStride, normalized, totalVertices, forceCopy); } } diff --git a/packages/dev/core/src/Buffers/bufferUtils.ts b/packages/dev/core/src/Buffers/bufferUtils.ts index b224e3f2a3a..db01aa7a345 100644 --- a/packages/dev/core/src/Buffers/bufferUtils.ts +++ b/packages/dev/core/src/Buffers/bufferUtils.ts @@ -1,6 +1,247 @@ -import type { DataArray } from "../types"; -import { VertexBuffer } from "./buffer"; +import { Constants } from "../Engines/constants"; import { Logger } from "../Misc/logger"; +import type { DataArray, FloatArray } from "../types"; + +function GetFloatValue(dataView: DataView, type: number, byteOffset: number, normalized: boolean): number { + switch (type) { + case Constants.BYTE: { + let value = dataView.getInt8(byteOffset); + if (normalized) { + value = Math.max(value / 127, -1); + } + return value; + } + case Constants.UNSIGNED_BYTE: { + let value = dataView.getUint8(byteOffset); + if (normalized) { + value = value / 255; + } + return value; + } + case Constants.SHORT: { + let value = dataView.getInt16(byteOffset, true); + if (normalized) { + value = Math.max(value / 32767, -1); + } + return value; + } + case Constants.UNSIGNED_SHORT: { + let value = dataView.getUint16(byteOffset, true); + if (normalized) { + value = value / 65535; + } + return value; + } + case Constants.INT: { + return dataView.getInt32(byteOffset, true); + } + case Constants.UNSIGNED_INT: { + return dataView.getUint32(byteOffset, true); + } + case Constants.FLOAT: { + return dataView.getFloat32(byteOffset, true); + } + default: { + throw new Error(`Invalid component type ${type}`); + } + } +} + +function SetFloatValue(dataView: DataView, type: number, byteOffset: number, normalized: boolean, value: number): void { + switch (type) { + case Constants.BYTE: { + if (normalized) { + value = Math.round(value * 127.0); + } + dataView.setInt8(byteOffset, value); + break; + } + case Constants.UNSIGNED_BYTE: { + if (normalized) { + value = Math.round(value * 255); + } + dataView.setUint8(byteOffset, value); + break; + } + case Constants.SHORT: { + if (normalized) { + value = Math.round(value * 32767); + } + dataView.setInt16(byteOffset, value, true); + break; + } + case Constants.UNSIGNED_SHORT: { + if (normalized) { + value = Math.round(value * 65535); + } + dataView.setUint16(byteOffset, value, true); + break; + } + case Constants.INT: { + dataView.setInt32(byteOffset, value, true); + break; + } + case Constants.UNSIGNED_INT: { + dataView.setUint32(byteOffset, value, true); + break; + } + case Constants.FLOAT: { + dataView.setFloat32(byteOffset, value, true); + break; + } + default: { + throw new Error(`Invalid component type ${type}`); + } + } +} + +/** + * Gets the byte length of the given type. + * @param type the type + * @returns the number of bytes + */ +export function GetTypeByteLength(type: number): number { + switch (type) { + case Constants.BYTE: + case Constants.UNSIGNED_BYTE: + return 1; + case Constants.SHORT: + case Constants.UNSIGNED_SHORT: + return 2; + case Constants.INT: + case Constants.UNSIGNED_INT: + case Constants.FLOAT: + return 4; + default: + throw new Error(`Invalid type '${type}'`); + } +} + +/** + * Enumerates each value of the data array and calls the given callback. + * @param data the data to enumerate + * @param byteOffset the byte offset of the data + * @param byteStride the byte stride of the data + * @param componentCount the number of components per element + * @param componentType the type of the component + * @param count the number of values to enumerate + * @param normalized whether the data is normalized + * @param callback the callback function called for each group of component values + */ +export function EnumerateFloatValues( + data: DataArray, + byteOffset: number, + byteStride: number, + componentCount: number, + componentType: number, + count: number, + normalized: boolean, + callback: (values: number[], index: number) => void +): void { + const oldValues = new Array(componentCount); + const newValues = new Array(componentCount); + + if (data instanceof Array) { + let offset = byteOffset / 4; + const stride = byteStride / 4; + for (let index = 0; index < count; index += componentCount) { + for (let componentIndex = 0; componentIndex < componentCount; componentIndex++) { + oldValues[componentIndex] = newValues[componentIndex] = data[offset + componentIndex]; + } + + callback(newValues, index); + + for (let componentIndex = 0; componentIndex < componentCount; componentIndex++) { + if (oldValues[componentIndex] !== newValues[componentIndex]) { + data[offset + componentIndex] = newValues[componentIndex]; + } + } + + offset += stride; + } + } else { + const dataView = data instanceof ArrayBuffer ? new DataView(data) : new DataView(data.buffer, data.byteOffset, data.byteLength); + const componentByteLength = GetTypeByteLength(componentType); + for (let index = 0; index < count; index += componentCount) { + for (let componentIndex = 0, componentByteOffset = byteOffset; componentIndex < componentCount; componentIndex++, componentByteOffset += componentByteLength) { + oldValues[componentIndex] = newValues[componentIndex] = GetFloatValue(dataView, componentType, componentByteOffset, normalized); + } + + callback(newValues, index); + + for (let componentIndex = 0, componentByteOffset = byteOffset; componentIndex < componentCount; componentIndex++, componentByteOffset += componentByteLength) { + if (oldValues[componentIndex] !== newValues[componentIndex]) { + SetFloatValue(dataView, componentType, componentByteOffset, normalized, newValues[componentIndex]); + } + } + + byteOffset += byteStride; + } + } +} + +/** + * Gets the given data array as a float array. Float data is constructed if the data array cannot be returned directly. + * @param data the input data array + * @param size the number of components + * @param type the component type + * @param byteOffset the byte offset of the data + * @param byteStride the byte stride of the data + * @param normalized whether the data is normalized + * @param totalVertices number of vertices in the buffer to take into account + * @param forceCopy defines a boolean indicating that the returned array must be cloned upon returning it + * @returns a float array containing vertex data + */ +export function GetFloatData( + data: DataArray, + size: number, + type: number, + byteOffset: number, + byteStride: number, + normalized: boolean, + totalVertices: number, + forceCopy?: boolean +): FloatArray { + const tightlyPackedByteStride = size * GetTypeByteLength(type); + const count = totalVertices * size; + + if (type !== Constants.FLOAT || byteStride !== tightlyPackedByteStride) { + const copy = new Float32Array(count); + EnumerateFloatValues(data, byteOffset, byteStride, size, type, count, normalized, (values, index) => { + for (let i = 0; i < size; i++) { + copy[index + i] = values[i]; + } + }); + return copy; + } + + if (!(data instanceof Array || data instanceof Float32Array) || byteOffset !== 0 || data.length !== count) { + if (data instanceof Array) { + const offset = byteOffset / 4; + return data.slice(offset, offset + count); + } else if (data instanceof ArrayBuffer) { + return new Float32Array(data, byteOffset, count); + } else { + const offset = data.byteOffset + byteOffset; + if ((offset & 3) !== 0) { + Logger.Warn("Float array must be aligned to 4-bytes border"); + forceCopy = true; + } + + if (forceCopy) { + return new Float32Array(data.buffer.slice(offset, offset + count * Float32Array.BYTES_PER_ELEMENT)); + } else { + return new Float32Array(data.buffer, offset, count); + } + } + } + + if (forceCopy) { + return data.slice(); + } + + return data; +} /** * Copies the given data array to the given float array. @@ -23,15 +264,19 @@ export function CopyFloatData( totalVertices: number, output: Float32Array ): void { - const tightlyPackedByteStride = size * VertexBuffer.GetTypeByteLength(type); + const tightlyPackedByteStride = size * GetTypeByteLength(type); const count = totalVertices * size; if (output.length !== count) { throw new Error("Output length is not valid"); } - if (type !== VertexBuffer.FLOAT || byteStride !== tightlyPackedByteStride) { - VertexBuffer.ForEach(input, byteOffset, byteStride, size, type, count, normalized, (value, index) => (output[index] = value)); + if (type !== Constants.FLOAT || byteStride !== tightlyPackedByteStride) { + EnumerateFloatValues(input, byteOffset, byteStride, size, type, count, normalized, (values, index) => { + for (let i = 0; i < size; i++) { + output[index + i] = values[i]; + } + }); return; } @@ -43,13 +288,9 @@ export function CopyFloatData( output.set(floatData); } else { const offset = input.byteOffset + byteOffset; - - // Protect against bad data - const remainder = offset % 4; - if (remainder) { - Logger.Warn("CopyFloatData: copied misaligned data."); - // If not aligned, copy the data to aligned buffer - output.set(new Float32Array(input.buffer.slice(offset, offset + count * 4))); + if ((offset & 3) !== 0) { + Logger.Warn("Float array must be aligned to 4-bytes border"); + output.set(new Float32Array(input.buffer.slice(offset, offset + count * Float32Array.BYTES_PER_ELEMENT))); return; } diff --git a/packages/dev/inspector/src/components/actionTabs/tabs/toolsTabComponent.tsx b/packages/dev/inspector/src/components/actionTabs/tabs/toolsTabComponent.tsx index 76c3c80dc8f..ea8a12720f0 100644 --- a/packages/dev/inspector/src/components/actionTabs/tabs/toolsTabComponent.tsx +++ b/packages/dev/inspector/src/components/actionTabs/tabs/toolsTabComponent.tsx @@ -283,17 +283,17 @@ export class ToolsTabComponent extends PaneComponent { return true; }; - GLTF2Export.GLBAsync(scene, "scene", { shouldExportNode: (node) => shouldExport(node) }).then( - (glb: GLTFData) => { + GLTF2Export.GLBAsync(scene, "scene", { shouldExportNode: (node) => shouldExport(node) }) + .then((glb: GLTFData) => { this._isExportingGltf = false; this.forceUpdate(); glb.downloadFiles(); - }, - () => { + }) + .catch((reason) => { + Logger.Error(`Failed to export GLB: ${reason}`); this._isExportingGltf = false; this.forceUpdate(); - } - ); + }); } exportBabylon() { 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 080dbdddf9c..31a23974752 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,14 +1,15 @@ import type { IBufferView, IAccessor, INode, IEXTMeshGpuInstancing } from "babylonjs-gltf2interface"; import { AccessorType, AccessorComponentType } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import type { _BinaryWriter } from "../glTFExporter"; -import { _Exporter } from "../glTFExporter"; +import type { DataWriter } from "../dataWriter"; +import { GLTFExporter } from "../glTFExporter"; import type { Nullable } from "core/types"; 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"; @@ -26,11 +27,11 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { /** Defines whether this extension is required */ public required = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _wasUsed = false; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -47,19 +48,21 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { * @param node the node exported * @param babylonNode the corresponding babylon node * @param nodeMap map from babylon node id to node index - * @param binaryWriter binary writer + * @param convertToRightHanded true if we need to convert data from left hand to right hand system. + * @param dataWriter binary writer * @returns nullable promise, resolves with the node */ public postExportNodeAsync( context: string, node: Nullable, babylonNode: Node, - nodeMap: { [key: number]: number }, - binaryWriter: _BinaryWriter + nodeMap: Map, + convertToRightHanded: boolean, + dataWriter: DataWriter ): Promise> { return new Promise((resolve) => { if (node && babylonNode instanceof Mesh) { - if (babylonNode.hasThinInstances && binaryWriter) { + if (babylonNode.hasThinInstances && this._exporter) { this._wasUsed = true; const noTranslation = Vector3.Zero(); @@ -86,6 +89,11 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { for (const m of matrix) { m.decompose(iws, iwr, iwt); + if (convertToRightHanded) { + ConvertToRightHandedPosition(iwt); + ConvertToRightHandedRotation(iwr); + } + // fill the temp buffer translationBuffer.set(iwt.asArray(), i * 3); rotationBuffer.set(iwr.normalize().asArray(), i * 4); // ensure the quaternion is normalized @@ -109,24 +117,18 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { translationBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, - binaryWriter, + dataWriter, 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, binaryWriter, componentType); + extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, dataWriter, componentType); } // do we need to write SCALE ? if (hasAnyInstanceWorldScale) { - extension.attributes["SCALE"] = this._buildAccessor( - scaleBuffer, - AccessorType.VEC3, - babylonNode.thinInstanceCount, - binaryWriter, - AccessorComponentType.FLOAT - ); + extension.attributes["SCALE"] = this._buildAccessor(scaleBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, dataWriter, AccessorComponentType.FLOAT); } /* eslint-enable @typescript-eslint/naming-convention*/ @@ -138,25 +140,25 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { }); } - private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, binaryWriter: _BinaryWriter, componentType: AccessorComponentType): number { + private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, binaryWriter: DataWriter, componentType: AccessorComponentType): number { // write the buffer - const bufferOffset = binaryWriter.getByteOffset(); + const bufferOffset = binaryWriter.byteOffset; switch (componentType) { case AccessorComponentType.FLOAT: { for (let i = 0; i != buffer.length; i++) { - binaryWriter.setFloat32(buffer[i]); + binaryWriter.writeFloat32(buffer[i]); } break; } case AccessorComponentType.BYTE: { for (let i = 0; i != buffer.length; i++) { - binaryWriter.setByte(buffer[i] * 127); + binaryWriter.writeInt8(buffer[i] * 127); } break; } case AccessorComponentType.SHORT: { for (let i = 0; i != buffer.length; i++) { - binaryWriter.setInt16(buffer[i] * 32767); + binaryWriter.writeInt16(buffer[i] * 32767); } break; @@ -182,4 +184,4 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -_Exporter.RegisterExtension(NAME, (exporter) => new EXT_mesh_gpu_instancing(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new EXT_mesh_gpu_instancing(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_lights_punctual.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_lights_punctual.ts index 780a50febf8..4af4988b8db 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_lights_punctual.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_lights_punctual.ts @@ -1,18 +1,28 @@ import type { SpotLight } from "core/Lights/spotLight"; import type { Nullable } from "core/types"; -import { Vector3, Quaternion, TmpVectors, Matrix } from "core/Maths/math.vector"; -import { Color3 } from "core/Maths/math.color"; +import { Vector3, Quaternion, TmpVectors } from "core/Maths/math.vector"; import { Light } from "core/Lights/light"; import type { Node } from "core/node"; import { ShadowLight } from "core/Lights/shadowLight"; import type { INode, IKHRLightsPunctual_LightReference, IKHRLightsPunctual_Light, IKHRLightsPunctual } from "babylonjs-gltf2interface"; import { KHRLightsPunctual_LightType } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import { Logger } from "core/Misc/logger"; -import { _GLTFUtilities } from "../glTFUtilities"; +import { ConvertToRightHandedPosition, OmitDefaultValues, CollapseParentNode, IsParentAddedByImporter } from "../glTFUtilities"; const NAME = "KHR_lights_punctual"; +const DEFAULTS: Omit = { + name: "", + color: [1, 1, 1], + intensity: 1, + range: Number.MAX_VALUE, +}; +const SPOTDEFAULTS: NonNullable = { + innerConeAngle: 0, + outerConeAngle: Math.PI / 4.0, +}; +const LIGHTDIRECTION = Vector3.Backward(); /** * [Specification](https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/README.md) @@ -29,14 +39,14 @@ export class KHR_lights_punctual implements IGLTFExporterExtensionV2 { public required = false; /** Reference to the glTF exporter */ - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _lights: IKHRLightsPunctual; /** * @internal */ - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -59,132 +69,112 @@ export class KHR_lights_punctual implements IGLTFExporterExtensionV2 { * @param context The context when exporting the node * @param node glTF node * @param babylonNode BabylonJS node - * @param nodeMap Node mapping of unique id to glTF node index + * @param nodeMap Node mapping of babylon node to glTF node index + * @param convertToRightHanded Flag to convert the values to right-handed * @returns nullable INode promise */ - public postExportNodeAsync(context: string, node: Nullable, babylonNode: Node, nodeMap: { [key: number]: number }): Promise> { + public postExportNodeAsync(context: string, node: INode, babylonNode: Node, nodeMap: Map, convertToRightHanded: boolean): Promise> { return new Promise((resolve) => { - if (node && babylonNode instanceof ShadowLight) { - let light: IKHRLightsPunctual_Light; - - const lightType = - babylonNode.getTypeID() == Light.LIGHTTYPEID_POINTLIGHT - ? KHRLightsPunctual_LightType.POINT - : babylonNode.getTypeID() == Light.LIGHTTYPEID_DIRECTIONALLIGHT - ? KHRLightsPunctual_LightType.DIRECTIONAL - : babylonNode.getTypeID() == Light.LIGHTTYPEID_SPOTLIGHT - ? KHRLightsPunctual_LightType.SPOT - : null; - if (lightType == null) { - Logger.Warn(`${context}: Light ${babylonNode.name} is not supported in ${NAME}`); - } else { - if (!babylonNode.position.equalsToFloats(0, 0, 0)) { - node.translation = babylonNode.position.asArray(); - } - if (lightType !== KHRLightsPunctual_LightType.POINT) { - const localAxis = babylonNode.direction; - const yaw = -Math.atan2(localAxis.z, localAxis.x) + Math.PI / 2; - const len = Math.sqrt(localAxis.x * localAxis.x + localAxis.z * localAxis.z); - const pitch = -Math.atan2(localAxis.y, len); - const lightRotationQuaternion = Quaternion.RotationYawPitchRoll(yaw + Math.PI, pitch, 0); - if (!Quaternion.IsIdentity(lightRotationQuaternion)) { - node.rotation = lightRotationQuaternion.asArray(); - } - } - - if (babylonNode.falloffType !== Light.FALLOFF_GLTF) { - Logger.Warn(`${context}: Light falloff for ${babylonNode.name} does not match the ${NAME} specification!`); - } - light = { - type: lightType, - }; - if (!babylonNode.diffuse.equals(Color3.White())) { - light.color = babylonNode.diffuse.asArray(); - } - if (babylonNode.intensity !== 1.0) { - light.intensity = babylonNode.intensity; - } - if (babylonNode.range !== Number.MAX_VALUE) { - light.range = babylonNode.range; - } - - if (lightType === KHRLightsPunctual_LightType.SPOT) { - const babylonSpotLight = babylonNode as SpotLight; - if (babylonSpotLight.angle !== Math.PI / 2.0) { - if (light.spot == null) { - light.spot = {}; - } - light.spot.outerConeAngle = babylonSpotLight.angle / 2.0; - } - if (babylonSpotLight.innerAngle !== 0) { - if (light.spot == null) { - light.spot = {}; - } - light.spot.innerConeAngle = babylonSpotLight.innerAngle / 2.0; - } - } - - this._lights ||= { - lights: [], - }; - - this._lights.lights.push(light); - - const lightReference: IKHRLightsPunctual_LightReference = { - light: this._lights.lights.length - 1, - }; - - // Avoid duplicating the Light's parent node if possible. - const parentBabylonNode = babylonNode.parent; - if (parentBabylonNode && parentBabylonNode.getChildren().length == 1) { - const parentNode = this._exporter._nodes[nodeMap[parentBabylonNode.uniqueId]]; - if (parentNode) { - const parentTranslation = Vector3.FromArrayToRef(parentNode.translation || [0, 0, 0], 0, TmpVectors.Vector3[0]); - const parentRotation = Quaternion.FromArrayToRef(parentNode.rotation || [0, 0, 0, 1], 0, TmpVectors.Quaternion[0]); - const parentScale = Vector3.FromArrayToRef(parentNode.scale || [1, 1, 1], 0, TmpVectors.Vector3[1]); - const parentMatrix = Matrix.ComposeToRef(parentScale, parentRotation, parentTranslation, TmpVectors.Matrix[0]); - - const translation = Vector3.FromArrayToRef(node.translation || [0, 0, 0], 0, TmpVectors.Vector3[2]); - const rotation = Quaternion.FromArrayToRef(node.rotation || [0, 0, 0, 1], 0, TmpVectors.Quaternion[1]); - const matrix = Matrix.ComposeToRef(Vector3.OneReadOnly, rotation, translation, TmpVectors.Matrix[1]); - - parentMatrix.multiplyToRef(matrix, matrix); - matrix.decompose(parentScale, parentRotation, parentTranslation); - - if (parentTranslation.equalsToFloats(0, 0, 0)) { - delete parentNode.translation; - } else { - parentNode.translation = parentTranslation.asArray(); - } - - if (Quaternion.IsIdentity(parentRotation)) { - delete parentNode.rotation; - } else { - parentNode.rotation = parentRotation.asArray(); - } - - if (parentScale.equalsToFloats(1, 1, 1)) { - delete parentNode.scale; - } else { - parentNode.scale = parentScale.asArray(); - } - - parentNode.extensions ||= {}; - parentNode.extensions[NAME] = lightReference; - - // Do not export the original node - resolve(null); - return; - } - } - - node.extensions ||= {}; - node.extensions[NAME] = lightReference; + if (!(babylonNode instanceof ShadowLight)) { + resolve(node); + return; + } + + const lightType = + babylonNode.getTypeID() == Light.LIGHTTYPEID_POINTLIGHT + ? KHRLightsPunctual_LightType.POINT + : babylonNode.getTypeID() == Light.LIGHTTYPEID_DIRECTIONALLIGHT + ? KHRLightsPunctual_LightType.DIRECTIONAL + : babylonNode.getTypeID() == Light.LIGHTTYPEID_SPOTLIGHT + ? KHRLightsPunctual_LightType.SPOT + : null; + if (!lightType) { + Logger.Warn(`${context}: Light ${babylonNode.name} is not supported in ${NAME}`); + resolve(node); + return; + } + + if (babylonNode.falloffType !== Light.FALLOFF_GLTF) { + Logger.Warn(`${context}: Light falloff for ${babylonNode.name} does not match the ${NAME} specification!`); + } + + // Set the node's translation and rotation here, since lights are not handled in exportNodeAsync + if (!babylonNode.position.equalsToFloats(0, 0, 0)) { + const translation = TmpVectors.Vector3[0].copyFrom(babylonNode.position); + if (convertToRightHanded) { + ConvertToRightHandedPosition(translation); + } + node.translation = translation.asArray(); + } + + // Babylon lights have "constant" rotation and variable direction, while + // glTF lights have variable rotation and constant direction. Therefore, + // compute a quaternion that aligns the Babylon light's direction with glTF's constant one. + if (lightType !== KHRLightsPunctual_LightType.POINT) { + const direction = babylonNode.direction.normalizeToRef(TmpVectors.Vector3[0]); + if (convertToRightHanded) { + ConvertToRightHandedPosition(direction); + } + const angle = Math.acos(Vector3.Dot(LIGHTDIRECTION, direction)); + const axis = Vector3.Cross(LIGHTDIRECTION, direction); + const lightRotationQuaternion = Quaternion.RotationAxisToRef(axis, angle, TmpVectors.Quaternion[0]); + if (!Quaternion.IsIdentity(lightRotationQuaternion)) { + node.rotation = lightRotationQuaternion.asArray(); + } + } + + const light: IKHRLightsPunctual_Light = { + type: lightType, + name: babylonNode.name, + color: babylonNode.diffuse.asArray(), + intensity: babylonNode.intensity, + range: babylonNode.range, + }; + OmitDefaultValues(light, DEFAULTS); + + // Separately handle the required 'spot' field for spot lights + if (lightType === KHRLightsPunctual_LightType.SPOT) { + const babylonSpotLight = babylonNode as SpotLight; + light.spot = { + innerConeAngle: babylonSpotLight.innerAngle / 2.0, + outerConeAngle: babylonSpotLight.angle / 2.0, + }; + OmitDefaultValues(light.spot, SPOTDEFAULTS); + } + + this._lights ||= { + lights: [], + }; + this._lights.lights.push(light); + + const lightReference: IKHRLightsPunctual_LightReference = { + light: this._lights.lights.length - 1, + }; + + // Assign the light to its parent node, if possible, to condense the glTF + // Why and when: the glTF loader generates a new parent TransformNode for each light node, which we should undo on export + const parentBabylonNode = babylonNode.parent; + + if (parentBabylonNode && IsParentAddedByImporter(babylonNode, parentBabylonNode)) { + const parentNodeIndex = nodeMap.get(parentBabylonNode); + if (parentNodeIndex) { + // Combine the light's transformation with the parent's + const parentNode = this._exporter._nodes[parentNodeIndex]; + CollapseParentNode(node, parentNode); + parentNode.extensions ||= {}; + parentNode.extensions[NAME] = lightReference; + + // Do not export the original node + resolve(null); + return; } } + + node.extensions ||= {}; + node.extensions[NAME] = lightReference; resolve(node); }); } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_lights_punctual(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_lights_punctual(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_anisotropy.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_anisotropy.ts index 16f0f80d382..2941ce70713 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_anisotropy.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_anisotropy.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsAnisotropy } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRBaseMaterial } from "core/Materials/PBR/pbrBaseMaterial"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; @@ -21,11 +21,11 @@ export class KHR_materials_anisotropy implements IGLTFExporterExtensionV2 { /** Defines whether this extension is required */ public required = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _wasUsed = false; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -62,17 +62,18 @@ export class KHR_materials_anisotropy implements IGLTFExporterExtensionV2 { node.extensions = node.extensions || {}; - const anisotropyTextureInfo = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.anisotropy.texture); + const anisotropyTextureInfo = this._exporter._materialExporter.getTextureInfo(babylonMaterial.anisotropy.texture); const anisotropyInfo: IKHRMaterialsAnisotropy = { anisotropyStrength: babylonMaterial.anisotropy.intensity, anisotropyRotation: babylonMaterial.anisotropy.angle, anisotropyTexture: anisotropyTextureInfo ?? undefined, - hasTextures: () => { - return anisotropyInfo.anisotropyTexture !== null; - }, }; + if (anisotropyInfo.anisotropyTexture !== null) { + this._exporter._materialNeedsUVsSet.add(babylonMaterial); + } + node.extensions[NAME] = anisotropyInfo; } resolve(node); @@ -80,4 +81,4 @@ export class KHR_materials_anisotropy implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_anisotropy(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_anisotropy(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_clearcoat.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_clearcoat.ts index 2d0419af55d..38a1b092589 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_clearcoat.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_clearcoat.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsClearcoat } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRBaseMaterial } from "core/Materials/PBR/pbrBaseMaterial"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; @@ -23,11 +23,11 @@ export class KHR_materials_clearcoat implements IGLTFExporterExtensionV2 { /** Defines whether this extension is required */ public required = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _wasUsed = false; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -70,12 +70,12 @@ export class KHR_materials_clearcoat implements IGLTFExporterExtensionV2 { node.extensions = node.extensions || {}; - const clearCoatTextureInfo = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.clearCoat.texture); + const clearCoatTextureInfo = this._exporter._materialExporter.getTextureInfo(babylonMaterial.clearCoat.texture); let clearCoatTextureRoughnessInfo; if (babylonMaterial.clearCoat.useRoughnessFromMainTexture) { - clearCoatTextureRoughnessInfo = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.clearCoat.texture); + clearCoatTextureRoughnessInfo = this._exporter._materialExporter.getTextureInfo(babylonMaterial.clearCoat.texture); } else { - clearCoatTextureRoughnessInfo = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.clearCoat.textureRoughness); + clearCoatTextureRoughnessInfo = this._exporter._materialExporter.getTextureInfo(babylonMaterial.clearCoat.textureRoughness); } if (babylonMaterial.clearCoat.isTintEnabled) { @@ -86,7 +86,7 @@ export class KHR_materials_clearcoat implements IGLTFExporterExtensionV2 { Tools.Warn(`Clear Color F0 remapping is not supported for glTF export. Ignoring for: ${babylonMaterial.name}`); } - const clearCoatNormalTextureInfo = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.clearCoat.bumpTexture); + const clearCoatNormalTextureInfo = this._exporter._materialExporter.getTextureInfo(babylonMaterial.clearCoat.bumpTexture); const clearCoatInfo: IKHRMaterialsClearcoat = { clearcoatFactor: babylonMaterial.clearCoat.intensity, @@ -94,11 +94,12 @@ export class KHR_materials_clearcoat implements IGLTFExporterExtensionV2 { clearcoatRoughnessFactor: babylonMaterial.clearCoat.roughness, clearcoatRoughnessTexture: clearCoatTextureRoughnessInfo ?? undefined, clearcoatNormalTexture: clearCoatNormalTextureInfo ?? undefined, - hasTextures: () => { - return clearCoatInfo.clearcoatTexture !== null || clearCoatInfo.clearcoatRoughnessTexture !== null || clearCoatInfo.clearcoatRoughnessTexture !== null; - }, }; + if (clearCoatInfo.clearcoatTexture !== null || clearCoatInfo.clearcoatRoughnessTexture !== null || clearCoatInfo.clearcoatRoughnessTexture !== null) { + this._exporter._materialNeedsUVsSet.add(babylonMaterial); + } + node.extensions[NAME] = clearCoatInfo; } resolve(node); @@ -106,4 +107,4 @@ export class KHR_materials_clearcoat implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_clearcoat(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_clearcoat(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.ts index d5a18fda883..562dc5f2750 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsDiffuseTransmission } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; @@ -22,11 +22,11 @@ export class KHR_materials_diffuse_transmission implements IGLTFExporterExtensio /** Defines whether this extension is required */ public required = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _wasUsed = false; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -98,19 +98,21 @@ export class KHR_materials_diffuse_transmission implements IGLTFExporterExtensio const subs = babylonMaterial.subSurface; const diffuseTransmissionFactor = subs.translucencyIntensity == 1 ? undefined : subs.translucencyIntensity; - const diffuseTransmissionTexture = this._exporter._glTFMaterialExporter._getTextureInfo(subs.translucencyIntensityTexture) ?? undefined; + const diffuseTransmissionTexture = this._exporter._materialExporter.getTextureInfo(subs.translucencyIntensityTexture) ?? undefined; const diffuseTransmissionColorFactor = !subs.translucencyColor || subs.translucencyColor.equalsFloats(1.0, 1.0, 1.0) ? undefined : subs.translucencyColor.asArray(); - const diffuseTransmissionColorTexture = this._exporter._glTFMaterialExporter._getTextureInfo(subs.translucencyColorTexture) ?? undefined; + const diffuseTransmissionColorTexture = this._exporter._materialExporter.getTextureInfo(subs.translucencyColorTexture) ?? undefined; const diffuseTransmissionInfo: IKHRMaterialsDiffuseTransmission = { diffuseTransmissionFactor, diffuseTransmissionTexture, diffuseTransmissionColorFactor, diffuseTransmissionColorTexture, - hasTextures: () => { - return this._hasTexturesExtension(babylonMaterial); - }, }; + + if (this._hasTexturesExtension(babylonMaterial)) { + this._exporter._materialNeedsUVsSet.add(babylonMaterial); + } + node.extensions = node.extensions || {}; node.extensions[NAME] = diffuseTransmissionInfo; } @@ -119,4 +121,4 @@ export class KHR_materials_diffuse_transmission implements IGLTFExporterExtensio } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_diffuse_transmission(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_diffuse_transmission(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_dispersion.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_dispersion.ts index 242d37dd893..a11b41e2342 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_dispersion.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_dispersion.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsDispersion } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; @@ -73,4 +73,4 @@ export class KHR_materials_dispersion implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, () => new KHR_materials_dispersion()); +GLTFExporter.RegisterExtension(NAME, () => new KHR_materials_dispersion()); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_emissive_strength.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_emissive_strength.ts index 0dd69ff6c4e..0443db6d6ce 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_emissive_strength.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_emissive_strength.ts @@ -1,5 +1,5 @@ import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; import type { IMaterial, IKHRMaterialsEmissiveStrength } from "babylonjs-gltf2interface"; @@ -67,4 +67,4 @@ export class KHR_materials_emissive_strength implements IGLTFExporterExtensionV2 } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_emissive_strength()); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_emissive_strength()); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_ior.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_ior.ts index 77c4d7cc0d1..ae2f34bad63 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_ior.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_ior.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsIor } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; @@ -64,4 +64,4 @@ export class KHR_materials_ior implements IGLTFExporterExtensionV2 { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_ior()); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_ior()); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_iridescence.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_iridescence.ts index 4b354c22c77..5bd01b5f29d 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_iridescence.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_iridescence.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsIridescence } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRBaseMaterial } from "core/Materials/PBR/pbrBaseMaterial"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; @@ -21,11 +21,11 @@ export class KHR_materials_iridescence implements IGLTFExporterExtensionV2 { /** Defines whether this extension is required */ public required = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _wasUsed = false; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -65,8 +65,8 @@ export class KHR_materials_iridescence implements IGLTFExporterExtensionV2 { node.extensions = node.extensions || {}; - const iridescenceTextureInfo = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.iridescence.texture); - const iridescenceThicknessTextureInfo = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.iridescence.thicknessTexture); + const iridescenceTextureInfo = this._exporter._materialExporter.getTextureInfo(babylonMaterial.iridescence.texture); + const iridescenceThicknessTextureInfo = this._exporter._materialExporter.getTextureInfo(babylonMaterial.iridescence.thicknessTexture); const iridescenceInfo: IKHRMaterialsIridescence = { iridescenceFactor: babylonMaterial.iridescence.intensity, @@ -76,11 +76,12 @@ export class KHR_materials_iridescence implements IGLTFExporterExtensionV2 { iridescenceTexture: iridescenceTextureInfo ?? undefined, iridescenceThicknessTexture: iridescenceThicknessTextureInfo ?? undefined, - hasTextures: () => { - return iridescenceInfo.iridescenceTexture !== null || iridescenceInfo.iridescenceThicknessTexture !== null; - }, }; + if (iridescenceInfo.iridescenceTexture !== null || iridescenceInfo.iridescenceThicknessTexture !== null) { + this._exporter._materialNeedsUVsSet.add(babylonMaterial); + } + node.extensions[NAME] = iridescenceInfo; } resolve(node); @@ -88,4 +89,4 @@ export class KHR_materials_iridescence implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_iridescence(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_iridescence(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_sheen.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_sheen.ts index abc4d285fd2..3a1e682ba30 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_sheen.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_sheen.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsSheen } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; @@ -23,9 +23,9 @@ export class KHR_materials_sheen implements IGLTFExporterExtensionV2 { private _wasUsed = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -62,19 +62,20 @@ export class KHR_materials_sheen implements IGLTFExporterExtensionV2 { const sheenInfo: IKHRMaterialsSheen = { sheenColorFactor: babylonMaterial.sheen.color.asArray(), sheenRoughnessFactor: babylonMaterial.sheen.roughness ?? 0, - hasTextures: () => { - return sheenInfo.sheenColorTexture !== null || sheenInfo.sheenRoughnessTexture !== null; - }, }; + if (sheenInfo.sheenColorTexture !== null || sheenInfo.sheenRoughnessTexture !== null) { + this._exporter._materialNeedsUVsSet.add(babylonMaterial); + } + if (babylonMaterial.sheen.texture) { - sheenInfo.sheenColorTexture = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.sheen.texture) ?? undefined; + sheenInfo.sheenColorTexture = this._exporter._materialExporter.getTextureInfo(babylonMaterial.sheen.texture) ?? undefined; } if (babylonMaterial.sheen.textureRoughness && !babylonMaterial.sheen.useRoughnessFromMainTexture) { - sheenInfo.sheenRoughnessTexture = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.sheen.textureRoughness) ?? undefined; + sheenInfo.sheenRoughnessTexture = this._exporter._materialExporter.getTextureInfo(babylonMaterial.sheen.textureRoughness) ?? undefined; } else if (babylonMaterial.sheen.texture && babylonMaterial.sheen.useRoughnessFromMainTexture) { - sheenInfo.sheenRoughnessTexture = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.sheen.texture) ?? undefined; + sheenInfo.sheenRoughnessTexture = this._exporter._materialExporter.getTextureInfo(babylonMaterial.sheen.texture) ?? undefined; } node.extensions[NAME] = sheenInfo; @@ -84,4 +85,4 @@ export class KHR_materials_sheen implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_sheen(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_sheen(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_specular.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_specular.ts index d2b929cb941..95b0b1a09b0 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_specular.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_specular.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsSpecular } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; @@ -21,11 +21,11 @@ export class KHR_materials_specular implements IGLTFExporterExtensionV2 { /** Defines whether this extension is required */ public required = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _wasUsed = false; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -92,8 +92,8 @@ export class KHR_materials_specular implements IGLTFExporterExtensionV2 { node.extensions = node.extensions || {}; - const metallicReflectanceTexture = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.metallicReflectanceTexture) ?? undefined; - const reflectanceTexture = this._exporter._glTFMaterialExporter._getTextureInfo(babylonMaterial.reflectanceTexture) ?? undefined; + const metallicReflectanceTexture = this._exporter._materialExporter.getTextureInfo(babylonMaterial.metallicReflectanceTexture) ?? undefined; + const reflectanceTexture = this._exporter._materialExporter.getTextureInfo(babylonMaterial.reflectanceTexture) ?? undefined; const metallicF0Factor = babylonMaterial.metallicF0Factor == 1.0 ? undefined : babylonMaterial.metallicF0Factor; const metallicReflectanceColor = babylonMaterial.metallicReflectanceColor.equalsFloats(1.0, 1.0, 1.0) ? undefined @@ -104,10 +104,12 @@ export class KHR_materials_specular implements IGLTFExporterExtensionV2 { specularTexture: metallicReflectanceTexture, specularColorFactor: metallicReflectanceColor, specularColorTexture: reflectanceTexture, - hasTextures: () => { - return this._hasTexturesExtension(babylonMaterial); - }, }; + + if (this._hasTexturesExtension(babylonMaterial)) { + this._exporter._materialNeedsUVsSet.add(babylonMaterial); + } + node.extensions[NAME] = specularInfo; } resolve(node); @@ -115,4 +117,4 @@ export class KHR_materials_specular implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_specular(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_specular(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_transmission.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_transmission.ts index 60e6d680129..fbea34a86d1 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_transmission.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_transmission.ts @@ -1,7 +1,7 @@ import type { IMaterial, IKHRMaterialsTransmission } from "babylonjs-gltf2interface"; import { ImageMimeType } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; @@ -23,11 +23,11 @@ export class KHR_materials_transmission implements IGLTFExporterExtensionV2 { /** Defines whether this extension is required */ public required = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _wasUsed = false; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -90,14 +90,15 @@ export class KHR_materials_transmission implements IGLTFExporterExtensionV2 { const volumeInfo: IKHRMaterialsTransmission = { transmissionFactor: transmissionFactor, - hasTextures: () => { - return this._hasTexturesExtension(babylonMaterial); - }, }; + if (this._hasTexturesExtension(babylonMaterial)) { + this._exporter._materialNeedsUVsSet.add(babylonMaterial); + } + if (subSurface.refractionIntensityTexture) { if (subSurface.useGltfStyleTextures) { - const transmissionTexture = await this._exporter._glTFMaterialExporter._exportTextureInfoAsync(subSurface.refractionIntensityTexture, ImageMimeType.PNG); + const transmissionTexture = await this._exporter._materialExporter.exportTextureAsync(subSurface.refractionIntensityTexture, ImageMimeType.PNG); if (transmissionTexture) { volumeInfo.transmissionTexture = transmissionTexture; } @@ -114,4 +115,4 @@ export class KHR_materials_transmission implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_transmission(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_transmission(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_unlit.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_unlit.ts index 17c9e909e67..9d2aece1665 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_unlit.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_unlit.ts @@ -1,6 +1,6 @@ import type { IMaterial } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; import { StandardMaterial } from "core/Materials/standardMaterial"; @@ -57,4 +57,4 @@ export class KHR_materials_unlit implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, () => new KHR_materials_unlit()); +GLTFExporter.RegisterExtension(NAME, () => new KHR_materials_unlit()); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_volume.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_volume.ts index c932c965079..214361ef8e4 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_volume.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_materials_volume.ts @@ -1,6 +1,6 @@ import type { IMaterial, IKHRMaterialsVolume } from "babylonjs-gltf2interface"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; import type { Material } from "core/Materials/material"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; @@ -22,11 +22,11 @@ export class KHR_materials_volume implements IGLTFExporterExtensionV2 { /** Defines whether this extension is required */ public required = false; - private _exporter: _Exporter; + private _exporter: GLTFExporter; private _wasUsed = false; - constructor(exporter: _Exporter) { + constructor(exporter: GLTFExporter) { this._exporter = exporter; } @@ -95,7 +95,7 @@ export class KHR_materials_volume implements IGLTFExporterExtensionV2 { const subs = babylonMaterial.subSurface; const thicknessFactor = subs.maximumThickness == 0 ? undefined : subs.maximumThickness; - const thicknessTexture = this._exporter._glTFMaterialExporter._getTextureInfo(subs.thicknessTexture) ?? undefined; + const thicknessTexture = this._exporter._materialExporter.getTextureInfo(subs.thicknessTexture) ?? undefined; const attenuationDistance = subs.tintColorAtDistance == Number.POSITIVE_INFINITY ? undefined : subs.tintColorAtDistance; const attenuationColor = subs.tintColor.equalsFloats(1.0, 1.0, 1.0) ? undefined : subs.tintColor.asArray(); @@ -104,10 +104,12 @@ export class KHR_materials_volume implements IGLTFExporterExtensionV2 { thicknessTexture: thicknessTexture, attenuationDistance: attenuationDistance, attenuationColor: attenuationColor, - hasTextures: () => { - return this._hasTexturesExtension(babylonMaterial); - }, }; + + if (this._hasTexturesExtension(babylonMaterial)) { + this._exporter._materialNeedsUVsSet.add(babylonMaterial); + } + node.extensions = node.extensions || {}; node.extensions[NAME] = volumeInfo; } @@ -116,4 +118,4 @@ export class KHR_materials_volume implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, (exporter) => new KHR_materials_volume(exporter)); +GLTFExporter.RegisterExtension(NAME, (exporter) => new KHR_materials_volume(exporter)); diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_texture_transform.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_texture_transform.ts index 093c5352973..a5a09ea5923 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_texture_transform.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/KHR_texture_transform.ts @@ -3,7 +3,7 @@ import { Tools } from "core/Misc/tools"; import type { Texture } from "core/Materials/Textures/texture"; import type { Nullable } from "core/types"; import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension"; -import { _Exporter } from "../glTFExporter"; +import { GLTFExporter } from "../glTFExporter"; const NAME = "KHR_texture_transform"; @@ -100,4 +100,4 @@ export class KHR_texture_transform implements IGLTFExporterExtensionV2 { } } -_Exporter.RegisterExtension(NAME, () => new KHR_texture_transform()); +GLTFExporter.RegisterExtension(NAME, () => new KHR_texture_transform()); 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 cd981e5901e..57c28d537f0 100644 --- a/packages/dev/serializers/src/glTF/2.0/Extensions/index.ts +++ b/packages/dev/serializers/src/glTF/2.0/Extensions/index.ts @@ -1,15 +1,15 @@ -export * from "./KHR_texture_transform"; +export * from "./EXT_mesh_gpu_instancing"; export * from "./KHR_lights_punctual"; +export * from "./KHR_materials_anisotropy"; export * from "./KHR_materials_clearcoat"; +export * from "./KHR_materials_diffuse_transmission"; +export * from "./KHR_materials_dispersion"; +export * from "./KHR_materials_emissive_strength"; +export * from "./KHR_materials_ior"; export * from "./KHR_materials_iridescence"; -export * from "./KHR_materials_anisotropy"; export * from "./KHR_materials_sheen"; -export * from "./KHR_materials_unlit"; -export * from "./KHR_materials_ior"; export * from "./KHR_materials_specular"; -export * from "./KHR_materials_volume"; -export * from "./KHR_materials_dispersion"; export * from "./KHR_materials_transmission"; -export * from "./EXT_mesh_gpu_instancing"; -export * from "./KHR_materials_emissive_strength"; -export * from "./KHR_materials_diffuse_transmission"; +export * from "./KHR_materials_unlit"; +export * from "./KHR_materials_volume"; +export * from "./KHR_texture_transform"; 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..a37b9b81dc5 --- /dev/null +++ b/packages/dev/serializers/src/glTF/2.0/dataWriter.ts @@ -0,0 +1,80 @@ +/* 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); + } + } +} diff --git a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts index 9e9c6e13b0d..84e6afe8714 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFAnimation.ts @@ -10,13 +10,20 @@ import type { Scene } from "core/scene"; import { MorphTarget } from "core/Morph/morphTarget"; import { Mesh } from "core/Meshes/mesh"; -import type { _BinaryWriter } from "./glTFExporter"; -import { _GLTFUtilities } from "./glTFUtilities"; import type { IAnimationKey } from "core/Animations/animationKey"; 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"; /** * @internal @@ -233,12 +240,13 @@ export class _GLTFAnimation { babylonNode: Node, runtimeGLTFAnimation: IAnimation, idleGLTFAnimations: IAnimation[], - nodeMap: { [key: number]: number }, + nodeMap: Map, nodes: INode[], - binaryWriter: _BinaryWriter, + binaryWriter: DataWriter, bufferViews: IBufferView[], accessors: IAccessor[], animationSampleRate: number, + useRightHanded: boolean, shouldExportAnimation?: (animation: Animation) => boolean ) { let glTFAnimation: IAnimation; @@ -267,7 +275,8 @@ export class _GLTFAnimation { bufferViews, accessors, animationInfo.useQuaternion, - animationSampleRate + animationSampleRate, + useRightHanded ); if (glTFAnimation.samplers.length && glTFAnimation.channels.length) { idleGLTFAnimations.push(glTFAnimation); @@ -295,12 +304,13 @@ export class _GLTFAnimation { babylonNode: Node, runtimeGLTFAnimation: IAnimation, idleGLTFAnimations: IAnimation[], - nodeMap: { [key: number]: number }, + nodeMap: Map, nodes: INode[], - binaryWriter: _BinaryWriter, + binaryWriter: DataWriter, bufferViews: IBufferView[], accessors: IAccessor[], animationSampleRate: number, + useRightHanded: boolean, shouldExportAnimation?: (animation: Animation) => boolean ) { let glTFAnimation: IAnimation; @@ -355,6 +365,7 @@ export class _GLTFAnimation { accessors, animationInfo.useQuaternion, animationSampleRate, + useRightHanded, morphTargetManager.numTargets ); if (glTFAnimation.samplers.length && glTFAnimation.channels.length) { @@ -382,11 +393,12 @@ export class _GLTFAnimation { public static _CreateNodeAndMorphAnimationFromAnimationGroups( babylonScene: Scene, glTFAnimations: IAnimation[], - nodeMap: { [key: number]: number }, - binaryWriter: _BinaryWriter, + nodeMap: Map, + binaryWriter: DataWriter, bufferViews: IBufferView[], accessors: IAccessor[], animationSampleRate: number, + leftHandedNodes: Set, shouldExportAnimation?: (animation: Animation) => boolean ) { let glTFAnimation: IAnimation; @@ -409,6 +421,9 @@ export class _GLTFAnimation { if (shouldExportAnimation && !shouldExportAnimation(animation)) { continue; } + + const convertToRightHanded = leftHandedNodes.has(target); + if (this._IsTransformable(target) || (target.length === 1 && this._IsTransformable(target[0]))) { const animationInfo = _GLTFAnimation._DeduceAnimationInfo(targetAnimation.animation); if (animationInfo) { @@ -426,7 +441,8 @@ export class _GLTFAnimation { bufferViews, accessors, animationInfo.useQuaternion, - animationSampleRate + animationSampleRate, + convertToRightHanded ); } } @@ -523,6 +539,7 @@ export class _GLTFAnimation { accessors, animationInfo.useQuaternion, animationSampleRate, + false, morphTargetManager?.numTargets ); } @@ -541,12 +558,13 @@ export class _GLTFAnimation { animation: Animation, dataAccessorType: AccessorType, animationChannelTargetPath: AnimationChannelTargetPath, - nodeMap: { [key: number]: number }, - binaryWriter: _BinaryWriter, + nodeMap: Map, + binaryWriter: DataWriter, bufferViews: IBufferView[], accessors: IAccessor[], useQuaternion: boolean, animationSampleRate: number, + convertToRightHanded: boolean, morphAnimationChannels?: number ) { const animationData = _GLTFAnimation._CreateNodeAnimation(babylonTransformNode, animation, animationChannelTargetPath, useQuaternion, animationSampleRate); @@ -578,44 +596,107 @@ export class _GLTFAnimation { animationData.inputs = newInputs; } - const nodeIndex = nodeMap[babylonTransformNode.uniqueId]; + const nodeIndex = nodeMap.get(babylonTransformNode); // Creates buffer view and accessor for key frames. let byteLength = animationData.inputs.length * 4; - bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, undefined, `${name} keyframe data view`); + const offset = binaryWriter.byteOffset; + bufferView = CreateBufferView(0, offset, byteLength); bufferViews.push(bufferView); animationData.inputs.forEach(function (input) { - binaryWriter.setFloat32(input); + binaryWriter.writeFloat32(input); + }); + + accessor = CreateAccessor(bufferViews.length - 1, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, null, { + min: [animationData.inputsMin], + max: [animationData.inputsMax], }); - accessor = _GLTFUtilities._CreateAccessor( - bufferViews.length - 1, - `${name} keyframes`, - AccessorType.SCALAR, - AccessorComponentType.FLOAT, - animationData.inputs.length, - null, - [animationData.inputsMin], - [animationData.inputsMax] - ); accessors.push(accessor); keyframeAccessorIndex = accessors.length - 1; // create bufferview and accessor for keyed values. outputLength = animationData.outputs.length; - byteLength = _GLTFUtilities._GetDataAccessorElementCount(dataAccessorType) * 4 * animationData.outputs.length; + byteLength = GetAccessorElementCount(dataAccessorType) * 4 * animationData.outputs.length; // check for in and out tangents - bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, undefined, `${name} data view`); + bufferView = CreateBufferView(0, binaryWriter.byteOffset, byteLength); bufferViews.push(bufferView); + const rotationQuaternion = new Quaternion(); + const eulerVec3 = new Vector3(); + const position = new Vector3(); + const isCamera = babylonTransformNode instanceof Camera; + animationData.outputs.forEach(function (output) { - output.forEach(function (entry) { - binaryWriter.setFloat32(entry); - }); + 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); + break; + + case AnimationChannelTargetPath.ROTATION: + if (output.length === 4) { + Quaternion.FromArrayToRef(output, 0, rotationQuaternion); + } else { + Vector3.FromArrayToRef(output, 0, eulerVec3); + Quaternion.FromEulerVectorToRef(eulerVec3, rotationQuaternion); + } + + if (isCamera) { + ConvertCameraRotationToGLTF(rotationQuaternion); + } else { + if (!Quaternion.IsIdentity(rotationQuaternion)) { + ConvertToRightHandedRotation(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); + }); + break; + } + } else { + switch (animationChannelTargetPath) { + case AnimationChannelTargetPath.ROTATION: + if (output.length === 4) { + Quaternion.FromArrayToRef(output, 0, rotationQuaternion); + } else { + 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); + }); + break; + } + } }); - accessor = _GLTFUtilities._CreateAccessor(bufferViews.length - 1, `${name} data`, dataAccessorType, AccessorComponentType.FLOAT, outputLength, null, null, null); + accessor = CreateAccessor(bufferViews.length - 1, dataAccessorType, AccessorComponentType.FLOAT, outputLength, null); accessors.push(accessor); dataAccessorIndex = accessors.length - 1; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFData.ts b/packages/dev/serializers/src/glTF/2.0/glTFData.ts index 6befaf8b627..91a26bd163f 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFData.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFData.ts @@ -1,4 +1,23 @@ import { ImageMimeType } from "babylonjs-gltf2interface"; +import { Tools } from "core/Misc/tools"; + +function GetMimeType(fileName: string): string | undefined { + if (fileName.endsWith(".glb")) { + return "model/gltf-binary"; + } else if (fileName.endsWith(".bin")) { + return "application/octet-stream"; + } else if (fileName.endsWith(".gltf")) { + return "model/gltf+json"; + } else if (fileName.endsWith(".jpeg") || fileName.endsWith(".jpg")) { + return ImageMimeType.JPEG; + } else if (fileName.endsWith(".png")) { + return ImageMimeType.PNG; + } else if (fileName.endsWith(".webp")) { + return ImageMimeType.WEBP; + } + + return undefined; +} /** * Class for holding and downloading glTF file data @@ -7,51 +26,23 @@ export class GLTFData { /** * Object which contains the file name as the key and its data as the value */ - glTFFiles: { [fileName: string]: string | Blob }; + public readonly files: { [fileName: string]: string | Blob } = {}; /** - * Initializes the glTF file object + * @deprecated Use files instead */ - public constructor() { - this.glTFFiles = {}; + public get glTFFiles() { + return this.files; } /** * Downloads the glTF data as files based on their names and data */ public downloadFiles(): void { - /** - * Checks for a matching suffix at the end of a string (for ES5 and lower) - * @param str Source string - * @param suffix Suffix to search for in the source string - * @returns Boolean indicating whether the suffix was found (true) or not (false) - */ - function endsWith(str: string, suffix: string): boolean { - return str.indexOf(suffix, str.length - suffix.length) !== -1; - } - - for (const key in this.glTFFiles) { - const link = document.createElement("a"); - document.body.appendChild(link); - link.setAttribute("type", "hidden"); - link.download = key; - const blob = this.glTFFiles[key]; - let mimeType; - - if (endsWith(key, ".glb")) { - mimeType = { type: "model/gltf-binary" }; - } else if (endsWith(key, ".bin")) { - mimeType = { type: "application/octet-stream" }; - } else if (endsWith(key, ".gltf")) { - mimeType = { type: "model/gltf+json" }; - } else if (endsWith(key, ".jpeg") || endsWith(key, ".jpg")) { - mimeType = { type: ImageMimeType.JPEG }; - } else if (endsWith(key, ".png")) { - mimeType = { type: ImageMimeType.PNG }; - } - - link.href = window.URL.createObjectURL(new Blob([blob], mimeType)); - link.click(); + for (const key in this.files) { + const value = this.files[key]; + const blob = new Blob([value], { type: GetMimeType(key) }); + Tools.Download(blob, key); } } } diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts index df74d41f952..b0b4db5490a 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporter.ts @@ -16,260 +16,264 @@ import type { ISkin, ICamera, } from "babylonjs-gltf2interface"; -import { AccessorType, ImageMimeType, MeshPrimitiveMode, AccessorComponentType, CameraType } from "babylonjs-gltf2interface"; +import { AccessorComponentType, AccessorType, CameraType, ImageMimeType } from "babylonjs-gltf2interface"; import type { FloatArray, IndicesArray, Nullable } from "core/types"; -import { Matrix, TmpVectors, Vector2, Vector3, Vector4, Quaternion } from "core/Maths/math.vector"; -import { Color3, Color4 } from "core/Maths/math.color"; +import { TmpVectors, Quaternion, Matrix } from "core/Maths/math.vector"; import { Tools } from "core/Misc/tools"; +import type { Buffer } from "core/Buffers/buffer"; import { VertexBuffer } from "core/Buffers/buffer"; import type { Node } from "core/node"; import { TransformNode } from "core/Meshes/transformNode"; -import type { AbstractMesh } from "core/Meshes/abstractMesh"; import type { SubMesh } from "core/Meshes/subMesh"; import { Mesh } from "core/Meshes/mesh"; -import type { MorphTarget } from "core/Morph/morphTarget"; -import { LinesMesh } from "core/Meshes/linesMesh"; import { InstancedMesh } from "core/Meshes/instancedMesh"; -import type { Bone } from "core/Bones/bone"; import type { BaseTexture } from "core/Materials/Textures/baseTexture"; import type { Texture } from "core/Materials/Textures/texture"; import { Material } from "core/Materials/material"; import { Engine } from "core/Engines/engine"; import type { Scene } from "core/scene"; +import { EngineStore } from "core/Engines/engineStore"; import type { IGLTFExporterExtensionV2 } from "./glTFExporterExtension"; -import { _GLTFMaterialExporter } from "./glTFMaterialExporter"; +import { GLTFMaterialExporter } from "./glTFMaterialExporter"; import type { IExportOptions } from "./glTFSerializer"; -import { _GLTFUtilities } from "./glTFUtilities"; import { GLTFData } from "./glTFData"; -import { _GLTFAnimation } from "./glTFAnimation"; +import { + AreIndices32Bits, + ConvertToRightHandedPosition, + ConvertToRightHandedRotation, + CreateAccessor, + CreateBufferView, + DataArrayToUint8Array, + GetAccessorType, + GetAttributeType, + GetMinMax, + GetPrimitiveMode, + IndicesArrayToUint8Array, + IsNoopNode, + IsTriangleFillMode, + IsParentAddedByImporter, + ConvertToRightHandedNode, + RotateNode180Y, + FloatsNeed16BitInteger, + IsStandardVertexAttribute, +} from "./glTFUtilities"; +import { DataWriter } from "./dataWriter"; import { Camera } from "core/Cameras/camera"; -import { EngineStore } from "core/Engines/engineStore"; import { MultiMaterial } from "core/Materials/multiMaterial"; +import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; +import { StandardMaterial } from "core/Materials/standardMaterial"; +import { Logger } from "core/Misc/logger"; +import { EnumerateFloatValues } from "core/Buffers/bufferUtils"; +import type { Bone, Skeleton } from "core/Bones"; +import { _GLTFAnimation } from "./glTFAnimation"; +import type { MorphTarget } from "core/Morph"; +import { BuildMorphTargetBuffers } from "./glTFMorphTargetsUtilities"; +import type { IMorphTargetData } from "./glTFMorphTargetsUtilities"; +import { LinesMesh } from "core/Meshes/linesMesh"; +import { Color3, Color4 } from "core/Maths/math.color"; -// Matrix that converts handedness on the X-axis. -const convertHandednessMatrix = Matrix.Compose(new Vector3(-1, 1, 1), Quaternion.Identity(), Vector3.Zero()); +class ExporterState { + // Babylon indices array, start, count, offset, flip -> glTF accessor index + private _indicesAccessorMap = new Map, Map>>>>(); -// 180 degrees rotation in Y. -const rotation180Y = new Quaternion(0, 1, 0, 0); + // Babylon buffer -> glTF buffer view index + private _vertexBufferViewMap = new Map(); -function isNoopNode(node: Node, useRightHandedSystem: boolean): boolean { - if (!(node instanceof TransformNode)) { - return false; + // Babylon vertex buffer, start, count -> glTF accessor index + private _vertexAccessorMap = new Map>>(); + + private _remappedBufferView = new Map>(); + + private _meshMorphTargetMap = new Map(); + + private _vertexMapColorAlpha = new Map(); + + private _exportedNodes = new Set(); + + // Babylon mesh -> glTF mesh index + private _meshMap = new Map(); + + public constructor(convertToRightHanded: boolean, wasAddedByNoopNode: boolean) { + this.convertToRightHanded = convertToRightHanded; + this.wasAddedByNoopNode = wasAddedByNoopNode; } - // Transform - if (useRightHandedSystem) { - const matrix = node.getWorldMatrix(); - if (!matrix.isIdentity()) { - return false; + public readonly convertToRightHanded: boolean; + + public readonly wasAddedByNoopNode: boolean; + + // 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 setIndicesAccessor(indices: Nullable, start: number, count: number, offset: number, flip: boolean, accessorIndex: number): void { + let map1 = this._indicesAccessorMap.get(indices); + if (!map1) { + map1 = new Map>>>(); + this._indicesAccessorMap.set(indices, map1); } - } else { - const matrix = node.getWorldMatrix().multiplyToRef(convertHandednessMatrix, TmpVectors.Matrix[0]); - if (!matrix.isIdentity()) { - return false; + + let map2 = map1.get(start); + if (!map2) { + map2 = new Map>>(); + map1.set(start, map2); + } + + let map3 = map2.get(count); + if (!map3) { + 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); } - // Geometry - if ((node instanceof Mesh && node.geometry) || (node instanceof InstancedMesh && node.sourceMesh.geometry)) { - return false; + public pushExportedNode(node: Node) { + if (!this._exportedNodes.has(node)) { + this._exportedNodes.add(node); + } } - return true; -} + public getNodesSet(): Set { + return this._exportedNodes; + } -function convertNodeHandedness(node: INode): void { - const translation = Vector3.FromArrayToRef(node.translation || [0, 0, 0], 0, TmpVectors.Vector3[0]); - const rotation = Quaternion.FromArrayToRef(node.rotation || [0, 0, 0, 1], 0, TmpVectors.Quaternion[0]); - const scale = Vector3.FromArrayToRef(node.scale || [1, 1, 1], 0, TmpVectors.Vector3[1]); - const matrix = Matrix.ComposeToRef(scale, rotation, translation, TmpVectors.Matrix[0]).multiplyToRef(convertHandednessMatrix, TmpVectors.Matrix[0]); + public getVertexBufferView(buffer: Buffer): number | undefined { + return this._vertexBufferViewMap.get(buffer); + } - matrix.decompose(scale, rotation, translation); + public setVertexBufferView(buffer: Buffer, bufferViewIndex: number): void { + this._vertexBufferViewMap.set(buffer, bufferViewIndex); + } - if (translation.equalsToFloats(0, 0, 0)) { - delete node.translation; - } else { - node.translation = translation.asArray(); + public setRemappedBufferView(buffer: Buffer, vertexBuffer: VertexBuffer, bufferViewIndex: number) { + this._remappedBufferView.set(buffer, new Map()); + this._remappedBufferView.get(buffer)!.set(vertexBuffer, bufferViewIndex); } - if (Quaternion.IsIdentity(rotation)) { - delete node.rotation; - } else { - node.rotation = rotation.asArray(); + public getRemappedBufferView(buffer: Buffer, vertexBuffer: VertexBuffer): number | undefined { + return this._remappedBufferView.get(buffer)?.get(vertexBuffer); } - if (scale.equalsToFloats(1, 1, 1)) { - delete node.scale; - } else { - node.scale = scale.asArray(); + public getVertexAccessor(vertexBuffer: VertexBuffer, start: number, count: number): number | undefined { + return this._vertexAccessorMap.get(vertexBuffer)?.get(start)?.get(count); } -} -function getBinaryWriterFunc(binaryWriter: _BinaryWriter, attributeComponentKind: AccessorComponentType): Nullable<(entry: number, byteOffset?: number) => void> { - switch (attributeComponentKind) { - case AccessorComponentType.UNSIGNED_BYTE: { - return binaryWriter.setUInt8.bind(binaryWriter); - } - case AccessorComponentType.UNSIGNED_SHORT: { - return binaryWriter.setUInt16.bind(binaryWriter); - } - case AccessorComponentType.UNSIGNED_INT: { - return binaryWriter.setUInt32.bind(binaryWriter); - } - case AccessorComponentType.FLOAT: { - return binaryWriter.setFloat32.bind(binaryWriter); + public setVertexAccessor(vertexBuffer: VertexBuffer, start: number, count: number, accessorIndex: number): void { + let map1 = this._vertexAccessorMap.get(vertexBuffer); + if (!map1) { + map1 = new Map>(); + this._vertexAccessorMap.set(vertexBuffer, map1); } - default: { - Tools.Warn("Unsupported Attribute Component kind: " + attributeComponentKind); - return null; + + let map2 = map1.get(start); + if (!map2) { + map2 = new Map(); + map1.set(start, map2); } + + map2.set(count, accessorIndex); } -} -/** - * Utility interface for storing vertex attribute data - * @internal - */ -// eslint-disable-next-line @typescript-eslint/naming-convention -interface _IVertexAttributeData { - /** - * Specifies the Babylon Vertex Buffer Type (Position, Normal, Color, etc.) - */ - kind: string; + public hasVertexColorAlpha(vertexBuffer: VertexBuffer): boolean { + return this._vertexMapColorAlpha.get(vertexBuffer) || false; + } - /** - * Specifies the glTF Accessor Type (VEC2, VEC3, etc.) - */ - accessorType: AccessorType; + public setHasVertexColorAlpha(vertexBuffer: VertexBuffer, hasAlpha: boolean) { + return this._vertexMapColorAlpha.set(vertexBuffer, hasAlpha); + } - /** - * Specifies the glTF Accessor Component Type (BYTE, UNSIGNED_BYTE, FLOAT, SHORT, INT, etc..) - */ - accessorComponentType: AccessorComponentType; + public getMesh(mesh: Mesh): number | undefined { + return this._meshMap.get(mesh); + } - /** - * Specifies the BufferView index for the vertex attribute data - */ - bufferViewIndex?: number; + public setMesh(mesh: Mesh, meshIndex: number): void { + this._meshMap.set(mesh, meshIndex); + } - /** - * Specifies the number of bytes per attribute element (e.g. position would be 3 floats (x/y/z) where each float is 4 bytes, so a 12 byte stride) - */ - byteStride?: number; + public bindMorphDataToMesh(mesh: Mesh, morphData: IMorphTargetData) { + const morphTargets = this._meshMorphTargetMap.get(mesh) || []; + this._meshMorphTargetMap.set(mesh, morphTargets); + if (morphTargets.indexOf(morphData) === -1) { + morphTargets.push(morphData); + } + } - /** - * Specifies information about each morph target associated with this attribute - */ - morphTargetInfo?: Readonly<{ bufferViewIndex: number; vertexCount: number; accessorType: AccessorType; minMax?: { min: Vector3; max: Vector3 } }>[]; + public getMorphTargetsFromMesh(mesh: Mesh): IMorphTargetData[] | undefined { + return this._meshMorphTargetMap.get(mesh); + } } -/** - * Converts Babylon Scene into glTF 2.0. - * @internal - */ -export class _Exporter { - /** - * Stores the glTF to export - */ - public _glTF: IGLTF; - /** - * Stores all generated buffer views, which represents views into the main glTF buffer data - */ - public _bufferViews: IBufferView[]; - /** - * Stores all the generated accessors, which is used for accessing the data within the buffer views in glTF - */ - public _accessors: IAccessor[]; - /** - * Stores all the generated nodes, which contains transform and/or mesh information per node - */ - public _nodes: INode[]; - /** - * Stores all the generated glTF scenes, which stores multiple node hierarchies - */ - private _scenes: IScene[]; - /** - * Stores all the generated glTF cameras - */ - private _cameras: ICamera[]; - /** - * Stores all the generated mesh information, each containing a set of primitives to render in glTF - */ - private _meshes: IMesh[]; - /** - * Stores all the generated material information, which represents the appearance of each primitive - */ - public _materials: IMaterial[]; +/** @internal */ +export class GLTFExporter { + public readonly _glTF: IGLTF = { + asset: { generator: `Babylon.js v${Engine.Version}`, version: "2.0" }, + }; + + public readonly _animations: IAnimation[] = []; + public readonly _accessors: IAccessor[] = []; + public readonly _bufferViews: IBufferView[] = []; + public readonly _cameras: ICamera[] = []; + public readonly _images: IImage[] = []; + public readonly _materials: IMaterial[] = []; + public readonly _meshes: IMesh[] = []; + public readonly _nodes: INode[] = []; + public readonly _samplers: ISampler[] = []; + public readonly _scenes: IScene[] = []; + public readonly _skins: ISkin[] = []; + public readonly _textures: ITexture[] = []; + + public readonly _babylonScene: Scene; + public readonly _imageData: { [fileName: string]: { data: ArrayBuffer; mimeType: ImageMimeType } } = {}; + private readonly _orderedImageData: Array<{ data: ArrayBuffer; mimeType: ImageMimeType }> = []; - public _materialMap: { [materialID: number]: number }; - /** - * Stores all the generated texture information, which is referenced by glTF materials - */ - public _textures: ITexture[]; /** - * Stores all the generated image information, which is referenced by glTF textures + * Baked animation sample rate */ - public _images: IImage[]; + private _animationSampleRate: number; - /** - * Stores all the texture samplers - */ - public _samplers: ISampler[]; - /** - * Stores all the generated glTF skins - */ - public _skins: ISkin[]; - /** - * Stores all the generated animation samplers, which is referenced by glTF animations - */ - /** - * Stores the animations for glTF models - */ - private _animations: IAnimation[]; - /** - * Stores the total amount of bytes stored in the glTF buffer - */ - private _totalByteLength: number; - /** - * Stores a reference to the Babylon scene containing the source geometry and material information - */ - public _babylonScene: Scene; - /** - * Stores a map of the image data, where the key is the file name and the value - * is the image data - */ - public _imageData: { [fileName: string]: { data: ArrayBuffer; mimeType: ImageMimeType } }; + private readonly _options: Required; - private _orderedImageData: Array<{ data: ArrayBuffer; mimeType: ImageMimeType }>; + public readonly _materialExporter = new GLTFMaterialExporter(this); - /** - * Stores a map of the unique id of a node to its index in the node array - */ - private _nodeMap: { [key: number]: number }; + private readonly _extensions: { [name: string]: IGLTFExporterExtensionV2 } = {}; - /** - * Baked animation sample rate - */ - private _animationSampleRate: number; + private readonly _dataWriter = new DataWriter(4); - private _options: IExportOptions; + private readonly _shouldExportNodeMap = new Map(); - private _localEngine: Engine; + // Babylon node -> glTF node index + private readonly _nodeMap = new Map(); - public _glTFMaterialExporter: _GLTFMaterialExporter; + // Babylon material -> glTF material index + public readonly _materialMap = new Map(); + private readonly _camerasMap = new Map(); + private readonly _nodesCameraMap = new Map(); + private readonly _skinMap = new Map(); + private readonly _nodesSkinMap = new Map(); - private _extensions: { [name: string]: IGLTFExporterExtensionV2 } = {}; + // A material in this set requires UVs + public readonly _materialNeedsUVsSet = new Set(); - private static _ExtensionNames = new Array(); - private static _ExtensionFactories: { [name: string]: (exporter: _Exporter) => IGLTFExporterExtensionV2 } = {}; + private static readonly _ExtensionNames = new Array(); + private static readonly _ExtensionFactories: { [name: string]: (exporter: GLTFExporter) => IGLTFExporterExtensionV2 } = {}; private _applyExtension( - node: Nullable, + node: T, extensions: IGLTFExporterExtensionV2[], index: number, - actionAsync: (extension: IGLTFExporterExtensionV2, node: Nullable) => Promise> | undefined + actionAsync: (extension: IGLTFExporterExtensionV2, node: T) => Promise> | undefined ): Promise> { if (index >= extensions.length) { return Promise.resolve(node); @@ -281,55 +285,44 @@ export class _Exporter { return this._applyExtension(node, extensions, index + 1, actionAsync); } - return currentPromise.then((newNode) => this._applyExtension(newNode, extensions, index + 1, actionAsync)); + return currentPromise.then((newNode) => (newNode ? this._applyExtension(newNode, extensions, index + 1, actionAsync) : null)); } - private _applyExtensions( - node: Nullable, - actionAsync: (extension: IGLTFExporterExtensionV2, node: Nullable) => Promise> | undefined - ): Promise> { + private _applyExtensions(node: T, actionAsync: (extension: IGLTFExporterExtensionV2, node: T) => Promise> | undefined): Promise> { const extensions: IGLTFExporterExtensionV2[] = []; - for (const name of _Exporter._ExtensionNames) { + for (const name of GLTFExporter._ExtensionNames) { extensions.push(this._extensions[name]); } return this._applyExtension(node, extensions, 0, actionAsync); } - public _extensionsPreExportTextureAsync(context: string, babylonTexture: Nullable, mimeType: ImageMimeType): Promise> { + public _extensionsPreExportTextureAsync(context: string, babylonTexture: Texture, mimeType: ImageMimeType): Promise> { return this._applyExtensions(babylonTexture, (extension, node) => extension.preExportTextureAsync && extension.preExportTextureAsync(context, node, mimeType)); } - public _extensionsPostExportMeshPrimitiveAsync( - context: string, - meshPrimitive: IMeshPrimitive, - babylonSubMesh: SubMesh, - binaryWriter: _BinaryWriter - ): Promise> { + public _extensionsPostExportMeshPrimitiveAsync(context: string, meshPrimitive: IMeshPrimitive, babylonSubMesh: SubMesh): Promise> { return this._applyExtensions( meshPrimitive, - (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(context, node, babylonSubMesh, binaryWriter) + (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(context, node, babylonSubMesh) ); } - public _extensionsPostExportNodeAsync( - context: string, - node: Nullable, - babylonNode: Node, - nodeMap: { [key: number]: number }, - binaryWriter: _BinaryWriter - ): Promise> { - return this._applyExtensions(node, (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap, binaryWriter)); + 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) + ); } - public _extensionsPostExportMaterialAsync(context: string, material: Nullable, babylonMaterial: Material): Promise> { + public _extensionsPostExportMaterialAsync(context: string, material: IMaterial, babylonMaterial: Material): Promise> { return this._applyExtensions(material, (extension, node) => extension.postExportMaterialAsync && extension.postExportMaterialAsync(context, node, babylonMaterial)); } public _extensionsPostExportMaterialAdditionalTextures(context: string, material: IMaterial, babylonMaterial: Material): BaseTexture[] { const output: BaseTexture[] = []; - for (const name of _Exporter._ExtensionNames) { + for (const name of GLTFExporter._ExtensionNames) { const extension = this._extensions[name]; if (extension.postExportMaterialAdditionalTextures) { @@ -341,7 +334,7 @@ export class _Exporter { } public _extensionsPostExportTextures(context: string, textureInfo: ITextureInfo, babylonTexture: BaseTexture): void { - for (const name of _Exporter._ExtensionNames) { + for (const name of GLTFExporter._ExtensionNames) { const extension = this._extensions[name]; if (extension.postExportTexture) { @@ -351,7 +344,7 @@ export class _Exporter { } private _forEachExtensions(action: (extension: IGLTFExporterExtensionV2) => void): void { - for (const name of _Exporter._ExtensionNames) { + for (const name of GLTFExporter._ExtensionNames) { const extension = this._extensions[name]; if (extension.enabled) { action(extension); @@ -362,27 +355,19 @@ export class _Exporter { private _extensionsOnExporting(): void { this._forEachExtensions((extension) => { if (extension.wasUsed) { - if (this._glTF.extensionsUsed == null) { - this._glTF.extensionsUsed = []; - } - + this._glTF.extensionsUsed ||= []; if (this._glTF.extensionsUsed.indexOf(extension.name) === -1) { this._glTF.extensionsUsed.push(extension.name); } if (extension.required) { - if (this._glTF.extensionsRequired == null) { - this._glTF.extensionsRequired = []; - } + this._glTF.extensionsRequired ||= []; if (this._glTF.extensionsRequired.indexOf(extension.name) === -1) { this._glTF.extensionsRequired.push(extension.name); } } - if (this._glTF.extensions == null) { - this._glTF.extensions = {}; - } - + this._glTF.extensions ||= {}; if (extension.onExporting) { extension.onExporting(); } @@ -390,56 +375,38 @@ export class _Exporter { }); } - /** - * Load glTF serializer extensions - */ private _loadExtensions(): void { - for (const name of _Exporter._ExtensionNames) { - const extension = _Exporter._ExtensionFactories[name](this); + for (const name of GLTFExporter._ExtensionNames) { + const extension = GLTFExporter._ExtensionFactories[name](this); this._extensions[name] = extension; } } - /** - * Creates a glTF Exporter instance, which can accept optional exporter options - * @param babylonScene Babylon scene object - * @param options Options to modify the behavior of the exporter - */ - public constructor(babylonScene?: Nullable, options?: IExportOptions) { - this._glTF = { - asset: { generator: `Babylon.js v${Engine.Version}`, version: "2.0" }, - }; - babylonScene = babylonScene || EngineStore.LastCreatedScene; + public constructor(babylonScene: Nullable = EngineStore.LastCreatedScene, options?: IExportOptions) { if (!babylonScene) { - return; + throw new Error("No scene available to export"); } + this._babylonScene = babylonScene; - this._bufferViews = []; - this._accessors = []; - this._meshes = []; - this._scenes = []; - this._cameras = []; - this._nodes = []; - this._images = []; - this._materials = []; - this._materialMap = []; - this._textures = []; - this._samplers = []; - this._skins = []; - this._animations = []; - this._imageData = {}; - this._orderedImageData = []; - this._options = options || {}; - this._animationSampleRate = this._options.animationSampleRate || 1 / 60; - - this._glTFMaterialExporter = new _GLTFMaterialExporter(this); + + this._options = { + shouldExportNode: () => true, + shouldExportAnimation: () => true, + metadataSelector: (metadata) => metadata, + animationSampleRate: 1 / 60, + exportWithoutWaitingForScene: false, + exportUnusedUVs: false, + removeNoopRootNodes: true, + includeCoordinateSystemConversionNodes: false, + ...options, + }; + this._loadExtensions(); } public dispose() { - for (const extensionKey in this._extensions) { - const extension = this._extensions[extensionKey]; - + for (const key in this._extensions) { + const extension = this._extensions[key]; extension.dispose(); } } @@ -448,677 +415,133 @@ export class _Exporter { return this._options; } - /** - * Registers a glTF exporter extension - * @param name Name of the extension to export - * @param factory The factory function that creates the exporter extension - */ - public static RegisterExtension(name: string, factory: (exporter: _Exporter) => IGLTFExporterExtensionV2): void { - if (_Exporter.UnregisterExtension(name)) { + public static RegisterExtension(name: string, factory: (exporter: GLTFExporter) => IGLTFExporterExtensionV2): void { + if (GLTFExporter.UnregisterExtension(name)) { Tools.Warn(`Extension with the name ${name} already exists`); } - _Exporter._ExtensionFactories[name] = factory; - _Exporter._ExtensionNames.push(name); + GLTFExporter._ExtensionFactories[name] = factory; + GLTFExporter._ExtensionNames.push(name); } - /** - * Un-registers an exporter extension - * @param name The name fo the exporter extension - * @returns A boolean indicating whether the extension has been un-registered - */ public static UnregisterExtension(name: string): boolean { - if (!_Exporter._ExtensionFactories[name]) { + if (!GLTFExporter._ExtensionFactories[name]) { return false; } - delete _Exporter._ExtensionFactories[name]; + delete GLTFExporter._ExtensionFactories[name]; - const index = _Exporter._ExtensionNames.indexOf(name); + const index = GLTFExporter._ExtensionNames.indexOf(name); if (index !== -1) { - _Exporter._ExtensionNames.splice(index, 1); + GLTFExporter._ExtensionNames.splice(index, 1); } return true; } - private _reorderIndicesBasedOnPrimitiveMode(submesh: SubMesh, primitiveMode: number, babylonIndices: IndicesArray, byteOffset: number, binaryWriter: _BinaryWriter) { - switch (primitiveMode) { - case Material.TriangleFillMode: { - if (!byteOffset) { - byteOffset = 0; - } - for (let i = submesh.indexStart, length = submesh.indexStart + submesh.indexCount; i < length; i = i + 3) { - const index = byteOffset + i * 4; - // swap the second and third indices - const secondIndex = binaryWriter.getUInt32(index + 4); - const thirdIndex = binaryWriter.getUInt32(index + 8); - binaryWriter.setUInt32(thirdIndex, index + 4); - binaryWriter.setUInt32(secondIndex, index + 8); - } - break; - } - case Material.TriangleFanDrawMode: { - for (let i = submesh.indexStart + submesh.indexCount - 1, start = submesh.indexStart; i >= start; --i) { - binaryWriter.setUInt32(babylonIndices[i], byteOffset); - byteOffset += 4; - } - break; - } - case Material.TriangleStripDrawMode: { - if (submesh.indexCount >= 3) { - binaryWriter.setUInt32(babylonIndices[submesh.indexStart + 2], byteOffset + 4); - binaryWriter.setUInt32(babylonIndices[submesh.indexStart + 1], byteOffset + 8); - } - break; - } - } - } + private _generateJSON(shouldUseGlb: boolean, 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; - /** - * Reorders the vertex attribute data based on the primitive mode. This is necessary when indices are not available and the winding order is - * clock-wise during export to glTF - * @param submesh BabylonJS submesh - * @param primitiveMode Primitive mode of the mesh - * @param vertexBufferKind The type of vertex attribute - * @param meshAttributeArray The vertex attribute data - * @param byteOffset The offset to the binary data - * @param binaryWriter The binary data for the glTF file - */ - private _reorderVertexAttributeDataBasedOnPrimitiveMode( - submesh: SubMesh, - primitiveMode: number, - vertexBufferKind: string, - meshAttributeArray: FloatArray, - byteOffset: number, - binaryWriter: _BinaryWriter - ): void { - switch (primitiveMode) { - case Material.TriangleFillMode: { - this._reorderTriangleFillMode(submesh, vertexBufferKind, meshAttributeArray, byteOffset, binaryWriter); - break; - } - case Material.TriangleStripDrawMode: { - this._reorderTriangleStripDrawMode(submesh, vertexBufferKind, meshAttributeArray, byteOffset, binaryWriter); - break; - } - case Material.TriangleFanDrawMode: { - this._reorderTriangleFanMode(submesh, vertexBufferKind, meshAttributeArray, byteOffset, binaryWriter); - break; - } + if (buffer.byteLength) { + this._glTF.buffers = [buffer]; } - } - - /** - * Reorders the vertex attributes in the correct triangle mode order . This is necessary when indices are not available and the winding order is - * clock-wise during export to glTF - * @param submesh BabylonJS submesh - * @param vertexBufferKind The type of vertex attribute - * @param meshAttributeArray The vertex attribute data - * @param byteOffset The offset to the binary data - * @param binaryWriter The binary data for the glTF file - */ - private _reorderTriangleFillMode(submesh: SubMesh, vertexBufferKind: string, meshAttributeArray: FloatArray, byteOffset: number, binaryWriter: _BinaryWriter) { - const vertexBuffer = this._getVertexBufferFromMesh(vertexBufferKind, submesh.getMesh() as Mesh); - if (vertexBuffer) { - const stride = vertexBuffer.byteStride / VertexBuffer.GetTypeByteLength(vertexBuffer.type); - if (submesh.verticesCount % 3 !== 0) { - Tools.Error("The submesh vertices for the triangle fill mode is not divisible by 3!"); - } else { - const vertexData: Vector2[] | Vector3[] | Vector4[] = []; - let index = 0; - switch (vertexBufferKind) { - case VertexBuffer.PositionKind: - case VertexBuffer.NormalKind: { - for (let x = submesh.verticesStart; x < submesh.verticesStart + submesh.verticesCount; x = x + 3) { - index = x * stride; - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index)); - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index + 2 * stride)); - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index + stride)); - } - break; - } - case VertexBuffer.TangentKind: { - for (let x = submesh.verticesStart; x < submesh.verticesStart + submesh.verticesCount; x = x + 3) { - index = x * stride; - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index)); - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index + 2 * stride)); - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index + stride)); - } - break; - } - case VertexBuffer.ColorKind: { - const size = vertexBuffer.getSize(); - for (let x = submesh.verticesStart; x < submesh.verticesStart + submesh.verticesCount; x = x + size) { - index = x * stride; - if (size === 4) { - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index)); - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index + 2 * stride)); - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index + stride)); - } else { - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index)); - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index + 2 * stride)); - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index + stride)); - } - } - break; - } - case VertexBuffer.UVKind: - case VertexBuffer.UV2Kind: { - for (let x = submesh.verticesStart; x < submesh.verticesStart + submesh.verticesCount; x = x + 3) { - index = x * stride; - (vertexData as Vector2[]).push(Vector2.FromArray(meshAttributeArray, index)); - (vertexData as Vector2[]).push(Vector2.FromArray(meshAttributeArray, index + 2 * stride)); - (vertexData as Vector2[]).push(Vector2.FromArray(meshAttributeArray, index + stride)); - } - break; - } - default: { - Tools.Error(`Unsupported Vertex Buffer type: ${vertexBufferKind}`); - } - } - this._writeVertexAttributeData(vertexData, byteOffset, vertexBufferKind, binaryWriter); - } - } else { - Tools.Warn(`reorderTriangleFillMode: Vertex Buffer Kind ${vertexBufferKind} not present!`); + if (this._nodes && this._nodes.length) { + this._glTF.nodes = this._nodes; } - } + if (this._meshes && this._meshes.length) { + this._glTF.meshes = this._meshes; + } + if (this._scenes && this._scenes.length) { + this._glTF.scenes = this._scenes; + this._glTF.scene = 0; + } + if (this._cameras && this._cameras.length) { + this._glTF.cameras = this._cameras; + } + if (this._bufferViews && this._bufferViews.length) { + this._glTF.bufferViews = this._bufferViews; + } + if (this._accessors && this._accessors.length) { + this._glTF.accessors = this._accessors; + } + if (this._animations && this._animations.length) { + this._glTF.animations = this._animations; + } + if (this._materials && this._materials.length) { + this._glTF.materials = this._materials; + } + if (this._textures && this._textures.length) { + this._glTF.textures = this._textures; + } + if (this._samplers && this._samplers.length) { + this._glTF.samplers = this._samplers; + } + if (this._skins && this._skins.length) { + this._glTF.skins = this._skins; + } + if (this._images && this._images.length) { + if (!shouldUseGlb) { + this._glTF.images = this._images; + } else { + this._glTF.images = []; - /** - * Reorders the vertex attributes in the correct triangle strip order. This is necessary when indices are not available and the winding order is - * clock-wise during export to glTF - * @param submesh BabylonJS submesh - * @param vertexBufferKind The type of vertex attribute - * @param meshAttributeArray The vertex attribute data - * @param byteOffset The offset to the binary data - * @param binaryWriter The binary data for the glTF file - */ - private _reorderTriangleStripDrawMode(submesh: SubMesh, vertexBufferKind: string, meshAttributeArray: FloatArray, byteOffset: number, binaryWriter: _BinaryWriter) { - const vertexBuffer = this._getVertexBufferFromMesh(vertexBufferKind, submesh.getMesh() as Mesh); - if (vertexBuffer) { - const stride = vertexBuffer.byteStride / VertexBuffer.GetTypeByteLength(vertexBuffer.type); - - const vertexData: Vector2[] | Vector3[] | Vector4[] = []; - let index = 0; - switch (vertexBufferKind) { - case VertexBuffer.PositionKind: - case VertexBuffer.NormalKind: { - index = submesh.verticesStart; - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index + 2 * stride)); - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index + stride)); - break; - } - case VertexBuffer.TangentKind: { - for (let x = submesh.verticesStart + submesh.verticesCount - 1; x >= submesh.verticesStart; --x) { - index = x * stride; - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index)); - } - break; - } - case VertexBuffer.ColorKind: { - for (let x = submesh.verticesStart + submesh.verticesCount - 1; x >= submesh.verticesStart; --x) { - index = x * stride; - vertexBuffer.getSize() === 4 - ? (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index)) - : (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index)); - } - break; - } - case VertexBuffer.UVKind: - case VertexBuffer.UV2Kind: { - for (let x = submesh.verticesStart + submesh.verticesCount - 1; x >= submesh.verticesStart; --x) { - index = x * stride; - (vertexData as Vector2[]).push(Vector2.FromArray(meshAttributeArray, index)); + 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); } - break; - } - default: { - Tools.Error(`Unsupported Vertex Buffer type: ${vertexBufferKind}`); - } + }); + + // Replace uri with bufferview and mime type for glb + buffer.byteLength = byteOffset; } - this._writeVertexAttributeData(vertexData, byteOffset + 12, vertexBufferKind, binaryWriter); - } else { - Tools.Warn(`reorderTriangleStripDrawMode: Vertex buffer kind ${vertexBufferKind} not present!`); } - } - /** - * Reorders the vertex attributes in the correct triangle fan order. This is necessary when indices are not available and the winding order is - * clock-wise during export to glTF - * @param submesh BabylonJS submesh - * @param vertexBufferKind The type of vertex attribute - * @param meshAttributeArray The vertex attribute data - * @param byteOffset The offset to the binary data - * @param binaryWriter The binary data for the glTF file - */ - private _reorderTriangleFanMode(submesh: SubMesh, vertexBufferKind: string, meshAttributeArray: FloatArray, byteOffset: number, binaryWriter: _BinaryWriter) { - const vertexBuffer = this._getVertexBufferFromMesh(vertexBufferKind, submesh.getMesh() as Mesh); - if (vertexBuffer) { - const stride = vertexBuffer.byteStride / VertexBuffer.GetTypeByteLength(vertexBuffer.type); - - const vertexData: Vector2[] | Vector3[] | Vector4[] = []; - let index = 0; - switch (vertexBufferKind) { - case VertexBuffer.PositionKind: - case VertexBuffer.NormalKind: { - for (let x = submesh.verticesStart + submesh.verticesCount - 1; x >= submesh.verticesStart; --x) { - index = x * stride; - (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index)); - } - break; - } - case VertexBuffer.TangentKind: { - for (let x = submesh.verticesStart + submesh.verticesCount - 1; x >= submesh.verticesStart; --x) { - index = x * stride; - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index)); - } - break; - } - case VertexBuffer.ColorKind: { - for (let x = submesh.verticesStart + submesh.verticesCount - 1; x >= submesh.verticesStart; --x) { - index = x * stride; - (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index)); - vertexBuffer.getSize() === 4 - ? (vertexData as Vector4[]).push(Vector4.FromArray(meshAttributeArray, index)) - : (vertexData as Vector3[]).push(Vector3.FromArray(meshAttributeArray, index)); - } - break; - } - case VertexBuffer.UVKind: - case VertexBuffer.UV2Kind: { - for (let x = submesh.verticesStart + submesh.verticesCount - 1; x >= submesh.verticesStart; --x) { - index = x * stride; - (vertexData as Vector2[]).push(Vector2.FromArray(meshAttributeArray, index)); - } - break; - } - default: { - Tools.Error(`Unsupported Vertex Buffer type: ${vertexBufferKind}`); - } - } - this._writeVertexAttributeData(vertexData, byteOffset, vertexBufferKind, binaryWriter); - } else { - Tools.Warn(`reorderTriangleFanMode: Vertex buffer kind ${vertexBufferKind} not present!`); + if (!shouldUseGlb) { + buffer.uri = fileName + ".bin"; } + + return prettyPrint ? JSON.stringify(this._glTF, null, 2) : JSON.stringify(this._glTF); } - /** - * Writes the vertex attribute data to binary - * @param vertices The vertices to write to the binary writer - * @param byteOffset The offset into the binary writer to overwrite binary data - * @param vertexAttributeKind The vertex attribute type - * @param binaryWriter The writer containing the binary data - */ - private _writeVertexAttributeData(vertices: Vector2[] | Vector3[] | Vector4[], byteOffset: number, vertexAttributeKind: string, binaryWriter: _BinaryWriter) { - for (const vertex of vertices) { - if (vertexAttributeKind === VertexBuffer.NormalKind) { - vertex.normalize(); - } else if (vertexAttributeKind === VertexBuffer.TangentKind && vertex instanceof Vector4) { - _GLTFUtilities._NormalizeTangentFromRef(vertex); - } + public async generateGLTFAsync(glTFPrefix: string): Promise { + const binaryBuffer = await this._generateBinaryAsync(); + + this._extensionsOnExporting(); + const jsonText = this._generateJSON(false, binaryBuffer.byteLength, glTFPrefix, true); + const bin = new Blob([binaryBuffer], { type: "application/octet-stream" }); + + const glTFFileName = glTFPrefix + ".gltf"; + const glTFBinFile = glTFPrefix + ".bin"; - for (const component of vertex.asArray()) { - binaryWriter.setFloat32(component, byteOffset); - byteOffset += 4; + const container = new GLTFData(); + + container.files[glTFFileName] = jsonText; + container.files[glTFBinFile] = bin; + + if (this._imageData) { + for (const image in this._imageData) { + container.files[image] = new Blob([this._imageData[image].data], { type: this._imageData[image].mimeType }); } } + + return container; } - /** - * Writes mesh attribute data to a data buffer - * Returns the bytelength of the data - * @param vertexBufferKind Indicates what kind of vertex data is being passed in - * @param attributeComponentKind - * @param meshAttributeArray Array containing the attribute data - * @param stride Specifies the space between data - * @param binaryWriter The buffer to write the binary data to - * @param babylonTransformNode - */ - public _writeAttributeData( - vertexBufferKind: string, - attributeComponentKind: AccessorComponentType, - meshAttributeArray: FloatArray, - stride: number, - binaryWriter: _BinaryWriter, - babylonTransformNode: TransformNode - ) { - let vertexAttributes: number[][] = []; - let index: number; - - switch (vertexBufferKind) { - case VertexBuffer.PositionKind: { - for (let k = 0, length = meshAttributeArray.length / stride; k < length; ++k) { - index = k * stride; - const vertexData = Vector3.FromArray(meshAttributeArray, index); - vertexAttributes.push(vertexData.asArray()); - } - break; - } - case VertexBuffer.NormalKind: { - for (let k = 0, length = meshAttributeArray.length / stride; k < length; ++k) { - index = k * stride; - const vertexData = Vector3.FromArray(meshAttributeArray, index); - vertexAttributes.push(vertexData.normalize().asArray()); - } - break; - } - case VertexBuffer.TangentKind: { - for (let k = 0, length = meshAttributeArray.length / stride; k < length; ++k) { - index = k * stride; - const vertexData = Vector4.FromArray(meshAttributeArray, index); - _GLTFUtilities._NormalizeTangentFromRef(vertexData); - vertexAttributes.push(vertexData.asArray()); - } - break; - } - case VertexBuffer.ColorKind: { - const meshMaterial = (babylonTransformNode as Mesh).material; - const convertToLinear = meshMaterial ? meshMaterial.getClassName() === "StandardMaterial" : true; - const vertexData: Color3 | Color4 = stride === 3 ? new Color3() : new Color4(); - const useExactSrgbConversions = this._babylonScene.getEngine().useExactSrgbConversions; - for (let k = 0, length = meshAttributeArray.length / stride; k < length; ++k) { - index = k * stride; - if (stride === 3) { - Color3.FromArrayToRef(meshAttributeArray, index, vertexData as Color3); - if (convertToLinear) { - (vertexData as Color3).toLinearSpaceToRef(vertexData as Color3, useExactSrgbConversions); - } - } else { - Color4.FromArrayToRef(meshAttributeArray, index, vertexData as Color4); - if (convertToLinear) { - (vertexData as Color4).toLinearSpaceToRef(vertexData as Color4, useExactSrgbConversions); - } - } - vertexAttributes.push(vertexData.asArray()); - } - break; - } - case VertexBuffer.UVKind: - case VertexBuffer.UV2Kind: { - for (let k = 0, length = meshAttributeArray.length / stride; k < length; ++k) { - index = k * stride; - const vertexData = Vector2.FromArray(meshAttributeArray, index); - vertexAttributes.push(vertexData.asArray()); - } - break; - } - case VertexBuffer.MatricesIndicesKind: - case VertexBuffer.MatricesIndicesExtraKind: { - for (let k = 0, length = meshAttributeArray.length / stride; k < length; ++k) { - index = k * stride; - const vertexData = Vector4.FromArray(meshAttributeArray, index); - vertexAttributes.push(vertexData.asArray()); - } - break; - } - case VertexBuffer.MatricesWeightsKind: - case VertexBuffer.MatricesWeightsExtraKind: { - for (let k = 0, length = meshAttributeArray.length / stride; k < length; ++k) { - index = k * stride; - const vertexData = Vector4.FromArray(meshAttributeArray, index); - vertexAttributes.push(vertexData.asArray()); - } - break; - } - default: { - Tools.Warn("Unsupported Vertex Buffer Type: " + vertexBufferKind); - vertexAttributes = []; - } - } - - const writeBinaryFunc = getBinaryWriterFunc(binaryWriter, attributeComponentKind); - - if (writeBinaryFunc) { - for (const vertexAttribute of vertexAttributes) { - for (const component of vertexAttribute) { - writeBinaryFunc(component); - } - } - } - } - - private _createMorphTargetBufferViewKind( - vertexBufferKind: string, - accessorType: AccessorType, - attributeComponentKind: AccessorComponentType, - mesh: Mesh, - morphTarget: MorphTarget, - binaryWriter: _BinaryWriter, - byteStride: number - ): Nullable<{ bufferViewIndex: number; vertexCount: number; accessorType: AccessorType; minMax?: { min: Vector3; max: Vector3 } }> { - let vertexCount: number; - let minMax: { min: Vector3; max: Vector3 } | undefined; - const morphData: number[] = []; - const difference: Vector3 = TmpVectors.Vector3[0]; - - switch (vertexBufferKind) { - case VertexBuffer.PositionKind: { - const morphPositions = morphTarget.getPositions(); - if (!morphPositions) { - return null; - } - - const originalPositions = mesh.getVerticesData(VertexBuffer.PositionKind, undefined, undefined, true)!; - const vertexStart = 0; - const min = new Vector3(Infinity, Infinity, Infinity); - const max = new Vector3(-Infinity, -Infinity, -Infinity); - vertexCount = originalPositions.length / 3; - for (let i = vertexStart; i < vertexCount; ++i) { - const originalPosition = Vector3.FromArray(originalPositions, i * 3); - const morphPosition = Vector3.FromArray(morphPositions, i * 3); - morphPosition.subtractToRef(originalPosition, difference); - min.copyFromFloats(Math.min(difference.x, min.x), Math.min(difference.y, min.y), Math.min(difference.z, min.z)); - max.copyFromFloats(Math.max(difference.x, max.x), Math.max(difference.y, max.y), Math.max(difference.z, max.z)); - morphData.push(difference.x, difference.y, difference.z); - } - - minMax = { min, max }; - - break; - } - case VertexBuffer.NormalKind: { - const morphNormals = morphTarget.getNormals(); - if (!morphNormals) { - return null; - } - - const originalNormals = mesh.getVerticesData(VertexBuffer.NormalKind, undefined, undefined, true)!; - const vertexStart = 0; - vertexCount = originalNormals.length / 3; - 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); - morphData.push(difference.x, difference.y, difference.z); - } - - break; - } - case VertexBuffer.TangentKind: { - const morphTangents = morphTarget.getTangents(); - if (!morphTangents) { - return null; - } - - // Handedness cannot be displaced, so morph target tangents omit the w component - accessorType = AccessorType.VEC3; - byteStride = 12; // 3 components (x/y/z) * 4 bytes (float32) - - const originalTangents = mesh.getVerticesData(VertexBuffer.TangentKind, undefined, undefined, true)!; - const vertexStart = 0; - vertexCount = originalTangents.length / 4; - for (let i = vertexStart; i < vertexCount; ++i) { - // Only read the x, y, z components and ignore w - const originalTangent = Vector3.FromArray(originalTangents, i * 4); - _GLTFUtilities._NormalizeTangentFromRef(originalTangent); - - // Morph target tangents omit the w component so it won't be present in the data - const morphTangent = Vector3.FromArray(morphTangents, i * 3); - _GLTFUtilities._NormalizeTangentFromRef(morphTangent); - - morphTangent.subtractToRef(originalTangent, difference); - morphData.push(difference.x, difference.y, difference.z); - } - - break; - } - default: { - return null; - } - } - - const binaryWriterFunc = getBinaryWriterFunc(binaryWriter, attributeComponentKind); - if (!binaryWriterFunc) { - return null; - } - - const typeByteLength = VertexBuffer.GetTypeByteLength(attributeComponentKind); - const byteLength = morphData.length * typeByteLength; - const bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, byteStride, `${vertexBufferKind} - ${morphTarget.name} (Morph Target)`); - this._bufferViews.push(bufferView); - const bufferViewIndex = this._bufferViews.length - 1; - - for (const value of morphData) { - binaryWriterFunc(value); - } - - return { bufferViewIndex, vertexCount, accessorType, minMax }; - } - - /** - * Generates glTF json data - * @param shouldUseGlb Indicates whether the json should be written for a glb file - * @param glTFPrefix Text to use when prefixing a glTF file - * @param prettyPrint Indicates whether the json file should be pretty printed (true) or not (false) - * @returns json data as string - */ - private _generateJSON(shouldUseGlb: boolean, glTFPrefix?: string, prettyPrint?: boolean): string { - const buffer: IBuffer = { byteLength: this._totalByteLength }; - let imageName: string; - let imageData: { data: ArrayBuffer; mimeType: ImageMimeType }; - let bufferView: IBufferView; - let byteOffset: number = this._totalByteLength; - - if (buffer.byteLength) { - this._glTF.buffers = [buffer]; - } - if (this._nodes && this._nodes.length) { - this._glTF.nodes = this._nodes; - } - if (this._meshes && this._meshes.length) { - this._glTF.meshes = this._meshes; - } - if (this._scenes && this._scenes.length) { - this._glTF.scenes = this._scenes; - this._glTF.scene = 0; - } - if (this._cameras && this._cameras.length) { - this._glTF.cameras = this._cameras; - } - if (this._bufferViews && this._bufferViews.length) { - this._glTF.bufferViews = this._bufferViews; - } - if (this._accessors && this._accessors.length) { - this._glTF.accessors = this._accessors; - } - if (this._animations && this._animations.length) { - this._glTF.animations = this._animations; - } - if (this._materials && this._materials.length) { - this._glTF.materials = this._materials; - } - if (this._textures && this._textures.length) { - this._glTF.textures = this._textures; - } - if (this._samplers && this._samplers.length) { - this._glTF.samplers = this._samplers; - } - if (this._skins && this._skins.length) { - 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); - imageName = image.uri.split(".")[0] + " image"; - bufferView = _GLTFUtilities._CreateBufferView(0, byteOffset, imageData.data.byteLength, undefined, imageName); - byteOffset += imageData.data.byteLength; - this._bufferViews.push(bufferView); - image.bufferView = this._bufferViews.length - 1; - image.name = imageName; - image.mimeType = imageData.mimeType; - image.uri = undefined; - if (!this._glTF.images) { - this._glTF.images = []; - } - this._glTF.images.push(image); - } - }); - // Replace uri with bufferview and mime type for glb - buffer.byteLength = byteOffset; - } - } - - if (!shouldUseGlb) { - buffer.uri = glTFPrefix + ".bin"; - } - - const jsonText = prettyPrint ? JSON.stringify(this._glTF, null, 2) : JSON.stringify(this._glTF); - - return jsonText; - } - - /** - * Generates data for .gltf and .bin files based on the glTF prefix string - * @param glTFPrefix Text to use when prefixing a glTF file - * @param dispose Dispose the exporter - * @returns GLTFData with glTF file data - */ - public _generateGLTFAsync(glTFPrefix: string, dispose = true): Promise { - return this._generateBinaryAsync().then((binaryBuffer) => { - this._extensionsOnExporting(); - const jsonText = this._generateJSON(false, glTFPrefix, true); - const bin = new Blob([binaryBuffer], { type: "application/octet-stream" }); - - const glTFFileName = glTFPrefix + ".gltf"; - const glTFBinFile = glTFPrefix + ".bin"; - - const container = new GLTFData(); - - container.glTFFiles[glTFFileName] = jsonText; - container.glTFFiles[glTFBinFile] = bin; - - if (this._imageData) { - for (const image in this._imageData) { - container.glTFFiles[image] = new Blob([this._imageData[image].data], { type: this._imageData[image].mimeType }); - } - } - - if (dispose) { - this.dispose(); - } - - return container; - }); - } - - /** - * Creates a binary buffer for glTF - * @returns array buffer for binary data - */ - private _generateBinaryAsync(): Promise { - const binaryWriter = new _BinaryWriter(4); - return this._createSceneAsync(binaryWriter).then(() => { - if (this._localEngine) { - this._localEngine.dispose(); - } - return binaryWriter.getArrayBuffer(); - }); + private async _generateBinaryAsync(): Promise { + await this._exportSceneAsync(); + return this._dataWriter.getOutputData(); } /** @@ -1133,129 +556,119 @@ export class _Exporter { return padding; } - /** - * @internal - */ - public _generateGLBAsync(glTFPrefix: string, dispose = true): Promise { - return this._generateBinaryAsync().then((binaryBuffer) => { - this._extensionsOnExporting(); - const jsonText = this._generateJSON(true); - 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; - - //header - const headerBuffer = new ArrayBuffer(headerLength); - const headerBufferView = new DataView(headerBuffer); - headerBufferView.setUint32(0, 0x46546c67, true); //glTF - headerBufferView.setUint32(4, 2, true); // version - headerBufferView.setUint32(8, byteLength, true); // total bytes in file - - //json chunk - const jsonChunkBuffer = new ArrayBuffer(chunkLengthPrefix + jsonLength + jsonPadding); - const jsonChunkBufferView = new DataView(jsonChunkBuffer); - jsonChunkBufferView.setUint32(0, jsonLength + jsonPadding, true); - jsonChunkBufferView.setUint32(4, 0x4e4f534a, true); - - //json chunk bytes - const jsonData = new Uint8Array(jsonChunkBuffer, chunkLengthPrefix); - // if TextEncoder was available, we can simply copy the encoded array - if (encodedJsonText) { - jsonData.set(encodedJsonText); - } else { - const blankCharCode = "_".charCodeAt(0); - for (let i = 0; i < jsonLength; ++i) { - const charCode = jsonText.charCodeAt(i); - // if the character doesn't fit into a single UTF-16 code unit, just put a blank character - if (charCode != jsonText.codePointAt(i)) { - jsonData[i] = blankCharCode; - } else { - jsonData[i] = charCode; - } + public async generateGLBAsync(glTFPrefix: string): Promise { + const binaryBuffer = await this._generateBinaryAsync(); + + this._extensionsOnExporting(); + const jsonText = this._generateJSON(true, 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; + + // header + const headerBuffer = new ArrayBuffer(headerLength); + const headerBufferView = new DataView(headerBuffer); + headerBufferView.setUint32(0, 0x46546c67, true); //glTF + headerBufferView.setUint32(4, 2, true); // version + headerBufferView.setUint32(8, byteLength, true); // total bytes in file + + // json chunk + const jsonChunkBuffer = new ArrayBuffer(chunkLengthPrefix + jsonLength + jsonPadding); + const jsonChunkBufferView = new DataView(jsonChunkBuffer); + jsonChunkBufferView.setUint32(0, jsonLength + jsonPadding, true); + jsonChunkBufferView.setUint32(4, 0x4e4f534a, true); + + // json chunk bytes + const jsonData = new Uint8Array(jsonChunkBuffer, chunkLengthPrefix); + // if TextEncoder was available, we can simply copy the encoded array + if (encodedJsonText) { + jsonData.set(encodedJsonText); + } else { + const blankCharCode = "_".charCodeAt(0); + for (let i = 0; i < jsonLength; ++i) { + const charCode = jsonText.charCodeAt(i); + // if the character doesn't fit into a single UTF-16 code unit, just put a blank character + if (charCode != jsonText.codePointAt(i)) { + jsonData[i] = blankCharCode; + } else { + jsonData[i] = charCode; } } + } - //json padding - const jsonPaddingView = new Uint8Array(jsonChunkBuffer, chunkLengthPrefix + jsonLength); - for (let i = 0; i < jsonPadding; ++i) { - jsonPaddingView[i] = 0x20; - } - - //binary chunk - const binaryChunkBuffer = new ArrayBuffer(chunkLengthPrefix); - const binaryChunkBufferView = new DataView(binaryChunkBuffer); - binaryChunkBufferView.setUint32(0, binaryBuffer.byteLength + imageByteLength + imagePadding, true); - binaryChunkBufferView.setUint32(4, 0x004e4942, true); - - // binary padding - const binPaddingBuffer = new ArrayBuffer(binPadding); - const binPaddingView = new Uint8Array(binPaddingBuffer); - for (let i = 0; i < binPadding; ++i) { - binPaddingView[i] = 0; - } + // json padding + const jsonPaddingView = new Uint8Array(jsonChunkBuffer, chunkLengthPrefix + jsonLength); + for (let i = 0; i < jsonPadding; ++i) { + jsonPaddingView[i] = 0x20; + } - const imagePaddingBuffer = new ArrayBuffer(imagePadding); - const imagePaddingView = new Uint8Array(imagePaddingBuffer); - for (let i = 0; i < imagePadding; ++i) { - imagePaddingView[i] = 0; - } + // binary chunk + const binaryChunkBuffer = new ArrayBuffer(chunkLengthPrefix); + const binaryChunkBufferView = new DataView(binaryChunkBuffer); + binaryChunkBufferView.setUint32(0, binaryBuffer.byteLength + binPadding + imageByteLength + imagePadding, true); + binaryChunkBufferView.setUint32(4, 0x004e4942, true); - const glbData = [headerBuffer, jsonChunkBuffer, binaryChunkBuffer, binaryBuffer]; + // binary padding + const binPaddingBuffer = new ArrayBuffer(binPadding); + const binPaddingView = new Uint8Array(binPaddingBuffer); + for (let i = 0; i < binPadding; ++i) { + binPaddingView[i] = 0; + } - // binary data - for (let i = 0; i < this._orderedImageData.length; ++i) { - glbData.push(this._orderedImageData[i].data); - } + const imagePaddingBuffer = new ArrayBuffer(imagePadding); + const imagePaddingView = new Uint8Array(imagePaddingBuffer); + for (let i = 0; i < imagePadding; ++i) { + imagePaddingView[i] = 0; + } - glbData.push(binPaddingBuffer); + const glbData = [headerBuffer, jsonChunkBuffer, binaryChunkBuffer, binaryBuffer]; - glbData.push(imagePaddingBuffer); + // binary data + for (let i = 0; i < this._orderedImageData.length; ++i) { + glbData.push(this._orderedImageData[i].data); + } - const glbFile = new Blob(glbData, { type: "application/octet-stream" }); + glbData.push(binPaddingBuffer); - const container = new GLTFData(); - container.glTFFiles[glbFileName] = glbFile; + glbData.push(imagePaddingBuffer); - if (this._localEngine != null) { - this._localEngine.dispose(); - } + const glbFile = new Blob(glbData, { type: "application/octet-stream" }); - if (dispose) { - this.dispose(); - } + const container = new GLTFData(); + container.files[glbFileName] = glbFile; - return container; - }); + return container; } - /** - * Sets the TRS for each node - * @param node glTF Node for storing the transformation data - * @param babylonTransformNode Babylon mesh used as the source for the transformation data - */ - private _setNodeTransformation(node: INode, babylonTransformNode: TransformNode): void { + private _setNodeTransformation(node: INode, babylonTransformNode: TransformNode, convertToRightHanded: boolean): void { if (!babylonTransformNode.getPivotPoint().equalsToFloats(0, 0, 0)) { Tools.Warn("Pivot points are not supported in the glTF serializer"); } + if (!babylonTransformNode.position.equalsToFloats(0, 0, 0)) { - node.translation = babylonTransformNode.position.asArray(); + const translation = TmpVectors.Vector3[0].copyFrom(babylonTransformNode.position); + if (convertToRightHanded) { + ConvertToRightHandedPosition(translation); + } + + node.translation = translation.asArray(); } if (!babylonTransformNode.scaling.equalsToFloats(1, 1, 1)) { @@ -1267,639 +680,581 @@ export class _Exporter { rotationQuaternion.multiplyInPlace(babylonTransformNode.rotationQuaternion); } if (!Quaternion.IsIdentity(rotationQuaternion)) { + if (convertToRightHanded) { + ConvertToRightHandedRotation(rotationQuaternion); + } + node.rotation = rotationQuaternion.normalize().asArray(); } } - private _setCameraTransformation(node: INode, babylonCamera: Camera): void { + private _setCameraTransformation(node: INode, babylonCamera: Camera, convertToRightHanded: boolean, parent: Nullable): void { const translation = TmpVectors.Vector3[0]; const rotation = TmpVectors.Quaternion[0]; - babylonCamera.getWorldMatrix().decompose(undefined, rotation, translation); + + if (parent !== null) { + // Camera.getWorldMatrix returns global coordinates. GLTF node must use local coordinates. If camera has parent we need to use local translation/rotation. + const parentWorldMatrix = Matrix.Invert(parent.getWorldMatrix()); + const cameraWorldMatrix = babylonCamera.getWorldMatrix(); + const cameraLocal = cameraWorldMatrix.multiply(parentWorldMatrix); + cameraLocal.decompose(undefined, rotation, translation); + } else { + babylonCamera.getWorldMatrix().decompose(undefined, rotation, translation); + } if (!translation.equalsToFloats(0, 0, 0)) { node.translation = translation.asArray(); } - // // Rotation by 180 as glTF has a different convention than Babylon. - rotation.multiplyInPlace(rotation180Y); - if (!Quaternion.IsIdentity(rotation)) { node.rotation = rotation.asArray(); } } - private _getVertexBufferFromMesh(attributeKind: string, bufferMesh: Mesh): Nullable { - if (bufferMesh.isVerticesDataPresent(attributeKind, true)) { - const vertexBuffer = bufferMesh.getVertexBuffer(attributeKind, true); - if (vertexBuffer) { - return vertexBuffer; - } - } - return null; - } + // Export babylon cameras to glTF cameras + private _listAvailableCameras(): void { + for (const camera of this._babylonScene.cameras) { + const glTFCamera: ICamera = { + type: camera.mode === Camera.PERSPECTIVE_CAMERA ? CameraType.PERSPECTIVE : CameraType.ORTHOGRAPHIC, + }; - /** - * Creates a bufferview based on the vertices type for the Babylon mesh - * @param kind Indicates the type of vertices data - * @param attributeComponentKind Indicates the numerical type used to store the data - * @param babylonTransformNode The Babylon mesh to get the vertices data from - * @param binaryWriter The buffer to write the bufferview data to - * @param byteStride - */ - private _createBufferViewKind( - kind: string, - attributeComponentKind: AccessorComponentType, - babylonTransformNode: TransformNode, - binaryWriter: _BinaryWriter, - byteStride: number - ) { - const bufferMesh = - babylonTransformNode instanceof Mesh - ? (babylonTransformNode as Mesh) - : babylonTransformNode instanceof InstancedMesh - ? (babylonTransformNode as InstancedMesh).sourceMesh - : null; - - if (bufferMesh) { - const vertexBuffer = bufferMesh.getVertexBuffer(kind, true); - const vertexData = bufferMesh.getVerticesData(kind, undefined, undefined, true); - - if (vertexBuffer && vertexData) { - const typeByteLength = VertexBuffer.GetTypeByteLength(attributeComponentKind); - const byteLength = vertexData.length * typeByteLength; - const bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, byteStride, kind + " - " + bufferMesh.name); - this._bufferViews.push(bufferView); + if (camera.name) { + glTFCamera.name = camera.name; + } - this._writeAttributeData(kind, attributeComponentKind, vertexData, byteStride / typeByteLength, binaryWriter, babylonTransformNode); + if (glTFCamera.type === CameraType.PERSPECTIVE) { + glTFCamera.perspective = { + aspectRatio: camera.getEngine().getAspectRatio(camera), + yfov: camera.fovMode === Camera.FOVMODE_VERTICAL_FIXED ? camera.fov : camera.fov * camera.getEngine().getAspectRatio(camera), + znear: camera.minZ, + zfar: camera.maxZ, + }; + } else if (glTFCamera.type === CameraType.ORTHOGRAPHIC) { + const halfWidth = camera.orthoLeft && camera.orthoRight ? 0.5 * (camera.orthoRight - camera.orthoLeft) : camera.getEngine().getRenderWidth() * 0.5; + const halfHeight = camera.orthoBottom && camera.orthoTop ? 0.5 * (camera.orthoTop - camera.orthoBottom) : camera.getEngine().getRenderHeight() * 0.5; + glTFCamera.orthographic = { + xmag: halfWidth, + ymag: halfHeight, + znear: camera.minZ, + zfar: camera.maxZ, + }; } + this._camerasMap.set(camera, glTFCamera); } } - /** - * The primitive mode of the Babylon mesh - * @param babylonMesh The BabylonJS mesh - * @returns Unsigned integer of the primitive mode or null - */ - private _getMeshPrimitiveMode(babylonMesh: AbstractMesh): number { - if (babylonMesh instanceof LinesMesh) { - return Material.LineListDrawMode; - } - if (babylonMesh instanceof InstancedMesh || babylonMesh instanceof Mesh) { - const baseMesh = babylonMesh instanceof Mesh ? babylonMesh : babylonMesh.sourceMesh; - if (typeof baseMesh.overrideRenderingFillMode === "number") { - return baseMesh.overrideRenderingFillMode; + // Cleanup unused cameras and assign index to nodes. + private _exportAndAssignCameras(): void { + const gltfCameras = Array.from(this._camerasMap.values()); + for (const gltfCamera of gltfCameras) { + const usedNodes = this._nodesCameraMap.get(gltfCamera); + if (usedNodes !== undefined) { + this._cameras.push(gltfCamera); + for (const node of usedNodes) { + node.camera = this._cameras.length - 1; + } } } - return babylonMesh.material ? babylonMesh.material.fillMode : Material.TriangleFillMode; } - /** - * Sets the primitive mode of the glTF mesh primitive - * @param meshPrimitive glTF mesh primitive - * @param primitiveMode The primitive mode - */ - private _setPrimitiveMode(meshPrimitive: IMeshPrimitive, primitiveMode: number) { - switch (primitiveMode) { - case Material.TriangleFillMode: { - // glTF defaults to using Triangle Mode - break; - } - case Material.TriangleStripDrawMode: { - meshPrimitive.mode = MeshPrimitiveMode.TRIANGLE_STRIP; - break; - } - case Material.TriangleFanDrawMode: { - meshPrimitive.mode = MeshPrimitiveMode.TRIANGLE_FAN; - break; - } - case Material.PointListDrawMode: { - meshPrimitive.mode = MeshPrimitiveMode.POINTS; - break; - } - case Material.PointFillMode: { - meshPrimitive.mode = MeshPrimitiveMode.POINTS; - break; - } - case Material.LineLoopDrawMode: { - meshPrimitive.mode = MeshPrimitiveMode.LINE_LOOP; - break; - } - case Material.LineListDrawMode: { - meshPrimitive.mode = MeshPrimitiveMode.LINES; - break; - } - case Material.LineStripDrawMode: { - meshPrimitive.mode = MeshPrimitiveMode.LINE_STRIP; - break; + // Builds all skins in the skins array so nodes can reference it during node parsing. + private _listAvailableSkeletons(): void { + for (const skeleton of this._babylonScene.skeletons) { + if (skeleton.bones.length <= 0) { + continue; } + + const skin: ISkin = { joints: [] }; + this._skinMap.set(skeleton, skin); } } - /** - * Sets the vertex attribute accessor based of the glTF mesh primitive - * @param meshPrimitive glTF mesh primitive - * @param attributeKind vertex attribute - */ - private _setAttributeKind(attributes: { [name: string]: number }, attributeKind: string): void { - switch (attributeKind) { - case VertexBuffer.PositionKind: { - attributes.POSITION = this._accessors.length - 1; - break; - } - case VertexBuffer.NormalKind: { - attributes.NORMAL = this._accessors.length - 1; - break; - } - case VertexBuffer.ColorKind: { - attributes.COLOR_0 = this._accessors.length - 1; - break; - } - case VertexBuffer.TangentKind: { - attributes.TANGENT = this._accessors.length - 1; - break; - } - case VertexBuffer.UVKind: { - attributes.TEXCOORD_0 = this._accessors.length - 1; - break; - } - case VertexBuffer.UV2Kind: { - attributes.TEXCOORD_1 = this._accessors.length - 1; - break; - } - case VertexBuffer.MatricesIndicesKind: { - attributes.JOINTS_0 = this._accessors.length - 1; - break; + private _exportAndAssignSkeletons() { + for (const skeleton of this._babylonScene.skeletons) { + if (skeleton.bones.length <= 0) { + continue; } - case VertexBuffer.MatricesIndicesExtraKind: { - attributes.JOINTS_1 = this._accessors.length - 1; - break; + + const skin = this._skinMap.get(skeleton); + + if (skin == undefined) { + continue; } - case VertexBuffer.MatricesWeightsKind: { - attributes.WEIGHTS_0 = this._accessors.length - 1; - break; + + const boneIndexMap: { [index: number]: Bone } = {}; + const inverseBindMatrices: Matrix[] = []; + + let maxBoneIndex = -1; + for (let i = 0; i < skeleton.bones.length; ++i) { + const bone = skeleton.bones[i]; + const boneIndex = bone.getIndex() ?? i; + if (boneIndex !== -1) { + boneIndexMap[boneIndex] = bone; + if (boneIndex > maxBoneIndex) { + maxBoneIndex = boneIndex; + } + } } - case VertexBuffer.MatricesWeightsExtraKind: { - attributes.WEIGHTS_1 = this._accessors.length - 1; - break; + + // Set joints index to scene node. + for (let boneIndex = 0; boneIndex <= maxBoneIndex; ++boneIndex) { + const bone = boneIndexMap[boneIndex]; + inverseBindMatrices.push(bone.getAbsoluteInverseBindMatrix()); + const transformNode = bone.getTransformNode(); + + if (transformNode !== null) { + const nodeID = this._nodeMap.get(transformNode); + if (transformNode && nodeID !== null && nodeID !== undefined) { + skin.joints.push(nodeID); + } else { + Tools.Warn("Exporting a bone without a linked transform node is currently unsupported"); + } + } else { + Tools.Warn("Exporting a bone without a linked transform node is currently unsupported"); + } } - default: { - Tools.Warn("Unsupported Vertex Buffer Type: " + attributeKind); + + // Nodes that use this skin. + const skinedNodes = this._nodesSkinMap.get(skin); + + // 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 + 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); + }); + }); + + this._skins.push(skin); + for (const skinedNode of skinedNodes) { + skinedNode.skin = this._skins.length - 1; + } } } } - /** - * Sets data for the primitive attributes of each submesh - * @param mesh glTF Mesh object to store the primitive attribute information - * @param babylonTransformNode Babylon mesh to get the primitive attribute data from - * @param binaryWriter Buffer to write the attribute data to - * @returns promise that resolves when done setting the primitive attributes - */ - private _setPrimitiveAttributesAsync(mesh: IMesh, babylonTransformNode: TransformNode, binaryWriter: _BinaryWriter): Promise { - const promises: Promise[] = []; - let bufferMesh: Nullable = null; - let bufferView: IBufferView; - let minMax: { min: Nullable; max: Nullable }; - - if (babylonTransformNode instanceof Mesh) { - bufferMesh = babylonTransformNode as Mesh; - } else if (babylonTransformNode instanceof InstancedMesh) { - bufferMesh = (babylonTransformNode as InstancedMesh).sourceMesh; - } - const attributeData: _IVertexAttributeData[] = [ - { kind: VertexBuffer.PositionKind, accessorType: AccessorType.VEC3, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 12 }, - { kind: VertexBuffer.NormalKind, accessorType: AccessorType.VEC3, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 12 }, - { kind: VertexBuffer.ColorKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 16 }, - { kind: VertexBuffer.TangentKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 16 }, - { kind: VertexBuffer.UVKind, accessorType: AccessorType.VEC2, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 8 }, - { kind: VertexBuffer.UV2Kind, accessorType: AccessorType.VEC2, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 8 }, - { kind: VertexBuffer.MatricesIndicesKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.UNSIGNED_SHORT, byteStride: 8 }, - { kind: VertexBuffer.MatricesIndicesExtraKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.UNSIGNED_SHORT, byteStride: 8 }, - { kind: VertexBuffer.MatricesWeightsKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 16 }, - { kind: VertexBuffer.MatricesWeightsExtraKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 16 }, - ]; - - if (bufferMesh) { - let indexBufferViewIndex: Nullable = null; - const primitiveMode = this._getMeshPrimitiveMode(bufferMesh); - const vertexAttributeBufferViews: { [attributeKind: string]: number } = {}; - const morphTargetManager = bufferMesh.morphTargetManager; - - // For each BabylonMesh, create bufferviews for each 'kind' - for (const attribute of attributeData) { - const attributeKind = attribute.kind; - const attributeComponentKind = attribute.accessorComponentType; - if (bufferMesh.isVerticesDataPresent(attributeKind, true)) { - const vertexBuffer = this._getVertexBufferFromMesh(attributeKind, bufferMesh); - attribute.byteStride = vertexBuffer - ? vertexBuffer.getSize() * VertexBuffer.GetTypeByteLength(attribute.accessorComponentType) - : VertexBuffer.DeduceStride(attributeKind) * 4; - if (attribute.byteStride === 12) { - attribute.accessorType = AccessorType.VEC3; - } + private async _exportSceneAsync(): Promise { + const scene: IScene = { nodes: [] }; - this._createBufferViewKind(attributeKind, attributeComponentKind, babylonTransformNode, binaryWriter, attribute.byteStride); - attribute.bufferViewIndex = this._bufferViews.length - 1; - vertexAttributeBufferViews[attributeKind] = attribute.bufferViewIndex; - - // Write any morph target data to the buffer and create an associated buffer view - if (morphTargetManager) { - for (let i = 0; i < morphTargetManager.numTargets; ++i) { - const morphTarget = morphTargetManager.getTarget(i); - const morphTargetInfo = this._createMorphTargetBufferViewKind( - attributeKind, - attribute.accessorType, - attributeComponentKind, - bufferMesh, - morphTarget, - binaryWriter, - attribute.byteStride - ); - - // Store info about the morph target that will be needed later when creating per-submesh accessors - if (morphTargetInfo) { - if (!attribute.morphTargetInfo) { - attribute.morphTargetInfo = []; - } - attribute.morphTargetInfo[i] = morphTargetInfo; - } - } - } - } + // Scene metadata + if (this._babylonScene.metadata) { + if (this._options.metadataSelector) { + scene.extras = this._options.metadataSelector(this._babylonScene.metadata); + } else if (this._babylonScene.metadata.gltf) { + scene.extras = this._babylonScene.metadata.gltf.extras; } + } - if (bufferMesh.getTotalIndices()) { - const indices = bufferMesh.getIndices(); - if (indices) { - const byteLength = indices.length * 4; - bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, undefined, "Indices - " + bufferMesh.name); - this._bufferViews.push(bufferView); - indexBufferViewIndex = this._bufferViews.length - 1; + // TODO: + // deal with this from the loader: + // babylonMaterial.invertNormalMapX = !this._babylonScene.useRightHandedSystem; + // babylonMaterial.invertNormalMapY = this._babylonScene.useRightHandedSystem; - for (let k = 0, length = indices.length; k < length; ++k) { - binaryWriter.setUInt32(indices[k]); - } - } + const rootNodesRH = new Array(); + const rootNodesLH = new Array(); + const rootNoopNodesRH = new Array(); + + for (const rootNode of this._babylonScene.rootNodes) { + if (this._options.removeNoopRootNodes && !this._options.includeCoordinateSystemConversionNodes && IsNoopNode(rootNode, this._babylonScene.useRightHandedSystem)) { + rootNoopNodesRH.push(...rootNode.getChildren()); + } else if (this._babylonScene.useRightHandedSystem) { + rootNodesRH.push(rootNode); + } else { + rootNodesLH.push(rootNode); } + } - if (bufferMesh.subMeshes) { - // go through all mesh primitives (submeshes) - for (const submesh of bufferMesh.subMeshes) { - let babylonMaterial = submesh.getMaterial() || bufferMesh.getScene().defaultMaterial; - - let materialIndex: Nullable = null; - if (babylonMaterial) { - if (bufferMesh instanceof LinesMesh) { - // get the color from the lines mesh and set it in the material - const material: IMaterial = { - name: bufferMesh.name + " material", - }; - if (!bufferMesh.color.equals(Color3.White()) || bufferMesh.alpha < 1) { - material.pbrMetallicRoughness = { - baseColorFactor: bufferMesh.color.asArray().concat([bufferMesh.alpha]), - }; - } - this._materials.push(material); - materialIndex = this._materials.length - 1; - } else if (babylonMaterial instanceof MultiMaterial) { - const subMaterial = babylonMaterial.subMaterials[submesh.materialIndex]; - if (subMaterial) { - babylonMaterial = subMaterial; - materialIndex = this._materialMap[babylonMaterial.uniqueId]; - } - } else { - materialIndex = this._materialMap[babylonMaterial.uniqueId]; - } - } + this._listAvailableCameras(); + this._listAvailableSkeletons(); - const glTFMaterial: Nullable = materialIndex != null ? this._materials[materialIndex] : null; + const stateLH = new ExporterState(true, false); + scene.nodes.push(...(await this._exportNodesAsync(rootNodesLH, stateLH))); + const stateRH = new ExporterState(false, false); + scene.nodes.push(...(await this._exportNodesAsync(rootNodesRH, stateRH))); + const noopRH = new ExporterState(false, true); + scene.nodes.push(...(await this._exportNodesAsync(rootNoopNodesRH, noopRH))); - const meshPrimitive: IMeshPrimitive = { attributes: {} }; - this._setPrimitiveMode(meshPrimitive, primitiveMode); + if (scene.nodes.length) { + this._scenes.push(scene); + } - for (const attribute of attributeData) { - const attributeKind = attribute.kind; - if ((attributeKind === VertexBuffer.UVKind || attributeKind === VertexBuffer.UV2Kind) && !this._options.exportUnusedUVs) { - if (!glTFMaterial || !this._glTFMaterialExporter._hasTexturesPresent(glTFMaterial)) { - continue; - } - } - const vertexData = bufferMesh.getVerticesData(attributeKind, undefined, undefined, true); - if (vertexData) { - const vertexBuffer = this._getVertexBufferFromMesh(attributeKind, bufferMesh); - if (vertexBuffer) { - const stride = vertexBuffer.getSize(); - const bufferViewIndex = attribute.bufferViewIndex; - if (bufferViewIndex != undefined) { - // check to see if bufferviewindex has a numeric value assigned. - minMax = { min: null, max: null }; - if (attributeKind == VertexBuffer.PositionKind) { - minMax = _GLTFUtilities._CalculateMinMaxPositions(vertexData, 0, vertexData.length / stride); - } - const accessor = _GLTFUtilities._CreateAccessor( - bufferViewIndex, - attributeKind + " - " + babylonTransformNode.name, - attribute.accessorType, - attribute.accessorComponentType, - vertexData.length / stride, - 0, - minMax.min, - minMax.max - ); - this._accessors.push(accessor); - this._setAttributeKind(meshPrimitive.attributes, attributeKind); - } - } - } - } + this._exportAndAssignCameras(); + this._exportAndAssignSkeletons(); - if (indexBufferViewIndex) { - // Create accessor - const accessor = _GLTFUtilities._CreateAccessor( - indexBufferViewIndex, - "indices - " + babylonTransformNode.name, - AccessorType.SCALAR, - AccessorComponentType.UNSIGNED_INT, - submesh.indexCount, - submesh.indexStart * 4, - null, - null - ); - this._accessors.push(accessor); - meshPrimitive.indices = this._accessors.length - 1; - } + if (this._babylonScene.animationGroups.length) { + _GLTFAnimation._CreateNodeAndMorphAnimationFromAnimationGroups( + this._babylonScene, + this._animations, + this._nodeMap, + this._dataWriter, + this._bufferViews, + this._accessors, + this._animationSampleRate, + stateLH.getNodesSet() + ); + } + } - if (Object.keys(meshPrimitive.attributes).length > 0) { - const sideOrientation = babylonMaterial._getEffectiveOrientation(bufferMesh); + private _shouldExportNode(babylonNode: Node): boolean { + let result = this._shouldExportNodeMap.get(babylonNode); - if (sideOrientation === (this._babylonScene.useRightHandedSystem ? Material.ClockWiseSideOrientation : Material.CounterClockWiseSideOrientation)) { - let byteOffset = indexBufferViewIndex != null ? this._bufferViews[indexBufferViewIndex].byteOffset : null; - if (byteOffset == null) { - byteOffset = 0; - } - let babylonIndices: Nullable = null; - if (indexBufferViewIndex != null) { - babylonIndices = bufferMesh.getIndices(); - } - if (babylonIndices) { - this._reorderIndicesBasedOnPrimitiveMode(submesh, primitiveMode, babylonIndices, byteOffset, binaryWriter); - } else { - for (const attribute of attributeData) { - const vertexData = bufferMesh.getVerticesData(attribute.kind, undefined, undefined, true); - if (vertexData) { - const byteOffset = this._bufferViews[vertexAttributeBufferViews[attribute.kind]].byteOffset || 0; - this._reorderVertexAttributeDataBasedOnPrimitiveMode(submesh, primitiveMode, attribute.kind, vertexData, byteOffset, binaryWriter); - } - } - } - } + if (result === undefined) { + result = this._options.shouldExportNode(babylonNode); + this._shouldExportNodeMap.set(babylonNode, result); + } - if (materialIndex != null) { - meshPrimitive.material = materialIndex; - } - } + return result; + } - // If there are morph targets, write out targets and associated accessors - if (morphTargetManager) { - // By convention, morph target names are stored in the mesh extras. - if (!mesh.extras) { - mesh.extras = {}; - } - mesh.extras.targetNames = []; - - for (let i = 0; i < morphTargetManager.numTargets; ++i) { - const morphTarget = morphTargetManager.getTarget(i); - mesh.extras.targetNames.push(morphTarget.name); - - for (const attribute of attributeData) { - const morphTargetInfo = attribute.morphTargetInfo?.[i]; - if (morphTargetInfo) { - // Write the accessor - const byteOffset = 0; - const accessor = _GLTFUtilities._CreateAccessor( - morphTargetInfo.bufferViewIndex, - `${attribute.kind} - ${morphTarget.name} (Morph Target)`, - morphTargetInfo.accessorType, - attribute.accessorComponentType, - morphTargetInfo.vertexCount, - byteOffset, - morphTargetInfo.minMax?.min?.asArray() ?? null, - morphTargetInfo.minMax?.max?.asArray() ?? null - ); - this._accessors.push(accessor); - - // Create a target that references the new accessor - if (!meshPrimitive.targets) { - meshPrimitive.targets = []; - } - - if (!meshPrimitive.targets[i]) { - meshPrimitive.targets[i] = {}; - } - - this._setAttributeKind(meshPrimitive.targets[i], attribute.kind); - } - } - } - } + private async _exportNodesAsync(babylonRootNodes: Node[], state: ExporterState): Promise { + const nodes = new Array(); - mesh.primitives.push(meshPrimitive); + this._exportBuffers(babylonRootNodes, state); - this._extensionsPostExportMeshPrimitiveAsync("postExport", meshPrimitive, submesh, binaryWriter); - promises.push(); + for (const babylonNode of babylonRootNodes) { + if (this._shouldExportNode(babylonNode)) { + const nodeIndex = await this._exportNodeAsync(babylonNode, state); + if (nodeIndex !== null) { + nodes.push(nodeIndex); } } } - return Promise.all(promises).then(() => { - /* do nothing */ - }); + + return nodes; } - /** - * Creates a glTF scene based on the array of meshes - * Returns the total byte offset - * @param binaryWriter Buffer to write binary data to - * @returns a promise that resolves when done - */ - private _createSceneAsync(binaryWriter: _BinaryWriter): Promise { - const scene: IScene = { nodes: [] }; - let glTFNodeIndex: number; - let glTFNode: INode; - let directDescendents: Node[]; - const nodes: Node[] = [...this._babylonScene.transformNodes, ...this._babylonScene.meshes, ...this._babylonScene.lights, ...this._babylonScene.cameras]; - const removedRootNodes = new Set(); + private _collectBuffers( + babylonNode: Node, + bufferToVertexBuffersMap: Map, + vertexBufferToMeshesMap: Map, + morphTargetsToMeshesMap: Map, + state: ExporterState + ): void { + if (!this._shouldExportNode(babylonNode)) { + return; + } - // Scene metadata - if (this._babylonScene.metadata) { - if (this._options.metadataSelector) { - scene.extras = this._options.metadataSelector(this._babylonScene.metadata); - } else if (this._babylonScene.metadata.gltf) { - scene.extras = this._babylonScene.metadata.gltf.extras; + if (babylonNode instanceof Mesh && babylonNode.geometry) { + const vertexBuffers = babylonNode.geometry.getVertexBuffers(); + if (vertexBuffers) { + for (const kind in vertexBuffers) { + const vertexBuffer = vertexBuffers[kind]; + state.setHasVertexColorAlpha(vertexBuffer, babylonNode.hasVertexAlpha); + const buffer = vertexBuffer._buffer; + const vertexBufferArray = bufferToVertexBuffersMap.get(buffer) || []; + bufferToVertexBuffersMap.set(buffer, vertexBufferArray); + if (vertexBufferArray.indexOf(vertexBuffer) === -1) { + vertexBufferArray.push(vertexBuffer); + } + + const meshes = vertexBufferToMeshesMap.get(vertexBuffer) || []; + vertexBufferToMeshesMap.set(vertexBuffer, meshes); + if (meshes.indexOf(babylonNode) === -1) { + meshes.push(babylonNode); + } + } } - } - // Remove no-op root nodes - if ((this._options.removeNoopRootNodes ?? true) && !this._options.includeCoordinateSystemConversionNodes) { - for (const rootNode of this._babylonScene.rootNodes) { - if (isNoopNode(rootNode, this._babylonScene.useRightHandedSystem)) { - removedRootNodes.add(rootNode); + const morphTargetManager = babylonNode.morphTargetManager; + + if (morphTargetManager) { + for (let morphIndex = 0; morphIndex < morphTargetManager.numTargets; morphIndex++) { + const morphTarget = morphTargetManager.getTarget(morphIndex); - // Exclude the node from list of nodes to export - nodes.splice(nodes.indexOf(rootNode), 1); + const meshes = morphTargetsToMeshesMap.get(morphTarget) || []; + morphTargetsToMeshesMap.set(morphTarget, meshes); + if (meshes.indexOf(babylonNode) === -1) { + meshes.push(babylonNode); + } } } } - // Export babylon cameras to glTFCamera - const cameraMap = new Map(); - this._babylonScene.cameras.forEach((camera) => { - if (this._options.shouldExportNode && !this._options.shouldExportNode(camera)) { - return; + for (const babylonChildNode of babylonNode.getChildren()) { + this._collectBuffers(babylonChildNode, bufferToVertexBuffersMap, vertexBufferToMeshesMap, morphTargetsToMeshesMap, state); + } + } + + private _exportBuffers(babylonRootNodes: Node[], state: ExporterState): void { + const bufferToVertexBuffersMap = new Map(); + const vertexBufferToMeshesMap = new Map(); + const morphTagetsMeshesMap = new Map(); + + for (const babylonNode of babylonRootNodes) { + this._collectBuffers(babylonNode, bufferToVertexBuffersMap, vertexBufferToMeshesMap, morphTagetsMeshesMap, state); + } + + const buffers = Array.from(bufferToVertexBuffersMap.keys()); + + for (const buffer of buffers) { + const data = buffer.getData(); + if (!data) { + throw new Error("Buffer data is not available"); } - const glTFCamera: ICamera = { - type: camera.mode === Camera.PERSPECTIVE_CAMERA ? CameraType.PERSPECTIVE : CameraType.ORTHOGRAPHIC, - }; + const vertexBuffers = bufferToVertexBuffersMap.get(buffer); - if (camera.name) { - glTFCamera.name = camera.name; + if (!vertexBuffers) { + continue; } - if (glTFCamera.type === CameraType.PERSPECTIVE) { - glTFCamera.perspective = { - aspectRatio: camera.getEngine().getAspectRatio(camera), - yfov: camera.fovMode === Camera.FOVMODE_VERTICAL_FIXED ? camera.fov : camera.fov * camera.getEngine().getAspectRatio(camera), - znear: camera.minZ, - zfar: camera.maxZ, - }; - } else if (glTFCamera.type === CameraType.ORTHOGRAPHIC) { - const halfWidth = camera.orthoLeft && camera.orthoRight ? 0.5 * (camera.orthoRight - camera.orthoLeft) : camera.getEngine().getRenderWidth() * 0.5; - const halfHeight = camera.orthoBottom && camera.orthoTop ? 0.5 * (camera.orthoTop - camera.orthoBottom) : camera.getEngine().getRenderHeight() * 0.5; - glTFCamera.orthographic = { - xmag: halfWidth, - ymag: halfHeight, - znear: camera.minZ, - zfar: camera.maxZ, - }; + const byteStride = vertexBuffers[0].byteStride; + if (vertexBuffers.some((vertexBuffer) => vertexBuffer.byteStride !== byteStride)) { + throw new Error("Vertex buffers pointing to the same buffer must have the same byte stride"); } - cameraMap.set(camera, this._cameras.length); - this._cameras.push(glTFCamera); - }); + const bytes = DataArrayToUint8Array(data).slice(); + + // Apply conversions to buffer data in-place. + for (const vertexBuffer of vertexBuffers) { + const { byteOffset, byteStride, type, normalized } = vertexBuffer; + const size = vertexBuffer.getSize(); + const meshes = vertexBufferToMeshesMap.get(vertexBuffer)!; + const maxTotalVertices = meshes.reduce((max, current) => { + return current.getTotalVertices() > max ? current.getTotalVertices() : max; + }, -Number.MAX_VALUE); // To ensure nothing is missed when enumerating, but may not be necessary. + + switch (vertexBuffer.getKind()) { + // Normalize normals and tangents. + 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; + }); + break; + } + // Convert StandardMaterial vertex colors from gamma to linear space. + case VertexBuffer.ColorKind: { + const stdMaterialCount = meshes.filter((mesh) => mesh.material instanceof StandardMaterial || mesh.material == null).length; + + if (stdMaterialCount == 0) { + break; // Buffer not used by StandardMaterials, so no conversion needed. + } + + // TODO: Implement this case. + if (stdMaterialCount != meshes.length) { + Logger.Warn("Not converting vertex color space, as buffer is shared by StandardMaterials and other material types. Results may look incorrect."); + break; + } - const [exportNodes, exportMaterials] = this._getExportNodes(nodes); - return this._glTFMaterialExporter._convertMaterialsToGLTFAsync(exportMaterials, ImageMimeType.PNG, true).then(() => { - return this._createNodeMapAndAnimationsAsync(exportNodes, binaryWriter).then((nodeMap) => { - return this._createSkinsAsync(nodeMap, binaryWriter).then((skinMap) => { - this._nodeMap = nodeMap; + if (type == VertexBuffer.UNSIGNED_BYTE) { + Logger.Warn("Converting uint8 vertex colors to linear space. Results may look incorrect."); + } - this._totalByteLength = binaryWriter.getByteOffset(); - if (this._totalByteLength == undefined) { - throw new Error("undefined byte length!"); - } + const vertexData3 = new Color3(); + const vertexData4 = new Color4(); + const useExactSrgbConversions = this._babylonScene.getEngine().useExactSrgbConversions; - // Build Hierarchy with the node map. - for (const babylonNode of nodes) { - glTFNodeIndex = this._nodeMap[babylonNode.uniqueId]; - if (glTFNodeIndex !== undefined) { - glTFNode = this._nodes[glTFNodeIndex]; - - if (babylonNode.metadata) { - if (this._options.metadataSelector) { - glTFNode.extras = this._options.metadataSelector(babylonNode.metadata); - } else if (babylonNode.metadata.gltf) { - glTFNode.extras = babylonNode.metadata.gltf.extras; - } + EnumerateFloatValues(bytes, byteOffset, byteStride, size, type, maxTotalVertices * size, normalized, (values) => { + // Using separate Color3 and Color4 objects to ensure the right functions are called. + if (values.length === 3) { + vertexData3.fromArray(values, 0); + vertexData3.toLinearSpaceToRef(vertexData3, useExactSrgbConversions); + vertexData3.toArray(values, 0); + } else { + vertexData4.fromArray(values, 0); + vertexData4.toLinearSpaceToRef(vertexData4, useExactSrgbConversions); + vertexData4.toArray(values, 0); } + }); + } + } + } - if (babylonNode instanceof Camera) { - glTFNode.camera = cameraMap.get(babylonNode); + // Performs coordinate conversion if needed (only for position, normal and tanget). + if (state.convertToRightHanded) { + for (const vertexBuffer of vertexBuffers) { + switch (vertexBuffer.getKind()) { + case VertexBuffer.PositionKind: + case VertexBuffer.NormalKind: + case VertexBuffer.TangentKind: { + for (const mesh of vertexBufferToMeshesMap.get(vertexBuffer)!) { + const { byteOffset, byteStride, type, normalized } = vertexBuffer; + const size = vertexBuffer.getSize(); + EnumerateFloatValues(bytes, byteOffset, byteStride, size, type, mesh.getTotalVertices() * size, normalized, (values) => { + values[0] = -values[0]; + }); } + } + } + } - if (this._options.shouldExportNode && !this._options.shouldExportNode(babylonNode)) { - Tools.Log("Omitting " + babylonNode.name + " from scene."); - } else { - if (!babylonNode.parent && !this._babylonScene.useRightHandedSystem) { - convertNodeHandedness(glTFNode); - } + // Save converted bytes for min/max computation. + state.convertedToRightHandedBuffers.set(buffer, bytes); + } - if (!babylonNode.parent || removedRootNodes.has(babylonNode.parent)) { - scene.nodes.push(glTFNodeIndex); - } - } + 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); - if (babylonNode instanceof Mesh) { - if (babylonNode.skeleton) { - glTFNode.skin = skinMap[babylonNode.skeleton.uniqueId]; - } - } + const floatMatricesIndices = new Map(); - directDescendents = babylonNode.getDescendants(true); - if (!glTFNode.children && directDescendents && directDescendents.length) { - const children: number[] = []; - for (const descendent of directDescendents) { - if (this._nodeMap[descendent.uniqueId] != null) { - children.push(this._nodeMap[descendent.uniqueId]); - } - } - if (children.length) { - glTFNode.children = children; + // If buffers are of type MatricesWeightsKind and have float values, we need to create a new buffer instead. + for (const vertexBuffer of vertexBuffers) { + switch (vertexBuffer.getKind()) { + case VertexBuffer.MatricesIndicesKind: + case VertexBuffer.MatricesIndicesExtraKind: { + if (vertexBuffer.type == VertexBuffer.FLOAT) { + for (const mesh of vertexBufferToMeshesMap.get(vertexBuffer)!) { + const floatData = vertexBuffer.getFloatData(mesh.getTotalVertices()); + if (floatData !== null) { + floatMatricesIndices.set(vertexBuffer, floatData); } } } } - if (scene.nodes.length) { - this._scenes.push(scene); + } + } + + if (floatMatricesIndices.size !== 0) { + Logger.Warn( + `Joints conversion needed: some joints are stored as floats in Babylon but GLTF requires UNSIGNED BYTES. We will perform the conversion but this might lead to unused data in the buffer.` + ); + } + + const floatArrayVertexBuffers = Array.from(floatMatricesIndices.keys()); + + for (const vertexBuffer of floatArrayVertexBuffers) { + const array = floatMatricesIndices.get(vertexBuffer); + + if (!array) { + 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)); + } + + state.setRemappedBufferView(buffer, vertexBuffer, this._bufferViews.length - 1); + } + } + + const morphTargets = Array.from(morphTagetsMeshesMap.keys()); + + for (const morphTarget of morphTargets) { + const meshes = morphTagetsMeshesMap.get(morphTarget); + + if (!meshes) { + continue; + } + + const glTFMorphTarget = BuildMorphTargetBuffers(morphTarget, meshes[0], this._dataWriter, this._bufferViews, this._accessors, state.convertToRightHanded); + + for (const mesh of meshes) { + state.bindMorphDataToMesh(mesh, glTFMorphTarget); + } + } } /** - * Getting the nodes and materials that would be exported. - * @param nodes Babylon transform nodes - * @returns Set of materials which would be exported. + * Processes a node to be exported to the glTF file + * @returns A promise that resolves with the node index when the processing is complete, or null if the node should not be exported + * @internal */ - private _getExportNodes(nodes: Node[]): [Node[], Set] { - const exportNodes: Node[] = []; - const exportMaterials: Set = new Set(); + private async _exportNodeAsync(babylonNode: Node, state: ExporterState): Promise> { + let nodeIndex = this._nodeMap.get(babylonNode); + if (nodeIndex !== undefined) { + return nodeIndex; + } - for (const babylonNode of nodes) { - if (!this._options.shouldExportNode || this._options.shouldExportNode(babylonNode)) { - exportNodes.push(babylonNode); + const node: INode = {}; - const babylonMesh = babylonNode as AbstractMesh; + if (babylonNode.name) { + node.name = babylonNode.name; + } + + if (babylonNode instanceof TransformNode) { + this._setNodeTransformation(node, babylonNode, state.convertToRightHanded); + + if (babylonNode instanceof Mesh || babylonNode instanceof InstancedMesh) { + const babylonMesh = babylonNode instanceof Mesh ? babylonNode : babylonNode.sourceMesh; if (babylonMesh.subMeshes && babylonMesh.subMeshes.length > 0) { - const material = babylonMesh.material || babylonMesh.getScene().defaultMaterial; - if (material instanceof MultiMaterial) { - for (const subMaterial of material.subMaterials) { - if (subMaterial) { - exportMaterials.add(subMaterial); - } + node.mesh = await this._exportMeshAsync(babylonMesh, state); + } + + if (babylonNode.skeleton) { + const skin = this._skinMap.get(babylonNode.skeleton); + + if (skin !== undefined) { + if (this._nodesSkinMap.get(skin) === undefined) { + this._nodesSkinMap.set(skin, []); } - } else { - exportMaterials.add(material); + + this._nodesSkinMap.get(skin)?.push(node); } } - } else { - `Excluding node ${babylonNode.name}`; } } - return [exportNodes, exportMaterials]; - } + if (babylonNode instanceof Camera) { + const gltfCamera = this._camerasMap.get(babylonNode); - /** - * Creates a mapping of Node unique id to node index and handles animations - * @param nodes Babylon transform nodes - * @param binaryWriter Buffer to write binary data to - * @returns Node mapping of unique id to index - */ - private _createNodeMapAndAnimationsAsync(nodes: Node[], binaryWriter: _BinaryWriter): Promise<{ [key: number]: number }> { - let promiseChain = Promise.resolve(); - const nodeMap: { [key: number]: number } = {}; - let nodeIndex: number; + if (gltfCamera) { + if (this._nodesCameraMap.get(gltfCamera) === undefined) { + this._nodesCameraMap.set(gltfCamera, []); + } + + const parentBabylonNode = babylonNode.parent; + this._setCameraTransformation(node, babylonNode, state.convertToRightHanded, parentBabylonNode); + + // If a camera has a node that was added by the GLTF importer, we can just use the parent node transform as the "camera" transform. + if (parentBabylonNode && IsParentAddedByImporter(babylonNode, parentBabylonNode)) { + const parentNodeIndex = this._nodeMap.get(parentBabylonNode); + if (parentNodeIndex) { + const parentNode = this._nodes[parentNodeIndex]; + this._nodesCameraMap.get(gltfCamera)?.push(parentNode); + return null; // Skip exporting this node + } + } else { + if (state.convertToRightHanded) { + ConvertToRightHandedNode(node); + RotateNode180Y(node); + } + this._nodesCameraMap.get(gltfCamera)?.push(node); + } + } + } + + // Apply extensions to the node. If this resolves to null, it means we should skip exporting this node (NOTE: This will also skip its children) + const processedNode = await this._extensionsPostExportNodeAsync("exportNodeAsync", node, babylonNode, this._nodeMap, state.convertToRightHanded); + if (!processedNode) { + Logger.Warn(`Not exporting node ${babylonNode.name}`); + return null; + } + + nodeIndex = this._nodes.length; + this._nodes.push(node); + this._nodeMap.set(babylonNode, nodeIndex); + state.pushExportedNode(babylonNode); + + // Process node's animations once the node has been added to nodeMap (TODO: This should be refactored) const runtimeGLTFAnimation: IAnimation = { name: "runtime animations", channels: [], @@ -1907,443 +1262,294 @@ export class _Exporter { }; const idleGLTFAnimations: IAnimation[] = []; - for (const babylonNode of nodes) { - promiseChain = promiseChain.then(() => { - return this._createNodeAsync(babylonNode, binaryWriter).then((node) => { - const promise = this._extensionsPostExportNodeAsync("createNodeAsync", node, babylonNode, nodeMap, binaryWriter); - if (promise == null) { - Tools.Warn(`Not exporting node ${babylonNode.name}`); - return Promise.resolve(); - } else { - return promise.then((node) => { - if (!node) { - return; - } - this._nodes.push(node); - nodeIndex = this._nodes.length - 1; - nodeMap[babylonNode.uniqueId] = nodeIndex; - - if (!this._babylonScene.animationGroups.length) { - _GLTFAnimation._CreateMorphTargetAnimationFromMorphTargetAnimations( - babylonNode, - runtimeGLTFAnimation, - idleGLTFAnimations, - nodeMap, - this._nodes, - binaryWriter, - this._bufferViews, - this._accessors, - this._animationSampleRate, - this._options.shouldExportAnimation - ); - if (babylonNode.animations.length) { - _GLTFAnimation._CreateNodeAnimationFromNodeAnimations( - babylonNode, - runtimeGLTFAnimation, - idleGLTFAnimations, - nodeMap, - this._nodes, - binaryWriter, - this._bufferViews, - this._accessors, - this._animationSampleRate, - this._options.shouldExportAnimation - ); - } - } - }); - } - }); - }); - } - - return promiseChain.then(() => { - if (runtimeGLTFAnimation.channels.length && runtimeGLTFAnimation.samplers.length) { - this._animations.push(runtimeGLTFAnimation); - } - idleGLTFAnimations.forEach((idleGLTFAnimation) => { - if (idleGLTFAnimation.channels.length && idleGLTFAnimation.samplers.length) { - this._animations.push(idleGLTFAnimation); - } - }); - - if (this._babylonScene.animationGroups.length) { - _GLTFAnimation._CreateNodeAndMorphAnimationFromAnimationGroups( - this._babylonScene, - this._animations, - nodeMap, - binaryWriter, + if (!this._babylonScene.animationGroups.length) { + _GLTFAnimation._CreateMorphTargetAnimationFromMorphTargetAnimations( + babylonNode, + runtimeGLTFAnimation, + idleGLTFAnimations, + this._nodeMap, + this._nodes, + this._dataWriter, + this._bufferViews, + this._accessors, + this._animationSampleRate, + state.convertToRightHanded, + this._options.shouldExportAnimation + ); + if (babylonNode.animations.length) { + _GLTFAnimation._CreateNodeAnimationFromNodeAnimations( + babylonNode, + runtimeGLTFAnimation, + idleGLTFAnimations, + this._nodeMap, + this._nodes, + this._dataWriter, this._bufferViews, this._accessors, this._animationSampleRate, + state.convertToRightHanded, this._options.shouldExportAnimation ); } + } - return nodeMap; - }); - } - - /** - * Creates a glTF node from a Babylon mesh - * @param babylonNode Source Babylon mesh - * @param binaryWriter Buffer for storing geometry data - * @returns glTF node - */ - private _createNodeAsync(babylonNode: Node, binaryWriter: _BinaryWriter): Promise { - return Promise.resolve().then(() => { - // create node to hold translation/rotation/scale and the mesh - const node: INode = {}; - // create mesh - const mesh: IMesh = { primitives: [] }; - - if (babylonNode.name) { - node.name = babylonNode.name; + if (runtimeGLTFAnimation.channels.length && runtimeGLTFAnimation.samplers.length) { + this._animations.push(runtimeGLTFAnimation); + } + idleGLTFAnimations.forEach((idleGLTFAnimation) => { + if (idleGLTFAnimation.channels.length && idleGLTFAnimation.samplers.length) { + this._animations.push(idleGLTFAnimation); } + }); - if (babylonNode instanceof TransformNode) { - // Set transformation - this._setNodeTransformation(node, babylonNode); - if (babylonNode instanceof Mesh) { - const morphTargetManager = babylonNode.morphTargetManager; - if (morphTargetManager && morphTargetManager.numTargets > 0) { - mesh.weights = []; - for (let i = 0; i < morphTargetManager.numTargets; ++i) { - mesh.weights.push(morphTargetManager.getTarget(i).influence); - } - } + // Begin processing child nodes once parent has been added to the node list + for (const babylonChildNode of babylonNode.getChildren()) { + if (this._shouldExportNode(babylonChildNode)) { + const childNodeIndex = await this._exportNodeAsync(babylonChildNode, state); + if (childNodeIndex !== null) { + node.children ||= []; + node.children.push(childNodeIndex); } - return this._setPrimitiveAttributesAsync(mesh, babylonNode, binaryWriter).then(() => { - if (mesh.primitives.length) { - this._meshes.push(mesh); - node.mesh = this._meshes.length - 1; - } - return node; - }); - } else if (babylonNode instanceof Camera) { - this._setCameraTransformation(node, babylonNode); - return node; - } else { - return node; } - }); + } + + return nodeIndex; } - /** - * Creates a glTF skin from a Babylon skeleton - * @param nodeMap Babylon transform nodes - * @param binaryWriter Buffer to write binary data to - * @returns Node mapping of unique id to index - */ - private _createSkinsAsync(nodeMap: { [key: number]: number }, binaryWriter: _BinaryWriter): Promise<{ [key: number]: number }> { - const promiseChain = Promise.resolve(); - const skinMap: { [key: number]: number } = {}; - for (const skeleton of this._babylonScene.skeletons) { - if (skeleton.bones.length <= 0) { - continue; - } - // create skin - const skin: ISkin = { joints: [] }; - const inverseBindMatrices: Matrix[] = []; + private _exportIndices( + indices: Nullable, + start: number, + count: number, + offset: number, + fillMode: number, + sideOrientation: number, + state: ExporterState, + primitive: IMeshPrimitive + ): void { + const is32Bits = AreIndices32Bits(indices, count); + let indicesToExport = indices; - const boneIndexMap: { [index: number]: Bone } = {}; - let maxBoneIndex = -1; - for (let i = 0; i < skeleton.bones.length; ++i) { - const bone = skeleton.bones[i]; - const boneIndex = bone.getIndex() ?? i; - if (boneIndex !== -1) { - boneIndexMap[boneIndex] = bone; - if (boneIndex > maxBoneIndex) { - maxBoneIndex = boneIndex; - } - } + primitive.mode = GetPrimitiveMode(fillMode); + + // Flip if triangle winding order is not CCW as glTF is always CCW. + const invertedMaterial = sideOrientation !== Material.CounterClockWiseSideOrientation; + + const flipWhenInvertedMaterial = !state.wasAddedByNoopNode && invertedMaterial; + + const flip = IsTriangleFillMode(fillMode) && flipWhenInvertedMaterial; + + if (flip) { + if (fillMode === Material.TriangleStripDrawMode || fillMode === Material.TriangleFanDrawMode) { + throw new Error("Triangle strip/fan fill mode is not implemented"); } - for (let boneIndex = 0; boneIndex <= maxBoneIndex; ++boneIndex) { - const bone = boneIndexMap[boneIndex]; - inverseBindMatrices.push(bone.getInvertedAbsoluteTransform()); + primitive.mode = GetPrimitiveMode(fillMode); - const transformNode = bone.getTransformNode(); - if (transformNode && nodeMap[transformNode.uniqueId] !== null && nodeMap[transformNode.uniqueId] !== undefined) { - skin.joints.push(nodeMap[transformNode.uniqueId]); - } else { - Tools.Warn("Exporting a bone without a linked transform node is currently unsupported"); + const newIndices = is32Bits ? new Uint32Array(count) : new Uint16Array(count); + + 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; } } - if (skin.joints.length > 0) { - // create buffer view for inverse bind matrices - const byteStride = 64; // 4 x 4 matrix of 32 bit float - const byteLength = inverseBindMatrices.length * byteStride; - const bufferViewOffset = binaryWriter.getByteOffset(); - const bufferView = _GLTFUtilities._CreateBufferView(0, bufferViewOffset, byteLength, undefined, "InverseBindMatrices" + " - " + skeleton.name); - this._bufferViews.push(bufferView); + 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; + } + + indicesToExport = newIndices; + } + + 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 bindMatrixAccessor = _GLTFUtilities._CreateAccessor( - bufferViewIndex, - "InverseBindMatrices" + " - " + skeleton.name, - AccessorType.MAT4, - AccessorComponentType.FLOAT, - inverseBindMatrices.length, - null, - null, - null - ); - const inverseBindAccessorIndex = this._accessors.push(bindMatrixAccessor) - 1; - skin.inverseBindMatrices = inverseBindAccessorIndex; - this._skins.push(skin); - skinMap[skeleton.uniqueId] = this._skins.length - 1; - inverseBindMatrices.forEach((mat) => { - mat.m.forEach((cell: number) => { - binaryWriter.setFloat32(cell); - }); - }); + const componentType = is32Bits ? AccessorComponentType.UNSIGNED_INT : AccessorComponentType.UNSIGNED_SHORT; + this._accessors.push(CreateAccessor(bufferViewIndex, AccessorType.SCALAR, componentType, count, 0)); + accessorIndex = this._accessors.length - 1; + state.setIndicesAccessor(indices, start, count, offset, flip, accessorIndex); } + + primitive.indices = accessorIndex; } - return promiseChain.then(() => { - return skinMap; - }); } -} -/** - * @internal - * - * Stores glTF binary data. If the array buffer byte length is exceeded, it doubles in size dynamically - */ -export class _BinaryWriter { - /** - * Array buffer which stores all binary data - */ - private _arrayBuffer: ArrayBuffer; - /** - * View of the array buffer - */ - private _dataView: DataView; - /** - * byte offset of data in array buffer - */ - private _byteOffset: number; - /** - * Initialize binary writer with an initial byte length - * @param byteLength Initial byte length of the array buffer - */ - constructor(byteLength: number) { - this._arrayBuffer = new ArrayBuffer(byteLength); - this._dataView = new DataView(this._arrayBuffer); - this._byteOffset = 0; - } - /** - * Resize the array buffer to the specified byte length - * @param byteLength The new byte length - * @returns The resized array buffer - */ - private _resizeBuffer(byteLength: number): ArrayBuffer { - const newBuffer = new ArrayBuffer(byteLength); - const copyOldBufferSize = Math.min(this._arrayBuffer.byteLength, byteLength); - const oldUint8Array = new Uint8Array(this._arrayBuffer, 0, copyOldBufferSize); - const newUint8Array = new Uint8Array(newBuffer); - newUint8Array.set(oldUint8Array, 0); - this._arrayBuffer = newBuffer; - this._dataView = new DataView(this._arrayBuffer); - - return newBuffer; - } - /** - * Get an array buffer with the length of the byte offset - * @returns ArrayBuffer resized to the byte offset - */ - public getArrayBuffer(): ArrayBuffer { - return this._resizeBuffer(this.getByteOffset()); - } - /** - * Get the byte offset of the array buffer - * @returns byte offset - */ - public getByteOffset(): number { - if (this._byteOffset == undefined) { - throw new Error("Byte offset is undefined!"); + private _exportVertexBuffer(vertexBuffer: VertexBuffer, babylonMaterial: Material, start: number, count: number, state: ExporterState, primitive: IMeshPrimitive): void { + const kind = vertexBuffer.getKind(); + + if (!IsStandardVertexAttribute(kind)) { + return; } - return this._byteOffset; - } - /** - * Stores an UInt8 in the array buffer - * @param entry - * @param byteOffset If defined, specifies where to set the value as an offset. - */ - public setUInt8(entry: number, byteOffset?: number) { - if (byteOffset != null) { - if (byteOffset < this._byteOffset) { - this._dataView.setUint8(byteOffset, entry); - } else { - Tools.Error("BinaryWriter: byteoffset is greater than the current binary buffer length!"); - } - } else { - if (this._byteOffset + 1 > this._arrayBuffer.byteLength) { - this._resizeBuffer(this._arrayBuffer.byteLength * 2); + + if (kind.startsWith("uv") && !this._options.exportUnusedUVs) { + if (!babylonMaterial || !this._materialNeedsUVsSet.has(babylonMaterial)) { + return; } - this._dataView.setUint8(this._byteOffset, entry); - this._byteOffset += 1; } - } - /** - * Stores an UInt16 in the array buffer - * @param entry - * @param byteOffset If defined, specifies where to set the value as an offset. - */ - public setUInt16(entry: number, byteOffset?: number) { - if (byteOffset != null) { - if (byteOffset < this._byteOffset) { - this._dataView.setUint16(byteOffset, entry, true); + let accessorIndex = state.getVertexAccessor(vertexBuffer, start, count); + + 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 { - Tools.Error("BinaryWriter: byteoffset is greater than the current binary buffer length!"); + 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 { - if (this._byteOffset + 2 > this._arrayBuffer.byteLength) { - this._resizeBuffer(this._arrayBuffer.byteLength * 2); - } - this._dataView.setUint16(this._byteOffset, entry, true); - this._byteOffset += 2; + primitive.attributes[GetAttributeType(kind)] = accessorIndex; } } - /** - * Gets an UInt32 in the array buffer - * @param byteOffset If defined, specifies where to set the value as an offset. - * @returns entry - */ - public getUInt32(byteOffset: number): number { - if (byteOffset < this._byteOffset) { - return this._dataView.getUint32(byteOffset, true); - } else { - Tools.Error("BinaryWriter: byteoffset is greater than the current binary buffer length!"); - throw new Error("BinaryWriter: byteoffset is greater than the current binary buffer length!"); - } - } + private async _exportMaterialAsync(babylonMaterial: Material, vertexBuffers: { [kind: string]: VertexBuffer }, subMesh: SubMesh, primitive: IMeshPrimitive): Promise { + 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 PBRMaterial) { + materialIndex = await this._materialExporter.exportPBRMaterialAsync(babylonMaterial, ImageMimeType.PNG, hasUVs); + } else if (babylonMaterial instanceof StandardMaterial) { + materialIndex = await this._materialExporter.exportStandardMaterialAsync(babylonMaterial, ImageMimeType.PNG, hasUVs); + } else { + Logger.Warn(`Unsupported material '${babylonMaterial.name}' with type ${babylonMaterial.getClassName()}`); + return; + } - public getVector3Float32FromRef(vector3: Vector3, byteOffset: number): void { - if (byteOffset + 8 > this._byteOffset) { - Tools.Error(`BinaryWriter: byteoffset is greater than the current binary buffer length!`); - } else { - vector3.x = this._dataView.getFloat32(byteOffset, true); - vector3.y = this._dataView.getFloat32(byteOffset + 4, true); - vector3.z = this._dataView.getFloat32(byteOffset + 8, true); + this._materialMap.set(babylonMaterial, materialIndex); } - } - public setVector3Float32FromRef(vector3: Vector3, byteOffset: number): void { - if (byteOffset + 8 > this._byteOffset) { - Tools.Error(`BinaryWriter: byteoffset is greater than the current binary buffer length!`); - } else { - this._dataView.setFloat32(byteOffset, vector3.x, true); - this._dataView.setFloat32(byteOffset + 4, vector3.y, true); - this._dataView.setFloat32(byteOffset + 8, vector3.z, true); - } + primitive.material = materialIndex; } - public getVector4Float32FromRef(vector4: Vector4, byteOffset: number): void { - if (byteOffset + 12 > this._byteOffset) { - Tools.Error(`BinaryWriter: byteoffset is greater than the current binary buffer length!`); - } else { - vector4.x = this._dataView.getFloat32(byteOffset, true); - vector4.y = this._dataView.getFloat32(byteOffset + 4, true); - vector4.z = this._dataView.getFloat32(byteOffset + 8, true); - vector4.w = this._dataView.getFloat32(byteOffset + 12, true); + private async _exportMeshAsync(babylonMesh: Mesh, state: ExporterState): Promise { + let meshIndex = state.getMesh(babylonMesh); + if (meshIndex !== undefined) { + return meshIndex; } - } - public setVector4Float32FromRef(vector4: Vector4, byteOffset: number): void { - if (byteOffset + 12 > this._byteOffset) { - Tools.Error(`BinaryWriter: byteoffset is greater than the current binary buffer length!`); - } else { - this._dataView.setFloat32(byteOffset, vector4.x, true); - this._dataView.setFloat32(byteOffset + 4, vector4.y, true); - this._dataView.setFloat32(byteOffset + 8, vector4.z, true); - this._dataView.setFloat32(byteOffset + 12, vector4.w, true); - } - } - /** - * Stores a Float32 in the array buffer - * @param entry - * @param byteOffset - */ - public setFloat32(entry: number, byteOffset?: number) { - if (isNaN(entry)) { - Tools.Error("Invalid data being written!"); - } - if (byteOffset != null) { - if (byteOffset < this._byteOffset) { - this._dataView.setFloat32(byteOffset, entry, true); - } else { - Tools.Error("BinaryWriter: byteoffset is greater than the current binary length!"); - } - } - if (this._byteOffset + 4 > this._arrayBuffer.byteLength) { - this._resizeBuffer(this._arrayBuffer.byteLength * 2); - } - this._dataView.setFloat32(this._byteOffset, entry, true); - this._byteOffset += 4; - } - /** - * Stores an UInt32 in the array buffer - * @param entry - * @param byteOffset If defined, specifies where to set the value as an offset. - */ - public setUInt32(entry: number, byteOffset?: number) { - if (byteOffset != null) { - if (byteOffset < this._byteOffset) { - this._dataView.setUint32(byteOffset, entry, true); - } else { - Tools.Error("BinaryWriter: byteoffset is greater than the current binary buffer length!"); - } - } else { - if (this._byteOffset + 4 > this._arrayBuffer.byteLength) { - this._resizeBuffer(this._arrayBuffer.byteLength * 2); - } - this._dataView.setUint32(this._byteOffset, entry, true); - this._byteOffset += 4; + const mesh: IMesh = { primitives: [] }; + meshIndex = this._meshes.length; + this._meshes.push(mesh); + state.setMesh(babylonMesh, meshIndex); + + const indices = babylonMesh.isUnIndexed ? null : babylonMesh.getIndices(); + const vertexBuffers = babylonMesh.geometry?.getVertexBuffers(); + const morphTargets = state.getMorphTargetsFromMesh(babylonMesh); + + let isLinesMesh = false; + + if (babylonMesh instanceof LinesMesh) { + isLinesMesh = true; } - } - /** - * Stores an Int16 in the array buffer - * @param entry - * @param byteOffset If defined, specifies where to set the value as an offset. - */ - public setInt16(entry: number, byteOffset?: number) { - if (byteOffset != null) { - if (byteOffset < this._byteOffset) { - this._dataView.setInt16(byteOffset, entry, true); - } else { - Tools.Error("BinaryWriter: byteoffset is greater than the current binary buffer length!"); - } - } else { - if (this._byteOffset + 2 > this._arrayBuffer.byteLength) { - this._resizeBuffer(this._arrayBuffer.byteLength * 2); + + const subMeshes = babylonMesh.subMeshes; + if (vertexBuffers && subMeshes && subMeshes.length > 0) { + for (const subMesh of subMeshes) { + const primitive: IMeshPrimitive = { attributes: {} }; + + const babylonMaterial = subMesh.getMaterial() || this._babylonScene.defaultMaterial; + + // Special case for LinesMesh + if (isLinesMesh) { + const material: IMaterial = { + name: babylonMaterial.name, + }; + + const babylonLinesMesh = babylonMesh as LinesMesh; + + if (!babylonLinesMesh.color.equals(Color3.White()) || babylonLinesMesh.alpha < 1) { + material.pbrMetallicRoughness = { + baseColorFactor: [...babylonLinesMesh.color.asArray(), babylonLinesMesh.alpha], + }; + } + + this._materials.push(material); + primitive.material = this._materials.length - 1; + } else { + // Material + await this._exportMaterialAsync(babylonMaterial, vertexBuffers, subMesh, primitive); + } + + // Index buffer + const fillMode = isLinesMesh ? Material.LineListDrawMode : (babylonMesh.overrideRenderingFillMode ?? babylonMaterial.fillMode); + + const sideOrientation = babylonMaterial._getEffectiveOrientation(babylonMesh); + + this._exportIndices(indices, subMesh.indexStart, subMesh.indexCount, -subMesh.verticesStart, fillMode, sideOrientation, state, primitive); + + // Vertex buffers + for (const vertexBuffer of Object.values(vertexBuffers)) { + 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); + } + } } - this._dataView.setInt16(this._byteOffset, entry, true); - this._byteOffset += 2; } - } - /** - * Stores a byte in the array buffer - * @param entry - * @param byteOffset If defined, specifies where to set the value as an offset. - */ - public setByte(entry: number, byteOffset?: number) { - if (byteOffset != null) { - if (byteOffset < this._byteOffset) { - this._dataView.setInt8(byteOffset, entry); - } else { - Tools.Error("BinaryWriter: byteoffset is greater than the current binary buffer length!"); + + if (morphTargets) { + mesh.weights = []; + + if (!mesh.extras) { + mesh.extras = {}; } - } else { - if (this._byteOffset + 1 > this._arrayBuffer.byteLength) { - this._resizeBuffer(this._arrayBuffer.byteLength * 2); + mesh.extras.targetNames = []; + + for (const gltfMorphTarget of morphTargets) { + mesh.weights.push(gltfMorphTarget.influence); + mesh.extras.targetNames.push(gltfMorphTarget.name); } - this._dataView.setInt8(this._byteOffset, entry); - this._byteOffset++; } + + return meshIndex; } } diff --git a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts index 4a52c97fa5e..91a0b8e6513 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFExporterExtension.ts @@ -6,10 +6,10 @@ import type { Texture } from "core/Materials/Textures/texture"; import type { SubMesh } from "core/Meshes/subMesh"; import type { IDisposable } from "core/scene"; -import type { _BinaryWriter } from "./glTFExporter"; 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"; /** @internal */ // eslint-disable-next-line no-var, @typescript-eslint/naming-convention @@ -27,7 +27,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo * @param mimeType The mime-type of the generated image * @returns A promise that resolves with the exported texture */ - preExportTextureAsync?(context: string, babylonTexture: Nullable, mimeType: ImageMimeType): Promise>; + preExportTextureAsync?(context: string, babylonTexture: Texture, mimeType: ImageMimeType): Promise>; /** * Define this method to get notified when a texture info is created @@ -38,23 +38,31 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo postExportTexture?(context: string, textureInfo: ITextureInfo, babylonTexture: BaseTexture): void; /** - * Define this method to modify the default behavior when exporting texture info + * 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 - * @param binaryWriter glTF serializer binary writer instance * @returns nullable IMeshPrimitive promise */ - postExportMeshPrimitiveAsync?(context: string, meshPrimitive: Nullable, babylonSubMesh: SubMesh, binaryWriter: _BinaryWriter): Promise; + postExportMeshPrimitiveAsync?(context: string, meshPrimitive: IMeshPrimitive, babylonSubMesh: SubMesh): Promise; /** * Define this method to modify the default behavior when exporting a node * @param context The context when exporting the node * @param node glTF node * @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 * @returns nullable INode promise */ - postExportNodeAsync?(context: string, node: Nullable, babylonNode: Node, nodeMap: { [key: number]: number }, binaryWriter: _BinaryWriter): Promise>; + postExportNodeAsync?( + context: string, + node: INode, + babylonNode: Node, + nodeMap: Map, + convertToRightHanded: boolean, + dataWriter: DataWriter + ): Promise>; /** * Define this method to modify the default behavior when exporting a material @@ -62,7 +70,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo * @param babylonMaterial BabylonJS material * @returns nullable IMaterial promise */ - postExportMaterialAsync?(context: string, node: Nullable, babylonMaterial: Material): Promise; + postExportMaterialAsync?(context: string, node: IMaterial, babylonMaterial: Material): Promise; /** * Define this method to return additional textures to export from a material diff --git a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts index 1c37db9c5d5..ff32e35db70 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFMaterialExporter.ts @@ -1,4 +1,6 @@ -import type { ITextureInfo, IMaterial, IMaterialPbrMetallicRoughness, IMaterialOcclusionTextureInfo, ISampler, IMaterialExtension } from "babylonjs-gltf2interface"; +/* eslint-disable babylonjs/available */ + +import type { ITextureInfo, IMaterial, IMaterialPbrMetallicRoughness, IMaterialOcclusionTextureInfo, ISampler } from "babylonjs-gltf2interface"; import { ImageMimeType, MaterialAlphaMode, TextureMagFilter, TextureMinFilter, TextureWrapMode } from "babylonjs-gltf2interface"; import type { Nullable } from "core/types"; @@ -13,64 +15,42 @@ import { RawTexture } from "core/Materials/Textures/rawTexture"; import type { Scene } from "core/scene"; -import type { _Exporter } from "./glTFExporter"; +import type { GLTFExporter } from "./glTFExporter"; import { Constants } from "core/Engines/constants"; import { DumpTools } from "core/Misc/dumpTools"; import type { Material } from "core/Materials/material"; import type { StandardMaterial } from "core/Materials/standardMaterial"; import type { PBRBaseMaterial } from "core/Materials/PBR/pbrBaseMaterial"; -import type { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; + +const epsilon = 1e-6; +const dielectricSpecular = new Color3(0.04, 0.04, 0.04); +const maxSpecularPower = 1024; +const white = Color3.White(); +const black = Color3.Black(); /** * Interface for storing specular glossiness factors * @internal */ -// eslint-disable-next-line @typescript-eslint/naming-convention -interface _IPBRSpecularGlossiness { +interface IPBRSpecularGlossiness { /** * Represents the linear diffuse factors of the material */ diffuseColor: Color3; - /** - * Represents the linear specular factors of the material - */ specularColor: Color3; - /** - * Represents the smoothness of the material - */ glossiness: number; } -/** - * Interface for storing metallic roughness factors - * @internal - */ -// eslint-disable-next-line @typescript-eslint/naming-convention -interface _IPBRMetallicRoughness { - /** - * Represents the albedo color of the material - */ +interface IPBRMetallicRoughness { baseColor: Color3; - /** - * Represents the metalness of the material - */ metallic: Nullable; - /** - * Represents the roughness of the material - */ roughness: Nullable; - /** - * The metallic roughness texture data - */ metallicRoughnessTextureData?: Nullable; - /** - * The base color texture data - */ baseColorTextureData?: Nullable; } -function getFileExtensionFromMimeType(mimeType: ImageMimeType): string { +function GetFileExtensionFromMimeType(mimeType: ImageMimeType): string { switch (mimeType) { case ImageMimeType.JPEG: return ".jpg"; @@ -84,240 +64,85 @@ function getFileExtensionFromMimeType(mimeType: ImageMimeType): string { } /** - * Utility methods for working with glTF material conversion properties. This class should only be used internally + * Computes the metallic factor. + * @param diffuse diffused value + * @param specular specular value + * @param oneMinusSpecularStrength one minus the specular strength + * @returns metallic value * @internal */ -export class _GLTFMaterialExporter { - /** - * Represents the dielectric specular values for R, G and B - */ - private static readonly _DielectricSpecular: Color3 = new Color3(0.04, 0.04, 0.04); - - /** - * Allows the maximum specular power to be defined for material calculations - */ - private static readonly _MaxSpecularPower = 1024; - - /** - * Mapping to store textures - */ - private _textureMap: { [textureId: string]: ITextureInfo } = {}; - - // Mapping of internal textures to images to avoid exporting duplicate images. - private _internalTextureToImage: { [uniqueId: number]: { [mimeType: string]: Promise } } = {}; - - /** - * Numeric tolerance value - */ - private static readonly _Epsilon = 1e-6; - - /** - * Reference to the glTF Exporter - */ - private _exporter: _Exporter; - - constructor(exporter: _Exporter) { - this._textureMap = {}; - this._exporter = exporter; - } - - /** - * Specifies if two colors are approximately equal in value - * @param color1 first color to compare to - * @param color2 second color to compare to - * @param epsilon threshold value - * @returns boolean specifying if the colors are approximately equal in value - */ - private static _FuzzyEquals(color1: Color3, color2: Color3, epsilon: number): boolean { - return Scalar.WithinEpsilon(color1.r, color2.r, epsilon) && Scalar.WithinEpsilon(color1.g, color2.g, epsilon) && Scalar.WithinEpsilon(color1.b, color2.b, epsilon); +export function _SolveMetallic(diffuse: number, specular: number, oneMinusSpecularStrength: number): number { + if (specular < dielectricSpecular.r) { + dielectricSpecular; + return 0; } - /** - * Gets the materials from a Babylon scene and converts them to glTF materials - * @param exportMaterials - * @param mimeType texture mime type - * @param hasTextureCoords specifies if texture coordinates are present on the material - * @returns promise that resolves after all materials have been converted - */ - public _convertMaterialsToGLTFAsync(exportMaterials: Set, mimeType: ImageMimeType, hasTextureCoords: boolean) { - const promises: Promise[] = []; - exportMaterials.forEach((material) => { - if (material.getClassName() === "StandardMaterial") { - promises.push(this._convertStandardMaterialAsync(material as StandardMaterial, mimeType, hasTextureCoords)); - } else if (material.getClassName().indexOf("PBR") !== -1) { - promises.push(this._convertPBRMaterialAsync(material as PBRMaterial, mimeType, hasTextureCoords)); - } else { - Tools.Warn(`Unsupported material type: ${material.name}`); - } - }); - - return Promise.all(promises).then(() => { - /* do nothing */ - }); - } + const a = dielectricSpecular.r; + const b = (diffuse * oneMinusSpecularStrength) / (1.0 - dielectricSpecular.r) + specular - 2.0 * dielectricSpecular.r; + const c = dielectricSpecular.r - specular; + const d = b * b - 4.0 * a * c; + return Scalar.Clamp((-b + Math.sqrt(d)) / (2.0 * a), 0, 1); +} - /** - * Makes a copy of the glTF material without the texture parameters - * @param originalMaterial original glTF material - * @returns glTF material without texture parameters - */ - public _stripTexturesFromMaterial(originalMaterial: IMaterial): IMaterial { - const newMaterial: IMaterial = {}; - if (originalMaterial) { - newMaterial.name = originalMaterial.name; - newMaterial.doubleSided = originalMaterial.doubleSided; - newMaterial.alphaMode = originalMaterial.alphaMode; - newMaterial.alphaCutoff = originalMaterial.alphaCutoff; - newMaterial.emissiveFactor = originalMaterial.emissiveFactor; - const originalPBRMetallicRoughness = originalMaterial.pbrMetallicRoughness; - if (originalPBRMetallicRoughness) { - newMaterial.pbrMetallicRoughness = {}; - newMaterial.pbrMetallicRoughness.baseColorFactor = originalPBRMetallicRoughness.baseColorFactor; - newMaterial.pbrMetallicRoughness.metallicFactor = originalPBRMetallicRoughness.metallicFactor; - newMaterial.pbrMetallicRoughness.roughnessFactor = originalPBRMetallicRoughness.roughnessFactor; - } - } - return newMaterial; +/** + * Sets the glTF alpha mode to a glTF material from the Babylon Material + * @param glTFMaterial glTF material + * @param babylonMaterial Babylon material + */ +function SetAlphaMode(glTFMaterial: IMaterial, babylonMaterial: Material & { alphaCutOff?: number }): void { + if (babylonMaterial.needAlphaBlending()) { + glTFMaterial.alphaMode = MaterialAlphaMode.BLEND; + } else if (babylonMaterial.needAlphaTesting()) { + glTFMaterial.alphaMode = MaterialAlphaMode.MASK; + glTFMaterial.alphaCutoff = babylonMaterial.alphaCutOff; } +} - /** - * Specifies if the material has any texture parameters present - * @param material glTF Material - * @returns boolean specifying if texture parameters are present - */ - public _hasTexturesPresent(material: IMaterial): boolean { - if (material.emissiveTexture || material.normalTexture || material.occlusionTexture) { - return true; - } - const pbrMat = material.pbrMetallicRoughness; - if (pbrMat) { - if (pbrMat.baseColorTexture || pbrMat.metallicRoughnessTexture) { - return true; - } - } - - if (material.extensions) { - for (const extension in material.extensions) { - const extensionObject = material.extensions[extension]; - if (extensionObject as IMaterialExtension) { - return extensionObject.hasTextures?.(); - } - } - } +function CreateWhiteTexture(width: number, height: number, scene: Scene): Texture { + const data = new Uint8Array(width * height * 4); - return false; + for (let i = 0; i < data.length; i = i + 4) { + data[i] = data[i + 1] = data[i + 2] = data[i + 3] = 0xff; } - public _getTextureInfo(babylonTexture: Nullable): Nullable { - if (babylonTexture) { - const textureUid = babylonTexture.uid; - if (textureUid in this._textureMap) { - return this._textureMap[textureUid]; - } - } - return null; - } + const rawTexture = RawTexture.CreateRGBATexture(data, width, height, scene); - /** - * Converts a Babylon StandardMaterial to a glTF Metallic Roughness Material - * @param babylonStandardMaterial - * @returns glTF Metallic Roughness Material representation - */ - public _convertToGLTFPBRMetallicRoughness(babylonStandardMaterial: StandardMaterial): IMaterialPbrMetallicRoughness { - // Defines a cubic bezier curve where x is specular power and y is roughness - const P0 = new Vector2(0, 1); - const P1 = new Vector2(0, 0.1); - const P2 = new Vector2(0, 0.1); - const P3 = new Vector2(1300, 0.1); - - /** - * Given the control points, solve for x based on a given t for a cubic bezier curve - * @param t a value between 0 and 1 - * @param p0 first control point - * @param p1 second control point - * @param p2 third control point - * @param p3 fourth control point - * @returns number result of cubic bezier curve at the specified t - */ - function cubicBezierCurve(t: number, p0: number, p1: number, p2: number, p3: number): number { - return (1 - t) * (1 - t) * (1 - t) * p0 + 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t * p3; - } + return rawTexture; +} - /** - * Evaluates a specified specular power value to determine the appropriate roughness value, - * based on a pre-defined cubic bezier curve with specular on the abscissa axis (x-axis) - * and roughness on the ordinant axis (y-axis) - * @param specularPower specular power of standard material - * @returns Number representing the roughness value - */ - function solveForRoughness(specularPower: number): number { - // Given P0.x = 0, P1.x = 0, P2.x = 0 - // x = t * t * t * P3.x - // t = (x / P3.x)^(1/3) - const t = Math.pow(specularPower / P3.x, 0.333333); - return cubicBezierCurve(t, P0.y, P1.y, P2.y, P3.y); +function ConvertPixelArrayToFloat32(pixels: ArrayBufferView): Float32Array { + if (pixels instanceof Uint8Array) { + const length = pixels.length; + const buffer = new Float32Array(pixels.length); + for (let i = 0; i < length; ++i) { + buffer[i] = pixels[i] / 255; } - - const diffuse = babylonStandardMaterial.diffuseColor.toLinearSpace(babylonStandardMaterial.getScene().getEngine().useExactSrgbConversions).scale(0.5); - const opacity = babylonStandardMaterial.alpha; - const specularPower = Scalar.Clamp(babylonStandardMaterial.specularPower, 0, _GLTFMaterialExporter._MaxSpecularPower); - - const roughness = solveForRoughness(specularPower); - - const glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness = { - baseColorFactor: [diffuse.r, diffuse.g, diffuse.b, opacity], - metallicFactor: 0, - roughnessFactor: roughness, - }; - - return glTFPbrMetallicRoughness; + return buffer; + } else if (pixels instanceof Float32Array) { + return pixels; + } else { + throw new Error("Unsupported pixel format!"); } +} - /** - * Computes the metallic factor - * @param diffuse diffused value - * @param specular specular value - * @param oneMinusSpecularStrength one minus the specular strength - * @returns metallic value - */ - public static _SolveMetallic(diffuse: number, specular: number, oneMinusSpecularStrength: number): number { - if (specular < this._DielectricSpecular.r) { - this._DielectricSpecular; - return 0; - } +/** + * Utility methods for working with glTF material conversion properties. + * @internal + */ +export class GLTFMaterialExporter { + // Mapping to store textures + private _textureMap = new Map(); - const a = this._DielectricSpecular.r; - const b = (diffuse * oneMinusSpecularStrength) / (1.0 - this._DielectricSpecular.r) + specular - 2.0 * this._DielectricSpecular.r; - const c = this._DielectricSpecular.r - specular; - const D = b * b - 4.0 * a * c; - return Scalar.Clamp((-b + Math.sqrt(D)) / (2.0 * a), 0, 1); - } + // Mapping of internal textures to images to avoid exporting duplicate images + private _internalTextureToImage: { [uniqueId: number]: { [mimeType: string]: Promise } } = {}; - /** - * Sets the glTF alpha mode to a glTF material from the Babylon Material - * @param glTFMaterial glTF material - * @param babylonMaterial Babylon material - */ - private static _SetAlphaMode(glTFMaterial: IMaterial, babylonMaterial: Material & { alphaCutOff: number }): void { - if (babylonMaterial.needAlphaBlending()) { - glTFMaterial.alphaMode = MaterialAlphaMode.BLEND; - } else if (babylonMaterial.needAlphaTesting()) { - glTFMaterial.alphaMode = MaterialAlphaMode.MASK; - glTFMaterial.alphaCutoff = babylonMaterial.alphaCutOff; - } + constructor(private readonly _exporter: GLTFExporter) {} + + public getTextureInfo(babylonTexture: Nullable): Nullable { + return babylonTexture ? (this._textureMap.get(babylonTexture) ?? null) : null; } - /** - * Converts a Babylon Standard Material to a glTF Material - * @param babylonStandardMaterial BJS Standard Material - * @param mimeType mime type to use for the textures - * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied - * @returns promise, resolved with the material - */ - public _convertStandardMaterialAsync(babylonStandardMaterial: StandardMaterial, mimeType: ImageMimeType, hasTextureCoords: boolean): Promise { - const materialMap = this._exporter._materialMap; - const materials = this._exporter._materials; - const promises = []; + public async exportStandardMaterialAsync(babylonStandardMaterial: StandardMaterial, mimeType: ImageMimeType, hasUVs: boolean): Promise { const pbrMetallicRoughness = this._convertToGLTFPBRMetallicRoughness(babylonStandardMaterial); const material: IMaterial = { name: babylonStandardMaterial.name }; @@ -327,20 +152,25 @@ export class _GLTFMaterialExporter { } material.doubleSided = true; } - if (hasTextureCoords) { - if (babylonStandardMaterial.diffuseTexture) { + + if (hasUVs) { + const promises: Promise[] = []; + + const diffuseTexture = babylonStandardMaterial.diffuseTexture; + if (diffuseTexture) { promises.push( - this._exportTextureAsync(babylonStandardMaterial.diffuseTexture, mimeType).then((textureInfo) => { + this.exportTextureAsync(diffuseTexture, mimeType).then((textureInfo) => { if (textureInfo) { pbrMetallicRoughness.baseColorTexture = textureInfo; } }) ); } + const bumpTexture = babylonStandardMaterial.bumpTexture; if (bumpTexture) { promises.push( - this._exportTextureAsync(bumpTexture, mimeType).then((textureInfo) => { + this.exportTextureAsync(bumpTexture, mimeType).then((textureInfo) => { if (textureInfo) { material.normalTexture = textureInfo; if (bumpTexture.level !== 1) { @@ -350,20 +180,24 @@ export class _GLTFMaterialExporter { }) ); } - if (babylonStandardMaterial.emissiveTexture) { + + const emissiveTexture = babylonStandardMaterial.emissiveTexture; + if (emissiveTexture) { material.emissiveFactor = [1.0, 1.0, 1.0]; promises.push( - this._exportTextureAsync(babylonStandardMaterial.emissiveTexture, mimeType).then((textureInfo) => { + this.exportTextureAsync(emissiveTexture, mimeType).then((textureInfo) => { if (textureInfo) { material.emissiveTexture = textureInfo; } }) ); } - if (babylonStandardMaterial.ambientTexture) { + + const ambientTexture = babylonStandardMaterial.ambientTexture; + if (ambientTexture) { promises.push( - this._exportTextureAsync(babylonStandardMaterial.ambientTexture, mimeType).then((textureInfo) => { + this.exportTextureAsync(ambientTexture, mimeType).then((textureInfo) => { if (textureInfo) { const occlusionTexture: IMaterialOcclusionTextureInfo = { index: textureInfo.index, @@ -373,6 +207,11 @@ export class _GLTFMaterialExporter { }) ); } + + if (promises.length > 0) { + this._exporter._materialNeedsUVsSet.add(babylonStandardMaterial); + await Promise.all(promises); + } } if (babylonStandardMaterial.alpha < 1.0 || babylonStandardMaterial.opacityTexture) { @@ -382,53 +221,85 @@ export class _GLTFMaterialExporter { Tools.Warn(babylonStandardMaterial.name + ": glTF 2.0 does not support alpha mode: " + babylonStandardMaterial.alphaMode.toString()); } } - if (babylonStandardMaterial.emissiveColor && !_GLTFMaterialExporter._FuzzyEquals(babylonStandardMaterial.emissiveColor, Color3.Black(), _GLTFMaterialExporter._Epsilon)) { + + if (babylonStandardMaterial.emissiveColor && !babylonStandardMaterial.emissiveColor.equalsWithEpsilon(black, epsilon)) { material.emissiveFactor = babylonStandardMaterial.emissiveColor.asArray(); } material.pbrMetallicRoughness = pbrMetallicRoughness; - _GLTFMaterialExporter._SetAlphaMode(material, babylonStandardMaterial); + SetAlphaMode(material, babylonStandardMaterial); + + await this._finishMaterialAsync(material, babylonStandardMaterial, mimeType); + const materials = this._exporter._materials; materials.push(material); - materialMap[babylonStandardMaterial.uniqueId] = materials.length - 1; + return materials.length - 1; + } + + private _convertToGLTFPBRMetallicRoughness(babylonStandardMaterial: StandardMaterial): IMaterialPbrMetallicRoughness { + // Defines a cubic bezier curve where x is specular power and y is roughness + const P0 = new Vector2(0, 1); + const P1 = new Vector2(0, 0.1); + const P2 = new Vector2(0, 0.1); + const P3 = new Vector2(1300, 0.1); + + /** + * Given the control points, solve for x based on a given t for a cubic bezier curve + * @param t a value between 0 and 1 + * @param p0 first control point + * @param p1 second control point + * @param p2 third control point + * @param p3 fourth control point + * @returns number result of cubic bezier curve at the specified t + */ + function cubicBezierCurve(t: number, p0: number, p1: number, p2: number, p3: number): number { + return (1 - t) * (1 - t) * (1 - t) * p0 + 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t * p3; + } + + /** + * Evaluates a specified specular power value to determine the appropriate roughness value, + * based on a pre-defined cubic bezier curve with specular on the abscissa axis (x-axis) + * and roughness on the ordinant axis (y-axis) + * @param specularPower specular power of standard material + * @returns Number representing the roughness value + */ + function solveForRoughness(specularPower: number): number { + // Given P0.x = 0, P1.x = 0, P2.x = 0 + // x = t * t * t * P3.x + // t = (x / P3.x)^(1/3) + const t = Math.pow(specularPower / P3.x, 0.333333); + return cubicBezierCurve(t, P0.y, P1.y, P2.y, P3.y); + } - return this._finishMaterial(promises, material, babylonStandardMaterial, mimeType); + const diffuse = babylonStandardMaterial.diffuseColor.toLinearSpace(babylonStandardMaterial.getScene().getEngine().useExactSrgbConversions).scale(0.5); + const opacity = babylonStandardMaterial.alpha; + const specularPower = Scalar.Clamp(babylonStandardMaterial.specularPower, 0, maxSpecularPower); + + const roughness = solveForRoughness(specularPower); + + const glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness = { + baseColorFactor: [diffuse.r, diffuse.g, diffuse.b, opacity], + metallicFactor: 0, + roughnessFactor: roughness, + }; + + return glTFPbrMetallicRoughness; } - private _finishMaterial(promises: Promise[], glTFMaterial: IMaterial, babylonMaterial: Material, mimeType: ImageMimeType) { - return Promise.all(promises).then(() => { - const textures = this._exporter._extensionsPostExportMaterialAdditionalTextures("exportMaterial", glTFMaterial, babylonMaterial); - let tasks: Nullable>[]> = null; + private async _finishMaterialAsync(glTFMaterial: IMaterial, babylonMaterial: Material, mimeType: ImageMimeType): Promise { + const textures = this._exporter._extensionsPostExportMaterialAdditionalTextures("exportMaterial", glTFMaterial, babylonMaterial); - for (const texture of textures) { - if (!tasks) { - tasks = []; - } - tasks.push(this._exportTextureAsync(texture, mimeType)); - } + const promises: Array>> = []; - if (!tasks) { - tasks = [Promise.resolve(null)]; - } + for (const texture of textures) { + promises.push(this.exportTextureAsync(texture, mimeType)); + } - return Promise.all(tasks).then(() => { - const extensionWork = this._exporter._extensionsPostExportMaterialAsync("exportMaterial", glTFMaterial, babylonMaterial); - if (!extensionWork) { - return glTFMaterial; - } - return extensionWork.then(() => glTFMaterial); - }); - }); + await Promise.all(promises); + + await this._exporter._extensionsPostExportMaterialAsync("exportMaterial", glTFMaterial, babylonMaterial); } - /** - * Converts an image typed array buffer to a base64 image - * @param buffer typed array buffer - * @param width width of the image - * @param height height of the image - * @param mimeType mimetype of the image - * @returns base64 image string - */ private async _getImageDataAsync(buffer: Uint8Array | Float32Array, width: number, height: number, mimeType: ImageMimeType): Promise { const textureType = Constants.TEXTURETYPE_UNSIGNED_INT; @@ -445,25 +316,6 @@ export class _GLTFMaterialExporter { return (await DumpTools.DumpDataAsync(width, height, data, mimeType, undefined, true, true)) as ArrayBuffer; } - /** - * Generates a white texture based on the specified width and height - * @param width width of the texture in pixels - * @param height height of the texture in pixels - * @param scene babylonjs scene - * @returns white texture - */ - private _createWhiteTexture(width: number, height: number, scene: Scene): Texture { - const data = new Uint8Array(width * height * 4); - - for (let i = 0; i < data.length; i = i + 4) { - data[i] = data[i + 1] = data[i + 2] = data[i + 3] = 0xff; - } - - const rawTexture = RawTexture.CreateRGBATexture(data, width, height, scene); - - return rawTexture; - } - /** * Resizes the two source textures to the same dimensions. If a texture is null, a default white texture is generated. If both textures are null, returns null * @param texture1 first texture to resize @@ -481,14 +333,14 @@ export class _GLTFMaterialExporter { if (texture1 && texture1 instanceof Texture) { resizedTexture1 = TextureTools.CreateResizedCopy(texture1, texture2Size.width, texture2Size.height, true); } else { - resizedTexture1 = this._createWhiteTexture(texture2Size.width, texture2Size.height, scene); + resizedTexture1 = CreateWhiteTexture(texture2Size.width, texture2Size.height, scene); } resizedTexture2 = texture2!; } else if (texture1Size.width > texture2Size.width) { if (texture2 && texture2 instanceof Texture) { resizedTexture2 = TextureTools.CreateResizedCopy(texture2, texture1Size.width, texture1Size.height, true); } else { - resizedTexture2 = this._createWhiteTexture(texture1Size.width, texture1Size.height, scene); + resizedTexture2 = CreateWhiteTexture(texture1Size.width, texture1Size.height, scene); } resizedTexture1 = texture1!; } else { @@ -502,31 +354,10 @@ export class _GLTFMaterialExporter { }; } - /** - * Converts an array of pixels to a Float32Array - * Throws an error if the pixel format is not supported - * @param pixels - array buffer containing pixel values - * @returns Float32 of pixels - */ - private _convertPixelArrayToFloat32(pixels: ArrayBufferView): Float32Array { - if (pixels instanceof Uint8Array) { - const length = pixels.length; - const buffer = new Float32Array(pixels.length); - for (let i = 0; i < length; ++i) { - buffer[i] = pixels[i] / 255; - } - return buffer; - } else if (pixels instanceof Float32Array) { - return pixels; - } else { - throw new Error("Unsupported pixel format!"); - } - } - /** * Convert Specular Glossiness Textures to Metallic Roughness * See link below for info on the material conversions from PBR Metallic/Roughness and Specular/Glossiness - * @link https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness/examples/convert-between-workflows-bjs/js/babylon.pbrUtilities.js + * @see https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness/examples/convert-between-workflows-bjs/js/babylon.pbrUtilities.js * @param diffuseTexture texture used to store diffuse information * @param specularGlossinessTexture texture used to store specular and glossiness information * @param factors specular glossiness material factors @@ -536,12 +367,12 @@ export class _GLTFMaterialExporter { private async _convertSpecularGlossinessTexturesToMetallicRoughnessAsync( diffuseTexture: Nullable, specularGlossinessTexture: Nullable, - factors: _IPBRSpecularGlossiness, + factors: IPBRSpecularGlossiness, mimeType: ImageMimeType - ): Promise<_IPBRMetallicRoughness> { + ): Promise { const promises = new Array>(); if (!(diffuseTexture || specularGlossinessTexture)) { - return Promise.reject("_ConvertSpecularGlosinessTexturesToMetallicRoughness: diffuse and specular glossiness textures are not defined!"); + return Promise.reject("diffuse and specular glossiness textures are not defined!"); } const scene: Nullable = diffuseTexture ? diffuseTexture.getScene() : specularGlossinessTexture ? specularGlossinessTexture.getScene() : null; @@ -560,12 +391,12 @@ export class _GLTFMaterialExporter { const specularPixels = await resizedTextures.texture2.readPixels(); if (diffusePixels) { - diffuseBuffer = this._convertPixelArrayToFloat32(diffusePixels); + diffuseBuffer = ConvertPixelArrayToFloat32(diffusePixels); } else { return Promise.reject("Failed to retrieve pixels from diffuse texture!"); } if (specularPixels) { - specularGlossinessBuffer = this._convertPixelArrayToFloat32(specularPixels); + specularGlossinessBuffer = ConvertPixelArrayToFloat32(specularPixels); } else { return Promise.reject("Failed to retrieve pixels from specular glossiness texture!"); } @@ -576,7 +407,7 @@ export class _GLTFMaterialExporter { const baseColorBuffer = new Uint8Array(byteLength); const strideSize = 4; - const maxBaseColor = Color3.Black(); + const maxBaseColor = black; let maxMetallic = 0; let maxRoughness = 0; @@ -592,7 +423,7 @@ export class _GLTFMaterialExporter { .multiply(factors.specularColor); const glossiness = specularGlossinessBuffer[offset + 3] * factors.glossiness; - const specularGlossiness: _IPBRSpecularGlossiness = { + const specularGlossiness: IPBRSpecularGlossiness = { diffuseColor: diffuseColor, specularColor: specularColor, glossiness: glossiness, @@ -618,7 +449,7 @@ export class _GLTFMaterialExporter { } // Retrieves the metallic roughness factors from the maximum texture values. - const metallicRoughnessFactors: _IPBRMetallicRoughness = { + const metallicRoughnessFactors: IPBRMetallicRoughness = { baseColor: maxBaseColor, metallic: maxMetallic, roughness: maxRoughness, @@ -631,9 +462,9 @@ export class _GLTFMaterialExporter { for (let w = 0; w < width; ++w) { const destinationOffset = (width * h + w) * strideSize; - baseColorBuffer[destinationOffset] /= metallicRoughnessFactors.baseColor.r > _GLTFMaterialExporter._Epsilon ? metallicRoughnessFactors.baseColor.r : 1; - baseColorBuffer[destinationOffset + 1] /= metallicRoughnessFactors.baseColor.g > _GLTFMaterialExporter._Epsilon ? metallicRoughnessFactors.baseColor.g : 1; - baseColorBuffer[destinationOffset + 2] /= metallicRoughnessFactors.baseColor.b > _GLTFMaterialExporter._Epsilon ? metallicRoughnessFactors.baseColor.b : 1; + baseColorBuffer[destinationOffset] /= metallicRoughnessFactors.baseColor.r > epsilon ? metallicRoughnessFactors.baseColor.r : 1; + baseColorBuffer[destinationOffset + 1] /= metallicRoughnessFactors.baseColor.g > epsilon ? metallicRoughnessFactors.baseColor.g : 1; + baseColorBuffer[destinationOffset + 2] /= metallicRoughnessFactors.baseColor.b > epsilon ? metallicRoughnessFactors.baseColor.b : 1; const linearBaseColorPixel = Color3.FromInts( baseColorBuffer[destinationOffset], @@ -645,17 +476,16 @@ export class _GLTFMaterialExporter { baseColorBuffer[destinationOffset + 1] = sRGBBaseColorPixel.g * 255; baseColorBuffer[destinationOffset + 2] = sRGBBaseColorPixel.b * 255; - if (!_GLTFMaterialExporter._FuzzyEquals(sRGBBaseColorPixel, Color3.White(), _GLTFMaterialExporter._Epsilon)) { + if (!sRGBBaseColorPixel.equalsWithEpsilon(white, epsilon)) { writeOutBaseColorTexture = true; } - metallicRoughnessBuffer[destinationOffset + 1] /= - metallicRoughnessFactors.roughness! > _GLTFMaterialExporter._Epsilon ? metallicRoughnessFactors.roughness! : 1; - metallicRoughnessBuffer[destinationOffset + 2] /= metallicRoughnessFactors.metallic! > _GLTFMaterialExporter._Epsilon ? metallicRoughnessFactors.metallic! : 1; + metallicRoughnessBuffer[destinationOffset + 1] /= metallicRoughnessFactors.roughness! > epsilon ? metallicRoughnessFactors.roughness! : 1; + metallicRoughnessBuffer[destinationOffset + 2] /= metallicRoughnessFactors.metallic! > epsilon ? metallicRoughnessFactors.metallic! : 1; const metallicRoughnessPixel = Color3.FromInts(255, metallicRoughnessBuffer[destinationOffset + 1], metallicRoughnessBuffer[destinationOffset + 2]); - if (!_GLTFMaterialExporter._FuzzyEquals(metallicRoughnessPixel, Color3.White(), _GLTFMaterialExporter._Epsilon)) { + if (!metallicRoughnessPixel.equalsWithEpsilon(white, epsilon)) { writeOutMetallicRoughnessTexture = true; } } @@ -689,21 +519,17 @@ export class _GLTFMaterialExporter { * @param specularGlossiness interface with specular glossiness material properties * @returns interface with metallic roughness material properties */ - private _convertSpecularGlossinessToMetallicRoughness(specularGlossiness: _IPBRSpecularGlossiness): _IPBRMetallicRoughness { + private _convertSpecularGlossinessToMetallicRoughness(specularGlossiness: IPBRSpecularGlossiness): IPBRMetallicRoughness { const diffusePerceivedBrightness = this._getPerceivedBrightness(specularGlossiness.diffuseColor); const specularPerceivedBrightness = this._getPerceivedBrightness(specularGlossiness.specularColor); const oneMinusSpecularStrength = 1 - this._getMaxComponent(specularGlossiness.specularColor); - const metallic = _GLTFMaterialExporter._SolveMetallic(diffusePerceivedBrightness, specularPerceivedBrightness, oneMinusSpecularStrength); - const baseColorFromDiffuse = specularGlossiness.diffuseColor.scale( - oneMinusSpecularStrength / (1.0 - _GLTFMaterialExporter._DielectricSpecular.r) / Math.max(1 - metallic, _GLTFMaterialExporter._Epsilon) - ); - const baseColorFromSpecular = specularGlossiness.specularColor - .subtract(_GLTFMaterialExporter._DielectricSpecular.scale(1 - metallic)) - .scale(1 / Math.max(metallic, _GLTFMaterialExporter._Epsilon)); + const metallic = _SolveMetallic(diffusePerceivedBrightness, specularPerceivedBrightness, oneMinusSpecularStrength); + const baseColorFromDiffuse = specularGlossiness.diffuseColor.scale(oneMinusSpecularStrength / (1.0 - dielectricSpecular.r) / Math.max(1 - metallic)); + const baseColorFromSpecular = specularGlossiness.specularColor.subtract(dielectricSpecular.scale(1 - metallic)).scale(1 / Math.max(metallic)); let baseColor = Color3.Lerp(baseColorFromDiffuse, baseColorFromSpecular, metallic * metallic); baseColor = baseColor.clampToRef(0, 1, baseColor); - const metallicRoughness: _IPBRMetallicRoughness = { + const metallicRoughness: IPBRMetallicRoughness = { baseColor: baseColor, metallic: metallic, roughness: 1 - specularGlossiness.glossiness, @@ -741,30 +567,28 @@ export class _GLTFMaterialExporter { * @param babylonPBRMaterial BJS PBR Metallic Roughness Material * @param mimeType mime type to use for the textures * @param glTFPbrMetallicRoughness glTF PBR Metallic Roughness interface - * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied + * @param hasUVs specifies if texture coordinates are present on the submesh to determine if textures should be applied * @returns glTF PBR Metallic Roughness factors */ - private _convertMetalRoughFactorsToMetallicRoughnessAsync( + private async _convertMetalRoughFactorsToMetallicRoughnessAsync( babylonPBRMaterial: PBRBaseMaterial, mimeType: ImageMimeType, glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness, - hasTextureCoords: boolean - ): Promise<_IPBRMetallicRoughness> { - const promises = []; - const baseColor = babylonPBRMaterial._albedoColor; - const metallic = babylonPBRMaterial._metallic; - const roughness = babylonPBRMaterial._roughness; - const metallicRoughness: _IPBRMetallicRoughness = { - baseColor: baseColor, - metallic: metallic, - roughness: roughness, + hasUVs: boolean + ): Promise { + const promises: Promise[] = []; + + const metallicRoughness: IPBRMetallicRoughness = { + baseColor: babylonPBRMaterial._albedoColor, + metallic: babylonPBRMaterial._metallic, + roughness: babylonPBRMaterial._roughness, }; - if (hasTextureCoords) { + if (hasUVs) { const albedoTexture = babylonPBRMaterial._albedoTexture; if (albedoTexture) { promises.push( - this._exportTextureAsync(babylonPBRMaterial._albedoTexture!, mimeType).then((glTFTexture) => { + this.exportTextureAsync(babylonPBRMaterial._albedoTexture!, mimeType).then((glTFTexture) => { if (glTFTexture) { glTFPbrMetallicRoughness.baseColorTexture = glTFTexture; } @@ -774,7 +598,7 @@ export class _GLTFMaterialExporter { const metallicTexture = babylonPBRMaterial._metallicTexture; if (metallicTexture) { promises.push( - this._exportTextureAsync(metallicTexture, mimeType).then((glTFTexture) => { + this.exportTextureAsync(metallicTexture, mimeType).then((glTFTexture) => { if (glTFTexture) { glTFPbrMetallicRoughness.metallicRoughnessTexture = glTFTexture; } @@ -782,9 +606,13 @@ export class _GLTFMaterialExporter { ); } } - return Promise.all(promises).then(() => { - return metallicRoughness; - }); + + if (promises.length > 0) { + this._exporter._materialNeedsUVsSet.add(babylonPBRMaterial); + await Promise.all(promises); + } + + return metallicRoughness; } private _getTextureSampler(texture: Nullable): ISampler { @@ -892,60 +720,59 @@ export class _GLTFMaterialExporter { * @param babylonPBRMaterial BJS PBR Metallic Roughness Material * @param mimeType mime type to use for the textures * @param pbrMetallicRoughness glTF PBR Metallic Roughness interface - * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied + * @param hasUVs specifies if texture coordinates are present on the submesh to determine if textures should be applied * @returns glTF PBR Metallic Roughness factors */ - private _convertSpecGlossFactorsToMetallicRoughnessAsync( + private async _convertSpecGlossFactorsToMetallicRoughnessAsync( babylonPBRMaterial: PBRBaseMaterial, mimeType: ImageMimeType, pbrMetallicRoughness: IMaterialPbrMetallicRoughness, - hasTextureCoords: boolean - ): Promise<_IPBRMetallicRoughness> { - return Promise.resolve().then(() => { - const specGloss: _IPBRSpecularGlossiness = { - diffuseColor: babylonPBRMaterial._albedoColor, - specularColor: babylonPBRMaterial._reflectivityColor, - glossiness: babylonPBRMaterial._microSurface, - }; - const albedoTexture = babylonPBRMaterial._albedoTexture; - const reflectivityTexture = babylonPBRMaterial._reflectivityTexture; - const useMicrosurfaceFromReflectivityMapAlpha = babylonPBRMaterial._useMicroSurfaceFromReflectivityMapAlpha; - if (reflectivityTexture && !useMicrosurfaceFromReflectivityMapAlpha) { - return Promise.reject("_ConvertPBRMaterial: Glossiness values not included in the reflectivity texture are currently not supported"); + hasUVs: boolean + ): Promise { + const specGloss: IPBRSpecularGlossiness = { + diffuseColor: babylonPBRMaterial._albedoColor, + specularColor: babylonPBRMaterial._reflectivityColor, + glossiness: babylonPBRMaterial._microSurface, + }; + + const albedoTexture = babylonPBRMaterial._albedoTexture; + const reflectivityTexture = babylonPBRMaterial._reflectivityTexture; + const useMicrosurfaceFromReflectivityMapAlpha = babylonPBRMaterial._useMicroSurfaceFromReflectivityMapAlpha; + if (reflectivityTexture && !useMicrosurfaceFromReflectivityMapAlpha) { + return Promise.reject("_ConvertPBRMaterial: Glossiness values not included in the reflectivity texture are currently not supported"); + } + + if ((albedoTexture || reflectivityTexture) && hasUVs) { + this._exporter._materialNeedsUVsSet.add(babylonPBRMaterial); + + const samplerIndex = this._exportTextureSampler(albedoTexture || reflectivityTexture); + const metallicRoughnessFactors = await this._convertSpecularGlossinessTexturesToMetallicRoughnessAsync(albedoTexture, reflectivityTexture, specGloss, mimeType); + + const textures = this._exporter._textures; + + if (metallicRoughnessFactors.baseColorTextureData) { + const imageIndex = this._exportImage(`baseColor${textures.length}`, mimeType, metallicRoughnessFactors.baseColorTextureData); + pbrMetallicRoughness.baseColorTexture = this._exportTextureInfo(imageIndex, samplerIndex, albedoTexture?.coordinatesIndex); } - if ((albedoTexture || reflectivityTexture) && hasTextureCoords) { - const samplerIndex = this._exportTextureSampler(albedoTexture || reflectivityTexture); - return this._convertSpecularGlossinessTexturesToMetallicRoughnessAsync(albedoTexture, reflectivityTexture, specGloss, mimeType).then((metallicRoughnessFactors) => { - const textures = this._exporter._textures; - if (metallicRoughnessFactors.baseColorTextureData) { - const imageIndex = this._exportImage(`baseColor${textures.length}`, mimeType, metallicRoughnessFactors.baseColorTextureData); - pbrMetallicRoughness.baseColorTexture = this._exportTextureInfo(imageIndex, samplerIndex, albedoTexture?.coordinatesIndex); - } - if (metallicRoughnessFactors.metallicRoughnessTextureData) { - const imageIndex = this._exportImage(`metallicRoughness${textures.length}`, mimeType, metallicRoughnessFactors.metallicRoughnessTextureData); - pbrMetallicRoughness.metallicRoughnessTexture = this._exportTextureInfo(imageIndex, samplerIndex, reflectivityTexture?.coordinatesIndex); - } - return metallicRoughnessFactors; - }); - } else { - return this._convertSpecularGlossinessToMetallicRoughness(specGloss); + if (metallicRoughnessFactors.metallicRoughnessTextureData) { + const imageIndex = this._exportImage(`metallicRoughness${textures.length}`, mimeType, metallicRoughnessFactors.metallicRoughnessTextureData); + pbrMetallicRoughness.metallicRoughnessTexture = this._exportTextureInfo(imageIndex, samplerIndex, reflectivityTexture?.coordinatesIndex); } - }); + + return metallicRoughnessFactors; + } else { + return this._convertSpecularGlossinessToMetallicRoughness(specGloss); + } } - /** - * Converts a Babylon PBR Base Material to a glTF Material - * @param babylonPBRMaterial BJS PBR Base Material - * @param mimeType mime type to use for the textures - * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied - * @returns async glTF Material representation - */ - public _convertPBRMaterialAsync(babylonPBRMaterial: PBRBaseMaterial, mimeType: ImageMimeType, hasTextureCoords: boolean): Promise { + public async exportPBRMaterialAsync(babylonPBRMaterial: PBRBaseMaterial, mimeType: ImageMimeType, hasUVs: boolean): Promise { const glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness = {}; + const glTFMaterial: IMaterial = { name: babylonPBRMaterial.name, }; + const useMetallicRoughness = babylonPBRMaterial.isMetallicWorkflow(); if (useMetallicRoughness) { @@ -954,68 +781,69 @@ export class _GLTFMaterialExporter { if (albedoColor) { glTFPbrMetallicRoughness.baseColorFactor = [albedoColor.r, albedoColor.g, albedoColor.b, alpha]; } - return this._convertMetalRoughFactorsToMetallicRoughnessAsync(babylonPBRMaterial, mimeType, glTFPbrMetallicRoughness, hasTextureCoords).then((metallicRoughness) => { - return this._setMetallicRoughnessPbrMaterial(metallicRoughness, babylonPBRMaterial, glTFMaterial, glTFPbrMetallicRoughness, mimeType, hasTextureCoords); - }); - } else { - return this._convertSpecGlossFactorsToMetallicRoughnessAsync(babylonPBRMaterial, mimeType, glTFPbrMetallicRoughness, hasTextureCoords).then((metallicRoughness) => { - return this._setMetallicRoughnessPbrMaterial(metallicRoughness, babylonPBRMaterial, glTFMaterial, glTFPbrMetallicRoughness, mimeType, hasTextureCoords); - }); } + + const metallicRoughness = useMetallicRoughness + ? await this._convertMetalRoughFactorsToMetallicRoughnessAsync(babylonPBRMaterial, mimeType, glTFPbrMetallicRoughness, hasUVs) + : await this._convertSpecGlossFactorsToMetallicRoughnessAsync(babylonPBRMaterial, mimeType, glTFPbrMetallicRoughness, hasUVs); + + await this._setMetallicRoughnessPbrMaterialAsync(metallicRoughness, babylonPBRMaterial, glTFMaterial, glTFPbrMetallicRoughness, mimeType, hasUVs); + await this._finishMaterialAsync(glTFMaterial, babylonPBRMaterial, mimeType); + + const materials = this._exporter._materials; + materials.push(glTFMaterial); + return materials.length - 1; } - private _setMetallicRoughnessPbrMaterial( - metallicRoughness: Nullable<_IPBRMetallicRoughness>, + private async _setMetallicRoughnessPbrMaterialAsync( + metallicRoughness: IPBRMetallicRoughness, babylonPBRMaterial: PBRBaseMaterial, glTFMaterial: IMaterial, glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness, mimeType: ImageMimeType, - hasTextureCoords: boolean - ): Promise { - const materialMap = this._exporter._materialMap; - const materials = this._exporter._materials; - const promises = []; - if (metallicRoughness) { - _GLTFMaterialExporter._SetAlphaMode(glTFMaterial, babylonPBRMaterial as PBRMaterial); - if ( - !( - _GLTFMaterialExporter._FuzzyEquals(metallicRoughness.baseColor, Color3.White(), _GLTFMaterialExporter._Epsilon) && - babylonPBRMaterial.alpha >= _GLTFMaterialExporter._Epsilon - ) - ) { - glTFPbrMetallicRoughness.baseColorFactor = [metallicRoughness.baseColor.r, metallicRoughness.baseColor.g, metallicRoughness.baseColor.b, babylonPBRMaterial.alpha]; - } + hasUVs: boolean + ): Promise { + SetAlphaMode(glTFMaterial, babylonPBRMaterial); - if (metallicRoughness.metallic != null && metallicRoughness.metallic !== 1) { - glTFPbrMetallicRoughness.metallicFactor = metallicRoughness.metallic; - } - if (metallicRoughness.roughness != null && metallicRoughness.roughness !== 1) { - glTFPbrMetallicRoughness.roughnessFactor = metallicRoughness.roughness; - } + if (!metallicRoughness.baseColor.equalsWithEpsilon(white, epsilon) || !Scalar.WithinEpsilon(babylonPBRMaterial.alpha, 1, epsilon)) { + glTFPbrMetallicRoughness.baseColorFactor = [metallicRoughness.baseColor.r, metallicRoughness.baseColor.g, metallicRoughness.baseColor.b, babylonPBRMaterial.alpha]; + } - if (babylonPBRMaterial.backFaceCulling != null && !babylonPBRMaterial.backFaceCulling) { - if (!babylonPBRMaterial._twoSidedLighting) { - Tools.Warn(babylonPBRMaterial.name + ": Back-face culling disabled and two-sided lighting disabled is not supported in glTF."); - } - glTFMaterial.doubleSided = true; + if (metallicRoughness.metallic != null && metallicRoughness.metallic !== 1) { + glTFPbrMetallicRoughness.metallicFactor = metallicRoughness.metallic; + } + if (metallicRoughness.roughness != null && metallicRoughness.roughness !== 1) { + glTFPbrMetallicRoughness.roughnessFactor = metallicRoughness.roughness; + } + + if (babylonPBRMaterial.backFaceCulling != null && !babylonPBRMaterial.backFaceCulling) { + if (!babylonPBRMaterial._twoSidedLighting) { + Tools.Warn(babylonPBRMaterial.name + ": Back-face culling disabled and two-sided lighting disabled is not supported in glTF."); } + glTFMaterial.doubleSided = true; + } - if (hasTextureCoords) { - const bumpTexture = babylonPBRMaterial._bumpTexture; - if (bumpTexture) { - const promise = this._exportTextureAsync(bumpTexture, mimeType).then((glTFTexture) => { + if (hasUVs) { + const promises: Promise[] = []; + + const bumpTexture = babylonPBRMaterial._bumpTexture; + if (bumpTexture) { + promises.push( + this.exportTextureAsync(bumpTexture, mimeType).then((glTFTexture) => { if (glTFTexture) { glTFMaterial.normalTexture = glTFTexture; if (bumpTexture.level !== 1) { glTFMaterial.normalTexture.scale = bumpTexture.level; } } - }); - promises.push(promise); - } - const ambientTexture = babylonPBRMaterial._ambientTexture; - if (ambientTexture) { - const promise = this._exportTextureAsync(ambientTexture, mimeType).then((glTFTexture) => { + }) + ); + } + + const ambientTexture = babylonPBRMaterial._ambientTexture; + if (ambientTexture) { + promises.push( + this.exportTextureAsync(ambientTexture, mimeType).then((glTFTexture) => { if (glTFTexture) { const occlusionTexture: IMaterialOcclusionTextureInfo = { index: glTFTexture.index, @@ -1029,30 +857,33 @@ export class _GLTFMaterialExporter { occlusionTexture.strength = ambientTextureStrength; } } - }); - promises.push(promise); - } - const emissiveTexture = babylonPBRMaterial._emissiveTexture; - if (emissiveTexture) { - const promise = this._exportTextureAsync(emissiveTexture, mimeType).then((glTFTexture) => { + }) + ); + } + + const emissiveTexture = babylonPBRMaterial._emissiveTexture; + if (emissiveTexture) { + promises.push( + this.exportTextureAsync(emissiveTexture, mimeType).then((glTFTexture) => { if (glTFTexture) { glTFMaterial.emissiveTexture = glTFTexture; } - }); - promises.push(promise); - } + }) + ); } - const emissiveColor = babylonPBRMaterial._emissiveColor; - if (!_GLTFMaterialExporter._FuzzyEquals(emissiveColor, Color3.Black(), _GLTFMaterialExporter._Epsilon)) { - glTFMaterial.emissiveFactor = emissiveColor.asArray(); + + if (promises.length > 0) { + this._exporter._materialNeedsUVsSet.add(babylonPBRMaterial); + await Promise.all(promises); } + } - glTFMaterial.pbrMetallicRoughness = glTFPbrMetallicRoughness; - materials.push(glTFMaterial); - materialMap[babylonPBRMaterial.uniqueId] = materials.length - 1; + const emissiveColor = babylonPBRMaterial._emissiveColor; + if (!emissiveColor.equalsWithEpsilon(black, epsilon)) { + glTFMaterial.emissiveFactor = emissiveColor.asArray(); } - return this._finishMaterial(promises, glTFMaterial, babylonPBRMaterial, mimeType); + glTFMaterial.pbrMetallicRoughness = glTFPbrMetallicRoughness; } private _getPixelsFromTexture(babylonTexture: BaseTexture): Promise> { @@ -1063,13 +894,7 @@ export class _GLTFMaterialExporter { return pixels; } - /** - * Extracts a texture from a Babylon texture into file data and glTF data - * @param babylonTexture Babylon texture to extract - * @param mimeType Mime Type of the babylonTexture - * @returns glTF texture info, or null if the texture format is not supported - */ - public _exportTextureAsync(babylonTexture: BaseTexture, mimeType: ImageMimeType): Promise> { + public async exportTextureAsync(babylonTexture: BaseTexture, mimeType: ImageMimeType): Promise> { const extensionPromise = this._exporter._extensionsPreExportTextureAsync("exporter", babylonTexture as Texture, mimeType); if (!extensionPromise) { return this._exportTextureInfoAsync(babylonTexture, mimeType); @@ -1083,9 +908,9 @@ export class _GLTFMaterialExporter { }); } - public async _exportTextureInfoAsync(babylonTexture: BaseTexture, mimeType: ImageMimeType): Promise> { - const textureUid = babylonTexture.uid; - if (!(textureUid in this._textureMap)) { + private async _exportTextureInfoAsync(babylonTexture: BaseTexture, mimeType: ImageMimeType): Promise> { + let textureInfo = this._textureMap.get(babylonTexture); + if (!textureInfo) { const pixels = await this._getPixelsFromTexture(babylonTexture); if (!pixels) { return null; @@ -1121,19 +946,19 @@ export class _GLTFMaterialExporter { internalTextureToImage[internalTextureUniqueId][mimeType] = imageIndexPromise; } - const textureInfo = this._exportTextureInfo(await imageIndexPromise, samplerIndex, babylonTexture.coordinatesIndex); - this._textureMap[textureUid] = textureInfo; - this._exporter._extensionsPostExportTextures("exporter", this._textureMap[textureUid], babylonTexture); + textureInfo = this._exportTextureInfo(await imageIndexPromise, samplerIndex, babylonTexture.coordinatesIndex); + this._textureMap.set(babylonTexture, textureInfo); + this._exporter._extensionsPostExportTextures("exporter", textureInfo, babylonTexture); } - return this._textureMap[textureUid]; + return textureInfo; } private _exportImage(name: string, mimeType: ImageMimeType, data: ArrayBuffer): number { const imageData = this._exporter._imageData; const baseName = name.replace(/\.\/|\/|\.\\|\\/g, "_"); - const extension = getFileExtensionFromMimeType(mimeType); + const extension = GetFileExtensionFromMimeType(mimeType); let fileName = baseName + extension; if (fileName in imageData) { fileName = `${baseName}_${Tools.RandomId()}${extension}`; diff --git a/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts b/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts new file mode 100644 index 00000000000..69257f1cb31 --- /dev/null +++ b/packages/dev/serializers/src/glTF/2.0/glTFMorphTargetsUtilities.ts @@ -0,0 +1,142 @@ +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 { CreateAccessor, CreateBufferView, NormalizeTangent } from "./glTFUtilities"; +import type { Mesh } from "core/Meshes/mesh"; +import { VertexBuffer } from "core/Buffers/buffer"; +import { Vector3 } from "core/Maths/math.vector"; +import { Tools } from "core/Misc/tools"; + +/** + * Interface to store morph target information. + * @internal + */ +export interface IMorphTargetData { + attributes: Record; + influence: number; + name: string; +} + +export function BuildMorphTargetBuffers( + morphTarget: MorphTarget, + mesh: Mesh, + dataWriter: DataWriter, + bufferViews: IBufferView[], + accessors: IAccessor[], + convertToRightHanded: boolean +): IMorphTargetData { + const result: IMorphTargetData = { + attributes: {}, + influence: morphTarget.influence, + name: morphTarget.name, + }; + + const flipX = convertToRightHanded ? -1 : 1; + const floatSize = 4; + 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 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); + const morphPosition = Vector3.FromArray(morphPositions, i * 3); + morphPosition.subtractToRef(originalPosition, difference); + difference.x *= flipX; + + min[0] = Math.min(min[0], difference.x); + max[0] = Math.max(max[0], difference.x); + + min[1] = Math.min(min[1], difference.y); + max[1] = Math.max(max[1], difference.y); + + 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); + } + + 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 })); + 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`); + } + } + + if (morphTarget.hasNormals) { + const morphNormals = morphTarget.getNormals()!; + const originalNormals = mesh.getVerticesData(VertexBuffer.NormalKind, undefined, undefined, true); + + if (originalNormals) { + 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); + } + + 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)); + 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`); + } + } + + if (morphTarget.hasTangents) { + const morphTangents = morphTarget.getTangents()!; + const originalTangents = mesh.getVerticesData(VertexBuffer.TangentKind, undefined, undefined, true); + + if (originalTangents) { + vertexCount = originalTangents.length / 4; + 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); + NormalizeTangent(originalTangent); + + // Morph target tangents omit the w component so it won't be present in the data + const morphTangent = Vector3.FromArray(morphTangents, i * 3); + NormalizeTangent(morphTangent); + + morphTangent.subtractToRef(originalTangent, difference); + dataWriter.writeFloat32(difference.x * flipX); + dataWriter.writeFloat32(difference.y); + dataWriter.writeFloat32(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)); + 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`); + } + } + + return result; +} diff --git a/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts b/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts index c346d8b61d9..1787b779fa3 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts @@ -2,7 +2,7 @@ import type { Node } from "core/node"; import type { Scene } from "core/scene"; import type { Animation } from "core/Animations/animation"; import type { GLTFData } from "./glTFData"; -import { _Exporter } from "./glTFExporter"; +import { GLTFExporter } from "./glTFExporter"; /** * Holds a collection of exporter options and parameters @@ -61,55 +61,40 @@ export interface IExportOptions { */ export class GLTF2Export { /** - * Exports the geometry of the scene to .gltf file format asynchronously - * @param scene Babylon scene with scene hierarchy information - * @param filePrefix File prefix to use when generating the glTF file + * Exports the scene to .gltf file format + * @param scene Babylon scene + * @param fileName Name to use for the .gltf file * @param options Exporter options - * @returns Returns an object with a .gltf file and associates texture names - * as keys and their data and paths as values + * @returns Returns the exported data */ - public static GLTFAsync(scene: Scene, filePrefix: string, options?: IExportOptions): Promise { - return scene.whenReadyAsync().then(() => { - const glTFPrefix = filePrefix.replace(/\.[^/.]+$/, ""); - const gltfGenerator = new _Exporter(scene, options); - return gltfGenerator._generateGLTFAsync(glTFPrefix); - }); - } + public static async GLTFAsync(scene: Scene, fileName: string, options?: IExportOptions): Promise { + if (!options || !options.exportWithoutWaitingForScene) { + await scene.whenReadyAsync(); + } - private static _PreExportAsync(scene: Scene, options?: IExportOptions): Promise { - return Promise.resolve().then(() => { - if (options && options.exportWithoutWaitingForScene) { - return Promise.resolve(); - } else { - return scene.whenReadyAsync(); - } - }); - } + const exporter = new GLTFExporter(scene, options); + const data = await exporter.generateGLTFAsync(fileName.replace(/\.[^/.]+$/, "")); + exporter.dispose(); - private static _PostExportAsync(scene: Scene, glTFData: GLTFData, options?: IExportOptions): Promise { - return Promise.resolve().then(() => { - if (options && options.exportWithoutWaitingForScene) { - return glTFData; - } else { - return glTFData; - } - }); + return data; } /** - * Exports the geometry of the scene to .glb file format asychronously - * @param scene Babylon scene with scene hierarchy information - * @param filePrefix File prefix to use when generating glb file + * Exports the scene to .glb file format + * @param scene Babylon scene + * @param fileName Name to use for the .glb file * @param options Exporter options - * @returns Returns an object with a .glb filename as key and data as value + * @returns Returns the exported data */ - public static GLBAsync(scene: Scene, filePrefix: string, options?: IExportOptions): Promise { - return this._PreExportAsync(scene, options).then(() => { - const glTFPrefix = filePrefix.replace(/\.[^/.]+$/, ""); - const gltfGenerator = new _Exporter(scene, options); - return gltfGenerator._generateGLBAsync(glTFPrefix).then((glTFData) => { - return this._PostExportAsync(scene, glTFData, options); - }); - }); + public static async GLBAsync(scene: Scene, fileName: string, options?: IExportOptions): Promise { + if (!options || !options.exportWithoutWaitingForScene) { + await scene.whenReadyAsync(); + } + + const exporter = new GLTFExporter(scene, options); + const data = await exporter.generateGLBAsync(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 a89f788042c..66eee24ccc6 100644 --- a/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts +++ b/packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts @@ -1,137 +1,423 @@ -import type { IBufferView, AccessorComponentType, IAccessor } from "babylonjs-gltf2interface"; -import { AccessorType } from "babylonjs-gltf2interface"; +/* eslint-disable jsdoc/require-jsdoc */ -import type { FloatArray, Nullable } from "core/types"; +import type { IBufferView, AccessorComponentType, IAccessor, INode } from "babylonjs-gltf2interface"; +import { AccessorType, MeshPrimitiveMode } from "babylonjs-gltf2interface"; + +import type { FloatArray, DataArray, IndicesArray, Nullable } from "core/types"; import type { Vector4 } from "core/Maths/math.vector"; -import { Vector3 } from "core/Maths/math.vector"; +import { Quaternion, TmpVectors, Matrix, Vector3 } from "core/Maths/math.vector"; +import { VertexBuffer } from "core/Buffers/buffer"; +import { Material } from "core/Materials/material"; +import { TransformNode } from "core/Meshes/transformNode"; +import { Mesh } from "core/Meshes/mesh"; +import { InstancedMesh } from "core/Meshes/instancedMesh"; +import { EnumerateFloatValues } from "core/Buffers/bufferUtils"; +import type { Node } from "core/node"; + +// Matrix that converts handedness on the X-axis. +const convertHandednessMatrix = Matrix.Compose(new Vector3(-1, 1, 1), Quaternion.Identity(), Vector3.Zero()); + +// 180 degrees rotation in Y. +const rotation180Y = new Quaternion(0, 1, 0, 0); + +// Default values for comparison. +const epsilon = 1e-6; +const defaultTranslation = Vector3.Zero(); +const defaultScale = Vector3.One(); /** - * @internal + * 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 class _GLTFUtilities { - /** - * 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 - * @param name name of the buffer view - * @returns bufferView for glTF - */ - public static _CreateBufferView(bufferIndex: number, byteOffset: number, byteLength: number, byteStride?: number, name?: string): IBufferView { - const bufferview: IBufferView = { buffer: bufferIndex, byteLength: byteLength }; - if (byteOffset) { - bufferview.byteOffset = byteOffset; - } - if (name) { - bufferview.name = name; - } - if (byteStride) { - bufferview.byteStride = byteStride; - } +export function CreateBufferView(bufferIndex: number, byteOffset: number, byteLength: number, byteStride?: number): IBufferView { + const bufferview: IBufferView = { buffer: bufferIndex, byteLength: byteLength }; - return bufferview; - } - - /** - * Creates an accessor based on the supplied arguments - * @param bufferviewIndex The index of the bufferview referenced by this accessor - * @param name The name of the 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 min Minimum value of each component in this attribute - * @param max Maximum value of each component in this attribute - * @returns accessor for glTF - */ - public static _CreateAccessor( - bufferviewIndex: number, - name: string, - type: AccessorType, - componentType: AccessorComponentType, - count: number, - byteOffset: Nullable, - min: Nullable, - max: Nullable - ): IAccessor { - const accessor: IAccessor = { name: name, bufferView: bufferviewIndex, componentType: componentType, count: count, type: type }; - - if (min != null) { - accessor.min = min; - } - if (max != null) { - accessor.max = max; + 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: + return 4; + case AccessorType.MAT3: + return 9; + case AccessorType.MAT4: + return 16; + case AccessorType.SCALAR: + return 1; + case AccessorType.VEC2: + return 2; + case AccessorType.VEC3: + return 3; + case AccessorType.VEC4: + return 4; + } +} + +export function FloatsNeed16BitInteger(floatArray: FloatArray): boolean { + return floatArray.some((value) => value >= 256); +} + +export function IsStandardVertexAttribute(type: string): boolean { + switch (type) { + case VertexBuffer.PositionKind: + case VertexBuffer.NormalKind: + case VertexBuffer.TangentKind: + case VertexBuffer.ColorKind: + case VertexBuffer.MatricesIndicesKind: + case VertexBuffer.MatricesIndicesExtraKind: + case VertexBuffer.MatricesWeightsKind: + case VertexBuffer.MatricesWeightsExtraKind: + case VertexBuffer.UVKind: + case VertexBuffer.UV2Kind: + case VertexBuffer.UV3Kind: + case VertexBuffer.UV4Kind: + case VertexBuffer.UV5Kind: + case VertexBuffer.UV6Kind: + return true; + } + return false; +} + +export function GetAccessorType(kind: string, hasVertexColorAlpha: boolean): AccessorType { + if (kind == VertexBuffer.ColorKind) { + return hasVertexColorAlpha ? AccessorType.VEC4 : AccessorType.VEC3; + } + + switch (kind) { + case VertexBuffer.PositionKind: + case VertexBuffer.NormalKind: + return AccessorType.VEC3; + case VertexBuffer.TangentKind: + case VertexBuffer.MatricesIndicesKind: + case VertexBuffer.MatricesIndicesExtraKind: + case VertexBuffer.MatricesWeightsKind: + case VertexBuffer.MatricesWeightsExtraKind: + return AccessorType.VEC4; + case VertexBuffer.UVKind: + case VertexBuffer.UV2Kind: + case VertexBuffer.UV3Kind: + case VertexBuffer.UV4Kind: + case VertexBuffer.UV5Kind: + case VertexBuffer.UV6Kind: + return AccessorType.VEC2; + } + + throw new Error(`Unknown kind ${kind}`); +} + +export function GetAttributeType(kind: string): string { + switch (kind) { + case VertexBuffer.PositionKind: + return "POSITION"; + case VertexBuffer.NormalKind: + return "NORMAL"; + case VertexBuffer.TangentKind: + return "TANGENT"; + case VertexBuffer.ColorKind: + return "COLOR_0"; + case VertexBuffer.UVKind: + return "TEXCOORD_0"; + case VertexBuffer.UV2Kind: + return "TEXCOORD_1"; + case VertexBuffer.UV3Kind: + return "TEXCOORD_2"; + case VertexBuffer.UV4Kind: + return "TEXCOORD_3"; + case VertexBuffer.UV5Kind: + return "TEXCOORD_4"; + case VertexBuffer.UV6Kind: + return "TEXCOORD_5"; + case VertexBuffer.MatricesIndicesKind: + return "JOINTS_0"; + case VertexBuffer.MatricesIndicesExtraKind: + return "JOINTS_1"; + case VertexBuffer.MatricesWeightsKind: + return "WEIGHTS_0"; + case VertexBuffer.MatricesWeightsExtraKind: + return "WEIGHTS_1"; + } + + throw new Error(`Unknown kind: ${kind}`); +} + +export function GetPrimitiveMode(fillMode: number): MeshPrimitiveMode { + switch (fillMode) { + case Material.TriangleFillMode: + return MeshPrimitiveMode.TRIANGLES; + case Material.TriangleStripDrawMode: + return MeshPrimitiveMode.TRIANGLE_STRIP; + case Material.TriangleFanDrawMode: + return MeshPrimitiveMode.TRIANGLE_FAN; + case Material.PointListDrawMode: + case Material.PointFillMode: + return MeshPrimitiveMode.POINTS; + case Material.LineLoopDrawMode: + return MeshPrimitiveMode.LINE_LOOP; + case Material.LineListDrawMode: + return MeshPrimitiveMode.LINES; + case Material.LineStripDrawMode: + return MeshPrimitiveMode.LINE_STRIP; + } + + throw new Error(`Unknown fill mode: ${fillMode}`); +} + +export function IsTriangleFillMode(fillMode: number): boolean { + switch (fillMode) { + case Material.TriangleFillMode: + case Material.TriangleStripDrawMode: + case Material.TriangleFanDrawMode: + return true; + } + + return false; +} + +export function NormalizeTangent(tangent: Vector4 | Vector3) { + const length = Math.sqrt(tangent.x * tangent.x + tangent.y * tangent.y + tangent.z * tangent.z); + if (length > 0) { + tangent.x /= length; + tangent.y /= length; + tangent.z /= length; + } +} + +export function ConvertToRightHandedPosition(value: Vector3): Vector3 { + value.x *= -1; + return value; +} + +export function ConvertToRightHandedRotation(value: Quaternion): Quaternion { + value.x *= -1; + value.y *= -1; + return value; +} + +export function ConvertToRightHandedNode(value: INode) { + let translation = Vector3.FromArrayToRef(value.translation || [0, 0, 0], 0, TmpVectors.Vector3[0]); + let rotation = Quaternion.FromArrayToRef(value.rotation || [0, 0, 0, 1], 0, TmpVectors.Quaternion[0]); + + translation = ConvertToRightHandedPosition(translation); + rotation = ConvertToRightHandedRotation(rotation); + + if (translation.equalsWithEpsilon(defaultTranslation, epsilon)) { + delete value.translation; + } else { + value.translation = translation.asArray(); + } + + if (Quaternion.IsIdentity(rotation)) { + delete value.rotation; + } else { + value.rotation = rotation.asArray(); + } +} + +/** + * Rotation by 180 as glTF has a different convention than Babylon. + * @param rotation Target camera rotation. + * @returns Ref to camera rotation. + */ +export function ConvertCameraRotationToGLTF(rotation: Quaternion): Quaternion { + return rotation.multiplyInPlace(rotation180Y); +} + +export function RotateNode180Y(node: INode) { + if (node.rotation) { + const rotation = Quaternion.FromArrayToRef(node.rotation || [0, 0, 0, 1], 0, TmpVectors.Quaternion[1]); + rotation180Y.multiplyToRef(rotation, rotation); + node.rotation = rotation.asArray(); + } +} + +/** + * Collapses GLTF parent and node into a single node. This is useful for removing nodes that were added by the GLTF importer. + * @param node Target parent node. + * @param parentNode Original GLTF node (Light or Camera). + */ +export function CollapseParentNode(node: INode, parentNode: INode) { + const parentTranslation = Vector3.FromArrayToRef(parentNode.translation || [0, 0, 0], 0, TmpVectors.Vector3[0]); + const parentRotation = Quaternion.FromArrayToRef(parentNode.rotation || [0, 0, 0, 1], 0, TmpVectors.Quaternion[0]); + const parentScale = Vector3.FromArrayToRef(parentNode.scale || [1, 1, 1], 0, TmpVectors.Vector3[1]); + const parentMatrix = Matrix.ComposeToRef(parentScale, parentRotation, parentTranslation, TmpVectors.Matrix[0]); + + const translation = Vector3.FromArrayToRef(node.translation || [0, 0, 0], 0, TmpVectors.Vector3[2]); + const rotation = Quaternion.FromArrayToRef(node.rotation || [0, 0, 0, 1], 0, TmpVectors.Quaternion[1]); + const scale = Vector3.FromArrayToRef(node.scale || [1, 1, 1], 0, TmpVectors.Vector3[1]); + const matrix = Matrix.ComposeToRef(scale, rotation, translation, TmpVectors.Matrix[1]); + + parentMatrix.multiplyToRef(matrix, matrix); + matrix.decompose(parentScale, parentRotation, parentTranslation); + + if (parentTranslation.equalsWithEpsilon(defaultTranslation, epsilon)) { + delete parentNode.translation; + } else { + parentNode.translation = parentTranslation.asArray(); + } + + if (Quaternion.IsIdentity(parentRotation)) { + delete parentNode.rotation; + } else { + parentNode.rotation = parentRotation.asArray(); + } + + if (parentScale.equalsWithEpsilon(defaultScale, epsilon)) { + delete parentNode.scale; + } else { + parentNode.scale = parentScale.asArray(); + } +} + +/** + * Sometimes the GLTF Importer can add extra transform nodes (for lights and cameras). This checks if a parent node was added by the GLTF Importer. If so, it should be removed during serialization. + * @param babylonNode Original GLTF node (Light or Camera). + * @param parentBabylonNode Target parent node. + * @returns True if the parent node was added by the GLTF importer. + */ +export function IsParentAddedByImporter(babylonNode: Node, parentBabylonNode: Node): boolean { + return parentBabylonNode instanceof TransformNode && parentBabylonNode.getChildren().length == 1 && babylonNode.getChildren().length == 0; +} + +export function IsNoopNode(node: Node, useRightHandedSystem: boolean): boolean { + if (!(node instanceof TransformNode)) { + return false; + } + + // Transform + if (useRightHandedSystem) { + const matrix = node.getWorldMatrix(); + if (!matrix.isIdentity()) { + return false; } - if (byteOffset != null) { - accessor.byteOffset = byteOffset; + } else { + const matrix = node.getWorldMatrix().multiplyToRef(convertHandednessMatrix, TmpVectors.Matrix[0]); + if (!matrix.isIdentity()) { + return false; } + } - return accessor; - } - - /** - * Calculates the minimum and maximum values of an array of position floats - * @param positions Positions array of a mesh - * @param vertexStart Starting vertex offset to calculate min and max values - * @param vertexCount Number of vertices to check for min and max values - * @returns min number array and max number array - */ - public static _CalculateMinMaxPositions(positions: FloatArray, vertexStart: number, vertexCount: number): { min: number[]; max: number[] } { - const min = [Infinity, Infinity, Infinity]; - const max = [-Infinity, -Infinity, -Infinity]; - const positionStrideSize = 3; - let indexOffset: number; - let position: Vector3; - let vector: number[]; - - if (vertexCount) { - for (let i = vertexStart, length = vertexStart + vertexCount; i < length; ++i) { - indexOffset = positionStrideSize * i; - - position = Vector3.FromArray(positions, indexOffset); - vector = position.asArray(); - - for (let j = 0; j < positionStrideSize; ++j) { - const num = vector[j]; - if (num < min[j]) { - min[j] = num; - } - if (num > max[j]) { - max[j] = num; - } - ++indexOffset; - } - } - } - return { min, max }; + // Geometry + if ((node instanceof Mesh && node.geometry) || (node instanceof InstancedMesh && node.sourceMesh.geometry)) { + return false; } - public static _NormalizeTangentFromRef(tangent: Vector4 | Vector3) { - const length = Math.sqrt(tangent.x * tangent.x + tangent.y * tangent.y + tangent.z * tangent.z); - if (length > 0) { - tangent.x /= length; - tangent.y /= length; - tangent.z /= length; + return true; +} + +export function AreIndices32Bits(indices: Nullable, count: number): boolean { + if (indices) { + if (indices instanceof Array) { + return indices.some((value) => value >= 65536); } + + return indices.BYTES_PER_ELEMENT === 4; + } + + return count >= 65536; +} + +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); } - public static _GetDataAccessorElementCount(accessorType: AccessorType) { - switch (accessorType) { - case AccessorType.MAT2: - return 4; - case AccessorType.MAT3: - return 9; - case AccessorType.MAT4: - return 16; - case AccessorType.SCALAR: - return 1; - case AccessorType.VEC2: - return 2; - case AccessorType.VEC3: - return 3; - case AccessorType.VEC4: - return 4; + return ArrayBuffer.isView(indices) ? new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength) : new Uint8Array(indices); +} + +export function DataArrayToUint8Array(data: DataArray): Uint8Array { + if (data instanceof Array) { + const floatData = new Float32Array(data); + return new Uint8Array(floatData.buffer, floatData.byteOffset, floatData.byteLength); + } + + return ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data); +} + +export function GetMinMax(data: DataArray, vertexBuffer: VertexBuffer, start: number, count: number): { min: number[]; max: number[] } { + const { byteOffset, byteStride, type, normalized } = vertexBuffer; + const size = vertexBuffer.getSize(); + const min = new Array(size).fill(Infinity); + const max = new Array(size).fill(-Infinity); + EnumerateFloatValues(data, byteOffset + start * byteStride, byteStride, size, type, count * size, normalized, (values) => { + for (let i = 0; i < size; i++) { + min[i] = Math.min(min[i], values[i]); + max[i] = Math.max(max[i], values[i]); + } + }); + + return { min, max }; +} + +/** + * Removes, in-place, object properties which have the same value as the default value. + * Useful for avoiding unnecessary properties in the glTF JSON. + * @param object the object to omit default values from + * @param defaultValues a partial object with default values + * @returns object with default values omitted + */ +export function OmitDefaultValues(object: T, defaultValues: Partial): T { + for (const [key, value] of Object.entries(object)) { + const defaultValue = defaultValues[key as keyof T]; + if ((Array.isArray(value) && Array.isArray(defaultValue) && AreArraysEqual(value, defaultValue)) || value === defaultValue) { + delete object[key as keyof T]; } } + return object; +} + +function AreArraysEqual(array1: unknown[], array2: unknown[]): boolean { + return array1.length === array2.length && array1.every((val, i) => val === array2[i]); } diff --git a/packages/dev/serializers/test/integration/glTFSerializer.test.ts b/packages/dev/serializers/test/integration/glTFSerializer.test.ts index c2595096669..13226c62ac3 100644 --- a/packages/dev/serializers/test/integration/glTFSerializer.test.ts +++ b/packages/dev/serializers/test/integration/glTFSerializer.test.ts @@ -46,7 +46,7 @@ describe("Babylon glTF Serializer", () => { babylonStandardMaterial.specularColor = BABYLON.Color3.Black(); babylonStandardMaterial.specularPower = 64; babylonStandardMaterial.alpha = 1; - const materialExporter = new BABYLON.GLTF2.Exporter._GLTFMaterialExporter(new BABYLON.GLTF2.Exporter._Exporter(window.scene)); + const materialExporter = new BABYLON.GLTF2.Exporter.GLTFMaterialExporter(new BABYLON.GLTF2.Exporter.GLTFExporter(window.scene)); const metalRough = materialExporter._convertToGLTFPBRMetallicRoughness(babylonStandardMaterial); return { @@ -62,15 +62,15 @@ describe("Babylon glTF Serializer", () => { }); it("should solve for metallic", async () => { const assertionData = await page.evaluate(() => { - const solveZero = BABYLON.GLTF2.Exporter._GLTFMaterialExporter._SolveMetallic(1.0, 0.0, 1.0); - const solveAproxOne = BABYLON.GLTF2.Exporter._GLTFMaterialExporter._SolveMetallic(0.0, 1.0, 1.0); + const solveZero = BABYLON.GLTF2.Exporter._SolveMetallic(1.0, 0.0, 1.0); + const solveApproxOne = BABYLON.GLTF2.Exporter._SolveMetallic(0.0, 1.0, 1.0); return { solveZero, - solveAproxOne, + solveApproxOne: solveApproxOne, }; }); expect(assertionData.solveZero).toBe(0.0); - expect(assertionData.solveAproxOne).toBeCloseTo(1.0, 1e-6); + expect(assertionData.solveApproxOne).toBeCloseTo(1.0, 1e-6); }); it("should serialize empty Babylon window.scene to glTF with only asset property", async () => { const assertionData = await page.evaluate(() => { @@ -535,5 +535,25 @@ describe("Babylon glTF Serializer", () => { expect(assertionData.extensions["KHR_lights_punctual"].lights).toHaveLength(3); expect(assertionData.nodes).toHaveLength(3); }); + it("should export instances as nodes pointing to same mesh", async () => { + const instanceCount = 3; + const assertionData = await page.evaluate((instanceCount) => { + const mesh = BABYLON.MeshBuilder.CreateBox("box", {}, window.scene!); + for (let i = 0; i < instanceCount; i++) { + mesh.createInstance("boxInstance" + i); + } + return BABYLON.GLTF2Export.GLTFAsync(window.scene!, "test").then((glTFData) => { + const jsonString = glTFData.glTFFiles["test.gltf"] as string; + const jsonData = JSON.parse(jsonString); + return jsonData; + }); + }, instanceCount); + expect(Object.keys(assertionData)).toHaveLength(9); + expect(assertionData.nodes).toHaveLength(instanceCount + 1); + expect(assertionData.meshes).toHaveLength(1); + for (const node of assertionData.nodes) { + expect(node.mesh).toEqual(0); + } + }); }); }); diff --git a/packages/public/glTF2Interface/babylon.glTF2Interface.d.ts b/packages/public/glTF2Interface/babylon.glTF2Interface.d.ts index 4193da355f3..e16dca1df1b 100644 --- a/packages/public/glTF2Interface/babylon.glTF2Interface.d.ts +++ b/packages/public/glTF2Interface/babylon.glTF2Interface.d.ts @@ -1041,12 +1041,7 @@ declare module BABYLON.GLTF2 { } /** @internal */ - interface IMaterialExtension { - hasTextures?(): boolean; - } - - /** @internal */ - interface IKHRMaterialsClearcoat extends IMaterialExtension { + interface IKHRMaterialsClearcoat { clearcoatFactor?: number; clearcoatTexture?: ITextureInfo; clearcoatRoughnessFactor?: number; @@ -1055,7 +1050,7 @@ declare module BABYLON.GLTF2 { } /** @internal */ - interface IKHRMaterialsIridescence extends IMaterialExtension { + interface IKHRMaterialsIridescence { iridescenceFactor?: number; iridescenceIor?: number; iridescenceThicknessMinimum?: number; @@ -1065,7 +1060,7 @@ declare module BABYLON.GLTF2 { } /** @internal */ - interface IKHRMaterialsAnisotropy extends IMaterialExtension { + interface IKHRMaterialsAnisotropy { anisotropyStrength?: number; anisotropyRotation?: number; anisotropyTexture?: ITextureInfo; @@ -1076,7 +1071,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsIor extends IMaterialExtension { + interface IKHRMaterialsIor { ior?: number; } @@ -1085,7 +1080,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsVolume extends IMaterialExtension { + interface IKHRMaterialsVolume { thicknessFactor?: number; thicknessTexture?: ITextureInfo; attenuationDistance?: number; @@ -1097,7 +1092,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsDispersion extends IMaterialExtension { + interface IKHRMaterialsDispersion { dispersion?: number; } @@ -1106,7 +1101,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsSpecular extends IMaterialExtension { + interface IKHRMaterialsSpecular { specularFactor?: number; specularColorFactor?: number[]; specularTexture?: ITextureInfo; @@ -1118,7 +1113,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsTransmission extends IMaterialExtension { + interface IKHRMaterialsTransmission { transmissionFactor?: number; transmissionTexture?: ITextureInfo; } @@ -1128,7 +1123,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsEmissiveStrength extends IMaterialExtension { + interface IKHRMaterialsEmissiveStrength { emissiveStrength: number; } @@ -1137,7 +1132,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsPbrSpecularGlossiness extends IMaterialExtension { + interface IKHRMaterialsPbrSpecularGlossiness { diffuseFactor: number[]; diffuseTexture: ITextureInfo; specularFactor: number[]; @@ -1150,7 +1145,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsSheen extends IMaterialExtension { + interface IKHRMaterialsSheen { sheenColorFactor?: number[]; sheenColorTexture?: ITextureInfo; sheenRoughnessFactor?: number; @@ -1163,7 +1158,7 @@ declare module BABYLON.GLTF2 { */ /** @internal */ - interface IKHRMaterialsDiffuseTransmission extends IMaterialExtension { + interface IKHRMaterialsDiffuseTransmission { diffuseTransmissionFactor?: number; diffuseTransmissionTexture?: ITextureInfo; diffuseTransmissionColorFactor?: number[]; diff --git a/packages/tools/devHost/src/babylon.glTF2Interface.ts b/packages/tools/devHost/src/babylon.glTF2Interface.ts index 9df496583e1..16391d50bfa 100644 --- a/packages/tools/devHost/src/babylon.glTF2Interface.ts +++ b/packages/tools/devHost/src/babylon.glTF2Interface.ts @@ -1028,12 +1028,7 @@ interface IKHRLightsPunctual { } /** @internal */ -interface IMaterialExtension { - hasTextures?(): boolean; -} - -/** @internal */ -interface IKHRMaterialsClearcoat extends IMaterialExtension { +interface IKHRMaterialsClearcoat { clearcoatFactor?: number; clearcoatTexture?: ITextureInfo; clearcoatRoughnessFactor?: number; @@ -1042,7 +1037,7 @@ interface IKHRMaterialsClearcoat extends IMaterialExtension { } /** @internal */ -interface IKHRMaterialsIridescence extends IMaterialExtension { +interface IKHRMaterialsIridescence { iridescenceFactor?: number; iridescenceIor?: number; iridescenceThicknessMinimum?: number; @@ -1052,7 +1047,7 @@ interface IKHRMaterialsIridescence extends IMaterialExtension { } /** @internal */ -interface IKHRMaterialsAnisotropy extends IMaterialExtension { +interface IKHRMaterialsAnisotropy { anisotropyStrength?: number; anisotropyRotation?: number; anisotropyTexture?: ITextureInfo; @@ -1063,7 +1058,7 @@ interface IKHRMaterialsAnisotropy extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsIor extends IMaterialExtension { +interface IKHRMaterialsIor { ior?: number; } @@ -1072,7 +1067,7 @@ interface IKHRMaterialsIor extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsVolume extends IMaterialExtension { +interface IKHRMaterialsVolume { thicknessFactor?: number; thicknessTexture?: ITextureInfo; attenuationDistance?: number; @@ -1084,7 +1079,7 @@ interface IKHRMaterialsVolume extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsDispersion extends IMaterialExtension { +interface IKHRMaterialsDispersion { dispersion?: number; } @@ -1093,7 +1088,7 @@ interface IKHRMaterialsDispersion extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsSpecular extends IMaterialExtension { +interface IKHRMaterialsSpecular { specularFactor?: number; specularColorFactor?: number[]; specularTexture?: ITextureInfo; @@ -1105,7 +1100,7 @@ interface IKHRMaterialsSpecular extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsTransmission extends IMaterialExtension { +interface IKHRMaterialsTransmission { transmissionFactor?: number; transmissionTexture?: ITextureInfo; } @@ -1115,7 +1110,7 @@ interface IKHRMaterialsTransmission extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsEmissiveStrength extends IMaterialExtension { +interface IKHRMaterialsEmissiveStrength { emissiveStrength: number; } @@ -1124,7 +1119,7 @@ interface IKHRMaterialsEmissiveStrength extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsPbrSpecularGlossiness extends IMaterialExtension { +interface IKHRMaterialsPbrSpecularGlossiness { diffuseFactor: number[]; diffuseTexture: ITextureInfo; specularFactor: number[]; @@ -1137,7 +1132,7 @@ interface IKHRMaterialsPbrSpecularGlossiness extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsSheen extends IMaterialExtension { +interface IKHRMaterialsSheen { sheenColorFactor?: number[]; sheenColorTexture?: ITextureInfo; sheenRoughnessFactor?: number; @@ -1150,7 +1145,7 @@ interface IKHRMaterialsSheen extends IMaterialExtension { */ /** @internal */ -interface IKHRMaterialsDiffuseTransmission extends IMaterialExtension { +interface IKHRMaterialsDiffuseTransmission { diffuseTransmissionFactor?: number; diffuseTransmissionTexture?: ITextureInfo; diffuseTransmissionColorFactor?: number; diff --git a/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerCameraLeftHand.png b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerCameraLeftHand.png new file mode 100644 index 00000000000..30567177134 Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerCameraLeftHand.png differ diff --git a/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerCameraRightHand.png b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerCameraRightHand.png new file mode 100644 index 00000000000..820b2ebf787 Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerCameraRightHand.png differ diff --git a/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKHRPunctualLightLH.png b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKHRPunctualLightLH.png new file mode 100644 index 00000000000..2ba7f0b67ae Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKHRPunctualLightLH.png differ diff --git a/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKHRPunctualLightRH.png b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKHRPunctualLightRH.png new file mode 100644 index 00000000000..c8fe73e3b1d Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/glTFSerializerKHRPunctualLightRH.png differ diff --git a/packages/tools/tests/test/visualization/config.json b/packages/tools/tests/test/visualization/config.json index 23a6943d29a..508bdd76e82 100644 --- a/packages/tools/tests/test/visualization/config.json +++ b/packages/tools/tests/test/visualization/config.json @@ -853,29 +853,29 @@ }, { "title": "GLTF Serializer with Negative World Matrix (left handed, once)", - "playgroundId": "#KX53VK#48", + "playgroundId": "#KX53VK#77", "referenceImage": "glTFSerializerNegativeWorldMatrix.png", "errorRatio": 1.1 }, { "title": "GLTF Serializer with Negative World Matrix (left handed, twice)", - "playgroundId": "#KX53VK#48", + "playgroundId": "#KX53VK#77", "referenceImage": "glTFSerializerNegativeWorldMatrix.png", - "replace": "iterations = 1, iterations = 2", + "replace": "//options//, iterations = 2;", "errorRatio": 1.1 }, { "title": "GLTF Serializer with Negative World Matrix (right handed, once)", - "playgroundId": "#KX53VK#48", + "playgroundId": "#KX53VK#76", "referenceImage": "glTFSerializerNegativeWorldMatrix.png", - "replace": "useRightHandedSystem = false, useRightHandedSystem = true", + "replace": "//options//, useRightHandedSystem = true;", "errorRatio": 1.1 }, { "title": "GLTF Serializer with Negative World Matrix (right handed, twice)", - "playgroundId": "#KX53VK#48", + "playgroundId": "#KX53VK#76", "referenceImage": "glTFSerializerNegativeWorldMatrix.png", - "replace": "useRightHandedSystem = false, useRightHandedSystem = true, iterations = 1, iterations = 2", + "replace": "//options//, useRightHandedSystem = true; iterations = 2;", "errorRatio": 1.1 }, { @@ -912,6 +912,42 @@ "playgroundId": "#1Q2BWN#10", "referenceImage": "glTFSerializerKhrGpuInstancing.png" }, + { + "title": "GLTF Serializer KHR punctual light, left-handed", + "playgroundId": "#FLXW8B#25", + "replace": "//options//, roundtrip = true; useRightHandedSystem = false;", + "referenceImage": "glTFSerializerKHRPunctualLightLH.png" + }, + { + "title": "GLTF Serializer KHR punctual light, right-handed", + "playgroundId": "#FLXW8B#25", + "replace": "//options//, roundtrip = true; useRightHandedSystem = true;", + "referenceImage": "glTFSerializerKHRPunctualLightRH.png" + }, + { + "title": "GLTF Serializer Camera, left-handed", + "playgroundId": "#O0M0J9#8", + "replace": "//options//, roundtripCount = 1; useRightHandedSystem = false;", + "referenceImage": "glTFSerializerCameraLeftHand.png" + }, + { + "title": "GLTF Serializer Camera, right-handed", + "playgroundId": "#O0M0J9#8", + "replace": "//options//, roundtripCount = 1; useRightHandedSystem = true;", + "referenceImage": "glTFSerializerCameraRightHand.png" + }, + { + "title": "GLTF Serializer Camera, left-handed, round trip twice", + "playgroundId": "#O0M0J9#8", + "replace": "//options//, roundtripCount = 2; useRightHandedSystem = false;", + "referenceImage": "glTFSerializerCameraLeftHand.png" + }, + { + "title": "GLTF Serializer Camera, right-handed, round trip twice", + "playgroundId": "#O0M0J9#8", + "replace": "//options//, roundtripCount = 2; useRightHandedSystem = true;", + "referenceImage": "glTFSerializerCameraRightHand.png" + }, { "title": "GLTF Buggy with Draco Mesh Compression", "playgroundId": "#JNW207#1",