Skip to content
Merged
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
27 changes: 16 additions & 11 deletions examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
const { BindingTwoWay, LabelGroup, BooleanInput, Panel, SelectInput, SliderInput, Label } = ReactPCUI;
const isWebGPU = observer.get('isWebGPU');
return fragment(
jsx(
Panel,
Expand Down Expand Up @@ -65,6 +64,22 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
jsx(
Panel,
{ headerText: 'Settings' },
jsx(
LabelGroup,
{ text: 'Renderer' },
jsx(SelectInput, {
type: 'number',
binding: new BindingTwoWay(),
link: { observer, path: 'renderer' },
value: observer.get('renderer') ?? 0,
options: [
{ v: 0, t: 'Auto' },
{ v: 1, t: 'Raster (CPU Sort)' },
{ v: 2, t: 'Raster (GPU Sort)' },
{ v: 3, t: 'Compute' }
]
})
),
jsx(
LabelGroup,
{ text: 'Min Pixel Size' },
Expand All @@ -86,16 +101,6 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
value: observer.get('radialSorting') ?? true
})
),
isWebGPU && jsx(
LabelGroup,
{ text: 'GPU Sorting' },
jsx(BooleanInput, {
type: 'toggle',
binding: new BindingTwoWay(),
link: { observer, path: 'gpuSorting' },
value: observer.get('gpuSorting') || false
})
),
jsx(
LabelGroup,
{ text: 'Compact' },
Expand Down
18 changes: 8 additions & 10 deletions examples/src/examples/gaussian-splatting/lod-streaming.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,6 @@ const assets = {
)
};

// Set device type flag before controls render (controls read this synchronously)
data.set('isWebGPU', device.isWebGPU);

const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
assetListLoader.load(() => {
app.start();
Expand All @@ -133,12 +130,13 @@ assetListLoader.load(() => {
app.scene.gsplat.lodUpdateDistance = config.lodUpdateDistance;
app.scene.gsplat.lodUnderfillLimit = config.lodUnderfillLimit;

// GPU sorting is a WebGPU-only feature
if (device.isWebGPU) {
data.on('gpuSorting:set', () => {
app.scene.gsplat.gpuSorting = !!data.get('gpuSorting');
});
}
data.on('renderer:set', () => {
app.scene.gsplat.renderer = data.get('renderer');
const current = app.scene.gsplat.currentRenderer;
if (current !== data.get('renderer')) {
data.set('renderer', current);
}
});
data.on('minPixelSize:set', () => {
app.scene.gsplat.minPixelSize = data.get('minPixelSize');
});
Expand All @@ -155,7 +153,7 @@ assetListLoader.load(() => {
data.set('exposure', 1.5);
data.set('minPixelSize', 2);
data.set('radialSorting', true);
data.set('gpuSorting', false);
data.set('renderer', pc.GSPLAT_RENDERER_AUTO);
data.set('culling', device.isWebGPU);
data.set('compact', true);
data.set('debugLod', false);
Expand Down
4 changes: 2 additions & 2 deletions examples/src/examples/gaussian-splatting/world.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ const assets = {
const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
assetListLoader.load(() => {

// Enable GPU sorting (desktop only for now)
// Use GPU sorting on desktop (experimental, WebGPU only)
if (!pc.platform.mobile) {
app.scene.gsplat.gpuSorting = true;
app.scene.gsplat.renderer = pc.GSPLAT_RENDERER_RASTER_GPU_SORT;
}

app.start();
Expand Down
33 changes: 33 additions & 0 deletions src/scene/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -1213,3 +1213,36 @@ export const GSPLATDATA_LARGE = 'large';
* @category Graphics
*/
export const GSPLATDATA_COMPACT = 'compact';

/**
* Automatically selects the best rendering pipeline for the current platform.
*
* @type {number}
* @category Graphics
*/
export const GSPLAT_RENDERER_AUTO = 0;

/**
* Rasterization-based rendering with CPU-side sorting.
*
* @type {number}
* @category Graphics
*/
export const GSPLAT_RENDERER_RASTER_CPU_SORT = 1;

/**
* Rasterization-based rendering with compute shader sorting. WebGPU only. Experimental with
* limited functionality.
*
* @type {number}
* @category Graphics
*/
export const GSPLAT_RENDERER_RASTER_GPU_SORT = 2;

/**
* Full compute pipeline for rendering. WebGPU only. Experimental with limited functionality.
*
* @type {number}
* @category Graphics
*/
export const GSPLAT_RENDERER_COMPUTE = 3;
100 changes: 49 additions & 51 deletions src/scene/gsplat-unified/gsplat-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { GSplatIntervalCompaction } from './gsplat-interval-compaction.js';
import { ComputeRadixSort } from '../graphics/compute-radix-sort.js';
import { Debug } from '../../core/debug.js';
import { BoundingBox } from '../../core/shape/bounding-box.js';
import {
GSPLAT_RENDERER_RASTER_CPU_SORT, GSPLAT_RENDERER_RASTER_GPU_SORT, GSPLAT_RENDERER_COMPUTE
} from '../constants.js';
import { Color } from '../../core/math/color.js';
import { GSplatBudgetBalancer } from './gsplat-budget-balancer.js';
import { BlockAllocator } from '../../core/block-allocator.js';
Expand All @@ -32,8 +35,6 @@ import { BlockAllocator } from '../../core/block-allocator.js';
* @import { GSplatRenderer } from './gsplat-renderer.js'
*/

const USE_LOCAL_COMPUTE_RENDERER = false;

const cameraPosition = new Vec3();
const cameraDirection = new Vec3();
const translation = new Vec3();
Expand Down Expand Up @@ -131,19 +132,12 @@ class GSplatManager {
lastWorldStateVersion = 0;

/**
* Whether to use GPU-based sorting (WebGPU only). Starts as undefined so the first
* prepareSortMode() call always creates the appropriate resources.
* The currently active renderer mode. Starts as undefined so the first
* prepareRendererMode() call always creates the appropriate resources.
*
* @type {boolean|undefined}
*/
useGpuSorting;

/**
* Whether the local compute renderer is active (compaction-only, no sorting).
*
* @type {boolean}
* @type {number|undefined}
*/
useLocalRenderer = false;
activeRenderer;

/**
* CPU-based sorter (when not using GPU sorting).
Expand Down Expand Up @@ -384,20 +378,10 @@ class GSplatManager {
this._allocator = new BlockAllocator(budget > 0 ? Math.ceil(budget * allocatorGrowMultiplier) : 0, allocatorGrowMultiplier);

this.workBuffer = new GSplatWorkBuffer(device, this.scene.gsplat.format);
if (USE_LOCAL_COMPUTE_RENDERER && device.isWebGPU) {
this.renderer = new GSplatComputeLocalRenderer(device, this.node, this.cameraNode, layer, this.workBuffer);
this.useLocalRenderer = true;
this.useGpuSorting = true;
} else {
this.renderer = new GSplatQuadRenderer(device, this.node, this.cameraNode, layer, this.workBuffer);
this.useLocalRenderer = false;
}
this._workBufferFormatVersion = this.workBuffer.format.extraStreamsVersion;

// Local renderer handles its own compaction; skip full sort initialization.
if (!this.useLocalRenderer) {
this.prepareSortMode();
}
this.layer = layer;
this._createRenderer(this.scene.gsplat.currentRenderer);
this._workBufferFormatVersion = this.workBuffer.format.extraStreamsVersion;
}

destroy() {
Expand Down Expand Up @@ -525,7 +509,7 @@ class GSplatManager {
* @returns {import('../mesh-instance.js').MeshInstance|null} The pick mesh instance, or null.
*/
prepareForPicking(camera, width, height) {
if (!this.useLocalRenderer) return null;
if (this.activeRenderer !== GSPLAT_RENDERER_COMPUTE) return null;

/** @type {GSplatComputeLocalRenderer} */
const localRenderer = /** @type {any} */ (this.renderer);
Expand Down Expand Up @@ -564,31 +548,48 @@ class GSplatManager {
* @private
*/
get canCull() {
return (this.useLocalRenderer || !!this.useGpuSorting) &&
return this.activeRenderer !== GSPLAT_RENDERER_RASTER_CPU_SORT &&
this.workBuffer.frustumCuller.totalBoundsEntries > 0;
}

/**
* Prepares the sorting mode by matching it to the current gpuSorting setting. Lazily creates
* GPU or CPU sorting resources as needed and handles transitions between modes.
* Creates the renderer and sort resources for the given mode. Used at init time.
*
* @param {number} mode - The GSPLAT_RENDERER_* constant.
* @private
*/
prepareSortMode() {
const gpuSorting = this.device.isWebGPU && this.scene.gsplat.gpuSorting;
if (gpuSorting !== this.useGpuSorting) {

if (gpuSorting) {
this.destroyCpuSorting();
_createRenderer(mode) {
if (mode === GSPLAT_RENDERER_COMPUTE) {
this.renderer = new GSplatComputeLocalRenderer(this.device, this.node, this.cameraNode, this.layer, this.workBuffer);
} else {
this.renderer = new GSplatQuadRenderer(this.device, this.node, this.cameraNode, this.layer, this.workBuffer);
if (mode === GSPLAT_RENDERER_RASTER_GPU_SORT) {
this.initGpuSorting();
} else {
this.destroyGpuSorting();
this.initCpuSorting();
}

this.useGpuSorting = gpuSorting;
this.sortNeeded = true;
}
this.activeRenderer = mode;
}

/**
* Checks whether the resolved renderer mode has changed and transitions to the new mode.
* Handles both sort-mode transitions (CPU <-> GPU sort) and full renderer swaps
* (quad <-> compute).
*
* @private
*/
prepareRendererMode() {
const requested = this.scene.gsplat.currentRenderer;
if (requested === this.activeRenderer) return;

this.destroyGpuSorting();
this.destroyCpuSorting();
this.renderer.destroy();
this._createRenderer(requested);
this.renderer.setRenderMode(this.renderMode);
this._workBufferRebuildRequired = true;
this.sortNeeded = true;
}

/**
Expand Down Expand Up @@ -822,7 +823,7 @@ class GSplatManager {

// Bounds and transforms storage buffers are needed for frustum culling,
// which only runs with interval compaction (local renderer or GPU sorting).
if (this.useLocalRenderer || this.useGpuSorting) {
if (this.activeRenderer !== GSPLAT_RENDERER_RASTER_CPU_SORT) {
this.workBuffer.frustumCuller.updateBoundsData(worldState.boundsGroups);
this.workBuffer.frustumCuller.updateTransformsData(worldState.boundsGroups);
}
Expand Down Expand Up @@ -1293,19 +1294,16 @@ class GSplatManager {
this.sortNeeded = true;
}

// Prepare sorting mode for current gpuSorting setting (may switch GPU <-> CPU).
// Skipped for the local renderer — it handles its own compaction without sorting.
if (!this.useLocalRenderer) {
this.prepareSortMode();
}
// Check for runtime renderer mode changes and transition if needed.
this.prepareRendererMode();

// apply any pending sorted results (CPU path only)
if (this.cpuSorter) {
this.cpuSorter.applyPendingSorted();
}

// GPU sorting is always ready, CPU sorting is ready if not too many jobs in flight
const sorterAvailable = this.useLocalRenderer || this.useGpuSorting || (this.cpuSorter && this.cpuSorter.jobsInFlight < 3);
const sorterAvailable = this.activeRenderer !== GSPLAT_RENDERER_RASTER_CPU_SORT || (this.cpuSorter && this.cpuSorter.jobsInFlight < 3);

// full update every 10 frames
let fullUpdate = false;
Expand Down Expand Up @@ -1478,11 +1476,11 @@ class GSplatManager {
// kick off sorting / compaction only if needed
let gpuSortedThisFrame = false;
if (this.sortNeeded && lastState) {
if (this.useLocalRenderer) {
// Local renderer: run compaction only (no key generation or radix sort)
if (this.activeRenderer === GSPLAT_RENDERER_COMPUTE) {
// Compute renderer: run compaction only (no key generation or radix sort)
this.compactGpu(lastState);
gpuSortedThisFrame = true;
} else if (this.useGpuSorting) {
} else if (this.activeRenderer === GSPLAT_RENDERER_RASTER_GPU_SORT) {
// GPU sort runs compaction internally, so indirect draw is always valid
this.sortGpu(lastState);
gpuSortedThisFrame = true;
Expand All @@ -1503,7 +1501,7 @@ class GSplatManager {

// Refresh the per-frame indirect draw slot on non-sort frames
// (sortGpu already handled GPU-sort frames).
if (!this.useLocalRenderer && this.intervalCompaction && !gpuSortedThisFrame) {
if (this.activeRenderer !== GSPLAT_RENDERER_COMPUTE && this.intervalCompaction && !gpuSortedThisFrame) {
this.refreshIndirectDraw();
}

Expand Down
Loading