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

GLTFSerializer : Ext mesh gpu instancing #12495

Merged
merged 5 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
@@ -0,0 +1,180 @@
import type { IBufferView, IAccessor, INode, IEXTMeshGpuInstancing } from "babylonjs-gltf2interface";
import { AccessorType, AccessorComponentType } from "babylonjs-gltf2interface";
import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension";
import { _Exporter, _BinaryWriter } from "../glTFExporter";
import type { Nullable } from "core/types";
import type { Node } from "core/node";
import { Mesh } from "core/Meshes";
import { TmpVectors, Quaternion, Vector3 } from "core/Maths/math.vector";
import { VertexBuffer } from "core/Buffers/buffer";

const NAME = "EXT_mesh_gpu_instancing";

/**
* [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing/README.md)
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {
/** Name of this extension */
public readonly name = NAME;

/** Defines whether this extension is enabled */
public enabled = true;

/** Defines whether this extension is required */
public required = false;

private _exporter: _Exporter;

private _wasUsed = false;

constructor(exporter: _Exporter) {
this._exporter = exporter;
}

public dispose() {}

/** @hidden */
public get wasUsed() {
return this._wasUsed;
}

public postExportNodeAsync?(
context: string,
node: Nullable<INode>,
babylonNode: Node,
nodeMap?: { [key: number]: number },
binaryWriter?: _BinaryWriter
): Promise<Nullable<INode>> {
return new Promise((resolve) => {
if (node && babylonNode instanceof Mesh) {
if (babylonNode.hasThinInstances && binaryWriter) {
this._wasUsed = true;

const noTranslation = Vector3.Zero();
const noRotation = Quaternion.Identity();
const noScale = Vector3.One();

// retreive all the instance world matrix
const matrix = babylonNode.thinInstanceGetWorldMatrices();

const iwt = TmpVectors.Vector3[2];
const iwr = TmpVectors.Quaternion[1];
const iws = TmpVectors.Vector3[3];

let hasAnyInstanceWorldTranslation = false;
let hasAnyInstanceWorldRotation = false;
let hasAnyInstanceWorldScale = false;

// prepare temp buffers
const translationBuffer = new Float32Array(babylonNode.thinInstanceCount * 3);
const rotationBuffer = new Float32Array(babylonNode.thinInstanceCount * 4);
const scaleBuffer = new Float32Array(babylonNode.thinInstanceCount * 3);

matrix.forEach((m, i) => {
pandaGaume marked this conversation as resolved.
Show resolved Hide resolved
m.decompose(iws, iwr, iwt);

// fill the temp buffer
translationBuffer.set(iwt.asArray(), i * 3);
rotationBuffer.set(iwr.normalize().asArray(), i * 4); // ensure the quaternion is normalized
scaleBuffer.set(iws.asArray(), i * 3);

// this is where we decide if there is any transformation
hasAnyInstanceWorldTranslation = hasAnyInstanceWorldTranslation || !iwt.equalsWithEpsilon(noTranslation);
hasAnyInstanceWorldRotation = hasAnyInstanceWorldRotation || !iwr.equalsWithEpsilon(noRotation);
hasAnyInstanceWorldScale = hasAnyInstanceWorldScale || !iws.equalsWithEpsilon(noScale);
});

/* eslint-disable @typescript-eslint/naming-convention*/
const gpu_instancing: IEXTMeshGpuInstancing = {
pandaGaume marked this conversation as resolved.
Show resolved Hide resolved
attributes: {},
};

// do we need to write TRANSLATION ?
if (hasAnyInstanceWorldTranslation) {
gpu_instancing.attributes["TRANSLATION"] = this._buildAccessor(
translationBuffer,
AccessorType.VEC3,
babylonNode.thinInstanceCount,
binaryWriter,
AccessorComponentType.FLOAT
);
}
// do we need to write ROTATION ?
if (hasAnyInstanceWorldRotation) {
// Data type can be
// - 5126 (FLOAT)
// - 5120 (BYTE) normalized
// - 5122 (SHORT) normalized
// this is defined by option.
let componentType = this._exporter.options?.meshGpuInstancingOptions?.quaternionType ?? AccessorComponentType.FLOAT;
if (componentType != AccessorComponentType.FLOAT && componentType != AccessorComponentType.SHORT && componentType != AccessorComponentType.BYTE) {
// force to float if wrong type.
componentType = AccessorComponentType.FLOAT;
}
gpu_instancing.attributes["ROTATION"] = this._buildAccessor(rotationBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, binaryWriter, componentType);
}
// do we need to write SCALE ?
if (hasAnyInstanceWorldScale) {
gpu_instancing.attributes["SCALE"] = this._buildAccessor(
scaleBuffer,
AccessorType.VEC3,
babylonNode.thinInstanceCount,
binaryWriter,
AccessorComponentType.FLOAT
);
}

/* eslint-enable @typescript-eslint/naming-convention*/
node.extensions = node.extensions || {};
node.extensions[NAME] = gpu_instancing;
}
}
resolve(node);
});
}

