Files
EVEngine/src/modules/physics/Shape3D.cpp
T

975 lines
41 KiB
C++

#include "physics/Shape3D.h"
#include "physics/Body3D.h"
#include "physics/World3D.h"
#include "common/Exception.h"
#include <box3d/box3d.h>
#include <cstdint>
#include <cmath>
#include <utility>
namespace eve::physics {
namespace {
constexpr uint64_t materialIdMask = 0xFFFFFFFFull;
constexpr int frictionModeShift = 32;
constexpr int restitutionModeShift = 35;
constexpr uint64_t combineModeMask = 0x7ull;
uint64_t parseCombineMode(const std::string &mode, const char *operation) {
if (mode == "default") return 0;
if (mode == "average") return 1;
if (mode == "minimum" || mode == "min") return 2;
if (mode == "multiply") return 3;
if (mode == "maximum" || mode == "max") return 4;
throw eve::Exception("%s: mode must be default|average|minimum|multiply|maximum",
operation);
}
std::string combineModeName(uint64_t mode) {
switch (mode) {
case 0: return "default";
case 1: return "average";
case 2: return "minimum";
case 3: return "multiply";
case 4: return "maximum";
default: return "default";
}
}
b3ShapeDef makeShapeDef(float density, float friction, float restitution, bool sensor,
bool hitEvents, bool preSolveEvents) {
b3ShapeDef def = b3DefaultShapeDef();
def.density = density;
def.baseMaterial.friction = friction;
def.baseMaterial.restitution = restitution;
def.isSensor = sensor;
def.enableContactEvents = !sensor;
def.enableSensorEvents = true;
def.enableHitEvents = hitEvents && !sensor;
def.enablePreSolveEvents = preSolveEvents && !sensor;
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,
b3Transform transform,
const std::vector<uint8_t> *materialIndices,
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);
const b3Vec3 point{vertices[i], vertices[i + 1], vertices[i + 2]};
points.push_back(transform.p + b3RotateVector(transform.q, point));
}
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.materialIndices = materialIndices && !materialIndices->empty()
? const_cast<uint8_t *>(materialIndices->data())
: nullptr;
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
Shape3D::Shape3D(World3D *world, Body3D *body, b3ShapeId shapeId, PhysicsShapeHandle runtimeHandle, Kind kind, float a,
float b, float c, std::vector<float> hullVertices, int hullMaxVertices,
std::vector<float> meshVertices, std::vector<int32_t> meshIndices, b3MeshData *meshData,
bool meshWeldVertices, float meshWeldTolerance, bool meshIdentifyEdges, bool meshUseMedianSplit,
std::vector<float> heightValues, int heightCountX, int heightCountZ, float heightCellSizeX,
float heightCellSizeZ, float heightGlobalMin, float heightGlobalMax, bool heightClockwise,
b3HeightFieldData *heightData)
: world_(world),
body_(body),
shapeId_(shapeId),
runtimeHandle_(runtimeHandle),
kind_(kind),
a_(a),
b_(b),
c_(c),
hullVertices_(std::move(hullVertices)),
hullMaxVertices_(hullMaxVertices),
meshVertices_(std::move(meshVertices)),
meshIndices_(std::move(meshIndices)),
meshData_(meshData),
meshWeldVertices_(meshWeldVertices),
meshWeldTolerance_(meshWeldTolerance),
meshIdentifyEdges_(meshIdentifyEdges),
meshUseMedianSplit_(meshUseMedianSplit),
heightValues_(std::move(heightValues)),
heightCountX_(heightCountX),
heightCountZ_(heightCountZ),
heightCellSizeX_(heightCellSizeX),
heightCellSizeZ_(heightCellSizeZ),
heightGlobalMin_(heightGlobalMin),
heightGlobalMax_(heightGlobalMax),
heightClockwise_(heightClockwise),
heightData_(heightData),
id_(world ? world->nextShapeId() : 0) {
if (kind_ == Kind::TriangleMesh && isValid() && !meshMaterialsDirty_) {
const int materialCount = b3Shape_GetMeshMaterialCount(shapeId_);
meshMaterials_.reserve(static_cast<size_t>(materialCount));
for (int i = 0; i < materialCount; ++i)
meshMaterials_.push_back(b3Shape_GetMeshSurfaceMaterial(shapeId_, i));
}
if (world_) world_->registerShapeHandle(this);
}
Shape3D::~Shape3D() {
if (isValid() && body_ && body_->isValid()) {
b3Shape_SetUserData(shapeId_, nullptr);
b3DestroyShape(shapeId_, true);
}
if (world_) world_->forgetShape(this);
if (meshData_) {
b3DestroyMesh(meshData_);
meshData_ = nullptr;
}
if (heightData_) {
b3DestroyHeightField(heightData_);
heightData_ = nullptr;
}
shapeId_ = {};
body_ = nullptr;
world_ = nullptr;
runtimeHandle_ = PhysicsShapeHandle::invalid();
}
bool Shape3D::isValid() const { return b3Shape_IsValid(shapeId_); }
void Shape3D::invalidate() {
if (isValid()) b3Shape_SetUserData(shapeId_, nullptr);
if (world_) world_->forgetShape(this);
shapeId_ = {};
body_ = nullptr;
world_ = nullptr;
runtimeHandle_ = PhysicsShapeHandle::invalid();
}
void Shape3D::destroy() {
if (!isValid() || !body_ || !body_->isValid()) {
invalidate();
if (meshData_) {
b3DestroyMesh(meshData_);
meshData_ = nullptr;
}
if (heightData_) {
b3DestroyHeightField(heightData_);
heightData_ = nullptr;
}
return;
}
b3Shape_SetUserData(shapeId_, nullptr);
b3DestroyShape(shapeId_, true);
if (meshData_) {
b3DestroyMesh(meshData_);
meshData_ = nullptr;
}
if (heightData_) {
b3DestroyHeightField(heightData_);
heightData_ = nullptr;
}
if (world_) world_->forgetShape(this);
shapeId_ = {};
body_ = nullptr;
world_ = nullptr;
runtimeHandle_ = PhysicsShapeHandle::invalid();
}
void Shape3D::recreate(bool sensor) {
if (!body_ || !body_->isValid())
throw eve::Exception("Shape3D: cannot rebuild geometry after body destruction");
float density = isValid() ? b3Shape_GetDensity(shapeId_) : 1.f;
b3SurfaceMaterial material =
isValid() ? b3Shape_GetSurfaceMaterial(shapeId_) : b3DefaultSurfaceMaterial();
if (kind_ == Kind::TriangleMesh && isValid() && !meshMaterialsDirty_) {
const int materialCount = b3Shape_GetMeshMaterialCount(shapeId_);
meshMaterials_.clear();
meshMaterials_.reserve(static_cast<size_t>(materialCount));
for (int i = 0; i < materialCount; ++i)
meshMaterials_.push_back(b3Shape_GetMeshSurfaceMaterial(shapeId_, i));
}
b3Filter filter = isValid() ? b3Shape_GetFilter(shapeId_) : b3DefaultShapeDef().filter;
if (isValid()) {
b3Shape_SetUserData(shapeId_, nullptr);
b3DestroyShape(shapeId_, false);
shapeId_ = {};
}
if (meshData_) {
b3DestroyMesh(meshData_);
meshData_ = nullptr;
}
if (heightData_) {
b3DestroyHeightField(heightData_);
heightData_ = nullptr;
}
b3ShapeDef def = makeShapeDef(density, material.friction, material.restitution, sensor,
hitEventsEnabled_, oneWayEnabled_);
def.enableCustomFiltering = contactOverrideFiltering_;
def.explosionScale = explosionScale_;
def.baseMaterial = material;
def.filter = filter;
const b3Quat localRotation{{localQx_, localQy_, localQz_}, localQw_};
const b3Vec3 localPosition{localX_, localY_, localZ_};
switch (kind_) {
case Kind::Box: {
b3BoxHull box = b3MakeBoxHull(a_, b_, c_);
b3Transform transform{localPosition, localRotation};
shapeId_ = b3CreateTransformedHullShape(body_->raw(), &def, &box.base, transform,
b3Vec3{1.f, 1.f, 1.f});
break;
}
case Kind::Sphere: {
b3Sphere sphere;
sphere.center = localPosition;
sphere.radius = a_;
shapeId_ = b3CreateSphereShape(body_->raw(), &def, &sphere);
break;
}
case Kind::Capsule: {
b3Capsule capsule;
const b3Vec3 axis = b3RotateVector(localRotation, b3Vec3{0.f, a_, 0.f});
capsule.center1 = localPosition - axis;
capsule.center2 = localPosition + axis;
capsule.radius = b_;
shapeId_ = b3CreateCapsuleShape(body_->raw(), &def, &capsule);
break;
}
case Kind::ConvexHull: {
b3HullData *hull =
createCheckedHull(hullVertices_, hullMaxVertices_, "Shape3D.recreate");
b3Transform transform{localPosition, localRotation};
shapeId_ = b3CreateTransformedHullShape(body_->raw(), &def, hull, transform,
b3Vec3{1.f, 1.f, 1.f});
b3DestroyHull(hull);
break;
}
case Kind::TriangleMesh: {
if (b3Body_GetType(body_->raw()) != b3_staticBody)
throw eve::Exception("Shape3D.recreate: triangle meshes require a static body");
b3Transform transform{localPosition, localRotation};
meshData_ = createCheckedMesh(meshVertices_, meshIndices_, meshWeldVertices_,
meshWeldTolerance_, meshIdentifyEdges_,
meshUseMedianSplit_, transform,
&meshMaterialIndices_, "Shape3D.recreate");
if (!meshMaterials_.empty()) {
def.materials = meshMaterials_.data();
def.materialCount = static_cast<int>(meshMaterials_.size());
}
shapeId_ = b3CreateMeshShape(body_->raw(), &def, meshData_, b3Vec3_one);
break;
}
case Kind::HeightField: {
if (b3Body_GetType(body_->raw()) != b3_staticBody)
throw eve::Exception("Shape3D.recreate: height fields require a static body");
heightData_ = createCheckedHeightField(
heightCountX_, heightCountZ_, heightCellSizeX_, heightCellSizeZ_, heightValues_,
heightGlobalMin_, heightGlobalMax_, heightClockwise_, "Shape3D.recreate");
shapeId_ = b3CreateHeightFieldShape(body_->raw(), &def, heightData_);
break;
}
}
b3Shape_SetUserData(shapeId_, this);
meshMaterialsDirty_ = false;
if (world_) world_->registerShapeHandle(this);
}
void Shape3D::setSensor(bool sensor) {
if (!body_ || !body_->isValid()) return;
if (isValid() && b3Shape_IsSensor(shapeId_) == sensor) return;
recreate(sensor);
}
void Shape3D::enableContactOverrideFiltering() {
if (contactOverrideFiltering_) return;
contactOverrideFiltering_ = true;
recreate(isSensor());
}
void Shape3D::setTag(int tag) {
tag_ = tag;
if (world_) world_->updateShapeTag(this);
}
void Shape3D::setHitEventsEnabled(bool enabled) {
hitEventsEnabled_ = enabled;
if (!isValid()) return;
b3Shape_EnableHitEvents(shapeId_, enabled && !isSensor());
}
bool Shape3D::areHitEventsEnabled() const {
return hitEventsEnabled_;
}
void Shape3D::setOneWay(float nx, float ny, float nz, float planeOffset, float margin,
float minNormalDot) {
if (!std::isfinite(nx) || !std::isfinite(ny) || !std::isfinite(nz) ||
!std::isfinite(planeOffset) || !std::isfinite(margin) ||
!std::isfinite(minNormalDot))
throw eve::Exception("Shape3D.setOneWay: all values must be finite");
const float lengthSquared = nx * nx + ny * ny + nz * nz;
if (!(lengthSquared > 1e-12f))
throw eve::Exception("Shape3D.setOneWay: normal must be non-zero");
if (margin < 0.f)
throw eve::Exception("Shape3D.setOneWay: margin must be >= 0");
if (minNormalDot < -1.f || minNormalDot > 1.f)
throw eve::Exception("Shape3D.setOneWay: minNormalDot must be in [-1,1]");
const float inverseLength = 1.f / std::sqrt(lengthSquared);
oneWayLocalNormalX_ = nx * inverseLength;
oneWayLocalNormalY_ = ny * inverseLength;
oneWayLocalNormalZ_ = nz * inverseLength;
oneWayPlaneOffset_ = planeOffset;
oneWayMargin_ = margin;
oneWayMinNormalDot_ = minNormalDot;
oneWayEnabled_ = true;
if (isValid()) b3Shape_EnablePreSolveEvents(shapeId_, !isSensor());
}
void Shape3D::disableOneWay() {
oneWayEnabled_ = false;
if (isValid()) b3Shape_EnablePreSolveEvents(shapeId_, false);
}
void Shape3D::refreshOneWayWorldData() {
if (!oneWayEnabled_ || !body_ || !body_->isValid()) return;
const b3WorldTransform bodyTransform = b3Body_GetTransform(body_->raw());
const b3Quat localRotation{{localQx_, localQy_, localQz_}, localQw_};
const b3Quat worldRotation = b3MulQuat(bodyTransform.q, localRotation);
const b3Vec3 localNormal{oneWayLocalNormalX_, oneWayLocalNormalY_, oneWayLocalNormalZ_};
const b3Vec3 worldNormal = b3RotateVector(worldRotation, localNormal);
const b3Vec3 localPosition{localX_, localY_, localZ_};
const b3Vec3 bodyOffset = b3RotateVector(bodyTransform.q, localPosition);
const b3Pos shapeOrigin = b3OffsetPos(bodyTransform.p, bodyOffset);
const b3Pos planePoint = b3OffsetPos(shapeOrigin, oneWayPlaneOffset_ * worldNormal);
oneWayWorldPointX_ = planePoint.x;
oneWayWorldPointY_ = planePoint.y;
oneWayWorldPointZ_ = planePoint.z;
oneWayWorldNormalX_ = worldNormal.x;
oneWayWorldNormalY_ = worldNormal.y;
oneWayWorldNormalZ_ = worldNormal.z;
}
bool Shape3D::allowsOneWayContact(double pointX, double pointY, double pointZ, float normalX,
float normalY, float normalZ) const {
if (!oneWayEnabled_) return true;
const double side = (pointX - oneWayWorldPointX_) * oneWayWorldNormalX_ +
(pointY - oneWayWorldPointY_) * oneWayWorldNormalY_ +
(pointZ - oneWayWorldPointZ_) * oneWayWorldNormalZ_;
const float alignment = normalX * oneWayWorldNormalX_ + normalY * oneWayWorldNormalY_ +
normalZ * oneWayWorldNormalZ_;
return side >= -static_cast<double>(oneWayMargin_) && alignment >= oneWayMinNormalDot_;
}
std::string Shape3D::getKind() const {
switch (kind_) {
case Kind::Box: return "box";
case Kind::Sphere: return "sphere";
case Kind::Capsule: return "capsule";
case Kind::ConvexHull: return "convexHull";
case Kind::TriangleMesh: return "triangleMesh";
case Kind::HeightField: return "heightField";
}
return "box";
}
void Shape3D::setBoxSize(float width, float height, float depth) {
if (kind_ != Kind::Box)
throw eve::Exception("Shape3D.setBoxSize: shape is not a box");
if (!(width > 0.f) || !(height > 0.f) || !(depth > 0.f) || !std::isfinite(width) ||
!std::isfinite(height) || !std::isfinite(depth))
throw eve::Exception("Shape3D.setBoxSize: dimensions must be finite and > 0");
if (!body_ || !body_->isValid())
throw eve::Exception("Shape3D.setBoxSize: body destroyed");
const float halfWidth = 0.5f * width;
const float halfHeight = 0.5f * height;
const float halfDepth = 0.5f * depth;
if (a_ == halfWidth && b_ == halfHeight && c_ == halfDepth) return;
const bool sensor = isSensor();
a_ = halfWidth;
b_ = halfHeight;
c_ = halfDepth;
recreate(sensor);
}
void Shape3D::setSphereRadius(float radius) {
if (kind_ != Kind::Sphere)
throw eve::Exception("Shape3D.setSphereRadius: shape is not a sphere");
if (!(radius > 0.f) || !std::isfinite(radius))
throw eve::Exception("Shape3D.setSphereRadius: radius must be finite and > 0");
if (!body_ || !body_->isValid())
throw eve::Exception("Shape3D.setSphereRadius: body destroyed");
if (a_ == radius) return;
const bool sensor = isSensor();
a_ = radius;
recreate(sensor);
}
void Shape3D::setCapsuleSize(float height, float radius) {
if (kind_ != Kind::Capsule)
throw eve::Exception("Shape3D.setCapsuleSize: shape is not a capsule");
if (!(height >= 0.f) || !(radius > 0.f) || !std::isfinite(height) ||
!std::isfinite(radius))
throw eve::Exception(
"Shape3D.setCapsuleSize: height must be finite and >= 0; radius finite and > 0");
if (!body_ || !body_->isValid())
throw eve::Exception("Shape3D.setCapsuleSize: body destroyed");
const float halfHeight = 0.5f * height;
if (a_ == halfHeight && b_ == radius) return;
const bool sensor = isSensor();
a_ = halfHeight;
b_ = radius;
recreate(sensor);
}
void Shape3D::setConvexHullVertices(const std::vector<float> &vertices, int maxVertices) {
if (kind_ != Kind::ConvexHull)
throw eve::Exception("Shape3D.setConvexHullVertices: shape is not a convex hull");
if (!body_ || !body_->isValid())
throw eve::Exception("Shape3D.setConvexHullVertices: body destroyed");
b3HullData *validated =
createCheckedHull(vertices, maxVertices, "Shape3D.setConvexHullVertices");
b3DestroyHull(validated);
if (vertices == hullVertices_ && maxVertices == hullMaxVertices_) return;
const bool sensor = isSensor();
hullVertices_ = vertices;
hullMaxVertices_ = maxVertices;
recreate(sensor);
}
void Shape3D::setTriangleMeshData(const std::vector<float> &vertices,
const std::vector<int32_t> &indices, bool weldVertices,
float weldTolerance, bool identifyEdges,
bool useMedianSplit) {
if (kind_ != Kind::TriangleMesh)
throw eve::Exception("Shape3D.setTriangleMeshData: shape is not a triangle mesh");
if (!body_ || !body_->isValid())
throw eve::Exception("Shape3D.setTriangleMeshData: body destroyed");
if (b3Body_GetType(body_->raw()) != b3_staticBody)
throw eve::Exception("Shape3D.setTriangleMeshData: triangle meshes require a static body");
const b3Transform transform{{localX_, localY_, localZ_},
b3Quat{{localQx_, localQy_, localQz_}, localQw_}};
b3MeshData *validated = createCheckedMesh(vertices, indices, weldVertices, weldTolerance,
identifyEdges, useMedianSplit, transform,
nullptr,
"Shape3D.setTriangleMeshData");
b3DestroyMesh(validated);
if (vertices == meshVertices_ && indices == meshIndices_ &&
weldVertices == meshWeldVertices_ && weldTolerance == meshWeldTolerance_ &&
identifyEdges == meshIdentifyEdges_ && useMedianSplit == meshUseMedianSplit_)
return;
const bool sensor = isSensor();
meshVertices_ = vertices;
meshIndices_ = indices;
meshWeldVertices_ = weldVertices;
meshWeldTolerance_ = weldTolerance;
meshIdentifyEdges_ = identifyEdges;
meshUseMedianSplit_ = useMedianSplit;
if (meshMaterialIndices_.size() != indices.size() / 3) {
meshMaterialIndices_.clear();
if (meshMaterials_.size() > 1) meshMaterials_.resize(1);
}
recreate(sensor);
}
void Shape3D::setTriangleMeshMaterialIndices(
const std::vector<int32_t> &materialIndices) {
if (kind_ != Kind::TriangleMesh)
throw eve::Exception(
"Shape3D.setTriangleMeshMaterialIndices: shape is not a triangle mesh");
const size_t triangleCount = meshIndices_.size() / 3;
if (materialIndices.size() != triangleCount)
throw eve::Exception(
"Shape3D.setTriangleMeshMaterialIndices: one index is required per triangle");
std::vector<uint8_t> encoded;
encoded.reserve(materialIndices.size());
int requiredCount = 1;
for (int32_t index : materialIndices) {
if (index < 0 || index > 254)
throw eve::Exception(
"Shape3D.setTriangleMeshMaterialIndices: indices must be in [0, 254]");
encoded.push_back(static_cast<uint8_t>(index));
requiredCount = std::max(requiredCount, static_cast<int>(index) + 1);
}
if (encoded == meshMaterialIndices_ &&
requiredCount == static_cast<int>(meshMaterials_.size()))
return;
const bool sensor = isSensor();
const auto oldIndices = meshMaterialIndices_;
const auto oldMaterials = meshMaterials_;
meshMaterialIndices_ = std::move(encoded);
if (meshMaterials_.empty()) meshMaterials_.push_back(b3DefaultSurfaceMaterial());
meshMaterials_.resize(static_cast<size_t>(requiredCount), meshMaterials_.front());
meshMaterialsDirty_ = true;
try {
recreate(sensor);
} catch (...) {
meshMaterialIndices_ = oldIndices;
meshMaterials_ = oldMaterials;
meshMaterialsDirty_ = true;
recreate(sensor);
throw;
}
}
int Shape3D::getTriangleMeshMaterialIndex(int triangleIndex) const {
if (kind_ != Kind::TriangleMesh)
throw eve::Exception(
"Shape3D.getTriangleMeshMaterialIndex: shape is not a triangle mesh");
const int triangleCount = static_cast<int>(meshIndices_.size() / 3);
if (triangleIndex < 0 || triangleIndex >= triangleCount)
throw eve::Exception("Shape3D.getTriangleMeshMaterialIndex: triangle out of range");
return meshMaterialIndices_.empty() ? 0 : meshMaterialIndices_[static_cast<size_t>(triangleIndex)];
}
int Shape3D::getTriangleMeshMaterialCount() const {
return kind_ == Kind::TriangleMesh ? std::max(1, static_cast<int>(meshMaterials_.size())) : 0;
}
void Shape3D::setTriangleMeshMaterial(int slot, float friction, float restitution,
float rollingResistance, float tangentX,
float tangentY, float tangentZ, int materialId,
const std::string &frictionMode,
const std::string &restitutionMode) {
if (kind_ != Kind::TriangleMesh)
throw eve::Exception("Shape3D.setTriangleMeshMaterial: shape is not a triangle mesh");
if (slot < 0 || slot >= getTriangleMeshMaterialCount())
throw eve::Exception("Shape3D.setTriangleMeshMaterial: slot out of range");
if (!std::isfinite(friction) || friction < 0.f || !std::isfinite(restitution) ||
restitution < 0.f || !std::isfinite(rollingResistance) || rollingResistance < 0.f ||
!std::isfinite(tangentX) || !std::isfinite(tangentY) || !std::isfinite(tangentZ))
throw eve::Exception(
"Shape3D.setTriangleMeshMaterial: numeric values must be finite and non-negative where required");
const uint64_t frictionCode =
parseCombineMode(frictionMode, "Shape3D.setTriangleMeshMaterial");
const uint64_t restitutionCode =
parseCombineMode(restitutionMode, "Shape3D.setTriangleMeshMaterial");
b3SurfaceMaterial material = b3DefaultSurfaceMaterial();
material.friction = friction;
material.restitution = restitution;
material.rollingResistance = rollingResistance;
material.tangentVelocity = {tangentX, tangentY, tangentZ};
material.userMaterialId = static_cast<uint32_t>(materialId) |
(frictionCode << frictionModeShift) |
(restitutionCode << restitutionModeShift);
if (meshMaterials_.empty()) meshMaterials_.push_back(b3DefaultSurfaceMaterial());
meshMaterials_[static_cast<size_t>(slot)] = material;
if (isValid()) b3Shape_SetMeshMaterial(shapeId_, material, slot);
}
float Shape3D::getTriangleMeshMaterialFriction(int slot) const {
if (kind_ != Kind::TriangleMesh || slot < 0 || slot >= getTriangleMeshMaterialCount())
throw eve::Exception("Shape3D.getTriangleMeshMaterialFriction: slot out of range");
return meshMaterials_.empty() ? getFriction() : meshMaterials_[static_cast<size_t>(slot)].friction;
}
float Shape3D::getTriangleMeshMaterialRestitution(int slot) const {
if (kind_ != Kind::TriangleMesh || slot < 0 || slot >= getTriangleMeshMaterialCount())
throw eve::Exception("Shape3D.getTriangleMeshMaterialRestitution: slot out of range");
return meshMaterials_.empty() ? getRestitution() : meshMaterials_[static_cast<size_t>(slot)].restitution;
}
float Shape3D::getTriangleMeshMaterialRollingResistance(int slot) const {
if (kind_ != Kind::TriangleMesh || slot < 0 || slot >= getTriangleMeshMaterialCount())
throw eve::Exception(
"Shape3D.getTriangleMeshMaterialRollingResistance: slot out of range");
return meshMaterials_.empty() ? getRollingResistance()
: meshMaterials_[static_cast<size_t>(slot)].rollingResistance;
}
int Shape3D::getTriangleMeshMaterialId(int slot) const {
if (kind_ != Kind::TriangleMesh || slot < 0 || slot >= getTriangleMeshMaterialCount())
throw eve::Exception("Shape3D.getTriangleMeshMaterialId: slot out of range");
const uint64_t id = meshMaterials_.empty()
? b3Shape_GetSurfaceMaterial(shapeId_).userMaterialId
: meshMaterials_[static_cast<size_t>(slot)].userMaterialId;
return static_cast<int>(static_cast<uint32_t>(id));
}
void Shape3D::setHeightFieldHeights(const std::vector<float> &heights) {
if (kind_ != Kind::HeightField)
throw eve::Exception("Shape3D.setHeightFieldHeights: shape is not a height field");
if (!body_ || !body_->isValid())
throw eve::Exception("Shape3D.setHeightFieldHeights: body destroyed");
b3HeightFieldData *validated = createCheckedHeightField(
heightCountX_, heightCountZ_, heightCellSizeX_, heightCellSizeZ_, heights,
heightGlobalMin_, heightGlobalMax_, heightClockwise_,
"Shape3D.setHeightFieldHeights");
b3DestroyHeightField(validated);
if (heights == heightValues_) return;
const bool sensor = isSensor();
heightValues_ = heights;
recreate(sensor);
}
void Shape3D::setHeightFieldRegion(int x, int z, int width, int depth,
const std::vector<float> &heights) {
if (kind_ != Kind::HeightField)
throw eve::Exception("Shape3D.setHeightFieldRegion: shape is not a height field");
if (x < 0 || z < 0 || width < 1 || depth < 1 || x > heightCountX_ - width ||
z > heightCountZ_ - depth)
throw eve::Exception("Shape3D.setHeightFieldRegion: region is outside the sample grid");
if (heights.size() != static_cast<size_t>(width) * static_cast<size_t>(depth))
throw eve::Exception("Shape3D.setHeightFieldRegion: data size must equal width * depth");
std::vector<float> updated = heightValues_;
for (int row = 0; row < depth; ++row) {
for (int column = 0; column < width; ++column) {
updated[static_cast<size_t>(z + row) * static_cast<size_t>(heightCountX_) +
static_cast<size_t>(x + column)] =
heights[static_cast<size_t>(row) * static_cast<size_t>(width) +
static_cast<size_t>(column)];
}
}
setHeightFieldHeights(updated);
}
float Shape3D::getHeightFieldHeight(int x, int z) const {
if (kind_ != Kind::HeightField)
throw eve::Exception("Shape3D.getHeightFieldHeight: shape is not a height field");
if (x < 0 || x >= heightCountX_ || z < 0 || z >= heightCountZ_)
throw eve::Exception("Shape3D.getHeightFieldHeight: sample index out of range");
return heightValues_[static_cast<size_t>(z) * static_cast<size_t>(heightCountX_) +
static_cast<size_t>(x)];
}
void Shape3D::setLocalTransform(float px, float py, float pz, float qx, float qy, float qz,
float qw) {
if (!body_ || !body_->isValid())
throw eve::Exception("Shape3D.setLocalTransform: body destroyed");
if (!std::isfinite(px) || !std::isfinite(py) || !std::isfinite(pz) ||
!std::isfinite(qx) || !std::isfinite(qy) || !std::isfinite(qz) ||
!std::isfinite(qw))
throw eve::Exception("Shape3D.setLocalTransform: values must be finite");
const float lengthSquared = qx * qx + qy * qy + qz * qz + qw * qw;
if (!(lengthSquared > 1e-12f))
throw eve::Exception("Shape3D.setLocalTransform: quaternion must be non-zero");
const float inverseLength = 1.f / std::sqrt(lengthSquared);
const float normalizedQx = qx * inverseLength;
const float normalizedQy = qy * inverseLength;
const float normalizedQz = qz * inverseLength;
const float normalizedQw = qw * inverseLength;
if (kind_ == Kind::HeightField &&
(px != 0.f || py != 0.f || pz != 0.f || normalizedQx != 0.f ||
normalizedQy != 0.f || normalizedQz != 0.f || normalizedQw != 1.f))
throw eve::Exception(
"Shape3D.setLocalTransform: height fields use their Body transform and require an identity local transform");
if (localX_ == px && localY_ == py && localZ_ == pz && localQx_ == normalizedQx &&
localQy_ == normalizedQy && localQz_ == normalizedQz && localQw_ == normalizedQw)
return;
const bool sensor = isSensor();
localX_ = px;
localY_ = py;
localZ_ = pz;
localQx_ = normalizedQx;
localQy_ = normalizedQy;
localQz_ = normalizedQz;
localQw_ = normalizedQw;
recreate(sensor);
}
void Shape3D::setLocalPosition(float x, float y, float z) {
setLocalTransform(x, y, z, localQx_, localQy_, localQz_, localQw_);
}
void Shape3D::setLocalRotation(float qx, float qy, float qz, float qw) {
setLocalTransform(localX_, localY_, localZ_, qx, qy, qz, qw);
}
bool Shape3D::isSensor() const { return isValid() ? b3Shape_IsSensor(shapeId_) : false; }
void Shape3D::setFriction(float friction) {
if (!std::isfinite(friction) || friction < 0.f)
throw eve::Exception("Shape3D.setFriction: friction must be finite and >= 0");
if (!isValid()) return;
b3Shape_SetFriction(shapeId_, friction);
}
float Shape3D::getFriction() const { return isValid() ? b3Shape_GetFriction(shapeId_) : 0.f; }
void Shape3D::setRestitution(float restitution) {
if (!std::isfinite(restitution) || restitution < 0.f)
throw eve::Exception("Shape3D.setRestitution: restitution must be finite and >= 0");
if (!isValid()) return;
b3Shape_SetRestitution(shapeId_, restitution);
}
float Shape3D::getRestitution() const {
return isValid() ? b3Shape_GetRestitution(shapeId_) : 0.f;
}
void Shape3D::setRollingResistance(float resistance) {
if (!std::isfinite(resistance) || resistance < 0.f)
throw eve::Exception(
"Shape3D.setRollingResistance: resistance must be finite and >= 0");
if (!isValid()) return;
b3SurfaceMaterial material = b3Shape_GetSurfaceMaterial(shapeId_);
material.rollingResistance = resistance;
b3Shape_SetSurfaceMaterial(shapeId_, material);
}
float Shape3D::getRollingResistance() const {
return isValid() ? b3Shape_GetSurfaceMaterial(shapeId_).rollingResistance : 0.f;
}
void Shape3D::setTangentVelocity(float x, float y, float z) {
if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
throw eve::Exception("Shape3D.setTangentVelocity: components must be finite");
if (!isValid()) return;
b3SurfaceMaterial material = b3Shape_GetSurfaceMaterial(shapeId_);
material.tangentVelocity = b3Vec3{x, y, z};
b3Shape_SetSurfaceMaterial(shapeId_, material);
}
float Shape3D::getTangentVelocityX() const {
return isValid() ? b3Shape_GetSurfaceMaterial(shapeId_).tangentVelocity.x : 0.f;
}
float Shape3D::getTangentVelocityY() const {
return isValid() ? b3Shape_GetSurfaceMaterial(shapeId_).tangentVelocity.y : 0.f;
}
float Shape3D::getTangentVelocityZ() const {
return isValid() ? b3Shape_GetSurfaceMaterial(shapeId_).tangentVelocity.z : 0.f;
}
void Shape3D::setMaterialId(int id) {
if (!isValid()) return;
b3SurfaceMaterial material = b3Shape_GetSurfaceMaterial(shapeId_);
material.userMaterialId =
(material.userMaterialId & ~materialIdMask) | static_cast<uint32_t>(id);
b3Shape_SetSurfaceMaterial(shapeId_, material);
}
void Shape3D::setFrictionCombineMode(const std::string &mode) {
const uint64_t encoded = parseCombineMode(mode, "Shape3D.setFrictionCombineMode");
if (!isValid()) return;
b3SurfaceMaterial material = b3Shape_GetSurfaceMaterial(shapeId_);
material.userMaterialId &= ~(combineModeMask << frictionModeShift);
material.userMaterialId |= encoded << frictionModeShift;
b3Shape_SetSurfaceMaterial(shapeId_, material);
}
std::string Shape3D::getFrictionCombineMode() const {
if (!isValid()) return "default";
return combineModeName((b3Shape_GetSurfaceMaterial(shapeId_).userMaterialId >>
frictionModeShift) & combineModeMask);
}
void Shape3D::setRestitutionCombineMode(const std::string &mode) {
const uint64_t encoded = parseCombineMode(mode, "Shape3D.setRestitutionCombineMode");
if (!isValid()) return;
b3SurfaceMaterial material = b3Shape_GetSurfaceMaterial(shapeId_);
material.userMaterialId &= ~(combineModeMask << restitutionModeShift);
material.userMaterialId |= encoded << restitutionModeShift;
b3Shape_SetSurfaceMaterial(shapeId_, material);
}
std::string Shape3D::getRestitutionCombineMode() const {
if (!isValid()) return "default";
return combineModeName((b3Shape_GetSurfaceMaterial(shapeId_).userMaterialId >>
restitutionModeShift) & combineModeMask);
}
int Shape3D::getMaterialId() const {
return isValid()
? static_cast<int>(static_cast<uint32_t>(
b3Shape_GetSurfaceMaterial(shapeId_).userMaterialId))
: 0;
}
void Shape3D::setExplosionScale(float scale) {
if (!std::isfinite(scale) || scale < 0.f)
throw eve::Exception("Shape3D.setExplosionScale: scale must be finite and >= 0");
if (scale == explosionScale_) return;
explosionScale_ = scale;
if (body_ && body_->isValid()) recreate(isSensor());
}
void Shape3D::setDensity(float density) {
if (!std::isfinite(density) || density < 0.f)
throw eve::Exception("Shape3D.setDensity: density must be finite and >= 0");
if (!isValid()) return;
b3Shape_SetDensity(shapeId_, density, true);
}
float Shape3D::getDensity() const { return isValid() ? b3Shape_GetDensity(shapeId_) : 0.f; }
void Shape3D::setFilterBits(uint64_t categoryBits, uint64_t maskBits) {
if (!isValid()) return;
b3Filter filter = b3Shape_GetFilter(shapeId_);
filter.categoryBits = categoryBits;
filter.maskBits = maskBits;
b3Shape_SetFilter(shapeId_, filter, false);
}
uint64_t Shape3D::getCategoryBits() const {
return isValid() ? b3Shape_GetFilter(shapeId_).categoryBits : 0;
}
void Shape3D::setCategoryBits(uint64_t bits) {
if (!isValid()) return;
b3Filter filter = b3Shape_GetFilter(shapeId_);
filter.categoryBits = bits;
b3Shape_SetFilter(shapeId_, filter, true);
}
void Shape3D::setMaskBits(uint64_t bits) {
if (!isValid()) return;
b3Filter filter = b3Shape_GetFilter(shapeId_);
filter.maskBits = bits;
b3Shape_SetFilter(shapeId_, filter, true);
}
uint64_t Shape3D::getMaskBits() const {
return isValid() ? b3Shape_GetFilter(shapeId_).maskBits : 0;
}
void Shape3D::setGroupIndex(int index) {
if (!isValid()) return;
b3Filter filter = b3Shape_GetFilter(shapeId_);
filter.groupIndex = index;
b3Shape_SetFilter(shapeId_, filter, true);
}
int Shape3D::getGroupIndex() const {
return isValid() ? b3Shape_GetFilter(shapeId_).groupIndex : 0;
}
bool Shape3D::testPoint(float x, float y, float z) const {
if (!isValid() || !body_ || !body_->isValid()) return false;
b3Vec3 point{x, y, z};
b3ShapeProxy proxy;
proxy.points = &point;
proxy.count = 1;
proxy.radius = 0.f;
b3WorldTransform wt = b3Body_GetTransform(body_->raw());
b3Transform xf;
xf.p = b3Vec3{static_cast<float>(wt.p.x), static_cast<float>(wt.p.y),
static_cast<float>(wt.p.z)};
xf.q = wt.q;
switch (kind_) {
case Kind::Box:
case Kind::ConvexHull: {
const b3HullData *hull = b3Shape_GetHull(shapeId_);
if (!hull) return false;
return b3OverlapHull(hull, xf, &proxy);
}
case Kind::Sphere: {
b3Sphere sphere = b3Shape_GetSphere(shapeId_);
return b3OverlapSphere(&sphere, xf, &proxy);
}
case Kind::Capsule: {
b3Capsule capsule = b3Shape_GetCapsule(shapeId_);
return b3OverlapCapsule(&capsule, xf, &proxy);
}
case Kind::TriangleMesh: {
const b3Mesh mesh = b3Shape_GetMesh(shapeId_);
return mesh.data ? b3OverlapMesh(&mesh, xf, &proxy) : false;
}
case Kind::HeightField: {
const b3HeightFieldData *heightField = b3Shape_GetHeightField(shapeId_);
return heightField ? b3OverlapHeightField(heightField, xf, &proxy) : false;
}
}
return false;
}
} // namespace eve::physics