values for this higher than sGroundOffset cause jittering on some surface; use safe-seeming value slightly less than sGroundOffset

pull/3036/head
wareya 3 years ago committed by psi29a
parent ddbfa5410d
commit 18ef32ca82

@ -3,12 +3,14 @@
Bug #832: OpenMW-CS: Handle deleted references 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 #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 #1952: Incorrect particle lighting
Bug #2069: Fireflies in Fireflies invade Morrowind look wrong Bug #2069: Fireflies in Fireflies invade Morrowind look wrong
Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs
Bug #2473: Unable to overstock merchants Bug #2473: Unable to overstock merchants
Bug #2798: Mutable ESM records Bug #2798: Mutable ESM records
Bug #2976 [reopened]: Issues combining settings from the command line and both config files 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 #3372: Projectiles and magic bolts go through moving targets
Bug #3676: NiParticleColorModifier isn't applied properly Bug #3676: NiParticleColorModifier isn't applied properly
Bug #3714: Savegame fails to load due to conflict between SpellState and MagicEffects 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 #4021: Attributes and skills are not stored as floats
Bug #4055: Local scripts don't inherit variables from their base record Bug #4055: Local scripts don't inherit variables from their base record
Bug #4083: Door animation freezes when colliding with actors 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 #4623: Corprus implementation is incorrect
Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level
Bug #4764: Data race in osg ParticleSystem Bug #4764: Data race in osg ParticleSystem
@ -71,12 +78,15 @@
Bug #5656: Sneaking characters block hits while standing Bug #5656: Sneaking characters block hits while standing
Bug #5661: Region sounds don't play at the right interval Bug #5661: Region sounds don't play at the right interval
Bug #5675: OpenMW-cs. FRMR subrecords are saved with the wrong MastIdx 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 #5688: Water shader broken indoors with enable indoor shadows = false
Bug #5695: ExplodeSpell for actors doesn't target the ground Bug #5695: ExplodeSpell for actors doesn't target the ground
Bug #5703: OpenMW-CS menu system crashing on XFCE Bug #5703: OpenMW-CS menu system crashing on XFCE
Bug #5706: AI sequences stop looping after the saved game is reloaded Bug #5706: AI sequences stop looping after the saved game is reloaded
Bug #5731: Editor: skirts are invisible on characters Bug #5731: Editor: skirts are invisible on characters
Bug #5758: Paralyzed actors behavior is inconsistent with vanilla 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 #390: 3rd person look "over the shoulder"
Feature #1536: Show more information about level on menu Feature #1536: Show more information about level on menu
Feature #2386: Distant Statics in the form of Object Paging Feature #2386: Distant Statics in the form of Object Paging

@ -41,4 +41,4 @@ Editor Bug Fixes:
Miscellaneous: Miscellaneous:
- Prevent save-game bloating by using an appropriate fog texture format (#5108) - 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) - Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363)

@ -1,4 +1,4 @@
#!/bin/sh -ex #!/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 unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null

@ -73,7 +73,7 @@ CONFIGURATIONS=()
TEST_FRAMEWORK="" TEST_FRAMEWORK=""
GOOGLE_INSTALL_ROOT="" GOOGLE_INSTALL_ROOT=""
INSTALL_PREFIX="." INSTALL_PREFIX="."
BULLET_DOUBLE="" BULLET_DOUBLE=true
BULLET_DBL="" BULLET_DBL=""
BULLET_DBL_DISPLAY="Single precision" BULLET_DBL_DISPLAY="Single precision"

@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.1.0)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Detect OS
include(cmake/OSIdentity.cmake)
# for link time optimization, remove if cmake version is >= 3.9 # for link time optimization, remove if cmake version is >= 3.9
if(POLICY CMP0069) if(POLICY CMP0069)
cmake_policy(SET CMP0069 NEW) 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_DOCS "Build documentation." OFF )
option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF)
option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" 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. set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up.

