Tutorial 6: Build a 3D Simulation with a Continuous 2D field

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:

Write the Body class

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.

Write the Model

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(new MersenneTwisterFast(seed), new Schedule(1));
        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!

We're storing the sun and planets in a Continuous2D. Since we don't care about neighborhood information, the discretization is the size of the entire field (out to Pluto). Now we'll load the bodies and schedule them:


    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);
        }
    
    }

Write the 3D Display Code

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.

The displayer for 3D is Display3D, just as the displayer for 2D is Display2D. These classes have very similar APIs, but Display3D has a few additional gizmos we'll talk about later. Now add the following code, which should look very familiar by now:


    public static void main(String[] args)
        {
        Tutorial6WithUI vid = new Tutorial6WithUI();
        Console c = new Console(vid);
        c.setVisible(true);
        }

    public Tutorial6WithUI() { super(new Tutorial6( System.currentTimeMillis())); }
    public Tutorial6WithUI(SimState state) { super(state); }
    public String getName() { return "Tutorial 6"; }
    public String 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.


    public void setupPortrayals()
        {
        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. :-)

We're loading the planets into the ContinuousPortrayal3D. To do so, we associate each planet with a unique SpherePortrayal3D (so we can have each one drawn with a different color and size sphere).

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.

The 50 means that each sphere will have 50 divisions. More divisions means that the sphere more closely resembles a true sphere (it is drawn with more): but they take longer to draw. The default for Java3D spheres is 15, which draws quickly but looks fairly crummy. 50 is a lot, but we want pretty spheres and we only have ten objects, so we can afford it.

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);
        }
    }

Run the Universe!

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!

Add Images

But wait, there's more! In the tutorial6 directory there are several JPEG images which hold spherical sun and planet "textures" (3D-speak for images wrapped around a shape). Let's wrap them around our planets. We need to modify the Tutorial6WithUI.java file. First we add a simple function for loading images:

    /** 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!

We can perform a transform (like a rotation) on SimplePortrayal3Ds -- of which SpherePortrayal3D is one -- by wrapping them in another SimplePortrayal3D called a TransformedPortrayal3D. This outer portrayal has facilities for transformations to perform on its underlying SimplePortrayal3D, much like the Display3D has facilities for transformations on the entire environment. Change the code as follows:

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.