poses
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/build
|
/build
|
||||||
|
imgui.ini
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ if(NOT CMAKE_BUILD_TYPE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(OpenSceneGraph REQUIRED COMPONENTS
|
find_package(OpenSceneGraph REQUIRED COMPONENTS
|
||||||
osgDB osgGA osgUtil osgViewer osgText osgSim)
|
osgDB osgGA osgUtil osgViewer osgText osgSim osgAnimation)
|
||||||
find_package(assimp REQUIRED)
|
find_package(assimp REQUIRED)
|
||||||
find_package(OpenGL REQUIRED)
|
find_package(OpenGL REQUIRED)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
@@ -26,6 +26,9 @@ set(SOURCES
|
|||||||
src/OrbitManipulator.cpp
|
src/OrbitManipulator.cpp
|
||||||
src/ShaderManager.cpp
|
src/ShaderManager.cpp
|
||||||
src/AppConfig.cpp
|
src/AppConfig.cpp
|
||||||
|
src/SkeletonLoader.cpp
|
||||||
|
src/PoseManager.cpp
|
||||||
|
src/BoneSelector.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(${PROJECT_NAME} ${SOURCES})
|
add_executable(${PROJECT_NAME} ${SOURCES})
|
||||||
|
|||||||
@@ -14,9 +14,12 @@
|
|||||||
|
|
||||||
#include "ShaderManager.h"
|
#include "ShaderManager.h"
|
||||||
#include "AppConfig.h"
|
#include "AppConfig.h"
|
||||||
|
#include "PoseManager.h"
|
||||||
|
|
||||||
class MorphManager;
|
class MorphManager;
|
||||||
class ImGuiLayer;
|
class ImGuiLayer;
|
||||||
|
class SkeletonLoader;
|
||||||
|
class BoneSelector;
|
||||||
|
|
||||||
class Application : public osgGA::GUIEventHandler {
|
class Application : public osgGA::GUIEventHandler {
|
||||||
public:
|
public:
|
||||||
@@ -33,27 +36,35 @@ public:
|
|||||||
|
|
||||||
void applyPendingShader(); // called by update callback
|
void applyPendingShader(); // called by update callback
|
||||||
void applyMorphWeights(); // called by update callback
|
void applyMorphWeights(); // called by update callback
|
||||||
|
void applyPoseUpdate(); // called by update callback
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupLighting();
|
void setupLighting();
|
||||||
void setupGrid();
|
void setupGrid();
|
||||||
void requestShader(const std::string& mode);
|
void requestShader(const std::string& mode);
|
||||||
void setModelScale(float scale);
|
void setModelScale(float scale);
|
||||||
|
void setPoseMode(bool enabled);
|
||||||
|
void handleViewportClick(float x, float y);
|
||||||
|
|
||||||
osg::ref_ptr<osgViewer::Viewer> m_viewer;
|
osg::ref_ptr<osgViewer::Viewer> m_viewer;
|
||||||
osg::ref_ptr<osg::Group> m_sceneRoot;
|
osg::ref_ptr<osg::Group> m_sceneRoot;
|
||||||
osg::ref_ptr<osg::Group> m_shaderGroup;
|
osg::ref_ptr<osg::Group> m_shaderGroup;
|
||||||
osg::ref_ptr<osg::Node> m_modelNode;
|
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;
|
AppConfig m_config;
|
||||||
std::unique_ptr<ShaderManager> m_shaderMgr;
|
std::unique_ptr<ShaderManager> m_shaderMgr;
|
||||||
std::unique_ptr<MorphManager> m_morphMgr;
|
std::unique_ptr<MorphManager> m_morphMgr;
|
||||||
|
std::unique_ptr<PoseManager> m_poseMgr;
|
||||||
|
std::unique_ptr<BoneSelector> m_boneSel;
|
||||||
std::unique_ptr<ImGuiLayer> m_imguiLayer;
|
std::unique_ptr<ImGuiLayer> m_imguiLayer;
|
||||||
|
|
||||||
|
bool m_poseMode = false;
|
||||||
|
|
||||||
std::mutex m_shaderMutex;
|
std::mutex m_shaderMutex;
|
||||||
std::string m_pendingShader;
|
std::string m_pendingShader;
|
||||||
std::string m_currentShader = "toon";
|
std::string m_currentShader = "toon";
|
||||||
bool m_shaderDirty = false;
|
bool m_shaderDirty = false;
|
||||||
bool m_reloadShaders = false;
|
bool m_reloadShaders = false;
|
||||||
};
|
};
|
||||||
|
|||||||
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>
|
#include <osgGA/GUIEventAdapter>
|
||||||
|
|
||||||
class MorphManager;
|
class MorphManager;
|
||||||
|
class PoseManager;
|
||||||
|
class BoneSelector;
|
||||||
class AppConfig;
|
class AppConfig;
|
||||||
|
|
||||||
class ImGuiLayer {
|
class ImGuiLayer {
|
||||||
public:
|
public:
|
||||||
explicit ImGuiLayer(MorphManager* morphMgr, const AppConfig* cfg);
|
explicit ImGuiLayer(MorphManager* morphMgr,
|
||||||
|
const AppConfig* cfg);
|
||||||
~ImGuiLayer();
|
~ImGuiLayer();
|
||||||
|
|
||||||
void init(osgViewer::Viewer* viewer);
|
void init(osgViewer::Viewer* viewer);
|
||||||
bool handleEvent(const osgGA::GUIEventAdapter& ea);
|
bool handleEvent(const osgGA::GUIEventAdapter& ea);
|
||||||
|
|
||||||
void renderPanel(); // called by ImGuiDrawCallback each frame
|
void renderPanel();
|
||||||
void markGLInitialized();
|
void markGLInitialized();
|
||||||
|
|
||||||
// Callbacks set by Application so ImGui can drive app state
|
// Callbacks
|
||||||
std::function<void(const std::string&)> onShaderChange;
|
std::function<void(const std::string&)> onShaderChange;
|
||||||
std::function<void()> onShaderReload;
|
std::function<void()> onShaderReload;
|
||||||
std::function<void(float)> onScaleChange;
|
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 setCurrentShader(const std::string& s) { m_currentShader = s; }
|
||||||
void setInitialScale(float s) { m_scale = s; m_scaleBuf[0] = 0; }
|
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:
|
private:
|
||||||
void renderMorphTab();
|
void renderMorphTab();
|
||||||
void renderShaderTab();
|
void renderShaderTab();
|
||||||
void renderTransformTab();
|
void renderTransformTab();
|
||||||
|
void renderPoseTab();
|
||||||
|
void renderBoneTree(const std::string& boneName, int depth);
|
||||||
|
|
||||||
MorphManager* m_morphMgr = nullptr;
|
MorphManager* m_morphMgr = nullptr;
|
||||||
|
PoseManager* m_poseMgr = nullptr;
|
||||||
|
BoneSelector* m_boneSel = nullptr;
|
||||||
const AppConfig* m_cfg = nullptr;
|
const AppConfig* m_cfg = nullptr;
|
||||||
osgViewer::Viewer* m_viewer = nullptr;
|
osgViewer::Viewer* m_viewer = nullptr;
|
||||||
osg::ref_ptr<osg::Camera> m_camera;
|
osg::ref_ptr<osg::Camera> m_camera;
|
||||||
|
|
||||||
bool m_contextCreated = false;
|
bool m_contextCreated = false;
|
||||||
bool m_glInitialized = 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_searchBuf[256] = {};
|
||||||
bool m_showOnlyActive = false;
|
char m_boneSearch[256] = {};
|
||||||
|
bool m_showOnlyActive = false;
|
||||||
std::string m_currentShader = "toon";
|
std::string m_currentShader = "toon";
|
||||||
float m_scale = 1.0f;
|
float m_scale = 1.0f;
|
||||||
char m_scaleBuf[32] = {};
|
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 "OrbitManipulator.h"
|
||||||
#include "MorphManager.h"
|
#include "MorphManager.h"
|
||||||
#include "AppConfig.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 "ImGuiLayer.h"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -15,6 +22,7 @@
|
|||||||
#include <osg/CullFace>
|
#include <osg/CullFace>
|
||||||
#include <osg/Program>
|
#include <osg/Program>
|
||||||
#include <osg/NodeCallback>
|
#include <osg/NodeCallback>
|
||||||
|
#include <osg/NodeVisitor>
|
||||||
#include <osg/MatrixTransform>
|
#include <osg/MatrixTransform>
|
||||||
#include <osgViewer/ViewerEventHandlers>
|
#include <osgViewer/ViewerEventHandlers>
|
||||||
|
|
||||||
@@ -24,8 +32,9 @@ class AppUpdateCallback : public osg::NodeCallback {
|
|||||||
public:
|
public:
|
||||||
explicit AppUpdateCallback(Application* app) : m_app(app) {}
|
explicit AppUpdateCallback(Application* app) : m_app(app) {}
|
||||||
void operator()(osg::Node* node, osg::NodeVisitor* nv) override {
|
void operator()(osg::Node* node, osg::NodeVisitor* nv) override {
|
||||||
m_app->applyPendingShader(); // shader switches
|
m_app->applyPendingShader();
|
||||||
m_app->applyMorphWeights(); // morph deformation — must be in update traversal
|
m_app->applyMorphWeights();
|
||||||
|
m_app->applyPoseUpdate(); // bone posing
|
||||||
traverse(node, nv);
|
traverse(node, nv);
|
||||||
}
|
}
|
||||||
private:
|
private:
|
||||||
@@ -37,6 +46,7 @@ private:
|
|||||||
Application::Application()
|
Application::Application()
|
||||||
: m_shaderMgr(std::make_unique<ShaderManager>("assets/shaders"))
|
: m_shaderMgr(std::make_unique<ShaderManager>("assets/shaders"))
|
||||||
, m_morphMgr (std::make_unique<MorphManager>())
|
, m_morphMgr (std::make_unique<MorphManager>())
|
||||||
|
, m_poseMgr (std::make_unique<PoseManager>())
|
||||||
{
|
{
|
||||||
m_config.load();
|
m_config.load();
|
||||||
m_imguiLayer = std::make_unique<ImGuiLayer>(m_morphMgr.get(), &m_config);
|
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) {
|
bool Application::loadModel(const std::string& filepath) {
|
||||||
std::cout << "[app] Loading model: " << filepath << "\n";
|
std::cout << "[app] Loading model: " << filepath << "\n";
|
||||||
|
|
||||||
ModelLoader loader;
|
// Try skeleton loader (handles FBX with bones)
|
||||||
m_modelNode = loader.load(filepath, m_morphMgr.get());
|
Assimp::Importer importer;
|
||||||
if (!m_modelNode) return false;
|
constexpr unsigned flags =
|
||||||
|
aiProcess_Triangulate | aiProcess_GenSmoothNormals |
|
||||||
|
aiProcess_SortByPType | aiProcess_ImproveCacheLocality;
|
||||||
|
const aiScene* scene = importer.ReadFile(filepath, flags);
|
||||||
|
|
||||||
// Wrap model in a transform so scale/position can be adjusted at runtime
|
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 in scale transform
|
||||||
m_modelXform = new osg::MatrixTransform;
|
m_modelXform = new osg::MatrixTransform;
|
||||||
m_modelXform->setName("ModelTransform");
|
m_modelXform->setName("ModelTransform");
|
||||||
|
|
||||||
float initScale = m_config.getFloat("model.scale", 1.0f);
|
float initScale = m_config.getFloat("model.scale", 1.0f);
|
||||||
m_modelXform->setMatrix(osg::Matrix::scale(initScale, initScale, initScale));
|
m_modelXform->setMatrix(osg::Matrix::scale(initScale, initScale, initScale));
|
||||||
m_modelXform->addChild(m_modelNode);
|
m_modelXform->addChild(m_modelNode);
|
||||||
m_shaderGroup->addChild(m_modelXform);
|
m_shaderGroup->addChild(m_modelXform);
|
||||||
|
|
||||||
requestShader(m_currentShader);
|
requestShader(m_currentShader);
|
||||||
m_viewer->home();
|
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
|
// Forward to ImGui first — if it wants the event, don't pass to camera
|
||||||
if (m_imguiLayer->handleEvent(ea)) return true;
|
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 (ea.getEventType() != osgGA::GUIEventAdapter::KEYDOWN) return false;
|
||||||
if (!m_modelNode) return false;
|
if (!m_modelNode) return false;
|
||||||
|
|
||||||
@@ -191,6 +252,30 @@ void Application::applyMorphWeights() {
|
|||||||
if (m_morphMgr) m_morphMgr->applyWeights();
|
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) {
|
void Application::setModelScale(float scale) {
|
||||||
if (m_modelXform) {
|
if (m_modelXform) {
|
||||||
scale = std::max(0.001f, scale);
|
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 "ImGuiLayer.h"
|
||||||
#include "MorphManager.h"
|
#include "MorphManager.h"
|
||||||
#include "AppConfig.h"
|
#include "AppConfig.h"
|
||||||
|
#include "PoseManager.h"
|
||||||
|
#include "BoneSelector.h"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -266,6 +268,10 @@ void ImGuiLayer::renderPanel() {
|
|||||||
renderTransformTab();
|
renderTransformTab();
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
if (ImGui::BeginTabItem("Pose")) {
|
||||||
|
renderPoseTab();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
ImGui::EndTabBar();
|
ImGui::EndTabBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +419,6 @@ void ImGuiLayer::renderShaderTab() {
|
|||||||
// ── Transform tab ─────────────────────────────────────────────────────────────
|
// ── Transform tab ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void ImGuiLayer::renderTransformTab() {
|
void ImGuiLayer::renderTransformTab() {
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::TextDisabled("Model scale:");
|
ImGui::TextDisabled("Model scale:");
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
@@ -482,4 +487,175 @@ void ImGuiLayer::renderTransformTab() {
|
|||||||
if (active) ImGui::PopStyleColor();
|
if (active) ImGui::PopStyleColor();
|
||||||
col = (col + 1) % 3;
|
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/CullFace>
|
||||||
#include <osg/BlendFunc>
|
#include <osg/BlendFunc>
|
||||||
#include <osg/NodeVisitor>
|
#include <osg/NodeVisitor>
|
||||||
|
#include <osgAnimation/RigGeometry>
|
||||||
#include <osg/Geode>
|
#include <osg/Geode>
|
||||||
#include <osg/Drawable>
|
#include <osg/Drawable>
|
||||||
|
|
||||||
@@ -83,11 +84,15 @@ public:
|
|||||||
osg::Drawable* drawable = geode.getDrawable(i);
|
osg::Drawable* drawable = geode.getDrawable(i);
|
||||||
if (!drawable) continue;
|
if (!drawable) continue;
|
||||||
|
|
||||||
// Use getStateSet() — don't create one if absent, it means
|
// RigGeometry stores material/texture on its SOURCE geometry
|
||||||
// this drawable truly has no material/texture.
|
osg::StateSet* ss = nullptr;
|
||||||
osg::StateSet* ss = drawable->getStateSet();
|
if (auto* rig = dynamic_cast<osgAnimation::RigGeometry*>(drawable)) {
|
||||||
|
if (rig->getSourceGeometry())
|
||||||
|
ss = rig->getSourceGeometry()->getStateSet();
|
||||||
|
}
|
||||||
|
if (!ss) ss = drawable->getStateSet();
|
||||||
|
|
||||||
if (!ss) {
|
if (!ss) {
|
||||||
// No StateSet at all — create one and mark no texture
|
|
||||||
ss = drawable->getOrCreateStateSet();
|
ss = drawable->getOrCreateStateSet();
|
||||||
ss->addUniform(new osg::Uniform("u_hasTexture", false),
|
ss->addUniform(new osg::Uniform("u_hasTexture", false),
|
||||||
osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
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