12#include <box3d/box3d.h>
18#include <unordered_map>
19#include <unordered_set>
25class ScopedQueryState3D {
27 ScopedQueryState3D(World3D&
world, QueryFilter3D
filter)
28 : world_(
world), category_(
world.getQueryCategoryBits()), mask_(
world.getQueryMaskBits()),
29 body_(
world.getQueryIgnoredBodyId()), shape_(
world.getQueryIgnoredShapeId()) {
30 world_.setQueryFilter(
static_cast<int>(
filter.categoryBits),
static_cast<int>(
filter.maskBits));
31 world_.setQueryIgnoredBodyId(
filter.ignoredBodyId);
32 world_.setQueryIgnoredShapeId(
filter.ignoredShapeId);
35 ~ScopedQueryState3D() noexcept {
36 world_.setQueryFilter(category_, mask_);
37 world_.setQueryIgnoredBodyId(body_);
38 world_.setQueryIgnoredShapeId(shape_);
51 std::string
path = {}) {
59constexpr uint64_t combineModeMask = 0x7ull;
60constexpr int frictionModeShift = 32;
61constexpr int restitutionModeShift = 35;
63float combineMaterialValue(
float a, uint64_t materialA,
float b, uint64_t materialB,
64 int shift,
bool friction) {
65 const uint64_t modeA = (materialA >> shift) & combineModeMask;
66 const uint64_t modeB = (materialB >> shift) & combineModeMask;
67 const uint64_t mode = std::max(modeA, modeB);
69 case 1:
return 0.5f * (
a +
b);
70 case 2:
return std::min(
a,
b);
72 case 4:
return std::max(
a,
b);
73 default:
return friction ? std::sqrt(
a *
b) :
std::
max(
a,
b);
77float mixFriction(
float a, uint64_t materialA,
float b, uint64_t materialB) {
78 return combineMaterialValue(
a, materialA,
b, materialB, frictionModeShift,
true);
81float mixRestitution(
float a, uint64_t materialA,
float b, uint64_t materialB) {
82 return combineMaterialValue(
a, materialA,
b, materialB, restitutionModeShift,
false);
85uint64_t stablePairKey(
int idA,
int idB) {
86 const uint32_t low =
static_cast<uint32_t
>(std::min(idA, idB));
87 const uint32_t high =
static_cast<uint32_t
>(std::max(idA, idB));
88 return (
static_cast<uint64_t
>(high) << 32) | low;
91bool pairContains(uint64_t
key,
int id) {
92 const uint32_t
value =
static_cast<uint32_t
>(
id);
93 return static_cast<uint32_t
>(
key) ==
value ||
static_cast<uint32_t
>(
key >> 32) ==
value;
96b3BodyType parseBodyType(
const std::string &
type) {
97 if (
type ==
"static")
return b3_staticBody;
98 if (
type ==
"kinematic")
return b3_kinematicBody;
99 if (
type ==
"dynamic")
return b3_dynamicBody;
100 throw eve::Exception(
"World3D.newBody: unknown body type '%s' (use static|kinematic|dynamic)",
104Body3D *bodyFromShape(b3ShapeId shapeId) {
105 if (!b3Shape_IsValid(shapeId))
return nullptr;
106 return static_cast<Body3D *
>(b3Body_GetUserData(b3Shape_GetBody(shapeId)));
109Shape3D *shapeFromShape(b3ShapeId shapeId) {
110 if (!b3Shape_IsValid(shapeId))
return nullptr;
111 return static_cast<Shape3D *
>(b3Shape_GetUserData(shapeId));
114b3Quat normalizedQueryQuat(
float qx,
float qy,
float qz,
float qw,
const char *
operation) {
115 if (!std::isfinite(
qx) || !std::isfinite(
qy) || !std::isfinite(
qz) || !std::isfinite(
qw))
118 if (!(lengthSquared > 1e-12f))
120 const float inverseLength = 1.f / std::sqrt(lengthSquared);
121 return b3Quat{{
qx * inverseLength,
qy * inverseLength,
qz * inverseLength},
132 for (
int z = -1;
z <= 1;
z += 2)
133 for (
int y = -1;
y <= 1;
y += 2)
134 for (
int x = -1;
x <= 1;
x += 2)
136 q, b3Vec3{
static_cast<float>(
x) * hx,
static_cast<float>(
y) * hy,
137 static_cast<float>(
z) * hz});
141 auto *worldId =
static_cast<b3WorldId *
>(
context);
142 b3World_Step(*worldId,
static_cast<float>(
step.delta.seconds()),
settings.subStepCount);
146 if (!std::isfinite(dt) || dt < 0.f) {
149 "World3D update dt must be finite and non-negative",
"physics.world3d.update.dt"));
155 "World3D simulation tick cannot be incremented",
"physics.world3d.simulationTick"));
162struct WorldRegistry {
164 std::unordered_map<PhysicsWorldHandle::index_type, World3D*>
worlds;
166WorldRegistry& worldRegistry() {
167 static auto* registry =
new WorldRegistry;
174 :
World3D(gravityX, gravityY, gravityZ, sleep, instanceId, true) {}
177 bool registerQueries)
178 : instanceId_(instanceId) {
179 b3WorldDef def = b3DefaultWorldDef();
180 def.gravity = b3Vec3{gravityX, gravityY, gravityZ};
181 def.enableSleep = sleep;
182 contactHertz_ = def.contactHertz;
183 contactDampingRatio_ = def.contactDampingRatio;
184 contactPushOutSpeed_ = def.contactSpeed;
187 worldId_ = b3CreateWorld(&def);
188 auto selection = detail::selectSimulationBackend(
195 throw eve::Exception(
"World3D: cannot select a simulation backend: %s",
status.describe().c_str());
197 backendSelectionStatus_ = selection.status();
198 auto selected = std::move(selection).takeValue();
199 backendFallback_ = selected.usedFallback;
200 simulation_ = std::move(selected.backend);
203 :
eve::Result<void>::success();
204 if (!registerQueries) {
205 targetingRegistration.ignore(
"detached topology candidate does not publish targeting queries");
206 }
else if (!targetingRegistration) {
210 targetingRegistration.ignore(
"another World3D is already the active targeting LOS world");
212 b3World_SetFrictionCallback(worldId_, &mixFriction);
213 b3World_SetRestitutionCallback(worldId_, &mixRestitution);
214 b3World_SetCustomFilterCallback(worldId_, &World3D::customFilterCallback,
this);
215 b3World_SetPreSolveCallback(worldId_, &World3D::preSolveCallback,
this);
216 auto& registry = worldRegistry();
217 std::scoped_lock registryLock(registry.mutex);
218 registry.worlds[runtimeHandle_.
index()] =
this;
226 return isValid() && b3World_IsContinuousEnabled(worldId_);
230 if (!std::isfinite(speed) || speed < 0.f)
231 throw eve::Exception(
"World3D.setRestitutionThreshold: speed must be finite and >= 0");
232 if (
isValid()) b3World_SetRestitutionThreshold(worldId_, speed);
236 return isValid() ? b3World_GetRestitutionThreshold(worldId_) : 0.f;
240 if (!std::isfinite(hertz) || !(hertz > 0.f))
241 throw eve::Exception(
"World3D.setContactTuning: hertz must be finite and > 0");
242 if (!std::isfinite(dampingRatio) || dampingRatio < 0.f)
243 throw eve::Exception(
"World3D.setContactTuning: dampingRatio must be finite and >= 0");
244 if (!std::isfinite(pushOutSpeed) || pushOutSpeed < 0.f)
245 throw eve::Exception(
"World3D.setContactTuning: pushOutSpeed must be finite and >= 0");
246 contactHertz_ = hertz;
247 contactDampingRatio_ = dampingRatio;
248 contactPushOutSpeed_ = pushOutSpeed;
249 if (
isValid()) b3World_SetContactTuning(worldId_, hertz, dampingRatio, pushOutSpeed);
254 throw eve::Exception(
"World3D.setContactRecycleDistance: distance must be finite and >= 0");
259 return isValid() ? b3World_GetContactRecycleDistance(worldId_) : 0.f;
263 if (!std::isfinite(speed) || !(speed > 0.f))
264 throw eve::Exception(
"World3D.setMaximumLinearSpeed: speed must be finite and > 0");
265 if (
isValid()) b3World_SetMaximumLinearSpeed(worldId_, speed);
269 return isValid() ? b3World_GetMaximumLinearSpeed(worldId_) : 0.f;
277 return isValid() && b3World_IsWarmStartingEnabled(worldId_);
281 float impulsePerArea,
int maskBits) {
282 explosionResults_.clear();
283 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z))
285 if (!std::isfinite(
radius) ||
radius < 0.f || !std::isfinite(falloff) || falloff < 0.f)
286 throw eve::Exception(
"World3D.explode: radius and falloff must be finite and >= 0");
287 if (!std::isfinite(impulsePerArea))
288 throw eve::Exception(
"World3D.explode: impulsePerArea must be finite");
289 if (!
isValid() || impulsePerArea == 0.f)
return 0;
296 std::vector<Before> before;
297 before.reserve(bodies_.size());
299 if (!
body || !
body->isValid() || b3Body_GetType(
body->raw()) != b3_dynamicBody)
301 before.push_back({
body, b3Body_GetLinearVelocity(
body->raw()),
302 b3Body_GetAngularVelocity(
body->raw())});
305 b3ExplosionDef def = b3DefaultExplosionDef();
306 def.position = b3Pos{
x,
y,
z};
308 def.falloff = falloff;
309 def.impulsePerArea = impulsePerArea;
310 def.maskBits =
static_cast<uint32_t
>(maskBits);
311 b3World_Explode(worldId_, &def);
313 for (
const Before &entry : before) {
314 if (!entry.body->isValid())
continue;
315 const b3Vec3 linear = b3Body_GetLinearVelocity(entry.body->raw());
316 const b3Vec3 angular = b3Body_GetAngularVelocity(entry.body->raw());
317 const b3Vec3 dv = linear - entry.linear;
318 const b3Vec3 dw = angular - entry.angular;
319 if (b3LengthSquared(dv) <= 1e-12f && b3LengthSquared(dw) <= 1e-12f)
continue;
320 explosionResults_.push_back({entry.body->getId(), dv.x, dv.y, dv.z,
323 std::sort(explosionResults_.begin(), explosionResults_.end(),
324 [](
const ExplosionResult &
a,
const ExplosionResult &
b) {
325 return a.bodyId < b.bodyId;
327 return static_cast<int>(explosionResults_.size());
330const World3D::ExplosionResult &World3D::explosionResultAt(
int index,
332 if (index < 0 || index >=
static_cast<int>(explosionResults_.size()))
334 return explosionResults_[
static_cast<size_t>(
index)];
337#define EV_EXPLOSION_INT_GETTER(name, member) \
338 int World3D::name(int index) const { return explosionResultAt(index, #name).member; }
341#undef EV_EXPLOSION_INT_GETTER
343#define EV_EXPLOSION_FLOAT_GETTER(name, member) \
344 float World3D::name(int index) const { return explosionResultAt(index, #name).member; }
352#undef EV_EXPLOSION_FLOAT_GETTER
355 return isValid() ?
static_cast<float>(b3World_GetBounds(worldId_).lowerBound.x) : 0.f;
358 return isValid() ?
static_cast<float>(b3World_GetBounds(worldId_).lowerBound.y) : 0.f;
361 return isValid() ?
static_cast<float>(b3World_GetBounds(worldId_).lowerBound.z) : 0.f;
364 return isValid() ?
static_cast<float>(b3World_GetBounds(worldId_).upperBound.x) : 0.f;
367 return isValid() ?
static_cast<float>(b3World_GetBounds(worldId_).upperBound.y) : 0.f;
370 return isValid() ?
static_cast<float>(b3World_GetBounds(worldId_).upperBound.z) : 0.f;
373#define EV_COUNTER_GETTER(name, member) \
374 int World3D::name() const { return isValid() ? b3World_GetCounters(worldId_).member : 0; }
386#undef EV_COUNTER_GETTER
389 return isValid() ? b3World_GetAwakeBodyCount(worldId_) : 0;
392#define EV_PROFILE_GETTER(name, member) \
393 float World3D::name() const { return isValid() ? b3World_GetProfile(worldId_).member : 0.f; }
401#undef EV_PROFILE_GETTER
405void World3D::adoptPreparedTopology(
World3D &prepared) {
406 std::swap(worldId_, prepared.worldId_);
407 std::swap(nextId_, prepared.nextId_);
408 std::swap(nextShapeId_, prepared.nextShapeId_);
409 std::swap(nextJointId_, prepared.nextJointId_);
410 std::swap(bodies_, prepared.bodies_);
411 std::swap(shapes_, prepared.shapes_);
412 std::swap(joints_, prepared.joints_);
413 std::swap(shapeRecords_, prepared.shapeRecords_);
414 std::swap(shapeHandles_, prepared.shapeHandles_);
415 std::swap(shapeRawHandles_, prepared.shapeRawHandles_);
416 std::swap(jointHandles_, prepared.jointHandles_);
417 std::swap(simulationTick_, prepared.simulationTick_);
420 for (
Joint3D *joint : joints_) joint->world_ = this;
423 for (
Joint3D *joint : prepared.joints_) joint->world_ = &prepared;
424 b3World_SetCustomFilterCallback(worldId_, &World3D::customFilterCallback,
this);
425 b3World_SetPreSolveCallback(worldId_, &World3D::preSolveCallback,
this);
426 b3World_SetCustomFilterCallback(prepared.worldId_, &World3D::customFilterCallback, &prepared);
427 b3World_SetPreSolveCallback(prepared.worldId_, &World3D::preSolveCallback, &prepared);
433 if (destroyed_)
return;
435 auto& registry = worldRegistry();
436 std::scoped_lock registryLock(registry.mutex);
437 auto found = registry.worlds.find(runtimeHandle_.
index());
438 if (
found != registry.worlds.end() &&
found->second ==
this) registry.worlds.erase(
found);
443 queryLifetime_.reset();
445 std::vector<Mechanism3D *> mechanisms(mechanisms_.begin(), mechanisms_.end());
447 if (mechanism) mechanism->invalidate();
451 std::vector<Joint3D *> joints(joints_.begin(), joints_.end());
452 for (
Joint3D *joint : joints) {
453 if (joint) joint->invalidate();
456 disabledBodyPairs_.clear();
457 disabledShapePairs_.clear();
459 std::vector<Body3D *> bodies(bodies_.begin(), bodies_.end());
461 if (
b)
b->invalidate();
465 std::vector<Shape3D *> shapes(shapes_.begin(), shapes_.end());
467 if (
s)
s->invalidate();
472 if (b3World_IsValid(worldId_)) {
473 b3DestroyWorld(worldId_);
476 shapeRecords_.clear();
477 shapeHandles_.clear();
478 shapeRawHandles_.clear();
479 jointHandles_.clear();
485 if (
handle.isInvalid())
return nullptr;
486 auto& registry = worldRegistry();
487 std::scoped_lock registryLock(registry.mutex);
489 if (
found == registry.worlds.end() || !
found->second ||
found->second->runtimeHandle() !=
handle)
return nullptr;
490 return found->second;
494 float radius, uint64_t maskBits,
int ignoredBodyId,
501 int ignoredBodyId = -1;
503 uint64_t,
int,
int,
void*
context) {
504 auto* self =
static_cast<Collector*
>(
context);
506 if (!
body ||
body->getId() == self->ignoredBodyId)
return -1.f;
507 if (!self->out->hit || fraction < self->out->
fraction) {
508 self->out->hit =
true;
509 self->out->bodyId =
body->getId();
510 self->out->fraction = fraction;
511 self->out->x =
static_cast<float>(
point.x);
512 self->out->y =
static_cast<float>(
point.y);
513 self->out->z =
static_cast<float>(
point.z);
522 const b3Vec3
point{0.f, 0.f, 0.f};
524 b3QueryFilter
filter = b3DefaultQueryFilter();
525 filter.maskBits = maskBits;
526 b3World_CastShape(worldId_, b3Pos{x1, y1, z1}, &proxy,
527 b3Vec3{x2 - x1, y2 - y1, z2 - z1},
filter,
533 float anchorAY,
float anchorAZ,
float anchorBX,
534 float anchorBY,
float anchorBZ,
float length,
535 bool collideConnected) {
538 throw eve::Exception(
"World3D.newDistanceJoint: bodies must be distinct and belong to this world");
539 const float values[] = {anchorAX, anchorAY, anchorAZ, anchorBX, anchorBY, anchorBZ,
length};
541 if (!std::isfinite(
value))
542 throw eve::Exception(
"World3D.newDistanceJoint: parameters must be finite");
545 throw eve::Exception(
"World3D.newDistanceJoint: length must be >= 0");
546 b3DistanceJointDef def = b3DefaultDistanceJointDef();
547 def.base.bodyIdA = bodyA->
raw();
548 def.base.bodyIdB = bodyB->
raw();
549 def.base.localFrameA.p = b3Body_GetLocalPoint(bodyA->
raw(), b3Pos{anchorAX, anchorAY, anchorAZ});
550 def.base.localFrameB.p = b3Body_GetLocalPoint(bodyB->
raw(), b3Pos{anchorBX, anchorBY, anchorBZ});
551 def.base.collideConnected = collideConnected;
554 b3JointId
id = b3CreateDistanceJoint(worldId_, &def);
556 b3Joint_SetUserData(
id, joint);
557 joints_.insert(joint);
563 float anchorY,
float anchorZ,
float axisX,
float axisY,
564 float axisZ,
bool collideConnected) {
567 throw eve::Exception(
"World3D.newRevoluteJoint: bodies must be distinct and belong to this world");
570 if (!std::isfinite(
value))
571 throw eve::Exception(
"World3D.newRevoluteJoint: parameters must be finite");
574 if (axisLength <= 1e-8f)
575 throw eve::Exception(
"World3D.newRevoluteJoint: axis length must be > 0");
576 const b3Vec3 worldAxis{
axisX / axisLength,
axisY / axisLength,
axisZ / axisLength};
577 b3RevoluteJointDef def = b3DefaultRevoluteJointDef();
578 def.base.bodyIdA = bodyA->
raw();
579 def.base.bodyIdB = bodyB->
raw();
580 const b3Pos
anchor{anchorX, anchorY, anchorZ};
581 def.base.localFrameA.p = b3Body_GetLocalPoint(bodyA->
raw(),
anchor);
582 def.base.localFrameB.p = b3Body_GetLocalPoint(bodyB->
raw(),
anchor);
583 const b3Vec3 localAxisA = b3Body_GetLocalVector(bodyA->
raw(), worldAxis);
584 const b3Vec3 localAxisB = b3Body_GetLocalVector(bodyB->
raw(), worldAxis);
585 def.base.localFrameA.q = b3ComputeQuatBetweenUnitVectors(b3Vec3_axisZ, localAxisA);
586 def.base.localFrameB.q = b3ComputeQuatBetweenUnitVectors(b3Vec3_axisZ, localAxisB);
587 def.base.collideConnected = collideConnected;
589 b3JointId
id = b3CreateRevoluteJoint(worldId_, &def);
591 b3Joint_SetUserData(
id, joint);
592 joints_.insert(joint);
598 float anchorY,
float anchorZ,
float axisX,
float axisY,
599 float axisZ,
bool collideConnected) {
602 throw eve::Exception(
"World3D.newPrismaticJoint: bodies must be distinct and belong to this world");
605 if (!std::isfinite(
value))
606 throw eve::Exception(
"World3D.newPrismaticJoint: parameters must be finite");
609 if (axisLength <= 1e-8f)
610 throw eve::Exception(
"World3D.newPrismaticJoint: axis length must be > 0");
611 const b3Vec3 worldAxis{
axisX / axisLength,
axisY / axisLength,
axisZ / axisLength};
612 const b3Pos
anchor{anchorX, anchorY, anchorZ};
613 b3PrismaticJointDef def = b3DefaultPrismaticJointDef();
614 def.base.bodyIdA = bodyA->
raw();
615 def.base.bodyIdB = bodyB->
raw();
616 def.base.localFrameA.p = b3Body_GetLocalPoint(bodyA->
raw(),
anchor);
617 def.base.localFrameB.p = b3Body_GetLocalPoint(bodyB->
raw(),
anchor);
618 def.base.localFrameA.q = b3ComputeQuatBetweenUnitVectors(
619 b3Vec3_axisX, b3Body_GetLocalVector(bodyA->
raw(), worldAxis));
620 def.base.localFrameB.q = b3ComputeQuatBetweenUnitVectors(
621 b3Vec3_axisX, b3Body_GetLocalVector(bodyB->
raw(), worldAxis));
622 def.base.collideConnected = collideConnected;
624 const b3JointId
id = b3CreatePrismaticJoint(worldId_, &def);
626 b3Joint_SetUserData(
id, joint);
627 joints_.insert(joint);
633 float anchorY,
float anchorZ,
float axisX,
float axisY,
634 float axisZ,
bool collideConnected) {
637 throw eve::Exception(
"World3D.newSphericalJoint: bodies must be distinct and belong to this world");
640 if (!std::isfinite(
value))
641 throw eve::Exception(
"World3D.newSphericalJoint: parameters must be finite");
644 if (axisLength <= 1e-8f)
645 throw eve::Exception(
"World3D.newSphericalJoint: axis length must be > 0");
646 const b3Vec3 worldAxis{
axisX / axisLength,
axisY / axisLength,
axisZ / axisLength};
647 const b3Pos
anchor{anchorX, anchorY, anchorZ};
648 b3SphericalJointDef def = b3DefaultSphericalJointDef();
649 def.base.bodyIdA = bodyA->
raw();
650 def.base.bodyIdB = bodyB->
raw();
651 def.base.localFrameA.p = b3Body_GetLocalPoint(bodyA->
raw(),
anchor);
652 def.base.localFrameB.p = b3Body_GetLocalPoint(bodyB->
raw(),
anchor);
653 def.base.localFrameA.q = b3ComputeQuatBetweenUnitVectors(
654 b3Vec3_axisZ, b3Body_GetLocalVector(bodyA->
raw(), worldAxis));
655 def.base.localFrameB.q = b3ComputeQuatBetweenUnitVectors(
656 b3Vec3_axisZ, b3Body_GetLocalVector(bodyB->
raw(), worldAxis));
657 def.base.collideConnected = collideConnected;
659 const b3JointId
id = b3CreateSphericalJoint(worldId_, &def);
661 b3Joint_SetUserData(
id, joint);
662 joints_.insert(joint);
668 float anchorY,
float anchorZ,
float suspensionAxisX,
669 float suspensionAxisY,
float suspensionAxisZ,
float wheelAxisX,
670 float wheelAxisY,
float wheelAxisZ,
bool collideConnected) {
673 throw eve::Exception(
"World3D.newWheelJoint: bodies must be distinct and belong to this world");
674 const float values[] = {anchorX, anchorY, anchorZ, suspensionAxisX, suspensionAxisY,
675 suspensionAxisZ, wheelAxisX, wheelAxisY, wheelAxisZ};
677 if (!std::isfinite(
value))
678 throw eve::Exception(
"World3D.newWheelJoint: parameters must be finite");
680 const float suspensionLength = std::sqrt(suspensionAxisX * suspensionAxisX +
681 suspensionAxisY * suspensionAxisY +
682 suspensionAxisZ * suspensionAxisZ);
683 const float wheelLength =
684 std::sqrt(wheelAxisX * wheelAxisX + wheelAxisY * wheelAxisY + wheelAxisZ * wheelAxisZ);
685 if (suspensionLength <= 1e-8f || wheelLength <= 1e-8f)
686 throw eve::Exception(
"World3D.newWheelJoint: both axis lengths must be > 0");
687 const b3Vec3 suspensionAxis{suspensionAxisX / suspensionLength,
688 suspensionAxisY / suspensionLength,
689 suspensionAxisZ / suspensionLength};
690 const b3Vec3 wheelAxis{wheelAxisX / wheelLength, wheelAxisY / wheelLength,
691 wheelAxisZ / wheelLength};
692 if (std::fabs(b3Dot(suspensionAxis, wheelAxis)) > 0.999f)
693 throw eve::Exception(
"World3D.newWheelJoint: suspension and wheel axes must not be parallel");
694 const b3Pos
anchor{anchorX, anchorY, anchorZ};
695 b3WheelJointDef def = b3DefaultWheelJointDef();
696 def.base.bodyIdA = bodyA->
raw();
697 def.base.bodyIdB = bodyB->
raw();
698 def.base.localFrameA.p = b3Body_GetLocalPoint(bodyA->
raw(),
anchor);
699 def.base.localFrameB.p = b3Body_GetLocalPoint(bodyB->
raw(),
anchor);
700 def.base.localFrameA.q = b3ComputeQuatBetweenUnitVectors(
701 b3Vec3_axisX, b3Body_GetLocalVector(bodyA->
raw(), suspensionAxis));
702 def.base.localFrameB.q = b3ComputeQuatBetweenUnitVectors(
703 b3Vec3_axisZ, b3Body_GetLocalVector(bodyB->
raw(), wheelAxis));
704 def.base.collideConnected = collideConnected;
706 const b3JointId
id = b3CreateWheelJoint(worldId_, &def);
708 b3Joint_SetUserData(
id, joint);
709 joints_.insert(joint);
716 joints_.erase(joint);
718 for (
auto it = jointHandles_.begin(); it != jointHandles_.end();) {
719 if (it->second == joint)
720 it = jointHandles_.erase(it);
730 auto legacyStep = makeLegacyStep(dt, simulationTick_);
732 legacyStep.ignore(
"legacy World3D::updateFull cannot return a structured error");
736 result.ignore(
"legacy World3D::updateFull cannot return a structured result");
740 if (!
isValid() || !simulation_) {
743 "physics.world3d.step"));
745 auto valid = detail::validateSimulationStep(stepValue,
settings, simulation_->observation());
748 if (mechanism) mechanism->syncBeforeStep();
753 auto result = simulation_->step(stepValue,
settings);
754 if (!result)
return result;
755 simulationTick_ = stepValue.
tick;
777 b3World_SetGravity(worldId_, b3Vec3{gx, gy, gz});
782 return b3World_GetGravity(worldId_).x;
787 return b3World_GetGravity(worldId_).y;
792 return b3World_GetGravity(worldId_).z;
796 if (!std::isfinite(speed) || speed < 0.f)
797 throw eve::Exception(
"World3D.setHitEventThreshold: speed must be finite and >= 0");
798 if (
isValid()) b3World_SetHitEventThreshold(worldId_, speed);
802 return isValid() ? b3World_GetHitEventThreshold(worldId_) : 0.f;
807 b3Shape_SetFilter(
shape->raw(), b3Shape_GetFilter(
shape->raw()),
true);
811 if (!bodyA || !bodyB || bodyA == bodyB || !bodyA->
isValid() || !bodyB->
isValid() ||
814 "World3D.setBodyPairCollisionEnabled: distinct live bodies must belong to this world");
815 const uint64_t
key = stablePairKey(bodyA->
getId(), bodyB->
getId());
818 shape->enableContactOverrideFiltering();
820 if (
enabled) disabledBodyPairs_.erase(
key);
else disabledBodyPairs_.insert(
key);
823 refreshContactFilter(
shape);
828 if (!bodyA || !bodyB || bodyA == bodyB || !bodyA->
isValid() || !bodyB->
isValid() ||
831 "World3D.isBodyPairCollisionEnabled: distinct live bodies must belong to this world");
832 return disabledBodyPairs_.count(stablePairKey(bodyA->
getId(), bodyB->
getId())) == 0;
836 if (!shapeA || !shapeB || shapeA == shapeB || !shapeA->
isValid() || !shapeB->
isValid() ||
840 "World3D.setShapePairCollisionEnabled: distinct live shapes must belong to this world");
841 const uint64_t
key = stablePairKey(shapeA->
getId(), shapeB->
getId());
842 shapeA->enableContactOverrideFiltering();
843 shapeB->enableContactOverrideFiltering();
844 if (
enabled) disabledShapePairs_.erase(
key);
else disabledShapePairs_.insert(
key);
845 refreshContactFilter(shapeA);
846 refreshContactFilter(shapeB);
850 if (!shapeA || !shapeB || shapeA == shapeB || !shapeA->
isValid() || !shapeB->
isValid() ||
854 "World3D.isShapePairCollisionEnabled: distinct live shapes must belong to this world");
855 return disabledShapePairs_.count(stablePairKey(shapeA->
getId(), shapeB->
getId())) == 0;
858bool World3D::customFilterCallback(b3ShapeId shapeIdA, b3ShapeId shapeIdB,
void *
context) {
860 Shape3D *shapeA = shapeFromShape(shapeIdA);
861 Shape3D *shapeB = shapeFromShape(shapeIdB);
862 Body3D *bodyA = bodyFromShape(shapeIdA);
863 Body3D *bodyB = bodyFromShape(shapeIdB);
864 if (!
world || !shapeA || !shapeB || !bodyA || !bodyB)
return false;
865 if (
world->disabledBodyPairs_.count(stablePairKey(bodyA->
getId(), bodyB->
getId())) != 0)
867 return world->disabledShapePairs_.count(stablePairKey(shapeA->
getId(), shapeB->
getId())) == 0;
870void World3D::removeCollisionOverridesForBody(
int bodyId) {
871 for (
auto it = disabledBodyPairs_.begin(); it != disabledBodyPairs_.end();) {
872 if (pairContains(*it, bodyId)) it = disabledBodyPairs_.erase(it);
else ++it;
876void World3D::removeCollisionOverridesForShape(
int shapeId) {
877 for (
auto it = disabledShapePairs_.begin(); it != disabledShapePairs_.end();) {
878 if (pairContains(*it, shapeId)) it = disabledShapePairs_.erase(it);
else ++it;
886 throw eve::Exception(
"World3D.newBody: process-local body handle space exhausted");
892 throw eve::Exception(
"Body3D.newShape: process-local shape handle space exhausted");
898 throw eve::Exception(
"World3D.newJoint: process-local joint handle space exhausted");
905 b3BodyDef def = b3DefaultBodyDef();
906 def.type = parseBodyType(bodyType);
907 def.position = b3Pos{
x,
y,
z};
908 def.rotation = b3Quat_identity;
911 b3BodyId
raw = b3CreateBody(worldId_, &def);
914 bodies_.insert(
body);
927 if (!
isValid() || bodyId < 0)
return nullptr;
937 if (
found == shapeHandles_.end() || !
found->second || !
found->second->isValid())
return nullptr;
938 return found->second;
942 if (!
isValid() || shapeId < 0)
return nullptr;
952 if (
found == jointHandles_.end() || !
found->second || !
found->second->isValid())
return nullptr;
953 return found->second;
956b3QueryFilter World3D::makeQueryFilter()
const {
957 b3QueryFilter
filter = b3DefaultQueryFilter();
958 filter.categoryBits = queryCategoryBits_;
959 filter.maskBits = queryMaskBits_;
963bool World3D::shouldIgnoreQueryShape(b3ShapeId shapeId)
const {
967 return body->getId() == queryIgnoredBodyId_ ||
shape->getId() == queryIgnoredShapeId_;
971 queryCategoryBits_ =
static_cast<uint32_t
>(categoryBits);
972 queryMaskBits_ =
static_cast<uint32_t
>(maskBits);
976 queryCategoryBits_ = 0xFFFFFFFFu;
977 queryMaskBits_ = 0xFFFFFFFFu;
981 if (bodyId == 0 || bodyId < -1)
982 throw eve::Exception(
"World3D.setQueryIgnoredBodyId: use a positive stable id or -1");
983 queryIgnoredBodyId_ = bodyId;
987 if (shapeId == 0 || shapeId < -1)
988 throw eve::Exception(
"World3D.setQueryIgnoredShapeId: use a positive stable id or -1");
989 queryIgnoredShapeId_ = shapeId;
993 queryIgnoredBodyId_ = -1;
994 queryIgnoredShapeId_ = -1;
1003 if (
body) removeCollisionOverridesForBody(
body->getId());
1004 bodies_.erase(
body);
1008 removeCollisionOverridesForShape(
shape->getId());
1009 shapes_.erase(
shape);
1012 for (
auto it = shapeHandles_.begin(); it != shapeHandles_.end();) {
1013 if (it->second ==
shape)
1014 it = shapeHandles_.erase(it);
1018 for (
auto it = shapeRawHandles_.begin(); it != shapeRawHandles_.end();) {
1019 if (it->second ==
handle)
1020 it = shapeRawHandles_.erase(it);
1027 if (!
shape || !
shape->getBody() || B3_IS_NULL(
shape->raw()) ||
shape->runtimeHandle().isInvalid())
return;
1029 for (
auto it = shapeHandles_.begin(); it != shapeHandles_.end();) {
1031 it = shapeHandles_.erase(it);
1035 for (
auto it = shapeRawHandles_.begin(); it != shapeRawHandles_.end();) {
1036 if (it->second ==
handle)
1037 it = shapeRawHandles_.erase(it);
1041 const uint64_t rawKey = b3StoreShapeId(
shape->raw());
1043 shapeRawHandles_[rawKey] =
handle;
1047bool World3D::preSolveCallback(b3ShapeId shapeIdA, b3ShapeId shapeIdB, b3Pos
point,
1050 if (!
world)
return true;
1052 const auto raw =
world->shapeRawHandles_.find(b3StoreShapeId(
id));
1053 if (
raw ==
world->shapeRawHandles_.end())
return nullptr;
1059 if (shapeA && shapeA->isOneWay() &&
1062 if (shapeB && shapeB->isOneWay() &&
1071 for (
auto &[
key, record] : shapeRecords_) {
1073 if (record.shapeId ==
shape->getId()) record.shapeTag =
shape->getTag();
1078 auto found = shapeRecords_.find(b3StoreShapeId(shapeId));
1079 if (
found != shapeRecords_.end())
return found->second;
1083 return EventShape{
body->getId(),
shape->getId(),
shape->getTag()};
1087 beginContacts_.clear();
1088 endContacts_.clear();
1089 beginTriggers_.clear();
1090 endTriggers_.clear();
1092 jointStressEvents_.clear();
1093 contactPoints_.clear();
1097 contactPoints_.clear();
1099 throw eve::Exception(
"World3D.queryBodyContacts: bodyId must be a positive stable id");
1100 constexpr int maxContactPoints = 4096;
1101 if (maxPoints < 1 || maxPoints > maxContactPoints)
1102 throw eve::Exception(
"World3D.queryBodyContacts: maxPoints must be in [1, 4096]");
1107 if (
body &&
body->isValid() &&
body->getId() == bodyId) {
1114 constexpr int maxContactPairs = 4096;
1115 const int capacity = std::min(b3Body_GetContactCapacity(
target->raw()), maxContactPairs);
1117 std::vector<b3ContactData> contacts(
static_cast<size_t>(
capacity));
1118 const int contactCount = b3Body_GetContactData(
target->raw(), contacts.data(),
capacity);
1119 contactPoints_.reserve(
static_cast<size_t>(std::min(maxPoints, contactCount * 2)));
1121 for (
int contactIndex = 0; contactIndex < contactCount; ++contactIndex) {
1122 const b3ContactData &contact = contacts[
static_cast<size_t>(contactIndex)];
1123 Shape3D *shapeA = shapeFromShape(contact.shapeIdA);
1124 Shape3D *shapeB = shapeFromShape(contact.shapeIdB);
1125 Body3D *bodyA = bodyFromShape(contact.shapeIdA);
1126 Body3D *bodyB = bodyFromShape(contact.shapeIdB);
1127 if (!shapeA || !shapeB || !bodyA || !bodyB)
continue;
1128 const bool targetIsA = bodyA ==
target;
1129 if (!targetIsA && bodyB !=
target)
continue;
1130 Shape3D *ownShape = targetIsA ? shapeA : shapeB;
1131 Shape3D *otherShape = targetIsA ? shapeB : shapeA;
1132 Body3D *otherBody = targetIsA ? bodyB : bodyA;
1133 const b3Pos centerOfMass = b3Body_GetWorldCenterOfMass(
target->raw());
1135 for (
int manifoldIndex = 0; manifoldIndex < contact.manifoldCount; ++manifoldIndex) {
1136 const b3Manifold &manifold = contact.manifolds[manifoldIndex];
1137 const b3Vec3
normal = targetIsA ? manifold.normal : -manifold.normal;
1138 for (
int pointIndex = 0; pointIndex < manifold.pointCount; ++pointIndex) {
1139 if (
static_cast<int>(contactPoints_.size()) >= maxContactPoints)
break;
1140 const b3ManifoldPoint &
point = manifold.points[pointIndex];
1141 const b3Pos worldPoint =
1142 b3OffsetPos(centerOfMass, targetIsA ?
point.anchorA :
point.anchorB);
1143 contactPoints_.push_back({ownShape->
getId(),
1146 otherShape->
getId(),
1149 static_cast<float>(worldPoint.x),
1150 static_cast<float>(worldPoint.y),
1151 static_cast<float>(worldPoint.z),
1156 point.normalImpulse,
1157 point.totalNormalImpulse,
1158 point.normalVelocity,
1164 std::sort(contactPoints_.begin(), contactPoints_.end(),
1165 [](
const ContactPointResult &
a,
const ContactPointResult &
b) {
1166 if (a.shapeId != b.shapeId) return a.shapeId < b.shapeId;
1167 if (a.otherShapeId != b.otherShapeId) return a.otherShapeId < b.otherShapeId;
1168 if (a.featureId != b.featureId) return a.featureId < b.featureId;
1169 if (a.x != b.x) return a.x < b.x;
1170 if (a.y != b.y) return a.y < b.y;
1173 if (
static_cast<int>(contactPoints_.size()) > maxPoints)
1174 contactPoints_.resize(
static_cast<size_t>(maxPoints));
1175 return static_cast<int>(contactPoints_.size());
1181 auto *ev = eve::ModuleManager::getInstance<eve::platform_event::PlatformEvent>(
"PlatformEvent");
1183 b3ContactEvents contacts = b3World_GetContactEvents(worldId_);
1184 for (
int i = 0; i < contacts.beginCount; ++i) {
1185 EventShape a = eventShapeFrom(contacts.beginEvents[i].shapeIdA);
1186 EventShape b = eventShapeFrom(contacts.beginEvents[i].shapeIdB);
1187 if (
a.shapeId == 0 ||
b.shapeId == 0)
continue;
1188 if (
a.shapeId >
b.shapeId) std::swap(
a,
b);
1189 beginContacts_.push_back({
a,
b});
1191 std::vector<eve::platform_event::Variant> args = {
1198 for (
int i = 0; i < contacts.endCount; ++i) {
1199 EventShape a = eventShapeFrom(contacts.endEvents[i].shapeIdA);
1200 EventShape b = eventShapeFrom(contacts.endEvents[i].shapeIdB);
1201 if (
a.shapeId == 0 ||
b.shapeId == 0)
continue;
1202 if (
a.shapeId >
b.shapeId) std::swap(
a,
b);
1203 endContacts_.push_back({
a,
b});
1205 std::vector<eve::platform_event::Variant> args = {
1213 for (
int i = 0; i < contacts.hitCount; ++i) {
1214 const b3ContactHitEvent &
source = contacts.hitEvents[i];
1217 if (
a.shapeId == 0 ||
b.shapeId == 0)
continue;
1219 float normalImpulse = 0.f;
1220 if (b3Contact_IsValid(
source.contactId)) {
1221 const b3ContactData data = b3Contact_GetData(
source.contactId);
1222 for (
int manifoldIndex = 0; manifoldIndex < data.manifoldCount; ++manifoldIndex) {
1223 const b3Manifold &manifold = data.manifolds[manifoldIndex];
1224 for (
int pointIndex = 0; pointIndex < manifold.pointCount; ++pointIndex)
1225 normalImpulse += manifold.points[pointIndex].totalNormalImpulse;
1230 if (
a.shapeId >
b.shapeId) {
1236 static_cast<float>(
source.point.x),
1237 static_cast<float>(
source.point.y),
1238 static_cast<float>(
source.point.z),
1245 std::vector<eve::platform_event::Variant> args = {
1253 b3SensorEvents sensors = b3World_GetSensorEvents(worldId_);
1254 for (
int i = 0; i < sensors.beginCount; ++i) {
1255 EventShape sensor = eventShapeFrom(sensors.beginEvents[i].sensorShapeId);
1256 EventShape visitor = eventShapeFrom(sensors.beginEvents[i].visitorShapeId);
1258 beginTriggers_.push_back({sensor, visitor});
1269 for (
int i = 0; i < sensors.endCount; ++i) {
1270 EventShape sensor = eventShapeFrom(sensors.endEvents[i].sensorShapeId);
1271 EventShape visitor = eventShapeFrom(sensors.endEvents[i].visitorShapeId);
1273 endTriggers_.push_back({sensor, visitor});
1285 const b3JointEvents jointEvents = b3World_GetJointEvents(worldId_);
1286 for (
int i = 0; i < jointEvents.count; ++i) {
1287 const b3JointEvent &
source = jointEvents.jointEvents[i];
1289 if (!joint || joints_.find(joint) == joints_.end() || !joint->isValid() ||
1290 joint->raw().index1 !=
source.jointId.index1 ||
1291 joint->raw().world0 !=
source.jointId.world0 ||
1292 joint->raw().generation !=
source.jointId.generation)
1294 const b3Vec3 force = b3Joint_GetConstraintForce(
source.jointId);
1295 const b3Vec3 torque = b3Joint_GetConstraintTorque(
source.jointId);
1296 jointStressEvents_.push_back(
1297 {joint->getId(), joint->getBodyAId(), joint->getBodyBId(),
1298 static_cast<int>(joint->kind_), force.x, force.y, force.z, torque.x, torque.y,
1301 std::vector<eve::platform_event::Variant> args = {
1312 for (
auto it = shapeRecords_.begin(); it != shapeRecords_.end();) {
1313 if (!b3Shape_IsValid(b3LoadShapeId(it->first)))
1314 it = shapeRecords_.erase(it);
1318 for (
auto it = shapeRawHandles_.begin(); it != shapeRawHandles_.end();) {
1319 if (!b3Shape_IsValid(b3LoadShapeId(it->first)))
1320 it = shapeRawHandles_.erase(it);
1324 for (
auto it = shapeHandles_.begin(); it != shapeHandles_.end();) {
1325 if (!it->second || !it->second->isValid())
1326 it = shapeHandles_.erase(it);
1334 return rayHitBodyId_;
1338 uint64_t maskBits) {
1339 b3QueryFilter
filter = b3DefaultQueryFilter();
1340 filter.maskBits = maskBits;
1341 rayCastAllInternal(x1, y1, z1, x2, y2, z2, 1,
filter);
1342 return rayHitBodyId_;
1349 "physics world is no longer valid",
"world");
1351 ScopedQueryState3D queryState(*
this,
filter);
1353 result.
world = runtimeHandle_;
1354 if (!std::isfinite(x1) || !std::isfinite(y1) || !std::isfinite(z1) ||
1355 !std::isfinite(x2) || !std::isfinite(y2) || !std::isfinite(z2))
1357 "ray endpoints must be finite",
"ray");
1362 uint64_t materialId,
int triangleIndex,
int,
void*
context) {
1363 auto* self =
static_cast<Collector*
>(
context);
1364 if (self->world->shouldIgnoreQueryShape(shapeId))
return -1.f;
1369 candidate.
hit =
true;
1370 candidate.
world = self->result->world;
1371 candidate.
body =
body->runtimeHandle();
1376 candidate.
materialId =
static_cast<int>(
static_cast<std::uint32_t
>(materialId));
1378 candidate.
x =
static_cast<float>(
point.x);
1379 candidate.
y =
static_cast<float>(
point.y);
1380 candidate.
z =
static_cast<float>(
point.z);
1385 if (!self->result->hit || fraction < self->result->
fraction ||
1386 (fraction == self->result->fraction && candidate.
shapeId < self->result->shapeId))
1387 *self->result = candidate;
1388 return self->result->fraction;
1391 b3World_CastRay(worldId_, b3Pos{x1, y1, z1}, b3Vec3{x2 - x1, y2 - y1, z2 - z1},
1392 makeQueryFilter(), &Collector::callback, &
collector);
1395 return ownedQueryFailure<RayHit3D>(
1397 "ray hit could not be mapped to live generation-qualified handles",
"hit");
1399 }
catch (
const std::exception&
error) {
1407 return rayCastAllInternal(x1, y1, z1, x2, y2, z2, maxHits, makeQueryFilter());
1410int World3D::rayCastAllInternal(
float x1,
float y1,
float z1,
float x2,
float y2,
1411 float z2,
int maxHits, b3QueryFilter
filter) {
1413 rayHitShapeId_ = -1;
1414 rayHitShapeTag_ = 0;
1415 rayHitMaterialId_ = 0;
1416 rayHitTriangleIndex_ = -1;
1420 rayHitNormalX_ = 0.f;
1421 rayHitNormalY_ = 0.f;
1422 rayHitNormalZ_ = 0.f;
1423 rayHitFraction_ = 0.f;
1424 rayResults_.clear();
1425 if (!std::isfinite(x1) || !std::isfinite(y1) || !std::isfinite(z1) ||
1426 !std::isfinite(x2) || !std::isfinite(y2) || !std::isfinite(z2))
1427 throw eve::Exception(
"World3D.rayCastAll: endpoints must be finite");
1428 constexpr int maxRayResults = 4096;
1429 if (maxHits < 1 || maxHits > maxRayResults)
1430 throw eve::Exception(
"World3D.rayCastAll: maxHits must be in [1, 4096]");
1433 b3Pos
origin{x1, y1, z1};
1434 b3Vec3 translation{x2 - x1, y2 - y1, z2 - z1};
1435 struct RayCollector {
1436 World3D *
world =
nullptr;
1439 static bool better(
const RayResult &
a,
const RayResult &
b) {
1440 return a.fraction <
b.fraction ||
1441 (
a.fraction ==
b.fraction &&
a.shapeId <
b.shapeId);
1444 float clipFraction()
const {
1445 if (
static_cast<int>(
world->rayResults_.size()) <
capacity)
return 1.f;
1446 const auto worst = std::max_element(
world->rayResults_.begin(),
1447 world->rayResults_.end(), better);
1448 return worst->fraction;
1452 uint64_t materialId,
int triangleIndex,
int,
void *
context) {
1453 auto *self =
static_cast<RayCollector *
>(
context);
1454 if (self->world->shouldIgnoreQueryShape(shapeId))
return -1.f;
1455 Body3D *
body = bodyFromShape(shapeId);
1456 Shape3D *
shape = shapeFromShape(shapeId);
1459 RayResult candidate{
body->getId(),
1462 static_cast<int>(
static_cast<uint32_t
>(materialId)),
1464 static_cast<float>(
point.x),
1465 static_cast<float>(
point.y),
1466 static_cast<float>(
point.z),
1471 auto &results = self->world->rayResults_;
1472 if (
static_cast<int>(results.size()) < self->capacity) {
1473 results.push_back(candidate);
1475 auto worst = std::max_element(results.begin(), results.end(), better);
1476 if (better(candidate, *worst)) *worst = candidate;
1478 return self->clipFraction();
1481 rayResults_.reserve(
static_cast<size_t>(maxHits));
1483 std::sort(rayResults_.begin(), rayResults_.end(), RayCollector::better);
1484 if (!rayResults_.empty()) {
1485 const RayResult &
hit = rayResults_.front();
1486 rayHitBodyId_ =
hit.bodyId;
1487 rayHitShapeId_ =
hit.shapeId;
1488 rayHitShapeTag_ =
hit.shapeTag;
1489 rayHitMaterialId_ =
hit.materialId;
1490 rayHitTriangleIndex_ =
hit.triangleIndex;
1494 rayHitNormalX_ =
hit.normalX;
1495 rayHitNormalY_ =
hit.normalY;
1496 rayHitNormalZ_ =
hit.normalZ;
1497 rayHitFraction_ =
hit.fraction;
1499 return static_cast<int>(rayResults_.size());
1503 queryBodyIds_.clear();
1504 queryShapes_.clear();
1509 std::vector<int> *bodyIds =
nullptr;
1510 std::vector<std::pair<int, int>> *shapes =
nullptr;
1511 std::unordered_set<int> seen;
1514 auto *self =
static_cast<Collector *
>(
context);
1515 if (self->world->shouldIgnoreQueryShape(shapeId))
return true;
1516 Body3D *
b = bodyFromShape(shapeId);
1518 if (!
b || !
shape)
return true;
1519 int id =
b->getId();
1520 if (self->seen.insert(
id).second) self->bodyIds->push_back(
id);
1521 self->shapes->emplace_back(
shape->getId(),
shape->getTag());
1526 cb.bodyIds = &queryBodyIds_;
1527 cb.shapes = &queryShapes_;
1530 aabb.lowerBound = b3Vec3{std::min(minX, maxX), std::min(minY, maxY), std::min(minZ, maxZ)};
1531 aabb.upperBound = b3Vec3{std::max(minX, maxX), std::max(minY, maxY), std::max(minZ, maxZ)};
1532 b3QueryFilter
filter = makeQueryFilter();
1533 b3World_OverlapAABB(worldId_, aabb,
filter, &Collector::callback, &cb);
1534 std::sort(queryBodyIds_.begin(), queryBodyIds_.end());
1535 std::sort(queryShapes_.begin(), queryShapes_.end());
1536 return static_cast<int>(queryBodyIds_.size());
1540 float minX,
float minY,
float minZ,
float maxX,
float maxY,
float maxZ,
1544 "physics world is no longer valid",
"world");
1545 if (!std::isfinite(minX) || !std::isfinite(minY) || !std::isfinite(minZ) ||
1546 !std::isfinite(maxX) || !std::isfinite(maxY) || !std::isfinite(maxZ))
1548 "broad-phase AABB must be finite",
"aabb");
1550 ScopedQueryState3D queryState(*
this,
filter);
1561 auto* self =
static_cast<Collector*
>(
context);
1562 if (self->world->shouldIgnoreQueryShape(shapeId))
return true;
1568 auto&
output = *self->result;
1569 if (
output.count < BroadPhaseAabb3D::Capacity) {
1573 auto worst = std::max_element(
output.hits.begin(),
output.hits.end(), less);
1574 if (less(
hit, *worst)) *worst =
hit;
1580 bounds.lowerBound = {std::min(minX, maxX), std::min(minY, maxY), std::min(minZ, maxZ)};
1581 bounds.upperBound = {std::max(minX, maxX), std::max(minY, maxY), std::max(minZ, maxZ)};
1582 b3World_OverlapAABB(worldId_,
bounds, makeQueryFilter(), &Collector::callback, &
collector);
1583 std::sort(result.
hits.begin(), result.
hits.begin() +
static_cast<std::ptrdiff_t
>(result.
count),
1586 }
catch (
const std::exception&
error) {
1588 error.what(),
"aabb");
1592int World3D::overlapProxy(b3Pos
origin,
const b3ShapeProxy &proxy) {
1593 queryBodyIds_.clear();
1594 queryShapes_.clear();
1599 std::vector<int> *bodyIds =
nullptr;
1600 std::vector<std::pair<int, int>> *shapes =
nullptr;
1601 std::unordered_set<int> seen;
1604 auto *self =
static_cast<Collector *
>(
context);
1605 if (self->world->shouldIgnoreQueryShape(shapeId))
return true;
1609 if (self->seen.insert(
body->getId()).second)
1610 self->bodyIds->push_back(
body->getId());
1611 self->shapes->emplace_back(
shape->getId(),
shape->getTag());
1619 b3QueryFilter
filter = makeQueryFilter();
1622 std::sort(queryBodyIds_.begin(), queryBodyIds_.end());
1623 std::sort(queryShapes_.begin(), queryShapes_.end());
1624 return static_cast<int>(queryBodyIds_.size());
1628 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z) || !(
radius >= 0.f) ||
1630 throw eve::Exception(
"World3D.querySphere: coordinates and radius must be finite; radius >= 0");
1631 const b3Vec3
point = b3Vec3_zero;
1633 return overlapProxy(b3Pos{
x,
y,
z}, proxy);
1638 if (!std::isfinite(
ax) || !std::isfinite(
ay) || !std::isfinite(
az) || !std::isfinite(
bx) ||
1639 !std::isfinite(
by) || !std::isfinite(
bz) || !(
radius >= 0.f) || !std::isfinite(
radius))
1640 throw eve::Exception(
"World3D.queryCapsule: coordinates and radius must be finite; radius >= 0");
1644 return overlapProxy(b3Pos{
ax,
ay,
az}, proxy);
1652 "physics world is no longer valid",
"world");
1654 ScopedQueryState3D queryState(*
this,
filter);
1655 if (!std::isfinite(
ax) || !std::isfinite(
ay) || !std::isfinite(
az) ||
1656 !std::isfinite(
bx) || !std::isfinite(
by) || !std::isfinite(
bz) ||
1659 "capsule coordinates and radius are invalid",
"capsule");
1661 result.
world = runtimeHandle_;
1664 std::array<int, 256> bodyIds{};
1665 std::size_t
count = 0;
1667 auto* self =
static_cast<Collector*
>(
context);
1668 if (self->world->shouldIgnoreQueryShape(shapeId))
return true;
1670 if (!
body || !
body->isValid())
return true;
1671 const int bodyId =
body->getId();
1672 if (std::find(self->bodyIds.begin(),
1673 self->bodyIds.begin() +
static_cast<std::ptrdiff_t
>(self->count),
1674 bodyId) != self->bodyIds.begin() +
static_cast<std::ptrdiff_t
>(self->count))
1676 if (self->count < self->bodyIds.size()) self->bodyIds[self->count++] = bodyId;
1682 b3World_OverlapShape(worldId_, b3Pos{
ax,
ay,
az}, &proxy, makeQueryFilter(),
1686 }
catch (
const std::exception&
error) {
1688 error.what(),
"capsule");
1693 float qy,
float qz,
float qw) {
1694 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z))
1696 const b3Quat
rotation = normalizedQueryQuat(
qx,
qy,
qz,
qw,
"World3D.queryBox");
1699 const b3ShapeProxy proxy{
points, 8, 0.f};
1700 return overlapProxy(b3Pos{
x,
y,
z}, proxy);
1703int World3D::castProxyAll(b3Pos
origin,
const b3ShapeProxy &proxy, b3Vec3 translation,
1705 shapeCastBodyId_ = -1;
1706 shapeCastShapeId_ = -1;
1707 shapeCastShapeTag_ = 0;
1708 shapeCastMaterialId_ = 0;
1709 shapeCastTriangleIndex_ = -1;
1710 shapeCastX_ = shapeCastY_ = shapeCastZ_ = 0.f;
1711 shapeCastNormalX_ = shapeCastNormalY_ = shapeCastNormalZ_ = 0.f;
1712 shapeCastFraction_ = 0.f;
1713 shapeCastResults_.clear();
1714 constexpr int maxShapeCastResults = 4096;
1715 if (maxHits < 1 || maxHits > maxShapeCastResults)
1716 throw eve::Exception(
"World3D Shape Cast: maxHits must be in [1, 4096]");
1719 struct CastCollector {
1723 static bool better(
const ShapeCastResult &
a,
const ShapeCastResult &
b) {
1724 return a.fraction <
b.fraction ||
1725 (
a.fraction ==
b.fraction &&
a.shapeId <
b.shapeId);
1728 float clipFraction()
const {
1729 if (
static_cast<int>(
world->shapeCastResults_.size()) <
capacity)
return 1.f;
1730 const auto worst = std::max_element(
world->shapeCastResults_.begin(),
1731 world->shapeCastResults_.end(), better);
1732 return worst->fraction;
1736 uint64_t materialId,
int triangleIndex,
int,
void *
context) {
1737 auto *self =
static_cast<CastCollector *
>(
context);
1738 if (self->world->shouldIgnoreQueryShape(shapeId))
return -1.f;
1739 Body3D *
body = bodyFromShape(shapeId);
1740 Shape3D *
shape = shapeFromShape(shapeId);
1743 ShapeCastResult candidate{
body->getId(),
1746 static_cast<int>(
static_cast<uint32_t
>(materialId)),
1748 static_cast<float>(
point.x),
1749 static_cast<float>(
point.y),
1750 static_cast<float>(
point.z),
1755 auto &results = self->world->shapeCastResults_;
1756 if (
static_cast<int>(results.size()) < self->capacity) {
1757 results.push_back(candidate);
1759 auto worst = std::max_element(results.begin(), results.end(), better);
1760 if (better(candidate, *worst)) *worst = candidate;
1762 return self->clipFraction();
1765 shapeCastResults_.reserve(
static_cast<size_t>(maxHits));
1766 b3QueryFilter
filter = makeQueryFilter();
1767 b3World_CastShape(worldId_,
origin, &proxy, translation,
filter, &CastCollector::callback,
1769 std::sort(shapeCastResults_.begin(), shapeCastResults_.end(), CastCollector::better);
1770 if (!shapeCastResults_.empty()) {
1771 const ShapeCastResult &
hit = shapeCastResults_.front();
1772 shapeCastBodyId_ =
hit.bodyId;
1773 shapeCastShapeId_ =
hit.shapeId;
1774 shapeCastShapeTag_ =
hit.shapeTag;
1775 shapeCastMaterialId_ =
hit.materialId;
1776 shapeCastTriangleIndex_ =
hit.triangleIndex;
1777 shapeCastX_ =
hit.x;
1778 shapeCastY_ =
hit.y;
1779 shapeCastZ_ =
hit.z;
1780 shapeCastNormalX_ =
hit.normalX;
1781 shapeCastNormalY_ =
hit.normalY;
1782 shapeCastNormalZ_ =
hit.normalZ;
1783 shapeCastFraction_ =
hit.fraction;
1785 return static_cast<int>(shapeCastResults_.size());
1790 return shapeCastBodyId_;
1795 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z) || !(
radius >= 0.f) ||
1796 !std::isfinite(
radius) || !std::isfinite(
dx) || !std::isfinite(
dy) || !std::isfinite(
dz))
1797 throw eve::Exception(
"World3D.castSphere: all values must be finite; radius >= 0");
1798 const b3Vec3
point = b3Vec3_zero;
1800 return castProxyAll(b3Pos{
x,
y,
z}, proxy, b3Vec3{
dx,
dy,
dz}, maxHits);
1804 float dx,
float dy,
float dz) {
1805 castCapsuleAll(
ax,
ay,
az,
bx,
by,
bz,
radius,
dx,
dy,
dz, 1);
1806 return shapeCastBodyId_;
1810 float radius,
float dx,
float dy,
float dz,
int maxHits) {
1811 if (!std::isfinite(
ax) || !std::isfinite(
ay) || !std::isfinite(
az) || !std::isfinite(
bx) ||
1812 !std::isfinite(
by) || !std::isfinite(
bz) || !(
radius >= 0.f) || !std::isfinite(
radius) ||
1813 !std::isfinite(
dx) || !std::isfinite(
dy) || !std::isfinite(
dz))
1814 throw eve::Exception(
"World3D.castCapsule: all values must be finite; radius >= 0");
1817 return castProxyAll(b3Pos{
ax,
ay,
az}, proxy, b3Vec3{
dx,
dy,
dz}, maxHits);
1821 float qy,
float qz,
float qw,
float dx,
float dy,
float dz) {
1822 castBoxAll(
x,
y,
z,
width,
height,
depth,
qx,
qy,
qz,
qw,
dx,
dy,
dz, 1);
1823 return shapeCastBodyId_;
1827 float qy,
float qz,
float qw,
float dx,
float dy,
float dz,
int maxHits) {
1828 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z) || !std::isfinite(
dx) ||
1829 !std::isfinite(
dy) || !std::isfinite(
dz))
1830 throw eve::Exception(
"World3D.castBox: center and translation must be finite");
1831 const b3Quat
rotation = normalizedQueryQuat(
qx,
qy,
qz,
qw,
"World3D.castBox");
1834 const b3ShapeProxy proxy{
points, 8, 0.f};
1835 return castProxyAll(b3Pos{
x,
y,
z}, proxy, b3Vec3{
dx,
dy,
dz}, maxHits);
1839 closestBodyId_ = -1;
1840 closestShapeId_ = -1;
1841 closestShapeTag_ = 0;
1842 closestX_ = closestY_ = closestZ_ = 0.f;
1843 closestNormalX_ = closestNormalY_ = closestNormalZ_ = 0.f;
1844 closestDistance_ = 0.f;
1845 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z) ||
1846 !(maxDistance >= 0.f) || !std::isfinite(maxDistance))
1848 "World3D.closestPoint: target must be finite; maxDistance finite and >= 0");
1849 constexpr double floatLimit =
static_cast<double>(FLT_MAX);
1850 const double distance =
static_cast<double>(maxDistance);
1852 std::fabs(
static_cast<double>(
x)) +
distance > floatLimit ||
1853 std::fabs(
static_cast<double>(
y)) +
distance > floatLimit ||
1854 std::fabs(
static_cast<double>(
z)) +
distance > floatLimit)
1855 throw eve::Exception(
"World3D.closestPoint: search bounds are too large");
1858 struct ClosestCollector {
1860 b3Vec3
target = b3Vec3_zero;
1861 float bestDistanceSquared = 0.f;
1863 int stableShapeId = -1;
1865 b3Vec3
point = b3Vec3_zero;
1868 auto *self =
static_cast<ClosestCollector *
>(
context);
1869 if (self->world->shouldIgnoreQueryShape(shapeId))
return true;
1873 const b3Vec3
point = b3Shape_GetClosestPoint(shapeId, self->target);
1874 const b3Vec3 delta = self->target -
point;
1875 const float distanceSquared = b3LengthSquared(delta);
1876 const int bodyId =
body->getId();
1877 const int stableShapeId =
shape->getId();
1878 constexpr float tieTolerance = 1e-7f;
1879 if (distanceSquared < self->bestDistanceSquared - tieTolerance ||
1880 (std::fabs(distanceSquared - self->bestDistanceSquared) <= tieTolerance &&
1881 (self->bodyId < 0 || bodyId < self->bodyId ||
1882 (bodyId == self->bodyId && stableShapeId < self->stableShapeId)))) {
1883 self->bestDistanceSquared = distanceSquared;
1884 self->bodyId = bodyId;
1885 self->stableShapeId = stableShapeId;
1886 self->shapeTag =
shape->getTag();
1887 self->point =
point;
1894 collector.bestDistanceSquared = maxDistance * maxDistance;
1896 const b3Vec3 extent{maxDistance, maxDistance, maxDistance};
1898 const b3QueryFilter
filter = makeQueryFilter();
1903 closestShapeId_ =
collector.stableShapeId;
1908 closestDistance_ = std::sqrt(std::max(0.f,
collector.bestDistanceSquared));
1909 if (closestDistance_ > 1e-6f) {
1910 const float inverseDistance = 1.f / closestDistance_;
1911 closestNormalX_ = (
x - closestX_) * inverseDistance;
1912 closestNormalY_ = (
y - closestY_) * inverseDistance;
1913 closestNormalZ_ = (
z - closestZ_) * inverseDistance;
1915 return closestBodyId_;
1919 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z))
1920 throw eve::Exception(
"World3D.setMoverUp: values must be finite");
1921 const float lengthSquared =
x *
x +
y *
y +
z *
z;
1922 if (lengthSquared <= 1e-12f)
1923 throw eve::Exception(
"World3D.setMoverUp: direction must be non-zero");
1924 const float inverseLength = 1.f / std::sqrt(lengthSquared);
1925 moverUp_ = b3Vec3{
x * inverseLength,
y * inverseLength,
z * inverseLength};
1930 throw eve::Exception(
"World3D.setMoverSlopeLimit: degrees must be finite");
1932 moverSlopeCos_ = std::cos(
degrees * 0.01745329251994329577f);
1937 if (!std::isfinite(
ax) || !std::isfinite(
ay) || !std::isfinite(
az) || !std::isfinite(
bx) ||
1938 !std::isfinite(
by) || !std::isfinite(
bz) || !(
radius > 0.f) || !std::isfinite(
radius) ||
1939 !std::isfinite(
dx) || !std::isfinite(
dy) || !std::isfinite(
dz))
1940 throw eve::Exception(
"World3D.moveCapsule: all values must be finite; radius > 0");
1942 moverDeltaX_ = moverDeltaY_ = moverDeltaZ_ = 0.f;
1943 moverNormalX_ = moverNormalY_ = moverNormalZ_ = 0.f;
1944 moverPlaneCount_ = 0;
1945 moverIterations_ = 0;
1946 moverGroundDot_ = -1.f;
1947 moverGrounded_ =
false;
1950 constexpr int planeCapacity = 16;
1951 struct PlaneCollector {
1953 b3CollisionPlane
planes[planeCapacity]{};
1955 b3Vec3 desired = b3Vec3_zero;
1956 b3Vec3
up = b3Vec3_axisY;
1957 b3Vec3 bestNormal = b3Vec3_zero;
1958 float bestDot = 0.f;
1959 float maxUpDot = -1.f;
1961 static bool callback(b3ShapeId shapeId,
const b3PlaneResult *results,
int resultCount,
1963 auto *self =
static_cast<PlaneCollector *
>(
context);
1964 if (self->world->shouldIgnoreQueryShape(shapeId))
return true;
1965 for (
int i = 0; i < resultCount && self->count < planeCapacity; ++i) {
1966 const b3Plane &plane = results[i].plane;
1967 self->planes[self->count++] = b3CollisionPlane{
1968 .plane = plane, .pushLimit = FLT_MAX, .push = 0.f, .clipVelocity =
true};
1969 const float d = b3Dot(plane.normal, self->desired);
1970 if (d < self->bestDot) {
1972 self->bestNormal = plane.normal;
1974 self->maxUpDot = std::max(self->maxUpDot, b3Dot(plane.normal, self->up));
1986 b3QueryFilter
filter = makeQueryFilter();
1987 constexpr float toleranceSquared = 1e-8f;
1988 bool collided =
false;
1990 for (
int pass = 0; pass < 5; ++pass) {
1993 b3World_CollideMover(worldId_,
current, &mover,
filter, &PlaneCollector::callback,
1996 collided = collided ||
collector.count > 0;
1999 b3PlaneSolverResult solved = b3SolvePlanes(targetDelta,
collector.planes,
collector.count);
2000 b3Vec3 delta = solved.delta;
2001 struct MoverFilter {
2004 auto *self =
static_cast<MoverFilter *
>(
context);
2005 return !self->world->shouldIgnoreQueryShape(shapeId);
2007 } moverFilter{
this};
2008 const float fraction = b3World_CastMover(worldId_,
current, &mover, delta,
filter,
2009 &MoverFilter::callback, &moverFilter);
2010 if (fraction < 1.f) collided =
true;
2011 delta = fraction * delta;
2013 moverIterations_ = pass + 1;
2015 if (b3LengthSquared(delta) <= toleranceSquared)
break;
2016 if (b3LengthSquared(
target -
current) <= toleranceSquared)
break;
2020 moverDeltaX_ = total.x;
2021 moverDeltaY_ = total.y;
2022 moverDeltaZ_ = total.z;
2027 moverGrounded_ = moverGroundDot_ >= moverSlopeCos_;
2036 "physics world is no longer valid",
"world");
2037 const float upLengthSquared = policy.
upX * policy.
upX + policy.
upY * policy.
upY + policy.
upZ * policy.
upZ;
2038 if (!std::isfinite(policy.
upX) || !std::isfinite(policy.
upY) || !std::isfinite(policy.
upZ) ||
2039 upLengthSquared <= 1e-12f || !std::isfinite(policy.
maxSlopeRadians) ||
2042 "capsule mover policy is invalid",
"policy");
2043 const b3Vec3 previousUp = moverUp_;
2044 const float previousSlopeCos = moverSlopeCos_;
2046 ScopedQueryState3D queryState(*
this,
filter);
2047 const float inverseUpLength = 1.f / std::sqrt(upLengthSquared);
2048 moverUp_ = {policy.
upX * inverseUpLength, policy.
upY * inverseUpLength,
2049 policy.
upZ * inverseUpLength};
2052 result.
world = runtimeHandle_;
2063 moverUp_ = previousUp;
2064 moverSlopeCos_ = previousSlopeCos;
2066 }
catch (
const std::exception&
error) {
2067 moverUp_ = previousUp;
2068 moverSlopeCos_ = previousSlopeCos;
2070 error.what(),
"capsuleMove");
2075 if (index < 0 || index >=
static_cast<int>(queryBodyIds_.size()))
2076 throw eve::Exception(
"World3D.getQueryBodyId: index out of range");
2077 return queryBodyIds_[
static_cast<size_t>(
index)];
2085 uint64_t categoryBits, uint64_t maskBits)
const {
2091 if (!
s || !
s->isValid() ||
s->isSensor())
continue;
2092 if ((maskBits &
s->getCategoryBits()) == 0 || (
s->getMaskBits() & categoryBits) == 0)
continue;
2095 const std::string
kind =
s->getKind();
2096 if (
kind ==
"triangleMesh" ||
kind ==
"heightField")
continue;
2097 const b3Vec3 closest = b3Shape_GetClosestPoint(
s->raw(),
target);
2098 const b3Vec3 delta =
target - closest;
2099 const float d = std::sqrt(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
2105 out->
nx = delta.x /
d;
2106 out->
ny = delta.y /
d;
2107 out->
nz = delta.z /
d;
2113 out->
body =
s->getBody();
2121const World3D::RayResult &World3D::rayResultAt(
int index,
const char *
operation)
const {
2122 if (index < 0 || index >=
static_cast<int>(rayResults_.size()))
2124 return rayResults_[
static_cast<size_t>(
index)];
2127#define EV_RAY_INT_GETTER(name, member) \
2128 int World3D::name(int index) const { return rayResultAt(index, #name).member; }
2136#undef EV_RAY_INT_GETTER
2138#define EV_RAY_FLOAT_GETTER(name, member) \
2139 float World3D::name(int index) const { return rayResultAt(index, #name).member; }
2149#undef EV_RAY_FLOAT_GETTER
2151const World3D::ShapeCastResult &World3D::shapeCastResultAt(
int index,
2153 if (index < 0 || index >=
static_cast<int>(shapeCastResults_.size()))
2155 return shapeCastResults_[
static_cast<size_t>(
index)];
2158#define EV_CAST_INT_GETTER(name, member) \
2159 int World3D::name(int index) const { return shapeCastResultAt(index, #name).member; }
2167#undef EV_CAST_INT_GETTER
2169#define EV_CAST_FLOAT_GETTER(name, member) \
2170 float World3D::name(int index) const { return shapeCastResultAt(index, #name).member; }
2180#undef EV_CAST_FLOAT_GETTER
2183 if (index < 0 || index >=
static_cast<int>(queryShapes_.size()))
2184 throw eve::Exception(
"World3D.getQueryShapeId: index out of range");
2185 return queryShapes_[
static_cast<size_t>(
index)].
first;
2189 if (index < 0 || index >=
static_cast<int>(queryShapes_.size()))
2190 throw eve::Exception(
"World3D.getQueryShapeTag: index out of range");
2191 return queryShapes_[
static_cast<size_t>(
index)].
second;
2196 if (index < 0 || index >=
static_cast<int>(
events.size()))
2201const World3D::TriggerEvent &World3D::triggerEventAt(
const std::vector<TriggerEvent> &
events,
2203 if (index < 0 || index >=
static_cast<int>(
events.size()))
2208const World3D::HitEvent &World3D::hitEventAt(
int index,
const char *
operation)
const {
2209 if (index < 0 || index >=
static_cast<int>(hits_.size()))
2211 return hits_[
static_cast<size_t>(
index)];
2214const World3D::JointStressEvent &World3D::jointStressEventAt(
int index,
2216 if (index < 0 || index >=
static_cast<int>(jointStressEvents_.size()))
2218 return jointStressEvents_[
static_cast<size_t>(
index)];
2221#define EV_CONTACT_GETTER(name, events, member) \
2222 int World3D::name(int index) const { \
2223 return contactEventAt(events, index, #name).member; \
2239#undef EV_CONTACT_GETTER
2241#define EV_TRIGGER_GETTER(name, events, member) \
2242 int World3D::name(int index) const { \
2243 return triggerEventAt(events, index, #name).member; \
2249EV_TRIGGER_GETTER(getBeginTriggerVisitorShapeId, beginTriggers_, visitor.shapeId)
2250EV_TRIGGER_GETTER(getBeginTriggerSensorShapeTag, beginTriggers_, sensor.shapeTag)
2251EV_TRIGGER_GETTER(getBeginTriggerVisitorShapeTag, beginTriggers_, visitor.shapeTag)
2259#undef EV_TRIGGER_GETTER
2261#define EV_HIT_INT_GETTER(name, member) \
2262 int World3D::name(int index) const { return hitEventAt(index, #name).member; }
2271#undef EV_HIT_INT_GETTER
2273#define EV_HIT_FLOAT_GETTER(name, member) \
2274 float World3D::name(int index) const { return hitEventAt(index, #name).member; }
2285#undef EV_HIT_FLOAT_GETTER
2287#define EV_JOINT_STRESS_INT_GETTER(name, member) \
2288 int World3D::name(int index) const { return jointStressEventAt(index, #name).member; }
2293#undef EV_JOINT_STRESS_INT_GETTER
2295#define EV_JOINT_STRESS_FLOAT_GETTER(name, member) \
2296 float World3D::name(int index) const { return jointStressEventAt(index, #name).member; }
2303#undef EV_JOINT_STRESS_FLOAT_GETTER
2305const World3D::ContactPointResult &World3D::contactPointAt(
int index,
2307 if (index < 0 || index >=
static_cast<int>(contactPoints_.size()))
2309 return contactPoints_[
static_cast<size_t>(
index)];
2312#define EV_CONTACT_POINT_INT_GETTER(name, member) \
2313 int World3D::name(int index) const { return contactPointAt(index, #name).member; }
2321#undef EV_CONTACT_POINT_INT_GETTER
2323#define EV_CONTACT_POINT_FLOAT_GETTER(name, member) \
2324 float World3D::name(int index) const { return contactPointAt(index, #name).member; }
2337#undef EV_CONTACT_POINT_FLOAT_GETTER
2340 return contactPointAt(
index,
"isContactPointPersisted").persisted;
ActionParameterOperation operation
std::map< std::string, Var > values
wgpu::PopErrorScopeStatus status
std::array< double, 10 > q
std::array< float, 4 > rotation
std::shared_ptr< const std::vector< glm::vec2 > > points
#define EV_PROFILE_MODULE(module, name)
Profile the enclosing scope, tagged with a module for grouping.
RenderSystem3D::GpuOpaqueCollector collector
TerrainThermalSettings settings
const UnitySourceAsset & source
const VegetationPresetContext & context
#define EV_RAY_FLOAT_GETTER(name, member)
#define EV_CONTACT_GETTER(name, events, member)
#define EV_COUNTER_GETTER(name, member)
#define EV_CAST_INT_GETTER(name, member)
#define EV_HIT_FLOAT_GETTER(name, member)
#define EV_RAY_INT_GETTER(name, member)
#define EV_CONTACT_POINT_INT_GETTER(name, member)
#define EV_CONTACT_POINT_FLOAT_GETTER(name, member)
#define EV_TRIGGER_GETTER(name, events, member)
#define EV_CAST_FLOAT_GETTER(name, member)
#define EV_HIT_INT_GETTER(name, member)
#define EV_JOINT_STRESS_INT_GETTER(name, member)
#define EV_JOINT_STRESS_FLOAT_GETTER(name, member)
#define EV_PROFILE_GETTER(name, member)
std::unordered_map< PhysicsWorldHandle::index_type, World3D * > worlds
#define EV_EXPLOSION_FLOAT_GETTER(name, member)
#define EV_EXPLOSION_INT_GETTER(name, member)
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.
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
EVENGINE_API_FOUNDATION public API.
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.
constexpr bool isInvalid() const noexcept
Returns whether this value is the invalid sentinel.
constexpr bool isValid() const noexcept
Returns whether both coordinates are usable handle values.
constexpr index_type index() const noexcept
Returns the owner-local slot index.
static constexpr index_type invalidIndex
Reserved index value shared by all invalid handles.
static constexpr RuntimeHandle invalid() noexcept
Returns the canonical invalid handle.
Structured status and zero or more diagnostics for an operation.
constexpr bool isNil() const noexcept
Returns whether this value is the all-zero nil ID.
constexpr std::optional< StrongUint64 > incremented() const noexcept
Returns the next value, or empty instead of unsigned wraparound.
3D rigid body (Box3D) in meter-space coordinates (+Y up by convention). Owned by a World3D; create pr...
World3D * getWorld()
Returns the owning world, or null after invalidation.
bool isValid() const
True while the underlying Box3D body is still alive.
int getId() const
Stable id used by collision events.
Script-facing Box3D joint owned by a World3D.
PhysicsJointHandle runtimeHandle() const noexcept
Process-local generation-qualified handle owned by World3D.
Composed mechanical assembly owned by a World3D.
3D shape (box/sphere/capsule) attached to a Body3D with material settings. Created via Body3D::new*Sh...
int getId() const
Stable world-local id, preserved when sensor state recreates the backend shape.
int getTag() const
User-defined integer tag, zero by default.
Body3D * getBody()
Returns the owning body, or null after invalidation.
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
eve::Result< RayHit3D > rayCastOwned(float x1, float y1, float z1, float x2, float y2, float z2, QueryFilter3D filter={})
Performs one ray query and returns an owning, generation-qualified snapshot.
float getRestitutionThreshold() const
Current restitution speed threshold in m/s.
void setGravity(float gx, float gy, float gz)
Gravity in m/s².
int getQueryBodyId(int index) const
Body id of the i-th query hit.
int castSphere(float x, float y, float z, float radius, float dx, float dy, float dz)
Sweep a sphere through the world and return the earliest hit body id, or -1.
PhysicsJointHandle nextJointRuntimeHandle()
Internal: next generation-qualified joint handle.
bool isWarmStartingEnabled() const
Whether constraint warm starting is enabled.
SimulationObservation simulationObservation() const noexcept
Snapshot of completed backend steps and logical simulation time.
float getMaximumLinearSpeed() const
Current world-wide maximum linear speed in m/s.
int getAwakeBodyCount() const
Number of awake bodies.
int queryCapsule(float ax, float ay, float az, float bx, float by, float bz, float radius)
Query shapes overlapping an arbitrarily oriented capsule. The endpoints are the centers of its hemisp...
friend void registerCameraObstructionWorld(World3D *world)
Registers camera obstruction world.
void update(float dt)
Steps the simulation by dt seconds (default substep count).
int closestPoint(float x, float y, float z, float maxDistance)
Find the closest collision surface within maxDistance of a world point. Uses the current query catego...
Joint3D * findJoint(PhysicsJointHandle handle) const
Resolves a live joint handle; returns null when stale or foreign.
PhysicsWorldHandle runtimeHandle() const noexcept
Process-local identity used by PhysicsLink; invalid after destruction.
eve::Result< BroadPhaseAabb3D > queryAabbBroadPhaseOwned(float minX, float minY, float minZ, float maxX, float maxY, float maxZ, QueryFilter3D filter={})
Queries one AABB into a fixed owning result without using the legacy query cache.
int castBox(float x, float y, float z, float width, float height, float depth, float qx, float qy, float qz, float qw, float dx, float dy, float dz)
Sweep an oriented box without changing its rotation.
void updateShapeTag(Shape3D *shape)
Internal: refreshes the tag snapshot for a live shape handle.
float getMoverNormalY() const
Representative blocking normal Y from the last move.
eve::Result< void > step(const eve::SimulationStep &step, const SimulationSettings &settings={})
Advances the 3D domain with an injected deterministic step.
void registerShapeHandle(Shape3D *shape)
Internal: snapshots a backend handle for destruction-safe end events.
Shape3D * findShapeById(int shapeId) const
Resolves a live shape by its world-local stable event/query id.
float getBoundsMinZ() const
Minimum world-space Z covered by current broad-phase bounds.
Shape3D * findShape(PhysicsShapeHandle handle) const
Resolves a live shape handle; returns null when stale or foreign.
void forgetBody(Body3D *body)
Internal: wrapper teardown bookkeeping.
int getQueryShapeId(int index) const
Stable shape id of the i-th shape hit, sorted ascending.
float getGravityX() const
int nextBodyId()
Internal: next stable body id.
void setBodyPairCollisionEnabled(Body3D *bodyA, Body3D *bodyB, bool enabled)
Overrides collision for a live body pair without changing layer masks.
void forgetJoint(Joint3D *joint)
Internal: removes a joint wrapper from ownership bookkeeping.
World3D(float gravityX, float gravityY, float gravityZ, bool sleep, eve::PersistentId instanceId=eve::PersistentId::nil())
Creates a 3D physics world.
int getQueryShapeTag(int index) const
User tag paired with the i-th shape hit.
float getMoverDeltaZ() const
Resolved translation Z from the last moveCapsule call.
void setMoverUp(float x, float y, float z)
Set the up direction used for grounded and slope classification.
Joint3D * newSphericalJoint(Body3D *bodyA, Body3D *bodyB, float anchorX, float anchorY, float anchorZ, float axisX, float axisY, float axisZ, bool collideConnected=false)
Creates a ball-and-socket joint with a world-space twist/cone axis.
Joint3D * newRevoluteJoint(Body3D *bodyA, Body3D *bodyB, float anchorX, float anchorY, float anchorZ, float axisX, float axisY, float axisZ, bool collideConnected=false)
Creates a hinge around a normalized world-space axis at a shared anchor.
float getMoverDeltaY() const
Resolved translation Y from the last moveCapsule call.
void setContactTuning(float hertz, float dampingRatio, float pushOutSpeed)
Configures contact overlap recovery.
eve::Status backendSelectionStatus() const
Selection status, including a structured absent-capability warning.
bool isBodyPairCollisionEnabled(Body3D *bodyA, Body3D *bodyB) const
Whether a body pair is permitted by the runtime contact override.
int queryBodyContacts(int bodyId, int maxPoints)
Caches current touching manifold points for one stable Body3D ID. Results are oriented from the queri...
void setQueryIgnoredBodyId(int bodyId)
Ignores one stable Body3D ID in subsequent spatial queries; -1 disables.
Body3D * findBody(PhysicsBodyHandle handle) const
Resolves a live body handle; returns null for a stale or foreign handle.
int castCapsule(float ax, float ay, float az, float bx, float by, float bz, float radius, float dx, float dy, float dz)
Sweep an arbitrarily oriented capsule through the world. The endpoints are cap centers and d{x,...
float getMoverNormalZ() const
Representative blocking normal Z from the last move.
bool isContinuousCollisionEnabled() const
Whether world-level continuous collision is enabled.
int nextJointId()
Internal: next stable joint id.
int querySphere(float x, float y, float z, float radius)
Query shapes overlapping a sphere in world meters.
void setWarmStartingEnabled(bool enabled)
Enables constraint warm starting for stable stacks and joints.
float getGravityY() const
PhysicsBodyHandle nextBodyRuntimeHandle()
Internal: next generation-qualified body handle.
bool isShapePairCollisionEnabled(Shape3D *shapeA, Shape3D *shapeB) const
Whether a shape pair is permitted by the runtime contact override.
int castCapsuleAll(float ax, float ay, float az, float bx, float by, float bz, float radius, float dx, float dy, float dz, int maxHits)
Collects the nearest maxHits swept-capsule intersections.
int rayCast(float x1, float y1, float z1, float x2, float y2, float z2)
Closest raycast from (x1,y1,z1) to (x2,y2,z2) in meters. Returns hit body id, or -1....
eve::Result< CapsuleMove3D > moveCapsuleOwned(float ax, float ay, float az, float bx, float by, float bz, float radius, float dx, float dy, float dz, QueryFilter3D filter={}, CapsuleMovePolicy3D policy={})
Moves a capsule and returns all resolved movement data as one owning result.
int explode(float x, float y, float z, float radius, float falloff, float impulsePerArea, int maskBits=-1)
Applies a geometry-aware radial impulse to dynamic convex shapes.
void setQueryFilter(int categoryBits, int maskBits)
Set category/mask bits for subsequent ray, overlap, cast and mover queries.
float getMoverDeltaX() const
Resolved translation X from the last moveCapsule call.
float getBoundsMaxZ() const
Maximum world-space Z covered by current broad-phase bounds.
void setMoverSlopeLimit(float degrees)
Set maximum walkable slope in degrees, clamped to [0, 89.9].
void setHitEventThreshold(float speed)
Minimum approach speed in m/s required for enabled Shape3D hit events.
int castBoxAll(float x, float y, float z, float width, float height, float depth, float qx, float qy, float qz, float qw, float dx, float dy, float dz, int maxHits)
Collects the nearest maxHits swept-oriented-box intersections.
eve::Result< CapsuleOverlap3D > queryCapsuleOwned(float ax, float ay, float az, float bx, float by, float bz, float radius, QueryFilter3D filter={})
Performs one capsule overlap and returns an owning summary without exposing the query cache.
b3WorldId raw() const
Raw Box3D world id.
int queryBox(float x, float y, float z, float width, float height, float depth, float qx, float qy, float qz, float qw)
Query shapes overlapping an oriented box.
void setMaximumLinearSpeed(float speed)
Sets the finite positive world-wide linear velocity clamp in m/s.
bool isValid() const
True while the underlying Box3D world is alive.
float getBoundsMinY() const
Minimum world-space Y covered by current broad-phase bounds.
bool sphereCast(float x1, float y1, float z1, float x2, float y2, float z2, float radius, uint64_t maskBits, int ignoredBodyId, CameraSphereHit3D *out) const
Internal camera query: swept sphere against non-sensor shapes.
static World3D * findWorld(PhysicsWorldHandle handle) noexcept
Resolve a live process-local world handle, or null after teardown. @thread Owning physics thread only...
Joint3D * newPrismaticJoint(Body3D *bodyA, Body3D *bodyB, float anchorX, float anchorY, float anchorZ, float axisX, float axisY, float axisZ, bool collideConnected=false)
Creates a slider whose permitted world-space translation follows axis.
float getBoundsMinX() const
Minimum world-space X covered by current broad-phase bounds.
SimulationDeterminism backendDeterminism() const noexcept
Replay/numeric guarantee declared by the selected backend.
Joint3D * newWheelJoint(Body3D *bodyA, Body3D *bodyB, float anchorX, float anchorY, float anchorZ, float suspensionAxisX, float suspensionAxisY, float suspensionAxisZ, float wheelAxisX, float wheelAxisY, float wheelAxisZ, bool collideConnected=false)
Creates a vehicle wheel joint with independent world suspension and spin axes.
void setRestitutionThreshold(float speed)
Minimum impact speed that permits material restitution, in m/s.
Joint3D * newDistanceJoint(Body3D *bodyA, Body3D *bodyB, float anchorAX, float anchorAY, float anchorAZ, float anchorBX, float anchorBY, float anchorBZ, float length, bool collideConnected=false)
Connects two world-space anchor points with a rigid distance constraint.
float getContactRecycleDistance() const
Current contact-point persistence distance in meters.
int queryAABB(float minX, float minY, float minZ, float maxX, float maxY, float maxZ)
Query shapes overlapping an AABB in meters (min/max corners). Returns match count; read ids with getQ...
void updateFull(float dt, int subStepCount)
Steps with an explicit substep count.
int castSphereAll(float x, float y, float z, float radius, float dx, float dy, float dz, int maxHits)
Collects the nearest maxHits swept-sphere intersections.
void destroyBody(Body3D *body)
Destroys a body (null is ignored).
void setContinuousCollisionEnabled(bool enabled)
Enables world-level continuous collision against static geometry.
void setQueryIgnoredShapeId(int shapeId)
Ignores one stable Shape3D ID in subsequent spatial queries; -1 disables.
void forgetShape(Shape3D *shape)
void clearQueryIgnores()
Clears Body and Shape query exclusions without changing layer filtering.
float getBoundsMaxX() const
Maximum world-space X covered by current broad-phase bounds.
Body3D * findBodyById(int bodyId) const
Resolves a live body by its world-local stable event/query id.
float getGravityZ() const
void clearContactEvents()
Clears all contact and trigger buffers before the next step.
void destroy()
Destroys the world and invalidates all wrappers.
bool pointProbe(float x, float y, float z, float radius, ClothContact3D *out) const
Probe the deepest non-sensor shape within radius of a meter-space point. Returns false when nothing i...
void resetQueryFilter()
Restore the query filter to accept all lower-32-bit categories.
void setShapePairCollisionEnabled(Shape3D *shapeA, Shape3D *shapeB, bool enabled)
Overrides collision for one live shape pair.
ClothProbeStatus pointProbeFiltered(float x, float y, float z, float radius, ClothContact3D *out, uint64_t categoryBits, uint64_t maskBits) const
Probe a cloth particle using symmetric category/mask filtering.
bool isContactPointPersisted(int index) const
Whether this manifold point existed in the previous step.
float getMoverNormalX() const
Representative blocking normal X from the last move.
float getHitEventThreshold() const
Current significant-hit speed threshold in m/s.
void emitContactEvents()
Internal: collects Box3D contact events into the event buffers.
int rayCastAll(float x1, float y1, float z1, float x2, float y2, float z2, int maxHits)
Collects the nearest maxHits ray intersections, sorted by fraction then Shape ID.
Body3D * newBody(const std::string &bodyType, float x, float y, float z)
Creates a body in meter-space units.
int rayCastFiltered(float x1, float y1, float z1, float x2, float y2, float z2, uint64_t maskBits)
带形状类别掩码的最短射线查询(掩码为接受的 shape categoryBits)。
float getBoundsMaxY() const
Maximum world-space Y covered by current broad-phase bounds.
void setContactRecycleDistance(float distance)
Sets contact-point persistence distance in meters; zero disables recycling.
bool moveCapsule(float ax, float ay, float az, float bx, float by, float bz, float radius, float dx, float dy, float dz)
Move a capsule with continuous collision and multi-plane sliding. Existing penetration is recovered b...
PhysicsShapeHandle nextShapeRuntimeHandle()
Internal: next generation-qualified shape handle.
int getMoverIterations() const
Number of outer solver passes used by the last move.
int getMoverPlaneCount() const
Number of collision planes considered during the last move.
SimulationBackendKind backendKind() const noexcept
Selected CPU/GPU/mock backend family.
bool isMoverGrounded() const
True when the last move touched a walkable plane.
float lengthSquared(Vec3 value)
Length squared.
PhysicsWorldHandle allocatePhysicsWorldHandle()
Allocates a process-local world handle from the physics owner.
eve::PersistentId makePhysicsWorldPersistentId(PhysicsWorldHandle runtimeHandle)
Creates a non-nil process-local generated identity for a world without an injected persistent ID.
Optional physics backend for vehicle mobility and body attach.
eve::RuntimeHandle< PhysicsShapeHandleTag > PhysicsShapeHandle
Generation-qualified runtime identity for a physics shape.
eve::Result< void > registerTargetingLineOfSightWorld(World3D *world)
Registers a borrowed live World3D with the targeting LOS adapter.
SimulationBackendKind
Kind of implementation that owns a simulation step.
SimulationDeterminism
Determinism guarantee made by a simulation backend.
@ ToleranceBounded
Results are equivalent within a documented numeric tolerance.
eve::RuntimeHandle< PhysicsJointHandleTag > PhysicsJointHandle
Generation-qualified runtime identity for a physics joint.
eve::RuntimeHandle< PhysicsBodyHandleTag > PhysicsBodyHandle
Generation-qualified runtime identity for a physics body.
ClothProbeStatus
Named outcome for a filtered cloth particle probe.
Build metadata (engine git commit, build time, third-party version).
DiagnosticCode
Stable machine-readable diagnostic codes.
One deterministic fixed-step emitted by SimulationClock.
SimulationTick tick
Tick reached after this step is applied.
Fixed-capacity deterministic result of one AABB broad-phase query.
std::array< BroadPhaseHit3D, Capacity > hits
One generation-qualified shape returned by an owning broad-phase query.
CameraSphereHit3D public API.
Owning result of continuous capsule movement.
Per-call capsule mover classification policy; never mutates persistent World3D settings.
Owning summary of a capsule overlap query.
Query filter applied atomically for one owning query operation.
Owning snapshot of the closest ray intersection.
Observable backend progress shared by CPU and accelerator providers.
Validated solver policy for one simulation step.
Stable snapshot describing one side of a 3D collision event.