From 04e4f0a086474c450a7191660db20f0480c0f764 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Wed, 25 Feb 2026 12:01:41 +0000 Subject: [PATCH 1/3] Compact GSplat work buffer format --- .../lod-streaming.controls.mjs | 10 + .../lod-streaming.example.mjs | 4 + .../gaussian-splatting/viewer.controls.mjs | 9 + .../gaussian-splatting/viewer.example.mjs | 3 + src/scene/constants.js | 18 ++ src/scene/gsplat-unified/gsplat-info.js | 14 +- src/scene/gsplat-unified/gsplat-manager.js | 24 +++ src/scene/gsplat-unified/gsplat-params.js | 126 +++++++++-- src/scene/gsplat-unified/gsplat-renderer.js | 5 +- .../gsplat-work-buffer-render-pass.js | 21 +- src/scene/gsplat/gsplat-format.js | 22 ++ src/scene/gsplat/gsplat-resource-base.js | 11 +- .../frag/formats/containerCompactWrite.js | 37 ++++ .../frag/formats/containerPackedWrite.js | 21 ++ .../gsplat/frag/gsplatCopyToWorkbuffer.js | 203 +++++++----------- .../vert/formats/containerCompactRead.js | 54 +++++ .../glsl/collections/gsplat-chunks-glsl.js | 2 - .../frag/formats/containerCompactWrite.js | 37 ++++ .../frag/formats/containerPackedWrite.js | 10 + .../gsplat/frag/gsplatCopyToWorkbuffer.js | 190 +++++++--------- .../vert/formats/containerCompactRead.js | 54 +++++ .../wgsl/collections/gsplat-chunks-wgsl.js | 2 - 22 files changed, 584 insertions(+), 293 deletions(-) create mode 100644 src/scene/shader-lib/glsl/chunks/gsplat/frag/formats/containerCompactWrite.js create mode 100644 src/scene/shader-lib/glsl/chunks/gsplat/frag/formats/containerPackedWrite.js create mode 100644 src/scene/shader-lib/glsl/chunks/gsplat/vert/formats/containerCompactRead.js create mode 100644 src/scene/shader-lib/wgsl/chunks/gsplat/frag/formats/containerCompactWrite.js create mode 100644 src/scene/shader-lib/wgsl/chunks/gsplat/frag/formats/containerPackedWrite.js create mode 100644 src/scene/shader-lib/wgsl/chunks/gsplat/vert/formats/containerCompactRead.js diff --git a/examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs b/examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs index d5f4bc9f7fe..9d16a3c9438 100644 --- a/examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs +++ b/examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs @@ -39,6 +39,16 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { value: observer.get('highRes') || false }) ), + jsx( + LabelGroup, + { text: 'Compact' }, + jsx(BooleanInput, { + type: 'toggle', + binding: new BindingTwoWay(), + link: { observer, path: 'compact' }, + value: observer.get('compact') || false + }) + ), jsx( LabelGroup, { text: 'Colorize LOD' }, diff --git a/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs b/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs index a8836692e14..48150023069 100644 --- a/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs +++ b/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs @@ -150,10 +150,14 @@ assetListLoader.load(() => { data.on('debugLod:set', () => { app.scene.gsplat.colorizeLod = !!data.get('debugLod'); }); + data.on('compact:set', () => { + app.scene.gsplat.dataFormat = data.get('compact') ? pc.GSPLATDATA_COMPACT : pc.GSPLATDATA_LARGE; + }); // initialize UI settings (must be after observer registration) data.set('gpuSorting', false); data.set('culling', false); + data.set('compact', true); data.set('debugLod', false); data.set('lodPreset', pc.platform.mobile ? 'mobile' : 'desktop'); data.set('splatBudget', pc.platform.mobile ? '1M' : '4M'); diff --git a/examples/src/examples/gaussian-splatting/viewer.controls.mjs b/examples/src/examples/gaussian-splatting/viewer.controls.mjs index d8e6e545561..3978525dac0 100644 --- a/examples/src/examples/gaussian-splatting/viewer.controls.mjs +++ b/examples/src/examples/gaussian-splatting/viewer.controls.mjs @@ -19,6 +19,15 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { link: { observer, path: 'data.skydome' } }) ), + jsx( + LabelGroup, + { text: 'Compact' }, + jsx(BooleanInput, { + type: 'toggle', + binding: new BindingTwoWay(), + link: { observer, path: 'data.compact' } + }) + ), jsx( LabelGroup, { text: 'Orientation' }, diff --git a/examples/src/examples/gaussian-splatting/viewer.example.mjs b/examples/src/examples/gaussian-splatting/viewer.example.mjs index 7edde857179..37192d87e7e 100644 --- a/examples/src/examples/gaussian-splatting/viewer.example.mjs +++ b/examples/src/examples/gaussian-splatting/viewer.example.mjs @@ -165,6 +165,7 @@ assetListLoader.load(() => { // Initialize data values data.set('data', { skydome: false, + compact: true, orientation: 180, tonemapping: pc.TONEMAP_LINEAR, grading: { @@ -225,6 +226,8 @@ assetListLoader.load(() => { data.on('*:set', (/** @type {string} */ path) => { if (path === 'data.skydome') { applySkydome(); + } else if (path === 'data.compact') { + app.scene.gsplat.dataFormat = data.get('data.compact') ? pc.GSPLATDATA_COMPACT : pc.GSPLATDATA_LARGE; } else if (path === 'data.orientation') { // Apply orientation to splat entity if (splatEntity) { diff --git a/src/scene/constants.js b/src/scene/constants.js index ae4b5abddcc..bf8a0aa8cd4 100644 --- a/src/scene/constants.js +++ b/src/scene/constants.js @@ -1195,3 +1195,21 @@ export const GSPLAT_STREAM_RESOURCE = 0; * @category Graphics */ export const GSPLAT_STREAM_INSTANCE = 1; + +/** + * Large work buffer data format with full precision. Uses RGBA16F color, float16 + * rotation and float16 scale. 32 bytes per splat. + * + * @type {string} + * @category Graphics + */ +export const GSPLATDATA_LARGE = 'large'; + +/** + * Compact work buffer data format optimized for reduced memory and bandwidth. Uses 11+11+10 bit + * RGB color, half-angle quaternion rotation and log-encoded scale. 20 bytes per splat. + * + * @type {string} + * @category Graphics + */ +export const GSPLATDATA_COMPACT = 'compact'; diff --git a/src/scene/gsplat-unified/gsplat-info.js b/src/scene/gsplat-unified/gsplat-info.js index 9094fa050fe..e5fb687ad1f 100644 --- a/src/scene/gsplat-unified/gsplat-info.js +++ b/src/scene/gsplat-unified/gsplat-info.js @@ -69,9 +69,6 @@ class GSplatInfo { /** @type {number} */ lineCount = 0; - /** @type {number} */ - padding = 0; - /** @type {Vec4} */ viewport = new Vec4(); @@ -213,14 +210,15 @@ class GSplatInfo { setLines(start, count, textureSize, activeSplats) { this.lineStart = start; this.lineCount = count; - this.padding = textureSize * count - activeSplats; - Debug.assert(this.padding >= 0); this.viewport.set(0, start, textureSize, count); - // Build sub-draw data for instanced interval rendering - if (this.intervals.length > 0) { - this.updateSubDraws(textureSize); + // Synthesize a full-range interval when none exist, so all paths use sub-draws + if (this.intervals.length === 0) { + this.intervals[0] = 0; + this.intervals[1] = activeSplats; } + + this.updateSubDraws(textureSize); } /** diff --git a/src/scene/gsplat-unified/gsplat-manager.js b/src/scene/gsplat-unified/gsplat-manager.js index 3bdc9457c86..2634d00335b 100644 --- a/src/scene/gsplat-unified/gsplat-manager.js +++ b/src/scene/gsplat-unified/gsplat-manager.js @@ -1112,8 +1112,29 @@ class GSplatManager { } } + /** + * Detects if the work buffer format has been replaced (e.g. dataFormat changed) and + * recreates the work buffer if needed. + * + * @private + */ + handleFormatChange() { + const currentFormat = this.scene.gsplat.format; + if (this.workBuffer.format !== currentFormat) { + this.workBuffer.destroy(); + this.workBuffer = new GSplatWorkBuffer(this.device, currentFormat); + this.renderer.workBuffer = this.workBuffer; + this.renderer.configureMaterial(); + this._workBufferFormatVersion = this.workBuffer.format.extraStreamsVersion; + this._workBufferRebuildRequired = true; + this.sortNeeded = true; + } + } + update() { + this.handleFormatChange(); + // detect work buffer format changes (extra streams added) and schedule a full rebuild const wbFormatVersion = this.workBuffer.format.extraStreamsVersion; if (this._workBufferFormatVersion !== wbFormatVersion) { @@ -1284,6 +1305,9 @@ class GSplatManager { this.rebuildWorkBuffer(sortedState, count); this._workBufferRebuildRequired = false; + // rebuildWorkBuffer may resize, which destroys/recreates orderBuffer — rebind it + this.renderer.setOrderData(); + // boundsBaseIndex may have changed — force interval metadata re-upload if (this.intervalCompaction) { this.intervalCompaction._uploadedVersion = -1; diff --git a/src/scene/gsplat-unified/gsplat-params.js b/src/scene/gsplat-unified/gsplat-params.js index 2d07763830e..85dedce2017 100644 --- a/src/scene/gsplat-unified/gsplat-params.js +++ b/src/scene/gsplat-unified/gsplat-params.js @@ -1,8 +1,19 @@ import { - PIXELFORMAT_R32U, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA16U, PIXELFORMAT_RGBA32U, PIXELFORMAT_RG32U + PIXELFORMAT_R32U, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA16U, + PIXELFORMAT_RGBA32U, PIXELFORMAT_RG32U } from '../../platform/graphics/constants.js'; import { ShaderMaterial } from '../materials/shader-material.js'; import { GSplatFormat } from '../gsplat/gsplat-format.js'; +import { GSPLATDATA_COMPACT } from '../constants.js'; + +import glslCompactRead from '../shader-lib/glsl/chunks/gsplat/vert/formats/containerCompactRead.js'; +import glslCompactWrite from '../shader-lib/glsl/chunks/gsplat/frag/formats/containerCompactWrite.js'; +import glslPackedRead from '../shader-lib/glsl/chunks/gsplat/vert/formats/containerPackedRead.js'; +import glslPackedWrite from '../shader-lib/glsl/chunks/gsplat/frag/formats/containerPackedWrite.js'; +import wgslCompactRead from '../shader-lib/wgsl/chunks/gsplat/vert/formats/containerCompactRead.js'; +import wgslCompactWrite from '../shader-lib/wgsl/chunks/gsplat/frag/formats/containerCompactWrite.js'; +import wgslPackedRead from '../shader-lib/wgsl/chunks/gsplat/vert/formats/containerPackedRead.js'; +import wgslPackedWrite from '../shader-lib/wgsl/chunks/gsplat/frag/formats/containerPackedWrite.js'; /** * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' @@ -29,28 +40,69 @@ class GSplatParams { */ _format; + /** + * @type {GraphicsDevice} + * @private + */ + _device; + + /** + * @type {string} + * @private + */ + _dataFormat = GSPLATDATA_COMPACT; + /** * Creates a new GSplatParams instance. * * @param {GraphicsDevice} device - The graphics device. */ constructor(device) { - // Check device capabilities for color format - use RGBA16U fallback if RGBA16F not supported - const colorFormat = device.getRenderableHdrFormat([PIXELFORMAT_RGBA16F]) || PIXELFORMAT_RGBA16U; - - // Work buffer textures format: - // - dataColor (RGBA16F/RGBA16U): RGBA color with alpha - // - dataTransformA (RGBA32U): worldCenter.xyz (3×32-bit floats as uint) + worldRotation.xy (2×16-bit halfs) - // - dataTransformB (RG32U): worldRotation.z + worldScale.xyz (4×16-bit halfs, w derived via sqrt) - this._format = new GSplatFormat(device, [ - { name: 'dataColor', format: colorFormat }, - { name: 'dataTransformA', format: PIXELFORMAT_RGBA32U }, - { name: 'dataTransformB', format: PIXELFORMAT_RG32U } - ], { - readGLSL: '#include "gsplatContainerPackedReadVS"', - readWGSL: '#include "gsplatContainerPackedReadVS"' - }); - this._format.allowStreamRemoval = true; + this._device = device; + this._format = this._createFormat(GSPLATDATA_COMPACT); + } + + /** + * @param {string} dataFormat - The data format constant. + * @returns {GSplatFormat} The created format. + * @private + */ + _createFormat(dataFormat) { + let format; + + if (dataFormat === GSPLATDATA_COMPACT) { + // Compact work buffer format (20 bytes/splat): + // - dataColor (R32U): RGB color (11+11+10 bits, range [0, 4]) + // - dataTransformA (RGBA32U): center.xyz (3×32-bit floats) + half-angle quaternion (11+11+10 bits) + // - dataTransformB (R32U): scale.xyz (3×8-bit log-encoded, e^-12..e^9) + alpha (8 bits) + format = new GSplatFormat(this._device, [ + { name: 'dataColor', format: PIXELFORMAT_R32U }, + { name: 'dataTransformA', format: PIXELFORMAT_RGBA32U }, + { name: 'dataTransformB', format: PIXELFORMAT_R32U } + ], { + readGLSL: glslCompactRead, + readWGSL: wgslCompactRead + }); + format.setWriteCode(glslCompactWrite, wgslCompactWrite); + } else { + // Large work buffer format (32 bytes/splat): + // - dataColor (RGBA16F/RGBA16U): RGBA color with alpha + // - dataTransformA (RGBA32U): center.xyz (3×32-bit floats as uint) + rotation.xy (2×16-bit halfs) + // - dataTransformB (RG32U): rotation.z + scale.xyz (4×16-bit halfs, scale.w derived via sqrt) + const colorFormat = this._device.getRenderableHdrFormat([PIXELFORMAT_RGBA16F]) || PIXELFORMAT_RGBA16U; + format = new GSplatFormat(this._device, [ + { name: 'dataColor', format: colorFormat }, + { name: 'dataTransformA', format: PIXELFORMAT_RGBA32U }, + { name: 'dataTransformB', format: PIXELFORMAT_RG32U } + ], { + readGLSL: glslPackedRead, + readWGSL: wgslPackedRead + }); + format.setWriteCode(glslPackedWrite, wgslPackedWrite); + } + + format.allowStreamRemoval = true; + return format; } /** @@ -455,6 +507,46 @@ class GSplatParams { */ cooldownTicks = 100; + /** + * Work buffer data format. Controls the precision and bandwidth of the intermediate work + * buffer used during unified GSplat rendering. Can be set to {@link GSPLATDATA_LARGE} + * (default, 32 bytes/splat, full precision) or {@link GSPLATDATA_COMPACT} (20 bytes/splat, + * reduced precision optimized for mobile). Changing this recreates the work buffer. + * + * @type {string} + */ + set dataFormat(value) { + if (this._dataFormat !== value) { + this._dataFormat = value; + + // capture extra streams from the old format + const extraStreams = this._format.extraStreams.map(s => ({ + name: s.name, + format: s.format, + storage: s.storage + })); + + // create new format with the new data layout + this._format = this._createFormat(value); + + // re-add extra streams + if (extraStreams.length > 0) { + this._format.addExtraStreams(extraStreams); + } + + this.dirty = true; + } + } + + /** + * Gets the work buffer data format. + * + * @type {string} + */ + get dataFormat() { + return this._dataFormat; + } + /** * A material template that can be customized by the user. Any defines, parameters, or shader * chunks set on this material will be automatically applied to all GSplat components rendered diff --git a/src/scene/gsplat-unified/gsplat-renderer.js b/src/scene/gsplat-unified/gsplat-renderer.js index d2b7202f7c6..a7638d015df 100644 --- a/src/scene/gsplat-unified/gsplat-renderer.js +++ b/src/scene/gsplat-unified/gsplat-renderer.js @@ -186,10 +186,7 @@ class GSplatRenderer { const dither = false; this._material.setParameter('numSplats', 0); - // Set order data - texture for WebGL only at init time, it does not need to be updated - if (workBuffer.orderTexture) { - this._material.setParameter('splatOrder', workBuffer.orderTexture); - } + this.setOrderData(); this._material.setParameter('alphaClip', 0.3); this._material.setDefine(`DITHER_${dither ? 'BLUENOISE' : 'NONE'}`, ''); diff --git a/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js b/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js index 20d6f9fd7e9..903682b3591 100644 --- a/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js +++ b/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js @@ -131,8 +131,7 @@ class GSplatWorkBufferRenderPass extends RenderPass { const scope = device.scope; Debug.assert(resource); - const { activeSplats, lineStart, viewport, subDrawTexture, subDrawCount } = splatInfo; - const useIntervals = subDrawTexture !== null && subDrawCount > 0; + const { lineStart, viewport, subDrawTexture, subDrawCount } = splatInfo; // Get work buffer modifier (live from placement, not a snapshot copy) const workBufferModifier = splatInfo.getWorkBufferModifier?.() ?? null; @@ -143,7 +142,6 @@ class GSplatWorkBufferRenderPass extends RenderPass { // quad renderer and material are cached in the resource const workBufferRenderInfo = resource.getWorkBufferRenderInfo( - useIntervals, this.colorOnly, workBufferModifier, formatHash, @@ -154,9 +152,7 @@ class GSplatWorkBufferRenderPass extends RenderPass { // Assign material properties to scope workBufferRenderInfo.material.setParameters(device); - scope.resolve('uActiveSplats').setValue(activeSplats); scope.resolve('uStartLine').setValue(lineStart); - scope.resolve('uViewportWidth').setValue(viewport.z); // Colorize by LOD using provided colors; use index 0 as fallback for non-LOD splats const color = this.colorsByLod?.[splatInfo.lodIndex] ?? this.colorsByLod?.[0] ?? _whiteColor; @@ -212,17 +208,12 @@ class GSplatWorkBufferRenderPass extends RenderPass { } } - if (useIntervals) { - // Instanced draw path: one quad per interval row-segment - scope.resolve('uSubDrawData').setValue(subDrawTexture); - scope.resolve('uLineCount').setValue(splatInfo.lineCount); - scope.resolve('uTextureWidth').setValue(viewport.z); + // Instanced draw: one quad per sub-draw row-segment + scope.resolve('uSubDrawData').setValue(subDrawTexture); + scope.resolve('uLineCount').setValue(splatInfo.lineCount); + scope.resolve('uTextureWidth').setValue(viewport.z); - workBufferRenderInfo.quadRender.render(viewport, undefined, subDrawCount); - } else { - // Standard single-quad draw path - workBufferRenderInfo.quadRender.render(viewport); - } + workBufferRenderInfo.quadRender.render(viewport, undefined, subDrawCount); } destroy() { diff --git a/src/scene/gsplat/gsplat-format.js b/src/scene/gsplat/gsplat-format.js index 7ee37501abc..42c5c5c0075 100644 --- a/src/scene/gsplat/gsplat-format.js +++ b/src/scene/gsplat/gsplat-format.js @@ -369,6 +369,28 @@ class GSplatFormat { return this._read; } + /** + * 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. diff --git a/src/scene/gsplat/gsplat-resource-base.js b/src/scene/gsplat/gsplat-resource-base.js index 5d2ba66fa97..f5fe2344254 100644 --- a/src/scene/gsplat/gsplat-resource-base.js +++ b/src/scene/gsplat/gsplat-resource-base.js @@ -226,7 +226,6 @@ class GSplatResourceBase { /** * Get or create a QuadRender for rendering to work buffer. * - * @param {boolean} useIntervals - Whether to use intervals. * @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. @@ -235,11 +234,11 @@ class GSplatResourceBase { * @returns {WorkBufferRenderInfo} The WorkBufferRenderInfo instance. * @ignore */ - getWorkBufferRenderInfo(useIntervals, colorOnly, workBufferModifier, formatHash, formatDeclarations, workBufferFormat) { + getWorkBufferRenderInfo(colorOnly, workBufferModifier, formatHash, formatDeclarations, workBufferFormat) { // configure defines to fetch cached data this.configureMaterialDefines(tempMap); - if (useIntervals) tempMap.set('GSPLAT_LOD', ''); + tempMap.set('GSPLAT_LOD', ''); if (colorOnly) tempMap.set('GSPLAT_COLOR_ONLY', ''); // Set HAS_NODE_MAPPING when resource has node mapping texture (octree resources) @@ -276,6 +275,12 @@ class GSplatResourceBase { chunks.set('gsplatWorkBufferOutputVS', outputCode); + // Inject format-specific write encoding chunk + const writeCode = workBufferFormat.getWriteCode(); + if (writeCode) { + chunks.set('gsplatWriteVS', writeCode); + } + // copy tempMap to material defines tempMap.forEach((v, k) => material.setDefine(k, v)); diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/frag/formats/containerCompactWrite.js b/src/scene/shader-lib/glsl/chunks/gsplat/frag/formats/containerCompactWrite.js new file mode 100644 index 00000000000..97d448a02b1 --- /dev/null +++ b/src/scene/shader-lib/glsl/chunks/gsplat/frag/formats/containerCompactWrite.js @@ -0,0 +1,37 @@ +// Write function for compact work buffer format (20 bytes/splat). +export default /* glsl */` +void writeSplat(vec3 center, vec4 rotation, vec3 scale, vec4 color) { + // Pack RGB as 11+11+10 bits into R32U, range [0, 4] + vec3 rgb = clamp(color.rgb, 0.0, 4.0); + uint rBits = uint(rgb.r * (2047.0 / 4.0) + 0.5); + uint gBits = uint(rgb.g * (2047.0 / 4.0) + 0.5); + uint bBits = uint(rgb.b * (1023.0 / 4.0) + 0.5); + writeDataColor(uvec4(rBits | (gBits << 11u) | (bBits << 22u), 0u, 0u, 0u)); + + #ifndef GSPLAT_COLOR_ONLY + // Half-angle quaternion projection: rotation is (x,y,z,w) with w >= 0 + vec4 q = rotation; + if (q.w < 0.0) q = -q; + vec3 p = q.xyz * inversesqrt(1.0 + q.w); + + // quantize from [-1, 1] to 11+11+10 bits + uint aBitsQ = uint(clamp((p.x * 0.5 + 0.5) * 2047.0 + 0.5, 0.0, 2047.0)); + uint bBitsQ = uint(clamp((p.y * 0.5 + 0.5) * 2047.0 + 0.5, 0.0, 2047.0)); + uint cBitsQ = uint(clamp((p.z * 0.5 + 0.5) * 1023.0 + 0.5, 0.0, 1023.0)); + uint packedQuat = aBitsQ | (bBitsQ << 11u) | (cBitsQ << 22u); + + writeDataTransformA(uvec4(floatBitsToUint(center.x), floatBitsToUint(center.y), floatBitsToUint(center.z), packedQuat)); + + // Log-encode scale (3x8 bits) + alpha (8 bits) + const float invLogRange = 255.0 / 21.0; + const float logMin = -12.0; + uint sxBits = scale.x < 1e-10 ? 0u : uint(clamp((log(scale.x) - logMin) * invLogRange + 0.5, 1.0, 255.0)); + uint syBits = scale.y < 1e-10 ? 0u : uint(clamp((log(scale.y) - logMin) * invLogRange + 0.5, 1.0, 255.0)); + uint szBits = scale.z < 1e-10 ? 0u : uint(clamp((log(scale.z) - logMin) * invLogRange + 0.5, 1.0, 255.0)); + uint alphaBits = uint(clamp(color.a, 0.0, 1.0) * 255.0 + 0.5); + uint packedScale = sxBits | (syBits << 8u) | (szBits << 16u) | (alphaBits << 24u); + + writeDataTransformB(uvec4(packedScale, 0u, 0u, 0u)); + #endif +} +`; diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/frag/formats/containerPackedWrite.js b/src/scene/shader-lib/glsl/chunks/gsplat/frag/formats/containerPackedWrite.js new file mode 100644 index 00000000000..bd943bc36f9 --- /dev/null +++ b/src/scene/shader-lib/glsl/chunks/gsplat/frag/formats/containerPackedWrite.js @@ -0,0 +1,21 @@ +// Write function for packed (large) work buffer format (32 bytes/splat). +export default /* glsl */` +void writeSplat(vec3 center, vec4 rotation, vec3 scale, vec4 color) { + #ifdef GSPLAT_COLOR_UINT + uint packed_rg = packHalf2x16(color.rg); + uint packed_ba = packHalf2x16(color.ba); + writeDataColor(uvec4( + packed_rg & 0xFFFFu, + packed_rg >> 16u, + packed_ba & 0xFFFFu, + packed_ba >> 16u + )); + #else + writeDataColor(color); + #endif + #ifndef GSPLAT_COLOR_ONLY + writeDataTransformA(uvec4(floatBitsToUint(center.x), floatBitsToUint(center.y), floatBitsToUint(center.z), packHalf2x16(rotation.xy))); + writeDataTransformB(uvec4(packHalf2x16(vec2(rotation.z, scale.x)), packHalf2x16(scale.yz), 0u, 0u)); + #endif +} +`; diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js b/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js index 08a7ae0b98f..cbb9170a0ef 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js @@ -12,21 +12,16 @@ export default /* glsl */` #include "gsplatQuatToMat3VS" #include "gsplatReadVS" #include "gsplatWorkBufferOutputVS" +#include "gsplatWriteVS" #include "gsplatModifyVS" uniform int uStartLine; // Start row in destination texture -uniform int uViewportWidth; // Width of the destination viewport in pixels -#ifdef GSPLAT_LOD - // Packed sub-draw params: (sourceBase, colStart, rowWidth, rowStart) - flat varying ivec4 vSubDraw; -#endif +// Packed sub-draw params: (sourceBase, colStart, rowWidth, rowStart) +flat varying ivec4 vSubDraw; uniform vec3 uColorMultiply; -// number of splats -uniform int uActiveSplats; - // pre-computed model matrix decomposition uniform vec3 model_scale; uniform vec4 model_rotation; // (x,y,z,w) format @@ -45,133 +40,85 @@ uniform vec4 model_rotation; // (x,y,z,w) format #endif void main(void) { - // local fragment coordinates (within the viewport) - ivec2 localFragCoords = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y) - uStartLine); + // Compute source index from packed sub-draw varying: (sourceBase, colStart, rowWidth, rowStart) + int localRow = int(gl_FragCoord.y) - uStartLine - vSubDraw.w; + int localCol = int(gl_FragCoord.x) - vSubDraw.y; + uint originalIndex = uint(vSubDraw.x + localRow * vSubDraw.z + localCol); + + // Initialize global splat for format read functions + setSplat(originalIndex); + + // read center in local space + vec3 modelCenter = getCenter(); + + // compute world-space center for storage + vec3 worldCenter = (matrix_model * vec4(modelCenter, 1.0)).xyz; + SplatCenter center; + initCenter(modelCenter, center); + + // Get source rotation and scale + // getRotation() returns (w,x,y,z) format, convert to (x,y,z,w) for quatMul + vec4 srcRotation = getRotation().yzwx; + vec3 srcScale = getScale(); + + // Combine: world = model * source (both in x,y,z,w format) + vec4 worldRotation = quatMul(model_rotation, srcRotation); + // Ensure w is positive so sqrt() reconstruction works correctly + // (quaternions q and -q represent the same rotation) + if (worldRotation.w < 0.0) { + worldRotation = -worldRotation; + } + vec3 worldScale = model_scale * srcScale; - // linear index of the splat - int targetIndex = localFragCoords.y * uViewportWidth + localFragCoords.x; - if (targetIndex >= uActiveSplats) { + // Apply custom center modification + vec3 originalCenter = worldCenter; + modifySplatCenter(worldCenter); - // Out of bounds: write zeros - #ifdef GSPLAT_COLOR_UINT - writeDataColor(uvec4(0u)); - #else - writeDataColor(vec4(0.0)); - #endif - #ifndef GSPLAT_COLOR_ONLY - writeDataTransformA(uvec4(0u)); - writeDataTransformB(uvec4(0u)); - #endif - #ifdef GSPLAT_ID - writePcId(uvec4(0u)); - #endif - #ifdef GSPLAT_NODE_INDEX - writePcNodeIndex(uvec4(0u)); - #endif + // Apply custom rotation/scale modification + modifySplatRotationScale(originalCenter, worldCenter, worldRotation, worldScale); - } else { + // read color + vec4 color = getColor(); - #ifdef GSPLAT_LOD - // Compute source index from packed sub-draw varying: (sourceBase, colStart, rowWidth, rowStart) - int localRow = int(gl_FragCoord.y) - uStartLine - vSubDraw.w; - int localCol = int(gl_FragCoord.x) - vSubDraw.y; - uint originalIndex = uint(vSubDraw.x + localRow * vSubDraw.z + localCol); - #else - uint originalIndex = uint(targetIndex); - #endif - - // Initialize global splat for format read functions - setSplat(originalIndex); - - // read center in local space - vec3 modelCenter = getCenter(); - - // compute world-space center for storage - vec3 worldCenter = (matrix_model * vec4(modelCenter, 1.0)).xyz; - SplatCenter center; - initCenter(modelCenter, center); - - // Get source rotation and scale - // getRotation() returns (w,x,y,z) format, convert to (x,y,z,w) for quatMul - vec4 srcRotation = getRotation().yzwx; - vec3 srcScale = getScale(); - - // Combine: world = model * source (both in x,y,z,w format) - vec4 worldRotation = quatMul(model_rotation, srcRotation); - // Ensure w is positive so sqrt() reconstruction works correctly - // (quaternions q and -q represent the same rotation) - if (worldRotation.w < 0.0) { - worldRotation = -worldRotation; - } - vec3 worldScale = model_scale * srcScale; - - // Apply custom center modification - vec3 originalCenter = worldCenter; - modifySplatCenter(worldCenter); - - // Apply custom rotation/scale modification - modifySplatRotationScale(originalCenter, worldCenter, worldRotation, worldScale); - - // read color - vec4 color = getColor(); - - // evaluate spherical harmonics - #if SH_BANDS > 0 - // calculate the model-space view direction - vec3 dir = normalize(center.view * mat3(center.modelView)); - - // read sh coefficients - vec3 sh[SH_COEFFS]; - float scale; - readSHData(sh, scale); - - // evaluate - color.xyz += evalSH(sh, dir) * scale; - #endif + // evaluate spherical harmonics + #if SH_BANDS > 0 + // calculate the model-space view direction + vec3 dir = normalize(center.view * mat3(center.modelView)); - // Apply custom color modification - modifySplatColor(worldCenter, color); - - color.xyz *= uColorMultiply; - - // write out results using generated write functions - #ifdef GSPLAT_COLOR_UINT - // Pack RGBA as 4x half-float (16-bit) values for RGBA16U format - uint packed_rg = packHalf2x16(color.rg); - uint packed_ba = packHalf2x16(color.ba); - writeDataColor(uvec4( - packed_rg & 0xFFFFu, // R as half - packed_rg >> 16u, // G as half - packed_ba & 0xFFFFu, // B as half - packed_ba >> 16u // A as half - )); - #else - writeDataColor(color); - #endif - #ifndef GSPLAT_COLOR_ONLY - // Store rotation (xyz, w derived) and scale as 6 half-floats - writeDataTransformA(uvec4(floatBitsToUint(worldCenter.x), floatBitsToUint(worldCenter.y), floatBitsToUint(worldCenter.z), packHalf2x16(worldRotation.xy))); - writeDataTransformB(uvec4(packHalf2x16(vec2(worldRotation.z, worldScale.x)), packHalf2x16(worldScale.yz), 0u, 0u)); - #endif + // read sh coefficients + vec3 sh[SH_COEFFS]; + float scale; + readSHData(sh, scale); - #ifdef GSPLAT_ID - writePcId(uvec4(uId, 0u, 0u, 0u)); - #endif + // evaluate + color.xyz += evalSH(sh, dir) * scale; + #endif + + // Apply custom color modification + modifySplatColor(worldCenter, color); + + color.xyz *= uColorMultiply; - #ifdef GSPLAT_NODE_INDEX - #ifdef HAS_NODE_MAPPING - // Octree resource: look up node index from source splat, then local bounds index - int srcTextureWidth = int(textureSize(nodeMappingTexture, 0).x); - ivec2 sourceCoord = ivec2(int(originalIndex) % srcTextureWidth, int(originalIndex) / srcTextureWidth); - uint nodeIndex = texelFetch(nodeMappingTexture, sourceCoord, 0).r; - ivec2 ntlCoord = ivec2(int(nodeIndex) % nodeToLocalBoundsWidth, int(nodeIndex) / nodeToLocalBoundsWidth); - uint localBoundsIdx = texelFetch(nodeToLocalBoundsTexture, ntlCoord, 0).r; - writePcNodeIndex(uvec4(uBoundsBaseIndex + localBoundsIdx, 0u, 0u, 0u)); - #else - // Non-octree resource: single bounds entry - writePcNodeIndex(uvec4(uBoundsBaseIndex, 0u, 0u, 0u)); - #endif + // write color + transform using format-specific encoding + writeSplat(worldCenter, worldRotation, worldScale, color); + + #ifdef GSPLAT_ID + writePcId(uvec4(uId, 0u, 0u, 0u)); + #endif + + #ifdef GSPLAT_NODE_INDEX + #ifdef HAS_NODE_MAPPING + // Octree resource: look up node index from source splat, then local bounds index + int srcTextureWidth = int(textureSize(nodeMappingTexture, 0).x); + ivec2 sourceCoord = ivec2(int(originalIndex) % srcTextureWidth, int(originalIndex) / srcTextureWidth); + uint nodeIndex = texelFetch(nodeMappingTexture, sourceCoord, 0).r; + ivec2 ntlCoord = ivec2(int(nodeIndex) % nodeToLocalBoundsWidth, int(nodeIndex) / nodeToLocalBoundsWidth); + uint localBoundsIdx = texelFetch(nodeToLocalBoundsTexture, ntlCoord, 0).r; + writePcNodeIndex(uvec4(uBoundsBaseIndex + localBoundsIdx, 0u, 0u, 0u)); + #else + // Non-octree resource: single bounds entry + writePcNodeIndex(uvec4(uBoundsBaseIndex, 0u, 0u, 0u)); #endif - } + #endif } `; diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/vert/formats/containerCompactRead.js b/src/scene/shader-lib/glsl/chunks/gsplat/vert/formats/containerCompactRead.js new file mode 100644 index 00000000000..4fc6169c0c8 --- /dev/null +++ b/src/scene/shader-lib/glsl/chunks/gsplat/vert/formats/containerCompactRead.js @@ -0,0 +1,54 @@ +// Read functions for compact work buffer format (20 bytes/splat): +// - dataColor: R32U (4B): RGB color (11+11+10 bits, range [0, 4]) +// - dataTransformA: RGBA32U (16B): center.xyz as f32 + half-angle quaternion (11+11+10 bits) +// - dataTransformB: R32U (4B): scale.xyz as 3x8-bit log-encoded + alpha (8 bits) +export default /* glsl */` +uvec4 cachedTransformA; +uint cachedTransformB; + +vec3 getCenter() { + cachedTransformA = loadDataTransformA(); + cachedTransformB = loadDataTransformB().x; + return vec3(uintBitsToFloat(cachedTransformA.r), uintBitsToFloat(cachedTransformA.g), uintBitsToFloat(cachedTransformA.b)); +} + +vec4 getColor() { + uint packed = loadDataColor().x; + float r = float(packed & 0x7FFu) * (4.0 / 2047.0); + float g = float((packed >> 11u) & 0x7FFu) * (4.0 / 2047.0); + float b = float((packed >> 22u) & 0x3FFu) * (4.0 / 1023.0); + float a = float(cachedTransformB >> 24u) / 255.0; + return vec4(r, g, b, a); +} + +vec4 getRotation() { + uint packed = cachedTransformA.a; + + // dequantize half-angle projected quaternion: 11+11+10 bits to [-1, 1] + vec3 p = vec3( + float(packed & 0x7FFu) / 2047.0 * 2.0 - 1.0, + float((packed >> 11u) & 0x7FFu) / 2047.0 * 2.0 - 1.0, + float((packed >> 22u) & 0x3FFu) / 1023.0 * 2.0 - 1.0 + ); + + // inverse half-angle transform, returns (w, x, y, z) format + float d = dot(p, p); + return vec4(1.0 - d, sqrt(max(0.0, 2.0 - d)) * p); +} + +vec3 getScale() { + uint packed = cachedTransformB; + float sx = float(packed & 0xFFu); + float sy = float((packed >> 8u) & 0xFFu); + float sz = float((packed >> 16u) & 0xFFu); + + // decode log-encoded scale: 0 = true zero, 1-255 maps linearly in log-space to e^-12..e^9 + const float logRange = 21.0 / 255.0; + const float logMin = -12.0; + return vec3( + sx == 0.0 ? 0.0 : exp(sx * logRange + logMin), + sy == 0.0 ? 0.0 : exp(sy * logRange + logMin), + sz == 0.0 ? 0.0 : exp(sz * logRange + logMin) + ); +} +`; diff --git a/src/scene/shader-lib/glsl/collections/gsplat-chunks-glsl.js b/src/scene/shader-lib/glsl/collections/gsplat-chunks-glsl.js index e3e3a643c95..b6af44a9b7a 100644 --- a/src/scene/shader-lib/glsl/collections/gsplat-chunks-glsl.js +++ b/src/scene/shader-lib/glsl/collections/gsplat-chunks-glsl.js @@ -23,7 +23,6 @@ import gsplatSogVS from '../chunks/gsplat/vert/formats/sog.js'; import gsplatSogSHVS from '../chunks/gsplat/vert/formats/sogSH.js'; import gsplatContainerDeclVS from '../chunks/gsplat/vert/formats/containerDecl.js'; import gsplatContainerReadVS from '../chunks/gsplat/vert/formats/containerRead.js'; -import gsplatContainerPackedReadVS from '../chunks/gsplat/vert/formats/containerPackedRead.js'; import gsplatContainerFloatReadVS from '../chunks/gsplat/vert/formats/containerFloatRead.js'; export const gsplatChunksGLSL = { @@ -50,6 +49,5 @@ export const gsplatChunksGLSL = { gsplatSogSHVS, gsplatContainerDeclVS, gsplatContainerReadVS, - gsplatContainerPackedReadVS, gsplatContainerFloatReadVS }; diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/frag/formats/containerCompactWrite.js b/src/scene/shader-lib/wgsl/chunks/gsplat/frag/formats/containerCompactWrite.js new file mode 100644 index 00000000000..06a0216a28a --- /dev/null +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/frag/formats/containerCompactWrite.js @@ -0,0 +1,37 @@ +// Write function for compact work buffer format (20 bytes/splat). +export default /* wgsl */` +fn writeSplat(center: vec3f, rotation: vec4f, scale: vec3f, color: vec4f) { + // Pack RGB as 11+11+10 bits into R32U, range [0, 4] + let rgb = clamp(color.rgb, vec3f(0.0), vec3f(4.0)); + let rBits = u32(rgb.r * (2047.0 / 4.0) + 0.5); + let gBits = u32(rgb.g * (2047.0 / 4.0) + 0.5); + let bBits = u32(rgb.b * (1023.0 / 4.0) + 0.5); + writeDataColor(vec4u(rBits | (gBits << 11u) | (bBits << 22u), 0u, 0u, 0u)); + + #ifndef GSPLAT_COLOR_ONLY + // Half-angle quaternion projection: rotation is (x,y,z,w) with w >= 0 + var q = rotation; + if (q.w < 0.0) { q = -q; } + let p = q.xyz * inverseSqrt(1.0 + q.w); + + // quantize from [-1, 1] to 11+11+10 bits + let aBitsQ = u32(clamp(p.x * 0.5 + 0.5, 0.0, 1.0) * 2047.0 + 0.5); + let bBitsQ = u32(clamp(p.y * 0.5 + 0.5, 0.0, 1.0) * 2047.0 + 0.5); + let cBitsQ = u32(clamp(p.z * 0.5 + 0.5, 0.0, 1.0) * 1023.0 + 0.5); + let packedQuat = aBitsQ | (bBitsQ << 11u) | (cBitsQ << 22u); + + writeDataTransformA(vec4u(bitcast(center.x), bitcast(center.y), bitcast(center.z), packedQuat)); + + // Log-encode scale (3x8 bits) + alpha (8 bits) + let invLogRange = 255.0 / 21.0; + let logMin = -12.0; + let sxBits = select(u32(clamp((log(scale.x) - logMin) * invLogRange + 0.5, 1.0, 255.0)), 0u, scale.x < 1e-10); + let syBits = select(u32(clamp((log(scale.y) - logMin) * invLogRange + 0.5, 1.0, 255.0)), 0u, scale.y < 1e-10); + let szBits = select(u32(clamp((log(scale.z) - logMin) * invLogRange + 0.5, 1.0, 255.0)), 0u, scale.z < 1e-10); + let alphaBits = u32(clamp(color.a, 0.0, 1.0) * 255.0 + 0.5); + let packedScale = sxBits | (syBits << 8u) | (szBits << 16u) | (alphaBits << 24u); + + writeDataTransformB(vec4u(packedScale, 0u, 0u, 0u)); + #endif +} +`; diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/frag/formats/containerPackedWrite.js b/src/scene/shader-lib/wgsl/chunks/gsplat/frag/formats/containerPackedWrite.js new file mode 100644 index 00000000000..966a1e7da29 --- /dev/null +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/frag/formats/containerPackedWrite.js @@ -0,0 +1,10 @@ +// Write function for packed (large) work buffer format (32 bytes/splat). +export default /* wgsl */` +fn writeSplat(center: vec3f, rotation: vec4f, scale: vec3f, color: vec4f) { + writeDataColor(color); + #ifndef GSPLAT_COLOR_ONLY + writeDataTransformA(vec4u(bitcast(center.x), bitcast(center.y), bitcast(center.z), pack2x16float(rotation.xy))); + writeDataTransformB(vec4u(pack2x16float(vec2f(rotation.z, scale.x)), pack2x16float(scale.yz), 0u, 0u)); + #endif +} +`; diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js b/src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js index 5523e95d043..9d9bd79c430 100644 --- a/src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js @@ -18,21 +18,16 @@ var processOutput: FragmentOutput; // Work buffer output write functions (generated by GSplatFormat.getOutputDeclarations) #include "gsplatWorkBufferOutputVS" +#include "gsplatWriteVS" #include "gsplatModifyVS" uniform uStartLine: i32; // Start row in destination texture -uniform uViewportWidth: i32; // Width of the destination viewport in pixels -#ifdef GSPLAT_LOD - // Packed sub-draw params: (sourceBase, colStart, rowWidth, rowStart) - varying @interpolate(flat) vSubDraw: vec4i; -#endif +// Packed sub-draw params: (sourceBase, colStart, rowWidth, rowStart) +varying @interpolate(flat) vSubDraw: vec4i; uniform uColorMultiply: vec3f; -// number of splats -uniform uActiveSplats: i32; - // pre-computed model matrix decomposition uniform model_scale: vec3f; uniform model_rotation: vec4f; // (x,y,z,w) format @@ -52,120 +47,87 @@ uniform model_rotation: vec4f; // (x,y,z,w) format @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { - // local fragment coordinates (within the viewport) - let localFragCoords = vec2i(i32(input.position.x), i32(input.position.y) - uniform.uStartLine); - - // linear index of the splat - let targetIndex = localFragCoords.y * uniform.uViewportWidth + localFragCoords.x; - - if (targetIndex >= uniform.uActiveSplats) { - - // Out of bounds: write zeros using generated write functions - writeDataColor(vec4f(0.0)); - #ifndef GSPLAT_COLOR_ONLY - writeDataTransformA(vec4u(0u)); - writeDataTransformB(vec4u(0u)); - #endif - #ifdef GSPLAT_ID - writePcId(vec4u(0u)); - #endif - #ifdef GSPLAT_NODE_INDEX - writePcNodeIndex(vec4u(0u)); - #endif + // Compute source index from packed sub-draw varying: (sourceBase, colStart, rowWidth, rowStart) + let localRow = i32(input.position.y) - uniform.uStartLine - input.vSubDraw.w; + let localCol = i32(input.position.x) - input.vSubDraw.y; + let originalIndex = u32(input.vSubDraw.x + localRow * input.vSubDraw.z + localCol); + + // Initialize global splat for format read functions + setSplat(originalIndex); + + // read center in local space + var modelCenter = getCenter(); + + // compute world-space center for storage + var worldCenter = (uniform.matrix_model * vec4f(modelCenter, 1.0)).xyz; + var center: SplatCenter; + initCenter(modelCenter, ¢er); + + // Get source rotation and scale + // getRotation() returns (w,x,y,z) format, convert to (x,y,z,w) for quatMul + let srcRotation = getRotation().yzwx; + let srcScale = getScale(); + + // Combine: world = model * source (both in x,y,z,w format) + var worldRotation = vec4f(quatMul(half4(uniform.model_rotation), half4(srcRotation))); + // Ensure w is positive so sqrt() reconstruction works correctly + // (quaternions q and -q represent the same rotation) + if (worldRotation.w < 0.0) { + worldRotation = -worldRotation; + } + var worldScale = uniform.model_scale * srcScale; - } else { + // Apply custom center modification + let originalCenter = worldCenter; + modifySplatCenter(&worldCenter); - #ifdef GSPLAT_LOD - // Compute source index from packed sub-draw varying: (sourceBase, colStart, rowWidth, rowStart) - let localRow = i32(input.position.y) - uniform.uStartLine - input.vSubDraw.w; - let localCol = i32(input.position.x) - input.vSubDraw.y; - let originalIndex = u32(input.vSubDraw.x + localRow * input.vSubDraw.z + localCol); - #else - let originalIndex = targetIndex; - #endif - - // Initialize global splat for format read functions - setSplat(u32(originalIndex)); - - // read center in local space - var modelCenter = getCenter(); - - // compute world-space center for storage - var worldCenter = (uniform.matrix_model * vec4f(modelCenter, 1.0)).xyz; - var center: SplatCenter; - initCenter(modelCenter, ¢er); - - // Get source rotation and scale - // getRotation() returns (w,x,y,z) format, convert to (x,y,z,w) for quatMul - let srcRotation = getRotation().yzwx; - let srcScale = getScale(); - - // Combine: world = model * source (both in x,y,z,w format) - var worldRotation = vec4f(quatMul(half4(uniform.model_rotation), half4(srcRotation))); - // Ensure w is positive so sqrt() reconstruction works correctly - // (quaternions q and -q represent the same rotation) - if (worldRotation.w < 0.0) { - worldRotation = -worldRotation; - } - var worldScale = uniform.model_scale * srcScale; - - // Apply custom center modification - let originalCenter = worldCenter; - modifySplatCenter(&worldCenter); - - // Apply custom rotation/scale modification - modifySplatRotationScale(originalCenter, worldCenter, &worldRotation, &worldScale); - - // read color - var color = getColor(); - - // evaluate spherical harmonics - #if SH_BANDS > 0 - // calculate the model-space view direction - let dir = normalize(center.view * mat3x3f(center.modelView[0].xyz, center.modelView[1].xyz, center.modelView[2].xyz)); - - // read sh coefficients - var sh: array; - var scale: f32; - readSHData(&sh, &scale); - - // evaluate - color = vec4f(color.xyz + vec3f(evalSH(&sh, dir) * half(scale)), color.w); - #endif + // Apply custom rotation/scale modification + modifySplatRotationScale(originalCenter, worldCenter, &worldRotation, &worldScale); - // Apply custom color modification - modifySplatColor(worldCenter, &color); + // read color + var color = getColor(); - color = vec4f(color.xyz * uniform.uColorMultiply, color.w); + // evaluate spherical harmonics + #if SH_BANDS > 0 + // calculate the model-space view direction + let dir = normalize(center.view * mat3x3f(center.modelView[0].xyz, center.modelView[1].xyz, center.modelView[2].xyz)); - // write out results using generated write functions - writeDataColor(color); - #ifndef GSPLAT_COLOR_ONLY - // Store rotation (xyz, w derived) and scale as 6 half-floats - writeDataTransformA(vec4u(bitcast(worldCenter.x), bitcast(worldCenter.y), bitcast(worldCenter.z), pack2x16float(worldRotation.xy))); - writeDataTransformB(vec4u(pack2x16float(vec2f(worldRotation.z, worldScale.x)), pack2x16float(worldScale.yz), 0u, 0u)); - #endif + // read sh coefficients + var sh: array; + var scale: f32; + readSHData(&sh, &scale); - #ifdef GSPLAT_ID - writePcId(vec4u(uniform.uId, 0u, 0u, 0u)); - #endif + // evaluate + color = vec4f(color.xyz + vec3f(evalSH(&sh, dir) * half(scale)), color.w); + #endif + + // Apply custom color modification + modifySplatColor(worldCenter, &color); + + color = vec4f(color.xyz * uniform.uColorMultiply, color.w); + + // write color + transform using format-specific encoding + writeSplat(worldCenter, worldRotation, worldScale, color); + + #ifdef GSPLAT_ID + writePcId(vec4u(uniform.uId, 0u, 0u, 0u)); + #endif - #ifdef GSPLAT_NODE_INDEX - #ifdef HAS_NODE_MAPPING - // Octree resource: look up node index from source splat, then local bounds index - let srcTextureWidth = i32(textureDimensions(nodeMappingTexture, 0).x); - let sourceCoord = vec2i(i32(originalIndex) % srcTextureWidth, i32(originalIndex) / srcTextureWidth); - let nodeIndex = textureLoad(nodeMappingTexture, sourceCoord, 0).r; - let ntlCoord = vec2i(i32(nodeIndex) % uniform.nodeToLocalBoundsWidth, i32(nodeIndex) / uniform.nodeToLocalBoundsWidth); - let localBoundsIdx = textureLoad(nodeToLocalBoundsTexture, ntlCoord, 0).r; - writePcNodeIndex(vec4u(uniform.uBoundsBaseIndex + localBoundsIdx, 0u, 0u, 0u)); - #else - // Non-octree resource: single bounds entry - writePcNodeIndex(vec4u(uniform.uBoundsBaseIndex, 0u, 0u, 0u)); - #endif + #ifdef GSPLAT_NODE_INDEX + #ifdef HAS_NODE_MAPPING + // Octree resource: look up node index from source splat, then local bounds index + let srcTextureWidth = i32(textureDimensions(nodeMappingTexture, 0).x); + let sourceCoord = vec2i(i32(originalIndex) % srcTextureWidth, i32(originalIndex) / srcTextureWidth); + let nodeIndex = textureLoad(nodeMappingTexture, sourceCoord, 0).r; + let ntlCoord = vec2i(i32(nodeIndex) % uniform.nodeToLocalBoundsWidth, i32(nodeIndex) / uniform.nodeToLocalBoundsWidth); + let localBoundsIdx = textureLoad(nodeToLocalBoundsTexture, ntlCoord, 0).r; + writePcNodeIndex(vec4u(uniform.uBoundsBaseIndex + localBoundsIdx, 0u, 0u, 0u)); + #else + // Non-octree resource: single bounds entry + writePcNodeIndex(vec4u(uniform.uBoundsBaseIndex, 0u, 0u, 0u)); #endif - } - + #endif + return processOutput; } `; diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/formats/containerCompactRead.js b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/formats/containerCompactRead.js new file mode 100644 index 00000000000..cf2fe805be6 --- /dev/null +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/formats/containerCompactRead.js @@ -0,0 +1,54 @@ +// Read functions for compact work buffer format (20 bytes/splat): +// - dataColor: R32U (4B): RGB color (11+11+10 bits, range [0, 4]) +// - dataTransformA: RGBA32U (16B): center.xyz as f32 + half-angle quaternion (11+11+10 bits) +// - dataTransformB: R32U (4B): scale.xyz as 3x8-bit log-encoded + alpha (8 bits) +export default /* wgsl */` +var cachedTransformA: vec4u; +var cachedTransformB: u32; + +fn getCenter() -> vec3f { + cachedTransformA = loadDataTransformA(); + cachedTransformB = loadDataTransformB().x; + return vec3f(bitcast(cachedTransformA.r), bitcast(cachedTransformA.g), bitcast(cachedTransformA.b)); +} + +fn getColor() -> vec4f { + let packed = loadDataColor().x; + let r = f32(packed & 0x7FFu) * (4.0 / 2047.0); + let g = f32((packed >> 11u) & 0x7FFu) * (4.0 / 2047.0); + let b = f32((packed >> 22u) & 0x3FFu) * (4.0 / 1023.0); + let a = f32(cachedTransformB >> 24u) / 255.0; + return vec4f(r, g, b, a); +} + +fn getRotation() -> vec4f { + let packed = cachedTransformA.a; + + // dequantize half-angle projected quaternion: 11+11+10 bits to [-1, 1] + let p = vec3f( + f32(packed & 0x7FFu) / 2047.0 * 2.0 - 1.0, + f32((packed >> 11u) & 0x7FFu) / 2047.0 * 2.0 - 1.0, + f32((packed >> 22u) & 0x3FFu) / 1023.0 * 2.0 - 1.0 + ); + + // inverse half-angle transform, returns (w, x, y, z) format + let d = dot(p, p); + return vec4f(1.0 - d, sqrt(max(0.0, 2.0 - d)) * p); +} + +fn getScale() -> vec3f { + let packed = cachedTransformB; + let sx = f32(packed & 0xFFu); + let sy = f32((packed >> 8u) & 0xFFu); + let sz = f32((packed >> 16u) & 0xFFu); + + // decode log-encoded scale: 0 = true zero, 1-255 maps linearly in log-space to e^-12..e^9 + let logRange = 21.0 / 255.0; + let logMin = -12.0; + return vec3f( + select(exp(sx * logRange + logMin), 0.0, sx == 0.0), + select(exp(sy * logRange + logMin), 0.0, sy == 0.0), + select(exp(sz * logRange + logMin), 0.0, sz == 0.0) + ); +} +`; diff --git a/src/scene/shader-lib/wgsl/collections/gsplat-chunks-wgsl.js b/src/scene/shader-lib/wgsl/collections/gsplat-chunks-wgsl.js index 7ddc6f81dff..8b168c4dd24 100644 --- a/src/scene/shader-lib/wgsl/collections/gsplat-chunks-wgsl.js +++ b/src/scene/shader-lib/wgsl/collections/gsplat-chunks-wgsl.js @@ -23,7 +23,6 @@ import gsplatSogVS from '../chunks/gsplat/vert/formats/sog.js'; import gsplatSogSHVS from '../chunks/gsplat/vert/formats/sogSH.js'; import gsplatContainerDeclVS from '../chunks/gsplat/vert/formats/containerDecl.js'; import gsplatContainerReadVS from '../chunks/gsplat/vert/formats/containerRead.js'; -import gsplatContainerPackedReadVS from '../chunks/gsplat/vert/formats/containerPackedRead.js'; import gsplatContainerFloatReadVS from '../chunks/gsplat/vert/formats/containerFloatRead.js'; export const gsplatChunksWGSL = { @@ -50,6 +49,5 @@ export const gsplatChunksWGSL = { gsplatSogSHVS, gsplatContainerDeclVS, gsplatContainerReadVS, - gsplatContainerPackedReadVS, gsplatContainerFloatReadVS }; From 6467ee2efc54c76c93441aa11226b32229aded80 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Wed, 25 Feb 2026 12:04:36 +0000 Subject: [PATCH 2/3] update --- src/scene/gsplat-unified/gsplat-params.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scene/gsplat-unified/gsplat-params.js b/src/scene/gsplat-unified/gsplat-params.js index 85dedce2017..66bc52138c8 100644 --- a/src/scene/gsplat-unified/gsplat-params.js +++ b/src/scene/gsplat-unified/gsplat-params.js @@ -74,6 +74,7 @@ class GSplatParams { // Compact work buffer format (20 bytes/splat): // - dataColor (R32U): RGB color (11+11+10 bits, range [0, 4]) // - dataTransformA (RGBA32U): center.xyz (3×32-bit floats) + half-angle quaternion (11+11+10 bits) + // See: https://marc-b-reynolds.github.io/quaternions/2017/05/02/QuatQuantPart1.html // - dataTransformB (R32U): scale.xyz (3×8-bit log-encoded, e^-12..e^9) + alpha (8 bits) format = new GSplatFormat(this._device, [ { name: 'dataColor', format: PIXELFORMAT_R32U }, From 289601edc0b6ced1853ea9dd335aae1e08b30e35 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Wed, 25 Feb 2026 12:25:05 +0000 Subject: [PATCH 3/3] comment update --- src/scene/gsplat-unified/gsplat-params.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/scene/gsplat-unified/gsplat-params.js b/src/scene/gsplat-unified/gsplat-params.js index 66bc52138c8..ca822310a75 100644 --- a/src/scene/gsplat-unified/gsplat-params.js +++ b/src/scene/gsplat-unified/gsplat-params.js @@ -509,10 +509,9 @@ class GSplatParams { cooldownTicks = 100; /** - * Work buffer data format. Controls the precision and bandwidth of the intermediate work - * buffer used during unified GSplat rendering. Can be set to {@link GSPLATDATA_LARGE} - * (default, 32 bytes/splat, full precision) or {@link GSPLATDATA_COMPACT} (20 bytes/splat, - * reduced precision optimized for mobile). Changing this recreates the work buffer. + * Work buffer data format. Controls the precision and bandwidth of the intermediate work buffer + * used during unified GSplat rendering. Can be set to {@link GSPLATDATA_COMPACT} (20 bytes/splat) + * or {@link GSPLATDATA_LARGE} (32 bytes/splat). Defaults to {@link GSPLATDATA_COMPACT}. * * @type {string} */