Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions Axiom/Assets/AssimpImporter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#include "AssimpImporter.h"
#include "MeshAsset.h"
#include "Core/Log.h"

#include <assimp/Importer.hpp>
#include <assimp/postprocess.h>
#include <assimp/scene.h>

#include <glm/mat4x4.hpp>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>

#include <functional>

namespace Axiom::Assets {
namespace {

MeshData ConvertMesh(const aiMesh *AiMesh) {
MeshData Data;
Data.Vertices.reserve(AiMesh->mNumVertices);

glm::vec3 BoundsMin{ 1e30f};
glm::vec3 BoundsMax{-1e30f};

for (unsigned i = 0; i < AiMesh->mNumVertices; ++i) {
MeshVertex V{};
V.Position = {AiMesh->mVertices[i].x, AiMesh->mVertices[i].y,
AiMesh->mVertices[i].z, 1.0f};
if (AiMesh->HasNormals()) {
V.Normal = {AiMesh->mNormals[i].x, AiMesh->mNormals[i].y,
AiMesh->mNormals[i].z, 0.0f};
} else {
V.Normal = {0.0f, 1.0f, 0.0f, 0.0f};
}
if (AiMesh->HasTextureCoords(0)) {
V.TexCoord = {AiMesh->mTextureCoords[0][i].x,
AiMesh->mTextureCoords[0][i].y};
}
BoundsMin = glm::min(BoundsMin, glm::vec3{V.Position});
BoundsMax = glm::max(BoundsMax, glm::vec3{V.Position});
Data.Vertices.push_back(V);
}

Data.Indices.reserve(AiMesh->mNumFaces * 3);
for (unsigned i = 0; i < AiMesh->mNumFaces; ++i) {
const aiFace &Face = AiMesh->mFaces[i];
for (unsigned j = 0; j < Face.mNumIndices; ++j)
Data.Indices.push_back(Face.mIndices[j]);
}

Data.BoundsMin = BoundsMin;
Data.BoundsMax = BoundsMax;
return Data;
}

MaterialInstanceRef ConvertMaterial(const aiScene *Scene, unsigned MatIndex,
const std::filesystem::path &AssetDir) {
auto Mat = std::make_shared<MaterialInstance>();
if (MatIndex >= Scene->mNumMaterials)
return Mat;

const aiMaterial *AiMat = Scene->mMaterials[MatIndex];

aiColor4D Diffuse(1.0f, 1.0f, 1.0f, 1.0f);
if (AiMat->Get(AI_MATKEY_COLOR_DIFFUSE, Diffuse) == AI_SUCCESS)
Mat->BaseColorFactor = {Diffuse.r, Diffuse.g, Diffuse.b, Diffuse.a};

float Metallic = 0.0f;
float Roughness = 0.5f;
AiMat->Get(AI_MATKEY_METALLIC_FACTOR, Metallic);
AiMat->Get(AI_MATKEY_ROUGHNESS_FACTOR, Roughness);
Mat->Metallic = Metallic;
Mat->Roughness = Roughness;

aiString TexPath;
if (AiMat->GetTexture(aiTextureType_DIFFUSE, 0, &TexPath) == AI_SUCCESS) {
const char *RawPath = TexPath.C_Str();
if (RawPath[0] == '*') {
// Embedded compressed texture (FBX)
int Idx = std::atoi(RawPath + 1);
if (Idx >= 0 && Idx < static_cast<int>(Scene->mNumTextures)) {
const aiTexture *Tex = Scene->mTextures[Idx];
if (Tex->mHeight == 0) {
Mat->BaseColorTexture = LoadTextureFromMemory(
reinterpret_cast<const unsigned char *>(Tex->pcData),
static_cast<int>(Tex->mWidth), RawPath);
}
}
} else {
const auto FullPath = AssetDir / RawPath;
auto Loaded = LoadTextureFromFile(FullPath);
if (Loaded) {
Mat->BaseColorTexture = Loaded;
Mat->TextureAssetPath = RawPath; // relative, as stored in the source file
}
}
}

return Mat;
}

glm::mat4 ToGlm(const aiMatrix4x4 &M) {
// assimp is row-major; glm is column-major
return glm::transpose(glm::mat4{
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,
});
}

void VisitNode(const aiScene *Scene, const aiNode *Node, const glm::mat4 &Parent,
const std::filesystem::path &AssetDir, MeshSceneData &Out) {
const glm::mat4 World = Parent * ToGlm(Node->mTransformation);
for (unsigned i = 0; i < Node->mNumMeshes; ++i) {
const aiMesh *AiMesh = Scene->mMeshes[Node->mMeshes[i]];
MeshSceneData::MeshInstanceData Inst;
Inst.Name = AiMesh->mName.length > 0 ? AiMesh->mName.C_Str()
: Node->mName.C_Str();
Inst.Mesh = ConvertMesh(AiMesh);
Inst.Material = ConvertMaterial(Scene, AiMesh->mMaterialIndex, AssetDir);
Inst.Transform = World;
Out.Instances.push_back(std::move(Inst));
}
for (unsigned i = 0; i < Node->mNumChildren; ++i)
VisitNode(Scene, Node->mChildren[i], World, AssetDir, Out);
}

} // namespace

std::optional<MeshSceneData>
AssimpImporter::Import(const std::filesystem::path &Path) {
Assimp::Importer Importer;
const aiScene *Scene = Importer.ReadFile(
Path.string(),
aiProcess_Triangulate | aiProcess_GenSmoothNormals |
aiProcess_FlipUVs | aiProcess_JoinIdenticalVertices |
aiProcess_SortByPType);

if (!Scene || !Scene->mRootNode ||
(Scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE)) {
A_CORE_WARN("AssimpImporter: failed to load '{}': {}", Path.string(),
Importer.GetErrorString());
return std::nullopt;
}

MeshSceneData Result;
VisitNode(Scene, Scene->mRootNode, glm::mat4{1.0f}, Path.parent_path(),
Result);

if (Result.Instances.empty()) {
A_CORE_WARN("AssimpImporter: no renderable meshes found in '{}'",
Path.string());
return std::nullopt;
}

return Result;
}

} // namespace Axiom::Assets
12 changes: 12 additions & 0 deletions Axiom/Assets/AssimpImporter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