@ -73,7 +73,7 @@ add_openmw_dir (mwworld
add_openmw_dir (mwphysics add_openmw_dir (mwphysics
physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback
contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile
closestnotmeconvexresultcallback raycasting mtphysics closestnotmeconvexresultcallback raycasting mtphysics contacttestwrapper
) )
add_openmw_dir (mwclass add_openmw_dir (mwclass

@ -1,6 +1,5 @@
#include "actor.hpp" #include "actor.hpp"
#include <BulletCollision/CollisionShapes/btCapsuleShape.h>
#include <BulletCollision/CollisionShapes/btBoxShape.h> #include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h> #include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
@ -14,6 +13,8 @@
#include "collisiontype.hpp" #include "collisiontype.hpp"
#include "mtphysics.hpp" #include "mtphysics.hpp"
#include <cmath>
namespace MWPhysics 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() << "\"."; 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) mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents)));
if (std::abs(mHalfExtents.x()-mHalfExtents.y())<mHalfExtents.x()*0.05 && mHalfExtents.z() >= mHalfExtents.x()) mRotationallyInvariant = (mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) && std::fabs(mHalfExtents.x() - mHalfExtents.y()) < 2.2;
{
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;
}
mConvexShape = static_cast<btConvexShape*>(mShape.get()); mConvexShape = static_cast<btConvexShape*>(mShape.get());
@ -72,8 +64,11 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic
mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setCollisionShape(mShape.get());
mCollisionObject->setUserPointer(this); mCollisionObject->setUserPointer(this);
updateRotation();
updateScale(); updateScale();
if(!mRotationallyInvariant)
updateRotation();
updatePosition(); updatePosition();
addCollisionMask(getCollisionMask()); addCollisionMask(getCollisionMask());
updateCollisionObjectPosition(); updateCollisionObjectPosition();
@ -154,6 +149,11 @@ osg::Vec3f Actor::getSimulationPosition() const
return mSimulationPosition; return mSimulationPosition;
} }
osg::Vec3f Actor::getScaledMeshTranslation() const
{
return mRotation * osg::componentMultiply(mMeshTranslation, mScale);
}
void Actor::updateCollisionObjectPosition() void Actor::updateCollisionObjectPosition()
{ {
std::scoped_lock lock(mPositionMutex); std::scoped_lock lock(mPositionMutex);

@ -82,6 +82,9 @@ namespace MWPhysics
*/ */
osg::Vec3f getOriginalHalfExtents() const; osg::Vec3f getOriginalHalfExtents() const;
/// Returns the mesh translation, scaled and rotated as necessary
osg::Vec3f getScaledMeshTranslation() const;
/** /**
* Returns the position of the collision body * 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. * @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.

@ -1,15 +1,39 @@
#include <mutex>
#include "closestnotmeconvexresultcallback.hpp" #include "closestnotmeconvexresultcallback.hpp"
#include "collisiontype.hpp"
#include "contacttestwrapper.h"
#include <BulletCollision/CollisionDispatch/btCollisionObject.h> #include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <components/misc/convert.hpp>
#include "collisiontype.hpp" #include "collisiontype.hpp"
#include "projectile.hpp" #include "projectile.hpp"
namespace MWPhysics 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)), : 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) if (convexResult.m_hitCollisionObject == mMe)
return btScalar(1); 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<btCollisionWorld*>(mWorld), const_cast<btCollisionObject*>(mMe), const_cast<btCollisionObject*>(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) if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile)
{ {
auto* projectileHolder = static_cast<Projectile*>(convexResult.m_hitCollisionObject->getUserPointer()); auto* projectileHolder = static_cast<Projectile*>(convexResult.m_hitCollisionObject->getUserPointer());

@ -10,7 +10,7 @@ namespace MWPhysics
class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback
{ {
public: 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; btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) override;
@ -18,6 +18,7 @@ namespace MWPhysics
const btCollisionObject *mMe; const btCollisionObject *mMe;
const btVector3 mMotion; const btVector3 mMotion;
const btScalar mMinCollisionDot; const btScalar mMinCollisionDot;
const btCollisionWorld * mWorld;
}; };
} }

@ -5,12 +5,22 @@ namespace MWPhysics
{ {
static const float sStepSizeUp = 34.0f; static const float sStepSizeUp = 34.0f;
static const float sStepSizeDown = 62.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 sGroundOffset = 1.0f;
static const float sMaxSlope = 49.0f; static const float sMaxSlope = 49.0f;
// Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared.
static const int sMaxIterations = 8; 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 #endif

@ -0,0 +1,21 @@
#include <mutex>
#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);
}
}

@ -0,0 +1,14 @@
#ifndef OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H
#define OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
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

@ -17,10 +17,13 @@
#include "actor.hpp" #include "actor.hpp"
#include "collisiontype.hpp" #include "collisiontype.hpp"
#include "constants.hpp" #include "constants.hpp"
#include "contacttestwrapper.h"
#include "physicssystem.hpp" #include "physicssystem.hpp"
#include "stepper.hpp" #include "stepper.hpp"
#include "trace.h" #include "trace.h"
#include <cmath>
namespace MWPhysics namespace MWPhysics
{ {
static bool isActor(const btCollisionObject *obj) static bool isActor(const btCollisionObject *obj)
@ -29,12 +32,50 @@ namespace MWPhysics
return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor; return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor;
} }
template <class Vec3> class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback
static bool isWalkableSlope(const Vec3 &normal)
{ {
static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); public:
return (normal.z() > sMaxSlopeCos); 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) 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(); 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). // Adjust for collision mesh offset relative to actor's "location"
// That means the collision shape used for moving this actor is in a different spot than the collision shape // (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our own)
// other actors are using to collide against this actor. // for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of from internal hull translation
// While this is strictly speaking wrong, it's needed for MW compatibility. // if not for this hack, the "correct" collision hull position would be physicActor->getScaledMeshTranslation()
actor.mPosition.z() += halfExtents.z(); osg::Vec3f halfExtents = physicActor->getHalfExtents();
actor.mPosition.z() += halfExtents.z(); // vanilla-accurate
static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fSwimHeightScale")->mValue.getFloat(); static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fSwimHeightScale")->mValue.getFloat();
float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); 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). * The initial velocity was set earlier (see above).
*/ */
float remainingTime = time; 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) for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.01f; ++iterations)
{ {
osg::Vec3f nextpos = newPosition + velocity * remainingTime; osg::Vec3f nextpos = newPosition + velocity * remainingTime;
@ -164,7 +212,7 @@ namespace MWPhysics
if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel) if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel)
{ {
const osg::Vec3f down(0,0,-1); const osg::Vec3f down(0,0,-1);
velocity = slide(velocity, down); velocity = reject(velocity, down);
// NOTE: remainingTime is unchanged before the loop continues // NOTE: remainingTime is unchanged before the loop continues
continue; // velocity updated, calculate nextpos again continue; // velocity updated, calculate nextpos again
} }
@ -193,92 +241,158 @@ namespace MWPhysics
break; break;
} }
// We are touching something. if (isWalkableSlope(tracer.mPlaneNormal) && !actor.mFlying && newPosition.z() >= swimlevel)
if (tracer.mFraction < 1E-9f) seenGround = true;
{
// Try to separate by backing off slighly to unstuck the solver
osg::Vec3f backOff = (newPosition - tracer.mHitPoint) * 1E-2f;
newPosition += backOff;
}
// We hit something. Check if we can step up. // We hit something. Check if we can step up.
float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z(); float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z();
osg::Vec3f oldPosition = newPosition; osg::Vec3f oldPosition = newPosition;
bool result = false; bool usedStepLogic = false;
if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject))
{ {
// Try to step up onto it. // Try to step up onto it.
// NOTE: stepMove does not allow stepping over, modifies newPosition if successful // NOTE: this modifies newPosition and velocity on its own if successful
result = stepper.step(newPosition, velocity*remainingTime, remainingTime); 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 // don't let pure water creatures move out of water after stepMove
const auto ptr = physicActor->getPtr(); const auto ptr = physicActor->getPtr();
if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel) if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel)
newPosition = oldPosition; newPosition = oldPosition;
else if(!actor.mFlying && actor.mPosition.z() >= swimlevel)
forceGroundTest = true;
} }
else else
{ {
// Can't move this way, try to find another spot along the plane // Can't step up, so slide against what we ran into
osg::Vec3f newVelocity = slide(velocity, tracer.mPlaneNormal); 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. // Do not allow sliding up steep slopes if there is gravity.
// Stepping will have taken care of that. if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal))
if(!(newPosition.z() < swimlevel || actor.mFlying)) newVelocity.z() = std::min(newVelocity.z(), velocity.z());
newVelocity.z() = std::min(newVelocity.z(), 0.0f);
if ((newVelocity-velocity).length2() < 0.01) if (newVelocity * origVelocity <= 0.0f)
break; break;
if ((newVelocity * origVelocity) <= 0.f)
break; // ^ dot product
numTimesSlid += 1;
lastSlideNormalFallback = lastSlideNormal;
lastSlideNormal = planeNormal;
velocity = newVelocity; velocity = newVelocity;
} }
} }
bool isOnGround = false; bool isOnGround = false;
bool isOnSlope = 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 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); tracer.doTrace(colobj, from, to, collisionWorld);
if(tracer.mFraction < 1.0f && !isActor(tracer.mHitObject)) if(tracer.mFraction < 1.0f)
{ {
const btCollisionObject* standingOn = tracer.mHitObject; if (!isActor(tracer.mHitObject))
PtrHolder* ptrHolder = static_cast<PtrHolder*>(standingOn->getUserPointer()); {
if (ptrHolder) isOnGround = true;
actor.mStandingOn = ptrHolder->getPtr(); isOnSlope = !isWalkableSlope(tracer.mPlaneNormal);
if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water)
physicActor->setWalkingOnWater(true);
if (!actor.mFlying)
newPosition.z() = tracer.mEndPos.z() + sGroundOffset;
isOnGround = true; const btCollisionObject* standingOn = tracer.mHitObject;
PtrHolder* ptrHolder = static_cast<PtrHolder*>(standingOn->getUserPointer());
if (ptrHolder)
actor.mStandingOn = ptrHolder->getPtr();
isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water)
} physicActor->setWalkingOnWater(true);
else if (!actor.mFlying && !isOnSlope)
{
// 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)
{ {
btVector3 aabbMin, aabbMax; if (tracer.mFraction*dropDistance > sGroundOffset)
tracer.mHitObject->getCollisionShape()->getAabb(tracer.mHitObject->getWorldTransform(), aabbMin, aabbMax); newPosition.z() = tracer.mEndPos.z() + sGroundOffset;
btVector3 center = (aabbMin + aabbMax) / 2.f; else
inertia = osg::Vec3f(actor.mPosition.x() - center.x(), actor.mPosition.y() - center.y(), 0); {
inertia.normalize(); newPosition.z() = tracer.mEndPos.z();
inertia *= 100; 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->setOnGround(isOnGround);
physicActor->setOnSlope(isOnSlope); physicActor->setOnSlope(isOnSlope);
newPosition.z() -= halfExtents.z(); // remove what was added at the beginning
actor.mPosition = newPosition; 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<btCollisionWorld*>(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;
} }
} }

@ -5,6 +5,9 @@
#include <osg/Vec3f> #include <osg/Vec3f>
#include "constants.hpp"
#include "../mwworld/ptr.hpp"
class btCollisionWorld; class btCollisionWorld;
namespace MWWorld namespace MWWorld
@ -14,29 +17,35 @@ namespace MWWorld
namespace MWPhysics 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 <class Vec3>
static bool isWalkableSlope(const Vec3 &normal)
{
static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope));
return (normal.z() > sMaxSlopeCos);
}
class Actor; class Actor;
struct ActorFrameData; struct ActorFrameData;
struct WorldFrameData; struct WorldFrameData;
class MovementSolver 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: public:
static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); 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 move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData);
static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld);
}; };
} }