private _buildAccessor(
buffer: Float32Array | Int8Array | Int16Array,
type: AccessorType,
count: number,
binaryWriter: _BinaryWriter,
componentType: AccessorComponentType
): number {
// write the buffer
const bufferOffset = binaryWriter.getByteOffset();
switch (componentType) {
case AccessorComponentType.FLOAT: {
for (let i = 0; i != buffer.length; i++) {
binaryWriter.setFloat32(buffer[i]);
}
break;
}
case AccessorComponentType.BYTE: {
for (let i = 0; i != buffer.length; i++) {
binaryWriter.setByte(buffer[i] * 127);
}
break;
}
case AccessorComponentType.SHORT: {
for (let i = 0; i != buffer.length; i++) {
binaryWriter.setInt16(buffer[i] * 32767);
}
break;
}
pandaGaume marked this conversation as resolved.
Show resolved Hide resolved
}
// 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);

// finally build the accessor
const accessorIndex = this._exporter._accessors.length;
const accessor: IAccessor = { bufferView: bufferViewIndex, componentType: componentType, count: count, type: type };
pandaGaume marked this conversation as resolved.
Show resolved Hide resolved
this._exporter._accessors.push(accessor);
return accessorIndex;
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
_Exporter.RegisterExtension(NAME, (exporter) => new EXT_mesh_gpu_instancing(exporter));
1 change: 1 addition & 0 deletions packages/dev/serializers/src/glTF/2.0/Extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./KHR_materials_ior";
export * from "./KHR_materials_specular";
export * from "./KHR_materials_volume";
export * from "./KHR_materials_transmission";
export * from "./EXT_mesh_gpu_instancing";
56 changes: 53 additions & 3 deletions packages/dev/serializers/src/glTF/2.0/glTFExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,14 @@ export class _Exporter {
);
}

public _extensionsPostExportNodeAsync(context: string, node: Nullable<INode>, babylonNode: Node, nodeMap?: { [key: number]: number }): Promise<Nullable<INode>> {
return this._applyExtensions(node, (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap));
public _extensionsPostExportNodeAsync(
context: string,
node: Nullable<INode>,
babylonNode: Node,
nodeMap?: { [key: number]: number },
binaryWriter?: _BinaryWriter
): Promise<Nullable<INode>> {
return this._applyExtensions(node, (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap, binaryWriter));
}

public _extensionsPostExportMaterialAsync(context: string, material: Nullable<IMaterial>, babylonMaterial: Material): Promise<Nullable<IMaterial>> {
Expand Down Expand Up @@ -366,6 +372,10 @@ export class _Exporter {
}
}

public get options() {
return this._options;
}

/**
* Registers a glTF exporter extension
* @param name Name of the extension to export
Expand Down Expand Up @@ -2094,7 +2104,7 @@ export class _Exporter {
promiseChain = promiseChain.then(() => {
const convertToRightHandedSystem = this._convertToRightHandedSystemMap[babylonNode.uniqueId];
return this._createNodeAsync(babylonNode, binaryWriter, convertToRightHandedSystem).then((node) => {
const promise = this._extensionsPostExportNodeAsync("createNodeAsync", node, babylonNode, nodeMap);
const promise = this._extensionsPostExportNodeAsync("createNodeAsync", node, babylonNode, nodeMap, binaryWriter);
if (promise == null) {
Tools.Warn(`Not exporting node ${babylonNode.name}`);
return Promise.resolve();
Expand Down Expand Up @@ -2487,4 +2497,44 @@ export class _BinaryWriter {
this._byteOffset += 4;
}
}
/**
* 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);
}
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!");
}
} else {
if (this._byteOffset + 1 > this._arrayBuffer.byteLength) {
this._resizeBuffer(this._arrayBuffer.byteLength * 2);
}
this._dataView.setInt8(this._byteOffset, entry);
this._byteOffset++;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo
* @param babylonNode BabylonJS node
* @returns nullable INode promise
*/
postExportNodeAsync?(context: string, node: Nullable<INode>, babylonNode: Node, nodeMap?: { [key: number]: number }): Promise<Nullable<INode>>;
postExportNodeAsync?(context: string, node: Nullable<INode>, babylonNode: Node, nodeMap?: { [key: number]: number }, binaryWriter?: _BinaryWriter): Promise<Nullable<INode>>;

/**
* Define this method to modify the default behavior when exporting a material
Expand Down
16 changes: 16 additions & 0 deletions packages/dev/serializers/src/glTF/2.0/glTFSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import type { Node } from "core/node";
import type { Scene } from "core/scene";
import type { GLTFData } from "./glTFData";
import { _Exporter } from "./glTFExporter";
import { AccessorComponentType } from "babylonjs-gltf2interface";

/**
* Holds a collection of Extension Mesh Gpu Instancing options and parameters
*/
export interface IMeshGpuInstancingOptions {
/**
* Indicates the type used to write quaternion
*/
quaternionType?: AccessorComponentType;
}
pandaGaume marked this conversation as resolved.
Show resolved Hide resolved

/**
* Holds a collection of exporter options and parameters
Expand Down Expand Up @@ -40,6 +51,11 @@ export interface IExportOptions {
* Indicates if coordinate system swapping root nodes should be included in export
*/
includeCoordinateSystemConversionNodes?: boolean;

/**
* the EXT_mesh_gpu_instancing specific options.
*/
meshGpuInstancingOptions?: IMeshGpuInstancingOptions;
}

/**
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/tools/tests/test/visualization/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,11 @@
"playgroundId": "#9N6CLU#23",
"referenceImage": "glTFSerializerKhrMaterialsClearcoat.png"
},
{
"title": "GLTF Serializer KHR gpu instancing",
"playgroundId": "#1Q2BWN#10",
"referenceImage": "glTFSerializerKhrGpuInstancing.png"
},
{
"title": "GLTF Buggy with Draco Mesh Compression",
"playgroundId": "#JNW207#1",
Expand Down