Toast Engine
Loading...
Searching...
No Matches
fracture.hpp
Go to the documentation of this file.
1
7
8#pragma once
9
10#include "fracture_pattern.hpp"
11#include "fracture_profile.hpp"
12#include "fracture_random.hpp"
13#include "hardness.hpp"
14#include "physics_settings.hpp"
15
16#include <algorithm>
17#include <array>
18#include <cmath>
19#include <cstdint>
20#include <glm/glm.hpp>
21#include <limits>
22#include <numbers>
23#include <optional>
24#include <span>
28#include <unordered_set>
29#include <vector>
30
31namespace physics {
32
33inline constexpr float k_voxel_volume = voxel::k_voxel_size * voxel::k_voxel_size * voxel::k_voxel_size;
34
35inline constexpr float k_joules_per_kilojoule = 1000.0f; // we use kJ to not have values in the millions
36
37[[nodiscard]]
38constexpr auto voxelBreakEnergy(const voxel::PhysicalMaterial& material) -> float {
39 return material.toughness * k_voxel_volume;
40}
41
43 glm::vec3 point {};
44 glm::vec3 direction {};
45 float radius = 0.0f;
46 float energy_density = 0.0f;
47 Hardness max_hardness = k_hardest;
48 uint64_t seed = 0;
49 float depth_share = tunables().fracture_depth_share;
50 float max_depth = tunables().fracture_max_depth;
51 float max_overshoot = tunables().fracture_max_overshoot;
52 float bond_cost = tunables().fracture_bond_cost;
53 uint32_t max_unglue_voxels = tunables().fracture_max_unglue_voxels;
54};
55
56inline constexpr uint32_t k_unglue_neighbor_count = 6;
57
58[[nodiscard]]
59constexpr auto voxelKey(const glm::ivec3& v) -> uint64_t {
60 return (static_cast<uint64_t>(v.x) << 42) | (static_cast<uint64_t>(v.y) << 21) | static_cast<uint64_t>(v.z);
61}
62
64 std::unordered_set<uint64_t> keys;
65 glm::vec3 low {std::numeric_limits<float>::max()};
66 glm::vec3 high {std::numeric_limits<float>::lowest()};
67 float cost = 0.0f;
68
69 [[nodiscard]]
70 auto empty() const -> bool {
71 return keys.empty();
72 }
73};
74
75struct Crater {
76 glm::vec3 start {};
77 glm::vec3 end {};
78 float radius = 0.0f;
79 bool breaks = false;
80 UnglueRegion unglue;
81 std::unordered_set<uint64_t> reachable;
82 uint64_t seed = 0;
83 float edge_noise = 0.0f;
84 float edge_noise_scale = 0.0f;
85 float edge_lobes = 0.0f;
86 FracturePattern pattern = FracturePattern::brittle;
87 FractureValues profile;
88 float toughness = 0.0f;
89 float fragment_energy = 0.0f;
90 float shatter_energy = 0.0f;
91 float debris_energy = 0.0f;
92};
93
94inline constexpr float k_max_edge_noise = 0.9f;
95inline constexpr float k_min_radius_factor = 0.15f;
96
97[[nodiscard]]
98inline auto craterRadiusAt(const Crater& crater, const glm::vec3& point) -> float {
99 float factor = 1.0f;
100 if (crater.edge_lobes > 0.0f && crater.radius > 0.0f) {
101 const float lobes = valueNoise3(streamSeed(crater.seed, FractureStream::edge_lobes), point / crater.radius);
102 factor += crater.edge_lobes * ((lobes * 2.0f) - 1.0f);
103 }
104 if (crater.edge_noise > 0.0f && crater.edge_noise_scale > 0.0f) {
105 const float noise = valueNoise3(streamSeed(crater.seed, FractureStream::edge_noise), point / crater.edge_noise_scale);
106 factor += crater.edge_noise * ((noise * 2.0f) - 1.0f);
107 }
108 return crater.radius * std::max(factor, k_min_radius_factor);
109}
110
111[[nodiscard]]
112constexpr auto craterMaxRadius(const Crater& crater) -> float {
113 return crater.radius * (1.0f + std::max(crater.edge_noise, 0.0f) + std::max(crater.edge_lobes, 0.0f));
114}
115
116[[nodiscard]]
117constexpr auto craterMinRadius(const Crater& crater) -> float {
118 return crater.radius *
119 std::max(1.0f - std::max(crater.edge_noise, 0.0f) - std::max(crater.edge_lobes, 0.0f), k_min_radius_factor);
120}
121
123 float depth = 0.0f;
124 float distance = 0.0f;
125 float cost = 0.0f;
126 bool breakable = false;
127 bool on_axis = false;
128};
129
131 float depth = 0.0f;
132 float spent = 0.0f;
133 uint32_t voxels = 0;
134};
135
136template<typename Visit>
137void forEachSolidVoxel(const voxel::Volume& volume, const glm::vec3& low, const glm::vec3& high, Visit&& visit) {
138 const glm::ivec3 voxel_dims = glm::ivec3(volume.voxelDims());
139 glm::ivec3 first = glm::ivec3(glm::floor(low / voxel::k_voxel_size));
140 glm::ivec3 last = glm::ivec3(glm::ceil(high / voxel::k_voxel_size)) - 1;
141 first = glm::clamp(first, glm::ivec3(0), voxel_dims - 1);
142 last = glm::clamp(last, glm::ivec3(0), voxel_dims - 1);
143 if (first.x > last.x || first.y > last.y || first.z > last.z) {
144 return;
145 }
146
147 // yeah this is O(n^6) 🥀
148 const auto brick_dim = static_cast<int32_t>(voxel::k_brick_dim);
149 const glm::ivec3 first_brick = first / brick_dim;
150 const glm::ivec3 last_brick = last / brick_dim;
151 for (int32_t bz = first_brick.z; bz <= last_brick.z; ++bz) {
152 for (int32_t by = first_brick.y; by <= last_brick.y; ++by) {
153 for (int32_t bx = first_brick.x; bx <= last_brick.x; ++bx) {
154 const glm::ivec3 brick {bx, by, bz};
155 if (volume.entryAt(brick).tag() == voxel::BrickTag::empty) {
156 continue;
157 }
158 const glm::ivec3 brick_first = glm::max(first, brick * brick_dim);
159 const glm::ivec3 brick_last = glm::min(last, (brick * brick_dim) + (brick_dim - 1));
160 for (int32_t z = brick_first.z; z <= brick_last.z; ++z) {
161 for (int32_t y = brick_first.y; y <= brick_last.y; ++y) {
162 for (int32_t x = brick_first.x; x <= brick_last.x; ++x) {
163 const glm::ivec3 v {x, y, z};
164 const uint8_t palette_index = volume.materialAt(v);
165 if (palette_index == voxel::k_empty_palette_index) {
166 continue;
167 }
168 visit(v, (glm::vec3 {v} + 0.5f) * voxel::k_voxel_size, palette_index);
169 }
170 }
171 }
172 }
173 }
174 }
175}
176
177[[nodiscard]]
178inline auto pointSegmentDistanceSquared(const glm::vec3& point, const glm::vec3& start, const glm::vec3& end) -> float {
179 const glm::vec3 axis = end - start;
180 const float axis_squared = glm::dot(axis, axis);
181 const float along = axis_squared > 0.0f ? glm::clamp(glm::dot(point - start, axis) / axis_squared, 0.0f, 1.0f) : 0.0f;
182 const glm::vec3 gap = point - (start + (axis * along));
183 return glm::dot(gap, gap);
184}
185
186[[nodiscard]]
187inline auto closestPointOnSegment(const glm::vec3& point, const glm::vec3& start, const glm::vec3& end) -> glm::vec3 {
188 const glm::vec3 axis = end - start;
189 const float axis_squared = glm::dot(axis, axis);
190 const float along = axis_squared > 0.0f ? glm::clamp(glm::dot(point - start, axis) / axis_squared, 0.0f, 1.0f) : 0.0f;
191 return start + (axis * along);
192}
193
194[[nodiscard]]
195inline auto reachesThroughMaterial(const voxel::Volume& volume, const glm::vec3& from, const glm::vec3& to) -> bool {
196 // damage travels through material
197 const glm::ivec3 dims = glm::ivec3(volume.voxelDims());
198 const float max_air = 1.5f * voxel::k_voxel_size;
199 const glm::vec3 span = to - from;
200 const float length = glm::length(span);
201 const auto steps = static_cast<int32_t>(std::ceil(length / (0.5f * voxel::k_voxel_size)));
202 if (steps <= 0) {
203 return true;
204 }
205
206 const float step_length = length / static_cast<float>(steps);
207 float air = 0.0f;
208 for (int32_t i = 1; i < steps; ++i) {
209 const glm::vec3 sample = from + (span * (static_cast<float>(i) / static_cast<float>(steps)));
210 const glm::ivec3 v = glm::ivec3(glm::floor(sample / voxel::k_voxel_size));
211 const bool solid = glm::all(glm::greaterThanEqual(v, glm::ivec3(0))) && glm::all(glm::lessThan(v, dims)) &&
212 volume.materialAt(v) != voxel::k_empty_palette_index;
213 if (not solid) {
214 air += step_length;
215 if (air > max_air) {
216 return false;
217 }
218 }
219 }
220 return true;
221}
222
223[[nodiscard]]
224inline auto reachableMaterial(const voxel::Volume& volume, const glm::vec3& start, const glm::vec3& end, float radius)
225 -> std::unordered_set<uint64_t> {
226 std::unordered_set<uint64_t> out;
227
228 // voxel closest to the hit
229 std::optional<glm::ivec3> seed;
230 float closest = radius * radius;
231 forEachSolidVoxel(volume, start - glm::vec3(radius), start + glm::vec3(radius), [&](glm::ivec3 v, glm::vec3 center, uint8_t) {
232 const glm::vec3 offset = center - start;
233 const float distance_squared = glm::dot(offset, offset);
234 if (distance_squared <= closest) {
235 closest = distance_squared;
236 seed = v;
237 }
238 });
239 if (not seed.has_value()) {
240 return out;
241 }
242
243 const glm::ivec3 dims = glm::ivec3(volume.voxelDims());
244 std::vector<glm::ivec3> stack {*seed};
245 out.insert(voxelKey(*seed));
246 while (not stack.empty()) {
247 const glm::ivec3 v = stack.back();
248 stack.pop_back();
249 for (int32_t z = -1; z <= 1; ++z) {
250 for (int32_t y = -1; y <= 1; ++y) {
251 for (int32_t x = -1; x <= 1; ++x) {
252 const glm::ivec3 next = v + glm::ivec3 {x, y, z};
253 if (next == v || glm::any(glm::lessThan(next, glm::ivec3(0))) || glm::any(glm::greaterThanEqual(next, dims)) ||
254 volume.materialAt(next) == voxel::k_empty_palette_index) {
255 continue;
256 }
257 const glm::vec3 center = (glm::vec3 {next} + 0.5f) * voxel::k_voxel_size;
258 if (pointSegmentDistanceSquared(center, start, end) > radius * radius) {
259 continue;
260 }
261 if (out.insert(voxelKey(next)).second) {
262 stack.push_back(next);
263 }
264 }
265 }
266 }
267 }
268 return out;
269}
270
271[[nodiscard]]
272inline auto damageReaches(
273 const std::unordered_set<uint64_t>& reachable, const voxel::Volume& volume, const glm::ivec3& voxel, const glm::vec3& from,
274 const glm::vec3& center
275) -> bool {
276 return reachable.contains(voxelKey(voxel)) || reachesThroughMaterial(volume, from, center);
277}
278
279[[nodiscard]]
280constexpr auto hitBreaks(const voxel::PhysicalMaterial& material, float energy_density, Hardness max_hardness) -> bool {
281 return material.breakableBy(max_hardness) && material.toughness <= energy_density;
282}
283
284[[nodiscard]]
285inline auto hitMaterial(
286 const voxel::Volume& volume, const voxel::Palette& palette, const voxel::MaterialLibrary& materials, const glm::vec3& point,
287 float reach
288) -> std::optional<voxel::PhysicalMaterial> {
289 std::optional<voxel::PhysicalMaterial> out;
290 if (materials.materials.empty()) {
291 return out;
292 }
293 reach = std::max(reach, voxel::k_voxel_size);
294 float best = reach * reach;
295 forEachSolidVoxel(volume, point - glm::vec3(reach), point + glm::vec3(reach), [&](glm::ivec3, glm::vec3 center, uint8_t index) {
296 const glm::vec3 offset = center - point;
297 const float distance_squared = glm::dot(offset, offset);
298 if (distance_squared <= best) {
299 best = distance_squared;
300 out = materials.materials[voxel::resolveMaterialIndex(palette, materials, index)];
301 }
302 });
303 return out;
304}
305
306[[nodiscard]]
307inline auto fractureCandidates(
308 const voxel::Volume& volume, const voxel::Palette& palette, const voxel::MaterialLibrary& materials,
309 const FractureRequest& request, const std::unordered_set<uint64_t>& reachable
310) -> std::vector<FractureCandidate> {
311 std::vector<FractureCandidate> out;
312 if (materials.materials.empty() || not(request.radius > 0.0f)) {
313 return out;
314 }
315
316 const float radius = request.radius;
317 const bool digs = glm::dot(request.direction, request.direction) > 0.0f && request.max_depth > 0.0f;
318 const glm::vec3 far = request.point + (digs ? request.direction * request.max_depth : glm::vec3 {0.0f});
319 const glm::vec3 low = glm::min(request.point, far) - glm::vec3(radius);
320 const glm::vec3 high = glm::max(request.point, far) + glm::vec3(radius);
321 const float axis_reach = 0.5f * std::sqrt(3.0f) * voxel::k_voxel_size; // half the diagonal of a voxel
322
323 forEachSolidVoxel(volume, low, high, [&](glm::ivec3 voxel, glm::vec3 center, uint8_t index) {
324 const glm::vec3 offset = center - request.point;
325 const float distance = glm::length(offset);
326 const voxel::PhysicalMaterial& material = materials.materials[voxel::resolveMaterialIndex(palette, materials, index)];
327 FractureCandidate candidate {
328 .distance = distance,
329 .cost = voxelBreakEnergy(material),
330 .breakable = material.breakableBy(request.max_hardness),
331 };
332
333 if (distance <= radius) {
334 if (damageReaches(reachable, volume, voxel, request.point, center)) {
335 out.push_back(candidate);
336 }
337 return;
338 }
339 if (not digs) {
340 return;
341 }
342
343 // capsule from p to p + dir * depth holds this voxel
344 const float along = glm::dot(offset, request.direction);
345 const glm::vec3 lateral_offset = offset - (request.direction * along);
346 const float lateral_squared = glm::dot(lateral_offset, lateral_offset);
347 if (along <= 0.0f || lateral_squared > radius * radius) {
348 return;
349 }
350 candidate.depth = along - std::sqrt((radius * radius) - lateral_squared);
351 if (candidate.depth > request.max_depth) {
352 return;
353 }
354 candidate.on_axis = lateral_squared <= axis_reach * axis_reach;
355 if (damageReaches(reachable, volume, voxel, request.point + (request.direction * along), center)) {
356 out.push_back(candidate);
357 }
358 });
359 return out;
360}
361
362[[nodiscard]]
363inline auto penetrationDepth(std::span<const FractureCandidate> candidates, float energy, float max_depth) -> Penetration {
364 Penetration out;
365 // air between the hit and something else stops it
366 const float max_gap = 1.5f * voxel::k_voxel_size;
367 float reached = 0.0f;
368 for (const FractureCandidate& candidate : candidates) {
369 if (candidate.depth > max_depth || candidate.depth - reached > max_gap) {
370 break;
371 }
372 reached = std::max(reached, candidate.depth);
373 if (not candidate.breakable) {
374 if (candidate.on_axis) {
375 break;
376 }
377 continue;
378 }
379 if (out.spent + candidate.cost > energy) {
380 break;
381 }
382 out.spent += candidate.cost;
383 out.depth = candidate.depth;
384 ++out.voxels;
385 }
386 return out;
387}
388
389[[nodiscard]]
390constexpr auto capsuleVolume(float radius, float length) -> float {
391 return (std::numbers::pi_v<float> * radius * radius * length) +
392 ((4.0f / 3.0f) * std::numbers::pi_v<float> * radius * radius * radius);
393}
394
395[[nodiscard]]
396inline auto overshootRadius(float energy_left, float toughness, float radius, float length, float max_overshoot) -> float {
397 const float cap = std::max(max_overshoot, 0.0f) * radius;
398 if (not(energy_left > 0.0f) || not(toughness > 0.0f) || not(cap > 0.0f)) {
399 return 0.0f;
400 }
401
402 const float affordable = energy_left / toughness;
403 const auto capsule_volume = [length](float r) { return capsuleVolume(r, length); };
404 const float base = capsule_volume(radius);
405 if (capsule_volume(radius + cap) - base <= affordable) {
406 return cap;
407 }
408
409 float low = 0.0f;
410 float high = cap;
411 for (int i = 0; i < 24; ++i) {
412 const float middle = 0.5f * (low + high);
413 if (capsule_volume(radius + middle) - base <= affordable) {
414 low = middle;
415 } else {
416 high = middle;
417 }
418 }
419 return low;
420}
421
423 float debris = 0.0f;
424 float fragment = 0.0f;
425 float shatter = 0.0f;
426 float overshoot = 0.0f;
427};
428
429[[nodiscard]]
430inline auto splitLeftoverEnergy(float left, float debris_share, float fragment_share, FracturePattern pattern) -> EnergySplit {
431 if (not std::isfinite(left) || left <= 0.0f) {
432 return {};
433 }
434
435 auto sanitize = [](float share) { return std::isfinite(share) ? std::clamp(share, 0.0f, 1.0f) : 0.0f; };
436 float debris = sanitize(debris_share);
437 float fragment = sanitize(fragment_share);
438 if (float total = debris + fragment; total > 1.0f) {
439 debris /= total;
440 fragment /= total;
441 }
442
443 EnergySplit out {.debris = left * debris, .fragment = left * fragment};
444 const float rest = std::max(left - out.debris - out.fragment, 0.0f);
445 if (pattern == FracturePattern::ductile) {
446 out.overshoot = rest;
447 } else {
448 out.shatter = rest;
449 }
450 return out;
451}
452
453[[nodiscard]]
454inline auto planUnglue(
455 const voxel::Volume& volume, const voxel::Palette& palette, const voxel::MaterialLibrary& materials, const glm::vec3& start,
456 const glm::vec3& end, float radius, Hardness max_hardness, float energy, float bond_cost, uint32_t max_voxels
457) -> UnglueRegion;
458
459[[nodiscard]]
460inline auto planCrater(
461 const voxel::Volume& volume, const voxel::Palette& palette, const voxel::MaterialLibrary& materials,
462 const FractureRequest& request
463) -> Crater {
464 Crater out {
465 .start = request.point,
466 .end = request.point,
467 .seed = request.seed,
468 };
469 if (not std::isfinite(request.energy_density) || not(request.energy_density > 0.0f) || not(request.radius > 0.0f)) {
470 return out;
471 }
472
473 // a hit never breaks anything of if the voxel is too tough or too hard
474 const std::optional<voxel::PhysicalMaterial> hit = hitMaterial(volume, palette, materials, request.point, request.radius);
475 if (not hit.has_value() || not hit->breakableBy(request.max_hardness)) {
476 return out;
477 }
478
479 out.pattern = hit->fracture_pattern;
480 out.profile = hit->fracture_profile.unpack();
481 out.toughness = hit->toughness;
482 out.edge_noise = std::clamp(out.profile.edge_noise, 0.0f, k_max_edge_noise);
483 out.edge_noise_scale = std::max(out.profile.edge_noise_scale, 0.0f) * voxel::k_voxel_size;
484 out.edge_lobes = std::clamp(out.profile.edge_lobes, 0.0f, k_max_edge_noise);
485
486 // as far as the crater can grow
487 const bool digs = glm::dot(request.direction, request.direction) > 0.0f && request.max_depth > 0.0f;
488 const glm::vec3 far = digs ? request.point + (request.direction * request.max_depth) : request.point;
489 out.reachable = reachableMaterial(volume, request.point, far, request.radius * (1.0f + std::max(request.max_overshoot, 0.0f)));
490 std::vector<FractureCandidate> candidates = fractureCandidates(volume, palette, materials, request, out.reachable);
491 const auto deeper = std::ranges::partition(candidates, [](const FractureCandidate& c) { return c.depth <= 0.0f; });
492 const std::span<FractureCandidate> inside(candidates.begin(), deeper.begin());
493 const std::span<FractureCandidate> beyond(deeper.begin(), deeper.end());
494
495 // the hit puts its energy density into all the material inside its fracture radius
496 // we need to conserve energy so everything that doesn't get used gets propagated outwards
497 const auto breakable = static_cast<float>(std::ranges::count_if(inside, &FractureCandidate::breakable));
498 // a little slack so float sums never leave a hit exactly as tough as the material one voxel short
499 const float budget = request.energy_density * breakable * k_voxel_volume * (1.0f + 1.0e-5f);
500
501 // paid nearest first
502 std::ranges::sort(inside, {}, &FractureCandidate::distance);
503 float spent = 0.0f;
504 float paid_cost = 0.0f;
505 uint32_t paid = 0;
506 float reached = 0.0f;
507 bool short_of_energy = false;
508 for (const FractureCandidate& candidate : inside) {
509 if (not candidate.breakable) {
510 continue;
511 }
512 if (spent + candidate.cost > budget) {
513 short_of_energy = true;
514 break;
515 }
516 spent += candidate.cost;
517 paid_cost += candidate.cost;
518 ++paid;
519 reached = candidate.distance;
520 }
521 if (paid == 0) {
522 return out; // not even the nearest voxel is affordable
523 }
524 out.breaks = true;
525 if (short_of_energy) {
526 out.radius = std::max(reached, 0.5f * voxel::k_voxel_size);
527 return out;
528 }
529 out.radius = request.radius;
530
531 float left = budget - spent;
532 if (glm::dot(request.direction, request.direction) > 0.0f) {
533 std::ranges::sort(beyond, {}, &FractureCandidate::depth);
534 const Penetration dig = penetrationDepth(beyond, left * std::clamp(request.depth_share, 0.0f, 1.0f), request.max_depth);
535 out.end = request.point + (request.direction * dig.depth);
536 left -= dig.spent;
537 paid_cost += dig.spent;
538 paid += dig.voxels;
539 }
540
541 out.unglue = planUnglue(
542 volume,
543 palette,
544 materials,
545 out.start,
546 out.end,
547 request.radius,
548 request.max_hardness,
549 left,
550 request.bond_cost,
551 request.max_unglue_voxels
552 );
553 left -= out.unglue.cost;
554
555 auto split = splitLeftoverEnergy(left, out.profile.debris_share, out.profile.fragment_share, out.pattern);
556 out.debris_energy = split.debris;
557 out.fragment_energy = split.fragment;
558 out.shatter_energy = split.shatter;
559 const float toughness = paid > 0 ? paid_cost / (static_cast<float>(paid) * k_voxel_volume) : hit->toughness;
560 out.radius +=
561 overshootRadius(split.overshoot, toughness, request.radius, glm::length(out.end - out.start), request.max_overshoot);
562 return out;
563}
564
565[[nodiscard]]
566inline auto planUnglue(
567 const voxel::Volume& volume, const voxel::Palette& palette, const voxel::MaterialLibrary& materials, const glm::vec3& start,
568 const glm::vec3& end, float radius, Hardness max_hardness, float energy, float bond_cost, uint32_t max_voxels
569) -> UnglueRegion {
570 UnglueRegion out;
571 if (not(energy > 0.0f) || not(bond_cost > 0.0f) || materials.materials.empty()) {
572 return out;
573 }
574
575 const glm::ivec3 dims = glm::ivec3(volume.voxelDims());
576 const auto inside = [&](const glm::ivec3& v) {
577 return glm::all(glm::greaterThanEqual(v, glm::ivec3(0))) && glm::all(glm::lessThan(v, dims));
578 };
579 const auto material_at = [&](const glm::ivec3& v) -> const voxel::PhysicalMaterial* {
580 if (not inside(v)) {
581 return nullptr;
582 }
583 const uint8_t index = volume.materialAt(v);
584 if (index == voxel::k_empty_palette_index) {
585 return nullptr;
586 }
587 return &materials.materials[voxel::resolveMaterialIndex(palette, materials, index)];
588 };
589
590 const std::array<glm::ivec3, k_unglue_neighbor_count> k_faces {
591 glm::ivec3 { 1, 0, 0},
592 glm::ivec3 {-1, 0, 0},
593 glm::ivec3 { 0, 1, 0},
594 glm::ivec3 { 0, -1, 0},
595 glm::ivec3 { 0, 0, 1},
596 glm::ivec3 { 0, 0, -1}
597 };
598
599 // breakable voxels at the crater wall that touch something this hit cannot break
600 std::vector<glm::ivec3> frontier;
601 const float reach = radius + (1.5f * voxel::k_voxel_size);
602 const glm::vec3 low = glm::min(start, end) - glm::vec3(reach);
603 const glm::vec3 high = glm::max(start, end) + glm::vec3(reach);
604 forEachSolidVoxel(volume, low, high, [&](glm::ivec3 v, glm::vec3 center, uint8_t) {
605 if (pointSegmentDistanceSquared(center, start, end) > reach * reach) {
606 return;
607 }
608 const voxel::PhysicalMaterial* self = material_at(v);
609 if (self == nullptr || not self->breakableBy(max_hardness)) {
610 return;
611 }
612 for (const glm::ivec3& face : k_faces) {
613 const voxel::PhysicalMaterial* other = material_at(v + face);
614 if (other != nullptr && not other->breakableBy(max_hardness)) {
615 frontier.push_back(v);
616 return;
617 }
618 }
619 });
620 if (frontier.empty()) {
621 return out;
622 }
623
624 // everything breakable that hangs together with them
625 std::vector<glm::ivec3> stack;
626 const auto visit = [&](const glm::ivec3& v) {
627 if (out.keys.insert(voxelKey(v)).second) {
628 stack.push_back(v);
629 }
630 };
631 for (const glm::ivec3& v : frontier) {
632 visit(v);
633 }
634
635 float cost = 0.0f;
636 while (not stack.empty()) {
637 const glm::ivec3 v = stack.back();
638 stack.pop_back();
639 if (out.keys.size() > max_voxels) {
640 return {};
641 }
642
643 const voxel::PhysicalMaterial* self = material_at(v);
644 for (const glm::ivec3& face : k_faces) {
645 const glm::ivec3 next = v + face;
646 const voxel::PhysicalMaterial* other = material_at(next);
647 if (other == nullptr) {
648 continue;
649 }
650 if (other->breakableBy(max_hardness)) {
651 visit(next);
652 } else {
653 cost += bond_cost * voxelBreakEnergy(*self);
654 }
655 }
656 if (cost > energy) {
657 return {};
658 }
659 out.low = glm::min(out.low, glm::vec3(v) * voxel::k_voxel_size);
660 out.high = glm::max(out.high, (glm::vec3(v) + 1.0f) * voxel::k_voxel_size);
661 }
662 out.cost = cost;
663 return out;
664}
665
666[[nodiscard]]
667inline auto explosionDensityAt(float density, float distance, float falloff) -> float {
668 return density / std::pow(std::max(distance, voxel::k_voxel_size), falloff);
669}
670
671[[nodiscard]]
672inline auto explosionReach(float density, float falloff, float toughness, float max_radius) -> float {
673 if (not(density > 0.0f) || not(falloff > 0.0f)) {
674 return 0.0f;
675 }
676 if (not(toughness > 0.0f)) {
677 return max_radius;
678 }
679 return std::min(std::pow(density / toughness, 1.0f / falloff), max_radius);
680}
681
682[[nodiscard]]
683inline auto weakestToughness(const voxel::MaterialLibrary& materials, Hardness max_hardness) -> float {
684 float out = std::numeric_limits<float>::infinity();
685 for (const voxel::PhysicalMaterial& material : materials.materials) {
686 if (material.breakableBy(max_hardness)) {
687 out = std::min(out, material.toughness);
688 }
689 }
690 return out;
691}
692
693[[nodiscard]]
694inline auto impactDensity(float energy, float patch_radius) -> float {
695 const float radius = std::max(patch_radius, voxel::k_voxel_size);
696 return energy / (k_joules_per_kilojoule * (2.0f / 3.0f) * std::numbers::pi_v<float> * radius * radius * radius);
697}
698
699}
Definition voxel_volume.hpp:21
How a material breaks: how the energy of a hit is spent and how coarse its cracks are.
Deterministic randomness for destruction.
Tiers of how hard a material is to break.
auto resolveMaterialIndex(const Palette &palette, const MaterialLibrary &library, uint8_t palette_index) noexcept -> uint32_t
The library must not be empty.
Definition palette.hpp:149
Definition fracture.hpp:75
Definition fracture.hpp:422
Definition fracture.hpp:122
Definition fracture.hpp:42
Definition fracture_profile.hpp:17
Definition fracture.hpp:130
Definition fracture.hpp:63
float toughness
kJ/m³ it takes to break per m3
Definition palette.hpp:106