301 lines
14 KiB
C++
301 lines
14 KiB
C++
#include "ModelLoader.h"
|
|
#include "MorphManager.h"
|
|
|
|
#include <iostream>
|
|
#include <filesystem>
|
|
|
|
#include <assimp/Importer.hpp>
|
|
#include <assimp/scene.h>
|
|
#include <assimp/postprocess.h>
|
|
|
|
#include <osg/Geode>
|
|
#include <osg/Geometry>
|
|
#include <osg/Array>
|
|
#include <osg/PrimitiveSet>
|
|
#include <osg/Material>
|
|
#include <osg/Texture2D>
|
|
#include <osg/BlendFunc>
|
|
#include <osg/StateSet>
|
|
#include <osg/MatrixTransform>
|
|
#include <osgDB/ReadFile>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
namespace {
|
|
|
|
// Section header morphs like "------EYE------" are not real morphs
|
|
bool isSectionHeader(const std::string& name) {
|
|
return name.size() >= 4 &&
|
|
name.front() == '-' && name.back() == '-';
|
|
}
|
|
|
|
osg::ref_ptr<osg::Texture2D> loadTexture(const std::string& path) {
|
|
osg::ref_ptr<osg::Image> img = osgDB::readImageFile(path);
|
|
if (!img) {
|
|
std::cerr << "[loader] texture not found: " << path << "\n";
|
|
return {};
|
|
}
|
|
auto tex = new osg::Texture2D(img);
|
|
tex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
|
|
tex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
|
|
tex->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR);
|
|
tex->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
|
|
return tex;
|
|
}
|
|
|
|
osg::ref_ptr<osg::Geode> convertMesh(const aiMesh* mesh,
|
|
const aiScene* scene,
|
|
const std::string& baseDir,
|
|
MorphManager* morphMgr) {
|
|
auto geode = new osg::Geode;
|
|
auto geom = new osg::Geometry;
|
|
|
|
// ── Base vertices ─────────────────────────────────────────────────────────
|
|
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});
|
|
|
|
// We set a COPY as the live array; the base is kept separately for morphing
|
|
auto liveVerts = new osg::Vec3Array(*baseVerts);
|
|
geom->setVertexArray(liveVerts);
|
|
|
|
// ── Base normals ──────────────────────────────────────────────────────────
|
|
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});
|
|
auto liveNormals = new osg::Vec3Array(*baseNormals);
|
|
geom->setNormalArray(liveNormals, osg::Array::BIND_PER_VERTEX);
|
|
}
|
|
|
|
// ── UVs ───────────────────────────────────────────────────────────────────
|
|
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);
|
|
}
|
|
|
|
// ── Vertex colours ────────────────────────────────────────────────────────
|
|
if (mesh->HasVertexColors(0)) {
|
|
auto cols = new osg::Vec4Array;
|
|
cols->reserve(mesh->mNumVertices);
|
|
for (unsigned i = 0; i < mesh->mNumVertices; ++i)
|
|
cols->push_back({mesh->mColors[0][i].r, mesh->mColors[0][i].g,
|
|
mesh->mColors[0][i].b, mesh->mColors[0][i].a});
|
|
geom->setColorArray(cols, osg::Array::BIND_PER_VERTEX);
|
|
}
|
|
|
|
// ── Indices ───────────────────────────────────────────────────────────────
|
|
auto indices = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES);
|
|
indices->reserve(mesh->mNumFaces * 3);
|
|
for (unsigned f = 0; f < mesh->mNumFaces; ++f) {
|
|
const aiFace& face = mesh->mFaces[f];
|
|
if (face.mNumIndices != 3) continue;
|
|
indices->push_back(face.mIndices[0]);
|
|
indices->push_back(face.mIndices[1]);
|
|
indices->push_back(face.mIndices[2]);
|
|
}
|
|
// Guard: skip meshes with no valid triangles (avoids front() crash on empty vector)
|
|
if (indices->empty()) {
|
|
std::cerr << "[loader] Skipping mesh with no valid triangles: "
|
|
<< mesh->mName.C_Str() << "\n";
|
|
return geode;
|
|
}
|
|
geom->addPrimitiveSet(indices);
|
|
|
|
// ── Material ──────────────────────────────────────────────────────────────
|
|
osg::StateSet* ss = geom->getOrCreateStateSet();
|
|
if (mesh->mMaterialIndex < scene->mNumMaterials) {
|
|
const aiMaterial* mat = scene->mMaterials[mesh->mMaterialIndex];
|
|
auto osgMat = new osg::Material;
|
|
aiColor4D colour;
|
|
|
|
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE, colour))
|
|
osgMat->setDiffuse(osg::Material::FRONT_AND_BACK,
|
|
{colour.r, colour.g, colour.b, colour.a});
|
|
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_AMBIENT, colour))
|
|
osgMat->setAmbient(osg::Material::FRONT_AND_BACK,
|
|
{colour.r, colour.g, colour.b, colour.a});
|
|
if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_SPECULAR, colour))
|
|
osgMat->setSpecular(osg::Material::FRONT_AND_BACK,
|
|
{colour.r, colour.g, colour.b, colour.a});
|
|
float shininess = 0.f;
|
|
if (AI_SUCCESS == mat->Get(AI_MATKEY_SHININESS, shininess))
|
|
osgMat->setShininess(osg::Material::FRONT_AND_BACK,
|
|
std::min(shininess, 128.f));
|
|
ss->setAttribute(osgMat, osg::StateAttribute::ON);
|
|
|
|
if (mat->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
|
|
aiString texPath;
|
|
mat->GetTexture(aiTextureType_DIFFUSE, 0, &texPath);
|
|
std::string fullPath = baseDir + "/" + texPath.C_Str();
|
|
for (char& c : fullPath) if (c == '\\') c = '/';
|
|
// Try the path as-is first, then common extension variants
|
|
// (FBX often references .png when textures are actually .jpg or .tga)
|
|
osg::ref_ptr<osg::Texture2D> tex = loadTexture(fullPath);
|
|
if (!tex) {
|
|
// Try swapping extension
|
|
auto swapExt = [](const std::string& p, const std::string& newExt) {
|
|
auto dot = p.rfind('.');
|
|
return dot != std::string::npos ? p.substr(0, dot) + newExt : p;
|
|
};
|
|
for (auto& ext : {".jpg", ".jpeg", ".png", ".tga", ".bmp"}) {
|
|
tex = loadTexture(swapExt(fullPath, ext));
|
|
if (tex) break;
|
|
}
|
|
}
|
|
if (tex) 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));
|
|
}
|
|
}
|
|
|
|
geode->addDrawable(geom);
|
|
|
|
// ── Morph targets ─────────────────────────────────────────────────────────
|
|
if (morphMgr && mesh->mNumAnimMeshes > 0) {
|
|
// Mark geometry and arrays as DYNAMIC — tells OSG this data changes
|
|
// every frame and must not be double-buffered or cached in display lists.
|
|
geom->setDataVariance(osg::Object::DYNAMIC);
|
|
geom->setUseDisplayList(false);
|
|
geom->setUseVertexBufferObjects(true);
|
|
|
|
auto* vArr = dynamic_cast<osg::Vec3Array*>(geom->getVertexArray());
|
|
auto* nArr = dynamic_cast<osg::Vec3Array*>(geom->getNormalArray());
|
|
if (vArr) {
|
|
vArr->setDataVariance(osg::Object::DYNAMIC);
|
|
vArr->setBinding(osg::Array::BIND_PER_VERTEX);
|
|
}
|
|
if (nArr) {
|
|
nArr->setDataVariance(osg::Object::DYNAMIC);
|
|
nArr->setBinding(osg::Array::BIND_PER_VERTEX);
|
|
}
|
|
|
|
morphMgr->registerMesh(geom, baseVerts, baseNormals);
|
|
|
|
int registered = 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() || isSectionHeader(name)) continue;
|
|
if (am->mNumVertices != mesh->mNumVertices) continue;
|
|
|
|
// Compute vertex deltas (animMesh stores ABSOLUTE positions)
|
|
auto deltaVerts = new osg::Vec3Array;
|
|
deltaVerts->resize(mesh->mNumVertices);
|
|
for (unsigned i = 0; i < mesh->mNumVertices; ++i) {
|
|
(*deltaVerts)[i] = osg::Vec3(
|
|
am->mVertices[i].x - mesh->mVertices[i].x,
|
|
am->mVertices[i].y - mesh->mVertices[i].y,
|
|
am->mVertices[i].z - mesh->mVertices[i].z);
|
|
}
|
|
|
|
// Normal deltas (optional)
|
|
osg::ref_ptr<osg::Vec3Array> deltaNormals;
|
|
if (am->mNormals && mesh->HasNormals()) {
|
|
deltaNormals = new osg::Vec3Array;
|
|
deltaNormals->resize(mesh->mNumVertices);
|
|
for (unsigned i = 0; i < mesh->mNumVertices; ++i) {
|
|
(*deltaNormals)[i] = osg::Vec3(
|
|
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, deltaVerts, deltaNormals);
|
|
++registered;
|
|
}
|
|
|
|
if (registered > 0)
|
|
std::cout << "[loader] Mesh \"" << mesh->mName.C_Str()
|
|
<< "\": registered " << registered << " morph targets\n";
|
|
}
|
|
|
|
return geode;
|
|
}
|
|
|
|
osg::ref_ptr<osg::Group> convertNode(const aiNode* node,
|
|
const aiScene* scene,
|
|
const std::string& baseDir,
|
|
MorphManager* morphMgr) {
|
|
const aiMatrix4x4& m = node->mTransformation;
|
|
osg::Matrixf mat(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);
|
|
|
|
auto xform = new osg::MatrixTransform(mat);
|
|
xform->setName(node->mName.C_Str());
|
|
|
|
for (unsigned i = 0; i < node->mNumMeshes; ++i)
|
|
xform->addChild(convertMesh(scene->mMeshes[node->mMeshes[i]],
|
|
scene, baseDir, morphMgr));
|
|
|
|
for (unsigned i = 0; i < node->mNumChildren; ++i)
|
|
xform->addChild(convertNode(node->mChildren[i], scene, baseDir, morphMgr));
|
|
|
|
return xform;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ── ModelLoader ───────────────────────────────────────────────────────────────
|
|
|
|
osg::ref_ptr<osg::Node> ModelLoader::load(const std::string& filepath,
|
|
MorphManager* morphMgr) {
|
|
Assimp::Importer importer;
|
|
|
|
// NOTE: JoinIdenticalVertices is intentionally omitted —
|
|
// it destroys the vertex correspondence that morph targets rely on.
|
|
//
|
|
// aiProcess_FlipUVs is omitted — FBX exports from Blender already have
|
|
// UVs in the correct OpenGL orientation (Y=0 at bottom). Flipping them
|
|
// was causing textures to appear mirrored/upside-down.
|
|
// If loading a raw PMX via Assimp ever becomes needed, this flag would
|
|
// need to be added back conditionally based on file extension.
|
|
constexpr unsigned flags =
|
|
aiProcess_Triangulate |
|
|
aiProcess_GenSmoothNormals |
|
|
aiProcess_SortByPType |
|
|
aiProcess_ImproveCacheLocality;
|
|
|
|
const aiScene* scene = importer.ReadFile(filepath, flags);
|
|
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
|
std::cerr << "[loader] Assimp error: " << importer.GetErrorString() << "\n";
|
|
return {};
|
|
}
|
|
|
|
std::cout << "[loader] Meshes: " << scene->mNumMeshes
|
|
<< " Materials: " << scene->mNumMaterials
|
|
<< " Animations: " << scene->mNumAnimations << "\n";
|
|
|
|
const std::string baseDir = fs::path(filepath).parent_path().string();
|
|
return buildOsgScene(scene, baseDir, morphMgr);
|
|
}
|
|
|
|
osg::ref_ptr<osg::Node> ModelLoader::buildOsgScene(const aiScene* scene,
|
|
const std::string& baseDir,
|
|
MorphManager* morphMgr) {
|
|
return convertNode(scene->mRootNode, scene, baseDir, morphMgr);
|
|
} |