## Introduction

When it comes to working with 3D graphics, the best and most powerful tool to use would be DirectX. Microsoft’s GDI+ would be a poor choice since it comes with no 3D support whatsoever. However, what’s to say we can't try to use GDI+ to draw some basic 3D shapes?

For simple 3D plotting in a Form or PictureBox, using DirectX would be too much. So the basic gist of what we need to do to plot 3D points with GDI+ is to:

- Create a
`Point3D`

class to hold `X`

, `Y`

, and `Z `

values - Handle all the math for 3D transformations with those
`Point3D`

’s - Set up a
`Camera `

class to establish the point of view - Use some math to a
`Point3D `

into a regular Point

To demonstrate the procedures, we are going to create a cube and rotate it in all directions.

## 3D Points to 2D Points

There is no direct way to convert a point in 3D space into a 2D point simply because that does not make any sense. In order to convert between spaces, we need to know from where we are looking at the 3D point. For that we'll need two values, the camera’s `Z`

position and the `zoom`

.

To calculate the `zoom`

, I found using the monitor’s `width `

gives a pretty “square” 3D drawing. (Less distortion in other words):

double zoom = (double)Screen.PrimaryScreen.Bounds.Width / 1.5;

So with the `zoom`

, we can now calculate the camera’s `Z`

position. For this example, I use the cube’s top right point and the cube’s center as a reference in order the keep the camera at the same relative distance. That prevents the cube from looking like it is getting smaller and bigger when it rotates.

Math3D.Point3D anchorPoint = (Math3D.Point3D)cubePoints[4];
double cameraZ = -(((anchorPoint.X - cubeOrigin.X) * zoom) /
cubeOrigin.X) + anchorPoint.Z;
Math3D.Camera camera1 = new Math3D.Camera();
camera1.Position = new Math3D.Point3D(cubeOrigin.X, cubeOrigin.Y, cameraZ);

Now, with perspective values, we are ready to take our 3D points and get a simple (`X`

, `Y`

) value out of them. Cold hard math here, if you can understand it that is good, if not just go with it.

Point point2D = new Point();
if (point3D.Z - camera1.Position.Z >= 0)
{
point2D.X = (int)((double)-(point3D.X - camera1.Position.X) /
(-0.1f) * zoom) + drawOrigin.X;
point2D.Y = (int)((double)(point3D.Y - camera1.Position.Y) /
(-0.1f) * zoom) + drawOrigin.Y;
}
else
{
Point tmpOrigin = new Point();
tmpOrigin.X = (int)((double)(cubeOrigin.X - camera1.Position.X) /
(double)(cubeOrigin.Z - camera1.Position.Z) * zoom) + drawOrigin.X;
tmpOrigin.Y = (int)((double)-(cubeOrigin.Y - camera1.Position.Y) /
(double)(cubeOrigin.Z - camera1.Position.Z) * zoom) + drawOrigin.Y;
float x = (float)((vec.X - camera1.Position.X) /
(point3D.Z - camera1.Position.Z) * zoom + drawOrigin.X);
float y = (float)(-(vec.Y - camera1.Position.Y) /
(point3D.Z - camera1.Position.Z) * zoom + drawOrigin.Y);
point2D.X = (int)x;
point2D.Y = (int)y;
}

## Drawing the Cube

The basis for drawing in 3D is set, so now we can use it to create a `Cube`

class. The cube will have `width`

, `height `

and `depth `

and will thus have 6 faces. The starting cube can simply be defined by creating an array for 24 total points and then defined as given below:

Math3D.Point3D[] verts = new Math3D.Point3D[24];
verts[0] = new Math3D.Point3D(0, 0, 0);
verts[1] = new Math3D.Point3D(0, height, 0);
verts[2] = new Math3D.Point3D(width, height, 0);
verts[3] = new Math3D.Point3D(width, 0, 0);
verts[4] = new Math3D.Point3D(0, 0, depth);
verts[5] = new Math3D.Point3D(0, height, depth);
verts[6] = new Math3D.Point3D(width, height, depth);
verts[7] = new Math3D.Point3D(width, 0, depth);

We will use this array of points later when we want to rotate the cube, but for now they are ready to be drawn. The drawing has no real trick to it; the lines just have to be drawn in the correct order:

