This commit is contained in:
Will
2026-03-29 16:16:15 +01:00
parent 3d573a200e
commit 1d349946d2
30 changed files with 23182 additions and 327 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1142,6 +1142,9 @@ CMakeFiles/ModernEngine.dir/src/Engine.cpp.o: \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__memory/assume_aligned.h \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__memory/ranges_uninitialized_algorithms.h \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__memory/uses_allocator_construction.h \
/opt/homebrew/include/glm/gtc/matrix_access.hpp \
/opt/homebrew/include/glm/gtc/../detail/setup.hpp \
/opt/homebrew/include/glm/gtc/matrix_access.inl \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/iostream \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/ostream \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__ostream/print.h \

View File

@@ -975,6 +975,9 @@ CMakeFiles/ModernEngine.dir/src/LevelEditor.cpp.o: \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__stop_token/stop_callback.h \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/future \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__exception/exception_ptr.h \
/opt/homebrew/include/glm/gtc/matrix_access.hpp \
/opt/homebrew/include/glm/gtc/../detail/setup.hpp \
/opt/homebrew/include/glm/gtc/matrix_access.inl \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/iostream \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/ostream \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__ostream/print.h \

View File

@@ -976,6 +976,9 @@ CMakeFiles/ModernEngine.dir/src/OBJModelPrimitive.cpp.o: \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__stop_token/stop_callback.h \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/future \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__exception/exception_ptr.h \
/opt/homebrew/include/glm/gtc/matrix_access.hpp \
/opt/homebrew/include/glm/gtc/../detail/setup.hpp \
/opt/homebrew/include/glm/gtc/matrix_access.inl \
/Users/will/Documents/zoomengine/src/OBJLoader.h \
/Users/will/Documents/zoomengine/src/SimpleTextureLoader.h \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/iostream \

Binary file not shown.

View File

@@ -975,6 +975,9 @@ CMakeFiles/ModernEngine.dir/src/main.cpp.o: \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__stop_token/stop_callback.h \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/future \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__exception/exception_ptr.h \
/opt/homebrew/include/glm/gtc/matrix_access.hpp \
/opt/homebrew/include/glm/gtc/../detail/setup.hpp \
/opt/homebrew/include/glm/gtc/matrix_access.inl \
/Users/will/Documents/zoomengine/src/LevelEditor.cpp \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/iostream \
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/ostream \

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
empty

View File

@@ -1 +0,0 @@
16

BIN
build/ModernEngine Executable file

Binary file not shown.

View File

