Tutorial 15 - Deferred Rendering.pdf

(2689 KB) Pobierz
Tutorial15:DeferredRendering
Summary
In this tutorial series, you've seen how to create real time lighting eects, and maybe even had a go at
creating a few pointlights in a single scene. But how about 10s of lights? Or even 100s? Traditional
lighting techniques using shaders are not well suited to handling mass numbers of lights in a single
scene, and certainly cannot perform them in a single pass, due to register limitations. This tutorial
will show how a large number number of lights can light a scene at once, all of them using the complex
lighting calculations you have been introduced, via a modern rendering technique known as deferred
rendering.
New Concepts
Deferred rendering, light volumes, G-Buers, world space recalculation
Deferred Rendering
As the number of objects and lights in a scene increases, the problems with performing lighting
using a fragment shader similar to the one presented earlier in this tutorial series becomes apparent.
As every vertex runs through the pipeline and gets turned into fragments, each fragment gets ran
through the lighting equations. But depending on how the scene is rendered, this fragment might not
actually be seen on screen, making the calculations performed on it a waste of processing. That's
not too bad if there's only one light, but as more and more are added to the scene, this waste of
processing time builds up. Objects not within reach of the light may still end up with fragments ran
through the lighting shader, wasting yet more time. Ideally, lighting calculations would be performed
in image space - i.e only ran on those fragments that have ended up as pixels in the nal back buer.
Fortunately, there is a way to make real-time lighting a form of post-processing eect, ran in image
space - deferred rendering. Deferred rendering allows many lights to be drawn on screen at once, with
the expensive fragment processing only performed on those pixels that are denitely within reach of
the light. Like the blur post-processing you saw earlier, deferred rendering is a multipass rendering
algorithm, only this time, instead of two passes, three passes are performed - one to draw the world,
one to light it, and one to present the results on screen. To pass data between the separate render
passes, a large number of texture buers are used, making use of the ability of a Frame Buer Object
to have multiple colour attachments.
1
979347012.003.png
G-Buers
Deferred rendering works by splitting up the data needed to perform lighting calculations into a num-
ber of screen sized buers, which together are known as the G-Buer. You should be pretty used to
multiple buers by now - classical rasterisation uses at least one colour buer, a depth buer, and
maybe a stencil buer. What deferred rendering does is extend this to include a buer for everything
we need to compute our real-time lighting. So, as well as rendering our colour buer and depth buer,
we might have a buer containing TBN matrix transformed normals, too. If the vertices to be ren-
dered have a specular power, either as a vertex attribute or generated from a gloss-map, we can write
that out to a buer, too. By using the Frame Buer Object capability of modern graphics hardware,
all of these buers can be created in a single rendering pass. Just like how we can load in normals
from a bump map, we can write them to a frame buer attachment texture - textures are just data!
Normally, each of these buers will be 32bits wide - the internals of graphics cards are optimised
to handle multiples of 32 bits. This raises some important issues. The normals we've been loading in
from bump maps are 24 bits - we store the x,y, and z axis as an unsigned byte. It's therefore quite
intuitive to think about writing them out to the G-Buer as 24 bits, too. But this leaves us with
8 bits 'spare' in our G-Buer, as the alpha channel would not be used. What to do with these 8
bits of extra data is entirely up to the exact implementation of deferred rendering that is to be used.
One popular solution is to put some per-pixel data in it , such as how much specularity to apply, or
maybe some ags to determine whether lighting should aect the model, or if it should be blurred in a
post-process pass. Sometimes, it might be decided to store the normals in a higher precision instead,
and maybe have 12-bits for the x and y axis, with the z axis reconstructed in a fragment shader.
Whatever per-pixel information required to correctly render the scene is placed into this G-Buer, in
a single rendering pass.
Example G-Buer output for a cityscape rendered using deferred rendering. Clockwise from top left:
Diuse colours, World space normals, depth, and material information such as fullbright and
shadowed areas
2
979347012.004.png
Light Volumes
Unlike the 'traditional' lighting techniques, where lighting is calculated in the same rendering pass as
the vertices are transformed in, deferred rendering does it in a second pass, once the G-Buer has
been lled (i.e. lighting has been deferred until after the geometry has been rendered). In this pass,
we render light volumes, to the screen, using the same view and projection matrices as the rst render
pass - so we are drawing lights in relation to our camera.
But what actually is a light volume? If you cast your mind back to the real time lighting tutori-
als, you were introduced to the concept of light attenuation, where each light had a position, and a
radius. That means that our light was essentially a sphere of light, illuminating everything within it.
That's what a light volume is, a shape that encapsulates the area we want the light to illuminate -
except in deferred rendering, we actually draw a mesh to the screen, at the position where our light
is. This mesh could simply be a sphere for a pointlight, or a cone for a spotlight - any shape we can
calculate the attenuation function for can be used for a light volume.
Light volume A encapsulates nothing, while light volume b encapsulates part of the tree - lighting
calculations will only be performed on the fragments of tree inside the light volume
However, remember that in 'view space' (i.e in relation to the camera), a light volume could appear
to encapsulate an object, but in actuality the object is too far away from the light to be within its
volume. So there is a chance that a light volume could 'capture' an object not inside it, which must
be checked for in the second render pass. Consider the following example of a light volume appearing
to contain an object not within its radius:
The cow on the left is small, and is fully enclosed within the light volume. The cow on the right is
far away, so although it looks like it is within the light volume from the camera's perspective, it is
actually outside of the light volume's radius
Deferred Lighting Considerations
So, how does splitting our rendering up into two stages help us render lots of lights? By using light
volumes, and only performing lighting calculations within that volume, we can drastically reduce the
number of fragment lighting calculations that must be performed.
Think about the traditional process: Imagine we are using our bump mapping shaders, expanded
to contain an array of 16 lights, spread throughout the game world. So for every fragment written to
the back buer, we must perform 16 sets of lighting calculations.
3
979347012.005.png 979347012.006.png
The vast majority of the time, any particular fragment will only be lit by 1 or 2 lights, so that
in itself causes a lot of wasted processing - even if we used the discard statement to 'early out' of
the lighting process, we've still waste processing, and fragment shaders aren't particularly strong at
using loops to start with. It gets worse! Imagine we have a really complex scene, with lots of objects
overlapping each other. That means lots of fragments will be overwritten, so the time spent on calcu-
lating lighting for them is eectively wasted. Even if we use the early-z functionality by drawing in
front-to-back order, and use face culling to remove back facing triangles, we still might get fragments
that are written to many times. If we want to draw more lights than we can calculate in a single
fragment shader, we must perform multiple rendering passes, drawing everything in the world multiple
times!
Deferred rendering solves all of these problems. When it comes to performing the lighting calcu-
lations in the second pass, we already know exactly which fragments have made their way to the
G-Buer, so we get no wasted lighting calculations - every G-Buer texel that is 'inside' a light vol-
ume will be seen in the nal image. By using additive blending of output fragments in the second
render pass, G-Buer texels that are inside mulitple light volumes get multiple lighting calculation
results added together. Finally, by only running the expensive lighting fragment shader on fragments
that are inside light volumes, we save time by not running any calculations on fragments that will
never be lit. By using deferred rendering, we go from maybe 8 or so lights on screen at a time before
rendering slows down, to being able to quickly process 1000s of small lights, and maybe 100s of larger
lights.
As with seemingly everything in graphical rendering, deferred rendering does have its downsides,
too. The most obvious problem is space - the G-Buer consists of multiple screen-sized textures, all of
which must in be in graphics memory. It is easy to end up using 64Mb of graphics memory just for the
G-Buer - no problem on a PC thesedays, but tricky on a console with only 256Mb of graphics memory!
The second problem is related to the rst - bandwidth. All of these G-Buer textures must be sampled
at least once per lit fragment, which can add up to a lot of data transfer every frame in cases where
lots of lights overlap each other. This increase in bandwidth is oset to some degree by being able
to do everything in a single lighting pass, and by guaranteeing that if data is transferred, then it is
because the data almost certainly will be used.
Thirdly, deferred rendering doesn't help us with the problem of transparent objects. If we write
transparent objects to the G-Buer, what is behind the transparent fragments won't be shaded cor-
rectly, as the normals and depth read in will be for the transparent object! As with 'forward' rendering,
it is usual to perform deferred lighting on only opaque objects, and then draw the transparent ones
on top, using 'normal' lighting techniques, or maybe not even lit at all.
Having to render data into G-Buers, rather than always having vertex attributes to hand, does
place some limitations on the types of eect used - there's only so much data we can pack into the
G-Buer without requiring even more textures, further increasing space and bandwidth requirements.
Lastly, depending on the API used, we might not be able to perform antialiasing using deferred
rendering. Antialiasing is the process of removing 'jaggies' - the jagged edges along the edges of
polygons. Older hardware cannot store the information required to perform hardware antialiasing in
a G-Buer texture. Antialising using edge detection and a blur lter has become a popular solution
to this.
Despite these drawbacks, variations on deferred rendering have become the standard method of draw-
ing highly complex, lit scenes - Starcraft 2, Killzone 2, and games using the CryEngine 3 and Unreal
3 game engines all support deferred lighting in some form.
Rendering Stages
Basic deferred rendering is split into three render passes - the rst renders graphical data out into the
G-Buer, the second uses that data to calculate the accumulated light for each visible pixel, and the
third combines the basic textures and lighting into a nal scene.
4
1) Fill the G-Buer
First, the G-Buer must be lled. This stage is similar to how you were rendering things before
we introduced per-pixel lighting. For every object in the scene, we transform their vertices using a
'ModelViewProjection' matrix, and in a fragment shader sample their textures. However, instead of
rendering the textured result to the back buer, we output the data we need to perform lighting to
several colour buers, via a frame buer object with multiple attachments.
2) Perform Lighting Calculations
With the G-Buer information rendered into textures, the second pass of calculating the lighting
information can be performed. By rendering light volumes such as spheres and cones into the scene,
the fragments which might be lit can be 'captured', and processed using the lighting fragment shader.
In the fragment shader, the world position of the fragment being processed is reconstructed with help
from the G-Buer depth map - using this we can perform the lighting calculations, or discard the
fragment if the fragment is too far away from the light volume to be rendered. We must still check
whether fragments 'captured' by the light volume are outside the light's attenuation, due to situations
where the light volume covers objects that are very far away.
3) Combine Into Final Image
In the nal draw pass, we draw a single screen-sized quad, just like we do to perform post-processing
techniques. In the fragment shader, we simply sample the lighting attachment textures, and the dif-
fuse texture G-Buer, and blend them together to create the nal image.
5
979347012.001.png 979347012.002.png
Zgłoś jeśli naruszono regulamin