psemek/examples/weather.cpp

591 lines
19 KiB
C++

#include <psemek/app/application_base.hpp>
#include <psemek/app/default_application_factory.hpp>
#include <psemek/gfx/painter.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/math/camera.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/device.hpp>
#include <psemek/random/uniform_ball.hpp>
#include <psemek/pcg/perlin.hpp>
#include <psemek/pcg/fractal.hpp>
#include <psemek/util/ndarray.hpp>
#include <psemek/log/log.hpp>
using namespace psemek;
auto make_perlin(random::generator & rng, int min_octave, int max_octave, float power)
{
std::vector<pcg::perlin<float, 2>> octaves;
std::vector<float> weights;
random::uniform_sphere_vector_distribution<float, 2> random_vector{};
for (int octave = min_octave; octave < max_octave; ++octave)
{
int size = 1 << octave;
util::ndarray<math::vector<float, 2>, 2> gradients({size + 1, size + 1});
for (auto & g : gradients)
g = random_vector(rng);
octaves.emplace_back(std::move(gradients));
weights.push_back(std::pow(power, - octave));
}
float weight_sum = 0.f;
for (auto w : weights)
weight_sum += w;
for (auto & w : weights)
w /= weight_sum;
return pcg::fractal<pcg::perlin<float, 2>>(std::move(octaves), std::move(weights));
}
void make_force_field(random::generator & rng, util::ndarray<math::vector<float, 2>, 2> & result, float scale)
{
auto noise_1 = make_perlin(rng, 2, 6, 2.f);
auto noise_2 = make_perlin(rng, 2, 6, 2.f);
for (int y = 0; y < result.height(); ++y)
{
for (int x = 0; x < result.width(); ++x)
{
math::point p{(x + 0.5f) / result.height(), (y + 0.5f) / result.width()};
result(x, y) = scale * (math::vector{noise_1(p), noise_2(p)} * 2.f - math::vector{1.f, 1.f});
}
}
}
struct weather_app
: app::application_base
{
static constexpr int N = 128;
const float dt = 4.f;
const float viscosity = 0.f;
const float advection_magnification = 1.f;
const float temperature_diffusion = 0.001f;
const float cooling = 0.01f / 300.f;
const float cooling_factor = std::exp(- cooling * dt);
const float heating = 323.f * (std::exp(cooling * dt) - 1.f) / dt;
const float coriolis = 0.01f;
const float coriolis_bands = 2.f;
const float friction = 0.f;
const float slope_force = 0.04f;
const float vorticity_confinement = 0.f;
const float force_field_amplitude = 0.00005f;
const float force_field_switch_duration = 720.f * 7.5f; // 7.5 days
const int force_field_switch_frames = std::round(force_field_switch_duration / dt);
// const float friction_factor = 1.f - std::exp(- friction * dt);
const bool periodic_x = true;
random::generator rng{random::device{}};
// random::generator rng{0, 0};
float expected_temperature_at(int y)
{
// float latitude = (y - N * 0.5f) * 2.f / N;
// return std::cos(latitude * float(math::pi));
return temperature_income_at(y) * dt / (std::exp(cooling * dt) - 1.f);
}
float temperature_income_at(int y)
{
float latitude = (y - N * 0.5f) * 2.f / N;
// return heating * math::lerp(0.75f, 1.f, std::cos(latitude * float(math::pi) / 2.f));
return heating * math::lerp(0.65f, 1.f, 1.f - std::abs(latitude));
}
weather_app(options const &, context const &)
{
simulation_box_ = {{{0.f, N}, {0.f, N}}};
terrain_.resize({N, N}, 0.f);
velocity_.resize({N, N});
new_velocity_.resize({N, N});
pressure_.resize({N, N}, 0.f);
temperature_.resize({N, N}, -1.f);
new_temperature_.resize({N, N});
average_temperature_.resize({N, N}, 0.f);
force_field_main_.resize({N, N});
force_field_current_.resize({N, N});
force_field_next_.resize({N, N});
vorticity_.resize({N, N});
// random::generator rng{0, 0};
random::uniform_ball_vector_distribution<float, 2> random_velocity{};
// for (auto & v : velocity_)
// {
// v = random_velocity(rng) * 0.01f;
// v += std::cos(0.5f * float(math::pi) * latitude * coriolis_bands
// }
for (int y = 0; y < N; ++y)
{
for (int x = 0; x < N; ++x)
{
float latitude = (N * 0.5f - y) * 2.f / N;
velocity_(x, y) = random_velocity(rng) * 0.f + 0.f * math::vector{-std::cos(0.5f * float(math::pi) * latitude * coriolis_bands), 0.f};
temperature_(x, y) = expected_temperature_at(y);
}
}
auto terrain_noise = make_perlin(rng, 2, 10, 1.6f);
for (int y = 0; y < N; ++y)
{
for (int x = 0; x < N; ++x)
{
auto d = math::length(math::vector{x - N / 2.f, y - N / 2.f}) / (N / 2.f);
(void)d;
float value = terrain_noise((x + 0.5f) / N, (y + 0.5f) / N);
value = pow(value, 4.f) - d / 4.f;
terrain_(x, y) = std::max(0.f, math::lerp(1.f, 16.f, value));
}
}
make_force_field(rng, force_field_main_, 0.5f);
make_force_field(rng, force_field_next_, 0.5f);
}
void on_event(app::key_event const & event) override
{
if (event.down && event.key == app::keycode::SPACE)
paused_ ^= true;
if (event.down && event.key == app::keycode::V)
show_velocity_ ^= true;
if (event.down && event.key == app::keycode::T)
show_temperature_ ^= true;
if (event.down && event.key == app::keycode::D)
show_temperature_delta_ ^= true;
if (event.down && event.key == app::keycode::A)
show_average_temperature_delta_ ^= true;
if (event.down && event.key == app::keycode::P)
show_pressure_ ^= true;
if (event.down && event.key == app::keycode::H)
show_land_ ^= true;
}
void update() override
{
if (paused_)
return;
// Update force field
if ((frame_ % force_field_switch_frames) == 0)
{
std::swap(force_field_current_, force_field_next_);
make_force_field(rng, force_field_next_, 0.5f);
}
float const force_field_t = ((frame_ % force_field_switch_frames) + 0.5f) / force_field_switch_frames;
// Temperature source
for (int y = 0; y < N; ++y)
{
for (int x = 0; x < N; ++x)
{
// temperature_(x, y) = math::lerp(temperature_(x, y), expected_temperature_at(y), 1.f - std::exp(- heating * dt));
temperature_(x, y) += dt * temperature_income_at(y);
temperature_(x, y) *= cooling_factor;
}
}
auto wrap = [](int i)
{
return (i + N) % N;
};
int xmin = periodic_x ? 0 : 1;
int xmax = periodic_x ? N : N - 1; // exclusive
// Velocity & temperature advection
for (int i = 0; i < N; ++i)
{
new_temperature_(i, 0) = temperature_(i, 0);
new_temperature_(i, N - 1) = temperature_(i, N - 1);
if (!periodic_x)
{
new_temperature_(0, i) = temperature_(0, i);
new_temperature_(N - 1, i) = temperature_(N - 1, i);
}
}
for (int y = 1; y < N - 1; ++y)
{
for (int x = xmin; x < xmax; ++x)
{
auto v = velocity_(x, y);
auto p = math::point{x + 0.5f, y + 0.5f} - (advection_magnification * dt) * v;
p[0] = p[0] - 0.5f;
p[1] = math::clamp(p[1] - 0.5f, {0.f, N - 1.f});
if (!periodic_x)
p[0] = math::clamp(p[0], {0.f, N - 1.f});
int ix = std::floor(p[0]);
int iy = std::min<int>(N - 1, std::floor(p[1]));
if (!periodic_x)
ix = std::min(N - 1, ix);
float tx = p[0] - ix;
float ty = p[1] - iy;
new_velocity_(x, y) = math::lerp(
math::lerp(velocity_(wrap(ix + 0), iy + 0), velocity_(wrap(ix + 1), iy + 0), tx),
math::lerp(velocity_(wrap(ix + 0), iy + 1), velocity_(wrap(ix + 1), iy + 1), tx),
ty
);
new_temperature_(x, y) = math::lerp(
math::lerp(temperature_(wrap(ix + 0), iy + 0), temperature_(wrap(ix + 1), iy + 0), tx),
math::lerp(temperature_(wrap(ix + 0), iy + 1), temperature_(wrap(ix + 1), iy + 1), tx),
ty
);
}
}
std::swap(velocity_, new_velocity_);
std::swap(temperature_, new_temperature_);
// Apply velocity diffusion
for (int y = 0; y < N; ++y)
for (int x = 0; x < N; ++x)
new_velocity_(x, y) = velocity_(x, y);
for (int y = 1; y < N - 1; ++y)
{
for (int x = xmin; x < xmax; ++x)
{
// Velocity Laplacian
auto laplacian = velocity_(wrap(x + 1), y) + velocity_(wrap(x - 1), y) + velocity_(x, y + 1) + velocity_(x, y - 1) - 4.f * velocity_(x, y);
new_velocity_(x, y) = velocity_(x, y) + viscosity * dt * laplacian;
}
}
std::swap(velocity_, new_velocity_);
// Apply temperature diffusion
for (int y = 0; y < N; ++y)
for (int x = 0; x < N; ++x)
new_temperature_(x, y) = temperature_(x, y);
for (int y = 1; y < N - 1; ++y)
{
for (int x = xmin; x < xmax; ++x)
{
// Temperature Laplacian
auto laplacian = temperature_(wrap(x + 1), y) + temperature_(wrap(x - 1), y) + temperature_(x, y + 1) + temperature_(x, y - 1) - 4.f * temperature_(x, y);
new_temperature_(x, y) = temperature_(x, y) + temperature_diffusion * dt * laplacian;
}
}
std::swap(temperature_, new_temperature_);
// Compute vorticity
for (int y = 1; y < N - 1; ++y)
for (int x = xmin; x < xmax; ++x)
vorticity_(x, y) = (velocity_(x, y + 1)[0] - velocity_(x, y - 1)[0]) / 2.f - (velocity_(wrap(x + 1), y)[1] - velocity_(wrap(x - 1), y)[1]) / 2;
// Apply forces & friction
for (int y = 1; y < N - 1; ++y)
{
for (int x = xmin; x < xmax; ++x)
{
float latitude = (N * 0.5f - y) * 2.f / N;
// float latitude = (N - y) * 1.f / N;
// velocity_(x, y) += math::ort(velocity_(x, y)) * (coriolis * dt * std::sin(0.5f * float(math::pi) * latitude * coriolis_bands));
velocity_(x, y) = math::rotate(velocity_(x, y), coriolis * dt * std::sin(0.5f * float(math::pi) * latitude * coriolis_bands));
auto force = force_field_main_(x, y) + math::lerp(force_field_current_(x, y), force_field_next_(x, y), force_field_t);
velocity_(x, y) += (dt * force_field_amplitude) * force;
math::vector terrain_gradient
{
(terrain_(x + 1, y) - terrain_(x - 1, y)) / 2.f,
(terrain_(x, y + 1) - terrain_(x, y - 1)) / 2.f,
};
[[maybe_unused]] float slope_factor = std::exp(- dt * slope_force * math::dot(math::normalized(velocity_(x, y)), terrain_gradient));
velocity_(x, y) *= std::min(1.f, slope_factor);
// velocity_(x, y) -= terrain_gradient * slope_force * dt;
// Directional external force
// velocity_(x, y)[1] += 0.001f * dt * std::sin(0.5f * float(math::pi) * latitude * coriolis_bands);
// velocity_(x, y) += math::direction(frame_ * dt * 2.f * float(math::pi) / 10080.f) * 0.001f * dt;
math::vector vorticity_gradient
{
(vorticity_(wrap(x + 1), y) - vorticity_(wrap(x - 1), y)) / 2.f,
(vorticity_(x, y + 1) - vorticity_(x, y - 1)) / 2.f,
};
if (auto l = math::length(vorticity_gradient); l > 0.f)
vorticity_gradient /= l;
velocity_(x, y) += vorticity_confinement * dt * vorticity_(x, y) * math::ort(vorticity_gradient);
float local_friction = friction * terrain_(x, y);
// velocity_(x, y) -= local_friction * velocity_(x, y) * math::length(velocity_(x, y));
float local_friction_factor = std::exp(- local_friction * dt);
velocity_(x, y) *= local_friction_factor;
}
}
// Solve Poisson equation for pressure
for (int iteration = 0; iteration < 16; ++iteration)
{
int ymin = ((iteration % 2) == 0) ? 1 : N - 2;
int ymax = ((iteration % 2) == 0) ? N - 1 : 0;
int ystep = ((iteration % 2) == 0) ? 1 : -1;
for (int y = ymin; y != ymax; y += ystep)
{
for (int x = xmin; x < xmax; ++x)
{
// Velocity divergence
float divergence = (velocity_(wrap(x + 1), y)[0] - velocity_(wrap(x - 1), y)[0] + velocity_(x, y + 1)[1] - velocity_(x, y - 1)[1]) / 2.f;
// Gauss-Seidel iteration step
pressure_(x, y) = (pressure_(wrap(x - 1), y) + pressure_(wrap(x + 1), y) + pressure_(x, y - 1) + pressure_(x, y + 1) - divergence) / 4.f;
}
}
}
// Apply boundary conditions for pressure
for (int i = 0; i < N; ++i)
{
if (!periodic_x)
{
pressure_(0, i) = pressure_(1, i);
pressure_(N - 1, i) = pressure_(N - 2, i);
}
pressure_(i, 0) = pressure_(i, 1);
pressure_(i, N - 1) = pressure_(i, N - 2);
}
if (!periodic_x)
{
pressure_(0, 0) = (pressure_(0, 1) + pressure_(1, 0)) / 2.f;
pressure_(N-1, 0) = (pressure_(N-1, 1) + pressure_(N-2, 0)) / 2.f;
pressure_(0, N-1) = (pressure_(0, N-2) + pressure_(1, N-2)) / 2.f;
pressure_(N-1, N-1) = (pressure_(N-1, N-2) + pressure_(N-2, N-1)) / 2.f;
}
// Normalize pressure
float average_pressure = 0.f;
for (auto const & value : pressure_)
average_pressure += value;
average_pressure /= (1.f * N * N);
for (auto & value : pressure_)
value -= average_pressure;
// Project velocity into divergence-free space
// by subtracting pressure gradient
for (int y = 1; y < N - 1; ++y)
{
for (int x = xmin; x < xmax; ++x)
{
// Pressure gradient
math::vector gradient{
(pressure_(wrap(x + 1), y) - pressure_(wrap(x - 1), y)) / 2.f,
(pressure_(x, y + 1) - pressure_(x, y - 1)) / 2.f
};
velocity_(x, y) -= gradient;
}
}
// Apply boundary conditions for velocity
for (int i = 0; i < N; ++i)
{
if (!periodic_x)
{
float left_boundary_flow = 0.f;//0.01f * std::sin((i * 1.f / N) * float(math::pi) * 4.f);
float right_boundary_flow = -left_boundary_flow;
velocity_(1, i)[0] = left_boundary_flow;
velocity_(N-2, i)[0] = right_boundary_flow;
velocity_(0, i)[0] = - velocity_(1, i)[0];
velocity_(N-1, i)[0] = - velocity_(N-2, i)[0];
}
velocity_(i, 0)[1] = -velocity_(i, 1)[1];
velocity_(i, N-2)[1] = -velocity_(i, N-2)[1];
}
// Uncomment to visualize the force field
// for (int y = 0; y < N; ++y)
// for (int x = 0; x < N; ++x)
// velocity_(x, y) = 100000.f * force_field_(x, y);
// Uncomment to visualize the terrain gradient field
// for (int y = 1; y < N - 1; ++y)
// {
// for (int x = 1; x < N - 1; ++x)
// {
// math::vector terrain_gradient
// {
// (terrain_(x + 1, y) - terrain_(x - 1, y)) / 2.f,
// (terrain_(x, y + 1) - terrain_(x, y - 1)) / 2.f,
// };
// velocity_(x, y) = terrain_gradient;
// }
// }
++frame_;
// Update all-time average temperature
for (int y = 0; y < N; ++y)
{
for (int x = 0; x < N; ++x)
{
float t = 1.f / frame_;
// float t = 1.f / std::min(8192, frame_);
average_temperature_(x, y) = math::lerp(average_temperature_(x, y), temperature_(x, y), t);
}
}
}
void present() override
{
gl::ClearColor(0.f, 0.f, 0.f, 0.f);
gl::Clear(gl::COLOR_BUFFER_BIT);
float const aspect_ratio = state().size[0] * 1.f / state().size[1];
math::box<float, 2> view_box = math::expand(simulation_box_, 1.f);
if (view_box[0].length() / view_box[1].length() > aspect_ratio)
view_box[1] = math::expand(view_box[1], (view_box[0].length() / aspect_ratio - view_box[1].length()) / 2.f);
else
view_box[0] = math::expand(view_box[0], (view_box[1].length() * aspect_ratio - view_box[0].length()) / 2.f);
std::optional<math::vector<int, 2>> mouseover_cell;
{
auto mouse = math::lerp(view_box, math::vector{state().mouse[0] * 1.f / state().size[0], 1.f - state().mouse[1] * 1.f / state().size[1]});
int x = std::floor(mouse[0]);
int y = std::floor(mouse[1]);
if (x >= 0 && x < N && y >= 0 && y < N)
mouseover_cell = {x, y};
}
[[maybe_unused]] float const pixel_size = view_box[0].length() / state().size[0];
auto map_color = [](float value, gfx::color_4f const & negative, gfx::color_4f const & positive){
return math::lerp(negative, positive, 1.f/ (1.f + std::exp(- value)));
};
for (int y = 0; y < N; ++y)
{
for (int x = 0; x < N; ++x)
{
gfx::color_4f color = gfx::color_4f::zero();
if (show_land_)
color = gfx::blend(color, map_color(terrain_(x, y), {-1.f, -1.f, -1.f, 1.f}, {1.f, 1.f, 1.f, 1.f}));
if (show_temperature_)
color = gfx::blend(color, map_color(0.1f * (temperature_(x, y) - 273.f), {0.125f, 0.5f, 1.f, 0.75f}, {1.f, 0.5f, 0.125f, 0.75f}));
if (show_temperature_delta_)
color = gfx::blend(color, map_color((temperature_(x, y) - expected_temperature_at(y)), {0.125f, 0.5f, 1.f, 0.75f}, {1.f, 0.5f, 0.125f, 0.75f}));
if (show_average_temperature_delta_)
color = gfx::blend(color, map_color((average_temperature_(x, y) - expected_temperature_at(y)), {0.125f, 0.5f, 1.f, 0.75f}, {1.f, 0.5f, 0.125f, 0.75f}));
if (show_pressure_)
color = gfx::blend(color, map_color(10000.f * pressure_(x, y), {0.f, 0.f, 1.f, 0.75f}, {1.f, 0.f, 0.f, 0.75f}));
painter_.rect({{{x, x + 1.f}, {y, y + 1.f}}}, gfx::to_coloru8(color));
}
}
if (show_velocity_)
{
for (int y = 0; y < N; ++y)
{
for (int x = 0; x < N; ++x)
{
math::point center{x + 0.5f, y + 0.5f};
auto v = velocity_(x, y);
auto color = gfx::color_4f::zero();
if (auto l = math::length(v); l > 0.f)
{
float const magnification = 1000.f;
float const max_length = 1.5f;
v *= 0.5f * max_length * (1.f - std::exp(- magnification * l)) / l;
// color = gfx::lerp(gfx::color_4f{0.5f, 1.f, 0.f, 1.f}, gfx::color_4f{1.f, 0.f, 0.f, 1.f}, 1.f - std::exp(- 0.25f * magnification * l));
color = map_color(0.1f * (temperature_(x, y) - 273.f), {0.125f, 0.5f, 1.f, 1.f}, {1.f, 0.5f, 0.125f, 1.f});
}
auto n = math::ort(v) * 0.3f;
painter_.triangle(center - v - n, center - v + n, center + v, gfx::to_coloru8(color));
}
}
}
auto push_text = [&, row = 0](std::string const & text) mutable
{
painter_.text(view_box.corner(0.f, 1.f) - math::vector{0.f, row * pixel_size * 2.f * 12.f}, text, {.scale = {2.f * pixel_size, - 2.f * pixel_size}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {255, 255, 255, 255}});
++row;
};
push_text(std::format("Frame {}", frame_));
push_text(std::format("Day {:.2f}", frame_ / (720.f / dt)));
if (mouseover_cell)
{
int x = (*mouseover_cell)[0];
int y = (*mouseover_cell)[1];
painter_.rect({{{x, x + 1.f}, {y, y + 1.f}}}, {255, 255, 255, 127});
push_text(std::format("{} {}", x, y));
push_text(std::format("V = {:.3f} {:.3f}", velocity_(x, y)[0] * 1000.f, velocity_(x, y)[1] * 1000.f));
push_text(std::format("P = {:.3f}", pressure_(x, y)));
push_text(std::format("T = {:.3f}", temperature_(x, y) - 273.f));
push_text(std::format("A = {:.3f}", average_temperature_(x, y) - 273.f));
push_text(std::format("E = {:.3f}", expected_temperature_at(y) - 273.f));
push_text(std::format("H = {:.3f}", terrain_(x, y)));
}
painter_.render(math::orthographic_camera{view_box}.transform());
}
private:
gfx::painter painter_;
math::box<float, 2> simulation_box_;
bool paused_ = true;
bool show_velocity_ = true;
bool show_temperature_ = false;
bool show_temperature_delta_ = false;
bool show_average_temperature_delta_ = false;
bool show_pressure_ = false;
bool show_land_ = true;
util::ndarray<float, 2> terrain_;
util::ndarray<math::vector<float, 2>, 2> velocity_;
util::ndarray<math::vector<float, 2>, 2> new_velocity_;
util::ndarray<float, 2> pressure_;
util::ndarray<float, 2> temperature_;
util::ndarray<float, 2> new_temperature_;
util::ndarray<float, 2> average_temperature_;
util::ndarray<math::vector<float, 2>, 2> force_field_main_;
util::ndarray<math::vector<float, 2>, 2> force_field_current_;
util::ndarray<math::vector<float, 2>, 2> force_field_next_;
util::ndarray<float, 2> vorticity_;
int frame_ = 0;
};
namespace psemek::app
{
std::unique_ptr<application::factory> make_application_factory()
{
return default_application_factory<weather_app>({.name = "Weather simulation test"});
}
}