OpenGL Programming/Modern OpenGL Tutorial 05
Our triangle animation is fun, but we're learning OpenGL to see 3D graphics.
Let's create a cube!
Adding the 3rd dimension
editA cube is 8 vertices in the 3D space (4 points in the front face, 4 in the back face).
triangle
can be renamed to cube
.
Also comment out the fade
bindings.
Now let's write our cube vertices. We'll position our (X,Y,Z) coordinate system like in this picture. We'll write them so they are related to the center of the object. It's cleaner, and it will allow us to rotate the cube around its center later:
Note: here, the Z coordinate is towards the user. You may find other conventions such as Z towards the top (height) in Blender, but OpenGL's default is Y-is-up.
GLfloat cube_vertices[] = {
// front
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// back
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
-1.0, 1.0, -1.0
};
To see something better than a black mass, we'll also define some colors:
GLfloat cube_colors[] = {
// front colors
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 1.0, 1.0,
// back colors
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 1.0, 1.0
};
Don't forget the global buffer handles:
GLuint vbo_cube_vertices, vbo_cube_colors;
Elements - Index Buffer Objects (IBO)
editOur cube has 6 faces. Two faces may share some vertices. In addition we'll write our face as a combination of 2 triangles (so 12 triangles total).
Consequently we'll introduce the concept of elements: we use glDrawElements
, instead of glDrawArrays
. It takes a set of indices that refer to the vertices array. With glDrawElements
, we can specify any order, and even the same vertex several times. We'll store these indices in an Index Buffer Object (IBO).
It is better to specify all faces in a similar way, here counter-clockwise, because it will be important for texture mapping (see next tutorial) and lighting (so triangle normals need to point to the right way).
/* Global */
GLuint ibo_cube_elements;
/* init_resources */
GLushort cube_elements[] = {
// front
0, 1, 2,
2, 3, 0,
// right
1, 5, 6,
6, 2, 1,
// back
7, 6, 5,
5, 4, 7,
// left
4, 0, 3,
3, 7, 4,
// bottom
4, 5, 1,
1, 0, 4,
// top
3, 2, 6,
6, 7, 3
};
glGenBuffers(1, &ibo_cube_elements);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_cube_elements);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(cube_elements), cube_elements, GL_STATIC_DRAW);
Note that we used a buffer object again, but with GL_ELEMENT_ARRAY_BUFFER
instead of GL_ARRAY_BUFFER
.
We can tell OpenGL to draw our cube in render
:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_cube_elements);
int size; glGetBufferParameteriv(GL_ELEMENT_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);
glDrawElements(GL_TRIANGLES, size/sizeof(GLushort), GL_UNSIGNED_SHORT, 0);
We use glGetBufferParameteriv
to grab the buffer size. This way, we don't have to declare cube_elements
.
Enabling depth
editglEnable(GL_DEPTH_TEST);
//glDepthFunc(GL_LESS);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
We now can see the square front face, but to see the other faces of the cube, we need to rotate it. We still can peek by removing one (or both!) of the front face's triangle. :)
Model-View-Projection matrix
editUntil now, we've worked with object coordinates, specified around the object's center. To work with multiple objects, and position each object in the 3D world, we compute a transformation matrix that will:
- shift from model (object) coordinates to world coordinates (model->world)
- then from world coordinates to view (camera) coordinates (world->view)
- then from view coordinates to projection (2D screen) coordinates (view->projection)
This will also take care of our aspect ratio issue.
The goal is to compute a global transformation matrix, called MVP, that we'll apply on each vertex to get the final 2D point on the screen.
Note that the 2D screen coordinates are in the [-1,1] interval. There is a 4th non-matrix step to convert these to [0, screen_size], controlled by glViewPort
.
History note: OpenGL 1.x had two built-in matrices, accessible through glMatrixMode(GL_PROJECTION)
and glMatrixMode(GL_MODELVIEW)
. Here we're replacing those, plus we're adding a camera :)
Let's add our code in the logic
function, where we updated the fade
uniform in the previous tutorial. We'll pass a mvp
uniform instead.
Start: at the beginning of each phase, we have an identity matrix, that does no transformation at all, created using glm::mat4(1.0f)
.
Model: we'll push our cube a bit in the background, so it doesn't mix with the camera:
glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(0.0, 0.0, -4.0));
View: GLM provides a re-implementation of gluLookAt(eye, center, up). eye is the position of the camera, center is where the camera is pointed to, and up is the top of the camera (in case it's tilted). Let's center on our object from a bit above, with the camera straight:
glm::mat4 view = glm::lookAt(glm::vec3(0.0, 2.0, 0.0), glm::vec3(0.0, 0.0, -4.0), glm::vec3(0.0, 1.0, 0.0));
Projection: GLM also provides a re-implementation of gluPerspective(fovy, aspect, zNear, zFar). aspect is the screen aspect ratio (width/height) which is horizontal by default and it's axis can be changed by recalculating the vertical field of view using trigonometric math functions when in radians multiplied by the aspect divided by the conditional/original aspect ratio, fovy is the vertical field of view (45° for a common 60° horizontal FOV in 4:3 resolution) that can be zoomed by recalculating the field of view the same way as changing the aspect axis constraint, zNear and zFar are the clipping plane (min/max depth), both positive, zNear usually small not equal to zero. We need to see up to our square, so we can use 10 for zFar:
glm::mat4 projection = glm::perspective(recalculatefov(), 1.0f * screen_width / screen_height, 0.1f, 10.0f);
float recalculatefov()
{
return 2.0f * glm::atan(glm::tan(glm::radians(45.0f / 2.0f)) / aspectaxis());
}
float aspectaxis()
{
float outputzoom = 1.0f;
float aspectorigin = 16.0f / 9.0f;
int aspectconstraint = 1;
switch (aspectconstraint)
{
case 1:
if ((screen_width / screen_height) < aspectorigin)
{
outputzoom *= (((float)screen_width / screen_height) / aspectorigin)
}
else
{
outputzoom *= ((float)aspectorigin / aspectorigin)
}
break;
case 2:
outputzoom *= (((float)screen_width / screen_height) / aspectorigin)
break;
default:
outputzoom *= ((float)aspectorigin / aspectorigin)
}
return outputzoom;
}
screen_width
and screen_height
are new global variables to define the size of the window:
/* global */
int screen_width=800, screen_height=600;
/* main */
SDL_Window* window = SDL_CreateWindow("My Textured Cube",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
screen_width, screen_height,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL);
Result:
glm::mat4 mvp = projection * view * model;
We can pass it to the shader:
/* Global */
#include <glm/gtc/type_ptr.hpp>
GLint uniform_mvp;
/* init_resources() */
const char* uniform_name;
uniform_name = "mvp";
uniform_mvp = glGetUniformLocation(program, uniform_name);
if (uniform_mvp == -1) {
fprintf(stderr, "Could not bind uniform %s\n", uniform_name);
return 0;
}
/* logic() */
glUniformMatrix4fv(uniform_mvp, 1, GL_FALSE, glm::value_ptr(mvp));
And in the shader:
uniform mat4 mvp;
void main(void) {
gl_Position = mvp * vec4(coord3d, 1.0);
[...]
Animation
editTo animate the object, we can simply apply additional transformations before the Model matrix.
To rotate the cube, we can add in logic
:
float angle = SDL_GetTicks() / 1000.0 * 45; // 45° per second
glm::vec3 axis_y(0, 1, 0);
glm::mat4 anim = glm::rotate(glm::mat4(1.0f), glm::radians(angle), axis_y);
[...]
glm::mat4 mvp = projection * view * model * anim;
We've made the traditional flying rotating cube!
Window resize
editTo support sizing the SDL2 window, you can check for SDL_WINDOWEVENT
s:
void onResize(int width, int height) {
screen_width = width;
screen_height = height;
glViewport(0, 0, screen_width, screen_height);
}
/* mainLoop */
if (ev.type == SDL_WINDOWEVENT && ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
onResize(ev.window.data1, ev.window.data2);