Rubik's Cube: cube and faces rotation

Hi! I’m doing a virtual model of the Rubik’s cube with WebGL.

I have already asked for help on StackOverflow for this problem with no luck (stackoverflow.com/questions/43015633/rubiks-cube-with-webgl-faces-rotation), so I decided to try also here.

Up to this time I am able to draw the entire cube, to define the rotations of the cube relative to the axes x, y, z and those of individual faces relative to the respective normal.

The problem comes about when I compose these rotations on different faces, in other words, when I want to mix the cube.

You can test the application at this page (please copy and paste the following in your browser address bar): giacomogarbin.altervista.org/stackoverflow/RotateFace.html.
The commands to rotate the cube and faces are the following:

[ul]
[li]press X (SHIFT+X) to rotate the cube than the x-axis in counterclockwise (clockwise)
[/li][li]press Y (SHIFT+Y) to rotate the cube than the y-axis in counterclockwise (clockwise)
[/li][li]press Z (SHIFT+Z) to rotate the cube than the x-axis in counterclockwise (clockwise)
[/li][li]press R (SHIFT+R) to rotate the right face in counterclockwise (clockwise)
[/li][li]press U (SHIFT+U) to rotate the top face in counterclockwise (clockwise)
[/li][li]press F (SHIFT+F) to rotate the front face in counterclockwise (clockwise)
[/li][li]press B (SHIFT+B) to rotate the rear face in counterclockwise (clockwise)
[/li][li]press D (SHIFT+D) to rotate the bottom face in counterclockwise (clockwise)
[/li][li]press L (SHIFT+L) to rotate the left face in counterclockwise (clockwise)
[/li][/ul]

Please note that a face is rotated by 30 degrees at each rotation, so it is necessary to press the respective key three times to complete the rotation before starting another one.

You can find the entire javascript code here: giacomogarbin.altervista.org/stackoverflow/RotateFace.js.
I report below only the significant parts to the problem.

The whole cube is made up of 27 cubes, each of them described by the following structure.

cube = {
    VertexBuffer,   // the position of the eight vertices that define the cube
    colorBuffer,    // the colors of the cube faces
    rotationMatrix, // the rotation matrix of the cube
    position,       // the relative position of the cube respect to the entire cube,
                    // identified by a triplet of coordinates (x, y, z) where
                    // the values ​​of x, y, z belong to {-1, 0, +1}
    type            // the type of the cube between {vertex, edge, center, kernel}
}

The information of each cube are collected in the array cubes.

var cubes = new Array(27);

The whole cube is drawn with the help of the following functions.

var drawCube = function (obj) {
    // handle the vertex buffer
    // ...

    // handle the color buffer
    // ...

    // mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix).multiply(obj.rotationMatrix);
    mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(obj.rotationMatrix).multiply(modelMatrix);
    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

    gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
};

var draw = function () {
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    cubes.forEach(drawCube);
};

mvpMatrix is the model view projection matrix.

Whenever the whole cube is rotated about an axis (x, y, or z), it is invoked the rotateCubeAnimation function that handles the respective rotation.

const axisX = 0;
const axisY = 1;
const axisZ = 2;

var rotateCubeAnimation = function (angle, axis) {
    var speed = 20;
    var currentAngle = angle / speed;
    var counter = 0;

    var tick = function () {
        var rotationMatrix = new Matrix4();
        switch (axis) {
            case axisX:
                rotationMatrix.setRotate(currentAngle, 1, 0, 0);
                break;
            case axisY:
                rotationMatrix.setRotate(currentAngle, 0, 1, 0);
                break;
            case axisZ:
                rotationMatrix.setRotate(currentAngle, 0, 0, 1);
                break;
        }
        modelMatrix = rotationMatrix.multiply(modelMatrix);
        draw();

        if (++counter < speed)
            requestAnimationFrame(tick);
        else {
            // update cubes position
            cubes.forEach(function (cube) {
            switch (axis) {
                    case axisX:
                        if (angle > 0) {
                            var tmp = cube.position.y;
                            cube.position.y = -cube.position.z;
                            cube.position.z = +tmp;
                        } else {
                            var tmp = cube.position.y;
                            cube.position.y = +cube.position.z;
                            cube.position.z = -tmp;
                        }
                        break;
                    case axisY:
                        if (angle > 0) {
                            var tmp = cube.position.x;
                            cube.position.x = +cube.position.z;
                            cube.position.z = -tmp;
                        } else {
                            var tmp = cube.position.x;
                            cube.position.x = -cube.position.z;
                            cube.position.z = +tmp;
                        }
                        break;
                    case axisZ:
                        if (angle > 0) {
                            var tmp = cube.position.x;
                            cube.position.x = -cube.position.y;
                            cube.position.y = +tmp;
                        } else {
                            var tmp = cube.position.x;
                            cube.position.x = +cube.position.y;
                            cube.position.y = -tmp;
                        }
                        break;
                }
            });

            rotating = false;   // rotation complete
        }
    };

    tick();
};

This function applies the corresponding rotation of the entire cube, performing a short animation, and after which updates the relative positions of each cube with respect to the whole cube.

Whenever a face is rotated, it is invoked the faceRotation function that handles the respective rotation.

const faceR = 0;    // right
const faceU = 1;    // up
const faceF = 2;    // front
const faceB = 3;    // back
const faceD = 4;    // down
const faceL = 5;    // left

