C++ OpenGL Engine Development

OPENGL ENGINE DEVELOPMENT

I've recently decided to start development of my own C++ OpenGL Game Engine. I spent time studying programming over the quarantine with the intention of making games, so moving to understand the fundamentals of 3D graphics (and making something to work in) is the next logical step.

Why not use unity?

I've used unity!

But I find that when it comes to creating something, I need to understand how the tool works before I can use it to its fullest. There are a lot of logical jumps that have to be taken when learning to use pre-packaged engines- things you are told to take for granted and just accept as "the way things are." This, for me atleast, always leads to problems down the line, and limits the way you can use the software.

So, to get a proper grasp on why these engines work the way they do, I went to go about learning to build one!

How do you learn to build a game engine?

Tutorials! (and books)

Fortunately, nowadays there's an exceptional amount of tutorials on just about everything. By following a few (eventually jumping off to develop my own framework) I've managed to build a little system I'm quite proud of. It's still a work in progress, and definitely needs some more time, but has served its purpose in helping get a deeper grasp on a the behind-the-scenes of computer generated graphics, and therefore games.

Victor Gordan's tutorial series is amazing, giving a real quick overview of the entire process

Game Engine Architecture is really informative (though I haven't finished it yet)

Rendering to a screen is a complicated and intensive task that is made so much easier by the libraries that exist out their to handle communication with the GPU, help with the maths, and generally make things work. I've obviously made extensive use of both OpenGL and GLM, as well as stb for image handling. Open source libraries give me faith in humanity.

RENDERING IN 3D

I want to render the Obama Cube. Don't ask me why, I just do.

Getting something to render on a screen is hard. The Rendering Pipeline is something I had roughly seen before, but never really seen indepth. I ended up going through several tutorial pages trying to get a better grasp on whats going on to get the GPU to draw to a window- though to be honest, it's not completely necessary for the current step- only really coming to play once you get to shaders and optimising code.

OpenGL is written as one big object- called the OpenGL context. Its a big object with different bind slots for the necessary steps, like for the window thats being used, or the render flags that have been set. Having it as one, large, hidden object helps keep code clean, and means that you, the programmer, don't have to fully understand and call each step, largely what the code does to manage memory. While I am after a complete understanding, I'm not looking to start with an atomised view.

To render a triangle, you need to set out some points in 3d space, and tell openGL to draw them. This requires implementing a very basic Rendering Pipeline- which itself requires an understanding ofVertex Buffers, Vertex Indexing, Vertex Array Objects, and Shaders.

With graphics programming the main concern is speed. Given how fast at processing most CPUs and GPUs are nowadays, the main cost to consider is moving data between the two. This has lead to a number of things that make OpenGL seem overly complicated. It ends up being much more efficient to send over all the data the GPU will need in one lump- sorted for contiguous processing. This means, for every vertex, you need to store its position, its texture data, its normals, etc, in one big buffer object, all vertices aligning next to each other in memory (in an array).

But, alone, that buffer is just garbage data - you need to provide both interpretation and indexing data. Indexing is essentially just telling what vertices to draw, when. The interpretation data is stored in theVertex Array Object (VAO), and describes to the GPU how the information is stored, through their location in relation to the other vertex component data types and the overall vertex size.

But how does the GPU know what to do with this data? Thats where Shaders come in! OpenGL has its own language for using the GPU- called GLSL, the OpenGL Shading Language. This is the beginning of the Rendering Pipeline. Firstly, we create a Vertex Shader. There, we have to use the interpretation information to sort the data- each location is given its own variable name for use within the main() shader function. The vertex shader is responsible for the creation of polygons within given vertex data- but importantly, not how they render to the screen.

Each shape made in the Primitive Assembly Phase (the step after the Vertex Shader) must be Rasterised to the screen before the computer can display them- every polygon that can be seen by the viewport is broken down into fragments (essentially pixels, with caveats) for processing by the fragment shader. Using the shader we can run a code per fragment on the GPU concurrently, calculating the final colour value using the data passed in by the vertex shader (and by extension the VBO.)

With this all set, we can start rendering! To begin, I made a basic triangle- 3 vertices, with colour data, following basic tutorials. I moved on to importing a texture- streaming in a .png as arbitrary data - and got one step closer to my obama cube.

To get the plane rotating, we need to use uniforms, which are another way of getting data onto the GPU. We can pass in the current time as a float, and use a sine wave to have it constantly loop values - making it rotate smoothly!

The final step towards making the Obama Cube is creating a Perspective Projection Matrix - this bit is really complicated! It involves applying a transformation on all of the data before we render it. This transformation turns the original "viewing volume", the 3d space in which objects are rendered onto the screen (by default a cube), into a frustum, which is a truncated cube.
Normally, OpenGL renders orthographically - essentially it casts a line from an object to the "viewing window". By default, they are all perpendicular with the the viewport, which means when they are projected, everything looks flat and the same size, regardless of distance.
However, we want to make it so that all of these "lines" are no longer parallel, and instead all focus onto a point just behind the viewport. This creates the desired illusion of perspective, as the closer you are, the more space you occupy on the screen, once projected onto this 2d viewing plane. We do this with a compound matrix transformation. This part is very mathmatical, and I won't explain it properly, so here's a good explanation on it.

Now... together, we can finally build the Obama Cube.

Getting to this point felt like a real success. I've never done anything graphic with programming, other than in prefab engines, so this felt very special.

Orthographic Spinning Obama PLANE, not quite there.

LONG LIVE THE OBAMA CUBE

OBJECT IMPORTING

Getting object importing was an important step in making my game engine actually usable and interesting. I found some resources on getting it working, and people seemed convinced that importing as .dae files will be the most useful.

Collada .dae files are essentially .fbx files, formatted in XML for ease of use. The main challenge here, therefore, was writing an XML parser, and sorting out the usful data from the noise.

I had less experience with XML than I'd have liked going in, so I ended up having a friend help me work through the process of interpretting the .dae.

We used "fstreams" to import the data as a string, and two "stringbuffers" to sort it. We created both an "XMLObj" struct, and a recursive "readXML" function to help fill them. Within the XMLObj is the tagname, the data, and a list of its child objects. Due to the fact XML is a markup language, you end up with nested data - each of the "elements" ends up being stored as their own XMLObj, with its internal "elements" being stored as their own XMLObj children.
To achieve this, we had the readXML function operate recursively- searching through the text as a string until it found its corresponding closing tag. When it did, it dumped its "contents," the stuff between the tags into a new readXML function call.

This worked! ...to an extent. There was an issue- I, at the time, didn't realise that .dae files stored all of its indexing data for each "pass" seperately- the texture co-ordinates have their own indices, which are stored contiguously alongside the position indices! I ended up needing to write a seperate Vertex Constructor function, that makes and re-organises the index data alongside fully formed vertices.

I still need to make it more robust- as .dae files can throw all sorts at you, but for now, I have basic object loading!

OBJECT HANDLING




Writing is hard, give me a bit



BETTER IMPORTING








there's lots to write about i swear