1

Not to long ago i started to create a small 3D engine in javascript to combine this with an html5 canvas.

One of the issues I run into is how can you transform 3d to 2d coords. Since I cannot use matrices or built in transformation methods I need another way.

I've tried implementing the next explanation + pseudo code: http://freespace.virgin.net/hugo.elias/routines/3d_to_2d.htm

Unfortunately no luck there. I've replace all the input variables with data from my own camera and object classes.

I have the following data: An object with a rotation, position vector and an array of 4 3d coords (its just a plane) a camera with a position and rotation vector the viewport -> a square 600 x 600 surface. The example uses a zoom factor which I've set as 1

Most hits on google use either matrix calculations or don't implement camera rotation. Basic transformation should be like this: screen.x = x / z * zoom screen.y = y / z * zoom

Can anyone point me in the right direction or explain to me howto achieve this?

edit: Thanks for all your posts, I haven't been able to apply all this to my project yet but I hope to do this soon.


Ok after playing around a while some issues still remain. I've created a new projection matrix:

    var w = (2 * nearPlane) / viewportWidth;
    var h = (2 * nearPlane) / viewportHeight;
    var q = farPlane / (farPlane - nearPlane);

    this.ProjectionMatrix = $M([
      [w, 0, 0, 0],
      [0, h, 0, 0],
      [0, 0, q, 1],
      [0, 0, (nearPlane * farPlane) / (nearPlane - farPlane), 0]
    ]);

where viewport = 600 * 450

and near plane = 0.5 and far plane = 500

the original code for drawing the vertices is this:

 var comboMatrix = world.WorldMatrix.x(camera.ViewMatrix);
    for (i = 0; i != this.NumberOfPolygons; i++) {
        var transformedVertices = new Array(verticesPerPolygon);
        var valid = new Boolean(1);
        for (j = 0; j != verticesPerPolygon; j++) {

            currentVertex = this.Mesh[i][j].x(comboMatrix);

            currentVertex.elements[0][0] = currentVertex.elements[0][0] / currentVertex.elements[0][2];
            currentVertex.elements[0][1] = currentVertex.elements[0][1] / currentVertex.elements[0][2];

            if (currentVertex.elements[0][0] < -1 || currentVertex.elements[0][0] > 1 || currentVertex.elements[0][1] < -1 || currentVertex.elements[0][1] > 1) {
                valid = new Boolean(0);
                break;
            }

            // Process Viewport Mapping Transformation
            currentVertex.elements[0][0] = currentVertex.elements[0][0] * 300 + 300;
            currentVertex.elements[0][1] = -currentVertex.elements[0][1] * 300 + 300;

            transformedVertices[j] = currentVertex;
        }

        // Draw the polygon
        if (valid == true) {
            this.DrawQuad(context, transformedVertices, true, new Boolean(0), new Boolean(1));
        }
    }

which kinda works since the letter H is drawn on my screen. Moving the camera however makes parts of the letter stop being drawn whenever one of the vertices is outside a 300 * 300 region. I'm assuming this has something to do with a very basic implementation of a projection in these lines:

            currentVertex.elements[0][0] = currentVertex.elements[0][0] * 300 + 300;
            currentVertex.elements[0][1] = -currentVertex.elements[0][1] * 300 + 300;

Can anybody help me out howto change this piece of code so it works with my projection matrix?

I've tried removing the last 2 lines and changing transformedVertices[j] = currentVertex; into transformedVertices[j] = currentVertex.x(projection); with no result.

Tetrad
  • 30,124
  • 12
  • 94
  • 143