@@ -522,6 +522,46 @@ Engine::Engine(int w, int h, const char* title)
InitOverlay();
}
// -------------------------------------------------------------
// CullSphere — Gribb/Hartmann frustum plane extraction
//
// Extracts 6 frustum planes directly from the view-projection
// matrix by combining its rows. [web:72][web:76]
// Returns TRUE if the sphere is fully OUTSIDE any plane
// (i.e. should be culled — skip rendering).
// -------------------------------------------------------------
bool Engine::CullSphere(const glm::mat4& vp,
const glm::vec3& center,
float radius)
{
// GLM stores matrices column-major, so row(vp, n) extracts row n.
// Each frustum plane = combination of row 3 ± row n. [web:75][web:76]
const glm::vec4 row0 = glm::row(vp, 0);
const glm::vec4 row1 = glm::row(vp, 1);
const glm::vec4 row2 = glm::row(vp, 2);
const glm::vec4 row3 = glm::row(vp, 3);
const glm::vec4 planes[6] = {
row3 + row0, // Left
row3 - row0, // Right
row3 + row1, // Bottom
row3 - row1, // Top
row3 + row2, // Near
row3 - row2, // Far
};
const glm::vec4 pt(center, 1.0f);
for (const auto& plane : planes) {
// Signed distance from sphere centre to plane.
// If distance < -radius, sphere is fully on the outside. [web:73]
if (glm::dot(plane, pt) < -radius) {
return true; // Cull — outside this plane
}
}
return false; // Inside or intersecting all 6 planes — keep
}
// Culling subset method
Engine::CullingResult Engine::CullObjectsSubset(
const std::vector<GameObject*>& objects,
@@ -1003,9 +1043,17 @@ void Engine::RenderSceneInstanced() {
// MULTITHREADED CULLING SECTION
glm::mat4 vp = proj * view;
std::vector<GameObject*> raw_objects;
raw_objects.reserve(game_objects_.size());
for (const auto& sp : game_objects_)
raw_objects.push_back(sp.get());
if (raw_objects.empty()) return;
// Determine number of threads and chunk size
size_t numThreads = std::min(threadPool_.getNumThreads(), game_objects_.size());
size_t numThreads = std::min(threadPool_.size(), game_objects_.size());
numThreads = std::max(numThreads, static_cast<size_t>(1));
size_t objectsPerThread = (game_objects_.size() + numThreads - 1) / numThreads;
// Launch culling tasks
@@ -1018,9 +1066,11 @@ void Engine::RenderSceneInstanced() {
if (startIdx >= game_objects_.size()) break;
futures.push_back(threadPool_.enqueue([this, startIdx, endIdx, vp]() {
return CullObjectsSubset(game_objects_, startIdx, endIdx, vp);
}));
futures.push_back(threadPool_.enqueue(
[this, &raw_objects, startIdx, endIdx, vp]() -> CullingResult {
return CullObjectsSubset(raw_objects, startIdx, endIdx, vp);
}
));
}
// Collect results

View File

@@ -6,7 +6,8 @@
#include <vector>
#include <memory>
#include <functional>
#include "ThreadPool.h"
#include "ThreadPool.h"
#include <glm/gtc/matrix_access.hpp>
// Forward declarations
@@ -80,6 +81,10 @@ private:
std::unordered_map<Primitive*, std::vector<glm::mat4>>& outBatches
);
static bool CullSphere(const glm::mat4& vp,
const glm::vec3& center,
float radius);
@@ -177,4 +182,4 @@ private:
glm::vec3 position_;
glm::vec3 rotation_;
glm::vec3 scale_{1.0f};
};
};

View File

