Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a 3D free-camera in WebGL - why do neither of these methods work?

EDIT

OK, I've tried a camera using quaternions:

qyaw = [Math.cos(rot[0]/2), 0, Math.sin(rot[0]/2), 0];
qpitch = [Math.cos(rot[1]/2), 0, 0, Math.sin(rot[1]/2)];
rotQuat = quat4.multiply (qpitch, qyaw);
camRot = quat4.toMat4(rotQuat);
camMat = mat4.multiply(camMat,camRot);

and I get exactly the same problem. So I'm guessing it's not gimbal lock. I've tried changing the order I multiply my matrices, but it just goes camera matrix * model view matrix, then object matrix * model view. That's right isn't it?

I'm trying to build a 3d camera in webGL that can move about the world and be rotated around the x and y (right and up) axes.

I'm getting the familiar problem (possibly gimbal lock?) that once one of the axes is rotated, the rotation around the other is screwed up; for example, when you rotate around the Y axis 90degrees, rotation around the x becomes a spin around z.

I appreciate this is a common problem, and there are copious guides to building a camera that avoid this problem, but as far as I can tell, I've implemented two different solutions and I'm still getting the same problem. Frankly, it's doing my head in...

One solution I'm using is this (adapted from http://www.toymaker.info/Games/html/camera.html):

function updateCam(){
    yAx = [0,1,0]; 
    xAx = [1,0,0];
    zAx = [0,0,1];

    mat4.identity(camMat);

    xRotMat = mat4.create();
    mat4.identity(xRotMat)
    mat4.rotate(xRotMat,rot[0],xAx);
    mat4.multiplyVec3(xRotMat,zAx); 
    mat4.multiplyVec3(xRotMat,yAx);


    yRotMat = mat4.create();
    mat4.identity(yRotMat)
    mat4.rotate(yRotMat,rot[1],yAx);
    mat4.multiplyVec3(yRotMat,zAx); 
    mat4.multiplyVec3(yRotMat,xAx);


    zRotMat = mat4.create();
    mat4.identity(zRotMat)
    mat4.rotate(zRotMat,rot[2],zAx);
    mat4.multiplyVec3(zRotMat,yAx); 
    mat4.multiplyVec3(zRotMat,xAx);


    camMat[0] = xAx[0];
    camMat[1] = yAx[0];
    camMat[2] = zAx[0];
    //camMat[3] = 
    camMat[4] = xAx[1]
    camMat[5] = yAx[1]; 
    camMat[6] = zAx[1];
    //camMat[7] = 
    camMat[8] = xAx[2]
    camMat[9] = yAx[2];
    camMat[10]= zAx[2];
    //camMat[11]=
    camMat[12]= -1* vec3.dot(camPos, xAx); 
    camMat[13]= -1* vec3.dot(camPos, yAx);
    camMat[14]= -1* vec3.dot(camPos, zAx);
    //camMat[15]=

    var movSpeed = 1.5 * forward;
    var movVec= vec3.create(zAx);
    vec3.scale(movVec, movSpeed);
    vec3.add(camPos, movVec);
    movVec= vec3.create(xAx);
    movSpeed = 1.5 * strafe;
    vec3.scale(movVec, movSpeed);
    vec3.add(camPos, movVec);

}

I also tried using this method using

mat4.rotate(camMat, rot[1], yAx);

instead of explicitly building the camera matrix - same result.