Thomas
  • 836
  • 2
  • 8
  • 18
  • 6
    Why can't you use matrices? – House Mar 14 '12 at 20:13
  • I don't think javascript supports matrices or matrix transformations by default. You'd have to write classes etc to do this. Creating a basic matrix class I can do, but I don't know how to implement rotation and world to view matrix transformations. – Thomas Mar 14 '12 at 20:16
  • p.s. I don't want to use WebGL since this isn't supported by all browsers. – Thomas Mar 14 '12 at 20:17
  • It's not going to happen without matrices. The notion of 3d to 2d is something that e.g. OpenGL does itself, because, of course, your monitor is a 2d plane. Rotation aside, you still run into the issue of projection transformation and perspective division, which you will need if you want to have any sense of depth. That link is related to OpenGL, but you'll face the same issues. –  Mar 14 '12 at 21:09
  • You can calculate everything using matrices by hand or with a computer algebra system, and just plug those in. –  Mar 14 '12 at 21:42
  • 1
    You can do everything without matrices. A rotation matrix is just a series of sines and cosines. A translation matrix is just a addition. A scale matrix is just a few multiplications. For the perspective projection matrix the easiest way is just to divide by Z. You can do any of these calculations manually. Just look up online the usual contents of these matrices, on paper take a generic (x,y,z) and multiply that by each of the matrices to figure out how they affect each of the coordinates. With that said, I've done this in Javascript before, and I used some Matrix library from the web. – David Gouveia Mar 14 '12 at 22:09
  • @DavidGouveia: "For the perspective projection matrix the easiest way is just to divide by Z." Um, that's not how perspective works. You have to do a little more than just "divide by Z." – Nicol Bolas Mar 14 '12 at 23:31
  • 7
    "Since I cannot use matrices or built in transformation methods I need another way." No, you don't. All you need to do is implement matrices in JavaScript. This isn't hard; a 4x4 matrix is just an array of 16 values, just like a 4-element vector is an array of 4 values. You then need some functions to implement matrix operations like matrix multiplication and so forth. It's not particularly hard. – Nicol Bolas Mar 14 '12 at 23:32
  • 1
    @NicolBolas Yes I know that a "real" perspective projection matrix does a lot more than that e.g. mapping to normalized device coordinates, taking into account the near and far planes, distorting the scene to account for the aspect ratio. I should have put more emphasis on the "easiest way" and been more clear about it but I was running low on characters for the comment. But basically, by easiest I mean it's a good educational starting point in order to get some perspective happening in your scene. A full understanding of projection matrices was outside the scope of my comment. – David Gouveia Mar 14 '12 at 23:47

4 Answers4

7

Linear Algebra Library

Although you can do all the calculations without using matrices, using them makes everything significantly easier. For starters, if you don't want to implement matrices and vectors yourself, I've successfully used the following library before:

Sylvester - Vector and Matrix Math for Javascript

You can just copy and paste the packed version of the library at the top of your code - it's a one liner.

If after this you still intend to do this without matrices, the process is pretty much the same as I'm going to show, but you'll have to unroll all of the matrices multiplications yourself into their own separate expressions, and run them separately.

Example

As for an example of how to use it, I've digged up some old code of mine which you can try below:

http://jsfiddle.net/JMa9b/

I'm not sure how much that will be helpful to you though. The code is a mess - it was done many years ago when I was just starting to learn about 3D, and it was pretty much just me messing around trying to get some 3D image rendered from scratch. Also, I didn't even know Javascript, so I was just making it up as I progressed, so disregard anything that looks wrong language wise.

More Info

The most relevant is the way I calculate the world matrix that is rotating the object:

var cx = Math.cos(rotX);
var sx = Math.sin(rotX);
var cy = Math.cos(rotY);
var sy = Math.sin(rotY);
var cz = Math.cos(rotZ);
var sz = Math.sin(rotZ);

var rotXMatrix =$M([
  [1,0,0,0],
  [0,cx,sx,0],
  [0,-sx,cx,0],
  [0,0,0,1]
]);

var rotYMatrix = $M([
  [cy,0,-sy,0],
  [0,1,0,0],
  [sy,0,cy,0],
  [0,0,0,1]
]);

var rotZMatrix = $M([
  [cz,sz,0,0],
  [-sz,cz,0,0],
  [0,0,1,0],
  [0,0,0,1]
]);

