Based on the original Euler Rotation article, it’s time to revisit the 3D graphics GDI+ world. In the last article, we discussed the fundamentals for drawing a simple cube shape on a two-dimensional bitmap surface. Now, it’s time to take it up a notch. We are going to draw a cube with shaded sides, and we are going to find a way around the pesky Gimbal Lock problem.
Avoiding Gimbal Lock
First things first, Gimbal Lock. In the last article, we encountered a strange problem when rotating the cube around separate axes. Due to the fact that matrix multiplication is not cumulative, rotating around different axes in different order produced results that were not always the same.
A way to avoid that problem is to step up to more advanced mathematics such as quaternions. However, a more simple solution is to restructure the way the
Cube class handles rotations.
Let’s compare the original structure and the new improved one. The way rotations were handled originally was to:
- Fill in the cube’s 3D points
- Rotate them (X-axis, then Y-axis, then Z-axis)
- Project it on the 2D surface
Can you spot the problem? Whenever a rotation is applied, the cube starts from scratch and reapplies all the rotations. Thus, certain rotations can seem to be going the wrong way. The answer is simple:
- Fill in the cube’s 3D points when the class is initiated
- Rotate the existing points the difference between the new rotation and the last rotation
- Project it on the 2D surface
Basically, instead of starting from scratch every time, we rotate what we already have.
Before going on to shading the cube, we have to restructure another part of the project. Instead of handling all the cube’s points separately, it is better to group them into cube faces. The advantages of doing things this way is we can avoid writing manually which points should connect to which when drawing the cube. All we have to do is tell how each face will be drawn.
Also, when it comes to shading the cube, we’ll have to know the order in which to draw the faces.
The faces class will only need some basic properties:
- Array of 2D points that correspond to the face
- Array of 3D points that correspond to the face
- The side of the cube it represents
- A 3D point that represents the center
We’ll also need to include a
CompareTo function where the
z value of the center points are compared.
public int CompareTo(Face otherFace)
return (int)(this.Center.z - otherFace.Center.z);
2D Plotting Fixed
One last thing to fix before shading the cube: the 3D projection. When I first started writing the shading routine, I discovered that the 2D plotting from the Euler Rotation article was flawed. It was drawing everything backwards! After much simplification and review, this is the new function:
private PointF Get2D(Vector3D vec)
PointF returnPoint = new PointF();
float zoom = (float)Screen.PrimaryScreen.Bounds.Width / 1.5f;
Camera tempCam = new Camera();
tempCam.position.x = cubeOrigin.x;
tempCam.position.y = cubeOrigin.y;
tempCam.position.z = (cubeOrigin.x * zoom) / cubeOrigin.x;
float zValue = -vec.z - tempCam.position.z;
returnPoint.X = (tempCam.position.x - vec.x) / zValue * zoom;
returnPoint.Y = (tempCam.position.y - vec.y) / zValue * zoom;
Let’s go over the code. The truth is this is the most basic form of 3D projection. The X and Y values are simply x/z and y/z of the 3D point. Only, we are factoring in the position of the perspective, or camera, which we placed at the center of the cube. The last key factor is the zoom variable. When projecting 3D points into a 2D surface, we have to factor in the area. In a smaller area, the cube would be more squished and vice versa. In this case, we use the width of the entire screen to keep straight lines in the cube parallel/perpendicular to the entire screen.
Shading the Cube
At last, time to shade in the cube. Believe it or not, it will be very simple, thanks to all the time we spent creating a solid foundation. Fittingly, a cube face will be shaded based on the 2D points already projected. Since the four corners could create any random quadrilateral, it is best to use the
g is the
Graphics object drawing to whatever surface is appropriate. Also, don’t worry about the
GetTopFace() call, it’s in the source code, and all it does is it finds the 2D corner points of the cube face (the top face in this example).
Now, a bit of reasoning will reveal that shading the faces at a specific order will not always display correctly. The order in which we want to fill in the cube’s faces depends on which way the cube is facing.
Luckily, that’s what the
CompareTo function in the
Face class is for. We can add all the faces into an array, sort the array, and then iterate through it. Remember that the faces are sorted based on the
z value of their center. That means the closest ones to the screen will be first. These are actually the last ones that should be filled. Thus, we iterate through the array backwards, starting from the end.
The result is impressive considering that it is all done with GDI+. A further improvement will be to create shading based on lighting. That, however, is beyond the scope of this article. For now, the sides are shaded based on a color specified.