#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace psemek; auto make_perlin(random::generator & rng, int min_octave, int max_octave, float power) { std::vector> octaves; std::vector weights; random::uniform_sphere_vector_distribution random_vector{}; for (int octave = min_octave; octave < max_octave; ++octave) { int size = 1 << octave; util::ndarray, 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>(std::move(octaves), std::move(weights)); } void make_random_vector_field(random::generator & rng, util::ndarray, 2> & result, int min_octave, int max_octave, float scale) { auto noise_1 = make_perlin(rng, min_octave, max_octave, 2.f); auto noise_2 = make_perlin(rng, min_octave, max_octave, 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) = math::normalized(math::vector{2.f * noise_1(p) - 1.f, 2.f * noise_2(p) - 1.f}) * scale; } } } template void add(util::ndarray & dst, util::ndarray const & src) { auto src_begin = src.begin(); auto src_end = src.end(); auto dst_begin = dst.begin(); for (; src_begin != src_end;) *dst_begin++ += *src_begin++; } template void scale(util::ndarray & array, float factor) { for (auto & v : array) v *= factor; } float do_lerp(float x, float y, float t) { return math::lerp(x, y, t); } math::vector do_lerp(math::vector const & x, math::vector const & y, float t) { return math::lerp(length(x), length(y), t) * math::slerp(math::normalized(x), math::normalized(y), t); } template void lerp(util::ndarray & dst, util::ndarray const & src0, util::ndarray const & src1, float t) { auto src0_begin = src0.begin(); auto src0_end = src0.end(); auto src1_begin = src1.begin(); auto dst_begin = dst.begin(); for (; src0_begin != src0_end;) *dst_begin++ = do_lerp(*src0_begin++, *src1_begin++, t); } template T sample_bilinear(util::ndarray const & array, math::point p) { p[0] = math::clamp(p[0] - 0.5f, {0.f, array.width() - 1.f}); p[1] = math::clamp(p[1] - 0.5f, {0.f, array.height() - 1.f}); int ix = std::min(array.width() - 2, std::floor(p[0])); int iy = std::min(array.height() - 2, std::floor(p[1])); float tx = p[0] - ix; float ty = p[1] - iy; return math::lerp( math::lerp(array(ix + 0, iy + 0), array(ix + 1, iy + 0), tx), math::lerp(array(ix + 0, iy + 1), array(ix + 1, iy + 1), tx), ty ); } std::string season_str(int season) { switch (season) { case 0: return "Winter"; case 1: return "Spring"; case 2: return "Summer"; case 3: return "Autumn"; } return "???"; } std::string day_night_str(int day_night) { switch (day_night) { case 0: return "Night"; case 1: return "Day"; } return "???"; } struct climate_snapshot { util::ndarray, 2> wind_velocity; util::ndarray temperature; util::ndarray humidity; }; static constexpr int N = 128; struct solver { util::ndarray const & terrain; random::generator & rng; int season; int day_night; climate_snapshot & snapshot; int solve_velocity_iterations = 0; int max_solve_velocity_iterations = N / 2; int solve_temperature_iterations = 0; int max_solve_temperature_iterations = N / 4; int solve_humidity_iterations = 0; int max_solve_humidity_iterations = 3 * N; util::ndarray, 2> initial_random_wind_velocity; util::ndarray pressure; util::ndarray new_temperature; util::ndarray new_humidity; int stage = 0; bool finished = false; solver(util::ndarray const & terrain, random::generator & rng, int season, int day_night, climate_snapshot & snapshot) : terrain(terrain) , rng(rng) , season(season) , day_night(day_night) , snapshot(snapshot) { initial_random_wind_velocity.resize({N, N}); snapshot.wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); pressure.resize({N, N}, 0.f); snapshot.temperature.resize({N, N}, 0.f); new_temperature.resize({N, N}, 0.f); snapshot.humidity.resize({N, N}, 0.f); new_humidity.resize({N, N}, 0.f); make_random_vector_field(rng, initial_random_wind_velocity, 3, 6, 0.005f); for (int y = 0; y < N; ++y) for (int x = 0; x < N; ++x) if (terrain(x, y) < 0.f) initial_random_wind_velocity(x, y) *= 5.f; else initial_random_wind_velocity(x, y) *= std::exp(- 2.f * terrain(x, y)); snapshot.wind_velocity = initial_random_wind_velocity.copy(); for (int y = 0; y < N; ++y) for (int x = 0; x < N; ++x) snapshot.temperature(x, y) = expected_temperature(y); } float expected_temperature(int y) { return math::lerp(40.f, -20.f, (y + 0.5f) / N) // Base latitude temperature in spring/autumn - 15.f * std::cos(season * math::pi * 0.5f) // Seasonal offset of +/-15C - 5.f * std::cos(day_night * math::pi) // Day-night fluctuation of +/-5C ; } float expected_humidity(int x, int y) { if (terrain(x, y) < 0.f) return 1.f; else return 0.f; } void step() { if (stage == 0) { for (int y = 1; y + 1 < N; ++y) { for (int x = 1; x + 1 < N; ++x) { float divergence = 0.f + (initial_random_wind_velocity(x + 1, y)[0] - initial_random_wind_velocity(x - 1, y)[0]) * 0.5f + (initial_random_wind_velocity(x, y + 1)[1] - initial_random_wind_velocity(x, y + 1)[1]) * 0.5f ; // (sum - 4.f * p(x,y)) = divergence // sum - 4.f * p = divergence // p = (sum - divergence) / 4.f pressure(x, y) = (pressure(x - 1, y) + pressure(x + 1, y) + pressure(x, y - 1) + pressure(x, y + 1) - divergence) / 4.f; } } for (int y = 1; y + 1 < N; ++y) { for (int x = 1; x + 1 < N; ++x) { math::vector pressure_gradient { (pressure(x + 1, y) - pressure(x - 1, y)) * 0.5f, (pressure(x, y + 1) - pressure(x, y - 1)) * 0.5f, }; snapshot.wind_velocity(x, y) = initial_random_wind_velocity(x, y) - pressure_gradient; if (terrain(x, y) > 0.f) snapshot.wind_velocity(x, y) *= std::exp(- 2.f * terrain(x, y)); } } ++solve_velocity_iterations; if (solve_velocity_iterations == max_solve_velocity_iterations) ++stage; return; } if (stage == 1) { for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { float step_time = 100.f; math::point p{x + 0.5f, y + 0.5f}; float advection = sample_bilinear(snapshot.temperature, p - snapshot.wind_velocity(x, y) * step_time); float heating_speed = (terrain(x, y) < 0.f) ? 0.001f : 0.0001f; float heating_delta = (expected_temperature(y) - snapshot.temperature(x, y)) * (- std::expm1(- heating_speed * step_time)); new_temperature(x, y) = heating_delta + advection; } } std::swap(snapshot.temperature, new_temperature); for (int y = 1; y + 1 < N; ++y) { for (int x = 1; x + 1 < N; ++x) { float average = (0.f + snapshot.temperature(x - 1, y) + snapshot.temperature(x + 1, y) + snapshot.temperature(x, y - 1) + snapshot.temperature(x, y + 1) ) * 0.25f; new_temperature(x, y) = math::lerp(snapshot.temperature(x, y), average, 0.5f); } } std::swap(snapshot.temperature, new_temperature); ++solve_temperature_iterations; if (solve_temperature_iterations >= max_solve_temperature_iterations) ++stage; return; } if (stage == 2) { for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { float step_time = 100.f; math::point p{x + 0.5f, y + 0.5f}; float advection = sample_bilinear(snapshot.humidity, p - snapshot.wind_velocity(x, y) * step_time); float evaporation_speed = 0.0001f; float evaporation_delta = (expected_humidity(x, y) - snapshot.humidity(x, y)) * (- std::expm1(- evaporation_speed * step_time)); new_humidity(x, y) = evaporation_delta + advection; } } std::swap(snapshot.humidity, new_humidity); for (int y = 1; y + 1 < N; ++y) { for (int x = 1; x + 1 < N; ++x) { float average = (0.f + snapshot.humidity(x - 1, y) + snapshot.humidity(x + 1, y) + snapshot.humidity(x, y - 1) + snapshot.humidity(x, y + 1) ) * 0.25f; new_humidity(x, y) = math::lerp(snapshot.humidity(x, y), average, 0.5f); } } std::swap(snapshot.humidity, new_humidity); ++solve_humidity_iterations; if (solve_humidity_iterations >= max_solve_humidity_iterations) { ++stage; finished = true; } return; } } }; struct river_vertex { // Empty for river deltas std::optional> parent; std::vector> children = {}; float flow = 0.f; }; struct river_net { util::ndarray, 2> vertices; util::ndarray water; std::vector> queue; }; struct weather_app : app::application_base { random::generator rng{random::device{}}; // random::generator rng{0, 0}; gfx::painter painter; math::box simulation_box; bool paused = true; bool show_velocity = false; bool show_temperature = false; bool show_humidity = false; bool show_biomes = true; bool show_rivers = true; gfx::pixmap_rgba biomes_map; util::ndarray terrain; // 0 - Winter // 1 - Spring // 2 - Summer // 3 - Autumn int season = 0; // 0 - Night // 1 - Day int day_night = 0; climate_snapshot snapshots[4][2]; climate_snapshot average; climate_snapshot display_snapshot; bool need_update_display_snapshot = false; std::optional solver; river_net rivers; float display_season = 0.f; util::clock<> frame_clock; weather_app(options const &, context const & context) { simulation_box = {{{0.f, N}, {0.f, N}}}; terrain.resize({N, N}, 0.f); auto heightmap = gfx::read_image(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "heightmap_seed_3.png"}); for (int y = 0; y < N; ++y) for (int x = 0; x < N; ++x) terrain(x, y) = ((heightmap(x, y) / 255.f) * 2048.f - 512.f) / 1024.f; biomes_map = gfx::read_image(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "biomes.png"}); context.vsync(false); } void on_event(app::key_event const & event) override { application_base::on_event(event); 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::W) show_humidity ^= true; if (event.down && event.key == app::keycode::B) show_biomes ^= true; if (event.down && event.key == app::keycode::R) show_rivers ^= true; } void update() override { float const dt = frame_clock.restart().count(); if (solver && solver->finished) { solver.reset(); ++day_night; if (day_night == 2) { day_night = 0; ++season; } if (season == 4) { compute_average(); init_river_deltas(); need_update_display_snapshot = true; } } if (!solver && season < 4) solver.emplace(terrain, rng, season, day_night, snapshots[season][day_night]); if (!paused) { if (solver) { for (int i = 0; i < 16; ++i) { solver->step(); if (solver->finished) break; } } else if (season == 4 && !rivers.queue.empty()) { for (int i = 0; i < 16; ++i) { propagate_rivers(); if (rivers.queue.empty()) { compute_river_flow(); break; } } } } if (state().key_down.contains(app::keycode::LEFT)) { display_season -= 0.5f * dt; need_update_display_snapshot = true; } if (state().key_down.contains(app::keycode::RIGHT)) { display_season += 0.5f * dt; need_update_display_snapshot = true; } while (display_season < 0.f) display_season += 1.f; while (display_season >= 1.f) display_season -= 1.f; } void compute_average() { average.wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); average.temperature.resize({N, N}, 0.f); average.humidity.resize({N, N}, 0.f); for (auto const & season : snapshots) for (auto const & s : season) { add(average.wind_velocity, s.wind_velocity); add(average.temperature, s.temperature); add(average.humidity, s.humidity); } scale(average.wind_velocity, 1.f / 8.f); scale(average.temperature, 1.f / 8.f); scale(average.humidity, 1.f / 8.f); } void init_river_deltas() { rivers.vertices.resize({N, N}); rivers.water.resize({N, N}, false); for (int y = 1; y + 1 < N; ++y) { for (int x = 1; x + 1 < N; ++x) { int land = 0; land += (terrain(x - 1, y - 1) >= 0.f ? 1 : 0); land += (terrain(x + 0, y - 1) >= 0.f ? 1 : 0); land += (terrain(x - 1, y + 0) >= 0.f ? 1 : 0); land += (terrain(x + 0, y + 0) >= 0.f ? 1 : 0); if (land > 0 && land < 4) { rivers.queue.push_back({x, y}); rivers.vertices(x, y).emplace(); } if (land < 4) rivers.water(x, y) = true; } } } void propagate_rivers() { auto i = random::uniform(rng, 0, rivers.queue.size() - 1); if (i + 1 < rivers.queue.size()) std::swap(rivers.queue[i], rivers.queue.back()); auto p = rivers.queue.back(); math::vector const neighbours[4] { {-1, 0}, { 1, 0}, { 0, -1}, { 0, 1}, }; float height = sample_bilinear(terrain, math::cast(p)); std::vector> candidates; for (auto n : neighbours) { auto q = p + n; if (q[0] == 0 || q[0] == N || q[1] == 0 || q[1] == N) continue; if (rivers.water(q[0], q[1])) continue; if (rivers.vertices(q[0], q[1])) continue; float nheight = sample_bilinear(terrain, math::cast(q)); if (nheight + 0.02f < height) continue; candidates.push_back(q); } if (candidates.size() <= 1) rivers.queue.pop_back(); if (candidates.empty()) { // TODO: remove if no parent and not going anywhere? return; } auto next = random::uniform_from(rng, candidates); rivers.vertices(p[0], p[1])->children.push_back(next); rivers.vertices(next[0], next[1]) = river_vertex{.parent = p}; rivers.queue.push_back(next); } void compute_river_flow() { for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { if (auto & v = rivers.vertices(x, y); v && !v->parent) { compute_river_flow_at({x, y}); } } } } float compute_river_flow_at(math::point const & p) { auto & v = rivers.vertices(p[0], p[1]); if (!v) return 0.f; if (v->children.empty()) { v->flow = std::max(0.f, sample_bilinear(average.humidity, math::cast(p)) - 0.05f); } else { for (auto c : v->children) v->flow += compute_river_flow_at(c); } return v->flow; } void update_display_snapshot() { display_snapshot.wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); display_snapshot.temperature.resize({N, N}, 0.f); display_snapshot.humidity.resize({N, N}, 0.f); int i = std::min(3, std::floor(4.f * display_season)); int j = (i + 1) % 4; float t = (4.f * display_season) - i; auto & s0 = snapshots[i][0]; auto & s1 = snapshots[j][0]; lerp(display_snapshot.wind_velocity, s0.wind_velocity, s1.wind_velocity, t); lerp(display_snapshot.temperature, s0.temperature, s1.temperature, t); lerp(display_snapshot.humidity, s0.humidity, s1.humidity, t); } void present() override { if (need_update_display_snapshot) { update_display_snapshot(); need_update_display_snapshot = false; } 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 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> 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))); }; auto map_color_positive = [](float value, gfx::color_4f const & zero, gfx::color_4f const & positive){ return math::lerp(zero, positive, 2.f / (1.f + std::exp(- value)) - 1.f); }; auto map_temperature = [&](float temperature) { return map_color(0.5f * std::floor(temperature / 10.f), {0.125f, 0.5f, 1.f, 0.75f}, {1.f, 0.5f, 0.125f, 0.75f}); }; auto map_humidity = [&](float humidity) { return map_color_positive(std::round(10.f * humidity), {0.f, 1.f, 1.f, 0.f}, {0.f, 1.f, 1.f, 0.75f}); }; auto map_biome = [this](float temperature, float precipitation) { auto x = math::clamp(math::unlerp({0.f, 0.5f}, precipitation) * biomes_map.width() , {0, biomes_map.width() - 1}); auto y = math::clamp(math::unlerp({-10.f, 30.f}, temperature ) * biomes_map.height(), {0, biomes_map.height() - 1}); return gfx::to_colorf(biomes_map(x, y)); }; auto & avg_snapshot = (season < 4) ? snapshots[season][day_night] : average; auto & snapshot = (season < 4) ? snapshots[season][day_night] : display_snapshot; for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { gfx::color_4f color = gfx::color_4f::zero(); bool const is_water = terrain(x, y) < 0.f; bool const is_land = !is_water; if (is_water) color = map_color(8.f * terrain(x, y), {0.f, 0.f, 0.125f, 1.f}, {0.f, 1.f, 1.5f, 1.f}); else if (show_biomes) { float temperature = avg_snapshot.temperature(x, y) - std::max(0.f, terrain(x, y)) * 30.f; color = map_biome(temperature, avg_snapshot.humidity(x, y)); } else color = gfx::color_4f{0.3f, 0.5f, 0.1f, 1.f}; if (is_land && x > 0 && x + 1 < N && y > 0 && y + 1 < N) { math::vector terrain_gradient { (terrain(x + 1, y) - terrain(x - 1, y)) / 2.f, (terrain(x, y + 1) - terrain(x, y - 1)) / 2.f, }; auto terrain_normal = math::normalized(math::vector{-terrain_gradient[0], -terrain_gradient[1], 0.125f}); float lightness = 0.5f + 0.5f * math::dot(terrain_normal, math::normalized(math::vector{1.f, 2.f, 3.f})); color = gfx::dark(color, 1.f - lightness); } if (show_temperature) { color = gfx::blend(color, map_temperature(snapshot.temperature(x, y))); } if (show_humidity) { color = gfx::blend(color, map_humidity(snapshot.humidity(x, y))); } 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 = snapshot.wind_velocity(x, y); if (auto l = math::length(v); l > 0.f) { float const magnification = 200.f; float const max_length = 1.5f; v *= 0.5f * max_length * (1.f - std::exp(- magnification * l)) / l; } auto n = math::ort(v) * 0.3f; auto color = gfx::color_4f{1.f, 1.f, 1.f, 1.f}; if (!show_temperature) { float t = 0.2f * snapshot.temperature(x, y); if (t >= 0.f) color = map_color_positive( t, color, {1.f, 0.5f, 0.125f, 1.f}); else color = map_color_positive(-t, color, {0.125f, 0.5f, 1.f, 1.f}); } painter.triangle(center - v - n, center - v + n, center + v, gfx::to_coloru8(color)); } } } if (show_rivers) { for (auto const & p : rivers.queue) { painter.circle(math::cast(p), 3.f * pixel_size, {0, 255, 255, 255}, 12); } if (!rivers.vertices.empty()) for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { auto & v = rivers.vertices(x, y); if (!v || !v->parent) continue; float width = 2.f; if (rivers.queue.empty()) width = 2.f * std::sqrt(v->flow); painter.line({x, y}, math::cast(*v->parent), width * pixel_size, {0, 255, 255, 255}, false); } } } int row = 0; auto push_text = [&](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; }; if (solver) { push_text(std::format("{} {}", season_str(season), day_night_str(day_night))); if (solver->stage >= 0) push_text(std::format("Velocity {}/{}", solver->solve_velocity_iterations, solver->max_solve_velocity_iterations)); else push_text(""); if (solver->stage >= 1) push_text(std::format("Temperature {}/{}", solver->solve_temperature_iterations, solver->max_solve_temperature_iterations)); else push_text(""); if (solver->stage >= 2) push_text(std::format("Humidity {}/{}", solver->solve_humidity_iterations, solver->max_solve_humidity_iterations)); else push_text(""); push_text(""); } 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}", length(snapshot.wind_velocity(x, y)) * 1000.f)); push_text(std::format("H = {:.3f}", terrain(x, y) * 1024.f)); push_text(std::format("T = {:.3f}", snapshot.temperature(x, y))); push_text(std::format("W = {:.3f}", snapshot.humidity(x, y))); push_text(""); } if (season == 4) { ++row; float axis_width = 300.f; float axis_height = 24.f; float axis_left = view_box[0].min + 30.f * pixel_size; float axis_bottom = view_box[1].max - row * pixel_size * 2.f * 12.f - axis_height * pixel_size; auto line = [&](float x0, float y0, float x1, float y1, float w, gfx::color_rgba const & c) { painter.line( {axis_left + x0 * axis_width * pixel_size, axis_bottom + y0 * axis_height * pixel_size}, {axis_left + x1 * axis_width * pixel_size, axis_bottom + y1 * axis_height * pixel_size}, pixel_size * w, c, false ); }; line(0.f, 0.5f, 1.f, 0.5f, 2.f, {255, 255, 255, 127}); line(display_season, 0.f, display_season, 1.f, 4.f, {255, 255, 255, 255}); ++row; ++row; } if (mouseover_cell && season == 4) { int x = (*mouseover_cell)[0]; int y = (*mouseover_cell)[1]; float plot_width = 300.f; float plot_height = 150.f; float plot_left = view_box[0].min + 30.f * pixel_size; float plot_bottom = view_box[1].max - row * pixel_size * 2.f * 12.f - plot_height * 1.5f * pixel_size; auto line = [&](float x0, float y0, float x1, float y1, gfx::color_rgba const & c) { painter.line( {plot_left + x0 * plot_width * pixel_size, plot_bottom + y0 * plot_height * pixel_size}, {plot_left + x1 * plot_width * pixel_size, plot_bottom + y1 * plot_height * pixel_size}, pixel_size * 2.f, c, false ); }; auto begin_plot = [&]{ painter.rect({{{plot_left, plot_left + plot_width * pixel_size}, {plot_bottom, plot_bottom + plot_height * pixel_size}}}, {255, 255, 255, 31}); for (int x = 0; x <= 4; ++x) line(x / 4.f, 0.f, x / 4.f, 1.f, {255, 255, 255, (x == 0 || x == 4) ? 127 : 31}); }; auto draw_plot = [&](std::vector const & ys, gfx::color_rgba const & c) { std::vector xs(5); for (int i = 0; i <= 4; ++i) xs[i] = i / 4.f; auto system = math::matrix::zero(); auto coeffs = math::vector::zero(); for (int i = 0; i < 4; ++i) { float x0 = xs[i]; system[2 * i + 0][4 * i + 0] = 1.f; system[2 * i + 0][4 * i + 1] = x0; system[2 * i + 0][4 * i + 2] = x0 * x0; system[2 * i + 0][4 * i + 3] = x0 * x0 * x0; coeffs[2 * i + 0] = ys[i]; float x1 = xs[i + 1]; system[2 * i + 1][4 * i + 0] = 1.f; system[2 * i + 1][4 * i + 1] = x1; system[2 * i + 1][4 * i + 2] = x1 * x1; system[2 * i + 1][4 * i + 3] = x1 * x1 * x1; coeffs[2 * i + 1] = ys[i + 1]; } for (int i = 0; i < 4; ++i) { int j = (i + 1) % 4; float x0 = xs[i + 1]; float x1 = xs[j]; system[8 + 2 * i + 0][4 * i + 1] = 1.f; system[8 + 2 * i + 0][4 * i + 2] = 2.f * x0; system[8 + 2 * i + 0][4 * i + 3] = 3.f * x0 * x0; system[8 + 2 * i + 0][4 * j + 1] = - 1.f; system[8 + 2 * i + 0][4 * j + 2] = - 2.f * x1; system[8 + 2 * i + 0][4 * j + 3] = - 3.f * x1 * x1; system[8 + 2 * i + 1][4 * i + 2] = 2.f; system[8 + 2 * i + 1][4 * i + 3] = 6.f * x0; system[8 + 2 * i + 1][4 * j + 2] = - 2.f; system[8 + 2 * i + 1][4 * j + 3] = - 6.f * x1; } math::gauss(system, coeffs); auto eval = [&](int i, float x) { return 0.f + coeffs[4 * i + 0] + coeffs[4 * i + 1] * x + coeffs[4 * i + 2] * x * x + coeffs[4 * i + 3] * x * x * x ; }; for (int i = 0; i < 4; ++i) { int D = 8; for (int j = 0; j < D; ++j) { float x0 = (i + (j + 0.f) / D) / 4.f; float x1 = (i + (j + 1.f) / D) / 4.f; line(x0, eval(i, x0), x1, eval(i, x1), c); } if (i == std::min(3, std::floor(4.f * display_season))) { float x = display_season; float y = eval(i, x); painter.circle({plot_left + x * plot_width * pixel_size, plot_bottom + y * plot_height * pixel_size}, 4.f * pixel_size, c); } } }; // Temperature { begin_plot(); for (int y = 0; y <= 10; ++y) { line(0.f, y / 10.f, 1.f, y / 10.f, {255, 255, 255, (y == 5) ? 127 : 31}); } std::vector ys[2]; for (int d = 0; d < 2; ++d) { for (int s = 0; s <= 4; ++s) { float value = 0.5f + snapshots[s % 4][d].temperature(x, y) / 100.f; ys[d].push_back(value); } } draw_plot(ys[0], gfx::color_rgba{0, 127, 255, 255}); draw_plot(ys[1], gfx::color_rgba{255, 127, 0, 255}); } plot_bottom -= (plot_height + 50.f) * pixel_size; // Humidity { begin_plot(); for (int y = 0; y <= 5; ++y) { line(0.f, y / 5.f, 1.f, y / 5.f, {255, 255, 255, (y == 0) ? 127 : 31}); } std::vector ys; for (int s = 0; s <= 4; ++s) { float value = (snapshots[s % 4][0].humidity(x, y) + snapshots[s % 4][1].humidity(x, y)) / 2.f; ys.push_back(value); } draw_plot(ys, {0, 255, 255, 255}); } } painter.render(math::orthographic_camera{view_box}.transform()); } }; namespace psemek::app { std::unique_ptr make_application_factory() { return default_application_factory({.name = "Weather simulation test"}); } }