载入中...
搜索中...
未找到
GeometryCollectionCooker.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cmath>
7#include <cstdint>
8#include <limits>
9#include <utility>
10#include <vector>
11
13namespace {
14
15struct Vec3 {
16 float x = 0.f;
17 float y = 0.f;
18 float z = 0.f;
19};
20
21struct Aabb {
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()};
25
26 void expand(float x, float y, float z) {
27 min.x = std::min(min.x, x);
28 min.y = std::min(min.y, y);
29 min.z = std::min(min.z, z);
30 max.x = std::max(max.x, x);
31 max.y = std::max(max.y, y);
32 max.z = std::max(max.z, z);
33 }
34
35 [[nodiscard]] Vec3 center() const {
36 return {0.5f * (min.x + max.x), 0.5f * (min.y + max.y), 0.5f * (min.z + max.z)};
37 }
38
39 [[nodiscard]] Vec3 extent() const { return {max.x - min.x, max.y - min.y, max.z - min.z}; }
40
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);
44 }
45};
46
47struct Cell {
48 Vec3 center;
50 int clusterId = 0;
51};
52
53struct Site {
55 int clusterId = 0;
56};
57
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) {
61 value ^= bytes[i];
62 value *= 0x100000001b3ull;
63 }
64 return value;
65}
66
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());
70 value ^= seed + 0x9e3779b97f4a7c15ull;
71 return value == 0 ? 0xA5A5A5A5A5A5A5A5ull : value;
72}
73
74class SplitMix64 {
75public:
76 explicit SplitMix64(std::uint64_t seed) : state_(seed) {}
77
78 std::uint64_t nextU64() {
79 std::uint64_t z = (state_ += 0x9e3779b97f4a7c15ull);
80 z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull;
81 z = (z ^ (z >> 27)) * 0x94d049bb133111ebull;
82 return z ^ (z >> 31);
83 }
84
85 float nextUnit() {
86 return static_cast<float>(nextU64() >> 11) * (1.f / 9007199254740992.f);
87 }
88
89 float nextRange(float lo, float hi) { return lo + (hi - lo) * nextUnit(); }
90
91private:
92 std::uint64_t state_;
93};
94
95float length3(float x, float y, float z) { return std::sqrt(x * x + y * y + z * z); }
96
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};
101}
102
103eve::Result<Aabb> meshBounds(const eve::asset::CanonicalMeshData& mesh, float minimumThickness) {
104 if (mesh.positions.empty() || mesh.positions.size() % 3 != 0)
106 eve::DiagnosticCode::InvalidArgument, "mesh positions must contain complete xyz vertices",
107 "mesh.positions"));
108 if (mesh.indices.empty() || mesh.indices.size() % 3 != 0)
110 eve::DiagnosticCode::InvalidArgument, "mesh indices must contain complete triangles", "mesh.indices"));
111 const std::size_t vertexCount = mesh.positions.size() / 3;
112 Aabb bounds;
113 for (std::size_t i = 0; i < vertexCount; ++i) {
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))
119 eve::DiagnosticCode::InvalidArgument, "mesh positions must be finite", "mesh.positions"));
120 bounds.expand(x, y, z);
121 }
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];
126 if (a >= vertexCount || b >= vertexCount || c >= vertexCount || a == b || b == c || a == c)
128 eve::DiagnosticCode::InvalidArgument, "mesh contains an invalid triangle", "mesh.indices"));
129 }
130 const Vec3 extent = bounds.extent();
131 if (extent.x < minimumThickness || extent.y < minimumThickness || extent.z < minimumThickness)
133 eve::DiagnosticCode::InvalidArgument, "mesh AABB is thinner than minimumThickness", "mesh.bounds"));
134 if (!(bounds.volume() > 0.f))
136 eve::DiagnosticCode::InvalidArgument, "mesh AABB volume must be positive", "mesh.bounds"));
138}
139
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));
144}
145
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) {
150 Site site;
151 site.position = {rng.nextRange(bounds.min.x, bounds.max.x), rng.nextRange(bounds.min.y, bounds.max.y),
152 rng.nextRange(bounds.min.z, bounds.max.z)};
153 site.clusterId = 0;
154 sites.push_back(site);
155 }
156 return sites;
157}
158
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) {
164 centers.push_back({rng.nextRange(bounds.min.x, bounds.max.x), rng.nextRange(bounds.min.y, bounds.max.y),
165 rng.nextRange(bounds.min.z, bounds.max.z)});
166 }
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;
171 const Vec3& center = centers[static_cast<std::size_t>(clusterId)];
172 Site site;
173 site.clusterId = clusterId;
174 site.position = {
175 std::clamp(center.x + rng.nextRange(-recipe.clusterRadius, recipe.clusterRadius), bounds.min.x,
176 bounds.max.x),
177 std::clamp(center.y + rng.nextRange(-recipe.clusterRadius, recipe.clusterRadius), bounds.min.y,
178 bounds.max.y),
179 std::clamp(center.z + rng.nextRange(-recipe.clusterRadius, recipe.clusterRadius), bounds.min.z,
180 bounds.max.z),
181 };
182 sites.push_back(site);
183 }
184 return sites;
185}
186
187std::vector<Cell> cellsFromSites(const Aabb& bounds, const std::vector<Site>& sites) {
188 std::vector<Cell> cells;
189 cells.reserve(sites.size());
190 const Vec3 origin = bounds.center();
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));
199 }
200 if (!std::isfinite(nearest) || nearest > 1e20f) {
201 const Vec3 extent = bounds.extent();
202 nearest = 0.5f * std::min({extent.x, extent.y, extent.z});
203 }
204 const float half = std::max(0.05f, 0.5f * nearest);
205 Cell cell;
206 cell.center = {sites[i].position.x - origin.x, sites[i].position.y - origin.y,
207 sites[i].position.z - origin.z};
208 // Clamp proxy to remaining distance to AABB walls in local space.
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;
214 cells.push_back(cell);
215 }
216 return cells;
217}
218
219std::vector<Cell> cellsFromPlanar(const Aabb& bounds, const FractureRecipe& recipe) {
220 struct BoxCell {
221 Aabb box;
222 int clusterId = 0;
223 };
224 std::vector<BoxCell> cells{{bounds, 0}};
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];
230 const Vec3 normal = normalize3(nx, ny, nz);
231 const float offset = recipe.planeOffsets[p];
232 std::vector<BoxCell> next;
233 next.reserve(cells.size() * 2);
234 for (const auto& cell : cells) {
235 // Split AABB along the dominant axis of the plane normal for a stable proxy.
236 const float ax = std::fabs(normal.x);
237 const float ay = std::fabs(normal.y);
238 const float az = std::fabs(normal.z);
239 Aabb left = cell.box;
240 Aabb right = cell.box;
241 bool split = false;
242 if (ax >= ay && ax >= az) {
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) {
245 left.max.x = cut;
246 right.min.x = cut;
247 split = true;
248 }
249 } else if (ay >= ax && ay >= az) {
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) {
252 left.max.y = cut;
253 right.min.y = cut;
254 split = true;
255 }
256 } else {
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) {
259 left.max.z = cut;
260 right.min.z = cut;
261 split = true;
262 }
263 }
264 if (!split) {
265 next.push_back(cell);
266 } else {
267 next.push_back({left, cell.clusterId});
268 next.push_back({right, cell.clusterId});
269 }
270 }
271 cells.swap(next);
272 }
273 const Vec3 origin = bounds.center();
274 std::vector<Cell> out;
275 out.reserve(cells.size());
276 for (const auto& cell : cells) {
277 const Vec3 c = cell.box.center();
278 const Vec3 e = cell.box.extent();
279 Cell proxy;
280 proxy.center = {c.x - origin.x, c.y - origin.y, c.z - origin.z};
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);
284 }
285 return out;
286}
287
288std::vector<Cell> cellsFromRadial(const Aabb& bounds, const FractureRecipe& recipe) {
289 FractureRecipe planar = recipe;
290 planar.mode = FractureMode::Planar;
291 planar.planeNormals.clear();
292 planar.planeOffsets.clear();
293 const Vec3 origin = bounds.center();
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);
302 // Plane through origin: n·x = n·origin
303 planar.planeOffsets.push_back(nx * origin.x + nz * origin.z);
304 }
305 // Optional shell cuts along Y for radialPlanes > 0.
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);
315 }
316 }
317 return cellsFromPlanar(bounds, planar);
318}
319
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) {
325 const float dx = cells[i].center.x - cells[j].center.x;
326 const float dy = cells[i].center.y - cells[j].center.y;
327 const float dz = cells[i].center.z - cells[j].center.z;
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) /
331 6.f;
332 if (dist > reach) continue;
333 GeometryCollectionEdge edge;
334 edge.boneA = static_cast<int>(i);
335 edge.boneB = static_cast<int>(j);
336 // Auto-cluster: intra-cluster edges are stronger so islands break apart first.
337 if (recipe.mode == FractureMode::ClusteredVoronoi && cells[i].clusterId == cells[j].clusterId)
338 edge.strainThreshold = recipe.defaultStrainThreshold * 2.f;
339 else if (recipe.mode == FractureMode::ClusteredVoronoi)
340 edge.strainThreshold = recipe.defaultStrainThreshold * 0.5f;
341 else
342 edge.strainThreshold = recipe.defaultStrainThreshold;
343 asset.edges.push_back(edge);
344 }
345 }
346}
347
348} // namespace
349
351 const FractureRecipe& recipe) {
352 auto valid = recipe.validate();
354 auto bounds = meshBounds(mesh, recipe.minimumThickness);
356
357 SplitMix64 rng(mixSeed(recipe.seed, recipe.randomStreamName));
358 std::vector<Cell> cells;
359 switch (recipe.mode) {
361 const int count = chooseSiteCount(recipe, rng);
362 cells = cellsFromSites(bounds.value(), makeUniformSites(bounds.value(), count, rng));
363 break;
364 }
366 cells = cellsFromSites(bounds.value(), makeClusteredSites(bounds.value(), recipe, rng));
367 break;
369 cells = cellsFromPlanar(bounds.value(), recipe);
370 break;
372 cells = cellsFromRadial(bounds.value(), recipe);
373 break;
374 }
375 if (cells.empty())
377 eve::DiagnosticCode::Failed, "fracture cook produced no bones", "cook"));
378 if (static_cast<int>(cells.size()) > recipe.maximumBones)
380 eve::DiagnosticCode::InvalidArgument, "fracture cook exceeded maximumBones", "maximumBones"));
381
383 asset.bones.reserve(cells.size());
384 for (const auto& cell : cells) {
386 bone.halfExtentX = cell.halfExtent.x;
387 bone.halfExtentY = cell.halfExtent.y;
388 bone.halfExtentZ = cell.halfExtent.z;
389 bone.localX = cell.center.x;
390 bone.localY = cell.center.y;
391 bone.localZ = cell.center.z;
392 const float volume =
393 8.f * cell.halfExtent.x * cell.halfExtent.y * cell.halfExtent.z;
394 bone.density = recipe.defaultDensity;
395 bone.mass = std::max(0.05f, volume * recipe.defaultDensity);
396 bone.friction = recipe.defaultFriction;
397 bone.restitution = recipe.defaultRestitution;
398 bone.clusterId = cell.clusterId;
399 bone.fractureLevel = 0;
400 asset.bones.push_back(bone);
401 }
402 buildConnectionGraph(cells, recipe, asset);
403 auto assetValid = asset.validate();
404 if (!assetValid) return eve::Result<GeometryCollectionAsset>::failure(assetValid.status());
405 return eve::Result<GeometryCollectionAsset>::success(std::move(asset));
406}
407
408} // namespace eve::physics::destruction_cook
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
double volume
const std::string & s
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
bool split
Definition CaveMesh.cpp:123
int az
Definition CaveMesh.cpp:113
float nx
float nz
float ny
glm::vec4 p[6]
std::uint32_t vertexCount
HexVec3 left
HexVec3 right
std::int32_t c
size_t offset
std::array< float, 3 > position
std::uint32_t bone
std::uint64_t bytes
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
Texture * normal
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::uint32_t seed
Definition PointSet.cpp:807
float t
Mesh * mesh
V3 origin
Definition RoadBake.cpp:138
const RoadEdge * edge
float dz
float dy
float dx
std::uint32_t count
Cell cell
float size
Definition TreeMesh.cpp:156
float angle
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
@ Cell
A cell was removed; the out-parameter holds it.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
eve::Result< GeometryCollectionAsset > cookGeometryCollection(const eve::asset::CanonicalMeshData &mesh, const FractureRecipe &recipe)
Cook an owning canonical mesh into a geometry-collection asset.
Owning CPU mesh snapshot; UV arrays are packed ST pairs indexed by source set number....
Versioned cook recipe for Chaos-style pre-fracture (schema v1).
int maximumBones
Soft cap on produced leaf bones.
float minimumThickness
Minimum AABB axis length in metres; thinner meshes are rejected.
eve::Result< void > validate() const
Validate.
Immutable cooked/authored geometry-collection asset.
std::vector< GeometryCollectionBone > bones
eve::Result< void > validate() const
Validate bone/edge topology and numeric ranges.
One pre-authored leaf bone (box collision proxy) inside a geometry collection.
glm::vec4 bounds