albedo/blast_zones.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/albedo/blast_zones.h" | ||
| 2 | |||
| 3 | #include "terraingen/layers/fade.h" | ||
| 4 | #include "terraingen/noise/fractal.h" | ||
| 5 | #include "terraingen/util/checks.h" | ||
| 6 | |||
| 7 | #include <algorithm> | ||
| 8 | #include <cmath> | ||
| 9 | #include <numbers> | ||
| 10 | #include <stdexcept> | ||
| 11 | #include <utility> | ||
| 12 | |||
| 13 | namespace terraingen { | ||
| 14 | |||
| 15 | namespace { | ||
| 16 | |||
| 17 | constexpr double kFullRadius = 0.3; | ||
| 18 | constexpr double kLobeRing = 180.0 / (std::numbers::pi * 45.0); | ||
| 19 | |||
| 20 | /// The zone's radial profile at `rho` of its radius: 1 to `kFullRadius`, fading to 0 at 1. | ||
| 21 | 200532 | double Profile(double rho) { | |
| 22 | 200532 | return 1.0 - Smoothstep(std::clamp((rho - kFullRadius) / (1.0 - kFullRadius), 0.0, 1.0)); | |
| 23 | } | ||
| 24 | |||
| 25 | /// The semi-axes (long, short) in metres of `zone`. | ||
| 26 | 1927697 | std::pair<double, double> SemiAxes(const BlastZone& zone) { | |
| 27 | 1927697 | const double minor = std::sqrt(zone.area / (std::numbers::pi * zone.elongation)); | |
| 28 | 1927697 | return {zone.elongation * minor, minor}; | |
| 29 | } | ||
| 30 | |||
| 31 | } // namespace | ||
| 32 | |||
| 33 | 30 | void ValidateBlastZone(const BlastZone& zone) { | |
| 34 | 30 | if (!(std::isfinite(zone.x) && std::isfinite(zone.y) && std::isfinite(zone.headingDegrees))) { | |
| 35 | 2 | throw std::invalid_argument("A blast zone needs a finite position and heading"); | |
| 36 | } | ||
| 37 | 28 | RequirePositive(zone.area, "A blast zone's area"); | |
| 38 | 44 | if (!(zone.elongation >= 1.0 && std::isfinite(zone.elongation) && zone.haloReach >= 1.0 && | |
| 39 | 20 | std::isfinite(zone.haloReach))) { | |
| 40 | 4 | throw std::invalid_argument("A blast zone's elongation and halo reach must be at least 1"); | |
| 41 | } | ||
| 42 | 72 | for (const double change : {zone.change, zone.haloChange, zone.coreChange}) { | |
| 43 | 56 | if (!(change > -1.0 && std::isfinite(change))) { | |
| 44 | 4 | throw std::invalid_argument("Blast zone changes must be finite and above -1"); | |
| 45 | } | ||
| 46 | } | ||
| 47 | 16 | RequireNonNegative(zone.coreRadius, "A blast zone's core radius"); | |
| 48 | 14 | if (!(zone.raggedness >= 0.0 && zone.raggedness < 1.0)) { | |
| 49 | 2 | throw std::invalid_argument("A blast zone's raggedness must be in [0, 1)"); | |
| 50 | } | ||
| 51 | 12 | } | |
| 52 | |||
| 53 | 451218 | double BlastZoneMeanShare() { | |
| 54 | 5 | static const double share = [] { | |
| 55 | 5 | constexpr int kSteps = 1 << 14; | |
| 56 | 5 | double sum = 0.0; | |
| 57 | 81925 | for (int i = 0; i < kSteps; ++i) { | |
| 58 | 81920 | const double rho = (i + 0.5) / kSteps; | |
| 59 | 81920 | sum += Profile(rho) * 2.0 * rho / kSteps; | |
| 60 | } | ||
| 61 | 5 | return sum; | |
| 62 | 451218 | }(); | |
| 63 | 451218 | return share; | |
| 64 | } | ||
| 65 | |||
| 66 | 15 | BlastZoneAlbedo::BlastZoneAlbedo(std::vector<BlastZone> zones, Surface surface) | |
| 67 | 15 | : zones_(std::move(zones)), surface_(std::move(surface)) { | |
| 68 | 22 | for (const BlastZone& zone : zones_) { | |
| 69 | 15 | ValidateBlastZone(zone); | |
| 70 | } | ||
| 71 | 31 | } | |
| 72 | |||
| 73 | 1476481 | double BlastZoneAlbedo::Reach(const BlastZone& zone) { | |
| 74 | 1476481 | return std::fmax(SemiAxes(zone).first * zone.haloReach, zone.coreRadius); | |
| 75 | } | ||
| 76 | |||
| 77 | 1476479 | double BlastZoneAlbedo::Change(const BlastZone& zone, double x, double y) const { | |
| 78 | 1476479 | const Vec2d offset = surface_.Offset({zone.x, zone.y}, {x, y}); | |
| 79 | 1476479 | const double distance = std::hypot(offset.x, offset.y); | |
| 80 | 1476479 | if (distance >= Reach(zone)) { | |
| 81 | 1025263 | return 0.0; | |
| 82 | } | ||
| 83 | 451216 | const auto [major, minor] = SemiAxes(zone); | |
| 84 | 451216 | const double heading = zone.headingDegrees * std::numbers::pi / 180.0; | |
| 85 | 451216 | const double u = (offset.x * std::cos(heading) + offset.y * std::sin(heading)) / major; | |
| 86 | 451216 | const double v = (offset.y * std::cos(heading) - offset.x * std::sin(heading)) / minor; | |
| 87 | 451216 | double lobe = 1.0; | |
| 88 | 451216 | if (zone.raggedness > 0.0) { | |
| 89 | 422429 | FractalParams params; | |
| 90 | 422429 | params.seed = zone.seed ^ 0x424c5354u; | |
| 91 | 422429 | params.wavelength = 1.0f; | |
| 92 | 422429 | params.amplitude = 1.0f; | |
| 93 | 422429 | params.octaves = 3; | |
| 94 | 422429 | const double angle = std::atan2(v, u); | |
| 95 | const double n = static_cast<double>( | ||
| 96 | 422429 | Fractal3(kLobeRing * std::cos(angle), kLobeRing * std::sin(angle), 0.0, params)); | |
| 97 | 422429 | lobe = 1.0 - zone.raggedness * std::clamp(0.5 - n, 0.0, 1.0); | |
| 98 | } | ||
| 99 | 451216 | const double rho = std::hypot(u, v) / lobe; | |
| 100 | 451216 | const double share = BlastZoneMeanShare(); | |
| 101 | 451216 | const double peak = (zone.change - zone.haloChange * (1.0 - share)) / share; | |
| 102 | 451216 | const double inner = rho < 1.0 ? Profile(rho) : 0.0; | |
| 103 | const double halo = | ||
| 104 | 783820 | rho <= 1.0 ? 1.0 | |
| 105 | 332604 | : zone.haloReach > 1.0 | |
| 106 | 332604 | ? 1.0 - Smoothstep(std::clamp((rho - 1.0) / (zone.haloReach - 1.0), 0.0, 1.0)) | |
| 107 | 451216 | : 0.0; | |
| 108 | 451216 | double change = peak * inner + (1.0 - inner) * zone.haloChange * halo; | |
| 109 | 451216 | if (zone.coreRadius > 0.0) { | |
| 110 | const double core = | ||
| 111 | 422433 | 1.0 - Smoothstep(std::clamp((distance / zone.coreRadius - 0.7) / 0.3, 0.0, 1.0)); | |
| 112 | 422433 | change = zone.coreChange * core + (1.0 - core) * change; | |
| 113 | } | ||
| 114 | 451216 | return change; | |
| 115 | } | ||
| 116 | |||
| 117 | 1447329 | void BlastZoneAlbedo::ApplyAt(AlbedoSample& value, std::size_t bands, double x, double y, | |
| 118 | const AlbedoContext& context) const { | ||
| 119 | 1447329 | ValidateAlbedoContext(context); | |
| 120 | 1447329 | double change = 0.0; | |
| 121 | 2894658 | for (const BlastZone& zone : zones_) { | |
| 122 | 1447329 | change += Change(zone, x, y); | |
| 123 | } | ||
| 124 | 1447329 | if (change == 0.0) { | |
| 125 | 1123757 | return; | |
| 126 | } | ||
| 127 | 323572 | const double factor = std::fmax(0.0, 1.0 + change); | |
| 128 | 647144 | for (std::size_t band = 0; band < bands; ++band) { | |
| 129 | 323572 | if (!std::isnan(value[band])) { | |
| 130 | 323572 | value[band] = static_cast<float>(factor * static_cast<double>(value[band])); | |
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | } // namespace terraingen | ||
| 136 |