Drawing a Triangle¶
Now things start to get a bit more interesting and we are going to draw our first triangle. We will use the same window from the previous example, so make sure you have that running.
Full Source
This getting started tutorial is based on the examples provided with PHP-GLFW.
You can check out the complete source code here: 01_triangle.php
Why a Triangle?¶
Why draw a triangle? Well, it's the simplest geometry you can draw and it's a good starting point to get familiar with the rendering pipeline. I honestly was kinda blown away when I realized that every object/model/mesh could be represented as a collection of triangles. (That sentence is going to make a few math people mad) but let me explain.
A square is made up of two triangles. A cube is made up of six squares, and so on. So if you can draw a triangle, you can draw anything. (Well, almost anything...)
My next level drawing skills besides, lets talk about the rendering pipeline.
The Rendering Pipeline¶
The rendering pipeline simplified, is a series of steps that are executed to transform a 3D model into a 2D image that can be displayed on the screen. The OpenGL pipeline in this example is divided into two parts, the vertex processing and fragment processing. There are many more steps/stages which we will not cover here to keep things simple. (If you are interested, you can read more about it here)
Each of these steps/stages is performed by a shader, a small program executed on the GPU. The vertex shader is executed for each vertex (point) in the your geometry, and the fragment shader is executed for each pixel on the screen.
Some of these shaders can be programmed by the user. The GPU driver handles others. Shaders are written in a language called GLSL, which is a C-like language with some extensions to make it easier to write shaders.
In PHP-GLFW (PHP OpenGL) you still write your shaders in GLSL. We could have written a transpiler of some sort to convert PHP to GLSL, but that would have been a lot of work and would have made the library a lot more complex, and I also believe it would ultimately defeat the purpose.
Vertex Arrays (VAO and VBO)¶
In order to draw a triangle, we need to define the vertices that make up the triangle. A vertex array is not necessarily just an array of vertex positions. It can also contain other information such as color, texture coordinates, normals, etc.
Note the vertex array object (VAO) does not directly store the vertices. It holds the state of the vertex array. For example, the VAO stores the vertex attribute configuration, which tells the GPU how to interpret the vertex data. The actual vertex data is stored in the vertex buffer object (VBO), which is really just a blob of data.
// create a vertex array (VertextArrayObject -> VAO)
glGenVertexArrays(1, $VAO);
// create a buffer for our vertices (VertextBufferObject -> VBO)
glGenBuffers(1, $VBO);
// bind the buffer to our VAO
glBindVertexArray($VAO);
glBindBuffer(GL_ARRAY_BUFFER, $VBO);
So what is happing here? We are creating a vertex array object (VAO) using the glGenVertexArrays
function. We are also creating a buffer for our vertices using the glGenBuffers
function. Using the glBindVertexArray
and glBindBuffer
functions we are binding the buffer (VBO) and our vertex array object (VAO) to the current context.
Uploading the vertex data¶
PHP-GLFW provides a few helper classes to make it easier to upload data to the GPU. PHP unfortunately, does not have built-in tools to work with "real" arrays of data (like C/C++). So this extension comes with custom buffer classes that can be used to upload data to the GPU "directly".
So let's declare our triangle vertices and upload them to the GPU using glBufferData
.
$buffer = new \GL\Buffer\FloatBuffer([ # (1)!
// positions // colors
0.5, -0.5, 0.0, 1.0, 0.0, 0.0, // bottom right
-0.5, -0.5, 0.0, 0.0, 1.0, 0.0, // bottom left
0.0, 0.5, 0.0, 0.0, 0.0, 1.0 // top
]);
glBufferData(GL_ARRAY_BUFFER, $buffer, GL_STATIC_DRAW);
- The FloatBuffer class is just one of many buffer helpers. Also available are:
But what does the data actually mean? If not clear from the code here a visual representation of the data.
Vertex attribute pointers¶
We now know how that buffer data is to be interpreted, but we still need to tell the GPU how to interpret the data. We do this by setting the vertex attribute pointers using the glVertexAttribPointer
function.
A vertex attribute pointer consists of 3 central values to allow the GPU to iterate over your vertex data. Lets assume a type uniform vertex buffer for now:
size
- The number of components per vertex attributestride
- The offset between consecutive vertex attributesoffset
- The offset of the first component of the vertex attribute
Now in practice the glVertexAttribPointer
function is declared like this:
function glVertexAttribPointer(
int $index, // Specifies the index of the generic vertex attribute to be modified.
int $size, // Specifies the number of components
int $type, // Specifies the data type of each component
bool $normalized,// Specifies whether fixed-point data values should be normalized
int $stride, // Specifies the byte offset between consecutive generic vertex attributes
int $offset // Specifies a offset of the first component of the first generic vertex attribute
) : void
In case of our triangle we have 6 components per vertex. The first 3 components are the position of the vertex and the last 3 components are the color of the vertex.
// positions
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, GL_SIZEOF_FLOAT * 6, 0);
glEnableVertexAttribArray(0);
// colors
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, GL_SIZEOF_FLOAT * 6, GL_SIZEOF_FLOAT * 3);
glEnableVertexAttribArray(1);
$stride
and $offset
expects the number of bytes. PHP-GLFW exposes the size of the data types as constants. For example GL_SIZEOF_FLOAT
is the size of a float in bytes. So you can simply multiply the number of components by the size of the data type to get the stride.
I know you probably did not expect a simple triangle to be so complicated. But this is the bare minimum you need to know to get started with OpenGL. In the next section we will look at the shaders.
Shaders¶
As we already touch on in the rendering pipeline, we need to cover two shading stages to render a triangle. The vertex shader and the fragment shader. The vertex shader is responsible for transforming the vertices into clip space. The fragment shader is responsible for calculating the color of each pixel.
Vertex Shader¶
The vertex shader is used to transform the input vertices from the vertex buffer object into clip space. This means transforming vertices from 3D space to 2D space, by multiplying them by our projection and view matrix. The output of the vertex shader is the position of the vertex on the screen, as a point in the normalized device coordinates (NDC) space. (i.e., values between -1 and 1).
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
out vec4 pcolor;
void main()
{
pcolor = vec4(color, 1.0f);
gl_Position = vec4(position, 1.0f);
}
In the above example, we define our vertex shader to expect a vertex position using layout (location = 0) in vec3 position;
and a color using layout (location = 1) in vec3 color;
. We then take that position
and set it as the gl_Position
. That's it. No fancy math. You can do fancy math in the vertex shader, but we are keeping things simple here.
This gl_Position
is how we tell OpenGL where our vertex is in clip space.
Also notice the out vec4 pcolor;
to pass the color from the vertex shader to the fragment shader. We will cover this in the next section.
Fragment Shader¶
The fragment shader takes as input pcolor
which represents the color of the current pixel (which we output from the vertex shader), and outputs the final pixel color.
Again, not much happening here, we just set the fragment_color
to the pcolor
we got from the vertex shader.
Compiling the shaders¶
To make use of the shaders we need to compile and link them to a shader program. This requires a few steps:
- Create a shader program
- Create a vertex shader
- Compile the vertex shader
- Create a fragment shader
- Compile the fragment shader
- Attach the vertex shader to the shader program
- Attach the fragment shader to the shader program
- Link the shader program
In this example we just define the shader source code as strings. In most applications I worked on we built a system to compile the shaders from the filesystem with an include system etc.. But for this example we will keep it simple.
$vertexShaderSource = <<< 'GLSL'
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
out vec4 pcolor;
void main()
{
pcolor = vec4(color, 1.0f);
gl_Position = vec4(position, 1.0f);
}
GLSL;
$fragmentShaderSource = <<< 'GLSL'
#version 330 core
out vec4 fragment_color;
in vec4 pcolor;
void main()
{
fragment_color = pcolor;
}
GLSL;
To create a new shader program we use the glCreateProgram
function. This function returns a handle to the shader program. We will use this handle to attach the shaders.
Next we need to create the vertex shader, load the source code into it and compile it.
// create, upload and compile the vertex shader
$vertexShader = glCreateShader(GL_VERTEX_SHADER); // 2.
glShaderSource($vertexShader, $vertexShaderSource);
glCompileShader($vertexShader); // 3.
OpenGL by itself does not throw any exceptions or erros and as we try to stay close to the original API this
holds true for PHP-GLFW as well. So we need to check for errors ourselves. We do this by calling glGetShaderiv
to get the compile status of the shader. If the compile status is not true
we throw an exception.
// check for errors
glGetShaderiv($vertexShader, GL_COMPILE_STATUS, $success);
if (!$success) {
throw new Exception("Vertex shader could not be compiled.");
}
For the fragment shader we repeat the same steps as for the vertex shader.
// create, upload and compile the fragment shader
$fragShader = glCreateShader(GL_FRAGMENT_SHADER); // 4.
glShaderSource($fragShader, $fragmentShaderSource);
glCompileShader($fragShader); // 5.
// check for errors
glGetShaderiv($fragShader, GL_COMPILE_STATUS, $success);
if (!$success) {
throw new Exception("Fragment shader could not be compiled.");
}
Now we just need to attach the shaders to the shader program and link it.
// attach the shaders to the shader program and link it
glAttachShader($shaderProgram, $vertexShader); // 6.
glAttachShader($shaderProgram, $fragShader); // 7.
glLinkProgram($shaderProgram); // 8.
// check for errors
glGetProgramiv($shaderProgram, GL_LINK_STATUS, $linkSuccess);
if (!$linkSuccess) {
throw new Exception("Shader program could not be linked.");
}
Thats it now we can use the shader with the glUseProgram
function to draw our triangle.
Note on VISU¶
To simplify the handling of shaders, it is recommended to abstract the necessary steps. Shader handling can often become complex and cumbersome otherwise. For this purpose, the VISU Framework offers a range of abstractions for tasks such as shader loading, handling, and compiling, as exemplified by the ShaderProgram class.
Draw loop¶
Similar to the previous example "Window Creation" we need to create a draw loop. The draw loop is the main loop of our application. It is responsible for rendering the scene and handling user input.
Here is the draw loop for our triangle example:
while (!glfwWindowShouldClose($window))
{
// setting the clear color to black and clearing the color buffer
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
// use the shader, will active the given shader program
// for the coming draw calls.
glUseProgram($shaderProgram);
// bind & draw the vertex array
glBindVertexArray($VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// swap the windows framebuffer and
// poll queued window events.
glfwSwapBuffers($window);
glfwPollEvents();
}
Now in details what do these new lines of code do?
The glUseProgram
function activates the given shader program. This means that all draw calls after this function will use the given shader program. Again OpenGL is a state machine and we are responsible for setting the correct state.
The glBindVertexArray
function binds the given vertex array object. This means that all draw calls after this function will use the given vertex array object. This vertex array contains our triangle vertices and colors if you rember.
The glDrawArrays
function then instructs OpenGL to draw the vertices as triangles. The first parameter is the primitive type, in this case triangles. The second parameter is the starting index of the vertex array and the third parameter is the number of vertices we want to draw.
Full Source Code¶
The full source code can be found here: 01_triangle.php