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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ jobs:
install_lavapipe: false

- name: Set up MSBuild
uses: microsoft/setup-msbuild@v2
uses: microsoft/setup-msbuild@v3
with:
msbuild-architecture: 'x64'

- name: Setup
working-directory: ${{env.GITHUB_WORKSPACE}}
run: |
cd scripts
python Setup.py
python Setup.py vs2026
Comment on lines +55 to +63
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title describes implementing a Hazel3D-style asset system, but the PR description checklist focuses on EditorLayer ImGui cleanup/perf work (and doesn’t mention the new asset-manager/serializer/async-loading changes). Please update the PR description to reflect the asset system work (or split the editor UI refactor into a separate PR) so reviewers can scope risk appropriately.

Copilot uses AI. Check for mistakes.

- name: Build
working-directory: ${{env.GITHUB_WORKSPACE}}
Expand Down
71 changes: 53 additions & 18 deletions Core/Source/Lux/Asset/AssetImporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,77 @@
#include "TextureImporter.h"
#include "SceneImporter.h"
#include "AudioImporter.h"
#include "TextureSerializer.h"
#include "MeshSerializer.h"
#include "MaterialSerializer.h"

#include <map>
#include <memory>

namespace Lux {

using AssetImportFunction = std::function<Ref<Asset>(AssetHandle, const AssetMetadata&)>;
static std::map<AssetType, AssetImportFunction> s_AssetImportFunctions = {
{AssetType::Texture, TextureImporter::ImportTexture },
{ AssetType::Scene, SceneImporter::ImportScene },
{ AssetType::Audio, AudioImporter::ImportAudio },/*
{ AssetType::ObjModel, ObjModelImporter::ImportObjModel },
{ AssetType::ScriptFile, SceneImporter::ImportScript }*/
};
// Serializer-based dispatch table (Hazel-style).
// Each entry owns its AssetSerializer instance.
static std::map<AssetType, std::unique_ptr<AssetSerializer>> s_Serializers;

static void InitSerializers()
{
// Texture
s_Serializers[AssetType::Texture] = std::make_unique<TextureSerializer>();
s_Serializers[AssetType::EnvMap] = std::make_unique<TextureSerializer>();

// Mesh
s_Serializers[AssetType::MeshSource] = std::make_unique<MeshSourceSerializer>();
s_Serializers[AssetType::Mesh] = std::make_unique<MeshSerializer>();
s_Serializers[AssetType::StaticMesh] = std::make_unique<StaticMeshSerializer>();

// Material
s_Serializers[AssetType::Material] = std::make_unique<MaterialSerializer>();
}

Ref<Asset> AssetImporter::ImportAsset(AssetHandle handle, const AssetMetadata& metadata)
{
LUX_PROFILE_FUNCTION_COLOR("AssetImporter::ImportAsset", 0xF2FA8A);

// Lazy-initialise the serializer table once.
static std::once_flag s_InitFlag;
std::call_once(s_InitFlag, InitSerializers);

// ── Serializer-based types ────────────────────────────────────────────
auto serializerIt = s_Serializers.find(metadata.Type);
if (serializerIt != s_Serializers.end())
{
LUX_PROFILE_SCOPE_COLOR("AssetImporter::ImportAsset Scope", 0x27628A);
// Build a metadata copy that carries the handle (in case the
// metadata came in without it already set).
AssetMetadata meta = metadata;
if (meta.Handle == 0)
meta.Handle = handle;

if (s_AssetImportFunctions.find(metadata.Type) == s_AssetImportFunctions.end())
{
LUX_CORE_ERROR("No importer available for asset type: {}", (uint16_t)metadata.Type);
return nullptr;
}
Ref<Asset> asset;
if (serializerIt->second->TryLoadData(meta, asset))
return asset;

LUX_CORE_ERROR("AssetImporter: serializer failed for type {} (handle {})",
(uint16_t)metadata.Type, (uint64_t)handle);
return nullptr;
}

auto& result = s_AssetImportFunctions.at(metadata.Type);//(metadata.Type)(handle, metadata);
// ── Legacy function-pointer importers ─────────────────────────────────
using AssetImportFunction = std::function<Ref<Asset>(AssetHandle, const AssetMetadata&)>;
static const std::map<AssetType, AssetImportFunction> s_LegacyImportFunctions = {
{ AssetType::Scene, SceneImporter::ImportScene },
{ AssetType::Audio, AudioImporter::ImportAudio },
};

auto legacyIt = s_LegacyImportFunctions.find(metadata.Type);
if (legacyIt == s_LegacyImportFunctions.end())
{
LUX_PROFILE_SCOPE_COLOR("AssetImporter::ImportAsset 2 Scope", 0xD1C48A);

return result(handle, metadata);
LUX_CORE_ERROR("AssetImporter: no importer for asset type {} (handle {})",
(uint16_t)metadata.Type, (uint64_t)handle);
return nullptr;
}

return legacyIt->second(handle, metadata);
}

}
3 changes: 3 additions & 0 deletions Core/Source/Lux/Asset/AssetImporter.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#pragma once

