I’m spending Christmas at home with my family. However, despite all the holiday bustle there’s still some time without anything to do, so I found some time to continue working on Duality. Rendering and content stuff; specifically: A simple Material system.
You may have noticed the complete lack of texturing in all previous screenshots. Textures simply weren’t implemented yet, so I was stuck with colored shapes. I postponed them until now because they’re tightly connected to some kind of Material system which I hadn’t figured out yet. So there is this DrawDevice abstraction layer: A DrawDevice collects vertex data associated with drawing parameters from Renderer Components (or any other instance that wants to render something). It optimizes it and wraps them down to the least possible number of DrawBatches. While some batches may share a single VBO, each DrawBatch generally means a state change of some kind: It represents vertex data grouped by the way it will be processed / displayed. You might also say: Grouped by how it looks like.
“How it looks” is defined by a Material. It consists of a set of textures, a main color and a DrawTechnique. If you look at Materials as an entities concrete “look”, a DrawTechnique would be the abstract version of it. While a Material might be something like “Wood”, “Grass Tile”, “Water” or “Spaceship Type C”, DrawTechniques could be described as “Solid”, “Partially translucent” or “Dynamically lit”. They combine information about blending and shaders. Each “look abstraction layer” exposes a SetupForRendering method that is used by the next upper layer. The DrawDevice doesn’t need to know how to set up different Materials for rendering, it just needs to be able to compare them. DrawBatches are basically sorted by four traits, from top to least priority:
- Vertex data type / format
- The Materials texture set
- Vertex mode (Quads, Triangles, …)
DrawBatches that are completely equal in all four traits are joined to a single DrawBatch, even just-in-time when calling AddVertices where possible. When two or more DrawBatches share only the same vertex data type, they will also share a single VBO, upload data once and perform multiple drawcalls on it. All other trait differences are handled using a “lazy switch” approach: State changes are only triggered if the required state differs from the one that has been active before. Rendering multiple DrawBatches that use the same set of textures won’t trigger a texture state change.
However, this kind of optimization only works with DrawTechniques that do not require Z sorting, i.e. Solid, Masked or Additive blending. Alpha blending for example can’t rely purely on the video cards z buffer so we can’t just sort DrawBatches freely as needed for optimizing. The DrawDevice is bound primarily to their Z values and secondarily to their to their drawing order. In most cases, there will be a lot of DrawBatches the DrawDevice can’t optimize further. Alpha blending is evil – but unfortunately the most frequently used kind of blending in 2D games.
Sprites generally have translucent regions but most of them only differ between “fully opaque” and “fully transparent”. Instead of Alpha blending, we could use Mask blending i.e, Alpha testing and avoid the necessity of Z sorting. The downside are ugly, sharp, unantialiazed pixel edges at the sprites opaque areas borders where a filtered alpha value would normally smooth them. Not good. Fortunately, there is multisampling / fullscreen antialiazing to save the day: While not working on transparent textures normally, enabling “Sample Alpha to Coverage” mode instead of Alpha testing solves the problem. A Sprites half opaque inner-texture edges are properly antialiazed as if they were antialiazed polygon edges.