Billboarding is the process of rotating a quad to face the camera so that no matter what the camera position, the quad is rotated to directly face the camera.
I’ve been fooling around with particle effects, and I was looking for some spherical billboard rotation code - but everything I found seemed to be tutorial style, and written for clarity instead of efficiency. All I wanted was to generate a 3x3 rotation matrix that I could apply to the quad vertices given a camera position and a particle pivot position. The pivot position is the center of the quad, with each of the 4 vertices in a different quadrant on the xy plane. I also wanted to avoid calling any trigonometric functions, and rely on simple pythagorean relationships. So, I tried to simplify the calculations necessary to orient a quad to face the camera, and after a little algebraic refactoring, I came up with the following function. It does need to calculate 3 square roots, but there are no trig functions or cross products. It references the java vecmath package for the Point3f, Vector3f and Matrix3f implementations, but not for anything really important (just vector normalize and dot product). For simplicity, I present it here as a static function that creates new objects instead of reusing members.
Here’s the stripped down version:
public static Matrix3f calculateBillboardRotation(Point3f eye, Point3f p)
{
Vector3f view = new Vector3f(eye.x - p.x, eye.y - p.y, eye.z - p.z);
Vector3f xzp = new Vector3f(view.x, 0, view.z);
xzp.normalize();
view.normalize();
float cosp = view.dot(xzp);
float sinp = (float) Math.sqrt(1 - cosp * cosp) * (view.y > 0 ? -1 : 1);
return new Matrix3f(xzp.z, xzp.x * sinp, xzp.x * cosp, 0, cosp, -sinp, -xzp.x, xzp.z * sinp, xzp.z * cosp);
}
If that seemed interesting to you, here’s the version with comments that hopefully explain what the heck I was thinking:
/**
* Calculates a 3x3 rotation matrix used to orient a quad billboard to directly face the camera eye.
* The camera eye and the point p about which the billboard should pivot should be in world co-ordinates.
* When applying the rotation matrix, first translate the vertices about the pivot point to the origin,
* transform the vertices with the matrix, and then translate the new vertices back to world space.
* The rotation is a 360 degree rotation around the y axis,
* combined with a +/- 90 degree rotation about the x axis (the tilt up or down).
* The initial billboard position assumes it is parallel to the xy plane with the normal
* vector pointing to (0,0,1).
* This method uses no trig functions, but it does calculate 3 square roots.
*/
public static Matrix3f calculateBillboardRotation(Point3f eye, Point3f p)
{
// The view vector is the direction from the point to the camera eye
Vector3f view = new Vector3f(eye.x - p.x, eye.y - p.y, eye.z - p.z);
// The xzp vector has the y component set to zero and is a projection
// of the view vector onto the xz plane.
Vector3f xzp = new Vector3f(view.x, 0, view.z);
// Once normalized, the components of the xzp vector will be direction cosines.
// So, xzp.z contains the direction cosine of the angle between xzp and the z-axis,
// and xzp.x contains the direction cosine of the angle between xzp and the x-axis.
// Consider that since xzp.y is zero, then xzp.x and xzp.y are two sides
// of a right triangle, and the magnitude of the xzp vector is the hypotenuse.
// Let theta be the angle between xzp and the z-axis (which is also the
// counter-clockwise rotation about the y axis that we want for our billboard).
// Using standard trigonometric ratios, we know that:
// cos(theta) = xzp.z / ||xzp||
// We can remove the ||xzp|| (magnitude) since it equals 1 (normalized vector)
// therefore, cos(theta) = xzp.z.
// Using the same approach, the trigonometric ratio for sine, would be:
// sin(theta) = xzp.x / ||xzp||
// So, xzp.z and xzp.x are the exact sines and cosines we need for the
// rotation about the y-axis, and we are done.
xzp.normalize();
// We have the sines and cosines for the rotation about the y axis, now
// we need to get the elevation (rotation about the x axis).
// The dot product of the normalized view and xzp vectors will provide
// the cosine of the angle between the two vectors.
// This cosine is related to the angle of elevation above the xz plane.
view.normalize();
float cosp = view.dot(xzp);
// We have the cosine, but for the rotation matrix we need the sine, as well.
// The sine of the elevation angle can be derived from the cosine using the
// trig identity: sine = sqrt( 1 - (cosine squared) )
// The sign must be negated when the view vector is looking up.
// (y > 0) (i.e. camera looking down)
float sinp = (float) Math.sqrt(1 - cosp * cosp) * (view.y > 0 ? -1 : 1);
// Prepopulate a 3x3 rotation matrix with the rotation around the y axis
// and the rotation around the x axis. This is how the matrix would wind
// up if we created a separate rotation matrix for the y axis rotation
// and the x axis rotation and then multiplied them together.
// This is quicker than actually doing a full matrix multiplication
// because the lack of a z axis rotation makes a lot of terms become
// zero and drop out.
return new Matrix3f(xzp.z, xzp.x * sinp, xzp.x * cosp, 0, cosp, -sinp, -xzp.x, xzp.z * sinp, xzp.z * cosp);
}
Comments?
Anyone care to check my math?
Should I add special code to handle angles close to zero?