UNPKG

playcanvas

Version:

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

487 lines (486 loc) 18.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 { 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 };