Fix some import bugs / add support for separated model + anim file

This commit is contained in:
2026-06-22 01:37:16 +02:00
parent 7e5536a02f
commit 6f679e7329
10 changed files with 1514 additions and 34 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,138 @@
#ifndef MODELDOCUTILS_H
#define MODELDOCUTILS_H
#include "destrum/Util/ModelDoc.h"
#include "spdlog/spdlog.h"
#include <algorithm>
#include <filesystem>
#include <initializer_list>
#include <optional>
#include <stdexcept>
#include <string>
namespace ModelDocUtils {
inline const ModelDoc::MeshPrimitive& GetFirstPrimitiveOrThrow(
const ModelDoc::ModelAsset& model,
const std::string& debugName
) {
if (model.primitives.empty()) {
throw std::runtime_error("ModelDoc loaded no mesh primitives for: " + debugName);
}
return model.primitives.front();
}
inline const ModelDoc::MeshPrimitive& GetFirstSkinnedPrimitiveOrFirstOrThrow(
const ModelDoc::ModelAsset& model,
const std::string& debugName
) {
if (model.primitives.empty()) {
throw std::runtime_error("ModelDoc loaded no mesh primitives for: " + debugName);
}
const auto it = std::find_if(
model.primitives.begin(),
model.primitives.end(),
[](const ModelDoc::MeshPrimitive& primitive) {
return primitive.skinned;
}
);
return it != model.primitives.end() ? *it : model.primitives.front();
}
inline glm::vec3 GetImportedBaseColor(
const ModelDoc::ModelAsset& model,
const ModelDoc::MeshPrimitive& primitive,
const glm::vec3& fallback = glm::vec3(1.0f)
) {
if (primitive.materialIndex == ModelDoc::InvalidIndex ||
primitive.materialIndex >= model.materials.size()) {
return fallback;
}
return glm::vec3(model.materials[primitive.materialIndex].baseColor);
}
inline std::string GetImportedMaterialName(
const ModelDoc::ModelAsset& model,
const ModelDoc::MeshPrimitive& primitive,
const std::string& fallback
) {
if (primitive.materialIndex == ModelDoc::InvalidIndex ||
primitive.materialIndex >= model.materials.size()) {
return fallback;
}
const auto& material = model.materials[primitive.materialIndex];
return material.name.empty() ? fallback : material.name;
}
inline std::optional<std::string> FindFirstTexturePath(
const ModelDoc::ModelAsset& model,
const ModelDoc::MeshPrimitive& primitive,
std::initializer_list<ModelDoc::TextureSemantic> preferredSemantics
) {
if (primitive.materialIndex == ModelDoc::InvalidIndex ||
primitive.materialIndex >= model.materials.size()) {
return std::nullopt;
}
const auto& material = model.materials[primitive.materialIndex];
for (const auto semantic : preferredSemantics) {
for (const auto& texture : material.textures) {
// gfxDevice.loadImageFromFile currently expects a real file path.
// Embedded GLB textures like "*0" need extraction/support in GfxDevice first.
if (texture.semantic == semantic && !texture.embedded && !texture.path.empty()) {
return texture.path;
}
}
}
return std::nullopt;
}
inline std::filesystem::path PickTexturePath(
const ModelDoc::ModelAsset& model,
const ModelDoc::MeshPrimitive& primitive,
const std::filesystem::path& fallbackPath
) {
const auto importedTexturePath = FindFirstTexturePath(
model,
primitive,
{
ModelDoc::TextureSemantic::BaseColor,
ModelDoc::TextureSemantic::Diffuse
}
);
if (importedTexturePath.has_value()) {
return std::filesystem::path(*importedTexturePath);
}
spdlog::info("Using fallback path {}", fallbackPath.string());
return fallbackPath;
}
inline void LogModelDocSummary(
const ModelDoc::ModelAsset& model,
const std::string& label
) {
spdlog::info(
"Loaded {} | primitives: {} | materials: {} | animations: {} | skinned: {}",
label,
model.primitives.size(),
model.materials.size(),
model.animations.size(),
model.IsSkinned()
);
}
} // namespace ModelDocUtils
#endif // MODELDOCUTILS_H
@@ -768,6 +768,35 @@ static SkinnedModel LoadSkinnedModel(const std::string& path) {
return model; return model;
} }
struct LoadedMesh {
CPUMesh mesh;
std::vector<std::string> diffuseTexturePaths;
};
static std::vector<std::string> LoadMaterialTexturePaths(
const aiScene* scene,
const aiMesh* mesh,
aiTextureType type,
const std::filesystem::path& modelDir
) {
std::vector<std::string> paths;
if (mesh->mMaterialIndex >= scene->mNumMaterials)
return paths;
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
for (unsigned int i = 0; i < material->GetTextureCount(type); ++i) {
aiString texPath;
if (material->GetTexture(type, i, &texPath) == AI_SUCCESS) {
auto fullPath = modelDir / texPath.C_Str();
paths.push_back(fullPath.generic_string());
}
}
return paths;
}
} // namespace ModelLoader } // namespace ModelLoader
#endif // MODELLOADER_H #endif // MODELLOADER_H
+3
View File
@@ -5,6 +5,7 @@
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
#include "destrum/Graphics/Resources/Cubemap.h" #include "destrum/Graphics/Resources/Cubemap.h"
#include "destrum/ObjectModel/GameObject.h"
class LightKeeper final : public App { class LightKeeper final : public App {
public: public:
@@ -28,6 +29,8 @@ private:
MaterialID testMaterialID; MaterialID testMaterialID;
std::unique_ptr<CubeMap> skyboxCubemap; std::unique_ptr<CubeMap> skyboxCubemap;
GameObject* capybara = nullptr;
}; };
#endif //LIGHTKEEPER_H #endif //LIGHTKEEPER_H
+206 -25
View File
@@ -10,9 +10,14 @@
#include "destrum/Components/Spinner.h" #include "destrum/Components/Spinner.h"
#include "destrum/Components/OrbitAndSpin.h" #include "destrum/Components/OrbitAndSpin.h"
#include "destrum/ObjectModel/GameObject.h" #include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/ModelLoader.h" #include "destrum/Util/ModelDoc.h"
#include "destrum/Components/Animator.h" #include "destrum/Components/Animator.h"
#include <filesystem>
#include <string>
#include "destrum/Util/ModelDocUtils.h"
LightKeeper::LightKeeper() : App(), renderer(meshCache, materialCache) { LightKeeper::LightKeeper() : App(), renderer(meshCache, materialCache) {
} }
@@ -26,17 +31,40 @@ void LightKeeper::customInit() {
const float aspectRatio = static_cast<float>(m_params.renderSize.x) / static_cast<float>(m_params.renderSize.y); const float aspectRatio = static_cast<float>(m_params.renderSize.x) / static_cast<float>(m_params.renderSize.y);
camera.setAspectRatio(aspectRatio); camera.setAspectRatio(aspectRatio);
ModelDoc::LoadOptions staticModelOptions{};
staticModelOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
staticModelOptions.loadMaterials = true;
staticModelOptions.loadSkeleton = false;
staticModelOptions.loadAnimations = false;
auto kittyModel = ModelDoc::LoadModel(
AssetFS::GetInstance().GetFullPath("game://kitty.glb").generic_string(),
staticModelOptions
);
ModelDocUtils::LogModelDocSummary(kittyModel, "kitty.glb");
const auto &kittyPrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(kittyModel, "game://kitty.glb");
testMesh = kittyPrimitive.mesh;
testMesh.name = "Test Mesh"; testMesh.name = "Test Mesh";
auto list_of_models = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://kitty.glb").generic_string());
testMesh = list_of_models[0];
testMeshID = meshCache.addMesh(gfxDevice, testMesh); testMeshID = meshCache.addMesh(gfxDevice, testMesh);
spdlog::info("TestMesh uploaded with id: {}", testMeshID); spdlog::info("TestMesh uploaded with id: {}", testMeshID);
auto testimgID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://kitty.png")); const auto testTexturePath = ModelDocUtils::PickTexturePath(
spdlog::info("Test image loaded with id: {}", testimgID); kittyModel,
kittyPrimitive,
AssetFS::GetInstance().GetFullPath("game://kitty.png")
);
const auto testimgID = gfxDevice.loadImageFromFile(testTexturePath);
spdlog::info("Test image loaded from '{}' with id: {}", testTexturePath.generic_string(), testimgID);
testMaterialID = materialCache.addMaterial(gfxDevice, { testMaterialID = materialCache.addMaterial(gfxDevice, {
.baseColor = glm::vec3(1.f), .baseColor = ModelDocUtils::GetImportedBaseColor(
kittyModel, kittyPrimitive),
.diffuseTexture = testimgID, .diffuseTexture = testimgID,
.name = ModelDocUtils::GetImportedMaterialName(
kittyModel, kittyPrimitive, "TestMaterial"),
}); });
spdlog::info("Test material created with id: {}", testMaterialID); spdlog::info("Test material created with id: {}", testMaterialID);
@@ -77,8 +105,6 @@ void LightKeeper::customInit() {
globeRoot->AddComponent<Spinner>(glm::vec3(0, 1, 0), 1.0f); // spin around Y, rad/sec globeRoot->AddComponent<Spinner>(glm::vec3(0, 1, 0), 1.0f); // spin around Y, rad/sec
scene.Add(globeRoot); scene.Add(globeRoot);
// scene.Add(testCube); // scene.Add(testCube);
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg"); // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg");
@@ -97,49 +123,83 @@ void LightKeeper::customInit() {
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID()); renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
//
const auto planeObj = std::make_shared<GameObject>("GroundPlane"); const auto planeObj = std::make_shared<GameObject>("GroundPlane");
const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>(); const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
const auto planeModel = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string());
const auto planeMeshID = meshCache.addMesh(gfxDevice, planeModel[0]);
const auto planeTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://grass.png")); auto planeModel = ModelDoc::LoadModel(
AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string(),
staticModelOptions
);
ModelDocUtils::LogModelDocSummary(planeModel, "plane.glb");
const auto &planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb");
const auto planeMeshID = meshCache.addMesh(gfxDevice, planePrimitive.mesh);
const auto planeTexturePath = ModelDocUtils::PickTexturePath(
planeModel,
planePrimitive,
AssetFS::GetInstance().GetFullPath("game://grass.png")
);
const auto planeTextureID = gfxDevice.loadImageFromFile(planeTexturePath);
const auto planeMaterialID = materialCache.addMaterial(gfxDevice, { const auto planeMaterialID = materialCache.addMaterial(gfxDevice, {
.baseColor = glm::vec3(1.f), .baseColor = ModelDocUtils::GetImportedBaseColor(
planeModel, planePrimitive),
.textureFilteringMode = TextureFilteringMode::Nearest, .textureFilteringMode = TextureFilteringMode::Nearest,
.diffuseTexture = planeTextureID, .diffuseTexture = planeTextureID,
.name = "GroundPlaneMaterial", .name = ModelDocUtils::GetImportedMaterialName(
planeModel, planePrimitive, "GroundPlaneMaterial"),
}); });
planeMeshComp->SetMeshID(planeMeshID); planeMeshComp->SetMeshID(planeMeshID);
planeMeshComp->SetMaterialID(planeMaterialID); planeMeshComp->SetMaterialID(planeMaterialID);
planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f)); planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f));
planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f)); planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f));
scene.Add(planeObj); scene.Add(planeObj);
// At the bottom of customInit(), replace the incomplete CharObj block:
const auto CharObj = std::make_shared<GameObject>("Character"); const auto CharObj = std::make_shared<GameObject>("Character");
capybara = CharObj.get();
auto charModel = ModelLoader::LoadSkinnedModel( ModelDoc::LoadOptions characterModelOptions{};
AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string() characterModelOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
characterModelOptions.loadMaterials = true;
characterModelOptions.loadSkeleton = true;
characterModelOptions.loadAnimations = true;
auto charModel = ModelDoc::LoadModel(
AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string(),
characterModelOptions
);
ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx");
const auto &charPrimitive = ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow(
charModel,
"engine://cotw-capybara-male/source/capybara.fbx"
); );
const auto charMeshID = meshCache.addMesh(gfxDevice, charModel.meshes[0]); const auto charMeshID = meshCache.addMesh(gfxDevice, charPrimitive.mesh);
const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.png")); const auto charTexturePath = ModelDocUtils::PickTexturePath(
charModel,
charPrimitive,
AssetFS::GetInstance().GetFullPath(
"engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.pngballs")
);
const auto charTextureID = gfxDevice.loadImageFromFile(charTexturePath);
// const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg")); // const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg"));
const auto charMaterialID = materialCache.addMaterial(gfxDevice, { const auto charMaterialID = materialCache.addMaterial(gfxDevice, {
.baseColor = glm::vec3(1.f), .baseColor = ModelDocUtils::GetImportedBaseColor(
charModel, charPrimitive),
.diffuseTexture = charTextureID, .diffuseTexture = charTextureID,
.name = "CharacterMaterial", .name = ModelDocUtils::GetImportedMaterialName(
charModel, charPrimitive, "CharacterMaterial"),
}); });
const auto charMeshComp = CharObj->AddComponent<MeshRendererComponent>(); const auto charMeshComp = CharObj->AddComponent<MeshRendererComponent>();
charMeshComp->SetMeshID(charMeshID); charMeshComp->SetMeshID(charMeshID);
charMeshComp->SetMaterialID(charMaterialID); charMeshComp->SetMaterialID(charMaterialID);
const auto animator = CharObj->AddComponent<Animator>(); const auto animator = CharObj->AddComponent<Animator>();
animator->setSkeleton(std::move(charModel.skeleton)); animator->setSkeleton(std::move(charModel.skeleton));
@@ -159,11 +219,132 @@ void LightKeeper::customInit() {
animator->play(firstAnimationName); animator->play(firstAnimationName);
} }
animator->play("capybara_canter_fwd_01|capybara_walk_fwd_01"); animator->play("capybara_canter_fwd_01|capybara_canter_fwd_01|run");
CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f));
// CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f); // CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f);
scene.Add(CharObj); scene.Add(CharObj);
ModelDoc::LoadOptions options{};
options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
auto model = ModelDoc::LoadModel(
AssetFS::GetInstance()
.GetFullPath("engine://multiple_object_test.fbx")
.generic_string(),
options
);
ModelDocUtils::LogModelDocSummary(model, "engine://multiple_object_test.fbx");
auto root = std::make_shared<GameObject>("MultiMeshModel");
root->GetTransform().SetWorldPosition(glm::vec3(0.1f, 0.1f, 0.1f));
scene.Add(root);
for (std::size_t i = 0; i < model.primitives.size(); ++i) {
const auto &primitive = model.primitives[i];
const auto meshID = meshCache.addMesh(gfxDevice, primitive.mesh);
const auto texturePath = ModelDocUtils::PickTexturePath(
model,
primitive,
AssetFS::GetInstance().GetFullPath("engine://textures/white.png")
);
const auto textureID = gfxDevice.loadImageFromFile(texturePath);
const auto materialID = materialCache.addMaterial(gfxDevice, {
.baseColor = ModelDocUtils::GetImportedBaseColor(
model, primitive),
.diffuseTexture = textureID,
.name = ModelDocUtils::GetImportedMaterialName(
model,
primitive,
primitive.mesh.name + "_Material"
),
});
auto part = std::make_shared<GameObject>(
primitive.mesh.name.empty()
? "Primitive_" + std::to_string(i)
: primitive.mesh.name
);
auto meshComp = part->AddComponent<MeshRendererComponent>();
meshComp->SetMeshID(meshID);
meshComp->SetMaterialID(materialID);
scene.Add(part);
}
{
const auto CharObj = std::make_shared<GameObject>("Character");
ModelDoc::LoadOptions characterOptions{};
characterOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode;
characterOptions.loadMaterials = true;
characterOptions.loadSkeleton = true;
characterOptions.loadAnimations = false;
auto charModel = ModelDoc::LoadModel(
AssetFS::GetInstance()
.GetFullPath("engine://characterMedium.fbx")
.generic_string(),
characterOptions
);
const auto &charPrimitive =
ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow(
charModel,
"engine://characterMedium.fbx"
);
const auto charMeshID = meshCache.addMesh(gfxDevice, charPrimitive.mesh);
const auto charTextureID = gfxDevice.loadImageFromFile(
AssetFS::GetInstance().GetFullPath("engine://textures/criminalMaleA.png")
);
const auto charMaterialID = materialCache.addMaterial(gfxDevice, {
.baseColor = ModelDocUtils::GetImportedBaseColor(
charModel, charPrimitive),
.diffuseTexture = charTextureID,
.name = ModelDocUtils::GetImportedMaterialName(
charModel,
charPrimitive,
"CharacterMaterial"
),
});
const auto charMeshComp = CharObj->AddComponent<MeshRendererComponent>();
charMeshComp->SetMeshID(charMeshID);
charMeshComp->SetMaterialID(charMaterialID);
const auto animator = CharObj->AddComponent<Animator>();
animator->setSkeleton(charModel.skeleton);
auto runClips = ModelDoc::LoadAnimationClips(
AssetFS::GetInstance()
.GetFullPath("engine://run.fbx")
.generic_string(),
charModel.skeleton
);
for (auto &clip: runClips) {
spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration);
animator->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
}
if (!runClips.empty()) {
animator->play("Root|Run");
}
CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f));
CharObj->GetTransform().SetWorldPosition(glm::vec3(5, 0, 0));
scene.Add(CharObj);
}
} }
void LightKeeper::customUpdate(float dt) { void LightKeeper::customUpdate(float dt) {