OTOM CFD is a high-performance computational fluid dynamics solver running entirely in the browser via WebGPU. It is heavily inspired by the methodologies popularized by the WaterLily.jl Julia package, specifically bringing Cartesian-grid, boundary-data immersion methods to massive parallel compute shaders.
The software supports both 2D and 3D incompressible Navier-Stokes simulations, a dedicated 2D compressible Euler engine (HLLC Riemann solver) for shock-bearing high-speed flows, multi-phase flows, and high-fidelity volumetric raymarching—all orchestrated through an interactive GUI and an integrated LLM-powered AI assistant.
The simulation is driven by a uniform buffer passed to the GPU every frame. Parameters are split between physical and visual properties.
| Parameter | Domain | Description / Physical Meaning |
|---|---|---|
| Engine Solver | 2D | Incompressible (projection Navier-Stokes, low speed) or Compressible (Euler/HLLC, shocks & high speed). See §4. |
| Grid Resolution | 2D / 3D | Number of computational cells (e.g., 910x512 in 2D, 64³ to 256³ in 3D). Dictates spatial frequency limits. |
| Flow Mode | 2D / 3D | Velocity enforces a constant inlet speed. Pressure drives flow via boundary pressure gradients (\(\Delta P\)). |
| Flow Speed (\(U_\infty\)) | 2D / 3D | Inlet velocity in grid cells per second (or m/s if SI units enabled). |
| Lattice dt (\(\Delta t\)) | 2D / 3D | Time advanced per sub-step. Governs the CFL condition (\(CFL = U \frac{\Delta t}{\Delta x}\)). |
| Physics Steps | 2D / 3D | Number of compute passes executed per visual render frame. Increases stability for high-speed flows. |
| Jacobi Itr | 2D / 3D | Iterations of the Poisson pressure solver. Higher values ensure stricter incompressibility (\(\nabla \cdot \mathbf{u} = 0\)). |
| Viscosity (\(\nu\)) | 2D / 3D | Kinematic viscosity. Defines fluid thickness. Air ~0.015, Water ~0.001. |
| Visual Parameter | Domain | Description |
|---|---|---|
| View Mode | 2D / 3D | Selects the Eulerian field to display: Vorticity, Velocity, Pressure, Phase, or Schlieren (Pressure Gradient). |
| Gain / VScale | 2D / 3D | Multiplicative factor applied before colormapping. Adjusts the sensitivity of the visualizer. |
| Isoline Density | 2D / 3D | Frequency of contour lines superimposed on the field to visualize gradients. |
| Arrow Density | 2D / 3D | Spacing of the vector field arrows. In 3D, this samples a specific 2D slice defined by Slice Depth. |
The core fluid solver uses a staggered Cartesian grid with a fractional-step (Projection) method.
1. Advection: The velocity field is self-advected using a semi-Lagrangian back-tracing scheme. To reduce numerical dissipation, we utilize high-order filtering.
2. Divergence: The divergence of the intermediate velocity field (\(\mathbf{u}^*\)) is calculated.
3. Pressure Poisson: A Jacobi iterative solver resolves the pressure field needed to counteract the divergence.
4. Gradient Subtraction: The velocity is corrected: \(\mathbf{u}^{n+1} = \mathbf{u}^* - \Delta t \nabla p\).
Instead of conforming a mesh to an obstacle, OTOM CFD uses Brinkman Penalization. Solid objects are defined by exact analytical Signed Distance Fields (SDFs). The velocity inside the SDF (\(d \le 0\)) is forced to match the object's velocity, allowing for arbitrary, moving, and overlapping geometries at near-zero computational cost.
Alongside the default incompressible projection solver, OTOM CFD provides a dedicated compressible engine (set Engine Solver = Compressible) for high-speed, transonic, and supersonic flows where density variation and shock waves are physically significant. Instead of enforcing incompressibility, it solves the 2D compressible Euler equations in conservative finite-volume form, so it naturally captures shocks, expansion fans, and contact discontinuities.
The conserved state stored in every cell is \(\mathbf{Q} = [\rho,\ \rho u,\ \rho v,\ E]^T\) (density, x/y momentum, total energy), evolved by the hyperbolic conservation law:
The system is closed by the ideal-gas equation of state, where \(\gamma\) is the ratio of specific heats and \(a\) the local speed of sound:
Each cell is advanced by the net flux across its four faces. The flux at every interface is obtained by solving a local Riemann problem with the HLLC (Harten–Lax–van Leer–Contact) approximate solver, which restores the central contact/shear wave that the simpler HLL solver smears out—essential for clean slip lines and wakes:
The full stencil—four face fluxes plus the conservative update—is fused into a single WebGPU compute pass for robustness and throughput. The scheme is first-order in space and time; a positivity guard floors density and internal energy each step to guarantee \(\rho>0\) and \(p>0\), preventing the NaN blow-up that otherwise occurs across strong shocks.
The explicit time step is bounded by the acoustic CFL condition \(\Delta t < \Delta x / (|u| + a)\). If a strong shock destabilizes the field, lower Lattice dt or raise Physics Steps. The engine exposes four parameters (active only in Compressible mode):
| Parameter | Range | Physical Meaning |
|---|---|---|
| Gamma (\(\gamma\)) | 1.1 – 2.0 | Ratio of specific heats (1.4 for diatomic air). |
| Inlet Mach | 0 – 5.0 | Free-stream Mach number \(M = u/a\). Values > 1 are supersonic. |
| Inlet Density | 0.1 – 5.0 | Free-stream density \(\rho_\infty\). |
| Inlet Pressure | 0.1 – 5.0 | Free-stream static pressure \(p_\infty\). |
Recommended view modes: Schlieren (density gradient \(|\nabla \rho|\), the classic shock-visualization technique), Velocity (rendered as Mach number), or Pressure. Lagrangian particles and streamlines are advected directly by the reconstructed velocity \(\mathbf{u} = (\rho u,\ \rho v)/\rho\), so they stream through and bend around shocks just like the field itself.
The Smagorinsky model calculates sub-grid scale (SGS) stresses. It derives an "eddy viscosity" \(\nu_t\) from the resolved strain rate tensor \(\bar{S}_{ij}\), dissipating energy at the smallest resolvable scales.
To counteract the inherent diffusion of the semi-Lagrangian advection, Vorticity Confinement applies a body force that pushes fluid towards local vorticity maxima, keeping vortices sharp and persistent.
OTOM CFD supports multi-phase simulations (e.g., air and water interaction) by advecting a scalar phase field \(\phi \in [0,1]\).
Inlet (Green) and Outlet (Blue) cells inside the domain. In Pressure Mode, the solver treats these as internal pumps and sinks, allowing for the simulation of complex ducting and internal flows.The 3D solver extends the 2D logic into a texture_3d environment. Rendering is achieved via a single-pass Raymarching fragment shader.
The camera casts rays through the domain bounding box. The shader performs two primary operations per ray:
get_sdf3d function. If hit, it calculates surface normals and applies PBR lighting (Metallic/Roughness/Ambient).Both 2D and 3D modes support massless Lagrangian particles advected by the velocity field via an RK1 or Euler step.
line-strip primitives, creating streaklines.OTOM CFD includes a built-in CFDChatbot that allows users to alter the simulation using natural language. The architecture works as follows:
The agent communicates with the Gemini API using strict JSON schemas (Function Calling). When a user asks "Make the fluid thicker" or "Add a large spinning wing," the LLM maps intent to two primary functions:
update_settings: Maps semantic concepts to internal variables (e.g., "thicker" \(\rightarrow\) viscosity: 0.05, "slower" \(\rightarrow\) inletVel: 5.0).set_obstacles / modify_existing_obstacles: Translates geometry requests into SDF parameters (e.g., obsType: 'Engineering', engShape: 'NACA 0012', angle: 15, scale: 2.0).The Chatbot class intercepts these tool calls and updates the Lil-GUI controllers, which in turn flush the updated data to the WebGPU uniform and storage buffers.
The software calculates Drag (\(C_d\)) and Lift (\(C_l\)) coefficients by integrating pressure and viscous shear forces over the surface of the obstacles.
The Reynolds number is calculated dynamically based on the largest obstacle's chord length \(L\):
Analytics can be exported as structured text or baked into PDF reports with canvas screenshots using the GUI tools.
The AI tab (brain icon in the left sidebar, directly below Demo) provides a machine-learning workflow for training and running neural-network surrogate models that approximate the physics solver. A surrogate learns the solver's behaviour from data and, once trained, can advance the flow with a single network evaluation instead of the full iterative solve. This is offered as an experimental research feature: surrogates trade physical fidelity for speed, and are most interesting in 3D where the iterative pressure solve is the dominant cost.
Rather than predicting the next flow field directly, the networks are trained to predict the residual — the change in the field over one solver step — which the simulation then integrates:
where \( \mathbf{q} \) is the stacked velocity/pressure state, \( \text{mask} \) is the obstacle occupancy, \( Re \) is the (normalised) Reynolds number, and \( \mathcal{N}_\theta \) is the trained network. Predicting the residual keeps the targets small and well-conditioned and improves step-to-step stability. All inputs and outputs are normalised internally by fixed reference scales so the network operates on \(\mathcal{O}(1)\) values.
Resolution lock: a network's weights encode the physical scale of the grid it trained on, so each model is bound to a single grid resolution. Applying a model automatically sets the simulation to that grid. To run at a different resolution you train a separate model — resolution is part of a model's identity, not a runtime knob.
Trains a compact U-Net (three encoder/decoder levels with skip connections, ~tens of thousands of parameters) on the 2D incompressible solver using TensorFlow.js. The data pipeline drives the real GPU solver headlessly across randomised parameters, captures (state, next-state) pairs near quasi-steady state, and converts them to normalised residual targets. Controls:
Collecting…, then Epoch n — loss …, then Saved ….2D inference uses a CPU round-trip (read the field off the GPU, evaluate the network in TensorFlow.js, write the result back). At small 2D grids the data volume is tiny, so this stays real-time.
Trains a small fully-convolutional 3D CNN (a flat stack of 3×3×3 convolutions, 8 → 16 → 16 channels, plus a 1×1×1 output convolution — deliberately not a U-Net so the GPU inference path needs only one kernel type). The data pipeline runs the real 3D incompressible solver headlessly, reads back the velocity and pressure volumes, and bakes the obstacle's occupancy mask as an additional input channel. Controls:
32, 64, 128, 256, 512 (the same set the 3D solver offers, so an applied model always lands on a valid sim resolution).Input channels (6): \(v_x, v_y, v_z\), pressure, obstacle occupancy (SDF), normalised Reynolds number. Output channels (4): residual \(\Delta v_x, \Delta v_y, \Delta v_z, \Delta p\).
Unlike the 2D path, the trained 3D network runs entirely on the GPU. Its weights are uploaded to GPU buffers and the forward pass executes as a chain of WebGPU compute shaders operating directly on the simulation's velocity/pressure volumes:
The pack stage gathers the velocity, pressure, and obstacle-mask channels into a packed input buffer; each conv3d stage performs one zero-padded ("same") convolution with ReLU; the write-back stage integrates the predicted residual and writes the new velocity and pressure back to the volume textures (zeroing velocity inside solid cells). Because there is no CPU round-trip, the surrogate can in principle outrun the iterative solver — whose 3D cost is dominated by the many Jacobi pressure-Poisson iterations per step. The advantage is largest at higher resolutions; at small grids the physics solver is already inexpensive, so the surrogate may show little or no speed-up.
Correctness self-test. When a 3D model is applied, a one-off check runs the GPU forward pass and compares it against the reference TensorFlow.js evaluation on the identical input, logging the maximum absolute difference to the browser console. A small value (≈\(10^{-3}\) or below) confirms the GPU kernels reproduce the trained network.
My Models lists every saved surrogate with its parameter ranges and final training loss; each row has a trash icon to delete it. Models (weights plus a metadata record) persist in the browser through IndexedDB, so they survive page reloads.
Apply Model selects an Active Model and toggles Enable AI Solver. Enabling a model swaps the surrogate in for the physics solver: a 2D model switches the simulation to 2D incompressible at the model's grid; a 3D model switches to 3D incompressible at the model's grid (open the 3D tab to view the result). Disabling the toggle — or selecting None — instantly restores the real physics solver.
The numerical methods and boundary immersion techniques driving this WebGPU solver heavily draw upon academic research published in top-tier Elsevier journals. For further reading on the mathematics of WaterLily-style models, consult the following literature: