Toast Engine
Loading...
Searching...
No Matches
shatter.hpp
Go to the documentation of this file.
1
7
8#pragma once
9
10#include "fracture.hpp"
11
12#include <algorithm>
13#include <array>
14#include <cmath>
15#include <cstdint>
16#include <glm/glm.hpp>
17#include <limits>
18#include <span>
19#include <unordered_set>
20#include <vector>
21
22namespace physics {
23
25 std::vector<glm::ivec3> crack;
26 float band_outer = 0.0f;
27 uint32_t cells = 0;
28 const char* stop = "ok"; // debug
29 float width = 0.0f;
30 float density = 0.0f;
31 size_t band_voxels = 0;
32};
33
34inline constexpr float k_min_crack_half_width = 0.5f * 1.7320508f;
35
36[[nodiscard]]
37constexpr auto patternShatters(FracturePattern pattern) -> bool {
38 return pattern != FracturePattern::ductile;
39}
40
41[[nodiscard]]
42inline auto crackThickness() -> float {
43 return 2.0f * k_min_crack_half_width * voxel::k_voxel_size;
44}
45
46[[nodiscard]]
47inline auto minChunkVoxels(float crack_thickness) -> uint32_t {
48 const float side = (2.0f * crack_thickness) / voxel::k_voxel_size;
49 return static_cast<uint32_t>(std::ceil(side * side * side));
50}
51
52[[nodiscard]]
53inline auto craterVolume(const Crater& crater) -> float {
54 return capsuleVolume(crater.radius, glm::length(crater.end - crater.start));
55}
56
57[[nodiscard]]
58inline auto planShatter(
59 const voxel::Volume& volume, const voxel::Palette& palette, const voxel::MaterialLibrary& materials, const Crater& crater,
60 Hardness max_hardness
61) -> ShatterPlan {
62 ShatterPlan out;
63 out.band_outer = craterMaxRadius(crater);
64
65 const FractureValues& values = crater.profile;
66 if (not crater.breaks || not(crater.shatter_energy > 0.0f) || not patternShatters(crater.pattern) ||
67 not(crater.radius > 0.0f) || materials.materials.empty()) {
68 out.stop = "preconditions";
69 return out;
70 }
71
72 const float crater_volume = craterVolume(crater);
73 out.density = crater.shatter_energy / crater_volume;
74 if (out.density < values.shatter_threshold * crater.toughness) {
75 out.stop = "energy density below the threshold of the material";
76 return out;
77 }
78
79 const float threshold = crackThickness();
80 const float min_width = 2.0f * threshold;
81 const float max_width = std::max(tunables().fracture_max_shatter_distance, min_width);
82
83 // everything that could crack
84 struct BandVoxel {
85 glm::ivec3 voxel;
86 glm::vec3 center;
87 float outside;
88 };
89
90 std::vector<BandVoxel> band;
91 float toughness_sum = 0.0f;
92 const float reach = craterMaxRadius(crater) + max_width;
93 forEachSolidVoxel(
94 volume,
95 glm::min(crater.start, crater.end) - glm::vec3(reach),
96 glm::max(crater.start, crater.end) + glm::vec3(reach),
97 [&](glm::ivec3 v, glm::vec3 center, uint8_t index) {
98 const glm::vec3 axis_point = closestPointOnSegment(center, crater.start, crater.end);
99 const float outside = glm::length(center - axis_point) - craterRadiusAt(crater, center);
100 if (outside <= 0.0f || outside > max_width) {
101 return;
102 }
103 const voxel::PhysicalMaterial& material = materials.materials[voxel::resolveMaterialIndex(palette, materials, index)];
104 if (not material.breakableBy(max_hardness) || not damageReaches(crater.reachable, volume, v, axis_point, center)) {
105 return;
106 }
107 band.push_back({.voxel = v, .center = center, .outside = outside});
108 toughness_sum += material.toughness;
109 }
110 );
111 if (band.empty()) {
112 out.stop = "no breakable material around the crater";
113 return out;
114 }
115
116 const float cell_size = std::max(values.shatter_cell_size, threshold);
117 const float mean_toughness = toughness_sum / static_cast<float>(band.size());
118 const float length = glm::length(crater.end - crater.start);
119 const float base_volume = capsuleVolume(crater.radius, length);
120 const float fill = std::min(1.0f, 3.0f * threshold / cell_size);
121 const auto band_volume_of = [&](float w) { return capsuleVolume(crater.radius + w, length) - base_volume; };
122 const auto cost_of = [&](float w) { return mean_toughness * values.shatter_crack_cost * fill * band_volume_of(w); };
123 float width = min_width;
124 if (cost_of(max_width) <= crater.shatter_energy) {
125 width = max_width;
126 } else if (cost_of(min_width) < crater.shatter_energy) {
127 float low = min_width;
128 float high = max_width;
129 for (int i = 0; i < 24; ++i) {
130 const float middle = 0.5f * (low + high);
131 (cost_of(middle) <= crater.shatter_energy ? low : high) = middle;
132 }
133 width = low;
134 }
135 out.width = width;
136 out.band_outer = craterMaxRadius(crater) + width;
137 std::erase_if(band, [width](const BandVoxel& b) { return b.outside > width; });
138 out.band_voxels = band.size();
139 if (band.size() < 2) {
140 out.stop = "no breakable material around the crater";
141 return out;
142 }
143
144 const float cells_wanted = std::clamp(
145 band_volume_of(width) / (cell_size * cell_size * cell_size),
146 2.0f,
147 static_cast<float>(std::max(tunables().fracture_max_shatter_cells, 2u))
148 );
149 const auto cell_count = static_cast<uint32_t>(cells_wanted);
150
151 // seeds
152 Rng rng {streamSeed(crater.seed, FractureStream::shatter_points)};
153 const float min_gap = 2.0f * threshold;
154 std::vector<glm::vec3> seeds;
155 for (uint32_t attempt = 0; attempt < cell_count * 12 && seeds.size() < cell_count; ++attempt) {
156 const glm::vec3 point = band[rng.below(static_cast<uint32_t>(band.size()))].center;
157 const bool crowded = std::ranges::any_of(seeds, [&](const glm::vec3& s) { return glm::distance(s, point) < min_gap; });
158 if (not crowded) {
159 seeds.push_back(point);
160 }
161 }
162 if (seeds.size() < 2) {
163 out.stop = "fewer than 2 seeds";
164 return out;
165 }
166 out.cells = static_cast<uint32_t>(seeds.size());
167
168 // fibrous
169 const bool fibrous = crater.pattern == FracturePattern::fibrous;
170 const glm::uvec3 dims = volume.voxelDims();
171 glm::vec3 grain {0.0f};
172 if (dims.x >= dims.y && dims.x >= dims.z) {
173 grain[0] = 1.0f;
174 } else {
175 grain[(dims.y >= dims.z ? 1 : 2)] = 1.0f;
176 }
177 const auto metric = [&](const glm::vec3& offset) {
178 if (not fibrous) {
179 return glm::length(offset);
180 }
181 const float along = glm::dot(offset, grain);
182 const glm::vec3 lateral = offset - (grain * along);
183 const float stretched = along / std::max(values.shatter_stretch, 1.0f);
184 return std::sqrt(glm::dot(lateral, lateral) + (stretched * stretched));
185 };
186
187 std::unordered_set<uint64_t> in_band;
188 in_band.reserve(band.size());
189 for (const BandVoxel& b : band) {
190 in_band.insert(voxelKey(b.voxel));
191 }
192
193 // solid material
194 const glm::ivec3 dims_i = glm::ivec3(dims);
195 const std::array<glm::ivec3, k_unglue_neighbor_count> k_faces {
196 glm::ivec3 { 1, 0, 0},
197 glm::ivec3 {-1, 0, 0},
198 glm::ivec3 { 0, 1, 0},
199 glm::ivec3 { 0, -1, 0},
200 glm::ivec3 { 0, 0, 1},
201 glm::ivec3 { 0, 0, -1},
202 };
203 const auto touches_wall = [&](const glm::ivec3& v) {
204 for (const glm::ivec3& offset : k_faces) {
205 const glm::ivec3 n = v + offset;
206 if (glm::any(glm::lessThan(n, glm::ivec3(0))) || glm::any(glm::greaterThanEqual(n, dims_i)) ||
207 volume.materialAt(n) == voxel::k_empty_palette_index || in_band.contains(voxelKey(n))) {
208 continue;
209 }
210 const glm::vec3 center = (glm::vec3 {n} + 0.5f) * voxel::k_voxel_size;
211 const float radius = craterRadiusAt(crater, center);
212 if (pointSegmentDistanceSquared(center, crater.start, crater.end) > radius * radius) {
213 return true;
214 }
215 }
216 return false;
217 };
218
219 struct Owner {
220 size_t nearest = 0;
221 size_t second = 0;
222 float gap = 0.0f; // d2 - d1
223 };
224
225 std::vector<Owner> owners(band.size());
226 std::vector<uint32_t> cell_voxels(seeds.size(), 0);
227 for (size_t b = 0; b < band.size(); ++b) {
228 float d1 = std::numeric_limits<float>::max();
229 float d2 = std::numeric_limits<float>::max();
230 Owner& owner = owners[b];
231 for (size_t i = 0; i < seeds.size(); ++i) {
232 const float d = metric(band[b].center - seeds[i]);
233 if (d < d1) {
234 d2 = d1;
235 owner.second = owner.nearest;
236 d1 = d;
237 owner.nearest = i;
238 } else if (d < d2) {
239 d2 = d;
240 owner.second = i;
241 }
242 }
243 owner.gap = d2 - d1;
244 ++cell_voxels[owner.nearest];
245 }
246
247 const uint32_t min_cell_voxels = minChunkVoxels(threshold);
248 const float release = std::clamp(values.shatter_release, 0.0f, 1.0f);
249 std::vector<bool> released(seeds.size(), false);
250 std::vector<size_t> eligible;
251 for (size_t i = 0; i < seeds.size(); ++i) {
252 if (cell_voxels[i] >= min_cell_voxels) {
253 eligible.push_back(i);
254 released[i] = rng.chance(release);
255 }
256 }
257 if (eligible.empty()) {
258 out.stop = "cells too small to come off";
259 return out;
260 }
261 if (std::ranges::none_of(eligible, [&](size_t i) { return released[i]; })) {
262 released[eligible[rng.below(static_cast<uint32_t>(eligible.size()))]] = true;
263 }
264
265 const uint64_t crumble_seed = streamSeed(crater.seed, FractureStream::granular);
266 for (size_t b = 0; b < band.size(); ++b) {
267 const Owner& owner = owners[b];
268 const bool mine = released[owner.nearest];
269 const bool border = owner.gap < threshold && (mine || released[owner.second]);
270 bool crack = border;
271 crack = crack || (mine && touches_wall(band[b].voxel));
272 crack = crack ||
273 (mine && crater.pattern == FracturePattern::granular && hash01(crumble_seed, band[b].voxel) < values.shatter_crumble);
274 if (crack) {
275 out.crack.push_back(band[b].voxel);
276 }
277 }
278 return out;
279}
280
281struct CoreVoxel {
282 glm::ivec3 voxel {0};
283 glm::vec3 center {0.0f};
284};
285
286struct CoreSplit {
287 std::vector<glm::ivec3> crack;
288 uint32_t parts = 0;
289 float density = 0.0f;
290};
291
292[[nodiscard]]
293inline auto planCoreSplit(std::span<const CoreVoxel> core, const Crater& crater) -> CoreSplit {
294 CoreSplit out;
295 const FractureValues& values = crater.profile;
296 if (not(crater.fragment_energy > 0.0f) || not(crater.radius > 0.0f)) {
297 return out;
298 }
299 out.density = crater.fragment_energy / craterVolume(crater);
300 const float part_density = std::max(values.fragment_threshold * crater.toughness, 1.0e-9f);
301 if (out.density < part_density) {
302 return out;
303 }
304
305 Rng rng {streamSeed(crater.seed, FractureStream::core_split)};
306 const float doublings = std::log2(out.density / part_density);
307 const float parts_wanted =
308 std::clamp(2.0f + std::floor(doublings), 2.0f, static_cast<float>(std::max(values.fragment_max_parts, 2u)));
309
310 const float threshold = crackThickness();
311 const auto room = static_cast<uint32_t>(core.size() / minChunkVoxels(threshold));
312 const uint32_t parts = std::min(static_cast<uint32_t>(parts_wanted), room);
313 if (parts < 2) {
314 return out;
315 }
316
317 std::vector<glm::vec3> seeds {core[rng.below(static_cast<uint32_t>(core.size()))].center};
318 while (seeds.size() < parts) {
319 glm::vec3 best = seeds.front();
320 float best_gap = -1.0f;
321 for (int attempt = 0; attempt < 12; ++attempt) {
322 const glm::vec3 point = core[rng.below(static_cast<uint32_t>(core.size()))].center;
323 float gap = std::numeric_limits<float>::max();
324 for (const glm::vec3& s : seeds) {
325 gap = std::min(gap, glm::distance(s, point));
326 }
327 if (gap > best_gap) {
328 best_gap = gap;
329 best = point;
330 }
331 }
332 if (best_gap < 2.0f * threshold) {
333 break; // too small
334 }
335 seeds.push_back(best);
336 }
337 if (seeds.size() < 2) {
338 return out;
339 }
340 out.parts = static_cast<uint32_t>(seeds.size());
341
342 for (const CoreVoxel& c : core) {
343 float d1 = std::numeric_limits<float>::max();
344 float d2 = std::numeric_limits<float>::max();
345 for (const glm::vec3& seed_point : seeds) {
346 const float d = glm::distance(c.center, seed_point);
347 if (d < d1) {
348 d2 = d1;
349 d1 = d;
350 } else if (d < d2) {
351 d2 = d;
352 }
353 }
354 if (d2 - d1 < threshold) {
355 out.crack.push_back(c.voxel);
356 }
357 }
358 return out;
359}
360
361}
How much of a voxel volume a hit breaks.
Definition shatter.hpp:286
Definition shatter.hpp:281
Definition fracture.hpp:75
Definition fracture_profile.hpp:17
Definition fracture_random.hpp:99
Definition shatter.hpp:24
@ release
the input went from on to off
Definition value.hpp:79
@ fill
Adds its voxels with its write mode.
Definition voxel_piece.hpp:27