Main Site Links Resources Tutorials
News VB Gaming Code Downloads DirectX 7
Contact Webmaster VB Programming Product Reviews DirectX 8
  General Multimedia Articles DirectX 9
      Miscellaneous

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

DirectX 4 VB 2000 Jack Hoxley. All rights reserved.
Reproduction of this site and it's contents, in whole or in part, is prohibited,
except where explicitly stated otherwise.
Design by Mateo
Contact Webmaster