This blog series is a part of the write-up assignments of my Real-Time Game Rendering class in the Master of Entertainment Arts & Engineering program at University of Utah. The series will focus on C++, Direct3D 11 API and HLSL.
In this post, I will talk about my new material file, which has a human-readable version of Lua format and a binary version that is used in the engine.
So far, I am only having two fields in my material format. One for the effect (shaders) file that it is using, and one for the base color. More fields such as metallic, roughness, specular, emissive might be added in the future.
To convert this Lua file into a binary format that my engine can use, I needed to create another material builder project that reads in the Lua file, checks all the fields and converts them accordingly, and then writes them out as a binary stream. What is different between the builder and other builders (mesh, shader, etc.) in the past is that material should allow the constants (except for effect) to be optional! The reason for this is to provide a larger degree of freedom and flexibility throughout the development process. If the developers decided to introduce a new constant into the material format late in the development process, and it’s not optional. Someone will need to go over all the previous material files and fix them accordingly, or at least writes a program for that.
Now let’s take a look at the building process. Since I am using a struct sBaseColor to store the base color, it is trivial to just get the memory of that struct, and write out the length of sizeof(baseColor).
You can see that since the data is floating points, that makes the binary file really hard to read. However, if we input the hex number 0x3f800000 into a hex-to-float converter, it will return us 1.0f. At the same time, we don’t really care nor need to understand this file. My material class can simply read it in and interpret as a base color struct.
I mentioned that the constants should have default values if not specified. But what values should they be defaulted to? Well, take color for example. It would only make sense if the default color is white (1.0, 1.0, 1.0, 1.0) since we are multiplying the fragment color by the material color, and we would not want to change the outcome if the material did not specify any color. And any color * (1.0, 1.0, 1.0, 1.0) will still be the same.
The constant buffer for the material is interesting since its lifetime is not per-frame nor per-drawcall, so I needed to create a new constant buffer type for it.
After adding the new constant buffer type and the codes for initialization properly, we can see the materials taken effect in action. In below, I am showing different materials all using the standard shader, which is directly using the vertex color without modifying.
Note that the default white material means that the color field did not get specified in the material file, so the builder put in white for it by default. From the binary file’s perspective, it has no idea whether a field was specified in the Lua file or not, it simply reads all the values into its corresponding fields.
If you look at where I load all the materials in my codes, you’ll see that I’m loading in the order of blue, then default, then red, and then green. Now let’s see if my GPU rendering order reflects that.
Obviously, we successfully take the materials into account for our rendering order. Starting now, the graphics system will group the same materials together so that the constant buffer does not need to be reset every time. We can see the detail of the GPU timeline below.
We can see that obj:5 only gets set again after the first four draw calls. Looking at the object table tells us that obj:5 has a size of 16 bytes, which matches our material constant buffer (4 floats)! This shows that our material is only being set when necessary.
Modifying Material In Game
More than often we will run into situations that we need to modify some material to reflect the gameplay scenario (such as enemies being damaged). Let’s see how we can implement this.
Changing the material base color with a keypress is trivial, we just need to retrieve the material pointer from the handle that we have, and then modify the value. However, this will affect all the meshes that are using this same material. Also, there is another issue. Remember that our engine is running on two different threads now, and using two buffers for application and rendering separately. If I directly modify the material value during gameplay (frame N), it will also change the data (frame N – 1) inside the rendering buffer. The easiest approach would be to also store the material constants at that frame inside our buffers, which will take more memory. What I decided to do is to leave it, for now, since it will probably never get noticed, especially in the application that I currently have.
However, we still need to address the issue of all materials changing together. To fix this, we need to implement a duplicated function in our asset manager, that will allow it to return a new handle with the same underlying material, which copied all the data from reference material, that we can change without having to worry about modifying the original one.
Some problems that might happen here is reference counting. We need to make sure that we decrement. Since the copying of data does not go through the asset manager, we need to manually call the unsafe increment function to increment the underlying asset record. The result is shown below. All the materials are from the same default material, but I made a duplicated version of it and assigned it to some of the objects, and then modify the color value at run-time.