layers/boulders/boulder_options.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/layers/boulders/boulder_options.h" | ||
| 2 | |||
| 3 | #include "terraingen/distribution/golombek_rapp.h" | ||
| 4 | #include "terraingen/distribution/power_law.h" | ||
| 5 | #include "terraingen/distribution/summed.h" | ||
| 6 | #include "terraingen/noise/worley.h" | ||
| 7 | #include "terraingen/util/checks.h" | ||
| 8 | |||
| 9 | #include <algorithm> | ||
| 10 | #include <cmath> | ||
| 11 | #include <numbers> | ||
| 12 | #include <stdexcept> | ||
| 13 | #include <string> | ||
| 14 | #include <utility> | ||
| 15 | #include <vector> | ||
| 16 | |||
| 17 | namespace terraingen { | ||
| 18 | |||
| 19 | namespace { | ||
| 20 | |||
| 21 | /// Whether `distribution` may be evaluated from `low` to `high` metres. | ||
| 22 | 71 | bool Covers(const SizeFrequencyDistribution& distribution, double low, double high) { | |
| 23 | 107 | return distribution.AllowsExtrapolation() || | |
| 24 | 107 | (distribution.ValidRange().min <= low && distribution.ValidRange().max >= high); | |
| 25 | } | ||
| 26 | |||
| 27 | /// Throws unless `distribution` is a density valid over the boulder sizes. | ||
| 28 | 73 | void CheckDistribution(const SizeFrequencyDistribution& distribution, const BoulderSizes& sizes, | |
| 29 | const char* what) { | ||
| 30 | 73 | if (distribution.Kind() != SfdKind::Density) { | |
| 31 | 6 | throw std::invalid_argument(std::string(what) + " must be a density, not a rate"); | |
| 32 | } | ||
| 33 | 71 | if (!Covers(distribution, sizes.minDiameter, sizes.maxDiameter)) { | |
| 34 | 4 | throw std::invalid_argument(std::string(what) + | |
| 35 | " must be valid from minDiameter to maxDiameter, or allow " | ||
| 36 | 6 | "extrapolation"); | |
| 37 | } | ||
| 38 | 69 | } | |
| 39 | |||
| 40 | /// Checks the settings that only matter when craters throw blocks. | ||
| 41 | 69 | void CheckThrownBlocks(const BoulderEjecta& ejecta, const BoulderSizes& sizes) { | |
| 42 | 69 | if (ejecta.distribution) { | |
| 43 | 21 | CheckDistribution(*ejecta.distribution, sizes, "The boulder ejecta distribution"); | |
| 44 | 48 | } else if (!(ejecta.coverage > 0.0 && ejecta.coverage < 1.0)) { | |
| 45 | 1 | throw std::invalid_argument("Boulder ejecta coverage must be between 0 and 1"); | |
| 46 | } | ||
| 47 | 66 | if (!(std::isfinite(ejecta.reach) && ejecta.reach > 1.0)) { | |
| 48 | 1 | throw std::invalid_argument("Boulder ejecta need a reach over 1 radius"); | |
| 49 | } | ||
| 50 | 65 | if (!(IsPositive(ejecta.decayRadii) && IsInUnitInterval(ejecta.floorShare))) { | |
| 51 | throw std::invalid_argument( | ||
| 52 | 1 | "Boulder ejecta need a positive decayRadii and a floorShare in [0, 1]"); | |
| 53 | } | ||
| 54 | 128 | if (!(ejecta.minFreshness >= 0.0 && ejecta.minFreshness < 1.0 && | |
| 55 | 64 | IsPositive(ejecta.lifetimeGyr) && IsNonNegative(ejecta.agingExponent))) { | |
| 56 | throw std::invalid_argument("Boulder ejecta need minFreshness in [0, 1), a positive " | ||
| 57 | 1 | "lifetimeGyr and a non-negative agingExponent"); | |
| 58 | } | ||
| 59 | 63 | if (ejecta.gravity && !IsPositive(*ejecta.gravity)) { | |
| 60 | 1 | throw std::invalid_argument("Boulder ejecta gravity must be positive"); | |
| 61 | } | ||
| 62 | 62 | } | |
| 63 | |||
| 64 | } // namespace | ||
| 65 | |||
| 66 | 418043 | bool BoulderModelThreshold::IsModel(double diameter, double spacing) const { | |
| 67 | 829847 | const bool large = (minDiameter && diameter >= *minDiameter) || | |
| 68 | 411804 | (minSamples && spacing > 0.0 && diameter >= *minSamples * spacing); | |
| 69 | 424290 | return large && !(maxDiameter && diameter >= *maxDiameter) && | |
| 70 | 424290 | !(maxSamples && spacing > 0.0 && diameter >= *maxSamples * spacing); | |
| 71 | } | ||
| 72 | |||
| 73 | 1010 | double BoulderEjecta::MinCraterDiameter() const { | |
| 74 | 1010 | const double excavated = regolithThickness ? *regolithThickness / excavationDepthRatio : 0.0; | |
| 75 | 1010 | return std::fmax(minCraterDiameter, excavated); | |
| 76 | } | ||
| 77 | |||
| 78 | 9 | double BoulderThrowDistance(double craterDiameter, double regolithThickness, double gravity) { | |
| 79 | 32 | for (const double value : {craterDiameter, regolithThickness, gravity}) { | |
| 80 | 25 | RequirePositive(value, "Crater diameter, regolith thickness and gravity"); | |
| 81 | } | ||
| 82 | 7 | return 138.5 / gravity * std::pow(craterDiameter / regolithThickness, 0.7086); | |
| 83 | } | ||
| 84 | |||
| 85 | 400 | void ValidateBoulderSizes(const BoulderSizes& sizes) { | |
| 86 | 799 | if (!(IsPositive(sizes.minDiameter) && std::isfinite(sizes.maxDiameter) && | |
| 87 | 399 | sizes.maxDiameter > sizes.minDiameter)) { | |
| 88 | 2 | throw std::invalid_argument("Boulder diameters must satisfy 0 < minDiameter < maxDiameter"); | |
| 89 | } | ||
| 90 | 398 | if (sizes.distribution) { | |
| 91 | 52 | CheckDistribution(*sizes.distribution, sizes, "The boulder distribution"); | |
| 92 | 346 | } else if (!(sizes.coverage > 0.0 && sizes.coverage < 1.0)) { | |
| 93 | 4 | throw std::invalid_argument("Boulder coverage must be between 0 and 1"); | |
| 94 | } | ||
| 95 | 392 | } | |
| 96 | |||
| 97 | 393 | void ValidateBoulderPatches(const BoulderPatches& patches) { | |
| 98 | 393 | if (!(std::isfinite(patches.spread) && patches.spread >= 1.0 && IsPositive(patches.size))) { | |
| 99 | 3 | throw std::invalid_argument("Boulder patches need spread >= 1 and a positive size"); | |
| 100 | } | ||
| 101 | 390 | } | |
| 102 | |||
| 103 | 381 | void ValidateBoulderEjecta(const BoulderEjecta& ejecta, const BoulderSizes& sizes) { | |
| 104 | 381 | if (!IsNonNegative(ejecta.minCraterDiameter)) { | |
| 105 | 1 | throw std::invalid_argument("Boulder ejecta minCraterDiameter must not be negative"); | |
| 106 | } | ||
| 107 | 380 | if (ejecta.regolithThickness && !IsPositive(*ejecta.regolithThickness)) { | |
| 108 | 1 | throw std::invalid_argument("Boulder ejecta regolithThickness must be positive"); | |
| 109 | } | ||
| 110 | 379 | if (!(ejecta.excavationDepthRatio > 0.0 && ejecta.excavationDepthRatio <= 1.0)) { | |
| 111 | 1 | throw std::invalid_argument("Boulder ejecta excavationDepthRatio must be in (0, 1]"); | |
| 112 | } | ||
| 113 | 378 | if (ejecta.MinCraterDiameter() > 0.0) { | |
| 114 | 69 | CheckThrownBlocks(ejecta, sizes); | |
| 115 | } | ||
| 116 | 371 | } | |
| 117 | |||
| 118 | 388 | void ValidateBoulderBedrock(const BoulderBedrock& bedrock) { | |
| 119 | 388 | RequirePositive(bedrock.abundance, "Boulder bedrockAbundance"); | |
| 120 | 386 | if (bedrock.exposure) { | |
| 121 | 28 | ValidateExposureParams(*bedrock.exposure); | |
| 122 | } | ||
| 123 | 385 | if (!IsPositive(bedrock.exposureSpacing)) { | |
| 124 | 2 | throw std::invalid_argument("Boulder exposureSpacing must be positive"); | |
| 125 | } | ||
| 126 | 383 | RequirePositive(bedrock.meanSize, "Boulder bedrock meanSize"); | |
| 127 | 383 | } | |
| 128 | |||
| 129 | 373 | void ValidateBoulderPose(const BoulderPose& pose) { | |
| 130 | 744 | if (!(pose.minHeightRatio > 0.0 && pose.maxHeightRatio >= pose.minHeightRatio && | |
| 131 | 371 | std::isfinite(pose.maxHeightRatio))) { | |
| 132 | 2 | throw std::invalid_argument("Boulder height ratios must satisfy 0 < min <= max"); | |
| 133 | } | ||
| 134 | 371 | if (!(pose.minBurial >= 0.0 && pose.maxBurial >= pose.minBurial && pose.maxBurial < 1.0)) { | |
| 135 | 2 | throw std::invalid_argument("Boulder burial must satisfy 0 <= min <= max < 1"); | |
| 136 | } | ||
| 137 | 369 | if (!IsNonNegative(pose.maxLean)) { | |
| 138 | 1 | throw std::invalid_argument("Boulder maxLean must not be negative"); | |
| 139 | } | ||
| 140 | 368 | if (!(pose.burialByWear >= 0.0 && pose.burialByWear <= 1.0)) { | |
| 141 | 1 | throw std::invalid_argument("Boulder burialByWear must be in [0, 1]"); | |
| 142 | } | ||
| 143 | 367 | if (!(pose.sideShare >= 0.0 && pose.endShare >= 0.0 && pose.sideShare + pose.endShare <= 1.0)) { | |
| 144 | throw std::invalid_argument( | ||
| 145 | 1 | "Boulder sideShare and endShare must not be negative and must sum to at most 1"); | |
| 146 | } | ||
| 147 | 366 | if (!(pose.maxTilt >= 0.0 && pose.maxTilt <= std::numbers::pi)) { | |
| 148 | ✗ | throw std::invalid_argument("Boulder maxTilt must be in [0, pi]"); | |
| 149 | } | ||
| 150 | 366 | if (pose.maxSlope && !(*pose.maxSlope > 0.0 && *pose.maxSlope < 0.5 * std::numbers::pi)) { | |
| 151 | 2 | throw std::invalid_argument("Boulder maxSlope must be in (0, pi / 2)"); | |
| 152 | } | ||
| 153 | 372 | if (pose.rollDistance && | |
| 154 | 372 | !(pose.maxSlope && *pose.rollDistance > 0.0 && std::isfinite(*pose.rollDistance))) { | |
| 155 | throw std::invalid_argument("Boulder rollDistance must be positive and finite, with a " | ||
| 156 | 1 | "maxSlope"); | |
| 157 | } | ||
| 158 | 363 | } | |
| 159 | |||
| 160 | 365 | void ValidateBoulderFillet(const BoulderFillet& fillet) { | |
| 161 | 365 | if (!(fillet.heightShare >= 0.0 && fillet.heightShare <= 1.0)) { | |
| 162 | 2 | throw std::invalid_argument("Boulder fillet heightShare must be in [0, 1]"); | |
| 163 | } | ||
| 164 | 363 | if (!IsPositive(fillet.widthRatio)) { | |
| 165 | 1 | throw std::invalid_argument("Boulder fillet widthRatio must be positive"); | |
| 166 | } | ||
| 167 | 362 | } | |
| 168 | |||
| 169 | 391 | void ValidateBoulderClusters(const BoulderClusters& clusters) { | |
| 170 | 391 | if (!(clusters.share >= 0.0 && clusters.share <= 1.0)) { | |
| 171 | ✗ | throw std::invalid_argument("Boulder cluster share must be in [0, 1]"); | |
| 172 | } | ||
| 173 | 391 | if (clusters.model == BoulderClusterModel::Thomas) { | |
| 174 | 20 | if (!(clusters.radius > 0.0 && clusters.radius <= 2.0 * clusters.spacing && | |
| 175 | 9 | std::isfinite(clusters.spacing))) { | |
| 176 | throw std::invalid_argument( | ||
| 177 | 2 | "Thomas boulder clusters must satisfy 0 < radius <= 2 spacing"); | |
| 178 | } | ||
| 179 | 9 | return; | |
| 180 | } | ||
| 181 | 758 | if (!(clusters.radius > 0.0 && clusters.radius < clusters.spacing && | |
| 182 | 378 | std::isfinite(clusters.spacing))) { | |
| 183 | 2 | throw std::invalid_argument("Boulder clusters must satisfy 0 < radius < spacing"); | |
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | 2054165 | double ClusterBump(double distance, const BoulderClusters& clusters) { | |
| 188 | 2054165 | const double r = distance / clusters.radius; | |
| 189 | 2054165 | return std::exp(-0.5 * r * r); | |
| 190 | } | ||
| 191 | |||
| 192 | 28 | double MeanClusterBump(const BoulderClusters& clusters) { | |
| 193 | 28 | constexpr int kSamples = 256; | |
| 194 | 28 | constexpr double kCells = 16.0; | |
| 195 | 28 | double sum = 0.0; | |
| 196 | 7196 | for (int j = 0; j < kSamples; ++j) { | |
| 197 | 1842176 | for (int i = 0; i < kSamples; ++i) { | |
| 198 | 1835008 | const double u = (i + 0.5) * kCells / kSamples; | |
| 199 | 1835008 | const double v = (j + 0.5) * kCells / kSamples; | |
| 200 | const WorleyDistances d = | ||
| 201 | 1835008 | Worley2(static_cast<float>(u), static_cast<float>(v), 0x434C5553u); | |
| 202 | 1835008 | sum += ClusterBump(static_cast<double>(d.f1) * clusters.spacing, clusters); | |
| 203 | } | ||
| 204 | } | ||
| 205 | 28 | return sum / (kSamples * kSamples); | |
| 206 | } | ||
| 207 | |||
| 208 | 26 | double MeanThomasField(const BoulderClusters& clusters, bool body) { | |
| 209 | 26 | const double sigma = clusters.radius; | |
| 210 | 26 | const double reach = kThomasReachRadii; | |
| 211 | 26 | const double perArea = 1.0 / (clusters.spacing * clusters.spacing); | |
| 212 | 26 | if (body) { | |
| 213 | 3 | const double perVolume = perArea / (std::sqrt(2.0 * std::numbers::pi) * sigma); | |
| 214 | 3 | const double inside = | |
| 215 | std::erf(reach / std::numbers::sqrt2) - | ||
| 216 | std::sqrt(2.0 / std::numbers::pi) * reach * std::exp(-0.5 * reach * reach); | ||
| 217 | 3 | return perVolume * std::pow(2.0 * std::numbers::pi * sigma * sigma, 1.5) * inside; | |
| 218 | } | ||
| 219 | 23 | return perArea * 2.0 * std::numbers::pi * sigma * sigma * | |
| 220 | 23 | (1.0 - std::exp(-0.5 * reach * reach)); | |
| 221 | } | ||
| 222 | |||
| 223 | 819 | double MaxClusterAbundance(const BoulderClusters& clusters) { | |
| 224 | 819 | if (clusters.share <= 0.0) { | |
| 225 | 778 | return 1.0; | |
| 226 | } | ||
| 227 | 41 | if (clusters.model == BoulderClusterModel::Nearest) { | |
| 228 | 23 | return 1.0 - clusters.share + clusters.share / MeanClusterBump(clusters); | |
| 229 | } | ||
| 230 | 18 | constexpr double kChance = 1e-6; | |
| 231 | 18 | const double ratio = clusters.radius / clusters.spacing; | |
| 232 | 18 | const double mean = 9.0 * std::numbers::pi * ratio * ratio; | |
| 233 | 18 | double term = std::exp(-mean); | |
| 234 | 18 | double above = 1.0 - term; | |
| 235 | 18 | int crowd = 0; | |
| 236 | 540 | while (above >= kChance) { | |
| 237 | 522 | ++crowd; | |
| 238 | 522 | term *= mean / crowd; | |
| 239 | 522 | above -= term; | |
| 240 | } | ||
| 241 | 18 | return 1.0 - clusters.share + | |
| 242 | 18 | clusters.share * std::fmax(1.0, crowd) / MeanThomasField(clusters); | |
| 243 | } | ||
| 244 | |||
| 245 | 389 | void ValidateBoulderFragmentation(const BoulderFragmentation& fragmentation) { | |
| 246 | 389 | RequirePositive(fragmentation.minDiameter, "Boulder fragmentation minDiameter"); | |
| 247 | 389 | if (!(fragmentation.splitWear >= 0.0 && fragmentation.rubbleWear >= fragmentation.splitWear && | |
| 248 | 388 | fragmentation.rubbleWear <= 1.0)) { | |
| 249 | throw std::invalid_argument( | ||
| 250 | 1 | "Boulder fragmentation must satisfy 0 <= splitWear <= rubbleWear <= 1"); | |
| 251 | } | ||
| 252 | 388 | if (!(fragmentation.minPieces >= 2 && fragmentation.maxPieces >= fragmentation.minPieces && | |
| 253 | 386 | fragmentation.maxPieces <= 12)) { | |
| 254 | 2 | throw std::invalid_argument("Boulder fragments must satisfy 2 <= min <= max <= 12 pieces"); | |
| 255 | } | ||
| 256 | 386 | RequireNonNegative(fragmentation.gapRatio, "Boulder fragmentation gapRatio"); | |
| 257 | 386 | RequireNonNegative(fragmentation.spreadRatio, "Boulder fragmentation spreadRatio"); | |
| 258 | 386 | if (!(fragmentation.slabShare >= 0.0 && fragmentation.slabShare <= 1.0)) { | |
| 259 | 1 | throw std::invalid_argument("Boulder fragmentation slabShare must be in [0, 1]"); | |
| 260 | } | ||
| 261 | 385 | } | |
| 262 | |||
| 263 | 371 | void ValidateBoulderVentifacts(const BoulderVentifacts& ventifacts) { | |
| 264 | 740 | const auto unit = [](double v) { return v >= 0.0 && v <= 1.0; }; | |
| 265 | 371 | if (!(unit(ventifacts.share) && unit(ventifacts.keelShare))) { | |
| 266 | 3 | throw std::invalid_argument("Boulder ventifact share and keelShare must be in [0, 1]"); | |
| 267 | } | ||
| 268 | 368 | if (!(ventifacts.minAngle > 0.0 && ventifacts.minAngle <= ventifacts.maxAngle && | |
| 269 | 365 | ventifacts.maxAngle < 0.5 * std::numbers::pi)) { | |
| 270 | throw std::invalid_argument( | ||
| 271 | 4 | "Boulder ventifact facet angles must satisfy 0 < minAngle <= maxAngle < 90 deg"); | |
| 272 | } | ||
| 273 | 735 | if (!(std::isfinite(ventifacts.windAzimuth) && | |
| 274 | 371 | (!ventifacts.secondWindAzimuth || std::isfinite(*ventifacts.secondWindAzimuth)))) { | |
| 275 | 2 | throw std::invalid_argument("Boulder ventifact wind azimuths must be finite"); | |
| 276 | } | ||
| 277 | 362 | } | |
| 278 | |||
| 279 | 2 | BoulderVentifacts GaleVentifacts() { | |
| 280 | 2 | BoulderVentifacts ventifacts; | |
| 281 | 2 | ventifacts.share = 0.5 * 0.78; | |
| 282 | 2 | ventifacts.keelShare = 12.0 / 39.0; | |
| 283 | 2 | return ventifacts; | |
| 284 | } | ||
| 285 | |||
| 286 | 20854 | double FilletReach(const Boulder& boulder, const BoulderFillet& fillet) { | |
| 287 | 20854 | return (0.5 + fillet.widthRatio) * boulder.diameter; | |
| 288 | } | ||
| 289 | |||
| 290 | 100500 | double FilletHeight(const Boulder& boulder, double distance, const BoulderFillet& fillet) { | |
| 291 | 100500 | const double radius = 0.5 * boulder.diameter; | |
| 292 | 100500 | const double top = fillet.heightShare * boulder.ExposedHeight(); | |
| 293 | 100500 | if (distance <= radius) { | |
| 294 | 25504 | return top; | |
| 295 | } | ||
| 296 | 74996 | const double t = (distance - radius) / (fillet.widthRatio * boulder.diameter); | |
| 297 | 74996 | return t >= 1.0 ? 0.0 : top * (1.0 - t) * (1.0 - t); | |
| 298 | } | ||
| 299 | |||
| 300 | 362 | void ValidateBoulderRendering(const BoulderRendering& rendering) { | |
| 301 | 362 | const BoulderModelThreshold& models = rendering.models; | |
| 302 | 1798 | for (const auto& threshold : | |
| 303 | 2160 | {models.minDiameter, models.minSamples, models.maxDiameter, models.maxSamples}) { | |
| 304 | 1440 | if (threshold) { | |
| 305 | 73 | RequirePositive(*threshold, "Boulder model thresholds"); | |
| 306 | } | ||
| 307 | } | ||
| 308 | 358 | const BoulderReliefRange& range = rendering.reliefRange; | |
| 309 | 358 | RequireNonNegative(range.minDiameter, "Boulder relief minDiameter"); | |
| 310 | 358 | if (range.maxDiameter && !(*range.maxDiameter > range.minDiameter)) { | |
| 311 | 1 | throw std::invalid_argument("Boulder relief maxDiameter must exceed its minDiameter"); | |
| 312 | } | ||
| 313 | 357 | ValidateBoulderShapeOptions(rendering.shapes); | |
| 314 | 357 | } | |
| 315 | |||
| 316 | 396 | void ValidateBoulderLayerOptions(const BoulderLayerOptions& options) { | |
| 317 | 396 | ValidateBoulderSizes(options.sizes); | |
| 318 | 391 | if (options.densityMap) { | |
| 319 | 12 | ValidateDensityMap(*options.densityMap); | |
| 320 | } | ||
| 321 | 391 | if (options.slopeMap) { | |
| 322 | 13 | ValidateSizeSlopeMap(*options.slopeMap); | |
| 323 | 13 | if (!options.sizes.distribution) { | |
| 324 | throw std::invalid_argument( | ||
| 325 | 1 | "A boulder size slope map needs a size distribution, not Golombek-Rapp sizes"); | |
| 326 | } | ||
| 327 | } | ||
| 328 | 390 | ValidateBoulderPatches(options.patches); | |
| 329 | 389 | ValidateBoulderClusters(options.clusters); | |
| 330 | 386 | ValidateBoulderFragmentation(options.fragmentation); | |
| 331 | 385 | ValidateBoulderBedrock(options.bedrock); | |
| 332 | 714 | if (!options.sizes.distribution && | |
| 333 | 332 | options.sizes.coverage * MaxBackgroundAbundance(options) >= 1.0) { | |
| 334 | throw std::invalid_argument("Boulder coverage times the patch spread, cluster and bedrock " | ||
| 335 | 3 | "abundance must stay below 1"); | |
| 336 | } | ||
| 337 | 379 | ValidateBoulderEjecta(options.ejecta, options.sizes); | |
| 338 | 370 | ValidateBoulderPose(options.pose); | |
| 339 | 362 | ValidateBoulderFillet(options.fillet); | |
| 340 | 362 | ValidateBoulderVentifacts(options.ventifacts); | |
| 341 | 361 | ValidateResolutionFade(options.fade); | |
| 342 | 360 | ValidateBoulderRendering(options.rendering); | |
| 343 | 356 | } | |
| 344 | |||
| 345 | namespace { | ||
| 346 | |||
| 347 | /// Coverages the Golombek-Rapp measures are tabulated at, log-spaced. | ||
| 348 | constexpr int kCoverageSteps = 512; | ||
| 349 | constexpr double kLeastCoverage = 1e-6; | ||
| 350 | constexpr double kMostCoverage = 0.999; | ||
| 351 | |||
| 352 | /// What `map` measures of `distribution`, with the areal fraction of Golombek-Rapp boulders in | ||
| 353 | /// closed form so that it needs no largest diameter. | ||
| 354 | 6150 | double BoulderMeasure(const DensityMap& map, const SizeFrequencyDistribution& distribution) { | |
| 355 | 6150 | const auto* rapp = dynamic_cast<const GolombekRappSfd*>(&distribution); | |
| 356 | 6150 | if (rapp != nullptr && map.measure == DensityMeasure::ArealFraction) { | |
| 357 | const double beyond = | ||
| 358 | 5632 | std::isinf(map.maxDiameter) ? 0.0 : rapp->FractionalArea(map.maxDiameter); | |
| 359 | 5632 | return rapp->FractionalArea(map.minDiameter) - beyond; | |
| 360 | } | ||
| 361 | 518 | return MapMeasure(map, distribution); | |
| 362 | } | ||
| 363 | |||
| 364 | } // namespace | ||
| 365 | |||
| 366 | 39 | BoulderMapAbundance::BoulderMapAbundance(const BoulderLayerOptions& options) { | |
| 367 | 39 | if (!options.densityMap) { | |
| 368 | 1 | throw std::invalid_argument("Boulder map abundance needs a density map"); | |
| 369 | } | ||
| 370 | 38 | map_ = *options.densityMap; | |
| 371 | 38 | ValidateDensityMap(map_); | |
| 372 | 38 | const BoulderSizes& sizes = options.sizes; | |
| 373 | 38 | if (map_.total && options.ejecta.MinCraterDiameter() > 0.0) { | |
| 374 | 1 | ejectaMeasure_ = BoulderMeasure(map_, *EjectaSizes(options)); | |
| 375 | } | ||
| 376 | 38 | if (sizes.distribution || map_.measure == DensityMeasure::Scale) { | |
| 377 | 26 | measure_ = sizes.distribution ? BoulderMeasure(map_, *sizes.distribution) : 1.0; | |
| 378 | 26 | if (!(measure_ > 0.0 && std::isfinite(measure_))) { | |
| 379 | ✗ | throw std::invalid_argument("A boulder density map must measure some of the sizes"); | |
| 380 | } | ||
| 381 | } else { | ||
| 382 | 12 | coverage_ = sizes.coverage; | |
| 383 | 6156 | for (int i = 0; i < kCoverageSteps; ++i) { | |
| 384 | const double k = | ||
| 385 | 6144 | kLeastCoverage * std::pow(kMostCoverage / kLeastCoverage, | |
| 386 | 6144 | static_cast<double>(i) / (kCoverageSteps - 1)); | |
| 387 | 6144 | coverages_.push_back(k); | |
| 388 | 6144 | measures_.push_back(BoulderMeasure( | |
| 389 | 12288 | map_, GolombekRappSfd(k, sizes.qConstant, sizes.qCoverageCoefficient))); | |
| 390 | } | ||
| 391 | 12 | if (!(measures_.back() > 0.0)) { | |
| 392 | ✗ | throw std::invalid_argument("A boulder density map must measure some of the sizes"); | |
| 393 | } | ||
| 394 | } | ||
| 395 | 38 | const double top = map_.field->Max(); | |
| 396 | 38 | const double largest = map_.measure == DensityMeasure::Count ? top / map_.areaUnit : top; | |
| 397 | 38 | max_ = std::fmax(1.0, At(largest, 0.0)); | |
| 398 | 41 | } | |
| 399 | |||
| 400 | 2777 | double BoulderMapAbundance::Coverage(double target) const { | |
| 401 | 2777 | if (target <= measures_.front()) { | |
| 402 | 2 | return coverages_.front() * target / measures_.front(); | |
| 403 | } | ||
| 404 | 2775 | if (target >= measures_.back()) { | |
| 405 | 1 | return coverages_.back(); | |
| 406 | } | ||
| 407 | 2774 | const auto above = std::lower_bound(measures_.begin(), measures_.end(), target); | |
| 408 | 2774 | const auto i = static_cast<std::size_t>(above - measures_.begin()); | |
| 409 | 2774 | const double t = (target - measures_[i - 1]) / (measures_[i] - measures_[i - 1]); | |
| 410 | 2774 | return coverages_[i - 1] + t * (coverages_[i] - coverages_[i - 1]); | |
| 411 | } | ||
| 412 | |||
| 413 | 1083759 | double BoulderMapAbundance::At(double value, double ejecta) const { | |
| 414 | 1083759 | if (std::isnan(value)) { | |
| 415 | 30001 | return 1.0; | |
| 416 | } | ||
| 417 | 1053758 | if (map_.measure == DensityMeasure::Scale) { | |
| 418 | 1050975 | return std::fmax(0.0, value); | |
| 419 | } | ||
| 420 | 2783 | const double target = value - ejecta * ejectaMeasure_; | |
| 421 | 2783 | if (!(target > 0.0)) { | |
| 422 | 1 | return 0.0; | |
| 423 | } | ||
| 424 | 2782 | return coverages_.empty() ? target / measure_ : Coverage(target) / coverage_; | |
| 425 | } | ||
| 426 | |||
| 427 | 36 | double MeanPatchAbundance(const BoulderPatches& patches) { | |
| 428 | 36 | if (!(patches.spread > 1.0)) { | |
| 429 | 30 | return 1.0; | |
| 430 | } | ||
| 431 | 6 | constexpr int kSamples = 256; | |
| 432 | 6 | constexpr double kPatches = 32.0; | |
| 433 | 6 | double sum = 0.0; | |
| 434 | 1542 | for (int j = 0; j < kSamples; ++j) { | |
| 435 | 394752 | for (int i = 0; i < kSamples; ++i) { | |
| 436 | 393216 | const double x = (i + 0.5) * kPatches / kSamples; | |
| 437 | 393216 | const double y = (j + 0.5) * kPatches / kSamples; | |
| 438 | 393216 | sum += std::pow(patches.spread, PatchNoise({}, x, y, 1.0, 0x50415443u)); | |
| 439 | } | ||
| 440 | } | ||
| 441 | 6 | return sum / (kSamples * kSamples); | |
| 442 | } | ||
| 443 | |||
| 444 | 815 | double MaxBackgroundAbundance(const BoulderLayerOptions& options) { | |
| 445 | 815 | const double a = options.bedrock.abundance; | |
| 446 | 815 | double patches = options.patches.spread; | |
| 447 | 815 | double bedrock = options.bedrock.exposure ? std::fmax(1.0, a) : 1.0; | |
| 448 | 815 | double map = 1.0; | |
| 449 | 815 | if (options.densityMap) { | |
| 450 | 26 | patches /= MeanPatchAbundance(options.patches); | |
| 451 | 26 | bedrock = options.bedrock.exposure ? std::fmax(1.0, a) / std::fmin(1.0, a) : 1.0; | |
| 452 | 26 | map = BoulderMapAbundance(options).Max(); | |
| 453 | } | ||
| 454 | 815 | return patches * bedrock * MaxClusterAbundance(options.clusters) * map; | |
| 455 | } | ||
| 456 | |||
| 457 | 76 | std::shared_ptr<const SizeFrequencyDistribution> EjectaSizes(const BoulderLayerOptions& options) { | |
| 458 | 76 | const BoulderEjecta& ejecta = options.ejecta; | |
| 459 | 76 | if (ejecta.distribution) { | |
| 460 | 21 | return ejecta.distribution; | |
| 461 | } | ||
| 462 | 55 | return std::make_shared<GolombekRappSfd>(ejecta.coverage, options.sizes.qConstant, | |
| 463 | 55 | options.sizes.qCoverageCoefficient); | |
| 464 | } | ||
| 465 | |||
| 466 | std::shared_ptr<const SizeFrequencyDistribution> | ||
| 467 | 306 | BoulderCandidates(const BoulderLayerOptions& options) { | |
| 468 | 306 | ValidateBoulderLayerOptions(options); | |
| 469 | 291 | const BoulderSizes& sizes = options.sizes; | |
| 470 | 291 | const double richest = MaxBackgroundAbundance(options); | |
| 471 | 291 | std::vector<WeightedSfd> terms; | |
| 472 | 291 | if (sizes.distribution) { | |
| 473 | 38 | terms.push_back({richest, sizes.distribution}); | |
| 474 | 38 | if (options.slopeMap) { | |
| 475 | 10 | const SizeSlopeMap& map = *options.slopeMap; | |
| 476 | 10 | const double d0 = map.referenceDiameter; | |
| 477 | 10 | const double count = richest * sizes.distribution->Cumulative(d0); | |
| 478 | 10 | const double low = map.minExponent; | |
| 479 | 10 | const double high = map.maxExponent; | |
| 480 | 10 | terms.push_back( | |
| 481 | 10 | {1.0, std::make_shared<PowerLawSfd>(count * std::pow(d0, low) * high / low, low)}); | |
| 482 | 10 | terms.push_back({1.0, std::make_shared<PowerLawSfd>(count * std::pow(d0, high), high)}); | |
| 483 | } | ||
| 484 | } else { | ||
| 485 | 253 | terms.push_back( | |
| 486 | 253 | {1.0, std::make_shared<GolombekRappSfd>(sizes.coverage * richest, sizes.qConstant, | |
| 487 | 253 | sizes.qCoverageCoefficient)}); | |
| 488 | } | ||
| 489 | 291 | if (options.ejecta.MinCraterDiameter() > 0.0) { | |
| 490 | 45 | terms.push_back({1.0, EjectaSizes(options)}); | |
| 491 | } | ||
| 492 | 582 | return std::make_shared<SummedSfd>(std::move(terms)); | |
| 493 | 647 | } | |
| 494 | |||
| 495 | } // namespace terraingen | ||
| 496 |