Drawing a Rotating Triangle
Now let's get moving – literally!
We are going to animate our triangle, by making it rotate. For this, we will just take the time since the program has started running, a give the triangle a rotation value based on that time:
\(\varphi(t) = \frac{1}{T} \cdot t\)
where T can be any time we would like the rotation to take. Here, we will let the triangle fully rotate once every 5 seconds, so \(T = 5\texttt{s}\).
Storing in an instant
First of all, in order to measure the time since program start, we'll need to import Instant
from std::time
:
use std::time::Instant;
We will store the Instant
of the start of the program in the State
as begin
.
pub struct State {
begin: Instant,
}
Uniform variables
We will add a new variable to our fragment shader, which will contain the rotation matrix to rotate our triangle. For this we will create a uniform variable – "uniform" meaning that it is constant for all invocations of the shader for the same object. So in our case, we will give the same rotation matrix to the invocation of the vertex shader for all three vertices.
In order to have a reference to OpenGL concept of the uniform variable, we will add it to our Renderer
object:
pub struct Renderer {
gl: gl::Gl,
program: gl::types::GLuint,
vao: gl::types::GLuint,
vbo: gl::types::GLuint,
rotation: gl::types::GLint,
}
Inside the vertex shader, we can now define the uniform variable by
uniform mat3 rotation;
Now we get the vertex position by multiplying the rotation matrix to the original vertex position from the left:
gl_Position = vec4(rotation * position, 1.0);
So our vertex shader now looks like this:
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;
}
";
Finally, in the new
method of our Renderer
we need to get the OpenGL reference to the uniform variable
let rotation =
gl.GetUniformLocation(program, c"rotation".as_ptr() as *const _);
and store it in the new Renderer
:
Self { gl, program, vao, vbo, rotation }
Building the rotation matrix
Firstly, we need to calculate our rotation angle \(\varphi\). We could simply do this:
let time = Instant::now().duration_since(state.begin).as_millis();
let phi = (time as f32) / 5000.0 * 2.0 * std::f32::consts::PI;
However, an f32
has a precision of 23 and since \(2^23 = 8388608\), our time variable converted into f32
will lose precision after \(8388608~\textrm{ms} \approx 2.3 ~\textrm{hours}\).
Thus it makes more sense to take the time modulo 5000:
let time = Instant::now().duration_since(state.begin).as_millis() % 5000;
Having our value of phi, we can now define our rotation matrix
let rotation: [f32; 9] = [ phi.cos(), 0.0, phi.sin(),
0.0, 1.0, 0.0,
-phi.sin(), 0.0, phi.cos()];
With that our `draw function looks like this:
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()];
Inside the unsafe
block, we can now set the uniform variable rotation
to our rotation matrix rotation
.
self.gl.UniformMatrix3fv(self.rotation, 1, 1, rotation.as_ptr());
The two parameters
Define the beginning of time
All that's missing now is just to initialize our app state with the current time at programm start:
let app_state = State{
begin: Instant::now(),
};
Running the code should then give you the rotating triangle, you've been waiting for:
Play Around With It
-
Let the triangle rotate around another axis: The x-axis or the z-axis!
-
Can you make the triangle rotate around another axis? (E.g. around the axis defined by the vector \((1, 1, 0)\)?)
-
What happens if you move the triangle by 0.6 in the z-direction (either way)? What if you move it by 0.8, 1.0 or 1.2?
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<_>)
}