Projecting Coordinates onto a Screen

Following the 3D Graphics Engine tutorial series by javidx9. I'm loosely transcribing the learnings from C++ to JavaScript with HTML canvas.

Notes:

Part 1 was about the most important piece of 3D graphics (and likely the most complicated); translating 3D points to a 2D plane with perspective.

Refer to video but:

The projection is made around the origin (0, 0, 0)
Axes can be used interchangably, but this is assuming Z points frontward, Y-downward(or up doesn't matter in this part) and X-right.

From trig: tan(angle) = O/A, also fov/2 is our angle from origin.
In our projection we use tan(fov/2) to get the ratio (X-distance from origin / Z-axis distance from orgin)

This is the ratio of how a X coordinate grows with distance / the how much space we can see at at a distance from the origin.
eg. tan(33.3..) = O/A = X/Z = 2/3, for every 3 Z units, we see 2 X units.
In order to create this perspective this perspective in a 2D plane, we need to inversely squeeze it

Eg. if our 2D planes see -1 to 1 and assuming tan(33.3..) is our tan(fov/2)
point (2X,3Z) should be seen at at the edge at 1
point (1X,3Z) should be seen at at 0.5X
point (-2X,6Z) should be seen at at -0.5X
This applies to Y.

This can be expressed in the equation 'X*(1/tan(33.33..)) / Z'

A [X, Y, Z] vector projected will be [X*(1/tan(fov/2)/ Z, Y*(1/tan(fov/2))/ Z]
but we're not done.

We need to map this to a screen eg. a 800x600, if we map our -1 to 1 per axis projection directly onto the screen we'll end up with a stretched image.
We need to inversely squeeze/stretch one of our axes with it to counter the stretching.
Eg. aspect ratio 4/3 inversely is 1/(4/3) or 3/4

[X*(1/tan(fov/2)/ Z, Y*(1/tan(fov/2))/ Z] can become [(3/4)*(X*tan(fov/2)/ Z, (Y*tan(fov/2)/ Z] to squeeze the projection on the X axis relative to the aspect ratio.


We also add a normalized Z axis depth for our projection. (Not sure exactly why, likely for optimisation).
From a zNear plane to a zMax plane, to make our 2D screen projection a 3D frustrum slice.
anything in between these planes is mapped from 0 to 1.

Eg. using the video a frustrum on the Z axis 1 to 10 has a range of 9
The furtherest point 10 should be 1 and closest point 1 should be 0
Following the 'scalar /Z' format we need an equation that Z*equation/Z = 0 to 1 
or (as matrices can't scale components of a vector by others eg. X /Z) Z*equation = 0 to Z when we scale the whole output vector.
This equation is Z*(zFar/(zFar-zNear) - (zFar*zNear)/(zFar-zNear).

Our output vector will be [(3/4)*(X*tan(fov/2) / Z, (Y*tan(fov/2) / Z, (Z*(zFar/(zFar-zNear)) - (zFar*zNear)/(zFar-zNear)) / Z]
Which is a 3D vector constrained to -1 to 1 magnitudes for what should be rendered.

------------------------------
Getting to the matrix / view frustum projection (row major order unlike the video).
------------------------------
The full matrix takes a vector
[x, y, z, 1] 

4th axis at 1 is used to perform additions/subtractions 
+ extract the Z value that will be used to scale our coordinates into -1 to 1.

and is:
    X                       Y           Z                   W
[
    aspectRatio*tan(fov/2), 0         , 0                 , 0
    0                     , tan(fov/2), 0                 , 0
    0,                    , 0         , zFar/(zFar-zNear) , -zNear(zFar/(zFar-zNear)
    0,                    , 0         , 1                 , 0
                                                                                        ]
Which outputs =
[(3/4)*(X*tan(fov/2), (Y*tan(fov/2), (Z*(zFar/(zFar-zNear)) - (zFar*zNear)/(zFar-zNear)), Z]

Which divided by our 4th vector component is our projected point/vector:
[(3/4)*(X*tan(fov/2)/Z, (Y*tan(fov/2)/Z, (Z*(zFar/(zFar-zNear)) - (zFar*zNear)/(zFar-zNear))/Z, 1]

                    
By Alanas Jakubauskas