19 February 2017

Math - Calculate a look-at matrix for OpenGL

The final piece of the puzzle to get my basic OpenGL scene up and running was the camera or look-at matrix. It is calculated from a position vector and a direction vector; the look-at vector.

The position and direction vectors

Here's how the position and direction vectors are calculated. The input comes from the mouse and keyboard. Note that I do use some extension methods to make life easier. Delta time is a time-based value to get the same speed on slow and fast computers.

rotationHorizontal += window.MouseMovement.X * 0.1f;
rotationHorizontal = rotationHorizontal.Circular(0, 359);
rotationVertical += window.MouseMovement.Y * 0.1f;
rotationVertical = rotationVertical.Constrain(-89, 89);

float h = rotationHorizontal.ToRadians();
float v = rotationVertical.ToRadians();
float hs = h.Sin();
float hc = h.Cos();
float vs = v.Sin();
float vc = v.Cos();
float speed = 0.005f * deltaTime;

if (window.KeyDown(Key.Up))
{
    position.X -= hc * speed;
    position.Z -= hs * speed;
}

if (window.KeyDown(Key.Down))
{
    position.X += hc * speed;
    position.Z += hs * speed;
}

if (window.KeyDown(Key.Left))
{
    position.X -= hs * speed;
    position.Z += hc * speed;
}

if (window.KeyDown(Key.Right))
{
    position.X += hs * speed;
    position.Z -= hc * speed;
}

var direction = new Vector(vc * hc, vs, vc * hs);
var camera = new Matrix(position, direction);

The look-at matrix

This one is again implemented as a constructor of the Matrix class. It uses some basic vector operations; the implementations of which can easily be found online.

public Matrix(Vector position, Vector direction)
{
    var up = new Vector(0, 1, 0);
    var zAxis = direction.Normalized();
    var xAxis = Vector.CrossProduct(up.Normalized(), zAxis).Normalized();
    var yAxis = Vector.CrossProduct(zAxis, xAxis);

    var translation = new Matrix();
    translation.Translate(position.Negated());

    var rotation = new Matrix();
    rotation.M[0, 0] = xAxis.X;
    rotation.M[0, 1] = xAxis.Y;
    rotation.M[0, 2] = xAxis.Z;

    rotation.M[1, 0] = yAxis.X;
    rotation.M[1, 1] = yAxis.Y;
    rotation.M[1, 2] = yAxis.Z;

    rotation.M[2, 0] = zAxis.X;
    rotation.M[2, 1] = zAxis.Y;
    rotation.M[2, 2] = zAxis.Z;

    var lookAt = rotation * translation;
    SetMatrix(lookAt.M);
}

See also

Simplified method to create a look-at matrix
Calculate a perspective projection matrix for OpenGL

09 February 2017

Math - Calculate a perspective projection matrix for OpenGL

Another method that took a while to find was the one that calculates a perspective projection matrix from a given field of view, aspect ratio and near and far plane to work with OpenGL. This constructor for a Matrix object does it though:

public Matrix(float fov, float aspect, float near, float far)
{
    Values = new float[4, 4];

    float scale = (float)Math.Tan(fov.ToRadians() * 0.5f) * near;
    float right = aspect * scale;
    float left = -right;
    float top = scale;
    float bottom = -top;

    Values[0, 0] = 2f * near / (right - left);
    Values[1, 1] = 2f * near / (top - bottom);
    Values[0, 2] = (right + left) / (right - left);
    Values[1, 2] = (top + bottom) / (top - bottom);
    Values[2, 2] = -(far + near) / (far - near);
    Values[3, 2] = -1f;
    Values[2, 3] = -2f * far * near / (far - near);
}

Note that this class uses row-major order, while OpenGL uses column-major order. When I pass my matrices to the glUniformMatrix4fv method, I set it's transpose parameter to True.

See also

Calculate a look-at matrix for OpenGL

04 February 2017

Math – Using quaternions to create a rotation matrix

The best way I found to create a rotation matrix with all three axes is with quaternions. The following code is C#:

Initialize

The Quaternion class has four properties: W, X, Y and Z. "W" is the angle, and the other represent the axis. I will only use unit quaternions, meaning the rotation is around one of the three axes only. You initialize an identity quaternion by setting them to (1, 0, 0, 0). I create one for each rotation like so:

var rotx = new Quaternion(Rotation.X, 1, 0, 0);
var roty = new Quaternion(Rotation.Y, 0, 1, 0);
var rotz = new Quaternion(Rotation.Z, 0, 0, 1);

The quaternion's constructor initializes those arguments as:

double r = (angle * (Math.PI / 180));
W = (float)Math.Cos(r / 2);

double s = Math.Sin(r / 2);
X = (float)(x * s);
Y = (float)(y * s);
Z = (float)(z * s);

Multiply

Next, we combine them all into one quaternion:

var rot = rotx * roty * rotz;

The method that multiplies two quaternions is defined as follows:

public static Quaternion operator *(Quaternion a, Quaternion b)
{
    var c = new Quaternion();
    c.W = a.W * b.W - a.X * b.X - a.Y * b.Y - a.Z * b.Z;
    c.X = a.W * b.X + a.X * b.W + a.Y * b.Z - a.Z * b.Y;
    c.Y = a.W * b.Y - a.X * b.Z + a.Y * b.W + a.Z * b.X;
    c.Z = a.W * b.Z + a.X * b.Y - a.Y * b.X + a.Z * b.W;
    return c;
}

Rotate

To get the matrix I needed to pass to OpenGL, I created another class called "Matrix", which is a 4x4 matrix containing a 16 element array. Then a method takes the quaternion and adds the rotation to this matrix. It took a while to find a way to do this that worked with OpenGL, but I finally found it in the .net framework.

var matrix = new Matrix();
matrix.Rotate(rot); //"rot" being the quaternion from before.

public void Rotate(Quaternion rotation)
{
    float x2 = rotation.X + rotation.X;
    float y2 = rotation.Y + rotation.Y;
    float z2 = rotation.Z + rotation.Z;

    float xx = rotation.X * x2;
    float xy = rotation.X * y2;
    float xz = rotation.X * z2;
    float yy = rotation.Y * y2;
    float yz = rotation.Y * z2;
    float zz = rotation.Z * z2;
    float wx = rotation.W * x2;
    float wy = rotation.W * y2;
    float wz = rotation.W * z2;

    Values[0, 0] = 1.0f - (yy + zz);
    Values[0, 1] = xy + wz;
    Values[0, 2] = xz - wy;

    Values[1, 0] = xy - wz;
    Values[1, 1] = 1.0f - (xx + zz);
    Values[1, 2] = yz + wx;

    Values[2, 0] = xz + wy;
    Values[2, 1] = yz - wx;
    Values[2, 2] = 1.0f - (xx + yy);
}

See also

Align a rotation matrix with a normal