javascriptmatrixrotationeuler-angles

Unable to find cause of why my implementation of Euler Rotation using Matrix in JavaScript does not work


I am trying to rotate an existing Euler Angle (XYZ) by an Input (XYZ) around static axis that don't move and have a result that is also in XYZ. In particular, I want to rotate multiple objects by the same XYZ input and have them all rotate in the same world space (so for example all of them are 10 degrees in the same world direction).

I have researched and learned that you use a rotation matrix for this and implemented a system to rotate my Angles, but somehow the values i get are not what i expected, at least not for all of them.

Here's my Matrix implementation:

class Matrix {
    #a1;
    #a2;
    #a3;
    #a4;
    #b2;
    #b1;
    #b3;
    #b4;
    #c1;
    #c2;
    #c3;
    #c4;
    #d1;
    #d2;
    #d3;
    #d4;
    
    /**
     * Constructor for Matrices
     * 
     * @param {number} a1 a1
     * @param {number} a2 a2
     * @param {number} a3 a3
     * @param {number} a4 a4
     * @param {number} b1 b1
     * @param {number} b2 b2
     * @param {number} b3 b3
     * @param {number} b4 b4
     * @param {number} c1 c1
     * @param {number} c2 c2
     * @param {number} c3 c3
     * @param {number} c4 c4
     * @param {number} d1 d1
     * @param {number} d2 d2
     * @param {number} d3 d3
     * @param {number} d4 d4
     */
    constructor(a1, a2, a3, a4, b1, b2, b3, b4, c1, c2, c3, c4, d1, d2, d3, d4) {
        this.#a1 = a1;
        this.#a2 = a2;
        this.#a3 = a3;
        this.#a4 = a4;
        this.#b1 = b1;
        this.#b2 = b2;
        this.#b3 = b3;
        this.#b4 = b4;
        this.#c1 = c1;
        this.#c2 = c2;
        this.#c3 = c3;
        this.#c4 = c4;
        this.#d1 = d1;
        this.#d2 = d2;
        this.#d3 = d3;
        this.#d4 = d4;
    }
    
    /**
     * Multiplies this Matrix with another one
     * 
     * @param {Matrix} otherMatrix The Matrix to multiply with
     * @returns {Matrix} The resulting Matrix
     */
    multiply(otherMatrix) {
        const a = this.toNestedArray();
        const b = otherMatrix.toNestedArray();
        
        /**@type {[[number]]} */
        const result = new Array(4);
        
        for(let r= 0; r < 4; r++) {
            result[r] = new Array(4);
            
            for(let c = 0; c < 4; c++) {
                result[r][c] = 0;
                for(let i = 0; i < 4; i++) {
                    result[r][c] += a[r][i] * b[i][c];
                }
            }
        }
        
        return Matrix.fromNestedArray(result);
    }
    
    /**
     * Get the Rotation Matrix for rotating around the X Axis
     * 
     * @param {number} angle Angle
     * @returns {Matrix} The Rotation Matrix
     */
    getRotationMatrixX(angle) {
        return new Matrix(
            1, 0, 0, 0,
            0, Math.cos(angle), -Math.sin(angle), 0,
            0, Math.sin(angle), Math.cos(angle), 0,
            0, 0, 0, 1,
        );
    }
    
    /**
     * Get the Rotation Matrix for rotating around the Y Axis
     * 
     * @param {number} angle Angle
     * @returns {Matrix} The Rotation Matrix
     */
    getRotationMatrixY(angle) {
        return new Matrix(
            Math.cos(angle), 0, Math.sin(angle), 0,
            0, 1, 0, 0,
            -Math.sin(angle), 0, Math.cos(angle), 0,
            0, 0, 0, 1,
        );
    }
    
    /**
     * Get the Rotation Matrix for rotating around the Z Axis
     * 
     * @param {number} angle Angle
     * @returns {Matrix} The Rotation Matrix
     */
    getRotationMatrixZ(angle) {
        return new Matrix(
            Math.cos(angle), -Math.sin(angle), 0, 0,
            Math.sin(angle), Math.cos(angle), 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1,
        );
    }
    
    /**
     * Returns an Array Representation of the Matrix
     * 
     * @returns {number[]} The Array
     */
    toArray() {
        return [
            this.#a1,
            this.#a2,
            this.#a3,
            this.#a4,
            this.#b1,
            this.#b2,
            this.#b3,
            this.#b4,
            this.#c1,
            this.#c2,
            this.#c3,
            this.#c4,
            this.#d1,
            this.#d2,
            this.#d3,
            this.#d4,
        ];
    }
    
    /**
     * Returns a Nested Array Representation of the Matrix
     * 
     * @returns {[[number]]} The Array
     */
    toNestedArray() {
        return [
            [
                this.#a1,
                this.#a2,
                this.#a3,
                this.#a4,
            ],
            [
                this.#b1,
                this.#b2,
                this.#b3,
                this.#b4,
            ],
            [
                this.#c1,
                this.#c2,
                this.#c3,
                this.#c4,
            ],
            [
                this.#d1,
                this.#d2,
                this.#d3,
                this.#d4,
            ],
        ];
    }
    
