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;
}
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
#endif // MODELLOADER_H
+3
View File
@@ -5,6 +5,7 @@
#include <destrum/Scene/SceneManager.h>
#include "destrum/Graphics/Resources/Cubemap.h"
#include "destrum/ObjectModel/GameObject.h"
class LightKeeper final : public App {
public:
@@ -28,6 +29,8 @@ private:
MaterialID testMaterialID;
std::unique_ptr<CubeMap> skyboxCubemap;
GameObject* capybara = nullptr;
};
#endif //LIGHTKEEPER_H
+210 -29
View File
@@ -10,10 +10,15 @@
#include "destrum/Components/Spinner.h"
#include "destrum/Components/OrbitAndSpin.h"
#include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/ModelLoader.h"
#include "destrum/Util/ModelDoc.h"
#include "destrum/Components/Animator.h"
LightKeeper::LightKeeper(): App(), renderer(meshCache, materialCache) {
#include <filesystem>
#include <string>
#include "destrum/Util/ModelDocUtils.h"
LightKeeper::LightKeeper() : App(), renderer(meshCache, materialCache) {
}
LightKeeper::~LightKeeper() {
@@ -26,23 +31,46 @@ void LightKeeper::customInit() {
const float aspectRatio = static_cast<float>(m_params.renderSize.x) / static_cast<float>(m_params.renderSize.y);
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";
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);
spdlog::info("TestMesh uploaded with id: {}", testMeshID);
auto testimgID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://kitty.png"));
spdlog::info("Test image loaded with id: {}", testimgID);
const auto testTexturePath = ModelDocUtils::PickTexturePath(
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, {
.baseColor = glm::vec3(1.f),
.baseColor = ModelDocUtils::GetImportedBaseColor(
kittyModel, kittyPrimitive),
.diffuseTexture = testimgID,
.name = ModelDocUtils::GetImportedMaterialName(
kittyModel, kittyPrimitive, "TestMaterial"),
});
spdlog::info("Test material created with id: {}", testMaterialID);
camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f)));
auto& scene = SceneManager::GetInstance().CreateScene("Main");
auto &scene = SceneManager::GetInstance().CreateScene("Main");
// auto testCube = std::make_shared<GameObject>("TestCube");
// auto meshComp = testCube->AddComponent<MeshRendererComponent>();
@@ -77,8 +105,6 @@ void LightKeeper::customInit() {
globeRoot->AddComponent<Spinner>(glm::vec3(0, 1, 0), 1.0f); // spin around Y, rad/sec
scene.Add(globeRoot);
// scene.Add(testCube);
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg");
@@ -97,55 +123,89 @@ void LightKeeper::customInit() {
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
//
const auto planeObj = std::make_shared<GameObject>("GroundPlane");
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, {
.baseColor = glm::vec3(1.f),
.baseColor = ModelDocUtils::GetImportedBaseColor(
planeModel, planePrimitive),
.textureFilteringMode = TextureFilteringMode::Nearest,
.diffuseTexture = planeTextureID,
.name = "GroundPlaneMaterial",
.name = ModelDocUtils::GetImportedMaterialName(
planeModel, planePrimitive, "GroundPlaneMaterial"),
});
planeMeshComp->SetMeshID(planeMeshID);
planeMeshComp->SetMaterialID(planeMaterialID);
planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f));
planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f));
scene.Add(planeObj);
// At the bottom of customInit(), replace the incomplete CharObj block:
const auto CharObj = std::make_shared<GameObject>("Character");
capybara = CharObj.get();
auto charModel = ModelLoader::LoadSkinnedModel(
AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string()
ModelDoc::LoadOptions characterModelOptions{};
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 charMaterialID = materialCache.addMaterial(gfxDevice, {
.baseColor = glm::vec3(1.f),
.baseColor = ModelDocUtils::GetImportedBaseColor(
charModel, charPrimitive),
.diffuseTexture = charTextureID,
.name = "CharacterMaterial",
.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(std::move(charModel.skeleton));
std::string firstAnimationName;
for (auto& clip : charModel.animations) {
for (auto &clip: charModel.animations) {
spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration);
if (firstAnimationName.empty())
@@ -159,11 +219,132 @@ void LightKeeper::customInit() {
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().SetWorldScale(0.01f, 0.01f, 0.01f);
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) {
@@ -194,7 +375,7 @@ void LightKeeper::customDraw() {
renderer.endDrawing();
const auto cmd = gfxDevice.beginFrame();
const auto& drawImage = renderer.getDrawImage(gfxDevice);
const auto &drawImage = renderer.getDrawImage(gfxDevice);
renderer.draw(
cmd, gfxDevice, camera, GameRenderer::SceneData{