Implements asset management system
Introduces a new asset management system for loading and managing game assets, including textures and models. This system includes: - Asset scanning and indexing with background loading. - Virtual file system for asset identification. - Resource tree for UI file browsing. - Lazy loading of textures with garbage collection. - OBJ model loading via Assimp integration Fixes file browser integration and asset handling.
This commit is contained in:
@@ -106,6 +106,7 @@ add_library(Core STATIC
|
||||
src/core/systems/Scene/Components/CameraComponent.h
|
||||
src/core/systems/Scene/Components/PointLightComponent.cpp
|
||||
src/core/systems/Scene/Components/PointLightComponent.h
|
||||
src/core/systems/assets/Model.h
|
||||
)
|
||||
|
||||
target_include_directories(Core PUBLIC src/core)
|
||||
|
||||
@@ -1,55 +1,172 @@
|
||||
// AssetManager.cpp
|
||||
|
||||
#include "AssetManager.h"
|
||||
#include "assets/Texture2D.h"
|
||||
#include "Logger.h"
|
||||
#include "Profiler.h"
|
||||
|
||||
|
||||
#include <stb/stb_image.h>
|
||||
#include <GL/glew.h>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <ranges>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "Profiler.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace OX
|
||||
{
|
||||
// statics
|
||||
std::unordered_map<std::string, std::shared_ptr<Asset> > AssetManager::s_LoadedAssets;
|
||||
std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::atomic<size_t> AssetManager::s_TotalTexturesToLoad{0};
|
||||
std::atomic<size_t> AssetManager::s_LoadedTexturesCount{0};
|
||||
// ============================================================================
|
||||
// Statics
|
||||
// ============================================================================
|
||||
std::filesystem::path AssetManager::s_ProjectRoot;
|
||||
|
||||
// Map from an actual file path to virtual ID
|
||||
std::unordered_map<std::string, std::string> AssetManager::s_PathToID;
|
||||
std::mutex AssetManager::s_AssetMutex;
|
||||
std::mutex AssetManager::s_TreeMutex;
|
||||
std::unordered_map<uint64_t, AssetManager::AssetRecord> AssetManager::s_RecordsById;
|
||||
std::unordered_map<uint64_t, AssetMetadata> AssetManager::s_MetaById;
|
||||
std::unordered_map<std::string, uint64_t> AssetManager::s_PathToId;
|
||||
std::unordered_map<std::string, uint64_t> AssetManager::s_VPathToId;
|
||||
|
||||
std::shared_ptr<ResourceTreeNode> AssetManager::s_FileTree = std::make_shared<ResourceTreeNode>();
|
||||
|
||||
std::filesystem::path AssetManager::s_ProjectRoot;
|
||||
std::atomic<bool> AssetManager::s_Scanning{true};
|
||||
std::thread AssetManager::s_ScanThread;
|
||||
|
||||
std::queue<AssetManager::PendingTexture> AssetManager::s_TextureQueue;
|
||||
std::mutex AssetManager::s_QueueMutex;
|
||||
std::mutex AssetManager::s_AssetMutex;
|
||||
std::mutex AssetManager::s_TreeMutex;
|
||||
|
||||
std::atomic<double> AssetManager::s_GCSeconds{120.0};
|
||||
|
||||
std::atomic<size_t> AssetManager::s_TotalTexturesDiscovered{0};
|
||||
std::atomic<size_t> AssetManager::s_LoadedTextures{0};
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
static inline uint64_t fnv1a64(const char *s)
|
||||
{
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
while (*s) {
|
||||
h ^= (uint8_t) *s++;
|
||||
h *= 1099511628211ull;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
uint64_t AssetManager::MakeIdFromVirtualPath_(const std::string &vpath)
|
||||
{
|
||||
return fnv1a64(vpath.c_str());
|
||||
}
|
||||
|
||||
OX_AssetType AssetManager::DetectAssetKind_(const fs::path &path)
|
||||
{
|
||||
std::string ext = path.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
||||
|
||||
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || ext == ".tga")
|
||||
return OX_AssetType::Texture;
|
||||
if (ext == ".obj")
|
||||
return OX_AssetType::Model;
|
||||
if (ext == ".ttf" || ext == ".otf")
|
||||
return OX_AssetType::Font;
|
||||
if (ext == ".wav" || ext == ".ogg" || ext == ".mp3")
|
||||
return OX_AssetType::Sound;
|
||||
return OX_AssetType::Unknown;
|
||||
}
|
||||
|
||||
void AssetManager::Touch_(AssetRecord &rec)
|
||||
{
|
||||
rec.lastAccess = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Texture loading (sync, creates GL texture now)
|
||||
// ============================================================================
|
||||
std::shared_ptr<Texture2D> AssetManager::LoadTextureNow_(const AssetMetadata &meta, size_t &outBytes)
|
||||
{
|
||||
int w = 0, h = 0, ch = 0;
|
||||
stbi_set_flip_vertically_on_load(1);
|
||||
unsigned char *data = stbi_load(meta.absolutePath.c_str(), &w, &h, &ch, 0);
|
||||
if (!data) {
|
||||
Logger::LogWarning("Texture failed to load: %s", meta.absolutePath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GLuint texID = 0;
|
||||
glGenTextures(1, &texID);
|
||||
glBindTexture(GL_TEXTURE_2D, texID);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
GLenum format = GL_RED, internal = GL_R8;
|
||||
switch (ch) {
|
||||
case 1: format = GL_RED;
|
||||
internal = GL_R8;
|
||||
break;
|
||||
case 2: format = GL_RG;
|
||||
internal = GL_RG8;
|
||||
break;
|
||||
case 3: format = GL_RGB;
|
||||
internal = GL_RGB8;
|
||||
break;
|
||||
case 4: format = GL_RGBA;
|
||||
internal = GL_RGBA8;
|
||||
break;
|
||||
default:
|
||||
Logger::LogWarning("Texture '%s' unexpected channels=%d, forcing RGBA", meta.absolutePath.c_str(), ch);
|
||||
format = GL_RGBA;
|
||||
internal = GL_RGBA8;
|
||||
ch = 4;
|
||||
break;
|
||||
}
|
||||
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, internal, w, h, 0, format, GL_UNSIGNED_BYTE, data);
|
||||
|
||||
if (ch == 1) {
|
||||
GLint sw[] = {GL_RED, GL_RED, GL_RED, GL_ONE};
|
||||
glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, sw);
|
||||
} else if (ch == 2) {
|
||||
GLint sw[] = {GL_RED, GL_GREEN, GL_ZERO, GL_ONE};
|
||||
glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, sw);
|
||||
}
|
||||
|
||||
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);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
auto tex = std::make_shared<Texture2D>();
|
||||
tex->SetFromGL(texID, w, h);
|
||||
|
||||
outBytes = size_t(w) * size_t(h) * size_t(ch);
|
||||
return tex;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Public API
|
||||
// ============================================================================
|
||||
void AssetManager::Init(const std::string &projectRoot)
|
||||
{
|
||||
s_ProjectRoot = fs::absolute(projectRoot);
|
||||
s_Scanning = true;
|
||||
s_Scanning = true; {
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
s_RecordsById.clear();
|
||||
s_MetaById.clear();
|
||||
s_PathToId.clear();
|
||||
s_VPathToId.clear();
|
||||
s_TotalTexturesDiscovered.store(0);
|
||||
s_LoadedTextures.store(0);
|
||||
} {
|
||||
std::scoped_lock lk(s_TreeMutex);
|
||||
s_FileTree = std::make_shared<ResourceTreeNode>();
|
||||
s_FileTree->name = "res://";
|
||||
s_FileTree->path = "res://";
|
||||
s_FileTree->isDirectory = true;
|
||||
}
|
||||
|
||||
s_FileTree = std::make_shared<ResourceTreeNode>();
|
||||
s_FileTree->name = "res://";
|
||||
s_FileTree->path = "res://";
|
||||
s_FileTree->isDirectory = true;
|
||||
s_TotalTexturesToLoad.store(0);
|
||||
s_LoadedTexturesCount.store(0);
|
||||
if (s_ScanThread.joinable()) s_ScanThread.join();
|
||||
s_ScanThread = std::thread(BackgroundScan);
|
||||
}
|
||||
|
||||
@@ -58,274 +175,31 @@ namespace OX
|
||||
s_Scanning = false;
|
||||
if (s_ScanThread.joinable())
|
||||
s_ScanThread.join();
|
||||
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
s_RecordsById.clear();
|
||||
s_MetaById.clear();
|
||||
s_PathToId.clear();
|
||||
s_VPathToId.clear();
|
||||
s_TotalTexturesDiscovered.store(0);
|
||||
s_LoadedTextures.store(0);
|
||||
}
|
||||
|
||||
void AssetManager::Rescan()
|
||||
{
|
||||
if (s_Scanning)
|
||||
return;
|
||||
if (s_Scanning) return;
|
||||
s_Scanning = true; {
|
||||
std::scoped_lock lk(s_TreeMutex);
|
||||
s_FileTree = std::make_shared<ResourceTreeNode>();
|
||||
s_FileTree->name = "res://";
|
||||
s_FileTree->path = "res://";
|
||||
s_FileTree->isDirectory = true;
|
||||
}
|
||||
|
||||
s_Scanning = true;
|
||||
s_FileTree = std::make_shared<ResourceTreeNode>();
|
||||
s_FileTree->name = "res://";
|
||||
s_FileTree->path = "res://";
|
||||
s_FileTree->isDirectory = true;
|
||||
s_TotalTexturesToLoad.store(0);
|
||||
s_LoadedTexturesCount.store(0);
|
||||
if (s_ScanThread.joinable()) s_ScanThread.join();
|
||||
s_ScanThread = std::thread(BackgroundScan);
|
||||
}
|
||||
|
||||
void AssetManager::BackgroundScan()
|
||||
{
|
||||
size_t totalTextures = 0;
|
||||
|
||||
try {
|
||||
for (auto it = fs::recursive_directory_iterator(s_ProjectRoot,
|
||||
fs::directory_options::skip_permission_denied);
|
||||
it != fs::recursive_directory_iterator(); ++it) {
|
||||
if (!s_Scanning) break;
|
||||
const auto &entry = *it;
|
||||
if (!entry.is_directory() && DetectAssetType(entry.path()) == "texture2D")
|
||||
++totalTextures;
|
||||
}
|
||||
} catch (const fs::filesystem_error &e) {
|
||||
Logger::LogError("AssetManager scan error (count): %s", e.what());
|
||||
}
|
||||
|
||||
s_TotalTexturesToLoad.store(totalTextures, std::memory_order_relaxed);
|
||||
s_LoadedTexturesCount.store(0, std::memory_order_relaxed);
|
||||
|
||||
std::vector<std::pair<std::string, AssetMetadata> > newMetadata;
|
||||
newMetadata.reserve(256);
|
||||
|
||||
bool showed_error = false;
|
||||
|
||||
while (s_Scanning) {
|
||||
std::vector<fs::directory_entry> allEntries;
|
||||
allEntries.reserve(1024);
|
||||
|
||||
try {
|
||||
for (auto it = fs::recursive_directory_iterator(s_ProjectRoot,
|
||||
fs::directory_options::skip_permission_denied);
|
||||
it != fs::recursive_directory_iterator(); ++it) {
|
||||
if (!s_Scanning) break;
|
||||
allEntries.push_back(*it);
|
||||
}
|
||||
} catch (const fs::filesystem_error &e) {
|
||||
if (!showed_error) {
|
||||
Logger::LogError("AssetManager scan error (gather): %s", e.what());
|
||||
showed_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
newMetadata.clear();
|
||||
std::unordered_set<std::string> foundAssets;
|
||||
foundAssets.reserve(allEntries.size());
|
||||
|
||||
for (auto &entry: allEntries) {
|
||||
if (!s_Scanning) break;
|
||||
|
||||
try {
|
||||
const auto &path = entry.path();
|
||||
std::string resPath = MakeVirtualPath(path);
|
||||
foundAssets.insert(resPath);
|
||||
|
||||
if (entry.is_directory()) {
|
||||
AddToTree(resPath, true);
|
||||
continue;
|
||||
} {
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
if (s_MetadataMap.count(resPath))
|
||||
continue;
|
||||
s_MetadataMap.emplace(resPath, AssetMetadata{"", ""});
|
||||
}
|
||||
|
||||
AddToTree(resPath, false);
|
||||
|
||||
if (DetectAssetType(path) == "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) {
|
||||
Logger::LogWarning("Failed to load texture: %s", path.string().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
newMetadata.emplace_back(resPath, AssetMetadata{"texture2D", path.string()}); {
|
||||
std::lock_guard<std::mutex> qlock(s_QueueMutex);
|
||||
s_TextureQueue.push({resPath, w, h, ch, data});
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
Logger::LogError("Error processing asset '%s': %s",
|
||||
entry.path().string().c_str(),
|
||||
e.what());
|
||||
}
|
||||
catch (...) {
|
||||
Logger::LogError("Unknown error processing asset '%s'",
|
||||
entry.path().string().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!newMetadata.empty()) {
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
for (auto &kv: newMetadata)
|
||||
s_MetadataMap[kv.first] = kv.second;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// REMOVE FILES THAT NO LONGER EXIST
|
||||
// ─────────────────────────────────────────────
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
|
||||
// 1) Remove from metadata map
|
||||
for (auto it = s_MetadataMap.begin(); it != s_MetadataMap.end();) {
|
||||
if (!foundAssets.contains(it->first)) {
|
||||
Logger::LogInfo("Removing missing asset: %s", it->first.c_str());
|
||||
|
||||
// Also clear from loaded assets if present
|
||||
s_LoadedAssets.erase(it->first);
|
||||
|
||||
// Remove path->id mapping
|
||||
for (auto pit = s_PathToID.begin(); pit != s_PathToID.end();) {
|
||||
if (pit->second == it->first)
|
||||
pit = s_PathToID.erase(pit);
|
||||
else
|
||||
++pit;
|
||||
}
|
||||
|
||||
it = s_MetadataMap.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s_Scanning)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AssetManager::Tick()
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
std::lock_guard<std::mutex> qlock(s_QueueMutex);
|
||||
|
||||
while (!s_TextureQueue.empty()) {
|
||||
auto pending = s_TextureQueue.front();
|
||||
s_TextureQueue.pop();
|
||||
s_LoadedTexturesCount.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
if (!pending.data || pending.width <= 0 || pending.height <= 0) {
|
||||
Logger::LogError("Texture load failed for '%s'", pending.id.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
GLuint texID = 0;
|
||||
glGenTextures(1, &texID);
|
||||
glBindTexture(GL_TEXTURE_2D, texID);
|
||||
|
||||
// Avoid row alignment pitfalls (e.g., 1-channel images)
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
GLenum format = GL_RED; // source data format
|
||||
GLenum internal = GL_R8; // GPU storage
|
||||
GLenum type = GL_UNSIGNED_BYTE;
|
||||
|
||||
switch (pending.channels) {
|
||||
case 1: format = GL_RED;
|
||||
internal = GL_R8;
|
||||
break;
|
||||
case 2: format = GL_RG;
|
||||
internal = GL_RG8;
|
||||
break;
|
||||
case 3: format = GL_RGB;
|
||||
internal = GL_RGB8;
|
||||
break;
|
||||
case 4: format = GL_RGBA;
|
||||
internal = GL_RGBA8;
|
||||
break;
|
||||
default:
|
||||
// force RGBA if stb returned something unexpected
|
||||
format = GL_RGBA;
|
||||
internal = GL_RGBA8;
|
||||
Logger::LogWarning("Unexpected channel count %d for '%s'; forcing RGBA",
|
||||
pending.channels, pending.id.c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, internal,
|
||||
pending.width, pending.height, 0,
|
||||
format, type, pending.data);
|
||||
|
||||
// If RED/RG, set swizzles so shader sampling .rgb/.rgba works
|
||||
if (pending.channels == 1) {
|
||||
GLint swizzle[] = {GL_RED, GL_RED, GL_RED, GL_ONE};
|
||||
glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzle);
|
||||
} else if (pending.channels == 2) {
|
||||
GLint swizzle[] = {GL_RED, GL_GREEN, GL_ZERO, GL_ONE};
|
||||
glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzle);
|
||||
}
|
||||
|
||||
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);
|
||||
pending.data = nullptr;
|
||||
|
||||
auto tex = std::make_shared<Texture2D>();
|
||||
tex->SetFromGL(texID, pending.width, pending.height); {
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
s_LoadedAssets[pending.id] = tex;
|
||||
auto it = s_MetadataMap.find(pending.id);
|
||||
if (it != s_MetadataMap.end())
|
||||
s_PathToID[it->second.absolutePath] = pending.id;
|
||||
}
|
||||
|
||||
// optional: unbind to keep state clean
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::shared_ptr<Asset> AssetManager::Get(const std::string &keyOrPath)
|
||||
{
|
||||
std::string key = keyOrPath;
|
||||
const std::string triple = "res:///";
|
||||
if (key.rfind(triple, 0) == 0)
|
||||
key = std::string("res://") + key.substr(triple.size());
|
||||
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
|
||||
// 1) Direct ID lookup
|
||||
if (auto it = s_LoadedAssets.find(key); it != s_LoadedAssets.end())
|
||||
return it->second;
|
||||
|
||||
// 2) Path-to-ID
|
||||
if (auto pit = s_PathToID.find(key); pit != s_PathToID.end()) {
|
||||
if (auto ait = s_LoadedAssets.find(pit->second); ait != s_LoadedAssets.end())
|
||||
return ait->second;
|
||||
}
|
||||
|
||||
// 3) Filesystem path -> virtual
|
||||
try {
|
||||
std::string virt = MakeVirtualPath(fs::absolute(key));
|
||||
if (!virt.empty() && virt.front() == '/')
|
||||
virt.erase(0, 1);
|
||||
if (auto vit = s_LoadedAssets.find(virt); vit != s_LoadedAssets.end())
|
||||
return vit->second;
|
||||
} catch (...) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<ResourceTreeNode> AssetManager::GetFileTree()
|
||||
{
|
||||
return s_FileTree;
|
||||
@@ -337,16 +211,31 @@ namespace OX
|
||||
out << YAML::BeginMap
|
||||
<< YAML::Key << "assets"
|
||||
<< YAML::Value << YAML::BeginSeq; {
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
for (auto &[id, meta]: s_MetadataMap) {
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
for (const auto &[id, meta]: s_MetaById) {
|
||||
fs::path rel = fs::relative(meta.absolutePath, s_ProjectRoot);
|
||||
|
||||
const char *typeStr = "unknown";
|
||||
switch (meta.kind) {
|
||||
case OX_AssetType::Texture: typeStr = "texture";
|
||||
break;
|
||||
case OX_AssetType::Model: typeStr = "model";
|
||||
break;
|
||||
case OX_AssetType::Font: typeStr = "font";
|
||||
break;
|
||||
case OX_AssetType::Sound: typeStr = "sound";
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
out << YAML::BeginMap
|
||||
<< YAML::Key << "id" << YAML::Value << id
|
||||
<< YAML::Key << "type" << YAML::Value << meta.type
|
||||
<< YAML::Key << "type" << YAML::Value << typeStr
|
||||
<< YAML::Key << "path" << YAML::Value << rel.generic_string()
|
||||
<< YAML::EndMap;
|
||||
}
|
||||
}
|
||||
|
||||
out << YAML::EndSeq << YAML::EndMap;
|
||||
|
||||
std::ofstream fout(outputPath);
|
||||
@@ -359,38 +248,278 @@ namespace OX
|
||||
Logger::LogError("AssetManager: error writing to '%s'", outputPath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::LogInfo("Saved asset pack: %s", outputPath.c_str());
|
||||
}
|
||||
|
||||
std::string AssetManager::MakeVirtualPath(const fs::path &full)
|
||||
uint64_t AssetManager::GetIdForPath(const std::string &keyOrPath)
|
||||
{
|
||||
auto rel = fs::relative(full, s_ProjectRoot);
|
||||
return "res://" + rel.generic_string();
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
|
||||
// 1) already a virtual path?
|
||||
if (auto it = s_VPathToId.find(keyOrPath); it != s_VPathToId.end())
|
||||
return it->second;
|
||||
|
||||
// 2) absolute/native path?
|
||||
if (auto it2 = s_PathToId.find(keyOrPath); it2 != s_PathToId.end())
|
||||
return it2->second;
|
||||
|
||||
// 3) try to normalize filesystem path -> virtual then lookup
|
||||
try {
|
||||
std::string virt = MakeVirtualPath(fs::absolute(keyOrPath));
|
||||
if (!virt.empty() && virt.front() == '/')
|
||||
virt.erase(0, 1);
|
||||
if (auto it3 = s_VPathToId.find(virt); it3 != s_VPathToId.end())
|
||||
return it3->second;
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
return 0; // not found
|
||||
}
|
||||
|
||||
OX_AssetType AssetManager::GetAssetTypeById(uint64_t id)
|
||||
{
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
if (auto it = s_MetaById.find(id); it != s_MetaById.end())
|
||||
return it->second.kind;
|
||||
return OX_AssetType::Invalid;
|
||||
}
|
||||
|
||||
std::string AssetManager::GetAbsolutePathById(uint64_t id)
|
||||
{
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
if (auto it = s_MetaById.find(id); it != s_MetaById.end())
|
||||
return it->second.absolutePath;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string AssetManager::GetVirtualPathById(uint64_t id)
|
||||
{
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
// Reverse lookup from id -> virtual path
|
||||
for (const auto &[vpath, nid]: s_VPathToId)
|
||||
if (nid == id) return vpath;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::shared_ptr<Asset> AssetManager::GetAssetById(uint64_t id)
|
||||
{
|
||||
// Currently only textures are lazily loaded here; others return nullptr (external loaders handle them)
|
||||
switch (GetAssetTypeById(id)) {
|
||||
case OX_AssetType::Texture: return GetTexture(id);
|
||||
default: return nullptr; // Model/Font/Sound not loaded here
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Texture2D> AssetManager::GetTexture(uint64_t id)
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
|
||||
AssetMetadata meta; {
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
|
||||
auto mit = s_MetaById.find(id);
|
||||
if (mit == s_MetaById.end() || mit->second.kind != OX_AssetType::Texture)
|
||||
return nullptr;
|
||||
|
||||
meta = mit->second;
|
||||
|
||||
// already loaded?
|
||||
if (auto rit = s_RecordsById.find(id); rit != s_RecordsById.end()) {
|
||||
if (rit->second.asset) {
|
||||
Touch_(rit->second);
|
||||
return std::dynamic_pointer_cast<Texture2D>(rit->second.asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load without holding the lock
|
||||
size_t bytes = 0;
|
||||
auto tex = LoadTextureNow_(meta, bytes);
|
||||
if (!tex) return nullptr;
|
||||
|
||||
// Commit to cache and bump progress
|
||||
{
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
auto &rec = s_RecordsById[id];
|
||||
rec.meta = meta;
|
||||
rec.asset = tex;
|
||||
rec.approxBytes = bytes;
|
||||
Touch_(rec);
|
||||
s_LoadedTextures.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
return tex;
|
||||
}
|
||||
|
||||
size_t AssetManager::GetTotalTexturesToLoad()
|
||||
{
|
||||
return s_TotalTexturesDiscovered.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
size_t AssetManager::GetLoadedTexturesCount()
|
||||
{
|
||||
return s_LoadedTextures.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void AssetManager::Tick()
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
RunGC_();
|
||||
}
|
||||
|
||||
void AssetManager::SetGCSeconds(double seconds)
|
||||
{
|
||||
if (seconds < 1.0) seconds = 1.0;
|
||||
s_GCSeconds.store(seconds);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Background scan (indexes assets + builds tree; only counts textures)
|
||||
// ============================================================================
|
||||
void AssetManager::BackgroundScan()
|
||||
{
|
||||
bool loggedErr = false;
|
||||
|
||||
// First pass: count textures so UI progress bar has a target
|
||||
size_t texCount = 0;
|
||||
try {
|
||||
for (auto it = fs::recursive_directory_iterator(s_ProjectRoot,
|
||||
fs::directory_options::skip_permission_denied);
|
||||
it != fs::recursive_directory_iterator(); ++it) {
|
||||
if (!s_Scanning) break;
|
||||
const auto &de = *it;
|
||||
if (!de.is_directory() && DetectAssetKind_(de.path()) == OX_AssetType::Texture)
|
||||
++texCount;
|
||||
}
|
||||
} catch (const fs::filesystem_error &e) {
|
||||
Logger::LogError("AssetManager scan error (count): %s", e.what());
|
||||
}
|
||||
s_TotalTexturesDiscovered.store(texCount, std::memory_order_relaxed);
|
||||
s_LoadedTextures.store(0, std::memory_order_relaxed);
|
||||
|
||||
while (s_Scanning) {
|
||||
std::vector<fs::directory_entry> entries;
|
||||
entries.reserve(2048);
|
||||
|
||||
try {
|
||||
for (auto it = fs::recursive_directory_iterator(s_ProjectRoot,
|
||||
fs::directory_options::skip_permission_denied);
|
||||
it != fs::recursive_directory_iterator(); ++it) {
|
||||
if (!s_Scanning) break;
|
||||
entries.push_back(*it);
|
||||
}
|
||||
} catch (const fs::filesystem_error &e) {
|
||||
if (!loggedErr) {
|
||||
Logger::LogError("AssetManager scan error: %s", e.what());
|
||||
loggedErr = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> foundV;
|
||||
foundV.reserve(entries.size());
|
||||
|
||||
for (auto &de: entries) {
|
||||
if (!s_Scanning) break;
|
||||
const auto &p = de.path();
|
||||
|
||||
try {
|
||||
std::string vpath = MakeVirtualPath(p);
|
||||
foundV.insert(vpath);
|
||||
|
||||
if (de.is_directory()) {
|
||||
AddToTree(vpath, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insert/refresh metadata + id
|
||||
{
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
|
||||
// meta by virtual path (string key used only here)
|
||||
OX_AssetType kind = DetectAssetKind_(p);
|
||||
auto abs = p.string();
|
||||
|
||||
// Build or update virtual->id
|
||||
uint64_t id = 0;
|
||||
if (auto vit = s_VPathToId.find(vpath); vit == s_VPathToId.end()) {
|
||||
id = MakeIdFromVirtualPath_(vpath);
|
||||
s_VPathToId[vpath] = id;
|
||||
} else {
|
||||
id = vit->second;
|
||||
}
|
||||
|
||||
// Update absolute->id and meta maps
|
||||
s_PathToId[abs] = id;
|
||||
|
||||
auto mit = s_MetaById.find(id);
|
||||
if (mit == s_MetaById.end()) {
|
||||
s_MetaById.emplace(id, AssetMetadata{kind, abs});
|
||||
} else {
|
||||
mit->second.kind = kind;
|
||||
mit->second.absolutePath = abs;
|
||||
}
|
||||
}
|
||||
|
||||
AddToTree(vpath, false);
|
||||
} catch (...) {
|
||||
Logger::LogError("AssetManager: error indexing '%s'", p.string().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove vanished assets from maps and cache
|
||||
{
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
|
||||
// Build reverse map vpath->id snapshot to iterate safely
|
||||
std::vector<std::pair<std::string, uint64_t> > vToId(s_VPathToId.begin(), s_VPathToId.end());
|
||||
for (auto &[vpath, id]: vToId) {
|
||||
if (!foundV.contains(vpath)) {
|
||||
// erase meta + records
|
||||
s_MetaById.erase(id);
|
||||
s_RecordsById.erase(id);
|
||||
|
||||
// erase absolute mapping entries pointing to id
|
||||
for (auto pit = s_PathToId.begin(); pit != s_PathToId.end();) {
|
||||
if (pit->second == id) pit = s_PathToId.erase(pit);
|
||||
else ++pit;
|
||||
}
|
||||
|
||||
s_VPathToId.erase(vpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s_Scanning)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tree building
|
||||
// ============================================================================
|
||||
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));
|
||||
if (next != pos) parts.push_back(resPath.substr(pos, next - pos));
|
||||
pos = next + 1;
|
||||
}
|
||||
if (pos < resPath.size())
|
||||
parts.push_back(resPath.substr(pos));
|
||||
if (pos < resPath.size()) parts.push_back(resPath.substr(pos));
|
||||
|
||||
std::lock_guard<std::mutex> lock(s_TreeMutex);
|
||||
std::scoped_lock lk(s_TreeMutex);
|
||||
auto current = s_FileTree;
|
||||
for (size_t i = 1; i < parts.size(); ++i) {
|
||||
auto &name = parts[i];
|
||||
// skip "res:"
|
||||
const auto &name = parts[i];
|
||||
auto it = std::find_if(current->children.begin(), current->children.end(),
|
||||
[&](auto &c) { return c->name == name; });
|
||||
if (it == current->children.end()) {
|
||||
auto node = std::make_shared<ResourceTreeNode>();
|
||||
node->name = name;
|
||||
node->path = current->path + "/" + name;
|
||||
node->path = (current->path == "res://")
|
||||
? std::string("res://") + name
|
||||
: current->path + "/" + name;
|
||||
node->isDirectory = (i + 1 < parts.size()) || isDir;
|
||||
current->children.push_back(node);
|
||||
current = node;
|
||||
@@ -400,11 +529,122 @@ namespace OX
|
||||
}
|
||||
}
|
||||
|
||||
std::string AssetManager::DetectAssetType(const fs::path &path)
|
||||
// ============================================================================
|
||||
// GC
|
||||
// ============================================================================
|
||||
void AssetManager::RunGC_()
|
||||
{
|
||||
auto e = path.extension().string();
|
||||
if (e == ".png" || e == ".jpg" || e == ".jpeg")
|
||||
return "texture2D";
|
||||
return "";
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const double ttlSec = s_GCSeconds.load();
|
||||
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
for (auto &[id, rec]: s_RecordsById) {
|
||||
if (!rec.asset) continue;
|
||||
const auto age = std::chrono::duration<double>(now - rec.lastAccess).count();
|
||||
if (age >= ttlSec) {
|
||||
if (rec.asset.use_count() == 1) {
|
||||
Logger::LogVerbose("GC: unloading id=%llu (idle %.1fs ~ %zu bytes)",
|
||||
(unsigned long long) id, age, rec.approxBytes);
|
||||
rec.asset.reset(); // Texture2D dtor should free GL resource
|
||||
rec.approxBytes = 0;
|
||||
// do NOT decrement s_LoadedTextures; this is "currently loaded", not lifetime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utilities
|
||||
// ============================================================================
|
||||
std::string AssetManager::MakeVirtualPath(const fs::path &full)
|
||||
{
|
||||
fs::path rel = fs::relative(full, s_ProjectRoot);
|
||||
std::string s = rel.generic_string();
|
||||
if (s == "." || s.empty())
|
||||
return "res://";
|
||||
return "res://" + s;
|
||||
}
|
||||
|
||||
const char *AssetManager::AssetTypeToString(OX_AssetType t)
|
||||
{
|
||||
switch (t) {
|
||||
case OX_AssetType::Texture: return "Texture";
|
||||
case OX_AssetType::Model: return "Model";
|
||||
case OX_AssetType::Font: return "Font";
|
||||
case OX_AssetType::Sound: return "Sound";
|
||||
case OX_AssetType::Unknown: return "Unknown";
|
||||
/* if you have it: */
|
||||
case OX_AssetType::Invalid: return "Invalid";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
std::shared_ptr<T> AssetManager::GetAsset(uint64_t id)
|
||||
{
|
||||
static_assert(std::is_base_of_v<Asset, T>, "T must derive from Asset");
|
||||
if (!id) return nullptr;
|
||||
|
||||
// Fast path: cached?
|
||||
{
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
auto it = s_RecordsById.find(id);
|
||||
if (it != s_RecordsById.end() && it->second.asset) {
|
||||
Touch_(it->second);
|
||||
return std::dynamic_pointer_cast<T>(it->second.asset);
|
||||
}
|
||||
}
|
||||
|
||||
const OX_AssetType kind = GetAssetTypeById(id);
|
||||
size_t approxBytes = 0;
|
||||
std::shared_ptr<Asset> loaded;
|
||||
|
||||
if constexpr (std::is_same_v<T, Texture2D>) {
|
||||
if (kind != OX_AssetType::Texture) return nullptr;
|
||||
|
||||
AssetMetadata meta{}; {
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
auto mit = s_MetaById.find(id);
|
||||
if (mit == s_MetaById.end()) return nullptr;
|
||||
meta = mit->second;
|
||||
}
|
||||
|
||||
loaded = LoadTextureNow_(meta, approxBytes);
|
||||
if (!loaded) return nullptr;
|
||||
} else if constexpr (std::is_same_v<T, ModelAsset>) {
|
||||
if (kind != OX_AssetType::Model) return nullptr;
|
||||
|
||||
// Your MeshLoaderOBJ currently has: bool LoadById(uint64_t, std::shared_ptr<ModelAsset>&)
|
||||
std::shared_ptr<ModelAsset> model = MeshLoaderOBJ::LoadById(id);
|
||||
|
||||
approxBytes = model->vertices.size() * sizeof(model->vertices[0])
|
||||
+ model->indices.size() * sizeof(model->indices[0]);
|
||||
loaded = model;
|
||||
} else {
|
||||
// Unknown T requested
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Commit to cache
|
||||
{
|
||||
std::scoped_lock lk(s_AssetMutex);
|
||||
auto &rec = s_RecordsById[id];
|
||||
auto mit = s_MetaById.find(id);
|
||||
rec.meta = (mit != s_MetaById.end()) ? mit->second : AssetMetadata{};
|
||||
rec.asset = loaded;
|
||||
rec.approxBytes = approxBytes;
|
||||
Touch_(rec);
|
||||
|
||||
if constexpr (std::is_same_v<T, Texture2D>) {
|
||||
s_LoadedTextures.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
return std::dynamic_pointer_cast<T>(loaded);
|
||||
}
|
||||
|
||||
template std::shared_ptr<Texture2D> AssetManager::GetAsset<Texture2D>(uint64_t);
|
||||
|
||||
template std::shared_ptr<ModelAsset> AssetManager::GetAsset<ModelAsset>(uint64_t);
|
||||
} // namespace OX
|
||||
|
||||
@@ -1,96 +1,180 @@
|
||||
/// AssetManager.h
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "assets/Asset.h"
|
||||
#include "assets/MeshLoaderObj.h"
|
||||
#include "assets/Model.h"
|
||||
#include "assets/Texture2D.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
class Asset;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ============================================================================
|
||||
// Asset Types
|
||||
// ============================================================================
|
||||
enum class OX_AssetType : uint8_t
|
||||
{
|
||||
Unknown = 0,
|
||||
Invalid,
|
||||
Texture,
|
||||
Model,
|
||||
Font,
|
||||
Sound,
|
||||
_MAX,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Drag-and-drop payload (for ImGui or other UI)
|
||||
// ============================================================================
|
||||
inline constexpr const char *OX_ASSET_DRAG_TYPE = "OX_AssetDrag";
|
||||
|
||||
struct OX_AssetDrag
|
||||
{
|
||||
uint64_t id = 0; // 64-bit asset id
|
||||
OX_AssetType type = OX_AssetType::Unknown;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Resource tree node (used by FileBrowser/UI)
|
||||
// ============================================================================
|
||||
struct ResourceTreeNode
|
||||
{
|
||||
std::string name;
|
||||
std::string path;
|
||||
std::string name; // leaf name
|
||||
std::string path; // virtual path "res://..."
|
||||
bool isDirectory = false;
|
||||
std::vector<std::shared_ptr<ResourceTreeNode> > children;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Asset metadata
|
||||
// ============================================================================
|
||||
struct AssetMetadata
|
||||
{
|
||||
OX_AssetType kind = OX_AssetType::Unknown;
|
||||
std::string absolutePath; // native absolute path
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// AssetManager
|
||||
// ============================================================================
|
||||
class AssetManager
|
||||
{
|
||||
public:
|
||||
// -- Lifecycle --
|
||||
// Startup / shutdown
|
||||
static void Init(const std::string &projectRoot);
|
||||
|
||||
static void Shutdown();
|
||||
|
||||
static void Tick();
|
||||
|
||||
// Trigger a new scan of project root
|
||||
static void Rescan();
|
||||
|
||||
|
||||
static size_t GetTotalTexturesToLoad() { return s_TotalTexturesToLoad.load(); }
|
||||
static size_t GetLoadedTexturesCount() { return s_LoadedTexturesCount.load(); }
|
||||
|
||||
// -- Queries --
|
||||
// Lookup by virtual path ("res://…") or real FS path.
|
||||
static std::shared_ptr<Asset> Get(const std::string &path);
|
||||
|
||||
// Virtual resource tree for UI
|
||||
static std::shared_ptr<ResourceTreeNode> GetFileTree();
|
||||
|
||||
// Save a tiny index of assets for packaging
|
||||
static void SaveAssetPack(const std::string &outputPath);
|
||||
|
||||
static std::mutex s_TreeMutex;
|
||||
// ---- ID & info lookups ----
|
||||
static uint64_t GetIdForPath(const std::string &keyOrPath); // accepts res:// or native path
|
||||
static OX_AssetType GetAssetTypeById(uint64_t id);
|
||||
|
||||
|
||||
static std::string GetAbsolutePathById(uint64_t id); // for external loaders (OBJ, etc.)
|
||||
static std::string GetVirtualPathById(uint64_t id);
|
||||
|
||||
// ---- Lazy getters (only textures are loaded here) ----
|
||||
static std::shared_ptr<Asset> GetAssetById(uint64_t id); // generic (currently only textures)
|
||||
static std::shared_ptr<Texture2D> GetTexture(uint64_t id);
|
||||
|
||||
// ---- Compatibility helpers (so your current FileBrowser keeps building) ----
|
||||
static std::shared_ptr<Asset> Get(const std::string &keyOrPath)
|
||||
{
|
||||
const uint64_t id = GetIdForPath(keyOrPath);
|
||||
return id ? GetAssetById(id) : nullptr;
|
||||
}
|
||||
|
||||
// Progress bar compatibility (textures only)
|
||||
static size_t GetTotalTexturesToLoad(); // discovered textures
|
||||
static size_t GetLoadedTexturesCount(); // currently loaded textures
|
||||
|
||||
// Utilities
|
||||
static std::string MakeVirtualPath(const fs::path &full);
|
||||
|
||||
// Call each frame; runs GC, etc.
|
||||
static void Tick();
|
||||
|
||||
static void SetGCSeconds(double seconds);
|
||||
|
||||
static const char *AssetTypeToString(OX_AssetType t);
|
||||
|
||||
|
||||
// Exposed for FileBrowser code that locks it directly
|
||||
static std::mutex s_TreeMutex; // NOTE: public for legacy UI usage
|
||||
|
||||
private:
|
||||
struct AssetMetadata
|
||||
struct AssetRecord
|
||||
{
|
||||
std::string type;
|
||||
std::string absolutePath;
|
||||
};
|
||||
|
||||
struct PendingTexture
|
||||
{
|
||||
std::string id;
|
||||
int width, height, channels;
|
||||
unsigned char *data;
|
||||
AssetMetadata meta;
|
||||
std::shared_ptr<Asset> asset; // null when unloaded
|
||||
std::chrono::steady_clock::time_point lastAccess{};
|
||||
size_t approxBytes = 0;
|
||||
};
|
||||
|
||||
// Internal
|
||||
static void BackgroundScan();
|
||||
|
||||
static void AddToTree(const std::string &virtualPath, bool isDir);
|
||||
static void AddToTree(const std::string &resPath, bool isDir);
|
||||
|
||||
static std::string DetectAssetType(const std::filesystem::path &path);
|
||||
static void RunGC_();
|
||||
|
||||
static std::string MakeVirtualPath(const std::filesystem::path &full);
|
||||
static void Touch_(AssetRecord &rec);
|
||||
|
||||
// loaded assets & metadata
|
||||
static std::unordered_map<std::string, std::string> s_PathToID;
|
||||
static std::unordered_map<std::string, std::shared_ptr<Asset> > s_LoadedAssets;
|
||||
static std::unordered_map<std::string, AssetMetadata> s_MetadataMap;
|
||||
static std::mutex s_AssetMutex;
|
||||
// Loading
|
||||
static std::shared_ptr<Texture2D> LoadTextureNow_(const AssetMetadata &meta, size_t &outBytes);
|
||||
|
||||
// directory tree
|
||||
// Helpers
|
||||
static uint64_t MakeIdFromVirtualPath_(const std::string &vpath);
|
||||
|
||||
static OX_AssetType DetectAssetKind_(const fs::path &path);
|
||||
|
||||
private:
|
||||
// Project root
|
||||
static std::filesystem::path s_ProjectRoot;
|
||||
|
||||
static std::unordered_map<uint64_t, AssetRecord> s_RecordsById; // id -> record (payload if loaded)
|
||||
static std::unordered_map<uint64_t, AssetMetadata> s_MetaById; // id -> metadata
|
||||
static std::unordered_map<std::string, uint64_t> s_PathToId; // absolute -> id
|
||||
static std::unordered_map<std::string, uint64_t> s_VPathToId; // "res://..." -> id
|
||||
|
||||
// Tree
|
||||
static std::shared_ptr<ResourceTreeNode> s_FileTree;
|
||||
|
||||
// background scan
|
||||
static std::filesystem::path s_ProjectRoot;
|
||||
// Threading
|
||||
static std::atomic<bool> s_Scanning;
|
||||
static std::thread s_ScanThread;
|
||||
|
||||
// texture upload queue
|
||||
static std::queue<PendingTexture> s_TextureQueue;
|
||||
static std::mutex s_QueueMutex;
|
||||
static std::mutex s_AssetMutex;
|
||||
|
||||
static std::atomic<size_t> s_TotalTexturesToLoad;
|
||||
static std::atomic<size_t> s_LoadedTexturesCount;
|
||||
// GC control
|
||||
static std::atomic<double> s_GCSeconds;
|
||||
|
||||
// Texture progress counters (for UI bar)
|
||||
static std::atomic<size_t> s_TotalTexturesDiscovered;
|
||||
static std::atomic<size_t> s_LoadedTextures;
|
||||
|
||||
public:
|
||||
template<typename T>
|
||||
static std::shared_ptr<T> GetAsset(uint64_t id);
|
||||
};
|
||||
}
|
||||
} // namespace OX
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
#include "systems/Logger.h"
|
||||
#include "systems/Scene/GameObject.h"
|
||||
#include "systems/assets/MeshLoaderOBJ.h"
|
||||
#include "systems/AssetManager.h"
|
||||
|
||||
#include <stb/stb_image.h>
|
||||
#include <unordered_map>
|
||||
#include <stb/stb_image.h>
|
||||
#include <GL/glew.h>
|
||||
|
||||
|
||||
namespace OX
|
||||
{
|
||||
// ---------- texture helper ----------
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// GLTexture helpers
|
||||
// ---------------------------------------------------------------------
|
||||
void GLTexture::Destroy()
|
||||
{
|
||||
if (id) {
|
||||
@@ -22,92 +23,12 @@ namespace OX
|
||||
path.clear();
|
||||
}
|
||||
|
||||
static GLuint CreateTextureFromFile(const std::string &path,
|
||||
int *outW = nullptr, int *outH = nullptr, int *outC = nullptr)
|
||||
{
|
||||
stbi_set_flip_vertically_on_load(true);
|
||||
int w = 0, h = 0, c = 0;
|
||||
unsigned char *data = stbi_load(path.c_str(), &w, &h, &c, 0);
|
||||
if (!data) {
|
||||
Logger::LogWarning("stb_image failed to load: %s", path.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLenum fmt = (c == 4 ? GL_RGBA : GL_RGB);
|
||||
|
||||
GLuint tex = 0;
|
||||
glGenTextures(1, &tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, fmt, w, h, 0, fmt, GL_UNSIGNED_BYTE, data);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
stbi_image_free(data);
|
||||
|
||||
if (outW) *outW = w;
|
||||
if (outH) *outH = h;
|
||||
if (outC) *outC = c;
|
||||
return tex;
|
||||
}
|
||||
|
||||
static void LoadTexturesFor(Material &m)
|
||||
{
|
||||
if (!m.mapKd.empty()) {
|
||||
m.texKd.id = CreateTextureFromFile(m.mapKd, &m.texKd.width, &m.texKd.height, &m.texKd.channels);
|
||||
m.texKd.path = m.mapKd;
|
||||
}
|
||||
if (!m.mapKs.empty()) {
|
||||
m.texKs.id = CreateTextureFromFile(m.mapKs, &m.texKs.width, &m.texKs.height, &m.texKs.channels);
|
||||
m.texKs.path = m.mapKs;
|
||||
}
|
||||
if (!m.mapNormal.empty()) {
|
||||
m.texNormal.id = CreateTextureFromFile(m.mapNormal, &m.texNormal.width, &m.texNormal.height,
|
||||
&m.texNormal.channels);
|
||||
m.texNormal.path = m.mapNormal;
|
||||
}
|
||||
}
|
||||
|
||||
// Compact a single submesh into local arrays
|
||||
static void BuildCompactedSubmesh(
|
||||
const std::vector<Vertex> &globalVerts,
|
||||
const std::vector<uint32_t> &globalIdx,
|
||||
uint32_t indexOffset,
|
||||
uint32_t indexCount,
|
||||
std::vector<Vertex> &outVerts,
|
||||
std::vector<uint32_t> &outIdx)
|
||||
{
|
||||
outVerts.clear();
|
||||
outIdx.clear();
|
||||
outVerts.reserve(indexCount);
|
||||
outIdx.reserve(indexCount);
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> remap;
|
||||
remap.reserve(indexCount);
|
||||
|
||||
for (uint32_t i = 0; i < indexCount; ++i) {
|
||||
uint32_t oldIdx = globalIdx[indexOffset + i];
|
||||
auto it = remap.find(oldIdx);
|
||||
if (it == remap.end()) {
|
||||
uint32_t newIdx = static_cast<uint32_t>(outVerts.size());
|
||||
remap.emplace(oldIdx, newIdx);
|
||||
outVerts.push_back(globalVerts[oldIdx]);
|
||||
outIdx.push_back(newIdx);
|
||||
} else {
|
||||
outIdx.push_back(it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- MeshComponent ----------
|
||||
|
||||
MeshComponent::MeshComponent(const std::string &objPath)
|
||||
: m_sourcePath(objPath)
|
||||
void GLTexture::SetFromGL(unsigned int id, int width, int height)
|
||||
{
|
||||
Destroy();
|
||||
this->id = id;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
}
|
||||
|
||||
MeshComponent::~MeshComponent()
|
||||
@@ -115,67 +36,81 @@ namespace OX
|
||||
Clear();
|
||||
}
|
||||
|
||||
bool MeshComponent::LoadOBJ(const std::string &path)
|
||||
// ---------------------------------------------------------------------
|
||||
// New ID-based API
|
||||
// ---------------------------------------------------------------------
|
||||
bool MeshComponent::LoadModel(uint64_t assetId)
|
||||
{
|
||||
Clear();
|
||||
|
||||
MeshLoadResult result;
|
||||
if (!MeshLoaderOBJ::Load(path, result)) {
|
||||
Logger::LogError("OBJ load failed: %s", path.c_str());
|
||||
auto model = AssetManager::GetAsset<ModelAsset>(assetId);
|
||||
if (!model) {
|
||||
Logger::LogError("MeshComponent: invalid asset ID %" PRIu64, assetId);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_sourcePath = path;
|
||||
m_vertices = std::move(result.vertices);
|
||||
m_indices = std::move(result.indices);
|
||||
m_submeshes = std::move(result.submeshes);
|
||||
m_materials = std::move(result.materials);
|
||||
m_loadedSubmeshIndex = -1; // full mesh
|
||||
m_sourceAssetId = assetId;
|
||||
|
||||
// Upload textures for materials
|
||||
for (auto &m: m_materials) {
|
||||
LoadTexturesFor(m);
|
||||
}
|
||||
m_vertices = model->vertices;
|
||||
m_indices = model->indices;
|
||||
m_submeshes = model->submeshes;
|
||||
m_materials = model->materials;
|
||||
m_loadedSubmeshIndex = -1;
|
||||
|
||||
CreateGL();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MeshComponent::LoadOBJSubmesh(const std::string &path, int submeshIndex)
|
||||
bool MeshComponent::LoadModelSubmesh(uint64_t assetId, int submeshIndex)
|
||||
{
|
||||
Clear();
|
||||
|
||||
MeshLoadResult result;
|
||||
if (!MeshLoaderOBJ::Load(path, result)) {
|
||||
Logger::LogError("OBJ load failed: %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
if (submeshIndex < 0 || submeshIndex >= (int) result.submeshes.size()) {
|
||||
Logger::LogError("LoadOBJSubmesh: index %d out of range [0,%zu) for %s",
|
||||
submeshIndex, result.submeshes.size(), path.c_str());
|
||||
auto model = AssetManager::GetAsset<ModelAsset>(assetId);
|
||||
if (!model) {
|
||||
Logger::LogError("MeshComponent: invalid asset ID %" PRIu64, assetId);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &sm = result.submeshes[submeshIndex];
|
||||
if (submeshIndex < 0 || submeshIndex >= (int) model->submeshes.size()) {
|
||||
Logger::LogError("MeshComponent: submesh index %d out of range for asset %" PRIu64,
|
||||
submeshIndex, assetId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// compact
|
||||
const auto &sm = model->submeshes[submeshIndex];
|
||||
|
||||
// Compact into local buffers
|
||||
std::vector<Vertex> v;
|
||||
std::vector<uint32_t> i;
|
||||
BuildCompactedSubmesh(result.vertices, result.indices, sm.indexOffset, sm.indexCount, v, i);
|
||||
v.reserve(sm.indexCount);
|
||||
i.reserve(sm.indexCount);
|
||||
|
||||
// set component data to "single submesh" layout
|
||||
m_sourcePath = path;
|
||||
std::unordered_map<uint32_t, uint32_t> remap;
|
||||
remap.reserve(sm.indexCount);
|
||||
|
||||
for (uint32_t j = 0; j < sm.indexCount; ++j) {
|
||||
uint32_t oldIdx = model->indices[sm.indexOffset + j];
|
||||
auto it = remap.find(oldIdx);
|
||||
if (it == remap.end()) {
|
||||
uint32_t newIdx = static_cast<uint32_t>(v.size());
|
||||
remap.emplace(oldIdx, newIdx);
|
||||
v.push_back(model->vertices[oldIdx]);
|
||||
i.push_back(newIdx);
|
||||
} else {
|
||||
i.push_back(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign to component
|
||||
m_sourceAssetId = assetId;
|
||||
m_vertices = std::move(v);
|
||||
m_indices = std::move(i);
|
||||
m_materials.clear();
|
||||
m_materials.reserve(1);
|
||||
|
||||
Material mat{};
|
||||
if (sm.materialIndex >= 0 && sm.materialIndex < (int) result.materials.size()) {
|
||||
mat = result.materials[sm.materialIndex];
|
||||
}
|
||||
LoadTexturesFor(mat);
|
||||
m_materials.push_back(std::move(mat));
|
||||
m_materials.clear();
|
||||
if (sm.materialIndex >= 0 && sm.materialIndex < (int) model->materials.size())
|
||||
m_materials.push_back(model->materials[sm.materialIndex]);
|
||||
else
|
||||
m_materials.emplace_back(Material{});
|
||||
|
||||
m_submeshes.clear();
|
||||
SubMesh one{};
|
||||
@@ -190,10 +125,124 @@ namespace OX
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MeshComponent::ImportModel(GameObject *parent, uint64_t assetId, MeshImportMode mode)
|
||||
{
|
||||
if (!parent) return false;
|
||||
|
||||
auto model = AssetManager::GetAsset<ModelAsset>(assetId);
|
||||
if (!model) {
|
||||
Logger::LogError("MeshComponent: invalid asset ID %" PRIu64, assetId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == MeshImportMode::SingleComponent) {
|
||||
auto *mc = parent->AddComponent<MeshComponent>();
|
||||
if (!mc) return false;
|
||||
return mc->LoadModel(assetId);
|
||||
}
|
||||
|
||||
if (model->submeshes.empty()) {
|
||||
Logger::LogError("MeshComponent: no submeshes in asset %" PRIu64, assetId);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < model->submeshes.size(); ++i) {
|
||||
std::string name = "Submesh_" + std::to_string(i);
|
||||
const auto &sm = model->submeshes[i];
|
||||
if (sm.materialIndex >= 0 && sm.materialIndex < (int) model->materials.size()) {
|
||||
const auto &mat = model->materials[sm.materialIndex];
|
||||
if (!mat.name.empty())
|
||||
name += " [" + mat.name + "]";
|
||||
}
|
||||
|
||||
GameObject *child = parent->createChild(name);
|
||||
if (!child) {
|
||||
Logger::LogError("MeshComponent: failed to create child for submesh %zu", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *mc = child->AddComponent<MeshComponent>();
|
||||
if (!mc) {
|
||||
Logger::LogError("MeshComponent: failed to add MeshComponent to child %s", name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mc->LoadModelSubmesh(assetId, static_cast<int>(i))) {
|
||||
Logger::LogError("MeshComponent: failed to load submesh %zu from asset %" PRIu64, i, assetId);
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Path wrappers (legacy)
|
||||
// ---------------------------------------------------------------------
|
||||
bool MeshComponent::LoadOBJ(const std::string &path)
|
||||
{
|
||||
uint64_t id = AssetManager::GetIdForPath(path);
|
||||
return LoadModel(id);
|
||||
}
|
||||
|
||||
bool MeshComponent::LoadOBJSubmesh(const std::string &path, int submeshIndex)
|
||||
{
|
||||
uint64_t id = AssetManager::GetIdForPath(path);
|
||||
return LoadModelSubmesh(id, submeshIndex);
|
||||
}
|
||||
|
||||
bool MeshComponent::ImportOBJ(GameObject *parent, const std::string &path, MeshImportMode mode)
|
||||
{
|
||||
uint64_t id = AssetManager::GetIdForPath(path);
|
||||
return ImportModel(parent, id, mode);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Component interface
|
||||
// ---------------------------------------------------------------------
|
||||
void MeshComponent::OnAttach(GameObject * /*owner*/)
|
||||
{
|
||||
}
|
||||
|
||||
void MeshComponent::OnDetach() { Clear(); }
|
||||
|
||||
YAML::Node MeshComponent::Serialize() const
|
||||
{
|
||||
YAML::Node n;
|
||||
n["type"] = "MeshComponent";
|
||||
n["asset_id"] = m_sourceAssetId;
|
||||
if (m_loadedSubmeshIndex >= 0)
|
||||
n["submesh"] = m_loadedSubmeshIndex;
|
||||
return n;
|
||||
}
|
||||
|
||||
void MeshComponent::Deserialize(const YAML::Node &node)
|
||||
{
|
||||
Clear();
|
||||
|
||||
if (node["asset_id"]) {
|
||||
const uint64_t id = node["asset_id"].as<uint64_t>();
|
||||
if (node["submesh"])
|
||||
(void) LoadModelSubmesh(id, node["submesh"].as<int>());
|
||||
else
|
||||
(void) LoadModel(id);
|
||||
} else if (node["path"]) {
|
||||
const std::string path = node["path"].as<std::string>();
|
||||
if (node["submesh"])
|
||||
(void) LoadOBJSubmesh(path, node["submesh"].as<int>());
|
||||
else
|
||||
(void) LoadOBJ(path);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------
|
||||
void MeshComponent::Clear()
|
||||
{
|
||||
DestroyGL();
|
||||
|
||||
for (auto &m: m_materials) {
|
||||
m.texKd.Destroy();
|
||||
m.texKs.Destroy();
|
||||
@@ -203,7 +252,7 @@ namespace OX
|
||||
m_indices.clear();
|
||||
m_submeshes.clear();
|
||||
m_materials.clear();
|
||||
m_sourcePath.clear();
|
||||
m_sourceAssetId = 0;
|
||||
m_loadedSubmeshIndex = -1;
|
||||
}
|
||||
|
||||
@@ -223,7 +272,6 @@ namespace OX
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indices.size() * sizeof(uint32_t), m_indices.data(), GL_STATIC_DRAW);
|
||||
|
||||
// layout: pos(0), norm(1), uv(2), tan(3), bitan(4)
|
||||
GLsizei stride = sizeof(Vertex);
|
||||
std::size_t offs = 0;
|
||||
|
||||
@@ -264,93 +312,4 @@ namespace OX
|
||||
m_vao = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void MeshComponent::OnAttach(GameObject * /*owner*/)
|
||||
{
|
||||
}
|
||||
|
||||
void MeshComponent::OnDetach() { Clear(); }
|
||||
|
||||
YAML::Node MeshComponent::Serialize() const
|
||||
{
|
||||
YAML::Node n;
|
||||
n["type"] = "MeshComponent";
|
||||
n["path"] = m_sourcePath;
|
||||
if (m_loadedSubmeshIndex >= 0)
|
||||
n["submesh"] = m_loadedSubmeshIndex; // indicates this component is single-submesh
|
||||
return n;
|
||||
}
|
||||
|
||||
void MeshComponent::Deserialize(const YAML::Node &node)
|
||||
{
|
||||
Clear();
|
||||
if (!node["path"]) return;
|
||||
|
||||
const std::string p = node["path"].as<std::string>();
|
||||
if (node["submesh"]) {
|
||||
const int sm = node["submesh"].as<int>();
|
||||
(void) LoadOBJSubmesh(p, sm);
|
||||
} else {
|
||||
(void) LoadOBJ(p);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Static importer ----------
|
||||
|
||||
bool MeshComponent::ImportOBJ(GameObject *parent,
|
||||
const std::string &path,
|
||||
MeshImportMode mode)
|
||||
{
|
||||
if (!parent) return false;
|
||||
|
||||
if (mode == MeshImportMode::SingleComponent) {
|
||||
auto *mc = parent->AddComponent<MeshComponent>();
|
||||
if (!mc) return false;
|
||||
return mc->LoadOBJ(path);
|
||||
}
|
||||
|
||||
// ChildrenPerSubmesh
|
||||
MeshLoadResult result;
|
||||
if (!MeshLoaderOBJ::Load(path, result)) {
|
||||
Logger::LogError("ImportOBJ: OBJ load failed: %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
if (result.submeshes.empty()) {
|
||||
Logger::LogError("ImportOBJ: no submeshes in %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < result.submeshes.size(); ++i) {
|
||||
// Create a child GO named "Submesh_i [MatName]"
|
||||
std::string name = "Submesh_" + std::to_string(i);
|
||||
const auto &sm = result.submeshes[i];
|
||||
if (sm.materialIndex >= 0 && sm.materialIndex < (int) result.materials.size()) {
|
||||
const auto &mat = result.materials[sm.materialIndex];
|
||||
if (!mat.name.empty()) name += " [" + mat.name + "]";
|
||||
}
|
||||
|
||||
// If your API differs, change this:
|
||||
GameObject *child = parent->createChild(name);
|
||||
if (!child) {
|
||||
Logger::LogError("ImportOBJ: failed to create child for submesh %zu", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *mc = child->AddComponent<MeshComponent>();
|
||||
if (!mc) {
|
||||
Logger::LogError("ImportOBJ: failed to add MeshComponent to child %s", name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mc->LoadOBJSubmesh(path, static_cast<int>(i))) {
|
||||
Logger::LogError("ImportOBJ: LoadOBJSubmesh failed for %s submesh %zu", path.c_str(), i);
|
||||
// Optionally remove child if failed
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
} // namespace OX
|
||||
|
||||
@@ -1,84 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "Component.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include "systems/Scene/Components/Component.h"
|
||||
#include "systems/assets/Asset.h"
|
||||
#include "systems/assets/Model.h"
|
||||
#include "systems/assets/MeshLoaderOBJ.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
struct GLTexture
|
||||
{
|
||||
unsigned int id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int channels = 0;
|
||||
std::string path;
|
||||
|
||||
void Destroy();
|
||||
};
|
||||
|
||||
struct Material
|
||||
{
|
||||
std::string name;
|
||||
std::array<float, 3> kd{1, 1, 1}; // diffuse
|
||||
std::array<float, 3> ks{0, 0, 0}; // specular
|
||||
float ns = 16.0f; // shininess
|
||||
|
||||
std::string mapKd; // diffuse map path
|
||||
std::string mapKs; // specular map path
|
||||
std::string mapNormal; // normal map path (bump/normal)
|
||||
|
||||
GLTexture texKd;
|
||||
GLTexture texKs;
|
||||
GLTexture texNormal;
|
||||
};
|
||||
|
||||
struct Vertex
|
||||
{
|
||||
float px, py, pz; // position
|
||||
float nx, ny, nz; // normal
|
||||
float u, v; // uv
|
||||
float tx, ty, tz; // tangent
|
||||
float bx, by, bz; // bitangent
|
||||
};
|
||||
|
||||
struct SubMesh
|
||||
{
|
||||
uint32_t indexOffset = 0;
|
||||
uint32_t indexCount = 0;
|
||||
int materialIndex = -1;
|
||||
};
|
||||
|
||||
enum class MeshImportMode
|
||||
{
|
||||
SingleComponent, // one MeshComponent with submeshes (your current default)
|
||||
ChildrenPerSubmesh // create child GOs, each with a single-submesh MeshComponent
|
||||
SingleComponent,
|
||||
ChildrenPerSubmesh
|
||||
};
|
||||
|
||||
|
||||
class MeshComponent : public Component
|
||||
{
|
||||
public:
|
||||
MeshComponent() = default;
|
||||
|
||||
explicit MeshComponent(const std::string &objPath);
|
||||
|
||||
~MeshComponent() override;
|
||||
~MeshComponent();
|
||||
|
||||
// Normal: load full mesh with submeshes/materials
|
||||
// === New Asset-ID based API ===
|
||||
bool LoadModel(uint64_t assetId);
|
||||
|
||||
bool LoadModelSubmesh(uint64_t assetId, int submeshIndex);
|
||||
|
||||
static bool ImportModel(GameObject *parent, uint64_t assetId, MeshImportMode mode);
|
||||
|
||||
// === Path-based API (legacy wrappers) ===
|
||||
bool LoadOBJ(const std::string &path);
|
||||
|
||||
// Load ONLY the specified submesh into this component (compacted, single material)
|
||||
bool LoadOBJSubmesh(const std::string &path, int submeshIndex);
|
||||
|
||||
void Clear(); // releases GPU and CPU
|
||||
static bool ImportOBJ(GameObject *parent, const std::string &path, MeshImportMode mode);
|
||||
|
||||
// Static importer helpers
|
||||
static bool ImportOBJ(class GameObject *parent,
|
||||
const std::string &path,
|
||||
MeshImportMode mode);
|
||||
|
||||
// GPU info
|
||||
unsigned int vao() const { return m_vao; }
|
||||
@@ -90,10 +53,11 @@ namespace OX
|
||||
const std::vector<Material> &materials() const { return m_materials; }
|
||||
const std::vector<uint32_t> &indices() const { return m_indices; }
|
||||
const std::vector<Vertex> &vertices() const { return m_vertices; }
|
||||
const std::string &sourcePath() const { return m_sourcePath; }
|
||||
const uint64_t &sourceAssetId() const { return m_sourceAssetId; }
|
||||
|
||||
// Component API
|
||||
void OnAttach(class GameObject *owner) override;
|
||||
|
||||
// === Component overrides ===
|
||||
void OnAttach(GameObject *owner) override;
|
||||
|
||||
void OnDetach() override;
|
||||
|
||||
@@ -102,25 +66,21 @@ namespace OX
|
||||
void Deserialize(const YAML::Node &node) override;
|
||||
|
||||
private:
|
||||
// === Core helpers ===
|
||||
void Clear();
|
||||
|
||||
void CreateGL();
|
||||
|
||||
void DestroyGL();
|
||||
|
||||
private:
|
||||
std::string m_sourcePath;
|
||||
|
||||
// CPU
|
||||
// === Fields ===
|
||||
uint64_t m_sourceAssetId = 0; // ID of the model asset used
|
||||
std::vector<Vertex> m_vertices;
|
||||
std::vector<uint32_t> m_indices;
|
||||
std::vector<SubMesh> m_submeshes;
|
||||
std::vector<Material> m_materials;
|
||||
int m_loadedSubmeshIndex = -1;
|
||||
|
||||
// GPU
|
||||
unsigned int m_vao = 0;
|
||||
unsigned int m_vbo = 0;
|
||||
unsigned int m_ebo = 0;
|
||||
|
||||
// For importer mode and serialization when this represents a single submesh
|
||||
int m_loadedSubmeshIndex = -1; // -1 = full mesh; >=0 = single-submesh mode
|
||||
unsigned int m_vao = 0, m_vbo = 0, m_ebo = 0;
|
||||
};
|
||||
} // namespace OX
|
||||
|
||||
8
src/core/systems/assets/Asset.cpp
Normal file
8
src/core/systems/assets/Asset.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// Created by spenc on 5/21/2025.
|
||||
//
|
||||
|
||||
#include "Asset.h"
|
||||
|
||||
namespace OX {
|
||||
} // OX
|
||||
20
src/core/systems/assets/Asset.h
Normal file
20
src/core/systems/assets/Asset.h
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Created by spenc on 5/21/2025.
|
||||
//
|
||||
|
||||
#ifndef ASSET_H
|
||||
#define ASSET_H
|
||||
#include "systems/Logger.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
class Asset
|
||||
{
|
||||
public:
|
||||
virtual ~Asset() = default;
|
||||
|
||||
virtual std::string GetTypeName() const = 0;
|
||||
};
|
||||
} // OX
|
||||
|
||||
#endif //ASSET_H
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "MeshLoaderOBJ.h"
|
||||
#include "systems/Logger.h"
|
||||
#include "systems/AssetManager.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -45,14 +46,12 @@ namespace OX
|
||||
return out;
|
||||
}
|
||||
|
||||
// Like split_ws, but preserves \"quoted strings\" as one token and allows backslash-escaped spaces
|
||||
// Like split_ws, but preserves "quoted strings" and backslash-escaped spaces
|
||||
static std::vector<std::string> split_ws_escaped(const std::string &s)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
bool in_quotes = false;
|
||||
bool esc = false;
|
||||
|
||||
bool in_quotes = false, esc = false;
|
||||
auto flush = [&]()
|
||||
{
|
||||
if (!cur.empty()) {
|
||||
@@ -60,21 +59,12 @@ namespace OX
|
||||
cur.clear();
|
||||
}
|
||||
};
|
||||
|
||||
for (char c: s) {
|
||||
if (esc) {
|
||||
// keep char literally
|
||||
cur.push_back(c);
|
||||
esc = false;
|
||||
} else if (c == '\\') {
|
||||
esc = true;
|
||||
} else if (c == '"') {
|
||||
in_quotes = !in_quotes;
|
||||
} else if (std::isspace((unsigned char) c) && !in_quotes) {
|
||||
flush();
|
||||
} else {
|
||||
cur.push_back(c);
|
||||
}
|
||||
} else if (c == '\\') { esc = true; } else if (c == '"') { in_quotes = !in_quotes; } else if (
|
||||
std::isspace((unsigned char) c) && !in_quotes) { flush(); } else cur.push_back(c);
|
||||
}
|
||||
flush();
|
||||
return out;
|
||||
@@ -82,15 +72,14 @@ namespace OX
|
||||
|
||||
static std::string unescape_backslashes(std::string s)
|
||||
{
|
||||
// turn "\ " -> " ", "\\\"" -> "\"", "\\\\" -> "\\"
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
bool esc = false;
|
||||
for (char c: s) {
|
||||
if (!esc) {
|
||||
if (c == '\\') { esc = true; } else out.push_back(c);
|
||||
if (c == '\\') esc = true;
|
||||
else out.push_back(c);
|
||||
} else {
|
||||
// consume the escape; keep the char as-is
|
||||
out.push_back(c);
|
||||
esc = false;
|
||||
}
|
||||
@@ -114,13 +103,6 @@ namespace OX
|
||||
}
|
||||
|
||||
// ---------- math helpers ----------
|
||||
static inline void v_add(float a[3], const float b[3])
|
||||
{
|
||||
a[0] += b[0];
|
||||
a[1] += b[1];
|
||||
a[2] += b[2];
|
||||
}
|
||||
|
||||
static inline void v_sub(const float a[3], const float b[3], float r[3])
|
||||
{
|
||||
r[0] = a[0] - b[0];
|
||||
@@ -150,34 +132,21 @@ namespace OX
|
||||
// ---------- index fixing (OBJ supports negative indexes) ----------
|
||||
static int fix_index(int idx, int count)
|
||||
{
|
||||
// input: 1-based or negative; output: 0-based
|
||||
if (idx > 0) return idx - 1;
|
||||
if (idx < 0) return count + idx;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------- Wavefront map_* tail parser ----------
|
||||
// Extracts the filename from a "map_* ..." line, supporting:
|
||||
// - Quoted filenames: map_Kd "C:\path with spaces\albedo.png"
|
||||
// - Backslash-escaped spaces: map_Kd C:\path\with\spaces\albedo\ file.png (as "file.png" with escaped space)
|
||||
// - Options like: -bm, -o/-s/-t (3 args), -mm (2), -clamp, -imfchan, -type, etc.
|
||||
static std::string extract_map_path_from_line(const std::string &line, const char *key)
|
||||
{
|
||||
// Find the key at the start (after trimming leading spaces)
|
||||
size_t start = 0;
|
||||
while (start < line.size() && std::isspace((unsigned char) line[start])) ++start;
|
||||
|
||||
// Must begin with key
|
||||
const size_t key_len = std::strlen(key);
|
||||
if (line.compare(start, key_len, key) != 0) return {};
|
||||
|
||||
size_t pos = start + key_len;
|
||||
// Skip spaces after key
|
||||
while (pos < line.size() && std::isspace((unsigned char) line[pos])) ++pos;
|
||||
|
||||
if (pos >= line.size()) return {};
|
||||
|
||||
// If quoted, take quoted content
|
||||
if (line[pos] == '"') {
|
||||
++pos;
|
||||
std::string out;
|
||||
@@ -187,127 +156,38 @@ namespace OX
|
||||
if (esc) {
|
||||
out.push_back(c);
|
||||
esc = false;
|
||||
} else if (c == '\\') {
|
||||
esc = true;
|
||||
} else if (c == '"') {
|
||||
} else if (c == '\\') { esc = true; } else if (c == '"') {
|
||||
++pos;
|
||||
break;
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
} else out.push_back(c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Otherwise, split with quotes/escapes respected
|
||||
std::string tail = line.substr(pos);
|
||||
auto toks = split_ws_escaped(tail);
|
||||
if (toks.empty()) return {};
|
||||
|
||||
// Consume known options and their parameters, per MTL spec
|
||||
// Each "consume(N)" eats N following tokens (if present)
|
||||
auto consume = [&](size_t &i, int n)
|
||||
{
|
||||
while (n-- > 0 && i < toks.size()) ++i;
|
||||
};
|
||||
|
||||
auto consume = [&](size_t &i, int n) { while (n-- > 0 && i < toks.size()) ++i; };
|
||||
size_t i = 0;
|
||||
while (i < toks.size()) {
|
||||
const std::string &t = toks[i];
|
||||
if (t == "-blendu" || t == "-blendv" || t == "-cc" || t == "-clamp" || t == "-texopt") {
|
||||
// on/off or flags
|
||||
consume(i, 1); // value
|
||||
} else if (t == "-bm" || t == "-boost" || t == "-texres" || t == "-type" || t == "-imfchan") {
|
||||
consume(i, 1);
|
||||
} else if (t == "-mm") {
|
||||
consume(i, 1 + 1); // base, gain
|
||||
} else if (t == "-o" || t == "-s" || t == "-t") {
|
||||
// up to 3 values; many files give 1 or 3 — consume up to 3 if present
|
||||
if (t == "-blendu" || t == "-blendv" || t == "-cc" || t == "-clamp" || t ==
|
||||
"-texopt") { consume(i, 1); } else if (
|
||||
t == "-bm" || t == "-boost" || t == "-texres" || t == "-type" || t ==
|
||||
"-imfchan") { consume(i, 1); } else if (t == "-mm") { consume(i, 2); } else if (
|
||||
t == "-o" || t == "-s" || t == "-t") {
|
||||
size_t take = std::min<size_t>(3, toks.size() - (i + 1));
|
||||
consume(i, 1 + (int) take);
|
||||
} else if (!t.empty() && t[0] == '-') {
|
||||
// Unknown option: conservatively consume next 1 value if present
|
||||
consume(i, 1 + (i + 1 < toks.size() ? 1 : 0));
|
||||
} else {
|
||||
// First non-option token begins the filename; everything remaining is part of the path
|
||||
break;
|
||||
}
|
||||
} else if (!t.empty() && t[0] == '-') { consume(i, 1 + (i + 1 < toks.size() ? 1 : 0)); } else break;
|
||||
}
|
||||
|
||||
if (i >= toks.size()) return {};
|
||||
|
||||
// Join the remaining tokens with spaces (rebuild paths like "C:\foo bar\baz.png")
|
||||
std::string joined = toks[i];
|
||||
for (size_t j = i + 1; j < toks.size(); ++j) {
|
||||
joined.push_back(' ');
|
||||
joined += toks[j];
|
||||
}
|
||||
// Unescape any backslash-escapes used to keep spaces
|
||||
return unescape_backslashes(joined);
|
||||
}
|
||||
|
||||
// ---------- MTL parsing ----------
|
||||
static void parse_mtl(const std::string &mtlFilePath,
|
||||
std::vector<Material> &outMaterials,
|
||||
std::unordered_map<std::string, int> &nameToIndex)
|
||||
{
|
||||
std::ifstream f(mtlFilePath);
|
||||
if (!f) {
|
||||
Logger::LogWarning("OBJ: failed to open MTL: %s", mtlFilePath.c_str());
|
||||
return;
|
||||
}
|
||||
std::string base = basedir(mtlFilePath);
|
||||
|
||||
Material cur;
|
||||
bool haveCur = false;
|
||||
|
||||
auto commit = [&]()
|
||||
{
|
||||
if (!haveCur) return;
|
||||
nameToIndex[cur.name] = (int) outMaterials.size();
|
||||
outMaterials.push_back(cur);
|
||||
};
|
||||
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
std::string raw = line; // keep raw for map_* parsing
|
||||
trim(line);
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
auto toks = split_ws(line);
|
||||
if (toks.empty()) continue;
|
||||
|
||||
const std::string &key = toks[0];
|
||||
if (key == "newmtl") {
|
||||
commit();
|
||||
cur = Material{};
|
||||
haveCur = true;
|
||||
// For newmtl, keep everything after the keyword to preserve names with spaces
|
||||
std::string name = raw.substr(raw.find("newmtl") + 6);
|
||||
trim(name);
|
||||
cur.name = name;
|
||||
} else if (key == "Kd" && toks.size() >= 4) {
|
||||
cur.kd = {std::stof(toks[1]), std::stof(toks[2]), std::stof(toks[3])};
|
||||
} else if (key == "Ks" && toks.size() >= 4) {
|
||||
cur.ks = {std::stof(toks[1]), std::stof(toks[2]), std::stof(toks[3])};
|
||||
} else if (key == "Ns" && toks.size() >= 2) {
|
||||
cur.ns = std::stof(toks[1]);
|
||||
} else if (key == "map_Kd" || key == "map_kd") {
|
||||
std::string path = extract_map_path_from_line(raw, key.c_str());
|
||||
if (!path.empty()) cur.mapKd = join_path(base, path);
|
||||
} else if (key == "map_Ks" || key == "map_ks") {
|
||||
std::string path = extract_map_path_from_line(raw, key.c_str());
|
||||
if (!path.empty()) cur.mapKs = join_path(base, path);
|
||||
} else if (key == "map_Bump" || key == "map_bump" || key == "bump" || key == "norm" || key ==
|
||||
"map_normal") {
|
||||
std::string path = extract_map_path_from_line(raw, key.c_str());
|
||||
if (!path.empty()) cur.mapNormal = join_path(base, path);
|
||||
}
|
||||
// ignore d/Tr/illum/etc for now
|
||||
}
|
||||
commit();
|
||||
}
|
||||
|
||||
// ---------- tangent generation ----------
|
||||
static void compute_tangents(std::vector<Vertex> &verts, const std::vector<uint32_t> &idx)
|
||||
{
|
||||
@@ -316,9 +196,7 @@ namespace OX
|
||||
|
||||
for (size_t i = 0; i + 2 < idx.size(); i += 3) {
|
||||
uint32_t i0 = idx[i], i1 = idx[i + 1], i2 = idx[i + 2];
|
||||
const Vertex &v0 = verts[i0];
|
||||
const Vertex &v1 = verts[i1];
|
||||
const Vertex &v2 = verts[i2];
|
||||
const Vertex &v0 = verts[i0], &v1 = verts[i1], &v2 = verts[i2];
|
||||
|
||||
float x1 = v1.px - v0.px, y1 = v1.py - v0.py, z1 = v1.pz - v0.pz;
|
||||
float x2 = v2.px - v0.px, y2 = v2.py - v0.py, z2 = v2.pz - v0.pz;
|
||||
@@ -327,8 +205,7 @@ namespace OX
|
||||
float s2 = v2.u - v0.u, t2 = v2.v - v0.v;
|
||||
|
||||
float r = (s1 * t2 - s2 * t1);
|
||||
if (std::fabs(r) < 1e-12f) r = 1.0f;
|
||||
else r = 1.0f / r;
|
||||
r = (std::fabs(r) < 1e-12f) ? 1.0f : 1.0f / r;
|
||||
|
||||
float tx = (t2 * x1 - t1 * x2) * r;
|
||||
float ty = (t2 * y1 - t1 * y2) * r;
|
||||
@@ -360,29 +237,27 @@ namespace OX
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < verts.size(); ++i) {
|
||||
// Gram-Schmidt orthonormalization
|
||||
float n[3] = {verts[i].nx, verts[i].ny, verts[i].nz};
|
||||
float t[3] = {tan[i][0], tan[i][1], tan[i][2]};
|
||||
float b[3] = {bit[i][0], bit[i][1], bit[i][2]};
|
||||
|
||||
// T = normalize(T - N * dot(N,T))
|
||||
float ndott = v_dot(n, t);
|
||||
float ndott = n[0] * t[0] + n[1] * t[1] + n[2] * t[2];
|
||||
float T[3] = {t[0] - n[0] * ndott, t[1] - n[1] * ndott, t[2] - n[2] * ndott};
|
||||
v_norm(T);
|
||||
float Bl[3] = {b[0], b[1], b[2]};
|
||||
|
||||
// B normalized (we’re not deriving handedness here)
|
||||
v_norm(b);
|
||||
v_norm(T);
|
||||
v_norm(Bl);
|
||||
|
||||
verts[i].tx = T[0];
|
||||
verts[i].ty = T[1];
|
||||
verts[i].tz = T[2];
|
||||
verts[i].bx = b[0];
|
||||
verts[i].by = b[1];
|
||||
verts[i].bz = b[2];
|
||||
verts[i].bx = Bl[0];
|
||||
verts[i].by = Bl[1];
|
||||
verts[i].bz = Bl[2];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- normal generation (if some normals missing) ----------
|
||||
// ---------- normal generation ----------
|
||||
static void compute_smooth_normals(std::vector<Vertex> &verts, const std::vector<uint32_t> &idx)
|
||||
{
|
||||
std::vector<std::array<float, 3> > acc(verts.size(), {0, 0, 0});
|
||||
@@ -394,7 +269,7 @@ namespace OX
|
||||
float e1[3], e2[3], n[3];
|
||||
v_sub(p1, p0, e1);
|
||||
v_sub(p2, p0, e2);
|
||||
v_cross(e1, e2, n); // not normalized (area-weighted)
|
||||
v_cross(e1, e2, n);
|
||||
acc[i0][0] += n[0];
|
||||
acc[i0][1] += n[1];
|
||||
acc[i0][2] += n[2];
|
||||
@@ -414,30 +289,10 @@ namespace OX
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- OBJ parsing ----------
|
||||
struct VKey
|
||||
{
|
||||
int v = -1, vt = -1, vn = -1, mat = -1;
|
||||
|
||||
bool operator==(const VKey &o) const noexcept
|
||||
{
|
||||
return v == o.v && vt == o.vt && vn == o.vn && mat == o.mat;
|
||||
}
|
||||
};
|
||||
|
||||
struct VKeyHash
|
||||
{
|
||||
size_t operator()(const VKey &k) const noexcept
|
||||
{
|
||||
return ((size_t) k.v * 73856093) ^ ((size_t) k.vt * 19349663) ^ ((size_t) k.vn * 83492791) ^ (size_t) (
|
||||
k.mat + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- OBJ index tuple parsing ----------
|
||||
static bool parse_vertex_triplet(const std::string &tok, int vCount, int vtCount, int vnCount, int &v, int &vt,
|
||||
int &vn)
|
||||
{
|
||||
// formats: v, v/vt, v//vn, v/vt/vn (OBJ indices are 1-based, negatives allowed)
|
||||
v = vt = vn = -1;
|
||||
size_t p1 = tok.find('/');
|
||||
if (p1 == std::string::npos) {
|
||||
@@ -450,14 +305,12 @@ namespace OX
|
||||
v = fix_index(vi, vCount);
|
||||
|
||||
if (p2 == std::string::npos) {
|
||||
// v/vt
|
||||
std::string vtstr = tok.substr(p1 + 1);
|
||||
if (!vtstr.empty()) {
|
||||
int vti = std::stoi(vtstr);
|
||||
vt = fix_index(vti, vtCount);
|
||||
}
|
||||
} else {
|
||||
// v//vn or v/vt/vn
|
||||
std::string vtstr = tok.substr(p1 + 1, p2 - (p1 + 1));
|
||||
std::string vnstr = tok.substr(p2 + 1);
|
||||
if (!vtstr.empty()) {
|
||||
@@ -472,16 +325,101 @@ namespace OX
|
||||
return v >= 0;
|
||||
}
|
||||
|
||||
bool MeshLoaderOBJ::Load(const std::string &objPath, MeshLoadResult &out)
|
||||
// ---------- MTL parsing (loads textures via AssetManager) ----------
|
||||
static void parse_mtl_and_load_textures(const std::string &mtlFilePath,
|
||||
std::vector<Material> &outMaterials,
|
||||
std::unordered_map<std::string, int> &nameToIndex)
|
||||
{
|
||||
out = {}; // reset
|
||||
std::ifstream f(mtlFilePath);
|
||||
if (!f) {
|
||||
Logger::LogWarning("OBJ: failed to open MTL: %s", mtlFilePath.c_str());
|
||||
return;
|
||||
}
|
||||
std::string base = basedir(mtlFilePath);
|
||||
|
||||
Material cur;
|
||||
bool haveCur = false;
|
||||
auto commit = [&]()
|
||||
{
|
||||
if (!haveCur) return;
|
||||
nameToIndex[cur.name] = (int) outMaterials.size();
|
||||
outMaterials.push_back(cur);
|
||||
};
|
||||
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
std::string raw = line;
|
||||
trim(line);
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
auto toks = split_ws(line);
|
||||
if (toks.empty()) continue;
|
||||
|
||||
const std::string &key = toks[0];
|
||||
if (key == "newmtl") {
|
||||
commit();
|
||||
cur = Material{};
|
||||
haveCur = true;
|
||||
std::string name = raw.substr(raw.find("newmtl") + 6);
|
||||
trim(name);
|
||||
cur.name = name;
|
||||
} else if (key == "Kd" && toks.size() >= 4) {
|
||||
cur.kd = {std::stof(toks[1]), std::stof(toks[2]), std::stof(toks[3])};
|
||||
} else if (key == "Ks" && toks.size() >= 4) {
|
||||
cur.ks = {std::stof(toks[1]), std::stof(toks[2]), std::stof(toks[3])};
|
||||
} else if (key == "Ns" && toks.size() >= 2) {
|
||||
cur.ns = std::stof(toks[1]);
|
||||
} else if (key == "map_Kd" || key == "map_kd") {
|
||||
std::string path = extract_map_path_from_line(raw, key.c_str());
|
||||
if (!path.empty()) cur.mapKd = join_path(base, path);
|
||||
} else if (key == "map_Ks" || key == "map_ks") {
|
||||
std::string path = extract_map_path_from_line(raw, key.c_str());
|
||||
if (!path.empty()) cur.mapKs = join_path(base, path);
|
||||
} else if (key == "map_Bump" || key == "map_bump" || key == "bump" || key == "norm" || key ==
|
||||
"map_normal") {
|
||||
std::string path = extract_map_path_from_line(raw, key.c_str());
|
||||
if (!path.empty()) cur.mapNormal = join_path(base, path);
|
||||
}
|
||||
// ignore other props (illum/d/Tr/etc)
|
||||
}
|
||||
commit();
|
||||
|
||||
// Todo: Get Textures from asset Manager
|
||||
|
||||
// ---- Resolve textures via AssetManager (paths may be absolute or virtual) ----
|
||||
auto load_tex_into = [](GLTexture &dst, const std::string &path)
|
||||
{
|
||||
if (path.empty()) return;
|
||||
const uint64_t tid = AssetManager::GetIdForPath(path);
|
||||
if (!tid) return;
|
||||
auto tex = AssetManager::GetTexture(tid);
|
||||
if (!tex) return;
|
||||
|
||||
if constexpr (true) {
|
||||
dst.SetFromGL(tex->GetID(), tex->GetWidth(), tex->GetHeight());
|
||||
}
|
||||
};
|
||||
|
||||
for (auto &m: outMaterials) {
|
||||
load_tex_into(m.texKd, m.mapKd);
|
||||
load_tex_into(m.texKs, m.mapKs);
|
||||
load_tex_into(m.texNormal, m.mapNormal);
|
||||
}
|
||||
}
|
||||
|
||||
// --- internals ---
|
||||
static bool load_obj_file(const std::string &objPath, std::shared_ptr<OX::ModelAsset> &out)
|
||||
{
|
||||
std::ifstream f(objPath);
|
||||
if (!f) {
|
||||
Logger::LogError("OBJ: failed to open %s", objPath.c_str());
|
||||
OX::Logger::LogError("OBJ: failed to open %s", objPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct target BEFORE any lambda uses it
|
||||
if (!out) out = std::make_shared<OX::ModelAsset>();
|
||||
out->clear();
|
||||
|
||||
const std::string base = basedir(objPath);
|
||||
|
||||
// temp arrays for raw indices
|
||||
@@ -492,23 +430,41 @@ namespace OX
|
||||
std::vector<std::array<float, 2> > T;
|
||||
T.reserve(1024);
|
||||
|
||||
std::vector<Material> materials;
|
||||
std::vector<OX::Material> materials;
|
||||
std::unordered_map<std::string, int> mtlNameToIndex;
|
||||
|
||||
// final mesh building
|
||||
struct VKey
|
||||
{
|
||||
int v = -1, vt = -1, vn = -1, mat = -1;
|
||||
|
||||
bool operator==(const VKey &o) const noexcept
|
||||
{
|
||||
return v == o.v && vt == o.vt && vn == o.vn && mat == o.mat;
|
||||
}
|
||||
};
|
||||
struct VKeyHash
|
||||
{
|
||||
size_t operator()(const VKey &k) const noexcept
|
||||
{
|
||||
return ((size_t) k.v * 73856093) ^ ((size_t) k.vt * 19349663)
|
||||
^ ((size_t) k.vn * 83492791) ^ (size_t) (k.mat + 1);
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<VKey, uint32_t, VKeyHash> dedup;
|
||||
std::vector<Vertex> verts;
|
||||
std::vector<OX::Vertex> verts;
|
||||
std::vector<uint32_t> indices;
|
||||
|
||||
// submesh splitting by material
|
||||
int currentMat = -1;
|
||||
bool haveCurrentSubmesh = false;
|
||||
SubMesh curSub{0, 0, -1};
|
||||
OX::SubMesh curSub{0, 0, -1};
|
||||
|
||||
auto flushSubmesh = [&]()
|
||||
{
|
||||
if (haveCurrentSubmesh) {
|
||||
out.submeshes.push_back(curSub);
|
||||
out->submeshes.push_back(curSub);
|
||||
haveCurrentSubmesh = false;
|
||||
}
|
||||
};
|
||||
@@ -526,7 +482,7 @@ namespace OX
|
||||
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
std::string raw = line; // keep raw for mtllib join if needed
|
||||
std::string raw = line;
|
||||
trim(line);
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
@@ -538,14 +494,12 @@ namespace OX
|
||||
if (key == "v" && toks.size() >= 4) {
|
||||
P.push_back({std::stof(toks[1]), std::stof(toks[2]), std::stof(toks[3])});
|
||||
} else if (key == "vt" && toks.size() >= 3) {
|
||||
// allow optional third component and ignore
|
||||
T.push_back({std::stof(toks[1]), std::stof(toks[2])});
|
||||
} else if (key == "vn" && toks.size() >= 4) {
|
||||
N.push_back({std::stof(toks[1]), std::stof(toks[2]), std::stof(toks[3])});
|
||||
} else if (key == "f" && toks.size() >= 4) {
|
||||
beginSubmeshIfNeeded();
|
||||
|
||||
// fan-triangulate: f v0 v1 v2 v3 -> (v0,v1,v2),(v0,v2,v3)...
|
||||
int v0 = -1, vt0 = -1, vn0 = -1;
|
||||
if (!parse_vertex_triplet(toks[1], (int) P.size(), (int) T.size(), (int) N.size(), v0, vt0, vn0))
|
||||
continue;
|
||||
@@ -559,15 +513,13 @@ namespace OX
|
||||
continue;
|
||||
|
||||
int idxs[3][3] = {{v0, vt0, vn0}, {v1, vt1, vn1}, {v2, vt2, vn2}};
|
||||
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
VKey keyv{idxs[k][0], idxs[k][1], idxs[k][2], currentMat};
|
||||
|
||||
auto it = dedup.find(keyv);
|
||||
if (it != dedup.end()) {
|
||||
indices.push_back(it->second);
|
||||
} else {
|
||||
Vertex V{};
|
||||
OX::Vertex V{};
|
||||
const auto &p = P[keyv.v];
|
||||
V.px = p[0];
|
||||
V.py = p[1];
|
||||
@@ -579,103 +531,127 @@ namespace OX
|
||||
V.ny = n[1];
|
||||
V.nz = n[2];
|
||||
} else {
|
||||
V.nx = V.ny = 0;
|
||||
V.nx = 0;
|
||||
V.ny = 0;
|
||||
V.nz = 1;
|
||||
}
|
||||
|
||||
if (keyv.vt >= 0) {
|
||||
const auto &t = T[keyv.vt];
|
||||
V.u = t[0];
|
||||
V.v = t[1];
|
||||
} else { V.u = V.v = 0; }
|
||||
|
||||
} else {
|
||||
V.u = 0;
|
||||
V.v = 0;
|
||||
}
|
||||
V.tx = V.ty = V.tz = 0;
|
||||
V.bx = V.by = V.bz = 0;
|
||||
|
||||
uint32_t newIndex = (uint32_t) verts.size();
|
||||
uint32_t newIdx = (uint32_t) verts.size();
|
||||
verts.push_back(V);
|
||||
dedup.emplace(keyv, newIndex);
|
||||
indices.push_back(newIndex);
|
||||
dedup.emplace(keyv, newIdx);
|
||||
indices.push_back(newIdx);
|
||||
}
|
||||
}
|
||||
|
||||
curSub.indexCount += 3;
|
||||
}
|
||||
} else if (key == "usemtl") {
|
||||
// Keep full material name (can have spaces) by slicing raw
|
||||
size_t at = raw.find("usemtl");
|
||||
std::string mname = (at == std::string::npos)
|
||||
? toks[1]
|
||||
? (toks.size() >= 2 ? toks[1] : std::string())
|
||||
: raw.substr(at + 6);
|
||||
trim(mname);
|
||||
if (mname.empty() && toks.size() >= 2) mname = toks[1];
|
||||
|
||||
auto it = mtlNameToIndex.find(mname);
|
||||
if (it == mtlNameToIndex.end()) {
|
||||
Material m;
|
||||
OX::Material m;
|
||||
m.name = mname;
|
||||
mtlNameToIndex[mname] = (int) materials.size();
|
||||
materials.push_back(m);
|
||||
}
|
||||
currentMat = mtlNameToIndex[mname];
|
||||
} else if (key == "mtllib") {
|
||||
// Preserve spaces in library filename(s)
|
||||
size_t at = raw.find("mtllib");
|
||||
std::string mtlFile = (at == std::string::npos) ? std::string() : raw.substr(at + 6);
|
||||
trim(mtlFile);
|
||||
if (mtlFile.empty() && toks.size() >= 2) {
|
||||
// fallback: join tokens with spaces
|
||||
for (size_t i = 1; i < toks.size(); ++i) {
|
||||
if (i > 1) mtlFile += " ";
|
||||
mtlFile += toks[i];
|
||||
}
|
||||
}
|
||||
std::string mtlPath = join_path(base, mtlFile);
|
||||
parse_mtl(mtlPath, materials, mtlNameToIndex);
|
||||
const std::string mtlPath = join_path(base, mtlFile);
|
||||
parse_mtl_and_load_textures(mtlPath, materials, mtlNameToIndex);
|
||||
}
|
||||
|
||||
// ignore o/g/s/others
|
||||
}
|
||||
flushSubmesh();
|
||||
|
||||
// transfer results
|
||||
out.vertices = std::move(verts);
|
||||
out.indices = std::move(indices);
|
||||
out.submeshes = std::move(std::vector<SubMesh>{}); // will rebuild below
|
||||
out.materials = std::move(materials);
|
||||
// Move parsed data into the already-constructed asset
|
||||
out->vertices = std::move(verts);
|
||||
out->indices = std::move(indices);
|
||||
out->materials = std::move(materials);
|
||||
|
||||
// Rebuild submeshes with contiguous ranges per material (already done above but guard if no faces parsed)
|
||||
if (curSub.indexCount > 0) {
|
||||
// already flushed
|
||||
} else if (!out.indices.empty()) {
|
||||
// single submesh default
|
||||
SubMesh s;
|
||||
if (out->submeshes.empty() && !out->indices.empty()) {
|
||||
OX::SubMesh s;
|
||||
s.indexOffset = 0;
|
||||
s.indexCount = (uint32_t) out.indices.size();
|
||||
s.indexCount = (uint32_t) out->indices.size();
|
||||
s.materialIndex = currentMat;
|
||||
out.submeshes.push_back(s);
|
||||
out->submeshes.push_back(s);
|
||||
}
|
||||
|
||||
// If any vertex had 0,0,1 as “no normal” marker, compute smooth normals
|
||||
// normals if needed
|
||||
bool needNormals = false;
|
||||
for (const auto &v: out.vertices) {
|
||||
for (const auto &v: out->vertices) {
|
||||
if (v.nx == 0 && v.ny == 0 && v.nz == 1) {
|
||||
needNormals = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (needNormals) compute_smooth_normals(out.vertices, out.indices);
|
||||
if (needNormals) compute_smooth_normals(out->vertices, out->indices);
|
||||
|
||||
// Tangents if we have UVs
|
||||
// tangents if we have UVs
|
||||
bool hasUV = false;
|
||||
for (const auto &v: out.vertices) {
|
||||
for (const auto &v: out->vertices) {
|
||||
if (v.u != 0 || v.v != 0) {
|
||||
hasUV = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasUV) compute_tangents(out.vertices, out.indices);
|
||||
if (hasUV) compute_tangents(out->vertices, out->indices);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// --- public API ---
|
||||
std::shared_ptr<OX::ModelAsset>
|
||||
OX::MeshLoaderOBJ::LoadFromPath(const std::string &objPathOrVPath)
|
||||
{
|
||||
std::string abs = objPathOrVPath;
|
||||
if (objPathOrVPath.rfind("res://", 0) == 0) {
|
||||
const uint64_t id = OX::AssetManager::GetIdForPath(objPathOrVPath);
|
||||
if (id) {
|
||||
std::string resolved = OX::AssetManager::GetAbsolutePathById(id);
|
||||
if (!resolved.empty()) abs = std::move(resolved);
|
||||
}
|
||||
}
|
||||
std::shared_ptr<OX::ModelAsset> model;
|
||||
if (!load_obj_file(abs, model)) return nullptr;
|
||||
return model;
|
||||
}
|
||||
|
||||
std::shared_ptr<OX::ModelAsset>
|
||||
OX::MeshLoaderOBJ::LoadById(uint64_t objId)
|
||||
{
|
||||
if (!objId) return nullptr;
|
||||
const std::string abs = OX::AssetManager::GetAbsolutePathById(objId);
|
||||
if (abs.empty()) {
|
||||
OX::Logger::LogError("OBJ: id=%llu not found in AssetManager", (unsigned long long) objId);
|
||||
return nullptr;
|
||||
}
|
||||
std::shared_ptr<OX::ModelAsset> model;
|
||||
if (!load_obj_file(abs, model)) return nullptr;
|
||||
return model;
|
||||
}
|
||||
} // namespace OX
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
|
||||
#include "systems/Scene/Components/MeshComponent.h" // for Vertex, SubMesh, Material
|
||||
#include <cstdint>
|
||||
#include "systems/assets/Model.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
struct MeshLoadResult
|
||||
{
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
std::vector<SubMesh> submeshes;
|
||||
std::vector<Material> materials;
|
||||
};
|
||||
|
||||
class MeshLoaderOBJ
|
||||
{
|
||||
public:
|
||||
// Load OBJ + MTL. Handles relative texture paths.
|
||||
static bool Load(const std::string &objPath, MeshLoadResult &out);
|
||||
static std::shared_ptr<ModelAsset> LoadFromPath(const std::string &objPathOrVPath);
|
||||
|
||||
static std::shared_ptr<ModelAsset> LoadById(uint64_t objId);
|
||||
};
|
||||
} // namespace OX
|
||||
}
|
||||
|
||||
77
src/core/systems/assets/Model.h
Normal file
77
src/core/systems/assets/Model.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "Asset.h"
|
||||
#include <array>
|
||||
|
||||
namespace OX
|
||||
{
|
||||
struct GLTexture
|
||||
{
|
||||
unsigned int id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int channels = 0;
|
||||
std::string path;
|
||||
|
||||
void Destroy();
|
||||
|
||||
void SetFromGL(unsigned int id, int width, int height);
|
||||
};
|
||||
|
||||
struct Material
|
||||
{
|
||||
std::string name;
|
||||
std::array<float, 3> kd{1, 1, 1}; // diffuse
|
||||
std::array<float, 3> ks{0, 0, 0}; // specular
|
||||
float ns = 16.0f; // shininess
|
||||
|
||||
std::string mapKd; // diffuse map path
|
||||
std::string mapKs; // specular map path
|
||||
std::string mapNormal; // normal map path (bump/normal)
|
||||
|
||||
GLTexture texKd;
|
||||
GLTexture texKs;
|
||||
GLTexture texNormal;
|
||||
};
|
||||
|
||||
struct Vertex
|
||||
{
|
||||
float px, py, pz; // position
|
||||
float nx, ny, nz; // normal
|
||||
float u, v; // uv
|
||||
float tx, ty, tz; // tangent
|
||||
float bx, by, bz; // bitangent
|
||||
};
|
||||
|
||||
struct SubMesh
|
||||
{
|
||||
uint32_t indexOffset = 0;
|
||||
uint32_t indexCount = 0;
|
||||
int materialIndex = -1;
|
||||
};
|
||||
|
||||
|
||||
struct ModelAsset : public Asset
|
||||
{
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
std::vector<SubMesh> submeshes;
|
||||
std::vector<Material> materials;
|
||||
|
||||
std::string GetTypeName() const override { return "model"; }
|
||||
|
||||
|
||||
void clear()
|
||||
{
|
||||
vertices.clear();
|
||||
vertices.shrink_to_fit();
|
||||
indices.clear();
|
||||
indices.shrink_to_fit();
|
||||
submeshes.clear();
|
||||
submeshes.shrink_to_fit();
|
||||
materials.clear();
|
||||
materials.shrink_to_fit();
|
||||
}
|
||||
};
|
||||
} // namespace OX
|
||||
@@ -9,18 +9,18 @@
|
||||
#include <GL/glew.h>
|
||||
#include "Texture2D.h"
|
||||
|
||||
namespace OX {
|
||||
|
||||
Texture2D::~Texture2D() {
|
||||
namespace OX
|
||||
{
|
||||
Texture2D::~Texture2D()
|
||||
{
|
||||
if (m_TextureID)
|
||||
glDeleteTextures(1, &m_TextureID);
|
||||
}
|
||||
|
||||
void Texture2D::SetFromGL(uint32_t texID, int width, int height) {
|
||||
void Texture2D::SetFromGL(uint32_t texID, int width, int height)
|
||||
{
|
||||
m_TextureID = texID;
|
||||
m_Width = width;
|
||||
m_Height = height;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Created by spenc on 5/21/2025.
|
||||
//
|
||||
#pragma once
|
||||
#include "systems/Asset.h"
|
||||
#include "Asset.h"
|
||||
#include <string>
|
||||
|
||||
namespace OX {
|
||||
@@ -12,7 +12,6 @@ namespace OX {
|
||||
~Texture2D();
|
||||
|
||||
std::string GetTypeName() const override { return "texture2D"; }
|
||||
bool LoadFromFile(const std::string& path) override { return false; } // Not used now
|
||||
|
||||
void SetFromGL(uint32_t texID, int width, int height);
|
||||
|
||||
|
||||
@@ -1,225 +1,557 @@
|
||||
// File: src/FileBrowser.cpp
|
||||
#include "FileBrowser.h"
|
||||
#include <filesystem>
|
||||
#include "systems/Profiler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <GL/glew.h>
|
||||
#include <stb/stb_image.h>
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
|
||||
#include "systems/AssetManager.h" // ResourceTreeNode, AssetManager
|
||||
#include "systems/assets/Asset.h"
|
||||
#include "systems/assets/Texture2D.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
FileBrowser::FileBrowser()
|
||||
: _lastProgress(0.0f)
|
||||
// ============================================================
|
||||
// Small helpers
|
||||
// ============================================================
|
||||
static inline GLuint ImTexToGL(ImTextureID id)
|
||||
{
|
||||
Logger::LogVerbose("Editor::InspectorWindow");
|
||||
return static_cast<GLuint>(reinterpret_cast<uintptr_t>(id));
|
||||
}
|
||||
|
||||
FileBrowser::~FileBrowser() = default;
|
||||
|
||||
// Call this every frame to report your current loading progress (0.0 → 1.0).
|
||||
// Passing in 0 resets it and hides the bar.
|
||||
void FileBrowser::SetProgress(const float p)
|
||||
static inline ImTextureID GLToImTex(GLuint tex)
|
||||
{
|
||||
if (p <= 0.0f) {
|
||||
_lastProgress = 0.0f;
|
||||
} else {
|
||||
_lastProgress = std::max(_lastProgress, p);
|
||||
return reinterpret_cast<ImTextureID>(static_cast<uintptr_t>(tex));
|
||||
}
|
||||
|
||||
static std::vector<std::string> SplitPathSegments(const std::string &vpath)
|
||||
{
|
||||
std::vector<std::string> segs;
|
||||
if (vpath.rfind("res://", 0) != 0) return segs;
|
||||
|
||||
segs.emplace_back("res://");
|
||||
std::string rest = vpath.substr(6);
|
||||
size_t pos = 0;
|
||||
while (pos < rest.size()) {
|
||||
size_t slash = rest.find('/', pos);
|
||||
if (slash == std::string::npos) {
|
||||
auto chunk = rest.substr(pos);
|
||||
if (!chunk.empty()) segs.push_back(chunk);
|
||||
break;
|
||||
}
|
||||
auto chunk = rest.substr(pos, slash - pos);
|
||||
if (!chunk.empty()) segs.push_back(chunk);
|
||||
pos = slash + 1;
|
||||
}
|
||||
return segs;
|
||||
}
|
||||
|
||||
static std::string JoinBreadcrumb(const std::vector<std::string> &segs, size_t upto)
|
||||
{
|
||||
if (segs.empty() || upto >= segs.size()) return "res://";
|
||||
if (upto == 0) return "res://";
|
||||
std::ostringstream os;
|
||||
os << "res://";
|
||||
for (size_t i = 1; i <= upto; ++i) {
|
||||
os << segs[i];
|
||||
if (i + 1 <= upto) os << "/";
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ctor / dtor
|
||||
// ============================================================
|
||||
FileBrowser::FileBrowser(const std::string &rootVPath)
|
||||
: m_rootVPath(rootVPath.empty() ? std::string("res://") : rootVPath),
|
||||
m_currentVPath(m_rootVPath)
|
||||
{
|
||||
// Default tile visuals
|
||||
m_thumbSize = 112.0f; // inner image square
|
||||
m_cellPadding = 10.0f; // inner padding in tile
|
||||
m_maxThumbsPerFrame = 8;
|
||||
|
||||
RebuildListing_();
|
||||
|
||||
SetCurrentVPath(m_rootVPath);
|
||||
}
|
||||
|
||||
FileBrowser::~FileBrowser()
|
||||
{
|
||||
for (auto &kv: m_thumbCache) {
|
||||
if (kv.second) {
|
||||
GLuint t = ImTexToGL(kv.second);
|
||||
glDeleteTextures(1, &t);
|
||||
}
|
||||
}
|
||||
m_thumbCache.clear();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Public API
|
||||
// ============================================================
|
||||
void FileBrowser::SetCurrentVPath(const std::string &vpath)
|
||||
{
|
||||
if (vpath.rfind("res://", 0) == 0) {
|
||||
m_currentVPath = vpath;
|
||||
RebuildListing_();
|
||||
}
|
||||
}
|
||||
|
||||
void FileBrowser::SetFilter(const std::string &f) { _filter = f; }
|
||||
void FileBrowser::SetFileSelectedCallback(FileSelectedCallback cb) { _onFileSelected = std::move(cb); }
|
||||
|
||||
void FileBrowser::Draw(const char *title)
|
||||
void FileBrowser::Draw(const char *windowTitle)
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
|
||||
|
||||
ImGui::Begin(title);
|
||||
|
||||
DrawToolbar();
|
||||
|
||||
// ——— main content ———
|
||||
auto root = AssetManager::GetFileTree();
|
||||
std::function<std::shared_ptr<ResourceTreeNode>(std::shared_ptr<ResourceTreeNode>, const std::string &)> find =
|
||||
[&](auto node, const std::string &path) -> std::shared_ptr<ResourceTreeNode>
|
||||
{
|
||||
if (node->path == path)
|
||||
return node;
|
||||
for (auto &c: node->children)
|
||||
if (c->isDirectory)
|
||||
if (auto f = find(c, path))
|
||||
return f;
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
auto node = find(root, _currentPath);
|
||||
if (!node) {
|
||||
ImGui::TextColored(ImGui::GetStyle().Colors[ImGuiCol_TextDisabled],
|
||||
"Path not found: %s", _currentPath.c_str());
|
||||
} else if (_gridMode) {
|
||||
// ——— Grid view with nicer spacing ———
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(_cfg.padding, _cfg.padding));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(_cfg.padding, _cfg.padding));
|
||||
DrawGridView(node);
|
||||
ImGui::PopStyleVar(2);
|
||||
} else {
|
||||
DrawListView(node);
|
||||
if (!windowTitle) windowTitle = "Assets";
|
||||
if (!ImGui::Begin(windowTitle)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
DrawToolbar_();
|
||||
DrawBreadcrumbs_();
|
||||
DrawGrid_();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ——— Combined toolbar & inline path ———
|
||||
void FileBrowser::DrawToolbar()
|
||||
// ============================================================
|
||||
// Snapshot & traversal
|
||||
// ============================================================
|
||||
std::shared_ptr<ResourceTreeNode> FileBrowser::SnapshotRoot_() const
|
||||
{
|
||||
if (_currentPath != "res://") {
|
||||
if (ImGui::Button("Back")) {
|
||||
auto pos = _currentPath.find_last_of('/');
|
||||
_currentPath = (pos != std::string::npos && pos > 6)
|
||||
? _currentPath.substr(0, pos)
|
||||
: "res://";
|
||||
std::scoped_lock lk(AssetManager::s_TreeMutex);
|
||||
return AssetManager::GetFileTree();
|
||||
}
|
||||
|
||||
std::shared_ptr<ResourceTreeNode>
|
||||
FileBrowser::FindNodeByVPath_(const std::shared_ptr<ResourceTreeNode> &root,
|
||||
const std::string &vpath) const
|
||||
{
|
||||
if (!root) return nullptr;
|
||||
if (vpath == "res://") return root;
|
||||
|
||||
auto segs = SplitPathSegments(vpath);
|
||||
if (segs.empty()) return nullptr;
|
||||
|
||||
auto node = root;
|
||||
for (size_t i = 1; i < segs.size(); ++i) {
|
||||
const std::string &want = segs[i];
|
||||
std::shared_ptr<ResourceTreeNode> next = nullptr;
|
||||
for (auto &c: node->children) {
|
||||
if (c && c->name == want && c->isDirectory) {
|
||||
next = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (!next) return nullptr;
|
||||
node = next;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
void FileBrowser::RebuildListing_()
|
||||
{
|
||||
m_entries.clear();
|
||||
|
||||
auto root = SnapshotRoot_();
|
||||
auto node = FindNodeByVPath_(root, m_currentVPath);
|
||||
if (!node) return;
|
||||
|
||||
// Add virtual ".." entry if not at root
|
||||
if (m_currentVPath != "res://") {
|
||||
Entry up;
|
||||
up.name = "..";
|
||||
up.vpath = m_currentVPath; // compute parent on click
|
||||
up.isDirectory = true;
|
||||
up.type = OX_AssetType::Unknown;
|
||||
up.id = 0;
|
||||
m_entries.push_back(std::move(up));
|
||||
}
|
||||
|
||||
ImGui::Text("%s", _currentPath.c_str());
|
||||
ImGui::SameLine();
|
||||
// Folders first
|
||||
for (auto &c: node->children) {
|
||||
if (!c) continue;
|
||||
if (c->isDirectory) {
|
||||
Entry e;
|
||||
e.name = c->name;
|
||||
e.vpath = c->path;
|
||||
e.isDirectory = true;
|
||||
e.type = OX_AssetType::Unknown;
|
||||
e.id = 0;
|
||||
m_entries.push_back(std::move(e));
|
||||
}
|
||||
}
|
||||
// Files
|
||||
for (auto &c: node->children) {
|
||||
if (!c) continue;
|
||||
if (!c->isDirectory) {
|
||||
Entry e;
|
||||
e.name = c->name;
|
||||
e.vpath = c->path;
|
||||
e.isDirectory = false;
|
||||
e.id = AssetManager::GetIdForPath(c->path);
|
||||
e.type = e.id ? AssetManager::GetAssetTypeById(e.id) : OX_AssetType::Unknown;
|
||||
m_entries.push_back(std::move(e));
|
||||
}
|
||||
}
|
||||
|
||||
float avail = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::PushItemWidth(avail * 0.5f);
|
||||
//ImGui::InputTextWithHint("##filter", "Filter files...", &_filter);
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::SameLine();
|
||||
// Stable sort: folders alpha, then files alpha
|
||||
auto key = [](const Entry &e) { return std::make_pair(!e.isDirectory, e.name); };
|
||||
std::stable_sort(m_entries.begin(), m_entries.end(),
|
||||
[&](const Entry &a, const Entry &b) { return key(a) < key(b); });
|
||||
}
|
||||
|
||||
if (_gridMode) {
|
||||
if (ImGui::Button("List View")) _gridMode = false;
|
||||
} else {
|
||||
if (ImGui::Button("Grid View")) _gridMode = true;
|
||||
// ============================================================
|
||||
// Thumbs
|
||||
// ============================================================
|
||||
ImTextureID FileBrowser::GetThumbFor_(uint64_t id, OX_AssetType type, const std::string & /*vpath*/)
|
||||
{
|
||||
if (type != OX_AssetType::Texture || id == 0) return 0;
|
||||
auto it = m_thumbCache.find(id);
|
||||
if (it != m_thumbCache.end()) return it->second;
|
||||
return EnsureTextureThumb_(id);
|
||||
}
|
||||
|
||||
ImTextureID FileBrowser::UploadGLTextureRGBA_(int w, int h, const unsigned char *rgba)
|
||||
{
|
||||
if (w <= 0 || h <= 0 || rgba == nullptr) return 0;
|
||||
|
||||
GLuint tex = 0;
|
||||
glGenTextures(1, &tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return GLToImTex(tex);
|
||||
}
|
||||
|
||||
ImTextureID FileBrowser::EnsureTextureThumb_(uint64_t id)
|
||||
{
|
||||
std::string abs = AssetManager::GetAbsolutePathById(id);
|
||||
if (abs.empty()) {
|
||||
m_thumbCache[id] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int w = 0, h = 0, comp = 0;
|
||||
unsigned char *data = stbi_load(abs.c_str(), &w, &h, &comp, 4);
|
||||
if (!data) {
|
||||
m_thumbCache[id] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImTextureID tex = UploadGLTextureRGBA_(w, h, data);
|
||||
stbi_image_free(data);
|
||||
|
||||
m_thumbCache[id] = tex;
|
||||
return tex;
|
||||
}
|
||||
|
||||
ImTextureID FileBrowser::ToImGuiID_(const std::shared_ptr<Texture2D> &tex) const
|
||||
{
|
||||
(void) tex;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UI
|
||||
// ============================================================
|
||||
void FileBrowser::DrawToolbar_()
|
||||
{
|
||||
// Minimal toolbar: Home + Up
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6, 6));
|
||||
|
||||
if (ImGui::Button("Home")) {
|
||||
SetCurrentVPath(m_rootVPath);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Rescan")) {
|
||||
AssetManager::Rescan();
|
||||
|
||||
if (ImGui::Button("Up")) {
|
||||
if (m_currentVPath != "res://") {
|
||||
auto segs = SplitPathSegments(m_currentVPath);
|
||||
if (segs.size() > 1) {
|
||||
std::string parent = "res://";
|
||||
if (segs.size() > 2) {
|
||||
for (size_t j = 1; j < segs.size() - 1; ++j) {
|
||||
parent += segs[j];
|
||||
if (j + 1 < segs.size() - 1) parent += "/";
|
||||
}
|
||||
}
|
||||
SetCurrentVPath(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t total = AssetManager::GetTotalTexturesToLoad();
|
||||
size_t loaded = AssetManager::GetLoadedTexturesCount();
|
||||
|
||||
if (total > 0 && loaded < total) {
|
||||
float progress = float(loaded) / float(total);
|
||||
ImGui::SameLine();
|
||||
ImGui::ProgressBar(progress, ImVec2(-1, 0));
|
||||
}
|
||||
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// ——— Polished grid view ———
|
||||
void FileBrowser::DrawGridView(const std::shared_ptr<ResourceTreeNode> &node)
|
||||
void FileBrowser::DrawBreadcrumbs_()
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
auto segs = SplitPathSegments(m_currentVPath);
|
||||
if (segs.empty()) segs.push_back("res://");
|
||||
|
||||
const float cellW = _cfg.thumbnailSize + _cfg.padding * 2;
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
int cols = std::max(1, int(avail.x / cellW));
|
||||
for (size_t i = 0; i < segs.size(); ++i) {
|
||||
if (i > 0) ImGui::SameLine();
|
||||
std::string label = (i == 0 ? "res://" : segs[i]);
|
||||
if (ImGui::Button(label.c_str())) {
|
||||
SetCurrentVPath(JoinBreadcrumb(segs, i));
|
||||
}
|
||||
if (i + 1 < segs.size()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("/");
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// Add a little padding around the child region
|
||||
ImGui::BeginChild("GridRegion",
|
||||
ImVec2(0, 0),
|
||||
false,
|
||||
ImGuiWindowFlags_AlwaysUseWindowPadding);
|
||||
static void DrawCenteredTextInRect(ImDrawList *dl, const ImVec2 &min, const ImVec2 &max, const char *text)
|
||||
{
|
||||
ImVec2 size = ImGui::CalcTextSize(text);
|
||||
ImVec2 center = ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
|
||||
ImVec2 pos = ImVec2(center.x - size.x * 0.5f, center.y - size.y * 0.5f);
|
||||
dl->AddText(pos, IM_COL32(220, 220, 220, 255), text);
|
||||
}
|
||||
|
||||
ImGui::Columns(cols, nullptr, false);
|
||||
void FileBrowser::DrawGrid_()
|
||||
{
|
||||
// Auto layout from font size (no sliders)
|
||||
const float font = ImGui::GetFontSize(); // ~16 by default
|
||||
m_thumbSize = font * 7.0f; // ~112 inner image
|
||||
m_cellPadding = std::round(font * 0.6f); // ~10
|
||||
const float spacing = std::round(font * 0.75f);
|
||||
|
||||
std::vector<std::shared_ptr<ResourceTreeNode> > children; {
|
||||
std::lock_guard<std::mutex> lock(AssetManager::s_TreeMutex);
|
||||
children = node->children;
|
||||
// Cell size
|
||||
const float cellW = m_thumbSize + m_cellPadding * 2.0f;
|
||||
const float cellH = m_thumbSize + m_cellPadding * 2.0f + font * 2.0f; // leave 1 line for file name
|
||||
|
||||
// Compute columns with a minimum of 1
|
||||
float availX = ImGui::GetContentRegionAvail().x;
|
||||
if (availX <= 0.0f) availX = 1.0f;
|
||||
int columns = (int) std::floor((availX + spacing) / (cellW + spacing));
|
||||
if (columns < 1) columns = 1;
|
||||
|
||||
// Scrollable region
|
||||
if (!ImGui::BeginChild("##fb_scroll_region", ImVec2(0, 0), false)) {
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto &c: children) {
|
||||
if (!c) continue;
|
||||
if (!_filter.empty() && !PassesFilter(c->name)) continue;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
|
||||
if (ImGui::BeginTable("##fb_grid", columns,
|
||||
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoBordersInBody)) {
|
||||
int generatedThisFrame = 0;
|
||||
|
||||
ImGui::PushID(c->path.c_str());
|
||||
ImGui::BeginGroup();
|
||||
for (size_t i = 0; i < m_entries.size(); ++i) {
|
||||
const Entry &e = m_entries[i];
|
||||
int col = (int) (i % columns);
|
||||
if (col == 0)
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(col);
|
||||
|
||||
ImVec2 btnSize(cellW, _cfg.thumbnailSize + _cfg.labelHeight + _cfg.padding);
|
||||
ImGui::InvisibleButton("cell", btnSize);
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
bool clicked = ImGui::IsItemClicked();
|
||||
ImGui::PushID((int) i);
|
||||
|
||||
// background
|
||||
ImVec2 mn = ImGui::GetItemRectMin();
|
||||
ImVec2 mx = ImGui::GetItemRectMax();
|
||||
ImU32 bg = hovered ? _cfg.highlightColor : _cfg.bgColor;
|
||||
ImGui::GetWindowDrawList()
|
||||
->AddRectFilled(mn, mx, bg, 4.0f);
|
||||
ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
|
||||
// thumbnail
|
||||
ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mn.y + _cfg.padding});
|
||||
if (c->isDirectory) {
|
||||
ImGui::Image(GetIconTexture(*c),
|
||||
{_cfg.thumbnailSize, _cfg.thumbnailSize});
|
||||
} else {
|
||||
auto asset = AssetManager::Get(c->path);
|
||||
if (asset && asset->GetTypeName() == "texture2D") {
|
||||
auto tex = std::static_pointer_cast<Texture2D>(asset);
|
||||
ImGui::Image((ImTextureID) (intptr_t) tex->GetID(),
|
||||
{_cfg.thumbnailSize, _cfg.thumbnailSize},
|
||||
{0, 1}, {1, 0});
|
||||
} else {
|
||||
ImGui::Dummy({_cfg.thumbnailSize, _cfg.thumbnailSize});
|
||||
// The InvisibleButton is the draggable “item”. Don’t submit any other ImGui widgets before the source.
|
||||
ImGui::InvisibleButton("cell", ImVec2(cellW, cellH));
|
||||
|
||||
// DRAG SOURCE: must be called right after the item you want to drag (the InvisibleButton)
|
||||
if (!e.isDirectory && e.id != 0) {
|
||||
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID
|
||||
| ImGuiDragDropFlags_SourceNoDisableHover
|
||||
| ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) {
|
||||
OX_AssetDrag payload{};
|
||||
payload.id = e.id;
|
||||
payload.type = e.type;
|
||||
|
||||
ImGui::SetDragDropPayload(OX_ASSET_DRAG_TYPE, &payload, sizeof(payload), ImGuiCond_Once);
|
||||
|
||||
// Drag preview content
|
||||
ImGui::Text("Asset #%llu", (unsigned long long) payload.id);
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Type: %s", AssetManager::AssetTypeToString(payload.type));
|
||||
ImGui::TextUnformatted(e.name.c_str());
|
||||
|
||||
if (e.type == OX_AssetType::Texture) {
|
||||
ImTextureID prev = 0;
|
||||
auto it = m_thumbCache.find(e.id);
|
||||
if (it != m_thumbCache.end()) prev = it->second;
|
||||
else prev = EnsureTextureThumb_(e.id);
|
||||
|
||||
if (prev) {
|
||||
ImGui::Separator();
|
||||
ImGui::Image(prev, ImVec2(128.0f, 128.0f));
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
}
|
||||
|
||||
// Now it’s safe to query state on the same item
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
bool held = ImGui::IsItemActive();
|
||||
bool clicked = ImGui::IsItemClicked();
|
||||
bool doubleClicked = hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
|
||||
|
||||
|
||||
ImVec2 min = start;
|
||||
ImVec2 max = ImVec2(start.x + cellW, start.y + cellH);
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// Tile background
|
||||
ImU32 bgCol = IM_COL32(36, 36, 38, 255);
|
||||
if (hovered) bgCol = IM_COL32(42, 42, 46, 255);
|
||||
dl->AddRectFilled(min, max, bgCol, 7.0f);
|
||||
|
||||
// Inner thumbnail rect
|
||||
ImVec2 innerMin(min.x + m_cellPadding, min.y + m_cellPadding);
|
||||
ImVec2 innerMax(innerMin.x + m_thumbSize, innerMin.y + m_thumbSize);
|
||||
|
||||
// If texture, draw image; else draw centered type text
|
||||
bool drewImage = false;
|
||||
if (!e.isDirectory && e.type == OX_AssetType::Texture && e.id != 0) {
|
||||
ImTextureID img = 0;
|
||||
auto it = m_thumbCache.find(e.id);
|
||||
if (it != m_thumbCache.end()) img = it->second;
|
||||
else if (generatedThisFrame < m_maxThumbsPerFrame) {
|
||||
img = EnsureTextureThumb_(e.id);
|
||||
++generatedThisFrame;
|
||||
}
|
||||
if (img) {
|
||||
dl->AddImageRounded(img, innerMin, innerMax, ImVec2(0, 0), ImVec2(1, 1),
|
||||
IM_COL32_WHITE, 6.0f /* rounding */);
|
||||
dl->AddRect(innerMin, innerMax, IM_COL32(80, 80, 86, 255), 6.0f, 0, 1.0f);
|
||||
drewImage = true;
|
||||
}
|
||||
}
|
||||
if (!drewImage) {
|
||||
dl->AddRectFilled(innerMin, innerMax, IM_COL32(50, 50, 54, 255), 5.0f);
|
||||
dl->AddRect(innerMin, innerMax, IM_COL32(80, 80, 86, 255), 5.0f);
|
||||
DrawCenteredTextInRect(dl, innerMin, innerMax,
|
||||
e.isDirectory ? "Folder" : AssetManager::AssetTypeToString(e.type));
|
||||
}
|
||||
|
||||
// File name (single line) with width-based end-ellipsis
|
||||
ImVec2 labelPos(innerMin.x, innerMax.y + m_cellPadding * 0.5f);
|
||||
ImGui::SetCursorScreenPos(labelPos);
|
||||
const float textMaxW = (cellW - m_cellPadding * 2.0f);
|
||||
|
||||
auto EllipsizeEnd = [&](const std::string &s, float maxW) -> std::string
|
||||
{
|
||||
if (s.empty()) return s;
|
||||
if (ImGui::CalcTextSize(s.c_str()).x <= maxW) return s;
|
||||
static const char *ell = "...";
|
||||
const float ellW = ImGui::CalcTextSize(ell).x;
|
||||
|
||||
// Binary trim for speed
|
||||
int lo = 0, hi = (int) s.size();
|
||||
int best = 0;
|
||||
while (lo <= hi) {
|
||||
int mid = (lo + hi) / 2;
|
||||
std::string t = s.substr(0, mid);
|
||||
float w = ImGui::CalcTextSize(t.c_str()).x + ellW;
|
||||
if (w <= maxW) {
|
||||
best = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
std::string out = s.substr(0, best);
|
||||
out.append(ell);
|
||||
return out;
|
||||
};
|
||||
|
||||
std::string displayName = EllipsizeEnd(e.name, textMaxW);
|
||||
ImGui::TextUnformatted(displayName.c_str());
|
||||
|
||||
// Selection / hover border
|
||||
ImU32 borderCol = IM_COL32(75, 75, 80, 255);
|
||||
if (m_selectedVPath == e.vpath) borderCol = IM_COL32(70, 160, 255, 255);
|
||||
if (held) borderCol = IM_COL32(120, 120, 160, 255);
|
||||
dl->AddRect(min, max, borderCol, 7.0f, 0, 1.0f);
|
||||
|
||||
// Tooltip on hover (full name, info, bigger preview for textures)
|
||||
if (hovered) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(e.name.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Type: %s", e.isDirectory ? "Folder" : AssetManager::AssetTypeToString(e.type));
|
||||
ImGui::Text("Path: %s", e.vpath.c_str());
|
||||
if (!e.isDirectory) {
|
||||
ImGui::Text("ID: %llu", (unsigned long long) e.id);
|
||||
std::string abs = AssetManager::GetAbsolutePathById(e.id);
|
||||
if (!abs.empty()) ImGui::Text("File: %s", abs.c_str());
|
||||
}
|
||||
|
||||
if (!e.isDirectory && e.type == OX_AssetType::Texture && e.id != 0) {
|
||||
ImTextureID big = 0;
|
||||
auto it2 = m_thumbCache.find(e.id);
|
||||
if (it2 != m_thumbCache.end()) big = it2->second;
|
||||
else big = EnsureTextureThumb_(e.id);
|
||||
|
||||
if (big) {
|
||||
ImGui::Separator();
|
||||
const float preview = 192.0f;
|
||||
ImGui::Image(big, ImVec2(preview, preview));
|
||||
}
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
// Click/select/double-click navigation
|
||||
if (clicked) {
|
||||
m_selectedVPath = e.vpath;
|
||||
m_selectedId = e.id;
|
||||
}
|
||||
if (doubleClicked) {
|
||||
if (e.isDirectory) {
|
||||
if (e.name == "..") {
|
||||
if (m_currentVPath != "res://") {
|
||||
auto segs = SplitPathSegments(m_currentVPath);
|
||||
if (segs.size() > 1) {
|
||||
std::string parent = "res://";
|
||||
if (segs.size() > 2) {
|
||||
for (size_t j = 1; j < segs.size() - 1; ++j) {
|
||||
parent += segs[j];
|
||||
if (j + 1 < segs.size() - 1) parent += "/";
|
||||
}
|
||||
}
|
||||
SetCurrentVPath(parent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SetCurrentVPath(e.vpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// label
|
||||
ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mx.y - _cfg.labelHeight});
|
||||
ImGui::PushTextWrapPos(mx.x);
|
||||
ImGui::TextUnformatted(c->name.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// click handling
|
||||
if (clicked) {
|
||||
if (c->isDirectory)
|
||||
_currentPath = c->path;
|
||||
else if (_onFileSelected)
|
||||
_onFileSelected(c->path);
|
||||
}
|
||||
|
||||
ImGui::EndGroup();
|
||||
ImGui::NextColumn();
|
||||
ImGui::PopID();
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::Columns(1);
|
||||
ImGui::PopStyleVar(); // ItemSpacing
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// ——— Simple list view ———
|
||||
void FileBrowser::DrawListView(const std::shared_ptr<ResourceTreeNode> &node)
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
bool FileBrowser::IsDirectoryType_(OX_AssetType t)
|
||||
{
|
||||
ImGui::BeginChild("ListRegion", ImVec2(0, 0), false);
|
||||
|
||||
for (auto &c: node->children) {
|
||||
if (!_filter.empty() && !PassesFilter(c->name)) continue;
|
||||
if (ImGui::Selectable(c->name.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
if (ImGui::IsMouseDoubleClicked(0)) {
|
||||
if (c->isDirectory)
|
||||
_currentPath = c->path;
|
||||
else if (_onFileSelected)
|
||||
_onFileSelected(c->path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
(void) t;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileBrowser::PassesFilter(const std::string &name) const
|
||||
{
|
||||
return _filter.empty() || (name.find(_filter) != std::string::npos);
|
||||
}
|
||||
|
||||
ImTextureID FileBrowser::GetIconTexture(const ResourceTreeNode &node)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
} // namespace OX
|
||||
}
|
||||
|
||||
@@ -1,72 +1,103 @@
|
||||
// File: src/FileBrowser.h
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include "systems/AssetManager.h" // for ResourceTreeNode
|
||||
#include "systems/assets/Texture2D.h" // for Texture2D
|
||||
#include "imgui.h"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include "systems/assets/Asset.h"
|
||||
#include "systems/assets/Texture2D.h"
|
||||
#include "systems/AssetManager.h" // for ResourceTreeNode, OX_AssetType, OX_AssetDrag, OX_ASSET_DRAG_TYPE
|
||||
|
||||
namespace OX
|
||||
{
|
||||
/// Configuration for look & feel
|
||||
struct FileBrowserConfig
|
||||
{
|
||||
float thumbnailSize = 64.0f;
|
||||
float padding = 8.0f;
|
||||
float labelHeight = 20.0f;
|
||||
ImU32 bgColor = IM_COL32(40, 40, 40, 255);
|
||||
ImU32 highlightColor = IM_COL32(75, 75, 75, 255);
|
||||
bool showGrid = true;
|
||||
bool showThumbnails = true;
|
||||
bool allowMultiple = false;
|
||||
};
|
||||
|
||||
class FileBrowser
|
||||
{
|
||||
public:
|
||||
using FileSelectedCallback = std::function<void(const std::string &path)>;
|
||||
|
||||
FileBrowser();
|
||||
// rootVPath like "res://"
|
||||
explicit FileBrowser(const std::string &rootVPath = "res://");
|
||||
|
||||
~FileBrowser();
|
||||
|
||||
/// Draw the entire browser window
|
||||
void Draw(const char *title = "File Browser");
|
||||
// Optional: jump to path (must be a virtual path like "res://Textures")
|
||||
void SetCurrentVPath(const std::string &vpath);
|
||||
|
||||
/// Set a filter string (wildcards, substrings, etc.)
|
||||
void SetFilter(const std::string &filter);
|
||||
// Main UI
|
||||
void Draw(const char *windowTitle = "Assets");
|
||||
|
||||
/// Called whenever the user clicks on a file (not directory)
|
||||
void SetFileSelectedCallback(FileSelectedCallback cb);
|
||||
// Selection API (simple)
|
||||
const std::string &GetSelectedVPath() const { return m_selectedVPath; }
|
||||
uint64_t GetSelectedId() const { return m_selectedId; }
|
||||
|
||||
|
||||
/// Access to tweak colors/sizes
|
||||
FileBrowserConfig &Config() { return _cfg; }
|
||||
// Grid settings
|
||||
void SetThumbSize(float px) { m_thumbSize = px; }
|
||||
void SetCellPadding(float px) { m_cellPadding = px; }
|
||||
|
||||
private:
|
||||
// helpers
|
||||
void DrawToolbar();
|
||||
// ---- Directory / tree traversal ----
|
||||
std::shared_ptr<ResourceTreeNode> SnapshotRoot_() const;
|
||||
|
||||
void DrawGridView(const std::shared_ptr<ResourceTreeNode> &node);
|
||||
std::shared_ptr<ResourceTreeNode> FindNodeByVPath_(const std::shared_ptr<ResourceTreeNode> &root,
|
||||
const std::string &vpath) const;
|
||||
|
||||
void DrawListView(const std::shared_ptr<ResourceTreeNode> &node);
|
||||
void RebuildListing_(); // build m_entries from current vpath
|
||||
|
||||
[[nodiscard]] bool PassesFilter(const std::string &name) const;
|
||||
// ---- Icons / thumbnails ----
|
||||
ImTextureID GetFolderIcon_();
|
||||
|
||||
void SetProgress(float p);
|
||||
ImTextureID GetThumbFor_(uint64_t id, OX_AssetType type, const std::string &vpath);
|
||||
|
||||
ImTextureID GetIconTexture(const ResourceTreeNode &); // stub for folder/file icons
|
||||
// Convert Texture2D* or ref to an ImGui ID (engine-specific hook point)
|
||||
ImTextureID ToImGuiID_(const std::shared_ptr<Texture2D> &tex) const;
|
||||
|
||||
// state
|
||||
FileBrowserConfig _cfg;
|
||||
std::string _currentPath = "res://";
|
||||
std::string _filter;
|
||||
bool _gridMode = true;
|
||||
FileSelectedCallback _onFileSelected;
|
||||
float _lastProgress;
|
||||
size_t _maxQueueSize;
|
||||
// Upload raw RGBA into GL texture
|
||||
ImTextureID UploadGLTextureRGBA_(int w, int h, const unsigned char *rgba);
|
||||
|
||||
// Try generate preview for a texture
|
||||
ImTextureID EnsureTextureThumb_(uint64_t id);
|
||||
|
||||
// ---- UI pieces ----
|
||||
void DrawToolbar_();
|
||||
|
||||
void DrawBreadcrumbs_();
|
||||
|
||||
void DrawGrid_();
|
||||
|
||||
// ---- Helpers ----
|
||||
static bool IsDirectoryType_(OX_AssetType t);
|
||||
|
||||
private:
|
||||
struct Entry
|
||||
{
|
||||
std::string name;
|
||||
std::string vpath;
|
||||
bool isDirectory = false;
|
||||
OX_AssetType type = OX_AssetType::Unknown;
|
||||
uint64_t id = 0; // zero for folders (not mapped by AssetManager)
|
||||
};
|
||||
|
||||
// State
|
||||
std::string m_rootVPath = "res://";
|
||||
std::string m_currentVPath = "res://";
|
||||
|
||||
std::vector<Entry> m_entries;
|
||||
|
||||
// Selection
|
||||
std::string m_selectedVPath;
|
||||
uint64_t m_selectedId = 0;
|
||||
|
||||
// Icons / thumbs
|
||||
ImTextureID m_folderIcon = 0;
|
||||
std::unordered_map<uint64_t, ImTextureID> m_thumbCache; // id -> texture
|
||||
|
||||
// UI config
|
||||
float m_thumbSize = 96.0f;
|
||||
float m_cellPadding = 12.0f;
|
||||
|
||||
// Internal throttle (build thumbs lazily)
|
||||
int m_maxThumbsPerFrame = 8;
|
||||
};
|
||||
} // namespace OX
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
#include "systems/Scene/Components/CameraComponent.h"
|
||||
#include "systems/Scene/Components/PointLightComponent.h"
|
||||
|
||||
#include "systems/AssetManager.h" // <-- for OX_AssetDrag + AssetManager::GetIdForPath
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <misc/cpp/imgui_stdlib.h> // std::string overloads for InputText
|
||||
#include <misc/cpp/imgui_stdlib.h>
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace OX
|
||||
{
|
||||
@@ -39,18 +41,15 @@ namespace OX
|
||||
ImGui::PushID(title);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 6));
|
||||
|
||||
// Draw the framed header
|
||||
const bool open = ImGui::TreeNodeEx("##comp", flags, "%s", title);
|
||||
if (openState) *openState = open;
|
||||
|
||||
// Right-click the header to open the context menu
|
||||
bool remove = false;
|
||||
if (ImGui::BeginPopupContextItem("comp_ctx")) {
|
||||
if (ImGui::MenuItem("Remove component"))
|
||||
remove = true;
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (toRemove) *toRemove = remove;
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
@@ -58,7 +57,6 @@ namespace OX
|
||||
return open;
|
||||
}
|
||||
|
||||
|
||||
bool InspectorWindow::DrawVec3Row(const char *label, float v[3],
|
||||
float reset, float speed)
|
||||
{
|
||||
@@ -70,17 +68,13 @@ namespace OX
|
||||
ImGui::TextUnformatted(label);
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
|
||||
// Unique ID scope per row to avoid X/Y/Z collisions across rows
|
||||
ImGui::PushID(label);
|
||||
|
||||
const float lineH = ImGui::GetFrameHeight();
|
||||
const ImVec2 btnSz(lineH, lineH);
|
||||
|
||||
// Inner table lays out [Btn, Drag] x 3 without SameLine() overflow
|
||||
if (ImGui::BeginTable("##vec", 6,
|
||||
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoPadInnerX)) {
|
||||
// Button columns are fixed; drag columns stretch
|
||||
ImGui::TableSetupColumn("bx", ImGuiTableColumnFlags_WidthFixed, lineH);
|
||||
ImGui::TableSetupColumn("dx", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("by", ImGuiTableColumnFlags_WidthFixed, lineH);
|
||||
@@ -144,7 +138,7 @@ namespace OX
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::PopID(); // end row id scope
|
||||
ImGui::PopID();
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -186,13 +180,12 @@ namespace OX
|
||||
if (selId == 0) return false;
|
||||
|
||||
auto &scene = core.GetScene();
|
||||
GameObject *alive = scene.get(selId); // lookup by ID only
|
||||
GameObject *alive = scene.get(selId);
|
||||
if (!alive) {
|
||||
editor.ClearSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Refresh local buffers when selection changed
|
||||
if (m_lastSelectedId != selId) {
|
||||
SyncBuffersOnSelection(alive);
|
||||
m_lastSelectedId = selId;
|
||||
@@ -200,7 +193,6 @@ namespace OX
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void InspectorWindow::SyncBuffersOnSelection(GameObject *go)
|
||||
{
|
||||
m_lastSelectedId = go ? go->id() : 0;
|
||||
@@ -209,15 +201,8 @@ namespace OX
|
||||
if (go) m_name = go->name();
|
||||
else m_name.clear();
|
||||
|
||||
// mesh path (if any)
|
||||
if (go) {
|
||||
if (auto *mc = go->getComponent<MeshComponent>())
|
||||
m_meshPath = mc->sourcePath();
|
||||
else
|
||||
m_meshPath.clear();
|
||||
} else {
|
||||
m_meshPath.clear();
|
||||
}
|
||||
// (legacy field) clear mesh path display; we no longer edit by path here
|
||||
m_meshPath.clear();
|
||||
}
|
||||
|
||||
void InspectorWindow::DrawSelectedObject(Core &core, Editor &editor)
|
||||
@@ -225,7 +210,6 @@ namespace OX
|
||||
GameObject *go = editor.GetSelected();
|
||||
if (!go) return;
|
||||
|
||||
// Name + ID row (using a 2-col table for clean alignment)
|
||||
if (ImGui::BeginTable("##go_header_tbl", 2,
|
||||
ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerV)) {
|
||||
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 90.0f);
|
||||
@@ -261,7 +245,6 @@ namespace OX
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, 6));
|
||||
|
||||
|
||||
if (go->getComponent<TransformComponent>()) {
|
||||
DrawTransformComponent(go);
|
||||
ImGui::Dummy(ImVec2(0, 4));
|
||||
@@ -281,16 +264,13 @@ namespace OX
|
||||
ImGui::Dummy(ImVec2(0, 4));
|
||||
}
|
||||
|
||||
|
||||
// Popup-only add
|
||||
DrawAddComponentMenu(go);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Components (draw ONLY if component exists)
|
||||
// Components
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
void InspectorWindow::DrawTransformComponent(GameObject *go)
|
||||
{
|
||||
bool toRemove = false;
|
||||
@@ -367,7 +347,7 @@ namespace OX
|
||||
if (ImGui::Combo("##cam_proj", &proj, items, IM_ARRAYSIZE(items)))
|
||||
cc->setProjection(static_cast<CameraComponent::Projection>(proj));
|
||||
|
||||
// FOV (persp) or Ortho Height (ortho)
|
||||
// FOV / Ortho Height
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
@@ -414,165 +394,180 @@ namespace OX
|
||||
if (toRemove) (void) go->removeComponent<CameraComponent>();
|
||||
}
|
||||
|
||||
|
||||
void InspectorWindow::DrawMeshComponent(GameObject *go)
|
||||
{
|
||||
bool openDummy = true, toRemove = false;
|
||||
if (ComponentHeader("Mesh", &openDummy, &toRemove)) {
|
||||
auto *mc = go->getComponent<MeshComponent>();
|
||||
|
||||
// Sync UI path once if empty but component has a path
|
||||
if (m_meshPath.empty() && !mc->sourcePath().empty())
|
||||
m_meshPath = mc->sourcePath();
|
||||
|
||||
if (ImGui::BeginTable("##mesh_tbl", 2,
|
||||
ImGuiTableFlags_SizingStretchProp |
|
||||
ImGuiTableFlags_BordersInnerV)) {
|
||||
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 110.0f);
|
||||
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
|
||||
|
||||
// Path
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Path");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
const bool pathCommitted =
|
||||
ImGui::InputText("##mesh_path", &m_meshPath,
|
||||
ImGuiInputTextFlags_AutoSelectAll |
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
|
||||
// Actions
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Actions");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
|
||||
bool doLoad = false;
|
||||
if (ImGui::Button("Load (Single Component)")) doLoad = true;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Import as Children (Per Submesh)")) {
|
||||
if (!m_meshPath.empty()) {
|
||||
// Creates child objects Submesh_i [...] each with its own MeshComponent
|
||||
if (!MeshComponent::ImportOBJ(go, m_meshPath, MeshImportMode::ChildrenPerSubmesh)) {
|
||||
Logger::LogError("Children-per-submesh import failed for: %s", m_meshPath.c_str());
|
||||
} else {
|
||||
// keep the parent’s MeshComponent as-is; user can remove it if desired
|
||||
}
|
||||
} else {
|
||||
Logger::LogWarning("Mesh path is empty (children import).");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reload")) {
|
||||
if (!mc->sourcePath().empty()) {
|
||||
mc->LoadOBJ(mc->sourcePath());
|
||||
m_meshPath = mc->sourcePath(); // reflect normalization
|
||||
} else {
|
||||
Logger::LogWarning("No mesh path to reload.");
|
||||
}
|
||||
}
|
||||
if (pathCommitted) doLoad = true;
|
||||
|
||||
if (doLoad) {
|
||||
if (!m_meshPath.empty()) {
|
||||
mc->LoadOBJ(m_meshPath);
|
||||
m_meshPath = mc->sourcePath(); // reflect any normalization
|
||||
} else {
|
||||
Logger::LogWarning("Mesh path is empty.");
|
||||
}
|
||||
}
|
||||
|
||||
// Stats
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Stats");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::Text("Verts: %zu Indices: %zu Submeshes: %zu Materials: %zu",
|
||||
mc->vertices().size(), mc->indices().size(),
|
||||
mc->submeshes().size(), mc->materials().size());
|
||||
|
||||
// Submesh / Material Browser
|
||||
if (!mc->submeshes().empty()) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Submeshes");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
|
||||
if (ImGui::BeginTable("##submesh_list", 5,
|
||||
ImGuiTableFlags_Borders |
|
||||
ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("i", ImGuiTableColumnFlags_WidthFixed, 30.0f);
|
||||
ImGui::TableSetupColumn("IndexOffset", ImGuiTableColumnFlags_WidthFixed, 90.0f);
|
||||
ImGui::TableSetupColumn("IndexCount", ImGuiTableColumnFlags_WidthFixed, 85.0f);
|
||||
ImGui::TableSetupColumn("Material", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, 160.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
const auto &subs = mc->submeshes();
|
||||
const auto &mats = mc->materials();
|
||||
|
||||
for (size_t i = 0; i < subs.size(); ++i) {
|
||||
const auto &sm = subs[i];
|
||||
const bool hasMat = (sm.materialIndex >= 0 && sm.materialIndex < (int) mats.size());
|
||||
const char *matName = hasMat ? mats[sm.materialIndex].name.c_str() : "(none)";
|
||||
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::Text("%zu", i);
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::Text("%u", sm.indexOffset);
|
||||
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::Text("%u", sm.indexCount);
|
||||
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::TextUnformatted(matName);
|
||||
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::PushID((int) i);
|
||||
if (ImGui::Button("Extract to Child")) {
|
||||
// Make one child GO with just this submesh
|
||||
if (!mc->sourcePath().empty()) {
|
||||
std::string childName = "Submesh_" + std::to_string(i);
|
||||
if (hasMat && mats[sm.materialIndex].name.size())
|
||||
childName += " [" + mats[sm.materialIndex].name + "]";
|
||||
GameObject *child = go->createChild(childName);
|
||||
if (child) {
|
||||
auto *childMc = child->AddComponent<MeshComponent>();
|
||||
if (!childMc->LoadOBJSubmesh(mc->sourcePath(), (int) i)) {
|
||||
Logger::LogError("Extract failed for submesh %zu", i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger::LogWarning("Cannot extract: component has no sourcePath.");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Select")) {
|
||||
// Optional: you can set current selection in your editor to this submesh,
|
||||
// or focus material inspector, etc. Placeholder hook:
|
||||
Logger::LogInfo("Selected submesh %zu", i);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
if (openDummy) ImGui::TreePop();
|
||||
bool open = true, toRemove = false;
|
||||
if (!ComponentHeader("Mesh", &open, &toRemove)) {
|
||||
if (toRemove) (void) go->removeComponent<MeshComponent>();
|
||||
return;
|
||||
}
|
||||
|
||||
auto *mc = go->getComponent<MeshComponent>();
|
||||
if (!mc) {
|
||||
if (open) ImGui::TreePop();
|
||||
if (toRemove) (void) go->removeComponent<MeshComponent>();
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve current source asset id if your MeshComponent exposes it
|
||||
uint64_t currentAssetId = 0;
|
||||
#if defined(__cpp_concepts) || (defined(_MSC_VER) && _MSC_VER >= 1920)
|
||||
if constexpr (requires(MeshComponent *c) { c->sourceAssetId(); }) {
|
||||
currentAssetId = mc->sourceAssetId();
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Current asset (simple property row) ---
|
||||
ImGui::SeparatorText("Asset"); {
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted("Model");
|
||||
ImGui::SameLine(140.0f);
|
||||
|
||||
if (currentAssetId != 0) {
|
||||
const std::string vpath = AssetManager::GetVirtualPathById(currentAssetId);
|
||||
ImGui::Text("ID: %llu", (unsigned long long) currentAssetId);
|
||||
if (!vpath.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(%s)", vpath.c_str());
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("None");
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
|
||||
// Actions (inline)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear")) {
|
||||
// Prefer a real Clear() in MeshComponent if available.
|
||||
// Here we just leave visuals; your component should own the "no mesh" state.
|
||||
currentAssetId = 0;
|
||||
// e.g. mc->Clear(); // if you have it
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (currentAssetId != 0 && ImGui::Button("Copy ID")) {
|
||||
ImGui::SetClipboardText(std::to_string(currentAssetId).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Drop zone (accepts only Model assets & resolvable ids) ---
|
||||
ImGui::SeparatorText("Assign"); {
|
||||
const float h = ImGui::GetFrameHeight() * 1.2f;
|
||||
ImVec2 size(-1.f, h);
|
||||
|
||||
// Peek the payload to highlight only if a real Model id is hovered
|
||||
bool highlight = false;
|
||||
if (const ImGuiPayload *peek = ImGui::GetDragDropPayload()) {
|
||||
if (peek->IsDataType(OX_ASSET_DRAG_TYPE) && peek->DataSize == sizeof(OX_AssetDrag)) {
|
||||
const auto *p = static_cast<const OX_AssetDrag *>(peek->Data);
|
||||
if (p && p->id != 0) {
|
||||
OX_AssetType t = (p->type == OX_AssetType::Unknown)
|
||||
? AssetManager::GetAssetTypeById(p->id)
|
||||
: p->type;
|
||||
const std::string abs = AssetManager::GetAbsolutePathById(p->id);
|
||||
highlight = (t == OX_AssetType::Model && !abs.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highlight) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.30f, 0.47f, 1.0f));
|
||||
ImGui::Button("Drag & drop a Model asset here", size);
|
||||
if (highlight) ImGui::PopStyleColor();
|
||||
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload *payload = ImGui::AcceptDragDropPayload(
|
||||
OX_ASSET_DRAG_TYPE, ImGuiDragDropFlags_AcceptBeforeDelivery)) {
|
||||
if (payload && payload->DataSize == sizeof(OX_AssetDrag)) {
|
||||
const auto *drag = static_cast<const OX_AssetDrag *>(payload->Data);
|
||||
if (drag && drag->id != 0) {
|
||||
const uint64_t id = drag->id;
|
||||
const OX_AssetType type = AssetManager::GetAssetTypeById(id);
|
||||
const std::string abspath = AssetManager::GetAbsolutePathById(id);
|
||||
|
||||
if (type != OX_AssetType::Model) {
|
||||
Logger::LogWarning("Dropped asset %" PRIu64 " is not a Model.", id);
|
||||
} else if (abspath.empty()) {
|
||||
Logger::LogError("Dropped model id=%" PRIu64 " has no absolute path.", id);
|
||||
} else {
|
||||
if (!mc->LoadModel(id)) {
|
||||
Logger::LogError("Failed to load model asset %" PRIu64, id);
|
||||
} else {
|
||||
currentAssetId = id;
|
||||
// If MeshComponent tracks the source id internally, update it there.
|
||||
// e.g. mc->setSourceAssetId(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Stats ---
|
||||
ImGui::SeparatorText("Stats");
|
||||
ImGui::Text("Verts: %zu Indices: %zu Submeshes: %zu Materials: %zu",
|
||||
mc->vertices().size(), mc->indices().size(),
|
||||
mc->submeshes().size(), mc->materials().size());
|
||||
|
||||
// --- Submeshes (simple table, collapsible) ---
|
||||
if (!mc->submeshes().empty() && ImGui::CollapsingHeader("Submeshes", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
if (ImGui::BeginTable("##submesh_list", 5,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("#", ImGuiTableColumnFlags_WidthFixed, 30.0f);
|
||||
ImGui::TableSetupColumn("IndexOffset", ImGuiTableColumnFlags_WidthFixed, 90.0f);
|
||||
ImGui::TableSetupColumn("IndexCount", ImGuiTableColumnFlags_WidthFixed, 85.0f);
|
||||
ImGui::TableSetupColumn("Material", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, 150.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
const auto &subs = mc->submeshes();
|
||||
const auto &mats = mc->materials();
|
||||
|
||||
for (size_t i = 0; i < subs.size(); ++i) {
|
||||
const auto &sm = subs[i];
|
||||
const bool hasMat = (sm.materialIndex >= 0 && sm.materialIndex < (int) mats.size());
|
||||
const char *matName = hasMat ? mats[sm.materialIndex].name.c_str() : "(none)";
|
||||
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::Text("%zu", i);
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::Text("%u", sm.indexOffset);
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::Text("%u", sm.indexCount);
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::TextUnformatted(matName);
|
||||
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::PushID((int) i);
|
||||
if (ImGui::Button("Extract")) {
|
||||
if (currentAssetId != 0) {
|
||||
std::string childName = "Submesh_" + std::to_string(i);
|
||||
if (hasMat && mats[sm.materialIndex].name.size())
|
||||
childName += " [" + mats[sm.materialIndex].name + "]";
|
||||
if (GameObject *child = go->createChild(childName)) {
|
||||
if (auto *childMc = child->AddComponent<MeshComponent>();
|
||||
!childMc->LoadModelSubmesh(currentAssetId, (int) i)) {
|
||||
Logger::LogError("Extract failed for submesh %zu", i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger::LogWarning("Cannot extract: no model asset bound.");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Select")) {
|
||||
Logger::LogInfo("Selected submesh %zu", i);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
if (open) ImGui::TreePop();
|
||||
if (toRemove) {
|
||||
(void) go->removeComponent<MeshComponent>();
|
||||
m_meshPath.clear();
|
||||
@@ -631,7 +626,7 @@ namespace OX
|
||||
if (ImGui::DragFloat("##pl_range", &R, 0.05f, 0.01f, 100000.0f))
|
||||
pl->setRange(std::max(0.01f, R));
|
||||
|
||||
// Shadows (reserved)
|
||||
// Shadows
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
@@ -649,7 +644,6 @@ namespace OX
|
||||
if (toRemove) (void) go->removeComponent<PointLightComponent>();
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Popup-only add
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -664,7 +658,6 @@ namespace OX
|
||||
const bool hasCam = go->getComponent<CameraComponent>() != nullptr;
|
||||
const bool hasPoint = go->getComponent<PointLightComponent>() != nullptr;
|
||||
|
||||
|
||||
if (ImGui::MenuItem("TransformComponent", nullptr, false, !hasTr)) {
|
||||
go->addComponent(std::make_unique<TransformComponent>());
|
||||
ImGui::CloseCurrentPopup();
|
||||
|
||||
Reference in New Issue
Block a user