载入中...
搜索中...
未找到
World3D.cpp
浏览该文件的文档.
1#include "physics/World3D.h"
2#include "physics/Body3D.h"
3#include "physics/Shape3D.h"
5#include "physics/Joint3D.h"
7
8#include "common/Exception.h"
9#include "common/Profile.h"
11
12#include <box3d/box3d.h>
13
14#include <algorithm>
15#include <cfloat>
16#include <cmath>
17#include <mutex>
18#include <unordered_map>
19#include <unordered_set>
20#include <utility>
21
22namespace eve::physics {
23namespace {
24
25class ScopedQueryState3D {
26public:
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);
33 }
34
35 ~ScopedQueryState3D() noexcept {
36 world_.setQueryFilter(category_, mask_);
37 world_.setQueryIgnoredBodyId(body_);
38 world_.setQueryIgnoredShapeId(shape_);
39 }
40
41private:
42 World3D& world_;
43 int category_;
44 int mask_;
45 int body_;
46 int shape_;
47};
48
49template <class T>
50eve::Result<T> ownedQueryFailure(eve::DiagnosticCode code, std::string message,
51 std::string path = {}) {
53 eve::Diagnostic::error(code, message, path, {}, "physics.query3d"));
54}
55
56} // namespace
57namespace {
58
59constexpr uint64_t combineModeMask = 0x7ull;
60constexpr int frictionModeShift = 32;
61constexpr int restitutionModeShift = 35;
62
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);
68 switch (mode) {
69 case 1: return 0.5f * (a + b);
70 case 2: return std::min(a, b);
71 case 3: return a * b;
72 case 4: return std::max(a, b);
73 default: return friction ? std::sqrt(a * b) : std::max(a, b);
74 }
75}
76
77float mixFriction(float a, uint64_t materialA, float b, uint64_t materialB) {
78 return combineMaterialValue(a, materialA, b, materialB, frictionModeShift, true);
79}
80
81float mixRestitution(float a, uint64_t materialA, float b, uint64_t materialB) {
82 return combineMaterialValue(a, materialA, b, materialB, restitutionModeShift, false);
83}
84
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;
89}
90
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;
94}
95
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)",
101 type.c_str());
102}
103
104Body3D *bodyFromShape(b3ShapeId shapeId) {
105 if (!b3Shape_IsValid(shapeId)) return nullptr;
106 return static_cast<Body3D *>(b3Body_GetUserData(b3Shape_GetBody(shapeId)));
107}
108
109Shape3D *shapeFromShape(b3ShapeId shapeId) {
110 if (!b3Shape_IsValid(shapeId)) return nullptr;
111 return static_cast<Shape3D *>(b3Shape_GetUserData(shapeId));
112}
113
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))
116 throw eve::Exception("%s: quaternion must be finite", operation);
117 const float lengthSquared = qx * qx + qy * qy + qz * qz + qw * qw;
118 if (!(lengthSquared > 1e-12f))
119 throw eve::Exception("%s: quaternion must be non-zero", operation);
120 const float inverseLength = 1.f / std::sqrt(lengthSquared);
121 return b3Quat{{qx * inverseLength, qy * inverseLength, qz * inverseLength},
122 qw * inverseLength};
123}
124
125void makeBoxProxyPoints(b3Vec3 (&points)[8], float width, float height, float depth, b3Quat q,
126 const char *operation) {
127 if (!(width > 0.f) || !(height > 0.f) || !(depth > 0.f) || !std::isfinite(width) ||
128 !std::isfinite(height) || !std::isfinite(depth))
129 throw eve::Exception("%s: box dimensions must be finite and > 0", operation);
130 const float hx = 0.5f * width, hy = 0.5f * height, hz = 0.5f * depth;
131 int point = 0;
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)
135 points[point++] = b3RotateVector(
136 q, b3Vec3{static_cast<float>(x) * hx, static_cast<float>(y) * hy,
137 static_cast<float>(z) * hz});
138}
139
140void stepBox3D(void *context, const eve::SimulationStep &step, const SimulationSettings &settings) noexcept {
141 auto *worldId = static_cast<b3WorldId *>(context);
142 b3World_Step(*worldId, static_cast<float>(step.delta.seconds()), settings.subStepCount);
143}
144
145eve::Result<eve::SimulationStep> makeLegacyStep(float dt, eve::SimulationTick currentTick) {
146 if (!std::isfinite(dt) || dt < 0.f) {
149 "World3D update dt must be finite and non-negative", "physics.world3d.update.dt"));
150 }
151 const auto nextTick = currentTick.incremented();
152 if (!nextTick) {
155 "World3D simulation tick cannot be incremented", "physics.world3d.simulationTick"));
156 }
157 auto duration = eve::Duration::fromSeconds(static_cast<double>(std::min(dt, 0.05f)));
159 return eve::Result<eve::SimulationStep>::success({*nextTick, std::move(duration).takeValue()});
160}
161
162struct WorldRegistry {
163 std::mutex mutex;
164 std::unordered_map<PhysicsWorldHandle::index_type, World3D*> worlds;
165};
166WorldRegistry& worldRegistry() {
167 static auto* registry = new WorldRegistry;
168 return *registry;
169}
170
171} // namespace
172
173World3D::World3D(float gravityX, float gravityY, float gravityZ, bool sleep, eve::PersistentId instanceId)
174 : World3D(gravityX, gravityY, gravityZ, sleep, instanceId, true) {}
175
176World3D::World3D(float gravityX, float gravityY, float gravityZ, bool sleep, eve::PersistentId instanceId,
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;
185 runtimeHandle_ = detail::allocatePhysicsWorldHandle();
186 if (instanceId_.isNil()) instanceId_ = detail::makePhysicsWorldPersistentId(runtimeHandle_);
187 worldId_ = b3CreateWorld(&def);
188 auto selection = detail::selectSimulationBackend(
190 detail::makeCallbackSimulationBackend(&worldId_, &stepBox3D, SimulationBackendKind::Cpu,
192 &worldId_, true);
193 if (!selection) {
194 const eve::Status status = selection.status();
195 throw eve::Exception("World3D: cannot select a simulation backend: %s", status.describe().c_str());
196 }
197 backendSelectionStatus_ = selection.status();
198 auto selected = std::move(selection).takeValue();
199 backendFallback_ = selected.usedFallback;
200 simulation_ = std::move(selected.backend);
201 if (registerQueries) registerCameraObstructionWorld(this);
202 auto targetingRegistration = registerQueries ? registerTargetingLineOfSightWorld(this)
203 : eve::Result<void>::success();
204 if (!registerQueries) {
205 targetingRegistration.ignore("detached topology candidate does not publish targeting queries");
206 } else if (!targetingRegistration) {
207 // Physics supports multiple worlds; targeting LOS deliberately exposes
208 // only one, so an explicitly consumed Conflict leaves this world valid
209 // without making its query context ambiguous.
210 targetingRegistration.ignore("another World3D is already the active targeting LOS world");
211 }
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;
219}
220
222 if (isValid()) b3World_EnableContinuous(worldId_, enabled);
223}
224
226 return isValid() && b3World_IsContinuousEnabled(worldId_);
227}
228
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);
233}
234
236 return isValid() ? b3World_GetRestitutionThreshold(worldId_) : 0.f;
237}
238
239void World3D::setContactTuning(float hertz, float dampingRatio, float pushOutSpeed) {
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);
250}
251
253 if (!std::isfinite(distance) || distance < 0.f)
254 throw eve::Exception("World3D.setContactRecycleDistance: distance must be finite and >= 0");
255 if (isValid()) b3World_SetContactRecycleDistance(worldId_, distance);
256}
257
259 return isValid() ? b3World_GetContactRecycleDistance(worldId_) : 0.f;
260}
261
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);
266}
267
269 return isValid() ? b3World_GetMaximumLinearSpeed(worldId_) : 0.f;
270}
271
273 if (isValid()) b3World_EnableWarmStarting(worldId_, enabled);
274}
275
277 return isValid() && b3World_IsWarmStartingEnabled(worldId_);
278}
279
280int World3D::explode(float x, float y, float z, float radius, float falloff,
281 float impulsePerArea, int maskBits) {
282 explosionResults_.clear();
283 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
284 throw eve::Exception("World3D.explode: center must be finite");
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;
290
291 struct Before {
292 Body3D *body = nullptr;
293 b3Vec3 linear{};
294 b3Vec3 angular{};
295 };
296 std::vector<Before> before;
297 before.reserve(bodies_.size());
298 for (Body3D *body : bodies_) {
299 if (!body || !body->isValid() || b3Body_GetType(body->raw()) != b3_dynamicBody)
300 continue;
301 before.push_back({body, b3Body_GetLinearVelocity(body->raw()),
302 b3Body_GetAngularVelocity(body->raw())});
303 }
304
305 b3ExplosionDef def = b3DefaultExplosionDef();
306 def.position = b3Pos{x, y, z};
307 def.radius = radius;
308 def.falloff = falloff;
309 def.impulsePerArea = impulsePerArea;
310 def.maskBits = static_cast<uint32_t>(maskBits);
311 b3World_Explode(worldId_, &def);
312
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,
321 dw.x, dw.y, dw.z});
322 }
323 std::sort(explosionResults_.begin(), explosionResults_.end(),
324 [](const ExplosionResult &a, const ExplosionResult &b) {
325 return a.bodyId < b.bodyId;
326 });
327 return static_cast<int>(explosionResults_.size());
328}
329
330const World3D::ExplosionResult &World3D::explosionResultAt(int index,
331 const char *operation) const {
332 if (index < 0 || index >= static_cast<int>(explosionResults_.size()))
333 throw eve::Exception("World3D.%s: index out of range", operation);
334 return explosionResults_[static_cast<size_t>(index)];
335}
336
337#define EV_EXPLOSION_INT_GETTER(name, member) \
338 int World3D::name(int index) const { return explosionResultAt(index, #name).member; }
339
340EV_EXPLOSION_INT_GETTER(getExplosionResultBodyId, bodyId)
341#undef EV_EXPLOSION_INT_GETTER
342
343#define EV_EXPLOSION_FLOAT_GETTER(name, member) \
344 float World3D::name(int index) const { return explosionResultAt(index, #name).member; }
345
346EV_EXPLOSION_FLOAT_GETTER(getExplosionResultDeltaVelocityX, deltaVX)
347EV_EXPLOSION_FLOAT_GETTER(getExplosionResultDeltaVelocityY, deltaVY)
348EV_EXPLOSION_FLOAT_GETTER(getExplosionResultDeltaVelocityZ, deltaVZ)
349EV_EXPLOSION_FLOAT_GETTER(getExplosionResultDeltaAngularVelocityX, deltaWX)
350EV_EXPLOSION_FLOAT_GETTER(getExplosionResultDeltaAngularVelocityY, deltaWY)
351EV_EXPLOSION_FLOAT_GETTER(getExplosionResultDeltaAngularVelocityZ, deltaWZ)
352#undef EV_EXPLOSION_FLOAT_GETTER
353
355 return isValid() ? static_cast<float>(b3World_GetBounds(worldId_).lowerBound.x) : 0.f;
356}
358 return isValid() ? static_cast<float>(b3World_GetBounds(worldId_).lowerBound.y) : 0.f;
359}
361 return isValid() ? static_cast<float>(b3World_GetBounds(worldId_).lowerBound.z) : 0.f;
362}
364 return isValid() ? static_cast<float>(b3World_GetBounds(worldId_).upperBound.x) : 0.f;
365}
367 return isValid() ? static_cast<float>(b3World_GetBounds(worldId_).upperBound.y) : 0.f;
368}
370 return isValid() ? static_cast<float>(b3World_GetBounds(worldId_).upperBound.z) : 0.f;
371}
372
373#define EV_COUNTER_GETTER(name, member) \
374 int World3D::name() const { return isValid() ? b3World_GetCounters(worldId_).member : 0; }
375
376EV_COUNTER_GETTER(getBodyCount, bodyCount)
377EV_COUNTER_GETTER(getShapeCount, shapeCount)
378EV_COUNTER_GETTER(getContactCount, contactCount)
379EV_COUNTER_GETTER(getJointCount, jointCount)
380EV_COUNTER_GETTER(getIslandCount, islandCount)
381EV_COUNTER_GETTER(getAwakeContactCount, awakeContactCount)
382EV_COUNTER_GETTER(getRecycledContactCount, recycledContactCount)
383EV_COUNTER_GETTER(getStaticTreeHeight, staticTreeHeight)
384EV_COUNTER_GETTER(getDynamicTreeHeight, treeHeight)
385EV_COUNTER_GETTER(getMemoryByteCount, byteCount)
386#undef EV_COUNTER_GETTER
387
389 return isValid() ? b3World_GetAwakeBodyCount(worldId_) : 0;
390}
391
392#define EV_PROFILE_GETTER(name, member) \
393 float World3D::name() const { return isValid() ? b3World_GetProfile(worldId_).member : 0.f; }
394
395EV_PROFILE_GETTER(getProfileStepMs, step)
396EV_PROFILE_GETTER(getProfilePairsMs, pairs)
397EV_PROFILE_GETTER(getProfileCollideMs, collide)
398EV_PROFILE_GETTER(getProfileSolveMs, solve)
399EV_PROFILE_GETTER(getProfileBulletsMs, bullets)
400EV_PROFILE_GETTER(getProfileSensorsMs, sensors)
401#undef EV_PROFILE_GETTER
402
404
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_);
418 for (Body3D *body : bodies_) body->world_ = this;
419 for (Shape3D *shape : shapes_) shape->world_ = this;
420 for (Joint3D *joint : joints_) joint->world_ = this;
421 for (Body3D *body : prepared.bodies_) body->world_ = &prepared;
422 for (Shape3D *shape : prepared.shapes_) shape->world_ = &prepared;
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);
428}
429
430bool World3D::isValid() const { return !destroyed_ && b3World_IsValid(worldId_); }
431
433 if (destroyed_) return;
434 {
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);
439 }
440 destroyed_ = true;
441 // Invalidate borrowed query registrations without touching any global
442 // registry. This keeps teardown safe for every static destruction order.
443 queryLifetime_.reset();
444
445 std::vector<Mechanism3D *> mechanisms(mechanisms_.begin(), mechanisms_.end());
446 for (Mechanism3D *mechanism : mechanisms) {
447 if (mechanism) mechanism->invalidate();
448 }
449 mechanisms_.clear();
450
451 std::vector<Joint3D *> joints(joints_.begin(), joints_.end());
452 for (Joint3D *joint : joints) {
453 if (joint) joint->invalidate();
454 }
455 joints_.clear();
456 disabledBodyPairs_.clear();
457 disabledShapePairs_.clear();
458
459 std::vector<Body3D *> bodies(bodies_.begin(), bodies_.end());
460 for (Body3D *b : bodies) {
461 if (b) b->invalidate();
462 }
463 bodies_.clear();
464
465 std::vector<Shape3D *> shapes(shapes_.begin(), shapes_.end());
466 for (Shape3D *s : shapes) {
467 if (s) s->invalidate();
468 }
469 shapes_.clear();
470
471 simulation_.reset();
472 if (b3World_IsValid(worldId_)) {
473 b3DestroyWorld(worldId_);
474 }
476 shapeRecords_.clear();
477 shapeHandles_.clear();
478 shapeRawHandles_.clear();
479 jointHandles_.clear();
480 worldId_ = {};
481 runtimeHandle_ = PhysicsWorldHandle::invalid();
482}
483
485 if (handle.isInvalid()) return nullptr;
486 auto& registry = worldRegistry();
487 std::scoped_lock registryLock(registry.mutex);
488 auto found = registry.worlds.find(handle.index());
489 if (found == registry.worlds.end() || !found->second || found->second->runtimeHandle() != handle) return nullptr;
490 return found->second;
491}
492
493bool World3D::sphereCast(float x1, float y1, float z1, float x2, float y2, float z2,
494 float radius, uint64_t maskBits, int ignoredBodyId,
495 CameraSphereHit3D* out) const {
496 if (out) *out = CameraSphereHit3D{};
497 if (!out || !isValid() || radius < 0.f) return false;
498
499 struct Collector {
500 CameraSphereHit3D* out = nullptr;
501 int ignoredBodyId = -1;
502 static float callback(b3ShapeId shapeId, b3Pos point, b3Vec3 normal, float fraction,
503 uint64_t, int, int, void* context) {
504 auto* self = static_cast<Collector*>(context);
505 Body3D* body = bodyFromShape(shapeId);
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);
514 self->out->nx = normal.x;
515 self->out->ny = normal.y;
516 self->out->nz = normal.z;
517 }
518 return fraction;
519 }
520 } collector{out, ignoredBodyId};
521
522 const b3Vec3 point{0.f, 0.f, 0.f};
523 const b3ShapeProxy proxy{&point, 1, radius};
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,
528 &Collector::callback, &collector);
529 return out->hit;
530}
531
532Joint3D *World3D::newDistanceJoint(Body3D *bodyA, Body3D *bodyB, float anchorAX,
533 float anchorAY, float anchorAZ, float anchorBX,
534 float anchorBY, float anchorBZ, float length,
535 bool collideConnected) {
536 if (!isValid() || !bodyA || !bodyB || !bodyA->isValid() || !bodyB->isValid() ||
537 bodyA->getWorld() != this || bodyB->getWorld() != this || bodyA == bodyB)
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};
540 for (float value : values) {
541 if (!std::isfinite(value))
542 throw eve::Exception("World3D.newDistanceJoint: parameters must be finite");
543 }
544 if (length < 0.f)
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;
552 def.length = length;
554 b3JointId id = b3CreateDistanceJoint(worldId_, &def);
555 auto *joint = new Joint3D(this, bodyA, bodyB, id, runtimeHandle, Joint3D::Kind::Distance, nextJointId());
556 b3Joint_SetUserData(id, joint);
557 joints_.insert(joint);
558 jointHandles_[runtimeHandle] = joint;
559 return joint;
560}
561
562Joint3D *World3D::newRevoluteJoint(Body3D *bodyA, Body3D *bodyB, float anchorX,
563 float anchorY, float anchorZ, float axisX, float axisY,
564 float axisZ, bool collideConnected) {
565 if (!isValid() || !bodyA || !bodyB || !bodyA->isValid() || !bodyB->isValid() ||
566 bodyA->getWorld() != this || bodyB->getWorld() != this || bodyA == bodyB)
567 throw eve::Exception("World3D.newRevoluteJoint: bodies must be distinct and belong to this world");
568 const float values[] = {anchorX, anchorY, anchorZ, axisX, axisY, axisZ};
569 for (float value : values) {
570 if (!std::isfinite(value))
571 throw eve::Exception("World3D.newRevoluteJoint: parameters must be finite");
572 }
573 const float axisLength = std::sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
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);
590 auto *joint = new Joint3D(this, bodyA, bodyB, id, runtimeHandle, Joint3D::Kind::Revolute, nextJointId());
591 b3Joint_SetUserData(id, joint);
592 joints_.insert(joint);
593 jointHandles_[runtimeHandle] = joint;
594 return joint;
595}
596
597Joint3D *World3D::newPrismaticJoint(Body3D *bodyA, Body3D *bodyB, float anchorX,
598 float anchorY, float anchorZ, float axisX, float axisY,
599 float axisZ, bool collideConnected) {
600 if (!isValid() || !bodyA || !bodyB || !bodyA->isValid() || !bodyB->isValid() ||
601 bodyA->getWorld() != this || bodyB->getWorld() != this || bodyA == bodyB)
602 throw eve::Exception("World3D.newPrismaticJoint: bodies must be distinct and belong to this world");
603 const float values[] = {anchorX, anchorY, anchorZ, axisX, axisY, axisZ};
604 for (float value : values) {
605 if (!std::isfinite(value))
606 throw eve::Exception("World3D.newPrismaticJoint: parameters must be finite");
607 }
608 const float axisLength = std::sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
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);
625 auto *joint = new Joint3D(this, bodyA, bodyB, id, runtimeHandle, Joint3D::Kind::Prismatic, nextJointId());
626 b3Joint_SetUserData(id, joint);
627 joints_.insert(joint);
628 jointHandles_[runtimeHandle] = joint;
629 return joint;
630}
631
632Joint3D *World3D::newSphericalJoint(Body3D *bodyA, Body3D *bodyB, float anchorX,
633 float anchorY, float anchorZ, float axisX, float axisY,
634 float axisZ, bool collideConnected) {
635 if (!isValid() || !bodyA || !bodyB || !bodyA->isValid() || !bodyB->isValid() ||
636 bodyA->getWorld() != this || bodyB->getWorld() != this || bodyA == bodyB)
637 throw eve::Exception("World3D.newSphericalJoint: bodies must be distinct and belong to this world");
638 const float values[] = {anchorX, anchorY, anchorZ, axisX, axisY, axisZ};
639 for (float value : values) {
640 if (!std::isfinite(value))
641 throw eve::Exception("World3D.newSphericalJoint: parameters must be finite");
642 }
643 const float axisLength = std::sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
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);
660 auto *joint = new Joint3D(this, bodyA, bodyB, id, runtimeHandle, Joint3D::Kind::Spherical, nextJointId());
661 b3Joint_SetUserData(id, joint);
662 joints_.insert(joint);
663 jointHandles_[runtimeHandle] = joint;
664 return joint;
665}
666
667Joint3D *World3D::newWheelJoint(Body3D *bodyA, Body3D *bodyB, float anchorX,
668 float anchorY, float anchorZ, float suspensionAxisX,
669 float suspensionAxisY, float suspensionAxisZ, float wheelAxisX,
670 float wheelAxisY, float wheelAxisZ, bool collideConnected) {
671 if (!isValid() || !bodyA || !bodyB || !bodyA->isValid() || !bodyB->isValid() ||
672 bodyA->getWorld() != this || bodyB->getWorld() != this || bodyA == bodyB)
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};
676 for (float value : values) {
677 if (!std::isfinite(value))
678 throw eve::Exception("World3D.newWheelJoint: parameters must be finite");
679 }
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);
707 auto *joint = new Joint3D(this, bodyA, bodyB, id, runtimeHandle, Joint3D::Kind::Wheel, nextJointId());
708 b3Joint_SetUserData(id, joint);
709 joints_.insert(joint);
710 jointHandles_[runtimeHandle] = joint;
711 return joint;
712}
713
715 if (!joint) return;
716 joints_.erase(joint);
717 if (joint->runtimeHandle().isValid()) jointHandles_.erase(joint->runtimeHandle());
718 for (auto it = jointHandles_.begin(); it != jointHandles_.end();) {
719 if (it->second == joint)
720 it = jointHandles_.erase(it);
721 else
722 ++it;
723 }
724}
725
726void World3D::update(float dt) { updateFull(dt, 4); }
727
728void World3D::updateFull(float dt, int subStepCount) {
729 EV_PROFILE_MODULE("physics", "World3D::update");
730 auto legacyStep = makeLegacyStep(dt, simulationTick_);
731 if (!legacyStep) {
732 legacyStep.ignore("legacy World3D::updateFull cannot return a structured error");
733 return;
734 }
735 auto result = step(std::move(legacyStep).takeValue(), SimulationSettings{8, 3, subStepCount});
736 result.ignore("legacy World3D::updateFull cannot return a structured result");
737}
738
740 if (!isValid() || !simulation_) {
742 eve::DiagnosticCode::PreconditionViolation, "Cannot step a destroyed or uninitialized 3D physics world",
743 "physics.world3d.step"));
744 }
745 auto valid = detail::validateSimulationStep(stepValue, settings, simulation_->observation());
746 if (!valid) return valid;
747 for (Mechanism3D *mechanism : mechanisms_) {
748 if (mechanism) mechanism->syncBeforeStep();
749 }
750 for (Shape3D *shape : shapes_) {
751 if (shape && shape->isOneWay()) shape->refreshOneWayWorldData();
752 }
753 auto result = simulation_->step(stepValue, settings);
754 if (!result) return result;
755 simulationTick_ = stepValue.tick;
758 return result;
759}
760
762 return simulation_ ? simulation_->observation() : SimulationObservation{};
763}
764
766 return simulation_ ? simulation_->kind() : SimulationBackendKind::Cpu;
767}
768
770 return simulation_ ? simulation_->determinism() : SimulationDeterminism::ToleranceBounded;
771}
772
773eve::Status World3D::backendSelectionStatus() const { return backendSelectionStatus_; }
774
775void World3D::setGravity(float gx, float gy, float gz) {
776 if (!isValid()) return;
777 b3World_SetGravity(worldId_, b3Vec3{gx, gy, gz});
778}
779
780float World3D::getGravityX() const {
781 if (!isValid()) return 0.f;
782 return b3World_GetGravity(worldId_).x;
783}
784
785float World3D::getGravityY() const {
786 if (!isValid()) return 0.f;
787 return b3World_GetGravity(worldId_).y;
788}
789
790float World3D::getGravityZ() const {
791 if (!isValid()) return 0.f;
792 return b3World_GetGravity(worldId_).z;
793}
794
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);
799}
800
802 return isValid() ? b3World_GetHitEventThreshold(worldId_) : 0.f;
803}
804
805void World3D::refreshContactFilter(Shape3D *shape) {
806 if (!shape || !shape->isValid()) return;
807 b3Shape_SetFilter(shape->raw(), b3Shape_GetFilter(shape->raw()), true);
808}
809
811 if (!bodyA || !bodyB || bodyA == bodyB || !bodyA->isValid() || !bodyB->isValid() ||
812 bodyA->getWorld() != this || bodyB->getWorld() != this)
813 throw eve::Exception(
814 "World3D.setBodyPairCollisionEnabled: distinct live bodies must belong to this world");
815 const uint64_t key = stablePairKey(bodyA->getId(), bodyB->getId());
816 for (Shape3D *shape : shapes_) {
817 if (shape && (shape->getBody() == bodyA || shape->getBody() == bodyB))
818 shape->enableContactOverrideFiltering();
819 }
820 if (enabled) disabledBodyPairs_.erase(key); else disabledBodyPairs_.insert(key);
821 for (Shape3D *shape : shapes_) {
822 if (shape && (shape->getBody() == bodyA || shape->getBody() == bodyB))
823 refreshContactFilter(shape);
824 }
825}
826
828 if (!bodyA || !bodyB || bodyA == bodyB || !bodyA->isValid() || !bodyB->isValid() ||
829 bodyA->getWorld() != this || bodyB->getWorld() != this)
830 throw eve::Exception(
831 "World3D.isBodyPairCollisionEnabled: distinct live bodies must belong to this world");
832 return disabledBodyPairs_.count(stablePairKey(bodyA->getId(), bodyB->getId())) == 0;
833}
834
836 if (!shapeA || !shapeB || shapeA == shapeB || !shapeA->isValid() || !shapeB->isValid() ||
837 !shapeA->getBody() || !shapeB->getBody() ||
838 shapeA->getBody()->getWorld() != this || shapeB->getBody()->getWorld() != this)
839 throw eve::Exception(
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);
847}
848
850 if (!shapeA || !shapeB || shapeA == shapeB || !shapeA->isValid() || !shapeB->isValid() ||
851 !shapeA->getBody() || !shapeB->getBody() ||
852 shapeA->getBody()->getWorld() != this || shapeB->getBody()->getWorld() != this)
853 throw eve::Exception(
854 "World3D.isShapePairCollisionEnabled: distinct live shapes must belong to this world");
855 return disabledShapePairs_.count(stablePairKey(shapeA->getId(), shapeB->getId())) == 0;
856}
857
858bool World3D::customFilterCallback(b3ShapeId shapeIdA, b3ShapeId shapeIdB, void *context) {
859 auto *world = static_cast<World3D *>(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)
866 return false;
867 return world->disabledShapePairs_.count(stablePairKey(shapeA->getId(), shapeB->getId())) == 0;
868}
869
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;
873 }
874}
875
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;
879 }
880}
881
882int World3D::nextBodyId() { return nextId_++; }
883
885 if (nextBodyHandleIndex_ == PhysicsBodyHandle::invalidIndex)
886 throw eve::Exception("World3D.newBody: process-local body handle space exhausted");
887 return PhysicsBodyHandle(nextBodyHandleIndex_++, 1u);
888}
889
891 if (nextShapeHandleIndex_ == PhysicsShapeHandle::invalidIndex)
892 throw eve::Exception("Body3D.newShape: process-local shape handle space exhausted");
893 return PhysicsShapeHandle(nextShapeHandleIndex_++, 1u);
894}
895
897 if (nextJointHandleIndex_ == PhysicsJointHandle::invalidIndex)
898 throw eve::Exception("World3D.newJoint: process-local joint handle space exhausted");
899 return PhysicsJointHandle(nextJointHandleIndex_++, 1u);
900}
901
902Body3D *World3D::newBody(const std::string &bodyType, float x, float y, float z) {
903 if (!isValid()) throw eve::Exception("World3D.newBody: world destroyed");
904
905 b3BodyDef def = b3DefaultBodyDef();
906 def.type = parseBodyType(bodyType);
907 def.position = b3Pos{x, y, z};
908 def.rotation = b3Quat_identity;
909
911 b3BodyId raw = b3CreateBody(worldId_, &def);
912 Body3D *body = new Body3D(this, raw, nextBodyId(), runtimeHandle);
913 b3Body_SetUserData(raw, body);
914 bodies_.insert(body);
915 return body;
916}
917
919 if (!isValid() || handle.isInvalid()) return nullptr;
920 for (Body3D *body : bodies_) {
921 if (body && body->isValid() && body->runtimeHandle() == handle) return body;
922 }
923 return nullptr;
924}
925
926Body3D *World3D::findBodyById(int bodyId) const {
927 if (!isValid() || bodyId < 0) return nullptr;
928 for (Body3D *body : bodies_) {
929 if (body && body->isValid() && body->getId() == bodyId) return body;
930 }
931 return nullptr;
932}
933
935 if (!isValid() || handle.isInvalid()) return nullptr;
936 const auto found = shapeHandles_.find(handle);
937 if (found == shapeHandles_.end() || !found->second || !found->second->isValid()) return nullptr;
938 return found->second;
939}
940
941Shape3D *World3D::findShapeById(int shapeId) const {
942 if (!isValid() || shapeId < 0) return nullptr;
943 for (Shape3D *shape : shapes_) {
944 if (shape && shape->isValid() && shape->getId() == shapeId) return shape;
945 }
946 return nullptr;
947}
948
950 if (!isValid() || handle.isInvalid()) return nullptr;
951 const auto found = jointHandles_.find(handle);
952 if (found == jointHandles_.end() || !found->second || !found->second->isValid()) return nullptr;
953 return found->second;
954}
955
956b3QueryFilter World3D::makeQueryFilter() const {
957 b3QueryFilter filter = b3DefaultQueryFilter();
958 filter.categoryBits = queryCategoryBits_;
959 filter.maskBits = queryMaskBits_;
960 return filter;
961}
962
963bool World3D::shouldIgnoreQueryShape(b3ShapeId shapeId) const {
964 Body3D *body = bodyFromShape(shapeId);
965 Shape3D *shape = shapeFromShape(shapeId);
966 if (!body || !shape) return true;
967 return body->getId() == queryIgnoredBodyId_ || shape->getId() == queryIgnoredShapeId_;
968}
969
970void World3D::setQueryFilter(int categoryBits, int maskBits) {
971 queryCategoryBits_ = static_cast<uint32_t>(categoryBits);
972 queryMaskBits_ = static_cast<uint32_t>(maskBits);
973}
974
976 queryCategoryBits_ = 0xFFFFFFFFu;
977 queryMaskBits_ = 0xFFFFFFFFu;
978}
979
981 if (bodyId == 0 || bodyId < -1)
982 throw eve::Exception("World3D.setQueryIgnoredBodyId: use a positive stable id or -1");
983 queryIgnoredBodyId_ = bodyId;
984}
985
987 if (shapeId == 0 || shapeId < -1)
988 throw eve::Exception("World3D.setQueryIgnoredShapeId: use a positive stable id or -1");
989 queryIgnoredShapeId_ = shapeId;
990}
991
993 queryIgnoredBodyId_ = -1;
994 queryIgnoredShapeId_ = -1;
995}
996
998 if (!body) return;
999 body->destroy();
1000}
1001
1003 if (body) removeCollisionOverridesForBody(body->getId());
1004 bodies_.erase(body);
1005}
1007 if (!shape) return;
1008 removeCollisionOverridesForShape(shape->getId());
1009 shapes_.erase(shape);
1010 const PhysicsShapeHandle handle = shape->runtimeHandle();
1011 if (handle.isValid()) shapeHandles_.erase(handle);
1012 for (auto it = shapeHandles_.begin(); it != shapeHandles_.end();) {
1013 if (it->second == shape)
1014 it = shapeHandles_.erase(it);
1015 else
1016 ++it;
1017 }
1018 for (auto it = shapeRawHandles_.begin(); it != shapeRawHandles_.end();) {
1019 if (it->second == handle)
1020 it = shapeRawHandles_.erase(it);
1021 else
1022 ++it;
1023 }
1024}
1025
1027 if (!shape || !shape->getBody() || B3_IS_NULL(shape->raw()) || shape->runtimeHandle().isInvalid()) return;
1028 const PhysicsShapeHandle handle = shape->runtimeHandle();
1029 for (auto it = shapeHandles_.begin(); it != shapeHandles_.end();) {
1030 if (it->second == shape || it->first == handle)
1031 it = shapeHandles_.erase(it);
1032 else
1033 ++it;
1034 }
1035 for (auto it = shapeRawHandles_.begin(); it != shapeRawHandles_.end();) {
1036 if (it->second == handle)
1037 it = shapeRawHandles_.erase(it);
1038 else
1039 ++it;
1040 }
1041 const uint64_t rawKey = b3StoreShapeId(shape->raw());
1042 shapeHandles_[handle] = shape;
1043 shapeRawHandles_[rawKey] = handle;
1044 shapeRecords_[rawKey] = EventShape{shape->getBody()->getId(), shape->getId(), shape->getTag()};
1045}
1046
1047bool World3D::preSolveCallback(b3ShapeId shapeIdA, b3ShapeId shapeIdB, b3Pos point,
1048 b3Vec3 normal, void *context) {
1049 auto *world = static_cast<World3D *>(context);
1050 if (!world) return true;
1051 auto findShape = [world](b3ShapeId id) -> Shape3D * {
1052 const auto raw = world->shapeRawHandles_.find(b3StoreShapeId(id));
1053 if (raw == world->shapeRawHandles_.end()) return nullptr;
1054 const auto found = world->shapeHandles_.find(raw->second);
1055 return found == world->shapeHandles_.end() ? nullptr : found->second;
1056 };
1057 Shape3D *shapeA = findShape(shapeIdA);
1058 Shape3D *shapeB = findShape(shapeIdB);
1059 if (shapeA && shapeA->isOneWay() &&
1060 !shapeA->allowsOneWayContact(point.x, point.y, point.z, normal.x, normal.y, normal.z))
1061 return false;
1062 if (shapeB && shapeB->isOneWay() &&
1063 !shapeB->allowsOneWayContact(point.x, point.y, point.z, -normal.x, -normal.y,
1064 -normal.z))
1065 return false;
1066 return true;
1067}
1068
1070 if (!shape) return;
1071 for (auto &[key, record] : shapeRecords_) {
1072 (void)key;
1073 if (record.shapeId == shape->getId()) record.shapeTag = shape->getTag();
1074 }
1075}
1076
1077World3D::EventShape World3D::eventShapeFrom(b3ShapeId shapeId) const {
1078 auto found = shapeRecords_.find(b3StoreShapeId(shapeId));
1079 if (found != shapeRecords_.end()) return found->second;
1080 Shape3D *shape = shapeFromShape(shapeId);
1081 Body3D *body = bodyFromShape(shapeId);
1082 if (!shape || !body) return {};
1083 return EventShape{body->getId(), shape->getId(), shape->getTag()};
1084}
1085
1087 beginContacts_.clear();
1088 endContacts_.clear();
1089 beginTriggers_.clear();
1090 endTriggers_.clear();
1091 hits_.clear();
1092 jointStressEvents_.clear();
1093 contactPoints_.clear();
1094}
1095
1096int World3D::queryBodyContacts(int bodyId, int maxPoints) {
1097 contactPoints_.clear();
1098 if (bodyId <= 0)
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]");
1103 if (!isValid()) return 0;
1104
1105 Body3D *target = nullptr;
1106 for (Body3D *body : bodies_) {
1107 if (body && body->isValid() && body->getId() == bodyId) {
1108 target = body;
1109 break;
1110 }
1111 }
1112 if (!target) return 0;
1113
1114 constexpr int maxContactPairs = 4096;
1115 const int capacity = std::min(b3Body_GetContactCapacity(target->raw()), maxContactPairs);
1116 if (capacity <= 0) return 0;
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)));
1120
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());
1134
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(),
1144 ownShape->getTag(),
1145 otherBody->getId(),
1146 otherShape->getId(),
1147 otherShape->getTag(),
1148 point.featureId,
1149 static_cast<float>(worldPoint.x),
1150 static_cast<float>(worldPoint.y),
1151 static_cast<float>(worldPoint.z),
1152 normal.x,
1153 normal.y,
1154 normal.z,
1155 point.separation,
1156 point.normalImpulse,
1157 point.totalNormalImpulse,
1158 point.normalVelocity,
1159 point.persisted});
1160 }
1161 }
1162 }
1163
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;
1171 return a.z < b.z;
1172 });
1173 if (static_cast<int>(contactPoints_.size()) > maxPoints)
1174 contactPoints_.resize(static_cast<size_t>(maxPoints));
1175 return static_cast<int>(contactPoints_.size());
1176}
1177
1179 if (!isValid()) return;
1180
1181 auto *ev = eve::ModuleManager::getInstance<eve::platform_event::PlatformEvent>("PlatformEvent");
1182
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});
1190 if (ev) {
1191 std::vector<eve::platform_event::Variant> args = {
1195 ev->push(new eve::platform_event::Message("begincontact3d", args));
1196 }
1197 }
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});
1204 if (ev) {
1205 std::vector<eve::platform_event::Variant> args = {
1209 ev->push(new eve::platform_event::Message("endcontact3d", args));
1210 }
1211 }
1212
1213 for (int i = 0; i < contacts.hitCount; ++i) {
1214 const b3ContactHitEvent &source = contacts.hitEvents[i];
1215 EventShape a = eventShapeFrom(source.shapeIdA);
1216 EventShape b = eventShapeFrom(source.shapeIdB);
1217 if (a.shapeId == 0 || b.shapeId == 0) continue;
1218
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;
1226 }
1227 }
1228
1229 b3Vec3 normal = source.normal;
1230 if (a.shapeId > b.shapeId) {
1231 std::swap(a, b);
1232 normal = -normal;
1233 }
1234 hits_.push_back({a,
1235 b,
1236 static_cast<float>(source.point.x),
1237 static_cast<float>(source.point.y),
1238 static_cast<float>(source.point.z),
1239 normal.x,
1240 normal.y,
1241 normal.z,
1242 source.approachSpeed,
1243 normalImpulse});
1244 if (ev) {
1245 std::vector<eve::platform_event::Variant> args = {
1249 ev->push(new eve::platform_event::Message("hit3d", args));
1250 }
1251 }
1252
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);
1257 if (sensor.shapeId == 0 || visitor.shapeId == 0) continue;
1258 beginTriggers_.push_back({sensor, visitor});
1259 if (ev) {
1260 std::vector<eve::platform_event::Variant> args = {eve::platform_event::Variant::makeInt(sensor.bodyId),
1266 ev->push(new eve::platform_event::Message("begintrigger3d", args));
1267 }
1268 }
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);
1272 if (sensor.shapeId == 0 || visitor.shapeId == 0) continue;
1273 endTriggers_.push_back({sensor, visitor});
1274 if (ev) {
1275 std::vector<eve::platform_event::Variant> args = {eve::platform_event::Variant::makeInt(sensor.bodyId),
1281 ev->push(new eve::platform_event::Message("endtrigger3d", args));
1282 }
1283 }
1284
1285 const b3JointEvents jointEvents = b3World_GetJointEvents(worldId_);
1286 for (int i = 0; i < jointEvents.count; ++i) {
1287 const b3JointEvent &source = jointEvents.jointEvents[i];
1288 auto *joint = static_cast<Joint3D *>(source.userData);
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)
1293 continue;
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,
1299 torque.z});
1300 if (ev) {
1301 std::vector<eve::platform_event::Variant> args = {
1303 eve::platform_event::Variant::makeInt(joint->getBodyAId()),
1304 eve::platform_event::Variant::makeInt(joint->getBodyBId()),
1305 eve::platform_event::Variant::makeInt(static_cast<int>(joint->kind_))};
1306 ev->push(new eve::platform_event::Message("jointstress3d", args));
1307 }
1308 }
1309
1310 // Invalid handles are retained only until the step that can report their end event.
1311 // This keeps destruction-safe metadata without allowing the history map to grow forever.
1312 for (auto it = shapeRecords_.begin(); it != shapeRecords_.end();) {
1313 if (!b3Shape_IsValid(b3LoadShapeId(it->first)))
1314 it = shapeRecords_.erase(it);
1315 else
1316 ++it;
1317 }
1318 for (auto it = shapeRawHandles_.begin(); it != shapeRawHandles_.end();) {
1319 if (!b3Shape_IsValid(b3LoadShapeId(it->first)))
1320 it = shapeRawHandles_.erase(it);
1321 else
1322 ++it;
1323 }
1324 for (auto it = shapeHandles_.begin(); it != shapeHandles_.end();) {
1325 if (!it->second || !it->second->isValid())
1326 it = shapeHandles_.erase(it);
1327 else
1328 ++it;
1329 }
1330}
1331
1332int World3D::rayCast(float x1, float y1, float z1, float x2, float y2, float z2) {
1333 rayCastAll(x1, y1, z1, x2, y2, z2, 1);
1334 return rayHitBodyId_;
1335}
1336
1337int World3D::rayCastFiltered(float x1, float y1, float z1, float x2, float y2, float z2,
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_;
1343}
1344
1345eve::Result<RayHit3D> World3D::rayCastOwned(float x1, float y1, float z1, float x2,
1346 float y2, float z2, QueryFilter3D filter) {
1347 if (!isValid())
1348 return ownedQueryFailure<RayHit3D>(eve::DiagnosticCode::StaleHandle,
1349 "physics world is no longer valid", "world");
1350 try {
1351 ScopedQueryState3D queryState(*this, filter);
1352 RayHit3D result;
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))
1356 return ownedQueryFailure<RayHit3D>(eve::DiagnosticCode::InvalidArgument,
1357 "ray endpoints must be finite", "ray");
1358 struct Collector {
1359 World3D* world = nullptr;
1360 RayHit3D* result = nullptr;
1361 static float callback(b3ShapeId shapeId, b3Pos point, b3Vec3 normal, float fraction,
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;
1365 Body3D* body = bodyFromShape(shapeId);
1366 Shape3D* shape = shapeFromShape(shapeId);
1367 if (!body || !shape || !body->isValid() || !shape->isValid()) return -1.f;
1368 RayHit3D candidate;
1369 candidate.hit = true;
1370 candidate.world = self->result->world;
1371 candidate.body = body->runtimeHandle();
1372 candidate.shape = shape->runtimeHandle();
1373 candidate.bodyId = body->getId();
1374 candidate.shapeId = shape->getId();
1375 candidate.shapeTag = shape->getTag();
1376 candidate.materialId = static_cast<int>(static_cast<std::uint32_t>(materialId));
1377 candidate.triangleIndex = triangleIndex;
1378 candidate.x = static_cast<float>(point.x);
1379 candidate.y = static_cast<float>(point.y);
1380 candidate.z = static_cast<float>(point.z);
1381 candidate.normalX = normal.x;
1382 candidate.normalY = normal.y;
1383 candidate.normalZ = normal.z;
1384 candidate.fraction = fraction;
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;
1389 }
1390 } collector{this, &result};
1391 b3World_CastRay(worldId_, b3Pos{x1, y1, z1}, b3Vec3{x2 - x1, y2 - y1, z2 - z1},
1392 makeQueryFilter(), &Collector::callback, &collector);
1393 if (!result.hit) return eve::Result<RayHit3D>::success(std::move(result));
1394 if (result.body.isInvalid() || result.shape.isInvalid())
1395 return ownedQueryFailure<RayHit3D>(
1397 "ray hit could not be mapped to live generation-qualified handles", "hit");
1398 return eve::Result<RayHit3D>::success(std::move(result));
1399 } catch (const std::exception& error) {
1400 return ownedQueryFailure<RayHit3D>(eve::DiagnosticCode::InvalidArgument, error.what(),
1401 "ray");
1402 }
1403}
1404
1405int World3D::rayCastAll(float x1, float y1, float z1, float x2, float y2, float z2,
1406 int maxHits) {
1407 return rayCastAllInternal(x1, y1, z1, x2, y2, z2, maxHits, makeQueryFilter());
1408}
1409
1410int World3D::rayCastAllInternal(float x1, float y1, float z1, float x2, float y2,
1411 float z2, int maxHits, b3QueryFilter filter) {
1412 rayHitBodyId_ = -1;
1413 rayHitShapeId_ = -1;
1414 rayHitShapeTag_ = 0;
1415 rayHitMaterialId_ = 0;
1416 rayHitTriangleIndex_ = -1;
1417 rayHitX_ = 0.f;
1418 rayHitY_ = 0.f;
1419 rayHitZ_ = 0.f;
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]");
1431 if (!isValid()) return 0;
1432
1433 b3Pos origin{x1, y1, z1};
1434 b3Vec3 translation{x2 - x1, y2 - y1, z2 - z1};
1435 struct RayCollector {
1436 World3D *world = nullptr;
1437 int capacity = 0;
1438
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);
1442 }
1443
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;
1449 }
1450
1451 static float callback(b3ShapeId shapeId, b3Pos point, b3Vec3 normal, float 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);
1457 if (!body || !shape) return -1.f;
1458
1459 RayResult candidate{body->getId(),
1460 shape->getId(),
1461 shape->getTag(),
1462 static_cast<int>(static_cast<uint32_t>(materialId)),
1463 triangleIndex,
1464 static_cast<float>(point.x),
1465 static_cast<float>(point.y),
1466 static_cast<float>(point.z),
1467 normal.x,
1468 normal.y,
1469 normal.z,
1470 fraction};
1471 auto &results = self->world->rayResults_;
1472 if (static_cast<int>(results.size()) < self->capacity) {
1473 results.push_back(candidate);
1474 } else {
1475 auto worst = std::max_element(results.begin(), results.end(), better);
1476 if (better(candidate, *worst)) *worst = candidate;
1477 }
1478 return self->clipFraction();
1479 }
1480 } collector{this, maxHits};
1481 rayResults_.reserve(static_cast<size_t>(maxHits));
1482 b3World_CastRay(worldId_, origin, translation, filter, &RayCollector::callback, &collector);
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;
1491 rayHitX_ = hit.x;
1492 rayHitY_ = hit.y;
1493 rayHitZ_ = hit.z;
1494 rayHitNormalX_ = hit.normalX;
1495 rayHitNormalY_ = hit.normalY;
1496 rayHitNormalZ_ = hit.normalZ;
1497 rayHitFraction_ = hit.fraction;
1498 }
1499 return static_cast<int>(rayResults_.size());
1500}
1501
1502int World3D::queryAABB(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) {
1503 queryBodyIds_.clear();
1504 queryShapes_.clear();
1505 if (!isValid()) return 0;
1506
1507 struct Collector {
1508 World3D *world = nullptr;
1509 std::vector<int> *bodyIds = nullptr;
1510 std::vector<std::pair<int, int>> *shapes = nullptr;
1511 std::unordered_set<int> seen;
1512
1513 static bool callback(b3ShapeId shapeId, void *context) {
1514 auto *self = static_cast<Collector *>(context);
1515 if (self->world->shouldIgnoreQueryShape(shapeId)) return true;
1516 Body3D *b = bodyFromShape(shapeId);
1517 Shape3D *shape = shapeFromShape(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());
1522 return true;
1523 }
1524 } cb;
1525 cb.world = this;
1526 cb.bodyIds = &queryBodyIds_;
1527 cb.shapes = &queryShapes_;
1528
1529 b3AABB aabb;
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());
1537}
1538
1540 float minX, float minY, float minZ, float maxX, float maxY, float maxZ,
1542 if (!isValid())
1543 return ownedQueryFailure<BroadPhaseAabb3D>(eve::DiagnosticCode::StaleHandle,
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))
1547 return ownedQueryFailure<BroadPhaseAabb3D>(eve::DiagnosticCode::InvalidArgument,
1548 "broad-phase AABB must be finite", "aabb");
1549 try {
1550 ScopedQueryState3D queryState(*this, filter);
1551 BroadPhaseAabb3D result;
1552 struct Collector {
1553 World3D* world = nullptr;
1554 BroadPhaseAabb3D* result = nullptr;
1555
1556 static bool less(const BroadPhaseHit3D& lhs, const BroadPhaseHit3D& rhs) noexcept {
1557 return lhs.bodyId != rhs.bodyId ? lhs.bodyId < rhs.bodyId : lhs.shapeId < rhs.shapeId;
1558 }
1559
1560 static bool callback(b3ShapeId shapeId, void* context) {
1561 auto* self = static_cast<Collector*>(context);
1562 if (self->world->shouldIgnoreQueryShape(shapeId)) return true;
1563 Body3D* body = bodyFromShape(shapeId);
1564 Shape3D* shape = shapeFromShape(shapeId);
1565 if (!body || !shape || !body->isValid() || !shape->isValid()) return true;
1566 BroadPhaseHit3D hit{body->runtimeHandle(), shape->runtimeHandle(), body->getId(),
1567 shape->getId(), shape->getTag(), shape->getMaterialId()};
1568 auto& output = *self->result;
1569 if (output.count < BroadPhaseAabb3D::Capacity) {
1570 output.hits[output.count++] = hit;
1571 } else {
1572 output.truncated = true;
1573 auto worst = std::max_element(output.hits.begin(), output.hits.end(), less);
1574 if (less(hit, *worst)) *worst = hit;
1575 }
1576 return true;
1577 }
1578 } collector{this, &result};
1579 b3AABB bounds;
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),
1584 Collector::less);
1585 return eve::Result<BroadPhaseAabb3D>::success(std::move(result));
1586 } catch (const std::exception& error) {
1587 return ownedQueryFailure<BroadPhaseAabb3D>(eve::DiagnosticCode::InvalidArgument,
1588 error.what(), "aabb");
1589 }
1590}
1591
1592int World3D::overlapProxy(b3Pos origin, const b3ShapeProxy &proxy) {
1593 queryBodyIds_.clear();
1594 queryShapes_.clear();
1595 if (!isValid()) return 0;
1596
1597 struct Collector {
1598 World3D *world = nullptr;
1599 std::vector<int> *bodyIds = nullptr;
1600 std::vector<std::pair<int, int>> *shapes = nullptr;
1601 std::unordered_set<int> seen;
1602
1603 static bool callback(b3ShapeId shapeId, void *context) {
1604 auto *self = static_cast<Collector *>(context);
1605 if (self->world->shouldIgnoreQueryShape(shapeId)) return true;
1606 Body3D *body = bodyFromShape(shapeId);
1607 Shape3D *shape = shapeFromShape(shapeId);
1608 if (body && shape) {
1609 if (self->seen.insert(body->getId()).second)
1610 self->bodyIds->push_back(body->getId());
1611 self->shapes->emplace_back(shape->getId(), shape->getTag());
1612 }
1613 return true;
1614 }
1615 } collector;
1616 collector.world = this;
1617 collector.bodyIds = &queryBodyIds_;
1618 collector.shapes = &queryShapes_;
1619 b3QueryFilter filter = makeQueryFilter();
1620 b3World_OverlapShape(worldId_, origin, &proxy, filter, &Collector::callback, &collector);
1621 // Broad-phase traversal order is unspecified; stable ids make script behavior deterministic.
1622 std::sort(queryBodyIds_.begin(), queryBodyIds_.end());
1623 std::sort(queryShapes_.begin(), queryShapes_.end());
1624 return static_cast<int>(queryBodyIds_.size());
1625}
1626
1627int World3D::querySphere(float x, float y, float z, float radius) {
1628 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) || !(radius >= 0.f) ||
1629 !std::isfinite(radius))
1630 throw eve::Exception("World3D.querySphere: coordinates and radius must be finite; radius >= 0");
1631 const b3Vec3 point = b3Vec3_zero;
1632 const b3ShapeProxy proxy{&point, 1, radius};
1633 return overlapProxy(b3Pos{x, y, z}, proxy);
1634}
1635
1636int World3D::queryCapsule(float ax, float ay, float az, float bx, float by, float bz,
1637 float radius) {
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");
1641 // Relative points preserve Box3D's large-world precision.
1642 const b3Vec3 points[2] = {b3Vec3_zero, b3Vec3{bx - ax, by - ay, bz - az}};
1643 const b3ShapeProxy proxy{points, 2, radius};
1644 return overlapProxy(b3Pos{ax, ay, az}, proxy);
1645}
1646
1648 float ax, float ay, float az, float bx, float by, float bz, float radius,
1650 if (!isValid())
1651 return ownedQueryFailure<CapsuleOverlap3D>(eve::DiagnosticCode::StaleHandle,
1652 "physics world is no longer valid", "world");
1653 try {
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) ||
1657 !std::isfinite(radius) || radius < 0.f)
1658 return ownedQueryFailure<CapsuleOverlap3D>(eve::DiagnosticCode::InvalidArgument,
1659 "capsule coordinates and radius are invalid", "capsule");
1660 CapsuleOverlap3D result;
1661 result.world = runtimeHandle_;
1662 struct Collector {
1663 World3D* world = nullptr;
1664 std::array<int, 256> bodyIds{};
1665 std::size_t count = 0;
1666 static bool callback(b3ShapeId shapeId, void* context) {
1667 auto* self = static_cast<Collector*>(context);
1668 if (self->world->shouldIgnoreQueryShape(shapeId)) return true;
1669 Body3D* body = bodyFromShape(shapeId);
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))
1675 return true;
1676 if (self->count < self->bodyIds.size()) self->bodyIds[self->count++] = bodyId;
1677 return true;
1678 }
1679 } collector{this};
1680 const b3Vec3 points[2] = {b3Vec3_zero, b3Vec3{bx - ax, by - ay, bz - az}};
1681 const b3ShapeProxy proxy{points, 2, radius};
1682 b3World_OverlapShape(worldId_, b3Pos{ax, ay, az}, &proxy, makeQueryFilter(),
1683 &Collector::callback, &collector);
1684 result.bodyCount = static_cast<int>(collector.count);
1685 return eve::Result<CapsuleOverlap3D>::success(std::move(result));
1686 } catch (const std::exception& error) {
1687 return ownedQueryFailure<CapsuleOverlap3D>(eve::DiagnosticCode::InvalidArgument,
1688 error.what(), "capsule");
1689 }
1690}
1691
1692int World3D::queryBox(float x, float y, float z, float width, float height, float depth, float qx,
1693 float qy, float qz, float qw) {
1694 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
1695 throw eve::Exception("World3D.queryBox: center must be finite");
1696 const b3Quat rotation = normalizedQueryQuat(qx, qy, qz, qw, "World3D.queryBox");
1697 b3Vec3 points[8];
1698 makeBoxProxyPoints(points, width, height, depth, rotation, "World3D.queryBox");
1699 const b3ShapeProxy proxy{points, 8, 0.f};
1700 return overlapProxy(b3Pos{x, y, z}, proxy);
1701}
1702
1703int World3D::castProxyAll(b3Pos origin, const b3ShapeProxy &proxy, b3Vec3 translation,
1704 int maxHits) {
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]");
1717 if (!isValid()) return 0;
1718
1719 struct CastCollector {
1720 World3D *world = nullptr;
1721 int capacity = 0;
1722
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);
1726 }
1727
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;
1733 }
1734
1735 static float callback(b3ShapeId shapeId, b3Pos point, b3Vec3 normal, float 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);
1741 if (!body || !shape) return -1.f;
1742
1743 ShapeCastResult candidate{body->getId(),
1744 shape->getId(),
1745 shape->getTag(),
1746 static_cast<int>(static_cast<uint32_t>(materialId)),
1747 triangleIndex,
1748 static_cast<float>(point.x),
1749 static_cast<float>(point.y),
1750 static_cast<float>(point.z),
1751 normal.x,
1752 normal.y,
1753 normal.z,
1754 fraction};
1755 auto &results = self->world->shapeCastResults_;
1756 if (static_cast<int>(results.size()) < self->capacity) {
1757 results.push_back(candidate);
1758 } else {
1759 auto worst = std::max_element(results.begin(), results.end(), better);
1760 if (better(candidate, *worst)) *worst = candidate;
1761 }
1762 return self->clipFraction();
1763 }
1764 } collector{this, maxHits};
1765 shapeCastResults_.reserve(static_cast<size_t>(maxHits));
1766 b3QueryFilter filter = makeQueryFilter();
1767 b3World_CastShape(worldId_, origin, &proxy, translation, filter, &CastCollector::callback,
1768 &collector);
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;
1784 }
1785 return static_cast<int>(shapeCastResults_.size());
1786}
1787
1788int World3D::castSphere(float x, float y, float z, float radius, float dx, float dy, float dz) {
1789 castSphereAll(x, y, z, radius, dx, dy, dz, 1);
1790 return shapeCastBodyId_;
1791}
1792
1793int World3D::castSphereAll(float x, float y, float z, float radius, float dx, float dy, float dz,
1794 int maxHits) {
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;
1799 const b3ShapeProxy proxy{&point, 1, radius};
1800 return castProxyAll(b3Pos{x, y, z}, proxy, b3Vec3{dx, dy, dz}, maxHits);
1801}
1802
1803int World3D::castCapsule(float ax, float ay, float az, float bx, float by, float bz, float radius,
1804 float dx, float dy, float dz) {
1805 castCapsuleAll(ax, ay, az, bx, by, bz, radius, dx, dy, dz, 1);
1806 return shapeCastBodyId_;
1807}
1808
1809int World3D::castCapsuleAll(float ax, float ay, float az, float bx, float by, float bz,
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");
1815 const b3Vec3 points[2] = {b3Vec3_zero, b3Vec3{bx - ax, by - ay, bz - az}};
1816 const b3ShapeProxy proxy{points, 2, radius};
1817 return castProxyAll(b3Pos{ax, ay, az}, proxy, b3Vec3{dx, dy, dz}, maxHits);
1818}
1819
1820int World3D::castBox(float x, float y, float z, float width, float height, float depth, float qx,
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_;
1824}
1825
1826int World3D::castBoxAll(float x, float y, float z, float width, float height, float depth, float qx,
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");
1832 b3Vec3 points[8];
1833 makeBoxProxyPoints(points, width, height, depth, rotation, "World3D.castBox");
1834 const b3ShapeProxy proxy{points, 8, 0.f};
1835 return castProxyAll(b3Pos{x, y, z}, proxy, b3Vec3{dx, dy, dz}, maxHits);
1836}
1837
1838int World3D::closestPoint(float x, float y, float z, float maxDistance) {
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))
1847 throw eve::Exception(
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);
1851 if (distance * distance > floatLimit ||
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");
1856 if (!isValid()) return -1;
1857
1858 struct ClosestCollector {
1859 World3D *world = nullptr;
1860 b3Vec3 target = b3Vec3_zero;
1861 float bestDistanceSquared = 0.f;
1862 int bodyId = -1;
1863 int stableShapeId = -1;
1864 int shapeTag = 0;
1865 b3Vec3 point = b3Vec3_zero;
1866
1867 static bool callback(b3ShapeId shapeId, void *context) {
1868 auto *self = static_cast<ClosestCollector *>(context);
1869 if (self->world->shouldIgnoreQueryShape(shapeId)) return true;
1870 Body3D *body = bodyFromShape(shapeId);
1871 Shape3D *shape = shapeFromShape(shapeId);
1872 if (!body || !shape) 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;
1888 }
1889 return true;
1890 }
1891 } collector;
1892 collector.world = this;
1893 collector.target = b3Vec3{x, y, z};
1894 collector.bestDistanceSquared = maxDistance * maxDistance;
1895
1896 const b3Vec3 extent{maxDistance, maxDistance, maxDistance};
1897 const b3AABB bounds{collector.target - extent, collector.target + extent};
1898 const b3QueryFilter filter = makeQueryFilter();
1899 b3World_OverlapAABB(worldId_, bounds, filter, &ClosestCollector::callback, &collector);
1900 if (collector.bodyId < 0) return -1;
1901
1902 closestBodyId_ = collector.bodyId;
1903 closestShapeId_ = collector.stableShapeId;
1904 closestShapeTag_ = collector.shapeTag;
1905 closestX_ = collector.point.x;
1906 closestY_ = collector.point.y;
1907 closestZ_ = collector.point.z;
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;
1914 }
1915 return closestBodyId_;
1916}
1917
1918void World3D::setMoverUp(float x, float y, float z) {
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};
1926}
1927
1929 if (!std::isfinite(degrees))
1930 throw eve::Exception("World3D.setMoverSlopeLimit: degrees must be finite");
1931 degrees = std::clamp(degrees, 0.f, 89.9f);
1932 moverSlopeCos_ = std::cos(degrees * 0.01745329251994329577f);
1933}
1934
1935bool World3D::moveCapsule(float ax, float ay, float az, float bx, float by, float bz,
1936 float radius, float dx, float dy, float dz) {
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");
1941
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;
1948 if (!isValid()) return false;
1949
1950 constexpr int planeCapacity = 16;
1951 struct PlaneCollector {
1952 World3D *world = nullptr;
1953 b3CollisionPlane planes[planeCapacity]{};
1954 int count = 0;
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;
1960
1961 static bool callback(b3ShapeId shapeId, const b3PlaneResult *results, int resultCount,
1962 void *context) {
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) {
1971 self->bestDot = d;
1972 self->bestNormal = plane.normal;
1973 }
1974 self->maxUpDot = std::max(self->maxUpDot, b3Dot(plane.normal, self->up));
1975 }
1976 return true;
1977 }
1978 } collector;
1979 collector.world = this;
1980 collector.up = moverUp_;
1981
1982 const b3Pos start{ax, ay, az};
1983 b3Pos current = start;
1984 const b3Pos target{ax + dx, ay + dy, az + dz};
1985 b3Capsule mover{b3Vec3_zero, b3Vec3{bx - ax, by - ay, bz - az}, radius};
1986 b3QueryFilter filter = makeQueryFilter();
1987 constexpr float toleranceSquared = 1e-8f;
1988 bool collided = false;
1989
1990 for (int pass = 0; pass < 5; ++pass) {
1991 collector.count = 0;
1992 collector.desired = target - current;
1993 b3World_CollideMover(worldId_, current, &mover, filter, &PlaneCollector::callback,
1994 &collector);
1995 moverPlaneCount_ += collector.count;
1996 collided = collided || collector.count > 0;
1997
1998 const b3Vec3 targetDelta = target - current;
1999 b3PlaneSolverResult solved = b3SolvePlanes(targetDelta, collector.planes, collector.count);
2000 b3Vec3 delta = solved.delta;
2001 struct MoverFilter {
2002 World3D *world = nullptr;
2003 static bool callback(b3ShapeId shapeId, void *context) {
2004 auto *self = static_cast<MoverFilter *>(context);
2005 return !self->world->shouldIgnoreQueryShape(shapeId);
2006 }
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;
2012 current = current + delta;
2013 moverIterations_ = pass + 1;
2014
2015 if (b3LengthSquared(delta) <= toleranceSquared) break;
2016 if (b3LengthSquared(target - current) <= toleranceSquared) break;
2017 }
2018
2019 const b3Vec3 total = current - start;
2020 moverDeltaX_ = total.x;
2021 moverDeltaY_ = total.y;
2022 moverDeltaZ_ = total.z;
2023 moverNormalX_ = collector.bestNormal.x;
2024 moverNormalY_ = collector.bestNormal.y;
2025 moverNormalZ_ = collector.bestNormal.z;
2026 moverGroundDot_ = collector.maxUpDot;
2027 moverGrounded_ = moverGroundDot_ >= moverSlopeCos_;
2028 return collided;
2029}
2030
2032 float ax, float ay, float az, float bx, float by, float bz, float radius, float dx,
2033 float dy, float dz, QueryFilter3D filter, CapsuleMovePolicy3D policy) {
2034 if (!isValid())
2035 return ownedQueryFailure<CapsuleMove3D>(eve::DiagnosticCode::StaleHandle,
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) ||
2040 policy.maxSlopeRadians < 0.f || policy.maxSlopeRadians >= 1.57079633f)
2041 return ownedQueryFailure<CapsuleMove3D>(eve::DiagnosticCode::InvalidArgument,
2042 "capsule mover policy is invalid", "policy");
2043 const b3Vec3 previousUp = moverUp_;
2044 const float previousSlopeCos = moverSlopeCos_;
2045 try {
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};
2050 moverSlopeCos_ = std::cos(policy.maxSlopeRadians);
2051 CapsuleMove3D result;
2052 result.world = runtimeHandle_;
2053 result.constrained = moveCapsule(ax, ay, az, bx, by, bz, radius, dx, dy, dz);
2054 result.grounded = isMoverGrounded();
2055 result.deltaX = getMoverDeltaX();
2056 result.deltaY = getMoverDeltaY();
2057 result.deltaZ = getMoverDeltaZ();
2058 result.normalX = getMoverNormalX();
2059 result.normalY = getMoverNormalY();
2060 result.normalZ = getMoverNormalZ();
2061 result.planeCount = getMoverPlaneCount();
2062 result.iterations = getMoverIterations();
2063 moverUp_ = previousUp;
2064 moverSlopeCos_ = previousSlopeCos;
2065 return eve::Result<CapsuleMove3D>::success(std::move(result));
2066 } catch (const std::exception& error) {
2067 moverUp_ = previousUp;
2068 moverSlopeCos_ = previousSlopeCos;
2069 return ownedQueryFailure<CapsuleMove3D>(eve::DiagnosticCode::InvalidArgument,
2070 error.what(), "capsuleMove");
2071 }
2072}
2073
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)];
2078}
2079
2080bool World3D::pointProbe(float x, float y, float z, float radius, ClothContact3D *out) const {
2081 return pointProbeFiltered(x, y, z, radius, out, ~uint64_t{0}, ~uint64_t{0}) == ClothProbeStatus::Hit;
2082}
2083
2085 uint64_t categoryBits, uint64_t maskBits) const {
2086 if (out) *out = ClothContact3D{};
2087 if (!isValid() || radius <= 0.f || !out) return ClothProbeStatus::Miss;
2088
2089 const b3Vec3 target{x, y, z};
2090 for (Shape3D *s : shapes_) {
2091 if (!s || !s->isValid() || s->isSensor()) continue;
2092 if ((maskBits & s->getCategoryBits()) == 0 || (s->getMaskBits() & categoryBits) == 0) continue;
2093 // Box3D closest-point proxies are convex-only. Concave surfaces are handled by
2094 // swept queries (and rigid contact generation), not b3Shape_GetClosestPoint.
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);
2100 const float depth = radius - d;
2101 if (depth > 0.f && depth > out->depth) {
2102 out->hit = true;
2103 out->depth = depth;
2104 if (d > 1e-6f) {
2105 out->nx = delta.x / d;
2106 out->ny = delta.y / d;
2107 out->nz = delta.z / d;
2108 } else {
2109 out->nx = 0.f;
2110 out->ny = 1.f;
2111 out->nz = 0.f;
2112 }
2113 out->body = s->getBody();
2114 out->friction = s->getFriction();
2115 out->restitution = s->getRestitution();
2116 }
2117 }
2119}
2120
2121const World3D::RayResult &World3D::rayResultAt(int index, const char *operation) const {
2122 if (index < 0 || index >= static_cast<int>(rayResults_.size()))
2123 throw eve::Exception("World3D.%s: index out of range", operation);
2124 return rayResults_[static_cast<size_t>(index)];
2125}
2126
2127#define EV_RAY_INT_GETTER(name, member) \
2128 int World3D::name(int index) const { return rayResultAt(index, #name).member; }
2129
2130EV_RAY_INT_GETTER(getRayResultBodyId, bodyId)
2131EV_RAY_INT_GETTER(getRayResultShapeId, shapeId)
2132EV_RAY_INT_GETTER(getRayResultShapeTag, shapeTag)
2133EV_RAY_INT_GETTER(getRayResultMaterialId, materialId)
2134EV_RAY_INT_GETTER(getRayResultTriangleIndex, triangleIndex)
2135
2136#undef EV_RAY_INT_GETTER
2137
2138#define EV_RAY_FLOAT_GETTER(name, member) \
2139 float World3D::name(int index) const { return rayResultAt(index, #name).member; }
2140
2141EV_RAY_FLOAT_GETTER(getRayResultX, x)
2142EV_RAY_FLOAT_GETTER(getRayResultY, y)
2143EV_RAY_FLOAT_GETTER(getRayResultZ, z)
2144EV_RAY_FLOAT_GETTER(getRayResultNormalX, normalX)
2145EV_RAY_FLOAT_GETTER(getRayResultNormalY, normalY)
2146EV_RAY_FLOAT_GETTER(getRayResultNormalZ, normalZ)
2147EV_RAY_FLOAT_GETTER(getRayResultFraction, fraction)
2148
2149#undef EV_RAY_FLOAT_GETTER
2150
2151const World3D::ShapeCastResult &World3D::shapeCastResultAt(int index,
2152 const char *operation) const {
2153 if (index < 0 || index >= static_cast<int>(shapeCastResults_.size()))
2154 throw eve::Exception("World3D.%s: index out of range", operation);
2155 return shapeCastResults_[static_cast<size_t>(index)];
2156}
2157
2158#define EV_CAST_INT_GETTER(name, member) \
2159 int World3D::name(int index) const { return shapeCastResultAt(index, #name).member; }
2160
2161EV_CAST_INT_GETTER(getShapeCastResultBodyId, bodyId)
2162EV_CAST_INT_GETTER(getShapeCastResultShapeId, shapeId)
2163EV_CAST_INT_GETTER(getShapeCastResultShapeTag, shapeTag)
2164EV_CAST_INT_GETTER(getShapeCastResultMaterialId, materialId)
2165EV_CAST_INT_GETTER(getShapeCastResultTriangleIndex, triangleIndex)
2166
2167#undef EV_CAST_INT_GETTER
2168
2169#define EV_CAST_FLOAT_GETTER(name, member) \
2170 float World3D::name(int index) const { return shapeCastResultAt(index, #name).member; }
2171
2172EV_CAST_FLOAT_GETTER(getShapeCastResultX, x)
2173EV_CAST_FLOAT_GETTER(getShapeCastResultY, y)
2174EV_CAST_FLOAT_GETTER(getShapeCastResultZ, z)
2175EV_CAST_FLOAT_GETTER(getShapeCastResultNormalX, normalX)
2176EV_CAST_FLOAT_GETTER(getShapeCastResultNormalY, normalY)
2177EV_CAST_FLOAT_GETTER(getShapeCastResultNormalZ, normalZ)
2178EV_CAST_FLOAT_GETTER(getShapeCastResultFraction, fraction)
2179
2180#undef EV_CAST_FLOAT_GETTER
2181
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;
2186}
2187
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;
2192}
2193
2194const World3D::ContactEvent &World3D::contactEventAt(const std::vector<ContactEvent> &events,
2195 int index, const char *operation) const {
2196 if (index < 0 || index >= static_cast<int>(events.size()))
2197 throw eve::Exception("World3D.%s: index out of range", operation);
2198 return events[static_cast<size_t>(index)];
2199}
2200
2201const World3D::TriggerEvent &World3D::triggerEventAt(const std::vector<TriggerEvent> &events,
2202 int index, const char *operation) const {
2203 if (index < 0 || index >= static_cast<int>(events.size()))
2204 throw eve::Exception("World3D.%s: index out of range", operation);
2205 return events[static_cast<size_t>(index)];
2206}
2207
2208const World3D::HitEvent &World3D::hitEventAt(int index, const char *operation) const {
2209 if (index < 0 || index >= static_cast<int>(hits_.size()))
2210 throw eve::Exception("World3D.%s: index out of range", operation);
2211 return hits_[static_cast<size_t>(index)];
2212}
2213
2214const World3D::JointStressEvent &World3D::jointStressEventAt(int index,
2215 const char *operation) const {
2216 if (index < 0 || index >= static_cast<int>(jointStressEvents_.size()))
2217 throw eve::Exception("World3D.%s: index out of range", operation);
2218 return jointStressEvents_[static_cast<size_t>(index)];
2219}
2220
2221#define EV_CONTACT_GETTER(name, events, member) \
2222 int World3D::name(int index) const { \
2223 return contactEventAt(events, index, #name).member; \
2224 }
2225
2226EV_CONTACT_GETTER(getBeginContactBodyAId, beginContacts_, shapeA.bodyId)
2227EV_CONTACT_GETTER(getBeginContactBodyBId, beginContacts_, shapeB.bodyId)
2228EV_CONTACT_GETTER(getBeginContactShapeAId, beginContacts_, shapeA.shapeId)
2229EV_CONTACT_GETTER(getBeginContactShapeBId, beginContacts_, shapeB.shapeId)
2230EV_CONTACT_GETTER(getBeginContactShapeATag, beginContacts_, shapeA.shapeTag)
2231EV_CONTACT_GETTER(getBeginContactShapeBTag, beginContacts_, shapeB.shapeTag)
2232EV_CONTACT_GETTER(getEndContactBodyAId, endContacts_, shapeA.bodyId)
2233EV_CONTACT_GETTER(getEndContactBodyBId, endContacts_, shapeB.bodyId)
2234EV_CONTACT_GETTER(getEndContactShapeAId, endContacts_, shapeA.shapeId)
2235EV_CONTACT_GETTER(getEndContactShapeBId, endContacts_, shapeB.shapeId)
2236EV_CONTACT_GETTER(getEndContactShapeATag, endContacts_, shapeA.shapeTag)
2237EV_CONTACT_GETTER(getEndContactShapeBTag, endContacts_, shapeB.shapeTag)
2238
2239#undef EV_CONTACT_GETTER
2240
2241#define EV_TRIGGER_GETTER(name, events, member) \
2242 int World3D::name(int index) const { \
2243 return triggerEventAt(events, index, #name).member; \
2244 }
2245
2246EV_TRIGGER_GETTER(getBeginTriggerSensorBodyId, beginTriggers_, sensor.bodyId)
2247EV_TRIGGER_GETTER(getBeginTriggerVisitorBodyId, beginTriggers_, visitor.bodyId)
2248EV_TRIGGER_GETTER(getBeginTriggerSensorShapeId, beginTriggers_, sensor.shapeId)
2249EV_TRIGGER_GETTER(getBeginTriggerVisitorShapeId, beginTriggers_, visitor.shapeId)
2250EV_TRIGGER_GETTER(getBeginTriggerSensorShapeTag, beginTriggers_, sensor.shapeTag)
2251EV_TRIGGER_GETTER(getBeginTriggerVisitorShapeTag, beginTriggers_, visitor.shapeTag)
2252EV_TRIGGER_GETTER(getEndTriggerSensorBodyId, endTriggers_, sensor.bodyId)
2253EV_TRIGGER_GETTER(getEndTriggerVisitorBodyId, endTriggers_, visitor.bodyId)
2254EV_TRIGGER_GETTER(getEndTriggerSensorShapeId, endTriggers_, sensor.shapeId)
2255EV_TRIGGER_GETTER(getEndTriggerVisitorShapeId, endTriggers_, visitor.shapeId)
2256EV_TRIGGER_GETTER(getEndTriggerSensorShapeTag, endTriggers_, sensor.shapeTag)
2257EV_TRIGGER_GETTER(getEndTriggerVisitorShapeTag, endTriggers_, visitor.shapeTag)
2258
2259#undef EV_TRIGGER_GETTER
2260
2261#define EV_HIT_INT_GETTER(name, member) \
2262 int World3D::name(int index) const { return hitEventAt(index, #name).member; }
2263
2264EV_HIT_INT_GETTER(getHitBodyAId, shapeA.bodyId)
2265EV_HIT_INT_GETTER(getHitBodyBId, shapeB.bodyId)
2266EV_HIT_INT_GETTER(getHitShapeAId, shapeA.shapeId)
2267EV_HIT_INT_GETTER(getHitShapeBId, shapeB.shapeId)
2268EV_HIT_INT_GETTER(getHitShapeATag, shapeA.shapeTag)
2269EV_HIT_INT_GETTER(getHitShapeBTag, shapeB.shapeTag)
2270
2271#undef EV_HIT_INT_GETTER
2272
2273#define EV_HIT_FLOAT_GETTER(name, member) \
2274 float World3D::name(int index) const { return hitEventAt(index, #name).member; }
2275
2276EV_HIT_FLOAT_GETTER(getHitPointX, pointX)
2277EV_HIT_FLOAT_GETTER(getHitPointY, pointY)
2278EV_HIT_FLOAT_GETTER(getHitPointZ, pointZ)
2279EV_HIT_FLOAT_GETTER(getHitNormalX, normalX)
2280EV_HIT_FLOAT_GETTER(getHitNormalY, normalY)
2281EV_HIT_FLOAT_GETTER(getHitNormalZ, normalZ)
2282EV_HIT_FLOAT_GETTER(getHitApproachSpeed, approachSpeed)
2283EV_HIT_FLOAT_GETTER(getHitNormalImpulse, normalImpulse)
2284
2285#undef EV_HIT_FLOAT_GETTER
2286
2287#define EV_JOINT_STRESS_INT_GETTER(name, member) \
2288 int World3D::name(int index) const { return jointStressEventAt(index, #name).member; }
2289EV_JOINT_STRESS_INT_GETTER(getJointStressJointId, jointId)
2290EV_JOINT_STRESS_INT_GETTER(getJointStressBodyAId, bodyAId)
2291EV_JOINT_STRESS_INT_GETTER(getJointStressBodyBId, bodyBId)
2292EV_JOINT_STRESS_INT_GETTER(getJointStressKind, kind)
2293#undef EV_JOINT_STRESS_INT_GETTER
2294
2295#define EV_JOINT_STRESS_FLOAT_GETTER(name, member) \
2296 float World3D::name(int index) const { return jointStressEventAt(index, #name).member; }
2297EV_JOINT_STRESS_FLOAT_GETTER(getJointStressForceX, forceX)
2298EV_JOINT_STRESS_FLOAT_GETTER(getJointStressForceY, forceY)
2299EV_JOINT_STRESS_FLOAT_GETTER(getJointStressForceZ, forceZ)
2300EV_JOINT_STRESS_FLOAT_GETTER(getJointStressTorqueX, torqueX)
2301EV_JOINT_STRESS_FLOAT_GETTER(getJointStressTorqueY, torqueY)
2302EV_JOINT_STRESS_FLOAT_GETTER(getJointStressTorqueZ, torqueZ)
2303#undef EV_JOINT_STRESS_FLOAT_GETTER
2304
2305const World3D::ContactPointResult &World3D::contactPointAt(int index,
2306 const char *operation) const {
2307 if (index < 0 || index >= static_cast<int>(contactPoints_.size()))
2308 throw eve::Exception("World3D.%s: index out of range", operation);
2309 return contactPoints_[static_cast<size_t>(index)];
2310}
2311
2312#define EV_CONTACT_POINT_INT_GETTER(name, member) \
2313 int World3D::name(int index) const { return contactPointAt(index, #name).member; }
2314
2315EV_CONTACT_POINT_INT_GETTER(getContactPointShapeId, shapeId)
2316EV_CONTACT_POINT_INT_GETTER(getContactPointShapeTag, shapeTag)
2317EV_CONTACT_POINT_INT_GETTER(getContactPointOtherBodyId, otherBodyId)
2318EV_CONTACT_POINT_INT_GETTER(getContactPointOtherShapeId, otherShapeId)
2319EV_CONTACT_POINT_INT_GETTER(getContactPointOtherShapeTag, otherShapeTag)
2320
2321#undef EV_CONTACT_POINT_INT_GETTER
2322
2323#define EV_CONTACT_POINT_FLOAT_GETTER(name, member) \
2324 float World3D::name(int index) const { return contactPointAt(index, #name).member; }
2325
2326EV_CONTACT_POINT_FLOAT_GETTER(getContactPointX, x)
2327EV_CONTACT_POINT_FLOAT_GETTER(getContactPointY, y)
2328EV_CONTACT_POINT_FLOAT_GETTER(getContactPointZ, z)
2329EV_CONTACT_POINT_FLOAT_GETTER(getContactPointNormalX, normalX)
2330EV_CONTACT_POINT_FLOAT_GETTER(getContactPointNormalY, normalY)
2331EV_CONTACT_POINT_FLOAT_GETTER(getContactPointNormalZ, normalZ)
2332EV_CONTACT_POINT_FLOAT_GETTER(getContactPointSeparation, separation)
2333EV_CONTACT_POINT_FLOAT_GETTER(getContactPointNormalImpulse, normalImpulse)
2334EV_CONTACT_POINT_FLOAT_GETTER(getContactPointTotalNormalImpulse, totalNormalImpulse)
2335EV_CONTACT_POINT_FLOAT_GETTER(getContactPointNormalVelocity, normalVelocity)
2336
2337#undef EV_CONTACT_POINT_FLOAT_GETTER
2338
2340 return contactPointAt(index, "isContactPointPersisted").persisted;
2341}
2342
2343} // namespace eve::physics
LogicalId target
ActionParameterOperation operation
double value
Duration start
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float duration
std::string output
const std::string & s
std::mutex mutex
float degrees
Definition CardTypes.cpp:35
int bz
Definition CaveMesh.cpp:114
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
int bx
Definition CaveMesh.cpp:114
float length
Definition CaveMesh.cpp:94
Vec3 anchor
Definition CaveMesh.cpp:90
int az
Definition CaveMesh.cpp:113
int by
Definition CaveMesh.cpp:114
float planes[6][4]
std::map< std::string, Var > values
std::string message
DiagnosticCode code
std::uint32_t capacity
std::uint32_t key
ShaderImageInput shape
wgpu::PopErrorScopeStatus status
std::array< double, 10 > q
HexVec3 up
std::int32_t second
std::int32_t first
std::uint32_t height
std::uint32_t width
TokenKind kind
std::array< float, 4 > rotation
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
Texture * normal
std::string error
Definition Package.cpp:60
World3D * world
float radius
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::shared_ptr< const std::vector< glm::vec2 > > points
bool hit
PrimitiveHandle handle
#define EV_PROFILE_MODULE(module, name)
Profile the enclosing scope, tagged with a module for grouping.
Definition Profile.h:140
float d
RenderSystem3D::GpuOpaqueCollector collector
V3 origin
Definition RoadBake.cpp:138
float axisZ
Definition RockMesh.cpp:22
float axisY
Definition RockMesh.cpp:22
float axisX
Definition RockMesh.cpp:22
bool found
std::string filter
double current
float dz
float dy
float dx
std::uint32_t count
Battle::Events events
TerrainThermalSettings settings
float separation
Definition TreeMesh.cpp:159
float step
Definition TreeMesh.cpp:314
std::string body
uint32_t index
const UnitySourceAsset & source
std::uint32_t depth
const VegetationPresetContext & context
ViewPreparation callback
glm::vec3 point
#define EV_RAY_FLOAT_GETTER(name, member)
Definition World3D.cpp:2138
#define EV_CONTACT_GETTER(name, events, member)
Definition World3D.cpp:2221
#define EV_COUNTER_GETTER(name, member)
Definition World3D.cpp:373
#define EV_CAST_INT_GETTER(name, member)
Definition World3D.cpp:2158
#define EV_HIT_FLOAT_GETTER(name, member)
Definition World3D.cpp:2273
#define EV_RAY_INT_GETTER(name, member)
Definition World3D.cpp:2127
#define EV_CONTACT_POINT_INT_GETTER(name, member)
Definition World3D.cpp:2312
#define EV_CONTACT_POINT_FLOAT_GETTER(name, member)
Definition World3D.cpp:2323
#define EV_TRIGGER_GETTER(name, events, member)
Definition World3D.cpp:2241
#define EV_CAST_FLOAT_GETTER(name, member)
Definition World3D.cpp:2169
#define EV_HIT_INT_GETTER(name, member)
Definition World3D.cpp:2261
#define EV_JOINT_STRESS_INT_GETTER(name, member)
Definition World3D.cpp:2287
#define EV_JOINT_STRESS_FLOAT_GETTER(name, member)
Definition World3D.cpp:2295
#define EV_PROFILE_GETTER(name, member)
Definition World3D.cpp:392
std::unordered_map< PhysicsWorldHandle::index_type, World3D * > worlds
Definition World3D.cpp:164
#define EV_EXPLOSION_FLOAT_GETTER(name, member)
Definition World3D.cpp:343
#define EV_EXPLOSION_INT_GETTER(name, member)
Definition World3D.cpp:337
float qy
float qx
float qw
float qz
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Definition Time.cpp:16
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
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.
Definition Status.h:68
Id128 public API.
Definition Identity.h:112
constexpr bool isNil() const noexcept
Returns whether this value is the all-zero nil ID.
Definition Identity.h:176
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...
Definition Body3D.h:24
b3BodyId raw() const
Raw.
Definition Body3D.h:349
World3D * getWorld()
Returns the owning world, or null after invalidation.
Definition Body3D.h:338
bool isValid() const
True while the underlying Box3D body is still alive.
Definition Body3D.cpp:216
int getId() const
Stable id used by collision events.
Definition Body3D.h:35
Script-facing Box3D joint owned by a World3D.
Definition Joint3D.h:17
PhysicsJointHandle runtimeHandle() const noexcept
Process-local generation-qualified handle owned by World3D.
Definition Joint3D.h:37
Composed mechanical assembly owned by a World3D.
Definition Mechanism3D.h:25
3D shape (box/sphere/capsule) attached to a Body3D with material settings. Created via Body3D::new*Sh...
Definition Shape3D.h:25
int getId() const
Stable world-local id, preserved when sensor state recreates the backend shape.
Definition Shape3D.h:44
int getTag() const
User-defined integer tag, zero by default.
Definition Shape3D.h:50
bool isValid() const
Definition Shape3D.cpp:229
Body3D * getBody()
Returns the owning body, or null after invalidation.
Definition Shape3D.h:284
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
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.
Definition World3D.cpp:1345
float getRestitutionThreshold() const
Current restitution speed threshold in m/s.
Definition World3D.cpp:235
void setGravity(float gx, float gy, float gz)
Gravity in m/s².
Definition World3D.cpp:775
int getQueryBodyId(int index) const
Body id of the i-th query hit.
Definition World3D.cpp:2074
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.
Definition World3D.cpp:1788
PhysicsJointHandle nextJointRuntimeHandle()
Internal: next generation-qualified joint handle.
Definition World3D.cpp:896
bool isWarmStartingEnabled() const
Whether constraint warm starting is enabled.
Definition World3D.cpp:276
SimulationObservation simulationObservation() const noexcept
Snapshot of completed backend steps and logical simulation time.
Definition World3D.cpp:761
float getMaximumLinearSpeed() const
Current world-wide maximum linear speed in m/s.
Definition World3D.cpp:268
int getAwakeBodyCount() const
Number of awake bodies.
Definition World3D.cpp:388
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...
Definition World3D.cpp:1636
friend void registerCameraObstructionWorld(World3D *world)
Registers camera obstruction world.
void update(float dt)
Steps the simulation by dt seconds (default substep count).
Definition World3D.cpp:726
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...
Definition World3D.cpp:1838
Joint3D * findJoint(PhysicsJointHandle handle) const
Resolves a live joint handle; returns null when stale or foreign.
Definition World3D.cpp:949
PhysicsWorldHandle runtimeHandle() const noexcept
Process-local identity used by PhysicsLink; invalid after destruction.
Definition World3D.h:141
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.
Definition World3D.cpp:1539
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.
Definition World3D.cpp:1820
void updateShapeTag(Shape3D *shape)
Internal: refreshes the tag snapshot for a live shape handle.
Definition World3D.cpp:1069
float getMoverNormalY() const
Representative blocking normal Y from the last move.
Definition World3D.h:757
eve::Result< void > step(const eve::SimulationStep &step, const SimulationSettings &settings={})
Advances the 3D domain with an injected deterministic step.
Definition World3D.cpp:739
void registerShapeHandle(Shape3D *shape)
Internal: snapshots a backend handle for destruction-safe end events.
Definition World3D.cpp:1026
Shape3D * findShapeById(int shapeId) const
Resolves a live shape by its world-local stable event/query id.
Definition World3D.cpp:941
float getBoundsMinZ() const
Minimum world-space Z covered by current broad-phase bounds.
Definition World3D.cpp:360
Shape3D * findShape(PhysicsShapeHandle handle) const
Resolves a live shape handle; returns null when stale or foreign.
Definition World3D.cpp:934
void forgetBody(Body3D *body)
Internal: wrapper teardown bookkeeping.
Definition World3D.cpp:1002
int getQueryShapeId(int index) const
Stable shape id of the i-th shape hit, sorted ascending.
Definition World3D.cpp:2182
float getGravityX() const
Definition World3D.cpp:780
int nextBodyId()
Internal: next stable body id.
Definition World3D.cpp:882
void setBodyPairCollisionEnabled(Body3D *bodyA, Body3D *bodyB, bool enabled)
Overrides collision for a live body pair without changing layer masks.
Definition World3D.cpp:810
void forgetJoint(Joint3D *joint)
Internal: removes a joint wrapper from ownership bookkeeping.
Definition World3D.cpp:714
World3D(float gravityX, float gravityY, float gravityZ, bool sleep, eve::PersistentId instanceId=eve::PersistentId::nil())
Creates a 3D physics world.
Definition World3D.cpp:173
int getQueryShapeTag(int index) const
User tag paired with the i-th shape hit.
Definition World3D.cpp:2188
float getMoverDeltaZ() const
Resolved translation Z from the last moveCapsule call.
Definition World3D.h:753
void setMoverUp(float x, float y, float z)
Set the up direction used for grounded and slope classification.
Definition World3D.cpp:1918
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.
Definition World3D.cpp:632
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.
Definition World3D.cpp:562
float getMoverDeltaY() const
Resolved translation Y from the last moveCapsule call.
Definition World3D.h:751
void setContactTuning(float hertz, float dampingRatio, float pushOutSpeed)
Configures contact overlap recovery.
Definition World3D.cpp:239
eve::Status backendSelectionStatus() const
Selection status, including a structured absent-capability warning.
Definition World3D.cpp:773
bool isBodyPairCollisionEnabled(Body3D *bodyA, Body3D *bodyB) const
Whether a body pair is permitted by the runtime contact override.
Definition World3D.cpp:827
int queryBodyContacts(int bodyId, int maxPoints)
Caches current touching manifold points for one stable Body3D ID. Results are oriented from the queri...
Definition World3D.cpp:1096
void setQueryIgnoredBodyId(int bodyId)
Ignores one stable Body3D ID in subsequent spatial queries; -1 disables.
Definition World3D.cpp:980
Body3D * findBody(PhysicsBodyHandle handle) const
Resolves a live body handle; returns null for a stale or foreign handle.
Definition World3D.cpp:918
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,...
Definition World3D.cpp:1803
float getMoverNormalZ() const
Representative blocking normal Z from the last move.
Definition World3D.h:759
bool isContinuousCollisionEnabled() const
Whether world-level continuous collision is enabled.
Definition World3D.cpp:225
int nextJointId()
Internal: next stable joint id.
Definition World3D.h:994
int querySphere(float x, float y, float z, float radius)
Query shapes overlapping a sphere in world meters.
Definition World3D.cpp:1627
void setWarmStartingEnabled(bool enabled)
Enables constraint warm starting for stable stacks and joints.
Definition World3D.cpp:272
float getGravityY() const
Definition World3D.cpp:785
PhysicsBodyHandle nextBodyRuntimeHandle()
Internal: next generation-qualified body handle.
Definition World3D.cpp:884
bool isShapePairCollisionEnabled(Shape3D *shapeA, Shape3D *shapeB) const
Whether a shape pair is permitted by the runtime contact override.
Definition World3D.cpp:849
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.
Definition World3D.cpp:1809
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....
Definition World3D.cpp:1332
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.
Definition World3D.cpp:2031
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.
Definition World3D.cpp:280
void setQueryFilter(int categoryBits, int maskBits)
Set category/mask bits for subsequent ray, overlap, cast and mover queries.
Definition World3D.cpp:970
float getMoverDeltaX() const
Resolved translation X from the last moveCapsule call.
Definition World3D.h:749
float getBoundsMaxZ() const
Maximum world-space Z covered by current broad-phase bounds.
Definition World3D.cpp:369
void setMoverSlopeLimit(float degrees)
Set maximum walkable slope in degrees, clamped to [0, 89.9].
Definition World3D.cpp:1928
void setHitEventThreshold(float speed)
Minimum approach speed in m/s required for enabled Shape3D hit events.
Definition World3D.cpp:795
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.
Definition World3D.cpp:1826
friend class Body3D
Definition World3D.h:1004
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.
Definition World3D.cpp:1647
b3WorldId raw() const
Raw Box3D world id.
Definition World3D.h:969
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.
Definition World3D.cpp:1692
void setMaximumLinearSpeed(float speed)
Sets the finite positive world-wide linear velocity clamp in m/s.
Definition World3D.cpp:262
bool isValid() const
True while the underlying Box3D world is alive.
Definition World3D.cpp:430
float getBoundsMinY() const
Minimum world-space Y covered by current broad-phase bounds.
Definition World3D.cpp:357
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.
Definition World3D.cpp:493
static World3D * findWorld(PhysicsWorldHandle handle) noexcept
Resolve a live process-local world handle, or null after teardown. @thread Owning physics thread only...
Definition World3D.cpp:484
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.
Definition World3D.cpp:597
float getBoundsMinX() const
Minimum world-space X covered by current broad-phase bounds.
Definition World3D.cpp:354
SimulationDeterminism backendDeterminism() const noexcept
Replay/numeric guarantee declared by the selected backend.
Definition World3D.cpp:769
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.
Definition World3D.cpp:667
void setRestitutionThreshold(float speed)
Minimum impact speed that permits material restitution, in m/s.
Definition World3D.cpp:229
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.
Definition World3D.cpp:532
float getContactRecycleDistance() const
Current contact-point persistence distance in meters.
Definition World3D.cpp:258
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...
Definition World3D.cpp:1502
void updateFull(float dt, int subStepCount)
Steps with an explicit substep count.
Definition World3D.cpp:728
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.
Definition World3D.cpp:1793
void destroyBody(Body3D *body)
Destroys a body (null is ignored).
Definition World3D.cpp:997
void setContinuousCollisionEnabled(bool enabled)
Enables world-level continuous collision against static geometry.
Definition World3D.cpp:221
void setQueryIgnoredShapeId(int shapeId)
Ignores one stable Shape3D ID in subsequent spatial queries; -1 disables.
Definition World3D.cpp:986
void forgetShape(Shape3D *shape)
Definition World3D.cpp:1006
void clearQueryIgnores()
Clears Body and Shape query exclusions without changing layer filtering.
Definition World3D.cpp:992
float getBoundsMaxX() const
Maximum world-space X covered by current broad-phase bounds.
Definition World3D.cpp:363
friend class Joint3D
Definition World3D.h:1005
Body3D * findBodyById(int bodyId) const
Resolves a live body by its world-local stable event/query id.
Definition World3D.cpp:926
float getGravityZ() const
Definition World3D.cpp:790
void clearContactEvents()
Clears all contact and trigger buffers before the next step.
Definition World3D.cpp:1086
void destroy()
Destroys the world and invalidates all wrappers.
Definition World3D.cpp:432
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...
Definition World3D.cpp:2080
void resetQueryFilter()
Restore the query filter to accept all lower-32-bit categories.
Definition World3D.cpp:975
void setShapePairCollisionEnabled(Shape3D *shapeA, Shape3D *shapeB, bool enabled)
Overrides collision for one live shape pair.
Definition World3D.cpp:835
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.
Definition World3D.cpp:2084
bool isContactPointPersisted(int index) const
Whether this manifold point existed in the previous step.
Definition World3D.cpp:2339
float getMoverNormalX() const
Representative blocking normal X from the last move.
Definition World3D.h:755
float getHitEventThreshold() const
Current significant-hit speed threshold in m/s.
Definition World3D.cpp:801
void emitContactEvents()
Internal: collects Box3D contact events into the event buffers.
Definition World3D.cpp:1178
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.
Definition World3D.cpp:1405
Body3D * newBody(const std::string &bodyType, float x, float y, float z)
Creates a body in meter-space units.
Definition World3D.cpp:902
int rayCastFiltered(float x1, float y1, float z1, float x2, float y2, float z2, uint64_t maskBits)
带形状类别掩码的最短射线查询(掩码为接受的 shape categoryBits)。
Definition World3D.cpp:1337
float getBoundsMaxY() const
Maximum world-space Y covered by current broad-phase bounds.
Definition World3D.cpp:366
void setContactRecycleDistance(float distance)
Sets contact-point persistence distance in meters; zero disables recycling.
Definition World3D.cpp:252
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...
Definition World3D.cpp:1935
PhysicsShapeHandle nextShapeRuntimeHandle()
Internal: next generation-qualified shape handle.
Definition World3D.cpp:890
int getMoverIterations() const
Number of outer solver passes used by the last move.
Definition World3D.h:763
int getMoverPlaneCount() const
Number of collision planes considered during the last move.
Definition World3D.h:761
friend class Shape3D
Definition World3D.h:1007
SimulationBackendKind backendKind() const noexcept
Selected CPU/GPU/mock backend family.
Definition World3D.cpp:765
bool isMoverGrounded() const
True when the last move touched a walkable plane.
Definition World3D.h:769
A named event carrying an ordered list of Variant payloads. Pushed messages are heap-allocated; the q...
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.
Definition Climbing.h:36
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.
Definition World3D.h:57
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
bool enabled
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
SimulationTick tick
Tick reached after this step is applied.
Definition Time.h:160
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.
Definition World3D.h:33
Owning result of continuous capsule movement.
PhysicsWorldHandle world
Per-call capsule mover classification policy; never mutates persistent World3D settings.
Owning summary of a capsule overlap query.
Result of a 3D point probe: deepest non-sensor shape within radius. Normal points from the shape towa...
Definition World3D.h:45
Query filter applied atomically for one owning query operation.
Owning snapshot of the closest ray intersection.
PhysicsShapeHandle shape
PhysicsBodyHandle body
PhysicsWorldHandle world
Observable backend progress shared by CPU and accelerator providers.
Validated solver policy for one simulation step.
Begin/end event for two non-sensor shapes.
Definition World3D.h:73
Stable snapshot describing one side of a 3D collision event.
Definition World3D.h:66
static Variant makeInt(int64_t v)
Constructs an integer variant.
glm::vec4 bounds