Today I'd like to introduce a freshly implemented complex of renders that operate with bounding boxes.
Bounding box generation on GPU
Works for each mesh as follows:
- Whole mesh is sent to GPU as an array of vertex positions
- A geometry shader sends each position as a color to the first target fragment.
- The same shader sends an inverted (multiplied by -1) duplicate of the position (as a color) into the second target fragment.
- A target FBO has a 1D color RGBA32F render buffer, containing 2 pixels for each objects in the scene. It's bound with a viewport selecting only the 2 pixels corresponding to the current object.
- The blending function is set to Minimum, and the coefficients are 1,1.
- After all objects BBoxes are updated, the render buffer data is read into a buffer object.
As a result I have a buffer object containing minimum position and negative maximum position of a vertex (in local space) for each mesh. If we need them on CPU side, we can read them at any time.
Note that models can be morphed and skinned as you like and still will produce the correct bounding boxes! You just need to update them whenever vertex positions change.
Bounding box drawing
We definitely would want to see the result on the screen. Here is how it's done in KRI:
- We send the corresponding objects spatial data into another buffer object. In case of KRI it's 2 vectors (position+scale and rotation).
- We issue a draw call on the global mesh containing bounding boxes and spatial information of the objects.
- Geometry shader reconstructs all 8 points of a bounding box and transforms them into camera projection space.
- The same shader generates 12 lines based on the bounding box vertices.
- Rendering states: DepthTest:ON, DepthWrite:OFF, LineOffset:-1,-1
As a result, we have all bounding boxes drawn to the screen in a single draw call! It's extremely cheap, not to mention that the BBox information has never left GPU side, so no extra bus transfer costs present.
Constructing Z-buffer mip chain
Before culling anything, we need to set all mipmap levels for our Z buffer. In KRI it is done by writing a shader and calling a single helper function (kri.gen.Texture.createMipmap). The shader chooses a maximum depth within given 4 fragments and writes it into the next mip level as a depth value.
Culling the scene
Here is the most interesting guest of our party. Within a single draw call we create a buffer containing boolean values of visibility, one for every object in a scene:
- We render the same mesh we constructed for BBox drawing, binding the Z-buffer as a texture (containing our mip map)
- For each BBox the shader computes its camera projection space box (also 3D).
- Then it determines the minimal mip level containing 4 nearby texels covering the entire box.
- By comparing the maximum of these depth values with a close plane level of your screen space box we set the visibility output value as boolean.
- Transform feedback carefully gathers all visibility values and disables the rasterizer for this stage.
- Now we need to read them back into system memory in order to discard draw calls in the future.
That's it! Thanks to Transform Feedback and our intelligent BBox handing on GPU we can effectively cull the whole scene in a single draw call!
Hope it helps and inspires you!