UNPKG

playcanvas

Version:

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

257 lines (256 loc) 10.1 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 { Debug } from "../../core/debug.js"; import { SHADERLANGUAGE_GLSL, SHADERLANGUAGE_WGSL, TYPE_FLOAT32, TYPE_INT32, TYPE_UINT32 } from "../../platform/graphics/constants.js"; import { CACHE_STRIDE } from "./gsplat-projector-constants.js"; import glslVaryingDeclVS from "../shader-lib/glsl/chunks/gsplat/varyings/gsplatVaryingDeclVS.js"; import glslVaryingDeclPS from "../shader-lib/glsl/chunks/gsplat/varyings/gsplatVaryingDeclPS.js"; import wgslVaryingDeclVS from "../shader-lib/wgsl/chunks/gsplat/varyings/gsplatVaryingDeclVS.js"; import wgslVaryingFlushVS from "../shader-lib/wgsl/chunks/gsplat/varyings/gsplatVaryingFlushVS.js"; import wgslVaryingDeclCS from "../shader-lib/wgsl/chunks/gsplat/varyings/gsplatVaryingDeclCS.js"; import wgslVaryingDeclPS from "../shader-lib/wgsl/chunks/gsplat/varyings/gsplatVaryingDeclPS.js"; import wgslVaryingCacheWriteCS from "../shader-lib/wgsl/chunks/gsplat/varyings/gsplatVaryingCacheWriteCS.js"; import wgslVaryingCacheReadVS from "../shader-lib/wgsl/chunks/gsplat/varyings/gsplatVaryingCacheReadVS.js"; const GLSL_TYPES = { [TYPE_FLOAT32]: ["float", "vec2", "vec3", "vec4"], [TYPE_INT32]: ["int", "ivec2", "ivec3", "ivec4"], [TYPE_UINT32]: ["uint", "uvec2", "uvec3", "uvec4"] }; const WGSL_TYPES = { [TYPE_FLOAT32]: ["f32", "vec2f", "vec3f", "vec4f"], [TYPE_INT32]: ["i32", "vec2i", "vec3i", "vec4i"], [TYPE_UINT32]: ["u32", "vec2u", "vec3u", "vec4u"] }; const COMPONENT_SWIZZLE = ["x", "y", "z", "w"]; const IDENTIFIER_REGEX = /^[a-z_]\w*$/i; const RE_NAME = /\{name\}/g; const RE_TYPE = /\{type\}/g; const RE_FUNC_NAME = /\{funcName\}/g; const RE_WORD = /\{word\}/g; const RE_VALUE = /\{value\}/g; const GLSL_CHUNK_NAMES = ["gsplatUserVaryingsVS", "gsplatUserVaryingsPS"]; const WGSL_CHUNK_NAMES = [ "gsplatUserVaryingsVS", "gsplatUserVaryingsFlushVS", "gsplatUserVaryingsCS", "gsplatUserVaryingsPS", "gsplatUserCacheWriteCS", "gsplatUserCacheReadVS" ]; const pascal = (name) => name.charAt(0).toUpperCase() + name.slice(1); const encodeWord = (type, expr) => { return type === TYPE_UINT32 ? expr : `bitcast<u32>(${expr})`; }; const decodeWord = (type, expr) => { if (type === TYPE_UINT32) return expr; return type === TYPE_INT32 ? `bitcast<i32>(${expr})` : `bitcast<f32>(${expr})`; }; class GSplatVaryings { /** * Creates a new GSplatVaryings instance. * * @param {GraphicsDevice} device - The graphics device. * @ignore */ constructor(device) { /** * @type {GraphicsDevice} * @private */ __publicField(this, "_device"); /** * @type {GSplatVaryingDescriptor[]} * @private */ __publicField(this, "_streams", []); /** * @type {number} * @private */ __publicField(this, "_words", 0); /** * @type {number} * @private */ __publicField(this, "_version", 0); this._device = device; } /** * Gets the varying stream descriptors. Do not modify the returned array. * * @type {GSplatVaryingDescriptor[]} */ get streams() { return this._streams; } /** * The number of u32 words the varying streams add to the per-splat projection cache of the * {@link GSPLAT_RENDERER_RASTER_GPU_SORT} renderer. * * @type {number} * @ignore */ get words() { return this._words; } /** * The version of the varying streams, incremented on every change. * * @type {number} * @ignore */ get version() { return this._version; } /** * Adds varying streams. For each stream, a set function (`set<Name>`) is generated and made * available to the `gsplatModifyVS` shader chunk, where it runs once per splat, and a * matching get function (`get<Name>`) is made available to the `gsplatModifyPS` shader * chunk, where the per-splat value can be read for each rendered fragment. * * Supported types are {@link TYPE_FLOAT32}, {@link TYPE_INT32} and {@link TYPE_UINT32}, with * 1 to 4 components. Adding or removing streams rebuilds the gsplat shaders. * * Note: on some platforms each component is stored in per-splat video memory, so its size * scales with the number of rendered splats. Keep the data as compact as possible - prefer * fewer components, and consider bit-packing multiple small values into a single uint * component instead of using separate streams. * * @param {GSplatVaryingDescriptor[]} streams - The streams to add. * @example * // Add a per-splat flag, written once per splat in gsplatModifyVS using setFlag(value), * // and read per fragment in gsplatModifyPS using getFlag() * app.scene.gsplat.varyings.add([{ * name: 'flag', * type: TYPE_UINT32, * components: 1 * }]); */ add(streams) { Debug.call(() => { Debug.assert(streams && streams.length > 0, "GSplatVaryings#add: streams must be a non-empty array."); streams?.forEach((s) => { Debug.assert(!!s && IDENTIFIER_REGEX.test(s.name ?? ""), `GSplatVaryings: varying name '${s?.name}' is not a valid identifier.`); Debug.assert(!!GLSL_TYPES[s.type], `GSplatVaryings: varying '${s.name}' has unsupported type. Use TYPE_FLOAT32, TYPE_INT32 or TYPE_UINT32.`); Debug.assert(s.components >= 1 && s.components <= 4, `GSplatVaryings: varying '${s.name}' must have 1 to 4 components.`); Debug.assert(!this._streams.some((v) => v.name === s.name), `GSplatVaryings: varying '${s.name}' already exists.`); }); }); for (const s of streams) { this._streams.push({ name: s.name, type: s.type, components: s.components }); } this._changed(); } /** * Removes varying streams previously added by {@link GSplatVaryings#add}. * * @param {string[]} names - The names of the streams to remove. */ remove(names) { Debug.call(() => { Debug.assert(names && names.length > 0, "GSplatVaryings#remove: names must be a non-empty array."); }); const count = this._streams.length; this._streams = this._streams.filter((v) => !names.includes(v.name)); if (this._streams.length !== count) { this._changed(); } } /** * Marks the streams as changed: recomputes the cache word count eagerly (so consumers never * see a stale value) and bumps the version. The shader chunks are regenerated and applied to * the material once per frame by the engine via {@link GSplatVaryings#apply}. * * @private */ _changed() { this._words = this._streams.reduce((sum, s) => sum + s.components, 0); this._version++; } /** * Generates the shader chunks implementing the varying streams: declarations and set * functions for the vertex stage (and its compute projector equivalent), declarations and * get functions for the fragment stage, and the projection cache read / write code used by * the hybrid renderer. * * @returns {object} The generated chunk sources. * @private */ _generateChunks() { const isWebGPU = this._device.isWebGPU; const types = isWebGPU ? WGSL_TYPES : GLSL_TYPES; const declVSTemplate = isWebGPU ? wgslVaryingDeclVS : glslVaryingDeclVS; const declPSTemplate = isWebGPU ? wgslVaryingDeclPS : glslVaryingDeclPS; const vs = []; const ps = []; const flush = []; const cs = []; const cacheWrite = []; const cacheRead = []; let wordOffset = 0; for (const s of this._streams) { const { name, type, components } = s; const shaderType = types[type][components - 1]; const funcName = pascal(name); const sub = (template) => template.replace(RE_NAME, name).replace(RE_TYPE, shaderType).replace(RE_FUNC_NAME, funcName); vs.push(sub(declVSTemplate)); ps.push(sub(declPSTemplate)); if (isWebGPU) { flush.push(sub(wgslVaryingFlushVS)); cs.push(sub(wgslVaryingDeclCS)); const words = []; for (let c = 0; c < components; c++) { const component = components === 1 ? `_user_${name}` : `_user_${name}.${COMPONENT_SWIZZLE[c]}`; cacheWrite.push(wgslVaryingCacheWriteCS.replace(RE_WORD, String(CACHE_STRIDE + wordOffset + c)).replace(RE_VALUE, encodeWord(type, component))); words.push(decodeWord(type, `projCache[base + ${CACHE_STRIDE + wordOffset + c}u]`)); } cacheRead.push(wgslVaryingCacheReadVS.replace(RE_NAME, name).replace(RE_VALUE, components === 1 ? words[0] : `${shaderType}(${words.join(", ")})`)); } wordOffset += components; } const chunks = { gsplatUserVaryingsVS: vs.join(""), gsplatUserVaryingsPS: ps.join("") }; if (isWebGPU) { chunks.gsplatUserVaryingsFlushVS = flush.join(""); chunks.gsplatUserVaryingsCS = cs.join(""); chunks.gsplatUserCacheWriteCS = cacheWrite.join(""); chunks.gsplatUserCacheReadVS = cacheRead.join(""); } return chunks; } /** * Regenerates the shader chunks and applies them to the material, from where the renderers * pick them up (and rebuild shaders) via the existing chunk synchronization. Consumers track * {@link GSplatVaryings#version} to call this only when the streams changed. * * @param {ShaderMaterial} material - The gsplat material to apply the chunks to. * @ignore */ apply(material) { const isWebGPU = this._device.isWebGPU; const chunks = material.getShaderChunks(isWebGPU ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL); material.setDefine("GSPLAT_USER_VARYINGS", this._streams.length > 0); if (this._streams.length > 0) { const sources = this._generateChunks(); for (const name in sources) { chunks.set(name, sources[name]); } } else { const names = isWebGPU ? WGSL_CHUNK_NAMES : GLSL_CHUNK_NAMES; names.forEach((name) => chunks.delete(name)); } material.update(); } } export { GSplatVaryings };