UNPKG

playcanvas

Version:

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

392 lines (391 loc) 18.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 { 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, SHADERLANGUAGE_WGSL, SHADERSTAGE_COMPUTE, UNIFORMTYPE_FLOAT, UNIFORMTYPE_UINT, UNIFORMTYPE_VEC3, UNIFORMTYPE_VEC4, UNIFORMTYPE_UVEC4 } from "../../platform/graphics/constants.js"; import { computeGsplatIntervalCullSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-interval-cull.js"; import { computeGsplatIntervalScatterSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-interval-scatter.js"; import { computeGsplatWriteIndirectArgsSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-write-indirect-args.js"; import { PrefixSumKernel } from "../graphics/prefix-sum-kernel.js"; import { GSplatResourceBase } from "../gsplat/gsplat-resource-base.js"; const WORKGROUP_SIZE = 256; const INDEX_COUNT = 6 * GSplatResourceBase.instanceSize; const INTERVAL_STRIDE = 4; class GSplatIntervalCompaction { /** * @param {GraphicsDevice} device - The graphics device (must support compute). */ constructor(device) { /** @type {GraphicsDevice} */ __publicField(this, "device"); /** @type {StorageBuffer|null} */ __publicField(this, "compactedSplatIds", null); /** @type {StorageBuffer|null} */ __publicField(this, "intervalsBuffer", null); /** @type {StorageBuffer|null} */ __publicField(this, "countBuffer", null); /** @type {PrefixSumKernel|null} */ __publicField(this, "prefixSumKernel", null); /** @type {StorageBuffer|null} */ __publicField(this, "numSplatsBuffer", null); /** @type {StorageBuffer|null} */ __publicField(this, "sortElementCountBuffer", null); /** @type {number} */ __publicField(this, "allocatedCompactedCount", 0); /** @type {number} */ __publicField(this, "allocatedIntervalCount", 0); /** @type {number} */ __publicField(this, "allocatedCountBufferSize", 0); /** * World state version for which intervals were last uploaded. Avoids redundant * uploads when sortGpu is called repeatedly with the same world state. */ __publicField(this, "_uploadedVersion", -1); /** @type {Compute|null} */ __publicField(this, "_cullComputePerspective", null); /** @type {Compute|null} */ __publicField(this, "_cullComputeFisheye", null); /** @type {Compute|null} */ __publicField(this, "_scatterCompute", null); /** @type {Compute|null} */ __publicField(this, "_writeIndirectArgsCompute", null); /** @type {BindGroupFormat|null} */ __publicField(this, "_cullBindGroupFormatPerspective", null); /** @type {BindGroupFormat|null} */ __publicField(this, "_cullBindGroupFormatFisheye", null); /** @type {BindGroupFormat|null} */ __publicField(this, "_scatterBindGroupFormat", null); /** @type {BindGroupFormat|null} */ __publicField(this, "_writeArgsBindGroupFormat", null); /** @type {UniformBufferFormat|null} */ __publicField(this, "_scatterUniformBufferFormat", null); /** @type {UniformBufferFormat|null} */ __publicField(this, "_writeArgsUniformBufferFormat", null); Debug.assert(device.supportsCompute, "GSplatIntervalCompaction requires compute shader support (WebGPU)"); this.device = device; this.numSplatsBuffer = new StorageBuffer(device, 4, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this.sortElementCountBuffer = new StorageBuffer(device, 4, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); DebugHelper.setName(this.numSplatsBuffer, "GsplatIntervalCompaction.numSplats"); DebugHelper.setName(this.sortElementCountBuffer, "GsplatIntervalCompaction.sortElementCount"); this.prefixSumKernel = new PrefixSumKernel(device); this._createUniformBufferFormats(); this._createScatterCompute(); this._createWriteIndirectArgsCompute(); } destroy() { this.compactedSplatIds?.destroy(); this.intervalsBuffer?.destroy(); this.countBuffer?.destroy(); this.prefixSumKernel?.destroy(); this.numSplatsBuffer?.destroy(); this.sortElementCountBuffer?.destroy(); this._destroyCullPass(); this._scatterCompute?.shader?.destroy(); this._scatterBindGroupFormat?.destroy(); this._writeIndirectArgsCompute?.shader?.destroy(); this._writeArgsBindGroupFormat?.destroy(); this.compactedSplatIds = null; this.intervalsBuffer = null; this.countBuffer = null; this.prefixSumKernel = null; this.numSplatsBuffer = null; this.sortElementCountBuffer = null; this._scatterCompute = null; this._scatterBindGroupFormat = null; this._writeIndirectArgsCompute = null; this._writeArgsBindGroupFormat = null; this._scatterUniformBufferFormat = null; this._writeArgsUniformBufferFormat = null; } /** @private */ _destroyCullPass() { this._cullComputePerspective?.shader?.destroy(); this._cullBindGroupFormatPerspective?.destroy(); this._cullComputePerspective = null; this._cullBindGroupFormatPerspective = null; this._cullComputeFisheye?.shader?.destroy(); this._cullBindGroupFormatFisheye?.destroy(); this._cullComputeFisheye = null; this._cullBindGroupFormatFisheye = null; } /** @private */ _createUniformBufferFormats() { const device = this.device; this._scatterUniformBufferFormat = new UniformBufferFormat(device, [ new UniformFormat("numIntervals", UNIFORMTYPE_UINT), new UniformFormat("pad0", UNIFORMTYPE_UINT), new UniformFormat("pad1", UNIFORMTYPE_UINT), new UniformFormat("pad2", UNIFORMTYPE_UINT) ]); this._writeArgsUniformBufferFormat = new UniformBufferFormat(device, [ new UniformFormat("drawSlot", UNIFORMTYPE_UINT), new UniformFormat("indexCount", UNIFORMTYPE_UINT), new UniformFormat("dispatchSlotBase", UNIFORMTYPE_UINT), new UniformFormat("totalSplats", UNIFORMTYPE_UINT), new UniformFormat("sortIndirectInfo", UNIFORMTYPE_UVEC4) ]); } /** * Creates a cull compute pass for the given mode. * * @param {boolean} fisheye - Whether to create the fisheye (cone) variant. * @returns {{ compute: Compute, bindGroupFormat: BindGroupFormat }} The created compute and bind group format. * @private */ _createCullPass(fisheye) { const device = this.device; const suffix = fisheye ? "Fisheye" : ""; const bindGroupFormat = new BindGroupFormat(device, [ new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE), new BindStorageBufferFormat("intervals", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("countBuffer", SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat("boundsBuffer", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("transformsBuffer", SHADERSTAGE_COMPUTE, true) ]); const cdefines = /* @__PURE__ */ new Map([["{WORKGROUP_SIZE}", WORKGROUP_SIZE.toString()]]); if (fisheye) { cdefines.set("GSPLAT_FISHEYE", ""); } const uniformBufferFormat = fisheye ? new UniformBufferFormat(device, [ new UniformFormat("cameraWorldPos", UNIFORMTYPE_VEC3), new UniformFormat("maxTheta", UNIFORMTYPE_FLOAT), new UniformFormat("cameraForward", UNIFORMTYPE_VEC3), new UniformFormat("numIntervals", UNIFORMTYPE_UINT) ]) : new UniformBufferFormat(device, [ new UniformFormat("frustumPlanes", UNIFORMTYPE_VEC4, 6), new UniformFormat("numIntervals", UNIFORMTYPE_UINT) ]); const shader = new Shader(device, { name: `GSplatIntervalCull${suffix}`, shaderLanguage: SHADERLANGUAGE_WGSL, cshader: computeGsplatIntervalCullSource, cdefines, computeBindGroupFormat: bindGroupFormat, computeUniformBufferFormats: { uniforms: uniformBufferFormat } }); const compute = new Compute(device, shader, `GSplatIntervalCull${suffix}`); return { compute, bindGroupFormat }; } /** * Returns the cached cull Compute for the given mode, lazily creating it on first use. * * @param {boolean} fisheye - Whether fisheye is active. * @returns {Compute} The cached Compute instance. * @private */ _getCullCompute(fisheye) { if (fisheye) { if (!this._cullComputeFisheye) { const { compute, bindGroupFormat } = this._createCullPass(true); this._cullComputeFisheye = compute; this._cullBindGroupFormatFisheye = bindGroupFormat; } return this._cullComputeFisheye; } if (!this._cullComputePerspective) { const { compute, bindGroupFormat } = this._createCullPass(false); this._cullComputePerspective = compute; this._cullBindGroupFormatPerspective = bindGroupFormat; } return this._cullComputePerspective; } /** @private */ _createScatterCompute() { const device = this.device; this._scatterBindGroupFormat = new BindGroupFormat(device, [ new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE), new BindStorageBufferFormat("intervals", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("prefixSumBuffer", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("compactedOutput", SHADERSTAGE_COMPUTE, false) ]); const cdefines = /* @__PURE__ */ new Map([ ["{WORKGROUP_SIZE}", WORKGROUP_SIZE.toString()] ]); const shader = new Shader(device, { name: "GSplatIntervalScatter", shaderLanguage: SHADERLANGUAGE_WGSL, cshader: computeGsplatIntervalScatterSource, cdefines, computeBindGroupFormat: this._scatterBindGroupFormat, computeUniformBufferFormats: { uniforms: this._scatterUniformBufferFormat } }); this._scatterCompute = new Compute(device, shader, "GSplatIntervalScatter"); } /** @private */ _createWriteIndirectArgsCompute() { const device = this.device; this._writeArgsBindGroupFormat = new BindGroupFormat(device, [ new BindStorageBufferFormat("prefixSumBuffer", 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], ["{KEYGEN_THREADS_PER_WORKGROUP}", 256], ["{MAX_WORKGROUPS_PER_DIM}", device.limits.maxComputeWorkgroupsPerDimension || 65535] ]); const shader = new Shader(device, { name: "GSplatIntervalWriteIndirectArgs", shaderLanguage: SHADERLANGUAGE_WGSL, cshader: computeGsplatWriteIndirectArgsSource, cdefines, computeBindGroupFormat: this._writeArgsBindGroupFormat, computeUniformBufferFormats: { uniforms: this._writeArgsUniformBufferFormat } }); this._writeIndirectArgsCompute = new Compute(device, shader, "GSplatIntervalWriteIndirectArgs"); } /** * Ensures all buffers have sufficient capacity. * * @param {number} numIntervals - Number of intervals. * @param {number} totalActiveSplats - Total active splats (max compacted output size). * @private */ _ensureCapacity(numIntervals, totalActiveSplats) { if (totalActiveSplats > this.allocatedCompactedCount) { this.compactedSplatIds?.destroy(); this.allocatedCompactedCount = totalActiveSplats; this.compactedSplatIds = new StorageBuffer(this.device, totalActiveSplats * 4, BUFFERUSAGE_COPY_SRC); DebugHelper.setName(this.compactedSplatIds, "GsplatIntervalCompaction.compactedSplatIds"); } const requiredCountSize = numIntervals + 1; if (requiredCountSize > this.allocatedCountBufferSize) { this.countBuffer?.destroy(); this.allocatedCountBufferSize = requiredCountSize; this.countBuffer = new StorageBuffer(this.device, requiredCountSize * 4); DebugHelper.setName(this.countBuffer, "GsplatIntervalCompaction.count"); if (this.prefixSumKernel) { this.prefixSumKernel.destroyPasses(); } } } /** * Builds and uploads interval metadata from the world state. Called once per * world state change (not every frame). * * @param {GSplatWorldState} worldState - The world state to extract intervals from. */ uploadIntervals(worldState) { if (worldState.version === this._uploadedVersion) return; this._uploadedVersion = worldState.version; const splats = worldState.splats; const numIntervals = worldState.totalIntervals; if (numIntervals === 0) return; if (numIntervals > this.allocatedIntervalCount) { this.intervalsBuffer?.destroy(); this.allocatedIntervalCount = numIntervals; this.intervalsBuffer = new StorageBuffer(this.device, numIntervals * INTERVAL_STRIDE * 4, BUFFERUSAGE_COPY_DST); DebugHelper.setName(this.intervalsBuffer, "GsplatIntervalCompaction.intervals"); } const data = new Uint32Array(numIntervals * INTERVAL_STRIDE); let writeIdx = 0; for (let s = 0; s < splats.length; s++) { const splat = splats[s]; if (splat.intervals.length > 0) { const nodeIndices = splat.intervalNodeIndices; for (let i = 0; i < splat.intervals.length; i += 2) { const count = splat.intervals[i + 1] - splat.intervals[i]; data[writeIdx++] = splat.intervalOffsets[i / 2]; data[writeIdx++] = count; data[writeIdx++] = splat.boundsBaseIndex + (nodeIndices.length > 0 ? nodeIndices[i / 2] : 0); data[writeIdx++] = 0; } } else { data[writeIdx++] = splat.intervalOffsets[0]; data[writeIdx++] = splat.activeSplats; data[writeIdx++] = splat.boundsBaseIndex; data[writeIdx++] = 0; } } this.intervalsBuffer.write(0, data, 0, numIntervals * INTERVAL_STRIDE); } /** * Runs the full interval compaction pipeline: cull+count, prefix sum, scatter. * * @param {GSplatFrustumCuller} frustumCuller - Frustum culler providing bounds/transforms storage buffers and frustum planes. * @param {number} numIntervals - Total number of intervals. * @param {number} totalActiveSplats - Total active splats across all intervals. * @param {boolean} fisheyeEnabled - Whether fisheye cone culling should be used instead of frustum planes. */ dispatchCompact(frustumCuller, numIntervals, totalActiveSplats, fisheyeEnabled) { if (numIntervals === 0) return; this._ensureCapacity(numIntervals, totalActiveSplats); const cullCompute = this._getCullCompute(fisheyeEnabled); cullCompute.setParameter("intervals", this.intervalsBuffer); cullCompute.setParameter("countBuffer", this.countBuffer); cullCompute.setParameter("boundsBuffer", frustumCuller.boundsBuffer); cullCompute.setParameter("transformsBuffer", frustumCuller.transformsBuffer); if (fisheyeEnabled) { cullCompute.setParameter("cameraWorldPos", frustumCuller.fisheyeCameraPos); cullCompute.setParameter("maxTheta", frustumCuller.fisheyeMaxTheta); cullCompute.setParameter("cameraForward", frustumCuller.fisheyeCameraForward); } else { cullCompute.setParameter("frustumPlanes[0]", frustumCuller.frustumPlanes); } cullCompute.setParameter("numIntervals", numIntervals); const cullWorkgroups = Math.ceil(numIntervals / WORKGROUP_SIZE); cullCompute.setupDispatch(cullWorkgroups); this.device.computeDispatch([cullCompute], "GSplatIntervalCull"); const prefixCount = numIntervals + 1; this.prefixSumKernel.resize(this.countBuffer, prefixCount); this.prefixSumKernel.dispatch(this.device); const scatterCompute = this._scatterCompute; scatterCompute.setParameter("intervals", this.intervalsBuffer); scatterCompute.setParameter("prefixSumBuffer", this.countBuffer); scatterCompute.setParameter("compactedOutput", this.compactedSplatIds); scatterCompute.setParameter("numIntervals", numIntervals); scatterCompute.setParameter("pad0", 0); scatterCompute.setParameter("pad1", 0); scatterCompute.setParameter("pad2", 0); scatterCompute.setupDispatch(numIntervals); this.device.computeDispatch([scatterCompute], "GSplatIntervalScatter"); } /** * Writes indirect draw and dispatch arguments from the prefix sum visible count. * * @param {number} drawSlot - Slot index in the device's indirect draw buffer. * @param {number} dispatchSlotBase - Base slot index in the device's indirect * dispatch buffer. Key-gen args go to `dispatchSlotBase`; sort args to * `dispatchSlotBase + 1` onwards (as described by `sortIndirectInfo`). * @param {number} numIntervals - Total interval count (index into prefix sum for visible count). * @param {Uint32Array} sortIndirectInfo - Sorter-owned 4-element Uint32 array * returned by `ComputeRadixSort.prepareIndirect()`, used as a `vec4<u32>` * uniform by the shader to drive the `writeSortIndirectArgs` helper. */ writeIndirectArgs(drawSlot, dispatchSlotBase, numIntervals, sortIndirectInfo) { const compute = this._writeIndirectArgsCompute; compute.setParameter("prefixSumBuffer", this.countBuffer); compute.setParameter("indirectDrawArgs", this.device.indirectDrawBuffer); compute.setParameter("numSplatsBuf", this.numSplatsBuffer); compute.setParameter("indirectDispatchArgs", this.device.indirectDispatchBuffer); compute.setParameter("sortElementCountBuf", this.sortElementCountBuffer); compute.setParameter("drawSlot", drawSlot); compute.setParameter("indexCount", INDEX_COUNT); compute.setParameter("dispatchSlotBase", dispatchSlotBase); compute.setParameter("totalSplats", numIntervals); compute.setParameter("sortIndirectInfo", sortIndirectInfo); compute.setupDispatch(1); this.device.computeDispatch([compute], "GSplatIntervalWriteIndirectArgs"); } } export { GSplatIntervalCompaction };