Initial commit - some engine bugs stopping compiling

This commit is contained in:
Will
2026-03-29 15:52:42 +01:00
commit 3d573a200e
361 changed files with 332759 additions and 0 deletions

186
src/Camera.cpp Normal file
View File

@@ -0,0 +1,186 @@
// src/Camera.cpp
#include "Camera.h"
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>
Camera::Camera(ProjectionType type,
const glm::vec3& position,
const glm::vec3& target,
const glm::vec3& up)
: projection_type_(type),
position_(position),
target_(target),
up_(up)
{
view_dirty_ = projection_dirty_ = true;
UpdateFromEulerAngles();
}
void Camera::SetPosition(const glm::vec3& position) {
position_ = position;
view_dirty_ = true;
}
void Camera::SetTarget(const glm::vec3& target) {
target_ = target;
view_dirty_ = true;
}
void Camera::SetUp(const glm::vec3& up) {
up_ = up;
view_dirty_ = true;
}
void Camera::LookAt(const glm::vec3& position,
const glm::vec3& target,
const glm::vec3& up) {
position_ = position;
target_ = target;
up_ = up;
view_dirty_ = true;
}
void Camera::SetPerspective(float fov,
float aspect_ratio,
float near_plane,
float far_plane) {
projection_type_ = ProjectionType::Perspective;
fov_ = fov;
aspect_ratio_ = aspect_ratio;
near_plane_ = near_plane;
far_plane_ = far_plane;
projection_dirty_ = true;
}
void Camera::SetOrthographic(float left,
float right,
float bottom,
float top,
float near_plane,
float far_plane) {
projection_type_ = ProjectionType::Orthographic;
ortho_left_ = left;
ortho_right_ = right;
ortho_bottom_ = bottom;
ortho_top_ = top;
near_plane_ = near_plane;
far_plane_ = far_plane;
projection_dirty_ = true;
}
void Camera::SetAspectRatio(float aspect_ratio) {
aspect_ratio_ = aspect_ratio;
projection_dirty_ = true;
}
void Camera::Move(const glm::vec3& offset) {
position_ += offset;
target_ += offset;
view_dirty_ = true;
}
void Camera::MoveForward(float distance) {
glm::vec3 forward = glm::normalize(target_ - position_);
Move(forward * distance);
}
void Camera::MoveBackward(float distance) { MoveForward(-distance); }
void Camera::MoveLeft(float distance) { Move(glm::normalize(glm::cross(up_, target_ - position_)) * -distance); }
void Camera::MoveRight(float distance) { Move(glm::normalize(glm::cross(up_, target_ - position_)) * distance); }
void Camera::MoveUp(float distance) { Move(up_ * distance); }
void Camera::MoveDown(float distance) { Move(-up_ * distance); }
void Camera::Rotate(float yaw, float pitch) {
yaw_ += yaw;
pitch_ += pitch;
if (pitch_ > MAX_PITCH) pitch_ = MAX_PITCH;
if (pitch_ < -MAX_PITCH) pitch_ = -MAX_PITCH;
UpdateFromEulerAngles();
}
void Camera::SetYaw(float yaw) { yaw_ = yaw; UpdateFromEulerAngles(); }
void Camera::SetPitch(float p) { pitch_ = p; UpdateFromEulerAngles(); }
const glm::mat4& Camera::GetViewMatrix() const {
if (view_dirty_) UpdateViewMatrix();
return view_matrix_;
}
const glm::mat4& Camera::GetProjectionMatrix() const {
if (projection_dirty_) UpdateProjectionMatrix();
return projection_matrix_;
}
glm::mat4 Camera::GetViewProjectionMatrix() const {
return GetProjectionMatrix() * GetViewMatrix();
}
glm::vec3 Camera::GetForward() const {
return glm::normalize(target_ - position_);
}
glm::vec3 Camera::GetRight() const {
return glm::normalize(glm::cross(GetForward(), up_));
}
glm::vec3 Camera::ScreenToWorld(const glm::vec2& screen_pos,
const glm::vec2& screen_size,
float depth) const {
glm::vec4 ndc{
(screen_pos.x / screen_size.x) * 2.0f - 1.0f,
(screen_pos.y / screen_size.y) * 2.0f - 1.0f,
depth * 2.0f - 1.0f,
1.0f
};
glm::mat4 invVP = glm::inverse(GetViewProjectionMatrix());
glm::vec4 world = invVP * ndc;
return glm::vec3(world) / world.w;
}
glm::vec2 Camera::WorldToScreen(const glm::vec3& world_pos,
const glm::vec2& screen_size) const {
glm::vec4 clip = GetViewProjectionMatrix() * glm::vec4(world_pos, 1.0f);
glm::vec3 ndc = glm::vec3(clip) / clip.w;
return {
((ndc.x + 1.0f) * 0.5f) * screen_size.x,
((ndc.y + 1.0f) * 0.5f) * screen_size.y
};
}
void Camera::PrintInfo() const {
std::cout << "Camera Position: ("
<< position_.x << ", " << position_.y << ", " << position_.z << ")\n"
<< "Yaw: " << yaw_ << ", Pitch: " << pitch_ << "\n";
}
void Camera::UpdateViewMatrix() const {
view_matrix_ = glm::lookAt(position_, target_, up_);
view_dirty_ = false;
}
void Camera::UpdateProjectionMatrix() const {
if (projection_type_ == ProjectionType::Perspective) {
projection_matrix_ = glm::perspective(
glm::radians(fov_),
aspect_ratio_,
near_plane_,
far_plane_
);
} else {
projection_matrix_ = glm::ortho(
ortho_left_, ortho_right_, ortho_bottom_, ortho_top_,
near_plane_, far_plane_
);
}
projection_dirty_ = false;
}
void Camera::UpdateFromEulerAngles() {
glm::vec3 dir;
dir.x = cos(glm::radians(yaw_)) * cos(glm::radians(pitch_));
dir.y = sin(glm::radians(pitch_));
dir.z = sin(glm::radians(yaw_)) * cos(glm::radians(pitch_));
target_ = position_ + glm::normalize(dir);
view_dirty_ = true;
}

118
src/Camera.h Normal file
View File

@@ -0,0 +1,118 @@
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
// Modern camera class using C++23 features
class Camera {
public:
enum class ProjectionType {
Perspective,
Orthographic
};
// Constructor with designated initializers-like approach
explicit Camera(ProjectionType type = ProjectionType::Perspective,
const glm::vec3& position = glm::vec3(0.0f, 0.0f, 3.0f),
const glm::vec3& target = glm::vec3(0.0f, 0.0f, 0.0f),
const glm::vec3& up = glm::vec3(0.0f, 1.0f, 0.0f));
~Camera() = default;
// Delete copy constructor and assignment
Camera(const Camera&) = delete;
Camera& operator=(const Camera&) = delete;
// Move semantics
Camera(Camera&&) noexcept = default;
Camera& operator=(Camera&&) noexcept = default;
// View matrix management
void SetPosition(const glm::vec3& position);
void SetTarget(const glm::vec3& target);
void SetUp(const glm::vec3& up);
void LookAt(const glm::vec3& position, const glm::vec3& target, const glm::vec3& up = glm::vec3(0.0f, 1.0f, 0.0f));
// Projection management
void SetPerspective(float fov, float aspect_ratio, float near_plane, float far_plane);
void SetOrthographic(float left, float right, float bottom, float top, float near_plane, float far_plane);
void SetAspectRatio(float aspect_ratio);
// Movement methods
void Move(const glm::vec3& offset);
void MoveForward(float distance);
void MoveBackward(float distance);
void MoveLeft(float distance);
void MoveRight(float distance);
void MoveUp(float distance);
void MoveDown(float distance);
// Rotation methods (first-person camera style)
void Rotate(float yaw, float pitch);
void SetYaw(float yaw);
void SetPitch(float pitch);
// Modern getters using [[nodiscard]]
[[nodiscard]] const glm::mat4& GetViewMatrix() const;
[[nodiscard]] const glm::mat4& GetProjectionMatrix() const;
[[nodiscard]] glm::mat4 GetViewProjectionMatrix() const;
[[nodiscard]] const glm::vec3& GetPosition() const { return position_; }
[[nodiscard]] const glm::vec3& GetTarget() const { return target_; }
[[nodiscard]] const glm::vec3& GetUp() const { return up_; }
[[nodiscard]] glm::vec3 GetForward() const;
[[nodiscard]] glm::vec3 GetRight() const;
[[nodiscard]] float GetFOV() const { return fov_; }
[[nodiscard]] float GetAspectRatio() const { return aspect_ratio_; }
[[nodiscard]] float GetNearPlane() const { return near_plane_; }
[[nodiscard]] float GetFarPlane() const { return far_plane_; }
[[nodiscard]] float GetYaw() const { return yaw_; }
[[nodiscard]] float GetPitch() const { return pitch_; }
[[nodiscard]] ProjectionType GetProjectionType() const { return projection_type_; }
// Utility functions
[[nodiscard]] glm::vec3 ScreenToWorld(const glm::vec2& screen_pos, const glm::vec2& screen_size, float depth = 1.0f) const;
[[nodiscard]] glm::vec2 WorldToScreen(const glm::vec3& world_pos, const glm::vec2& screen_size) const;
// Debug functions
void PrintInfo() const;
private:
ProjectionType projection_type_;
// View parameters
glm::vec3 position_;
glm::vec3 target_;
glm::vec3 up_;
// Euler angles for first-person camera
float yaw_ = -90.0f; // Initialized to look along negative Z-axis
float pitch_ = 0.0f;
// Projection parameters
float fov_ = 45.0f;
float aspect_ratio_ = 16.0f / 9.0f;
float near_plane_ = 0.1f;
float far_plane_ = 100.0f;
// Orthographic parameters
float ortho_left_ = -10.0f;
float ortho_right_ = 10.0f;
float ortho_bottom_ = -10.0f;
float ortho_top_ = 10.0f;
// Cached matrices (mutable for lazy evaluation)
mutable glm::mat4 view_matrix_{1.0f};
mutable glm::mat4 projection_matrix_{1.0f};
mutable bool view_dirty_ = true;
mutable bool projection_dirty_ = true;
void UpdateViewMatrix() const;
void UpdateProjectionMatrix() const;
void UpdateFromEulerAngles();
// Constrain pitch to avoid gimbal lock
static constexpr float MAX_PITCH = 89.0f;
};

1213
src/Engine.cpp Normal file

File diff suppressed because it is too large Load Diff

180
src/Engine.h Normal file
View File

@@ -0,0 +1,180 @@
#pragma once
#include <glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <vector>
#include <memory>
#include <functional>
#include "ThreadPool.h"
// Forward declarations
class Primitive;
class GameObject;
class Engine {
public:
Engine(int window_width = 1280, int window_height = 720, const char* title = "C++ Engine");
virtual ~Engine();
virtual bool OnInitialize();
virtual void OnShutdown();
virtual void OnUpdate(float delta_time);
virtual void OnRender();
void Run();
// Primitive factory methods
std::shared_ptr<Primitive> CreateCube(const glm::vec3& color = glm::vec3(1.0f));
std::shared_ptr<Primitive> CreateSphere(int segments = 32, const glm::vec3& color = glm::vec3(1.0f));
std::shared_ptr<Primitive> CreatePlane(float width = 2.0f, float height = 2.0f, const glm::vec3& color = glm::vec3(1.0f));
std::shared_ptr<Primitive> CreateGrid(int size = 20, float spacing = 1.0f, const glm::vec3& color = glm::vec3(0.6f));
std::shared_ptr<Primitive> CreateOBJModel(const std::string& objPath, const std::string& texturePath = "");
// GameObject management
std::shared_ptr<GameObject> CreateGameObject(std::shared_ptr<Primitive> primitive, const glm::vec3& position = glm::vec3(0.0f));
void AddGameObject(std::shared_ptr<GameObject> obj);
void RemoveGameObject(std::shared_ptr<GameObject> obj);
// Camera control
void SetCameraPosition(const glm::vec3& position);
void SetCameraTarget(const glm::vec3& target);
void SetCameraOrbit(float distance, float angle, float height = 5.0f);
GLuint text_vao = 0, text_vbo = 0;
GLFWwindow* GetWindow() const { return window_; }
float GetTime() const { return (float)glfwGetTime(); }
private:
ThreadPool threadPool_;
// Culling result structure
struct CullingBatch {
Primitive* primitive;
std::vector<glm::mat4> modelMatrices;
};
using CullingResult = std::vector<CullingBatch>;
// Helper methods
CullingResult CullObjectsSubset(
const std::vector<GameObject*>& objects,
size_t startIdx,
size_t endIdx,
const glm::mat4& viewProjection
);
void MergeCullingResults(
const std::vector<CullingResult>& results,
std::unordered_map<Primitive*, std::vector<glm::mat4>>& outBatches
);
protected:
void InitializeShader();
void RenderScene();
void RenderSceneInstanced();
void RenderOverlay();
void RenderOverlayTestPoint();
void InitOverlay();
static void FramebufferSizeCallback(GLFWwindow* window, int width, int height);
int width_, height_;
GLFWwindow* window_;
float last_time_ = 0.0f;
// Camera
glm::vec3 camera_position_ = glm::vec3(0, 5, 10);
glm::vec3 camera_target_ = glm::vec3(0, 0, 0);
glm::vec3 camera_up_ = glm::vec3(0, 1, 0);
// Rendering
GLuint shader_program_ = 0;
std::vector<std::shared_ptr<GameObject>> game_objects_;
// Overlay VAO/VBO
GLuint overlay_vao = 0;
GLuint overlay_vbo = 0;
GLuint overlay_program = 0;
// Performance stats
static constexpr int PERF_HISTORY = 300;
float perf_times[PERF_HISTORY] = {0};
int perf_pos = 0;
int perf_count = 0;
// Projection matrix for overlay
glm::mat4 ortho2D;
// Font buffer for stb_easy_font
static const int FONT_BUFFER_SIZE = 9999;
char font_buffer[FONT_BUFFER_SIZE];
};
// Primitive base class
class Primitive {
public:
virtual ~Primitive() = default;
virtual void Render() = 0;
virtual void Bind() = 0;
virtual void Cleanup() = 0;
virtual GLenum GetDrawMode() const { return GL_TRIANGLES; }
virtual GLsizei GetIndexCount() const { return index_count_; }
virtual GLsizei GetVertexCount() const { return vertex_count_; }
virtual void Bind() const { glBindVertexArray(vao_); }
protected:
GLuint vao_ = 0;
GLuint vbo_ = 0;
GLuint ebo_ = 0;
GLsizei index_count_ = 0;
GLsizei vertex_count_ = 0;
GLenum draw_mode_ = GL_TRIANGLES;
glm::vec3 color_;
};
// GameObject class
class GameObject {
public:
GameObject(std::shared_ptr<Primitive> prim, const glm::vec3& pos);
// Add:
Primitive* GetPrimitive() const { return primitive_.get(); }
float GetBoundingRadius() const;
glm::vec3 GetPosition() const { return position_; }
glm::mat4 GetModelMatrix() const;
void Render();
void Cleanup();
void SetPosition(const glm::vec3& p) { position_ = p; }
void SetRotation(const glm::vec3& r) { rotation_ = r; }
void SetScale(const glm::vec3& s) { scale_ = s; }
private:
std::shared_ptr<Primitive> primitive_;
glm::vec3 position_;
glm::vec3 rotation_;
glm::vec3 scale_{1.0f};
};

87
src/Input.cpp Normal file
View File

