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 all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}

deltakosh marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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
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