#include "Rendering.h" #include "Camera.h" #include "ModelLoader.h" #include #include #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 generateSphere(int segments, int rings) { std::vector 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 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 generateCapsule(int segments, int rings) { std::vector 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 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(i); } } tinyobj::attrib_t attrib; std::vector shapes; std::vector 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 vertices; bool hasNormalsInFile = !attrib.normals.empty(); int faceCount = 0; for (const auto& shape : shapes) { faceCount += static_cast(shape.mesh.num_face_vertices.size()); } glm::vec3 boundsMin(FLT_MAX); glm::vec3 boundsMax(-FLT_MAX); std::vector 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 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(vertices.data(), vertices.size() * sizeof(float)); loaded.vertexCount = static_cast(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(loadedMeshes.size() - 1); } Mesh* OBJLoader::getMesh(int index) { if (index < 0 || index >= static_cast(loadedMeshes.size())) { return nullptr; } return loadedMeshes[index].mesh.get(); } const OBJLoader::LoadedMesh* OBJLoader::getMeshInfo(int index) const { if (index < 0 || index >= static_cast(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(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 newShader = std::make_unique(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(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(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& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane) { if (sceneObjects.empty() || width <= 0 || height <= 0) return; std::unordered_set 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& 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 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(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& 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& 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(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& 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& 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); }