@@ -0,0 +1,87 @@
// src/Input.cpp
#include "Input.h"
#include <GLFW/glfw3.h>
// Static member definitions
GLFWwindow* Input::window_ = nullptr;
std::unordered_set<int> Input::pressed_keys_;
std::unordered_set<int> Input::held_keys_;
std::unordered_set<int> Input::released_keys_;
std::array<bool, GLFW_MOUSE_BUTTON_LAST> Input::mouse_buttons_{};
glm::vec2 Input::mouse_position_{0.0f, 0.0f};
glm::vec2 Input::mouse_delta_{0.0f, 0.0f};
float Input::scroll_offset_ = 0.0f;
void Input::Initialize(GLFWwindow* window) {
window_ = window;
glfwSetKeyCallback(window, KeyCallback);
glfwSetMouseButtonCallback(window, MouseButtonCallback);
glfwSetCursorPosCallback(window, CursorPositionCallback);
glfwSetScrollCallback(window, ScrollCallback);
}
void Input::Update() {
// Clear previous frame states
released_keys_.clear();
scroll_offset_ = 0.0f;
mouse_delta_ = {0.0f, 0.0f};
// Process held keys: keys that were pressed last frame and still down
for (auto key : held_keys_) {
if (glfwGetKey(window_, key) == GLFW_PRESS) {
// still held
} else {
released_keys_.insert(key);
}
}
// Update held_keys_: all currently pressed
held_keys_.clear();
pressed_keys_.clear(); // consumed this frame
}
// GLFW callbacks
void Input::KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (action == GLFW_PRESS) {
pressed_keys_.insert(key);
held_keys_.insert(key);
} else if (action == GLFW_RELEASE) {
released_keys_.insert(key);
held_keys_.erase(key);
}
}
void Input::MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
if (button < 0 || button >= GLFW_MOUSE_BUTTON_LAST) return;
if (action == GLFW_PRESS) {
mouse_buttons_[button] = true;
} else if (action == GLFW_RELEASE) {
mouse_buttons_[button] = false;
}
}
void Input::CursorPositionCallback(GLFWwindow* window, double xpos, double ypos) {
glm::vec2 newPos{static_cast<float>(xpos), static_cast<float>(ypos)};
mouse_delta_ = newPos - mouse_position_;
mouse_position_ = newPos;
}
void Input::ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
scroll_offset_ = static_cast<float>(yoffset);
}
bool Input::IsMouseButtonPressed(MouseButton button) {
return mouse_buttons_[static_cast<int>(button)];
}
glm::vec2 Input::GetMousePosition() {
return mouse_position_;
}
glm::vec2 Input::GetMouseDelta() {
return mouse_delta_;
}
float Input::GetScrollOffset() {
return scroll_offset_;
}

64
src/Input.h Normal file
View File

@@ -0,0 +1,64 @@
#pragma once
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <unordered_set>
#include <array>
#include <glm/glm.hpp>
// Modern input system
class Input {
public:
enum class KeyState : int {
Released = GLFW_RELEASE,
Pressed = GLFW_PRESS,
Held = GLFW_REPEAT
};
enum class MouseButton : int {
Left = GLFW_MOUSE_BUTTON_LEFT,
Right = GLFW_MOUSE_BUTTON_RIGHT,
Middle = GLFW_MOUSE_BUTTON_MIDDLE
};
static void Initialize(GLFWwindow* window);
static void Update();
[[nodiscard]] static bool IsKeyPressed(int key);
[[nodiscard]] static bool IsKeyHeld(int key);
[[nodiscard]] static bool IsKeyReleased(int key);
[[nodiscard]] static bool IsMouseButtonPressed(MouseButton button);
[[nodiscard]] static glm::vec2 GetMousePosition();
[[nodiscard]] static glm::vec2 GetMouseDelta();
[[nodiscard]] static float GetScrollOffset();
[[nodiscard]] static constexpr bool IsValidKey(int key) noexcept {
return key >= 0 && key < GLFW_KEY_LAST;
}
private:
static GLFWwindow* window_;
static std::unordered_set<int> pressed_keys_;
static std::unordered_set<int> held_keys_;
static std::unordered_set<int> released_keys_;
static std::array<bool, GLFW_MOUSE_BUTTON_LAST> mouse_buttons_;
static glm::vec2 mouse_position_;
static glm::vec2 mouse_delta_;
static float scroll_offset_;
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
static void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods);
static void CursorPositionCallback(GLFWwindow* window, double xpos, double ypos);
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset);
};
// Out-of-class definitions
inline bool Input::IsKeyPressed(int key) {
return IsValidKey(key) && pressed_keys_.contains(key);
}
inline bool Input::IsKeyHeld(int key) {
return IsValidKey(key) && held_keys_.contains(key);
}
inline bool Input::IsKeyReleased(int key) {
return IsValidKey(key) && released_keys_.contains(key);
}

392
src/LevelEditor.cpp Normal file
View File

