Gltf: unofficial _COLOR_0 for EXT_mesh_gpu_instancing

In the EXT_mesh_gpu_instancing it says:

As with vertex attributes, custom instance attributes may be prefixed with an underscore (e.g. _ID, _COLOR, etc.) and used for application-specific effects.

Currently three.js and gltfpack supports it to be used as instance color.

cc @bghgary

Never opposed to new extensions but I do not think we were planning on implementing it. do you want to contribute it ?

It seems three.js makes its instanceColor RGB instead of RGBA, so imported color should be converted to color4, and exported color will lose alpha info.

1 Like

Importing would be ok with a simple length check to handle both RGA and RGBA buffer, but for exporting things goes more complex:

  • Should it export with RGB or RGBA buffer, maybe with an extra option (or should it auto detect whether alpha channel used)?
  • Should user be warned (console, or custom callback for programmatically accessible) or should it just throw errors?
  • Should the feature be enabled by default (with unofficial color channel) or must manually enabled by user?
diff --git a/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
index d0d5460fa7..a494ac0e7f 100644
--- a/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
+++ b/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
@@ -92,10 +92,11 @@ export class EXT_mesh_gpu_instancing implements IGLTFLoaderExtension {
             loadAttribute("TRANSLATION");
             loadAttribute("ROTATION");
             loadAttribute("SCALE");
+            loadAttribute("_COLOR");
 
             // eslint-disable-next-line github/no-then
             return await promise.then(async (babylonTransformNode) => {
-                const [translationBuffer, rotationBuffer, scaleBuffer] = await Promise.all(promises);
+                const [translationBuffer, rotationBuffer, scaleBuffer, colorBuffer] = await Promise.all(promises);
                 const matrices = new Float32Array(instanceCount * 16);
                 TmpVectors.Vector3[0].copyFromFloats(0, 0, 0); // translation
                 TmpVectors.Quaternion[0].copyFromFloats(0, 0, 0, 1); // rotation
@@ -111,6 +112,19 @@ export class EXT_mesh_gpu_instancing implements IGLTFLoaderExtension {
                 }
                 for (const babylonMesh of node._primitiveBabylonMeshes!) {
                     (babylonMesh as Mesh).thinInstanceSetBuffer("matrix", matrices, 16, true);
+                    if (colorBuffer) {
+                        if (colorBuffer.length === instanceCount * 3) {
+                            const colorBufferRgba = new Float32Array(instanceCount * 4);
+                            for (let i = 0; i < instanceCount; i++) {
+                                colorBufferRgba[i * 4 + 0] = colorBuffer[i * 3 + 0];
+                                colorBufferRgba[i * 4 + 1] = colorBuffer[i * 3 + 1];
+                                colorBufferRgba[i * 4 + 2] = colorBuffer[i * 3 + 2];
+                                colorBufferRgba[i * 4 + 3] = 1;
+                            }
+                        } else if (colorBuffer && colorBuffer.length === instanceCount * 4) {
+                            (babylonMesh as Mesh).thinInstanceSetBuffer("color", colorBuffer, 4, true);
+                        }
+                    }
                 }
                 return babylonTransformNode;
             });
diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
index ebec77fdd9..c072359596 100644
--- a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
+++ b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
@@ -26,6 +26,17 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {
     /** Defines whether this extension is required */
     public required = false;
 
+    /** Defines whether unofficial instance color should be exported */
+    public exportInstanceColor = false;
+
+    /**
+     * Defines which format of unofficial instance color should be exported
+     * - AUTO: VEC3 if all alpha === 1.0, VEC4 if not
+     * - VEC3: three.js compatible RGB color, without alpha channel
+     * - VEC4: RGBA color
+     */
+    public instanceColorFormat = "AUTO";
+
     private _exporter: GLTFExporter;
 
     private _wasUsed = false;
@@ -123,6 +134,35 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {
                     if (hasAnyInstanceWorldScale) {
                         extension.attributes["SCALE"] = this._buildAccessor(scaleBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, bufferManager);
                     }
+                    const colorBuffer = babylonNode._userThinInstanceBuffersStorage?.data?.instanceColor;
+                    if (colorBuffer && this.exportInstanceColor) {
+                        let format = this.instanceColorFormat;
+                        if (format === "AUTO") {
+                            for (let i = 3, length = colorBuffer.length; i < length; i += 4) {
+                                // Or maybe a slower comparison with tolerance `Math.abs(colorBuffer[i] - 1) > 1.2e-7`
+                                if (colorBuffer[i] !== 1) {
+                                    format = "VEC4";
+                                    break;
+                                }
+                            }
+                            if (format === "AUTO") {
+                                format = "VEC3";
+                            }
+                        }
+                        if (format === "VEC4") {
+                            extension.attributes["_COLOR"] = this._buildAccessor(colorBuffer, AccessorType.VEC4, babylonNode.thinInstanceCount, bufferManager);
+                        } else {
+                            const instanceCount = babylonNode.thinInstanceCount;
+                            const colorBufferRgb = new Float32Array(instanceCount * 3);
+
+                            for (let i = 0; i < instanceCount; i++) {
+                                colorBufferRgb[i * 3 + 0] = colorBuffer[i * 4 + 0];
+                                colorBufferRgb[i * 3 + 1] = colorBuffer[i * 4 + 1];
+                                colorBufferRgb[i * 3 + 2] = colorBuffer[i * 4 + 2];
+                            }
+                            extension.attributes["_COLOR"] = this._buildAccessor(colorBufferRgb, AccessorType.VEC3, babylonNode.thinInstanceCount, bufferManager);
+                        }
+                    }
 
                     /* eslint-enable @typescript-eslint/naming-convention*/
                     node.extensions = node.extensions || {};

let see what @alexchuber our exporter wizard thinks ?

While I’m hesitant about introducing application-specific features to the glTF loader and exporter, I agree that since instance colors are widely supported, adding them to EXT_mesh_gpu_instancing is a logical next step. Let’s see what @bghgary thinks.

If we do proceed, I think the implementation should eventually align with existing glTF conventions for vertex colors.

  • Import: Babylon.js already handles both RGB and RGBA instance colors, so we can simply use the data as provided.
  • Export: To be extra safe, we should start by exporting only RGB data. If the source data has an alpha channel, we can log a warning that it’s being discarded. This will give us room to include RGBA export later.

I’m okay with adding support for instance colors. They are application specific, but Babylon.js can be considered an application in this case.

1 Like

Not fully sure about that, since in shaders the instanceColor is vec4, so I’m assuming the colorBuffer.length should be instanceCount * 4

Ok, then default to RGB with warning, but can be configured to RGB without warning, or RGBA.

This case is handled at the rendering context level, where a missing fourth component will fall back to a default value:

That works for me— thanks a bunch!
Though I’d recommend against adding any exporter options. We can always add an option later, but we can’t remove it once it’s there :wink:

Yeah, seems I’ve missed that, so importer can just keep the source data, and exporter should handle both color3 and color4.

What about marking the option @experimental, this would enable it to be changed in future, or we can just review the option here.

Sounds good to me!

Ok, now here is the updated options:


/**
 * Defines which format of unofficial instance color should be exported
 * @experimental This might be changed in future
 */
export enum GLTFExporterInstanceColorFormat {
    /**
     * Export instance color as RGB and ignore alpha channel,
     * and emit a warning if mesh.hasVertexAlpha is true and color buffer is Color4.
     */
    RGBAndWarnOnAlpha,
    /**
     * Export instance color as RGB and ignore alpha channel without a warning
     */
    RGB,
    /**
     * Export instance color as RGBA
     */
    RGBA,
    /**
     * Export instance color as RGBA if mesh.hasVertexAlpha is true and color buffer is Color4, or export as RGB
     */
    AUTO,
}

class EXT_mesh_gpu_instancing {

    /**
     * Defines whether unofficial instance color should be exported
     * To keep behavior compatible with previous versions, this defaults to false
     * @experimental This might be changed in future
     */
    public exportInstanceColor = false;

    /**
     * Defines which format of unofficial instance color should be exported
     * @experimental This might be changed in future
     */
    public instanceColorFormat  = GLTFExporterInstanceColorFormat.RGBAndWarnOnAlpha;

    /**
     * Internal state to emit warning about instance color alpha once
     */
    private _instanceColorWarned = false;
}

About detecting alpha channel, to detect from mesh.hasVertexAlpha or color buffer, which is better, or should we just use both?
Also, do you think that mesh.hasVertexAlpha should be set to true if the imported color buffer is VEC4 (or has one or more data with alpha != 0)?

And the updated patch here:

diff --git a/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
index d0d5460fa7..fa11a4bc03 100644
--- a/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
+++ b/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
@@ -92,10 +92,11 @@ export class EXT_mesh_gpu_instancing implements IGLTFLoaderExtension {
             loadAttribute("TRANSLATION");
             loadAttribute("ROTATION");
             loadAttribute("SCALE");
+            loadAttribute("_COLOR");
 
             // eslint-disable-next-line github/no-then
             return await promise.then(async (babylonTransformNode) => {
-                const [translationBuffer, rotationBuffer, scaleBuffer] = await Promise.all(promises);
+                const [translationBuffer, rotationBuffer, scaleBuffer, colorBuffer] = await Promise.all(promises);
                 const matrices = new Float32Array(instanceCount * 16);
                 TmpVectors.Vector3[0].copyFromFloats(0, 0, 0); // translation
                 TmpVectors.Quaternion[0].copyFromFloats(0, 0, 0, 1); // rotation
@@ -111,6 +112,13 @@ export class EXT_mesh_gpu_instancing implements IGLTFLoaderExtension {
                 }
                 for (const babylonMesh of node._primitiveBabylonMeshes!) {
                     (babylonMesh as Mesh).thinInstanceSetBuffer("matrix", matrices, 16, true);
+                    if (colorBuffer) {
+                        if (colorBuffer.length === instanceCount * 3) {
+                            (babylonMesh as Mesh).thinInstanceSetBuffer("color", colorBuffer, 3, true);
+                        } else if (colorBuffer && colorBuffer.length === instanceCount * 4) {
+                            (babylonMesh as Mesh).thinInstanceSetBuffer("color", colorBuffer, 4, true);
+                        }
+                    }
                 }
                 return babylonTransformNode;
             });
diff --git a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
index ebec77fdd9..5b158f6c04 100644
--- a/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
+++ b/packages/dev/serializers/src/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.ts
@@ -10,8 +10,56 @@ import "core/Meshes/thinInstanceMesh";
 import { TmpVectors, Quaternion, Vector3 } from "core/Maths/math.vector";
 import { ConvertToRightHandedPosition, ConvertToRightHandedRotation } from "../glTFUtilities";
 
+import { Logger } from "core/Misc/logger";
+
+/**
+ * Defines which format of unofficial instance color should be exported
+ * @experimental This might be changed in future
+ */
+export enum GLTFExporterInstanceColorFormat {
+    /**
+     * Export instance color as RGB and ignore alpha channel,
+     * and emit a warning if mesh.hasVertexAlpha is true and color buffer is Color4.
+     */
+    RGBAndWarnOnAlpha,
+    /**
+     * Export instance color as RGB and ignore alpha channel without a warning
+     */
+    RGB,
+    /**
+     * Export instance color as RGBA
+     */
+    RGBA,
+    /**
+     * Export instance color as RGBA if mesh.hasVertexAlpha is true and color buffer is Color4, or export as RGB
+     */
+    AUTO,
+}
+
 const NAME = "EXT_mesh_gpu_instancing";
 
+function ColorBufferToRGBAToRGB(colorBuffer: Float32Array, instanceCount: number) {
+    const colorBufferRgb = new Float32Array(instanceCount * 3);
+
+    for (let i = 0; i < instanceCount; i++) {
+        colorBufferRgb[i * 3 + 0] = colorBuffer[i * 4 + 0];
+        colorBufferRgb[i * 3 + 1] = colorBuffer[i * 4 + 1];
+        colorBufferRgb[i * 3 + 2] = colorBuffer[i * 4 + 2];
+    }
+    return colorBufferRgb;
+}
+
+function ColorBufferToRGBToRGBA(colorBuffer: Float32Array, instanceCount: number) {
+    const colorBufferRgb = new Float32Array(instanceCount * 4);
+
+    for (let i = 0; i < instanceCount; i++) {
+        colorBufferRgb[i * 4 + 0] = colorBuffer[i * 3 + 0];
+        colorBufferRgb[i * 4 + 1] = colorBuffer[i * 3 + 1];
+        colorBufferRgb[i * 4 + 2] = colorBuffer[i * 3 + 2];
+        colorBufferRgb[i * 4 + 3] = 1;
+    }
+    return colorBufferRgb;
+}
 /**
  * [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing/README.md)
  */
@@ -26,6 +74,24 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {
     /** Defines whether this extension is required */
     public required = false;
 
+    /**
+     * Defines whether unofficial instance color should be exported
+     * To keep behavior compatible with previous versions, this defaults to false
+     * @experimental This might be changed in future
+     */
+    public exportInstanceColor = false;
+
+    /**
+     * Defines which format of unofficial instance color should be exported
+     * @experimental This might be changed in future
+     */
+    public instanceColorFormat = GLTFExporterInstanceColorFormat.RGBAndWarnOnAlpha;
+
+    /**
+     * Internal state to emit warning about instance color alpha once
+     */
+    private _instanceColorWarned = false;
+
     private _exporter: GLTFExporter;
 
     private _wasUsed = false;
@@ -123,6 +189,42 @@ export class EXT_mesh_gpu_instancing implements IGLTFExporterExtensionV2 {
                     if (hasAnyInstanceWorldScale) {
                         extension.attributes["SCALE"] = this._buildAccessor(scaleBuffer, AccessorType.VEC3, babylonNode.thinInstanceCount, bufferManager);
                     }
+                    let colorBuffer = babylonNode._userThinInstanceBuffersStorage?.data?.instanceColor;
+                    if (colorBuffer && this.exportInstanceColor) {
+                        const format = this.instanceColorFormat;
+                        const instanceCount = babylonNode.thinInstanceCount;
+                        if (format === GLTFExporterInstanceColorFormat.RGBAndWarnOnAlpha) {
+                            if (babylonNode.hasVertexAlpha && colorBuffer.length === instanceCount * 4) {
+                                if (!this._instanceColorWarned) {
+                                    Logger.Warn("EXT_mesh_gpu_instancing: Exporting instance colors as RGB, alpha channel of instance color is not exported");
+                                    this._instanceColorWarned = true;
+                                }
+                                colorBuffer = ColorBufferToRGBAToRGB(colorBuffer, instanceCount);
+                            } else if (colorBuffer.length === instanceCount * 4) {
+                                colorBuffer = ColorBufferToRGBAToRGB(colorBuffer, instanceCount);
+                            }
+                            extension.attributes["_COLOR"] = this._buildAccessor(colorBuffer, AccessorType.VEC3, instanceCount, bufferManager);
+                        } else if (format === GLTFExporterInstanceColorFormat.RGB) {
+                            if (colorBuffer.length === instanceCount * 4) {
+                                colorBuffer = ColorBufferToRGBAToRGB(colorBuffer, instanceCount);
+                            }
+                            extension.attributes["_COLOR"] = this._buildAccessor(colorBuffer, AccessorType.VEC3, instanceCount, bufferManager);
+                        } else if (format === GLTFExporterInstanceColorFormat.RGBA) {
+                            if (colorBuffer.length === instanceCount * 3) {
+                                colorBuffer = ColorBufferToRGBToRGBA(colorBuffer, instanceCount);
+                            }
+                            extension.attributes["_COLOR"] = this._buildAccessor(colorBuffer, AccessorType.VEC4, instanceCount, bufferManager);
+                        } else if (format === GLTFExporterInstanceColorFormat.AUTO) {
+                            if (babylonNode.hasVertexAlpha && colorBuffer.length === instanceCount * 4) {
+                                extension.attributes["_COLOR"] = this._buildAccessor(colorBuffer, AccessorType.VEC4, instanceCount, bufferManager);
+                            } else if (colorBuffer.length === instanceCount * 4) {
+                                colorBuffer = ColorBufferToRGBAToRGB(colorBuffer, instanceCount);
+                                extension.attributes["_COLOR"] = this._buildAccessor(colorBuffer, AccessorType.VEC3, instanceCount, bufferManager);
+                            } else {
+                                extension.attributes["_COLOR"] = this._buildAccessor(colorBuffer, AccessorType.VEC3, instanceCount, bufferManager);
+                            }
+                        }
+                    }
 
                     /* eslint-enable @typescript-eslint/naming-convention*/
                     node.extensions = node.extensions || {};

I think it’d be easier if we moved this discussion to a draft PR-- wanna start one up? :slight_smile:

3 Likes