#include "IAssetImporter.h"

namespace Axiom::Assets {

class AssimpImporter final : public IAssetImporter {
public:
std::optional<MeshSceneData> Import(const std::filesystem::path &Path) override;
};

} // namespace Axiom::Assets
16 changes: 16 additions & 0 deletions Axiom/Assets/IAssetImporter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include "Renderer/Mesh.h"

#include <filesystem>
#include <optional>

namespace Axiom::Assets {

class IAssetImporter {
public:
virtual ~IAssetImporter() = default;
virtual std::optional<MeshSceneData> Import(const std::filesystem::path &Path) = 0;
};

} // namespace Axiom::Assets
7 changes: 4 additions & 3 deletions Axiom/Assets/IAssetSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace {

AssetKind KindFromExtension(const std::filesystem::path &Path) {
auto ext = Path.extension().string();
if (ext == ".glb" || ext == ".gltf")
if (ext == ".glb" || ext == ".gltf" || ext == ".fbx" || ext == ".obj")
return AssetKind::Mesh;
return AssetKind::Texture;
}
Expand All @@ -15,8 +15,9 @@ AssetId IdFromRelPath(const std::filesystem::path &RelPath) {
return AssetId{std::hash<std::string>{}(RelPath.string())};
}

constexpr std::string_view kContentExtensions[] = {".glb", ".gltf", ".png",
".jpg", ".jpeg"};
constexpr std::string_view kContentExtensions[] = {".glb", ".gltf", ".fbx",
".obj", ".png", ".jpg",
".jpeg"};

bool IsContentFile(const std::filesystem::path &Path) {
auto ext = Path.extension().string();
Expand Down
63 changes: 51 additions & 12 deletions Axiom/Assets/MeshAsset.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "Assets/MeshAsset.h"
#include "Assets/AssimpImporter.h"

#include "Core/Log.h"

Expand Down Expand Up @@ -375,6 +376,12 @@ void AppendNodeMeshes(const fastgltf::Asset &Asset, size_t NodeIndex,
} // namespace

std::optional<MeshSceneData> LoadBasicMeshAsset(const std::filesystem::path &Path) {
const std::string Ext = ToLowerCopy(Path.extension().string());
if (Ext == ".fbx" || Ext == ".obj") {
AssimpImporter Importer;
return Importer.Import(Path);
}

fastgltf::GltfDataBuffer Buffer;
if (!Buffer.loadFromFile(Path)) {
A_CORE_ERROR("Failed to open mesh asset: {0}", Path.string());
Expand Down Expand Up @@ -419,9 +426,25 @@ std::optional<MeshSceneData> LoadBasicMeshAsset(const std::filesystem::path &Pat
std::vector<MaterialInstanceRef> MaterialCache(ParsedAsset.materials.size());
MaterialInstanceRef FallbackMaterial = std::make_shared<MaterialInstance>();

auto MakeMaterialWithFactors =
[](const fastgltf::Material &Mat,
TextureSourceDataRef Texture) -> MaterialInstanceRef {
auto Ref = std::make_shared<MaterialInstance>();
Ref->BaseColorTexture = std::move(Texture);
const auto &Pbr = Mat.pbrData;
Ref->BaseColorFactor = glm::vec4(
static_cast<float>(Pbr.baseColorFactor[0]),
static_cast<float>(Pbr.baseColorFactor[1]),
static_cast<float>(Pbr.baseColorFactor[2]),
static_cast<float>(Pbr.baseColorFactor[3]));
Ref->Metallic = static_cast<float>(Pbr.metallicFactor);
Ref->Roughness = static_cast<float>(Pbr.roughnessFactor);
return Ref;
};

auto ResolveMaterial = [&](const fastgltf::Primitive &Primitive,
bool HasTexCoord0) -> MaterialInstanceRef {
if (!HasTexCoord0 || !Primitive.materialIndex.has_value() ||
if (!Primitive.materialIndex.has_value() ||
*Primitive.materialIndex >= ParsedAsset.materials.size()) {
return FallbackMaterial;
}
Expand All @@ -432,23 +455,28 @@ std::optional<MeshSceneData> LoadBasicMeshAsset(const std::filesystem::path &Pat
}

const auto &Material = ParsedAsset.materials[MaterialIndex];
if (!Material.pbrData.baseColorTexture.has_value() ||

// No texture (or no UV) — still carry PBR color factors.
if (!HasTexCoord0 || !Material.pbrData.baseColorTexture.has_value() ||
Material.pbrData.baseColorTexture->texCoordIndex != 0) {
MaterialCache[MaterialIndex] = FallbackMaterial;
return FallbackMaterial;
auto Ref = MakeMaterialWithFactors(Material, nullptr);
MaterialCache[MaterialIndex] = Ref;
return Ref;
}

const size_t TextureIndex = Material.pbrData.baseColorTexture->textureIndex;
if (TextureIndex >= ParsedAsset.textures.size()) {
MaterialCache[MaterialIndex] = FallbackMaterial;
return FallbackMaterial;
auto Ref = MakeMaterialWithFactors(Material, nullptr);
MaterialCache[MaterialIndex] = Ref;
return Ref;
}

const auto &Texture = ParsedAsset.textures[TextureIndex];
if (!Texture.imageIndex.has_value() ||
*Texture.imageIndex >= ParsedAsset.images.size()) {
MaterialCache[MaterialIndex] = FallbackMaterial;
return FallbackMaterial;
auto Ref = MakeMaterialWithFactors(Material, nullptr);
MaterialCache[MaterialIndex] = Ref;
return Ref;
}

const size_t ImageIndex = *Texture.imageIndex;
Expand All @@ -458,12 +486,12 @@ std::optional<MeshSceneData> LoadBasicMeshAsset(const std::filesystem::path &Pat
}

if (!ImageCache[ImageIndex] || !ImageCache[ImageIndex]->IsValid()) {
MaterialCache[MaterialIndex] = FallbackMaterial;
return FallbackMaterial;
auto Ref = MakeMaterialWithFactors(Material, nullptr);
MaterialCache[MaterialIndex] = Ref;
return Ref;
}

auto MaterialRef = std::make_shared<MaterialInstance>();
MaterialRef->BaseColorTexture = ImageCache[ImageIndex];
auto MaterialRef = MakeMaterialWithFactors(Material, ImageCache[ImageIndex]);
MaterialCache[MaterialIndex] = MaterialRef;
return MaterialRef;
};
Expand All @@ -483,4 +511,15 @@ std::optional<MeshSceneData> LoadBasicMeshAsset(const std::filesystem::path &Pat

return SceneData;
}

TextureSourceDataRef LoadTextureFromFile(const std::filesystem::path &Path) {
return DecodeTextureFromFile(Path);
}

TextureSourceDataRef LoadTextureFromMemory(const unsigned char *Bytes,
int Length,
const std::string &DebugName) {
return DecodeTextureFromMemory(
reinterpret_cast<const stbi_uc *>(Bytes), Length, DebugName);
}
} // namespace Axiom::Assets
6 changes: 5 additions & 1 deletion Axiom/Assets/MeshAsset.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#pragma once

#include "Renderer/Material.h"
#include "Renderer/Mesh.h"

#include <filesystem>
#include <optional>

namespace Axiom::Assets {
std::optional<MeshSceneData> LoadBasicMeshAsset(const std::filesystem::path &Path);
}
TextureSourceDataRef LoadTextureFromFile(const std::filesystem::path &Path);
TextureSourceDataRef LoadTextureFromMemory(const unsigned char *Bytes, int Length,
const std::string &DebugName);
} // namespace Axiom::Assets
Loading
Loading