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
62 changes: 44 additions & 18 deletions src/scene/gsplat-unified/gsplat-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '../constants.js';
import { ShaderMaterial } from '../materials/shader-material.js';
import { GSplatResourceBase } from '../gsplat/gsplat-resource-base.js';
import { Mesh } from '../mesh.js';
import { MeshInstance } from '../mesh-instance.js';
import { math } from '../../core/math/math.js';

Expand Down Expand Up @@ -228,8 +229,12 @@ class GSplatRenderer {

update(count, textureSize) {

// limit splat render count to exclude those behind the camera
this.meshInstance.instancingCount = Math.ceil(count / GSplatResourceBase.instanceSize);
// On WebGL each instance is one quad; on WebGPU each instance is instanceSize quads
if (this.device.isWebGPU) {
this.meshInstance.instancingCount = Math.ceil(count / GSplatResourceBase.instanceSize);
} else {
this.meshInstance.instancingCount = count;
}

// update splat count on the material
this._material.setParameter('numSplats', count);
Expand Down Expand Up @@ -291,8 +296,9 @@ class GSplatRenderer {
// Set the appropriate order data resource based on device type
if (this.device.isWebGPU) {
this._material.setParameter('splatOrder', this.workBuffer.orderBuffer);
} else {
this._material.setParameter('splatOrder', this.workBuffer.orderTexture);
} else if (this.meshInstance?.instancingData) {
// Update the VB reference without recreating InstancingData (which would reset instancingCount)
this.meshInstance.instancingData.vertexBuffer = this.workBuffer.orderVB ?? null;
}
}

Expand Down Expand Up @@ -422,32 +428,52 @@ class GSplatRenderer {

setMaxNumSplats(numSplats) {

// round up to the nearest multiple of instanceSize (same as createInstanceIndices does internally)
const roundedNumSplats = math.roundUp(numSplats, GSplatResourceBase.instanceSize);
if (this.device.isWebGPU) {
// round up to the nearest multiple of instanceSize (same as createInstanceIndices does internally)
const roundedNumSplats = math.roundUp(numSplats, GSplatResourceBase.instanceSize);

if (this.instanceIndicesCount < roundedNumSplats) {
this.instanceIndicesCount = roundedNumSplats;
if (this.instanceIndicesCount < roundedNumSplats) {
this.instanceIndicesCount = roundedNumSplats;

// destroy old instance indices
this.instanceIndices?.destroy();
// destroy old instance indices
this.instanceIndices?.destroy();

// create new instance indices
this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, numSplats);
this.meshInstance.setInstancing(this.instanceIndices, true);
// create new instance indices
this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, numSplats);
this.meshInstance.setInstancing(this.instanceIndices, true);

// update texture size uniform
// update texture size uniform
this._material.setParameter('splatTextureSize', this.workBuffer.textureSize);
}
} else {
// On WebGL the orderVB is the instance buffer, managed by workBuffer.resize()
this._material.setParameter('splatTextureSize', this.workBuffer.textureSize);
}
}

createMeshInstance() {

const mesh = GSplatResourceBase.createMesh(this.device);
const textureSize = this.workBuffer.textureSize;
const instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, textureSize * textureSize);
let mesh;
let instanceVB;

if (this.device.isWebGPU) {
mesh = GSplatResourceBase.createMesh(this.device);
const textureSize = this.workBuffer.textureSize;
instanceVB = GSplatResourceBase.createInstanceIndices(this.device, textureSize * textureSize);
} else {
// Single quad mesh — corners derived from gl_VertexID in the shader
mesh = new Mesh(this.device);
mesh.setPositions(new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]), 3);
mesh.setIndices(new Uint32Array([0, 1, 2, 0, 2, 3]));
mesh.update();

// Instance buffer carries sorted splatIds directly
instanceVB = this.workBuffer.orderVB;
}

const meshInstance = new MeshInstance(mesh, this._material);
meshInstance.node = this.node;
meshInstance.setInstancing(instanceIndices, true);
meshInstance.setInstancing(instanceVB, true);

