Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@
"webgpu_equirectangular",
"webgpu_fog_height",
"webgpu_furnace_test",
"webgpu_gaussian_splatting",
"webgpu_generator_building",
"webgpu_generator_city",
"webgpu_geometry_loft",
Expand Down
5 changes: 5 additions & 0 deletions examples/jsm/Addons.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,13 @@ export * from './loaders/EXRLoader.js';
export * from './loaders/FBXLoader.js';
export * from './loaders/FontLoader.js';
export * from './loaders/GCodeLoader.js';
export * from './loaders/GLTFGaussianSplatLoaderExtension.js';
export * from './loaders/GLTFLoader.js';
export * from './loaders/HDRLoader.js';
export * from './loaders/HDRCubeTextureLoader.js';
export * from './loaders/IESLoader.js';
export * from './loaders/KMZLoader.js';
export * from './loaders/KSPLATLoader.js';
export * from './loaders/KTX2Loader.js';
export * from './loaders/KTXLoader.js';
export * from './loaders/LDrawLoader.js';
Expand All @@ -114,6 +116,8 @@ export * from './loaders/PDBLoader.js';
export * from './loaders/PLYLoader.js';
export * from './loaders/PVRLoader.js';
export * from './loaders/RGBELoader.js';
export * from './loaders/SPLATLoader.js';
export * from './loaders/SPZLoader.js';
export * from './loaders/UltraHDRLoader.js';
export * from './loaders/STLLoader.js';
export * from './loaders/SVGLoader.js';
Expand Down Expand Up @@ -160,6 +164,7 @@ export * from './modifiers/SimplifyModifier.js';
export * from './modifiers/TessellateModifier.js';

export * from './objects/GroundedSkybox.js';
export * from './objects/GaussianSplatMesh.js';
export * from './objects/Lensflare.js';
export * from './objects/MarchingCubes.js';
export * from './objects/Reflector.js';
Expand Down
271 changes: 271 additions & 0 deletions examples/jsm/gpgpu/CountingSort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { StorageBufferAttribute, DynamicDrawUsage } from 'three/webgpu';
import { Fn, Loop, atomicAdd, atomicLoad, atomicStore, instanceIndex, storage, uint } from 'three/tsl';

