0
\$\begingroup\$

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"?

\$\endgroup\$
  • 1
    \$\begingroup\$ A good source for why someone wrote code in a way is likely whomever wrote the code in that way. \$\endgroup\$ – Lars Viklund Jul 2 '15 at 8:52
  • \$\begingroup\$ @Lars: you are right, i tried all possibilities before asking here, but the google site is not maintained anymore and i'm nowhere receiving help. Moreover, changes to the rotation system to get the effect of camera rotation around the model ( not the model rotation in front of the camera) is one of the most asked feature for jsc3d since years. Now i try to make this myself, and if i succeed, i will share this with the community, as i did with many others pieces of code of my. \$\endgroup\$ – deblocker Jul 3 '15 at 10:58
0
\$\begingroup\$

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.

\$\endgroup\$
  • 1
    \$\begingroup\$ i got it this afternoon, after your highly appreciated help and after reading this guide:link \$\endgroup\$ – deblocker Jul 23 '15 at 20:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.