Drawing a Simple Triangle

In this chapter will will draw our first triangle onto the screen!

Extending the Render State

Our Renderer struct will need to know about three new properties:

  • An OpenGL program: This is the set of shaders that we will want to execute on our hardware.

    The first one, the vertex shader, will calculate the on-screen coordinates of our triangle. Thus it will output a coordinate for each point of our triangle that we put in.

    The second one, the fragment shader (also known as pixel shader), will calculate the color of each pixel of the resulting on-screen triangle. Since we are starting with a simple single-color triangle, our fragment shader will just output a constant colour.

  • An OpenGL Vertex Array Object (VAO): This will store informations on all vertex buffers we are using and their meaning.

    In our case this will contain only our single VBO, together with the information that our vertex buffer contains 3 sets of data (one for each point of the triangle) that each will be bound to the position variable of our OpenGL program and is consisting of three floats each.

  • And finally an OpenGL Vertex Buffer Object (VBO): This will just store the positions of our vertices – i.e. the three points of our triangle.

Each of these three properties will just be an integer (GLUint) that is meant for OpenGL to identify the corresponding internal object:

pub struct Renderer {
    gl: gl::Gl,
    program: gl::types::GLuint,
    vao: gl::types::GLuint,
    vbo: gl::types::GLuint,
}

Our Vertex Buffer

This will be very easy, we just define a simple static array of all our vertex coordinates for our triangle.

This will just be a single array consisting of \([x_1, y_1, z_1, x_2, y_2, z_2, …]\). So \((x_1, y_1, z_1)\) will describe the \(x\)-, \(y\)- and \(z\)-coordinate of our first point, respectively, \((x_2, y_2, z_2)\) will describe the \(x\)-, \(y\)- and \(z\)-coordinate of our second point, respectively, and so on. And since we want to describe a triangle, “and so on” only means one more point.

#[rustfmt::skip]
static VERTEX_DATA: [f32; 9] = [
    -0.5, -0.5,  0.0,
     0.0,  0.5,  0.0,
     0.5, -0.5,  0.0,
];

[Add graphic of coordinate system here]

Here Come the Shaders

This one of the cornerstones of our OpenGL tutorial: The shaders!

As mentioned above, we will need two shaders:

  • A vertex shader. The vertex shader will calculate the on-screen coordinates of our triangle. It get a vertex coordinate from our vertex buffer as an input (which we will later tell OpenGL we want to name position) and outputs the on-srceen coordinates in by setting the output gl_Position. We already set the vertex buffer to contain the on-screen coordinates, so we will just output the same coordinate as as 4-dimensional vector. For now you can just ignore the fourth coordinate. Just remember that it always needs to be set to 1.0, for everything to look normal. (You can play around with a few values close to 1.0 and will see that it acts like an inverse scale factor. That is actually exactly what it does, but we will come to the reasoning for that later on!)

  • A fragment shader. The fragment shader will calculate the color of each pixel of the resulting on-screen triangle. Since we are starting with a simple single-color triangle, our fragment shader will just output a constant colour. The color is given as a 4-dimensional vector describing a point in the RGBA-space. So the first coordinate sets a red-value between 0.0 and 1.0, the second green, the third blue, and finally the fourth value gives an alpha-value, also called “opacity”–this describes how non-transparent the colour is on a scale from 0.0 (completeley transparent) to 1.0 (completely opaque).

I choose a nice orange, here, with no red, 0.7 green, 0.6 fifth of blue and full opacity \((r=0.0, g=0.6, b=0.7, a=1.0)\). Thus our shaders look like this:

const VERTEX_SHADER_SOURCE: &CStr = c"
#version 410 core

in vec3 position;

void main() {
    gl_Position = vec4(position, 1.0f);
}
";

const FRAGMENT_SHADER_SOURCE: &CStr = c"
#version 410 core

out vec4 color;

void main() {
    color = vec4(0.0, 0.7, 0.6, 1.0);
}
";

The shaders need to be given to OpenGL as null-terminated strings (though we could also pass them as a string with length). For this we need to import CStr from std::ffi (or use std::ffi::CStr directly):

use std::ffi::CStr;

As you can see, the shaders are their own small program written in a dialect of C (called OpenGL Shading Language or short GLSL). The first line sets the version of GLSL we want to use–in our case we're using 4.1: #version 330 core.

Next we can define input (in) and output (out) parameters.

For our vertex shader that is the position that we get from our vertex buffer as input: in vec4 position. The output is an OpenGL built-in output variable vec4 gl_Position.

