#include "physics/Shape3D.h" #include "physics/Body3D.h" #include "physics/World3D.h" #include "common/Exception.h" #include #include #include #include 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 &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 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(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 &vertices, const std::vector &indices, bool weldVertices, float weldTolerance, bool identifyEdges, bool useMedianSplit, b3Transform transform, const std::vector *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 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(index) >= points.size()) throw eve::Exception("%s: triangle index is outside the vertex array", operation); } std::vector mutableIndices = indices; b3MeshDef def{}; def.vertices = points.data(); def.indices = mutableIndices.data(); def.materialIndices = materialIndices && !materialIndices->empty() ? const_cast(materialIndices->data()) : nullptr; def.vertexCount = static_cast(points.size()); def.triangleCount = static_cast(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 &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(countX) * static_cast(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 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 hullVertices, int hullMaxVertices, std::vector meshVertices, std::vector meshIndices, b3MeshData *meshData, bool meshWeldVertices, float meshWeldTolerance, bool meshIdentifyEdges, bool meshUseMedianSplit, std::vector 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(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(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(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(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 &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 &vertices, const std::vector &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 &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 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(index)); requiredCount = std::max(requiredCount, static_cast(index) + 1); } if (encoded == meshMaterialIndices_ && requiredCount == static_cast(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(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(meshIndices_.size() / 3); if (triangleIndex < 0 || triangleIndex >= triangleCount) throw eve::Exception("Shape3D.getTriangleMeshMaterialIndex: triangle out of range"); return meshMaterialIndices_.empty() ? 0 : meshMaterialIndices_[static_cast(triangleIndex)]; } int Shape3D::getTriangleMeshMaterialCount() const { return kind_ == Kind::TriangleMesh ? std::max(1, static_cast(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(materialId) | (frictionCode << frictionModeShift) | (restitutionCode << restitutionModeShift); if (meshMaterials_.empty()) meshMaterials_.push_back(b3DefaultSurfaceMaterial()); meshMaterials_[static_cast(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(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(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(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(slot)].userMaterialId; return static_cast(static_cast(id)); } void Shape3D::setHeightFieldHeights(const std::vector &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 &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(width) * static_cast(depth)) throw eve::Exception("Shape3D.setHeightFieldRegion: data size must equal width * depth"); std::vector updated = heightValues_; for (int row = 0; row < depth; ++row) { for (int column = 0; column < width; ++column) { updated[static_cast(z + row) * static_cast(heightCountX_) + static_cast(x + column)] = heights[static_cast(row) * static_cast(width) + static_cast(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(z) * static_cast(heightCountX_) + static_cast(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(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(static_cast( 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(wt.p.x), static_cast(wt.p.y), static_cast(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