Drawing lines using OpenGL and Rust

I’ve been playing with graphics programming in Rust recently using glium, which is an OpenGL wrapper. One of the things that I wanted to do was draw a map using lines from a file. OpenGL works by drawing triangles, so I had to figure out a way to turn a line into one or more triangles.

After Googling a bit, I found this blog post from Mapbox about drawing antialiased lines using OpenGL. It looked like what I wanted, but it took some experimentation to figure out exactly what I needed to do to get it to work, so I decided to write it down here. I’m also just beginning to learn about OpenGL, so I’m going to explain many of the things I learned about OpenGL while figuring this out. If you already know OpenGL basics you might be able to just look at the code samples in this post.

The general concept is to think of each line segment as a very long and thin rectangle. One side is the length of the line, and the other side is the width of the line. A rectangle can be drawn as two triangles by dividing it along one of the diagonals.

Okay, so now we need to generate all the vertices (points). We could calculate all four corners right away, but since I want to be able to zoom in and out, that won’t work. When we zoom in, the lines would get longer (which we want), but also wider (which we don’t want). Instead we’re going to store the endpoints of the line, and the direction we need to move the vertices to get them in the right place. I ended up with this Vertex struct definition:

#[derive(Copy, Clone, Debug)]
struct Vertex {
    a_pos: [f32; 2],
    a_normal: [f32; 2],
    a_color: [f32; 3], 
}

implement_vertex!(Vertex, a_pos, a_normal, a_color);

The a_pos attribute will contain the “start position” of the vertex before we move it to give the line width. For each line, we’ll have four vertices: two at each end.

The a_normal attribute contains a normal unit vector for the line. “Normal” comes from geometry and means that the vector is perpendicular to our line. A unit vector is a vector that has length 1. In short, a_normal is a vector that points in the direction we want to move a_pos to move it to the corner of the rectangle. Since it’s 1 unit long, we’ll need to multiply it by half the width of the line before adding it to a_pos.

Finally, the a_color attribute stores the color of the line. I’m just using 24-bit colors, so using 12 bytes is a bit overkill, but I haven’t figured out how to use u8s instead of f32s yet.

Now that we have a data structure for the vertices, we need to make all the vertices and store them somewhere. Since we’re drawing triangles, not rectangles, each triangle will have two points that are shared with the other triangle in the rectangle. Instead of storing those vertices twice, we’ll store a list of indices, which tells OpenGL which vertices make up a triangle. Take this rectangle for example:

1----0 <-- This is the start of the line
|   /|
|  / |
| /  |
|/   |
3----2 <-- This is the end of the line

The points 1 and 4 are the same for both triangles, so we can tell OpenGL that point 0, 1 and 3 make a triangle, and point 0, 3 and 2 make another triangle. We’ll use a Vec<Vertex> to store the vertices and a Vec<u32> to store the indices:

let vertices: Vec<Vertex> = Vec::new();
let indices: Vec<u32> = Vec::new(); 

Let’s first define a line that we want to draw. I’ll chose a line from (-0.5, -0.5) to (0.5, 0.5), which will go diagonally across the screen from the lower left portion of the screen to the upper right portion of the screen:

let (x1, y1) = (-0.5, -0.5);
let (x2, y2) = (0.5, 0.5);

To make the normal unit vector, we need to know the length of the line, as well as the “length” it has along the x and the y-axis:

let dx = x2 - x1;
let dy = y2 - y1;
let length = (dx.powi(2) + dy.powi(2)).sqrt();

We actually have two normal unit vectors for each point, one in each direction perpendicular to the line. I found out that the normal unit vectors would be (-dy/length, dx/length) and (dy/length, -dx/length). See the end of the blog post for a longer explanation of why that works.

vertices.push(Vertex {
    a_pos: [x1, y1],
    a_normal: [-dy / length, dx / length],
    a_color: color,
});
vertices.push(Vertex {
    a_pos: [x1, y1],
    a_normal: [dy / length, -dx / length],
    a_color: color,
});
vertices.push(Vertex {
    a_pos: [x2, y2],
    a_normal: [-dy / length, dx / length],
    a_color: color,
});
vertices.push(Vertex {
    a_pos: [x2, y2],
    a_normal: [dy / length, -dx / length],
    a_color: color,
});