// only start rendering the splat after we've received the splat order data
meshInstance.instancingCount = 0;
Expand Down
58 changes: 44 additions & 14 deletions src/scene/gsplat-unified/gsplat-work-buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { Frustum } from '../../core/shape/frustum.js';
import { Mat4 } from '../../core/math/mat4.js';
import { Vec2 } from '../../core/math/vec2.js';
import {
ADDRESS_CLAMP_TO_EDGE, PIXELFORMAT_R32U, PIXELFORMAT_RGBA16U, PIXELFORMAT_RGBA32F,
BUFFERUSAGE_COPY_DST, SEMANTIC_POSITION, getGlslShaderType
PIXELFORMAT_R32U, PIXELFORMAT_RGBA16U, PIXELFORMAT_RGBA32F,
BUFFERUSAGE_COPY_DST, SEMANTIC_POSITION, getGlslShaderType,
BUFFER_DYNAMIC, SEMANTIC_ATTR13, TYPE_UINT32
} 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';
import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js';
import { VertexFormat } from '../../platform/graphics/vertex-format.js';
import { TextureUtils } from '../../platform/graphics/texture-utils.js';
import { UploadStream } from '../../platform/graphics/upload-stream.js';
import { QuadRender } from '../graphics/quad-render.js';
Expand Down Expand Up @@ -170,6 +173,9 @@ class GSplatWorkBuffer {
/** @type {StorageBuffer|undefined} */
orderBuffer;

/** @type {VertexBuffer|undefined} */
orderVB;

/** @type {UploadStream} */
uploadStream;

Expand Down Expand Up @@ -254,19 +260,11 @@ class GSplatWorkBuffer {
// Create upload stream for non-blocking uploads
this.uploadStream = new UploadStream(device);

// Use storage buffer on WebGPU, texture on WebGL
// Use storage buffer on WebGPU, dynamic vertex buffer on WebGL
if (device.isWebGPU) {
this.orderBuffer = new StorageBuffer(device, 4, BUFFERUSAGE_COPY_DST);
} else {
this.orderTexture = new Texture(device, {
name: 'SplatGlobalOrder',
width: 1,
height: 1,
format: PIXELFORMAT_R32U,
mipmaps: false,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE
});
this.orderVB = this._createOrderVB(1);
}

// Create the optimized render pass for batched splat rendering
Expand Down Expand Up @@ -345,6 +343,7 @@ class GSplatWorkBuffer {
this.streams.destroy();
this.orderTexture?.destroy();
this.orderBuffer?.destroy();
this.orderVB?.destroy();
this.renderTarget?.destroy();
this.colorRenderTarget?.destroy();
this.uploadStream.destroy();
Expand All @@ -367,7 +366,17 @@ class GSplatWorkBuffer {
this.uploadStream.upload(data, this.orderBuffer, 0, data.length);
} else {
Debug.assert(data.length === size * size);
this.uploadStream.upload(data, this.orderTexture, 0, data.length);
const vb = this.orderVB;
const gl = this.device.gl;
if (!vb.impl.bufferId) {
vb.impl.bufferId = gl.createBuffer();
}
gl.bindBuffer(gl.ARRAY_BUFFER, vb.impl.bufferId);
if (!vb._glAllocated) {
gl.bufferData(gl.ARRAY_BUFFER, vb.numVertices * 4, gl.STREAM_DRAW);
vb._glAllocated = true;
}
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data);
}
}

Expand All @@ -387,10 +396,31 @@ class GSplatWorkBuffer {
this.orderBuffer = new StorageBuffer(this.device, newByteSize, BUFFERUSAGE_COPY_DST);
}
} else {
this.orderTexture.resize(textureSize, textureSize);
const newCount = textureSize * textureSize;
if (!this.orderVB || this.orderVB.numVertices < newCount) {
this.orderVB?.destroy();
this.orderVB = this._createOrderVB(newCount);
}
}
}

/**
* Creates a dynamic vertex buffer for order data (WebGL path).
*
* @param {number} count - Number of splat entries.
* @returns {VertexBuffer} The dynamic vertex buffer.
* @private
*/
_createOrderVB(count) {
const vertexFormat = new VertexFormat(this.device, [
{ semantic: SEMANTIC_ATTR13, components: 1, type: TYPE_UINT32, asInt: true }
]);
vertexFormat.instancing = true;
return new VertexBuffer(this.device, vertexFormat, count, {
usage: BUFFER_DYNAMIC
});
}

/**
* Render given splats to the work buffer.
*
Expand Down
6 changes: 6 additions & 0 deletions src/scene/gsplat-unified/gsplat-world-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ class GSplatWorldState {
const intervalOffsets = [];

if (numIntervals > 0 && allocIds.length === numIntervals) {
let missingBlocks = 0;
for (let j = 0; j < numIntervals; j++) {
this.allocIdToSplat.set(allocIds[j], splat);
const block = allocationMap.get(allocIds[j]);
Expand All @@ -302,8 +303,13 @@ class GSplatWorldState {
splatChanged = true;
this.needsUploadIds.add(allocIds[j]);
}
} else {
missingBlocks++;
}
}
if (missingBlocks > 0) {
console.warn(`assignSplatOffsets: ${missingBlocks}/${numIntervals} intervals missing from allocationMap (intervalOffsets.length=${intervalOffsets.length}, numIntervals=${numIntervals})`);
}
} else {
this.allocIdToSplat.set(splat.allocId, splat);
const block = allocationMap.get(splat.allocId);
Expand Down
25 changes: 10 additions & 15 deletions src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatSource.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
export default /* glsl */`
attribute vec3 vertex_position; // xy: cornerUV, z: render order offset
attribute uint vertex_id_attrib; // render order base
attribute vec3 vertex_position; // unused on WebGL (corners from gl_VertexID)
attribute uint vertex_id_attrib; // sorted splatId (per-instance)
uniform uint numSplats; // total number of splats
uniform highp usampler2D splatOrder; // per-splat index to source gaussian
// initialize the splat source structure and global splat
bool initSource(out SplatSource source) {
// calculate splat order
source.order = vertex_id_attrib + uint(vertex_position.z);
// splatId comes directly from the instance vertex buffer
uint splatId = vertex_id_attrib;
// return if out of range (since the last block of splats may be partially full)
if (source.order >= numSplats) {
return false;
}
source.order = uint(gl_InstanceID);
ivec2 orderUV = ivec2(source.order % splatTextureSize, source.order / splatTextureSize);
// read splat id and initialize global splat for format read functions
uint splatId = texelFetch(splatOrder, orderUV, 0).r;
setSplat(splatId);
// get the corner
source.cornerUV = vertex_position.xy;
// derive quad corner from gl_VertexID (index buffer: 0,1,2, 0,2,3)
source.cornerUV = vec2(
(gl_VertexID == 1 || gl_VertexID == 2) ? 1.0 : -1.0,
(gl_VertexID >= 2) ? 1.0 : -1.0
);
return true;
}
Expand Down