#include "AssetMetadata.h"
#include "AssetSerializer.h"

namespace Lux
{
// Routes asset load requests to the appropriate AssetSerializer (or legacy
// importer function) based on the asset type stored in the metadata.
class AssetImporter
{
public:
Expand Down
1 change: 1 addition & 0 deletions Core/Source/Lux/Asset/AssetMetadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Lux
{
struct AssetMetadata
{
AssetHandle Handle = 0;
AssetType Type = AssetType::None;
std::filesystem::path FilePath = "";

Expand Down
22 changes: 22 additions & 0 deletions Core/Source/Lux/Asset/AssetSerializer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include "AssetMetadata.h"

namespace Lux
{
// Abstract interface that every asset type's serializer must satisfy.
// Editor serializers load data from disk (e.g. via YAML / Assimp).
// Runtime serializers read from packed binary streams produced at build time.
class AssetSerializer
{
public:
virtual ~AssetSerializer() = default;

// Serialize the asset back to its source representation (YAML, binary, etc.)
virtual void Serialize(const AssetMetadata& metadata, const Ref<Asset>& asset) const = 0;

// Attempt to load the asset described by metadata.
// Returns true and sets asset on success; returns false on failure.
virtual bool TryLoadData(const AssetMetadata& metadata, Ref<Asset>& asset) const = 0;
};
}
213 changes: 213 additions & 0 deletions Core/Source/Lux/Asset/AssimpMeshImporter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
#include "lpch.h"
#include "AssimpMeshImporter.h"

#include "Lux/Core/Math/AABB.h"
#include "Lux/Renderer/VertexBuffer.h"
#include "Lux/Renderer/IndexBuffer.h"

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

#define GLM_ENABLE_EXPERIMENTAL
#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtc/type_ptr.hpp>

namespace Lux
{
static constexpr uint32_t s_InvalidParentIndex = 0xffffffffu;

static glm::mat4 AssimpMat4ToGlm(const aiMatrix4x4& m)
{
glm::mat4 result;
result[0][0] = m.a1; result[1][0] = m.a2; result[2][0] = m.a3; result[3][0] = m.a4;
result[0][1] = m.b1; result[1][1] = m.b2; result[2][1] = m.b3; result[3][1] = m.b4;
result[0][2] = m.c1; result[1][2] = m.c2; result[2][2] = m.c3; result[3][2] = m.c4;
result[0][3] = m.d1; result[1][3] = m.d2; result[2][3] = m.d3; result[3][3] = m.d4;
return result;
}

AssimpMeshImporter::AssimpMeshImporter(const std::filesystem::path& path)
: m_Path(path)
{
}

void AssimpMeshImporter::TraverseNodes(Ref<MeshSource> meshSource,
aiNode* node,
const glm::mat4& parentTransform,
uint32_t parentIndex)
{
glm::mat4 localTransform = AssimpMat4ToGlm(node->mTransformation);
glm::mat4 worldTransform = parentTransform * localTransform;

MeshNode luxNode;
luxNode.Name = node->mName.C_Str();
luxNode.LocalTransform = localTransform;
luxNode.Parent = parentIndex;

uint32_t nodeIndex = (uint32_t)meshSource->m_Nodes.size();
meshSource->m_Nodes.push_back(luxNode);

if (parentIndex != s_InvalidParentIndex)
meshSource->m_Nodes[parentIndex].Children.push_back(nodeIndex);

auto& currentNode = meshSource->m_Nodes[nodeIndex];

for (uint32_t i = 0; i < node->mNumMeshes; i++)
{
uint32_t submeshIndex = node->mMeshes[i];
currentNode.Submeshes.push_back(submeshIndex);
meshSource->m_Submeshes[submeshIndex].Transform = worldTransform;
meshSource->m_Submeshes[submeshIndex].LocalTransform = localTransform;
meshSource->m_Submeshes[submeshIndex].NodeName = node->mName.C_Str();
}

for (uint32_t i = 0; i < node->mNumChildren; i++)
TraverseNodes(meshSource, node->mChildren[i], worldTransform, nodeIndex);
}

Ref<MeshSource> AssimpMeshImporter::ImportToMeshSource()
{
Ref<MeshSource> meshSource = Ref<MeshSource>::Create();
meshSource->m_FilePath = m_Path.string();

Assimp::Importer importer;
importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);

constexpr uint32_t meshImportFlags =
aiProcess_CalcTangentSpace |
aiProcess_Triangulate |
aiProcess_SortByPType |
aiProcess_GenNormals |
aiProcess_GenUVCoords |
aiProcess_OptimizeMeshes |
aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights |
aiProcess_ValidateDataStructure |
aiProcess_GlobalScale;

const aiScene* scene = importer.ReadFile(m_Path.string(), meshImportFlags);
if (!scene || !scene->HasMeshes())
{
LUX_CORE_ERROR("AssimpMeshImporter: Failed to import mesh from '{}'", m_Path.string());
LUX_CORE_ERROR(" Assimp error: {}", importer.GetErrorString());
return nullptr;
}

// ── Reserve submeshes ─────────────────────────────────────────────────
meshSource->m_Submeshes.reserve(scene->mNumMeshes);

uint32_t vertexCount = 0;
uint32_t indexCount = 0;

meshSource->m_BoundingBox.Min = { FLT_MAX, FLT_MAX, FLT_MAX };
meshSource->m_BoundingBox.Max = { -FLT_MAX, -FLT_MAX, -FLT_MAX };

for (uint32_t m = 0; m < scene->mNumMeshes; m++)
{
aiMesh* mesh = scene->mMeshes[m];

Submesh& submesh = meshSource->m_Submeshes.emplace_back();
submesh.BaseVertex = vertexCount;
submesh.BaseIndex = indexCount;
submesh.MaterialIndex = mesh->mMaterialIndex;
submesh.IndexCount = mesh->mNumFaces * 3;
submesh.VertexCount = mesh->mNumVertices;
submesh.MeshName = mesh->mName.C_Str();

vertexCount += mesh->mNumVertices;
indexCount += submesh.IndexCount;

// ── AABB per submesh ──────────────────────────────────────────────
AABB& aabb = submesh.BoundingBox;
aabb.Min = { FLT_MAX, FLT_MAX, FLT_MAX };
aabb.Max = { -FLT_MAX, -FLT_MAX, -FLT_MAX };

for (uint32_t v = 0; v < mesh->mNumVertices; v++)
{
Vertex vertex;
vertex.Position = { mesh->mVertices[v].x, mesh->mVertices[v].y, mesh->mVertices[v].z };
vertex.Normal = { mesh->mNormals[v].x, mesh->mNormals[v].y, mesh->mNormals[v].z };

if (mesh->HasTangentsAndBitangents())
{
vertex.Tangent = { mesh->mTangents[v].x, mesh->mTangents[v].y, mesh->mTangents[v].z };
vertex.Binormal = { mesh->mBitangents[v].x, mesh->mBitangents[v].y, mesh->mBitangents[v].z };
}

if (mesh->HasTextureCoords(0))
vertex.Texcoord = { mesh->mTextureCoords[0][v].x, mesh->mTextureCoords[0][v].y };
else
vertex.Texcoord = { 0.0f, 0.0f };

aabb.Min.x = glm::min(vertex.Position.x, aabb.Min.x);
aabb.Min.y = glm::min(vertex.Position.y, aabb.Min.y);
aabb.Min.z = glm::min(vertex.Position.z, aabb.Min.z);
aabb.Max.x = glm::max(vertex.Position.x, aabb.Max.x);
aabb.Max.y = glm::max(vertex.Position.y, aabb.Max.y);
aabb.Max.z = glm::max(vertex.Position.z, aabb.Max.z);

meshSource->m_Vertices.push_back(vertex);
}

meshSource->m_BoundingBox.Min.x = glm::min(aabb.Min.x, meshSource->m_BoundingBox.Min.x);
meshSource->m_BoundingBox.Min.y = glm::min(aabb.Min.y, meshSource->m_BoundingBox.Min.y);
meshSource->m_BoundingBox.Min.z = glm::min(aabb.Min.z, meshSource->m_BoundingBox.Min.z);
meshSource->m_BoundingBox.Max.x = glm::max(aabb.Max.x, meshSource->m_BoundingBox.Max.x);
meshSource->m_BoundingBox.Max.y = glm::max(aabb.Max.y, meshSource->m_BoundingBox.Max.y);
meshSource->m_BoundingBox.Max.z = glm::max(aabb.Max.z, meshSource->m_BoundingBox.Max.z);

// ── Indices ───────────────────────────────────────────────────────
for (uint32_t f = 0; f < mesh->mNumFaces; f++)
{
const aiFace& face = mesh->mFaces[f];
LUX_CORE_ASSERT(face.mNumIndices == 3, "Only triangles are supported!");
Index idx;
idx.V1 = face.mIndices[0];
idx.V2 = face.mIndices[1];
idx.V3 = face.mIndices[2];
meshSource->m_Indices.push_back(idx);
}
}

// ── Node hierarchy ────────────────────────────────────────────────────
// Insert sentinel root so every real node has a valid parentIndex
meshSource->m_Nodes.emplace_back(); // root placeholder
Comment on lines +175 to +176
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The node hierarchy construction inserts a root placeholder at m_Nodes[0], but then calls TraverseNodes(..., parentIndex = s_InvalidParentIndex), so the real imported root ends up at index 1 and MeshSource::GetRootNode() returns the empty placeholder instead of the actual root.

Either populate the placeholder with the imported root node (and use parentIndex 0 for its children), or remove the placeholder and update GetRootNode()/root assumptions accordingly.

Suggested change
// Insert sentinel root so every real node has a valid parentIndex
meshSource->m_Nodes.emplace_back(); // root placeholder
// Store the imported scene root at m_Nodes[0] so GetRootNode() returns
// the actual root node rather than an empty placeholder.

Copilot uses AI. Check for mistakes.
TraverseNodes(meshSource, scene->mRootNode, glm::mat4(1.0f), s_InvalidParentIndex);

// ── Materials (allocate zero-material placeholders) ───────────────────
meshSource->m_Materials.resize(scene->mNumMaterials, 0);

// ── Triangle cache ────────────────────────────────────────────────────
for (uint32_t i = 0; i < (uint32_t)meshSource->m_Submeshes.size(); i++)
{
const Submesh& sm = meshSource->m_Submeshes[i];
for (uint32_t f = 0; f < sm.IndexCount / 3; f++)
{
const Index& idx = meshSource->m_Indices[sm.BaseIndex / 3 + f];
meshSource->m_TriangleCache[i].emplace_back(
meshSource->m_Vertices[sm.BaseVertex + idx.V1],
meshSource->m_Vertices[sm.BaseVertex + idx.V2],
meshSource->m_Vertices[sm.BaseVertex + idx.V3]);
}
}

// ── GPU buffers ───────────────────────────────────────────────────────
meshSource->m_VertexBuffer = VertexBuffer::Create(
Buffer(meshSource->m_Vertices.data(),
(uint32_t)(meshSource->m_Vertices.size() * sizeof(Vertex))));

meshSource->m_IndexBuffer = IndexBuffer::Create(
Buffer(meshSource->m_Indices.data(),
(uint32_t)(meshSource->m_Indices.size() * sizeof(Index))));

LUX_CORE_INFO("AssimpMeshImporter: Loaded '{}' – {} submeshes, {} vertices, {} indices",
m_Path.filename().string(),
meshSource->m_Submeshes.size(),
meshSource->m_Vertices.size(),
meshSource->m_Indices.size() * 3);

return meshSource;
}
}
26 changes: 26 additions & 0 deletions Core/Source/Lux/Asset/AssimpMeshImporter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

#include "Lux/Core/Base.h"
#include "Lux/Renderer/Mesh.h"

#include <assimp/scene.h>
#include <filesystem>

namespace Lux
{
// Imports a mesh file from disk using the Assimp library and populates
// a MeshSource asset with vertices, indices, submeshes and basic material handles.
class AssimpMeshImporter
{
public:
explicit AssimpMeshImporter(const std::filesystem::path& path);

// Load the mesh source. Returns nullptr on failure.
Ref<MeshSource> ImportToMeshSource();

private:
void TraverseNodes(Ref<MeshSource> meshSource, aiNode* node, const glm::mat4& parentTransform, uint32_t parentIndex);

std::filesystem::path m_Path;
};
}
Loading