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
Now that we understand what we’re doing, we can stop doing it and use a library! :)
We will use the crate glam. glam is a pure-rust 3D math library that offers to generate rotation and perspetive matrices for us. It can do a lot more with matrices and also with quaternions, should you wish to use them later for managing rotations.
We need to add glam to the [dependencies] section in our Cargo.toml:
glam = "0.29.2"
Not only does glam help us calculate a perspective transform, it will also take the aspect ratio of our window into account, so our triangle doesn’t get all squishy. For this we need to add information about the viewport, viewport_size: (i32, i32), to our Renderer:
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,
}
We will initialize the viewport with (0, 0), as at the point of building the renderer, the window has not been created yet, so we don’t actually know what size the window has. Soon, we will add the code that sets viewport_size to the correct value as soon as the window has been created
Self { gl, viewport_size: (0, 0), program, vao, vbo, rotation, perspective }
Now, assuming that viewport_size is correctly set, we can calculate the aspect ratio of our window. With that we can calculate our rotation and perspective matrices using the glam methods glam::Mat4::rotation_y and glam::Mat4::perspective_rh_gl, respectively.
The rh in glam::Mat4::perspective_rh_gl means that we are generating a perspective transform for a right-handed coordinate system (as OpenGL does) and gl means that the near and far planes are mapped into \(z \in [-1,1]\), as OpenGL expects. (DirectX, in contrast, expects a left-handed coordinate system and \(z \in [0,1]\).)
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);
Now, since we are using glam matrices instead of our own hard-coded arrays, we’ll need to change a bit how we pass the pointer to the internal data to OpenGL. Luckily for us, glam matrices internally are still just a continuous strip of f32 values in memory, we can just convert the matrices into the pointers we need:
self.gl.UniformMatrix3fv(self.rotation, 1, 0, (&rotation as *const _) as *const _);
self.gl.UniformMatrix4fv(self.perspective, 1, 0, (&perspective as *const _) as *const _);
In our resize method, we can finally set the viewport_size of the Renderer. This function will be called at least once, when the window is created. So we can be sure, that our viewport_size always contains the actual size of our window.
self.viewport_size = (width, height);
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<_>)
}