@@ -1,53 +1,65 @@
// LevelEditor.cpp
// LevelEditor.cpp — FIXED
#include "Engine.h"
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <cmath>
#include <cstring>
// ---------------------------------------------
// 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<Primitive> monkeyPrim_;
std::shared_ptr<GameObject> selected_;
// FIX: Button stores its own pressed state so DrawButton can be
// called for multiple buttons without a shared static variable
struct Button {
float x, y, w, h;
bool pressed;
bool pressed = false;
};
// Free-cam state
bool firstMouse_ = true;
double lastX_ = 0.0, lastY_ = 0.0;
float yaw_ = -90.0f, pitch_ = 0.0f;
float camSpeed_ = 10.0f, mouseSens_ = 0.1f;
// Free cam state
bool firstMouse_ = true;
double lastX_, lastY_;
float yaw_ = -90.0f, pitch_ = 0.0f;
float camSpeed_ = 10.0f, mouseSens_ = 0.1f;
// Light sliders
float lightR = 1.0f, lightG = 1.0f, lightB = 1.0f, lightI = 1.0f;
bool dragR = false, dragG = false, dragB = false, dragI = false;
// Light control state
float lightR = 1, lightG = 1, lightB = 1, lightI = 1;
bool dragR = false, dragG = false, dragB = false, dragI = false;
Button btnOrtho{ 560.0f, 150.0f, 80.0f, 20.0f, false };
bool useOrtho_ = false;
// FIX: per-key previous-state for edge detection (one-shot placement)
bool prevKey1_ = false, prevKey2_ = false, prevKey3_ = false,
prevKey4_ = false, prevKey5_ = false;
Button btnOrtho { 560, 150, 80, 20, false };
bool useOrtho_ = false;
// Helpers
// ---------------------------------------------------------------
// Input helpers
// ---------------------------------------------------------------
bool KeyDown(int k) const { return glfwGetKey(window_, k) == GLFW_PRESS; }
// Returns true only on the frame the key transitions low→high [web:61]
bool KeyJustPressed(int k, bool& prev) const {
bool cur = KeyDown(k);
bool just = cur && !prev;
prev = cur;
return just;
}
void ProcessMouse(double xpos, double ypos) {
if (firstMouse_) {
lastX_ = xpos; lastY_ = ypos;
firstMouse_ = false;
return; // FIX: skip first frame to avoid a jump
}
float xoff = (xpos - lastX_) * mouseSens_;
float yoff = (lastY_ - ypos) * mouseSens_;
float xoff = static_cast<float>(xpos - lastX_) * mouseSens_;
float yoff = static_cast<float>(lastY_ - ypos) * mouseSens_;
lastX_ = xpos; lastY_ = ypos;
yaw_ += xoff;
yaw_ += xoff;
pitch_ = glm::clamp(pitch_ + yoff, -89.0f, 89.0f);
glm::vec3 front;
front.x = cos(glm::radians(yaw_)) * cos(glm::radians(pitch_));
@@ -56,266 +68,268 @@ private:
camera_target_ = camera_position_ + glm::normalize(front);
}
// Place primitive at camera target
void Place(std::shared_ptr<Primitive> prim) {
glm::vec3 pos = camera_target_;
// FIX: overload — explicit position for use before camera is set up
void Place(std::shared_ptr<Primitive> prim, const glm::vec3& pos) {
auto go = CreateGameObject(prim, pos);
objects_.push_back(go);
AddGameObject(go);
}
void Place(std::shared_ptr<Primitive> prim) { Place(prim, camera_target_); }
// 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
int w, h; glfwGetWindowSize(window_, &w, &h);
float nx = (2.0f * mx) / w - 1.0f;
float ny = 1.0f - (2.0f * my) / h;
glm::vec4 ray_clip{ nx, ny, -1.0f, 1.0f };
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::vec4 ray_eye = glm::inverse(proj) * ray_clip;
ray_eye.z = -1.0f; ray_eye.w = 0.0f;
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; }
float b = glm::dot(oc, dir);
float c = glm::dot(oc, oc)
- obj->GetBoundingRadius() * obj->GetBoundingRadius();
float disc = b * b - c;
if (disc < 0.0f) continue;
float t = -b - std::sqrt(disc);
if (t > 0.0f && 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);
// ---------------------------------------------------------------
// 2D drawing — unified vertex type + helper to avoid repeating
// glBufferData / glVertexAttribPointer for every draw call
// ---------------------------------------------------------------
struct V2D { float x, y, r, g, b; };
// FIX: single upload-and-draw helper used everywhere — the original
// code was missing glVertexAttribPointer calls before several
// glDrawArrays calls (e.g. the button fill triangles)
void UploadAndDraw(const std::vector<V2D>& v, GLenum mode) {
if (v.empty()) return;
glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(v.size() * sizeof(V2D)),
v.data(), GL_DYNAMIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE,
sizeof(V2D), reinterpret_cast<void*>(0));
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
sizeof(V2D), reinterpret_cast<void*>(2 * sizeof(float)));
glDrawArrays(mode, 0, static_cast<GLsizei>(v.size()));
}
// ---------------------------------------------------------------
// Segment-display font — digits AND letters
// FIX: original DrawChar only handled '0''9'; calling it with
// 'R','G','B','I' (and button labels) produced no output.
// Merged into one function covering AZ and 09.
// ---------------------------------------------------------------
void DrawChar(char c, float x, float y, float s,
float r = 1.0f, float g = 1.0f, float b = 1.0f)
{
using P = std::pair<float,float>;
std::vector<P> pts;
if (c >= '0' && c <= '9') {
static const std::vector<std::vector<P>> D = {
{{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}}
};
pts = D[c - '0'];
} else {
switch (c) {
case 'A': pts={{0,0},{0.5f,1},{1,0},{0.25f,0.5f},{0.75f,0.5f}}; break;
case 'B': pts={{0,0},{0,1},{0.7f,1},{0.7f,0.5f},{0,0.5f},{0.7f,0.5f},{0.7f,0},{0,0}}; break;
case 'C': pts={{1,1},{0,1},{0,0},{1,0}}; break;
case 'D': pts={{0,0},{0,1},{0.7f,0.8f},{0.7f,0.2f},{0,0}}; break;
case 'E': pts={{1,1},{0,1},{0,0.5f},{0.7f,0.5f},{0,0.5f},{0,0},{1,0}}; break;
case 'F': pts={{0,0},{0,1},{1,1},{0,1},{0,0.5f},{0.7f,0.5f}}; break;
case 'G': pts={{1,1},{0,1},{0,0},{1,0},{1,0.5f},{0.5f,0.5f}}; break;
case 'H': pts={{0,0},{0,1},{0,0.5f},{1,0.5f},{1,1},{1,0}}; break;
case 'I': pts={{0,1},{1,1},{0.5f,1},{0.5f,0},{0,0},{1,0}}; break;
case 'J': pts={{0,1},{1,1},{1,0},{0,0},{0,0.3f}}; break;
case 'K': pts={{0,0},{0,1},{0,0.5f},{1,1},{0,0.5f},{1,0}}; break;
case 'L': pts={{0,1},{0,0},{1,0}}; break;
case 'M': pts={{0,0},{0,1},{0.5f,0.5f},{1,1},{1,0}}; break;
case 'N': pts={{0,0},{0,1},{1,0},{1,1}}; break;
case 'O': pts={{0,0},{0,1},{1,1},{1,0},{0,0}}; break;
case 'P': pts={{0,0},{0,1},{1,1},{1,0.5f},{0,0.5f}}; break;
case 'Q': pts={{1,0},{0,0},{0,1},{1,1},{1,0},{0.6f,0.2f}}; break;
case 'R': pts={{0,0},{0,1},{1,1},{1,0.5f},{0,0.5f},{1,0}}; break;
case 'S': pts={{1,1},{0,1},{0,0.5f},{1,0.5f},{1,0},{0,0}}; break;
case 'T': pts={{0,1},{1,1},{0.5f,1},{0.5f,0}}; break;
case 'U': pts={{0,1},{0,0},{1,0},{1,1}}; break;
case 'V': pts={{0,1},{0.5f,0},{1,1}}; break;
case 'W': pts={{0,1},{0.25f,0},{0.5f,0.5f},{0.75f,0},{1,1}}; break;
case 'X': pts={{0,0},{1,1},{0.5f,0.5f},{0,1},{1,0}}; break;
case 'Y': pts={{0,1},{0.5f,0.5f},{1,1},{0.5f,0.5f},{0.5f,0}}; break;
case 'Z': pts={{0,1},{1,1},{0,0},{1,0}}; break;
default: return; // skip spaces / unknown
}
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));
}
std::vector<V2D> v;
v.reserve(pts.size());
for (auto& p : pts)
v.push_back({ x + p.first * s, y + p.second * s, r, g, b });
UploadAndDraw(v, GL_LINE_STRIP);
}
// 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());
}
// Draw a string left-to-right with fixed character advance
void DrawString(const char* text, float x, float y, float charSize,
float r = 1.0f, float g = 1.0f, float b = 1.0f)
{
const float advance = charSize * 0.75f + 2.0f;
for (int i = 0; text[i] != '\0'; ++i)
if (text[i] != ' ')
DrawChar(text[i], x + i * advance, y, charSize * 0.7f, r, g, b);
}
float MeasureTextWidth(const char* text, float size) {
return strlen(text) * size * 0.6f;
return strlen(text) * (size * 0.7f + 2.0f);
}
// 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
// FIX: takes Button& (not const) so pressed state is stored per-button,
// replacing the broken shared static bool in the original
bool DrawButton(Button& btn, const char* label,
double mx, double my, bool mb)
{
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;
bool prevPressed = btn.pressed;
btn.pressed = mb && hovered;
bool clicked = btn.pressed && !prevPressed;
// 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;
}
glm::vec3 top = hovered ? glm::vec3(0.3f,0.5f,0.8f)
: glm::vec3(0.2f,0.4f,0.7f);
glm::vec3 bot = hovered ? glm::vec3(0.1f,0.3f,0.6f)
: glm::vec3(0.0f,0.2f,0.5f);
if (btn.pressed) { top *= 0.8f; bot *= 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 }
};
// FIX: UploadAndDraw now sets glVertexAttribPointer — the original
// triangle draw was missing those calls entirely
UploadAndDraw({
{btn.x, btn.y, bot.r, bot.g, bot.b},
{btn.x + btn.w, btn.y, bot.r, bot.g, bot.b},
{btn.x + btn.w, btn.y + btn.h, top.r, top.g, top.b},
{btn.x, btn.y, bot.r, bot.g, bot.b},
{btn.x + btn.w, btn.y + btn.h, top.r, top.g, top.b},
{btn.x, btn.y + btn.h, top.r, top.g, top.b},
}, GL_TRIANGLES);
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW);
glDrawArrays(GL_TRIANGLES, 0, 6);
UploadAndDraw({
{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},
}, GL_LINE_STRIP);
// 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
// Centred label using DrawString (supports full AZ)
float tw = MeasureTextWidth(label, 10.0f);
float tx = btn.x + (btn.w - tw) * 0.5f;
float ty = btn.y + (btn.h - 10.0f) * 0.5f;
DrawString(label, tx, ty, 10.0f);
return clicked;
}
void DrawSlider(float x, float y, float w, float h,
float& val, bool& drag,
double mx, double my, bool mb)
{
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);
// 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);
// Track background
UploadAndDraw({
{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}
}, GL_TRIANGLE_FAN);
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 portion
float fw = w * val;
UploadAndDraw({
{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}
}, GL_TRIANGLE_FAN);
// 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);
// Knob
float kx = x + fw - 2.0f, ky = y - 2.0f, ks = h + 4.0f;
UploadAndDraw({
{kx, ky, 1,1,1}, {kx+ks, ky, 1,1,1},
{kx+ks, ky+ks, 1,1,1}, {kx, ky+ks, 1,1,1}
}, GL_TRIANGLE_FAN);
}
public:
LevelEditor() : Engine(3840,2160,"Level Editor").enable_vsync = true {}
// FIX 1 — constructor: enable_vsync set in body, not on base ctor call
LevelEditor() : Engine(3840, 2160, "Level Editor") {
}
bool OnInitialize() override {
// load monkey
try {
monkeyPrim_ = CreateOBJModel("monkey.obj");
}
catch(...) {
monkeyPrim_ = CreateCube(glm::vec3(1.0f, 0.5f, 0.2f));
}
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)));
// FIX: use explicit-position overload — camera_target_ is not yet
// set at this point, so passing it would place the grid at (0,0,0)
// only by accident if Engine zero-inits it. Be explicit.
Place(CreateGrid(20, 1.0f, glm::vec3(0.4f)), glm::vec3(0.0f));
// 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;
camera_position_ = glm::vec3(0.0f, 20.0f, 20.0f);
camera_target_ = camera_position_
+ glm::normalize(glm::vec3(0.0f) - camera_position_);
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;
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;
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_);
// FIX: KeyJustPressed — one object per key-press, not one per frame [web:61]
if (KeyJustPressed(GLFW_KEY_1, prevKey1_)) Place(CreateCube(glm::vec3(0.8f,0.2f,0.2f)));
if (KeyJustPressed(GLFW_KEY_2, prevKey2_)) Place(CreateSphere(16, glm::vec3(0.2f,0.8f,0.2f)));
if (KeyJustPressed(GLFW_KEY_3, prevKey3_)) Place(CreatePlane(5.0f, 5.0f, glm::vec3(0.2f,0.2f,0.8f)));
if (KeyJustPressed(GLFW_KEY_4, prevKey4_)) Place(CreateGrid(5, 1.0f, glm::vec3(0.5f)));
if (KeyJustPressed(GLFW_KEY_5, prevKey5_)) Place(monkeyPrim_);
// picking
if(glfwGetMouseButton(window_,GLFW_MOUSE_BUTTON_LEFT)==GLFW_PRESS)
selected_ = Pick(mx,my);
if (glfwGetMouseButton(window_, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS)
selected_ = Pick(mx, my);
// move selected with arrows
if(selected_){
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;
if (KeyDown(GLFW_KEY_UP)) p.z -= dt * 5.0f;
if (KeyDown(GLFW_KEY_DOWN)) p.z += dt * 5.0f;
if (KeyDown(GLFW_KEY_LEFT)) p.x -= dt * 5.0f;
if (KeyDown(GLFW_KEY_RIGHT)) p.x += dt * 5.0f;
selected_->SetPosition(p);
}
@@ -323,70 +337,64 @@ public:
}
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]);
? glm::ortho(-30.0f * float(width_)/height_, 30.0f * float(width_)/height_,
-30.0f, 30.0f, 0.1f, 1000.0f)
: glm::perspective(glm::radians(60.0f),
float(width_) / float(height_), 0.1f, 1000.0f);
glUniformMatrix4fv(glGetUniformLocation(shader_program_, "uProjection"),
1, GL_FALSE, &proj[0][0]);
// 1) 3D scene + overlay graphs
Engine::OnRender();
RenderOverlay();
// 2) custom 2D UI: sliders + stats
RenderOverlay(); // draws graphs & text
// setup 2D
// ── 2D overlay ───────────────────────────────────────────────
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
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]);
glm::mat4 ortho2D = glm::ortho(0.0f, float(width_), 0.0f, float(height_));
glUniformMatrix4fv(glGetUniformLocation(overlay_program, "uOrtho"),
1, GL_FALSE, &ortho2D[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;
double mx, my; glfwGetCursorPos(window_, &mx, &my);
my = height_ - my; // flip Y to match OpenGL bottom-left origin
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);
// Sliders
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)) {
// FIX: DrawButton now takes Button& — state is stored per button
// FIX: labels use uppercase AZ which DrawChar now supports
if (DrawButton(btnOrtho, useOrtho_ ? "ORTHO" : "PERSP", mx, my, mb))
useOrtho_ = !useOrtho_;
}
// FIX: was DrawChar('R',...) etc. — DrawChar now handles letters;
// coloured to match channel (red/green/blue/white)
DrawChar('R', 30, 52, 10, 1.0f, 0.3f, 0.3f);
DrawChar('G', 30, 72, 10, 0.3f, 1.0f, 0.3f);
DrawChar('B', 30, 92, 10, 0.3f, 0.5f, 1.0f);
DrawChar('I', 30, 112, 10, 1.0f, 1.0f, 1.0f);
// 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
// Push updated light values to 3D shader
glUseProgram(shader_program_);
glUniform3f(glGetUniformLocation(shader_program_,"uLightColor"),lightR,lightG,lightB);
glUniform1f(glGetUniformLocation(shader_program_,"uAmbientStrength"),0.1f*lightI);
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();
}
void OnShutdown() override { Engine::OnShutdown(); }
};

