util/surface.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/util/surface.h" | ||
| 2 | |||
| 3 | #include "terraingen/util/checks.h" | ||
| 4 | |||
| 5 | #include <algorithm> | ||
| 6 | #include <cmath> | ||
| 7 | #include <memory> | ||
| 8 | #include <numbers> | ||
| 9 | #include <stdexcept> | ||
| 10 | #include <utility> | ||
| 11 | |||
| 12 | namespace terraingen { | ||
| 13 | |||
| 14 | namespace { | ||
| 15 | |||
| 16 | /// The east and north unit vectors at longitude `lon` and latitude `lat` (radians). | ||
| 17 | 1978597 | void LocalAxes(double lon, double lat, Vec3d& east, Vec3d& north) { | |
| 18 | 1978597 | const double sinLon = std::sin(lon); | |
| 19 | 1978597 | const double cosLon = std::cos(lon); | |
| 20 | 1978597 | const double sinLat = std::sin(lat); | |
| 21 | 1978597 | east = {-sinLon, cosLon, 0.0}; | |
| 22 | 1978597 | north = {-sinLat * cosLon, -sinLat * sinLon, std::cos(lat)}; | |
| 23 | 1978597 | } | |
| 24 | |||
| 25 | } // namespace | ||
| 26 | |||
| 27 | 15016452 | Vec3d UnitVector(double lonDeg, double latDeg) { | |
| 28 | 15016452 | const double lon = lonDeg * kDegree; | |
| 29 | 15016452 | const double lat = latDeg * kDegree; | |
| 30 | 15016452 | const double cosLat = std::cos(lat); | |
| 31 | 15016452 | return {cosLat * std::cos(lon), cosLat * std::sin(lon), std::sin(lat)}; | |
| 32 | } | ||
| 33 | |||
| 34 | 539312 | Surface Surface::Sphere(double radius) { | |
| 35 | 539312 | RequirePositive(radius, "A sphere's radius"); | |
| 36 | 539309 | Surface surface; | |
| 37 | 539309 | surface.kind_ = Kind::Body; | |
| 38 | 539309 | surface.radius_ = radius; | |
| 39 | 539309 | surface.unitLength_ = radius * kDegree; | |
| 40 | 539309 | surface.radii_ = {radius, radius, radius}; | |
| 41 | 539309 | return surface; | |
| 42 | } | ||
| 43 | |||
| 44 | 94 | Surface Surface::Ellipsoid(double a, double b, double c) { | |
| 45 | 94 | RequirePositive(a, "An ellipsoid's radius a"); | |
| 46 | 93 | RequirePositive(b, "An ellipsoid's radius b"); | |
| 47 | 91 | RequirePositive(c, "An ellipsoid's radius c"); | |
| 48 | 90 | if (a == b && b == c) { | |
| 49 | 3 | return Sphere(a); | |
| 50 | } | ||
| 51 | 87 | Surface surface; | |
| 52 | 87 | surface.kind_ = Kind::Body; | |
| 53 | 87 | surface.radius_ = std::cbrt(a * b * c); | |
| 54 | 87 | surface.unitLength_ = surface.radius_ * kDegree; | |
| 55 | 87 | surface.radii_ = {a, b, c}; | |
| 56 | 87 | return surface; | |
| 57 | 87 | } | |
| 58 | |||
| 59 | 75 | Surface Surface::ShapeModel(std::shared_ptr<const RadialShape> shape) { | |
| 60 | 75 | if (!shape) { | |
| 61 | 1 | throw std::invalid_argument("A shape model surface needs a radial shape"); | |
| 62 | } | ||
| 63 | 74 | constexpr int kSteps = 180; | |
| 64 | 74 | double cubed = 0.0; | |
| 65 | 74 | double weight = 0.0; | |
| 66 | 13394 | for (int j = 0; j < kSteps; ++j) { | |
| 67 | 13320 | const double lat = -90.0 + (j + 0.5) * 180.0 / kSteps; | |
| 68 | 13320 | const double cosLat = std::cos(lat * kDegree); | |
| 69 | 4808520 | for (int i = 0; i < 2 * kSteps; ++i) { | |
| 70 | 4795200 | const double r = shape->At(UnitVector(-180.0 + (i + 0.5) * 180.0 / kSteps, lat)); | |
| 71 | 4795200 | cubed += r * r * r * cosLat; | |
| 72 | 4795200 | weight += cosLat; | |
| 73 | } | ||
| 74 | } | ||
| 75 | 74 | Surface surface; | |
| 76 | 74 | surface.kind_ = Kind::Body; | |
| 77 | 74 | surface.radius_ = std::cbrt(cubed / weight); | |
| 78 | 74 | surface.unitLength_ = surface.radius_ * kDegree; | |
| 79 | 74 | surface.shape_ = std::move(shape); | |
| 80 | 74 | return surface; | |
| 81 | } | ||
| 82 | |||
| 83 | 7416 | double Surface::MinRadius() const { | |
| 84 | 7416 | if (shape_) { | |
| 85 | 9 | return shape_->MinRadius(); | |
| 86 | } | ||
| 87 | 7407 | return std::min({radii_.x, radii_.y, radii_.z}); | |
| 88 | } | ||
| 89 | |||
| 90 | 4 | double Surface::MaxRadius() const { | |
| 91 | 4 | if (shape_) { | |
| 92 | 3 | return shape_->MaxRadius(); | |
| 93 | } | ||
| 94 | 1 | return std::max({radii_.x, radii_.y, radii_.z}); | |
| 95 | } | ||
| 96 | |||
| 97 | 2759907 | double Surface::RadiusAt(const Vec2d& point) const { | |
| 98 | 2759907 | if (kind_ == Kind::Plane) { | |
| 99 | 1 | return 0.0; | |
| 100 | } | ||
| 101 | 2759906 | if (IsSphere()) { | |
| 102 | 178339 | return radius_; | |
| 103 | } | ||
| 104 | 2581567 | const Vec3d u = UnitVector(point.x, point.y); | |
| 105 | 2581567 | if (shape_) { | |
| 106 | 617818 | return shape_->At(u); | |
| 107 | } | ||
| 108 | 1963749 | const double ex = u.x / radii_.x; | |
| 109 | 1963749 | const double ey = u.y / radii_.y; | |
| 110 | 1963749 | const double ez = u.z / radii_.z; | |
| 111 | 1963749 | return 1.0 / std::sqrt(ex * ex + ey * ey + ez * ez); | |
| 112 | } | ||
| 113 | |||
| 114 | 2139625 | double Surface::AreaPerSteradian(const Vec2d& point) const { | |
| 115 | 2139625 | if (kind_ == Kind::Plane) { | |
| 116 | 1 | return 1.0; | |
| 117 | } | ||
| 118 | 2139624 | if (IsSphere()) { | |
| 119 | 1 | return radius_ * radius_; | |
| 120 | } | ||
| 121 | 2139623 | const Vec3d p = Position(point); | |
| 122 | 2139623 | const double r = Length(p); | |
| 123 | 2139623 | const double cosine = Dot(p, Normal(point, p)) / r; | |
| 124 | 2139623 | return r * r / cosine; | |
| 125 | } | ||
| 126 | |||
| 127 | 2185762 | Vec3d Surface::Normal(const Vec2d& point, const Vec3d& p) const { | |
| 128 | 2185762 | Vec3d n; | |
| 129 | 2185762 | if (shape_) { | |
| 130 | 536205 | constexpr double kStep = 1e-6; | |
| 131 | 536205 | const Vec3d u = UnitVector(point.x, point.y); | |
| 132 | 536205 | Vec3d east; | |
| 133 | 536205 | Vec3d north; | |
| 134 | 536205 | LocalAxes(point.x * kDegree, point.y * kDegree, east, north); | |
| 135 | 2144820 | const auto at = [&](const Vec3d& axis, double sign) { | |
| 136 | 2144820 | const Vec3d d{u.x + sign * kStep * axis.x, u.y + sign * kStep * axis.y, | |
| 137 | 2144820 | u.z + sign * kStep * axis.z}; | |
| 138 | 2144820 | const double length = Length(d); | |
| 139 | 2144820 | const Vec3d unit{d.x / length, d.y / length, d.z / length}; | |
| 140 | 4289640 | return unit * shape_->At(unit); | |
| 141 | 536205 | }; | |
| 142 | 536205 | n = Cross(at(east, 1.0) - at(east, -1.0), at(north, 1.0) - at(north, -1.0)); | |
| 143 | } else { | ||
| 144 | 1649557 | n = {p.x / (radii_.x * radii_.x), p.y / (radii_.y * radii_.y), p.z / (radii_.z * radii_.z)}; | |
| 145 | } | ||
| 146 | 2185762 | const double length = Length(n); | |
| 147 | 2185762 | return {n.x / length, n.y / length, n.z / length}; | |
| 148 | } | ||
| 149 | |||
| 150 | 45133 | Vec2d Surface::TangentOffset(const Vec2d& centre, const Vec2d& point) const { | |
| 151 | 45133 | const Vec3d c = Position(centre); | |
| 152 | 45133 | const Vec3d p = Position(point); | |
| 153 | 45133 | const LocalFrame frame = Frame(centre); | |
| 154 | 45133 | const Vec3d d{p.x - c.x, p.y - c.y, p.z - c.z}; | |
| 155 | 90266 | return {Dot(d, frame.east), Dot(d, frame.north)}; | |
| 156 | } | ||
| 157 | |||
| 158 | 402 | Vec2d Surface::ShapeLocate(const Vec2d& centre, const Vec2d& offset) const { | |
| 159 | 402 | if (offset.x == 0.0 && offset.y == 0.0) { | |
| 160 | 2 | return centre; | |
| 161 | } | ||
| 162 | 400 | const Vec3d c = Position(centre); | |
| 163 | 400 | const LocalFrame frame = Frame(centre); | |
| 164 | 400 | const Vec3d q = c + frame.east * offset.x + frame.north * offset.y; | |
| 165 | 41211 | const auto along = [&](double s) { return q - frame.up * s; }; | |
| 166 | 40811 | const auto excess = [&](double s) { | |
| 167 | 40811 | const Vec3d x = along(s); | |
| 168 | 40811 | const double r = Length(x); | |
| 169 | 40811 | return r - shape_->At({x.x / r, x.y / r, x.z / r}); | |
| 170 | 400 | }; | |
| 171 | 400 | const double step = shape_->MaxRadius() / 64.0; | |
| 172 | 400 | double inside = 0.0; | |
| 173 | 400 | double outside = 0.0; | |
| 174 | 400 | if (excess(0.0) >= 0.0) { | |
| 175 | 374 | inside = step; | |
| 176 | 385 | while (excess(inside) >= 0.0 && inside < 4.0 * shape_->MaxRadius()) { | |
| 177 | 11 | outside = inside; | |
| 178 | 11 | inside += step; | |
| 179 | } | ||
| 180 | } else { | ||
| 181 | 26 | outside = -step; | |
| 182 | 26 | while (excess(outside) < 0.0 && outside > -4.0 * shape_->MaxRadius()) { | |
| 183 | ✗ | inside = outside; | |
| 184 | ✗ | outside -= step; | |
| 185 | } | ||
| 186 | } | ||
| 187 | 40400 | for (int iteration = 0; iteration < 100; ++iteration) { | |
| 188 | 40000 | const double middle = 0.5 * (inside + outside); | |
| 189 | 40000 | (excess(middle) >= 0.0 ? outside : inside) = middle; | |
| 190 | } | ||
| 191 | 400 | return PointOf(along(0.5 * (inside + outside))); | |
| 192 | } | ||
| 193 | |||
| 194 | 602 | Vec2d Surface::EllipsoidLocate(const Vec2d& centre, const Vec2d& offset) const { | |
| 195 | 602 | if (offset.x == 0.0 && offset.y == 0.0) { | |
| 196 | 2 | return centre; | |
| 197 | } | ||
| 198 | 600 | const Vec3d c = Position(centre); | |
| 199 | 600 | const LocalFrame frame = Frame(centre); | |
| 200 | 600 | const Vec3d q{c.x + offset.x * frame.east.x + offset.y * frame.north.x, | |
| 201 | 600 | c.y + offset.x * frame.east.y + offset.y * frame.north.y, | |
| 202 | 600 | c.z + offset.x * frame.east.z + offset.y * frame.north.z}; | |
| 203 | 600 | const Vec3d& n = frame.up; | |
| 204 | 600 | const Vec3d inverse{1.0 / (radii_.x * radii_.x), 1.0 / (radii_.y * radii_.y), | |
| 205 | 600 | 1.0 / (radii_.z * radii_.z)}; | |
| 206 | 600 | const double a = n.x * n.x * inverse.x + n.y * n.y * inverse.y + n.z * n.z * inverse.z; | |
| 207 | 600 | const double b = q.x * n.x * inverse.x + q.y * n.y * inverse.y + q.z * n.z * inverse.z; | |
| 208 | 600 | const double cq = q.x * q.x * inverse.x + q.y * q.y * inverse.y + q.z * q.z * inverse.z; | |
| 209 | 600 | const double discriminant = b * b - a * (cq - 1.0); | |
| 210 | 600 | const double t = (b - std::sqrt(std::max(discriminant, 0.0))) / a; | |
| 211 | 600 | return PointOf({q.x - t * n.x, q.y - t * n.y, q.z - t * n.z}); | |
| 212 | } | ||
| 213 | |||
| 214 | 1345729 | Vec2d Surface::SphereOffset(const Vec2d& centre, const Vec2d& point) const { | |
| 215 | 1345729 | const Vec3d c = UnitVector(centre.x, centre.y); | |
| 216 | 1345729 | const Vec3d p = UnitVector(point.x, point.y); | |
| 217 | 1345729 | Vec3d east; | |
| 218 | 1345729 | Vec3d north; | |
| 219 | 1345729 | LocalAxes(centre.x * kDegree, centre.y * kDegree, east, north); | |
| 220 | 1345729 | const double angle = std::atan2(Length(Cross(c, p)), Dot(c, p)); | |
| 221 | 1345729 | const double e = Dot(p, east); | |
| 222 | 1345729 | const double n = Dot(p, north); | |
| 223 | 1345729 | const double across = std::hypot(e, n); | |
| 224 | 1345729 | if (across == 0.0) { | |
| 225 | 6 | return {0.0, angle == 0.0 ? 0.0 : radius_ * angle}; | |
| 226 | } | ||
| 227 | 1345723 | const double scale = radius_ * angle / across; | |
| 228 | 1345723 | return {e * scale, n * scale}; | |
| 229 | } | ||
| 230 | |||
| 231 | 50406 | Vec2d Surface::SphereLocate(const Vec2d& centre, const Vec2d& offset) const { | |
| 232 | 50406 | const double distance = std::hypot(offset.x, offset.y); | |
| 233 | 50406 | if (distance == 0.0) { | |
| 234 | 10 | return centre; | |
| 235 | } | ||
| 236 | 50396 | const Vec3d c = UnitVector(centre.x, centre.y); | |
| 237 | 50396 | Vec3d east; | |
| 238 | 50396 | Vec3d north; | |
| 239 | 50396 | LocalAxes(centre.x * kDegree, centre.y * kDegree, east, north); | |
| 240 | 50396 | const double angle = distance / radius_; | |
| 241 | 50396 | const double along = std::sin(angle) / distance; | |
| 242 | 50396 | const double cosAngle = std::cos(angle); | |
| 243 | 50396 | const Vec3d p{cosAngle * c.x + along * (offset.x * east.x + offset.y * north.x), | |
| 244 | 50396 | cosAngle * c.y + along * (offset.x * east.y + offset.y * north.y), | |
| 245 | 50396 | cosAngle * c.z + along * (offset.x * east.z + offset.y * north.z)}; | |
| 246 | 50396 | return PointOf(p); | |
| 247 | } | ||
| 248 | |||
| 249 | 1316487 | double Surface::Distance(const Vec2d& a, const Vec2d& b) const { | |
| 250 | 1316487 | if (kind_ == Kind::Plane) { | |
| 251 | 1213096 | return std::hypot(b.x - a.x, b.y - a.y); | |
| 252 | } | ||
| 253 | 103391 | if (IsNonSphere()) { | |
| 254 | 100401 | const Vec3d p = Position(a); | |
| 255 | 100401 | const Vec3d q = Position(b); | |
| 256 | 100401 | return Length(Vec3d{q.x - p.x, q.y - p.y, q.z - p.z}); | |
| 257 | } | ||
| 258 | 2990 | const Vec3d u = UnitVector(a.x, a.y); | |
| 259 | 2990 | const Vec3d v = UnitVector(b.x, b.y); | |
| 260 | 2990 | return radius_ * std::atan2(Length(Cross(u, v)), Dot(u, v)); | |
| 261 | } | ||
| 262 | |||
| 263 | 3884841 | Vec3d Surface::Position(const Vec2d& point) const { | |
| 264 | 3884841 | if (kind_ == Kind::Plane) { | |
| 265 | 88916 | return {point.x, point.y, 0.0}; | |
| 266 | } | ||
| 267 | 3795925 | const Vec3d unit = UnitVector(point.x, point.y); | |
| 268 | 3795925 | const double r = IsNonSphere() ? RadiusAt(point) : radius_; | |
| 269 | 3795925 | return {r * unit.x, r * unit.y, r * unit.z}; | |
| 270 | } | ||
| 271 | |||
| 272 | 46274 | LocalFrame Surface::Frame(const Vec2d& point) const { | |
| 273 | 46274 | if (kind_ == Kind::Plane) { | |
| 274 | 7 | return {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}; | |
| 275 | } | ||
| 276 | 46267 | LocalFrame frame; | |
| 277 | 46267 | LocalAxes(point.x * kDegree, point.y * kDegree, frame.east, frame.north); | |
| 278 | 46267 | frame.up = UnitVector(point.x, point.y); | |
| 279 | 46267 | if (IsNonSphere()) { | |
| 280 | 46139 | const Vec3d n = Normal(point, Position(point)); | |
| 281 | 46139 | const double along = Dot(frame.east, n); | |
| 282 | 46139 | const Vec3d east{frame.east.x - along * n.x, frame.east.y - along * n.y, | |
| 283 | 46139 | frame.east.z - along * n.z}; | |
| 284 | 46139 | const double length = Length(east); | |
| 285 | 46139 | frame.up = n; | |
| 286 | 46139 | frame.east = {east.x / length, east.y / length, east.z / length}; | |
| 287 | 46139 | frame.north = Cross(n, frame.east); | |
| 288 | } | ||
| 289 | 46267 | return frame; | |
| 290 | } | ||
| 291 | |||
| 292 | 973068 | Vec2d Surface::PointOf(const Vec3d& direction) const { | |
| 293 | 973068 | if (kind_ == Kind::Plane) { | |
| 294 | 1 | return {direction.x, direction.y}; | |
| 295 | } | ||
| 296 | 973067 | const double lon = std::atan2(direction.y, direction.x) / kDegree; | |
| 297 | 973067 | const double lat = std::atan2(direction.z, std::hypot(direction.x, direction.y)) / kDegree; | |
| 298 | 973067 | return {lon == -180.0 ? 180.0 : lon, lat}; | |
| 299 | } | ||
| 300 | |||
| 301 | 89594 | BucketIndex Surface::Bucket(const Vec2d& point, double size) const { | |
| 302 | 89594 | const Vec3d p = Position(point); | |
| 303 | 89594 | return {static_cast<long long>(std::floor(p.x / size)), | |
| 304 | 89594 | static_cast<long long>(std::floor(p.y / size)), | |
| 305 | 179188 | static_cast<long long>(std::floor(p.z / size))}; | |
| 306 | } | ||
| 307 | |||
| 308 | 10894196 | Bounds2d Surface::Expand(const Bounds2d& region, double metres) const { | |
| 309 | 10894196 | if (kind_ == Kind::Plane) { | |
| 310 | 10870919 | return region.Expanded(metres); | |
| 311 | } | ||
| 312 | 23277 | double dLat = metres / unitLength_; | |
| 313 | 23277 | double angle = metres / radius_; | |
| 314 | 23277 | if (IsNonSphere()) { | |
| 315 | 7358 | const double shortest = MinRadius(); | |
| 316 | 7358 | angle = metres < shortest ? std::asin(metres / shortest) : std::numbers::pi; | |
| 317 | 7358 | dLat = angle / kDegree; | |
| 318 | } | ||
| 319 | 69831 | Bounds2d expanded{region.minX, std::max(-90.0, region.minY - dLat), region.maxX, | |
| 320 | 23277 | std::min(90.0, region.maxY + dLat)}; | |
| 321 | 23277 | const double widest = std::max(std::fabs(region.minY), std::fabs(region.maxY)); | |
| 322 | 23277 | const double reach = std::sin(std::min(angle, std::numbers::pi / 2.0)); | |
| 323 | 23277 | const double cosLat = std::cos(std::min(widest, 90.0) * kDegree); | |
| 324 | 23275 | const double dLon = angle < std::numbers::pi / 2.0 && reach < cosLat | |
| 325 | 46552 | ? std::asin(reach / cosLat) / kDegree | |
| 326 | 23277 | : 360.0; | |
| 327 | 23277 | if (expanded.minY <= -90.0 || expanded.maxY >= 90.0 || dLon >= 180.0 || | |
| 328 | 21737 | region.maxX - region.minX + 2.0 * dLon >= 360.0) { | |
| 329 | 5278 | expanded.minX = -180.0; | |
| 330 | 5278 | expanded.maxX = 180.0; | |
| 331 | } else { | ||
| 332 | 17999 | expanded.minX = region.minX - dLon; | |
| 333 | 17999 | expanded.maxX = region.maxX + dLon; | |
| 334 | } | ||
| 335 | 23277 | return expanded; | |
| 336 | } | ||
| 337 | |||
| 338 | } // namespace terraingen | ||
| 339 |