diff --git a/examples/src/examples/loaders/gsplat-many.mjs b/examples/src/examples/loaders/gsplat-many.mjs index 8b082552596..212bc7e1ee5 100644 --- a/examples/src/examples/loaders/gsplat-many.mjs +++ b/examples/src/examples/loaders/gsplat-many.mjs @@ -26,7 +26,8 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath pc.RenderComponentSystem, pc.CameraComponentSystem, pc.LightComponentSystem, - pc.ScriptComponentSystem + pc.ScriptComponentSystem, + pc.GSplatComponentSystem ]; createOptions.resourceHandlers = [ // @ts-ignore @@ -55,8 +56,8 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath const assets = { gallery: new pc.Asset('gallery', 'container', { url: assetPath + 'models/vr-gallery.glb' }), - guitar: new pc.Asset('splat', 'gsplat', { url: assetPath + 'splats/guitar.ply' }), - biker: new pc.Asset('splat', 'gsplat', { url: assetPath + 'splats/biker.ply' }), + guitar: new pc.Asset('gsplat', 'gsplat', { url: assetPath + 'splats/guitar.ply' }), + biker: new pc.Asset('gsplat', 'gsplat', { url: assetPath + 'splats/biker.ply' }), orbit: new pc.Asset('script', 'script', { url: scriptsPath + 'camera/orbit-camera.js' }) }; @@ -80,8 +81,7 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath const createSplatInstance = (name, resource, px, py, pz, scale, vertex, fragment) => { - const splat = resource.instantiateRenderEntity({ - cameraEntity: camera, + const splat = resource.instantiate({ debugRender: false, fragment: fragment, vertex: vertex @@ -95,8 +95,12 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath const guitar = createSplatInstance('guitar', assets.guitar.resource, 0, 0.8, 0, 0.4, files['shader.vert'], files['shader.frag']); const biker1 = createSplatInstance('biker1', assets.biker.resource, -1.5, 0.05, 0, 0.7); - const biker2 = createSplatInstance('biker2', assets.biker.resource, 1.5, 0.05, 0.8, 0.7); + + // clone the biker and add the clone to the scene + const biker2 = biker1.clone(); + biker2.setLocalPosition(1.5, 0.05, 0); biker2.rotate(0, 150, 0); + app.root.addChild(biker2); // add orbit camera script with a mouse and a touch support camera.addComponent("script"); @@ -116,7 +120,7 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath app.on("update", function (dt) { currentTime += dt; - const material = guitar.render.meshInstances[0].material; + const material = guitar.gsplat.material; material.setParameter('uTime', currentTime); }); }); diff --git a/examples/src/examples/loaders/gsplat.mjs b/examples/src/examples/loaders/gsplat.mjs index bf4b65734d7..05d110af74f 100644 --- a/examples/src/examples/loaders/gsplat.mjs +++ b/examples/src/examples/loaders/gsplat.mjs @@ -26,7 +26,8 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath pc.RenderComponentSystem, pc.CameraComponentSystem, pc.LightComponentSystem, - pc.ScriptComponentSystem + pc.ScriptComponentSystem, + pc.GSplatComponentSystem ]; createOptions.resourceHandlers = [ // @ts-ignore @@ -54,7 +55,7 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath }); const assets = { - biker: new pc.Asset('splat', 'gsplat', { url: assetPath + 'splats/biker.ply' }), + biker: new pc.Asset('gsplat', 'gsplat', { url: assetPath + 'splats/biker.ply' }), orbit: new pc.Asset('script', 'script', { url: scriptsPath + 'camera/orbit-camera.js' }) }; @@ -74,8 +75,7 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath const createSplatInstance = (resource, px, py, pz, scale, vertex, fragment) => { - const splat = resource.instantiateRenderEntity({ - cameraEntity: camera, + const splat = resource.instantiate({ debugRender: false, fragment: fragment, vertex: vertex diff --git a/scripts/camera/orbit-camera.js b/scripts/camera/orbit-camera.js index d53cf072ab6..395e676097c 100644 --- a/scripts/camera/orbit-camera.js +++ b/scripts/camera/orbit-camera.js @@ -310,6 +310,15 @@ OrbitCamera.prototype._buildAabb = function (entity) { } } + var gsplats = entity.findComponents("gsplat"); + for (i = 0; i < gsplats.length; i++) { + var gsplat = gsplats[i]; + var instance = gsplat.instance; + if (instance) { + meshInstances.push(instance.meshInstance); + } + } + for (i = 0; i < meshInstances.length; i++) { if (i === 0) { this._modelsAabb.copy(meshInstances[i].aabb); diff --git a/src/framework/components/gsplat/component.js b/src/framework/components/gsplat/component.js new file mode 100644 index 00000000000..956c0073303 --- /dev/null +++ b/src/framework/components/gsplat/component.js @@ -0,0 +1,343 @@ +import { LAYERID_WORLD } from '../../../scene/constants.js'; +import { Asset } from '../../asset/asset.js'; +import { AssetReference } from '../../asset/asset-reference.js'; +import { Component } from '../component.js'; + +/** + * Enables an Entity to render a Gaussian Splat (asset of the 'gsplat' type). + * + * @augments Component + * @category Graphics + */ +class GSplatComponent extends Component { + /** @private */ + _layers = [LAYERID_WORLD]; // assign to the default world layer + + /** + * @type {import('../../../scene/gsplat/gsplat-instance.js').GSplatInstance|null} + * @private + */ + _instance = null; + + /** + * @type {import('../../../core/shape/bounding-box.js').BoundingBox|null} + * @private + */ + _customAabb = null; + + /** + * @type {AssetReference} + * @private + */ + _assetReference; + + /** + * Create a new GSplatComponent. + * + * @param {import('./system.js').GSplatComponentSystem} system - The ComponentSystem that + * created this Component. + * @param {import('../../entity.js').Entity} entity - The Entity that this Component is + * attached to. + */ + constructor(system, entity) { + super(system, entity); + + // gsplat asset reference + this._assetReference = new AssetReference( + 'asset', + this, + system.app.assets, { + add: this._onGSplatAssetAdded, + load: this._onGSplatAssetLoad, + remove: this._onGSplatAssetRemove, + unload: this._onGSplatAssetUnload + }, + this + ); + + // handle events when the entity is directly (or indirectly as a child of sub-hierarchy) + // added or removed from the parent + entity.on('remove', this.onRemoveChild, this); + entity.on('removehierarchy', this.onRemoveChild, this); + entity.on('insert', this.onInsertChild, this); + entity.on('inserthierarchy', this.onInsertChild, this); + } + + /** + * If set, the object space bounding box is used as a bounding box for visibility culling of + * attached gsplat. This allows a custom bounding box to be specified. + * + * @type {import('../../../core/shape/bounding-box.js').BoundingBox} + */ + set customAabb(value) { + this._customAabb = value; + + // set it on meshInstance + this._instance?.meshInstance.setCustomAabb(this._customAabb); + } + + get customAabb() { + return this._customAabb; + } + + /** + * A {@link GSplatInstance} contained in the component. If not set or loaded, it returns null. + * + * @ignore + */ + set instance(value) { + + // destroy existing instance + this.destroyInstance(); + + this._instance = value; + + if (this._instance) { + + // if mesh instance was created without a node, assign it here + const mi = this._instance.meshInstance; + if (!mi.node) { + mi.node = this.entity; + } + + mi.setCustomAabb(this._customAabb); + + if (this.enabled && this.entity.enabled) { + this.addToLayers(); + } + } + } + + get instance() { + return this._instance; + } + + /** + * Material used to render the gsplat. + * + * @type {import('../../../scene/materials/material.js').Material|undefined} + */ + get material() { + return this._instance?.material; + } + + /** + * An array of layer IDs ({@link Layer#id}) to which gsplats should belong. Don't push, pop, + * splice or modify this array, if you want to change it - set a new one instead. + * + * @type {number[]} + */ + set layers(value) { + + // remove the mesh instances from old layers + this.removeFromLayers(); + + // set the layer list + this._layers.length = 0; + for (let i = 0; i < value.length; i++) { + this._layers[i] = value[i]; + } + + // don't add into layers until we're enabled + if (!this.enabled || !this.entity.enabled) + return; + + // add the mesh instance to new layers + this.addToLayers(); + } + + get layers() { + return this._layers; + } + + /** + * The gsplat asset for the gsplat component - can also be an asset id. + * + * @type {Asset|number} + */ + set asset(value) { + + const id = value instanceof Asset ? value.id : value; + if (this._assetReference.id === id) return; + + if (this._assetReference.asset && this._assetReference.asset.resource) { + this._onGSplatAssetRemove(); + } + + this._assetReference.id = id; + + if (this._assetReference.asset) { + this._onGSplatAssetAdded(); + } + } + + get asset() { + return this._assetReference.id; + } + + /** + * Assign asset id to the component, without updating the component with the new asset. + * This can be used to assign the asset id to already fully created component. + * + * @param {Asset|number} asset - The gsplat asset or asset id to assign. + * @ignore + */ + assignAsset(asset) { + const id = asset instanceof Asset ? asset.id : asset; + this._assetReference.id = id; + } + + /** @private */ + destroyInstance() { + if (this._instance) { + this.removeFromLayers(); + this._instance?.destroy(); + this._instance = null; + } + } + + /** @private */ + addToLayers() { + const meshInstance = this.instance?.meshInstance; + if (meshInstance) { + const layers = this.system.app.scene.layers; + for (let i = 0; i < this._layers.length; i++) { + layers.getLayerById(this._layers[i])?.addMeshInstances([meshInstance]); + } + } + } + + removeFromLayers() { + const meshInstance = this.instance?.meshInstance; + if (meshInstance) { + const layers = this.system.app.scene.layers; + for (let i = 0; i < this._layers.length; i++) { + layers.getLayerById(this._layers[i])?.removeMeshInstances([meshInstance]); + } + } + } + + /** @private */ + onRemoveChild() { + this.removeFromLayers(); + } + + /** @private */ + onInsertChild() { + if (this._instance && this.enabled && this.entity.enabled) { + this.addToLayers(); + } + } + + onRemove() { + this.destroyInstance(); + + this.asset = null; + this._assetReference.id = null; + + this.entity.off('remove', this.onRemoveChild, this); + this.entity.off('insert', this.onInsertChild, this); + } + + onLayersChanged(oldComp, newComp) { + this.addToLayers(); + oldComp.off('add', this.onLayerAdded, this); + oldComp.off('remove', this.onLayerRemoved, this); + newComp.on('add', this.onLayerAdded, this); + newComp.on('remove', this.onLayerRemoved, this); + } + + onLayerAdded(layer) { + const index = this.layers.indexOf(layer.id); + if (index < 0) return; + if (this._instance) { + layer.addMeshInstances(this._instance.meshInstance); + } + } + + onLayerRemoved(layer) { + const index = this.layers.indexOf(layer.id); + if (index < 0) return; + if (this._instance) { + layer.removeMeshInstances(this._instance.meshInstance); + } + } + + onEnable() { + const scene = this.system.app.scene; + scene.on('set:layers', this.onLayersChanged, this); + if (scene.layers) { + scene.layers.on('add', this.onLayerAdded, this); + scene.layers.on('remove', this.onLayerRemoved, this); + } + + if (this._instance) { + this.addToLayers(); + } else if (this.asset) { + this._onGSplatAssetAdded(); + } + } + + onDisable() { + const scene = this.system.app.scene; + scene.off('set:layers', this.onLayersChanged, this); + if (scene.layers) { + scene.layers.off('add', this.onLayerAdded, this); + scene.layers.off('remove', this.onLayerRemoved, this); + } + + this.removeFromLayers(); + } + + /** + * Stop rendering this component without removing its mesh instance from the scene hierarchy. + */ + hide() { + if (this._instance) { + this._instance.meshInstance.visible = false; + } + } + + /** + * Enable rendering of the component if hidden using {@link GSplatComponent#hide}. + */ + show() { + if (this._instance) { + this._instance.meshInstance.visible = true; + } + } + + _onGSplatAssetAdded() { + if (!this._assetReference.asset) + return; + + if (this._assetReference.asset.resource) { + this._onGSplatAssetLoad(); + } else if (this.enabled && this.entity.enabled) { + this.system.app.assets.load(this._assetReference.asset); + } + } + + _onGSplatAssetLoad() { + + // remove existing instance + this.destroyInstance(); + + // create new instance + const asset = this._assetReference.asset; + if (asset) { + this.instance = asset.resource.createInstance(); + } + } + + _onGSplatAssetUnload() { + // when unloading asset, only remove the instance + this.destroyInstance(); + } + + _onGSplatAssetRemove() { + this._onGSplatAssetUnload(); + } +} + +export { GSplatComponent }; diff --git a/src/framework/components/gsplat/data.js b/src/framework/components/gsplat/data.js new file mode 100644 index 00000000000..c8b347b0158 --- /dev/null +++ b/src/framework/components/gsplat/data.js @@ -0,0 +1,7 @@ +class GSplatComponentData { + constructor() { + this.enabled = true; + } +} + +export { GSplatComponentData }; diff --git a/src/framework/components/gsplat/system.js b/src/framework/components/gsplat/system.js new file mode 100644 index 00000000000..2b5614913d0 --- /dev/null +++ b/src/framework/components/gsplat/system.js @@ -0,0 +1,100 @@ +import { Vec3 } from '../../../core/math/vec3.js'; +import { BoundingBox } from '../../../core/shape/bounding-box.js'; + +import { Component } from '../component.js'; +import { ComponentSystem } from '../system.js'; + +import { GSplatComponent } from './component.js'; +import { GSplatComponentData } from './data.js'; + +const _schema = [ + 'enabled' +]; + +// order matters here +const _properties = [ + 'instance', + 'asset', + 'layers' +]; + +/** + * Allows an Entity to render a gsplat. + * + * @augments ComponentSystem + * @category Graphics + */ +class GSplatComponentSystem extends ComponentSystem { + /** + * Create a new GSplatComponentSystem. + * + * @param {import('../../app-base.js').AppBase} app - The Application. + * @hideconstructor + */ + constructor(app) { + super(app); + + this.id = 'gsplat'; + + this.ComponentType = GSplatComponent; + this.DataType = GSplatComponentData; + + this.schema = _schema; + + this.on('beforeremove', this.onRemove, this); + } + + initializeComponentData(component, _data, properties) { + // duplicate layer list + if (_data.layers && _data.layers.length) { + _data.layers = _data.layers.slice(0); + } + + for (let i = 0; i < _properties.length; i++) { + if (_data.hasOwnProperty(_properties[i])) { + component[_properties[i]] = _data[_properties[i]]; + } + } + + if (_data.aabbCenter && _data.aabbHalfExtents) { + component.customAabb = new BoundingBox(new Vec3(_data.aabbCenter), new Vec3(_data.aabbHalfExtents)); + } + + super.initializeComponentData(component, _data, _schema); + } + + cloneComponent(entity, clone) { + + const gSplatComponent = entity.gsplat; + + // copy properties + const data = {}; + for (let i = 0; i < _properties.length; i++) { + data[_properties[i]] = gSplatComponent[_properties[i]]; + } + data.enabled = gSplatComponent.enabled; + + // gsplat instance cannot be used this way, remove it and manually clone it later + delete data.instance; + + // clone component + const component = this.addComponent(clone, data); + + // clone gsplat instance + component.instance = gSplatComponent.instance.clone(); + + if (gSplatComponent.customAabb) { + component.customAabb = gSplatComponent.customAabb.clone(); + } + + return component; + } + + onRemove(entity, component) { + component.onRemove(); + } +} + +Component._buildAccessors(GSplatComponent.prototype, _schema); + +export { GSplatComponentSystem }; diff --git a/src/framework/entity.js b/src/framework/entity.js index 408903b25ea..8d270838a74 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -94,6 +94,14 @@ class Entity extends GraphNode { */ element; + /** + * Gets the {@link GSplatComponent} attached to this entity. + * + * @type {import('./components/gsplat/component.js').GSplatComponent|undefined} + * @readonly + */ + gsplat; + /** * Gets the {@link LayoutChildComponent} attached to this entity. * @@ -288,6 +296,7 @@ class Entity extends GraphNode { * - "camera" - see {@link CameraComponent} * - "collision" - see {@link CollisionComponent} * - "element" - see {@link ElementComponent} + * - "gsplat" - see {@link GSplatComponent} * - "layoutchild" - see {@link LayoutChildComponent} * - "layoutgroup" - see {@link LayoutGroupComponent} * - "light" - see {@link LightComponent} diff --git a/src/framework/handlers/gsplat.js b/src/framework/handlers/gsplat.js index ae83a93f492..15c6e90e786 100644 --- a/src/framework/handlers/gsplat.js +++ b/src/framework/handlers/gsplat.js @@ -34,7 +34,6 @@ class GSplatHandler { } patch(asset, assets) { - } } diff --git a/src/framework/parsers/gsplat-resource.js b/src/framework/parsers/gsplat-resource.js index 2ca3ee4c764..655cc837945 100644 --- a/src/framework/parsers/gsplat-resource.js +++ b/src/framework/parsers/gsplat-resource.js @@ -1,21 +1,31 @@ import { BoundingBox } from '../../core/shape/bounding-box.js'; import { Entity } from '../entity.js'; import { GSplatInstance } from '../../scene/gsplat/gsplat-instance.js'; -import { Splat } from '../../scene/gsplat/gsplat.js'; +import { GSplat } from '../../scene/gsplat/gsplat.js'; class GSplatResource { - /** @type {import('../../platform/graphics/graphics-device.js').GraphicsDevice} */ + /** + * @type {import('../../platform/graphics/graphics-device.js').GraphicsDevice} + * @ignore + */ device; - /** @type {import('../../scene/gsplat/gsplat-data.js').GSplatData} */ + /** + * @type {import('../../scene/gsplat/gsplat-data.js').GSplatData} + * @ignore + */ splatData; - /** @type {Splat | null} */ + /** + * @type {GSplat | null} + * @ignore + */ splat = null; /** * @param {import('../../platform/graphics/graphics-device.js').GraphicsDevice} device - The graphics device. * @param {import('../../scene/gsplat/gsplat-data.js').GSplatData} splatData - The splat data. + * @hideconstructor */ constructor(device, splatData) { this.device = device; @@ -37,7 +47,7 @@ class GSplatResource { const aabb = new BoundingBox(); this.splatData.calcAabb(aabb); - const splat = new Splat(this.device, splatData.numSplats, aabb); + const splat = new GSplat(this.device, splatData.numSplats, aabb); this.splat = splat; // texture data @@ -65,43 +75,28 @@ class GSplatResource { /** * @param {import('../../scene/gsplat/gsplat-material.js').SplatMaterialOptions} [options] - The options. - * @returns {Entity} The GS entity. + * @returns {Entity} The entity with {@link GSplatComponent}. */ - instantiateRenderEntity(options = {}) { - - // shared splat between instances - const splat = this.createSplat(); + instantiate(options = {}) { - const splatInstance = new GSplatInstance(splat, options); + const splatInstance = this.createInstance(options); - const entity = new Entity('Splat'); - entity.addComponent('render', { - type: 'asset', - meshInstances: [splatInstance.meshInstance], - - // shadows not supported - castShadows: false + const entity = new Entity(); + const component = entity.addComponent('gsplat', { + instance: splatInstance }); // set custom aabb - entity.render.customAabb = splat.aabb.clone(); - - // HACK: store splat instance on the render component, to allow it to be destroyed in the following code - entity.render.splatInstance = splatInstance; + component.customAabb = splatInstance.splat.aabb.clone(); - // when the render component gets deleted, destroy the splat instance - entity.render.system.on('beforeremove', (entity, component) => { + return entity; + } - // HACK: the render component is already destroyed, so cannot get splat instance from the mesh instance, - // and so get it from the temporary property - // TODO: if this gets integrated into the engine, mesh instance would destroy splat instance - if (component.splatInstance) { - component.splatInstance?.destroy(); - component.splatInstance = null; - } - }, this); + createInstance(options = {}) { - return entity; + // shared splat between instances + const splat = this.createSplat(); + return new GSplatInstance(splat, options); } } diff --git a/src/index.js b/src/index.js index 00810baf157..5ad3b0ea95a 100644 --- a/src/index.js +++ b/src/index.js @@ -181,7 +181,7 @@ export { ShaderGenerator } from './scene/shader-lib/programs/shader-generator.js // SCENE / SPLAT export { GSplatData } from './scene/gsplat/gsplat-data.js'; -export { Splat } from './scene/gsplat/gsplat.js'; +export { GSplat } from './scene/gsplat/gsplat.js'; export { GSplatInstance } from './scene/gsplat/gsplat-instance.js'; // FRAMEWORK @@ -215,6 +215,7 @@ export { ElementComponentSystem } from './framework/components/element/system.js export { ElementDragHelper } from './framework/components/element/element-drag-helper.js'; export { Entity } from './framework/entity.js'; export { EntityReference } from './framework/utils/entity-reference.js'; +export { GSplatComponentSystem } from './framework/components/gsplat/system.js'; export { ImageElement } from './framework/components/element/image-element.js'; export * from './framework/components/joint/constants.js'; export { JointComponent } from './framework/components/joint/component.js'; diff --git a/src/scene/gsplat/gsplat-instance.js b/src/scene/gsplat/gsplat-instance.js index 5340d495dcb..aec1fcd1c3f 100644 --- a/src/scene/gsplat/gsplat-instance.js +++ b/src/scene/gsplat/gsplat-instance.js @@ -13,8 +13,9 @@ const cameraPosition = new Vec3(); const cameraDirection = new Vec3(); const viewport = [0, 0]; +/** @ignore */ class GSplatInstance { - /** @type {import('./gsplat.js').Splat} */ + /** @type {import('./gsplat.js').GSplat} */ splat; /** @type {Mesh} */ @@ -29,20 +30,33 @@ class GSplatInstance { /** @type {VertexBuffer} */ vb; - /** @type {GSplatSorter} */ - sorter; + options = {}; + + /** @type {GSplatSorter | null} */ + sorter = null; lastCameraPosition = new Vec3(); lastCameraDirection = new Vec3(); /** - * @param {import('./gsplat.js').Splat} splat - The splat instance. + * List of cameras this instance is visible for. Updated every frame by the renderer. + * + * @type {import('../camera.js').Camera[]} + * @ignore + */ + cameras = []; + + /** + * @param {import('./gsplat.js').GSplat} splat - The splat instance. * @param {import('./gsplat-material.js').SplatMaterialOptions} options - The options. */ constructor(splat, options) { this.splat = splat; + // clone options object + options = Object.assign(this.options, options); + // material const debugRender = options.debugRender; this.material = splat.createMaterial(options); @@ -89,25 +103,16 @@ class GSplatInstance { this.meshInstance = new MeshInstance(this.mesh, this.material); this.meshInstance.setInstancing(vb, true); - this.meshInstance.splatInstance = this; + this.meshInstance.gsplatInstance = this; - // clone centers to allow multiple instancing of sorter + // clone centers to allow multiple instances of sorter this.centers = new Float32Array(splat.centers); + // create sorter if (!options.dither || options.dither === DITHER_NONE) { this.sorter = new GSplatSorter(); this.sorter.init(this.vb, this.centers, !this.splat.device.isWebGL1); - - // if camera entity is provided, automatically use it to sort splats - const cameraEntity = options.cameraEntity; - if (cameraEntity) { - this.callbackHandle = cameraEntity._app.on('prerender', () => { - this.sort(cameraEntity); - }); - } } - - this.updateViewport(); } destroy() { @@ -115,10 +120,14 @@ class GSplatInstance { this.vb.destroy(); this.meshInstance.destroy(); this.sorter?.destroy(); - this.callbackHandle?.off(); + } + + clone() { + return new GSplatInstance(this.splat, this.options); } updateViewport() { + // TODO: improve, needs to handle render targets of different sizes const device = this.splat.device; viewport[0] = device.width; viewport[1] = device.height; @@ -126,35 +135,42 @@ class GSplatInstance { } /** - * Sorts the GS vertices based on the given camera entity. - * @param {import('playcanvas').Entity} camera - The camera entity used for sorting. - * @returns {boolean} Returns true if the sorting was performed, otherwise false. + * Sorts the GS vertices based on the given camera. + * @param {import('../graph-node.js').GraphNode} cameraNode - The camera node used for sorting. */ - sort(camera) { - - let sorted = false; + sort(cameraNode) { + if (this.sorter) { + const cameraMat = cameraNode.getWorldTransform(); + cameraMat.getTranslation(cameraPosition); + cameraMat.getZ(cameraDirection); + + const modelMat = this.meshInstance.node.getWorldTransform(); + const invModelMat = mat.invert(modelMat); + invModelMat.transformPoint(cameraPosition, cameraPosition); + invModelMat.transformVector(cameraDirection, cameraDirection); + + // sort if the camera has changed + if (!cameraPosition.equalsApprox(this.lastCameraPosition) || !cameraDirection.equalsApprox(this.lastCameraDirection)) { + this.lastCameraPosition.copy(cameraPosition); + this.lastCameraDirection.copy(cameraDirection); + this.sorter.setCamera(cameraPosition, cameraDirection); + } + } - const cameraMat = camera.getWorldTransform(); - cameraMat.getTranslation(cameraPosition); - cameraMat.getZ(cameraDirection); + this.updateViewport(); + } - const modelMat = this.meshInstance.node.getWorldTransform(); - const invModelMat = mat.invert(modelMat); - invModelMat.transformPoint(cameraPosition, cameraPosition); - invModelMat.transformVector(cameraDirection, cameraDirection); + update() { + if (this.cameras.length > 0) { - // sort if the camera has changed - if (!cameraPosition.equalsApprox(this.lastCameraPosition) || !cameraDirection.equalsApprox(this.lastCameraDirection)) { - this.lastCameraPosition.copy(cameraPosition); - this.lastCameraDirection.copy(cameraDirection); - sorted = true; + // sort by the first camera it's visible for + // TODO: extend to support multiple cameras + const camera = this.cameras[0]; + this.sort(camera._node); - this.sorter.setCamera(cameraPosition, cameraDirection); + // we get new list of cameras each frame + this.cameras.length = 0; } - - this.updateViewport(); - - return sorted; } } diff --git a/src/scene/gsplat/gsplat.js b/src/scene/gsplat/gsplat.js index 70cddec9d8f..d10f75d1e1f 100644 --- a/src/scene/gsplat/gsplat.js +++ b/src/scene/gsplat/gsplat.js @@ -17,7 +17,8 @@ import { createGSplatMaterial } from './gsplat-material.js'; * @property {boolean} isHalf - Indicates if the format uses half-precision floats. */ -class Splat { +/** @ignore */ +class GSplat { device; numSplats; @@ -305,4 +306,4 @@ class Splat { } } -export { Splat }; +export { GSplat }; diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js index 8a3c9fe73f3..f26122ae566 100644 --- a/src/scene/mesh-instance.js +++ b/src/scene/mesh-instance.js @@ -280,6 +280,12 @@ class MeshInstance { */ this._morphInstance = null; + /** + * @type {import('./gsplat/gsplat-instance.js').GSplatInstance|null} + * @ignore + */ + this.gsplatInstance = null; + this.instancingData = null; /** diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js index 2cd6ed2c21f..bcf5cc9d844 100644 --- a/src/scene/renderer/renderer.js +++ b/src/scene/renderer/renderer.js @@ -673,6 +673,19 @@ class Renderer { // #endif } + /** + * Update gsplats ahead of rendering. + * + * @param {import('../mesh-instance.js').MeshInstance[]|Set} drawCalls - MeshInstances + * containing gsplatInstances. + * @ignore + */ + updateGSplats(drawCalls) { + for (const drawCall of drawCalls) { + drawCall.gsplatInstance.update(); + } + } + /** * Update draw calls ahead of rendering. * @@ -685,6 +698,7 @@ class Renderer { // that are visible in this frame this.updateGpuSkinMatrices(drawCalls); this.updateMorphing(drawCalls); + this.updateGSplats(drawCalls); } setVertexBuffers(device, mesh) { @@ -930,8 +944,14 @@ class Renderer { const bucket = drawCall.transparent ? transparent : opaque; bucket.push(drawCall); - if (drawCall.skinInstance || drawCall.morphInstance) + if (drawCall.skinInstance || drawCall.morphInstance || drawCall.gsplatInstance) { this.processingMeshInstances.add(drawCall); + + // register visible cameras + if (drawCall.gsplatInstance) { + drawCall.gsplatInstance.cameras.push(camera); + } + } } } }