Lines
What?
This repository explores different ways of rendering wide lines using OpenGL. Rather than a library, it is meant to be a number of reference implementations to produce thick, anti-aliased lines in OpenGL. Currently there are 5 implementations available in this repo:
- OpenGL lines - using
glLineWidth(width_value)
functionality. - CPU lines - extending lines to quads on the CPU.
- Geometry Shaders - extending lines to quads on GPU during Geometry Shader stage.
- Instancing Lines - hijacking the instancing functionality to render a number of line segments by repeating instanced quad.
- Texture buffer lines - using combination of a texture that stores line data and using gl_VertexID to sample data from the texutre, in order to generate quads in the vertex shader.
- SSBO lines - Analogous to approach no. 5, where instead of using Texture Buffer Object we use SSBO.
For simplicity, the implementation assume vertex buffer where each pair of points creates a line segment (GL_LINES
behavior)
Why?
The reason for exploring different implementations of wide line rendering is stemming from the fact that using the build-in OpenGL functionality for this task is very limited, if working at all. While combining glLineWidth(width_values)
and glEnable(GL_LINE_SMOOTH)
"can" be used to produce anti-aliased lines, there are a number of issues:
-
No guarantee that this approach will work. Implementations of OpenGL vary across systems - the
glGet( GL_LINE_WIDTH_RANGE, range)
can simply tell you that your implementation only supports a range (1.0,1.0], leading to effectively no control over the line width. On my system at the time of writing (NVidia GTX 1070 Max-Q, OpenGL 4.5, driver 441.08) the supportedGL_LINE_WIDTH_RANGE
is [1.0, 10.0]. As such I am able to produce image below, but your milage may vary: -
Limited range. As stated above the
glGet( GL_LINE_WIDTH_RANGE, ...)
line width will return supported range - creating lines with widths outside this range will fail. For example, using implementations 1 (usingGL_LINES
+glLineWidth
) vs other four will produce following images when asking to draw 15 lines segments with widths between 1 and 15 pixels:
- No control over AA.
glEnable(GL_LINE_SMOOTH)
enables anti-aliasing, however we have no control over this behavior. The custom implementations (2-5) allow user to specify the width of the smoothed region. In the figure below the smoothing radius is varied between 0 and 5 pixels wide:
- No control over line width within a draw call. Due to the nature of
glLineWidth(width_value)
the line width need to constant within a draw call. This means multiple draw calls needs to be issue to render lines with varying widths. In contrast, the custom implementation expose line width as a per-vertex attribute, allowing to render variable-width lines with a single draw call.
Methods
All custom line-drawing methods follow the same baseline paradigm - drawing a quad that is oriented as the line segment, and aligned with the viewport. The gross simplification of the general algorithm is:
- Transform line segment end points
p
andq
to normalized device space. At this point we work in 2D space, regardless of whether original line was specified as 2D or 3D line. - Calculate direction
d
, corrected for viewport aspect ratio. - Calculate unit normal vector
n = {-d.y, d.x}
- Modulate
n
by approperiate line widths to get vectorsn_a
andn_b
- Create quad points
a = p + n_a
,b = p - n_a
,c = q + n_b
,d = q - n_b
- Move points
a
,b
,c
,d
back to clip space and pass them down the rasterization pipeline
The smoothing is done by adding the requested smoothing radius to the user-specified line width, and modifying the alpha value based on the distance from the edge, giving the quad a total width of w+2r
, as seen below:
The circle area along the edge at length w
from the actual line is the area of smoothing, based on ideas from
Fast Prefiltered Lines. Essentially, the alpha value will be modulated between 0 and 1 in the region between h-r
and h+r
, where h=0.5w
:
The falloff is controlled using the GLSL build-in smoothstep
function.
Different method vary in terms of how a line segment between p
and q
is transformed into such grid. Read on for a brief differences in implementations:
Below we discuss different variants of the method
CPU lines
Least efficient implementation, as it simply takes in the buffer of line segments and expands it on CPU side into quad buffer, and passes it into OpenGL which simply rasterizes the triangles. While it's possible to use array+element buffer in this instance, for simplicity, each quad is represented as two triangles with vertices duplicated:
In general, this is a decent way to go about it if your OpenGL version does not support any other option.
Geometry Shader lines
Using geometry shaders is a straightforward extension of the CPU Lines approach, simply moving the calculation onto the GPU side. Vertex Shader computes the clip coordinates for both endpoints and passes them to the Geometry Shader stage. The Geometry Shader emits a triangle strip, with 4 vertices per each line segment. This implementation is based on the Im3D work by John Chapman.
Instancing lines
In this implementation a simple base-quad is drawn for each line segment, and the vertex positions are modulated in the vertex shader. The base-quad has following positions:
Three buffers are bound in total: one describing the actual quad (array + element buffer), and a buffer describing line geometry. In vertex shader, we get the attributes of endpoints and modify the quad positions accordingly. The idea behind this implementation is explained in Instanced Line Rendering
Texture Buffer lines
This implementation renders the triangles out of thin air using gl_VertexID
. We ask to render 2 triangles for each line segment and in the Vertex Buffer, based on 'gl_VertexID' we can sample appropriate positions from a texture buffer which stores our line locations. This implementation is loosely based on ideas presented in OpenGL Blueprint Rendering
SSBO lines
This implementation is extremly close to the "Texture Buffer" approach. However, the use of the SSBO allows for much simple logic to address the memory to pull the vertices from.
Compilation
The project is relatively simple to build. The external dependency not included in this repository is glfw3. You also need a OpenGL 4.5 to run this code, due to usage of OpenGL DSA APIs.
If you have a command line compiler like clang of gcc, and glfw3 is in path, you could simply run a following command in your terminal (± the OpenGL libs, depending on your system)
gcc -std=c11 -I. extern/glad.c main.c -o lines -lglfw3 -lopengl32
The source tree also includes a CMakeLists.txt to generate build files, if that's your jam.
References
OpenGL Blueprint Rendering; Kubisch '16
Fast Prefiltered Lines; Chan & Durand '05
Shader-Based Antialiased, Dashed, Stroked Polylines, Rougier '13