Speed Run A Rectangle

There are a few things we need to do before we can start rendering triangles, the meatiest of which is shader compilation. There are many ways to get triangles onto the screen in modern OpenGL but all of them require shaders so we will start there.

Shaders

To render triangles in modern OpenGL at least one shader program is required.

What's a shader program?

A shader program in opengl is an assembly of one or more 'shaders' which are 'linked' together.

Shaders are pieces of code that are sent to and execute on the GPU to perform different functions within a pipeline. They are where the bulk of your customisability comes from in your rendering pipeline.

You can read details on the Khronos OpenGL wiki.

To get started we'll need two shaders: a vertex shader, a fragment shader, and some boilerplate for compiling these two shaders and linking them into a 'program'.

For the sake of getting something rendering quickly I will not start with the typical 'hello world' that most tutorials start with, but with something a little more advanced so we can skip some of the other boilerplate we'd normally need.

Try to follow if you can, but if you don't understand it right away don't worry. Understanding can come once we have something to look at.

The Vertex Shader

Lets start at the start of the pipeline, the vertex shader - a small program that is run for each vertex we want to render. We have some creative liberty as to what constitutes a 'vertex' here, but for now lets just start with code:

#version 450

// Our vertex positions - in clip space.
const vec2[4] g_positions = {
	{-0.5,-0.5},
	{ 0.5,-0.5},
	{ 0.5, 0.5},
	{-0.5, 0.5},
};

// Indices into g_positions. Each three indices defines a triangle
// in counter-clockwise winding order.
const uint g_indices[6] = {0, 1, 2, 0, 2, 3};

// The entry point for our shader.
// It will be called once for every vertex.
void main() {
	const uint position_index = g_indices[gl_VertexID];
	const vec2 position = g_positions[position_index];
	gl_Position = vec4(position, 0.0, 1.0);
}

This shader will generate the vertices we need to render two triangles.

This shader is a little unusual but hopefully should be small enough to follow. To fully understand it however, you need to be aware of two special variables:

  • gl_VertexID: Holds the vertex index. When used with glDrawArrays(_, _, num_vertices), will hold a value between zero and num_vertices.
  • gl_Position: The output of the vertex shader. The final position of our vertex (in clip space) should be written here.

See https://www.khronos.org/opengl/wiki/Vertex_Shader for more detail.

The Fragment Shader

Next the fragment shader - a program that is run for each fragment (opengl speak for "maybe a pixel but not quite") and determines (among other things) the colour to be written to the backbuffer. We will be using this shader for now:

#version 450

out vec4 o_color;

// The entry point for our fragment shader.
void main() {
	o_color = vec4(1.0, 0.0, 1.0, 1.0);
}

This shader will write magenta to the backbuffer for each fragment generated during rasterisation - or put more simply, our triangles will be pink.

out vec4 o_color; specifies an output for the shader. The name can be anything, only its type and the fact that it is an output matters. Whatever we write to this variable during an invocation of this shader will be (potentially) written to the backbuffer.

This shader will become more involved later, but for now it will do.

See https://www.khronos.org/opengl/wiki/Fragment_Shader for more detail.

Shader Compilation

This part is a bit gnarly, but its something that only needs to be done once. Shader compilation is not complex, just messy due to the error checking required, and messier still due to needing to deal with nul terminated strings thanks to OpenGL being designed for C.

That said, the process for compiling shaders can be summarised as such:

After this, assuming no errors, you will have a usable program. Though it is worth noting that once linked, shader objects are no longer required and can be cleaned up. It can be detached from the program (glDetachShader) and optionally deleted (glDeleteShader).

Enough summarising, time for code:

use framework::prelude::*;


fn main() {
	framework::run("ch01 - speedrun a rectangle", Example::new);
}



struct Example {
	main_shader: u32,
}

impl Example {
	fn new() -> anyhow::Result<Example> {
		// Create a shader program.
		let main_shader = unsafe {
			// Compile our shaders, bailing if we hit any problems.
			let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?;
			let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?;

			// Create our program and attach our shaders to it for linking.
			let program = gl::CreateProgram();
			gl::AttachShader(program, vert_shader);
			gl::AttachShader(program, frag_shader);

			gl::LinkProgram(program);

			// Clean up the shaders we compiled above since we no longer need
			// them after linking, even if linking failed.
			for shader in [vert_shader, frag_shader] {
				gl::DetachShader(program, shader);
				gl::DeleteShader(shader);
			}

			check_program_status(program)?;

			program
		};

		// Create our empty VAO to allow glDrawArrays to draw without buffers bound.
		unsafe {
			let mut vao = 0;
			gl::CreateVertexArrays(1, &mut vao);
			gl::BindVertexArray(vao);
		}

		Ok(Example {
			main_shader,
		})
	}
}


