Suppose that I have some world-space volume (which we may assume is a convex polyhedron with a small number of vertices), and I want to render its intersection with the view frustum, such that every relevant pixel gets rendered exactly once. My use-case is rendering light influence volumes for a deferred renderer, but I guess there may be other uses for this.

Everything is fine as long as the volume is completely behind the near clipping plane (i.e. isn't clipped by the near z-plane): rendering with back-face culling does the job, thanks to convexity of the volume. If, on the other hand, the volume intersects the near z-plane a bit, the intersection area gets clipped, but there are still parts of the volume behind this area that didn't get rendered. We could switch to front-face culling, but that breaks if the volume intersects the far z-plane. The volume could theoretically intersect both z-planes, so this problem cannot be solved simply by adjusting the culling mode. The volume could as well completely embody the view frustum, in which case nothing will be draw at all (all triangles outside the frustum), while in reality I'd like it to be rendered as if I'm rendering a fullscreen quad (since the intersection of the volume and the frustum equals the frustum).

Right now my solution is to check if any of the volume's vertices are in front of the near z-plane (i.e. are clipped by the near z-plane) and resort to a fullscreen quad in this case. It works fine, but I'm curious whether there is a better solution.

One solution that I don't particularly like is actually computing the mesh of the intersection of volume and the view frustum (both are convex polyhedrons, which should simplify the job a bit). It has the following disadvantages:

  • I have to either find a good enough library for mesh intersection, which means adding another dependency for the engine, or write the intersection code myself
  • More importantly, even if the intersection is computed with maximal possible precision, it still goes through the vertex shader, gets multiplied by the view-projection matrix, with numerical errors inevitably introduced. Then, a vertex at z=near may appear at z=near+/-error, and it might still be erroneously clipped.
  • $\begingroup$ "the volume is completely behind the near clipping plane" If the volume is behind the near clipping plane, then it's not visible. Maybe you meant "in front of". $\endgroup$ – Nicol Bolas Oct 27 '20 at 18:16
  • $\begingroup$ @NicolBolas Well, it depends on the definitions. To me it seems reasonable to define "in front of" to mean "closer to the camera", which is what I did mean. I'll update the question to prevent misunderstanding. $\endgroup$ – lisyarus Oct 27 '20 at 18:28

First, regarding the specific problem of bits of the geometry being lost to the near and far planes, this can be solved by using depth clamping instead of depth clipping. This disables clipping against the near and far planes, in favor of clamping the output depth values to [0, 1]. (Geometry behind the camera will still be clipped though.) Given this, instead of a fullscreen quad you could render the light volume with inverted culling, depth testing inverted (to cull pixels behind the volume), and use depth clamping to prevent losing parts of the volume to the far clip plane.

(Depth clamping is also handy for shadow maps, as it lets you contract the shadow map depth bounds to bracket the view frustum, without losing geometry between the view frustum and the light source.)

In a broader sense, though, this doesn't really solve the problem of shading pixels within a given convex volume. The problem is that you need two depth tests: front of volume < pixel depth < back of volume. The approaches discussed so far can only do one of these tests at a time. You can switch which one you're doing by swapping culling and depth-testing modes, but you can't do both at once, which leaves performance on the table.

One approach to address this is to use the stencil buffer in concert with depth testing. Basically this works by testing scene depth against the front of the light volume first, storing that to the stencil buffer, then doing a second pass that tests scene depth against the back of the light volume, and combining that with the stencil result from the first pass. This has the advantage of getting tight culling (no pixels shaded outside the volume), but you need to draw each light volume twice, plus making a lot of state changes to switch back and forth, which can be inefficient for both CPU and GPU (though you can batch up to 8 lights together using 1 stencil bit per light).

Finally, using EXT_depth_bounds_test, if available, can be a good solution. This enables a form of depth test that checks whether the pixel already in the framebuffer (not the geometry being rasterized) is within a certain range. With this, you set the depth bounds to the min/max depth of your light volume, then draw the geometry with inverted culling, no depth test, and depth clamping enabled. This will shade all pixels within a sort of prism aligned with the view frustum, that bounds the actual volume reasonably tightly in most cases (though long narrow spotlights can be bad), and it does it in only one pass. However, this extension isn't universally available; it seems to be found on current NVIDIA and AMD GPUs, but not Intel ones (see OpenGL Hardware Database).

All of this difficulty is one reason that most game engines since the early 2010s have switched to compute-shader tiled culling instead of using the rasterizer to decide which lights to shade at which pixel.

  • $\begingroup$ Thanks a lot for such an elaborate, Nathan! $\endgroup$ – lisyarus Nov 10 '20 at 9:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.