Simple billboard orientation in world space

I had a great deal of difficulty in working out how to do billboards properly, simply because I don’t have the mental agility to handle the descriptions usually given in 3D programming tutorials. The method I’ve managed to figure out is related to gluLookAt: and uses a similar method to work out the rotation matrix needed to map one vector to another.

Here’s how the process works:

1) Create a vertex array object (VAO) for a single quad. How you do this will depend in large part on how you’ve set up your axes for your world space; in my case, +x is east, +y is north and +z is up; hence my billboard model is a square quad in the x and z axes, whose normal is the y axis:

Simple quad for billboarding

2a) When rendering, work out the direction from the centre of the billboard (in world space) to the camera (in world space). It’s easy to constrain your billboard in one axis (for example, you might want to render billboards which always stick straight up by ignoring the z component of the direction vector). Normalise this vector.

2b) Alternatively you might want to orient your billboards to the screen rather than the camera:

Billboarding_image_two

This is surprisingly easy to do: get the opposite of your camera’s direction vector and apply it to all of the billboards, which will line them all up parallel to the screen; this has the double advantage that you only need to work the transform out once, and hence you can save a lot of CPU time. This sort of technique works particularly nicely for rounded objects.

3) Specify a “facing” vector for the billboard, which is the vector in model space you want to use to orient your billboard in world space – in this example, that would be the vector (0.0, 1.0, 0.0).

3) Get the dot product and the normalised cross product of the vector to the camera and the “facing” vector

4) The cross product gives you the weights for each axis of a rotation matrix, while the arccosine of the dot product gives you the magnitude of the rotation (using radians).

5) Your billboard’s modelToWorld matrix is now TranslationMatrix(billboard.worldSpaceCoord.x, billboard.worldSpaceCoord.y, billboard.worldSpaceCoord.z) * RotationMatrix (crossProduct.x, crossProduct.y, crossProduct.z, acos(dotProduct)) * ScaleMatrix(billboard.size.x, billboard.size.y, billboard.size.z).

 
void normalize(float v[3]) {    
    GLfloat d = sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]); 
    if (d == 0.0) {
        //NSLog(@"zero length vector");    
        return;
    }
    v[0] /= d; v[1] /= d; v[2] /= d; 
}
 
void crossprod(float v1[3], float v2[3], float output[3])
{
    output[0] = v1[1]*v2[2] - v1[2]*v2[1];
    output[1] = v1[2]*v2[0] - v1[0]*v2[2];
    output[2] = v1[0]*v2[1] - v1[1]*v2[0];
}
 
void normcrossprod(float v1[3], float v2[3], float output[3])
{ 
    crossprod(v1, v2, output);
    normalize(output); 
}
 
- (Matrix4x4)matrixRotatingBasisVector:(float *)basisVector toDirectionVector:(float *)directionVector {
    float cosTheta = dotProduct(basisVector, directionVector);
    float rotationAxis[3];
    normcrossprod(basisVector, directionVector, rotationAxis);
    return RotationMatrix(rotationAxis[0], rotationAxis[1], rotationAxis[2], acosf(cosTheta));
}
 
 
//the code above actually works as long as you drop in your own matrix class; everything below is pseudocode.
 
- (void)renderParticles {
    for (Billboard *aBillboard in myArrayOfBillboards) {
        float billboardToCamera[3] = {aBillboard.worldPosition.x - cameraWorldPositionX,
                                      aBillboard.worldPosition.y - cameraWorldPositionY,
                                      aBillboard.worldPosition.z - cameraWorldPositionZ};
        normalize(billboardToCamera);
 
        float billboardNormalVector[3] = {0.0, 1.0, 0.0};
        Matrix4x4 modelRot = [self matrixRotatingBasisVector:billboardNormalVector toDirectionVector:billboardToCamera];
 
        Matrix4x4 modelScale = ScaleMatrix(aBillboard.size.x, aBillboard.size.y, aBillboard.size.z);
        Matrix4x4 worldPosition = (TranslationMatrix(aBillboard.worldPosition.x, aBillboard.worldPosition.y, aBillboard.worldPosition.z);
        Matrix4x4 modelToWorld =  worldPosition * modelRot * modelScale;
 
        //rendering code
    }
}

Protip: you can use the same method to orient any model in world space – just chose an axis in model space and a direction in world space, and you’re done!

Protip #2: when trying to get this sort of thing to work, disable face culling. It saves a lot of fiddling about tweaking the wrong bit of code when the actual reason you can’t see any of your billboards is that they’re facing away from you.

One thought on “Simple billboard orientation in world space

Leave a Reply

Your email address will not be published. Required fields are marked *