Material system rewrite
With the game projects done, we’ve gotten some feedback related to the game creation process with Nebula. So we have a gigantic list of stuff we should implement/fix/change for the next iteration.
One of these things (one of the biggest) is making a new material system. Currently, a material is defined in an XML which explains where during a frame a material is used. Materials are then put on models, and the material explains when during the frame a model should be rendered, and with which shader. So basically, you have this relation:
- Material
- Pass 1
- BatchGroup ‘FlatGeometryLit’ with shader ‘static’ and variation ‘Static’
- Pass 2
- BatchGroup ‘GlobalShadows’ with shader ‘shadow’ and variation ‘Static|CSM’
- Pass 1
- Model
- Model node
- Material
- Variables
- FrameShader
- FrameBatch, which denotes a BatchGroup
- For all materials belonging to BatchGroup
- Apply material
- For all visible model nodes using material
- Apply mesh
- For all visible model node instances using material
- Apply node state
- Draw
- For all materials belonging to BatchGroup
- FrameBatch, which denotes a BatchGroup
- State of a model is saved within each model node.
- Material is saved on a resource level for each model node, so it can not be switched out.
- Material variables has to be set on each model node, and cannot be reused.
- SurfaceMaterial
- Material
- Variables
- Model
- Model node
- Surface material name
- Model node instances
- Pointer to surface material
- Model node
So a material describes when an object should be rendered, and how, a model uses a material to understand when it should be rendered, and the frame batch tells when a certain batch group should be executed.
While this method works, it’s a bit inflexible. The main downsides are:
So the new system is different, instead of saving the shading state (material settings) within a model, they are instead saved in a separate resource, which at the time is called SurfaceMaterial. A SurfaceMaterial is created as a resource in the content browser, and it contains the values for each material variable. It uses a currently existing material as a template, since the surface material doesn’t denote when it should be rendered. This makes it possible to create new surfaces by taking a template of a material, and then save material variables (textures, intensities etc) in a separate resource. When making new assets later on, it will be possible to use the same surface on several models, which is nice because it makes it faster for graphics artists to assign textures, alpha clip thresholds and texture multipliers since they only need to assign an already created material.
Furthermore, since the state of a model is now detached from the model resource, it also allows us to change the material during run-time. This means we now (finally) have the ability to switch the material on objects in real-time, something which is extremely useful when for example hiding opaque objects to close to the camera, procedural dissolves, fades, etc.
In theory, it would also be possible to further improve performance by sorting each model node instance based on surface, which results in a surface only being applied once per frame, however it is difficult to filter out instance unique surfaces (for example if we have one object with a unique alpha blend factor). This might not be necessary (or even noticeable), since the shader subsystem will only actually apply a shader variable if it differs from what it currently is set, so setting the same surface multiple times result in close to no overhead.
The original render system used a clever bucket-system, where model node instances would be put into buckets depending on shader. When I made the first iteration of the material system, I made it so that this bucket-system used materials to group (sort) objects, so that material switches would be as few as possible. However this system relied on the fact that materials are defined in the model resource. It was easy to switch this to a system where each model node instance would decide which bucket it would be put in.
The biggest change is to convert all .attributes into surface XMLs, then remove those who are duplicates, then replace the field in each state node which corresponds to its material name to instead be the name of a generated surface. Perhaps it is just easiest to go through the projects, collect their values, make new materials and assign them manually. Then we also need to create tools to make materials with. This should be fairly straight-forward, seeing as we can change the state of a material by setting a value, and swap materials on models using the new system, so we should be able to visualize it easily enough.
So instead of the above hierarchy, we have something like this:
- Model node