Saturday, February 4, 2017

3d - How to understand kind of matrix/world from implemented math operations?


i'm a newbie in 3d programming, and i approach for the first time this argument. I try to make some changes to an existing 3d object viewer, to better suite my needs. This object viewer is jsc3d.


I read many different articles about 3d matrices and programming, and my scholar math allows me to understand the matrix operations. What i'm missing now is how to apply all this theory to get the desired result. What is the relationship between matrix math and the resulting projected 3d rotations in 2d space, or: "When i apply some matrix operations, what shall be the expected "visual" result"?


This is the implementation of the matrices:


JSC3D.Matrix3x4.prototype.identity = function() {

this.m00 = 1; this.m01 = 0; this.m02 = 0; this.m03 = 0;
this.m10 = 0; this.m11 = 1; this.m12 = 0; this.m13 = 0;
this.m20 = 0; this.m21 = 0; this.m22 = 1; this.m23 = 0;
};
JSC3D.Matrix3x4.prototype.scale = function(sx, sy, sz) {
this.m00 *= sx; this.m01 *= sx; this.m02 *= sx; this.m03 *= sx;
this.m10 *= sy; this.m11 *= sy; this.m12 *= sy; this.m13 *= sy;
this.m20 *= sz; this.m21 *= sz; this.m22 *= sz; this.m23 *= sz;
};
JSC3D.Matrix3x4.prototype.translate = function(tx, ty, tz) {

this.m03 += tx;
this.m13 += ty;
this.m23 += tz;
};

For example: could please someone kindly explain me how to recognize what kind of rotation is generated from following matrix:


JSC3D.Matrix3x4.prototype.rotateAboutXAxis = function(angle) {
if(angle != 0) {
angle *= Math.PI / 180;
var c = Math.cos(angle);

var s = Math.sin(angle);

var m10 = c * this.m10 - s * this.m20;
var m11 = c * this.m11 - s * this.m21;
var m12 = c * this.m12 - s * this.m22;
var m13 = c * this.m13 - s * this.m23;
var m20 = c * this.m20 + s * this.m10;
var m21 = c * this.m21 + s * this.m11;
var m22 = c * this.m22 + s * this.m12;
var m23 = c * this.m23 + s * this.m13;


this.m10 = m10; this.m11 = m11; this.m12 = m12; this.m13 = m13;
this.m20 = m20; this.m21 = m21; this.m22 = m22; this.m23 = m23;
}
};

JSC3D.Matrix3x4.prototype.rotateAboutYAxis = function(angle) {
if(angle != 0) {
angle *= Math.PI / 180;
var c = Math.cos(angle);

var s = Math.sin(angle);

var m00 = c * this.m00 + s * this.m20;
var m01 = c * this.m01 + s * this.m21;
var m02 = c * this.m02 + s * this.m22;
var m03 = c * this.m03 + s * this.m23;
var m20 = c * this.m20 - s * this.m00;
var m21 = c * this.m21 - s * this.m01;
var m22 = c * this.m22 - s * this.m02;
var m23 = c * this.m23 - s * this.m03;


this.m00 = m00; this.m01 = m01; this.m02 = m02; this.m03 = m03;
this.m20 = m20; this.m21 = m21; this.m22 = m22; this.m23 = m23;
}
};

JSC3D.Matrix3x4.prototype.rotateAboutZAxis = function(angle) {
if(angle != 0) {
angle *= Math.PI / 180;
var c = Math.cos(angle);

var s = Math.sin(angle);

var m10 = c * this.m10 + s * this.m00;
var m11 = c * this.m11 + s * this.m01;
var m12 = c * this.m12 + s * this.m02;
var m13 = c * this.m13 + s * this.m03;
var m00 = c * this.m00 - s * this.m10;
var m01 = c * this.m01 - s * this.m11;
var m02 = c * this.m02 - s * this.m12;
var m03 = c * this.m03 - s * this.m13;


this.m00 = m00; this.m01 = m01; this.m02 = m02; this.m03 = m03;
this.m10 = m10; this.m11 = m11; this.m12 = m12; this.m13 = m13;
}
};

It seems to me that this matrices are rows based and follows the right-hand rule, but i'm not sure. Why is also the 4th column involved in rotation? I think i'm missing somewhat else here.


