layers/boulders/abundance.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/layers/boulders/boulder_layer.h" | ||
| 2 | |||
| 3 | #include "terraingen/layers/exposure.h" | ||
| 4 | #include "terraingen/layers/fade.h" | ||
| 5 | #include "terraingen/noise/worley.h" | ||
| 6 | #include "terraingen/random/philox.h" | ||
| 7 | #include "terraingen/random/samplers.h" | ||
| 8 | #include "terraingen/random/stream.h" | ||
| 9 | #include "terraingen/util/checks.h" | ||
| 10 | #include "terraingen/util/parallel.h" | ||
| 11 | |||
| 12 | #include <algorithm> | ||
| 13 | #include <cmath> | ||
| 14 | #include <cstdint> | ||
| 15 | #include <numbers> | ||
| 16 | |||
| 17 | namespace terraingen { | ||
| 18 | |||
| 19 | namespace { | ||
| 20 | |||
| 21 | constexpr std::uint32_t kBoulderPatches = 0x50415443u; | ||
| 22 | constexpr std::uint32_t kBoulderClusters = 0x434C5553u; | ||
| 23 | constexpr std::uint32_t kThomasCentres2 = 0x54484D32u; | ||
| 24 | constexpr std::uint32_t kThomasCentres3 = 0x54484D33u; | ||
| 25 | constexpr std::uint32_t kBedrockMeans = 0x4245444Du; | ||
| 26 | |||
| 27 | /// Golombek-Rapp boulders per m^2 per metre of diameter at `diameter` for coverage `k`. | ||
| 28 | 5858606 | double GolombekRappDensity(double diameter, double k, double qa, double qb) { | |
| 29 | 5858606 | const double q = qa + qb / k; | |
| 30 | 5858606 | return k * q * std::exp(-q * diameter) / (std::numbers::pi * diameter * diameter / 4.0); | |
| 31 | } | ||
| 32 | |||
| 33 | /// How far, in radii from its centre, a crater's blocks reach. | ||
| 34 | 1219494 | double EjectaReach(const BoulderEjecta& ejecta, double diameter) { | |
| 35 | 1219494 | if (!(ejecta.gravity && ejecta.regolithThickness)) { | |
| 36 | 1219490 | return ejecta.reach; | |
| 37 | } | ||
| 38 | 4 | const double radius = 0.5 * diameter; | |
| 39 | const double thrown = | ||
| 40 | 4 | BoulderThrowDistance(diameter, *ejecta.regolithThickness, *ejecta.gravity) / radius; | |
| 41 | 4 | return std::fmin(ejecta.reach, std::fmax(1.0, thrown)); | |
| 42 | } | ||
| 43 | |||
| 44 | /// A crater's share of blocks at `rho` radii from its centre, within its `reach`. | ||
| 45 | 265596 | double RadialShare(const BoulderEjecta& ejecta, double rho, double reach) { | |
| 46 | 265596 | if (ejecta.profile == EjectaProfile::Smoothstep) { | |
| 47 | 265592 | return rho <= 1.0 ? 1.0 : Smoothstep((reach - rho) / (reach - 1.0)); | |
| 48 | } | ||
| 49 | 4 | return rho <= 1.0 ? ejecta.floorShare : std::exp(-(rho - 1.0) / ejecta.decayRadii); | |
| 50 | } | ||
| 51 | |||
| 52 | /// The share of its blocks `crater` keeps at its age. | ||
| 53 | 205345 | double Retention(const BoulderEjecta& ejecta, const Crater& crater, | |
| 54 | const DegradationModel& degradation) { | ||
| 55 | 205345 | if (ejecta.aging == EjectaAging::Freshness) { | |
| 56 | 205341 | const CraterState state = degradation.State(crater); | |
| 57 | 205341 | const double freshness = state.fresh.depthRatio > 0.0 ? state.Freshness() : 0.0; | |
| 58 | 205341 | return Smoothstep((freshness - ejecta.minFreshness) / (1.0 - ejecta.minFreshness)); | |
| 59 | } | ||
| 60 | 4 | return std::pow(std::fmax(0.0, 1.0 - crater.ageGyr / ejecta.lifetimeGyr), ejecta.agingExponent); | |
| 61 | } | ||
| 62 | |||
| 63 | } // namespace | ||
| 64 | |||
| 65 | 2884723 | EjectaIndex BoulderLayer::EjectaCraters(const Bounds2d& region) const { | |
| 66 | 2884723 | if (minCraterDiameter_ <= 0.0) { | |
| 67 | 2878436 | return {}; | |
| 68 | } | ||
| 69 | 6287 | const double margin = options_.ejecta.reach * 0.5 * craters_->MaxFeatureDiameter(); | |
| 70 | 6287 | const double spacing = minCraterDiameter_ / craters_->Options().apply.fade.startSamples; | |
| 71 | 6287 | const CraterList all = craters_->Craters(surface_.Expand(region, margin), spacing); | |
| 72 | ✗ | CraterList ejecta = FilterCraters(all, [&](std::size_t, const Crater& crater) { | |
| 73 | 15065 | return crater.diameter >= minCraterDiameter_; | |
| 74 | 6287 | }); | |
| 75 | 12630 | const auto reach = [&](const Crater& crater) { | |
| 76 | 12630 | return EjectaReach(options_.ejecta, crater.diameter) * 0.5 * crater.diameter; | |
| 77 | 6287 | }; | |
| 78 | 6287 | return {std::move(ejecta), surface_, reach}; | |
| 79 | 6287 | } | |
| 80 | |||
| 81 | 219157 | double BoulderLayer::NearestClusterDistance(double x, double y) const { | |
| 82 | 219157 | const double spacing = options_.clusters.spacing; | |
| 83 | 219157 | const std::uint32_t seed = options_.seed ^ kBoulderClusters; | |
| 84 | 219157 | if (surface_.IsBody()) { | |
| 85 | 2088 | const Vec3d p = surface_.Position({x, y}); | |
| 86 | 2088 | const double s = 1.0 / spacing; | |
| 87 | 2088 | return spacing * static_cast<double>( | |
| 88 | 2088 | Worley3(SplitLatticePrecise3(s * p.x, s * p.y, s * p.z), seed).f1); | |
| 89 | } | ||
| 90 | return spacing * | ||
| 91 | 217069 | static_cast<double>( | |
| 92 | 217069 | Worley2(static_cast<float>(x / spacing), static_cast<float>(y / spacing), seed).f1); | |
| 93 | } | ||
| 94 | |||
| 95 | 392549 | double BoulderLayer::ThomasClusterField(double x, double y) const { | |
| 96 | 392549 | const BoulderClusters& clusters = options_.clusters; | |
| 97 | 392549 | const double sigma = clusters.radius; | |
| 98 | 392549 | const double side = kThomasReachRadii * sigma; | |
| 99 | 392549 | const double perArea = 1.0 / (clusters.spacing * clusters.spacing); | |
| 100 | 392549 | const std::uint32_t seed = options_.seed ^ kBoulderClusters; | |
| 101 | 12735882 | const auto word = [](double cell) { | |
| 102 | 12735882 | return static_cast<std::uint32_t>(static_cast<std::int64_t>(cell)); | |
| 103 | }; | ||
| 104 | 12762650 | const auto bump = [&](double squared) { | |
| 105 | 12762650 | return squared < side * side ? std::exp(-0.5 * squared / (sigma * sigma)) : 0.0; | |
| 106 | 392549 | }; | |
| 107 | 392549 | double sum = 0.0; | |
| 108 | 392549 | if (surface_.IsBody()) { | |
| 109 | 90000 | const Vec3d p = surface_.Position({x, y}); | |
| 110 | 90000 | const double perCell = | |
| 111 | 90000 | perArea / (std::sqrt(2.0 * std::numbers::pi) * sigma) * side * side * side; | |
| 112 | 90000 | const double cx = std::floor(p.x / side); | |
| 113 | 90000 | const double cy = std::floor(p.y / side); | |
| 114 | 90000 | const double cz = std::floor(p.z / side); | |
| 115 | 360000 | for (double k = cz - 1.0; k <= cz + 1.0; k += 1.0) { | |
| 116 | 1080000 | for (double j = cy - 1.0; j <= cy + 1.0; j += 1.0) { | |
| 117 | 3240000 | for (double i = cx - 1.0; i <= cx + 1.0; i += 1.0) { | |
| 118 | 2430000 | RandomStream random(seed, kThomasCentres3, word(i), word(j), word(k)); | |
| 119 | 2430000 | const std::uint64_t count = SamplePoisson(random, perCell); | |
| 120 | 11255112 | for (std::uint64_t n = 0; n < count; ++n) { | |
| 121 | 8825112 | const double dx = (i + random.NextDouble()) * side - p.x; | |
| 122 | 8825112 | const double dy = (j + random.NextDouble()) * side - p.y; | |
| 123 | 8825112 | const double dz = (k + random.NextDouble()) * side - p.z; | |
| 124 | 8825112 | sum += bump(dx * dx + dy * dy + dz * dz); | |
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 128 | } | ||
| 129 | 90000 | return sum; | |
| 130 | } | ||
| 131 | 302549 | const double perCell = perArea * side * side; | |
| 132 | 302549 | const double cx = std::floor(x / side); | |
| 133 | 302549 | const double cy = std::floor(y / side); | |
| 134 | 1210196 | for (double j = cy - 1.0; j <= cy + 1.0; j += 1.0) { | |
| 135 | 3630588 | for (double i = cx - 1.0; i <= cx + 1.0; i += 1.0) { | |
| 136 | 2722941 | RandomStream random(seed, kThomasCentres2, word(i), word(j)); | |
| 137 | 2722941 | const std::uint64_t count = SamplePoisson(random, perCell); | |
| 138 | 6660479 | for (std::uint64_t n = 0; n < count; ++n) { | |
| 139 | 3937538 | const double dx = (i + random.NextDouble()) * side - x; | |
| 140 | 3937538 | const double dy = (j + random.NextDouble()) * side - y; | |
| 141 | 3937538 | sum += bump(dx * dx + dy * dy); | |
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
| 145 | 302549 | return sum; | |
| 146 | } | ||
| 147 | |||
| 148 | 3712818 | double BoulderLayer::BackgroundAbundance(double x, double y, const Ground* ground, | |
| 149 | double ejecta) const { | ||
| 150 | double background = | ||
| 151 | 3712818 | mapAbundance_ ? mapAbundance_->At(MapValueAt(*options_.densityMap, x, y), ejecta) : 1.0; | |
| 152 | 3712818 | const BoulderPatches& patches = options_.patches; | |
| 153 | 3712818 | if (patches.spread > 1.0) { | |
| 154 | const double noise = | ||
| 155 | 2157678 | PatchNoise(surface_, x, y, patches.size, options_.seed ^ kBoulderPatches); | |
| 156 | 2157678 | background *= std::pow(patches.spread, noise) / patchMean_; | |
| 157 | } | ||
| 158 | 3712818 | const BoulderClusters& clusters = options_.clusters; | |
| 159 | 3712818 | if (clusters.share > 0.0) { | |
| 160 | 611706 | const double field = clusters.model == BoulderClusterModel::Thomas | |
| 161 | 611706 | ? ThomasClusterField(x, y) | |
| 162 | 219157 | : ClusterBump(NearestClusterDistance(x, y), clusters); | |
| 163 | 611706 | background *= 1.0 - clusters.share + clusters.share * field / clusterMean_; | |
| 164 | } | ||
| 165 | 3712818 | if (exposure_) { | |
| 166 | 126790 | RequireGround(ground, "Boulders that read the exposure field"); | |
| 167 | 126789 | const double bedrock = exposure_->Exposure(*ground, x, y, options_.bedrock.exposureSpacing); | |
| 168 | 126789 | background *= 1.0 + bedrock * (options_.bedrock.abundance - 1.0); | |
| 169 | 126789 | if (bedrockMeans_) { | |
| 170 | 80002 | background /= BedrockMean(x, y, *ground); | |
| 171 | } | ||
| 172 | } | ||
| 173 | 7425634 | return std::min(background, maxBackground_); | |
| 174 | } | ||
| 175 | |||
| 176 | 640104 | std::size_t BoulderLayer::MeanCellHash::operator()(const MeanCell& cell) const { | |
| 177 | 640104 | const auto words = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(cell.ground)); | |
| 178 | const U32x4 hash = | ||
| 179 | 640104 | Philox4x32({static_cast<std::uint32_t>(cell.i), static_cast<std::uint32_t>(cell.j), | |
| 180 | 640104 | static_cast<std::uint32_t>(words), static_cast<std::uint32_t>(words >> 32)}, | |
| 181 | {kBedrockMeans, 0u}); | ||
| 182 | 640104 | return (static_cast<std::size_t>(hash.x) << 32) ^ hash.y; | |
| 183 | } | ||
| 184 | |||
| 185 | 80002 | double BoulderLayer::BedrockMean(double x, double y, const Ground& ground) const { | |
| 186 | 80002 | constexpr int kSide = 8; | |
| 187 | 80002 | const double gain = options_.bedrock.abundance - 1.0; | |
| 188 | 320008 | const auto cellMean = [&](std::int64_t i, std::int64_t j) { | |
| 189 | 640016 | return *bedrockMeans_->Get({&ground, i, j}, [&] { | |
| 190 | 72 | double sum = 0.0; | |
| 191 | 648 | for (int b = 0; b < kSide; ++b) { | |
| 192 | 5184 | for (int a = 0; a < kSide; ++a) { | |
| 193 | 4608 | const double px = | |
| 194 | 4608 | meanOrigin_.x + (static_cast<double>(i) + (a + 0.5) / kSide) * meanCell_.x; | |
| 195 | 4608 | const double py = | |
| 196 | 4608 | meanOrigin_.y + (static_cast<double>(j) + (b + 0.5) / kSide) * meanCell_.y; | |
| 197 | 9216 | sum += 1.0 + gain * exposure_->Exposure(ground, px, py, | |
| 198 | 4608 | options_.bedrock.exposureSpacing); | |
| 199 | } | ||
| 200 | } | ||
| 201 | 72 | return sum / (kSide * kSide); | |
| 202 | 640016 | }); | |
| 203 | 80002 | }; | |
| 204 | 80002 | const double u = (x - meanOrigin_.x) / meanCell_.x - 0.5; | |
| 205 | 80002 | const double v = (y - meanOrigin_.y) / meanCell_.y - 0.5; | |
| 206 | 80002 | const double fu = std::floor(u); | |
| 207 | 80002 | const double fv = std::floor(v); | |
| 208 | 80002 | const auto i = static_cast<std::int64_t>(fu); | |
| 209 | 80002 | const auto j = static_cast<std::int64_t>(fv); | |
| 210 | 80002 | const double tu = u - fu; | |
| 211 | 80002 | const double tv = v - fv; | |
| 212 | 80002 | return (1.0 - tu) * (1.0 - tv) * cellMean(i, j) + tu * (1.0 - tv) * cellMean(i + 1, j) + | |
| 213 | 160004 | (1.0 - tu) * tv * cellMean(i, j + 1) + tu * tv * cellMean(i + 1, j + 1); | |
| 214 | } | ||
| 215 | |||
| 216 | 193137 | void BoulderLayer::EjectaAbundance(double x, double y, const EjectaIndex& ejecta, | |
| 217 | BoulderAbundance& abundance) const { | ||
| 218 | 193137 | const BoulderEjecta& eject = options_.ejecta; | |
| 219 | 193137 | const DegradationModel& degradation = *craters_->Models().degradation; | |
| 220 | 193137 | double share = 0.0; | |
| 221 | 193137 | double wear = 0.0; | |
| 222 | 1400001 | for (const std::uint32_t n : ejecta.Near(x, y)) { | |
| 223 | 1206864 | const Crater crater = ejecta.Craters().At(n); | |
| 224 | 1206864 | const double reach = EjectaReach(eject, crater.diameter); | |
| 225 | const double rho = | ||
| 226 | 1206864 | surface_.Distance({crater.x, crater.y}, {x, y}) / (0.5 * crater.diameter); | |
| 227 | 1206864 | if (rho >= reach) { | |
| 228 | 1001519 | continue; | |
| 229 | } | ||
| 230 | 265596 | const double radial = RadialShare(eject, rho, reach); | |
| 231 | 265596 | if (radial <= share) { | |
| 232 | 60251 | continue; | |
| 233 | } | ||
| 234 | 205345 | const double retention = Retention(eject, crater, degradation); | |
| 235 | 205345 | if (retention * radial > share) { | |
| 236 | 205333 | share = retention * radial; | |
| 237 | 205333 | wear = 1.0 - retention; | |
| 238 | } | ||
| 239 | } | ||
| 240 | 193137 | abundance.ejecta = share; | |
| 241 | 193137 | abundance.ejectaWear = wear; | |
| 242 | 193137 | } | |
| 243 | |||
| 244 | 3712818 | BoulderAbundance BoulderLayer::Abundance(double x, double y, const Ground* ground, | |
| 245 | const EjectaIndex& ejecta) const { | ||
| 246 | 3712818 | BoulderAbundance abundance; | |
| 247 | 3712818 | if (!ejecta.Empty()) { | |
| 248 | 193137 | EjectaAbundance(x, y, ejecta, abundance); | |
| 249 | } | ||
| 250 | 3712818 | abundance.background = BackgroundAbundance(x, y, ground, abundance.ejecta); | |
| 251 | 3712817 | if (options_.slopeMap) { | |
| 252 | 287377 | abundance.exponent = SizeSlopeAt(*options_.slopeMap, x, y); | |
| 253 | } | ||
| 254 | 3712817 | return abundance; | |
| 255 | } | ||
| 256 | |||
| 257 | 2740647 | BoulderAbundance BoulderLayer::AbundanceAt(double x, double y, const Ground* ground) const { | |
| 258 | 2740648 | return Abundance(x, y, ground, EjectaCraters(PointBounds(x, y))); | |
| 259 | } | ||
| 260 | |||
| 261 | 6259317 | double BoulderLayer::Density(double diameter, const BoulderAbundance& abundance) const { | |
| 262 | 6259317 | const BoulderSizes& sizes = options_.sizes; | |
| 263 | 6259317 | double density = 0.0; | |
| 264 | 6259317 | if (abundance.exponent) { | |
| 265 | 287376 | const double alpha = *abundance.exponent; | |
| 266 | 287376 | const double d0 = options_.slopeMap->referenceDiameter; | |
| 267 | 287376 | density = abundance.background * referenceCount_ * alpha * std::pow(d0, alpha) * | |
| 268 | 287376 | std::pow(diameter, -alpha - 1.0); | |
| 269 | } else { | ||
| 270 | 11943882 | density = sizes.distribution | |
| 271 | 5971941 | ? abundance.background * sizes.distribution->Incremental(diameter) | |
| 272 | 5858606 | : GolombekRappDensity(diameter, sizes.coverage * abundance.background, | |
| 273 | 5858606 | sizes.qConstant, sizes.qCoverageCoefficient); | |
| 274 | } | ||
| 275 | 6259317 | if (abundance.ejecta > 0.0) { | |
| 276 | 4558924 | density += abundance.ejecta * ejectaSizes_->Incremental(diameter); | |
| 277 | } | ||
| 278 | 6259317 | return density; | |
| 279 | } | ||
| 280 | |||
| 281 | 13 | double BoulderLayer::DensityAt(double diameter, double x, double y, const Ground* ground) const { | |
| 282 | 13 | return Density(diameter, AbundanceAt(x, y, ground)); | |
| 283 | } | ||
| 284 | |||
| 285 | 170 | std::vector<BoulderLayer::UnresolvedNode> BoulderLayer::UnresolvedNodes(double spacing) const { | |
| 286 | 170 | const BoulderSizes& sizes = options_.sizes; | |
| 287 | 170 | const double start = options_.fade.startSamples * spacing; | |
| 288 | 170 | const double end = std::fmin(options_.fade.endSamples * spacing, sizes.maxDiameter); | |
| 289 | 170 | std::vector<UnresolvedNode> nodes; | |
| 290 | 340 | const auto add = [&](double low, double high) { | |
| 291 | 340 | if (!(high > low)) { | |
| 292 | 4 | return; | |
| 293 | } | ||
| 294 | 336 | constexpr int kIntervals = 16; | |
| 295 | 336 | const double a = std::log(low); | |
| 296 | 336 | const double h = (std::log(high) - a) / kIntervals; | |
| 297 | 6048 | for (int i = 0; i <= kIntervals; ++i) { | |
| 298 | 5712 | const double d = std::exp(a + h * i); | |
| 299 | 5712 | const double simpson = (i == 0 || i == kIntervals) ? 1.0 : (i % 2 == 1 ? 4.0 : 2.0); | |
| 300 | 5712 | const double unresolved = 1.0 - ResolutionWeight(d, spacing, options_.fade); | |
| 301 | 5712 | if (unresolved > 0.0) { | |
| 302 | 5546 | nodes.push_back( | |
| 303 | 5546 | {d, 0.25 * std::numbers::pi * d * d, unresolved * d * simpson * h / 3.0}); | |
| 304 | } | ||
| 305 | } | ||
| 306 | 170 | }; | |
| 307 | 170 | add(sizes.minDiameter, std::fmin(start, end)); | |
| 308 | 170 | add(std::fmax(sizes.minDiameter, start), end); | |
| 309 | 340 | return nodes; | |
| 310 | } | ||
| 311 | |||
| 312 | 165220 | UnresolvedCover BoulderLayer::UnresolvedCoverWith(const BoulderAbundance& abundance, | |
| 313 | const std::vector<UnresolvedNode>& nodes, | ||
| 314 | double spacing) const { | ||
| 315 | 165220 | UnresolvedCover cover; | |
| 316 | 5617416 | for (const UnresolvedNode& node : nodes) { | |
| 317 | 5452196 | const double n = Density(node.diameter, abundance) * node.weight; | |
| 318 | 5452196 | cover.mean += node.footprint * n; | |
| 319 | 5452196 | cover.variance += node.footprint * node.footprint * n; | |
| 320 | } | ||
| 321 | 165220 | cover.variance /= spacing * spacing; | |
| 322 | 165220 | return cover; | |
| 323 | } | ||
| 324 | |||
| 325 | 167 | UnresolvedCover BoulderLayer::UnresolvedCoverAt(double x, double y, double spacing, | |
| 326 | const Ground* ground) const { | ||
| 327 | 167 | RequirePositive(spacing, "Unresolved cover spacing"); | |
| 328 | 166 | if (options_.sizes.minDiameter >= options_.fade.endSamples * spacing) { | |
| 329 | 1 | return {}; | |
| 330 | } | ||
| 331 | 165 | return UnresolvedCoverWith(AbundanceAt(x, y, ground), UnresolvedNodes(spacing), spacing); | |
| 332 | } | ||
| 333 | |||
| 334 | 6 | std::vector<UnresolvedCover> BoulderLayer::UnresolvedCoverOver(const RasterGrid& grid, | |
| 335 | double spacing, const Ground* ground, | ||
| 336 | unsigned threads) const { | ||
| 337 | 6 | RequirePositive(spacing, "Unresolved cover spacing"); | |
| 338 | 6 | std::vector<UnresolvedCover> cover(grid.width * grid.height); | |
| 339 | 6 | if (options_.sizes.minDiameter >= options_.fade.endSamples * spacing) { | |
| 340 | 1 | return cover; | |
| 341 | } | ||
| 342 | 5 | const EjectaIndex ejecta = EjectaCraters(grid.Bounds()); | |
| 343 | 5 | const std::vector<UnresolvedNode> nodes = UnresolvedNodes(spacing); | |
| 344 | 5 | ParallelFor(grid.height, threads, [&](std::size_t row) { | |
| 345 | 165541 | for (std::size_t col = 0; col < grid.width; ++col) { | |
| 346 | 165055 | const Vec2d centre = grid.transform.PixelToWorld(static_cast<double>(col) + 0.5, | |
| 347 | 165055 | static_cast<double>(row) + 0.5); | |
| 348 | 165055 | cover[row * grid.width + col] = | |
| 349 | 330110 | UnresolvedCoverWith(Abundance(centre.x, centre.y, ground, ejecta), nodes, spacing); | |
| 350 | } | ||
| 351 | 486 | }); | |
| 352 | 5 | return cover; | |
| 353 | 5 | } | |
| 354 | |||
| 355 | } // namespace terraingen | ||
| 356 |