Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add KHR_draco_mesh_compression support to glTF Exporter #16064

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d8e5280
Factor encodeMesh API for glTF
alexchuber Jan 13, 2025
7862742
Add KHR_draco_mesh_compression and centralize buffer management in se…
alexchuber Jan 14, 2025
f6c6d14
Rename class to BufferManager
alexchuber Jan 14, 2025
1474308
Remove unnecessary async + return value from postExportMeshPrimitive;…
alexchuber Jan 14, 2025
a06a536
Use DataWriter in BufferManager
alexchuber Jan 14, 2025
32dab1b
Unrelated: avoid exporting buffers not associated with standard verte…
alexchuber Jan 14, 2025
e877b12
Don't use Set iterator to fix UMD build
alexchuber Jan 15, 2025
1324e68
Maintain buffer alignment
alexchuber Jan 16, 2025
dca39d7
Fix SKIN_IBM_ACCESSOR_WITH_BYTESTRIDE (why not)
alexchuber Jan 16, 2025
f9e2df2
Don't cull buffers of primitives whose encoding failed
alexchuber Jan 16, 2025
7ebb753
Add roundtrip vis test
alexchuber Jan 16, 2025
731b211
Clean up
alexchuber Jan 16, 2025
53f27ad
Clean up
alexchuber Jan 16, 2025
918b59d
Merge branch 'master' into exporter_KHR_draco_mesh_compression
alexchuber Jan 16, 2025
3e91566
Fix iterator UMD issue
alexchuber Jan 16, 2025
6e342d9
Unrelated: prevent NaN in tangents and normals
alexchuber Jan 17, 2025
7bde3e4
Update TypedArray type
alexchuber Jan 17, 2025
5b55b61
Log errors
alexchuber Jan 17, 2025
d5d5479
Rename to kind, dracoName
alexchuber Jan 17, 2025
cc88fa2
Remove unused short and byte handling
alexchuber Jan 17, 2025
dcb9ce8
Use .set for IBM
alexchuber Jan 18, 2025
a04f0c2
Rename to meshCompressionMethod to prepare for meshopt
alexchuber Jan 18, 2025
2726415
Rename to getPropertiesWithBufferView
alexchuber Jan 18, 2025
03a3e93
Update comment
alexchuber Jan 18, 2025
6953490
Use Float32Array for animation output writing
alexchuber Jan 18, 2025
70d1870
Remove unused DataWriter import
alexchuber Jan 18, 2025
6a9db89
Update typedArrayToWriteMethod
alexchuber Jan 18, 2025
c4dc52a
Missing byteOffset update!
alexchuber Jan 18, 2025
a40d768
Fix update to animation output writing
alexchuber Jan 18, 2025
5f63f10
Add float64 support for consistency
alexchuber Jan 21, 2025
1a91469
Exclude BigInt TypedArrays in param type
alexchuber Jan 21, 2025
428c47b
Rename vars
alexchuber Jan 21, 2025
ab91e05
Update bufferManager data type
alexchuber Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function EncodeMesh(
const attributeIDs: Record<string, number> = {}; // Babylon kind -> Draco unique id

// Double-check that at least a position attribute is provided
const positionAttribute = attributes.find((a) => a.babylonAttribute === "position");
const positionAttribute = attributes.find((a) => a.dracoName === "POSITION");
if (!positionAttribute) {
throw new Error("Position attribute is required for Draco encoding");
}
Expand All @@ -61,9 +61,9 @@ export function EncodeMesh(
// Add the attributes
for (const attribute of attributes) {
const verticesCount = attribute.data.length / attribute.size;
attributeIDs[attribute.babylonAttribute] = meshBuilder.AddFloatAttribute(mesh, encoderModule[attribute.dracoAttribute], verticesCount, attribute.size, attribute.data);
if (options.quantizationBits && options.quantizationBits[attribute.dracoAttribute]) {
encoder.SetAttributeQuantization(encoderModule[attribute.dracoAttribute], options.quantizationBits[attribute.dracoAttribute]);
attributeIDs[attribute.kind] = meshBuilder.AddFloatAttribute(mesh, encoderModule[attribute.dracoName], verticesCount, attribute.size, attribute.data);
if (options.quantizationBits && options.quantizationBits[attribute.dracoName]) {
encoder.SetAttributeQuantization(encoderModule[attribute.dracoName], options.quantizationBits[attribute.dracoName]);
}
}

Expand Down
50 changes: 31 additions & 19 deletions packages/dev/core/src/Meshes/Compression/dracoEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function PrepareAttributesForDraco(input: Mesh | Geometry, excludedAttributes?:
if (!(data instanceof Float32Array)) {
data = Float32Array.from(data!);
}
attributes.push({ babylonAttribute: kind, dracoAttribute: GetDracoAttributeName(kind), size: input.getVertexBuffer(kind)!.getSize(), data: data });
attributes.push({ kind: kind, dracoName: GetDracoAttributeName(kind), size: input.getVertexBuffer(kind)!.getSize(), data: data });
}

return attributes;
Expand Down Expand Up @@ -202,26 +202,14 @@ export class DracoEncoder extends DracoCodec {
}

/**
* Encodes a mesh or geometry into a Draco-encoded mesh data.
* @param input the mesh or geometry to encode
* @param options options for the encoding
* @returns a promise that resolves to the newly-encoded data
* @internal
*/
public async encodeMeshAsync(input: Mesh | Geometry, options?: IDracoEncoderOptions): Promise<Nullable<IDracoEncodedMeshData>> {
const verticesCount = input.getTotalVertices();
if (verticesCount == 0) {
throw new Error("Cannot compress geometry with Draco. There are no vertices.");
}

// Prepare parameters for encoding
public async _encodeAsync(
attributes: Array<IDracoAttributeData>,
indices: Nullable<Uint16Array | Uint32Array>,
options?: IDracoEncoderOptions
): Promise<Nullable<IDracoEncodedMeshData>> {
alexchuber marked this conversation as resolved.
Show resolved Hide resolved
const mergedOptions = options ? deepMerge(DefaultEncoderOptions, options) : DefaultEncoderOptions;
if (input instanceof Mesh && input.morphTargetManager && mergedOptions.method === "MESH_EDGEBREAKER_ENCODING") {
Logger.Warn("Cannot use Draco EDGEBREAKER method with morph targets. Falling back to SEQUENTIAL method.");
mergedOptions.method = "MESH_SEQUENTIAL_ENCODING";
}

let indices = PrepareIndicesForDraco(input);
const attributes = PrepareAttributesForDraco(input, mergedOptions.excludedAttributes);

if (this._workerPoolPromise) {
const workerPool = await this._workerPoolPromise;
Expand Down Expand Up @@ -269,4 +257,28 @@ export class DracoEncoder extends DracoCodec {

throw new Error("Draco encoder module is not available");
}

/**
* Encodes a mesh or geometry into a Draco-encoded mesh data.
* @param input the mesh or geometry to encode
* @param options options for the encoding
* @returns a promise that resolves to the newly-encoded data
*/
public async encodeMeshAsync(input: Mesh | Geometry, options?: IDracoEncoderOptions): Promise<Nullable<IDracoEncodedMeshData>> {
const verticesCount = input.getTotalVertices();
if (verticesCount == 0) {
throw new Error("Cannot compress geometry with Draco. There are no vertices.");
}

// Prepare parameters for encoding
if (input instanceof Mesh && input.morphTargetManager && options?.method === "MESH_EDGEBREAKER_ENCODING") {
Logger.Warn("Cannot use Draco EDGEBREAKER method with morph targets. Falling back to SEQUENTIAL method.");
options.method = "MESH_SEQUENTIAL_ENCODING";
}

const indices = PrepareIndicesForDraco(input);
const attributes = PrepareAttributesForDraco(input, options?.excludedAttributes);

return this._encodeAsync(attributes, indices, options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ export interface IDracoEncoderOptions {
*/
export interface IDracoAttributeData {
/**
* The Babylon kind of the attribute.
* The kind of the attribute.
*/
babylonAttribute: string;
kind: string;
/**
* The Draco kind to use for the attribute.
* The Draco name for the kind of the attribute.
*/
dracoAttribute: DracoAttributeName;
dracoName: DracoAttributeName;
/**
* The size of the attribute.
*/
Expand Down
16 changes: 16 additions & 0 deletions packages/dev/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ export type FloatArray = number[] | Float32Array;
/** Alias type for number array or Float32Array or Int32Array or Uint32Array or Uint16Array */
export type IndicesArray = number[] | Int32Array | Uint32Array | Uint16Array;

/**
* Alias type for all TypedArrays
*/
export type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| BigInt64Array
| BigUint64Array;

/**
* Alias for types that can be used by a Buffer or VertexBuffer.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type { IBufferView, IAccessor, INode, IEXTMeshGpuInstancing } from "babylonjs-gltf2interface";
import type { INode, IEXTMeshGpuInstancing } from "babylonjs-gltf2interface";
import { AccessorType, AccessorComponentType } from "babylonjs-gltf2interface";
import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension";
import type { DataWriter } from "../dataWriter";
import type { BufferManager } from "../bufferManager";
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";
Expand Down Expand Up @@ -49,7 +48,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {
* @param babylonNode the corresponding babylon node
* @param nodeMap map from babylon node id to node index
* @param convertToRightHanded true if we need to convert data from left hand to right hand system.
* @param dataWriter binary writer
* @param bufferManager buffer manager
* @returns nullable promise, resolves with the node
*/
public postExportNodeAsync(
Expand All @@ -58,7 +57,7 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {
babylonNode: Node,
nodeMap: Map<Node, number>,
convertToRightHanded: boolean,
dataWriter: DataWriter
bufferManager: BufferManager
): Promise<Nullable<INode>> {
return new Promise((resolve) => {
if (node && babylonNode instanceof Mesh) {
Expand Down Expand Up @@ -113,22 +112,16 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {

// do we need to write TRANSLATION ?
if (hasAnyInstanceWorldTranslation) {
extension.attributes["TRANSLATION"] = this._buildAccessor(
translationBuffer,
AccessorType.VEC3,
babylonNode.thinInstanceCount,
dataWriter,
AccessorComponentType.FLOAT
);
extension.attributes["TRANSLATION"] = this._buildAccessor(translationBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, bufferManager);
}
// do we need to write ROTATION ?
if (hasAnyInstanceWorldRotation) {
const componentType = AccessorComponentType.FLOAT; // we decided to stay on FLOAT for now see https://github.com/BabylonJS/Babylon.js/pull/12495
extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, dataWriter, componentType);
// we decided to stay on FLOAT for now see https://github.com/BabylonJS/Babylon.js/pull/12495
extension.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, bufferManager);
}
// do we need to write SCALE ?
if (hasAnyInstanceWorldScale) {
extension.attributes["SCALE"] = this._buildAccessor(scaleBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, dataWriter, AccessorComponentType.FLOAT);
extension.attributes["SCALE"] = this._buildAccessor(scaleBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, bufferManager);
}

/* eslint-enable @typescript-eslint/naming-convention*/
Expand All @@ -140,46 +133,14 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {
});
}

private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, binaryWriter: DataWriter, componentType: AccessorComponentType): number {
// write the buffer
const bufferOffset = binaryWriter.byteOffset;
switch (componentType) {
case AccessorComponentType.FLOAT: {
for (let i = 0; i != buffer.length; i++) {
binaryWriter.writeFloat32(buffer[i]);
}
break;
}
case AccessorComponentType.BYTE: {
for (let i = 0; i != buffer.length; i++) {
binaryWriter.writeInt8(buffer[i] * 127);
}
break;
}
case AccessorComponentType.SHORT: {
for (let i = 0; i != buffer.length; i++) {
binaryWriter.writeInt16(buffer[i] * 32767);
}

break;
}
}
private _buildAccessor(buffer: Float32Array, type: AccessorType, count: number, bufferManager: BufferManager): number {
// build the buffer view
const bv: IBufferView = { buffer: 0, byteOffset: bufferOffset, byteLength: buffer.length * VertexBuffer.GetTypeByteLength(componentType) };
const bufferViewIndex = this._exporter._bufferViews.length;
this._exporter._bufferViews.push(bv);
const bv = bufferManager.createBufferView(buffer);

// finally build the accessor
const accessorIndex = this._exporter._accessors.length;
const accessor: IAccessor = {
bufferView: bufferViewIndex,
componentType: componentType,
count: count,
type: type,
normalized: componentType == AccessorComponentType.BYTE || componentType == AccessorComponentType.SHORT,
};
const accessor = bufferManager.createAccessor(bv, type, AccessorComponentType.FLOAT, count);
this._exporter._accessors.push(accessor);
return accessorIndex;
return this._exporter._accessors.length - 1;
}
}

Expand Down
Loading