Search code examples
opengllwjgllinear-algebra

OpenGL Java Matrix4f implementation


I'm trying to write a 4*4 float matrix class to create a 3D space with model, view, and projection matrices. In its current state when i try to rotate the view matrix, it seems to also apply translation, and the space gets distorted (as if it was squeezed). The projection, view, and model matrix multiplication is done in the vertex shader.

Edit5: The non-functioning state of the transformation functions is found below:

public class Mat4f {
public float m00, m10, m20, m30,
             m01, m11, m21, m31,
             m02, m12, m22, m32,
             m03, m13, m23, m33;

public Mat4f() {
    loadIdentity();
}

public Mat4f loadIdentity() {
    m00 = 1.0f; m10 = 0.0f; m20 = 0.0f; m30 = 0.0f;
    m01 = 0.0f; m11 = 1.0f; m21 = 0.0f; m31 = 0.0f;
    m02 = 0.0f; m12 = 0.0f; m22 = 1.0f; m32 = 0.0f;
    m03 = 0.0f; m13 = 0.0f; m23 = 0.0f; m33 = 1.0f;
    return this;
}

public Mat4f store(FloatBuffer buffer) {
    buffer.put(m00);
    buffer.put(m01);
    buffer.put(m02);
    buffer.put(m03);

    buffer.put(m10);
    buffer.put(m11);
    buffer.put(m12);
    buffer.put(m13);

    buffer.put(m20);
    buffer.put(m21);
    buffer.put(m22);
    buffer.put(m23);

    buffer.put(m30);
    buffer.put(m31);
    buffer.put(m32);
    buffer.put(m33);

    buffer.flip();

    return this;
}

public Mat4f loadPerspective(float fov, float ratio, float near, float far) {
    m11 = (float) (1.0f / (Math.tan(fov / 2.0f)));
    m00 = m11 / ratio;
    m22 = -(far + near) / (far - near);
    m23 = -1.0f;
    m32 = -2.0f * far * near / (far - near);
    m33 = 0.0f;
    return this;
}

public Mat4f translate(float x, float y, float z) {
    m30 = x;
    m31 = y;
    m32 = z;
    return this;
}

public Mat4f scale(float x, float y, float z) {
    m00 = x;
    m11 = y;
    m22 = z;
    return this;
}

public Mat4f rotateX(float x) {
    m11 = (float) Math.cos(x);
    m12 = (float) Math.sin(x);
    m21 = (float) -(Math.sin(x));
    m22 = (float) Math.cos(x);
    return this;
}

public Mat4f rotateY(float y) {
    m00 = (float) Math.cos(y);
    m02 = (float) -(Math.sin(y));
    m20 = (float) Math.sin(y);
    m22 = (float) Math.cos(y);
    return this;
}

public Mat4f rotateZ(float z) {
    m00 = (float) Math.cos(z);
    m01 = (float) Math.sin(z);
    m10 = (float) -(Math.sin(z));
    m11 = (float) Math.cos(z);
    return this;
}
}

And the proper way to do those is as follows:

public Mat4f translate(float x, float y, float z, Mat4f dest) {
    dest.m00 = m00;
    dest.m01 = m01;
    dest.m02 = m02;
    dest.m03 = m03;
    dest.m10 = m10;
    dest.m11 = m11;
    dest.m12 = m12;
    dest.m13 = m13;
    dest.m20 = m20;
    dest.m21 = m21;
    dest.m22 = m22;
    dest.m23 = m23;
    dest.m30 = m00 * x + m10 * y + m20 * z + m30;
    dest.m31 = m01 * x + m11 * y + m21 * z + m31;
    dest.m32 = m02 * x + m12 * y + m22 * z + m32;
    dest.m33 = m03 * x + m13 * y + m23 * z + m33;
    return this;
}

public Mat4f translate(float x, float y, float z) {
    return translate(x, y, z, this);
}

public Mat4f scale(float x, float y, float z, Mat4f dest) {
    dest.m00 = m00 * x;
    dest.m01 = m01 * x;
    dest.m02 = m02 * x;
    dest.m03 = m03 * x;
    dest.m10 = m10 * y;
    dest.m11 = m11 * y;
    dest.m12 = m12 * y;
    dest.m13 = m13 * y;
    dest.m20 = m20 * z;
    dest.m21 = m21 * z;
    dest.m22 = m22 * z;
    dest.m23 = m23 * z;
    dest.m30 = m30;
    dest.m31 = m31;
    dest.m32 = m32;
    dest.m33 = m33;
    return this;
}

public Mat4f scale(float x, float y, float z) {
    return scale(x, y, z, this);
}

public Mat4f rotateX(float x, Mat4f dest) {
    float cos = (float) Math.cos(x);
    float sin = (float) Math.sin(x);
    float rm11 = cos;
    float rm12 = sin;
    float rm21 = -sin;
    float rm22 = cos;

    float nm10 = m10 * rm11 + m20 * rm12;
    float nm11 = m11 * rm11 + m21 * rm12;
    float nm12 = m12 * rm11 + m22 * rm12;
    float nm13 = m13 * rm11 + m23 * rm12;

    dest. m20 = m10 * rm21 + m20 * rm22;
    dest.m21 = m11 * rm21 + m21 * rm22;
    dest.m22 = m12 * rm21 + m22 * rm22;
    dest. m23 = m13 * rm21 + m23 * rm22;

    dest.m10 = nm10;
    dest.m11 = nm11;
    dest.m12 = nm12;
    dest.m13 = nm13;

    return this;
}

