Merge branch 'LAPTOP-DEV'
This commit is contained in:
@@ -128,6 +128,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)
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
#include "Core.h"
|
||||
|
||||
#include <ranges>
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
#include "../editor/Editor.h"
|
||||
#include "systems/MACROS.h"
|
||||
#include "systems/AssetManager.h"
|
||||
#include "systems/assets/Texture2D.h"
|
||||
#include "systems/Scene/Components/PointLightComponent.h"
|
||||
#include "systems/Scene/Components/TransformComponent.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
@@ -46,7 +50,7 @@ namespace OX
|
||||
|
||||
Logger::LogOk("Core Initialization Complete.");
|
||||
|
||||
//AssetManager::Init("C:/Users/spenc/OneDrive/Desktop/OnyxProject");
|
||||
AssetManager::Init("C:/Users/spenc/OneDrive/Desktop/Test_project");
|
||||
}
|
||||
|
||||
|
||||
@@ -83,11 +87,82 @@ namespace OX
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
|
||||
if (renderer.Begin(GetScene())) {
|
||||
// helper: draw a single circle (ring) in world space on a fixed plane
|
||||
auto drawCirclePlane = [this](const glm::vec3 &C, float R, const glm::vec3 &rgb,
|
||||
char plane = 'Z', int segments = 64, float thickness = 2.0f)
|
||||
{
|
||||
if (R <= 0.0f) return;
|
||||
const float twoPi = glm::two_pi<float>();
|
||||
const float step = twoPi / std::max(3, segments);
|
||||
|
||||
auto ringPoint = [&](float t)-> glm::vec3
|
||||
{
|
||||
const float ct = std::cos(t), st = std::sin(t);
|
||||
switch (plane) {
|
||||
case 'x':
|
||||
case 'X': // YZ plane
|
||||
return C + glm::vec3(0.0f, R * ct, R * st);
|
||||
case 'y':
|
||||
case 'Y': // XZ plane
|
||||
return C + glm::vec3(R * ct, 0.0f, R * st);
|
||||
default: // XY plane
|
||||
return C + glm::vec3(R * ct, R * st, 0.0f);
|
||||
}
|
||||
};
|
||||
|
||||
glm::vec3 p0 = ringPoint(0.0f);
|
||||
for (int i = 1; i <= segments; ++i) {
|
||||
float t1 = i * step;
|
||||
glm::vec3 p1 = ringPoint(t1);
|
||||
renderer.DrawLine3D(p0, p1, rgb, thickness);
|
||||
p0 = p1;
|
||||
}
|
||||
};
|
||||
|
||||
if (renderer.Begin(*this)) {
|
||||
// 1) shaded scene
|
||||
renderer.RenderSceneShaded(GetScene());
|
||||
|
||||
// 2) selected light gizmo: full sphere + cross lines
|
||||
if (auto *sel = GetEditor() ? GetEditor()->GetSelected() : nullptr) {
|
||||
if (auto *tr = sel->getComponent<TransformComponent>()) {
|
||||
if (auto *lc = sel->getComponent<PointLightComponent>()) {
|
||||
if (lc->enabled() && lc->range() > 0.0f) {
|
||||
// world center from transform
|
||||
const auto p = tr->getPosition();
|
||||
const glm::vec3 C(p[0], p[1], p[2]);
|
||||
|
||||
// color & radii
|
||||
const glm::vec3 color = lc->color(); // assumed 0..1
|
||||
const float R = lc->range();
|
||||
|
||||
// Full sphere: three great circles (XY, XZ, YZ)
|
||||
drawCirclePlane(C, R, color, 'Z', 64, 2.0f); // XY plane
|
||||
drawCirclePlane(C, R, color, 'X', 64, 2.0f); // YZ plane
|
||||
drawCirclePlane(C, R, color, 'Y', 64, 2.0f); // XZ plane
|
||||
|
||||
|
||||
// Cross lines through center along X/Y/Z axes — sized to hit the sphere
|
||||
const float thick = 2.0f;
|
||||
// tiny shrink to avoid precision artifacts when matching circle endpoints (optional)
|
||||
const float eps = 1e-4f;
|
||||
const float L = glm::max(R - eps, 0.0f);
|
||||
|
||||
// X axis: intersects the YZ ring
|
||||
renderer.DrawLine3D(C - glm::vec3(L, 0, 0), C + glm::vec3(L, 0, 0), color, thick);
|
||||
// Y axis: intersects the XZ ring
|
||||
renderer.DrawLine3D(C - glm::vec3(0, L, 0), C + glm::vec3(0, L, 0), color, thick);
|
||||
// Z axis: intersects the XY ring
|
||||
renderer.DrawLine3D(C - glm::vec3(0, 0, L), C + glm::vec3(0, 0, L), color, thick);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
renderer.End();
|
||||
|
||||
|
||||
// 3) UI layers after 3D
|
||||
for (auto &layer: m_layers)
|
||||
layer->Draw(*this);
|
||||
}
|
||||
@@ -113,4 +188,55 @@ namespace OX
|
||||
|
||||
Logger::LogOk("Core Shutdown Complete.");
|
||||
}
|
||||
|
||||
|
||||
int Core::GetIndexByName(const std::string &name) const
|
||||
{
|
||||
for (size_t i = 0; i < m_layers.size(); ++i) {
|
||||
if (m_layers[i]->GetName() == name)
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return -1; // Not found
|
||||
}
|
||||
|
||||
Layer *Core::GetLayerByIndex(size_t index)
|
||||
{
|
||||
if (index >= m_layers.size())
|
||||
return nullptr;
|
||||
return m_layers[index].get();
|
||||
}
|
||||
|
||||
const Layer *Core::GetLayerByIndex(size_t index) const
|
||||
{
|
||||
if (index >= m_layers.size())
|
||||
return nullptr;
|
||||
return m_layers[index].get();
|
||||
}
|
||||
|
||||
Editor *Core::GetEditor()
|
||||
{
|
||||
// Quick path: return cached editor if we already found it
|
||||
if (m_cachedEditor)
|
||||
return m_cachedEditor;
|
||||
|
||||
// Search all layers for one that's actually an Editor
|
||||
for (auto &layer: m_layers) {
|
||||
if (auto *editor = dynamic_cast<Editor *>(layer.get())) {
|
||||
m_cachedEditor = editor; // Cache it for future calls
|
||||
return m_cachedEditor;
|
||||
}
|
||||
}
|
||||
|
||||
// If no editor layer exists, return nullptr
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Convenience proxy: returns the currently selected GameObject.
|
||||
GameObject *Core::GetSelected()
|
||||
{
|
||||
if (auto *editor = GetEditor())
|
||||
return editor->GetSelected();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
#define CORE_H
|
||||
#define OX_ENGINE_VERSION "Onyx Engine (2025.1)"
|
||||
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Layer.h"
|
||||
#include "renderer/Renderer.h"
|
||||
#include "systems/Logger.h"
|
||||
@@ -18,19 +19,22 @@
|
||||
#include "systems/MACROS.h"
|
||||
#include "systems/Scene/Scene.h"
|
||||
|
||||
|
||||
namespace OX
|
||||
{
|
||||
// fwd decls to avoid heavy includes here
|
||||
class Editor; // your editor layer type (derives from Layer)
|
||||
class GameObject; // scene object
|
||||
|
||||
class Core
|
||||
{
|
||||
public:
|
||||
Core(std::string name) : m_name(std::move(name))
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
Core() : m_name(OX_ENGINE_VERSION)
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
~Core() = default;
|
||||
|
||||
@@ -40,27 +44,41 @@ namespace OX
|
||||
|
||||
void Shutdown();
|
||||
|
||||
|
||||
WindowManager &GetWindow() { return window; }
|
||||
Renderer &GetRenderer() { return renderer; }
|
||||
Scene &GetScene() { return m_activeScene; }
|
||||
|
||||
// Layers access
|
||||
std::vector<std::unique_ptr<Layer> > &GetLayers() { return m_layers; }
|
||||
const std::vector<std::unique_ptr<Layer> > &GetLayers() const { return m_layers; }
|
||||
|
||||
int GetIndexByName(const std::string &name) const;
|
||||
|
||||
Layer *GetLayerByIndex(size_t index);
|
||||
|
||||
const Layer *GetLayerByIndex(size_t index) const;
|
||||
|
||||
void AddLayer(std::unique_ptr<Layer> layer);
|
||||
|
||||
// --- Editor helpers (no selection in Core; we just find/cache the editor) ---
|
||||
Editor *GetEditor(); // returns nullptr if no editor layer present
|
||||
GameObject *GetSelected(); // convenience proxy; nullptr if no selection/editor
|
||||
|
||||
private:
|
||||
void Update();
|
||||
|
||||
void Draw();
|
||||
|
||||
|
||||
std::vector<std::unique_ptr<Layer> > m_layers;
|
||||
WindowManager window;
|
||||
Renderer renderer;
|
||||
|
||||
Scene m_activeScene;
|
||||
|
||||
bool m_running = false;
|
||||
std::string m_name = "Application";
|
||||
|
||||
// cached pointer to editor layer for quick lookup (non-owning)
|
||||
Editor *m_cachedEditor = nullptr;
|
||||
};
|
||||
|
||||
class DummyLayer : public Layer
|
||||
@@ -90,4 +108,5 @@ namespace OX
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif // CORE_H
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
#include "renderer/Renderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
#define GLM_ENABLE_EXPERIMENTAL
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/compatibility.hpp>
|
||||
#include <GL/glew.h>
|
||||
|
||||
|
||||
#include "Core.h"
|
||||
#include "systems/Scene/Scene.h"
|
||||
#include "systems/Scene/GameObject.h"
|
||||
#include "systems/Scene/Components/TransformComponent.h"
|
||||
@@ -16,14 +18,131 @@
|
||||
#include "systems/Scene/Components/CameraComponent.h"
|
||||
#include "systems/Scene/Components/PointLightComponent.h"
|
||||
|
||||
#ifndef OX_PROFILE_FUNCTION
|
||||
#define OX_PROFILE_FUNCTION()
|
||||
#endif
|
||||
|
||||
namespace OX
|
||||
{
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Small helpers
|
||||
// ===== inline shader (unchanged) =================================================
|
||||
class Renderer::_InlineShader
|
||||
{
|
||||
public:
|
||||
_InlineShader() = default;
|
||||
|
||||
// Expect vertex layout: pos(3) + normal(3) tightly packed. Avoids field names.
|
||||
~_InlineShader();
|
||||
|
||||
void SetName(const char *n);
|
||||
|
||||
void LoadFromSource(const char *vs, const char *fs);
|
||||
|
||||
void Use() const;
|
||||
|
||||
unsigned GetID() const;
|
||||
|
||||
void SetMat4(const char *name, const glm::mat4 &m) const;
|
||||
|
||||
void SetMat3(const char *name, const glm::mat3 &m) const;
|
||||
|
||||
void SetVec3(const char *name, const glm::vec3 &v) const;
|
||||
|
||||
private:
|
||||
unsigned m_prog = 0;
|
||||
};
|
||||
|
||||
static unsigned _Compile(GLenum type, const char *src, const char *dbgName)
|
||||
{
|
||||
unsigned s = glCreateShader(type);
|
||||
glShaderSource(s, 1, &src, nullptr);
|
||||
glCompileShader(s);
|
||||
GLint ok = GL_FALSE;
|
||||
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
GLint len = 0;
|
||||
glGetShaderiv(s, GL_INFO_LOG_LENGTH, &len);
|
||||
std::string log(len, '\0');
|
||||
glGetShaderInfoLog(s, len, nullptr, log.data());
|
||||
fprintf(stderr, "[Renderer Shader] %s compile error:\n%s\n", dbgName, log.c_str());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static unsigned _Link(unsigned vs, unsigned fs)
|
||||
{
|
||||
unsigned p = glCreateProgram();
|
||||
glAttachShader(p, vs);
|
||||
glAttachShader(p, fs);
|
||||
glLinkProgram(p);
|
||||
GLint ok = GL_FALSE;
|
||||
glGetProgramiv(p, GL_LINK_STATUS, &ok);
|
||||
if (!ok) {
|
||||
GLint len = 0;
|
||||
glGetProgramiv(p, GL_INFO_LOG_LENGTH, &len);
|
||||
std::string log(len, '\0');
|
||||
glGetProgramInfoLog(p, len, nullptr, log.data());
|
||||
fprintf(stderr, "[Renderer Shader] link error:\n%s\n", log.c_str());
|
||||
}
|
||||
glDetachShader(p, vs);
|
||||
glDetachShader(p, fs);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
return p;
|
||||
}
|
||||
|
||||
Renderer::_InlineShader::~_InlineShader()
|
||||
{
|
||||
if (m_prog)
|
||||
glDeleteProgram(m_prog);
|
||||
}
|
||||
|
||||
void Renderer::_InlineShader::SetName(const char *)
|
||||
{
|
||||
}
|
||||
|
||||
void Renderer::_InlineShader::LoadFromSource(const char *vsSrc, const char *fsSrc)
|
||||
{
|
||||
unsigned vs = _Compile(GL_VERTEX_SHADER, vsSrc, "VS");
|
||||
unsigned fs = _Compile(GL_FRAGMENT_SHADER, fsSrc, "FS");
|
||||
m_prog = _Link(vs, fs);
|
||||
}
|
||||
|
||||
void Renderer::_InlineShader::Use() const
|
||||
{
|
||||
glUseProgram(m_prog);
|
||||
}
|
||||
|
||||
unsigned Renderer::_InlineShader::GetID() const { return m_prog; }
|
||||
|
||||
void Renderer::_InlineShader::SetMat4(const char *name, const glm::mat4 &m) const
|
||||
{
|
||||
GLint loc = glGetUniformLocation(m_prog, name);
|
||||
if (loc >= 0)
|
||||
glUniformMatrix4fv(loc, 1, GL_FALSE, &m[0][0]);
|
||||
}
|
||||
|
||||
void Renderer::_InlineShader::SetMat3(const char *name, const glm::mat3 &m) const
|
||||
{
|
||||
GLint loc = glGetUniformLocation(m_prog, name);
|
||||
if (loc >= 0)
|
||||
glUniformMatrix3fv(loc, 1, GL_FALSE, &m[0][0]);
|
||||
}
|
||||
|
||||
void Renderer::_InlineShader::SetVec3(const char *name, const glm::vec3 &v) const
|
||||
{
|
||||
GLint loc = glGetUniformLocation(m_prog, name);
|
||||
if (loc >= 0)
|
||||
glUniform3f(loc, v.x, v.y, v.z);
|
||||
}
|
||||
|
||||
Shader &Renderer::m_meshShaderRef()
|
||||
{
|
||||
if (!m_meshShaderStorage) m_meshShaderStorage = new _InlineShader();
|
||||
return *reinterpret_cast<Shader *>(m_meshShaderStorage);
|
||||
}
|
||||
|
||||
// ===== helpers / fallbacks =======================================================
|
||||
template<class V>
|
||||
inline void ExtractPN(const V &v, glm::vec3 &p, glm::vec3 &n)
|
||||
static inline void ExtractPN(const V &v, glm::vec3 &p, glm::vec3 &n)
|
||||
{
|
||||
const float *f = reinterpret_cast<const float *>(&v);
|
||||
p = glm::vec3(f[0], f[1], f[2]);
|
||||
@@ -31,9 +150,37 @@ namespace OX
|
||||
if (!glm::all(glm::isfinite(n))) n = glm::vec3(0, 1, 0);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Lifecycle
|
||||
static GLuint g_whiteTex = 0, g_blackTex = 0, g_normalFlatTex = 0;
|
||||
|
||||
static GLuint MakeSolidTex(unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255)
|
||||
{
|
||||
GLuint t = 0;
|
||||
glGenTextures(1, &t);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
unsigned char px[4] = {r, g, b, a};
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, px);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return t;
|
||||
}
|
||||
|
||||
static void EnsureFallbackTextures()
|
||||
{
|
||||
if (!g_whiteTex) g_whiteTex = MakeSolidTex(255, 255, 255, 255);
|
||||
if (!g_blackTex) g_blackTex = MakeSolidTex(0, 0, 0, 255);
|
||||
if (!g_normalFlatTex) g_normalFlatTex = MakeSolidTex(128, 128, 255, 255);
|
||||
}
|
||||
|
||||
// camera / scene helpers (unchanged)
|
||||
static void FindPrimaryCamera(const Scene &, const GameObject *&, const CameraComponent *&);
|
||||
|
||||
static bool ComputeSceneWorldAABB(const Scene &, glm::vec3 &, glm::vec3 &,
|
||||
glm::mat4 (*)(const TransformComponent *));
|
||||
|
||||
static void BuildFramingCameraFromAABB(const glm::vec3 &, const glm::vec3 &, float, glm::mat4 &, glm::mat4 &);
|
||||
|
||||
// ===== lifecycle ================================================================
|
||||
Renderer::~Renderer()
|
||||
{
|
||||
if (m_meshEBO)
|
||||
@@ -43,28 +190,40 @@ namespace OX
|
||||
if (m_meshVAO)
|
||||
glDeleteVertexArrays(1, &m_meshVAO);
|
||||
|
||||
if (m_lineVBO)
|
||||
glDeleteBuffers(1, &m_lineVBO);
|
||||
if (m_lineVAO)
|
||||
glDeleteVertexArrays(1, &m_lineVAO);
|
||||
|
||||
if (m_depthRBO)
|
||||
glDeleteRenderbuffers(1, &m_depthRBO);
|
||||
if (m_colorTex) glDeleteTextures(1, &m_colorTex);
|
||||
if (m_fbo)
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
|
||||
delete m_meshShaderStorage;
|
||||
m_meshShaderStorage = nullptr;
|
||||
delete m_lineShaderStorage;
|
||||
m_lineShaderStorage = nullptr;
|
||||
}
|
||||
|
||||
void Renderer::Init(int targetWidth, int targetHeight)
|
||||
{
|
||||
CreateFramebuffer(targetWidth, targetHeight);
|
||||
EnsureFallbackTextures();
|
||||
EnsureMeshPipeline();
|
||||
EnsureLinePipeline();
|
||||
}
|
||||
|
||||
void Renderer::ResizeTarget(int width, int height)
|
||||
void Renderer::ResizeTarget(int w, int h)
|
||||
{
|
||||
if (m_size.x == width && m_size.y == height) return;
|
||||
CreateFramebuffer(width, height);
|
||||
if (m_size.x == w && m_size.y == h) return;
|
||||
CreateFramebuffer(w, h);
|
||||
}
|
||||
|
||||
void Renderer::CreateFramebuffer(int width, int height)
|
||||
void Renderer::CreateFramebuffer(int w, int h)
|
||||
{
|
||||
m_size = {width, height};
|
||||
m_size = {w, h};
|
||||
|
||||
if (m_depthRBO)
|
||||
glDeleteRenderbuffers(1, &m_depthRBO);
|
||||
@@ -74,13 +233,13 @@ namespace OX
|
||||
|
||||
glGenTextures(1, &m_colorTex);
|
||||
glBindTexture(GL_TEXTURE_2D, m_colorTex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
glGenRenderbuffers(1, &m_depthRBO);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_depthRBO);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h);
|
||||
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
@@ -89,16 +248,11 @@ namespace OX
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Scene traversal / camera lookup
|
||||
|
||||
static void FindPrimaryCamera(const Scene &scene,
|
||||
const GameObject *&camObj,
|
||||
const CameraComponent *&cam)
|
||||
// ===== camera lookup / framing ==================================================
|
||||
static void FindPrimaryCamera(const Scene &scene, const GameObject *&camObj, const CameraComponent *&cam)
|
||||
{
|
||||
camObj = nullptr;
|
||||
cam = nullptr;
|
||||
|
||||
auto tryFind = [&](bool requirePrimary)
|
||||
{
|
||||
const auto &roots = scene.roots();
|
||||
@@ -107,7 +261,6 @@ namespace OX
|
||||
while (!stack.empty()) {
|
||||
GameObject *n = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
if (auto *c = n->getComponent<CameraComponent>()) {
|
||||
if (!requirePrimary || c->isPrimary()) {
|
||||
camObj = n;
|
||||
@@ -115,19 +268,16 @@ namespace OX
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const auto &cup: n->children())
|
||||
stack.push_back(cup.get());
|
||||
for (const auto &cup: n->children()) stack.push_back(cup.get());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (tryFind(true)) return;
|
||||
(void) tryFind(false);
|
||||
}
|
||||
|
||||
static bool ComputeSceneWorldAABB(const Scene &scene,
|
||||
glm::vec3 &outMin, glm::vec3 &outMax,
|
||||
static bool ComputeSceneWorldAABB(const Scene &scene, glm::vec3 &outMin, glm::vec3 &outMax,
|
||||
glm::mat4 (*BuildModel)(const TransformComponent *))
|
||||
{
|
||||
bool any = false;
|
||||
@@ -158,11 +308,9 @@ namespace OX
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &cup: go->children())
|
||||
stack.push_back(cup.get());
|
||||
for (const auto &cup: go->children()) stack.push_back(cup.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (!any) return false;
|
||||
outMin = wsMin;
|
||||
outMax = wsMax;
|
||||
@@ -194,11 +342,11 @@ namespace OX
|
||||
outP = glm::perspective(fovY, std::max(0.001f, aspect), nearZ, farZ);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Begin / End
|
||||
|
||||
bool Renderer::Begin(const Scene &scene)
|
||||
// ===== Begin / End ==============================================================
|
||||
bool Renderer::Begin(Core &core)
|
||||
{
|
||||
const Scene &scene = core.GetScene();
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, m_size.x, m_size.y);
|
||||
|
||||
@@ -235,7 +383,6 @@ namespace OX
|
||||
}
|
||||
|
||||
m_viewProj = P * V;
|
||||
// Camera world position from inverse view
|
||||
glm::mat4 invV = glm::inverse(V);
|
||||
m_cameraPosWS = glm::vec3(invV[3]);
|
||||
|
||||
@@ -244,10 +391,10 @@ namespace OX
|
||||
glClearColor(0.10f, 0.10f, 0.11f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Gather lights for this frame (after we know camera)
|
||||
GatherPointLights(scene);
|
||||
|
||||
EnsureMeshPipeline();
|
||||
EnsureLinePipeline();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -256,12 +403,9 @@ namespace OX
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Draw list + render
|
||||
|
||||
// ===== Draw list + shaded render ===============================================
|
||||
void Renderer::CollectDrawList(const Scene &scene, std::vector<DrawItem> &out, glm::mat4 &outView) const
|
||||
{
|
||||
// (Optional) compute a view matrix to sort by view-space depth if a camera exists
|
||||
const GameObject *camObj = nullptr;
|
||||
const CameraComponent *cam = nullptr;
|
||||
FindPrimaryCamera(scene, camObj, cam);
|
||||
@@ -287,33 +431,38 @@ namespace OX
|
||||
out.push_back(DrawItem{go, tr, mesh, vs.z});
|
||||
}
|
||||
|
||||
for (const auto &cup: go->children())
|
||||
stack.push_back(cup.get());
|
||||
for (const auto &cup: go->children()) stack.push_back(cup.get());
|
||||
}
|
||||
}
|
||||
|
||||
std::stable_sort(out.begin(), out.end(),
|
||||
[](const DrawItem &a, const DrawItem &b) { return a.depth < b.depth; });
|
||||
}
|
||||
|
||||
void Renderer::RenderSceneShaded(const Scene &scene)
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
std::vector<DrawItem> list;
|
||||
list.reserve(256);
|
||||
glm::mat4 V;
|
||||
CollectDrawList(scene, list, V);
|
||||
|
||||
// Set lighting uniforms once per frame
|
||||
m_meshShader.Use();
|
||||
auto &sh = m_meshShaderRef();
|
||||
reinterpret_cast<_InlineShader &>(sh).Use();
|
||||
|
||||
if (auto loc = glGetUniformLocation(reinterpret_cast<_InlineShader &>(sh).GetID(), "u_DiffuseTex"); loc >= 0)
|
||||
glUniform1i(loc, 0);
|
||||
if (auto loc = glGetUniformLocation(reinterpret_cast<_InlineShader &>(sh).GetID(), "u_SpecTex"); loc >= 0)
|
||||
glUniform1i(loc, 1);
|
||||
if (auto loc = glGetUniformLocation(reinterpret_cast<_InlineShader &>(sh).GetID(), "u_NormalTex"); loc >= 0)
|
||||
glUniform1i(loc, 2);
|
||||
|
||||
UploadFrameLightsUniforms();
|
||||
|
||||
for (const DrawItem &di: list)
|
||||
DrawMeshLambert(di, m_viewProj);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Math helpers
|
||||
|
||||
// ===== math helpers =============================================================
|
||||
glm::mat4 Renderer::BuildModelMatrix(const TransformComponent *tr)
|
||||
{
|
||||
auto p = tr->getPosition();
|
||||
@@ -345,13 +494,11 @@ namespace OX
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Lights: gather + upload
|
||||
|
||||
// ===== lights ===================================================================
|
||||
void Renderer::GatherPointLights(const Scene &scene)
|
||||
{
|
||||
m_frameLights.clear();
|
||||
constexpr int MAX_LIGHTS = 32; // keep below typical uniform limits
|
||||
constexpr int MAX_LIGHTS = 32;
|
||||
|
||||
const auto &roots = scene.roots();
|
||||
for (const auto &rup: roots) {
|
||||
@@ -370,13 +517,12 @@ namespace OX
|
||||
L.color = lc->color();
|
||||
L.intensity = lc->intensity();
|
||||
m_frameLights.push_back(L);
|
||||
if ((int) m_frameLights.size() >= MAX_LIGHTS) goto done; // stop early
|
||||
if ((int) m_frameLights.size() >= MAX_LIGHTS) goto done;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &cup: go->children())
|
||||
stack.push_back(cup.get());
|
||||
for (const auto &cup: go->children()) stack.push_back(cup.get());
|
||||
}
|
||||
}
|
||||
done:;
|
||||
@@ -384,14 +530,12 @@ namespace OX
|
||||
|
||||
void Renderer::UploadFrameLightsUniforms()
|
||||
{
|
||||
// Program already bound by m_meshShader.Use()
|
||||
GLint prog = 0;
|
||||
glGetIntegerv(GL_CURRENT_PROGRAM, &prog);
|
||||
if (prog == 0) return;
|
||||
|
||||
const int count = (int) m_frameLights.size();
|
||||
|
||||
// Scalar uniforms
|
||||
if (auto loc = glGetUniformLocation(prog, "u_LightCount"); loc >= 0)
|
||||
glUniform1i(loc, count);
|
||||
if (auto loc = glGetUniformLocation(prog, "u_Ambient"); loc >= 0)
|
||||
@@ -405,7 +549,6 @@ namespace OX
|
||||
|
||||
if (count == 0) return;
|
||||
|
||||
// Pack arrays
|
||||
std::vector<glm::vec3> pos(count), col(count);
|
||||
std::vector<float> range(count), intensity(count);
|
||||
|
||||
@@ -416,7 +559,6 @@ namespace OX
|
||||
intensity[i] = m_frameLights[i].intensity;
|
||||
}
|
||||
|
||||
// Upload arrays (query [0] base)
|
||||
if (auto loc = glGetUniformLocation(prog, "u_LightPos[0]"); loc >= 0)
|
||||
glUniform3fv(loc, count, &pos[0].x);
|
||||
if (auto loc = glGetUniformLocation(prog, "u_LightColor[0]"); loc >= 0)
|
||||
@@ -427,29 +569,38 @@ namespace OX
|
||||
glUniform1fv(loc, count, intensity.data());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Mesh pipeline
|
||||
|
||||
// ===== mesh pipeline (unchanged) ===============================================
|
||||
void Renderer::EnsureMeshPipeline()
|
||||
{
|
||||
if (!m_meshShaderReady) {
|
||||
// MAX_LIGHTS must match UploadFrameLightsUniforms() assumptions
|
||||
const char *vs = R"GLSL(
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPos;
|
||||
layout(location=1) in vec3 aNormal;
|
||||
layout(location=2) in vec2 aUV;
|
||||
layout(location=3) in vec3 aTangent;
|
||||
layout(location=4) in vec3 aBitangent;
|
||||
|
||||
uniform mat4 u_M;
|
||||
uniform mat4 u_VP;
|
||||
uniform mat3 u_N;
|
||||
|
||||
out vec3 vNormalWS;
|
||||
out vec3 vPosWS;
|
||||
out vec2 vUV;
|
||||
out mat3 vTBN;
|
||||
|
||||
void main(){
|
||||
vec3 T = normalize(u_N * aTangent);
|
||||
vec3 B = normalize(u_N * aBitangent);
|
||||
vec3 N = normalize(u_N * aNormal);
|
||||
T = normalize(T - dot(T, N) * N);
|
||||
B = normalize(cross(N, T));
|
||||
|
||||
vTBN = mat3(T, B, N);
|
||||
vUV = aUV;
|
||||
|
||||
vec4 ws = u_M * vec4(aPos, 1.0);
|
||||
vPosWS = ws.xyz;
|
||||
vNormalWS = normalize(u_N * aNormal);
|
||||
gl_Position = u_VP * ws;
|
||||
}
|
||||
)GLSL";
|
||||
@@ -458,44 +609,58 @@ namespace OX
|
||||
#version 330 core
|
||||
#define MAX_LIGHTS 32
|
||||
|
||||
in vec3 vNormalWS;
|
||||
in vec3 vPosWS;
|
||||
in vec2 vUV;
|
||||
in mat3 vTBN;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
// Per-object
|
||||
uniform vec3 u_BaseColor;
|
||||
uniform sampler2D u_DiffuseTex;
|
||||
uniform sampler2D u_SpecTex;
|
||||
uniform sampler2D u_NormalTex;
|
||||
uniform bool u_HasDiffuseTex;
|
||||
uniform bool u_HasSpecTex;
|
||||
uniform bool u_HasNormalTex;
|
||||
|
||||
uniform vec3 u_BaseColor;
|
||||
uniform vec3 u_SpecColor;
|
||||
uniform float u_Shininess;
|
||||
|
||||
// Per-frame
|
||||
uniform int u_LightCount;
|
||||
uniform vec3 u_LightPos[MAX_LIGHTS];
|
||||
uniform vec3 u_LightColor[MAX_LIGHTS];
|
||||
uniform float u_LightIntensity[MAX_LIGHTS];
|
||||
uniform float u_LightRange[MAX_LIGHTS];
|
||||
|
||||
uniform vec3 u_Ambient;
|
||||
uniform vec3 u_SpecColor;
|
||||
uniform float u_Shininess;
|
||||
uniform vec3 u_CameraPosWS;
|
||||
|
||||
// Smooth inverse-square-ish attenuation with soft edge at range
|
||||
float smoothAtten(float dist, float range)
|
||||
{
|
||||
// 0..1 smooth factor (1 near, 0 at range)
|
||||
float x = clamp(1.0 - dist / max(range, 1e-4), 0.0, 1.0);
|
||||
// ease-in curve (square)
|
||||
float fall = x * x;
|
||||
// add mild inverse-square to keep highlights crisp
|
||||
float inv2 = 1.0 / (1.0 + dist*dist);
|
||||
return fall * inv2;
|
||||
}
|
||||
|
||||
void main(){
|
||||
vec3 N = normalize(vNormalWS);
|
||||
vec3 V = normalize(u_CameraPosWS - vPosWS);
|
||||
vec3 N = vec3(0.0, 0.0, 1.0);
|
||||
if (u_HasNormalTex) {
|
||||
vec3 n = texture(u_NormalTex, vUV).xyz * 2.0 - 1.0;
|
||||
N = normalize(vTBN * n);
|
||||
} else {
|
||||
N = normalize(vTBN[2]);
|
||||
}
|
||||
|
||||
vec3 color = u_Ambient * u_BaseColor;
|
||||
vec3 spec = vec3(0.0);
|
||||
vec3 V = normalize(u_CameraPosWS - vPosWS);
|
||||
|
||||
vec3 base = u_BaseColor;
|
||||
if (u_HasDiffuseTex) base *= texture(u_DiffuseTex, vUV).rgb;
|
||||
|
||||
vec3 specColor = u_SpecColor;
|
||||
if (u_HasSpecTex) specColor *= texture(u_SpecTex, vUV).rgb;
|
||||
|
||||
vec3 color = u_Ambient * base;
|
||||
vec3 spec = vec3(0.0);
|
||||
|
||||
for (int i=0; i<u_LightCount && i<MAX_LIGHTS; ++i) {
|
||||
vec3 Lvec = u_LightPos[i] - vPosWS;
|
||||
@@ -509,22 +674,21 @@ namespace OX
|
||||
float att = smoothAtten(d, u_LightRange[i]);
|
||||
vec3 diff = u_LightColor[i] * (u_LightIntensity[i] * ndl * att);
|
||||
|
||||
// Blinn-Phong specular
|
||||
vec3 H = normalize(L + V);
|
||||
float ndh = max(dot(N, H), 0.0);
|
||||
float sp = pow(ndh, max(u_Shininess, 1.0)) * att;
|
||||
spec += u_SpecColor * sp;
|
||||
spec += specColor * sp;
|
||||
|
||||
color += diff * u_BaseColor;
|
||||
color += diff * base;
|
||||
}
|
||||
|
||||
vec3 outCol = color + spec;
|
||||
FragColor = vec4(outCol, 1.0);
|
||||
FragColor = vec4(color + spec, 1.0);
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
m_meshShader.SetName("PointLitMesh");
|
||||
m_meshShader.LoadFromSource(vs, fs);
|
||||
if (!m_meshShaderStorage) m_meshShaderStorage = new _InlineShader();
|
||||
m_meshShaderStorage->SetName("PointLitTextured");
|
||||
m_meshShaderStorage->LoadFromSource(vs, fs);
|
||||
m_meshShaderReady = true;
|
||||
}
|
||||
|
||||
@@ -532,54 +696,165 @@ namespace OX
|
||||
glGenVertexArrays(1, &m_meshVAO);
|
||||
glGenBuffers(1, &m_meshVBO);
|
||||
glGenBuffers(1, &m_meshEBO);
|
||||
|
||||
glBindVertexArray(m_meshVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_meshVBO);
|
||||
glEnableVertexAttribArray(0); // pos
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void *) 0);
|
||||
glEnableVertexAttribArray(1); // normal
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void *) (sizeof(float) * 3));
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_meshEBO);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
||||
static void BindMaterialUniforms(Renderer::_InlineShader &sh, const OX::Material *mat)
|
||||
{
|
||||
glm::vec3 kd(0.82f, 0.82f, 0.84f);
|
||||
glm::vec3 ks(0.04f, 0.04f, 0.04f);
|
||||
float ns = 32.0f;
|
||||
|
||||
extern GLuint g_whiteTex, g_normalFlatTex; // from above
|
||||
GLuint texKd = g_whiteTex, texKs = g_whiteTex, texN = g_normalFlatTex;
|
||||
GLboolean hasKd = GL_FALSE, hasKs = GL_FALSE, hasN = GL_FALSE;
|
||||
|
||||
if (mat) {
|
||||
kd = glm::vec3(mat->kd[0], mat->kd[1], mat->kd[2]);
|
||||
ks = glm::vec3(mat->ks[0], mat->ks[1], mat->ks[2]);
|
||||
ns = mat->ns;
|
||||
|
||||
if (mat->texKd.id) {
|
||||
texKd = mat->texKd.id;
|
||||
hasKd = GL_TRUE;
|
||||
}
|
||||
if (mat->texKs.id) {
|
||||
texKs = mat->texKs.id;
|
||||
hasKs = GL_TRUE;
|
||||
}
|
||||
if (mat->texNormal.id) {
|
||||
texN = mat->texNormal.id;
|
||||
hasN = GL_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
sh.SetVec3("u_BaseColor", kd);
|
||||
sh.SetVec3("u_SpecColor", ks);
|
||||
|
||||
if (auto loc = glGetUniformLocation(sh.GetID(), "u_Shininess"); loc >= 0)
|
||||
glUniform1f(loc, ns);
|
||||
if (auto loc = glGetUniformLocation(sh.GetID(), "u_HasDiffuseTex"); loc >= 0)
|
||||
glUniform1i(loc, hasKd);
|
||||
if (auto loc = glGetUniformLocation(sh.GetID(), "u_HasSpecTex"); loc >= 0)
|
||||
glUniform1i(loc, hasKs);
|
||||
if (auto loc = glGetUniformLocation(sh.GetID(), "u_HasNormalTex"); loc >= 0)
|
||||
glUniform1i(loc, hasN);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texKd);
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, texKs);
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
glBindTexture(GL_TEXTURE_2D, texN);
|
||||
}
|
||||
|
||||
void Renderer::DrawMeshLambert(const DrawItem &di, const glm::mat4 &VP)
|
||||
{
|
||||
const auto &vin = di.mesh->vertices();
|
||||
const auto &idx = di.mesh->indices();
|
||||
const auto *mesh = di.mesh;
|
||||
const auto &vin = mesh->vertices();
|
||||
const auto &idx = mesh->indices();
|
||||
if (vin.empty() || idx.empty()) return;
|
||||
|
||||
// Interleave [px,py,pz, nx,ny,nz]
|
||||
std::vector<float> packed;
|
||||
packed.reserve(vin.size() * 6);
|
||||
for (const auto &v: vin) {
|
||||
glm::vec3 p, n;
|
||||
ExtractPN(v, p, n);
|
||||
packed.insert(packed.end(), {p.x, p.y, p.z, n.x, n.y, n.z});
|
||||
}
|
||||
|
||||
std::vector<uint32_t> indices;
|
||||
indices.reserve(idx.size());
|
||||
for (auto i: idx) indices.push_back(static_cast<uint32_t>(i));
|
||||
|
||||
const glm::mat4 M = BuildModelMatrix(di.tr);
|
||||
const glm::mat3 N = glm::mat3(glm::transpose(glm::inverse(M)));
|
||||
|
||||
m_meshShader.Use();
|
||||
m_meshShader.SetMat4("u_M", M);
|
||||
m_meshShader.SetMat4("u_VP", VP);
|
||||
m_meshShader.SetMat3("u_N", N);
|
||||
m_meshShader.SetVec3("u_BaseColor", glm::vec3(0.82f, 0.82f, 0.84f)); // light gray by default
|
||||
auto &shB = m_meshShaderRef();
|
||||
auto &sh = reinterpret_cast<_InlineShader &>(shB);
|
||||
sh.Use();
|
||||
sh.SetMat4("u_M", M);
|
||||
sh.SetMat4("u_VP", VP);
|
||||
sh.SetMat3("u_N", N);
|
||||
|
||||
glBindVertexArray(m_meshVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_meshVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * packed.size(), packed.data(), GL_STREAM_DRAW);
|
||||
glBindVertexArray(mesh->vao());
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_meshEBO);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint32_t) * indices.size(), indices.data(), GL_STREAM_DRAW);
|
||||
const auto &subs = mesh->submeshes();
|
||||
const auto &mats = mesh->materials();
|
||||
|
||||
if (!subs.empty()) {
|
||||
for (const auto &sm: subs) {
|
||||
const OX::Material *mat = nullptr;
|
||||
if (sm.materialIndex >= 0 && sm.materialIndex < (int) mats.size())
|
||||
mat = &mats[sm.materialIndex];
|
||||
|
||||
BindMaterialUniforms(sh, mat);
|
||||
|
||||
const GLsizei count = static_cast<GLsizei>(sm.indexCount);
|
||||
const GLvoid *off = (const GLvoid *) (sizeof(uint32_t) * sm.indexOffset);
|
||||
glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, off);
|
||||
}
|
||||
} else {
|
||||
const OX::Material *mat = !mats.empty() ? &mats[0] : nullptr;
|
||||
BindMaterialUniforms(sh, mat);
|
||||
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(idx.size()), GL_UNSIGNED_INT, nullptr);
|
||||
}
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// ===== line pipeline + API ======================================================
|
||||
void Renderer::EnsureLinePipeline()
|
||||
{
|
||||
if (m_lineReady) return;
|
||||
|
||||
const char *vs = R"GLSL(
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPos;
|
||||
uniform mat4 u_MVP;
|
||||
void main(){ gl_Position = u_MVP * vec4(aPos, 1.0); }
|
||||
)GLSL";
|
||||
|
||||
const char *fs = R"GLSL(
|
||||
#version 330 core
|
||||
uniform vec3 u_Color;
|
||||
out vec4 FragColor;
|
||||
void main(){ FragColor = vec4(u_Color, 1.0); }
|
||||
)GLSL";
|
||||
|
||||
if (!m_lineShaderStorage) m_lineShaderStorage = new _InlineShader();
|
||||
m_lineShaderStorage->SetName("Line3D");
|
||||
m_lineShaderStorage->LoadFromSource(vs, fs);
|
||||
|
||||
glGenVertexArrays(1, &m_lineVAO);
|
||||
glGenBuffers(1, &m_lineVBO);
|
||||
|
||||
glBindVertexArray(m_lineVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_lineVBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6, nullptr, GL_DYNAMIC_DRAW); // 2 verts
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, (void *) 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
m_lineReady = true;
|
||||
}
|
||||
|
||||
void Renderer::DrawLine3D(const glm::vec3 &a,
|
||||
const glm::vec3 &b,
|
||||
const glm::vec3 &rgb, float thickness)
|
||||
{
|
||||
EnsureLinePipeline();
|
||||
|
||||
const float verts[6] = {a.x, a.y, a.z, b.x, b.y, b.z};
|
||||
|
||||
auto &sh = *m_lineShaderStorage;
|
||||
sh.Use();
|
||||
sh.SetMat4("u_MVP", m_viewProj);
|
||||
sh.SetVec3("u_Color", rgb);
|
||||
|
||||
glBindVertexArray(m_lineVAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_lineVBO);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);
|
||||
|
||||
glLineWidth(glm::max(1.0f, thickness));
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDrawArrays(GL_LINES, 0, 2);
|
||||
|
||||
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(indices.size()), GL_UNSIGNED_INT, nullptr);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
} // namespace OX
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <glm/glm.hpp>
|
||||
#include "types/vec2.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
class Core;
|
||||
class Scene;
|
||||
class GameObject;
|
||||
class MeshComponent;
|
||||
|
||||
class TransformComponent;
|
||||
class MeshComponent;
|
||||
class CameraComponent;
|
||||
class PointLightComponent;
|
||||
|
||||
|
||||
class Shader; // your wrapper
|
||||
}
|
||||
|
||||
#include "systems/Shader.h"
|
||||
#include <GL/glew.h>
|
||||
|
||||
namespace OX
|
||||
{
|
||||
struct DrawItem
|
||||
{
|
||||
GameObject *go{};
|
||||
TransformComponent *tr{};
|
||||
MeshComponent *mesh{};
|
||||
float depth{};
|
||||
};
|
||||
class Shader;
|
||||
struct Material;
|
||||
|
||||
struct PointLightGPU
|
||||
{
|
||||
glm::vec3 positionWS{0.0f};
|
||||
float range{10.0f};
|
||||
float range{5.0f};
|
||||
glm::vec3 color{1.0f};
|
||||
float intensity{1.0f};
|
||||
};
|
||||
@@ -39,65 +29,94 @@ namespace OX
|
||||
class Renderer
|
||||
{
|
||||
public:
|
||||
class _InlineShader;
|
||||
|
||||
|
||||
Renderer() = default;
|
||||
|
||||
~Renderer();
|
||||
|
||||
void Init(int targetWidth, int targetHeight);
|
||||
|
||||
void ResizeTarget(int width, int height);
|
||||
|
||||
// Begin locates camera (or auto-frames AABB) and binds FBO
|
||||
bool Begin(const Scene &scene);
|
||||
// No editor usage; Core is only for accessing the Scene.
|
||||
bool Begin(Core &core);
|
||||
|
||||
void End();
|
||||
|
||||
// Build draw list (front-to-back) and render with point lights
|
||||
void RenderSceneShaded(const Scene &scene);
|
||||
|
||||
GLuint GetColorTexture() const { return m_colorTex; }
|
||||
Vec2i GetTargetSize() const { return m_size; }
|
||||
// --- 3D line API (world-space), color in RGB [0..1], thickness in px ---
|
||||
void DrawLine3D(const glm::vec3 &a,
|
||||
const glm::vec3 &b,
|
||||
const glm::vec3 &rgb, float thickness = 1.0f);
|
||||
|
||||
inline unsigned GetColorTexture() const { return m_colorTex; }
|
||||
inline const glm::mat4 &GetViewProj() const { return m_viewProj; }
|
||||
inline const glm::vec3 &GetCameraPosWS() const { return m_cameraPosWS; }
|
||||
|
||||
private:
|
||||
void CreateFramebuffer(int width, int height);
|
||||
|
||||
// Scene traversal helpers
|
||||
static glm::mat4 BuildModelMatrix(const TransformComponent *tr);
|
||||
|
||||
static glm::mat4 BuildViewMatrix(const TransformComponent *camTr);
|
||||
|
||||
static glm::mat4 BuildProjectionMatrix(const CameraComponent *cam, float aspect);
|
||||
|
||||
void EnsureMeshPipeline();
|
||||
|
||||
void CollectDrawList(const Scene &scene, std::vector<DrawItem> &out, glm::mat4 &outView) const;
|
||||
|
||||
void GatherPointLights(const Scene &scene);
|
||||
|
||||
void UploadFrameLightsUniforms(); // uses glGetIntegerv(GL_CURRENT_PROGRAM, ...)
|
||||
void UploadFrameLightsUniforms();
|
||||
|
||||
void EnsureMeshPipeline();
|
||||
|
||||
void EnsureLinePipeline();
|
||||
|
||||
struct DrawItem
|
||||
{
|
||||
GameObject *go = nullptr;
|
||||
TransformComponent *tr = nullptr;
|
||||
MeshComponent *mesh = nullptr;
|
||||
float depth = 0.0f;
|
||||
};
|
||||
|
||||
void CollectDrawList(const Scene &scene, std::vector<DrawItem> &out, glm::mat4 &outView) const;
|
||||
|
||||
void DrawMeshLambert(const DrawItem &di, const glm::mat4 &VP);
|
||||
|
||||
private:
|
||||
// Offscreen target
|
||||
Vec2i m_size{};
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_colorTex = 0;
|
||||
GLuint m_depthRBO = 0;
|
||||
// Target/FBO
|
||||
glm::ivec2 m_size{1, 1};
|
||||
unsigned m_fbo = 0;
|
||||
unsigned m_colorTex = 0;
|
||||
unsigned m_depthRBO = 0;
|
||||
|
||||
// Mesh pipeline
|
||||
Shader m_meshShader;
|
||||
bool m_meshShaderReady = false;
|
||||
GLuint m_meshVAO = 0;
|
||||
GLuint m_meshVBO = 0;
|
||||
GLuint m_meshEBO = 0;
|
||||
// Legacy VAO/VBO (kept for compatibility)
|
||||
unsigned m_meshVAO = 0;
|
||||
unsigned m_meshVBO = 0;
|
||||
unsigned m_meshEBO = 0;
|
||||
|
||||
// Per-frame state
|
||||
// Frame state
|
||||
glm::mat4 m_viewProj{1.0f};
|
||||
glm::vec3 m_cameraPosWS{0.0f};
|
||||
glm::vec3 m_ambient{0.04f, 0.04f, 0.05f};
|
||||
glm::vec3 m_specColor{0.04f, 0.04f, 0.04f};
|
||||
float m_shininess = 32.0f;
|
||||
std::vector<PointLightGPU> m_frameLights;
|
||||
|
||||
// Artistic tuning
|
||||
glm::vec3 m_ambient{0.06f, 0.065f, 0.07f};
|
||||
glm::vec3 m_specColor{0.18f, 0.18f, 0.18f};
|
||||
float m_shininess = 32.0f;
|
||||
// Mesh pipeline
|
||||
Shader *m_meshShaderPtr = nullptr;
|
||||
|
||||
Shader &m_meshShaderRef();
|
||||
|
||||
_InlineShader *m_meshShaderStorage = nullptr;
|
||||
bool m_meshShaderReady = false;
|
||||
|
||||
// Line pipeline
|
||||
bool m_lineReady = false;
|
||||
unsigned m_lineVAO = 0;
|
||||
unsigned m_lineVBO = 0;
|
||||
_InlineShader *m_lineShaderStorage = nullptr;
|
||||
};
|
||||
} // namespace OX
|
||||
|
||||
@@ -1,54 +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 "Profiler.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -57,187 +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();
|
||||
|
||||
for (auto &entry: allEntries) {
|
||||
if (!s_Scanning) break;
|
||||
|
||||
try {
|
||||
const auto &path = entry.path();
|
||||
std::string resPath = MakeVirtualPath(path);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
GLuint texID;
|
||||
glGenTextures(1, &texID);
|
||||
glBindTexture(GL_TEXTURE_2D, texID);
|
||||
GLenum fmt = (pending.channels == 4 ? GL_RGBA : GL_RGB);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, fmt, pending.width, pending.height, 0, fmt, GL_UNSIGNED_BYTE, pending.data);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
stbi_image_free(pending.data);
|
||||
|
||||
auto tex = std::make_shared<Texture2D>();
|
||||
tex->SetFromGL(texID, pending.width, pending.height);
|
||||
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
s_LoadedAssets[pending.id] = tex;
|
||||
auto meta = s_MetadataMap[pending.id];
|
||||
s_PathToID[meta.absolutePath] = pending.id;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -249,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);
|
||||
@@ -271,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;
|
||||
@@ -312,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,12 +2,17 @@
|
||||
#include "systems/Logger.h"
|
||||
#include "systems/Scene/GameObject.h"
|
||||
#include "systems/assets/MeshLoaderOBJ.h"
|
||||
#include "systems/AssetManager.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <stb/stb_image.h>
|
||||
|
||||
#include <GL/glew.h>
|
||||
|
||||
namespace OX
|
||||
{
|
||||
// ---------------------------------------------------------------------
|
||||
// GLTexture helpers
|
||||
// ---------------------------------------------------------------------
|
||||
void GLTexture::Destroy()
|
||||
{
|
||||
if (id) {
|
||||
@@ -18,82 +23,223 @@ namespace OX
|
||||
path.clear();
|
||||
}
|
||||
|
||||
static GLuint CreateTextureFromFile(const std::string &path, int *outW = nullptr, int *outH = nullptr,
|
||||
int *outC = nullptr)
|
||||
void GLTexture::SetFromGL(unsigned int id, int width, int height)
|
||||
{
|
||||
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;
|
||||
Destroy();
|
||||
this->id = id;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
}
|
||||
|
||||
MeshComponent::MeshComponent(const std::string &objPath) : m_sourcePath(objPath)
|
||||
MeshComponent::~MeshComponent()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
MeshComponent::~MeshComponent() { 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_sourceAssetId = assetId;
|
||||
|
||||
// Upload textures for materials
|
||||
for (auto &m: m_materials) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
m_vertices = model->vertices;
|
||||
m_indices = model->indices;
|
||||
m_submeshes = model->submeshes;
|
||||
m_materials = model->materials;
|
||||
m_loadedSubmeshIndex = -1;
|
||||
|
||||
CreateGL();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MeshComponent::LoadModelSubmesh(uint64_t assetId, int submeshIndex)
|
||||
{
|
||||
Clear();
|
||||
|
||||
auto model = AssetManager::GetAsset<ModelAsset>(assetId);
|
||||
if (!model) {
|
||||
Logger::LogError("MeshComponent: invalid asset ID %" PRIu64, assetId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (submeshIndex < 0 || submeshIndex >= (int) model->submeshes.size()) {
|
||||
Logger::LogError("MeshComponent: submesh index %d out of range for asset %" PRIu64,
|
||||
submeshIndex, assetId);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &sm = model->submeshes[submeshIndex];
|
||||
|
||||
// Compact into local buffers
|
||||
std::vector<Vertex> v;
|
||||
std::vector<uint32_t> i;
|
||||
v.reserve(sm.indexCount);
|
||||
i.reserve(sm.indexCount);
|
||||
|
||||
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();
|
||||
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{};
|
||||
one.indexOffset = 0;
|
||||
one.indexCount = static_cast<uint32_t>(m_indices.size());
|
||||
one.materialIndex = 0;
|
||||
m_submeshes.push_back(one);
|
||||
|
||||
m_loadedSubmeshIndex = submeshIndex;
|
||||
|
||||
CreateGL();
|
||||
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();
|
||||
@@ -106,7 +252,8 @@ namespace OX
|
||||
m_indices.clear();
|
||||
m_submeshes.clear();
|
||||
m_materials.clear();
|
||||
m_sourcePath.clear();
|
||||
m_sourceAssetId = 0;
|
||||
m_loadedSubmeshIndex = -1;
|
||||
}
|
||||
|
||||
void MeshComponent::CreateGL()
|
||||
@@ -125,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;
|
||||
|
||||
@@ -147,7 +293,6 @@ namespace OX
|
||||
|
||||
glEnableVertexAttribArray(4);
|
||||
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, stride, (void *) offs);
|
||||
offs += sizeof(float) * 3;
|
||||
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
@@ -167,30 +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;
|
||||
return n;
|
||||
}
|
||||
|
||||
void MeshComponent::Deserialize(const YAML::Node &node)
|
||||
{
|
||||
Clear();
|
||||
if (node["path"]) {
|
||||
const std::string p = node["path"].as<std::string>();
|
||||
(void) LoadOBJ(p);
|
||||
}
|
||||
}
|
||||
} // namespace OX
|
||||
|
||||
@@ -1,85 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "Component.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
// OpenGL (your project uses GLEW)
|
||||
#include <GL/glew.h>
|
||||
#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
|
||||
enum class MeshImportMode
|
||||
{
|
||||
GLuint id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int channels = 0;
|
||||
std::string path;
|
||||
|
||||
void Destroy();
|
||||
SingleComponent,
|
||||
ChildrenPerSubmesh
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// MeshComponent holds CPU data + GPU buffers
|
||||
class MeshComponent : public Component
|
||||
{
|
||||
public:
|
||||
MeshComponent() = default;
|
||||
|
||||
explicit MeshComponent(const std::string &objPath);
|
||||
|
||||
~MeshComponent() override;
|
||||
~MeshComponent();
|
||||
|
||||
// Load/clear
|
||||
bool LoadOBJ(const std::string &path); // loads CPU+GPU data
|
||||
void Clear(); // releases GPU and CPU
|
||||
// === New Asset-ID based API ===
|
||||
bool LoadModel(uint64_t assetId);
|
||||
|
||||
// GPU info for renderer
|
||||
GLuint vao() const { return m_vao; }
|
||||
GLuint vbo() const { return m_vbo; }
|
||||
GLuint ebo() const { return m_ebo; }
|
||||
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);
|
||||
|
||||
bool LoadOBJSubmesh(const std::string &path, int submeshIndex);
|
||||
|
||||
static bool ImportOBJ(GameObject *parent, const std::string &path, MeshImportMode mode);
|
||||
|
||||
|
||||
// GPU info
|
||||
unsigned int vao() const { return m_vao; }
|
||||
unsigned int vbo() const { return m_vbo; }
|
||||
unsigned int ebo() const { return m_ebo; }
|
||||
|
||||
// CPU data
|
||||
const std::vector<SubMesh> &submeshes() const { return m_submeshes; }
|
||||
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;
|
||||
|
||||
@@ -88,22 +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
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
unsigned int m_vao = 0, m_vbo = 0, m_ebo = 0;
|
||||
};
|
||||
} // namespace OX
|
||||
|
||||
@@ -1,85 +1,111 @@
|
||||
//
|
||||
// Created by spenc on 6/7/2025.
|
||||
//
|
||||
|
||||
#include "TransformComponent.h"
|
||||
#include "systems/Scene/GameObject.h" // Needed for parent access
|
||||
|
||||
namespace OX {
|
||||
|
||||
TransformComponent::TransformComponent()
|
||||
: m_position{0.0f, 0.0f, 0.0f}
|
||||
, m_rotation{0.0f, 0.0f, 0.0f}
|
||||
, m_scale {1.0f, 1.0f, 1.0f}
|
||||
{}
|
||||
|
||||
TransformComponent::TransformComponent(float x, float y, float z)
|
||||
: m_position{x, y, z }
|
||||
, m_rotation{0.0f, 0.0f, 0.0f}
|
||||
, m_scale {1.0f, 1.0f, 1.0f}
|
||||
{}
|
||||
|
||||
void TransformComponent::setPosition(float x, float y, float z) {
|
||||
m_position = { x, y, z };
|
||||
}
|
||||
const std::array<float,3>& TransformComponent::getPosition() const {
|
||||
return m_position;
|
||||
}
|
||||
|
||||
void TransformComponent::setRotation(float x, float y, float z) {
|
||||
m_rotation = { x, y, z };
|
||||
}
|
||||
const std::array<float,3>& TransformComponent::getRotation() const {
|
||||
return m_rotation;
|
||||
}
|
||||
|
||||
void TransformComponent::setScale(float x, float y, float z) {
|
||||
m_scale = { x, y, z };
|
||||
}
|
||||
const std::array<float,3>& TransformComponent::getScale() const {
|
||||
return m_scale;
|
||||
}
|
||||
|
||||
YAML::Node TransformComponent::Serialize() const {
|
||||
YAML::Node node;
|
||||
node["type"] = "TransformComponent";
|
||||
|
||||
YAML::Node pos(YAML::NodeType::Sequence);
|
||||
pos.push_back(m_position[0]);
|
||||
pos.push_back(m_position[1]);
|
||||
pos.push_back(m_position[2]);
|
||||
node["position"] = pos;
|
||||
|
||||
YAML::Node rot(YAML::NodeType::Sequence);
|
||||
rot.push_back(m_rotation[0]);
|
||||
rot.push_back(m_rotation[1]);
|
||||
rot.push_back(m_rotation[2]);
|
||||
node["rotation"] = rot;
|
||||
|
||||
YAML::Node scl(YAML::NodeType::Sequence);
|
||||
scl.push_back(m_scale[0]);
|
||||
scl.push_back(m_scale[1]);
|
||||
scl.push_back(m_scale[2]);
|
||||
node["scale"] = scl;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
void TransformComponent::Deserialize(const YAML::Node &node) {
|
||||
if (auto p = node["position"]; p && p.IsSequence() && p.size() >= 3) {
|
||||
m_position[0] = p[0].as<float>();
|
||||
m_position[1] = p[1].as<float>();
|
||||
m_position[2] = p[2].as<float>();
|
||||
namespace OX
|
||||
{
|
||||
TransformComponent::TransformComponent()
|
||||
: m_position{0.0f, 0.0f, 0.0f}
|
||||
, m_rotation{0.0f, 0.0f, 0.0f}
|
||||
, m_scale{1.0f, 1.0f, 1.0f}
|
||||
{
|
||||
}
|
||||
if (auto r = node["rotation"]; r && r.IsSequence() && r.size() >= 3) {
|
||||
m_rotation[0] = r[0].as<float>();
|
||||
m_rotation[1] = r[1].as<float>();
|
||||
m_rotation[2] = r[2].as<float>();
|
||||
}
|
||||
if (auto s = node["scale"]; s && s.IsSequence() && s.size() >= 3) {
|
||||
m_scale[0] = s[0].as<float>();
|
||||
m_scale[1] = s[1].as<float>();
|
||||
m_scale[2] = s[2].as<float>();
|
||||
}
|
||||
}
|
||||
|
||||
TransformComponent::TransformComponent(float x, float y, float z)
|
||||
: m_position{x, y, z}
|
||||
, m_rotation{0.0f, 0.0f, 0.0f}
|
||||
, m_scale{1.0f, 1.0f, 1.0f}
|
||||
{
|
||||
}
|
||||
|
||||
void TransformComponent::setPosition(float x, float y, float z)
|
||||
{
|
||||
m_position = {x, y, z};
|
||||
}
|
||||
|
||||
std::array<float, 3> TransformComponent::getPosition() const
|
||||
{
|
||||
// Start with local position
|
||||
std::array<float, 3> worldPos = m_position;
|
||||
|
||||
// If we have an owning object, accumulate parent transforms
|
||||
if (m_owner) {
|
||||
GameObject *parent = m_owner->parent();
|
||||
while (parent) {
|
||||
if (auto *parentTransform = parent->getComponent<TransformComponent>()) {
|
||||
auto parentPos = parentTransform->getPosition(); // recursive
|
||||
worldPos[0] += parentPos[0];
|
||||
worldPos[1] += parentPos[1];
|
||||
worldPos[2] += parentPos[2];
|
||||
}
|
||||
parent = parent->parent();
|
||||
}
|
||||
}
|
||||
|
||||
return worldPos;
|
||||
}
|
||||
|
||||
void TransformComponent::setRotation(float x, float y, float z)
|
||||
{
|
||||
m_rotation = {x, y, z};
|
||||
}
|
||||
|
||||
const std::array<float, 3> &TransformComponent::getRotation() const
|
||||
{
|
||||
return m_rotation;
|
||||
}
|
||||
|
||||
void TransformComponent::setScale(float x, float y, float z)
|
||||
{
|
||||
m_scale = {x, y, z};
|
||||
}
|
||||
|
||||
const std::array<float, 3> &TransformComponent::getScale() const
|
||||
{
|
||||
return m_scale;
|
||||
}
|
||||
|
||||
YAML::Node TransformComponent::Serialize() const
|
||||
{
|
||||
YAML::Node node;
|
||||
node["type"] = "TransformComponent";
|
||||
|
||||
YAML::Node pos(YAML::NodeType::Sequence);
|
||||
pos.push_back(m_position[0]);
|
||||
pos.push_back(m_position[1]);
|
||||
pos.push_back(m_position[2]);
|
||||
node["position"] = pos;
|
||||
|
||||
YAML::Node rot(YAML::NodeType::Sequence);
|
||||
rot.push_back(m_rotation[0]);
|
||||
rot.push_back(m_rotation[1]);
|
||||
rot.push_back(m_rotation[2]);
|
||||
node["rotation"] = rot;
|
||||
|
||||
YAML::Node scl(YAML::NodeType::Sequence);
|
||||
scl.push_back(m_scale[0]);
|
||||
scl.push_back(m_scale[1]);
|
||||
scl.push_back(m_scale[2]);
|
||||
node["scale"] = scl;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
void TransformComponent::Deserialize(const YAML::Node &node)
|
||||
{
|
||||
if (auto p = node["position"]; p && p.IsSequence() && p.size() >= 3) {
|
||||
m_position[0] = p[0].as<float>();
|
||||
m_position[1] = p[1].as<float>();
|
||||
m_position[2] = p[2].as<float>();
|
||||
}
|
||||
if (auto r = node["rotation"]; r && r.IsSequence() && r.size() >= 3) {
|
||||
m_rotation[0] = r[0].as<float>();
|
||||
m_rotation[1] = r[1].as<float>();
|
||||
m_rotation[2] = r[2].as<float>();
|
||||
}
|
||||
if (auto s = node["scale"]; s && s.IsSequence() && s.size() >= 3) {
|
||||
m_scale[0] = s[0].as<float>();
|
||||
m_scale[1] = s[1].as<float>();
|
||||
m_scale[2] = s[2].as<float>();
|
||||
}
|
||||
}
|
||||
} // namespace OX
|
||||
|
||||
@@ -8,33 +8,38 @@
|
||||
#include <yaml-cpp/yaml.h>
|
||||
#include <array>
|
||||
|
||||
namespace OX {
|
||||
|
||||
class TransformComponent : public Component {
|
||||
namespace OX
|
||||
{
|
||||
class TransformComponent : public Component
|
||||
{
|
||||
public:
|
||||
TransformComponent();
|
||||
|
||||
TransformComponent(float x, float y, float z);
|
||||
|
||||
// — Position
|
||||
void setPosition(float x, float y, float z);
|
||||
const std::array<float,3>& getPosition() const;
|
||||
|
||||
std::array<float, 3> getPosition() const;
|
||||
|
||||
// — Rotation (Euler angles, in degrees)
|
||||
void setRotation(float x, float y, float z);
|
||||
const std::array<float,3>& getRotation() const;
|
||||
|
||||
const std::array<float, 3> &getRotation() const;
|
||||
|
||||
// — Scale
|
||||
void setScale(float x, float y, float z);
|
||||
const std::array<float,3>& getScale() const;
|
||||
|
||||
const std::array<float, 3> &getScale() const;
|
||||
|
||||
// — Serialization
|
||||
YAML::Node Serialize() const override;
|
||||
|
||||
void Deserialize(const YAML::Node &node) override;
|
||||
|
||||
private:
|
||||
std::array<float,3> m_position;
|
||||
std::array<float,3> m_rotation;
|
||||
std::array<float,3> m_scale;
|
||||
std::array<float, 3> m_position;
|
||||
std::array<float, 3> m_rotation;
|
||||
std::array<float, 3> m_scale;
|
||||
};
|
||||
|
||||
} // namespace OX
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
namespace OX
|
||||
{
|
||||
// Stable 64-bit ID for all objects in a Scene.
|
||||
// (Scene indexes by this ID; tags are optional & user-facing.)
|
||||
using UUID = std::uint64_t;
|
||||
|
||||
class GameObject
|
||||
@@ -44,25 +42,33 @@ namespace OX
|
||||
|
||||
const std::vector<std::unique_ptr<GameObject> > &children() const;
|
||||
|
||||
/// Create and attach a child object. Returns the raw pointer.
|
||||
GameObject *createChild(const std::string &name = "");
|
||||
|
||||
/// Adopt an existing heap-owned node. Prevents cycles automatically.
|
||||
void addChild(std::unique_ptr<GameObject> child);
|
||||
|
||||
/// Detach a direct child and return ownership to the caller.
|
||||
std::unique_ptr<GameObject> removeChild(const GameObject *child);
|
||||
|
||||
/// Move an existing object (that already has a parent) under this. Returns true on success.
|
||||
/// NOTE: This helper cannot reparent root nodes because it does not own scene roots.
|
||||
bool reparentChild(GameObject *child);
|
||||
|
||||
/// True if `this` is an ancestor of `other`.
|
||||
bool isAncestorOf(const GameObject *other) const;
|
||||
|
||||
// — Components
|
||||
void addComponent(std::unique_ptr<Component> comp);
|
||||
|
||||
template<typename T, typename... Args>
|
||||
T *AddComponent(Args &&... args)
|
||||
{
|
||||
static_assert(std::is_base_of<Component, T>::value,
|
||||
"AddComponent<T> requires T to derive from Component");
|
||||
|
||||
// Construct and attach the component
|
||||
auto comp = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
T *ptr = comp.get();
|
||||
comp->OnAttach(this);
|
||||
m_components.push_back(std::move(comp));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *getComponent() const;
|
||||
|
||||
@@ -83,8 +89,7 @@ namespace OX
|
||||
return rng();
|
||||
}
|
||||
|
||||
// internal
|
||||
void detachAllComponents(); // calls OnDetach() for each component
|
||||
void detachAllComponents();
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
@@ -116,9 +121,7 @@ namespace OX
|
||||
if (it == m_components.end())
|
||||
return nullptr;
|
||||
|
||||
// Allow components to clean up links/resources before destruction.
|
||||
(*it)->OnDetach();
|
||||
|
||||
auto out = std::move(*it);
|
||||
m_components.erase(it);
|
||||
return out;
|
||||
|
||||
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>
|
||||
@@ -8,6 +9,8 @@
|
||||
#include <filesystem>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -43,6 +46,47 @@ namespace OX
|
||||
return out;
|
||||
}
|
||||
|
||||
// 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, esc = false;
|
||||
auto flush = [&]()
|
||||
{
|
||||
if (!cur.empty()) {
|
||||
out.push_back(cur);
|
||||
cur.clear();
|
||||
}
|
||||
};
|
||||
for (char c: s) {
|
||||
if (esc) {
|
||||
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);
|
||||
}
|
||||
flush();
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string unescape_backslashes(std::string s)
|
||||
{
|
||||
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);
|
||||
} else {
|
||||
out.push_back(c);
|
||||
esc = false;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string join_path(const std::string &base, const std::string &rel)
|
||||
{
|
||||
if (rel.empty()) return rel;
|
||||
@@ -59,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];
|
||||
@@ -95,65 +132,60 @@ 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;
|
||||
}
|
||||
|
||||
// ---------- MTL parsing ----------
|
||||
static void parse_mtl(const std::string &mtlFilePath,
|
||||
std::vector<Material> &outMaterials,
|
||||
std::unordered_map<std::string, int> &nameToIndex)
|
||||
// ---------- Wavefront map_* tail parser ----------
|
||||
static std::string extract_map_path_from_line(const std::string &line, const char *key)
|
||||
{
|
||||
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)) {
|
||||
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;
|
||||
cur.name = (toks.size() >= 2 ? toks[1] : "");
|
||||
} 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") && toks.size() >= 2) {
|
||||
cur.mapKd = join_path(base, toks[1]);
|
||||
} else if ((key == "map_Ks" || key == "map_ks") && toks.size() >= 2) {
|
||||
cur.mapKs = join_path(base, toks[1]);
|
||||
} else if (key == "map_Bump" || key == "map_bump" || key == "bump" || key == "norm" || key ==
|
||||
"map_normal") {
|
||||
if (toks.size() >= 2) cur.mapNormal = join_path(base, toks.back()); // ignore options like -bm
|
||||
size_t start = 0;
|
||||
while (start < line.size() && std::isspace((unsigned char) line[start])) ++start;
|
||||
const size_t key_len = std::strlen(key);
|
||||
if (line.compare(start, key_len, key) != 0) return {};
|
||||
size_t pos = start + key_len;
|
||||
while (pos < line.size() && std::isspace((unsigned char) line[pos])) ++pos;
|
||||
if (pos >= line.size()) return {};
|
||||
if (line[pos] == '"') {
|
||||
++pos;
|
||||
std::string out;
|
||||
bool esc = false;
|
||||
for (; pos < line.size(); ++pos) {
|
||||
char c = line[pos];
|
||||
if (esc) {
|
||||
out.push_back(c);
|
||||
esc = false;
|
||||
} else if (c == '\\') { esc = true; } else if (c == '"') {
|
||||
++pos;
|
||||
break;
|
||||
} else out.push_back(c);
|
||||
}
|
||||
// ignore d/Tr/illum/etc for now
|
||||
return out;
|
||||
}
|
||||
commit();
|
||||
std::string tail = line.substr(pos);
|
||||
auto toks = split_ws_escaped(tail);
|
||||
if (toks.empty()) return {};
|
||||
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") { 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] == '-') { consume(i, 1 + (i + 1 < toks.size() ? 1 : 0)); } else break;
|
||||
}
|
||||
if (i >= toks.size()) return {};
|
||||
std::string joined = toks[i];
|
||||
for (size_t j = i + 1; j < toks.size(); ++j) {
|
||||
joined.push_back(' ');
|
||||
joined += toks[j];
|
||||
}
|
||||
return unescape_backslashes(joined);
|
||||
}
|
||||
|
||||
// ---------- tangent generation ----------
|
||||
@@ -164,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;
|
||||
@@ -175,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;
|
||||
@@ -208,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});
|
||||
@@ -242,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];
|
||||
@@ -262,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) {
|
||||
@@ -298,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()) {
|
||||
@@ -320,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
|
||||
@@ -340,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;
|
||||
}
|
||||
};
|
||||
@@ -374,6 +482,7 @@ namespace OX
|
||||
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
std::string raw = line;
|
||||
trim(line);
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
@@ -385,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;
|
||||
@@ -402,18 +509,17 @@ namespace OX
|
||||
if (!parse_vertex_triplet(toks[i], (int) P.size(), (int) T.size(), (int) N.size(), v1, vt1, vn1))
|
||||
continue;
|
||||
if (!parse_vertex_triplet(toks[i + 1], (int) P.size(), (int) T.size(), (int) N.size(), v2, vt2,
|
||||
vn2)) continue;
|
||||
vn2))
|
||||
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];
|
||||
@@ -425,85 +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" && toks.size() >= 2) {
|
||||
std::string mname = toks[1];
|
||||
} else if (key == "usemtl") {
|
||||
size_t at = raw.find("usemtl");
|
||||
std::string mname = (at == std::string::npos)
|
||||
? (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()) {
|
||||
// create a placeholder material if referenced before defined
|
||||
Material m;
|
||||
OX::Material m;
|
||||
m.name = mname;
|
||||
mtlNameToIndex[mname] = (int) materials.size();
|
||||
materials.push_back(m);
|
||||
}
|
||||
currentMat = mtlNameToIndex[mname];
|
||||
} else if (key == "mtllib" && toks.size() >= 2) {
|
||||
std::string mtlPath = join_path(base, toks[1]);
|
||||
parse_mtl(mtlPath, materials, mtlNameToIndex);
|
||||
} else if (key == "mtllib") {
|
||||
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) {
|
||||
for (size_t i = 1; i < toks.size(); ++i) {
|
||||
if (i > 1) mtlFile += " ";
|
||||
mtlFile += toks[i];
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
#include "Windows/InspectorWindow.h"
|
||||
#include "Windows/SceneExplorer.h"
|
||||
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <imgui_impl_glfw.h>
|
||||
|
||||
namespace OX
|
||||
{
|
||||
void Editor::Init(Core &core)
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
#undef IM_ASSERT
|
||||
#define IM_ASSERT(_EXPR) do { OX_ASSERT(_EXPR, "ImGui internal assertion failed"); } while (0)
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <imgui_impl_glfw.h>
|
||||
|
||||
|
||||
namespace OX
|
||||
{
|
||||
@@ -40,8 +36,20 @@ namespace OX
|
||||
|
||||
//? Getters
|
||||
|
||||
GameObject *GetSelected() { return selected; }
|
||||
void SetSelected(GameObject *obj) { selected = obj; }
|
||||
void SetSelected(GameObject *go)
|
||||
{
|
||||
m_selected = go;
|
||||
m_selectedId = go ? go->id() : 0;
|
||||
}
|
||||
|
||||
GameObject *GetSelected() const { return m_selected; }
|
||||
uint64_t GetSelectedId() const { return m_selectedId; }
|
||||
|
||||
void ClearSelection()
|
||||
{
|
||||
m_selected = nullptr;
|
||||
m_selectedId = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Viewport> primaryViewport;
|
||||
@@ -49,7 +57,8 @@ namespace OX
|
||||
std::shared_ptr<InspectorWindow> inspectorWindow;
|
||||
std::shared_ptr<SceneExplorer> sceneExplorer;
|
||||
|
||||
GameObject *selected;
|
||||
GameObject *m_selected;
|
||||
uint64_t m_selectedId = 0;
|
||||
};
|
||||
} // OX
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -180,21 +174,22 @@ namespace OX
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Verify selected pointer is still alive in the Scene; clear selection if not
|
||||
bool InspectorWindow::EnsureAliveOrClearSelection(Core &core, Editor &editor)
|
||||
{
|
||||
GameObject *selected = editor.GetSelected();
|
||||
if (!selected) return false;
|
||||
const uint64_t selId = editor.GetSelectedId();
|
||||
if (selId == 0) return false;
|
||||
|
||||
auto &scene = core.GetScene();
|
||||
GameObject *alive = scene.get(selected->id());
|
||||
if (alive == nullptr) {
|
||||
editor.SetSelected(nullptr);
|
||||
GameObject *alive = scene.get(selId);
|
||||
if (!alive) {
|
||||
editor.ClearSelection();
|
||||
return false;
|
||||
}
|
||||
// Refresh local buffers when selection changed
|
||||
if (m_lastSelectedId != alive->id())
|
||||
|
||||
if (m_lastSelectedId != selId) {
|
||||
SyncBuffersOnSelection(alive);
|
||||
m_lastSelectedId = selId;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -206,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)
|
||||
@@ -222,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);
|
||||
@@ -258,7 +245,6 @@ namespace OX
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, 6));
|
||||
|
||||
|
||||
if (go->getComponent<TransformComponent>()) {
|
||||
DrawTransformComponent(go);
|
||||
ImGui::Dummy(ImVec2(0, 4));
|
||||
@@ -278,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;
|
||||
@@ -364,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();
|
||||
@@ -411,79 +394,187 @@ namespace OX
|
||||
if (toRemove) (void) go->removeComponent<CameraComponent>();
|
||||
}
|
||||
|
||||
|
||||
void InspectorWindow::DrawMeshComponent(GameObject *go)
|
||||
{
|
||||
bool openDummy = true, toRemove = false;
|
||||
if (ComponentHeader("Mesh", &openDummy, &toRemove)) {
|
||||
if (auto *mc = go->getComponent<MeshComponent>()) {
|
||||
if (ImGui::BeginTable("##mesh_tbl", 2,
|
||||
ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerV)) {
|
||||
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, 90.0f);
|
||||
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
|
||||
bool open = true, toRemove = false;
|
||||
if (!ComponentHeader("Mesh", &open, &toRemove)) {
|
||||
if (toRemove) (void) go->removeComponent<MeshComponent>();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
auto *mc = go->getComponent<MeshComponent>();
|
||||
if (!mc) {
|
||||
if (open) ImGui::TreePop();
|
||||
if (toRemove) (void) go->removeComponent<MeshComponent>();
|
||||
return;
|
||||
}
|
||||
|
||||
// Actions
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Actions");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
// 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
|
||||
|
||||
bool doLoad = false;
|
||||
if (ImGui::Button("Load")) doLoad = true;
|
||||
// --- 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();
|
||||
if (ImGui::Button("Reload")) {
|
||||
if (!mc->sourcePath().empty()) {
|
||||
mc->LoadOBJ(mc->sourcePath());
|
||||
m_meshPath = mc->sourcePath(); // keep UI in sync if loader normalizes path
|
||||
} else {
|
||||
Logger::LogWarning("No mesh path to reload.");
|
||||
}
|
||||
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 (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());
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
if (openDummy) ImGui::TreePop();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void InspectorWindow::DrawPointLightComponent(GameObject *go)
|
||||
{
|
||||
bool toRemove = false;
|
||||
@@ -535,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();
|
||||
@@ -553,7 +644,6 @@ namespace OX
|
||||
if (toRemove) (void) go->removeComponent<PointLightComponent>();
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Popup-only add
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -568,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();
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
namespace OX
|
||||
{
|
||||
class Editor;
|
||||
|
||||
class Viewport
|
||||
{
|
||||
public:
|
||||
@@ -22,7 +24,11 @@ namespace OX
|
||||
|
||||
void Draw(Core &core) const;
|
||||
|
||||
[[nodiscard]] int GetID() const { return m_id; }
|
||||
|
||||
[[nodiscard]]
|
||||
|
||||
int GetID() const { return m_id; }
|
||||
|
||||
[[nodiscard]] const std::string &GetName() const { return m_name; }
|
||||
|
||||
private:
|
||||
|
||||
Reference in New Issue
Block a user