From e934d436ed21d37a8602ff174696b67a45de1ef5 Mon Sep 17 00:00:00 2001 From: owner Date: Mon, 16 Mar 2026 03:49:43 -0400 Subject: [PATCH] poses 2 --- include/BoneSelector.h | 7 +++++ include/PoseManager.h | 6 ++-- include/SkeletonLoader.h | 1 + src/Application.cpp | 16 ++++++++-- src/BoneSelector.cpp | 29 ++++++++++++------ src/ImGuiLayer.cpp | 50 +++++++++++++++++++++++++++--- src/PoseManager.cpp | 66 ++++++++++++++++++---------------------- src/SkeletonLoader.cpp | 37 ++++++++++++++++++++-- 8 files changed, 153 insertions(+), 59 deletions(-) diff --git a/include/BoneSelector.h b/include/BoneSelector.h index 8f1c986..14148ec 100644 --- a/include/BoneSelector.h +++ b/include/BoneSelector.h @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include #include #include @@ -31,6 +33,9 @@ public: void setVisible(bool v); bool isVisible() const { return m_visible; } + /// Set the model world matrix so bone positions are transformed correctly. + void setModelMatrix(const osg::Matrix& m) { m_modelMatrix = m; } + private: void buildInitialGeometry(); // called once on init void updatePositions(); // cheap per-frame position update @@ -43,6 +48,7 @@ private: // Line geometry arrays — updated in place each frame osg::ref_ptr m_lineVerts; osg::ref_ptr m_lineColors; + osg::ref_ptr m_lineDrawArrays; // Sphere positions — one MatrixTransform per bone std::vector> m_sphereXforms; @@ -52,4 +58,5 @@ private: std::string m_prevSelected; // detect selection changes bool m_visible = true; bool m_geometryBuilt = false; + osg::Matrix m_modelMatrix; // model world transform for coordinate conversion }; \ No newline at end of file diff --git a/include/PoseManager.h b/include/PoseManager.h index 6aebef3..93a766a 100644 --- a/include/PoseManager.h +++ b/include/PoseManager.h @@ -46,7 +46,8 @@ public: /// Called once after skeleton is loaded. void init(osgAnimation::Skeleton* skeleton, const std::unordered_map>& boneMap); + osg::ref_ptr>& boneMap, + const std::unordered_map& bindPositions = {}); /// Sorted list of all bone names (for UI tree). const std::vector& boneNames() const { return m_boneNames; } @@ -84,7 +85,8 @@ private: osg::ref_ptr m_skeleton; std::unordered_map> m_boneMap; std::unordered_map m_poses; - std::unordered_map m_bindMatrices; // local bind pose + std::unordered_map m_bindMatrices; // local bind pose + std::unordered_map m_bindWorldPositions; // world pos from offset matrix std::unordered_map m_parents; // child → parent std::unordered_map> m_children; std::vector m_boneNames; // sorted diff --git a/include/SkeletonLoader.h b/include/SkeletonLoader.h index 132bf8f..47ae180 100644 --- a/include/SkeletonLoader.h +++ b/include/SkeletonLoader.h @@ -45,6 +45,7 @@ public: osg::ref_ptr skeleton; std::unordered_map> boneMap; + std::unordered_map bindPositions; // world pos from inverted offset bool valid = false; }; diff --git a/src/Application.cpp b/src/Application.cpp index 6d66437..d699cee 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -133,7 +133,8 @@ bool Application::loadModel(const std::string& filepath) { auto skelResult = skelLoader.load(scene, baseDir, m_morphMgr.get()); if (skelResult.valid) { m_modelNode = skelResult.root; - m_poseMgr->init(skelResult.skeleton.get(), skelResult.boneMap); + m_poseMgr->init(skelResult.skeleton.get(), skelResult.boneMap, + skelResult.bindPositions); m_boneSel = std::make_unique(m_poseMgr.get()); m_boneSel->init(m_viewer.get(), m_sceneRoot.get()); m_imguiLayer->setPoseManager(m_poseMgr.get()); @@ -260,9 +261,18 @@ void Application::setPoseMode(bool enabled) { void Application::applyPoseUpdate() { if (!m_poseMgr || !m_poseMgr->isInitialized()) return; - // applyPose sets local matrices, then updateSkeletonMatrices propagates FK if (m_poseMode) m_poseMgr->applyPose(); - m_poseMgr->updateSkeletonMatrices(); // always keep world positions fresh + m_poseMgr->updateSkeletonMatrices(); + + // Keep BoneSelector in sync with the model's current world transform + // so overlay positions match the rendered mesh position/scale + if (m_boneSel && m_modelXform) { + // Compute full world matrix by walking up parent transforms + osg::Matrix worldMat = m_modelXform->getMatrix(); + // m_modelXform -> m_shaderGroup -> m_sceneRoot (no further transforms) + m_boneSel->setModelMatrix(worldMat); + } + if (m_poseMode && m_boneSel) m_boneSel->updateVisualization(); } diff --git a/src/BoneSelector.cpp b/src/BoneSelector.cpp index bb50080..2c9d253 100644 --- a/src/BoneSelector.cpp +++ b/src/BoneSelector.cpp @@ -44,17 +44,24 @@ void BoneSelector::buildInitialGeometry() { const auto& names = m_poseMgr->boneNames(); // ── Lines ───────────────────────────────────────────────────────────────── + // Count bones that have parents (those get a line) + int lineCount = 0; + for (const auto& name : names) + if (!m_poseMgr->boneParent(name).empty()) ++lineCount; + m_lineVerts = new osg::Vec3Array; m_lineColors = new osg::Vec4Array; - m_lineVerts->resize(names.size() * 2, osg::Vec3()); - m_lineColors->resize(names.size() * 2, osg::Vec4(0.4f,0.8f,1.f,0.7f)); + m_lineVerts->resize(lineCount * 2, osg::Vec3()); + m_lineColors->resize(lineCount * 2, osg::Vec4(0.4f,0.8f,1.f,0.7f)); auto lineGeom = new osg::Geometry; lineGeom->setDataVariance(osg::Object::DYNAMIC); lineGeom->setVertexArray(m_lineVerts); lineGeom->setColorArray(m_lineColors, osg::Array::BIND_PER_VERTEX); - lineGeom->addPrimitiveSet( - new osg::DrawArrays(osg::PrimitiveSet::LINES, 0, m_lineVerts->size())); + // Store DrawArrays so we can update its count + m_lineDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES, + 0, lineCount * 2); + lineGeom->addPrimitiveSet(m_lineDrawArrays); lineGeom->getOrCreateStateSet()->setAttribute(new osg::LineWidth(1.5f)); m_lines = new osg::Geode; @@ -104,9 +111,10 @@ void BoneSelector::updatePositions() { int lineIdx = 0; for (unsigned i = 0; i < m_sphereBoneNames.size() && i < m_sphereXforms.size(); ++i) { const std::string& name = m_sphereBoneNames[i]; - osg::Vec3 pos = m_poseMgr->getBoneWorldPos(name); + // Transform from skeleton space to world space via model matrix + osg::Vec3 skelPos = m_poseMgr->getBoneWorldPos(name); + osg::Vec3 pos = skelPos * m_modelMatrix; - // Move sphere via transform m_sphereXforms[i]->setMatrix(osg::Matrix::translate(pos)); // Update selection colour if changed @@ -126,7 +134,8 @@ void BoneSelector::updatePositions() { // Line: parent → this bone std::string parent = m_poseMgr->boneParent(name); if (!parent.empty() && lineIdx + 1 < (int)m_lineVerts->size()) { - osg::Vec3 parentPos = m_poseMgr->getBoneWorldPos(parent); + osg::Vec3 parentSkelPos = m_poseMgr->getBoneWorldPos(parent); + osg::Vec3 parentPos = parentSkelPos * m_modelMatrix; bool isSel = (name == m_selected || parent == m_selected); (*m_lineVerts)[lineIdx] = parentPos; (*m_lineVerts)[lineIdx+1] = pos; @@ -139,11 +148,13 @@ void BoneSelector::updatePositions() { } } - // Dirty line arrays + // Dirty line arrays so OSG re-uploads them if (m_lineVerts) m_lineVerts->dirty(); if (m_lineColors) m_lineColors->dirty(); - if (m_lines && m_lines->getNumDrawables() > 0) + if (m_lines && m_lines->getNumDrawables() > 0) { m_lines->getDrawable(0)->dirtyBound(); + m_lines->getDrawable(0)->dirtyDisplayList(); + } } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/ImGuiLayer.cpp b/src/ImGuiLayer.cpp index 8e7ad1c..f869a0b 100644 --- a/src/ImGuiLayer.cpp +++ b/src/ImGuiLayer.cpp @@ -540,7 +540,7 @@ void ImGuiLayer::renderPoseTab() { // (changed flag removed — pose applied immediately via callback) ImGui::Spacing(); - ImGui::TextUnformatted("Rotation (degrees):"); + ImGui::TextUnformatted("Rotation (bone-local degrees):"); ImGui::SetNextItemWidth(-1.f); if (ImGui::SliderFloat3("##rot", m_boneEuler, -180.f, 180.f)) { // Convert euler degrees to quaternion @@ -550,14 +550,15 @@ void ImGuiLayer::renderPoseTab() { osg::Quat qx(rx, osg::Vec3(1,0,0)); osg::Quat qy(ry, osg::Vec3(0,1,0)); osg::Quat qz(rz, osg::Vec3(0,0,1)); - m_poseMgr->setBoneRotation(m_selectedBone, qz * qy * qx); + // Local-space: Rx * Ry * Rz — rotates around bone's own axes + m_poseMgr->setBoneRotation(m_selectedBone, qx * qy * qz); } ImGui::Spacing(); ImGui::TextUnformatted("Translation:"); ImGui::SetNextItemWidth(-1.f); - if (ImGui::SliderFloat3("##trans", m_boneTrans, -0.5f, 0.5f)) { + if (ImGui::SliderFloat3("##trans", m_boneTrans, -20.f, 20.f)) { m_poseMgr->setBoneTranslation(m_selectedBone, osg::Vec3(m_boneTrans[0], m_boneTrans[1], m_boneTrans[2])); @@ -606,9 +607,29 @@ void ImGuiLayer::renderPoseTab() { if (ImGui::Selectable(name.c_str(), sel)) { m_selectedBone = name; if (m_boneSel) m_boneSel->setSelected(name); + // Sync sliders to current pose + if (m_poseMgr) { + const auto& p = m_poseMgr->getBonePose(m_selectedBone); + if (p.modified) { + // Extract euler angles from quaternion via rotation matrix + osg::Matrix rotMat; rotMat.makeRotate(p.rotation); + // Extract XYZ euler: atan2 from rotation matrix elements + float ex = std::atan2( rotMat(2,1), rotMat(2,2)); + float ey = std::atan2(-rotMat(2,0), + std::sqrt(rotMat(2,1)*rotMat(2,1)+rotMat(2,2)*rotMat(2,2))); + float ez = std::atan2( rotMat(1,0), rotMat(0,0)); + m_boneEuler[0] = ex * 180.f / (float)M_PI; + m_boneEuler[1] = ey * 180.f / (float)M_PI; + m_boneEuler[2] = ez * 180.f / (float)M_PI; + m_boneTrans[0] = p.translation.x(); + m_boneTrans[1] = p.translation.y(); + m_boneTrans[2] = p.translation.z(); + } else { memset(m_boneEuler, 0, sizeof(m_boneEuler)); memset(m_boneTrans, 0, sizeof(m_boneTrans)); } + } + } if (sel) ImGui::PopStyleColor(); } } @@ -646,8 +667,27 @@ void ImGuiLayer::renderBoneTree(const std::string& boneName, int depth) { if (ImGui::IsItemClicked()) { m_selectedBone = boneName; if (m_boneSel) m_boneSel->setSelected(boneName); - memset(m_boneEuler, 0, sizeof(m_boneEuler)); - memset(m_boneTrans, 0, sizeof(m_boneTrans)); + if (m_poseMgr) { + const auto& p = m_poseMgr->getBonePose(boneName); + if (p.modified) { + // Extract euler angles from quaternion via rotation matrix + osg::Matrix rotMat; rotMat.makeRotate(p.rotation); + // Extract XYZ euler: atan2 from rotation matrix elements + float ex = std::atan2( rotMat(2,1), rotMat(2,2)); + float ey = std::atan2(-rotMat(2,0), + std::sqrt(rotMat(2,1)*rotMat(2,1)+rotMat(2,2)*rotMat(2,2))); + float ez = std::atan2( rotMat(1,0), rotMat(0,0)); + m_boneEuler[0] = ex * 180.f / (float)M_PI; + m_boneEuler[1] = ey * 180.f / (float)M_PI; + m_boneEuler[2] = ez * 180.f / (float)M_PI; + m_boneTrans[0] = p.translation.x(); + m_boneTrans[1] = p.translation.y(); + m_boneTrans[2] = p.translation.z(); + } else { + memset(m_boneEuler, 0, sizeof(m_boneEuler)); + memset(m_boneTrans, 0, sizeof(m_boneTrans)); + } + } } if (open) { diff --git a/src/PoseManager.cpp b/src/PoseManager.cpp index 6786b9e..41351b6 100644 --- a/src/PoseManager.cpp +++ b/src/PoseManager.cpp @@ -9,13 +9,13 @@ void PoseManager::init( osgAnimation::Skeleton* skeleton, const std::unordered_map>& boneMap) { + osg::ref_ptr>& boneMap, + const std::unordered_map& bindPositions) { m_skeleton = skeleton; m_boneMap = boneMap; - // Run one update traversal so all bones get their initial matrices computed - // from their StackedTransform, then we capture those as bind matrices. + // Run one update traversal to get initial bone matrices { osg::NodeVisitor nv(osg::NodeVisitor::UPDATE_VISITOR, osg::NodeVisitor::TRAVERSE_ALL_CHILDREN); @@ -24,36 +24,28 @@ void PoseManager::init( } for (auto& [name, bone] : boneMap) { - // Capture bind pose AFTER the first update so it reflects the - // fully-composed local matrix from StackedTransform m_bindMatrices[name] = bone->getMatrix(); m_poses[name] = BonePose{}; - - // Remove the UpdateBone callback permanently — we drive all bone - // matrices manually from now on. This prevents the callback from - // ever overwriting our pose values. - bone->setUpdateCallback(nullptr); + bone->setUpdateCallback(nullptr); // we drive bones manually } // Build hierarchy from root bones only for (auto& [name, bone] : boneMap) { bool hasParentInMap = false; for (unsigned i = 0; i < bone->getNumParents(); ++i) { - auto* parent = dynamic_cast(bone->getParent(i)); - if (parent && boneMap.count(parent->getName())) { - hasParentInMap = true; - break; - } + auto* p = dynamic_cast(bone->getParent(i)); + if (p && boneMap.count(p->getName())) { hasParentInMap = true; break; } } - if (!hasParentInMap) - buildHierarchy(bone.get(), ""); + if (!hasParentInMap) buildHierarchy(bone.get(), ""); } m_boneNames.clear(); - for (auto& [name, _] : boneMap) - m_boneNames.push_back(name); + for (auto& [name, _] : boneMap) m_boneNames.push_back(name); std::sort(m_boneNames.begin(), m_boneNames.end()); + // Store Assimp bind positions as authoritative overlay positions at rest + m_bindWorldPositions = bindPositions; + m_traversalCount = 1000; m_initialized = true; std::cout << "[pose] PoseManager initialized: " << boneMap.size() << " bones.\n"; @@ -62,7 +54,7 @@ void PoseManager::init( // ───────────────────────────────────────────────────────────────────────────── void PoseManager::buildHierarchy(osgAnimation::Bone* bone, - const std::string& parentName) { + const std::string& parentName) { std::string name = bone->getName(); if (!parentName.empty()) { m_parents[name] = parentName; @@ -74,8 +66,6 @@ void PoseManager::buildHierarchy(osgAnimation::Bone* bone, } } -// ───────────────────────────────────────────────────────────────────────────── - std::string PoseManager::boneParent(const std::string& name) const { auto it = m_parents.find(name); return it != m_parents.end() ? it->second : ""; @@ -110,23 +100,24 @@ const PoseManager::BonePose& PoseManager::getBonePose(const std::string& name) c return it != m_poses.end() ? it->second : m_defaultPose; } +// ───────────────────────────────────────────────────────────────────────────── + osg::Vec3 PoseManager::getBoneWorldPos(const std::string& name) const { - auto it = m_boneMap.find(name); - if (it == m_boneMap.end()) return {}; - return it->second->getMatrixInSkeletonSpace().getTrans(); + // Use Assimp bind positions — correct at rest, matched to Blender + auto it = m_bindWorldPositions.find(name); + if (it != m_bindWorldPositions.end()) return it->second; + return {}; } // ───────────────────────────────────────────────────────────────────────────── void PoseManager::updateSkeletonMatrices() { - // Since all UpdateBone callbacks are removed, we manually propagate - // bone matrices through the hierarchy by walking parent→child. if (!m_initialized || !m_skeleton) return; - // Walk the skeleton tree and compute skeleton-space matrices + // Propagate FK: compute skeleton-space matrices for OSG skinning std::function propagate = - [&](osgAnimation::Bone* bone, const osg::Matrix& parentSkelMat) { - osg::Matrix skelMat = bone->getMatrix() * parentSkelMat; + [&](osgAnimation::Bone* bone, const osg::Matrix& parentMat) { + osg::Matrix skelMat = bone->getMatrix() * parentMat; bone->setMatrixInSkeletonSpace(skelMat); for (unsigned i = 0; i < bone->getNumChildren(); ++i) { auto* child = dynamic_cast(bone->getChild(i)); @@ -134,7 +125,6 @@ void PoseManager::updateSkeletonMatrices() { } }; - // Skeleton's own matrix is identity by default for (unsigned i = 0; i < m_skeleton->getNumChildren(); ++i) { auto* root = dynamic_cast(m_skeleton->getChild(i)); if (root) propagate(root, m_skeleton->getMatrix()); @@ -146,7 +136,6 @@ void PoseManager::updateSkeletonMatrices() { void PoseManager::applyPose() { if (!m_initialized) return; - // Set each bone's local matrix: bind pose + pose delta for (auto& [name, pose] : m_poses) { auto boneIt = m_boneMap.find(name); if (boneIt == m_boneMap.end()) continue; @@ -161,13 +150,16 @@ void PoseManager::applyPose() { continue; } - osg::Vec3 bindTrans = bind.getTrans(); - osg::Quat bindRot; osg::Vec3 sc; osg::Quat so; + osg::Vec3 bindTrans, sc; osg::Quat bindRot, so; bind.decompose(bindTrans, bindRot, sc, so); + // Apply pose rotation in bone-local space: bindRot * poseRot + osg::Quat newRot = bindRot * pose.rotation; + osg::Vec3 newTrans = bindTrans + pose.translation; + + // Build clean TRS matrix — no manual scale baking which causes shear bone->setMatrix( - osg::Matrix::scale(sc) * - osg::Matrix::rotate(pose.rotation * bindRot) * - osg::Matrix::translate(bindTrans + pose.translation)); + osg::Matrix::rotate(newRot) * + osg::Matrix::translate(newTrans)); } } \ No newline at end of file diff --git a/src/SkeletonLoader.cpp b/src/SkeletonLoader.cpp index 47c5c87..abbfa37 100644 --- a/src/SkeletonLoader.cpp +++ b/src/SkeletonLoader.cpp @@ -48,8 +48,24 @@ static osg::ref_ptr loadTex(const std::string& path) { } static osg::Matrix aiToOsg(const aiMatrix4x4& m) { - return osg::Matrix(m.a1,m.b1,m.c1,m.d1, m.a2,m.b2,m.c2,m.d2, - m.a3,m.b3,m.c3,m.d3, m.a4,m.b4,m.c4,m.d4); + // Assimp: row-major, row[i] = (a,b,c,d)[i], translation in col 4 (a4,b4,c4) + // OSG Matrix(a,b,c,...) fills row-by-row, translation in row 3 (indices [12],[13],[14]) + // Transpose is needed to convert between the two conventions. + return osg::Matrix( + m.a1, m.b1, m.c1, m.d1, + m.a2, m.b2, m.c2, m.d2, + m.a3, m.b3, m.c3, m.d3, + m.a4, m.b4, m.c4, m.d4); +} + +// Correctly transposed version for inverse bind matrices +// (offset matrix goes mesh→bone, needs proper convention) +static osg::Matrix aiToOsgTransposed(const aiMatrix4x4& m) { + return osg::Matrix( + m.a1, m.a2, m.a3, m.a4, + m.b1, m.b2, m.b3, m.b4, + m.c1, m.c2, m.c3, m.c4, + m.d1, m.d2, m.d3, m.d4); } // ── applyMaterial ───────────────────────────────────────────────────────────── @@ -364,13 +380,28 @@ SkeletonLoader::Result SkeletonLoader::load( const aiBone* bone = mesh->mBones[b]; boneNames[bone->mName.C_Str()] = true; if (!invBindMatrices.count(bone->mName.C_Str())) - invBindMatrices[bone->mName.C_Str()] = aiToOsg(bone->mOffsetMatrix); + invBindMatrices[bone->mName.C_Str()] = aiToOsgTransposed(bone->mOffsetMatrix); } } if (boneNames.empty()) { std::cerr << "[skel] No bones.\n"; return result; } std::cout << "[skel] Found " << boneNames.size() << " bones.\n"; + // Invert offset matrices in Assimp space to get bind-pose world positions. + // This must be done BEFORE any OSG conversion to avoid matrix convention issues. + for (unsigned m = 0; m < scene->mNumMeshes; ++m) { + const aiMesh* mesh = scene->mMeshes[m]; + for (unsigned b = 0; b < mesh->mNumBones; ++b) { + const aiBone* bone = mesh->mBones[b]; + std::string name = bone->mName.C_Str(); + if (result.bindPositions.count(name)) continue; + aiMatrix4x4 inv = bone->mOffsetMatrix; + inv.Inverse(); + // Translation is in (a4, b4, c4) in Assimp row-major convention + result.bindPositions[name] = osg::Vec3(inv.a4, inv.b4, inv.c4); + } + } + // Build skeleton auto skeleton = new osgAnimation::Skeleton; skeleton->setName("Skeleton");