Merge branch 'LAPTOP-DEV'

This commit is contained in:
2025-08-24 13:17:22 -05:00
25 changed files with 2895 additions and 1198 deletions

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
//
// Created by spenc on 5/21/2025.
//
#include "Asset.h"
namespace OX {
} // OX

View 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

View File

@@ -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 (were 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

View File

@@ -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
}

View 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

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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

View File

@@ -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”. Dont 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 its 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
}

View File

@@ -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
}

View File

@@ -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();

View File

@@ -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: