util/radial_shape.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/util/radial_shape.h" | ||
| 2 | |||
| 3 | #include "terraingen/util/cube_faces.h" | ||
| 4 | |||
| 5 | #include <algorithm> | ||
| 6 | #include <array> | ||
| 7 | #include <cmath> | ||
| 8 | #include <map> | ||
| 9 | #include <numbers> | ||
| 10 | #include <stdexcept> | ||
| 11 | #include <utility> | ||
| 12 | |||
| 13 | namespace terraingen { | ||
| 14 | |||
| 15 | namespace { | ||
| 16 | |||
| 17 | constexpr double kQuarter = std::numbers::pi / 4.0; | ||
| 18 | |||
| 19 | /// Factor on the largest area per steradian found at quadrilateral corners, for its variation | ||
| 20 | /// within the curved bilinear quadrilaterals. | ||
| 21 | constexpr double kAreaMargin = 1.25; | ||
| 22 | |||
| 23 | /// The gnomonic coordinate of label `k` of `q`, exactly -1, 0 or 1 at multiples of 45 degrees and | ||
| 24 | /// odd in `k - q/2`, so labels shared between faces get bitwise the same direction. | ||
| 25 | 15068710 | double Gnomonic(std::size_t k, std::size_t q) { | |
| 26 | 15068710 | if (2 * k > q) { | |
| 27 | 4951770 | return -Gnomonic(q - k, q); | |
| 28 | } | ||
| 29 | 10116940 | if (k == 0) { | |
| 30 | 341584 | return -1.0; | |
| 31 | } | ||
| 32 | 9775356 | if (2 * k == q) { | |
| 33 | 213320 | return 0.0; | |
| 34 | } | ||
| 35 | 9562036 | return std::tan(kQuarter * (2.0 * static_cast<double>(k) / static_cast<double>(q) - 1.0)); | |
| 36 | } | ||
| 37 | |||
| 38 | } // namespace | ||
| 39 | |||
| 40 | 5058476 | Vec3d RadialShape::LabelDirection(std::size_t q, std::size_t i, std::size_t j, std::size_t face) { | |
| 41 | 5058476 | if (q == 0 || i > q || j > q || face < 1 || face > 6) { | |
| 42 | 6 | throw std::invalid_argument("An ICQ label needs Q > 0, I and J <= Q and F from 1 to 6"); | |
| 43 | } | ||
| 44 | const Vec3d p = | ||
| 45 | 5058470 | detail::CubeFacePoint(detail::kIcqFaces[face - 1], Gnomonic(i, q), Gnomonic(j, q)); | |
| 46 | 5058470 | const double length = Length(p); | |
| 47 | 5058470 | return {p.x / length, p.y / length, p.z / length}; | |
| 48 | } | ||
| 49 | |||
| 50 | 81 | RadialShape RadialShape::Sample(std::size_t q, const std::function<double(const Vec3d&)>& radius) { | |
| 51 | 81 | if (q == 0) { | |
| 52 | 1 | throw std::invalid_argument("A radial shape needs Q > 0"); | |
| 53 | } | ||
| 54 | 80 | std::vector<double> radii(6 * (q + 1) * (q + 1)); | |
| 55 | 548 | for (std::size_t face = 1; face <= 6; ++face) { | |
| 56 | 21182 | for (std::size_t j = 0; j <= q; ++j) { | |
| 57 | 1033214 | for (std::size_t i = 0; i <= q; ++i) { | |
| 58 | 2025000 | radii[((face - 1) * (q + 1) + j) * (q + 1) + i] = | |
| 59 | 1012502 | radius(LabelDirection(q, i, j, face)); | |
| 60 | } | ||
| 61 | } | ||
| 62 | } | ||
| 63 | 156 | return RadialShape(q, std::move(radii)); | |
| 64 | 80 | } | |
| 65 | |||
| 66 | 91 | RadialShape::RadialShape(std::size_t q, std::vector<double> radii) | |
| 67 | 91 | : q_(q), radii_(std::move(radii)) { | |
| 68 | 91 | if (q_ == 0) { | |
| 69 | ✗ | throw std::invalid_argument("A radial shape needs Q > 0"); | |
| 70 | } | ||
| 71 | 91 | if (radii_.size() != 6 * (q_ + 1) * (q_ + 1)) { | |
| 72 | 2 | throw std::invalid_argument("A radial shape needs 6 (Q + 1)^2 label radii"); | |
| 73 | } | ||
| 74 | 1023693 | for (const double r : radii_) { | |
| 75 | 1023605 | if (!(std::isfinite(r) && r > 0.0)) { | |
| 76 | 1 | throw std::invalid_argument("A radial shape's radii must be positive and finite"); | |
| 77 | } | ||
| 78 | } | ||
| 79 | 88 | const auto [lo, hi] = std::minmax_element(radii_.begin(), radii_.end()); | |
| 80 | 88 | min_ = *lo; | |
| 81 | 88 | max_ = *hi; | |
| 82 | 88 | std::map<std::array<double, 3>, double> shared; | |
| 83 | 612 | for (std::size_t face = 1; face <= 6; ++face) { | |
| 84 | 21885 | for (std::size_t j = 0; j <= q_; ++j) { | |
| 85 | 1044925 | for (std::size_t i = 0; i <= q_; ++i) { | |
| 86 | 1023565 | if (i != 0 && i != q_ && j != 0 && j != q_) { | |
| 87 | 940220 | continue; | |
| 88 | } | ||
| 89 | 83345 | const Vec3d d = LabelDirection(q_, i, j, face); | |
| 90 | 83345 | const double r = radii_[LabelIndex(i, j, face)]; | |
| 91 | 83345 | const auto [it, inserted] = shared.emplace(std::array<double, 3>{d.x, d.y, d.z}, r); | |
| 92 | 83345 | if (!inserted && std::fabs(it->second - r) > 1e-9 * max_) { | |
| 93 | throw std::invalid_argument( | ||
| 94 | 1 | "A radial shape's labels shared between faces must have equal radii"); | |
| 95 | } | ||
| 96 | } | ||
| 97 | } | ||
| 98 | } | ||
| 99 | 609 | for (std::size_t face = 1; face <= 6; ++face) { | |
| 100 | 21354 | for (std::size_t j = 0; j < q_; ++j) { | |
| 101 | 1002192 | for (std::size_t i = 0; i < q_; ++i) { | |
| 102 | const std::array<std::pair<std::size_t, std::size_t>, 4> corners{ | ||
| 103 | 981360 | {{i, j}, {i + 1, j}, {i + 1, j + 1}, {i, j + 1}}}; | |
| 104 | 981360 | std::array<Vec3d, 4> p{}; | |
| 105 | 4906800 | for (std::size_t k = 0; k < 4; ++k) { | |
| 106 | 3925440 | const auto [ci, cj] = corners[k]; | |
| 107 | 3925440 | p[k] = LabelDirection(q_, ci, cj, face) * radii_[LabelIndex(ci, cj, face)]; | |
| 108 | } | ||
| 109 | 981360 | Vec3d normal = Cross(p[2] - p[0], p[3] - p[1]); | |
| 110 | 981360 | normal = normal * (1.0 / Length(normal)); | |
| 111 | 981360 | if (Dot(normal, p[0]) < 0.0) { | |
| 112 | 981360 | normal = normal * -1.0; | |
| 113 | } | ||
| 114 | 4906800 | for (const Vec3d& corner : p) { | |
| 115 | 3925440 | const double r = Length(corner); | |
| 116 | 3925440 | const double cosine = Dot(corner, normal) / r; | |
| 117 | 3925440 | maxArea_ = std::max(maxArea_, r * r / std::max(cosine, 1e-3)); | |
| 118 | } | ||
| 119 | } | ||
| 120 | } | ||
| 121 | } | ||
| 122 | 87 | maxArea_ *= kAreaMargin; | |
| 123 | 92 | } | |
| 124 | |||
| 125 | 7606049 | double RadialShape::At(const Vec3d& direction) const { | |
| 126 | 7606049 | const std::size_t index = detail::CubeFaceOf(detail::kIcqFaces, direction); | |
| 127 | 7606049 | const detail::CubeFace& f = detail::kIcqFaces[index]; | |
| 128 | 7606049 | const double along = Dot(direction, f.normal); | |
| 129 | 15212098 | const auto position = [&](const Vec3d& axis) { | |
| 130 | 15212098 | const double angle = std::atan(Dot(direction, axis) / along); | |
| 131 | 15212098 | const double s = (angle + kQuarter) / (2.0 * kQuarter) * static_cast<double>(q_); | |
| 132 | 15212098 | const double k = std::clamp(std::floor(s), 0.0, static_cast<double>(q_ - 1)); | |
| 133 | 15212098 | return std::pair<std::size_t, double>{static_cast<std::size_t>(k), | |
| 134 | 30424196 | std::clamp(s - k, 0.0, 1.0)}; | |
| 135 | 7606049 | }; | |
| 136 | 7606049 | const auto [i, fi] = position(f.u); | |
| 137 | 7606049 | const auto [j, fj] = position(f.v); | |
| 138 | 7606049 | const std::size_t face = index + 1; | |
| 139 | 7606049 | const double r00 = radii_[LabelIndex(i, j, face)]; | |
| 140 | 7606049 | const double r10 = radii_[LabelIndex(i + 1, j, face)]; | |
| 141 | 7606049 | const double r01 = radii_[LabelIndex(i, j + 1, face)]; | |
| 142 | 7606049 | const double r11 = radii_[LabelIndex(i + 1, j + 1, face)]; | |
| 143 | 7606049 | return (1.0 - fj) * ((1.0 - fi) * r00 + fi * r10) + fj * ((1.0 - fi) * r01 + fi * r11); | |
| 144 | } | ||
| 145 | |||
| 146 | } // namespace terraingen | ||
| 147 |