diff --git a/CHANGELOG b/CHANGELOG
index 46abcd48..ace83b98 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -9,6 +9,14 @@
https://glvis.org
+Version 4.5.1 (development)
+===========================
+- Added optional order-independent transparency (OIT) rendering for translucent
+ objects, toggled with 'Alt+o'. OIT is enabled only when supported by the
+ current OpenGL/WebGL context (requires float color-buffer render targets) and
+ falls back to legacy blending otherwise.
+
+
Version 4.5 released on Feb 6, 2026
===================================
diff --git a/README.md b/README.md
index 2a78818e..5e6d775d 100644
--- a/README.md
+++ b/README.md
@@ -177,6 +177,7 @@ Key commands
- 1, 2, 3, 4, 5, 6, 7, 8, 9 – Manual rotation along coordinate axes
- Alt + a – Set axes number format
- Alt + c – Set colorbar number format
+- Alt + o – Toggle order-independent transparency (OIT) for translucent objects (when supported)
- Ctrl + ←, →, ↑, ↓ – Translate the viewpoint
- Ctrl + o – Toggle an element ordering curve
- n / N – Cycle through numbering: `None` → `Elements` → `Edges` → `Vertices` → `DOFs`
diff --git a/lib/gl/renderer.cpp b/lib/gl/renderer.cpp
index bb601a54..0a1cbfeb 100644
--- a/lib/gl/renderer.cpp
+++ b/lib/gl/renderer.cpp
@@ -10,12 +10,23 @@
// CONTRIBUTING.md for details.
#include "renderer.hpp"
+#include "renderer_core.hpp"
namespace gl3
{
using namespace resource;
+namespace
+{
+const std::string kOITFullscreenVs =
+#include "shaders/oit_fullscreen.vert"
+ ;
+const std::string kOITFinalizeFs =
+#include "shaders/oit_finalize.frag"
+ ;
+}
+
// Beginning in OpenGL 3.0, there were two changes in texture format support:
// - The older single-channel internal format GL_ALPHA was deprecated in favor
// of GL_RED
@@ -108,6 +119,234 @@ void MeshRenderer::init()
#endif
}
+bool MeshRenderer::canUseOIT(const RenderQueue& queue) const
+{
+ if (!oit_enable || !device || !feat_use_fbo_antialias)
+ {
+ return false;
+ }
+ if (device->getType() != GLDevice::CORE_DEVICE)
+ {
+ return false;
+ }
+ return std::any_of(queue.begin(), queue.end(),
+ [](const RenderQueue::value_type& q)
+ {
+ return q.first.contains_translucent;
+ });
+}
+
+bool MeshRenderer::ensureOITTargets(int width, int height)
+{
+ if (!oit_support_checked)
+ {
+ oit_support = probeOITSupport();
+ oit_support_checked = true;
+ if (!oit_support)
+ {
+ std::cerr <<
+ "OIT: Disabled (float color-buffer render targets are not supported "
+ "by this OpenGL/WebGL context)." << std::endl;
+ }
+ }
+ if (!oit_support)
+ {
+ return false;
+ }
+
+ if (width <= 0 || height <= 0)
+ {
+ return false;
+ }
+
+ const bool size_changed = (width != oit_width) || (height != oit_height);
+ const bool msaa_changed = (oit_msaa_samples != (msaa_enable ? msaa_samples :
+ 0));
+ const bool need_realloc =
+ size_changed || msaa_changed ||
+ !oit_scene_color_tex || !oit_scene_depth_rb || !oit_scene_fb ||
+ !oit_accum_tex || !oit_reveal_tex || !oit_accum_fb || !oit_reveal_fb;
+
+ if (!need_realloc)
+ {
+ return true;
+ }
+
+ oit_width = width;
+ oit_height = height;
+
+ auto createTexture2D = [&](resource::TextureHandle& tex,
+ GLint internal_fmt,
+ GLenum fmt,
+ GLenum type,
+ GLint min_filter,
+ GLint mag_filter) -> void
+ {
+ GLuint tex_id = 0;
+ glGenTextures(1, &tex_id);
+ tex = TextureHandle(tex_id);
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexImage2D(GL_TEXTURE_2D, 0, internal_fmt, width, height, 0, fmt,
+ type, nullptr);
+ glBindTexture(GL_TEXTURE_2D, 0);
+ };
+
+ auto createDepthRenderbuffer = [&](resource::RenderBufHandle& rb,
+ GLenum depth_fmt) -> void
+ {
+ GLuint rb_id = 0;
+ glGenRenderbuffers(1, &rb_id);
+ rb = RenderBufHandle(rb_id);
+ glBindRenderbuffer(GL_RENDERBUFFER, rb);
+ glRenderbufferStorage(GL_RENDERBUFFER, depth_fmt, width, height);
+ glBindRenderbuffer(GL_RENDERBUFFER, 0);
+ };
+
+ auto createFramebuffer = [&](
+ resource::FBOHandle& fb,
+ const resource::TextureHandle& color_tex,
+ const resource::RenderBufHandle& depth_rb) -> bool
+ {
+ GLuint fb_id = 0;
+ glGenFramebuffers(1, &fb_id);
+ fb = FBOHandle(fb_id);
+ glBindFramebuffer(GL_FRAMEBUFFER, fb);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+ color_tex, 0);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+ GL_RENDERBUFFER, depth_rb);
+ const bool ok = (glCheckFramebufferStatus(GL_FRAMEBUFFER)
+ == GL_FRAMEBUFFER_COMPLETE);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ return ok;
+ };
+
+ // Scene color target (opaque render target)
+ createTexture2D(oit_scene_color_tex, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE,
+ GL_LINEAR, GL_LINEAR);
+
+ // Shared depth buffer for opaque + OIT passes
+ createDepthRenderbuffer(oit_scene_depth_rb, GL_DEPTH_COMPONENT24);
+
+ // Scene framebuffer
+ if (!createFramebuffer(oit_scene_fb, oit_scene_color_tex, oit_scene_depth_rb))
+ {
+ return false;
+ }
+
+ // Accumulation + revealage textures
+#ifdef __EMSCRIPTEN__
+ const GLenum kOITAccumType = GL_HALF_FLOAT;
+#else
+ const GLenum kOITAccumType = GL_FLOAT;
+#endif
+ createTexture2D(oit_accum_tex, GL_RGBA16F, GL_RGBA, kOITAccumType,
+ GL_NEAREST, GL_NEAREST);
+ createTexture2D(oit_reveal_tex, GL_R8, GL_RED, GL_UNSIGNED_BYTE,
+ GL_NEAREST, GL_NEAREST);
+
+ // Accumulation framebuffer
+ if (!createFramebuffer(oit_accum_fb, oit_accum_tex, oit_scene_depth_rb))
+ {
+ return false;
+ }
+
+ // Revealage framebuffer
+ if (!createFramebuffer(oit_reveal_fb, oit_reveal_tex, oit_scene_depth_rb))
+ {
+ return false;
+ }
+
+ // Optional MSAA framebuffer for the opaque pass
+ oit_msaa_fb = FBOHandle(0);
+ oit_msaa_samples = 0;
+ if (msaa_enable && msaa_samples > 0)
+ {
+ GLuint color_rb = 0, depth_rb = 0, fb_id = 0;
+ glGenRenderbuffers(1, &color_rb);
+ glGenRenderbuffers(1, &depth_rb);
+ oit_msaa_color_rb = RenderBufHandle(color_rb);
+ oit_msaa_depth_rb = RenderBufHandle(depth_rb);
+
+ glGenFramebuffers(1, &fb_id);
+ oit_msaa_fb = FBOHandle(fb_id);
+
+ glBindRenderbuffer(GL_RENDERBUFFER, oit_msaa_color_rb);
+ glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa_samples,
+ GL_RGBA8, width, height);
+ glBindRenderbuffer(GL_RENDERBUFFER, oit_msaa_depth_rb);
+ glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa_samples,
+ GL_DEPTH_COMPONENT24, width, height);
+ glBindRenderbuffer(GL_RENDERBUFFER, 0);
+
+ glBindFramebuffer(GL_FRAMEBUFFER, oit_msaa_fb);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_RENDERBUFFER, oit_msaa_color_rb);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+ GL_RENDERBUFFER, oit_msaa_depth_rb);
+ if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
+ {
+ oit_msaa_fb = FBOHandle(0);
+ oit_msaa_samples = 0;
+ }
+ else
+ {
+ oit_msaa_samples = msaa_samples;
+ }
+ }
+
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ return true;
+}
+
+bool MeshRenderer::probeOITSupport()
+{
+ // Probe whether GL_RGBA16F textures are color-renderable in this context.
+ // This is required for the OIT accumulation buffer.
+ GLuint tex_id = 0;
+ glGenTextures(1, &tex_id);
+ TextureHandle tex(tex_id);
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+#ifdef __EMSCRIPTEN__
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 1, 1, 0, GL_RGBA,
+ GL_HALF_FLOAT, nullptr);
+#else
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 1, 1, 0, GL_RGBA,
+ GL_FLOAT, nullptr);
+#endif
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ GLuint depth_id = 0;
+ glGenRenderbuffers(1, &depth_id);
+ RenderBufHandle depth(depth_id);
+ glBindRenderbuffer(GL_RENDERBUFFER, depth);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 1, 1);
+ glBindRenderbuffer(GL_RENDERBUFFER, 0);
+
+ GLuint fb_id = 0;
+ glGenFramebuffers(1, &fb_id);
+ FBOHandle fb(fb_id);
+ glBindFramebuffer(GL_FRAMEBUFFER, fb);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex,
+ 0);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER,
+ depth);
+
+ const bool ok = (glCheckFramebufferStatus(GL_FRAMEBUFFER) ==
+ GL_FRAMEBUFFER_COMPLETE);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ return ok;
+}
+
void MeshRenderer::render(const RenderQueue& queue)
{
// elements containing opaque objects should be rendered first
@@ -117,6 +356,270 @@ void MeshRenderer::render(const RenderQueue& queue)
{
return !renderPair.first.contains_translucent;
});
+
+ struct DrawableBuffers
+ {
+ std::vector tex_bufs;
+ std::vector no_tex_bufs;
+ TextBuffer* text_buf = nullptr;
+ };
+
+ auto collectBuffers = [&](GlDrawable* drawable) -> DrawableBuffers
+ {
+ DrawableBuffers out;
+ for (int i = 0; i < NUM_LAYOUTS; i++)
+ {
+ for (size_t j = 0; j < GlDrawable::NUM_SHAPES; j++)
+ {
+ if (drawable->buffers[i][j])
+ {
+ if (i == LAYOUT_VTX_TEXTURE0 || i == LAYOUT_VTX_NORMAL_TEXTURE0)
+ {
+ out.tex_bufs.emplace_back(drawable->buffers[i][j].get()->getHandle());
+ }
+ else
+ {
+ out.no_tex_bufs.emplace_back(drawable->buffers[i][j].get()->getHandle());
+ }
+ }
+ if (drawable->indexed_buffers[i][j])
+ {
+ if (i == LAYOUT_VTX_TEXTURE0 || i == LAYOUT_VTX_NORMAL_TEXTURE0)
+ {
+ out.tex_bufs.emplace_back(
+ drawable->indexed_buffers[i][j].get()->getHandle());
+ }
+ else
+ {
+ out.no_tex_bufs.emplace_back(
+ drawable->indexed_buffers[i][j].get()->getHandle());
+ }
+ }
+ }
+ }
+ out.text_buf = &drawable->text_buffer;
+ return out;
+ };
+
+ CoreGLDevice* core_dev = dynamic_cast(device.get());
+ if (canUseOIT(sorted_queue) && core_dev && core_dev->hasOITPrograms())
+ {
+ int vp[4];
+ device->getViewport(vp);
+ int width = vp[2];
+ int height = vp[3];
+
+ if (!ensureOITTargets(width, height))
+ {
+ std::cerr << "OIT: Unable to create render targets, falling back to legacy "
+ "transparency." << std::endl;
+ bool prev_oit = oit_enable;
+ oit_enable = false;
+ render(queue);
+ oit_enable = prev_oit;
+ return;
+ }
+
+ auto setRenderParams = [&](const RenderParams& params)
+ {
+ device->setTransformMatrices(params.model_view.mtx, params.projection.mtx);
+ device->setMaterial(params.mesh_material);
+ device->setNumLights(params.num_pt_lights);
+ for (int i = 0; i < params.num_pt_lights; i++)
+ {
+ device->setPointLight(i, params.lights[i]);
+ }
+ device->setAmbientLight(params.light_amb_scene);
+ device->setStaticColor(params.static_color);
+ device->setClipPlaneUse(params.use_clip_plane);
+ device->setClipPlaneEqn(params.clip_plane_eqn);
+ };
+
+ auto drawDrawableGeometry = [&](GlDrawable* drawable)
+ {
+ DrawableBuffers bufs = collectBuffers(drawable);
+ device->attachTexture(GLDevice::SAMPLER_COLOR, color_tex);
+ device->attachTexture(GLDevice::SAMPLER_ALPHA, alpha_tex);
+ for (auto buf : bufs.tex_bufs)
+ {
+ device->drawDeviceBuffer(buf);
+ }
+ device->detachTexture(GLDevice::SAMPLER_COLOR);
+ device->detachTexture(GLDevice::SAMPLER_ALPHA);
+ for (auto buf : bufs.no_tex_bufs)
+ {
+ device->drawDeviceBuffer(buf);
+ }
+ };
+
+ auto drawDrawableText = [&](const RenderParams& params, GlDrawable* drawable)
+ {
+ DrawableBuffers bufs = collectBuffers(drawable);
+ device->setTransformMatrices(params.model_view.mtx, params.projection.mtx);
+ device->setClipPlaneUse(params.use_clip_plane);
+ device->setClipPlaneEqn(params.clip_plane_eqn);
+ device->drawDeviceBuffer(*bufs.text_buf);
+ };
+
+ // === Opaque pass (optionally MSAA-resolved into oit_scene_fb) ===
+ glBindFramebuffer(GL_FRAMEBUFFER, oit_msaa_fb ? (GLuint)oit_msaa_fb
+ : (GLuint)oit_scene_fb);
+#ifndef __EMSCRIPTEN__
+ if (oit_msaa_fb) { glEnable(GL_MULTISAMPLE); }
+#endif
+ glClearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ core_dev->bindDefaultProgram();
+ glDisable(GL_BLEND);
+ device->enableDepthWrite();
+ for (auto& q_elem : sorted_queue)
+ {
+ const RenderParams& params = q_elem.first;
+ if (params.contains_translucent)
+ {
+ continue;
+ }
+ setRenderParams(params);
+ drawDrawableGeometry(q_elem.second);
+ }
+
+ if (oit_msaa_fb)
+ {
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, oit_msaa_fb);
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, oit_scene_fb);
+ glBlitFramebuffer(0, 0, width, height,
+ 0, 0, width, height,
+ GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT,
+ GL_NEAREST);
+#ifndef __EMSCRIPTEN__
+ glDisable(GL_MULTISAMPLE);
+#endif
+ }
+
+ // === Accumulation pass ===
+ glBindFramebuffer(GL_FRAMEBUFFER, oit_accum_fb);
+ glClearColor(0.f, 0.f, 0.f, 0.f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glEnable(GL_DEPTH_TEST);
+ device->disableDepthWrite();
+ glEnable(GL_BLEND);
+ glBlendEquation(GL_FUNC_ADD);
+ glBlendFunc(GL_ONE, GL_ONE);
+ core_dev->bindOITAccumProgram();
+ for (auto& q_elem : sorted_queue)
+ {
+ const RenderParams& params = q_elem.first;
+ if (!params.contains_translucent)
+ {
+ continue;
+ }
+ setRenderParams(params);
+ drawDrawableGeometry(q_elem.second);
+ }
+
+ // === Revealage pass ===
+ glBindFramebuffer(GL_FRAMEBUFFER, oit_reveal_fb);
+ glClearColor(1.f, 1.f, 1.f, 1.f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glEnable(GL_DEPTH_TEST);
+ device->disableDepthWrite();
+ glEnable(GL_BLEND);
+ glBlendEquation(GL_FUNC_ADD);
+ glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
+ core_dev->bindOITRevealProgram();
+ for (auto& q_elem : sorted_queue)
+ {
+ const RenderParams& params = q_elem.first;
+ if (!params.contains_translucent)
+ {
+ continue;
+ }
+ setRenderParams(params);
+ drawDrawableGeometry(q_elem.second);
+ }
+
+ // === Composite to default framebuffer ===
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glDisable(GL_BLEND);
+ glDisable(GL_DEPTH_TEST);
+
+ if (!oit_finalize_ready)
+ {
+ if (!oit_finalize_prgm.create(kOITFullscreenVs, kOITFinalizeFs, {}, 1))
+ {
+ std::cerr << "OIT: Failed to compile composite shader, falling back to "
+ "legacy transparency." << std::endl;
+ glEnable(GL_DEPTH_TEST);
+ glBlendEquation(GL_FUNC_ADD);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glClearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]);
+ glActiveTexture(GL_TEXTURE0);
+ bool prev_oit = oit_enable;
+ oit_enable = false;
+ render(queue);
+ oit_enable = prev_oit;
+ return;
+ }
+ oit_finalize_prgm.bind();
+ glUniform1i(oit_finalize_prgm.uniform("sceneTex"), 0);
+ glUniform1i(oit_finalize_prgm.uniform("accumTex"), 1);
+ glUniform1i(oit_finalize_prgm.uniform("revealTex"), 2);
+ oit_finalize_ready = true;
+ }
+
+ if (!oit_finalize_vao && (GLEW_VERSION_3_0 || GLEW_ARB_vertex_array_object))
+ {
+ GLuint vao_id;
+ glGenVertexArrays(1, &vao_id);
+ oit_finalize_vao = VtxArrayHandle(vao_id);
+ }
+ if (oit_finalize_vao)
+ {
+ glBindVertexArray(oit_finalize_vao);
+ }
+
+ oit_finalize_prgm.bind();
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, oit_scene_color_tex);
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, oit_accum_tex);
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, oit_reveal_tex);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, 0);
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, 0);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ // Restore expected baseline state for the rest of GLVis.
+ glEnable(GL_DEPTH_TEST);
+ glBlendEquation(GL_FUNC_ADD);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glClearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]);
+ glActiveTexture(GL_TEXTURE0);
+
+ // === Text overlay pass ===
+ core_dev->bindDefaultProgram();
+ device->enableBlend();
+ device->disableDepthWrite();
+ device->attachTexture(1, font_tex);
+ device->setNumLights(0);
+ for (auto& q_elem : sorted_queue)
+ {
+ drawDrawableText(q_elem.first, q_elem.second);
+ }
+ device->enableDepthWrite();
+ device->disableBlend();
+
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glActiveTexture(GL_TEXTURE0);
+ glClearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]);
+ return;
+ }
+
RenderBufHandle renderBufs[2];
FBOHandle msaaFb;
if (feat_use_fbo_antialias && msaa_enable)
diff --git a/lib/gl/renderer.hpp b/lib/gl/renderer.hpp
index 588235a3..db799044 100644
--- a/lib/gl/renderer.hpp
+++ b/lib/gl/renderer.hpp
@@ -16,6 +16,7 @@
#include
#include "platform_gl.hpp"
+#include "shader.hpp"
#include "types.hpp"
#include "../material.hpp"
#include "../palettes.hpp"
@@ -194,18 +195,59 @@ class MeshRenderer
std::unique_ptr device;
bool msaa_enable;
int msaa_samples;
+
+ // Order-independent transparency (OIT) rendering is implemented using an
+ // off-screen accumulation/revealage pass and a final compositing pass. It is
+ // enabled per-context only when float color-buffer render targets are
+ // supported.
+ bool oit_enable;
+ ShaderProgram oit_finalize_prgm;
+ resource::VtxArrayHandle oit_finalize_vao;
+ bool oit_finalize_ready;
+ int oit_width;
+ int oit_height;
+ int oit_msaa_samples;
+ resource::TextureHandle oit_scene_color_tex;
+ resource::RenderBufHandle oit_scene_depth_rb;
+ resource::FBOHandle oit_scene_fb;
+ resource::TextureHandle oit_accum_tex;
+ resource::TextureHandle oit_reveal_tex;
+ resource::FBOHandle oit_accum_fb;
+ resource::FBOHandle oit_reveal_fb;
+ resource::RenderBufHandle oit_msaa_color_rb;
+ resource::RenderBufHandle oit_msaa_depth_rb;
+ resource::FBOHandle oit_msaa_fb;
GLuint color_tex, alpha_tex, font_tex;
float line_w, line_w_aa;
PaletteState* palette;
+ std::array clear_color;
+ bool oit_support_checked;
+ bool oit_support;
bool feat_use_fbo_antialias;
+ bool canUseOIT(const RenderQueue& queued) const;
+ // Probe if OIT render targets are supported (e.g. RGBA16F is color-renderable).
+ bool probeOITSupport();
+ bool ensureOITTargets(int width, int height);
void init();
public:
MeshRenderer()
: msaa_enable(false)
, msaa_samples(0)
+ , oit_enable(false)
+ , oit_finalize_ready(false)
+ , oit_width(0)
+ , oit_height(0)
+ , oit_msaa_samples(0)
+ , color_tex(0)
+ , alpha_tex(0)
+ , font_tex(0)
, line_w(1.f)
- , line_w_aa(LINE_WIDTH_AA) { init(); }
+ , line_w_aa(LINE_WIDTH_AA)
+ , palette(nullptr)
+ , clear_color{0.f, 0.f, 0.f, 1.f}
+ , oit_support_checked(false)
+ , oit_support(false) { init(); }
template
void setDevice()
@@ -232,6 +274,9 @@ class MeshRenderer
void setAntialiasing(bool aa_status);
bool getAntialiasing() { return msaa_enable; }
+ // Enables/disables OIT (when supported by the current OpenGL/WebGL context).
+ void setOrderIndependentTransparency(bool enable) { oit_enable = enable; }
+ bool getOrderIndependentTransparency() const { return oit_enable; }
void setSamplesMSAA(int samples)
{
if (msaa_samples < samples)
@@ -253,7 +298,11 @@ class MeshRenderer
void setLineWidthMS(float w);
float getLineWidthMS() { return line_w_aa; }
- void setClearColor(float r, float g, float b, float a) { glClearColor(r, g, b, a); }
+ void setClearColor(float r, float g, float b, float a)
+ {
+ clear_color = {r, g, b, a};
+ glClearColor(r, g, b, a);
+ }
void setViewport(GLsizei w, GLsizei h) { device->setViewport(w, h); }
void render(const RenderQueue& queued);
diff --git a/lib/gl/renderer_core.cpp b/lib/gl/renderer_core.cpp
index 9db5ab24..c2f20372 100644
--- a/lib/gl/renderer_core.cpp
+++ b/lib/gl/renderer_core.cpp
@@ -31,6 +31,14 @@ const std::string DEFAULT_FS =
BLINN_PHONG_FS +
#include "shaders/default.frag"
;
+const std::string OIT_ACCUM_FS =
+ BLINN_PHONG_FS +
+#include "shaders/oit_accum.frag"
+ ;
+const std::string OIT_REVEAL_FS =
+ BLINN_PHONG_FS +
+#include "shaders/oit_reveal.frag"
+ ;
const std::string PRINTING_VS =
BLINN_PHONG_FS +
#include "shaders/printing.vert"
@@ -110,6 +118,18 @@ bool CoreGLDevice::compileShaders()
return false;
}
+ if (!oit_accum_prgm.create(DEFAULT_VS, OIT_ACCUM_FS, attribMap, 1))
+ {
+ std::cerr << "Failed to create the OIT accumulation shader program." <<
+ std::endl;
+ return false;
+ }
+ if (!oit_reveal_prgm.create(DEFAULT_VS, OIT_REVEAL_FS, attribMap, 1))
+ {
+ std::cerr << "Failed to create the OIT revealage shader program." << std::endl;
+ return false;
+ }
+
#ifndef __EMSCRIPTEN__
if (GLEW_EXT_transform_feedback || GLEW_VERSION_3_0)
{
diff --git a/lib/gl/renderer_core.hpp b/lib/gl/renderer_core.hpp
index e3956fe1..c684d8c8 100644
--- a/lib/gl/renderer_core.hpp
+++ b/lib/gl/renderer_core.hpp
@@ -43,6 +43,8 @@ class CoreGLDevice : public GLDevice
private:
ShaderProgram default_prgm;
+ ShaderProgram oit_accum_prgm;
+ ShaderProgram oit_reveal_prgm;
ShaderProgram feedback_prgm;
resource::VtxArrayHandle global_vao;
@@ -114,10 +116,22 @@ class CoreGLDevice : public GLDevice
}
void bindExternalProgram(const ShaderProgram& prog)
{
+ // Core profile rendering requires a VAO to be bound.
+ if (global_vao)
+ {
+ glBindVertexArray(global_vao);
+ }
glDisable(GL_RASTERIZER_DISCARD);
initializeShaderState(prog);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
}
+ bool hasOITPrograms() const
+ {
+ return oit_accum_prgm.isCompiled() && oit_reveal_prgm.isCompiled();
+ }
+ void bindOITAccumProgram() { bindExternalProgram(oit_accum_prgm); }
+ void bindOITRevealProgram() { bindExternalProgram(oit_reveal_prgm); }
+ void bindDefaultProgram() { bindExternalProgram(default_prgm); }
void captureXfbBuffer(PaletteState& pal, CaptureBuffer& cbuf, int hnd) override;
};
diff --git a/lib/gl/shaders/oit_accum.frag b/lib/gl/shaders/oit_accum.frag
new file mode 100644
index 00000000..84274166
--- /dev/null
+++ b/lib/gl/shaders/oit_accum.frag
@@ -0,0 +1,48 @@
+R"(
+// Copyright (c) 2010-2026, Lawrence Livermore National Security, LLC. Produced
+// at the Lawrence Livermore National Laboratory. All Rights reserved. See files
+// LICENSE and NOTICE for details. LLNL-CODE-443271.
+//
+// This file is part of the GLVis visualization tool and library. For more
+// information and source code availability see https://glvis.org.
+//
+// GLVis is free software; you can redistribute it and/or modify it under the
+// terms of the BSD-3 license. We welcome feedback and contributions, see file
+// CONTRIBUTING.md for details.
+
+uniform sampler2D alphaTex;
+uniform sampler2D colorTex;
+
+varying vec3 fNormal;
+varying vec3 fPosition;
+varying vec4 fColor;
+varying vec2 fTexCoord;
+
+uniform bool useClipPlane;
+varying float fClipVal;
+
+void fragmentClipPlane()
+{
+ if (useClipPlane && fClipVal < 0.0)
+ {
+ discard;
+ }
+}
+
+void main()
+{
+ fragmentClipPlane();
+ vec4 color = fColor * texture2D(colorTex, vec2(fTexCoord));
+ color = blinnPhong(fPosition, fNormal, color);
+#ifdef USE_ALPHA
+ color.a *= texture2D(alphaTex, vec2(fTexCoord)).a;
+#else
+ color.a *= texture2D(alphaTex, vec2(fTexCoord)).r;
+#endif
+
+ float alpha = clamp(color.a, 0.0, 1.0);
+ float weight = clamp(pow(alpha, 4.0) * 1000.0 + 0.01, 0.01, 3000.0);
+ gl_FragColor = vec4(color.rgb * alpha * weight, alpha * weight);
+}
+)"
+
diff --git a/lib/gl/shaders/oit_finalize.frag b/lib/gl/shaders/oit_finalize.frag
new file mode 100644
index 00000000..93780649
--- /dev/null
+++ b/lib/gl/shaders/oit_finalize.frag
@@ -0,0 +1,31 @@
+R"(
+// Copyright (c) 2010-2026, Lawrence Livermore National Security, LLC. Produced
+// at the Lawrence Livermore National Laboratory. All Rights reserved. See files
+// LICENSE and NOTICE for details. LLNL-CODE-443271.
+//
+// This file is part of the GLVis visualization tool and library. For more
+// information and source code availability see https://glvis.org.
+//
+// GLVis is free software; you can redistribute it and/or modify it under the
+// terms of the BSD-3 license. We welcome feedback and contributions, see file
+// CONTRIBUTING.md for details.
+
+uniform sampler2D sceneTex;
+uniform sampler2D accumTex;
+uniform sampler2D revealTex;
+
+varying vec2 vUv;
+
+void main()
+{
+ vec3 scene = texture2D(sceneTex, vUv).rgb;
+ vec4 accum = texture2D(accumTex, vUv);
+ float reveal = texture2D(revealTex, vUv).r;
+
+ float transAlpha = 1.0 - reveal;
+ vec3 transColor = (accum.a > 1e-5) ? (accum.rgb / accum.a) : vec3(0.0);
+ vec3 outColor = transColor * transAlpha + scene * reveal;
+ gl_FragColor = vec4(outColor, 1.0);
+}
+)"
+
diff --git a/lib/gl/shaders/oit_fullscreen.vert b/lib/gl/shaders/oit_fullscreen.vert
new file mode 100644
index 00000000..48aa2285
--- /dev/null
+++ b/lib/gl/shaders/oit_fullscreen.vert
@@ -0,0 +1,34 @@
+R"(
+// Copyright (c) 2010-2026, Lawrence Livermore National Security, LLC. Produced
+// at the Lawrence Livermore National Laboratory. All Rights reserved. See files
+// LICENSE and NOTICE for details. LLNL-CODE-443271.
+//
+// This file is part of the GLVis visualization tool and library. For more
+// information and source code availability see https://glvis.org.
+//
+// GLVis is free software; you can redistribute it and/or modify it under the
+// terms of the BSD-3 license. We welcome feedback and contributions, see file
+// CONTRIBUTING.md for details.
+
+varying vec2 vUv;
+
+void main()
+{
+ vec2 pos;
+ if (gl_VertexID == 0)
+ {
+ pos = vec2(-1.0, -1.0);
+ }
+ else if (gl_VertexID == 1)
+ {
+ pos = vec2(3.0, -1.0);
+ }
+ else
+ {
+ pos = vec2(-1.0, 3.0);
+ }
+ vUv = 0.5 * pos + 0.5;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+)"
+
diff --git a/lib/gl/shaders/oit_reveal.frag b/lib/gl/shaders/oit_reveal.frag
new file mode 100644
index 00000000..d2c13709
--- /dev/null
+++ b/lib/gl/shaders/oit_reveal.frag
@@ -0,0 +1,47 @@
+R"(
+// Copyright (c) 2010-2026, Lawrence Livermore National Security, LLC. Produced
+// at the Lawrence Livermore National Laboratory. All Rights reserved. See files
+// LICENSE and NOTICE for details. LLNL-CODE-443271.
+//
+// This file is part of the GLVis visualization tool and library. For more
+// information and source code availability see https://glvis.org.
+//
+// GLVis is free software; you can redistribute it and/or modify it under the
+// terms of the BSD-3 license. We welcome feedback and contributions, see file
+// CONTRIBUTING.md for details.
+
+uniform sampler2D alphaTex;
+uniform sampler2D colorTex;
+
+varying vec3 fNormal;
+varying vec3 fPosition;
+varying vec4 fColor;
+varying vec2 fTexCoord;
+
+uniform bool useClipPlane;
+varying float fClipVal;
+
+void fragmentClipPlane()
+{
+ if (useClipPlane && fClipVal < 0.0)
+ {
+ discard;
+ }
+}
+
+void main()
+{
+ fragmentClipPlane();
+ vec4 color = fColor * texture2D(colorTex, vec2(fTexCoord));
+ color = blinnPhong(fPosition, fNormal, color);
+#ifdef USE_ALPHA
+ color.a *= texture2D(alphaTex, vec2(fTexCoord)).a;
+#else
+ color.a *= texture2D(alphaTex, vec2(fTexCoord)).r;
+#endif
+
+ float alpha = clamp(color.a, 0.0, 1.0);
+ gl_FragColor = vec4(alpha, alpha, alpha, alpha);
+}
+)"
+
diff --git a/lib/gl/types.hpp b/lib/gl/types.hpp
index 4ad241cf..d543eafc 100644
--- a/lib/gl/types.hpp
+++ b/lib/gl/types.hpp
@@ -56,6 +56,10 @@ class Handle
{
if (this != &other)
{
+ if (hnd)
+ {
+ GLFinalizer(hnd);
+ }
hnd = other.hnd;
other.hnd = 0;
}
diff --git a/lib/vssolution.cpp b/lib/vssolution.cpp
index 7d25f38f..dc0bef43 100644
--- a/lib/vssolution.cpp
+++ b/lib/vssolution.cpp
@@ -87,6 +87,7 @@ std::string VisualizationSceneSolution::GetHelpString() const
<< "| Alt+a - Axes number format |" << endl
<< "| Alt+c - Colorbar number format |" << endl
<< "| Alt+n - Numberings method |" << endl
+ << "| Alt+o - Toggle OIT transparency |" << endl
<< "| Ctrl+o - Element ordering curve |" << endl
<< "| Ctrl+p - Print to a PDF file |" << endl
<< "+------------------------------------+" << endl
@@ -304,7 +305,15 @@ static void KeyNPressed(GLenum state)
static void KeyoPressed(GLenum state)
{
- if (state & KMOD_CTRL)
+ if (state & KMOD_ALT)
+ {
+ const bool enable =
+ !GetAppWindow()->getRenderer().getOrderIndependentTransparency();
+ GetAppWindow()->getRenderer().setOrderIndependentTransparency(enable);
+ cout << "Order-independent transparency: " << (enable ? "on" : "off") << endl;
+ SendExposeEvent();
+ }
+ else if (state & KMOD_CTRL)
{
vssol -> ToggleDrawOrdering();
vssol -> PrepareOrderingCurve();
diff --git a/lib/vssolution3d.cpp b/lib/vssolution3d.cpp
index 2383fb3e..b93e407f 100644
--- a/lib/vssolution3d.cpp
+++ b/lib/vssolution3d.cpp
@@ -87,6 +87,7 @@ std::string VisualizationSceneSolution3d::GetHelpString() const
<< "| \\ - Set light source position |" << endl
<< "| Alt+a - Axes number format |" << endl
<< "| Alt+c - Colorbar number format |" << endl
+ << "| Alt+o - Toggle OIT transparency |" << endl
<< "| Ctrl+o - Element ordering curve |" << endl
<< "| Ctrl+p - Print to a PDF file |" << endl
<< "+------------------------------------+" << endl
@@ -353,7 +354,15 @@ static void KeyFPressed()
static void KeyoPressed(GLenum state)
{
- if (state & KMOD_CTRL)
+ if (state & KMOD_ALT)
+ {
+ const bool enable =
+ !GetAppWindow()->getRenderer().getOrderIndependentTransparency();
+ GetAppWindow()->getRenderer().setOrderIndependentTransparency(enable);
+ cout << "Order-independent transparency: " << (enable ? "on" : "off") << endl;
+ SendExposeEvent();
+ }
+ else if (state & KMOD_CTRL)
{
vssol3d -> ToggleDrawOrdering();
vssol3d -> PrepareOrderingCurve();