Learn OpenGL with Rust

An OpenGL tutorial with lots of basic OpenGL and lots of unsafe Rust code. Uses glwindow, a tiny wrapper around winit and glutin.

To start reading, use your arrow keys ([←], [→]), the page turners on the left and the right of the page, the table of contents on the left, or just click on one of the images below to jump straight in! If you find the page too dark, you can change the colours to another mdBook theme by clicking on the brush in the top left corner of the page.

You can find the complete source code for everything in this tutorial on github.

If you stumble upon any problem, or if you have something else you want to tell me, feel free to open an issue or PR at github, or write me an email to opengl[ AT ]zgtm.eu.

This tutorial is still incomplete and currently being worked on.

– zgtm @ January 2025

Jump Straight In

Start by installing Rust or click on one of the following images to jump directly to the corresponding chapter:

Our first window Our first window with background

Our first triangle Our first coloured triangle

Foreword

Why another OpenGL tutorial?

Like many, I took my first steps with OpenGL around 2010 with the legendary NeHe tutorials on nehe.gamedev.net. But back then they were already starting to become a bit outdated. They were Windows-only and use the glBegin-glEnd-style of OpenGl programming which is no longer supported in current versions of OpenGl since OpenGl 3.3. (Today, the NeHe tutorials are even listed as “Legacy Tutorials” on their page.)

But apart from them being slightly outdated, what actually annoyed me was the usage of a helper libraries that created thin wrappers and helper functions around the OpenGL API, like GLU, for example. While helper libraries can lift some amout of work, I feel they are diametrical to a basic tutorial because they sometimes hide complexity or make very simple things seem more magical than they really are and thus blur the understanding one can have about OpenGl. But one of the worst ones was GLAux, which felt like a set of functions that purely stop you from learning how things work in OpenGL. Nowadays even the NeHe tutorials warn about using GLAux


A few years later, around 2014, I started playing around with OpenGL again, this time on Linux and mostly without helper libraries. I found some setup code that creates an OpenGL context with the SDL2 library. I cannot recall where it came from. But I think I got some (probably most) of the OpenGL code I used from the OpenGl Tutorial on opengl-tutorial.org.

This tutorial still is really good and I had a lot of fun playing around and got to a point where I could just extend my project into new directions no longer bound to the tutorial.

But, alas, this was written in C++. They say “if all you have is a hammer, everything looks like a nail”. All I knew back then was OOP, and thus, everything started to look like an object. I made a complete mess in the end and at some point forgot about it.


Nowadays there also exists a quite good looking tutorial at the Learn OpenGL course on learnopengl.com.

If you are reading my tutorial and are missing something of the more advanced techniques of OpenGL programming, I really encourage you to check out both the OpenGl Tutorial on opengl-tutorial.org and the Learn OpenGL course on learnopengl.com.


Around 2017 I started learning Rust and really fell in love with this language.

There already exists at least one other OpenGl tutorial based on Rust: Learn OpenGL Rust. The main reason that I am writing another one is that that one is based on SDL and I wanted to have a more lightweight library for window and OpenGl context creation.

What I wanted was a small library with minimal overhead but at the same time support for Wayland.

Finally, in 2024, I discovered the winit and glutin crates by the “Rust Windowing” community, which were offering exactly what I wanted:

  • lightweight window creation,
  • lightweight OpenGL context creation,
  • supporting Linux, Mac and Windows,
  • supporting X11, but mostly, also supporting Wayland!
  • And having crate features to disable the support you don't need.

So I rolled up my sleeves, downloaded the glutin example and played around with it. And the code was so simple and usable, that I thought to myself, “this needs to become a tutorial”. The only downside was, that creating the window itself is a bit of a hassle (because of inherent complexity in window creation). So I created a tiny helper crate called glwindow to simplify the window creation to the most basic parameters needed for a tutorial and quite a bit beyond; Just allowing for simple window parameters as:

  • size,
  • resizability,
  • fullscreen display,
  • transparency,
  • cursor capture,

and that's mostly it. You can find all the current parameters you can set in the documentation.

The glwindow crate source is currently less than 500 lines of code. If you start out using the glwindow crate in this tutorial, and your project grows, I encourage you to just copy its sources directly into your project and then refactor to your liking. Because glwindow is so tiny, this will be easy!

