Problem with z axis rotation projected orthografically

I’ve been trying to understand how orthographic projection and matrices work. Not just blindly using GLM. I’ve managed to build my own poorman’s math lib, move an object, scale, and rotate in 2 Axis.

BUT, when I rotate in the third axis: Z, the image does not rotate the way I expect it to:

If I DO NOT multiply the model matrix with the projection matrix, then I see the model rotate in the Z axis properly. This issue only happens when I multiply my model matrix with the projection matrix. It’s worth noting that every other rotation, translation and scaling works as expected when multiplying with the projection matrix. This visual issue only happens rotating in the Z axis.

I’m writing everything in JavaScript and using column first matrices. This is my projection matrix:


	    var proj = [
		    2/ (right - left), 0, 0, 0,
		    0, 2 / (top - bottom), 0, 0,
		    0, 0, -2 / (far - near), 0,
		    -((right + left) / (right - left)), -((top + bottom) / (top - bottom)), -((far + near) / (far - near)), 1
	   	];

this is how I’m initializing my projection matrix. The canvas size is 640x480:


var aspectRatio  = Renderer.gl.canvas.clientWidth / Renderer.gl.canvas.clientHeight,
canvasWidth  = (Renderer.gl.canvas.clientWidth / Renderer.gl.canvas.clientWidth)  * aspectRatio,
canvasHeight = (Renderer.gl.canvas.clientHeight / Renderer.gl.canvas.clientWidth) * aspectRatio;
		
mesh.projection  = Mathf.ortho(-canvasWidth, canvasWidth, -canvasHeight, canvasHeight, -1.0, 1.0);

Translation Matrix:


// Translation works even with orthographic projection. 
	Mat.translate = function (x, y, z) {
		var tr = Mat.identity();
		tr[12] =  x;
		tr[13] =  y;
		tr[14] =  z;

		return tr;
	};


Scale Matrix:


 // Not using right now, but when I do it works perfectly even with ortho projection
	Mat.scale = function (x, y, z) {
		var r = Mat.identity();
		r[0] =  x;
		r[5] =  y;
		r[10] = z;

		return r; 
	};

Rotation Z Axis.


// Rotates in the Z axis properly IF I don't multiply with orthographic projection.
	Mat.rotateZ = function (degree) {
		var r = Mat.Identity(),
		angle = Mat.degToRad(degree),
		cos 	= Math.cos(angle),
		sin 	= Math.sin(angle);

		r[0] =  cos; r[1] =  -sin;
		r[4] =  sin; r[5] =   cos;

		return r;
	};


…and this is where I compute my model and projection matrices before sending the final matrix to the GPU.

	
		this.model = Mat.Identity();

		// Set Matrices
		this.translation 	= Mat.translate(this.x, this.y, this.z);
		this.scale 			= Mat.scale(this.sx, this.sy, this.sz); // Not using yet
		this.rotationZ 		= Mat.rotateZ(this.angle);

		// Model Matrix
		this.final = Mat.mul(this.final, this.translation);
		this.final = Mat.mul(this.final, this.rotationY); 

		// Projection
		this.final = Mat.mul(this.final, this.projection);

		Renderer.gl.uniformMatrix4fv(this.modelUniform, false, this.final);
		Renderer.gl.drawElements(Renderer.gl.TRIANGLES, 6, Renderer.gl.UNSIGNED_SHORT, 0);

I’m really trying to figure out why I’m unable to rotate on the Z axis properly. Every other transformation works perfectly along with the projection. Thanks in advance, hopefully I’ve provided enough detail. Oh and if you want to see my matrix multiplication implementation, here it is:

https://gist.github.com/anonymous/1fdc609d56264fb389f6f4deeef521ae

[QUOTE=hashbrown;1286761]I’m writing everything in JavaScript and using column first matrices. This is my projection matrix:


	    var proj = [
		    2/ (right - left), 0, 0, 0,
		    0, 2 / (top - bottom), 0, 0,
		    0, 0, -2 / (far - near), 0,
		    -((right + left) / (right - left)), -((top + bottom) / (top - bottom)), -((far + near) / (far - near)), 1
	   	];

[/QUOTE]
You have the third and fourth columns swapped.