impl framework::App for Example {
	fn draw(&mut self, state: &framework::State) {
		unsafe {
			let size = state.backbuffer_size();
			gl::Viewport(0, 0, size.x, size.y);

			gl::ClearColor(0.1, 0.1, 0.1, 1.0);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			// This corresponds to the number of indices in our vertex shader.
			let num_vertices = 6;

			gl::UseProgram(self.main_shader);
			gl::DrawArrays(gl::TRIANGLES, 0, num_vertices);
		}
	}
}






fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> {
	let src_c = std::ffi::CString::new(src)?;

	unsafe {
		let shader = gl::CreateShader(ty);
		gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null());
		gl::CompileShader(shader);

		// NOTE: on error, this will technically leak `shader`.
		// But we don't care for now since we're planning to
		// bail immediately on error
		check_shader_status(shader)?;

		Ok(shader)
	}
}


fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetShaderInfoLog(
				shader_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Shader failed to compile: {error_msg}");
		}   
	}

	Ok(())
}


fn check_program_status(program_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetProgramInfoLog(
				program_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Program failed to link: {error_msg}");
		}   
	}

	Ok(())
}

This snippet represents step one of the above process: compiling and error checking an individual shader.

Next we can create a program and start linking:

use framework::prelude::*;


fn main() {
	framework::run("ch01 - speedrun a rectangle", Example::new);
}



struct Example {
	main_shader: u32,
}

impl Example {
	fn new() -> anyhow::Result<Example> {
		// Create a shader program.
		let main_shader = unsafe {
			// Compile our shaders, bailing if we hit any problems.
			let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?;
			let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?;

			// Create our program and attach our shaders to it for linking.
			let program = gl::CreateProgram();
			gl::AttachShader(program, vert_shader);
			gl::AttachShader(program, frag_shader);

			gl::LinkProgram(program);

			// Clean up the shaders we compiled above since we no longer need
			// them after linking, even if linking failed.
			for shader in [vert_shader, frag_shader] {
				gl::DetachShader(program, shader);
				gl::DeleteShader(shader);
			}

			check_program_status(program)?;

			program
		};

		// Create our empty VAO to allow glDrawArrays to draw without buffers bound.
		unsafe {
			let mut vao = 0;
			gl::CreateVertexArrays(1, &mut vao);
			gl::BindVertexArray(vao);
		}

		Ok(Example {
			main_shader,
		})
	}
}


impl framework::App for Example {
	fn draw(&mut self, state: &framework::State) {
		unsafe {
			let size = state.backbuffer_size();
			gl::Viewport(0, 0, size.x, size.y);

			gl::ClearColor(0.1, 0.1, 0.1, 1.0);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			// This corresponds to the number of indices in our vertex shader.
			let num_vertices = 6;

			gl::UseProgram(self.main_shader);
			gl::DrawArrays(gl::TRIANGLES, 0, num_vertices);
		}
	}
}






fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> {
	let src_c = std::ffi::CString::new(src)?;

	unsafe {
		let shader = gl::CreateShader(ty);
		gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null());
		gl::CompileShader(shader);

		// NOTE: on error, this will technically leak `shader`.
		// But we don't care for now since we're planning to
		// bail immediately on error
		check_shader_status(shader)?;

		Ok(shader)
	}
}


fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetShaderInfoLog(
				shader_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Shader failed to compile: {error_msg}");
		}   
	}

	Ok(())
}


fn check_program_status(program_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetProgramInfoLog(
				program_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Program failed to link: {error_msg}");
		}   
	}

	Ok(())
}

check_program_status in the above snippet is very similar to check_shader_status barre some different names

use framework::prelude::*;


fn main() {
	framework::run("ch01 - speedrun a rectangle", Example::new);
}



struct Example {
	main_shader: u32,
}