let start = vertices.len() as u32 - 4;
indices.append(&mut vec![start + 0, start + 3, start + 2]);
indices.append(&mut vec![start + 0, start + 1, start + 3]);

If you look at that example line again, you’ll see that the indices are added in the counter-clockwise direction:

1----0 <-- This is the start of the line
|   /|
|  / |
| /  |
|/   |
3----2 <-- This is the end of the line

This allows us to tell OpenGL to only draw one side of the triangle (we’re just making a 2D map, so drawing the back of it isn’t necessary) using something called face culling. I’m not doing this yet, since the performance seems to be fast enough without it (again, just boring 2D instead of 3D), but at least it’s possible.

Now we need to write the shaders that take these vertices, move them all around and colors the lines. Let’s start with the vertex shader:

#version 150
// Version 150 was the lowest version that worked on my Mac.

// The attributes from the Vertex struct
in vec2 a_pos;
in vec2 a_normal;
in vec3 a_color;

// We'll output the color again so the fragment shader can use it.
out vec3 v_color;

// We have three uniforms set: The width of the line, the modelview
// transformation matrix and the perspective transformation matrix
// (more on the transformation matrices later).
uniform float u_linewidth;
uniform mat4 u_mv_matrix;
uniform mat4 u_p_matrix;

void main() {
    // Set v_color so the fragment shader gets access to the color.
    v_color = a_color;
    // Multiply the normal vector by half the line width to make a
    // delta vector that we can add to the position to give the line
    // the right width.
    vec4 delta = vec4(a_normal * (u_linewidth / 2.0), 0, 0);
    // Multiply a_pos by the modelview matrix to move it to the right
    // place on the screen and zoom.
    vec4 pos = u_mv_matrix * vec4(a_pos, 1, 1);
    // Finally, add the delta and multiply with the perspective matrix
    // (which corrects for the aspect ratio).
    gl_Position = u_p_matrix * (pos + delta);
}

Now we have all the rectangles, and we need to give them some color. Since everything has the same solid color, the fragment shader is quite simple:

#version 150

in vec3 v_color;
out vec4 color;

void main() {
    color = vec4(v_color, 1.0);
}

Once we have all this, we can draw the lines using glium:

let vertex_buffer =
    glium::VertexBuffer::new(&display, &vertices).unwrap();
let index_buffer =
    glium::IndexBuffer::new(
        &display,
        glium::index::PrimitiveType::TrianglesList,
        &indices
    ).unwrap();

let program =
    glium::Program::from_source(
        &display,
        vertex_shader_src,
        fragment_shader_src,
        None
    ).unwrap();

let zoom_level = 1.0;
let center = (0.1, 0.1);

loop {
    let mut target = display.draw();
    target.clear_color_and_depth((0.0, 0.0, 0.0, 1.0), 1.0);

    let (width, height) = target.get_dimensions();
    let aspect_ratio = height as f32 / width as f32;
    let line_width = 2.0f32 / (height as f32);

    let perspective = [[aspect_ratio, 0.0, 0.0, 0.0],
                       [0.0, 1.0, 0.0, 0.0],
                       [0.0, 0.0, 1.0, 0.0],
                       [0.0, 0.0, 0.0, 1.0f32]];

    let modelview = [[zoom_level, 0.0, 0.0, 0.0],
                     [0.0, zoom_level, 0.0, 0.0],
                     [0.0, 0.0, 1.0, 0.0],
                     [(-center.0 as f32), (-center.1 as f32), 0.0, 1.0f32]];

    let uniforms = uniform! {
        u_linewidth: line_width,
        u_mv_matrix: modelview,
        u_p_matrix: perspective,
    };

    target.draw(
        &vertex_buffer,
        &index_buffer,
        &program,
        &uniforms,
        Default::default()
    ).unwrap();
    target.finish().unwrap();
}