var faceRotation = function (angle, face) {
    var rotationMatrix = new Matrix4();

    var axisXHandler = function (cube) {
        if (cube.position.x == this) {
            rotationMatrix.setRotate(angle, 1, 0, 0);
            cube.rotationMatrix = rotationMatrix.multiply(cube.rotationMatrix);
            // cube.rotationMatrix = cube.rotationMatrix.multiply(rotationMatrix);

            // update cube position
            if (angle > 0) {
                var tmp = cube.position.y;
                cube.position.y = -cube.position.z;
                cube.position.z = +tmp;
            } else {
                var tmp = cube.position.y;
                cube.position.y = +cube.position.z;
                cube.position.z = -tmp;
            }
        }
    }

    var axisYHandler = function (cube) {
        if (cube.position.y == this) {
            rotationMatrix.setRotate(angle, 0, 1, 0);
            cube.rotationMatrix = rotationMatrix.multiply(cube.rotationMatrix);
            // cube.rotationMatrix = cube.rotationMatrix.multiply(rotationMatrix);

            // update cube position
            if (angle > 0) {
                var tmp = cube.position.x;
                cube.position.x = +cube.position.z;
                cube.position.z = -tmp;
            } else {
                var tmp = cube.position.x;
                cube.position.x = -cube.position.z;
                cube.position.z = +tmp;
            }
        }
    }

    var axisZHandler = function (cube) {
        if (cube.position.z == this) {
            rotationMatrix.setRotate(angle, 0, 0, 1);
            cube.rotationMatrix = rotationMatrix.multiply(cube.rotationMatrix);
            // cube.rotationMatrix = cube.rotationMatrix.multiply(rotationMatrix);

            // update cube position
            if (angle > 0) {
                var tmp = cube.position.x;
                cube.position.x = -cube.position.y;
                cube.position.y = +tmp;
            } else {
                var tmp = cube.position.x;
                cube.position.x = +cube.position.y;
                cube.position.y = -tmp;
            }
        }
    }

    switch (face) {
        case faceR:
            cubes.forEach(axisXHandler, +1);
            break;
        case faceU:
            cubes.forEach(axisYHandler, +1);
            break;
        case faceF:
            cubes.forEach(axisZHandler, +1);
            break;
        case faceB:
            cubes.forEach(axisZHandler, -1);
            break;
        case faceD:
            cubes.forEach(axisYHandler, -1);
            break;
        case faceL:
            cubes.forEach(axisXHandler, -1);
            break;
    }

    draw();
};

This function updates the rotation matrix of each cube according to the selected face and the relative position of the cube with respect to the whole cube.

I believe that the problem may reside in the order of multiplication of rotation matrices; I left comments with various attempts experimented, none of which solved the problem.

In a way I improved the application by using the rotation matrices derived from quaternions. This is the function that generates the new rotation matrices.

var quaternionRotationMatrix = function (axis, angle) {
    // angle = -1 * angle;
    var radians = angle * Math.PI/180;
    var c = Math.cos(radians);
    var s = Math.sin(radians);

    switch (axis) {
        case axisX:
            var aX = 1, aY = 0, aZ = 0;
            break;
        case axisY:
            var aX = 0, aY = 1, aZ = 0;
            break;
        case axisZ:
            var aX = 0, aY = 0, aZ = 1;
            break;
    }

    var aX2 = Math.pow(aX, 2);
    var aY2 = Math.pow(aY, 2);
    var aZ2 = Math.pow(aZ, 2);

    var obj = new Object();
    obj.elements = new Float32Array([
             c + aX2*(1-c),  aX*aY*(1-c) - aZ*s,  aX*aZ*(1-c) + aY*s,  0.0,
        aY*aX*(1-c) + aZ*s,       c + aY2*(1-c),  aY*aZ*(1-c) - aX*s,  0.0,
        aZ*aX*(1-c) - aY*s,  aZ*aY*(1-c) + aX*s,       c + aZ2*(1-c),  0.0,
                       0.0,                 0.0,                 0.0,  1.0
    ]);

    // return (new Matrix4(obj)).transpose();
    return new Matrix4(obj);
};

In this version of the application - you can find links to both the application and the complete javascript code here below - I replaced the simple rotation matrices in the functions rotateCubeAnimation and faceRotation with those generated by the function quaternionRotationMatrix.

[ul]
[li]application: giacomogarbin.altervista.org/stackoverflow/RotateFaceQuaternion.html
[/li][li]javascript code: giacomogarbin.altervista.org/stackoverflow/RotateFaceQuaternion.js
[/li][/ul]

The result improves slightly, since it is now possible from the starting position (without then rotate the cube along the axes x, y, or z) to mix the cube in pleasure and the rotations of the faces lead to the expected result.

Please note again that you need press three times in a row the same key to complete the rotation of a face before starting another one.

In addition, even after having mixed the cube - keeping it still in the starting position - the successive cube rotations along axes x, y, and z lead to the expected result.

Arrived at this point however, if are carried further rotations of the faces, the application produces an unexpected result and then incorrectly.

If it were possible I would prefer to arrive at a correct solution without the use of quaternions and keep this solution method as a viable alternative.

Finally, let me point out as with the use of quaternions the direction of all rotations (both of the cube and of the faces) appears reversed. I believe this is due to the fact that WebGL is column major order, while the function quaternionRotationMatrix returns a matrix in row major order. I then tried to transpose the matrix before returning it or simply to change the direction of rotation (see the comments), in doing so we obtain the rotation in the correct direction but unfortunately also unexpected results in the combination of faces rotations.

Thank you very much for helping!