    /**
     * Converts this Matrix into an Rotation Array in Euler Degrees
     * 
     * @returns {number[]} The Rotation Array
     */
    toRotation() {
        const array = this.toNestedArray();
        
        const y = radiansToDegrees(Math.asin(clamp(array[2][0], -1, 1)));
        
        if(Math.abs(array[2][0]) < 0.9999999) {
            const x = radiansToDegrees(Math.atan2(-array[2][1], array[2][2]));
            const z = radiansToDegrees(Math.atan2(-array[1][0], array[0][0]));
            return [x, y, z];
        } else {
            const x = radiansToDegrees(Math.atan2(array[1][2], array[1][1]));
            return [x, y, 0];
        }
    }
    
    
    /**
     * Creates a Matrix from an Array
     * 
     * @param {[[number]]} array The Input Array
     * @returns {Matrix} The resulting Matrix
     */
     static fromArray(array) {
        return new Matrix(
            array[0],
            array[1],
            array[2],
            array[3],
            array[4],
            array[5],
            array[6],
            array[7],
            array[8],
            array[9],
            array[10],
            array[11],
            array[12],
            array[13],
            array[14],
            array[15],
        );
    }
    
    /**
     * Creates a Matrix from an nexted Array
     * 
     * @param {[[number]]} array The Input Array
     * @returns {Matrix} The resulting Matrix
     */
    static fromNestedArray(array) {
        return new Matrix(
            array[0][0],
            array[0][1],
            array[0][2],
            array[0][3],
            array[1][0],
            array[1][1],
            array[1][2],
            array[1][3],
            array[2][0],
            array[2][1],
            array[2][2],
            array[2][3],
            array[3][0],
            array[3][1],
            array[3][2],
            array[3][3],
        );
    }
}

/**
 * Clamps a Value
 * 
 * @param {number} value The Value to clamp
 * @param {number} min The Inclusive Min
 * @param {number} max The Inclusive Max
 * @returns {number} The Clamped Number
 */
function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
}

/**
 * Converts an angle from Radians to Degrees
 * 
 * @param {number} radians Radians
 * @returns {number} Degrees
 */
function radiansToDegrees(radians) {
    return radians * (180 / Math.PI);
}

/**
 * Converts an angle from Degrees to Radians
 * 
 * @param {number} degrees Degrees
 * @returns {number} Radians
 */
 function degreesToRadians(degrees) {
    return degrees * (Math.PI / 180);
}




const IDENTITY_MATRIX = new Matrix(
    1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
);

And here is the Code where i rotate:

const rotationMatrixX = IDENTITY_MATRIX.getRotationMatrixX(degreesToRadians(rotationInput[0]));
const rotationMatrixY = IDENTITY_MATRIX.getRotationMatrixY(degreesToRadians(rotationInput[1]));
const rotationMatrixZ = IDENTITY_MATRIX.getRotationMatrixZ(degreesToRadians(rotationInput[2]));

const relativeRotationMatrix = rotationMatrixX.multiply(rotationMatrixY).multiply(rotationMatrixZ);

const selectedRotationMatrixX = IDENTITY_MATRIX.getRotationMatrixX(degreesToRadians(initialRotation[0]));
const selectedRotationMatrixY = IDENTITY_MATRIX.getRotationMatrixY(degreesToRadians(initialRotation[1]));
const selectedRotationMatrixZ = IDENTITY_MATRIX.getRotationMatrixZ(degreesToRadians(initialRotation[2]));

const result = selectedRotationMatrixX.multiply(selectedRotationMatrixY).multiply(selectedRotationMatrixZ).multiply(relativeRotationMatrix);
const rotation = result.toRotation();

I have done some tests and the results i expect from them:

First Test:

initialRotation = [0, 0, 0];
rotationInput = [10, 10, 10];
The Output is, as expected: [10, 10, 10];

Second Test:

initialRotation = [10, -10, 10];
rotationInput = [0, 0, 10];
The Output is, as expected: [10, -10, 20];

Third Test:

initialRotation = [10, -10, 20];
rotationInput = [10, 0, 0];
The expected Output is: [19.651, -13.268, 18.049];
But the actual Output is: [19.452, -6.461, 21.357]

Fourth Test:

initialRotation = [0, 0, -90];
rotationInput = [10, 0, 0];
The expected Output is: [0, 10, -90];
But the actual Output is: [0, -10, -90]

So the Third Test produces a wildly different output and the Fourth Test produces an incorrectly signed y coordinate.

I just can't figure out what I've done wrong and why I'm getting wrong numbers


Solution

  • The correct solution:

    const rotationMatrixX = IDENTITY_MATRIX.getRotationMatrixX(degreesToRadians(rotationInput[0]));
    const rotationMatrixY = IDENTITY_MATRIX.getRotationMatrixY(degreesToRadians(rotationInput[1]));
    const rotationMatrixZ = IDENTITY_MATRIX.getRotationMatrixZ(degreesToRadians(rotationInput[2]));
    
    const relativeRotationMatrix = rotationMatrixZ.multiply(rotationMatrixY).multiply(rotationMatrixX);
    
    const selectedRotationMatrixX = IDENTITY_MATRIX.getRotationMatrixX(degreesToRadians(initialRotation[0]));
    const selectedRotationMatrixY = IDENTITY_MATRIX.getRotationMatrixY(degreesToRadians(initialRotation[1]));
    const selectedRotationMatrixZ = IDENTITY_MATRIX.getRotationMatrixZ(degreesToRadians(initialRotation[2]));
    
    const result = relativeRotationMatrix.multiply(selectedRotationMatrixZ).multiply(selectedRotationMatrixY).multiply(selectedRotationMatrixX);
    const rotation = result.toRotation();
    

    The const relativeRotationMatrix = ... and const result = ... were previously incorrect.