/**
* A reusable GPU counting sort.
*
* This computes a stable-ish permutation of the integers `[0, count)` that orders them by an
* arbitrary, user supplied `uint` key ("bin") in the range `[0, binCount)`. It is a good fit for
* approximate ordering of large element counts (hundreds of thousands to millions) where an exact
* comparison sort such as a bitonic sort (see {@link BitonicSort}) would be too slow: a counting
* sort only requires a fixed number of passes (reset, histogram, prefix sum, scatter) regardless of
* `count`, at the cost of only being accurate to the resolution of `binCount` - elements that land
* in the same bin end up in an unspecified relative order.
*
* This class does not compute the sort key itself. Instead, a TSL function is supplied via
* {@link CountingSort#setBinNode} that maps the current `instanceIndex` to a bin, and an equivalent
* plain JavaScript function can be supplied to {@link CountingSort#computeCPU} for platforms without
* compute shader support (e.g. the WebGL backend of {@link WebGPURenderer}).
*
* ```js
* const sort = new CountingSort( count, { binCount: 4096 } );
* sort.setBinNode( () => {
*
* // return a `Node<uint>` bin index for `instanceIndex`, e.g. derived from a depth value.
*
* } );
*
* sort.compute( renderer );
*
* // `sort.orderRead` now holds a storage buffer of `count` indices, ordered by bin.
* ```
*
* @three_import import { CountingSort } from 'three/addons/gpgpu/CountingSort.js';
*/
class CountingSort {

/**
* Constructs a new counting sort.
*
* @param {number} count - The number of elements to sort.
* @param {Object} [options={}] - Options that modify the counting sort.
* @param {number} [options.binCount=4096] - The number of bins/buckets the sort key is quantized into. Larger values improve sort accuracy at the cost of a longer (but still single-pass) prefix sum.
* @param {number} [options.workgroupSize=256] - The workgroup size of the compute shaders executed during the sort.
*/
constructor( count, { binCount = 4096, workgroupSize = 256 } = {} ) {

/**
* The number of elements to sort.
*
* @type {number}
*/
this.count = count;

/**
* The number of bins/buckets the sort key is quantized into.
*
* @type {number}
*/
this.binCount = binCount;

/**
* The workgroup size of the compute shaders executed during the sort.
*
* @type {number}
*/
this.workgroupSize = workgroupSize;

const orderData = new Uint32Array( count );
for ( let i = 0; i < count; i ++ ) orderData[ i ] = i;

/**
* The buffer attribute holding the sorted order (a permutation of `[0, count)`). This is
* also the attribute that is kept up to date by {@link CountingSort#computeCPU}.
*
* @type {StorageBufferAttribute}
*/
this.orderAttribute = new StorageBufferAttribute( orderData, 1, Uint32Array );

const binAttribute = new StorageBufferAttribute( new Uint32Array( count ), 1, Uint32Array );
const histogramAttribute = new StorageBufferAttribute( new Uint32Array( binCount ), 1, Uint32Array );
const offsetAttribute = new StorageBufferAttribute( new Uint32Array( binCount ), 1, Uint32Array );

/**
* A read-only storage node for the sorted order buffer.
*
* @type {StorageBufferNode}
*/
this.orderRead = storage( this.orderAttribute, 'uint', count ).toReadOnly();

/**
* A writable storage node for the sorted order buffer.
*
* @type {StorageBufferNode}
*/
this.orderWrite = storage( this.orderAttribute, 'uint', count );

/**
* A read-only storage node holding each element's bin, computed during the histogram pass.
*
* @type {StorageBufferNode}
*/
this.binRead = storage( binAttribute, 'uint', count ).toReadOnly();

/**
* A writable storage node holding each element's bin.
*
* @type {StorageBufferNode}
*/
this.binWrite = storage( binAttribute, 'uint', count );

/**
* An atomic storage node used to accumulate the per-bin histogram.
*
* @type {StorageBufferNode}
*/
this.histogramAtomic = storage( histogramAttribute, 'uint', binCount ).toAtomic();

/**
* An atomic storage node used both for the exclusive prefix sum of the histogram and, during
* the scatter pass, as a per-bin write cursor.
*
* @type {StorageBufferNode}
*/
this.offsetAtomic = storage( offsetAttribute, 'uint', binCount ).toAtomic();

this._webGLBuffersEnabled = false;

this._cpuBins = new Uint32Array( count );
this._cpuCounts = new Uint32Array( binCount );
this._cpuOffsets = new Uint32Array( binCount );

this._resetNode = null;
this._histogramNode = null;
this._prefixNode = null;
this._scatterNode = null;

}

/**
* Sets the TSL function used to compute the bin of the element currently referenced by
* `instanceIndex`, and (re)builds the compute nodes used by {@link CountingSort#compute}.
*
* @param {Function} binNode - A parameterless function returning a `Node<uint>` in `[0, binCount)`.
*/
setBinNode( binNode ) {

const { binCount, workgroupSize, count } = this;

this._resetNode = Fn( () => {

atomicStore( this.histogramAtomic.element( instanceIndex ), uint( 0 ) );
atomicStore( this.offsetAtomic.element( instanceIndex ), uint( 0 ) );

} )().compute( binCount, [ workgroupSize ] ).setName( 'CountingSortReset' );

this._histogramNode = Fn( () => {

const bin = binNode().toVar( 'bin' );

this.binWrite.element( instanceIndex ).assign( bin );
atomicAdd( this.histogramAtomic.element( bin ), uint( 1 ) );

} )().compute( count, [ workgroupSize ] ).setName( 'CountingSortHistogram' );

this._prefixNode = Fn( () => {

const sum = uint( 0 ).toVar( 'sum' );

Loop( { start: 0, end: binCount, type: 'uint', name: 'bin', condition: '<' }, ( { bin } ) => {

const binCountValue = atomicLoad( this.histogramAtomic.element( bin ) ).toVar( 'count' );
atomicStore( this.offsetAtomic.element( bin ), sum );
sum.addAssign( binCountValue );

} );

} )().compute( 1 ).setName( 'CountingSortPrefix' );

this._scatterNode = Fn( () => {

const bin = this.binRead.element( instanceIndex ).toVar( 'bin' );
const targetIndex = atomicAdd( this.offsetAtomic.element( bin ), uint( 1 ) ).toVar( 'targetIndex' );

this.orderWrite.element( targetIndex ).assign( instanceIndex );

} )().compute( count, [ workgroupSize ] ).setName( 'CountingSortScatter' );

}

/**
* Executes a complete counting sort on the GPU, updating {@link CountingSort#orderRead}.
*
* @param {Renderer} renderer - The current scene's renderer.
*/
compute( renderer ) {

renderer.compute( this._resetNode );
renderer.compute( this._histogramNode );
renderer.compute( this._prefixNode );
renderer.compute( this._scatterNode );

}

/**
* Executes a complete counting sort on the CPU, updating {@link CountingSort#orderAttribute}.
* Intended as a fallback for backends without compute shader support.
*
* @param {Function} binFn - A function taking an element index and returning its bin (a plain number in `[0, binCount)`).
*/
computeCPU( binFn ) {

const { count, binCount } = this;
const order = this.orderAttribute.array;
const bins = this._cpuBins;
const counts = this._cpuCounts;
const offsets = this._cpuOffsets;

counts.fill( 0 );

for ( let i = 0; i < count; i ++ ) {

const bin = binFn( i );

bins[ i ] = bin;
counts[ bin ] ++;

}

let sum = 0;

for ( let i = 0; i < binCount; i ++ ) {

offsets[ i ] = sum;
sum += counts[ i ];

}

for ( let i = 0; i < count; i ++ ) {

order[ offsets[ bins[ i ] ] ++ ] = i;

}

this.orderAttribute.needsUpdate = true;

if ( this.orderAttribute.pbo !== undefined ) {

this.orderAttribute.pbo.needsUpdate = true;

}

}

/**
* Enables the WebGL-specific storage buffer path (PBO + dynamic draw usage) for the order buffer.
* Only needed when {@link CountingSort#computeCPU} is used with the WebGL backend of {@link WebGPURenderer}.
*/
enableWebGLBuffers() {

if ( this._webGLBuffersEnabled === true ) return;

this.orderAttribute.setUsage( DynamicDrawUsage );
this.orderRead.setPBO( true );

this._webGLBuffersEnabled = true;

}

}

export { CountingSort };
Loading