1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-03-14 01:19:59 +00:00

Merge branch 'IK' into 'master'

Draft: Inverse Kinematics for legs and overall better Actor gound alignment.

See merge request OpenMW/openmw!4548
This commit is contained in:
MaxYari 2025-03-13 12:34:49 +00:00
commit c32e8c092e
8 changed files with 470 additions and 21 deletions

View File

@ -23,7 +23,7 @@ add_openmw_dir (mwrender
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples
actorutil distortion animationpriority bonegroup blendmask animblendcontroller
actorutil distortion easyik animationpriority bonegroup blendmask animblendcontroller
)
add_openmw_dir (mwinput

View File

@ -197,6 +197,9 @@ bool OMW::Engine::frame(unsigned frameNumber, float frametime)
mEnvironment.setFrameDuration(frametime);
mViewer->eventTraversal();
mViewer->updateTraversal();
try
{
// update input
@ -285,16 +288,6 @@ bool OMW::Engine::frame(unsigned frameNumber, float frametime)
}
}
// update physics
{
ScopedProfile<UserStatsType::Physics> profile(frameStart, frameNumber, *timer, *stats);
if (mStateManager->getState() != MWBase::StateManager::State_NoGame)
{
mWorld->updatePhysics(frametime, paused, frameStart, frameNumber, *stats);
}
}
// update world
{
ScopedProfile<UserStatsType::World> profile(frameStart, frameNumber, *timer, *stats);
@ -305,6 +298,16 @@ bool OMW::Engine::frame(unsigned frameNumber, float frametime)
}
}
// update physics
{
ScopedProfile<UserStatsType::Physics> profile(frameStart, frameNumber, *timer, *stats);
if (mStateManager->getState() != MWBase::StateManager::State_NoGame)
{
mWorld->updatePhysics(frametime, paused, frameStart, frameNumber, *stats);
}
}
// update GUI
{
ScopedProfile<UserStatsType::Gui> profile(frameStart, frameNumber, *timer, *stats);
@ -339,9 +342,6 @@ bool OMW::Engine::frame(unsigned frameNumber, float frametime)
mStereoManager->updateSettings(Settings::camera().mNearClip, Settings::camera().mViewingDistance);
mViewer->eventTraversal();
mViewer->updateTraversal();
// update GUI by world data
{
ScopedProfile<UserStatsType::WindowManager> profile(frameStart, frameNumber, *timer, *stats);

View File

@ -36,6 +36,7 @@ namespace MWPhysics
, mExternalCollisionMode(true)
, mActive(false)
, mTaskScheduler(scheduler)
, mCurrentDeltaZ(0.0f)
{
// We can not create actor without collisions - he will fall through the ground.
// In this case we should autogenerate collision box based on mesh shape

View File

@ -146,6 +146,8 @@ namespace MWPhysics
void setActive(bool value) { mActive = value; }
float mCurrentDeltaZ;
DetourNavigator::CollisionShapeType getCollisionShapeType() const { return mCollisionShapeType; }
private:

View File

@ -19,6 +19,7 @@
#include <LinearMath/btQuickprof.h>
#include <LinearMath/btVector3.h>
#include <components/debug/debugdraw.hpp>
#include <components/debug/debuglog.hpp>
#include <components/esm3/loadgmst.hpp>
#include <components/esm3/loadmgef.hpp>
@ -27,6 +28,7 @@
#include <components/misc/strings/conversion.hpp>
#include <components/resource/bulletshapemanager.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
#include <components/settings/values.hpp>
#include "../mwbase/environment.hpp"
@ -41,6 +43,8 @@
#include "../mwworld/player.hpp"
#include "../mwrender/bulletdebugdraw.hpp"
#include "../mwrender/easyik.hpp"
#include "../mwrender/renderingmanager.hpp"
#include "../mwworld/class.hpp"
@ -744,18 +748,162 @@ namespace MWPhysics
mActorsPositions.clear();
if (!mActors.empty())
mActorsPositions.reserve(mActors.size() - 1);
for (const auto& [ptr, physicActor] : mActors)
for (const auto& [ref, physicActor] : mActors)
{
if (physicActor.get() == player)
continue;
mActorsPositions.emplace_back(physicActor->getPtr(), physicActor->getSimulationPosition());
auto colliderPos = physicActor->getSimulationPosition();
auto actptr = physicActor->getPtr();
// Reset actor armature to collider's position before doing any additional adjustments
auto visualPos = actptr.getRefData().getBaseNode()->getPosition();
world->moveObject(actptr, colliderPos, false, false);
// IK config and variables
osg::Vec3 desiredPos = colliderPos;
float armatureZLerpSpeed = 5;
float maxVisualZDelta = physicActor.get()->getHalfExtents().z() * 2 / 4;
MWPhysics::RayCastingResult leftRayCast;
MWPhysics::RayCastingResult rightRayCast;
std::vector<osg::ref_ptr<osg::MatrixTransform>> leftLegBoneChain;
std::vector<osg::ref_ptr<osg::MatrixTransform>> rightLegBoneChain;
float leftFootHeight
= estimateFootHeight(actptr.getRefData().getBaseNode()->asGroup(), "Bip01 L Foot", "Bip01 L Toe0");
float rightFootHeight
= estimateFootHeight(actptr.getRefData().getBaseNode()->asGroup(), "Bip01 R Foot", "Bip01 R Toe0");
osg::ref_ptr<osg::MatrixTransform> leftFootBone;
osg::ref_ptr<osg::MatrixTransform> rightFootBone;
float desiredDeltaZ = 0.0f;
if (physicActor.get()->getOnGround())
{
// Finding leg bones
std::vector<std::string> leftLegBoneChainNames = { "Bip01 L Thigh", "Bip01 L Calf", "Bip01 L Foot" };
std::vector<std::string> rightLegBoneChainNames = { "Bip01 R Thigh", "Bip01 R Calf", "Bip01 R Foot" };
gatherMatchingBones(
actptr.getRefData().getBaseNode()->asGroup(), leftLegBoneChainNames, leftLegBoneChain);
gatherMatchingBones(
actptr.getRefData().getBaseNode()->asGroup(), rightLegBoneChainNames, rightLegBoneChain);
// Compute world transformation for foot bones
osg::Matrix leftFootWorldMatrix, rightFootWorldMatrix;
if (leftLegBoneChain.size() == leftLegBoneChainNames.size())
leftFootBone = leftLegBoneChain.back();
if (leftFootBone)
leftFootWorldMatrix = osg::computeLocalToWorld(leftFootBone->getParentalNodePaths()[0]);
osg::Vec3f leftFootPos
= leftFootBone ? static_cast<osg::Vec3f>(leftFootWorldMatrix.getTrans()) : colliderPos;
if (rightLegBoneChain.size() == rightLegBoneChainNames.size())
rightFootBone = rightLegBoneChain.back();
if (rightFootBone)
rightFootWorldMatrix = osg::computeLocalToWorld(rightFootBone->getParentalNodePaths()[0]);
osg::Vec3f rightFootPos
= rightFootBone ? static_cast<osg::Vec3f>(rightFootWorldMatrix.getTrans()) : colliderPos;
// Perform raycasts
leftRayCast = castRay(leftFootPos, leftFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f),
CollisionType_HeightMap + CollisionType_World);
rightRayCast = castRay(rightFootPos, rightFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f),
CollisionType_HeightMap + CollisionType_World);
// Determine the desired delta Z
if (leftRayCast.mHit && rightRayCast.mHit)
{
desiredDeltaZ = std::min(leftRayCast.mHitPos.z(), rightRayCast.mHitPos.z()) - colliderPos.z();
}
else if (leftRayCast.mHit)
{
desiredDeltaZ = leftRayCast.mHitPos.z() - colliderPos.z();
}
else if (rightRayCast.mHit)
{
desiredDeltaZ = rightRayCast.mHitPos.z() - colliderPos.z();
}
}
// Lerp the current delta Z towards the desired delta Z
physicActor->mCurrentDeltaZ
= std::lerp(physicActor->mCurrentDeltaZ, desiredDeltaZ, mPhysicsDt * armatureZLerpSpeed);
// Calculate the new armature position using the lerped delta Z
osg::Vec3 armaturePos = colliderPos;
armaturePos.z() += physicActor->mCurrentDeltaZ;
// For some reason player and actors supposed to be kept separately
if (physicActor.get() != player)
mActorsPositions.emplace_back(physicActor->getPtr(), physicActor->getSimulationPosition());
// Finally adjust position based on the new armature position
world->moveObject(actptr, armaturePos, false, false);
// Casting up from feet to detect clipping
MWPhysics::RayCastingResult rightRayUpCast, leftRayUpCast;
if (rightFootBone)
{
auto rightFootWorldMatrix = osg::computeLocalToWorld(rightFootBone->getParentalNodePaths()[0]);
osg::Vec3f rightFootPos = static_cast<osg::Vec3f>(rightFootWorldMatrix.getTrans());
rightRayUpCast = castRay(rightFootPos, rightFootPos + osg::Vec3f(0.0f, 0.0f, 50.0f),
CollisionType_HeightMap + CollisionType_World);
}
if (rightRayUpCast.mHit)
{
auto& debugRender = world->getRenderingManager()->getDebugDrawer();
/*debugRender.drawCube(rightRayCast.mHitPos, osg::Vec3(10.0, 10.0, 10.0), Debug::colorBlue);*/
// Perform IK for right leg
if (rightLegBoneChain.size() > 0)
{
// Note that its possible to provide a pole direction to specify in which direction a knee should
// bend. Good guesstimate for bipedal actors might be providing actor facing direction (in world
// space) as a pole direction (not done here, I haven't figured how to find actor facing direction)
EasyIK rightLegIK(rightLegBoneChain,
rightRayUpCast.mHitPos + osg::Vec3f(0.0f, 0.0f, rightFootHeight), 10, 0.05f, std::nullopt,
&debugRender);
rightLegIK.solveIK();
rightLegIK.alignFoot(rightRayUpCast.mHitNormal * -1);
if (mDebugDrawEnabled)
rightLegIK.debugDraw();
}
}
if (leftFootBone)
{
auto leftFootWorldMatrix = osg::computeLocalToWorld(leftFootBone->getParentalNodePaths()[0]);
osg::Vec3f leftFootPos = static_cast<osg::Vec3f>(leftFootWorldMatrix.getTrans());
leftRayUpCast = castRay(leftFootPos, leftFootPos + osg::Vec3f(0.0f, 0.0f, 50.0f),
CollisionType_HeightMap + CollisionType_World);
}
if (leftRayUpCast.mHit)
{
auto& debugRender = world->getRenderingManager()->getDebugDrawer();
/*debugRender.drawCube(leftRayCast.mHitPos, osg::Vec3(10.0, 10.0, 10.0), Debug::colorBlue);*/
// Perform IK for left leg
if (leftLegBoneChain.size() > 0)
{
EasyIK leftLegIK(leftLegBoneChain, leftRayUpCast.mHitPos + osg::Vec3f(0.0f, 0.0f, leftFootHeight),
10, 0.05f, std::nullopt, &debugRender);
leftLegIK.solveIK();
leftLegIK.alignFoot(leftRayUpCast.mHitNormal * -1);
if (mDebugDrawEnabled)
leftLegIK.debugDraw();
}
}
}
for (const auto& [ptr, pos] : mActorsPositions)
world->moveObject(ptr, pos, false, false);
/*for (const auto& [ptr, pos] : mActorsPositions)
{
world->moveObject(ptr, desiredPos, false, false);
}
if (player != nullptr)
world->moveObject(player->getPtr(), player->getSimulationPosition(), false, false);
world->moveObject(player->getPtr(), player->getSimulationPosition(), false, false);*/
}
void PhysicsSystem::updateAnimatedCollisionShape(const MWWorld::Ptr& object)
@ -973,4 +1121,47 @@ namespace MWPhysics
{
return lhs.mRawActors == rhs.mRawActors;
}
void PhysicsSystem::gatherMatchingBones(osg::Group* parentNode, const std::vector<std::string>& boneChainNames,
std::vector<osg::ref_ptr<osg::MatrixTransform>>& boneChain)
{
if (!parentNode)
return;
for (unsigned int i = 0; i < parentNode->getNumChildren(); ++i)
{
osg::ref_ptr<osg::Node> child = parentNode->getChild(i);
// asMatrixTransform will break if its not a bone
if (std::find(boneChainNames.begin(), boneChainNames.end(), child->getName()) != boneChainNames.end())
{
boneChain.push_back(child->asTransform()->asMatrixTransform());
}
osg::Group* group = child->asGroup();
if (group)
{
gatherMatchingBones(group, boneChainNames, boneChain);
}
}
}
float PhysicsSystem::estimateFootHeight(
osg::Group* parentNode, const std::string& footBoneName, const std::string& toeBoneName)
{
std::vector<std::string> boneNames = { footBoneName, toeBoneName };
std::vector<osg::ref_ptr<osg::MatrixTransform>> bones;
gatherMatchingBones(parentNode, boneNames, bones);
if (bones.size() == 2)
{
osg::Vec3f footPos = bones[0]->getMatrix().getTrans();
osg::Vec3f toePos = bones[1]->getMatrix().getTrans();
return std::abs(footPos.z() - toePos.z());
}
// Return a fixed constant value as a fallback
return 8.0f; // Adjust this value as needed
}
}