@@ -0,0 +1,392 @@
// LevelEditor.cpp
#include "Engine.h"
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <cmath>
// ---------------------------------------------
// LevelEditor: free camera, object placement, light sliders, stats
// ---------------------------------------------
class LevelEditor : public Engine {
private:
std::vector<std::shared_ptr<GameObject>> objects_;
std::shared_ptr<Primitive> monkeyPrim_;
std::shared_ptr<GameObject> selected_;
struct Button {
float x, y, w, h;
bool pressed;
};
// Free cam state
bool firstMouse_ = true;
double lastX_, lastY_;
float yaw_ = -90.0f, pitch_ = 0.0f;
float camSpeed_ = 10.0f, mouseSens_ = 0.1f;
// Light control state
float lightR = 1, lightG = 1, lightB = 1, lightI = 1;
bool dragR = false, dragG = false, dragB = false, dragI = false;
Button btnOrtho { 560, 150, 80, 20, false };
bool useOrtho_ = false;
// Helpers
bool KeyDown(int k) const { return glfwGetKey(window_, k) == GLFW_PRESS; }
void ProcessMouse(double xpos, double ypos) {
if (firstMouse_) {
lastX_ = xpos; lastY_ = ypos;
firstMouse_ = false;
}
float xoff = (xpos - lastX_) * mouseSens_;
float yoff = (lastY_ - ypos) * mouseSens_;
lastX_ = xpos; lastY_ = ypos;
yaw_ += xoff;
pitch_ = glm::clamp(pitch_ + yoff, -89.0f, 89.0f);
glm::vec3 front;
front.x = cos(glm::radians(yaw_)) * cos(glm::radians(pitch_));
front.y = sin(glm::radians(pitch_));
front.z = sin(glm::radians(yaw_)) * cos(glm::radians(pitch_));
camera_target_ = camera_position_ + glm::normalize(front);
}
// Place primitive at camera target
void Place(std::shared_ptr<Primitive> prim) {
glm::vec3 pos = camera_target_;
auto go = CreateGameObject(prim, pos);
objects_.push_back(go);
AddGameObject(go);
}
// Raycast pick uses bounding spheres
std::shared_ptr<GameObject> Pick(double mx, double my) {
int w,h; glfwGetWindowSize(window_,&w,&h);
float x = (2.0f*mx)/w -1.0f, y=1.0f-(2.0f*my)/h;
glm::vec4 ray_clip{x,y,-1,1};
// Create projection and view matrices locally
glm::mat4 proj = glm::perspective(glm::radians(60.0f), float(w)/h, 0.1f, 1000.0f);
glm::mat4 view = glm::lookAt(camera_position_, camera_target_, camera_up_);
glm::vec4 ray_eye = glm::inverse(proj)*ray_clip; ray_eye.z=-1; ray_eye.w=0;
glm::vec3 dir = glm::normalize(glm::vec3(glm::inverse(view)*ray_eye));
std::shared_ptr<GameObject> best; float bd=FLT_MAX;
for(auto& obj:objects_){
glm::vec3 oc = camera_position_ - obj->GetPosition();
float b = glm::dot(oc,dir);
float c = glm::dot(oc,oc) - obj->GetBoundingRadius()*obj->GetBoundingRadius();
float disc = b*b - c;
if(disc<0) continue;
float t = -b - sqrt(disc);
if(t>0 && t<bd){ bd=t; best=obj; }
}
return best;
}
void DrawChar(char c, float x, float y, float s) {
static const std::vector<std::vector<std::pair<float,float>>> strokes = {
{{0,0},{1,0},{1,1},{0,1},{0,0}},{{0.5f,0},{0.5f,1}},
{{0,1},{1,1},{1,0.5f},{0,0.5f},{0,0},{1,0}},
{{0,1},{1,1},{1,0},{0,0},{1,0.5f}},
{{0,1},{0,0.5f},{1,0.5f},{1,1},{1,0}},
{{1,1},{0,1},{0,0.5f},{1,0.5f},{1,0},{0,0}},
{{1,1},{0,1},{0,0},{1,0},{1,0.5f},{0,0.5f}},
{{0,1},{1,1},{0.5f,0}},
{{0,0},{0,1},{1,1},{1,0},{0,0},{0,0.5f},{1,0.5f}},
{{1,0},{1,1},{0,1},{0,0.5f},{1,0.5f}}
};
std::vector<float> v;
if(c>='0'&&c<='9'){
for(auto&p:strokes[c-'0']){
v.push_back(x+p.first*s);
v.push_back(y+p.second*s);
v.push_back(1);v.push_back(1);v.push_back(1);
}
glBufferData(GL_ARRAY_BUFFER,v.size()*sizeof(float),v.data(),GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
glDrawArrays(GL_LINE_STRIP,0,(GLsizei)(v.size()/5));
}
}
// Draw a letter R,G,B,I
void DrawLetter(char c, float x, float y, float s) {
struct V{float x,y,r,g,b;};
std::vector<V> L;
if(c=='R'){
L={{x,y,1,1,1},{x,y+s,1,1,1},{x,y+s,1,1,1},{x+s*0.7f,y+s,1,1,1},
{x+s*0.7f,y+s,1,1,1},{x+s*0.7f,y+s*0.5f,1,1,1},{x+s*0.7f,y+s*0.5f,1,1,1},
{x,y+s*0.5f,1,1,1},{x,y+s*0.5f,1,1,1},{x+s*0.7f,y,1,1,1}};
} else if(c=='G'){
L={{x+s,y+s*0.3f,1,1,1},{x,y+s*0.3f,1,1,1},{x,y+s*0.3f,1,1,1},
{x,y+s*0.7f,1,1,1},{x,y+s*0.7f,1,1,1},{x+s,y+s*0.7f,1,1,1},
{x+s,y+s*0.7f,1,1,1},{x+s,y+s*0.5f,1,1,1},{x+s,y+s*0.5f,1,1,1},
{x+s*0.5f,y+s*0.5f,1,1,1}};
} else if(c=='B'){
L={{x,y,1,1,1},{x,y+s,1,1,1},{x,y+s,1,1,1},{x+s*0.7f,y+s,1,1,1},
{x+s*0.7f,y+s,1,1,1},{x+s*0.7f,y+s*0.5f,1,1,1},{x+s*0.7f,y+s*0.5f,1,1,1},
{x,y+s*0.5f,1,1,1},{x,y+s*0.5f,1,1,1},{x+s*0.7f,y,1,1,1},
{x+s*0.7f,y,1,1,1},{x,y,1,1,1}};
} else if(c=='I'){
L={{x,y+s,1,1,1},{x+s,y+s,1,1,1},{x+s*0.5f,y+s,1,1,1},
{x+s*0.5f,y,1,1,1},{x,y,1,1,1},{x+s,y,1,1,1}};
}
if(!L.empty()){
glBufferData(GL_ARRAY_BUFFER,L.size()*sizeof(V),L.data(),GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(V),(void*)0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(V),(void*)(2*sizeof(float)));
glDrawArrays(GL_LINES,0,(GLsizei)L.size());
}
}
float MeasureTextWidth(const char* text, float size) {
return strlen(text) * size * 0.6f;
}
// Draw a production-ready gradient button
bool DrawButton(const Button &btn, const char* label,
double mx, double my, bool mb) {
struct V { float x,y,r,g,b; };
// Determine states
bool hovered = mx >= btn.x && mx <= btn.x + btn.w
&& my >= btn.y && my <= btn.y + btn.h;
static bool pressedPrev = false;
bool pressed = mb && hovered;
bool clicked = pressed && !pressedPrev;
pressedPrev = pressed;
// Colors
glm::vec3 topColor = hovered ? glm::vec3(0.3f,0.5f,0.8f) : glm::vec3(0.2f,0.4f,0.7f);
glm::vec3 bottomColor = hovered ? glm::vec3(0.1f,0.3f,0.6f) : glm::vec3(0.0f,0.2f,0.5f);
if (pressed) {
topColor *= 0.8f;
bottomColor *= 0.8f;
}
// Vertex data: two triangles forming the quad with gradient
V verts[6] = {
// first triangle
{btn.x, btn.y, bottomColor.r, bottomColor.g, bottomColor.b},
{btn.x + btn.w, btn.y, bottomColor.r, bottomColor.g, bottomColor.b},
{btn.x + btn.w, btn.y + btn.h, topColor.r, topColor.g, topColor.b },
// second triangle
{btn.x, btn.y, bottomColor.r, bottomColor.g, bottomColor.b},
{btn.x + btn.w, btn.y + btn.h, topColor.r, topColor.g, topColor.b },
{btn.x, btn.y + btn.h, topColor.r, topColor.g, topColor.b }
};
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW);
glDrawArrays(GL_TRIANGLES, 0, 6);
// Draw border
struct V2 { float x,y,r,g,b; };
V2 border[5] = {
{btn.x, btn.y, 0,0,0},
{btn.x + btn.w, btn.y, 0,0,0},
{btn.x + btn.w, btn.y + btn.h, 0,0,0},
{btn.x, btn.y + btn.h, 0,0,0},
{btn.x, btn.y, 0,0,0}
};
glBufferData(GL_ARRAY_BUFFER, sizeof(border), border, GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(V2),(void*)0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(V2),(void*)(2*sizeof(float)));
glDrawArrays(GL_LINE_STRIP,0,5);
// Draw label centered
float textWidth = MeasureTextWidth(label, 12.0f);
float tx = btn.x + (btn.w - textWidth) * 0.5f;
float ty = btn.y + (btn.h - 12.0f) * 0.5f;
DrawChar(label[0], tx, ty, 12); // loop over each char if full string support needed
return clicked;
}
// Draw slider at (x,y), width w height h controlling value 01
void DrawSlider(float x,float y,float w,float h,
float &val,bool &drag,
double mx,double my,bool mb){
// handle drag
if(mb && mx>=x && mx<=x+w && my>=y && my<=y+h) drag=true;
if(!mb) drag=false;
if(drag) val = glm::clamp(float((mx-x)/w),0.0f,1.0f);
struct V{float x,y,r,g,b;};
// track bg
std::vector<V> quad = {
{x,y,0.2f,0.2f,0.2f},{x+w,y,0.2f,0.2f,0.2f},
{x+w,y+h,0.2f,0.2f,0.2f},{x,y+h,0.2f,0.2f,0.2f}
};
glBufferData(GL_ARRAY_BUFFER,quad.size()*sizeof(V),quad.data(),GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(V),(void*)0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(V),(void*)(2*sizeof(float)));
glDrawArrays(GL_TRIANGLE_FAN,0,4);
// filled
float fw = w*val;
quad = {
{x,y,0.8f,0.4f,0.4f},{x+fw,y,0.8f,0.4f,0.4f},
{x+fw,y+h,0.8f,0.4f,0.4f},{x,y+h,0.8f,0.4f,0.4f}
};
glBufferData(GL_ARRAY_BUFFER,quad.size()*sizeof(V),quad.data(),GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(V),(void*)0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(V),(void*)(2*sizeof(float)));
glDrawArrays(GL_TRIANGLE_FAN,0,4);
// knob
float kx=x+fw-2, ky=y-2, ks=h+4;
quad = {
{kx,ky,1.0f,1.0f,1.0f},{kx+ks,ky,1.0f,1.0f,1.0f},
{kx+ks,ky+ks,1.0f,1.0f,1.0f},{kx,ky+ks,1.0f,1.0f,1.0f}
};
glBufferData(GL_ARRAY_BUFFER,quad.size()*sizeof(V),quad.data(),GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(V),(void*)0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(V),(void*)(2*sizeof(float)));
glDrawArrays(GL_TRIANGLE_FAN,0,4);
}
public:
LevelEditor() : Engine(3840,2160,"Level Editor").enable_vsync = true {}
bool OnInitialize() override {
// load monkey
try {
monkeyPrim_ = CreateOBJModel("monkey.obj");
}
catch(...) {
monkeyPrim_ = CreateCube(glm::vec3(1.0f, 0.5f, 0.2f));
}
// initial grid - fix glm::vec3 constructor
Place(CreateGrid(20, 1.0f, glm::vec3(0.4f, 0.4f, 0.4f)));
// camera
camera_position_ = glm::vec3(0, 20, 20);
glm::vec3 dir = glm::normalize(glm::vec3(0,0,0) - camera_position_);
camera_target_ = camera_position_ + dir;
return true;
}
void OnUpdate(float dt) override {
// free flight WASD QE
glm::vec3 front = glm::normalize(camera_target_ - camera_position_);
glm::vec3 right = glm::normalize(glm::cross(front, camera_up_));
if(KeyDown(GLFW_KEY_W)) camera_position_ += front * camSpeed_ * dt;
if(KeyDown(GLFW_KEY_S)) camera_position_ -= front * camSpeed_ * dt;
if(KeyDown(GLFW_KEY_A)) camera_position_ -= right * camSpeed_ * dt;
if(KeyDown(GLFW_KEY_D)) camera_position_ += right * camSpeed_ * dt;
if(KeyDown(GLFW_KEY_Q)) camera_position_ -= camera_up_ * camSpeed_ * dt;
if(KeyDown(GLFW_KEY_E)) camera_position_ += camera_up_ * camSpeed_ * dt;
camera_target_ = camera_position_ + front;
// mouse look on RMB
double mx,my; glfwGetCursorPos(window_,&mx,&my);
if(glfwGetMouseButton(window_,GLFW_MOUSE_BUTTON_RIGHT)==GLFW_PRESS)
ProcessMouse(mx,my);
else firstMouse_=true;
// placement keys - fix glm::vec3 constructors
if(KeyDown(GLFW_KEY_1)) Place(CreateCube(glm::vec3(0.8f, 0.2f, 0.2f)));
if(KeyDown(GLFW_KEY_2)) Place(CreateSphere(16, glm::vec3(0.2f, 0.8f, 0.2f)));
if(KeyDown(GLFW_KEY_3)) Place(CreatePlane(5, 5, glm::vec3(0.2f, 0.2f, 0.8f)));
if(KeyDown(GLFW_KEY_4)) Place(CreateGrid(5, 1.0f, glm::vec3(0.5f, 0.5f, 0.5f)));
if(KeyDown(GLFW_KEY_5)) Place(monkeyPrim_);
// picking
if(glfwGetMouseButton(window_,GLFW_MOUSE_BUTTON_LEFT)==GLFW_PRESS)
selected_ = Pick(mx,my);
// move selected with arrows
if(selected_){
glm::vec3 p = selected_->GetPosition();
if(KeyDown(GLFW_KEY_UP)) p.z -= dt*5;
if(KeyDown(GLFW_KEY_DOWN)) p.z += dt*5;
if(KeyDown(GLFW_KEY_LEFT)) p.x -= dt*5;
if(KeyDown(GLFW_KEY_RIGHT)) p.x += dt*5;
selected_->SetPosition(p);
}
Engine::OnUpdate(dt);
}
void OnRender() override {
glUseProgram(shader_program_);
int w = width_, h = height_;
glm::mat4 proj = useOrtho_
? glm::ortho(-30.0f*float(w)/h, 30.0f*float(w)/h, -30.0f, 30.0f, 0.1f, 1000.0f)
: glm::perspective(glm::radians(60.0f), float(w)/h, 0.1f, 1000.0f);
glUniformMatrix4fv(glGetUniformLocation(shader_program_, "uProjection"), 1, GL_FALSE, &proj[0][0]);
// 1) 3D scene + overlay graphs
Engine::OnRender();
// 2) custom 2D UI: sliders + stats
RenderOverlay(); // draws graphs & text
// setup 2D
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glUseProgram(overlay_program);
GLint loc = glGetUniformLocation(overlay_program,"uOrtho");
glm::mat4 ortho = glm::ortho(0.0f,float(width_),0.0f,float(height_));
glUniformMatrix4fv(loc,1,GL_FALSE,&ortho[0][0]);
glBindVertexArray(overlay_vao);
glBindBuffer(GL_ARRAY_BUFFER, overlay_vbo);
// mouse state
double mx,my; glfwGetCursorPos(window_,&mx,&my);
my = height_ - my; // flip Y coordinate for OpenGL
bool mb = glfwGetMouseButton(window_,GLFW_MOUSE_BUTTON_LEFT)==GLFW_PRESS;
// draw sliders and labels
DrawSlider(50,50,200,10, lightR, dragR, mx,my, mb);
DrawSlider(50,70,200,10, lightG, dragG, mx,my, mb);
DrawSlider(50,90,200,10, lightB, dragB, mx,my, mb);
DrawSlider(50,110,200,10,lightI, dragI, mx,my, mb);
if (DrawButton(btnOrtho, useOrtho_ ? "Ortho On" : "Persp On", mx, my, mb)) {
useOrtho_ = !useOrtho_;
}
// draw labels
DrawChar('R',30,50,12);
DrawChar('G',30,70,12);
DrawChar('B',30,90,12);
DrawChar('I',30,110,12);
// update 3D shader light
glUseProgram(shader_program_);
glUniform3f(glGetUniformLocation(shader_program_,"uLightColor"),lightR,lightG,lightB);
glUniform1f(glGetUniformLocation(shader_program_,"uAmbientStrength"),0.1f*lightI);
// restore
glBindVertexArray(0);
glUseProgram(0);
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
void OnShutdown() override {
Engine::OnShutdown();
}
};

223
src/Mesh.cpp Normal file
View File

@@ -0,0 +1,223 @@
// src/Mesh.cpp
#include "Mesh.h"
#include <glad.h>
#include <iostream>
#include <cmath>
// Constructor using spans
Mesh::Mesh(std::span<const Vertex> vertices, std::span<const unsigned int> indices)
: vertex_count_(vertices.size()), index_count_(indices.size()) {
SetupMesh(vertices, indices);
}
// Constructor using moved vectors
Mesh::Mesh(std::vector<Vertex>&& vertices, std::vector<unsigned int>&& indices)
: vertex_count_(vertices.size()), index_count_(indices.size()) {
SetupMesh(vertices, indices);
}
// Move constructor
Mesh::Mesh(Mesh&& other) noexcept
: VAO_(other.VAO_), VBO_(other.VBO_), EBO_(other.EBO_),
vertex_count_(other.vertex_count_), index_count_(other.index_count_) {
other.VAO_ = other.VBO_ = other.EBO_ = 0;
other.vertex_count_ = other.index_count_ = 0;
}
// Move assignment
Mesh& Mesh::operator=(Mesh&& other) noexcept {
if (this != &other) {
Cleanup();
VAO_ = other.VAO_;
VBO_ = other.VBO_;
EBO_ = other.EBO_;
vertex_count_ = other.vertex_count_;
index_count_ = other.index_count_;
other.VAO_ = other.VBO_ = other.EBO_ = 0;
other.vertex_count_ = other.index_count_ = 0;
}
return *this;
}
Mesh::~Mesh() {
Cleanup();
}
void Mesh::SetupMesh(std::span<const Vertex> vertices, std::span<const unsigned int> indices) {
glGenVertexArrays(1, &VAO_);
glGenBuffers(1, &VBO_);
if (!indices.empty()) {
glGenBuffers(1, &EBO_);
}
glBindVertexArray(VAO_);
// Vertex buffer
glBindBuffer(GL_ARRAY_BUFFER, VBO_);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW);
// Index buffer
if (!indices.empty()) {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW);
}
// Setup attributes
SetupVertexAttributes();
glBindVertexArray(0);
}
void Mesh::Cleanup() {
if (EBO_) glDeleteBuffers(1, &EBO_);
if (VBO_) glDeleteBuffers(1, &VBO_);
if (VAO_) glDeleteVertexArrays(1, &VAO_);
}
void Mesh::Draw() const {
glBindVertexArray(VAO_);
if (index_count_ > 0) {
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(index_count_), GL_UNSIGNED_INT, nullptr);
} else {
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(vertex_count_));
}
glBindVertexArray(0);
}
void Mesh::DrawInstanced(int instance_count) const {
glBindVertexArray(VAO_);
if (index_count_ > 0) {
glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(index_count_), GL_UNSIGNED_INT, nullptr, instance_count);
} else {
glDrawArraysInstanced(GL_TRIANGLES, 0, static_cast<GLsizei>(vertex_count_), instance_count);
}
glBindVertexArray(0);
}
std::unique_ptr<Mesh> Mesh::CreateQuad() {
std::vector<Vertex> verts = {
{{-1, -1, 0}, {0,0,1}, {0,0}, {}, {}},
{{ 1, -1, 0}, {0,0,1}, {1,0}, {}, {}},
{{ 1, 1, 0}, {0,0,1}, {1,1}, {}, {}},
{{-1, 1, 0}, {0,0,1}, {0,1}, {}, {}},
};
std::vector<unsigned int> idx = {0,1,2, 2,3,0};
return std::make_unique<Mesh>(std::move(verts), std::move(idx));
}
std::unique_ptr<Mesh> Mesh::CreateCube() {
std::vector<Vertex> verts = {
{{-1, -1, -1}, {0,0,-1}, {0,0}, {}, {}},
{{ 1, -1, -1}, {0,0,-1}, {1,0}, {}, {}},
{{ 1, 1, -1}, {0,0,-1}, {1,1}, {}, {}},
{{-1, 1, -1}, {0,0,-1}, {0,1}, {}, {}},
{{-1, -1, 1}, {0,0, 1}, {0,0}, {}, {}},
{{ 1, -1, 1}, {0,0, 1}, {1,0}, {}, {}},
{{ 1, 1, 1}, {0,0, 1}, {1,1}, {}, {}},
{{-1, 1, 1}, {0,0, 1}, {0,1}, {}, {}},
};
std::vector<unsigned int> idx = {
0,1,2, 2,3,0, // back
4,5,6, 6,7,4, // front
0,4,7, 7,3,0, // left
1,5,6, 6,2,1, // right
3,2,6, 6,7,3, // top
0,1,5, 5,4,0 // bottom
};
return std::make_unique<Mesh>(std::move(verts), std::move(idx));
}
std::unique_ptr<Mesh> Mesh::CreateSphere(int sectors, int stacks) {
std::vector<Vertex> verts;
std::vector<unsigned int> idx;
const float PI = 3.14159265359f;
for (int i = 0; i <= stacks; ++i) {
float stackAngle = PI / 2 - i * PI / stacks;
float xy = cosf(stackAngle);
float z = sinf(stackAngle);
for (int j = 0; j <= sectors; ++j) {
float sectorAngle = j * 2 * PI / sectors;
Vertex v;
v.position = { xy * cosf(sectorAngle), xy * sinf(sectorAngle), z };
v.normal = glm::normalize(v.position);
v.tex_coords = { (float)j / sectors, (float)i / stacks };
verts.push_back(v);
}
}
for (int i = 0; i < stacks; ++i) {
int k1 = i * (sectors + 1);
int k2 = k1 + sectors + 1;
for (int j = 0; j < sectors; ++j) {
if (i != 0) {
idx.push_back(k1 + j);
idx.push_back(k2 + j);
idx.push_back(k1 + j + 1);
}
if (i != (stacks - 1)) {
idx.push_back(k1 + j + 1);
idx.push_back(k2 + j);
idx.push_back(k2 + j + 1);
}
}
}
return std::make_unique<Mesh>(std::move(verts), std::move(idx));
}
std::unique_ptr<Mesh> Mesh::CreatePlane(float width, float height, int subdivisions) {
std::vector<Vertex> verts;
std::vector<unsigned int> idx;
int rows = subdivisions + 1;
int cols = subdivisions + 1;
for (int y = 0; y < rows; ++y) {
for (int x = 0; x < cols; ++x) {
float fx = (float)x / subdivisions * width - width / 2;
float fy = (float)y / subdivisions * height - height / 2;
verts.push_back({ {fx, fy, 0}, {0,0,1}, {(float)x/subdivisions, (float)y/subdivisions}, {}, {} });
}
}
for (int y = 0; y < subdivisions; ++y) {
for (int x = 0; x < subdivisions; ++x) {
int i0 = y * cols + x;
int i1 = i0 + 1;
int i2 = i0 + cols;
int i3 = i2 + 1;
idx.push_back(i0); idx.push_back(i2); idx.push_back(i1);
idx.push_back(i1); idx.push_back(i2); idx.push_back(i3);
}
}
return std::make_unique<Mesh>(std::move(verts), std::move(idx));
}
void Mesh::UpdateVertices(std::span<const Vertex> new_vertices) {
glBindBuffer(GL_ARRAY_BUFFER, VBO_);
glBufferSubData(GL_ARRAY_BUFFER, 0, new_vertices.size() * sizeof(Vertex), new_vertices.data());
glBindBuffer(GL_ARRAY_BUFFER, 0);
vertex_count_ = new_vertices.size();
}
void Mesh::UpdateIndices(std::span<const unsigned int> new_indices) {
if (!EBO_) {
glGenBuffers(1, &EBO_);
}
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, new_indices.size() * sizeof(unsigned int), new_indices.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
index_count_ = new_indices.size();
}
glm::vec3 Mesh::CalculateCenter() const {
// User should implement actual center calculation
return glm::vec3(0.0f);
}
std::pair<glm::vec3, glm::vec3> Mesh::CalculateBoundingBox() const {
// User should implement bounding box calculation
return { glm::vec3(0.0f), glm::vec3(0.0f) };
}
void Mesh::PrintInfo() const {
std::cout << "Mesh - Vertices: " << vertex_count_
<< ", Indices: " << index_count_
<< ", VAO: " << VAO_ << "\n";
}

92
src/Mesh.h Normal file
View File

