Repository: playcanvas/engine
Pull request #6711 (2024-06-17 12:29:40)

Gsplat fixes
Cherry picked #6710 from engine v1.


Patch:
diff --git a/src/core/sort.js b/src/core/sort.js
index 062667ced..627891ff0 100644
--- a/src/core/sort.js
+++ b/src/core/sort.js
@@ -6,6 +6,15 @@
  */
 const cmpPriority = (a, b) => a.priority - b.priority;
 
+
+/**
+ * @param {{order: number}} a - First object with `order` property.
+ * @param {{order: number}} b - Second object with `order` property.
+ * @returns {number} A number indicating the relative position.
+ * @ignore
+ */
+const cmpStaticOrder = (a, b) => a.constructor.order - b.constructor.order;
+
 /**
  * @param {Array<{priority: number}>} arr - Array to be sorted in place where each element contains
  * an object with at least a priority property.
@@ -13,3 +22,11 @@ const cmpPriority = (a, b) => a.priority - b.priority;
  * @ignore
  */
 export const sortPriority = arr => arr.sort(cmpPriority);
+
+/**
+ * @param {Array<{order: number}>} arr - Array to be sorted in place where each element contains
+ * an object with a static `order` property.
+ * @returns {Array<{order: number}>} In place sorted array.
+ * @ignore
+ */
+export const sortStaticOrder = arr => arr.sort(cmpStaticOrder);
diff --git a/src/framework/components/component.js b/src/framework/components/component.js
index a034fa226..062c4e5ba 100644
--- a/src/framework/components/component.js
+++ b/src/framework/components/component.js
@@ -5,6 +5,15 @@ import { EventHandler } from '../../core/event-handler.js';
  * events each frame, and expose properties to the PlayCanvas Editor.
  */
 class Component extends EventHandler {
+    /**
+     * Component order. When an entity with multiple components gets enabled, this order specifies
+     * in which order the components get enabled. The lowest number gets enabled first.
+     *
+     * @type {number} - Component order number.
+     * @private
+     */
+    static order = 0;
+
     /**
      * The ComponentSystem used to create this Component.
      *
diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js
index 226cc5a5a..2d8d9d579 100644
--- a/src/framework/components/rigid-body/component.js
+++ b/src/framework/components/rigid-body/component.js
@@ -114,6 +114,8 @@ class RigidBodyComponent extends Component {
      */
     static EVENT_TRIGGERLEAVE = 'triggerleave';
 
+    static order = -1;
+
     /** @private */
     _angularDamping = 0;
 
diff --git a/src/framework/components/system.js b/src/framework/components/system.js
index 506d68032..07679e438 100644
--- a/src/framework/components/system.js
+++ b/src/framework/components/system.js
@@ -67,15 +67,16 @@ class ComponentSystem extends EventHandler {
      * @ignore
      */
     removeComponent(entity) {
+        const id = this.id;
         const record = this.store[entity.getGuid()];
-        const component = entity.c[this.id];
+        const component = entity.c[id];
 
         this.fire('beforeremove', entity, component);
 
         delete this.store[entity.getGuid()];
 
-        entity[this.id] = undefined;
-        delete entity.c[this.id];
+        entity[id] = undefined;
+        delete entity.c[id];
 
         this.fire('remove', entity, record.data);
     }
diff --git a/src/framework/entity.js b/src/framework/entity.js
index de2ad5d2e..059b1302c 100644
--- a/src/framework/entity.js
+++ b/src/framework/entity.js
@@ -1,5 +1,6 @@
 import { Debug } from '../core/debug.js';
 import { guid } from '../core/guid.js';
+import { sortStaticOrder } from '../core/sort.js';
 
 import { GraphNode } from '../scene/graph-node.js';
 
@@ -11,6 +12,12 @@ import { getApplication } from './globals.js';
  */
 const _enableList = [];
 
+/**
+ * @type {Array<import('./components/component.js').Component>}
+ * @ignore
+ */
+const _sortedArray = [];
+
 /**
  * The Entity is the core primitive of a PlayCanvas game. Generally speaking an object in your game
  * will consist of an {@link Entity}, and a set of {@link Component}s which are managed by their
@@ -503,17 +510,14 @@ class Entity extends GraphNode {
     _onHierarchyStateChanged(enabled) {
         super._onHierarchyStateChanged(enabled);
 
-        // enable / disable all the components
-        const components = this.c;
-        for (const type in components) {
-            if (components.hasOwnProperty(type)) {
-                const component = components[type];
-                if (component.enabled) {
-                    if (enabled) {
-                        component.onEnable();
-                    } else {
-                        component.onDisable();
-                    }
+        const components = this._getSortedComponents();
+        for (let i = 0; i < components.length; i++) {
+            const component = components[i];
+            if (component.enabled) {
+                if (enabled) {
+                    component.onEnable();
+                } else {
+                    component.onDisable();
                 }
             }
         }
@@ -522,10 +526,9 @@ class Entity extends GraphNode {
     /** @private */
     _onHierarchyStatePostChanged() {
         // post enable all the components
-        const components = this.c;
-        for (const type in components) {
-            if (components.hasOwnProperty(type))
-                components[type].onPostStateChange();
+        const components = this._getSortedComponents();
+        for (let i = 0; i < components.length; i++) {
+            components[i].onPostStateChange();
         }
     }
 
@@ -601,6 +604,26 @@ class Entity extends GraphNode {
         return clone;
     }
 
+    _getSortedComponents() {
+        _sortedArray.length = 0;
+
+        const components = this.c;
+        let needSort = 0;
+        for (const type in components) {
+            if (components.hasOwnProperty(type)) {
+                const component = components[type];
+                needSort |= component.constructor.order !== 0;
+                _sortedArray.push(component);
+            }
+        }
+
+        if (needSort && _sortedArray.length > 1) {
+            sortStaticOrder(_sortedArray);
+        }
+
+        return _sortedArray;
+    }
+
     /**
      * @param {Object<string, Entity>} duplicatedIdsMap - A map of original entity GUIDs to cloned
      * entities.
diff --git a/src/scene/gsplat/gsplat-compressed-material.js b/src/scene/gsplat/gsplat-compressed-material.js
index c1b898cae..bd4101cd0 100644
--- a/src/scene/gsplat/gsplat-compressed-material.js
+++ b/src/scene/gsplat/gsplat-compressed-material.js
@@ -27,11 +27,12 @@ varying vec4 color;
 #endif
 
 uniform vec2 viewport;
-uniform vec4 bufferWidths;
+uniform vec4 bufferWidths;                  // packed width, chunked width, numSplats
 uniform highp usampler2D splatOrder;
 uniform highp usampler2D packedTexture;
 uniform highp sampler2D chunkTexture;
 
+uint orderId;
 uint splatId;
 ivec2 splatUV;
 ivec2 chunkUV;
@@ -46,7 +47,7 @@ void calcUV() {
     int chunkWidth = int(bufferWidths.y);
 
     // sample order texture
-    uint orderId = vertex_id_attrib + uint(vertex_position.z);
+    orderId = vertex_id_attrib + uint(vertex_position.z);
     ivec2 orderUV = ivec2(
         int(orderId) % packedWidth,
         int(orderId) / packedWidth
@@ -197,7 +198,7 @@ vec4 evalSplat() {
     vec4 centerClip = matrix_projection * centerView;
 
     // cull behind camera
-    if (centerClip.z < -centerClip.w) {
+    if (centerClip.z < -centerClip.w || orderId >= uint(bufferWidths.z)) {
         return vec4(0.0, 0.0, 2.0, 1.0);
     }
 
diff --git a/src/scene/gsplat/gsplat-compressed.js b/src/scene/gsplat/gsplat-compressed.js
index 000b41fcf..cdd675936 100644
--- a/src/scene/gsplat/gsplat-compressed.js
+++ b/src/scene/gsplat/gsplat-compressed.js
@@ -110,7 +110,7 @@ class GSplatCompressed {
         const result = createGSplatCompressedMaterial(options);
         result.setParameter('packedTexture', this.packedTexture);
         result.setParameter('chunkTexture', this.chunkTexture);
-        result.setParameter('bufferWidths', new Float32Array([this.packedTexture.width, this.chunkTexture.width / 3, 0, 0]));
+        result.setParameter('bufferWidths', new Float32Array([this.packedTexture.width, this.chunkTexture.width / 3, this.numSplats, 0]));
         return result;
     }
 
diff --git a/src/scene/gsplat/gsplat-instance.js b/src/scene/gsplat/gsplat-instance.js
index f31f9f537..89b80762b 100644
--- a/src/scene/gsplat/gsplat-instance.js
+++ b/src/scene/gsplat/gsplat-instance.js
@@ -118,7 +118,9 @@ class GSplatInstance {
         this.meshInstance = new MeshInstance(this.mesh, this.material);
         this.meshInstance.setInstancing(indicesVB, true);
         this.meshInstance.gsplatInstance = this;
-        this.meshInstance.instancingCount = numSplatInstances;
+
+        // only start rendering the splat after we've received the splat order data
+        this.meshInstance.instancingCount = 0;
 
         // clone centers to allow multiple instances of sorter
         this.centers = new Float32Array(splat.centers);


Test patch:
diff --git a/test/framework/entity.test.mjs b/test/framework/entity.test.mjs
index 51dd33c1e..1cd638507 100644
--- a/test/framework/entity.test.mjs
+++ b/test/framework/entity.test.mjs
@@ -32,6 +32,7 @@ import { DummyComponentSystem } from './test-component/system.mjs';
 import { HTMLCanvasElement } from '@playcanvas/canvas-mock';
 
 import { expect } from 'chai';
+import { stub } from 'sinon';
 
 describe('Entity', function () {
 
@@ -121,6 +122,68 @@ describe('Entity', function () {
             });
         }
 
+        it('respects components order on disable', function () {
+            const entity = new Entity();
+            entity.enabled = true;
+
+            entity.addComponent('collision');
+            entity.addComponent('rigidbody');
+
+            const colOnDisable = stub();
+            const rbOnDisable = stub();
+            let disableOrder = 0;
+
+            entity.collision.onDisable = colOnDisable;
+            entity.rigidbody.onDisable = rbOnDisable;
+
+            colOnDisable.onFirstCall().callsFake(() => {
+                disableOrder = 2;
+            });
+            rbOnDisable.onFirstCall().callsFake(() => {
+                disableOrder = 1;
+            });
+
+            entity.enabled = false;
+
+            expect(disableOrder).to.equal(2);
+
+            entity.destroy();
+        });
+
+        it('respects components order on enable', function () {
+            const entity = new Entity('Child');
+            const parent = new Entity('Parent');
+
+            parent.addChild(entity);
+            parent._enabled = true;
+            parent._enabledInHierarchy = true;
+
+            entity.addComponent('collision');
+            entity.addComponent('rigidbody');
+
+            entity.enabled = false;
+
+            const rbOnEnable = stub();
+            const colOnEnable = stub();
+            let enableOrder = 0;
+
+            entity.collision.onEnable = colOnEnable;
+            entity.rigidbody.onEnable = rbOnEnable;
+
+            colOnEnable.onFirstCall().callsFake(() => {
+                enableOrder = 2;
+            });
+            rbOnEnable.onFirstCall().callsFake(() => {
+                enableOrder = 1;
+            });
+
+            entity.enabled = true;
+
+            expect(enableOrder).to.equal(2);
+
+            parent.destroy();
+        });
+
     });
 
     const createSubtree = () => {