tl;dr
Building a triangle takes 3 vertices, and only uses each one once. Building a tetrahedron (a polyhedron with four triangular faces) takes 4 vertex locations, but references each one 3 times. An Element Array helps us define the vertex locations once, and build a tetrahedron by referring to those vertices by index.
Tetrahedrons
The next step after a triangle is something with another dimension (depth, you could say), which is a tetrahedron.
Consider our triangle:
C / \ / \ A-----B
A tetrahedron would share the ABC vertices, and add a D vertex "behind" the center of the triangle, like so:
C /|\ / D \ / / \ \ A-------B
In addition to the ABC triangle, we would need to draw the ABD, ACD, and BCD triangle faces of the tetrahedron.
Assuming the original triangle vertices:
A (bottom-left) -1, -1, 0, 1 B (bottom-right) 1, -1, 0, 1 C (top-middle) 0, 1, 0, 1
And the new vertex:
D (middle-back) 0, 0, 1, 1
Our triangle vertex buffer changes from:
const GLfloat triangleVertices[] = [ -1.0f, -1.0f, 0, 1, 1.0f, -1.0f, 0, 1, 0.0f, 1.0f, 0, 1, ]
To:
const GLfloat triangleVertices[] = [ -1.0f, -1.0f, 0, 1, // A 1.0f, -1.0f, 0, 1, // B 0.0f, 1.0f, 0, 1, // C -1.0f, -1.0f, 0, 1, // A 1.0f, -1.0f, 0, 1, // B 0.0f, 0.0f, 1, 1, // D -1.0f, -1.0f, 0, 1, // A 0.0f, 1.0f, 0, 1, // C 0.0f, 0.0f, 1, 1, // D 1.0f, -1.0f, 0, 1, // B 0.0f, 1.0f, 0, 1, // C 0.0f, 0.0f, 1, 1, // D ]
With some other minor changes per my first commit, the result is a somewhat-oddly-shaped tetrahedron.
Element Arrays
Changing D's location now (maybe to fix the odd shape?) would be annoying now - we have it in three places. One could use a variable (or even macro if C/C++) in your code, or have a function generate this list given the four vertex locations), but another concern is that we're sending and storing a lot more data on the graphics card.
Since the tetrahedron shape is a little hard to understand now given its constant colour, I'd like to put lines on the edges to make them more distinct. I would need to make lines AB, AC, AD, BC, BD, and CD, and thus I would again need to copy these vertices to the graphics card - for 6 copies of each vertex at this point.
Element Arrays are a solution to this. We store each vertex once in the vertex buffer, and then define the shapes (triangle, line) referencing those vertices by index.
Our vertex buffer and array element setup now looks like this:
const GLfloat triangleVertices[] = [ -1f, -1f, 0f, 1f, 1f, -1f, 0f, 1f, 0f, 1f, 0f, 1f, 0f, 0f, 1f, 1f, ]; GLushort tetrahedron_elements[] = [ 0, 1, 2, 0, 1, 3, 0, 2, 3, 1, 2, 3, ]; glGenBuffers(1, &vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, triangleVertices.length * GLfloat.sizeof, triangleVertices.ptr, GL_STATIC_DRAW); glGenBuffers(1, &tetrahedronElements); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, tetrahedronElements); glBufferData(GL_ELEMENT_ARRAY_BUFFER, tetrahedron_elements.length * GLushort.sizeof, tetrahedron_elements.ptr, GL_STATIC_DRAW);
And then to draw the tetrahedron, we have:
// What to draw glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, tetrahedronElements); // Layout of the stuff to draw glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, null); // Draw it! glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_SHORT, cast(void *)0);
glDrawElements
replaces glDrawArrays
, and the tetrahedronElements
element
array buffer now provides indices into the vertexBuffer
array buffer.
With some other minor changes per my second commit, the result is the exact same somewhat-oddly-shaped tetrahedron.
Adding lines
Adding lines is fairly easy now. We set up the element array for the lines:
GLushort line_elements[] = [ 0, 1, 0, 2, 1, 2, 0, 3, 2, 3, 1, 3, ]; glGenBuffers(1, &lineElements); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, lineElements); glBufferData(GL_ELEMENT_ARRAY_BUFFER, line_elements.length * GLushort.sizeof, line_elements.ptr, GL_STATIC_DRAW);
Then, in the display loop:
// What to draw glUniform1i(isLine, 0); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, tetrahedronElements); // Layout of the stuff to draw glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, null); // Draw it! glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_SHORT, cast(void *)0); glUniform1i(isLine, 1); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, lineElements); glDrawElements(GL_LINES, 12, GL_UNSIGNED_SHORT, cast(void *)0);
Ignoring the glUniform1i
calls for a second, you can see that first we draw
triangles (glDrawElements
with GL_TRIANGLES
with the tetrahedronElements
element array buffer bound, and then we bind instead lineElements
and make
another call to glDrawElements
except with `GL_LINES.
The glUniform1i
calls are to change the is_line
uniform that I have now
enabled in my shaders. Now, instead of the fragment shader always drawing
pixels in red, it will draw pixels in red when is_line
is 0, and it will draw
pixels in white when is_line
is 1.
The new shaders
Here is the new vertex shader:
#version 330 core layout(location = 0) in vec4 pos; uniform mat4 u_transform; uniform int is_line; out vec3 Color; void main(){ if (is_line == 0) { Color = vec3(1, 0, 0); } else { Color = vec3(1, 1, 1); } gl_Position = pos * u_transform; }
We've added the is_line
uniform, and now specify an out
vector called
Color
. This will be carried over as input to the fragment shader.
The frament shader now looks like:
#version 330 core out vec3 color; in vec3 Color; void main() { color = Color; }
Putting it together
Here is the state of of my code repo at this point.
Here is a short demo of what it looks like at this point: