diff --git a/src/scene/gsplat-unified/gsplat-renderer.js b/src/scene/gsplat-unified/gsplat-renderer.js index b502d96745c..22a9eac6e86 100644 --- a/src/scene/gsplat-unified/gsplat-renderer.js +++ b/src/scene/gsplat-unified/gsplat-renderer.js @@ -1,4 +1,4 @@ -import { SEMANTIC_POSITION, SEMANTIC_ATTR13, CULLFACE_NONE } from '../../platform/graphics/constants.js'; +import { SEMANTIC_POSITION, SEMANTIC_ATTR13, CULLFACE_NONE, PIXELFORMAT_RGBA16U } from '../../platform/graphics/constants.js'; import { BLEND_NONE, BLEND_PREMULTIPLIED, BLEND_ADDITIVE } from '../constants.js'; import { ShaderMaterial } from '../materials/shader-material.js'; import { GSplatResourceBase } from '../gsplat/gsplat-resource-base.js'; @@ -83,6 +83,10 @@ class GSplatRenderer { this._material.setDefine('GSPLAT_WORKBUFFER_DATA', true); this._material.setDefine('STORAGE_ORDER', device.isWebGPU); + // Check if using RGBA16U format (fallback for when RGBA16F not supported) + const isColorUint = workBuffer.colorTextureFormat === PIXELFORMAT_RGBA16U; + this._material.setDefine('GSPLAT_COLOR_UINT', isColorUint); + // input textures (work buffer textures) this._material.setParameter('splatColor', workBuffer.colorTexture); this._material.setParameter('splatTexture0', workBuffer.splatTexture0); 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 4517ff6b174..dc0968822e7 100644 --- a/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js +++ b/src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js @@ -10,6 +10,7 @@ import { CULLFACE_NONE } from '../../platform/graphics/constants.js'; * @import { GSplatInfo } from './gsplat-info.js' * @import { GraphNode } from '../graph-node.js' * @import { RenderTarget } from '../../platform/graphics/render-target.js' + * @import { GSplatWorkBuffer } from './gsplat-work-buffer.js' */ const _viewMat = new Mat4(); @@ -39,6 +40,14 @@ class GSplatWorkBufferRenderPass extends RenderPass { */ cameraNode = /** @type {any} */ (null); + /** @type {GSplatWorkBuffer} */ + workBuffer; + + constructor(device, workBuffer) { + super(device); + this.workBuffer = workBuffer; + } + /** * Initialize the render pass with the specified render target. * @@ -111,7 +120,7 @@ class GSplatWorkBufferRenderPass extends RenderPass { const { intervals, activeSplats, lineStart, viewport, intervalTexture } = splatInfo; // quad renderer and material are cached in the resource - const workBufferRenderInfo = resource.getWorkBufferRenderInfo(intervals.length > 0); + const workBufferRenderInfo = resource.getWorkBufferRenderInfo(intervals.length > 0, this.workBuffer.colorTextureFormat); // Assign material properties to scope workBufferRenderInfo.material.setParameters(device); diff --git a/src/scene/gsplat-unified/gsplat-work-buffer.js b/src/scene/gsplat-unified/gsplat-work-buffer.js index d7e275cf508..30c3ea6e38f 100644 --- a/src/scene/gsplat-unified/gsplat-work-buffer.js +++ b/src/scene/gsplat-unified/gsplat-work-buffer.js @@ -1,5 +1,5 @@ import { Debug } from '../../core/debug.js'; -import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_R32U, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32U, PIXELFORMAT_RG32U, BUFFERUSAGE_COPY_DST, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; +import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_R32U, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA16U, PIXELFORMAT_RGBA32U, PIXELFORMAT_RG32U, BUFFERUSAGE_COPY_DST, SEMANTIC_POSITION } from '../../platform/graphics/constants.js'; import { RenderTarget } from '../../platform/graphics/render-target.js'; import { StorageBuffer } from '../../platform/graphics/storage-buffer.js'; import { Texture } from '../../platform/graphics/texture.js'; @@ -31,11 +31,18 @@ class WorkBufferRenderInfo { /** @type {QuadRender} */ quadRender; - constructor(device, key, material) { + constructor(device, key, material, colorTextureFormat) { this.device = device; this.material = material; const clonedDefines = new Map(material.defines); + + // when using fallback RGBA16U format + const isColorUint = colorTextureFormat === PIXELFORMAT_RGBA16U; + if (isColorUint) { + clonedDefines.set('GSPLAT_COLOR_UINT', ''); + } + const shader = ShaderUtils.createShader(this.device, { uniqueName: `SplatCopyToWorkBuffer:${key}`, attributes: { vertex_position: SEMANTIC_POSITION }, @@ -44,7 +51,7 @@ class WorkBufferRenderInfo { vertexChunk: 'fullscreenQuadVS', fragmentGLSL: glslGsplatCopyToWorkBufferPS, fragmentWGSL: wgslGsplatCopyToWorkBufferPS, - fragmentOutputTypes: ['vec4', 'uvec4', 'uvec2'] + fragmentOutputTypes: [isColorUint ? 'uvec4' : 'vec4', 'uvec4', 'uvec2'] }); this.quadRender = new QuadRender(shader); @@ -66,6 +73,9 @@ class GSplatWorkBuffer { /** @type {number} */ id = id++; + /** @type {number} */ + colorTextureFormat; + /** @type {Texture} */ colorTexture; @@ -96,7 +106,10 @@ class GSplatWorkBuffer { constructor(device) { this.device = device; - this.colorTexture = this.createTexture('splatColor', PIXELFORMAT_RGBA16F, 1, 1); + // Detect compatible HDR format for color texture, fallback to RGBA16U if RGBA16F not supported + this.colorTextureFormat = device.getRenderableHdrFormat([PIXELFORMAT_RGBA16F]) || PIXELFORMAT_RGBA16U; + + this.colorTexture = this.createTexture('splatColor', this.colorTextureFormat, 1, 1); this.splatTexture0 = this.createTexture('splatTexture0', PIXELFORMAT_RGBA32U, 1, 1); this.splatTexture1 = this.createTexture('splatTexture1', PIXELFORMAT_RG32U, 1, 1); @@ -118,7 +131,7 @@ class GSplatWorkBuffer { } // Create the optimized render pass for batched splat rendering - this.renderPass = new GSplatWorkBufferRenderPass(device); + this.renderPass = new GSplatWorkBufferRenderPass(device, this); this.renderPass.init(this.renderTarget); } diff --git a/src/scene/gsplat/gsplat-resource-base.js b/src/scene/gsplat/gsplat-resource-base.js index f671c12f7fc..76e678dde7f 100644 --- a/src/scene/gsplat/gsplat-resource-base.js +++ b/src/scene/gsplat/gsplat-resource-base.js @@ -79,9 +79,10 @@ class GSplatResourceBase { * Get or create a QuadRender for rendering to work buffer. * * @param {boolean} useIntervals - Whether to use intervals. + * @param {number} colorTextureFormat - The format of the color texture (RGBA16F or RGBA16U). * @returns {WorkBufferRenderInfo} The WorkBufferRenderInfo instance. */ - getWorkBufferRenderInfo(useIntervals) { + getWorkBufferRenderInfo(useIntervals, colorTextureFormat) { // configure defines to fetch cached data this.configureMaterialDefines(tempMap); @@ -99,7 +100,7 @@ class GSplatResourceBase { tempMap.forEach((v, k) => material.setDefine(k, v)); // create new cache entry - info = new WorkBufferRenderInfo(this.device, key, material); + info = new WorkBufferRenderInfo(this.device, key, material, colorTextureFormat); this.workBufferRenderInfos.set(key, info); } 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 8b171ca944c..2a865053f0d 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js @@ -33,7 +33,11 @@ void main(void) { if (targetIndex >= uActiveSplats) { // Out of bounds: write zeros - pcFragColor0 = vec4(0.0); + #ifdef GSPLAT_COLOR_UINT + pcFragColor0 = uvec4(0u); + #else + pcFragColor0 = vec4(0.0); + #endif pcFragColor1 = uvec4(0u); pcFragColor2 = uvec2(0u); @@ -100,7 +104,19 @@ void main(void) { color.xyz *= uColorMultiply; // write out results - pcFragColor0 = color; + #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); + pcFragColor0 = 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 + pcFragColor0 = color; + #endif pcFragColor1 = uvec4(floatBitsToUint(modelCenter.x), floatBitsToUint(modelCenter.y), floatBitsToUint(modelCenter.z), packHalf2x16(vec2(covA.z, covB.z))); pcFragColor2 = uvec2(packHalf2x16(covA.xy), packHalf2x16(covB.xy)); } diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatWorkBuffer.js b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatWorkBuffer.js index c13c22fc690..f742e9034d0 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatWorkBuffer.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatWorkBuffer.js @@ -1,7 +1,12 @@ export default /* glsl */` uniform highp usampler2D splatTexture0; uniform highp usampler2D splatTexture1; -uniform mediump sampler2D splatColor; + +#ifdef GSPLAT_COLOR_UINT + uniform highp usampler2D splatColor; +#else + uniform mediump sampler2D splatColor; +#endif // cached texture fetches uvec4 cachedSplatTexture0Data; @@ -28,6 +33,14 @@ void readCovariance(in SplatSource source, out vec3 cov_A, out vec3 cov_B) { } vec4 readColor(in SplatSource source) { - return texelFetch(splatColor, source.uv, 0); + #ifdef GSPLAT_COLOR_UINT + // Unpack RGBA from 4x half-float (16-bit) values stored in RGBA16U format + uvec4 packed = texelFetch(splatColor, source.uv, 0); + uint packed_rg = packed.r | (packed.g << 16u); + uint packed_ba = packed.b | (packed.a << 16u); + return vec4(unpackHalf2x16(packed_rg), unpackHalf2x16(packed_ba)); + #else + return texelFetch(splatColor, source.uv, 0); + #endif } `;