504 lines
23 KiB
C++
504 lines
23 KiB
C++
#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) {
|
|
// Assimp: row-major, row[i] = (a,b,c,d)[i], translation in col 4 (a4,b4,c4)
|
|
// OSG Matrix(a,b,c,...) fills row-by-row, translation in row 3 (indices [12],[13],[14])
|
|
// Transpose is needed to convert between the two conventions.
|
|
return osg::Matrix(
|
|
m.a1, m.b1, m.c1, m.d1,
|
|
m.a2, m.b2, m.c2, m.d2,
|
|
m.a3, m.b3, m.c3, m.d3,
|
|
m.a4, m.b4, m.c4, m.d4);
|
|
}
|
|
|
|
// Correctly transposed version for inverse bind matrices
|
|
// (offset matrix goes mesh→bone, needs proper convention)
|
|
static osg::Matrix aiToOsgTransposed(const aiMatrix4x4& m) {
|
|
return osg::Matrix(
|
|
m.a1, m.a2, m.a3, m.a4,
|
|
m.b1, m.b2, m.b3, m.b4,
|
|
m.c1, m.c2, m.c3, m.c4,
|
|
m.d1, m.d2, m.d3, m.d4);
|
|
}
|
|
|
|
// ── applyMaterial ─────────────────────────────────────────────────────────────
|
|
|
|
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()] = aiToOsgTransposed(bone->mOffsetMatrix);
|
|
}
|
|
}
|
|
|
|
if (boneNames.empty()) { std::cerr << "[skel] No bones.\n"; return result; }
|
|
std::cout << "[skel] Found " << boneNames.size() << " bones.\n";
|
|
|
|
// Invert offset matrices in Assimp space to get bind-pose world positions.
|
|
// This must be done BEFORE any OSG conversion to avoid matrix convention issues.
|
|
for (unsigned m = 0; m < scene->mNumMeshes; ++m) {
|
|
const aiMesh* mesh = scene->mMeshes[m];
|
|
for (unsigned b = 0; b < mesh->mNumBones; ++b) {
|
|
const aiBone* bone = mesh->mBones[b];
|
|
std::string name = bone->mName.C_Str();
|
|
if (result.bindPositions.count(name)) continue;
|
|
aiMatrix4x4 inv = bone->mOffsetMatrix;
|
|
inv.Inverse();
|
|
// Translation is in (a4, b4, c4) in Assimp row-major convention
|
|
result.bindPositions[name] = osg::Vec3(inv.a4, inv.b4, inv.c4);
|
|
}
|
|
}
|
|
|
|
// Build skeleton
|
|
auto skeleton = new osgAnimation::Skeleton;
|
|
skeleton->setName("Skeleton");
|
|
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;
|
|
} |