View File

@ -15,6 +15,7 @@
#include <osg/Quat>
#include <osg/Timer>
#include <osg/ref_ptr>
#include <osgAnimation/Bone>
#include <components/vfs/pathutil.hpp>
@ -287,6 +288,12 @@ namespace MWPhysics
void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
void reportCollision(const btVector3& position, const btVector3& normal);
void gatherMatchingBones(osg::Group* parentNode, const std::vector<std::string>& boneChainNames,
std::vector<osg::ref_ptr<osg::MatrixTransform>>& boneChain);
float estimateFootHeight(
osg::Group* parentNode, const std::string& footBoneName, const std::string& toeBoneName);
private:
void updateWater();

View File

@ -0,0 +1,206 @@
// Authors: Joar Engberg, GPT4o, Maksim Eremenko :)
// Ported with a significant GPT4o (Copilot) assistance from a C# (Unity script) Inverse Kinematic solver implementation
// by Joar Engberg (EasyIK) https://github.com/joaen/EasyIK/blob/master/EasyIK/Assets/Scripts/EasyIK.cs
#include "easyik.hpp"
#include <algorithm>
#include <cmath>
#include <optional>
#include <osg/MatrixTransform>
#include <osg/Quat>
#include <osg/Vec3>
#include <vector>
// Note: armatureBones are in local space, poleDirection and ikTarget are in world space.
EasyIK::EasyIK(const std::vector<osg::ref_ptr<osg::MatrixTransform>>& armatureBones, const osg::Vec3f& ikTarget,
int iterations, float tolerance, std::optional<osg::Vec3f> poleDirection, Debug::DebugDrawer* debugDrawer)
: mNumberOfJoints(armatureBones.size()) // Number of joints is one more than the number of bones
, mIkTarget(ikTarget)
, mIterations(iterations)
, mTolerance(tolerance)
, mPoleDirection(poleDirection)
, mDebugDrawer(debugDrawer)
, mArmatureBones(armatureBones)
{
initialize();
}
void EasyIK::initialize()
{
mJointChainLength = 0;
mJointPositions.resize(mNumberOfJoints);
mLocalJointPositions.resize(mNumberOfJoints);
mBoneLengths.resize(mNumberOfJoints - 1);
for (int i = 0; i < mNumberOfJoints - 1; ++i)
{
osg::ref_ptr<osg::MatrixTransform> current = mArmatureBones[i];
osg::ref_ptr<osg::MatrixTransform> child = mArmatureBones[i + 1];
mBoneLengths[i] = child->getMatrix().getTrans().length();
mJointChainLength += mBoneLengths[i];
}
}
void EasyIK::poleConstraint()
{
if (mNumberOfJoints != 3 || !mPoleDirection.has_value())
return;
osg::Vec3 limbAxis = mJointPositions[2] - mJointPositions[0];
limbAxis.normalize();
osg::Vec3 poleDirection = mPoleDirection.value();
poleDirection.normalize();
osg::Vec3 boneDirection = mJointPositions[1] - mJointPositions[0];
boneDirection.normalize();
// Orthonormalize the vectors
poleDirection -= limbAxis * (poleDirection * limbAxis);
poleDirection.normalize();
boneDirection -= limbAxis * (boneDirection * limbAxis);
boneDirection.normalize();
osg::Quat angle;
angle.makeRotate(boneDirection, poleDirection);
mJointPositions[1] = angle * (mJointPositions[1] - mJointPositions[0]) + mJointPositions[0];
}
void EasyIK::backward()
{
for (int i = mNumberOfJoints - 1; i >= 0; --i)
{
if (i == mNumberOfJoints - 1)
{
mJointPositions[i] = mIkTarget;
}
else
{
osg::Vec3 pos = mJointPositions[i] - mJointPositions[i + 1];
pos.normalize();
mJointPositions[i] = mJointPositions[i + 1] + pos * mBoneLengths[i];
}
}
}
void EasyIK::forward()
{
for (int i = 0; i < mNumberOfJoints; ++i)
{
if (i == 0)
{
mJointPositions[i] = osg::computeLocalToWorld(mArmatureBones[0]->getParentalNodePaths()[0]).getTrans();
}
else
{
osg::Vec3 pos = mJointPositions[i] - mJointPositions[i - 1];
pos.normalize();
mJointPositions[i] = mJointPositions[i - 1] + pos * mBoneLengths[i - 1];
}
}
}
void EasyIK::solveIK()
{
// Initialize joint positions in world space
for (int i = 0; i < mNumberOfJoints; ++i)
{
mJointPositions[i] = osg::computeLocalToWorld(mArmatureBones[i]->getParentalNodePaths()[0]).getTrans();
}
// Calculate distance to target
mDistanceToTarget = (mJointPositions[0] - mIkTarget).length();
// If the target is out of reach, stretch the chain towards the target
if (mDistanceToTarget > mJointChainLength)
{
osg::Vec3 direction = mIkTarget - mJointPositions[0];
direction.normalize();
for (int i = 1; i < mNumberOfJoints; ++i)
{
mJointPositions[i] = mJointPositions[i - 1] + direction * mBoneLengths[i - 1];
}
}
else
{
// Iteratively solve the IK problem
float distToTarget = (mJointPositions[mNumberOfJoints - 1] - mIkTarget).length();
int counter = 0;
while (distToTarget > mTolerance)
{
backward();
forward();
distToTarget = (mJointPositions[mNumberOfJoints - 1] - mIkTarget).length();
counter++;
if (counter > mIterations)
{
break;
}
}
}
// Apply pole constraint
poleConstraint();
// Apply the calculated positions to the bones in local space
for (int i = 0; i < mNumberOfJoints - 1; ++i)
{
auto boneName = mArmatureBones[i]->getName();
// World/local conversion data
osg::Matrix worldToLocal = osg::computeWorldToLocal(mArmatureBones[i]->getParentalNodePaths()[0]);
osg::Quat worldToLocalRot = worldToLocal.getRotate();
// Directions should be in local space, so after deriving them from joint positions - we convert them to the
// local space of the bone. Start direction IS a direction in which the bone is
// currently pointing (in its own local space!), an assumption here is made that the bone is always pointing
// along 1.0 0 0 in its own local space, so far - this assumption seem to stand correct.
osg::Vec3f startDirection = osg::Vec3f(1.0f, 0.0f, 0.0f);
osg::Vec3f solvedDirection = worldToLocalRot * (mJointPositions[i + 1] - mJointPositions[i]);
solvedDirection.normalize();
// Find a rotation from startDirection to solvedDirection
osg::Quat targetRotation;
targetRotation.makeRotate(startDirection, solvedDirection);
// Rotate the bone
osg::Matrix currentMatrix = mArmatureBones[i]->getMatrix();
currentMatrix.preMultRotate(targetRotation);
mArmatureBones[i]->setMatrix(currentMatrix);
}
}
void EasyIK::alignFoot(const osg::Vec3f& normal)
{
// This alignes a foot to the ground based on provided normal. Probably angular limits should be introduced.
if (mNumberOfJoints < 1)
return;
osg::Vec3f invertedNormal = -normal;
osg::Matrix worldToLocal = osg::computeWorldToLocal(mArmatureBones.back()->getParentalNodePaths()[0]);
osg::Vec3f localNormal = worldToLocal.getRotate() * invertedNormal;
osg::Vec3f startDirection = osg::Vec3f(1.0f, 0.0f, 0.0f);
osg::Quat rotation;
rotation.makeRotate(startDirection, localNormal);
osg::Matrix currentMatrix = mArmatureBones.back()->getMatrix();
currentMatrix.preMultRotate(rotation);
mArmatureBones.back()->setMatrix(currentMatrix);
}
void EasyIK::debugDraw()
{
for (const auto& jointPosition : mJointPositions)
{
mDebugDrawer->drawCube(jointPosition, osg::Vec3f(10.0f, 10.0f, 10.0f), Debug::colorCyan);
}
if (mPoleDirection.has_value())
{
osg::Vec3f polePosition
= mJointPositions[0] + mPoleDirection.value() * 10.0f; // Arbitrary distance for visualization
mDebugDrawer->drawCube(polePosition, osg::Vec3f(5.0f, 5.0f, 5.0f), Debug::colorYellow);
}
mDebugDrawer->drawCube(mIkTarget, osg::Vec3f(5.0f, 5.0f, 5.0f), Debug::colorGreen);
}

