#include "Debug.h" #include "RenderSystem.h" #include "common.h" #include "VBO.h" #include "VAO.h" #include "EBO.h" #include "FBO.h" #include "shaderClass.h" #include "Object.h" #include "Camera.h" #include "Cubemap.h" #include "QuadVertices.h" #include "Lighting.h" #include "Animator.h" static const float hitboxLineVertices[] = { -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, }; static void ConfigurePBR(Shader* shader, Lighting* lighting) { shader->Set3F("dirLight.direction", lighting->sun.direction); shader->Set3F("dirLight.color", lighting->sun.color); shader->Set1F("dirLight.intensity", lighting->sun.intensity); shader->Set1F("envIntensity", 0.8f); shader->SetInt("environmentMap", 2); const int directionalCount = glm::min((int)lighting->directionalLights.size(), MAX_DIRECTIONAL_LIGHTS); shader->SetInt("directionalLightCount", directionalCount); for (int i = 0; i < directionalCount; i++) { const DirectionalLight* light = lighting->directionalLights[i]; const std::string name = "directionalLights[" + std::to_string(i) + "]."; shader->Set3F((name + "direction").c_str(), light->direction); shader->Set3F((name + "color").c_str(), light->color); shader->Set1F((name + "intensity").c_str(), light->intensity); } const int pointCount = glm::min((int)lighting->pointLights.size(), MAX_POINT_LIGHTS); shader->SetInt("pointLightCount", pointCount); for (int i = 0; i < pointCount; i++) { const PointLight* light = lighting->pointLights[i]; const std::string name = "pointLights[" + std::to_string(i) + "]."; shader->Set3F((name + "position").c_str(), light->position); shader->Set3F((name + "color").c_str(), light->color); shader->Set1F((name + "intensity").c_str(), light->intensity); shader->Set1F((name + "radius").c_str(), light->radius); shader->SetInt((name + "shadowTile").c_str(), light->shadowTile); } shader->SetInt("localShadowMap", 10); if (!lighting->shadowTileMatrices.empty()) { shader->SetMat4Array("shadowTileMatrices", lighting->shadowTileMatrices.data(), (int)lighting->shadowTileMatrices.size()); } const int spotCount = glm::min((int)lighting->spotLights.size(), MAX_SPOT_LIGHTS); shader->SetInt("spotLightCount", spotCount); for (int i = 0; i < spotCount; i++) { const SpotLight* light = lighting->spotLights[i]; const std::string name = "spotLights[" + std::to_string(i) + "]."; shader->Set3F((name + "position").c_str(), light->position); shader->Set3F((name + "direction").c_str(), light->direction); shader->Set3F((name + "color").c_str(), light->color); shader->Set1F((name + "intensity").c_str(), light->intensity); shader->Set1F((name + "radius").c_str(), light->radius); shader->Set1F((name + "innerCos").c_str(), std::cos(glm::radians(light->innerAngle))); shader->Set1F((name + "outerCos").c_str(), std::cos(glm::radians(light->outerAngle))); shader->SetInt((name + "shadowTile").c_str(), light->shadowTile); } } // ============================================================================ // Initialize // ============================================================================ void RenderSystem::Initialize() { glEnable(GL_DEPTH_TEST); glViewport(0, 0, window->width, window->height); glEnable(GL_MULTISAMPLE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); glFrontFace(GL_CCW); // Screen quad screenfbo = new FBO(); skyboxVAO = new VAO(); skyboxVBO = new VBO(); quadVAO = new VAO(); quadVBO = new VBO(); skyboxVAO->GenerateID(); skyboxVBO->GenerateID(); skyboxVAO->Bind(); skyboxVBO->Bind(); glBufferData(GL_ARRAY_BUFFER, sizeof(skyboxVertices), &skyboxVertices, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); quadVAO->GenerateID(); quadVBO->GenerateID(); quadVAO->Bind(); quadVBO->Bind(); quadVBO->BufferData(quadVertices, sizeof(quadVertices)); quadVAO->LinkVBO(*quadVBO, 0, 2, GL_FLOAT, 4 * sizeof(float), (void*)0); quadVAO->LinkVBO(*quadVBO, 1, 2, GL_FLOAT, 4 * sizeof(float), (void*)(2 * sizeof(float))); glGenVertexArrays(1, &hitboxVAO); glGenBuffers(1, &hitboxVBO); glBindVertexArray(hitboxVAO); glBindBuffer(GL_ARRAY_BUFFER, hitboxVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(hitboxLineVertices), hitboxLineVertices, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glBindVertexArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); std::vector faces = { "assets/miramar_ft.tga", "assets/miramar_bk.tga", "assets/miramar_up.tga", "assets/miramar_dn.tga", "assets/miramar_rt.tga", "assets/miramar_lf.tga" }; Cubemap* skyboxcm = new Cubemap(); skyboxcm->FillCubemap(faces); skybox = skyboxcm; // Screen framebuffer screenfbo->Bind(); glGenTextures(1, &textureColorbuffer); glBindTexture(GL_TEXTURE_2D, textureColorbuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, window->width, window->height, 0, GL_RGBA, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glBindTexture(GL_TEXTURE_2D, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorbuffer, 0); glGenRenderbuffers(1, &rbo); glBindRenderbuffer(GL_RENDERBUFFER, rbo); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, window->width, window->height); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!\n"; screenfbo->Unbind(); // G-buffer gBuffer = new FBO(); gBuffer->Bind(); unsigned int* gTextures[] = { &gPosition, &gNormal, &gAlbedo, &gMaterial, &gDepth }; for (unsigned int* texture : gTextures) { glGenTextures(1, texture); glBindTexture(GL_TEXTURE_2D, *texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } AllocateGBuffer(window->width, window->height); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedo, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT3, GL_TEXTURE_2D, gMaterial, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, gDepth, 0); const GLenum gDrawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 }; glDrawBuffers(4, gDrawBuffers); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "ERROR::FRAMEBUFFER:: G-buffer is not complete!\n"; gBuffer->Unbind(); // Bloom framebuffers, the blur bounces between these two for (int i = 0; i < 2; i++) { bloomFBO[i] = new FBO(); glGenTextures(1, &bloomTextures[i]); glBindTexture(GL_TEXTURE_2D, bloomTextures[i]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } AllocateBloom(window->width, window->height); for (int i = 0; i < 2; i++) { bloomFBO[i]->Bind(); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, bloomTextures[i], 0); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "ERROR::FRAMEBUFFER:: Bloom framebuffer is not complete!\n"; bloomFBO[i]->Unbind(); } // Shadow framebuffer shadowdepthmapfbo = new FBO(); SHADOW_RESOLUTION = 4096; glGenTextures(1, &depthMaptexture); glBindTexture(GL_TEXTURE_2D, depthMaptexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_RESOLUTION, SHADOW_RESOLUTION, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); float borderColor[] = { 1.0f, 1.0f, 1.0f, 1.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); shadowdepthmapfbo->Bind(); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMaptexture, 0); glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); glBindFramebuffer(GL_FRAMEBUFFER, 0); // Spot and point light shadow atlas. Unit 10 stays reserved for it, so it's only bound here. const int shadowAtlasSize = SHADOW_ATLAS_COLUMNS * SHADOW_ATLAS_TILE_SIZE; localShadowFBO = new FBO(); glGenTextures(1, &localShadowAtlas); glActiveTexture(GL_TEXTURE10); glBindTexture(GL_TEXTURE_2D, localShadowAtlas); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, shadowAtlasSize, shadowAtlasSize, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); glActiveTexture(GL_TEXTURE0); localShadowFBO->Bind(); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, localShadowAtlas, 0); glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "ERROR::FRAMEBUFFER:: Shadow atlas is not complete!\n"; localShadowFBO->Unbind(); // The shadow map uses compare mode, which can't be read as plain depth without this glGenSamplers(1, &shadowDebugSampler); glSamplerParameteri(shadowDebugSampler, GL_TEXTURE_COMPARE_MODE, GL_NONE); glSamplerParameteri(shadowDebugSampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glSamplerParameteri(shadowDebugSampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR); lighting = new Lighting(); } void RenderSystem::AllocateGBuffer(int width, int height) { glBindTexture(GL_TEXTURE_2D, gPosition); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, NULL); glBindTexture(GL_TEXTURE_2D, gNormal); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL); glBindTexture(GL_TEXTURE_2D, gAlbedo); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL); glBindTexture(GL_TEXTURE_2D, gMaterial); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); // Same format as the screen rbo, the depth blit between them requires it glBindTexture(GL_TEXTURE_2D, gDepth); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL); glBindTexture(GL_TEXTURE_2D, 0); } // Quarter resolution, the blur reaches much further for the same number of samples void RenderSystem::AllocateBloom(int width, int height) { bloomWidth = glm::max(width / 4, 1); bloomHeight = glm::max(height / 4, 1); for (unsigned int texture : bloomTextures) { glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, bloomWidth, bloomHeight, 0, GL_RGBA, GL_FLOAT, NULL); } glBindTexture(GL_TEXTURE_2D, 0); } void RenderSystem::CycleDebugView() { static const char* const viewNames[] = { "FINAL", "POSITION", "NORMAL", "ALBEDO", "MATERIAL", "DEPTH", "SHADOW MAP", "BLOOM", "SHADOW ATLAS" }; debugView = (debugView + 1) % 9; std::cout << "DEBUG VIEW: " << viewNames[debugView] << "\n"; } void RenderSystem::Resize(int width, int height) { if (!screenfbo || !gBuffer || !bloomFBO[0] || textureColorbuffer == 0 || rbo == 0 || width <= 0 || height <= 0) return; AllocateBloom(width, height); AllocateGBuffer(width, height); gBuffer->Bind(); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "ERROR::FRAMEBUFFER:: G-buffer is not complete after resize!\n"; gBuffer->Unbind(); glBindTexture(GL_TEXTURE_2D, textureColorbuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL); glBindTexture(GL_TEXTURE_2D, 0); glBindRenderbuffer(GL_RENDERBUFFER, rbo); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); glBindRenderbuffer(GL_RENDERBUFFER, 0); screenfbo->Bind(); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete after resize!\n"; screenfbo->Unbind(); glViewport(0, 0, width, height); } // ============================================================================ // RenderTranslucent // Collects all non-opaque 3D renderables (mesh + particle), sorts back-to-front, // then renders each with the correct shader. Blending stays on throughout. // ============================================================================ void RenderSystem::RenderTranslucent(Camera& camera, const glm::mat4& lightSpaceMatrix, const glm::vec3& camPos) { // Collect translucent 3D renderables std::vector> sorted; for (Renderable* r : renderables) { if (r->opaque) continue; if (r->shadertype != Renderable::MeshShader && r->shadertype != Renderable::ParticleShader && r->shadertype != Renderable::RigShader) continue; glm::vec3 pos = r->GetWorldPosition(); float dist = glm::length(pos - camPos); sorted.push_back({ dist, r }); } // Sort back-to-front std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { return a.first > b.first; }); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDepthMask(GL_FALSE); // write to color but not depth � prevents z-fighting between translucents Renderable::ShaderType lastShader = Renderable::ShaderType::MeshShader; bool shaderSet = false; for (auto& [dist, r] : sorted) { // Only re-bind shader when it changes if (!shaderSet || r->shadertype != lastShader) { if (r->shadertype == Renderable::MeshShader) { PrepareMeshShader(camera); MeshShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, depthMaptexture); } else if (r->shadertype == Renderable::ParticleShader) { PrepareParticleShader(camera); } else if (r->shadertype == Renderable::RigShader) { PrepareRigShader(camera); RigShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, depthMaptexture); } lastShader = r->shadertype; shaderSet = true; } Shader* sh = (r->shadertype == Renderable::MeshShader) ? MeshShader : (r->shadertype == Renderable::ParticleShader) ? ParticleShader : RigShader; r->Render(*sh); } glDepthMask(GL_TRUE); // restore depth writing for subsequent passes } // ============================================================================ // Render // ============================================================================ void RenderSystem::Render(Camera& camera) { lighting->sun.direction = glm::normalize(glm::quat(glm::vec3(glm::radians(0.05f), 0.0f, 0.0f)) * lighting->sun.direction); const glm::mat4 proj = camera.GetProjectionMatrix(0.05f, 2000.0f); const glm::mat4 view = camera.GetViewMatrix(); const glm::vec3 camPos = camera.t.GetTranslation(); const float near_plane = 0.1f, far_plane = 100.0f; const glm::mat4 lightSpaceMatrix = glm::ortho(-32.0f, 32.0f, -32.0f, 32.0f, near_plane, far_plane) * glm::lookAt(camPos - lighting->sun.direction * 10.0f, camPos, camera.t.GetUpVector()); // ---- Shadow pass ---- glCullFace(GL_FRONT); glEnable(GL_DEPTH_TEST); glViewport(0, 0, SHADOW_RESOLUTION, SHADOW_RESOLUTION); shadowdepthmapfbo->Bind(); glClear(GL_DEPTH_BUFFER_BIT); if (graphics.shadows) RenderShadowCasters(lightSpaceMatrix); // with nothing drawn, the cleared map leaves everything lit RenderLocalShadows(); glCullFace(GL_BACK); // ---- Geometry pass ---- RenderGeometryPass(proj, view, camPos); // ---- Main scene ---- // Translucents and hitboxes are still forward, they depth test against the G-buffer's depth glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer->ID); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, screenfbo->ID); glBlitFramebuffer(0, 0, window->width, window->height, 0, 0, window->width, window->height, GL_DEPTH_BUFFER_BIT, GL_NEAREST); screenfbo->Bind(); glClearColor(212.f / 255.f, 223.f / 255.f, 232.f / 255.f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Skybox, depth tested so nothing covered by geometry gets shaded glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glDepthFunc(GL_LEQUAL); glDepthMask(GL_FALSE); SkyboxShader->Activate(); SkyboxShader->SetMat4("proj", proj); SkyboxShader->SetMat4("view", glm::mat4(glm::mat3(view))); skyboxVAO->Bind(); glActiveTexture(GL_TEXTURE0); skybox->Bind(); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthMask(GL_TRUE); glDepthFunc(GL_LESS); // ---- Lighting pass ---- glDisable(GL_DEPTH_TEST); RenderLightingPass(camPos, lightSpaceMatrix); glEnable(GL_DEPTH_TEST); // ---- Translucent pass (meshes + particles + rigs, sorted back-to-front) ---- // Disable face culling so thin translucent quads render both sides glDisable(GL_CULL_FACE); RenderTranslucent(camera, lightSpaceMatrix, camPos); glEnable(GL_CULL_FACE); RenderHitboxDebug(camera); // ---- Bloom ---- glDisable(GL_DEPTH_TEST); if (graphics.bloom) RenderBloom(); else screenfbo->Unbind(); // ---- Framebuffer blit to screen ---- glViewport(0, 0, window->width, window->height); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); if (debugView == 0) { ScreenShader->Activate(); ScreenShader->Set1F("exposure", 1.0f); ScreenShader->Set1F("bloomStrength", graphics.bloom ? 0.6f : 0.0f); ScreenShader->SetInt("screenTexture", 0); ScreenShader->SetInt("bloomTexture", 1); quadVAO->Bind(); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, bloomTextures[0]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureColorbuffer); glDrawArrays(GL_TRIANGLES, 0, 6); } else { RenderDebugView(); } // ---- GUI (always on top, no depth) ---- // In the order things were made, boxes and text mixed, so a menu made later covers the text of one under it. // The shader only gets switched when the kind of element changes. glDisable(GL_CULL_FACE); int activeGuiShader = -1; for (Renderable* r : renderables) { const int type = r->shadertype; if (type != Renderable::BoxShader && type != Renderable::ImageBoxShader && type != Renderable::TextShader) continue; if (type != activeGuiShader) { if (type == Renderable::BoxShader) PrepareBoxShader(camera); else if (type == Renderable::ImageBoxShader) PrepareImageBoxShader(camera); else PrepareTextShader(camera); activeGuiShader = type; } if (type == Renderable::BoxShader) r->Render(*BoxShader); else if (type == Renderable::ImageBoxShader) r->Render(*ImageBoxShader); else r->Render(*Text2DShader); } glEnable(GL_CULL_FACE); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, 0); glfwSwapBuffers(window->handle); } // ============================================================================ // Deferred passes // ============================================================================ void RenderSystem::RenderGeometryPass(const glm::mat4& proj, const glm::mat4& view, const glm::vec3& camPos) { gBuffer->Bind(); glViewport(0, 0, window->width, window->height); glEnable(GL_DEPTH_TEST); glDisable(GL_BLEND); // the attachments store data in alpha, blending would corrupt them glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Frustum planes as (normal, distance), pulled straight out of the view projection matrix const glm::mat4 viewProjection = proj * view; glm::vec4 frustum[6]; for (int axis = 0; axis < 3; axis++) { const glm::vec4 row(viewProjection[0][axis], viewProjection[1][axis], viewProjection[2][axis], viewProjection[3][axis]); const glm::vec4 last(viewProjection[0][3], viewProjection[1][3], viewProjection[2][3], viewProjection[3][3]); frustum[axis * 2] = last + row; frustum[axis * 2 + 1] = last - row; } occlusionCandidates.clear(); GeometryShader->Activate(); GeometryShader->SetMat4("proj", proj); GeometryShader->SetMat4("view", view); for (Renderable* r : renderables) { if (r->shadertype == Renderable::MeshShader && r->opaque && IsVisible(r, frustum, camPos)) r->Render(*GeometryShader); } GeometryRigShader->Activate(); GeometryRigShader->SetMat4("proj", proj); GeometryRigShader->SetMat4("view", view); for (Renderable* r : renderables) { if (r->shadertype == Renderable::RigShader && r->opaque && IsVisible(r, frustum, camPos)) r->Render(*GeometryRigShader); } IssueOcclusionQueries(proj, view); glEnable(GL_BLEND); } // Bounds padding, so a box sitting exactly on its object's surface can't fail its own depth test static constexpr float OCCLUSION_PADDING = 0.05f; bool RenderSystem::IsVisible(Renderable* r, const glm::vec4 (&frustum)[6], const glm::vec3& camPos) { BoundingBox bounds; if (!graphics.culling || !r->GetWorldBounds(bounds)) { r->occluded = false; return true; } // Frustum: outside if the corner furthest along a plane's normal is still behind it for (const glm::vec4& plane : frustum) { const glm::vec3 normal(plane); const glm::vec3 furthest( normal.x >= 0.0f ? bounds.max.x : bounds.min.x, normal.y >= 0.0f ? bounds.max.y : bounds.min.y, normal.z >= 0.0f ? bounds.max.z : bounds.min.z); if (glm::dot(normal, furthest) + plane.w < 0.0f) { r->occluded = false; // so it draws straight away when it comes back into view return false; } } // Only use an answer the GPU already has, waiting for one would stall the frame if (r->occlusionPending) { GLuint available = 0; glGetQueryObjectuiv(r->occlusionQuery, GL_QUERY_RESULT_AVAILABLE, &available); if (available) { GLuint samplesPassed = 0; glGetQueryObjectuiv(r->occlusionQuery, GL_QUERY_RESULT, &samplesPassed); r->occluded = samplesPassed == 0; r->occlusionPending = false; } } // The query box can't be seen from inside it, so never trust it then const glm::vec3 padding(OCCLUSION_PADDING * 2.0f); if (glm::all(glm::greaterThan(camPos, bounds.min - padding)) && glm::all(glm::lessThan(camPos, bounds.max + padding))) r->occluded = false; occlusionCandidates.push_back(r); return !r->occluded; } // Draws each candidate's box against this frame's depth, colour and depth writes off. // The results get used next frame. void RenderSystem::IssueOcclusionQueries(const glm::mat4& proj, const glm::mat4& view) { if (occlusionCandidates.empty()) return; glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glDepthMask(GL_FALSE); glDisable(GL_CULL_FACE); // the skybox cube faces inward HitboxShader->Activate(); HitboxShader->SetMat4("proj", proj); HitboxShader->SetMat4("view", view); skyboxVAO->Bind(); for (Renderable* r : occlusionCandidates) { if (r->occlusionPending) continue; BoundingBox bounds; r->GetWorldBounds(bounds); const glm::vec3 center = (bounds.min + bounds.max) * 0.5f; const glm::vec3 halfSize = (bounds.max - bounds.min) * 0.5f + glm::vec3(OCCLUSION_PADDING); if (r->occlusionQuery == 0) glGenQueries(1, &r->occlusionQuery); // The skybox cube runs from -1 to 1, so half size is the scale HitboxShader->SetMat4("modl", glm::scale(glm::translate(glm::mat4(1.0f), center), halfSize)); glBeginQuery(GL_ANY_SAMPLES_PASSED, r->occlusionQuery); glDrawArrays(GL_TRIANGLES, 0, 36); glEndQuery(GL_ANY_SAMPLES_PASSED); r->occlusionPending = true; } glBindVertexArray(0); glEnable(GL_CULL_FACE); glDepthMask(GL_TRUE); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); } void RenderSystem::RenderShadowCasters(const glm::mat4& lightSpaceMatrix) { MeshShadowShader->Activate(); MeshShadowShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); for (Renderable* r : renderables) { if (!r->castshadow || r->shadertype != Renderable::MeshShader) continue; r->Render(*MeshShadowShader); } RigShadowShader->Activate(); RigShadowShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); for (Renderable* r : renderables) { if (!r->castshadow || r->shadertype != Renderable::RigShader) continue; r->Render(*RigShadowShader); } } // Draws the casters from each spot and point light into its tiles of the atlas void RenderSystem::RenderLocalShadows() { static constexpr float nearPlane = 0.2f; std::vector& tiles = lighting->shadowTileMatrices; tiles.clear(); if (!graphics.lightShadows) { for (SpotLight* light : lighting->spotLights) light->shadowTile = -1; for (PointLight* light : lighting->pointLights) light->shadowTile = -1; return; } localShadowFBO->Bind(); glClear(GL_DEPTH_BUFFER_BIT); glEnable(GL_SCISSOR_TEST); auto RenderTile = [&](const glm::mat4& matrix) { const int index = (int)tiles.size(); const int x = (index % SHADOW_ATLAS_COLUMNS) * SHADOW_ATLAS_TILE_SIZE; const int y = (index / SHADOW_ATLAS_COLUMNS) * SHADOW_ATLAS_TILE_SIZE; glViewport(x, y, SHADOW_ATLAS_TILE_SIZE, SHADOW_ATLAS_TILE_SIZE); glScissor(x, y, SHADOW_ATLAS_TILE_SIZE, SHADOW_ATLAS_TILE_SIZE); RenderShadowCasters(matrix); tiles.push_back(matrix); }; for (SpotLight* light : lighting->spotLights) { light->shadowTile = -1; if (!light->castShadow || (int)tiles.size() >= SHADOW_ATLAS_TILES) continue; light->shadowTile = (int)tiles.size(); const glm::vec3 up = glm::abs(light->direction.y) > 0.99f ? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f); RenderTile(glm::perspective(glm::radians(light->outerAngle * 2.0f + 2.0f), 1.0f, nearPlane, light->radius) * glm::lookAt(light->position, light->position + light->direction, up)); } // Order has to match PointShadowFace in the shaders. Slightly over 90 degrees so the faces overlap at the seams. static const glm::vec3 faceDirections[6] = { { 1.0f, 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f, -1.0f } }; static const glm::vec3 faceUps[6] = { { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } }; for (PointLight* light : lighting->pointLights) { light->shadowTile = -1; if (!light->castShadow || (int)tiles.size() + 6 > SHADOW_ATLAS_TILES) continue; light->shadowTile = (int)tiles.size(); const glm::mat4 projection = glm::perspective(glm::radians(92.0f), 1.0f, nearPlane, light->radius); for (int face = 0; face < 6; face++) RenderTile(projection * glm::lookAt(light->position, light->position + faceDirections[face], faceUps[face])); } glDisable(GL_SCISSOR_TEST); localShadowFBO->Unbind(); } // Leaves the finished glow in bloomTextures[0] and the default framebuffer bound void RenderSystem::RenderBloom() { static constexpr int blurPasses = 8; // has to stay even, or the result ends up in bloomTextures[1] glDisable(GL_BLEND); glViewport(0, 0, bloomWidth, bloomHeight); quadVAO->Bind(); glActiveTexture(GL_TEXTURE0); bloomFBO[0]->Bind(); BloomExtractShader->Activate(); BloomExtractShader->SetInt("screenTexture", 0); BloomExtractShader->Set1F("threshold", 1.8f); glBindTexture(GL_TEXTURE_2D, textureColorbuffer); glDrawArrays(GL_TRIANGLES, 0, 6); BloomBlurShader->Activate(); BloomBlurShader->SetInt("image", 0); for (int pass = 0; pass < blurPasses; pass++) { const int source = pass % 2; bloomFBO[1 - source]->Bind(); BloomBlurShader->SetInt("horizontal", source == 0 ? 1 : 0); glBindTexture(GL_TEXTURE_2D, bloomTextures[source]); glDrawArrays(GL_TRIANGLES, 0, 6); } bloomFBO[0]->Unbind(); glEnable(GL_BLEND); } // Units 0 and 2 stay the shadow map and environment map, same as the forward shaders void RenderSystem::BindGBufferTextures(Shader* shader) { shader->SetInt("gPosition", 3); shader->SetInt("gNormal", 4); shader->SetInt("gAlbedo", 5); shader->SetInt("gMaterial", 6); glActiveTexture(GL_TEXTURE3); glBindTexture(GL_TEXTURE_2D, gPosition); glActiveTexture(GL_TEXTURE4); glBindTexture(GL_TEXTURE_2D, gNormal); glActiveTexture(GL_TEXTURE5); glBindTexture(GL_TEXTURE_2D, gAlbedo); glActiveTexture(GL_TEXTURE6); glBindTexture(GL_TEXTURE_2D, gMaterial); } // Expects depth testing off, the fullscreen quad shouldn't touch the depth buffer void RenderSystem::RenderLightingPass(const glm::vec3& camPos, const glm::mat4& lightSpaceMatrix) { DeferredLightingShader->Activate(); DeferredLightingShader->Set3F("viewPos", camPos); DeferredLightingShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); DeferredLightingShader->SetInt("shadowMap", 0); ConfigurePBR(DeferredLightingShader, lighting); BindGBufferTextures(DeferredLightingShader); glActiveTexture(GL_TEXTURE2); skybox->Bind(); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, depthMaptexture); quadVAO->Bind(); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); } void RenderSystem::RenderDebugView() { GBufferDebugShader->Activate(); const bool showAtlas = debugView == 8; // reuses the shadow map view GBufferDebugShader->SetInt("debugMode", showAtlas ? 6 : debugView); GBufferDebugShader->Set1F("nearPlane", 0.05f); GBufferDebugShader->Set1F("farPlane", 2000.0f); BindGBufferTextures(GBufferDebugShader); GBufferDebugShader->SetInt("depthTex", 7); GBufferDebugShader->SetInt("shadowTex", 8); glActiveTexture(GL_TEXTURE7); glBindTexture(GL_TEXTURE_2D, gDepth); glActiveTexture(GL_TEXTURE8); glBindTexture(GL_TEXTURE_2D, showAtlas ? localShadowAtlas : depthMaptexture); glBindSampler(8, shadowDebugSampler); GBufferDebugShader->SetInt("bloomTex", 9); glActiveTexture(GL_TEXTURE9); glBindTexture(GL_TEXTURE_2D, bloomTextures[0]); quadVAO->Bind(); glDrawArrays(GL_TRIANGLES, 0, 6); glBindSampler(8, 0); glActiveTexture(GL_TEXTURE0); } // ============================================================================ // Shader preparation helpers // ============================================================================ void RenderSystem::RenderRenderable(Renderable* renderable, Camera& camera) { Shader* shader = (renderable->shadertype == Renderable::MeshShader) ? MeshShader : (renderable->shadertype == Renderable::ParticleShader) ? ParticleShader : RigShader; shader->Activate(); renderable->Render(*shader); } void RenderSystem::RenderHitboxDebug(Camera& camera) { if (hitboxDebugLines.empty() || !HitboxShader || hitboxVAO == 0) return; HitboxShader->Activate(); HitboxShader->SetMat4("proj", camera.GetProjectionMatrix(0.05f, 2000.0f)); HitboxShader->SetMat4("view", camera.GetViewMatrix()); glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); glLineWidth(2.0f); glBindVertexArray(hitboxVAO); for (const HitboxDebugLine& line : hitboxDebugLines) { HitboxShader->SetMat4("modl", line.transform); HitboxShader->Set3F("hitboxColor", line.color); glDrawArrays(GL_LINES, 0, 24); } glBindVertexArray(0); glLineWidth(1.0f); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); } void RenderSystem::PrepareMeshShader(Camera& camera) { MeshShader->Activate(); MeshShader->SetMat4("proj", camera.GetProjectionMatrix(0.05f, 2000.0f)); MeshShader->SetMat4("view", camera.GetViewMatrix()); MeshShader->Set3F("viewPos", camera.t.GetTranslation()); ConfigurePBR(MeshShader, lighting); glActiveTexture(GL_TEXTURE2); skybox->Bind(); glActiveTexture(GL_TEXTURE0); } void RenderSystem::PrepareRigShader(Camera& camera) { RigShader->Activate(); RigShader->SetMat4("proj", camera.GetProjectionMatrix(0.05f, 2000.0f)); RigShader->SetMat4("view", camera.GetViewMatrix()); RigShader->Set3F("viewPos", camera.t.GetTranslation()); ConfigurePBR(RigShader, lighting); glActiveTexture(GL_TEXTURE2); skybox->Bind(); glActiveTexture(GL_TEXTURE0); } void RenderSystem::PrepareParticleShader(Camera& camera) { ParticleShader->Activate(); ParticleShader->SetMat4("proj", camera.GetProjectionMatrix(0.05f, 2000.0f)); ParticleShader->SetMat4("view", camera.GetViewMatrix()); } void RenderSystem::PrepareBoxShader(Camera& camera) { BoxShader->Activate(); BoxShader->Set2F("screenSize", { (float)window->width, (float)window->height }); } void RenderSystem::PrepareImageBoxShader(Camera& camera) { ImageBoxShader->Activate(); ImageBoxShader->Set2F("screenSize", { (float)window->width, (float)window->height }); } void RenderSystem::PrepareTextShader(Camera& camera) { Text2DShader->Activate(); Text2DShader->Set2F("screenSize", { (float)window->width, (float)window->height }); } // ============================================================================ // Renderable base // ============================================================================ Renderable::Renderable() {} Renderable::~Renderable() { if (occlusionQuery != 0) glDeleteQueries(1, &occlusionQuery); engine->rendersystem->RemoveRenderable(this); } void Renderable::BindToRenderSystem() { engine->rendersystem->AddRenderable(this); }