import * as THREE from 'three/webgpu'; import type { Renderer } from 'three/webgpu'; import type { GUIController, GUIFolder } from '../../Utils/guiUtils.js'; import { SpatialGrid } from '../Boids/SpatialGrid.js'; import type { SpatialGridOptions } from '../Boids/SpatialGrid.js'; import type { ParticleInteractionWorld, SimulationInteractionOptions } from '../Interaction/GPUInteractionSimulation.js'; import { ParticleFluidSimulation } from '../Fluid/ParticleFluidSimulation.js'; import type { FluidInitializationConfiguration, FluidParticleConfigurationOptions, FluidDomainConfigurationOptions, FluidMaterialConfiguration, FluidNeighborConfigurationOptions, FluidSolverConfigurationOptions, FluidSpatialGridOptions, FluidTimeStepConfigurationOptions } from '../Fluid/FluidConfiguration.js'; import type { FluidComponentsInput } from '../Fluid/FluidInitialization.js'; import type { FluidDiagnosticsOptions } from '../Fluid/FluidDiagnostics.js'; import type { FluidCalibrationMode, FluidCalibrationResult } from '../Fluid/FluidCalibration.js'; import type { BVHVolumeConstraint } from '../BVHVolumeConstraint.js'; import type { SDFVolumeConstraint } from '../SDFVolumeConstraint.js'; import type { TSLFunction, TSLMat4Node, TSLStorageNode, TSLUintNode, TSLUniformNode } from '../../types/tsl.js'; export type PBFConstraintMode = 'compression-only' | 'symmetric'; export type PBFInitialPositions = THREE.StorageBufferAttribute | THREE.StorageInstancedBufferAttribute; export type PBFSpatialGridOptions = SpatialGridOptions | FluidSpatialGridOptions; export interface PBFDebugBuffers { activated: TSLStorageNode<'uint'>; cellId: TSLStorageNode<'uint'>; } export interface PBFBuffers { positions: TSLStorageNode<'vec3'>; velocities: TSLStorageNode<'vec3'>; densities: TSLStorageNode<'float'>; pressures: TSLStorageNode<'float'>; pressureForces: TSLStorageNode<'vec3'>; viscosityForces: TSLStorageNode<'vec3'>; prevPositions: TSLStorageNode<'vec3'>; vorticities?: TSLStorageNode<'vec3'> | undefined; lambdas: TSLStorageNode<'float'>; positionDeltas: TSLStorageNode<'vec3'>; prevVelocities?: TSLStorageNode<'vec3'> | undefined; directions?: TSLStorageNode<'vec3'> | undefined; instanceMatrices: TSLStorageNode<'mat4'> | null; debug?: PBFDebugBuffers | undefined; } export interface PBFSolverBuffers { lambdas: TSLStorageNode<'float'>; positionDeltas: TSLStorageNode<'vec3'>; xsphVelocityDeltas: TSLStorageNode<'vec3'>; vorticities: TSLStorageNode<'vec3'> | null; vorticityVelocityDeltas: TSLStorageNode<'vec3'>; pressureAcceleration?: null | undefined; viscosityAcceleration?: null | undefined; } export interface PBFUniforms { now: TSLUniformNode<'float', number>; deltaTime: TSLUniformNode<'float', number>; domainDimensions: TSLUniformNode<'vec3', THREE.Vector3>; domainCenter: TSLUniformNode<'vec3', THREE.Vector3>; domainMatrix: TSLUniformNode<'mat4', THREE.Matrix4>; domainMatrixInverse: TSLUniformNode<'mat4', THREE.Matrix4>; domainScale: TSLUniformNode<'vec3', THREE.Vector3>; domainObjectMatrixInverse: TSLUniformNode<'mat4', THREE.Matrix4>; mass: TSLUniformNode<'float', number>; h: TSLUniformNode<'float', number>; h2: TSLUniformNode<'float', number>; h6: TSLUniformNode<'float', number>; poly6Kernel: TSLUniformNode<'float', number>; spiky: TSLUniformNode<'float', number>; viscosity: TSLUniformNode<'float', number>; restDensity: TSLUniformNode<'float', number>; pressureStiffness: TSLUniformNode<'float', number>; viscosityMu: TSLUniformNode<'float', number>; restitution: TSLUniformNode<'float', number>; maxSpeed: TSLUniformNode<'float', number>; particleRadius: TSLUniformNode<'float', number>; particleSpacing: TSLUniformNode<'float', number>; friction: TSLUniformNode<'float', number>; timeScale: TSLUniformNode<'float', number>; randomSeed: TSLUniformNode<'vec2', THREE.Vector2>; particleCount: TSLUniformNode<'uint', number>; gravity: TSLUniformNode<'vec3', THREE.Vector3>; lambdaEpsilon: TSLUniformNode<'float', number>; sCorrK: TSLUniformNode<'float', number>; sCorrN: TSLUniformNode<'float', number>; xsphViscosity: TSLUniformNode<'float', number>; vorticityConfinement: TSLUniformNode<'float', number>; deltaQ: TSLUniformNode<'float', number>; rayOrigin: TSLUniformNode<'vec3', THREE.Vector3>; rayDirection: TSLUniformNode<'vec3', THREE.Vector3>; pointerRadius: TSLUniformNode<'float', number>; pointerStrength: TSLUniformNode<'float', number>; pointerEnabled: TSLUniformNode<'float', number>; } export interface PBFDomainBindingOptions { padding?: number | THREE.Vector3 | undefined; autoUpdate?: boolean | undefined; simulationScale?: number | THREE.Vector3 | null | undefined; } export interface PBFOptions { count?: number | undefined; is3D?: boolean | undefined; domainDimensions?: THREE.Vector3 | FluidComponentsInput | undefined; mass?: number | undefined; h?: number | undefined; restDensity?: number | null | undefined; pressureStiffness?: number | undefined; viscosityMu?: number | undefined; restitution?: number | undefined; maxSpeed?: number | undefined; gravity?: THREE.Vector3 | undefined; solverIterations?: number | undefined; lambdaEpsilon?: number | null | undefined; lambdaRelaxation?: number | undefined; corrK?: number | undefined; corrN?: number | undefined; corrDeltaQMultiplier?: number | undefined; debug?: boolean | undefined; useDirection?: boolean | undefined; scaleKernelWithDomain?: boolean | undefined; useSpatialGrid?: boolean | undefined; gridUpdatePerIteration?: boolean | undefined; gridReusePadding?: number | null | undefined; useMatrices?: boolean | undefined; spatialGridOptions?: PBFSpatialGridOptions | null | undefined; spatialGrid?: SpatialGrid | null | undefined; sdfVolumeConstraint?: SDFVolumeConstraint | null | undefined; bvhVolumeConstraint?: BVHVolumeConstraint | null | undefined; initialPositions?: PBFInitialPositions | null | undefined; scatterZeroInitialPositions?: boolean | undefined; fixedTimeStep?: number | null | undefined; maxSubsteps?: number | undefined; maxFrameDelta?: number | undefined; interactionWorld?: ParticleInteractionWorld | null | undefined; interaction?: SimulationInteractionOptions | undefined; timeScale?: number | undefined; initialization?: FluidInitializationConfiguration | null | undefined; calibration?: Readonly | null | undefined; particleRadius?: number | null | undefined; spacing?: number | null | undefined; calibrationMode?: FluidCalibrationMode | undefined; friction?: number | undefined; constraintMode?: PBFConstraintMode | undefined; xsphViscosity?: number | undefined; vorticityConfinement?: number | undefined; boundaryDensitySupport?: boolean | undefined; particles?: FluidParticleConfigurationOptions | null | undefined; material?: FluidMaterialConfiguration | null | undefined; timeStep?: FluidTimeStepConfigurationOptions | null | undefined; domain?: FluidDomainConfigurationOptions | null | undefined; neighbors?: FluidNeighborConfigurationOptions | null | undefined; solverOptions?: FluidSolverConfigurationOptions | null | undefined; diagnostics?: FluidDiagnosticsOptions | null | undefined; readonly [name: string]: unknown; } export interface PBFBoundaryContext extends Record { phase: 'constraint' | 'density-only' | 'post-solve'; iteration?: number | undefined; deltaTime: number; positionFallback?: boolean | undefined; } export interface PBFGUIController extends GUIController { name(label: string): this; } export interface PBFGUIFolder extends GUIFolder { addFolder(name: string): PBFGUIFolder; add(object: PBF, property: '_baseKernelRadius', minimum?: number, maximum?: number, step?: number): PBFGUIController; add(object: TObject, property: TKey, minimum?: number, maximum?: number, step?: number): PBFGUIController; close(): unknown; } export interface PBFGUI { addFolder(name: string): PBFGUIFolder; } export interface PBFGUIFolders { main: PBFGUIFolder; physics: PBFGUIFolder; simulation: PBFGUIFolder; constraints: PBFGUIFolder; domain: PBFGUIFolder; pointer: PBFGUIFolder; } /** * @typedef {Object} PBFBuffers * @memberof PBF * @property {THREE.InstancedBufferAttribute} positions Particle world positions (vec3). * @property {THREE.InstancedBufferAttribute} velocities Particle velocities (vec3). * @property {THREE.InstancedBufferAttribute} densities Computed particle densities (float). * @property {THREE.InstancedBufferAttribute} pressures Stores PBF lambda values per particle (float). * @property {THREE.InstancedBufferAttribute} pressureForces Accumulated position corrections (vec3). * @property {THREE.InstancedBufferAttribute} prevPositions Previous-frame positions used to derive velocities (vec3). * @property {THREE.InstancedBufferAttribute} viscosityForces Shared XSPH/vorticity velocity-delta scratch buffer retained for compatibility (vec3). * @property {THREE.InstancedBufferAttribute} [vorticities] Optional vorticity-confinement curl buffer (vec3). * @property {THREE.InstancedBufferAttribute} [prevVelocities] Previous frame velocities (optional, if `useDirection` enabled). * @property {THREE.InstancedBufferAttribute} [directions] Smoothed directions (optional, if `useDirection` enabled). */ /** * Position Based Fluids (PBF) particle simulation that is purpose-built for GPU * constraint solving. It predicts velocities, enforces incompressibility via * density → lambda → position-delta iterations, and exposes an ergonomic API for * real-time fluid setups in Three Blocks. * * **Features** * - Deterministic velocity prediction + multi-iteration lambda solver with configurable passes * - Spatial grid acceleration (default) or naive O(N²) neighbor search * - 2D and 3D modes with adaptable kernel parameters * - Domain attachment to Three.js objects for dynamic boundary transforms * - Pointer-based interaction for user-driven forces * - Optional direction storage for oriented particle rendering * * **Notes** * - Shares kernel definitions (Poly6, Spiky) with the SPH module for material parity when switching solvers * @fileoverview Position Based Fluids (PBF) particle simulation optimized for GPU compute with optional spatial grid acceleration. * * ```js * import { PBF, sphereImpostorPosition } from 'three-blocks'; * import { TriangleGeometry } from '../helpers/exampleGeometries.js'; * import { sphereImpostorAlpha, sphereImpostorNormal, sphereImpostorShadow } from 'three-blocks/sphere-impostors'; * import { Mesh, MeshStandardNodeMaterial } from 'three/webgpu'; * import { instanceIndex } from 'three/tsl'; * * const pbf = new PBF({ * count: 2000, * is3D: true, * domainDimensions: new THREE.Vector3(20, 20, 20), * h: 1.2, * viscosityMu: 0.15, * useMatrices: false * }); * * // One camera-facing triangle per particle; no instance matrices. * const radius = 0.3; * const material = new MeshStandardNodeMaterial(); * material.positionNode = sphereImpostorPosition({ * position: pbf.buffers.positions.element(instanceIndex), radius, * }); * material.normalNode = sphereImpostorNormal(); * material.opacityNode = sphereImpostorAlpha(); * material.alphaTest = 0.5; * material.castShadowNode = sphereImpostorShadow(); * const mesh = new Mesh(new TriangleGeometry(), material); * mesh.count = pbf.particleCount; * mesh.frustumCulled = false; * mesh.raycast = () => {}; * scene.add(mesh); * * // Simulation loop * async function animate() { * await pbf.step(renderer); * renderer.render(scene, camera); * requestAnimationFrame(animate); * } * ``` * * @class PBF * @short GPU position-based fluids solver with optional spatial grid acceleration and domain binding. * @category Simulation * @tags WebGPU * @demo docs/demos/pbf.html * @see SpatialGrid */ export declare class PBF extends ParticleFluidSimulation { ubos: PBFUniforms; buffers: PBFBuffers; grid: SpatialGrid | null; _domainSize: THREE.Vector3; _positionsArray: Float32Array | undefined; _velocitiesArray: Float32Array | undefined; _prevPositionsArray: Float32Array | undefined; _prev: number | null; debug: boolean; useDirection: boolean; scaleKernelWithDomain: boolean; useMatrices: boolean; gridUpdatePerIteration: boolean; gridReusePadding: number; constraintMode: PBFConstraintMode; xsphViscosity: number; vorticityConfinement: number; boundaryDensitySupport: boolean; solverIterations: number; sdfVolumeConstraint: SDFVolumeConstraint | null; bvhVolumeConstraint: BVHVolumeConstraint | null; interactionWorld: ParticleInteractionWorld | null; interaction: Readonly | null; particleMaxCount: number; friction: number; solverBuffers: PBFSolverBuffers; instanceMatrix: TSLFunction<[], TSLMat4Node>; instanceMatrixNode: (indexNode?: TSLUintNode) => TSLMat4Node; mesh: THREE.Object3D | null; gridCellSize: THREE.Vector3; domainObject: THREE.Object3D | null; folder: PBFGUIFolder | null | undefined; private _autoGridReusePadding; private _baseKernelRadius; private _autoParticleSpacing; private _particleSpacing; private _calibrationMode; private _autoParticleRadius; private _pbfConfig; private _firstStepPending; private _autoRestDensity; private _domainMatrix; private _domainMatrixInverse; private _domainScale; private _domainScaleMax; private _domainCenterLocal; private _domainBounds; private _domainBoundsBase; private _objectMatrixInverse; private _objectMatrix; private _domainPadding; private _domainAutoUpdate; private _scratch; private _gridLastDomainDimensions; private _gridLastKernelRadius; private _copyInitPending; private _computeCopyInit; private _prevVelocitiesArray; private _directionsArray; private _gridOwnsInstance; private _gridOptionsUser; private _interactionUsesParticleRadius; private _interactionKind; private _interactionCompute; private _interactionComputes; private _interactionComputeWorld; private _computeDensity; private _computeLambda; private _computeBoundaryDensitySupport; private _computePositionDelta; private _computeBoundaryPositionDelta; private _applyDelta; private _projectBoxPositions; private _predictPositions; private _finalizePositions; private _computeXSPHDelta; private _applyXSPHDelta; private _computeVorticity; private _computeVorticityConfinementDelta; private _applyVorticityConfinementDelta; private _applyPostSolveBoxVelocityResponse; private _computeInteraction; private _computePostConstraint; /** * @param {Object} [options={}] Configuration options. * @param {number} [options.count=5000] Number of particles to simulate. * @param {boolean} [options.is3D=true] Enable 3D mode; false uses 2D kernels. * @param {THREE.Vector3} [options.domainDimensions] Simulation domain size (default: 32x32x32). * @param {number} [options.mass=0.4] Particle mass. * @param {number} [options.h=1.0] Smoothing kernel radius. * @param {number|null} [options.restDensity=null] Target rest density; null derives a populated-lattice rest state. * @param {number|null} [options.particleRadius=null] Boundary offset radius; null derives approximately `0.48 * spacing`. * @param {number|null} [options.spacing=null] Nominal rest spacing; null derives `0.5 * h`. * @param {string} [options.calibrationMode='discrete-lattice'] Automatic rest-density mode; `self-kernel` uses an isolated-particle reference. * @param {number} [options.pressureStiffness=100.0] Direct pressure-force multiplier. * @param {number} [options.viscosityMu=0.12] Direct viscosity value (unused in the PBF solver). * @param {number} [options.restitution=0.1] Boundary collision restitution. * @param {number} [options.maxSpeed=15.0] Maximum particle velocity. * @param {THREE.Vector3} [options.gravity] Gravity acceleration (default: -9.81 Y). * @param {number|null} [options.fixedTimeStep=1/60] Fixed timestep (null for variable dt). * @param {number} [options.maxSubsteps=5] Max fixed timesteps per frame. * @param {number} [options.maxFrameDelta=0.1] Max frame delta to avoid large jumps. * @param {number} [options.solverIterations=4] Constraint iterations per frame. * @param {number|null} [options.lambdaEpsilon=null] Explicit lambda-denominator epsilon override. * @param {number} [options.lambdaRelaxation=1e-6] Scale-aware lambda relaxation. * @param {boolean} [options.boundaryDensitySupport=true] Add deficit-only mirrored box and curved-boundary density support. * @param {number} [options.vorticityConfinement=0] Optional post-solve vorticity-confinement acceleration strength. * @param {number} [options.corrK=0.001] Artificial pressure strength (sCorr). * @param {number} [options.corrN=4.0] Artificial pressure exponent. * @param {number} [options.corrDeltaQMultiplier=0.3] sCorr reference distance fraction. * @param {boolean} [options.debug=false] Enable debug buffers. * @param {boolean} [options.useDirection=false] Allocate direction buffers. * @param {boolean} [options.scaleKernelWithDomain=true] Scale kernel with domain. * @param {boolean} [options.useSpatialGrid=true] Enable spatial grid acceleration. * @param {boolean} [options.useMatrices=false] Write per-instance matrices. Leave disabled when using `instanceMatrix()` in the material. * @param {boolean} [options.gridUpdatePerIteration=false] Rebuild grid each iteration for strict membership instead of the faster padded per-substep grid. * @param {number|null} [options.gridReusePadding=null] Extra grid cell radius used when reusing one grid across iterations. Defaults to half the smoothing radius. * @param {Object} [options.spatialGridOptions={}] SpatialGrid options. * @param {SpatialGrid} [options.spatialGrid=null] Preconfigured SpatialGrid. * @param {THREE.StorageInstancedBufferAttribute} [options.initialPositions=null] External positions buffer. * @param {boolean} [options.scatterZeroInitialPositions=false] Replace zero-valued sampled positions with deterministic in-domain positions. * @param {SDFVolumeConstraint} [options.sdfVolumeConstraint=null] SDF boundary constraint. * @param {BVHVolumeConstraint} [options.bvhVolumeConstraint=null] BVH boundary constraint. */ constructor(options?: PBFOptions); /** Attach a shared moving-collider interaction world. */ setInteractionWorld(interactionWorld: ParticleInteractionWorld | null, options?: SimulationInteractionOptions): this; /** Disable shared moving-collider interaction. */ clearInteractionWorld(): this; private _applyDomainScaleToBounds; private _reprojectParticles; private _buildCompute; private _updateKernelForScale; private _setDomainScaleValue; private _syncGridDomain; private _getGridLookupRadius; /** * Manually set the simulation domain dimensions. * Updates UBOs and synchronizes the spatial grid. * * @param {THREE.Vector3} dimensions New domain dimensions. * @returns {this} */ setDomainDimensions(dimensions: THREE.Vector3): this; /** * Bind simulation domain to a Three.js object's world-space bounds. * Particles will be constrained to the object's local space and follow its transforms. * * @param {THREE.Object3D} object Target object (mesh or group). * @param {Object} [options={}] Configuration options. * @param {number|THREE.Vector3} [options.padding=0] Extend domain bounds by padding. * @param {boolean} [options.autoUpdate=true] Update domain matrices every frame. * @param {number|THREE.Vector3} [options.simulationScale=null] Override domain scale for visualization. * @returns {this} */ setDomainFromObject(object: THREE.Object3D, { padding, autoUpdate, simulationScale }?: PBFDomainBindingOptions): this; private _updateDomainBoundsFromObject; private _updateDomainMatricesFromObject; private _applySpatialGridInstance; private _createOwnedSpatialGrid; /** * Attach an external `SpatialGrid` instance for neighbor acceleration. * Rebuilds compute passes to use grid-accelerated lookups. * * @param {SpatialGrid} grid External SpatialGrid instance. * @returns {this} * @throws {Error} If grid is not a valid SpatialGrid instance. */ attachSpatialGrid(grid: SpatialGrid): this; /** * Update the PBF smoothing kernel radius (h parameter). * Recomputes kernel coefficients and syncs spatial grid cell size. * * @param {number} radius New smoothing radius (h). * @returns {this} */ setSmoothingRadius(radius: number): this; /** * Set rest density manually. Passing null/undefined re-enables auto rest density. * @param {number|null|undefined} value * @returns {this} */ setRestDensity(value: number | null | undefined): this; /** * Set particle mass. Recomputes auto rest density when enabled. * @param {number} value * @returns {this} */ setMass(value: number): this; private _updateLambdaRelaxation; /** * Enable/disable automatic kernel radius scaling with domain transformations. * * @param {boolean} enabled Whether to scale h with domain scale. * @returns {this} */ setKernelScaleEnabled(enabled: boolean): this; /** * Control whether the spatial grid rebuilds each solver iteration or once per step. * * @param {boolean} enabled When true, rebuild on every solver iteration (slower, more accurate). * @returns {this} */ setGridUpdatePerIteration(enabled: boolean): this; /** * Set conservative padding for a grid reused across solver iterations. * The padding must be at least the maximum cumulative particle correction between rebuilds. * * @param {number} padding Additional neighbor-grid radius. * @returns {this} */ setGridReusePadding(padding: number): this; /** * Create and attach an internal spatial grid for neighbor acceleration. * Automatically configures grid based on current domain and kernel radius. * * @param {Object} [options={}] Options forwarded to `SpatialGrid` constructor. * @returns {this} */ enableSpatialGrid(options?: PBFSpatialGridOptions | null): this; /** * Toggle spatial grid acceleration. * * @param {boolean} enabled Enable (true) or disable (false) spatial grid. * @param {Object} [options] Options passed to `enableSpatialGrid()` if enabling. * @returns {this} */ setSpatialGridEnabled(enabled: boolean, options?: PBFSpatialGridOptions | null | undefined): this; /** * Detach and dispose of the spatial grid, reverting to naive O(N²) neighbor search. * * @returns {this} */ detachSpatialGrid(): this; /** * Synchronize spatial grid configuration with current domain and kernel radius. * Automatically called when domain or kernel changes. * * @returns {this} */ syncSpatialGrid(): this; /** * Advance the simulation by one frame using GPU compute passes. * Executes: grid update → density → pressure → forces → interaction → integration. * * @param {THREE.WebGPURenderer} renderer WebGPU renderer instance. * @returns {Promise} */ step(renderer: Renderer, deltaTime?: number | undefined): Promise; private _hasExternalBoundaryDensitySupport; private _contributeExternalBoundaryDensity; private _projectExternalBoundaries; private _resolveStepConfig; dispose(): void; /** * Attach PBF parameters to a GUI for interactive tuning. * Compatible with lil-gui, dat.gui, and Three.js Inspector. * * @param {Object} gui A lil-gui instance or folder. * @param {Object} [options={}] Configuration options. * @param {string} [options.folderName='PBF'] Name for the main folder. * @param {boolean} [options.open=false] Whether folders start open. * @returns {Object} Object containing created GUI folders: `{ main, physics, simulation, domain, pointer }`. * * @example * const gui = new GUI( { title: 'Simulation' } ); * const pbf = new PBF({ count: 1000 }); * const folders = pbf.attachGUI(gui, { open: true }); * folders.physics.add(pbf.ubos.mass, 'value', 0.1, 2).name('Custom Mass'); */ attachGUI(gui: PBFGUI, { folderName, open }?: { folderName?: string | undefined; open?: boolean | undefined; }): PBFGUIFolders; /** * Detach the PBF parameters GUI. */ disposeGUI(): void; }