From 46d8a9f6d4f22e10cb59c03648d50e96e8b4ce60 Mon Sep 17 00:00:00 2001 From: Tzanio Kolev Date: Fri, 13 Feb 2026 15:08:05 -0800 Subject: [PATCH] Runtime line width (keys ;/') --- CHANGELOG | 13 ++ README.md | 1 + lib/aux_vis.cpp | 22 ++ lib/aux_vis.hpp | 2 + lib/gl/renderer.cpp | 8 + lib/gl/renderer.hpp | 14 +- lib/gl/renderer_core.cpp | 393 ++++++++++++++++++++++++++++++++---- lib/gl/renderer_core.hpp | 6 + lib/gl/shaders/default.frag | 119 +++++++++++ lib/gl/shaders/default.vert | 89 ++++++++ lib/gl/types.hpp | 4 +- lib/vsdata.cpp | 9 +- makefile | 2 +- 13 files changed, 643 insertions(+), 39 deletions(-) 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)\