This commit is contained in:
2026-03-16 03:49:43 -04:00
parent 60873c50b2
commit e934d436ed
8 changed files with 153 additions and 59 deletions

View File

@@ -5,7 +5,9 @@
#include <osg/ref_ptr>
#include <osg/Vec3>
#include <osg/Matrix>
#include <osg/Geode>
#include <osg/PrimitiveSet>
#include <osg/Group>
#include <osg/Geometry>
#include <osgViewer/Viewer>
@@ -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<osg::Vec3Array> m_lineVerts;
osg::ref_ptr<osg::Vec4Array> m_lineColors;
osg::ref_ptr<osg::DrawArrays> m_lineDrawArrays;
// Sphere positions — one MatrixTransform per bone
std::vector<osg::ref_ptr<osg::MatrixTransform>> 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
};

View File

@@ -46,7 +46,8 @@ public:
/// Called once after skeleton is loaded.
void init(osgAnimation::Skeleton* skeleton,
const std::unordered_map<std::string,
osg::ref_ptr<osgAnimation::Bone>>& boneMap);
osg::ref_ptr<osgAnimation::Bone>>& boneMap,
const std::unordered_map<std::string, osg::Vec3>& bindPositions = {});
/// Sorted list of all bone names (for UI tree).
const std::vector<std::string>& boneNames() const { return m_boneNames; }
@@ -85,6 +86,7 @@ private:
std::unordered_map<std::string, osg::ref_ptr<osgAnimation::Bone>> m_boneMap;
std::unordered_map<std::string, BonePose> m_poses;
std::unordered_map<std::string, osg::Matrix> m_bindMatrices; // local bind pose
std::unordered_map<std::string, osg::Vec3> m_bindWorldPositions; // world pos from offset matrix
std::unordered_map<std::string, std::string> m_parents; // child → parent
std::unordered_map<std::string, std::vector<std::string>> m_children;
std::vector<std::string> m_boneNames; // sorted

View File

@@ -45,6 +45,7 @@ public:
osg::ref_ptr<osgAnimation::Skeleton> skeleton;
std::unordered_map<std::string,
osg::ref_ptr<osgAnimation::Bone>> boneMap;
std::unordered_map<std::string, osg::Vec3> bindPositions; // world pos from inverted offset
bool valid = false;
};

View File

@@ -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<BoneSelector>(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();
}

View File

@@ -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();
}
}
// ─────────────────────────────────────────────────────────────────────────────

View File

@@ -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,9 +667,28 @@ void ImGuiLayer::renderBoneTree(const std::string& boneName, int depth) {
if (ImGui::IsItemClicked()) {
m_selectedBone = boneName;
if (m_boneSel) m_boneSel->setSelected(boneName);
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) {
if (hasChildren)

View File

@@ -9,13 +9,13 @@
void PoseManager::init(
osgAnimation::Skeleton* skeleton,
const std::unordered_map<std::string,
osg::ref_ptr<osgAnimation::Bone>>& boneMap) {
osg::ref_ptr<osgAnimation::Bone>>& boneMap,
const std::unordered_map<std::string, osg::Vec3>& 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<osgAnimation::Bone*>(bone->getParent(i));
if (parent && boneMap.count(parent->getName())) {
hasParentInMap = true;
break;
auto* p = dynamic_cast<osgAnimation::Bone*>(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";
@@ -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<void(osgAnimation::Bone*, const osg::Matrix&)> 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<osgAnimation::Bone*>(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<osgAnimation::Bone*>(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));
}
}

View File

@@ -48,8 +48,24 @@ static osg::ref_ptr<osg::Texture2D> 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");