1.3.64
 
Loading...
Searching...
No Matches
VisualizerCore.cpp
Go to the documentation of this file.
1
16// OpenGL Includes
17#include <GL/glew.h>
18#include <GLFW/glfw3.h>
19
20// Freetype Libraries (rendering fonts)
21extern "C" {
22#include <ft2build.h>
23#include FT_FREETYPE_H
24}
25
26#include "Visualizer.h"
27
28using namespace helios;
29
30// Reference counter for GLFW initialization
31// GLFW should be initialized once and kept alive for the entire application lifetime
32// This avoids macOS-specific issues with reinitializing GLFW after termination
33static int glfw_reference_count = 0;
34
35int read_JPEG_file(const char *filename, std::vector<unsigned char> &texture, uint &height, uint &width) {
36 std::vector<helios::RGBcolor> rgb_data;
37 helios::readJPEG(filename, width, height, rgb_data);
38
39 texture.clear();
40 texture.reserve(width * height * 4);
41
42 for (const auto &pixel: rgb_data) {
43 texture.push_back(static_cast<unsigned char>(pixel.r * 255.0f));
44 texture.push_back(static_cast<unsigned char>(pixel.g * 255.0f));
45 texture.push_back(static_cast<unsigned char>(pixel.b * 255.0f));
46 texture.push_back(255); // alpha channel - opaque
47 }
48
49 return 0;
50}
51
52int write_JPEG_file(const char *filename, uint width, uint height, bool buffers_swapped_since_render, bool print_messages) {
53 if (print_messages) {
54 std::cout << "writing JPEG image: " << filename << std::endl;
55 }
56
57 // Validate framebuffer completeness
58 GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
59 if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) {
60 helios_runtime_error("ERROR (write_JPEG_file): Framebuffer is not complete (status: " + std::to_string(framebuffer_status) + ")");
61 }
62
63 // Clear any existing OpenGL errors
64 while (glGetError() != GL_NO_ERROR) {
65 }
66
67 const size_t bsize = 3 * width * height;
68 std::vector<GLubyte> screen_shot_trans;
69 screen_shot_trans.resize(bsize);
70
71 // Set proper pixel alignment for reliable reading
72 glPixelStorei(GL_PACK_ALIGNMENT, 1);
73
74 // Deterministic buffer selection based on swap state tracking
75 // If buffers have been swapped since last render, newest content is in GL_FRONT
76 // If buffers have NOT been swapped since last render, newest content is in GL_BACK (current render target)
77 GLenum error;
78 if (buffers_swapped_since_render) {
79 glReadBuffer(GL_FRONT);
80 error = glGetError();
81 if (error != GL_NO_ERROR) {
82 // Fallback to back buffer if front buffer fails
83 glReadBuffer(GL_BACK);
84 error = glGetError();
85 if (error != GL_NO_ERROR) {
86 helios_runtime_error("ERROR (write_JPEG_file): Cannot set read buffer (error: " + std::to_string(error) + ")");
87 }
88 }
89 } else {
90 glReadBuffer(GL_BACK);
91 error = glGetError();
92 if (error != GL_NO_ERROR) {
93 // Fallback to front buffer if back buffer fails
94 glReadBuffer(GL_FRONT);
95 error = glGetError();
96 if (error != GL_NO_ERROR) {
97 helios_runtime_error("ERROR (write_JPEG_file): Cannot set read buffer (error: " + std::to_string(error) + ")");
98 }
99 }
100 }
101
102 // Ensure all rendering commands complete before reading
103 glFinish();
104
105 // Read pixels with error checking
106 glReadPixels(0, 0, scast<GLsizei>(width), scast<GLsizei>(height), GL_RGB, GL_UNSIGNED_BYTE, &screen_shot_trans[0]);
107 error = glGetError();
108 if (error != GL_NO_ERROR) {
109 helios_runtime_error("ERROR (write_JPEG_file): glReadPixels failed (error: " + std::to_string(error) + ")");
110 }
111
112 // Check if we got all black pixels (common failure mode)
113 bool all_black = true;
114 for (size_t i = 0; i < bsize && all_black; i++) {
115 if (screen_shot_trans[i] != 0) {
116 all_black = false;
117 }
118 }
119
120 if (all_black) {
121 std::cout << "WARNING (write_JPEG_file): All pixels are black - this may indicate a timing or buffer issue" << std::endl;
122 }
123
124 // Convert to RGBcolor vector and use Context's writeJPEG
125 std::vector<helios::RGBcolor> rgb_data;
126 rgb_data.reserve(width * height);
127
128 for (size_t i = 0; i < width * height; i++) {
129 size_t byte_idx = i * 3;
130 rgb_data.emplace_back(screen_shot_trans[byte_idx] / 255.0f, screen_shot_trans[byte_idx + 1] / 255.0f, screen_shot_trans[byte_idx + 2] / 255.0f);
131 }
132
133 helios::writeJPEG(filename, width, height, rgb_data);
134 return 1;
135}
136
137int write_JPEG_file(const char *filename, uint width, uint height, const std::vector<helios::RGBcolor> &data, bool print_messages) {
138 if (print_messages) {
139 std::cout << "writing JPEG image: " << filename << std::endl;
140 }
141
142 helios::writeJPEG(filename, width, height, data);
143 return 1;
144}
145
146int write_PNG_file(const char *filename, uint width, uint height, bool buffers_swapped_since_render, bool transparent_background, bool print_messages) {
147 if (print_messages) {
148 std::cout << "writing PNG image: " << filename << std::endl;
149 }
150
151 // Validate framebuffer completeness
152 GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
153 if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) {
154 helios_runtime_error("ERROR (write_PNG_file): Framebuffer is not complete (status: " + std::to_string(framebuffer_status) + ")");
155 }
156
157 // Clear any existing OpenGL errors
158 while (glGetError() != GL_NO_ERROR) {
159 }
160
161 // Set proper pixel alignment for reliable reading
162 glPixelStorei(GL_PACK_ALIGNMENT, 1);
163
164 // Deterministic buffer selection based on swap state tracking
165 GLenum error;
166 if (buffers_swapped_since_render) {
167 glReadBuffer(GL_FRONT);
168 error = glGetError();
169 if (error != GL_NO_ERROR) {
170 // Fallback to back buffer if front buffer fails
171 glReadBuffer(GL_BACK);
172 error = glGetError();
173 if (error != GL_NO_ERROR) {
174 helios_runtime_error("ERROR (write_PNG_file): Cannot set read buffer (error: " + std::to_string(error) + ")");
175 }
176 }
177 } else {
178 glReadBuffer(GL_BACK);
179 error = glGetError();
180 if (error != GL_NO_ERROR) {
181 // Fallback to front buffer if back buffer fails
182 glReadBuffer(GL_FRONT);
183 error = glGetError();
184 if (error != GL_NO_ERROR) {
185 helios_runtime_error("ERROR (write_PNG_file): Cannot set read buffer (error: " + std::to_string(error) + ")");
186 }
187 }
188 }
189
190 // Ensure all rendering commands complete before reading
191 glFinish();
192
193 std::vector<helios::RGBAcolor> rgba_data;
194 rgba_data.reserve(width * height);
195
196 if (transparent_background) {
197 // Read RGBA pixels for transparency
198 const size_t bsize = 4 * width * height;
199 std::vector<GLubyte> screen_shot_trans(bsize);
200
201 glReadPixels(0, 0, scast<GLsizei>(width), scast<GLsizei>(height), GL_RGBA, GL_UNSIGNED_BYTE, &screen_shot_trans[0]);
202 error = glGetError();
203 if (error != GL_NO_ERROR) {
204 helios_runtime_error("ERROR (write_PNG_file): glReadPixels failed (error: " + std::to_string(error) + ")");
205 }
206
207 // Convert to RGBAcolor vector (glReadPixels returns bottom-to-top, so we need to flip vertically)
208 for (int row = height - 1; row >= 0; row--) {
209 for (size_t col = 0; col < width; col++) {
210 size_t byte_idx = (row * width + col) * 4;
211 rgba_data.emplace_back(screen_shot_trans[byte_idx] / 255.0f, screen_shot_trans[byte_idx + 1] / 255.0f, screen_shot_trans[byte_idx + 2] / 255.0f, screen_shot_trans[byte_idx + 3] / 255.0f);
212 }
213 }
214 } else {
215 // Read RGB pixels for opaque background
216 const size_t bsize = 3 * width * height;
217 std::vector<GLubyte> screen_shot_trans(bsize);
218
219 glReadPixels(0, 0, scast<GLsizei>(width), scast<GLsizei>(height), GL_RGB, GL_UNSIGNED_BYTE, &screen_shot_trans[0]);
220 error = glGetError();
221 if (error != GL_NO_ERROR) {
222 helios_runtime_error("ERROR (write_PNG_file): glReadPixels failed (error: " + std::to_string(error) + ")");
223 }
224
225 // Convert to RGBAcolor vector with opaque alpha (glReadPixels returns bottom-to-top, so we need to flip vertically)
226 for (int row = height - 1; row >= 0; row--) {
227 for (size_t col = 0; col < width; col++) {
228 size_t byte_idx = (row * width + col) * 3;
229 rgba_data.emplace_back(screen_shot_trans[byte_idx] / 255.0f, screen_shot_trans[byte_idx + 1] / 255.0f, screen_shot_trans[byte_idx + 2] / 255.0f, 1.0f);
230 }
231 }
232 }
233
234 helios::writePNG(filename, width, height, rgba_data);
235 return 1;
236}
237
238int write_PNG_file(const char *filename, uint width, uint height, const std::vector<helios::RGBAcolor> &data, bool print_messages) {
239 if (print_messages) {
240 std::cout << "writing PNG image: " << filename << std::endl;
241 }
242
243 helios::writePNG(filename, width, height, data);
244 return 1;
245}
246
247void read_png_file(const char *filename, std::vector<unsigned char> &texture, uint &height, uint &width) {
248 std::vector<helios::RGBAcolor> rgba_data;
249 helios::readPNG(filename, width, height, rgba_data);
250
251 texture.clear();
252 texture.reserve(width * height * 4);
253
254 for (const auto &pixel: rgba_data) {
255 texture.push_back(static_cast<unsigned char>(pixel.r * 255.0f));
256 texture.push_back(static_cast<unsigned char>(pixel.g * 255.0f));
257 texture.push_back(static_cast<unsigned char>(pixel.b * 255.0f));
258 texture.push_back(static_cast<unsigned char>(pixel.a * 255.0f));
259 }
260}
261
262Visualizer::Visualizer(uint Wdisplay) : colormap_current(), colormap_hot(), colormap_cool(), colormap_lava(), colormap_rainbow(), colormap_parula(), colormap_gray(), colormap_lines() {
263 initialize(Wdisplay, uint(std::round(Wdisplay * 0.8)), 16, true, false);
264}
265
266Visualizer::Visualizer(uint Wdisplay, uint Hdisplay) : colormap_current(), colormap_hot(), colormap_cool(), colormap_lava(), colormap_rainbow(), colormap_parula(), colormap_gray(), colormap_lines() {
267 initialize(Wdisplay, Hdisplay, 16, true, false);
268}
269
270Visualizer::Visualizer(uint Wdisplay, uint Hdisplay, int aliasing_samples) : colormap_current(), colormap_hot(), colormap_cool(), colormap_lava(), colormap_rainbow(), colormap_parula(), colormap_gray(), colormap_lines() {
271 initialize(Wdisplay, Hdisplay, aliasing_samples, true, false);
272}
273
274Visualizer::Visualizer(uint Wdisplay, uint Hdisplay, int aliasing_samples, bool window_decorations, bool headless) :
275 colormap_current(), colormap_hot(), colormap_cool(), colormap_lava(), colormap_rainbow(), colormap_parula(), colormap_gray(), colormap_lines() {
276 initialize(Wdisplay, Hdisplay, aliasing_samples, window_decorations, headless);
277}
278
279void Visualizer::openWindow() {
280 // Open a window and create its OpenGL context
281 GLFWwindow *_window = glfwCreateWindow(Wdisplay, Hdisplay, "Helios 3D Simulation", nullptr, nullptr);
282 if (_window == nullptr) {
283 std::string errorsrtring;
284 errorsrtring.append("ERROR(Visualizer): Failed to initialize graphics.\n");
285 errorsrtring.append("Common causes for this error:\n");
286 errorsrtring.append("-- OSX\n - Is XQuartz installed (xquartz.org) and configured as the default X11 window handler? When running the visualizer, XQuartz should automatically open and appear in the dock, indicating it is working.\n");
287 errorsrtring.append("-- Linux\n - Are you running this program remotely via SSH? Remote X11 graphics along with OpenGL are not natively supported. Installing and using VirtualGL is a good solution for this (virtualgl.org).\n");
288 helios_runtime_error(errorsrtring);
289 }
290 glfwMakeContextCurrent(_window);
291
292 // Associate this Visualizer instance with the GLFW window so that
293 // callbacks have access to it.
294 glfwSetWindowUserPointer(_window, this);
295
296 // Ensure we can capture the escape key being pressed below
297 glfwSetInputMode(_window, GLFW_STICKY_KEYS, GL_TRUE);
298
299 window = (void *) _window;
300
301 int window_width, window_height;
302 glfwGetWindowSize(_window, &window_width, &window_height);
303
304 int framebuffer_width, framebuffer_height;
305 glfwGetFramebufferSize(_window, &framebuffer_width, &framebuffer_height);
306
307 Wframebuffer = uint(framebuffer_width);
308 Hframebuffer = uint(framebuffer_height);
309
310 if (window_width < Wdisplay || window_height < Hdisplay) {
311 std::cerr << "WARNING (Visualizer): requested size of window is larger than the screen area." << std::endl;
312 Wdisplay = uint(window_width);
313 Hdisplay = uint(window_height);
314 }
315
316 glfwSetWindowSize(_window, window_width, window_height);
317
318 // Allow the window to freely resize so that entering full-screen
319 // results in the framebuffer matching the display resolution.
320 // This prevents the operating system from simply scaling the
321 // window contents, which can skew geometry.
322 glfwSetWindowAspectRatio(_window, GLFW_DONT_CARE, GLFW_DONT_CARE);
323
324 // Register callbacks so that window and framebuffer size changes
325 // properly update the internal dimensions used for rendering.
326 glfwSetWindowSizeCallback(_window, Visualizer::windowResizeCallback);
327 glfwSetFramebufferSizeCallback(_window, Visualizer::framebufferResizeCallback);
328
329 // Initialize GLEW
330 glewExperimental = GL_TRUE; // Needed in core profile
331 GLenum glew_result = glewInit();
332 if (glew_result != GLEW_OK) {
333 std::string error_msg = "ERROR (Visualizer): Failed to initialize GLEW. ";
334 error_msg += "GLEW error: " + std::string((const char *) glewGetErrorString(glew_result));
335 helios_runtime_error(error_msg);
336 }
337
338 // Check for and handle the expected GL_INVALID_ENUM error from glewExperimental
339 // This is a known issue with glewExperimental on some OpenGL implementations
340 GLenum gl_error = glGetError();
341 if (gl_error != GL_NO_ERROR && gl_error != GL_INVALID_ENUM) {
342 std::string error_msg = "ERROR (Visualizer): Unexpected OpenGL error after GLEW initialization: ";
343 error_msg += std::to_string(gl_error);
344 helios_runtime_error(error_msg);
345 }
346}
347
348void Visualizer::createOffscreenContext() {
349 // Create an offscreen context for headless rendering
350 // This avoids the need for a display server on CI systems
351 //
352 // NOTE: On macOS, you may see OpenGL warnings like:
353 // "UNSUPPORTED (log once): POSSIBLE ISSUE: unit 6 GLD_TEXTURE_INDEX_2D is unloadable..."
354 // This is a known macOS OpenGL driver warning in headless mode and is harmless.
355
356 // Check for environment variables that indicate CI/headless operation
357 const char *ci_env = std::getenv("CI");
358 const char *display_env = std::getenv("DISPLAY");
359 const char *force_offscreen = std::getenv("HELIOS_FORCE_OFFSCREEN");
360
361 bool is_ci = (ci_env != nullptr && std::string(ci_env) == "true");
362 bool has_display = (display_env != nullptr && std::strlen(display_env) > 0);
363 bool force_software = (force_offscreen != nullptr && std::string(force_offscreen) == "1");
364
365 // NOTE: Don't call glfwDefaultWindowHints() here - it was already called in initialize()
366 // We're just adding headless-specific hints on top of the common hints
367
368 // Configure GLFW window hints for optimal CI compatibility
369 glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
370 glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API);
371
372#if __APPLE__
373 // On macOS, configure for better CI compatibility
374 glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_FALSE); // Disable double buffering for offscreen
375 if (is_ci || force_software) {
376 // In CI environments, prefer compatibility profile for better software rendering support
377 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
378 }
379#elif __linux__
380 // On Linux, detect and configure for software rendering if needed
381 if (is_ci && !has_display) {
382 // Likely a headless CI environment - use software rendering hints
383 glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_FALSE);
384 glfwWindowHint(GLFW_SAMPLES, 0); // Disable multisampling for software rendering
385 }
386#elif _WIN32
387 // On Windows, configure for CI environments
388 if (is_ci || force_software) {
389 glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_FALSE);
390 }
391#endif
392
393 // Create a minimal 1x1 window for the OpenGL context
394 // This window will be invisible but provides the necessary OpenGL context
395 GLFWwindow *_window = glfwCreateWindow(1, 1, "Helios Offscreen", nullptr, nullptr);
396
397 if (_window == nullptr) {
398 // Failed to create even an offscreen context - provide platform-specific guidance
399 std::string error_msg = "ERROR (Visualizer::createOffscreenContext): Unable to create OpenGL context for headless rendering.\n";
400 error_msg += "This typically occurs in CI environments without GPU drivers or display servers.\n";
401 error_msg += "Platform-specific solutions:\n";
402
403#if __APPLE__
404 error_msg += "-- macOS CI: Install Xcode command line tools and ensure graphics frameworks are available\n";
405 error_msg += "-- Try setting HELIOS_FORCE_OFFSCREEN=1 environment variable\n";
406 error_msg += "-- Consider using a macOS runner with graphics support\n";
407#elif __linux__
408 error_msg += "-- Linux CI: Install virtual display server: apt-get install xvfb\n";
409 error_msg += "-- Start virtual display: Xvfb :99 -screen 0 1024x768x24 &\n";
410 error_msg += "-- Set display variable: export DISPLAY=:99\n";
411 error_msg += "-- Install Mesa software rendering: apt-get install mesa-utils libgl1-mesa-dev\n";
412 error_msg += "-- Force software rendering: export LIBGL_ALWAYS_SOFTWARE=1\n";
413#elif _WIN32
414 error_msg += "-- Windows CI: Ensure OpenGL drivers are available\n";
415 error_msg += "-- Install Mesa3D for software rendering\n";
416 error_msg += "-- Try using a Windows runner with graphics support\n";
417#endif
418 error_msg += "-- Alternative: Skip visualizer tests in CI with conditional test execution\n";
419 error_msg += "-- Set HELIOS_FORCE_OFFSCREEN=1 to attempt software rendering";
420
421 helios_runtime_error(error_msg);
422 }
423
424 glfwMakeContextCurrent(_window);
425 window = (void *) _window;
426
427 // Verify the context is current and functional
428 const char *gl_version = (const char *) glGetString(GL_VERSION);
429 if (gl_version == nullptr) {
430 glfwDestroyWindow(_window);
431 helios_runtime_error("ERROR (Visualizer::createOffscreenContext): Failed to obtain OpenGL version. Context creation failed.");
432 }
433
434 // Set framebuffer dimensions to match requested display size for offscreen rendering
435 Wframebuffer = Wdisplay;
436 Hframebuffer = Hdisplay;
437
438 // Initialize offscreen framebuffer for true headless rendering
439 // Delay this until after GLEW initialization
440 // setupOffscreenFramebuffer();
441
442 // Note: In headless mode, we won't set up window callbacks since there's no user interaction
443}
444
446 // Create a complete framebuffer for offscreen rendering with both color and depth attachments
447 // This enables full OpenGL testing in CI environments
448
449 // Validate OpenGL context and required extensions are available
450 const char *gl_version = (const char *) glGetString(GL_VERSION);
451 if (gl_version == nullptr) {
452 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): OpenGL context is not valid - unable to retrieve version string.");
453 }
454
455 // Check for framebuffer object support (OpenGL 3.0+ or ARB_framebuffer_object extension)
456 if (!GLEW_VERSION_3_0 && !GLEW_ARB_framebuffer_object) {
457 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): OpenGL context does not support framebuffer objects (requires OpenGL 3.0+ or ARB_framebuffer_object extension).");
458 }
459
460 // Validate framebuffer dimensions (must be positive and within reasonable limits)
461 if (Wframebuffer == 0 || Hframebuffer == 0) {
462 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): Invalid framebuffer dimensions (" + std::to_string(Wframebuffer) + "x" + std::to_string(Hframebuffer) + "). Dimensions must be positive.");
463 }
464
465 // Get maximum texture size to validate our request
466 GLint max_texture_size;
467 glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
468 if (static_cast<GLint>(Wframebuffer) > max_texture_size || static_cast<GLint>(Hframebuffer) > max_texture_size) {
469 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): Requested framebuffer size (" + std::to_string(Wframebuffer) + "x" + std::to_string(Hframebuffer) + ") exceeds maximum texture size (" + std::to_string(max_texture_size) +
470 ").");
471 }
472
473 // Ensure we start with a clean OpenGL state
474 if (!checkerrors()) {
475 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): OpenGL errors detected before framebuffer setup.");
476 }
477
478 // Generate the framebuffer with error checking
479 glGenFramebuffers(1, &offscreenFramebufferID);
480 GLenum error = glGetError();
481 if (error != GL_NO_ERROR || offscreenFramebufferID == 0) {
482 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): Failed to generate framebuffer object. OpenGL error: " + std::to_string(error));
483 }
484
485 glBindFramebuffer(GL_FRAMEBUFFER, offscreenFramebufferID);
486 error = glGetError();
487 if (error != GL_NO_ERROR) {
488 glDeleteFramebuffers(1, &offscreenFramebufferID);
489 offscreenFramebufferID = 0;
490 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): Failed to bind framebuffer object. OpenGL error: " + std::to_string(error));
491 }
492
493 // Create color texture attachment with comprehensive error checking
494 glGenTextures(1, &offscreenColorTexture);
495 error = glGetError();
496 if (error != GL_NO_ERROR || offscreenColorTexture == 0) {
497 glBindFramebuffer(GL_FRAMEBUFFER, 0);
498 glDeleteFramebuffers(1, &offscreenFramebufferID);
499 offscreenFramebufferID = 0;
500 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): Failed to generate color texture. OpenGL error: " + std::to_string(error));
501 }
502
503 glBindTexture(GL_TEXTURE_2D, offscreenColorTexture);
504 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<GLsizei>(Wframebuffer), static_cast<GLsizei>(Hframebuffer), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
505 error = glGetError();
506 if (error != GL_NO_ERROR) {
507 glBindFramebuffer(GL_FRAMEBUFFER, 0);
508 glDeleteTextures(1, &offscreenColorTexture);
509 glDeleteFramebuffers(1, &offscreenFramebufferID);
510 offscreenColorTexture = 0;
511 offscreenFramebufferID = 0;
512 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): Failed to create color texture storage. OpenGL error: " + std::to_string(error) + ". This may indicate insufficient GPU memory or unsupported texture format.");
513 }
514
515 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
516 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
517 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, offscreenColorTexture, 0);
518
519 // Create depth texture attachment with error checking
520 glGenTextures(1, &offscreenDepthTexture);
521 error = glGetError();
522 if (error != GL_NO_ERROR || offscreenDepthTexture == 0) {
523 glBindFramebuffer(GL_FRAMEBUFFER, 0);
524 glDeleteTextures(1, &offscreenColorTexture);
525 glDeleteFramebuffers(1, &offscreenFramebufferID);
526 offscreenColorTexture = 0;
527 offscreenFramebufferID = 0;
528 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): Failed to generate depth texture. OpenGL error: " + std::to_string(error));
529 }
530
531 glBindTexture(GL_TEXTURE_2D, offscreenDepthTexture);
532 glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, static_cast<GLsizei>(Wframebuffer), static_cast<GLsizei>(Hframebuffer), 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
533 error = glGetError();
534 if (error != GL_NO_ERROR) {
535 glBindFramebuffer(GL_FRAMEBUFFER, 0);
536 glDeleteTextures(1, &offscreenDepthTexture);
537 glDeleteTextures(1, &offscreenColorTexture);
538 glDeleteFramebuffers(1, &offscreenFramebufferID);
539 offscreenDepthTexture = 0;
540 offscreenColorTexture = 0;
541 offscreenFramebufferID = 0;
542 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): Failed to create depth texture storage. OpenGL error: " + std::to_string(error) + ". This may indicate insufficient GPU memory or unsupported depth format.");
543 }
544
545 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
546 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
547 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, offscreenDepthTexture, 0);
548
549 // Check framebuffer completeness with detailed error reporting
550 GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
551 if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) {
552 // Clean up before throwing error
553 glBindFramebuffer(GL_FRAMEBUFFER, 0);
555
556 std::string error_message = "ERROR (Visualizer::setupOffscreenFramebuffer): Offscreen framebuffer is not complete. Status: ";
557 switch (framebuffer_status) {
558 case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
559 error_message += "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT - One or more attachment points are not framebuffer attachment complete";
560 break;
561 case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
562 error_message += "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT - No images are attached to the framebuffer";
563 break;
564 case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
565 error_message += "GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER - Draw buffer configuration error";
566 break;
567 case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
568 error_message += "GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER - Read buffer configuration error";
569 break;
570 case GL_FRAMEBUFFER_UNSUPPORTED:
571 error_message += "GL_FRAMEBUFFER_UNSUPPORTED - Combination of internal formats is not supported";
572 break;
573 case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
574 error_message += "GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE - Multisample configuration error";
575 break;
576 default:
577 error_message += "Unknown framebuffer error (code: " + std::to_string(framebuffer_status) + ")";
578 break;
579 }
580 error_message += ". This typically occurs in virtualized graphics environments or with limited OpenGL driver support.";
581 helios_runtime_error(error_message);
582 }
583
584 // Restore default framebuffer
585 glBindFramebuffer(GL_FRAMEBUFFER, 0);
586
587 // Final error check
588 if (!checkerrors()) {
590 helios_runtime_error("ERROR (Visualizer::setupOffscreenFramebuffer): OpenGL errors occurred during offscreen framebuffer setup completion.");
591 }
592}
593
595 // Clean up offscreen rendering resources safely
596 // Only attempt cleanup if we have a valid OpenGL context
597 if (window != nullptr) {
598 if (offscreenFramebufferID != 0) {
599 glDeleteFramebuffers(1, &offscreenFramebufferID);
600 offscreenFramebufferID = 0;
601 }
602 if (offscreenColorTexture != 0) {
603 glDeleteTextures(1, &offscreenColorTexture);
604 offscreenColorTexture = 0;
605 }
606 if (offscreenDepthTexture != 0) {
607 glDeleteTextures(1, &offscreenDepthTexture);
608 offscreenDepthTexture = 0;
609 }
610 }
611}
612
613std::vector<helios::RGBcolor> Visualizer::readOffscreenPixels() const {
614 // Read pixels from the offscreen framebuffer for printWindow functionality
615
616 if (offscreenFramebufferID == 0) {
617 helios_runtime_error("ERROR (Visualizer::readOffscreenPixels): No offscreen framebuffer available. "
618 "Ensure setupOffscreenFramebuffer() was called successfully in headless mode.");
619 }
620
621 // Validate framebuffer dimensions
622 if (Wframebuffer == 0 || Hframebuffer == 0) {
623 helios_runtime_error("ERROR (Visualizer::readOffscreenPixels): Invalid framebuffer dimensions (" + std::to_string(Wframebuffer) + "x" + std::to_string(Hframebuffer) +
624 "). This indicates the offscreen framebuffer was not properly initialized.");
625 }
626
627 // Check that we have a valid OpenGL context
628 const char *gl_version = (const char *) glGetString(GL_VERSION);
629 if (gl_version == nullptr) {
630 helios_runtime_error("ERROR (Visualizer::readOffscreenPixels): Invalid OpenGL context. "
631 "This indicates OpenGL initialization failed or the context was lost.");
632 }
633
634 // Clear any existing OpenGL errors
635 while (glGetError() != GL_NO_ERROR) {
636 }
637
638 // Bind the offscreen framebuffer
639 glBindFramebuffer(GL_FRAMEBUFFER, offscreenFramebufferID);
640 GLenum error = glGetError();
641 if (error != GL_NO_ERROR) {
642 glBindFramebuffer(GL_FRAMEBUFFER, 0);
643 helios_runtime_error("ERROR (Visualizer::readOffscreenPixels): Failed to bind offscreen framebuffer (OpenGL error: " + std::to_string(error) + "). This indicates graphics driver issues or corrupted framebuffer.");
644 }
645
646 // Verify framebuffer is complete
647 GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
648 if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) {
649 glBindFramebuffer(GL_FRAMEBUFFER, 0);
650 helios_runtime_error("ERROR (Visualizer::readOffscreenPixels): Framebuffer is not complete (status: " + std::to_string(framebuffer_status) + "). This indicates missing attachments or graphics driver incompatibility.");
651 }
652
653 // Calculate pixel data size with overflow protection
654 const size_t pixel_count = static_cast<size_t>(Wframebuffer) * static_cast<size_t>(Hframebuffer);
655 const size_t data_size = pixel_count * 3;
656
657 // Check for potential overflow
658 if (pixel_count > SIZE_MAX / 3 || data_size > SIZE_MAX / sizeof(unsigned char)) {
659 glBindFramebuffer(GL_FRAMEBUFFER, 0);
660 helios_runtime_error("ERROR (Visualizer::readOffscreenPixels): Framebuffer dimensions too large (" + std::to_string(Wframebuffer) + "x" + std::to_string(Hframebuffer) + "). This would cause memory allocation overflow.");
661 }
662
663 std::vector<helios::RGBcolor> pixels;
664 try {
665 // Read pixels from the color attachment
666 std::vector<unsigned char> pixel_data(data_size);
667 glReadPixels(0, 0, static_cast<GLsizei>(Wframebuffer), static_cast<GLsizei>(Hframebuffer), GL_RGB, GL_UNSIGNED_BYTE, pixel_data.data());
668
669 error = glGetError();
670 if (error != GL_NO_ERROR) {
671 glBindFramebuffer(GL_FRAMEBUFFER, 0);
672 helios_runtime_error("ERROR (Visualizer::readOffscreenPixels): Failed to read pixels from framebuffer (OpenGL error: " + std::to_string(error) + "). This indicates graphics driver issues or framebuffer format problems.");
673 }
674
675 // Convert to RGBcolor format with bounds checking
676 pixels.reserve(pixel_count);
677 for (size_t i = 0; i + 2 < pixel_data.size(); i += 3) {
678 float r = pixel_data[i] / 255.0f;
679 float g = pixel_data[i + 1] / 255.0f;
680 float b = pixel_data[i + 2] / 255.0f;
681 pixels.emplace_back(make_RGBcolor(r, g, b));
682 }
683 } catch (const std::exception &e) {
684 glBindFramebuffer(GL_FRAMEBUFFER, 0);
685 helios_runtime_error("ERROR (Visualizer::readOffscreenPixels): Memory allocation or conversion failed: " + std::string(e.what()) + ". This may indicate insufficient memory or data corruption.");
686 }
687
688 // Restore default framebuffer
689 glBindFramebuffer(GL_FRAMEBUFFER, 0);
690
691 return pixels;
692}
693
694std::vector<helios::RGBAcolor> Visualizer::readOffscreenPixelsRGBA(bool read_alpha) const {
695 // Read pixels from the offscreen framebuffer for PNG output with optional transparency
696
697 if (offscreenFramebufferID == 0) {
698 helios_runtime_error("ERROR (Visualizer::readOffscreenPixelsRGBA): No offscreen framebuffer available. "
699 "Ensure setupOffscreenFramebuffer() was called successfully in headless mode.");
700 }
701
702 // Validate framebuffer dimensions
703 if (Wframebuffer == 0 || Hframebuffer == 0) {
704 helios_runtime_error("ERROR (Visualizer::readOffscreenPixelsRGBA): Invalid framebuffer dimensions (" + std::to_string(Wframebuffer) + "x" + std::to_string(Hframebuffer) +
705 "). This indicates the offscreen framebuffer was not properly initialized.");
706 }
707
708 // Check that we have a valid OpenGL context
709 const char *gl_version = (const char *) glGetString(GL_VERSION);
710 if (gl_version == nullptr) {
711 helios_runtime_error("ERROR (Visualizer::readOffscreenPixelsRGBA): Invalid OpenGL context. "
712 "This indicates OpenGL initialization failed or the context was lost.");
713 }
714
715 // Clear any existing OpenGL errors
716 while (glGetError() != GL_NO_ERROR) {
717 }
718
719 // Bind the offscreen framebuffer
720 glBindFramebuffer(GL_FRAMEBUFFER, offscreenFramebufferID);
721 GLenum error = glGetError();
722 if (error != GL_NO_ERROR) {
723 glBindFramebuffer(GL_FRAMEBUFFER, 0);
724 helios_runtime_error("ERROR (Visualizer::readOffscreenPixelsRGBA): Failed to bind offscreen framebuffer (OpenGL error: " + std::to_string(error) + "). This indicates graphics driver issues or corrupted framebuffer.");
725 }
726
727 // Verify framebuffer is complete
728 GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
729 if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) {
730 glBindFramebuffer(GL_FRAMEBUFFER, 0);
731 helios_runtime_error("ERROR (Visualizer::readOffscreenPixelsRGBA): Framebuffer is not complete (status: " + std::to_string(framebuffer_status) + "). This indicates missing attachments or graphics driver incompatibility.");
732 }
733
734 // Calculate pixel data size with overflow protection
735 const size_t pixel_count = static_cast<size_t>(Wframebuffer) * static_cast<size_t>(Hframebuffer);
736 const size_t channels = read_alpha ? 4 : 3;
737 const size_t data_size = pixel_count * channels;
738
739 // Check for potential overflow
740 if (pixel_count > SIZE_MAX / 4 || data_size > SIZE_MAX / sizeof(unsigned char)) {
741 glBindFramebuffer(GL_FRAMEBUFFER, 0);
742 helios_runtime_error("ERROR (Visualizer::readOffscreenPixelsRGBA): Framebuffer dimensions too large (" + std::to_string(Wframebuffer) + "x" + std::to_string(Hframebuffer) + "). This would cause memory allocation overflow.");
743 }
744
745 std::vector<helios::RGBAcolor> pixels;
746 try {
747 // Read pixels from the color attachment
748 std::vector<unsigned char> pixel_data(data_size);
749 if (read_alpha) {
750 glReadPixels(0, 0, static_cast<GLsizei>(Wframebuffer), static_cast<GLsizei>(Hframebuffer), GL_RGBA, GL_UNSIGNED_BYTE, pixel_data.data());
751 } else {
752 glReadPixels(0, 0, static_cast<GLsizei>(Wframebuffer), static_cast<GLsizei>(Hframebuffer), GL_RGB, GL_UNSIGNED_BYTE, pixel_data.data());
753 }
754
755 error = glGetError();
756 if (error != GL_NO_ERROR) {
757 glBindFramebuffer(GL_FRAMEBUFFER, 0);
758 helios_runtime_error("ERROR (Visualizer::readOffscreenPixelsRGBA): Failed to read pixels from framebuffer (OpenGL error: " + std::to_string(error) + "). This indicates graphics driver issues or framebuffer format problems.");
759 }
760
761 // Convert to RGBAcolor format with bounds checking (glReadPixels returns bottom-to-top, so we need to flip vertically)
762 pixels.reserve(pixel_count);
763 if (read_alpha) {
764 for (int row = Hframebuffer - 1; row >= 0; row--) {
765 for (size_t col = 0; col < Wframebuffer; col++) {
766 size_t byte_idx = (row * Wframebuffer + col) * 4;
767 float r = pixel_data[byte_idx] / 255.0f;
768 float g = pixel_data[byte_idx + 1] / 255.0f;
769 float b = pixel_data[byte_idx + 2] / 255.0f;
770 float a = pixel_data[byte_idx + 3] / 255.0f;
771 pixels.emplace_back(make_RGBAcolor(r, g, b, a));
772 }
773 }
774 } else {
775 for (int row = Hframebuffer - 1; row >= 0; row--) {
776 for (size_t col = 0; col < Wframebuffer; col++) {
777 size_t byte_idx = (row * Wframebuffer + col) * 3;
778 float r = pixel_data[byte_idx] / 255.0f;
779 float g = pixel_data[byte_idx + 1] / 255.0f;
780 float b = pixel_data[byte_idx + 2] / 255.0f;
781 pixels.emplace_back(make_RGBAcolor(r, g, b, 1.0f));
782 }
783 }
784 }
785 } catch (const std::exception &e) {
786 glBindFramebuffer(GL_FRAMEBUFFER, 0);
787 helios_runtime_error("ERROR (Visualizer::readOffscreenPixelsRGBA): Memory allocation or conversion failed: " + std::string(e.what()) + ". This may indicate insufficient memory or data corruption.");
788 }
789
790 // Restore default framebuffer
791 glBindFramebuffer(GL_FRAMEBUFFER, 0);
792
793 return pixels;
794}
795
797 // Switch rendering target to offscreen framebuffer
798 if (offscreenFramebufferID != 0) {
799 glBindFramebuffer(GL_FRAMEBUFFER, offscreenFramebufferID);
800 glViewport(0, 0, Wframebuffer, Hframebuffer);
801 }
802}
803
804void Visualizer::initialize(uint window_width_pixels, uint window_height_pixels, int aliasing_samples, bool window_decorations, bool headless_mode) {
805 Wdisplay = window_width_pixels;
806 Hdisplay = window_height_pixels;
807
808 // Check environment variables for automatic headless mode detection
809 const char *force_offscreen = std::getenv("HELIOS_FORCE_OFFSCREEN");
810 const char *ci_env = std::getenv("CI");
811 const char *display_env = std::getenv("DISPLAY");
812
813 bool should_force_headless = false;
814 if (force_offscreen != nullptr && std::string(force_offscreen) == "1") {
815 should_force_headless = true;
816 } else if (ci_env != nullptr && std::string(ci_env) == "true") {
817 // In CI environment, check if we have a display
818#if __linux__
819 if (display_env == nullptr || std::strlen(display_env) == 0) {
820 should_force_headless = true; // Linux CI without DISPLAY
821 }
822#elif __APPLE__ || _WIN32
823 // On macOS and Windows CI, graphics might not be available
824 should_force_headless = true; // Can be overridden by explicit headless=false
825#endif
826 }
827
828 // Final headless determination: explicit parameter OR environment-forced
829 headless = headless_mode || should_force_headless;
830
831 shadow_buffer_size = make_uint2(8192, 8192);
832
833 maximum_texture_size = make_uint2(2048, 2048);
834
835 texArray = 0;
836 texture_array_layers = 0;
837 textures_dirty = false;
838
839 // Initialize offscreen rendering variables
840 offscreenFramebufferID = 0;
841 offscreenColorTexture = 0;
842 offscreenDepthTexture = 0;
843
844 message_flag = true;
845
846 frame_counter = 0;
847
848 buffers_swapped_since_render = false;
849
850 camera_FOV = 45;
851
852 minimum_view_radius = 0.05f;
853
854 context = nullptr;
855 primitiveColorsNeedUpdate = false;
856
857 isWatermarkVisible = true;
858 watermark_ID = 0;
859
860 navigation_gizmo_enabled = true;
861 previous_camera_eye_location = make_vec3(0, 0, 0);
862 previous_camera_lookat_center = make_vec3(0, 0, 0);
863 navigation_gizmo_IDs.clear();
864 hovered_gizmo_bubble = -1; // -1 = no hover
865
866 background_is_transparent = false;
867 watermark_was_visible_before_transparent = false;
868 navigation_gizmo_was_enabled_before_image_display = false;
869 background_rectangle_ID = 0;
870
871 colorbar_flag = 0;
872
873 colorbar_min = 0.f;
874 colorbar_max = 0.f;
875 colorbar_integer_data = false;
876
877 colorbar_title = "";
878 colorbar_fontsize = 12;
879 colorbar_fontcolor = RGB::black;
880
881 colorbar_position = make_vec3(0.65, 0.1, 0.1);
882 colorbar_size = make_vec2(0.15, 0.1);
883 colorbar_intended_aspect_ratio = 1.5f; // width/height = 0.15/0.1
884 colorbar_IDs.clear();
885
886 point_width = 1;
887
888 // Initialize point cloud culling settings
889 point_culling_enabled = true;
890 point_culling_threshold = 10000; // Enable culling for point clouds with 10K+ points
891 point_max_render_distance = 0; // Auto-calculated based on scene size
892 point_lod_factor = 10.0f; // Cull every 10th point in far regions
893
894 // Initialize performance metrics
895 points_total_count = 0;
896 points_rendered_count = 0;
897 last_culling_time_ms = 0;
898
899 // Initialize OpenGL context for both regular and headless modes
900 // Headless mode needs an offscreen context for geometry operations
901
902 // Initialize GLFW using reference counting
903 // GLFW should be initialized once and kept alive to avoid macOS-specific issues
904 // with rapid init/terminate cycles (pixel format caching, autorelease pool issues)
905 if (glfw_reference_count == 0) {
906 if (!glfwInit()) {
907 helios_runtime_error("ERROR (Visualizer::initialize): Failed to initialize GLFW");
908 }
909 // CRITICAL for macOS: Process events immediately after first init to drain autorelease pool
910 glfwPollEvents();
911 }
912 glfw_reference_count++;
913
914 // CRITICAL for macOS: Process any pending events before configuring new window
915 // This ensures previous window destruction is fully processed
916 glfwPollEvents();
917
918 // Reset all GLFW window hints to defaults before configuring
919 // This is critical when multiple Visualizer instances are created with different modes,
920 // as hints like GLFW_DOUBLEBUFFER persist between glfwCreateWindow calls
921 glfwDefaultWindowHints();
922
923 glfwWindowHint(GLFW_SAMPLES, std::max(0, aliasing_samples)); // antialiasing
924 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // We want OpenGL 3.3
925 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
926#if __APPLE__
927 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // To make MacOS happy; should not be needed
928 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // We don't want the old OpenGL
929#endif
930
931 if (headless) {
932 // Create offscreen context for headless mode
933 createOffscreenContext();
934 } else {
935 // Regular windowed mode
936 glfwWindowHint(GLFW_VISIBLE, 0); // Initially hidden, will show later if needed
937
938 if (!window_decorations) {
939 glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
940 }
941
942 openWindow();
943 }
944
945 // Initialize GLEW - required for both headless and windowed modes
946 glewExperimental = GL_TRUE; // Needed in core profile
947 GLenum glew_result = glewInit();
948 if (glew_result != GLEW_OK) {
949 std::string error_msg = "ERROR (Visualizer::initialize): Failed to initialize GLEW. ";
950 error_msg += "GLEW error: " + std::string((const char *) glewGetErrorString(glew_result));
951
952 if (headless) {
953 error_msg += "\nIn headless mode, this usually indicates:";
954 error_msg += "\n- Missing or incompatible OpenGL drivers";
955 error_msg += "\n- Virtual display server not properly configured";
956 error_msg += "\n- VirtualGL or Mesa software rendering issues";
957 error_msg += "\nConsider setting LIBGL_ALWAYS_SOFTWARE=1 for software rendering";
958 }
959
960 helios_runtime_error(error_msg);
961 }
962
963 // Check for and handle the expected GL_INVALID_ENUM error from glewExperimental
964 // This is a known issue with glewExperimental on some OpenGL implementations
965 GLenum gl_error = glGetError();
966 if (gl_error != GL_NO_ERROR && gl_error != GL_INVALID_ENUM) {
967 std::string error_msg = "ERROR (Visualizer): Unexpected OpenGL error after GLEW initialization: ";
968 error_msg += std::to_string(gl_error);
969 if (headless) {
970 error_msg += "\nThis indicates a serious OpenGL context or driver issue in headless mode.";
971 }
972 helios_runtime_error(error_msg);
973 }
974
975 // Validate basic OpenGL functionality after GLEW initialization
976 const char *gl_version = (const char *) glGetString(GL_VERSION);
977 const char *gl_vendor = (const char *) glGetString(GL_VENDOR);
978 const char *gl_renderer = (const char *) glGetString(GL_RENDERER);
979
980 if (gl_version == nullptr || gl_vendor == nullptr || gl_renderer == nullptr) {
981 helios_runtime_error("ERROR (Visualizer::initialize): OpenGL context is not functional - unable to query basic GL information. "
982 "This indicates a fundamental issue with OpenGL context creation or driver compatibility.");
983 }
984
985 // In debug mode or verbose CI, log the OpenGL information
986 if (headless && (std::getenv("CI") != nullptr || std::getenv("HELIOS_DEBUG") != nullptr)) {
987 std::cout << "OpenGL Version: " << gl_version << std::endl;
988 std::cout << "OpenGL Vendor: " << gl_vendor << std::endl;
989 std::cout << "OpenGL Renderer: " << gl_renderer << std::endl;
990 }
991
992 // Test basic OpenGL operations that are required for visualizer functionality
993 GLint max_texture_size;
994 glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
995 GLenum validation_error = glGetError();
996 if (validation_error != GL_NO_ERROR) {
997 helios_runtime_error("ERROR (Visualizer::initialize): Basic OpenGL query operations failed (error: " + std::to_string(validation_error) +
998 "). "
999 "This indicates the OpenGL context is not properly initialized or lacks required functionality.");
1000 }
1001
1002 // Warn if texture size is unusually small (indicates software rendering or limited drivers)
1003 if (max_texture_size < 1024 && headless) {
1004 std::cerr << "WARNING (Visualizer::initialize): Maximum texture size is very small (" << max_texture_size << "x" << max_texture_size << "). This may indicate software rendering or limited driver support." << std::endl;
1005 }
1006
1007 // Final verification that we don't have accumulated OpenGL errors
1008 if (!checkerrors()) {
1009 helios_runtime_error("ERROR (Visualizer::initialize): OpenGL context initialization failed after GLEW setup and validation. "
1010 "This often occurs in headless CI environments without proper GPU drivers or display servers. "
1011 "For headless operation, ensure proper virtual display or software rendering is configured.");
1012 }
1013
1014 // Enable relevant parameters for both regular and headless modes
1015
1016 glEnable(GL_DEPTH_TEST); // Enable depth test
1017 glDepthFunc(GL_LEQUAL); // Accept fragment if it closer to or equal to the camera than the former one (required for sky rendering)
1018 // glEnable(GL_DEPTH_CLAMP);
1019
1020 if (aliasing_samples <= 0) {
1021 glDisable(GL_MULTISAMPLE);
1022 glDisable(GL_MULTISAMPLE_ARB);
1023 }
1024
1025 if (aliasing_samples <= 1) {
1026 glDisable(GL_POLYGON_SMOOTH);
1027 } else {
1028 glEnable(GL_POLYGON_SMOOTH);
1029 }
1030
1031 // glEnable(GL_TEXTURE0);
1032 // glEnable(GL_TEXTURE_2D_ARRAY);
1033 // glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
1034 // glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
1035
1036 // Check for OpenGL errors after basic setup
1037 if (!checkerrors()) {
1038 helios_runtime_error("ERROR (Visualizer::initialize): OpenGL context setup failed during basic parameter configuration. "
1039 "This typically indicates graphics driver incompatibility or missing OpenGL support in the execution environment.");
1040 }
1041
1042 // Initialize offscreen framebuffer for headless mode after OpenGL context is fully set up
1043 if (headless) {
1045 }
1046
1047 // glEnable(GL_TEXTURE1);
1048 glEnable(GL_POLYGON_OFFSET_FILL);
1049 glPolygonOffset(1.0f, 1.0f);
1050 glDisable(GL_CULL_FACE);
1051
1052 // Check for OpenGL errors after advanced setup
1053 if (!checkerrors()) {
1054 helios_runtime_error("ERROR (Visualizer::initialize): OpenGL context setup failed during advanced parameter configuration. "
1055 "Verify that the graphics environment supports the required OpenGL version and features.");
1056 }
1057
1058 // Initialize VBO's and texture buffers with comprehensive error checking
1059 constexpr size_t Ntypes = GeometryHandler::all_geometry_types.size();
1060
1061 // Validate that we have a reasonable number of geometry types to prevent memory issues
1062 if (Ntypes == 0 || Ntypes > 1000) {
1063 helios_runtime_error("ERROR (Visualizer::initialize): Invalid number of geometry types (" + std::to_string(Ntypes) +
1064 "). "
1065 "This indicates a configuration issue with GeometryHandler.");
1066 }
1067
1068 try {
1069 // per-vertex data with error checking after each allocation
1070 face_index_buffer.resize(Ntypes);
1071 vertex_buffer.resize(Ntypes);
1072 uv_buffer.resize(Ntypes);
1073
1074 // Generate per-vertex buffers with immediate error checking
1075 glGenBuffers((GLsizei) face_index_buffer.size(), face_index_buffer.data());
1076 GLenum error = glGetError();
1077 if (error != GL_NO_ERROR) {
1078 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate face index buffers. OpenGL error: " + std::to_string(error));
1079 }
1080
1081 glGenBuffers((GLsizei) vertex_buffer.size(), vertex_buffer.data());
1082 error = glGetError();
1083 if (error != GL_NO_ERROR) {
1084 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate vertex buffers. OpenGL error: " + std::to_string(error));
1085 }
1086
1087 glGenBuffers((GLsizei) uv_buffer.size(), uv_buffer.data());
1088 error = glGetError();
1089 if (error != GL_NO_ERROR) {
1090 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate UV buffers. OpenGL error: " + std::to_string(error));
1091 }
1092
1093 // per-primitive data with error checking after each allocation
1094 color_buffer.resize(Ntypes);
1095 color_texture_object.resize(Ntypes);
1096 normal_buffer.resize(Ntypes);
1097 normal_texture_object.resize(Ntypes);
1098 texture_flag_buffer.resize(Ntypes);
1099 texture_flag_texture_object.resize(Ntypes);
1100 texture_ID_buffer.resize(Ntypes);
1101 texture_ID_texture_object.resize(Ntypes);
1102 coordinate_flag_buffer.resize(Ntypes);
1103 coordinate_flag_texture_object.resize(Ntypes);
1104 hidden_flag_buffer.resize(Ntypes);
1105 hidden_flag_texture_object.resize(Ntypes);
1106 sky_geometry_flag_buffer.resize(Ntypes);
1107 sky_geometry_flag_texture_object.resize(Ntypes);
1108
1109 // Generate per-primitive buffers and textures with comprehensive error checking
1110 glGenBuffers((GLsizei) color_buffer.size(), color_buffer.data());
1111 error = glGetError();
1112 if (error != GL_NO_ERROR) {
1113 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate color buffers. OpenGL error: " + std::to_string(error));
1114 }
1115
1116 glGenTextures((GLsizei) color_texture_object.size(), color_texture_object.data());
1117 error = glGetError();
1118 if (error != GL_NO_ERROR) {
1119 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate color texture objects. OpenGL error: " + std::to_string(error));
1120 }
1121
1122 glGenBuffers((GLsizei) normal_buffer.size(), normal_buffer.data());
1123 error = glGetError();
1124 if (error != GL_NO_ERROR) {
1125 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate normal buffers. OpenGL error: " + std::to_string(error));
1126 }
1127
1128 glGenTextures((GLsizei) normal_texture_object.size(), normal_texture_object.data());
1129 error = glGetError();
1130 if (error != GL_NO_ERROR) {
1131 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate normal texture objects. OpenGL error: " + std::to_string(error));
1132 }
1133
1134 glGenBuffers((GLsizei) texture_flag_buffer.size(), texture_flag_buffer.data());
1135 error = glGetError();
1136 if (error != GL_NO_ERROR) {
1137 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate texture flag buffers. OpenGL error: " + std::to_string(error));
1138 }
1139
1140 glGenTextures((GLsizei) texture_flag_texture_object.size(), texture_flag_texture_object.data());
1141 error = glGetError();
1142 if (error != GL_NO_ERROR) {
1143 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate texture flag texture objects. OpenGL error: " + std::to_string(error));
1144 }
1145
1146 glGenBuffers((GLsizei) texture_ID_buffer.size(), texture_ID_buffer.data());
1147 error = glGetError();
1148 if (error != GL_NO_ERROR) {
1149 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate texture ID buffers. OpenGL error: " + std::to_string(error));
1150 }
1151
1152 glGenTextures((GLsizei) texture_ID_texture_object.size(), texture_ID_texture_object.data());
1153 error = glGetError();
1154 if (error != GL_NO_ERROR) {
1155 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate texture ID texture objects. OpenGL error: " + std::to_string(error));
1156 }
1157
1158 glGenBuffers((GLsizei) coordinate_flag_buffer.size(), coordinate_flag_buffer.data());
1159 error = glGetError();
1160 if (error != GL_NO_ERROR) {
1161 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate coordinate flag buffers. OpenGL error: " + std::to_string(error));
1162 }
1163
1164 glGenTextures((GLsizei) coordinate_flag_texture_object.size(), coordinate_flag_texture_object.data());
1165 error = glGetError();
1166 if (error != GL_NO_ERROR) {
1167 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate coordinate flag texture objects. OpenGL error: " + std::to_string(error));
1168 }
1169
1170 glGenBuffers((GLsizei) hidden_flag_buffer.size(), hidden_flag_buffer.data());
1171 error = glGetError();
1172 if (error != GL_NO_ERROR) {
1173 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate hidden flag buffers. OpenGL error: " + std::to_string(error));
1174 }
1175
1176 glGenTextures((GLsizei) hidden_flag_texture_object.size(), hidden_flag_texture_object.data());
1177 error = glGetError();
1178 if (error != GL_NO_ERROR) {
1179 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate hidden flag texture objects. OpenGL error: " + std::to_string(error));
1180 }
1181
1182 glGenBuffers((GLsizei) sky_geometry_flag_buffer.size(), sky_geometry_flag_buffer.data());
1183 error = glGetError();
1184 if (error != GL_NO_ERROR) {
1185 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate sky geometry flag buffers. OpenGL error: " + std::to_string(error));
1186 }
1187
1188 glGenTextures((GLsizei) sky_geometry_flag_texture_object.size(), sky_geometry_flag_texture_object.data());
1189 error = glGetError();
1190 if (error != GL_NO_ERROR) {
1191 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate sky geometry flag texture objects. OpenGL error: " + std::to_string(error));
1192 }
1193
1194 // Generate UV rescaling buffers
1195 glGenBuffers(1, &uv_rescale_buffer);
1196 error = glGetError();
1197 if (error != GL_NO_ERROR) {
1198 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate UV rescale buffer. OpenGL error: " + std::to_string(error));
1199 }
1200
1201 glGenTextures(1, &uv_rescale_texture_object);
1202 error = glGetError();
1203 if (error != GL_NO_ERROR) {
1204 helios_runtime_error("ERROR (Visualizer::initialize): Failed to generate UV rescale texture object. OpenGL error: " + std::to_string(error));
1205 }
1206
1207 } catch (const std::exception &e) {
1208 helios_runtime_error("ERROR (Visualizer::initialize): Exception during buffer allocation: " + std::string(e.what()) + ". This may indicate insufficient memory or OpenGL driver issues.");
1209 }
1210
1211 // Final verification that all buffer operations completed successfully
1212 if (!checkerrors()) {
1213 helios_runtime_error("ERROR (Visualizer::initialize): OpenGL buffer creation failed with accumulated errors. "
1214 "This indicates insufficient graphics memory or unsupported buffer operations in the current OpenGL context.");
1215 }
1216
1217 //~~~~~~~~~~~~~ Load the Shaders ~~~~~~~~~~~~~~~~~~~//
1218
1219 std::string primaryVertShader = helios::resolvePluginAsset("visualizer", "shaders/primaryShader.vert").string();
1220 std::string primaryFragShader = helios::resolvePluginAsset("visualizer", "shaders/primaryShader.frag").string();
1221 std::string shadowVertShader = helios::resolvePluginAsset("visualizer", "shaders/shadow.vert").string();
1222 std::string shadowFragShader = helios::resolvePluginAsset("visualizer", "shaders/shadow.frag").string();
1223 std::string lineVertShader = helios::resolvePluginAsset("visualizer", "shaders/line.vert").string();
1224 std::string lineGeomShader = helios::resolvePluginAsset("visualizer", "shaders/line.geom").string();
1225
1226 primaryShader.initialize(primaryVertShader.c_str(), primaryFragShader.c_str(), this);
1227 depthShader.initialize(shadowVertShader.c_str(), shadowFragShader.c_str(), this);
1228 lineShader.initialize(lineVertShader.c_str(), primaryFragShader.c_str(), this, lineGeomShader.c_str());
1229
1230 // Check for OpenGL errors after shader initialization
1231 if (!checkerrors()) {
1232 helios_runtime_error("ERROR (Visualizer::initialize): Shader initialization failed. "
1233 "Verify that shader files are accessible and the OpenGL context supports the required shading language version.");
1234 }
1235
1236 primaryShader.useShader();
1237
1238 // Initialize frame buffer only for windowed mode
1239 if (!headless) {
1240 // The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer.
1241 glGenFramebuffers(1, &framebufferID);
1242 glBindFramebuffer(GL_FRAMEBUFFER, framebufferID);
1243
1244 // Depth texture. Slower than a depth buffer, but you can sample it later in your shader
1245 glActiveTexture(GL_TEXTURE1);
1246 glGenTextures(1, &depthTexture);
1247 glBindTexture(GL_TEXTURE_2D, depthTexture);
1248 glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, shadow_buffer_size.x, shadow_buffer_size.y, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
1249
1250 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
1251 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1252
1253 // clamp to border so any lookup outside [0,1] returns 1.0 (no shadow)
1254 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
1255 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
1256 GLfloat borderColor[4] = {1.0f, 1.0f, 1.0f, 1.0f};
1257 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
1258
1259 // enable hardware depth comparison
1260 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
1261
1262 if (!checkerrors()) {
1263 helios_runtime_error("ERROR (Visualizer::initialize): OpenGL setup failed during texture configuration. "
1264 "This may indicate graphics driver issues or insufficient OpenGL support.");
1265 }
1266
1267 // restore default active texture for subsequent texture setup
1268 glActiveTexture(GL_TEXTURE0);
1269
1270 glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0);
1271
1272 glDrawBuffer(GL_NONE); // No color buffer is drawn to.
1273
1274 // Always check that our framebuffer is ok
1275 int max_checks = 10000;
1276 int checks = 0;
1277 while (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE && checks < max_checks) {
1278 checks++;
1279 }
1280 // Check framebuffer completeness instead of using assert
1281 GLenum framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
1282 if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) {
1283 std::string error_message = "ERROR (Visualizer::initialize): Framebuffer is incomplete. Status: ";
1284 switch (framebuffer_status) {
1285 case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
1286 error_message += "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT - Framebuffer attachment is incomplete";
1287 break;
1288 case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
1289 error_message += "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT - No attachments";
1290 break;
1291 case GL_FRAMEBUFFER_UNSUPPORTED:
1292 error_message += "GL_FRAMEBUFFER_UNSUPPORTED - Unsupported framebuffer format";
1293 break;
1294 default:
1295 error_message += "Unknown framebuffer error code: " + std::to_string(framebuffer_status);
1296 break;
1297 }
1298 error_message += ". This typically occurs in CI environments with limited graphics support or missing GPU drivers.";
1299 helios_runtime_error(error_message);
1300 }
1301
1302 // Finished OpenGL setup
1303 // Check for OpenGL errors after framebuffer setup
1304 if (!checkerrors()) {
1305 helios_runtime_error("ERROR (Visualizer::initialize): Framebuffer setup failed. "
1306 "This indicates issues with OpenGL framebuffer operations, often related to graphics driver limitations or insufficient resources.");
1307 }
1308 } else {
1309 // Set framebuffer dimensions for headless mode (no framebuffer created)
1310 Wframebuffer = Wdisplay;
1311 Hframebuffer = Hdisplay;
1312 }
1313
1314 // Initialize transformation matrices
1315
1316 perspectiveTransformationMatrix = glm::mat4(1.f);
1317
1318 customTransformationMatrix = glm::mat4(1.f);
1319
1320 // Default values
1321
1322 light_direction = make_vec3(1, 1, 1);
1323 light_direction.normalize();
1324 if (!headless) {
1325 primaryShader.setLightDirection(light_direction);
1326 }
1327
1328 primaryLightingModel = Visualizer::LIGHTING_NONE;
1329
1330 camera_lookat_center = make_vec3(0, 0, 0);
1331 camera_eye_location = camera_lookat_center + sphere2cart(make_SphericalCoord(2.f, 90.f * PI_F / 180.f, 0));
1332
1333 backgroundColor = make_RGBcolor(0.4, 0.4, 0.4);
1334
1335 // colormaps
1336
1337 // HOT
1338 std::vector<RGBcolor> ctable_c{{0.f, 0.f, 0.f}, {0.5f, 0.f, 0.5f}, {1.f, 0.f, 0.f}, {1.f, 0.5f, 0.f}, {1.f, 1.f, 0.f}};
1339
1340 std::vector<float> clocs_c{0.f, 0.25f, 0.5f, 0.75f, 1.f};
1341
1342 colormap_hot.set(ctable_c, clocs_c, 100, 0, 1);
1343
1344 // COOL
1345 ctable_c = {RGB::cyan, RGB::magenta};
1346
1347 clocs_c = {0.f, 1.f};
1348
1349 colormap_cool.set(ctable_c, clocs_c, 100, 0, 1);
1350
1351 // LAVA
1352 ctable_c = {{0.f, 0.05f, 0.05f}, {0.f, 0.6f, 0.6f}, {1.f, 1.f, 1.f}, {1.f, 0.f, 0.f}, {0.5f, 0.f, 0.f}};
1353
1354 clocs_c = {0.f, 0.4f, 0.5f, 0.6f, 1.f};
1355
1356 colormap_lava.set(ctable_c, clocs_c, 100, 0, 1);
1357
1358 // RAINBOW
1359 ctable_c = {RGB::navy, RGB::cyan, RGB::yellow, make_RGBcolor(0.75f, 0.f, 0.f)};
1360
1361 clocs_c = {0, 0.3f, 0.7f, 1.f};
1362
1363 colormap_rainbow.set(ctable_c, clocs_c, 100, 0, 1);
1364
1365 // PARULA
1366 ctable_c = {RGB::navy, make_RGBcolor(0, 0.6, 0.6), RGB::goldenrod, RGB::yellow};
1367
1368 clocs_c = {0, 0.4f, 0.7f, 1.f};
1369
1370 colormap_parula.set(ctable_c, clocs_c, 100, 0, 1);
1371
1372 // GRAY
1373 ctable_c = {RGB::black, RGB::white};
1374
1375 clocs_c = {0.f, 1.f};
1376
1377 colormap_gray.set(ctable_c, clocs_c, 100, 0, 1);
1378
1379 // LINES (MATLAB-style distinct colors)
1380 ctable_c = {
1381 {0.f, 0.4470f, 0.7410f}, // blue
1382 {0.8500f, 0.3250f, 0.0980f}, // orange
1383 {0.9290f, 0.6940f, 0.1250f}, // yellow
1384 {0.4940f, 0.1840f, 0.5560f}, // purple
1385 {0.4660f, 0.6740f, 0.1880f}, // green
1386 {0.3010f, 0.7450f, 0.9330f}, // cyan
1387 {0.6350f, 0.0780f, 0.1840f} // dark red
1388 };
1389
1390 clocs_c = {0.f, 1.f / 6.f, 2.f / 6.f, 3.f / 6.f, 4.f / 6.f, 5.f / 6.f, 1.f};
1391
1392 colormap_lines.set(ctable_c, clocs_c, 100, 0, 1);
1393
1394 colormap_current = colormap_hot;
1395
1396 if (!headless) {
1397 glfwSetMouseButtonCallback((GLFWwindow *) window, mouseCallback);
1398 glfwSetCursorPosCallback((GLFWwindow *) window, cursorCallback);
1399 glfwSetScrollCallback((GLFWwindow *) window, scrollCallback);
1400
1401 // Check for OpenGL errors after callback setup
1402 if (!checkerrors()) {
1403 helios_runtime_error("ERROR (Visualizer::initialize): Final OpenGL setup failed during callback configuration. "
1404 "The OpenGL context may be in an invalid state or missing required extensions.");
1405 }
1406 }
1407
1408 // Set gradient background as the default background
1409 setBackgroundGradient();
1410}
1411
1413 // Clean up resources for both headless and windowed modes
1414 if (headless) {
1416 } else {
1417 if (framebufferID != 0) {
1418 glDeleteFramebuffers(1, &framebufferID);
1419 }
1420 if (depthTexture != 0) {
1421 glDeleteTextures(1, &depthTexture);
1422 }
1423 }
1424
1425 // Clean up common OpenGL resources regardless of mode
1426 if (window != nullptr) {
1427
1428 glDeleteBuffers((GLsizei) face_index_buffer.size(), face_index_buffer.data());
1429 glDeleteBuffers((GLsizei) vertex_buffer.size(), vertex_buffer.data());
1430 glDeleteBuffers((GLsizei) uv_buffer.size(), uv_buffer.data());
1431
1432 glDeleteBuffers((GLsizei) color_buffer.size(), color_buffer.data());
1433 glDeleteTextures((GLsizei) color_texture_object.size(), color_texture_object.data());
1434 glDeleteBuffers((GLsizei) normal_buffer.size(), normal_buffer.data());
1435 glDeleteTextures((GLsizei) normal_texture_object.size(), normal_texture_object.data());
1436 glDeleteBuffers((GLsizei) texture_flag_buffer.size(), texture_flag_buffer.data());
1437 glDeleteTextures((GLsizei) texture_flag_texture_object.size(), texture_flag_texture_object.data());
1438 glDeleteBuffers((GLsizei) texture_ID_buffer.size(), texture_ID_buffer.data());
1439 glDeleteTextures((GLsizei) texture_ID_texture_object.size(), texture_ID_texture_object.data());
1440 glDeleteBuffers((GLsizei) coordinate_flag_buffer.size(), coordinate_flag_buffer.data());
1441 glDeleteTextures((GLsizei) coordinate_flag_texture_object.size(), coordinate_flag_texture_object.data());
1442 glDeleteBuffers((GLsizei) hidden_flag_buffer.size(), hidden_flag_buffer.data());
1443 glDeleteTextures((GLsizei) hidden_flag_texture_object.size(), hidden_flag_texture_object.data());
1444 glDeleteBuffers((GLsizei) sky_geometry_flag_buffer.size(), sky_geometry_flag_buffer.data());
1445 glDeleteTextures((GLsizei) sky_geometry_flag_texture_object.size(), sky_geometry_flag_texture_object.data());
1446
1447 // Clean up texture array and UV rescaling resources
1448 if (texArray != 0) {
1449 glDeleteTextures(1, &texArray);
1450 }
1451 glDeleteBuffers(1, &uv_rescale_buffer);
1452 glDeleteTextures(1, &uv_rescale_texture_object);
1453
1454 // CRITICAL for macOS: Process events to clean up window state before destroying
1455 // Without this, macOS Cocoa/NSGL backend leaves cached state that causes crashes
1456 // See: https://github.com/glfw/glfw/issues/1412, #1018, #721
1457 glfwPollEvents();
1458
1459 glfwDestroyWindow(scast<GLFWwindow *>(window));
1460
1461 // CRITICAL for macOS: Process events again after destroying to drain autorelease pool
1462 // This ensures all macOS window resources are properly cleaned up
1463 glfwPollEvents();
1464
1465 // Decrement reference count and only terminate GLFW when last Visualizer is destroyed
1466 // Keeping GLFW initialized avoids macOS issues with pixel format caching and context reinitialization
1467 glfw_reference_count--;
1468 if (glfw_reference_count == 0) {
1469 glfwTerminate();
1470 }
1471 }
1472}
1473
1475 message_flag = true;
1476}
1477
1479 message_flag = false;
1480}
1481
1482void Visualizer::setCameraPosition(const helios::vec3 &cameraPosition, const helios::vec3 &lookAt) {
1483 camera_eye_location = cameraPosition;
1484 camera_lookat_center = lookAt;
1485}
1486
1488 camera_lookat_center = lookAt;
1489 camera_eye_location = camera_lookat_center + sphere2cart(cameraAngle);
1490}
1491
1493 camera_FOV = angle_FOV;
1494}
1495
1497 light_direction = direction / direction.magnitude();
1498 primaryShader.setLightDirection(direction);
1499}
1500
1502 primaryLightingModel = lightingmodel;
1503}
1504
1505void Visualizer::setLightIntensityFactor(float lightintensityfactor) {
1506 lightintensity = lightintensityfactor;
1507}
1508
1509void Visualizer::removeBackgroundRectangle() {
1510 // Remove background rectangle if it exists
1511 if (background_rectangle_ID != 0) {
1512 geometry_handler.deleteGeometry(background_rectangle_ID);
1513 background_rectangle_ID = 0;
1514 }
1515
1516 // Remove background sky geometry if it exists
1517 for (size_t id: background_sky_IDs) {
1518 geometry_handler.deleteGeometry(id);
1519 }
1520 background_sky_IDs.clear();
1521}
1522
1524 backgroundColor = color;
1525 background_is_transparent = false;
1526
1527 // Remove background rectangle if it exists
1528 removeBackgroundRectangle();
1529
1530 // Restore watermark if it was visible before transparent background was enabled
1531 if (watermark_was_visible_before_transparent) {
1532 this->showWatermark();
1533 watermark_was_visible_before_transparent = false; // Reset the flag
1534 }
1535}
1536
1537void Visualizer::setBackgroundGradient() {
1538 background_is_transparent = false;
1539
1540 // Remove any existing background rectangle
1541 removeBackgroundRectangle();
1542
1543 // Create full-screen rectangle with gradient texture as background
1544 // Define rectangle vertices in normalized window coordinates
1545 std::vector<helios::vec3> vertices = {
1546 helios::make_vec3(0.f, 0.f, 0.99f), // Bottom-left
1547 helios::make_vec3(1.f, 0.f, 0.99f), // Bottom-right
1548 helios::make_vec3(1.f, 1.f, 0.99f), // Top-right
1549 helios::make_vec3(0.f, 1.f, 0.99f) // Top-left
1550 };
1551
1552 // Use standard UV coordinates - gradient texture will stretch to fill window
1553 std::vector<helios::vec2> uvs = {
1554 helios::make_vec2(0.f, 0.f), // Bottom-left
1555 helios::make_vec2(1.f, 0.f), // Bottom-right
1556 helios::make_vec2(1.f, 1.f), // Top-right
1557 helios::make_vec2(0.f, 1.f) // Top-left
1558 };
1559
1560 // Add the textured rectangle as background
1561 background_rectangle_ID = addRectangleByVertices(vertices, "plugins/visualizer/textures/gradient_background.jpg", uvs, COORDINATES_WINDOW_NORMALIZED);
1562}
1563
1565 background_is_transparent = true;
1566
1567 // Save watermark visibility state before hiding it (so we can restore it if user switches back to solid color)
1568 watermark_was_visible_before_transparent = isWatermarkVisible;
1569 this->hideWatermark();
1570
1571 // Remove any existing background rectangle
1572 removeBackgroundRectangle();
1573
1574 // Create full-screen rectangle with checkerboard texture as background
1575 // Define rectangle vertices in normalized window coordinates
1576 std::vector<helios::vec3> vertices = {
1577 helios::make_vec3(0.f, 0.f, 0.99f), // Bottom-left
1578 helios::make_vec3(1.f, 0.f, 0.99f), // Bottom-right
1579 helios::make_vec3(1.f, 1.f, 0.99f), // Top-right
1580 helios::make_vec3(0.f, 1.f, 0.99f) // Top-left
1581 };
1582
1583 // Calculate aspect ratio and adjust UV coordinates to maintain square checkerboard pattern
1584 float aspect_ratio = static_cast<float>(Wframebuffer) / static_cast<float>(Hframebuffer);
1585 std::vector<helios::vec2> uvs;
1586 if (aspect_ratio > 1.f) {
1587 // Window is wider than tall - stretch UV in x-direction
1588 uvs = {helios::make_vec2(0.f, 0.f), helios::make_vec2(aspect_ratio, 0.f), helios::make_vec2(aspect_ratio, 1.f), helios::make_vec2(0.f, 1.f)};
1589 } else {
1590 // Window is taller than wide - stretch UV in y-direction
1591 uvs = {helios::make_vec2(0.f, 0.f), helios::make_vec2(1.f, 0.f), helios::make_vec2(1.f, 1.f / aspect_ratio), helios::make_vec2(0.f, 1.f / aspect_ratio)};
1592 }
1593
1594 // Add the textured rectangle as background
1595 background_rectangle_ID = addRectangleByVertices(vertices, "plugins/visualizer/textures/transparent.jpg", uvs, COORDINATES_WINDOW_NORMALIZED);
1596}
1597
1598void Visualizer::setBackgroundImage(const char *texture_file) {
1599 background_is_transparent = false;
1600
1601 // Remove any existing background rectangle
1602 removeBackgroundRectangle();
1603
1604 // Create full-screen rectangle with custom texture as background
1605 // Define rectangle vertices in normalized window coordinates
1606 std::vector<helios::vec3> vertices = {
1607 helios::make_vec3(0.f, 0.f, 0.99f), // Bottom-left
1608 helios::make_vec3(1.f, 0.f, 0.99f), // Bottom-right
1609 helios::make_vec3(1.f, 1.f, 0.99f), // Top-right
1610 helios::make_vec3(0.f, 1.f, 0.99f) // Top-left
1611 };
1612
1613 // Use standard UV coordinates - texture will stretch to fill window
1614 std::vector<helios::vec2> uvs = {
1615 helios::make_vec2(0.f, 0.f), // Bottom-left
1616 helios::make_vec2(1.f, 0.f), // Bottom-right
1617 helios::make_vec2(1.f, 1.f), // Top-right
1618 helios::make_vec2(0.f, 1.f) // Top-left
1619 };
1620
1621 // Add the textured rectangle as background
1622 background_rectangle_ID = addRectangleByVertices(vertices, texture_file, uvs, COORDINATES_WINDOW_NORMALIZED);
1623
1624 // Restore watermark if it was visible before transparent background was enabled
1625 if (watermark_was_visible_before_transparent) {
1626 this->showWatermark();
1627 watermark_was_visible_before_transparent = false; // Reset the flag
1628 }
1629}
1630
1631void Visualizer::setBackgroundSkyTexture(const char *texture_file, uint Ndivisions) {
1632 using namespace helios;
1633
1634 background_is_transparent = false;
1635
1636 // Remove any existing background (rectangle or sky)
1637 removeBackgroundRectangle();
1638
1639 // Use default sky texture if none specified
1640 std::string texture_path;
1641 if (texture_file == nullptr) {
1642 texture_path = resolvePluginAsset("visualizer", "textures/SkyDome_clouds.jpg").string();
1643 } else {
1644 texture_path = texture_file;
1645 }
1646
1647 // Load the texture
1648 uint textureID = registerTextureImage(texture_path.c_str());
1649
1650 // Create sky sphere geometry centered at origin
1651 // The shader will handle making it appear infinitely distant
1652 float radius = 1.0f; // Unit sphere - actual size doesn't matter due to shader transformations
1653
1654 // Full sphere from nadir (-π/2) to zenith (+π/2)
1655 float thetaStart = -0.5f * M_PI;
1656 float dtheta = (0.5f * M_PI - thetaStart) / float(Ndivisions - 1);
1657 float dphi = 2.f * M_PI / float(Ndivisions - 1);
1658
1659 vec3 center = make_vec3(0, 0, 0);
1660
1661 // Reserve space for all triangles
1662 background_sky_IDs.reserve(2u * Ndivisions * Ndivisions);
1663
1664 // Top cap
1665 for (int j = 0; j < scast<int>(Ndivisions - 1); j++) {
1666 vec3 cart = sphere2cart(make_SphericalCoord(1.f, 0.5f * M_PI, 0));
1667 vec3 v0 = center + radius * cart;
1668 cart = sphere2cart(make_SphericalCoord(1.f, 0.5f * M_PI - dtheta, float(j + 1) * dphi));
1669 vec3 v1 = center + radius * cart;
1670 cart = sphere2cart(make_SphericalCoord(1.f, 0.5f * M_PI - dtheta, float(j) * dphi));
1671 vec3 v2 = center + radius * cart;
1672
1673 vec3 n0 = v0 - center;
1674 n0.normalize();
1675 vec3 n1 = v1 - center;
1676 n1.normalize();
1677 vec3 n2 = v2 - center;
1678 n2.normalize();
1679
1680 vec2 uv0 = make_vec2(1.f - atan2f(sinf((float(j) + 0.5f) * dphi), -cosf((float(j) + 0.5f) * dphi)) / (2.f * M_PI) - 0.5f, n0.z * 0.5f + 0.5f);
1681 vec2 uv1 = make_vec2(1.f - atan2f(n1.x, -n1.y) / (2.f * M_PI) - 0.5f, n1.z * 0.5f + 0.5f);
1682 vec2 uv2 = make_vec2(1.f - atan2f(n2.x, -n2.y) / (2.f * M_PI) - 0.5f, n2.z * 0.5f + 0.5f);
1683
1684 // Fix seam at wrap boundary
1685 if (j == scast<int>(Ndivisions - 2)) {
1686 uv2.x = 1;
1687 }
1688
1689 std::vector<vec3> vertices = {v0, v1, v2};
1690 std::vector<vec2> uvs = {uv0, uv1, uv2};
1691
1692 size_t UUID = geometry_handler.sampleUUID();
1693 geometry_handler.addGeometry(UUID, GeometryHandler::GEOMETRY_TYPE_TRIANGLE, vertices, make_RGBAcolor(1, 1, 1, 1), uvs, textureID, false, false, 1, true, false, true, 0);
1694 background_sky_IDs.push_back(UUID);
1695 }
1696
1697 // Main body
1698 for (int i = 1; i < scast<int>(Ndivisions - 1); i++) {
1699 for (int j = 0; j < scast<int>(Ndivisions - 1); j++) {
1700 vec3 cart = sphere2cart(make_SphericalCoord(1.f, 0.5f * M_PI - float(i) * dtheta, float(j) * dphi));
1701 vec3 v0 = center + radius * cart;
1702 cart = sphere2cart(make_SphericalCoord(1.f, 0.5f * M_PI - float(i + 1) * dtheta, float(j) * dphi));
1703 vec3 v1 = center + radius * cart;
1704 cart = sphere2cart(make_SphericalCoord(1.f, 0.5f * M_PI - float(i) * dtheta, float(j + 1) * dphi));
1705 vec3 v2 = center + radius * cart;
1706 cart = sphere2cart(make_SphericalCoord(1.f, 0.5f * M_PI - float(i + 1) * dtheta, float(j + 1) * dphi));
1707 vec3 v3 = center + radius * cart;
1708
1709 vec3 n0 = v0 - center;
1710 n0.normalize();
1711 vec3 n1 = v1 - center;
1712 n1.normalize();
1713 vec3 n2 = v2 - center;
1714 n2.normalize();
1715 vec3 n3 = v3 - center;
1716 n3.normalize();
1717
1718 vec2 uv0 = make_vec2(1.f - atan2f(n0.x, -n0.y) / (2.f * M_PI) - 0.5f, n0.z * 0.5f + 0.5f);
1719 vec2 uv1 = make_vec2(1.f - atan2f(n1.x, -n1.y) / (2.f * M_PI) - 0.5f, n1.z * 0.5f + 0.5f);
1720 vec2 uv2 = make_vec2(1.f - atan2f(n2.x, -n2.y) / (2.f * M_PI) - 0.5f, n2.z * 0.5f + 0.5f);
1721 vec2 uv3 = make_vec2(1.f - atan2f(n3.x, -n3.y) / (2.f * M_PI) - 0.5f, n3.z * 0.5f + 0.5f);
1722
1723 // Fix seam at wrap boundary
1724 if (j == scast<int>(Ndivisions - 2)) {
1725 uv2.x = 1;
1726 uv3.x = 1;
1727 }
1728
1729 // First triangle
1730 {
1731 std::vector<vec3> vertices = {v0, v1, v2};
1732 std::vector<vec2> uvs = {uv0, uv1, uv2};
1733 size_t UUID = geometry_handler.sampleUUID();
1734 geometry_handler.addGeometry(UUID, GeometryHandler::GEOMETRY_TYPE_TRIANGLE, vertices, make_RGBAcolor(1, 1, 1, 1), uvs, textureID, false, false, 1, true, false, true, 0);
1735 background_sky_IDs.push_back(UUID);
1736 }
1737
1738 // Second triangle
1739 {
1740 std::vector<vec3> vertices = {v2, v1, v3};
1741 std::vector<vec2> uvs = {uv2, uv1, uv3};
1742 size_t UUID = geometry_handler.sampleUUID();
1743 geometry_handler.addGeometry(UUID, GeometryHandler::GEOMETRY_TYPE_TRIANGLE, vertices, make_RGBAcolor(1, 1, 1, 1), uvs, textureID, false, false, 1, true, false, true, 0);
1744 background_sky_IDs.push_back(UUID);
1745 }
1746 }
1747 }
1748
1749 // Restore watermark if it was visible before transparent background was enabled
1750 if (watermark_was_visible_before_transparent) {
1751 this->showWatermark();
1752 watermark_was_visible_before_transparent = false;
1753 }
1754}
1755
1757 isWatermarkVisible = false;
1758 if (watermark_ID != 0) {
1759 geometry_handler.deleteGeometry(watermark_ID);
1760 watermark_ID = 0;
1761 }
1762}
1763
1765 isWatermarkVisible = true;
1767}
1768
1770 navigation_gizmo_enabled = false;
1771 hovered_gizmo_bubble = -1; // Reset hover state
1772 if (!navigation_gizmo_IDs.empty()) {
1773 geometry_handler.deleteGeometry(navigation_gizmo_IDs);
1774 navigation_gizmo_IDs.clear();
1775 }
1776}
1777
1779 navigation_gizmo_enabled = true;
1780 updateNavigationGizmo();
1781}
1782
1783bool Visualizer::testGizmoBubbleHit(const helios::vec2 &normalized_pos, int bubble_index) const {
1784 if (!navigation_gizmo_enabled || navigation_gizmo_IDs.empty() || bubble_index < 0 || bubble_index > 2) {
1785 return false;
1786 }
1787
1788 // Get the bubble vertices directly from geometry (in normalized window coordinates [0,1])
1789 // Bubbles are at indices 3, 4, 5 (after the 3 axis lines at 0, 1, 2)
1790 size_t bubble_id = navigation_gizmo_IDs[3 + bubble_index];
1791 std::vector<helios::vec3> vertices = getGeometryVertices(bubble_id);
1792
1793 if (vertices.size() != 4) {
1794 return false;
1795 }
1796
1797 // Calculate bounding box from vertices
1798 float min_x = std::min({vertices[0].x, vertices[1].x, vertices[2].x, vertices[3].x});
1799 float max_x = std::max({vertices[0].x, vertices[1].x, vertices[2].x, vertices[3].x});
1800 float min_y = std::min({vertices[0].y, vertices[1].y, vertices[2].y, vertices[3].y});
1801 float max_y = std::max({vertices[0].y, vertices[1].y, vertices[2].y, vertices[3].y});
1802
1803 // Test if click position is within bubble bounds
1804 return (normalized_pos.x >= min_x && normalized_pos.x <= max_x && normalized_pos.y >= min_y && normalized_pos.y <= max_y);
1805}
1806
1807void Visualizer::handleGizmoClick(double screen_x, double screen_y) {
1808 if (!navigation_gizmo_enabled) {
1809 return;
1810 }
1811
1812 // Convert screen coordinates to normalized window coordinates (0 to 1)
1813 // Note: Screen Y is top-to-bottom, but normalized window Y is bottom-to-top
1814 float normalized_x = static_cast<float>(screen_x) / static_cast<float>(Wdisplay);
1815 float normalized_y = 1.0f - (static_cast<float>(screen_y) / static_cast<float>(Hdisplay));
1816 helios::vec2 normalized_pos = make_vec2(normalized_x, normalized_y);
1817
1818 // Test each axis bubble for hit
1819 for (int axis = 0; axis < 3; axis++) {
1820 if (testGizmoBubbleHit(normalized_pos, axis)) {
1821 reorientCameraToAxis(axis);
1822 break; // Only process the first hit
1823 }
1824 }
1825}
1826
1827void Visualizer::handleGizmoHover(double screen_x, double screen_y) {
1828 if (!navigation_gizmo_enabled) {
1829 return;
1830 }
1831
1832 // Don't test for hover if geometry doesn't exist yet or is being updated
1833 // Expected size: 3 lines + 3 bubbles = 6 elements
1834 if (navigation_gizmo_IDs.size() != 6) {
1835 return;
1836 }
1837
1838 // Convert screen coordinates to normalized window coordinates (0 to 1)
1839 // Note: Screen Y is top-to-bottom, but normalized window Y is bottom-to-top
1840 float normalized_x = static_cast<float>(screen_x) / static_cast<float>(Wdisplay);
1841 float normalized_y = 1.0f - (static_cast<float>(screen_y) / static_cast<float>(Hdisplay));
1842 helios::vec2 normalized_pos = make_vec2(normalized_x, normalized_y);
1843
1844 // Test each axis bubble for hover
1845 int new_hovered_bubble = -1;
1846 for (int axis = 0; axis < 3; axis++) {
1847 if (testGizmoBubbleHit(normalized_pos, axis)) {
1848 new_hovered_bubble = axis;
1849 break; // Only process the first hit
1850 }
1851 }
1852
1853 // Only update if hover state changed
1854 if (new_hovered_bubble != hovered_gizmo_bubble) {
1855 hovered_gizmo_bubble = new_hovered_bubble;
1856 updateNavigationGizmo();
1857 // Transfer updated geometry to GPU immediately
1858 transferBufferData();
1859 }
1860}
1861
1862void Visualizer::reorientCameraToAxis(int axis_index) {
1863 if (axis_index < 0 || axis_index > 2) {
1864 return;
1865 }
1866
1867 // Calculate current camera radius (distance from eye to lookat center)
1868 helios::SphericalCoord current_spherical = cart2sphere(camera_eye_location - camera_lookat_center);
1869 float radius = current_spherical.radius;
1870
1871 // Define standard viewing angles for each axis
1872 // axis_index: 0 = X, 1 = Y, 2 = Z
1873 helios::SphericalCoord new_spherical;
1874
1875 switch (axis_index) {
1876 case 0: // +X axis - swap azimuth with Y since azimuth=0 points along Y in Helios
1877 new_spherical = make_SphericalCoord(radius, 0.0f, 0.5f * M_PI);
1878 break;
1879 case 1: // +Y axis - swap azimuth with X since azimuth=0 points along Y in Helios
1880 new_spherical = make_SphericalCoord(radius, 0.0f, 0.0f);
1881 break;
1882 case 2: // +Z axis (top view)
1883 new_spherical = make_SphericalCoord(radius, 0.49f * M_PI, 0.0f); // Slightly below 90 degrees to avoid singularity
1884 break;
1885 }
1886
1887 // Set the new camera position
1888 setCameraPosition(new_spherical, camera_lookat_center);
1889}
1890
1891bool Visualizer::cameraHasChanged() const {
1892 constexpr float epsilon = 1e-6f;
1893 return (camera_eye_location - previous_camera_eye_location).magnitude() > epsilon || (camera_lookat_center - previous_camera_lookat_center).magnitude() > epsilon;
1894}
1895
1896void Visualizer::updatePerspectiveTransformation(bool shadow) {
1897 float dist = glm::distance(glm_vec3(camera_lookat_center), glm_vec3(camera_eye_location));
1898 float nearPlane = std::max(0.1f, 0.05f * dist); // avoid 0
1899 if (shadow) {
1900 float farPlane = std::max(5.f * camera_eye_location.z, 2.0f * dist);
1901 cameraProjectionMatrix = glm::perspective(glm::radians(camera_FOV), float(Wframebuffer) / float(Hframebuffer), nearPlane, farPlane);
1902 } else {
1903 cameraProjectionMatrix = glm::infinitePerspective(glm::radians(camera_FOV), float(Wframebuffer) / float(Hframebuffer), nearPlane);
1904 }
1905 cameraViewMatrix = glm::lookAt(glm_vec3(camera_eye_location), glm_vec3(camera_lookat_center), glm::vec3(0, 0, 1));
1906
1907 perspectiveTransformationMatrix = cameraProjectionMatrix * cameraViewMatrix;
1908}
1909
1910void Visualizer::updateCustomTransformation(const glm::mat4 &matrix) {
1911 customTransformationMatrix = matrix;
1912}
1913
1915 colorbar_flag = 2;
1916}
1917
1919 if (!colorbar_IDs.empty()) {
1920 geometry_handler.deleteGeometry(colorbar_IDs);
1921 colorbar_IDs.clear();
1922 }
1923 colorbar_flag = 1;
1924}
1925
1927 if (position.x < 0 || position.x > 1 || position.y < 0 || position.y > 1 || position.z < -1 || position.z > 1) {
1928 helios_runtime_error("ERROR (Visualizer::setColorbarPosition): position is out of range. Coordinates must be: 0<x<1, 0<y<1, -1<z<1.");
1929 }
1930 colorbar_position = position;
1931}
1932
1934 if (size.x < 0 || size.x > 1 || size.y < 0 || size.y > 1) {
1935 helios_runtime_error("ERROR (Visualizer::setColorbarSize): Size must be greater than 0 and less than the window size (i.e., 1).");
1936 }
1937 colorbar_size = size;
1938 // Store the intended aspect ratio (width/height) to maintain proportions across window aspect ratios
1939 if (size.y > 0) {
1940 colorbar_intended_aspect_ratio = size.x / size.y;
1941 } else {
1942 colorbar_intended_aspect_ratio = 0.1f; // Default thin bar aspect ratio
1943 }
1944}
1945
1946void Visualizer::setColorbarRange(float cmin, float cmax) {
1947 if (message_flag && cmin > cmax) {
1948 std::cerr << "WARNING (Visualizer::setColorbarRange): Maximum colorbar value must be greater than minimum value...Ignoring command." << std::endl;
1949 return;
1950 }
1951 colorbar_min = cmin;
1952 colorbar_max = cmax;
1953}
1954
1955void Visualizer::setColorbarTicks(const std::vector<float> &ticks) {
1956 // check that vector is not empty
1957 if (ticks.empty()) {
1958 helios_runtime_error("ERROR (Visualizer::setColorbarTicks): Colorbar ticks vector is empty.");
1959 }
1960
1961 // Check that ticks are monotonically increasing
1962 for (int i = 1; i < ticks.size(); i++) {
1963 if (ticks.at(i) <= ticks.at(i - 1)) {
1964 helios_runtime_error("ERROR (Visualizer::setColorbarTicks): Colorbar ticks must be monotonically increasing.");
1965 }
1966 }
1967
1968 // Check that ticks are within the range of colorbar values
1969 for (int i = ticks.size() - 1; i >= 0; i--) {
1970 if (ticks.at(i) < colorbar_min) {
1971 colorbar_min = ticks.at(i);
1972 }
1973 }
1974 for (float tick: ticks) {
1975 if (tick > colorbar_max) {
1976 colorbar_max = tick;
1977 }
1978 }
1979
1980 colorbar_ticks = ticks;
1981}
1982
1983double Visualizer::niceNumber(double value, bool round) {
1984 // Handle special cases
1985 if (value == 0.0 || !std::isfinite(value)) {
1986 return value;
1987 }
1988
1989 // Calculate the exponent (power of 10)
1990 double exp = std::floor(std::log10(std::fabs(value)));
1991 // Calculate the fraction (normalized value between 1 and 10)
1992 double frac = std::fabs(value) / std::pow(10.0, exp);
1993 double niceFrac;
1994
1995 if (round) {
1996 // Round to nearest nice number
1997 if (frac < 1.5) {
1998 niceFrac = 1.0;
1999 } else if (frac < 3.0) {
2000 niceFrac = 2.0;
2001 } else if (frac < 7.0) {
2002 niceFrac = 5.0;
2003 } else {
2004 niceFrac = 10.0;
2005 }
2006 } else {
2007 // Round up to next nice number
2008 if (frac <= 1.0) {
2009 niceFrac = 1.0;
2010 } else if (frac <= 2.0) {
2011 niceFrac = 2.0;
2012 } else if (frac <= 5.0) {
2013 niceFrac = 5.0;
2014 } else {
2015 niceFrac = 10.0;
2016 }
2017 }
2018
2019 // Restore the sign
2020 double result = niceFrac * std::pow(10.0, exp);
2021 return value < 0.0 ? -result : result;
2022}
2023
2024std::string Visualizer::formatTickLabel(double value, double spacing, bool isIntegerData) {
2025 std::ostringstream oss;
2026
2027 // Handle special values
2028 if (!std::isfinite(value)) {
2029 return "0";
2030 }
2031
2032 // For integer data, always format as integer (unless very large)
2033 if (isIntegerData) {
2034 // Use scientific notation for very large integers to save space
2035 if (std::fabs(value) >= 10000) {
2036 oss << std::scientific << std::setprecision(0) << value;
2037 return oss.str();
2038 }
2039 oss << static_cast<int>(std::round(value));
2040 return oss.str();
2041 }
2042
2043 // Determine if we should use scientific notation
2044 // Use scientific notation for large values (>=10000) or very small values
2045 bool useScientific = (std::fabs(value) >= 1e4 || (std::fabs(value) < 1e-3 && value != 0.0));
2046
2047 if (useScientific) {
2048 // Use scientific notation
2049 int decimalPlaces = std::max(0, static_cast<int>(-std::floor(std::log10(std::fabs(spacing)))));
2050 decimalPlaces = std::min(decimalPlaces, 6); // Cap at 6 decimal places
2051 oss << std::scientific << std::setprecision(decimalPlaces) << value;
2052 } else {
2053 // Use fixed-point notation
2054 // Calculate decimal places based on spacing
2055 int decimalPlaces;
2056 if (spacing >= 1.0) {
2057 decimalPlaces = 0;
2058 } else {
2059 decimalPlaces = std::max(0, static_cast<int>(-std::floor(std::log10(spacing))));
2060 decimalPlaces = std::min(decimalPlaces, 6); // Cap at 6 decimal places
2061 }
2062
2063 oss << std::fixed << std::setprecision(decimalPlaces) << value;
2064 }
2065
2066 return oss.str();
2067}
2068
2069std::vector<float> Visualizer::generateNiceTicks(float dataMin, float dataMax, bool isIntegerData, int targetTicks) {
2070 std::vector<float> ticks;
2071
2072 // Handle edge cases
2073 if (!std::isfinite(dataMin) || !std::isfinite(dataMax) || dataMax <= dataMin) {
2074 // Return default ticks
2075 ticks.push_back(dataMin);
2076 ticks.push_back(dataMax);
2077 return ticks;
2078 }
2079
2080 // Handle zero or very small range
2081 double range = dataMax - dataMin;
2082 if (range < 1e-10) {
2083 ticks.push_back(dataMin);
2084 return ticks;
2085 }
2086
2087 // Calculate nice range
2088 double niceRange = niceNumber(range / (targetTicks - 1), true);
2089
2090 // For integer data, ensure spacing is at least 1
2091 if (isIntegerData && niceRange < 1.0) {
2092 niceRange = 1.0;
2093 }
2094
2095 // Calculate nice bounds
2096 double graphMin = std::floor(dataMin / niceRange) * niceRange;
2097 double graphMax = std::ceil(dataMax / niceRange) * niceRange;
2098
2099 // For integer data, round to integers
2100 if (isIntegerData) {
2101 graphMin = std::floor(graphMin);
2102 graphMax = std::ceil(graphMax);
2103 niceRange = std::max(1.0, niceRange);
2104 }
2105
2106 // Generate tick positions
2107 // Use a small epsilon to handle floating-point precision issues
2108 double epsilon = niceRange * 0.5;
2109 for (double tick = graphMin; tick <= graphMax + epsilon; tick += niceRange) {
2110 double tickValue = tick;
2111
2112 // For integer data, round to nearest integer
2113 if (isIntegerData) {
2114 tickValue = std::round(tick);
2115 }
2116
2117 // Avoid duplicates due to floating-point precision
2118 if (ticks.empty() || std::fabs(tickValue - ticks.back()) > niceRange * 0.01) {
2119 ticks.push_back(static_cast<float>(tickValue));
2120 }
2121 }
2122
2123 // Ensure we have at least 2 ticks
2124 if (ticks.size() < 2) {
2125 ticks.clear();
2126 ticks.push_back(dataMin);
2127 ticks.push_back(dataMax);
2128 }
2129
2130 return ticks;
2131}
2132
2133void Visualizer::setColorbarTitle(const char *title) {
2134 colorbar_title = title;
2135}
2136
2138 if (font_size <= 0) {
2139 helios_runtime_error("ERROR (Visualizer::setColorbarFontSize): Font size must be greater than zero.");
2140 }
2141 colorbar_fontsize = font_size;
2142}
2143
2145 colorbar_fontcolor = fontcolor;
2146}
2147
2148void Visualizer::setColormap(Ctable colormap_name) {
2149 if (colormap_name == COLORMAP_HOT) {
2150 colormap_current = colormap_hot;
2151 } else if (colormap_name == COLORMAP_COOL) {
2152 colormap_current = colormap_cool;
2153 } else if (colormap_name == COLORMAP_LAVA) {
2154 colormap_current = colormap_lava;
2155 } else if (colormap_name == COLORMAP_RAINBOW) {
2156 colormap_current = colormap_rainbow;
2157 } else if (colormap_name == COLORMAP_PARULA) {
2158 colormap_current = colormap_parula;
2159 } else if (colormap_name == COLORMAP_GRAY) {
2160 colormap_current = colormap_gray;
2161 } else if (colormap_name == COLORMAP_LINES) {
2162 colormap_current = colormap_lines;
2163 } else if (colormap_name == COLORMAP_CUSTOM) {
2164 helios_runtime_error("ERROR (Visualizer::setColormap): Setting a custom colormap requires calling setColormap with additional arguments defining the colormap.");
2165 } else {
2166 helios_runtime_error("ERROR (Visualizer::setColormap): Invalid colormap.");
2167 }
2168}
2169
2170void Visualizer::setColormap(const std::vector<RGBcolor> &colors, const std::vector<float> &divisions) {
2171 if (colors.size() != divisions.size()) {
2172 helios_runtime_error("ERROR (Visualizer::setColormap): The number of colors must be equal to the number of divisions.");
2173 }
2174
2175 Colormap colormap_custom(colors, divisions, 100, 0, 1);
2176
2177 colormap_current = colormap_custom;
2178}
2179
2181 return colormap_current;
2182}
2183
2184glm::mat4 Visualizer::computeShadowDepthMVP() const {
2185 glm::vec3 lightDir = -glm::normalize(glm_vec3(light_direction));
2186
2187 const float margin = 0.01;
2188
2189 // Get the eight corners of the camera frustum in world space (NDC cube corners → clip → view → world)
2190
2191 // NDC cube
2192 static const std::array<glm::vec4, 8> ndcCorners = {glm::vec4(-1, -1, -1, 1), glm::vec4(+1, -1, -1, 1), glm::vec4(+1, +1, -1, 1), glm::vec4(-1, +1, -1, 1),
2193 glm::vec4(-1, -1, +1, 1), glm::vec4(+1, -1, +1, 1), glm::vec4(+1, +1, +1, 1), glm::vec4(-1, +1, +1, 1)};
2194
2195 glm::mat4 invCam = glm::inverse(this->perspectiveTransformationMatrix);
2196
2197 std::array<glm::vec3, 8> frustumWs;
2198 for (std::size_t i = 0; i < 8; i++) {
2199 glm::vec4 ws = invCam * ndcCorners[i];
2200 frustumWs[i] = glm::vec3(ws) / ws.w;
2201 }
2202
2203 // Build a light-view matrix (orthographic, directional light) We choose an arbitrary but stable "up" vector.
2204 glm::vec3 lightUp(0.0f, 1.0f, 0.0f);
2205 if (glm::abs(glm::dot(lightUp, lightDir)) > 0.9f) // almost collinear
2206 lightUp = glm::vec3(1, 0, 0);
2207
2208 // Position the "camera" that generates the shadow map so that every
2209 // frustum corner is in front of it. We place it on the negative light
2210 // direction, centered on the frustum’s centroid.
2211 glm::vec3 centroid(0);
2212 for (auto &c: frustumWs)
2213 centroid += c;
2214 centroid /= 8.0f;
2215
2216 glm::vec3 lightPos = centroid - lightDir * 100.0f; // 100 is arbitrary,
2217 // we will tighten z
2218 glm::mat4 lightView = glm::lookAt(lightPos, centroid, lightUp);
2219
2220 // Transform frustum corners to light space and find min/max extents
2221 glm::vec3 minL(std::numeric_limits<float>::infinity());
2222 glm::vec3 maxL(-std::numeric_limits<float>::infinity());
2223
2224 for (auto &c: frustumWs) {
2225 glm::vec3 p = glm::vec3(lightView * glm::vec4(c, 1));
2226 minL = glm::min(minL, p);
2227 maxL = glm::max(maxL, p);
2228 }
2229
2230 // Build orthographic projection that exactly fits the camera frustum and enlarge slightly to avoid clipping due to kernel offsets.
2231 glm::vec3 extent = maxL - minL;
2232 minL -= extent * margin;
2233 maxL += extent * margin;
2234
2235 float zNear = -maxL.z; // light space points toward -z
2236 float zFar = -minL.z;
2237
2238 glm::mat4 lightProj = glm::ortho(minL.x, maxL.x, minL.y, maxL.y, zNear, zFar);
2239
2240 // Transform into [0,1] texture space (bias matrix)
2241 const glm::mat4 bias(0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f);
2242
2243 return bias * lightProj * lightView;
2244}
2245
2246Visualizer::Texture::Texture(const std::string &texture_file, uint textureID, const helios::uint2 &maximum_texture_size, bool loadalphaonly) : texture_file(texture_file), glyph(), textureID(textureID) {
2247#ifdef HELIOS_DEBUG
2248 if (loadalphaonly) {
2249 assert(validateTextureFile(texture_file, true));
2250 } else {
2251 assert(validateTextureFile(texture_file));
2252 }
2253#endif
2254
2255 //--- Load the Texture ----//
2256
2257 if (loadalphaonly) {
2258 num_channels = 1;
2259 } else {
2260 num_channels = 4;
2261 }
2262
2263 std::vector<unsigned char> image_data;
2264
2265 if (texture_file.substr(texture_file.find_last_of('.') + 1) == "png") {
2266 read_png_file(texture_file.c_str(), image_data, texture_resolution.y, texture_resolution.x);
2267 } else { // JPEG
2268 read_JPEG_file(texture_file.c_str(), image_data, texture_resolution.y, texture_resolution.x);
2269 }
2270
2271 texture_data = std::move(image_data);
2272
2273 // If the texture image is too large, resize it
2274 if (texture_resolution.x > maximum_texture_size.x || texture_resolution.y > maximum_texture_size.y) {
2275 const uint2 new_texture_resolution(std::min(texture_resolution.x, maximum_texture_size.x), std::min(texture_resolution.y, maximum_texture_size.y));
2276 resizeTexture(new_texture_resolution);
2277 }
2278}
2279
2280Visualizer::Texture::Texture(const Glyph *glyph_ptr, uint textureID, const helios::uint2 &maximum_texture_size) : textureID(textureID) {
2281 assert(glyph_ptr != nullptr);
2282
2283 glyph = *glyph_ptr;
2284
2285 texture_resolution = glyph_ptr->size;
2286
2287 // Texture only has 1 channel, and contains transparency data
2288 texture_data.resize(texture_resolution.x * texture_resolution.y);
2289 for (int j = 0; j < texture_resolution.y; j++) {
2290 for (int i = 0; i < texture_resolution.x; i++) {
2291 texture_data[i + j * texture_resolution.x] = glyph_ptr->data.at(j).at(i);
2292 }
2293 }
2294
2295 // If the texture image is too large, resize it
2296 if (texture_resolution.x > maximum_texture_size.x || texture_resolution.y > maximum_texture_size.y) {
2297 const uint2 new_texture_resolution(std::min(texture_resolution.x, maximum_texture_size.x), std::min(texture_resolution.y, maximum_texture_size.y));
2298 resizeTexture(new_texture_resolution);
2299 }
2300
2301 num_channels = 1;
2302}
2303
2304Visualizer::Texture::Texture(const std::vector<unsigned char> &pixel_data, uint textureID, const helios::uint2 &image_resolution, const helios::uint2 &maximum_texture_size) : textureID(textureID) {
2305#ifdef HELIOS_DEBUG
2306 assert(pixel_data.size() == 4u * image_resolution.x * image_resolution.y);
2307#endif
2308
2309 texture_data = pixel_data;
2310 texture_resolution = image_resolution;
2311 num_channels = 4;
2312
2313 // If the texture image is too large, resize it
2314 if (texture_resolution.x > maximum_texture_size.x || texture_resolution.y > maximum_texture_size.y) {
2315 const uint2 new_texture_resolution(std::min(texture_resolution.x, maximum_texture_size.x), std::min(texture_resolution.y, maximum_texture_size.y));
2316 resizeTexture(new_texture_resolution);
2317 }
2318}
2319
2320void Visualizer::Texture::resizeTexture(const helios::uint2 &new_image_resolution) {
2321 int old_width = texture_resolution.x;
2322 int old_height = texture_resolution.y;
2323 int new_width = new_image_resolution.x;
2324 int new_height = new_image_resolution.y;
2325
2326 std::vector<unsigned char> new_data(new_width * new_height * num_channels);
2327
2328 // map each new pixel to a floating-point src coordinate
2329 float x_ratio = scast<float>(old_width) / scast<float>(new_width);
2330 float y_ratio = scast<float>(old_height) / scast<float>(new_height);
2331
2332 for (int y = 0; y < new_height; ++y) {
2333 float srcY = y * y_ratio;
2334 int y0 = std::min(scast<int>(std::floor(srcY)), old_height - 1);
2335 int y1 = std::min(y0 + 1, old_height - 1);
2336 float dy = srcY - y0;
2337
2338 for (int x = 0; x < new_width; ++x) {
2339 float srcX = x * x_ratio;
2340 int x0 = std::min(scast<int>(std::floor(srcX)), old_width - 1);
2341 int x1 = std::min(x0 + 1, old_width - 1);
2342 float dx = srcX - x0;
2343
2344 // for each channel, fetch 4 neighbors and lerp
2345 for (int c = 0; c < num_channels; ++c) {
2346 float p00 = texture_data[(y0 * old_width + x0) * num_channels + c];
2347 float p10 = texture_data[(y0 * old_width + x1) * num_channels + c];
2348 float p01 = texture_data[(y1 * old_width + x0) * num_channels + c];
2349 float p11 = texture_data[(y1 * old_width + x1) * num_channels + c];
2350
2351 float top = p00 * (1.f - dx) + p10 * dx;
2352 float bottom = p01 * (1.f - dx) + p11 * dx;
2353 float value = top * (1.f - dy) + bottom * dy;
2354
2355 new_data[(y * new_width + x) * num_channels + c] = scast<unsigned char>(clamp(std::round(value + 0.5f), 0.f, 255.f));
2356 }
2357 }
2358 }
2359
2360 texture_data = std::move(new_data);
2361 texture_resolution = new_image_resolution;
2362}
2363
2364
2365uint Visualizer::registerTextureImage(const std::string &texture_file) {
2366#ifdef HELIOS_DEBUG
2367 // assert( validateTextureFile(texture_file) );
2368#endif
2369
2370 for (const auto &[textureID, texture]: texture_manager) {
2371 if (texture.texture_file == texture_file) {
2372 // if it does, return its texture ID
2373 return textureID;
2374 }
2375 }
2376
2377 const uint textureID = texture_manager.size();
2378
2379 texture_manager.try_emplace(textureID, texture_file, textureID, this->maximum_texture_size, false);
2380 textures_dirty = true;
2381
2382 return textureID;
2383}
2384
2385uint Visualizer::registerTextureImage(const std::vector<unsigned char> &texture_data, const helios::uint2 &image_resolution) {
2386#ifdef HELIOS_DEBUG
2387 assert(!texture_data.empty() && texture_data.size() == 4 * image_resolution.x * image_resolution.y);
2388#endif
2389
2390 const uint textureID = texture_manager.size();
2391
2392 texture_manager.try_emplace(textureID, texture_data, textureID, image_resolution, this->maximum_texture_size);
2393 textures_dirty = true;
2394
2395 return textureID;
2396}
2397
2398uint Visualizer::registerTextureTransparencyMask(const std::string &texture_file) {
2399#ifdef HELIOS_DEBUG
2400 assert(validateTextureFile(texture_file));
2401#endif
2402
2403 for (const auto &[textureID, texture]: texture_manager) {
2404 if (texture.texture_file == texture_file) {
2405 // if it does, return its texture ID
2406 return textureID;
2407 }
2408 }
2409
2410 const uint textureID = texture_manager.size();
2411
2412 texture_manager.try_emplace(textureID, texture_file, textureID, this->maximum_texture_size, true);
2413 textures_dirty = true;
2414
2415 return textureID;
2416}
2417
2418uint Visualizer::registerTextureGlyph(const Glyph *glyph) {
2419
2420 const uint textureID = texture_manager.size();
2421
2422 texture_manager.try_emplace(textureID, glyph, textureID, this->maximum_texture_size);
2423 textures_dirty = true;
2424
2425 return textureID;
2426}
2427
2428helios::uint2 Visualizer::getTextureResolution(uint textureID) const {
2429 if (texture_manager.find(textureID) == texture_manager.end()) {
2430 }
2431 return texture_manager.at(textureID).texture_resolution;
2432}
2433
2434bool validateTextureFile(const std::string &texture_file, bool pngonly) {
2435 const std::filesystem::path p(texture_file);
2436
2437 // Check that the file exists and is a regular file
2438 if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p)) {
2439 return false;
2440 }
2441
2442 // Extract and lowercase the extension
2443 std::string ext = p.extension().string();
2444 std::transform(ext.begin(), ext.end(), ext.begin(), [](const unsigned char c) { return scast<char>(std::tolower(c)); });
2445
2446 // Verify it's .png, .jpg or .jpeg
2447 if (pngonly) {
2448 if (ext != ".png") {
2449 return false;
2450 }
2451 } else {
2452 if (ext != ".png" && ext != ".jpg" && ext != ".jpeg") {
2453 return false;
2454 }
2455 }
2456
2457 return true;
2458}
2459
2461 return window;
2462}
2463
2464std::vector<uint> Visualizer::getFrameBufferSize() const {
2465 return {Wframebuffer, Hframebuffer};
2466}
2467
2468void Visualizer::setFrameBufferSize(int width, int height) {
2469 Wframebuffer = width;
2470 Hframebuffer = height;
2471}
2472
2474 return backgroundColor;
2475}
2476
2477Shader Visualizer::getPrimaryShader() const {
2478 return primaryShader;
2479}
2480
2481std::vector<helios::vec3> Visualizer::getCameraPosition() const {
2482 return {camera_lookat_center, camera_eye_location};
2483}
2484
2486 return perspectiveTransformationMatrix;
2487}
2488
2489glm::mat4 Visualizer::getViewMatrix() const {
2490 vec3 forward = camera_lookat_center - camera_eye_location;
2491 forward = forward.normalize();
2492
2493 vec3 right = cross(vec3(0, 0, 1), forward);
2494 right = right.normalize();
2495
2496 vec3 up = cross(forward, right);
2497 up = up.normalize();
2498
2499 glm::vec3 camera_pos{camera_eye_location.x, camera_eye_location.y, camera_eye_location.z};
2500 glm::vec3 lookat_pos{camera_lookat_center.x, camera_lookat_center.y, camera_lookat_center.z};
2501 glm::vec3 up_vec{up.x, up.y, up.z};
2502
2503 return glm::lookAt(camera_pos, lookat_pos, up_vec);
2504}
2505
2506Visualizer::LightingModel Visualizer::getPrimaryLightingModel() {
2507 return primaryLightingModel;
2508}
2509
2510uint Visualizer::getDepthTexture() const {
2511 return depthTexture;
2512}