Compare commits

...

2 Commits

Author SHA1 Message Date
OusmBlueNinja
7cee708801 Refactors asset loading for textures
Removes Assimp dependency.

Changes the asset loading process to use a queue system for texture uploads to OpenGL, which improves performance by offloading the texture loading to the main thread.

The asset manager now directly loads texture data using stb_image and uploads it to the GPU in the main thread, eliminating the need for Assimp.
2025-05-21 13:59:27 -05:00
OusmBlueNinja
86c90cbf8d Chore: Bumped Versions 2025-05-21 12:34:23 -05:00
8 changed files with 297 additions and 258 deletions

View File

@ -48,17 +48,7 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(glfw)
# Assimp
FetchContent_Declare(
assimp
GIT_REPOSITORY https://github.com/assimp/assimp.git
GIT_TAG master
)
set(ASSIMP_NO_EXPORT ON CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_ASSIMP_TOOLS OFF CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(assimp)
# ImGui (Docking)
FetchContent_Declare(
@ -145,7 +135,6 @@ target_link_libraries(Editor PRIVATE
Core
ImGui
glfw
assimp::assimp
GLEW::GLEW
yaml-cpp
${CMAKE_DL_LIBS}

View File

@ -1,196 +1,189 @@
#include "AssetManager.h"
#include "assets/Texture2D.h"
#include "Logger.h"
#include <string>
#include <stb/stb_image.h>
#include <GL/glew.h>
#include <fstream>
#include <yaml-cpp/yaml.h>
namespace fs = std::filesystem;
namespace OX
{
std::unordered_map<std::string, std::shared_ptr<Asset> > AssetManager::s_LoadedAssets;
std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap;
std::shared_ptr<ResourceTreeNode> AssetManager::s_FileTree = std::make_shared<ResourceTreeNode>();
namespace OX {
std::mutex AssetManager::s_QueueMutex;
std::queue<std::string> AssetManager::s_DeferredLoadQueue;
std::unordered_map<std::string, std::shared_ptr<Asset>> AssetManager::s_LoadedAssets;
std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap;
std::shared_ptr<ResourceTreeNode> AssetManager::s_FileTree = std::make_shared<ResourceTreeNode>();
fs::path AssetManager::s_ProjectRoot;
std::atomic<bool> AssetManager::s_Scanning = false;
std::thread AssetManager::s_ScanThread;
std::mutex AssetManager::s_TextureQueueMutex;
std::queue<AssetManager::PendingTexture> AssetManager::s_TextureUploadQueue;
void AssetManager::Init(const std::string &projectRoot)
{
s_ProjectRoot = fs::absolute(projectRoot);
s_Scanning = true;
s_FileTree = std::make_shared<ResourceTreeNode>();
s_FileTree->name = "res://";
s_FileTree->path = "res://";
s_FileTree->isDirectory = true;
fs::path AssetManager::s_ProjectRoot;
std::atomic<bool> AssetManager::s_Scanning = false;
std::thread AssetManager::s_ScanThread;
void AssetManager::Init(const std::string& projectRoot) {
s_ProjectRoot = fs::absolute(projectRoot);
s_Scanning = true;
s_FileTree = std::make_shared<ResourceTreeNode>();
s_FileTree->name = "res://";
s_FileTree->path = "res://";
s_FileTree->isDirectory = true;
s_ScanThread = std::thread([=] {
BackgroundScan(s_ProjectRoot);
s_Scanning = false;
});
}
void AssetManager::Shutdown() {
if (s_ScanThread.joinable())
s_ScanThread.join();
}
void AssetManager::Tick() {
std::lock_guard<std::mutex> lock(s_TextureQueueMutex);
if (!s_TextureUploadQueue.empty()) {
auto pending = s_TextureUploadQueue.front();
s_TextureUploadQueue.pop();
GLuint texID;
glGenTextures(1, &texID);
glBindTexture(GL_TEXTURE_2D, texID);
GLenum format = pending.channels == 4 ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, pending.width, pending.height, 0, format, GL_UNSIGNED_BYTE, pending.data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(pending.data);
auto tex = std::make_shared<Texture2D>();
tex->SetFromGL(texID, pending.width, pending.height);
s_LoadedAssets[pending.id] = tex;
Logger::LogInfo("%u | %u", tex->GetID(), texID);
s_ScanThread = std::thread([=]
{
BackgroundScan(s_ProjectRoot);
s_Scanning = false;
});
}
}
void AssetManager::Shutdown()
{
if (s_ScanThread.joinable())
s_ScanThread.join();
}
void AssetManager::Rescan() {
if (s_Scanning) return;
s_Scanning = true;
void AssetManager::Tick()
{
std::lock_guard<std::mutex> lock(s_QueueMutex);
if (!s_DeferredLoadQueue.empty()) {
auto resPath = s_DeferredLoadQueue.front();
s_DeferredLoadQueue.pop();
LoadAsset(resPath);
}
}
s_FileTree = std::make_shared<ResourceTreeNode>();
s_FileTree->name = "res://";
s_FileTree->path = "res://";
s_FileTree->isDirectory = true;
s_ScanThread = std::thread([=] {
BackgroundScan(s_ProjectRoot);
s_Scanning = false;
});
}
void AssetManager::SaveAssetPack(const std::string& outputPath)
{
YAML::Emitter out;
out << YAML::BeginMap;
out << YAML::Key << "assets" << YAML::Value << YAML::BeginSeq;
void AssetManager::BackgroundScan(const fs::path& root) {
for (const auto& entry : fs::recursive_directory_iterator(root)) {
const auto& path = entry.path();
std::string resPath = MakeVirtualPath(path);
for (const auto& [resPath, meta] : s_MetadataMap) {
fs::path relative = fs::relative(meta.absolutePath, s_ProjectRoot);
if (entry.is_directory()) {
AddToTree(resPath, true);
} else {
AddToTree(resPath, false);
out << YAML::BeginMap;
out << YAML::Key << "id" << YAML::Value << resPath;
out << YAML::Key << "type" << YAML::Value << meta.type;
out << YAML::Key << "path" << YAML::Value << relative.generic_string();
out << YAML::EndMap;
}
out << YAML::EndSeq;
out << YAML::EndMap;
std::ofstream fout(outputPath);
fout << out.c_str();
fout.close();
Logger::LogInfo("Saved asset pack: %s", outputPath.c_str());
}
void AssetManager::Rescan()
{
if (s_Scanning) return;
s_Scanning = true;
s_FileTree = std::make_shared<ResourceTreeNode>();
s_FileTree->name = "res://";
s_FileTree->path = "res://";
s_FileTree->isDirectory = true;
s_ScanThread = std::thread([=]
{
BackgroundScan(s_ProjectRoot);
s_Scanning = false;
});
}
void AssetManager::BackgroundScan(const fs::path &root)
{
for (const auto &entry: fs::recursive_directory_iterator(root)) {
const auto &path = entry.path();
std::string resPath = MakeVirtualPath(path);
if (entry.is_directory()) {
AddToTree(resPath, true);
} else {
AddToTree(resPath, false);
std::string type = DetectAssetType(path);
if (!type.empty()) {
std::lock_guard<std::mutex> lock(s_QueueMutex);
s_MetadataMap[resPath] = {type, path.string()};
s_DeferredLoadQueue.push(resPath);
std::string type = DetectAssetType(path);
if (!type.empty()) {
if (type == "texture2D") {
int w, h, ch;
stbi_set_flip_vertically_on_load(1);
unsigned char* data = stbi_load(path.string().c_str(), &w, &h, &ch, 0);
if (data) {
std::lock_guard<std::mutex> lock(s_TextureQueueMutex);
s_TextureUploadQueue.push({ resPath, path.string(), w, h, ch, data });
s_MetadataMap[resPath] = { type, path.string() };
}
}
}
}
}
}
void AssetManager::AddToTree(const std::string &resPath, bool isDir)
{
std::vector<std::string> parts;
size_t pos = 0, next;
while ((next = resPath.find('/', pos)) != std::string::npos) {
if (next != pos) parts.push_back(resPath.substr(pos, next - pos));
pos = next + 1;
}
if (pos < resPath.length()) parts.push_back(resPath.substr(pos));
void AssetManager::AddToTree(const std::string& resPath, bool isDir) {
std::vector<std::string> parts;
size_t pos = 0, next;
while ((next = resPath.find('/', pos)) != std::string::npos) {
if (next != pos) parts.push_back(resPath.substr(pos, next - pos));
pos = next + 1;
}
if (pos < resPath.length()) parts.push_back(resPath.substr(pos));
auto current = s_FileTree;
for (size_t i = 1; i < parts.size(); ++i) {
auto &name = parts[i];
auto it = std::find_if(current->children.begin(), current->children.end(), [&](auto &child)
{
return child->name == name;
});
auto current = s_FileTree;
for (size_t i = 1; i < parts.size(); ++i) {
auto& name = parts[i];
auto it = std::find_if(current->children.begin(), current->children.end(), [&](auto& child) {
return child->name == name;
});
if (it == current->children.end()) {
auto newNode = std::make_shared<ResourceTreeNode>();
newNode->name = name;
newNode->path = current->path + "/" + name;
newNode->isDirectory = (i < parts.size() - 1) || isDir;
current->children.push_back(newNode);
current = newNode;
} else {
current = *it;
}
if (it == current->children.end()) {
auto newNode = std::make_shared<ResourceTreeNode>();
newNode->name = name;
newNode->path = current->path + "/" + name;
newNode->isDirectory = (i < parts.size() - 1) || isDir;
current->children.push_back(newNode);
current = newNode;
} else {
current = *it;
}
}
}
std::string AssetManager::MakeVirtualPath(const fs::path &full)
{
fs::path rel = fs::relative(full, s_ProjectRoot);
return "res://" + rel.generic_string();
std::string AssetManager::MakeVirtualPath(const fs::path& full) {
fs::path rel = fs::relative(full, s_ProjectRoot);
return "res://" + rel.generic_string();
}
std::string AssetManager::DetectAssetType(const fs::path& ext) {
auto e = ext.extension().string();
if (e == ".png" || e == ".jpg") return "texture2D";
return "";
}
std::shared_ptr<Asset> AssetManager::Get(const std::string& resPath) {
auto it = s_LoadedAssets.find(resPath);
return (it != s_LoadedAssets.end()) ? it->second : nullptr;
}
void AssetManager::SaveAssetPack(const std::string& outputPath) {
YAML::Emitter out;
out << YAML::BeginMap;
out << YAML::Key << "assets" << YAML::Value << YAML::BeginSeq;
for (const auto& [resPath, meta] : s_MetadataMap) {
fs::path relative = fs::relative(meta.absolutePath, s_ProjectRoot);
out << YAML::BeginMap;
out << YAML::Key << "id" << YAML::Value << resPath;
out << YAML::Key << "type" << YAML::Value << meta.type;
out << YAML::Key << "path" << YAML::Value << relative.generic_string();
out << YAML::EndMap;
}
std::string AssetManager::DetectAssetType(const fs::path &ext)
{
auto e = ext.extension().string();
if (e == ".png" || e == ".jpg") return "texture2D";
if (e == ".ogg" || e == ".wav") return "audio";
return "";
}
out << YAML::EndSeq;
out << YAML::EndMap;
std::shared_ptr<Asset> AssetManager::Get(const std::string &resPath)
{
auto it = s_LoadedAssets.find(resPath);
return (it != s_LoadedAssets.end()) ? it->second : nullptr;
}
std::ofstream fout(outputPath);
fout << out.c_str();
fout.close();
bool AssetManager::LoadAsset(const std::string &resPath)
{
auto it = s_MetadataMap.find(resPath);
if (it == s_MetadataMap.end()) return false;
Logger::LogInfo("Saved asset pack: %s", outputPath.c_str());
}
const auto &meta = it->second;
std::shared_ptr<Asset> asset;
std::shared_ptr<ResourceTreeNode> AssetManager::GetFileTree() {
return s_FileTree;
}
if (meta.type == "texture2D")
asset = std::make_shared<Texture2D>();
if (asset && asset->LoadFromFile(meta.absolutePath)) {
s_LoadedAssets[resPath] = asset;
Logger::LogDebug("Loaded asset: %s", resPath.c_str());
return true;
}
Logger::LogError("Failed to load: %s", resPath.c_str());
return false;
}
std::shared_ptr<ResourceTreeNode> AssetManager::GetFileTree()
{
return s_FileTree;
}
} // namespace OX

View File

@ -1,12 +1,11 @@
#pragma once
#include <string>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <memory>
#include <filesystem>
#include <mutex>
#include <filesystem>
#include <thread>
#include <atomic>
@ -16,7 +15,7 @@ namespace OX {
struct ResourceTreeNode {
std::string name;
std::string path; // res://...
std::string path;
bool isDirectory = false;
std::vector<std::shared_ptr<ResourceTreeNode>> children;
};
@ -25,15 +24,12 @@ namespace OX {
public:
static void Init(const std::string& projectRoot);
static void Shutdown();
static void Tick(); // Call every frame
static void SaveAssetPack(const std::string& outputPath);
static void Tick(); // Main-thread
static void Rescan();
static std::shared_ptr<Asset> Get(const std::string& resPath);
static std::shared_ptr<ResourceTreeNode> GetFileTree();
static bool LoadAsset(const std::string& resPath);
static void Rescan(); // Rebuilds file tree (slow)
static void SaveAssetPack(const std::string& outputPath);
private:
struct AssetMetadata {
@ -41,6 +37,13 @@ namespace OX {
std::string absolutePath;
};
struct PendingTexture {
std::string id;
std::string path;
int width, height, channels;
unsigned char* data;
};
static void BackgroundScan(const std::filesystem::path& root);
static void AddToTree(const std::string& virtualPath, bool isDir);
static std::string DetectAssetType(const std::filesystem::path& ext);
@ -50,8 +53,8 @@ namespace OX {
static std::unordered_map<std::string, AssetMetadata> s_MetadataMap;
static std::shared_ptr<ResourceTreeNode> s_FileTree;
static std::mutex s_QueueMutex;
static std::queue<std::string> s_DeferredLoadQueue;
static std::mutex s_TextureQueueMutex;
static std::queue<PendingTexture> s_TextureUploadQueue;
static std::filesystem::path s_ProjectRoot;
static std::atomic<bool> s_Scanning;

View File

@ -7,42 +7,20 @@
#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>
#include <GL/glew.h>
#include "Texture2D.h"
namespace OX
{
Texture2D::Texture2D() = default;
namespace OX {
Texture2D::~Texture2D()
{
if (m_TextureID != 0) {
Texture2D::~Texture2D() {
if (m_TextureID)
glDeleteTextures(1, &m_TextureID);
}
}
bool Texture2D::LoadFromFile(const std::string &path)
{
stbi_set_flip_vertically_on_load(1);
int channels;
unsigned char *data = stbi_load(path.c_str(), &m_Width, &m_Height, &channels, 0);
if (!data) {
Logger::LogError("Failed to load texture: %s", path.c_str());
return false;
}
GLenum format = (channels == 4) ? GL_RGBA : GL_RGB;
glGenTextures(1, &m_TextureID);
glBindTexture(GL_TEXTURE_2D, m_TextureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, m_Width, m_Height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
return true;
void Texture2D::SetFromGL(uint32_t texID, int width, int height) {
m_TextureID = texID;
m_Width = width;
m_Height = height;
}
} // namespace OX
}

View File

@ -1,27 +1,23 @@
//
// Created by spenc on 5/21/2025.
//
#pragma once
#include "systems/Asset.h"
#include <string>
#include <cstdint>
#include "GL/glew.h"
namespace OX
{
class Texture2D : public Asset
{
namespace OX {
class Texture2D : public Asset {
public:
Texture2D();
Texture2D() = default;
~Texture2D();
bool LoadFromFile(const std::string &path) override;
std::string GetTypeName() const override { return "texture2D"; }
bool LoadFromFile(const std::string& path) override { return false; } // Not used now
uint32_t GetID() const { return m_TextureID; }
void SetFromGL(uint32_t texID, int width, int height);
GLuint GetID() const { return m_TextureID; }
int GetWidth() const { return m_Width; }
int GetHeight() const { return m_Height; }
@ -30,4 +26,4 @@ namespace OX
int m_Width = 0;
int m_Height = 0;
};
} // OX
};

View File

@ -28,10 +28,7 @@ namespace OX
operator glm::vec3() const { return {r, g, b}; }
operator glm::vec4() const { return {r, g, b, a}; }
#ifdef HAS_IMGUI
operator ImVec3() const { return {r, g, b}; }
operator ImVec4() const { return {r, g, b, a}; }
#endif
// Arithmetic

View File

@ -4,7 +4,7 @@
#ifndef EDITOR_H
#define EDITOR_H
#define OX_EDITOR_VERSION "Obsidian Editor (0.1.5)"
#define OX_EDITOR_VERSION "Obsidian Editor (0.1.8)"
#include "Layer.h"
#include "Core.h"

View File

@ -1,47 +1,116 @@
//
// Created by spenc on 5/21/2025.
//
#include "FileBrowser.h"
#include "systems/AssetManager.h"
#include "systems/assets/Texture2D.h"
#include "imgui.h"
#include <filesystem>
namespace OX {
namespace OX
{
static std::string s_CurrentPath = "res://";
static void DrawGrid(const std::shared_ptr<ResourceTreeNode>& node) {
const int columns = 4;
int itemIndex = 0;
static void DrawGrid(const std::shared_ptr<ResourceTreeNode> &node)
{
const float thumbSize = 64.0f;
const float padding = 8.0f;
const float labelHeight = 20.0f;
const float cellWidth = thumbSize + padding * 2;
const float cellHeight = thumbSize + labelHeight + padding;
ImVec2 region = ImGui::GetContentRegionAvail();
int columns = std::max(1, (int) (region.x / cellWidth));
ImGui::BeginChild("FileGrid", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysUseWindowPadding);
ImGui::Columns(columns, nullptr, false);
for (const auto& child : node->children) {
std::string label = (child->isDirectory ? "[folder] " : "[file] ") + child->name;
for (const auto &child: node->children) {
ImGui::PushID(child->path.c_str()); // Unique ID per item
if (ImGui::Selectable(label.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, 64))) {
if (child->isDirectory && ImGui::IsMouseDoubleClicked(0)) {
s_CurrentPath = child->path;
ImGui::BeginGroup();
std::string tooltip;
std::string label = child->name;
ImVec2 cellSize(cellWidth, cellHeight);
ImGui::InvisibleButton("Cell", cellSize);
bool hovered = ImGui::IsItemHovered();
bool clicked = ImGui::IsItemClicked();
// Background
ImVec2 min = ImGui::GetItemRectMin();
ImVec2 max = ImGui::GetItemRectMax();
ImGui::GetWindowDrawList()->AddRectFilled(min, max, IM_COL32(40, 40, 40, 255), 4.0f);
ImVec2 center = ImVec2(min.x + padding, min.y + padding);
// === Thumbnail ===
if (child->isDirectory) {
ImGui::SetCursorScreenPos(center);
ImGui::Text("[Folder]");
} else {
auto asset = AssetManager::Get(child->path);
if (asset) {
std::string type = asset->GetTypeName();
if (type == "texture2D") {
auto tex = std::static_pointer_cast<Texture2D>(asset);
if (tex->GetID() != 0) {
ImGui::SetCursorScreenPos(center);
ImGui::Image((ImTextureID) (intptr_t) tex->GetID(), ImVec2(thumbSize, thumbSize),
ImVec2(0, 1), ImVec2(1, 0));
tooltip += "Name: " + child->name + "\n";
tooltip += "ID: " + std::to_string(tex->GetID()) + "\n";
tooltip += "Size: " + std::to_string(tex->GetWidth()) + "x" + std::to_string(
tex->GetHeight()) + "\n";
tooltip += "Path: " + child->path + "\n";
label += " (ID: " + std::to_string(tex->GetID()) + ")";
} else {
ImGui::SetCursorScreenPos(center);
ImGui::Text("[Loading]");
}
} else {
ImGui::SetCursorScreenPos(center);
ImGui::Text("[Type: %s]", type.c_str());
}
} else {
ImGui::SetCursorScreenPos(center);
ImGui::Text("[Unloaded]");
}
}
// === Label ===
ImGui::SetCursorScreenPos(ImVec2(min.x + padding, max.y - labelHeight));
ImGui::PushTextWrapPos(min.x + cellWidth);
ImGui::TextUnformatted(label.c_str());
ImGui::PopTextWrapPos();
// Click handling
if (clicked && child->isDirectory) {
s_CurrentPath = child->path;
}
if (hovered && !tooltip.empty()) {
ImGui::BeginTooltip();
ImGui::TextUnformatted(tooltip.c_str());
ImGui::EndTooltip();
}
ImGui::EndGroup();
ImGui::NextColumn();
++itemIndex;
ImGui::PopID();
}
ImGui::Columns(1);
ImGui::EndChild();
}
static std::shared_ptr<ResourceTreeNode> FindNode(const std::shared_ptr<ResourceTreeNode>& root, const std::string& targetPath) {
if (root->path == targetPath) return root;
for (const auto& child : root->children) {
static std::shared_ptr<ResourceTreeNode> FindNode(const std::shared_ptr<ResourceTreeNode> &root,
const std::string &targetPath)
{
if (root->path == targetPath) return root;
for (const auto &child: root->children) {
if (child->isDirectory) {
auto found = FindNode(child, targetPath);
if (found) return found;
@ -50,9 +119,24 @@ namespace OX {
return nullptr;
}
void FileBrowser::Draw() {
void FileBrowser::Draw()
{
ImGui::Begin("File Browser");
// === Back Button ===
if (s_CurrentPath != "res://") {
if (ImGui::Button("..")) {
size_t lastSlash = s_CurrentPath.find_last_of('/');
if (lastSlash != std::string::npos && lastSlash > 6) {
// skip 'res://'
s_CurrentPath = s_CurrentPath.substr(0, lastSlash);
} else {
s_CurrentPath = "res://";
}
}
}
ImGui::SameLine();
ImGui::Text("Current Path: %s", s_CurrentPath.c_str());
ImGui::Separator();
@ -66,5 +150,4 @@ namespace OX {
ImGui::End();
}
} // namespace OX