layers/boulders/boulder_shape.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/layers/boulders/boulder_shape.h" | ||
| 2 | |||
| 3 | #include "terraingen/noise/hash.h" | ||
| 4 | #include "terraingen/noise/perlin.h" | ||
| 5 | #include "terraingen/random/stream.h" | ||
| 6 | #include "terraingen/util/checks.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 kBoulderStream = 0x424F554Cu; | ||
| 19 | |||
| 20 | /// Throws unless `low` <= `range.min` <= `range.max` <= `high`. | ||
| 21 | 108819 | void CheckRange(const ValueRange& range, double low, double high, const char* what) { | |
| 22 | 108819 | if (!(range.min >= low && range.min <= range.max && range.max <= high)) { | |
| 23 | 8 | throw std::invalid_argument(std::string("Boulder style ") + what + " must satisfy " + | |
| 24 | 16 | std::to_string(low) + | |
| 25 | 12 | " <= min <= max <= " + std::to_string(high)); | |
| 26 | } | ||
| 27 | 108815 | } | |
| 28 | |||
| 29 | 18552 | double Draw(RandomStream& random, const ValueRange& range) { | |
| 30 | 18552 | return range.min + (range.max - range.min) * random.NextDouble(); | |
| 31 | } | ||
| 32 | |||
| 33 | /// The surface roughness of a style at direction `p`: `noiseOctaves` octaves of 3D Perlin noise | ||
| 34 | /// halving in wavelength from `noiseWavelength`, each of half the amplitude, in about [-1, 1]. | ||
| 35 | 1924039 | double SurfaceRoughness(const Vec3d& p, const BoulderStyle& style, std::uint32_t seed) { | |
| 36 | 1924039 | double sum = 0.0; | |
| 37 | 1924039 | double norm = 0.0; | |
| 38 | 1924039 | double amplitude = 1.0; | |
| 39 | 1924039 | double frequency = 1.0 / style.noiseWavelength; | |
| 40 | 9554245 | for (int octave = 0; octave < style.noiseOctaves; ++octave) { | |
| 41 | 7630206 | sum += amplitude * | |
| 42 | 15260412 | static_cast<double>(PerlinNoise3(static_cast<float>(p.x * frequency + 17.31), | |
| 43 | 7630206 | static_cast<float>(p.y * frequency - 5.77), | |
| 44 | 7630206 | static_cast<float>(p.z * frequency + 3.13), | |
| 45 | 7630206 | seed + static_cast<std::uint32_t>(octave))); | |
| 46 | 7630206 | norm += amplitude; | |
| 47 | 7630206 | amplitude *= 0.5; | |
| 48 | 7630206 | frequency *= 2.0; | |
| 49 | } | ||
| 50 | 1924039 | return sum / norm; | |
| 51 | } | ||
| 52 | |||
| 53 | /// A unit vector, uniform on the sphere when `maxTilt` is 1, with |z| at most `maxTilt`. | ||
| 54 | 7283 | Vec3d DrawNormal(RandomStream& random, double maxTilt) { | |
| 55 | 7283 | const double z = maxTilt * (2.0 * random.NextDouble() - 1.0); | |
| 56 | 7283 | const double angle = 2.0 * std::numbers::pi * random.NextDouble(); | |
| 57 | 7283 | const double ring = std::sqrt(std::fmax(0.0, 1.0 - z * z)); | |
| 58 | 7283 | return {ring * std::cos(angle), ring * std::sin(angle), z}; | |
| 59 | } | ||
| 60 | |||
| 61 | /// The index of the style a uniform draw picks, each in proportion to its weight. | ||
| 62 | /// The extents of a library rock, which must be a valid mesh with depth along every axis. | ||
| 63 | 271 | Vec3d CheckRock(const TriangleMesh& mesh) { | |
| 64 | 271 | ValidateMesh(mesh); | |
| 65 | 271 | const Vec3d extents = Bounds(mesh).Extents(); | |
| 66 | 271 | if (!(extents.x > 0.0 && extents.y > 0.0 && extents.z > 0.0)) { | |
| 67 | throw std::invalid_argument( | ||
| 68 | 1 | "Boulder shape library meshes must have depth along every axis"); | |
| 69 | } | ||
| 70 | 270 | return extents; | |
| 71 | } | ||
| 72 | |||
| 73 | 3319 | std::size_t PickStyle(RandomStream& random, const std::vector<BoulderStyle>& styles) { | |
| 74 | 3319 | double total = 0.0; | |
| 75 | 20990 | for (const BoulderStyle& style : styles) { | |
| 76 | 17671 | total += style.weight; | |
| 77 | } | ||
| 78 | 3319 | double pick = total * random.NextDouble(); | |
| 79 | 3319 | std::size_t index = 0; | |
| 80 | 14761 | while (index + 1 < styles.size() && | |
| 81 | 7360 | (styles[index].weight == 0.0 || pick >= styles[index].weight)) { | |
| 82 | 4082 | pick -= styles[index].weight; | |
| 83 | 4082 | ++index; | |
| 84 | } | ||
| 85 | 3319 | return index; | |
| 86 | } | ||
| 87 | |||
| 88 | /// The solid's vertex `p` stretched to `axes`, pulled `rounding` of the way to the ellipsoid of | ||
| 89 | /// those axes and roughened by the style's noise. | ||
| 90 | 719214 | Vec3d ShapeVertex(const Vec3d& p, const Vec3d& axes, double rounding, const BoulderStyle& style, | |
| 91 | std::uint32_t noiseSeed) { | ||
| 92 | 719214 | const Vec3d stretched{p.x * axes.x, p.y * axes.y, p.z * axes.z}; | |
| 93 | 719214 | const Vec3d ray = Normalized(stretched); | |
| 94 | const double ellipsoid = | ||
| 95 | 719214 | 1.0 / std::sqrt(std::pow(ray.x / axes.x, 2.0) + std::pow(ray.y / axes.y, 2.0) + | |
| 96 | 719214 | std::pow(ray.z / axes.z, 2.0)); | |
| 97 | 719214 | const double radius = (1.0 - rounding) * Length(stretched) + rounding * ellipsoid; | |
| 98 | 719214 | const double rough = 1.0 + style.noiseAmplitude * SurfaceRoughness(ray, style, noiseSeed); | |
| 99 | 1438428 | return ray * (radius * rough); | |
| 100 | } | ||
| 101 | |||
| 102 | /// Cuts `mesh` with the style's fracture planes, each at a drawn share of the reach of the solid's | ||
| 103 | /// original corners. `Subdivide` keeps those corners as the first `corners` vertices. | ||
| 104 | /// The smooth minimum of `a` and `b` over `k`: their minimum, eased by up to `k` / 4 where they | ||
| 105 | /// are within `k` of each other. | ||
| 106 | 977132 | double SmoothMin(double a, double b, double k) { | |
| 107 | 977132 | const double h = std::fmax(k - std::fabs(a - b), 0.0) / k; | |
| 108 | 977132 | return std::fmin(a, b) - 0.25 * k * h * h; | |
| 109 | } | ||
| 110 | |||
| 111 | /// A fracture plane: points beyond `offset` along `normal` are cut away. | ||
| 112 | struct Fracture { | ||
| 113 | Vec3d normal; | ||
| 114 | double offset; | ||
| 115 | }; | ||
| 116 | |||
| 117 | /// The planes of `solid`'s faces once stretched to `axes`, one per face. | ||
| 118 | 458 | std::vector<Fracture> SolidFaces(const TriangleMesh& solid, const Vec3d& axes) { | |
| 119 | 458 | std::vector<Fracture> faces; | |
| 120 | 4102 | for (const auto& t : solid.triangles) { | |
| 121 | 3644 | const Vec3d& a = solid.positions[t[0]]; | |
| 122 | const Vec3d normal = | ||
| 123 | 3644 | Normalized(Cross(solid.positions[t[1]] - a, solid.positions[t[2]] - a)); | |
| 124 | 3644 | const Vec3d stretched{normal.x / axes.x, normal.y / axes.y, normal.z / axes.z}; | |
| 125 | 3644 | const double length = Length(stretched); | |
| 126 | 3644 | const Fracture face{stretched * (1.0 / length), Dot(normal, a) / length}; | |
| 127 | 3644 | const bool seen = std::any_of(faces.begin(), faces.end(), [&](const Fracture& f) { | |
| 128 | 10637 | return Dot(f.normal, face.normal) > 1.0 - 1e-9 && | |
| 129 | 10637 | std::fabs(f.offset - face.offset) < 1e-9; | |
| 130 | }); | ||
| 131 | 3644 | if (!seen) { | |
| 132 | 2351 | faces.push_back(face); | |
| 133 | } | ||
| 134 | } | ||
| 135 | 458 | return faces; | |
| 136 | } | ||
| 137 | |||
| 138 | /// Cuts `mesh` with `fractures`, each vertex moving along its ray from the origin to the nearest | ||
| 139 | /// plane: rippled by the style's face roughness and met through a smooth minimum over `softness`, | ||
| 140 | /// so faces undulate and edges are worn round. With `polyhedral`, the planes include the solid's | ||
| 141 | /// own faces, which then bound every ray, roughened by the style's noise of `noiseSeed`. | ||
| 142 | /// Planes too far beyond the cut so far to move a vertex are passed over unroughened. | ||
| 143 | 1312 | void CutSoftly(TriangleMesh& mesh, const std::vector<Fracture>& fractures, | |
| 144 | const BoulderStyle& style, double softness, std::uint32_t seed, bool polyhedral, | ||
| 145 | std::uint32_t noiseSeed) { | ||
| 146 | 1312 | const double frequency = 1.5; | |
| 147 | 1312 | const double blend = softness > 0.0 ? 4.0 * softness : 0.0; | |
| 148 | 654576 | for (Vec3d& p : mesh.positions) { | |
| 149 | 653264 | const double radius = Length(p); | |
| 150 | 653264 | if (!(radius > 0.0)) { | |
| 151 | ✗ | continue; | |
| 152 | } | ||
| 153 | 653264 | const Vec3d ray = p * (1.0 / radius); | |
| 154 | 653264 | double reach = polyhedral ? std::numeric_limits<double>::infinity() : radius; | |
| 155 | 5260986 | for (std::size_t i = 0; i < fractures.size(); ++i) { | |
| 156 | 4607722 | const double along = Dot(fractures[i].normal, ray); | |
| 157 | 4607722 | if (!(along > 0.0)) { | |
| 158 | 2313565 | continue; | |
| 159 | } | ||
| 160 | 2294157 | double offset = fractures[i].offset; | |
| 161 | 4475428 | if (std::isfinite(reach) && | |
| 162 | 2181271 | offset * (1.0 - style.faceRoughness) / along >= (reach + blend) * (1.0 + 1e-9)) { | |
| 163 | 1202218 | continue; | |
| 164 | } | ||
| 165 | 1091939 | if (style.faceRoughness > 0.0) { | |
| 166 | 1091939 | const double shift = 7.13 * static_cast<double>(i); | |
| 167 | 1091939 | const Vec3d at{ray.x * frequency + shift, ray.y * frequency - shift, | |
| 168 | 1091939 | ray.z * frequency + 0.5 * shift}; | |
| 169 | 1091939 | offset *= | |
| 170 | 2183878 | 1.0 + style.faceRoughness * | |
| 171 | 1091939 | SurfaceRoughness(at, style, seed + static_cast<std::uint32_t>(i)); | |
| 172 | } | ||
| 173 | 1091939 | const double cut = offset / along; | |
| 174 | 1206746 | reach = softness > 0.0 && std::isfinite(reach) ? SmoothMin(reach, cut, blend) | |
| 175 | 114807 | : std::fmin(reach, cut); | |
| 176 | } | ||
| 177 | 653264 | if (polyhedral) { | |
| 178 | 112886 | reach *= 1.0 + style.noiseAmplitude * SurfaceRoughness(ray, style, noiseSeed); | |
| 179 | } | ||
| 180 | 653264 | p = ray * reach; | |
| 181 | } | ||
| 182 | 1312 | } | |
| 183 | |||
| 184 | 3319 | void ApplyFractures(TriangleMesh& mesh, std::size_t corners, const BoulderStyle& style, | |
| 185 | RandomStream& random, double softness, std::uint32_t seed, | ||
| 186 | const std::vector<Fracture>& faces, std::uint32_t noiseSeed) { | ||
| 187 | 3319 | const int span = style.maxFractures - style.minFractures + 1; | |
| 188 | const int fractures = | ||
| 189 | 3319 | style.minFractures + std::min(span - 1, static_cast<int>(random.NextDouble() * span)); | |
| 190 | 3319 | if (softness > 0.0 || style.faceRoughness > 0.0) { | |
| 191 | 1312 | std::vector<Fracture> planes = faces; | |
| 192 | 8592 | for (int n = 0; n < fractures; ++n) { | |
| 193 | 7280 | const Vec3d normal = DrawNormal(random, style.maxFractureTilt); | |
| 194 | 7280 | const double depth = Draw(random, style.fractureDepth); | |
| 195 | 7280 | double reach = 0.0; | |
| 196 | 85265 | for (std::size_t v = 0; v < corners; ++v) { | |
| 197 | 77985 | reach = std::fmax(reach, Dot(normal, mesh.positions[v])); | |
| 198 | } | ||
| 199 | 7280 | if (depth * reach > 0.0) { | |
| 200 | 7280 | planes.push_back({normal, depth * reach}); | |
| 201 | } | ||
| 202 | } | ||
| 203 | 1312 | CutSoftly(mesh, planes, style, softness, seed, !faces.empty(), noiseSeed); | |
| 204 | 1312 | return; | |
| 205 | 1312 | } | |
| 206 | 2010 | for (int n = 0; n < fractures; ++n) { | |
| 207 | 3 | const Vec3d normal = DrawNormal(random, style.maxFractureTilt); | |
| 208 | 3 | const double depth = Draw(random, style.fractureDepth); | |
| 209 | 3 | double reach = 0.0; | |
| 210 | 31 | for (std::size_t v = 0; v < corners; ++v) { | |
| 211 | 28 | reach = std::fmax(reach, Dot(normal, mesh.positions[v])); | |
| 212 | } | ||
| 213 | 3 | const double plane = depth * reach; | |
| 214 | 3 | if (!(plane > 0.0)) { | |
| 215 | ✗ | continue; | |
| 216 | } | ||
| 217 | 1417 | for (Vec3d& p : mesh.positions) { | |
| 218 | 1414 | const double along = Dot(normal, p); | |
| 219 | 1414 | if (along > plane) { | |
| 220 | 248 | p = p * (plane / along); | |
| 221 | } | ||
| 222 | } | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | } // namespace | ||
| 227 | |||
| 228 | 438 | std::vector<BoulderStyle> DefaultBoulderStyles() { | |
| 229 | return { | ||
| 230 | {.name = "angular", | ||
| 231 | .weight = 0.2, | ||
| 232 | .solid = BaseSolid::Icosahedron, | ||
| 233 | .intermediate = {0.6, 0.9}, | ||
| 234 | .shortAxis = {0.45, 0.7}, | ||
| 235 | .minFractures = 7, | ||
| 236 | .maxFractures = 12, | ||
| 237 | .fractureDepth = {0.55, 0.85}, | ||
| 238 | .maxFractureTilt = 1.0, | ||
| 239 | .noiseAmplitude = 0.06, | ||
| 240 | .noiseWavelength = 0.9, | ||
| 241 | .noiseOctaves = 4, | ||
| 242 | .edgeSoftness = {0.02, 0.06}, | ||
| 243 | .faceRoughness = 0.03}, | ||
| 244 | {.name = "platy", | ||
| 245 | .weight = 0.12, | ||
| 246 | .solid = BaseSolid::Icosahedron, | ||
| 247 | .intermediate = {0.6, 0.9}, | ||
| 248 | .shortAxis = {0.22, 0.38}, | ||
| 249 | .minFractures = 5, | ||
| 250 | .maxFractures = 9, | ||
| 251 | .fractureDepth = {0.6, 0.88}, | ||
| 252 | .maxFractureTilt = 0.7, | ||
| 253 | .noiseAmplitude = 0.05, | ||
| 254 | .noiseWavelength = 0.9, | ||
| 255 | .noiseOctaves = 4, | ||
| 256 | .edgeSoftness = {0.02, 0.05}, | ||
| 257 | .faceRoughness = 0.03}, | ||
| 258 | {.name = "subrounded", | ||
| 259 | .weight = 0.15, | ||
| 260 | .solid = BaseSolid::Icosahedron, | ||
| 261 | .intermediate = {0.65, 0.9}, | ||
| 262 | .shortAxis = {0.45, 0.7}, | ||
| 263 | .minFractures = 4, | ||
| 264 | .maxFractures = 7, | ||
| 265 | .fractureDepth = {0.65, 0.9}, | ||
| 266 | .maxFractureTilt = 1.0, | ||
| 267 | .noiseAmplitude = 0.07, | ||
| 268 | .noiseWavelength = 0.8, | ||
| 269 | .noiseOctaves = 4, | ||
| 270 | .edgeSoftness = {0.06, 0.12}, | ||
| 271 | .faceRoughness = 0.02}, | ||
| 272 | {.name = "rounded", | ||
| 273 | .weight = 0.1, | ||
| 274 | .solid = BaseSolid::Icosahedron, | ||
| 275 | .intermediate = {0.7, 0.95}, | ||
| 276 | .shortAxis = {0.5, 0.75}, | ||
| 277 | .minFractures = 0, | ||
| 278 | .maxFractures = 3, | ||
| 279 | .fractureDepth = {0.75, 0.95}, | ||
| 280 | .maxFractureTilt = 1.0, | ||
| 281 | .noiseAmplitude = 0.1, | ||
| 282 | .noiseWavelength = 0.7, | ||
| 283 | .noiseOctaves = 4, | ||
| 284 | .edgeSoftness = {0.1, 0.18}, | ||
| 285 | .faceRoughness = 0.02}, | ||
| 286 | {.name = "block", | ||
| 287 | .weight = 0.13, | ||
| 288 | .solid = BaseSolid::Cube, | ||
| 289 | .intermediate = {0.6, 0.85}, | ||
| 290 | .shortAxis = {0.4, 0.6}, | ||
| 291 | .rounding = {0.0, 0.15}, | ||
| 292 | .minFractures = 3, | ||
| 293 | .maxFractures = 7, | ||
| 294 | .fractureDepth = {0.6, 0.85}, | ||
| 295 | .maxFractureTilt = 1.0, | ||
| 296 | .noiseAmplitude = 0.07, | ||
| 297 | .noiseWavelength = 0.8, | ||
| 298 | .noiseOctaves = 4, | ||
| 299 | .edgeSoftness = {0.06, 0.12}, | ||
| 300 | .faceRoughness = 0.06}, | ||
| 301 | {.name = "wedge", | ||
| 302 | .weight = 0.1, | ||
| 303 | .solid = BaseSolid::Prism, | ||
| 304 | .intermediate = {0.55, 0.85}, | ||
| 305 | .shortAxis = {0.4, 0.6}, | ||
| 306 | .rounding = {0.0, 0.2}, | ||
| 307 | .minFractures = 2, | ||
| 308 | .maxFractures = 5, | ||
| 309 | .fractureDepth = {0.65, 0.9}, | ||
| 310 | .maxFractureTilt = 1.0, | ||
| 311 | .noiseAmplitude = 0.07, | ||
| 312 | .noiseWavelength = 0.8, | ||
| 313 | .noiseOctaves = 4, | ||
| 314 | .edgeSoftness = {0.06, 0.12}, | ||
| 315 | .faceRoughness = 0.06}, | ||
| 316 | {.name = "pyramid", | ||
| 317 | .weight = 0.1, | ||
| 318 | .solid = BaseSolid::Pyramid, | ||
| 319 | .intermediate = {0.7, 0.95}, | ||
| 320 | .shortAxis = {0.45, 0.7}, | ||
| 321 | .rounding = {0.0, 0.2}, | ||
| 322 | .minFractures = 2, | ||
| 323 | .maxFractures = 4, | ||
| 324 | .fractureDepth = {0.7, 0.92}, | ||
| 325 | .maxFractureTilt = 1.0, | ||
| 326 | .noiseAmplitude = 0.07, | ||
| 327 | .noiseWavelength = 0.8, | ||
| 328 | .noiseOctaves = 4, | ||
| 329 | .edgeSoftness = {0.06, 0.12}, | ||
| 330 | .faceRoughness = 0.06}, | ||
| 331 | {.name = "shard", | ||
| 332 | .weight = 0.05, | ||
| 333 | .solid = BaseSolid::Tetrahedron, | ||
| 334 | .intermediate = {0.6, 0.9}, | ||
| 335 | .shortAxis = {0.4, 0.6}, | ||
| 336 | .rounding = {0.0, 0.2}, | ||
| 337 | .minFractures = 2, | ||
| 338 | .maxFractures = 4, | ||
| 339 | .fractureDepth = {0.7, 0.92}, | ||
| 340 | .maxFractureTilt = 1.0, | ||
| 341 | .noiseAmplitude = 0.04, | ||
| 342 | .noiseWavelength = 0.8, | ||
| 343 | .noiseOctaves = 4, | ||
| 344 | .edgeSoftness = {0.02, 0.05}, | ||
| 345 | .faceRoughness = 0.03}, | ||
| 346 | {.name = "sphere", | ||
| 347 | .weight = 0.05, | ||
| 348 | .solid = BaseSolid::Icosahedron, | ||
| 349 | .intermediate = {0.85, 1.0}, | ||
| 350 | .shortAxis = {0.7, 0.9}, | ||
| 351 | .minFractures = 0, | ||
| 352 | .maxFractures = 1, | ||
| 353 | .fractureDepth = {0.8, 0.95}, | ||
| 354 | .maxFractureTilt = 1.0, | ||
| 355 | .noiseAmplitude = 0.08, | ||
| 356 | .noiseWavelength = 0.7, | ||
| 357 | .noiseOctaves = 4, | ||
| 358 | .edgeSoftness = {0.12, 0.2}, | ||
| 359 | .faceRoughness = 0.02}, | ||
| 360 | 5256 | }; | |
| 361 | 1314 | } | |
| 362 | |||
| 363 | 3785 | void ValidateBoulderShapeOptions(const BoulderShapeOptions& options) { | |
| 364 | 3785 | if (options.styles.empty()) { | |
| 365 | 2 | throw std::invalid_argument("Boulder shapes need at least one style"); | |
| 366 | } | ||
| 367 | 3783 | double total = 0.0; | |
| 368 | 25540 | for (const BoulderStyle& style : options.styles) { | |
| 369 | 21767 | RequireNonNegative(style.weight, "Boulder style weights"); | |
| 370 | 21766 | total += style.weight; | |
| 371 | 21766 | CheckRange(style.intermediate, 1e-3, 1.0, "intermediate"); | |
| 372 | 21765 | CheckRange(style.shortAxis, 1e-3, 1.0, "shortAxis"); | |
| 373 | 21764 | CheckRange(style.rounding, 0.0, 1.0, "rounding"); | |
| 374 | 21763 | CheckRange(style.fractureDepth, 1e-3, 1.0, "fractureDepth"); | |
| 375 | 21763 | if (!(style.minFractures >= 0 && style.minFractures <= style.maxFractures && | |
| 376 | 21762 | style.maxFractures <= 64)) { | |
| 377 | throw std::invalid_argument( | ||
| 378 | 1 | "Boulder style fractures must satisfy 0 <= min <= max <= 64"); | |
| 379 | } | ||
| 380 | 21762 | RequireUnitInterval(style.maxFractureTilt, "Boulder style maxFractureTilt"); | |
| 381 | 21761 | CheckRange(style.edgeSoftness, 0.0, 0.5, "edgeSoftness"); | |
| 382 | 21760 | if (!(style.faceRoughness >= 0.0 && style.faceRoughness <= 0.5)) { | |
| 383 | 1 | throw std::invalid_argument("Boulder style faceRoughness must be in [0, 0.5]"); | |
| 384 | } | ||
| 385 | 43518 | if (!(style.noiseAmplitude >= 0.0 && style.noiseAmplitude < 1.0 && | |
| 386 | 21759 | IsPositive(style.noiseWavelength) && style.noiseOctaves > 0 && | |
| 387 | 21757 | style.noiseOctaves <= 12)) { | |
| 388 | throw std::invalid_argument("Boulder style noise needs an amplitude in [0, 1), a " | ||
| 389 | 2 | "positive wavelength and 1 to 12 octaves"); | |
| 390 | } | ||
| 391 | } | ||
| 392 | 3773 | if (!(total > 0.0)) { | |
| 393 | 1 | throw std::invalid_argument("Boulder style weights must not all be zero"); | |
| 394 | } | ||
| 395 | 3772 | RequireNonNegative(options.weathering, "Boulder weathering"); | |
| 396 | 3771 | if (options.subdivisions < 0 || options.subdivisions > 6) { | |
| 397 | 1 | throw std::invalid_argument("Boulder subdivisions must be between 0 and 6"); | |
| 398 | } | ||
| 399 | 3947 | for (const auto& rock : options.library) { | |
| 400 | 178 | if (rock == nullptr) { | |
| 401 | 1 | throw std::invalid_argument("Boulder shape library meshes must not be null"); | |
| 402 | } | ||
| 403 | 177 | CheckRock(rock->mesh); | |
| 404 | } | ||
| 405 | 3769 | } | |
| 406 | |||
| 407 | 94 | BoulderShape NormalizedBoulderShape(const SceneMesh& rock) { | |
| 408 | 94 | const Vec3d extents = CheckRock(rock.mesh); | |
| 409 | 93 | const Box3d box = Bounds(rock.mesh); | |
| 410 | 93 | const Vec3d centre = (box.min + box.max) * 0.5; | |
| 411 | 93 | const bool turned = extents.y > extents.x; | |
| 412 | 93 | const double length = std::fmax(extents.x, extents.y); | |
| 413 | 93 | BoulderShape shape; | |
| 414 | 93 | shape.mesh.triangles = rock.mesh.triangles; | |
| 415 | 687 | for (const Vec3d& p : rock.mesh.positions) { | |
| 416 | 594 | const Vec3d q = (p - centre) * (1.0 / length); | |
| 417 | 594 | shape.mesh.positions.push_back(turned ? Vec3d{q.y, -q.x, q.z} : q); | |
| 418 | } | ||
| 419 | 93 | shape.intermediate = std::fmin(extents.x, extents.y) / length; | |
| 420 | 93 | shape.shortAxis = extents.z / length; | |
| 421 | 93 | shape.material = rock.material; | |
| 422 | 186 | return shape; | |
| 423 | } | ||
| 424 | |||
| 425 | 3411 | BoulderShape GenerateBoulderShape(std::uint64_t shapeSeed, const BoulderShapeOptions& options) { | |
| 426 | 3411 | ValidateBoulderShapeOptions(options); | |
| 427 | 3410 | RandomStream random(options.seed, kBoulderStream, static_cast<std::uint32_t>(shapeSeed), | |
| 428 | 3410 | static_cast<std::uint32_t>(shapeSeed >> 32)); | |
| 429 | 3410 | if (!options.library.empty()) { | |
| 430 | 91 | const std::size_t index = random.NextU32() % options.library.size(); | |
| 431 | 91 | BoulderShape shape = NormalizedBoulderShape(*options.library[index]); | |
| 432 | 91 | shape.style = index; | |
| 433 | 91 | return shape; | |
| 434 | 91 | } | |
| 435 | 3319 | const std::size_t index = PickStyle(random, options.styles); | |
| 436 | 3319 | const BoulderStyle& style = options.styles[index]; | |
| 437 | 3319 | const double intermediate = Draw(random, style.intermediate); | |
| 438 | 3319 | const double shortAxis = std::fmin(Draw(random, style.shortAxis), intermediate); | |
| 439 | 3319 | const double rounding = Draw(random, style.rounding); | |
| 440 | 3319 | const std::uint32_t noiseSeed = random.NextU32(); | |
| 441 | const double softness = | ||
| 442 | 3319 | style.edgeSoftness.max > 0.0 ? Draw(random, style.edgeSoftness) * options.weathering : 0.0; | |
| 443 | 3319 | const Vec3d axes{1.0, intermediate, shortAxis}; | |
| 444 | |||
| 445 | 3319 | const TriangleMesh solid = BaseSolidMesh(style.solid); | |
| 446 | TriangleMesh mesh = | ||
| 447 | 3319 | Subdivide(solid, options.subdivisions, style.solid == BaseSolid::Icosahedron); | |
| 448 | 722533 | for (Vec3d& p : mesh.positions) { | |
| 449 | 719214 | p = ShapeVertex(p, axes, rounding, style, noiseSeed); | |
| 450 | } | ||
| 451 | 3319 | const bool soft = softness > 0.0 || style.faceRoughness > 0.0; | |
| 452 | 1312 | const std::vector<Fracture> faces = soft && style.solid != BaseSolid::Icosahedron | |
| 453 | 3319 | ? SolidFaces(solid, axes) | |
| 454 | 3319 | : std::vector<Fracture>{}; | |
| 455 | 3319 | ApplyFractures(mesh, solid.positions.size(), style, random, softness, noiseSeed ^ 0x43555453u, | |
| 456 | faces, noiseSeed); | ||
| 457 | |||
| 458 | 3319 | const Vec3d extents = Bounds(mesh).Extents(); | |
| 459 | 3319 | mesh = Scaled(mesh, {1.0 / extents.x, intermediate / extents.y, shortAxis / extents.z}); | |
| 460 | 6638 | return {std::move(mesh), index, intermediate, shortAxis}; | |
| 461 | 9957 | } | |
| 462 | |||
| 463 | 17044 | std::uint64_t BoulderShapeSeed(std::uint64_t id, std::uint32_t prototypes) { | |
| 464 | 17044 | if (prototypes == 0) { | |
| 465 | 7 | return id; | |
| 466 | } | ||
| 467 | 17037 | const UVec3 hash = Pcg3dHash( | |
| 468 | 17037 | {static_cast<std::uint32_t>(id), static_cast<std::uint32_t>(id >> 32), 0x50524F54u}); | |
| 469 | 17037 | return hash.x % prototypes; | |
| 470 | } | ||
| 471 | |||
| 472 | } // namespace terraingen | ||
| 473 |