How 3D Rendering Works
Triangle: The Fundamental of Mesh Rendering
In graphic programming, all 3D mesh or models are drawn using triangles. This is because triangles are the simplest polygon that always lies flat in 3D space. This provides a high level of flexibility and make triangles ideal for approximating complex surfaces.
Each triangle is defined by three vertices, and a mesh would appear distorted if vertices of triangles are wound in a wrong order. A vertex is a point in 3D space (also in 2D space) that carries additional information like color and texture coordinates (+ normal vector used for telling in which direction a surface receives light).
Introducing Graphic Buffers: VAO, VBO, and EBO
When rendering a mesh in OpenGL, this vertex data should be sent to the GPU. But instead of sending it directly every frame (which would be inefficient), the data is stored in GPU memory using special objects called buffers. These buffers allow OpenGL to draw the mesh quickly and efficiently.
To draw meshes in OpenGL, three main types of buffer objects are used:
- VAO (Vertex Array Buffer Object): stores the format of vertex data; tells OpenGL how to interpret the data in memory.
- VBO (Vertex Buffer Object): stores the actual vertex data.
- EBO (Element Buffer Object): tells OpenGL how to reuse vertices (optional but essential).
A typical vertex data might look like this:
struct Vertex {
vec3 position; //3 floats (x, y, z)
vec2 texture; //2 floats (u, v)
vec3 normal; //3 floats (x, y, z)
}
Initialize a VBO with a mesh's vertices.
VBO vbo = VBO(vertices, sizeof(vertex) * vertices.size(), GL_STATIC_DRAW);
Then tell OpenGL how to interpret the provided data. I specify each attributes of vertex like this:
vao.bind();
vao.link_attrib(vbo, 0, 3, float, false, sizeof(Vertex), (void*)0); //vertex position
vao.link_attrib(vbo, 1, 2, float, false, sizeof(Vertex), (void*)(3 * sizeof(float))); //vertex texture
vao.link_attrib(vbo, 2, 3, float, false, sizeof(Vertex), (void*)(5 * sizeof(float))); //vertex normal
Explanation of parameters:
- 1st parameter(vbo): activates a VBO to which this attribute is linked
- 2nd parameter(layout): layout of the attribute in a vertex shader
- 3rd parameter(count): number of components (i.e. 3 for vec3)
- 4th parameter(type): type of components of the attribute.
- 5th parameter(normalized): tell whether data should be normalized.
- 6th parameter(stride): total size of one vertex (so OpenGL knows where the next vertex starts)
- 7th parameter(offset): where this attribute starts within the vertex
Incorrect setting of this can cause a mesh to be invisible. Initially, I spent hours debugging this until realizing this was the reason.
First Step Toward Optimization: EBO
When building a mesh using just a VAO and VBO, the same vertex data is repeated multiple times. For example, if two triangles share a corner, that vertex gets defined twice. This is inefficient, especially for larger meshes.
An EBO (Element Buffer Object) solves this by storing only the unique vertices and use an index array to tell OpenGL how to connect them into triangles. An EBO acts like a map: it tells OpenGL which vertices to reuse and in what order.
So How to Draw a Mesh in OpenGL
Putting everything altogether, a mesh can be drawn as follows:
vertex_shader.activate();
vao.bind();
ebo.bind();
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
This draw call must be executed every frame to render the mesh continuously. By storig vertex data on GPU, graphic buffers eliminate the need to re-initialize the data all over again each frame. This minimizes data trasfers between the CPU and GPU, allowing the progrm to run more efficiently.
While updating a VBO with a new vertex data is one of the options, check out other ways to manage vertex data in OpenGL.