From 65fb2cc203cab635e21592f3d4c29799371243ba Mon Sep 17 00:00:00 2001 From: MaxYari Date: Mon, 17 Feb 2025 18:51:42 +0100 Subject: [PATCH 1/4] Looking for ways to adjust visual actor position --- apps/openmw/mwphysics/actor.cpp | 2 +- apps/openmw/mwphysics/physicssystem.cpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index e1efe6d242..c8679f97b3 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -1,4 +1,4 @@ -#include "actor.hpp" + #include "actor.hpp" #include diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 20a9c38b0f..3a75f16669 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -751,7 +751,11 @@ namespace MWPhysics } for (const auto& [ptr, pos] : mActorsPositions) - world->moveObject(ptr, pos, false, false); + { + osg::Vec3 adjustedPos = pos + osg::Vec3f(0.0f, 0.0f, 100.0f); + world->moveObject(ptr, adjustedPos, false, false); + } + if (player != nullptr) world->moveObject(player->getPtr(), player->getSimulationPosition(), false, false); From 4c4b455bacc59181836638358519616764c0139d Mon Sep 17 00:00:00 2001 From: MaxYari Date: Tue, 18 Feb 2025 15:21:47 +0100 Subject: [PATCH 2/4] Raycasting from feet, adjusting actor to the lowest hit point. For some reason sometimes this doesnt work and feet still float. --- apps/openmw/mwphysics/physicssystem.cpp | 111 ++++++++++++++++++++++-- apps/openmw/mwphysics/physicssystem.hpp | 5 ++ 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index f5ec43bb7e..40e54c5023 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -744,22 +745,88 @@ namespace MWPhysics mActorsPositions.clear(); if (!mActors.empty()) mActorsPositions.reserve(mActors.size() - 1); + for (const auto& [ptr, physicActor] : mActors) { - if (physicActor.get() == player) - continue; - mActorsPositions.emplace_back(physicActor->getPtr(), physicActor->getSimulationPosition()); + // if (physicActor.get() == player) + // continue; + + //// Handling visual vertical actor adjustments and leg IK + auto pos = physicActor->getSimulationPosition(); + auto actptr = physicActor->getPtr(); + osg::Vec3 adjustedPos = pos; + + // Finding left leg bones + std::vector> leftLegBoneChain; + osg::ref_ptr leftFootBone; + std::vector leftLegBoneChainNames = { "Bip01 L Thigh", "Bip01 L Calf" }; + std::string leftFootBoneName = "Bip01 L Foot"; + + gatherMatchingBones(actptr.getRefData().getBaseNode()->asGroup(), leftLegBoneChainNames, leftFootBoneName, + leftLegBoneChain, leftFootBone); + + // Finding right leg bones + std::vector> rightLegBoneChain; + osg::ref_ptr rightFootBone; + std::vector rightLegBoneChainNames = { "Bip01 R Thigh", "Bip01 R Calf" }; + std::string rightFootBoneName = "Bip01 R Foot"; + + gatherMatchingBones(actptr.getRefData().getBaseNode()->asGroup(), rightLegBoneChainNames, rightFootBoneName, + rightLegBoneChain, rightFootBone); + + // Compute world transformation for left foot bone + osg::Matrix leftFootWorldMatrix; + if (leftFootBone) + leftFootWorldMatrix = osg::computeLocalToWorld(leftFootBone->getParentalNodePaths()[0]); + osg::Vec3f leftFootPos = leftFootBone ? static_cast(leftFootWorldMatrix.getTrans()) : pos; + auto leftRayCast = castRay(leftFootPos, leftFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f), CollisionType_World); + + // Compute world transformation for right foot bone + osg::Matrix rightFootWorldMatrix; + if (rightFootBone) + rightFootWorldMatrix = osg::computeLocalToWorld(rightFootBone->getParentalNodePaths()[0]); + osg::Vec3f rightFootPos = rightFootBone ? static_cast(rightFootWorldMatrix.getTrans()) : pos; + auto rightRayCast + = castRay(rightFootPos, rightFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f), CollisionType_World); + + // Determine the largest raycast distance and adjust the position accordingly + if (leftRayCast.mHit && rightRayCast.mHit) + { + if (leftRayCast.mHitPos.z() < rightRayCast.mHitPos.z()) + { + adjustedPos.z() = leftRayCast.mHitPos.z(); + } + else + { + adjustedPos.z() = rightRayCast.mHitPos.z(); + } + } + else if (leftRayCast.mHit) + { + adjustedPos.z() = leftRayCast.mHitPos.z(); + } + else if (rightRayCast.mHit) + { + adjustedPos.z() = rightRayCast.mHitPos.z(); + } + ////////////////////////////////////////////// + + // For some reason player and actors supposed to be kept seprately + if (physicActor.get() != player) + mActorsPositions.emplace_back(physicActor->getPtr(), physicActor->getSimulationPosition()); + + // Finally adjust position based on raytraced data, this will adjust both player and actors + world->moveObject(actptr, adjustedPos, false, false); } - for (const auto& [ptr, pos] : mActorsPositions) + /*for (const auto& [ptr, pos] : mActorsPositions) { - osg::Vec3 adjustedPos = pos + osg::Vec3f(0.0f, 0.0f, 100.0f); + world->moveObject(ptr, adjustedPos, 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) @@ -977,4 +1044,34 @@ namespace MWPhysics { return lhs.mRawActors == rhs.mRawActors; } + + void PhysicsSystem::gatherMatchingBones(osg::Group* parentNode, const std::vector& boneChainNames, + const std::string& leafBoneName, std::vector>& boneChain, + osg::ref_ptr& leafBone) + { + 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()); + } + + if (child->getName() == leafBoneName) + { + leafBone = child->asTransform()->asMatrixTransform(); + } + + osg::Group* group = child->asGroup(); + if (group) + { + gatherMatchingBones(group, boneChainNames, leafBoneName, boneChain, leafBone); + } + } + } } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 546d72676e..ac975a9c94 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,10 @@ 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, + const std::string& leafBoneName, std::vector>& boneChain, + osg::ref_ptr& leafBone); + private: void updateWater(); From e8e9fdafa291e6e1e9dce83a4bdf4aded9678bc6 Mon Sep 17 00:00:00 2001 From: MaxYari Date: Fri, 21 Feb 2025 16:10:18 +0100 Subject: [PATCH 3/4] Foot IK Proof of concept done --- apps/openmw/CMakeLists.txt | 2 +- apps/openmw/engine.cpp | 26 +-- apps/openmw/mwphysics/physicssystem.cpp | 235 +++++++++++++++++------- apps/openmw/mwphysics/physicssystem.hpp | 6 +- apps/openmw/mwrender/easyik.cpp | 206 +++++++++++++++++++++ apps/openmw/mwrender/easyik.hpp | 42 +++++ 6 files changed, 430 insertions(+), 87 deletions(-) create mode 100644 apps/openmw/mwrender/easyik.cpp create mode 100644 apps/openmw/mwrender/easyik.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 37de0abeab..6bdf9f3a3b 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 2736f339e4..3065827058 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/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 40e54c5023..80bc77a1a3 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,8 @@ #include "../mwworld/player.hpp" #include "../mwrender/bulletdebugdraw.hpp" +#include "../mwrender/easyik.hpp" +#include "../mwrender/renderingmanager.hpp" #include "../mwworld/class.hpp" @@ -746,87 +749,164 @@ namespace MWPhysics 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; - - //// Handling visual vertical actor adjustments and leg IK - auto pos = physicActor->getSimulationPosition(); + auto colliderPos = physicActor->getSimulationPosition(); auto actptr = physicActor->getPtr(); - osg::Vec3 adjustedPos = pos; - // Finding left leg bones + // 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; - osg::ref_ptr leftFootBone; - std::vector leftLegBoneChainNames = { "Bip01 L Thigh", "Bip01 L Calf" }; - std::string leftFootBoneName = "Bip01 L Foot"; - - gatherMatchingBones(actptr.getRefData().getBaseNode()->asGroup(), leftLegBoneChainNames, leftFootBoneName, - leftLegBoneChain, leftFootBone); - - // Finding right leg bones 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; - std::vector rightLegBoneChainNames = { "Bip01 R Thigh", "Bip01 R Calf" }; - std::string rightFootBoneName = "Bip01 R Foot"; - gatherMatchingBones(actptr.getRefData().getBaseNode()->asGroup(), rightLegBoneChainNames, rightFootBoneName, - rightLegBoneChain, rightFootBone); - - // Compute world transformation for left foot bone - osg::Matrix leftFootWorldMatrix; - if (leftFootBone) - leftFootWorldMatrix = osg::computeLocalToWorld(leftFootBone->getParentalNodePaths()[0]); - osg::Vec3f leftFootPos = leftFootBone ? static_cast(leftFootWorldMatrix.getTrans()) : pos; - auto leftRayCast = castRay(leftFootPos, leftFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f), CollisionType_World); - - // Compute world transformation for right foot bone - osg::Matrix rightFootWorldMatrix; - if (rightFootBone) - rightFootWorldMatrix = osg::computeLocalToWorld(rightFootBone->getParentalNodePaths()[0]); - osg::Vec3f rightFootPos = rightFootBone ? static_cast(rightFootWorldMatrix.getTrans()) : pos; - auto rightRayCast - = castRay(rightFootPos, rightFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f), CollisionType_World); - - // Determine the largest raycast distance and adjust the position accordingly - if (leftRayCast.mHit && rightRayCast.mHit) + // Cast rays from actor feet and pull the actor down to the ground + if (physicActor.get()->getOnGround()) { - if (leftRayCast.mHitPos.z() < rightRayCast.mHitPos.z()) + // Finding left leg bones + std::vector leftLegBoneChainNames = { "Bip01 L Thigh", "Bip01 L Calf", "Bip01 L Foot" }; + gatherMatchingBones( + actptr.getRefData().getBaseNode()->asGroup(), leftLegBoneChainNames, leftLegBoneChain); + + // Finding right leg bones + std::vector rightLegBoneChainNames = { "Bip01 R Thigh", "Bip01 R Calf", "Bip01 R Foot" }; + gatherMatchingBones( + actptr.getRefData().getBaseNode()->asGroup(), rightLegBoneChainNames, rightLegBoneChain); + + // Left foot stuff and ray cast + osg::Matrix leftFootWorldMatrix; + 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; + leftRayCast = castRay(leftFootPos, leftFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f), + CollisionType_HeightMap + CollisionType_World); + + // Right foot stuff and ray cast + osg::Matrix rightFootWorldMatrix; + 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; + rightRayCast = castRay(rightFootPos, rightFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f), + CollisionType_HeightMap + CollisionType_World); + + // Determine the largest raycast distance and adjust the position accordingly + if (leftRayCast.mHit && rightRayCast.mHit) { - adjustedPos.z() = leftRayCast.mHitPos.z(); + if (leftRayCast.mHitPos.z() < rightRayCast.mHitPos.z()) + { + desiredPos.z() = leftRayCast.mHitPos.z(); + } + else + { + desiredPos.z() = rightRayCast.mHitPos.z(); + } } - else + else if (leftRayCast.mHit) { - adjustedPos.z() = rightRayCast.mHitPos.z(); + desiredPos.z() = leftRayCast.mHitPos.z(); + } + else if (rightRayCast.mHit) + { + desiredPos.z() = rightRayCast.mHitPos.z(); } } - else if (leftRayCast.mHit) - { - adjustedPos.z() = leftRayCast.mHitPos.z(); - } - else if (rightRayCast.mHit) - { - adjustedPos.z() = rightRayCast.mHitPos.z(); - } - ////////////////////////////////////////////// - // For some reason player and actors supposed to be kept seprately + // Lerp actor Z position to smooth out sudden elevation transitions + // Note: current solution is bad, this smoothing results in actors lagging behind on downslopes, + // ideally only actual sudden jarring changes should be detected and smoothed out, but not slopes. + osg::Vec3 lerpedPos = desiredPos; + lerpedPos.z() = std::lerp(visualPos.z(), desiredPos.z(), mPhysicsDt * armatureZLerpSpeed); + + // For some reason player and actors supposed to be kept separately, so we do that here if (physicActor.get() != player) mActorsPositions.emplace_back(physicActor->getPtr(), physicActor->getSimulationPosition()); - // Finally adjust position based on raytraced data, this will adjust both player and actors - world->moveObject(actptr, adjustedPos, false, false); + // Finally adjust position based on lerped data, this will adjust both player and actors + world->moveObject(actptr, lerpedPos, false, false); + + // At this point actor is moved down (if necessary) under its collider to touch the ground level, one of its + // feet is most likely clipping through the ground, we raycast to detect that and use ik to bend the leg up + + // Detecting righ leg clipping + MWPhysics::RayCastingResult rightRayUpCast; + MWPhysics::RayCastingResult 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); + } + + // IK-fixing the right leg + if (rightRayUpCast.mHit) + { + + auto& debugRender = world->getRenderingManager()->getDebugDrawer(); + /*debugRender.drawCube(rightRayCast.mHitPos, osg::Vec3(10.0, 10.0, 10.0), Debug::colorBlue);*/ + + 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(); + } + } + + // Detecting left leg clipping + 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); + } + + // IK-fixing the left leg + if (leftRayUpCast.mHit) + { + auto& debugRender = world->getRenderingManager()->getDebugDrawer(); + /*debugRender.drawCube(leftRayCast.mHitPos, osg::Vec3(10.0, 10.0, 10.0), Debug::colorBlue);*/ + + 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, adjustedPos, false, false); - } - - if (player != nullptr) - world->moveObject(player->getPtr(), player->getSimulationPosition(), false, false);*/ } void PhysicsSystem::updateAnimatedCollisionShape(const MWWorld::Ptr& object) @@ -1046,8 +1126,7 @@ namespace MWPhysics } void PhysicsSystem::gatherMatchingBones(osg::Group* parentNode, const std::vector& boneChainNames, - const std::string& leafBoneName, std::vector>& boneChain, - osg::ref_ptr& leafBone) + std::vector>& boneChain) { if (!parentNode) return; @@ -1062,16 +1141,30 @@ namespace MWPhysics boneChain.push_back(child->asTransform()->asMatrixTransform()); } - if (child->getName() == leafBoneName) - { - leafBone = child->asTransform()->asMatrixTransform(); - } - osg::Group* group = child->asGroup(); if (group) { - gatherMatchingBones(group, boneChainNames, leafBoneName, boneChain, leafBone); + 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 ac975a9c94..5dc6448343 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -289,8 +289,10 @@ namespace MWPhysics void reportCollision(const btVector3& position, const btVector3& normal); void gatherMatchingBones(osg::Group* parentNode, const std::vector& boneChainNames, - const std::string& leafBoneName, std::vector>& boneChain, - osg::ref_ptr& leafBone); + 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 From a2d77ae1644320ca63422852656d99a7ac27b521 Mon Sep 17 00:00:00 2001 From: MaxYari Date: Fri, 21 Feb 2025 22:08:41 +0100 Subject: [PATCH 4/4] Better slope handling, worse sudden elevation change handling. --- apps/openmw/mwphysics/actor.cpp | 3 +- apps/openmw/mwphysics/actor.hpp | 2 + apps/openmw/mwphysics/physicssystem.cpp | 77 ++++++++++++------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index c8679f97b3..8187ba3254 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -1,4 +1,4 @@ - #include "actor.hpp" +#include "actor.hpp" #include @@ -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 80bc77a1a3..26cdca4974 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -775,82 +775,72 @@ namespace MWPhysics osg::ref_ptr leftFootBone; osg::ref_ptr rightFootBone; - // Cast rays from actor feet and pull the actor down to the ground + float desiredDeltaZ = 0.0f; + if (physicActor.get()->getOnGround()) { - // Finding left leg bones + // 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); - - // Finding right leg bones - std::vector rightLegBoneChainNames = { "Bip01 R Thigh", "Bip01 R Calf", "Bip01 R Foot" }; gatherMatchingBones( actptr.getRefData().getBaseNode()->asGroup(), rightLegBoneChainNames, rightLegBoneChain); - // Left foot stuff and ray cast - osg::Matrix leftFootWorldMatrix; + // 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; - leftRayCast = castRay(leftFootPos, leftFootPos - osg::Vec3f(0.0f, 0.0f, 50.0f), - CollisionType_HeightMap + CollisionType_World); - // Right foot stuff and ray cast - osg::Matrix rightFootWorldMatrix; 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 largest raycast distance and adjust the position accordingly + // Determine the desired delta Z if (leftRayCast.mHit && rightRayCast.mHit) { - if (leftRayCast.mHitPos.z() < rightRayCast.mHitPos.z()) - { - desiredPos.z() = leftRayCast.mHitPos.z(); - } - else - { - desiredPos.z() = rightRayCast.mHitPos.z(); - } + desiredDeltaZ = std::min(leftRayCast.mHitPos.z(), rightRayCast.mHitPos.z()) - colliderPos.z(); } else if (leftRayCast.mHit) { - desiredPos.z() = leftRayCast.mHitPos.z(); + desiredDeltaZ = leftRayCast.mHitPos.z() - colliderPos.z(); } else if (rightRayCast.mHit) { - desiredPos.z() = rightRayCast.mHitPos.z(); + desiredDeltaZ = rightRayCast.mHitPos.z() - colliderPos.z(); } } - // Lerp actor Z position to smooth out sudden elevation transitions - // Note: current solution is bad, this smoothing results in actors lagging behind on downslopes, - // ideally only actual sudden jarring changes should be detected and smoothed out, but not slopes. - osg::Vec3 lerpedPos = desiredPos; - lerpedPos.z() = std::lerp(visualPos.z(), desiredPos.z(), mPhysicsDt * armatureZLerpSpeed); + // Lerp the current delta Z towards the desired delta Z + physicActor->mCurrentDeltaZ + = std::lerp(physicActor->mCurrentDeltaZ, desiredDeltaZ, mPhysicsDt * armatureZLerpSpeed); - // For some reason player and actors supposed to be kept separately, so we do that here + // 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 lerped data, this will adjust both player and actors - world->moveObject(actptr, lerpedPos, false, false); + // Finally adjust position based on the new armature position + world->moveObject(actptr, armaturePos, false, false); - // At this point actor is moved down (if necessary) under its collider to touch the ground level, one of its - // feet is most likely clipping through the ground, we raycast to detect that and use ik to bend the leg up - - // Detecting righ leg clipping - MWPhysics::RayCastingResult rightRayUpCast; - MWPhysics::RayCastingResult leftRayUpCast; + // Casting up from feet to detect clipping + MWPhysics::RayCastingResult rightRayUpCast, leftRayUpCast; if (rightFootBone) { auto rightFootWorldMatrix = osg::computeLocalToWorld(rightFootBone->getParentalNodePaths()[0]); @@ -859,13 +849,12 @@ namespace MWPhysics CollisionType_HeightMap + CollisionType_World); } - // IK-fixing the right leg 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 @@ -881,7 +870,6 @@ namespace MWPhysics } } - // Detecting left leg clipping if (leftFootBone) { auto leftFootWorldMatrix = osg::computeLocalToWorld(leftFootBone->getParentalNodePaths()[0]); @@ -890,12 +878,12 @@ namespace MWPhysics CollisionType_HeightMap + CollisionType_World); } - // IK-fixing the left leg 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), @@ -907,6 +895,15 @@ namespace MWPhysics } } } + + /*for (const auto& [ptr, pos] : mActorsPositions) + { + + world->moveObject(ptr, desiredPos, false, false); + } + + if (player != nullptr) + world->moveObject(player->getPtr(), player->getSimulationPosition(), false, false);*/ } void PhysicsSystem::updateAnimatedCollisionShape(const MWWorld::Ptr& object)