What you can expect from this tutorial

This tutorial will not teach good Rust code! While I try not to write the worst Rust you will ever have seen, OpenGL is a C library. So there will be a lot of unsafe code.

Also I believe that OpenGL is learned better if you can read all the OpenGL code in one function (or rather two function, one for initializing and one for drawing). So the code you can read here will be very much like spaghetti code (even though it's just one strand of spaghetti). But that way, you can see every OpenGL API invocation one after each other and in the order that they will be called.

But I firmly want to encourage you to change the structure of the code to your own liking as you work along this tutorial. Write your own helpers, encapsulate and generalize to your needs! That way, you can understand much better what is happening and will get much more out of the tutorial that by simple reading.

As for the unsafe Rust; I have a chapter planned on how to structure things and help encapsulate the unsafety into a small module or crate. If you're really keen on reading that, check back in the summer of 2025.

How to continue afterwards

A tutorial can only be the start of an adventure!

As mentioned above, be sure to have a look at both the OpenGl Tutorial on opengl-tutorial.org and the Learn OpenGL course on learnopengl.com. Both tutorials should provide helpful information for readers of this tutorial when interested in certain details that are not discussed here!

You can find links to documentation and book recommondations in one of the final chapters of this tutorial.

Preparations

Build system setup

First of all, you will need to install Rust! You can find download instructions for Rust for your operating system under rustup.rs.

If this is your first time using Rust, don't worry. We will go through all changes step-by-step and every change you'll need to do we be written out. However you will probably profit from learning rust, for which I recommend reading the fantastic Rust Book.

Here the basic steps for installing Rust on all three major operating systems:

I'm using Linux for writing this tutorial, but I've tested all examples in all three major operating systems! :)

Windows setup

Rust can be installed by downloading rust-init.exe from rustup.rs.

By default, the Rust installer will install the Windows SDK so you can compile Windows applications with Rust. This is needed to run the examples from this tutorial. You can also download and install the Windows SDK from Microsoft directly if you prefer that or did not install it when you installed Rust.

Be sure to add the Rust binarys to your path when asked (the default), so you can invoke cargo from the command line!

MacOS setup

Rust can most easily be installed by running rustup. In your shell, type:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

In order to compile the OpenGL examples in this tutorial, you need to install the Xcode Command Line Tools

MacOS will automatically ask you to install the Xcode Command Line Tools, if they are not installed already, the first time you are compiling any of the examples in this tutorial.

However you can also install them manually by running:

xcode-select --install

Linux setup

Rust can most easily be installed by running rustup. Install curl if it isn't already installed. Then, in your shell, type:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Alternatively you can install cargo and rustc with your package manager. Note that you need at least Rust 1.70, which should be included in most current stable distributions (except Debian, which currently only ships Rust 1.63).

You will also need to install the base development tools that go under different names in different distributions:

Debian- / Ubuntu-based:

sudo apt-get install build-essentials

Fedora- / Redhat-based:

sudo dnf group install c-development development-tools

Arch-based:

sudo pacman base-devel

SuSE-based:

sudo zypper install devel_basis

Creating and Displaying an Empty Window

In this chapter, we will start by

  • setting up our project
  • making it draw an empty window
  • handle events such as key presses

At the end of this chapter, as in any other chapter, you will find the complete code for the chapter. So if something does not seem to work you can check there for things that might look different in your code.

Be sure to have Rust installed and all necessary libraries present on your system.

Initialising a New Rust Project

First of all create a new Rust project with a name of your liking – we'll be using opengl_project for this tutorial. Run:

cargo new opengl_project

We will use the crate glwindow which is a very small wrapper-crate around glutin and winit. It simplifies the usage of winit but thus does not offer all the flexibility (such as creating multiple windows, non-resizeable windows and the-like). If you want to have more control about creating your windows and OpenGl contexts, I recommend having a peek at the outlook chapter of this tutorial.

Add glwindow to your Crates.toml, e.g. by running:

cargo add glwindow@=0.1.0

Some Imports

Now open the Rust file src/main.rs. First of all we're going to need to import

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

