It's been a few months since I last posted here, it's been busy times, busy at work, busy at home, but busy fun!
I'm hoping to find some time in the next few weeks to do another blog post in relation to my OpenGL project. I checked in some small cleanup changes that were part of some experimentation I've done but nothing noteworthy.
A couple of weeks ago I started playing HALO and had my daughter walked in (I usually don't start playing more serious games until my kids are in bed) and before I knew it we played through HALO Reach together. Now last year we went to the EB Games Expo and she was mesmerized by the people doing Cosplay there. So putting 1 and 1 together we started looking into doing HALO cosplay and that is the thing that has really kept me busy, and probably will keep me busy for weeks to come.
Finally, it's pretty much all sorted out, I'll be a guest speaker at EurOmnis again this year and there is plenty of preparation to do for that as well.
So apologies, updates will remain slow, but I still hope to post something new soon.
Monday, 13 July 2015
Sunday, 12 April 2015
Rendering tiles method 2 (part 7)
So I promised a second approach to the same tile rendering demo in my previous post. With this approach we'll put the solution on its head.
This solution isn't necessarily better or faster, it can be depending on how complex your map is and whether you applied the optimizations to the previous method I suggested or not.
It is however a very worth while technique to learn.
What we are doing in essence here is to draw every pixel on screen and figure out what part of our map should be drawn here. We'll be moving part of our solution into our fragment shader. That in itself should sound some alarm bells as this will increase the workload of the GPU however we are removing overhead as we no longer generate a complex mesh nor attempt to draw anything that would have been off screen. It is how this balance shifts that determines whether this approach is better or worse.
As simple as our example is we're probably implementing a slower solution. However when you are looking at multiple layers of the maps drawn over each other it would be possible to combine the logic within a single fragment shader and possibly remove a lot of overhead.
It isn't always possible to create the inverse of a matrix but generally speaking it works very well.
In our case we're going to take the inverse of our projection matrix. This allows us to take a screen coordinate, apply our inverse matrix and figure out the coordinate for our "model". Remember that in our original example we generated a 40x40 mesh for rendering our map, it is the coordinates within this 40x40 mesh that we calculate.
Applying our inverse matrix to every pixel we are rendering would be very wasteful but luckily for us we're dealing with a linear projection. As a result we only need to calculate our coordinates at each corner of the screen and interpolate those values, something OpenGL is very good at. We'll look into this a little more once we look at our vertex shader.
What you'll see is that our call:
will fail even though our mvp variable is defined in our shader, it just isn't used. GLSL will automatically filter out any unused variables during compiling the shader and thus there is no variable to obtain our uniform location for.
There is a lot of debate to be found on the internet about this and various strategies on how to deal with it. It is a good thing to research and make up your own mind about. For now I just log variables I can't find so I can make up my mind whether this is intentional or indicates a fault in my program. Especially when you have multiple shaders you may not want to custom make different loaders but simply call the same code to load the shaders and just assume common variables to be there.
Our shader loader code has only slightly been changed, the only noteworthy change is the addition of getting our uniform for invmvp, our inverse model-view-projection matrix.
In our render function we then see that we calculate the inverse of our matrix and set it in our shader:
A little further down we also see that our call to glDrawArrays now tells OpenGL to only draw 2 triangles.
Notice also that we apply our inverse model-view-projection matrix but only store the x/y into a 2D vector, that is all we're interested in here. As mentioned before, OpenGL will nicely interpolate this value for us.
The real work now happens in our fragment shader so we'll handle that in parts:
We floor "ti" to get integer values and than subtract "ti" from "to" so "to" contains offsets within a single tile.
And voila, we've got the same result as before we started but using a very different way to get there.
For one, if you want to overlay multiple layers of maps it would be good to look into retrieving the pixel values for each layer in one fragment shader and either picking the right color or blending them. It would be much more effective then rendering each layer separately and having OpenGL blending it for you.
This technique does not lend itself well initially when you start looking at projecting the map using a 3D projection however using the inverse of a 3D projection opens up an entirely different door. Look around on shadertoy.com and you'll find legions of effects that use this approach as a starting point to apply raycasting type techniques. That however is an entirely different subject for another day.
On a related subject, and this applies to the previous example as well, it will not have escaped you that allowing the user to rotate our map isn't exactly looking nice with the map we are using. That option really only works if we have a pure top down map. I just wanted to show what we can do with our view matrix even if it may not be something you wish to use in a real game environment.
It may thus take awhile before I make the next blog as I'll be experimenting with a few ideas before I get something worth blogging about.
This solution isn't necessarily better or faster, it can be depending on how complex your map is and whether you applied the optimizations to the previous method I suggested or not.
It is however a very worth while technique to learn.
What we are doing in essence here is to draw every pixel on screen and figure out what part of our map should be drawn here. We'll be moving part of our solution into our fragment shader. That in itself should sound some alarm bells as this will increase the workload of the GPU however we are removing overhead as we no longer generate a complex mesh nor attempt to draw anything that would have been off screen. It is how this balance shifts that determines whether this approach is better or worse.
As simple as our example is we're probably implementing a slower solution. However when you are looking at multiple layers of the maps drawn over each other it would be possible to combine the logic within a single fragment shader and possibly remove a lot of overhead.
Inverse of a matrix
One new function that was added to our math library is a function that generates the inverse of a matrix. The inverse of a matrix is a matrix that applies the inverse of the translation of that matrix. Say you have a matrix that rotates an object 90 degrees clockwise, the inverse of that matrix would rotate the object 90 degrees counter clockwise.It isn't always possible to create the inverse of a matrix but generally speaking it works very well.
In our case we're going to take the inverse of our projection matrix. This allows us to take a screen coordinate, apply our inverse matrix and figure out the coordinate for our "model". Remember that in our original example we generated a 40x40 mesh for rendering our map, it is the coordinates within this 40x40 mesh that we calculate.
Applying our inverse matrix to every pixel we are rendering would be very wasteful but luckily for us we're dealing with a linear projection. As a result we only need to calculate our coordinates at each corner of the screen and interpolate those values, something OpenGL is very good at. We'll look into this a little more once we look at our vertex shader.
Where did my model-view-projection matrix go?!?
First however we take a very important sidestep. I've made only minor changes to my sample application. I don't tent to remove things often used as we may change things back for our next example. In our shader loader we are now loading "invtilemap.vs" and "invtilemap.fs" instead of our orignal tilemap shader. While I'm still defining my mvp matrix we are now using the inverse of this matrix in our solution.What you'll see is that our call:
mvpId = glGetUniformLocation(shaderProgram, "mvp");
will fail even though our mvp variable is defined in our shader, it just isn't used. GLSL will automatically filter out any unused variables during compiling the shader and thus there is no variable to obtain our uniform location for.
There is a lot of debate to be found on the internet about this and various strategies on how to deal with it. It is a good thing to research and make up your own mind about. For now I just log variables I can't find so I can make up my mind whether this is intentional or indicates a fault in my program. Especially when you have multiple shaders you may not want to custom make different loaders but simply call the same code to load the shaders and just assume common variables to be there.
Our shader loader code has only slightly been changed, the only noteworthy change is the addition of getting our uniform for invmvp, our inverse model-view-projection matrix.
In our render function we then see that we calculate the inverse of our matrix and set it in our shader:
// set our model-view-projection matrix first
mat4Copy(&mvp, &projection);
mat4Multiply(&mvp, &view);
if (mvpId >= 0) {
glUniformMatrix4fv(mvpId, 1, false, (const GLfloat *) mvp.m);
};
if (invMvpId >= 0) {
// also want our inverse..
mat4Inverse(&invmvp, &mvp);
glUniformMatrix4fv(invMvpId, 1, false, (const GLfloat *) invmvp.m);
};
A little further down we also see that our call to glDrawArrays now tells OpenGL to only draw 2 triangles.
Our inverse shaders
This is due to our main change that we are going to draw each pixel on screen once. As a result we need to draw a single square, thus two triangles, that encompass the entire screen. In OpenGL we're dealing with a coordinate system that has (1.0, 1.0) in the top right and (-1.0, -1.0) so that is what we're drawing in our vertex shader:#version 330
uniform mat4 mvp;
uniform mat4 invmvp;
out vec2 T;
void main() {
// our triangle primitive
// 2--------1/5
// | /|
// | / |
// | / |
// | / |
// |/ |
//0/3--------4
const vec2 vertices[] = vec2[](
vec2(-1.0, -1.0),
vec2( 1.0, 1.0),
vec2(-1.0, 1.0),
vec2(-1.0, -1.0),
vec2( 1.0, -1.0),
vec2( 1.0, 1.0)
);
// Get our vertice
vec4 V = vec4(vertices[gl_VertexID], 0.0, 1.0);
// and project it as is
gl_Position = V;
// now apply the inverse of our projection and use the x/y
T = (invmvp * V).xy;
}
Notice also that we apply our inverse model-view-projection matrix but only store the x/y into a 2D vector, that is all we're interested in here. As mentioned before, OpenGL will nicely interpolate this value for us.
The real work now happens in our fragment shader so we'll handle that in parts:
#version 330 uniform sampler2D mapdata; uniform sampler2D tiles; in vec2 T; out vec4 fragcolor;Nothing much special in our header, we now have both texture maps here, our T input from our vertex shader and our fragcolor output.
void main() {
vec2 to, ti;
// our tiles are 100.0 x 100.0 sized, need to map that
to = T / 100.0;
ti = vec2(floor(to.x), floor(to.y));
to = to - ti;
Here we've defined two variables, "to" which will be our coordinate in our tilemap and "ti" which will be our coordinate in our map data. You may recall from our original solution we created our tiles as 1.0 x 1.0 tiles and multiplied them by 100 to get 100.0 x 100.0 tiles. Here we need to divide them by 100.We floor "ti" to get integer values and than subtract "ti" from "to" so "to" contains offsets within a single tile.
// our bitmaps are 32.0 x 32.0 within a 256x256 bitmap: to = 31.0 * to / 256.0;As our tiles are 32x32 tiles within a 256x256 bitmap, just like in our original solution we need to adjust our offset accordingly
// now add an offset for our tile ti += 20.0; int tileidx = int(texture(mapdata, (ti + 0.5) / 40.0).r * 256.0); int s = tileidx % 8; int t = (tileidx - s) / 8; to = to + vec2((float(s * 32) + 0.5) / 256.0, (float(t * 32) + 0.5) / 256.0);And now we use our "ti" value to figure out which tile we need to draw at our pixel. Note that this is pretty much what we originally did in our vertex shader.
// and get out color fragcolor = texture(tiles, to); }
And voila, we've got the same result as before we started but using a very different way to get there.
So where from here?
There is much less to say about this then the previous technique.For one, if you want to overlay multiple layers of maps it would be good to look into retrieving the pixel values for each layer in one fragment shader and either picking the right color or blending them. It would be much more effective then rendering each layer separately and having OpenGL blending it for you.
This technique does not lend itself well initially when you start looking at projecting the map using a 3D projection however using the inverse of a 3D projection opens up an entirely different door. Look around on shadertoy.com and you'll find legions of effects that use this approach as a starting point to apply raycasting type techniques. That however is an entirely different subject for another day.
On a related subject, and this applies to the previous example as well, it will not have escaped you that allowing the user to rotate our map isn't exactly looking nice with the map we are using. That option really only works if we have a pure top down map. I just wanted to show what we can do with our view matrix even if it may not be something you wish to use in a real game environment.
What's next?
I'm not sure yet. I've been going back and forth on what type of mini game to use to get to the next stage. I want to start looking into the OpenGL equivalent of sprites. In its simplest forms we'll just be rendering textured quads but it will allow us to go into rendering with blending turned on at one end, and at GLFW and control input on the other.It may thus take awhile before I make the next blog as I'll be experimenting with a few ideas before I get something worth blogging about.
Saturday, 4 April 2015
Rendering tiles (part 6)
It's finally time to step it up a little and start drawing something usable. As I mentioned in my previous part I want to stick with 2D techniques for awhile.
The first technique I want to look at is two different ways of drawing a 2D tiled map. 2D tiled maps are used in many games to render our background or floor with and are cornerstone to many 2D platformers.
First off, I have to give a wave to Thorbjørn Lindeijer who's behind the excellent http://www.mapeditor.org/. Mapeditor is a tiled map editor that allows you to design your maps that you can subsequently use in your application and the best bit is that it is free to use and supports Windows, Mac and Linux.
Now I'm not going to look into a full loader for the TMX file format that Tiled uses but stick much closer to the basics. I'm going to use the desert sample that comes with Tiled.
There are two parts to our map.
The first are our tiles. For our tiles we have a simple image file in which we have lots of smaller bitmaps all of equal size that we can use repeatedly to make our map:
This looks really handy to the human eye, nice lines in between the tiles, easy to identify, but for a computer they are annoying. Each tile is nicely 32x32 pixels wide and that is something computers like. Another thing OpenGL tends to like, thought this limitation no longer for all hardware, is that texture maps are a magnitude of 2 and are square. You'll see that the png that ended up in our sample code has the black lines removed and is a nice 256x256 texture map.
Another question you may ask is why not create 48 individual image files, one for each tile? Besides it being easier to handle one file instead of 48, it is also an optimization on our hardware. We'll be drawing each of those tiles multiple times and if we'd have to switch between texture maps our hardware will be wasting precious time.
The second part builds our map itself. We've numbered our tiles 0 through 47 (0-7 being the first row, 8-15 the second, etc) and can now create a map of any size where each cell references one of our tiles that needs to be drawn at that cell. Our sample map is 40x40 tiles and all pieced together looks like this:
That's already a pretty big map :) This could still be off a size where you would just load the entire image as a single texture, it's "only" 1280x1280 big (we've lost a few pixels in the export) but that said, this is a very small sample map.
For a real implementation you may use tiles of a much higher resolution and a much larger map.
Tiled stores this information in a format called TMX but for our example logic that format contains way more functionality that I'd want to cover here so we're taking a simpler approach. Tiled luckily has a CSV export to export a map to CSV files.
To make life easy for our sample logic we're taking that CSV file, add some comma's to the end of each lines, and copy-paste it into our source code. This once again is not something you would do for production, you would save the map to a binary file or maybe even to a texture and load that but for our purposes of today, it will suffice:
Now we need to load this information in a way we can use. We are going to do so by loading them as texture maps into our GPU memory.
For our map data this is pretty easy as we already have our data in memory:
We start with creating two texture objects by calling glGenTextures. Textures is simply an array of unsigned int 2 ints large just like our other GL objects.
We create one for our data map and one for our tile map which we will load in a minute.
We then need to bind the texture we wish to interact with using glBindTexture. Any texture related commands we issue after that effect that texture. First we call glTexParameteri twice to tell OpenGL how we want to filter our image. This determines how OpenGL will interpolate our image if it is rendered at a higher resolution or when it is scaled back. We used GL_NEAREST here as our data map should not be interpolated at all. We also set our image wrapping to clamp to edge. This means that we do not "tile" our image but that the edges of the images are the borders for our texture lookups.
Finally we load our texture map. Note that we're loading it as a single channel image (hence GL_RED).
For our tile map we will use the image loader that can be found in the STB library. I've mentioned STB before as we're using the truetype font logic embedded within to render our text. It is a wonderful library containing a collection of handy functions to do various things. Loading image files that we can then use as textures is one of those.
After we've loaded our texture into GPU memory we no longer need to retain the copy we just loaded and can free the data by calling stbi_image_free.
Note: we do need to include our image library and add our implementation #define in main.c.
P.S. Another library to look at is SOIL. SOIL is a library that uses stb_image but adds various file formats and many handy wrappers for loading images directly into OpenGL texture objects.
The first method we'll use to render our map is pretty straight forward but somewhat wasteful. But it has some advantages in flexibility. Basically what we are going to do is take our entire 40x40 map and turn it into a 40x40 mesh (well x2 as we need two triangles to render one tile).
We are not however going to build the mesh using vertex buffers. Instead we're going to off load all this to the GPU. Basically what we are going to do is call glDrawArrays (a simpler version of glDrawElements), tell it to draw triangles and tell it the number of vertices involved (40 tiles wide * 40 tiles high * 2 triangles * 3 vertices) and we're doing this without giving it any data.
The result is simply that our vertex shader gets called for each of our vertices and OpenGL will start rendering triangles for every 3 vertices. We'll need to build all our data within our shaders.
Note: we do still need a vertex array object to encapsulate our state even though we hardly load any state.
Before we get to our shaders that do the heavy lifting this is the code in our render loop that eventually will result in our map being drawn:
Drawing our map is now a question of making our VAO active (which is pretty much an empty VAO) and calling glDrawArrays.
Note that earlier on in our source code we're loading different shaders and retrieving our mapdataId and tileId uniforms.
We're going to stick with our reverse order and look at our fragment shader first. Just to recap, our fragment shader gets called for every pixel we draw on screen to determine its color:
Note that we have two new variables. The first is a uniform (so set from our C source) but of type sampler. This tells OpenGL this is linked to a texture resource.
The second is a new input called T which is a 2D vector, we'll be setting this in our vertex shader. This is the texture coordinate within our tilemap that we'll be drawing.
Our main function simply looks up our texture value using the texture function and assigns it to fragcolor.
Our vertex shader however has become fairly complex so we'll handle that part by part.
OpenGL does the rest:)
If we'd compile of sample at this time OpenGL would nicely draw the center of our map but only about 1/4th of our map but we have no way to interact with it so it is time to look at some basic keyboard interaction.
We'll add support for WASD controls for moving up/left/down/right. We'll also add support for rotating our map using our O and P keys.
There are two ways of handling this. When we press our key our keyboard callback function gets called with a GLFW_PRESS action, when we release the key we get a GLFW_RELEASE action but more importantly if we keep our key pressed we'll also get GLFW_REPEAT calls every couple of ticks. Checking for GLFW_PRESS and/or GLFW_REPEAT is one way to handle this (and this we will use), the other is checking the status of the key by calling glfwGetKey within our update loop.
Finally important to note is that we will do all our key handling in our main.c but add actions to our engine for our movements. This allows us to call the same actions based on other inputs as well (say a mouse, or joystick, or touch) at some later stage without needing to make changes to our core game engine.
We're going to start with our O and P keys for rotating our map.
For this we're going to define our view matrix properly in our source code first as this is what we'll be modifying using our keystrokes (we're kinda changing our 'camera', the map doesn't move, it is our viewpoint to the map that changes).
All we need to do to kick this off is change our view matrix to be a global variable and to initialize it as an identity matrix in engineLoad instead of in our render loop.
For rotating we add the following method to our engine.c source:
So what are things that you'll want to approve or can do with this next?
The next post we will look at a different technique to render the same map as in this write-up. While the method described here is very flexible it suffers from wasted processing power as we're rendering far more triangles then we actually display on screen. Often enough the added flexibility far outweighs the overhead especially if we apply the enhancement in our first bullet point up above there is another technique which means we only render what we need.
The first technique I want to look at is two different ways of drawing a 2D tiled map. 2D tiled maps are used in many games to render our background or floor with and are cornerstone to many 2D platformers.
First off, I have to give a wave to Thorbjørn Lindeijer who's behind the excellent http://www.mapeditor.org/. Mapeditor is a tiled map editor that allows you to design your maps that you can subsequently use in your application and the best bit is that it is free to use and supports Windows, Mac and Linux.
Now I'm not going to look into a full loader for the TMX file format that Tiled uses but stick much closer to the basics. I'm going to use the desert sample that comes with Tiled.
There are two parts to our map.
The first are our tiles. For our tiles we have a simple image file in which we have lots of smaller bitmaps all of equal size that we can use repeatedly to make our map:
This looks really handy to the human eye, nice lines in between the tiles, easy to identify, but for a computer they are annoying. Each tile is nicely 32x32 pixels wide and that is something computers like. Another thing OpenGL tends to like, thought this limitation no longer for all hardware, is that texture maps are a magnitude of 2 and are square. You'll see that the png that ended up in our sample code has the black lines removed and is a nice 256x256 texture map.
Another question you may ask is why not create 48 individual image files, one for each tile? Besides it being easier to handle one file instead of 48, it is also an optimization on our hardware. We'll be drawing each of those tiles multiple times and if we'd have to switch between texture maps our hardware will be wasting precious time.
The second part builds our map itself. We've numbered our tiles 0 through 47 (0-7 being the first row, 8-15 the second, etc) and can now create a map of any size where each cell references one of our tiles that needs to be drawn at that cell. Our sample map is 40x40 tiles and all pieced together looks like this:
That's already a pretty big map :) This could still be off a size where you would just load the entire image as a single texture, it's "only" 1280x1280 big (we've lost a few pixels in the export) but that said, this is a very small sample map.
For a real implementation you may use tiles of a much higher resolution and a much larger map.
Tiled stores this information in a format called TMX but for our example logic that format contains way more functionality that I'd want to cover here so we're taking a simpler approach. Tiled luckily has a CSV export to export a map to CSV files.
To make life easy for our sample logic we're taking that CSV file, add some comma's to the end of each lines, and copy-paste it into our source code. This once again is not something you would do for production, you would save the map to a binary file or maybe even to a texture and load that but for our purposes of today, it will suffice:
// map data
unsigned char mapdata[1600] = {
...
};
Now we need to load this information in a way we can use. We are going to do so by loading them as texture maps into our GPU memory.
For our map data this is pretty easy as we already have our data in memory:
// Need to create our texture objects glGenTextures(2, textures); // now load in our map texture into textures[0] glBindTexture(GL_TEXTURE_2D, textures[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, 40, 40, 0, GL_RED, GL_UNSIGNED_BYTE, mapdata);
We start with creating two texture objects by calling glGenTextures. Textures is simply an array of unsigned int 2 ints large just like our other GL objects.
We create one for our data map and one for our tile map which we will load in a minute.
We then need to bind the texture we wish to interact with using glBindTexture. Any texture related commands we issue after that effect that texture. First we call glTexParameteri twice to tell OpenGL how we want to filter our image. This determines how OpenGL will interpolate our image if it is rendered at a higher resolution or when it is scaled back. We used GL_NEAREST here as our data map should not be interpolated at all. We also set our image wrapping to clamp to edge. This means that we do not "tile" our image but that the edges of the images are the borders for our texture lookups.
Finally we load our texture map. Note that we're loading it as a single channel image (hence GL_RED).
For our tile map we will use the image loader that can be found in the STB library. I've mentioned STB before as we're using the truetype font logic embedded within to render our text. It is a wonderful library containing a collection of handy functions to do various things. Loading image files that we can then use as textures is one of those.
// and we load our tiled map
data = stbi_load("desert256.png", &x, &y, &comp, 4);
if (data != 0) {
glBindTexture(GL_TEXTURE_2D, textures[1]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, x, y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
stbi_image_free(data);
};
The code here isn't all that different from loading our mapdata. We are using stbi_load to load our PNG file and we are setting our filter to a linear filter.After we've loaded our texture into GPU memory we no longer need to retain the copy we just loaded and can free the data by calling stbi_image_free.
Note: we do need to include our image library and add our implementation #define in main.c.
P.S. Another library to look at is SOIL. SOIL is a library that uses stb_image but adds various file formats and many handy wrappers for loading images directly into OpenGL texture objects.
First method - render code
The first method we'll use to render our map is pretty straight forward but somewhat wasteful. But it has some advantages in flexibility. Basically what we are going to do is take our entire 40x40 map and turn it into a 40x40 mesh (well x2 as we need two triangles to render one tile).
We are not however going to build the mesh using vertex buffers. Instead we're going to off load all this to the GPU. Basically what we are going to do is call glDrawArrays (a simpler version of glDrawElements), tell it to draw triangles and tell it the number of vertices involved (40 tiles wide * 40 tiles high * 2 triangles * 3 vertices) and we're doing this without giving it any data.
The result is simply that our vertex shader gets called for each of our vertices and OpenGL will start rendering triangles for every 3 vertices. We'll need to build all our data within our shaders.
Note: we do still need a vertex array object to encapsulate our state even though we hardly load any state.
Before we get to our shaders that do the heavy lifting this is the code in our render loop that eventually will result in our map being drawn:
// now tell it which textures to use
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textures[0]);
glUniform1i(mapdataId, 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, textures[1]);
glUniform1i(tileId, 1);
// and draw our triangles
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 40 * 40 * 3 * 2);
glBindVertexArray(0);
Here we see that we inform our shader of our two textures we are about to use. We do this by assigning our textures to be the active textures. We can only have a limited number of textures active at any given time, the number depending on the capabilities of the hardware. The GL_TEXTUREn constants may only be defined up to a limited amount but you can access any active texture index by using GL_TEXTURE0 + n instead.Drawing our map is now a question of making our VAO active (which is pretty much an empty VAO) and calling glDrawArrays.
Note that earlier on in our source code we're loading different shaders and retrieving our mapdataId and tileId uniforms.
Shaders
We're going to stick with our reverse order and look at our fragment shader first. Just to recap, our fragment shader gets called for every pixel we draw on screen to determine its color:
#version 330
uniform sampler2D tiles;
in vec2 T;
out vec4 fragcolor;
void main() {
fragcolor = texture(tiles, T);
}
Note that we have two new variables. The first is a uniform (so set from our C source) but of type sampler. This tells OpenGL this is linked to a texture resource.
The second is a new input called T which is a 2D vector, we'll be setting this in our vertex shader. This is the texture coordinate within our tilemap that we'll be drawing.
Our main function simply looks up our texture value using the texture function and assigns it to fragcolor.
Our vertex shader however has become fairly complex so we'll handle that part by part.
#version 330 uniform mat4 mvp; uniform sampler2D mapdata; out vec2 T;We still have our mvp matrix but new is another sampler, this time for our mapdata, and our texture coordinate output T.
void main() {
// our triangle primitive
// 2--------1/5
// | /|
// | / |
// | / |
// | / |
// |/ |
//0/3--------4
const vec3 vertices[] = vec3[](
vec3(-0.5, 0.5, 0.0),
vec3( 0.5, -0.5, 0.0),
vec3(-0.5, -0.5, 0.0),
vec3(-0.5, 0.5, 0.0),
vec3( 0.5, 0.5, 0.0),
vec3( 0.5, -0.5, 0.0)
);
const vec2 texcoord[] = vec2[](
vec2( 0.0, 31.0 / 256.0),
vec2(31.0 / 256.0, 0.0),
vec2( 0.0, 0.0),
vec2( 0.0, 31.0 / 256.0),
vec2(31.0 / 256.0, 31.0 / 256.0),
vec2(31.0 / 256.0, 0.0)
);
Much like in C code we can define arrays and that is just what we're doing here. We're using two small arrays to create primitives for the two triangles that make up each tile. The first array contains our vertex coordinates for a uniform square centered on 0.0, 0.0, 0.0. The second array contains the coordinates for one tile as if our tile is our top right most tile. Note that as with everything else our coordinate system is unified so our texture map is 1.0 wide by 1.0 high. As our real texture map is 256x256 we need to divide our coordinates.
// now figure out for which tile we are handling our vertex int v = gl_VertexID % 6; int i = (gl_VertexID - v) / 6; // and for which cell int x = i % 40; int y = (i - x) / 40;gl_VertexID is an index given to our shader that tells us which of our 9600 vertices we're currently handling. By taking modulus 6 of this we know which of our 6 vertices for each tile we are handling, that then gives us i which indicates the tile we are currently dealing with. From i we can then determine the x and y of the cell within our map.
// figure out our vertex position vec4 V = vec4((vertices[v] + vec3(float(x - 20), float(y - 20), 0.0)), 1.0); // scale it to a usable size V.xy *= 100.0; // and project it gl_Position = mvp * V;Here we calculate V which initially builds a 40.0 by 40.0 mesh. We scale this up to a 4000.0 by 4000.0 map and finally project it onscreen using our mvp. Remember we set our mvp so that the height of our window is considered to be 1000.0 and the width adjusted for aspect ratio so our map is roughly 4x larger then our screen can display.
// now figure out our texture coord int ti = int(texture(mapdata, vec2((float(x) + 0.5) / 40.0, (float(y) + 0.5) / 40.0)).r * 256.0); int s = ti % 8; int t = (ti - s) / 8; T = texcoord[v] + vec2((float(s * 32) + 0.5) / 256.0, (float(t * 32) + 0.5) / 256.0); }Finally using our x,y cell coordinates we lookup our value in our mapdata for that cell. Note again our texture map coordinates being unified so we need to divide the coordinates by 40.0 (the +0.5 is to ensure we get the center of our pixel). The output of our texture function is an RGBA value (vec4) but our map only uses the red channel so we grab only our red value. Finally our color also is unified so we need to multiply by 256.0 to get our actual tile index value (ti). We then modulo 8 our tile index to determine the offset of our tile in our tilemap texture and assign the result to T (again unified, so divide by 256.0).
OpenGL does the rest:)
Navigating our map
If we'd compile of sample at this time OpenGL would nicely draw the center of our map but only about 1/4th of our map but we have no way to interact with it so it is time to look at some basic keyboard interaction.
We'll add support for WASD controls for moving up/left/down/right. We'll also add support for rotating our map using our O and P keys.
There are two ways of handling this. When we press our key our keyboard callback function gets called with a GLFW_PRESS action, when we release the key we get a GLFW_RELEASE action but more importantly if we keep our key pressed we'll also get GLFW_REPEAT calls every couple of ticks. Checking for GLFW_PRESS and/or GLFW_REPEAT is one way to handle this (and this we will use), the other is checking the status of the key by calling glfwGetKey within our update loop.
Finally important to note is that we will do all our key handling in our main.c but add actions to our engine for our movements. This allows us to call the same actions based on other inputs as well (say a mouse, or joystick, or touch) at some later stage without needing to make changes to our core game engine.
We're going to start with our O and P keys for rotating our map.
For this we're going to define our view matrix properly in our source code first as this is what we'll be modifying using our keystrokes (we're kinda changing our 'camera', the map doesn't move, it is our viewpoint to the map that changes).
All we need to do to kick this off is change our view matrix to be a global variable and to initialize it as an identity matrix in engineLoad instead of in our render loop.
For rotating we add the following method to our engine.c source:
void engineViewRotate(float pAngle) {
vec3 axis;
mat4 rotate;
mat4Identity(&rotate);
mat4Rotate(&rotate, pAngle, vec3Set(&axis, 0.0, 0.0, 1.0));
mat4Multiply(&rotate, &view);
mat4Copy(&view, &rotate);
};
And then in our main.c we change our key callback to:static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
glfwSetWindowShouldClose(window, GL_TRUE);
} else if ((action == GLFW_PRESS) || (action == GLFW_REPEAT)) {
switch (key) {
case GLFW_KEY_O: {
engineViewRotate(-1.0);
} break;
case GLFW_KEY_P: {
engineViewRotate( 1.0);
} break;
default: {
// ignore
} break;
};
};
};
Thanks to being able to rotate our map our WASD function gets a bit more complex because we need to take our rotation into account. Thankfully we can use our view matrix to help us here. We'll use a small part of our matrix to rotate our movement vector and get the following function to move our view:void engineViewMove(float pX, float pY) {
vec3 translate;
// we apply our matrix "transposed" to get counter rotate our movement to our rotation
vec3Set(&translate
, pX * view.m[0][0] + pY * view.m[0][1]
, pX * view.m[1][0] + pY * view.m[1][1]
, 0.0
);
mat4Translate(&view, &translate);
};
And then enhance our keyboard callback: case GLFW_KEY_W: {
engineViewMove( 0.0, 5.0);
} break;
case GLFW_KEY_S: {
engineViewMove( 0.0, -5.0);
} break;
case GLFW_KEY_A: {
engineViewMove( 5.0, 0.0);
} break;
case GLFW_KEY_D: {
engineViewMove(-5.0, 0.0);
} break;
So where from here?
So that takes care of our basic approach but it is still far from a complete solution. This is however as far as I will take it for now.So what are things that you'll want to approve or can do with this next?
- The most important improvement is that this approach is nice for a 40x40 map but once it gets larger we've got a lot of overhead. Say our map is 1000x1000 cells that is a lot of triangles being created every frame 90% of which are off screen. You could load the 1000x1000 cell map as your texture but only create a, say, 10x10 mesh centered on the screen and use an offset to render only a 10x10 slice of cells in your larger map. That wouldn't take more then a few additional lines of code in the vertex shader to accomplish.
- In line with that, you'd probably want to convert your mapdata into an image file so you can just load it. Do be sure you don't use a format such as JPEG as it will corrupt your data.
- Add multi layered maps. A good optimization is to load the map into a single texture using R for the first layer, B for the second, G for your third and A for your fourth and render your map 4 times (vary the Z!). Do make sure when using the alpha channel that you do not use 0 as the GPU assumes that is transparent and will discard the RGBA value. Do use your alpha channel in your tile map for your overlapping layers, remember that even when blending is turned off, an alpha of zero means the pixel is not drawn. You could even keep 4 separate view matrices to create parallax scrolling.
- Use a perspective matrix instead of an orthographical matrix and you can create an F-Zero type map. Add a texture map as a height field and you've got the beginnings of a full 3D terrain renderer (we'll definitely revisit that once we start looking at 3D).
- Finally, add support for other input devices like the mouse (we'll add mouse support later on in the series).
What's next?
As with the previous large write-up I'll be rereading all this over the next couple of days so expect some edits as I find dumb typos and stuff.The next post we will look at a different technique to render the same map as in this write-up. While the method described here is very flexible it suffers from wasted processing power as we're rendering far more triangles then we actually display on screen. Often enough the added flexibility far outweighs the overhead especially if we apply the enhancement in our first bullet point up above there is another technique which means we only render what we need.
Friday, 27 March 2015
The world, in 2 dimensions (part 5)
Many of these tutorials I've seen come and go jump straight into 3D rendering at this point. I feel however there is still plenty to talk about before we get there. Heck there are still plenty of really nice 2D games being build today and then there are a legion of games where a good mix of both 2D and 3D techniques are applied. Even when you're working on a nice 3D game or 3D application 2D still has a place when it comes to the user interface.
In the following parts of the series we'll look a bit in some handy 2D rendering techniques and learn a little bit more about OpenGL in general along the way.
But first we need to do a bit of house keeping and a bit more preparation then I initially thought.
On the subject of Mac and Windows I'll probably stop splitting these posts in two. We're at a point where things compile on both platforms and while there is a little bit extra needed on Windows in adding stuff to the makefiles manually that's hardly a big deal. I'll try and make the changes as I go but I may lag behind actually testing things on Windows so expect that you may need to correct the makefile. I'll generally be using the issue tracker on my GitHub page to clear up those sorts of issues keeping this writeup relatively clean.
The other is that I'm finally going to strip most of the code from main.c that I feel doesn't belong there. A lot of the code has been moved into engine.h and engine.c. The thing needed a name and I couldn't think of anything better but the main principle we'll be following is that we've got a straight forward interface into whatever it is we're running.
At this point in time that interface consists of 4 methods:
- load, called at the start to load resources
- update, called in our loop and basically should implement updating data structures etc. as a result of either time passing or user interaction
- render, called in our loop to render our stuff
- unload, called to unload resources
The idea here is that you could have multiple sets of these "engines" or "parts" or whatever you wish to call them. You could have one for our start screen that shows game stats and a start game button, and then a second for the game itself or maybe for each level of your game. When the user presses start game you unload the start screen and load the games first level and start calling the update/render functions for the game. Once the player dies or reaches the end of the game and its game over, the game is unloaded and the start screen is reloaded. We may one day bring even more structure to this. We will see. In C++ you would most likely create a base class to define this interface and implement each part of your program as a subclass.
As you'll notice I also left the viewport and clear logic in main.c. Think of an application where we call the render function of our game first but then after that we call a UI render function that renders the UI on top of that. We can swap out the game part depending on the level we're running but reuse the UI part.
The split between update and render logic is simply good structure but won't really come to shine until we start to create some real user interaction.
Off course for now we'll simply be running examples so there isn't any major user interaction and we'll leave that for the future.
GLFW has the ability to open up multiple windows but that is something I won't go into here. For the applications I have for GLFW I've not needed this (yet) and I think it's just a distraction to what we're trying to achieve here.
GLFW also has the ability to open up our main window full screen. What it lacks at this point in time is an easy way to switch between windowed mode and full screen. You could close one window and open another to switch between modes but as this creates a new OpenGL context it means reloading all your resources as well. I don't know if context sharing might be a solution here but I've not seen any samples of code dealing with this. I do hope to add this into our little frame work at some point.
For now I'm pretty happy just having it as a toggle and just starting my application with a setting or switch. At this stage I'm managing it with an #ifdef block but in due course we'll improve on this:
We'll look into querying the OS for available resolutions later. Do note that I noticed some issues with full screen on my retina display, even though the documentation suggest the querying the frame buffer size it seems this isn't working correctly in full screen mode at the this time (or I'm doing it wrong:)).
Lets start with glViewPort. glViewPort basically tells OpenGL what area of the window we're rendering too using screen coordinates. Generally speaking you would set this to the full size of your window.
Inside OpenGL however the world is upside down and unified and we have no knowledge of the underlying screen resolution.
As far as OpenGL is concerned the world starts in the bottom left of your screen at (-1.0, -1.0) and ends at the top right at (1.0, 1.0). This always takes some getting used to as historically developers are used to (0.0, 0.0) being at the top left of the screen. This is how Dos, Windows and Mac OS have always handled screen coordinates.
However in 3D having Y pointing 'up' makes sense, in a 3D world going up means going higher so an increasing Y value should equal going up.
Another interesting thing about this approach is that our render area seems kinda square even when most monitors are in some sort of landscape format. If you would draw a square with equal sized sides, say both width and height are 1.0, you'd actually end up with a rectangle as it is all stretched to fit your widescreen monitor.
The orthographic projection that we used in our examples so far allows us to specify what coordinate system we want to use. We specifying a left, right, bottom, top, near and far value and from that a matrix is calculated that handles the transformation from our coordinate system to the one OpenGL requires. We'll ignore the near and far values until the next section but here is how the left, right, bottom and top parameters work.
In our example we saw that we left our top and bottom at -1.0 and 1.0 respectively but we set our left and right based on the aspect ratio of our window size which at 640x480 would be roughly 1.33:
The nice thing is that whether we actually open our window at 640x480, 800x600, 1024x768 or any other resolution, our triangle would take up an equal portion of the screen. If we go wide screen the aspect ration will add space to the sides but our triangle would still have the same relative height.
Also if we were to draw a square, it would actually come out looking square as an equal number of pixels are used for the height and the width of the square.
If the real resolution is important to what you are rendering, which it can be when you are dealing with UI, you can easily adjust your projection matrix to accommodate this:
For our examples going further we'll be using a "virtual" resolution with (0.0, 0.0) at the center of the screen, the height of our screen being 1000 units and the width adjusted by aspect ratio:
The main reason for the Y is down choice is for rendering out text but also, as I mentioned before, it's a more natural choice for 2D rendering. It does mean that to render our triangle, we'll have to make it 500x larger as well.
The real resolution is still important to us, if we're on a small screen, say we're working on a small tablet, we might end up with unreadable text if we don't accommodate for this. Also bitmaps may be scaled down too much and look ugly.
Equally so, if we're on a high resolution monitor we may start to get an interface that is pixelated when bitmaps are scared up.
Often this is easily solved by having bitmaps in multiple resolutions and simply selecting the set of bitmaps that best suit the resolution you are rendering to.
As long as using our depth buffer is disabled things are simply drawn in the order they are given. This can be good enough in many 2D applications as that is the natural order in which you draw things anyway but even in 2D applications using the Z buffer can be very handy.
With a Z buffer the depth you specify for what you are drawing will determine what will overlap. If you draw two squares that overlap, the square that is "closest" to you will be drawn over the square that is furthest away even if it is drawn first.
Note that in our orthographic projection we defined near as being 1.0 and far as being -1.0 but it actually ends up with 1.0 being furthest away:
I'm not sure why the near/far planes for orthographic projections behave like this, obviously a larger Z value being further back does make sense.
Note that when we start using 3D projections later on we'll find out our near plane defines our closer value but there is lots more going on in this case.
The one situation that you do need to be mindful off is once you start using transparency. We'll discuss this in more detail later on but the order in which you draw does become important when you want to draw something semi transparent as you can't draw "underneath" something you've already drawn.
There are a number of libraries out there that deal with this. Some simple, some bloated, some completely beyond what we need.
We'll be using fontstash which was written by Mikko Mononen and while it originally only had an implementation for OpenGL 1/2 I added OpenGL 3 support to it some time ago. Fontstash uses the truetype to bitmap capabilities of one of the STB libraries to generate a texture with which we can render out text. We'll get back to STB later as we'll be using it to load textures as well but it is worth mentioning that STB now also has an implementation called stb_easy_font that allows direct rendering of text through OpenGL. I haven't looked into that yet as its very new but it sounds like a promising alternative.
For now to demonstrate how to use fontstash we'll add a simple frames per second counter to our interface.
First we need a font to actually use to draw our text in. Fontstash comes with a font called DroidSerif which we're also using in our sample code. For now I'm still putting all these support files in the resources folder, on Mac they are nicely placed within our bundle, on windows they're next to our exe. We'll deal with this another day.
While we include our header files through our engine.h header file we again want our implementation to be compiled along with our main.c file so our first change is way at the top of main.c:
The rest of our logic is all in our engine.c file. First, way at the top we define a few globals. Again I'm using globals to keep these examples simple, you probably want to put things in a bit more targeted scope in your application.
font is simply a variable that holds an index to one of the fonts loaded into our context.
lineHeight is the height of a single line of text for the font we are using.
Besides these 3 variables that we use for our font rendering we've also got a few variables for calculating our FPS.
And a little further down we have added a load and unload function that loads, and unloads, our font:
Next is our engine update function in which we calculate our frames per second value:
It is important to know that GLFW limits the frame rate depending on your monitors refresh rate. While our sample application drawing our triangle theoretically could render hundreds of frames a second (if not thousands) my Macbook caps out at 60 fps. Most of the time our application is thus twiddling its thumbs doing absolutely nothing.
Finally at the end of our engines render function, after we've drawn our triangle(s), we draw our FPS counter:
This deserves a little more explanation.
To make text look a little prettier we do not draw the text in solid colors but apply some anti-aliasing to make it all look a bit smoother. As a result we need to blend the color of our text with the underlying colors we have already drawn on screen. This is done by using the alpha channel in our texture map that contains our pre-rendered characters for our font.
Doing so is relatively costly as we're not just writing colors to our screen buffer, we're first reading the current color and applying a blend. As a result by default this behavior is turned off. By default the alpha channel works as an on/off switch. If the alpha value is 0 pixels are simply not drawn. If the alpha value is non-zero the pixel is drawn without any blending. Nice and fast.
For rendering our font we need to turn blending on. More specifically we need to tell OpenGL we want to use standard alpha color blending meaning our alpha value in our color is directly used to blend the underlying color with the color we are drawing. There are other blending modes that allow us to do some really funky color effects but that is a subject for another day.
Next, as the shader embedded in fontstash has no idea of our projection matrix, we need to give it a copy. We're reusing the projection matrix we already have here but obviously we could create one specifically for rendering text. It all depends on what we are doing.
Then we simply use sprintf to create a nicely formatted string and finally we use fonsDrawText to write out our text. This function takes our font context, an left and top coordinate, our text and an optional end of text pointer (if NULL the text is assumed to be zero terminated).
After this we should have a nice FPS counter at the bottom left of our screen.
Next part we're going to dive into one of the most common used techniques for 2D games and that is using a tiled map to draw the background to a game.
In the following parts of the series we'll look a bit in some handy 2D rendering techniques and learn a little bit more about OpenGL in general along the way.
But first we need to do a bit of house keeping and a bit more preparation then I initially thought.
On the subject of Mac and Windows I'll probably stop splitting these posts in two. We're at a point where things compile on both platforms and while there is a little bit extra needed on Windows in adding stuff to the makefiles manually that's hardly a big deal. I'll try and make the changes as I go but I may lag behind actually testing things on Windows so expect that you may need to correct the makefile. I'll generally be using the issue tracker on my GitHub page to clear up those sorts of issues keeping this writeup relatively clean.
The other is that I'm finally going to strip most of the code from main.c that I feel doesn't belong there. A lot of the code has been moved into engine.h and engine.c. The thing needed a name and I couldn't think of anything better but the main principle we'll be following is that we've got a straight forward interface into whatever it is we're running.
At this point in time that interface consists of 4 methods:
- load, called at the start to load resources
- update, called in our loop and basically should implement updating data structures etc. as a result of either time passing or user interaction
- render, called in our loop to render our stuff
- unload, called to unload resources
The idea here is that you could have multiple sets of these "engines" or "parts" or whatever you wish to call them. You could have one for our start screen that shows game stats and a start game button, and then a second for the game itself or maybe for each level of your game. When the user presses start game you unload the start screen and load the games first level and start calling the update/render functions for the game. Once the player dies or reaches the end of the game and its game over, the game is unloaded and the start screen is reloaded. We may one day bring even more structure to this. We will see. In C++ you would most likely create a base class to define this interface and implement each part of your program as a subclass.
As you'll notice I also left the viewport and clear logic in main.c. Think of an application where we call the render function of our game first but then after that we call a UI render function that renders the UI on top of that. We can swap out the game part depending on the level we're running but reuse the UI part.
The split between update and render logic is simply good structure but won't really come to shine until we start to create some real user interaction.
Off course for now we'll simply be running examples so there isn't any major user interaction and we'll leave that for the future.
Windows in GLFW
At this point I want to go briefly into the subject of windows and GLFW.GLFW has the ability to open up multiple windows but that is something I won't go into here. For the applications I have for GLFW I've not needed this (yet) and I think it's just a distraction to what we're trying to achieve here.
GLFW also has the ability to open up our main window full screen. What it lacks at this point in time is an easy way to switch between windowed mode and full screen. You could close one window and open another to switch between modes but as this creates a new OpenGL context it means reloading all your resources as well. I don't know if context sharing might be a solution here but I've not seen any samples of code dealing with this. I do hope to add this into our little frame work at some point.
For now I'm pretty happy just having it as a toggle and just starting my application with a setting or switch. At this stage I'm managing it with an #ifdef block but in due course we'll improve on this:
#ifdef GLFW_FULLSCREEN window = glfwCreateWindow(1024, 768, "GLFW Tutorial", glfwGetPrimaryMonitor(), NULL); #else window = glfwCreateWindow(640, 480, "GLFW Tutorial", NULL, NULL); #endifNote that we always give a screen resolution as the first two parameters. In full screen mode GLFW will find the closest screen mode that will match this resolution. The 3rd parameter is a window title, the fourth identifies the monitor on which we wish to run full screen. The 5th and final parameter allows context sharing between windows but I've yet to experiment with this.
We'll look into querying the OS for available resolutions later. Do note that I noticed some issues with full screen on my retina display, even though the documentation suggest the querying the frame buffer size it seems this isn't working correctly in full screen mode at the this time (or I'm doing it wrong:)).
Window coordinate system and 2D projections
It's time to take a closer look at how our coordinate system works.Lets start with glViewPort. glViewPort basically tells OpenGL what area of the window we're rendering too using screen coordinates. Generally speaking you would set this to the full size of your window.
Inside OpenGL however the world is upside down and unified and we have no knowledge of the underlying screen resolution.
As far as OpenGL is concerned the world starts in the bottom left of your screen at (-1.0, -1.0) and ends at the top right at (1.0, 1.0). This always takes some getting used to as historically developers are used to (0.0, 0.0) being at the top left of the screen. This is how Dos, Windows and Mac OS have always handled screen coordinates.
However in 3D having Y pointing 'up' makes sense, in a 3D world going up means going higher so an increasing Y value should equal going up.
Another interesting thing about this approach is that our render area seems kinda square even when most monitors are in some sort of landscape format. If you would draw a square with equal sized sides, say both width and height are 1.0, you'd actually end up with a rectangle as it is all stretched to fit your widescreen monitor.
The orthographic projection that we used in our examples so far allows us to specify what coordinate system we want to use. We specifying a left, right, bottom, top, near and far value and from that a matrix is calculated that handles the transformation from our coordinate system to the one OpenGL requires. We'll ignore the near and far values until the next section but here is how the left, right, bottom and top parameters work.
In our example we saw that we left our top and bottom at -1.0 and 1.0 respectively but we set our left and right based on the aspect ratio of our window size which at 640x480 would be roughly 1.33:
The nice thing is that whether we actually open our window at 640x480, 800x600, 1024x768 or any other resolution, our triangle would take up an equal portion of the screen. If we go wide screen the aspect ration will add space to the sides but our triangle would still have the same relative height.
Also if we were to draw a square, it would actually come out looking square as an equal number of pixels are used for the height and the width of the square.
If the real resolution is important to what you are rendering, which it can be when you are dealing with UI, you can easily adjust your projection matrix to accommodate this:
mat4ortho(&mvp, 0, ScreenWidth, ScreenHeight, 0, 1.0, -1.0);For our 640x480 window this results in a 1:1 mapping to our screen coordinates:
For our examples going further we'll be using a "virtual" resolution with (0.0, 0.0) at the center of the screen, the height of our screen being 1000 units and the width adjusted by aspect ratio:
mat4Ortho(&mvp, -ratio * 500.0, ratio * 500.0, 500.0f, -500.0f, 1.0f, -1.0f);We end up with a coordinate system like so for a 4:3 screen:
The main reason for the Y is down choice is for rendering out text but also, as I mentioned before, it's a more natural choice for 2D rendering. It does mean that to render our triangle, we'll have to make it 500x larger as well.
The real resolution is still important to us, if we're on a small screen, say we're working on a small tablet, we might end up with unreadable text if we don't accommodate for this. Also bitmaps may be scaled down too much and look ugly.
Equally so, if we're on a high resolution monitor we may start to get an interface that is pixelated when bitmaps are scared up.
Often this is easily solved by having bitmaps in multiple resolutions and simply selecting the set of bitmaps that best suit the resolution you are rendering to.
The depth buffer
While we haven't used it yet, and its turned off by default, a depth buffer is pretty much a standard thing you get in any modern day rendering engine. We can already see in our example so far, even though all we're doing is 2D rendering, that we are specifying all our coordinates in 3D. Even our orthographic projection had a near and far value that determine our translation into the Z axis.As long as using our depth buffer is disabled things are simply drawn in the order they are given. This can be good enough in many 2D applications as that is the natural order in which you draw things anyway but even in 2D applications using the Z buffer can be very handy.
With a Z buffer the depth you specify for what you are drawing will determine what will overlap. If you draw two squares that overlap, the square that is "closest" to you will be drawn over the square that is furthest away even if it is drawn first.
Note that in our orthographic projection we defined near as being 1.0 and far as being -1.0 but it actually ends up with 1.0 being furthest away:
I'm not sure why the near/far planes for orthographic projections behave like this, obviously a larger Z value being further back does make sense.
Note that when we start using 3D projections later on we'll find out our near plane defines our closer value but there is lots more going on in this case.
The one situation that you do need to be mindful off is once you start using transparency. We'll discuss this in more detail later on but the order in which you draw does become important when you want to draw something semi transparent as you can't draw "underneath" something you've already drawn.
Rendering text
There is one omission in OpenGL that we should deal with first and that is rendering out text. Yes there were some simple text rendering functions in OpenGL once but you guessed it, we can't use them anymore now that we're using our programmable pipeline.There are a number of libraries out there that deal with this. Some simple, some bloated, some completely beyond what we need.
We'll be using fontstash which was written by Mikko Mononen and while it originally only had an implementation for OpenGL 1/2 I added OpenGL 3 support to it some time ago. Fontstash uses the truetype to bitmap capabilities of one of the STB libraries to generate a texture with which we can render out text. We'll get back to STB later as we'll be using it to load textures as well but it is worth mentioning that STB now also has an implementation called stb_easy_font that allows direct rendering of text through OpenGL. I haven't looked into that yet as its very new but it sounds like a promising alternative.
For now to demonstrate how to use fontstash we'll add a simple frames per second counter to our interface.
First we need a font to actually use to draw our text in. Fontstash comes with a font called DroidSerif which we're also using in our sample code. For now I'm still putting all these support files in the resources folder, on Mac they are nicely placed within our bundle, on windows they're next to our exe. We'll deal with this another day.
While we include our header files through our engine.h header file we again want our implementation to be compiled along with our main.c file so our first change is way at the top of main.c:
#define FONTSTASH_IMPLEMENTATION #define GLFONTSTASH_IMPLEMENTATIONIn our engine.h we're including the header file for both fontstash itself and our OpenGL 3 implementation:
#include "fontstash/fontstash.h" #include "fontstash/gl3fontstash.h"Note that the files themselves are stored in our 3rdparty subfolder.
The rest of our logic is all in our engine.c file. First, way at the top we define a few globals. Again I'm using globals to keep these examples simple, you probably want to put things in a bit more targeted scope in your application.
// and some globals for our fonts FONScontext * fs = NULL; int font = FONS_INVALID; float lineHeight = 0.0f; // and some runtime variables. double rotation = 0.0f; double frames = 0.0f; double fps = 0.0f; double lastframes = 0.0f; double lastsecs = 0.0f;FONScontext is a structure that holds all the information fontstash needs to render out fonts. This is basically a struct that contains all our data from our raw font data to our vertex array and vertex buffer in OpenGL.
font is simply a variable that holds an index to one of the fonts loaded into our context.
lineHeight is the height of a single line of text for the font we are using.
Besides these 3 variables that we use for our font rendering we've also got a few variables for calculating our FPS.
And a little further down we have added a load and unload function that loads, and unloads, our font:
void load_font() {
// we start with creating a font context that tells us about the font we'll be rendering
fs = gl3fonsCreate(512, 512, FONS_ZERO_TOPLEFT);
if (fs != NULL) {
// then we load our font
font = fonsAddFont(fs, "sans", "DroidSerif-Regular.ttf");
if (font != FONS_INVALID) {
// setup our font
fonsSetColor(fs, gl3fonsRGBA(255,255,255,255)); // white
fonsSetSize(fs, 32.0f); // 32 point font
fonsSetAlign(fs, FONS_ALIGN_LEFT | FONS_ALIGN_TOP); // left/top aligned
fonsVertMetrics(fs, NULL, NULL, &lineHeight);
} else {
engineErrCallback(-201, "Couldn't load DroidSerif-Regular.ttf");
};
} else {
engineErrCallback(-200, "Couldn't create our font context");
};
};
void unload_font() {
if (fs != NULL) {
gl3fonsDelete(fs);
fs = NULL;
};
};
These two functions are called from our engine load and unload functions.Next is our engine update function in which we calculate our frames per second value:
void engineUpdate(double pSecondsPassed) {
rotation = pSecondsPassed * 50.0f;
frames += 1.0f;
fps = (frames - lastframes) / (pSecondsPassed - lastsecs);
if (frames - lastframes > 100.0f) {
// reset every 100 frames
lastsecs = pSecondsPassed;
lastframes = frames;
};
};
Calculating our fps is simple a matter of dividing the number of frames drawn by the number of seconds that have passed. We reset our counter every 100 frames so our fps is calculated over the last few seconds that have passed.It is important to know that GLFW limits the frame rate depending on your monitors refresh rate. While our sample application drawing our triangle theoretically could render hundreds of frames a second (if not thousands) my Macbook caps out at 60 fps. Most of the time our application is thus twiddling its thumbs doing absolutely nothing.
Finally at the end of our engines render function, after we've drawn our triangle(s), we draw our FPS counter:
// change our state a little
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
// now render our FPS
if ((fs != NULL) && (font != FONS_INVALID)) {
char info[256];
// we can use our same projection matrix
gl3fonsProjection(fs, (GLfloat *)projection.m);
// what text shall we draw?
sprintf(info,"FPS: %0.1f", fps);
// and draw some text
fonsDrawText(fs, -ratio * 500.0f, 460.0f, info, NULL);
};
Before we render out our text we disable our Z buffer, as we're just drawing our text on top of everything else, but enable blending.This deserves a little more explanation.
To make text look a little prettier we do not draw the text in solid colors but apply some anti-aliasing to make it all look a bit smoother. As a result we need to blend the color of our text with the underlying colors we have already drawn on screen. This is done by using the alpha channel in our texture map that contains our pre-rendered characters for our font.
Doing so is relatively costly as we're not just writing colors to our screen buffer, we're first reading the current color and applying a blend. As a result by default this behavior is turned off. By default the alpha channel works as an on/off switch. If the alpha value is 0 pixels are simply not drawn. If the alpha value is non-zero the pixel is drawn without any blending. Nice and fast.
For rendering our font we need to turn blending on. More specifically we need to tell OpenGL we want to use standard alpha color blending meaning our alpha value in our color is directly used to blend the underlying color with the color we are drawing. There are other blending modes that allow us to do some really funky color effects but that is a subject for another day.
Next, as the shader embedded in fontstash has no idea of our projection matrix, we need to give it a copy. We're reusing the projection matrix we already have here but obviously we could create one specifically for rendering text. It all depends on what we are doing.
Then we simply use sprintf to create a nicely formatted string and finally we use fonsDrawText to write out our text. This function takes our font context, an left and top coordinate, our text and an optional end of text pointer (if NULL the text is assumed to be zero terminated).
After this we should have a nice FPS counter at the bottom left of our screen.
What's next?
At this point in time we've got our original triangle spinning but with a Z-buffer enabled and an FPS counter, still not all that spectacular. We've done more then I was expecting when I started writing this part so and I suspect the next step will be of equal length.Next part we're going to dive into one of the most common used techniques for 2D games and that is using a tiled map to draw the background to a game.
Saturday, 14 March 2015
Making the move to OpenGL 3+ (Part 4)
So I tried to get my little example to run on Windows and it threw up a few interesting things.
First off, I had forgotten to mention the shader files are in the resource folder. On Mac OS X I copy the contents into the resource folder of the application bundle. GLFW already ensures that is the working directly after initialization.
On windows the files get copied into the same folder as the exe. This is only a temporary solution. Eventually we'll look into packing the files for distribution.
I also had to make the move to GLEW. Without it there is no support for any of the OpenGL 3 commands. Microsoft isn't very interested to include support in their compilers because they rather have you work on Direct X. Understandable as Direct X generally is a generation ahead of OpenGL as far as the latest hardware is concerned though things are balancing out with OpenGL ES becoming as popular as it is in the mobile market.
I'll be changing the make files soon to also use GLEW on Mac OS X. On Windows we are lucky that fully compiled binaries are available so just download those and go. Again I've included just the base files into my GitHub repository but I highly recommend you download the latest files from the GLEW website. I also had issues getting the static libraries to link into the project so it is using the DLL for now.
GLEW is an extension manager. Basically it takes care of all the platform and hardware dependent issues so you don't have to worry about which specific extension function to call. You can also query the hardware to find out if it supports what you want to use. I currently am not doing so and already found out it will simply fall flat on its face if things are not supported.
I'm using this really nice AlienWare laptop with a state of the art nVidia graphics card but as it eats power like there is no tomorrow there is also a low power Intel GPU embedded that is used for less spectacular stuff. Generally the Intel chip will be used unless you start a game that requires power. For some reason that I have to look into it decided my little test application would work just fine with the Intel chipset not withstanding that OpenGL 3+ support on Intel hardware is non-existent. I had to go into the nVidia control panel to tell it to always used the nVidia GPU.
So there is a warning for you Windows people. Yes OpenGL is supported on Windows but poorly so. Especially all you poor people with non-gamer laptops, most have the Intel chipset in one form or another and you'll most likely get stuck. So far for X-platform support.
Embedding GLEW is pretty straight forward and requires us to do 2 changes, first include our GLEW library before you include GLFW:
I also found out that the compiler that came with VC 2010 is slightly stricter when it comes to C then the mac one. It does not allow inline variable declarations, they must be placed at the start of code blocks.
Finally we had to enhance our makefile to copy our shader files and our GLEW32.DLL in place. Pretty straight forward.
Download the source code for GLEW from: http://glew.sourceforge.net/index.html
Then unzip the contents somewhere to your liking. I've downloaded and tested this with GLEW 1.12.0 which is currently the latest version.
Unfortunately there are a few faults in the makefiles. First off, for Mac OS X there are three, Makefile.darwin (used by default), Makefile.darwin-ppc and Makefile.darwin-x86_64. You can find these in the config folder.
They build GLEW for the different platforms but then go and use AR to build the static libraries which will fail if we want to support universal binaries. So we need to make a few enhancements to the Makefile.darwin file.
We're going to add at the end of the file:
Also note that all though LDFLAGS.STATIC is defined it isn't used in our central make file so we also need to edit the makefile in the root. Search for any accurance of "$(AR) cr" (there should be two) and change it to: $(AR) $(LDFLAGS.STATIC)
Now open up terminal, cd into the root of your GLEW source and type make.
Finally we have to add our new library to our folder structure and add it to our Mac OS X makefile. I've placed these all in the GitHub repository.
First off, I had forgotten to mention the shader files are in the resource folder. On Mac OS X I copy the contents into the resource folder of the application bundle. GLFW already ensures that is the working directly after initialization.
On windows the files get copied into the same folder as the exe. This is only a temporary solution. Eventually we'll look into packing the files for distribution.
I also had to make the move to GLEW. Without it there is no support for any of the OpenGL 3 commands. Microsoft isn't very interested to include support in their compilers because they rather have you work on Direct X. Understandable as Direct X generally is a generation ahead of OpenGL as far as the latest hardware is concerned though things are balancing out with OpenGL ES becoming as popular as it is in the mobile market.
I'll be changing the make files soon to also use GLEW on Mac OS X. On Windows we are lucky that fully compiled binaries are available so just download those and go. Again I've included just the base files into my GitHub repository but I highly recommend you download the latest files from the GLEW website. I also had issues getting the static libraries to link into the project so it is using the DLL for now.
GLEW is an extension manager. Basically it takes care of all the platform and hardware dependent issues so you don't have to worry about which specific extension function to call. You can also query the hardware to find out if it supports what you want to use. I currently am not doing so and already found out it will simply fall flat on its face if things are not supported.
I'm using this really nice AlienWare laptop with a state of the art nVidia graphics card but as it eats power like there is no tomorrow there is also a low power Intel GPU embedded that is used for less spectacular stuff. Generally the Intel chip will be used unless you start a game that requires power. For some reason that I have to look into it decided my little test application would work just fine with the Intel chipset not withstanding that OpenGL 3+ support on Intel hardware is non-existent. I had to go into the nVidia control panel to tell it to always used the nVidia GPU.
So there is a warning for you Windows people. Yes OpenGL is supported on Windows but poorly so. Especially all you poor people with non-gamer laptops, most have the Intel chipset in one form or another and you'll most likely get stuck. So far for X-platform support.
Embedding GLEW is pretty straight forward and requires us to do 2 changes, first include our GLEW library before you include GLFW:
// include GLEW #include <GL/glew.h> // include these defines to let GLFW know we need OpenGL 3 support #define GLFW_INCLUDE_GL_3 #include <GLFW/glfw3.h>And then right after creating your GLFW window you need to initialize GLEW:
// create our window
window = glfwCreateWindow(640, 480, "GLFW Tutorial", NULL, NULL);
if (window) {
GLenum err;
// make our context current
glfwMakeContextCurrent(window);
// init GLEW
glewExperimental=true;
err = glewInit();
if (err != GLEW_OK) {
error_callback(err, glewGetErrorString(err));
exit(EXIT_FAILURE);
};
Note that we set a global called glewExperimental to true, without this OpenGL 3 won't be properly supported. I guess they are still working things out too:)I also found out that the compiler that came with VC 2010 is slightly stricter when it comes to C then the mac one. It does not allow inline variable declarations, they must be placed at the start of code blocks.
Finally we had to enhance our makefile to copy our shader files and our GLEW32.DLL in place. Pretty straight forward.
GLEW on the MAC
Originally I wasn't planning on adding this but as I just updated the source code for Mac OS X I was reminded of the default installation of GLEW not compiling on my machine.Download the source code for GLEW from: http://glew.sourceforge.net/index.html
Then unzip the contents somewhere to your liking. I've downloaded and tested this with GLEW 1.12.0 which is currently the latest version.
Unfortunately there are a few faults in the makefiles. First off, for Mac OS X there are three, Makefile.darwin (used by default), Makefile.darwin-ppc and Makefile.darwin-x86_64. You can find these in the config folder.
They build GLEW for the different platforms but then go and use AR to build the static libraries which will fail if we want to support universal binaries. So we need to make a few enhancements to the Makefile.darwin file.
We're going to add at the end of the file:
AR = LIBTOOL STRIP = CFLAGS.EXTRA += -arch i386 -arch x86_64 LDFLAGS.EXTRA += -arch i386 -arch x86_64 LDFLAGS.STATIC += -static -o
Also note that all though LDFLAGS.STATIC is defined it isn't used in our central make file so we also need to edit the makefile in the root. Search for any accurance of "$(AR) cr" (there should be two) and change it to: $(AR) $(LDFLAGS.STATIC)
Now open up terminal, cd into the root of your GLEW source and type make.
Finally we have to add our new library to our folder structure and add it to our Mac OS X makefile. I've placed these all in the GitHub repository.
What's next?
I'll be playing around for a little while so the next blog post may take a week or two but I'm planning on diving into more 2D samples first. Most of these tutorials skip ahead right away to rotating 3D cubes and I feel an entire body of knowledge gets skipped. Learning some of the basics using 2D rendering techniques is definitely not wasted time and it postpones some of the really difficult stuff until we've covered some more of the basics.
Thursday, 12 March 2015
Making the move to OpenGL 3+ (part 3)
Ok, this is going to be a loooong one...
One of the things that was so appealing in OpenGL 1/2 was that the setup was so easy. The hard work was in the platform specific stuff which was handled with GLUT.
GLUT was nice to get you started and to learn but did fall short when you wanted to do more serious stuff. GLUT is also retired which was one of the reasons GLFW and other such frameworks were brought into existence.
Once you make the jump to OpenGL 3+ or its little brother OpenGL ES 2 suddenly you find that there is a lot less out of the box stuff you can use. Not only has the fixed pipeline been replaced by a programmable pipeling (in OpenGL 2 they still lived side by side), there are many more "must dos" that were optional before and suddenly you have to do a lot more of the math yourself.
This is one of the reasons I haven't attempted to explain any of the code presented in the "Getting started" part of this series. We're about to abandon half of it. I will explain most of the code I'll be presenting in this and following parts. I'll mostly rely on comments I've added to any of the supporting libraries (where I've written them) but will go in depth into the code you'll find yourself writing or dealing with.
Before we get started I'm going to impose a few rules (on myself):
Most vector logic is basic trigonometry anyway, it gets a bit more complex when we talk about matrices. I'm going to talk about neither in detail here, those are topics on themselves and while most interesting only a basic understanding is needed to use them and I hope we'll learn by example here.
Ergo, I present you with math3d.h which is a self contained support library with various mathematical functions useful for what we are trying to do here. The name is a little misleading because it is just as applicable for 2D applications as 3D.
I do fully have to admit that some of the more complex functions are simply copied from the OpenGL documentation or other sources related on the subject.
As I'm writing this the library contains basic vector logic for 2D, 3D and 4D vectors and support for 3x3 and 4x4 matrices with only a few of our most basic matrix functions (I've had to rewrite most of my C++ code back into C code and as I've not tested it all end to end yet, we'll be fixing bugs for awhile).
I'll be adding more functionality over time and we may even get to quaternions at some point. It definitely is missing some key functions that we will need at some point in this series (like inverting a matrix).
I'll leave out for a moment how we actually calculate these matrices but will purely take a look the theory behind them.
A matrix is a mathematical tool that allows us to apply a transformation to a vector. This transformation can move that vector, rotate that vector around a center, scale that vector etc. etc. etc.
What makes matrices super cool is that you can multiply them to create a new matrix that combines the two transformations of the other two.
When we look at an object that we wish to draw that object usually starts of as a series of vertices and faces (triangles) that make up that object.
Lets say that we have a simply 3D box. That box consists of 8 vertices and 12 faces (2 triangle for each side, 6 sides in total). This object is usually stored in a way where its center is at (0.0, 0.0, 0.0). For a square box that is 10.0 units wide, high and deep that means our 8 vertices are at: (-5.0, -5.0, -5.0), (5.0, -5.0, -5.0), (5.0, 5.0, -5.0), (-5.0, 5.0, -5.0), (-5.0, -5.0, 5.0), (5.0, -5.0, 5.0), (5.0, 5.0, 5.0) and (-5.0, 5.0, 5.0).
We call this our model space. The first thing we need to do is move and rotate our box to where it actually is positioned within our virtual 3D world.
The matrix that performs the transformation is called our model matrix.
Note that this matrix often is formed by combining a number of individual transformations that determine how our box needs to be positioned.
Once our model is positioned at its place in the 3D world, we say that our coordinates are now in, you guessed it, world space.
Unfortunately our "observer", our virtual "camera" so to say, isn't stuck at the center of the 3D world. The "observer" moves around the 3D world and as such, our view into the 3D world changes. We thus need to move and rotate our box in relation to our "observer" to place it in front of our "camera".
The matrix that performs this transformation is called our view matrix and after applying it to our model we're now in view space.
Very often, actually nearly always, the model matrix and the view matrix are combined to form a model-view matrix that does the transformation from model space into view space in one go. This simply halves the time required to project all our models into view space and as we can still do all our lighting calculations properly that works out pretty good. You can actually see this in our OpenGL 1 code:
The last step we need to do is to take our 3D view space coordinates and decide how they translate to screen coordinates.
We do this by applying a projection matrix.
In our OpenGL 1 sample code we apply an orthographic projection:
This projection is simply a 2D projection that scales our x and y coordinates based on the aspect ratio of our screen resolution. The z coordinate doesn't influence how we draw anything other then how things overlap.
We'll look into calculating other projection matrices later on in the series.
There are many situations, especially when dealing with lighting where you want to work in view space first and only perform our projection last and this is why in OpenGL 1 we set the model-view matrix and the projection matrix separately.
However what is more often done is that we set the model-view matrix and a model-view-projection matrix where this last matrix combines all three matrices into one final matrix that takes model space straight to screen space. We'll be doing that here as well.
Our main shader code will reside in shader.h, another self contained support library. Again I won't go into too much detail about how it works and initially rely on the comments I've placed in this library. I've also only included basic code for now and we'll enhance on this as time goes by. I will go into more detail about the shaders themselves.
Do note that my shader library does not contain any code to load the shader text from disk. This code resides in main.c for the time being. This is simply because I believe they don't go hand in hand. If you wanted to, and I've indeed done this in some of my projections, you could keep your shader code inline in you C file. On the other end of the spectrum you may use a text file loader library that allows you to add precompiler directives into your shader code or you may generate your shader code based on some sort of higher level design document.
A shader program is build up (currently) from 5 programmable stages:
- the vertex shader
- the tessellation control shader
- the evaluation shader
- the geometry shader
- the fragment shader
Only the vertex shader and fragment shader are mandatory, the other 3 were added later and are optional. We'll ignore the existence of the new 3 shader stages for now and implement a simple vertex shader and fragment shader that will allow us to reproduce what we did in OpenGL 1.
The vertex shader
When we look at our triangle that we are rendering it consists of 3 vertices. The vertex shader is responsible for transforming those 3 vertices so they are projected in the right spot and we setup any additional information. For each of our 3 vertices our vertex shader is called
At the bare minimum we have:
- our model-view-projection matrix as a variable to apply to our vertices
- an input for the vertex that we are handling
- an output for our projected vertex in screen space
For our example code we need two more things:
- an input for the color of the vertex that we are handling
- an output for this color so we draw our triangle in the right color
Lets look at the shader code bit by bit and explain what is happening:
Here we define a uniform of type mat4 (a 4x4 matrix) called mvp, which is our model-view-projection variable. A uniform simply is a variable that can be set from outside our shader but is basically a constant inside of our shader. We'll end up setting it in our code later on.
The bit in front of that is our bit of magic: "layout (location=n)".
We'll look at this a bit closer once we start putting things together but in essence this allows us to bind our array of vertices and our array of colors that make up our triangle to our shader.
Here we define a single vec4 (4D vectors) variable called color. This is our color output that is sent to our fragment shader. As I mentioned up above we also need an output for our projected vertex however this is one of the few bonuses we get and it is called gl_Position. In OpenGL 2 when programmable shaders were first added we had loads of these build in variables but only a few survive in OpenGL 3.
The first line of code takes our current vertex transforms it into a 4D vector and then multiplies this with our model-view-projection matrix storing the end result into gl_Position.
Our second line of code simply copies our input color into our output color.
The fragment shader
Now that we know the state of affairs at each corner of our triangle we need to draw the triangle itself. OpenGL nicely interpolates our two outputs from our vertex shader to draw out our triangle. For each pixel that needs to be drawn to screen our fragment shader is called.
Obviously the inputs of our fragment shader must match the outputs of our vertex shader but the fragment shader itself has only one output: the color our pixel needs to be drawn with.
Our fragment shader therefor is as simple as can be:
The 3rd line contains our input called color which matches the output of our vertex shader.
The 4th line defines our output called fragcolor, this used to be a build in variable but we now need to define it. As our fragment shader is only allowed to have one output OpenGL knows what you're intending here (this change may not make any sense but there is good reason for it but it won't be apparent until you start using frame buffers, which is a topic for another day).
Finally we have our main function and in it a single line of code which copies our color input to our output.
This is about as simple as it gets and obviously it gets more complex from here.
When we look at our original OpenGL 1 code we see that we use glBegin, glColor3f, glVertex3f and glEnd to send all our data related to our triangle to OpenGL. We do this every frame. We waste a lot here. Not noticeable on a single triangle but once you have tens of thousands of triangles to draw it really starts to show.
In OpenGL 1 they realized this and initially solved this with the functions glVertexPointer, glColorPointer and glDrawElements (and a couple more but these 3 would draw our triangle).
These allowed us to copy arrays of vertex data and color data into GPU memory and then draw all elements making up whatever it was we are drawing. But it still meant copying this data into GPU memory every frame.
Vertex Buffer Object
Eventually Vertex Buffer Objects (or VBO for short) were added to OpenGL. These allowed us to copy vertex data into GPU memory once and then use it repeatedly saving us a lot of overhead.. The solution was a bit of a hodgepodge however as they kept using the old functions but you needed to bind the VBO containing the data and then call this function with an optional offset.
VBOs however weren't just for vertice data but could be used for much more such as indice data to draw many faces, color data, texture coordinate data, normal vectors data, etc. (don't worry, we'll explain what these are in due time).
This was already a giant leap forward however this still dependent on a very fixed architecture. For instance, we could only use two sets of texture coordinates. What if we needed 3? Or what if I had data I needed in my shader that didn't have a construct in Open GL?
I'm not going into too much detail here yet but eventually we became pretty free as to what data we loaded into a VBO and we could define how OpenGL should dissect this data by defining attributes using the glVertexAttribPointer function.
At this point we saved ourselves lots of overhead in copying data into GPU memory each frame but the tradeoff was an immense amount of setup each time you needed to draw an object.
Vertex Array Objects
To solve this Vertex Arrays Objects were introduced (or VAO for short). A VAO remembers a certain amount of state and once you activate it, that state is restored.
The first time you make a new VAO active it is blank and you would then setup the required state once binding the right VBO, setting up your attributes, etc.
Then before you need to draw your object you simply make the correct VAO active and call our age old glDrawElements function.
When they were initially introduced in OpenGL 2 (I believe) they were optional.
In OpenGL 3 they are mandatory and it is a constant source of blank screens as it is very easy to forget this or to realize you haven't set the correct state in relation to your VAO.
Do note that in explaining I'm trying to stick to the basics and not go too far in depth. Eventually we'll get there as we'll build more and more complex examples in future posts.
You must have the first. The second is optional in the sence that if you do not include it, you will need an extension manager such as GLEW.
We may do this in a future post as I'm not sure how much is supported by default in GLFW as I've previously used GLEW in all my projects.
I think this is also supported on Linux but I haven't done enough testing on Windows, we'll probably get there in the next post.
These are globals for now to keep our example simple.
I have taken the same approach within the shader library and we will thus also give our shader library a pointer to this function.
For now the function simply writes the error to our system log.
At the top we define a few arrays containing the data.
We then create our VAO using the function glGenVertexArrays and make that our current VAO by binding it with glBindVertexArray.
We then create our two VBOs with a single call to glGenBuffers. Our first will be used for vertex and color data, the second for index data.
We bind our first VBO using glBindBuffer to make it current. Note the constant GL_ARRAY_BUFFER which tells OpenGL we're building our buffer containing vertex data. Also note that because our VAO was bound before, our first VBO now becomes the buffer containing vertex data for our VAO.
At this point however our VBO is an empty buffer, we need to tell OpenGL how large our buffer needs to be. We do this by calling glBufferData.
Note here that the first parameter specifies that we're still dealing with our buffer containing our vertex data, the second parameter defines the size of our buffer (we are storing vertex and color data here), our third parameter is NULL (we'll come back to this later) and our last parameter defines this as a static buffer (this tells OpenGL we'll set our data once and it won't change after that.
Now we need to load our vertex and color data into our data buffer. We do these with the next set of commands.
glBufferSubData loads data into a part of our buffer and we use it to load our vertex data in the first half and the color data in the second half of our buffer.
glEnableVertexAttribArray simply enables the use of an attribute, again this is remembered in the state of our VAO.
glVertexAttribPointer finally binds the data we just loaded into our buffer to an attribute in our shader, remember our "layout (location=n)" prefix in our vertex shader? The n corrosponds to the attribute number in our calls. Here we are telling OpenGL how to interpret the data, in our case we tell it that we have 3 floats for each entry.
Attribute 0 is now our vertice data, attribute 1 our color data.
We again use glBindBuffer and glBufferData but now with GL_ELEMENT_ARRAY_BUFFER to load our index information. Our indices define which vertices make up our triangles (well in our case our single triangle).
Note that this time we do use our 3rd parameter of glBufferData as we load our indices directly into our buffer. There is no need to break this up. There is also no need to define attributes here.
Finally we unbind our VAO by calling glBindVertexArray with our parameter set to 0. This isn't very important in our little example here but it is a good habit to learn as it prevents accidental changes to whatever VAO is current.
This is just some standard setup
We also set our keyboard callback and call our load shaders and load objects functions we already talked about up above.
First we apply our orthographic projection.
We skip our view matrix as we're just looking out of origin.
Last we apply our model matrix by rotating our model.
To do this we first need to get our uniform ID for our mvp variable and then use that to set our mvp.
This is the quick and dirty way of doing things, we'll talk about better strategies another day as it is a subject on itself.
In our final part of our render loop we tell GLFW to swap our buffers to make all our drawing work visible and to poll for any events like a good application should.
That's it folks! Compile and we get the same colorful triangle as before, but after 10x as much code it is now being rendered using OpenGL 3+ techniques.
I'll also look at the windows side of this in the weekend.
After thats behind me the next full post we'll start looking into some 2D techniques to draw things a bit more interesting then a spinning triangle.
One of the things that was so appealing in OpenGL 1/2 was that the setup was so easy. The hard work was in the platform specific stuff which was handled with GLUT.
GLUT was nice to get you started and to learn but did fall short when you wanted to do more serious stuff. GLUT is also retired which was one of the reasons GLFW and other such frameworks were brought into existence.
Once you make the jump to OpenGL 3+ or its little brother OpenGL ES 2 suddenly you find that there is a lot less out of the box stuff you can use. Not only has the fixed pipeline been replaced by a programmable pipeling (in OpenGL 2 they still lived side by side), there are many more "must dos" that were optional before and suddenly you have to do a lot more of the math yourself.
This is one of the reasons I haven't attempted to explain any of the code presented in the "Getting started" part of this series. We're about to abandon half of it. I will explain most of the code I'll be presenting in this and following parts. I'll mostly rely on comments I've added to any of the supporting libraries (where I've written them) but will go in depth into the code you'll find yourself writing or dealing with.
Before we get started I'm going to impose a few rules (on myself):
- I've already mentioned this but we're sticking with C. While I love C++ and am more confident with C++ sticking with C leaves out some complexities that get in the way of explaining things. Also it makes it easier to make the jump to other platforms.
- Our main.c file should contain only that which is needed to interact with the user and setup our environment so basically the code that relates directly to GLFW. The main reason for this is to make it easier to port the code to platforms not supported by GLFW or for you to use the stuff here with the framework of your choice but it also just makes sense. Note that I'll break that rule in the beginning as we are still building everything up but further on in this series we'll move more and more out of there.
- Reusable support libraries we will be creating will follow a "single file" approach instead of having a separate .h and .c file. I encountered this first when I started using Sean T. Barrett's excellent STB libraries (which we will end up using here as well) and I found it a great way to write code that is easily distributable. Basically it means that you include the file as normal where you have dependencies on the code but in one place (for us always in main.c) you precede the include with a define that tells the library to include the implementation.
Math library
The first bit we need to implement are vectors and matrices. There are plenty of math libraries around (GLM is a really nice one written in C++ that mimics what you write in GLSL shaders). I'm sure there are some really good ones written in C but I have to admit, I haven't looked and only know some of the C++ ones. Over the years I've build a collection of applicable functions many dating from a time when such libraries weren't available and mostly out of habit I've stuck with them.Most vector logic is basic trigonometry anyway, it gets a bit more complex when we talk about matrices. I'm going to talk about neither in detail here, those are topics on themselves and while most interesting only a basic understanding is needed to use them and I hope we'll learn by example here.
Ergo, I present you with math3d.h which is a self contained support library with various mathematical functions useful for what we are trying to do here. The name is a little misleading because it is just as applicable for 2D applications as 3D.
I do fully have to admit that some of the more complex functions are simply copied from the OpenGL documentation or other sources related on the subject.
As I'm writing this the library contains basic vector logic for 2D, 3D and 4D vectors and support for 3x3 and 4x4 matrices with only a few of our most basic matrix functions (I've had to rewrite most of my C++ code back into C code and as I've not tested it all end to end yet, we'll be fixing bugs for awhile).
I'll be adding more functionality over time and we may even get to quaternions at some point. It definitely is missing some key functions that we will need at some point in this series (like inverting a matrix).
Projection, view and model matrices
Before we continue there are 3 matrices that require a bit of a closer look as they determine how something will be drawn on screen.I'll leave out for a moment how we actually calculate these matrices but will purely take a look the theory behind them.
A matrix is a mathematical tool that allows us to apply a transformation to a vector. This transformation can move that vector, rotate that vector around a center, scale that vector etc. etc. etc.
What makes matrices super cool is that you can multiply them to create a new matrix that combines the two transformations of the other two.
When we look at an object that we wish to draw that object usually starts of as a series of vertices and faces (triangles) that make up that object.
Lets say that we have a simply 3D box. That box consists of 8 vertices and 12 faces (2 triangle for each side, 6 sides in total). This object is usually stored in a way where its center is at (0.0, 0.0, 0.0). For a square box that is 10.0 units wide, high and deep that means our 8 vertices are at: (-5.0, -5.0, -5.0), (5.0, -5.0, -5.0), (5.0, 5.0, -5.0), (-5.0, 5.0, -5.0), (-5.0, -5.0, 5.0), (5.0, -5.0, 5.0), (5.0, 5.0, 5.0) and (-5.0, 5.0, 5.0).
We call this our model space. The first thing we need to do is move and rotate our box to where it actually is positioned within our virtual 3D world.
The matrix that performs the transformation is called our model matrix.
Note that this matrix often is formed by combining a number of individual transformations that determine how our box needs to be positioned.
Once our model is positioned at its place in the 3D world, we say that our coordinates are now in, you guessed it, world space.
Unfortunately our "observer", our virtual "camera" so to say, isn't stuck at the center of the 3D world. The "observer" moves around the 3D world and as such, our view into the 3D world changes. We thus need to move and rotate our box in relation to our "observer" to place it in front of our "camera".
The matrix that performs this transformation is called our view matrix and after applying it to our model we're now in view space.
Very often, actually nearly always, the model matrix and the view matrix are combined to form a model-view matrix that does the transformation from model space into view space in one go. This simply halves the time required to project all our models into view space and as we can still do all our lighting calculations properly that works out pretty good. You can actually see this in our OpenGL 1 code:
glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // our "view matrix" stays at our center looking forward glRotatef((float) glfwGetTime() * 50.f, 0.f, 0.f, 1.f); // our model matrix rotates our triangle as time passes
The last step we need to do is to take our 3D view space coordinates and decide how they translate to screen coordinates.
We do this by applying a projection matrix.
In our OpenGL 1 sample code we apply an orthographic projection:
glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-ratio, ratio, -1.f, 1.f, 1.f, -1.f);
This projection is simply a 2D projection that scales our x and y coordinates based on the aspect ratio of our screen resolution. The z coordinate doesn't influence how we draw anything other then how things overlap.
We'll look into calculating other projection matrices later on in the series.
There are many situations, especially when dealing with lighting where you want to work in view space first and only perform our projection last and this is why in OpenGL 1 we set the model-view matrix and the projection matrix separately.
However what is more often done is that we set the model-view matrix and a model-view-projection matrix where this last matrix combines all three matrices into one final matrix that takes model space straight to screen space. We'll be doing that here as well.
Shader library
With the fixed pipeline gone you have to implement your own shader code. A very basic shader isn't all that hard to implement but there are a few steps into actually getting the shader loaded into your application.Our main shader code will reside in shader.h, another self contained support library. Again I won't go into too much detail about how it works and initially rely on the comments I've placed in this library. I've also only included basic code for now and we'll enhance on this as time goes by. I will go into more detail about the shaders themselves.
Do note that my shader library does not contain any code to load the shader text from disk. This code resides in main.c for the time being. This is simply because I believe they don't go hand in hand. If you wanted to, and I've indeed done this in some of my projections, you could keep your shader code inline in you C file. On the other end of the spectrum you may use a text file loader library that allows you to add precompiler directives into your shader code or you may generate your shader code based on some sort of higher level design document.
A shader program is build up (currently) from 5 programmable stages:
- the vertex shader
- the tessellation control shader
- the evaluation shader
- the geometry shader
- the fragment shader
Only the vertex shader and fragment shader are mandatory, the other 3 were added later and are optional. We'll ignore the existence of the new 3 shader stages for now and implement a simple vertex shader and fragment shader that will allow us to reproduce what we did in OpenGL 1.
The vertex shader
When we look at our triangle that we are rendering it consists of 3 vertices. The vertex shader is responsible for transforming those 3 vertices so they are projected in the right spot and we setup any additional information. For each of our 3 vertices our vertex shader is called
At the bare minimum we have:
- our model-view-projection matrix as a variable to apply to our vertices
- an input for the vertex that we are handling
- an output for our projected vertex in screen space
For our example code we need two more things:
- an input for the color of the vertex that we are handling
- an output for this color so we draw our triangle in the right color
Lets look at the shader code bit by bit and explain what is happening:
#version 330This simply tells OpenGL what capabilities we need for our shader and is directly related to the version of OpenGL we are targeting
uniform mat4 mvp;
Here we define a uniform of type mat4 (a 4x4 matrix) called mvp, which is our model-view-projection variable. A uniform simply is a variable that can be set from outside our shader but is basically a constant inside of our shader. We'll end up setting it in our code later on.
layout (location=0) in vec3 vertices; layout (location=1) in vec3 colors;Here we define two vec3 (3D vectors) variables called vertices and colors. The "in" keyword in front specifies they are inputs.
The bit in front of that is our bit of magic: "layout (location=n)".
We'll look at this a bit closer once we start putting things together but in essence this allows us to bind our array of vertices and our array of colors that make up our triangle to our shader.
out vec4 color;
Here we define a single vec4 (4D vectors) variable called color. This is our color output that is sent to our fragment shader. As I mentioned up above we also need an output for our projected vertex however this is one of the few bonuses we get and it is called gl_Position. In OpenGL 2 when programmable shaders were first added we had loads of these build in variables but only a few survive in OpenGL 3.
void main() {
gl_Position = mvp * vec4(vertices, 1.0);
color = vec4(colors, 1.0);
}
And finally our shader itself. It looks a bit like a C program doesn't it? Every shader stage has its own main function which is its entrypoint. Just like in C we can define additional functions but it is the main function that OpenGL looks for.The first line of code takes our current vertex transforms it into a 4D vector and then multiplies this with our model-view-projection matrix storing the end result into gl_Position.
Our second line of code simply copies our input color into our output color.
The fragment shader
Now that we know the state of affairs at each corner of our triangle we need to draw the triangle itself. OpenGL nicely interpolates our two outputs from our vertex shader to draw out our triangle. For each pixel that needs to be drawn to screen our fragment shader is called.
Obviously the inputs of our fragment shader must match the outputs of our vertex shader but the fragment shader itself has only one output: the color our pixel needs to be drawn with.
Our fragment shader therefor is as simple as can be:
#version 330
in vec4 color;
out vec4 fragcolor;
void main() {
fragcolor = color;
}
The first line is the same as our vertex shader and identifies what OpenGL capabilities we require for this shader.The 3rd line contains our input called color which matches the output of our vertex shader.
The 4th line defines our output called fragcolor, this used to be a build in variable but we now need to define it. As our fragment shader is only allowed to have one output OpenGL knows what you're intending here (this change may not make any sense but there is good reason for it but it won't be apparent until you start using frame buffers, which is a topic for another day).
Finally we have our main function and in it a single line of code which copies our color input to our output.
This is about as simple as it gets and obviously it gets more complex from here.
Buffer objects and vertex arrays
The last piece of the puzzle we need before we can build are our buffer objects and arrays.When we look at our original OpenGL 1 code we see that we use glBegin, glColor3f, glVertex3f and glEnd to send all our data related to our triangle to OpenGL. We do this every frame. We waste a lot here. Not noticeable on a single triangle but once you have tens of thousands of triangles to draw it really starts to show.
In OpenGL 1 they realized this and initially solved this with the functions glVertexPointer, glColorPointer and glDrawElements (and a couple more but these 3 would draw our triangle).
These allowed us to copy arrays of vertex data and color data into GPU memory and then draw all elements making up whatever it was we are drawing. But it still meant copying this data into GPU memory every frame.
Vertex Buffer Object
Eventually Vertex Buffer Objects (or VBO for short) were added to OpenGL. These allowed us to copy vertex data into GPU memory once and then use it repeatedly saving us a lot of overhead.. The solution was a bit of a hodgepodge however as they kept using the old functions but you needed to bind the VBO containing the data and then call this function with an optional offset.
VBOs however weren't just for vertice data but could be used for much more such as indice data to draw many faces, color data, texture coordinate data, normal vectors data, etc. (don't worry, we'll explain what these are in due time).
This was already a giant leap forward however this still dependent on a very fixed architecture. For instance, we could only use two sets of texture coordinates. What if we needed 3? Or what if I had data I needed in my shader that didn't have a construct in Open GL?
I'm not going into too much detail here yet but eventually we became pretty free as to what data we loaded into a VBO and we could define how OpenGL should dissect this data by defining attributes using the glVertexAttribPointer function.
At this point we saved ourselves lots of overhead in copying data into GPU memory each frame but the tradeoff was an immense amount of setup each time you needed to draw an object.
Vertex Array Objects
To solve this Vertex Arrays Objects were introduced (or VAO for short). A VAO remembers a certain amount of state and once you activate it, that state is restored.
The first time you make a new VAO active it is blank and you would then setup the required state once binding the right VBO, setting up your attributes, etc.
Then before you need to draw your object you simply make the correct VAO active and call our age old glDrawElements function.
When they were initially introduced in OpenGL 2 (I believe) they were optional.
In OpenGL 3 they are mandatory and it is a constant source of blank screens as it is very easy to forget this or to realize you haven't set the correct state in relation to your VAO.
Putting it all together
Now that we have all our basic ingredients it is time to take a closer look at our new main.c and this time I'm going to explain most of it line by line. I am removing most of the comments from the source code and a lot of the error checking so be sure to look at the original on GitHub.Do note that in explaining I'm trying to stick to the basics and not go too far in depth. Eventually we'll get there as we'll build more and more complex examples in future posts.
// include these defines to let GLFW know we need OpenGL 3 support #define GLFW_INCLUDE_GL_3 #define GLFW_INCLUDE_GLCOREARB #include <GLFW/glfw3.h>Here we include our GLFW library. Note the addition of two defines which enable OpenGL 3 and COREARB support.
You must have the first. The second is optional in the sence that if you do not include it, you will need an extension manager such as GLEW.
We may do this in a future post as I'm not sure how much is supported by default in GLFW as I've previously used GLEW in all my projects.
// include some standard libraries #include <stdlib.h> #include <stdio.h> #include <string.h> #include <syslog.h>These are just some standard C libraries that are being included. Note that we also include syslog for error logging.
I think this is also supported on Linux but I haven't done enough testing on Windows, we'll probably get there in the next post.
// include support libraries including their implementation #define MATH3D_IMPLEMENTATION #include "math3d.h" #define SHADER_IMPLEMENTATION #include "shaders.h"This is where we include our two new libraries. Note the preceding defines that result in the implementation being included.
// For now just some global state to store our shader program and buffers
GLuint shaderProgram = NO_SHADER;
GLuint VAO = 0;
GLuint VBOs[2] = { 0, 0 };
Here we define some global variables into which we'll store our shader program ID and the IDs for our VAO and two VBOs.These are globals for now to keep our example simple.
// function for logging errors
void error_callback(int error, const char* description) {
syslog(LOG_ALERT, "%i: %s", error, description);
};
GLFW requires us to define a callback function that it can call when an error is encountered. We will tell GLFW later on that this is our function.I have taken the same approach within the shader library and we will thus also give our shader library a pointer to this function.
For now the function simply writes the error to our system log.
// load contents of a file
// return NULL on failure
// returns string on success, calling function is responsible for freeing the text
char* loadFile(const char* pFileName) {
...
};
This function loads the contents of a file into a string. This is C 101 so I'll skip the code.void load_shaders() {
char* shaderText = NULL;
GLuint vertexShader = NO_SHADER, fragmentShader = NO_SHADER;
// set our error callback...
shaderSetErrorCallback(error_callback);
// and load our vertex shader
shaderText = loadFile("simple.vs");
vertexShader = shaderCompile(GL_VERTEX_SHADER, shaderText);
free(shaderText);
// and load our fragment shader
shaderText = loadFile("simple.fs");
fragmentShader = shaderCompile(GL_FRAGMENT_SHADER, shaderText);
free(shaderText);
// link our program
shaderProgram = shaderLink(2, vertexShader, fragmentShader);
// no longer need our shaders
glDeleteShader(fragmentShader);
glDeleteShader(vertexShader);
};
I've removed the error handling here to make the code more readable. Basically we load our vertex shader, then compile it, then free our text, then repeat the same for our fragment shader and finally link our shader program. After this we no longer need the shaders so we free those up.void unload_shaders() {
if (shaderProgram != NO_SHADER) {
glDeleteProgram(shaderProgram);
shaderProgram = NO_SHADER;
};
};
We also have a function to delete our shader program which we'll call when we're cleaning up.void load_objects() {
// data for our triangle
GLfloat vertices[9] = {
-0.6f, -0.4f, 0.0f,
0.6f, -0.4f, 0.0f,
0.0f, 0.6f, 0.0f
};
GLfloat colors[9] = {
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f
};
GLuint indices[3] = { 0, 1, 2 };
// we start with creating our vertex array object
glGenVertexArrays(1, &VAO);
// and make it current, all actions we do now relate to this VAO
glBindVertexArray(VAO);
// and create our two vertex buffer objects
glGenBuffers(2, VBOs);
// load up our vertices
glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
// size our buffer
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices) + sizeof(colors), NULL, GL_STATIC_DRAW);
// layout (location=0) in vec3 vertices;
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, (GLvoid *) 0);
// layout (location=1) in vec3 colors;
glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertices), sizeof(colors), colors);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, (GLvoid *) sizeof(vertices));
// load up our indices
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBOs[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// and clear our selected vertex array object
glBindVertexArray(0);
};
Here it all gets a bit more complex. In this method we initialise our VAO and two VBOs that hold all our data needed to draw our one triangle.At the top we define a few arrays containing the data.
We then create our VAO using the function glGenVertexArrays and make that our current VAO by binding it with glBindVertexArray.
We then create our two VBOs with a single call to glGenBuffers. Our first will be used for vertex and color data, the second for index data.
We bind our first VBO using glBindBuffer to make it current. Note the constant GL_ARRAY_BUFFER which tells OpenGL we're building our buffer containing vertex data. Also note that because our VAO was bound before, our first VBO now becomes the buffer containing vertex data for our VAO.
At this point however our VBO is an empty buffer, we need to tell OpenGL how large our buffer needs to be. We do this by calling glBufferData.
Note here that the first parameter specifies that we're still dealing with our buffer containing our vertex data, the second parameter defines the size of our buffer (we are storing vertex and color data here), our third parameter is NULL (we'll come back to this later) and our last parameter defines this as a static buffer (this tells OpenGL we'll set our data once and it won't change after that.
Now we need to load our vertex and color data into our data buffer. We do these with the next set of commands.
glBufferSubData loads data into a part of our buffer and we use it to load our vertex data in the first half and the color data in the second half of our buffer.
glEnableVertexAttribArray simply enables the use of an attribute, again this is remembered in the state of our VAO.
glVertexAttribPointer finally binds the data we just loaded into our buffer to an attribute in our shader, remember our "layout (location=n)" prefix in our vertex shader? The n corrosponds to the attribute number in our calls. Here we are telling OpenGL how to interpret the data, in our case we tell it that we have 3 floats for each entry.
Attribute 0 is now our vertice data, attribute 1 our color data.
We again use glBindBuffer and glBufferData but now with GL_ELEMENT_ARRAY_BUFFER to load our index information. Our indices define which vertices make up our triangles (well in our case our single triangle).
Note that this time we do use our 3rd parameter of glBufferData as we load our indices directly into our buffer. There is no need to break this up. There is also no need to define attributes here.
Finally we unbind our VAO by calling glBindVertexArray with our parameter set to 0. This isn't very important in our little example here but it is a good habit to learn as it prevents accidental changes to whatever VAO is current.
void unload_objects() {
glDeleteBuffers(2, VBOs);
glDeleteVertexArrays(1, &VAO);
};
Eventually we'll want to clean up and we do this by deleting our two VBOs and our VAO.static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
};
This is another callback GLFW allows us to use. GLFW will call this function whenever the user presses a key. We'll tell it to do so later on. Diving into this and other such callbacks is a subject for another dayint main(void) {
GLFWwindow* window;
// tell GLFW how to inform us of issues
glfwSetErrorCallback(error_callback);
// see if we can initialize GLFW
if (!glfwInit()) {
exit(EXIT_FAILURE);
};
Finally we got to the start of our main function.This is just some standard setup
- a variable to hold a pointer to the window we're about to open,
- telling GLFW about our error callback routine
- and initializing GLFW
// make sure we're using OpenGL 3.2+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);This is a new bit, I'm not entirely sure if this is required on every platform but it is on Mac OS X. Basically this will instruct GLFW once it creates our window that this window needs at least OpenGL 3.2 support including our core profile (i.e. our new rendering pipeline).
// create our window
window = glfwCreateWindow(640, 480, "GLFW Tutorial", NULL, NULL);
if (window) {
// make our context current
glfwMakeContextCurrent(window);
// tell GLFW how to inform us of keyboard input
glfwSetKeyCallback(window, key_callback);
// load, compile and link our shader(s)
load_shaders();
// load our objects
load_objects();
Now we open our window and set the OpenGL context of our window as the current context. We'll talk about our options when opening a window such as full screen rendering, multi monitor support, etc. in a later post.We also set our keyboard callback and call our load shaders and load objects functions we already talked about up above.
// and start our render loop
while (!glfwWindowShouldClose(window)) {
float ratio;
int width, height;
mat4 mvp;
vec3 axis;
Now it is getting interesting, this is the start of our render loop. We'll keep repeating the next bit of code over and over again for as long as our window remains open. This is where a graphics program differs from a normal window application as we're constantly updating the contents of our window instead of waiting for an event to come in. glfwGetFramebufferSize(window, &width, &height);
ratio = width / (float) height;
glViewport(0, 0, width, height);
glClear(GL_COLOR_BUFFER_BIT);
First we retrieve our frame buffer size and set our viewport, then we clear the contents of our OpenGL buffer so we can start on a nice blank canvas.
There are improvements to be made here but for now this will do. One thing that is important here is that we retrieve our frame buffer size, not our window size. On most hardware these will be the same but on for instance Retina screens the frame buffer might be much larger. mat4Identity(&mvp);
mat4Ortho(&mvp, -ratio, ratio, -1.0f, 1.0f, 1.0f, -1.0f);
mat4Rotate(&mvp, (float) glfwGetTime() * 50.0f, vec3Set(&axis, 0.0f, 0.0f, 1.0f));
Next we set up our model-view-projection matrix. Interestingly we do this in "reverse" order.First we apply our orthographic projection.
We skip our view matrix as we're just looking out of origin.
Last we apply our model matrix by rotating our model.
// select our shader
glUseProgram(shaderProgram);
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "mvp"), 1, false, (const GLfloat *) mvp.m);
Next we tell OpenGL which shader program we wish to use and we load our mvp into our shader.To do this we first need to get our uniform ID for our mvp variable and then use that to set our mvp.
This is the quick and dirty way of doing things, we'll talk about better strategies another day as it is a subject on itself.
// draw our triangles:)
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, NULL);
glBindVertexArray(0);
Thanks to all the setup we've done actually drawing our triangle is brought down to these 3 calls. Binding our VAO sets the entire state, then we draw, and finally we unbind. // unset our shader
glUseProgram(0);
// swap our buffers around so the user sees our new frame
glfwSwapBuffers(window);
glfwPollEvents();
};
Again for our simple example unsetting our shader program is a bit of overkill but it is a good habit to learn for when things get more complex.In our final part of our render loop we tell GLFW to swap our buffers to make all our drawing work visible and to poll for any events like a good application should.
// close our window
glfwDestroyWindow(window);
};
// lets be nice and cleanup
unload_objects();
unload_shaders();
// the end....
glfwTerminate();
};
And in this last bit we simply nicely clean up after ourselves....That's it folks! Compile and we get the same colorful triangle as before, but after 10x as much code it is now being rendered using OpenGL 3+ techniques.
What's next?
What's next is that I'm going to get some sleep:) I'll reread what I've posted here over the weekend and fix any stupid mistakes I've made. The code will be up on GitHub in a minute.I'll also look at the windows side of this in the weekend.
After thats behind me the next full post we'll start looking into some 2D techniques to draw things a bit more interesting then a spinning triangle.
Friday, 6 March 2015
Getting started with GLFW (part 2)
This will be a relative small part though I was surprised to find I took the hard way in doing this.
I'm going to walk through the steps for getting our sample library to compile on Windows.
Obviously you need a compiler, I've had Visual Studio 2010 Express installed on my laptop for awhile now so that is what I'm going to stick with. I am however going to compile this using makefiles and using the static libraries. A better way forward probably is to just create a solution in Visual Studio and let it take care of the difficult stuff, you simply start with an empty console project and work from there.
One thing that is nice is that for Windows you can download all the libraries precompiled and save you a bunch of hassle, no need to go through CMake and compile the source code yourself. Off course if you feel inclined to do so, or wish to use the latest source out of github, you have no choice.
The binaries come as dynamic and static libraries. I'm going for static but if you rather use the dynamic libraries simply link in glfw3dll.lib and copy the dll in place instead of using glfw3.lib. Also the makefile becomes a little simpler as you need to include less standard static libraries.
I was surprised to find that the static library was compiled with /MD instead of /MT and that caused a bit of a hassle.
The other thing is that the last time I've worked with makefiles for nmake was years ago and I struggled with the syntax for a bit. A lot of the stuff I'm doing in the makefile on the Mac didn't want to work.
While the makefile for Mac OS X I probably won't need to touch anymore (except maybe for resources) I'll be editing the makefile on Windows each time I add files. If I find how to do this better, I'll revisit this post.
The other thing that is important to know when using makefiles, and thus compiling on the command line, is that the environment is not setup by default. VC does come with a handy batch file you can run that does all the work for you. I've got a batch file init.bat in my home folder so all I need to remember after opening my command prompt is to type in init:
So lets first look at our new folder structure with our new windows files in place:
The contents of the Windows makefile is as follows:
Now in your command line window cd into the windows subfolder and type nmake
This time our result is a simple exe file.
I'm going to walk through the steps for getting our sample library to compile on Windows.
Obviously you need a compiler, I've had Visual Studio 2010 Express installed on my laptop for awhile now so that is what I'm going to stick with. I am however going to compile this using makefiles and using the static libraries. A better way forward probably is to just create a solution in Visual Studio and let it take care of the difficult stuff, you simply start with an empty console project and work from there.
One thing that is nice is that for Windows you can download all the libraries precompiled and save you a bunch of hassle, no need to go through CMake and compile the source code yourself. Off course if you feel inclined to do so, or wish to use the latest source out of github, you have no choice.
The binaries come as dynamic and static libraries. I'm going for static but if you rather use the dynamic libraries simply link in glfw3dll.lib and copy the dll in place instead of using glfw3.lib. Also the makefile becomes a little simpler as you need to include less standard static libraries.
I was surprised to find that the static library was compiled with /MD instead of /MT and that caused a bit of a hassle.
The other thing is that the last time I've worked with makefiles for nmake was years ago and I struggled with the syntax for a bit. A lot of the stuff I'm doing in the makefile on the Mac didn't want to work.
While the makefile for Mac OS X I probably won't need to touch anymore (except maybe for resources) I'll be editing the makefile on Windows each time I add files. If I find how to do this better, I'll revisit this post.
The other thing that is important to know when using makefiles, and thus compiling on the command line, is that the environment is not setup by default. VC does come with a handy batch file you can run that does all the work for you. I've got a batch file init.bat in my home folder so all I need to remember after opening my command prompt is to type in init:
call "c:\Program Files (x86)\Microsoft Visual Studio 10.0\vc\vcvarsall.bat" x86 cd Development
So lets first look at our new folder structure with our new windows files in place:
3rdparty
- GLFW
- glfw3.h
- libglfw3_mac.a
- glfw3.lib
build
include
macosx
- Info.plist
- makefile
resources
- app.icns
source
- main.c
windows
- makefile
Pretty straight forward, just the windows glfw library and the makefile in our windows folder that have been added.The contents of the Windows makefile is as follows:
# Compiler directives for Windows CFLAGS = /c /WX /MD /nologo /D "WIN32" /I..\include /I..\3rdparty LDFLAGS = /nologo /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup /WX WINLIBS = opengl32.lib kernel32.lib user32.lib gdi32.lib comdlg32.lib advapi32.lib shell32.lib uuid.lib APPNAME = glfw-tutorial.exe OBJECTDIR = ..\build\Objects CONTENTSDIR = ..\build OBJECTS = $(OBJECTDIR)\main.obj all: $(CONTENTSDIR) \ $(OBJECTDIR) \ $(CONTENTSDIR)\$(APPNAME) \ $(CONTENTSDIR): mkdir $(CONTENTSDIR) $(OBJECTDIR): mkdir $(OBJECTDIR) $(CONTENTSDIR)\$(APPNAME): $(OBJECTS) ..\3rdparty\GLFW\glfw3.lib link $(LDFLAGS) /out:$@ $** $(WINLIBS) $(OBJECTDIR)\main.obj: cl $(CFLAGS) /Fo$@ ..\source\main.c clean: rmdir ..\build /s /q
Now in your command line window cd into the windows subfolder and type nmake
This time our result is a simple exe file.
What's next?
As mentioned in part 1, now that we've got our basic example working it is time to make it work using OpenGL 3.
Subscribe to:
Posts (Atom)







