config/generate.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/config/generate.h" | ||
| 2 | |||
| 3 | #include "terraingen/config/build.h" | ||
| 4 | #include "terraingen/config/json.h" | ||
| 5 | #include "terraingen/mesh/io/icq.h" | ||
| 6 | #include "terraingen/mesh/io/mesh_io.h" | ||
| 7 | #include "terraingen/pipeline/body_mesh.h" | ||
| 8 | #include "terraingen/pipeline/height_mesh.h" | ||
| 9 | #include "terraingen/sources/geotiff_source.h" | ||
| 10 | #include "terraingen/sources/source_chain.h" | ||
| 11 | #include "terraingen/util/log.h" | ||
| 12 | |||
| 13 | #include <algorithm> | ||
| 14 | #include <cmath> | ||
| 15 | #include <iomanip> | ||
| 16 | #include <numbers> | ||
| 17 | #include <optional> | ||
| 18 | #include <sstream> | ||
| 19 | #include <stdexcept> | ||
| 20 | #include <string> | ||
| 21 | #include <type_traits> | ||
| 22 | #include <variant> | ||
| 23 | #include <vector> | ||
| 24 | |||
| 25 | namespace terraingen { | ||
| 26 | |||
| 27 | namespace { | ||
| 28 | |||
| 29 | /// The projection of a terrain's raster base, or geographic keys on a body without one. | ||
| 30 | 153 | GeoKeys KeysOf(const Terrain& terrain) { | |
| 31 | 153 | GeoKeys keys = terrain.source ? terrain.source->Keys() : GeoKeys{}; | |
| 32 | 153 | if (terrain.surface.IsBody() && keys.Empty()) { | |
| 33 | 13 | keys = GeographicSphereKeys(terrain.surface.Radius()); | |
| 34 | } | ||
| 35 | 153 | return keys; | |
| 36 | } | ||
| 37 | |||
| 38 | /// The grid of the terrain's base, or none without one. | ||
| 39 | 257 | std::optional<RasterGrid> BaseGrid(const Terrain& terrain) { | |
| 40 | 257 | return terrain.source ? std::optional<RasterGrid>(terrain.source->Grid()) : std::nullopt; | |
| 41 | } | ||
| 42 | |||
| 43 | /// The sample spacing in metres an output lists features at; 0 for its grid's. | ||
| 44 | 187 | double SpacingOf(const OutputSpec& output) { | |
| 45 | 187 | return std::visit( | |
| 46 | 187 | [](const auto& o) { | |
| 47 | if constexpr (config::FeatureOutput<std::decay_t<decltype(o)>>) { | ||
| 48 | 64 | return o.spacing.value_or(0.0); | |
| 49 | } | ||
| 50 | 123 | return 0.0; | |
| 51 | }, | ||
| 52 | 374 | output); | |
| 53 | } | ||
| 54 | |||
| 55 | /// The boulders placed over one grid at one spacing, shared by the outputs that list them. | ||
| 56 | struct PlacedBoulderSet { | ||
| 57 | RasterGrid grid; | ||
| 58 | double spacing = 0.0; | ||
| 59 | BoulderFeatures features; | ||
| 60 | }; | ||
| 61 | |||
| 62 | /// One step of generating a terrain and the time it took over every grid. | ||
| 63 | struct StepTime { | ||
| 64 | std::string name; | ||
| 65 | double milliseconds = 0.0; | ||
| 66 | }; | ||
| 67 | |||
| 68 | /// A sample spacing in metres to three significant figures, or in kilometres from 1 km. | ||
| 69 | 145 | std::string FormatSpacing(double metres) { | |
| 70 | 145 | std::ostringstream text; | |
| 71 | 145 | text << std::setprecision(3); | |
| 72 | 145 | if (metres >= 1000.0) { | |
| 73 | ✗ | text << metres / 1000.0 << " km"; | |
| 74 | } else { | ||
| 75 | 145 | text << metres << " m"; | |
| 76 | } | ||
| 77 | 290 | return text.str(); | |
| 78 | 145 | } | |
| 79 | |||
| 80 | /// Logs the time of each step that ran and its share of `total` milliseconds. | ||
| 81 | 142 | void LogStepTimes(const std::vector<StepTime>& steps, std::size_t grids, double total) { | |
| 82 | 142 | std::size_t width = 0; | |
| 83 | 718 | for (const StepTime& step : steps) { | |
| 84 | 576 | width = std::max(width, step.name.size()); | |
| 85 | } | ||
| 86 | 142 | LogInfo("pipeline", "Time per step over ", grids, grids == 1 ? " grid" : " grids", ", of ", | |
| 87 | 284 | FormatDuration(total), ":"); | |
| 88 | 718 | for (const StepTime& step : steps) { | |
| 89 | 576 | if (step.milliseconds <= 0.0) { | |
| 90 | 261 | continue; | |
| 91 | } | ||
| 92 | 315 | std::ostringstream line; | |
| 93 | 315 | line << " " << std::left << std::setw(static_cast<int>(width)) << step.name << std::right | |
| 94 | 630 | << std::setw(10) << FormatDuration(step.milliseconds) << std::setw(5) | |
| 95 | 315 | << std::llround(100.0 * step.milliseconds / std::fmax(total, 1e-9)) << "%"; | |
| 96 | 315 | LogInfo("pipeline", line.str()); | |
| 97 | 315 | } | |
| 98 | 142 | } | |
| 99 | |||
| 100 | /// What `WriteOutputs` writes one output from. | ||
| 101 | struct OutputContext { | ||
| 102 | const GeneratedTerrain& generated; | ||
| 103 | std::optional<RasterGrid> grid; ///< The grid the output covers; none for a mesh. | ||
| 104 | double spacing = 0.0; ///< The output's sample spacing; 0 for its grid's. | ||
| 105 | const TerrainConfig* config = nullptr; ///< The configuration the outputs come from. | ||
| 106 | std::vector<PlacedBoulderSet>* placed = nullptr; ///< Boulders placed for earlier outputs. | ||
| 107 | |||
| 108 | /// The boulders placed over the output's grid, placed once for every output that lists them. | ||
| 109 | 34 | const BoulderFeatures& Boulders() const { | |
| 110 | 34 | for (const PlacedBoulderSet& set : *placed) { | |
| 111 | 10 | if (set.grid == *grid && set.spacing == spacing) { | |
| 112 | 10 | return set.features; | |
| 113 | } | ||
| 114 | } | ||
| 115 | 48 | placed->push_back({*grid, spacing, PlacedBoulders(generated.terrain, *grid, spacing)}); | |
| 116 | 24 | return placed->back().features; | |
| 117 | 24 | } | |
| 118 | |||
| 119 | /// The heights and albedo generated over the output's grid. | ||
| 120 | /// @throws std::runtime_error if they were not generated. | ||
| 121 | 115 | const GeneratedGrid& Generated(const std::filesystem::path& path) const { | |
| 122 | 115 | const GeneratedGrid* found = grid ? generated.Find(*grid) : nullptr; | |
| 123 | 115 | if (!found) { | |
| 124 | 1 | throw std::runtime_error("No heights generated for " + path.string()); | |
| 125 | } | ||
| 126 | 114 | return *found; | |
| 127 | } | ||
| 128 | |||
| 129 | /// The centre of the output's grid, or the origin for a whole-body list. | ||
| 130 | 14 | Vec2d Centre() const { | |
| 131 | 14 | const Bounds2d bounds = grid->Bounds(); | |
| 132 | 14 | return {0.5 * (bounds.minX + bounds.maxX), 0.5 * (bounds.minY + bounds.maxY)}; | |
| 133 | } | ||
| 134 | }; | ||
| 135 | |||
| 136 | /// Each `Write` returns whether it wrote the file. | ||
| 137 | 87 | bool Write(const HeightmapOutput& output, const std::filesystem::path& path, | |
| 138 | const OutputContext& context) { | ||
| 139 | 86 | WriteGeoTiff(path, context.Generated(path).heights, context.generated.geoKeys, | |
| 140 | 87 | {.compression = output.compression, .tileSize = output.tileSize}); | |
| 141 | 86 | return true; | |
| 142 | } | ||
| 143 | |||
| 144 | 13 | bool Write(const AlbedoOutput& output, const std::filesystem::path& path, | |
| 145 | const OutputContext& context) { | ||
| 146 | 13 | const std::vector<Raster<float>>& albedo = context.Generated(path).albedo; | |
| 147 | 13 | if (albedo.empty()) { | |
| 148 | ✗ | throw std::runtime_error("No albedo to write to " + path.string()); | |
| 149 | } | ||
| 150 | 26 | WriteGeoTiff(path, albedo, context.generated.geoKeys, | |
| 151 | 13 | {.compression = output.compression, .tileSize = output.tileSize}); | |
| 152 | 13 | return true; | |
| 153 | } | ||
| 154 | |||
| 155 | /// The material rasters generated over the output's grid. | ||
| 156 | /// @throws std::runtime_error if they were not generated. | ||
| 157 | 12 | const std::vector<Raster<float>>& MaterialsOf(const OutputContext& context, | |
| 158 | const std::filesystem::path& path) { | ||
| 159 | 12 | const std::vector<Raster<float>>& materials = context.Generated(path).materials; | |
| 160 | 12 | if (materials.empty()) { | |
| 161 | 1 | throw std::runtime_error("No materials to write to " + path.string()); | |
| 162 | } | ||
| 163 | 11 | return materials; | |
| 164 | } | ||
| 165 | |||
| 166 | 7 | bool Write(const MaterialsOutput& output, const std::filesystem::path& path, | |
| 167 | const OutputContext& context) { | ||
| 168 | 6 | WriteGeoTiff(path, MaterialsOf(context, path), context.generated.geoKeys, | |
| 169 | 7 | {.compression = output.compression, .tileSize = output.tileSize}); | |
| 170 | 6 | return true; | |
| 171 | } | ||
| 172 | |||
| 173 | 5 | bool Write(const PhotometryOutput& output, const std::filesystem::path& path, | |
| 174 | const OutputContext& context) { | ||
| 175 | 5 | const std::vector<Raster<float>>& materials = MaterialsOf(context, path); | |
| 176 | const MaterialPhotometry photometry = | ||
| 177 | 5 | config::BuildPhotometry(context.config->materials->photometry); | |
| 178 | 5 | WriteGeoTiff(path, EvaluatePhotometry(materials, photometry), context.generated.geoKeys, | |
| 179 | 5 | {.compression = output.compression, .tileSize = output.tileSize}); | |
| 180 | 5 | return true; | |
| 181 | } | ||
| 182 | |||
| 183 | 30 | bool Write(const CraterListOutput& output, const std::filesystem::path& path, | |
| 184 | const OutputContext& context) { | ||
| 185 | 30 | const Stopwatch stopwatch; | |
| 186 | const CraterFeatures placed = | ||
| 187 | 30 | PlacedCraters(context.generated.terrain, *context.grid, context.spacing); | |
| 188 | 30 | WriteCraterList(placed.craters, path, output.format, placed.attributes); | |
| 189 | 30 | LogInfo("pipeline", "Wrote ", placed.craters.Size(), " craters to ", path.string(), " in ", | |
| 190 | 60 | FormatDuration(stopwatch.Milliseconds())); | |
| 191 | 30 | return true; | |
| 192 | 30 | } | |
| 193 | |||
| 194 | 22 | bool Write(const BoulderListOutput& output, const std::filesystem::path& path, | |
| 195 | const OutputContext& context) { | ||
| 196 | 22 | const Stopwatch stopwatch; | |
| 197 | 22 | const std::vector<Boulder>& boulders = context.Boulders().boulders; | |
| 198 | 22 | WriteBoulderList(boulders, path, output.format); | |
| 199 | 22 | LogInfo("pipeline", "Wrote ", boulders.size(), " boulders to ", path.string(), " in ", | |
| 200 | 44 | FormatDuration(stopwatch.Milliseconds())); | |
| 201 | 22 | return true; | |
| 202 | } | ||
| 203 | |||
| 204 | 12 | bool Write(const BoulderModelsOutput& output, const std::filesystem::path& path, | |
| 205 | const OutputContext& context) { | ||
| 206 | 12 | const Stopwatch stopwatch; | |
| 207 | 12 | const std::vector<BoulderModel>& models = context.Boulders().models; | |
| 208 | 12 | if (models.empty()) { | |
| 209 | 1 | LogWarning("pipeline", "No boulder models to write to ", path.string()); | |
| 210 | 1 | return false; | |
| 211 | } | ||
| 212 | 11 | WriteBoulderModels(models, path, output.creaseAngle, context.generated.terrain.surface, | |
| 213 | 11 | context.Centre(), output.instanced); | |
| 214 | 11 | LogInfo("pipeline", "Wrote ", models.size(), " boulder models to ", path.string(), " in ", | |
| 215 | 22 | FormatDuration(stopwatch.Milliseconds())); | |
| 216 | 11 | return true; | |
| 217 | } | ||
| 218 | |||
| 219 | 7 | bool Write(const MeshOutput& output, const std::filesystem::path& path, | |
| 220 | const OutputContext& context) { | ||
| 221 | 7 | const Stopwatch stopwatch; | |
| 222 | 7 | if (IsIcqPath(path)) { | |
| 223 | 2 | WriteIcq(path, BodyIcq(context.generated.terrain, output.cellsPerFace, | |
| 224 | 2 | output.spacing.value_or(0.0))); | |
| 225 | 2 | LogInfo("pipeline", "Wrote an ICQ shape model of Q = ", output.cellsPerFace, " to ", | |
| 226 | 4 | path.string(), " in ", FormatDuration(stopwatch.Milliseconds())); | |
| 227 | 2 | return true; | |
| 228 | } | ||
| 229 | const TriangleMesh mesh = | ||
| 230 | 5 | BodyMesh(context.generated.terrain, output.cellsPerFace, output.spacing.value_or(0.0)); | |
| 231 | 5 | ShadedMesh shaded = WithCreases(mesh, std::numbers::pi); | |
| 232 | 5 | WriteMesh(path, {std::move(shaded.mesh), std::move(shaded.normals)}); | |
| 233 | 5 | LogInfo("pipeline", "Wrote a body mesh of ", mesh.triangles.size(), " triangles to ", | |
| 234 | 10 | path.string(), " in ", FormatDuration(stopwatch.Milliseconds())); | |
| 235 | 5 | return true; | |
| 236 | 20 | } | |
| 237 | |||
| 238 | 3 | bool Write(const TerrainMeshOutput& /*output*/, const std::filesystem::path& path, | |
| 239 | const OutputContext& context) { | ||
| 240 | 3 | const Stopwatch stopwatch; | |
| 241 | 6 | TriangleMesh mesh = HeightMesh(context.Generated(path).heights, | |
| 242 | 3 | context.generated.terrain.surface, context.Centre()); | |
| 243 | 3 | const std::size_t triangles = mesh.triangles.size(); | |
| 244 | 3 | std::vector<Vec3d> normals = VertexNormals(mesh); | |
| 245 | 3 | WriteMesh(path, {std::move(mesh), std::move(normals), {"terrain", {0.5, 0.5, 0.5}}}); | |
| 246 | 3 | LogInfo("pipeline", "Wrote a terrain mesh of ", triangles, " triangles to ", path.string(), | |
| 247 | 6 | " in ", FormatDuration(stopwatch.Milliseconds())); | |
| 248 | 3 | return true; | |
| 249 | 12 | } | |
| 250 | |||
| 251 | /// Whether an output writes the material map or its photometry. | ||
| 252 | 350 | bool MapsMaterials(const OutputSpec& output) { | |
| 253 | 682 | return std::holds_alternative<MaterialsOutput>(output) || | |
| 254 | 682 | std::holds_alternative<PhotometryOutput>(output); | |
| 255 | } | ||
| 256 | |||
| 257 | /// Whether an output writes the heights, albedo or materials over its grid. | ||
| 258 | 502 | bool WritesGrid(const OutputSpec& output) { | |
| 259 | 722 | return std::holds_alternative<HeightmapOutput>(output) || | |
| 260 | 406 | std::holds_alternative<AlbedoOutput>(output) || | |
| 261 | 908 | std::holds_alternative<TerrainMeshOutput>(output) || MapsMaterials(output); | |
| 262 | } | ||
| 263 | |||
| 264 | } // namespace | ||
| 265 | |||
| 266 | 118 | const GeneratedGrid* GeneratedTerrain::Find(const RasterGrid& grid) const { | |
| 267 | 122 | for (const GeneratedGrid& generated : grids) { | |
| 268 | 120 | if (RasterGrid::FromRaster(generated.heights) == grid) { | |
| 269 | 116 | return &generated; | |
| 270 | } | ||
| 271 | } | ||
| 272 | 2 | return nullptr; | |
| 273 | } | ||
| 274 | |||
| 275 | 154 | GeneratedTerrain GenerateTerrain(const TerrainConfig& config, | |
| 276 | const std::filesystem::path& directory, unsigned threads, | ||
| 277 | std::optional<Backend> backend) { | ||
| 278 | 154 | GeneratedTerrain generated{BuildTerrain(config, directory)}; | |
| 279 | 153 | generated.backend = ResolveBackend(backend.value_or(DefaultBackend())); | |
| 280 | 153 | const Terrain& terrain = generated.terrain; | |
| 281 | 153 | const Stopwatch stopwatch; | |
| 282 | 153 | const std::optional<RasterGrid> base = BaseGrid(terrain); | |
| 283 | 153 | std::vector<RasterGrid> grids; | |
| 284 | 404 | for (const OutputSpec& output : config.outputs) { | |
| 285 | 251 | if (WritesGrid(output)) { | |
| 286 | 174 | const RasterGrid grid = *config::OutputGrid(output, base, terrain.surface); | |
| 287 | 174 | if (std::find(grids.begin(), grids.end(), grid) == grids.end()) { | |
| 288 | 145 | grids.push_back(grid); | |
| 289 | } | ||
| 290 | } | ||
| 291 | } | ||
| 292 | 153 | std::vector<StepTime> steps; | |
| 293 | 462 | for (std::size_t n = 0; n < config.layers.size(); ++n) { | |
| 294 | 618 | steps.push_back({"layer " + std::to_string(n + 1) + " " + LayerType(config.layers[n])}); | |
| 295 | } | ||
| 296 | 153 | const std::size_t albedoStep = steps.size(); | |
| 297 | 153 | steps.push_back({"albedo"}); | |
| 298 | 153 | steps.push_back({"materials"}); | |
| 299 | 321 | const auto timed = [&](std::size_t step, double milliseconds) { | |
| 300 | 321 | steps[step].milliseconds += milliseconds; | |
| 301 | 321 | LogInfo("pipeline", " ", steps[step].name, " in ", FormatDuration(milliseconds)); | |
| 302 | 321 | }; | |
| 303 | 174 | const auto generatedOver = [&](const RasterGrid& grid) -> GeneratedGrid& { | |
| 304 | 177 | for (GeneratedGrid& existing : generated.grids) { | |
| 305 | 32 | if (RasterGrid::FromRaster(existing.heights) == grid) { | |
| 306 | 29 | return existing; | |
| 307 | } | ||
| 308 | } | ||
| 309 | 145 | LogInfo("pipeline", "Grid ", generated.grids.size() + 1, " of ", grids.size(), ": ", | |
| 310 | 290 | grid.width, "x", grid.height, " at ", FormatSpacing(grid.Spacing(terrain.surface))); | |
| 311 | 145 | EvaluationOptions options{0.0, threads, generated.backend}; | |
| 312 | 145 | options.onLayer = timed; | |
| 313 | 145 | generated.grids.push_back({Evaluate(terrain, grid, options), {}}); | |
| 314 | 145 | return generated.grids.back(); | |
| 315 | 290 | }; | |
| 316 | 153 | std::optional<Albedo> albedo; | |
| 317 | 404 | for (const OutputSpec& output : config.outputs) { | |
| 318 | 251 | if (!WritesGrid(output)) { | |
| 319 | 77 | continue; | |
| 320 | } | ||
| 321 | 174 | const RasterGrid grid = *config::OutputGrid(output, base, terrain.surface); | |
| 322 | 174 | GeneratedGrid& target = generatedOver(grid); | |
| 323 | 174 | if (std::holds_alternative<AlbedoOutput>(output) && target.albedo.empty()) { | |
| 324 | 17 | const Stopwatch albedoTime; | |
| 325 | 17 | if (!albedo) { | |
| 326 | 17 | albedo = BuildAlbedo(config, terrain, directory); | |
| 327 | } | ||
| 328 | 17 | const TerrainGround ground(terrain, terrain.layers.size()); | |
| 329 | 34 | target.albedo = EvaluateAlbedo(*albedo, grid, | |
| 330 | 17 | {.spacing = 0.0, .threads = threads, .ground = &ground}); | |
| 331 | 17 | timed(albedoStep, albedoTime.Milliseconds()); | |
| 332 | 17 | } | |
| 333 | 174 | if (MapsMaterials(output) && target.materials.empty()) { | |
| 334 | 6 | const Stopwatch materialsTime; | |
| 335 | 12 | target.materials = EvaluateMaterials(terrain, config.materials->options, grid, | |
| 336 | 18 | EvaluationOptions{0.0, threads}); | |
| 337 | 6 | timed(albedoStep + 1, materialsTime.Milliseconds()); | |
| 338 | } | ||
| 339 | } | ||
| 340 | 153 | generated.geoKeys = KeysOf(terrain); | |
| 341 | 153 | generated.seconds = stopwatch.Milliseconds() / 1000.0; | |
| 342 | 153 | if (!generated.grids.empty()) { | |
| 343 | 142 | LogStepTimes(steps, generated.grids.size(), stopwatch.Milliseconds()); | |
| 344 | } | ||
| 345 | 306 | return generated; | |
| 346 | 1536 | } | |
| 347 | |||
| 348 | 88 | std::string Summary(const GeneratedTerrain& terrain) { | |
| 349 | 88 | std::ostringstream text; | |
| 350 | 88 | text << std::setprecision(3); | |
| 351 | 88 | if (terrain.grids.empty()) { | |
| 352 | 6 | return "Built the terrain; no output grids to evaluate before writing"; | |
| 353 | } | ||
| 354 | 85 | if (terrain.grids.size() == 1) { | |
| 355 | 83 | text << "Generated a " << terrain.grids[0].heights.Width() << "x" | |
| 356 | 83 | << terrain.grids[0].heights.Height() << " terrain"; | |
| 357 | } else { | ||
| 358 | 2 | text << "Generated " << terrain.grids.size() << " grids ("; | |
| 359 | 6 | for (std::size_t i = 0; i < terrain.grids.size(); ++i) { | |
| 360 | 4 | text << (i > 0 ? ", " : "") << terrain.grids[i].heights.Width() << "x" | |
| 361 | 4 | << terrain.grids[i].heights.Height(); | |
| 362 | } | ||
| 363 | 2 | text << ")"; | |
| 364 | } | ||
| 365 | 85 | text << " in " << terrain.seconds << " s on " << DescribeBackend(terrain.backend); | |
| 366 | 85 | return text.str(); | |
| 367 | 88 | } | |
| 368 | |||
| 369 | 104 | std::vector<std::filesystem::path> WriteOutputs(const TerrainConfig& config, | |
| 370 | const GeneratedTerrain& terrain, | ||
| 371 | const std::filesystem::path& directory) { | ||
| 372 | 104 | const std::optional<RasterGrid> base = BaseGrid(terrain.terrain); | |
| 373 | 104 | std::vector<std::filesystem::path> written; | |
| 374 | 104 | std::vector<PlacedBoulderSet> placed; | |
| 375 | 288 | for (const OutputSpec& spec : config.outputs) { | |
| 376 | const std::string& relative = | ||
| 377 | 188 | std::visit([](const auto& output) -> const std::string& { return output.path; }, spec); | |
| 378 | 188 | const std::filesystem::path path = config::Resolve(relative, directory); | |
| 379 | 188 | if (path.has_parent_path()) { | |
| 380 | 189 | std::filesystem::create_directories(path.parent_path()); | |
| 381 | } | ||
| 382 | const OutputContext context{terrain, | ||
| 383 | 187 | config::OutputGrid(spec, base, terrain.terrain.surface), | |
| 384 | 187 | SpacingOf(spec), &config, &placed}; | |
| 385 | 187 | if (std::holds_alternative<MeshOutput>(spec) == context.grid.has_value()) { | |
| 386 | 1 | throw std::runtime_error("No grid to write " + path.string() + " over"); | |
| 387 | } | ||
| 388 | 186 | if (std::visit([&](const auto& output) { return Write(output, path, context); }, spec)) { | |
| 389 | 183 | written.push_back(path); | |
| 390 | } | ||
| 391 | 188 | } | |
| 392 | 200 | return written; | |
| 393 | 108 | } | |
| 394 | |||
| 395 | } // namespace terraingen | ||
| 396 |