I made also a fiddle to test the results: https://jsfiddle.net/50dhpLq5/


The final transformation matrix is then translated to the origin, rotated, scaled, and finally translated as needed:


this.transformMatrix.identity();

this.transformMatrix.translate(...);
this.transformMatrix.multiply(this.rotMatrix);
this.transformMatrix.scale(...);
this.transformMatrix.translate(...);

It would be for me also a great step forward to learn how to recognize the kind of 3d space used. I'm right if i call this a "world rotation matrix"?



Answer



Pipeline background


The spaces involved in most 3D graphics pipelines are:




  • model/object space, the space in which the vertices of your geometry are specified;

  • world space, a common space into which you transform your meshes;

  • eye space, a space in which the observer/camera is at the origin and the axes are along their right/up/forward vectors;

  • ndc/screen space, a homogeneous space in which the view frustum occupies a cube around the origin, which maps directly to your window.


You typically represent these by matrices, constructed from several basic operations chained together. These matrices take you from one space to another:



  • world transform takes you from a model space to the world space;

  • view transform takes you from world space to eye space;

  • projection transform takes you from eye space to screen space.



Transformation is simply vtx_ndc = projection * view * world * vtx_model.


The transformMatrix you manipulate appears to be the world transform of your pipeline.


As for your operations, they are just the basic operations mentioned, they work in any regular space and you use them to construct both world and view transforms, for moving/scaling/rotating objects and placing the camera, respectively.


Deciphering your code


Optimized code may appear unobvious to the observer if the actions inside are unknown and only the end result is seen.


Apart from the identity function you cite, all your functions do two things in unison, they construct a matrix for the requested operation and they multiply it with the current matrix in-place.


Wikipedia describes the matrices for rotating around the cardinal axes, here I'll cover the case of the X axis.


Rx(a) = [ 1     0        0    ]
[ 0 +cos(a) -sin(a) ]

[ 0 +sin(a) +cos(a) ]

The function to add an X rotation in an unoptimzed form would be like:


function makeXRotation(angle) {
var M = new Matrix3x4();
var c = cos(angle);
var s = sin(angle);
M.m00 = 1; M.m01 = 0; M.m02 = 0; M.m03 = 0;
M.m10 = 0; M.m11 = +c; M.m12 = -s; M.m13 = 0;
M.m20 = 0; M.m21 = +s; M.m22 = +c; M.m23 = 0;

return M;
};

function multiplyInPlace(lhs, rhs) {
// performs rhs = lhs*rhs
// dot product of rows of LHS vs. cols of RHS
// last row assumed (0,0,0,1)
var m00 = lhs.m00*rhs.m00 + lhs.m01*rhs.m10 + lhs.m02*rhs.m20 + lhs.m03*0;
var m01 = lhs.m00*rhs.m01 + lhs.m01*rhs.m11 + lhs.m02*rhs.m21 + lhs.m03*0;
var m02 = lhs.m00*rhs.m02 + lhs.m01*rhs.m12 + lhs.m02*rhs.m22 + lhs.m03*0;

var m03 = lhs.m00*rhs.m03 + lhs.m01*rhs.m13 + lhs.m02*rhs.m23 + lhs.m03*1;
var m10 = lhs.m10*rhs.m00 + lhs.m11*rhs.m10 + lhs.m12*rhs.m20 + lhs.m13*0;
var m11 = lhs.m10*rhs.m01 + lhs.m11*rhs.m11 + lhs.m12*rhs.m21 + lhs.m13*0;
var m12 = lhs.m10*rhs.m02 + lhs.m11*rhs.m12 + lhs.m12*rhs.m22 + lhs.m13*0;
var m13 = lhs.m10*rhs.m03 + lhs.m11*rhs.m13 + lhs.m12*rhs.m23 + lhs.m13*1;
var m20 = lhs.m20*rhs.m00 + lhs.m21*rhs.m10 + lhs.m22*rhs.m20 + lhs.m23*0;
var m21 = lhs.m20*rhs.m01 + lhs.m21*rhs.m11 + lhs.m22*rhs.m21 + lhs.m23*0;
var m22 = lhs.m20*rhs.m02 + lhs.m21*rhs.m12 + lhs.m22*rhs.m22 + lhs.m23*0;
var m23 = lhs.m20*rhs.m03 + lhs.m21*rhs.m13 + lhs.m22*rhs.m23 + lhs.m23*1;
rhs.m00 = m00; rhs.m01 = m01; rhs.m02 = m02; rhs.m03 = m03;

rhs.m10 = m10; rhs.m11 = m11; rhs.m12 = m12; rhs.m13 = m13;
rhs.m20 = m20; rhs.m21 = m21; rhs.m22 = m22; rhs.m23 = m23;
}

