C++/raylib semi-realistic fantasy/sci-fi planet generator on a fixed icosphere grid (Eulerian: properties flow over fixed cells). World-creation stages: tectonics, continental drift & erosion, hydrology (rivers/lakes), climate (temperature + orographic precipitation), and biome classification. Engine in src/sim (raylib-free, headless-testable), viewer in src/render. See CLAUDE.md and docs/ (design-notes.md, fauna-flora-plan.md = next step). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.8 KiB
C++
72 lines
2.8 KiB
C++
#pragma once
|
|
#include "Vec3.hpp"
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
|
|
// Equal Earth projection (Savric, Patterson & Jenny, 2018): an equal-area
|
|
// pseudocylindrical projection with a natural, low-distortion "globe" look.
|
|
// Forward is closed-form; the inverse uses a few Newton steps (cheap, used per
|
|
// mouse point for hover). Kept raylib-free so it can be unit-tested headless.
|
|
//
|
|
// Convention: y is "up". lat = asin(unit.y), lon = atan2(unit.z, unit.x).
|
|
namespace EqualEarth {
|
|
|
|
constexpr double A1 = 1.340264;
|
|
constexpr double A2 = -0.081106;
|
|
constexpr double A3 = 0.000893;
|
|
constexpr double A4 = 0.003796;
|
|
|
|
inline double M_() { return std::sqrt(3.0) / 2.0; } // sin(theta) scale
|
|
inline double K_() { return 2.0 * std::sqrt(3.0) / 3.0; } // x scale factor
|
|
|
|
// dy/dtheta == x denominator; shared by forward and inverse.
|
|
inline double denom(double th2, double th6) {
|
|
return A1 + 3.0 * A2 * th2 + th6 * (7.0 * A3 + 9.0 * A4 * th2);
|
|
}
|
|
|
|
// (lon,lat) radians -> projection (x,y).
|
|
inline void forward(double lon, double lat, double& x, double& y) {
|
|
double th = std::asin(M_() * std::sin(lat));
|
|
double th2 = th * th, th6 = th2 * th2 * th2;
|
|
x = K_() * lon * std::cos(th) / denom(th2, th6);
|
|
y = th * (A1 + A2 * th2 + th6 * (A3 + A4 * th2)); // A1 th + A2 th^3 + A3 th^7 + A4 th^9
|
|
}
|
|
|
|
// projection (x,y) -> (lon,lat) radians. Returns false if (x,y) is outside the
|
|
// projected globe (so callers can reject hovers off the map).
|
|
inline bool inverse(double x, double y, double& lon, double& lat) {
|
|
double th = y; // good initial guess (y ~ A1*theta near 0)
|
|
for (int it = 0; it < 16; ++it) {
|
|
double th2 = th * th, th6 = th2 * th2 * th2;
|
|
double fy = th * (A1 + A2 * th2 + th6 * (A3 + A4 * th2)) - y;
|
|
double d = fy / denom(th2, th6);
|
|
th -= d;
|
|
if (std::fabs(d) < 1e-12) break;
|
|
}
|
|
double s = std::sin(th) / M_();
|
|
if (s < -1.0 - 1e-9 || s > 1.0 + 1e-9) return false;
|
|
lat = std::asin(std::clamp(s, -1.0, 1.0));
|
|
double th2 = th * th, th6 = th2 * th2 * th2;
|
|
double c = std::cos(th);
|
|
if (std::fabs(c) < 1e-12) return false;
|
|
lon = x * denom(th2, th6) / (K_() * c);
|
|
if (lon < -M_PI - 1e-6 || lon > M_PI + 1e-6) return false;
|
|
return true;
|
|
}
|
|
|
|
// Half-extents of the projected map (x in [-halfW,halfW], y in [-halfH,halfH]).
|
|
inline double halfWidth() { double x, y; forward(M_PI, 0.0, x, y); return x; }
|
|
inline double halfHeight() { double x, y; forward(0.0, M_PI / 2.0, x, y); return y; }
|
|
|
|
} // namespace EqualEarth
|
|
|
|
// Sphere direction <-> geographic coordinates (y up).
|
|
inline void dirToLonLat(const Vec3& u, double& lon, double& lat) {
|
|
lat = std::asin(std::clamp(u.y, -1.0, 1.0));
|
|
lon = std::atan2(u.z, u.x);
|
|
}
|
|
inline Vec3 lonLatToDir(double lon, double lat) {
|
|
double c = std::cos(lat);
|
|
return Vec3{ c * std::cos(lon), std::sin(lat), c * std::sin(lon) };
|
|
}
|