import * as THREE from 'three/webgpu'; import type { Camera, ComputeNode, IndirectStorageBufferAttribute, Matrix4, Renderer, Vector2 } from 'three/webgpu'; import { ComputePrefixSum } from './ComputePrefixSum.js'; import { DeferredSurfaceNodeMaterial } from '../Materials/DeferredSurfaceNodeMaterial.js'; import { TriangleGeometry } from '../Geometries/TriangleGeometry.js'; import type { TSLBoolNode, TSLFloatNode, TSLStorageNode, TSLUintNode, TSLUniformNode, TSLUVec2Node, TSLVec2Node, TSLVec3Node } from '../types/tsl.js'; /** Particle center/radius storage consumed by the rasterizer. */ export type ComputeSphereRasterizerParticleStorageNode = TSLStorageNode<'vec4'>; /** Optional RGB or RGBA particle albedo storage. */ export type ComputeSphereRasterizerColorStorageNode = TSLStorageNode<'vec3'> | TSLStorageNode<'vec4'>; /** Construction options for the tiled compute rasterizer. */ export interface ComputeSphereRasterizerOptions { count?: number | undefined; colors?: ComputeSphereRasterizerColorStorageNode | null | undefined; maxTilesPerParticle?: number | undefined; entryMultiplier?: number | undefined; depthSlices?: number | undefined; lodPixelRadius?: number | undefined; maxPixelRadius?: number | undefined; readbackInterval?: number | undefined; } /** Mutable TSL uniforms shared by the binning, raster, and resolve graphs. */ export interface ComputeSphereRasterizerUniforms { viewModelMatrix: TSLUniformNode<'mat4', Matrix4>; cameraWorldMatrix: TSLUniformNode<'mat4', Matrix4>; focal: TSLUniformNode<'vec2', Vector2>; principal: TSLUniformNode<'vec2', Vector2>; near: TSLUniformNode<'float', number>; far: TSLUniformNode<'float', number>; width: TSLUniformNode<'uint', number>; height: TSLUniformNode<'uint', number>; tilesX: TSLUniformNode<'uint', number>; tilesY: TSLUniformNode<'uint', number>; tileCount: TSLUniformNode<'uint', number>; entryCapacity: TSLUniformNode<'uint', number>; particleCount: TSLUniformNode<'uint', number>; modelScale: TSLUniformNode<'float', number>; lodPixelRadius: TSLUniformNode<'float', number>; maxPixelRadius: TSLUniformNode<'float', number>; logNear: TSLUniformNode<'float', number>; sliceScale: TSLUniformNode<'float', number>; } /** Latest CPU-visible allocation and overflow statistics. */ export interface ComputeSphereRasterizerStats { width: number; height: number; tiles: number; tileSize: number; depthSlices: number; particleCapacity: number; particleCount: number; entryCapacity: number; entryCount: number | null; largeCount: number | null; overflowCount: number | null; gpuBytes: number; } /** Single-submit frame batch, including the recursive prefix-sum nodes. */ export interface ComputeSphereRasterizerBatch extends Array { id: string; name: 'csr_frame'; isComputeNode: true; } /** Conservative projected bounds shared by the count and scatter passes. */ export interface ComputeSphereRasterizerScreenBounds { centerView: TSLVec3Node; radius: TSLFloatNode; visible: TSLBoolNode; centerPx: TSLVec2Node; radiusPx: TSLVec2Node; tileMin: TSLUVec2Node; span: TSLUVec2Node; slice: TSLUintNode; } /** Per-pixel nearest-hit nodes exposed by {@link ComputeSphereRasterizer#surfaceNodes}. */ export interface ComputeSphereRasterizerSurfaceNodes { /** True where this pixel resolved a sphere. */ valid: TSLBoolNode; /** Positive linear view distance to the hit. */ viewDistance: TSLFloatNode; /** View-space hit position. */ positionView: TSLVec3Node; /** View-space sphere normal at the hit. */ normalView: TSLVec3Node; /** Capacity-clamped particle id of the hit sphere. */ id: TSLUintNode; } /** Indirect workgroup dispatch storage owned by the rasterizer. */ export interface ComputeSphereRasterizerLargeDispatch { attribute: IndirectStorageBufferAttribute; node: TSLStorageNode<'uint'>; } /** * Software sphere rasterizer: a compute-shader particle renderer that draws opaque * sphere impostors with zero hardware overdraw. * * **Why** * The hardware impostor path (one triangle per particle) pays raster + fragment cost * for every covered sample of every particle, so a million overlapping spheres shade * the same pixels dozens of times. This block replaces that scatter with a tiled * gather: particles are binned into 16×16 pixel tiles on the GPU, then one compute * thread per pixel walks its tile's particle list, keeps the nearest analytic * ray/sphere hit in registers (no atomics in the hot loop, no sorting — opacity makes * the min associative), and a single fullscreen resolve shades each pixel exactly once. * * **Pipeline (one `renderer.compute()` batch per frame)** * 1. clear the (tile × depth-slice) counts * 2. project: view transform + conservative screen ellipse + counting into a * frustum-voxel grid — 16×16 screen tiles × `depthSlices` log-spaced depth slices * keyed by each sphere's nearest depth (spheres overlapping more than * `maxTilesPerParticle` tiles defer to a cooperative workgroup pass) * 3. exclusive prefix sum over the flat grid counts ({@link ComputePrefixSum}) * 4. scatter particle ids — each tile's entry list comes out depth-slice-ordered * 5. raster: per-pixel nearest ray/sphere intersection walking the slices * front-to-back; a pixel breaks as soon as its best hit is closer than the next * slice's lower depth bound, so dense overlap costs O(first opaque surface) * instead of O(everything behind it) * * **Resolve** * The class *is* a fullscreen `THREE.Mesh` whose {@link DeferredSurfaceNodeMaterial} * reconstructs the exact view-space hit point, sphere normal, and fragment depth per * pixel, so native scene lighting (environment IBL, punctual lights, fog) and every * depth/normal-driven post effect (GTAO through `pass()` MRT) keep working. Pixels * without a hit are discarded through the stock alpha-test path, leaving background * depth untouched. * * ```js * import { ComputeSphereRasterizer } from 'three-blocks/water'; * import { instancedArray } from 'three/tsl'; * * const particles = instancedArray( count, 'vec4' ); // xyz = position, w = radius * const colors = instancedArray( count, 'vec3' ); // optional albedo * * const rasterizer = new ComputeSphereRasterizer( particles, { colors } ); * rasterizer.material.roughness = 0.35; * scene.add( rasterizer ); * * function animate() { * * rasterizer.update( renderer, camera ); // binning + raster compute * renderer.render( scene, camera ); // fullscreen resolve draws the spheres * * } * ``` * * **Limitations (v1)** * - WebGPU backend + perspective cameras only (`update()` returns `false` otherwise). * - The compute path does not cast shadow-map shadows (it never runs a light pass); * pair it with the `sphereImpostor*` hardware path when casters are required. * - Non-uniform object scale is unsupported (spheres would become ellipsoids). * * @class ComputeSphereRasterizer * @extends THREE.Mesh * @short Tiled compute-shader rasterizer for opaque sphere-impostor particles. * @category Compute * @tags WebGPU, TSL, Particles, Deferred */ export declare class ComputeSphereRasterizer extends THREE.Mesh { /** Runtime type guard that is always `true` for this rasterizer. */ isComputeSphereRasterizer: true; /** Particle centre-and-radius storage consumed by the compute graph. */ particles: ComputeSphereRasterizerParticleStorageNode; /** Allocated particle capacity. */ capacity: number; /** Active particle count rendered by the next update. */ particleCount: number; /** Optional per-particle colour storage. */ colors: ComputeSphereRasterizerColorStorageNode | null; /** Tile-overlap threshold for the cooperative large-particle path. */ maxTilesPerParticle: number; /** Initial tile-entry capacity multiplier. */ entryMultiplier: number; /** Per-tile depth slices used for front-to-back early termination. */ depthSlices: number; /** Frames between asynchronous counter readbacks. */ readbackInterval: number; /** Mutable TSL uniforms shared by rasterizer compute stages. */ uniforms: ComputeSphereRasterizerUniforms; /** Latest asynchronously read tile-entry count. */ entryCount: number | null; /** Latest asynchronously read large-particle count. */ largeCount: number | null; /** Latest asynchronously read entry-overflow count. */ overflowCount: number | null; _renderer: Renderer | null; _disposed: boolean; _unsupportedWarned: boolean; _frame: number; _readbackPending: boolean; _buffersNeedResize: boolean; _frameBatch: ComputeSphereRasterizerBatch | null; _prefixSum: ComputePrefixSum | null; _width: number; _height: number; _tilesX: number; _tilesY: number; _tileCount: number; _allocTilesX: number; _allocTilesY: number; _allocTileCount: number; _allocEntryCapacity: number; _tileCounts: TSLStorageNode<'uint'> | null | undefined; _tileOffsets: TSLStorageNode<'uint'> | null | undefined; _tileCursors: TSLStorageNode<'uint'> | null | undefined; _tileEntries: TSLStorageNode<'uint'> | null | undefined; _counters: TSLStorageNode<'uint'> | null | undefined; _largeIds: TSLStorageNode<'uint'> | null | undefined; _projected: TSLStorageNode<'vec4'> | null | undefined; _idBuffer: TSLStorageNode<'uint'> | null | undefined; _depthBuffer: TSLStorageNode<'float'> | null | undefined; _largeDispatch: ComputeSphereRasterizerLargeDispatch | null | undefined; _clear: ComputeNode | null | undefined; _project: ComputeNode | null | undefined; _prepareLargeDispatch: ComputeNode | null | undefined; _largeCount: ComputeNode | null | undefined; _copyCounts: ComputeNode | null | undefined; _copyCursors: ComputeNode | null | undefined; _scatter: ComputeNode | null | undefined; _largeScatter: ComputeNode | null | undefined; _raster: ComputeNode | null | undefined; /** * Create a compute sphere rasterizer for a particle storage buffer. * * @param {StorageBufferNode} particles - `vec4` storage node: xyz = center in the * rasterizer's local space, w = radius in local units. * @param {Object} [options={}] - Configuration options. * @param {number} [options.count] - Active particle count (defaults to the buffer * capacity). Mutable later through {@link ComputeSphereRasterizer#particleCount}. * @param {StorageBufferNode} [options.colors=null] - Optional `vec3`/`vec4` albedo * storage node indexed by particle id. * @param {number} [options.maxTilesPerParticle=64] - Tile-overlap threshold above * which a sphere is binned by a cooperative 256-thread workgroup instead of its * own projection thread. * @param {number} [options.entryMultiplier=1.5] - Initial tile-entry capacity as a * multiple of the particle capacity. Grows automatically on overflow readback. * @param {number} [options.depthSlices=32] - Depth slices per tile for the * front-to-back early-termination grid. `1` disables depth ordering. * @param {number} [options.lodPixelRadius=0] - Optional stochastic LOD floor in * pixels: spheres projecting smaller are hash-thinned and the survivors inflated * to the floor size, bounding per-tile entry counts when the cloud collapses to * a few tiles. `0` disables. * @param {number} [options.maxPixelRadius=8192] - Deflation clamp on the projected * pixel radius (entry-capacity safety valve; visually inert at the default). * @param {number} [options.readbackInterval=4] - Frames between async overflow * readbacks. */ constructor(particles: ComputeSphereRasterizerParticleStorageNode, options?: ComputeSphereRasterizerOptions); /** * Target allocation for the tile grid: the current drawing buffer with the display * as the natural cap, so window resizes after the first frame are uniform-only. * @private */ _allocSize(renderer: Renderer): Vector2; /** * Resize the active raster grid to the renderer's drawing buffer. Within the * current allocation this only updates uniforms and dispatch sizes; growing past * it reallocates the buffers and rebuilds the compute graph. * * @param {THREE.Renderer} renderer - The renderer to size against. * @returns {boolean} Whether anything changed. */ resize(renderer: Renderer): boolean; _updateDispatchCounts(): void; _reallocate(allocTilesX: number, allocTilesY: number, entryCapacity: number): void; /** * Shared projection helper: view-space center/radius, LOD thinning, conservative * screen-space ellipse, and the clamped tile bounding box for one particle. * @private */ _screenBounds(index: TSLUintNode): ComputeSphereRasterizerScreenBounds; _buildCompute(): void; /** * Per-pixel nearest-hit nodes for the current frame, for passes that want the * rasterized surface as data rather than as a shaded mesh: a depth source for * screen-space fluid reconstruction, a custom resolve shader, a thickness or * shadow term. Call it inside the graph that consumes it. * * The returned nodes read the rasterizer's per-pixel buffers directly, so the * consuming pass must run at drawing-buffer resolution, and the graph must be * built after the rasterizer has been sized to the real drawing buffer: * growing the tile grid replaces those buffers, and an older graph keeps * reading the discarded pair. * * @returns {ComputeSphereRasterizerSurfaceNodes} Nearest-hit nodes for this pixel. */ surfaceNodes(): ComputeSphereRasterizerSurfaceNodes; /** @private */ _surfaceNodes(): ComputeSphereRasterizerSurfaceNodes; /** * Wire the fullscreen resolve material: per-pixel id/view-distance reads, exact * hit reconstruction along the same camera-ray model the raster used, sphere * normal, and fragment depth in the renderer's depth convention. * @private */ _wireSurface(): void; /** * Run the binning + raster compute batch for the current camera. Call once per * frame before rendering the scene that contains this mesh. * * @param {THREE.Renderer} renderer - WebGPU renderer. * @param {THREE.PerspectiveCamera} camera - The camera the scene will render with. * @returns {boolean} `false` when the environment is unsupported (WebGL backend or * non-perspective camera); the mesh hides itself in that case. */ update(renderer: Renderer, camera: Camera): boolean; /** * Async entry-capacity validation: total attempted entries live at the active * prefix-sum sentinel; a non-zero drop counter grows the multiplier and schedules * a reallocation so the next frames stop truncating tiles. * @private */ _requestOverflowReadback(renderer: Renderer): Promise; /** * Renderer statistics for HUDs and tests. * @returns {Object} Sizes, capacities, and the latest readback counters. */ get stats(): ComputeSphereRasterizerStats; _disposeBuffers(keepSurface?: boolean): void; /** * Dispose GPU buffers, compute pipelines, and the resolve mesh resources. */ dispose(): void; }