Followed by the composition of those two functions to perform the operation:


function rotateAroundX(angle) {
var R = makeXRotation(angle);
multiplyInPlace(R, this);
}


That's a lot of operations, many of which are pointless as some matrix components are known to have fixed values like 0 or 1. We can mechanically substitute in the constants to see what we can statically remove:


var c = cos(angle);
var s = sin(angle);
var m00 = 1*rhs.m00 + 0*rhs.m10 + 0*rhs.m20 + 0*0;
var m01 = 1*rhs.m01 + 0*rhs.m11 + 0*rhs.m21 + 0*0;
var m02 = 1*rhs.m02 + 0*rhs.m12 + 0*rhs.m22 + 0*0;
var m03 = 1*rhs.m03 + 0*rhs.m13 + 0*rhs.m23 + 0*1;
var m10 = 0*rhs.m00 + +c*rhs.m10 + -s*rhs.m20 + 0*0;
var m11 = 0*rhs.m01 + +c*rhs.m11 + -s*rhs.m21 + 0*0;
var m12 = 0*rhs.m02 + +c*rhs.m12 + -s*rhs.m22 + 0*0;

var m13 = 0*rhs.m03 + +c*rhs.m13 + -s*rhs.m23 + 0*1;
var m20 = 0*rhs.m00 + +s*rhs.m10 + +c*rhs.m20 + 0*0;
var m21 = 0*rhs.m01 + +s*rhs.m11 + +c*rhs.m21 + 0*0;
var m22 = 0*rhs.m02 + +s*rhs.m12 + +c*rhs.m22 + 0*0;
var m23 = 0*rhs.m03 + +s*rhs.m13 + +c*rhs.m23 + 0*1;
rhs.m00 = m00; rhs.m01 = m01; rhs.m02 = m02; rhs.m03 = m03;
rhs.m10 = m10; rhs.m11 = m11; rhs.m12 = m12; rhs.m13 = m13;
rhs.m20 = m20; rhs.m21 = m21; rhs.m22 = m22; rhs.m23 = m23;

Lots of zeroes and ones, let's trim:



var c = cos(angle);
var s = sin(angle);
var m00 = rhs.m00 ;
var m01 = rhs.m01 ;
var m02 = rhs.m02 ;
var m03 = rhs.m03 ;
var m10 = +c*rhs.m10 + -s*rhs.m20 ;
var m11 = +c*rhs.m11 + -s*rhs.m21 ;
var m12 = +c*rhs.m12 + -s*rhs.m22 ;
var m13 = +c*rhs.m13 + -s*rhs.m23 ;

var m20 = +s*rhs.m10 + +c*rhs.m20 ;
var m21 = +s*rhs.m11 + +c*rhs.m21 ;
var m22 = +s*rhs.m12 + +c*rhs.m22 ;
var m23 = +s*rhs.m13 + +c*rhs.m23 ;
// remove writes to m0X, as they're no-ops
// rhs.m00 = m00; rhs.m01 = m01; rhs.m02 = m02; rhs.m03 = m03;
rhs.m10 = m10; rhs.m11 = m11; rhs.m12 = m12; rhs.m13 = m13;
rhs.m20 = m20; rhs.m21 = m21; rhs.m22 = m22; rhs.m23 = m23;

This appears to resemble your original code rather closely. It's kind of obvious if you think about it that modifications pertaining to the X axis disappears as rotating around X by definition cannot affect the X component.



By following this chain of transformations, you can track how the last column comes into play. As we are chaining transformations we need to take in account all the effects of the previous transformation, including the translation parts. Matrix multiplication is always full rows vs. full columns. As pure rotation always acts around the origin, omitting the encoded translation would result in a rotation around the wrong location.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...