more physics fixes (somewhat finished hulls, gravity-related jitter-bounces from position correction (removed it), fixed BVH bugs, islands/bodies always being awake, refit/rebuild strategies, non-flattened BVHs not working, spheres not rolling, etc)

This commit is contained in:
ecker 2026-04-30 22:44:32 -05:00
parent 9c25bf9a9a
commit 60527c4e6b
19 changed files with 409 additions and 258 deletions

View File

@ -117,7 +117,7 @@
"gui": true,
"vsync": true, // vsync on vulkan side rather than engine-side
"hdr": true,
"vxgi": true,
"vxgi": false,
"culling": true,
"bloom": true,
"dof": true,

View File

@ -1,7 +1,7 @@
{
"type": "Object",
"name": "Burger",
"ignore": false,
"ignore": true,
"import": "/model.json",
"assets": [
// "/burger/burger.glb"

View File

@ -6,12 +6,11 @@
"metadata": {
"holdable": true,
"physics": {
// "gravity": [ 0, -1, 0 ],
"mass": 100,
"inertia": false,
// "type": "bounding box"
"type": "mesh"
// "type": "hull"
// "type": "mesh"
"type": "hull"
}
}
}

View File

@ -6,11 +6,12 @@
"metadata": {
"holdable": true,
"physics": {
"mass": 100000,
// "gravity": [0,0,0],
"mass": 100,
"inertia": false,
"type": "bounding box"
// "type": "mesh"
// "type": "mesh"
// "type": "hull"
}
}
}

View File

