TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
215 / 217
99.1%
Branches:
195 / 236
82.6%
Functions:
17 / 17
100.0%

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
6/6
✓ Branch 0 taken 108818 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 108817 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 108815 times.
108819 if (!(range.min >= low && range.min <= range.max && range.max <= high)) {
23
4/4
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
✓ Branch 7 taken 4 times.
✓ Branch 10 taken 4 times.
8 throw std::invalid_argument(std::string("Boulder style ") + what + " must satisfy " +
24
2/2
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
16 std::to_string(low) +
25
3/3
✓ Branch 2 taken 4 times.
✓ Branch 5 taken 4 times.
✓ Branch 8 taken 4 times.
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
2/2
✓ Branch 0 taken 7630206 times.
✓ Branch 1 taken 1924039 times.
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
1/1
✓ Branch 1 taken 271 times.
271 const Vec3d extents = Bounds(mesh).Extents();
66
4/6
✓ Branch 0 taken 271 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 271 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 270 times.
271 if (!(extents.x > 0.0 && extents.y > 0.0 && extents.z > 0.0)) {
67 throw std::invalid_argument(
68
1/1
✓ Branch 2 taken 1 time.
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
2/2
✓ Branch 4 taken 17671 times.
✓ Branch 5 taken 3319 times.
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
4/4
✓ Branch 1 taken 7360 times.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 4082 times.
✓ Branch 4 taken 3319 times.
14761 while (index + 1 < styles.size() &&
81
3/4
✓ Branch 1 taken 7360 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 4082 times.
✓ Branch 5 taken 3278 times.
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
1/1
✓ Branch 1 taken 719214 times.
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
2/2
✓ Branch 5 taken 3644 times.
✓ Branch 6 taken 458 times.
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
1/1
✓ Branch 3 taken 3644 times.
3644 const bool seen = std::any_of(faces.begin(), faces.end(), [&](const Fracture& f) {
128
2/2
✓ Branch 1 taken 1293 times.
✓ Branch 2 taken 8051 times.
10637 return Dot(f.normal, face.normal) > 1.0 - 1e-9 &&
129
1/2
✓ Branch 0 taken 1293 times.
✗ Branch 1 not taken.
10637 std::fabs(f.offset - face.offset) < 1e-9;
130 });
131
2/2
✓ Branch 0 taken 2351 times.
✓ Branch 1 taken 1293 times.
3644 if (!seen) {
132
1/1
✓ Branch 1 taken 2351 times.
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
2/2
✓ Branch 0 taken 1309 times.
✓ Branch 1 taken 3 times.
1312 const double blend = softness > 0.0 ? 4.0 * softness : 0.0;
148
2/2
✓ Branch 5 taken 653264 times.
✓ Branch 6 taken 1312 times.
654576 for (Vec3d& p : mesh.positions) {
149 653264 const double radius = Length(p);
150
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 653264 times.
653264 if (!(radius > 0.0)) {
151 ✗ continue;
152 }
153 653264 const Vec3d ray = p * (1.0 / radius);
154
2/2
✓ Branch 0 taken 112886 times.
✓ Branch 1 taken 540378 times.
653264 double reach = polyhedral ? std::numeric_limits<double>::infinity() : radius;
155
2/2
✓ Branch 1 taken 4607722 times.
✓ Branch 2 taken 653264 times.
5260986 for (std::size_t i = 0; i < fractures.size(); ++i) {
156 4607722 const double along = Dot(fractures[i].normal, ray);
157
2/2
✓ Branch 0 taken 2313565 times.
✓ Branch 1 taken 2294157 times.
4607722 if (!(along > 0.0)) {
158 2313565 continue;
159 }
160 2294157 double offset = fractures[i].offset;
161
4/4
✓ Branch 1 taken 2181271 times.
✓ Branch 2 taken 112886 times.
✓ Branch 3 taken 1202218 times.
✓ Branch 4 taken 1091939 times.
4475428 if (std::isfinite(reach) &&
162
2/2
✓ Branch 0 taken 1202218 times.
✓ Branch 1 taken 979053 times.
2181271 offset * (1.0 - style.faceRoughness) / along >= (reach + blend) * (1.0 + 1e-9)) {
163 1202218 continue;
164 }
165
1/2
✓ Branch 0 taken 1091939 times.
✗ Branch 1 not taken.
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
1/1
✓ Branch 1 taken 1091939 times.
1091939 SurfaceRoughness(at, style, seed + static_cast<std::uint32_t>(i));
172 }
173 1091939 const double cut = offset / along;
174
4/4
✓ Branch 0 taken 1089374 times.
✓ Branch 1 taken 2565 times.
✓ Branch 3 taken 977132 times.
✓ Branch 4 taken 112242 times.
1206746 reach = softness > 0.0 && std::isfinite(reach) ? SmoothMin(reach, cut, blend)
175 114807 : std::fmin(reach, cut);
176 }
177
2/2
✓ Branch 0 taken 112886 times.
✓ Branch 1 taken 540378 times.
653264 if (polyhedral) {
178
1/1
✓ Branch 1 taken 112886 times.
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
1/1
✓ Branch 1 taken 3319 times.
3319 style.minFractures + std::min(span - 1, static_cast<int>(random.NextDouble() * span));
190
4/4
✓ Branch 0 taken 2010 times.
✓ Branch 1 taken 1309 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 2007 times.
3319 if (softness > 0.0 || style.faceRoughness > 0.0) {
191
1/1
✓ Branch 1 taken 1312 times.
1312 std::vector<Fracture> planes = faces;
192
2/2
✓ Branch 0 taken 7280 times.
✓ Branch 1 taken 1312 times.
8592 for (int n = 0; n < fractures; ++n) {
193
1/1
✓ Branch 1 taken 7280 times.
7280 const Vec3d normal = DrawNormal(random, style.maxFractureTilt);
194
1/1
✓ Branch 1 taken 7280 times.
7280 const double depth = Draw(random, style.fractureDepth);
195 7280 double reach = 0.0;
196
2/2
✓ Branch 0 taken 77985 times.
✓ Branch 1 taken 7280 times.
85265 for (std::size_t v = 0; v < corners; ++v) {
197 77985 reach = std::fmax(reach, Dot(normal, mesh.positions[v]));
198 }
199
1/2
✓ Branch 0 taken 7280 times.
✗ Branch 1 not taken.
7280 if (depth * reach > 0.0) {
200
1/1
✓ Branch 1 taken 7280 times.
7280 planes.push_back({normal, depth * reach});
201 }
202 }
203
1/1
✓ Branch 2 taken 1312 times.
1312 CutSoftly(mesh, planes, style, softness, seed, !faces.empty(), noiseSeed);
204 1312 return;
205 1312 }
206
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2007 times.
2010 for (int n = 0; n < fractures; ++n) {
207
1/1
✓ Branch 1 taken 3 times.
3 const Vec3d normal = DrawNormal(random, style.maxFractureTilt);
208
1/1
✓ Branch 1 taken 3 times.
3 const double depth = Draw(random, style.fractureDepth);
209 3 double reach = 0.0;
210
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 3 times.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!(plane > 0.0)) {
215 ✗ continue;
216 }
217
2/2
✓ Branch 5 taken 1414 times.
✓ Branch 6 taken 3 times.
1417 for (Vec3d& p : mesh.positions) {
218 1414 const double along = Dot(normal, p);
219
2/2
✓ Branch 0 taken 248 times.
✓ Branch 1 taken 1166 times.
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
3/5
✓ Branch 1 taken 438 times.
✓ Branch 3 taken 3942 times.
✓ Branch 4 taken 438 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
5256 };
361
9/31
✓ Branch 1 taken 438 times.
✓ Branch 4 taken 438 times.
✓ Branch 7 taken 438 times.
✓ Branch 10 taken 438 times.
✓ Branch 13 taken 438 times.
✓ Branch 16 taken 438 times.
✓ Branch 19 taken 438 times.
✓ Branch 22 taken 438 times.
✓ Branch 25 taken 438 times.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✗ Branch 36 not taken.
✗ Branch 37 not taken.
✗ Branch 39 not taken.
✗ Branch 40 not taken.
✗ Branch 42 not taken.
✗ Branch 43 not taken.
✗ Branch 45 not taken.
✗ Branch 46 not taken.
✗ Branch 48 not taken.
✗ Branch 49 not taken.
✗ Branch 51 not taken.
✗ Branch 52 not taken.
✗ Branch 54 not taken.
✗ Branch 55 not taken.
✗ Branch 56 not taken.
✗ Branch 57 not taken.
1314 }
362
363 3785 void ValidateBoulderShapeOptions(const BoulderShapeOptions& options) {
364
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3783 times.
3785 if (options.styles.empty()) {
365
1/1
✓ Branch 2 taken 2 times.
2 throw std::invalid_argument("Boulder shapes need at least one style");
366 }
367 3783 double total = 0.0;
368
2/2
✓ Branch 5 taken 21767 times.
✓ Branch 6 taken 3773 times.
25540 for (const BoulderStyle& style : options.styles) {
369
1/1
✓ Branch 2 taken 21766 times.
21767 RequireNonNegative(style.weight, "Boulder style weights");
370 21766 total += style.weight;
371
1/1
✓ Branch 1 taken 21765 times.
21766 CheckRange(style.intermediate, 1e-3, 1.0, "intermediate");
372
1/1
✓ Branch 1 taken 21764 times.
21765 CheckRange(style.shortAxis, 1e-3, 1.0, "shortAxis");
373
1/1
✓ Branch 1 taken 21763 times.
21764 CheckRange(style.rounding, 0.0, 1.0, "rounding");
374
1/1
✓ Branch 1 taken 21763 times.
21763 CheckRange(style.fractureDepth, 1e-3, 1.0, "fractureDepth");
375
3/4
✓ Branch 0 taken 21763 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21762 times.
✓ Branch 3 taken 1 time.
21763 if (!(style.minFractures >= 0 && style.minFractures <= style.maxFractures &&
376
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21762 times.
21762 style.maxFractures <= 64)) {
377 throw std::invalid_argument(
378
1/1
✓ Branch 2 taken 1 time.
1 "Boulder style fractures must satisfy 0 <= min <= max <= 64");
379 }
380
1/1
✓ Branch 2 taken 21761 times.
21762 RequireUnitInterval(style.maxFractureTilt, "Boulder style maxFractureTilt");
381
1/1
✓ Branch 1 taken 21760 times.
21761 CheckRange(style.edgeSoftness, 0.0, 0.5, "edgeSoftness");
382
3/4
✓ Branch 0 taken 21760 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 21759 times.
21760 if (!(style.faceRoughness >= 0.0 && style.faceRoughness <= 0.5)) {
383
1/1
✓ Branch 2 taken 1 time.
1 throw std::invalid_argument("Boulder style faceRoughness must be in [0, 0.5]");
384 }
385
4/6
✓ Branch 0 taken 21759 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21759 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 21757 times.
43518 if (!(style.noiseAmplitude >= 0.0 && style.noiseAmplitude < 1.0 &&
386
4/4
✓ Branch 1 taken 21758 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 21757 times.
✓ Branch 4 taken 1 time.
21759 IsPositive(style.noiseWavelength) && style.noiseOctaves > 0 &&
387
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21757 times.
21757 style.noiseOctaves <= 12)) {
388 throw std::invalid_argument("Boulder style noise needs an amplitude in [0, 1), a "
389
1/1
✓ Branch 2 taken 2 times.
2 "positive wavelength and 1 to 12 octaves");
390 }
391 }
392
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3772 times.
3773 if (!(total > 0.0)) {
393
1/1
✓ Branch 2 taken 1 time.
1 throw std::invalid_argument("Boulder style weights must not all be zero");
394 }
395
1/1
✓ Branch 2 taken 3771 times.
3772 RequireNonNegative(options.weathering, "Boulder weathering");
396
3/4
✓ Branch 0 taken 3771 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 3770 times.
3771 if (options.subdivisions < 0 || options.subdivisions > 6) {
397
1/1
✓ Branch 2 taken 1 time.
1 throw std::invalid_argument("Boulder subdivisions must be between 0 and 6");
398 }
399
2/2
✓ Branch 4 taken 178 times.
✓ Branch 5 taken 3769 times.
3947 for (const auto& rock : options.library) {
400
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 177 times.
178 if (rock == nullptr) {
401
1/1
✓ Branch 2 taken 1 time.
1 throw std::invalid_argument("Boulder shape library meshes must not be null");
402 }
403
1/1
✓ Branch 2 taken 177 times.
177 CheckRock(rock->mesh);
404 }
405 3769 }
406
407 94 BoulderShape NormalizedBoulderShape(const SceneMesh& rock) {
408
1/1
✓ Branch 1 taken 93 times.
94 const Vec3d extents = CheckRock(rock.mesh);
409
1/1
✓ Branch 1 taken 93 times.
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
1/1
✓ Branch 1 taken 93 times.
93 BoulderShape shape;
414
1/1
✓ Branch 1 taken 93 times.
93 shape.mesh.triangles = rock.mesh.triangles;
415
2/2
✓ Branch 4 taken 594 times.
✓ Branch 5 taken 93 times.
687 for (const Vec3d& p : rock.mesh.positions) {
416 594 const Vec3d q = (p - centre) * (1.0 / length);
417
3/3
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 586 times.
✓ Branch 3 taken 594 times.
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
1/1
✓ Branch 1 taken 93 times.
93 shape.material = rock.material;
422 186 return shape;
423 }
424
425 3411 BoulderShape GenerateBoulderShape(std::uint64_t shapeSeed, const BoulderShapeOptions& options) {
426
1/1
✓ Branch 1 taken 3410 times.
3411 ValidateBoulderShapeOptions(options);
427 3410 RandomStream random(options.seed, kBoulderStream, static_cast<std::uint32_t>(shapeSeed),
428
1/1
✓ Branch 1 taken 3410 times.
3410 static_cast<std::uint32_t>(shapeSeed >> 32));
429
2/2
✓ Branch 1 taken 91 times.
✓ Branch 2 taken 3319 times.
3410 if (!options.library.empty()) {
430
1/1
✓ Branch 1 taken 91 times.
91 const std::size_t index = random.NextU32() % options.library.size();
431
1/1
✓ Branch 3 taken 91 times.
91 BoulderShape shape = NormalizedBoulderShape(*options.library[index]);
432 91 shape.style = index;
433 91 return shape;
434 91 }
435
1/1
✓ Branch 1 taken 3319 times.
3319 const std::size_t index = PickStyle(random, options.styles);
436 3319 const BoulderStyle& style = options.styles[index];
437
1/1
✓ Branch 1 taken 3319 times.
3319 const double intermediate = Draw(random, style.intermediate);
438
1/1
✓ Branch 1 taken 3319 times.
3319 const double shortAxis = std::fmin(Draw(random, style.shortAxis), intermediate);
439
1/1
✓ Branch 1 taken 3319 times.
3319 const double rounding = Draw(random, style.rounding);
440
1/1
✓ Branch 1 taken 3319 times.
3319 const std::uint32_t noiseSeed = random.NextU32();
441 const double softness =
442
3/3
✓ Branch 0 taken 1312 times.
✓ Branch 1 taken 2007 times.
✓ Branch 3 taken 1312 times.
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
1/1
✓ Branch 1 taken 3319 times.
3319 const TriangleMesh solid = BaseSolidMesh(style.solid);
446 TriangleMesh mesh =
447
1/1
✓ Branch 1 taken 3319 times.
3319 Subdivide(solid, options.subdivisions, style.solid == BaseSolid::Icosahedron);
448
2/2
✓ Branch 5 taken 719214 times.
✓ Branch 6 taken 3319 times.
722533 for (Vec3d& p : mesh.positions) {
449
1/1
✓ Branch 1 taken 719214 times.
719214 p = ShapeVertex(p, axes, rounding, style, noiseSeed);
450 }
451
4/4
✓ Branch 0 taken 2010 times.
✓ Branch 1 taken 1309 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 2007 times.
3319 const bool soft = softness > 0.0 || style.faceRoughness > 0.0;
452
2/2
✓ Branch 0 taken 458 times.
✓ Branch 1 taken 854 times.
1312 const std::vector<Fracture> faces = soft && style.solid != BaseSolid::Icosahedron
453
2/2
✓ Branch 0 taken 1312 times.
✓ Branch 1 taken 2007 times.
3319 ? SolidFaces(solid, axes)
454
1/1
✓ Branch 1 taken 458 times.
3319 : std::vector<Fracture>{};
455
1/1
✓ Branch 2 taken 3319 times.
3319 ApplyFractures(mesh, solid.positions.size(), style, random, softness, noiseSeed ^ 0x43555453u,
456 faces, noiseSeed);
457
458
1/1
✓ Branch 1 taken 3319 times.
3319 const Vec3d extents = Bounds(mesh).Extents();
459
1/1
✓ Branch 1 taken 3319 times.
3319 mesh = Scaled(mesh, {1.0 / extents.x, intermediate / extents.y, shortAxis / extents.z});
460 6638 return {std::move(mesh), index, intermediate, shortAxis};
461
1/3
✓ Branch 3 taken 3319 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
9957 }
462
463 17044 std::uint64_t BoulderShapeSeed(std::uint64_t id, std::uint32_t prototypes) {
464
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 17037 times.
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