Files
Modularity/src/main.cpp
Anemunt efb684e57a
Some checks failed
Build C++ Project with Fedora Container and Create Release / build (push) Has been cancelled
Some changes to make the hierarchy look a bit better lol.
2025-12-01 02:15:59 -05:00

3333 lines
124 KiB
C++

#include <iostream>
#include <algorithm>
#include <stdexcept>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <ctime>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <glad/glad.h>
#include "ThirdParty/imgui/imgui.h"
#include "ThirdParty/imgui/imgui_internal.h"
#include "ThirdParty/imgui/backends/imgui_impl_glfw.h"
#include "ThirdParty/imgui/backends/imgui_impl_opengl3.h"
#include "ThirdParty/ImGuizmo/ImGuizmo.h"
#include "ThirdParty/glm/gtc/matrix_transform.hpp"
#include "ThirdParty/glm/gtc/type_ptr.hpp"
#define TINYOBJLOADER_IMPLEMENTATION
#include "../include/ThirdParty/tiny_obj_loader.h"
#include "../include/Window/Window.h"
#include "../include/Shaders/Shader.h"
#include "../include/Textures/Texture.h"
#include "../include/Skybox/Skybox.h"
#ifdef _WIN32
#include <windows.h>
#include <shlobj.h>
#endif
namespace fs = std::filesystem;
constexpr float SENSITIVITY = 0.1f;
constexpr float CAMERA_SPEED = 2.5f;
constexpr float FOV = 45.0f;
constexpr float NEAR_PLANE = 0.1f;
constexpr float FAR_PLANE = 100.0f;
constexpr float PI = 3.14159265359f;
// Replace the existing float vertices[] array (lines ~50-100) with this full 8-float version (pos + normal + texcoord)
float vertices[] = {
// Back face (z = -0.5f)
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
// Front face (z = 0.5f)
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
// Left face (x = -0.5f)
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// Right face (x = 0.5f)
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// Bottom face (y = -0.5f)
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
// Top face (y = 0.5f)
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
};
enum class ObjectType {
Cube,
Sphere,
Capsule,
OBJMesh // New type for loaded OBJ models
};
enum class ConsoleMessageType {
Info,
Warning,
Error,
Success
};
class SceneObject {
public:
std::string name;
ObjectType type;
glm::vec3 position;
glm::vec3 rotation;
glm::vec3 scale;
int id;
int parentId = -1;
std::vector<int> childIds;
bool isExpanded = true;
std::string meshPath; // Path to OBJ file (for OBJMesh type)
int meshId = -1; // Index into loaded meshes cache
SceneObject(const std::string& name, ObjectType type, int id)
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}
};
class FileBrowser {
public:
fs::path currentPath;
fs::path selectedFile;
std::vector<fs::directory_entry> entries;
bool needsRefresh = true;
FileBrowser() {
currentPath = fs::current_path();
}
void refresh() {
entries.clear();
try {
for (const auto& entry : fs::directory_iterator(currentPath)) {
entries.push_back(entry);
}
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
if (a.is_directory() != b.is_directory()) {
return a.is_directory() > b.is_directory();
}
return a.path().filename().string() < b.path().filename().string();
});
} catch (...) {
}
needsRefresh = false;
}
void navigateUp() {
if (currentPath.has_parent_path() && currentPath != currentPath.root_path()) {
currentPath = currentPath.parent_path();
needsRefresh = true;
}
}
void navigateTo(const fs::path& path) {
if (fs::is_directory(path)) {
currentPath = path;
needsRefresh = true;
}
}
const char* getFileIcon(const fs::directory_entry& entry) const {
if (entry.is_directory()) return "[D]";
std::string ext = entry.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == ".cpp" || ext == ".c" || ext == ".h" || ext == ".hpp") return "[C]";
if (ext == ".glsl" || ext == ".vert" || ext == ".frag") return "[S]";
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") return "[I]";
if (ext == ".obj" || ext == ".fbx" || ext == ".gltf") return "[M]";
if (ext == ".txt" || ext == ".md") return "[T]";
return "[F]";
}
bool isOBJFile(const fs::directory_entry& entry) const {
if (entry.is_directory()) return false;
std::string ext = entry.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
return ext == ".obj";
}
};
// Replace the entire generateSphere() function (around line ~200) with this version that adds normals (now 8 floats per vertex)
std::vector<float> generateSphere(int segments = 32, int rings = 16) {
std::vector<float> vertices;
for (int ring = 0; ring <= rings; ring++) {
float theta = ring * PI / rings;
float sinTheta = sin(theta);
float cosTheta = cos(theta);
for (int seg = 0; seg <= segments; seg++) {
float phi = seg * 2.0f * PI / segments;
float sinPhi = sin(phi);
float cosPhi = cos(phi);
float x = cosPhi * sinTheta;
float y = cosTheta;
float z = sinPhi * sinTheta;
// Position
vertices.push_back(x * 0.5f);
vertices.push_back(y * 0.5f);
vertices.push_back(z * 0.5f);
// Normal (same as position for unit sphere)
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
// Texcoord
vertices.push_back((float)seg / segments);
vertices.push_back((float)ring / rings);
}
}
std::vector<float> triangulated;
int stride = segments + 1;
for (int ring = 0; ring < rings; ring++) {
for (int seg = 0; seg < segments; seg++) {
int current = ring * stride + seg;
int next = current + stride;
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[current * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(next + 1) * 8 + i]);
}
}
return triangulated;
}
// Replace the entire generateCapsule() function (around line ~250) with this version that adds normals (now 8 floats per vertex)
std::vector<float> generateCapsule(int segments = 16, int rings = 8) {
std::vector<float> vertices;
float cylinderHeight = 0.5f;
float radius = 0.25f;
// Top hemisphere
for (int ring = 0; ring <= rings / 2; ring++) {
float theta = ring * PI / rings;
float sinTheta = sin(theta);
float cosTheta = cos(theta);
for (int seg = 0; seg <= segments; seg++) {
float phi = seg * 2.0f * PI / segments;
float sinPhi = sin(phi);
float cosPhi = cos(phi);
float x = cosPhi * sinTheta * radius;
float y = cosTheta * radius + cylinderHeight;
float z = sinPhi * sinTheta * radius;
// Position
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
// Normal (normalized direction from center)
glm::vec3 normal = glm::normalize(glm::vec3(x, y - cylinderHeight, z));
vertices.push_back(normal.x);
vertices.push_back(normal.y);
vertices.push_back(normal.z);
// Texcoord
vertices.push_back((float)seg / segments);
vertices.push_back((float)ring / (rings / 2));
}
}
// Cylinder body (top and bottom rings)
for (int i = 0; i <= 1; i++) {
float y = i == 0 ? cylinderHeight : -cylinderHeight;
for (int seg = 0; seg <= segments; seg++) {
float phi = seg * 2.0f * PI / segments;
float x = cos(phi) * radius;
float z = sin(phi) * radius;
// Position
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
// Normal (outward for cylinder)
glm::vec3 normal = glm::normalize(glm::vec3(x, 0.0f, z));
vertices.push_back(normal.x);
vertices.push_back(normal.y);
vertices.push_back(normal.z);
// Texcoord
vertices.push_back((float)seg / segments);
vertices.push_back(0.5f);
}
}
// Bottom hemisphere
for (int ring = rings / 2; ring <= rings; ring++) {
float theta = ring * PI / rings;
float sinTheta = sin(theta);
float cosTheta = cos(theta);
for (int seg = 0; seg <= segments; seg++) {
float phi = seg * 2.0f * PI / segments;
float sinPhi = sin(phi);
float cosPhi = cos(phi);
float x = cosPhi * sinTheta * radius;
float y = cosTheta * radius - cylinderHeight;
float z = sinPhi * sinTheta * radius;
// Position
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
// Normal
glm::vec3 normal = glm::normalize(glm::vec3(x, y + cylinderHeight, z));
vertices.push_back(normal.x);
vertices.push_back(normal.y);
vertices.push_back(normal.z);
// Texcoord
vertices.push_back((float)seg / segments);
vertices.push_back((float)ring / rings);
}
}
std::vector<float> triangulated;
int stride = segments + 1;
int totalRings = rings + 3; // hemispheres + 3 for cylinder rings? Wait, adjust if needed
for (int ring = 0; ring < totalRings - 1; ring++) {
for (int seg = 0; seg < segments; seg++) {
int current = ring * stride + seg;
int next = current + stride;
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[current * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(next + 1) * 8 + i]);
}
}
return triangulated;
}
class Mesh {
private:
unsigned int VAO, VBO;
int vertexCount;
public:
Mesh(const float* vertexData, size_t dataSizeBytes) {
vertexCount = dataSizeBytes / (8 * sizeof(float));
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, dataSizeBytes, vertexData, GL_STATIC_DRAW);
// 0: Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 1: Normal
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 2: TexCoord
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
~Mesh() {
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
}
void draw() const {
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
glBindVertexArray(0);
}
int getVertexCount() const { return vertexCount; }
};
class OBJLoader {
public:
struct LoadedMesh {
std::string path;
std::unique_ptr<Mesh> mesh;
std::string name;
int vertexCount = 0;
int faceCount = 0;
bool hasNormals = false;
bool hasTexCoords = false;
};
private:
std::vector<LoadedMesh> loadedMeshes;
public:
// Load an OBJ file and return index into cache, or -1 on failure
int loadOBJ(const std::string& filepath, std::string& errorMsg) {
// Check if already loaded
for (size_t i = 0; i < loadedMeshes.size(); i++) {
if (loadedMeshes[i].path == filepath) {
return static_cast<int>(i);
}
}
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
std::string warn, err;
std::string baseDir = fs::path(filepath).parent_path().string();
if (!baseDir.empty()) baseDir += "/";
bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err,
filepath.c_str(), baseDir.c_str());
if (!warn.empty()) {
errorMsg += "Warning: " + warn + "\n";
}
if (!err.empty()) {
errorMsg += "Error: " + err + "\n";
}
if (!ret || shapes.empty()) {
errorMsg += "Failed to load OBJ file: " + filepath;
return -1;
}
// Convert to our vertex format (pos + uv)
// Convert to our vertex format (pos + normal + uv)
std::vector<float> vertices;
bool hasNormalsInFile = !attrib.normals.empty();
size_t totalIndices = 0;
int faceCount = 0;
for (const auto& shape : shapes) {
totalIndices += shape.mesh.indices.size();
faceCount += static_cast<int>(shape.mesh.num_face_vertices.size());
}
vertices.reserve(totalIndices * 8 / 3); // rough estimate for triangulated verts
for (const auto& shape : shapes) {
size_t indexOffset = 0;
for (size_t f = 0; f < shape.mesh.num_face_vertices.size(); f++) {
int fv = shape.mesh.num_face_vertices[f];
// Gather vertices for the face
struct TempVertex {
glm::vec3 pos;
glm::vec2 uv;
glm::vec3 normal;
bool hasNormal = false;
};
std::vector<TempVertex> faceVerts;
for (int v = 0; v < fv; v++) {
tinyobj::index_t idx = shape.mesh.indices[indexOffset + v];
TempVertex tv;
tv.pos.x = attrib.vertices[3 * size_t(idx.vertex_index) + 0];
tv.pos.y = attrib.vertices[3 * size_t(idx.vertex_index) + 1];
tv.pos.z = attrib.vertices[3 * size_t(idx.vertex_index) + 2];
if (idx.texcoord_index >= 0 && !attrib.texcoords.empty()) {
tv.uv.x = attrib.texcoords[2 * size_t(idx.texcoord_index) + 0];
tv.uv.y = attrib.texcoords[2 * size_t(idx.texcoord_index) + 1];
} else {
tv.uv = glm::vec2(0.0f);
}
if (idx.normal_index >= 0 && hasNormalsInFile) {
tv.normal.x = attrib.normals[3 * size_t(idx.normal_index) + 0];
tv.normal.y = attrib.normals[3 * size_t(idx.normal_index) + 1];
tv.normal.z = attrib.normals[3 * size_t(idx.normal_index) + 2];
tv.hasNormal = true;
}
faceVerts.push_back(tv);
}
// Compute face normal if no normals in file
if (!hasNormalsInFile && fv >= 3) {
glm::vec3 v0 = faceVerts[0].pos;
glm::vec3 v1 = faceVerts[1].pos;
glm::vec3 v2 = faceVerts[2].pos;
glm::vec3 faceNormal = glm::normalize(glm::cross(v1 - v0, v2 - v0));
for (auto& tv : faceVerts) {
tv.normal = faceNormal;
tv.hasNormal = true;
}
}
// Triangulate fan
for (int v = 1; v < fv - 1; v++) {
const TempVertex* tri[3] = { &faceVerts[0], &faceVerts[v], &faceVerts[v+1] };
for (int i = 0; i < 3; i++) {
// Pos
vertices.push_back(tri[i]->pos.x);
vertices.push_back(tri[i]->pos.y);
vertices.push_back(tri[i]->pos.z);
// Normal
vertices.push_back(tri[i]->normal.x);
vertices.push_back(tri[i]->normal.y);
vertices.push_back(tri[i]->normal.z);
// UV
vertices.push_back(tri[i]->uv.x);
vertices.push_back(tri[i]->uv.y);
}
}
indexOffset += fv;
}
}
if (vertices.empty()) {
errorMsg += "No vertices found in OBJ file";
return -1;
}
// Create mesh
LoadedMesh loaded;
loaded.path = filepath;
loaded.name = fs::path(filepath).stem().string();
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
loaded.faceCount = faceCount;
loaded.hasNormals = hasNormalsInFile;
loaded.hasTexCoords = !attrib.texcoords.empty();
loadedMeshes.push_back(std::move(loaded));
return static_cast<int>(loadedMeshes.size() - 1);
}
// Get mesh by index
Mesh* getMesh(int index) {
if (index < 0 || index >= static_cast<int>(loadedMeshes.size())) {
return nullptr;
}
return loadedMeshes[index].mesh.get();
}
// Get mesh info
const LoadedMesh* getMeshInfo(int index) const {
if (index < 0 || index >= static_cast<int>(loadedMeshes.size())) {
return nullptr;
}
return &loadedMeshes[index];
}
// Get all loaded meshes (for UI display)
const std::vector<LoadedMesh>& getAllMeshes() const {
return loadedMeshes;
}
// Clear all loaded meshes
void clear() {
loadedMeshes.clear();
}
size_t getMeshCount() const { return loadedMeshes.size(); }
};
// Global OBJ loader instance
OBJLoader g_objLoader;
class Camera {
public:
glm::vec3 position = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 front = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 velocity = glm::vec3(0.0f);
float yaw = -90.0f;
float pitch = 0.0f;
float speed = CAMERA_SPEED;
float lastX = 400.0f, lastY = 300.0f;
bool firstMouse = true;
void processMouse(double xpos, double ypos) {
if (ImGuizmo::IsUsing() || ImGuizmo::IsOver()) {
// Don't process camera movement when gizmo is active
return;
}
if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = (xpos - lastX) * SENSITIVITY;
float yoffset = (lastY - ypos) * SENSITIVITY;
lastX = xpos;
lastY = ypos;
yaw += xoffset;
pitch += yoffset;
if (pitch > 89.0f) pitch = 89.0f;
if (pitch < -89.0f) pitch = -89.0f;
glm::vec3 direction;
direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
front = glm::normalize(direction);
}
void processKeyboard(float deltaTime, GLFWwindow* window) {
const float CAMERA_SPEED = 5.0f;
const float SPRINT_SPEED = 10.0f;
const float ACCELERATION = 15.0f;
float currentSpeed = CAMERA_SPEED;
if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) {
currentSpeed = SPRINT_SPEED;
}
glm::vec3 desiredDir(0.0f);
bool isMoving = false;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
desiredDir += front;
isMoving = true;
}
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
desiredDir -= front;
isMoving = true;
}
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) {
desiredDir -= glm::normalize(glm::cross(front, up));
isMoving = true;
}
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
desiredDir += glm::normalize(glm::cross(front, up));
isMoving = true;
}
if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) {
desiredDir -= up;
isMoving = true;
}
if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) {
desiredDir += up;
isMoving = true;
}
if (glfwGetKey(window, GLFW_KEY_1) == GLFW_PRESS) {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}
if (glfwGetKey(window, GLFW_KEY_2) == GLFW_PRESS) {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
glm::vec3 targetVelocity(0.0f);
if (isMoving) {
float length = glm::length(desiredDir);
if (length > 0.0001f) {
desiredDir = desiredDir / length;
targetVelocity = desiredDir * currentSpeed;
} else {
targetVelocity = glm::vec3(0.0f);
}
}
float smoothFactor = 1.0f - std::exp(-ACCELERATION * deltaTime);
velocity = glm::mix(velocity, targetVelocity, smoothFactor);
position += velocity * deltaTime;
}
glm::mat4 getViewMatrix() const {
return glm::lookAt(position, position + front, up);
}
};
class ViewportController {
private:
bool viewportFocused = false;
bool manualUnfocus = false;
public:
void updateFocusFromImGui(bool windowFocused) {
if (!windowFocused && viewportFocused && !manualUnfocus) {
viewportFocused = false;
}
}
void setFocused(bool focused) {
viewportFocused = focused;
}
bool isViewportFocused() const {
return viewportFocused;
}
void clearManualUnfocus() {
manualUnfocus = false;
}
void update(GLFWwindow* window, bool& cursorLocked) {
if (viewportFocused && glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
viewportFocused = false;
manualUnfocus = true;
cursorLocked = false;
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}
}
};
class Renderer {
private:
unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0;
int currentWidth = 800, currentHeight = 600;
Shader* shader = nullptr;
Texture* texture1 = nullptr;
Texture* texture2 = nullptr;
Mesh* cubeMesh = nullptr;
Mesh* sphereMesh = nullptr;
Mesh* capsuleMesh = nullptr;
Skybox* skybox = nullptr;
public:
Renderer() = default;
~Renderer() {
delete shader;
delete texture1;
delete texture2;
delete cubeMesh;
delete sphereMesh;
delete capsuleMesh;
delete skybox;
if (framebuffer) glDeleteFramebuffers(1, &framebuffer);
if (viewportTexture) glDeleteTextures(1, &viewportTexture);
if (rbo) glDeleteRenderbuffers(1, &rbo);
}
void initialize() {
shader = new Shader("Resources/Shaders/vert.glsl", "Resources/Shaders/frag.glsl");
if (shader->ID == 0) {
std::cerr << "Shader compilation failed!\n";
delete shader;
shader = nullptr;
throw std::runtime_error("Shader error");
}
texture1 = new Texture("Resources/Textures/container.jpg");
texture2 = new Texture("Resources/Textures/awesomeface.png");
cubeMesh = new Mesh(vertices, sizeof(vertices));
auto sphereVerts = generateSphere();
sphereMesh = new Mesh(sphereVerts.data(), sphereVerts.size() * sizeof(float));
auto capsuleVerts = generateCapsule();
capsuleMesh = new Mesh(capsuleVerts.data(), capsuleVerts.size() * sizeof(float));
skybox = new Skybox();
setupFBO();
glEnable(GL_DEPTH_TEST);
}
void resize(int w, int h) {
if (w <= 0 || h <= 0 || (w == currentWidth && h == currentHeight)) return;
std::cout << "RESIZE TRIGGERED: " << w << "x" << h << std::endl;
GLint currentFB;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &currentFB);
std::cout << "Framebuffer bound during resize: " << currentFB << " (should be " << framebuffer << ")" << std::endl;
currentWidth = w;
currentHeight = h;
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glBindTexture(GL_TEXTURE_2D, viewportTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, currentWidth, currentHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, currentWidth, currentHeight);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Framebuffer incomplete after resize!\n";
}
std::cout << "Resize complete" << std::endl;
}
int getWidth() const { return currentWidth; }
int getHeight() const { return currentHeight; }
void beginRender(const glm::mat4& view, const glm::mat4& proj) {
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, currentWidth, currentHeight);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader->use();
shader->setMat4("view", view);
shader->setMat4("projection", proj);
texture1->Bind(GL_TEXTURE0);
texture2->Bind(GL_TEXTURE1);
shader->setInt("texture1", 0);
shader->setInt("texture2", 1);
}
void renderSkybox(const glm::mat4& view, const glm::mat4& proj) {
if (skybox) {
GLint currentFB;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &currentFB);
glDepthFunc(GL_LEQUAL);
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
glDepthFunc(GL_LESS);
GLint afterFB;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &afterFB);
if (currentFB != afterFB) {
std::cerr << "WARNING: Framebuffer changed during skybox render! "
<< currentFB << " -> " << afterFB << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
}
shader->use();
shader->setMat4("view", view);
shader->setMat4("projection", proj);
shader->use();
}
}
Skybox* getSkybox() { return skybox; }
void renderObject(const SceneObject& obj) {
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
model = glm::scale(model, obj.scale);
shader->setMat4("model", model);
switch (obj.type) {
case ObjectType::Cube:
cubeMesh->draw();
break;
case ObjectType::Sphere:
sphereMesh->draw();
break;
case ObjectType::Capsule:
capsuleMesh->draw();
break;
case ObjectType::OBJMesh:
// Draw loaded OBJ mesh
if (obj.meshId >= 0) {
Mesh* objMesh = g_objLoader.getMesh(obj.meshId);
if (objMesh) {
objMesh->draw();
}
}
break;
}
}
void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects) {
shader->use();
shader->setMat4("view", camera.getViewMatrix());
shader->setMat4("projection", glm::perspective(glm::radians(FOV), (float)currentWidth / (float)currentHeight, NEAR_PLANE, FAR_PLANE));
shader->setVec3("lightPos", glm::vec3(4.0f, 6.0f, 4.0f)); // Slightly higher and farther
shader->setVec3("lightColor", glm::vec3(1.0f, 1.0f, 1.0f));
shader->setFloat("ambientStrength", 0.25f);
shader->setFloat("specularStrength", 0.8f);
shader->setFloat("shininess", 64.0f);
shader->setFloat("mixAmount", 0.3f);
texture1->Bind(0);
texture2->Bind(1);
for (const auto& obj : sceneObjects) {
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, obj.scale);
shader->setMat4("model", model);
Mesh* meshToDraw = nullptr;
if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh;
else if (obj.type == ObjectType::Sphere) meshToDraw = sphereMesh;
else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh;
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) {
meshToDraw = g_objLoader.getMesh(obj.meshId);
}
if (meshToDraw) {
meshToDraw->draw();
}
}
// Skybox last
if (skybox) {
glm::mat4 view = camera.getViewMatrix();
glm::mat4 proj = glm::perspective(glm::radians(FOV),
(float)currentWidth / currentHeight,
NEAR_PLANE, FAR_PLANE);
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void endRender() {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
unsigned int getViewportTexture() const { return viewportTexture; }
private:
void setupFBO() {
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glGenTextures(1, &viewportTexture);
glBindTexture(GL_TEXTURE_2D, viewportTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, currentWidth, currentHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, viewportTexture, 0);
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, currentWidth, currentHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Framebuffer setup failed!\n";
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
};
struct RecentProject {
std::string name;
std::string path;
std::string lastOpened;
};
class Project {
public:
std::string name;
fs::path projectPath;
fs::path scenesPath;
fs::path assetsPath;
fs::path scriptsPath;
std::string currentSceneName;
bool isLoaded = false;
bool hasUnsavedChanges = false;
Project() = default;
Project(const std::string& projectName, const fs::path& basePath)
: name(projectName) {
projectPath = basePath / projectName;
scenesPath = projectPath / "Scenes";
assetsPath = projectPath / "Assets";
scriptsPath = projectPath / "Scripts";
}
bool create() {
try {
fs::create_directories(projectPath);
fs::create_directories(scenesPath);
fs::create_directories(assetsPath);
fs::create_directories(assetsPath / "Textures");
fs::create_directories(assetsPath / "Models");
fs::create_directories(assetsPath / "Shaders");
fs::create_directories(scriptsPath);
saveProjectFile();
currentSceneName = "Main";
isLoaded = true;
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to create project: " << e.what() << std::endl;
return false;
}
}
bool load(const fs::path& projectFilePath) {
try {
projectPath = projectFilePath.parent_path();
scenesPath = projectPath / "Scenes";
assetsPath = projectPath / "Assets";
scriptsPath = projectPath / "Scripts";
std::ifstream file(projectFilePath);
if (!file.is_open()) return false;
std::string line;
while (std::getline(file, line)) {
if (line.find("name=") == 0) {
name = line.substr(5);
} else if (line.find("lastScene=") == 0) {
currentSceneName = line.substr(10);
}
}
file.close();
if (currentSceneName.empty()) {
currentSceneName = "Main";
}
isLoaded = true;
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to load project: " << e.what() << std::endl;
return false;
}
}
void saveProjectFile() const {
std::ofstream file(projectPath / "project.modu");
file << "name=" << name << "\n";
file << "lastScene=" << currentSceneName << "\n";
file.close();
}
std::vector<std::string> getSceneList() const {
std::vector<std::string> scenes;
try {
for (const auto& entry : fs::directory_iterator(scenesPath)) {
if (entry.path().extension() == ".scene") {
scenes.push_back(entry.path().stem().string());
}
}
} catch (...) {}
return scenes;
}
fs::path getSceneFilePath(const std::string& sceneName) const {
return scenesPath / (sceneName + ".scene");
}
};
class ProjectManager {
public:
std::vector<RecentProject> recentProjects;
fs::path appDataPath;
char newProjectName[128] = "";
char newProjectLocation[512] = "";
char openProjectPath[512] = "";
bool showNewProjectDialog = false;
bool showOpenProjectDialog = false;
std::string errorMessage;
Project currentProject;
ProjectManager() {
#ifdef _WIN32
const char* appdata = std::getenv("APPDATA");
if (appdata) {
appDataPath = fs::path(appdata) / ".Modularity";
} else {
appDataPath = fs::current_path() / "AppData";
}
#else
const char* home = std::getenv("HOME");
if (home) {
appDataPath = fs::path(home) / ".Modularity";
} else {
appDataPath = fs::current_path() / ".Modularity";
}
#endif
fs::create_directories(appDataPath);
loadRecentProjects();
std::string defaultPath = (fs::current_path() / "Projects").string();
strncpy(newProjectLocation, defaultPath.c_str(), sizeof(newProjectLocation) - 1);
}
void loadRecentProjects() {
recentProjects.clear();
fs::path recentFile = appDataPath / "recent_projects.txt";
if (!fs::exists(recentFile)) return;
std::ifstream file(recentFile);
std::string line;
while (std::getline(file, line)) {
if (line.empty()) continue;
RecentProject rp;
size_t pos1 = line.find('|');
size_t pos2 = line.find('|', pos1 + 1);
if (pos1 != std::string::npos && pos2 != std::string::npos) {
rp.name = line.substr(0, pos1);
rp.path = line.substr(pos1 + 1, pos2 - pos1 - 1);
rp.lastOpened = line.substr(pos2 + 1);
if (fs::exists(rp.path)) {
recentProjects.push_back(rp);
}
}
}
file.close();
}
void saveRecentProjects() {
fs::path recentFile = appDataPath / "recent_projects.txt";
std::ofstream file(recentFile);
for (const auto& rp : recentProjects) {
file << rp.name << "|" << rp.path << "|" << rp.lastOpened << "\n";
}
file.close();
}
void addToRecentProjects(const std::string& name, const std::string& path) {
recentProjects.erase(
std::remove_if(recentProjects.begin(), recentProjects.end(),
[&path](const RecentProject& rp) { return rp.path == path; }),
recentProjects.end()
);
std::time_t now = std::time(nullptr);
char timeStr[64];
std::strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M", std::localtime(&now));
RecentProject rp;
rp.name = name;
rp.path = path;
rp.lastOpened = timeStr;
recentProjects.insert(recentProjects.begin(), rp);
if (recentProjects.size() > 10) {
recentProjects.resize(10);
}
saveRecentProjects();
}
bool loadProject(const std::string& projectPath) {
fs::path path(projectPath);
if (fs::is_directory(path)) {
path = path / "project.modu";
}
if (!fs::exists(path)) {
errorMessage = "Project file not found.";
return false;
}
Project loadedProject;
if (loadedProject.load(path)) {
currentProject = loadedProject;
addToRecentProjects(currentProject.name, path.string());
return true;
} else {
errorMessage = "Failed to load project.";
return false;
}
}
};
class SceneSerializer {
public:
static bool saveScene(const fs::path& filePath,
const std::vector<SceneObject>& objects,
int nextId) {
try {
std::ofstream file(filePath);
if (!file.is_open()) return false;
file << "# Scene File\n";
file << "version=2\n"; // Bumped version for new format
file << "nextId=" << nextId << "\n";
file << "objectCount=" << objects.size() << "\n";
file << "\n";
for (const auto& obj : objects) {
file << "[Object]\n";
file << "id=" << obj.id << "\n";
file << "name=" << obj.name << "\n";
file << "type=" << static_cast<int>(obj.type) << "\n";
file << "parentId=" << obj.parentId << "\n";
file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n";
file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n";
file << "scale=" << obj.scale.x << "," << obj.scale.y << "," << obj.scale.z << "\n";
// Save mesh path for OBJ meshes
if (obj.type == ObjectType::OBJMesh && !obj.meshPath.empty()) {
file << "meshPath=" << obj.meshPath << "\n";
}
file << "children=";
for (size_t i = 0; i < obj.childIds.size(); i++) {
if (i > 0) file << ",";
file << obj.childIds[i];
}
file << "\n\n";
}
file.close();
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to save scene: " << e.what() << std::endl;
return false;
}
}
static bool loadScene(const fs::path& filePath,
std::vector<SceneObject>& objects,
int& nextId) {
try {
std::ifstream file(filePath);
if (!file.is_open()) return false;
objects.clear();
std::string line;
SceneObject* currentObj = nullptr;
while (std::getline(file, line)) {
line.erase(0, line.find_first_not_of(" \t\r\n"));
line.erase(line.find_last_not_of(" \t\r\n") + 1);
if (line.empty() || line[0] == '#') continue;
if (line == "[Object]") {
objects.push_back(SceneObject("", ObjectType::Cube, 0));
currentObj = &objects.back();
continue;
}
size_t eqPos = line.find('=');
if (eqPos == std::string::npos) continue;
std::string key = line.substr(0, eqPos);
std::string value = line.substr(eqPos + 1);
if (key == "nextId") {
nextId = std::stoi(value);
} else if (currentObj) {
if (key == "id") {
currentObj->id = std::stoi(value);
} else if (key == "name") {
currentObj->name = value;
} else if (key == "type") {
currentObj->type = static_cast<ObjectType>(std::stoi(value));
} else if (key == "parentId") {
currentObj->parentId = std::stoi(value);
} else if (key == "position") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->position.x,
&currentObj->position.y,
&currentObj->position.z);
} else if (key == "rotation") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->rotation.x,
&currentObj->rotation.y,
&currentObj->rotation.z);
} else if (key == "scale") {
sscanf(value.c_str(), "%f,%f,%f",
&currentObj->scale.x,
&currentObj->scale.y,
&currentObj->scale.z);
} else if (key == "meshPath") {
currentObj->meshPath = value;
// Reload the mesh
if (!value.empty() && currentObj->type == ObjectType::OBJMesh) {
std::string err;
currentObj->meshId = g_objLoader.loadOBJ(value, err);
}
} else if (key == "children" && !value.empty()) {
std::stringstream ss(value);
std::string item;
while (std::getline(ss, item, ',')) {
if (!item.empty()) {
currentObj->childIds.push_back(std::stoi(item));
}
}
}
}
}
file.close();
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to load scene: " << e.what() << std::endl;
return false;
}
}
};
void window_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
class Engine;
class Engine {
private:
Window window;
GLFWwindow* editorWindow = nullptr;
Renderer renderer;
Camera camera;
ViewportController viewportController;
float deltaTime = 0.0f;
float lastFrame = 0.0f;
bool cursorLocked = false;
int viewportWidth = 800;
int viewportHeight = 600;
std::vector<SceneObject> sceneObjects;
int selectedObjectId = -1;
int nextObjectId = 0;
SceneObject* getSelectedObject() {
if (selectedObjectId == -1) return nullptr;
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
return (it != sceneObjects.end()) ? &(*it) : nullptr;
}
static void DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale) {
// Extract translation
pos = glm::vec3(matrix[3]);
// Extract scale
scale.x = glm::length(glm::vec3(matrix[0]));
scale.y = glm::length(glm::vec3(matrix[1]));
scale.z = glm::length(glm::vec3(matrix[2]));
// Extract rotation (convert to Euler angles)
glm::mat3 rotMat(matrix);
if (scale.x != 0.0f) rotMat[0] /= scale.x;
if (scale.y != 0.0f) rotMat[1] /= scale.y;
if (scale.z != 0.0f) rotMat[2] /= scale.z;
rot = glm::eulerAngles(glm::quat_cast(rotMat));
}
// Gizmo state
ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::LOCAL;
bool useSnap = false;
float snapValue[3] = { 0.5f, 0.5f, 0.5f };
float rotationSnapValue = 15.0f;
FileBrowser fileBrowser;
bool viewportFullscreen = false;
bool showHierarchy = true;
bool showInspector = true;
bool showFileBrowser = true;
bool showConsole = true;
bool showProjectBrowser = true;
bool firstFrame = true;
std::vector<std::string> consoleLog;
int draggedObjectId = -1;
ProjectManager projectManager;
bool showLauncher = true;
bool showNewSceneDialog = false;
bool showSaveSceneAsDialog = false;
char newSceneName[128] = "";
char saveSceneAsName[128] = "";
bool rendererInitialized = false;
bool showImportOBJDialog = false;
std::string pendingOBJPath;
char importOBJName[128] = "";
public:
Engine() = default;
bool init() {
editorWindow = window.makeWindow();
if (!editorWindow) return false;
glfwSetWindowUserPointer(editorWindow, this);
glfwSetWindowSizeCallback(editorWindow, window_size_callback);
auto mouse_cb = [](GLFWwindow* window, double xpos, double ypos) {
auto* engine = static_cast<Engine*>(glfwGetWindowUserPointer(window));
if (!engine) return;
int cursorMode = glfwGetInputMode(window, GLFW_CURSOR);
if (!engine->viewportController.isViewportFocused() || cursorMode != GLFW_CURSOR_DISABLED) {
return;
}
engine->camera.processMouse(xpos, ypos);
};
glfwSetCursorPosCallback(editorWindow, mouse_cb);
setupImGui();
logToConsole("Engine initialized - Waiting for project selection");
return true;
}
bool initRenderer() {
if (rendererInitialized) return true;
try {
renderer.initialize();
rendererInitialized = true;
return true;
} catch (...) {
return false;
}
}
void run() {
while (!glfwWindowShouldClose(editorWindow)) {
if (glfwGetWindowAttrib(editorWindow, GLFW_ICONIFIED)) {
ImGui_ImplGlfw_Sleep(10);
continue;
}
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
deltaTime = std::min(deltaTime, 1.0f / 30.0f);
glfwPollEvents();
if (!showLauncher) {
handleKeyboardShortcuts();
}
viewportController.update(editorWindow, cursorLocked);
if (!viewportController.isViewportFocused() && cursorLocked) {
cursorLocked = false;
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
camera.firstMouse = true;
}
if (viewportController.isViewportFocused() && cursorLocked) {
camera.processKeyboard(deltaTime, editorWindow);
}
if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) {
glm::mat4 view = camera.getViewMatrix();
float aspect = static_cast<float>(viewportWidth) / static_cast<float>(viewportHeight);
if (aspect <= 0.0f) aspect = 1.0f;
glm::mat4 proj = glm::perspective(glm::radians(FOV), aspect, NEAR_PLANE, FAR_PLANE);
renderer.beginRender(view, proj);
for (const auto& obj : sceneObjects) {
renderer.renderObject(obj);
}
renderer.renderSkybox(view, proj);
renderer.endRender();
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
if (showLauncher) {
renderLauncher();
} else {
setupDockspace();
renderMainMenuBar();
if (!viewportFullscreen) {
if (showHierarchy) renderHierarchyPanel();
if (showInspector) renderInspectorPanel();
if (showFileBrowser) renderFileBrowserPanel();
if (showConsole) renderConsolePanel();
if (showProjectBrowser) renderProjectBrowserPanel();
}
renderViewport();
renderDialogs();
}
int displayW, displayH;
glfwGetFramebufferSize(editorWindow, &displayW, &displayH);
glViewport(0, 0, displayW, displayH);
glClearColor(0.1f, 0.1f, 0.12f, 1.00f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
GLFWwindow* backup_current_context = glfwGetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
glfwMakeContextCurrent(backup_current_context);
}
glfwSwapBuffers(editorWindow);
firstFrame = false;
}
}
void shutdown() {
if (projectManager.currentProject.isLoaded && projectManager.currentProject.hasUnsavedChanges) {
saveCurrentScene();
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwTerminate();
}
private:
void importOBJToScene(const std::string& filepath, const std::string& objectName) {
std::string errorMsg;
int meshId = g_objLoader.loadOBJ(filepath, errorMsg);
if (meshId < 0) {
addConsoleMessage("Failed to load OBJ: " + errorMsg, ConsoleMessageType::Error);
return;
}
// Create scene object
int id = nextObjectId++;
std::string name = objectName.empty() ? fs::path(filepath).stem().string() : objectName;
SceneObject obj(name, ObjectType::OBJMesh, id);
obj.meshPath = filepath;
obj.meshId = meshId;
sceneObjects.push_back(obj);
selectedObjectId = id;
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}
const auto* meshInfo = g_objLoader.getMeshInfo(meshId);
if (meshInfo) {
addConsoleMessage("Imported OBJ: " + name + " (" +
std::to_string(meshInfo->vertexCount) + " vertices, " +
std::to_string(meshInfo->faceCount) + " faces)",
ConsoleMessageType::Success);
} else {
addConsoleMessage("Imported OBJ: " + name, ConsoleMessageType::Success);
}
}
void handleKeyboardShortcuts() {
static bool f11Pressed = false;
if (glfwGetKey(editorWindow, GLFW_KEY_F11) == GLFW_PRESS && !f11Pressed) {
viewportFullscreen = !viewportFullscreen;
f11Pressed = true;
}
if (glfwGetKey(editorWindow, GLFW_KEY_F11) == GLFW_RELEASE) {
f11Pressed = false;
}
static bool ctrlSPressed = false;
bool ctrlDown = glfwGetKey(editorWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
glfwGetKey(editorWindow, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS;
if (ctrlDown && glfwGetKey(editorWindow, GLFW_KEY_S) == GLFW_PRESS && !ctrlSPressed) {
if (projectManager.currentProject.isLoaded) {
saveCurrentScene();
}
ctrlSPressed = true;
}
if (glfwGetKey(editorWindow, GLFW_KEY_S) == GLFW_RELEASE) {
ctrlSPressed = false;
}
static bool ctrlNPressed = false;
if (ctrlDown && glfwGetKey(editorWindow, GLFW_KEY_N) == GLFW_PRESS && !ctrlNPressed) {
if (projectManager.currentProject.isLoaded) {
showNewSceneDialog = true;
memset(newSceneName, 0, sizeof(newSceneName));
}
ctrlNPressed = true;
}
if (glfwGetKey(editorWindow, GLFW_KEY_N) == GLFW_RELEASE) {
ctrlNPressed = false;
}
// Gizmo operation hotkeys
if (ImGui::IsKeyPressed(ImGuiKey_Q)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
if (ImGui::IsKeyPressed(ImGuiKey_W)) mCurrentGizmoOperation = ImGuizmo::ROTATE;
if (ImGui::IsKeyPressed(ImGuiKey_E)) mCurrentGizmoOperation = ImGuizmo::SCALE;
if (ImGui::IsKeyPressed(ImGuiKey_R)) mCurrentGizmoOperation = ImGuizmo::UNIVERSAL;
// Local / World toggle
if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
mCurrentGizmoMode = (mCurrentGizmoMode == ImGuizmo::LOCAL) ? ImGuizmo::WORLD : ImGuizmo::LOCAL;
}
// Snap toggle
if (ImGui::IsKeyPressed(ImGuiKey_LeftCtrl)) useSnap = !useSnap;
}
void OpenProjectPath(const std::string& path) {
if (projectManager.loadProject(path)) {
if (!initRenderer()) {
addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error);
} else {
showLauncher = false;
loadRecentScenes();
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
}
} else {
addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error);
}
}
void renderLauncher() {
ImGuiIO& io = ImGui::GetIO();
ImVec2 displaySize = io.DisplaySize;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.09f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(displaySize);
ImGuiWindowFlags flags =
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoBringToFrontOnFocus;
if (ImGui::Begin("Launcher", nullptr, flags))
{
float leftPanelWidth = 280.0f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.06f, 0.06f, 0.07f, 1.0f));
ImGui::BeginChild("LauncherLeft", ImVec2(leftPanelWidth, 0), true);
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.45f, 0.72f, 0.95f, 1.0f), "MODULARITY");
ImGui::TextDisabled("Game Engine");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "GET STARTED");
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.38f, 0.55f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.48f, 0.68f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.20f, 0.42f, 0.60f, 1.0f));
if (ImGui::Button("New Project", ImVec2(-1, 36.0f)))
{
projectManager.showNewProjectDialog = true;
projectManager.errorMessage.clear();
std::memset(projectManager.newProjectName, 0, sizeof(projectManager.newProjectName));
#ifdef _WIN32
char documentsPath[MAX_PATH];
SHGetFolderPathA(NULL, CSIDL_MYDOCUMENTS, NULL, 0, documentsPath);
std::strcpy(projectManager.newProjectLocation, documentsPath);
std::strcat(projectManager.newProjectLocation, "\\ModularityProjects");
#else
const char* home = std::getenv("HOME");
if (home)
{
std::strcpy(projectManager.newProjectLocation, home);
std::strcat(projectManager.newProjectLocation, "/ModularityProjects");
}
#endif
}
ImGui::Spacing();
if (ImGui::Button("Open Project", ImVec2(-1, 36.0f)))
{
projectManager.showOpenProjectDialog = true;
projectManager.errorMessage.clear();
}
ImGui::PopStyleColor(3);
ImGui::Spacing();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "QUICK ACTIONS");
ImGui::Spacing();
if (ImGui::Button("Documentation", ImVec2(-1, 30.0f)))
{
#ifdef _WIN32
system("start https://github.com");
#endif
}
if (ImGui::Button("Exit", ImVec2(-1, 30.0f)))
{
glfwSetWindowShouldClose(editorWindow, GLFW_TRUE);
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.11f, 1.0f));
ImGui::BeginChild("LauncherRight", ImVec2(0, 0), true);
ImGui::PopStyleColor();
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "RECENT PROJECTS");
ImGui::Spacing();
if (projectManager.recentProjects.empty())
{
ImGui::Spacing();
ImGui::TextDisabled("No recent projects");
ImGui::TextDisabled("Create a new project to get started!");
}
else
{
for (size_t i = 0; i < projectManager.recentProjects.size(); ++i)
{
const auto& rp = projectManager.recentProjects[i];
ImGui::PushID(static_cast<int>(i));
char label[512];
std::snprintf(label, sizeof(label), "%s\n%s",
rp.name.c_str(), rp.path.c_str());
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.20f, 0.30f, 0.45f, 0.40f));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.25f, 0.38f, 0.55f, 0.70f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.20f, 0.35f, 0.60f, 0.90f));
bool selected = ImGui::Selectable(
label,
false,
ImGuiSelectableFlags_AllowDoubleClick,
ImVec2(0.0f, 48.0f)
);
ImGui::PopStyleColor(3);
if (selected || ImGui::IsItemClicked(ImGuiMouseButton_Left))
{
OpenProjectPath(rp.path);
}
if (ImGui::BeginPopupContextItem("RecentProjectContext"))
{
if (ImGui::MenuItem("Open"))
{
OpenProjectPath(rp.path);
}
if (ImGui::MenuItem("Remove from Recent"))
{
projectManager.recentProjects.erase(
projectManager.recentProjects.begin() + i
);
projectManager.saveRecentProjects();
ImGui::EndPopup();
ImGui::PopID();
break;
}
ImGui::EndPopup();
}
ImGui::PopID();
ImGui::Spacing();
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextDisabled("Modularity Engine - Version 1.0.1");
ImGui::EndChild();
}
ImGui::End();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(3);
if (projectManager.showNewProjectDialog)
renderNewProjectDialog();
if (projectManager.showOpenProjectDialog)
renderOpenProjectDialog();
}
void renderNewProjectDialog() {
ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(500, 250), ImGuiCond_Appearing);
if (ImGui::Begin("New Project", &projectManager.showNewProjectDialog,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
ImGui::Text("Project Name:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##ProjectName", projectManager.newProjectName,
sizeof(projectManager.newProjectName));
ImGui::Spacing();
ImGui::Text("Location:");
ImGui::SetNextItemWidth(-70);
ImGui::InputText("##Location", projectManager.newProjectLocation,
sizeof(projectManager.newProjectLocation));
ImGui::SameLine();
if (ImGui::Button("Browse")) {
}
ImGui::Spacing();
if (strlen(projectManager.newProjectName) > 0) {
fs::path previewPath = fs::path(projectManager.newProjectLocation) /
projectManager.newProjectName;
ImGui::TextDisabled("Project will be created at:");
ImGui::TextWrapped("%s", previewPath.string().c_str());
}
if (!projectManager.errorMessage.empty()) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
projectManager.errorMessage.c_str());
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float buttonWidth = 100;
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
projectManager.showNewProjectDialog = false;
memset(projectManager.newProjectName, 0, sizeof(projectManager.newProjectName));
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.4f, 1.0f));
if (ImGui::Button("Create", ImVec2(buttonWidth, 0))) {
if (strlen(projectManager.newProjectName) == 0) {
projectManager.errorMessage = "Please enter a project name";
} else if (strlen(projectManager.newProjectLocation) == 0) {
projectManager.errorMessage = "Please specify a location";
} else {
createNewProject(projectManager.newProjectName,
projectManager.newProjectLocation);
projectManager.showNewProjectDialog = false;
}
}
ImGui::PopStyleColor(2);
}
ImGui::End();
}
void renderOpenProjectDialog() {
ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Appearing);
if (ImGui::Begin("Open Project", &projectManager.showOpenProjectDialog,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
ImGui::Text("Project File Path (.modu):");
ImGui::SetNextItemWidth(-70);
ImGui::InputText("##OpenPath", projectManager.openProjectPath,
sizeof(projectManager.openProjectPath));
ImGui::SameLine();
if (ImGui::Button("Browse")) {
}
ImGui::TextDisabled("Select a project.modu file");
if (!projectManager.errorMessage.empty()) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
projectManager.errorMessage.c_str());
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float buttonWidth = 100;
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
projectManager.showOpenProjectDialog = false;
memset(projectManager.openProjectPath, 0, sizeof(projectManager.openProjectPath));
}
ImGui::SameLine();
if (ImGui::Button("Open", ImVec2(buttonWidth, 0))) {
if (strlen(projectManager.openProjectPath) == 0) {
projectManager.errorMessage = "Please enter a project path";
} else {
if (projectManager.loadProject(projectManager.openProjectPath)) {
if (!initRenderer()) {
addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error);
} else {
showLauncher = false;
loadRecentScenes();
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
}
} else {
addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error);
}
}
}
}
ImGui::End();
}
void createNewProject(const char* name, const char* location) {
fs::path basePath(location);
fs::create_directories(basePath);
Project newProject(name, basePath);
if (newProject.create()) {
projectManager.currentProject = newProject;
projectManager.addToRecentProjects(name,
(newProject.projectPath / "project.modu").string());
if (!initRenderer()) {
logToConsole("Error: Failed to initialize renderer!");
return;
}
sceneObjects.clear();
selectedObjectId = -1;
nextObjectId = 0;
addObject(ObjectType::Cube, "Cube");
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
fileBrowser.needsRefresh = true;
showLauncher = false;
firstFrame = true;
addConsoleMessage("Created new project: " + std::string(name), ConsoleMessageType::Success);
addConsoleMessage("Project location: " + newProject.projectPath.string(), ConsoleMessageType::Info);
saveCurrentScene();
} else {
projectManager.errorMessage = "Failed to create project directory";
}
}
void loadRecentScenes() {
sceneObjects.clear();
selectedObjectId = -1;
nextObjectId = 0;
fs::path scenePath = projectManager.currentProject.getSceneFilePath(projectManager.currentProject.currentSceneName);
if (fs::exists(scenePath)) {
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId)) {
addConsoleMessage("Loaded scene: " + projectManager.currentProject.currentSceneName, ConsoleMessageType::Success);
} else {
addConsoleMessage("Warning: Failed to load scene, starting fresh", ConsoleMessageType::Warning);
addObject(ObjectType::Cube, "Cube");
}
} else {
addConsoleMessage("Default scene not found, starting with a new scene.", ConsoleMessageType::Info);
addObject(ObjectType::Cube, "Cube");
}
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
fileBrowser.needsRefresh = true;
}
void saveCurrentScene() {
if (!projectManager.currentProject.isLoaded) return;
fs::path scenePath = projectManager.currentProject.getSceneFilePath(projectManager.currentProject.currentSceneName);
if (SceneSerializer::saveScene(scenePath, sceneObjects, nextObjectId)) {
projectManager.currentProject.hasUnsavedChanges = false;
projectManager.currentProject.saveProjectFile();
addConsoleMessage("Saved scene: " + projectManager.currentProject.currentSceneName, ConsoleMessageType::Success);
} else {
addConsoleMessage("Error: Failed to save scene!", ConsoleMessageType::Error);
}
}
void loadScene(const std::string& sceneName) {
if (!projectManager.currentProject.isLoaded) return;
if (projectManager.currentProject.hasUnsavedChanges) {
saveCurrentScene();
}
fs::path scenePath = projectManager.currentProject.getSceneFilePath(sceneName);
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId)) {
projectManager.currentProject.currentSceneName = sceneName;
projectManager.currentProject.hasUnsavedChanges = false;
projectManager.currentProject.saveProjectFile();
selectedObjectId = -1;
addConsoleMessage("Loaded scene: " + sceneName, ConsoleMessageType::Success);
} else {
addConsoleMessage("Error: Failed to load scene: " + sceneName, ConsoleMessageType::Error);
}
}
void createNewScene(const std::string& sceneName) {
if (!projectManager.currentProject.isLoaded || sceneName.empty()) return;
if (projectManager.currentProject.hasUnsavedChanges) {
saveCurrentScene();
}
sceneObjects.clear();
selectedObjectId = -1;
nextObjectId = 0;
projectManager.currentProject.currentSceneName = sceneName;
projectManager.currentProject.hasUnsavedChanges = true;
addObject(ObjectType::Cube, "Cube");
saveCurrentScene();
addConsoleMessage("Created new scene: " + sceneName, ConsoleMessageType::Success);
}
void renderDialogs() {
if (showNewSceneDialog) {
ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(350, 130), ImGuiCond_Appearing);
if (ImGui::Begin("New Scene", &showNewSceneDialog,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
ImGui::Text("Scene Name:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##NewSceneName", newSceneName, sizeof(newSceneName));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float buttonWidth = 80;
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
showNewSceneDialog = false;
}
ImGui::SameLine();
if (ImGui::Button("Create", ImVec2(buttonWidth, 0))) {
if (strlen(newSceneName) > 0) {
createNewScene(newSceneName);
showNewSceneDialog = false;
memset(newSceneName, 0, sizeof(newSceneName));
}
}
}
ImGui::End();
}
if (showSaveSceneAsDialog) {
ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(350, 130), ImGuiCond_Appearing);
if (ImGui::Begin("Save Scene As", &showSaveSceneAsDialog,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
ImGui::Text("Scene Name:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##SaveSceneAsName", saveSceneAsName, sizeof(saveSceneAsName));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float buttonWidth = 80;
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
showSaveSceneAsDialog = false;
}
ImGui::SameLine();
if (ImGui::Button("Save", ImVec2(buttonWidth, 0))) {
if (strlen(saveSceneAsName) > 0) {
projectManager.currentProject.currentSceneName = saveSceneAsName;
saveCurrentScene();
showSaveSceneAsDialog = false;
memset(saveSceneAsName, 0, sizeof(saveSceneAsName));
}
}
}
ImGui::End();
}
// OBJ Import dialog
if (showImportOBJDialog) {
ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(400, 160), ImGuiCond_Appearing);
if (ImGui::Begin("Import OBJ Model", &showImportOBJDialog,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
ImGui::Text("File: %s", fs::path(pendingOBJPath).filename().string().c_str());
ImGui::TextDisabled("%s", pendingOBJPath.c_str());
ImGui::Spacing();
ImGui::Text("Object Name:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##ImportOBJName", importOBJName, sizeof(importOBJName));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float buttonWidth = 80;
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
showImportOBJDialog = false;
pendingOBJPath.clear();
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.4f, 1.0f));
if (ImGui::Button("Import", ImVec2(buttonWidth, 0))) {
importOBJToScene(pendingOBJPath, importOBJName);
showImportOBJDialog = false;
pendingOBJPath.clear();
memset(importOBJName, 0, sizeof(importOBJName));
}
ImGui::PopStyleColor(2);
}
ImGui::End();
}
}
void renderProjectBrowserPanel() {
ImGui::Begin("Project", &showProjectBrowser);
if (!projectManager.currentProject.isLoaded) {
ImGui::TextDisabled("No project loaded");
ImGui::End();
return;
}
ImGui::TextColored(ImVec4(0.4f, 0.7f, 0.95f, 1.0f), "[P] %s", projectManager.currentProject.name.c_str());
if (projectManager.currentProject.hasUnsavedChanges) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "*");
}
ImGui::Separator();
if (ImGui::CollapsingHeader("Scenes", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Button("+ New Scene")) {
showNewSceneDialog = true;
memset(newSceneName, 0, sizeof(newSceneName));
}
ImGui::Spacing();
auto scenes = projectManager.currentProject.getSceneList();
for (const auto& scene : scenes) {
bool isCurrentScene = (scene == projectManager.currentProject.currentSceneName);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_SpanAvailWidth |
ImGuiTreeNodeFlags_NoTreePushOnOpen;
if (isCurrentScene) flags |= ImGuiTreeNodeFlags_Selected;
ImGui::TreeNodeEx(scene.c_str(), flags, "[S] %s", scene.c_str());
if (ImGui::IsItemClicked() && !isCurrentScene) {
loadScene(scene);
}
if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Load") && !isCurrentScene) {
loadScene(scene);
}
if (ImGui::MenuItem("Duplicate")) {
addConsoleMessage("Scene duplication not yet implemented.", ConsoleMessageType::Info);
}
ImGui::Separator();
if (ImGui::MenuItem("Delete") && !isCurrentScene) {
fs::remove(projectManager.currentProject.getSceneFilePath(scene));
addConsoleMessage("Deleted scene: " + scene, ConsoleMessageType::Info);
}
ImGui::EndPopup();
}
}
if (scenes.empty()) {
ImGui::TextDisabled("No scenes yet");
}
}
if (ImGui::CollapsingHeader("Loaded Meshes")) {
const auto& meshes = g_objLoader.getAllMeshes();
if (meshes.empty()) {
ImGui::TextDisabled("No meshes loaded");
ImGui::TextDisabled("Import .obj files from File Browser");
} else {
for (size_t i = 0; i < meshes.size(); i++) {
const auto& mesh = meshes[i];
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_SpanAvailWidth |
ImGuiTreeNodeFlags_NoTreePushOnOpen;
ImGui::TreeNodeEx((void*)(intptr_t)i, flags, "[M] %s", mesh.name.c_str());
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Vertices: %d", mesh.vertexCount);
ImGui::Text("Faces: %d", mesh.faceCount);
ImGui::Text("Has Normals: %s", mesh.hasNormals ? "Yes" : "No");
ImGui::Text("Has UVs: %s", mesh.hasTexCoords ? "Yes" : "No");
ImGui::TextDisabled("%s", mesh.path.c_str());
ImGui::EndTooltip();
}
// Right-click to add instance to scene
if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Add to Scene")) {
int id = nextObjectId++;
SceneObject obj(mesh.name, ObjectType::OBJMesh, id);
obj.meshPath = mesh.path;
obj.meshId = static_cast<int>(i);
sceneObjects.push_back(obj);
selectedObjectId = id;
projectManager.currentProject.hasUnsavedChanges = true;
addConsoleMessage("Added mesh instance: " + mesh.name, ConsoleMessageType::Info);
}
ImGui::EndPopup();
}
}
}
}
if (ImGui::CollapsingHeader("Folders", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGuiTreeNodeFlags folderFlags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_SpanAvailWidth |
ImGuiTreeNodeFlags_NoTreePushOnOpen;
ImGui::TreeNodeEx("Assets", folderFlags, "[D] Assets");
if (ImGui::IsItemClicked()) {
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
fileBrowser.needsRefresh = true;
}
ImGui::TreeNodeEx("Scripts", folderFlags, "[D] Scripts");
if (ImGui::IsItemClicked()) {
fileBrowser.currentPath = projectManager.currentProject.scriptsPath;
fileBrowser.needsRefresh = true;
}
ImGui::TreeNodeEx("Scenes", folderFlags, "[D] Scenes");
if (ImGui::IsItemClicked()) {
fileBrowser.currentPath = projectManager.currentProject.scenesPath;
fileBrowser.needsRefresh = true;
}
}
ImGui::End();
}
void addConsoleMessage(const std::string& message, ConsoleMessageType type) {
std::string prefix;
switch (type) {
case ConsoleMessageType::Info: prefix = "Info: "; break;
case ConsoleMessageType::Warning: prefix = "Warning: "; break;
case ConsoleMessageType::Error: prefix = "Error: "; break;
case ConsoleMessageType::Success: prefix = "Success: "; break;
}
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::string timeStr = std::ctime(&time);
timeStr = timeStr.substr(11, 8);
consoleLog.push_back("[" + timeStr + "] " + prefix + " " + message);
if (consoleLog.size() > 1000) {
consoleLog.erase(consoleLog.begin());
}
}
void logToConsole(const std::string& message) {
addConsoleMessage(message, ConsoleMessageType::Info);
}
void applyModernTheme() {
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
colors[ImGuiCol_WindowBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
colors[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
colors[ImGuiCol_PopupBg] = ImVec4(0.12f, 0.12f, 0.14f, 0.98f);
colors[ImGuiCol_Header] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.28f, 0.32f, 1.00f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.24f, 0.24f, 0.28f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.22f, 0.22f, 0.26f, 1.00f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.30f, 0.36f, 1.00f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.26f, 0.30f, 1.00f);
colors[ImGuiCol_FrameBg] = ImVec4(0.14f, 0.14f, 0.16f, 1.00f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.18f, 0.18f, 0.22f, 1.00f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.22f, 0.22f, 0.26f, 1.00f);
colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.14f, 0.16f, 1.00f);
colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.28f, 0.32f, 1.00f);
colors[ImGuiCol_TabActive] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
colors[ImGuiCol_TabUnfocused] = ImVec4(0.12f, 0.12f, 0.14f, 1.00f);
colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.16f, 0.16f, 0.18f, 1.00f);
colors[ImGuiCol_TitleBg] = ImVec4(0.08f, 0.08f, 0.10f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.12f, 0.12f, 0.14f, 1.00f);
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.08f, 0.08f, 0.10f, 0.75f);
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.24f, 0.28f, 1.00f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.30f, 0.30f, 0.34f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.36f, 0.40f, 1.00f);
colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.55f, 0.75f, 1.00f);
colors[ImGuiCol_SeparatorActive] = ImVec4(0.30f, 0.55f, 0.75f, 1.00f);
colors[ImGuiCol_ResizeGrip] = ImVec4(0.24f, 0.24f, 0.28f, 0.25f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.30f, 0.55f, 0.75f, 0.67f);
colors[ImGuiCol_ResizeGripActive] = ImVec4(0.30f, 0.55f, 0.75f, 0.95f);
colors[ImGuiCol_DockingPreview] = ImVec4(0.30f, 0.55f, 0.75f, 0.70f);
colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.08f, 0.08f, 0.10f, 1.00f);
colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.92f, 1.00f);
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.52f, 1.00f);
colors[ImGuiCol_Border] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.12f, 0.12f, 0.14f, 1.00f);
colors[ImGuiCol_CheckMark] = ImVec4(0.40f, 0.70f, 0.95f, 1.00f);
colors[ImGuiCol_SliderGrab] = ImVec4(0.40f, 0.70f, 0.95f, 1.00f);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.50f, 0.80f, 1.00f, 1.00f);
style.WindowRounding = 4.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.ScrollbarRounding = 6.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 0.0f;
style.PopupBorderSize = 1.0f;
style.WindowPadding = ImVec2(10, 10);
style.FramePadding = ImVec2(6, 4);
style.ItemSpacing = ImVec2(8, 6);
style.ItemInnerSpacing = ImVec2(6, 4);
style.IndentSpacing = 20.0f;
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 12.0f;
}
void setupDockspace() {
ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking;
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGui::SetNextWindowViewport(viewport->ID);
window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
window_flags |= ImGuiWindowFlags_NoBackground;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("DockSpace", nullptr, window_flags);
ImGui::PopStyleVar(3);
ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
if (firstFrame) {
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);
ImGuiID dock_main_id = dockspace_id;
ImGuiID dock_left = ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Left, 0.2f, nullptr, &dock_main_id);
ImGuiID dock_right = ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Right, 0.25f, nullptr, &dock_main_id);
ImGuiID dock_bottom = ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Down, 0.25f, nullptr, &dock_main_id);
ImGui::DockBuilderDockWindow("Hierarchy", dock_left);
ImGui::DockBuilderDockWindow("Project", dock_left);
ImGui::DockBuilderDockWindow("File Browser", dock_left);
ImGui::DockBuilderDockWindow("Viewport", dock_main_id);
ImGui::DockBuilderDockWindow("Inspector", dock_right);
ImGui::DockBuilderDockWindow("Console", dock_bottom);
ImGui::DockBuilderFinish(dockspace_id);
}
ImGui::End();
}
void renderMainMenuBar() {
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New Scene", "Ctrl+N")) {
showNewSceneDialog = true;
memset(newSceneName, 0, sizeof(newSceneName));
}
if (ImGui::MenuItem("Save Scene", "Ctrl+S")) {
saveCurrentScene();
}
if (ImGui::MenuItem("Save Scene As...")) {
showSaveSceneAsDialog = true;
strncpy(saveSceneAsName, projectManager.currentProject.currentSceneName.c_str(),
sizeof(saveSceneAsName) - 1);
}
ImGui::Separator();
if (ImGui::MenuItem("Close Project")) {
if (projectManager.currentProject.hasUnsavedChanges) {
saveCurrentScene();
}
projectManager.currentProject = Project();
sceneObjects.clear();
selectedObjectId = -1;
showLauncher = true;
addConsoleMessage("Closed project", ConsoleMessageType::Info);
}
ImGui::Separator();
if (ImGui::MenuItem("Exit", "Alt+F4")) {
glfwSetWindowShouldClose(editorWindow, true);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit")) {
if (ImGui::MenuItem("Undo", "Ctrl+Z")) {}
if (ImGui::MenuItem("Redo", "Ctrl+Y")) {}
ImGui::Separator();
if (ImGui::MenuItem("Duplicate", "Ctrl+D") && selectedObjectId != -1) {
duplicateSelected();
}
if (ImGui::MenuItem("Delete", "Delete") && selectedObjectId != -1) {
deleteSelected();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("GameObject")) {
if (ImGui::BeginMenu("3D Object")) {
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
ImGui::Separator();
// Import OBJ from menu
if (ImGui::MenuItem("Import OBJ...")) {
addConsoleMessage("Use File Browser to import .obj files (double-click or right-click)", ConsoleMessageType::Info);
}
ImGui::EndMenu();
}
ImGui::EndMenu();
}
// So uh, why did i have to change one line in here???
if (ImGui::BeginMenu("Window")) {
ImGui::MenuItem("Hierarchy", nullptr, &showHierarchy);
ImGui::MenuItem("Inspector", nullptr, &showInspector);
ImGui::MenuItem("File Browser", nullptr, &showFileBrowser);
ImGui::MenuItem("Project", nullptr, &showProjectBrowser);
ImGui::MenuItem("Console", nullptr, &showConsole);
ImGui::Separator();
if (ImGui::MenuItem("Fullscreen Viewport", "F11", &viewportFullscreen)) {}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("About")) {
addConsoleMessage("Modularity - V1.0.1", ConsoleMessageType::Info);
}
ImGui::EndMenu();
}
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 100);
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "FPS: %.1f", 1.0f / deltaTime);
ImGui::EndMainMenuBar();
}
}
void renderHierarchyPanel() {
ImGui::Begin("Hierarchy", &showHierarchy);
// -------------------------------
// TOP TOOLBAR (row with + button)
// -------------------------------
{
// Child helps align things more cleanly
ImGui::BeginChild("HierarchyToolbar", ImVec2(0, 28), false);
// "+" button top-left
if (ImGui::Button("+", ImVec2(24, 24))) {
ImGui::OpenPopup("CreateMenu");
}
// Popup create menu
if (ImGui::BeginPopup("CreateMenu")) {
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
ImGui::EndPopup();
}
ImGui::EndChild();
}
ImGui::Separator();
// -------------------------------
// SEARCH BAR
// -------------------------------
static char searchBuffer[128] = "";
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##search", "Search objects...", searchBuffer, sizeof(searchBuffer));
ImGui::Separator();
// -------------------------------
// HIERARCHY TREE
// -------------------------------
ImGui::BeginChild("SceneTree", ImVec2(0, 0), false);
std::string filter = searchBuffer;
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
for (size_t i = 0; i < sceneObjects.size(); i++) {
if (sceneObjects[i].parentId != -1)
continue;
renderObjectNode(sceneObjects[i], filter);
}
// Right-click empty space
if (ImGui::BeginPopupContextWindow("HierarchyBackground",
ImGuiPopupFlags_MouseButtonRight |
ImGuiPopupFlags_NoOpenOverItems))
{
if (ImGui::BeginMenu("Create")) {
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
ImGui::EndMenu();
}
ImGui::EndPopup();
}
ImGui::EndChild();
ImGui::End();
}
void renderObjectNode(SceneObject& obj, const std::string& filter) {
std::string nameLower = obj.name;
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
if (!filter.empty() && nameLower.find(filter) == std::string::npos) {
return;
}
bool hasChildren = !obj.childIds.empty();
bool isSelected = (selectedObjectId == obj.id);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth;
if (isSelected) flags |= ImGuiTreeNodeFlags_Selected;
if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf;
const char* icon = "";
switch (obj.type) {
case ObjectType::Cube: icon = "[#]"; break;
case ObjectType::Sphere: icon = "(O)"; break;
case ObjectType::Capsule: icon = "[|]"; break;
case ObjectType::OBJMesh: icon = "[M]"; break; // OBJ mesh icon
}
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s %s", icon, obj.name.c_str());
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
selectedObjectId = obj.id;
}
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
ImGui::SetDragDropPayload("SCENE_OBJECT", &obj.id, sizeof(int));
ImGui::Text("Moving: %s", obj.name.c_str());
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("SCENE_OBJECT")) {
int draggedId = *(const int*)payload->Data;
if (draggedId != obj.id) {
setParent(draggedId, obj.id);
}
}
ImGui::EndDragDropTarget();
}
if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Duplicate")) {
selectedObjectId = obj.id;
duplicateSelected();
}
if (ImGui::MenuItem("Delete")) {
selectedObjectId = obj.id;
deleteSelected();
}
ImGui::Separator();
if (ImGui::MenuItem("Clear Parent") && obj.parentId != -1) {
setParent(obj.id, -1);
}
ImGui::EndPopup();
}
if (nodeOpen) {
for (int childId : obj.childIds) {
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[childId](const SceneObject& o) { return o.id == childId; });
if (it != sceneObjects.end()) {
renderObjectNode(*it, filter);
}
}
ImGui::TreePop();
}
}
void renderFileBrowserPanel() {
ImGui::Begin("File Browser", &showFileBrowser);
if (fileBrowser.needsRefresh) {
fileBrowser.refresh();
}
if (ImGui::Button("<")) {
fileBrowser.navigateUp();
}
ImGui::SameLine();
if (ImGui::Button("Refresh")) {
fileBrowser.needsRefresh = true;
}
ImGui::SameLine();
std::string pathStr = fileBrowser.currentPath.string();
ImGui::TextWrapped("%s", pathStr.c_str());
ImGui::Separator();
ImGui::BeginChild("FileList", ImVec2(0, 0), false);
for (const auto& entry : fileBrowser.entries) {
const char* icon = fileBrowser.getFileIcon(entry);
std::string filename = entry.path().filename().string();
bool isSelected = (fileBrowser.selectedFile == entry.path());
bool isOBJ = fileBrowser.isOBJFile(entry);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_NoTreePushOnOpen;
if (isSelected) flags |= ImGuiTreeNodeFlags_Selected;
// Highlight OBJ files
if (isOBJ) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f));
}
ImGui::TreeNodeEx(filename.c_str(), flags, "%s %s", icon, filename.c_str());
if (isOBJ) {
ImGui::PopStyleColor();
}
if (ImGui::IsItemClicked()) {
fileBrowser.selectedFile = entry.path();
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
if (entry.is_directory()) {
fileBrowser.navigateTo(entry.path());
} else if (isOBJ) {
// Double-click OBJ to import
pendingOBJPath = entry.path().string();
std::string defaultName = entry.path().stem().string();
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
showImportOBJDialog = true;
} else {
logToConsole("Selected file: " + filename);
}
}
if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Open")) {
if (entry.is_directory()) {
fileBrowser.navigateTo(entry.path());
}
}
// Add Import option for OBJ files
if (isOBJ) {
if (ImGui::MenuItem("Import to Scene")) {
pendingOBJPath = entry.path().string();
std::string defaultName = entry.path().stem().string();
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
showImportOBJDialog = true;
}
if (ImGui::MenuItem("Quick Import")) {
importOBJToScene(entry.path().string(), "");
}
}
if (ImGui::MenuItem("Show in Explorer")) {
#ifdef _WIN32
std::string cmd = "explorer \"" + entry.path().parent_path().string() + "\"";
system(cmd.c_str());
#endif
}
ImGui::EndPopup();
}
}
ImGui::EndChild();
ImGui::End();
}
void renderInspectorPanel() {
ImGui::Begin("Inspector", &showInspector);
if (selectedObjectId == -1) {
ImGui::TextDisabled("No object selected");
ImGui::End();
return;
}
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
if (it == sceneObjects.end()) {
ImGui::TextDisabled("Object not found");
ImGui::End();
return;
}
SceneObject& obj = *it;
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.4f, 0.6f, 1.0f));
if (ImGui::CollapsingHeader("Object Info", ImGuiTreeNodeFlags_DefaultOpen)) {
char nameBuffer[128];
strncpy(nameBuffer, obj.name.c_str(), sizeof(nameBuffer));
nameBuffer[sizeof(nameBuffer) - 1] = '\0';
ImGui::Text("Name:");
ImGui::SameLine();
ImGui::SetNextItemWidth(-1);
if (ImGui::InputText("##Name", nameBuffer, sizeof(nameBuffer))) {
obj.name = nameBuffer;
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::Text("Type:");
ImGui::SameLine();
const char* typeNames[] = { "Cube", "Sphere", "Capsule", "OBJ Mesh" };
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "%s", typeNames[(int)obj.type]);
ImGui::Text("ID:");
ImGui::SameLine();
ImGui::TextDisabled("%d", obj.id);
}
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.5f, 0.3f, 1.0f));
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
ImGui::Text("Position");
ImGui::PushItemWidth(-1);
if (ImGui::DragFloat3("##Position", &obj.position.x, 0.1f)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::PopItemWidth();
ImGui::Spacing();
ImGui::Text("Rotation");
ImGui::PushItemWidth(-1);
if (ImGui::DragFloat3("##Rotation", &obj.rotation.x, 1.0f, -360.0f, 360.0f)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::PopItemWidth();
ImGui::Spacing();
ImGui::Text("Scale");
ImGui::PushItemWidth(-1);
if (ImGui::DragFloat3("##Scale", &obj.scale.x, 0.05f, 0.01f, 100.0f)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::PopItemWidth();
ImGui::Spacing();
if (ImGui::Button("Reset Transform", ImVec2(-1, 0))) {
obj.position = glm::vec3(0.0f);
obj.rotation = glm::vec3(0.0f);
obj.scale = glm::vec3(1.0f);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::Unindent(10.0f);
}
ImGui::PopStyleColor();
// OBJ Mesh info section
if (obj.type == ObjectType::OBJMesh) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.3f, 0.5f, 0.4f, 1.0f));
if (ImGui::CollapsingHeader("Mesh Info", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
const auto* meshInfo = g_objLoader.getMeshInfo(obj.meshId);
if (meshInfo) {
ImGui::Text("Source File:");
ImGui::TextDisabled("%s", fs::path(meshInfo->path).filename().string().c_str());
ImGui::Spacing();
ImGui::Text("Vertices: %d", meshInfo->vertexCount);
ImGui::Text("Faces: %d", meshInfo->faceCount);
ImGui::Text("Has Normals: %s", meshInfo->hasNormals ? "Yes" : "No");
ImGui::Text("Has UVs: %s", meshInfo->hasTexCoords ? "Yes" : "No");
ImGui::Spacing();
if (ImGui::Button("Reload Mesh", ImVec2(-1, 0))) {
std::string errMsg;
int newId = g_objLoader.loadOBJ(obj.meshPath, errMsg);
if (newId >= 0) {
obj.meshId = newId;
addConsoleMessage("Reloaded mesh: " + obj.name, ConsoleMessageType::Success);
} else {
addConsoleMessage("Failed to reload: " + errMsg, ConsoleMessageType::Error);
}
}
} else {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Mesh data not found!");
ImGui::TextDisabled("Path: %s", obj.meshPath.c_str());
if (ImGui::Button("Try Reload", ImVec2(-1, 0))) {
std::string errMsg;
obj.meshId = g_objLoader.loadOBJ(obj.meshPath, errMsg);
if (obj.meshId >= 0) {
addConsoleMessage("Mesh reloaded successfully", ConsoleMessageType::Success);
} else {
addConsoleMessage("Reload failed: " + errMsg, ConsoleMessageType::Error);
}
}
}
ImGui::Unindent(10.0f);
}
ImGui::PopStyleColor();
}
ImGui::Spacing();
if (renderer.getSkybox()) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.5f, 0.3f, 0.5f, 1.0f));
if (ImGui::CollapsingHeader("Environment")) {
ImGui::Indent(10.0f);
float timeOfDay = renderer.getSkybox()->getTimeOfDay();
ImGui::Text("Time of Day");
ImGui::PushItemWidth(-1);
if (ImGui::SliderFloat("##TimeOfDay", &timeOfDay, 0.0f, 1.0f, "%.2f")) {
renderer.getSkybox()->setTimeOfDay(timeOfDay);
}
ImGui::PopItemWidth();
if (ImGui::Button("Night", ImVec2(60, 0))) renderer.getSkybox()->setTimeOfDay(0.0f);
ImGui::SameLine();
if (ImGui::Button("Sunrise", ImVec2(60, 0))) renderer.getSkybox()->setTimeOfDay(0.25f);
ImGui::SameLine();
if (ImGui::Button("Day", ImVec2(60, 0))) renderer.getSkybox()->setTimeOfDay(0.5f);
ImGui::SameLine();
if (ImGui::Button("Sunset", ImVec2(60, 0))) renderer.getSkybox()->setTimeOfDay(0.75f);
ImGui::Unindent(10.0f);
}
ImGui::PopStyleColor();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Duplicate", ImVec2(ImGui::GetContentRegionAvail().x * 0.5f - 4, 0))) {
duplicateSelected();
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button("Delete", ImVec2(-1, 0))) {
deleteSelected();
}
ImGui::PopStyleColor(2);
ImGui::End();
}
void renderConsolePanel() {
ImGui::Begin("Console", &showConsole);
if (ImGui::Button("Clear")) {
consoleLog.clear();
}
ImGui::SameLine();
static bool autoScroll = true;
ImGui::Checkbox("Auto-scroll", &autoScroll);
ImGui::Separator();
ImGui::BeginChild("LogArea", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
for (const auto& log : consoleLog) {
ImVec4 color = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
if (log.find("error") != std::string::npos || log.find("Error") != std::string::npos || log.find("[ERROR]") != std::string::npos) {
color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f);
} else if (log.find("warning") != std::string::npos || log.find("Warning") != std::string::npos || log.find("[WARN]") != std::string::npos) {
color = ImVec4(1.0f, 0.8f, 0.4f, 1.0f);
} else if (log.find("success") != std::string::npos || log.find("Success") != std::string::npos || log.find("[SUCCESS]") != std::string::npos) {
color = ImVec4(0.4f, 1.0f, 0.4f, 1.0f);
}
ImGui::TextColored(color, "%s", log.c_str());
}
if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
ImGui::SetScrollHereY(1.0f);
}
ImGui::EndChild();
ImGui::End();
}
void renderViewport() {
ImGuiWindowFlags viewportFlags = ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar;
if (viewportFullscreen) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
viewportFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::Begin("Viewport", nullptr, viewportFlags);
ImGui::PopStyleVar();
ImVec2 fullAvail = ImGui::GetContentRegionAvail();
const float toolbarHeight = 20.0f;
ImVec2 imageSize = fullAvail;
imageSize.y = ImMax(1.0f, imageSize.y - toolbarHeight);
if (imageSize.x > 0 && imageSize.y > 0) {
viewportWidth = static_cast<int>(imageSize.x);
viewportHeight = static_cast<int>(imageSize.y);
if (rendererInitialized) {
renderer.resize(viewportWidth, viewportHeight);
}
}
// We'll forward this outside the block for camera focus
bool mouseOverViewportImage = false;
if (rendererInitialized) {
// RENDER SCENE TO TEXTURE
glm::mat4 proj = glm::perspective(
glm::radians(FOV),
(float)viewportWidth / (float)viewportHeight,
NEAR_PLANE, FAR_PLANE
);
glm::mat4 view = camera.getViewMatrix();
renderer.beginRender(view, proj);
renderer.renderScene(camera, sceneObjects);
unsigned int tex = renderer.getViewportTexture();
// DRAW THE VIEWPORT IMAGE (only top region, below we keep space for toolbar)
ImGui::Image((void*)(intptr_t)tex, imageSize, ImVec2(0, 1), ImVec2(1, 0));
// Get the exact rect of the image we just drew
ImVec2 imageMin = ImGui::GetItemRectMin();
ImVec2 imageMax = ImGui::GetItemRectMax();
mouseOverViewportImage = ImGui::IsItemHovered();
// GIZMO (Please work...)
SceneObject* selectedObj = getSelectedObject();
if (selectedObj) {
ImGuizmo::BeginFrame();
ImGuizmo::Enable(true);
ImGuizmo::SetOrthographic(false);
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
ImGuizmo::SetRect(
imageMin.x,
imageMin.y,
imageMax.x - imageMin.x,
imageMax.y - imageMin.y
);
// Build model matrix from your SceneObject
glm::mat4 modelMatrix(1.0f);
modelMatrix = glm::translate(modelMatrix, selectedObj->position);
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.x), glm::vec3(1, 0, 0));
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.y), glm::vec3(0, 1, 0));
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.z), glm::vec3(0, 0, 1));
modelMatrix = glm::scale(modelMatrix, selectedObj->scale);
float* snapPtr = nullptr;
float snapRot[3] = { rotationSnapValue, rotationSnapValue, rotationSnapValue };
if (useSnap) {
if (mCurrentGizmoOperation == ImGuizmo::ROTATE) {
snapPtr = snapRot;
} else {
snapPtr = snapValue;
}
}
ImGuizmo::Manipulate(
glm::value_ptr(view),
glm::value_ptr(proj),
mCurrentGizmoOperation,
mCurrentGizmoMode,
glm::value_ptr(modelMatrix),
nullptr,
snapPtr
);
if (ImGuizmo::IsUsing()) {
// Use ImGuizmo's own decompose helper to be safe
float t[3], r[3], s[3];
ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(modelMatrix), t, r, s);
selectedObj->position = glm::vec3(t[0], t[1], t[2]);
// r[] is already in degrees
selectedObj->rotation = glm::vec3(r[0], r[1], r[2]);
selectedObj->scale = glm::vec3(s[0], s[1], s[2]);
projectManager.currentProject.hasUnsavedChanges = true;
}
}
// Place it just under the image
ImGui::SetCursorPos(ImVec2(20, imageSize.y + 20));
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.95f);
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.12f, 0.12f, 0.14f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 0.6f));
// I hate how long this took to actually work. :sob:
if (ImGui::RadioButton("Move", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
ImGui::SameLine();
if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE)) mCurrentGizmoOperation = ImGuizmo::ROTATE;
ImGui::SameLine();
if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) mCurrentGizmoOperation = ImGuizmo::SCALE;
ImGui::SameLine();
if (ImGui::RadioButton("Uni", mCurrentGizmoOperation == ImGuizmo::UNIVERSAL)) mCurrentGizmoOperation = ImGuizmo::UNIVERSAL;
ImGui::SameLine();
if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL)) mCurrentGizmoMode = ImGuizmo::LOCAL;
ImGui::SameLine();
if (ImGui::RadioButton("World", mCurrentGizmoMode == ImGuizmo::WORLD)) mCurrentGizmoMode = ImGuizmo::WORLD;
ImGui::SameLine();
ImGui::Checkbox("Snap", &useSnap);
if (useSnap) {
ImGui::SameLine();
switch (mCurrentGizmoOperation) {
case ImGuizmo::TRANSLATE:
case ImGuizmo::SCALE:
ImGui::SetNextItemWidth(100);
ImGui::DragFloat3("##snap", snapValue, 0.1f, 0.01f, 10.0f);
break;
case ImGuizmo::ROTATE:
ImGui::SetNextItemWidth(80);
ImGui::DragFloat("Angle", &rotationSnapValue, 1.0f, 0.0f, 90.0f);
break;
}
}
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
// CAMERA FOCUS CLICK (only when not dragging gizmo and ONLY over the image)
if (mouseOverViewportImage &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGuizmo::IsUsing())
{
viewportController.setFocused(true);
cursorLocked = true;
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
camera.firstMouse = true;
}
// ESC to release cursor
if (glfwGetKey(editorWindow, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
viewportController.setFocused(false);
cursorLocked = false;
camera.firstMouse = true;
}
}
// OVERLAY HINT TEXT (top-left over the image)
ImGui::SetCursorPos(ImVec2(10, 30));
ImGui::TextColored(
ImVec4(1, 1, 1, 0.7f),
"WASD: Move | QE: Up/Down | Shift: Sprint | ESC: Release | F11: Fullscreen"
);
if (viewportController.isViewportFocused()) {
ImGui::SetCursorPos(ImVec2(10, 50));
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Camera Active");
}
bool windowFocused = ImGui::IsWindowFocused();
viewportController.updateFocusFromImGui(windowFocused);
ImGui::End();
}
void addObject(ObjectType type, const std::string& baseName) {
int id = nextObjectId++;
std::string name = baseName + " " + std::to_string(id);
sceneObjects.push_back(SceneObject(name, type, id));
selectedObjectId = id;
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}
logToConsole("Created: " + name);
}
void duplicateSelected() {
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
if (it != sceneObjects.end()) {
int id = nextObjectId++;
SceneObject newObj(it->name + " (Copy)", it->type, id);
newObj.position = it->position + glm::vec3(1.0f, 0.0f, 0.0f);
newObj.rotation = it->rotation;
newObj.scale = it->scale;
// Copy mesh data for OBJ meshes
newObj.meshPath = it->meshPath;
newObj.meshId = it->meshId;
sceneObjects.push_back(newObj);
selectedObjectId = id;
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}
logToConsole("Duplicated: " + newObj.name);
}
}
void deleteSelected() {
auto it = std::remove_if(sceneObjects.begin(), sceneObjects.end(),
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
if (it != sceneObjects.end()) {
logToConsole("Deleted object");
sceneObjects.erase(it, sceneObjects.end());
selectedObjectId = -1;
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}
}
}
void setParent(int childId, int parentId) {
auto childIt = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[childId](const SceneObject& obj) { return obj.id == childId; });
if (childIt == sceneObjects.end()) return;
if (childIt->parentId != -1) {
auto oldParentIt = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[&childIt](const SceneObject& obj) { return obj.id == childIt->parentId; });
if (oldParentIt != sceneObjects.end()) {
auto& children = oldParentIt->childIds;
children.erase(std::remove(children.begin(), children.end(), childId), children.end());
}
}
childIt->parentId = parentId;
if (parentId != -1) {
auto newParentIt = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[parentId](const SceneObject& obj) { return obj.id == parentId; });
if (newParentIt != sceneObjects.end()) {
newParentIt->childIds.push_back(childId);
}
}
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
}
logToConsole("Reparented object");
}
void setupImGui() {
float mainScale = ImGui_ImplGlfw_GetContentScaleForMonitor(glfwGetPrimaryMonitor());
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
applyModernTheme();
ImGuiStyle& style = ImGui::GetStyle();
style.ScaleAllSizes(mainScale);
style.FontScaleDpi = mainScale;
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
ImGui_ImplGlfw_InitForOpenGL(editorWindow, true);
if (!ImGui_ImplOpenGL3_Init("#version 330")) {
std::cerr << "ImGui OpenGL3 init failed!\n";
throw std::runtime_error("ImGui error");
}
}
};
int main() {
Engine engine;
if (!engine.init()) {
std::cerr << "Engine init failed!\n";
return -1;
}
engine.run();
engine.shutdown();
return 0;
}