10#include "fracture_pattern.hpp"
28#include <unordered_set>
33inline constexpr float k_voxel_volume = voxel::k_voxel_size * voxel::k_voxel_size * voxel::k_voxel_size;
35inline constexpr float k_joules_per_kilojoule = 1000.0f;
38constexpr auto voxelBreakEnergy(
const voxel::PhysicalMaterial& material) ->
float {
39 return material.toughness * k_voxel_volume;
44 glm::vec3 direction {};
46 float energy_density = 0.0f;
47 Hardness max_hardness = k_hardest;
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;
56inline constexpr uint32_t k_unglue_neighbor_count = 6;
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);
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()};
70 auto empty()
const ->
bool {
81 std::unordered_set<uint64_t> reachable;
83 float edge_noise = 0.0f;
84 float edge_noise_scale = 0.0f;
85 float edge_lobes = 0.0f;
86 FracturePattern pattern = FracturePattern::brittle;
88 float toughness = 0.0f;
89 float fragment_energy = 0.0f;
90 float shatter_energy = 0.0f;
91 float debris_energy = 0.0f;
94inline constexpr float k_max_edge_noise = 0.9f;
95inline constexpr float k_min_radius_factor = 0.15f;
98inline auto craterRadiusAt(
const Crater& crater,
const glm::vec3& point) ->
float {
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);
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);
108 return crater.radius * std::max(factor, k_min_radius_factor);
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));
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);
124 float distance = 0.0f;
126 bool breakable =
false;
127 bool on_axis =
false;
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) {
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) {
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) {
168 visit(v, (glm::vec3 {v} + 0.5f) * voxel::k_voxel_size, palette_index);
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);
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);
195inline auto reachesThroughMaterial(
const voxel::Volume& volume,
const glm::vec3& from,
const glm::vec3& to) ->
bool {
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)));
206 const float step_length = length /
static_cast<float>(steps);
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;
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;
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;
239 if (not seed.has_value()) {
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();
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) {
257 const glm::vec3 center = (glm::vec3 {next} + 0.5f) * voxel::k_voxel_size;
258 if (pointSegmentDistanceSquared(center, start, end) > radius * radius) {
261 if (out.insert(voxelKey(next)).second) {
262 stack.push_back(next);
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
276 return reachable.contains(voxelKey(voxel)) || reachesThroughMaterial(volume, from, center);
280constexpr auto hitBreaks(
const voxel::PhysicalMaterial& material,
float energy_density, Hardness max_hardness) ->
bool {
281 return material.breakableBy(max_hardness) && material.toughness <= energy_density;
285inline auto hitMaterial(
286 const voxel::Volume& volume,
const voxel::Palette& palette,
const voxel::MaterialLibrary& materials,
const glm::vec3& point,
288) -> std::optional<voxel::PhysicalMaterial> {
289 std::optional<voxel::PhysicalMaterial> out;
290 if (materials.materials.empty()) {
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;
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)) {
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;
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);
328 .distance = distance,
329 .cost = voxelBreakEnergy(material),
330 .breakable = material.breakableBy(request.max_hardness),
333 if (distance <= radius) {
334 if (damageReaches(reachable, volume, voxel, request.point, center)) {
335 out.push_back(candidate);
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) {
350 candidate.depth = along - std::sqrt((radius * radius) - lateral_squared);
351 if (candidate.depth > request.max_depth) {
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);
363inline auto penetrationDepth(std::span<const FractureCandidate> candidates,
float energy,
float max_depth) ->
Penetration {
366 const float max_gap = 1.5f * voxel::k_voxel_size;
367 float reached = 0.0f;
369 if (candidate.depth > max_depth || candidate.depth - reached > max_gap) {
372 reached = std::max(reached, candidate.depth);
373 if (not candidate.breakable) {
374 if (candidate.on_axis) {
379 if (out.spent + candidate.cost > energy) {
382 out.spent += candidate.cost;
383 out.depth = candidate.depth;
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);
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)) {
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) {
411 for (
int i = 0; i < 24; ++i) {
412 const float middle = 0.5f * (low + high);
413 if (capsule_volume(radius + middle) - base <= affordable) {
424 float fragment = 0.0f;
425 float shatter = 0.0f;
426 float overshoot = 0.0f;
430inline auto splitLeftoverEnergy(
float left,
float debris_share,
float fragment_share, FracturePattern pattern) ->
EnergySplit {
431 if (not std::isfinite(left) || left <= 0.0f) {
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) {
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;
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
460inline auto planCrater(
461 const voxel::Volume& volume,
const voxel::Palette& palette,
const voxel::MaterialLibrary& materials,
465 .start = request.point,
466 .end = request.point,
467 .seed = request.seed,
469 if (not std::isfinite(request.energy_density) || not(request.energy_density > 0.0f) || not(request.radius > 0.0f)) {
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)) {
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);
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());
497 const auto breakable =
static_cast<float>(std::ranges::count_if(inside, &FractureCandidate::breakable));
499 const float budget = request.energy_density * breakable * k_voxel_volume * (1.0f + 1.0e-5f);
502 std::ranges::sort(inside, {}, &FractureCandidate::distance);
504 float paid_cost = 0.0f;
506 float reached = 0.0f;
507 bool short_of_energy =
false;
509 if (not candidate.breakable) {
512 if (spent + candidate.cost > budget) {
513 short_of_energy =
true;
516 spent += candidate.cost;
517 paid_cost += candidate.cost;
519 reached = candidate.distance;
525 if (short_of_energy) {
526 out.radius = std::max(reached, 0.5f * voxel::k_voxel_size);
529 out.radius = request.radius;
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);
537 paid_cost += dig.spent;
541 out.unglue = planUnglue(
548 request.max_hardness,
551 request.max_unglue_voxels
553 left -= out.unglue.cost;
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;
561 overshootRadius(split.overshoot, toughness, request.radius, glm::length(out.end - out.start), request.max_overshoot);
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
571 if (not(energy > 0.0f) || not(bond_cost > 0.0f) || materials.materials.empty()) {
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));
579 const auto material_at = [&](
const glm::ivec3& v) ->
const voxel::PhysicalMaterial* {
583 const uint8_t index = volume.materialAt(v);
584 if (index == voxel::k_empty_palette_index) {
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}
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) {
608 const voxel::PhysicalMaterial* self = material_at(v);
609 if (self ==
nullptr || not self->breakableBy(max_hardness)) {
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);
620 if (frontier.empty()) {
625 std::vector<glm::ivec3> stack;
626 const auto visit = [&](
const glm::ivec3& v) {
627 if (out.keys.insert(voxelKey(v)).second) {
631 for (
const glm::ivec3& v : frontier) {
636 while (not stack.empty()) {
637 const glm::ivec3 v = stack.back();
639 if (out.keys.size() > max_voxels) {
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) {
650 if (other->breakableBy(max_hardness)) {
653 cost += bond_cost * voxelBreakEnergy(*self);
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);
667inline auto explosionDensityAt(
float density,
float distance,
float falloff) ->
float {
668 return density / std::pow(std::max(distance, voxel::k_voxel_size), falloff);
672inline auto explosionReach(
float density,
float falloff,
float toughness,
float max_radius) ->
float {
673 if (not(density > 0.0f) || not(falloff > 0.0f)) {
676 if (not(toughness > 0.0f)) {
679 return std::min(std::pow(density / toughness, 1.0f / falloff), max_radius);
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)) {
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);
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