22 Vec3
min{std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max()};
23 Vec3
max{-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max(),
24 -std::numeric_limits<float>::max()};
26 void expand(
float x,
float y,
float z) {
41 [[nodiscard]]
float volume()
const {
42 const Vec3 e = extent();
43 return std::max(0.f, e.x) * std::max(0.f, e.y) * std::max(0.f, e.z);
58std::uint64_t hashBytes(std::uint64_t
value,
const void* data, std::size_t
size) {
59 const auto*
bytes =
static_cast<const std::uint8_t*
>(data);
60 for (std::size_t i = 0; i <
size; ++i) {
62 value *= 0x100000001b3ull;
67std::uint64_t mixSeed(std::uint64_t
seed,
const std::string& stream) {
68 std::uint64_t
value = 0xcbf29ce484222325ull ^
seed;
69 value = hashBytes(
value, stream.data(), stream.size());
71 return value == 0 ? 0xA5A5A5A5A5A5A5A5ull :
value;
76 explicit SplitMix64(std::uint64_t
seed) : state_(
seed) {}
78 std::uint64_t nextU64() {
79 std::uint64_t
z = (state_ += 0x9e3779b97f4a7c15ull);
80 z = (
z ^ (
z >> 30)) * 0xbf58476d1ce4e5b9ull;
81 z = (
z ^ (
z >> 27)) * 0x94d049bb133111ebull;
86 return static_cast<float>(nextU64() >> 11) * (1.f / 9007199254740992.f);
89 float nextRange(
float lo,
float hi) {
return lo + (hi - lo) * nextUnit(); }
95float length3(
float x,
float y,
float z) {
return std::sqrt(
x *
x +
y *
y +
z *
z); }
97Vec3 normalize3(
float x,
float y,
float z) {
98 const float len = length3(
x,
y,
z);
99 if (len <= 1e-8f)
return {0.f, 1.f, 0.f};
100 return {
x / len,
y / len,
z / len};
104 if (
mesh.positions.empty() ||
mesh.positions.size() % 3 != 0)
108 if (
mesh.indices.empty() ||
mesh.indices.size() % 3 != 0)
114 const float x =
mesh.positions[i * 3];
115 const float y =
mesh.positions[i * 3 + 1];
116 const float z =
mesh.positions[i * 3 + 2];
117 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z))
122 for (std::size_t
t = 0;
t <
mesh.indices.size();
t += 3) {
123 const auto a =
mesh.indices[
t];
124 const auto b =
mesh.indices[
t + 1];
125 const auto c =
mesh.indices[
t + 2];
131 if (extent.x < minimumThickness || extent.y < minimumThickness || extent.z < minimumThickness)
134 if (!(
bounds.volume() > 0.f))
140int chooseSiteCount(
const FractureRecipe& recipe, SplitMix64& rng) {
141 if (recipe.siteCountMin == recipe.siteCountMax)
return recipe.siteCountMin;
142 const int span = recipe.siteCountMax - recipe.siteCountMin + 1;
143 return recipe.siteCountMin +
static_cast<int>(rng.nextU64() %
static_cast<std::uint64_t
>(span));
146std::vector<Site> makeUniformSites(
const Aabb&
bounds,
int count, SplitMix64& rng) {
147 std::vector<Site> sites;
148 sites.reserve(
static_cast<std::size_t
>(
count));
149 for (
int i = 0; i <
count; ++i) {
154 sites.push_back(site);
159std::vector<Site> makeClusteredSites(
const Aabb&
bounds,
const FractureRecipe& recipe, SplitMix64& rng) {
160 const int siteCount = chooseSiteCount(recipe, rng);
161 std::vector<Vec3> centers;
162 centers.reserve(
static_cast<std::size_t
>(recipe.clusterCount));
163 for (
int c = 0;
c < recipe.clusterCount; ++
c) {
167 std::vector<Site> sites;
168 sites.reserve(
static_cast<std::size_t
>(siteCount));
169 for (
int i = 0; i < siteCount; ++i) {
170 const int clusterId = i % recipe.clusterCount;
175 std::clamp(
center.x + rng.nextRange(-recipe.clusterRadius, recipe.clusterRadius),
bounds.min.x,
177 std::clamp(
center.y + rng.nextRange(-recipe.clusterRadius, recipe.clusterRadius),
bounds.min.y,
179 std::clamp(
center.z + rng.nextRange(-recipe.clusterRadius, recipe.clusterRadius),
bounds.min.z,
182 sites.push_back(site);
187std::vector<Cell> cellsFromSites(
const Aabb&
bounds,
const std::vector<Site>& sites) {
188 std::vector<Cell>
cells;
189 cells.reserve(sites.size());
191 for (std::size_t i = 0; i < sites.size(); ++i) {
192 float nearest = std::numeric_limits<float>::max();
193 for (std::size_t j = 0; j < sites.size(); ++j) {
194 if (i == j)
continue;
195 const float dx = sites[i].position.x - sites[j].position.x;
196 const float dy = sites[i].position.y - sites[j].position.y;
197 const float dz = sites[i].position.z - sites[j].position.z;
198 nearest = std::min(nearest, length3(
dx,
dy,
dz));
200 if (!std::isfinite(nearest) || nearest > 1e20f) {
202 nearest = 0.5f * std::min({extent.x, extent.y, extent.z});
204 const float half = std::max(0.05f, 0.5f * nearest);
206 cell.center = {sites[i].position.x -
origin.x, sites[i].position.y -
origin.y,
207 sites[i].position.z -
origin.z};
209 const float maxHx = std::max(0.05f, std::min(sites[i].
position.x -
bounds.min.x,
bounds.max.x - sites[i].position.x));
210 const float maxHy = std::max(0.05f, std::min(sites[i].
position.y -
bounds.min.y,
bounds.max.y - sites[i].position.y));
211 const float maxHz = std::max(0.05f, std::min(sites[i].
position.z -
bounds.min.z,
bounds.max.z - sites[i].position.z));
212 cell.halfExtent = {std::min(half, maxHx), std::min(half, maxHy), std::min(half, maxHz)};
213 cell.clusterId = sites[i].clusterId;
219std::vector<Cell> cellsFromPlanar(
const Aabb&
bounds,
const FractureRecipe& recipe) {
225 const std::size_t planeCount = recipe.planeOffsets.size();
226 for (std::size_t
p = 0;
p < planeCount; ++
p) {
227 const float nx = recipe.planeNormals[
p * 3];
228 const float ny = recipe.planeNormals[
p * 3 + 1];
229 const float nz = recipe.planeNormals[
p * 3 + 2];
231 const float offset = recipe.planeOffsets[
p];
232 std::vector<BoxCell>
next;
236 const float ax = std::fabs(
normal.x);
237 const float ay = std::fabs(
normal.y);
238 const float az = std::fabs(
normal.z);
243 const float cut = std::clamp(
offset,
cell.box.min.x,
cell.box.max.x);
244 if (cut >
cell.box.min.x + 1e-4f && cut <
cell.box.max.x - 1e-4f) {
250 const float cut = std::clamp(
offset,
cell.box.min.y,
cell.box.max.y);
251 if (cut >
cell.box.min.y + 1e-4f && cut <
cell.box.max.y - 1e-4f) {
257 const float cut = std::clamp(
offset,
cell.box.min.z,
cell.box.max.z);
258 if (cut >
cell.box.min.z + 1e-4f && cut <
cell.box.max.z - 1e-4f) {
274 std::vector<Cell> out;
275 out.reserve(
cells.size());
281 proxy.halfExtent = {std::max(0.05f, 0.5f * e.x), std::max(0.05f, 0.5f * e.y), std::max(0.05f, 0.5f * e.z)};
282 proxy.clusterId =
cell.clusterId;
283 out.push_back(proxy);
288std::vector<Cell> cellsFromRadial(
const Aabb&
bounds,
const FractureRecipe& recipe) {
289 FractureRecipe planar = recipe;
291 planar.planeNormals.clear();
292 planar.planeOffsets.clear();
294 const float twoPi = 6.28318530718f;
295 for (
int s = 0;
s < recipe.radialSpokes; ++
s) {
296 const float angle = twoPi *
static_cast<float>(
s) /
static_cast<float>(recipe.radialSpokes);
297 const float nx = std::cos(
angle);
298 const float nz = std::sin(
angle);
299 planar.planeNormals.push_back(
nx);
300 planar.planeNormals.push_back(0.f);
301 planar.planeNormals.push_back(
nz);
306 if (recipe.radialPlanes > 0) {
307 const float y0 =
bounds.min.y;
308 const float y1 =
bounds.max.y;
309 for (
int p = 1;
p <= recipe.radialPlanes; ++
p) {
310 const float t =
static_cast<float>(
p) /
static_cast<float>(recipe.radialPlanes + 1);
311 planar.planeNormals.push_back(0.f);
312 planar.planeNormals.push_back(1.f);
313 planar.planeNormals.push_back(0.f);
314 planar.planeOffsets.push_back(y0 + (y1 - y0) *
t);
317 return cellsFromPlanar(
bounds, planar);
320void buildConnectionGraph(
const std::vector<Cell>&
cells,
const FractureRecipe& recipe,
321 GeometryCollectionAsset& asset) {
322 const float connectScale = 2.25f;
323 for (std::size_t i = 0; i <
cells.size(); ++i) {
324 for (std::size_t j = i + 1; j <
cells.size(); ++j) {
328 const float dist = length3(
dx,
dy,
dz);
329 const float reach = connectScale * (
cells[i].halfExtent.x +
cells[i].halfExtent.y +
cells[i].halfExtent.z +
330 cells[j].halfExtent.x +
cells[j].halfExtent.y +
cells[j].halfExtent.z) /
332 if (dist > reach)
continue;
333 GeometryCollectionEdge
edge;
334 edge.boneA =
static_cast<int>(i);
335 edge.boneB =
static_cast<int>(j);
338 edge.strainThreshold = recipe.defaultStrainThreshold * 2.f;
340 edge.strainThreshold = recipe.defaultStrainThreshold * 0.5f;
342 edge.strainThreshold = recipe.defaultStrainThreshold;
343 asset.edges.push_back(
edge);
358 std::vector<Cell>
cells;
359 switch (recipe.
mode) {
361 const int count = chooseSiteCount(recipe, rng);
366 cells = cellsFromSites(
bounds.value(), makeClusteredSites(
bounds.value(), recipe, rng));
387 bone.halfExtentY =
cell.halfExtent.y;
388 bone.halfExtentZ =
cell.halfExtent.z;
393 8.f *
cell.halfExtent.x *
cell.halfExtent.y *
cell.halfExtent.z;
399 bone.fractureLevel = 0;
402 buildConnectionGraph(
cells, recipe, asset);