From e70c14ab19d0692231a623ba9b028a6a7072db3e Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 21 Jul 2025 14:04:57 +0300 Subject: [PATCH 1/2] add/remove mesh instance --- src/framework/components/render/component.js | 150 ++++++++++++---- src/scene/layer.js | 167 ++++++++++++------ .../components/render/component.test.mjs | 82 +++++++++ 3 files changed, 313 insertions(+), 86 deletions(-) create mode 100644 test/framework/components/render/component.test.mjs diff --git a/src/framework/components/render/component.js b/src/framework/components/render/component.js index 75bc3b37579..8da8e822ef3 100644 --- a/src/framework/components/render/component.js +++ b/src/framework/components/render/component.js @@ -129,7 +129,7 @@ class RenderComponent extends Component { /** * Material used to render meshes other than asset type. It gets priority when set to - * something else than defaultMaterial, otherwise materialASsets[0] is used. + * something else than defaultMaterial, otherwise materialAssets[0] is used. * * @type {Material} * @private @@ -324,7 +324,7 @@ class RenderComponent extends Component { } /** - * Sets the array of meshInstances contained in the component. + * Sets the array of MeshInstances contained in the component. * * @type {MeshInstance[]} */ @@ -338,17 +338,7 @@ class RenderComponent extends Component { const mi = this._meshInstances; for (let i = 0; i < mi.length; i++) { - - // if mesh instance was created without a node, assign it here - if (!mi[i].node) { - mi[i].node = this.entity; - } - - mi[i].castShadow = this._castShadows; - mi[i].receiveShadow = this._receiveShadows; - mi[i].renderStyle = this._renderStyle; - mi[i].setLightmapped(this._lightmapped); - mi[i].setCustomAabb(this._customAabb); + this._updateMeshInstance(mi[i]); } if (this.enabled && this.entity.enabled) { @@ -760,6 +750,75 @@ class RenderComponent extends Component { return this._rootBone; } + /** + * @param {MeshInstance} meshInstance - MeshInstance that needs its properties updated. + * @private + */ + _updateMeshInstance(meshInstance) { + // if mesh instance was created without a node, assign it here + if (!meshInstance.node) { + meshInstance.node = this.entity; + } + + meshInstance.castShadow = this._castShadows; + meshInstance.receiveShadow = this._receiveShadows; + meshInstance.renderStyle = this._renderStyle; + meshInstance.setLightmapped(this._lightmapped); + meshInstance.setCustomAabb(this._customAabb); + } + + /** + * Adds a MeshInstance to this component. + * + * @param {MeshInstance} meshInstance - MeshInstance to add. + */ + addMeshInstance(meshInstance) { + Debug.assert(meshInstance instanceof MeshInstance, 'Invalid MeshInstance'); + const meshInstances = this._meshInstances; + + if (meshInstances) { + const index = meshInstances.indexOf(meshInstance); + if (index >= 0) { + Debug.warn('This MeshInstance already exists in this component'); + return; + } + meshInstances.push(meshInstance); + } else { + this._meshInstances = [meshInstance]; + } + + this._updateMeshInstance(meshInstance); + + if (this.enabled && this.entity.enabled) { + this.addToLayers(meshInstance); + } + } + + /** + * Removes a MeshInstance from this component. + * + * @param {MeshInstance} instance - MeshInstance to remove. + */ + removeMeshInstance(instance) { + Debug.assert(instance instanceof MeshInstance, 'Invalid MeshInstance'); + const meshInstances = this._meshInstances; + + if (meshInstances) { + const j = meshInstances.indexOf(instance); + if (j >= 0) { + const meshInstance = meshInstances[j]; + + this.removeFromLayers(meshInstance); + this._clearSkinInstance(meshInstance); + + meshInstances.splice(j, 1); + + // TODO + // do we want to destroy it on remove? + } + } + } + /** @private */ destroyMeshInstances() { const meshInstances = this._meshInstances; @@ -776,24 +835,45 @@ class RenderComponent extends Component { } } - /** @private */ - addToLayers() { - const layers = this.system.app.scene.layers; - for (let i = 0; i < this._layers.length; i++) { - const layer = layers.getLayerById(this._layers[i]); + /** + * @param {MeshInstance | null} [meshInstance] - An optional MeshInstance to add to layers. If + * not provided, all mesh instances will be added. + * @private + */ + addToLayers(meshInstance = null) { + const sceneLayers = this.system.app.scene.layers; + const componentLayers = this._layers; + const meshInstances = this._meshInstances; + + for (let i = 0; i < componentLayers.length; i++) { + const layer = sceneLayers.getLayerById(componentLayers[i]); if (layer) { - layer.addMeshInstances(this._meshInstances); + if (meshInstance) { + layer.addMeshInstance(meshInstance); + } else { + layer.addMeshInstances(meshInstances); + } } } } - removeFromLayers() { - if (this._meshInstances && this._meshInstances.length) { - const layers = this.system.app.scene.layers; - for (let i = 0; i < this._layers.length; i++) { - const layer = layers.getLayerById(this._layers[i]); - if (layer) { - layer.removeMeshInstances(this._meshInstances); + /** + * @param {MeshInstance | null} [meshInstance] - An optional MeshInstance to remove. If not + * provided, all mesh instances will be removed. + * @private + */ + removeFromLayers(meshInstance = null) { + const sceneLayers = this.system.app.scene.layers; + const componentLayers = this._layers; + const meshInstances = this._meshInstances; + + for (let i = 0; i < componentLayers.length; i++) { + const layer = sceneLayers.getLayerById(componentLayers[i]); + if (layer) { + if (meshInstance) { + layer.removeMeshInstance(meshInstance); + } else if (meshInstances?.length) { + layer.removeMeshInstances(meshInstances); } } } @@ -964,15 +1044,21 @@ class RenderComponent extends Component { } _clearSkinInstances() { - for (let i = 0; i < this._meshInstances.length; i++) { - const meshInstance = this._meshInstances[i]; - - // remove it from the cache - SkinInstanceCache.removeCachedSkinInstance(meshInstance.skinInstance); - meshInstance.skinInstance = null; + const meshInstances = this._meshInstances; + for (let i = 0; i < meshInstances.length; i++) { + this._clearSkinInstance(meshInstances[i]); } } + /** + * @param {MeshInstance} meshInstance - MeshInstance that needs to have skin instnace cleared. + */ + _clearSkinInstance(meshInstance) { + // remove it from the cache + SkinInstanceCache.removeCachedSkinInstance(meshInstance.skinInstance); + meshInstance.skinInstance = null; + } + _cloneSkinInstances() { if (this._meshInstances.length && this._rootBone instanceof GraphNode) { for (let i = 0; i < this._meshInstances.length; i++) { diff --git a/src/scene/layer.js b/src/scene/layer.js index 8de1b68115a..0263a3c2298 100644 --- a/src/scene/layer.js +++ b/src/scene/layer.js @@ -479,42 +479,75 @@ class Layer { * to cast shadows in this layer. Defaults to false. */ addMeshInstances(meshInstances, skipShadowCasters) { + for (let i = 0; i < meshInstances.length; i++) { + const meshInstance = meshInstances[i]; - const destMeshInstances = this.meshInstances; - const destMeshInstancesSet = this.meshInstancesSet; + // add mesh instance to the layer's array and the set + this._addInstanceToLayer(meshInstance); - // add mesh instances to the layer's array and the set - for (let i = 0; i < meshInstances.length; i++) { - const mi = meshInstances[i]; - if (!destMeshInstancesSet.has(mi)) { - destMeshInstances.push(mi); - destMeshInstancesSet.add(mi); - _tempMaterials.add(mi.material); + // shadow casters + if (!skipShadowCasters) { + this.addShadowCaster(meshInstance); } } - // shadow casters - if (!skipShadowCasters) { - this.addShadowCasters(meshInstances); + // clear old shader variants if necessary + if (_tempMaterials.size > 0) { + this._clearShaderVariants(); + } + } + + /** + * Adds a single MeshInstance to this layer. + * + * @param {MeshInstance} meshInstance - An instance of {@link MeshInstance}. + * @param {boolean} [skipShadowCaster] - Set it to true if you don't want this mesh instance + * to cast shadows in this layer. Defaults to false. + */ + addMeshInstance(meshInstance, skipShadowCaster = false) { + // add mesh instance to the layer's array and the set + this._addInstanceToLayer(meshInstance); + + // shadow caster + if (!skipShadowCaster) { + this.addShadowCaster(meshInstance); } // clear old shader variants if necessary if (_tempMaterials.size > 0) { - const sceneShaderVer = this._shaderVersion; - _tempMaterials.forEach((mat) => { - if (sceneShaderVer >= 0 && mat._shaderVersion !== sceneShaderVer) { - // skip this for materials not using variants - if (mat.getShaderVariant !== Material.prototype.getShaderVariant) { - // clear shader variants on the material and also on mesh instances that use it - mat.clearVariants(); - } - mat._shaderVersion = sceneShaderVer; - } - }); - _tempMaterials.clear(); + this._clearShaderVariants(); + } + } + + /** + * @param {MeshInstance} meshInstance - MeshInstance to add to layer's array and the set + */ + _addInstanceToLayer(meshInstance) { + const destMeshInstancesSet = this.meshInstancesSet; + + // add mesh instances to the layer's array and the set + if (!destMeshInstancesSet.has(meshInstance)) { + this.meshInstances.push(meshInstance); + destMeshInstancesSet.add(meshInstance); + _tempMaterials.add(meshInstance.material); } } + _clearShaderVariants() { + const sceneShaderVer = this._shaderVersion; + _tempMaterials.forEach((mat) => { + if (sceneShaderVer >= 0 && mat._shaderVersion !== sceneShaderVer) { + // skip this for materials not using variants + if (mat.getShaderVariant !== Material.prototype.getShaderVariant) { + // clear shader variants on the material and also on mesh instances that use it + mat.clearVariants(); + } + mat._shaderVersion = sceneShaderVer; + } + }); + _tempMaterials.clear(); + } + /** * Removes multiple mesh instances from this layer. * @@ -523,28 +556,37 @@ class Layer { * @param {boolean} [skipShadowCasters] - Set it to true if you want to still cast shadows from * removed mesh instances or if they never did cast shadows before. Defaults to false. */ - removeMeshInstances(meshInstances, skipShadowCasters) { + removeMeshInstances(meshInstances, skipShadowCasters = false) { + // mesh instances + for (let i = 0; i < meshInstances.length; i++) { + this.removeMeshInstance(meshInstances[i], skipShadowCasters); + } + } + /** + * Removes a single mesh instance from this layer. + * + * @param {MeshInstance} meshInstance - An instance of {@link MeshInstance}. If it was added to + * this layer, it will be removed. + * @param {boolean} [skipShadowCaster] - Set it to true if you want to still cast shadows from + * removed mesh instance or if it never did cast shadows before. Defaults to false. + */ + removeMeshInstance(meshInstance, skipShadowCaster = false) { const destMeshInstances = this.meshInstances; const destMeshInstancesSet = this.meshInstancesSet; - // mesh instances - for (let i = 0; i < meshInstances.length; i++) { - const mi = meshInstances[i]; - - // remove from mesh instances list - if (destMeshInstancesSet.has(mi)) { - destMeshInstancesSet.delete(mi); - const j = destMeshInstances.indexOf(mi); - if (j >= 0) { - destMeshInstances.splice(j, 1); - } + // remove from mesh instances list + if (destMeshInstancesSet.has(meshInstance)) { + destMeshInstancesSet.delete(meshInstance); + const j = destMeshInstances.indexOf(meshInstance); + if (j >= 0) { + destMeshInstances.splice(j, 1); } } // shadow casters - if (!skipShadowCasters) { - this.removeShadowCasters(meshInstances); + if (!skipShadowCaster) { + this.removeShadowCaster(meshInstance); } } @@ -555,15 +597,22 @@ class Layer { * @param {MeshInstance[]} meshInstances - Array of {@link MeshInstance}. */ addShadowCasters(meshInstances) { - const shadowCasters = this.shadowCasters; - const shadowCastersSet = this.shadowCastersSet; - for (let i = 0; i < meshInstances.length; i++) { - const mi = meshInstances[i]; - if (mi.castShadow && !shadowCastersSet.has(mi)) { - shadowCastersSet.add(mi); - shadowCasters.push(mi); - } + this.addShadowCaster(meshInstances[i]); + } + } + + /** + * Adds a single MeshInstance to this layer, but only as a shadow caster (it will not be + * rendered anywhere, but only cast shadows on other objects). + * + * @param {MeshInstance} meshInstance - Instance of {@link MeshInstance}. + */ + addShadowCaster(meshInstance) { + const shadowCastersSet = this.shadowCastersSet; + if (meshInstance.castShadow && !shadowCastersSet.has(meshInstance)) { + shadowCastersSet.add(meshInstance); + this.shadowCasters.push(meshInstance); } } @@ -575,17 +624,27 @@ class Layer { * this layer, they will be removed. */ removeShadowCasters(meshInstances) { + for (let i = 0; i < meshInstances.length; i++) { + this.removeShadowCaster(meshInstances[i]); + } + } + + /** + * Removes a single mesh instance from the shadow casters list of this layer, meaning it + * will stop casting shadows. + * + * @param {MeshInstance} meshInstance - Instance of {@link MeshInstance}. If it was added to + * this layer, it will be removed. + */ + removeShadowCaster(meshInstance) { const shadowCasters = this.shadowCasters; const shadowCastersSet = this.shadowCastersSet; - for (let i = 0; i < meshInstances.length; i++) { - const mi = meshInstances[i]; - if (shadowCastersSet.has(mi)) { - shadowCastersSet.delete(mi); - const j = shadowCasters.indexOf(mi); - if (j >= 0) { - shadowCasters.splice(j, 1); - } + if (shadowCastersSet.has(meshInstance)) { + shadowCastersSet.delete(meshInstance); + const j = shadowCasters.indexOf(meshInstance); + if (j >= 0) { + shadowCasters.splice(j, 1); } } } diff --git a/test/framework/components/render/component.test.mjs b/test/framework/components/render/component.test.mjs new file mode 100644 index 00000000000..56c1f44a27a --- /dev/null +++ b/test/framework/components/render/component.test.mjs @@ -0,0 +1,82 @@ +import { expect } from 'chai'; + +import { Entity } from '../../../../src/framework/entity.js'; +import { BoxGeometry } from '../../../../src/scene/geometry/box-geometry.js'; +import { MeshInstance } from '../../../../src/scene/mesh-instance.js'; +import { Mesh } from '../../../../src/scene/mesh.js'; +import { createApp } from '../../../app.mjs'; +import { jsdomSetup, jsdomTeardown } from '../../../jsdom.mjs'; + + +describe.only('RenderComponent', function () { + let app; + + beforeEach(function () { + jsdomSetup(); + app = createApp(); + }); + + afterEach(function () { + jsdomTeardown(); + app = null; + }); + + it('Add single MeshInstance', function () { + const entity = new Entity(); + app.root.addChild(entity); + entity.addComponent('render'); + + expect(entity.render._meshInstances.length).to.be.equal(0); + + const mesh = Mesh.fromGeometry(app.graphicsDevice, new BoxGeometry()); + + const instance1 = new MeshInstance(mesh, app.systems.render.defaultMaterial); + const instance2 = new MeshInstance(mesh, app.systems.render.defaultMaterial); + + entity.render.addMeshInstance(instance1); + entity.render.addMeshInstance(instance2); + entity.render.addMeshInstance(instance2); // test adding a duplicate instance + + expect(entity.render._meshInstances.length).to.be.equal(2); + + // Make sure the properties are updated + expect(instance1.node).to.be.equal(entity); + expect(instance1.castShadow).to.be.equal(entity.render._castShadows); + expect(instance1.receiveShadow).to.be.equal(entity.render._receiveShadows); + expect(instance1.renderStyle).to.be.equal(entity.render._renderStyle); + + // Make sure the instance is added to layers + const sceneLayers = app.scene.layers; + const componentLayers = entity.render._layers; + for (let i = 0; i < componentLayers.length; i++) { + const layer = sceneLayers.getLayerById(componentLayers[i]); + expect(layer.meshInstancesSet.has(instance1)).to.be.true; + expect(layer.shadowCastersSet.has(instance1)).to.be.true; + } + }); + + it('Remove single MeshInstance', function () { + const entity = new Entity(); + app.root.addChild(entity); + entity.addComponent('render'); + + const mesh = Mesh.fromGeometry(app.graphicsDevice, new BoxGeometry()); + const instance = new MeshInstance(mesh, app.systems.render.defaultMaterial); + entity.render.addMeshInstance(instance); + + expect(entity.render._meshInstances.length).to.be.equal(1); + + entity.render.removeMeshInstance(instance); + + expect(entity.render._meshInstances.length).to.be.equal(0); + + // Make sure the instance is removed from layers + const sceneLayers = app.scene.layers; + const componentLayers = entity.render._layers; + for (let i = 0; i < componentLayers.length; i++) { + const layer = sceneLayers.getLayerById(componentLayers[i]); + expect(layer.meshInstancesSet.has(instance)).to.be.false; + expect(layer.shadowCastersSet.has(instance)).to.be.false; + } + }); +}); From aa0b22e04160796996e35ec1da541b39570a10a5 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 21 Jul 2025 15:12:17 +0300 Subject: [PATCH 2/2] add destroy flag --- src/framework/components/render/component.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/framework/components/render/component.js b/src/framework/components/render/component.js index 8da8e822ef3..8540018eeae 100644 --- a/src/framework/components/render/component.js +++ b/src/framework/components/render/component.js @@ -798,8 +798,9 @@ class RenderComponent extends Component { * Removes a MeshInstance from this component. * * @param {MeshInstance} instance - MeshInstance to remove. + * @param {boolean} destroy - If true (default), destroys MeshInstance after remove. */ - removeMeshInstance(instance) { + removeMeshInstance(instance, destroy = true) { Debug.assert(instance instanceof MeshInstance, 'Invalid MeshInstance'); const meshInstances = this._meshInstances; @@ -813,8 +814,9 @@ class RenderComponent extends Component { meshInstances.splice(j, 1); - // TODO - // do we want to destroy it on remove? + if (destroy) { + meshInstance.destroy(); + } } } }