@@ -0,0 +1,92 @@
#pragma once
#include <vector>
#include <memory>
#include <span>
#include <glm/glm.hpp>
#include "glad.h"
// Vertex structure with alignment for GPU (C++11 alignas)
struct alignas(16) Vertex {
glm::vec3 position{0.0f};
glm::vec3 normal{0.0f, 1.0f, 0.0f};
glm::vec2 tex_coords{0.0f};
glm::vec3 tangent{0.0f};
glm::vec3 bitangent{0.0f};
};
// Modern mesh class using C++23 features
class Mesh {
public:
// Constructor using spans (C++20) for better safety
explicit Mesh(std::span<const Vertex> vertices, std::span<const unsigned int> indices = {});
// Constructor with move semantics for large meshes
Mesh(std::vector<Vertex>&& vertices, std::vector<unsigned int>&& indices = {});
~Mesh();
// Delete copy constructor and assignment (expensive operation)
Mesh(const Mesh&) = delete;
Mesh& operator=(const Mesh&) = delete;
// Move semantics
Mesh(Mesh&& other) noexcept;
Mesh& operator=(Mesh&& other) noexcept;
void Draw() const;
void DrawInstanced(int instance_count) const;
// Modern getters using [[nodiscard]]
[[nodiscard]] size_t GetVertexCount() const { return vertex_count_; }
[[nodiscard]] size_t GetIndexCount() const { return index_count_; }
[[nodiscard]] bool HasIndices() const { return index_count_ > 0; }
[[nodiscard]] GLuint GetVAO() const { return VAO_; }
// Static factory methods for common shapes
[[nodiscard]] static std::unique_ptr<Mesh> CreateQuad();
[[nodiscard]] static std::unique_ptr<Mesh> CreateCube();
[[nodiscard]] static std::unique_ptr<Mesh> CreateSphere(int sectors = 36, int stacks = 18);
[[nodiscard]] static std::unique_ptr<Mesh> CreatePlane(float width = 1.0f, float height = 1.0f, int subdivisions = 1);
// Utility functions
void UpdateVertices(std::span<const Vertex> new_vertices);
void UpdateIndices(std::span<const unsigned int> new_indices);
// Calculate mesh properties
[[nodiscard]] glm::vec3 CalculateCenter() const;
[[nodiscard]] std::pair<glm::vec3, glm::vec3> CalculateBoundingBox() const;
// Debug functions
void PrintInfo() const;
private:
GLuint VAO_ = 0, VBO_ = 0, EBO_ = 0;
size_t vertex_count_ = 0;
size_t index_count_ = 0;
void SetupMesh(std::span<const Vertex> vertices, std::span<const unsigned int> indices);
void Cleanup();
// Vertex attribute setup using constexpr (C++11)
static constexpr void SetupVertexAttributes() {
// Position attribute (location = 0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
glEnableVertexAttribArray(0);
// Normal attribute (location = 1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
glEnableVertexAttribArray(1);
// Texture coordinate attribute (location = 2)
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, tex_coords));
glEnableVertexAttribArray(2);
// Tangent attribute (location = 3)
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, tangent));
glEnableVertexAttribArray(3);
// Bitangent attribute (location = 4)
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, bitangent));
glEnableVertexAttribArray(4);
}
};

144
src/OBJLoader.cpp Normal file
View File

@@ -0,0 +1,144 @@
#include "OBJLoader.h"
#include <fstream>
#include <sstream>
#include <iostream>
#include <algorithm>
bool OBJLoader::LoadOBJ(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) {
std::cerr << "Failed to open OBJ file: " << filepath << std::endl;
return false;
}
std::string line;
while (std::getline(file, line)) {
// Skip empty lines and comments
if (line.empty() || line[0] == '#') {
continue;
}
ProcessLine(line);
}
file.close();
BuildVertexArray();
return true;
}
void OBJLoader::ProcessLine(const std::string& line) {
std::istringstream iss(line);
std::string prefix;
iss >> prefix;
if (prefix == "v") {
ProcessVertex(line);
} else if (prefix == "vt") {
ProcessTexCoord(line);
} else if (prefix == "vn") {
ProcessNormal(line);
} else if (prefix == "f") {
ProcessFace(line);
} else if (prefix == "mtllib") {
iss >> materialFile;
}
}
void OBJLoader::ProcessVertex(const std::string& line) {
std::istringstream iss(line);
std::string prefix;
glm::vec3 vertex;
iss >> prefix >> vertex.x >> vertex.y >> vertex.z;
temp_vertices.push_back(vertex);
}
void OBJLoader::ProcessTexCoord(const std::string& line) {
std::istringstream iss(line);
std::string prefix;
glm::vec2 texCoord;
iss >> prefix >> texCoord.x >> texCoord.y;
temp_texCoords.push_back(texCoord);
}
void OBJLoader::ProcessNormal(const std::string& line) {
std::istringstream iss(line);
std::string prefix;
glm::vec3 normal;
iss >> prefix >> normal.x >> normal.y >> normal.z;
temp_normals.push_back(normal);
}
void OBJLoader::ProcessFace(const std::string& line) {
// Handle faces like: f 1/1/1 2/2/2 3/3/3 or f 1//1 2//2 3//3 or f 1 2 3
std::istringstream iss(line);
std::string prefix;
iss >> prefix;
OBJFace face;
std::string vertex;
int vertexCount = 0;
while (iss >> vertex && vertexCount < 3) {
std::replace(vertex.begin(), vertex.end(), '/', ' ');
std::istringstream vertexStream(vertex);
int vertexIndex = 0, texIndex = 0, normalIndex = 0;
vertexStream >> vertexIndex;
if (vertexStream >> texIndex) {
if (vertexStream >> normalIndex) {
// All three indices present
}
}
// OBJ indices start at 1, convert to 0-based
face.vertexIndices[vertexCount] = vertexIndex - 1;
face.texCoordIndices[vertexCount] = texIndex > 0 ? texIndex - 1 : -1;
face.normalIndices[vertexCount] = normalIndex > 0 ? normalIndex - 1 : -1;
vertexCount++;
}
if (vertexCount == 3) {
faces.push_back(face);
}
}
void OBJLoader::BuildVertexArray() {
vertices.clear();
indices.clear();
for (const auto& face : faces) {
for (int i = 0; i < 3; ++i) {
OBJVertex vertex;
// Position
if (face.vertexIndices[i] >= 0 && face.vertexIndices[i] < temp_vertices.size()) {
vertex.position = temp_vertices[face.vertexIndices[i]];
}
// Texture coordinates
if (face.texCoordIndices[i] >= 0 && face.texCoordIndices[i] < temp_texCoords.size()) {
vertex.texCoord = temp_texCoords[face.texCoordIndices[i]];
} else {
vertex.texCoord = glm::vec2(0.0f);
}
// Normal
if (face.normalIndices[i] >= 0 && face.normalIndices[i] < temp_normals.size()) {
vertex.normal = temp_normals[face.normalIndices[i]];
} else {
vertex.normal = glm::vec3(0.0f, 1.0f, 0.0f); // Default up normal
}
// Default white color
vertex.color = glm::vec3(1.0f);
vertices.push_back(vertex);
indices.push_back(static_cast<unsigned int>(vertices.size() - 1));
}
}
}

40
src/OBJLoader.h Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include <vector>
#include <string>
#include <glm/glm.hpp>
#include <glad.h>
struct OBJVertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
glm::vec3 color;
};
struct OBJFace {
int vertexIndices[3];
int texCoordIndices[3];
int normalIndices[3];
};
class OBJLoader {
public:
std::vector<OBJVertex> vertices;
std::vector<unsigned int> indices;
std::string materialFile;
bool LoadOBJ(const std::string& filepath);
private:
std::vector<glm::vec3> temp_vertices;
std::vector<glm::vec2> temp_texCoords;
std::vector<glm::vec3> temp_normals;
std::vector<OBJFace> faces;
void ProcessLine(const std::string& line);
void ProcessVertex(const std::string& line);
void ProcessTexCoord(const std::string& line);
void ProcessNormal(const std::string& line);
void ProcessFace(const std::string& line);
void BuildVertexArray();
};

89
src/OBJModelPrimitive.cpp Normal file
View File

@@ -0,0 +1,89 @@
#include "OBJModelPrimitive.h"
#include <iostream>
OBJModelPrimitive::OBJModelPrimitive(const std::string& objPath, const std::string& texturePath)
: textureID_(0), hasTexture_(false) {
// Load OBJ file
OBJLoader loader;
if (!loader.LoadOBJ(objPath)) {
std::cerr << "Failed to load OBJ file: " << objPath << std::endl;
return;
}
// Load texture if provided
if (!texturePath.empty()) {
if (texturePath.ends_with(".bmp")) {
textureID_ = SimpleTextureLoader::LoadBMP(texturePath);
} else if (texturePath.ends_with(".tga")) {
textureID_ = SimpleTextureLoader::LoadTGA(texturePath);
}
hasTexture_ = (textureID_ != 0);
}
// Create VAO/VBO/EBO
glGenVertexArrays(1, &vao_);
glGenBuffers(1, &vbo_);
glGenBuffers(1, &ebo_);
glBindVertexArray(vao_);
// Upload vertex data (position + color + normal + texcoord = 11 floats)
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferData(GL_ARRAY_BUFFER, loader.vertices.size() * sizeof(OBJVertex),
loader.vertices.data(), GL_STATIC_DRAW);
// Upload indices
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, loader.indices.size() * sizeof(unsigned int),
loader.indices.data(), GL_STATIC_DRAW);
// Position attribute (location 0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(OBJVertex), (void*)offsetof(OBJVertex, position));
glEnableVertexAttribArray(0);
// Color attribute (location 1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(OBJVertex), (void*)offsetof(OBJVertex, color));
glEnableVertexAttribArray(1);
// Normal attribute (location 2)
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(OBJVertex), (void*)offsetof(OBJVertex, normal));
glEnableVertexAttribArray(2);
// Texture coordinate attribute (location 3)
glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, sizeof(OBJVertex), (void*)offsetof(OBJVertex, texCoord));
glEnableVertexAttribArray(3);
glBindVertexArray(0);
index_count_ = static_cast<GLsizei>(loader.indices.size());
}
OBJModelPrimitive::~OBJModelPrimitive() {
Cleanup();
}
void OBJModelPrimitive::Render() {
if (hasTexture_) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID_);
}
glBindVertexArray(vao_);
glDrawElements(GL_TRIANGLES, index_count_, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
if (hasTexture_) {
glBindTexture(GL_TEXTURE_2D, 0);
}
}
void OBJModelPrimitive::Bind() {
glBindVertexArray(vao_);
}
void OBJModelPrimitive::Cleanup() {
if (vao_) glDeleteVertexArrays(1, &vao_);
if (vbo_) glDeleteBuffers(1, &vbo_);
if (ebo_) glDeleteBuffers(1, &ebo_);
if (textureID_) glDeleteTextures(1, &textureID_);
}

18
src/OBJModelPrimitive.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include "Engine.h"
#include "OBJLoader.h"
#include "SimpleTextureLoader.h"
class OBJModelPrimitive : public Primitive {
public:
OBJModelPrimitive(const std::string& objPath, const std::string& texturePath = "");
~OBJModelPrimitive();
void Render() override;
void Bind() override;
void Cleanup() override;
private:
GLuint textureID_;
bool hasTexture_;
};

129
src/Renderer.cpp Normal file
View File