View File

@@ -8,11 +8,11 @@
#include <future>
#include <functional>
#include <atomic>
#include <type_traits> // for std::invoke_result_t
#include <stdexcept>
#include <type_traits>
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)
{
@@ -36,21 +36,29 @@ public:
}
}
// Destructor shuts down the pool and joins all threads
~ThreadPool() {
shutdown();
}
~ThreadPool() { shutdown(); }
// Non-copyable, non-movable
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
// 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...>>
// FIX: use decay_t so invoke_result matches what is actually stored —
// std::bind produced a wrapper whose result type differed from F's
-> std::future<std::invoke_result_t<std::decay_t<F>, std::decay_t<Args>...>>
{
using return_type = std::invoke_result_t<F, Args...>;
using return_type =
std::invoke_result_t<std::decay_t<F>, std::decay_t<Args>...>;
// Wrap the function and its arguments into a packaged_task
// FIX: replace std::bind with tuple + std::apply — bind wrappers are
// not transparently invocable and break invoke_result deduction
auto taskPtr = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
[func = std::forward<F>(f),
args = std::make_tuple(std::forward<Args>(args)...)]() mutable
-> return_type {
return std::apply(func, args);
}
);
std::future<return_type> result = taskPtr->get_future();
@@ -58,30 +66,31 @@ public:
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
// FIX: added size() — Engine.cpp called getNumThreads() which didn't exist
size_t size() const { return workers.size(); }
void shutdown() {
{
std::unique_lock<std::mutex> lock(queueMutex);
stopFlag.store(true);
}
condition.notify_all();
for (std::thread &worker : workers) {
for (std::thread& worker : workers)
if (worker.joinable())
worker.join();
}
workers.clear(); // safe to call shutdown() more than once
}
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
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
std::atomic<bool> stopFlag;
};