In this article, we will be refactoring the code to be more like a 3D engine/framework. Specifically, we will be replacing some of the globals with structs that represent "assets" and "instances." At the end, we will have a single wooden crate asset, and five instances of that asset arranged to spell out "Hi" in 3D. Accessing The CodeDownload all the code as a zip from here: https://github.com/tomdalling/opengl-series/archive/master.zip All the code in this series of articles is available from github: https://github.com/tomdalling/opengl-series. You can download a zip of all the files from that page, or you can clone the repository if you are familiar with git. This article builds on the code from the previous article.
The code for this article can be found in the
The project includes all of its dependencies, so you shouldn't have to install or configure anything extra. Please let me know if you have any issues compiling and running the code. Assets, In GeneralFor the purpose of this article, we will define an asset as a 3D object that can be drawn – often just referred to as a "model." The term "asset" is very broad and can mean a variety of things. Other 3D engines and frameworks might use the term "resources" instead of "assets." Often, the term "asset" includes things like music, sound effects, particle emitters, shaders, meshes, game levels, etc. For the purpose of this article, we will define an asset as a 3D object that can be drawn – often just referred to as a "model." In more complicated 3D engines, a model is typically made up of meshes and materials. A mesh typically contains the per-vertex data: the vertices, texture coordinates, etc. A material typically contains the shaders, and some values for uniform variables of the shader, for example: textures, colors, shininess, etc. In the code for this article, we will not have separate classes for meshes and materials. For simplicity, we will just have a single struct called The ModelAsset StructWe will represent assets with this struct: struct ModelAsset { tdogl::Program* shaders; tdogl::Texture* texture; GLuint vbo; GLuint vao; GLenum drawType; GLint drawStart; GLint drawCount; }; Each asset contains shaders, a single texture, a VBO, VAO, and all the parameters to You will notice that each asset has its own shader. This allows us to use different shaders for different assets. A single shader can also be shared by multiple assets, because it is stored as a pointer. Each asset also has a single texture. Most 3D engines have multitexture support, but we will just stick to a single texture per model for simplicity. The VBO contains all the vertices and texture coordinates, the same as in the previous articles. The VAO is also the same as in previous articles. The Instances, In GeneralAn instance is an asset with an individual position, size, and other properties. The main reason why you separate assets from instances is that you can have multiple instances of a single asset. An instance is an asset with an individual position, size, and other properties. The main reason why you separate assets from instances is that you can have multiple instances of a single asset. The fewer assets, the less video memory you need. For example, you can make a forest scene by creating 100 instances of a single tree asset. Each instance would be in a different position, with a slightly different size, and rotated a little bit. From the viewer's perspective, it looks like 100 different trees. From the programmer's perspective, it is actually just one tree that is drawn 100 times. Instances vs EntitiesYou may find that "instances" sound very similar to "entities" that you have seen in other frameworks and engines. Indeed, they both have a position, a size, a rotation, and you can draw them. You could design your code so that instances and entities are the same thing, or maybe the In some situations it is nice to have entities that are not instances. For example, maybe your camera is an entity – or maybe you have a "trigger" entity that is invisible, but something happens when the player collides with it. In other situations, you want to have instances that are not entities. For example, maybe there is a static area of your game that the player can not get to, so you just want to draw a bunch of instances without any of the other features of entities, such as collision detection, animation, movement, etc. From the examples above, we can conclude that entities and instances are separate things, so one should not inherit from the other. A good rule of thumb, no matter what you're programming, is to prefer composition to inheritance, so let's do that. A decent design would be for each entity to have an instance pointer. If the instance pointer is null, then the entity can't be drawn, and is invisible. If you want instances that are not entities, you can do that too, because the instances do not depend on entities in any way. Basically, the instance class is only used for drawing, and the entity class contains the rest of the functionality. If you keep following this avenue of design, you eventually end up with an entity system architecture. Entity system architectures are quite elegant, in my opinion, and I might do an article about them in the future. The ModelInstance StructWe will represent instances with this struct: struct ModelInstance { ModelAsset* asset; glm::mat4 transform; }; This struct is very simple. It only contains an asset and a transformation matrix. A single matrix is all that is necessary to control the size, position, and rotation of the instance. This per-instance matrix is often call the "model matrix." In the code for this article, we have one asset and five instances. This means the one asset will get drawn five times. Every time the asset gets drawn, it will have a different transformation matrix applied, which will change the position, size and rotation of the asset. Integrating ModelAsset and ModelInstanceFirst lets look at how the globals have changed. // new globals this article ModelAsset gWoodenCrate; std::list<ModelInstance> gInstances; // deleted globals from last article /* tdogl::Texture* gTexture = NULL; tdogl::Program* gProgram = NULL; GLuint gVAO = 0; GLuint gVBO = 0; */ // unchanged globals GLFWwindow* gWindow = NULL; double gScrollY = 0.0; tdogl::Camera gCamera; GLfloat gDegreesRotated = 0.0f; The deleted globals are all part of the Now lets look in the start of the static void LoadWoodenCrateAsset() { gWoodenCrate.shaders = LoadShaders("vertex-shader.txt", "fragment-shader.txt"); gWoodenCrate.drawType = GL_TRIANGLES; gWoodenCrate.drawStart = 0; gWoodenCrate.drawCount = 6*2*3; gWoodenCrate.texture = LoadTexture("wooden-crate.jpg"); glGenBuffers(1, &gWoodenCrate.vbo); glGenVertexArrays(1, &gWoodenCrate.vao); //... The code is almost identical to the Now that we have seen how the asset is loaded, let's look at how the instances are made inside of the static void CreateInstances() { ModelInstance dot; dot.asset = &gWoodenCrate; dot.transform = glm::mat4(); gInstances.push_back(dot); ModelInstance i; i.asset = &gWoodenCrate; i.transform = translate(0,-4,0) * scale(1,2,1); gInstances.push_back(i); ModelInstance hLeft; hLeft.asset = &gWoodenCrate; hLeft.transform = translate(-8,0,0) * scale(1,6,1); gInstances.push_back(hLeft); ModelInstance hRight; hRight.asset = &gWoodenCrate; hRight.transform = translate(-4,0,0) * scale(1,6,1); gInstances.push_back(hRight); ModelInstance hMid; hMid.asset = &gWoodenCrate; hMid.transform = translate(-6,0,0) * scale(2,1,0.8); gInstances.push_back(hMid); } For each of the five instances, we set the asset to The functions The identity matrix is a special matrix that does not do any transformation. Notice that the first instance does not have a transformation. The matrix for this instance is the identity matrix. The identity matrix is a special matrix that does not do any transformation. We are going to make this first instance spin like in the previous article, so let's look at the void Update(float secondsElapsed) { //rotate the first instance in `gInstances` const GLfloat degreesPerSecond = 180.0f; gDegreesRotated += secondsElapsed * degreesPerSecond; while(gDegreesRotated > 360.0f) gDegreesRotated -= 360.0f; gInstances.front().transform = glm::rotate(glm::mat4(), gDegreesRotated, glm::vec3(0,1,0)); //... The only difference is the addition of that last statement: gInstances.front().transform = glm::rotate(glm::mat4(), gDegreesRotated, glm::vec3(0,1,0)); This takes the first instance in Now that we have a list of instances to draw, let's look at the static void Render() { // clear everything glClearColor(0, 0, 0, 1); // black glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // render all the instances std::list<ModelInstance>::const_iterator it; for(it = gInstances.begin(); it != gInstances.end(); ++it){ RenderInstance(*it); } // swap the display buffers (displays what was just drawn) glfwSwapBuffers(); } This function is simpler than in the last article. Now, it just loops over the static void RenderInstance(const ModelInstance& inst) { ModelAsset* asset = inst.asset; tdogl::Program* shaders = asset->shaders; //bind the shaders shaders->use(); //set the shader uniforms shaders->setUniform("camera", gCamera.matrix()); shaders->setUniform("model", inst.transform); shaders->setUniform("tex", 0); //set to 0 because the texture will be bound to GL_TEXTURE0 //bind the texture glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, asset->texture->object()); //bind VAO and draw glBindVertexArray(asset->vao); glDrawArrays(asset->drawType, asset->drawStart, asset->drawCount); //unbind everything glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); shaders->stopUsing(); } The code comments explain the steps of the function fairly well. The code in this function is almost identical to the code inside That's pretty much it. We have restructured the code into assets and instances, which is much more flexible than using globals. To test out the flexibility, try adding some more instances. Or, if you're brave, try adding a new asset with a different texture, or different shaders. OptimisationThe code in this article is naïve, and unoptimised. It still runs fast enough, so it's not a problem at this stage. However, there are a few fairly simple optimisations that would make the rendering faster. The current implementation binds and unbinds the same shader five times. If the shader is the same for all five instances, then it only needs to be bound once. In pseudocode, a more-optimised render loop would look something like this: currentShader = NULL; foreach(instance in gInstances) { if(instance.asset.shader != currentShader){ if(currentShader) currentShader.stopUsing(); currentShader = instance.asset.shader; currentShader.use(); } RenderInstance(instance); } currentShader.stopUsing(); This way, if the shader doesn't change, then it doesn't get unbound. Binding and unbinding shaders is a relatively expensive operation, so eliminating unnecessary shader bindings will speed up the loop. To make this work with multiple different shaders, you would need to sort the The next most expensive operation is binding the texture for each instance. We can optimise texture binding in the exact same way as shader binding. Sort the instances by shader, then by texture. Inside the loop, check if the correct texture is already bound, and, if so, don't unbind and rebind it. Scene GraphsIn other engines/frameworks, you may find that instances are stored hierarchically – that is, they are stored in a tree structure instead of the It's possible to have both a list of instances and a scene graph. If you use the same design rationale explained in the Instances vs Entities section of this article, the solution would be for each scene graph node to have a pointer to an instance. Keeping a list of instances separate to the scene graph could be helpful when it comes to optimisation. We don't require a scene graph yet, so let's ignore them for now. OpenGL Instanced Rendering FunctionalityThere is an OpenGL extension called ARB_draw_instanced that provides functions for doing what was described in this article. It involves putting the model matrices into a VBO, and calling This functionality became available in OpenGL ES 3, which means it's not available on most mobile devices that exist right now. On the desktop, it is available in OpenGL 2.1 as an extension, and became part of the core profile in OpenGL 3.1. Future Article Sneak PeekIn the next article, we will begin to implement lighting, starting with diffuse reflection of a single point light. Additional Resources
|
|