@@ -0,0 +1,129 @@
// src/Renderer.cpp
#include "Renderer.h"
#include <glad.h>
#include <iostream>
#include <chrono>
Renderer::Renderer() = default;
void Renderer::Initialize() {
// Nothing specific for now; ensure GLAD is loaded before this call
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
void Renderer::Shutdown() {
// Cleanup if needed
}
void Renderer::BeginFrame() {
stats_.draw_calls = 0;
stats_.vertices_rendered = 0;
stats_.triangles_rendered = 0;
frame_start_time_ = GetTime();
}
void Renderer::EndFrame() {
float frame_end = GetTime();
stats_.frame_time = frame_end - frame_start_time_;
}
void Renderer::Clear(const glm::vec4& color) {
glClearColor(color.r, color.g, color.b, color.a);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void Renderer::SetViewport(int x, int y, int width, int height) {
viewport_size_ = { width, height };
glViewport(x, y, width, height);
}
void Renderer::SetCullFace(CullFace c) {
if (c == CullFace::None) glDisable(GL_CULL_FACE);
else {
glEnable(GL_CULL_FACE);
glCullFace(static_cast<GLenum>(c));
}
current_cull_face_ = c;
}
void Renderer::SetBlendMode(BlendMode mode) {
if (mode == BlendMode::None) {
glDisable(GL_BLEND);
} else {
glEnable(GL_BLEND);
switch (mode) {
case BlendMode::Alpha: glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break;
case BlendMode::Additive: glBlendFunc(GL_SRC_ALPHA, GL_ONE); break;
case BlendMode::Multiply: glBlendFunc(GL_DST_COLOR, GL_ZERO); break;
default: break;
}
}
current_blend_mode_ = mode;
}
void Renderer::SetDepthTest(DepthTest dt) {
if (dt == DepthTest::None) glDisable(GL_DEPTH_TEST);
else {
glEnable(GL_DEPTH_TEST);
GLenum func = GL_LESS;
switch (dt) {
case DepthTest::Less: func = GL_LESS; break;
case DepthTest::LessEqual: func = GL_LEQUAL; break;
case DepthTest::Greater: func = GL_GREATER; break;
case DepthTest::GreaterEqual:func = GL_GEQUAL; break;
case DepthTest::Equal: func = GL_EQUAL; break;
case DepthTest::NotEqual: func = GL_NOTEQUAL; break;
case DepthTest::Always: func = GL_ALWAYS; break;
case DepthTest::Never: func = GL_NEVER; break;
default: break;
}
glDepthFunc(func);
}
current_depth_test_ = dt;
}
void Renderer::SetWireframe(bool enable) {
glPolygonMode(GL_FRONT_AND_BACK, enable ? GL_LINE : GL_FILL);
wireframe_enabled_ = enable;
}
void Renderer::PrintRenderInfo() const {
std::cout << "OpenGL Version: " << GetOpenGLVersion() << "\n";
std::cout << "Viewport: " << viewport_size_.x << "x" << viewport_size_.y << "\n";
std::cout << "Draw Calls: " << stats_.draw_calls << "\n";
std::cout << "Vertices: " << stats_.vertices_rendered << "\n";
std::cout << "Triangles: " << stats_.triangles_rendered << "\n";
}
void Renderer::ResetStats() {
stats_ = RenderStats{};
}
std::string Renderer::GetOpenGLVersion() {
return reinterpret_cast<const char*>(glGetString(GL_VERSION));
}
std::string Renderer::GetGPUInfo() {
return reinterpret_cast<const char*>(glGetString(GL_RENDERER));
}
bool Renderer::CheckOpenGLError(const std::string& op) {
GLenum err;
bool hasError = false;
while ((err = glGetError()) != GL_NO_ERROR) {
std::cerr << "OpenGL Error";
if (!op.empty()) std::cerr << " in " << op;
std::cerr << ": " << err << "\n";
hasError = true;
}
return !hasError;
}
float Renderer::GetTime() const {
using clock = std::chrono::high_resolution_clock;
return std::chrono::duration<float>(clock::now().time_since_epoch()).count();
}

96
src/Renderer.h Normal file
View File

@@ -0,0 +1,96 @@
#pragma once
#include <glm/glm.hpp>
#include <string>
#include "glad.h"
// Modern renderer using C++23 features
class Renderer {
public:
// Render states using scoped enum
enum class CullFace : GLenum {
None = 0,
Front = GL_FRONT,
Back = GL_BACK,
FrontAndBack = GL_FRONT_AND_BACK
};
enum class BlendMode : int {
None = 0,
Alpha,
Additive,
Multiply
};
enum class DepthTest : int {
None = 0,
Less,
LessEqual,
Greater,
GreaterEqual,
Equal,
NotEqual,
Always,
Never
};
struct RenderStats {
size_t draw_calls = 0;
size_t vertices_rendered = 0;
size_t triangles_rendered = 0;
float frame_time = 0.0f;
};
Renderer();
~Renderer() = default;
// Delete copy constructor and assignment
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
void Initialize();
void Shutdown();
// Frame management
void BeginFrame();
void EndFrame();
void Clear(const glm::vec4& color = {0.0f, 0.0f, 0.0f, 1.0f});
void SetViewport(int x, int y, int width, int height);
// Render state management
void SetCullFace(CullFace cull_face);
void SetBlendMode(BlendMode blend_mode);
void SetDepthTest(DepthTest depth_test);
void SetWireframe(bool enable);
// Modern getters using [[nodiscard]]
[[nodiscard]] const RenderStats& GetStats() const { return stats_; }
[[nodiscard]] glm::ivec2 GetViewportSize() const { return viewport_size_; }
[[nodiscard]] bool IsWireframeEnabled() const { return wireframe_enabled_; }
// Debug functions
void PrintRenderInfo() const;
void ResetStats();
// Utility functions
[[nodiscard]] static std::string GetOpenGLVersion();
[[nodiscard]] static std::string GetGPUInfo();
[[nodiscard]] static bool CheckOpenGLError(const std::string& operation = "");
private:
RenderStats stats_;
glm::ivec2 viewport_size_{0, 0};
// Current render states
CullFace current_cull_face_ = CullFace::Back;
BlendMode current_blend_mode_ = BlendMode::None;
DepthTest current_depth_test_ = DepthTest::Less;
bool wireframe_enabled_ = false;
// Performance tracking
float frame_start_time_ = 0.0f;
void SetOpenGLState();
void UpdateStats();
[[nodiscard]] float GetTime() const;
};

350
src/ResourceManager.cpp Normal file
View File

@@ -0,0 +1,350 @@
// src/ResourceManager.cpp
#include "ResourceManager.h"
#include "Shader.h"
#include "Mesh.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <unordered_set>
#include <algorithm>
// Static member definitions
std::unordered_map<std::string, std::shared_ptr<Shader>> ResourceManager::shaders_;
std::unordered_map<std::string, std::shared_ptr<Texture>> ResourceManager::textures_;
std::unordered_map<std::string, std::shared_ptr<Mesh>> ResourceManager::meshes_;
bool ResourceManager::hot_reload_enabled_ = false;
std::unordered_map<std::string, std::filesystem::file_time_type> ResourceManager::shader_timestamps_;
std::unordered_map<std::string, std::filesystem::path> ResourceManager::shader_paths_;
std::unordered_map<std::string, std::filesystem::file_time_type> ResourceManager::texture_timestamps_;
std::unordered_map<std::string, std::filesystem::path> ResourceManager::texture_paths_;
std::expected<std::shared_ptr<Shader>, std::string>
ResourceManager::LoadShader(const std::string& name,
const std::filesystem::path& vert,
const std::filesystem::path& frag) {
try {
auto shader = std::make_shared<Shader>(vert, frag);
if (!shader->IsValid()) {
return std::unexpected("Shader compilation/linking failed: " + name);
}
shaders_[name] = shader;
if (hot_reload_enabled_) {
shader_timestamps_[name] = std::filesystem::last_write_time(vert);
shader_paths_[name] = vert;
}
return shader;
} catch (const std::exception& e) {
return std::unexpected(std::string("Exception loading shader ") + name + ": " + e.what());
}
}
std::expected<std::shared_ptr<Shader>, std::string>
ResourceManager::LoadShader(const std::string& name,
const std::filesystem::path& vert,
const std::filesystem::path& geom,
const std::filesystem::path& frag) {
try {
auto shader = std::make_shared<Shader>(vert, geom, frag);
if (!shader->IsValid()) {
return std::unexpected("Shader compilation/linking failed: " + name);
}
shaders_[name] = shader;
if (hot_reload_enabled_) {
shader_timestamps_[name] = std::filesystem::last_write_time(vert);
shader_paths_[name] = vert;
}
return shader;
} catch (const std::exception& e) {
return std::unexpected(std::string("Exception loading shader ") + name + ": " + e.what());
}
}
std::shared_ptr<Shader> ResourceManager::GetShader(const std::string& name) {
auto it = shaders_.find(name);
return (it != shaders_.end()) ? it->second : nullptr;
}
std::expected<std::shared_ptr<Texture>, std::string>
ResourceManager::LoadTexture(const std::string& name,
const std::filesystem::path& path,
bool generate_mipmaps) {
try {
// TODO: Replace this stub with actual texture loading
// auto texture = std::make_shared<Texture>(path, generate_mipmaps);
// if (!texture->IsValid()) {
// return std::unexpected("Texture loading failed: " + name);
// }
// textures_[name] = texture;
if (hot_reload_enabled_) {
texture_timestamps_[name] = std::filesystem::last_write_time(path);
texture_paths_[name] = path;
}
// For now, return error since texture loading is not implemented
return std::unexpected("Texture loading not implemented");
} catch (const std::exception& e) {
return std::unexpected(std::string("Exception loading texture ") + name + ": " + e.what());
}
}
std::shared_ptr<Texture> ResourceManager::GetTexture(const std::string& name) {
auto it = textures_.find(name);
return (it != textures_.end()) ? it->second : nullptr;
}
void ResourceManager::RegisterMesh(const std::string& name, std::shared_ptr<Mesh> mesh) {
meshes_[name] = std::move(mesh);
}
std::shared_ptr<Mesh> ResourceManager::GetMesh(const std::string& name) {
auto it = meshes_.find(name);
return (it != meshes_.end()) ? it->second : nullptr;
}
void ResourceManager::ClearShaders() {
shaders_.clear();
shader_timestamps_.clear();
shader_paths_.clear();
}
void ResourceManager::ClearTextures() {
textures_.clear();
texture_timestamps_.clear();
texture_paths_.clear();
}
void ResourceManager::ClearMeshes() {
meshes_.clear();
}
void ResourceManager::ClearAll() {
ClearShaders();
ClearTextures();
ClearMeshes();
}
void ResourceManager::PrintResourceInfo() {
std::cout << "Shaders: " << shaders_.size()
<< ", Textures: " << textures_.size()
<< ", Meshes: " << meshes_.size() << "\n";
}
bool ResourceManager::HasShader(const std::string& name) {
return shaders_.count(name) > 0;
}
bool ResourceManager::HasTexture(const std::string& name) {
return textures_.count(name) > 0;
}
bool ResourceManager::HasMesh(const std::string& name) {
return meshes_.count(name) > 0;
}
void ResourceManager::LoadShadersFromDirectory(const std::filesystem::path& dir) {
if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) {
std::cerr << "Directory does not exist: " << dir << "\n";
return;
}
std::unordered_map<std::string, std::filesystem::path> vert_files;
std::unordered_map<std::string, std::filesystem::path> frag_files;
std::unordered_map<std::string, std::filesystem::path> geom_files;
// Collect shader files by base name
for (const auto& entry : std::filesystem::directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
auto path = entry.path();
auto base_name = path.stem().string();
auto extension = path.extension().string();
if (extension == ".vert") {
vert_files[base_name] = path;
} else if (extension == ".frag") {
frag_files[base_name] = path;
} else if (extension == ".geom") {
geom_files[base_name] = path;
}
}
// Match and load shader pairs/triplets
for (const auto& [base_name, vert_path] : vert_files) {
auto frag_it = frag_files.find(base_name);
if (frag_it != frag_files.end()) {
auto geom_it = geom_files.find(base_name);
if (geom_it != geom_files.end()) {
// Load with geometry shader
auto result = LoadShader(base_name, vert_path, geom_it->second, frag_it->second);
if (result) {
std::cout << "Loaded shader with geometry: " << base_name << "\n";
} else {
std::cerr << "Failed to load shader " << base_name << ": " << result.error() << "\n";
}
} else {
// Load without geometry shader
auto result = LoadShader(base_name, vert_path, frag_it->second);
if (result) {
std::cout << "Loaded shader: " << base_name << "\n";
} else {
std::cerr << "Failed to load shader " << base_name << ": " << result.error() << "\n";
}
}
}
}
}
void ResourceManager::LoadTexturesFromDirectory(const std::filesystem::path& dir) {
if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) {
std::cerr << "Directory does not exist: " << dir << "\n";
return;
}
// Common image extensions using unordered_set instead of set
std::unordered_set<std::string> image_extensions = {".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds"};
for (const auto& entry : std::filesystem::directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
auto path = entry.path();
auto extension = path.extension().string();
// Convert to lowercase for comparison
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
if (image_extensions.count(extension)) {
auto name = path.stem().string();
auto result = LoadTexture(name, path);
if (result) {
std::cout << "Loaded texture: " << name << "\n";
} else {
std::cerr << "Failed to load texture " << name << ": " << result.error() << "\n";
}
}
}
}
void ResourceManager::ReloadShader(const std::string& name) {
auto shader_it = shaders_.find(name);
if (shader_it == shaders_.end()) {
std::cerr << "Cannot reload shader: " << name << " not found\n";
return;
}
auto path_it = shader_paths_.find(name);
if (path_it == shader_paths_.end()) {
std::cerr << "Cannot reload shader: " << name << " path not stored\n";
return;
}
try {
// Determine fragment shader path (assume same base name, different extension)
auto vert_path = path_it->second;
auto frag_path = vert_path;
frag_path.replace_extension(".frag");
// Check if geometry shader exists
auto geom_path = vert_path;
geom_path.replace_extension(".geom");
if (std::filesystem::exists(geom_path)) {
// Reload with geometry shader
auto new_shader = std::make_shared<Shader>(vert_path, geom_path, frag_path);
if (new_shader->IsValid()) {
shaders_[name] = new_shader;
shader_timestamps_[name] = std::filesystem::last_write_time(vert_path);
std::cout << "Reloaded shader with geometry: " << name << "\n";
} else {
std::cerr << "Failed to reload shader with geometry: " << name << "\n";
}
} else {
// Reload without geometry shader
auto new_shader = std::make_shared<Shader>(vert_path, frag_path);
if (new_shader->IsValid()) {
shaders_[name] = new_shader;
shader_timestamps_[name] = std::filesystem::last_write_time(vert_path);
std::cout << "Reloaded shader: " << name << "\n";
} else {
std::cerr << "Failed to reload shader: " << name << "\n";
}
}
} catch (const std::exception& e) {
std::cerr << "Exception reloading shader " << name << ": " << e.what() << "\n";
}
}
void ResourceManager::ReloadTexture(const std::string& name) {
auto texture_it = textures_.find(name);
if (texture_it == textures_.end()) {
std::cerr << "Cannot reload texture: " << name << " not found\n";
return;
}
auto path_it = texture_paths_.find(name);
if (path_it == texture_paths_.end()) {
std::cerr << "Cannot reload texture: " << name << " path not stored\n";
return;
}
try {
// TODO: Replace this with actual texture reloading
// auto new_texture = std::make_shared<Texture>(path_it->second);
// if (new_texture->IsValid()) {
// textures_[name] = new_texture;
// texture_timestamps_[name] = std::filesystem::last_write_time(path_it->second);
// std::cout << "Reloaded texture: " << name << "\n";
// } else {
// std::cerr << "Failed to reload texture: " << name << "\n";
// }
// For now, just update timestamp
texture_timestamps_[name] = std::filesystem::last_write_time(path_it->second);
std::cout << "Texture reload placeholder for: " << name << "\n";
} catch (const std::exception& e) {
std::cerr << "Exception reloading texture " << name << ": " << e.what() << "\n";
}
}
void ResourceManager::CheckForChanges() {
if (!hot_reload_enabled_) return;
// Check for shader changes
for (const auto& [name, shader] : shaders_) {
auto path_it = shader_paths_.find(name);
if (path_it == shader_paths_.end()) continue;
try {
auto current = std::filesystem::last_write_time(path_it->second);
auto stored_it = shader_timestamps_.find(name);
if (stored_it != shader_timestamps_.end() && current != stored_it->second) {
std::cout << "Detected change in shader: " << name << "\n";
ReloadShader(name);
}
} catch (const std::filesystem::filesystem_error& e) {
std::cerr << "Error checking shader file " << name << ": " << e.what() << "\n";
}
}
// Check for texture changes
for (const auto& [name, texture] : textures_) {
auto path_it = texture_paths_.find(name);
if (path_it == texture_paths_.end()) continue;
try {
auto current = std::filesystem::last_write_time(path_it->second);
auto stored_it = texture_timestamps_.find(name);
if (stored_it != texture_timestamps_.end() && current != stored_it->second) {
std::cout << "Detected change in texture: " << name << "\n";
ReloadTexture(name);
texture_timestamps_[name] = current;
}
} catch (const std::filesystem::filesystem_error& e) {
std::cerr << "Error checking texture file " << name << ": " << e.what() << "\n";
}
}
}

95
src/ResourceManager.h Normal file
View File

@@ -0,0 +1,95 @@
#pragma once
#include "Shader.h"
#include "Mesh.h"
#include <string>
#include <memory>
#include <unordered_map>
#include <filesystem>
#include <expected>
// Forward declarations
class Texture;
// Modern resource manager using C++23 features
class ResourceManager {
public:
// Delete constructor - this is a static-only class
ResourceManager() = delete;
~ResourceManager() = delete;
ResourceManager(const ResourceManager&) = delete;
ResourceManager& operator=(const ResourceManager&) = delete;
static bool IsHotReloadEnabled();
// Shader management using std::expected (C++23)
[[nodiscard]] static std::expected<std::shared_ptr<Shader>, std::string>
LoadShader(const std::string& name,
const std::filesystem::path& vertex_path,
const std::filesystem::path& fragment_path);
[[nodiscard]] static std::expected<std::shared_ptr<Shader>, std::string>
LoadShader(const std::string& name,
const std::filesystem::path& vertex_path,
const std::filesystem::path& geometry_path,
const std::filesystem::path& fragment_path);
[[nodiscard]] static std::shared_ptr<Shader> GetShader(const std::string& name);
// Texture management
[[nodiscard]] static std::expected<std::shared_ptr<Texture>, std::string>
LoadTexture(const std::string& name, const std::filesystem::path& path, bool generate_mipmaps = true);
[[nodiscard]] static std::shared_ptr<Texture> GetTexture(const std::string& name);
// Mesh management (for commonly used meshes)
static void RegisterMesh(const std::string& name, std::shared_ptr<Mesh> mesh);
[[nodiscard]] static std::shared_ptr<Mesh> GetMesh(const std::string& name);
// Resource cleanup
static void ClearShaders();
static void ClearTextures();
static void ClearMeshes();
static void ClearAll();
// Resource information
[[nodiscard]] static size_t GetShaderCount() { return shaders_.size(); }
[[nodiscard]] static size_t GetTextureCount() { return textures_.size(); }
[[nodiscard]] static size_t GetMeshCount() { return meshes_.size(); }
// Debug functions
static void PrintResourceInfo();
[[nodiscard]] static bool HasShader(const std::string& name);
[[nodiscard]] static bool HasTexture(const std::string& name);
[[nodiscard]] static bool HasMesh(const std::string& name);
// Batch loading functions
static void LoadShadersFromDirectory(const std::filesystem::path& directory);
static void LoadTexturesFromDirectory(const std::filesystem::path& directory);
// Hot reloading support (for development)
static void EnableHotReload(bool enable) { hot_reload_enabled_ = enable; }
static void CheckForChanges(); // Call this each frame in debug builds
private:
static std::unordered_map<std::string, std::shared_ptr<Shader>> shaders_;
static std::unordered_map<std::string, std::shared_ptr<Texture>> textures_;
static std::unordered_map<std::string, std::shared_ptr<Mesh>> meshes_;
// Hot reload support
static bool hot_reload_enabled_;
static std::unordered_map<std::string, std::filesystem::file_time_type> shader_timestamps_;
static std::unordered_map<std::string, std::filesystem::file_time_type> texture_timestamps_;
static std::unordered_map<std::string, std::filesystem::path> texture_paths_;
// Helper functions
[[nodiscard]] static bool IsShaderFile(const std::filesystem::path& path);
[[nodiscard]] static bool IsTextureFile(const std::filesystem::path& path);
static void ReloadShader(const std::string& name);
static void ReloadTexture(const std::string& name);
static std::unordered_map<std::string, std::filesystem::path> shader_paths_;
};

215
src/Scene.cpp Normal file
View File

