In this tutorial we will build a simple model of the Solar System using a Continuous2D, but will display the model using 3D displays and portrayals.
This tutorial teaches:
The Body class will represent the Sun and various planets in our model. All a body needs is a distance from the sun and a velocity. Create the tutorial6 directory, and in it place the following file, called Body.java:
package sim.app.tutorial6; import sim.engine.*; import sim.util.*; public class Body implements Steppable { public double velocity; public double distanceFromSun; public double getVelocity() { return velocity; } public double getDistanceFromSun() { return distanceFromSun; } public Body(double vel, double d) { velocity = vel; distanceFromSun = d; }Now we need to implement Body's steppable. Basically, each time a body is stepped, it will orbit further around the sun. Ordinarily we'd do that by getting the current location and adjusting it. But for a periodic orbit it's easily enough done by just getting the current timestep and computing the location based on that. Add:
public void step(SimState state) { Tutorial6 tut = (Tutorial6) state; if (distanceFromSun > 0) // the sun's at 0, and you can't divide by 0 { double theta = ((velocity / distanceFromSun) * state.schedule.getSteps())%(2*Math.PI) ; tut.bodies.setObjectLocation(this, new Double2D(distanceFromSun*Math.cos(theta), distanceFromSun*Math.sin(theta))); } } }
Looking good.
Now we'll add the model class (Tutorial6). The model basically creates the ten bodies, then schedules them. Not much else. Create a file called Tutorial6.java, and add to it:
package sim.app.tutorial6; import sim.engine.*; import sim.field.continuous.*; import sim.util.*; import ec.util.*; public class Tutorial6 extends SimState { static final int PLUTO = 9; // Furthest-out body public Continuous2D bodies; public Tutorial6(long seed) { super(seed); bodies = new Continuous2D(DISTANCE[PLUTO],DISTANCE[PLUTO],DISTANCE[PLUTO]); } // distance from sun in 10^5 km public static final double[] DISTANCE = new double[] {0, 579, 1082, 1496, 2279, 7786, 14335, 28725, 44951, 58700}; // diameters in 10 km public static final double[] DIAMETER = new double[] {139200.0, 487.9, 1210.4, 1275.6, 679.4, 14298.4, 12053.6, 5111.8, 4952.8, 239.0}; // period in days public static final double[] PERIOD = new double[] {1 /* don't care :-) */, 88.0, 224.7, 365.2, 687.0, 4331, 10747, 30589, 59800, 90588 };
Why not use a Continuous3D?
Well, all the planets are in the same plane, so there's really no need. But sure, we could have used a Continuous3D also. We figured since you already are familiar with a Continuous2D.... Pluto's not in the same plane! Um, yeah, well, this is a tutorial, remember! |
public void start() { super.start(); bodies = new Continuous2D(DISTANCE[PLUTO],DISTANCE[PLUTO],DISTANCE[PLUTO]); // make the bodies -- stick them out the x axis, sweeping towards the y axis. for(int i=0; i<10;i++) { Body b = new Body((2*Math.PI*DISTANCE[i]) / PERIOD[i], DISTANCE[i]); bodies.setObjectLocation(b, new Double2D(DISTANCE[i],0)); schedule.scheduleRepeating(b); } }
We finish with your usual boilerplate main(...), in case you want to run headless:
public static void main(String[] args) { doLoop(Tutorial6.class, args); System.exit(0); } }
Now we're ready to code the 3D display code. The engine underneath the code is Java3D, which does not come with the standard Java distribution: you will need to download and install the Java3D SDK for your system to compile MASON's 3D libraries.
Java3D is a sophisticated 3D Scenegraph library, and while you have considerable access to the Java3D underpinnings if you need them, we've done our best to shield you from it. In fact, for most purposes will need no Java3D function calls at all. The 3D code should look very similar to previous 2D code.
Create a file called Tutorial6WithUI.java. In this file, add the following:
package sim.app.tutorial6; import sim.portrayal3d.continuous.*; import sim.portrayal3d.simple.*; import sim.engine.*; import sim.display.*; import sim.display3d.*; import javax.swing.*; import java.awt.*; import sim.util.*; public class Tutorial6WithUI extends GUIState { public Display3D display; public JFrame displayFrame; ContinuousPortrayal3D bodyPortrayal = new ContinuousPortrayal3D();
Why ContinuousPortrayal3D? Aren't we using a Continuous2D?
Many 3D portrayals can display either 3D and 2D fields. |
public static void main(String[] args) { new Tutorial6WithUI().createController(); } public Tutorial6WithUI() { super(new Tutorial6( System.currentTimeMillis())); } public Tutorial6WithUI(SimState state) { super(state); } public static String getName() { return "Tutorial 6: Planets"; } public static Object getInfo() { return "<H2>Tutorial 6</H2> Planetary Orbits"; } public void start() { super.start(); setupPortrayals(); } public void load(SimState state) { super.load(state); setupPortrayals(); } public void quit() { super.quit(); if (displayFrame!=null) displayFrame.dispose(); displayFrame = null; display = null; }
Next we need to define our personal setupPortrayals() method. This is where we'll put most of the relevant 3D code. The first thing we'll do in this code is tell our Display3D to remove any existing scene it had constructed (if any).
public void setupPortrayals() { display.destroySceneGraph();
Next we add portrayals for our planets. This needs to be done after destroying the scene graph because it could potentially try to modify Java3D objects which Java3D has (too cleverly) shared with existing objects in its scene graph and which are not allowed to be modified. Destroying the scene graph prevents this possibility.
Tutorial6 tut = (Tutorial6) state; bodyPortrayal.setField(tut.bodies); // planetary colors Color colors[] = {Color.yellow, Color.white, Color.green, Color.blue, Color.red, Color.orange, Color.magenta, Color.cyan, Color.pink, Color.white}; Bag objs = tut.bodies.getAllObjects(); for(int i=0;i<10;i++) bodyPortrayal.setPortrayalForObject( objs.objs[i], new SpherePortrayal3D(colors[i], (float) (Math.log(Tutorial6.DIAMETER[i])*50), 50));
Wait a minute. Math.log?
Astronomical differences are huge. We need to scale them down in a nonlinear fashion. The size of the planets will be the log of their true size, times fifty. That makes them more easily visible. And yes, it also makes the Earth nearly as big as the Sun. :-) |
SpherePortrayal3D draws objects as, you guessed it, spheres. There are lots of other portrayal options: cones, cubes, cylinders, arbitrary 3D shapes, images and text. Most portrayals by default are drawn as flat colored objects of varying transparency, not needing external light, or they can be wrapped with an image.
How about shading and reflection?
You can set the Java3D Appearance object for your portrayals, to add any Java3D gizmos you need. You can also provide lights as portrayals. |
We conclude setupPortrayals() by telling the Display3D to reset() itself, as usual, and to set itself up. In Display2D this was done with a simple repaint(). But Display3D needs to be told, at start() or load() time, to build its Java3D scene graph:
display.reset(); display.createSceneGraph(); }
Now let's add the init() method:
public void init(Controller c) { super.init(c); Tutorial6 tut = (Tutorial6) state; bodyPortrayal.setField(tut.bodies); display = new Display3D(600,600,this,1); display.attach(bodyPortrayal, "The Solar System"); // scale down to fit the region a little beyond pluto into the 2x2x2 box display.scale(1.0/(Tutorial6.DISTANCE[Tutorial6.PLUTO]*1.05));
Everything but the last line should be clear. But we have a problem: if our model is as big as the solar system, we need to fit it into the viewable region of our Display3D. Ordinarily the Display3D is set up so that fitting into the window is a 2 by 2 by 2 box centered at the origin, and the eye is on the negative Z axis and is looking down the positive Z axis. But right now our model goes clear out to pluto. We solve this by scaling the entire universe down to fit into the 2 by 2 by 2 box, using the scale() method. This is an example of a transform: adjusting the universe by translating (moving) it, rotating it, or scaling it. Our solar system is centered perfectly at the origin (where we put the Sun), and is going around the Z axis, so there's no need to translate or rotate. We just scale.
The rest of the code is obvious:
displayFrame = display.createFrame(); c.registerFrame(displayFrame); displayFrame.setVisible(true); } }
Save and compile all files, then run java sim.app.tutorial6.Tutorial6WithUI. There are two ways to "zoom in" on the solar system to see the planets better. You can move the "eye" back and forth along the Z axis (dragging the right mouse button). Or you can magnify by changing the scale text field. The second is a better choice. The reason for this is that the scaled-down planets are very small objects. By the time you "zoom in" close enough to see them, they literally pass "behind" the eye and disappear from view!
/** Gets an image relative to the tutorial6 directory */ public static Image loadImage(String filename) { return new ImageIcon(Tutorial6.class.getResource(filename)).getImage(); }
Now let's set up the SpherePortrayal3D to use images rather than colors.
FROM... |
// planetary colors Color colors[] = {Color.yellow, Color.white, Color.green, Color.blue, Color.red, Color.orange, Color.magenta, Color.cyan, Color.pink, Color.white}; Bag objs = tut.bodies.getAllObjects(); for(int i=0;i<10;i++) bodyPortrayal.setPortrayalForObject( objs.objs[i], new SpherePortrayal3D(colors[i], (float) (Math.log(Tutorial6.DIAMETER[i])*50), 50)); |
CHANGE TO |
// planetary images String imageNames[] = {"sunmap.jpg","mercurymap.jpg","venusmap.jpg", "earthmap.jpg","marsmap.jpg","jupitermap.jpg", "saturnmap.jpg","uranusmap.jpg","neptunemap.jpg","plutomap.jpg"}; for(int i=0;i<10;i++) { bodyPortrayal.setPortrayalForObject( objs.objs[i], new SpherePortrayal3D(loadImage(imageNames[i]), (float) (Math.log(Tutorial6.DIAMETER[i])*50), 50)); } |
Some of the images look thresholded and crummy.
Yeah. Java3D converts the textures using what appears to be non-dithered thresholds to 256 colors. We don't know if this is a Java3D bug or if it's life with 3D graphics cards. :-( |
Now that we've wrapped our planets with images, compile and run the code again. Notice that after you press play, there's a significant (several second) pause. Here Java3D is setting up the images on the planets. We're sorry about the wait.
If you look closely at Earth you may discern another problem: it's tilted sideways (the north pole starts pointing along the orbital circle!). This is because Java3D wraps images around Spheres by wrapping around the Y Axis. We need to rotate the sphere so that its "Y Axis" actually points up at us -- essentially moving the Y Axis to the location where the Z Axis was. Specifically, we want to rotate the sphere 90 degrees in the X direction.
Why use degrees? Why not radians?
Java3D of course uses radians underneath. But we thought degrees would be easier for inexperienced model builders to work with. We may live to regret that decision! |
FROM... |
bodyPortrayal.setPortrayalForObject( objs.objs[i], new SpherePortrayal3D(loadImage(imageNames[i]), (float) (Math.log(Tutorial6.DIAMETER[i])*50), 50)); |
CHANGE TO |
TransformedPortrayal3D trans = new TransformedPortrayal3D(new SpherePortrayal3D(loadImage(imageNames[i]), (float) (Math.log(Tutorial6.DIAMETER[i])*50), 50)); trans.rotateX(90.0); // move pole from Y axis up to Z axis bodyPortrayal.setPortrayalForObject(objs.objs[i], trans); |
Recompile and run the code, and now the planets are drawn correctly.