g.DrawLine(Pens.Black, point3D[0], point3D[1]);
g.DrawLine(Pens.Black, point3D[1], point3D[2]);
g.DrawLine(Pens.Black, point3D[2], point3D[3]);
g.DrawLine(Pens.Black, point3D[3], point3D[0]);
g.DrawLine(Pens.Black, point3D[4], point3D[5]);
g.DrawLine(Pens.Black, point3D[5], point3D[6]);
g.DrawLine(Pens.Black, point3D[6], point3D[7]);
g.DrawLine(Pens.Black, point3D[7], point3D[4]);
g.DrawLine(Pens.Black, point3D[8], point3D[9]);
g.DrawLine(Pens.Black, point3D[9], point3D[10]);
g.DrawLine(Pens.Black, point3D[10], point3D[11]);
g.DrawLine(Pens.Black, point3D[11], point3D[8]);
g.DrawLine(Pens.Black, point3D[12], point3D[13]);
g.DrawLine(Pens.Black, point3D[13], point3D[14]);
g.DrawLine(Pens.Black, point3D[14], point3D[15]);
g.DrawLine(Pens.Black, point3D[15], point3D[12]);
g.DrawLine(Pens.Black, point3D[16], point3D[17]);
g.DrawLine(Pens.Black, point3D[17], point3D[18]);
g.DrawLine(Pens.Black, point3D[18], point3D[19]);
g.DrawLine(Pens.Black, point3D[19], point3D[16]);
g.DrawLine(Pens.Black, point3D[20], point3D[21]);
g.DrawLine(Pens.Black, point3D[21], point3D[22]);
g.DrawLine(Pens.Black, point3D[22], point3D[23]);
g.DrawLine(Pens.Black, point3D[23], point3D[20]);

Alas we have a cube! By the way, `g`

is a `Graphics `

object from wherever you want, be it the Form, PictureBox, or like in my example a Bitmap.

## Rotating the Cube

To rotate the cube we'll employ the simplest form of 3D rotation: Euler rotation. For those unfamiliar with Euler rotation, the idea is to basically turn the `X`

, `Y`

, and `Z `

values of a 3D point into a matrix like:

[ x ]
[ y ]
[ z ]

And multiply that by one of 3 matrices depending on which axis we are rotating.

For x-axis rotation, we have the matrix:

[ 1 0 0 ]
[ 0 cos(x) sin(x)]
[ 0 -sin(x) cos(x)]

For y-axis:

[ cos(x) 0 sin(x)]
[ 0 1 0 ]
[-sin(x) 0 cos(x)]

And for z-axis:

[ cos(x) sin(x) 0]
[ -sin(x) cos(x) 0]
[ 0 0 1]

The value `x `

is the degree of rotation. So for example, we could multiple matrixX times matrixOurValues and the resulting matrix will have the rotated `X`

, `Y`

, and `Z `

values. Easy huh?

For those of us who can't remember how to multiply matrices together, we are in luck, I looked it up and wrote out the three functions (the actual code is in the download):

public static Point3D RotateX(Point3D point3D, double degrees)
public static Point3D RotateY(Point3D point3D, double degrees)
public static Point3D RotateZ(Point3D point3D, double degrees)

The only thing we need to do for rotating the cube now is to loop through that array of 24 points and rotate each of the points the same number of degrees. The only tricky part is that we want to first translate (move) all the points so that the cube’s origin is at `(0, 0, 0)`

, then we rotate, then translate back to the original position. That makes it so that the cube will stay in place as it rotates.

## Gimbal Lock

Some math-savvy people will quickly see that there is a fatal flaw to this kind of rotation. Matrix multiplication is not like normal multiplication where A * B = B * A. In multiplying matrices, A * B is not the same thing as B * A. In other words, applying different degrees of `X`

, `Y`

, and `Z `

rotations in different orders will make the outcome different.

This problem is referred to as Gimbal Lock, where we rotate one way, then want to rotate on an axis but the resulting rotation seems to be on a different axis.

Unfortunately this problem cannot be truly overcome using Euler rotation. If it is something simple, you can try using `if`

statements to determine the order in which `RotateX`

, `RotateY`

, and `RotateZ`

should be applied. Otherwise I recommend reading into Quaternions.

## Conclusion

Take a look at the attached project and application to see how it all comes together. You will also see that despite the intense mathematics, the program runs quite smoothly.

Once the math and the basics are in place, doing some basic 3D plotting with GDI+ becomes relatively easy. As I mentioned before, once you want to get into more complicated 3D shapes and movements then you'll have to take a deep breath and integrate DirectX.

## Image Rotation?

As a quick note I'd like to mention that these techniques can be theoretically used to rotate pictures in 3D space as well. The missing piece there is to find the appropriate distortion algorithms once all the points in the image have been converted. Why do that? Perhaps you want to add picture faces to your cube.