The mean values (left+right)/2 and (top+bottom)/2 should provide a constant offset in NDC (and thus in window coordinates). Ordinarily, that would mean that they go in the right-hand column (i.e. they’re multiplied by W); but for a perspective transformation, W is proportional to Z, so the X and Y offsets also need to be proportional to Z so that projective division results in the Z factor in both numerator and denominator cancelling. If it isn’t clear, consider a symmetric perspective projection (where right+left=0 and top+bottom=0) multiplied by a translation on the left (so the transformation is applied to the coordinates after projection):


[ 1  0  0 tx ]   [ sx  0  0  0 ]   [ sx  0  D*tx  0 ]
[ 0  1  0 ty ] * [  0 sy  0  0 ] = [  0 sy  D*ty  0 ]
[ 0  0  1  0 ]   [  0  0  C -1 ]   [  0  0  C    -1 ]
[ 0  0  0  1 ]   [  0  0  D  0 ]   [  0  0  D     0 ]

Also, you’d typically have the first and second columns scaled by the near value. As it stands, the left, right, top and bottom values correspond to the Z=-1 plane (compared with e.g. glFrustum(), where they’re on the near plane).

[QUOTE=GClements;1286762]You have the third and fourth columns swapped.

The mean values (left+right)/2 and (top+bottom)/2 should provide a constant offset in NDC (and thus in window coordinates). Ordinarily, that would mean that they go in the right-hand column (i.e. they’re multiplied by W); but for a perspective transformation, W is proportional to Z, so the X and Y offsets also need to be proportional to Z so that projective division results in the Z factor in both numerator and denominator cancelling. If it isn’t clear, consider a symmetric perspective projection (where right+left=0 and top+bottom=0) multiplied by a translation on the left (so the transformation is applied to the coordinates after projection):


[ 1  0  0 tx ]   [ sx  0  0  0 ]   [ sx  0  D*tx  0 ]
[ 0  1  0 ty ] * [  0 sy  0  0 ] = [  0 sy  D*ty  0 ]
[ 0  0  1  0 ]   [  0  0  C -1 ]   [  0  0  C    -1 ]
[ 0  0  0  1 ]   [  0  0  D  0 ]   [  0  0  D     0 ]

Also, you’d typically have the first and second columns scaled by the near value. As it stands, the left, right, top and bottom values correspond to the Z=-1 plane (compared with e.g. glFrustum(), where they’re on the near plane).[/QUOTE]

GC thank you very much for the answer. When you referred to providing a constant offset in NDC, you mean computing the values in normalized numbers (-1 to 1) right? Which is what I’m passing to my ortho function:


var aspectRatio  = Renderer.gl.canvas.clientWidth / Renderer.gl.canvas.clientHeight,
canvasWidth  = (Renderer.gl.canvas.clientWidth / Renderer.gl.canvas.clientWidth)  * aspectRatio,
canvasHeight = (Renderer.gl.canvas.clientHeight / Renderer.gl.canvas.clientWidth) * aspectRatio;
 
mesh.projection  = Mathf.ortho(-canvasWidth, canvasWidth, -canvasHeight, canvasHeight, -1.0, 1.0);

I figured I needed to insert normalized values into the ortho matrix since opengl works with normalized numbers. I guess I’m still a little lost not because of your answer, but because I’m still learning. You mentioned swapping the third and forth columns. You mean:

Just to share my understanding on multiplying against the projection matrix, the whole purpose of these matrices is to build several that will end up solving certain equations. For example, if i multiply my translate and scale matrices, I ended up with: (x * sx) + … and (y * sy) + … and so on.

When it comes to the projection matrix I understand my position xyz values will end up multiplying against the projection matrix c0r0 (column0 row0), c1r1, and c2r2 values…which as far as I know will scale xyz values of the moel. c0r3, c1r3, and c2r3 are offsets for the model’s xyz position. That’s as far as my understanding goes when it comes to multiplying against the projection matrix.

Sorry for asking again, I’m just really trying to teach myself this, and a little stuck in this area.

the purpose of the “projection matrix” is not to solve any equation (what equation?), is shapes the “viewing volume”

what is the “viewing volume” ? (some might ask)
everything you see in the window is 2D (because its not a hologram), it is a quad ranging from [-1; +1] x [-1; +1], but there is (most likely) also a depth buffer that can store depth information of your scene (the invisible 3rd dimesion)

to make 1 vertex appear on screen, you transform is first:
– into “world” space using the “mat4 ModelToWorld”
– then into “view” space using the “mat4 WorldToView”
now you see the “viewing volume” as seen from the “camera”, which is just a virtual point in the virtual “world”
you still cant see any 3rd dimension

