848 lines
34 KiB
C++
848 lines
34 KiB
C++
#include "physics/Body3D.h"
|
|
#include "physics/Shape3D.h"
|
|
#include "physics/World3D.h"
|
|
#include "physics/Joint3D.h"
|
|
|
|
#include "common/Exception.h"
|
|
|
|
#include <box3d/box3d.h>
|
|
|
|
#include <cmath>
|
|
#include <vector>
|
|
|
|
namespace eve::physics {
|
|
namespace {
|
|
|
|
template <class T>
|
|
eve::Result<T> bodyValueFailure(eve::DiagnosticCode code, std::string message, std::string path) {
|
|
return eve::Result<T>::failure(
|
|
eve::Diagnostic::error(code, std::move(message), std::move(path), {}, "physics.body3d"));
|
|
}
|
|
|
|
eve::Result<void> validateOwnedTransformInput(const Body3D& body, float x, float y, float z) {
|
|
if (!body.isValid())
|
|
return bodyValueFailure<void>(eve::DiagnosticCode::StaleHandle,
|
|
"physics body is no longer valid", "body");
|
|
if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
|
|
return bodyValueFailure<void>(eve::DiagnosticCode::InvalidArgument,
|
|
"transform input must be finite", "value");
|
|
return eve::Result<void>::success();
|
|
}
|
|
|
|
b3BodyType parseBodyType(const std::string &type) {
|
|
if (type == "static") return b3_staticBody;
|
|
if (type == "kinematic") return b3_kinematicBody;
|
|
if (type == "dynamic") return b3_dynamicBody;
|
|
throw eve::Exception("Body3D.setType: unknown body type '%s'", type.c_str());
|
|
}
|
|
|
|
const char *bodyTypeName(b3BodyType t) {
|
|
switch (t) {
|
|
case b3_staticBody: return "static";
|
|
case b3_kinematicBody: return "kinematic";
|
|
case b3_dynamicBody: return "dynamic";
|
|
default: return "static";
|
|
}
|
|
}
|
|
|
|
void requireFinite(float value, const char *operation, const char *parameter) {
|
|
if (!std::isfinite(value))
|
|
throw eve::Exception("%s: %s must be finite", operation, parameter);
|
|
}
|
|
|
|
void requireNonNegative(float value, const char *operation, const char *parameter) {
|
|
requireFinite(value, operation, parameter);
|
|
if (value < 0.f)
|
|
throw eve::Exception("%s: %s must be >= 0", operation, parameter);
|
|
}
|
|
|
|
b3Vec3 checkedVector(float x, float y, float z, const char *operation,
|
|
const char *parameter) {
|
|
if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
|
|
throw eve::Exception("%s: %s components must be finite", operation, parameter);
|
|
return {x, y, z};
|
|
}
|
|
|
|
b3Quat normalizedQuaternion(float qx, float qy, float qz, float qw, const char *operation) {
|
|
requireFinite(qx, operation, "qx");
|
|
requireFinite(qy, operation, "qy");
|
|
requireFinite(qz, operation, "qz");
|
|
requireFinite(qw, operation, "qw");
|
|
const float lengthSquared = qx * qx + qy * qy + qz * qz + qw * qw;
|
|
if (lengthSquared <= 1e-16f)
|
|
throw eve::Exception("%s: quaternion length must be > 0", operation);
|
|
const float inverseLength = 1.f / std::sqrt(lengthSquared);
|
|
return b3Quat{{qx * inverseLength, qy * inverseLength, qz * inverseLength},
|
|
qw * inverseLength};
|
|
}
|
|
|
|
b3ShapeDef makeShapeDef(float density, float friction, float restitution) {
|
|
b3ShapeDef def = b3DefaultShapeDef();
|
|
def.density = density;
|
|
def.baseMaterial.friction = friction;
|
|
def.baseMaterial.restitution = restitution;
|
|
def.enableContactEvents = true;
|
|
def.enableSensorEvents = true;
|
|
return def;
|
|
}
|
|
|
|
b3HullData *createCheckedHull(const std::vector<float> &vertices, int maxVertices,
|
|
const char *operation) {
|
|
if (vertices.size() < 12 || vertices.size() % 3 != 0)
|
|
throw eve::Exception("%s: vertices must contain at least four packed XYZ points",
|
|
operation);
|
|
if (vertices.size() / 3 > 100000)
|
|
throw eve::Exception("%s: source point count must be <= 100000", operation);
|
|
if (maxVertices < 4 || maxVertices > 254)
|
|
throw eve::Exception("%s: maxVertices must be in [4, 254]", operation);
|
|
std::vector<b3Vec3> points;
|
|
points.reserve(vertices.size() / 3);
|
|
for (size_t i = 0; i < vertices.size(); i += 3) {
|
|
if (!std::isfinite(vertices[i]) || !std::isfinite(vertices[i + 1]) ||
|
|
!std::isfinite(vertices[i + 2]))
|
|
throw eve::Exception("%s: all vertex components must be finite", operation);
|
|
points.push_back({vertices[i], vertices[i + 1], vertices[i + 2]});
|
|
}
|
|
b3HullData *hull = b3CreateHull(points.data(), static_cast<int>(points.size()), maxVertices);
|
|
if (!hull)
|
|
throw eve::Exception("%s: points do not form a valid three-dimensional convex hull",
|
|
operation);
|
|
return hull;
|
|
}
|
|
|
|
b3MeshData *createCheckedMesh(const std::vector<float> &vertices,
|
|
const std::vector<int32_t> &indices, bool weldVertices,
|
|
float weldTolerance, bool identifyEdges, bool useMedianSplit,
|
|
const char *operation) {
|
|
if (vertices.size() < 9 || vertices.size() % 3 != 0)
|
|
throw eve::Exception("%s: vertices must contain at least three packed XYZ points",
|
|
operation);
|
|
if (indices.size() < 3 || indices.size() % 3 != 0)
|
|
throw eve::Exception("%s: indices must contain complete triangles", operation);
|
|
if (vertices.size() / 3 > 1000000 || indices.size() / 3 > 2000000)
|
|
throw eve::Exception("%s: mesh exceeds 1000000 vertices or 2000000 triangles",
|
|
operation);
|
|
if (!std::isfinite(weldTolerance) || weldTolerance < 0.f)
|
|
throw eve::Exception("%s: weldTolerance must be finite and >= 0", operation);
|
|
std::vector<b3Vec3> points;
|
|
points.reserve(vertices.size() / 3);
|
|
for (size_t i = 0; i < vertices.size(); i += 3) {
|
|
if (!std::isfinite(vertices[i]) || !std::isfinite(vertices[i + 1]) ||
|
|
!std::isfinite(vertices[i + 2]))
|
|
throw eve::Exception("%s: all vertex components must be finite", operation);
|
|
points.push_back({vertices[i], vertices[i + 1], vertices[i + 2]});
|
|
}
|
|
for (int32_t index : indices) {
|
|
if (index < 0 || static_cast<size_t>(index) >= points.size())
|
|
throw eve::Exception("%s: triangle index is outside the vertex array", operation);
|
|
}
|
|
std::vector<int32_t> mutableIndices = indices;
|
|
b3MeshDef def{};
|
|
def.vertices = points.data();
|
|
def.indices = mutableIndices.data();
|
|
def.vertexCount = static_cast<int>(points.size());
|
|
def.triangleCount = static_cast<int>(indices.size() / 3);
|
|
def.weldVertices = weldVertices;
|
|
def.weldTolerance = weldTolerance;
|
|
def.identifyEdges = identifyEdges;
|
|
def.useMedianSplit = useMedianSplit;
|
|
b3MeshData *mesh = b3CreateMesh(&def, nullptr, 0);
|
|
if (!mesh || mesh->triangleCount != def.triangleCount) {
|
|
if (mesh) b3DestroyMesh(mesh);
|
|
throw eve::Exception("%s: mesh contains degenerate or zero-area triangles", operation);
|
|
}
|
|
return mesh;
|
|
}
|
|
|
|
b3HeightFieldData *createCheckedHeightField(int countX, int countZ, float cellSizeX,
|
|
float cellSizeZ,
|
|
const std::vector<float> &heights,
|
|
float globalMin, float globalMax,
|
|
bool clockwiseWinding, const char *operation) {
|
|
if (countX < 2 || countZ < 2)
|
|
throw eve::Exception("%s: countX and countZ must be >= 2", operation);
|
|
const size_t sampleCount = static_cast<size_t>(countX) * static_cast<size_t>(countZ);
|
|
if (sampleCount > 16000000)
|
|
throw eve::Exception("%s: sample count must be <= 16000000", operation);
|
|
if (heights.size() != sampleCount)
|
|
throw eve::Exception("%s: heights size must equal countX * countZ", operation);
|
|
if (!(cellSizeX > 0.f) || !(cellSizeZ > 0.f) || !std::isfinite(cellSizeX) ||
|
|
!std::isfinite(cellSizeZ))
|
|
throw eve::Exception("%s: cell sizes must be finite and > 0", operation);
|
|
if (!std::isfinite(globalMin) || !std::isfinite(globalMax) || globalMin > globalMax)
|
|
throw eve::Exception("%s: global height range must be finite and ordered", operation);
|
|
for (float height : heights) {
|
|
if (!std::isfinite(height) || height < globalMin || height > globalMax)
|
|
throw eve::Exception("%s: every height must be finite and inside global range",
|
|
operation);
|
|
}
|
|
std::vector<float> mutableHeights = heights;
|
|
b3HeightFieldDef def{};
|
|
def.heights = mutableHeights.data();
|
|
def.scale = {cellSizeX, 1.f, cellSizeZ};
|
|
def.countX = countX;
|
|
def.countZ = countZ;
|
|
def.globalMinimumHeight = globalMin;
|
|
def.globalMaximumHeight = globalMax;
|
|
def.clockwiseWinding = clockwiseWinding;
|
|
return b3CreateHeightField(&def);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Body3D::Body3D(World3D *world, b3BodyId bodyId, int id, PhysicsBodyHandle runtimeHandle)
|
|
: world_(world), bodyId_(bodyId), id_(id), runtimeHandle_(runtimeHandle) {}
|
|
|
|
Body3D::~Body3D() {
|
|
if (isValid() && world_ && world_->isValid()) {
|
|
std::vector<Joint3D *> joints(world_->joints_.begin(), world_->joints_.end());
|
|
for (Joint3D *joint : joints) {
|
|
if (joint && (joint->bodyA_ == this || joint->bodyB_ == this)) {
|
|
world_->forgetJoint(joint);
|
|
joint->invalidate();
|
|
}
|
|
}
|
|
// Invalidate shapes that still reference this body.
|
|
std::vector<Shape3D *> shapes(world_->shapes_.begin(), world_->shapes_.end());
|
|
for (Shape3D *s : shapes) {
|
|
if (s && s->getBody() == this) {
|
|
world_->forgetShape(s);
|
|
s->invalidate();
|
|
}
|
|
}
|
|
b3Body_SetUserData(bodyId_, nullptr);
|
|
b3DestroyBody(bodyId_);
|
|
world_->forgetBody(this);
|
|
}
|
|
bodyId_ = {};
|
|
world_ = nullptr;
|
|
runtimeHandle_ = PhysicsBodyHandle::invalid();
|
|
}
|
|
|
|
bool Body3D::isValid() const { return b3Body_IsValid(bodyId_); }
|
|
|
|
void Body3D::invalidate() {
|
|
if (isValid()) b3Body_SetUserData(bodyId_, nullptr);
|
|
bodyId_ = {};
|
|
world_ = nullptr;
|
|
runtimeHandle_ = PhysicsBodyHandle::invalid();
|
|
}
|
|
|
|
void Body3D::destroy() {
|
|
if (!isValid() || !world_ || !world_->isValid()) {
|
|
invalidate();
|
|
return;
|
|
}
|
|
std::vector<Joint3D *> joints(world_->joints_.begin(), world_->joints_.end());
|
|
for (Joint3D *joint : joints) {
|
|
if (joint && (joint->bodyA_ == this || joint->bodyB_ == this)) {
|
|
world_->forgetJoint(joint);
|
|
joint->invalidate();
|
|
}
|
|
}
|
|
std::vector<Shape3D *> shapes(world_->shapes_.begin(), world_->shapes_.end());
|
|
for (Shape3D *s : shapes) {
|
|
if (s && s->getBody() == this) {
|
|
world_->forgetShape(s);
|
|
// Destroy the underlying shape before invalidating the wrapper.
|
|
if (s->isValid()) b3DestroyShape(s->raw(), false);
|
|
s->invalidate();
|
|
}
|
|
}
|
|
b3Body_SetUserData(bodyId_, nullptr);
|
|
b3DestroyBody(bodyId_);
|
|
world_->forgetBody(this);
|
|
bodyId_ = {};
|
|
world_ = nullptr;
|
|
runtimeHandle_ = PhysicsBodyHandle::invalid();
|
|
}
|
|
|
|
void Body3D::setPosition(float x, float y, float z) {
|
|
if (!isValid()) return;
|
|
b3Body_SetTransform(bodyId_, b3Pos{x, y, z}, b3Body_GetRotation(bodyId_));
|
|
}
|
|
|
|
float Body3D::getX() const {
|
|
if (!isValid()) return 0.f;
|
|
return static_cast<float>(b3Body_GetPosition(bodyId_).x);
|
|
}
|
|
|
|
float Body3D::getY() const {
|
|
if (!isValid()) return 0.f;
|
|
return static_cast<float>(b3Body_GetPosition(bodyId_).y);
|
|
}
|
|
|
|
float Body3D::getZ() const {
|
|
if (!isValid()) return 0.f;
|
|
return static_cast<float>(b3Body_GetPosition(bodyId_).z);
|
|
}
|
|
|
|
void Body3D::setRotation(float qx, float qy, float qz, float qw) {
|
|
if (!isValid()) return;
|
|
b3Quat q;
|
|
q.v = b3Vec3{qx, qy, qz};
|
|
q.s = qw;
|
|
float len = std::sqrt(qx * qx + qy * qy + qz * qz + qw * qw);
|
|
if (len > 1e-8f) {
|
|
q.v.x /= len;
|
|
q.v.y /= len;
|
|
q.v.z /= len;
|
|
q.s /= len;
|
|
} else {
|
|
q = b3Quat_identity;
|
|
}
|
|
b3Body_SetTransform(bodyId_, b3Body_GetPosition(bodyId_), q);
|
|
}
|
|
|
|
float Body3D::getRotX() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetRotation(bodyId_).v.x;
|
|
}
|
|
|
|
float Body3D::getRotY() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetRotation(bodyId_).v.y;
|
|
}
|
|
|
|
float Body3D::getRotZ() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetRotation(bodyId_).v.z;
|
|
}
|
|
|
|
float Body3D::getRotW() const {
|
|
if (!isValid()) return 1.f;
|
|
return b3Body_GetRotation(bodyId_).s;
|
|
}
|
|
|
|
void Body3D::setLinearVelocity(float vx, float vy, float vz) {
|
|
if (!isValid()) return;
|
|
b3Body_SetLinearVelocity(bodyId_, b3Vec3{vx, vy, vz});
|
|
}
|
|
|
|
float Body3D::getLinearVelocityX() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetLinearVelocity(bodyId_).x;
|
|
}
|
|
|
|
float Body3D::getLinearVelocityY() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetLinearVelocity(bodyId_).y;
|
|
}
|
|
|
|
float Body3D::getLinearVelocityZ() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetLinearVelocity(bodyId_).z;
|
|
}
|
|
|
|
float Body3D::getMass() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetMass(bodyId_);
|
|
}
|
|
|
|
void Body3D::setAngularVelocity(float wx, float wy, float wz) {
|
|
if (!isValid()) return;
|
|
b3Body_SetAngularVelocity(bodyId_, b3Vec3{wx, wy, wz});
|
|
}
|
|
|
|
float Body3D::getAngularVelocityX() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetAngularVelocity(bodyId_).x;
|
|
}
|
|
|
|
float Body3D::getAngularVelocityY() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetAngularVelocity(bodyId_).y;
|
|
}
|
|
|
|
float Body3D::getAngularVelocityZ() const {
|
|
if (!isValid()) return 0.f;
|
|
return b3Body_GetAngularVelocity(bodyId_).z;
|
|
}
|
|
|
|
std::vector<float> Body3D::localToWorldPoint(float x, float y, float z) const {
|
|
if (!isValid()) return {0.f, 0.f, 0.f};
|
|
const b3Pos value = b3Body_GetWorldPoint(
|
|
bodyId_, checkedVector(x, y, z, "Body3D.localToWorldPoint", "point"));
|
|
return {static_cast<float>(value.x), static_cast<float>(value.y),
|
|
static_cast<float>(value.z)};
|
|
}
|
|
|
|
eve::Result<PhysicsVector3D> Body3D::localToWorldPointOwned(float x, float y, float z) const {
|
|
auto valid = validateOwnedTransformInput(*this, x, y, z);
|
|
if (!valid) return eve::Result<PhysicsVector3D>::failure(valid.status());
|
|
const b3Pos value = b3Body_GetWorldPoint(bodyId_, b3Vec3{x, y, z});
|
|
return eve::Result<PhysicsVector3D>::success(
|
|
{static_cast<float>(value.x), static_cast<float>(value.y), static_cast<float>(value.z)});
|
|
}
|
|
|
|
std::vector<float> Body3D::worldToLocalPoint(float x, float y, float z) const {
|
|
if (!isValid()) return {0.f, 0.f, 0.f};
|
|
const b3Vec3 value = b3Body_GetLocalPoint(
|
|
bodyId_, checkedVector(x, y, z, "Body3D.worldToLocalPoint", "point"));
|
|
return {value.x, value.y, value.z};
|
|
}
|
|
|
|
eve::Result<PhysicsVector3D> Body3D::worldToLocalPointOwned(float x, float y, float z) const {
|
|
auto valid = validateOwnedTransformInput(*this, x, y, z);
|
|
if (!valid) return eve::Result<PhysicsVector3D>::failure(valid.status());
|
|
const b3Vec3 value = b3Body_GetLocalPoint(bodyId_, b3Vec3{x, y, z});
|
|
return eve::Result<PhysicsVector3D>::success({value.x, value.y, value.z});
|
|
}
|
|
|
|
std::vector<float> Body3D::localToWorldVector(float x, float y, float z) const {
|
|
if (!isValid()) return {0.f, 0.f, 0.f};
|
|
const b3Vec3 value = b3Body_GetWorldVector(
|
|
bodyId_, checkedVector(x, y, z, "Body3D.localToWorldVector", "vector"));
|
|
return {value.x, value.y, value.z};
|
|
}
|
|
|
|
eve::Result<PhysicsVector3D> Body3D::localToWorldVectorOwned(float x, float y, float z) const {
|
|
auto valid = validateOwnedTransformInput(*this, x, y, z);
|
|
if (!valid) return eve::Result<PhysicsVector3D>::failure(valid.status());
|
|
const b3Vec3 value = b3Body_GetWorldVector(bodyId_, b3Vec3{x, y, z});
|
|
return eve::Result<PhysicsVector3D>::success({value.x, value.y, value.z});
|
|
}
|
|
|
|
std::vector<float> Body3D::worldToLocalVector(float x, float y, float z) const {
|
|
if (!isValid()) return {0.f, 0.f, 0.f};
|
|
const b3Vec3 value = b3Body_GetLocalVector(
|
|
bodyId_, checkedVector(x, y, z, "Body3D.worldToLocalVector", "vector"));
|
|
return {value.x, value.y, value.z};
|
|
}
|
|
|
|
std::vector<float> Body3D::getLocalPointVelocity(float x, float y, float z) const {
|
|
if (!isValid()) return {0.f, 0.f, 0.f};
|
|
const b3Vec3 value = b3Body_GetLocalPointVelocity(
|
|
bodyId_, checkedVector(x, y, z, "Body3D.getLocalPointVelocity", "point"));
|
|
return {value.x, value.y, value.z};
|
|
}
|
|
|
|
eve::Result<PhysicsVector3D> Body3D::getLocalPointVelocityOwned(float x, float y, float z) const {
|
|
auto valid = validateOwnedTransformInput(*this, x, y, z);
|
|
if (!valid) return eve::Result<PhysicsVector3D>::failure(valid.status());
|
|
const b3Vec3 value = b3Body_GetLocalPointVelocity(bodyId_, b3Vec3{x, y, z});
|
|
return eve::Result<PhysicsVector3D>::success({value.x, value.y, value.z});
|
|
}
|
|
|
|
std::vector<float> Body3D::getWorldPointVelocity(float x, float y, float z) const {
|
|
if (!isValid()) return {0.f, 0.f, 0.f};
|
|
const b3Vec3 value = b3Body_GetWorldPointVelocity(
|
|
bodyId_, checkedVector(x, y, z, "Body3D.getWorldPointVelocity", "point"));
|
|
return {value.x, value.y, value.z};
|
|
}
|
|
|
|
void Body3D::applyForce(float fx, float fy, float fz) {
|
|
if (!isValid()) return;
|
|
b3Body_ApplyForceToCenter(bodyId_, b3Vec3{fx, fy, fz}, true);
|
|
}
|
|
|
|
void Body3D::applyForceAt(float fx, float fy, float fz, float x, float y, float z) {
|
|
if (!isValid()) return;
|
|
b3Body_ApplyForce(bodyId_, b3Vec3{fx, fy, fz}, b3Pos{x, y, z}, true);
|
|
}
|
|
|
|
void Body3D::applyTorque(float tx, float ty, float tz) {
|
|
if (!isValid()) return;
|
|
b3Body_ApplyTorque(bodyId_, b3Vec3{tx, ty, tz}, true);
|
|
}
|
|
|
|
void Body3D::applyLinearImpulse(float ix, float iy, float iz) {
|
|
if (!isValid()) return;
|
|
b3Body_ApplyLinearImpulseToCenter(bodyId_, b3Vec3{ix, iy, iz}, true);
|
|
}
|
|
|
|
void Body3D::applyLinearImpulseAt(float ix, float iy, float iz, float x, float y, float z) {
|
|
if (!isValid()) return;
|
|
b3Body_ApplyLinearImpulse(bodyId_, b3Vec3{ix, iy, iz}, b3Pos{x, y, z}, true);
|
|
}
|
|
|
|
void Body3D::applyAngularImpulse(float ix, float iy, float iz) {
|
|
if (!isValid()) return;
|
|
b3Body_ApplyAngularImpulse(bodyId_, b3Vec3{ix, iy, iz}, true);
|
|
}
|
|
|
|
void Body3D::applyLocalForce(float fx, float fy, float fz, float x, float y, float z) {
|
|
if (!isValid()) return;
|
|
const b3Vec3 force = b3Body_GetWorldVector(
|
|
bodyId_, checkedVector(fx, fy, fz, "Body3D.applyLocalForce", "force"));
|
|
const b3Pos point = b3Body_GetWorldPoint(
|
|
bodyId_, checkedVector(x, y, z, "Body3D.applyLocalForce", "point"));
|
|
b3Body_ApplyForce(bodyId_, force, point, true);
|
|
}
|
|
|
|
void Body3D::applyLocalForceToCenter(float fx, float fy, float fz) {
|
|
if (!isValid()) return;
|
|
const b3Vec3 force = checkedVector(fx, fy, fz, "Body3D.applyLocalForceToCenter",
|
|
"force");
|
|
b3Body_ApplyForceToCenter(bodyId_, b3Body_GetWorldVector(bodyId_, force), true);
|
|
}
|
|
|
|
void Body3D::applyLocalTorque(float tx, float ty, float tz) {
|
|
if (!isValid()) return;
|
|
const b3Vec3 torque = checkedVector(tx, ty, tz, "Body3D.applyLocalTorque", "torque");
|
|
b3Body_ApplyTorque(bodyId_, b3Body_GetWorldVector(bodyId_, torque), true);
|
|
}
|
|
|
|
void Body3D::applyLocalLinearImpulse(float ix, float iy, float iz, float x, float y,
|
|
float z) {
|
|
if (!isValid()) return;
|
|
const b3Vec3 impulse = b3Body_GetWorldVector(
|
|
bodyId_, checkedVector(ix, iy, iz, "Body3D.applyLocalLinearImpulse", "impulse"));
|
|
const b3Pos point = b3Body_GetWorldPoint(
|
|
bodyId_, checkedVector(x, y, z, "Body3D.applyLocalLinearImpulse", "point"));
|
|
b3Body_ApplyLinearImpulse(bodyId_, impulse, point, true);
|
|
}
|
|
|
|
void Body3D::applyLocalLinearImpulseToCenter(float ix, float iy, float iz) {
|
|
if (!isValid()) return;
|
|
const b3Vec3 impulse = checkedVector(ix, iy, iz,
|
|
"Body3D.applyLocalLinearImpulseToCenter", "impulse");
|
|
b3Body_ApplyLinearImpulseToCenter(bodyId_, b3Body_GetWorldVector(bodyId_, impulse), true);
|
|
}
|
|
|
|
void Body3D::applyLocalAngularImpulse(float ix, float iy, float iz) {
|
|
if (!isValid()) return;
|
|
const b3Vec3 impulse =
|
|
checkedVector(ix, iy, iz, "Body3D.applyLocalAngularImpulse", "impulse");
|
|
b3Body_ApplyAngularImpulse(bodyId_, b3Body_GetWorldVector(bodyId_, impulse), true);
|
|
}
|
|
|
|
void Body3D::setTargetTransform(float x, float y, float z, float qx, float qy, float qz,
|
|
float qw, float timeStep) {
|
|
if (!isValid()) return;
|
|
requireFinite(x, "Body3D.setTargetTransform", "x");
|
|
requireFinite(y, "Body3D.setTargetTransform", "y");
|
|
requireFinite(z, "Body3D.setTargetTransform", "z");
|
|
requireFinite(timeStep, "Body3D.setTargetTransform", "timeStep");
|
|
if (timeStep <= 0.f)
|
|
throw eve::Exception("Body3D.setTargetTransform: timeStep must be > 0");
|
|
const b3Quat rotation =
|
|
normalizedQuaternion(qx, qy, qz, qw, "Body3D.setTargetTransform");
|
|
b3Body_SetTargetTransform(bodyId_, b3WorldTransform{b3Pos{x, y, z}, rotation}, timeStep,
|
|
true);
|
|
}
|
|
|
|
void Body3D::setLinearDamping(float damping) {
|
|
if (!isValid()) return;
|
|
requireNonNegative(damping, "Body3D.setLinearDamping", "damping");
|
|
b3Body_SetLinearDamping(bodyId_, damping);
|
|
}
|
|
|
|
float Body3D::getLinearDamping() const {
|
|
return isValid() ? b3Body_GetLinearDamping(bodyId_) : 0.f;
|
|
}
|
|
|
|
void Body3D::setAngularDamping(float damping) {
|
|
if (!isValid()) return;
|
|
requireNonNegative(damping, "Body3D.setAngularDamping", "damping");
|
|
b3Body_SetAngularDamping(bodyId_, damping);
|
|
}
|
|
|
|
float Body3D::getAngularDamping() const {
|
|
return isValid() ? b3Body_GetAngularDamping(bodyId_) : 0.f;
|
|
}
|
|
|
|
void Body3D::setGravityScale(float scale) {
|
|
if (!isValid()) return;
|
|
requireFinite(scale, "Body3D.setGravityScale", "scale");
|
|
b3Body_SetGravityScale(bodyId_, scale);
|
|
}
|
|
|
|
float Body3D::getGravityScale() const {
|
|
return isValid() ? b3Body_GetGravityScale(bodyId_) : 0.f;
|
|
}
|
|
|
|
void Body3D::setSleepEnabled(bool enabled) {
|
|
if (!isValid()) return;
|
|
b3Body_EnableSleep(bodyId_, enabled);
|
|
}
|
|
|
|
bool Body3D::isSleepEnabled() const {
|
|
return isValid() ? b3Body_IsSleepEnabled(bodyId_) : false;
|
|
}
|
|
|
|
void Body3D::setSleepThreshold(float threshold) {
|
|
if (!isValid()) return;
|
|
requireNonNegative(threshold, "Body3D.setSleepThreshold", "threshold");
|
|
b3Body_SetSleepThreshold(bodyId_, threshold);
|
|
}
|
|
|
|
float Body3D::getSleepThreshold() const {
|
|
return isValid() ? b3Body_GetSleepThreshold(bodyId_) : 0.f;
|
|
}
|
|
|
|
void Body3D::setMotionLocks(bool linearX, bool linearY, bool linearZ, bool angularX,
|
|
bool angularY, bool angularZ) {
|
|
if (!isValid()) return;
|
|
b3Body_SetMotionLocks(bodyId_,
|
|
b3MotionLocks{linearX, linearY, linearZ, angularX, angularY,
|
|
angularZ});
|
|
}
|
|
|
|
#define EV_BODY_LOCK_GETTER(name, member) \
|
|
bool Body3D::name() const { \
|
|
return isValid() ? b3Body_GetMotionLocks(bodyId_).member : false; \
|
|
}
|
|
EV_BODY_LOCK_GETTER(isLinearXLocked, linearX)
|
|
EV_BODY_LOCK_GETTER(isLinearYLocked, linearY)
|
|
EV_BODY_LOCK_GETTER(isLinearZLocked, linearZ)
|
|
EV_BODY_LOCK_GETTER(isAngularXLocked, angularX)
|
|
EV_BODY_LOCK_GETTER(isAngularYLocked, angularY)
|
|
EV_BODY_LOCK_GETTER(isAngularZLocked, angularZ)
|
|
#undef EV_BODY_LOCK_GETTER
|
|
|
|
void Body3D::setMassProperties(float mass, float centerX, float centerY, float centerZ,
|
|
float inertiaXX, float inertiaYY, float inertiaZZ,
|
|
float inertiaXY, float inertiaXZ, float inertiaYZ) {
|
|
if (!isValid()) return;
|
|
constexpr const char *operation = "Body3D.setMassProperties";
|
|
if (b3Body_GetType(bodyId_) != b3_dynamicBody)
|
|
throw eve::Exception("%s: body must be dynamic", operation);
|
|
requireFinite(mass, operation, "mass");
|
|
requireFinite(centerX, operation, "centerX");
|
|
requireFinite(centerY, operation, "centerY");
|
|
requireFinite(centerZ, operation, "centerZ");
|
|
requireFinite(inertiaXX, operation, "inertiaXX");
|
|
requireFinite(inertiaYY, operation, "inertiaYY");
|
|
requireFinite(inertiaZZ, operation, "inertiaZZ");
|
|
requireFinite(inertiaXY, operation, "inertiaXY");
|
|
requireFinite(inertiaXZ, operation, "inertiaXZ");
|
|
requireFinite(inertiaYZ, operation, "inertiaYZ");
|
|
if (!(mass > 0.f)) throw eve::Exception("%s: mass must be > 0", operation);
|
|
|
|
// Sylvester's criterion for a symmetric positive-definite 3x3 tensor.
|
|
const double xx = inertiaXX, yy = inertiaYY, zz = inertiaZZ;
|
|
const double xy = inertiaXY, xz = inertiaXZ, yz = inertiaYZ;
|
|
const double minor2 = xx * yy - xy * xy;
|
|
const double determinant =
|
|
xx * (yy * zz - yz * yz) - xy * (xy * zz - yz * xz) +
|
|
xz * (xy * yz - yy * xz);
|
|
if (!(xx > 0.0 && minor2 > 0.0 && determinant > 0.0) ||
|
|
!std::isfinite(minor2) || !std::isfinite(determinant))
|
|
throw eve::Exception("%s: inertia tensor must be positive definite", operation);
|
|
|
|
b3MassData data{};
|
|
data.mass = mass;
|
|
data.center = {centerX, centerY, centerZ};
|
|
data.inertia = {{inertiaXX, inertiaXY, inertiaXZ},
|
|
{inertiaXY, inertiaYY, inertiaYZ},
|
|
{inertiaXZ, inertiaYZ, inertiaZZ}};
|
|
b3Body_SetMassData(bodyId_, data);
|
|
}
|
|
|
|
void Body3D::resetMassProperties() {
|
|
if (!isValid()) return;
|
|
b3Body_ApplyMassFromShapes(bodyId_);
|
|
}
|
|
|
|
#define EV_BODY_INERTIA_GETTER(name, column, member) \
|
|
float Body3D::name() const { \
|
|
return isValid() ? b3Body_GetMassData(bodyId_).inertia.column.member : 0.f; \
|
|
}
|
|
EV_BODY_INERTIA_GETTER(getInertiaXX, cx, x)
|
|
EV_BODY_INERTIA_GETTER(getInertiaYY, cy, y)
|
|
EV_BODY_INERTIA_GETTER(getInertiaZZ, cz, z)
|
|
EV_BODY_INERTIA_GETTER(getInertiaXY, cx, y)
|
|
EV_BODY_INERTIA_GETTER(getInertiaXZ, cx, z)
|
|
EV_BODY_INERTIA_GETTER(getInertiaYZ, cy, z)
|
|
#undef EV_BODY_INERTIA_GETTER
|
|
|
|
#define EV_BODY_CENTER_GETTER(name, functionName, member) \
|
|
float Body3D::name() const { \
|
|
return isValid() ? static_cast<float>(functionName(bodyId_).member) : 0.f; \
|
|
}
|
|
EV_BODY_CENTER_GETTER(getLocalCenterX, b3Body_GetLocalCenterOfMass, x)
|
|
EV_BODY_CENTER_GETTER(getLocalCenterY, b3Body_GetLocalCenterOfMass, y)
|
|
EV_BODY_CENTER_GETTER(getLocalCenterZ, b3Body_GetLocalCenterOfMass, z)
|
|
EV_BODY_CENTER_GETTER(getWorldCenterX, b3Body_GetWorldCenterOfMass, x)
|
|
EV_BODY_CENTER_GETTER(getWorldCenterY, b3Body_GetWorldCenterOfMass, y)
|
|
EV_BODY_CENTER_GETTER(getWorldCenterZ, b3Body_GetWorldCenterOfMass, z)
|
|
#undef EV_BODY_CENTER_GETTER
|
|
|
|
void Body3D::setType(const std::string &bodyType) {
|
|
if (!isValid()) return;
|
|
const b3BodyType type = parseBodyType(bodyType);
|
|
if (type != b3_staticBody && world_) {
|
|
for (Shape3D *shape : world_->shapes_) {
|
|
if (shape && shape->getBody() == this &&
|
|
(shape->kind_ == Shape3D::Kind::TriangleMesh ||
|
|
shape->kind_ == Shape3D::Kind::HeightField))
|
|
throw eve::Exception(
|
|
"Body3D.setType: triangle-mesh and height-field colliders require a static body");
|
|
}
|
|
}
|
|
b3Body_SetType(bodyId_, type);
|
|
}
|
|
|
|
std::string Body3D::getType() const {
|
|
if (!isValid()) return "static";
|
|
return bodyTypeName(b3Body_GetType(bodyId_));
|
|
}
|
|
|
|
void Body3D::setFixedRotation(bool fixed) {
|
|
if (!isValid()) return;
|
|
b3MotionLocks locks = b3Body_GetMotionLocks(bodyId_);
|
|
locks.angularX = fixed;
|
|
locks.angularY = fixed;
|
|
locks.angularZ = fixed;
|
|
b3Body_SetMotionLocks(bodyId_, locks);
|
|
}
|
|
|
|
bool Body3D::isFixedRotation() const {
|
|
if (!isValid()) return false;
|
|
b3MotionLocks locks = b3Body_GetMotionLocks(bodyId_);
|
|
return locks.angularX && locks.angularY && locks.angularZ;
|
|
}
|
|
|
|
void Body3D::setActive(bool active) {
|
|
if (!isValid()) return;
|
|
if (active)
|
|
b3Body_Enable(bodyId_);
|
|
else
|
|
b3Body_Disable(bodyId_);
|
|
}
|
|
|
|
bool Body3D::isActive() const { return isValid() ? b3Body_IsEnabled(bodyId_) : false; }
|
|
|
|
void Body3D::setBullet(bool bullet) {
|
|
if (!isValid()) return;
|
|
b3Body_SetBullet(bodyId_, bullet);
|
|
}
|
|
|
|
bool Body3D::isBullet() const { return isValid() ? b3Body_IsBullet(bodyId_) : false; }
|
|
|
|
void Body3D::setAwake(bool awake) {
|
|
if (!isValid()) return;
|
|
b3Body_SetAwake(bodyId_, awake);
|
|
}
|
|
|
|
bool Body3D::isAwake() const { return isValid() ? b3Body_IsAwake(bodyId_) : false; }
|
|
|
|
Shape3D *Body3D::newBoxShape(float width, float height, float depth, float density,
|
|
float friction, float restitution) {
|
|
if (!isValid() || !world_) throw eve::Exception("Body3D.newBoxShape: body destroyed");
|
|
if (width <= 0.f || height <= 0.f || depth <= 0.f)
|
|
throw eve::Exception("Body3D.newBoxShape: width/height/depth must be > 0");
|
|
|
|
float hx = width * 0.5f;
|
|
float hy = height * 0.5f;
|
|
float hz = depth * 0.5f;
|
|
|
|
b3BoxHull box = b3MakeBoxHull(hx, hy, hz);
|
|
b3ShapeDef def = makeShapeDef(density, friction, restitution);
|
|
b3ShapeId id = b3CreateHullShape(bodyId_, &def, &box.base);
|
|
|
|
auto *shape = new Shape3D(world_, this, id, world_->nextShapeRuntimeHandle(), Shape3D::Kind::Box, hx, hy, hz);
|
|
b3Shape_SetUserData(id, shape);
|
|
world_->shapes_.insert(shape);
|
|
return shape;
|
|
}
|
|
|
|
Shape3D *Body3D::newSphereShape(float radius, float density, float friction, float restitution) {
|
|
if (!isValid() || !world_) throw eve::Exception("Body3D.newSphereShape: body destroyed");
|
|
if (radius <= 0.f) throw eve::Exception("Body3D.newSphereShape: radius must be > 0");
|
|
|
|
b3Sphere sphere;
|
|
sphere.center = b3Vec3_zero;
|
|
sphere.radius = radius;
|
|
|
|
b3ShapeDef def = makeShapeDef(density, friction, restitution);
|
|
b3ShapeId id = b3CreateSphereShape(bodyId_, &def, &sphere);
|
|
|
|
auto *shape =
|
|
new Shape3D(world_, this, id, world_->nextShapeRuntimeHandle(), Shape3D::Kind::Sphere, radius, 0.f, 0.f);
|
|
b3Shape_SetUserData(id, shape);
|
|
world_->shapes_.insert(shape);
|
|
return shape;
|
|
}
|
|
|
|
Shape3D *Body3D::newCapsuleShape(float height, float radius, float density, float friction,
|
|
float restitution) {
|
|
if (!isValid() || !world_) throw eve::Exception("Body3D.newCapsuleShape: body destroyed");
|
|
if (height < 0.f || radius <= 0.f)
|
|
throw eve::Exception("Body3D.newCapsuleShape: height >= 0 and radius > 0 required");
|
|
|
|
float half = height * 0.5f;
|
|
b3Capsule capsule;
|
|
capsule.center1 = b3Vec3{0.f, -half, 0.f};
|
|
capsule.center2 = b3Vec3{0.f, half, 0.f};
|
|
capsule.radius = radius;
|
|
|
|
b3ShapeDef def = makeShapeDef(density, friction, restitution);
|
|
b3ShapeId id = b3CreateCapsuleShape(bodyId_, &def, &capsule);
|
|
|
|
auto *shape =
|
|
new Shape3D(world_, this, id, world_->nextShapeRuntimeHandle(), Shape3D::Kind::Capsule, half, radius, 0.f);
|
|
b3Shape_SetUserData(id, shape);
|
|
world_->shapes_.insert(shape);
|
|
return shape;
|
|
}
|
|
|
|
Shape3D *Body3D::newConvexHullShape(const std::vector<float> &vertices, int maxVertices,
|
|
float density, float friction, float restitution) {
|
|
if (!isValid() || !world_)
|
|
throw eve::Exception("Body3D.newConvexHullShape: body destroyed");
|
|
b3HullData *hull = createCheckedHull(vertices, maxVertices, "Body3D.newConvexHullShape");
|
|
b3ShapeDef def = makeShapeDef(density, friction, restitution);
|
|
b3ShapeId id = b3CreateHullShape(bodyId_, &def, hull);
|
|
b3DestroyHull(hull);
|
|
|
|
auto *shape = new Shape3D(world_, this, id, world_->nextShapeRuntimeHandle(), Shape3D::Kind::ConvexHull, 0.f, 0.f,
|
|
0.f, vertices, maxVertices);
|
|
b3Shape_SetUserData(id, shape);
|
|
world_->shapes_.insert(shape);
|
|
return shape;
|
|
}
|
|
|
|
Shape3D *Body3D::newTriangleMeshShape(const std::vector<float> &vertices,
|
|
const std::vector<int32_t> &indices, bool weldVertices,
|
|
float weldTolerance, bool identifyEdges,
|
|
bool useMedianSplit) {
|
|
if (!isValid() || !world_)
|
|
throw eve::Exception("Body3D.newTriangleMeshShape: body destroyed");
|
|
if (b3Body_GetType(bodyId_) != b3_staticBody)
|
|
throw eve::Exception("Body3D.newTriangleMeshShape: triangle meshes require a static body");
|
|
b3MeshData *mesh = createCheckedMesh(vertices, indices, weldVertices, weldTolerance,
|
|
identifyEdges, useMedianSplit,
|
|
"Body3D.newTriangleMeshShape");
|
|
b3ShapeDef def = makeShapeDef(0.f, 0.2f, 0.f);
|
|
b3ShapeId id = b3CreateMeshShape(bodyId_, &def, mesh, b3Vec3_one);
|
|
if (B3_IS_NULL(id)) {
|
|
b3DestroyMesh(mesh);
|
|
throw eve::Exception("Body3D.newTriangleMeshShape: Box3D rejected the mesh shape");
|
|
}
|
|
auto *shape =
|
|
new Shape3D(world_, this, id, world_->nextShapeRuntimeHandle(), Shape3D::Kind::TriangleMesh, 0.f, 0.f, 0.f, {},
|
|
64, vertices, indices, mesh, weldVertices, weldTolerance, identifyEdges, useMedianSplit);
|
|
b3Shape_SetUserData(id, shape);
|
|
world_->shapes_.insert(shape);
|
|
return shape;
|
|
}
|
|
|
|
Shape3D *Body3D::newHeightFieldShape(int countX, int countZ, float cellSizeX,
|
|
float cellSizeZ, const std::vector<float> &heights,
|
|
float globalMin, float globalMax,
|
|
bool clockwiseWinding) {
|
|
if (!isValid() || !world_)
|
|
throw eve::Exception("Body3D.newHeightFieldShape: body destroyed");
|
|
if (b3Body_GetType(bodyId_) != b3_staticBody)
|
|
throw eve::Exception("Body3D.newHeightFieldShape: height fields require a static body");
|
|
b3HeightFieldData *heightData = createCheckedHeightField(
|
|
countX, countZ, cellSizeX, cellSizeZ, heights, globalMin, globalMax,
|
|
clockwiseWinding, "Body3D.newHeightFieldShape");
|
|
b3ShapeDef def = makeShapeDef(0.f, 0.2f, 0.f);
|
|
b3ShapeId id = b3CreateHeightFieldShape(bodyId_, &def, heightData);
|
|
if (B3_IS_NULL(id)) {
|
|
b3DestroyHeightField(heightData);
|
|
throw eve::Exception("Body3D.newHeightFieldShape: Box3D rejected the height field");
|
|
}
|
|
auto *shape = new Shape3D(world_, this, id, world_->nextShapeRuntimeHandle(), Shape3D::Kind::HeightField, 0.f, 0.f,
|
|
0.f, {}, 64, {}, {}, nullptr, true, 0.001f, true, false, heights, countX, countZ,
|
|
cellSizeX, cellSizeZ, globalMin, globalMax, clockwiseWinding, heightData);
|
|
b3Shape_SetUserData(id, shape);
|
|
world_->shapes_.insert(shape);
|
|
return shape;
|
|
}
|
|
|
|
} // namespace eve::physics
|