Saturday 4 April 2009

Adventures in 3D: Interlude

As mentioned in the intro to this series, this is largely an unplanned foray into 3D graphics, and the code presented here is as I write it, without necessarily being the best or neatest way to do things. At this point, I feel a need to do some refactoring on the code to try and make a solid base to build on further. These bits are not necessarily necessary or instructive, so feel free to skip this bit if you like, although if you're going to carry on I suggest you download the source so that future entries make sense. Even if you do continue reading, there are some bits that I'm simply going to point out rather than describe why I've made those decisions.

So far we've only created one object, our lonely spheroid. Naturally you are already deep into planning your own FPS that will, like, totally make Halo look like Wolfenstein in comparison, and so we'll need to start thinking about creating and managing multiple objects. An object is simply a bunch of polygons that are glued together - when the object moves 3 units to the left, all the polygons in that object move 3 units to the left. Let's change the class hierarchies around a bit. Top of the tree is a SceneObject, something that can be a) rotated and b) drawn. That extends to two classes. A BasicSceneObject is an object such as a sphere or a cube, or some other 3D shape we may wish to create. BasicSceneObjects are really just a way of group together the other type of SceneObject, which is a Primitive, the 2D polygons that make up that object - to all intents and purposes, this is our Triangle class, although we could choose to render objects with squares, pentagons, or icosagons if we so choose.

The BasicSceneObject class defines how to draw and rotate multiple Primitives - just loop over every Primitive in the object, and also allows a method to get at it's polygons. This becomes important because when sorting for z-order, we need to consider all polygons in the scene together at the same time, for instance if two objects overlap. The BSO also defines offsets for x,y,z, which defines where the object is in the world, and a translate() method to move the object.

Now we've got a basis for objects, we can add some concrete object classes. We simply extend BasicSceneObject, and in the constructor define how to build it in terms of Primitives. So there's a Spheroid object, which just uses the code we had in createScene() previously, and a Cuboid. The Cuboid just defines 12 polygons (6 faces of 2 triangles). The createScene() method now just becomes pretty simple:

    BasicSceneObject sphere = new Spheroid(100,60,60,50);
sphere.translate(-100, -30, 100);
BasicSceneObject sphere2 = new Spheroid(40,30,10,50);
sphere2.translate(50, 20, -10);
BasicSceneObject cube = new Cuboid(40,40,40);
cube.translate(100, 100, 30);

scene.add(sphere);
scene.add(sphere2);
scene.add(cube);



On the lighting front, until now the light has simply been a hardcoded vector in the Triangle class. Now there's a LightScene object, which could contain multiple Lights. A Light has a direction, a position (which is not yet taken into account), and a colour (also not used yet), and as things progress may have some other characteristics specific to a particular type of lighting (e.g. an ambient light, spotlight etc.). The LightScene is passed to a light() method on the Primitive class to decide what color the polygon should be rendered with.

The final notable change is that you may have noticed the Y-axis problem. That is, traditionally the Y axis points up. But in Java 2D, the Y axis goes down the screen, so essentially we're rendering the scene upside down. There's a simple answer to this, and it's back in the AffineTransform class we first met way back in Part I. That time, we cheated by moving the axis origin to the centre of the screen with the Graphics2D.translate() method, so we never had to actually touch the AffineTransform ourselves. This time, we'll create an actual AffineTransform which represents a matrix:

    |  1    0   width/2  |
| 0 -1 height/2 |
| 0 0 1 |


which, once you've famliarised yourself with matrix maths, you'll see means

x' = x + (width/2)
y' = -y + (height/2)


Although we're not explicitly stating it as such, this is a model-to-device transformation, the last step in our pipeline. As a minor optimisation, the AffineTransform object is created ahead of time and reused in each call to paintComponent(), rather than a new Transform object being created each time. However, as the panel can be resized, we catch the call to setBounds() and recreate the Transform when required.

Now that things are a bit better defined, download the source and let's move on.

No comments: