Browse Source

Add KHR_mesh_instancing (#7729)

* Initial draft of KHR_mesh_instancing implementation. Parses expected fields for attributes and instances meshes using the parsed buffer values.

* Modified KHR_mesh_instancing to lint properly, cleaned up instance TRS setting logic, added extension to glTF loader index

* Fix import hierarchy, disable parent/source mesh, properly use the mesh indicated on the extension for indexing

* remove comments

* remove hack, clean up instancing logic to reuse source instance as the first instance

* what's new

* fix linting errors

* Update loaders/src/glTF/2.0/glTFLoader.ts

Co-Authored-By: Gary Hsu <bghgary@users.noreply.github.com>

* Update loaders/src/glTF/2.0/glTFLoader.ts

Co-Authored-By: Gary Hsu <bghgary@users.noreply.github.com>

* Addressing code review comments, restructured promise execution order, added to ref population of quaternion components, created platform agnostic number string padding implementation

* replace let with const L66

* Fixed linting, updated docstrings

* Addressing code review comments, reworked attribute data storage and instance creation logic

* Add explicit include, remove semicolon

* Add error checking, cleanup imports and linting

* modify error message

Co-authored-by: Gary Hsu <bghgary@users.noreply.github.com>
Co-authored-by: David Catuhe <david.catuhe@live.fr>
Nicholas Barlow 5 years ago
parent
commit
b2f4f7c59a

+ 4 - 1
dist/preview release/what's new.md

@@ -20,8 +20,11 @@
 
 - Ammo.js IDL exposed property update and raycast vehicle stablization support ([MackeyK24](https://github.com/MackeyK24))
 
-### Materials
+### Loaders
+- Added support for glTF mesh instancing extension ([#7521](https://github.com/BabylonJS/Babylon.js/issues/7521)) ([drigax](https://github.com/Drigax))
+
 
+### Materials
 - Added the `roughness` and `albedoScaling` parameters to PBR sheen ([Popov72](https://github.com/Popov72))
 
 ## Bugs

+ 112 - 0
loaders/src/glTF/2.0/Extensions/KHR_mesh_instancing.ts

@@ -0,0 +1,112 @@
+import { Vector3, Quaternion } from 'babylonjs/Maths/math.vector';
+import { InstancedMesh } from 'babylonjs/Meshes/instancedMesh';
+import { Mesh } from 'babylonjs/Meshes/mesh';
+import { TransformNode } from "babylonjs/Meshes/transformNode";
+import { StringTools } from 'babylonjs/Misc/stringTools';
+import { Nullable } from "babylonjs/types";
+import { GLTFLoader, ArrayItem } from "../glTFLoader";
+import { IGLTFLoaderExtension } from "../glTFLoaderExtension";
+import { INode } from "../glTFLoaderInterfaces";
+
+const NAME = "KHR_mesh_instancing";
+
+interface IKHRMeshInstancing {
+    mesh?: number;
+    attributes: { [name: string]: number };
+}
+
+/**
+ * [Proposed Specification](https://github.com/KhronosGroup/glTF/pull/1691)
+ * [Playground Sample](//TODO)
+ * !!! Experimental Extension Subject to Changes !!!
+ */
+export class KHR_mesh_instancing implements IGLTFLoaderExtension {
+    /**
+     * The name of this extension.
+     */
+    public readonly name = NAME;
+
+    /**
+     * Defines whether this extension is enabled.
+     */
+    public enabled: boolean;
+
+    private _loader: GLTFLoader;
+
+    /** @hidden */
+    constructor(loader: GLTFLoader) {
+        this._loader = loader;
+        this.enabled = this._loader.isExtensionUsed(NAME);
+    }
+
+    /** @hidden */
+    public dispose() {
+        delete this._loader;
+    }
+
+    /** @hidden */
+    public loadNodeAsync(context: string, node: INode, assign: (babylonTransformNode: TransformNode) => void): Nullable<Promise<TransformNode>> {
+        return GLTFLoader.LoadExtensionAsync<IKHRMeshInstancing, TransformNode>(context, node, this.name, (extensionContext, extension) => {
+            return this._loader.loadNodeAsync(`#/nodes/${node.index}`, node, (babylonTransformNode) => {
+                const promises = new Array<Promise<any>>();
+                let instanceCount: Nullable<number> = null;
+                const loadAttribute = (attribute: string, assignBufferFunc: (data: Float32Array) => void) => {
+                    if (extension.attributes[attribute] == undefined) {
+                        return;
+                    }
+                    const accessor = ArrayItem.Get(`${extensionContext}/attributes/${attribute}`, this._loader.gltf.accessors, extension.attributes[attribute]);
+                    if (instanceCount === null) {
+                        instanceCount = accessor.count;
+                    } else if (instanceCount !== accessor.count) {
+                        throw new Error(`${extensionContext}/attributes: Instance buffer accessors do not have the same count.`);
+                    }
+                    promises.push(this._loader._loadFloatAccessorAsync(`/accessors/${accessor.bufferView}`, accessor).then((data) => {
+                        assignBufferFunc(data);
+                    }));
+                };
+                let translationBuffer: Nullable<Float32Array> = null;
+                let rotationBuffer: Nullable<Float32Array> = null;
+                let scaleBuffer: Nullable<Float32Array> = null;
+
+                loadAttribute("TRANSLATION", (data) => { translationBuffer = data; });
+                loadAttribute("ROTATION", (data) => { rotationBuffer = data; });
+                loadAttribute("SCALE", (data) => { scaleBuffer = data; });
+
+                return Promise.all(promises).then(() => {
+                    if (instanceCount) {
+                        let instanceName: string = "";
+                        let instance: Nullable<TransformNode> = null;
+                        const digitLength = instanceCount.toString().length;
+
+                        for (let i = 0; i < instanceCount; ++i) {
+                            if (node._primitiveBabylonMeshes) {
+                                for (let j = 0; j < node._primitiveBabylonMeshes.length; ++j) {
+                                    const babylonMeshPrimitive = node._primitiveBabylonMeshes[j];
+                                    instanceName = (babylonMeshPrimitive.name || babylonMeshPrimitive.id) + "_" + StringTools.PadNumber(i, digitLength);
+                                    if (babylonMeshPrimitive.isAnInstance) {
+                                        instance = (babylonMeshPrimitive as InstancedMesh).sourceMesh.createInstance(instanceName);
+                                    } else if ((babylonMeshPrimitive as Mesh).createInstance) {
+                                        instance = (babylonMeshPrimitive as Mesh).createInstance(instanceName);
+                                    }
+                                    if (instance) {
+                                        instance.setParent(babylonMeshPrimitive);
+                                        translationBuffer ? Vector3.FromArrayToRef(translationBuffer, i * 3, instance.position)
+                                            : instance.position.set(0, 0, 0);
+                                        rotationBuffer ? Quaternion.FromArrayToRef(rotationBuffer, i * 4, instance.rotationQuaternion!)
+                                            : instance.rotationQuaternion!.set(0, 0, 0, 1);
+                                        scaleBuffer ? Vector3.FromArrayToRef(scaleBuffer, i * 3, instance.scaling)
+                                            : instance.scaling.set(1, 1, 1);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    assign(babylonTransformNode);
+                    return babylonTransformNode;
+                });
+            });
+        });
+    }
+}
+
+GLTFLoader.RegisterExtension(NAME, (loader) => new KHR_mesh_instancing(loader));

+ 1 - 0
loaders/src/glTF/2.0/Extensions/index.ts

@@ -6,6 +6,7 @@ export * from "./KHR_materials_unlit";
 export * from "./KHR_materials_clearcoat";
 export * from "./KHR_materials_sheen";
 export * from "./KHR_materials_specular";
+export * from "./KHR_mesh_instancing";
 export * from "./KHR_mesh_quantization";
 export * from "./KHR_texture_basisu";
 export * from "./KHR_texture_transform";

+ 2 - 1
loaders/src/glTF/2.0/glTFLoader.ts

@@ -1561,7 +1561,8 @@ export class GLTFLoader implements IGLTFLoader {
         return accessor._data;
     }
 
-    private _loadFloatAccessorAsync(context: string, accessor: IAccessor): Promise<Float32Array> {
+    /** @hidden */
+    public _loadFloatAccessorAsync(context: string, accessor: IAccessor): Promise<Float32Array> {
         return this._loadAccessorAsync(context, accessor, Float32Array) as Promise<Float32Array>;
     }
 

+ 13 - 0
src/Maths/math.vector.ts

@@ -3189,6 +3189,19 @@ export class Quaternion {
     }
 
     /**
+     * Updates the given quaternion "result" from the starting index of the given array.
+     * @param array the array to pull values from
+     * @param offset the offset into the array to start at
+     * @param result the quaternion to store the result in
+     */
+    public static FromArrayToRef(array: DeepImmutable<ArrayLike<number>>, offset: number, result: Quaternion): void {
+        result.x = array[offset];
+        result.y = array[offset + 1];
+        result.z = array[offset + 2];
+        result.w = array[offset + 3];
+    }
+
+    /**
      * Create a quaternion from Euler rotation angles
      * @param x Pitch
      * @param y Yaw

+ 13 - 0
src/Misc/stringTools.ts

@@ -73,4 +73,17 @@ export class StringTools {
 
         return output;
     }
+
+    /**
+    * Converts a number to string and pads with preceeding zeroes until it is of specified length.
+    * @param num the number to convert and pad
+    * @param length the expected length of the string
+    * @returns the padded string
+    */
+    public static PadNumber(num: number, length: number): string {
+        var str = String(num);
+        while (str.length < length) { str = "0" + str; }
+        return str;
+    }
+
 }