Skip to content

Commit

Permalink
GLTFSerializer : Ext mesh gpu instancing (#12495)
Browse files Browse the repository at this point in the history
* add Ext_mesh_gpu_instancing

* finalize Ext_mesh_gpu_instancing

* cleaning

* change signature of _buildAccessor

the buffer might be always a Float32Array because all the values are coming from matrix decompose

* remove quaternion quantization option
  • Loading branch information
pandaGaume committed May 27, 2022
1 parent 49a6cee commit 5a8db04
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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);

let i = 0;
for (const m of matrix) {
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);

i++;
}

const extension: IEXTMeshGpuInstancing = {
attributes: {},
};

// do we need to write TRANSLATION ?
if (hasAnyInstanceWorldTranslation) {
extension.attributes["TRANSLATION"] = this._buildAccessor(
translationBuffer,
AccessorType.VEC3,
babylonNode.thinInstanceCount,
binaryWriter,
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);
}
// do we need to write SCALE ?
if (hasAnyInstanceWorldScale) {
extension.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] = extension;
}
}
resolve(node);
});
}

private _buildAccessor(buffer: Float32Array, 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;
}
}
// 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,
normalized: componentType == AccessorComponentType.BYTE || componentType == AccessorComponentType.SHORT,
};
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 @@ -2490,4 +2500,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
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

0 comments on commit 5a8db04

Please sign in to comment.