2020-03-31 00:05:54 +03:00
|
|
|
#include "movementsolver.hpp"
|
|
|
|
|
|
|
|
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
|
|
|
|
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
|
2021-10-09 18:16:29 +02:00
|
|
|
#include <BulletCollision/CollisionShapes/btConvexShape.h>
|
2020-03-31 00:05:54 +03:00
|
|
|
|
2022-01-22 15:58:41 +01:00
|
|
|
#include <components/esm3/loadgmst.hpp>
|
2020-03-31 00:05:54 +03:00
|
|
|
#include <components/misc/convert.hpp>
|
|
|
|
|
|
|
|
#include "../mwbase/environment.hpp"
|
|
|
|
|
|
|
|
#include "../mwworld/esmstore.hpp"
|
|
|
|
|
|
|
|
#include "actor.hpp"
|
|
|
|
#include "collisiontype.hpp"
|
|
|
|
#include "constants.hpp"
|
2020-12-27 22:16:11 +00:00
|
|
|
#include "contacttestwrapper.h"
|
2024-01-20 16:50:51 +01:00
|
|
|
#include "object.hpp"
|
2020-10-15 06:11:00 +02:00
|
|
|
#include "physicssystem.hpp"
|
2021-10-09 18:16:29 +02:00
|
|
|
#include "projectile.hpp"
|
|
|
|
#include "projectileconvexcallback.hpp"
|
2020-03-31 00:05:54 +03:00
|
|
|
#include "stepper.hpp"
|
|
|
|
#include "trace.h"
|
|
|
|
|
2020-12-27 22:16:11 +00:00
|
|
|
#include <cmath>
|
|
|
|
|
2020-03-31 00:05:54 +03:00
|
|
|
namespace MWPhysics
|
|
|
|
{
|
|
|
|
static bool isActor(const btCollisionObject* obj)
|
|
|
|
{
|
|
|
|
assert(obj);
|
|
|
|
return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor;
|
|
|
|
}
|
|
|
|
|
2024-02-02 22:16:49 +01:00
|
|
|
namespace
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2024-02-02 22:16:49 +01:00
|
|
|
class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback
|
2020-12-27 22:16:11 +00:00
|
|
|
{
|
2024-02-02 22:16:49 +01:00
|
|
|
public:
|
|
|
|
explicit ContactCollectionCallback(const btCollisionObject& me, const osg::Vec3f& velocity)
|
|
|
|
: mVelocity(Misc::Convert::toBullet(velocity))
|
2020-12-27 22:16:11 +00:00
|
|
|
{
|
2024-02-02 22:16:49 +01:00
|
|
|
m_collisionFilterGroup = me.getBroadphaseHandle()->m_collisionFilterGroup;
|
|
|
|
m_collisionFilterMask = me.getBroadphaseHandle()->m_collisionFilterMask & ~CollisionType_Projectile;
|
2020-12-27 22:16:11 +00:00
|
|
|
}
|
2024-02-02 22:16:49 +01:00
|
|
|
|
|
|
|
btScalar addSingleResult(btManifoldPoint& contact, const btCollisionObjectWrapper* colObj0Wrap,
|
|
|
|
int /*partId0*/, int /*index0*/, const btCollisionObjectWrapper* colObj1Wrap, int /*partId1*/,
|
|
|
|
int /*index1*/) override
|
2020-12-27 22:16:11 +00:00
|
|
|
{
|
2024-02-02 22:16:49 +01:00
|
|
|
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;
|
|
|
|
}
|
2020-12-27 22:16:11 +00:00
|
|
|
}
|
2024-02-02 22:16:49 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
}
|
2020-03-31 00:05:54 +03:00
|
|
|
|
|
|
|
osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, Actor* actor,
|
|
|
|
btCollisionWorld* collisionWorld, float maxHeight)
|
|
|
|
{
|
|
|
|
osg::Vec3f offset = actor->getCollisionObjectPosition() - ptr.getRefData().getPosition().asVec3();
|
|
|
|
|
|
|
|
ActorTracer tracer;
|
|
|
|
tracer.findGround(actor, position + offset, position + offset - osg::Vec3f(0, 0, maxHeight), collisionWorld);
|
|
|
|
if (tracer.mFraction >= 1.0f)
|
|
|
|
{
|
|
|
|
actor->setOnGround(false);
|
|
|
|
return position;
|
|
|
|
}
|
|
|
|
|
|
|
|
actor->setOnGround(true);
|
|
|
|
|
|
|
|
// Check if we actually found a valid spawn point (use an infinitely thin ray this time).
|
|
|
|
// Required for some broken door destinations in Morrowind.esm, where the spawn point
|
|
|
|
// intersects with other geometry if the actor's base is taken into account
|
2023-05-09 01:20:02 +02:00
|
|
|
btVector3 from = Misc::Convert::toBullet(position);
|
2020-03-31 00:05:54 +03:00
|
|
|
btVector3 to = from - btVector3(0, 0, maxHeight);
|
|
|
|
|
2023-05-09 01:20:02 +02:00
|
|
|
btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to);
|
2021-04-11 10:28:56 +02:00
|
|
|
resultCallback1.m_collisionFilterGroup = CollisionType_AnyPhysical;
|
2020-03-31 00:05:54 +03:00
|
|
|
resultCallback1.m_collisionFilterMask = CollisionType_World | CollisionType_HeightMap;
|
|
|
|
|
2023-05-09 01:20:02 +02:00
|
|
|
collisionWorld->rayTest(from, to, resultCallback1);
|
2020-03-31 00:05:54 +03:00
|
|
|
|
|
|
|
if (resultCallback1.hasHit()
|
|
|
|
&& ((Misc::Convert::toOsg(resultCallback1.m_hitPointWorld) - tracer.mEndPos + offset).length2() > 35 * 35
|
|
|
|
|| !isWalkableSlope(tracer.mPlaneNormal)))
|
|
|
|
{
|
|
|
|
actor->setOnSlope(!isWalkableSlope(resultCallback1.m_hitNormalWorld));
|
2023-05-09 01:20:02 +02:00
|
|
|
return Misc::Convert::toOsg(resultCallback1.m_hitPointWorld) + osg::Vec3f(0.f, 0.f, sGroundOffset);
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
actor->setOnSlope(!isWalkableSlope(tracer.mPlaneNormal));
|
|
|
|
|
|
|
|
return tracer.mEndPos - offset + osg::Vec3f(0.f, 0.f, sGroundOffset);
|
|
|
|
}
|
|
|
|
|
2020-10-15 06:11:00 +02:00
|
|
|
void MovementSolver::move(
|
|
|
|
ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
|
|
|
// Reset per-frame data
|
2021-07-21 19:04:36 +02:00
|
|
|
actor.mWalkingOnWater = false;
|
2020-03-31 00:05:54 +03:00
|
|
|
// Anything to collide with?
|
2021-07-21 19:04:36 +02:00
|
|
|
if (actor.mSkipCollisionDetection)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2021-07-21 19:04:36 +02:00
|
|
|
actor.mPosition += (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0))
|
|
|
|
* osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1)))
|
2020-10-15 06:11:00 +02:00
|
|
|
* actor.mMovement * time;
|
|
|
|
return;
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
|
2020-12-27 22:16:11 +00:00
|
|
|
// 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
|
2022-02-22 04:04:08 +00:00
|
|
|
// 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()
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mPosition.z() += actor.mHalfExtentsZ; // vanilla-accurate
|
2020-03-31 00:05:54 +03:00
|
|
|
|
2021-07-22 19:29:20 +02:00
|
|
|
float swimlevel = actor.mSwimLevel + actor.mHalfExtentsZ;
|
2020-03-31 00:05:54 +03:00
|
|
|
|
|
|
|
ActorTracer tracer;
|
|
|
|
|
|
|
|
osg::Vec3f velocity;
|
|
|
|
|
2021-07-21 23:19:17 +02:00
|
|
|
// Dead and paralyzed actors underwater will float to the surface,
|
|
|
|
// if the CharacterController tells us to do so
|
|
|
|
if (actor.mMovement.z() > 0 && actor.mInert && actor.mPosition.z() < swimlevel)
|
|
|
|
{
|
|
|
|
velocity = osg::Vec3f(0, 0, 1) * 25;
|
|
|
|
}
|
|
|
|
else if (actor.mPosition.z() < swimlevel || actor.mFlying)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2021-07-21 19:04:36 +02:00
|
|
|
velocity = (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0))
|
|
|
|
* osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1)))
|
|
|
|
* actor.mMovement;
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-07-21 19:04:36 +02:00
|
|
|
velocity = (osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement;
|
2020-03-31 00:05:54 +03:00
|
|
|
|
2021-07-21 19:04:36 +02:00
|
|
|
if ((velocity.z() > 0.f && actor.mIsOnGround && !actor.mIsOnSlope)
|
2021-07-22 19:29:20 +02:00
|
|
|
|| (velocity.z() > 0.f && velocity.z() + actor.mInertia.z() <= -velocity.z() && actor.mIsOnSlope))
|
|
|
|
actor.mInertia = velocity;
|
2021-07-21 19:04:36 +02:00
|
|
|
else if (!actor.mIsOnGround || actor.mIsOnSlope)
|
2021-07-22 19:29:20 +02:00
|
|
|
velocity = velocity + actor.mInertia;
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Now that we have the effective movement vector, apply wind forces to it
|
2021-08-08 17:19:18 +02:00
|
|
|
if (worldData.mIsInStorm && velocity.length() > 0)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2024-10-26 07:54:52 +03:00
|
|
|
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
|
|
|
|
const float fStromWalkMult = store.get<ESM::GameSetting>().find("fStromWalkMult")->mValue.getFloat();
|
|
|
|
const float angleCos = worldData.mStormDirection * velocity / velocity.length();
|
|
|
|
velocity *= 1.f + fStromWalkMult * angleCos;
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
|
2021-07-22 19:29:20 +02:00
|
|
|
Stepper stepper(collisionWorld, actor.mCollisionObject);
|
2020-03-31 00:05:54 +03:00
|
|
|
osg::Vec3f origVelocity = velocity;
|
2020-10-15 06:11:00 +02:00
|
|
|
osg::Vec3f newPosition = actor.mPosition;
|
2020-03-31 00:05:54 +03:00
|
|
|
/*
|
|
|
|
* A loop to find newPosition using tracer, if successful different from the starting position.
|
|
|
|
* nextpos is the local variable used to find potential newPosition, using velocity and remainingTime
|
|
|
|
* The initial velocity was set earlier (see above).
|
|
|
|
*/
|
|
|
|
float remainingTime = time;
|
2020-12-27 22:16:11 +00:00
|
|
|
|
|
|
|
int numTimesSlid = 0;
|
|
|
|
osg::Vec3f lastSlideNormal(0, 0, 1);
|
|
|
|
osg::Vec3f lastSlideNormalFallback(0, 0, 1);
|
|
|
|
bool forceGroundTest = false;
|
|
|
|
|
2021-03-20 21:14:56 -04:00
|
|
|
for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.0001f; ++iterations)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2023-12-27 19:11:49 +00:00
|
|
|
osg::Vec3f nextpos = newPosition + velocity * remainingTime;
|
2021-07-21 23:19:17 +02:00
|
|
|
bool underwater = newPosition.z() < swimlevel;
|
2020-03-31 00:05:54 +03:00
|
|
|
|
|
|
|
// If not able to fly, don't allow to swim up into the air
|
2021-07-21 23:19:17 +02:00
|
|
|
if (!actor.mFlying && nextpos.z() > swimlevel && underwater)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
|
|
|
const osg::Vec3f down(0, 0, -1);
|
2020-12-27 22:16:11 +00:00
|
|
|
velocity = reject(velocity, down);
|
2020-03-31 00:05:54 +03:00
|
|
|
// NOTE: remainingTime is unchanged before the loop continues
|
|
|
|
continue; // velocity updated, calculate nextpos again
|
|
|
|
}
|
|
|
|
|
2023-12-27 19:11:49 +00:00
|
|
|
if ((newPosition - nextpos).length2() > 0.0001)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
|
|
|
// trace to where character would go if there were no obstructions
|
2021-10-29 15:11:08 -04:00
|
|
|
tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld, actor.mIsOnGround);
|
2020-03-31 00:05:54 +03:00
|
|
|
|
|
|
|
// check for obstructions
|
|
|
|
if (tracer.mFraction >= 1.0f)
|
|
|
|
{
|
|
|
|
newPosition = tracer.mEndPos; // ok to move, so set newPosition
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// The current position and next position are nearly the same, so just exit.
|
|
|
|
// Note: Bullet can trigger an assert in debug modes if the positions
|
|
|
|
// are the same, since that causes it to attempt to normalize a zero
|
|
|
|
// length vector (which can also happen with nearly identical vectors, since
|
|
|
|
// precision can be lost due to any math Bullet does internally). Since we
|
|
|
|
// aren't performing any collision detection, we want to reject the next
|
|
|
|
// position, so that we don't slowly move inside another object.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-07-21 23:19:17 +02:00
|
|
|
bool seenGround = !actor.mFlying && !underwater
|
|
|
|
&& ((actor.mIsOnGround && !actor.mIsOnSlope) || isWalkableSlope(tracer.mPlaneNormal));
|
2020-03-31 00:05:54 +03:00
|
|
|
|
|
|
|
// We hit something. Check if we can step up.
|
2021-07-22 19:29:20 +02:00
|
|
|
float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + actor.mHalfExtentsZ;
|
2020-03-31 00:05:54 +03:00
|
|
|
osg::Vec3f oldPosition = newPosition;
|
2020-12-27 22:16:11 +00:00
|
|
|
bool usedStepLogic = false;
|
2024-01-20 16:50:51 +01:00
|
|
|
if (!isActor(tracer.mHitObject))
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2024-01-20 16:50:51 +01:00
|
|
|
if (hitHeight < Constants::sStepSizeUp)
|
|
|
|
{
|
|
|
|
// Try to step up onto it.
|
|
|
|
// NOTE: this modifies newPosition and velocity on its own if successful
|
|
|
|
usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0);
|
|
|
|
}
|
|
|
|
auto* ptrHolder = static_cast<PtrHolder*>(tracer.mHitObject->getUserPointer());
|
|
|
|
if (Object* hitObject = dynamic_cast<Object*>(ptrHolder))
|
|
|
|
{
|
|
|
|
hitObject->addCollision(
|
|
|
|
actor.mIsPlayer ? ScriptedCollisionType_Player : ScriptedCollisionType_Actor);
|
|
|
|
}
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
2020-12-27 22:16:11 +00:00
|
|
|
if (usedStepLogic)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2021-07-22 19:29:20 +02:00
|
|
|
if (actor.mIsAquatic && newPosition.z() + actor.mHalfExtentsZ > actor.mWaterlevel)
|
2020-03-31 00:05:54 +03:00
|
|
|
newPosition = oldPosition;
|
2020-12-27 22:16:11 +00:00
|
|
|
else if (!actor.mFlying && actor.mPosition.z() >= swimlevel)
|
|
|
|
forceGroundTest = true;
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-12-27 22:16:11 +00:00
|
|
|
// Can't step up, so slide against what we ran into
|
|
|
|
remainingTime *= (1.0f - tracer.mFraction);
|
|
|
|
|
|
|
|
auto planeNormal = tracer.mPlaneNormal;
|
2021-11-05 14:45:31 -04:00
|
|
|
// need to know the unadjusted normal to handle certain types of seams properly
|
|
|
|
const auto origPlaneNormal = planeNormal;
|
2020-12-27 22:16:11 +00:00
|
|
|
|
|
|
|
// 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
|
2021-11-05 14:45:31 -04:00
|
|
|
// they do for this, we want to use the original plane normal, or else certain types of geometry will
|
|
|
|
// snag
|
2020-12-27 22:16:11 +00:00
|
|
|
if (numTimesSlid > 0)
|
|
|
|
{
|
2021-11-05 14:45:31 -04:00
|
|
|
auto dotA = lastSlideNormal * origPlaneNormal;
|
|
|
|
auto dotB = lastSlideNormalFallback * origPlaneNormal;
|
2020-12-27 22:16:11 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-11-05 14:45:31 -04:00
|
|
|
auto constraintVector = bestNormal ^ origPlaneNormal; // cross product
|
2020-12-27 22:16:11 +00:00
|
|
|
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
|
2021-11-05 14:45:31 -04:00
|
|
|
auto averageNormal = bestNormal + origPlaneNormal;
|
2020-12-27 22:16:11 +00:00
|
|
|
averageNormal.normalize();
|
2021-07-22 19:29:20 +02:00
|
|
|
tracer.doTrace(actor.mCollisionObject, newPosition,
|
|
|
|
newPosition + averageNormal * (sCollisionMargin * 2.0), collisionWorld);
|
2020-12-27 22:16:11 +00:00
|
|
|
newPosition = (newPosition + tracer.mEndPos) / 2.0;
|
|
|
|
|
|
|
|
usedSeamLogic = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// otherwise just keep the normal vector rejection
|
|
|
|
|
|
|
|
// 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
|
2021-11-05 14:45:31 -04:00
|
|
|
if (!usedSeamLogic)
|
2020-12-27 22:16:11 +00:00
|
|
|
{
|
2021-07-22 19:29:20 +02:00
|
|
|
tracer.doTrace(actor.mCollisionObject, newPosition,
|
|
|
|
newPosition + planeNormal * (sCollisionMargin * 2.0), collisionWorld);
|
2020-12-27 22:16:11 +00:00
|
|
|
newPosition = (newPosition + tracer.mEndPos) / 2.0;
|
|
|
|
}
|
2020-03-31 00:05:54 +03:00
|
|
|
|
2022-01-16 17:58:04 -05:00
|
|
|
// short circuit if we went backwards, but only if it was mostly horizontal and we're on the ground
|
|
|
|
if (seenGround && newVelocity * origVelocity <= 0.0f)
|
|
|
|
{
|
|
|
|
auto perpendicular = newVelocity ^ origVelocity;
|
|
|
|
if (perpendicular.length2() > 0.0f)
|
|
|
|
{
|
|
|
|
perpendicular.normalize();
|
|
|
|
if (std::abs(perpendicular.z()) > 0.7071f)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-27 22:16:11 +00:00
|
|
|
// Do not allow sliding up steep slopes if there is gravity.
|
2021-11-05 14:45:31 -04:00
|
|
|
// The purpose of this is to prevent air control from letting you slide up tall, unwalkable slopes.
|
|
|
|
// For that purpose, it is not necessary to do it when trying to slide along acute seams/crevices (i.e.
|
|
|
|
// usedSeamLogic) and doing so would actually break air control in some situations where vanilla allows
|
|
|
|
// air control. Vanilla actually allows you to slide up slopes as long as you're in the "walking"
|
|
|
|
// animation, which can be true even in the air, so allowing this for seams isn't a compatibility break.
|
|
|
|
if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal) && !usedSeamLogic)
|
2020-12-27 22:16:11 +00:00
|
|
|
newVelocity.z() = std::min(newVelocity.z(), velocity.z());
|
2020-03-31 00:05:54 +03:00
|
|
|
|
2020-12-27 22:16:11 +00:00
|
|
|
numTimesSlid += 1;
|
|
|
|
lastSlideNormalFallback = lastSlideNormal;
|
2021-11-05 14:45:31 -04:00
|
|
|
lastSlideNormal = origPlaneNormal;
|
2020-03-31 00:05:54 +03:00
|
|
|
velocity = newVelocity;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isOnGround = false;
|
|
|
|
bool isOnSlope = false;
|
2021-07-22 19:29:20 +02:00
|
|
|
if (forceGroundTest || (actor.mInertia.z() <= 0.f && newPosition.z() >= swimlevel))
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
|
|
|
osg::Vec3f from = newPosition;
|
2021-07-21 19:04:36 +02:00
|
|
|
auto dropDistance = 2 * sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0);
|
2020-12-27 22:16:11 +00:00
|
|
|
osg::Vec3f to = newPosition - osg::Vec3f(0, 0, dropDistance);
|
2021-10-29 15:11:08 -04:00
|
|
|
tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld, actor.mIsOnGround);
|
2020-12-27 22:16:11 +00:00
|
|
|
if (tracer.mFraction < 1.0f)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2020-12-27 22:16:11 +00:00
|
|
|
if (!isActor(tracer.mHitObject))
|
|
|
|
{
|
|
|
|
isOnGround = true;
|
|
|
|
isOnSlope = !isWalkableSlope(tracer.mPlaneNormal);
|
2021-07-23 22:37:55 +02:00
|
|
|
actor.mStandingOn = tracer.mHitObject;
|
2020-03-31 00:05:54 +03:00
|
|
|
|
2021-07-23 22:37:55 +02:00
|
|
|
if (actor.mStandingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water)
|
2021-07-21 19:04:36 +02:00
|
|
|
actor.mWalkingOnWater = true;
|
2020-12-27 22:16:11 +00:00
|
|
|
if (!actor.mFlying && !isOnSlope)
|
2020-03-31 00:05:54 +03:00
|
|
|
{
|
2020-12-27 22:16:11 +00:00
|
|
|
if (tracer.mFraction * dropDistance > sGroundOffset)
|
|
|
|
newPosition.z() = tracer.mEndPos.z() + sGroundOffset;
|
|
|
|
else
|
|
|
|
{
|
|
|
|
newPosition.z() = tracer.mEndPos.z();
|
2021-07-22 19:29:20 +02:00
|
|
|
tracer.doTrace(actor.mCollisionObject, newPosition,
|
|
|
|
newPosition + osg::Vec3f(0, 0, 2 * sGroundOffset), collisionWorld);
|
2020-12-27 22:16:11 +00:00
|
|
|
newPosition = (newPosition + tracer.mEndPos) / 2.0;
|
|
|
|
}
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
}
|
2020-12-27 22:16:11 +00:00
|
|
|
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;
|
2020-03-31 00:05:54 +03:00
|
|
|
|
2020-12-27 22:16:11 +00:00
|
|
|
isOnGround = false;
|
|
|
|
}
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
2021-03-20 21:14:56 -04:00
|
|
|
// forcibly treat stuck actors as if they're on flat ground because buggy collisions when inside of things
|
|
|
|
// can/will break ground detection
|
2021-07-22 19:29:20 +02:00
|
|
|
if (actor.mStuckFrames > 0)
|
2021-03-20 21:14:56 -04:00
|
|
|
{
|
|
|
|
isOnGround = true;
|
|
|
|
isOnSlope = false;
|
|
|
|
}
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
|
2020-10-15 06:11:00 +02:00
|
|
|
if ((isOnGround && !isOnSlope) || newPosition.z() < swimlevel || actor.mFlying)
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mInertia = osg::Vec3f(0.f, 0.f, 0.f);
|
2020-03-31 00:05:54 +03:00
|
|
|
else
|
|
|
|
{
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mInertia.z() -= time * Constants::GravityConst * Constants::UnitsPerMeter;
|
|
|
|
if (actor.mInertia.z() < 0)
|
|
|
|
actor.mInertia.z() *= actor.mSlowFall;
|
2020-10-15 06:11:00 +02:00
|
|
|
if (actor.mSlowFall < 1.f)
|
|
|
|
{
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mInertia.x() *= actor.mSlowFall;
|
|
|
|
actor.mInertia.y() *= actor.mSlowFall;
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
}
|
2021-07-21 19:04:36 +02:00
|
|
|
actor.mIsOnGround = isOnGround;
|
|
|
|
actor.mIsOnSlope = isOnSlope;
|
2020-03-31 00:05:54 +03:00
|
|
|
|
2020-10-15 06:11:00 +02:00
|
|
|
actor.mPosition = newPosition;
|
2020-12-27 22:16:11 +00:00
|
|
|
// remove what was added earlier in compensating for doTrace not taking interior transformation into account
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mPosition.z() -= actor.mHalfExtentsZ; // vanilla-accurate
|
2020-12-27 22:16:11 +00:00
|
|
|
}
|
|
|
|
|
2021-10-09 18:16:29 +02:00
|
|
|
void MovementSolver::move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld)
|
|
|
|
{
|
|
|
|
btVector3 btFrom = Misc::Convert::toBullet(projectile.mPosition);
|
|
|
|
btVector3 btTo = Misc::Convert::toBullet(projectile.mPosition + projectile.mMovement * time);
|
|
|
|
|
|
|
|
if (btFrom == btTo)
|
|
|
|
return;
|
|
|
|
|
2024-02-02 22:16:49 +01:00
|
|
|
assert(projectile.mProjectile != nullptr);
|
|
|
|
|
2021-10-09 18:16:29 +02:00
|
|
|
ProjectileConvexCallback resultCallback(
|
2024-02-02 22:16:49 +01:00
|
|
|
projectile.mCaster, projectile.mCollisionObject, btFrom, btTo, *projectile.mProjectile);
|
2021-04-11 10:28:56 +02:00
|
|
|
resultCallback.m_collisionFilterMask = CollisionType_AnyPhysical;
|
2021-10-09 18:16:29 +02:00
|
|
|
resultCallback.m_collisionFilterGroup = CollisionType_Projectile;
|
|
|
|
|
|
|
|
const btQuaternion btrot = btQuaternion::getIdentity();
|
|
|
|
btTransform from_(btrot, btFrom);
|
|
|
|
btTransform to_(btrot, btTo);
|
|
|
|
|
|
|
|
const btCollisionShape* shape = projectile.mCollisionObject->getCollisionShape();
|
|
|
|
assert(shape->isConvex());
|
|
|
|
collisionWorld->convexSweepTest(static_cast<const btConvexShape*>(shape), from_, to_, resultCallback);
|
|
|
|
|
|
|
|
projectile.mPosition
|
|
|
|
= Misc::Convert::toOsg(projectile.mProjectile->isActive() ? btTo : resultCallback.m_hitPointWorld);
|
|
|
|
}
|
|
|
|
|
2020-12-27 22:16:11 +00:00
|
|
|
btVector3 addMarginToDelta(btVector3 delta)
|
|
|
|
{
|
|
|
|
if (delta.length2() == 0.0)
|
|
|
|
return delta;
|
|
|
|
return delta + delta.normalized() * sCollisionMargin;
|
|
|
|
}
|
|
|
|
|
|
|
|
void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld)
|
|
|
|
{
|
2021-07-21 19:04:36 +02:00
|
|
|
if (actor.mSkipCollisionDetection) // noclipping/tcl
|
2020-12-27 22:16:11 +00:00
|
|
|
return;
|
|
|
|
|
In 0.46, SetPos was setting position of actors before physics simulation, and from this position movement was simulated. This changed with async physics merging, and at the same time problems started, mostly with abot's scenic travel.
Skipping the simulation, switching off collisions, and other approaches were not correct as they either broke some mods, or some core mechanics of the engine such as teleportation or waterwalking. As it turns out, the way to go is to simply do _nothing_ (modulo some gymnastics to account for the 1 frame difference in case of async).
Scripted movement and the unstucking logic tends to collide. Early out of unstuck in case the actor doesn't attempt to move. This means there is no AI package for NPC, which are the case for some boats and striders, or the player is content with their position.
2023-03-16 21:33:36 +01:00
|
|
|
if (actor.mMovement.length2() == 0) // no AI nor player attempted to move, current position is assumed correct
|
|
|
|
return;
|
|
|
|
|
2020-12-27 22:16:11 +00:00
|
|
|
auto tempPosition = actor.mPosition;
|
|
|
|
|
2021-07-22 19:29:20 +02:00
|
|
|
if (actor.mStuckFrames >= 10)
|
2021-03-20 21:14:56 -04:00
|
|
|
{
|
2021-07-22 19:29:20 +02:00
|
|
|
if ((actor.mLastStuckPosition - actor.mPosition).length2() < 100)
|
2021-03-20 21:14:56 -04:00
|
|
|
return;
|
|
|
|
else
|
|
|
|
{
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mStuckFrames = 0;
|
|
|
|
actor.mLastStuckPosition = { 0, 0, 0 };
|
2021-03-20 21:14:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-22 04:04:08 +00:00
|
|
|
// 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()
|
2021-07-22 19:29:20 +02:00
|
|
|
const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, actor.mHalfExtentsZ);
|
2020-12-27 22:16:11 +00:00
|
|
|
|
|
|
|
// use a 3d approximation of the movement vector to better judge player intent
|
2021-07-21 19:04:36 +02:00
|
|
|
auto velocity = (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0))
|
|
|
|
* osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1)))
|
|
|
|
* actor.mMovement;
|
2020-12-27 22:16:11 +00:00
|
|
|
// try to pop outside of the world before doing anything else if we're inside of it
|
2021-07-21 19:04:36 +02:00
|
|
|
if (!actor.mIsOnGround || actor.mIsOnSlope)
|
2021-07-22 19:29:20 +02:00
|
|
|
velocity += actor.mInertia;
|
2020-12-27 22:16:11 +00:00
|
|
|
|
|
|
|
// 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;
|
2021-07-22 19:29:20 +02:00
|
|
|
const btTransform oldTransform = actor.mCollisionObject->getWorldTransform();
|
2020-12-27 22:16:11 +00:00
|
|
|
btTransform newTransform = oldTransform;
|
|
|
|
|
|
|
|
auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback {
|
|
|
|
goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset));
|
|
|
|
newTransform.setOrigin(Misc::Convert::toBullet(goodPosition));
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mCollisionObject->setWorldTransform(newTransform);
|
2020-12-27 22:16:11 +00:00
|
|
|
|
2024-02-02 22:16:49 +01:00
|
|
|
ContactCollectionCallback callback(*actor.mCollisionObject, velocity);
|
2021-07-22 19:29:20 +02:00
|
|
|
ContactTestWrapper::contactTest(
|
|
|
|
const_cast<btCollisionWorld*>(collisionWorld), actor.mCollisionObject, callback);
|
2020-12-27 22:16:11 +00:00
|
|
|
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)
|
|
|
|
{
|
2021-07-22 19:29:20 +02:00
|
|
|
++actor.mStuckFrames;
|
|
|
|
actor.mLastStuckPosition = actor.mPosition;
|
2020-12-27 22:16:11 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-20 21:14:56 -04:00
|
|
|
else
|
|
|
|
{
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mStuckFrames = 0;
|
|
|
|
actor.mLastStuckPosition = { 0, 0, 0 };
|
2021-03-20 21:14:56 -04:00
|
|
|
}
|
2020-12-27 22:16:11 +00:00
|
|
|
|
2021-07-22 19:29:20 +02:00
|
|
|
actor.mCollisionObject->setWorldTransform(oldTransform);
|
2020-12-27 22:16:11 +00:00
|
|
|
actor.mPosition = tempPosition;
|
2020-03-31 00:05:54 +03:00
|
|
|
}
|
|
|
|
}
|