UNPKG

playcanvas

Version:

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

317 lines (314 loc) 12.3 kB
import { Debug } from '../../core/debug.js'; import { BoundingBox } from '../../core/shape/bounding-box.js'; import { Mesh } from '../mesh.js'; import { ShaderMaterial } from '../materials/shader-material.js'; import { WorkBufferRenderInfo } from '../gsplat-unified/gsplat-work-buffer.js'; import { GSplatStreams } from './gsplat-streams.js'; import { GSplatResourceCleanup } from './gsplat-resource-cleanup.js'; /** * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' * @import { GSplatData } from './gsplat-data.js' * @import { GSplatCompressedData } from './gsplat-compressed-data.js' * @import { GSplatSogData } from './gsplat-sog-data.js' * @import { GSplatFormat } from './gsplat-format.js' * @import { Texture } from '../../platform/graphics/texture.js' * @import { Vec2 } from '../../core/math/vec2.js' */ let id = 0; const tempMap = new Map(); /** * Base class for a GSplat resource and defines common properties. * * @ignore */ class GSplatResourceBase { /** * Destroys this resource. If the resource is still in use by the sorter, destruction is * automatically deferred until it's safe. */ destroy() { if (this.refCount > 0) { // Still in use by sorter, queue for deferred destruction GSplatResourceCleanup.queueDestroy(this.device, this); return; } this._actualDestroy(); } /** * Actually destroys this resource and releases all GPU resources. * Derived classes should override this method instead of destroy(). * * @protected */ _actualDestroy() { this.streams.destroy(); this.mesh?.destroy(); this.workBufferRenderInfos.forEach((info)=>info.destroy()); this.workBufferRenderInfos.clear(); } /** * Increments the reference count. * * @ignore */ incRefCount() { this._refCount++; } /** * Decrements the reference count. * * @ignore */ decRefCount() { this._refCount--; } /** * Gets the current reference count. This represents how many times this resource is currently * being used internally by the engine. For {@link GSplatComponent#asset|assets} assigned to * {@link GSplatComponent#unified|unified} gsplat components, this tracks active usage during * rendering and sorting operations. * * Resources should not be unloaded while the reference count is non-zero, as they are still * in use by the rendering pipeline. * * @type {number} * @ignore */ get refCount() { return this._refCount; } /** * Ensures mesh and instanceIndices exist. Creates them lazily on first call. Must be paired * with a call to releaseMesh() when done. * * @ignore */ ensureMesh() { if (!this.mesh) { this.mesh = GSplatResourceBase.createMesh(this.device); this.mesh.aabb.copy(this.aabb); } this._meshRefCount++; } /** * Releases reference to mesh. When all references are released, cleans up instanceIndices. * The mesh itself is destroyed by MeshInstance when its internal refCount reaches zero. * * @ignore */ releaseMesh() { this._meshRefCount--; if (this._meshRefCount < 1) { this.mesh = null; // mesh instances destroy mesh when their refCount reaches zero } } /** * Get or create a QuadRender for rendering to work buffer. * * @param {boolean} colorOnly - Whether to render only color (not full MRT). * @param {{ code: string, hash: number }|null} workBufferModifier - Optional custom modifier (object with code and pre-computed hash). * @param {number} formatHash - Captured format hash for shader caching. * @param {string} formatDeclarations - Captured format declarations for shader compilation. * @param {GSplatFormat} workBufferFormat - The work buffer format descriptor. * @returns {WorkBufferRenderInfo} The WorkBufferRenderInfo instance. * @ignore */ getWorkBufferRenderInfo(colorOnly, workBufferModifier, formatHash, formatDeclarations, workBufferFormat) { // configure defines to fetch cached data this.configureMaterialDefines(tempMap); tempMap.set('GSPLAT_LOD', ''); if (colorOnly) tempMap.set('GSPLAT_COLOR_ONLY', ''); let definesKey = ''; for (const [k, v] of tempMap){ if (definesKey) definesKey += ';'; definesKey += `${k}=${v}`; } const key = `${formatHash};${workBufferFormat.hash};${workBufferModifier?.hash ?? 0};${definesKey}`; // get or create quad render let info = this.workBufferRenderInfos.get(key); if (!info) { const material = new ShaderMaterial(); this.configureMaterial(material, workBufferModifier, formatDeclarations); // Inject work buffer output declarations const chunks = this.device.isWebGPU ? material.shaderChunks.wgsl : material.shaderChunks.glsl; // For color-only mode, only output color stream; otherwise output all streams const outputStreams = colorOnly ? [ workBufferFormat.getStream('dataColor') ] : [ ...workBufferFormat.streams, ...workBufferFormat.extraStreams ]; let outputCode = workBufferFormat.getOutputDeclarations(outputStreams); // In color-only mode, generate no-op stubs for extra streams so user modifiers compile if (colorOnly && workBufferFormat.extraStreams.length > 0) { outputCode += `\n${workBufferFormat.getOutputStubs(workBufferFormat.extraStreams)}`; } chunks.set('gsplatWorkBufferOutputVS', outputCode); // Inject format-specific write encoding chunk const writeCode = workBufferFormat.getWriteCode(); if (writeCode) { chunks.set('gsplatWriteVS', writeCode); } // copy tempMap to material defines tempMap.forEach((v, k)=>material.setDefine(k, v)); // create new cache entry info = new WorkBufferRenderInfo(this.device, key, material, colorOnly, workBufferFormat); this.workBufferRenderInfos.set(key, info); } tempMap.clear(); return info; } static createMesh(device) { // number of quads to combine into a single instance. this is to increase occupancy // in the vertex shader. const splatInstanceSize = GSplatResourceBase.instanceSize; // build the instance mesh const meshPositions = new Float32Array(12 * splatInstanceSize); const meshIndices = new Uint32Array(6 * splatInstanceSize); for(let i = 0; i < splatInstanceSize; ++i){ meshPositions.set([ -1, -1, i, 1, -1, i, 1, 1, i, -1, 1, i ], i * 12); const b = i * 4; meshIndices.set([ 0 + b, 1 + b, 2 + b, 0 + b, 2 + b, 3 + b ], i * 6); } const mesh = new Mesh(device); mesh.setPositions(meshPositions, 3); mesh.setIndices(meshIndices); mesh.update(); return mesh; } static get instanceSize() { return 128; // number of splats per instance } get numSplats() { return this.gsplatData.numSplats; } /** * Gets the format descriptor for this resource. The format defines texture streams and * shader code for reading splat data. Use this to add extra streams. * * @type {GSplatFormat} */ get format() { return this._format; } /** * Gets a texture by name. * * @param {string} name - The name of the texture. * @returns {Texture|null} The texture, or null if not found. */ getTexture(name) { return this.streams.getTexture(name) ?? null; } /** * Gets the texture dimensions (width and height) used by this resource's data textures. * * @type {Vec2} */ get textureDimensions() { return this.streams.textureDimensions; } /** * Configures a material to use this resource's data. Base implementation injects format's * shader chunks and binds textures from the streams. * * @param {ShaderMaterial} material - The material to configure. * @param {{ code: string, hash: number }|null} workBufferModifier - Optional custom modifier (object with code and pre-computed hash). * @param {string} formatDeclarations - Captured format declarations for shader compilation. * @ignore */ configureMaterial(material, workBufferModifier, formatDeclarations) { this.configureMaterialDefines(material.defines); // Sync resource textures with format (handles extra streams) this.streams.syncWithFormat(this.format); // Inject format's shader chunks const chunks = this.device.isWebGPU ? material.shaderChunks.wgsl : material.shaderChunks.glsl; chunks.set('gsplatDeclarationsVS', formatDeclarations); chunks.set('gsplatReadVS', this.format.getReadCode()); // Set modify chunk if provided if (workBufferModifier?.code) { chunks.set('gsplatModifyVS', workBufferModifier.code); } // Bind all textures from streams for (const [name, texture] of this.streams.textures){ material.setParameter(name, texture); } // Bind non-texture parameters (e.g., dequantization uniforms) for (const [name, value] of this.parameters){ material.setParameter(name, value); } // Set texture size if (this.textureDimensions.x > 0) { material.setParameter('splatTextureSize', this.textureDimensions.x); } } /** * Configures material defines for this resource. Derived classes should override this. * * @param {Map<string, string|number|boolean>} defines - The defines map to configure. * @ignore */ configureMaterialDefines(defines) {} instantiate() { Debug.removed('GSplatResource.instantiate is removed. Use gsplat component instead'); } constructor(device, gsplatData){ /** * Version counter for centers array changes. Remains 0 for static resources. * Only GSplatContainer increments this via its update() method. * * @type {number} * @ignore */ this.centersVersion = 0; /** * @type {Mesh|null} * @ignore */ this.mesh = null; /** * @type {number} * @ignore */ this.id = id++; /** * Cache for work buffer render materials/shaders. Keyed by configuration hash. * Stored per-resource because materials depend on resource-specific configuration * (SH bands, textures, defines). Cleaned up when resource is destroyed. * * @type {Map<string, WorkBufferRenderInfo>} * @ignore */ this.workBufferRenderInfos = new Map(); /** * Format descriptor for this resource. Assigned by derived classes. * * @type {GSplatFormat} * @protected */ this._format = null; /** * Non-texture uniform parameters required by this resource's format. * This is the single source of truth for format-specific uniforms (e.g., dequantization * parameters) used by both material configuration and processing. * * @type {Map<string, any>} * @ignore */ this.parameters = new Map(); /** * @type {number} * @private */ this._refCount = 0; /** * @type {number} * @private */ this._meshRefCount = 0; this.device = device; this.gsplatData = gsplatData; this.streams = new GSplatStreams(device); this.centers = gsplatData.getCenters(); this.aabb = new BoundingBox(); gsplatData.calcAabb(this.aabb); } } export { GSplatResourceBase };