In this blog, we will create binary files for our Effects, similar to how we have done for meshes in the previous blog. Effects store different data from meshes. We will discuss the data stored by Effects and how we store it in binary.
Before we create the binary file for our Effect, we have to first create the human-readable source file that will be used to generate the binary file. So what data do we require to create an Effect? This can be found by looking at the InitializeEffect method, we need a render state which represents all the possible render states for the Effect and two paths one for vertex and one for fragment shaders. Remember the goal of the human-readable file is to give the representation of an Object(ex: Mesh, Effect, etc) in a readable & understandable way so that developers can read and understand what that object is intended for and debug issues. It should also be maintainable(i.e It should be flexible for future changes).
So keeping all these things in mind I created the human-readable file for my Effect as following.
{
RenderStates =
{
AlphaTransparency = "Disable",
DepthTesting = "Enable",
DepthWriting = "Enable",
DrawBothTriangleSides = "Disable"
},
VertexShader ="Shaders/Vertex/standard.shader",
FragmentShader ="Shaders/Fragment/standard.shader",
}
I am using Lua to represent the Effect file other formats can also be used. The root table contains Renderstates table, Vertex & Fragment Shader paths. I used a key table for render states instead of an array table because it is easily understandable and allows me to set multiple possible states for each state. For example, if there are more options than just enable and disable it is hard to represent them in an array table. For shaders we need a path again I went with a key-value approach because it makes things simple. Having these keys helps a developer in determining which path is for what shader if the path itself is not obvious. For example what if the path for vertex shader is "Shader/Assets/standard.shader" looking at the path it is not obvious what shader it is so having a key makes it more readable and helps when debugging.
Now Let's discuss the binary file that will be generated using the above source file. We have already discussed the advantages of binary files over human-readable files in the previous blog now let's discuss how we generate the binary file for Effect. I have created an Effect builder that converts the above source file to the following binary file.
So we need three things to initialize an Effect a RenderState and paths for each shader. There are many ways you could set up the layout of your binary effect file based on your priorities. In my file, the first thing I store is the render state, all the possible render states for an effect can be represented in a single byte so I am storing that first. The next data is the path for vertex and fragment shaders. But notice that there is no additional information about the path's size, unlike we had for meshes. So how do we traverse the memory and obtain the correct paths? There is no additional information about the size of the path, but there is an additional byte at the end of each path, which is a null termination. When we load this binary file into memory, we extract the path as c-strings or const char*(if it is easy to understand), which means if we have a null termination character at the end of each path, the standard c-string treats it as the end of the string. So we don't need to add any other information about the size of each path.
There is also one more difference in the shader-paths in the binary file compared to the human-readable file there is an additional "data/" that is added to the paths from the human-readable file. This is because the game install path has an additional data folder compared to the relative paths used in my asset build system so I added the "data/" in my Effect builder. Again there are different ways this can be done, For example, instead of adding the "data/" & the null termination character in my Effect builder during the build time, I can add them during the runtime both ways have their advantages and disadvantages so let's discuss them and understand why I choose the first way.
If we add "data/" & null termination character during runtime then there is no need of storing them in the binary file. This reduces the size of the binary file and possibly faster to save and load the file to disk if your platform has difficulties in writing/reading big files. But the disadvantage to this approach is you have to do more work during runtime not only that but you have to allocate new memory if you plan on adding anything to the existing binary data. This is because when you load the binary file a certain amount of memory is allocated for that binary file so if you need to add more data(for example adding a null character) you just can't go to the end of the allocated memory for the binary file and add a null termination character because you will be writing in memory that is not allocated for you and It might cause issues in your application. So the right way to do is you would calculate how much additional memory you would require and allocate new memory, then copy the binary file data to your new memory and make new additions. So you can see that there is a lot of work to do during the runtime which is why I did not choose this method as we want our run time to be as fast as possible. Build times should also be considered but we can have less efficient code or can do more work at build time especially if it reduces the work you need to do during the run time.
In the first method by adding the "data/" & null character during the build time we increase the file size but we have very little work to do during runtime. We don't need to allocate new memory as everything we need is already there and no additions to be done. This saves a lot of work during the runtime but increases the file size by a few bytes.
The binary Effect file can then be extracted simply. First I extract the Renderstate as I know that is the first thing in my layout this is the reason I had it first in the layout so that I need not do any computations to find it. Then I traverse to the first path which is a vertex shader by adding one to the memory address of RenderStateBits. I am using const char* to represent the paths so the VertexShaderPath ends at the null character we had in the binary file. For the second path, we need to know the length of the first path but it is easy to determine as we are using c-strings we can use string methods like strlen which returns the size of the c-string. Add the length plus one(strlen does not count the null character so we need to add that) to the memory address of the VertexShaderPath to traverse to the second path which is the fragment shader as we have the null character it ends at the null character, that's it. Now we have extracted all the details required to initialize the Effect. We can now pass the extracted details to our InitializeEffect method to create the Effect as before.
Controls:
ESC – exit.
Up/Down/Left/Right - Move Santa
W/A/S/D/Q/E - Move Camera Forward/Left/Backward/Right/Down/Up
ENTER - Changes the mesh to square and effect to the animating color of the object.
Space - Toggle between main and secondary camera. Note: the secondary camera does not support the movement.
Comments