diff --git a/CHANGELOG b/CHANGELOG
index 46abcd48..3fa7f739 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -9,6 +9,19 @@
https://glvis.org
+Version 4.5.1 (development)
+===========================
+- Added configurable line width for mesh edges and element boundaries when MSAA
+ is enabled. Line width can be adjusted at runtime using ';' (decrease) and '''
+ (increase) keys in 0.25 pixel increments (minimum 0.25, no maximum). Default
+ line width now is 1.5 pixels.
+
+- Without antialiasing, the original GL_LINES rendering is still used. With
+ antialiasing, lines are rendered using shader-expanded triangles and hardware
+ MSAA with fragment shader techniques. The line rendering format switches
+ automatically when toggling MSAA with the 'A' key.
+
+
Version 4.5 released on Feb 6, 2026
===================================
diff --git a/README.md b/README.md
index 2a78818e..63d99fb8 100644
--- a/README.md
+++ b/README.md
@@ -171,6 +171,7 @@ Key commands
- * / / – Zoom in/out
- + / - – Stretch/compress in `z`-direction
- [ / ] – Shrink/enlarge the bounding box (relative to the colorbar)
+- ; / ' – Decrease/increase line width
- ( / ) – Shrink/enlarge the visualization window
- . – Start/stop `z`-spinning (speed/direction can be controlled with 0 / Enter)
- ←, →, ↑, ↓ – Manual rotation
diff --git a/lib/aux_vis.cpp b/lib/aux_vis.cpp
index 5aa0b5aa..0695874f 100644
--- a/lib/aux_vis.cpp
+++ b/lib/aux_vis.cpp
@@ -258,6 +258,8 @@ GLWindow* InitVisualization(const char name[], int x, int y, int w, int h,
wnd->setOnKeyDown (SDLK_LEFTBRACKET, ScaleDown);
wnd->setOnKeyDown (SDLK_RIGHTBRACKET, ScaleUp);
+ wnd->setOnKeyDown (SDLK_SEMICOLON, DecreaseLineWidth);
+ wnd->setOnKeyDown (SDLK_QUOTE, IncreaseLineWidth);
wnd->setOnKeyDown (SDLK_AT, LookAt);
#ifndef __EMSCRIPTEN__
@@ -1650,6 +1652,26 @@ void ScaleDown()
SendExposeEvent();
}
+void IncreaseLineWidth()
+{
+ float new_w = GetLineWidth() + 0.25f;
+ float new_w_aa = GetLineWidthMS() + 0.25f;
+ SetLineWidth(new_w);
+ SetLineWidthMS(new_w_aa);
+ cout << "Line width: " << new_w << " (antialiased: " << new_w_aa << ")" << endl;
+ SendExposeEvent();
+}
+
+void DecreaseLineWidth()
+{
+ float new_w = std::max(0.25f, GetLineWidth() - 0.25f);
+ float new_w_aa = std::max(0.25f, GetLineWidthMS() - 0.25f);
+ SetLineWidth(new_w);
+ SetLineWidthMS(new_w_aa);
+ cout << "Line width: " << new_w << " (antialiased: " << new_w_aa << ")" << endl;
+ SendExposeEvent();
+}
+
void LookAt()
{
cout << "ViewCenter = (" << locscene->ViewCenterX << ','
diff --git a/lib/aux_vis.hpp b/lib/aux_vis.hpp
index 7df7fdb6..b889fc2e 100644
--- a/lib/aux_vis.hpp
+++ b/lib/aux_vis.hpp
@@ -104,6 +104,8 @@ void ScaleDown();
void LookAt();
void ShrinkWindow();
void EnlargeWindow();
+void IncreaseLineWidth();
+void DecreaseLineWidth();
void MoveResizeWindow(int x, int y, int w, int h);
void ResizeWindow(int w, int h);
void SetWindowTitle(const char *title);
diff --git a/lib/gl/renderer.cpp b/lib/gl/renderer.cpp
index bb601a54..766af31f 100644
--- a/lib/gl/renderer.cpp
+++ b/lib/gl/renderer.cpp
@@ -49,6 +49,11 @@ void MeshRenderer::setAntialiasing(bool aa_status)
if (msaa_enable != aa_status)
{
msaa_enable = aa_status;
+ // Update device's MSAA state for conditional line rendering
+ if (device)
+ {
+ device->setMSAAEnabled(aa_status);
+ }
if (msaa_enable)
{
if (!feat_use_fbo_antialias)
@@ -398,6 +403,9 @@ void MeshRenderer::buffer(GlDrawable* buf)
void GLDevice::init()
{
+ // Initialize line width
+ line_w = 1.0f;
+ msaa_enabled = false;
// enable depth testing
glDepthFunc(GL_LEQUAL);
glEnable(GL_DEPTH_TEST);
diff --git a/lib/gl/renderer.hpp b/lib/gl/renderer.hpp
index 588235a3..a622eff4 100644
--- a/lib/gl/renderer.hpp
+++ b/lib/gl/renderer.hpp
@@ -100,6 +100,9 @@ class GLDevice
std::array static_color;
+ float line_w;
+ bool msaa_enabled;
+
protected:
resource::TextureHandle passthrough_texture;
@@ -135,7 +138,9 @@ class GLDevice
void disableBlend() { glDisable(GL_BLEND); }
void enableDepthWrite() { glDepthMask(GL_TRUE); }
void disableDepthWrite() { glDepthMask(GL_FALSE); }
- void setLineWidth(float w) { glLineWidth(w); }
+ void setLineWidth(float w) { line_w = w; glLineWidth(w); }
+ // Update MSAA state for conditional line rendering
+ void setMSAAEnabled(bool enabled) { msaa_enabled = enabled; }
virtual void init();
virtual DeviceType getType() = 0;
@@ -195,6 +200,8 @@ class MeshRenderer
bool msaa_enable;
int msaa_samples;
GLuint color_tex, alpha_tex, font_tex;
+ // Separate line widths for non-MSAA (line_w) and MSAA (line_w_aa) modes
+ // Allows different defaults: 1.0 for GL_LINES, 1.5 for shader-expanded
float line_w, line_w_aa;
PaletteState* palette;
@@ -213,7 +220,8 @@ class MeshRenderer
device.reset(new TDevice());
device->setLineWidth(line_w);
device->init();
- msaa_enable = false;
+ // Synchronize device's MSAA state with renderer's state
+ device->setMSAAEnabled(msaa_enable);
}
template
@@ -248,8 +256,10 @@ class MeshRenderer
}
int getSamplesMSAA() { return msaa_samples; }
+ // Line width for non-MSAA mode (GL_LINES)
void setLineWidth(float w);
float getLineWidth() { return line_w; }
+ // Line width for MSAA mode (shader-expanded triangles)
void setLineWidthMS(float w);
float getLineWidthMS() { return line_w_aa; }
diff --git a/lib/gl/renderer_core.cpp b/lib/gl/renderer_core.cpp
index 9db5ab24..46fc5595 100644
--- a/lib/gl/renderer_core.cpp
+++ b/lib/gl/renderer_core.cpp
@@ -49,6 +49,10 @@ const std::vector CoreGLDevice::unif_list =
"useClipPlane",
"clipPlane",
"containsText",
+ "expandLines",
+ "lineWidth",
+ "aspectRatio",
+ "useLineAA",
"modelViewMatrix",
"projectionMatrix",
"textProjMatrix",
@@ -70,6 +74,98 @@ const std::vector CoreGLDevice::unif_list =
"alphaTex"
};
+// Extended vertex format for shader-expanded lines (MSAA mode). Each line
+// segment is converted to 2 triangles (4 vertices) orientation: ±1 indicates
+// which side of the line centerline prev/next: neighboring vertices for
+// computing line direction and joins.
+struct alignas(16) CoreGLDevice::LineVertex
+{
+ std::array vtx;
+ float orientation;
+ std::array prev;
+ std::array next;
+
+ static constexpr array_layout layout = LAYOUT_EXT_LINE_VTX;
+
+ static void Setup()
+ {
+ constexpr LineVertex base{};
+ const size_t vtx_offset = (size_t)(&base.vtx) - (size_t)(&base);
+ const size_t orient_offset = (size_t)(&base.orientation) - (size_t)(&base);
+ const size_t prev_offset = (size_t)(&base.prev) - (size_t)(&base);
+ const size_t next_offset = (size_t)(&base.next) - (size_t)(&base);
+
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_VERTEX);
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_LINE_ORIENT);
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_LINE_PREV);
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_LINE_NEXT);
+
+ glVertexAttribPointer(CoreGLDevice::ATTR_VERTEX, 3, GL_FLOAT, false,
+ sizeof(LineVertex), (void*)vtx_offset);
+ glVertexAttribPointer(CoreGLDevice::ATTR_LINE_ORIENT, 1, GL_FLOAT, false,
+ sizeof(LineVertex), (void*)orient_offset);
+ glVertexAttribPointer(CoreGLDevice::ATTR_LINE_PREV, 3, GL_FLOAT, false,
+ sizeof(LineVertex), (void*)prev_offset);
+ glVertexAttribPointer(CoreGLDevice::ATTR_LINE_NEXT, 3, GL_FLOAT, false,
+ sizeof(LineVertex), (void*)next_offset);
+ }
+
+ static void Finish()
+ {
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_VERTEX);
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_LINE_ORIENT);
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_LINE_PREV);
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_LINE_NEXT);
+ }
+};
+
+// LineVertex with per-vertex color
+struct alignas(16) CoreGLDevice::LineColorVertex
+{
+ std::array vtx;
+ std::array color;
+ float orientation;
+ std::array prev;
+ std::array next;
+
+ static constexpr array_layout layout = LAYOUT_EXT_LINE_VTX_COLOR;
+
+ static void Setup()
+ {
+ constexpr LineColorVertex base{};
+ const size_t vtx_offset = (size_t)(&base.vtx) - (size_t)(&base);
+ const size_t color_offset = (size_t)(&base.color) - (size_t)(&base);
+ const size_t orient_offset = (size_t)(&base.orientation) - (size_t)(&base);
+ const size_t prev_offset = (size_t)(&base.prev) - (size_t)(&base);
+ const size_t next_offset = (size_t)(&base.next) - (size_t)(&base);
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_VERTEX);
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_COLOR);
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_LINE_ORIENT);
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_LINE_PREV);
+ glEnableVertexAttribArray(CoreGLDevice::ATTR_LINE_NEXT);
+
+ glVertexAttribPointer(CoreGLDevice::ATTR_VERTEX, 3, GL_FLOAT, false,
+ sizeof(LineColorVertex), (void*)vtx_offset);
+ glVertexAttribPointer(CoreGLDevice::ATTR_COLOR, 4, GL_UNSIGNED_BYTE, true,
+ sizeof(LineColorVertex), (void*)color_offset);
+ glVertexAttribPointer(CoreGLDevice::ATTR_LINE_ORIENT, 1, GL_FLOAT, false,
+ sizeof(LineColorVertex), (void*)orient_offset);
+ glVertexAttribPointer(CoreGLDevice::ATTR_LINE_PREV, 3, GL_FLOAT, false,
+ sizeof(LineColorVertex), (void*)prev_offset);
+ glVertexAttribPointer(CoreGLDevice::ATTR_LINE_NEXT, 3, GL_FLOAT, false,
+ sizeof(LineColorVertex), (void*)next_offset);
+ }
+
+ static void Finish()
+ {
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_VERTEX);
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_COLOR);
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_LINE_ORIENT);
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_LINE_PREV);
+ glDisableVertexAttribArray(CoreGLDevice::ATTR_LINE_NEXT);
+ }
+};
+
template
void setupVtxAttrLayout()
{
@@ -100,7 +196,10 @@ bool CoreGLDevice::compileShaders()
{ CoreGLDevice::ATTR_TEXT_VERTEX, "textVertex"},
{ CoreGLDevice::ATTR_NORMAL, "normal"},
{ CoreGLDevice::ATTR_COLOR, "color"},
- { CoreGLDevice::ATTR_TEXCOORD0, "texCoord0"}
+ { CoreGLDevice::ATTR_TEXCOORD0, "texCoord0"},
+ { CoreGLDevice::ATTR_LINE_ORIENT, "line_orientation"},
+ { CoreGLDevice::ATTR_LINE_PREV, "line_prev_vtx"},
+ { CoreGLDevice::ATTR_LINE_NEXT, "line_next_vtx"}
};
if (!default_prgm.create(DEFAULT_VS, DEFAULT_FS, attribMap, 1))
@@ -163,6 +262,8 @@ void CoreGLDevice::initializeShaderState(const ShaderProgram& prog)
#endif
glUniform1i(uniforms["colorTex"], 0);
glUniform1i(uniforms["alphaTex"], 1);
+ glUniform1i(uniforms["expandLines"], false);
+ glUniform1i(uniforms["useLineAA"], false);
use_clip_plane = false;
}
@@ -250,22 +351,176 @@ void CoreGLDevice::setClipPlaneEqn(const std::array &eqn)
void CoreGLDevice::bufferToDevice(array_layout layout, IVertexBuffer &buf)
{
- if (buf.getHandle() == 0)
+ // Special handling for lines: convert to triangles with extended vertex
+ // data. Only do this when MSAA is enabled (for configurable width with good
+ // quality) When MSAA is disabled, use original GL_LINES approach.
+ bool use_shader_lines = msaa_enabled &&
+ buf.getShape() == GL_LINES &&
+ (layout == Vertex::layout || layout == VertexColor::layout);
+
+ if (use_shader_lines)
{
+ array_layout ext_layout;
+ if (layout == Vertex::layout) { ext_layout = LineVertex::layout; }
+ else if (layout == VertexColor::layout) { ext_layout = LineColorVertex::layout; }
+
+ if (buf.getHandle() == 0)
+ {
+ if (buf.count() == 0) { return; }
+ GLuint handle[2];
+ glGenBuffers(2, &handle[0]);
+ buf.setHandle(vbos.size());
+ vbos.emplace_back(VBOData{handle[0], handle[1], GL_TRIANGLES, 0, ext_layout});
+ }
+ else
+ {
+ // Update existing VBO metadata to shader-expanded format
+ vbos[buf.getHandle()].layout = ext_layout;
+ vbos[buf.getHandle()].shape = GL_TRIANGLES;
+
+ // Create element buffer if it doesn't exist (converting from GL_LINES to triangles)
+ if (vbos[buf.getHandle()].elem_buf == 0)
+ {
+ GLuint elem_handle;
+ glGenBuffers(1, &elem_handle);
+ vbos[buf.getHandle()].elem_buf = elem_handle;
+ }
+ }
+ glBindBuffer(GL_ARRAY_BUFFER, vbos[buf.getHandle()].vert_buf);
+ glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_STATIC_DRAW);
if (buf.count() == 0) { return; }
- GLuint handle;
- glGenBuffers(1, &handle);
- buf.setHandle(vbos.size());
- vbos.emplace_back(VBOData{handle, 0, buf.getShape(), buf.count(), layout});
+
+ // Create extended vertex data for generating shader-expanded lines
+ if (layout == Vertex::layout)
+ {
+ std::vector ext_data(buf.count() * 2);
+ auto pts = static_cast&>(buf).begin();
+ for (size_t i = 0; i < buf.count(); i++)
+ {
+ const Vertex& pt = *(pts + i);
+ ext_data[2*i].vtx = pt.coord;
+ ext_data[2*i].orientation = 1;
+ ext_data[2*i+1].vtx = pt.coord;
+ ext_data[2*i+1].orientation = -1;
+ if (i % 2 == 0)
+ {
+ // first node in the segment
+ ext_data[2*(i+1)].prev = pt.coord;
+ ext_data[2*(i+1) + 1].prev = pt.coord;
+ ext_data[2*i].prev = pt.coord;
+ ext_data[2*i+1].prev = pt.coord;
+ }
+ else
+ {
+ // last node in the segment
+ ext_data[2*(i-1)].next = pt.coord;
+ ext_data[2*(i-1) + 1].next = pt.coord;
+ ext_data[2*i].next = pt.coord;
+ ext_data[2*i+1].next = pt.coord;
+ }
+ }
+ glBufferData(GL_ARRAY_BUFFER, ext_data.size() * sizeof(LineVertex),
+ ext_data.data(), GL_STATIC_DRAW);
+
+ // Create index buffer for triangles (2 triangles per line segment)
+ std::vector indices;
+ for (size_t i = 0; i < buf.count() / 2; i++)
+ {
+ GLuint base = 4 * i;
+ indices.push_back(base);
+ indices.push_back(base + 1);
+ indices.push_back(base + 2);
+ indices.push_back(base + 2);
+ indices.push_back(base + 1);
+ indices.push_back(base + 3);
+ }
+ vbos[buf.getHandle()].count = indices.size();
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbos[buf.getHandle()].elem_buf);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint),
+ indices.data(), GL_STATIC_DRAW);
+ }
+ else if (layout == VertexColor::layout)
+ {
+ std::vector ext_data(buf.count() * 2);
+ auto pts = static_cast&>(buf).begin();
+ for (size_t i = 0; i < buf.count(); i++)
+ {
+ const VertexColor& pt = *(pts + i);
+ ext_data[2*i].vtx = pt.coord;
+ ext_data[2*i].color = pt.color;
+ ext_data[2*i].orientation = 1;
+ ext_data[2*i+1].vtx = pt.coord;
+ ext_data[2*i+1].color = pt.color;
+ ext_data[2*i+1].orientation = -1;
+ if (i % 2 == 0)
+ {
+ // first node in the segment
+ ext_data[2*(i+1)].prev = pt.coord;
+ ext_data[2*(i+1) + 1].prev = pt.coord;
+ ext_data[2*i].prev = pt.coord;
+ ext_data[2*i+1].prev = pt.coord;
+ }
+ else
+ {
+ // last node in the segment
+ ext_data[2*(i-1)].next = pt.coord;
+ ext_data[2*(i-1) + 1].next = pt.coord;
+ ext_data[2*i].next = pt.coord;
+ ext_data[2*i+1].next = pt.coord;
+ }
+ }
+ glBufferData(GL_ARRAY_BUFFER, ext_data.size() * sizeof(LineColorVertex),
+ ext_data.data(), GL_STATIC_DRAW);
+
+ // Create index buffer for triangles
+ std::vector indices;
+ for (size_t i = 0; i < buf.count() / 2; i++)
+ {
+ GLuint base = 4 * i;
+ indices.push_back(base);
+ indices.push_back(base + 1);
+ indices.push_back(base + 2);
+ indices.push_back(base + 2);
+ indices.push_back(base + 1);
+ indices.push_back(base + 3);
+ }
+ vbos[buf.getHandle()].count = indices.size();
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbos[buf.getHandle()].elem_buf);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint),
+ indices.data(), GL_STATIC_DRAW);
+ }
}
else
{
- vbos[buf.getHandle()].count = buf.count();
+ // Original code for non-line geometries
+ if (buf.getHandle() == 0)
+ {
+ if (buf.count() == 0) { return; }
+ GLuint handle;
+ glGenBuffers(1, &handle);
+ buf.setHandle(vbos.size());
+ vbos.emplace_back(VBOData{handle, 0, buf.getShape(), buf.count(), layout});
+ }
+ else
+ {
+ vbos[buf.getHandle()].count = buf.count();
+ // Update layout and shape in case we're switching from shader-expanded back to regular
+ vbos[buf.getHandle()].layout = layout;
+ vbos[buf.getHandle()].shape = buf.getShape();
+
+ // Delete element buffer if switching from triangles back to GL_LINES
+ if (buf.getShape() == GL_LINES && vbos[buf.getHandle()].elem_buf != 0)
+ {
+ GLuint elem_buf = vbos[buf.getHandle()].elem_buf;
+ glDeleteBuffers(1, &elem_buf);
+ vbos[buf.getHandle()].elem_buf = 0;
+ }
+ }
+ glBindBuffer(GL_ARRAY_BUFFER, vbos[buf.getHandle()].vert_buf);
+ glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_STATIC_DRAW);
+ glBufferData(GL_ARRAY_BUFFER, buf.count() * buf.getStride(),
+ buf.getData(), GL_STATIC_DRAW);
}
- glBindBuffer(GL_ARRAY_BUFFER, vbos[buf.getHandle()].vert_buf);
- glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_STATIC_DRAW);
- glBufferData(GL_ARRAY_BUFFER, buf.count() * buf.getStride(),
- buf.getData(), GL_STATIC_DRAW);
}
void CoreGLDevice::bufferToDevice(array_layout layout, IIndexedBuffer& buf)
@@ -376,34 +631,104 @@ void CoreGLDevice::drawDeviceBuffer(int hnd)
indexed = true;
}
if (vbos[hnd].layout == Vertex::layout
- || vbos[hnd].layout == VertexNorm::layout)
+ || vbos[hnd].layout == VertexNorm::layout
+ || vbos[hnd].layout == LineVertex::layout)
{
glVertexAttrib4fv(ATTR_COLOR, static_color.data());
}
GLenum shape = vbos[hnd].shape;
int count = vbos[hnd].count;
- switch (vbos[hnd].layout)
- {
- case Vertex::layout:
- drawDeviceBufferImpl(shape, count, indexed);
- break;
- case VertexColor::layout:
- drawDeviceBufferImpl(shape, count, indexed);
- break;
- case VertexTex::layout:
- drawDeviceBufferImpl(shape, count, indexed);
- break;
- case VertexNorm::layout:
- drawDeviceBufferImpl(shape, count, indexed);
- break;
- case VertexNormColor::layout:
- drawDeviceBufferImpl(shape, count, indexed);
- break;
- case VertexNormTex::layout:
- drawDeviceBufferImpl(shape, count, indexed);
- break;
- default:
- cerr << "WARNING: Unhandled vertex layout " << vbos[hnd].layout << endl;
+ array_layout type = vbos[hnd].layout;
+
+ // Special handling for line expansion
+ if (type == LineVertex::layout || type == LineColorVertex::layout)
+ {
+ // Always enable fragment shader AA for shader-expanded lines
+ // When combined with MSAA, gives "double-AA" for ultra-smooth quality
+ bool use_line_aa = true;
+
+ // Disable polygon offset for lines to prevent z-fighting
+ glPolygonOffset(0, 0);
+
+ // Fragment shader AA requires blending and no depth write
+ GLboolean depthWriteEnabled = GL_TRUE;
+ bool blendWasEnabled = glIsEnabled(GL_BLEND);
+ if (!blendWasEnabled)
+ {
+ glEnable(GL_BLEND);
+ }
+ glGetBooleanv(GL_DEPTH_WRITEMASK, &depthWriteEnabled);
+ if (depthWriteEnabled)
+ {
+ glDepthMask(GL_FALSE);
+ }
+
+ glUniform1i(uniforms["expandLines"], true);
+ glUniform1i(uniforms["useLineAA"], use_line_aa);
+ glUniform1f(uniforms["lineWidth"], 2 * line_w / vp_width);
+ glUniform1f(uniforms["aspectRatio"], (float)vp_width / vp_height);
+ // Set up attributes
+ glVertexAttrib3f(CoreGLDevice::ATTR_NORMAL, 0.f, 0.f, 1.f);
+ if (type == LineVertex::layout)
+ {
+ LineVertex::Setup();
+ }
+ else if (type == LineColorVertex::layout)
+ {
+ LineColorVertex::Setup();
+ }
+ glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, (void*)0);
+ if (type == LineVertex::layout)
+ {
+ LineVertex::Finish();
+ }
+ else if (type == LineColorVertex::layout)
+ {
+ LineColorVertex::Finish();
+ }
+ glUniform1i(uniforms["expandLines"], false);
+
+ // Restore state
+ if (depthWriteEnabled)
+ {
+ glDepthMask(GL_TRUE);
+ }
+ if (!blendWasEnabled)
+ {
+ glDisable(GL_BLEND);
+ }
+
+ // Reset polygon offset to default value
+ glPolygonOffset(1, 1);
+ }
+ else
+ {
+ // Ensure expandLines is disabled for non-line buffers
+ glUniform1i(uniforms["expandLines"], false);
+
+ switch (vbos[hnd].layout)
+ {
+ case Vertex::layout:
+ drawDeviceBufferImpl(shape, count, indexed);
+ break;
+ case VertexColor::layout:
+ drawDeviceBufferImpl(shape, count, indexed);
+ break;
+ case VertexTex::layout:
+ drawDeviceBufferImpl(shape, count, indexed);
+ break;
+ case VertexNorm::layout:
+ drawDeviceBufferImpl(shape, count, indexed);
+ break;
+ case VertexNormColor::layout:
+ drawDeviceBufferImpl(shape, count, indexed);
+ break;
+ case VertexNormTex::layout:
+ drawDeviceBufferImpl(shape, count, indexed);
+ break;
+ default:
+ cerr << "WARNING: Unhandled vertex layout " << vbos[hnd].layout << endl;
+ }
}
}
void CoreGLDevice::drawDeviceBuffer(const TextBuffer& t_buf)
diff --git a/lib/gl/renderer_core.hpp b/lib/gl/renderer_core.hpp
index e3956fe1..3536bab2 100644
--- a/lib/gl/renderer_core.hpp
+++ b/lib/gl/renderer_core.hpp
@@ -31,6 +31,9 @@ class CoreGLDevice : public GLDevice
ATTR_NORMAL,
ATTR_COLOR,
ATTR_TEXCOORD0,
+ ATTR_LINE_ORIENT,
+ ATTR_LINE_PREV,
+ ATTR_LINE_NEXT,
NUM_ATTRS
};
@@ -41,6 +44,9 @@ class CoreGLDevice : public GLDevice
float clipCoord;
};
+ struct LineVertex;
+ struct LineColorVertex;
+
private:
ShaderProgram default_prgm;
ShaderProgram feedback_prgm;
diff --git a/lib/gl/shaders/default.frag b/lib/gl/shaders/default.frag
index 5cacf474..60410205 100644
--- a/lib/gl/shaders/default.frag
+++ b/lib/gl/shaders/default.frag
@@ -20,6 +20,9 @@ varying vec2 fTexCoord;
uniform bool useClipPlane;
varying float fClipVal;
+varying float fLineEdgeDist;
+uniform bool useLineAA;
+uniform float lineWidth;
void fragmentClipPlane()
{
@@ -39,6 +42,122 @@ void main()
#else
color.a *= texture2D(alphaTex, vec2(fTexCoord)).r;
#endif
+
+ // Fragment shader antialiasing for lines (combined with MSAA when enabled)
+ if (useLineAA && abs(fLineEdgeDist) > 0.001)
+ {
+ float dist = abs(fLineEdgeDist);
+ float delta = fwidth(dist);
+
+ // Edge transition with asymmetric smoothing
+ float edge0 = 1.0 - delta * 1.6;
+ float edge1 = 1.0 + delta * 0.6;
+ float t = clamp((dist - edge0) / (edge1 - edge0), 0.0, 1.0);
+
+ // Smootherstep interpolation (C2 continuous)
+ float smootherT = t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
+ float alpha = 1.0 - smootherT;
+ alpha = alpha * (1.0 + alpha * 0.03);
+
+ // Dual-ring multi-sampling (8 inner + 4 outer samples)
+ float dx = dFdx(fLineEdgeDist);
+ float dy = dFdy(fLineEdgeDist);
+
+ const float r1 = 0.3535533905932738; // Inner ring: 1/sqrt(8)
+ float s1 = abs(fLineEdgeDist + dx * r1);
+ float s2 = abs(fLineEdgeDist - dx * r1);
+ float s3 = abs(fLineEdgeDist + dy * r1);
+ float s4 = abs(fLineEdgeDist - dy * r1);
+ float s5 = abs(fLineEdgeDist + (dx + dy) * r1 * 0.707);
+ float s6 = abs(fLineEdgeDist + (dx - dy) * r1 * 0.707);
+ float s7 = abs(fLineEdgeDist - (dx + dy) * r1 * 0.707);
+ float s8 = abs(fLineEdgeDist - (dx - dy) * r1 * 0.707);
+
+ const float r2 = 0.5; // Outer ring
+ float s9 = abs(fLineEdgeDist + (dx + dy) * r2);
+ float s10 = abs(fLineEdgeDist + (dx - dy) * r2);
+ float s11 = abs(fLineEdgeDist - (dx + dy) * r2);
+ float s12 = abs(fLineEdgeDist - (dx - dy) * r2);
+
+ // Apply smootherstep to all 12 samples
+ float t1 = clamp((s1 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t2 = clamp((s2 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t3 = clamp((s3 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t4 = clamp((s4 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t5 = clamp((s5 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t6 = clamp((s6 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t7 = clamp((s7 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t8 = clamp((s8 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t9 = clamp((s9 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t10 = clamp((s10 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t11 = clamp((s11 - edge0) / (edge1 - edge0), 0.0, 1.0);
+ float t12 = clamp((s12 - edge0) / (edge1 - edge0), 0.0, 1.0);
+
+ float a1 = 1.0 - t1 * t1 * t1 * (t1 * (t1 * 6.0 - 15.0) + 10.0);
+ float a2 = 1.0 - t2 * t2 * t2 * (t2 * (t2 * 6.0 - 15.0) + 10.0);
+ float a3 = 1.0 - t3 * t3 * t3 * (t3 * (t3 * 6.0 - 15.0) + 10.0);
+ float a4 = 1.0 - t4 * t4 * t4 * (t4 * (t4 * 6.0 - 15.0) + 10.0);
+ float a5 = 1.0 - t5 * t5 * t5 * (t5 * (t5 * 6.0 - 15.0) + 10.0);
+ float a6 = 1.0 - t6 * t6 * t6 * (t6 * (t6 * 6.0 - 15.0) + 10.0);
+ float a7 = 1.0 - t7 * t7 * t7 * (t7 * (t7 * 6.0 - 15.0) + 10.0);
+ float a8 = 1.0 - t8 * t8 * t8 * (t8 * (t8 * 6.0 - 15.0) + 10.0);
+ float a9 = 1.0 - t9 * t9 * t9 * (t9 * (t9 * 6.0 - 15.0) + 10.0);
+ float a10 = 1.0 - t10 * t10 * t10 * (t10 * (t10 * 6.0 - 15.0) + 10.0);
+ float a11 = 1.0 - t11 * t11 * t11 * (t11 * (t11 * 6.0 - 15.0) + 10.0);
+ float a12 = 1.0 - t12 * t12 * t12 * (t12 * (t12 * 6.0 - 15.0) + 10.0);
+
+ // Weighted average: center(3x) + inner_ring(8×1x) + outer_ring(4×0.6x)
+ alpha = (alpha * 3.0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + (a9 + a10 + a11 + a12) * 0.6) / 13.4;
+
+ // Contrast-adaptive sharpening based on sample variance
+ float variance = 0.0;
+ variance += (a1 - alpha) * (a1 - alpha);
+ variance += (a2 - alpha) * (a2 - alpha);
+ variance += (a3 - alpha) * (a3 - alpha);
+ variance += (a4 - alpha) * (a4 - alpha);
+ variance += (a5 - alpha) * (a5 - alpha);
+ variance += (a6 - alpha) * (a6 - alpha);
+ variance += (a7 - alpha) * (a7 - alpha);
+ variance += (a8 - alpha) * (a8 - alpha);
+ variance += (a9 - alpha) * (a9 - alpha);
+ variance += (a10 - alpha) * (a10 - alpha);
+ variance += (a11 - alpha) * (a11 - alpha);
+ variance += (a12 - alpha) * (a12 - alpha);
+ variance /= 12.0;
+
+ float sharpness = smoothstep(0.0, 0.10, variance);
+ float sharpenAmount = sharpness * 0.25;
+ float deviation = alpha - 0.5;
+ alpha = clamp(alpha + deviation * sharpenAmount, 0.0, 1.0);
+
+ // Subtle outer feather for smooth edges
+ if (dist > 0.88 && dist < 1.25)
+ {
+ float featherDist = (dist - 0.88) / 0.37;
+ float featherAlpha = exp(-featherDist * featherDist * 3.5);
+ alpha = alpha * 0.88 + featherAlpha * 0.12;
+ }
+
+ // Sub-pixel coverage correction
+ float pixelWidth = lineWidth * 1000.0;
+ if (pixelWidth < 1.0)
+ {
+ alpha *= mix(pixelWidth, 1.0, pixelWidth * pixelWidth);
+ }
+
+ // Gamma correction for sRGB
+ alpha = pow(clamp(alpha, 0.0, 1.0), 1.0 / 2.2);
+
+ // Dual-frequency dithering to reduce banding
+ vec2 screenPos = gl_FragCoord.xy;
+ float noise1 = fract(52.9829189 * fract(0.06711056 * screenPos.x + 0.00583715 * screenPos.y));
+ float noise2 = fract(31.5491234 * fract(0.09127341 * screenPos.x + 0.04283951 * screenPos.y));
+ float noise = noise1 * 0.6 + noise2 * 0.4;
+ alpha = clamp(alpha + (noise - 0.5) * 0.012, 0.0, 1.0);
+
+ color.a *= alpha;
+ }
+
gl_FragColor = color;
}
)"
diff --git a/lib/gl/shaders/default.vert b/lib/gl/shaders/default.vert
index 4c8aafff..b1bf6b83 100644
--- a/lib/gl/shaders/default.vert
+++ b/lib/gl/shaders/default.vert
@@ -16,8 +16,16 @@ attribute vec4 color;
attribute vec3 normal;
attribute vec2 texCoord0;
+attribute float line_orientation;
+attribute vec3 line_prev_vtx;
+attribute vec3 line_next_vtx;
+
uniform bool containsText;
+uniform bool expandLines;
+uniform float lineWidth;
+uniform float aspectRatio;
+
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 textProjMatrix;
@@ -31,6 +39,7 @@ varying vec4 fColor;
varying vec2 fTexCoord;
varying float fClipVal;
+varying float fLineEdgeDist;
void setupClipPlane(in float dist)
{
@@ -44,6 +53,7 @@ void main()
fNormal = normalize(normalMatrix * normal);
fColor = color;
fTexCoord = texCoord0.xy;
+ fLineEdgeDist = 0.0; // Default: not a line
setupClipPlane(dot(vec4(pos.xyz, 1.0), clipPlane));
pos = projectionMatrix * pos;
gl_Position = pos;
@@ -52,4 +62,83 @@ void main()
vec4 textOffset = textProjMatrix * vec4(textVertex, 0.0, 0.0);
gl_Position += vec4((textOffset.xy * pos.w), -0.005, 0.0);
}
+ // Line expansion (adapted from: https://github.com/mattdesl/webgl-lines)
+ if (expandLines)
+ {
+ fLineEdgeDist = line_orientation; // ±1 at edges, 0 at center
+
+ // Transform to screen space
+ mat4 mvp = projectionMatrix * modelViewMatrix;
+ vec4 prev_clip = mvp * vec4(line_prev_vtx, 1.0);
+ vec4 next_clip = mvp * vec4(line_next_vtx, 1.0);
+
+ vec2 curr_scrn = pos.xy / pos.w;
+ vec2 prev_scrn = prev_clip.xy / prev_clip.w;
+ vec2 next_scrn = next_clip.xy / next_clip.w;
+
+ curr_scrn.x *= aspectRatio;
+ prev_scrn.x *= aspectRatio;
+ next_scrn.x *= aspectRatio;
+
+ float width = lineWidth;
+
+ // Compute line segment direction
+ float dist_to_prev = distance(vertex, line_prev_vtx);
+ float dist_to_next = distance(vertex, line_next_vtx);
+
+ vec2 dir;
+ vec2 tangent_offset = vec2(0.0);
+
+ if (dist_to_prev < 0.0001)
+ {
+ // Start cap (rounded)
+ dir = normalize(next_scrn - curr_scrn);
+ tangent_offset = -dir * lineWidth * 0.5;
+ }
+ else if (dist_to_next < 0.0001)
+ {
+ // End cap (rounded)
+ dir = normalize(curr_scrn - prev_scrn);
+ tangent_offset = dir * lineWidth * 0.5;
+ }
+ else
+ {
+ // Line join
+ vec2 dirA = normalize(curr_scrn - prev_scrn);
+ vec2 dirB = normalize(next_scrn - curr_scrn);
+
+ float dot_dirs = dot(dirA, dirB);
+ if (dot_dirs > 0.99)
+ {
+ // Nearly straight line
+ dir = normalize(dirA + dirB);
+ }
+ else
+ {
+ // Miter join with bevel fallback for sharp angles
+ vec2 tangent = normalize(dirA + dirB);
+ vec2 miter = vec2(-tangent.y, tangent.x);
+ vec2 normal = vec2(-dirA.y, dirA.x);
+ float miter_length = 1.0 / max(dot(miter, normal), 0.001);
+
+ if (miter_length > 2.5)
+ {
+ // Bevel join for sharp angles (< ~75°)
+ dir = dirA;
+ width = lineWidth;
+ }
+ else
+ {
+ dir = tangent;
+ width = lineWidth * miter_length;
+ }
+ }
+ }
+
+ vec2 line_normal = vec2(-dir.y, dir.x);
+ line_normal.x /= aspectRatio;
+ tangent_offset.x /= aspectRatio;
+ vec4 offset = vec4(line_normal * line_orientation * width * pos.w / 2.0 + tangent_offset * pos.w, 0.0, 0.0);
+ gl_Position += offset;
+ }
})"
diff --git a/lib/gl/types.hpp b/lib/gl/types.hpp
index 4ad241cf..a13d8165 100644
--- a/lib/gl/types.hpp
+++ b/lib/gl/types.hpp
@@ -174,7 +174,9 @@ enum array_layout
LAYOUT_VTX_TEXTURE0,
LAYOUT_VTX_NORMAL_COLOR,
LAYOUT_VTX_NORMAL_TEXTURE0,
- NUM_LAYOUTS
+ NUM_LAYOUTS,
+ LAYOUT_EXT_LINE_VTX,
+ LAYOUT_EXT_LINE_VTX_COLOR
};
inline std::array ColorU8(float rgba[])
diff --git a/lib/vsdata.cpp b/lib/vsdata.cpp
index c43d4dc6..e6401a55 100644
--- a/lib/vsdata.cpp
+++ b/lib/vsdata.cpp
@@ -937,7 +937,14 @@ void KeyAPressed()
cout << "Multisampling/Antialiasing: "
<< strings_off_on[!curr_aa ? 1 : 0] << endl;
- // vsdata -> EventUpdateColors();
+ // Force scene update to re-upload buffers with correct line format
+ // (shader-expanded when MSAA on, regular GL_LINES when MSAA off)
+ if (window->vs)
+ {
+ window->vs->PrepareLines();
+ window->vs->Prepare();
+ }
+
SendExposeEvent();
}
diff --git a/makefile b/makefile
index 2fe2e491..e77f2d60 100644
--- a/makefile
+++ b/makefile
@@ -131,7 +131,7 @@ NOTMAC := $(subst Darwin,,$(shell uname -s))
# Default multisampling mode and multisampling line-width
GLVIS_MULTISAMPLE ?= 4
-GLVIS_MS_LINEWIDTH ?= $(if $(NOTMAC),1.4,1.0)
+GLVIS_MS_LINEWIDTH ?= $(if $(NOTMAC),1.4,1.5)
GLVIS_FONT_SIZE ?= 12
DEFINES = -DGLVIS_MULTISAMPLE=$(GLVIS_MULTISAMPLE)\
-DGLVIS_MS_LINEWIDTH=$(GLVIS_MS_LINEWIDTH)\