Drawing a Triangle with Colours

Next, we want to make our triangle a bit more colourful. Instead of having the whole triangle be of the same colour, we can make each vertex have a different colour. OpenGL will then interpolate the colours between the vertices and we'll have a nice gradient.

Vertex buffer

First of all we need to add the colour information to our vertex buffer. We could in principle also create a new buffer for the colour information, but since the colour information for each vertex is needed at the same time as the position information, it makes sense to store them as close to each other as possible. This helps with cache locality.

So we will store our vertex data like this:

\([x_1, y_1, z_1, \)   \( r_1, g_1, b_1, \)
 \( x_2, y_2, z_2, \)   \( r_2, g_2, b_2,\)
 \(…]\)

with \((x_1, y_1, z_1)\) as before describing the position of the first vertex and \((r_1, g_1, b_1)\) describing the colour of the the first vertex as an RGB value, and so on.

This is straightforward to translate to rust:

static VERTEX_DATA: [f32; 18] = [
    -0.5, -0.5,  0.0,     0.8,  0.8,  0.0,
     0.0,  0.5,  0.0,     0.0,  0.8,  0.8,
     0.5, -0.5,  0.0,     0.8,  0.0,  0.8,
];

Vertex Shader

Now our vertex shader has the glorious task of taking the colour value for each vertex … and just passing it on.

We need to define an input variable for the colour,

in vec3 color;

and an output variable,

out vec3 v_color;

and then the main function will just set the output to the input:

    v_color = color;

The complete vertex shader now looks like this:

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

in vec3 position;
in vec3 color;

out vec3 v_color;

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

Fragment shader

The fragment shader now has a similarly glorious task as the vertex shader.

First we define the input and output variables:

out vec4 color;
in vec3 v_color;

And now, for every pixel, we get the interpolated colour value from OpenGL and set the pixel colour to that exact value. But additionally we also set the alpha-value to 1.0 to have our triangle be non-transparent:

    color = vec4(v_color, 1.0);

The resulting complete shader:

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

out vec4 color;

in vec3 v_color;

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

Shader attributes

Finally, all that's left to do is to tell OpenGL about our change of the VERTEX_DATA array, and wire up the right values of the array to the vertex-shader's input variables position and color.

The code for the attribute of position (pos_attrib) is almost the same as before, except that we set the stride parameter in gl.VertexAttribPointer to 6 instead of 3, as now the vertex position information starts at exery 6th value.

For the attribute of color (color_attrib), we have almost the same code, but here we not only set the stride parameter to 6, but also the offset to 3, as the colour information first starts after the first three position values:

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

            let color_attrib = gl.GetAttribLocation(program,
                                                    c"color".as_ptr() as *const _);
            gl.VertexAttribPointer(
                color_attrib as gl::types::GLuint,
                3,
                gl::FLOAT,
                0,
                6 * std::mem::size_of::<f32>() as gl::types::GLsizei,
                (3 * std::mem::size_of::<f32>()) as *const () as *const _,
            );
            gl.EnableVertexAttribArray(color_attrib as gl::types::GLuint);

Now, we can run our code and it should look something like this:

Our first coloured triangle

A nice colourful triangle!

Play Around With It

  • Instead of our cyan-magenta-yellow-triangle, try to make a classical RGB triangle where one vertex is just fully red, one vertex is just fully green and one vertex is just fully blue:
Our first coloured triangle in RGB
  • Since the \(z\)-data of all our vertexes is 0 all the time anyway, try to see if you can just drop it from the vertex buffer and instead set it to zero in the fragment shader. You will need to modify both calls to gl.VertexAttribPointer at five locations in the code in addition to the changed vertex buffer VERTEX_DATA and the changed fragment shader.

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,
}

static VERTEX_DATA: [f32; 18] = [
    -0.5, -0.5,  0.0,     0.8,  0.8,  0.0,
     0.0,  0.5,  0.0,     0.0,  0.8,  0.8,
     0.5, -0.5,  0.0,     0.8,  0.0,  0.8,
];

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

in vec3 position;
in vec3 color;

out vec3 v_color;

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

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

out vec4 color;

in vec3 v_color;

void main() {
    color = vec4(v_color, 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,
                                                  c"position".as_ptr() as *const _);
            gl.VertexAttribPointer(
                pos_attrib as gl::types::GLuint,
                3,
                gl::FLOAT,
                0,
                6 * std::mem::size_of::<f32>() as gl::types::GLsizei,
                std::ptr::null(),
            );
            gl.EnableVertexAttribArray(pos_attrib as gl::types::GLuint);

            let color_attrib = gl.GetAttribLocation(program,
                                                    c"color".as_ptr() as *const _);
            gl.VertexAttribPointer(
                color_attrib as gl::types::GLuint,
                3,
                gl::FLOAT,
                0,
                6 * std::mem::size_of::<f32>() as gl::types::GLsizei,
                (3 * std::mem::size_of::<f32>()) as *const () as *const _,
            );
            gl.EnableVertexAttribArray(color_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<_>)
}