Previously, we saw how the new renderer can combine multiple cameras and even multiple worlds in a single render to combine 3D and 2D graphics. During the process of implementing Z-sorting for multiple layers of transparency, I found that Vulkan does in fact respect rasterization order. That is, objects are in fact drawn in the same order you provide draw calls to a command buffer.
Within a subpass of a render pass instance, for a given (x,y,layer,sample) sample location, the following operations are guaranteed to execute in rasterization order, for each separate primitive that includes that sample location:
- Scissor test
- Sample mask generation
- Depth bounds test
- Stencil test, stencil op and stencil write
- Depth test and depth write
- Sample counting for occlusion queries
- coverage reduction
- Blending, logic operations, and color writes
Each of these operations is atomically executed for each primitive and sample location.
Execution of these operations for each primitive in a subpass occurs in primitive order.
Furthermore, individual primitives (polygons) are also rendered in the order they are stored in the indice buffer:
Primitives generated by drawing commands progress through the stages of the graphics pipeline in primitive order. Primitive order is initially determined in the following way:
- Submission order determines the initial ordering
- For indirect draw commands, the order in which accessed instances of the VkDrawIndirectCommand are stored in buffer, from lower indirect buffer addresses to higher addresses.
- If a draw command includes multiple instances, the order in which instances are executed, from lower numbered instances to higher.
- The order in which primitives are specified by a draw command:
- For non-indexed draws, from vertices with a lower numbered vertexIndex to a higher numbered vertexIndex.
- For indexed draws, vertices sourced from a lower index buffer addresses to higher addresses.
Within this order implementations further sort primitives:
- If tessellation shading is active, by an implementation-dependent order of new primitives generated by tessellation.
- If geometry shading is active, by the order new primitives are generated by geometry shading.
- If the polygon mode is not VK_POLYGON_MODE_FILL, by an implementation-dependent ordering of the new primitives generated within the original primitive.
Primitive order is later used to define rasterization order, which determines the order in which fragments output results to a framebuffer.
Now if you were making a 2D game with 1000 zombie sprites onscreen you would undoubtedly want to use 3D-in-2D rendering with an orthographic camera. Batching and depth discard would give you much faster performance when the number of objects goes up. However, the 2D aspect of most games is relatively simple, with only a dozen or so 2D sprites making up the user interface. Given that 2D graphics are not normally going to be much of a bottleneck, and that the biggest performance savings we have achieved was in making text a static object, I decided to rework the 2D rendering system into something that was a little simpler to use.
Sprites are no longer a 3D entity, but are a new type of pure 2D object. They act in a similar way as entities with position, rotation, and scale commands, but they only use 2D coordinates:
//Create a sprite auto sprite = CreateSprite(world,100,100); //Make blue sprite->SetColor(0,0,1); //Position in upper-left corner of screen sprite->SetPosition(10,10)
Sprites have a handle you can set. By default this is in the upper-left corner of the sprite, but you can change it to recenter them. Sprites can also be rotated around the Z axis:
//Center the handle sprite->SetHandle(0.5,0.5); //Rotation around center sprite->SetRotation(45);
SVG vector images are great for 2D drawing and GUIs because they can scale for different display resolutions. We support these as well, with an optional scale value the image can be rasterized at.
auto sprite = LoadSprite(world, "tiger.svg", 0, 2.0);
Text is now just another type of sprite:
auto text = CreateSprite(world, font, L"Hello, how are you today?\nI am fine.", 72, TEXT_LEFT);
These sprites are all displayed within the same world as the 3D rendering, so unlike what I previously wrote about...
- You do not have to create extra cameras or worlds just to draw 2D graphics. (If you are doing something advanced then the multi-camera method I previously described is a good option, but you have to have very demanding needs for it to make a difference.)
- Regular old screen coordinates you are used to will be used (coordinate [0,0] is top-left).
By default sprites will be drawn in the order they are created. However, I definitely see a need for additional control here and I am open to ideas. Should there be a sprite order value, a MoveToFront() method, or a system of different layers? I'm not sure yet.
I'm also not sure how per-camera sprites will be controlled. At this time sprites are stored in a per-world list, but we will want some 2D elements to only appear on some cameras. I am not sure yet how this will be controlled.
I am going to try to get an update out soon with these features so you can try them out yourself.