The Mapbox blog post had some details on anti-aliasing and line joins too, but I’m just drawing really thin lines (1-2 pixels wide), so 4x MSAA and no line joins looks good to my eyes.

Making a normal unit vector

Earlier I said that the normal unit vectors would be (-dy/length, dx/length) and (dy/length, -dx/length). I got this by first making the vector (dx, dy) (which is a vector that has the same direction and length as the line segment we’re drawing), and divided it by the length of the line segment to make a unit vector. To make it a normal, we need to rotate it 90°, which we can do by using a rotation matrix:

$$ \left[\matrix{\cos\theta & -\sin\theta \cr \sin\theta & -\cos\theta}\right]\left[\matrix{\frac{dx}{length}\cr\frac{dy}{length}}\right] \\ = \left[\matrix{\frac{dx\cos\theta}{length} - \frac{dy\sin\theta}{length}\cr \frac{dx\sin\theta}{length} - \frac{dy\cos\theta}{length}}\right] = \left[\matrix{\frac{dx\cos\theta - dy\sin\theta}{length} \cr \frac{dx\sin\theta - dy\cos\theta}{length}}\right] $$

Since we’re rotating by 90°, $ \cos\theta $ becomes 0 and $ \sin\theta $ becomes 1, and we end up with

$$ \left[\matrix{\frac{-dy}{length} \cr \frac{dx}{length}}\right] $$

SSL on nginx and FreeBSD using Let's Encrypt

Note: These instructions were written on December 3rd, 2015, the day the public beta was launched. If you’re reading this in the future, the instructions may not be up-to-date anymore. I’d recommend checking the official documentation, but as of today there wasn’t much on FreeBSD+nginx, so I decided to write down some things here.

Let’s Encrypt entered public beta today, so I decided to try it out. It turns out that it’s pretty smooth, even if things are still in beta.

First of all, you need the Let’s Encrypt client. You could get letsencrypt-auto from the Let’s Encrypt Git repo, but FreeBSD has letsencrypt in the port tree now, so I decided to go for that:

cd /usr/ports/security/py-letsencrypt
sudo make install

It looks like there’s supposed to be an nginx plugin that makes everything a lot easier, but for now we need to set things up manually. As far as I can tell, only the standalone mode is usable right now for the client that’s installed on FreeBSD. To generate a certificate:

$ sudo letsencrypt certonly \
    --standalone-supported-challenges tls-sni-01 \
    --server https://acme-v01.api.letsencrypt.org/directory

The --server argument is required for now, since the client currently still defaults to a sandbox of some sort. I also got a big warning saying that the certificates issues would not be valid, but as you can see if you’re visiting this site over https, they work just fine. Guessing this will disappear in the next version of the client.

The client will bind to port 443 in order to authenticate with the Let’s Encrypt servers to validate that the domain you provided is actually yours (if you prefer, you can bind to port 80 by switching tls-sni-01 out with http-01). Make sure you don’t have a web server running just yet, or stop it temporarily. The authentication process only takes about a minute with the little wizard.

In the end you end up with some certificates in /usr/local/etc/letsencrypt/live/your-domain. The /usr/local/etc/letsencrypt has some other directories, apparently it keeps your old certificates and keys when you renew, so but symlinks the current one into the live/ subdirectory. The documentation suggested that again symlinking this into your web server config was a good idea, so that’s what I did:

sudo ln -s \
    /usr/local/etc/letsencrypt/live/your-domain/privkey.pem \
    /usr/local/etc/nginx/your-domain.key
sudo ln -s \
    /usr/local/etc/letsencrypt/live/your-domain/fullchain.pem \
    /usr/local/etc/nginx/your-domain.crt

There are other files in the directory too, see the Let’s Encrypt docs for information on what they all do. I just symlinked in the ones that nginx needs. Now we just need to set up the server block in nginx:

server {
    listen 443 ssl;
    server_name your-domain;
    ssl_certificate your-domain.crt;
    ssl_certificate_key your-domain.key;
}

I’m also using the configuration from Cipherli.st for some better configuration than the defaults. Probably overkill for a blog, but I might as well.