diff --git a/src/scene/gsplat-unified/gsplat-renderer.js b/src/scene/gsplat-unified/gsplat-renderer.js index 611c0521f9b..881a2b5a08d 100644 --- a/src/scene/gsplat-unified/gsplat-renderer.js +++ b/src/scene/gsplat-unified/gsplat-renderer.js @@ -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'; @@ -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); @@ -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; } } @@ -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; diff --git a/src/scene/gsplat-unified/gsplat-work-buffer.js b/src/scene/gsplat-unified/gsplat-work-buffer.js index 6bde6974740..acab76ca095 100644 --- a/src/scene/gsplat-unified/gsplat-work-buffer.js +++ b/src/scene/gsplat-unified/gsplat-work-buffer.js @@ -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'; @@ -170,6 +173,9 @@ class GSplatWorkBuffer { /** @type {StorageBuffer|undefined} */ orderBuffer; + /** @type {VertexBuffer|undefined} */ + orderVB; + /** @type {UploadStream} */ uploadStream; @@ -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 @@ -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(); @@ -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); } } @@ -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. * diff --git a/src/scene/gsplat-unified/gsplat-world-state.js b/src/scene/gsplat-unified/gsplat-world-state.js index b263bb8e9d2..1b9cb1361c2 100644 --- a/src/scene/gsplat-unified/gsplat-world-state.js +++ b/src/scene/gsplat-unified/gsplat-world-state.js @@ -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]); @@ -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); diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatSource.js b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatSource.js index e34d4f9a28d..7800ce5abdb 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatSource.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatSource.js @@ -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; }