Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
9 changes: 9 additions & 0 deletions examples/src/examples/gaussian-splatting/viewer.controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
3 changes: 3 additions & 0 deletions examples/src/examples/gaussian-splatting/viewer.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ assetListLoader.load(() => {
// Initialize data values
data.set('data', {
skydome: false,
compact: true,
orientation: 180,
tonemapping: pc.TONEMAP_LINEAR,
grading: {
Expand Down Expand Up @@ -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) {
Expand Down
18 changes: 18 additions & 0 deletions src/scene/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
14 changes: 6 additions & 8 deletions src/scene/gsplat-unified/gsplat-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,6 @@ class GSplatInfo {
/** @type {number} */
lineCount = 0;

/** @type {number} */
padding = 0;

/** @type {Vec4} */
viewport = new Vec4();

Expand Down Expand Up @@ -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);
}

/**
Expand Down
24 changes: 24 additions & 0 deletions src/scene/gsplat-unified/gsplat-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
126 changes: 109 additions & 17 deletions src/scene/gsplat-unified/gsplat-params.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -29,28 +40,70 @@ 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)
// 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 },
{ 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;
}

/**
Expand Down Expand Up @@ -455,6 +508,45 @@ 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_COMPACT} (20 bytes/splat)
* or {@link GSPLATDATA_LARGE} (32 bytes/splat). Defaults to {@link GSPLATDATA_COMPACT}.
*
* @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
Expand Down
5 changes: 1 addition & 4 deletions src/scene/gsplat-unified/gsplat-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'}`, '');
Expand Down
21 changes: 6 additions & 15 deletions src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Loading