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