In this tutorial we will build a model of randomly-moving objects and their effects on projected spaces in three dimensions. The multidimensional projection is basically an excuse to show off various neat features of the ValueGrid2DPortrayal3D.
This tutorial teaches:
The Fly class is our randomly-moving object. It wanders around a 3D grid, and as it wanders it computes how far it is away from the X axis, the Y axis, and the Z axis, and adds the Log of that amount (plus 2) to its projected location on the the YZ plane, the XZ plane, and the XY plane.
Tutorial7 will thus have four fields: a SparseGrid3D ('flies') which holds the Flies, and three DoubleGrid2Ds ('xProjection', 'yProjection', and 'zProjection') which represent the projected planes. That should be enough to make the following code fairly straightforward. Create a file called Fly.java, and put into it:
package sim.app.tutorial7; import sim.engine.*; import sim.util.*; import sim.field.grid.*; public class Fly implements Steppable { static final long serialVersionUID = 1; public void step(SimState state) { Tutorial7 tut = (Tutorial7) state; SparseGrid3D flies = tut.flies; // Move me in a random direction Int3D myLoc = flies.getObjectLocation(this); int x = flies.stx(myLoc.x + (tut.random.nextBoolean() ? 1 : -1)); int y = flies.sty(myLoc.y + (tut.random.nextBoolean() ? 1 : -1)); int z = flies.stz(myLoc.z + (tut.random.nextBoolean() ? 1 : -1)); flies.setObjectLocation(this, new Int3D(x,y,z)); // Update the projections with, I dunno, some function based on my position :-) tut.xProjection.field[y][z] += Math.log(x+2); tut.yProjection.field[x][z] += Math.log(y+2); tut.zProjection.field[x][y] += Math.log(z+2); } }
Now we'll add the model class (Tutorial7). The model holds the fields, which define a space of 30x30x30, and three 30x30 projections of that space. The model will then create and schedule one hundred flies. Create a file called Tutorial7.java, and add to it:
package sim.app.tutorial7; import sim.engine.*; import sim.field.grid.*; import sim.util.*; import ec.util.*; public class Tutorial7 extends SimState { static final long serialVersionUID = 1; public SparseGrid3D flies; public DoubleGrid2D xProjection; public DoubleGrid2D yProjection; public DoubleGrid2D zProjection; int width = 30; int height = 30; int length = 30; public void setWidth(int val) { if (val > 0) width = val; } public int getWidth() { return width; } public void setHeight(int val) { if (val > 0) height = val; } public int getHeight() { return height; } public void setLength(int val) { if (val > 0) length = val; } public int getLength() { return length; } public Tutorial7(long seed) { super(seed); }
The schedule will use two orders because in the first order we'll zero out the projections, then in the second order we'll move our flies and let them draw into the projections. Continuing:
public void start() { super.start(); flies = new SparseGrid3D(width,height,length); xProjection = new DoubleGrid2D(height,length); yProjection = new DoubleGrid2D(width,length); zProjection = new DoubleGrid2D(width,height); // schedule the zero-er at ordering 0 schedule.scheduleRepeating(new Steppable() { public void step(SimState state) { xProjection.setTo(0); yProjection.setTo(0); zProjection.setTo(0); } // because I am an anonymous nested subclass (see Tutorial 3)... static final long serialVersionUID = 1; }); // make some random flies at ordering 1 for(int i=0; i < 100;i++) { Fly fly = new Fly(); flies.setObjectLocation(fly, random.nextInt(width), random.nextInt(height), random.nextInt(length)); schedule.scheduleRepeating(Schedule.EPOCH,1,fly,1); } }
We finish with your usual boilerplate main(...), and another serialVersionUID:
public static void main(String[] args) { doLoop(Tutorial7.class, args); System.exit(0); } }
The 3D display code is more complex, but nearly all of it is due to the fact that we want (1) to move the projections into their proper locations, and (2) to show off features.
Create a file called Tutorial7WithUI.java. In this file, add the following:
package sim.app.tutorial7; import sim.portrayal3d.grid.*; import sim.portrayal3d.grid.quad.*; import sim.portrayal3d.simple.*; import sim.engine.*; import sim.display.*; import sim.display3d.*; import sim.util.gui.*; import javax.swing.*; import java.awt.*; public class Tutorial7WithUI extends GUIState { public Display3D display; public JFrame displayFrame; Tutorial7 tutorial7; SparseGridPortrayal3D fliesPortrayal = new SparseGridPortrayal3D(); ValueGrid2DPortrayal3D xProjectionPortrayal = new ValueGrid2DPortrayal3D("X Projection"); ValueGrid2DPortrayal3D yProjectionPortrayal = new ValueGrid2DPortrayal3D("Y Projection"); ValueGrid2DPortrayal3D zProjectionPortrayal = new ValueGrid2DPortrayal3D("Z Projection");
The ValueGrid2DPortrayal3D is a special 3D portrayal which only portrays DoubleGrid2Ds and IntGrid2Ds. It does so by drawing a 2 dimensional plane which is then perturbed in different ways: changing colors, bumping it up and down, etc. The SparseGridPortrayal3D portrayals SparseGrid2Ds and SparseGrid3Ds. Continuing, all of the following should be straightforward:
public static void main(String[] args) { new Tutorial7WithUI().createController(); } public Tutorial7WithUI() { super(new Tutorial7( System.currentTimeMillis())); } public Tutorial7WithUI(SimState state) { super(state); } public static String getName() { return "Tutorial 7: Projections"; } public static Object getInfo() { return "<H2>Tutorial 7</H2> Projections of randomly moving stuff! Woohoo!"; } public void start() { super.start(); setupPortrayals(); } public void load(SimState state) { super.load(state); setupPortrayals(); } public void setupPortrayals() { Tutorial7 tut = (Tutorial7) state; fliesPortrayal.setField(tut.flies); xProjectionPortrayal.setField(tut.xProjection); yProjectionPortrayal.setField(tut.yProjection); zProjectionPortrayal.setField(tut.zProjection); display.reset(); display.createSceneGraph(); } public void quit() { super.quit(); if (displayFrame!=null) displayFrame.dispose(); displayFrame = null; display = null; }
Now we get to the big shebang: the init() method. Here we're going to prepare the various portrayals to draw things as we like. We start with the easiest one (the SparseGridPortrayal3D), which will draw the flies as spheres half their normal diameter:
public void init(Controller c) { super.init(c); Tutorial7 tut = (Tutorial7) state; // the flies will be white spheres, half normal size fliesPortrayal.setPortrayalForAll(new SpherePortrayal3D(0.5f));
Next we define the xProjection. We start by defining it to draw values in the range from 0 to 4, drawn from green to yellow. The portrayed field will be semitransparent. We use a TilePortrayal as the underlying portrayal to draw the individual points. TilePortrayal draws the points by treating them as squares in the grid ("tiles").
TilePortrayal is a QuadPortrayal, a special kind of Portrayal used only by the ValueGrid2DPortrayal3D. You can't put SimplePortrayals as the subsidiary of ValueGrid2DPortrayal3D -- only QuadPortrayals. There's another QuadPortrayal which we'll get to in a bit.
// X projection: // go from green to yellow, semitransparent. SimpleColorMap map = new SimpleColorMap(0.0,4.0, Color.green, Color.yellow); xProjectionPortrayal.setPortrayalForAll(new TilePortrayal(map)); xProjectionPortrayal.setTransparency(0.8f); // a little transparent
Back it off a whole unit? Isn't the field scaled to 1 by 1 just like in the 2D code?
No. In the 2D code, field portrayals are given the 1x1 square they're supposed to draw relative to. This isn't the case for the 3D code. Instead field portrayals draw themselves using a natural size, and you're responsible for scaling them. Most portrayals draw themselves exactly as their height and width and length would expect. So in this case, the 2D grid portrayals are drawing themselves from (0,0,0) to (30,30,0). The SparseGridPortrayal3D will draw itself from (0,0,0) to (30,30,30). |
To do all this we use the internal transform of FieldPortrayal3Ds. To transform a SimplePortrayal3D you need to wrap it in a TransformedPortrayal3D. But FieldPortrayals need to be transformed so often to situate them in the right place that they have built-in transform functions you can use.
// rotate it in place and back it up a little xProjectionPortrayal.translate(0,0,-1); xProjectionPortrayal.rotateX(90); xProjectionPortrayal.rotateZ(90); // swing around Z axis
Now let's set up the Y projection. This time we'll use TilePortrayals, but the "z scale" of the Tiles will vary according to their values. This means that the tiles will literally "pop out" of the field, like some kind of space-age stairsteps. This one will draw from red to blue and be totally opaque. The degree of "popping out" will be 1.0 times the value.
// Y projection: // go from blue to yellow, opaque, stairstep-style, scale = 1.0 map = new SimpleColorMap(0.0,4.0,Color.blue,Color.yellow); yProjectionPortrayal.setPortrayalForAll(new TilePortrayal(map, 1.0f)); // rotate it in place and back it up a little yProjectionPortrayal.translate(0,0,1); yProjectionPortrayal.rotateX(90);
Last, we set up the Z projection. This one is easier because it's already in place and doesn't need to be moved (other than backed up a little). However, let's use another QuadPortrayal this time: the MeshPortrayal. While the TilePortrayal values elements as squares in the grid, the MeshPortrayal draws elements as the intersections of squares in the grid. Think chess versus go: you place pieces on the squares in chess, but on the intersections of squares in go.
MeshPortrayal also has a "z scale", and indeed is usually used with it. This has the effect of making things look like mountainscapes. We'll point the scale downwards, and run from red to blue:
// Z projection: // go from red to blue, opaque, landscape-style (mesh grid), scale = 1/2 (but pointing down) map = new SimpleColorMap(0.0,4.0,Color.red,Color.blue); zProjectionPortrayal.setPortrayalForAll(new MeshPortrayal(map,-0.5f)); // back it up a little (it's already in the right rotation) zProjectionPortrayal.translate(0,0,-1);
Now we build the display and scale it down to fit inside the 2x2x2 cube:
// make the display display = new Display3D(600,600,this); display.attach(fliesPortrayal,"Flies"); display.attach(xProjectionPortrayal,"X Projection"); display.attach(yProjectionPortrayal,"Y Projection"); display.attach(zProjectionPortrayal,"Z Projection"); // scale down the display to fit in the 2x2x2 cube float scale = Math.max(Math.max(tut.width,tut.height),tut.length); display.scale(1f/scale); displayFrame = display.createFrame(); c.registerFrame(displayFrame); displayFrame.setVisible(true); } }
Save and compile the files. Run the simulation as sim.app.tutorial7.Tutorial7WithUI . The portrayals will appear in the top-right corner of the screen. Press play, then rotate the scene about with the mouse to see the effects of the various portrayals.
If the values in MeshPortrayals cause "bends" in the angle of the underlying squares that are too severe (we've seen over 45 degrees), then when Java3D tries to pick the square you've double-clicked on, the "bent" squares will insist on being included in the pick collection, even if they're not even close to the double-click location. Often they'll claim to be "closer" than the true squares as well.
This happens in Tutorial7: double click on the Z projection (the "mountainscape") and you'll often get very incorrect coordinate values. It does not happen in the HeatBugs3D tutorial as those changes in angle are gradual from square to square (due to the diffuser). Try it!
How to fix this? This is a bug in Java3D's handling of four-sided polygons. The easiest way around this is to represent each square not as a four-sided polygon but as two triangles. There are two disadvantages to doing so. First, triangles can very slightly slower than rectangles in drawing. It's so small a difference that we probably shouldn't bother mentioning it. Second, if you display the mesh's lines rather than polygons (click on the Wrench icon and choose "Draw Polygons As Edges"), you'll see the diagonal line the triangles make passing through each rect.
Neither of these is much of a big deal. Still, the default is to use quads unless one is triggering the bug. To use triangles instead, change:
FROM... |
// Z projection: // go from red to blue, opaque, landscape-style (mesh grid), scale = 1/2 (but pointing down) map = new SimpleColorMap(0.0,4.0,Color.red,Color.blue); zProjectionPortrayal.setPortrayalForAll(new MeshPortrayal(map,-0.5f)); // back it up a little (it's already in the right rotation) zProjectionPortrayal.translate(0,0,-1); |
CHANGE TO |
// Z projection: // go from red to blue, opaque, landscape-style (mesh grid), scale = 1/2 (but pointing down) map = new SimpleColorMap(0.0,4.0,Color.red,Color.blue); zProjectionPortrayal.setPortrayalForAll(new MeshPortrayal(map,-0.5f)); // back it up a little (it's already in the right rotation) zProjectionPortrayal.translate(0,0,-1); // Make the Z projection use triangles rather than quads zProjectionPortrayal.setUsingTriangles(true); |
Recompile and run the code. Click on the Wrench, and choose "Draw Polygons as Edges". Notice the triangles being used now instead of squares.
ValueGrid2DPortrayal3Ds can have images attached -- this is particularly useful with MeshPortrayal to create "landscapes". Try it! Use the Earth map from Tutorial 6. Change:
FROM... |
// Z projection: // go from red to blue, opaque, landscape-style (mesh grid), scale = 1/2 (but pointing down) map = new SimpleColorMap(0.0,4.0,Color.red,Color.blue); zProjectionPortrayal.setPortrayalForAll(new MeshPortrayal(map,-0.5f)); // back it up a little (it's already in the right rotation) zProjectionPortrayal.translate(0,0,-1); // Make the Z projection use triangles rather than quads zProjectionPortrayal.setUsingTriangles(true); |
CHANGE TO |
// Z projection: // go from red to blue, opaque, landscape-style (mesh grid), scale = 1/2 (but pointing down) map = new SimpleColorMap(0.0,4.0,Color.red,Color.blue); zProjectionPortrayal.setPortrayalForAll(new MeshPortrayal(map,-0.5f)); // back it up a little (it's already in the right rotation) zProjectionPortrayal.translate(0,0,-1); // Make the Z projection use triangles rather than quads zProjectionPortrayal.setUsingTriangles(true); // Change the Z projection to display an image instead. :-) zProjectionPortrayal.setImage(sim.app.tutorial6.Tutorial6WithUI.loadImage("earthmap.jpg")); |
Recompile and run the code. Notice that even though the earthmap.jpg file is a rectangular image, it gets squished into the dimensions of the grid. And as usual, it takes slightly longer to fire up the code do to Java3D setting up the image wrapping.
Also notice the effects, in the Display3D options pane, of changing the Polygon Attributes. ValueGrid2DPortrayal3Ds are effected by this. Try drawing the polygons as Edges rather than filling, for example.
One last item: if you look closely, you'll notice that the Z projection is a little further away from the X and Y projections than they are from one another. This is because the Z projection is drawing a mesh of intersections at exactly the proper grid coordinates, rather than square centers -- so it's shifted by half a grid point in each direction.