UNPKG

playcanvas

Version:

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

488 lines (485 loc) 21.2 kB
import { PIXELFORMAT_RGBA32F, getWgslShaderType, pixelFormatInfo, SHADERSTAGE_COMPUTE, SAMPLETYPE_FLOAT, PIXELFORMAT_RGBA16F, getGlslShaderType, SAMPLETYPE_UINT, SAMPLETYPE_INT } from '../../platform/graphics/constants.js'; import { hashCode } from '../../core/hash.js'; import { Debug } from '../../core/debug.js'; import { GSPLAT_STREAM_INSTANCE, GSPLAT_STREAM_RESOURCE } 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 gsplatContainerFloatReadVS$1 from '../shader-lib/glsl/chunks/gsplat/vert/formats/containerFloatRead.js'; import gsplatContainerFloatReadVS 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'; /** * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' */ /** * @typedef {object} GSplatStreamDescriptor * @property {string} name - The name of the stream (used as texture uniform name). * @property {number} format - The pixel format of the texture (e.g. PIXELFORMAT_RGBA32F). * When used as an extra stream for work buffers or as a destination stream for * GSplatProcessor, the format must be renderable as these textures are used as render * targets. Ensure the format is renderable on all target devices. See {@link Texture} for * details on renderable formats and device capabilities. * @property {number} [storage] - Storage type: GSPLAT_STREAM_RESOURCE (default, shared across * instances) or GSPLAT_STREAM_INSTANCE (per-component instance). Note: Work buffer formats * (accessed via `app.scene.gsplat.format`) do not support GSPLAT_STREAM_INSTANCE. */ /** * Serializes an array of stream descriptors to a string for hashing. * * @param {GSplatStreamDescriptor[]} streams - Array of stream descriptors. * @returns {string} Serialized string. */ const serializeStreams = (streams)=>streams.map((s)=>`${s.name}:${s.format}:${s.storage}`).join(','); // Pre-compiled regex patterns for template replacement 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; /** * Gsplat resources store per-splat data (positions, colors, rotations, scales, spherical * harmonics) in GPU textures. This class describes those texture streams and generates the * shader code needed to access them. * * Each stream defines a texture with a name and pixel format. The class automatically generates * shader declarations (uniforms/samplers) and load functions (e.g. `loadColor()`) for each * stream. A read shader can be provided to define how splat attributes are extracted from * these textures. * * Users can add extra streams via {@link addExtraStreams} for custom per-splat data. These * can be per-resource (shared across instances) or per-instance (unique to each gsplat * component). * * For loaded gsplat resources, base streams are automatically configured based on the loaded * data format. For {@link GSplatContainer}, users define both base and extra streams to * specify the complete data layout. * * @category Graphics */ class GSplatFormat { /** * 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 === undefined) { 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) { // Base streams + extra streams that are not instance-level 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; } // Add with storage default 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 = []; // Get streams - filter if names specified 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, undefined, 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 = []; // Generate output declarations using chunk template 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) { // Check base streams first let stream = this.streams.find((s)=>s.name === name); if (!stream) { // Check extra streams stream = this._extraStreams.find((s)=>s.name === name); } return stream; } /** * Invalidates all cached values when streams change. * * @private */ _invalidateCaches() { this._hash = undefined; 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: gsplatContainerFloatReadVS$1, readWGSL: gsplatContainerFloatReadVS }); } /** * 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 }); } /** * 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){ /** * When true, allows extra streams to be removed via {@link removeExtraStreams}. * Only work buffer formats (returned by {@link GSplatParams#format}) should set this. * * @type {boolean} * @ignore */ this.allowStreamRemoval = false; /** * 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 */ this._extraStreams = []; /** * Set of all stream names (base + extra) for fast duplicate checking. * * @type {Set<string>} * @private */ this._streamNames = new Set(); /** * Version counter that increments when extra streams change. * * @type {number} * @private */ this._extraStreamsVersion = 0; /** * Cached resource streams array. * * @type {GSplatStreamDescriptor[]|null} * @private */ this._resourceStreams = null; /** * Cached instance streams array. * * @type {GSplatStreamDescriptor[]|null} * @private */ this._instanceStreams = null; this._device = device; // Shallow copy streams array this.streams = [ ...streams ]; // Initialize stream names set for duplicate checking this._streamNames = new Set(this.streams.map((s)=>s.name)); // Pick the appropriate shader language based on device const isWebGPU = device.isWebGPU; this._read = isWebGPU ? options.readWGSL : options.readGLSL; // Validate read code is provided for the current device Debug.assert(this._read, `GSplatFormat: ${isWebGPU ? 'readWGSL' : 'readGLSL'} is required`); } } export { GSplatFormat };