R&D · Fluid Dynamics · UE5 · RenderTarget

Fluid Simulation

Fluid Simulation

A real-time Navier-Stokes fluid simulator running on the CPU in Unreal Engine 5. A 256×256 float grid evolves each frame through a Diffuse → Project → Advect pipeline, written to a UTextureRenderTarget2D and sampled by a dynamic material. Perlin-noise turbulence continuously injects energy to keep the fluid alive.

Navier-Stokes RenderTarget2D Bilinear Advect Perlin Turbulence UE5 C++
What I Built
  • 256×256 CPU grid — three TArray<float> fields (Density, Vx, Vy) backed by flat 1D storage; 1D-indexed for cache efficiency.
  • Diffuse step — Gauss-Seidel relaxation (20 iterations) spreads velocity and density; diffusion rate controlled by a configurable viscosity constant.
  • Project step — iterative divergence correction enforces incompressibility; velocity field becomes divergence-free after projection.
  • Advect step — semi-Lagrangian backward trace; bilinear interpolation samples the four surrounding cells for smooth transport.
  • RenderTarget output — density written to UTextureRenderTarget2D each frame via FUpdateTextureRegion2D; sampled by a UMaterialInstanceDynamic on a plane mesh.

Grid Layout & Step Pipeline

The grid header declares the three float fields and the full step order. Each sub-step mutates in place; the Project step reads and writes the same arrays with Gauss-Seidel convergence.

FluidGrid.h + StepSimulation()
class UFluidGrid : public UActorComponent
{
    static constexpr int32 N = 256;   // grid side
    TArray<float> Density, Vx, Vy;  // flat N×N storage
    TArray<float> Density0, Vx0, Vy0;

    UPROPERTY() UTextureRenderTarget2D*   RenderTarget;
    UPROPERTY() UMaterialInstanceDynamic* DynMaterial;

    void StepSimulation(float dt, float Viscosity, float Diffusion);
    void Diffuse (int b, TArray<float>& x, TArray<float>& x0, float diff, float dt);
    void Project (TArray<float>& vx, TArray<float>& vy, TArray<float>& p, TArray<float>& div);
    void Advect  (int b, TArray<float>& d, TArray<float>& d0, TArray<float>& vx, TArray<float>& vy, float dt);
};

void UFluidGrid::StepSimulation(float dt, float Viscosity, float Diffusion)
{
    Diffuse (1, Vx0, Vx, Viscosity, dt);  Diffuse (2, Vy0, Vy, Viscosity, dt);
    Project (Vx0, Vy0, Vx, Vy);
    Advect  (1, Vx,  Vx0, Vx0, Vy0, dt); Advect  (2, Vy,  Vy0, Vx0, Vy0, dt);
    Project (Vx,  Vy,  Vx0, Vy0);
    Diffuse (0, Density0, Density, Diffusion, dt);
    Advect  (0, Density,  Density0, Vx, Vy, dt);
}
Semi-Lagrangian Advect — Bilinear Interpolation

Each cell traces backward along the velocity field to find where the fluid came from; the four surrounding source cells are bilinearly blended.

FluidGrid.cpp — Advect()
void UFluidGrid::Advect(int b, TArray<float>& d, TArray<float>& d0,
                          TArray<float>& vx, TArray<float>& vy, float dt)
{
    const float dtN = dt * N;
    for (int32 j = 1; j <= N; ++j)
    for (int32 i = 1; i <= N; ++i)
    {
        // Backward trace: where did the fluid at (i,j) come from?
        float x = i - dtN * vx[IX(i,j)];
        float y = j - dtN * vy[IX(i,j)];
        x = FMath::Clamp(x, 0.5f, N + 0.5f);
        y = FMath::Clamp(y, 0.5f, N + 0.5f);

        int32 i0 = (int32)x, i1 = i0 + 1;
        int32 j0 = (int32)y, j1 = j0 + 1;
        float s1 = x - i0, s0 = 1.f - s1;
        float t1 = y - j0, t0 = 1.f - t1;

        // Bilinear blend of four neighbours
        d[IX(i,j)] = s0*(t0*d0[IX(i0,j0)] + t1*d0[IX(i0,j1)])
                   + s1*(t0*d0[IX(i1,j0)] + t1*d0[IX(i1,j1)]);
    }
    SetBnd(b, d);
}
Grid 256 × 256 Algorithm Navier-Stokes (Jos Stam) Pipeline Diffuse → Project → Advect Output UTextureRenderTarget2D Engine Unreal Engine 5 Language C++ Source github.com/khaled71612000 ↗
Connect