Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A 3D Plotting Library in C#

0.00/5 (No votes)
9 Jun 2006 10  
A library which draws 3D images on any GDI+ Graphics object.

Screenshot

Introduction

This article presents a set of classes which allows you to draw pictures in a 3-dimensional coordinate space. Within this article, I'll touch on some of the mathematical concepts involved (it doesn't require anything more than what you learned in high school trigonometry), I'll explain the basics of how to use the library, and I'll list the resources I used in creating it.

Background

I recently became disturbingly obsessed with the motion of falling dominoes and the math that could describe it. I spent about two weeks scrawling calculations on the back of napkins and ATM receipts, and I covered the walls of my cubicle at work with increasingly refined problem descriptions and incomplete solutions. Once I had developed a working formula, I decided to implement it in code. This library is the end result of my attempt to animate a row of falling dominoes.

My domino calculations were based on relative positioning ("Turn right 45 degrees, then move forward 100 units") rather than absolute positioning ("Move to location [45, 12, 83]"). A language like LOGO is well suited to this kind of drawing, and my library has similar functionality. But because we're dealing in 3-dimensions, my library allows you to turn the cursor not only left and right, but also up (out of the screen, towards you) and down (into the screen, away from you).

Establishing Orientation

In order for the cursor to move around in 2-dimensions, it needs to be aware of both its location and its direction. Location can be established with a point [x,y], and we can establish direction with a vector. A vector establishes the cursor's forward direction, and is used to calculate the left and right directions.

In 3-dimensions, though, it's a bit trickier. The cursor's direction doesn't present us with all the information we need. Have a look at this example. Here are two airplanes, both flying in the same direction. Each airplane has a green guide pointing in the direction that it considers to be forward, a blue guide pointing towards what it considers to be right, and a purple guide pointing towards what it considers to be up.

Airplane Right-side-upAirplane Up-side-down

Both of these planes are headed in the same direction, but if you told them both to turn right, they'd each do something different. That's why it's not enough to establish a direction in 3-dimensions. When moving around in 3D, we need to establish an orientation. Luckily, this is pretty easy. All we need is one extra vector. In addition to a vector pointing towards what we consider to be forward, we'll also need one pointing towards what we consider to be up. Once we establish two directions, we can use them to calculate all the other directions.

Using the code

The first class you need to become familiar with is Plotter3D. The simplest constructor just takes a Graphics object. This can be any valid Graphics object, from a Form, a Bitmap, a Metafile, etc.

using (Graphics g = this.CreateGraphics())
{
    using (CPI.Plot3D.Plotter3D p = new CPI.Plot3D.Plotter3D(g))
    {
        // Do some stuff with the plotter here

    }
}

Once you've created a Plotter3D object, you can start drawing. Here's how you draw a square:

public void DrawSquare(Plotter3D p, float sideLength)
{
    for(int i = 0; i < 4; i++)
    {
        p.Forward(sideLength);  // Draw a line sideLength long

        p.TurnRight(90);        // Turn right 90 degrees

    }
}

In this function, p.Forward(sideLength) draws a side of the square, and p.TurnRight(90) turns right 90 degrees. If we repeat this sequence four times, we'll have drawn a square, and we'll end up back at our starting point. The function draws a square that looks like this:

Square

You can call the DrawSquare multiple times, to create a cube:

public void DrawCube(Plotter3D p, float sideLength)
{
    for (int i = 0; i < 4; i++)
    {
        DrawSquare(p, sideLength);
        p.Forward(sideLength);
        p.TurnDown(90);
    }
}

Notice that in this function, we call p.TurnDown(90). This turns the cursor in 3D so that it's moving away from you, into the computer screen. When we draw four squares at different locations, we end up with a cube that looks something like this:

Cube

Rotation

Once you've defined a shape, it's pretty easy to rotate it. That's one of the benefits of using relative coordinates instead of absolute coordinates. To draw a rotated shape, move the cursor to the point you want to use as the center of the rotation, turn the cursor left or right or up or down as much as you'd like, then retrace your steps back to the starting point, and draw your shape. An example will probably help. Let's rotate our square:

public void DrawRotatedSquare(Plotter3D p, float sideLength, float rotationAngle)
{
    // Since we don't want to draw while repositioning ourselves at the

    // center of the object, we'll lift the pen up

    p.PenUp();

    // Move to the center of the square

    p.Forward(sideLength / 2);
    p.TurnRight(90);
    p.Forward(sideLength / 2);
    p.TurnLeft(90);

    // Now we rotate as much as we want

    p.TurnRight(rotationAngle);

    // Now we retrace our steps to get back

    // to the (rotated) starting point

    p.TurnLeft(90);
    p.Forward(sideLength / 2);
    p.TurnLeft(90);
    p.Forward(sideLength / 2);
    p.TurnRight(180);

    // Put the pen back down, so we start drawing again

    p.PenDown();

    // Finally we draw the square as we normally would

    DrawSquare(p, sideLength);
}

So if we were to call:

DrawRotatedSquare(p, 50, 45);

we'd get something that looks like this:

Rotated Square

Using this technique, you can generate multiple images, each rotated a bit more than the last, to create animations like this:

Spinning Square

This rotation logic also works in 3D, so you can use it to rotate 3D objects however you like:

Spinning Cube 3

Perspective

When displaying 3D images on a 2D computer screen, we need to account for perspective. I shamelessly lifted my perspective logic from Paresh Solanki's excellent CodeProject article: A short discussion on mapping 3D objects to a 2D display. In my implementation, the "screen" is the plane formed by the X and Y axes, and the "camera" is a Point3D instance that you can specify in the constructor of a Plotter3D object. If you don't specify a camera position, it will default to the (completely arbitrary) location [-30, 0, -600]. Experiment with changing the camera position, and see how it affects your drawings.