@ -13,6 +13,7 @@
#include "../mwworld/player.hpp" #include "../mwworld/player.hpp"
#include "actor.hpp" #include "actor.hpp"
#include "contacttestwrapper.h"
#include "movementsolver.hpp" #include "movementsolver.hpp"
#include "mtphysics.hpp" #include "mtphysics.hpp"
#include "object.hpp" #include "object.hpp"
@ -167,7 +168,14 @@ namespace MWPhysics
mPreStepBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]() mPreStepBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]()
{ {
if (mDeferAabbUpdate)
updateAabbs(); updateAabbs();
for (auto& data : mActorsFrameData)
if (data.mActor.lock())
{
std::unique_lock lock(mCollisionWorldMutex);
MovementSolver::unstuck(data, mCollisionWorld.get());
}
}); });
mPostStepBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]() mPostStepBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]()
@ -295,7 +303,7 @@ namespace MWPhysics
void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback)
{ {
std::shared_lock lock(mCollisionWorldMutex); std::shared_lock lock(mCollisionWorldMutex);
mCollisionWorld->contactTest(colObj, resultCallback); ContactTestWrapper::contactTest(mCollisionWorld.get(), colObj, resultCallback);
} }
std::optional<btVector3> PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) std::optional<btVector3> PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target)
@ -438,8 +446,7 @@ namespace MWPhysics
if (!mNewFrame) if (!mNewFrame)
mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; }); mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; });
if (mDeferAabbUpdate) mPreStepBarrier->wait();
mPreStepBarrier->wait();
int job = 0; int job = 0;
while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs)
@ -505,7 +512,10 @@ namespace MWPhysics
while (mRemainingSteps--) while (mRemainingSteps--)
{ {
for (auto& actorData : mActorsFrameData) for (auto& actorData : mActorsFrameData)
{
MovementSolver::unstuck(actorData, mCollisionWorld.get());
MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData); MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData);
}
updateActorsPositions(); updateActorsPositions();
} }