My second (actually first...) method looks like this (rot is an array containing the current rotations around x, y and z (z is always zero):

   function updateCam(){
        mat4.identity(camRot);
        mat4.identity(camMat);
        camRot = fullRotate(rot);
        mat4.set(camRot,camMat);
        mat4.translate(camMat, camPos); 
    }

    function fullRotate(angles){
        var cosX = Math.cos(angles[0]);
        var sinX = Math.sin(angles[0]);
        var cosY = Math.cos(angles[1]);
        var sinY = Math.sin(angles[1]);
        var cosZ = Math.cos(angles[2]);
        var sinZ = Math.sin(angles[2]); 
        rotMatrix = mat4.create([cosZ*cosY, -1*sinZ*cosX + cosZ*sinY*sinX, sinZ*sinX+cosZ*sinY*cosX, 0,
            sinZ*cosY, cosZ*cosX + sinZ*sinY*sinX, -1*cosZ*sinX + sinZ*sinY*cosX, 0,
            -1*sinY, cosY*sinX, cosY*cosX, 0,
            0,0,0,1 ] );
        mat4.transpose(rotMatrix);
        return (rotMatrix);
    }

The code (I've taken out most of the boilerplate gl lighting stuff etc and just left the transformations) to actually draw the scene is:

   function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 2000.0, pMatrix);

    mat4.identity(mvMatrix);

    for(var i=0; i<planets.length; i++){
        if (planets[i].type =="sun"){
            currentProgram = perVertexSunProgram;
        } else {
            currentProgram = perVertexNormalProgram;
        }
        alpha = planets[i].alphaFlag;

        mat4.identity(planets[i].rotMat);

        mvPushMatrix(); 
            //all the following puts planets in orbit around a central sun, but it's not really relevant to my current problem
            var rot = [0,rotCount*planets[i].orbitSpeed,0];

            var planetMat;
            planetMat = mat4.create(fullRotate(rot));

            mat4.multiply(planets[i].rotMat, planetMat);

            mat4.translate(planets[i].rotMat, planets[i].position);

            if (planets[i].type == "moon"){
                var rot = [0,rotCount*planets[i].moonOrbitSpeed,0];
                moonMat = mat4.create(fullRotate(rot));
                mat4.multiply(planets[i].rotMat, moonMat);
                mat4.translate(planets[i].rotMat, planets[i].moonPosition);
                mat4.multiply(planets[i].rotMat, mat4.inverse(moonMat));
            }

            mat4.multiply(planets[i].rotMat, mat4.inverse(planetMat));
            mat4.rotate(planets[i].rotMat, rotCount*planets[i].spinSpd, [0, 1, 0]);


                        //this bit does the work - multiplying the model view by the camera matrix, then by the matrix of the object we want to render
            mat4.multiply(mvMatrix, camMat);
            mat4.multiply(mvMatrix, planets[i].rotMat);



            gl.useProgram(currentProgram);

            setMatrixUniforms();
            gl.drawElements(gl.TRIANGLES, planets[i].VertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);     
        mvPopMatrix();
        }
    }

However, most of the transformations can be ignored, the same effect cab be seen simply displaying a sphere at world coords 0,0,0.

I thought my two methods - either rotating the axes one at a time as you go, or building up the rotation matrix in one go avoided the problem of doing two rotations one after the other. Any ideas where I'm going wrong?

PS - I'm still very much starting to learn WebGL and 3d maths, so be gentle and talk to me like someone who hadn't heard of a matrix til a couple of months ago... Also, I know quaternions are a good solution to 3d rotation, and that would be my next attempt, however, I think I need to understand why these two methods don't work first...

like image 286
Fridge Avatar asked Oct 04 '11 09:10

Fridge


People also ask

Which matrix positions the camera in the WebGL?

Answer: WebGL cameras generally point down the -Z axis so to move in the direction the camera is facing you just add the camera's Z axis (elements 8, 9, 10) to the position of the camera multiplied by some velocity.

What is WebGL and OpenGL?

WebGL enables web content to use an API based on OpenGL ES 2.0 to perform 2D and 3D rendering in an HTML canvas in browsers that support it without the use of plug-ins. WebGL programs consist of control code written in JavaScript and shader code (GLSL) that is executed on a computer's Graphics Processing Unit (GPU).


2 Answers

For the sake of clarification, think about gimbal lock this way: You've played Quake/Unreal/Call of Duty/Any First Person Shooter, right? You know how when you are looking forward and move the mouse side to side your view swings around in a nice wide arc, but if you look straight up or down and move your mouse side to side you basically just spin tightly around a single point? That's gimbal lock. It's something that pretty much any FPS game uses because it happens to mimic what we would do in real life, and thus most people don't usually think of it as a problem.

For something like a space flight sim, however, or (more commonly) skeletal animation that type of effect is undesirable, and so we use things like quaternions to help us get around it. Wether or not you care about gimbal lock for your camera depends on the effect that you are looking to achieve.

I don't think you're experiencing that, however. What it sounds like is that your order of matrix multiplication is messed up, and as a result your view is rotating in a way that you don't expect. I would try playing with the order that you do your X/Y/Z rotations in and see if you can find an order than gives you the desired results.

Now, I hate doing code dumps, but this may be useful to you so here we go: This is the code that I use in most of my newer WebGL projects to manage a free-floating camera. It is gimbal locked, but as I mentioned earlier it doesn't really matter in this case. Basically it just gives you FPS style controls that you can use to fly around your scene.

/**
 * A Flying Camera allows free motion around the scene using FPS style controls (WASD + mouselook)
 * This type of camera is good for displaying large scenes
 */