public Mat4f rotateX(float x) {
    return rotateX(x, this);
}

public Mat4f rotateY(float y, Mat4f dest) {
    float cos = (float) Math.cos(y);
    float sin = (float) Math.sin(y);
    float rm00 = cos;
    float rm02 = -sin;
    float rm20 = sin;
    float rm22 = cos;

    float nm00 = m00 * rm00 + m20 * rm02;
    float nm01 = m01 * rm00 + m21 * rm02;
    float nm02 = m02 * rm00 + m22 * rm02;
    float nm03 = m03 * rm00 + m23 * rm02;

    dest.m20 = m00 * rm20 + m20 * rm22;
    dest.m21 = m01 * rm20 + m21 * rm22;
    dest.m22 = m02 * rm20 + m22 * rm22;
    dest.m23 = m03 * rm20 + m23 * rm22;

    dest.m00 = nm00;
    dest.m01 = nm01;
    dest.m02 = nm02;
    dest.m03 = nm03;

    return this;
}

public Mat4f rotateY(float y) {
    return rotateY(y, this);
}

public Mat4f rotateZ(float z, Mat4f dest) {
    float cos = (float) Math.cos(z);
    float sin = (float) Math.sin(z);
    float rm00 = cos;
    float rm01 = sin;
    float rm10 = -sin;
    float rm11 = cos;

    float nm00 = m00 * rm00 + m10 * rm01;
    float nm01 = m01 * rm00 + m11 * rm01;
    float nm02 = m02 * rm00 + m12 * rm01;
    float nm03 = m03 * rm00 + m13 * rm01;

    dest.m10 = m00 * rm10 + m10 * rm11;
    dest.m11 = m01 * rm10 + m11 * rm11;
    dest.m12 = m02 * rm10 + m12 * rm11;
    dest.m13 = m03 * rm10 + m13 * rm11;

    dest.m00 = nm00;
    dest.m01 = nm01;
    dest.m02 = nm02;
    dest.m03 = nm03;

    return this;
}

public Mat4f rotateZ(float z) {
    return rotateZ(z, this);
}

To modify the matrices I used the following order of transformations:

public void transform() {
    mMat.loadIdentity();
    mMat.translate(position.x, position.y, position.z);
    mMat.rotateX((float) Math.toRadians(orientation.x));
    mMat.rotateY((float) Math.toRadians(orientation.y));
    mMat.rotateZ((float) Math.toRadians(orientation.z));
    mMat.scale(scale.x, scale.y, scale.z);
}

public void updateCamera() {
    Vec3f position = World.camera.getPosition();
    vMat.loadIdentity();
    vMat.rotateX((float) Math.toRadians(World.camera.getPitch()));
    vMat.rotateY((float) Math.toRadians(World.camera.getYaw()));
    vMat.translate(-position.x, -position.y, position.z);
}

Edit: the perspective projection is fine, and so is the translation, but if i store the model matrix in a Mat4f, then models' rotation follows the camera's one.

Edit2: The model's orientation no longer follows the camera rotation when I use Mat4f as a model matrix. The projection matrix, translation, and scaling works well.

Edit3: Edited the code, the rotation applied is not a full circle rotation, the model swings left and right.

Edit4: I have attempted doing the rotation with matrix multiplication


Solution

  • Your matrix class does not implement the composition of transformations. Each of your matrix method just overwrites certain elements.

    For example, look at the following sequenc of operations:

    translate(1,1,1);
    rotateX(45);
    translate(1,1,1);
    

    Such a sequence mathematically should be represented by the matrix multiplication T * R * T (where T and R are the basic translate / rotate matrices with the respective parameters).

    However, what your code does is just creating T (because initially, the matrix is identity), overwriting some parts for the rotation, and finally overwriting the translation part again.

    To fix your code, you need to implement a proper matrix multipilcation method, and actually use it whenever you want to apply a further transformation to the matrix. It might be helpful to create some methods for the basic transformation matrices.

    However, this still looks like the usual row major vs column major matrix storage layout order to me.

    Your layout of the mXY members suggest that you use mathematical convention where m12 would be second row, third column. Your store method then does put the matrix into a buffer with column major layout. Such a buffer could be directly used by legacy GL's matrix functions (like glLoadMatrix or glMultMatrix). It would also be the "standard" layout for matrix uniforms, where the transpose parameter is set to GL_FALSE.

    However, looking at your translate method:

    public Mat4f translate(float x, float y, float z) {
        m30 = x;
        m31 = y;
        m32 = z;
       return this;
    }
    

    This would set the translation vector to the last row, not the last column. (All the other functions also seem to use a transposed layout).

    Now legacy GL uses the matrix * vector convention (as opposed to vector * matrix which D3D prefers). In that case, your matrices are transposed to what the GL expects. If you use shaders, it is up to you which multiplication order you use, though - but it must match the convention you are using for your matrices, and M * v == v^T * M^T.