playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
487 lines (486 loc) • 18.6 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 {
getGlslShaderType,
getWgslShaderType,
pixelFormatInfo,
PIXELFORMAT_RGBA16F,
PIXELFORMAT_RGBA32F,
SAMPLETYPE_FLOAT,
SAMPLETYPE_INT,
SAMPLETYPE_UINT,
SHADERSTAGE_COMPUTE
} from "../../platform/graphics/constants.js";
import { hashCode } from "../../core/hash.js";
import { Debug } from "../../core/debug.js";
import { GSPLAT_STREAM_RESOURCE, GSPLAT_STREAM_INSTANCE } from "../constants.js";
import { BindTextureFormat } from "../../platform/graphics/bind-group-format.js";
import glslStreamDecl from "../shader-lib/glsl/chunks/gsplat/vert/gsplatStreamDecl.js";
import wgslStreamDecl from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatStreamDecl.js";
import wgslComputeStreamDecl from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatComputeStreamDecl.js";
import glslStreamOutput from "../shader-lib/glsl/chunks/gsplat/vert/gsplatStreamOutput.js";
import wgslStreamOutput from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatStreamOutput.js";
import glslContainerFloatRead from "../shader-lib/glsl/chunks/gsplat/vert/formats/containerFloatRead.js";
import wgslContainerFloatRead from "../shader-lib/wgsl/chunks/gsplat/vert/formats/containerFloatRead.js";
import glslContainerSimpleRead from "../shader-lib/glsl/chunks/gsplat/vert/formats/containerSimpleRead.js";
import wgslContainerSimpleRead from "../shader-lib/wgsl/chunks/gsplat/vert/formats/containerSimpleRead.js";
const serializeStreams = (streams) => streams.map((s) => `${s.name}:${s.format}:${s.storage}`).join(",");
const RE_NAME = /\{name\}/g;
const RE_SAMPLER = /\{sampler\}/g;
const RE_TEXTURE_TYPE = /\{textureType\}/g;
const RE_RETURN_TYPE = /\{returnType\}/g;
const RE_FUNC_NAME = /\{funcName\}/g;
const RE_BINDING = /\{binding\}/g;
const RE_INDEX = /\{index\}/g;
const RE_COLOR_SLOT = /\{colorSlot\}/g;
const RE_DEFINE_GUARD = /\{defineGuard\}/g;
class GSplatFormat {
/**
* Creates a new GSplatFormat instance.
*
* @param {GraphicsDevice} device - The graphics device.
* @param {GSplatStreamDescriptor[]} streams - Array of stream descriptors.
* @param {object} options - Format options.
* @param {string} [options.readGLSL] - GLSL code defining getCenter(), getColor(),
* getRotation(), getScale() functions. Can include additional declarations at module scope.
* Required for WebGL.
* @param {string} [options.readWGSL] - WGSL code defining getCenter(), getColor(),
* getRotation(), getScale() functions. Can include additional declarations at module scope.
* Required for WebGPU.
*/
constructor(device, streams, options) {
/**
* @type {GraphicsDevice}
* @private
*/
__publicField(this, "_device");
/**
* Array of stream descriptors.
*
* @type {GSplatStreamDescriptor[]}
* @readonly
*/
__publicField(this, "streams");
/**
* User-provided code for reading splat data (GLSL or WGSL based on device).
* Must define getCenter(), getColor(), getRotation(), getScale() functions.
*
* @type {string}
* @private
*/
__publicField(this, "_read");
/**
* When true, allows extra streams to be removed via {@link removeExtraStreams}.
* Only work buffer formats (returned by {@link GSplatParams#format}) should set this.
*
* @ignore
*/
__publicField(this, "allowStreamRemoval", false);
/**
* Work buffer data layout identifier (one of GSPLATDATA_*), or null for resource formats.
* Set by {@link GSplatParams} when creating a work buffer format. Identifies how transform
* data is encoded, so consumers (e.g. the work-buffer-sourced spherical harmonics update)
* can select the matching decode without inspecting individual stream pixel formats.
*
* @type {string|null}
* @ignore
*/
__publicField(this, "dataFormat", null);
/**
* Extra streams added via addExtraStreams(). For resource formats, streams can only be
* added, never removed. For work buffer formats (where {@link allowStreamRemoval} is true),
* streams can also be removed via {@link removeExtraStreams}.
*
* @type {GSplatStreamDescriptor[]}
* @private
*/
__publicField(this, "_extraStreams", []);
/**
* Set of all stream names (base + extra) for fast duplicate checking.
*
* @type {Set<string>}
* @private
*/
__publicField(this, "_streamNames", /* @__PURE__ */ new Set());
/**
* Version counter that increments when extra streams change.
*
* @private
*/
__publicField(this, "_extraStreamsVersion", 0);
/**
* Cached hash value.
*
* @type {number|undefined}
* @private
*/
__publicField(this, "_hash");
/**
* Cached resource streams array.
*
* @type {GSplatStreamDescriptor[]|null}
* @private
*/
__publicField(this, "_resourceStreams", null);
/**
* Cached instance streams array.
*
* @type {GSplatStreamDescriptor[]|null}
* @private
*/
__publicField(this, "_instanceStreams", null);
this._device = device;
this.streams = [...streams];
this._streamNames = new Set(this.streams.map((s) => s.name));
const isWebGPU = device.isWebGPU;
this._read = isWebGPU ? options.readWGSL : options.readGLSL;
Debug.assert(this._read, `GSplatFormat: ${isWebGPU ? "readWGSL" : "readGLSL"} is required`);
}
/**
* Returns a hash of this format's configuration. Used for shader caching.
* Computed from raw inputs to avoid generating shader code just for the hash.
*
* @type {number}
* @ignore
*/
get hash() {
if (this._hash === void 0) {
const streamsStr = serializeStreams(this.streams);
const extraStr = serializeStreams(this._extraStreams);
this._hash = hashCode(
streamsStr + extraStr + this._read
);
}
return this._hash;
}
/**
* Returns the version counter. Increments when extra streams change.
*
* @type {number}
* @ignore
*/
get extraStreamsVersion() {
return this._extraStreamsVersion;
}
/**
* Gets the extra streams array. Streams can only be added via {@link addExtraStreams},
* not removed. Do not modify the returned array directly.
*
* @type {GSplatStreamDescriptor[]}
*/
get extraStreams() {
return this._extraStreams;
}
/**
* Returns all resource-level streams (base streams + extra streams where instance !== true).
* Used by GSplatStreams for resource texture management.
*
* @type {GSplatStreamDescriptor[]}
* @ignore
*/
get resourceStreams() {
if (this._resourceStreams === null) {
this._resourceStreams = [
...this.streams.filter((s) => s.storage !== GSPLAT_STREAM_INSTANCE),
...this._extraStreams.filter((s) => s.storage !== GSPLAT_STREAM_INSTANCE)
];
}
return this._resourceStreams;
}
/**
* Returns all instance-level streams (extra streams with GSPLAT_STREAM_INSTANCE storage).
* Used by GSplatStreams for per-component-instance texture management.
*
* @type {GSplatStreamDescriptor[]}
* @ignore
*/
get instanceStreams() {
if (this._instanceStreams === null) {
this._instanceStreams = this._extraStreams.filter((s) => s.storage === GSPLAT_STREAM_INSTANCE);
}
return this._instanceStreams;
}
/**
* Adds additional texture streams for custom gsplat data. Each stream defines a texture
* that can store extra information, accessible in shaders via generated load functions.
* Streams with `storage: GSPLAT_STREAM_INSTANCE` are created per gsplat component instance,
* while others are shared across all instances of the same resource.
*
* Note: Streams cannot be removed once added currently.
*
* @param {GSplatStreamDescriptor[]} streams - Array of stream descriptors to add.
*/
addExtraStreams(streams) {
if (!streams || streams.length === 0) return;
let added = false;
for (const s of streams) {
if (this._streamNames.has(s.name)) {
Debug.error(`GSplatFormat: Stream '${s.name}' already exists, ignoring.`);
continue;
}
this._extraStreams.push({
name: s.name,
format: s.format,
storage: s.storage ?? GSPLAT_STREAM_RESOURCE
});
this._streamNames.add(s.name);
added = true;
}
if (added) {
this._extraStreamsVersion++;
this._invalidateCaches();
}
}
/**
* Removes extra streams by name. Only supported on work buffer formats
* (returned by {@link GSplatParams#format}). Removing streams from resource
* formats is not supported.
*
* @param {string[]} names - Array of stream names to remove.
* @ignore
*/
removeExtraStreams(names) {
if (!this.allowStreamRemoval) {
Debug.assert(false, "GSplatFormat.removeExtraStreams: only supported on work buffer formats");
return;
}
let removed = false;
for (const name of names) {
const idx = this._extraStreams.findIndex((s) => s.name === name);
if (idx !== -1) {
this._extraStreams.splice(idx, 1);
this._streamNames.delete(name);
removed = true;
}
}
if (removed) {
this._extraStreamsVersion++;
this._invalidateCaches();
}
}
/**
* Generates input declarations (texture uniforms + load functions).
*
* @param {string[]} [streamNames] - Optional array of stream names to filter. If not provided,
* generates declarations for all streams.
* @returns {string} Shader code for declarations.
* @ignore
*/
getInputDeclarations(streamNames) {
const isWebGPU = this._device.isWebGPU;
const template = isWebGPU ? wgslStreamDecl : glslStreamDecl;
const getShaderType = isWebGPU ? getWgslShaderType : getGlslShaderType;
const lines = [];
let streams = [...this.streams, ...this._extraStreams];
if (streamNames) {
streams = streams.filter((s) => streamNames.includes(s.name));
}
for (const stream of streams) {
const info = getShaderType(stream.format);
const funcName = stream.name.charAt(0).toUpperCase() + stream.name.slice(1);
let textureType = info.textureType ?? "";
if (isWebGPU && stream.format === PIXELFORMAT_RGBA32F) {
textureType = "texture_2d<uff>";
}
const decl = template.replace(RE_NAME, stream.name).replace(RE_SAMPLER, info.sampler ?? "").replace(RE_TEXTURE_TYPE, textureType).replace(RE_RETURN_TYPE, info.returnType).replace(RE_FUNC_NAME, funcName);
lines.push(decl);
}
return lines.join("\n");
}
/**
* Returns the read code.
*
* @returns {string} Shader code for reading splat data.
* @ignore
*/
getReadCode() {
return this._read;
}
/**
* Generates compute shader input declarations with explicit binding annotations.
* Format texture bindings are placed at indices starting from startBinding.
*
* @param {number} startBinding - The first @group(0) @binding() index for format textures.
* @param {string[]} [streamNames] - Optional array of stream names to filter.
* @returns {string} WGSL code for compute shader declarations.
* @ignore
*/
getComputeInputDeclarations(startBinding, streamNames) {
const lines = [];
let streams = [...this.streams, ...this._extraStreams];
if (streamNames) {
streams = streams.filter((s) => streamNames.includes(s.name));
}
for (let i = 0; i < streams.length; i++) {
const stream = streams[i];
const info = getWgslShaderType(stream.format);
const funcName = stream.name.charAt(0).toUpperCase() + stream.name.slice(1);
let textureType = info.textureType ?? "";
if (stream.format === PIXELFORMAT_RGBA32F) {
textureType = "texture_2d<uff>";
}
const decl = wgslComputeStreamDecl.replace(RE_BINDING, String(startBinding + i)).replace(RE_NAME, stream.name).replace(RE_TEXTURE_TYPE, textureType).replace(RE_RETURN_TYPE, info.returnType).replace(RE_FUNC_NAME, funcName);
lines.push(decl);
}
return lines.join("\n");
}
/**
* Returns an array of BindTextureFormat entries for the format's streams, suitable for
* appending to a compute shader's BindGroupFormat. Sample types are derived from pixel formats.
*
* @param {string[]} [streamNames] - Optional array of stream names to filter.
* @returns {BindTextureFormat[]} Array of bind texture format entries.
* @ignore
*/
getComputeBindFormats(streamNames) {
let streams = [...this.streams, ...this._extraStreams];
if (streamNames) {
streams = streams.filter((s) => streamNames.includes(s.name));
}
return streams.map((stream) => {
const info = pixelFormatInfo.get(stream.format);
let sampleType = SAMPLETYPE_FLOAT;
if (info?.isUint) sampleType = SAMPLETYPE_UINT;
else if (info?.isInt) sampleType = SAMPLETYPE_INT;
return new BindTextureFormat(stream.name, SHADERSTAGE_COMPUTE, void 0, sampleType, false);
});
}
/**
* Sets the write code for encoding splat data into the work buffer. The appropriate code
* for the current backend (GLSL or WGSL) is stored.
*
* @param {string} writeGLSL - GLSL code for writing/encoding splat data.
* @param {string} writeWGSL - WGSL code for writing/encoding splat data.
* @ignore
*/
setWriteCode(writeGLSL, writeWGSL) {
this._write = this._device.isWebGPU ? writeWGSL : writeGLSL;
}
/**
* Returns the write code for encoding splat data into the work buffer.
*
* @returns {string|undefined} Shader code for writing splat data, or undefined if not set.
* @ignore
*/
getWriteCode() {
return this._write;
}
/**
* Generates output declarations (write functions) for MRT output streams.
* Used by GSplatProcessor to generate output functions for dstStreams.
* Each stream maps to an MRT slot (pcFragColor0, pcFragColor1, etc. in GLSL or
* processOutput.color, processOutput.color1, etc. in WGSL).
*
* @param {GSplatStreamDescriptor[]} outputStreams - Stream descriptors for output.
* @returns {string} Shader code for output write functions.
* @ignore
*/
getOutputDeclarations(outputStreams) {
const isWebGPU = this._device.isWebGPU;
const lines = [];
const template = isWebGPU ? wgslStreamOutput : glslStreamOutput;
const getShaderType = isWebGPU ? getWgslShaderType : getGlslShaderType;
for (let i = 0; i < outputStreams.length; i++) {
const stream = outputStreams[i];
const info = getShaderType(stream.format);
const funcName = stream.name.charAt(0).toUpperCase() + stream.name.slice(1);
const colorSlot = i === 0 ? "color" : `color${i}`;
const decl = template.replace(RE_FUNC_NAME, funcName).replace(RE_RETURN_TYPE, info.returnType).replace(RE_INDEX, String(i)).replace(RE_COLOR_SLOT, colorSlot).replace(RE_DEFINE_GUARD, "1");
lines.push(decl);
}
return lines.join("\n");
}
/**
* Generates no-op stub functions for streams that aren't render targets.
* Used in color-only mode so user modifier code compiles but writes are ignored.
*
* @param {GSplatStreamDescriptor[]} streams - Stream descriptors to generate stubs for.
* @returns {string} Shader code for no-op write functions.
* @ignore
*/
getOutputStubs(streams) {
const isWebGPU = this._device.isWebGPU;
const lines = [];
const template = isWebGPU ? wgslStreamOutput : glslStreamOutput;
const getShaderType = isWebGPU ? getWgslShaderType : getGlslShaderType;
for (const stream of streams) {
const info = getShaderType(stream.format);
const funcName = stream.name.charAt(0).toUpperCase() + stream.name.slice(1);
const stub = template.replace(RE_FUNC_NAME, funcName).replace(RE_RETURN_TYPE, info.returnType).replace(RE_DEFINE_GUARD, "0");
lines.push(stub);
}
return lines.join("\n");
}
/**
* Returns a stream descriptor by name.
*
* @param {string} name - The name of the stream to find.
* @returns {GSplatStreamDescriptor|undefined} The stream descriptor, or undefined if not found.
* @ignore
*/
getStream(name) {
let stream = this.streams.find((s) => s.name === name);
if (!stream) {
stream = this._extraStreams.find((s) => s.name === name);
}
return stream;
}
/**
* Invalidates all cached values when streams change.
*
* @private
*/
_invalidateCaches() {
this._hash = void 0;
this._resourceStreams = null;
this._instanceStreams = null;
}
/**
* Creates a default format using 32F/16F textures, simple to use for CPU data population.
* This format can be rendered to by {@link GSplatProcessor} when supported. Check
* {@link GraphicsDevice#textureFloatRenderable} (for RGBA32F) and
* {@link GraphicsDevice#textureHalfFloatRenderable} (for RGBA16F).
*
* The format stores:
* - `dataColor` (RGBA16F): color.rgba as half floats
* - `dataCenter` (RGBA32F): center.xyz as floats (w unused)
* - `dataScale` (RGBA16F): scale.xyz as half floats (w unused)
* - `dataRotation` (RGBA16F): rotation.xyzw as half floats (w stored directly, not derived)
*
* @param {GraphicsDevice} device - The graphics device.
* @returns {GSplatFormat} The default format.
*/
static createDefaultFormat(device) {
return new GSplatFormat(device, [
{ name: "dataColor", format: PIXELFORMAT_RGBA16F },
{ name: "dataCenter", format: PIXELFORMAT_RGBA32F },
{ name: "dataScale", format: PIXELFORMAT_RGBA16F },
{ name: "dataRotation", format: PIXELFORMAT_RGBA16F }
], {
readGLSL: glslContainerFloatRead,
readWGSL: wgslContainerFloatRead
});
}
/**
* Creates a simple format with uniform-scale splats and no rotation.
* Streams:
* - `dataCenter` (RGBA32F): center.xyz + uniform size in w
* - `dataColor` (RGBA16F): color.rgba as half floats
*
* @param {GraphicsDevice} device - The graphics device.
* @returns {GSplatFormat} The simple format.
*/
static createSimpleFormat(device) {
return new GSplatFormat(device, [
{ name: "dataCenter", format: PIXELFORMAT_RGBA32F },
{ name: "dataColor", format: PIXELFORMAT_RGBA16F }
], {
readGLSL: glslContainerSimpleRead,
readWGSL: wgslContainerSimpleRead
});
}
}
export {
GSplatFormat
};