var translationMatrix = $M([
  [1,0,0,0],
  [0,1,0,0],
  [0,0,1,0],
  [posX,posY,posZ,1]
]);    

worldMatrix = Matrix.I(4);
worldMatrix = worldMatrix.x(rotXMatrix);
worldMatrix = worldMatrix.x(rotYMatrix);
worldMatrix = worldMatrix.x(rotZMatrix);
worldMatrix = worldMatrix.x(translationMatrix);

Adding scaling would have been pretty easy too. For instance, for a uniform scale, it would be something like:

var scaleMatrix=$M([
  [scale,0,0,0],
  [0,scale,0,0],
  [0,0,scale,0],
  [0,0,0,1]
]);

The most important thing is that when creating the World Matrix for your objects, you must multiply these individual matrices in the correct order, which is scale, rotation and translation.

As for the View Matrix for your camera, it's somewhat similar to the World Matrix, but everything is done the other way around. You negate the rotation angles and the positions , and multiply the matrices together in the inverse order as the one above.

I got the formulas from here by the way (they can be slightly different in OpenGL though).

Notes on Perspective

For the perspective projection I'm simply taking the view space X and Y components and dividing by Z. That works in a simple "toy" case like this, but if you want to deal with other parameters such as the aspect ratio, field of view, near and far planes, etc, you'll want to do it with a matrix too. For example, a matrix such as this (taken from DX documentation):

2*zn/w  0       0              0
0       2*zn/h  0              0
0       0       zf/(zf-zn)     1
0       0       zn*zf/(zn-zf)  0

The way to use such a matrix is that you take your coordinates in view space (after applying the world and view matrix) with an added 1 as the fourth component, i.e. (x,y,z,1), multiply that by the projection matrix, and then homogenize the result back into 3D by dividing everything by W, i.e. (x,y,z,w)=(x/w,y/w,z/w).

And by the way, the simplest perspective matrix possible is something like:

1 0 0 0
0 1 0 0
0 0 1 1
0 0 0 0

Which basically discards the W component and stores Z on it. Then when you homogenize the result is the same as dividing by Z, which is what I did directly in my code.

