
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.
TArray<float> fields (Density, Vx, Vy) backed by flat 1D storage; 1D-indexed for cache efficiency.UTextureRenderTarget2D each frame via FUpdateTextureRegion2D; sampled by a UMaterialInstanceDynamic on a plane mesh.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.
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);
}
Each cell traces backward along the velocity field to find where the fluid came from; the four surrounding source cells are bilinearly blended.
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);
}