impl Example {
	fn new() -> anyhow::Result<Example> {
		// Create a shader program.
		let main_shader = unsafe {
			// Compile our shaders, bailing if we hit any problems.
			let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?;
			let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?;

			// Create our program and attach our shaders to it for linking.
			let program = gl::CreateProgram();
			gl::AttachShader(program, vert_shader);
			gl::AttachShader(program, frag_shader);

			gl::LinkProgram(program);

			// Clean up the shaders we compiled above since we no longer need
			// them after linking, even if linking failed.
			for shader in [vert_shader, frag_shader] {
				gl::DetachShader(program, shader);
				gl::DeleteShader(shader);
			}

			check_program_status(program)?;

			program
		};

		// Create our empty VAO to allow glDrawArrays to draw without buffers bound.
		unsafe {
			let mut vao = 0;
			gl::CreateVertexArrays(1, &mut vao);
			gl::BindVertexArray(vao);
		}

		Ok(Example {
			main_shader,
		})
	}
}


impl framework::App for Example {
	fn draw(&mut self, state: &framework::State) {
		unsafe {
			let size = state.backbuffer_size();
			gl::Viewport(0, 0, size.x, size.y);

			gl::ClearColor(0.1, 0.1, 0.1, 1.0);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			// This corresponds to the number of indices in our vertex shader.
			let num_vertices = 6;

			gl::UseProgram(self.main_shader);
			gl::DrawArrays(gl::TRIANGLES, 0, num_vertices);
		}
	}
}






fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> {
	let src_c = std::ffi::CString::new(src)?;

	unsafe {
		let shader = gl::CreateShader(ty);
		gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null());
		gl::CompileShader(shader);

		// NOTE: on error, this will technically leak `shader`.
		// But we don't care for now since we're planning to
		// bail immediately on error
		check_shader_status(shader)?;

		Ok(shader)
	}
}


fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetShaderInfoLog(
				shader_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Shader failed to compile: {error_msg}");
		}   
	}

	Ok(())
}


fn check_program_status(program_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetProgramInfoLog(
				program_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Program failed to link: {error_msg}");
		}   
	}

	Ok(())
}

Once this is done you should have a program ready to render with. We only need to do a couple more things before we can get something on screen.

Finally Rendering?

Rendering our generated, magenta rectangle should now just be a matter of binding our program and emitting the appropriate drawcall. That would look something like this:

use framework::prelude::*;


fn main() {
	framework::run("ch01 - speedrun a rectangle", Example::new);
}



struct Example {
	main_shader: u32,
}

impl Example {
	fn new() -> anyhow::Result<Example> {
		// Create a shader program.
		let main_shader = unsafe {
			// Compile our shaders, bailing if we hit any problems.
			let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?;
			let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?;

			// Create our program and attach our shaders to it for linking.
			let program = gl::CreateProgram();
			gl::AttachShader(program, vert_shader);
			gl::AttachShader(program, frag_shader);

			gl::LinkProgram(program);

			// Clean up the shaders we compiled above since we no longer need
			// them after linking, even if linking failed.
			for shader in [vert_shader, frag_shader] {
				gl::DetachShader(program, shader);
				gl::DeleteShader(shader);
			}

			check_program_status(program)?;

			program
		};

		// Create our empty VAO to allow glDrawArrays to draw without buffers bound.
		unsafe {
			let mut vao = 0;
			gl::CreateVertexArrays(1, &mut vao);
			gl::BindVertexArray(vao);
		}

		Ok(Example {
			main_shader,
		})
	}
}


impl framework::App for Example {
	fn draw(&mut self, state: &framework::State) {
		unsafe {
			let size = state.backbuffer_size();
			gl::Viewport(0, 0, size.x, size.y);

			gl::ClearColor(0.1, 0.1, 0.1, 1.0);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			// This corresponds to the number of indices in our vertex shader.
			let num_vertices = 6;

			gl::UseProgram(self.main_shader);
			gl::DrawArrays(gl::TRIANGLES, 0, num_vertices);
		}
	}
}






fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> {
	let src_c = std::ffi::CString::new(src)?;

	unsafe {
		let shader = gl::CreateShader(ty);
		gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null());
		gl::CompileShader(shader);

		// NOTE: on error, this will technically leak `shader`.
		// But we don't care for now since we're planning to
		// bail immediately on error
		check_shader_status(shader)?;

		Ok(shader)
	}
}


fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetShaderInfoLog(
				shader_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Shader failed to compile: {error_msg}");
		}   
	}

	Ok(())
}


fn check_program_status(program_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetProgramInfoLog(
				program_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Program failed to link: {error_msg}");
		}   
	}

	Ok(())
}