@ -198,7 +198,7 @@ ent:bind( "tick", function(self)
heldObject.uid = 0
heldObjectPhysicsState:enableGravity(true)
heldObjectPhysicsState:applyImpulse( flattenedTransform.forward * heldObjectPhysicsState:getMass() * 1000 )
heldObjectPhysicsState:applyImpulse( flattenedTransform.forward * heldObjectPhysicsState:getMass() * 50 )
playSound("phys_launch"..math.random(1,4))
else

View File

@ -15,7 +15,7 @@
"func_door_rotating_5568": { "action": "load", "payload": { "import": "/door.json", "metadata": { "angle":-1.570795, "normal": [1,0,0] } } },
"func_door_rotating_5584": { "action": "load", "payload": { "import": "/door.json", "metadata": { "angle":-1.570795, "normal": [1,0,0] } } },
// "prop_physics_override_5813": { "action": "load", "payload": { "import": "/physics_prop.json" } },
"prop_physics_override_5813": { "action": "load", "payload": { "import": "/physics_prop.json" } },
// regex matches
"/^prop_physics_[^o]/": { "action": "load", "payload": { "import": "/prop.json" } },

View File

@ -170,8 +170,8 @@ namespace pod {
float sleepTimer = 0.0f;
int32_t islandID = -1;
static constexpr float sleepThreshold = 0.5f; // seconds
static constexpr float linearSleepEpsilon = 0.1f; // m/s
static constexpr float angularSleepEpsilon = 0.1f; // rad/s
static constexpr float linearSleepEpsilon = 0.2f; // m/s
static constexpr float angularSleepEpsilon = 0.2f; // rad/s
};
struct World; // forward declare
@ -211,7 +211,10 @@ namespace pod {
pod::Vector3f normal;
float penetration;
// Warm-start cached values
pod::Vector3f localA;
pod::Vector3f localB;
// warm-start cached values
int lifetime = 0;
pod::Vector3f tangent = {};
float accumulatedNormalImpulse = 0.0f;
@ -241,7 +244,7 @@ namespace pod {
bool warmupSolver = true; // cache manifold data to warm up the solver
bool blockContactSolver = true; // use BlockNxN solvers (where N = number of contacts for a manifold)
bool psgContactSolver = true; // use PSG contact solver
bool useGjk = true; // currently don't have a way to broadphase mesh => narrowphase tri via GJK
bool useGjk = false; // currently don't have a way to broadphase mesh => narrowphase tri via GJK
bool fixedStep = true; // run physics simulation with a fixed delta time (with accumulation), rather than rely on actual engine deltatime
uint32_t substeps = 4; // number of substeps per frame tick
uint32_t reserveCount = 32; // amount of elements to reserve for vectors used in this system, to-do: have it tie to a memory pool allocator
@ -262,7 +265,7 @@ namespace pod {
// to-do: find possibly better values for this
uint32_t solverIterations = 10;
float baumgarteCorrectionPercent = 0.4f;
float baumgarteCorrectionPercent = 0.01f;
float baumgarteCorrectionSlop = 0.01f;
uf::stl::unordered_map<size_t, pod::Manifold> manifoldsCache;
@ -272,10 +275,12 @@ namespace pod {
// to-do: tweak this to not be annoying
pod::BVH::UpdatePolicy bvhUpdatePolicy = {
// triggers a refit
.displacementThreshold = 0.25f,
// triggers a rebuild
.overlapThreshold = 2.0f,
.dirtyRatioThreshold = 0.3f,
.maxFramesBeforeRebuild = 60, // * 10, // 10 seconds
.dirtyRatioThreshold = 0.3,
.maxFramesBeforeRebuild = 60,
};
float groundedThreshold = 0.7f; // threshold before marking a body as grounded

View File

@ -195,9 +195,10 @@ namespace uf {
uf::stl::unordered_map<uf::stl::string, uf::Mesh::AttributeView> attributes;
const AttributeView& operator[](const uf::stl::string& name) const {
static AttributeView null{};
//static AttributeView null{};
if ( auto it = attributes.find(name); it != attributes.end() ) return it->second;
return null;
UF_EXCEPTION("invalid view: {}", name);
//return null;
}
};
typedef uf::stl::vector<uf::Mesh::View> views_t;

View File

@ -349,7 +349,7 @@ void ext::PlayerBehavior::tick( uf::Object& self ) {
if ( physicsBody.gravity == pod::Vector3f{0,0,0} ) stats.noclipped = true;
{
speed.rotate = metadata.movement.rotate * ONE_OVER_SIXTY /*uf::physics::time::delta*/;
speed.rotate = metadata.movement.rotate * uf::physics::time::delta;
speed.move = metadata.movement.move;
speed.run = metadata.movement.run;
speed.walk = metadata.movement.walk;
@ -466,7 +466,7 @@ void ext::PlayerBehavior::tick( uf::Object& self ) {
if ( stats.walking ) {
float factor = stats.floored ? 1.0f : speed.air;
if ( stats.noclipped ) {
physicsBody.velocity += target * speed.move;
physicsBody.velocity += target * speed.move * ONE_OVER_SIXTY;
} else {
physicsBody.velocity += target * std::clamp( speed.move * factor - uf::vector::dot( physicsBody.velocity, target ), 0.0f, speed.move * 10 * ONE_OVER_SIXTY /*uf::physics::time::delta*/ );
}
@ -487,9 +487,9 @@ void ext::PlayerBehavior::tick( uf::Object& self ) {
TIMER(0.0625, stats.floored && keys.jump && !stats.noclipped ) {
physicsBody.velocity += translator.up * metadata.movement.jump;
}
if ( stats.floored && keys.jump && stats.noclipped ) transform.position += translator.up * metadata.movement.jump * ONE_OVER_SIXTY /*uf::physics::time::delta*/ * 4.0f;
if ( stats.floored && keys.jump && stats.noclipped ) transform.position += translator.up * metadata.movement.jump * uf::physics::time::delta * 4.0f;
if ( keys.crouch ) {
if ( stats.noclipped ) transform.position -= translator.up * metadata.movement.jump * ONE_OVER_SIXTY /*uf::physics::time::delta*/ * 4.0f;
if ( stats.noclipped ) transform.position -= translator.up * metadata.movement.jump * uf::physics::time::delta * 4.0f;
else {
if ( !metadata.system.crouching ) stats.deltaCrouch = true;
metadata.system.crouching = true;

View File

@ -39,6 +39,9 @@ namespace {
return bounds;
}
FORCE_INLINE pod::AABB computeConvexHullAABB( const uf::Mesh::View& view, pod::AABB bounds = { { FLT_MAX, FLT_MAX, FLT_MAX }, { -FLT_MAX, -FLT_MAX, -FLT_MAX } } ) {
return ::computeConvexHullAABB( view, view["position"], bounds );
}
FORCE_INLINE pod::AABB mergeAabb( const pod::AABB& a, const pod::AABB& b ) {
return {
@ -110,7 +113,8 @@ namespace {
auto [ p1, p2 ] = ::getCapsuleSegment( body );
return ::computeSegmentAABB( p1, p2, body.collider.capsule.radius );
} break;
case pod::ShapeType::MESH: {
case pod::ShapeType::MESH:
case pod::ShapeType::CONVEX_HULL: {
if ( body.collider.mesh.bvh && !body.collider.mesh.bvh->bounds.empty() )
return {
transform.position + body.collider.mesh.bvh->bounds[0].min,
@ -120,18 +124,7 @@ namespace {
pod::AABB bounds = { { FLT_MAX, FLT_MAX, FLT_MAX }, { -FLT_MAX, -FLT_MAX, -FLT_MAX } };
for ( const auto& view : meshData.buffer_views ) ::computeConvexHullAABB( view, view["position"], bounds );
return ::transformAabbToWorld( bounds, transform );
} break;
case pod::ShapeType::CONVEX_HULL: {
if ( body.collider.convexHull.bvh && !body.collider.convexHull.bvh->bounds.empty() )
return {
transform.position + body.collider.convexHull.bvh->bounds[0].min,
transform.position + body.collider.convexHull.bvh->bounds[0].max,
};
const auto& meshData = *body.collider.convexHull.mesh;
pod::AABB bounds = { { FLT_MAX, FLT_MAX, FLT_MAX }, { -FLT_MAX, -FLT_MAX, -FLT_MAX } };
for ( const auto& view : meshData.buffer_views ) ::computeConvexHullAABB( view, view["position"], bounds );
return ::transformAabbToWorld( bounds, transform );
} break;
}
default: {
} break;
}

View File

@ -282,7 +282,7 @@ namespace {
// populate initial indices and bounds
for ( size_t viewID = 0; viewID < hullCount; ++viewID ) {
const auto& view = views[viewID];
auto aabb = ::computeConvexHullAABB( view, view["position"] );
auto aabb = ::computeConvexHullAABB( view );
bounds.emplace_back( aabb );
bvh.indices.emplace_back( viewID );
@ -314,9 +314,29 @@ namespace {
float oldRootArea = ::aabbSurfaceArea( bvh.bounds[0] );
// update/check each body
for ( auto i = 0; i < bvh.nodes.size(); ++i ) {
auto& node = bvh.nodes[i];
if ( /*node.count*/ node.getCount() == 0 ) continue;
auto& body = *bodies[bvh.indices[node.start]];
auto& oldBounds = bvh.bounds[i];
auto& newBounds = body.bounds;
// compute displacement relative to size
pod::Vector3f oldCenter = ( oldBounds.min + oldBounds.max ) * 0.5f;
pod::Vector3f newCenter = ( newBounds.min + newBounds.max ) * 0.5f;
float displacement = uf::vector::distance( newCenter, oldCenter );
pod::Vector3f extent = oldBounds.max - oldBounds.min;
float size = std::max({extent.x, extent.y, extent.z, 1e-6f});
if ( displacement > policy.displacementThreshold * size ) ++dirtyCount;
}
/*
for ( auto idx : bvh.indices ) {
auto& body = *bodies[idx];
// to-do: instead check against bounds in BVH
pod::AABB oldBounds = body.bounds;
body.bounds = ::computeAABB( body );
pod::AABB newBounds = body.bounds;
@ -334,11 +354,12 @@ namespace {
// update nodes
for ( auto i = 0; i < bvh.nodes.size(); ++i ) {
auto& node = bvh.nodes[i];
if ( /*node.count*/ node.getCount() == 0 ) continue;
if ( node.getCount() == 0 ) continue;
auto& bound = bvh.bounds[i];
bound = bodies[bvh.indices[node.start]]->bounds;
for ( auto i = 1; i < node.getCount() /*node.count*/; ++i ) bound = ::mergeAabb( bound, bodies[bvh.indices[node.start + i]]->bounds );
for ( auto i = 1; i < node.getCount(); ++i ) bound = ::mergeAabb( bound, bodies[bvh.indices[node.start + i]]->bounds );
}
*/
float dirtyRatio = (float) dirtyCount / (float) bodies.size();
@ -388,6 +409,8 @@ namespace {
bound = ::mergeAabb(bvh.bounds[node.left], bvh.bounds[node.right]);
}
}
if ( !bvh.flattened.empty() ) ::flattenBVH( bvh, 0 );
}
// avoids creating a vector for bounds
@ -421,6 +444,8 @@ namespace {
bvh.bounds[i] = ::mergeAabb( bvh.bounds[node.left], bvh.bounds[node.right] );
node.setAsleep( bvh.nodes[node.left].isAsleep() && bvh.nodes[node.right].isAsleep());
}
if ( !bvh.flattened.empty() ) ::flattenBVH( bvh, 0 );
}
void refitBVH( pod::BVH& bvh, const uf::Mesh& mesh ) {
@ -500,7 +525,8 @@ namespace {
const auto& nodeA = bvh.nodes[nodeAID];
const auto& nodeB = bvh.nodes[nodeBID];
if ( (nodeA.isAsleep() && nodeB.isAsleep()) || !::aabbOverlap( bvh.bounds[nodeAID], bvh.bounds[nodeBID] ) ) return;
if ( nodeA.isAsleep() && nodeB.isAsleep() ) return;
if ( !::aabbOverlap( bvh.bounds[nodeAID], bvh.bounds[nodeBID] ) ) return;
if ( nodeA.getCount() > 0 && nodeB.getCount() > 0 ) {
for ( auto i = 0; i < nodeA.getCount(); ++i ) {
@ -519,8 +545,7 @@ namespace {
if ( nodeA.getCount() == 0 ) {
::traverseNodePair( bvh, nodeA.left, nodeBID, pairs );
::traverseNodePair( bvh, nodeA.right, nodeBID, pairs );
}
if ( nodeB.getCount() == 0 ) {
} else if ( nodeB.getCount() == 0 ) {
::traverseNodePair( bvh, nodeAID, nodeB.left, pairs );
::traverseNodePair( bvh, nodeAID, nodeB.right, pairs );
}
@ -530,7 +555,8 @@ namespace {
const auto& nodeA = bvhA.nodes[nodeAID];
const auto& nodeB = bvhB.nodes[nodeBID];
if ( (nodeA.isAsleep() && nodeB.isAsleep()) || !::aabbOverlap( bvhA.bounds[nodeAID], bvhB.bounds[nodeBID] ) ) return;
if ( nodeA.isAsleep() && nodeB.isAsleep() ) return;
if ( !::aabbOverlap( bvhA.bounds[nodeAID], bvhB.bounds[nodeBID] ) ) return;
if ( nodeA.getCount() > 0 && nodeB.getCount() > 0 ) {
for ( auto i = 0; i < nodeA.getCount(); ++i ) {
@ -538,7 +564,7 @@ namespace {
pod::BVH::index_t bodyA = bvhA.indices[nodeA.start + i];
pod::BVH::index_t bodyB = bvhB.indices[nodeB.start + j];
if ( bodyA == bodyB ) continue;
if ( bodyA > bodyB ) std::swap( bodyA, bodyB );
//if ( bodyA > bodyB ) std::swap( bodyA, bodyB );
pairs.emplace_back(bodyA, bodyB);
}
@ -621,6 +647,9 @@ namespace {
}
::traverseNodePair( bvh, node.left, node.right, pairs );
::traverseBVH( bvh, node.left, pairs );
::traverseBVH( bvh, node.right, pairs );
}
void queryOverlaps( const pod::BVH& bvh, pod::BVH::pairs_t& outPairs ) {
@ -649,7 +678,6 @@ namespace {
if ( bvhA.nodes.empty() || bvhB.nodes.empty() ) return;
outPairs.reserve(uf::physics::impl::settings.reserveCount);
::traverseNodePair(bvhA, 0, bvhB, 0, relTransform, outPairs);
UF_EXCEPTION("unimplemented");
::postprocessPairs( outPairs );
}
@ -671,7 +699,8 @@ namespace {
while ( !stack.empty() ) {
pod::BVH::index_t idx = stack.top(); stack.pop();
auto& node = bvh.nodes[idx];
if ( node.isAsleep() || !::aabbOverlap( bounds, bvh.bounds[idx] ) ) continue;
if ( node.isAsleep() ) continue;
if ( !::aabbOverlap( bounds, bvh.bounds[idx] ) ) continue;
if ( node.getCount() > 0 ) {
for ( auto i = 0; i < node.getCount(); ++i) outIndices.emplace_back(bvh.indices[node.start + i]);
@ -692,7 +721,8 @@ namespace {
if ( nodeID == 0 ) outIndices.reserve(uf::physics::impl::settings.reserveCount);
const auto& node = bvh.nodes[nodeID];
if ( node.isAsleep() || !::aabbOverlap( bounds, bvh.bounds[nodeID] ) ) return;
if ( node.isAsleep() ) return;
if ( !::aabbOverlap( bounds, bvh.bounds[nodeID] ) ) return;
if ( node.getCount() > 0 ) {
for ( auto i = 0; i < node.getCount(); ++i ) outIndices.emplace_back(bvh.indices[node.start + i]);
@ -720,7 +750,8 @@ namespace {
const auto& node = bvh.nodes[idx];
float tMin, tMax;
if ( node.isAsleep() || !::rayAabbIntersect( ray, bvh.bounds[idx], tMin, tMax ) ) continue;
//if ( node.isAsleep() ) continue;
if ( !::rayAabbIntersect( ray, bvh.bounds[idx], tMin, tMax ) ) continue;
if ( tMin > maxDist ) continue;
if ( node.getCount() > 0 ) {
@ -739,7 +770,8 @@ namespace {
const auto& node = bvh.nodes[nodeID];
float tMin, tMax;
if ( node.isAsleep() || !::rayAabbIntersect( ray, bvh.bounds[nodeID], tMin, tMax ) ) return;
//if ( node.isAsleep() ) return;
if ( !::rayAabbIntersect( ray, bvh.bounds[nodeID], tMin, tMax ) ) return;
if ( tMin > maxDist ) return;
if ( node.getCount() > 0 ) {
@ -765,14 +797,14 @@ namespace {
for ( pod::BVH::index_t a = 0; a < nodes.size(); ++a ) {
const auto& nodeA = nodes[a];
if ( nodeA.getCount() <= 0 || nodeA.isAsleep() ) continue;
if ( nodeA.getCount() <= 0 ) continue;
const auto& boundsA = bounds[a];
pod::BVH::index_t b = a + 1;
while ( b < nodes.size() ) {
const auto& nodeB = nodes[b];
if ( nodeB.isAsleep() || !::aabbOverlap( boundsA, bounds[b] ) ) {
if ( (nodeA.isAsleep() && nodeB.isAsleep()) || !::aabbOverlap( boundsA, bounds[b] ) ) {
b = nodeB.skipIndex;
continue;
}
@ -820,7 +852,9 @@ namespace {
const auto& nodeB = bvhB.flattened[b];
if ( nodeA.isAsleep() && nodeB.isAsleep() ) continue;
if ( !::aabbOverlap( bvhA.flatBounds[a], bvhB.flatBounds[b] ) ) continue;
if ( !::aabbOverlap( bvhA.flatBounds[a], bvhB.flatBounds[b] ) ) {
continue;
}
bool isLeafA = (nodeA.getCount() > 0);
bool isLeafB = (nodeB.getCount() > 0);
@ -885,11 +919,6 @@ namespace {
if ( nodeA.isAsleep() && nodeB.isAsleep() ) continue;
/*
pod::AABB worldBoundsA = ::transformAabbToWorld( boundsA[a], tA );
pod::AABB worldBoundsB = ::transformAabbToWorld( boundsB[b], tB );
if ( !::aabbOverlap( worldBoundsA, worldBoundsB ) ) continue;
*/
pod::AABB boundsB_in_A = ::transformAabbToWorld(boundsB[b], relTransform);
if ( !::aabbOverlap( boundsA[a], boundsB_in_A ) ) continue;
@ -965,7 +994,7 @@ namespace {
while ( idx < nodes.size() ) {
const auto& node = nodes[idx];
float tMin, tMax;
if ( !node.isAsleep() && ::rayAabbIntersect( ray, bvh.flatBounds[idx], tMin, tMax ) && tMin <= maxDist ) {
if ( /*!node.isAsleep() &&*/ ::rayAabbIntersect( ray, bvh.flatBounds[idx], tMin, tMax ) && tMin <= maxDist ) {
// leaf
if ( node.getCount() > 0 ) {
for ( auto i = 0; i < node.getCount(); ++i ) {
@ -1014,6 +1043,8 @@ namespace {
}
}
};
// to-do: rewrite this, I'm pretty sure it's faulty
void buildIslands( const pod::BVH::pairs_t& pairs, const uf::stl::vector<pod::PhysicsBody*>& bodies, uf::stl::vector<pod::Island>& islands ) {
UnionFind unionizer(bodies.size());
@ -1039,13 +1070,6 @@ namespace {
auto [ it, inserted ] = rootToIsland.try_emplace( root, (pod::BVH::index_t) islands.size());
if ( inserted ) islands.emplace_back();
/*
if ( rootToIsland.find(root) == rootToIsland.end() ) {
rootToIsland[root] = (pod::BVH::index_t) islands.size();
islands.emplace_back();
}
*/
pod::BVH::index_t islandID = rootToIsland[root];
islands[islandID].indices.emplace_back( i );
}
@ -1070,34 +1094,29 @@ namespace {
}
}
}
}
bool updateIsland( pod::Island& island, uf::stl::vector<pod::PhysicsBody*>& bodies, float dt ) {
island.awake = false;
// update islands
for ( auto it = islands.begin(); it != islands.end(); ) {
auto& island = *it;
island.awake = false;
for ( auto idx : island.indices ) {
auto& body = *bodies[idx];
if ( !body.activity.awake ) continue;
float linSpeedSq = uf::vector::magnitude( body.velocity );
float angSpeedSq = uf::vector::magnitude( body.angularVelocity );
if ( linSpeedSq < pod::Activity::linearSleepEpsilon && angSpeedSq < pod::Activity::angularSleepEpsilon) {
body.activity.sleepTimer += dt;
} else {
body.activity.sleepTimer = 0.0f;
// wake island if something is awake in it
for ( auto idx : island.indices ) {
auto& body = *bodies[idx];
if ( !body.activity.awake ) continue;
island.awake = true;
}
// update bodies within island
for ( auto idx : island.indices )
(island.awake ? ::wakeBody : ::sleepBody)( *bodies[idx] );
if ( body.activity.sleepTimer < pod::Activity::sleepThreshold ) {
island.awake = true;
// erase sleeping island
if ( !island.awake ) {
it = islands.erase(it);
} else {
++it;
}
}
// update bodies within island
for ( auto idx : island.indices )
(island.awake ? ::wakeBody : ::sleepBody)( *bodies[idx] );
return island.awake;
}
}

View File

@ -19,12 +19,8 @@ namespace {
pod::Simplex simplex;
if ( !::gjk( hullView, body, simplex ) ) continue;
auto result = ::epa( hullView, body, simplex );
if ( !uf::vector::isValid(result.point) ) continue;
manifold.points.emplace_back(result);
if ( !::generateClippingManifold( hullView, body, result, manifold ) ) continue;
hit = true;
}
return hit;
@ -75,10 +71,7 @@ namespace {
if ( !::gjk( viewA, viewB, simplex ) ) continue;
auto result = ::epa( viewA, viewB, simplex );
if ( !uf::vector::isValid(result.point) ) continue;
manifold.points.emplace_back(result);
if ( !::generateClippingManifold( viewA, viewB, result, manifold ) ) continue;
hit = true;
}
return hit;

View File

@ -34,6 +34,153 @@ namespace {
return face;
};
void getSupportFace( const pod::PhysicsBody& body, const pod::Vector3f& dir, pod::Vector3f outPoly[3], int& outCount ) {
outCount = 0;
const auto transform = ::getTransform(body);
pod::Vector3f localDir = uf::quaternion::rotate(uf::quaternion::inverse(transform.orientation), dir);
switch (body.collider.type) {
case pod::ShapeType::TRIANGLE: {
outCount = 3;
bool hasTransform = ( body.transform != nullptr );
FOR_EACH(3, {
outPoly[i] = hasTransform ? uf::transform::apply( transform, body.collider.triangle.points[i] ) : body.collider.triangle.points[i];
});
} break;
case pod::ShapeType::AABB: {
outCount = 4;
pod::Vector3f n = localDir;
pod::Vector3f absN = { std::fabs(n.x), std::fabs(n.y), std::fabs(n.z) };
pod::Vector3f min = body.collider.aabb.min;
pod::Vector3f max = body.collider.aabb.max;
// dominant axis
if ( absN.x > absN.y && absN.x > absN.z ) {
float x = (n.x > 0) ? max.x : min.x;
outPoly[0] = {x, min.y, max.z};
outPoly[1] = {x, min.y, min.z};
outPoly[2] = {x, max.y, min.z};
outPoly[3] = {x, max.y, max.z};
if ( n.x < 0 ) std::swap(outPoly[1], outPoly[3]);
} else if ( absN.y > absN.z ) {
float y = (n.y > 0) ? max.y : min.y;
outPoly[0] = {max.x, y, max.z};
outPoly[1] = {max.x, y, min.z};
outPoly[2] = {min.x, y, min.z};
outPoly[3] = {min.x, y, max.z};
if ( n.y < 0 ) std::swap(outPoly[1], outPoly[3]);
} else {
float z = (n.z > 0) ? max.z : min.z;
outPoly[0] = {min.x, max.y, z};
outPoly[1] = {min.x, min.y, z};
outPoly[2] = {max.x, min.y, z};
outPoly[3] = {max.x, max.y, z};
if (n.z < 0) std::swap(outPoly[1], outPoly[3]);
}
FOR_EACH(4, {
outPoly[i] = uf::transform::apply(transform, outPoly[i]);
});
} break;
case pod::ShapeType::MESH:
case pod::ShapeType::CONVEX_HULL: {
if ( !body.collider.convexHull.mesh ) return;
const auto& mesh = *body.collider.convexHull.mesh;
auto selectedViewIdx = body.viewIndex;
float bestDot = -FLT_MAX;
pod::Triangle bestTri;
for ( auto viewIdx = 0; viewIdx < mesh.buffer_views.size(); ++viewIdx ) {
if ( 0 <= selectedViewIdx && selectedViewIdx != viewIdx ) continue;
const auto& view = mesh.buffer_views[viewIdx];
auto& indices = view["index"];
auto& positions = view["position"];
for ( size_t i = 0; i < view.index.count / 3; ++i ) {
pod::Triangle tri = ::fetchTriangle( view, indices, positions, i );
pod::Vector3f normal = ::triangleNormal( tri );
float d = uf::vector::dot( normal, localDir );
if ( d > bestDot ) {
bestDot = d;
bestTri = tri;
}
}
}
outCount = 3;
FOR_EACH(3, {
outPoly[i] = uf::transform::apply(transform, bestTri.points[i]);
});
} break;
// unsupported, fallback to single contact point
default: {
outCount = 0;
} break;
}
}
bool generateClippingManifold( const pod::PhysicsBody& a, const pod::PhysicsBody& b, const pod::Contact& contact, pod::Manifold& manifold ) {
if ( !uf::vector::isValid(contact.point) ) return false;
auto& normal = contact.normal;
pod::Vector3f polyA[4];
pod::Vector3f polyB[4];
int countA = 0;
int countB = 0;
::getSupportFace(a, normal, polyA, countA);
::getSupportFace(b, -normal, polyB, countB);
if ( countA < 3 || countB < 3 ) {
if ( manifold.points.empty() ) manifold.points.emplace_back(contact);
return true;
}
// to-do: reference face is the most perpendicular face
pod::Vector3f* refPoly = polyA;
pod::Vector3f* incPoly = polyB;
int refCount = countA;
int incCount = countB;
pod::Vector3f clipBuffer1[8];
pod::Vector3f clipBuffer2[8];
int clipCount = incCount;
for ( auto i = 0; i < incCount; ++i ) clipBuffer1[i] = incPoly[i];
pod::Vector3f refNormal = uf::vector::normalize(uf::vector::cross(refPoly[1] - refPoly[0], refPoly[2] - refPoly[0]));
for ( auto i = 0; i < refCount; ++i ) {
pod::Vector3f edgeStart = refPoly[i];
pod::Vector3f edgeEnd = refPoly[(i + 1) % refCount];
pod::Vector3f edgeVector = edgeEnd - edgeStart;
pod::Vector3f edgePlaneNormal = uf::vector::normalize(uf::vector::cross(edgeVector, refNormal));
float edgePlaneOffset = uf::vector::dot(edgePlaneNormal, edgeStart);
if (i % 2 == 0) {
clipCount = ::clipPolygonAgainstPlane(clipBuffer1, clipCount, edgePlaneNormal, edgePlaneOffset, clipBuffer2);
} else {
clipCount = ::clipPolygonAgainstPlane(clipBuffer2, clipCount, edgePlaneNormal, edgePlaneOffset, clipBuffer1);
}
}
pod::Vector3f* finalPoly = (refCount % 2 == 0) ? clipBuffer1 : clipBuffer2;
float refOffset = uf::vector::dot(refNormal, refPoly[0]);
for (int i = 0; i < clipCount; ++i) {
float distance = uf::vector::dot(finalPoly[i], refNormal) - refOffset;
// point is penetrating or touching
if ( distance <= EPS ) {
pod::Contact c;
c.point = finalPoly[i] - refNormal * (distance * 0.5f);
c.normal = normal;
c.penetration = -distance;
manifold.points.emplace_back(c);
}
}
if ( manifold.points.empty() ) manifold.points.emplace_back(contact);
return true;
}
uf::stl::vector<pod::Face> initialPolytope( const pod::Simplex& s ) {
UF_ASSERT( s.pts.size() == 4 );
@ -68,7 +215,7 @@ namespace {
}
// remove visible faces
for ( auto i = (int32_t) remove.size() - 1; i >= 0; --i ) {
for ( auto i = (int32_t) remove.size() - 1; i >= 0; --i ) {
auto idx = remove[i];
faces[idx] = faces.back();
faces.pop_back();
@ -116,7 +263,7 @@ namespace {
auto contact = ( pA + pB ) * 0.5f;
auto normal = f.normal;
float penetration = uf::vector::dot( (pB - pA), normal );
// flip normal
if ( penetration < 0.0f ) {
f.normal = -f.normal;

View File

@ -41,6 +41,7 @@ namespace {
if ( d1 > d2 ) return tri.points[1];
return tri.points[2];
} break;
case pod::ShapeType::MESH:
case pod::ShapeType::CONVEX_HULL: {
const auto transform = ::getTransform( body );
const auto& mesh = *body.collider.convexHull.mesh;
@ -203,16 +204,10 @@ namespace {
if ( uf::vector::dot( newPt.p, dir ) < 0 ) return false; // didn't pass origin, no collision
// would invalidate the simplex
if ( ::isDegenerate( simplex, newPt ) ) {
#if 1
// nudge direction with a small orthogonal component
if ( fabs(dir.x) < fabs(dir.y) && fabs(dir.x) < fabs(dir.z) ) dir = uf::vector::normalize( pod::Vector3f{1,0,0} + dir * 0.01f );
else if ( fabs(dir.y) < fabs(dir.z) ) dir = uf::vector::normalize( pod::Vector3f{0,1,0} + dir * 0.01f );
else dir = uf::vector::normalize( pod::Vector3f{0,0,1} + dir * 0.01f );
#else
// choose an alternate probe
static pod::Vector3f fallbackDirs[3] = { {1,0,0}, {0,1,0}, {0,0,1} };
dir = fallbackDirs[ (it + simplex.pts.size()) % 3 ];
#endif
continue; // try again
}
// add new point to simplex
@ -221,27 +216,7 @@ namespace {
if ( ::updateSimplex(simplex, dir) ) return true; // simplex contains origin, finished
}
#if 1
return false;
#else
// if overlap detected but simplex ended at triangle, fix it, as EPA requires a tetrahedron:
if ( simplex.pts.size() == 3 ) {
// points
auto& A0 = simplex.pts[0].p;
auto& B0 = simplex.pts[1].p;
auto& C0 = simplex.pts[2].p;
// triangle normal
auto normal = uf::vector::normalize( uf::vector::cross( B0 - A0, C0 - A0 ) );
// try support in +normal
auto extra = ::supportMinkowskiDetailed( a, b, normal );
float vol = fabs( uf::vector::dot( extra.p - A0, uf::vector::cross( B0 - A0, C0 - A0 ) ) );
if ( vol < eps ) extra = ::supportMinkowskiDetailed( a, b, -normal ); // if still coplanar, try -normal
simplex.pts.emplace_back(extra); // force tetrahedron
}
return !simplex.pts.empty();
#endif
}
}

View File

@ -55,15 +55,17 @@ namespace {
bool hullHull( const pod::PhysicsBody& a, const pod::PhysicsBody& b, pod::Manifold& manifold, float eps = EPS );
// ugh
pod::AABB computeAABB( const pod::PhysicsBody& body );
FORCE_INLINE bool aabbOverlap( const pod::AABB& a, const pod::AABB& b, float eps = EPS );
pod::Vector3f aabbCenter( const pod::AABB& aabb );
pod::Vector3f getVertex( const uf::Mesh::View& view, const uf::Mesh::AttributeView& positions, size_t index );
pod::TriangleWithNormal fetchTriangle( const uf::Mesh& mesh, size_t triID, const pod::PhysicsBody& body, bool fast = false );
pod::TriangleWithNormal fetchTriangle( const uf::Mesh& mesh, size_t triID, const pod::PhysicsBody& body );
// to-do: define maxIterations as a setting
bool gjk( const pod::PhysicsBody& a, const pod::PhysicsBody& b, pod::Simplex& simplex, int maxIterations = 20, float eps = EPS );
pod::Contact epa( const pod::PhysicsBody& a, const pod::PhysicsBody& b, const pod::Simplex& simplex, uint32_t maxIterations = 64, float eps = EPS );
bool gjk( const pod::Ray& ray, const pod::PhysicsBody& body, float maxDist, float& outT, pod::Vector3f& outNormal, float eps = EPS );
pod::Contact epa( const pod::PhysicsBody& a, const pod::PhysicsBody& b, const pod::Simplex& simplex, uint32_t maxIterations = 64, float eps = EPS );
bool generateClippingManifold( const pod::PhysicsBody& a, const pod::PhysicsBody& b, const pod::Contact& contact, pod::Manifold& manifold );
void queryBVH( const pod::BVH& bvh, const pod::AABB& bounds, uf::stl::vector<pod::BVH::index_t>& indices );
void queryBVH( const pod::BVH& bvh, const pod::AABB& bounds, uf::stl::vector<pod::BVH::index_t>& indices, pod::BVH::index_t nodeID );
@ -89,13 +91,14 @@ namespace {
// marks a body as asleep
void wakeBody( pod::PhysicsBody& body ) {
bool wasAwake = body.activity.awake;
if ( !wasAwake ) UF_MSG_DEBUG("name={} waking up", body.object->getName());
if ( !wasAwake ) {
body.activity.sleepTimer = 0.0f;
}
body.activity.awake = true;
body.activity.sleepTimer = 0.0f;
}
void sleepBody( pod::PhysicsBody& body ) {
if ( body.activity.awake ) UF_MSG_DEBUG("name={} sleeping", body.object->getName());
bool wasAsleep = !body.activity.awake;
body.activity.awake = false;
body.velocity = pod::Vector3f{};
@ -106,6 +109,9 @@ namespace {
bool wasGrounded = body.activity.grounded;
body.activity.grounded = false;
// update bounds
body.bounds = ::computeAABB( body );
// already asleep
if ( !body.activity.awake ) return;
@ -437,4 +443,33 @@ namespace {
return best;
}
}
int clipPolygonAgainstPlane( const pod::Vector3f* inPoly, int inCount, const pod::Vector3f& planeNormal, float planeOffset, pod::Vector3f* outPoly ) {
if (inCount == 0) return 0;
int outCount = 0;
pod::Vector3f prevPoint = inPoly[inCount - 1];
float prevDistance = uf::vector::dot(prevPoint, planeNormal) - planeOffset;
for (int i = 0; i < inCount; ++i) {
pod::Vector3f currPoint = inPoly[i];
float currDistance = uf::vector::dot(currPoint, planeNormal) - planeOffset;
// If they cross the plane, compute the intersection point
if ((prevDistance * currDistance) < 0.0f) {
float t = prevDistance / (prevDistance - currDistance);
outPoly[outCount++] = prevPoint + (currPoint - prevPoint) * t;
}
// If the current point is 'inside' or on the plane (distance <= 0), keep it
if (currDistance <= 0.0f) {
outPoly[outCount++] = currPoint;
}
prevPoint = currPoint;
prevDistance = currDistance;
}
return outCount;
}
}

View File

@ -134,8 +134,9 @@ void uf::physics::impl::step( pod::World& world, float dt ) {
manifolds.clear();
manifolds.reserve(uf::physics::impl::settings.reserveCount);
// sleeping island, skip
if ( !::updateIsland( island, bodies, dt ) ) continue;
// sleeping island, skip (asleep islands shouldn't ever be in here)
if ( !island.awake ) continue;
// iterate overlap pairs
for ( auto& [ ia, ib ] : island.pairs ) {
auto& a = *bodies[ia];
@ -145,6 +146,9 @@ void uf::physics::impl::step( pod::World& world, float dt ) {
// did not collide
if ( !::generateContacts( a, b, manifold, dt ) ) continue;
// compute local points (for reprojection)
::computeLocalContacts( manifold );
// bodies with meshes already reorient the normal to the triangle's center
// do not do it for meshes because it'll reorient to the mesh's origin
// do not do it for planes
@ -174,9 +178,9 @@ void uf::physics::impl::step( pod::World& world, float dt ) {
for ( auto& c : manifold.points ) {
if ( std::fabs(uf::vector::dot(c.normal, pod::Vector3f{0,1,0})) > uf::physics::impl::settings.groundedThreshold ) {
// only mark if contact point is below body
if ( c.point.y < getPosition(a).y ) a.activity.grounded = true;
if ( c.point.y < getPosition(b).y ) b.activity.grounded = true;
}
if ( c.point.y < ::getPosition(a).y ) a.activity.grounded = true;
if ( c.point.y < ::getPosition(b).y ) b.activity.grounded = true;
}
}
// store manifold
@ -186,11 +190,11 @@ void uf::physics::impl::step( pod::World& world, float dt ) {
// pass manifolds to solver
::solveContacts( manifolds, dt );
// do position correction
::solvePositions( manifolds, dt );
// ::solvePositions( manifolds, dt );
// cache manifold positions
if ( uf::physics::impl::settings.warmupSolver ) {
::updateManifoldCache( manifolds, uf::physics::impl::settings.manifoldsCache );
}
::updateManifoldCache( manifolds, uf::physics::impl::settings.manifoldsCache );
}
}
if ( uf::physics::impl::settings.warmupSolver ) ::pruneManifoldCache( uf::physics::impl::settings.manifoldsCache );
@ -253,14 +257,6 @@ void uf::physics::impl::updateInertia( pod::PhysicsBody& body ) {
switch ( body.collider.type ) {
case pod::ShapeType::AABB: {
/*
// old
pod::Vector3f extents = (body.collider.aabb.max - body.collider.aabb.min);
extents *= extents; // square it;
body.inertiaTensor = extents * (body.mass / 12.0f);
body.inverseInertiaTensor = 1.0f / body.inertiaTensor;
*/
pod::Vector3f dims = (body.collider.aabb.max - body.collider.aabb.min);
pod::Vector3f dimsSq = dims * dims;
body.inertiaTensor = pod::Vector3f{ dimsSq.y + dimsSq.z, dimsSq.x + dimsSq.z, dimsSq.x + dimsSq.y } * (body.mass / 12.0f);
@ -285,7 +281,8 @@ void uf::physics::impl::updateInertia( pod::PhysicsBody& body ) {
body.inertiaTensor = { Ixx, Iyy, Izz };
body.inverseInertiaTensor = { 1.0f/Ixx, 1.0f/Iyy, 1.0f/Izz };
} break;
case pod::ShapeType::MESH: {
case pod::ShapeType::MESH:
case pod::ShapeType::CONVEX_HULL: {
const auto& bvh = *body.collider.mesh.bvh;
pod::Matrix3f inertia = {};

View File

@ -8,12 +8,16 @@ namespace {
float angularTermB = 0.0f;
if ( !a.isStatic ) {
pod::Vector3f crossA = uf::vector::cross(rA, n);
angularTermA = uf::vector::dot(uf::vector::cross(crossA * a.inverseInertiaTensor, rA), n);
auto invIa = ::computeWorldInverseInertia(a);
auto crossA = uf::vector::cross(rA, n);
auto I_crossA = uf::matrix::multiply(invIa, crossA);
angularTermA = uf::vector::dot(uf::vector::cross(I_crossA, rA), n);
}
if ( !b.isStatic ) {
pod::Vector3f crossB = uf::vector::cross(rB, n);
angularTermB = uf::vector::dot(uf::vector::cross(crossB * b.inverseInertiaTensor, rB), n);
auto invIb = ::computeWorldInverseInertia(b);
auto crossB = uf::vector::cross(rB, n);
auto I_crossB = uf::matrix::multiply(invIb, crossB);
angularTermB = uf::vector::dot(uf::vector::cross(I_crossB, rB), n);
}
float result = inverseMass + angularTermA + angularTermB;
@ -25,13 +29,13 @@ namespace {
if ( !a.isStatic ) {
a.velocity -= impulse * a.inverseMass;
//a.angularVelocity -= (uf::vector::cross(rA, impulse)) * a.inverseInertiaTensor;
pod::Matrix3f invIa = computeWorldInverseInertia( a );
pod::Matrix3f invIa = ::computeWorldInverseInertia( a );
a.angularVelocity -= uf::matrix::multiply( invIa, uf::vector::cross(rA, impulse) );
}
if ( !b.isStatic ) {
b.velocity += impulse * b.inverseMass;
//b.angularVelocity += (uf::vector::cross(rB, impulse)) * b.inverseInertiaTensor;
pod::Matrix3f invIb = computeWorldInverseInertia( b );
pod::Matrix3f invIb = ::computeWorldInverseInertia( b );
b.angularVelocity += uf::matrix::multiply( invIb, uf::vector::cross(rB, impulse) );
}
}
@ -43,8 +47,8 @@ namespace {
float angularSpeed2 = uf::vector::magnitude( body.angularVelocity );
if ( angularSpeed2 < EPS2 ) return;
body.angularVelocity += body.angularVelocity * body.mass * -rollingFriction * dt;
// body.angularVelocity *= -rollingFriction * dt;
//body.angularVelocity += body.angularVelocity * body.mass * -rollingFriction * dt;
body.angularVelocity *= std::max(0.0f, 1.0f - rollingFriction * dt);
}
void bindManifold( pod::PhysicsBody& a, pod::PhysicsBody& b, pod::Manifold& manifold, float dt = 0 ) {
@ -60,11 +64,9 @@ namespace {
pod::Simplex simplex;
if ( !::gjk(a,b,simplex) ) return false;
auto result = ::epa( a, b, simplex );
manifold.points.clear();
manifold.points.emplace_back(result);
if ( !::generateClippingManifold( a, b, result, manifold ) ) return false;
return true;
}
@ -125,6 +127,21 @@ namespace {
return false;
}
void computeLocalContacts( pod::Manifold& manifold ) {
if ( manifold.points.empty() ) return;
auto& a = *manifold.a;
auto& b = *manifold.b;
auto tA = ::getTransform( a );
auto tB = ::getTransform( b );
for ( auto& c : manifold.points ) {
c.localA = uf::transform::applyInverse( tA, c.point - c.normal * (c.penetration * 0.5f) );
c.localB = uf::transform::applyInverse( tB, c.point + c.normal * (c.penetration * 0.5f) );
}
}
bool similarContact( const pod::Contact& a, const pod::Contact& b, float distSq = 1.0e-2f, float norm = 0.9f ) {
return uf::vector::distanceSquared(a.point, b.point) < distSq && uf::vector::dot(a.normal, b.normal) > norm;
}
@ -185,16 +202,54 @@ namespace {
manifold.points = result;
}
void retrieveContacts( pod::Manifold& current, const pod::Manifold& previous, float decay = 0.35f ) {
for ( auto& c : current.points ) {
for ( auto& p : previous.points ) {
if ( !::similarContact( c, p ) ) continue;
c.accumulatedNormalImpulse = p.accumulatedNormalImpulse * decay;
c.accumulatedTangentImpulse = p.accumulatedTangentImpulse * decay;
c.lifetime = p.lifetime + 1;
break;
void retrieveContacts( pod::Manifold& current, const pod::Manifold& previous, float distanceThreshold = 0.1f, float separationThreshold = 0.1f, float decay = 0.85f ) {
auto& a = *current.a;
auto& b = *current.b;
auto tA = ::getTransform( a );
auto tB = ::getTransform( b );
uf::stl::vector<pod::Contact> merged = current.points;
float distSqThresh = distanceThreshold * distanceThreshold;
for ( const auto& oldContact : previous.points ) {
// reproject point according to current transform
auto worldA = uf::transform::apply( tA, oldContact.localA );
auto worldB = uf::transform::apply( tB, oldContact.localB );
auto delta = worldB - worldA;
auto normal = current.points.empty() ? oldContact.normal : current.points[0].normal;
float penetration = -uf::vector::dot( delta, normal );
if ( penetration < -separationThreshold ) continue;
pod::Vector3f projectedDelta = delta + normal * penetration;
float tangentialDriftSq = uf::vector::dot( projectedDelta, projectedDelta );
if ( tangentialDriftSq > distSqThresh ) continue;
pod::Contact validContact = oldContact;
validContact.point = (worldA + worldB) * 0.5f;
validContact.normal = normal;
validContact.penetration = penetration;
++validContact.lifetime;
validContact.accumulatedNormalImpulse *= decay;
validContact.accumulatedTangentImpulse *= decay;
bool isDuplicate = false;
for ( auto& c : current.points ) {
if ( ::similarContact( validContact, c ) ) {
c.accumulatedNormalImpulse = validContact.accumulatedNormalImpulse;
c.accumulatedTangentImpulse = validContact.accumulatedTangentImpulse;
c.lifetime = validContact.lifetime;
isDuplicate = true;
break;
}
}
if ( !isDuplicate ) merged.emplace_back( validContact );
}
current.points = merged;
}
void prepareManifoldCache( uf::stl::unordered_map<size_t, pod::Manifold>& cache, const uf::stl::vector<pod::Island>& islands, const uf::stl::vector<pod::PhysicsBody*>& bodies ) {

View File

@ -69,8 +69,6 @@ namespace {
::applyImpulseTo(a, b, rA, rB, tangent * jt);
}
// ::positionCorrection(a, b, contact);
}
template<size_t N, typename T = float>
@ -121,18 +119,6 @@ namespace {
K(i,i) += 1e-3f;
}
#if 0
pod::Vector3f relVelLinear = b.velocity - a.velocity;
for ( auto i = 0; i < N; i++ ) {
float vRel = uf::vector::dot( relVelLinear, manifold.points[i].normal );
float penetrationBias = std::max( manifold.points[i].penetration - uf::physics::impl::settings.baumgarteCorrectionSlop, 0.0f ) * ( uf::physics::impl::settings.baumgarteCorrectionPercent / dt );
float cDot = vRel + penetrationBias;
rhs[i] = (cDot < 0.0f) ? -cDot : 0.0f;
lambda[i] = manifold.points[i].accumulatedNormalImpulse; // warmup
}
#endif
for ( auto i = 0; i < N; i++ ) {
auto& contact = manifold.points[i];
// full relative velocity, linear + angular
@ -140,7 +126,6 @@ namespace {
pod::Vector3f vB = b.velocity + uf::vector::cross( b.angularVelocity, contact.point - pB );
float vRel = uf::vector::dot((vB - vA), contact.normal);
/*
// penetration bias with clamp
float penetrationBias = std::max(contact.penetration - uf::physics::impl::settings.baumgarteCorrectionSlop, 0.0f) * (uf::physics::impl::settings.baumgarteCorrectionPercent / dt);
penetrationBias = std::min(penetrationBias, 2.0f / dt); // clamp
@ -148,11 +133,7 @@ namespace {
float maxPenetrationRecovery = 2.0f; // limit to 2 units per second
if ( penetrationBias > maxPenetrationRecovery ) penetrationBias = maxPenetrationRecovery;
float cDot = vRel + penetrationBias;
rhs[i] = (cDot < 0.0f) ? -cDot : 0.0f; // RHS is magnitude of correction needed
lambda[i] = contact.accumulatedNormalImpulse;
*/
rhs[i] = (vRel < 0.0f) ? -vRel : 0.0f;
rhs[i] = -vRel + penetrationBias; // RHS is magnitude of correction needed
lambda[i] = contact.accumulatedNormalImpulse;
}
@ -172,7 +153,6 @@ namespace {
pod::Vector3f rB = manifold.points[i].point - pB;
::applyImpulseTo( a, b, rA, rB, manifold.points[i].normal * dLambda[i] );
// ::positionCorrection( a, b, manifold.points[i] );
}
}
@ -218,7 +198,8 @@ namespace {
// restitution bias + baumgarte
float e = std::min( a.material.restitution, b.material.restitution );
float penetrationBias = std::max( c.penetration - uf::physics::impl::settings.baumgarteCorrectionSlop, 0.0f ) * (uf::physics::impl::settings.baumgarteCorrectionPercent / dt);
cc.bias = 0; // (vn < -1.0f ? -e * vn : 0.0f) + penetrationBias;
float restitutionBias = (vn < -1.0f) ? -e * vn : 0.0f;
cc.bias = restitutionBias + penetrationBias;
// effective mass (normal)
pod::Vector3f rnA = uf::vector::cross( cc.rA, cc.normal );
@ -256,7 +237,7 @@ namespace {
// normal constraint
float vn = uf::vector::dot( dv, cc.normal );
float lambdaN = cc.effectiveMassN * (-(vn + cc.bias));
float lambdaN = cc.effectiveMassN * (-vn + cc.bias);
float oldImpulseN = cc.accumulatedNormalImpulse;
cc.accumulatedNormalImpulse = std::max( oldImpulseN + lambdaN, 0.0f );
float dPn = cc.accumulatedNormalImpulse - oldImpulseN;

View File

@ -91,6 +91,12 @@ namespace {
});
return tri;
}
// for clean code, this would be preferable
// but this incurs two lookups every triangle fetch, and I doubt the optimizer will optimize that away, so explicitly passing attribute views is preferable
FORCE_INLINE pod::Triangle fetchTriangle( const uf::Mesh::View& view, size_t triID ) {
return ::fetchTriangle( view, view["index"], view["position"], triID );
}
pod::TriangleWithNormal fetchTriangle( const uf::Mesh& mesh, size_t triID ) {
const auto& views = mesh.buffer_views;
@ -109,33 +115,24 @@ namespace {
triBase += trisInView;
}
UF_ASSERT( view );
auto& positions = (*view)["position"];
auto& indices = (*view)["index"];
pod::TriangleWithNormal tri = { ::fetchTriangle( *view, indices, positions, triID ) };
pod::TriangleWithNormal tri = { ::fetchTriangle( *view, triID ) };
tri.normal = uf::vector::normalize(uf::vector::cross(tri.points[1] - tri.points[0], tri.points[2] - tri.points[0]));
return tri;
}
// if body is a mesh, apply its transform to the triangles, else reorient the normal with respect to the body
pod::TriangleWithNormal fetchTriangle( const uf::Mesh& mesh, size_t triID, const pod::PhysicsBody& body, bool fast ) {
pod::TriangleWithNormal fetchTriangle( const uf::Mesh& mesh, size_t triID, const pod::PhysicsBody& body ) {
auto tri = ::fetchTriangle( mesh, triID );
auto transform = ::getTransform( body );
if ( body.collider.type == pod::ShapeType::MESH ) {
if ( fast ) {
FOR_EACH(3, {
tri.points[i] += transform.position;
});
} else {
FOR_EACH(3, {
tri.points[i] = uf::transform::apply( transform, tri.points[i] );
});
tri.normal = uf::quaternion::rotate( transform.orientation, tri.normal );
}
FOR_EACH(3, {
tri.points[i] = uf::transform::apply( transform, tri.points[i] );
});
tri.normal = uf::quaternion::rotate( transform.orientation, tri.normal );
}
else {
#if REORIENT_NORMALS_ON_FETCH
@ -237,8 +234,6 @@ namespace {
// triangle colliders
namespace {
// i feel like it'd just be better to treat an AABB as a 12-triangle mesh and do a triangleTriangle collision instead of 3 + 1 + 3 * 3 axis tests
// but what do i know
bool triangleAabbSAT( const pod::TriangleWithNormal& tri, const pod::PhysicsBody& body, pod::Manifold& manifold, float eps = 1e-6f ) {
const float eps2 = eps * eps;
@ -303,16 +298,6 @@ namespace {
float planeDist = uf::vector::dot(triNormal, v0);
if ( uf::vector::dot(bestAxis, triNormal) < 0.0f ) bestAxis = -bestAxis;
pod::Vector3f contact = boxCenter - bestAxis * (boxHalf.x * fabs(bestAxis.x) + boxHalf.y * fabs(bestAxis.y) + boxHalf.z * fabs(bestAxis.z));
//pod::Vector3f contact = boxCenter - triNormal * planeDist;
/*
float d = uf::vector::dot(triNormal, v0);
float dist = uf::vector::dot(triNormal, -boxCenter) - d;
pod::Vector3f contact = boxCenter - triNormal * dist;
if ( uf::vector::dot(bestAxis, triNormal) < 0.0f ) bestAxis = -bestAxis;
*/
manifold.points.emplace_back( pod::Contact{ contact, bestAxis, minOverlap } );
return true;
@ -387,12 +372,6 @@ namespace {
};
if ( uf::vector::dot(bestAxis, ::triangleCenter(b) - ::triangleCenter(a)) < 0.0f ) bestAxis = -bestAxis;
/*
pod::Vector3f centroid{0,0,0};
for ( auto i = 0; i < polyCount; i++ ) centroid += poly[i];
centroid /= (float) polyCount;
if ( uf::vector::dot(bestAxis, centroid - ::triangleCenter(a)) < 0.0f ) bestAxis = -bestAxis;
*/
for ( auto i = 0; i < 3; i++ ) {
auto p0 = a.points[i];
@ -412,37 +391,8 @@ namespace {
return ( polyCount > 0 );
}
bool triangleAabbTri( const pod::TriangleWithNormal& tri, const pod::PhysicsBody& body, pod::Manifold& manifold, float eps ) {
// 8 corners
pod::Vector3f v[8] = {
{body.bounds.min.x, body.bounds.min.y, body.bounds.min.z},
{body.bounds.max.x, body.bounds.min.y, body.bounds.min.z},
{body.bounds.max.x, body.bounds.max.y, body.bounds.min.z},
{body.bounds.min.x, body.bounds.max.y, body.bounds.min.z},
{body.bounds.min.x, body.bounds.min.y, body.bounds.max.z},
{body.bounds.max.x, body.bounds.min.y, body.bounds.max.z},
{body.bounds.max.x, body.bounds.max.y, body.bounds.max.z},
{body.bounds.min.x, body.bounds.max.y, body.bounds.max.z}
};
pod::TriangleWithNormal tris[12] = {
{ {v[0], v[4], v[7]}, {-1,0,0} }, { {v[0], v[7], v[3]}, {-1,0,0} }, // left (x-)
{ {v[1], v[5], v[6]}, {1,0,0} }, { {v[1], v[6], v[2]}, {1,0,0} }, // right (x+)
{ {v[0], v[1], v[5]}, {0,-1,0} }, { {v[0], v[5], v[4]}, {0,-1,0} }, // bottom (y-)
{ {v[3], v[2], v[6]}, {0,1,0} }, { {v[3], v[6], v[7]}, {0,1,0} }, // top (y+)
{ {v[0], v[1], v[2]}, {0,0,-1} }, { {v[0], v[2], v[3]}, {0,0,-1} }, // back (z-)
{ {v[4], v[5], v[6]}, {0,0,1} }, { {v[4], v[6], v[7]}, {0,0,1} }, // front (z+)
};
bool hit = false;
for ( auto& t : tris ) {
if ( ::triangleTriangle( tri, t, manifold, eps ) ) hit = true;
}
return hit;
}
bool triangleAabb( const pod::TriangleWithNormal& tri, const pod::PhysicsBody& body, pod::Manifold& manifold, float eps ) {
return ::triangleAabbSAT( tri, body, manifold, eps );
//return ::triangleAabbTri( tri, body, manifold, eps );
}
bool triangleSphere( const pod::TriangleWithNormal& tri, const pod::PhysicsBody& body, pod::Manifold& manifold, float eps ) {
const auto& sphere = body;
@ -459,9 +409,11 @@ namespace {
if ( dist2 > r * r ) return false;
float dist = std::sqrt(dist2);
auto triNormal = ::triangleNormal( tri );
auto contact = ( center + closest ) * 0.5f;
auto normal = ( dist > eps ) ? ( delta / dist ) : ::triangleNormal( tri );
auto normal = ( dist > eps ) ? ( delta / dist ) : triNormal;
float penetration = r - dist;
if ( uf::vector::dot( normal, triNormal ) > 0.707f ) normal = triNormal;
#if REORIENT_NORMALS_ON_CONTACT
if ( uf::vector::dot( normal, delta ) < 0.0f ) normal = -normal;
@ -534,9 +486,11 @@ namespace {
auto delta = ( closestSeg - closest );
// to-do: properly derive the contact information
auto triNormal = ::triangleNormal( tri );
auto contact = closest; // ( closestSeg + closest ) * 0.5f;
auto normal = ( dist > eps ) ? ( delta / dist ) : ::triangleNormal( tri );
auto normal = ( dist > eps ) ? ( delta / dist ) : triNormal;
float penetration = r - dist;
if ( uf::vector::dot( normal, triNormal ) > 0.707f ) normal = triNormal;
#if REORIENT_NORMALS_ON_CONTACT
if ( uf::vector::dot( normal, delta ) < 0.0f ) normal = -normal;
@ -573,12 +527,8 @@ namespace {
pod::Simplex simplex;
if ( !::gjk( tri, hull, simplex ) ) return false;
auto result = ::epa( tri, hull, simplex );
if ( !uf::vector::isValid(result.point) ) return false;
manifold.points.emplace_back( result );
if ( !::generateClippingManifold( tri, hull, result, manifold ) ) return false;
return true;
}