planetsim/src/sim/Projection.hpp
Jonas Reith acc0e5eec9 Initial commit: fanworgen planet sim
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>
2026-06-23 15:08:25 +02:00

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) };
}