SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
OpenGLRenderer.cpp
Go to the documentation of this file.
13#include <Camera/Camera.hpp>
14#include <Core/Application.hpp>
15#include <Core/SceneBase.hpp>
16#include <Runtime/Skybox.hpp>
17#include <Runtime/Material.hpp>
18#include <Core/GameBase.hpp>
19#include <SDL3/SDL.h>
20#include <vector>
21#include <cstdint>
22#include <cstring>
23
24namespace Sleak {
25namespace RenderEngine {
26
27/// Registers this backend's buffer/shader/texture factories with ResourceManager.
50
54
56 if (m_Initialized) {
57 return true;
58 }
59
60 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
61 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
62 SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
63 SDL_GL_CONTEXT_PROFILE_CORE);
64 SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
65 SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
66
67 m_GLContext = SDL_GL_CreateContext(m_Window->GetSDLWindow());
68 if (!m_GLContext) {
69 SDL_Log("Failed to create OpenGL context: %s", SDL_GetError());
70 return false;
71 }
72
73 if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) {
74 SDL_Log("Failed to initialize Glad (OpenGL loader)");
75 return false;
76 }
77
78 int width, height;
79 SDL_GetWindowSize(m_Window->GetSDLWindow(), &width, &height);
80 glViewport(0, 0, width, height);
81
82 glEnable(GL_DEPTH_TEST);
83 glEnable(GL_BLEND);
84 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
85
86 // Apply initial VSync setting
87 SDL_GL_SetSwapInterval(m_vsync ? 1 : 0);
88
89 // Query max MSAA samples
90 GLint maxSamples = 1;
91 glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
93 if (maxSamples >= 8) m_maxMsaaSampleCount = 8;
94 else if (maxSamples >= 4) m_maxMsaaSampleCount = 4;
95 else if (maxSamples >= 2) m_maxMsaaSampleCount = 2;
96 SLEAK_INFO("OpenGL max MSAA samples: {}", m_maxMsaaSampleCount);
97
98 SetupVertexLayout();
99
100 m_Initialized = true;
102
103 // Create GBuffer resources (deferred rendering)
104 if (m_deferredEnabled) {
105 CreateGBufferResources();
106 }
107
108 SLEAK_INFO("OpenGL renderer has been initialized successfully!");
109 SLEAK_INFO("OpenGL Version: {}",
110 (const char*)glGetString(GL_VERSION));
111
112 return true;
113}
114
115void OpenGLRenderer::SetupVertexLayout() {
116 glGenVertexArrays(1, &m_VAO);
117}
118
124
125 // Commit staged lightVP before shadow pass so shadow + main agree.
126 if (m_hasPendingLightVP) {
127 memcpy(m_lightVP, m_pendingLightVP, sizeof(m_lightVP));
128 }
129
130 // Shadow pass before main rendering
132 RenderShadowPass();
133 }
134
135 if (m_gbufferCreated && m_deferredEnabled) {
136 // Deferred path: geometry pass writes to GBuffer
137 glBindFramebuffer(GL_FRAMEBUFFER, m_gbufferFBO);
138 GLenum drawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
139 glDrawBuffers(3, drawBuffers);
140 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
141 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
142 // Geometry pass must NOT blend — each pixel writes final GBuffer values
143 glDisable(GL_BLEND);
144 m_inGeometryPass = true;
145 } else {
146 // Forward path: bind MSAA FBO if active, else default FBO
147 if (m_msaaFBO != 0)
148 glBindFramebuffer(GL_FRAMEBUFFER, m_msaaFBO);
149 else
150 glBindFramebuffer(GL_FRAMEBUFFER, 0);
151
152 glClearColor(0.39f, 0.58f, 0.93f, 1.0f);
153 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
154 }
155
156 glBindVertexArray(m_VAO);
157
158 if (bImInitialized) {
159 ImGui_ImplOpenGL3_NewFrame();
160 ImGui_ImplSDL3_NewFrame();
161 ImGui::NewFrame();
162 }
163}
164
166 // Resolve MSAA FBO to default framebuffer before ImGui.
167 // Skip in deferred mode — we rendered directly to FBO 0.
168 if (m_msaaFBO != 0 && !IsDeferredEnabled()) {
169 int width, height;
170 SDL_GetWindowSize(m_Window->GetSDLWindow(), &width, &height);
171 glBindFramebuffer(GL_READ_FRAMEBUFFER, m_msaaFBO);
172 glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
173 glBlitFramebuffer(0, 0, width, height,
174 0, 0, width, height,
175 GL_COLOR_BUFFER_BIT, GL_NEAREST);
176 glBindFramebuffer(GL_FRAMEBUFFER, 0);
177 }
178
179 if (bImInitialized) {
180 ImGui::Render();
181 ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
182 }
183
184 glBindVertexArray(0);
185 SDL_GL_SwapWindow(m_Window->GetSDLWindow());
186
188}
189
191 if (m_Initialized) {
192 CleanupGBufferResources();
193 if (m_shadowTransformUBO) { glDeleteBuffers(1, &m_shadowTransformUBO); m_shadowTransformUBO = 0; }
194 if (m_shadowUBO) { glDeleteBuffers(1, &m_shadowUBO); m_shadowUBO = 0; }
195 if (m_shadowDepthTex) { glDeleteTextures(1, &m_shadowDepthTex); m_shadowDepthTex = 0; }
196 if (m_shadowFBO) { glDeleteFramebuffers(1, &m_shadowFBO); m_shadowFBO = 0; }
197 m_shadowMapCreated = false;
198 m_shadowUBOCreated = false;
199 CleanupMSAAFramebuffer();
200 if (bImInitialized) {
201 ImGui_ImplOpenGL3_Shutdown();
202 ImGui_ImplSDL3_Shutdown();
203 ImGui::DestroyContext();
204 bImInitialized = false;
205 }
206 if (m_VAO != 0) {
207 glDeleteVertexArrays(1, &m_VAO);
208 m_VAO = 0;
209 }
210 if (m_GLContext) {
211 SDL_GL_DestroyContext(m_GLContext);
212 m_GLContext = nullptr;
213 }
214 m_Initialized = false;
215 }
216}
217
218void OpenGLRenderer::Resize(uint32_t width, uint32_t height) {
219 glViewport(0, 0, width, height);
220 // Recreate MSAA FBO at new size
221 if (m_msaaFBO != 0) {
222 CleanupMSAAFramebuffer();
223 CreateMSAAFramebuffer();
224 }
225 // Resize GBuffer textures
226 if (m_gbufferCreated) {
227 RecreateGBufferOnResize(static_cast<int>(width), static_cast<int>(height));
228 }
229}
230
231void OpenGLRenderer::Draw(uint32_t vertexCount) {
232 GLenum mode = m_debugLineMode ? GL_LINES : GL_TRIANGLES;
233 glDrawArrays(mode, 0, vertexCount);
234 DrawnVertices += vertexCount;
235 DrawnTriangles += vertexCount / 3;
236}
237
238void OpenGLRenderer::DrawIndexed(uint32_t indexCount) {
239 GLenum mode = m_debugLineMode ? GL_LINES : GL_TRIANGLES;
240 glDrawElements(mode, indexCount, GL_UNSIGNED_INT, 0);
241 DrawnVertices += indexCount;
242 DrawnTriangles += indexCount / 3;
243}
244
245void OpenGLRenderer::DrawInstance(uint32_t instanceCount,
246 uint32_t vertexPerInstance) {
247 GLenum mode = m_debugLineMode ? GL_LINES : GL_TRIANGLES;
248 glDrawArraysInstanced(mode, 0, vertexPerInstance,
249 instanceCount);
250 DrawnVertices += vertexPerInstance * instanceCount;
251 DrawnTriangles += (vertexPerInstance / 3) * instanceCount;
252}
253
254void OpenGLRenderer::DrawIndexedInstance(uint32_t instanceCount,
255 uint32_t indexPerInstance) {
256 GLenum mode = m_debugLineMode ? GL_LINES : GL_TRIANGLES;
257 glDrawElementsInstanced(mode, indexPerInstance,
258 GL_UNSIGNED_INT, 0, instanceCount);
259 DrawnVertices += indexPerInstance * instanceCount;
260 DrawnTriangles += (indexPerInstance / 3) * instanceCount;
261}
262
264 m_debugLineMode = true;
265}
266
268 m_debugLineMode = false;
269}
270
275
280
281void OpenGLRenderer::SetViewport(float x, float y, float width,
282 float height, float minDepth,
283 float maxDepth) {
284 glViewport(static_cast<GLint>(x), static_cast<GLint>(y),
285 static_cast<GLsizei>(width),
286 static_cast<GLsizei>(height));
287 glDepthRangef(minDepth, maxDepth);
288}
289
290void OpenGLRenderer::ClearRenderTarget(float r, float g, float b,
291 float a) {
292 glClearColor(r, g, b, a);
293 glClear(GL_COLOR_BUFFER_BIT);
294}
295
296void OpenGLRenderer::ClearDepthStencil(bool clearDepth, bool clearStencil,
297 float depth, uint8_t stencil) {
298 GLbitfield mask = 0;
299 if (clearDepth) {
300 glClearDepthf(depth);
301 mask |= GL_DEPTH_BUFFER_BIT;
302 }
303 if (clearStencil) {
304 glClearStencil(stencil);
305 mask |= GL_STENCIL_BUFFER_BIT;
306 }
307 if (mask) glClear(mask);
308}
309
311 glDepthMask(enabled ? GL_TRUE : GL_FALSE);
312}
313
315 GLenum glFunc = GL_LESS;
316 switch (compare) {
317 case DepthCompare::Less: glFunc = GL_LESS; break;
318 case DepthCompare::LessEqual: glFunc = GL_LEQUAL; break;
319 case DepthCompare::Greater: glFunc = GL_GREATER; break;
320 case DepthCompare::GreaterEqual: glFunc = GL_GEQUAL; break;
321 case DepthCompare::Equal: glFunc = GL_EQUAL; break;
322 case DepthCompare::NotEqual: glFunc = GL_NOTEQUAL; break;
323 case DepthCompare::Always: glFunc = GL_ALWAYS; break;
324 case DepthCompare::Never: glFunc = GL_NEVER; break;
325 }
326 glDepthFunc(glFunc);
327}
328
330 if (enabled)
331 glEnable(GL_CULL_FACE);
332 else
333 glDisable(GL_CULL_FACE);
334}
335
337 if (texture.IsValid())
338 texture->Bind(slot);
339}
340
342 uint32_t slot) {
343 auto* glBuf = dynamic_cast<OpenGLBuffer*>(buffer.get());
344 if (!glBuf) return;
345
346 glBindBuffer(GL_ARRAY_BUFFER, glBuf->GetGLBuffer());
347
348 // A registered layout drives the attribute pointers; 0 means default Vertex
349 const uint32_t customFormat = buffer->GetVertexFormat();
350 const VertexLayoutDesc* customDesc =
351 customFormat != 0 ? VertexFormatRegistry::Get(customFormat) : nullptr;
352
353 if (customDesc) {
354 // One attribute pointer per registry entry, tracking which slots stay live
355 const GLsizei stride = static_cast<GLsizei>(customDesc->stride);
356 uint32_t enabledMask = 0;
357 for (const auto& attr : customDesc->attributes) {
358 const void* off = reinterpret_cast<const void*>(
359 static_cast<uintptr_t>(attr.offset));
360 glEnableVertexAttribArray(attr.location);
361 switch (attr.format) {
363 glVertexAttribPointer(attr.location, 1, GL_FLOAT, GL_FALSE, stride, off);
364 break;
366 glVertexAttribPointer(attr.location, 2, GL_FLOAT, GL_FALSE, stride, off);
367 break;
369 glVertexAttribPointer(attr.location, 3, GL_FLOAT, GL_FALSE, stride, off);
370 break;
372 glVertexAttribPointer(attr.location, 4, GL_FLOAT, GL_FALSE, stride, off);
373 break;
375 glVertexAttribIPointer(attr.location, 1, GL_UNSIGNED_INT, stride, off);
376 break;
378 glVertexAttribIPointer(attr.location, 1, GL_INT, stride, off);
379 break;
380 }
381 if (attr.location < 32) enabledMask |= (1u << attr.location);
382 }
383 // Disable attribute slots left enabled by a previous bind
384 constexpr uint32_t TRACKED_ATTRIB_SLOTS = 8;
385 for (uint32_t loc = 0; loc < TRACKED_ATTRIB_SLOTS; ++loc) {
386 if (!(enabledMask & (1u << loc))) glDisableVertexAttribArray(loc);
387 }
388 } else {
389 // Standard 96-byte Vertex layout: 7 attributes
390 // Position: 3 floats at offset 0
391 glEnableVertexAttribArray(0);
392 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
393 (void*)offsetof(Vertex, px));
394
395 // Normal: 3 floats at offset 12
396 glEnableVertexAttribArray(1);
397 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
398 (void*)offsetof(Vertex, nx));
399
400 // Tangent: 4 floats at offset 24
401 glEnableVertexAttribArray(2);
402 glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex),
403 (void*)offsetof(Vertex, tx));
404
405 // Color: 4 floats at offset 40
406 glEnableVertexAttribArray(3);
407 glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex),
408 (void*)offsetof(Vertex, r));
409
410 // UV: 2 floats at offset 56
411 glEnableVertexAttribArray(4);
412 glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
413 (void*)offsetof(Vertex, u));
414
415 // BoneIDs: 4 ints (must use IPointer for integer attributes)
416 glEnableVertexAttribArray(5);
417 glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex),
418 (void*)offsetof(Vertex, boneIDs));
419
420 // BoneWeights: 4 floats
421 glEnableVertexAttribArray(6);
422 glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex),
423 (void*)offsetof(Vertex, boneWeights));
424 }
425}
426
428 uint32_t slot) {
429 auto* glBuf = dynamic_cast<OpenGLBuffer*>(buffer.get());
430 if (!glBuf) return;
431
432 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, glBuf->GetGLBuffer());
433}
434
436 uint32_t slot) {
437 auto* glBuf = dynamic_cast<OpenGLBuffer*>(buffer.get());
438 if (!glBuf) return;
439
440 // Shadow pass: use dedicated shadow CB with LightVP*World for slot 0
441 if (m_inShadowPass && slot == 0 && glBuf->GetCPUShadowCopySize() >= 128) {
442 const float* srcWorld = reinterpret_cast<const float*>(
443 static_cast<const char*>(glBuf->GetCPUShadowCopy()) + 64);
444
445 float shadowPC[32];
446 for (int r = 0; r < 4; ++r) {
447 for (int c = 0; c < 4; ++c) {
448 float sum = 0.0f;
449 for (int k = 0; k < 4; ++k) {
450 sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c];
451 }
452 shadowPC[r * 4 + c] = sum;
453 }
454 }
455 memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16);
456
457 // Create dedicated shadow transform UBO on first use
458 if (m_shadowTransformUBO == 0) {
459 glGenBuffers(1, &m_shadowTransformUBO);
460 glBindBuffer(GL_UNIFORM_BUFFER, m_shadowTransformUBO);
461 glBufferData(GL_UNIFORM_BUFFER, 128, nullptr, GL_DYNAMIC_DRAW);
462 glBindBuffer(GL_UNIFORM_BUFFER, 0);
463 }
464
465 glBindBuffer(GL_UNIFORM_BUFFER, m_shadowTransformUBO);
466 glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(shadowPC), shadowPC);
467 glBindBuffer(GL_UNIFORM_BUFFER, 0);
468 glBindBufferBase(GL_UNIFORM_BUFFER, slot, m_shadowTransformUBO);
469 return;
470 }
471
472 glBindBufferBase(GL_UNIFORM_BUFFER, slot, glBuf->GetGLBuffer());
473}
474
478
480 void* data) {
481 auto* buffer = new OpenGLBuffer(size, type);
482 if (!buffer->Initialize(data)) {
483 delete buffer;
484 return nullptr;
485 }
486 return buffer;
487}
488
489Shader* OpenGLRenderer::CreateShader(const std::string& shaderSource) {
490 auto* shader = new OpenGLShader();
491 if (shader->compile(shaderSource)) {
492 return shader;
493 }
494 delete shader;
495 return nullptr;
496}
497
498Texture* OpenGLRenderer::CreateTexture(const std::string& TexturePath) {
499 auto* texture = new OpenGLTexture();
500 if (texture->LoadFromFile(TexturePath)) {
501 return texture;
502 }
503 delete texture;
504 return nullptr;
505}
506
508 uint32_t height,
509 void* data) {
510 auto* texture = new OpenGLTexture();
511 if (texture->LoadFromMemory(data, width, height,
513 return texture;
514 }
515 delete texture;
516 return nullptr;
517}
518
519void OpenGLRenderer::CreateMSAAFramebuffer() {
520 if (m_msaaSampleCount <= 1)
521 return;
522
523 int width, height;
524 SDL_GetWindowSize(m_Window->GetSDLWindow(), &width, &height);
525
526 glGenFramebuffers(1, &m_msaaFBO);
527 glBindFramebuffer(GL_FRAMEBUFFER, m_msaaFBO);
528
529 // Multisampled color renderbuffer
530 glGenRenderbuffers(1, &m_msaaColorRBO);
531 glBindRenderbuffer(GL_RENDERBUFFER, m_msaaColorRBO);
532 glRenderbufferStorageMultisample(GL_RENDERBUFFER, m_msaaSampleCount,
533 GL_RGBA8, width, height);
534 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
535 GL_RENDERBUFFER, m_msaaColorRBO);
536
537 // Multisampled depth renderbuffer
538 glGenRenderbuffers(1, &m_msaaDepthRBO);
539 glBindRenderbuffer(GL_RENDERBUFFER, m_msaaDepthRBO);
540 glRenderbufferStorageMultisample(GL_RENDERBUFFER, m_msaaSampleCount,
541 GL_DEPTH24_STENCIL8, width, height);
542 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
543 GL_RENDERBUFFER, m_msaaDepthRBO);
544
545 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
546 SLEAK_ERROR("OpenGL MSAA framebuffer is not complete!");
547
548 glBindFramebuffer(GL_FRAMEBUFFER, 0);
549}
550
551void OpenGLRenderer::CleanupMSAAFramebuffer() {
552 if (m_msaaDepthRBO) { glDeleteRenderbuffers(1, &m_msaaDepthRBO); m_msaaDepthRBO = 0; }
553 if (m_msaaColorRBO) { glDeleteRenderbuffers(1, &m_msaaColorRBO); m_msaaColorRBO = 0; }
554 if (m_msaaFBO) { glDeleteFramebuffers(1, &m_msaaFBO); m_msaaFBO = 0; }
555}
556
559 return;
560 m_msaaChangeRequested = false;
562
563 CleanupMSAAFramebuffer();
564 CreateMSAAFramebuffer();
565
566 SLEAK_INFO("OpenGL MSAA changed to {}x", m_msaaSampleCount);
567}
568
571 return;
573 SDL_GL_SetSwapInterval(m_vsync ? 1 : 0);
574 SLEAK_INFO("OpenGL VSync {}", m_vsync ? "enabled" : "disabled");
575}
576
578 switch (Mode) {
579 case RenderMode::Fill:
580 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
581 break;
583 glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
584 break;
586 glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
587 break;
588 default:
589 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
590 break;
591 }
592}
593
595 switch (Face) {
596 case RenderFace::None:
597 glDisable(GL_CULL_FACE);
598 break;
599 case RenderFace::Back:
600 glEnable(GL_CULL_FACE);
601 glCullFace(GL_BACK);
602 break;
604 glEnable(GL_CULL_FACE);
605 glCullFace(GL_FRONT);
606 break;
607 default:
608 glDisable(GL_CULL_FACE);
609 break;
610 }
611}
612
613Texture* OpenGLRenderer::CreateCubemapTexture(const std::array<std::string, 6>& facePaths) {
614 auto* texture = new OpenGLCubemapTexture();
615 if (texture->LoadCubemap(facePaths)) {
616 return texture;
617 }
618 delete texture;
619 return nullptr;
620}
621
623 auto* texture = new OpenGLCubemapTexture();
624 if (texture->LoadEquirectangular(panoramaPath)) {
625 return texture;
626 }
627 delete texture;
628 return nullptr;
629}
630
632 IMGUI_CHECKVERSION();
633 ImGui::CreateContext();
634 ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
635 ImGui::StyleColorsDark();
636
637 if (!ImGui_ImplSDL3_InitForOpenGL(m_Window->GetSDLWindow(),
638 m_GLContext))
639 return false;
640
641 if (!ImGui_ImplOpenGL3_Init("#version 450"))
642 return false;
643
644 bImInitialized = true;
645 return true;
646}
647
648void OpenGLRenderer::SetLightVP(const float* mat) {
649 if (mat) {
650 memcpy(m_pendingLightVP, mat, sizeof(m_pendingLightVP));
651 m_hasPendingLightVP = true;
652 }
653}
654
655bool OpenGLRenderer::CreateShadowUBO() {
656 if (m_shadowUBOCreated) return true;
657 glGenBuffers(1, &m_shadowUBO);
658 glBindBuffer(GL_UNIFORM_BUFFER, m_shadowUBO);
659 glBufferData(GL_UNIFORM_BUFFER, sizeof(RenderEngine::PCSSShadowGPUData), nullptr, GL_DYNAMIC_DRAW);
660 glBindBuffer(GL_UNIFORM_BUFFER, 0);
661 m_shadowUBOCreated = true;
662 return true;
663}
664
665void OpenGLRenderer::UpdateShadowLightUBO(const void* data, uint32_t size) {
666 if (!m_shadowUBOCreated && !CreateShadowUBO()) return;
667 if (!data) return;
668
669 const auto* ubo = static_cast<const RenderEngine::ShadowLightUBO*>(data);
670
671 RenderEngine::PCSSShadowGPUData shadowData{};
672 memcpy(shadowData.LightVP, ubo->LightVP, sizeof(float) * 16);
673 memcpy(shadowData.NdcToShadow, ubo->NdcToShadow, sizeof(float) * 16);
674 shadowData.ShadowBias = ubo->ShadowBias;
675 shadowData.ShadowStrength = ubo->ShadowStrength;
676 shadowData.ShadowTexelSize = ubo->ShadowTexelSize;
677 shadowData.ShadowLightSize = ubo->LightSize;
678 shadowData.PCSSEnabled = m_pcssEnabled ? 1 : 0;
679 shadowData.ShadowMapEnabled = m_shadowPassEnabled ? 1 : 0;
680
681 glBindBuffer(GL_UNIFORM_BUFFER, m_shadowUBO);
682 glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(shadowData), &shadowData);
683 glBindBuffer(GL_UNIFORM_BUFFER, 0);
684
685 // Bind shadow UBO at binding 5 (matches shader)
686 glBindBufferBase(GL_UNIFORM_BUFFER, 5, m_shadowUBO);
687}
688
689bool OpenGLRenderer::CreateShadowMapResources() {
690 if (m_shadowMapCreated) return true;
691
692 // Create depth texture
693 glGenTextures(1, &m_shadowDepthTex);
694 glBindTexture(GL_TEXTURE_2D, m_shadowDepthTex);
695 glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F,
697 GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
698 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
699 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
700 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
701 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
702 float borderColor[] = {1.0f, 1.0f, 1.0f, 1.0f};
703 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
704 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
705 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
706 glBindTexture(GL_TEXTURE_2D, 0);
707
708 // Create FBO
709 glGenFramebuffers(1, &m_shadowFBO);
710 glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFBO);
711 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowDepthTex, 0);
712 glDrawBuffer(GL_NONE);
713 glReadBuffer(GL_NONE);
714 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
715 SLEAK_ERROR("OpenGL shadow FBO is not complete!");
716 glBindFramebuffer(GL_FRAMEBUFFER, 0);
717 return false;
718 }
719 glBindFramebuffer(GL_FRAMEBUFFER, 0);
720
721 m_shadowMapCreated = true;
723 SLEAK_INFO("OpenGL shadow map resources created ({}x{})", m_shadowMapResolution, m_shadowMapResolution);
724 return true;
725}
726
727void OpenGLRenderer::RenderShadowPass() {
728 // Skip if no cached draws — preserve previous frame's shadow map
729 auto* queue = RenderCommandQueue::GetInstance();
730 if (!queue || !queue->HasCachedShadowDraws()) return;
731
732 if (!m_shadowMapCreated) {
733 if (!CreateShadowMapResources()) return;
734 }
735
736 // Save current viewport
737 GLint savedViewport[4];
738 glGetIntegerv(GL_VIEWPORT, savedViewport);
739
740 // Bind shadow FBO
741 glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFBO);
743 glClear(GL_DEPTH_BUFFER_BIT);
744
745 // Enable polygon offset for slope-scale depth bias
746 glEnable(GL_POLYGON_OFFSET_FILL);
747 glPolygonOffset(2.0f, 4.0f);
748
749 // Back-face cull: closed solid volumes always have the light-facing
750 // surface win — same depth result, half the raster.
751 // (FRONT culling stays wrong: it removed the outward-facing surfaces.)
752 glEnable(GL_CULL_FACE);
753 glCullFace(GL_BACK);
754
755 // Ensure VAO is bound for shadow pass draws
756 glBindVertexArray(m_VAO);
757
758 // Execute shadow draw commands
759 m_inShadowPass = true;
760 if (queue) {
761 queue->ExecuteShadowPass(this);
762 }
763 m_inShadowPass = false;
764
765 // Restore state
766 glDisable(GL_POLYGON_OFFSET_FILL);
767 glEnable(GL_CULL_FACE); // re-enable culling for main pass
769
770 // Restore framebuffer
771 if (m_msaaFBO != 0)
772 glBindFramebuffer(GL_FRAMEBUFFER, m_msaaFBO);
773 else
774 glBindFramebuffer(GL_FRAMEBUFFER, 0);
775
776 glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]);
777
778 // Bind shadow map texture at unit 3
779 glActiveTexture(GL_TEXTURE3);
780 glBindTexture(GL_TEXTURE_2D, m_shadowDepthTex);
781 glActiveTexture(GL_TEXTURE0);
782}
783
784// ============================================================
785// Deferred rendering — GBuffer implementation
786// ============================================================
787
789 return m_deferredEnabled && m_gbufferCreated;
790}
791
792bool OpenGLRenderer::CreateGBufferResources() {
793 if (m_gbufferCreated) return true;
794
795 int w, h;
796 SDL_GetWindowSize(m_Window->GetSDLWindow(), &w, &h);
797 m_gbufferWidth = w;
798 m_gbufferHeight = h;
799
800 // ---- GBuffer FBO ----
801 glGenFramebuffers(1, &m_gbufferFBO);
802 glBindFramebuffer(GL_FRAMEBUFFER, m_gbufferFBO);
803
804 auto makeColorTex = [](GLuint& tex, GLenum internalFmt, int w, int h) {
805 glGenTextures(1, &tex);
806 glBindTexture(GL_TEXTURE_2D, tex);
807 GLenum fmt = GL_RGBA;
808 GLenum type = GL_UNSIGNED_BYTE;
809 if (internalFmt == GL_RGBA16F || internalFmt == GL_RGBA32F) {
810 type = GL_FLOAT;
811 }
812 glTexImage2D(GL_TEXTURE_2D, 0, internalFmt, w, h, 0, fmt, type, nullptr);
813 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
814 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
815 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
816 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
817 };
818
819 // RT0: Albedo + AO (RGBA8)
820 makeColorTex(m_gbufferAlbedoAO, GL_RGBA8, w, h);
821 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_gbufferAlbedoAO, 0);
822
823 // RT1: Normal + Roughness (RGBA16F)
824 makeColorTex(m_gbufferNormalRough, GL_RGBA16F, w, h);
825 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, m_gbufferNormalRough, 0);
826
827 // RT2: Metallic + Emissive (RGBA8)
828 makeColorTex(m_gbufferMetalEmit, GL_RGBA8, w, h);
829 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, m_gbufferMetalEmit, 0);
830
831 // World position is reconstructed from depth + InvViewProj in the
832 // lighting/SSAO passes (no RGBA32F worldpos RT) to cut GBuffer bandwidth.
833
834 // Depth texture (read back in lighting pass for world pos reconstruction)
835 glGenTextures(1, &m_gbufferDepth);
836 glBindTexture(GL_TEXTURE_2D, m_gbufferDepth);
837 glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, w, h, 0,
838 GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
839 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
840 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
841 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
842 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
843 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_gbufferDepth, 0);
844
845 GLenum drawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1,
846 GL_COLOR_ATTACHMENT2 };
847 glDrawBuffers(3, drawBuffers);
848
849 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
850 SLEAK_ERROR("OpenGL GBuffer FBO is not complete!");
851 glBindFramebuffer(GL_FRAMEBUFFER, 0);
852 return false;
853 }
854 glBindFramebuffer(GL_FRAMEBUFFER, 0);
855
856 // ---- GBuffer geometry shader ----
857 m_gbufferShader = new OpenGLShader();
858 if (!m_gbufferShader->compile("assets/shaders/gbuffer_gl.vert",
859 "assets/shaders/gbuffer_gl.frag")) {
860 SLEAK_ERROR("Failed to compile GBuffer geometry shader!");
861 delete m_gbufferShader;
862 m_gbufferShader = nullptr;
863 CleanupGBufferResources();
864 return false;
865 }
866
867 // ---- Lighting pass shader ----
868 m_lightingShader = new OpenGLShader();
869 if (!m_lightingShader->compile("assets/shaders/lighting_pass_gl.vert",
870 "assets/shaders/lighting_pass_gl.frag")) {
871 SLEAK_ERROR("Failed to compile deferred lighting pass shader!");
872 delete m_lightingShader;
873 m_lightingShader = nullptr;
874 CleanupGBufferResources();
875 return false;
876 }
877
878 // ---- Empty VAO for fullscreen triangle ----
879 glGenVertexArrays(1, &m_lightingVAO);
880
881 // ---- Deferred CB UBO (binding 6) ----
882 glGenBuffers(1, &m_deferredCBUBO);
883 glBindBuffer(GL_UNIFORM_BUFFER, m_deferredCBUBO);
884 glBufferData(GL_UNIFORM_BUFFER, sizeof(RenderEngine::DeferredCBData), nullptr, GL_DYNAMIC_DRAW);
885 glBindBuffer(GL_UNIFORM_BUFFER, 0);
886 glBindBufferBase(GL_UNIFORM_BUFFER, 6, m_deferredCBUBO);
887 m_deferredCBCreated = true;
888
889 m_gbufferCreated = true;
890 SLEAK_INFO("OpenGL GBuffer created ({}x{}) — deferred rendering active", w, h);
891
892 if (!CreateSSAOResources()) {
893 SLEAK_WARN("OpenGL SSAO resources failed to initialize — SSAO disabled");
894 }
895 return true;
896}
897
898void OpenGLRenderer::CleanupGBufferResources() {
899 CleanupSSAOResources();
900 CleanupIBL();
901 if (m_gbufferAlbedoAO) { glDeleteTextures(1, &m_gbufferAlbedoAO); m_gbufferAlbedoAO = 0; }
902 if (m_gbufferNormalRough) { glDeleteTextures(1, &m_gbufferNormalRough); m_gbufferNormalRough = 0; }
903 if (m_gbufferMetalEmit) { glDeleteTextures(1, &m_gbufferMetalEmit); m_gbufferMetalEmit = 0; }
904 if (m_gbufferDepth) { glDeleteTextures(1, &m_gbufferDepth); m_gbufferDepth = 0; }
905 if (m_gbufferFBO) { glDeleteFramebuffers(1, &m_gbufferFBO); m_gbufferFBO = 0; }
906 if (m_lightingVAO) { glDeleteVertexArrays(1, &m_lightingVAO); m_lightingVAO = 0; }
907 if (m_deferredCBUBO) { glDeleteBuffers(1, &m_deferredCBUBO); m_deferredCBUBO = 0; }
908 delete m_gbufferShader; m_gbufferShader = nullptr;
909 delete m_lightingShader; m_lightingShader = nullptr;
910 m_gbufferCreated = false;
911 m_deferredCBCreated = false;
912}
913
914// IBL — lazy bake from active scene's skybox cubemap.
915// First call (or when the skybox cubemap changes) reruns the precompute.
916void OpenGLRenderer::EnsureIBL() {
917 auto* app = Application::GetInstance();
918 if (!app) return;
919 auto* game = app->GetGame();
920 if (!game) return;
921 auto* scene = game->GetActiveScene();
922 if (!scene) return;
923 auto* skybox = scene->GetSkybox();
924 if (!skybox || !skybox->IsInitialized()) return;
925 auto* cube = skybox->GetCubemapTexture();
926 if (!cube) return;
927 auto* glCube = dynamic_cast<OpenGLCubemapTexture*>(cube);
928 if (!glCube) return;
929
930 GLuint cubeHandle = glCube->GetGLTexture();
931 if (cubeHandle == 0) return;
932
933 if (!m_ibl) m_ibl = new OpenGLIBL();
934 if (cubeHandle != m_iblBoundCubemap) {
935 m_ibl->Cleanup();
936 if (m_ibl->Initialize(cubeHandle)) {
937 m_iblBoundCubemap = cubeHandle;
938 } else {
939 m_iblBoundCubemap = 0;
940 }
941 }
942
943 if (m_iblSettingsUBO == 0) {
944 glGenBuffers(1, &m_iblSettingsUBO);
945 glBindBuffer(GL_UNIFORM_BUFFER, m_iblSettingsUBO);
946 glBufferData(GL_UNIFORM_BUFFER,
947 sizeof(IBLSettingsGPUData),
948 nullptr, GL_DYNAMIC_DRAW);
949 glBindBuffer(GL_UNIFORM_BUFFER, 0);
950 }
951}
952
953void OpenGLRenderer::CleanupIBL() {
954 if (m_ibl) { delete m_ibl; m_ibl = nullptr; }
955 if (m_iblSettingsUBO) {
956 glDeleteBuffers(1, &m_iblSettingsUBO);
957 m_iblSettingsUBO = 0;
958 }
959 m_iblBoundCubemap = 0;
960}
961
962void OpenGLRenderer::RecreateGBufferOnResize(int width, int height) {
963 if (!m_gbufferCreated) return;
964
965 m_gbufferWidth = width;
966 m_gbufferHeight = height;
967
968 // Resize AlbedoAO
969 glBindTexture(GL_TEXTURE_2D, m_gbufferAlbedoAO);
970 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
971
972 // Resize NormalRough
973 glBindTexture(GL_TEXTURE_2D, m_gbufferNormalRough);
974 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
975
976 // Resize MetalEmit
977 glBindTexture(GL_TEXTURE_2D, m_gbufferMetalEmit);
978 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
979
980 // Resize depth
981 glBindTexture(GL_TEXTURE_2D, m_gbufferDepth);
982 glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0,
983 GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
984
985 glBindTexture(GL_TEXTURE_2D, 0);
986
987 RecreateSSAOOnResize(width, height);
988
989 SLEAK_INFO("OpenGL GBuffer resized to {}x{}", width, height);
990}
991
993 if (m_gbufferShader) {
994 // Ensure GBuffer FBO is active with all 3 color attachments
995 // (RT0=AlbedoAO, RT1=NormalRough, RT2=MetalEmit)
996 glBindFramebuffer(GL_FRAMEBUFFER, m_gbufferFBO);
997 GLenum drawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1,
998 GL_COLOR_ATTACHMENT2 };
999 glDrawBuffers(3, drawBuffers);
1000 // Ensure depth writes are enabled for geometry pass
1001 glEnable(GL_DEPTH_TEST);
1002 glDepthMask(GL_TRUE);
1003
1004 // Clear stale shadow sampler from unit 3 — the shadow depth texture
1005 // has GL_TEXTURE_COMPARE_MODE enabled which conflicts with the GBuffer
1006 // shader's sampler2D declaration at binding 3 (roughnessTexture).
1007 glActiveTexture(GL_TEXTURE3);
1008 glBindTexture(GL_TEXTURE_2D, 0);
1009 glActiveTexture(GL_TEXTURE0);
1010
1011 m_gbufferShader->bind();
1012 glBindVertexArray(m_VAO);
1013 } else {
1014 static bool warned = false;
1015 if (!warned) {
1016 SLEAK_ERROR("BindGBufferShader: m_gbufferShader is NULL!");
1017 warned = true;
1018 }
1019 }
1020}
1021
1022void OpenGLRenderer::BindPBRMaterial(Sleak::Material* material) {
1023 // Deferred geometry-pass material bind (restores pre-rewrite OpenGL path):
1024 // bind the GBuffer shader + the material textures/CB. Without this, terrain
1025 // is drawn with the leftover water program (all-blue bug).
1027 if (material) material->BindTexturesAndCB();
1028}
1029
1030void OpenGLRenderer::UpdateDeferredCB(const void* data, uint32_t size) {
1031 if (!m_deferredCBCreated) return;
1032 glBindBuffer(GL_UNIFORM_BUFFER, m_deferredCBUBO);
1033 glBufferSubData(GL_UNIFORM_BUFFER, 0, size, data);
1034 glBindBuffer(GL_UNIFORM_BUFFER, 0);
1035 glBindBufferBase(GL_UNIFORM_BUFFER, 6, m_deferredCBUBO);
1036}
1037
1039 m_inGeometryPass = false;
1040
1041 if (!m_gbufferCreated || !m_lightingShader) return;
1042
1043 // SSAO must run BEFORE the lighting pass so its blurred output is
1044 // available for the lighting shader to multiply against vertex AO.
1045 ExecuteSSAOPass();
1046
1047 // IBL precompute (cheap when already baked) — must happen before the
1048 // lighting shader binds units 14/15/16 and UBO 10.
1049 EnsureIBL();
1050
1051 // Deferred rendering always outputs to the default FBO (framebuffer 0).
1052 // MSAA is incompatible with deferred (multisampled GBuffer is too expensive),
1053 // so we skip the MSAA FBO and render directly to the default framebuffer.
1054 constexpr GLuint targetFBO = 0;
1055 glBindFramebuffer(GL_FRAMEBUFFER, targetFBO);
1056
1057 // Keep the sky color cleared
1058 glClearColor(0.39f, 0.58f, 0.93f, 1.0f);
1059 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
1060
1061 // Enable depth writes so the lighting shader can output gl_FragDepth.
1062 // This replaces the old glBlitFramebuffer approach which silently failed
1063 // on some drivers when GBuffer depth (32F) differs from default FBO depth (24-bit).
1064 glEnable(GL_DEPTH_TEST);
1065 glDepthFunc(GL_ALWAYS); // fullscreen triangle must always pass
1066 glDepthMask(GL_TRUE); // allow depth writes
1067 glDisable(GL_BLEND);
1068
1069 // Bind lighting shader
1070 m_lightingShader->bind();
1071
1072 // Bind GBuffer textures at units 8-11
1073 glActiveTexture(GL_TEXTURE8); glBindTexture(GL_TEXTURE_2D, m_gbufferAlbedoAO);
1074 glActiveTexture(GL_TEXTURE9); glBindTexture(GL_TEXTURE_2D, m_gbufferNormalRough);
1075 glActiveTexture(GL_TEXTURE10); glBindTexture(GL_TEXTURE_2D, m_gbufferMetalEmit);
1076 glActiveTexture(GL_TEXTURE11); glBindTexture(GL_TEXTURE_2D, m_gbufferDepth);
1077 // SSAO result at unit 13 (default to 0 = "fully lit" if SSAO disabled)
1078 glActiveTexture(GL_TEXTURE13);
1079 glBindTexture(GL_TEXTURE_2D, m_ssaoCreated ? m_ssaoBlurTex : 0);
1080 // SSAO enable flag via plain uniform — avoids growing DeferredCBData
1081 GLint ssaoLoc = glGetUniformLocation(
1082 m_lightingShader->GetProgram(), "uSSAOEnabled");
1083 if (ssaoLoc >= 0) {
1084 glUniform1i(ssaoLoc, (m_ssaoEnabled && m_ssaoCreated) ? 1 : 0);
1085 }
1086 glActiveTexture(GL_TEXTURE0);
1087
1088 // Rebind shadow map at unit 3 — BindGBufferShader cleared it to avoid
1089 // sampler2DShadow/sampler2D conflict during the geometry pass.
1090 if (m_shadowMapCreated) {
1091 glActiveTexture(GL_TEXTURE3);
1092 glBindTexture(GL_TEXTURE_2D, m_shadowDepthTex);
1093 glActiveTexture(GL_TEXTURE0);
1094 }
1095
1096 // IBL — bind irradiance/prefilter/BRDF at units 14/15/16 and upload the
1097 // settings UBO at slot 10. Disabled if precompute hasn't completed.
1098 bool iblReady = (m_ibl && m_ibl->IsInitialized());
1099 if (iblReady) m_ibl->Bind();
1100 if (m_iblSettingsUBO) {
1101 IBLSettingsGPUData ibl{};
1102 ibl.IBLEnabled = (iblReady && m_iblEnabled) ? 1u : 0u;
1103 ibl.IBLIntensity = m_iblIntensity;
1104 ibl.MaxReflectionLOD = static_cast<float>(OpenGLIBL::PREFILTER_MIPS - 1);
1105 ibl._iblPad0 = 0;
1106 glBindBuffer(GL_UNIFORM_BUFFER, m_iblSettingsUBO);
1107 glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(ibl), &ibl);
1108 glBindBuffer(GL_UNIFORM_BUFFER, 0);
1109 glBindBufferBase(GL_UNIFORM_BUFFER, 10, m_iblSettingsUBO);
1110 }
1111
1112 // Draw fullscreen triangle — shader writes color + gl_FragDepth
1113 glBindVertexArray(m_lightingVAO);
1114 glDrawArrays(GL_TRIANGLES, 0, 3);
1115
1116 // Restore depth state for forward pass
1117 glDepthFunc(GL_LESS);
1118 glEnable(GL_BLEND);
1119 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1120
1121 // Rebind main VAO for forward pass
1122 glBindVertexArray(m_VAO);
1123}
1124
1126 m_inForwardTransparentPass = true;
1127 // Transparent objects need alpha blending
1128 glEnable(GL_BLEND);
1129 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1130}
1131
1133 m_inForwardTransparentPass = false;
1134}
1135
1136// ============================================================
1137// SSAO pass — runs between GBuffer write and deferred lighting
1138// Output: R8 half-or-full-res AO in m_ssaoBlurTex, sampled by
1139// lighting_pass_gl.frag at texture unit 13 and multiplied into ao.
1140// ============================================================
1141
1142bool OpenGLRenderer::CreateSSAOResources() {
1143 if (m_ssaoCreated) return true;
1144 if (!m_gbufferCreated) return false;
1145
1146 int w = m_gbufferWidth;
1147 int h = m_gbufferHeight;
1148
1149 auto makeR8Tex = [](GLuint& tex, int w, int h) {
1150 glGenTextures(1, &tex);
1151 glBindTexture(GL_TEXTURE_2D, tex);
1152 glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
1153 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
1154 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
1155 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1156 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1157 };
1158
1159 // ---- Raw SSAO FBO ----
1160 makeR8Tex(m_ssaoTexture, w, h);
1161 glGenFramebuffers(1, &m_ssaoFBO);
1162 glBindFramebuffer(GL_FRAMEBUFFER, m_ssaoFBO);
1163 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
1164 GL_TEXTURE_2D, m_ssaoTexture, 0);
1165 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
1166 SLEAK_ERROR("OpenGL SSAO FBO incomplete");
1167 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1168 CleanupSSAOResources();
1169 return false;
1170 }
1171
1172 // ---- Blur FBO ----
1173 makeR8Tex(m_ssaoBlurTex, w, h);
1174 glGenFramebuffers(1, &m_ssaoBlurFBO);
1175 glBindFramebuffer(GL_FRAMEBUFFER, m_ssaoBlurFBO);
1176 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
1177 GL_TEXTURE_2D, m_ssaoBlurTex, 0);
1178 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
1179 SLEAK_ERROR("OpenGL SSAO blur FBO incomplete");
1180 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1181 CleanupSSAOResources();
1182 return false;
1183 }
1184 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1185
1186 // ---- 4x4 noise texture ----
1187 SSAOKernel kernelGen;
1188 kernelGen.Generate();
1189
1190 glGenTextures(1, &m_ssaoNoiseTex);
1191 glBindTexture(GL_TEXTURE_2D, m_ssaoNoiseTex);
1192 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F,
1194 GL_RGBA, GL_FLOAT, kernelGen.noiseData.data());
1195 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1196 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
1197 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
1198 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
1199
1200 // ---- Shaders ----
1201 m_ssaoShader = new OpenGLShader();
1202 if (!m_ssaoShader->compile("assets/shaders/ssao_gl.vert",
1203 "assets/shaders/ssao_gl.frag")) {
1204 SLEAK_ERROR("Failed to compile SSAO shader");
1205 CleanupSSAOResources();
1206 return false;
1207 }
1208 m_ssaoBlurShader = new OpenGLShader();
1209 if (!m_ssaoBlurShader->compile("assets/shaders/ssao_blur_gl.vert",
1210 "assets/shaders/ssao_blur_gl.frag")) {
1211 SLEAK_ERROR("Failed to compile SSAO blur shader");
1212 CleanupSSAOResources();
1213 return false;
1214 }
1215
1216 // ---- UBOs ----
1217 // Settings (binding 0) — 64 bytes
1218 glGenBuffers(1, &m_ssaoSettingsUBO);
1219 glBindBuffer(GL_UNIFORM_BUFFER, m_ssaoSettingsUBO);
1220 glBufferData(GL_UNIFORM_BUFFER, sizeof(SSAOSettingsGPUData), nullptr, GL_DYNAMIC_DRAW);
1221
1222 // Kernel (binding 1) — 1024 bytes, upload once (deterministic seed)
1223 glGenBuffers(1, &m_ssaoKernelUBO);
1224 glBindBuffer(GL_UNIFORM_BUFFER, m_ssaoKernelUBO);
1225 glBufferData(GL_UNIFORM_BUFFER,
1226 sizeof(SSAOKernel::Sample) * SSAOKernel::MAX_KERNEL_SIZE,
1227 kernelGen.samples.data(), GL_STATIC_DRAW);
1228
1229 // Camera (binding 2) — Projection + View + InvViewProj = 3 * 64 = 192 bytes
1230 glGenBuffers(1, &m_ssaoCameraUBO);
1231 glBindBuffer(GL_UNIFORM_BUFFER, m_ssaoCameraUBO);
1232 glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * 16 * 3, nullptr, GL_DYNAMIC_DRAW);
1233
1234 glBindBuffer(GL_UNIFORM_BUFFER, 0);
1235
1236 m_ssaoCreated = true;
1237 SLEAK_INFO("OpenGL SSAO ready ({}x{})", w, h);
1238 return true;
1239}
1240
1241void OpenGLRenderer::CleanupSSAOResources() {
1242 if (m_ssaoFBO) { glDeleteFramebuffers(1, &m_ssaoFBO); m_ssaoFBO = 0; }
1243 if (m_ssaoBlurFBO) { glDeleteFramebuffers(1, &m_ssaoBlurFBO); m_ssaoBlurFBO = 0; }
1244 if (m_ssaoTexture) { glDeleteTextures(1, &m_ssaoTexture); m_ssaoTexture = 0; }
1245 if (m_ssaoBlurTex) { glDeleteTextures(1, &m_ssaoBlurTex); m_ssaoBlurTex = 0; }
1246 if (m_ssaoNoiseTex) { glDeleteTextures(1, &m_ssaoNoiseTex); m_ssaoNoiseTex = 0; }
1247 if (m_ssaoSettingsUBO) { glDeleteBuffers(1, &m_ssaoSettingsUBO); m_ssaoSettingsUBO = 0; }
1248 if (m_ssaoKernelUBO) { glDeleteBuffers(1, &m_ssaoKernelUBO); m_ssaoKernelUBO = 0; }
1249 if (m_ssaoCameraUBO) { glDeleteBuffers(1, &m_ssaoCameraUBO); m_ssaoCameraUBO = 0; }
1250 delete m_ssaoShader; m_ssaoShader = nullptr;
1251 delete m_ssaoBlurShader; m_ssaoBlurShader = nullptr;
1252 m_ssaoCreated = false;
1253}
1254
1255void OpenGLRenderer::RecreateSSAOOnResize(int width, int height) {
1256 if (!m_ssaoCreated) return;
1257
1258 glBindTexture(GL_TEXTURE_2D, m_ssaoTexture);
1259 glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
1260
1261 glBindTexture(GL_TEXTURE_2D, m_ssaoBlurTex);
1262 glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
1263
1264 glBindTexture(GL_TEXTURE_2D, 0);
1265}
1266
1267void OpenGLRenderer::ExecuteSSAOPass() {
1268 if (!m_ssaoCreated || !m_ssaoShader || !m_ssaoBlurShader) return;
1269
1270 // Fast-path: if SSAO is disabled, clear the blur target to white so the
1271 // lighting shader's ao * ssao multiply is a no-op.
1272 if (!m_ssaoEnabled) {
1273 glBindFramebuffer(GL_FRAMEBUFFER, m_ssaoBlurFBO);
1274 glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
1275 glClear(GL_COLOR_BUFFER_BIT);
1276 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1277 return;
1278 }
1279
1280 // ---- Upload per-frame settings UBO ----
1281 SSAOSettingsGPUData s{};
1282 s.Radius = m_ssaoRadius;
1283 s.Bias = m_ssaoBias;
1284 s.Power = m_ssaoPower;
1285 s.KernelSize = SSAOKernel::MAX_KERNEL_SIZE;
1286 s.ScreenWidth = static_cast<float>(m_gbufferWidth);
1287 s.ScreenHeight = static_cast<float>(m_gbufferHeight);
1288 s.SSAOEnabled = 1;
1289 s.NearPlane = 0.1f;
1290 s.FarPlane = 2000.0f;
1291
1292 glBindBuffer(GL_UNIFORM_BUFFER, m_ssaoSettingsUBO);
1293 glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(s), &s);
1294
1295 // ---- Upload Camera UBO (Projection, View, InvViewProj) ----
1298 float camData[16 * 3];
1299 std::memcpy(camData, &P(0,0), sizeof(float) * 16);
1300 std::memcpy(camData + 16, &V(0,0), sizeof(float) * 16);
1301 // InvViewProj = inverse(View * Proj) — reconstructs world pos from depth.
1302 // Row-vector engine convention: clip = world * (View * Proj).
1303 Math::Matrix4 VP = V * P;
1304 float invP[16];
1305 {
1306 // Inline determinant-based 4x4 inverse (Cramer). Returns identity if singular.
1307 const float* m = &VP(0,0);
1308 float i0 = m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10];
1309 float i4 = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10];
1310 float i8 = m[4]*m[9] *m[15] - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9];
1311 float i12 = -m[4]*m[9] *m[14] + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9];
1312 float det = m[0]*i0 + m[1]*i4 + m[2]*i8 + m[3]*i12;
1313 if (std::fabs(det) < 1e-8f) {
1314 std::memset(invP, 0, sizeof(invP));
1315 invP[0] = invP[5] = invP[10] = invP[15] = 1.0f;
1316 } else {
1317 float id = 1.0f / det;
1318 invP[0] = i0 * id; invP[4] = i4 * id; invP[8] = i8 * id; invP[12] = i12 * id;
1319 invP[1] = (-m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10]) * id;
1320 invP[5] = ( m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10]) * id;
1321 invP[9] = (-m[0]*m[9] *m[15] + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9] ) * id;
1322 invP[13] = ( m[0]*m[9] *m[14] - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9] ) * id;
1323 invP[2] = ( m[1]*m[6]*m[15] - m[1]*m[7]*m[14] - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7] - m[13]*m[3]*m[6]) * id;
1324 invP[6] = (-m[0]*m[6]*m[15] + m[0]*m[7]*m[14] + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7] + m[12]*m[3]*m[6]) * id;
1325 invP[10] = ( m[0]*m[5]*m[15] - m[0]*m[7]*m[13] - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7] - m[12]*m[3]*m[5]) * id;
1326 invP[14] = (-m[0]*m[5]*m[14] + m[0]*m[6]*m[13] + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6] + m[12]*m[2]*m[5]) * id;
1327 invP[3] = (-m[1]*m[6]*m[11] + m[1]*m[7]*m[10] + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7] + m[9]*m[3]*m[6]) * id;
1328 invP[7] = ( m[0]*m[6]*m[11] - m[0]*m[7]*m[10] - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7] - m[8]*m[3]*m[6]) * id;
1329 invP[11] = (-m[0]*m[5]*m[11] + m[0]*m[7]*m[9] + m[4]*m[1]*m[11] - m[4]*m[3]*m[9] - m[8]*m[1]*m[7] + m[8]*m[3]*m[5]) * id;
1330 invP[15] = ( m[0]*m[5]*m[10] - m[0]*m[6]*m[9] - m[4]*m[1]*m[10] + m[4]*m[2]*m[9] + m[8]*m[1]*m[6] - m[8]*m[2]*m[5]) * id;
1331 }
1332 }
1333 std::memcpy(camData + 32, invP, sizeof(float) * 16);
1334
1335 glBindBuffer(GL_UNIFORM_BUFFER, m_ssaoCameraUBO);
1336 glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(camData), camData);
1337 glBindBuffer(GL_UNIFORM_BUFFER, 0);
1338
1339 // Bind UBOs at private slots 7/8/9 — slots 0-6 are used by the lighting
1340 // pass (LightUBO=2, ShadowUBO=5, DeferredCB=6, material/transform=0,1,3).
1341 // Binding SSAO's UBOs to those would silently nuke the lighting pass.
1342 glBindBufferBase(GL_UNIFORM_BUFFER, 7, m_ssaoSettingsUBO);
1343 glBindBufferBase(GL_UNIFORM_BUFFER, 8, m_ssaoKernelUBO);
1344 glBindBufferBase(GL_UNIFORM_BUFFER, 9, m_ssaoCameraUBO);
1345
1346 // ---- Pass 1: SSAO ----
1347 glBindFramebuffer(GL_FRAMEBUFFER, m_ssaoFBO);
1348 glViewport(0, 0, m_gbufferWidth, m_gbufferHeight);
1349 glDisable(GL_DEPTH_TEST);
1350 glDepthMask(GL_FALSE);
1351 glDisable(GL_BLEND);
1352 glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
1353 glClear(GL_COLOR_BUFFER_BIT);
1354
1355 m_ssaoShader->bind();
1356 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_gbufferDepth);
1357 glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, m_gbufferNormalRough);
1358 glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, m_ssaoNoiseTex);
1359 glActiveTexture(GL_TEXTURE0);
1360
1361 glBindVertexArray(m_lightingVAO);
1362 glDrawArrays(GL_TRIANGLES, 0, 3);
1363
1364 // ---- Pass 2: Blur ----
1365 glBindFramebuffer(GL_FRAMEBUFFER, m_ssaoBlurFBO);
1366 glClear(GL_COLOR_BUFFER_BIT);
1367
1368 m_ssaoBlurShader->bind();
1369 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_ssaoTexture);
1370
1371 glDrawArrays(GL_TRIANGLES, 0, 3);
1372
1373 // Restore depth/blend state for the upcoming lighting pass
1374 glEnable(GL_DEPTH_TEST);
1375 glDepthMask(GL_TRUE);
1376
1377 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1378}
1379
1380} // namespace RenderEngine
1381} // namespace Sleak
int width
int height
#define SLEAK_ERROR(...)
Definition Logger.hpp:22
#define SLEAK_INFO(...)
Definition Logger.hpp:20
#define SLEAK_WARN(...)
Definition Logger.hpp:21
static Application * GetInstance()
The one Application for this process, or null before construction.
static const Math::Matrix4 & GetMainProjectionMatrix()
Definition Camera.hpp:107
static const Math::Matrix4 & GetMainViewMatrix()
Definition Camera.hpp:103
void BindTexturesAndCB()
Binds textures and uploads the material CB for the deferred geometry pass, skipping the shader bind.
Definition Material.cpp:108
T * get() const
Definition RefPtr.hpp:170
Backend-agnostic GPU buffer: vertex, index, constant, or resource view target.
OpenGL VBO/EBO/UBO wrapper; target is derived from BufferType at construction.
OpenGL cubemap texture, loadable from six face images, a panorama, or a procedural gradient.
static constexpr uint32_t PREFILTER_MIPS
Definition OpenGLIBL.hpp:25
virtual void SetDepthWrite(bool enabled) override
virtual Texture * CreateTexture(const std::string &TexturePath) override
Loads a texture from disk.
virtual void SetRenderFace(RenderFace face) override
virtual void DrawInstance(uint32_t instanceCount, uint32_t vertexPerInstance) override
Non-indexed instanced draw.
virtual void BindIndexBuffer(RefPtr< BufferBase > buffer, uint32_t slot=0) override
virtual void SetCullEnabled(bool enabled) override
virtual Shader * CreateShader(const std::string &shaderSource) override
Compiles a shader program from source or a source-file path.
virtual Texture * CreateTextureFromData(uint32_t width, uint32_t height, void *data) override
Creates a texture from an in-memory pixel buffer.
virtual void SetViewport(float x, float y, float width, float height, float minDepth=0.0f, float maxDepth=1.0f) override
Sets the active viewport rect and depth range in pixel/NDC units.
OpenGLRenderer(Window *window)
Registers this backend's buffer/shader/texture factories with ResourceManager.
virtual BufferBase * CreateBuffer(BufferType Type, uint32_t size, void *data) override
Allocates a backend buffer, optionally seeded with initial data.
virtual void BeginDebugLinePass() override
void BeginRender() override
Acquires the next frame and prepares it for command recording.
virtual void DrawIndexedInstance(uint32_t instanceCount, uint32_t indexPerInstance) override
Indexed instanced draw.
void EndRender() override
Submits recorded commands and presents the frame.
virtual void BindTexture(RefPtr< Sleak::Texture > texture, uint32_t slot=0) override
virtual void Resize(uint32_t width, uint32_t height) override
Recreates swapchain-dependent resources for a new surface size.
virtual void BindConstantBuffer(RefPtr< BufferBase > buffer, uint32_t slot=0) override
virtual void ClearDepthStencil(bool clearDepth, bool clearStencil, float depth, uint8_t stencil) override
virtual bool CreateImGUI() override
Initializes ImGui integration for this backend.
virtual void DrawIndexed(uint32_t indexCount) override
Indexed draw from the currently bound vertex/index buffers.
virtual void ClearRenderTarget(float r, float g, float b, float a) override
virtual void SetDepthCompare(DepthCompare compare) override
bool Initialize() override
Creates the device, swapchain, and backend resources.
Texture * CreateCubemapTextureFromPanorama(const std::string &panoramaPath)
Loads a cubemap by converting a single equirectangular panorama.
virtual void SetRenderMode(RenderMode mode) override
virtual void BindVertexBuffer(RefPtr< BufferBase > buffer, uint32_t slot=0) override
virtual void BindBoneBuffer(RefPtr< BufferBase > buffer) override
virtual void EndDebugLinePass() override
virtual void Draw(uint32_t vertexCount) override
Non-indexed draw from the currently bound vertex buffer.
Texture * CreateCubemapTexture(const std::array< std::string, 6 > &facePaths)
Loads a cubemap from six face image paths.
GLSL shader program compiled from either a combined source file or separate vertex/fragment files.
OpenGL 2D texture, loadable from disk or an in-memory pixel buffer.
static RenderCommandQueue * GetInstance()
Lazily creates and returns the process-wide singleton instance.
virtual void ConfigureRenderMode()=0
Applies the current RenderMode (fill/wireframe/points) to backend state.
virtual void ConfigureRenderFace()=0
Applies the current RenderFace (cull mode) to backend state.
void UpdateFrameMetrics()
Rolls up frame count into rate/time/triangle stats once per MetricUpdateInterval.
Definition Renderer.hpp:241
void SetPerformanceCounter(bool value)
Definition Renderer.hpp:127
static void RegisterCreateCubemapTextureFromPanorama(T *instance, Texture *(T::*method)(const std::string &))
static void RegisterCreateCubemapTexture(T *instance, Texture *(T::*method)(const std::array< std::string, 6 > &))
static void RegisterCreateBuffer(T *instance, BufferBase *(T::*method)(BufferType, uint32_t, void *))
static void RegisterCreateShader(T *instance, Shader *(T::*method)(const std::string &))
static void RegisterCreateTextureFromMemory(std::function< Texture *(const void *, uint32_t, uint32_t, TextureFormat, uint32_t)> func)
static void RegisterCreateTexture(T *instance, Texture *(T::*method)(const std::string &))
Backend-agnostic compiled shader program.
Definition Shader.hpp:11
virtual bool IsValid() const
True if the pointer is non-null.
static const VertexLayoutDesc * Get(VertexFormatHandle handle)
SDL_Window * GetSDLWindow()
Definition Window.hpp:36
TextureFormat
Definition Texture.hpp:10
Matrix< float, 4, 4 > Matrix4
Definition Matrix.hpp:413
Backend-facing rendering layer shared by the four graphics backends.
DepthCompare
Depth test comparison function.
RenderMode
Rasterizer fill style for a draw.
BufferType
GPU buffer usage kind, drives backend binding flags and layout.
RenderFace
Which triangle winding gets culled.
Root namespace for everything the engine exposes.
Definition Camera.hpp:10
static constexpr uint32_t MAX_KERNEL_SIZE
static constexpr uint32_t NOISE_SIZE
std::vector< VertexAttribute > attributes