config/build.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/config/build.h" | ||
| 2 | |||
| 3 | #include "terraingen/albedo/crater_albedo.h" | ||
| 4 | #include "terraingen/albedo/raster_albedo.h" | ||
| 5 | #include "terraingen/albedo/rock_albedo.h" | ||
| 6 | #include "terraingen/albedo/slope_albedo.h" | ||
| 7 | #include "terraingen/albedo/synthetic.h" | ||
| 8 | #include "terraingen/config/fields/checks.h" | ||
| 9 | #include "terraingen/config/generate.h" | ||
| 10 | #include "terraingen/config/validate.h" | ||
| 11 | #include "terraingen/distribution/equilibrium.h" | ||
| 12 | #include "terraingen/distribution/ivanov.h" | ||
| 13 | #include "terraingen/distribution/log_polynomial.h" | ||
| 14 | #include "terraingen/distribution/minimum.h" | ||
| 15 | #include "terraingen/distribution/neukum.h" | ||
| 16 | #include "terraingen/distribution/power_law.h" | ||
| 17 | #include "terraingen/distribution/production_surface.h" | ||
| 18 | #include "terraingen/distribution/spliced.h" | ||
| 19 | #include "terraingen/distribution/tabulated.h" | ||
| 20 | #include "terraingen/layers/craters/ages/age_model.h" | ||
| 21 | #include "terraingen/layers/craters/ages/steady_state.h" | ||
| 22 | #include "terraingen/layers/craters/degradation/degradation_model.h" | ||
| 23 | #include "terraingen/layers/craters/degradation/diffusivities.h" | ||
| 24 | #include "terraingen/layers/craters/degradation/freshness.h" | ||
| 25 | #include "terraingen/layers/craters/profiles/martin_parkes_dunstan.h" | ||
| 26 | #include "terraingen/layers/craters/shaping/morphometry.h" | ||
| 27 | #include "terraingen/layers/detail.h" | ||
| 28 | #include "terraingen/layers/noise_relief.h" | ||
| 29 | #include "terraingen/layers/transforms.h" | ||
| 30 | #include "terraingen/mesh/io/icq.h" | ||
| 31 | #include "terraingen/mesh/io/mesh_io.h" | ||
| 32 | #include "terraingen/mesh/shape_model.h" | ||
| 33 | #include "terraingen/sources/geotiff_source.h" | ||
| 34 | #include "terraingen/sources/source_chain.h" | ||
| 35 | #include "terraingen/util/overloaded.h" | ||
| 36 | |||
| 37 | #include <algorithm> | ||
| 38 | #include <cmath> | ||
| 39 | #include <filesystem> | ||
| 40 | #include <limits> | ||
| 41 | #include <map> | ||
| 42 | #include <memory> | ||
| 43 | #include <mutex> | ||
| 44 | #include <optional> | ||
| 45 | #include <span> | ||
| 46 | #include <string> | ||
| 47 | #include <tuple> | ||
| 48 | #include <utility> | ||
| 49 | #include <variant> | ||
| 50 | #include <vector> | ||
| 51 | |||
| 52 | namespace terraingen { | ||
| 53 | |||
| 54 | namespace { | ||
| 55 | |||
| 56 | using config::At; | ||
| 57 | using config::BuildBoulderOptions; | ||
| 58 | using config::BuildRegionShape; | ||
| 59 | using config::Child; | ||
| 60 | using config::Element; | ||
| 61 | using config::Resolve; | ||
| 62 | using SfdPtr = std::shared_ptr<const SizeFrequencyDistribution>; | ||
| 63 | using LayerPtr = std::shared_ptr<const Layer>; | ||
| 64 | |||
| 65 | 660 | SfdPtr BuildProduction(ProductionFunction production) { | |
| 66 | 660 | switch (production) { | |
| 67 | 10 | case ProductionFunction::Neukum: | |
| 68 | 10 | return std::make_shared<LogPolynomialSfd>(NeukumProductionFunction()); | |
| 69 | 13 | case ProductionFunction::Ivanov: | |
| 70 | 13 | return std::make_shared<LogPolynomialSfd>(IvanovMarsProductionFunction()); | |
| 71 | 637 | case ProductionFunction::ExtendedNeukum: | |
| 72 | 637 | break; | |
| 73 | } | ||
| 74 | 637 | return std::make_shared<SplicedSfd>(ExtendedNeukumProductionFunction()); | |
| 75 | } | ||
| 76 | |||
| 77 | 660 | Chronology BuildChronology(ChronologyModel chronology) { | |
| 78 | 660 | return chronology == ChronologyModel::Ivanov ? Chronology(IvanovMarsChronology) | |
| 79 | 660 | : Chronology(NeukumLunarChronology); | |
| 80 | } | ||
| 81 | |||
| 82 | 318 | SfdPtr BuildEquilibrium(EquilibriumModel equilibrium) { | |
| 83 | 636 | return std::make_shared<PowerLawSfd>( | |
| 84 | 954 | equilibrium == EquilibriumModel::Hartmann ? HartmannEquilibrium() : TraskEquilibrium()); | |
| 85 | } | ||
| 86 | |||
| 87 | 1090 | SfdPtr BuildDensity(const DensitySpec& spec) { | |
| 88 | return std::visit( | ||
| 89 | 1080 | Overloaded{ | |
| 90 | 342 | [](const ProductionSurfaceSpec& s) -> SfdPtr { | |
| 91 | 684 | return std::make_shared<ProductionSurface>(BuildProduction(s.production), s.ageGyr, | |
| 92 | 1026 | BuildChronology(s.chronology)); | |
| 93 | }, | ||
| 94 | 326 | [](const MinimumSpec& s) -> SfdPtr { | |
| 95 | 326 | std::vector<SfdPtr> distributions; | |
| 96 | 970 | for (const DensitySpec& distribution : s.distributions) { | |
| 97 | 645 | distributions.push_back(BuildDensity(distribution)); | |
| 98 | } | ||
| 99 | 647 | return std::make_shared<MinimumSfd>(std::move(distributions)); | |
| 100 | 326 | }, | |
| 101 | 59 | [](const PowerLawSpec& s) -> SfdPtr { | |
| 102 | 59 | return std::make_shared<PowerLawSfd>(s.coefficient, s.exponent); | |
| 103 | }, | ||
| 104 | 5 | [](const PiecewisePowerLawSpec& s) -> SfdPtr { | |
| 105 | 5 | const double start = s.segments.empty() ? 0.0 : s.segments.front().start; | |
| 106 | 5 | return std::make_shared<PiecewisePowerLawSfd>(s.segments, SfdKind::Density, | |
| 107 | 10 | DiameterRange{start}); | |
| 108 | }, | ||
| 109 | 8 | [](const TabulatedSpec& s) -> SfdPtr { | |
| 110 | 8 | return std::make_shared<TabulatedSfd>(s.diameters, s.cumulative, SfdKind::Density, | |
| 111 | 13 | s.diameterUnit, s.areaUnit); | |
| 112 | }, | ||
| 113 | 9 | [](const PiecewiseUniformSpec& s) -> SfdPtr { | |
| 114 | 9 | return std::make_shared<PiecewiseUniformSfd>(s.bands, SfdKind::Density, s.total); | |
| 115 | }, | ||
| 116 | 334 | [](const TraskSpec&) -> SfdPtr { | |
| 117 | 334 | return std::make_shared<PowerLawSfd>(TraskEquilibrium()); | |
| 118 | }, | ||
| 119 | 1 | [](const HartmannSpec&) -> SfdPtr { | |
| 120 | 1 | return std::make_shared<PowerLawSfd>(HartmannEquilibrium()); | |
| 121 | }, | ||
| 122 | 3 | [](const MooreSpec& s) -> SfdPtr { | |
| 123 | 3 | return std::make_shared<PiecewisePowerLawSfd>(MooreDistribution(s.terrain)); | |
| 124 | }, | ||
| 125 | 3 | [](const ViperSpec&) -> SfdPtr { | |
| 126 | 3 | return std::make_shared<PiecewisePowerLawSfd>(ViperSpecification()); | |
| 127 | }, | ||
| 128 | }, | ||
| 129 | 2170 | spec); | |
| 130 | } | ||
| 131 | |||
| 132 | 419 | std::shared_ptr<const CraterAgeModel> BuildAges(const AgesSpec& spec) { | |
| 133 | using Result = std::shared_ptr<const CraterAgeModel>; | ||
| 134 | return std::visit( | ||
| 135 | 415 | Overloaded{ | |
| 136 | 5 | [](const FreshAgesSpec&) -> Result { return std::make_shared<FreshAges>(); }, | |
| 137 | 96 | [](const UniformAgesSpec& s) -> Result { | |
| 138 | 96 | return std::make_shared<UniformAges>(s.lifetimeGyr); | |
| 139 | }, | ||
| 140 | 318 | [](const SteadyStateAgesSpec& s) -> Result { | |
| 141 | 636 | return std::make_shared<SteadyStateAges>(BuildProduction(s.production), | |
| 142 | 638 | BuildEquilibrium(s.equilibrium), s.ageGyr, | |
| 143 | 954 | BuildChronology(s.chronology)); | |
| 144 | }, | ||
| 145 | }, | ||
| 146 | 834 | spec); | |
| 147 | } | ||
| 148 | |||
| 149 | 423 | FreshMorphometry MorphometryOf(CraterMorphometryFit fit) { | |
| 150 | 423 | return fit == CraterMorphometryFit::Mars ? FreshMorphometry(MarsFreshCraterMorphometry) | |
| 151 | 423 | : FreshMorphometry(LunarFreshCraterMorphometry); | |
| 152 | } | ||
| 153 | |||
| 154 | 4 | ComplexCraterShape ComplexShapeOf(CraterMorphometryFit fit) { | |
| 155 | 4 | return fit == CraterMorphometryFit::Mars ? MarsComplexCraterShape() : LunarComplexCraterShape(); | |
| 156 | } | ||
| 157 | |||
| 158 | 31 | DiffusionAgeModel BuildDiffusionAges(const DiffusionDegradationSpec& spec, const AgesSpec& ages) { | |
| 159 | 31 | switch (spec.diffusivity) { | |
| 160 | 18 | case DiffusivityModel::Matched: { | |
| 161 | 18 | const auto* steady = std::get_if<SteadyStateAgesSpec>(&ages); | |
| 162 | 17 | return steady && steady->equilibrium == EquilibriumModel::Trask | |
| 163 | 52 | ? Fassett2022TraskDiffusivity() | |
| 164 | 18 | : Fassett2022HartmannDiffusivity(); | |
| 165 | } | ||
| 166 | 6 | case DiffusivityModel::Fassett2022Hartmann: | |
| 167 | 6 | return Fassett2022HartmannDiffusivity(); | |
| 168 | 1 | case DiffusivityModel::Fassett2022Trask: | |
| 169 | 1 | return Fassett2022TraskDiffusivity(); | |
| 170 | 1 | case DiffusivityModel::FassettThomson2014: | |
| 171 | 1 | return FassettThomson2014Diffusivity(); | |
| 172 | 1 | case DiffusivityModel::FassettThomson2014Calibration: | |
| 173 | 2 | return [](double, double ageGyr) { return FassettThomson2014DiffusionAge(ageGyr); }; | |
| 174 | 4 | case DiffusivityModel::PowerLaw: | |
| 175 | 4 | ValidatePowerLawDiffusivity(spec.powerLaw); | |
| 176 | 2 | return spec.powerLaw; | |
| 177 | } | ||
| 178 | ✗ | return Fassett2022HartmannDiffusivity(); | |
| 179 | } | ||
| 180 | |||
| 181 | 415 | std::shared_ptr<const CraterProfile> BuildProfile(const ProfileSpec& spec, | |
| 182 | CraterMorphometryFit morphometry) { | ||
| 183 | using Result = std::shared_ptr<const CraterProfile>; | ||
| 184 | 415 | const auto simple = [](const SimpleProfileSpec& simpleSpec) { | |
| 185 | 415 | return std::visit(Overloaded{ | |
| 186 | 17 | [](const FassettThomsonSpec& s) -> Result { | |
| 187 | 17 | return std::make_shared<FassettThomsonProfile>(s.rim); | |
| 188 | }, | ||
| 189 | 398 | [](const MartinParkesDunstanSpec&) -> Result { | |
| 190 | 398 | return std::make_shared<MartinParkesDunstanProfile>(); | |
| 191 | }, | ||
| 192 | }, | ||
| 193 | 830 | simpleSpec); | |
| 194 | }; | ||
| 195 | 415 | return std::visit(Overloaded{ | |
| 196 | 15 | [&](const FassettThomsonSpec& s) -> Result { return simple(s); }, | |
| 197 | 396 | [&](const MartinParkesDunstanSpec& s) -> Result { return simple(s); }, | |
| 198 | 4 | [&](const ComplexProfileSpec& s) -> Result { | |
| 199 | 8 | return std::make_shared<ComplexCraterProfile>( | |
| 200 | 12 | simple(s.simple), ComplexShapeOf(morphometry)); | |
| 201 | }, | ||
| 202 | }, | ||
| 203 | 830 | spec); | |
| 204 | } | ||
| 205 | |||
| 206 | /// Sets `models.degradation` from `craters`; diffusion also wears `models.profile`. | ||
| 207 | 415 | void AddDegradation(const CraterLayerSpec& craters, CraterModels& models) { | |
| 208 | 415 | const FreshMorphometry fresh = MorphometryOf(craters.morphometry); | |
| 209 | 415 | std::visit( | |
| 210 | 5 | Overloaded{ | |
| 211 | 368 | [&](const FreshnessDegradationSpec& s) { | |
| 212 | 366 | models.degradation = | |
| 213 | 368 | std::make_shared<FreshnessDegradation>(models.ages, fresh, s.cutoffDepthRatio); | |
| 214 | 366 | }, | |
| 215 | 16 | [&](const NoDegradationSpec& s) { | |
| 216 | 16 | models.degradation = std::make_shared<NoDegradation>(fresh, s.depthRatio); | |
| 217 | 16 | }, | |
| 218 | 31 | [&](const DiffusionDegradationSpec& s) { | |
| 219 | 31 | const DiffusionAgeModel ages = BuildDiffusionAges(s, craters.ages); | |
| 220 | 29 | auto profile = std::make_shared<DiffusedProfile>(models.profile, s.reachRadii); | |
| 221 | 28 | models.degradation = std::make_shared<DiffusionDegradation>(fresh, ages, profile); | |
| 222 | 28 | models.profile = std::move(profile); | |
| 223 | 29 | }, | |
| 224 | }, | ||
| 225 | 415 | craters.degradation); | |
| 226 | 415 | } | |
| 227 | |||
| 228 | /// The distribution a boulder size spec names, or null for the layer's Golombek-Rapp boulders. | ||
| 229 | 249 | std::shared_ptr<SizeFrequencyDistribution> BuildBoulderSizes(const BoulderSizesSpec& spec) { | |
| 230 | using Result = std::shared_ptr<SizeFrequencyDistribution>; | ||
| 231 | 20 | const auto density = [](const DensitySpec& named) { | |
| 232 | 20 | return std::const_pointer_cast<SizeFrequencyDistribution>(BuildDensity(named)); | |
| 233 | }; | ||
| 234 | return std::visit( | ||
| 235 | 249 | Overloaded{ | |
| 236 | 207 | [](const GolombekRappBouldersSpec&) -> Result { return nullptr; }, | |
| 237 | 6 | [](const Sp8023IntercraterBouldersSpec& s) -> Result { | |
| 238 | 6 | return Sp8023IntercraterBlocks(s.curve); | |
| 239 | }, | ||
| 240 | 14 | [](const Sp8023CraterBouldersSpec& s) -> Result { return Sp8023CraterBlocks(s.curve); }, | |
| 241 | 2 | [](const SynthterrainBouldersSpec& s) -> Result { | |
| 242 | 2 | return SynthterrainBoulderDistribution(s.model); | |
| 243 | }, | ||
| 244 | 11 | [&](const PowerLawSpec& s) -> Result { return density(s); }, | |
| 245 | 2 | [&](const PiecewisePowerLawSpec& s) -> Result { return density(s); }, | |
| 246 | 5 | [&](const TabulatedSpec& s) -> Result { return density(s); }, | |
| 247 | 2 | [&](const PiecewiseUniformSpec& s) -> Result { return density(s); }, | |
| 248 | }, | ||
| 249 | 495 | spec); | |
| 250 | } | ||
| 251 | |||
| 252 | 13 | std::shared_ptr<const Source> BuildSource(const SourceSpec& spec, | |
| 253 | const std::filesystem::path& directory, | ||
| 254 | const Surface& surface) { | ||
| 255 | using Result = std::shared_ptr<const Source>; | ||
| 256 | 13 | return std::visit(Overloaded{ | |
| 257 | 12 | [&](const RasterSourceSpec& s) -> Result { | |
| 258 | 12 | GeoTiffSourceOptions read = s.read; | |
| 259 | 12 | if (read.pixelSize) { | |
| 260 | 1 | *read.pixelSize /= surface.UnitLength(); | |
| 261 | } | ||
| 262 | 24 | return std::make_shared<GeoTiffSource>(Resolve(s.path, directory), | |
| 263 | 24 | read); | |
| 264 | }, | ||
| 265 | 1 | [&](const ChainSourceSpec& s) -> Result { | |
| 266 | 1 | std::vector<Result> sources; | |
| 267 | 3 | for (const SourceSpec& source : s.sources) { | |
| 268 | 2 | sources.push_back(BuildSource(source, directory, surface)); | |
| 269 | } | ||
| 270 | 2 | return std::make_shared<SourceChain>(std::move(sources)); | |
| 271 | 1 | }, | |
| 272 | }, | ||
| 273 | 26 | spec); | |
| 274 | } | ||
| 275 | |||
| 276 | /// The point noise is measured from: the base's reference point, or the origin without a base. | ||
| 277 | 388 | Vec2d ReferencePoint(const Terrain& terrain) { | |
| 278 | 388 | return terrain.source ? terrain.source->ReferencePoint() : Vec2d{}; | |
| 279 | } | ||
| 280 | |||
| 281 | /// What a layer is built on: the base's reference point, the last top-level crater and boulder | ||
| 282 | /// layers so far (for boulder ejecta, secondaries and sand shadows) and the surface. | ||
| 283 | struct LayerInputs { | ||
| 284 | Vec2d referencePoint; | ||
| 285 | const std::filesystem::path& directory; | ||
| 286 | const std::shared_ptr<const CraterLayer>& craters; | ||
| 287 | const std::shared_ptr<const BoulderLayer>& boulders; | ||
| 288 | const Surface& surface; | ||
| 289 | /// The secondaries later layers throw from a crater layer built with these inputs. | ||
| 290 | std::vector<SecondaryCraterOptions> thrown{}; | ||
| 291 | }; | ||
| 292 | |||
| 293 | 128 | LayerPtr BuildCraterLayer(const CraterLayerSpec& craters, const std::string& path, | |
| 294 | const Surface& surface, const std::filesystem::path& directory, | ||
| 295 | std::vector<SecondaryCraterOptions> thrown) { | ||
| 296 | 128 | CraterLayerOptions options; | |
| 297 | 128 | options.secondaries = std::move(thrown); | |
| 298 | 128 | options.generate = craters.generate; | |
| 299 | 128 | options.seed = craters.seed; | |
| 300 | 128 | options.generation.dMin = craters.minDiameter; | |
| 301 | 128 | options.generation.dMax = craters.maxDiameter; | |
| 302 | 128 | options.generation.spacing = craters.spacing; | |
| 303 | 128 | options.generation.maxCraters = craters.maxCraters; | |
| 304 | 165 | for (Crater crater : craters.manual) { | |
| 305 | 37 | crater.manual = true; | |
| 306 | 37 | options.generation.manual.Append(crater); | |
| 307 | } | ||
| 308 | 128 | options.apply = craters.apply; | |
| 309 | 128 | options.surface = surface; | |
| 310 | 128 | if (craters.densityMap) { | |
| 311 | options.densityMap = | ||
| 312 | 2 | config::BuildDensityMap(*craters.densityMap, Child(path, "density_map"), directory); | |
| 313 | } | ||
| 314 | 128 | if (craters.completeness) { | |
| 315 | options.generation.completeness = | ||
| 316 | 2 | config::BuildField(*craters.completeness, Child(path, "completeness"), directory); | |
| 317 | } | ||
| 318 | 128 | CraterModels models = BuildCraterModels(craters, path); | |
| 319 | 256 | return At(path, | |
| 320 | 384 | [&] { return std::make_shared<CraterLayer>(std::move(models), std::move(options)); }); | |
| 321 | 128 | } | |
| 322 | |||
| 323 | 8 | LayerPtr BuildSecondaryLayer(const SecondaryLayerSpec& spec, | |
| 324 | const std::shared_ptr<const CraterLayer>& primaries) { | ||
| 325 | const CraterShapeModels models{ | ||
| 326 | 8 | primaries->Models().profile, | |
| 327 | 8 | std::make_shared<NoDegradation>(MorphometryOf(spec.morphometry), spec.depthRatio)}; | |
| 328 | 16 | return std::make_shared<SecondaryCraterLayer>(primaries, models, spec.secondaries); | |
| 329 | 8 | } | |
| 330 | |||
| 331 | 374 | LayerPtr BuildLayer(const LayerSpec& spec, const std::string& path, const LayerInputs& context) { | |
| 332 | 374 | const Surface& surface = context.surface; | |
| 333 | return std::visit( | ||
| 334 | 374 | Overloaded{ | |
| 335 | 152 | [&](const NoiseLayerSpec& s) -> LayerPtr { | |
| 336 | 152 | return std::make_shared<NoiseRelief>(s.Params(), context.referencePoint, surface); | |
| 337 | }, | ||
| 338 | 128 | [&](const CraterLayerSpec& s) -> LayerPtr { | |
| 339 | 128 | return BuildCraterLayer(s, path, surface, context.directory, context.thrown); | |
| 340 | }, | ||
| 341 | 48 | [&](const BoulderLayerSpec& s) -> LayerPtr { | |
| 342 | 48 | BoulderLayerOptions options = BuildBoulderOptions(s, path, context.directory); | |
| 343 | 48 | const bool throws = options.ejecta.MinCraterDiameter() > 0.0; | |
| 344 | 96 | return std::make_shared<BoulderLayer>(std::move(options), | |
| 345 | 192 | throws ? context.craters : nullptr, surface); | |
| 346 | 48 | }, | |
| 347 | 7 | [&](const RoughnessLayerSpec& s) -> LayerPtr { | |
| 348 | 7 | return std::make_shared<RoughnessLayer>(s.roughness, surface); | |
| 349 | }, | ||
| 350 | 2 | [&](const DuneLayerSpec& s) -> LayerPtr { | |
| 351 | 2 | return std::make_shared<DuneLayer>(s.dunes, surface); | |
| 352 | }, | ||
| 353 | 1 | [&](const PavementLayerSpec& s) -> LayerPtr { | |
| 354 | 1 | return std::make_shared<PavementLayer>(s.pavement, surface); | |
| 355 | }, | ||
| 356 | 1 | [&](const StrataLayerSpec& s) -> LayerPtr { | |
| 357 | 1 | return std::make_shared<StrataLayer>(s.strata, surface); | |
| 358 | }, | ||
| 359 | 1 | [&](const SandShadowLayerSpec& s) -> LayerPtr { | |
| 360 | 1 | return std::make_shared<SandShadowLayer>(context.boulders, s.shadows, surface); | |
| 361 | }, | ||
| 362 | 4 | [&](const SandFillLayerSpec& s) -> LayerPtr { | |
| 363 | 4 | return std::make_shared<SandFillLayer>(s.sandFill, surface); | |
| 364 | }, | ||
| 365 | 6 | [&](const BedformLayerSpec& s) -> LayerPtr { | |
| 366 | 6 | return std::make_shared<BedformLayer>(s.bedforms, surface); | |
| 367 | }, | ||
| 368 | 5 | [&](const ScaleLayerSpec& s) -> LayerPtr { | |
| 369 | 5 | return std::make_shared<ScaleLayer>(s.scale, s.offset); | |
| 370 | }, | ||
| 371 | 4 | [&](const ClampLayerSpec& s) -> LayerPtr { | |
| 372 | 4 | return std::make_shared<ClampLayer>(s.min, s.max); | |
| 373 | }, | ||
| 374 | 6 | [&](const RegionLayerSpec& s) -> LayerPtr { | |
| 375 | 12 | return std::make_shared<RegionLayer>( | |
| 376 | 12 | BuildLayer(*s.layer, Child(path, "layer"), context), BuildRegionShape(s.shape), | |
| 377 | 18 | s.feather, s.blend, surface); | |
| 378 | }, | ||
| 379 | 8 | [&](const SecondaryLayerSpec& s) -> LayerPtr { | |
| 380 | 8 | return BuildSecondaryLayer(s, context.craters); | |
| 381 | }, | ||
| 382 | 1 | [&](const DemDetailLayerSpec& s) -> LayerPtr { | |
| 383 | 1 | const auto file = Resolve(s.path, context.directory); | |
| 384 | 1 | GeoTiffSourceOptions full = s.read; | |
| 385 | 1 | full.lowPass = 0.0; | |
| 386 | 2 | return std::make_shared<DetailLayer>(std::make_shared<GeoTiffSource>(file, full), | |
| 387 | 1 | std::make_shared<GeoTiffSource>(file, s.read), | |
| 388 | 3 | s.weight, surface); | |
| 389 | 1 | }, | |
| 390 | }, | ||
| 391 | 748 | spec); | |
| 392 | } | ||
| 393 | |||
| 394 | /// The bands of a raster albedo map, scaled as its metadata or the spec says. | ||
| 395 | 2 | std::vector<Raster<float>> LoadAlbedoBands(const RasterAlbedoSpec& spec, | |
| 396 | const std::filesystem::path& directory) { | ||
| 397 | 2 | const std::filesystem::path path = Resolve(spec.path, directory); | |
| 398 | 2 | const GeoTiffInfo info = ReadGeoTiffInfo(path); | |
| 399 | 2 | std::vector<Raster<float>> bands; | |
| 400 | 4 | for (const std::size_t band : spec.bands) { | |
| 401 | 2 | Raster<float> raster = ReadGeoTiff(path, band).raster; | |
| 402 | 2 | const BandScaling scaling{spec.scale.value_or(info.bandScaling[band].scale), | |
| 403 | 2 | spec.offset.value_or(info.bandScaling[band].offset)}; | |
| 404 | 2 | if (!scaling.Identity()) { | |
| 405 | 81 | for (float& value : raster.Values()) { | |
| 406 | 80 | if (!raster.IsNoData(value)) { | |
| 407 | 80 | value = static_cast<float>(static_cast<double>(value) * scaling.scale + | |
| 408 | 80 | scaling.offset); | |
| 409 | } | ||
| 410 | } | ||
| 411 | } | ||
| 412 | 2 | bands.push_back(std::move(raster)); | |
| 413 | 2 | } | |
| 414 | 4 | return bands; | |
| 415 | 2 | } | |
| 416 | |||
| 417 | 22 | std::shared_ptr<const AlbedoSource> BuildAlbedoSource(const AlbedoSourceSpec& spec, | |
| 418 | const Terrain& terrain, | ||
| 419 | const std::filesystem::path& directory) { | ||
| 420 | using Result = std::shared_ptr<const AlbedoSource>; | ||
| 421 | 22 | return std::visit(Overloaded{ | |
| 422 | 20 | [&](const SyntheticAlbedoSpec& s) -> Result { | |
| 423 | 40 | return std::make_shared<SyntheticAlbedo>( | |
| 424 | 60 | s.Params(), ReferencePoint(terrain), terrain.surface); | |
| 425 | }, | ||
| 426 | 2 | [&](const RasterAlbedoSpec& s) -> Result { | |
| 427 | 4 | return std::make_shared<RasterAlbedo>(LoadAlbedoBands(s, directory), | |
| 428 | 6 | s.options, terrain.surface); | |
| 429 | }, | ||
| 430 | }, | ||
| 431 | 44 | spec); | |
| 432 | } | ||
| 433 | |||
| 434 | } // namespace | ||
| 435 | |||
| 436 | namespace config { | ||
| 437 | |||
| 438 | 274 | std::filesystem::path Resolve(const std::string& path, const std::filesystem::path& directory) { | |
| 439 | 274 | const std::filesystem::path resolved = path; | |
| 440 | 548 | return resolved.is_relative() && !directory.empty() ? directory / resolved : resolved; | |
| 441 | 274 | } | |
| 442 | |||
| 443 | 125 | BoulderLayerOptions BuildBoulderOptions(const BoulderLayerSpec& spec, const std::string& path, | |
| 444 | const std::filesystem::path& directory) { | ||
| 445 | 125 | BoulderLayerOptions options = spec.boulders; | |
| 446 | 249 | const auto sizes = [&](const BoulderSizesSpec& named, const std::string& key) { | |
| 447 | 498 | auto built = At(Child(path, key), [&] { return BuildBoulderSizes(named); }); | |
| 448 | 246 | if (built && spec.extrapolate) { | |
| 449 | 22 | built->SetAllowExtrapolation(true); | |
| 450 | } | ||
| 451 | 246 | return built; | |
| 452 | 125 | }; | |
| 453 | 250 | options.sizes.distribution = sizes(spec.sizes, "sizes"); | |
| 454 | 126 | options.ejecta.distribution = sizes(spec.ejectaSizes, "ejecta_sizes"); | |
| 455 | 124 | for (std::size_t i = 0; i < spec.shapeFiles.size(); ++i) { | |
| 456 | 3 | const std::string file = Element(Child(path, "shape_files"), i); | |
| 457 | try { | ||
| 458 | 3 | options.rendering.shapes.library.push_back(std::make_shared<const SceneMesh>( | |
| 459 | 6 | ReadMesh(Resolve(spec.shapeFiles[i], directory)))); | |
| 460 | 1 | } catch (const std::exception& error) { | |
| 461 | 3 | throw ConfigError(file, error.what()); | |
| 462 | 1 | } | |
| 463 | 3 | } | |
| 464 | 121 | if (spec.densityMap) { | |
| 465 | options.densityMap = | ||
| 466 | 6 | BuildDensityMap(*spec.densityMap, Child(path, "density_map"), directory); | |
| 467 | } | ||
| 468 | 121 | if (spec.slopeMap) { | |
| 469 | 6 | const SizeSlopeMapSpec& s = *spec.slopeMap; | |
| 470 | 6 | const std::string at = Child(path, "slope_map"); | |
| 471 | options.slopeMap = | ||
| 472 | 12 | SizeSlopeMap{BuildField(s.field, Child(at, "field"), directory), s.measure, | |
| 473 | 6 | s.referenceDiameter, s.minExponent, s.maxExponent}; | |
| 474 | 12 | At(at, [&] { ValidateSizeSlopeMap(*options.slopeMap); }); | |
| 475 | 6 | } | |
| 476 | 127 | for (Crater boulder : spec.manual) { | |
| 477 | 7 | boulder.manual = true; | |
| 478 | 7 | options.manual.push_back(boulder); | |
| 479 | } | ||
| 480 | 120 | if (spec.completeness) { | |
| 481 | options.completeness = | ||
| 482 | 5 | BuildField(*spec.completeness, Child(path, "completeness"), directory); | |
| 483 | } | ||
| 484 | 240 | return options; | |
| 485 | 5 | } | |
| 486 | |||
| 487 | 27 | std::shared_ptr<const SurfaceField> BuildField(const FieldSpec& spec, const std::string& path, | |
| 488 | const std::filesystem::path& directory) { | ||
| 489 | 27 | if (spec.path.empty()) { | |
| 490 | 24 | return std::make_shared<ConstantField>(spec.value); | |
| 491 | } | ||
| 492 | 3 | RasterFieldOptions read; | |
| 493 | 3 | read.method = spec.method; | |
| 494 | 3 | read.scale = spec.scale; | |
| 495 | 3 | read.fallback = spec.fallback.value_or(std::numeric_limits<double>::quiet_NaN()); | |
| 496 | 3 | read.periodicLongitude = spec.periodicLongitude; | |
| 497 | try { | ||
| 498 | 5 | return ReadRasterField(Resolve(spec.path, directory), read, spec.band); | |
| 499 | 2 | } catch (const std::exception& error) { | |
| 500 | 6 | throw ConfigError(path, error.what()); | |
| 501 | 2 | } | |
| 502 | } | ||
| 503 | |||
| 504 | 10 | DensityMap BuildDensityMap(const DensityMapSpec& spec, const std::string& path, | |
| 505 | const std::filesystem::path& directory) { | ||
| 506 | 10 | DensityMap map; | |
| 507 | 10 | map.field = BuildField(spec.field, Child(path, "field"), directory); | |
| 508 | 10 | map.measure = spec.measure; | |
| 509 | 10 | map.minDiameter = spec.minDiameter; | |
| 510 | 10 | map.maxDiameter = spec.maxDiameter.value_or(std::numeric_limits<double>::infinity()); | |
| 511 | 10 | map.areaUnit = spec.areaUnit; | |
| 512 | 10 | map.total = spec.total; | |
| 513 | 20 | At(path, [&] { ValidateDensityMap(map); }); | |
| 514 | 9 | return map; | |
| 515 | 1 | } | |
| 516 | |||
| 517 | 1343 | RasterGrid RegionGrid(const GridRegionSpec& region, const Surface& surface) { | |
| 518 | 1343 | const double cell = region.pixelSize / surface.UnitLength(); | |
| 519 | 1343 | const double halfX = 0.5 * static_cast<double>(region.width) * cell; | |
| 520 | 1343 | const double halfY = 0.5 * static_cast<double>(region.height) * cell; | |
| 521 | 1343 | return {region.width, region.height, | |
| 522 | 1343 | GeoTransform::NorthUp(region.centreX - halfX, region.centreY + halfY, cell)}; | |
| 523 | } | ||
| 524 | |||
| 525 | 10 | RasterGrid WholeBodyGrid(const Surface& surface, double spacing) { | |
| 526 | const auto rows = | ||
| 527 | 10 | static_cast<std::size_t>(std::max(1.0, std::ceil(180.0 * surface.UnitLength() / spacing))); | |
| 528 | 10 | return {2 * rows, rows, GeoTransform::NorthUp(-180.0, 90.0, 180.0 / static_cast<double>(rows))}; | |
| 529 | } | ||
| 530 | |||
| 531 | 993 | std::optional<RasterGrid> OutputGrid(const OutputSpec& output, | |
| 532 | const std::optional<RasterGrid>& base, | ||
| 533 | const Surface& surface) { | ||
| 534 | 978 | const auto covered = [&](const std::optional<GridRegionSpec>& region) { | |
| 535 | 978 | return region ? std::optional<RasterGrid>(RegionGrid(*region, surface)) : base; | |
| 536 | 993 | }; | |
| 537 | 154 | const auto listed = [&](const auto& list) -> std::optional<RasterGrid> { | |
| 538 | 154 | if (list.region || base || !surface.IsBody() || !list.spacing) { | |
| 539 | 147 | return covered(list.region); | |
| 540 | } | ||
| 541 | 7 | return WholeBodyGrid(surface, *list.spacing); | |
| 542 | }; | ||
| 543 | 993 | return std::visit( | |
| 544 | 1986 | Overloaded{ | |
| 545 | 811 | [&](const GeoTiffOutput& o) { return covered(o.region); }, | |
| 546 | 20 | [&](const TerrainMeshOutput& o) { return covered(o.region); }, | |
| 547 | 129 | [&](const ListOutput& o) { return listed(o); }, | |
| 548 | 25 | [&](const BoulderModelsOutput& o) { return listed(o); }, | |
| 549 | 8 | [](const MeshOutput&) -> std::optional<RasterGrid> { return std::nullopt; }, | |
| 550 | }, | ||
| 551 | 1986 | output); | |
| 552 | } | ||
| 553 | |||
| 554 | 20 | MaterialPhotometry BuildPhotometry(const PhotometrySpec& spec) { | |
| 555 | 20 | MaterialPhotometry photometry = spec.reference == PhotometryReference::MarsPancam | |
| 556 | 20 | ? MarsPancamPhotometry() | |
| 557 | 7 | : MaterialPhotometry{}; | |
| 558 | 120 | for (std::size_t k = 0; k < kMaterialClasses; ++k) { | |
| 559 | 100 | if (spec.classes[k]) { | |
| 560 | 9 | photometry[k] = spec.classes[k]; | |
| 561 | } | ||
| 562 | } | ||
| 563 | 20 | return photometry; | |
| 564 | } | ||
| 565 | |||
| 566 | 23 | RegionShape BuildRegionShape(const RegionShapeSpec& spec) { | |
| 567 | 23 | return std::visit( | |
| 568 | 23 | Overloaded{ | |
| 569 | 13 | [](const RectangleRegionSpec& s) { | |
| 570 | 13 | return RegionShape::Rectangle({s.minX, s.minY, s.maxX, s.maxY}); | |
| 571 | }, | ||
| 572 | 10 | [](const CircleRegionSpec& s) { return RegionShape::Circle({s.x, s.y}, s.radius); }, | |
| 573 | }, | ||
| 574 | 46 | spec); | |
| 575 | } | ||
| 576 | |||
| 577 | } // namespace config | ||
| 578 | |||
| 579 | 425 | CraterModels BuildCraterModels(const CraterLayerSpec& craters, const std::string& path) { | |
| 580 | 425 | CraterModels models; | |
| 581 | 856 | models.density = At(Child(path, "density"), [&] { return BuildDensity(craters.density); }); | |
| 582 | 842 | models.ages = At(Child(path, "ages"), [&] { return BuildAges(craters.ages); }); | |
| 583 | 830 | models.profile = At(Child(path, "profile"), | |
| 584 | 830 | [&] { return BuildProfile(craters.profile, craters.morphometry); }); | |
| 585 | 835 | At(Child(path, "degradation"), [&] { AddDegradation(craters, models); }); | |
| 586 | 410 | return models; | |
| 587 | 15 | } | |
| 588 | |||
| 589 | namespace { | ||
| 590 | |||
| 591 | /// The radial shape a shape model spec describes, loaded once per file, modification time, Q and | ||
| 592 | /// unit, as validating and building a terrain both need it. | ||
| 593 | /// @throws ConfigError at "body.path", "body.q" or "body.metres_per_unit". | ||
| 594 | 12 | std::shared_ptr<const RadialShape> LoadShapeModel(const ShapeModelSurfaceSpec& spec, | |
| 595 | const std::filesystem::path& directory) { | ||
| 596 | 13 | config::RequireNonEmpty(spec.path, "body.path"); | |
| 597 | 11 | if (spec.q && (*spec.q == 0 || *spec.q > 4096)) { | |
| 598 | 10 | throw ConfigError("body.q", "must be 1 to 4096"); | |
| 599 | } | ||
| 600 | 9 | if (spec.metresPerUnit && !(std::isfinite(*spec.metresPerUnit) && *spec.metresPerUnit > 0.0)) { | |
| 601 | 5 | throw ConfigError("body.metres_per_unit", "must be positive"); | |
| 602 | } | ||
| 603 | 8 | const std::filesystem::path file = Resolve(spec.path, directory); | |
| 604 | 8 | std::error_code error; | |
| 605 | 8 | const auto modified = std::filesystem::last_write_time(file, error); | |
| 606 | 8 | if (error) { | |
| 607 | 3 | throw ConfigError("body.path", "cannot read " + file.string()); | |
| 608 | } | ||
| 609 | using Key = std::tuple<std::string, std::filesystem::file_time_type, std::size_t, double>; | ||
| 610 | static std::mutex mutex; | ||
| 611 | 7 | static std::map<Key, std::shared_ptr<const RadialShape>> loaded; | |
| 612 | 14 | const Key key{std::filesystem::absolute(file).string(), modified, spec.q.value_or(0), | |
| 613 | 7 | spec.metresPerUnit.value_or(0.0)}; | |
| 614 | 7 | const std::lock_guard<std::mutex> lock(mutex); | |
| 615 | 7 | if (const auto found = loaded.find(key); found != loaded.end()) { | |
| 616 | 1 | return found->second; | |
| 617 | } | ||
| 618 | 8 | const auto shape = At("body.path", [&] { | |
| 619 | try { | ||
| 620 | 6 | if (IsIcqPath(file)) { | |
| 621 | 4 | const IcqModel model = ReadIcq(file, spec.metresPerUnit.value_or(1000.0)); | |
| 622 | 6 | return spec.q && *spec.q != model.q ? ShapeModelFromMesh(IcqToMesh(model), *spec.q) | |
| 623 | 6 | : ToRadialShape(model); | |
| 624 | 3 | } | |
| 625 | 2 | const double scale = spec.metresPerUnit.value_or(1.0); | |
| 626 | 2 | const TriangleMesh mesh = Scaled(ReadMesh(file).mesh, {scale, scale, scale}); | |
| 627 | 2 | return ShapeModelFromMesh(mesh, spec.q.value_or(256)); | |
| 628 | 4 | } catch (const std::runtime_error& failure) { | |
| 629 | 2 | throw std::invalid_argument(failure.what()); | |
| 630 | 2 | } | |
| 631 | 14 | }); | |
| 632 | 4 | return loaded.emplace(key, std::make_shared<const RadialShape>(shape)).first->second; | |
| 633 | 12 | } | |
| 634 | |||
| 635 | } // namespace | ||
| 636 | |||
| 637 | 585 | Surface BuildSurface(const SurfaceSpec& spec, const std::filesystem::path& directory) { | |
| 638 | return std::visit( | ||
| 639 | 563 | Overloaded{ | |
| 640 | 483 | [](const PlaneSurfaceSpec&) -> Surface { return Surface::Plane(); }, | |
| 641 | 75 | [](const SphereSurfaceSpec& s) -> Surface { | |
| 642 | 75 | if (!(std::isfinite(s.radius) && s.radius > 0.0)) { | |
| 643 | 45 | throw ConfigError("body.radius", "must be positive: the body's radius"); | |
| 644 | } | ||
| 645 | 66 | return Surface::Sphere(s.radius); | |
| 646 | }, | ||
| 647 | 15 | [](const EllipsoidSurfaceSpec& s) -> Surface { | |
| 648 | 15 | if (s.radii.size() != 3) { | |
| 649 | 15 | throw ConfigError("body.radii", "must hold exactly three semi-axes a, b, c"); | |
| 650 | } | ||
| 651 | 43 | for (std::size_t i = 0; i < 3; ++i) { | |
| 652 | 34 | if (!(std::isfinite(s.radii[i]) && s.radii[i] > 0.0)) { | |
| 653 | 6 | throw ConfigError(Element("body.radii", i), | |
| 654 | 15 | "must be positive: a semi-axis in metres"); | |
| 655 | } | ||
| 656 | } | ||
| 657 | 9 | return Surface::Ellipsoid(s.radii[0], s.radii[1], s.radii[2]); | |
| 658 | }, | ||
| 659 | 12 | [&](const ShapeModelSurfaceSpec& s) -> Surface { | |
| 660 | 12 | return Surface::ShapeModel(LoadShapeModel(s, directory)); | |
| 661 | }, | ||
| 662 | }, | ||
| 663 | 1148 | spec); | |
| 664 | } | ||
| 665 | |||
| 666 | namespace { | ||
| 667 | |||
| 668 | 172 | Terrain BuildValidTerrain(const TerrainConfig& config, const std::filesystem::path& directory) { | |
| 669 | 172 | Terrain terrain; | |
| 670 | 172 | terrain.surface = BuildSurface(config.body, directory); | |
| 671 | 172 | if (config.base) { | |
| 672 | 11 | terrain.source = BuildSource(*config.base, directory, terrain.surface); | |
| 673 | } | ||
| 674 | 172 | std::shared_ptr<const CraterLayer> craters; | |
| 675 | 172 | std::shared_ptr<const BoulderLayer> boulders; | |
| 676 | 540 | for (std::size_t i = 0; i < config.layers.size(); ++i) { | |
| 677 | 368 | LayerInputs context{ReferencePoint(terrain), directory, craters, boulders, terrain.surface}; | |
| 678 | 425 | for (std::size_t j = i + 1; | |
| 679 | 608 | std::holds_alternative<CraterLayerSpec>(config.layers[i]) && | |
| 680 | 608 | j < config.layers.size() && !std::holds_alternative<CraterLayerSpec>(config.layers[j]); | |
| 681 | ++j) { | ||
| 682 | 57 | if (const auto* secondaries = std::get_if<SecondaryLayerSpec>(&config.layers[j])) { | |
| 683 | 8 | context.thrown.push_back(secondaries->secondaries); | |
| 684 | } | ||
| 685 | } | ||
| 686 | 736 | terrain.layers.push_back(BuildLayer(config.layers[i], Element("layers", i), context)); | |
| 687 | 368 | if (std::holds_alternative<CraterLayerSpec>(config.layers[i])) { | |
| 688 | 126 | craters = std::static_pointer_cast<const CraterLayer>(terrain.layers.back()); | |
| 689 | } | ||
| 690 | 368 | if (std::holds_alternative<BoulderLayerSpec>(config.layers[i])) { | |
| 691 | 48 | boulders = std::static_pointer_cast<const BoulderLayer>(terrain.layers.back()); | |
| 692 | } | ||
| 693 | 368 | } | |
| 694 | 344 | return terrain; | |
| 695 | 172 | } | |
| 696 | |||
| 697 | /// The top-level boulder layers of a terrain built from `config`, in order, each with the | ||
| 698 | /// ground beneath it. | ||
| 699 | 2 | std::vector<RockSource> RockSourcesOf(const TerrainConfig& config, const Terrain& terrain) { | |
| 700 | 2 | std::vector<RockSource> sources; | |
| 701 | 10 | for (std::size_t i = 0; i < config.layers.size(); ++i) { | |
| 702 | 8 | if (std::holds_alternative<BoulderLayerSpec>(config.layers[i])) { | |
| 703 | 6 | sources.push_back({std::static_pointer_cast<const BoulderLayer>(terrain.layers[i]), | |
| 704 | SharedGround(terrain, i)}); | ||
| 705 | } | ||
| 706 | } | ||
| 707 | 2 | return sources; | |
| 708 | 3 | } | |
| 709 | |||
| 710 | /// The top-level crater layers of a terrain built from `config`, in order. | ||
| 711 | 22 | std::vector<std::shared_ptr<const CraterLayer>> CraterLayersOf(const TerrainConfig& config, | |
| 712 | const Terrain& terrain) { | ||
| 713 | 22 | std::vector<std::shared_ptr<const CraterLayer>> craterLayers; | |
| 714 | 69 | for (std::size_t i = 0; i < config.layers.size(); ++i) { | |
| 715 | 47 | if (std::holds_alternative<CraterLayerSpec>(config.layers[i])) { | |
| 716 | 21 | craterLayers.push_back(std::static_pointer_cast<const CraterLayer>(terrain.layers[i])); | |
| 717 | } | ||
| 718 | } | ||
| 719 | 22 | return craterLayers; | |
| 720 | } | ||
| 721 | |||
| 722 | 22 | Albedo BuildValidAlbedo(const TerrainConfig& config, const Terrain& terrain, | |
| 723 | const std::filesystem::path& directory) { | ||
| 724 | 22 | const AlbedoSpec& spec = *config.albedo; | |
| 725 | 22 | Albedo albedo; | |
| 726 | 22 | albedo.surface = terrain.surface; | |
| 727 | 22 | albedo.source = BuildAlbedoSource(spec.source, terrain, directory); | |
| 728 | const std::vector<std::shared_ptr<const CraterLayer>> craterLayers = | ||
| 729 | 22 | CraterLayersOf(config, terrain); | |
| 730 | 40 | for (const AlbedoLayerSpec& layer : spec.layers) { | |
| 731 | 18 | std::visit(Overloaded{ | |
| 732 | 11 | [&](const CraterAlbedoSpec& craters) { | |
| 733 | 25 | for (std::size_t i = 0; i < craterLayers.size(); ++i) { | |
| 734 | 14 | if (!craters.craterLayer || *craters.craterLayer == i) { | |
| 735 | 12 | albedo.layers.push_back(std::make_shared<CraterAlbedo>( | |
| 736 | 12 | craterLayers[i], craters.options)); | |
| 737 | } | ||
| 738 | } | ||
| 739 | 11 | }, | |
| 740 | 2 | [&](const SplotchAlbedoSpec& splotches) { | |
| 741 | 2 | albedo.layers.push_back( | |
| 742 | 4 | std::make_shared<SplotchAlbedo>(splotches.options, terrain.surface)); | |
| 743 | 2 | }, | |
| 744 | 1 | [&](const BlastZoneAlbedoSpec& zones) { | |
| 745 | 1 | albedo.layers.push_back( | |
| 746 | 2 | std::make_shared<BlastZoneAlbedo>(zones.zones, terrain.surface)); | |
| 747 | 1 | }, | |
| 748 | 2 | [&](const RockAlbedoSpec& rocks) { | |
| 749 | 2 | std::vector<RockSource> sources = RockSourcesOf(config, terrain); | |
| 750 | 2 | if (rocks.boulderLayer) { | |
| 751 | ✗ | sources = {sources[*rocks.boulderLayer]}; | |
| 752 | } | ||
| 753 | 2 | albedo.layers.push_back( | |
| 754 | 4 | std::make_shared<RockAlbedo>(rocks.options, std::move(sources))); | |
| 755 | 2 | }, | |
| 756 | 2 | [&](const SlopeAlbedoSpec& slopes) { | |
| 757 | 2 | albedo.layers.push_back( | |
| 758 | 4 | std::make_shared<SlopeAlbedo>(slopes.options, terrain.surface)); | |
| 759 | 2 | }, | |
| 760 | }, | ||
| 761 | layer); | ||
| 762 | } | ||
| 763 | 44 | return albedo; | |
| 764 | 22 | } | |
| 765 | } // namespace | ||
| 766 | |||
| 767 | 174 | Terrain BuildTerrain(const TerrainConfig& config, const std::filesystem::path& directory) { | |
| 768 | 174 | ValidateConfig(config, directory); | |
| 769 | 172 | return BuildValidTerrain(config, directory); | |
| 770 | } | ||
| 771 | |||
| 772 | 24 | Albedo BuildAlbedo(const TerrainConfig& config, const Terrain& terrain, | |
| 773 | const std::filesystem::path& directory) { | ||
| 774 | 24 | if (!config.albedo) { | |
| 775 | 5 | throw ConfigError("albedo", "the configuration has no albedo"); | |
| 776 | } | ||
| 777 | 23 | ValidateAlbedo(config, directory); | |
| 778 | 22 | return BuildValidAlbedo(config, terrain, directory); | |
| 779 | } | ||
| 780 | |||
| 781 | } // namespace terraingen | ||
| 782 |