Our State, Renderer and Event Handler

Next we will define two struct—for now empty: AppState and AppRenderer. Those two will be responsible for handling the state of our OpenGL app and the OpenGL rendering respectively.

pub struct State {}
pub struct Renderer {}

For glwindow to make sense of our AppRenderer, we need to implement the Renderer trait:

impl glwindow::AppRenderer for Renderer {
    type AppState = State;
    fn new<D: glwindow::GlDisplay>(_gl_display: &D) -> Self {Renderer{}}
    fn draw(&self, _app_state: &mut State) {}
}

We also need an event handler. Let's use an event handler that does not handle any events first:

fn handle_event(_app_state: &mut State, event: WindowEvent)
                -> Result<AppControl, Box<dyn Error>> {
    Ok(EventLoopControl::Continue)
}

The Main Function and Running our Code

Finally we can tie everything up in our main function. We will create an AppState and pass it to glwindow::Window::run together with our event handler function handle_event:

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

Here, glwindow::Window::<_,_,Renderer>::new() creates a new window (the ::<_,_,Renderer> lets it know about the our Render, which we'll implement in the next chapter) and the run method starts the event loop for our window.

Now we can run our programm:

cargo run

You should see something like this:

Our first window
Don't worry that it might seem like the window can not be closed. Just press `Ctrl-C` in your terminal or stop execution of the programm in your IDE to quit the programm and thus also close the window! We'll come to that in a minute!

Maybe the window is just completely transparent and shows what's behind it or maybe when you move the window, it will start showing the solitair effect that you might know from frozen windows. It depends on whether your platform supports transparent windows:

Our first window

Either way, this is because we don't draw anything in our app yet! So what was on the screen before, will still be shown inside of our window.

Well, congratulations! You've now drawn your first window! Let's make it so the user can close it, too.

Adding Event Handling and Allowing the User to Close our Window

Now we want to handle some events! Events are created whenever the user does something with our App:

  • When the mouse is moved, an event is created.
  • When the mouse is clicked, an event is created.
  • When a key is pressed, an event is created.

You get the idea!

Most importantly for us now:

  • When the user tries to close our window, an event is created.

So this is why we can't close our window for now! When we try to close it, an event is created, but it is our responsibility to react to this event and actually close the app.

Whenever an event is created, our event handler function handle_event will be called with this event.

We can do what we want by checking whether the event that is passed to handle_event is of the value WindowEvent::CloseRequested and then closing our app. We can close the app by returning Ok(AppControl::Exit) instead of Ok(AppControl::Continue).

So we can just do that! Replace the function body of handle_event by the following code:

    if let WindowEvent::CloseRequested = event {
        Ok(AppControl::Exit)
    } else {
        Ok(AppControl::Continue)
    }

If you run the code again, you should now be able to close the window as one would expect of a window!

Handling Key Presses as Well

While we're at it, let's also handle key presses. As soon as we're handling more than one type of event, it makes sens to replace the if-else control flow by a match control flow. And since in the future we'll want to handle a lot more events and most of them will not close the app, we'll defer the decision of returning AppControl::Continue or AppControl::Exit to the end of the handle_event function.

A key press will register as a WindowEvent::KeyboardInput valued event. This value has more data, we're only interested if the event field has a KeyEvent value. An inside there we can check whether the logical_key field has a certain value. We can for example check if logical_key is of the value Key::Named(Escape) to check whether the event was created because the user did something with the escape-key.

Here is a definition of handle_event that allows the user to close the window by closing it normally or by just pressing the escape-key:

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

Play Around with it

Try if you can make the app close itself with another key than the escape-key. For example the user could want to press 'Q' to quit the app. For the identification of the keys you can follow the glwindown crate documentation .

E.g. for the key 'Q' you can check whether logical_key has the value Key::Character("q".into()).

The Full Code for This Chapter

At the end of each chapter, I will append the full code of the chapter so you can copy and paste everything to try it out without collecting every tiny bit in all the chapters before. If anything in seems mysterious, though, I recommend checking the chapter for the introduction of this code, though. ;)

Cargo.toml

[package]
name = "empty_window"
edition = "2021"

[dependencies]
glwindow = "0.1"

src/main.rs

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

