15constexpr float kSleepSpeed = 0.05f;
21 body.setType(
"static");
24 body.setType(
"dynamic");
34 : worldHandle_(worldHandle), originX_(originX), originY_(originY), originZ_(originZ) {}
36GeometryCollectionInstance::~GeometryCollectionInstance() { releaseBodies(); }
46 auto instance = std::unique_ptr<GeometryCollectionInstance>(
48 instance->bones_.reserve(asset.
bones.size());
49 instance->edges_.reserve(asset.
edges.size());
51 std::vector<Body3D*> createdBodies;
52 createdBodies.reserve(asset.
bones.size());
53 auto rollback = [&]() {
57 createdBodies.clear();
58 instance->bones_.clear();
61 for (
const auto& boneDef : asset.
bones) {
62 const float x = originX + boneDef.localX;
63 const float y = originY + boneDef.localY;
64 const float z = originZ + boneDef.localZ;
74 body->newBoxShape(boneDef.halfExtentX * 2.f, boneDef.halfExtentY * 2.f, boneDef.halfExtentZ * 2.f,
75 boneDef.density, boneDef.friction, boneDef.restitution);
76 auto link = PhysicsLink::fromBody(*
body);
79 if (
body->isValid())
body->destroy();
83 bone.state = BoneRuntimeState::Attached;
84 bone.anchored = boneDef.anchoredDefault;
85 bone.link = link.value();
86 bone.halfExtentX = boneDef.halfExtentX;
87 bone.halfExtentY = boneDef.halfExtentY;
88 bone.halfExtentZ = boneDef.halfExtentZ;
89 bone.clusterId = boneDef.clusterId;
90 bone.fractureLevel = boneDef.fractureLevel;
91 instance->bones_.push_back(
bone);
92 createdBodies.push_back(
body);
95 for (
const auto& edgeDef : asset.
edges) {
97 edge.boneA = edgeDef.boneA;
98 edge.boneB = edgeDef.boneB;
99 edge.strainThreshold = edgeDef.strainThreshold;
100 instance->edges_.push_back(
edge);
106bool GeometryCollectionInstance::hasLiveWorld() const noexcept {
return resolveWorld() !=
nullptr; }
108World3D* GeometryCollectionInstance::resolveWorld() const noexcept {
110 if (!
world || !
world->isValid())
return nullptr;
115 if (boneIndex < 0 || boneIndex >=
static_cast<int>(bones_.size()))
118 World3D*
world = resolveWorld();
122 return bones_[
static_cast<std::size_t
>(boneIndex)].link.resolve(*
world);
125float GeometryCollectionInstance::falloffAt(
const DestructionField& field,
float x,
float y,
float z)
const {
126 const float dx =
x - field.centerX;
127 const float dy =
y - field.centerY;
128 const float dz =
z - field.centerZ;
129 const float dist = std::sqrt(
dx *
dx +
dy *
dy +
dz *
dz);
130 if (!(field.radius > 0.f) || !std::isfinite(field.radius))
return 0.f;
131 if (dist > field.radius)
return 0.f;
132 if (field.falloff == DestructionFieldFalloff::None)
return 1.f;
133 return 1.f - dist / field.radius;
137 if (!std::isfinite(field.centerX) || !std::isfinite(field.centerY) || !std::isfinite(field.centerZ) ||
138 !std::isfinite(field.radius) || field.radius < 0.f || !std::isfinite(field.magnitude))
141 if (field.kind == DestructionFieldKind::Impulse &&
142 (!std::isfinite(field.dirX) || !std::isfinite(field.dirY) || !std::isfinite(field.dirZ)))
147 orphanedPhysics_ =
true;
153 int sleepsThisCall = 0;
154 bool needGraphSync =
false;
155 for (std::size_t i = 0; i < bones_.size(); ++i) {
156 auto bodyResult = bones_[i].link.resolve(*
world);
157 if (!bodyResult)
continue;
160 if (
weight <= 0.f)
continue;
161 switch (field.kind) {
162 case DestructionFieldKind::Anchor: {
163 const bool wasSleeping = bones_[i].state == BoneRuntimeState::Sleeping;
164 bones_[i].anchored =
true;
165 bones_[i].state = BoneRuntimeState::Attached;
166 body->setType(
"static");
167 if (wasSleeping) ++sleepBatchRevision_;
169 needGraphSync =
true;
172 case DestructionFieldKind::Impulse: {
174 if (bones_[i].anchored || bones_[i].
state == BoneRuntimeState::Attached)
break;
175 float dx = field.dirX,
dy = field.dirY,
dz = field.dirZ;
176 const float len = std::sqrt(
dx *
dx +
dy *
dy +
dz *
dz);
186 const float impulse = field.magnitude *
weight;
187 const bool wasSleeping = bones_[i].state == BoneRuntimeState::Sleeping;
188 body->setType(
"dynamic");
189 body->applyLinearImpulse(
dx * impulse,
dy * impulse,
dz * impulse);
190 bones_[i].state = BoneRuntimeState::Detached;
191 if (wasSleeping) ++sleepBatchRevision_;
195 case DestructionFieldKind::Sleep: {
196 if (bones_[i].
state != BoneRuntimeState::Detached)
break;
197 const float vx =
body->getLinearVelocityX();
198 const float vy =
body->getLinearVelocityY();
199 const float vz =
body->getLinearVelocityZ();
200 const float speed = std::sqrt(
vx *
vx +
vy *
vy +
vz *
vz);
201 if (speed > kSleepSpeed)
break;
202 if (budget_.maxSleepsPerStep > 0 && sleepsThisCall >= budget_.maxSleepsPerStep) {
206 body->setType(
"static");
207 if (bones_[i].
state != BoneRuntimeState::Sleeping) ++sleepBatchRevision_;
208 bones_[i].state = BoneRuntimeState::Sleeping;
213 case DestructionFieldKind::Strain:
217 if (needGraphSync) syncBoneActivationFromGraph();
219 if (field.kind == DestructionFieldKind::Strain) {
220 for (
auto&
edge : edges_) {
221 if (
edge.broken)
continue;
222 if (
edge.boneA < 0 ||
edge.boneB < 0 ||
edge.boneA >=
static_cast<int>(bones_.size()) ||
223 edge.boneB >=
static_cast<int>(bones_.size()))
225 auto bodyA = bones_[
static_cast<std::size_t
>(
edge.boneA)].link.resolve(*
world);
226 auto bodyB = bones_[
static_cast<std::size_t
>(
edge.boneB)].link.resolve(*
world);
227 if (!bodyA || !bodyB)
continue;
228 const float midX = 0.5f * (bodyA.value()->getX() + bodyB.value()->getX());
229 const float midY = 0.5f * (bodyA.value()->getY() + bodyB.value()->getY());
230 const float midZ = 0.5f * (bodyA.value()->getZ() + bodyB.value()->getZ());
231 const float weight = falloffAt(field, midX, midY, midZ);
232 if (
weight <= 0.f)
continue;
233 edge.accumulatedStrain += field.magnitude *
weight;
241 auto&
edge = edges_[
static_cast<std::size_t
>(edgeIndex)];
242 if (
edge.broken)
return;
248 event.boneB =
edge.boneB;
249 event.tick =
static_cast<std::int64_t
>(
tick);
250 detachEvents_.push_back(event);
252 if (
edge.boneA >= 0 &&
edge.boneB >= 0 &&
edge.boneA <
static_cast<int>(bones_.size()) &&
253 edge.boneB <
static_cast<int>(bones_.size())) {
254 const int clusterA = bones_[
static_cast<std::size_t
>(
edge.boneA)].
clusterId;
255 const int clusterB = bones_[
static_cast<std::size_t
>(
edge.boneB)].
clusterId;
256 if (clusterA != clusterB) {
262 clusterEvent.
tick =
static_cast<std::int64_t
>(
tick);
263 clusterBreakEvents_.push_back(clusterEvent);
269void GeometryCollectionInstance::syncBoneActivationFromGraph() {
270 const int boneCount =
static_cast<int>(bones_.size());
271 if (boneCount == 0)
return;
273 std::vector<std::vector<int>> adjacency(
static_cast<std::size_t
>(boneCount));
274 for (
const auto&
edge : edges_) {
275 if (
edge.broken)
continue;
276 if (
edge.boneA < 0 ||
edge.boneB < 0 ||
edge.boneA >= boneCount ||
edge.boneB >= boneCount)
continue;
277 adjacency[
static_cast<std::size_t
>(
edge.boneA)].push_back(
edge.boneB);
278 adjacency[
static_cast<std::size_t
>(
edge.boneB)].push_back(
edge.boneA);
281 std::vector<int> component(
static_cast<std::size_t
>(boneCount), -1);
282 int componentCount = 0;
283 bool anyAnchored =
false;
284 for (
int i = 0; i < boneCount; ++i) {
285 if (bones_[
static_cast<std::size_t
>(i)].anchored) anyAnchored =
true;
286 if (component[
static_cast<std::size_t
>(i)] >= 0)
continue;
287 std::queue<int> queue;
289 component[
static_cast<std::size_t
>(i)] = componentCount;
290 while (!queue.empty()) {
291 const int cur = queue.front();
293 for (
int next : adjacency[static_cast<
std::size_t>(cur)]) {
294 if (component[
static_cast<std::size_t
>(next)] >= 0)
continue;
295 component[
static_cast<std::size_t
>(
next)] = componentCount;
302 std::vector<bool> componentHasAnchor(
static_cast<std::size_t
>(componentCount),
false);
303 for (
int i = 0; i < boneCount; ++i) {
304 if (bones_[
static_cast<std::size_t
>(i)].anchored)
305 componentHasAnchor[
static_cast<std::size_t
>(component[
static_cast<std::size_t
>(i)])] =
true;
309 for (
int i = 0; i < boneCount; ++i) {
310 auto&
bone = bones_[
static_cast<std::size_t
>(i)];
311 if (
bone.state == BoneRuntimeState::Sleeping)
continue;
312 const bool stayAttached =
314 componentHasAnchor[
static_cast<std::size_t
>(component[
static_cast<std::size_t
>(i)])] ||
315 (!anyAnchored && componentCount == 1);
317 stayAttached ? BoneRuntimeState::Attached : BoneRuntimeState::Detached;
318 if (
bone.state == desired) {
320 if (desired == BoneRuntimeState::Attached &&
world) {
322 if (
body) setBoneBodyType(*
body.value(), desired);
326 bone.state = desired;
327 if (!
world)
continue;
329 if (
body) setBoneBodyType(*
body.value(), desired);
335 if (
step.delta.nanoseconds() < 0)
338 const std::uint64_t
tick =
step.tick.value();
339 if (lastTick_ != 0 &&
tick <= lastTick_)
344 orphanedPhysics_ =
true;
348 orphanedPhysics_ =
false;
349 detachEvents_.clear();
350 clusterBreakEvents_.clear();
352 std::vector<int> toBreak = pendingEdgeBreaks_;
353 pendingEdgeBreaks_.clear();
354 for (std::size_t i = 0; i < edges_.size(); ++i) {
355 const auto&
edge = edges_[i];
356 if (
edge.broken)
continue;
357 if (
edge.accumulatedStrain >=
edge.strainThreshold) toBreak.push_back(
static_cast<int>(i));
359 std::sort(toBreak.begin(), toBreak.end());
360 toBreak.erase(std::unique(toBreak.begin(), toBreak.end()), toBreak.end());
363 budget_.maxEdgeBreaksPerStep > 0 ? budget_.maxEdgeBreaksPerStep :
static_cast<int>(toBreak.size());
364 int brokenThisStep = 0;
365 for (
int edgeIndex : toBreak) {
366 if (edgeIndex < 0 || edgeIndex >=
static_cast<int>(edges_.size()))
continue;
367 if (edges_[
static_cast<std::size_t
>(edgeIndex)].broken)
continue;
368 if (brokenThisStep >= budget) {
369 pendingEdgeBreaks_.push_back(edgeIndex);
373 breakEdge(edgeIndex,
tick, receipt);
376 if (receipt.
edgesBroken > 0) syncBoneActivationFromGraph();
382 if (boneIndex < 0 || boneIndex >=
static_cast<int>(bones_.size()))
return BoneRuntimeState::Attached;
383 return bones_[
static_cast<std::size_t
>(boneIndex)].
state;
386int GeometryCollectionInstance::boneClusterId(
int boneIndex)
const {
387 if (boneIndex < 0 || boneIndex >=
static_cast<int>(bones_.size()))
return -1;
388 return bones_[
static_cast<std::size_t
>(boneIndex)].
clusterId;
391float GeometryCollectionInstance::edgeStrain(
int edgeIndex)
const {
392 if (edgeIndex < 0 || edgeIndex >=
static_cast<int>(edges_.size()))
return 0.f;
393 return edges_[
static_cast<std::size_t
>(edgeIndex)].accumulatedStrain;
396bool GeometryCollectionInstance::isEdgeBroken(
int edgeIndex)
const {
397 if (edgeIndex < 0 || edgeIndex >=
static_cast<int>(edges_.size()))
return false;
398 return edges_[
static_cast<std::size_t
>(edgeIndex)].broken;
402 if (index < 0 || index >=
static_cast<int>(detachEvents_.size()))
return {};
403 return detachEvents_[
static_cast<std::size_t
>(
index)];
407 if (index < 0 || index >=
static_cast<int>(clusterBreakEvents_.size()))
return {};
408 return clusterBreakEvents_[
static_cast<std::size_t
>(
index)];
412 if (boneIndex < 0 || boneIndex >=
static_cast<int>(bones_.size()))
419 auto body = bones_[
static_cast<std::size_t
>(boneIndex)].link.resolve(*
world);
425 if (boneIndex < 0 || boneIndex >=
static_cast<int>(bones_.size()))
432 const auto&
bone = bones_[
static_cast<std::size_t
>(boneIndex)];
443 presentation.
rotX =
body.value()->getRotX();
444 presentation.
rotY =
body.value()->getRotY();
445 presentation.
rotZ =
body.value()->getRotZ();
446 presentation.
rotW =
body.value()->getRotW();
457 snapshot.
lastTick =
static_cast<std::int64_t
>(lastTick_);
459 snapshot.
bones.reserve(bones_.size());
460 for (
const auto&
bone : bones_) {
462 boneSnap.
state =
static_cast<std::uint8_t
>(
bone.state);
469 snapshot.
bones.push_back(boneSnap);
471 snapshot.
edges.reserve(edges_.size());
472 for (
const auto&
edge : edges_) {
479 snapshot.
edges.push_back(edgeSnap);
489 if (snapshot.
bones.size() != bones_.size() || snapshot.
edges.size() != edges_.size())
492 "instance-snapshot bone/edge counts must match the live instance",
"snapshot"));
495 orphanedPhysics_ =
true;
502 std::vector<BoneRuntime> stagedBones = bones_;
503 std::vector<EdgeRuntime> stagedEdges = edges_;
504 std::vector<Body3D*> bodies(snapshot.
bones.size(),
nullptr);
505 for (std::size_t i = 0; i < snapshot.
bones.size(); ++i) {
506 const auto& src = snapshot.
bones[i];
508 stagedBones[i].anchored = src.anchored;
509 stagedBones[i].clusterId = src.clusterId;
510 stagedBones[i].fractureLevel = src.fractureLevel;
511 stagedBones[i].halfExtentX = src.halfExtentX;
512 stagedBones[i].halfExtentY = src.halfExtentY;
513 stagedBones[i].halfExtentZ = src.halfExtentZ;
514 auto body = stagedBones[i].link.resolve(*
world);
516 bodies[i] =
body.value();
518 for (std::size_t i = 0; i < snapshot.
edges.size(); ++i) {
519 const auto& src = snapshot.
edges[i];
520 if (src.boneA != stagedEdges[i].boneA || src.boneB != stagedEdges[i].boneB)
523 stagedEdges[i].strainThreshold = src.strainThreshold;
524 stagedEdges[i].accumulatedStrain = src.accumulatedStrain;
525 stagedEdges[i].broken = src.broken;
528 for (std::size_t i = 0; i < stagedBones.size(); ++i) {
530 switch (stagedBones[i].
state) {
531 case BoneRuntimeState::Attached:
532 body->
setType(stagedBones[i].anchored ?
"static" :
"dynamic");
534 case BoneRuntimeState::Detached:
535 body->setType(
"dynamic");
536 body->setAwake(
true);
538 case BoneRuntimeState::Sleeping:
539 body->setType(
"static");
544 bones_ = std::move(stagedBones);
545 edges_ = std::move(stagedEdges);
549 lastTick_ =
static_cast<std::uint64_t
>(snapshot.
lastTick);
551 detachEvents_.clear();
552 clusterBreakEvents_.clear();
553 pendingEdgeBreaks_.clear();
554 orphanedPhysics_ =
false;
558void GeometryCollectionInstance::destroyOwnedBodies(
World3D&
world) {
559 for (
auto&
bone : bones_) {
561 if (
body &&
body.value() &&
body.value()->isValid())
body.value()->destroy();
566void GeometryCollectionInstance::releaseBodies() {
568 destroyOwnedBodies(*
world);
570 for (
auto&
bone : bones_)
bone.link = {};
571 orphanedPhysics_ =
true;
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.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
3D rigid body (Box3D) in meter-space coordinates (+Y up by convention). Owned by a World3D; create pr...
void setType(const std::string &bodyType)
"static" | "kinematic" | "dynamic".
Runtime owner of bone state, connection strain, and PhysicsLink bindings.
GeometryCollectionInstance(const GeometryCollectionInstance &)=delete
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Optional physics backend for vehicle mobility and body attach.
BoneRuntimeState
Runtime lifecycle for one bone inside a geometry-collection instance.
eve::RuntimeHandle< PhysicsWorldHandleTag > PhysicsWorldHandle
Generation-qualified runtime identity for a physics world.
One deterministic fixed-step emitted by SimulationClock.
One ordered detach event emitted during step.
Presentation snapshot for one bone (world pose + proxy extents).
Emitted when a broken edge separates two different cook clusters.
One volumetric influence applied to a geometry-collection instance.
Observable outcome of one checked step.
Observable receipt for one checked field application.
int sleepsDeferred
Sleep conversions skipped because of the per-step sleep budget.
Immutable cooked/authored geometry-collection asset.
std::vector< GeometryCollectionEdge > edges
std::vector< GeometryCollectionBone > bones
eve::Result< void > validate() const
Validate bone/edge topology and numeric ranges.
Persistent runtime snapshot for one geometry-collection instance.
std::uint64_t sleepBatchRevision
std::vector< BoneSnapshot > bones
eve::Result< void > validate() const
Validate counts and numeric ranges.
std::vector< EdgeSnapshot > edges