@@ -0,0 +1,215 @@
// src/Scene.cpp
#include "Scene.h"
#include "Shader.h"
#include <glm/gtc/matrix_inverse.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>
// Add a node with a full transform matrix
size_t Scene::AddNode(std::shared_ptr<Mesh> mesh, const glm::mat4& transform) {
SceneNode node;
node.mesh = std::move(mesh);
node.transform = transform;
node.UpdateTransform();
nodes_.push_back(node);
node_visibility_cache_.push_back(true);
return nodes_.size() - 1;
}
// Add a node with position/rotation/scale decomposition
size_t Scene::AddNode(std::shared_ptr<Mesh> mesh,
const glm::vec3& position,
const glm::vec3& rotation,
const glm::vec3& scale) {
SceneNode node;
node.mesh = std::move(mesh);
node.position = position;
node.rotation = rotation;
node.scale = scale;
node.UpdateTransform();
nodes_.push_back(node);
node_visibility_cache_.push_back(true);
return nodes_.size() - 1;
}
void Scene::SceneNode::UpdateTransform() {
// Create transformation matrix from position, rotation, and scale
glm::mat4 T = glm::translate(glm::mat4(1.0f), position);
glm::mat4 R = glm::rotate(glm::mat4(1.0f), rotation.x, glm::vec3(1, 0, 0));
R = glm::rotate(R, rotation.y, glm::vec3(0, 1, 0));
R = glm::rotate(R, rotation.z, glm::vec3(0, 0, 1));
glm::mat4 S = glm::scale(glm::mat4(1.0f), scale);
transform = T * R * S;
}
void Scene::RemoveNode(size_t index) {
if (index < nodes_.size()) {
nodes_.erase(nodes_.begin() + index);
node_visibility_cache_.erase(node_visibility_cache_.begin() + index);
}
}
void Scene::SetNodeTransform(size_t index, const glm::mat4& transform) {
if (index < nodes_.size()) {
nodes_[index].transform = transform;
nodes_[index].UpdateTransform();
}
}
void Scene::SetNodePosition(size_t index, const glm::vec3& position) {
if (index < nodes_.size()) {
nodes_[index].position = position;
nodes_[index].UpdateTransform();
}
}
void Scene::SetNodeRotation(size_t index, const glm::vec3& rotation) {
if (index < nodes_.size()) {
nodes_[index].rotation = rotation;
nodes_[index].UpdateTransform();
}
}
void Scene::SetNodeScale(size_t index, const glm::vec3& scale) {
if (index < nodes_.size()) {
nodes_[index].scale = scale;
nodes_[index].UpdateTransform();
}
}
void Scene::SetNodeVisibility(size_t index, bool visible) {
if (index < nodes_.size()) {
nodes_[index].visible = visible;
node_visibility_cache_[index] = visible;
}
}
size_t Scene::AddDirectionalLight(const glm::vec3& direction,
const glm::vec3& color,
float intensity) {
Light light;
light.type = Light::Type::Directional;
light.direction = direction;
light.color = color;
light.intensity = intensity;
lights_.push_back(light);
return lights_.size() - 1;
}
size_t Scene::AddPointLight(const glm::vec3& position,
const glm::vec3& color,
float intensity) {
Light light;
light.type = Light::Type::Point;
light.position = position;
light.color = color;
light.intensity = intensity;
lights_.push_back(light);
return lights_.size() - 1;
}
size_t Scene::AddSpotLight(const glm::vec3& position,
const glm::vec3& direction,
const glm::vec3& color,
float intensity,
float inner_cutoff,
float outer_cutoff) {
Light light;
light.type = Light::Type::Spot;
light.position = position;
light.direction = direction;
light.color = color;
light.intensity = intensity;
light.inner_cutoff = inner_cutoff;
light.outer_cutoff = outer_cutoff;
lights_.push_back(light);
return lights_.size() - 1;
}
void Scene::RemoveLight(size_t index) {
if (index < lights_.size()) {
lights_.erase(lights_.begin() + index);
}
}
void Scene::SetLightEnabled(size_t index, bool enabled) {
if (index < lights_.size()) {
lights_[index].enabled = enabled;
}
}
void Scene::Render(Shader& shader,
const glm::mat4& view_matrix, // Changed from 'view'
const glm::mat4& projection_matrix) { // Changed from 'proj'
shader.Bind();
for (size_t i = 0; i < nodes_.size(); ++i) {
const auto& node = nodes_[i];
if (!node.visible) continue;
// Set model matrix
shader.SetUniform("u_Model", node.transform);
// Compute normal matrix
glm::mat3 normalMat = glm::inverseTranspose(glm::mat3(node.transform));
shader.SetUniform("u_NormalMatrix", normalMat);
// Set view/projection
shader.SetUniform("u_View", view_matrix); // Updated variable name
shader.SetUniform("u_Projection", projection_matrix); // Updated variable name
// Draw mesh
node.mesh->Draw();
}
}
void Scene::RenderShadowMap(Shader& shader, // Remove const here
const glm::mat4& lightSpace) {
shader.Bind();
shader.SetUniform("u_LightSpace", lightSpace);
for (auto& node : nodes_) {
if (!node.visible) continue;
shader.SetUniform("u_Model", node.transform);
node.mesh->Draw();
}
}
void Scene::Clear() {
nodes_.clear();
lights_.clear();
node_visibility_cache_.clear();
}
void Scene::PrintSceneInfo() const {
std::cout << "Scene has "
<< nodes_.size() << " nodes and "
<< lights_.size() << " lights.\n";
}
void Scene::SetAllNodesVisibility(bool visible) {
for (size_t i = 0; i < nodes_.size(); ++i) {
nodes_[i].visible = visible;
node_visibility_cache_[i] = visible;
}
}
void Scene::UpdateAllNodeTransforms() {
for (auto& node : nodes_) {
node.UpdateTransform();
}
}
void Scene::FrustumCull(const glm::mat4& vp) {
for (size_t i = 0; i < nodes_.size(); ++i) {
node_visibility_cache_[i] = IsNodeInFrustum(nodes_[i], vp);
nodes_[i].visible = node_visibility_cache_[i];
}
}
size_t Scene::GetVisibleNodeCount() const {
return std::count(node_visibility_cache_.begin(),
node_visibility_cache_.end(), true);
}
bool Scene::IsNodeInFrustum(const SceneNode& node, const glm::mat4& vp) const {
// TODO: Implement proper frustum culling
// For now, just return true (no culling)
return true;
}

117
src/Scene.h Normal file
View File

@@ -0,0 +1,117 @@
#pragma once
#include "Mesh.h"
#include "Shader.h"
#include <vector>
#include <memory>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
// Forward declarations
class Camera;
// Modern scene management using C++23
class Scene {
public:
// Scene node using modern C++ features
struct SceneNode {
std::shared_ptr<Mesh> mesh;
glm::mat4 transform{1.0f};
glm::vec3 position{0.0f};
glm::vec3 rotation{0.0f}; // Euler angles in degrees
glm::vec3 scale{1.0f};
bool visible = true;
// Update transform matrix from position, rotation, scale
void UpdateTransform();
// C++20 spaceship operator
auto operator<=>(const SceneNode& other) const = default;
};
// Light structure
struct Light {
enum class Type { Directional, Point, Spot };
Type type = Type::Directional;
glm::vec3 position{0.0f};
glm::vec3 direction{0.0f, -1.0f, 0.0f};
glm::vec3 color{1.0f};
float intensity = 1.0f;
// Point/Spot light attenuation
float constant = 1.0f;
float linear = 0.09f;
float quadratic = 0.032f;
// Spot light specific
float inner_cutoff = 12.5f; // degrees
float outer_cutoff = 15.0f; // degrees
bool enabled = true;
};
Scene() = default;
~Scene() = default;
// Delete copy constructor and assignment
Scene(const Scene&) = delete;
Scene& operator=(const Scene&) = delete;
// Move semantics
Scene(Scene&&) noexcept = default;
Scene& operator=(Scene&&) noexcept = default;
// Node management
size_t AddNode(std::shared_ptr<Mesh> mesh, const glm::mat4& transform = glm::mat4(1.0f));
size_t AddNode(std::shared_ptr<Mesh> mesh, const glm::vec3& position,
const glm::vec3& rotation = glm::vec3(0.0f),
const glm::vec3& scale = glm::vec3(1.0f));
void RemoveNode(size_t index);
void SetNodeTransform(size_t index, const glm::mat4& transform);
void SetNodePosition(size_t index, const glm::vec3& position);
void SetNodeRotation(size_t index, const glm::vec3& rotation);
void SetNodeScale(size_t index, const glm::vec3& scale);
void SetNodeVisibility(size_t index, bool visible);
// Light management
size_t AddDirectionalLight(const glm::vec3& direction, const glm::vec3& color = glm::vec3(1.0f), float intensity = 1.0f);
size_t AddPointLight(const glm::vec3& position, const glm::vec3& color = glm::vec3(1.0f), float intensity = 1.0f);
size_t AddSpotLight(const glm::vec3& position, const glm::vec3& direction,
const glm::vec3& color = glm::vec3(1.0f), float intensity = 1.0f,
float inner_cutoff = 12.5f, float outer_cutoff = 15.0f);
void RemoveLight(size_t index);
void SetLightEnabled(size_t index, bool enabled);
// Rendering
void Render(Shader& shader, const glm::mat4& view_matrix, const glm::mat4& projection_matrix);
void RenderShadowMap(Shader& shader, const glm::mat4& lightSpace);
// Utility functions
void Clear();
[[nodiscard]] size_t GetNodeCount() const { return nodes_.size(); }
[[nodiscard]] size_t GetLightCount() const { return lights_.size(); }
[[nodiscard]] const SceneNode& GetNode(size_t index) const { return nodes_[index]; }
[[nodiscard]] const Light& GetLight(size_t index) const { return lights_[index]; }
// Debug functions
void PrintSceneInfo() const;
// Batch operations using ranges (C++20)
void SetAllNodesVisibility(bool visible);
void UpdateAllNodeTransforms();
// Culling and optimization
void FrustumCull(const glm::mat4& view_projection_matrix);
[[nodiscard]] size_t GetVisibleNodeCount() const;
private:
std::vector<SceneNode> nodes_;
std::vector<Light> lights_;
std::vector<bool> node_visibility_cache_; // For frustum culling
void SetupLightUniforms(const Shader& shader) const;
[[nodiscard]] bool IsNodeInFrustum(const SceneNode& node, const glm::mat4& view_projection_matrix) const;
};

200
src/Shader.cpp Normal file
View File

@@ -0,0 +1,200 @@
// src/Shader.cpp
#include "Shader.h"
#include <vector> // Add this include
// Helper to read a file into a string
std::optional<std::string> Shader::ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::in | std::ios::binary);
if (!file) return std::nullopt;
std::ostringstream contents;
contents << file.rdbuf();
return contents.str();
}
// Constructor (vertex + fragment)
Shader::Shader(const std::filesystem::path& vertex_path,
const std::filesystem::path& fragment_path) {
auto vert_src_opt = ReadFile(vertex_path);
auto frag_src_opt = ReadFile(fragment_path);
if (!vert_src_opt || !frag_src_opt) {
std::cerr << "Failed to read shader files\n";
return;
}
auto vert_res = TryCompileShader(GL_VERTEX_SHADER, *vert_src_opt, vertex_path.string());
auto frag_res = TryCompileShader(GL_FRAGMENT_SHADER, *frag_src_opt, fragment_path.string());
if (!vert_res.success || !frag_res.success) {
std::cerr << vert_res.error_message << "\n" << frag_res.error_message << "\n";
return;
}
std::vector<GLuint> shaders = { vert_res.shader_id, frag_res.shader_id };
program_id_ = CreateProgram(shaders);
// Cleanup shader objects
glDeleteShader(vert_res.shader_id);
glDeleteShader(frag_res.shader_id);
}
// Constructor (vertex + geometry + fragment)
Shader::Shader(const std::filesystem::path& vertex_path,
const std::filesystem::path& geometry_path,
const std::filesystem::path& fragment_path) {
auto vert_src_opt = ReadFile(vertex_path);
auto geom_src_opt = ReadFile(geometry_path);
auto frag_src_opt = ReadFile(fragment_path);
if (!vert_src_opt || !geom_src_opt || !frag_src_opt) {
std::cerr << "Failed to read shader files\n";
return;
}
auto vert_res = TryCompileShader(GL_VERTEX_SHADER, *vert_src_opt, vertex_path.string());
auto geom_res = TryCompileShader(GL_GEOMETRY_SHADER, *geom_src_opt, geometry_path.string());
auto frag_res = TryCompileShader(GL_FRAGMENT_SHADER, *frag_src_opt, fragment_path.string());
if (!vert_res.success || !geom_res.success || !frag_res.success) {
std::cerr << vert_res.error_message << "\n"
<< geom_res.error_message << "\n"
<< frag_res.error_message << "\n";
return;
}
std::vector<GLuint> shaders = { vert_res.shader_id, geom_res.shader_id, frag_res.shader_id };
program_id_ = CreateProgram(shaders);
glDeleteShader(vert_res.shader_id);
glDeleteShader(geom_res.shader_id);
glDeleteShader(frag_res.shader_id);
}
Shader::~Shader() {
Cleanup();
}
Shader::Shader(Shader&& other) noexcept {
program_id_ = other.program_id_;
other.program_id_ = 0;
}
Shader& Shader::operator=(Shader&& other) noexcept {
if (this != &other) {
Cleanup();
program_id_ = other.program_id_;
other.program_id_ = 0;
}
return *this;
}
void Shader::Bind() const {
if (program_id_) glUseProgram(program_id_);
}
void Shader::Unbind() const {
glUseProgram(0);
}
void Shader::Cleanup() {
if (program_id_) {
glDeleteProgram(program_id_);
program_id_ = 0;
}
}
GLuint Shader::CompileShader(GLenum type, const std::string& source) {
GLuint id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
return id;
}
Shader::CompileResult Shader::TryCompileShader(GLenum type,
const std::string& source,
const std::string& name) {
CompileResult result;
GLuint shader = CompileShader(type, source);
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint length;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::string log(length, ' ');
glGetShaderInfoLog(shader, length, &length, log.data());
result.success = false;
result.error_message = "Error compiling shader " + name + ": " + log;
glDeleteShader(shader);
} else {
result.success = true;
result.shader_id = shader;
}
return result;
}
GLuint Shader::CreateProgram(const std::vector<GLuint>& shaders) {
GLuint program = glCreateProgram();
for (auto s : shaders) {
glAttachShader(program, s);
}
glLinkProgram(program);
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint length;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::string log(length, ' ');
glGetProgramInfoLog(program, length, &length, log.data());
std::cerr << "Error linking program: " << log << "\n";
glDeleteProgram(program);
return 0;
}
return program;
}
int Shader::GetUniformLocation(const std::string& name) {
if (uniform_location_cache_.count(name)) {
return uniform_location_cache_[name];
}
int location = glGetUniformLocation(program_id_, name.c_str());
uniform_location_cache_[name] = location;
return location;
}
void Shader::PrintActiveUniforms() const {
GLint count;
glGetProgramiv(program_id_, GL_ACTIVE_UNIFORMS, &count);
for (GLint i = 0; i < count; ++i) {
char name[256];
GLsizei length;
GLint size;
GLenum type;
glGetActiveUniform(program_id_, i, sizeof(name), &length, &size, &type, name);
std::cout << "Uniform #" << i << ": " << name << "\n";
}
}
void Shader::PrintActiveAttributes() const {
GLint count;
glGetProgramiv(program_id_, GL_ACTIVE_ATTRIBUTES, &count);
for (GLint i = 0; i < count; ++i) {
char name[256];
GLsizei length;
GLint size;
GLenum type;
glGetActiveAttrib(program_id_, i, sizeof(name), &length, &size, &type, name);
std::cout << "Attribute #" << i << ": " << name << "\n";
}
}
void Shader::SetUniform(const std::string& name, const glm::vec2& value) {
glUniform2fv(GetUniformLocation(name), 1, &value.x);
}
void Shader::SetUniform(const std::string& name, const glm::vec3& value) {
glUniform3fv(GetUniformLocation(name), 1, &value.x);
}
void Shader::SetUniform(const std::string& name, const glm::vec4& value) {
glUniform4fv(GetUniformLocation(name), 1, &value.x);
}
void Shader::SetUniform(const std::string& name, const glm::mat3& value) {
glUniformMatrix3fv(GetUniformLocation(name), 1, GL_FALSE, &value[0][0]);
}
void Shader::SetUniform(const std::string& name, const glm::mat4& value) {
glUniformMatrix4fv(GetUniformLocation(name), 1, GL_FALSE, &value[0][0]);
}

