layers/pavement.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/layers/pavement.h" | ||
| 2 | |||
| 3 | #include "terraingen/noise/perlin.h" | ||
| 4 | #include "terraingen/random/stream.h" | ||
| 5 | #include "terraingen/util/checks.h" | ||
| 6 | #include "terraingen/util/surface.h" | ||
| 7 | |||
| 8 | #include <algorithm> | ||
| 9 | #include <cmath> | ||
| 10 | #include <limits> | ||
| 11 | #include <numbers> | ||
| 12 | #include <stdexcept> | ||
| 13 | |||
| 14 | namespace terraingen { | ||
| 15 | |||
| 16 | namespace { | ||
| 17 | |||
| 18 | constexpr std::uint32_t kPavementStream = 0x50415645u; | ||
| 19 | constexpr const char* kExposureReader = "A pavement that reads the exposure field"; | ||
| 20 | /// Plate sizes per lattice unit of the coarse and fine edge warp, and the fine warp's share. | ||
| 21 | constexpr double kWarpScale = 3.0; | ||
| 22 | constexpr double kFineWarpScale = 0.75; | ||
| 23 | constexpr double kFineWarp = 0.35; | ||
| 24 | /// Cells searched each way for the nearest plate sites; enough for any jitter in [0, 1]. | ||
| 25 | constexpr std::int64_t kRings = 2; | ||
| 26 | /// The most crack octaves. | ||
| 27 | constexpr int kMaxOctaves = 8; | ||
| 28 | /// Seed step between octaves. | ||
| 29 | constexpr std::uint32_t kOctaveSeed = 101u; | ||
| 30 | /// Mixes a parent plate's id into its finer octave's seed, so finer cracks end at its edges. | ||
| 31 | constexpr std::uint32_t kParentMix = 0x9e3779b9u; | ||
| 32 | |||
| 33 | /// One plate: its site and the plane it lies in. | ||
| 34 | struct Site { | ||
| 35 | Vec2d centre; | ||
| 36 | double step = 0.0; | ||
| 37 | double slope = 0.0; | ||
| 38 | Vec2d downhill; | ||
| 39 | std::uint32_t id = 0; | ||
| 40 | |||
| 41 | 11042902 | double HeightAt(const Vec2d& p) const { | |
| 42 | 11042902 | return step + slope * ((p.x - centre.x) * downhill.x + (p.y - centre.y) * downhill.y); | |
| 43 | } | ||
| 44 | }; | ||
| 45 | |||
| 46 | } // namespace | ||
| 47 | |||
| 48 | 31 | void ValidatePavementOptions(const PavementOptions& options) { | |
| 49 | 31 | RequirePositive(options.plateSize, "Pavement plateSize"); | |
| 50 | 30 | RequirePositive(options.crackWidth, "Pavement crackWidth"); | |
| 51 | 29 | RequirePositive(options.exposureSpacing, "Pavement exposureSpacing"); | |
| 52 | 55 | if (!(IsNonNegative(options.crackDepth) && IsNonNegative(options.edgeRounding) && | |
| 53 | 27 | IsNonNegative(options.maxStep) && IsNonNegative(options.warp))) { | |
| 54 | throw std::invalid_argument( | ||
| 55 | 1 | "Pavement crack depth, edge rounding, step and warp must be finite and not negative"); | |
| 56 | } | ||
| 57 | 27 | if (!(options.maxTilt >= 0.0 && options.maxTilt < 0.5 * std::numbers::pi)) { | |
| 58 | 1 | throw std::invalid_argument("Pavement maxTilt must be in [0, 90) deg"); | |
| 59 | } | ||
| 60 | 26 | if (!(options.jitter >= 0.0 && options.jitter <= 1.0)) { | |
| 61 | 1 | throw std::invalid_argument("Pavement jitter must be in [0, 1]"); | |
| 62 | } | ||
| 63 | 45 | if (!(options.octaves >= 1 && options.octaves <= kMaxOctaves && options.lacunarity > 1.0 && | |
| 64 | 20 | std::isfinite(options.lacunarity) && options.gain > 0.0 && options.gain <= 1.0)) { | |
| 65 | throw std::invalid_argument( | ||
| 66 | 6 | "Pavement needs 1 to 8 octaves, a lacunarity above 1 and a gain in (0, 1]"); | |
| 67 | } | ||
| 68 | 19 | if (!(std::isfinite(options.anchor.x) && std::isfinite(options.anchor.y))) { | |
| 69 | 1 | throw std::invalid_argument("Pavement anchor must be finite"); | |
| 70 | } | ||
| 71 | 18 | if (options.exposure) { | |
| 72 | 4 | ValidateExposureParams(*options.exposure); | |
| 73 | } | ||
| 74 | 17 | ValidateResolutionFade(options.fade); | |
| 75 | 17 | } | |
| 76 | |||
| 77 | 5 | PavementOptions MarsDesiccationRidges() { | |
| 78 | 5 | PavementOptions ridges; | |
| 79 | 5 | ridges.plateSize = 0.02; | |
| 80 | 5 | ridges.jitter = 0.6; | |
| 81 | 5 | ridges.warp = 0.15; | |
| 82 | 5 | ridges.raised = true; | |
| 83 | 5 | ridges.crackDepth = 0.005; | |
| 84 | 5 | ridges.crackWidth = 0.002; | |
| 85 | 5 | ridges.edgeRounding = 0.001; | |
| 86 | 5 | ridges.maxTilt = 0.0; | |
| 87 | 5 | ridges.maxStep = 0.0; | |
| 88 | 5 | return ridges; | |
| 89 | } | ||
| 90 | |||
| 91 | 27 | PavementLayer::PavementLayer(PavementOptions options, Surface surface) | |
| 92 | 27 | : FormulaLayer(surface), options_(options) { | |
| 93 | 27 | ValidatePavementOptions(options_); | |
| 94 | 14 | if (options_.exposure) { | |
| 95 | 2 | exposure_.emplace(*options_.exposure, surface_); | |
| 96 | } | ||
| 97 | 14 | if (surface_.IsBody()) { | |
| 98 | 1 | anchorDirection_ = UnitVector(options_.anchor.x, options_.anchor.y); | |
| 99 | } | ||
| 100 | 14 | double size = 1.0; | |
| 101 | 14 | double relief = 1.0; | |
| 102 | 39 | for (int k = 0; k < options_.octaves; ++k) { | |
| 103 | ✗ | levels_.push_back({options_.plateSize / size, relief * options_.crackWidth, | |
| 104 | 25 | relief * options_.crackDepth, relief * options_.edgeRounding, | |
| 105 | 25 | relief * options_.maxTilt, relief * options_.maxStep, | |
| 106 | 25 | options_.seed + kOctaveSeed * static_cast<std::uint32_t>(k)}); | |
| 107 | 25 | size *= options_.lacunarity; | |
| 108 | 25 | relief *= options_.gain; | |
| 109 | } | ||
| 110 | 66 | } | |
| 111 | |||
| 112 | 2343524 | bool PavementLayer::Planar(double x, double y, Vec2d& planar) const { | |
| 113 | 2343524 | if (!surface_.IsBody()) { | |
| 114 | 2343521 | planar = {x, y}; | |
| 115 | 2343521 | return true; | |
| 116 | } | ||
| 117 | 3 | if (Dot(UnitVector(x, y), anchorDirection_) <= 0.0) { | |
| 118 | 2 | return false; | |
| 119 | } | ||
| 120 | 1 | planar = surface_.Offset(options_.anchor, {x, y}); | |
| 121 | 1 | return true; | |
| 122 | } | ||
| 123 | |||
| 124 | 5521451 | PavementLayer::Plate PavementLayer::PlateAt(const Vec2d& q, const Level& level, std::uint32_t seed, | |
| 125 | double crackWeight) const { | ||
| 126 | 5521451 | const double size = level.size; | |
| 127 | 22085804 | const auto warp = [&](double scale, double ox, double oy, std::uint32_t salt) { | |
| 128 | return static_cast<double>( | ||
| 129 | 22085804 | PerlinNoise2Precise(q.x / (scale * size) + ox, q.y / (scale * size) + oy, seed + salt)); | |
| 130 | 5521451 | }; | |
| 131 | 5521451 | const double reach = options_.warp * size; | |
| 132 | 5521451 | const Vec2d p{q.x + reach * (warp(kWarpScale, 0.0, 0.0, 7u) + | |
| 133 | 5521451 | kFineWarp * warp(kFineWarpScale, 5.3, -8.1, 9u)), | |
| 134 | 11042902 | q.y + reach * (warp(kWarpScale, 31.7, -12.3, 8u) + | |
| 135 | 5521451 | kFineWarp * warp(kFineWarpScale, -17.9, 4.4, 10u))}; | |
| 136 | 5521451 | const auto ix = static_cast<std::int64_t>(std::floor(p.x / size)); | |
| 137 | 5521451 | const auto iy = static_cast<std::int64_t>(std::floor(p.y / size)); | |
| 138 | 5521451 | Site sites[(2 * kRings + 1) * (2 * kRings + 1)]; | |
| 139 | 5521451 | std::size_t count = 0; | |
| 140 | 5521451 | std::size_t nearest = 0; | |
| 141 | 5521451 | double best = std::numeric_limits<double>::infinity(); | |
| 142 | 33128706 | for (std::int64_t gy = iy - kRings; gy <= iy + kRings; ++gy) { | |
| 143 | 165643530 | for (std::int64_t gx = ix - kRings; gx <= ix + kRings; ++gx) { | |
| 144 | RandomStream random(seed, kPavementStream, static_cast<std::uint32_t>(gx), | ||
| 145 | static_cast<std::uint32_t>(gy), | ||
| 146 | 138036275 | static_cast<std::uint32_t>((gx >> 32) ^ ((gy >> 32) << 16))); | |
| 147 | 138036275 | const double u = random.NextDouble(); | |
| 148 | 138036275 | const double v = random.NextDouble(); | |
| 149 | 138036275 | Site& site = sites[count]; | |
| 150 | 138036275 | site.centre = {(static_cast<double>(gx) + 0.5 + options_.jitter * (u - 0.5)) * size, | |
| 151 | 138036275 | (static_cast<double>(gy) + 0.5 + options_.jitter * (v - 0.5)) * size}; | |
| 152 | 138036275 | site.step = level.maxStep * (2.0 * random.NextDouble() - 1.0); | |
| 153 | 138036275 | site.slope = std::tan(level.maxTilt * random.NextDouble()); | |
| 154 | 138036275 | const double turn = 2.0 * std::numbers::pi * random.NextDouble(); | |
| 155 | 138036275 | site.downhill = {std::cos(turn), std::sin(turn)}; | |
| 156 | 138036275 | site.id = random.NextU32(); | |
| 157 | 138036275 | const double d = std::hypot(p.x - site.centre.x, p.y - site.centre.y); | |
| 158 | 138036275 | if (d < best) { | |
| 159 | 35256511 | best = d; | |
| 160 | 35256511 | nearest = count; | |
| 161 | } | ||
| 162 | 138036275 | ++count; | |
| 163 | } | ||
| 164 | } | ||
| 165 | 5521451 | const Site& own = sites[nearest]; | |
| 166 | 5521451 | double edge = std::numeric_limits<double>::infinity(); | |
| 167 | 5521451 | std::size_t neighbour = nearest; | |
| 168 | 143557726 | for (std::size_t k = 0; k < count; ++k) { | |
| 169 | 138036275 | if (k == nearest) { | |
| 170 | 5521451 | continue; | |
| 171 | } | ||
| 172 | 132514824 | const Vec2d across{sites[k].centre.x - own.centre.x, sites[k].centre.y - own.centre.y}; | |
| 173 | 132514824 | const double length = std::hypot(across.x, across.y); | |
| 174 | 132514824 | const Vec2d middle{0.5 * (sites[k].centre.x + own.centre.x), | |
| 175 | 132514824 | 0.5 * (sites[k].centre.y + own.centre.y)}; | |
| 176 | const double d = | ||
| 177 | 132514824 | ((middle.x - p.x) * across.x + (middle.y - p.y) * across.y) / std::max(length, 1e-12); | |
| 178 | 132514824 | if (d < edge) { | |
| 179 | 33826861 | edge = d; | |
| 180 | 33826861 | neighbour = k; | |
| 181 | } | ||
| 182 | } | ||
| 183 | 5521451 | const double shoulder = 0.5 * level.crackWidth + level.rounding; | |
| 184 | 5521451 | const double crack = 1.0 - Smoothstep(edge / shoulder); | |
| 185 | 5521451 | const double ownHeight = own.HeightAt(p); | |
| 186 | 5521451 | const double plate = ownHeight + 0.5 * crack * (sites[neighbour].HeightAt(p) - ownHeight); | |
| 187 | 5521451 | const double sign = options_.raised ? 1.0 : -1.0; | |
| 188 | 5521451 | return {plate + crackWeight * sign * level.crackDepth * crack, edge, 1.0 - crack, own.id}; | |
| 189 | } | ||
| 190 | |||
| 191 | 1846256 | double PavementLayer::Relief(const Vec2d& q, double spacing) const { | |
| 192 | 1846256 | double relief = 0.0; | |
| 193 | 1846256 | double interior = 1.0; | |
| 194 | 1846256 | std::uint32_t parent = 0; | |
| 195 | 7365585 | for (const Level& level : levels_) { | |
| 196 | const double plateWeight = | ||
| 197 | 5519346 | spacing > 0.0 ? ResolutionWeight(level.size, spacing, options_.fade) : 1.0; | |
| 198 | 5519346 | if (plateWeight <= 0.0 || interior <= 0.0) { | |
| 199 | break; | ||
| 200 | } | ||
| 201 | const double crackWeight = | ||
| 202 | spacing > 0.0 | ||
| 203 | 5519329 | ? ResolutionWeight(level.crackWidth + 2.0 * level.rounding, spacing, options_.fade) | |
| 204 | 5519329 | : 1.0; | |
| 205 | 5519329 | const Plate plate = PlateAt(q, level, level.seed ^ (parent * kParentMix), crackWeight); | |
| 206 | 5519329 | relief += interior * plateWeight * plate.relief; | |
| 207 | 5519329 | interior *= plate.interior; | |
| 208 | 5519329 | parent = plate.id; | |
| 209 | } | ||
| 210 | 1846256 | return relief; | |
| 211 | } | ||
| 212 | |||
| 213 | 2314386 | double PavementLayer::Gate(double x, double y, const Ground* ground) const { | |
| 214 | 2314386 | if (!exposure_) { | |
| 215 | 2436 | return 1.0; | |
| 216 | } | ||
| 217 | 2311950 | RequireGround(ground, kExposureReader); | |
| 218 | 2311949 | return exposure_->Exposure(*ground, x, y, options_.exposureSpacing); | |
| 219 | } | ||
| 220 | |||
| 221 | 27014 | double PavementLayer::ReliefAt(double x, double y) const { | |
| 222 | 27014 | Vec2d q; | |
| 223 | 54028 | return Planar(x, y, q) ? Relief(q, 0.0) : 0.0; | |
| 224 | } | ||
| 225 | |||
| 226 | 2122 | double PavementLayer::EdgeDistanceAt(double x, double y) const { | |
| 227 | 2122 | Vec2d q; | |
| 228 | 4244 | return Planar(x, y, q) ? PlateAt(q, levels_.front(), levels_.front().seed, 1.0).edge | |
| 229 | 4244 | : std::numeric_limits<double>::infinity(); | |
| 230 | } | ||
| 231 | |||
| 232 | 66054 | double PavementLayer::MaterialCoverAt(double x, double y, double /*spacing*/, | |
| 233 | const Ground* ground) const { | ||
| 234 | 66054 | Vec2d q; | |
| 235 | 132107 | return Planar(x, y, q) ? Gate(x, y, ground) : 0.0; | |
| 236 | } | ||
| 237 | |||
| 238 | 2248334 | float PavementLayer::HeightAt(float height, double x, double y, double spacing, | |
| 239 | const Ground* ground) const { | ||
| 240 | 2248334 | Vec2d q; | |
| 241 | 2248334 | if (!Planar(x, y, q) || ResolutionWeight(options_.plateSize, spacing, options_.fade) <= 0.0) { | |
| 242 | 2 | return height; | |
| 243 | } | ||
| 244 | 2248332 | const double gate = Gate(x, y, ground); | |
| 245 | 2248332 | if (gate <= 0.0) { | |
| 246 | 429089 | return height; | |
| 247 | } | ||
| 248 | 1819243 | return static_cast<float>(static_cast<double>(height) + gate * Relief(q, spacing)); | |
| 249 | } | ||
| 250 | |||
| 251 | 2181088 | void PavementLayer::CheckContext(const LayerContext& context) const { | |
| 252 | 2181088 | if (exposure_) { | |
| 253 | 2179851 | RequireGround(context.ground, kExposureReader); | |
| 254 | } | ||
| 255 | 2181087 | } | |
| 256 | |||
| 257 | } // namespace terraingen | ||
| 258 |