@ -16,7 +16,7 @@ namespace MWPhysics
osg::Vec3f mHitNormal; osg::Vec3f mHitNormal;
MWWorld::Ptr mHitObject; MWWorld::Ptr mHitObject;
}; };
class RayCastingInterface class RayCastingInterface
{ {
public: public:

@ -7,6 +7,7 @@
#include "collisiontype.hpp" #include "collisiontype.hpp"
#include "constants.hpp" #include "constants.hpp"
#include "movementsolver.hpp"
namespace MWPhysics namespace MWPhysics
{ {
@ -24,125 +25,155 @@ namespace MWPhysics
Stepper::Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj) Stepper::Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj)
: mColWorld(colWorld) : mColWorld(colWorld)
, mColObj(colObj) , 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)
{ {
/* if(velocity.x() == 0.0 && velocity.y() == 0.0)
* Slide up an incline or set of stairs. Should be called only after a return false;
* collision detection otherwise unnecessary tracing will be performed.
* // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the ground.
* NOTE: with a small change this method can be used to step over an obstacle // This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry.
* 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;
mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld);
if (mUpStepper.mFraction < std::numeric_limits<float>::epsilon())
return false; // didn't even move the smallest representable amount float upDistance = 0;
// (TODO: shouldn't this be larger? Why bother with such a small amount?) if(!mUpStepper.mHitObject)
upDistance = sStepSizeUp;
else if(mUpStepper.mFraction*sStepSizeUp > sCollisionMargin)
upDistance = mUpStepper.mFraction*sStepSizeUp - sCollisionMargin;
else
{
return false;
} }
/* auto toMove = velocity * remainingTime;
* Try moving from the elevated position using tracer.
* osg::Vec3f tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance);
* +--+ +--+
* | | |YY| FIXME: collision with object YY 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
* |XX| the moved amount is toMove*tracer.mFraction // 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;
osg::Vec3f tracerPos = mUpStepper.mEndPos; while(attempt < 3)
mTracer.doTrace(mColObj, tracerPos, tracerPos + toMove, mColWorld);
if (mTracer.mFraction < std::numeric_limits<float>::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))
{ {
// Try again with increased step length attempt++;
if (mTracer.mFraction < 1.0f || toMove.length2() > sMinStep*sMinStep)
return false;
osg::Vec3f direction = toMove; if(attempt == 1)
direction.normalize(); tracerDest = tracerPos + toMove;
mTracer.doTrace(mColObj, tracerPos, tracerPos + direction*sMinStep, mColWorld); else if (!firstIteration || !sDoExtraStairHacks) // first attempt failed and not on first movement solver iteration, can't retry -- or we have extra hacks disabled
if (mTracer.mFraction < 0.001f) {
return false; 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; 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. auto planeNormal = mTracer.mPlaneNormal;
// TODO: stepper.mPlaneNormal does not appear to be reliable - needs more testing if (onGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0)
// NOTE: caller's variables 'position' & 'remainingTime' are modified here {
position = mDownStepper.mEndPos; planeNormal.z() = 0;
remainingTime *= (1.0f-mTracer.mFraction); // remaining time is proportional to remaining distance planeNormal.normalize();
mHaveMoved = true; }
return true; 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;
} }
} }

@ -20,12 +20,11 @@ namespace MWPhysics
const btCollisionObject *mColObj; const btCollisionObject *mColObj;
ActorTracer mTracer, mUpStepper, mDownStepper; ActorTracer mTracer, mUpStepper, mDownStepper;
bool mHaveMoved;
public: public:
Stepper(const btCollisionWorld *colWorld, const btCollisionObject *colObj); 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);
}; };
} }

@ -24,7 +24,7 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star
to.setOrigin(btend); to.setOrigin(btend);
const btVector3 motion = btstart-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 // Inherit the actor's collision group and mask
newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup;
newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; 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); btTransform to(trans.getBasis(), btend);
const btVector3 motion = btstart-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 // Inherit the actor's collision group and mask
newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup; newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup;
newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask; newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask;

@ -50,7 +50,7 @@ install:
before_build: before_build:
- cmd: git submodule update --init --recursive - 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: build_script:
- cmd: if %PLATFORM%==Win32 set build=MSVC%msvc%_32 - cmd: if %PLATFORM%==Win32 set build=MSVC%msvc%_32

@ -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)

@ -236,7 +236,6 @@ target_link_libraries(components
${OSGGA_LIBRARIES} ${OSGGA_LIBRARIES}
${OSGSHADOW_LIBRARIES} ${OSGSHADOW_LIBRARIES}
${OSGANIMATION_LIBRARIES} ${OSGANIMATION_LIBRARIES}
${BULLET_LIBRARIES}
${SDL2_LIBRARIES} ${SDL2_LIBRARIES}
${OPENGL_gl_LIBRARY} ${OPENGL_gl_LIBRARY}
${MyGUI_LIBRARIES} ${MyGUI_LIBRARIES}
@ -246,6 +245,12 @@ target_link_libraries(components
RecastNavigation::Recast 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) if (WIN32)
target_link_libraries(components target_link_libraries(components
${Boost_LOCALE_LIBRARY} ${Boost_LOCALE_LIBRARY}

Loading…
Cancel
Save