However, if you were to try this now (and you've set up debug callbacks properly) you should get OpenGL complaining at you and refusing to draw anything.

Perhaps it will look something like this:

GL ERROR!
Source:   api
Severity: high
Type:     error
Message: GL_INVALID_OPERATION in glDrawArrays

Why is that?

Because we are missing one final piece of state - a Vertex Array Object.

A VAO is an object that more or less describes how to feed vertices to the GPU. It does so via a number of 'vertex attributes'. Each attribute represents a stream of data that describes some aspect of a 'vertex' (or an instance), like position, color, uvs, etc. On top of information about attributes, a VAO also potentially contains references to buffers of vertex data and information about to turn them into attributes, and information about which attributes should draw from buffers at all.

This last part is why we need a VAO. We don't have any buffers to draw data from, our vertex shader doesn't have any attributes to feed. BUT glDrawArrays still needs to query the VAO to determine if there are any buffers it needs to read. In effect, we just need a VAO that says "no, you don't need to pull data from anywhere, just draw".

Lucky for us this is easy to communicate, as this is the default state of a new VAO.

use framework::prelude::*;


fn main() {
	framework::run("ch01 - speedrun a rectangle", Example::new);
}



struct Example {
	main_shader: u32,
}

impl Example {
	fn new() -> anyhow::Result<Example> {
		// Create a shader program.
		let main_shader = unsafe {
			// Compile our shaders, bailing if we hit any problems.
			let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?;
			let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?;

			// Create our program and attach our shaders to it for linking.
			let program = gl::CreateProgram();
			gl::AttachShader(program, vert_shader);
			gl::AttachShader(program, frag_shader);

			gl::LinkProgram(program);

			// Clean up the shaders we compiled above since we no longer need
			// them after linking, even if linking failed.
			for shader in [vert_shader, frag_shader] {
				gl::DetachShader(program, shader);
				gl::DeleteShader(shader);
			}

			check_program_status(program)?;

			program
		};

		// Create our empty VAO to allow glDrawArrays to draw without buffers bound.
		unsafe {
			let mut vao = 0;
			gl::CreateVertexArrays(1, &mut vao);
			gl::BindVertexArray(vao);
		}

		Ok(Example {
			main_shader,
		})
	}
}


impl framework::App for Example {
	fn draw(&mut self, state: &framework::State) {
		unsafe {
			let size = state.backbuffer_size();
			gl::Viewport(0, 0, size.x, size.y);

			gl::ClearColor(0.1, 0.1, 0.1, 1.0);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			// This corresponds to the number of indices in our vertex shader.
			let num_vertices = 6;

			gl::UseProgram(self.main_shader);
			gl::DrawArrays(gl::TRIANGLES, 0, num_vertices);
		}
	}
}






fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> {
	let src_c = std::ffi::CString::new(src)?;

	unsafe {
		let shader = gl::CreateShader(ty);
		gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null());
		gl::CompileShader(shader);

		// NOTE: on error, this will technically leak `shader`.
		// But we don't care for now since we're planning to
		// bail immediately on error
		check_shader_status(shader)?;

		Ok(shader)
	}
}


fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetShaderInfoLog(
				shader_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Shader failed to compile: {error_msg}");
		}   
	}

	Ok(())
}


fn check_program_status(program_handle: u32) -> anyhow::Result<()> {
	unsafe {
		let mut status = 0;
		gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status);

		if status == 0 {
			let mut length = 0;
			gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length);

			let mut buffer = vec![0u8; length as usize];
			gl::GetProgramInfoLog(
				program_handle,
				length,
				std::ptr::null_mut(),
				buffer.as_mut_ptr() as *mut _
			);

			let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]);
			anyhow::bail!("Program failed to link: {error_msg}");
		}   
	}

	Ok(())
}

The above snippet only needs to run once during setup, and that should be enough to allow us to draw. Normally you'd want to bind your vao just before you draw, but since we only have one its fine to just bind once at startup. This code is only temporary so don't worry too much about it just yet.

So if you try calling glDrawArrays one more time, you should be presented with something like this:

A magenta rectangle in the centre of a grey window

More Resources

The Khronos wiki has a lot of decent information and is worth reading through:

The OpenGL 4.5 Quick Reference Card also contains a reference for GLSL 450 which is worthwhile looking through if you're not already familiar with GLSL