In Part 1 we created a window, nothing too fancy. Now we get to actually display a triangle.
Just to follow along as well, I'm moving through the Learning Modern 3D Graphics Programming online book to learn OpenGL (again), so the OpenGL examples I will be displaying will be ports from the code that that is providing. I would suggest for the complete theory behind this code, read the linked section before going through the JRuby code. I expect my explanations to only be commentary on the information already provided in that series and discussion on some of the finer points on JRuby and Java library integration as well.
If you have any questions, please feel free to ask, but be aware, I'm very new to OpenGL, so writing this series is very much part of my learning experience. However, I will attempt to answer the best way I can. On the other hand, if you find anything wrong with what I'm written, please point it out so it can be corrected.
This example is from Hello, Triangle!.
The full code can be seen on Github here.
To run this example use bin/triangle
Making Vertex Data Available to OpenGL
When I first did OpenGL back in University, we used the
glEnd() paradigm. This was definitely far easier that the more modern APIs, as it was very clear and easy to draw a simple polygon on the screen (example). However, it did mean more computation was occurring on the CPU and a larger use of the system RAM than the newer APIs. The newer APIs, while (far?) more complicated, shift much of the work to the GPU and also provide a far more flexible implementation. I liken it to working with HTML and tables back in early HTML days. Sure it worked, but CSS and semantic markup gives a clear separation and creates far more flexible implementation options (at least in theory 😉 ).
So we have some basic vertex information to display a right angle triangle:
@vertex_positions = [
0.75, 0.75, 0.0, 1.0,
0.75, -0.75, 0.0, 1.0,
-0.75, -0.75, 0.0, 1.0,
Each line of this array define the x, y and z coordinates of our triangle. You will notice there is a fourth coordinate (1.0) on each line. This defines the clip space. For now we'll just say this just means that the vertexes you see in the window has to have values between -1 and 1 on the x, y and z axis. More than that will render outside of the window.
As discussed previously, in old school OpenGL you would just look through this list of vertexes and say draw a triangle here, however, this is no longer the case!
I feel like modern OpenGL is almost like a database – you put some data into it, and have an id to reference that data that was placed in. Then you can work on that data that is stored on the GPU through some other techniques (that we will look at in a minute) via that id. This seems to be a concept that is used across the board.
The code that inserts our vertex data into the GPU can be seen in the method
@buffer_id = GL15.gl_gen_buffers
So first thing we do, is we generate an id for the vertex buffer, which is where we will store our vertex data. (
gl_gen_buffers). Then we tell OpenGL, hey, this is the buffer we want to work with for the moment through
gl_bind_buffer, passing in the specific @buffer_id we generated before. We also tell OpenGL that the buffer we are working with an
GL_ARRAY_BUFFER, so it knows what data to expect.
In case you aren't aware, JRuby will convert Java static constants to Ruby constants, so we can access these static fields very easily.
To pass the vertex data into the new vertex array buffer, LWJGL has us use it's BufferUtils class to create a NIO buffer, and push the data into it, like so:
float_buffer = BufferUtils.create_float_buffer(@vertex_positions.size)
#MUST FLIP THE BUFFER! THIS PUTS IT BACK TO THE BEGINNING!
A couple of interesting notes:
- You will noticed the
.to_java(:float). That is the JRuby code for converting a Ruby array to a Java array. Passing in
:floattells it to make it an array of primitive floats.
.flipat the end. This is very important (and took me a day to work out, as I'm not familiar with NIO buffers). Here is a great article that explains it more detail, but essentially the buffer tracks where it is at, and
flipsends it back to the beginning. Without this, no data goes to our Vertex Array, and nothing happens!
GL15.gl_buffer_data(GL15::GL_ARRAY_BUFFER, float_buffer, GL15::GL_STATIC_DRAW)
This then pushes our vertex data to the bound vertex buffer.
After we are done, we tell OpenGL not to be bound to any array buffers (0 works like NULL in OpenGL land). This could be considered optional, but ensures that weird things don't occur.
Now all we have to do is actually write the code that makes the data that displays the triangle!
Telling OpenGL how to Render the Vertexes
So now we have the vertex data stored on the GPU, we have to tell it how to render it, and to do that, we have to build a program of a couple of different types of shaders. Think of Shaders kind of like the CSS of HTML. They simply work on the existing data in the GPU and tell it how it to render (although that's a bit of an over simplification).
First thing, we'll write a simple vertex shader in GLSL, the language for writing shaders. This specifies to the GPU where the vertexes actually are from the data you entered earlier. We'll just say that it's correct and basically pass it through.
layout(location = 0) in vec4 position;
gl_Position = position;
Then we write a fragment shader to it what colours to make things. We'll just make everything white for convenience.
out vec4 outputColor;
outputColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
To make life easier for myself, I wrote a quick little
create_shader function, that loads the shader from a file loads it into memory and compiles it. I just want to make note of one line:
puts file_name, GL20.gl_get_shader_info_log(shader_id, 200)
Without this, you have no idea why your shader fails (if it does). Mine was failing, and I didn't realise it until I looked deeper. (Version 330 of GLSL wasn't supported on my Ultrabook on Linux with Mesa. I had to switch to my main laptop with the Nvidia graphics card)
Make sure to look at the output logs!
Creating the program to define how our data is output on the screen, is quite similar to what we did before. We generate an id and then link the shaders to the program like so:
vertex_shader = create_shader(GL20::GL_VERTEX_SHADER, 'basic_vertex.glsl')
frag_shader = create_shader(GL20::GL_FRAGMENT_SHADER, 'basic_fragment.glsl')
@program_id = GL20.gl_create_program
puts "Validate Program", GL20.gl_get_program_info_log(@program_id, 200)
A few things to note:
create_shaderreturns the id of the shader. Much like a database, you reference everything you are creating using OpenGL by the generated ID.
- We attach the shaders to the program by the id we have for that program.
gl_link_programessentially turns the program we're creating and makes it able to executedon the GPU.
- We also get the program info log with
gl_get_program_info_logmuch like we did with the shader. Make sure to check those logs!
Finally, we can get to our display loop, which you can see in the
#set the colour to clear.
GL11.gl_clear_color(0.0, 0.0, 0.0, 0.0)
We clear the page with a nice transparent colour.
This clears the screen, so we can redraw with impunity.
Tell it to use our shaders
This tells it to use our vertex buffer
Tells it to pass each value of the vertex buffer to the first argument of our vertex shader.
GL20.gl_vertex_attrib_pointer(0, 4, GL11::GL_FLOAT, false, 0, 0)
This tells OpenGL how the structure of the vertex buffer data matches is structured. Here we are saying that we have an array of floats, and every 3 elements is a x, y and then z member, with the fourth defining the clip space, as we saw earlier. (0 is the start, 4 is the size).
GL11.gl_draw_arrays(GL11::GL_TRIANGLES, 0, 3)
DRAW THE TRIANGLE! 0 is the start of the array of vertices you want to draw, and 3 is the number of indexes you want to process – in this case 3 to make a triangle.
Finally we use the magic OpenGL 0 to clean things up again, and we are done!
We now have this amazing triangle.