Saturday, 30 August 2008

Adventures in 3D: Part I - The Basics

[If you haven't already, you might want to read the Intro]

So, let's set off with some fairly modest aims - we just want to produce a simple 3D scene that we can rotate using the mouse. There's a few simple concepts that we'll need to get under our belts to do that. Thankfully, most of them are little more than GCSE maths. If you want to really get a grasp on these, you could do a lot worse than this page.

Let's also put a disclaimer on this. This is my first real dive into this sort of stuff, and I make no claim to be an expert, or that what's included here is necessarily the fastest, easiest or correctest way to achieve the aims. It's just a gentle amble trying to discover the key concepts and hopefully get something out the other end that looks reasonable.

First concept: the Point. A Point is exactly what you'd think it would be, a point in space. It consists of x, y and z coordinates, and it can be translated in some way to another point in space. It has location, but no length, and no direction.

Second concept: the Vector. A Vector can sometimes look suspiciously like a Point, but it's important not to confuse them. Like a Point, a Vector consists of x,y,z components, but the key difference is that these indicate a change in those axes, not a point. Think of it as an arrow. So a Vector (2,4,5) goes 2 units along the X axis, and 4 units along the Y axis, and 5 units along the Z axis. It has a direction, and it has a length, but it doesn't exist in any particular location. In that sense it's the precise opposite of a Point. You can do funky things with a Vector. You can take another Vector and calculate their dot product, which gives you a measure of how orthogonal they are. You can also take another Vector and find their cross product, which will result in another Vector which points at right angles to the first two. You can take a Vector and scale it, to double it's length, or halve it's length, or make it's length equal to one, a.k.a. normalisation. Under the covers, all of these things involve nothing more than a bit of multiplication, but they are immensely powerful.

Third concept: the 3 sided polygon, a.k.a the Triangle. Triangles are useful things for 3D graphics, because they are coplanar. Safe to say, everything we build we'll make out of triangles in 3D space. All a triangle consists of is three Points. From those Points, you can work out what the edges look like, and represent them as Vectors, and with those Vectors you can work out a cross product to get a Vector that points perpendicular to the triangle.

Fourth and final concept: Coordinate systems. A coordinate system simply means that you agree on the numbers you're going to use to define your x,y,z values. When it comes to 3D graphics, you're usually concerned with at least two coordinate systems. World coordinates tell you where the object is in absolute space. If the object is not moving, its world coordinates remain the same. But if you're moving the camera around it, the object moves on screen, so it must be moving in some coordinate system. That's your view coordinates - the position of the object in relation to your eye. If your eye (the camera) moves, the object moves in your view coordinates. Generally, the two are equal and opposite. Let's say I'm looking at the front face of a cube, and I want to see the right hand side of it. I could do two things to achieve the same effect. I could stay still and turn the cube 90 degrees clockwise, which would involve changing it's world coordinates. Or I could leave the cube alone and move myself 90 degrees anti-clockwise, which would be a change in my view coordinates. Either way, the result with regards to the cube is the same. In the following examples, our viewpoint will stay the same, and we'll just rotate the object in space. If we wanted, we could achieve the same thing by keeping the same world coordinates and moving the camera instead.

Right, let's get cracking on some code. We're working in Java, so first thing we're going to want is a JFrame and a JPanel to draw our scene on. This is pretty standard stuff. Standard practice is to override the JComponent's paintComponent() method, call super.paintComponent(), and then do our business. We'll use one little trick here. We'll generally be creating stuff centred around the origin, which is at point (0,0,0). The problem is that as far as the Java2D libraries are concerned, the point (0,0) is the top left of the screen, so everything we do will be squeezed up in that corner. We could load our equations involving coordinates with some kind of offset to push it into the middle of the screen, but the Graphics2D object offers us an easier solution, the AffineTransform. The Javadoc looks a little scary, but ultimately it's just a way to tell Java2D to automatically do the offset for us without having to think about it in the calculations. It's made even easier by helper methods such as translate() which mean you don't even have to get your hands dirty in matrix maths. So if we specify graphics.translate(panel.getWidth()/2, panel.getHeight()/2), then anything that we draw at (0,0) will be in the middle of the panel, and not the top left.

We'll have to draw something, otherwise we'll just get a black screen, so let's plot some Points. That gives us an opportunity to write a class to model our first concept, the Point. As we already said, it just has three coordinates, so those are just instance members of our class. Let's not bother with getters and setters, it's just unnecessary bloat, and those equations are going to look pretty ugly otherwise. This isn't Enterprise Code now, you know.

I'm also going to define an interface, Drawable, with a single method draw(Graphics2D) which any objects, be they Points or Triangles or whatever, will implement. In the case of the Point, we're just going to draw that point at the x,y coordinates, and ignore the z component. You might call it lazy, I call it Parallel Projection. We'll store all the objects to be drawn in an ArrayList of Drawables, and then the panel just has to iterate that list and ask each object in turn to draw itself.

I think we're ready to go - download the source. So, we bung all this together, and what do you get?

Yup, that's, errr, impressive alright. Still, we've got the basic code in place, let's head to Part II - Round We Go.

No comments: