
Advertisement
First person camera and matrix transforms
By
TheChubu
, in For Beginners
This topic is 1810 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.
If you intended to correct an error in the post then please contact us.
Recommended Posts
BCullis 1955
All right. But then, to go from world to view wouldn't require another translation (camera position), rotation(where the camera is looking at) and scaling (zoom effects)? Or I'm missing something?
Yes, it does. I'm just saying it doesn't change "spaces" when you apply the view matrix. Like others have said, it essentially applies an inverse of the camera's position, rotation, and scale. I use helper functions like "createLookAt", but my camera stores peraxis rotations and a worldposition vector that end up being used for the calculations (world position gives the origin for the lookat vector, and the camera rotations are fed into a "CreateRotationYawPitchRoll" matrix that modifies the camera's orientation vectors (left, up, and forward)).
Gah, any more parentheses there and I'd be writing Lisp o_o
TheChubu 9467
I see. Now it makes more sense. So, where the lookat vector comes from? It is computed from mouse input?
Trienco 2555
Personally I would absolutely avoid storing angles, though it works fine for a simple FPS style camera (just make sure you always rotate around "up" first and "right" second). The simple fact is that the order in which your rotations are applied is important and completely lost if you just accumulate angles.
Also, pleaaaase don't abuse scaling to zoom. Zooming is a perspective effect and the result of focusing your lens to show a smaller area, ie. what you get by reducing your field of view. Scaling will scale your objects, can make them grow beyond your near plane or even behind your camera and suddenly zooming turns into a wall hack.
My usual camera class is using my own vector/matrix stuff, so it would be mostly useless to you, but the basic setup is that it stores a transformation matrix (4x4). All rotations and translations are directly applied and accumulated in that matrix when processing the users input. Since matrix math is neat, applying a rotation around (1,0,0) will always look up/down around your current "right", just like a translation along (1,0,0) will always strafe left/right. For the rare situations where you want to move/rotate using the world axes, you just multiply the new transformation from the other side (to visualize it, one way the rotation happens after the previous transformations, the other way it happens before... while all local axes are still aligned with the world axes).
Btw., your typical lookAt function will just build a transformation matrix from the passed vectors and then invert it, so it becomes extremely superfluous if you store the transformation matrix in the first place. Inverting it is simple, as long as you stay away from scaling.
viewMatrix = Matrix44( Transform[0], Transform[4], Transform[8], 0, Transform[1], Transform[5], Transform[9], 0, Transform[2], Transform[6], Transform[10], 0, (Transform[0]*Transform[12] + Transform[1]*Transform[13] + Transform[2]*Transform[14]), (Transform[4]*Transform[12] + Transform[5]*Transform[13] + Transform[6]*Transform[14]), (Transform[8]*Transform[12] + Transform[9]*Transform[13] + Transform[10]*Transform[14]), 1);
BCullis 1955
I see. Now it makes more sense. So, where the lookat vector comes from? It is computed from mouse input?
It depends on your implementation. I could technically say "yes" when talking about my own code, because mouse movement alters the camera's rotation, and that rotation modifies the camera's "Forward" vector, which is used together with the camera's world position to make my lookat matrix. But the DirectX LookAt method takes an origin and a destination, and calculates the matrix that would represent a view "looking at" the destination from the origin.
The following is C# code, but probably explains better than I just tried to:
(_rotation is a vector representing x, y, and zaxis rotation of the camera)
public virtual void Update(float timeDelta) { Matrix rotation = Matrix.RotationYawPitchRoll(_rotation.Y, _rotation.X, 0); _forward = Vector3.TransformCoordinate(Vector3.UnitZ, rotation); _up = Vector3.TransformCoordinate(Vector3.UnitY, rotation); _left = Vector3.Normalize(Vector3.Cross(_forward, _up)); _viewMatrix = Matrix.LookAtLH(_worldPosition, _worldPosition + _forward, _up); }Edited by BCullis
TheChubu 9467
All right. Thank you all for your answers! I'm trying to implement it right now. If I come across another rock I'll be back :D
TheChubu 9467
Gah, I'm still having issues. Translation works ok but rotations work awkwardly. Like the camera isn't rotating on itself but around some point elsewhere. And it seems I can only rotate around a limited arc, like 60ºC left and 60ºC right.
This is my lookAt function. I use DefaultMatrixFloat for both matrices and vectors. eyePos, target and such are treated as column vectors with 3 elements.
public static DefaultMatrixFloat lookAt ( DefaultMatrixFloat eyePos, DefaultMatrixFloat target, DefaultMatrixFloat up) { DefaultMatrixFloat zAxis = UtilsMath.substractMatrix(target,eyePos); UtilsMath.normalize3f(zAxis); DefaultMatrixFloat xAxis = UtilsMath.crossProduct3f(up, zAxis); UtilsMath.normalize3f(xAxis); DefaultMatrixFloat yAxis = UtilsMath.crossProduct3f(zAxis, xAxis); UtilsMath.normalize3f(yAxis); // This is emulating GLM's lookAt function. Reverses cross products. Has the same issues. // DefaultMatrixFloat zAxis = UtilsMath.substractMatrix(target,eyePos); // UtilsMath.normalize3f(zAxis); // // DefaultMatrixFloat xAxis = UtilsMath.crossProduct3f(zAxis,up); // UtilsMath.normalize3f(xAxis); // // DefaultMatrixFloat yAxis = UtilsMath.crossProduct3f(xAxis,zAxis); // UtilsMath.normalize3f(yAxis); //4x4 matrix. DefaultMatrixFloat transform = new DefaultMatrixFloat(4,4); transform.setIdentity(); //setValueAt takes (row,column,value) as parameters. //getValueAt takes (row,column) and returns the float in that position. transform.setValueAt(0, 0, xAxis.getValueAt(0, 0)); transform.setValueAt(0, 1, yAxis.getValueAt(0, 0)); transform.setValueAt(0, 2, zAxis.getValueAt(0, 0)); transform.setValueAt(1, 0, xAxis.getValueAt(1, 0)); transform.setValueAt(1, 1, yAxis.getValueAt(1, 0)); transform.setValueAt(1, 2, zAxis.getValueAt(1, 0)); transform.setValueAt(2, 0, xAxis.getValueAt(2, 0)); transform.setValueAt(2, 1, yAxis.getValueAt(2, 0)); transform.setValueAt(2, 2, zAxis.getValueAt(2, 0)); transform.setValueAt(0, 3,  dotProduct3f(xAxis, eyePos)); transform.setValueAt(1, 3,  dotProduct3f(yAxis, eyePos)); transform.setValueAt(2, 3,  dotProduct3f(zAxis, eyePos)); // Also tried doing the dotProduct in the last row like GLM's implementation but it warps everything. // transform.setValueAt(3, 0,  dotProduct3f(xAxis, eyePos)); // transform.setValueAt(3, 1,  dotProduct3f(yAxis, eyePos)); // transform.setValueAt(3,2,  dotProduct3f(zAxis, eyePos)); return transform; }
And this is how I get input from the user:
public void pollInput ( ZGLClass glObject ) { float posOff = 5.0f; float viewOff = 0.2f; //eyePos,target,up are 3 element column vectors. //up vector is defined as (0,1,0). //Translations. Affect both eyePos and target vectors by the same offset. if (Keyboard.isKeyDown(Keyboard.KEY_S)) { zPos += posOff; zView += posOff; this.eyePos.setValueAt(2, 0, zPos); this.target.setValueAt(2, 0, zView); } if (Keyboard.isKeyDown(Keyboard.KEY_W)) { zPos = posOff; zView = posOff; this.eyePos.setValueAt(2, 0, zPos); this.target.setValueAt(2, 0, zView); } //Looking right/left only modify the target vector, not the position of the eye. if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT)) { xView += viewOff; this.target.setValueAt(0, 0, xView); } if (Keyboard.isKeyDown(Keyboard.KEY_LEFT)) { xView = viewOff; this.target.setValueAt(0, 0, xView); } //"rotationMatrix" is actually the view matrix. I just hijacked the variable for now. //updateViewMatrix updates the related "rottionMatrix" uniform where the viewMatrix is saved. glObject.rotationMatrix = UtilsMath.lookAt(eyePos, target, up); glObject.updateViewMatrix(); }
There are more ifs for translations along X (xPos) and Y (yPos), and up/down rotation, they work the same. Adding an offset and setting it up on the eyePos/target vector.
I have no idea where I'm going wrong.
Edited by TheChubuTrienco 2555
Judging from your code the very simple reason why your rotation isn't working is because you aren't rotating at any point. Shifting the point to look at left and right has nothing to do with rotation and a very simple question should make it obvious: "how do you expect to look behind your position?"
Rotation involves trigonometry, either used directly (I wouldn't) or hidden away (constructing a rotation matrix). Look at the graphs for sin and cos, remember that an angle of 0° is straight to the right and you should notice how x = cos and y = sin for any given angle.
TheChubu 9467
Let's see. I can view up/down in a 180º arc (directly up, directly down). And I can view right/left in a 180º arc too (directly left, directly right).
Then what exactly handles the view matrix? Translations work fine, just displace the eyePos vector by an offset. I can look up, press W, and I will advance in that direction (sort of). While looking left/right doesn't quite works (due lack of rotation as you said), looking up/down should. And it isn't working. It works at the start, when the camera is at (0,0) world position, if I move, then looking up/down breaks.
Say that I rotate 10º generate a rotation matrix along Y with that (yaw). Then what I should rotate with that matrix? The model? The vectors?
Anyway I've been thinking, I don't know exactly what the view matrix should do. What should I expect if it works properly? Viewing up/down and viewing right/left by 360º ? And what happens if I turn to the left and press W (forward), I should expect the view matrix translates accordingly? (towards y i think).
Khatharr 8814
This article has an in depth look along with a sample LookAt() implementation.
TheChubu 9467
That article is where my first lookAt method came from (that's why the view direction is called "target" like the article).
Since that article made no mention about further rotating vertices with another rotation thought that you could change look directions by just changing the target vector, but Trineco says I also need another rotation?
Anyway that article doesn't mentions if I should expect fully working 1rst person camera out of the view matrix (ie, looking up/down, and 360º left/right).
Trienco 2555
The problem is that by storing a target position, you are making something that is conceptually really simple a lot more messy. For one, you can't simply rotate a target position, because it would rotate around the origin instead of the camera position. You can't just linearly move it around either, as you might have noticed from things not working. So stop dealing with "which point am I looking at" and think about "which direction am I looking in".
For an fps camera, you probably want to limit your up/down rotation to +/89°, so in your case, the otherwise ugly Euler Angles are the easiest approach. Why not 90° you might ask? Because of something many people don't quite understand about the lookAt helper function. "Up" must be roughly your local up (ie. imagine sticking a pencil in the top of your head, that's the "up" lookAt needs. Now, simply using (0,1,0) works great, until you rotate exactly +/90° up/down (where it should be (0,0,1) or (0,0,1)) or more than 90° (where it has to be (0,1,0)).
So what you really need for lookAt is your position, a point to look at (or the direction to look in) and a vector pointing "up". That just happens to be the 4th, 3rd and 2nd column of the cameras transformation matrix, but we just want fps style and take some shortcuts:
//Don't try to use this for a flight simulator without stocking up on aspirin first float pitch = 0; float yaw = 0; if (left) yaw = rotationSpeed * mouseDelta; if (right) yaw += rotationSpeed * mouseDelta; if (up) pitch = ... ... while (yaw < 0) yaw += 360; while (yaw > 360) yaw = 360; if (pitch > 89) pitch = 89; ... matrix rotation = createRotationMatrix(pitch, 1,0,0) * createRotationMatrix(yaw, 0,1,0); //At this point, you can either transform some default view direction or just have the camera look right by default and extract the columns of this matrix. //For simplicity, we use lookAt viewMatrix = lookAt(cameraPosition, cameraPosition + rotation.3rdColumn, rotation.2ndColumn);
Probably a lot more reusable and not really much more complicated:
matrix rotation; vector translation; //Note that left/right and up/down are a different order to use the global "up" and the local "right" if (left/right) rotation = rotation * createRotation(mouseDelta, 0,1,0); if (up/down) rotation = createRotation(mouseDelta, 1,0,0) * rotation; if (forward/backward) translation += distance * rotation.3rdColumn; if (strafeLeft/Right) translation += distance * rotation.1stColumn; matrix transformation = rotation;transformation.4thColumn = translation; viewMatrix = inverse(transformation); //Use the simple version (same as in lookAt)
You can also keep it all in one matrix, which makes translation easier, but requires extra steps when rotating around a global axis.
matrix transformation; if (left/right) { vector position = transformation.4thColumn; transformation = transformation * createRotation(yaw, 0,1,0); //This will also rotate our position around the origin transformation.4thColumn = position; } if (up/down) transformation = createRotation(mouseDelta, 1,0,0) * transformation; //This will rotate around our current position if (movement) transformation = createTranslation(x,y,z) * transformation; //This will use local x,y,z if (gettingPushed) transformation = transformation * createTranslation(x,y,z); //This will use local x,y,z viewMatrix = inverse(transformation);
This is obviously pseudo code. Since 3D graphics is basically nothing but vector and matrix math, there simply is no way to get around learning and embracing them. The absolute minimum to start out is getting familiar with rotation and translation matrices and getting an intuitive grasp of what the components represent and what multiplication will do to them.
You might notice that the last versions will also work just fine for all your other game objects and won't fall apart when you start turning things upside down. So it's pretty much the most generic way if you stay away from scaling (or keep it separated). There is also no lookAt, because most of what that does would be completely redundant.
One thing to keep in mind is that you need to occasionally reorthonormalize the rotation/transformation matrix, as floating point errors will accumulate and start creating some trippy effects. When using lookAt, that happens automatically, since it is reconstructing the whole matrix every frame.
Eventually people will tell you how quaternions are essentially the "one true way of rotation" or even the second coming, but in the end they are completely equivalent to rotation matrices and the only important thing is to know when they are more efficient. There is however absolutely no reason to worry about them at this point (and no, they are not the "only solution to gimbal lock", that's "don't use frigging Euler angles").
edit: ok, great. Never edit a post with code, unless you want every single line break to disappear...
Edited by TriencoTheChubu 9467
For an fps camera, you probably want to limit your up/down rotation to +/89°, so in your case, the otherwise ugly Euler Angles are the easiest approach. Why not 90° you might ask? Because of something many people don't quite understand about the lookAt helper function. "Up" must be roughly your local up (ie. imagine sticking a pencil in the top of your head, that's the "up" lookAt needs. Now, simply using (0,1,0) works great, until you rotate exactly +/90° up/down (where it should be (0,0,1) or (0,0,1)) or more than 90° (where it has to be (0,1,0)).
Then it locks two rotation axis? Forward vector and up vector become the same. A gimbal lock.
I implemented the first approach you showed (since I don't know how to do the inversions without going all crazy with cofactors).
This is getting frustrating but I think I'm almost there. See, now moving works fine. If say, I look to the right and press forward, I advance in whatever direction that is instead of just along z.
Looking around works fine too, I can look upwards and all around. But when I move, it seems like the camera is "orbiting" the (0,0,0) coord of the world. It doesn't rotates on itself but around the world.
I thought I just mixed up the translation/orientation multiplication inside lookAt. The lookAt method I'm using is the one from the article Khatharr linked.
Changed the order of the multiplication from (translation * orientation) to (orientation * translation), which makes more sense since that's how I was doing it when I had all the matrices in the shader.
Trienco 2555
You already are doing the inversion in the last block of your lookAt function, when you transpose the rotation part and invert the translation part. This method only works for special cases, but unless you "break" things by adding scaling to your matrix, you have that special case.
TheChubu 9467
Ah I see. Everywhere I read said "and you dont have to do the inversion!" but never mentioned why (transpose the rotation, then invert translation).
Any idea why I can either move accordingly but rotate around world (0,0) or move always in the same directions but rotating around camera's (0,0) ? I'm very annoyed right now because it's been a week and I can't make it work.
There is something missing that should translate accordingly to where I'm looking at. How do you update your eye position? Just plain "if W is pressed, add 10 to Z component of eye position" ? Or there is more math involved?
I can't figure it out. Forward is always Z, right is always +Y, etc, no matter where I'm looking at.
Edited by TheChubuTheChubu 9467
Finally got that part to work a few days ago.
public void updateMovement () { float deltaMovement = movementSpeed * (UtilsMisc.getTime()  timer); /* * Movement vectors. */ forwardVec.setValueAt(0, 0, orientationMatrix.getValueAt(0, 2)*deltaMovement); // forwardVec.setValueAt(1, 0, orientationMatrix.getValueAt(1, 2)*deltaMovement); forwardVec.setValueAt(2, 0,  orientationMatrix.getValueAt(2, 2)*deltaMovement); rightVec.setValueAt(0, 0, orientationMatrix.getValueAt(0, 0)*deltaMovement); // rightVec.setValueAt(1, 0, orientationMatrix.getValueAt(1, 0)*deltaMovement); rightVec.setValueAt(2, 0,  orientationMatrix.getValueAt(2, 0)*deltaMovement); // upwardVec.setValueAt(0, 0, orientationMatrix.getValueAt(0, 1)*deltaMovement); upwardVec.setValueAt(1, 0, orientationMatrix.getValueAt(1, 1)*deltaMovement); // upwardVec.setValueAt(2, 0,  orientationMatrix.getValueAt(2, 1)*deltaMovement); }
I created unit vectors out of the orientation matrix (rotation along X * rotation along Y) so they're based on the camera coordinates instead of the world ones.
Now if I press W, I just add the forward vector to the camera's position and it will advance forward in whatever direction is looking at.
I commented out certain vectors from affecting some axes (ie, right/strafe vector can't affect the vertical/Y axis) otherwise the camera can move in some weird ways that aren't "FPS like".
Now I got mouse input working too, a little jerky but its ok for now.
Thanks for all the answers!
Edited by TheChubuTrienco 2555
Unless you somewhere normalize your forward and right vector, you are going to move veeeeeeery slowly while looking up (and not diving by 0 is then another reason to limit up/down rotation to less than 90°).

Advertisement
Ohh, I read that somewhere in Arcsynthesis book. So, a view matrix is the inverse of the various transformation matrices that place a camera in the world? So say that I'm at (30,30,0) coords in the world, looking 45º down (rotation of 45º along a (1,0,0) unit vector), I multiply those matrices, invert the result, and I have my view matrix?
I'm reading that MSDN article right now. Too bad that the View Transform section uses functions that I don't have. There wasn't any helper function in OGL, but functions to deal with the fixedfunction pipeline constructs which had matrix stacks, projection matrices, transforms, etc. Since those constructs are removed in modern OGL, I have to implement them myself.
There is GLM which is a C++ library that do has some helper functions and data types that mirror GLSL types for example. I'm using Java though.
Anyway, I should point out that I had separate rotations for each axis working before, and those did stack. It was an awful amount of multiplying though (3 separate matrices for rotations, one for translation and the perspective). I was looking for a more straightforward way.
.
I thought that the general rotation matrix with 1 vector and 1 angle would work but it didn't quite worked as I imagined.
In the code I set up three unit vectors, (1,0,0), (0,1,0), (0,0,1). A 3 floats for each angle and an offset (say, +5 degrees for each update). Camera looking down Z axis by default. When you press a key that rotates, say, along Z , I send the angle (currentAngle = currentAngle + offset) and the Z unit vector.
That works, it rotates the world along the Z axis by currentAngle (in radians). But if I rotate again, along Y for example, the rotation will work as if the camera was looking down the Z axis again (which it isnt, because I rotated it before). So each rotation works separately.
Then I thought, well yeah because the unit vectors, which represent the axis of rotations, never changed. So I set it up so after a rotation, it rotates each vector by the same matrix and then normalizes the result. That didn't worked either, it still rotates as if the camera is always looking down the Z axis, but in awkward ways.
Even if it didn't worked, I couldn't recognize where I was doing my world transform and view transform. Now I think that I'm not doing any world transform since I did not need it for what I was doing. It renders the vertices as they are (it is supposed to be the terrain so (0,0) position of the terrain is (0,0) of the world), and then trying to transform them to camera space.
Edited by TheChubuShare this post
Link to post
Share on other sites