DirectXGraphics:
Stencil Buffer Tricks
Author:
Jack Hoxley
Written: 21st July 2002
Contact: [EMail]
Download: Stencil
Buffers.Zip (78kb)
Contents
of this lesson
1. What is the stencil buffer?
2. A simple example: Draw depth complexity
1.
What is the stencil buffer?
The stencil
buffer is part of the memory space used by the
Z-Buffer in the Direct3D flipping chain. It allows
you to set a "hidden" value for each
pixel rendered, and/or determine what pixels get
rendered based on the contents of the buffer at
the given coordinate. Essentially it operates as a
stencil-template for future rendering...
You should be
familiar with the Z buffer by now, and should
therefore be aware that you specify the Z buffer
resolution as the number of bits of memory per
pixel it uses - 16, 24 or 32. when using either 16
or 32 bit modes the z buffer occupies an amount of
memory equal to 2 or 4 bytes (with no spare). When
using 24bit mode there is 8bits of unused memory
(to pad it out to a more convenient 32bit/4byte
boundary). It is in this unused 8bits that we can
store the stencil buffer data.
There are two
formats available in D3D8 - 24bit Z and 8bit
Stencil OR 24bit Z and 4bit Stencil (4bits
unused). In order to make use of the stencil
buffer you must check to see if the device
supports either of these formats. The majority of
modern hardware (GeForce and above) will support
stencil buffers, but support on older cards is
very sketchy at best.
Once we know we
can use the stencil buffer we need to make use of
a very important equation:
(StencilRef
And StencilMask) CompFunc (StencilBufferValue
And StencilMask)
There are 4
components to this equation:
1. StencilRef - a reference value you can
use for comparisons
2. StencilMask - used so you can focus on
particular bits of accuracy (say you only need the
3rd bit)
3. CompFunc - very important, this
essentially defines what and how a pixel can pass
a stencil test.
4. StencilBufferValue - This is the value
currently stored in the stencil buffer.
The first 3 in
the list can be configured using
Direct3DDevice8.SetRenderState( ) calls.
Stencil Buffer
'tricks' can be very useful in creating some of
the more advanced special effects you see in
industrial-quality 3D games currently. Shadows,
Reflections, Sniper-zooms, heat-sensing
displays... You can also use them for more subtle
effects such as screen dissolves, non-rectangular
rendering (render to an oval-shaped view for
example).
2. A simple example: Draw depth complexity
First off, what
is depth complexity? sometimes referred to as
'Overdraw' it is essentially a count of the number
of times each pixel is drawn each frame. In theory
this should only be one - why would you want to
draw a pixel two, three or four times? if you
think about it - each time you render a triangle
it is compared with the depth buffer and only
rendered if is in front of whatever is already
there - BUT it doesn't know if anything later in
the frame will be in front of it (hence obscuring
it from our view). It is this method that means
that in a complex scene it is quite possible that
each pixel will be rendered more than once.
The
following code is a rather simple use of the depth
buffer, and doesn't really serve much use in a
real-world application. But it's a useful example
for learning about the stencil buffer. The source
code presented is intended to be plugged into the
standard framework presented in my tutorials, if
you're unfamiliar with this you'll need to
download it.
The
first step is to enumerate for stencil buffer
support. This can be done using this simple logic
tree. The end result is that the 'D3DWindow'
structure is filled appropriately (and then the
device is created) or the sample will exit:
If
D3D.CheckDepthStencilMatch(0, D3DDEVTYPE_HAL,
_
DispMode.Format,
DispMode.Format, _
D3DFMT_D24S8) = D3D_OK
Then
'we can use the D24/S8 format
D3DWindow.AutoDepthStencilFormat = D3DFMT_D24S8
Else
If D3D.CheckDepthStencilMatch(0, D3DDEVTYPE_HAL,
_
DispMode.Format,
DispMode.Format, _
D3DFMT_D24X4S4) = D3D_OK
Then
'we can use the D24/S4 format
D3DWindow.AutoDepthStencilFormat = D3DFMT_D24X4S4
Else
'we cant use either stencil format... oh well.
Unload
Me
End
End If
End If |
|
|
|
The
above code must be executed before the
CreateDevice() call, or the results are
meaningless! The next change comes in the Render(
) function - in particular the part where we clear
the buffers. Now that we're using the stencil
buffer we must also clear it each frame:
D3DDevice.Clear 0,
ByVal 0, D3DCLEAR_TARGET
Or _
D3DCLEAR_ZBUFFER Or _
D3DCLEAR_STENCIL, _
D3DColorXRGB(200, 200, 255), _
1#, 0 |
|
|
|
Not
a particularly complicated change that one...
However, this next one is a little tricky. In
order to display the depth complexity of a scene
you have to first render the scene and update the
stencil buffer; you then need to re-render the
scene with a set of full-screen quads to display
the contents of the stencil buffer. It is useful
to note that the first-pass (with the normal
objects) won't actually be seen on screen -
therefore there is no point in rendering anything
but the raw geometry; lights, textures, pixel
shaders etc... won't be seen and are therefore a
complete waste of time :-)
This
next piece of code shows you how to configure the
stencil buffer for the first pass:
Private Sub
SetupStencilBuffer()
With D3DDevice
.SetRenderState D3DRS_STENCILENABLE, 1
.SetRenderState D3DRS_STENCILFUNC, D3DCMP_ALWAYS
.SetRenderState D3DRS_STENCILREF, 0
.SetRenderState D3DRS_STENCILMASK, 0
.SetRenderState D3DRS_STENCILWRITEMASK, &HFFFFFFFF
.SetRenderState D3DRS_STENCILZFAIL, D3DSTENCILOP_INCRSAT
.SetRenderState D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP
.SetRenderState D3DRS_STENCILPASS, D3DSTENCILOP_INCRSAT
End With
End Sub |
|
|
|
The above code
looks far more complex than it really is. The
first two lines deal with turning on the stencil
buffer and setting it so that all tests always
pass. To display overdraw we don't need a
reference or mask value, so we leave them as 0.
The last three lines are the most important
really, D3DSTENCILOP_INCRSAT
indicates that we'll increment the current value
in the buffer but STOP when we've reached the
maximum value (15 or 255 depending on the bit
depth). D3DSTENCILOP_INCR would also increment the
value in the buffer, but when it reaches the
maximum value it would wrap back to 0 - which we
don't want. The last three lines are used to tell
D3D what it should do to the values in the buffer
depending on what the outcome of the various tests
were.
After
rendering the geometry with these stencil buffer
settings we can go about displaying the final
overdraw values. Consider that the stencil buffer
now contains a complete set of values indicating
how many times each pixel was drawn to. In order
to display this we need to re-configure the device
to ONLY render on a given value. This basically
means reading the stencil buffer, but not actually
writing to it.
Private Sub RenderOverDraw()
Dim TLVerts(0
To 3)
As TLVERTEX
'//needed to size the quad properly.
Dim VP
As D3DVIEWPORT8
D3DDevice.GetViewport VP
'//set up the default parameters for the TL Quad. The colour will
'//be altered for each level we draw.
TLVerts(0).rhw = 1: TLVerts(1).rhw = 1: TLVerts(2).rhw = 1: TLVerts(3).rhw = 1
TLVerts(0).Color = D3DColorXRGB(255, 0, 0)
TLVerts(1).Color = TLVerts(0).Color
TLVerts(2).Color = TLVerts(0).Color
TLVerts(3).Color = TLVerts(0).Color
TLVerts(0).X = 0: TLVerts(0).Y = 0
TLVerts(1).X = VP.Width: TLVerts(1).Y = 0
TLVerts(2).X = 0: TLVerts(2).Y = VP.Height
TLVerts(3).X = VP.Width: TLVerts(3).Y = VP.Height
D3DDevice.SetTexture 0,
Nothing
D3DDevice.SetVertexShader FVF_TLVERTEX
'//This next line is necessary to clear any pixels with
'//a 0 overdraw value... as they wont be picked up in the next
'//stage.
D3DDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET, 0, 0, 0
With D3DDevice
If bWire
Then .SetRenderState D3DRS_FILLMODE, D3DFILL_SOLID
'//we dont care whats in the z buffer, so ignore it.
.SetRenderState D3DRS_ZENABLE, 0
.SetRenderState D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP
.SetRenderState D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP
.SetRenderState D3DRS_STENCILPASS, D3DSTENCILOP_KEEP
.SetRenderState D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL
.SetRenderState D3DRS_STENCILREF, 0
'DRAW LEVEL 1 OVERDRAW
.SetRenderState D3DRS_STENCILMASK, &H1
TLVerts(0).Color = D3DColorXRGB(0, 0, 255)
TLVerts(1).Color = TLVerts(0).Color
TLVerts(2).Color = TLVerts(0).Color
TLVerts(3).Color = TLVerts(0).Color
.DrawPrimitiveUP D3DPT_TRIANGLESTRIP, 2, TLVerts(0), Len(TLVerts(0))
'DRAW LEVEL 2 OVERDRAW
.SetRenderState D3DRS_STENCILMASK, &H2
TLVerts(0).Color = D3DColorXRGB(0, 255, 0)
TLVerts(1).Color = TLVerts(0).Color
TLVerts(2).Color = TLVerts(0).Color
TLVerts(3).Color = TLVerts(0).Color
.DrawPrimitiveUP D3DPT_TRIANGLESTRIP, 2, TLVerts(0), Len(TLVerts(0))
'DRAW LEVEL 3 OVERDRAW
.SetRenderState D3DRS_STENCILMASK, &H4
TLVerts(0).Color = D3DColorXRGB(255, 128, 0)
TLVerts(1).Color = TLVerts(0).Color
TLVerts(2).Color = TLVerts(0).Color
TLVerts(3).Color = TLVerts(0).Color
.DrawPrimitiveUP D3DPT_TRIANGLESTRIP, 2, TLVerts(0), Len(TLVerts(0))
'DRAW LEVEL 4 OVERDRAW
.SetRenderState D3DRS_STENCILMASK, &H8
TLVerts(0).Color = D3DColorXRGB(255, 0, 0)
TLVerts(1).Color = TLVerts(0).Color
TLVerts(2).Color = TLVerts(0).Color
TLVerts(3).Color = TLVerts(0).Color
.DrawPrimitiveUP D3DPT_TRIANGLESTRIP, 2, TLVerts(0), Len(TLVerts(0))
'DRAW ALL REMAINING LEVELS OVERDRAW
'(this level wont exist for a 4bit stencil buffer)
.SetRenderState D3DRS_STENCILMASK, &HF0
TLVerts(0).Color = D3DColorXRGB(255, 255, 255)
TLVerts(1).Color = TLVerts(0).Color
TLVerts(2).Color = TLVerts(0).Color
TLVerts(3).Color = TLVerts(0).Color
.DrawPrimitiveUP D3DPT_TRIANGLESTRIP, 2, TLVerts(0), Len(TLVerts(0))
'restore the various stage states...
.SetRenderState D3DRS_ZENABLE, 1
.SetRenderState D3DRS_STENCILENABLE, 0
End With
End Sub |
|
|
|
There's a lot of
code in that table, but the majority of it is
quite simple - you should all be familiar with the
TLVerts( ) configuration and rendering code by
now! For each layer all we need to do is change
the stencil mask value - accending in powers of 2:
1,2,4,8 (1st, 2nd, 3rd and 4th bits respectively).
D3D will handle the rest... we attempt to render
the quad over the entire screen, but D3D will only
actually render a pixel when it passes the stencil
test - in this case, when the overdraw is of the
correct value.
The following two
images are from the sample program. The first is
of normal rendering - not very representative of a
real game environment, but a good demonstration of
overdraw. The second is showing the scenes
overdraw.
As
you can see from the second image, overdraw
follows an almost radial pattern - only the middle
shows the highest overdraw. This is fairly logical
- because of the way that the scene is rendered
(camera POV) the majority of cubes are clustered
around the center of the image. The next
screenshot shows the output of a simple analysis
program I made that will process a screenshot and
calculate overdraw values:
|