Our fragment shader gets no input as we just want to draw a single colour, but we have to define our output variable which outputs this colour: out vec4 color. (Note that on some platforms you don't have to define the output variable, since there's a built-in called gl_FragColor. But since it's not working on all platforms, we will just define our own output!)

Compiling and Loading our Shaders

Now we have to compile and load our shaders and tell OpenGL about our vertex buffers. All of this happens in Renderer::new.

Compiling the Shaders

in Renderer::new, right after the call to gl::Gl::load_with, we will create our shaders using, gl.CreateShader, gl.ShaderSource and gl.CompileShader:

            let vertex_shader = gl.CreateShader(gl::VERTEX_SHADER);
            gl.ShaderSource(vertex_shader, 1, [VERTEX_SHADER_SOURCE.as_ptr()].as_ptr(), std::ptr::null());
            gl.CompileShader(vertex_shader);

            let fragment_shader = gl.CreateShader(gl::FRAGMENT_SHADER);
            gl.ShaderSource(fragment_shader, 1, [FRAGMENT_SHADER_SOURCE.as_ptr()].as_ptr(), std::ptr::null());
            gl.CompileShader(fragment_shader);

Now we can combine both shaders into what OpenGL calls a "program". Afterwards we can delete the shaders again, as their compilation product will live on in the programm.

            let program = gl.CreateProgram();

            gl.AttachShader(program, vertex_shader);
            gl.AttachShader(program, fragment_shader);

            gl.LinkProgram(program);

            gl.UseProgram(program);

            gl.DeleteShader(vertex_shader);
            gl.DeleteShader(fragment_shader);

Now what's left to do is to create our Vertex Array Object (VAO) and our Vertex Buffer Object (VBO).

First the VAO. Not much to do here:

            let mut vao = std::mem::zeroed();
            gl.GenVertexArrays(1, &mut vao);
            gl.BindVertexArray(vao);

Because we have bound the VAO using gl.BindVertexArray(vao), the VBO we will create now will live on our VAO:

            let mut vbo = std::mem::zeroed();
            gl.GenBuffers(1, &mut vbo);
            gl.BindBuffer(gl::ARRAY_BUFFER, vbo);
            gl.BufferData(
                gl::ARRAY_BUFFER,
                (VERTEX_DATA.len() * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr,
                VERTEX_DATA.as_ptr() as *const _,
                gl::STATIC_DRAW,
            );

And finally we have to tell our program what input variables should come from which part of our buffer:

            let pos_attrib = gl.GetAttribLocation(program, b"position\0".as_ptr() as *const _);
            gl.VertexAttribPointer(
                pos_attrib as gl::types::GLuint,
                3,
                gl::FLOAT,
                0,
                3 * std::mem::size_of::<f32>() as gl::types::GLsizei,
                std::ptr::null(),
            );
            gl.EnableVertexAttribArray(pos_attrib as gl::types::GLuint);

Finally, we need to modify the return value to contain our program, VAO and VBO.

            Self { gl, program, vao, vbo }

New code in Renderer::draw

    fn draw(&self, _state: &mut State) {
        unsafe {
            self.gl.UseProgram(self.program);

            self.gl.BindVertexArray(self.vao);
            self.gl.BindBuffer(gl::ARRAY_BUFFER, self.vbo);

            self.gl.ClearColor(0.1, 0.1, 0.1, 0.9);
            self.gl.Clear(gl::COLOR_BUFFER_BIT);
            self.gl.DrawArrays(gl::TRIANGLES, 0, 3);
        }
    }

Cleanup of OpenGL Objects

Last but not least, there is some cleanup to do. We do not really have to do it, but we don't want our programm to leak any memory and OpenGL objects, especially when it becomes bigger and we might create more than one OpenGL programm.

For this, we implement the Drop trait for Renderer so the cleanup gets done automatically when our renderer is dropped by glwindow, either because of the program exit or because we it needed to recreate the OpenGL context.

impl Drop for Renderer {
    fn drop(&mut self) {
        unsafe {
            self.gl.DeleteProgram(self.program);
            self.gl.DeleteBuffers(1, &self.vbo);
            self.gl.DeleteVertexArrays(1, &self.vao);
        }
    }
}

Run the Code

Let the code run, and if everything works it should look like this:

Our first triangle

Play Around With It

Change the colour of the triangle and the background to your liking.

If your platform supports transparency, play around with different opacity values for both the background and the triangle! What happens, when you set the background completely opaque, but the triangle to transparent or semi-transparent?

Try what happens when you move one of the triangle vertices in the z-direction? (E.g. \(z=2.0\))

Full code

As always, here comes the full code of everything we've done in all the chapters before and this chapter (though some things might just reference previous chapters):

Cargo.toml

Unchanged from Chapter 2's Cargo.toml.

build.rs

Unchanged from Chapter 2's build.rs.

src/main.rs

use std::error::Error;
use std::ffi::{CStr, CString};
use glwindow::AppControl;
use glwindow::event::{WindowEvent, KeyEvent};
use glwindow::keyboard::{Key, NamedKey::Escape};

pub mod gl {
    #![allow(clippy::all)]
    include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs"));
    pub use Gles2 as Gl;
}

pub struct State {}
pub struct Renderer {
    gl: gl::Gl,
    program: gl::types::GLuint,
    vao: gl::types::GLuint,
    vbo: gl::types::GLuint,
}

#[rustfmt::skip]
static VERTEX_DATA: [f32; 9] = [
    -0.5, -0.5,  0.0,
     0.0,  0.5,  0.0,
     0.5, -0.5,  0.0,
];

const VERTEX_SHADER_SOURCE: &CStr = c"
#version 410 core

in vec3 position;

void main() {
    gl_Position = vec4(position, 1.0f);
}
";

const FRAGMENT_SHADER_SOURCE: &CStr = c"
#version 410 core

out vec4 color;

void main() {
    color = vec4(0.0, 0.7, 0.6, 1.0);
}
";

impl glwindow::AppRenderer for Renderer {
    type AppState = State;

    fn new<D: glwindow::GlDisplay>(gl_display: &D) -> Self {
        unsafe {
            let gl = gl::Gl::load_with(|symbol| {
                let symbol = CString::new(symbol).unwrap();
                gl_display.get_proc_address(symbol.as_c_str()).cast()
            });

            let vertex_shader = gl.CreateShader(gl::VERTEX_SHADER);
            gl.ShaderSource(vertex_shader, 1, [VERTEX_SHADER_SOURCE.as_ptr()].as_ptr(), std::ptr::null());
            gl.CompileShader(vertex_shader);

            let fragment_shader = gl.CreateShader(gl::FRAGMENT_SHADER);
            gl.ShaderSource(fragment_shader, 1, [FRAGMENT_SHADER_SOURCE.as_ptr()].as_ptr(), std::ptr::null());
            gl.CompileShader(fragment_shader);

            let program = gl.CreateProgram();

            gl.AttachShader(program, vertex_shader);
            gl.AttachShader(program, fragment_shader);

            gl.LinkProgram(program);

            gl.UseProgram(program);

            gl.DeleteShader(vertex_shader);
            gl.DeleteShader(fragment_shader);

            let mut vao = std::mem::zeroed();
            gl.GenVertexArrays(1, &mut vao);
            gl.BindVertexArray(vao);

            let mut vbo = std::mem::zeroed();
            gl.GenBuffers(1, &mut vbo);
            gl.BindBuffer(gl::ARRAY_BUFFER, vbo);
            gl.BufferData(
                gl::ARRAY_BUFFER,
                (VERTEX_DATA.len() * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr,
                VERTEX_DATA.as_ptr() as *const _,
                gl::STATIC_DRAW,
            );

            let pos_attrib = gl.GetAttribLocation(program, b"position\0".as_ptr() as *const _);
            gl.VertexAttribPointer(
                pos_attrib as gl::types::GLuint,
                3,
                gl::FLOAT,
                0,
                3 * std::mem::size_of::<f32>() as gl::types::GLsizei,
                std::ptr::null(),
            );
            gl.EnableVertexAttribArray(pos_attrib as gl::types::GLuint);

            Self { gl, program, vao, vbo }
        }
    }

    fn draw(&self, _state: &mut State) {
        unsafe {
            self.gl.UseProgram(self.program);

            self.gl.BindVertexArray(self.vao);
            self.gl.BindBuffer(gl::ARRAY_BUFFER, self.vbo);

            self.gl.ClearColor(0.1, 0.1, 0.1, 0.9);
            self.gl.Clear(gl::COLOR_BUFFER_BIT);
            self.gl.DrawArrays(gl::TRIANGLES, 0, 3);
        }
    }

    fn resize(&mut self, width: i32, height: i32) {
        unsafe {
            self.gl.Viewport(0, 0, width, height);
        }
    }
}

impl Drop for Renderer {
    fn drop(&mut self) {
        unsafe {
            self.gl.DeleteProgram(self.program);
            self.gl.DeleteBuffers(1, &self.vbo);
            self.gl.DeleteVertexArrays(1, &self.vao);
        }
    }
}

fn handle_event(_app_state: &mut State, event: WindowEvent)
                -> Result<AppControl, Box<dyn Error>> {
    let mut exit = false;
    match event {
        WindowEvent::CloseRequested => {
            exit = true;
        }
        WindowEvent::KeyboardInput { event: KeyEvent { logical_key: Key::Named(Escape), .. }, .. } => {
            exit = true;
        }
        _ => (),
    }

    Ok(if exit { AppControl::Exit } else { AppControl::Continue })
}

fn main() -> Result<(), Box<dyn Error>> {
    let app_state = State{};
    glwindow::Window::<_,_,Renderer>::new()
        .run(app_state, handle_event as glwindow::HandleFn<_>)
}