DirectXGraphics:
Billboarding
Author:
Jack Hoxley
Written: 25th May 2001
Contact: [EMail]
Download: Graph_15.Zip
(181kb)
Contents
of this lesson
1. Introduction
2. The maths behind it all
3. Migrating this into D3D
1.
Introduction
Welcome
back for another lesson in the world of
Direct3D graphics. In this lesson we'll
learn how to build cutting edge graphics
into our games - without requiring a
super computer. Before you get excited
though, this isn't quite the answer to
everything - it only works in certain
cases, and doesn't always look perfect -
but it's a commonly used technique in
commercial games, so there's no reason
to avoid it.
When
you play any action game on the PC your
almost guaranteed to see an explosion at
some point - sometimes they are
breathtaking, other times they are
fairly lame excuses for explosions -
either way, with the techniques and
skills you've learnt in this series
you'll be aware that something clever
must be happening. The only way we've
learnt so far to replicate an explosion
is to use the particle effects (point
sprites), but you'll also be aware that
your limited to the number that can be
rendered - a decent explosion would
require 1000's if not millions of these
particles - very slow using these
methods. So how do they do it, if
they're not using raw geometry?
The
answer is very simple, and in truth it's
just a visual trick - after reading this
article, if you go back and play unreal
tournament, Deus Ex, Half Life (or
whatever takes your fancy) you may be
able to catch them at it. The explosion
itself is just a clever use of
animation, textures and a little bit of
geometry; one of the artists has used a
program to render an explosion onto a
series of textures, and the programmer
uses these textures, a few triangles and
some alpha blending to put it in the
game engine. The original explosion
textures could take 3 days to render for
all we care - once they're rendered we
can use them in our game just the same
as any other texture.
(Thanks
to Xtreme Game LLC)
Above
is an example of the animations I'm
talking about, you can see that straight
away it would be very very difficult to
replicate something like that
on-the-fly, and doing it fast would be
even harder! The visual trick behind all
of this is called billboarding. In our
3D world we orientate 2 triangles in the
form of a square so that they exactly
face the camera; this means that they
appear with the correct aspect ratio and
perspective, yet they can still be
scaled and occluded by other aspects of
our 3D world (distance for example).
2.
The maths behind it all
The
maths involved is simply down to working out the
correct angle that we want to rotate the billboard
by, in some worlds this can be extremely simple
- just around the Y axis, whereas others will
need to be rotated around all 3 axis; on the other
hand, you may well not want them rotated around
a particular axis because it'll look wrong - take
a tree for example, you don't want it to rotate
so that the trunk/roots appear to come out of
the ground...
Before
we get started here credit is due to Eric Coleman
who pretty much wrote all of the proper billboarding
maths and code - thanks to him! You can see what
he's working on here: Gladiator
- Fields Of Slaughter. The following steps
are an adapted version of what Eric explained
to me via email and a sample program...
The
problem: To rotate a simple quad (2 triangles)
so that it faces the camera:
Step
1: Calculate the direction vector:
Maths: vN = -vTo + vFrom
In VB:
vN.X = -vTo.X + vFrom.X vN.Y = -vTo.Y + vFrom.Y vN.Z = -vTo.Z + vFrom.Z
|
|
|
|
Step
2: Convert To Spherical Coordinates
We now have a direction vector, which if placed
at the origin can be treated as a point in 3D
Space, as shown in the following diagram:
We
want to change this point into spherical coordinates,
which can be done using the following maths:
And all this in VB is:
R = Sqr(vN.X * vN.X + vN.Y * vN.Y + vN.Z * vN.Z)
temp = vN.Z / R
If temp = 1 Then
BBphi = 0
ElseIf temp = -1 Then BBphi = PI Else BBphi = Atn(-temp / Sqr(-temp * temp + 1)) + (PI / 2) End If
temp = vN.X / (R * Sin(BBphi)) If temp = 1 Then BBtheta = 0 ElseIf temp = -1 Then BBtheta = PI Else BBtheta = Atn(-temp / Sqr(Abs(-temp * temp + 1))) + (PI / 2) End If
If vN.Y < 0 Then BBtheta = -BBtheta End If
|
|
|
|
This
next image shows the relationship of phi and theta
- the two angles that we just found while converting
to spherical coordinates...
Step
3: Rotating
We now know the angles by which we need to
rotate the billboard, all we need to do is actually
rotate them...
First we start off with the plain geometry; this
must be created in the XY plane - as demonstrated
by the next diagram:
The first step to rotating the billboard is to
rotate it phi radians around the Y axis, in maths
this will look like this:
Graphically it looks like this:
and in VB this will be:
x = V.x * cos(phi) - V.z * sin(phi)
z = V.x * sin(phi) + V.z * cos(phi)
V.x = x
V.z = z
|
|
|
|
The
next step is to rotate around the Z axis by theta
radians. Graphically this will be:
and mathematically:
finally, in VB this looks like
x = V.x * cos(theta) + V.y * sin(theta)
y = -V.x * sin(theta) + V.y * cos(theta)
V.x = x
V.y = y
|
|
|
|
The
final step is to translate the billboard back
to it's original coordinates - a simple vector
addition.
Conclusion
Right, above is all you need to know to construct
the relevent rotations for a billboard - but it
isn't the end of the world if you dont completely
understand all the maths behind it - the following
D3D code implementation should be enough for most
people.
3.
Migrating this to Direct3D
Now
that we have the mathematics worked out, we need
to make this work with code...
We're
going to break this into two main sections - cheap
billboards and proper billboards, the former is
a simple method that I came up that doesn't rely
on any maths; the latter is the implementation
of Eric Coleman's maths for rotating the geometry.
Cheap billboards are functional and can be set
up easily in about 2 minutes, but aren't really
useful when you can do proper billboarding - but
I thought I'd leave them in here, should you really
want to use them...
A:
Cheap Billboards
This method uses transformed and lit vertices
- you transform the billboard coordinates into
screen space, then draw a quad (2 triangles in
a square shape) around the coordinate - simple
really. It's effectively the same as the point
sprites demonstrated in
this previous tutorial. Here's the following
code that makes it all work (taken from the sample
application):
Private Sub RenderCheapBillboards(vp As D3DVIEWPORT8) '###################### '## 0. DECLARATIONS ## '##################### Dim I As Long, X As Long, Y As Long Dim v2D(0 To 4) As D3DVECTOR, Verts(0 To 3) As TLVertex Dim Ref(0 To 4) As Long 'sort the depths... Dim Depths(0 To 4) As Single ' - NB: wont respond to camera roll. '########################## '## 1. SETUP THE DEVICE ## '######################## D3DXMatrixIdentity matWorld D3DDevice.SetRenderState D3DRS_ALPHATESTENABLE, 1 'alpha testing is useful... ;) D3DDevice.SetRenderState D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL 'Pixel passes if (pxAlpha>=ALPHAREF) D3DDevice.SetRenderState D3DRS_ALPHAREF, 50 'only if the pixels alpha is greater than 'or equal to 50 will it be rendered (skips lots of rendering!) D3DDevice.SetRenderState D3DRS_ZWRITEENABLE, 0 'we dont want to affect the depth buffer D3DDevice.SetTexture 0, TexExplosion D3DDevice.SetVertexShader FVF_TLV '###################################### '## 2. TRANSFORM TO SCREEN SPACE ## '#################################### For I = 0 To 4 D3DXVec3Project v2D(I), ExpTranslate(I), vp, matProj, matView, matWorld Ref(I) = I Depths(I) = v2D(I).Z Next I '##################### '## 3. SORT DEPTHS ## '################### Dim Changes As Long, tmp As Single, lTmp As Long Changes = 1 'just to get it started... Do While Changes > 0 Changes = 0 For I = 0 To 3 If Depths(I + 1) > Depths(I) Then tmp = Depths(I) Depths(I) = Depths(I + 1) Depths(I + 1) = tmp lTmp = Ref(I) Ref(I) = Ref(I + 1) Ref(I + 1) = lTmp Changes = Changes + 1 End If Next I Loop '########################### '## 4. RENDER THE QUADS ## '######################### For I = 0 To 4 '//Generate the vertices. Verts(0) = CreateTLV(v2D(Ref(I)).X - 100, v2D(Ref(I)).Y - 100, v2D(Ref(I)).Z, _ _ 1, &HFFFFFF, 0, 0) Verts(1) = CreateTLV(v2D(Ref(I)).X + 100, v2D(Ref(I)).Y - 100, v2D(Ref(I)).Z, _ _ 1, &HFFFFFF, 1, 0) Verts(2) = CreateTLV(v2D(Ref(I)).X - 100, v2D(Ref(I)).Y + 100, v2D(Ref(I)).Z, _ _ 1, &HFFFFFF, 0, 1) Verts(3) = CreateTLV(v2D(Ref(I)).X + 100, v2D(Ref(I)).Y + 100, v2D(Ref(I)).Z, _ _ 1, &HFFFFFF, 1, 1) X = ExpFrame(Ref(I)) Mod 4 Y = ExpFrame(Ref(I)) \ 4 '//Set up the correct texture coordinates Verts(0).T.X = X / 4: Verts(0).T.Y = Y / 4 Verts(1).T.X = (X + 1) / 4: Verts(1).T.Y = Y / 4 Verts(2).T.X = X / 4: Verts(2).T.Y = (Y + 1) / 4 Verts(3).T.X = (X + 1) / 4: Verts(3).T.Y = (Y + 1) / 4 '//Update the current frame if necessary If GetTickCount - ExpLastChange(I) > ExpSpeed(I) Then ExpLastChange(I) = GetTickCount ExpFrame(I) = ExpFrame(I) + 1 If ExpFrame(I) > 11 Then ExpFrame(I) = 0 End If '//Finally, render the quad D3DDevice.DrawPrimitiveUP D3DPT_TRIANGLESTRIP, 2, Verts(0), Len(Verts(0)) Next I '############################# '## 5. CLEAN UP THE DEVICE ## '########################### D3DDevice.SetRenderState D3DRS_ALPHATESTENABLE, 0 D3DDevice.SetRenderState D3DRS_ZWRITEENABLE, 1 'we dont want to affect the depth buffer End Sub
|
|
|
|
right,
that shouldn't look too nasty :)
Basically, we collect the depth values for all
the billboards, as well as the 2D coordinate that
they'll be rendered to. We then sort out the Ref(
) array to point, in order, to the deepest-closest
before rendering them. Why do this? Because we're
alpha blending the sprites - alpha blending is
dependant on the current pixels rendered behind
it - so if you draw them in any order you'll get
some strange artifacts, so we sort them, then
draw the deepest before the shallowest so that
we dont get any artifacts. The only two drawbacks
with this method is that the billboards dont respond
to the roll parameter in the projection matrix
setup (so if the rest of the world rolls over
the billboards wont), also there may well end
up being some depth related issues - whilst they
should be rendered at the correct depth, there
is a slight possibility that they'll get out of
sync towards the end of the depth buffer range
(1.0) - this is a property of Z-Buffers and cant
be worked around.
B:
Proper Billboards
Okay, now onto the more complicated method, the
one that Eric designed, and the one with all the
maths... :)
We're
going to set up two main functions, one to calculate
the required angles - this must be called on every
update of the camera; and another function to
construct the relevent matrix for the billboards
- this must be called for every billboard. Finally
there is a sub routine that uses the results of
these two master functions to render the final
product.
First
up, the function for calculating the angles, some
of the code here may look familiar:
Public Sub FindAngles(vFrom As D3DVECTOR, vTo As D3DVECTOR) '//Finds the angles required to set up the correct '//billboard rotations. Written by Eric Coleman (thanks!)
Dim vN As D3DVECTOR Dim R As Single, temp As Single '//1. Calc. Vector from Cam->BBoard vN.X = -vTo.X + vFrom.X vN.Y = -vTo.Y + vFrom.Y vN.Z = -vTo.Z + vFrom.Z
'//2. Convert to spherical Coords R = Sqr(vN.X * vN.X + vN.Y * vN.Y + vN.Z * vN.Z)
temp = vN.Z / R If temp = 1 Then BBphi = 0 ElseIf temp = -1 Then BBphi = PI Else BBphi = Atn(-temp / Sqr(-temp * temp + 1)) + (PI / 2) End If
temp = vN.X / (R * Sin(BBphi)) If temp = 1 Then BBtheta = 0 ElseIf temp = -1 Then BBtheta = PI Else BBtheta = Atn(-temp / Sqr(Abs(-temp * temp + 1))) + (PI / 2) End If
If vN.Y < 0 Then BBtheta = -BBtheta End If
End Sub
|
|
|
|
The
next function is the generate matrix function...
Private Sub GenerateBBMatrix(Index As Long) Dim tempMatrix As D3DMATRIX Dim tempMatrix2 As D3DMATRIX
D3DXMatrixIdentity
matWorld
D3DXMatrixIdentity
tempMatrix
D3DXMatrixRotationY
tempMatrix, BBphi
D3DXMatrixRotationZ
tempMatrix2, BBtheta
D3DXMatrixMultiply
matWorld, tempMatrix,
tempMatrix2
matWorld.m41
= ExpTranslate(Index).X
matWorld.m42 = ExpTranslate(Index).Y
matWorld.m43 = ExpTranslate(Index).Z
D3DDevice.SetTransform
D3DTS_WORLD, matWorld
End Sub
|
|
|
|
Finally
we have the wrapper sub, the one that controls
everything! This may require some explanation
though. It's divided into 6 nice sections for
you - which makes things nice and easy. The first
and the last section configure the device, the
only important parts here are the alpha testing
and Z-Writing; firstly I've set it up to not render
any pixels with an alpha value of less than 50
(of 255) - the sample code uses alpha maps (as
demonstrated in this previous
tutorial) and alpha testing is an interesting
little feature I thought I may as well include.
Secondly there is the Z writing, which I disable
at the start of the sub and enable again at the
end - the billboards aren't really 'there' in
this case, so I dont want them to affect other
geometry (if Z writing was enabled anything behind
them would not get rendered if the billboard has
already been rendered). Also, we're using a simple
sorting algorithm (bubble sort to be precise)
so that we render the billboards back-front in
world space - this is purely for alpha blending
purposes; if you're not using any form of alpha
blending then you can skip this part. Alpha blending
is draw depth dependent, which is why this is
necessary.
Private Sub RenderProperBillBoards(vp As D3DVIEWPORT8) Dim I As Long, X As Long, Y As Long Dim StoredMatrices(0 To 4) As D3DMATRIX '//we need to cache the matrices... Dim Depth(0 To 4) As Single, vTmp As D3DVECTOR Dim Ref(0 To 4) As Long 'pointer to correct value...
'######################### '## 1: SETUP THE DEVICE ## '########################
D3DDevice.SetVertexShader FVF_LV D3DDevice.SetTexture 0, TexExplosion D3DDevice.SetRenderState D3DRS_ALPHATESTENABLE, 1 'alpha testing is useful... ;) D3DDevice.SetRenderState D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL 'Pixel passes if (pxAlpha>=ALPHAREF) D3DDevice.SetRenderState D3DRS_ALPHAREF, 50 D3DDevice.SetRenderState D3DRS_ZWRITEENABLE, 0 'we dont want to affect the depth buffer '#################################### '## 2: CALCULATE NECESSARY DATA ## '###################################
For I = 0 To 4 X = ExpFrame(I) Mod 4 Y = ExpFrame(I) \ 4
'//Set up the correct texture coordinates Exp(I).V(0).T.X = X / 4: Exp(I).V(0).T.Y = Y / 4 Exp(I).V(1).T.X = (X + 1) / 4: Exp(I).V(1).T.Y = Y / 4 Exp(I).V(2).T.X = X / 4: Exp(I).V(2).T.Y = (Y + 1) / 4 Exp(I).V(3).T.X = (X + 1) / 4: Exp(I).V(3).T.Y = (Y + 1) / 4
'//Update the current frame if necessary... If GetTickCount - ExpLastChange(I) > ExpSpeed(I) Then ExpLastChange(I) = GetTickCount ExpFrame(I) = ExpFrame(I) + 1 If ExpFrame(I) > 11 Then ExpFrame(I) = 0 End If
'//Calculate the correct rotation/translation matrix for the geometry GenerateBBMatrix I StoredMatrices(I) = matWorld 'cache this matrix for usage later on. NB: it is the currently set matrix though... Next I
'########################################## '## 3: GATHER PROJECTED DEPTH VALUES ## '######################################## For I = 0 To 4 D3DXVec3Project vTmp, ExpTranslate(I), vp, matProj, matView, matWorld Ref(I) = I Depth(I) = vTmp.Z 'all we want is the Depth-Buffer value. Next I
'########################### '## 4: SORT DEPTH VALUES ## '##########################
Dim Changes As Long, tmp As Single, lTmp As Long Changes = 1 'just to get it started... Do While Changes > 0 Changes = 0
For I = 0 To 3 If Depth(I + 1) > Depth(I) Then tmp = Depth(I) Depth(I) = Depth(I + 1) Depth(I + 1) = tmp lTmp = Ref(I) Ref(I) = Ref(I + 1) Ref(I + 1) = lTmp Changes = Changes + 1 End If Next I
Loop
'############################################# '## 5: RENDER DEEPEST FIRST-CLOSEST LAST ## '############################################ For I = 0 To 4 D3DDevice.SetTransform D3DTS_WORLD, StoredMatrices(Ref(I)) D3DDevice.DrawPrimitiveUP D3DPT_TRIANGLESTRIP, 2, Exp(Ref(I)).V(0), Len(Exp(Ref(I)).V(0)) Next I
'########################### '## 6: TIDY UP THE DEVICE ## '######################### D3DDevice.SetRenderState D3DRS_ALPHATESTENABLE, 0 D3DDevice.SetRenderState D3DRS_ZWRITEENABLE, 1
End Sub
|
|
|
|
Well,
there we have it - another tutorial down. Hopefully
you'll find this technique useful in the future...
go play some of the top 3D action games and I'm
pretty sure you'll notice some nice billboard-based
special effects in action...
I
strongly suggest that you download the complete
source code for this tutorial. After that, you
can move onto the next tutorial - Lesson
16 : Visibility Testing and Culling to increase
speed
|