UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

785 lines (784 loc) 25.6 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { hashCode } from "../../../core/hash.js"; import { LAYERID_WORLD, WORKBUFFER_UPDATE_AUTO } from "../../../scene/constants.js"; import { GSplatInstance } from "../../../scene/gsplat/gsplat-instance.js"; import { Asset } from "../../asset/asset.js"; import { AssetReference } from "../../asset/asset-reference.js"; import { Component } from "../component.js"; import { Debug } from "../../../core/debug.js"; import { GSplatPlacement } from "../../../scene/gsplat-unified/gsplat-placement.js"; import { PickerId } from "../../../scene/picker-id.js"; const UNIFIED_LEGACY_HINT = "GSplatComponent#unified now defaults to true (unified rendering). To temporarily restore the deprecated legacy behavior, explicitly set unified=false when creating the component \u2014 note that non-unified mode will be removed in a future release."; class GSplatComponent extends Component { /** * Create a new GSplatComponent. * * @param {GSplatComponentSystem} system - The ComponentSystem that created this Component. * @param {Entity} entity - The Entity that this Component is attached to. */ constructor(system, entity) { super(system, entity); /** @private */ __publicField(this, "_layers", [LAYERID_WORLD]); // assign to the default world layer /** * @type {GSplatInstance|null} * @private */ __publicField(this, "_instance", null); /** * @type {GSplatPlacement|null} * @private */ __publicField(this, "_placement", null); /** * Unique identifier for this component, used by the picking system. * * @type {number} * @private */ __publicField(this, "_id", PickerId.get()); /** * @type {ShaderMaterial|null} * @private */ __publicField(this, "_materialTmp", null); /** @private */ __publicField(this, "_highQualitySH", true); /** * Base distance for the first LOD transition (LOD 0 to LOD 1). * * @private */ __publicField(this, "_lodBaseDistance", 5); /** * Geometric multiplier between successive LOD distance thresholds. * * @private */ __publicField(this, "_lodMultiplier", 3); /** * @type {BoundingBox|null} * @private */ __publicField(this, "_customAabb", null); /** * @type {AssetReference} * @private */ __publicField(this, "_assetReference"); /** * Direct resource reference (for container splats). * * @type {GSplatResourceBase|null} * @private */ __publicField(this, "_resource", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtLayersChanged", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtLayerAdded", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtLayerRemoved", null); /** @private */ __publicField(this, "_castShadows", false); /** * Whether to use the unified gsplat rendering. Defaults to true. * * @private */ __publicField(this, "_unified", true); /** * Per-instance shader parameters. Stores objects with scopeId and data. * * @type {Map<string, {scopeId: ScopeId, data: *}>} * @private */ __publicField(this, "_parameters", /* @__PURE__ */ new Map()); /** * Render mode for work buffer updates. * * @type {number} * @private */ __publicField(this, "_workBufferUpdate", WORKBUFFER_UPDATE_AUTO); /** * Custom shader modify code for this component (object with code and pre-computed hash). * * @type {{ code: string, hash: number }|null} * @private */ __publicField(this, "_workBufferModifier", null); this._assetReference = new AssetReference( "asset", this, system.app.assets, { add: this._onGSplatAssetAdded, load: this._onGSplatAssetLoad, remove: this._onGSplatAssetRemove, unload: this._onGSplatAssetUnload }, this ); entity.on("remove", this.onRemoveChild, this); entity.on("removehierarchy", this.onRemoveChild, this); entity.on("insert", this.onInsertChild, this); entity.on("inserthierarchy", this.onInsertChild, this); } /** * Sets a custom object space bounding box for visibility culling of the attached gsplat. * * @type {BoundingBox|null} */ set customAabb(value) { this._customAabb = value; this._instance?.meshInstance?.setCustomAabb(this._customAabb); if (this._placement) { this._placement.aabb = this._customAabb; } } /** * Gets the custom object space bounding box for visibility culling of the attached gsplat. * Returns the custom AABB if set, otherwise falls back to the resource's AABB. * * @type {BoundingBox|null} */ get customAabb() { return this._customAabb ?? this._placement?.aabb ?? this.resource?.aabb ?? null; } /** * Sets a {@link GSplatInstance} on the component. If not set or loaded, it returns null. * * @type {GSplatInstance|null} * @ignore */ set instance(value) { if (this.unified) { Debug.errorOnce(`GSplatComponent#instance setter is only available in legacy non-unified mode. ${UNIFIED_LEGACY_HINT}`); return; } this.destroyInstance(); this._instance = value; if (this._instance) { const mi = this._instance.meshInstance; if (!mi.node) { mi.node = this.entity; } mi.castShadow = this._castShadows; mi.setCustomAabb(this._customAabb); if (this.enabled && this.entity.enabled) { this.addToLayers(); } } } /** * Gets the {@link GSplatInstance} on the component. * * @type {GSplatInstance|null} * @ignore */ get instance() { if (this.unified) { Debug.warnOnce(`GSplatComponent#instance getter returns null in unified mode. ${UNIFIED_LEGACY_HINT}`); } return this._instance; } set material(value) { if (this.unified) { Debug.warn(`GSplatComponent#material setter is only available in legacy non-unified mode; in unified mode use app.systems.gsplat.getMaterial(camera, layer). ${UNIFIED_LEGACY_HINT}`); return; } if (this._instance) { this._instance.material = value; } else { this._materialTmp = value; } } get material() { if (this.unified) { Debug.warnOnce(`GSplatComponent#material getter returns null in unified mode; use app.systems.gsplat.getMaterial(camera, layer) instead. ${UNIFIED_LEGACY_HINT}`); return null; } return this._instance?.material ?? this._materialTmp ?? null; } set highQualitySH(value) { if (value !== this._highQualitySH) { this._highQualitySH = value; this._instance?.setHighQualitySH(value); } } get highQualitySH() { return this._highQualitySH; } /** * Sets whether gsplat will cast shadows for lights that have shadow casting enabled. Defaults * to false. * * @type {boolean} */ set castShadows(value) { if (this._castShadows !== value) { const layers = this.layers; const scene = this.system.app.scene; if (this._placement) { if (value) { for (let i = 0; i < layers.length; i++) { const layer = scene.layers.getLayerById(layers[i]); layer?.addGSplatShadowCaster(this._placement); } } else { for (let i = 0; i < layers.length; i++) { const layer = scene.layers.getLayerById(layers[i]); layer?.removeGSplatShadowCaster(this._placement); } } } const mi = this._instance?.meshInstance; if (mi) { if (this._castShadows && !value) { for (let i = 0; i < layers.length; i++) { const layer = scene.layers.getLayerById(this.layers[i]); layer?.removeShadowCasters([mi]); } } mi.castShadow = value; if (!this._castShadows && value) { for (let i = 0; i < layers.length; i++) { const layer = scene.layers.getLayerById(layers[i]); layer?.addShadowCasters([mi]); } } } this._castShadows = value; } } /** * Gets whether gsplat will cast shadows for lights that have shadow casting enabled. * * @type {boolean} */ get castShadows() { return this._castShadows; } /** * Sets the base distance for the first LOD transition (LOD 0 to LOD 1). Objects closer * than this distance use the highest quality LOD. Each subsequent LOD level transitions * at a progressively larger distance, controlled by {@link lodMultiplier}. Clamped to a * minimum of 0.1. Defaults to 5. * * @type {number} */ set lodBaseDistance(value) { this._lodBaseDistance = Math.max(0.1, value); if (this._placement) { this._placement.lodBaseDistance = this._lodBaseDistance; } } /** * Gets the base distance for the first LOD transition. * * @type {number} */ get lodBaseDistance() { return this._lodBaseDistance; } /** * Sets the multiplier between successive LOD distance thresholds. Each LOD level * transitions at this factor times the previous level's distance, creating a geometric * progression. Lower values keep higher quality at distance; higher values switch to * coarser LODs sooner. Clamped to a minimum of 1.2 to avoid degenerate logarithmic LOD * computation. LOD distances are automatically compensated for the camera's field of * view — a wider FOV makes objects appear smaller on screen, so LOD switches to coarser * levels sooner to match the reduced screen-space detail. Defaults to 3. * * @type {number} */ set lodMultiplier(value) { this._lodMultiplier = Math.max(1.2, value); if (this._placement) { this._placement.lodMultiplier = this._lodMultiplier; } } /** * Gets the geometric multiplier between successive LOD distance thresholds. * * @type {number} */ get lodMultiplier() { return this._lodMultiplier; } /** * @type {number[]|null} * @deprecated Use {@link lodBaseDistance} and {@link lodMultiplier} instead. * @ignore */ set lodDistances(value) { Debug.removed("GSplatComponent#lodDistances is removed. Use lodBaseDistance and lodMultiplier instead."); if (Array.isArray(value) && value.length > 0) { this.lodBaseDistance = value[0]; this.lodMultiplier = 3; } } /** * @type {number[]} * @deprecated Use {@link lodBaseDistance} and {@link lodMultiplier} instead. * @ignore */ get lodDistances() { Debug.removed("GSplatComponent#lodDistances is removed. Use lodBaseDistance and lodMultiplier instead."); return []; } /** * @type {number} * @deprecated Use app.scene.gsplat.splatBudget instead for global budget control. * @ignore */ set splatBudget(value) { Debug.removed("GSplatComponent.splatBudget is removed. Use app.scene.gsplat.splatBudget instead for global budget control."); } /** * @type {number} * @deprecated Use app.scene.gsplat.splatBudget instead for global budget control. * @ignore */ get splatBudget() { Debug.removed("GSplatComponent.splatBudget is removed. Use app.scene.gsplat.splatBudget instead for global budget control."); return 0; } /** * Sets whether to use the unified gsplat rendering. * * @type {boolean} * @deprecated Non-unified gsplat rendering is being removed; unified rendering will be the only supported mode. * @ignore */ set unified(value) { if (value === false) { Debug.deprecated("GSplatComponent#unified is deprecated. Non-unified gsplat rendering will be removed in a future release; please migrate to unified rendering (the new default)."); } if (this._unified !== value) { this._unified = value; this._onGSplatAssetAdded(); } } /** * Gets whether to use the unified gsplat rendering. * * @type {boolean} * @deprecated Non-unified gsplat rendering is being removed; unified rendering will be the only supported mode. * @ignore */ get unified() { return this._unified; } /** * Gets the unique identifier for this component. This ID is used by the picking system * and is also written to the work buffer when `app.scene.gsplat.enableIds` is enabled, making * it available to custom shaders for effects like highlighting or animation. * * @type {number} */ get id() { return this._id; } /** * Sets the work buffer update mode. * * Splat data is rendered to a work buffer only when needed (e.g., when transforms change). * Can be: * - {@link WORKBUFFER_UPDATE_AUTO}: Update only when needed (default). * - {@link WORKBUFFER_UPDATE_ONCE}: Force update this frame, then switch to AUTO. * - {@link WORKBUFFER_UPDATE_ALWAYS}: Update every frame. * * This is typically useful when using custom shader code via {@link setWorkBufferModifier} * that depends on external factors like time or animated uniforms. * * Note: {@link WORKBUFFER_UPDATE_ALWAYS} has a performance impact as it re-renders * all splat data to the work buffer every frame. Where possible, consider using shader * customization on the gsplat material (`app.scene.gsplat.material`) which is applied * during final rendering without re-rendering the work buffer. * * @type {number} */ set workBufferUpdate(value) { this._workBufferUpdate = value; if (this._placement) { this._placement.workBufferUpdate = value; } } /** * Gets the work buffer update mode. * * @type {number} */ get workBufferUpdate() { return this._workBufferUpdate; } /** * Sets custom shader code for modifying splats when written to the work buffer. * * Must provide all three functions: * - `modifySplatCenter`: Modify the splat center position * - `modifySplatRotationScale`: Modify the splat rotation and scale * - `modifySplatColor`: Modify the splat color * * Calling this method automatically triggers a work buffer re-render. * * @param {{ glsl?: string, wgsl?: string }|null} value - The modifier code for GLSL and/or WGSL. * @example * entity.gsplat.setWorkBufferModifier({ * glsl: ` * void modifySplatCenter(inout vec3 center) {} * void modifySplatRotationScale(vec3 originalCenter, vec3 modifiedCenter, inout vec4 rotation, inout vec3 scale) {} * void modifySplatColor(vec3 center, inout vec4 color) { color.rgb *= vec3(1.0, 0.0, 0.0); } * `, * wgsl: ` * fn modifySplatCenter(center: ptr<function, vec3f>) {} * fn modifySplatRotationScale(originalCenter: vec3f, modifiedCenter: vec3f, rotation: ptr<function, vec4f>, scale: ptr<function, vec3f>) {} * fn modifySplatColor(center: vec3f, color: ptr<function, vec4f>) { (*color).r = 1.0; (*color).g = 0.0; (*color).b = 0.0; } * ` * }); */ setWorkBufferModifier(value) { if (value) { const device = this.system.app.graphicsDevice; const code = (device.isWebGPU ? value.wgsl : value.glsl) ?? null; this._workBufferModifier = code ? { code, hash: hashCode(code) } : null; } else { this._workBufferModifier = null; } if (this._placement) { this._placement.workBufferModifier = this._workBufferModifier; } } /** * Sets an array of layer IDs ({@link Layer#id}) to which this gsplat 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) { this.removeFromLayers(); this._layers.length = 0; for (let i = 0; i < value.length; i++) { this._layers[i] = value[i]; } if (!this.enabled || !this.entity.enabled) { return; } this.addToLayers(); } /** * Gets the array of layer IDs ({@link Layer#id}) to which this gsplat belongs. * * @type {number[]} */ get layers() { return this._layers; } /** * Sets the gsplat asset for this 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(); } } /** * Gets the gsplat asset id for this gsplat component. * * @type {Asset|number} */ get asset() { return this._assetReference.id; } /** * Sets a GSplat resource directly (for procedural/container splats). * When set, this takes precedence over the asset property. * * @type {GSplatResourceBase|null} */ set resource(value) { if (this._resource === value) return; if (this._resource || this._assetReference.asset?.resource) { this._onGSplatAssetRemove(); } if (value && this._assetReference.id) { this._assetReference.id = null; } this._resource = value; if (this._resource && this.enabled && this.entity.enabled) { this._onGSplatAssetLoad(); } } /** * Gets the GSplat resource. Returns the directly set resource if available, * otherwise returns the resource from the assigned asset. * * @type {GSplatResourceBase|null} */ get resource() { return this._resource ?? this._assetReference.asset?.resource ?? null; } /** @private */ destroyInstance() { if (this._placement) { this.removeFromLayers(); this._placement.destroy(); this._placement = null; } if (this._instance) { this.removeFromLayers(); this._instance?.destroy(); this._instance = null; } } /** @private */ addToLayers() { if (this._placement) { 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.addGSplatPlacement(this._placement); if (this._castShadows) { layer.addGSplatShadowCaster(this._placement); } } } return; } 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() { if (this._placement) { 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.removeGSplatPlacement(this._placement); layer.removeGSplatShadowCaster(this._placement); } } return; } 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.enabled && this.entity.enabled) { if (this._instance || this._placement) { 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.unified) return; if (this._instance) { layer.addMeshInstances(this._instance.meshInstance); } } onLayerRemoved(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; if (this.unified) return; if (this._instance) { layer.removeMeshInstances(this._instance.meshInstance); } } onEnable() { const scene = this.system.app.scene; const layers = scene.layers; this._evtLayersChanged = scene.on("set:layers", this.onLayersChanged, this); if (layers) { this._evtLayerAdded = layers.on("add", this.onLayerAdded, this); this._evtLayerRemoved = layers.on("remove", this.onLayerRemoved, this); } if (this._instance || this._placement) { this.addToLayers(); } else if (this.asset) { this._onGSplatAssetAdded(); } else if (this._resource) { this._onGSplatAssetLoad(); } } onDisable() { const scene = this.system.app.scene; const layers = scene.layers; this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } 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 hide}. */ show() { if (this._instance) { this._instance.meshInstance.visible = true; } } /** * Sets a shader parameter for this gsplat instance. Parameters set here are applied * during rendering. * * @param {string} name - The name of the parameter (uniform name in shader). * @param {number|number[]|ArrayBufferView|Texture|StorageBuffer} data - The value for the parameter. */ setParameter(name, data) { const scopeId = this.system.app.graphicsDevice.scope.resolve(name); this._parameters.set(name, { scopeId, data }); if (this._placement) this._placement.renderDirty = true; } /** * Gets a shader parameter value previously set with {@link setParameter}. * * @param {string} name - The name of the parameter. * @returns {number|number[]|ArrayBufferView|undefined} The parameter value, or undefined if not set. */ getParameter(name) { return this._parameters.get(name)?.data; } /** * Deletes a shader parameter previously set with {@link setParameter}. * * @param {string} name - The name of the parameter to delete. */ deleteParameter(name) { this._parameters.delete(name); if (this._placement) this._placement.renderDirty = true; } /** * Gets an instance texture by name. Instance textures are per-component textures defined * in the resource's format with `storage: GSPLAT_STREAM_INSTANCE`. * * @param {string} name - The name of the texture. * @returns {Texture|null} The texture, or null if not found. * @example * // Add an instance stream to the resource format * resource.format.addExtraStreams([ * { name: 'instanceTint', format: pc.PIXELFORMAT_RGBA8, storage: pc.GSPLAT_STREAM_INSTANCE } * ]); * * // Get the instance texture and fill it with data * const texture = entity.gsplat.getInstanceTexture('instanceTint'); * if (texture) { * const data = texture.lock(); * // Fill texture data... * texture.unlock(); * } */ getInstanceTexture(name) { if (!this._placement) { return null; } return this._placement.getInstanceTexture(name, this.system.app.graphicsDevice) ?? null; } _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() { this.destroyInstance(); const resource = this._resource ?? this._assetReference.asset?.resource; if (!resource) return; if (this.unified) { this._placement = null; this._placement = new GSplatPlacement(resource, this.entity, 0, this._parameters, null, this._id); this._placement.lodBaseDistance = this._lodBaseDistance; this._placement.lodMultiplier = this._lodMultiplier; this._placement.workBufferUpdate = this._workBufferUpdate; this._placement.workBufferModifier = this._workBufferModifier; if (this.enabled && this.entity.enabled) { this.addToLayers(); } } else { this.instance = new GSplatInstance(resource, { material: this._materialTmp, highQualitySH: this._highQualitySH, scene: this.system.app.scene }); this._materialTmp = null; } } _onGSplatAssetUnload() { this.destroyInstance(); } _onGSplatAssetRemove() { this._onGSplatAssetUnload(); } } export { GSplatComponent };