110
src/Shader.h Normal file
View File

@@ -0,0 +1,110 @@
#pragma once
#include <string>
#include <unordered_map>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <iostream>
#include <optional>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "glad.h"
// Modern shader class using C++23 features
class Shader {
public:
// Constructor with path validation using std::filesystem (C++17)
explicit Shader(const std::filesystem::path& vertex_path,
const std::filesystem::path& fragment_path);
// Constructor overload for geometry shaders
Shader(const std::filesystem::path& vertex_path,
const std::filesystem::path& geometry_path,
const std::filesystem::path& fragment_path);
~Shader();
// Delete copy constructor and assignment (RAII)
Shader(const Shader&) = delete;
Shader& operator=(const Shader&) = delete;
// Move semantics
Shader(Shader&& other) noexcept;
Shader& operator=(Shader&& other) noexcept;
void Bind() const;
void Unbind() const;
[[nodiscard]] bool IsValid() const { return program_id_ != 0; }
// Modern uniform setters using function templates and concepts (C++20)
template<typename T>
requires std::same_as<T, int>
void SetUniform(const std::string& name, T value);
template<typename T>
requires std::same_as<T, float>
void SetUniform(const std::string& name, T value);
template<typename T>
requires std::same_as<T, bool>
void SetUniform(const std::string& name, T value);
void SetUniform(const std::string& name, const glm::vec2& value);
void SetUniform(const std::string& name, const glm::vec3& value);
void SetUniform(const std::string& name, const glm::vec4& value);
void SetUniform(const std::string& name, const glm::mat3& value);
void SetUniform(const std::string& name, const glm::mat4& value);
// Array uniforms
void SetUniformArray(const std::string& name, const float* values, int count);
void SetUniformArray(const std::string& name, const glm::vec3* values, int count);
// Utility functions using C++20 features
[[nodiscard]] int GetUniformLocation(const std::string& name);
[[nodiscard]] static std::optional<std::string> ReadFile(const std::filesystem::path& path);
// Debug information
void PrintActiveUniforms() const;
void PrintActiveAttributes() const;
private:
GLuint program_id_ = 0;
mutable std::unordered_map<std::string, GLint> uniform_location_cache_;
// Compilation and linking
[[nodiscard]] static GLuint CompileShader(GLenum type, const std::string& source);
[[nodiscard]] static GLuint CreateProgram(const std::vector<GLuint>& shaders);
// Error handling using std::expected-like pattern (manual implementation for pre-C++23)
struct CompileResult {
bool success;
std::string error_message;
GLuint shader_id = 0;
};
[[nodiscard]] static CompileResult TryCompileShader(GLenum type, const std::string& source, const std::string& shader_name);
static void CheckCompileErrors(GLuint shader, const std::string& type);
static void CheckLinkErrors(GLuint program);
void Cleanup();
};
// Template implementations
template<typename T>
requires std::same_as<T, int>
void Shader::SetUniform(const std::string& name, T value) {
glUniform1i(GetUniformLocation(name), value);
}
template<typename T>
requires std::same_as<T, float>
void Shader::SetUniform(const std::string& name, T value) {
glUniform1f(GetUniformLocation(name), value);
}
template<typename T>
requires std::same_as<T, bool>
void Shader::SetUniform(const std::string& name, T value) {
glUniform1i(GetUniformLocation(name), static_cast<int>(value));
}

110
src/SimpleTextureLoader.cpp Normal file
View File

@@ -0,0 +1,110 @@
#include "SimpleTextureLoader.h"
#include <fstream>
#include <iostream>
#include <vector>
GLuint SimpleTextureLoader::LoadBMP(const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) {
std::cerr << "Failed to open BMP file: " << filepath << std::endl;
return 0;
}
BMPHeader header;
file.read(reinterpret_cast<char*>(&header), sizeof(BMPHeader));
// Check BMP signature
if (header.signature[0] != 'B' || header.signature[1] != 'M') {
std::cerr << "Not a valid BMP file: " << filepath << std::endl;
return 0;
}
// Only support 24-bit BMP for simplicity
if (header.bitsPerPixel != 24) {
std::cerr << "Only 24-bit BMP files supported: " << filepath << std::endl;
return 0;
}
// Calculate image size
int imageSize = header.width * header.height * 3;
std::vector<unsigned char> imageData(imageSize);
// Seek to image data
file.seekg(header.dataOffset);
file.read(reinterpret_cast<char*>(imageData.data()), imageSize);
file.close();
// BMP stores BGR, convert to RGB
for (int i = 0; i < imageSize; i += 3) {
std::swap(imageData[i], imageData[i + 2]);
}
// Create OpenGL texture
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, header.width, header.height,
0, GL_RGB, GL_UNSIGNED_BYTE, imageData.data());
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
return textureID;
}
GLuint SimpleTextureLoader::LoadTGA(const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) {
std::cerr << "Failed to open TGA file: " << filepath << std::endl;
return 0;
}
TGAHeader header;
file.read(reinterpret_cast<char*>(&header), sizeof(TGAHeader));
// Skip image ID if present
if (header.idLength > 0) {
file.seekg(header.idLength, std::ios::cur);
}
// Only support uncompressed RGB/RGBA TGA
if (header.imageType != 2) {
std::cerr << "Only uncompressed TGA files supported: " << filepath << std::endl;
return 0;
}
int bytesPerPixel = header.bitsPerPixel / 8;
int imageSize = header.width * header.height * bytesPerPixel;
std::vector<unsigned char> imageData(imageSize);
file.read(reinterpret_cast<char*>(imageData.data()), imageSize);
file.close();
// TGA stores BGR(A), convert to RGB(A)
for (int i = 0; i < imageSize; i += bytesPerPixel) {
std::swap(imageData[i], imageData[i + 2]);
}
// Create OpenGL texture
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
GLenum format = (bytesPerPixel == 4) ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, header.width, header.height,
0, format, GL_UNSIGNED_BYTE, imageData.data());
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
return textureID;
}

43
src/SimpleTextureLoader.h Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#include <glad.h>
#include <string>
class SimpleTextureLoader {
public:
static GLuint LoadBMP(const std::string& filepath);
static GLuint LoadTGA(const std::string& filepath);
private:
struct BMPHeader {
char signature[2];
uint32_t fileSize;
uint32_t reserved;
uint32_t dataOffset;
uint32_t headerSize;
uint32_t width;
uint32_t height;
uint16_t planes;
uint16_t bitsPerPixel;
uint32_t compression;
uint32_t imageSize;
uint32_t xPixelsPerMeter;
uint32_t yPixelsPerMeter;
uint32_t colorsUsed;
uint32_t colorsImportant;
};
struct TGAHeader {
uint8_t idLength;
uint8_t colorMapType;
uint8_t imageType;
uint16_t colorMapStart;
uint16_t colorMapLength;
uint8_t colorMapDepth;
uint16_t xOffset;
uint16_t yOffset;
uint16_t width;
uint16_t height;
uint8_t bitsPerPixel;
uint8_t imageDescriptor;
};
};

87
src/ThreadPool.h Normal file
View File

@@ -0,0 +1,87 @@
#pragma once
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <atomic>
#include <type_traits> // for std::invoke_result_t
class ThreadPool {
public:
// Construct a thread pool with the given number of threads (defaults to hardware concurrency)
explicit ThreadPool(size_t numThreads = std::thread::hardware_concurrency())
: stopFlag(false)
{
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this] {
return stopFlag.load() || !tasks.empty();
});
if (stopFlag.load() && tasks.empty())
return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
// Destructor shuts down the pool and joins all threads
~ThreadPool() {
shutdown();
}
// Enqueue a task into the thread pool. Returns a future for the result.
template <class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<std::invoke_result_t<F, Args...>>
{
using return_type = std::invoke_result_t<F, Args...>;
// Wrap the function and its arguments into a packaged_task
auto taskPtr = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> result = taskPtr->get_future();
{
std::unique_lock<std::mutex> lock(queueMutex);
if (stopFlag.load())
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([taskPtr]() { (*taskPtr)(); });
}
condition.notify_one();
return result;
}
// Signal all threads to shut down and join them
void shutdown() {
{
std::unique_lock<std::mutex> lock(queueMutex);
stopFlag.store(true);
}
condition.notify_all();
for (std::thread &worker : workers) {
if (worker.joinable())
worker.join();
}
}
private:
std::vector<std::thread> workers; // worker threads
std::queue<std::function<void()>> tasks; // task queue
std::mutex queueMutex; // protects tasks
std::condition_variable condition; // signals availability
std::atomic<bool> stopFlag; // indicates shutdown
};

106
src/Window.cpp Normal file
View File

@@ -0,0 +1,106 @@
// src/Window.cpp
#include "Window.h"
#include "glad.h"
#include <iostream>
// Static storage for user callbacks
static std::function<void(int, int)> g_resizeCallback;
static std::function<void(int, int, int, int)> g_keyCallback;
Window::Window(int width, int height, const std::string& title)
: width_(width), height_(height), title_(title) {
Initialize();
}
Window::~Window() {
Cleanup();
}
// Move constructor
Window::Window(Window&& other) noexcept
: window_(other.window_), width_(other.width_), height_(other.height_), title_(std::move(other.title_)) {
other.window_ = nullptr;
}
// Move assignment
Window& Window::operator=(Window&& other) noexcept {
if (this != &other) {
Cleanup();
window_ = other.window_;
width_ = other.width_;
height_ = other.height_;
title_ = std::move(other.title_);
other.window_ = nullptr;
}
return *this;
}
void Window::Initialize() {
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW\n";
return;
}
// Create window with OpenGL context
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
window_ = glfwCreateWindow(width_, height_, title_.c_str(), nullptr, nullptr);
if (!window_) {
std::cerr << "Failed to create GLFW window\n";
glfwTerminate();
return;
}
glfwMakeContextCurrent(window_);
glfwSetFramebufferSizeCallback(window_, FramebufferSizeCallback);
glfwSetKeyCallback(window_, KeyCallback);
}
// Cleans up GLFW window and terminates GLFW if needed
void Window::Cleanup() {
if (window_) {
glfwDestroyWindow(window_);
window_ = nullptr;
}
glfwTerminate();
}
void Window::PollEvents() const {
glfwPollEvents();
}
bool Window::ShouldClose() const {
return glfwWindowShouldClose(window_);
}
void Window::SwapBuffers() const {
glfwSwapBuffers(window_);
}
void Window::SetVSync(bool enable) const {
glfwSwapInterval(enable ? 1 : 0);
}
void Window::SetResizeCallback(std::function<void(int, int)> callback) {
g_resizeCallback = std::move(callback);
}
void Window::SetKeyCallback(std::function<void(int, int, int, int)> callback) {
g_keyCallback = std::move(callback);
}
// Static GLFW callbacks
void Window::FramebufferSizeCallback(GLFWwindow* window, int width, int height) {
if (g_resizeCallback) {
g_resizeCallback(width, height);
}
}
void Window::KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (g_keyCallback) {
g_keyCallback(key, scancode, action, mods);
}
}

48
src/Window.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <string>
#include <memory>
#include <functional>
class Window {
public:
Window(int width, int height, const std::string& title);
~Window();
// Delete copy constructor and assignment operator (RAII principle)
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
// Move semantics (C++11 and later)
Window(Window&& other) noexcept;
Window& operator=(Window&& other) noexcept;
void PollEvents() const;
bool ShouldClose() const;
void SwapBuffers() const;
void SetVSync(bool enable) const;
GLFWwindow* Handle() const { return window_; }
// Modern C++ getters using [[nodiscard]] (C++17)
[[nodiscard]] int GetWidth() const { return width_; }
[[nodiscard]] int GetHeight() const { return height_; }
[[nodiscard]] float GetAspectRatio() const { return static_cast<float>(width_) / static_cast<float>(height_); }
// Callback setters using std::function (more flexible than raw function pointers)
void SetResizeCallback(std::function<void(int, int)> callback);
void SetKeyCallback(std::function<void(int, int, int, int)> callback);
private:
GLFWwindow* window_;
int width_, height_;
std::string title_;
// Static callbacks for GLFW
static void FramebufferSizeCallback(GLFWwindow* window, int width, int height);
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
void Initialize();
void Cleanup();
};

2532
src/glad.c Normal file

File diff suppressed because it is too large Load Diff

509
src/main.cpp Normal file
View File

