diff --git a/src/scene/gsplat-unified/gsplat-manager.js b/src/scene/gsplat-unified/gsplat-manager.js index ec067735bd9..b304f7dd00f 100644 --- a/src/scene/gsplat-unified/gsplat-manager.js +++ b/src/scene/gsplat-unified/gsplat-manager.js @@ -832,7 +832,6 @@ class GSplatManager { const textureSize = worldState.textureSize; if (textureSize !== this.workBuffer.textureSize) { this.workBuffer.resize(textureSize); - this.renderer.setMaxNumSplats(textureSize * textureSize); } // Bounds and transforms textures are needed for frustum culling. diff --git a/src/scene/gsplat-unified/gsplat-renderer.js b/src/scene/gsplat-unified/gsplat-renderer.js index 611c0521f9b..e45a63153e5 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, PIXELFORMAT_RGBA16U } from '../../platform/graphics/constants.js'; +import { SEMANTIC_POSITION, CULLFACE_NONE, PIXELFORMAT_RGBA16U } from '../../platform/graphics/constants.js'; import { BLEND_NONE, BLEND_PREMULTIPLIED, BLEND_ADDITIVE, GSPLAT_FORWARD, GSPLAT_SHADOW, SHADOWCAMERA_NAME @@ -6,10 +6,8 @@ import { import { ShaderMaterial } from '../materials/shader-material.js'; import { GSplatResourceBase } from '../gsplat/gsplat-resource-base.js'; import { MeshInstance } from '../mesh-instance.js'; -import { math } from '../../core/math/math.js'; /** - * @import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js' * @import { StorageBuffer } from '../../platform/graphics/storage-buffer.js' * @import { Layer } from '../layer.js' * @import { GraphNode } from '../graph-node.js' @@ -29,12 +27,6 @@ class GSplatRenderer { /** @type {MeshInstance} */ meshInstance; - /** @type {VertexBuffer|null} */ - instanceIndices = null; - - /** @type {number} */ - instanceIndicesCount = 0; - /** @type {Layer} */ layer; @@ -84,11 +76,12 @@ class GSplatRenderer { vertexWGSL: '#include "gsplatVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { - vertex_position: SEMANTIC_POSITION, - vertex_id_attrib: SEMANTIC_ATTR13 + vertex_position: SEMANTIC_POSITION } }); + this._material.setDefine('{GSPLAT_INSTANCE_SIZE}', GSplatResourceBase.instanceSize); + this.configureMaterial(); // Capture internal define names to protect them from being cleared @@ -97,6 +90,7 @@ class GSplatRenderer { }); // Also protect defines that may be added dynamically + this._internalDefines.add('{GSPLAT_INSTANCE_SIZE}'); this._internalDefines.add('GSPLAT_UNIFIED_ID'); this._internalDefines.add('PICK_CUSTOM_ID'); this._internalDefines.add('GSPLAT_INDIRECT_DRAW'); @@ -248,6 +242,12 @@ class GSplatRenderer { updateIndirect(textureSize) { this._material.setParameter('splatTextureSize', textureSize); this.meshInstance.visible = true; + + // Ensure instancingCount is non-zero so the forward/shadow renderers don't + // skip this draw call. The actual instance count is GPU-driven via indirect args. + if (this.meshInstance.instancingCount <= 0) { + this.meshInstance.instancingCount = 1; + } } /** @@ -420,34 +420,12 @@ 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.instanceIndicesCount < roundedNumSplats) { - this.instanceIndicesCount = roundedNumSplats; - - // destroy old instance indices - this.instanceIndices?.destroy(); - - // create new instance indices - this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, numSplats); - this.meshInstance.setInstancing(this.instanceIndices, true); - - // update texture size uniform - 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); const meshInstance = new MeshInstance(mesh, this._material); meshInstance.node = this.node; - meshInstance.setInstancing(instanceIndices, true); + meshInstance.setInstancing(true, true); // only start rendering the splat after we've received the splat order data meshInstance.instancingCount = 0; diff --git a/src/scene/gsplat/gsplat-instance.js b/src/scene/gsplat/gsplat-instance.js index 8675054ba93..e29266f6f82 100644 --- a/src/scene/gsplat/gsplat-instance.js +++ b/src/scene/gsplat/gsplat-instance.js @@ -1,7 +1,7 @@ import { Debug } from '../../core/debug.js'; import { Mat4 } from '../../core/math/mat4.js'; import { Vec3 } from '../../core/math/vec3.js'; -import { BUFFERUSAGE_COPY_DST, CULLFACE_NONE, SEMANTIC_ATTR13, SEMANTIC_POSITION, PIXELFORMAT_R32U } from '../../platform/graphics/constants.js'; +import { BUFFERUSAGE_COPY_DST, CULLFACE_NONE, SEMANTIC_POSITION, PIXELFORMAT_R32U } from '../../platform/graphics/constants.js'; import { StorageBuffer } from '../../platform/graphics/storage-buffer.js'; import { MeshInstance } from '../mesh-instance.js'; import { GSplatResolveSH } from './gsplat-resolve-sh.js'; @@ -16,7 +16,6 @@ import { BLEND_NONE, BLEND_PREMULTIPLIED } from '../constants.js'; * @import { GraphNode } from '../graph-node.js' * @import { Mesh } from '../mesh.js' * @import { Texture } from '../../platform/graphics/texture.js' - * @import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js' */ const mat = new Mat4(); @@ -88,6 +87,7 @@ class GSplatInstance { if (options.material) { this._material = options.material; + this._material.setDefine('{GSPLAT_INSTANCE_SIZE}', String(GSplatResourceBase.instanceSize)); this.setMaterialOrderData(this._material); } else { this._material = new ShaderMaterial({ @@ -97,8 +97,7 @@ class GSplatInstance { vertexWGSL: '#include "gsplatVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { - vertex_position: SEMANTIC_POSITION, - vertex_id_attrib: SEMANTIC_ATTR13 + vertex_position: SEMANTIC_POSITION } }); @@ -108,7 +107,7 @@ class GSplatInstance { resource.ensureMesh(); this.meshInstance = new MeshInstance(/** @type {Mesh} */ (resource.mesh), this._material); - this.meshInstance.setInstancing(/** @type {VertexBuffer} */ (resource.instanceIndices), true); + this.meshInstance.setInstancing(true, true); this.meshInstance.gsplatInstance = this; // only start rendering the splat after we've received the splat order data @@ -154,6 +153,7 @@ class GSplatInstance { set material(value) { if (this._material !== value) { this._material = value; + this._material.setDefine('{GSPLAT_INSTANCE_SIZE}', String(GSplatResourceBase.instanceSize)); this.setMaterialOrderData(this._material); if (this.meshInstance) { @@ -176,6 +176,7 @@ class GSplatInstance { configureMaterial(material, options = {}) { this.resource.configureMaterial(material, null, this.resource.format.getInputDeclarations()); + material.setDefine('{GSPLAT_INSTANCE_SIZE}', GSplatResourceBase.instanceSize); material.setParameter('numSplats', 0); this.setMaterialOrderData(material); material.setParameter('alphaClip', 0.3); diff --git a/src/scene/gsplat/gsplat-material.js b/src/scene/gsplat/gsplat-material.js index 60eaf69edfc..38792d5bc13 100644 --- a/src/scene/gsplat/gsplat-material.js +++ b/src/scene/gsplat/gsplat-material.js @@ -1,10 +1,11 @@ import { - CULLFACE_NONE, SEMANTIC_ATTR13, SEMANTIC_POSITION, SHADERLANGUAGE_GLSL, SHADERLANGUAGE_WGSL + CULLFACE_NONE, SEMANTIC_POSITION, SHADERLANGUAGE_GLSL, SHADERLANGUAGE_WGSL } from '../../platform/graphics/constants.js'; import { BLEND_NONE, BLEND_PREMULTIPLIED, DITHER_NONE } from '../constants.js'; import { ShaderMaterial } from '../materials/shader-material.js'; import { ShaderChunks } from '../shader-lib/shader-chunks.js'; +import { GSplatResourceBase } from './gsplat-resource-base.js'; /** * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' @@ -38,11 +39,11 @@ const createGSplatMaterial = (device, options = {}) => { vertexWGSL: options.vertex ? '' : ShaderChunks.get(device, SHADERLANGUAGE_WGSL).get('gsplatVS'), fragmentWGSL: options.vertex ? '' : ShaderChunks.get(device, SHADERLANGUAGE_WGSL).get('gsplatPS'), attributes: { - vertex_position: SEMANTIC_POSITION, - vertex_id_attrib: SEMANTIC_ATTR13 + vertex_position: SEMANTIC_POSITION } }); + material.setDefine('{GSPLAT_INSTANCE_SIZE}', GSplatResourceBase.instanceSize); material.setDefine(`DITHER_${ditherEnum.toUpperCase()}`, ''); material.cull = CULLFACE_NONE; diff --git a/src/scene/gsplat/gsplat-resource-base.js b/src/scene/gsplat/gsplat-resource-base.js index f5fe2344254..05f7d22dbd6 100644 --- a/src/scene/gsplat/gsplat-resource-base.js +++ b/src/scene/gsplat/gsplat-resource-base.js @@ -1,8 +1,5 @@ import { Debug } from '../../core/debug.js'; import { BoundingBox } from '../../core/shape/bounding-box.js'; -import { BUFFER_STATIC, SEMANTIC_ATTR13, TYPE_UINT32 } from '../../platform/graphics/constants.js'; -import { VertexFormat } from '../../platform/graphics/vertex-format.js'; -import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js'; import { Mesh } from '../mesh.js'; import { ShaderMaterial } from '../materials/shader-material.js'; import { WorkBufferRenderInfo } from '../gsplat-unified/gsplat-work-buffer.js'; @@ -61,12 +58,6 @@ class GSplatResourceBase { */ mesh = null; - /** - * @type {VertexBuffer|null} - * @ignore - */ - instanceIndices = null; - /** * @type {number} * @ignore @@ -154,7 +145,6 @@ class GSplatResourceBase { _actualDestroy() { this.streams.destroy(); this.mesh?.destroy(); - this.instanceIndices?.destroy(); this.workBufferRenderInfos.forEach(info => info.destroy()); this.workBufferRenderInfos.clear(); } @@ -203,7 +193,6 @@ class GSplatResourceBase { if (!this.mesh) { this.mesh = GSplatResourceBase.createMesh(this.device); this.mesh.aabb.copy(this.aabb); - this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, this.gsplatData.numSplats); } this._meshRefCount++; } @@ -218,8 +207,6 @@ class GSplatResourceBase { this._meshRefCount--; if (this._meshRefCount < 1) { this.mesh = null; // mesh instances destroy mesh when their refCount reaches zero - this.instanceIndices?.destroy(); - this.instanceIndices = null; } } @@ -323,28 +310,6 @@ class GSplatResourceBase { return mesh; } - static createInstanceIndices(device, splatCount) { - const splatInstanceSize = GSplatResourceBase.instanceSize; - const numSplats = Math.ceil(splatCount / splatInstanceSize) * splatInstanceSize; - const numSplatInstances = numSplats / splatInstanceSize; - - const indexData = new Uint32Array(numSplatInstances); - for (let i = 0; i < numSplatInstances; ++i) { - indexData[i] = i * splatInstanceSize; - } - - const vertexFormat = new VertexFormat(device, [ - { semantic: SEMANTIC_ATTR13, components: 1, type: TYPE_UINT32, asInt: true } - ]); - - const instanceIndices = new VertexBuffer(device, vertexFormat, numSplatInstances, { - usage: BUFFER_STATIC, - data: indexData.buffer - }); - - return instanceIndices; - } - static get instanceSize() { return 128; // number of splats per instance } diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js index 2e92e454e84..c0c765775a1 100644 --- a/src/scene/mesh-instance.js +++ b/src/scene/mesh-instance.js @@ -3,6 +3,7 @@ import { BoundingBox } from '../core/shape/bounding-box.js'; import { BoundingSphere } from '../core/shape/bounding-sphere.js'; import { BindGroup } from '../platform/graphics/bind-group.js'; import { UniformBuffer } from '../platform/graphics/uniform-buffer.js'; +import { VertexBuffer } from '../platform/graphics/vertex-buffer.js'; import { DrawCommands } from '../platform/graphics/draw-commands.js'; import { indexFormatByteSize } from '../platform/graphics/constants.js'; import { @@ -39,7 +40,6 @@ import { PickerId } from './picker-id.js'; * @import { Texture } from '../platform/graphics/texture.js' * @import { UniformBufferFormat } from '../platform/graphics/uniform-buffer-format.js' * @import { Vec3 } from '../core/math/vec3.js' - * @import { VertexBuffer } from '../platform/graphics/vertex-buffer.js' * @import { CameraComponent } from '../framework/components/camera/component.js'; */ @@ -1129,8 +1129,11 @@ class MeshInstance { * Note that {@link instancingCount} is automatically set to the number of vertices of the * vertex buffer when it is provided. * - * @param {VertexBuffer|null} vertexBuffer - Vertex buffer to hold per-instance vertex data - * (usually world matrices). Pass null to turn off hardware instancing. + * @param {VertexBuffer|true|null} vertexBuffer - Vertex buffer to hold per-instance vertex data + * (usually world matrices). Pass `true` to enable attributeless instancing where the instance + * index is derived from `gl_InstanceID` / `instance_index` builtins rather than a vertex + * buffer attribute — the caller must set {@link instancingCount} manually. Pass null to turn + * off hardware instancing. * @param {boolean} cull - Whether to perform frustum culling on this instance. If true, the whole * instance will be culled by the camera frustum. This often involves setting * {@link RenderComponent#customAabb} containing all instances. Defaults to false, which means @@ -1138,11 +1141,15 @@ class MeshInstance { */ setInstancing(vertexBuffer, cull = false) { if (vertexBuffer) { - this.instancingData = new InstancingData(vertexBuffer.numVertices); - this.instancingData.vertexBuffer = vertexBuffer; + if (vertexBuffer === true) { + this.instancingData = new InstancingData(0); + } else { + this.instancingData = new InstancingData(vertexBuffer.numVertices); + this.instancingData.vertexBuffer = vertexBuffer; - // mark vertex buffer as instancing data - vertexBuffer.format.instancing = true; + // mark vertex buffer as instancing data + vertexBuffer.format.instancing = true; + } // set up culling this.cull = cull; @@ -1151,7 +1158,9 @@ class MeshInstance { this.cull = true; } - this._updateShaderDefs(vertexBuffer ? (this._shaderDefs | SHADERDEF_INSTANCING) : (this._shaderDefs & ~SHADERDEF_INSTANCING)); + this._updateShaderDefs(vertexBuffer instanceof VertexBuffer ? + (this._shaderDefs | SHADERDEF_INSTANCING) : + (this._shaderDefs & ~SHADERDEF_INSTANCING)); } /** 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..d83d3704ef1 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatSource.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatSource.js @@ -1,14 +1,13 @@ 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; // xy: cornerUV, z: render order offset within 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); + // calculate splat order from instance index and vertex position offset + source.order = uint(gl_InstanceID) * {GSPLAT_INSTANCE_SIZE}u + uint(vertex_position.z); // return if out of range (since the last block of splats may be partially full) if (source.order >= numSplats) { diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatSource.js b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatSource.js index c5203b30aa0..367169bbb5a 100644 --- a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatSource.js +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatSource.js @@ -1,6 +1,5 @@ export default /* wgsl */` -attribute vertex_position: vec3f; // xy: cornerUV, z: render order offset -attribute vertex_id_attrib: u32; // render order base +attribute vertex_position: vec3f; // xy: cornerUV, z: render order offset within instance #ifdef GSPLAT_INDIRECT_DRAW // When using indirect draw with compaction, numSplats is written by the @@ -16,8 +15,8 @@ attribute vertex_id_attrib: u32; // render order base // initialize the splat source structure fn initSource(source: ptr) -> bool { - // calculate splat order - source.order = vertex_id_attrib + u32(vertex_position.z); + // calculate splat order from instance index and vertex position offset + source.order = pcInstanceIndex * {GSPLAT_INSTANCE_SIZE}u + u32(vertex_position.z); // return if out of range (since the last block of splats may be partially full) #ifdef GSPLAT_INDIRECT_DRAW