to make things appear 3D, thaat means the further things are away from the “camera” (again, which is just a virtual point in the virtual “world”), the smaller they get on screen, the more centered (on screen) they appear

that transformation, from “view” space into an actual 3D world, that does the projection matrix
it transforms the “viewing volume” from “cube” to “frustum”, at the top of the frustum is the “camera”, looking into that frustum, seeing only whats in that frustum, where zNear & zFar are the clipping planes, fieldofview is the angle describing the frustum, and aspectratio the information needed to “fit” that frustum on screen (width x height)

glm::mat4 projection = glm::perspective(fieldofview, aspectratio, zNear, Zfar);

take a look at this tutorial: (scroll down to “projection”)

thats not all (of the “viewing pipeline”):

vec4 result = Projection * WorldToView * ModelToWorld * vertex;

the result (which has to lie within the cube [-1; +1] x [-1; +1] x [-1; +1]) has to be mapped to the window coordinates (a pixel within the rectangle [0; width] x [0; height])
that is done by opengl, you just need to call:

glViewport(0, 0, width, height)

[QUOTE=john_connor;1286782]the purpose of the “projection matrix” is not to solve any equation (what equation?), is shapes the “viewing volume”

what is the “viewing volume” ? (some might ask)
everything you see in the window is 2D (because its not a hologram), it is a quad ranging from [-1; +1] x [-1; +1], but there is (most likely) also a depth buffer that can store depth information of your scene (the invisible 3rd dimesion)

to make 1 vertex appear on screen, you transform is first:
– into “world” space using the “mat4 ModelToWorld”
– then into “view” space using the “mat4 WorldToView”
now you see the “viewing volume” as seen from the “camera”, which is just a virtual point in the virtual “world”
you still cant see any 3rd dimension

to make things appear 3D, thaat means the further things are away from the “camera” (again, which is just a virtual point in the virtual “world”), the smaller they get on screen, the more centered (on screen) they appear

that transformation, from “view” space into an actual 3D world, that does the projection matrix
it transforms the “viewing volume” from “cube” to “frustum”, at the top of the frustum is the “camera”, looking into that frustum, seeing only whats in that frustum, where zNear & zFar are the clipping planes, fieldofview is the angle describing the frustum, and aspectratio the information needed to “fit” that frustum on screen (width x height)

glm::mat4 projection = glm::perspective(fieldofview, aspectratio, zNear, Zfar);

take a look at this tutorial: (scroll down to “projection”)

thats not all (of the “viewing pipeline”):

vec4 result = Projection * WorldToView * ModelToWorld * vertex;

the result (which has to lie within the cube [-1; +1] x [-1; +1] x [-1; +1]) has to be mapped to the window coordinates (a pixel within the rectangle [0; width] x [0; height])
that is done by opengl, you just need to call:

glViewport(0, 0, width, height)

Thanks a lot John, I managed to fix it by sending the model and projection matrices as uniforms to the vertex shader and multiplied them there. When I mentioned equations, I was referring to things like (x * (2/R-L)), but I’m probably using the word wrong, sorry about that :stuck_out_tongue:

I will have to sit down and read more because now every time I rotate in the Z axis and multiply matrices on the cpu, the model does not rotate distorted anymore, but it does move in and out slightly. But if I multiply the model and projection matrices on the GPU, everything works, and I’m using the same Matrices.

I’ll figure out what I’m doing wrong, for now I can keep going. Thanks to both of you.

Well, it looks like the multiplication of matrices is wrong. You can try to post it.

Hey Silence, you were right it was definitely the matrix multiplication function. Maybe those were the swapped columns GCElements mentioned in the first answer. The fact that it worked perfectly in the shader with the same matrices gave it away. I went back to the code and found I had this:


                // Don't know why I inverted these, maybe it was late.
		rm[0] = a[1] * b[0];
		rm[1] = a[5] * b[1];
		rm[2] = a[9] * b[2];
		rm[3] = a[13] * b[3];

and I changed it to:


                // This works
		rm[0] = a[0] * b[1];
		rm[1] = a[1] * b[5];
		rm[2] = a[2] * b[9];
		rm[3] = a[3] * b[13];


Now I’m multiplying the model and projection matrices from the cpu with no problem. Going to work on a camera now. Thanks Silence!