UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

522 lines (521 loc) 24.7 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug, DebugHelper } from "../../core/debug.js"; import { Mat4 } from "../../core/math/mat4.js"; import { Vec2 } from "../../core/math/vec2.js"; import { Vec3 } from "../../core/math/vec3.js"; import { Compute } from "../../platform/graphics/compute.js"; import { Shader } from "../../platform/graphics/shader.js"; import { StorageBuffer } from "../../platform/graphics/storage-buffer.js"; import { BindGroupFormat, BindStorageBufferFormat, BindUniformBufferFormat } from "../../platform/graphics/bind-group-format.js"; import { UniformBufferFormat, UniformFormat } from "../../platform/graphics/uniform-buffer-format.js"; import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, PIXELFORMAT_RGBA16U, SHADERLANGUAGE_WGSL, SHADERSTAGE_COMPUTE, UNIFORMTYPE_FLOAT, UNIFORMTYPE_MAT4, UNIFORMTYPE_UINT, UNIFORMTYPE_UVEC4, UNIFORMTYPE_VEC3 } from "../../platform/graphics/constants.js"; import { PROJECTION_ORTHOGRAPHIC } from "../constants.js"; import { Camera } from "../camera.js"; import { GSplatResourceBase } from "../gsplat/gsplat-resource-base.js"; import { GSplatSortBinWeights } from "./gsplat-sort-bin-weights.js"; import { CACHE_STRIDE } from "./gsplat-projector-constants.js"; import { computeGsplatProjectorSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-projector.js"; import { computeGsplatProjectorWriteIndirectArgsSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-projector-write-indirect-args.js"; import { computeGsplatProjectCommonSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-project-common.js"; import { computeGsplatCommonSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-common.js"; import { computeGsplatTileIntersectSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-tile-intersect.js"; import computeSplatSource from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatComputeSplat.js"; const INDEX_COUNT = 6 * GSplatResourceBase.instanceSize; const PROJECTOR_WORKGROUP_SIZE = 256; const _cameraDir = new Vec3(); const _dispatchSize = new Vec2(); const _viewProjMat = new Mat4(); const _viewProjData = new Float32Array(16); const _viewData = new Float32Array(16); const _shaderProjMat = new Mat4(); class GSplatProjector { /** * @param {GraphicsDevice} device - The graphics device (must support compute). */ constructor(device) { /** @type {GraphicsDevice} */ __publicField(this, "device"); /** * 32 B per splat (8 u32 slots), sized to the work-buffer capacity (not the * post-cull count) to avoid undersizing on transient cull-rate drops. * * @type {StorageBuffer|null} */ __publicField(this, "projCache", null); /** @type {StorageBuffer|null} */ __publicField(this, "sortKeys", null); /** * Single-element atomic counter; reset to 0 every frame and incremented by * the projector pass via per-workgroup atomicAdd. * * @type {StorageBuffer|null} */ __publicField(this, "renderCounter", null); /** * Storage buffer holding interleaved bin weights {base, divider} consumed by * the projector for camera-relative sort key precision. * * @type {StorageBuffer|null} */ __publicField(this, "binWeightsBuffer", null); /** @type {GSplatSortBinWeights} */ __publicField(this, "binWeightsUtil"); /** * Lazily-created projector compute variants keyed by `radial|pick|fisheye|aa` * combination (see `_projectorKey`). AA is mutually exclusive with pick, so up to * 12 variants exist; only the combinations actually needed by the running app are * compiled. * * @type {Map<string, Compute>} */ __publicField(this, "_projectorComputes", /* @__PURE__ */ new Map()); /** @type {BindGroupFormat|null} */ __publicField(this, "_projectorBindGroupFormat", null); /** * Uniform buffer format for the non-fisheye projector variant. * * @type {UniformBufferFormat|null} */ __publicField(this, "_projectorUniformBufferFormat", null); /** * Uniform buffer format for the fisheye projector variant. Adds 4 fisheye * scalar uniforms after the shared fields. * * @type {UniformBufferFormat|null} */ __publicField(this, "_projectorUniformBufferFormatFisheye", null); /** @type {Compute|null} */ __publicField(this, "_writeIndirectArgsCompute", null); /** @type {BindGroupFormat|null} */ __publicField(this, "_writeArgsBindGroupFormat", null); /** @type {UniformBufferFormat|null} */ __publicField(this, "_writeArgsUniformBufferFormat", null); /** * Cached work-buffer format version; changes invalidate the projector * compute (its bind group format is derived from the work-buffer format). */ __publicField(this, "_formatVersion", -1); /** @type {number} */ __publicField(this, "_allocatedCacheCount", 0); /** @type {Float32Array} */ __publicField(this, "cameraPositionData", new Float32Array(3)); /** @type {Float32Array} */ __publicField(this, "cameraDirectionData", new Float32Array(3)); Debug.assert(device.supportsCompute, "GSplatProjector requires compute shader support (WebGPU)"); this.device = device; this.binWeightsUtil = new GSplatSortBinWeights(); this.binWeightsBuffer = new StorageBuffer( device, GSplatSortBinWeights.NUM_BINS * 2 * 4, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST ); DebugHelper.setName(this.binWeightsBuffer, "GsplatProjector.binWeights"); this.renderCounter = new StorageBuffer(device, 4, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); DebugHelper.setName(this.renderCounter, "GsplatProjector.renderCounter"); this._createUniformBufferFormats(); this._createWriteIndirectArgsCompute(); } destroy() { this.projCache?.destroy(); this.sortKeys?.destroy(); this.renderCounter?.destroy(); this.binWeightsBuffer?.destroy(); for (const compute of this._projectorComputes.values()) { compute.shader?.destroy(); } this._projectorComputes.clear(); this._projectorBindGroupFormat?.destroy(); this._writeIndirectArgsCompute?.shader?.destroy(); this._writeArgsBindGroupFormat?.destroy(); this.projCache = null; this.sortKeys = null; this.renderCounter = null; this.binWeightsBuffer = null; this._projectorBindGroupFormat = null; this._projectorUniformBufferFormat = null; this._projectorUniformBufferFormatFisheye = null; this._writeIndirectArgsCompute = null; this._writeArgsBindGroupFormat = null; this._writeArgsUniformBufferFormat = null; } /** @private */ _createUniformBufferFormats() { const device = this.device; const baseFields = [ new UniformFormat("splatTextureSize", UNIFORMTYPE_UINT), new UniformFormat("numBins", UNIFORMTYPE_UINT), new UniformFormat("isOrtho", UNIFORMTYPE_UINT), new UniformFormat("pad0", UNIFORMTYPE_UINT), new UniformFormat("viewProj", UNIFORMTYPE_MAT4), new UniformFormat("viewMatrix", UNIFORMTYPE_MAT4), new UniformFormat("cameraPosition", UNIFORMTYPE_VEC3), new UniformFormat("minPixelSize", UNIFORMTYPE_FLOAT), new UniformFormat("cameraDirection", UNIFORMTYPE_VEC3), new UniformFormat("focal", UNIFORMTYPE_FLOAT), new UniformFormat("viewportWidth", UNIFORMTYPE_FLOAT), new UniformFormat("viewportHeight", UNIFORMTYPE_FLOAT), new UniformFormat("nearClip", UNIFORMTYPE_FLOAT), new UniformFormat("farClip", UNIFORMTYPE_FLOAT), new UniformFormat("alphaClip", UNIFORMTYPE_FLOAT), new UniformFormat("minContribution", UNIFORMTYPE_FLOAT), new UniformFormat("minDist", UNIFORMTYPE_FLOAT), new UniformFormat("invRange", UNIFORMTYPE_FLOAT) ]; this._projectorUniformBufferFormat = new UniformBufferFormat(device, baseFields); this._projectorUniformBufferFormatFisheye = new UniformBufferFormat(device, [ ...baseFields.map((f) => new UniformFormat(f.name, f.type)), new UniformFormat("fisheye_k", UNIFORMTYPE_FLOAT), new UniformFormat("fisheye_inv_k", UNIFORMTYPE_FLOAT), new UniformFormat("fisheye_projMat00", UNIFORMTYPE_FLOAT), new UniformFormat("fisheye_projMat11", UNIFORMTYPE_FLOAT) ]); this._writeArgsUniformBufferFormat = new UniformBufferFormat(device, [ new UniformFormat("drawSlot", UNIFORMTYPE_UINT), new UniformFormat("indexCount", UNIFORMTYPE_UINT), new UniformFormat("sortSlotBase", UNIFORMTYPE_UINT), new UniformFormat("pad0", UNIFORMTYPE_UINT), new UniformFormat("sortIndirectInfo", UNIFORMTYPE_UVEC4) ]); } /** @private */ _createWriteIndirectArgsCompute() { const device = this.device; this._writeArgsBindGroupFormat = new BindGroupFormat(device, [ new BindStorageBufferFormat("renderCounter", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("indirectDrawArgs", SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat("numSplatsBuf", SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat("indirectDispatchArgs", SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat("sortElementCountBuf", SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE) ]); const cdefines = /* @__PURE__ */ new Map([ ["{INSTANCE_SIZE}", GSplatResourceBase.instanceSize.toString()] ]); const shader = new Shader(device, { name: "GSplatProjectorWriteIndirectArgs", shaderLanguage: SHADERLANGUAGE_WGSL, cshader: computeGsplatProjectorWriteIndirectArgsSource, cdefines, computeBindGroupFormat: this._writeArgsBindGroupFormat, computeUniformBufferFormats: { uniforms: this._writeArgsUniformBufferFormat } }); this._writeIndirectArgsCompute = new Compute(device, shader, "GSplatProjectorWriteIndirectArgs"); } /** * Destroys the projector compute pipelines so they are lazily rebuilt against the * current work-buffer format. * * @private */ _destroyProjectorComputes() { for (const compute of this._projectorComputes.values()) { compute.shader?.destroy(); } this._projectorComputes.clear(); this._projectorBindGroupFormat?.destroy(); this._projectorBindGroupFormat = null; } /** * Builds the cache key for a projector variant combination. * * @param {boolean} radialSort - Radial vs linear sort. * @param {boolean} pickMode - Pick output mode. * @param {boolean} fisheyeMode - Fisheye projection mode. * @param {boolean} antiAlias - Anti-aliasing opacity compensation mode. * @returns {string} The cache key. * @private */ _projectorKey(radialSort, pickMode, fisheyeMode, antiAlias) { return `${radialSort ? "r" : "l"}${pickMode ? "p" : ""}${fisheyeMode ? "f" : ""}${antiAlias ? "a" : ""}`; } /** * Builds the projector Compute (one variant per sort/pick/fisheye combination). * * @param {GSplatWorkBuffer} workBuffer - The current work buffer (provides format). * @param {boolean} radialSort - Whether to compile the RADIAL_SORT variant. * @param {boolean} pickMode - Whether to write pcId into the cache for picking. * @param {boolean} fisheyeMode - Whether to compile the GSPLAT_FISHEYE variant. * @param {boolean} antiAlias - Whether to compile the GSPLAT_AA variant (bakes the * anti-aliasing opacity compensation into the cached alpha). * @returns {Compute} The created compute instance. * @private */ _createProjectorCompute(workBuffer, radialSort, pickMode, fisheyeMode, antiAlias) { const device = this.device; const wbFormat = workBuffer.format; const fixedBindings = [ new BindStorageBufferFormat("compactedSplatIds", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("sortElementCount", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("projCache", SHADERSTAGE_COMPUTE), new BindStorageBufferFormat("sortKeys", SHADERSTAGE_COMPUTE), new BindStorageBufferFormat("renderCounter", SHADERSTAGE_COMPUTE), new BindStorageBufferFormat("binWeights", SHADERSTAGE_COMPUTE, true), new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE) ]; if (!this._projectorBindGroupFormat) { this._projectorBindGroupFormat = new BindGroupFormat(device, [ ...fixedBindings, ...wbFormat.getComputeBindFormats() ]); } const cincludes = /* @__PURE__ */ new Map(); cincludes.set("gsplatCommonCS", computeGsplatCommonSource); cincludes.set("gsplatTileIntersectCS", computeGsplatTileIntersectSource); cincludes.set("gsplatComputeSplatCS", computeSplatSource); cincludes.set("gsplatFormatDeclCS", wbFormat.getComputeInputDeclarations(fixedBindings.length)); cincludes.set("gsplatFormatReadCS", wbFormat.getReadCode()); cincludes.set("gsplatProjectCommonCS", computeGsplatProjectCommonSource); const cdefines = /* @__PURE__ */ new Map(); cdefines.set("{CACHE_STRIDE}", CACHE_STRIDE.toString()); if (radialSort) { cdefines.set("RADIAL_SORT", ""); } if (pickMode) { cdefines.set("PICK_MODE", ""); } if (fisheyeMode) { cdefines.set("GSPLAT_FISHEYE", ""); } if (antiAlias) { cdefines.set("GSPLAT_AA", ""); } const colorStream = wbFormat.getStream("dataColor"); if (colorStream && colorStream.format !== PIXELFORMAT_RGBA16U) { cdefines.set("GSPLAT_COLOR_FLOAT", ""); } const name = `GSplatProjector${radialSort ? "Radial" : "Linear"}${pickMode ? "Pick" : ""}${fisheyeMode ? "Fisheye" : ""}${antiAlias ? "Aa" : ""}`; const ubFormat = fisheyeMode ? this._projectorUniformBufferFormatFisheye : this._projectorUniformBufferFormat; const shader = new Shader(device, { name, shaderLanguage: SHADERLANGUAGE_WGSL, cshader: computeGsplatProjectorSource, cincludes, cdefines, computeBindGroupFormat: this._projectorBindGroupFormat, computeUniformBufferFormats: { uniforms: ubFormat } }); return new Compute(device, shader, name); } /** * Returns the projector Compute for the requested sort/pick/fisheye combination, * lazily creating it. Recreates all compute instances when the work-buffer format * version changes. * * @param {GSplatWorkBuffer} workBuffer - The current work buffer. * @param {boolean} radialSort - Whether to use the radial sort variant. * @param {boolean} [pickMode] - Whether to use the pick-output variant. * @param {boolean} [fisheyeMode] - Whether to use the fisheye projection variant. * @param {boolean} [antiAlias] - Whether to use the anti-aliasing variant. * @returns {Compute} The projector compute instance. * @private */ _getProjectorCompute(workBuffer, radialSort, pickMode = false, fisheyeMode = false, antiAlias = false) { const wbFormat = workBuffer.format; if (this._formatVersion !== wbFormat.extraStreamsVersion) { this._destroyProjectorComputes(); this._formatVersion = wbFormat.extraStreamsVersion; } const key = this._projectorKey(radialSort, pickMode, fisheyeMode, antiAlias); let compute = this._projectorComputes.get(key); if (!compute) { compute = this._createProjectorCompute(workBuffer, radialSort, pickMode, fisheyeMode, antiAlias); this._projectorComputes.set(key, compute); } return compute; } /** * Ensures projCache and sortKeys storage buffers are sized to at least `capacity` splats. * Both buffers are sized to the work-buffer capacity (passed in by the caller) rather than * post-cull counts, mirroring `compactedSplatIds` allocation in interval compaction. * * @param {number} capacity - Required splat capacity (work-buffer total active splats). * @private */ _ensureCapacity(capacity) { if (capacity > this._allocatedCacheCount) { this.projCache?.destroy(); this.sortKeys?.destroy(); this._allocatedCacheCount = capacity; this.projCache = new StorageBuffer(this.device, capacity * CACHE_STRIDE * 4); this.sortKeys = new StorageBuffer(this.device, capacity * 4, BUFFERUSAGE_COPY_SRC); DebugHelper.setName(this.projCache, "GsplatProjector.projCache"); DebugHelper.setName(this.sortKeys, "GsplatProjector.sortKeys"); } } /** * Runs the project + cull + key-gen + compact compute pass. * * @param {object} params - Dispatch parameters. * @param {GSplatWorkBuffer} params.workBuffer - The current work buffer. * @param {GraphNode} params.cameraNode - The camera node (with attached CameraComponent). * @param {StorageBuffer} params.compactedSplatIds - Output of interval compaction. * @param {StorageBuffer} params.sortElementCountBuffer - GPU-written visible count from * interval compaction (single u32 element); the projector reads this to early-out * threads beyond the post-frustum-cull range. * @param {number} params.totalCapacity - Work-buffer capacity used to size projCache / * sortKeys (typically `worldState.totalActiveSplats`). * @param {boolean} params.radialSort - Whether to use the radial sort key variant. * @param {number} params.numBits - Sort key bit count (defines bucket count = 1 << numBits). * @param {number} params.minDist - Minimum distance for sort key normalisation. * @param {number} params.maxDist - Maximum distance for sort key normalisation. * @param {number} params.alphaClip - Alpha cull threshold. * @param {number} params.minPixelSize - Minimum on-screen pixel size before culling. * @param {number} params.minContribution - Minimum total contribution before culling. * @param {number} params.viewportWidth - Render viewport width in pixels. * @param {number} params.viewportHeight - Render viewport height in pixels. * @param {boolean} params.flipY - Whether the active render target uses `flipY` (must match * {@link Renderer#setCameraUniforms}). * @param {boolean} [params.pickMode] - Whether to write picking IDs into the cache. * @param {import('../graphics/fisheye-projection.js').FisheyeProjection} [params.fisheyeProj] - * Fisheye projection state. When `fisheyeProj.enabled` is true the projector picks the * GSPLAT_FISHEYE variant and writes NDC-style clip data; otherwise the linear path runs. * @param {boolean} [params.antiAlias] - Whether to bake anti-aliasing opacity * compensation into the cached alpha. Ignored in pick mode (picking only gates on a * binary opacity threshold), which keeps the projector variant count down. */ dispatch(params) { const { workBuffer, cameraNode, compactedSplatIds, sortElementCountBuffer, totalCapacity, radialSort, numBits, minDist, maxDist, alphaClip, minPixelSize, minContribution, viewportWidth, viewportHeight, flipY, pickMode = false, fisheyeProj, antiAlias = false } = params; const fisheyeMode = !!fisheyeProj?.enabled; const aaMode = antiAlias && !pickMode; this._ensureCapacity(totalCapacity); this.renderCounter.clear(); const compute = this._getProjectorCompute(workBuffer, radialSort, pickMode, fisheyeMode, aaMode); const cameraPos = cameraNode.getPosition(); const cameraMat = cameraNode.getWorldTransform(); const cameraDir = cameraMat.getZ(_cameraDir).normalize(); const range = maxDist - minDist; const invRange = range > 0 ? 1 / range : 1; const bucketCount = 1 << numBits; const cameraBin = GSplatSortBinWeights.computeCameraBin(radialSort, minDist, range); const binWeights = this.binWeightsUtil.compute(cameraBin, bucketCount); this.binWeightsBuffer.write(0, binWeights); compute.setParameter("compactedSplatIds", compactedSplatIds); compute.setParameter("sortElementCount", sortElementCountBuffer); compute.setParameter("projCache", this.projCache); compute.setParameter("sortKeys", this.sortKeys); compute.setParameter("renderCounter", this.renderCounter); compute.setParameter("binWeights", this.binWeightsBuffer); for (const stream of workBuffer.format.resourceStreams) { const texture = workBuffer.getTexture(stream.name); if (texture) { compute.setParameter(stream.name, texture); } } const cameraComponent = cameraNode.camera; const cam = cameraComponent.camera; const view = cam.viewMatrix; const webgpu = this.device.isWebGPU; _viewProjMat.mul2(Camera.applyShaderProjectionTransform(cam.projectionMatrix, _shaderProjMat, flipY, webgpu), view); _viewProjData.set(_viewProjMat.data); _viewData.set(view.data); const focal = viewportWidth * _shaderProjMat.data[0]; this.cameraPositionData[0] = cameraPos.x; this.cameraPositionData[1] = cameraPos.y; this.cameraPositionData[2] = cameraPos.z; compute.setParameter("cameraPosition", this.cameraPositionData); this.cameraDirectionData[0] = cameraDir.x; this.cameraDirectionData[1] = cameraDir.y; this.cameraDirectionData[2] = cameraDir.z; compute.setParameter("cameraDirection", this.cameraDirectionData); compute.setParameter("viewMatrix", _viewData); compute.setParameter("viewProj", _viewProjData); compute.setParameter("focal", focal); compute.setParameter("viewportWidth", viewportWidth); compute.setParameter("viewportHeight", viewportHeight); compute.setParameter("nearClip", cam.nearClip); compute.setParameter("farClip", cam.farClip); compute.setParameter("alphaClip", alphaClip); compute.setParameter("minPixelSize", minPixelSize); compute.setParameter("minContribution", minContribution); compute.setParameter("isOrtho", cam.projection === PROJECTION_ORTHOGRAPHIC ? 1 : 0); compute.setParameter("splatTextureSize", workBuffer.textureSize); compute.setParameter("numBins", GSplatSortBinWeights.NUM_BINS); compute.setParameter("minDist", minDist); compute.setParameter("invRange", invRange); compute.setParameter("pad0", 0); if (fisheyeMode) { compute.setParameter("fisheye_k", fisheyeProj.k); compute.setParameter("fisheye_inv_k", fisheyeProj.invK); compute.setParameter("fisheye_projMat00", fisheyeProj.projMat00); compute.setParameter("fisheye_projMat11", fisheyeProj.projMat11); } const workgroupCount = Math.ceil(totalCapacity / PROJECTOR_WORKGROUP_SIZE); Compute.calcDispatchSize( workgroupCount, _dispatchSize, this.device.limits.maxComputeWorkgroupsPerDimension || 65535 ); compute.setupDispatch(_dispatchSize.x, _dispatchSize.y, 1); this.device.computeDispatch([compute], "GSplatProjector"); } /** * Writes per-frame indirect draw / dispatch arguments derived from `renderCounter[0]`. * * @param {number} drawSlot - Slot index in `device.indirectDrawBuffer`. * @param {number} sortSlotBase - Base slot index in `device.indirectDispatchBuffer`. The * radix sort backend uses `sortIndirectInfo[0]` consecutive slots starting here. * @param {StorageBuffer} numSplatsBuffer - Storage buffer the vertex shader reads for the * post-cull splat count (single u32 element). * @param {StorageBuffer} sortElementCountBuffer - Storage buffer the radix sort reads for * its element count (single u32 element). * @param {Uint32Array} sortIndirectInfo - Sorter-owned 4-element Uint32 array returned by * `ComputeRadixSort.prepareIndirect()`, used as a `vec4<u32>` uniform by the shader. */ writeIndirectArgs(drawSlot, sortSlotBase, numSplatsBuffer, sortElementCountBuffer, sortIndirectInfo) { const compute = this._writeIndirectArgsCompute; compute.setParameter("renderCounter", this.renderCounter); compute.setParameter("indirectDrawArgs", this.device.indirectDrawBuffer); compute.setParameter("numSplatsBuf", numSplatsBuffer); compute.setParameter("indirectDispatchArgs", this.device.indirectDispatchBuffer); compute.setParameter("sortElementCountBuf", sortElementCountBuffer); compute.setParameter("drawSlot", drawSlot); compute.setParameter("indexCount", INDEX_COUNT); compute.setParameter("sortSlotBase", sortSlotBase); compute.setParameter("pad0", 0); compute.setParameter("sortIndirectInfo", sortIndirectInfo); compute.setupDispatch(1); this.device.computeDispatch([compute], "GSplatProjectorWriteIndirectArgs"); } } export { GSplatProjector };