FERRETGame engine over Babylon.js
Ferret

@ferret/flow-solver

flow boxes: a solved 2d slice of air or water round bodies (staggered grid, pressure projection) that publishes its wake into the flow field and pushes bodies by surface pressure.

16 exports · source in packages/flow-solver/src

Classes

FlowBox class

class FlowBox
readonly def: FlowBoxDef
readonly name: string
readonly plane: 'xz' | 'xy'
grid: FlowGrid
cell: numberthe cell size actually used (coarser than asked when over maxCells)
degraded: booleantrue when the box had to coarsen to fit its budget
sleeping: booleana sleeping box adds nothing: the analytic field stays (no body, nobody near it)
readonly bodies: Map<string, Placed>
time: numberthe world time of the field in the grid, s
steps: number
readonly oa: number
readonly ob: number
readonly level: number
constructor(def: FlowBoxDef, readonly field: FlowField)
get medium(): Medium
get cells()
dispose()
world(a: number, b: number): V3
contains(p: V3): boolean
addBody(d: BoxBodyDef): void
removeBody(id: string)
setBody(id: string, s: { position: [number, number]; angle?: number; velocity?: [number, number]; omega?: number })move a body: its position, angle and velocity in the plane
maxSpeed(): numberThe largest speed in the grid, m/s.
step(dt: number, t: number, maxSub = 4)Advance by dt at world time t (the box sub-steps so the Courant number stays near one, at most maxSub times).
force(id: string): [number, number]The force of the flow on a body, N, in the two axes of the plane (per metre of slice times the body's depth).
velocityAt(p: V3): V3
pressureAt(p: V3): numberpressure at a point, Pa gauge (zero at the outflow)
dynamicPressure(p: V3): numberdynamic pressure ½·ρ·v², Pa
slice(every = 1): Array<{ at: V3; velocity: V3; speed: number; pressure: number; solid: boolean }>A decimated slice for the inspector: points with velocity, speed and pressure (at most 4096).
wake()
sleep()

packages/flow-solver/src/box.ts:60

FlowGrid class

class FlowGrid
readonly nx: number
readonly ny: number
readonly h: number
readonly density: number
nu: number
confinement: number
iterations: number
tolerance: number
u: Float64Array
v: Float64Array
p: Float64Array
readonly owner: Uint16Array0 fluid, otherwise the id of the body that owns the cell (1 = static colliders)
readonly sx: Float64Arrayvelocity of the solid in a solid cell (the wall's velocity)
readonly sy: Float64Array
sides: [Side, Side, Side, Side]boundary types: left, right, bottom, top; and the velocity prescribed on inflow faces (per face along that side)
readonly inL: Float64Array
readonly inR: Float64Array
readonly inB: Float64Array
readonly inT: Float64Array
divergence: numberdivergence left after the last projection (max over fluid cells, 1/s), iterations used, and the residual
used: number
residual: number
constructor(o: GridOptions)
solid(i: number, j: number): boolean
fluid(i: number, j: number): booleanthe cell is fluid and inside the grid
applyBoundary()Put the values of the boundaries and of the solids on the faces they own.
sampU(x: number, y: number): numberu and v at a cell-unit position, as numbers (no allocation: the advection calls these a million times)
sampV(x: number, y: number): number
velocityCells(x: number, y: number): [number, number]velocity at a point in cell units (x, y from the grid origin / h)
velocityAt(x: number, y: number): [number, number]velocity at a point in metres from the grid origin
pressureAt(x: number, y: number): number
centre(i: number, j: number): [number, number]velocity at the centre of a cell
step(dt: number)
forces(): Map<number, SolidBody>The force the flow puts on every body (per unit depth, N/m): the pressure on the faces that touch the fluid plus the wall shear.

packages/flow-solver/src/grid.ts:25

Functions

applyFlowTier function

function applyFlowTier(field: FlowField, tier: { flowSources: number; flowBoxCells: number }): void

Give a field the source budget of a tier.

packages/flow-solver/src/box.ts:274

flowBoxes function

function flowBoxes(fieldOf: (host: PhysicsHost) => FlowField | undefined): FlowBoxCapability

packages/flow-solver/src/capability.ts:45

flowBoxForms function

function flowBoxForms(br: FormBridge, cap: FlowBoxCapability): void

packages/flow-solver/src/capability.ts:135

flowTierBudget function

function flowTierBudget(tier: { flowSources: number; flowBoxCells: number }, every = 1): { maxSources: number; maxCells: number }

The budget of a hardware tier for the flow: the sources a field takes and the cells a box may have when stepped every every-th physics step.

packages/flow-solver/src/box.ts:270

validateFlowBox function

function validateFlowBox(d: unknown, path = 'flowBoxes'): string[]

packages/flow-solver/src/capability.ts:20

Interfaces

BoxBodyDef interface

interface BoxBodyDef
id: string
shape: BoxShape
depth?: numberextent along the slice normal, m (the force is the force per metre times this); default 1
position?: [number, number]centre in the plane, m (world coordinates of the plane's two axes)
angle?: number
velocity?: [number, number]
omega?: number
static?: booleanstatic colliders are voxelised once

packages/flow-solver/src/box.ts:15

FlowBoxCapability interface

interface FlowBoxCapability extends Capability
readonly boxes: Map<string, { box: FlowBox; def: FlowBoxLevelDef; acc: number }>
add(def: FlowBoxLevelDef): FlowBox
remove(name: string): void

packages/flow-solver/src/capability.ts:39

FlowBoxDef interface

interface FlowBoxDef
name: string
plane?: 'xz' | 'xy'
origin: V3the corner of the box with the smallest coordinates, [x, y, z]; the coordinate off the plane is the slice's level
size: [number, number]extent in the plane, m
cell: numbercell size, m
medium?: Medium
density?: numberkg/m3 and Pa·s; defaults of the medium
viscosity?: number
margin?: numbercells over which the solved field blends into the analytic one at the edge (default 4)
maxCells?: numbercells most the box may have; more are coarsened and degraded is set (default 40000)
confinement?: number
iterations?: number
bodies?: BoxBodyDef[]
forces?: 'solved' | 'coefficients'solved: the pressure on the surfaces drives the bodies; coefficients: only the field is solved (cheap), the media drag stays
reach?: numberhalf-thickness of the slice along its normal beyond which the box adds nothing, m (default 5)

packages/flow-solver/src/box.ts:29

FlowBoxLevelDef interface

interface FlowBoxLevelDef extends Omit<FlowBoxDef, 'bodies'>

The flowbox capability: the flow boxes of a level, stepped in the field phase, with the rigid bodies in them.

Level block physics.flowBoxes: a list of FlowBoxDef where each body may have a node (a rigid body of the level whose position, angle and velocity in the plane the box takes every step, and which the force of the flow is applied to when the box's forces is solved). solved: the pressure and shear on the body's surface drive it and the media drag of a body in an air box is switched off (exempt: ['media']), so nothing is counted twice. coefficients (default): the box only solves the field (the wake for the render and for sails); the media drag stays. A box sleeps while it has no body and nobody wakes it; every steps it every N-th physics step.

bodies?: Array<BoxBodyDef & { node?: string }>
every?: numberstep the solver every N-th physics step with the accumulated time (default 1)

packages/flow-solver/src/capability.ts:14

FormBridge interface

interface FormBridge

Sensors for the language (3.9): (pressure-at x y z), (dynamic-pressure x y z) read the box that contains the point.

define(name: string, sig: { args: string[]; returns?: string }, fn: (ctx: unknown, ...args: any[]) => unknown): unknown

packages/flow-solver/src/capability.ts:134

GridOptions interface

interface GridOptions
nx: number
ny: number
h: number
density: number
nu: numberkinematic viscosity m2/s
confinement?: numbervorticity confinement strength, 0 = off
iterations?: numbermost CG iterations per projection (default 80) and the relative residual to stop at (default 1e-5)
tolerance?: number

packages/flow-solver/src/grid.ts:11

SolidBody interface

interface SolidBody
id: number
fx: number
fy: number
cells: number

packages/flow-solver/src/grid.ts:23

Types

BoxShape type

type BoxShape = | { type: 'circle'; radius: number } | { type: 'box'; half: [number, number] } | { type: 'polygon'; points: Array<[number, number]> }

A flow box: a bounded slice of the level where the flow is solved. It takes its inflow from the analytic sources of the level (@ferret/flow), solves the flow round the bodies in it, and publishes itself back into the field as a solved source, so the media, the vessels, the character, the rotors and the render read the wake without a change on their side. 2d slices: plane: 'xz' is horizontal (the streets of a town, a river seen from above) at height level, plane: 'xy' vertical (the section of a roof or a wing) at depth level.

packages/flow-solver/src/box.ts:10

Side type

type Side = 'wall' | 'inflow' | 'outflow'

The solver of the flow box: incompressible flow on a staggered (MAC) grid in two dimensions. One step is: boundary values, advection (MacCormack with a limiter, so a coarse grid keeps its eddies), viscosity, optional vorticity confinement, and a pressure projection (conjugate gradient with a modified incomplete Cholesky preconditioner, a fixed iteration cap, residual test on the data only). Everything is a function of the data, never of the clock, so two peers with the same history have the same field.

Units are SI: cell size h in m, velocity m/s, density kg/m3, pressure Pa (gauge: zero at the outflow). u lives on the vertical faces ((nx+1) x ny), v on the horizontal faces (nx x (ny+1)), pressure and the solid flags in the cells.

packages/flow-solver/src/grid.ts:10