playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
378 lines (377 loc) • 12.7 kB
JavaScript
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 { Debug } from "../../core/debug.js";
import { BoundingBox } from "../../core/shape/bounding-box.js";
import { GSPLATDATA_COMPACT } from "../constants.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";
let id = 0;
const tempMap = /* @__PURE__ */ new Map();
class GSplatResourceBase {
/**
* @param {GraphicsDevice} device - The graphics device.
* @param {object} gsplatData - Data source with getCenters(), calcAabb(), numSplats, etc.
* @param {object} [options] - Construction options.
* @param {boolean} [options.prepareCenters] - When omitted or true, calls gsplatData.getCenters()
* and stores the result. When false, {@link GSplatResourceBase#centers} stays null until set or
* materialized by a subclass (e.g. lazy allocation in GSplatContainer).
*/
constructor(device, gsplatData, options = {}) {
/**
* @type {GraphicsDevice}
* @ignore
*/
__publicField(this, "device");
/**
* @type {GSplatData | GSplatCompressedData | GSplatSogData}
* @ignore
*/
__publicField(this, "gsplatData");
/**
* @type {Float32Array|null}
* @protected
*/
__publicField(this, "_centers", null);
/**
* Version counter for centers array changes. Remains 0 for static resources.
* Only GSplatContainer increments this via its update() method.
*
* @ignore
*/
__publicField(this, "centersVersion", 0);
/** @type {BoundingBox} */
__publicField(this, "aabb");
/**
* @type {Mesh|null}
* @ignore
*/
__publicField(this, "mesh", null);
/**
* @type {number}
* @ignore
*/
__publicField(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
*/
__publicField(this, "workBufferRenderInfos", /* @__PURE__ */ new Map());
/**
* Format descriptor for this resource. Assigned by derived classes.
*
* @type {GSplatFormat}
* @protected
*/
__publicField(this, "_format", null);
/**
* Manages textures for this resource based on format streams.
*
* @type {GSplatStreams}
* @ignore
*/
__publicField(this, "streams");
/**
* 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
*/
__publicField(this, "parameters", /* @__PURE__ */ new Map());
/** @private */
__publicField(this, "_refCount", 0);
/** @private */
__publicField(this, "_meshRefCount", 0);
this.device = device;
this.gsplatData = gsplatData;
this.streams = new GSplatStreams(device);
if (options.prepareCenters !== false) {
this._centers = gsplatData.getCenters();
} else {
this._centers = null;
}
this.aabb = new BoundingBox();
gsplatData.calcAabb(this.aabb);
}
/**
* CPU-side splat center positions (xyz per splat), or null when not built for this resource.
*
* @type {Float32Array|null}
*/
set centers(value) {
this._centers = value;
}
get centers() {
return this._centers;
}
/**
* True when a centers buffer has been allocated ({@link GSplatResourceBase#centers} is non-null).
* Reads internal storage only so checks do not trigger lazy allocation in {@link GSplatContainer}.
*
* @type {boolean}
*/
get hasCenters() {
return this._centers != null;
}
/**
* 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) {
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;
}
}
/**
* True when this resource's color-only work buffer updates (spherical harmonics refresh) can
* source geometry from the work buffer itself instead of re-reading the source textures. The
* resource format's read chunk must compile out getCenter/getRotation/getScale when
* GSPLAT_WORKBUFFER_GEOMETRY is defined (see gsplatWorkBufferGeometryPS chunk).
*
* @type {boolean}
* @ignore
*/
get supportsWorkBufferGeometry() {
return false;
}
/**
* 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) {
this.configureMaterialDefines(tempMap);
tempMap.set("GSPLAT_LOD", "");
if (colorOnly) {
tempMap.set("GSPLAT_COLOR_ONLY", "");
if (this.supportsWorkBufferGeometry) {
tempMap.set("GSPLAT_WORKBUFFER_GEOMETRY", "");
if (workBufferFormat.dataFormat === GSPLATDATA_COMPACT) {
tempMap.set("GSPLAT_WORKBUFFER_COMPACT", "");
}
}
}
let definesKey = "";
for (const [k, v] of tempMap) {
if (definesKey) definesKey += ";";
definesKey += `${k}=${v}`;
}
const key = `${formatHash};${workBufferFormat.hash};${workBufferModifier?.hash ?? 0};${definesKey}`;
let info = this.workBufferRenderInfos.get(key);
if (!info) {
const material = new ShaderMaterial();
this.configureMaterial(material, workBufferModifier, formatDeclarations);
const chunks = this.device.isWebGPU ? material.shaderChunks.wgsl : material.shaderChunks.glsl;
const outputStreams = colorOnly ? [workBufferFormat.getStream("dataColor")] : [...workBufferFormat.streams, ...workBufferFormat.extraStreams];
let outputCode = workBufferFormat.getOutputDeclarations(outputStreams);
if (colorOnly && workBufferFormat.extraStreams.length > 0) {
outputCode += `
${workBufferFormat.getOutputStubs(workBufferFormat.extraStreams)}`;
}
chunks.set("gsplatWorkBufferOutputVS", outputCode);
const writeCode = workBufferFormat.getWriteCode();
if (writeCode) {
chunks.set("gsplatWriteVS", writeCode);
}
tempMap.forEach((v, k) => material.setDefine(k, v));
info = new WorkBufferRenderInfo(this.device, key, material, colorOnly, workBufferFormat);
this.workBufferRenderInfos.set(key, info);
}
tempMap.clear();
return info;
}
static createMesh(device) {
const splatInstanceSize = GSplatResourceBase.instanceSize;
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;
}
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);
this.streams.syncWithFormat(this.format);
const chunks = this.device.isWebGPU ? material.shaderChunks.wgsl : material.shaderChunks.glsl;
chunks.set("gsplatDeclarationsVS", formatDeclarations);
chunks.set("gsplatReadVS", this.format.getReadCode());
if (workBufferModifier?.code) {
chunks.set("gsplatModifyVS", workBufferModifier.code);
}
for (const [name, texture] of this.streams.textures) {
material.setParameter(name, texture);
}
for (const [name, value] of this.parameters) {
material.setParameter(name, value);
}
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");
}
}
export {
GSplatResourceBase
};