David Gouveia
  • 24,875
  • 5
  • 84
  • 124
  • Finally found some time to try and implement this. Your example code is absolutely perfect, exacly what I need! I've managed to get this working with all kinds of javascript classes for the world, camera and objects in it. I've got only one question left, I noticed you have a world and camera matrix, now I want to be able to rotate an object itself without rotating the world or camera. From XNA I remember something called an Identity matrix. I can create this exactle like the camera and world in your example, but how do I use this in the draw method? – Thomas Apr 08 '12 at 12:31
  • my draw code is now: var comboMatrix = world.WorldMatrix.x(camera.ViewMatrix);
    How do I add the identity matrix to this? Do I first transform this with the view or world matrix or with comboMatrix?
    – Thomas Apr 08 '12 at 12:35
  • After playing around some more, i also find it difficult to implement the projection matrix, ill post some code below. – Thomas Apr 08 '12 at 14:06
  • @Thomas You are mistaken about something. The world matrix is actually what you use to transform individual objects. You should have a different world matrix for each object, and that is where the rotation goes. An identity matrix is just a matrix with ones in the main diagonal and zeros everywhere else, and it represents the concept if not being transformed at all. – David Gouveia Apr 08 '12 at 21:13
  • @Thomas As for the projection matrix, you need to convert all your other code to work with 4x4 matrices and 4D vectors. Then you add the projection matrix to the end of the combo matrix, same way as the view matrix. Finally within the loop instead of dividing x and y by z, you need to divide x y and z by w instead. You should not need any change to the viewpor transformation at the end, unless the size of the viewport changes. I recommend researching the subject a bit. Do not have time now to provide a full example, and hate typing on a tablet :-P – David Gouveia Apr 08 '12 at 21:19
  • Thanks for the update, the part about the world matrix is absolutely clear, but would it make sense to have a world matrix for the entire scene, as well as a world matrix per object? This way it would seem I can rotate or transform all objects at the same time. For example, I have a "table top" like view on my objects, I then want to rotate all objects by 90 degrees. I know I could simply change the camera for this, but it seems easier to just rotate the entire scene (animated rotation) Moving and rotating the camera would require a lot more math I think. – Thomas Apr 09 '12 at 10:18
  • I think I have part of the projection matrix working with the viewport width, height, near plane and far plane. Although rotating the camera shows some unexpected results, also moving the camera towards the object gives some strange behaviour. When the camera z-position is < 0 it works fine, whenever the z position > 0 the entire scene just flips, its like looking backwards and upside down. I think this all relates to the view and projection matrices somehow, I just haven't been able to resolve this yet. If possible I'll try to setup a working sample online. – Thomas Apr 09 '12 at 10:22
  • @Thomas Yes you can have a world matrix for the entire world, which you'd multiply right after the individual world matrix. That's pretty much the concept of having a bone hierarchy, with the world being considered as an object itself in this case. But moving the camera is no different from this. The view matrix is not some "special" camera matrix at all, it's just a world matrix like any other, but applied to the entire world, and moving things in the opposite direction than they normally would. – David Gouveia Apr 09 '12 at 12:32
  • @Thomas Not sure about your other problem though. The JS example I linked in this answer is extremely simplistic. Since then I've had another try at it, and my second attempt had a bit more stuff in it. For reference here's the relevant bit of code. See if you can get anything useful out of it. I think the flipped-scene syndrome was corrected by clipping (although I'm not doing actual polygon clipping). I also used a slightly different projection matrix (here). – David Gouveia Apr 09 '12 at 12:53
  • I've put a sample online here: http://test.cellgames.nl/3D/Default.aspx

    this shows the issues at hand. Move the camera with w,a,s,d and arrow keys. Still work in progress but it's starting to look like what I want :)

    – Thomas Apr 10 '12 at 18:29
  • @Thomas Okay, as I thought it's not a camera problem, it's simply that you need to have a check that prevents polygons that are behind the near plane from being drawn. Check line 40 on my previous link for how I solved it once, although it's not the most complete implementation. Basically, before the perspective divide, check if the values are outside the [-W,W] range for X and Y and [0,W] for Z. Depending on the projection matrix used it could also be [-W,W] for Z, don't recall which is your case. – David Gouveia Apr 10 '12 at 21:26
  • Ok Thanks for the post, I'm going to try to implement this as soon as I can. I also want to try to work in some gradient shading of some kind so the part of the object that's closest to the camera is brighter then the rest. From there I also want to try to draw textures. Do you know if it's possible to transform images realtime based on the vertices so the can be used as textures? – Thomas Apr 11 '12 at 09:41
1

Two alternatives I'd recommend:

A) Calculate all the matrices by hand. It doesn't require any matrix4 classes or anything, just a lot of math. You'd have to calculate something like

NDC_Coordinates=PerspectiveDivide(Projection*Cameraview*Modelview*Vector)

The Modelview one is optional, so it'll be the CameraView that will probably contain all the annoying rotation and translation stuff.

B) Make a Doom-style renderer. I won't elaborate much here because there are tons of resources online:

Anko
  • 13,393
  • 10
  • 54
  • 82
1

Well those are for 3D transformations/rotations, to convert from 3D to 2D (to draw on a screen), xp = x * scale / (z + scale) yp = y * scale / (z + scale)

where scale is a depth scaling factor (500 for example) after that, generally you would add that to half the screen dimensions to center it

read above for in 3D manipulation

CobaltHex
  • 11
  • 1
1

Another alternative other then Sylvester which is already mentioned is gl-matrix which is meant to be a fast implementation of matrices vectors and quaternions in javascript. That should be most of what you need.

Nicolas K.
  • 163
  • 4