View File

@ -0,0 +1,42 @@
#ifndef OPENMW_MWRENDER_EASYIK_HPP
#define OPENMW_MWRENDER_EASYIK_HPP
#include <components/debug/debugdraw.hpp>
#include <optional>
#include <osg/MatrixTransform>
#include <osg/Quat>
#include <osg/Vec3>
#include <vector>
class EasyIK
{
public:
EasyIK(const std::vector<osg::ref_ptr<osg::MatrixTransform>>& armatureBones, const osg::Vec3f& ikTarget,
int iterations, float tolerance, std::optional<osg::Vec3f> poleDirection, Debug::DebugDrawer* debugDrawer);
void solveIK();
void debugDraw();
void alignFoot(const osg::Vec3f& normal);
private:
int mNumberOfJoints;
int mIterations;
float mTolerance;
osg::Vec3f mIkTarget; // As a position in world space
std::optional<osg::Vec3f> mPoleDirection; // As a direction vector in world space
Debug::DebugDrawer* mDebugDrawer;
std::vector<osg::ref_ptr<osg::MatrixTransform>> mArmatureBones;
std::vector<osg::Vec3f> mLocalJointPositions;
std::vector<osg::Vec3f> mJointPositions;
std::vector<float> mBoneLengths;
float mJointChainLength;
float mDistanceToTarget;
void initialize();
void poleConstraint();
void backward();
void forward();
};
#endif // OPENMW_MWRENDER_EASYIK_HPP