Files
Modularity/src/Rendering.cpp
2025-12-19 07:03:03 -05:00

1382 lines
52 KiB
C++

#include "Rendering.h"
#include "Camera.h"
#include "ModelLoader.h"
#include <unordered_map>
#include <unordered_set>
#define TINYOBJLOADER_IMPLEMENTATION
#include "../include/ThirdParty/tiny_obj_loader.h"
// Global OBJ loader instance
OBJLoader g_objLoader;
// Cube vertex data
float vertices[] = {
// Back face (z = -0.5f)
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
// Front face (z = 0.5f)
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
// Left face (x = -0.5f)
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// Right face (x = 0.5f)
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// Bottom face (y = -0.5f)
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
// Top face (y = 0.5f)
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
};
float mirrorPlaneVertices[] = {
// positions // normals // texcoords
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f
};
std::vector<float> generateSphere(int segments, int rings) {
std::vector<float> vertices;
for (int ring = 0; ring <= rings; ring++) {
float theta = ring * PI / rings;
float sinTheta = sin(theta);
float cosTheta = cos(theta);
for (int seg = 0; seg <= segments; seg++) {
float phi = seg * 2.0f * PI / segments;
float sinPhi = sin(phi);
float cosPhi = cos(phi);
float x = cosPhi * sinTheta;
float y = cosTheta;
float z = sinPhi * sinTheta;
// Position
vertices.push_back(x * 0.5f);
vertices.push_back(y * 0.5f);
vertices.push_back(z * 0.5f);
// Normal (same as position for unit sphere)
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
// Texcoord
vertices.push_back((float)seg / segments);
vertices.push_back((float)ring / rings);
}
}
std::vector<float> triangulated;
int stride = segments + 1;
for (int ring = 0; ring < rings; ring++) {
for (int seg = 0; seg < segments; seg++) {
int current = ring * stride + seg;
int next = current + stride;
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[current * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(next + 1) * 8 + i]);
}
}
return triangulated;
}
std::vector<float> generateCapsule(int segments, int rings) {
std::vector<float> vertices;
float cylinderHeight = 0.5f;
float radius = 0.25f;
// Top hemisphere
for (int ring = 0; ring <= rings / 2; ring++) {
float theta = ring * PI / rings;
float sinTheta = sin(theta);
float cosTheta = cos(theta);
for (int seg = 0; seg <= segments; seg++) {
float phi = seg * 2.0f * PI / segments;
float sinPhi = sin(phi);
float cosPhi = cos(phi);
float x = cosPhi * sinTheta * radius;
float y = cosTheta * radius + cylinderHeight;
float z = sinPhi * sinTheta * radius;
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
glm::vec3 normal = glm::normalize(glm::vec3(x, y - cylinderHeight, z));
vertices.push_back(normal.x);
vertices.push_back(normal.y);
vertices.push_back(normal.z);
vertices.push_back((float)seg / segments);
vertices.push_back((float)ring / (rings / 2));
}
}
// Cylinder body
for (int i = 0; i <= 1; i++) {
float y = i == 0 ? cylinderHeight : -cylinderHeight;
for (int seg = 0; seg <= segments; seg++) {
float phi = seg * 2.0f * PI / segments;
float x = cos(phi) * radius;
float z = sin(phi) * radius;
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
glm::vec3 normal = glm::normalize(glm::vec3(x, 0.0f, z));
vertices.push_back(normal.x);
vertices.push_back(normal.y);
vertices.push_back(normal.z);
vertices.push_back((float)seg / segments);
vertices.push_back(0.5f);
}
}
// Bottom hemisphere
for (int ring = rings / 2; ring <= rings; ring++) {
float theta = ring * PI / rings;
float sinTheta = sin(theta);
float cosTheta = cos(theta);
for (int seg = 0; seg <= segments; seg++) {
float phi = seg * 2.0f * PI / segments;
float sinPhi = sin(phi);
float cosPhi = cos(phi);
float x = cosPhi * sinTheta * radius;
float y = cosTheta * radius - cylinderHeight;
float z = sinPhi * sinTheta * radius;
vertices.push_back(x);
vertices.push_back(y);
vertices.push_back(z);
glm::vec3 normal = glm::normalize(glm::vec3(x, y + cylinderHeight, z));
vertices.push_back(normal.x);
vertices.push_back(normal.y);
vertices.push_back(normal.z);
vertices.push_back((float)seg / segments);
vertices.push_back((float)ring / rings);
}
}
std::vector<float> triangulated;
int stride = segments + 1;
int totalRings = rings + 3;
for (int ring = 0; ring < totalRings - 1; ring++) {
for (int seg = 0; seg < segments; seg++) {
int current = ring * stride + seg;
int next = current + stride;
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[current * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(next + 1) * 8 + i]);
}
}
return triangulated;
}
// Mesh implementation
Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) {
vertexCount = dataSizeBytes / (8 * sizeof(float));
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, dataSizeBytes, vertexData, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
Mesh::~Mesh() {
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
}
void Mesh::draw() const {
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
glBindVertexArray(0);
}
// OBJLoader implementation
int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) {
// Check if already loaded
for (size_t i = 0; i < loadedMeshes.size(); i++) {
if (loadedMeshes[i].path == filepath) {
return static_cast<int>(i);
}
}
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
std::string warn, err;
std::string baseDir = fs::path(filepath).parent_path().string();
if (!baseDir.empty()) baseDir += "/";
bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err,
filepath.c_str(), baseDir.c_str());
if (!warn.empty()) {
errorMsg += "Warning: " + warn + "\n";
}
if (!err.empty()) {
errorMsg += "Error: " + err + "\n";
}
if (!ret || shapes.empty()) {
errorMsg += "Failed to load OBJ file: " + filepath;
return -1;
}
std::vector<float> vertices;
bool hasNormalsInFile = !attrib.normals.empty();
int faceCount = 0;
for (const auto& shape : shapes) {
faceCount += static_cast<int>(shape.mesh.num_face_vertices.size());
}
glm::vec3 boundsMin(FLT_MAX);
glm::vec3 boundsMax(-FLT_MAX);
std::vector<glm::vec3> triPositions;
for (const auto& shape : shapes) {
size_t indexOffset = 0;
for (size_t f = 0; f < shape.mesh.num_face_vertices.size(); f++) {
int fv = shape.mesh.num_face_vertices[f];
struct TempVertex {
glm::vec3 pos;
glm::vec2 uv;
glm::vec3 normal;
bool hasNormal = false;
};
std::vector<TempVertex> faceVerts;
for (int v = 0; v < fv; v++) {
tinyobj::index_t idx = shape.mesh.indices[indexOffset + v];
TempVertex tv;
tv.pos.x = attrib.vertices[3 * size_t(idx.vertex_index) + 0];
tv.pos.y = attrib.vertices[3 * size_t(idx.vertex_index) + 1];
tv.pos.z = attrib.vertices[3 * size_t(idx.vertex_index) + 2];
boundsMin.x = std::min(boundsMin.x, tv.pos.x);
boundsMin.y = std::min(boundsMin.y, tv.pos.y);
boundsMin.z = std::min(boundsMin.z, tv.pos.z);
boundsMax.x = std::max(boundsMax.x, tv.pos.x);
boundsMax.y = std::max(boundsMax.y, tv.pos.y);
boundsMax.z = std::max(boundsMax.z, tv.pos.z);
if (idx.texcoord_index >= 0 && !attrib.texcoords.empty()) {
tv.uv.x = attrib.texcoords[2 * size_t(idx.texcoord_index) + 0];
tv.uv.y = attrib.texcoords[2 * size_t(idx.texcoord_index) + 1];
} else {
tv.uv = glm::vec2(0.0f);
}
if (idx.normal_index >= 0 && hasNormalsInFile) {
tv.normal.x = attrib.normals[3 * size_t(idx.normal_index) + 0];
tv.normal.y = attrib.normals[3 * size_t(idx.normal_index) + 1];
tv.normal.z = attrib.normals[3 * size_t(idx.normal_index) + 2];
tv.hasNormal = true;
}
faceVerts.push_back(tv);
}
if (!hasNormalsInFile && fv >= 3) {
glm::vec3 v0 = faceVerts[0].pos;
glm::vec3 v1 = faceVerts[1].pos;
glm::vec3 v2 = faceVerts[2].pos;
glm::vec3 faceNormal = glm::normalize(glm::cross(v1 - v0, v2 - v0));
for (auto& tv : faceVerts) {
tv.normal = faceNormal;
tv.hasNormal = true;
}
}
for (int v = 1; v < fv - 1; v++) {
const TempVertex* tri[3] = { &faceVerts[0], &faceVerts[v], &faceVerts[v+1] };
for (int i = 0; i < 3; i++) {
triPositions.push_back(tri[i]->pos);
vertices.push_back(tri[i]->pos.x);
vertices.push_back(tri[i]->pos.y);
vertices.push_back(tri[i]->pos.z);
vertices.push_back(tri[i]->normal.x);
vertices.push_back(tri[i]->normal.y);
vertices.push_back(tri[i]->normal.z);
vertices.push_back(tri[i]->uv.x);
vertices.push_back(tri[i]->uv.y);
}
}
indexOffset += fv;
}
}
if (vertices.empty()) {
errorMsg += "No vertices found in OBJ file";
return -1;
}
LoadedMesh loaded;
loaded.path = filepath;
loaded.name = fs::path(filepath).stem().string();
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
loaded.faceCount = faceCount;
loaded.hasNormals = hasNormalsInFile;
loaded.hasTexCoords = !attrib.texcoords.empty();
loaded.boundsMin = boundsMin;
loaded.boundsMax = boundsMax;
loaded.triangleVertices = std::move(triPositions);
loadedMeshes.push_back(std::move(loaded));
return static_cast<int>(loadedMeshes.size() - 1);
}
Mesh* OBJLoader::getMesh(int index) {
if (index < 0 || index >= static_cast<int>(loadedMeshes.size())) {
return nullptr;
}
return loadedMeshes[index].mesh.get();
}
const OBJLoader::LoadedMesh* OBJLoader::getMeshInfo(int index) const {
if (index < 0 || index >= static_cast<int>(loadedMeshes.size())) {
return nullptr;
}
return &loadedMeshes[index];
}
// Renderer implementation
Renderer::~Renderer() {
shaderCache.clear();
shader = nullptr;
defaultShader = nullptr;
delete texture1;
delete texture2;
delete cubeMesh;
delete sphereMesh;
delete capsuleMesh;
delete planeMesh;
delete skybox;
delete postShader;
delete brightShader;
delete blurShader;
if (previewTarget.fbo) glDeleteFramebuffers(1, &previewTarget.fbo);
if (previewTarget.texture) glDeleteTextures(1, &previewTarget.texture);
if (previewTarget.rbo) glDeleteRenderbuffers(1, &previewTarget.rbo);
if (postTarget.fbo) glDeleteFramebuffers(1, &postTarget.fbo);
if (postTarget.texture) glDeleteTextures(1, &postTarget.texture);
if (postTarget.rbo) glDeleteRenderbuffers(1, &postTarget.rbo);
if (historyTarget.fbo) glDeleteFramebuffers(1, &historyTarget.fbo);
if (historyTarget.texture) glDeleteTextures(1, &historyTarget.texture);
if (historyTarget.rbo) glDeleteRenderbuffers(1, &historyTarget.rbo);
if (bloomTargetA.fbo) glDeleteFramebuffers(1, &bloomTargetA.fbo);
if (bloomTargetA.texture) glDeleteTextures(1, &bloomTargetA.texture);
if (bloomTargetA.rbo) glDeleteRenderbuffers(1, &bloomTargetA.rbo);
if (bloomTargetB.fbo) glDeleteFramebuffers(1, &bloomTargetB.fbo);
if (bloomTargetB.texture) glDeleteTextures(1, &bloomTargetB.texture);
if (bloomTargetB.rbo) glDeleteRenderbuffers(1, &bloomTargetB.rbo);
for (auto& entry : mirrorTargets) {
releaseRenderTarget(entry.second);
}
mirrorTargets.clear();
mirrorSmooth.clear();
if (framebuffer) glDeleteFramebuffers(1, &framebuffer);
if (viewportTexture) glDeleteTextures(1, &viewportTexture);
if (rbo) glDeleteRenderbuffers(1, &rbo);
if (quadVBO) glDeleteBuffers(1, &quadVBO);
if (quadVAO) glDeleteVertexArrays(1, &quadVAO);
}
Texture* Renderer::getTexture(const std::string& path) {
if (path.empty()) return nullptr;
auto it = textureCache.find(path);
if (it != textureCache.end()) return it->second.get();
auto tex = std::make_unique<Texture>(path);
if (!tex->GetID()) {
return nullptr;
}
Texture* raw = tex.get();
textureCache[path] = std::move(tex);
return raw;
}
void Renderer::initialize() {
shader = new Shader(defaultVertPath.c_str(), defaultFragPath.c_str());
defaultShader = shader;
if (shader->ID == 0) {
std::cerr << "Shader compilation failed!\n";
delete shader;
shader = nullptr;
throw std::runtime_error("Shader error");
}
postShader = new Shader(postVertPath.c_str(), postFragPath.c_str());
if (!postShader || postShader->ID == 0) {
std::cerr << "PostFX shader compilation failed!\n";
delete postShader;
postShader = nullptr;
} else {
postShader->use();
postShader->setInt("sceneTex", 0);
postShader->setInt("bloomTex", 1);
postShader->setInt("historyTex", 2);
}
brightShader = new Shader(postVertPath.c_str(), postBrightFragPath.c_str());
if (!brightShader || brightShader->ID == 0) {
std::cerr << "Bright-pass shader compilation failed!\n";
delete brightShader;
brightShader = nullptr;
} else {
brightShader->use();
brightShader->setInt("sceneTex", 0);
}
blurShader = new Shader(postVertPath.c_str(), postBlurFragPath.c_str());
if (!blurShader || blurShader->ID == 0) {
std::cerr << "Blur shader compilation failed!\n";
delete blurShader;
blurShader = nullptr;
} else {
blurShader->use();
blurShader->setInt("image", 0);
}
ShaderEntry entry;
entry.shader.reset(defaultShader);
entry.vertPath = defaultVertPath;
entry.fragPath = defaultFragPath;
if (fs::exists(defaultVertPath)) entry.vertTime = fs::last_write_time(defaultVertPath);
if (fs::exists(defaultFragPath)) entry.fragTime = fs::last_write_time(defaultFragPath);
shaderCache[defaultVertPath + "|" + defaultFragPath] = std::move(entry);
texture1 = new Texture("Resources/Textures/container.jpg");
texture2 = new Texture("Resources/Textures/awesomeface.png");
cubeMesh = new Mesh(vertices, sizeof(vertices));
auto sphereVerts = generateSphere();
sphereMesh = new Mesh(sphereVerts.data(), sphereVerts.size() * sizeof(float));
auto capsuleVerts = generateCapsule();
capsuleMesh = new Mesh(capsuleVerts.data(), capsuleVerts.size() * sizeof(float));
planeMesh = new Mesh(mirrorPlaneVertices, sizeof(mirrorPlaneVertices));
skybox = new Skybox();
setupFBO();
ensureRenderTarget(postTarget, currentWidth, currentHeight);
ensureRenderTarget(historyTarget, currentWidth, currentHeight);
ensureRenderTarget(bloomTargetA, currentWidth, currentHeight);
ensureRenderTarget(bloomTargetB, currentWidth, currentHeight);
ensureQuad();
clearHistory();
glEnable(GL_DEPTH_TEST);
}
Shader* Renderer::getShader(const std::string& vert, const std::string& frag) {
std::string vPath = vert.empty() ? defaultVertPath : vert;
std::string fPath = frag.empty() ? defaultFragPath : frag;
std::string key = vPath + "|" + fPath;
auto reloadEntry = [&](ShaderEntry& e) -> Shader* {
std::unique_ptr<Shader> newShader = std::make_unique<Shader>(vPath.c_str(), fPath.c_str());
if (!newShader || newShader->ID == 0) {
std::cerr << "Shader reload failed for " << key << ", falling back to default\n";
return defaultShader;
}
e.shader = std::move(newShader);
e.vertPath = vPath;
e.fragPath = fPath;
if (fs::exists(vPath)) e.vertTime = fs::last_write_time(vPath);
if (fs::exists(fPath)) e.fragTime = fs::last_write_time(fPath);
return e.shader.get();
};
auto it = shaderCache.find(key);
if (it != shaderCache.end()) {
ShaderEntry& entry = it->second;
if (autoReloadShaders) {
bool changed = false;
if (fs::exists(vPath)) {
auto t = fs::last_write_time(vPath);
if (t != entry.vertTime) { changed = true; entry.vertTime = t; }
}
if (fs::exists(fPath)) {
auto t = fs::last_write_time(fPath);
if (t != entry.fragTime) { changed = true; entry.fragTime = t; }
}
if (changed) {
return reloadEntry(entry);
}
}
return entry.shader ? entry.shader.get() : defaultShader;
}
ShaderEntry entry;
entry.vertPath = vPath;
entry.fragPath = fPath;
if (fs::exists(vPath)) entry.vertTime = fs::last_write_time(vPath);
if (fs::exists(fPath)) entry.fragTime = fs::last_write_time(fPath);
entry.shader = std::make_unique<Shader>(vPath.c_str(), fPath.c_str());
if (!entry.shader || entry.shader->ID == 0) {
std::cerr << "Shader compile failed for " << key << ", using default\n";
shaderCache[key] = std::move(entry);
return defaultShader;
}
Shader* ptr = entry.shader.get();
shaderCache[key] = std::move(entry);
return ptr;
}
bool Renderer::forceReloadShader(const std::string& vert, const std::string& frag) {
std::string vPath = vert.empty() ? defaultVertPath : vert;
std::string fPath = frag.empty() ? defaultFragPath : frag;
std::string key = vPath + "|" + fPath;
auto it = shaderCache.find(key);
if (it != shaderCache.end()) {
shaderCache.erase(it);
}
ShaderEntry entry;
entry.vertPath = vPath;
entry.fragPath = fPath;
if (fs::exists(vPath)) entry.vertTime = fs::last_write_time(vPath);
if (fs::exists(fPath)) entry.fragTime = fs::last_write_time(fPath);
entry.shader = std::make_unique<Shader>(vPath.c_str(), fPath.c_str());
if (!entry.shader || entry.shader->ID == 0) {
std::cerr << "Shader force reload failed for " << key << "\n";
return false;
}
if (vPath == defaultVertPath && fPath == defaultFragPath) {
defaultShader = entry.shader.get();
}
shaderCache[key] = std::move(entry);
return true;
}
void Renderer::setupFBO() {
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glGenTextures(1, &viewportTexture);
glBindTexture(GL_TEXTURE_2D, viewportTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, currentWidth, currentHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, viewportTexture, 0);
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, currentWidth, currentHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Framebuffer setup failed!\n";
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
displayTexture = viewportTexture;
}
void Renderer::ensureRenderTarget(RenderTarget& target, int w, int h) {
if (w <= 0 || h <= 0) return;
if (target.fbo == 0) {
glGenFramebuffers(1, &target.fbo);
glGenTextures(1, &target.texture);
glGenRenderbuffers(1, &target.rbo);
}
if (target.width == w && target.height == h) return;
target.width = w;
target.height = h;
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
glBindTexture(GL_TEXTURE_2D, target.texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, target.width, target.height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
glBindRenderbuffer(GL_RENDERBUFFER, target.rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, target.width, target.height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, target.rbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Preview framebuffer setup failed!\n";
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Renderer::releaseRenderTarget(RenderTarget& target) {
if (target.texture) {
glDeleteTextures(1, &target.texture);
}
if (target.rbo) {
glDeleteRenderbuffers(1, &target.rbo);
}
if (target.fbo) {
glDeleteFramebuffers(1, &target.fbo);
}
target = {};
}
void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane) {
if (sceneObjects.empty() || width <= 0 || height <= 0) return;
std::unordered_set<int> active;
GLint prevFBO = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFBO);
auto planeNormal = [](const SceneObject& obj) {
glm::quat q = glm::quat(glm::radians(obj.rotation));
glm::vec3 n = q * glm::vec3(0.0f, 0.0f, 1.0f);
if (!std::isfinite(n.x) || glm::length(n) < 1e-3f) {
n = glm::vec3(0.0f, 0.0f, 1.0f);
}
return glm::normalize(n);
};
auto planeUp = [](const SceneObject& obj) {
glm::quat q = glm::quat(glm::radians(obj.rotation));
glm::vec3 u = q * glm::vec3(0.0f, 1.0f, 0.0f);
if (!std::isfinite(u.x) || glm::length(u) < 1e-3f) {
u = glm::vec3(0.0f, 1.0f, 0.0f);
}
return glm::normalize(u);
};
for (const auto& obj : sceneObjects) {
if (!obj.enabled || obj.type != ObjectType::Mirror) continue;
active.insert(obj.id);
RenderTarget& target = mirrorTargets[obj.id];
ensureRenderTarget(target, width, height);
if (target.fbo == 0 || target.texture == 0) continue;
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
glViewport(0, 0, target.width, target.height);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glm::vec3 n = planeNormal(obj);
glm::vec3 planePoint = obj.position;
glm::vec3 upVec = planeUp(obj);
glm::vec3 tangent = glm::normalize(glm::cross(upVec, n));
if (!std::isfinite(tangent.x) || glm::length(tangent) < 1e-3f) {
tangent = glm::vec3(1.0f, 0.0f, 0.0f);
}
glm::vec3 bitangent = glm::cross(n, tangent);
Camera mirrorCam = camera;
glm::vec3 relToPlane = camera.position - planePoint;
float alongT = glm::dot(relToPlane, tangent);
float alongB = glm::dot(relToPlane, bitangent);
MirrorSmoothing& sm = mirrorSmooth[obj.id];
if (!sm.initialized) {
sm.planar = glm::vec2(alongT, alongB);
sm.initialized = true;
} else {
float lerp = 0.2f; // slow the planar tracking slightly
sm.planar = glm::mix(sm.planar, glm::vec2(alongT, alongB), lerp);
}
float fixedDepth = 0.05f; // keep a small offset off the plane; ignore viewer local Z movement
mirrorCam.position = planePoint + tangent * sm.planar.x + bitangent * sm.planar.y + n * fixedDepth;
mirrorCam.front = n; // Look straight out from the mirror face
mirrorCam.up = upVec;
if (!std::isfinite(mirrorCam.front.x) || glm::length(mirrorCam.front) < 1e-3f) {
mirrorCam.front = glm::vec3(0.0f, 0.0f, -1.0f);
}
if (!std::isfinite(mirrorCam.up.x) || glm::length(mirrorCam.up) < 1e-3f) {
mirrorCam.up = glm::vec3(0.0f, 1.0f, 0.0f);
}
renderSceneInternal(mirrorCam, sceneObjects, target.width, target.height, false, fovDeg, nearPlane, farPlane, false);
}
glBindFramebuffer(GL_FRAMEBUFFER, prevFBO);
for (auto it = mirrorTargets.begin(); it != mirrorTargets.end(); ) {
if (active.find(it->first) == active.end()) {
releaseRenderTarget(it->second);
mirrorSmooth.erase(it->first);
it = mirrorTargets.erase(it);
} else {
++it;
}
}
}
void Renderer::ensureQuad() {
if (quadVAO != 0) return;
float quadVertices[] = {
// positions // texcoords
-1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f
};
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
}
void Renderer::drawFullscreenQuad() {
if (quadVAO == 0) ensureQuad();
glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
}
void Renderer::clearHistory() {
historyValid = false;
if (historyTarget.fbo != 0 && historyTarget.width > 0 && historyTarget.height > 0) {
glBindFramebuffer(GL_FRAMEBUFFER, historyTarget.fbo);
glViewport(0, 0, historyTarget.width, historyTarget.height);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
void Renderer::clearTarget(RenderTarget& target) {
if (target.fbo == 0 || target.width <= 0 || target.height <= 0) return;
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
glViewport(0, 0, target.width, target.height);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void Renderer::resize(int w, int h) {
if (w <= 0 || h <= 0 || (w == currentWidth && h == currentHeight)) return;
currentWidth = w;
currentHeight = h;
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glBindTexture(GL_TEXTURE_2D, viewportTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, currentWidth, currentHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, currentWidth, currentHeight);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Framebuffer incomplete after resize!\n";
}
ensureRenderTarget(postTarget, currentWidth, currentHeight);
ensureRenderTarget(historyTarget, currentWidth, currentHeight);
ensureRenderTarget(bloomTargetA, currentWidth, currentHeight);
ensureRenderTarget(bloomTargetB, currentWidth, currentHeight);
clearHistory();
displayTexture = viewportTexture;
}
void Renderer::beginRender(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& cameraPos) {
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, currentWidth, currentHeight);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
displayTexture = viewportTexture;
shader->use();
shader->setMat4("view", view);
shader->setMat4("projection", proj);
shader->setVec3("viewPos", cameraPos);
texture1->Bind(GL_TEXTURE0);
texture2->Bind(GL_TEXTURE1);
shader->setInt("texture1", 0);
shader->setInt("overlayTex", 1);
shader->setInt("normalMap", 2);
}
void Renderer::renderSkybox(const glm::mat4& view, const glm::mat4& proj) {
if (skybox) {
glDepthFunc(GL_LEQUAL);
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
glDepthFunc(GL_LESS);
shader->use();
shader->setMat4("view", view);
shader->setMat4("projection", proj);
}
}
void Renderer::renderObject(const SceneObject& obj) {
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
model = glm::scale(model, obj.scale);
shader->setMat4("model", model);
shader->setVec3("materialColor", obj.material.color);
shader->setFloat("ambientStrength", obj.material.ambientStrength);
shader->setFloat("specularStrength", obj.material.specularStrength);
shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix);
shader->setBool("unlit", obj.type == ObjectType::Mirror);
Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) {
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
}
if (baseTex) baseTex->Bind(GL_TEXTURE0);
bool overlayUsed = false;
if (obj.type == ObjectType::Mirror) {
auto it = mirrorTargets.find(obj.id);
if (it != mirrorTargets.end() && it->second.texture != 0) {
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, it->second.texture);
overlayUsed = true;
}
}
if (!overlayUsed && obj.useOverlay && !obj.overlayTexturePath.empty()) {
if (auto* t = getTexture(obj.overlayTexturePath)) {
t->Bind(GL_TEXTURE1);
overlayUsed = true;
}
}
if (!overlayUsed && texture2) {
texture2->Bind(GL_TEXTURE1);
}
shader->setBool("hasOverlay", overlayUsed);
bool normalUsed = false;
if (!obj.normalMapPath.empty()) {
if (auto* t = getTexture(obj.normalMapPath)) {
t->Bind(GL_TEXTURE2);
normalUsed = true;
}
}
shader->setBool("hasNormalMap", normalUsed);
switch (obj.type) {
case ObjectType::Cube:
cubeMesh->draw();
break;
case ObjectType::Sphere:
sphereMesh->draw();
break;
case ObjectType::Capsule:
capsuleMesh->draw();
break;
case ObjectType::Mirror:
if (planeMesh) planeMesh->draw();
break;
case ObjectType::OBJMesh:
if (obj.meshId >= 0) {
Mesh* objMesh = g_objLoader.getMesh(obj.meshId);
if (objMesh) {
objMesh->draw();
}
}
break;
case ObjectType::Model:
if (obj.meshId >= 0) {
Mesh* modelMesh = getModelLoader().getMesh(obj.meshId);
if (modelMesh) {
modelMesh->draw();
}
}
break;
case ObjectType::PointLight:
case ObjectType::SpotLight:
case ObjectType::AreaLight:
// Lights are not rendered as geometry
break;
case ObjectType::DirectionalLight:
// Not rendered as geometry
break;
case ObjectType::Camera:
// Cameras are editor helpers only
break;
case ObjectType::PostFXNode:
break;
}
}
void Renderer::renderSceneInternal(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane, bool drawMirrorObjects) {
if (!defaultShader || width <= 0 || height <= 0) return;
struct LightUniform {
int type = 0; // 0 dir,1 point,2 spot,3 area
glm::vec3 dir = glm::vec3(0.0f, -1.0f, 0.0f);
glm::vec3 pos = glm::vec3(0.0f);
glm::vec3 color = glm::vec3(1.0f);
float intensity = 1.0f;
float range = 10.0f;
float inner = glm::cos(glm::radians(15.0f));
float outer = glm::cos(glm::radians(25.0f));
glm::vec2 areaSize = glm::vec2(1.0f); // width/height for area lights
float areaFade = 0.0f; // 0 sharp, 1 fully softened
};
auto forwardFromRotation = [](const SceneObject& obj) {
glm::vec3 f = glm::normalize(glm::vec3(
glm::sin(glm::radians(obj.rotation.y)) * glm::cos(glm::radians(obj.rotation.x)),
glm::sin(glm::radians(obj.rotation.x)),
glm::cos(glm::radians(obj.rotation.y)) * glm::cos(glm::radians(obj.rotation.x))
));
if (glm::length(f) < 1e-3f ||
!std::isfinite(f.x) || !std::isfinite(f.y) || !std::isfinite(f.z)) {
f = glm::vec3(0.0f, -1.0f, 0.0f);
}
return f;
};
std::vector<LightUniform> lights;
lights.reserve(10);
for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (obj.light.enabled && obj.type == ObjectType::DirectionalLight) {
LightUniform l;
l.type = 0;
l.dir = forwardFromRotation(obj);
l.color = obj.light.color;
l.intensity = obj.light.intensity;
lights.push_back(l);
if (lights.size() >= 10) break;
}
}
if (lights.size() < 10) {
for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (obj.light.enabled && obj.type == ObjectType::SpotLight) {
LightUniform l;
l.type = 2;
l.pos = obj.position;
l.dir = forwardFromRotation(obj);
l.color = obj.light.color;
l.intensity = obj.light.intensity;
l.range = obj.light.range;
l.inner = glm::cos(glm::radians(obj.light.innerAngle));
l.outer = glm::cos(glm::radians(obj.light.outerAngle));
lights.push_back(l);
if (lights.size() >= 10) break;
}
}
}
if (lights.size() < 10) {
for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (obj.light.enabled && obj.type == ObjectType::PointLight) {
LightUniform l;
l.type = 1;
l.pos = obj.position;
l.color = obj.light.color;
l.intensity = obj.light.intensity;
l.range = obj.light.range;
lights.push_back(l);
if (lights.size() >= 10) break;
}
}
}
if (lights.size() < 10) {
for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (obj.light.enabled && obj.type == ObjectType::AreaLight) {
LightUniform l;
l.type = 3; // area
l.pos = obj.position;
l.dir = forwardFromRotation(obj); // plane normal
l.color = obj.light.color;
l.intensity = obj.light.intensity;
float sizeHint = glm::max(obj.light.size.x, obj.light.size.y);
l.range = (obj.light.range > 0.0f) ? obj.light.range : glm::max(sizeHint * 2.0f, 1.0f);
l.areaSize = obj.light.size;
l.areaFade = glm::clamp(obj.light.edgeFade, 0.0f, 1.0f);
lights.push_back(l);
if (lights.size() >= 10) break;
}
}
}
for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (!drawMirrorObjects && obj.type == ObjectType::Mirror) continue;
// Skip light gizmo-only types and camera helpers
if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode) {
continue;
}
Shader* active = getShader(obj.vertexShaderPath, obj.fragmentShaderPath);
if (!active) continue;
shader = active;
shader->use();
shader->setMat4("view", camera.getViewMatrix());
shader->setMat4("projection", glm::perspective(glm::radians(fovDeg), (float)width / (float)height, nearPlane, farPlane));
shader->setVec3("viewPos", camera.position);
shader->setBool("unlit", obj.type == ObjectType::Mirror);
shader->setVec3("ambientColor", ambientColor);
shader->setVec3("ambientColor", ambientColor);
shader->setInt("lightCount", static_cast<int>(lights.size()));
for (size_t i = 0; i < lights.size() && i < 10; ++i) {
const auto& l = lights[i];
std::string idx = "[" + std::to_string(i) + "]";
shader->setInt("lightTypeArr" + idx, l.type);
shader->setVec3("lightDirArr" + idx, l.dir);
shader->setVec3("lightPosArr" + idx, l.pos);
shader->setVec3("lightColorArr" + idx, l.color);
shader->setFloat("lightIntensityArr" + idx, l.intensity);
shader->setFloat("lightRangeArr" + idx, l.range);
shader->setFloat("lightInnerCosArr" + idx, l.inner);
shader->setFloat("lightOuterCosArr" + idx, l.outer);
shader->setVec2("lightAreaSizeArr" + idx, l.areaSize);
shader->setFloat("lightAreaFadeArr" + idx, l.areaFade);
}
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, obj.position);
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, obj.scale);
shader->setMat4("model", model);
shader->setVec3("materialColor", obj.material.color);
shader->setFloat("ambientStrength", obj.material.ambientStrength);
shader->setFloat("specularStrength", obj.material.specularStrength);
shader->setFloat("shininess", obj.material.shininess);
shader->setFloat("mixAmount", obj.material.textureMix);
Texture* baseTex = texture1;
if (!obj.albedoTexturePath.empty()) {
if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t;
}
if (baseTex) baseTex->Bind(GL_TEXTURE0);
bool overlayUsed = false;
if (obj.type == ObjectType::Mirror) {
auto it = mirrorTargets.find(obj.id);
if (it != mirrorTargets.end() && it->second.texture != 0) {
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, it->second.texture);
overlayUsed = true;
}
}
if (!overlayUsed && obj.useOverlay && !obj.overlayTexturePath.empty()) {
if (auto* t = getTexture(obj.overlayTexturePath)) {
t->Bind(GL_TEXTURE1);
overlayUsed = true;
}
}
if (!overlayUsed && texture2) {
texture2->Bind(GL_TEXTURE1);
}
shader->setBool("hasOverlay", overlayUsed);
bool normalUsed = false;
if (!obj.normalMapPath.empty()) {
if (auto* t = getTexture(obj.normalMapPath)) {
t->Bind(GL_TEXTURE2);
normalUsed = true;
}
}
shader->setBool("hasNormalMap", normalUsed);
Mesh* meshToDraw = nullptr;
if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh;
else if (obj.type == ObjectType::Sphere) meshToDraw = sphereMesh;
else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh;
else if (obj.type == ObjectType::Mirror) meshToDraw = planeMesh;
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) {
meshToDraw = g_objLoader.getMesh(obj.meshId);
} else if (obj.type == ObjectType::Model && obj.meshId != -1) {
meshToDraw = getModelLoader().getMesh(obj.meshId);
}
if (meshToDraw) {
meshToDraw->draw();
}
}
if (skybox) {
glm::mat4 view = camera.getViewMatrix();
glm::mat4 proj = glm::perspective(glm::radians(fovDeg),
(float)width / height,
nearPlane, farPlane);
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
}
if (unbindFramebuffer) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
PostFXSettings Renderer::gatherPostFX(const std::vector<SceneObject>& sceneObjects) const {
PostFXSettings combined;
combined.enabled = false;
for (const auto& obj : sceneObjects) {
if (obj.type != ObjectType::PostFXNode) continue;
if (!obj.postFx.enabled) continue;
combined = obj.postFx; // Last enabled node wins for now
combined.enabled = true;
}
return combined;
}
unsigned int Renderer::applyPostProcessing(const std::vector<SceneObject>& sceneObjects, unsigned int sourceTexture, int width, int height, bool allowHistory) {
PostFXSettings settings = gatherPostFX(sceneObjects);
GLint polygonMode[2] = { GL_FILL, GL_FILL };
#ifdef GL_POLYGON_MODE
glGetIntegerv(GL_POLYGON_MODE, polygonMode);
#endif
bool wireframe = (polygonMode[0] == GL_LINE || polygonMode[1] == GL_LINE);
bool wantsEffects = settings.enabled &&
(settings.bloomEnabled || settings.colorAdjustEnabled || settings.motionBlurEnabled ||
settings.vignetteEnabled || settings.chromaticAberrationEnabled || settings.ambientOcclusionEnabled);
if (wireframe) {
wantsEffects = false;
}
if (!wantsEffects || !postShader || width <= 0 || height <= 0 || sourceTexture == 0) {
if (allowHistory) {
displayTexture = sourceTexture;
clearHistory();
}
return sourceTexture;
}
RenderTarget& target = allowHistory ? postTarget : previewPostTarget;
ensureRenderTarget(target, width, height);
if (allowHistory) {
ensureRenderTarget(historyTarget, width, height);
}
if (target.fbo == 0 || target.texture == 0) {
if (allowHistory) {
displayTexture = sourceTexture;
clearHistory();
}
return sourceTexture;
}
// --- Bloom using bright pass + separable blur (inspired by ProcessingPostFX) ---
unsigned int bloomTex = 0;
if (settings.bloomEnabled && brightShader && blurShader) {
// Bright pass
glDisable(GL_DEPTH_TEST);
brightShader->use();
brightShader->setFloat("threshold", settings.bloomThreshold);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sourceTexture);
glBindFramebuffer(GL_FRAMEBUFFER, bloomTargetA.fbo);
glViewport(0, 0, width, height);
glClear(GL_COLOR_BUFFER_BIT);
drawFullscreenQuad();
// Blur ping-pong
blurShader->use();
float sigma = glm::max(settings.bloomRadius * 2.5f, 0.1f);
int radius = static_cast<int>(glm::clamp(settings.bloomRadius * 4.0f, 2.0f, 12.0f));
blurShader->setFloat("sigma", sigma);
blurShader->setInt("radius", radius);
bool horizontal = true;
unsigned int pingTex = bloomTargetA.texture;
RenderTarget* writeTarget = &bloomTargetB;
for (int i = 0; i < 4; ++i) {
blurShader->setBool("horizontal", horizontal);
blurShader->setVec2("texelSize", glm::vec2(1.0f / width, 1.0f / height));
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, pingTex);
glBindFramebuffer(GL_FRAMEBUFFER, writeTarget->fbo);
glViewport(0, 0, width, height);
glClear(GL_COLOR_BUFFER_BIT);
drawFullscreenQuad();
// swap
pingTex = writeTarget->texture;
writeTarget = (writeTarget == &bloomTargetA) ? &bloomTargetB : &bloomTargetA;
horizontal = !horizontal;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glEnable(GL_DEPTH_TEST);
bloomTex = pingTex;
} else {
bloomTex = 0;
clearTarget(bloomTargetA);
clearTarget(bloomTargetB);
}
glDisable(GL_DEPTH_TEST);
postShader->use();
postShader->setBool("enableBloom", settings.bloomEnabled && bloomTex != 0);
postShader->setFloat("bloomIntensity", settings.bloomIntensity);
postShader->setBool("enableColorAdjust", settings.colorAdjustEnabled);
postShader->setFloat("exposure", settings.exposure);
postShader->setFloat("contrast", settings.contrast);
postShader->setFloat("saturation", settings.saturation);
postShader->setVec3("colorFilter", settings.colorFilter);
postShader->setBool("enableMotionBlur", settings.motionBlurEnabled);
postShader->setFloat("motionBlurStrength", settings.motionBlurStrength);
postShader->setBool("hasHistory", allowHistory && historyValid);
postShader->setBool("enableVignette", settings.vignetteEnabled);
postShader->setFloat("vignetteIntensity", settings.vignetteIntensity);
postShader->setFloat("vignetteSmoothness", settings.vignetteSmoothness);
postShader->setBool("enableChromatic", settings.chromaticAberrationEnabled);
postShader->setFloat("chromaticAmount", settings.chromaticAmount);
postShader->setBool("enableAO", settings.ambientOcclusionEnabled);
postShader->setFloat("aoRadius", settings.aoRadius);
postShader->setFloat("aoStrength", settings.aoStrength);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sourceTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, bloomTex ? bloomTex : sourceTexture);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, allowHistory ? historyTarget.texture : 0);
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
glViewport(0, 0, width, height);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
drawFullscreenQuad();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glEnable(GL_DEPTH_TEST);
if (allowHistory) {
displayTexture = target.texture;
}
if (settings.motionBlurEnabled && allowHistory && historyTarget.fbo != 0) {
glBindFramebuffer(GL_READ_FRAMEBUFFER, target.fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, historyTarget.fbo);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
historyValid = true;
} else if (allowHistory) {
clearHistory();
}
return target.texture;
}
void Renderer::renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int /*selectedId*/, float fovDeg, float nearPlane, float farPlane) {
updateMirrorTargets(camera, sceneObjects, currentWidth, currentHeight, fovDeg, nearPlane, farPlane);
renderSceneInternal(camera, sceneObjects, currentWidth, currentHeight, true, fovDeg, nearPlane, farPlane, true);
unsigned int result = applyPostProcessing(sceneObjects, viewportTexture, currentWidth, currentHeight, true);
displayTexture = result ? result : viewportTexture;
}
unsigned int Renderer::renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX) {
ensureRenderTarget(previewTarget, width, height);
if (previewTarget.fbo == 0) return 0;
glBindFramebuffer(GL_FRAMEBUFFER, previewTarget.fbo);
glViewport(0, 0, width, height);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
updateMirrorTargets(camera, sceneObjects, width, height, fovDeg, nearPlane, farPlane);
renderSceneInternal(camera, sceneObjects, width, height, true, fovDeg, nearPlane, farPlane, true);
if (!applyPostFX) {
return previewTarget.texture;
}
unsigned int processed = applyPostProcessing(sceneObjects, previewTarget.texture, width, height, false);
return processed ? processed : previewTarget.texture;
}
void Renderer::endRender() {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}