poses
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/build
|
||||
imgui.ini
|
||||
|
||||
@@ -10,7 +10,7 @@ if(NOT CMAKE_BUILD_TYPE)
|
||||
endif()
|
||||
|
||||
find_package(OpenSceneGraph REQUIRED COMPONENTS
|
||||
osgDB osgGA osgUtil osgViewer osgText osgSim)
|
||||
osgDB osgGA osgUtil osgViewer osgText osgSim osgAnimation)
|
||||
find_package(assimp REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
@@ -26,6 +26,9 @@ set(SOURCES
|
||||
src/OrbitManipulator.cpp
|
||||
src/ShaderManager.cpp
|
||||
src/AppConfig.cpp
|
||||
src/SkeletonLoader.cpp
|
||||
src/PoseManager.cpp
|
||||
src/BoneSelector.cpp
|
||||
)
|
||||
|
||||
add_executable(${PROJECT_NAME} ${SOURCES})
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
|
||||
#include "ShaderManager.h"
|
||||
#include "AppConfig.h"
|
||||
#include "PoseManager.h"
|
||||
|
||||
class MorphManager;
|
||||
class ImGuiLayer;
|
||||
class SkeletonLoader;
|
||||
class BoneSelector;
|
||||
|
||||
class Application : public osgGA::GUIEventHandler {
|
||||
public:
|
||||
@@ -33,24 +36,32 @@ public:
|
||||
|
||||
void applyPendingShader(); // called by update callback
|
||||
void applyMorphWeights(); // called by update callback
|
||||
void applyPoseUpdate(); // called by update callback
|
||||
|
||||
private:
|
||||
void setupLighting();
|
||||
void setupGrid();
|
||||
void requestShader(const std::string& mode);
|
||||
void setModelScale(float scale);
|
||||
void setPoseMode(bool enabled);
|
||||
void handleViewportClick(float x, float y);
|
||||
|
||||
osg::ref_ptr<osgViewer::Viewer> m_viewer;
|
||||
osg::ref_ptr<osg::Group> m_sceneRoot;
|
||||
osg::ref_ptr<osg::Group> m_shaderGroup;
|
||||
osg::ref_ptr<osg::Node> m_modelNode;
|
||||
osg::ref_ptr<osg::MatrixTransform> m_modelXform; // scale/transform wrapper
|
||||
osg::ref_ptr<osg::MatrixTransform> m_modelXform;
|
||||
osg::ref_ptr<osg::Group> m_skeletonRoot;
|
||||
|
||||
AppConfig m_config;
|
||||
std::unique_ptr<ShaderManager> m_shaderMgr;
|
||||
std::unique_ptr<MorphManager> m_morphMgr;
|
||||
std::unique_ptr<PoseManager> m_poseMgr;
|
||||
std::unique_ptr<BoneSelector> m_boneSel;
|
||||
std::unique_ptr<ImGuiLayer> m_imguiLayer;
|
||||
|
||||
bool m_poseMode = false;
|
||||
|
||||
std::mutex m_shaderMutex;
|
||||
std::string m_pendingShader;
|
||||
std::string m_currentShader = "toon";
|
||||
|
||||
55
include/BoneSelector.h
Normal file
55
include/BoneSelector.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <osg/ref_ptr>
|
||||
#include <osg/Vec3>
|
||||
#include <osg/Geode>
|
||||
#include <osg/Group>
|
||||
#include <osg/Geometry>
|
||||
#include <osgViewer/Viewer>
|
||||
#include <osgAnimation/Bone>
|
||||
|
||||
class PoseManager;
|
||||
|
||||
class BoneSelector {
|
||||
public:
|
||||
explicit BoneSelector(PoseManager* poseMgr);
|
||||
|
||||
void init(osgViewer::Viewer* viewer, osg::Group* sceneRoot);
|
||||
|
||||
/// Update bone positions each frame. Only rebuilds geometry when
|
||||
/// selection changes — sphere positions update via vertex array dirty().
|
||||
void updateVisualization();
|
||||
|
||||
std::string pick(float mouseX, float mouseY, osgViewer::Viewer* viewer);
|
||||
|
||||
void setSelected(const std::string& boneName);
|
||||
const std::string& selected() const { return m_selected; }
|
||||
|
||||
void setVisible(bool v);
|
||||
bool isVisible() const { return m_visible; }
|
||||
|
||||
private:
|
||||
void buildInitialGeometry(); // called once on init
|
||||
void updatePositions(); // cheap per-frame position update
|
||||
|
||||
PoseManager* m_poseMgr = nullptr;
|
||||
osg::ref_ptr<osg::Group> m_root;
|
||||
osg::ref_ptr<osg::Geode> m_lines;
|
||||
osg::ref_ptr<osg::Geode> m_spheres;
|
||||
|
||||
// Line geometry arrays — updated in place each frame
|
||||
osg::ref_ptr<osg::Vec3Array> m_lineVerts;
|
||||
osg::ref_ptr<osg::Vec4Array> m_lineColors;
|
||||
|
||||
// Sphere positions — one MatrixTransform per bone
|
||||
std::vector<osg::ref_ptr<osg::MatrixTransform>> m_sphereXforms;
|
||||
std::vector<std::string> m_sphereBoneNames;
|
||||
|
||||
std::string m_selected;
|
||||
std::string m_prevSelected; // detect selection changes
|
||||
bool m_visible = true;
|
||||
bool m_geometryBuilt = false;
|
||||
};
|
||||
@@ -7,45 +7,70 @@
|
||||
#include <osgGA/GUIEventAdapter>
|
||||
|
||||
class MorphManager;
|
||||
class PoseManager;
|
||||
class BoneSelector;
|
||||
class AppConfig;
|
||||
|
||||
class ImGuiLayer {
|
||||
public:
|
||||
explicit ImGuiLayer(MorphManager* morphMgr, const AppConfig* cfg);
|
||||
explicit ImGuiLayer(MorphManager* morphMgr,
|
||||
const AppConfig* cfg);
|
||||
~ImGuiLayer();
|
||||
|
||||
void init(osgViewer::Viewer* viewer);
|
||||
bool handleEvent(const osgGA::GUIEventAdapter& ea);
|
||||
|
||||
void renderPanel(); // called by ImGuiDrawCallback each frame
|
||||
void renderPanel();
|
||||
void markGLInitialized();
|
||||
|
||||
// Callbacks set by Application so ImGui can drive app state
|
||||
// Callbacks
|
||||
std::function<void(const std::string&)> onShaderChange;
|
||||
std::function<void()> onShaderReload;
|
||||
std::function<void(float)> onScaleChange;
|
||||
// Pose callbacks
|
||||
std::function<void(bool)> onPoseModeToggle;
|
||||
std::function<void(bool)> onBoneVisToggle;
|
||||
|
||||
// Called by Application each frame so the shader tab shows current state
|
||||
void setCurrentShader(const std::string& s) { m_currentShader = s; }
|
||||
void setInitialScale(float s) { m_scale = s; m_scaleBuf[0] = 0; }
|
||||
|
||||
// Set pose subsystem references so the Pose tab can drive them
|
||||
void setPoseManager(PoseManager* pm) { m_poseMgr = pm; }
|
||||
void setBoneSelector(BoneSelector* bs) { m_boneSel = bs; }
|
||||
|
||||
// Called by Application when the user clicks a bone in the viewport
|
||||
void selectBone(const std::string& name) { m_selectedBone = name; }
|
||||
|
||||
private:
|
||||
void renderMorphTab();
|
||||
void renderShaderTab();
|
||||
void renderTransformTab();
|
||||
void renderPoseTab();
|
||||
void renderBoneTree(const std::string& boneName, int depth);
|
||||
|
||||
MorphManager* m_morphMgr = nullptr;
|
||||
PoseManager* m_poseMgr = nullptr;
|
||||
BoneSelector* m_boneSel = nullptr;
|
||||
const AppConfig* m_cfg = nullptr;
|
||||
osgViewer::Viewer* m_viewer = nullptr;
|
||||
osg::ref_ptr<osg::Camera> m_camera;
|
||||
|
||||
bool m_contextCreated = false;
|
||||
bool m_glInitialized = false;
|
||||
float m_panelWidth = 380.f; // wider default so names are visible
|
||||
float m_panelWidth = 380.f;
|
||||
|
||||
char m_searchBuf[256] = {};
|
||||
char m_boneSearch[256] = {};
|
||||
bool m_showOnlyActive = false;
|
||||
std::string m_currentShader = "toon";
|
||||
float m_scale = 1.0f;
|
||||
char m_scaleBuf[32] = {};
|
||||
|
||||
// Pose tab state
|
||||
bool m_poseEnabled = false;
|
||||
bool m_bonesVisible = true;
|
||||
std::string m_selectedBone;
|
||||
// Euler angles for selected bone (degrees, for display)
|
||||
float m_boneEuler[3] = {};
|
||||
float m_boneTrans[3] = {};
|
||||
};
|
||||
94
include/PoseManager.h
Normal file
94
include/PoseManager.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <osg/ref_ptr>
|
||||
#include <osg/Quat>
|
||||
#include <osg/Vec3>
|
||||
#include <osg/Matrix>
|
||||
#include <osg/NodeVisitor>
|
||||
#include <osgAnimation/Bone>
|
||||
#include <osgAnimation/Skeleton>
|
||||
|
||||
/**
|
||||
* PoseManager
|
||||
* -----------
|
||||
* Owns the current pose as a map of bone name → local rotation + translation
|
||||
* offsets from the bind pose. Applies them to the osgAnimation bone tree
|
||||
* each frame during the update traversal.
|
||||
*
|
||||
* Usage:
|
||||
* // At load time:
|
||||
* mgr.init(skeleton, boneMap);
|
||||
*
|
||||
* // From ImGui / bone gizmo:
|
||||
* mgr.setBoneRotation("Head", osg::Quat(...));
|
||||
*
|
||||
* // In update callback:
|
||||
* mgr.applyPose();
|
||||
*
|
||||
* // Reset:
|
||||
* mgr.resetBone("Head");
|
||||
* mgr.resetAll();
|
||||
*/
|
||||
class PoseManager {
|
||||
public:
|
||||
struct BonePose {
|
||||
osg::Quat rotation = { 0, 0, 0, 1 }; // local rotation delta
|
||||
osg::Vec3 translation = { 0, 0, 0 }; // local translation delta
|
||||
bool modified = false;
|
||||
};
|
||||
|
||||
PoseManager() = default;
|
||||
|
||||
/// Called once after skeleton is loaded.
|
||||
void init(osgAnimation::Skeleton* skeleton,
|
||||
const std::unordered_map<std::string,
|
||||
osg::ref_ptr<osgAnimation::Bone>>& boneMap);
|
||||
|
||||
/// Sorted list of all bone names (for UI tree).
|
||||
const std::vector<std::string>& boneNames() const { return m_boneNames; }
|
||||
|
||||
/// Bone hierarchy as parent name → child names (for tree display).
|
||||
const std::unordered_map<std::string, std::vector<std::string>>&
|
||||
boneChildren() const { return m_children; }
|
||||
|
||||
std::string boneParent(const std::string& name) const;
|
||||
|
||||
// ── Pose setters ──────────────────────────────────────────────────────────
|
||||
void setBoneRotation(const std::string& name, const osg::Quat& q);
|
||||
void setBoneTranslation(const std::string& name, const osg::Vec3& t);
|
||||
void resetBone(const std::string& name);
|
||||
void resetAll();
|
||||
|
||||
// ── Pose getters ──────────────────────────────────────────────────────────
|
||||
const BonePose& getBonePose(const std::string& name) const;
|
||||
osg::Vec3 getBoneWorldPos(const std::string& name) const;
|
||||
|
||||
// ── Per-frame update ──────────────────────────────────────────────────────
|
||||
/// Apply current pose to the osgAnimation bone tree.
|
||||
void applyPose();
|
||||
|
||||
/// Run a skeleton update traversal to refresh world-space bone matrices.
|
||||
/// Call every frame so getBoneWorldPos() returns current positions.
|
||||
void updateSkeletonMatrices();
|
||||
|
||||
bool isInitialized() const { return m_initialized; }
|
||||
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
unsigned int m_traversalCount = 1000;
|
||||
|
||||
osg::ref_ptr<osgAnimation::Skeleton> m_skeleton;
|
||||
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, std::string> m_parents; // child → parent
|
||||
std::unordered_map<std::string, std::vector<std::string>> m_children;
|
||||
std::vector<std::string> m_boneNames; // sorted
|
||||
BonePose m_defaultPose; // returned for unknowns
|
||||
|
||||
void buildHierarchy(osgAnimation::Bone* bone, const std::string& parentName);
|
||||
};
|
||||
93
include/SkeletonLoader.h
Normal file
93
include/SkeletonLoader.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <osg/ref_ptr>
|
||||
#include <osg/Group>
|
||||
#include <osgAnimation/Skeleton>
|
||||
#include <osgAnimation/Bone>
|
||||
#include <osgAnimation/RigGeometry>
|
||||
#include <osgAnimation/MorphGeometry>
|
||||
|
||||
// Forward declarations
|
||||
struct aiScene;
|
||||
struct aiNode;
|
||||
struct aiMesh;
|
||||
|
||||
/**
|
||||
* SkeletonLoader
|
||||
* --------------
|
||||
* Reads the Assimp scene's bone hierarchy and mesh skin weights,
|
||||
* and builds an osgAnimation scene graph:
|
||||
*
|
||||
* osgAnimation::Skeleton
|
||||
* └─ osgAnimation::Bone (Hips)
|
||||
* └─ osgAnimation::Bone (Spine)
|
||||
* └─ ...
|
||||
*
|
||||
* Each skinned aiMesh becomes an osgAnimation::RigGeometry instead
|
||||
* of a plain osg::Geometry, so the GPU handles skinning automatically.
|
||||
*
|
||||
* Usage:
|
||||
* SkeletonLoader loader;
|
||||
* auto result = loader.load(assimpScene, baseDir, morphMgr);
|
||||
* // result.root — attach to scene graph
|
||||
* // result.boneMap — name → Bone* for posing
|
||||
*/
|
||||
class MorphManager;
|
||||
|
||||
class SkeletonLoader {
|
||||
public:
|
||||
struct Result {
|
||||
osg::ref_ptr<osg::Group> root;
|
||||
osg::ref_ptr<osgAnimation::Skeleton> skeleton;
|
||||
std::unordered_map<std::string,
|
||||
osg::ref_ptr<osgAnimation::Bone>> boneMap;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
Result load(const struct aiScene* scene,
|
||||
const std::string& baseDir,
|
||||
MorphManager* morphMgr = nullptr);
|
||||
|
||||
private:
|
||||
// ── Bone hierarchy ────────────────────────────────────────────────────────
|
||||
|
||||
/// Recursively build Bone nodes from the aiNode tree.
|
||||
osg::ref_ptr<osgAnimation::Bone> buildBoneTree(
|
||||
const aiNode* node,
|
||||
const std::unordered_map<std::string, bool>& boneNames,
|
||||
std::unordered_map<std::string,
|
||||
osg::ref_ptr<osgAnimation::Bone>>& boneMap,
|
||||
const osg::Matrix& parentAccum = osg::Matrix::identity());
|
||||
|
||||
/// Find the armature root node (first ancestor whose children are all bones).
|
||||
const aiNode* findArmatureRoot(const aiNode* root,
|
||||
const std::unordered_map<std::string, bool>& boneNames);
|
||||
|
||||
// ── Mesh conversion ───────────────────────────────────────────────────────
|
||||
|
||||
/// Convert a skinned aiMesh to RigGeometry.
|
||||
osg::ref_ptr<osgAnimation::RigGeometry> convertSkinnedMesh(
|
||||
const aiMesh* mesh,
|
||||
const aiScene* scene,
|
||||
const std::string& baseDir,
|
||||
MorphManager* morphMgr,
|
||||
const std::unordered_map<std::string,
|
||||
osg::ref_ptr<osgAnimation::Bone>>& boneMap);
|
||||
|
||||
/// Convert an unskinned aiMesh to plain osg::Geometry (same as before).
|
||||
osg::ref_ptr<osg::Geode> convertStaticMesh(
|
||||
const aiMesh* mesh,
|
||||
const aiScene* scene,
|
||||
const std::string& baseDir,
|
||||
MorphManager* morphMgr);
|
||||
|
||||
/// Shared material/texture setup used by both converters.
|
||||
void applyMaterial(osg::StateSet* ss,
|
||||
const aiMesh* mesh,
|
||||
const aiScene* scene,
|
||||
const std::string& baseDir);
|
||||
};
|
||||
65
logs.txt
Normal file
65
logs.txt
Normal file
@@ -0,0 +1,65 @@
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f36c1ae050 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1ae050 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f36c1ae150 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1ae150 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f374ff0910 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f374ff0910 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f372abe300 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f372abe300 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f372986100 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f372986100 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f36bd40a10 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36bd40a10 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f36bf07ac0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36bf07ac0 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f3755f8410 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f3755f8410 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f37527b890 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37527b890 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f36c180aa0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c180aa0 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f36c1807d0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1807d0 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f37526bc40 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37526bc40 RigTransformSoftware::VertexGroup no bones found
|
||||
RigTransformSoftware::VertexGroup: warning try to normalize a zero sum vertexgroup
|
||||
0x55f37526cb80 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37526cb80 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1ae050 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1ae050 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1ae150 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1ae150 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f374ff0910 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f374ff0910 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f372abe300 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f372abe300 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f372986100 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f372986100 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36bd40a10 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36bd40a10 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36bf07ac0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36bf07ac0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f3755f8410 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f3755f8410 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37527b890 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37527b890 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c180aa0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c180aa0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1807d0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f36c1807d0 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37526bc40 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37526bc40 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37526cb80 RigTransformSoftware::VertexGroup no bones found
|
||||
0x55f37526cb80 RigTransformSoftware::VertexGroup no bones found
|
||||
@@ -5,6 +5,13 @@
|
||||
#include "OrbitManipulator.h"
|
||||
#include "MorphManager.h"
|
||||
#include "AppConfig.h"
|
||||
#include "SkeletonLoader.h"
|
||||
#include "PoseManager.h"
|
||||
#include "BoneSelector.h"
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/scene.h>
|
||||
#include <assimp/postprocess.h>
|
||||
#include <filesystem>
|
||||
#include "ImGuiLayer.h"
|
||||
|
||||
#include <iostream>
|
||||
@@ -15,6 +22,7 @@
|
||||
#include <osg/CullFace>
|
||||
#include <osg/Program>
|
||||
#include <osg/NodeCallback>
|
||||
#include <osg/NodeVisitor>
|
||||
#include <osg/MatrixTransform>
|
||||
#include <osgViewer/ViewerEventHandlers>
|
||||
|
||||
@@ -24,8 +32,9 @@ class AppUpdateCallback : public osg::NodeCallback {
|
||||
public:
|
||||
explicit AppUpdateCallback(Application* app) : m_app(app) {}
|
||||
void operator()(osg::Node* node, osg::NodeVisitor* nv) override {
|
||||
m_app->applyPendingShader(); // shader switches
|
||||
m_app->applyMorphWeights(); // morph deformation — must be in update traversal
|
||||
m_app->applyPendingShader();
|
||||
m_app->applyMorphWeights();
|
||||
m_app->applyPoseUpdate(); // bone posing
|
||||
traverse(node, nv);
|
||||
}
|
||||
private:
|
||||
@@ -37,6 +46,7 @@ private:
|
||||
Application::Application()
|
||||
: m_shaderMgr(std::make_unique<ShaderManager>("assets/shaders"))
|
||||
, m_morphMgr (std::make_unique<MorphManager>())
|
||||
, m_poseMgr (std::make_unique<PoseManager>())
|
||||
{
|
||||
m_config.load();
|
||||
m_imguiLayer = std::make_unique<ImGuiLayer>(m_morphMgr.get(), &m_config);
|
||||
@@ -109,18 +119,61 @@ bool Application::init(int width, int height, const std::string& title) {
|
||||
bool Application::loadModel(const std::string& filepath) {
|
||||
std::cout << "[app] Loading model: " << filepath << "\n";
|
||||
|
||||
// Try skeleton loader (handles FBX with bones)
|
||||
Assimp::Importer importer;
|
||||
constexpr unsigned flags =
|
||||
aiProcess_Triangulate | aiProcess_GenSmoothNormals |
|
||||
aiProcess_SortByPType | aiProcess_ImproveCacheLocality;
|
||||
const aiScene* scene = importer.ReadFile(filepath, flags);
|
||||
|
||||
if (scene && scene->mRootNode) {
|
||||
const std::string baseDir =
|
||||
std::filesystem::path(filepath).parent_path().string();
|
||||
SkeletonLoader skelLoader;
|
||||
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_boneSel = std::make_unique<BoneSelector>(m_poseMgr.get());
|
||||
m_boneSel->init(m_viewer.get(), m_sceneRoot.get());
|
||||
m_imguiLayer->setPoseManager(m_poseMgr.get());
|
||||
m_imguiLayer->setBoneSelector(m_boneSel.get());
|
||||
m_imguiLayer->onPoseModeToggle = [this](bool e) { setPoseMode(e); };
|
||||
m_imguiLayer->onBoneVisToggle = [this](bool v) {
|
||||
if (m_boneSel) m_boneSel->setVisible(v);
|
||||
};
|
||||
// Force a skeleton update traversal immediately so the bone map
|
||||
// is populated before the first draw call. Without this, RigGeometry
|
||||
// tries to skin on frame 0 before bones are linked, collapsing verts.
|
||||
// We run it twice — once to build the bone map, once to propagate.
|
||||
for (int warmup = 0; warmup < 3; ++warmup) {
|
||||
osg::NodeVisitor uv(osg::NodeVisitor::UPDATE_VISITOR,
|
||||
osg::NodeVisitor::TRAVERSE_ALL_CHILDREN);
|
||||
uv.setTraversalNumber(warmup);
|
||||
skelResult.root->accept(uv);
|
||||
}
|
||||
std::cout << "[app] Skeleton warmup done.\n";
|
||||
std::cout << "[app] Skeleton loaded: "
|
||||
<< m_poseMgr->boneNames().size() << " bones.\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to plain loader if skeleton load failed
|
||||
if (!m_modelNode) {
|
||||
ModelLoader loader;
|
||||
m_modelNode = loader.load(filepath, m_morphMgr.get());
|
||||
if (!m_modelNode) return false;
|
||||
std::cout << "[app] Loaded as static mesh.\n";
|
||||
}
|
||||
|
||||
// Wrap model in a transform so scale/position can be adjusted at runtime
|
||||
// Wrap in scale transform
|
||||
m_modelXform = new osg::MatrixTransform;
|
||||
m_modelXform->setName("ModelTransform");
|
||||
|
||||
float initScale = m_config.getFloat("model.scale", 1.0f);
|
||||
m_modelXform->setMatrix(osg::Matrix::scale(initScale, initScale, initScale));
|
||||
m_modelXform->addChild(m_modelNode);
|
||||
m_shaderGroup->addChild(m_modelXform);
|
||||
|
||||
requestShader(m_currentShader);
|
||||
m_viewer->home();
|
||||
|
||||
@@ -149,6 +202,14 @@ bool Application::handle(const osgGA::GUIEventAdapter& ea,
|
||||
// Forward to ImGui first — if it wants the event, don't pass to camera
|
||||
if (m_imguiLayer->handleEvent(ea)) return true;
|
||||
|
||||
// Bone picking on left click in pose mode
|
||||
if (m_poseMode &&
|
||||
ea.getEventType() == osgGA::GUIEventAdapter::PUSH &&
|
||||
ea.getButton() == osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON) {
|
||||
handleViewportClick(ea.getX(), ea.getY());
|
||||
// Don't consume — let camera manipulator also handle it
|
||||
}
|
||||
|
||||
if (ea.getEventType() != osgGA::GUIEventAdapter::KEYDOWN) return false;
|
||||
if (!m_modelNode) return false;
|
||||
|
||||
@@ -191,6 +252,30 @@ void Application::applyMorphWeights() {
|
||||
if (m_morphMgr) m_morphMgr->applyWeights();
|
||||
}
|
||||
|
||||
void Application::setPoseMode(bool enabled) {
|
||||
m_poseMode = enabled;
|
||||
if (m_boneSel) m_boneSel->setVisible(enabled);
|
||||
std::cout << "[app] Pose mode: " << (enabled ? "ON" : "OFF") << "\n";
|
||||
}
|
||||
|
||||
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
|
||||
if (m_poseMode && m_boneSel) m_boneSel->updateVisualization();
|
||||
}
|
||||
|
||||
void Application::handleViewportClick(float x, float y) {
|
||||
if (!m_poseMode || !m_boneSel) return;
|
||||
std::string hit = m_boneSel->pick(x, y, m_viewer.get());
|
||||
if (!hit.empty()) {
|
||||
m_boneSel->setSelected(hit);
|
||||
m_imguiLayer->selectBone(hit);
|
||||
std::cout << "[app] Selected bone: " << hit << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void Application::setModelScale(float scale) {
|
||||
if (m_modelXform) {
|
||||
scale = std::max(0.001f, scale);
|
||||
|
||||
203
src/BoneSelector.cpp
Normal file
203
src/BoneSelector.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
#include "BoneSelector.h"
|
||||
#include "PoseManager.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
#include <osg/Geometry>
|
||||
#include <osg/ShapeDrawable>
|
||||
#include <osg/Shape>
|
||||
#include <osg/LineWidth>
|
||||
#include <osg/StateSet>
|
||||
#include <osg/MatrixTransform>
|
||||
#include <osgUtil/LineSegmentIntersector>
|
||||
#include <osgUtil/IntersectionVisitor>
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
BoneSelector::BoneSelector(PoseManager* poseMgr)
|
||||
: m_poseMgr(poseMgr)
|
||||
{}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void BoneSelector::init(osgViewer::Viewer* viewer, osg::Group* sceneRoot) {
|
||||
(void)viewer;
|
||||
|
||||
m_root = new osg::Group;
|
||||
m_root->setName("BoneOverlay");
|
||||
|
||||
osg::StateSet* ss = m_root->getOrCreateStateSet();
|
||||
ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
|
||||
ss->setMode(GL_DEPTH_TEST,osg::StateAttribute::OFF);
|
||||
ss->setRenderBinDetails(100, "RenderBin");
|
||||
|
||||
sceneRoot->addChild(m_root);
|
||||
// Geometry built lazily on first updateVisualization() call
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void BoneSelector::buildInitialGeometry() {
|
||||
if (!m_poseMgr || !m_poseMgr->isInitialized()) return;
|
||||
|
||||
const auto& names = m_poseMgr->boneNames();
|
||||
|
||||
// ── Lines ─────────────────────────────────────────────────────────────────
|
||||
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));
|
||||
|
||||
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()));
|
||||
lineGeom->getOrCreateStateSet()->setAttribute(new osg::LineWidth(1.5f));
|
||||
|
||||
m_lines = new osg::Geode;
|
||||
m_lines->addDrawable(lineGeom);
|
||||
m_root->addChild(m_lines);
|
||||
|
||||
// ── Spheres — one MatrixTransform per bone ────────────────────────────────
|
||||
// We move them by updating their matrix, not rebuilding the drawables.
|
||||
auto sphereGroup = new osg::Group;
|
||||
m_root->addChild(sphereGroup);
|
||||
|
||||
m_sphereXforms.clear();
|
||||
m_sphereBoneNames.clear();
|
||||
|
||||
for (const auto& name : names) {
|
||||
auto xform = new osg::MatrixTransform;
|
||||
xform->setName(name);
|
||||
xform->setDataVariance(osg::Object::DYNAMIC);
|
||||
|
||||
auto sphere = new osg::ShapeDrawable(new osg::Sphere(osg::Vec3(), 0.012f));
|
||||
sphere->setName(name);
|
||||
sphere->setColor(osg::Vec4(0.3f, 0.9f, 1.f, 0.9f));
|
||||
|
||||
auto geode = new osg::Geode;
|
||||
geode->addDrawable(sphere);
|
||||
geode->setName(name);
|
||||
xform->addChild(geode);
|
||||
|
||||
sphereGroup->addChild(xform);
|
||||
m_sphereXforms.push_back(xform);
|
||||
m_sphereBoneNames.push_back(name);
|
||||
}
|
||||
|
||||
m_geometryBuilt = true;
|
||||
std::cout << "[bones] Built overlay for " << names.size() << " bones.\n";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void BoneSelector::updatePositions() {
|
||||
if (!m_poseMgr) return;
|
||||
// names accessed via m_sphereBoneNames
|
||||
bool selChanged = (m_selected != m_prevSelected);
|
||||
m_prevSelected = m_selected;
|
||||
|
||||
// Update line vertices and sphere transforms
|
||||
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);
|
||||
|
||||
// Move sphere via transform
|
||||
m_sphereXforms[i]->setMatrix(osg::Matrix::translate(pos));
|
||||
|
||||
// Update selection colour if changed
|
||||
if (selChanged) {
|
||||
bool isSel = (name == m_selected);
|
||||
if (auto* geode = dynamic_cast<osg::Geode*>(
|
||||
m_sphereXforms[i]->getChild(0))) {
|
||||
if (auto* sd = dynamic_cast<osg::ShapeDrawable*>(
|
||||
geode->getDrawable(0))) {
|
||||
sd->setColor(isSel
|
||||
? osg::Vec4(1.f, 0.9f, 0.f, 1.f)
|
||||
: osg::Vec4(0.3f, 0.9f, 1.f, 0.9f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
bool isSel = (name == m_selected || parent == m_selected);
|
||||
(*m_lineVerts)[lineIdx] = parentPos;
|
||||
(*m_lineVerts)[lineIdx+1] = pos;
|
||||
osg::Vec4 col = isSel
|
||||
? osg::Vec4(1.f, 0.8f, 0.f, 1.f)
|
||||
: osg::Vec4(0.4f, 0.8f, 1.f, 0.7f);
|
||||
(*m_lineColors)[lineIdx] = col;
|
||||
(*m_lineColors)[lineIdx+1] = col;
|
||||
lineIdx += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Dirty line arrays
|
||||
if (m_lineVerts) m_lineVerts->dirty();
|
||||
if (m_lineColors) m_lineColors->dirty();
|
||||
if (m_lines && m_lines->getNumDrawables() > 0)
|
||||
m_lines->getDrawable(0)->dirtyBound();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void BoneSelector::updateVisualization() {
|
||||
if (!m_poseMgr || !m_poseMgr->isInitialized() || !m_visible) {
|
||||
if (m_root) m_root->setNodeMask(0);
|
||||
return;
|
||||
}
|
||||
m_root->setNodeMask(~0u);
|
||||
|
||||
if (!m_geometryBuilt) buildInitialGeometry();
|
||||
|
||||
updatePositions();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
std::string BoneSelector::pick(float mouseX, float mouseY,
|
||||
osgViewer::Viewer* viewer) {
|
||||
if (!viewer || !m_root) return {};
|
||||
|
||||
osg::ref_ptr<osgUtil::LineSegmentIntersector> intersector =
|
||||
new osgUtil::LineSegmentIntersector(
|
||||
osgUtil::Intersector::WINDOW, mouseX, mouseY);
|
||||
|
||||
osgUtil::IntersectionVisitor iv(intersector);
|
||||
m_root->accept(iv);
|
||||
|
||||
if (!intersector->containsIntersections()) return {};
|
||||
|
||||
const auto& hit = *intersector->getIntersections().begin();
|
||||
|
||||
// Walk node path for named node
|
||||
for (auto it = hit.nodePath.rbegin(); it != hit.nodePath.rend(); ++it) {
|
||||
const std::string& n = (*it)->getName();
|
||||
if (!n.empty() && std::find(m_sphereBoneNames.begin(),
|
||||
m_sphereBoneNames.end(), n)
|
||||
!= m_sphereBoneNames.end())
|
||||
return n;
|
||||
}
|
||||
if (hit.drawable.valid() && !hit.drawable->getName().empty())
|
||||
return hit.drawable->getName();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void BoneSelector::setSelected(const std::string& boneName) {
|
||||
m_selected = boneName;
|
||||
}
|
||||
|
||||
void BoneSelector::setVisible(bool v) {
|
||||
m_visible = v;
|
||||
if (m_root) m_root->setNodeMask(v ? ~0u : 0u);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "ImGuiLayer.h"
|
||||
#include "MorphManager.h"
|
||||
#include "AppConfig.h"
|
||||
#include "PoseManager.h"
|
||||
#include "BoneSelector.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
@@ -266,6 +268,10 @@ void ImGuiLayer::renderPanel() {
|
||||
renderTransformTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Pose")) {
|
||||
renderPoseTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
@@ -413,7 +419,6 @@ void ImGuiLayer::renderShaderTab() {
|
||||
// ── Transform tab ─────────────────────────────────────────────────────────────
|
||||
|
||||
void ImGuiLayer::renderTransformTab() {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Model scale:");
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -483,3 +488,174 @@ void ImGuiLayer::renderTransformTab() {
|
||||
col = (col + 1) % 3;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pose tab ──────────────────────────────────────────────────────────────────
|
||||
|
||||
void ImGuiLayer::renderPoseTab() {
|
||||
ImGui::Spacing();
|
||||
|
||||
// ── Enable toggle ─────────────────────────────────────────────────────────
|
||||
if (ImGui::Checkbox("Enable Pose Mode", &m_poseEnabled)) {
|
||||
if (onPoseModeToggle) onPoseModeToggle(m_poseEnabled);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(?)");
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Click bones in viewport to select.\n"
|
||||
"Adjust rotation/translation below.");
|
||||
|
||||
if (!m_poseEnabled) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Enable pose mode to edit bones.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_poseMgr || !m_poseMgr->isInitialized()) {
|
||||
ImGui::TextDisabled("No skeleton loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Skeleton visibility ───────────────────────────────────────────────────
|
||||
if (ImGui::Checkbox("Show Skeleton", &m_bonesVisible)) {
|
||||
if (onBoneVisToggle) onBoneVisToggle(m_bonesVisible);
|
||||
}
|
||||
|
||||
float bw = ImGui::GetContentRegionAvail().x;
|
||||
if (ImGui::Button("Reset All Bones", ImVec2(bw, 28.f)))
|
||||
m_poseMgr->resetAll();
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// ── Selected bone gizmo ───────────────────────────────────────────────────
|
||||
if (!m_selectedBone.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.85f, 0.3f, 1.f));
|
||||
ImGui::Text("Selected: %s", m_selectedBone.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// pose fetched per-control below
|
||||
|
||||
// Sync euler display from current pose quat
|
||||
// Convert quat → euler (degrees) on first selection or change
|
||||
// (changed flag removed — pose applied immediately via callback)
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted("Rotation (degrees):");
|
||||
ImGui::SetNextItemWidth(-1.f);
|
||||
if (ImGui::SliderFloat3("##rot", m_boneEuler, -180.f, 180.f)) {
|
||||
// Convert euler degrees to quaternion
|
||||
float rx = m_boneEuler[0] * M_PI / 180.f;
|
||||
float ry = m_boneEuler[1] * M_PI / 180.f;
|
||||
float rz = m_boneEuler[2] * M_PI / 180.f;
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted("Translation:");
|
||||
ImGui::SetNextItemWidth(-1.f);
|
||||
if (ImGui::SliderFloat3("##trans", m_boneTrans, -0.5f, 0.5f)) {
|
||||
m_poseMgr->setBoneTranslation(m_selectedBone,
|
||||
osg::Vec3(m_boneTrans[0], m_boneTrans[1], m_boneTrans[2]));
|
||||
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Reset This Bone", ImVec2(-1.f, 26.f))) {
|
||||
m_poseMgr->resetBone(m_selectedBone);
|
||||
memset(m_boneEuler, 0, sizeof(m_boneEuler));
|
||||
memset(m_boneTrans, 0, sizeof(m_boneTrans));
|
||||
}
|
||||
ImGui::Separator();
|
||||
} else {
|
||||
ImGui::TextDisabled("Click a bone in the viewport\nor select from the tree below.");
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// ── Bone search + tree ────────────────────────────────────────────────────
|
||||
ImGui::Spacing();
|
||||
ImGui::SetNextItemWidth(-1.f);
|
||||
ImGui::InputText("##bonesearch", m_boneSearch, sizeof(m_boneSearch));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::BeginChild("##bonetree", ImVec2(0, 0), false);
|
||||
|
||||
std::string filter(m_boneSearch);
|
||||
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
|
||||
|
||||
if (filter.empty()) {
|
||||
// Show as tree — find root bones (no parent)
|
||||
const auto& allNames = m_poseMgr->boneNames();
|
||||
for (const auto& name : allNames) {
|
||||
if (m_poseMgr->boneParent(name).empty())
|
||||
renderBoneTree(name, 0);
|
||||
}
|
||||
} else {
|
||||
// Flat filtered list
|
||||
for (const auto& name : m_poseMgr->boneNames()) {
|
||||
std::string lname = name;
|
||||
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
|
||||
if (lname.find(filter) == std::string::npos) continue;
|
||||
|
||||
bool sel = (name == m_selectedBone);
|
||||
if (sel) ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImVec4(1.f, 0.85f, 0.3f, 1.f));
|
||||
if (ImGui::Selectable(name.c_str(), sel)) {
|
||||
m_selectedBone = name;
|
||||
if (m_boneSel) m_boneSel->setSelected(name);
|
||||
memset(m_boneEuler, 0, sizeof(m_boneEuler));
|
||||
memset(m_boneTrans, 0, sizeof(m_boneTrans));
|
||||
}
|
||||
if (sel) ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void ImGuiLayer::renderBoneTree(const std::string& boneName, int depth) {
|
||||
const auto& childMap = m_poseMgr->boneChildren();
|
||||
auto it = childMap.find(boneName);
|
||||
bool hasChildren = (it != childMap.end() && !it->second.empty());
|
||||
bool sel = (boneName == m_selectedBone);
|
||||
|
||||
// PushID ensures unique IDs even for bones with identical display names
|
||||
ImGui::PushID(boneName.c_str());
|
||||
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow
|
||||
| ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||
if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf;
|
||||
if (sel) flags |= ImGuiTreeNodeFlags_Selected;
|
||||
|
||||
const auto& pose = m_poseMgr->getBonePose(boneName);
|
||||
int colorsPushed = 0;
|
||||
if (pose.modified) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.85f, 0.3f, 1.f));
|
||||
++colorsPushed;
|
||||
}
|
||||
|
||||
bool open = ImGui::TreeNodeEx("##node", flags, "%s", boneName.c_str());
|
||||
|
||||
if (colorsPushed) ImGui::PopStyleColor(colorsPushed);
|
||||
|
||||
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 (open) {
|
||||
if (hasChildren)
|
||||
for (const auto& child : it->second)
|
||||
renderBoneTree(child, depth + 1);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
173
src/PoseManager.cpp
Normal file
173
src/PoseManager.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include "PoseManager.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
#include <osg/NodeVisitor>
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void PoseManager::init(
|
||||
osgAnimation::Skeleton* skeleton,
|
||||
const std::unordered_map<std::string,
|
||||
osg::ref_ptr<osgAnimation::Bone>>& boneMap) {
|
||||
|
||||
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.
|
||||
{
|
||||
osg::NodeVisitor nv(osg::NodeVisitor::UPDATE_VISITOR,
|
||||
osg::NodeVisitor::TRAVERSE_ALL_CHILDREN);
|
||||
nv.setTraversalNumber(1);
|
||||
skeleton->accept(nv);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
if (!hasParentInMap)
|
||||
buildHierarchy(bone.get(), "");
|
||||
}
|
||||
|
||||
m_boneNames.clear();
|
||||
for (auto& [name, _] : boneMap)
|
||||
m_boneNames.push_back(name);
|
||||
std::sort(m_boneNames.begin(), m_boneNames.end());
|
||||
|
||||
m_traversalCount = 1000;
|
||||
m_initialized = true;
|
||||
std::cout << "[pose] PoseManager initialized: " << boneMap.size() << " bones.\n";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void PoseManager::buildHierarchy(osgAnimation::Bone* bone,
|
||||
const std::string& parentName) {
|
||||
std::string name = bone->getName();
|
||||
if (!parentName.empty()) {
|
||||
m_parents[name] = parentName;
|
||||
m_children[parentName].push_back(name);
|
||||
}
|
||||
for (unsigned i = 0; i < bone->getNumChildren(); ++i) {
|
||||
auto* child = dynamic_cast<osgAnimation::Bone*>(bone->getChild(i));
|
||||
if (child) buildHierarchy(child, name);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
std::string PoseManager::boneParent(const std::string& name) const {
|
||||
auto it = m_parents.find(name);
|
||||
return it != m_parents.end() ? it->second : "";
|
||||
}
|
||||
|
||||
void PoseManager::setBoneRotation(const std::string& name, const osg::Quat& q) {
|
||||
auto it = m_poses.find(name);
|
||||
if (it == m_poses.end()) return;
|
||||
it->second.rotation = q;
|
||||
it->second.modified = true;
|
||||
}
|
||||
|
||||
void PoseManager::setBoneTranslation(const std::string& name, const osg::Vec3& t) {
|
||||
auto it = m_poses.find(name);
|
||||
if (it == m_poses.end()) return;
|
||||
it->second.translation = t;
|
||||
it->second.modified = true;
|
||||
}
|
||||
|
||||
void PoseManager::resetBone(const std::string& name) {
|
||||
auto it = m_poses.find(name);
|
||||
if (it == m_poses.end()) return;
|
||||
it->second = BonePose{};
|
||||
}
|
||||
|
||||
void PoseManager::resetAll() {
|
||||
for (auto& [name, pose] : m_poses) pose = BonePose{};
|
||||
}
|
||||
|
||||
const PoseManager::BonePose& PoseManager::getBonePose(const std::string& name) const {
|
||||
auto it = m_poses.find(name);
|
||||
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();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
std::function<void(osgAnimation::Bone*, const osg::Matrix&)> propagate =
|
||||
[&](osgAnimation::Bone* bone, const osg::Matrix& parentSkelMat) {
|
||||
osg::Matrix skelMat = bone->getMatrix() * parentSkelMat;
|
||||
bone->setMatrixInSkeletonSpace(skelMat);
|
||||
for (unsigned i = 0; i < bone->getNumChildren(); ++i) {
|
||||
auto* child = dynamic_cast<osgAnimation::Bone*>(bone->getChild(i));
|
||||
if (child) propagate(child, skelMat);
|
||||
}
|
||||
};
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
osgAnimation::Bone* bone = boneIt->second.get();
|
||||
|
||||
auto bindIt = m_bindMatrices.find(name);
|
||||
if (bindIt == m_bindMatrices.end()) continue;
|
||||
const osg::Matrix& bind = bindIt->second;
|
||||
|
||||
if (!pose.modified) {
|
||||
bone->setMatrix(bind);
|
||||
continue;
|
||||
}
|
||||
|
||||
osg::Vec3 bindTrans = bind.getTrans();
|
||||
osg::Quat bindRot; osg::Vec3 sc; osg::Quat so;
|
||||
bind.decompose(bindTrans, bindRot, sc, so);
|
||||
|
||||
bone->setMatrix(
|
||||
osg::Matrix::scale(sc) *
|
||||
osg::Matrix::rotate(pose.rotation * bindRot) *
|
||||
osg::Matrix::translate(bindTrans + pose.translation));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <osg/CullFace>
|
||||
#include <osg/BlendFunc>
|
||||
#include <osg/NodeVisitor>
|
||||
#include <osgAnimation/RigGeometry>
|
||||
#include <osg/Geode>
|
||||
#include <osg/Drawable>
|
||||
|
||||
@@ -83,11 +84,15 @@ public:
|
||||
osg::Drawable* drawable = geode.getDrawable(i);
|
||||
if (!drawable) continue;
|
||||
|
||||
// Use getStateSet() — don't create one if absent, it means
|
||||
// this drawable truly has no material/texture.
|
||||
osg::StateSet* ss = drawable->getStateSet();
|
||||
// RigGeometry stores material/texture on its SOURCE geometry
|
||||
osg::StateSet* ss = nullptr;
|
||||
if (auto* rig = dynamic_cast<osgAnimation::RigGeometry*>(drawable)) {
|
||||
if (rig->getSourceGeometry())
|
||||
ss = rig->getSourceGeometry()->getStateSet();
|
||||
}
|
||||
if (!ss) ss = drawable->getStateSet();
|
||||
|
||||
if (!ss) {
|
||||
// No StateSet at all — create one and mark no texture
|
||||
ss = drawable->getOrCreateStateSet();
|
||||
ss->addUniform(new osg::Uniform("u_hasTexture", false),
|
||||
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
||||
|
||||
473
src/SkeletonLoader.cpp
Normal file
473
src/SkeletonLoader.cpp
Normal file
@@ -0,0 +1,473 @@
|
||||
#include "SkeletonLoader.h"
|
||||
#include "MorphManager.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <assimp/scene.h>
|
||||
#include <assimp/postprocess.h>
|
||||
|
||||
#include <osg/Geode>
|
||||
#include <osg/Geometry>
|
||||
#include <osg/Material>
|
||||
#include <osg/Texture2D>
|
||||
#include <osg/BlendFunc>
|
||||
#include <osg/MatrixTransform>
|
||||
#include <osgDB/ReadFile>
|
||||
|
||||
#include <osgAnimation/RigGeometry>
|
||||
#include <osgAnimation/MorphGeometry>
|
||||
#include <osgAnimation/UpdateBone>
|
||||
#include <osgAnimation/StackedTransform>
|
||||
#include <osgAnimation/StackedQuaternionElement>
|
||||
#include <osgAnimation/StackedTranslateElement>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
static osg::ref_ptr<osg::Texture2D> loadTex(const std::string& path) {
|
||||
auto tryLoad = [](const std::string& p) -> osg::ref_ptr<osg::Texture2D> {
|
||||
auto img = osgDB::readImageFile(p);
|
||||
if (!img) return {};
|
||||
auto t = new osg::Texture2D(img);
|
||||
t->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
|
||||
t->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
|
||||
t->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR);
|
||||
t->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
|
||||
return t;
|
||||
};
|
||||
if (auto t = tryLoad(path)) return t;
|
||||
auto dot = path.rfind('.');
|
||||
if (dot != std::string::npos)
|
||||
for (auto ext : {".jpg",".jpeg",".png",".tga",".bmp"})
|
||||
if (auto t = tryLoad(path.substr(0, dot) + ext)) return t;
|
||||
return {};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ── applyMaterial ─────────────────────────────────────────────────────────────
|
||||
|
||||
void SkeletonLoader::applyMaterial(osg::StateSet* ss, const aiMesh* mesh,
|
||||
const aiScene* scene, const std::string& baseDir) {
|
||||
if (mesh->mMaterialIndex >= scene->mNumMaterials) return;
|
||||
const aiMaterial* mat = scene->mMaterials[mesh->mMaterialIndex];
|
||||
|
||||
auto osgMat = new osg::Material;
|
||||
aiColor4D col;
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE, col))
|
||||
osgMat->setDiffuse (osg::Material::FRONT_AND_BACK,{col.r,col.g,col.b,col.a});
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_AMBIENT, col))
|
||||
osgMat->setAmbient (osg::Material::FRONT_AND_BACK,{col.r,col.g,col.b,col.a});
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_SPECULAR, col))
|
||||
osgMat->setSpecular(osg::Material::FRONT_AND_BACK,{col.r,col.g,col.b,col.a});
|
||||
float shin = 0;
|
||||
if (AI_SUCCESS == mat->Get(AI_MATKEY_SHININESS, shin))
|
||||
osgMat->setShininess(osg::Material::FRONT_AND_BACK, std::min(shin,128.f));
|
||||
ss->setAttribute(osgMat, osg::StateAttribute::ON);
|
||||
|
||||
if (mat->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
|
||||
aiString tp; mat->GetTexture(aiTextureType_DIFFUSE, 0, &tp);
|
||||
std::string full = baseDir + "/" + tp.C_Str();
|
||||
for (char& c : full) if (c=='\\') c='/';
|
||||
if (auto tex = loadTex(full))
|
||||
ss->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON);
|
||||
}
|
||||
|
||||
float opacity = 1.f; mat->Get(AI_MATKEY_OPACITY, opacity);
|
||||
if (opacity < 1.f) {
|
||||
ss->setMode(GL_BLEND, osg::StateAttribute::ON);
|
||||
ss->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
|
||||
ss->setAttribute(new osg::BlendFunc(osg::BlendFunc::SRC_ALPHA,
|
||||
osg::BlendFunc::ONE_MINUS_SRC_ALPHA));
|
||||
}
|
||||
}
|
||||
|
||||
// ── convertStaticMesh ─────────────────────────────────────────────────────────
|
||||
|
||||
osg::ref_ptr<osg::Geode> SkeletonLoader::convertStaticMesh(
|
||||
const aiMesh* mesh, const aiScene* scene,
|
||||
const std::string& baseDir, MorphManager* morphMgr) {
|
||||
|
||||
auto geode = new osg::Geode;
|
||||
auto geom = new osg::Geometry;
|
||||
|
||||
auto baseVerts = new osg::Vec3Array;
|
||||
baseVerts->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
baseVerts->push_back({mesh->mVertices[i].x,mesh->mVertices[i].y,mesh->mVertices[i].z});
|
||||
geom->setVertexArray(new osg::Vec3Array(*baseVerts));
|
||||
|
||||
osg::ref_ptr<osg::Vec3Array> baseNormals;
|
||||
if (mesh->HasNormals()) {
|
||||
baseNormals = new osg::Vec3Array;
|
||||
baseNormals->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
baseNormals->push_back({mesh->mNormals[i].x,mesh->mNormals[i].y,mesh->mNormals[i].z});
|
||||
geom->setNormalArray(new osg::Vec3Array(*baseNormals), osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
|
||||
if (mesh->HasTextureCoords(0)) {
|
||||
auto uvs = new osg::Vec2Array; uvs->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
uvs->push_back({mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y});
|
||||
geom->setTexCoordArray(0, uvs, osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
|
||||
auto indices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES);
|
||||
for (unsigned f = 0; f < mesh->mNumFaces; ++f) {
|
||||
if (mesh->mFaces[f].mNumIndices != 3) continue;
|
||||
indices->push_back(mesh->mFaces[f].mIndices[0]);
|
||||
indices->push_back(mesh->mFaces[f].mIndices[1]);
|
||||
indices->push_back(mesh->mFaces[f].mIndices[2]);
|
||||
}
|
||||
if (indices->empty()) return geode;
|
||||
geom->addPrimitiveSet(indices);
|
||||
applyMaterial(geom->getOrCreateStateSet(), mesh, scene, baseDir);
|
||||
|
||||
if (morphMgr && mesh->mNumAnimMeshes > 0) {
|
||||
geom->setDataVariance(osg::Object::DYNAMIC);
|
||||
geom->setUseDisplayList(false);
|
||||
geom->setUseVertexBufferObjects(true);
|
||||
if (auto* v = dynamic_cast<osg::Vec3Array*>(geom->getVertexArray()))
|
||||
v->setDataVariance(osg::Object::DYNAMIC);
|
||||
if (auto* n = dynamic_cast<osg::Vec3Array*>(geom->getNormalArray()))
|
||||
n->setDataVariance(osg::Object::DYNAMIC);
|
||||
|
||||
morphMgr->registerMesh(geom, baseVerts, baseNormals);
|
||||
int reg = 0;
|
||||
for (unsigned a = 0; a < mesh->mNumAnimMeshes; ++a) {
|
||||
const aiAnimMesh* am = mesh->mAnimMeshes[a];
|
||||
std::string name = am->mName.C_Str();
|
||||
if (name.empty() || (name.size()>3&&name.front()=='-'&&name.back()=='-')) continue;
|
||||
if (am->mNumVertices != mesh->mNumVertices) continue;
|
||||
auto dv = new osg::Vec3Array; dv->resize(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
(*dv)[i]={am->mVertices[i].x-mesh->mVertices[i].x,
|
||||
am->mVertices[i].y-mesh->mVertices[i].y,
|
||||
am->mVertices[i].z-mesh->mVertices[i].z};
|
||||
osg::ref_ptr<osg::Vec3Array> dn;
|
||||
if (am->mNormals && mesh->HasNormals()) {
|
||||
dn = new osg::Vec3Array; dn->resize(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
(*dn)[i]={am->mNormals[i].x-mesh->mNormals[i].x,
|
||||
am->mNormals[i].y-mesh->mNormals[i].y,
|
||||
am->mNormals[i].z-mesh->mNormals[i].z};
|
||||
}
|
||||
morphMgr->addTarget(geom, name, dv, dn);
|
||||
++reg;
|
||||
}
|
||||
if (reg) std::cout << "[skel] Static \"" << mesh->mName.C_Str()
|
||||
<< "\": " << reg << " morphs\n";
|
||||
}
|
||||
|
||||
geode->addDrawable(geom);
|
||||
return geode;
|
||||
}
|
||||
|
||||
// ── convertSkinnedMesh ────────────────────────────────────────────────────────
|
||||
|
||||
osg::ref_ptr<osgAnimation::RigGeometry> SkeletonLoader::convertSkinnedMesh(
|
||||
const aiMesh* mesh, const aiScene* scene,
|
||||
const std::string& baseDir, MorphManager* morphMgr,
|
||||
const std::unordered_map<std::string,
|
||||
osg::ref_ptr<osgAnimation::Bone>>& boneMap) {
|
||||
|
||||
auto srcGeom = new osg::Geometry;
|
||||
|
||||
auto verts = new osg::Vec3Array; verts->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
verts->push_back({mesh->mVertices[i].x,mesh->mVertices[i].y,mesh->mVertices[i].z});
|
||||
srcGeom->setVertexArray(verts);
|
||||
|
||||
if (mesh->HasNormals()) {
|
||||
auto norms = new osg::Vec3Array; norms->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
norms->push_back({mesh->mNormals[i].x,mesh->mNormals[i].y,mesh->mNormals[i].z});
|
||||
srcGeom->setNormalArray(norms, osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
|
||||
if (mesh->HasTextureCoords(0)) {
|
||||
auto uvs = new osg::Vec2Array; uvs->reserve(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
uvs->push_back({mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y});
|
||||
srcGeom->setTexCoordArray(0, uvs, osg::Array::BIND_PER_VERTEX);
|
||||
}
|
||||
|
||||
auto indices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES);
|
||||
for (unsigned f = 0; f < mesh->mNumFaces; ++f) {
|
||||
if (mesh->mFaces[f].mNumIndices != 3) continue;
|
||||
indices->push_back(mesh->mFaces[f].mIndices[0]);
|
||||
indices->push_back(mesh->mFaces[f].mIndices[1]);
|
||||
indices->push_back(mesh->mFaces[f].mIndices[2]);
|
||||
}
|
||||
if (!indices->empty()) srcGeom->addPrimitiveSet(indices);
|
||||
applyMaterial(srcGeom->getOrCreateStateSet(), mesh, scene, baseDir);
|
||||
|
||||
// Build vertex influence map
|
||||
auto* vim = new osgAnimation::VertexInfluenceMap;
|
||||
int mappedBones = 0, skippedBones = 0;
|
||||
for (unsigned b = 0; b < mesh->mNumBones; ++b) {
|
||||
const aiBone* bone = mesh->mBones[b];
|
||||
std::string bname = bone->mName.C_Str();
|
||||
if (!boneMap.count(bname)) { ++skippedBones; continue; }
|
||||
++mappedBones;
|
||||
auto& vi = (*vim)[bname];
|
||||
vi.setName(bname);
|
||||
for (unsigned w = 0; w < bone->mNumWeights; ++w)
|
||||
vi.push_back(osgAnimation::VertexIndexWeight(
|
||||
bone->mWeights[w].mVertexId, bone->mWeights[w].mWeight));
|
||||
}
|
||||
|
||||
if (skippedBones > 0)
|
||||
std::cout << "[skel] Influence map \"" << mesh->mName.C_Str()
|
||||
<< "\": " << mappedBones << " mapped, "
|
||||
<< skippedBones << " skipped (not in boneMap)\n";
|
||||
|
||||
auto rig = new osgAnimation::RigGeometry;
|
||||
rig->setName(mesh->mName.C_Str());
|
||||
rig->setSourceGeometry(srcGeom);
|
||||
rig->setInfluenceMap(vim);
|
||||
rig->setDataVariance(osg::Object::DYNAMIC);
|
||||
rig->setUseDisplayList(false);
|
||||
rig->setUseVertexBufferObjects(true);
|
||||
|
||||
// Register morphs on the source geometry if present
|
||||
// RigGeometry deforms the source geometry, so morphs must target it
|
||||
if (morphMgr && mesh->mNumAnimMeshes > 0) {
|
||||
auto baseVerts = dynamic_cast<osg::Vec3Array*>(srcGeom->getVertexArray());
|
||||
auto baseNorms = dynamic_cast<osg::Vec3Array*>(srcGeom->getNormalArray());
|
||||
if (baseVerts) {
|
||||
srcGeom->setDataVariance(osg::Object::DYNAMIC);
|
||||
morphMgr->registerMesh(srcGeom, baseVerts,
|
||||
osg::ref_ptr<osg::Vec3Array>(baseNorms));
|
||||
int reg = 0;
|
||||
for (unsigned a = 0; a < mesh->mNumAnimMeshes; ++a) {
|
||||
const aiAnimMesh* am = mesh->mAnimMeshes[a];
|
||||
std::string mname = am->mName.C_Str();
|
||||
if (mname.empty() || (mname.size()>3 &&
|
||||
mname.front()=='-' && mname.back()=='-')) continue;
|
||||
if (am->mNumVertices != mesh->mNumVertices) continue;
|
||||
auto dv = new osg::Vec3Array; dv->resize(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
(*dv)[i] = {am->mVertices[i].x - mesh->mVertices[i].x,
|
||||
am->mVertices[i].y - mesh->mVertices[i].y,
|
||||
am->mVertices[i].z - mesh->mVertices[i].z};
|
||||
osg::ref_ptr<osg::Vec3Array> dn;
|
||||
if (am->mNormals && mesh->HasNormals()) {
|
||||
dn = new osg::Vec3Array; dn->resize(mesh->mNumVertices);
|
||||
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
||||
(*dn)[i] = {am->mNormals[i].x - mesh->mNormals[i].x,
|
||||
am->mNormals[i].y - mesh->mNormals[i].y,
|
||||
am->mNormals[i].z - mesh->mNormals[i].z};
|
||||
}
|
||||
morphMgr->addTarget(srcGeom, mname, dv, dn);
|
||||
++reg;
|
||||
}
|
||||
if (reg) std::cout << "[skel] Rig \"" << mesh->mName.C_Str()
|
||||
<< "\" morphs=" << reg << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[skel] Rig \"" << mesh->mName.C_Str()
|
||||
<< "\" bones=" << mesh->mNumBones
|
||||
<< " verts=" << mesh->mNumVertices << "\n";
|
||||
return rig;
|
||||
}
|
||||
|
||||
// ── buildBoneTree ─────────────────────────────────────────────────────────────
|
||||
|
||||
osg::ref_ptr<osgAnimation::Bone> SkeletonLoader::buildBoneTree(
|
||||
const aiNode* node,
|
||||
const std::unordered_map<std::string, bool>& boneNames,
|
||||
std::unordered_map<std::string,
|
||||
osg::ref_ptr<osgAnimation::Bone>>& boneMap,
|
||||
const osg::Matrix& parentAccum) {
|
||||
|
||||
std::string name = node->mName.C_Str();
|
||||
|
||||
// Assimp injects "$AssimpFbx$_Translation" / "$AssimpFbx$_PreRotation"
|
||||
// helper nodes between real bone nodes when reading FBX files.
|
||||
// We must pass through them transparently to reach the real bone children.
|
||||
if (!boneNames.count(name)) {
|
||||
// "$AssimpFbx$_Translation", "$AssimpFbx$_PreRotation" etc.
|
||||
// These helpers carry transform data that belongs to the next real bone.
|
||||
// Accumulate them and pass the combined matrix down to the real bone.
|
||||
// Chain this helper's transform onto the incoming accumulation
|
||||
osg::Matrix accumulated = parentAccum * aiToOsg(node->mTransformation);
|
||||
for (unsigned i = 0; i < node->mNumChildren; ++i) {
|
||||
auto child = buildBoneTree(node->mChildren[i], boneNames, boneMap,
|
||||
accumulated);
|
||||
if (child) return child;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Bail if already built (prevents double-add via multiple helper paths)
|
||||
if (boneMap.count(name)) return boneMap[name];
|
||||
|
||||
// Combine accumulated parent helper transforms with this bone's own transform
|
||||
osg::Matrix localMat = parentAccum * aiToOsg(node->mTransformation);
|
||||
auto bone = new osgAnimation::Bone(name);
|
||||
bone->setMatrix(localMat);
|
||||
|
||||
auto updateCB = new osgAnimation::UpdateBone(name);
|
||||
osg::Vec3 t = localMat.getTrans();
|
||||
osg::Quat q; osg::Vec3 sc; osg::Quat so;
|
||||
localMat.decompose(t, q, sc, so);
|
||||
auto& stack = updateCB->getStackedTransforms();
|
||||
stack.push_back(new osgAnimation::StackedTranslateElement("translate", t));
|
||||
stack.push_back(new osgAnimation::StackedQuaternionElement("quaternion", q));
|
||||
bone->setUpdateCallback(updateCB);
|
||||
|
||||
boneMap[name] = bone; // register BEFORE recursing to break cycles
|
||||
|
||||
for (unsigned i = 0; i < node->mNumChildren; ++i) {
|
||||
auto child = buildBoneTree(node->mChildren[i], boneNames, boneMap,
|
||||
osg::Matrix::identity());
|
||||
if (child && child.get() != bone && child->getNumParents() == 0)
|
||||
bone->addChild(child);
|
||||
}
|
||||
return bone;
|
||||
}
|
||||
|
||||
// ── findArmatureRoot ──────────────────────────────────────────────────────────
|
||||
|
||||
const aiNode* SkeletonLoader::findArmatureRoot(
|
||||
const aiNode* node,
|
||||
const std::unordered_map<std::string, bool>& boneNames) {
|
||||
if (boneNames.count(node->mName.C_Str())) return node;
|
||||
for (unsigned i = 0; i < node->mNumChildren; ++i)
|
||||
if (auto f = findArmatureRoot(node->mChildren[i], boneNames)) return f;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ── load ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
SkeletonLoader::Result SkeletonLoader::load(
|
||||
const aiScene* scene, const std::string& baseDir, MorphManager* morphMgr) {
|
||||
|
||||
Result result;
|
||||
|
||||
// Collect bone names + inverse bind matrices
|
||||
std::unordered_map<std::string, bool> boneNames;
|
||||
std::unordered_map<std::string, osg::Matrix> invBindMatrices;
|
||||
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];
|
||||
boneNames[bone->mName.C_Str()] = true;
|
||||
if (!invBindMatrices.count(bone->mName.C_Str()))
|
||||
invBindMatrices[bone->mName.C_Str()] = aiToOsg(bone->mOffsetMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
if (boneNames.empty()) { std::cerr << "[skel] No bones.\n"; return result; }
|
||||
std::cout << "[skel] Found " << boneNames.size() << " bones.\n";
|
||||
|
||||
// Build skeleton
|
||||
auto skeleton = new osgAnimation::Skeleton;
|
||||
skeleton->setName("Skeleton");
|
||||
skeleton->setDefaultUpdateCallback();
|
||||
result.skeleton = skeleton;
|
||||
|
||||
const aiNode* armRoot = findArmatureRoot(scene->mRootNode, boneNames);
|
||||
if (!armRoot) { std::cerr << "[skel] No armature root.\n"; return result; }
|
||||
|
||||
auto rootBone = buildBoneTree(armRoot, boneNames, result.boneMap,
|
||||
osg::Matrix::identity());
|
||||
if (!rootBone) { std::cerr << "[skel] Bone tree failed.\n"; return result; }
|
||||
skeleton->addChild(rootBone);
|
||||
std::cout << "[skel] Bone tree: " << result.boneMap.size() << " bones.\n";
|
||||
|
||||
// Recompute inverse bind matrices from our actual bone world positions.
|
||||
// We can't use Assimp's mOffsetMatrix directly because we accumulated
|
||||
// helper node transforms differently, so the bone world matrices we built
|
||||
// don't match what Assimp computed. Instead, walk the bone tree to get
|
||||
// each bone's world matrix and invert it.
|
||||
{
|
||||
// Build world matrices by walking the bone hierarchy
|
||||
std::unordered_map<std::string, osg::Matrix> worldMatrices;
|
||||
std::function<void(osgAnimation::Bone*, const osg::Matrix&)> computeWorld =
|
||||
[&](osgAnimation::Bone* bone, const osg::Matrix& parentWorld) {
|
||||
osg::Matrix world = bone->getMatrix() * parentWorld;
|
||||
worldMatrices[bone->getName()] = world;
|
||||
for (unsigned i = 0; i < bone->getNumChildren(); ++i) {
|
||||
auto* child = dynamic_cast<osgAnimation::Bone*>(bone->getChild(i));
|
||||
if (child) computeWorld(child, world);
|
||||
}
|
||||
};
|
||||
computeWorld(rootBone.get(), osg::Matrix::identity());
|
||||
|
||||
for (auto& [name, bone] : result.boneMap) {
|
||||
auto it = worldMatrices.find(name);
|
||||
if (it != worldMatrices.end()) {
|
||||
osg::Matrix invBind = osg::Matrix::inverse(it->second);
|
||||
bone->setInvBindMatrixInSkeletonSpace(invBind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scene graph ───────────────────────────────────────────────────────────
|
||||
// RigGeometry finds its Skeleton by walking UP the parent path.
|
||||
// So all mesh nodes must be DESCENDANTS of the Skeleton node.
|
||||
//
|
||||
// result.root
|
||||
// skeleton <- ancestor of all RigGeometry
|
||||
// meshGroup <- mesh nodes go here
|
||||
// Body xform -> Geode -> RigGeometry
|
||||
// ...
|
||||
// rootBone (already added above)
|
||||
|
||||
result.root = new osg::Group;
|
||||
result.root->setName("SkinnedModel");
|
||||
result.root->addChild(skeleton);
|
||||
|
||||
// The skeleton populates its internal bone map automatically during
|
||||
// the first update traversal via its default update callback.
|
||||
|
||||
auto meshGroup = new osg::Group;
|
||||
meshGroup->setName("Meshes");
|
||||
skeleton->addChild(meshGroup); // meshes are children of skeleton
|
||||
|
||||
std::function<void(const aiNode*, osg::Group*)> walkNodes =
|
||||
[&](const aiNode* node, osg::Group* parent) {
|
||||
if (boneNames.count(node->mName.C_Str())) return;
|
||||
|
||||
osg::Matrix nodeMat = aiToOsg(node->mTransformation);
|
||||
auto xform = new osg::MatrixTransform(nodeMat);
|
||||
xform->setName(node->mName.C_Str());
|
||||
|
||||
for (unsigned mi = 0; mi < node->mNumMeshes; ++mi) {
|
||||
const aiMesh* mesh = scene->mMeshes[node->mMeshes[mi]];
|
||||
auto geode = new osg::Geode;
|
||||
if (mesh->mNumBones > 0) {
|
||||
auto rig = convertSkinnedMesh(mesh, scene, baseDir,
|
||||
morphMgr, result.boneMap);
|
||||
if (rig) geode->addDrawable(rig);
|
||||
} else {
|
||||
auto sg = convertStaticMesh(mesh, scene, baseDir, morphMgr);
|
||||
for (unsigned d = 0; d < sg->getNumDrawables(); ++d)
|
||||
geode->addDrawable(sg->getDrawable(d));
|
||||
}
|
||||
if (geode->getNumDrawables() > 0) xform->addChild(geode);
|
||||
}
|
||||
|
||||
parent->addChild(xform);
|
||||
for (unsigned ci = 0; ci < node->mNumChildren; ++ci)
|
||||
walkNodes(node->mChildren[ci], xform);
|
||||
};
|
||||
|
||||
for (unsigned n = 0; n < scene->mRootNode->mNumChildren; ++n)
|
||||
walkNodes(scene->mRootNode->mChildren[n], meshGroup);
|
||||
|
||||
result.valid = true;
|
||||
std::cout << "[skel] Done. " << result.boneMap.size() << " bones.\n";
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user