Questions Which Will Probably Be Frequently Asked...

How do I draw a circle?

Short answer...you don't. You can only draw lines. Longer answer...you can fake it pretty convincingly by drawing a polygon with a lot of sides. Here's an example function which will draw an approximation of a circle with a specified diameter:

private void DrawCircle(Plotter3D p, float diameter)
{
    float radius = diameter / 2;

    // Increasing this number will create a better approximation,

    // but will require more work to draw

    int sides = 64;

    float innerAngle = 360F / sides;

    float sideLength = (float)(radius * 
      Math.Sin(Orientation3D.DegreesToRadians(innerAngle) / 2) * 2);

    // Save the initial position and orientation of the cursor

    Point3D initialLocation = p.Location;
    Orientation3D initialOrientation = p.Orientation.Clone();

    // Move to the starting point of the circle

    p.PenUp();
    p.Forward(radius - (sideLength / 2));
    p.PenDown();

    // Draw the circle

    for (int i = 0; i < sides; i++)
    {
        p.Forward(sideLength);
        p.TurnRight(innerAngle);
    }

    // Restore the position and orientation to what they were before

    // we drew the circle

    p.Location = initialLocation;
    p.Orientation = initialOrientation;
}

There are a couple of interesting things to notice about this function. First, it's actually drawing a polygon with 64 sides. If you increase the number of sides in the polygon, it'll draw a closer approximation of a circle, but you'll probably find that 64 is plenty in most cases.

You'll also notice that before we draw the circle, we save our location and orientation, and we restore it after we've drawn the circle. The reason for this is because this library does a lot of floating point math, and floating point math is imprecise. So even though we should end up back at our starting point after rotating 360 degrees, we can only really guarantee that we'll end up pretty close to the starting point. The errors caused by lack of precision are very small, but they could potentially add up after a while. So by saving our initial position and orientation beforehand, then restoring them afterwards, we can guarantee that our end point is exactly the same as our start point.

Finally, you'll notice that when saving the initial location, we simply call initialLocation = p.Location, but when saving the initial orientation, we call initialOrientation = p.Orientation.Clone(). We don't have to call Clone() on p.Location because p.Location is a Point3D, which is a value type, which means that initialLocation gets populated with a copy of the value. In contrast, p.Orientation is a reference type, which means that if we hadn't called Clone(), initialOrientation would have pointed to the same object as p.Orientation, which is not the behavior we want. The Clone() method creates a deep copy of the Orientation3D object.

Let's try the function out.

DrawCircle(p, 100);

draws a circle that looks like this:

Circle

I'd say that it looks pretty circular, all things considered. All of the rotation and animation stuff that we've learned so far also applies to this object, so we could, for example, draw a series of circles to create a sphere, and then rotate it around, like this:

Spinning Sphere

How do I draw a filled area?

You don't. You can only draw lines. As soon as you start doing anything fancier than lines, you need to start taking other things into account, like visibility determination, for example. That's beyond the scope of this project.

Why don't you mention Euler Angles, or Rotation Matrices, or Quaternions?

Because, all I really need this project to do is draw some lines in 3D, and that can be done entirely with vector arithmetic and some straightforward algebra and trigonometry. Of course, no discussion of 3D graphics is complete without mentioning a whole list of things that I haven't talked about, but this project isn't designed to be a complete 3D graphics package. This is just an easy-to-use method of visualizing and experimenting with 3D geometry. If you're looking for a high-powered, full-featured, 3D graphics library, have a look at DirectX or OpenGL.

So did you ever get around to animating dominoes, or what?

I sure did. The source code is included in the download, or you can look at the final result (exported to Flash) here.

Tips and Tricks

  • Whenever you design a new shape, try to abstract it into a separate function (like DrawCube, DrawSphere, etc.) rather than embedding the movements directly into your code. In some future revision, I'll probably create an abstract Shape class that can be built upon.
  • It's easy to lose your bearings while maneuvering the cursor around. Throughout the development cycle, I kept a toy airplane nearby and used it to keep track of the cursor's rotation. I encourage you to get a toy airplane as well, partly because it will help you visualize 3D movement, and partly because airplanes are awesome.

    Toy Airplane

About the Math

There's a lot of math under the covers here. That makes sense, given that this whole project started as an attempt to visualize a math problem that I'd been working on. Most of the math involves vectors in one way or another, and you'd do well to learn about vector arithmetic if you want to mess with the code. I learned everything I know about vector math from the book 3D Math Primer for Graphics and Game Development, which is a very good introduction to 3D math. The book gives a geometric interpretation for almost all of the math it presents, which really helps you to visualize just exactly what you're doing. I've included references to individual pages of the book in the code's XML comments.

About the Unit Tests

I've included a relatively large battery of NUnit tests with this project. Remember that floating-point arithmetic is complicated. There are about half-a-zillion different corner cases that will trip you up if you're not paying attention. Apart from having to deal with things like Infinity and NaN, it's relatively easy to perform an operation which gives you a tiny loss of precision, but which quickly multiplies into a very large loss of precision. The included tests check the results of a wide range of inputs. By and large, I've dealt with these concerns, so you don't have to worry very much if you're just using the library as is. If you're planning on extending this library, however, it would be a good idea to extend the unit tests accordingly.

References

History

  • June 7, 2006 - Initial posting.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here