|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
This interactive simulation demonstrates fluid dynamics by solving the incompressible Navier-Stokes equations in real-time. It uses Jos Stam's "Stable Fluids" algorithm, which is the industry standard for real-time smoke and fluid effects in games and visualizations. The Navier-Stokes EquationsThe Navier-Stokes equations describe how fluids (liquids and gases) move. For an incompressible fluid, they are: Momentum Equation: ∂u/∂t = −(u·∇)u + ν∇²u − ∇p + f Incompressibility Constraint: ∇·u = 0 Where:
The Stable Fluids Algorithm (3 Core Steps)Jos Stam's brilliant insight was to split the equation into three sequential steps:
Why "Stable"?Traditional explicit methods "explode" when the time step is too large (the simulation becomes unstable). Stam's method uses implicit integration solved via Gauss-Seidel relaxation, which remains stable regardless of time step size. This is crucial for real-time applications where we can't afford tiny time steps. Controls
Demo Scenarios
FPS:
0
Total Density:
0
Max Velocity:
0.00
Click and Drag on the canvas to add smoke and velocity. Move your mouse quickly to create strong currents!
Algorithm Pipeline (Each Frame)
▸ STEP 1: DIFFUSE (Viscosity & Spreading)
Solves: (1 + 4a)·x[i,j] = x₀[i,j] + a·(x[i±1,j] + x[i,j±1])
Uses Gauss-Seidel relaxation (16 iterations). Higher viscosity = faster velocity decay. Higher diffusion = smoke spreads faster. ▸ STEP 2: PROJECT (Mass Conservation)
Enforces ∇·u = 0 (incompressibility). Calculates divergence, solves Poisson equation ∇²p = ∇·u for pressure, then subtracts pressure gradient from velocity. This is why fluid "pushes around" rather than "through."
▸ STEP 3: ADVECT (Transport)
Semi-Lagrangian backtracing: For each cell, trace backwards along velocity field to find source, then bilinearly interpolate value. This moves smoke with the flow and creates swirling patterns.
Density (Smoke)
Velocity Vectors
Solid Obstacle
Demo Scenario Descriptions
🌬️ Wind Tunnel (CFD/Aircraft)
Constant airflow from left. Add obstacles to see flow separation, vortex shedding, and wake turbulence — exactly what engineers analyze in Computational Fluid Dynamics.
🩸 Pipe Flow (Blood/Medical)
Simulates flow through a channel with walls. Shows parabolic velocity profile (Poiseuille flow) — the same physics governing blood flow in arteries.
🔥 Fire (Games/Movies)
Upward buoyancy force based on density. Hot gases rise, creating realistic flame behavior used in video game and movie VFX.
Interactive Controls
Demo ScenariosThe simulation includes several pre-configured scenarios demonstrating real-world applications:
Obstacle TypesSolid obstacles block fluid flow (velocity = 0 inside). Available shapes:
Wind Speed ControlThe Wind slider (0.5 - 5.0) controls inflow velocity for Wind Tunnel and Pipe scenarios. Higher speeds create more dramatic vortices but may require lower time steps for stability. Understanding the PhysicsViscosity (ν): This is the "internal friction" of the fluid. At the molecular level, it's how much momentum is transferred between adjacent fluid layers. High viscosity fluids (honey, molasses) resist flow. Low viscosity fluids (water, air) flow easily. In the simulation, viscosity causes the velocity field to "blur" over time. Diffusion: This represents how quickly "stuff" in the fluid spreads out due to random molecular motion. For smoke, high diffusion means it disperses into the air quickly. For dye in water, low diffusion means it stays concentrated longer. Incompressibility (∇·u = 0): This constraint says that fluid cannot be created or destroyed at any point — what flows in must flow out. The PROJECT step enforces this by computing a pressure field that "pushes back" against any compression. This is why fluid flows around obstacles rather than through them. Advection: This is the transport of quantities (density, velocity itself) along the flow. The semi-Lagrangian method traces particles backwards in time to find where they came from, then samples the field there. This "backtracking" approach is unconditionally stable — it won't explode even with large time steps. The Gauss-Seidel SolverBoth the diffusion and pressure steps require solving large systems of linear equations. We use the Gauss-Seidel relaxation method: x[i,j] = (x₀[i,j] + a·(x[i-1,j] + x[i+1,j] + x[i,j-1] + x[i,j+1])) / c We iterate this formula 16 times per step. More iterations = more accurate but slower. The beauty of implicit methods is that they're stable even with few iterations. Grid-Based (Eulerian) ApproachThis simulation uses a fixed 128×128 grid where each cell stores:
This is called the Eulerian approach (named after mathematician Leonhard Euler). The alternative is Lagrangian (tracking individual particles), which is used in SPH simulations. Grid-based methods are simpler to implement and understand, which is why they're standard for educational purposes. Boundary ConditionsThe simulation uses reflective boundaries:
Detailed FDM Calculation Process💡 Simple Summary: How It WorksThe simulation has two main parts: 1. Update Velocity Field (FDM on fixed grid)
2. Move "Stuff" Along the Velocity (Advection by interpolation)
Key Insight: We don't track individual particles! The grid is fixed — values flow through it. This is the Eulerian approach (fixed observation points). The alternative, tracking actual moving particles, is called Lagrangian (used in SPH simulations). The Finite Difference Method (FDM) approximates continuous derivatives using discrete grid values. Here's exactly how each step works: Grid SetupWe use a 128×128 grid with 2 extra boundary cells on each side (130×130 total). Each cell is indexed as: IX(i, j) = i + (N + 2) × j where N = 128 Cell (i,j) has neighbors at (i±1, j) and (i, j±1). The grid stores:
STEP 1: DIFFUSE — Solving ∂u/∂t = ν∇²uThe Continuous Equation: ∂u/∂t = ν × ∇²u = ν × (∂²u/∂x² + ∂²u/∂y²) FDM Discretization of Laplacian (∇²): The second derivative is approximated using the 5-point stencil: ∇²u[i,j] ≈ (u[i+1,j] + u[i-1,j] + u[i,j+1] + u[i,j-1] − 4×u[i,j]) / h² where h = 1/N is the grid spacing. Implicit Time Integration: Instead of explicit (u_new = u_old + dt × ν × ∇²u_old), we use implicit: u_new = u_old + dt × ν × ∇²u_new Rearranging with a = dt × ν × N²: (1 + 4a) × x[i,j] = x₀[i,j] + a × (x[i+1,j] + x[i-1,j] + x[i,j+1] + x[i,j-1]) Gauss-Seidel Iteration (16 passes): for (k = 0; k < 16; k++) { // 16 iterations for (j = 1; j <= N; j++) { for (i = 1; i <= N; i++) { x[IX(i,j)] = (x0[IX(i,j)] + a × ( x[IX(i+1,j)] + x[IX(i-1,j)] + x[IX(i,j+1)] + x[IX(i,j-1)] )) / (1 + 4×a); } } set_bnd(b, x); // Apply boundary conditions } STEP 2: PROJECT — Enforcing ∇·u = 0This step ensures mass conservation (incompressibility). Fluid cannot be created or destroyed. Sub-step 2a: Calculate Divergence Divergence measures how much fluid is being "created" at each point: div[i,j] = −0.5 × h × ((Vx[i+1,j] − Vx[i−1,j]) + (Vy[i,j+1] − Vy[i,j−1])) This uses central differences for first derivatives. Sub-step 2b: Solve Poisson Equation for Pressure We need to find pressure p such that: ∇²p = div p[i,j] = (div[i,j] + p[i+1,j] + p[i-1,j] + p[i,j+1] + p[i,j-1]) / 4 Again solved with 16 Gauss-Seidel iterations. Sub-step 2c: Subtract Pressure Gradient Remove the compressive component from velocity:
Vx[i,j] -= 0.5 × N × (p[i+1,j] − p[i−1,j]) STEP 3: ADVECT — Semi-Lagrangian BacktracingThis step moves quantities (density, velocity) along the flow. Instead of pushing particles forward, we trace backwards to find where each value came from. Backtrace Calculation:
x_src = i − dt × N × Vx[i,j] This finds the "source position" — where the particle at (i,j) came from. Bilinear Interpolation: Since (x_src, y_src) usually falls between grid points, we interpolate: i0 = floor(x_src); i1 = i0 + 1; // neighboring grid cells j0 = floor(y_src); j1 = j0 + 1; s1 = x_src − i0; s0 = 1 − s1; // interpolation weights t1 = y_src − j0; t0 = 1 − t1; // Bilinear interpolation formula: d_new[i,j] = s0 × (t0 × d_old[i0,j0] + t1 × d_old[i0,j1]) + s1 × (t0 × d_old[i1,j0] + t1 × d_old[i1,j1]); d[i,j] = s0×t0×d[i0,j0] + s0×t1×d[i0,j1] + s1×t0×d[i1,j0] + s1×t1×d[i1,j1] Display & Rendering LogicThe visualization converts the numerical grid data into pixels on the canvas. Density RenderingFor each grid cell (i,j), we map density to pixel brightness: for (j = 0; j < N; j++) { for (i = 0; i < N; i++) { d = density[IX(i+1, j+1)]; // Get density (skip boundary) color = getColor(d); // Map to RGB based on scheme // Fill SCALE×SCALE pixels (4×4 = 16 pixels per cell) for (sy = 0; sy < SCALE; sy++) { for (sx = 0; sx < SCALE; sx++) { px = i × SCALE + sx; py = j × SCALE + sy; idx = (py × canvasWidth + px) × 4; // RGBA index imageData[idx] = color.r; imageData[idx + 1] = color.g; imageData[idx + 2] = color.b; imageData[idx + 3] = 255; // Alpha } } } } ctx.putImageData(imageData, 0, 0); // Draw all pixels at once Color Schemes
Velocity Vector RenderingWhen "Show Velocity" is enabled, arrows are drawn on top of the density field: for (j = 1; j < N; j += 12) { // Every 12th cell for (i = 1; i < N; i += 12) { vx = Vx[IX(i,j)]; vy = Vy[IX(i,j)]; mag = sqrt(vx² + vy²); if (mag < 0.0005) continue; // Skip tiny vectors // Normalize direction nx = vx / mag; ny = vy / mag; // Arrow length (8-40 pixels) len = min(40, max(8, mag × 50)); // Color by magnitude (green → yellow → red) intensity = min(1, mag × 5); r = 255 × intensity; g = 255 × (1 − 0.5 × intensity); // Draw shaft + arrowhead drawLine(i×SCALE, j×SCALE, endX, endY); drawArrowhead(endX, endY, angle); } } Complete Frame Pipeline
Performance Considerations
Applications of Navier-StokesThe Navier-Stokes equations are fundamental to understanding fluid behavior across many fields. Try the demo scenarios above to explore these applications:
The NACA 2412 AirfoilThe airfoil obstacle uses the NACA 4-digit airfoil system developed by NASA's predecessor (NACA) in the 1930s. The designation "2412" encodes:
This profile is used on aircraft like the Cessna 172 — the most produced aircraft in history. At 12° angle of attack (as shown in simulation), the airfoil generates significant lift but approaches the stall angle where flow separates from the upper surface. The NACA thickness formula: yt = 5t × (0.2969√x − 0.1260x − 0.3516x² + 0.2843x³ − 0.1015x⁴) where t = thickness ratio and x = position along chord (0 to 1). The Millennium Prize ProblemFun fact: Proving whether smooth solutions to the 3D Navier-Stokes equations always exist is one of the seven Millennium Prize Problems in mathematics, with a $1 million prize! We can simulate fluids numerically, but mathematically proving the equations are well-behaved in all cases remains unsolved. Tips for Experimentation
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||