UNPKG

playcanvas

Version:

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

220 lines (219 loc) 7.64 kB
/** * A frame pass that performs GPU-based radix sort using mipmap-based prefix sums. * * This implementation is based on: * - VRChat Gaussian Splatting by MichaelMoroz: https://github.com/MichaelMoroz/VRChatGaussianSplatting * - Mipmap prefix sum trick by d4rkpl4y3r: https://github.com/d4rkc0d3r/CompactSparseTextureDemo * * ## Algorithm Overview * * The sort uses a 4-bit radix (16 buckets) and processes keys in multiple passes, * one pass per 4-bit chunk. Each pass consists of: * * 1. **Count Pass**: For each digit (0-15), count how many keys in each group have that digit. * Output is an R32F texture where each pixel stores a count. Groups are 16 elements. * * 2. **Mipmap Generation**: Generate mipmaps for the count texture using hardware mipmap * generation. This creates a quadtree of counts that enables efficient binary search. * * 3. **Reorder Pass**: For each output position, binary search through the mipmap hierarchy * to find which source element maps to it. The mipmap structure enables O(log N) lookup * per element instead of O(N) linear scan. * * ## Mipmap Prefix Sum Trick * * The key insight is that mipmaps naturally form a quadtree of averages. By writing counts * (e.g., 1.0 for active pixels) into an R32F texture with auto-generated mipmaps: * * - Each mip level stores the average of the 4 pixels below it * - To reconstruct actual counts, multiply by 4^level (i.e., `1 << (level * 2)`) * - This gives us a hierarchical prefix sum structure * * Binary search traversal: * - Start at maxMipLevel and work down to level 0 * - At each level, check 3 quadrants (can skip 4th - if not in first 3, must be in 4th) * - Order: bottom-left → bottom-right → top-left → top-right (Z-order/Morton curve) * - Accumulate prefix sums while descending to find the target element * * The Z-order traversal ensures stable sorting: if element A comes before B in the input, * it remains before B in the output. * * ## Internal Data Layout * * - Internal keys/indices use Morton order (Z-order curve) for better texture cache locality * - Source keys texture uses linear (row-major) layout * - Output sorted indices use linear layout for simple consumer access * * ## Complexity * * - Time: O(N log N) per pass due to mipmap binary search * - Passes: ceil(numBits / 4) passes for numBits-bit keys * - Memory: 2x keys textures + 2x indices textures + 1x prefix sums texture (all power-of-2) * * @category Graphics * @ignore */ export class FramePassRadixSort extends FramePass { /** * The current sorted indices texture (R32U). Access sorted indices using Morton lookup. * * @type {Texture|null} */ _currentIndices: Texture | null; /** * Current number of radix passes. */ _numPasses: number; /** * Current internal texture size (power of 2). */ _internalSize: number; /** * Internal keys texture 0 (ping-pong buffer). * * @type {Texture|null} */ _keys0: Texture | null; /** * Internal keys texture 1 (ping-pong buffer). * * @type {Texture|null} */ _keys1: Texture | null; /** * Internal indices texture 0 (ping-pong buffer). * * @type {Texture|null} */ _indices0: Texture | null; /** * Internal indices texture 1 (ping-pong buffer). * * @type {Texture|null} */ _indices1: Texture | null; /** * Prefix sums texture (R32F with mipmaps). * * @type {Texture|null} */ _prefixSums: Texture | null; /** * Sort render target 0 (MRT for keys + indices). * * @type {RenderTarget|null} */ _sortRT0: RenderTarget | null; /** * Sort render target 1 (MRT for keys + indices). * * @type {RenderTarget|null} */ _sortRT1: RenderTarget | null; /** * Prefix sums render target. * * @type {RenderTarget|null} */ _prefixSumsRT: RenderTarget | null; /** * Count passes for each radix iteration. * * @type {RenderPassRadixSortCount[]} */ _countPasses: RenderPassRadixSortCount[]; /** * Reorder passes for each radix iteration. * * @type {RenderPassRadixSortReorder[]} */ _reorderPasses: RenderPassRadixSortReorder[]; /** * Number of elements to sort (set by setup()). */ _elementCount: number; /** * The source keys texture (set by setup()). * * @type {Texture|null} */ _keysTexture: Texture | null; /** * Gets the sorted indices texture (R32U, linear layout). Use `.width` for texture dimensions. * Access with: `texelFetch(texture, ivec2(index % width, index / width), 0).r` * * @type {Texture|null} */ get sortedIndices(): Texture | null; /** * Sets up the sort for the current frame. * * Note: The source keys texture is read-only and can be any size. * The sorted indices will be in a separate power-of-2 texture. * * @param {Texture} keysTexture - R32U texture containing sort keys (linear layout, any size). * @param {number} elementCount - Number of elements to sort. * @param {number} [numBits] - Number of bits to sort (1-24). More bits = more passes. */ setup(keysTexture: Texture, elementCount: number, numBits?: number): void; /** * Calculates the required power-of-2 texture size for the given element count. * * @param {number} elementCount - Number of elements. * @returns {number} Power-of-2 size. * @private */ private _calculateInternalSize; /** * Creates or resizes internal textures. * * @param {number} size - Power-of-2 size for textures. * @private */ private _resizeInternalTextures; /** * Creates a texture for radix sort. * * @param {string} name - Texture name. * @param {number} size - Texture size. * @param {number} format - Pixel format (PIXELFORMAT_R32U or PIXELFORMAT_R32F). * @param {boolean} [mipmaps] - Whether to generate mipmaps. Defaults to false. * @returns {Texture} The created texture. * @private */ private _createTexture; /** * Destroys internal textures and render targets. * * @private */ private _destroyInternalTextures; /** * Creates the sort passes based on numBits. * Sets up beforePasses with the complete pass sequence (count, mipmap, reorder for each iteration). * * @private */ private _createPasses; /** * Destroys all sort passes. * * @private */ private _destroyPasses; /** * Executes the GPU radix sort. This is a convenience method that combines setup, frameUpdate, * and rendering all passes in one call. * * @param {Texture} keysTexture - R32U texture containing sort keys (linear layout, any size). * @param {number} elementCount - Number of elements to sort. * @param {number} [numBits] - Number of bits to sort (1-24). More bits = more passes. Defaults to 16. * @returns {Texture} The sorted indices texture (R32U, linear layout). */ sort(keysTexture: Texture, elementCount: number, numBits?: number): Texture; } import { FramePass } from '../../platform/graphics/frame-pass.js'; import { Texture } from '../../platform/graphics/texture.js'; import { RenderTarget } from '../../platform/graphics/render-target.js'; import { RenderPassRadixSortCount } from './render-pass-radix-sort-count.js'; import { RenderPassRadixSortReorder } from './render-pass-radix-sort-reorder.js';