|
|||||||||||||||||||||||||
|
|||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis 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. BackgroundI 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 OrientationIn 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.
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 codeThe first class you need to become familiar with is 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 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,
You can call the 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
RotationOnce 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:
Using this technique, you can generate multiple images, each rotated a bit more than the last, to create animations like this:
This rotation logic also works in 3D, so you can use it to rotate 3D objects however you like:
PerspectiveWhen 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 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 Let's try the function out. DrawCircle(p, 100);
draws a circle that looks like this:
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:
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
About the MathThere'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 TestsI'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
|
||||||||||||||||||||||||