@@ -0,0 +1,509 @@
#include "Engine.h"
#include "LevelEditor.cpp"
#include <iostream>
#include <random>
#include <deque>
#include <chrono>
// ---------------------------------------------
// SceneDemo: existing complex scene demo
// ---------------------------------------------
class SceneDemo : public Engine {
private:
float time_ = 0.0f;
std::vector<std::shared_ptr<GameObject>> rotating_cubes_;
std::vector<std::shared_ptr<GameObject>> floating_spheres_;
std::shared_ptr<GameObject> central_sphere_;
std::shared_ptr<GameObject> ground_plane_;
std::shared_ptr<GameObject> grid_;
std::mt19937 rng_;
std::uniform_real_distribution<float> color_dist_{0.3f, 1.0f};
public:
SceneDemo()
: Engine(1920, 1080, "Modern C++ Engine - Complex Scene Demo"),
rng_(std::random_device{}()) {}
bool OnInitialize() override {
std::cout << "Creating complex scene with primitives..." << std::endl;
auto grid_primitive = CreateGrid(25, 1.0f, glm::vec3(0.4f));
grid_ = CreateGameObject(grid_primitive, glm::vec3(0,0,0));
AddGameObject(grid_);
auto plane_primitive = CreatePlane(30.0f, 30.0f, glm::vec3(0.2f,0.2f,0.3f));
ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0,-0.01f,0));
AddGameObject(ground_plane_);
auto central_sphere_primitive = CreateSphere(64, glm::vec3(0.9f,0.3f,0.1f));
central_sphere_ = CreateGameObject(central_sphere_primitive, glm::vec3(0,3,0));
central_sphere_->SetScale(glm::vec3(2.0f));
AddGameObject(central_sphere_);
for (int i = 0; i < 8; ++i) {
glm::vec3 color{color_dist_(rng_), color_dist_(rng_), color_dist_(rng_)};
auto cube = CreateGameObject(CreateCube(color), glm::vec3(0.0f));
rotating_cubes_.push_back(cube);
AddGameObject(cube);
}
for (int i = 0; i < 15; ++i) {
glm::vec3 color{color_dist_(rng_), color_dist_(rng_), color_dist_(rng_)};
float x = (rng_() % 20) - 10.0f;
float z = (rng_() % 20) - 10.0f;
float y = 1.0f + (rng_() % 5);
auto sphere = CreateGameObject(CreateSphere(32, color),
glm::vec3(x,y,z));
sphere->SetScale(glm::vec3(0.5f + (rng_()%10)*0.1f));
floating_spheres_.push_back(sphere);
AddGameObject(sphere);
}
for (int i = 0; i < 5; ++i) {
glm::vec3 color{1.0f - i*0.15f, 0.2f + i*0.15f, 0.5f};
auto cube = CreateGameObject(CreateCube(color),
glm::vec3(-8, i*2.1f+1, 8));
cube->SetScale(glm::vec3(0.8f + i*0.1f));
AddGameObject(cube);
}
for (int i = 0; i < 10; ++i) {
float hue = i/10.0f;
glm::vec3 color{hue, 1.0f-hue, 0.5f};
auto sphere = CreateGameObject(CreateSphere(24, color),
glm::vec3(i*2-9,1,-10));
sphere->SetScale(glm::vec3(0.3f));
AddGameObject(sphere);
}
std::cout << "Scene created with " << game_objects_.size() << " objects!" << std::endl;
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
if (central_sphere_)
central_sphere_->SetRotation(glm::vec3(0, time_*30, 0));
for (size_t i = 0; i < rotating_cubes_.size(); ++i) {
float angle = time_*60.0f + i*(360.0f/rotating_cubes_.size());
float radius=8.0f, height=2.0f+std::sin(time_*2.0f+i)*1.5f;
glm::vec3 pos{std::cos(glm::radians(angle))*radius,
height,
std::sin(glm::radians(angle))*radius};
rotating_cubes_[i]->SetPosition(pos);
rotating_cubes_[i]->SetRotation(glm::vec3(angle,angle*1.5f,0));
}
for (size_t i = 0; i < floating_spheres_.size(); ++i) {
auto sphere = floating_spheres_[i];
glm::vec3 p = sphere->GetPosition();
float bob = std::sin(time_*3.0f + i*0.5f)*0.3f;
sphere->SetPosition(glm::vec3(p.x, p.y+bob, p.z));
sphere->SetRotation(glm::vec3(0, time_*20.0f + i*30.0f, 0));
}
float camAng = time_*15.0f;
SetCameraOrbit(20.0f, camAng, 12.0f);
SetCameraTarget(glm::vec3(0,2,0));
static float status_timer=0.0f;
status_timer += dt;
if (status_timer >= 3.0f) {
std::cout<<"Scene running: "<<int(time_)<<"s | "
<<game_objects_.size()<<" objects | Cam angle: "
<<int(camAng)%360<<"°"<<std::endl;
status_timer=0.0f;
}
}
};
// ---------------------------------------------
// StressTest: incremental spawn until quit
// ---------------------------------------------
class StressTest : public Engine {
public:
StressTest() : Engine(1920, 1080, "Zoom Stresser") {}
bool OnInitialize() override {
last_report_ = std::chrono::high_resolution_clock::now();
// Add a ground plane for visual reference
auto plane = CreateGameObject(CreatePlane(50.0f, 50.0f, glm::vec3(0.2f, 0.3f, 0.5f)),
glm::vec3(0, -1, 0));
AddGameObject(plane);
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
// Spawn new objects every second
if (game_objects_.size() < 10000) {
spawn_timer_ += dt;
if (spawn_timer_ >= 1.0f) {
spawn_timer_ = 0.0f;
AddRow();
}
}
// Animate existing objects
AnimateObjects();
// Orbit camera around the scene
float camera_angle = time_ * 20.0f; // 20 degrees per second
float radius = 30.0f + std::sin(time_ * 0.5f) * 10.0f; // Varying radius
float height = 15.0f + std::cos(time_ * 0.3f) * 5.0f; // Varying height
SetCameraOrbit(radius, camera_angle, height);
SetCameraTarget(glm::vec3(0, 5, 0));
// Report performance every 2 seconds
auto now = std::chrono::high_resolution_clock::now();
float elapsed = std::chrono::duration<float>(now - last_report_).count();
if (elapsed >= 2.0f) {
float fps = 1.0f/dt;
std::cout << "Objects: " << game_objects_.size()
<< " | FPS: " << fps
<< " | Cam angle: " << int(camera_angle) % 360 << "°" << std::endl;
last_report_ = now;
}
}
private:
void AddRow() {
int row = int(stress_objects_.size()) / columns_;
for (int i = 0; i < columns_; ++i) {
// Colorful gradient based on position
float hue = (i + row * columns_) / 100.0f;
glm::vec3 color{
0.5f + 0.5f * std::sin(hue * 6.28f),
0.5f + 0.5f * std::sin(hue * 6.28f + 2.09f),
0.5f + 0.5f * std::sin(hue * 6.28f + 4.19f)
};
glm::vec3 pos{
i * spacing_ - columns_ * spacing_ / 2.0f,
0.5f + row * spacing_,
-20.0f + row * 2.0f // Spread them out in depth too
};
// Alternate between cubes and spheres
std::shared_ptr<GameObject> obj;
if ((i + row) % 2 == 0) {
obj = CreateGameObject(CreateCube(color), pos);
} else {
obj = CreateGameObject(CreateSphere(16, color), pos);
}
obj->SetScale(glm::vec3(0.4f + (i % 5) * 0.1f)); // Varying sizes
stress_objects_.push_back(obj);
AddGameObject(obj);
}
}
void AnimateObjects() {
for (size_t i = 0; i < stress_objects_.size(); ++i) {
auto& obj = stress_objects_[i];
glm::vec3 pos = obj->GetPosition();
// Floating/bobbing motion
float bob_speed = 2.0f + (i % 10) * 0.2f;
float bob_height = 0.3f + (i % 7) * 0.1f;
float bob = std::sin(time_ * bob_speed + i * 0.1f) * bob_height;
obj->SetPosition(glm::vec3(pos.x, pos.y + bob, pos.z));
// Rotation animation
float rot_speed = 30.0f + (i % 13) * 10.0f;
glm::vec3 rotation{
time_ * rot_speed + i * 10.0f,
time_ * (rot_speed * 0.7f) + i * 15.0f,
time_ * (rot_speed * 0.4f) + i * 20.0f
};
obj->SetRotation(rotation);
}
}
float time_{0.0f};
float spawn_timer_{0.0f};
std::chrono::high_resolution_clock::time_point last_report_;
std::vector<std::shared_ptr<GameObject>> stress_objects_;
static constexpr int columns_{500}; // Reduced for better performance
static constexpr float spacing_{1.0f};
};
// ---------------------------------------------
// SpinningMonkey: Loads OBJ monkey and spins it
// ---------------------------------------------
class SpinningMonkey : public Engine {
private:
float time_ = 0.0f;
std::shared_ptr<GameObject> monkey_;
std::shared_ptr<GameObject> ground_plane_;
std::shared_ptr<GameObject> grid_;
// Animation parameters
float spin_speed_ = 45.0f; // degrees per second
float bob_height_ = 2.0f; // vertical bobbing range
float bob_speed_ = 1.5f; // bobbing frequency
float scale_pulse_ = 0.2f; // scale pulsing amount
public:
SpinningMonkey()
: Engine(1920, 1080, "Spinning Monkey Demo - OBJ Model Loader") {}
bool OnInitialize() override {
std::cout << "Loading Suzanne (Blender Monkey) model..." << std::endl;
// Create ground reference
auto grid_primitive = CreateGrid(15, 1.0f, glm::vec3(0.3f, 0.3f, 0.4f));
grid_ = CreateGameObject(grid_primitive, glm::vec3(0, 0, 0));
AddGameObject(grid_);
auto plane_primitive = CreatePlane(20.0f, 20.0f, glm::vec3(0.15f, 0.25f, 0.35f));
ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0, -0.01f, 0));
AddGameObject(ground_plane_);
// Try to load monkey model (with optional texture)
try {
// First try with texture
auto monkey_primitive = CreateOBJModel("monkey.obj", "monkey.jpg");
if (monkey_primitive) {
monkey_ = CreateGameObject(monkey_primitive, glm::vec3(0, 3, 0));
monkey_->SetScale(glm::vec3(2.0f));
AddGameObject(monkey_);
std::cout << "✓ Loaded monkey with texture!" << std::endl;
}
} catch (...) {
std::cout << "! Texture loading failed, trying without texture..." << std::endl;
try {
// Fallback: load without texture
auto monkey_primitive = CreateOBJModel("monkey.obj");
if (monkey_primitive) {
monkey_ = CreateGameObject(monkey_primitive, glm::vec3(0, 3, 0));
monkey_->SetScale(glm::vec3(2.0f));
AddGameObject(monkey_);
std::cout << "✓ Loaded monkey without texture!" << std::endl;
}
} catch (...) {
std::cout << "✗ Failed to load monkey.obj, using fallback cube..." << std::endl;
// Ultimate fallback: colored cube
auto fallback_primitive = CreateCube(glm::vec3(1.0f, 0.5f, 0.2f));
monkey_ = CreateGameObject(fallback_primitive, glm::vec3(0, 3, 0));
monkey_->SetScale(glm::vec3(2.0f));
AddGameObject(monkey_);
}
}
// Set initial camera position
SetCameraPosition(glm::vec3(8, 6, 8));
SetCameraTarget(glm::vec3(0, 3, 0));
std::cout << "Scene initialized with " << game_objects_.size() << " objects!" << std::endl;
std::cout << "Controls:" << std::endl;
std::cout << " - Camera orbits automatically" << std::endl;
std::cout << " - Monkey spins and bobs up and down" << std::endl;
std::cout << " - Scale pulses with time" << std::endl;
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
float spin_y = time_ * spin_speed_;
if (monkey_) {
// Spinning animation
float spin_x = std::sin(time_ * 0.3f) * 15.0f; // Gentle x-axis wobble
float spin_z = std::cos(time_ * 0.7f) * 10.0f; // Gentle z-axis wobble
monkey_->SetRotation(glm::vec3(spin_x, spin_y, spin_z));
// Vertical bobbing
float base_height = 3.0f;
float bob_offset = std::sin(time_ * bob_speed_) * bob_height_;
monkey_->SetPosition(glm::vec3(0, base_height + bob_offset, 0));
// Scale pulsing
float base_scale = 2.0f;
float scale_offset = std::sin(time_ * 2.0f) * scale_pulse_;
float current_scale = base_scale + scale_offset;
monkey_->SetScale(glm::vec3(current_scale));
}
// Orbiting camera
float camera_angle = time_ * 25.0f; // 25 degrees per second
float camera_radius = 10.0f + std::sin(time_ * 0.8f) * 3.0f; // Varying distance
float camera_height = 6.0f + std::cos(time_ * 0.5f) * 2.0f; // Varying height
SetCameraOrbit(camera_radius, camera_angle, camera_height);
SetCameraTarget(glm::vec3(0, 3, 0)); // Always look at monkey
// Status updates every 5 seconds
static float status_timer = 0.0f;
status_timer += dt;
if (status_timer >= 5.0f) {
std::cout << "🐵 Monkey spinning: " << int(time_) << "s | "
<< "Rotation: " << int(spin_y) % 360 << "° | "
<< "Camera: " << int(camera_angle) % 360 << "°" << std::endl;
status_timer = 0.0f;
}
}
};
// ---------------------------------------------
// MonkeyGridDemo: 50 jiggling monkeys on grid
// ---------------------------------------------
class MonkeyGridDemo : public Engine {
private:
float time_ = 0.0f;
std::vector<std::shared_ptr<GameObject>> monkeys_;
std::shared_ptr<GameObject> ground_plane_;
std::shared_ptr<GameObject> grid_;
static constexpr int MONKEY_COUNT = 50;
static constexpr int GRID_COLS = 7;
static constexpr int GRID_ROWS = 8;
static constexpr float SPACING = 4.0f;
std::mt19937 rng_{ std::random_device{}() };
std::uniform_real_distribution<float> jiggle_dist_{ -0.1f, 0.1f };
public:
MonkeyGridDemo()
: Engine(1920, 1080, "Monkey Grid Demo") {}
bool OnInitialize() override {
std::cout << "Spawning " << MONKEY_COUNT << " miniature monkeys on grid..." << std::endl;
// Grid and plane for reference
auto grid_primitive = CreateGrid(15, 1.0f, glm::vec3(0.3f));
grid_ = CreateGameObject(grid_primitive, glm::vec3(0, 0, 0));
AddGameObject(grid_);
auto plane_primitive = CreatePlane(60.0f, 60.0f, glm::vec3(0.2f, 0.25f, 0.3f));
ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0, -0.01f, 0));
AddGameObject(ground_plane_);
// Load OBJ monkey primitive once
std::shared_ptr<Primitive> monkey_primitive;
try {
monkey_primitive = CreateOBJModel("monkey.obj", "monkey.jpg");
if (!monkey_primitive)
monkey_primitive = CreateOBJModel("monkey.obj");
} catch (...) {
monkey_primitive = CreateCube(glm::vec3(1.0f, 0.5f, 0.2f));
}
// Spawn monkeys on GRID_COLS×GRID_ROWS grid
int spawned = 0;
for (int r = 0; r < GRID_ROWS && spawned < MONKEY_COUNT; ++r) {
for (int c = 0; c < GRID_COLS && spawned < MONKEY_COUNT; ++c) {
float x = (c - (GRID_COLS - 1) * 0.5f) * SPACING;
float z = (r - (GRID_ROWS - 1) * 0.5f) * SPACING;
auto monkey = CreateGameObject(monkey_primitive, glm::vec3(x, 1.0f, z));
monkey->SetScale(glm::vec3(0.5f)); // shrink to half size
monkeys_.push_back(monkey);
AddGameObject(monkey);
++spawned;
}
}
// Setup camera
SetCameraOrbit(50.0f, 45.0f, 30.0f);
SetCameraTarget(glm::vec3(0, 1.0f, 0));
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
// Jiggle and slight bob for each monkey
for (auto& monkey : monkeys_) {
glm::vec3 basePos = monkey->GetPosition();
float jigX = jiggle_dist_(rng_);
float jigZ = jiggle_dist_(rng_);
float bobY = std::sin(time_ * 3.0f + basePos.x + basePos.z) * 0.2f;
monkey->SetPosition(glm::vec3(
basePos.x + jigX,
1.0f + bobY,
basePos.z + jigZ
));
// Optional subtle rotation
float angle = std::sin(time_ * 2.0f + basePos.x - basePos.z) * 15.0f;
monkey->SetRotation(glm::vec3(0, angle, 0));
}
// Slow orbiting camera
float camAng = time_ * 10.0f;
SetCameraOrbit(50.0f, camAng, 30.0f);
SetCameraTarget(glm::vec3(0, 1.0f, 0));
}
};
// ---------------------------------------------
// main: choose demo, stress, or monkey by arg
// ---------------------------------------------
int main(int argc, char** argv) {
try {
std::string mode = (argc > 1) ? std::string(argv[1]) : "demo";
if (mode == "stress") {
std::cout << "Running Stress Test..." << std::endl;
StressTest demo;
demo.Run();
return 0;
}
else if (mode == "monkeygrid") {
std::cout << "Starting Monkey Grid Demo..." << std::endl;
MonkeyGridDemo demo;
demo.Run();
return 0;
}
else if (mode == "edit") {
std::cout << "Starting Zoom Level Editor ..." << std::endl;
LevelEditor edit;
edit.Run();
return 0;
}
else if (mode == "monkey") {
std::cout << "🐵 Starting Spinning Monkey Demo..." << std::endl;
std::cout << "====================================" << std::endl;
std::cout << "Place your monkey.obj in models/ folder" << std::endl;
std::cout << "Optionally add monkey.bmp in textures/ folder" << std::endl;
std::cout << "====================================" << std::endl;
SpinningMonkey demo;
demo.Run();
std::cout << "====================================" << std::endl;
std::cout << "Monkey demo completed successfully! 🐵" << std::endl;
return 0;
} else {
std::cout << "Starting Modern C++ Engine Scene Demo..." << std::endl;
std::cout << "=========================================" << std::endl;
SceneDemo demo;
demo.Run();
std::cout << "=========================================" << std::endl;
std::cout << "Demo completed successfully!" << std::endl;
return 0;
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return -1;
}
}