diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 2a5ad2d18d..eeb070d675 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -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 diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 0ea8451774..0945cb02e3 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -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 profile(frameStart, frameNumber, *timer, *stats); - - if (mStateManager->getState() != MWBase::StateManager::State_NoGame) - { - mWorld->updatePhysics(frametime, paused, frameStart, frameNumber, *stats); - } - } - // update world { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); @@ -305,6 +298,16 @@ bool OMW::Engine::frame(unsigned frameNumber, float frametime) } } + // update physics + { + ScopedProfile profile(frameStart, frameNumber, *timer, *stats); + + if (mStateManager->getState() != MWBase::StateManager::State_NoGame) + { + mWorld->updatePhysics(frametime, paused, frameStart, frameNumber, *stats); + } + } + // update GUI { ScopedProfile 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 profile(frameStart, frameNumber, *timer, *stats); diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index e1efe6d242..8187ba3254 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -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 diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index e53477506c..dd5b554287 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -146,6 +146,8 @@ namespace MWPhysics void setActive(bool value) { mActive = value; } + float mCurrentDeltaZ; + DetourNavigator::CollisionShapeType getCollisionShapeType() const { return mCollisionShapeType; } private: diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index f403f97c2f..ad6e4061be 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #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" @@ -742,18 +746,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> leftLegBoneChain; + std::vector> 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 leftFootBone; + osg::ref_ptr rightFootBone; + + float desiredDeltaZ = 0.0f; + + if (physicActor.get()->getOnGround()) + { + // Finding leg bones + std::vector leftLegBoneChainNames = { "Bip01 L Thigh", "Bip01 L Calf", "Bip01 L Foot" }; + std::vector 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(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(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(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(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) @@ -953,4 +1101,47 @@ namespace MWPhysics { return lhs.mRawActors == rhs.mRawActors; } + + void PhysicsSystem::gatherMatchingBones(osg::Group* parentNode, const std::vector& boneChainNames, + std::vector>& boneChain) + { + if (!parentNode) + return; + + for (unsigned int i = 0; i < parentNode->getNumChildren(); ++i) + { + osg::ref_ptr 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 boneNames = { footBoneName, toeBoneName }; + std::vector> 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 + } + } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 8a845b4c41..627b44c473 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include @@ -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& boneChainNames, + std::vector>& boneChain); + + float estimateFootHeight( + osg::Group* parentNode, const std::string& footBoneName, const std::string& toeBoneName); + private: void updateWater(); diff --git a/apps/openmw/mwrender/easyik.cpp b/apps/openmw/mwrender/easyik.cpp new file mode 100644 index 0000000000..e1b4a89845 --- /dev/null +++ b/apps/openmw/mwrender/easyik.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +// Note: armatureBones are in local space, poleDirection and ikTarget are in world space. +EasyIK::EasyIK(const std::vector>& armatureBones, const osg::Vec3f& ikTarget, + int iterations, float tolerance, std::optional 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 current = mArmatureBones[i]; + osg::ref_ptr 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); +} diff --git a/apps/openmw/mwrender/easyik.hpp b/apps/openmw/mwrender/easyik.hpp new file mode 100644 index 0000000000..14825ee4e5 --- /dev/null +++ b/apps/openmw/mwrender/easyik.hpp @@ -0,0 +1,42 @@ +#ifndef OPENMW_MWRENDER_EASYIK_HPP +#define OPENMW_MWRENDER_EASYIK_HPP + +#include +#include +#include +#include +#include +#include + +class EasyIK +{ +public: + EasyIK(const std::vector>& armatureBones, const osg::Vec3f& ikTarget, + int iterations, float tolerance, std::optional 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 mPoleDirection; // As a direction vector in world space + Debug::DebugDrawer* mDebugDrawer; + + std::vector> mArmatureBones; + std::vector mLocalJointPositions; + std::vector mJointPositions; + std::vector mBoneLengths; + float mJointChainLength; + float mDistanceToTarget; + + void initialize(); + void poleConstraint(); + void backward(); + void forward(); +}; + +#endif // OPENMW_MWRENDER_EASYIK_HPP