TWGL Camera rotation not expected

168 Views Asked by At

I'm so confused,

I am trying to use mouse and keyboard controls (With WebGL2), and I'm trying to rotate and move the camera with these four functions:

m4.rotateX(m, angleInRadians, dst);
m4.rotateY(m, angleInRadians, dst);
m4.rotateX(m, angleInRadians, dst);
m4.translate(m, v, dst);

(Provided by twgl) And it works fine with X, and Y (When seperate), but when added it provides weird output (As of a gyroscope) Heres what I use to rotate the camera and create the scene:

(() => {
    class Camera {
        static setPosRot(x, y, z, cx, cy, cz, camera) {
/*
x,y,z = Players X,Y,Z Position
cx,cy,cz = Players X,Y,Z Rotation
*/
            m4.identity(camera);
            m4.translate(camera, [
                x,
                y,
                z
            ], camera);
            m4.rotateX(camera, ((cx - 90) * Math.PI) / 180, camera);
            m4.rotateY(camera, (cy * Math.PI) / 180, camera);
            m4.rotateZ(camera, (cz * Math.PI) / 180, camera);
            return camera;
        }
    }

    class Block {
        #texture;
        #x;
        #y;
        #z;

        set texture(texture) {
            this.#texture = texture;
        }

        get getTexture() {
            return texture;
        }

        set x(x = 0) {
            x = parseInt(x);
            if (isNaN(x)) {
                console.warn(`Invalid X pos, exiting`);
                return;
            }
            this.#x = x;
            return;
        }

        set y(y = 0) {
            y = parseInt(y);
            if (isNaN(y)) {
                console.warn(`Invalid Y pos, exiting`);
                return;
            }
            this.#y = y;
            return;
        }

        set z(z = 0) {
            z = parseInt(z);
            if (isNaN(z)) {
                console.warn(`Invalid Z pos, exiting`);
                return;
            }
            this.#z = z;
            return;
        }

        get getX() {
            return this.#x;
        }

        get getY() {
            return this.#y;
        }

        get getZ() {
            return this.#z;
        }

        set pos(pos = [
            0,
            0,
            0
        ]) {
            var x = parseInt(pos[0]);
            var y = parseInt(pos[1]);
            var z = parseInt(pos[2]);
            if (isNaN(x)) {
                console.warn(`Invalid X pos, setting to default.`);
                x = 0;
            }
            if (isNaN(y)) {
                console.warn(`Invalid Y pos, setting to default.`);
                y = 0;
            }
            if (isNaN(z)) {
                console.warn(`Invalid Z pos, setting to default.`);
                z = 0;
            }
            this.#x = x;
            this.#y = y;
            this.#z = z;
            return;
        }

        get getPos() {
            return [
                this.#x,
                this.#y,
                this.#z
            ]
        }

        render() {
            m4.identity(Render.world);
            m4.translate(Render.world, [
                this.#x * 3,
                this.#y * 3,
                this.#z * 3
            ], Render.world);

            m4.multiply(Render.viewProjection, Render.world, Render.worldViewProjection);
            m4.inverse(Render.world, Render.worldInverse);
            m4.transpose(Render.worldInverse, Render.worldInverseTranspose);

            twgl.setBuffersAndAttributes(Render.gl, Render.pinfo, Render.binfo);
            twgl.setUniforms(Render.pinfo, {
                u_worldViewProjection: Render.worldViewProjection,
                u_worldInverseTranspose: Render.worldInverseTranspose,
                u_color: [
                    (this.#x + 2) / 3,
                    (this.#y + 2) / 3,
                    (this.#z + 2) / 3,
                    1
                ],
                u_lightDir: Render.lightDir,
            });
            twgl.drawBufferInfo(Render.gl, Render.binfo);
        }

        constructor(x = 0, y = 0, z = 0) {
            var x = parseInt(x);
            var y = parseInt(y);
            var z = parseInt(z);
            if (isNaN(x)) {
                console.warn(`Invalid X pos, setting to default.`);
                x = 0;
            }
            if (isNaN(y)) {
                console.warn(`Invalid Y pos, setting to default.`);
                y = 0;
            }
            if (isNaN(z)) {
                console.warn(`Invalid Z pos, setting to default.`);
                z = 0;
            }
            this.#x = x;
            this.#y = y;
            this.#z = z;
            return;
        }
    }

    class Render {
        static vertexShader = [
`precision highp float;`,
`uniform mat4 u_worldViewProjection;`,
`uniform mat4 u_worldInverseTranspose;`,
`attribute vec4 position;`,
`attribute vec3 normal;`,
`varying vec3 v_normal;`,
`void main() {`,
    `gl_Position = u_worldViewProjection * position;`,
    `v_normal = (u_worldInverseTranspose * vec4(normal, 0)).xyz;`,
`}`
        ].join(`\n`);

        static fragmentShader = [
`precision highp float;`,
`varying vec3 v_normal;`,
`uniform vec3 u_lightDir;`,
`uniform vec4 u_color;`,
`void main() {`,
`vec3 norm = normalize(v_normal);`,
    `float light = dot(u_lightDir, norm) * .5 + .5;`,
    `gl_FragColor = vec4(u_color.rgb * light, u_color.a);`,
`}`
        ].join(`\n`);

        static canvas;
        static gl;
        static gl2d;
        static program;

        static width = 1920;
        static height = 1080;

        static pinfo;
        static binfo;

        static #cx = 90;
        static #cy = 0;
        static #cz = 0;

        static #x = 0.0;
        static #y = 0.0;
        static #z = 0.0;

        static webgloptions = {
            powerPreference: `default`,
            antialias: true,
            depth: true
        };

        static load() {
this.width = document.documentElement.clientWidth;
            this.height = document.documentElement.clientHeight;
            this.canvas = document.createElement(`canvas`);
            this.canvas.id = `root`;
            this.canvas.width = this.width;
            this.canvas.height = this.height;
            this.canvas.style.width = this.width;
            this.canvas.style.height = this.height;
            document.body.appendChild(this.canvas);
            this.canvas = document.getElementById(`root`);
            this.canvas.addEventListener(`contextmenu`, event => event.preventDefault());
            this.gl = this.canvas.getContext(`webgl2`, this.webgloptions);
            this.gl2d = this.canvas.getContext(`2d`, this.webgloptions); // Rendering UI
            if (this.gl == null || this.gl == undefined || !this.gl) {
                console.warn(`WebGL2 not supported, attempting to use WebGL, expect bugs.`);
                this.gl = this.canvas.getContext(`webgl`, this.webgloptions) || this.canvas.getContext(`experimental-webgl`, this.webgloptions);
                if (this.gl == null || this.gl == undefined || !this.gl) {
                    console.error(`Neither WebGL nor WebGL2 is supported, exiting`);
                    return true;
                }
            }
            this.pinfo = twgl.createProgramInfo(this.gl, [
                this.vertexShader,
                this.fragmentShader
            ]);
            this.binfo = twgl.primitives.createCubeBufferInfo(this.gl, 1);
            this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
            this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
            return false;
        }

        static projection = m4.identity();
        static camera = m4.identity();
        static view = m4.identity();
        static viewProjection = m4.identity();
        static world = m4.identity();
        static worldViewProjection = m4.identity();
        static worldInverse = m4.identity();
        static worldInverseTranspose = m4.identity();
        static lightDir = v3.normalize([1, 2, 3]);

        static render() {
            requestAnimationFrame(this.render.bind(this));
            var now = Date.now() * 0.001;
            this.#deltaTime = now - this.#then;
            this.#then = now;
            this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
            this.gl.clearDepth(1.0);
            this.gl.viewport(0, 0, this.width, this.height);
            this.gl.enable(this.gl.DEPTH_TEST);
            this.gl.enable(this.gl.CULL_FACE);
            this.gl.depthFunc(this.gl.LEQUAL);
            this.gl.useProgram(this.pinfo.program);
            this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
            const fov = (this.#fov * Math.PI) / 180;
            const aspect = this.gl.canvas.clientWidth / this.gl.canvas.clientHeight;
            m4.perspective(fov, aspect, 0.0, 100.0, this.projection);
            /*
            m4.axisRotate(this.camera, m4.getAxis(this.camera, 0), (this.#cx - 90) * Math.PI / 180, this.camera);
            m4.axisRotate(this.camera, m4.getAxis(this.camera, 1), this.#cy * Math.PI / 180, this.camera);
            m4.axisRotate(this.camera, m4.getAxis(this.camera, 2), this.#cz * Math.PI / 180, this.camera);
            */
            /*
            m4.rotateX(this.camera, ((this.#cx - 90) * Math.PI) / 180, this.camera);
            m4.rotateY(this.camera, (this.#cy * Math.PI) / 180, this.camera);
            m4.rotateZ(this.camera, (this.#cz * Math.PI) / 180, this.camera);
            */

            this.camera = Camera.setPosRot(this.#x, this.#y, this.#z, this.#cx, this.#cy, this.#cz, this.camera);

            m4.inverse(this.camera, this.view);

            m4.multiply(this.projection, this.view, this.viewProjection);

            for (let z = -1; z <= 1; ++z) {
                for (let y = -1; y <= 1; ++y) {
                    for (let x = -1; x <= 1; ++x) {
                        if (x === 0 && y === 0 && z === 0) {
                            continue;
                        }

                        const block = new Block();
                        block.pos = [
                            x,
                            y,
                            z
                        ];
                        Render.addObj(block);
                    }
                }
            }
        }
    }
})();

(Taken from this website (Runnable))

2

There are 2 best solutions below

3
On BEST ANSWER

Twgl does not rotate stuff like mat4 example: Twgl:

function rotateX(m, angleInRadians, dst) {
  dst = dst || new MatType(16);
  const m10 = m[4];
  const m11 = m[5];
  const m12 = m[6];
  const m13 = m[7];
  const m20 = m[8];
  const m21 = m[9];
  const m22 = m[10];
  const m23 = m[11];
  const c = Math.cos(angleInRadians);
  const s = Math.sin(angleInRadians);
  dst[4] = c * m10 + s * m20;
  dst[5] = c * m11 + s * m21;
  dst[6] = c * m12 + s * m22;
  dst[7] = c * m13 + s * m23;
  dst[8] = c * m20 - s * m10;
  dst[9] = c * m21 - s * m11;
  dst[10] = c * m22 - s * m12;
  dst[11] = c * m23 - s * m13;
  if (m !== dst) {
    dst[0] = m[0];
    dst[1] = m[1];
    dst[2] = m[2];
    dst[3] = m[3];
    dst[12] = m[12];
    dst[13] = m[13];
    dst[14] = m[14];
    dst[15] = m[15];
  }
  return dst;
}

function rotateY(m, angleInRadians, dst) {
  dst = dst || new MatType(16);
  const m00 = m[0 * 4 + 0];
  const m01 = m[0 * 4 + 1];
  const m02 = m[0 * 4 + 2];
  const m03 = m[0 * 4 + 3];
  const m20 = m[2 * 4 + 0];
  const m21 = m[2 * 4 + 1];
  const m22 = m[2 * 4 + 2];
  const m23 = m[2 * 4 + 3];
  const c = Math.cos(angleInRadians);
  const s = Math.sin(angleInRadians);
  dst[0] = c * m00 - s * m20;
  dst[1] = c * m01 - s * m21;
  dst[2] = c * m02 - s * m22;
  dst[3] = c * m03 - s * m23;
  dst[8] = c * m20 + s * m00;
  dst[9] = c * m21 + s * m01;
  dst[10] = c * m22 + s * m02;
  dst[11] = c * m23 + s * m03;
  if (m !== dst) {
    dst[4] = m[4];
    dst[5] = m[5];
    dst[6] = m[6];
    dst[7] = m[7];
    dst[12] = m[12];
    dst[13] = m[13];
    dst[14] = m[14];
    dst[15] = m[15];
  }
  return dst;
}

function rotateZ(m, angleInRadians, dst) {
  dst = dst || new MatType(16);
  const m00 = m[0 * 4 + 0];
  const m01 = m[0 * 4 + 1];
  const m02 = m[0 * 4 + 2];
  const m03 = m[0 * 4 + 3];
  const m10 = m[1 * 4 + 0];
  const m11 = m[1 * 4 + 1];
  const m12 = m[1 * 4 + 2];
  const m13 = m[1 * 4 + 3];
  const c = Math.cos(angleInRadians);
  const s = Math.sin(angleInRadians);
  dst[0] = c * m00 + s * m10;
  dst[1] = c * m01 + s * m11;
  dst[2] = c * m02 + s * m12;
  dst[3] = c * m03 + s * m13;
  dst[4] = c * m10 - s * m00;
  dst[5] = c * m11 - s * m01;
  dst[6] = c * m12 - s * m02;
  dst[7] = c * m13 - s * m03;
  if (m !== dst) {
    dst[8] = m[8];
    dst[9] = m[9];
    dst[10] = m[10];
    dst[11] = m[11];
    dst[12] = m[12];
    dst[13] = m[13];
    dst[14] = m[14];
    dst[15] = m[15];
  }
  return dst;
}

Mat4 (What im looking for):

function rotateX(out, a, rad) {
  let s = Math.sin(rad);
  let c = Math.cos(rad);
  let a10 = a[4];
  let a11 = a[5];
  let a12 = a[6];
  let a13 = a[7];
  let a20 = a[8];
  let a21 = a[9];
  let a22 = a[10];
  let a23 = a[11];
  if (a !== out) {
    out[0] = a[0];
    out[1] = a[1];
    out[2] = a[2];
    out[3] = a[3];
    out[12] = a[12];
    out[13] = a[13];
    out[14] = a[14];
    out[15] = a[15];
  }
  out[4] = a10 * c + a20 * s;
  out[5] = a11 * c + a21 * s;
  out[6] = a12 * c + a22 * s;
  out[7] = a13 * c + a23 * s;
  out[8] = a20 * c - a10 * s;
  out[9] = a21 * c - a11 * s;
  out[10] = a22 * c - a12 * s;
  out[11] = a23 * c - a13 * s;
  return out;
}

function rotateY(out, a, rad) {
  let s = Math.sin(rad);
  let c = Math.cos(rad);
  let a00 = a[0];
  let a01 = a[1];
  let a02 = a[2];
  let a03 = a[3];
  let a20 = a[8];
  let a21 = a[9];
  let a22 = a[10];
  let a23 = a[11];
  if (a !== out) {
    out[4] = a[4];
    out[5] = a[5];
    out[6] = a[6];
    out[7] = a[7];
    out[12] = a[12];
    out[13] = a[13];
    out[14] = a[14];
    out[15] = a[15];
  }
  out[0] = a00 * c - a20 * s;
  out[1] = a01 * c - a21 * s;
  out[2] = a02 * c - a22 * s;
  out[3] = a03 * c - a23 * s;
  out[8] = a00 * s + a20 * c;
  out[9] = a01 * s + a21 * c;
  out[10] = a02 * s + a22 * c;
  out[11] = a03 * s + a23 * c;
  return out;
}

function rotateZ(out, a, rad) {
  let s = Math.sin(rad);
  let c = Math.cos(rad);
  let a00 = a[0];
  let a01 = a[1];
  let a02 = a[2];
  let a03 = a[3];
  let a10 = a[4];
  let a11 = a[5];
  let a12 = a[6];
  let a13 = a[7];
  if (a !== out) {
    out[8] = a[8];
    out[9] = a[9];
    out[10] = a[10];
    out[11] = a[11];
    out[12] = a[12];
    out[13] = a[13];
    out[14] = a[14];
    out[15] = a[15];
  }
  out[0] = a00 * c + a10 * s;
  out[1] = a01 * c + a11 * s;
  out[2] = a02 * c + a12 * s;
  out[3] = a03 * c + a13 * s;
  out[4] = a10 * c - a00 * s;
  out[5] = a11 * c - a01 * s;
  out[6] = a12 * c - a02 * s;
  out[7] = a13 * c - a03 * s;
  return out;
}
0
On

I was able to get rid of the odd 'gyroscopic' rotation you're observing and replace it with more natural yaw-pitch-roll rotation by re-ordering the camera transforms to the following (I've added comments for clarity):

  m4.identity(camera); // loads identity
  m4.translate(camera, [px, py, pz], camera); // applies translation at END
  m4.rotateY(camera, degToRad(ang), camera); // applies y-rotation before that (yaw)
  m4.rotateX(camera, degToRad(elev), camera); // applies x-rotation before that (pitch)
  m4.rotateZ(camera, degToRad(roll), camera); // applies z-rotation before that (roll)

When setting up a yaw-pitch-roll camera matrix, you typically want to apply roll rotation first, then pitch, then yaw. The key here to keep in mind is, when working with TWGL (and most matrix libraries frankly) the transform order actually applies to the vector in reverse order. I believe this is due to the fact that matrices are stored transposed. This is one of those annoying gotchas when working with OpenGL matrices and associated libraries.

In any case, below I've updated the code from that (pretty poorly done) WGL2 fundamentals page you've linked: I applied the transforms in the order shown above (and also made a few sanity adjustments). But for your code, you may be able to fix it just by re-ordering the transforms as mentioned.

I say may because, when it comes to matrices, transforms, and 3D coordinate systems, there are a couple other possibly confusing factors to keep in mind:

  • Up-axis: The axis in the world whose positive direction is considered upward, i.e., from the 'ground' to the 'sky'. Developers have only ever used either Y or Z.
  • Left- or Right-handed systems: When viewing the 'ground' from a bird's-eye view and assuming positive X is east, the handed system determines whether the positive direction of the final axis faces north or south.
    • If using Y-up...
      • a left-handed system means +Z faces north,
      • and a right-handed system means +Z faces south.
    • If using Z-up...
      • a left-handed system means +Y faces south,
      • and a right-handed system means +Y faces north.

Both factors will affect the order of your transforms. Additionally, which handed system you choose will affect the direction of rotation (there are many online articles that help explain the details here). The WGL2 page you linked and that I copied here uses Y-up with a right-handed system (+Z faces south).

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<style>

body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
pre { position: absolute; left: 1em; top: 0; }


</style>
</head>
<body>

<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
<pre>
A = left
D = right
W = forward
S = down
Q = roll left
E = roll right
UP = look up
DN = look down
</pre>


</body>
<script>

const m4 = twgl.m4;
const v3 = twgl.v3;
const gl = document.querySelector("canvas").getContext("webgl");
const vs = `
uniform mat4 u_worldViewProjection;
uniform mat4 u_worldInverseTranspose;

attribute vec4 position;
attribute vec3 normal;

varying vec3 v_normal;

void main() {
  gl_Position = u_worldViewProjection * position;
  v_normal = (u_worldInverseTranspose * vec4(normal, 0)).xyz;
}
`;
const fs = `
precision mediump float;

varying vec3 v_normal;
uniform vec3 u_lightDir;
uniform vec4 u_color;

void main() {
  vec3 norm = normalize(v_normal);
  float light = dot(u_lightDir, norm) * .5 + .5;
  gl_FragColor = vec4(u_color.rgb * light, u_color.a);
}
`;

const progInfo = twgl.createProgramInfo(gl, [vs, fs]);
const bufferInfo = twgl.primitives.createCubeBufferInfo(gl, 1);

const projection = m4.identity();
const camera = m4.identity();
const view = m4.identity();
const viewProjection = m4.identity();
const world = m4.identity();
const worldViewProjection = m4.identity();
const worldInverse = m4.identity();
const worldInverseTranspose = m4.identity();

const fov = degToRad(90);
const zNear = 0.1;
const zFar = 100;

const lightDir = v3.normalize([1, 2, 3]);

const keys = {};

let px = 0;
let py = 0;
let pz = 0;
let elev = 0;
let ang = 0;
let roll = 0;
const speed = 1;
const turnSpeed = 90;

let then = 0;
function render(now) {
  now *= 0.001;  // seconds;
  const deltaTime = now - then;
  then = now;

  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);

  gl.useProgram(progInfo.program);

  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  m4.perspective(fov, aspect, zNear, zFar, projection);

  m4.identity(camera);
  m4.translate(camera, [px, py, pz], camera);
  m4.rotateY(camera, degToRad(ang), camera);
  m4.rotateX(camera, degToRad(elev), camera);
  m4.rotateZ(camera, degToRad(roll), camera);

  m4.inverse(camera, view);

  m4.multiply(projection, view, viewProjection);

  for (let z = -1; z <= 1; ++z) {
    for (let y = -1; y <= 1; ++y) {
      for (let x = -1; x <= 1; ++x) {
        if (x === 0 && y === 0 && z === 0) {
          continue;
        }

        m4.identity(world);
        m4.translate(world, [x * 3, y * 3, z * 3], world);

        m4.multiply(viewProjection, world, worldViewProjection);
        m4.inverse(world, worldInverse);
        m4.transpose(worldInverse, worldInverseTranspose);

        twgl.setBuffersAndAttributes(gl, progInfo, bufferInfo);
        twgl.setUniforms(progInfo, {
          u_worldViewProjection: worldViewProjection,
          u_worldInverseTranspose: worldInverseTranspose,
          u_color: [
            (x + 1) / 2, // x is red
            (y + 1) / 2, // y is green
            (z + 1) / 2, // z is blue
          1],
          u_lightDir: lightDir,
        });
        twgl.drawBufferInfo(gl, bufferInfo);
      }
    }
  }

  if (keys.W || keys.S) {
    const direction = keys.W ? 1 : -1;
    px -= camera[ 8] * deltaTime * speed * direction;
    py -= camera[ 9] * deltaTime * speed * direction;
    pz -= camera[10] * deltaTime * speed * direction;
  }

  if (keys.A || keys.D) {
    const direction = keys.A ? 1 : -1;
    ang += deltaTime * turnSpeed * direction;
  }

  if (keys.Q || keys.E) {
    const direction = keys.Q ? 1 : -1;
    roll += deltaTime * turnSpeed * direction;
  }

  if (keys.ARROWUP || keys.ARROWDOWN) {
    const direction = keys.ARROWUP ? 1 : -1;
    elev += deltaTime * turnSpeed * direction;
  }

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

window.addEventListener('keydown', (e) => {
  keys[e.key.toUpperCase()] = true;
  // e.preventDefault();
});
window.addEventListener('keyup', (e) => {
  keys[e.key.toUpperCase()] = false;
  // e.preventDefault();
});

function degToRad(d) {
  return d * Math.PI / 180;
}


</script>