pub struct State {}
pub struct Renderer {}

impl glwindow::AppRenderer for Renderer {
    type AppState = State;
    fn new<D: glwindow::GlDisplay>(_gl_display: &D) -> Self {Renderer{}}
    fn draw(&self, _app_state: &mut State) {}
}

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<_>)
}

Adding OpenGL Bindings and Drawing a Background Colour

In this chapter we will add the OpenGl bindings to our project, use our OpenGL context and draw a background colour on our window using OpenGL.

OpenGL Bindings

First of all, we will need to generate the OpenGL bindings so we can use them in Rust.

We will add a build script to our project so that we can generate the OpenGL bindings when building our project. We will tell Cargo to cache the bindings and only regenerate them, if the build script changes.

We need the package gl_generator as a build dependency. Add the following two lines to your Cargo.toml:

[build-dependencies]
gl_generator = "0.14"

Now, add a new file called build.rs and add the following contents:

use std::env;
use std::fs::File;
use std::path::PathBuf;

use gl_generator::{Api, Fallbacks, Profile, Registry, StructGenerator};

fn main() {
    let dest = PathBuf::from(&env::var("OUT_DIR").unwrap());

    println!("cargo:rerun-if-changed=build.rs");

    let mut file = File::create(dest.join("gl_bindings.rs")).unwrap();
    Registry::new(Api::Gles2, (3, 0), Profile::Core, Fallbacks::All, [])
        .write_bindings(StructGenerator, &mut file)
        .unwrap();
}

Note: If you're interested in what the output of this buildscript is, just run cargo build. Afterwards, you will find a file called target/debug/build/background-????????????????/out/gl_bindings.rs (where the questionmarks are a hexadecimal string, e. g. in my case cf930aed0e6f0fe0).

Add the following lines to your main.rs to make the OpenGL bindings available to your code:

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

OpenGL Renderer

Now, let's initialise our OpenGL renderer.

In main.rs, our struct Renderer needs to store a Gl handle:

pub struct Renderer {
    gl: gl::Gl,
}

Now we can implement the functions new, draw and add new function resize for the trait implementation of glwindow::AppRenderer for Renderer.

This will need a new import:

use std::ffi::CString;

The new function will call gl::Gl::load_with and store the result in Renderer::gl:

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

        Self { gl }
    }

The draw function will just render the background for our app. We do that by setting the background colour as RGBA quadruple (0.1, 0.1, 0.1, 0.9), which is a dark gray with slight transparency. After setting the background colour using ClearColor, we need to actually draw the background using Clear:

    fn draw(&self, _state: &mut State) {
        unsafe {
            self.gl.ClearColor(0.1, 0.1, 0.1, 0.9);
            self.gl.Clear(gl::COLOR_BUFFER_BIT);
        }
    }

And finally the resize function will just tell OpenGL about the new size of our OpenGL context:

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

See It in Action

This is all we need for now. Try it out! Run

cargo run

and lo and behold

Our first window with background

our window is now drawing a slightly transparent dark gray background on our window—using OpenGL!

And it even redraws when we move the window!

Now we have everything prepared to draw our first triangle in the next chapter.

Play Around With It

Definitely try two things:

  • Change the colour of the background.
  • Change the amount of transparency.

The Full Code for This Chapter

As always, here you can find the complete code for this chapter. You can use this to get up too speed quickly!

Cargo.toml

[package]
name = "background"
edition = "2021"

[dependencies]
glwindow = "0.1"

[build-dependencies]
gl_generator = "0.14"

build.rs

use std::env;
use std::fs::File;
use std::path::PathBuf;

use gl_generator::{Api, Fallbacks, Profile, Registry, StructGenerator};

fn main() {
    let dest = PathBuf::from(&env::var("OUT_DIR").unwrap());

    println!("cargo:rerun-if-changed=build.rs");

    let mut file = File::create(dest.join("gl_bindings.rs")).unwrap();
    Registry::new(Api::Gles2, (3, 0), Profile::Core, Fallbacks::All, [])
        .write_bindings(StructGenerator, &mut file)
        .unwrap();
}

src/main.rs

