UNPKG

playcanvas

Version:

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

271 lines (270 loc) 12 kB
/** * Renders splats using a tiled compute pipeline with per-tile binning and local sorting. * Receives a compacted splat ID list from {@link GSplatIntervalCompaction}, projects each * splat into a projection cache, bins them into screen-space tiles via a fused * count+pair-write pass, classifies tiles by size, sorts each tile by depth (with bucket * pre-sort for large tiles), then rasterizes front-to-back. Pipeline: * * 1. Tile count: project each visible splat, write projection cache (screen pos, conic, * color, depth). Iterate overlapping tiles twice: first to count intersections and * build a 6x5 bitmask, then after a workgroup prefix sum + single global atomicAdd, * to perform capped atomicAdd on per-tile counters and write (tileIdx, localOffset) * pairs into a contiguous pair buffer. Large splats (AABB > 64 tiles) are deferred * to a cooperative pass — their IDs are appended to a largeSplatIds buffer. * 1b. Large tile count: one workgroup (256 threads) per deferred large splat reads * projCache, recomputes the AABB, and cooperatively iterates tiles. Same pair-buffer * and tileSplatCounts writes as pass 1. Sets the high bit of splatPairCount to flag * these splats for the cooperative place-entries pass. * 2. Prefix sum: exclusive prefix sum over per-tile counts produces offsets + total. * 3. Place entries: each thread reads its (tileIdx, localOffset) pairs from the pair * buffer and writes its splat index into tileEntries at deterministic positions * (prefix-summed offset + localOffset). No atomics, no projCache reads. Skips large * splats (high bit of splatPairCount). * 3b. Large place entries: cooperative pass — one workgroup (256 threads) per large * splat, same dispatch as 1b. Reads pairs and writes tileEntries in parallel. * 3.5. Classify: scan tiles, build small/large/rasterize tile lists, assign compact * overflow scratch offsets for large tiles, write indirect args. * 4b. Bucket pre-sort: logarithmic-depth bucket histogram + scatter for large tiles * (>4096 entries), using overflow scratch in the unified tileEntries buffer, packs * whole buckets into <=4096 chunks (indirect dispatch). * 4b.5. Copy chunk sort indirect dispatch args (separate pass for inter-pass barrier). * 4a. Small tile sort: bitonic sort for tiles with 1..4096 entries (indirect dispatch). * 4c. Chunk sort: bitonic sort on each chunk from bucket pre-sort (indirect dispatch). * 5. Rasterize: one workgroup per non-empty tile reads its sorted entry range, loads * from the projection cache via shared memory, and blends front-to-back with * early-out (indirect dispatch). * * The tileEntries buffer is unified: main tile entry lists occupy [0, totalEntries), * overflow scratch for bucket sort occupies [totalEntries, totalEntries + overflowUsed). * Buffer capacity adapts dynamically via async GPU readback of actual usage. * * Supports both color and pick dispatch via two {@link GSplatLocalDispatchSet} instances. * Splat/entry-dependent buffers (projCache, tileEntries) are shared between dispatch sets * with a submitVersion guard to prevent resizing within the same command encoder. * * @ignore */ export class GSplatComputeLocalRenderer extends GSplatRenderer { /** @type {GSplatLocalDispatchSet} */ _mainSet: GSplatLocalDispatchSet; /** @type {GSplatLocalDispatchSet|null} */ _pickSet: GSplatLocalDispatchSet | null; /** @type {FramePassGSplatComputeLocal} */ framePass: FramePassGSplatComputeLocal; /** @type {GSplatTileComposite} */ tileComposite: GSplatTileComposite; /** @type {boolean} */ _needsFramePassRegister: boolean; /** @type {number} */ _textureSize: number; /** @type {number} */ _minPixelSize: number; /** @type {number} */ _minContribution: number; /** @type {number} */ _alphaClip: number; /** @type {number} */ _exposure: number; /** @type {number} */ _numSplats: number; /** @type {StorageBuffer|null} */ _compactedSplatIds: StorageBuffer | null; /** @type {StorageBuffer|null} */ _sortElementCountBuffer: StorageBuffer | null; /** @type {StorageBuffer|null} */ _projCacheBuffer: StorageBuffer | null; /** @type {StorageBuffer|null} */ _tileEntriesBuffer: StorageBuffer | null; /** @type {StorageBuffer|null} */ _pairBuffer: StorageBuffer | null; /** * Packed atomic counters: [0] = global pair counter, [1] = large splat count. * * @type {StorageBuffer|null} */ _countersBuffer: StorageBuffer | null; /** @type {StorageBuffer|null} */ _splatPairStartBuffer: StorageBuffer | null; /** @type {StorageBuffer|null} */ _splatPairCountBuffer: StorageBuffer | null; /** @type {StorageBuffer|null} */ _largeSplatIdsBuffer: StorageBuffer | null; /** @type {number} */ _largeSplatIdsCapacity: number; /** @type {number} */ _allocatedLargeSplatCapacity: number; /** @type {number} */ _allocatedSplatCapacity: number; /** @type {number} */ _allocatedEntryCapacity: number; /** @type {number} */ _tileEntryMultiplier: number; /** @type {number} Last device.submitVersion when shared buffers were resized */ _lastBufferSubmitVersion: number; /** @type {number} Last readback entry count (-1 = consumed / no fresh data) */ _lastReadbackEntryCount: number; /** @type {number} Consecutive frames where usage < half capacity */ _shrinkFrameCount: number; /** @type {number} */ _fisheye: number; /** * Active data source providing format and texture access. When set via {@link setDataSource}, * the renderer reads format and textures from this object instead of the inherited workBuffer. * Defaults to the workBuffer passed to the constructor. * * @type {{ format: GSplatFormat, getTexture: (name: string) => Texture }} * @private */ private _dataSource; /** @type {Shader} */ _placeEntriesShader: Shader; /** @type {BindGroupFormat} */ _placeEntriesBindGroupFormat: BindGroupFormat; /** @type {Shader} */ _placeEntryPrepShader: Shader; /** @type {BindGroupFormat} */ _placeEntryPrepBindGroupFormat: BindGroupFormat; /** @type {StorageBuffer|null} */ _placeEntryPrepDispatchBuffer: StorageBuffer | null; /** @type {Compute|null} */ _placeEntryPrepCompute: Compute | null; /** @type {Shader} */ _largeSplatShader: Shader; /** @type {BindGroupFormat} */ _largeSplatBindGroupFormat: BindGroupFormat; /** @type {Shader} */ _largeSplatPrepShader: Shader; /** @type {BindGroupFormat} */ _largeSplatPrepBindGroupFormat: BindGroupFormat; /** @type {StorageBuffer|null} */ _largeSplatDispatchBuffer: StorageBuffer | null; /** @type {Compute|null} */ _largeSplatPrepCompute: Compute | null; /** @type {Shader} */ _largePlaceEntriesShader: Shader; /** @type {BindGroupFormat} */ _largePlaceEntriesBindGroupFormat: BindGroupFormat; /** @type {Shader} */ _classifyShader: Shader; /** @type {BindGroupFormat} */ _classifyBindGroupFormat: BindGroupFormat; /** @type {Shader} */ _sortShader: Shader; /** @type {BindGroupFormat} */ _sortBindGroupFormat: BindGroupFormat; /** @type {Shader} */ _bucketSortShader: Shader; /** @type {BindGroupFormat} */ _bucketSortBindGroupFormat: BindGroupFormat; /** @type {Shader} */ _copyShader: Shader; /** @type {BindGroupFormat} */ _copyBindGroupFormat: BindGroupFormat; /** @type {Shader} */ _chunkSortShader: Shader; /** @type {BindGroupFormat} */ _chunkSortBindGroupFormat: BindGroupFormat; setRenderMode(renderMode: any): void; frameUpdate(gsplat: any, exposure: any, fogParams: any): void; _fogParams: any; _debugMode: any; _formatHash: any; /** * @param {StorageBuffer} compactedSplatIds - Dense buffer of visible splat IDs. * @param {StorageBuffer} sortElementCountBuffer - Single-u32 buffer with visible count. * @param {number} textureSize - The work buffer texture size. * @param {number} numSplats - Upper bound on visible splats. */ setCompactedData(compactedSplatIds: StorageBuffer, sortElementCountBuffer: StorageBuffer, textureSize: number, numSplats: number): void; /** @private */ private _registerFramePass; /** @private */ private _unregisterFramePass; resizeOutputTexture(width: any, height: any): void; /** * Ensure shared splat/entry buffers are large enough. Only resizes when * device.submitVersion has changed (preventing mid-encoder buffer destruction). * * @param {number} numSplats - Upper bound on visible splats. * @private */ private _ensureSharedBuffers; _depthBuffer: StorageBuffer; /** * Main color dispatch. */ dispatch(): void; /** * Pick dispatch: runs the compute pick pipeline and returns the configured pick mesh instance. * * @param {object} cam - The camera. * @param {number} width - Pick target width. * @param {number} height - Pick target height. * @returns {MeshInstance|null} The pick mesh instance ready for the picker's render list. */ dispatchPick(cam: object, width: number, height: number): MeshInstance | null; /** @type {number} */ _lastDrawSlot: number; /** @type {number} */ _lastNumTilesX: number; /** * Unified dispatch pipeline used by both color and pick paths. * * @param {GSplatLocalDispatchSet} set - The dispatch set to use. * @param {number} width - Render target width. * @param {number} height - Render target height. * @param {boolean} pickMode - Whether this is a pick dispatch. * @private */ private _dispatchPipeline; /** * @param {number} numSplats - Splat count at dispatch time. * @param {number} numTiles - Tile count at dispatch time. * @param {GSplatLocalDispatchSet} set - The dispatch set to readback from. * @private */ private _scheduleReadback; /** * Invalidates all cached count resources on both dispatch sets. Called when the * work buffer format changes, making all compiled count shaders invalid. * * @private */ private _invalidateCountCompute; /** @private */ private _createCommonIncludes; /** @private */ private _createBitonicIncludes; /** * Creates all shared shaders and bind group formats (called once in constructor). * * @private */ private _createSharedShaders; /** * Creates the count shader + shared bind group format. * * @param {boolean} pickMode - Whether to create the pick variant. * @param {boolean} fisheyeEnabled - Whether to include the GSPLAT_FISHEYE define and fisheye uniforms. * @returns {{ shader: Shader, bindGroupFormat: BindGroupFormat }} The shader and format. * @private */ private _createCountShaderAndFormat; /** * Creates a dispatch set with its 8 Compute instances. * * @param {boolean} pickMode - Whether this set is for picking. * @returns {GSplatLocalDispatchSet} The populated dispatch set. * @private */ private _createDispatchSet; } import { GSplatRenderer } from './gsplat-renderer.js'; import { GSplatLocalDispatchSet } from './gsplat-local-dispatch-set.js'; import { FramePassGSplatComputeLocal } from './frame-pass-gsplat-compute-local.js'; import { GSplatTileComposite } from './gsplat-tile-composite.js'; import { StorageBuffer } from '../../platform/graphics/storage-buffer.js'; import { Shader } from '../../platform/graphics/shader.js'; import { BindGroupFormat } from '../../platform/graphics/bind-group-format.js'; import { Compute } from '../../platform/graphics/compute.js'; import type { MeshInstance } from '../mesh-instance.js';