var FlyingCamera = Object.create(Object, {
    _angles: {
        value: null
    },

    angles: {
        get: function() {
            return this._angles;
        },
        set: function(value) {
            this._angles = value;
            this._dirty = true;
        }
    },

    _position: {
        value: null
    },

    position: {
        get: function() {
            return this._position;
        },
        set: function(value) {
            this._position = value;
            this._dirty = true;
        }
    },

    speed: {
        value: 100
    },

    _dirty: {
        value: true
    },

    _cameraMat: {
        value: null
    },

    _pressedKeys: {
        value: null
    },

    _viewMat: {
        value: null
    },

    viewMat: {
        get: function() {
            if(this._dirty) {
                var mv = this._viewMat;
                mat4.identity(mv);
                mat4.rotateX(mv, this.angles[0]-Math.PI/2.0);
                mat4.rotateZ(mv, this.angles[1]);
                mat4.rotateY(mv, this.angles[2]);
                mat4.translate(mv, [-this.position[0], -this.position[1], - this.position[2]]);
                this._dirty = false;
            }

            return this._viewMat;
        }
    },

    init: {
        value: function(canvas) {
            this.angles = vec3.create();
            this.position = vec3.create();
            this.pressedKeys = new Array(128);

            // Initialize the matricies
            this.projectionMat = mat4.create();
            this._viewMat = mat4.create();
            this._cameraMat = mat4.create();

            // Set up the appropriate event hooks
            var moving = false;
            var lastX, lastY;
            var self = this;

            window.addEventListener("keydown", function(event) {
                self.pressedKeys[event.keyCode] = true;
            }, false);

            window.addEventListener("keyup", function(event) {
                self.pressedKeys[event.keyCode] = false;
            }, false);

            canvas.addEventListener('mousedown', function(event) {
                if(event.which == 1) {
                    moving = true;
                }
                lastX = event.pageX;
                lastY = event.pageY;
            }, false);

            canvas.addEventListener('mousemove', function(event) {
                if (moving) {
                    var xDelta = event.pageX  - lastX;
                    var yDelta = event.pageY  - lastY;
                    lastX = event.pageX;
                    lastY = event.pageY;

                    self.angles[1] += xDelta*0.025;
                    while (self.angles[1] < 0)
                        self.angles[1] += Math.PI*2;
                    while (self.angles[1] >= Math.PI*2)
                        self.angles[1] -= Math.PI*2;

                    self.angles[0] += yDelta*0.025;
                    while (self.angles[0] < -Math.PI*0.5)
                        self.angles[0] = -Math.PI*0.5;
                    while (self.angles[0] > Math.PI*0.5)
                        self.angles[0] = Math.PI*0.5;

                    self._dirty = true;
                }
            }, false);

            canvas.addEventListener('mouseup', function(event) {
                moving = false;
            }, false);

            return this;
        }
    },

    update: {
        value: function(frameTime) {
            var dir = [0, 0, 0];

            var speed = (this.speed / 1000) * frameTime;

            // This is our first person movement code. It's not really pretty, but it works
            if(this.pressedKeys['W'.charCodeAt(0)]) {
                dir[1] += speed;
            }
            if(this.pressedKeys['S'.charCodeAt(0)]) {
                dir[1] -= speed;
            }
            if(this.pressedKeys['A'.charCodeAt(0)]) {
                dir[0] -= speed;
            }
            if(this.pressedKeys['D'.charCodeAt(0)]) {
                dir[0] += speed;
            }
            if(this.pressedKeys[32]) { // Space, moves up
                dir[2] += speed;
            }
            if(this.pressedKeys[17]) { // Ctrl, moves down
                dir[2] -= speed;
            }

            if(dir[0] != 0 || dir[1] != 0 || dir[2] != 0) {
                var cam = this._cameraMat;
                mat4.identity(cam);
                mat4.rotateX(cam, this.angles[0]);
                mat4.rotateZ(cam, this.angles[1]);
                mat4.inverse(cam);

                mat4.multiplyVec3(cam, dir);

                // Move the camera in the direction we are facing
                vec3.add(this.position, dir);

                this._dirty = true;
            }
        }
    }
});

This camera assumes that Z is your "Up" axis, which may or may not be true for you. It's also using ECMAScript 5 style objects, but that shouldn't be an issue for any WebGL-enabled browser, and it utilizes my glMatrix library but it looks like you're already using that anyway. Basic usage is pretty simple:

// During your init code
var camera = Object.create(FlyingCamera).init(canvasElement);

// During your draw loop
camera.update(16); // 16ms per-frame == 60 FPS

// Bind a shader, etc, etc...
gl.uniformMatrix4fv(shaderUniformModelViewMat, false, camera.viewMat);

Everything else is handled internally for you, including keyboard and mouse controls. May not fit your needs exactly, but hopefully you can glean what you need to from there. (Note: This is essentially the same as the camera used in my Quake 3 demo, so that should give you an idea of how it works.)

Okay, that's enough babbling from me for one post! Good luck!

like image 143
Toji Avatar answered Sep 30 '22 13:09

Toji


It doesn't matter how you build your matrices, using euler angle rotations (like both of your code snippets do) will always result in a transformation that shows the gimble lock problem.

You may want to have a look at https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation as a starting point for creating transformations that avoid gimble locks.

like image 36
Tobias Schlegel Avatar answered Sep 30 '22 12:09

Tobias Schlegel