use std::error::Error;
use std::ffi::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,
}

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

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

        Self { gl }
    }

    fn draw(&self, _state: &mut State) {
        unsafe {
            self.gl.ClearColor(0.1, 0.1, 0.1, 0.9);
            self.gl.Clear(gl::COLOR_BUFFER_BIT);
        }
    }

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

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<_>)
}

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<_>)
}

Drawing a Triangle with Colours

Vertex buffer

\([x_1, y_1, z_1, r_1, g_1, b_1, …]\)

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

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

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

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

            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);
Our first coloured triangle

Play Around With It

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, you can 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<_>)
}

Drawing a Rotating Triangle

use std::time::Instant;
pub struct State {
    begin: Instant,
}
pub struct Renderer {
    gl: gl::Gl,
    program: gl::types::GLuint,
    vao: gl::types::GLuint,
    vbo: gl::types::GLuint,
    rotation: gl::types::GLint,
}
const VERTEX_SHADER_SOURCE: &CStr = c"
#version 410 core

uniform mat3 rotation;

in vec3 position;
in vec3 color;

out vec3 v_color;

void main() {
    gl_Position = vec4(rotation * position, 1.0);
    v_color = color;
}
";
            let rotation =
                gl.GetUniformLocation(program, c"rotation".as_ptr() as *const _);
            Self { gl, program, vao, vbo, rotation }
    fn draw(&self, state: &mut State) {
        let time = Instant::now().duration_since(state.begin).as_millis() % 5000;
        let phi = (time as f32) / 5000.0 * 2.0 * std::f32::consts::PI;

        let rotation: [f32; 9]  = [ phi.cos(),      0.0,  phi.sin(),
                                        0.0,        1.0,        0.0,
                                   -phi.sin(),      0.0,  phi.cos()];
            self.gl.UniformMatrix3fv(self.rotation, 1, 1, rotation.as_ptr());
    let app_state = State{
        begin: Instant::now(),
    };

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 std::time::Instant;
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 {
    begin: Instant,
}

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

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

uniform mat3 rotation;

in vec3 position;
in vec3 color;

out vec3 v_color;

void main() {
    gl_Position = vec4(rotation * 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);

            let rotation =
                gl.GetUniformLocation(program, c"rotation".as_ptr() as *const _);

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

    fn draw(&self, state: &mut State) {
        let time = Instant::now().duration_since(state.begin).as_millis() % 5000;
        let phi = (time as f32) / 5000.0 * 2.0 * std::f32::consts::PI;

        let rotation: [f32; 9]  = [ phi.cos(),      0.0,  phi.sin(),
                                        0.0,        1.0,        0.0,
                                   -phi.sin(),      0.0,  phi.cos()];

        unsafe {
            self.gl.UseProgram(self.program);
            self.gl.UniformMatrix3fv(self.rotation, 1, 1, rotation.as_ptr());

            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{
        begin: Instant::now(),
    };
    glwindow::Window::<_,_,Renderer>::new()
        .run(app_state, handle_event as glwindow::HandleFn<_>)
}

Drawing a 3D-Looking Rotating Triangle

pub struct Renderer {
    gl: gl::Gl,
    program: gl::types::GLuint,
    vao: gl::types::GLuint,
    vbo: gl::types::GLuint,
    rotation: gl::types::GLint,
    perspective: gl::types::GLint,
}
const VERTEX_SHADER_SOURCE: &CStr = c"
#version 410 core

uniform mat3 rotation;
uniform mat4 perspective;

const mat4 translation = mat4(1.0, 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, -1.5, 1.0);

in vec3 position;
in vec3 color;

out vec3 v_color;

void main() {
    gl_Position = perspective * translation * vec4(rotation * position, 1.0);
    v_color = color;
}
";
            let perspective =
                gl.GetUniformLocation(program, c"perspective".as_ptr() as *const _);
            Self { gl, program, vao, vbo, rotation, perspective }
        let near: f32 = 1.0;
        let far: f32 = 10.0;
        let left: f32 = -0.5;
        let right: f32 = 0.5;
        let bottom: f32 = -0.5;
        let top: f32 = 0.5;

        // from http://learnwebgl.brown37.net/08_projections/projections_perspective.html
        let perspective: [f32; 16]  = [ 2.0*near/(right-left),  0.0,  0.0,  -near*(right+left)/(right-left),
                                        0.0,  2.0*near/(top-bottom),  0.0,  -near*(top+bottom)/(top-bottom),
                                        0.0,  0.0,  -(far+near)/(far-near),  2.0*far*near/(near-far),
                                        0.0,  0.0,  -1.0,  0.0];
            self.gl.UniformMatrix3fv(self.rotation, 1, 1, rotation.as_ptr());
            self.gl.UniformMatrix4fv(self.perspective, 1, 1, perspective.as_ptr());

Using a Math Library

pub struct Renderer {
    gl: gl::Gl,
    viewport_size: (i32, i32),
    program: gl::types::GLuint,
    vao: gl::types::GLuint,
    vbo: gl::types::GLuint,
    rotation: gl::types::GLint,
    perspective: gl::types::GLint,
}
        }
        let perspective = glam::Mat4::perspective_rh_gl(1.0, aspect_ratio, 0.5, 10.0);

            self.gl.UniformMatrix4fv(self.perspective, 1, 0, (&perspective as *const _) as *const _);

            self.gl.BindVertexArray(self.vao);
        unsafe {

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

[package]
name = "rotating_triangle_3d_glam"
edition = "2021"

[dependencies]
glam = "0.29.2"
glwindow = "0.1"

[build-dependencies]
gl_generator = "0.14"

build.rs

Unchanged from Chapter 2's build.rs.

src/main.rs

use std::error::Error;
use std::ffi::{CStr, CString};
use std::time::Instant;
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 {
    begin: Instant,
}

pub struct Renderer {
    gl: gl::Gl,
    viewport_size: (i32, i32),
    program: gl::types::GLuint,
    vao: gl::types::GLuint,
    vbo: gl::types::GLuint,
    rotation: gl::types::GLint,
    perspective: gl::types::GLint,
}

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

uniform mat3 rotation;
uniform mat4 perspective;

const mat4 translation = mat4(1.0, 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, -1.5, 1.0);

in vec3 position;
in vec3 color;

out vec3 v_color;

void main() {
    gl_Position = perspective * translation * vec4(rotation * 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);

            let rotation =
                gl.GetUniformLocation(program, c"rotation".as_ptr() as *const _);
            let perspective =
                gl.GetUniformLocation(program, c"perspective".as_ptr() as *const _);

            Self { gl, viewport_size: (0, 0), program, vao, vbo, rotation, perspective }
        }
    }

    fn draw(&self, state: &mut State) {
        let time = Instant::now().duration_since(state.begin).as_millis() % 5000;
        let phi = (time as f32) / 5000.0 * 2.0 * std::f32::consts::PI;

        let rotation = glam::Mat3::from_rotation_y(phi);
        let aspect_ratio = self.viewport_size.0 as f32 / self.viewport_size.1 as f32;
        let perspective = glam::Mat4::perspective_rh_gl(1.0, aspect_ratio, 0.5, 10.0);

        unsafe {
            self.gl.UseProgram(self.program);
            self.gl.UniformMatrix3fv(self.rotation, 1, 0, (&rotation as *const _) as *const _);
            self.gl.UniformMatrix4fv(self.perspective, 1, 0, (&perspective as *const _) as *const _);

            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) {
        self.viewport_size = (width, height);
        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{
        begin: Instant::now(),
    };
    glwindow::Window::<_,_,Renderer>::new()
        .run(app_state, handle_event as glwindow::HandleFn<_>)
}

Website and Book Recommondations

OpenGl Learning Resources

OpenGl Tutorials complementing this one

Browse through the chapters, see what's interesting and try to reimplement that in Rust!

OpenGl References

Rust Crates

Computer Graphics in General

Books

Real-Time Rendering, Fourth Edition by Tomas Akenine-Möller, Eric Haines, Naty Hoffman, Angelo Pesce, Michał Iwanicki, and Sébastien Hillaire – real good overview over the basic concept of everything in 3D rendering.

Physically Based Rendering: From Theory To Implementation, Fourth Edition by Matt Pharr, Wenzel Jakob, and Greg Humphreys – more focused on physical realism and ray-tracing.