diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b139253f8..661a058d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,14 @@ Bug #832: OpenMW-CS: Handle deleted references Bug #1662: Qt4 and Windows binaries crash if there's a non-ASCII character in a file path/config path + Bug #1901: Actors colliding behaviour is different from vanilla Bug #1952: Incorrect particle lighting Bug #2069: Fireflies in Fireflies invade Morrowind look wrong Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs Bug #2473: Unable to overstock merchants Bug #2798: Mutable ESM records Bug #2976 [reopened]: Issues combining settings from the command line and both config files + Bug #3137: Walking into a wall prevents jumping Bug #3372: Projectiles and magic bolts go through moving targets Bug #3676: NiParticleColorModifier isn't applied properly Bug #3714: Savegame fails to load due to conflict between SpellState and MagicEffects @@ -18,6 +20,11 @@ Bug #4021: Attributes and skills are not stored as floats Bug #4055: Local scripts don't inherit variables from their base record Bug #4083: Door animation freezes when colliding with actors + Bug #4247: Cannot walk up stairs in Ebonheart docks + Bug #4447: Actor collision capsule shape allows looking through some walls + Bug #4465: Collision shape overlapping causes twitching + Bug #4476: Abot Gondoliers: player hangs in air during scenic travel + Bug #4568: Too many actors in one spot can push other actors out of bounds Bug #4623: Corprus implementation is incorrect Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level Bug #4764: Data race in osg ParticleSystem @@ -71,12 +78,15 @@ Bug #5656: Sneaking characters block hits while standing Bug #5661: Region sounds don't play at the right interval Bug #5675: OpenMW-cs. FRMR subrecords are saved with the wrong MastIdx + Bug #5681: Player character can clip or pass through bridges instead of colliding against them + Bug #5683: Player character can get stuck with MR's balmora's wooden gate Bug #5688: Water shader broken indoors with enable indoor shadows = false Bug #5695: ExplodeSpell for actors doesn't target the ground Bug #5703: OpenMW-CS menu system crashing on XFCE Bug #5706: AI sequences stop looping after the saved game is reloaded Bug #5731: Editor: skirts are invisible on characters Bug #5758: Paralyzed actors behavior is inconsistent with vanilla + Bug #5762: Movement solver is insufficiently robust Feature #390: 3rd person look "over the shoulder" Feature #1536: Show more information about level on menu Feature #2386: Distant Statics in the form of Object Paging diff --git a/CHANGELOG_PR.md b/CHANGELOG_PR.md index 8a7948bb70..69ad0cc1b9 100644 --- a/CHANGELOG_PR.md +++ b/CHANGELOG_PR.md @@ -41,4 +41,4 @@ Editor Bug Fixes: Miscellaneous: - Prevent save-game bloating by using an appropriate fog texture format (#5108) -- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363) \ No newline at end of file +- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363) diff --git a/CI/before_install.android.sh b/CI/before_install.android.sh index 791377dd96..0243a96092 100755 --- a/CI/before_install.android.sh +++ b/CI/before_install.android.sh @@ -1,4 +1,4 @@ #!/bin/sh -ex -curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201018.zip -o ~/openmw-android-deps.zip +curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201129.zip -o ~/openmw-android-deps.zip unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 0ef67f47ef..0c26801cc4 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -73,7 +73,7 @@ CONFIGURATIONS=() TEST_FRAMEWORK="" GOOGLE_INSTALL_ROOT="" INSTALL_PREFIX="." -BULLET_DOUBLE="" +BULLET_DOUBLE=true BULLET_DBL="" BULLET_DBL_DISPLAY="Single precision" diff --git a/CMakeLists.txt b/CMakeLists.txt index be7bc79e3f..4a615e8432 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.1.0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Detect OS +include(cmake/OSIdentity.cmake) + # for link time optimization, remove if cmake version is >= 3.9 if(POLICY CMP0069) cmake_policy(SET CMP0069 NEW) @@ -21,7 +24,7 @@ option(BUILD_NIFTEST "Build nif file tester" ON) option(BUILD_DOCS "Build documentation." OFF ) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) -option(BULLET_USE_DOUBLES "Use double precision for Bullet" OFF) +option(BULLET_USE_DOUBLES "Use double precision for Bullet" ON) set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up. diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 6b6134dfb9..a4c3b91361 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -73,7 +73,7 @@ add_openmw_dir (mwworld add_openmw_dir (mwphysics physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile - closestnotmeconvexresultcallback raycasting mtphysics + closestnotmeconvexresultcallback raycasting mtphysics contacttestwrapper ) add_openmw_dir (mwclass diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index 015424db58..3b7b0d0616 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -1,6 +1,5 @@ #include "actor.hpp" -#include #include #include @@ -14,6 +13,8 @@ #include "collisiontype.hpp" #include "mtphysics.hpp" +#include + namespace MWPhysics { @@ -52,17 +53,8 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\"."; } - // Use capsule shape only if base is square (nonuniform scaling apparently doesn't work on it) - if (std::abs(mHalfExtents.x()-mHalfExtents.y())= mHalfExtents.x()) - { - mShape.reset(new btCapsuleShapeZ(mHalfExtents.x(), 2*mHalfExtents.z() - 2*mHalfExtents.x())); - mRotationallyInvariant = true; - } - else - { - mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents))); - mRotationallyInvariant = false; - } + mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents))); + mRotationallyInvariant = (mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) && std::fabs(mHalfExtents.x() - mHalfExtents.y()) < 2.2; mConvexShape = static_cast(mShape.get()); @@ -72,8 +64,11 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setUserPointer(this); - updateRotation(); updateScale(); + + if(!mRotationallyInvariant) + updateRotation(); + updatePosition(); addCollisionMask(getCollisionMask()); updateCollisionObjectPosition(); @@ -154,6 +149,11 @@ osg::Vec3f Actor::getSimulationPosition() const return mSimulationPosition; } +osg::Vec3f Actor::getScaledMeshTranslation() const +{ + return mRotation * osg::componentMultiply(mMeshTranslation, mScale); +} + void Actor::updateCollisionObjectPosition() { std::scoped_lock lock(mPositionMutex); diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index 18419c2c80..9d129f2ba6 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -82,6 +82,9 @@ namespace MWPhysics */ osg::Vec3f getOriginalHalfExtents() const; + /// Returns the mesh translation, scaled and rotated as necessary + osg::Vec3f getScaledMeshTranslation() const; + /** * Returns the position of the collision body * @note The collision shape's origin is in its center, so the position returned can be described as center of the actor collision box in world space. diff --git a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp index 18c196e444..8dd5a624fc 100644 --- a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp +++ b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.cpp @@ -1,15 +1,39 @@ +#include + #include "closestnotmeconvexresultcallback.hpp" +#include "collisiontype.hpp" +#include "contacttestwrapper.h" #include +#include #include "collisiontype.hpp" #include "projectile.hpp" namespace MWPhysics { - ClosestNotMeConvexResultCallback::ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot) + class ActorOverlapTester : public btCollisionWorld::ContactResultCallback + { + public: + bool overlapping = false; + + btScalar addSingleResult(btManifoldPoint& cp, + const btCollisionObjectWrapper* colObj0Wrap, + int partId0, + int index0, + const btCollisionObjectWrapper* colObj1Wrap, + int partId1, + int index1) + { + if(cp.getDistance() <= 0.0f) + overlapping = true; + return btScalar(1); + } + }; + + ClosestNotMeConvexResultCallback::ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world) : btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)), - mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot) + mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot), mWorld(world) { } @@ -18,6 +42,39 @@ namespace MWPhysics if (convexResult.m_hitCollisionObject == mMe) return btScalar(1); + // override data for actor-actor collisions + // vanilla Morrowind seems to make overlapping actors collide as though they are both cylinders with a diameter of the distance between them + // For some reason this doesn't work as well as it should when using capsules, but it still helps a lot. + if(convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor) + { + ActorOverlapTester isOverlapping; + // FIXME: This is absolutely terrible and bullet should feel terrible for not making contactPairTest const-correct. + ContactTestWrapper::contactPairTest(const_cast(mWorld), const_cast(mMe), const_cast(convexResult.m_hitCollisionObject), isOverlapping); + + if(isOverlapping.overlapping) + { + auto originA = Misc::Convert::toOsg(mMe->getWorldTransform().getOrigin()); + auto originB = Misc::Convert::toOsg(convexResult.m_hitCollisionObject->getWorldTransform().getOrigin()); + osg::Vec3f motion = Misc::Convert::toOsg(mMotion); + osg::Vec3f normal = (originA-originB); + normal.z() = 0; + normal.normalize(); + // only collide if horizontally moving towards the hit actor (note: the motion vector appears to be inverted) + // FIXME: This kinda screws with standing on actors that walk up slopes for some reason. Makes you fall through them. + // It happens in vanilla Morrowind too, but much less often. + // I tried hunting down why but couldn't figure it out. Possibly a stair stepping or ground ejection bug. + if(normal * motion > 0.0f) + { + convexResult.m_hitFraction = 0.0f; + convexResult.m_hitNormalLocal = Misc::Convert::toBullet(normal); + return ClosestConvexResultCallback::addSingleResult(convexResult, true); + } + else + { + return btScalar(1); + } + } + } if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile) { auto* projectileHolder = static_cast(convexResult.m_hitCollisionObject->getUserPointer()); diff --git a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp index 97aaa64a1c..538721ad80 100644 --- a/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp +++ b/apps/openmw/mwphysics/closestnotmeconvexresultcallback.hpp @@ -10,7 +10,7 @@ namespace MWPhysics class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback { public: - ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot); + ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world); btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) override; @@ -18,6 +18,7 @@ namespace MWPhysics const btCollisionObject *mMe; const btVector3 mMotion; const btScalar mMinCollisionDot; + const btCollisionWorld * mWorld; }; } diff --git a/apps/openmw/mwphysics/constants.hpp b/apps/openmw/mwphysics/constants.hpp index 46367ab343..c6f2f3b530 100644 --- a/apps/openmw/mwphysics/constants.hpp +++ b/apps/openmw/mwphysics/constants.hpp @@ -5,12 +5,22 @@ namespace MWPhysics { static const float sStepSizeUp = 34.0f; static const float sStepSizeDown = 62.0f; - static const float sMinStep = 10.f; + + static const float sMinStep = 10.0f; // hack to skip over tiny unwalkable slopes + static const float sMinStep2 = 20.0f; // hack to skip over shorter but longer/wider/further unwalkable slopes + // whether to do the above stairstepping logic hacks to work around bad morrowind assets - disabling causes problems but improves performance + static const bool sDoExtraStairHacks = true; + static const float sGroundOffset = 1.0f; static const float sMaxSlope = 49.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static const int sMaxIterations = 8; + // Allows for more precise movement solving without getting stuck or snagging too easily. + static const float sCollisionMargin = 0.1; + // Allow for a small amount of penetration to prevent numerical precision issues from causing the "unstuck"ing code to run unnecessarily + // Currently set to 0 because having the "unstuck"ing code run whenever possible prevents some glitchy snagging issues + static const float sAllowedPenetration = 0.0; } #endif diff --git a/apps/openmw/mwphysics/contacttestwrapper.cpp b/apps/openmw/mwphysics/contacttestwrapper.cpp new file mode 100644 index 0000000000..c11a7e2926 --- /dev/null +++ b/apps/openmw/mwphysics/contacttestwrapper.cpp @@ -0,0 +1,21 @@ +#include + +#include "contacttestwrapper.h" + +namespace MWPhysics +{ + // Concurrent calls to contactPairTest (and by extension contactTest) are forbidden. + static std::mutex contactMutex; + void ContactTestWrapper::contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) + { + std::unique_lock lock(contactMutex); + collisionWorld->contactTest(colObj, resultCallback); + } + + void ContactTestWrapper::contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback) + { + std::unique_lock lock(contactMutex); + collisionWorld->contactPairTest(colObjA, colObjB, resultCallback); + } + +} diff --git a/apps/openmw/mwphysics/contacttestwrapper.h b/apps/openmw/mwphysics/contacttestwrapper.h new file mode 100644 index 0000000000..b3b6edc59a --- /dev/null +++ b/apps/openmw/mwphysics/contacttestwrapper.h @@ -0,0 +1,14 @@ +#ifndef OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H +#define OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H + +#include + +namespace MWPhysics +{ + struct ContactTestWrapper + { + static void contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback); + static void contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback); + }; +} +#endif diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index 1feaf3c94e..69bf042369 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -17,10 +17,13 @@ #include "actor.hpp" #include "collisiontype.hpp" #include "constants.hpp" +#include "contacttestwrapper.h" #include "physicssystem.hpp" #include "stepper.hpp" #include "trace.h" +#include + namespace MWPhysics { static bool isActor(const btCollisionObject *obj) @@ -29,12 +32,50 @@ namespace MWPhysics return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor; } - template - static bool isWalkableSlope(const Vec3 &normal) + class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback { - static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); - return (normal.z() > sMaxSlopeCos); - } + public: + ContactCollectionCallback(const btCollisionObject * me, osg::Vec3f velocity) : mMe(me) + { + m_collisionFilterGroup = me->getBroadphaseHandle()->m_collisionFilterGroup; + m_collisionFilterMask = me->getBroadphaseHandle()->m_collisionFilterMask; + mVelocity = Misc::Convert::toBullet(velocity); + } + virtual btScalar addSingleResult(btManifoldPoint & contact, const btCollisionObjectWrapper * colObj0Wrap, int partId0, int index0, const btCollisionObjectWrapper * colObj1Wrap, int partId1, int index1) + { + if (isActor(colObj0Wrap->getCollisionObject()) && isActor(colObj1Wrap->getCollisionObject())) + return 0.0; + // ignore overlap if we're moving in the same direction as it would push us out (don't change this to >=, that would break detection when not moving) + if (contact.m_normalWorldOnB.dot(mVelocity) > 0.0) + return 0.0; + auto delta = contact.m_normalWorldOnB * -contact.m_distance1; + mContactSum += delta; + mMaxX = std::max(std::abs(delta.x()), mMaxX); + mMaxY = std::max(std::abs(delta.y()), mMaxY); + mMaxZ = std::max(std::abs(delta.z()), mMaxZ); + if (contact.m_distance1 < mDistance) + { + mDistance = contact.m_distance1; + mNormal = contact.m_normalWorldOnB; + mDelta = delta; + return mDistance; + } + else + { + return 0.0; + } + } + btScalar mMaxX = 0.0; + btScalar mMaxY = 0.0; + btScalar mMaxZ = 0.0; + btVector3 mContactSum{0.0, 0.0, 0.0}; + btVector3 mNormal{0.0, 0.0, 0.0}; // points towards "me" + btVector3 mDelta{0.0, 0.0, 0.0}; // points towards "me" + btScalar mDistance = 0.0; // negative or zero + protected: + btVector3 mVelocity; + const btCollisionObject * mMe; + }; osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight) { @@ -99,13 +140,13 @@ namespace MWPhysics } const btCollisionObject *colobj = physicActor->getCollisionObject(); - osg::Vec3f halfExtents = physicActor->getHalfExtents(); - // NOTE: here we don't account for the collision box translation (i.e. physicActor->getPosition() - refpos.pos). - // That means the collision shape used for moving this actor is in a different spot than the collision shape - // other actors are using to collide against this actor. - // While this is strictly speaking wrong, it's needed for MW compatibility. - actor.mPosition.z() += halfExtents.z(); + // Adjust for collision mesh offset relative to actor's "location" + // (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our own) + // for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of from internal hull translation + // if not for this hack, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() + osg::Vec3f halfExtents = physicActor->getHalfExtents(); + actor.mPosition.z() += halfExtents.z(); // vanilla-accurate static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat(); float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); @@ -156,6 +197,13 @@ namespace MWPhysics * The initial velocity was set earlier (see above). */ float remainingTime = time; + bool seenGround = physicActor->getOnGround() && !physicActor->getOnSlope() && !actor.mFlying; + + int numTimesSlid = 0; + osg::Vec3f lastSlideNormal(0,0,1); + osg::Vec3f lastSlideNormalFallback(0,0,1); + bool forceGroundTest = false; + for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.01f; ++iterations) { osg::Vec3f nextpos = newPosition + velocity * remainingTime; @@ -164,7 +212,7 @@ namespace MWPhysics if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel) { const osg::Vec3f down(0,0,-1); - velocity = slide(velocity, down); + velocity = reject(velocity, down); // NOTE: remainingTime is unchanged before the loop continues continue; // velocity updated, calculate nextpos again } @@ -193,92 +241,158 @@ namespace MWPhysics break; } - // We are touching something. - if (tracer.mFraction < 1E-9f) - { - // Try to separate by backing off slighly to unstuck the solver - osg::Vec3f backOff = (newPosition - tracer.mHitPoint) * 1E-2f; - newPosition += backOff; - } + if (isWalkableSlope(tracer.mPlaneNormal) && !actor.mFlying && newPosition.z() >= swimlevel) + seenGround = true; // We hit something. Check if we can step up. float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z(); osg::Vec3f oldPosition = newPosition; - bool result = false; + bool usedStepLogic = false; if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) { // Try to step up onto it. - // NOTE: stepMove does not allow stepping over, modifies newPosition if successful - result = stepper.step(newPosition, velocity*remainingTime, remainingTime); + // NOTE: this modifies newPosition and velocity on its own if successful + usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0); } - if (result) + if (usedStepLogic) { // don't let pure water creatures move out of water after stepMove const auto ptr = physicActor->getPtr(); if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel) newPosition = oldPosition; + else if(!actor.mFlying && actor.mPosition.z() >= swimlevel) + forceGroundTest = true; } else { - // Can't move this way, try to find another spot along the plane - osg::Vec3f newVelocity = slide(velocity, tracer.mPlaneNormal); + // Can't step up, so slide against what we ran into + remainingTime *= (1.0f-tracer.mFraction); + + auto planeNormal = tracer.mPlaneNormal; + + // If we touched the ground this frame, and whatever we ran into is a wall of some sort, + // pretend that its collision normal is pointing horizontally + // (fixes snagging on slightly downward-facing walls, and crawling up the bases of very steep walls because of the collision margin) + if (seenGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) + { + planeNormal.z() = 0; + planeNormal.normalize(); + } + + // Move up to what we ran into (with a bit of a collision margin) + if ((newPosition-tracer.mEndPos).length2() > sCollisionMargin*sCollisionMargin) + { + auto direction = velocity; + direction.normalize(); + newPosition = tracer.mEndPos; + newPosition -= direction*sCollisionMargin; + } + + osg::Vec3f newVelocity = (velocity * planeNormal <= 0.0) ? reject(velocity, planeNormal) : velocity; + bool usedSeamLogic = false; + + // check for the current and previous collision planes forming an acute angle; slide along the seam if they do + if(numTimesSlid > 0) + { + auto dotA = lastSlideNormal * planeNormal; + auto dotB = lastSlideNormalFallback * planeNormal; + if(numTimesSlid <= 1) // ignore fallback normal if this is only the first or second slide + dotB = 1.0; + if(dotA <= 0.0 || dotB <= 0.0) + { + osg::Vec3f bestNormal = lastSlideNormal; + // use previous-to-previous collision plane if it's acute with current plane but actual previous plane isn't + if(dotB < dotA) + { + bestNormal = lastSlideNormalFallback; + lastSlideNormal = lastSlideNormalFallback; + } + + auto constraintVector = bestNormal ^ planeNormal; // cross product + if(constraintVector.length2() > 0) // only if it's not zero length + { + constraintVector.normalize(); + newVelocity = project(velocity, constraintVector); + + // version of surface rejection for acute crevices/seams + auto averageNormal = bestNormal + planeNormal; + averageNormal.normalize(); + tracer.doTrace(colobj, newPosition, newPosition + averageNormal*(sCollisionMargin*2.0), collisionWorld); + newPosition = (newPosition + tracer.mEndPos)/2.0; + + usedSeamLogic = true; + } + } + } + // otherwise just keep the normal vector rejection + + // if this isn't the first iteration, or if the first iteration is also the last iteration, + // move away from the collision plane slightly, if possible + // this reduces getting stuck in some concave geometry, like the gaps above the railings in some ald'ruhn buildings + // this is different from the normal collision margin, because the normal collision margin is along the movement path, + // but this is along the collision normal + if(!usedSeamLogic && (iterations > 0 || remainingTime < 0.01f)) + { + tracer.doTrace(colobj, newPosition, newPosition + planeNormal*(sCollisionMargin*2.0), collisionWorld); + newPosition = (newPosition + tracer.mEndPos)/2.0; + } - // Do not allow sliding upward if there is gravity. - // Stepping will have taken care of that. - if(!(newPosition.z() < swimlevel || actor.mFlying)) - newVelocity.z() = std::min(newVelocity.z(), 0.0f); + // Do not allow sliding up steep slopes if there is gravity. + if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal)) + newVelocity.z() = std::min(newVelocity.z(), velocity.z()); - if ((newVelocity-velocity).length2() < 0.01) + if (newVelocity * origVelocity <= 0.0f) break; - if ((newVelocity * origVelocity) <= 0.f) - break; // ^ dot product + numTimesSlid += 1; + lastSlideNormalFallback = lastSlideNormal; + lastSlideNormal = planeNormal; velocity = newVelocity; } } bool isOnGround = false; bool isOnSlope = false; - if (!(inertia.z() > 0.f) && !(newPosition.z() < swimlevel)) + if (forceGroundTest || (inertia.z() <= 0.f && newPosition.z() >= swimlevel)) { osg::Vec3f from = newPosition; - osg::Vec3f to = newPosition - (physicActor->getOnGround() ? osg::Vec3f(0,0,sStepSizeDown + 2*sGroundOffset) : osg::Vec3f(0,0,2*sGroundOffset)); + auto dropDistance = 2*sGroundOffset + (physicActor->getOnGround() ? sStepSizeDown : 0); + osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); tracer.doTrace(colobj, from, to, collisionWorld); - if(tracer.mFraction < 1.0f && !isActor(tracer.mHitObject)) + if(tracer.mFraction < 1.0f) { - const btCollisionObject* standingOn = tracer.mHitObject; - PtrHolder* ptrHolder = static_cast(standingOn->getUserPointer()); - if (ptrHolder) - actor.mStandingOn = ptrHolder->getPtr(); - - if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) - physicActor->setWalkingOnWater(true); - if (!actor.mFlying) - newPosition.z() = tracer.mEndPos.z() + sGroundOffset; + if (!isActor(tracer.mHitObject)) + { + isOnGround = true; + isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); - isOnGround = true; + const btCollisionObject* standingOn = tracer.mHitObject; + PtrHolder* ptrHolder = static_cast(standingOn->getUserPointer()); + if (ptrHolder) + actor.mStandingOn = ptrHolder->getPtr(); - isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); - } - else - { - // standing on actors is not allowed (see above). - // in addition to that, apply a sliding effect away from the center of the actor, - // so that we do not stay suspended in air indefinitely. - if (tracer.mFraction < 1.0f && isActor(tracer.mHitObject)) - { - if (osg::Vec3f(velocity.x(), velocity.y(), 0).length2() < 100.f*100.f) + if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) + physicActor->setWalkingOnWater(true); + if (!actor.mFlying && !isOnSlope) { - btVector3 aabbMin, aabbMax; - tracer.mHitObject->getCollisionShape()->getAabb(tracer.mHitObject->getWorldTransform(), aabbMin, aabbMax); - btVector3 center = (aabbMin + aabbMax) / 2.f; - inertia = osg::Vec3f(actor.mPosition.x() - center.x(), actor.mPosition.y() - center.y(), 0); - inertia.normalize(); - inertia *= 100; + if (tracer.mFraction*dropDistance > sGroundOffset) + newPosition.z() = tracer.mEndPos.z() + sGroundOffset; + else + { + newPosition.z() = tracer.mEndPos.z(); + tracer.doTrace(colobj, newPosition, newPosition + osg::Vec3f(0, 0, 2*sGroundOffset), collisionWorld); + newPosition = (newPosition+tracer.mEndPos)/2.0; + } } } + else + { + // Vanilla allows actors to float on top of other actors. Do not push them off. + if (!actor.mFlying && isWalkableSlope(tracer.mPlaneNormal) && tracer.mEndPos.z()+sGroundOffset <= newPosition.z()) + newPosition.z() = tracer.mEndPos.z() + sGroundOffset; - isOnGround = false; + isOnGround = false; + } } } @@ -298,7 +412,98 @@ namespace MWPhysics physicActor->setOnGround(isOnGround); physicActor->setOnSlope(isOnSlope); - newPosition.z() -= halfExtents.z(); // remove what was added at the beginning actor.mPosition = newPosition; + // remove what was added earlier in compensating for doTrace not taking interior transformation into account + actor.mPosition.z() -= halfExtents.z(); // vanilla-accurate + } + + btVector3 addMarginToDelta(btVector3 delta) + { + if(delta.length2() == 0.0) + return delta; + return delta + delta.normalized() * sCollisionMargin; + } + + void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld) + { + const auto& ptr = actor.mActorRaw->getPtr(); + if (!ptr.getClass().isMobile(ptr)) + return; + + auto* physicActor = actor.mActorRaw; + if(!physicActor->getCollisionMode()) // noclipping/tcl + return; + + auto* collisionObject = physicActor->getCollisionObject(); + auto tempPosition = actor.mPosition; + + // use vanilla-accurate collision hull position hack (do same hitbox offset hack as movement solver) + // if vanilla compatibility didn't matter, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() + const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, physicActor->getHalfExtents().z()); + + // use a 3d approximation of the movement vector to better judge player intent + const ESM::Position& refpos = ptr.getRefData().getPosition(); + auto velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + // try to pop outside of the world before doing anything else if we're inside of it + if (!physicActor->getOnGround() || physicActor->getOnSlope()) + velocity += physicActor->getInertialForce(); + + // because of the internal collision box offset hack, and the fact that we're moving the collision box manually, + // we need to replicate part of the collision box's transform process from scratch + osg::Vec3f refPosition = tempPosition + verticalHalfExtent; + osg::Vec3f goodPosition = refPosition; + const btTransform oldTransform = collisionObject->getWorldTransform(); + btTransform newTransform = oldTransform; + + auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback + { + goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset)); + newTransform.setOrigin(Misc::Convert::toBullet(goodPosition)); + collisionObject->setWorldTransform(newTransform); + + ContactCollectionCallback callback{collisionObject, velocity}; + ContactTestWrapper::contactTest(const_cast(collisionWorld), collisionObject, callback); + return callback; + }; + + // check whether we're inside the world with our collision box with manually-derived offset + auto contactCallback = gatherContacts({0.0, 0.0, 0.0}); + if(contactCallback.mDistance < -sAllowedPenetration) + { + // we are; try moving it out of the world + auto positionDelta = contactCallback.mContactSum; + // limit rejection delta to the largest known individual rejections + if(std::abs(positionDelta.x()) > contactCallback.mMaxX) + positionDelta *= contactCallback.mMaxX / std::abs(positionDelta.x()); + if(std::abs(positionDelta.y()) > contactCallback.mMaxY) + positionDelta *= contactCallback.mMaxY / std::abs(positionDelta.y()); + if(std::abs(positionDelta.z()) > contactCallback.mMaxZ) + positionDelta *= contactCallback.mMaxZ / std::abs(positionDelta.z()); + + auto contactCallback2 = gatherContacts(positionDelta); + // successfully moved further out from contact (does not have to be in open space, just less inside of things) + if(contactCallback2.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + // try again but only upwards (fixes some bad coc floors) + else + { + // upwards-only offset + auto contactCallback3 = gatherContacts({0.0, 0.0, std::abs(positionDelta.z())}); + // success + if(contactCallback3.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + else + // try again but fixed distance up + { + auto contactCallback4 = gatherContacts({0.0, 0.0, 10.0}); + // success + if(contactCallback4.mDistance > contactCallback.mDistance) + tempPosition = goodPosition - verticalHalfExtent; + } + } + } + + collisionObject->setWorldTransform(oldTransform); + actor.mPosition = tempPosition; } } diff --git a/apps/openmw/mwphysics/movementsolver.hpp b/apps/openmw/mwphysics/movementsolver.hpp index 75fba1cf0e..76141ec0e9 100644 --- a/apps/openmw/mwphysics/movementsolver.hpp +++ b/apps/openmw/mwphysics/movementsolver.hpp @@ -5,6 +5,9 @@ #include +#include "constants.hpp" +#include "../mwworld/ptr.hpp" + class btCollisionWorld; namespace MWWorld @@ -14,29 +17,35 @@ namespace MWWorld namespace MWPhysics { + /// Vector projection + static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v) + { + return v * (u * v); + } + + /// Vector rejection + static inline osg::Vec3f reject(const osg::Vec3f& direction, const osg::Vec3f &planeNormal) + { + return direction - project(direction, planeNormal); + } + + template + static bool isWalkableSlope(const Vec3 &normal) + { + static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); + return (normal.z() > sMaxSlopeCos); + } + class Actor; struct ActorFrameData; struct WorldFrameData; class MovementSolver { - private: - ///Project a vector u on another vector v - static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v) - { - return v * (u * v); - // ^ dot product - } - - ///Helper for computing the character sliding - static inline osg::Vec3f slide(const osg::Vec3f& direction, const osg::Vec3f &planeNormal) - { - return direction - project(direction, planeNormal); - } - public: static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData); + static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index f610ceaea3..0d1e5962a0 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -13,6 +13,7 @@ #include "../mwworld/player.hpp" #include "actor.hpp" +#include "contacttestwrapper.h" #include "movementsolver.hpp" #include "mtphysics.hpp" #include "object.hpp" @@ -167,7 +168,14 @@ namespace MWPhysics mPreStepBarrier = std::make_unique(mNumThreads, [&]() { + if (mDeferAabbUpdate) updateAabbs(); + for (auto& data : mActorsFrameData) + if (data.mActor.lock()) + { + std::unique_lock lock(mCollisionWorldMutex); + MovementSolver::unstuck(data, mCollisionWorld.get()); + } }); mPostStepBarrier = std::make_unique(mNumThreads, [&]() @@ -295,7 +303,7 @@ namespace MWPhysics void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { std::shared_lock lock(mCollisionWorldMutex); - mCollisionWorld->contactTest(colObj, resultCallback); + ContactTestWrapper::contactTest(mCollisionWorld.get(), colObj, resultCallback); } std::optional PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) @@ -438,8 +446,7 @@ namespace MWPhysics if (!mNewFrame) mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; }); - if (mDeferAabbUpdate) - mPreStepBarrier->wait(); + mPreStepBarrier->wait(); int job = 0; while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) @@ -505,7 +512,10 @@ namespace MWPhysics while (mRemainingSteps--) { for (auto& actorData : mActorsFrameData) + { + MovementSolver::unstuck(actorData, mCollisionWorld.get()); MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData); + } updateActorsPositions(); } diff --git a/apps/openmw/mwphysics/raycasting.hpp b/apps/openmw/mwphysics/raycasting.hpp index 8ca6965d8e..b176e83304 100644 --- a/apps/openmw/mwphysics/raycasting.hpp +++ b/apps/openmw/mwphysics/raycasting.hpp @@ -16,7 +16,7 @@ namespace MWPhysics osg::Vec3f mHitNormal; MWWorld::Ptr mHitObject; }; - + class RayCastingInterface { public: diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index 0ab383dd1e..2a28381bee 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -7,6 +7,7 @@ #include "collisiontype.hpp" #include "constants.hpp" +#include "movementsolver.hpp" namespace MWPhysics { @@ -24,125 +25,155 @@ namespace MWPhysics Stepper::Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj) : mColWorld(colWorld) , mColObj(colObj) - , mHaveMoved(true) { } - bool Stepper::step(osg::Vec3f &position, const osg::Vec3f &toMove, float &remainingTime) + bool Stepper::step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration) { - /* - * Slide up an incline or set of stairs. Should be called only after a - * collision detection otherwise unnecessary tracing will be performed. - * - * NOTE: with a small change this method can be used to step over an obstacle - * of height sStepSize. - * - * If successful return 'true' and update 'position' to the new possible - * location and adjust 'remainingTime'. - * - * If not successful return 'false'. May fail for these reasons: - * - can't move directly up from current position - * - having moved up by between epsilon() and sStepSize, can't move forward - * - having moved forward by between epsilon() and toMove, - * = moved down between 0 and just under sStepSize but slope was too steep, or - * = moved the full sStepSize down (FIXME: this could be a bug) - * - * Starting position. Obstacle or stairs with height upto sStepSize in front. - * - * +--+ +--+ |XX - * | | -------> toMove | | +--+XX - * | | | | |XXXXX - * | | +--+ | | +--+XXXXX - * | | |XX| | | |XXXXXXXX - * +--+ +--+ +--+ +-------- - * ============================================== - */ - - /* - * Try moving up sStepSize using stepper. - * FIXME: does not work in case there is no front obstacle but there is one above - * - * +--+ +--+ - * | | | | - * | | | | |XX - * | | | | +--+XX - * | | | | |XXXXX - * +--+ +--+ +--+ +--+XXXXX - * |XX| |XXXXXXXX - * +--+ +-------- - * ============================================== - */ - if (mHaveMoved) - { - mHaveMoved = false; + if(velocity.x() == 0.0 && velocity.y() == 0.0) + return false; + + // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the ground. + // This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry. - mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); - if (mUpStepper.mFraction < std::numeric_limits::epsilon()) - return false; // didn't even move the smallest representable amount - // (TODO: shouldn't this be larger? Why bother with such a small amount?) + mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); + + float upDistance = 0; + if(!mUpStepper.mHitObject) + upDistance = sStepSizeUp; + else if(mUpStepper.mFraction*sStepSizeUp > sCollisionMargin) + upDistance = mUpStepper.mFraction*sStepSizeUp - sCollisionMargin; + else + { + return false; } - /* - * Try moving from the elevated position using tracer. - * - * +--+ +--+ - * | | |YY| FIXME: collision with object YY - * | | +--+ - * | | - * <------------------->| | - * +--+ +--+ - * |XX| the moved amount is toMove*tracer.mFraction - * +--+ - * ============================================== - */ - osg::Vec3f tracerPos = mUpStepper.mEndPos; - mTracer.doTrace(mColObj, tracerPos, tracerPos + toMove, mColWorld); - if (mTracer.mFraction < std::numeric_limits::epsilon()) - return false; // didn't even move the smallest representable amount - - /* - * Try moving back down sStepSizeDown using stepper. - * NOTE: if there is an obstacle below (e.g. stairs), we'll be "stepping up". - * Below diagram is the case where we "stepped over" an obstacle in front. - * - * +--+ - * |YY| - * +--+ +--+ - * | | - * | | - * +--+ | | - * |XX| | | - * +--+ +--+ - * ============================================== - */ - mDownStepper.doTrace(mColObj, mTracer.mEndPos, mTracer.mEndPos-osg::Vec3f(0.0f,0.0f,sStepSizeDown), mColWorld); - if (!canStepDown(mDownStepper)) + auto toMove = velocity * remainingTime; + + osg::Vec3f tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); + + osg::Vec3f tracerDest; + auto normalMove = toMove; + auto moveDistance = normalMove.normalize(); + // attempt 1: normal movement + // attempt 2: fixed distance movement, only happens on the first movement solver iteration/bounce each frame to avoid a glitch + // attempt 3: further, less tall fixed distance movement, same as above + // If you're making a full conversion you should purge the logic for attempts 2 and 3. Attempts 2 and 3 just try to work around problems with vanilla Morrowind assets. + int attempt = 0; + float downStepSize; + while(attempt < 3) { - // Try again with increased step length - if (mTracer.mFraction < 1.0f || toMove.length2() > sMinStep*sMinStep) - return false; + attempt++; - osg::Vec3f direction = toMove; - direction.normalize(); - mTracer.doTrace(mColObj, tracerPos, tracerPos + direction*sMinStep, mColWorld); - if (mTracer.mFraction < 0.001f) + if(attempt == 1) + tracerDest = tracerPos + toMove; + else if (!firstIteration || !sDoExtraStairHacks) // first attempt failed and not on first movement solver iteration, can't retry -- or we have extra hacks disabled + { return false; + } + else if(attempt == 2) + { + moveDistance = sMinStep; + tracerDest = tracerPos + normalMove*sMinStep; + } + else if(attempt == 3) + { + if(upDistance > sStepSizeUp) + { + upDistance = sStepSizeUp; + tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); + } + moveDistance = sMinStep2; + tracerDest = tracerPos + normalMove*sMinStep2; + } + + mTracer.doTrace(mColObj, tracerPos, tracerDest, mColWorld); + if(mTracer.mHitObject) + { + // map against what we hit, minus the safety margin + moveDistance *= mTracer.mFraction; + if(moveDistance <= sCollisionMargin) // didn't move enough to accomplish anything + { + return false; + } + + moveDistance -= sCollisionMargin; + tracerDest = tracerPos + normalMove*moveDistance; + + // safely eject from what we hit by the safety margin + auto tempDest = tracerDest + mTracer.mPlaneNormal*sCollisionMargin*2; + + ActorTracer tempTracer; + tempTracer.doTrace(mColObj, tracerDest, tempDest, mColWorld); + + if(tempTracer.mFraction > 0.5f) // distance to any object is greater than sCollisionMargin (we checked sCollisionMargin*2 distance) + { + auto effectiveFraction = tempTracer.mFraction*2.0f - 1.0f; + tracerDest += mTracer.mPlaneNormal*sCollisionMargin*effectiveFraction; + } + } + + if(attempt > 2) // do not allow stepping down below original height for attempt 3 + downStepSize = upDistance; + else + downStepSize = moveDistance + upDistance + sStepSizeDown; + mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld); + + // can't step down onto air, non-walkable-slopes, or actors + // NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were intended to be valid at the bottoms of stairs + // (like the bottoms of the staircases in aldruhn's guild of mages) + // The old code worked around this by trying to do mTracer again with a fixed distance of sMinStep (10.0) but it caused all sorts of other problems. + // Switched back to cylinders to avoid that and similer problems. + if(canStepDown(mDownStepper)) + { + break; + } + else + { + // do not try attempt 3 if we just tried attempt 2 and the horizontal distance was rather large + // (forces actor to get snug against the defective ledge for attempt 3 to be tried) + if(attempt == 2 && moveDistance > upDistance-(mDownStepper.mFraction*downStepSize)) + { + return false; + } + // do next attempt if first iteration of movement solver and not out of attempts + if(firstIteration && attempt < 3) + { + continue; + } - mDownStepper.doTrace(mColObj, mTracer.mEndPos, mTracer.mEndPos-osg::Vec3f(0.0f,0.0f,sStepSizeDown), mColWorld); - if (!canStepDown(mDownStepper)) return false; + } } - if (mDownStepper.mFraction < 1.0f) + // note: can't downstep onto actors so no need to pick safety margin + float downDistance = 0; + if(mDownStepper.mFraction*downStepSize > sCollisionMargin) + downDistance = mDownStepper.mFraction*downStepSize - sCollisionMargin; + + if(downDistance-sCollisionMargin-sGroundOffset > upDistance && !onGround) + return false; + + auto newpos = tracerDest + osg::Vec3f(0.0f, 0.0f, -downDistance); + + if((position-newpos).length2() < sCollisionMargin*sCollisionMargin) + return false; + + if(mTracer.mHitObject) { - // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. - // TODO: stepper.mPlaneNormal does not appear to be reliable - needs more testing - // NOTE: caller's variables 'position' & 'remainingTime' are modified here - position = mDownStepper.mEndPos; - remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance - mHaveMoved = true; - return true; + auto planeNormal = mTracer.mPlaneNormal; + if (onGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) + { + planeNormal.z() = 0; + planeNormal.normalize(); + } + velocity = reject(velocity, planeNormal); } - return false; + velocity = reject(velocity, mDownStepper.mPlaneNormal); + + position = newpos; + + remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance + return true; } } diff --git a/apps/openmw/mwphysics/stepper.hpp b/apps/openmw/mwphysics/stepper.hpp index 27e6294b05..512493c524 100644 --- a/apps/openmw/mwphysics/stepper.hpp +++ b/apps/openmw/mwphysics/stepper.hpp @@ -20,12 +20,11 @@ namespace MWPhysics const btCollisionObject *mColObj; ActorTracer mTracer, mUpStepper, mDownStepper; - bool mHaveMoved; public: Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj); - bool step(osg::Vec3f &position, const osg::Vec3f &toMove, float &remainingTime); + bool step(osg::Vec3f &position, osg::Vec3f &velocity, float &remainingTime, const bool & onGround, bool firstIteration); }; } diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index 58082f4db2..f50b6100a6 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -24,7 +24,7 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star to.setOrigin(btend); const btVector3 motion = btstart-btend; - ClosestNotMeConvexResultCallback newTraceCallback(actor, motion, btScalar(0.0)); + ClosestNotMeConvexResultCallback newTraceCallback(actor, motion, btScalar(0.0), world); // Inherit the actor's collision group and mask newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; @@ -62,7 +62,7 @@ void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const btTransform to(trans.getBasis(), btend); const btVector3 motion = btstart-btend; - ClosestNotMeConvexResultCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0)); + ClosestNotMeConvexResultCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0), world); // Inherit the actor's collision group and mask newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask; diff --git a/appveyor.yml b/appveyor.yml index e2c13ed948..ed6f727be5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -50,7 +50,7 @@ install: before_build: - cmd: git submodule update --init --recursive - - cmd: sh %APPVEYOR_BUILD_FOLDER%\CI\before_script.msvc.sh -c %configuration% -p %PLATFORM% -v %msvc% -V -i %APPVEYOR_BUILD_FOLDER%\install + - cmd: sh %APPVEYOR_BUILD_FOLDER%\CI\before_script.msvc.sh -c %configuration% -p %PLATFORM% -v %msvc% -V -i %APPVEYOR_BUILD_FOLDER%\install -D build_script: - cmd: if %PLATFORM%==Win32 set build=MSVC%msvc%_32 diff --git a/cmake/OSIdentity.cmake b/cmake/OSIdentity.cmake new file mode 100644 index 0000000000..40b7b20895 --- /dev/null +++ b/cmake/OSIdentity.cmake @@ -0,0 +1,67 @@ +if (UNIX) + + if (APPLE) + + set(CMAKE_OS_NAME "OSX" CACHE STRING "Operating system name" FORCE) + + else (APPLE) + + ## Check for Debian GNU/Linux ________________ + + find_file(DEBIAN_FOUND debian_version debconf.conf + PATHS /etc + ) + if (DEBIAN_FOUND) + set(CMAKE_OS_NAME "Debian" CACHE STRING "Operating system name" FORCE) + endif (DEBIAN_FOUND) + + ## Check for Fedora _________________________ + + find_file(FEDORA_FOUND fedora-release + PATHS /etc + ) + if (FEDORA_FOUND) + set(CMAKE_OS_NAME "Fedora" CACHE STRING "Operating system name" FORCE) + endif (FEDORA_FOUND) + + ## Check for RedHat _________________________ + + find_file(REDHAT_FOUND redhat-release inittab.RH + PATHS /etc + ) + if (REDHAT_FOUND) + set(CMAKE_OS_NAME "RedHat" CACHE STRING "Operating system name" FORCE) + endif (REDHAT_FOUND) + + ## Extra check for Ubuntu ____________________ + + if (DEBIAN_FOUND) + + ## At its core Ubuntu is a Debian system, with + ## a slightly altered configuration; hence from + ## a first superficial inspection a system will + ## be considered as Debian, which signifies an + ## extra check is required. + + find_file(UBUNTU_EXTRA legal issue + PATHS /etc + ) + + if (UBUNTU_EXTRA) + ## Scan contents of file + file(STRINGS ${UBUNTU_EXTRA} UBUNTU_FOUND + REGEX Ubuntu + ) + ## Check result of string search + if (UBUNTU_FOUND) + set(CMAKE_OS_NAME "Ubuntu" CACHE STRING "Operating system name" FORCE) + set(DEBIAN_FOUND FALSE) + endif (UBUNTU_FOUND) + + endif (UBUNTU_EXTRA) + + endif (DEBIAN_FOUND) + + endif (APPLE) + +endif (UNIX) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 10a5d22fbe..832fc611f6 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -236,7 +236,6 @@ target_link_libraries(components ${OSGGA_LIBRARIES} ${OSGSHADOW_LIBRARIES} ${OSGANIMATION_LIBRARIES} - ${BULLET_LIBRARIES} ${SDL2_LIBRARIES} ${OPENGL_gl_LIBRARY} ${MyGUI_LIBRARIES} @@ -246,6 +245,12 @@ target_link_libraries(components RecastNavigation::Recast ) +if (BULLET_USE_DOUBLES AND (UBUNTU_FOUND OR DEBIAN_FOUND)) + target_link_libraries(components BulletCollision-float64 LinearMath-float64) +else() + target_link_libraries(components ${BULLET_LIBRARIES}) +endif() + if (WIN32) target_link_libraries(components ${Boost_LOCALE_LIBRARY}