import { d } from '../kit'; import { GaussianBrushOptions } from './simShared'; import { ComputeStep, KitComputePipeline } from '../compute'; import { TgpuRoot } from 'typegpu'; /** * The storage-texture form every fluid consumer publishes through: rgba16float, write-only. Half * floats are enough for a velocity or dye field and they give the fragment hardware-bilinear * filtering on the upsample, which a 32-bit format would not on most Android GPUs. */ export type FluidStorageTexture = d.textureStorage2d<'rgba16float', 'write-only'>; /** The solve uniforms every kernel set reads. */ export interface FluidSolveParams { readonly dt: number; readonly curlStrength: number; readonly velFade: number; } /** The additional uniforms the dye passes read. */ export interface FluidDyeParams extends FluidSolveParams { readonly dyeFade: number; readonly colorDecay: number; readonly ambient: number; readonly ambientFreq: number; readonly ambientTime: number; } /** * The union of every layout entry the solver can touch. * * Which entries are actually required depends on the options, and TypeScript cannot express * "`dyeA` is required iff `dye !== 'none'`" across one signature — so the builder takes the core * shape and casts once internally. A layout missing an entry its options need fails at * `tgpu.resolve` time, which every consumer's resolve gate exercises. */ export interface FluidSolverLayout { readonly $: { readonly velA: d.v4f[]; readonly velB: d.v4f[]; readonly pressure: number[]; readonly divergence: number[]; readonly params: FluidSolveParams; }; } /** The standard splat-stamp ABI (`sparams` uniform) the impulse family reads. */ export interface FluidSplatParams { readonly posX: number; readonly posY: number; readonly velX: number; readonly velY: number; readonly radius: number; } /** A solver layout that also carries the splat-stamp uniform. */ export interface FluidImpulseLayout extends FluidSolverLayout { readonly $: FluidSolverLayout['$'] & { readonly sparams: FluidSplatParams; }; } /** The output pass's layout: the dye buffer read-only, plus the storage texture it publishes to. */ export interface FluidOutputLayout { readonly $: { readonly dyeA: d.v4f[]; readonly outTex: FluidStorageTexture; }; } /** * How the grid edges behave. * * - `clamped` — neighbour lookups clamp to the grid, and the divergence pass reflects the wall-normal * velocity at the border (free-slip): the fluid is in a box. * - `toroidal` — neighbour lookups and advection backtraces wrap modulo N: the field is seamless and * has no walls at all. Fog uses this so its cloud has no visible frame. */ export type FluidBoundary = 'clamped' | 'toroidal'; /** * What the dye field carries. * * - `none` — velocity only. The consumer reads the field for something else (ParticleFlow advects * particles through it), so there is no dye to advect, copy or publish. * - `densityAge` — `x` = density, `y` = age in [0, 1]. The fragment colours density by age, which is * what gives smoke its fresh→aged ramp. * - `rgb` — `xyz` = colour. The dye IS the picture (InkFlow). */ export type FluidDyeMode = 'none' | 'densityAge' | 'rgb'; export interface StableFluidsOptions { /** Grid resolution per axis. The kernels are dispatched 2D over N×N. */ n: number; /** * Prefix for every kernel's `$name`. Kernel names appear in the emitted WGSL, so this is what * keeps each consumer's snapshots stable and keeps two fluid shaders in one tree from colliding * on WGSL identifiers (C3). */ namePrefix: string; boundary: FluidBoundary; dye: FluidDyeMode; /** Divide advected dye by `1 + dyeFade·dt` and floor it at zero. Fog's fog is permanent. */ dyeDissipation?: boolean; /** Advance the age channel by `dt · 0.4 · colorDecay` (capped at 1). `densityAge` only. */ ageAdvance?: boolean; /** * Gate the fluid on `maskBuf.x`: cells outside the mask are solid walls in the divergence pass, * and advected velocity/dye is zeroed there. SmokeFill uses it to confine smoke to a shape. * `clamped` only — a toroidal field with interior walls is not a configuration we ship. */ solidMask?: boolean; /** * Clamp the post-projection velocity magnitude to this many grid cells per second. Fog needs it * because a permanent, never-dissipating field can otherwise accumulate enough energy over * minutes to advect further than one cell per step and go unstable. */ velocityCap?: number; /** Also `textureStore` the solved velocity into `velOutTex` from the copy pass. */ publishVelocityTexture?: boolean; /** * Fold an ambient divergence-free breeze into the vorticity pass: an in-place-evolving * curl-noise force added to velocity BEFORE the pressure projection (curl noise is itself * divergence-free, so the projection preserves it). Extends the params ABI with * `ambient`, `ambientFreq`, `ambientTime` f32 members; `gain` scales the force and is baked. * The noise taps hide behind a uniform-valued `ambient > 0` branch, so the mode that never * uses the breeze pays nothing. */ ambientWind?: { gain: number; }; /** Per-frame pressure retention in the divergence pass (the warm start). Default 0.8. */ pressureDecay?: number; } /** * A velocity-only Gaussian impulse splat: kick momentum into the field around the stamp * centre (ADDED, so overlapping stamps build momentum). The dye-less member of the splat * family — pair with `dye: 'none'` solvers. Reads the standard `sparams` splat ABI * (posX/posY/velX/velY/radius). */ export declare function buildVelocityImpulseKernel(layout: FluidImpulseLayout, opts: { n: number; namePrefix: string; }): import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; /** The brush-splat `sparams` ABI: the impulse ABI plus two working-space colour endpoints. */ export interface FluidBrushSplatParams extends FluidSplatParams { readonly colR: number; readonly colG: number; readonly colB: number; readonly col2R: number; readonly col2G: number; readonly col2B: number; /** Mix position between the endpoints (0 degenerates to endpoint A). */ readonly mixT: number; /** Paint-over coverage multiplier on the Gaussian influence. */ readonly strength: number; } /** A brush splat's layout: velocity + dye mutable, plus the per-stamp `sparams` uniform. */ export interface FluidBrushSplatLayout { readonly $: { readonly velA: d.v4f[]; readonly dyeA: d.v4f[]; readonly sparams: FluidBrushSplatParams; }; } /** * A dye BRUSH splat: paint the brush colour over the dye field (a MIX — the newest colour wins, so * hues stay crisp) and kick a velocity impulse, both Gaussian-weighted around the stamp centre. * * The brush colour arrives as TWO endpoints CPU-preconverted into the compile-time working colour * space (`convertP3ToMixSpaceCPU`) plus a mix position; the kernel blends them with the canonical * `mixPreconverted*` variant (which back-converts to P3-linear). One kernel per colour space — * build the variants at module scope (the colorMixing "pre-transpiled variants" idiom). */ export declare function buildBrushSplatKernel(layout: FluidBrushSplatLayout, opts: { n: number; namePrefix: string; colorSpace: number; brush?: GaussianBrushOptions; }): import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; /** The cursor-shove params ABI (the force/emitter kernels that let the mouse push the field). */ export interface FluidCursorForceParams { readonly cursorX: number; readonly cursorY: number; readonly cursorVelX: number; readonly cursorVelY: number; /** 1 while the cursor is actively pushing, 0 otherwise (multiplies the shove). */ readonly mouseActive: number; /** Squared cursor-influence radius in grid cells. */ readonly mouseRadSq: number; } /** The emitter-splat params ABI: a fixed/moving source injecting density+age dye and momentum. */ export interface FluidEmitterParams { readonly dt: number; readonly emitX: number; readonly emitY: number; readonly emitVelX: number; readonly emitVelY: number; /** Emission radius in grid cells (the Gaussian's σ). */ readonly emitRad: number; readonly emitIntensity: number; /** Buoyancy/weight: a y-velocity force proportional to local density. */ readonly gravity: number; } /** An emitter splat's layout: velocity + dye mutable, the per-frame `params` uniform. */ export interface FluidEmitterLayout { readonly $: { readonly velA: d.v4f[]; readonly dyeA: d.v4f[]; readonly params: FluidEmitterParams; }; } export interface EmitterSplatOptions { n: number; namePrefix: string; /** * Fan the injected velocity across an emission cone: cells offset along the perpendicular get * a proportional sideways component (`perpDirX/perpDirY/spreadFactor` ABI members). */ cone: boolean; /** Add a Gaussian cursor shove to the injected velocity (the `FluidCursorForceParams` ABI). */ cursorPush: boolean; /** * Multiply the whole emission by `emitGate` (a runtime 0–1 gate — SmokeFlow's cursor-speed * gate, so a slow graze emits a wisp and a flick a full puff). `cone`/`cursorPush` excluded. */ gated?: boolean; /** Gate emission, gravity and the cursor shove by `maskBuf.x` (pair with `solidMask` solvers). */ masked?: boolean; /** Density injected per second at full influence × intensity. */ densityGain: number; /** Rate (1/s) the local velocity blends toward the injected velocity at full influence. */ velocityBlendGain: number; } /** * A dye EMITTER splat: ADD density around the source (capped at 1), rejuvenate the age channel in * proportion to the fresh material, blend velocity toward the injected (optionally cone-fanned) * velocity, and apply the density-proportional gravity force — the `densityAge` member of the * splat family. Two shapes ship: the cone emitter (fixed source, optional cursor shove, optional * solid mask) and the gated puff (the source IS the cursor; emission scaled by `emitGate`). */ export declare function buildEmitterSplatKernel(layoutIn: FluidEmitterLayout, opts: EmitterSplatOptions): import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; /** * The neighbour flat index for a (row, col) plus a (di, dj) offset, under the given boundary mode. * Exported for consumers whose own kernels need the same lookup (a mask or obstacle pass). */ export declare function neighbourIndex(n: number, boundary: FluidBoundary): import('typegpu').TgpuFn<(ii: d.I32, ji: d.I32, di: d.I32, dj: d.I32) => d.U32>; /** The kernel set for one configuration. Absent members are the ones the options exclude. */ export interface StableFluidsKernels { curl: (cx: number, cy: number) => void; vorticity: (cx: number, cy: number) => void; divergence: (cx: number, cy: number) => void; jacobi: (cx: number, cy: number) => void; gradSubtract: (cx: number, cy: number) => void; advectVel: (cx: number, cy: number) => void; copyVel: (cx: number, cy: number) => void; advectDye?: (cx: number, cy: number) => void; copyDye?: (cx: number, cy: number) => void; } /** * Bake the solver kernels against a consumer's layout. * * Call this at MODULE scope, next to the layout declaration — the kernels are `tgpu.fn`s and want to * exist once per module, not once per component instance. */ export declare function buildStableFluidsKernels(layoutIn: FluidSolverLayout, opts: StableFluidsOptions): StableFluidsKernels; /** * The output pass: publish the dye buffer to a storage texture the fragment samples. * * Separate from `buildStableFluidsKernels` because it reads a DIFFERENT bind group — the dye buffer * bound read-only alongside a write-only storage texture — and a kernel may only reference one * layout. * * `densityAge` writes `(density, age, 0, 0)`; `rgb` writes `(r, g, b, 1)`. Both formats are * rgba16float, so the fragment gets hardware-bilinear filtering for free on the upsample. */ export declare function buildFluidOutputKernel(layout: FluidOutputLayout, opts: { n: number; namePrefix: string; dye: Exclude; }): import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; /** The seeded-field params ABI (the init / ambient-wind / restore kernels read these). */ export interface FluidNoiseFieldParams { readonly dt: number; /** The ambient wind's clock (seconds — advanced by warm-up thunks during a silent warm-up). */ readonly time: number; readonly seed: number; /** Ambient wind strength. */ readonly turbulence: number; /** Restore softness: 0 = distinct dye channels (oil & water), 1 = fully mixed. */ readonly blending: number; } export interface NoiseFieldOptions { n: number; namePrefix: string; /** How many noise lattice cells span the field. Default 3. */ frequency?: number; } /** * Seed the whole state from the fBm pattern: dye `x` = density (biased into a mid range so the * field starts neither empty nor saturated), dye `y` = the raw pattern (the colour-variation * channel the restore kernel later pulls back toward); velocity/pressure/divergence zeroed. * Reads `params.seed` — pair with `seededFieldInit` for the run-once warm-up. */ export declare function buildNoiseFieldInitKernel(layoutIn: FluidSolverLayout, opts: NoiseFieldOptions & { density?: { scale: number; bias: number; min: number; max: number; }; }): import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; /** * An analytic ambient wind: a sum of slowly-drifting incommensurate sine/cosine eddies (a cheap * always-alive stirring force that never repeats visibly), scaled by `params.turbulence`, plus the * Gaussian cursor shove. Dispatched before the solve chain (`ambientForce`). */ export declare function buildTrigTurbulenceKernel(layoutIn: FluidSolverLayout, opts: { n: number; namePrefix: string; gain?: number; }): import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; /** * Counteract numerical diffusion: gently blend the dye's variation channel (`y`) back toward the * seed pattern. `params.blending` sets the character — low boosts the reference's contrast and the * restore rate (distinct channels, sharp boundaries), high leaves the advected mix alone. * Dispatched after the solve chain (`restoreToward`). */ export declare function buildNoiseRestoreKernel(layoutIn: FluidSolverLayout, opts: NoiseFieldOptions): import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; /** The shape-mask params ABI: the shape-space transform + the analytic SDF sub-props + the * volumetric field's aspect-fit domain. A consumer's params struct carries all of them (only the * members the active source reads are live). */ export interface FluidShapeMaskParams { readonly centerX: number; readonly centerY: number; readonly scale: number; readonly rotation: number; readonly aspect: number; readonly saRadius: number; readonly saSides: number; readonly saRounding: number; readonly saInnerRatio: number; readonly saRotation: number; readonly saHeight: number; readonly saOffset: number; readonly saAperture: number; readonly vfOriginX: number; readonly vfOriginY: number; readonly vfSpanX: number; readonly vfSpanY: number; readonly vfActiveRes: number; } /** * The mask-kernel set for one shape-confined fluid: three layouts (analytic / SVG texture / * volumetric field texture — a kernel may reference exactly one) and their kernels, all writing * `(inside, signedDist, 0, 0)` per cell. `paramsSchema` is the consumer's OWN params struct (its * members must include the {@link FluidShapeMaskParams} ABI); the same params buffer binds to * whichever mask layout the shape routing picks. Build at module scope. */ export declare function makeShapeMaskSet(paramsSchema: unknown, opts: { n: number; namePrefix: string; }): { analyticLayout: import('typegpu').TgpuBindGroupLayout<{ maskBuf: { readonly storage: d.WgslArray; readonly access: "mutable"; }; params: { uniform: d.WgslStruct<{ centerX: d.F32; centerY: d.F32; scale: d.F32; rotation: d.F32; aspect: d.F32; saRadius: d.F32; saSides: d.F32; saRounding: d.F32; saInnerRatio: d.F32; saRotation: d.F32; saHeight: d.F32; saOffset: d.F32; saAperture: d.F32; vfOriginX: d.F32; vfOriginY: d.F32; vfSpanX: d.F32; vfSpanY: d.F32; vfActiveRes: d.F32; }>; }; }>; svgLayout: import('typegpu').TgpuBindGroupLayout<{ maskBuf: { readonly storage: d.WgslArray; readonly access: "mutable"; }; params: { uniform: d.WgslStruct<{ centerX: d.F32; centerY: d.F32; scale: d.F32; rotation: d.F32; aspect: d.F32; saRadius: d.F32; saSides: d.F32; saRounding: d.F32; saInnerRatio: d.F32; saRotation: d.F32; saHeight: d.F32; saOffset: d.F32; saAperture: d.F32; vfOriginX: d.F32; vfOriginY: d.F32; vfSpanX: d.F32; vfSpanY: d.F32; vfActiveRes: d.F32; }>; }; sdfSource: { texture: d.WgslTexture2d; }; }>; fieldLayout: import('typegpu').TgpuBindGroupLayout<{ maskBuf: { readonly storage: d.WgslArray; readonly access: "mutable"; }; params: { uniform: d.WgslStruct<{ centerX: d.F32; centerY: d.F32; scale: d.F32; rotation: d.F32; aspect: d.F32; saRadius: d.F32; saSides: d.F32; saRounding: d.F32; saInnerRatio: d.F32; saRotation: d.F32; saHeight: d.F32; saOffset: d.F32; saAperture: d.F32; vfOriginX: d.F32; vfOriginY: d.F32; vfSpanX: d.F32; vfSpanY: d.F32; vfActiveRes: d.F32; }>; }; fieldTex: { texture: d.WgslTexture2d; sampleType: string; }; }>; analytic: (shapeType: string) => import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; svgKernel: import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; fieldKernel: import('typegpu').TgpuFn<(cx: d.U32, cy: d.U32) => d.Void>; }; /** The guarded compute pipelines for one kernel set, plus the ordered chain that drives them. */ export interface StableFluidsPasses { curl: KitComputePipeline; vorticity: KitComputePipeline; divergence: KitComputePipeline; jacobi: KitComputePipeline; gradSubtract: KitComputePipeline; advectVel: KitComputePipeline; copyVel: KitComputePipeline; advectDye?: KitComputePipeline; copyDye?: KitComputePipeline; /** * The core per-frame chain: curl → vorticity → divergence → J× jacobi → gradSubtract → * advectVel → copyVel → advectDye → copyDye. * * Emission splats go BEFORE it, the output pass and any per-shader restoration go after. Pass * `vorticity` to substitute a shader's own variant (ParticleFlow folds an ambient breeze into * that pass) without giving up the rest of the chain. */ solveSteps(opts: { jacobiIters: number; vorticity?: ComputeStep; }): ComputeStep[]; } /** * One kernel as a guarded 2D compute pass over the N×N grid — the same wrapper every consumer's * local `mk` closure was. Exported for the std fluid vocabulary (`std/sim/fluids`), whose module * is not a `'use gpu'` transpilation site. */ export declare function createFluidKernelPass(root: TgpuRoot, kernel: (cx: number, cy: number) => void, opts: { n: number; bindGroup: unknown; }): KitComputePipeline; /** * Wrap a kernel set in guarded compute pipelines bound to the caller's bind group. * * All passes share one bind group and one 2D dispatch size, which is what makes the whole solve a * flat ordered list of dispatches with no per-pass rebinding. */ export declare function createStableFluidsPasses(root: TgpuRoot, kernels: StableFluidsKernels, opts: { n: number; bindGroup: unknown; }): StableFluidsPasses; //# sourceMappingURL=fluids.d.ts.map