From 792f5bec020df876ff244f9d68ab172befba3b52 Mon Sep 17 00:00:00 2001 From: Heesup Yun Date: Tue, 4 Aug 2026 23:02:57 -0700 Subject: [PATCH] fix(visualizer): create shadow-map FBO in headless mode\n\nPreviously shadow-map framebuffer and depth texture were only created\nin windowed mode, causing shadows to be missing in offscreen renders.\nNow the same FBO setup is performed for headless mode as well. --- plugins/visualizer/src/VisualizerCore.cpp | 154 ++++++++++++---------- 1 file changed, 82 insertions(+), 72 deletions(-) diff --git a/plugins/visualizer/src/VisualizerCore.cpp b/plugins/visualizer/src/VisualizerCore.cpp index b3e32e307..ba8be918d 100644 --- a/plugins/visualizer/src/VisualizerCore.cpp +++ b/plugins/visualizer/src/VisualizerCore.cpp @@ -1235,78 +1235,85 @@ void Visualizer::initialize(uint window_width_pixels, uint window_height_pixels, primaryShader.useShader(); - // Initialize frame buffer only for windowed mode - if (!headless) { - // The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer. - glGenFramebuffers(1, &framebufferID); - glBindFramebuffer(GL_FRAMEBUFFER, framebufferID); + // Initialize shadow-map framebuffer (needed for both windowed and headless modes) + glGenFramebuffers(1, &framebufferID); + glBindFramebuffer(GL_FRAMEBUFFER, framebufferID); - // Depth texture. Slower than a depth buffer, but you can sample it later in your shader - glActiveTexture(GL_TEXTURE1); - glGenTextures(1, &depthTexture); - glBindTexture(GL_TEXTURE_2D, depthTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, shadow_buffer_size.x, shadow_buffer_size.y, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); + // Depth texture. Slower than a depth buffer, but you can sample it later in your shader + glActiveTexture(GL_TEXTURE1); + glGenTextures(1, &depthTexture); + glBindTexture(GL_TEXTURE_2D, depthTexture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, shadow_buffer_size.x, shadow_buffer_size.y, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - 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_MIN_FILTER, GL_NEAREST); - // clamp to border so any lookup outside [0,1] returns 1.0 (no shadow) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); - GLfloat borderColor[4] = {1.0f, 1.0f, 1.0f, 1.0f}; - glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); + // clamp to border so any lookup outside [0,1] returns 1.0 (no shadow) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + GLfloat borderColor[4] = {1.0f, 1.0f, 1.0f, 1.0f}; + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); - // enable hardware depth comparison - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE); + // enable hardware depth comparison + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE); - if (!checkerrors()) { - helios_runtime_error("ERROR (Visualizer::initialize): OpenGL setup failed during texture configuration. " - "This may indicate graphics driver issues or insufficient OpenGL support."); - } - - // restore default active texture for subsequent texture setup - glActiveTexture(GL_TEXTURE0); - - glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0); - - glDrawBuffer(GL_NONE); // No color buffer is drawn to. - - // Always check that our framebuffer is ok - int max_checks = 10000; - int checks = 0; - while (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE && checks < max_checks) { - checks++; - } - // Check framebuffer completeness instead of using assert - GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) { - std::string error_message = "ERROR (Visualizer::initialize): Framebuffer is incomplete. Status: "; - switch (framebuffer_status) { - case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - error_message += "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT - Framebuffer attachment is incomplete"; - break; - case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - error_message += "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT - No attachments"; - break; - case GL_FRAMEBUFFER_UNSUPPORTED: - error_message += "GL_FRAMEBUFFER_UNSUPPORTED - Unsupported framebuffer format"; - break; - default: - error_message += "Unknown framebuffer error code: " + std::to_string(framebuffer_status); - break; - } + if (!checkerrors()) { + helios_runtime_error("ERROR (Visualizer::initialize): OpenGL setup failed during texture configuration. " + "This may indicate graphics driver issues or insufficient OpenGL support."); + } + + // restore default active texture for subsequent texture setup + glActiveTexture(GL_TEXTURE0); + + glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0); + + glDrawBuffer(GL_NONE); // No color buffer is drawn to. + + // Always check that our framebuffer is ok + int max_checks = 10000; + int checks = 0; + while (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE && checks < max_checks) { + checks++; + } + // Check framebuffer completeness instead of using assert + GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) { + std::string error_message = "ERROR (Visualizer::initialize): "; + if (headless) { + error_message += "Headless shadow-map framebuffer"; + } else { + error_message += "Framebuffer"; + } + error_message += " is incomplete. Status: "; + switch (framebuffer_status) { + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + error_message += "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT - Framebuffer attachment is incomplete"; + break; + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + error_message += "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT - No attachments"; + break; + case GL_FRAMEBUFFER_UNSUPPORTED: + error_message += "GL_FRAMEBUFFER_UNSUPPORTED - Unsupported framebuffer format"; + break; + default: + error_message += "Unknown framebuffer error code: " + std::to_string(framebuffer_status); + break; + } + if (!headless) { error_message += ". This typically occurs in CI environments with limited graphics support or missing GPU drivers."; - helios_runtime_error(error_message); } + helios_runtime_error(error_message); + } - // Finished OpenGL setup - // Check for OpenGL errors after framebuffer setup - if (!checkerrors()) { - helios_runtime_error("ERROR (Visualizer::initialize): Framebuffer setup failed. " - "This indicates issues with OpenGL framebuffer operations, often related to graphics driver limitations or insufficient resources."); - } - } else { - // Set framebuffer dimensions for headless mode (no framebuffer created) + // Finished OpenGL setup + // Check for OpenGL errors after framebuffer setup + if (!checkerrors()) { + helios_runtime_error("ERROR (Visualizer::initialize): Framebuffer setup failed. " + "This indicates issues with OpenGL framebuffer operations, often related to graphics driver limitations or insufficient resources."); + } + + if (headless) { + // Set framebuffer dimensions for headless mode Wframebuffer = Wdisplay; Hframebuffer = Hdisplay; } @@ -1410,16 +1417,19 @@ void Visualizer::initialize(uint window_width_pixels, uint window_height_pixels, } Visualizer::~Visualizer() { - // Clean up resources for both headless and windowed modes + // Clean up shadow-map FBO (needed for both headless and windowed modes) + if (framebufferID != 0) { + glDeleteFramebuffers(1, &framebufferID); + framebufferID = 0; + } + if (depthTexture != 0) { + glDeleteTextures(1, &depthTexture); + depthTexture = 0; + } + + // Clean up offscreen resources for headless mode if (headless) { cleanupOffscreenFramebuffer(); - } else { - if (framebufferID != 0) { - glDeleteFramebuffers(1, &framebufferID); - } - if (depthTexture != 0) { - glDeleteTextures(1, &depthTexture); - } } // Clean up common OpenGL resources regardless of mode