Vis1Tutorial

From April
Jump to: navigation, search

WARNING: This guide is for the original version of Vis: The guide for the new version is Vis1

Original Vis Version (No longer supported)

This page will introduce you to Vis and some of its core functionality.

What is Vis?

Vis is a tool that allows you to rapidly create 2D and 3D visualizations. It provides a clean and simple interface built upon OpenGL. Vis is not a 3D game engine and it should not be mistaken for such, as it lacks the heavy optimization typically seen in most engines.

Learning to use Vis

This tutorial will introduce you to some basic concepts of Vis. In it, you will create a 3D scene of a snowman with a snowball rolling (or in this case, gliding) around him. If you're impatient, this is the final product and you can twiddle with it at your leisure: File:MyVisDemo.java. Otherwise, start by creating a file MyVisDemo.java in some convenient location and fill it in with the following:

import april.vis.*;
import april.jmat.*;
import april.util.*;

import java.util.*;
import java.awt.*;
import javax.swing.*;

class MyVisDemo
{
    // Content goes here...
}

Step 1: Creating your VisWorld

At the top of your new class, create the following objects and constructor:

class MyVisDemo
{
    VisWorld vw;                        // The world holds all of the VisObjects we create
    VisCanvas vc;                       // This will be a rendering of our VisWorld
    VisWorld.Buffer snowmanBuffer;      // Drop your VisObjects into a buffer for rendering
    VisWorld.Buffer animatedBuffer;
    VisWorld.Buffer textBuffer;

    public MyVisDemo()
    {
        vw = new VisWorld();
        vc = new VisCanvas(vw);

        snowmanBuffer = vw.getBuffer("Snowman_Buffer");
        animatedBuffer = vw.getBuffer("Animation_Buffer");
        textBuffer = vw.getBuffer("Text_Buffer");
    }
}

At the core of ever Vis-based visualization is a VisWorld. This is where all of your VisObjects will live. You can view your world through a VisCanvas. The VisCanvas provides you with an interface for changing your view, taking screenshots, recording movies, and all sorts of things. It's fairly intuitive, so my best advice to is play around with it as you work through this tutorial. The three buffers you created are rendering buffers linked to your world. You place objects you want rendered into these buffers. We'll talk more about them in a little bit. For now, all you need to know is that you need at least 1 buffer.

All objects rendered in your VisWorld are VisObjects. A VisObject is simply an object that knows how to render itself. They usually represent physical objects in the world: a tree, a robot, or the ground could be VisObjects. VisObjects are also used for things like text that should appear somewhere. This tutorial will introduce your to several VisObjects.

Step 2: Seeing your new world

Our world doesn't have anything in is yet, but it would be nice to have a GUI so we can see what we're doing as we add to our world, so let's get that set up. We don't need much, so this will just be a frame with our VisCanvas in it:

class MyVisDemo
{
    // Vis stuff...

    JFrame jf;
    public MyVisDemo()
    {
        // Vis stuff...

        jf = new JFrame("Vis Demo");
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  // We want our program to actually close when we quit
        jf.setLayout(new BorderLayout());
        jf.add(vc, BorderLayout.CENTER);

        jf.setSize(800, 600);
        jf.setVisible(true);
    }

    static public void main(String args[])
    {
        MyVisDemo demo = new MyVisDemo();
    }
}

Save your work and, in another terminal, go to wherever you saved your file and compile and run it:

javac MyVisDemo.java
java MyVisDemo

It should look like this:

Step 3: Making a rendering thread

We want to have a running loop to update our visualization, someday, so let's create a thread responsible for updating our VisWorld. It will update at around 10 fps:

    public MyVisDemo()
    {
        // Vis stuff...
        // GUI stuff...

        RenderThread renderer = new RenderThread();
        renderer.start();
    }

    // This thread will be responsible for rendering our VisWorld
    class RenderThread extends Thread
    {
        // RefreshRate
        int fps = 10;

        public void run()
        {
            // Our update loop...refreshes our animation buffer to show a fresh copy of the world at ~10 Hz
            while (true) {
                TimeUtil.sleep(1000/fps);
            }
        }
    }

Step 4: Give our snowman a body

Let's give our snowman a body, now. He'll be a fairly typical snowman, with 3 spheres making up his abdomen, thorax, and head. The VisSphere should be perfect for our needs!

    class RenderThread extends Thread
    {
        // FPS ...

        // Snowball sizes
        double headRadius = 0.2;
        double thoraxRadius = 0.35;
        double abdomenRadius = 0.5;

        // The body components of our snowman
        VisSphere snowHead = new VisSphere(headRadius, Color.white);
        VisSphere snowThorax = new VisSphere(thoraxRadius, Color.white);
        VisSphere snowAbdomen = new VisSphere(abdomenRadius, Color.white); 

        public void run()
        {
            drawSnowBody();
            snowmanBuffer.switchBuffer();
            
            while (true) {    
                TimeUtil.sleep(1000/fps);
            }
        }
       
        // Draw our snow man's body
        void drawSnowBody()
        {
            snowmanBuffer.addBuffered(new VisChain(LinAlg.translate(new double[] {0, 0, abdomenRadius}), snowAbdomen));
            snowmanBuffer.addBuffered(new VisChain(LinAlg.translate(new double[] {0, 0, 2*abdomenRadius + thoraxRadius}), snowThorax));
            snowmanBuffer.addBuffered(new VisChain(LinAlg.translate(new double[] {0, 0, 2*abdomenRadius + 2*thoraxRadius + headRadius}), snowHead));
        }
    }

Wow, that didn't take very much code, did it? There's some non-obvious stuff happening, though, so let's look at each of our changes. We established some snowball sizes. This is just so it's easy to change the size of our snowballs in the future and so we can place them relative to each other. We also created some VisSpheres, one for each part of our snowman. All we had to do was feed in a radius and color.

Side note: Other VisObjects will obviosuly have type-specific construction requirements. If you don't know how to construct a particular object, your best bet is to look directly at the source for an answer. We are currently in the process of bringing our documentation up to date so there's no handy javadoc most of the time. We'll keep you up to date!

We also created a drawSnowBody function responsible for updating the snowman's position. We have to add each portion of the snow man's body to the buffer for rendering. Things are not that simple, though. Every object, by default, is rendered at the origin. We will still have to move our object to the desired location in the scene. If you are already familiar with 3D graphics, you will know that this is generally accomplished with matrix transformations. You can use various matrices to translate, rotate, and scale your objects. Vis and the April LinAlg utility (found in april.util) try to make this simpler by providing the VisChain and matrix construction utilities, respectively. You can feed a series of transformations into a VisChain along with the object you with to transform to shift the object to the desired location and orientation. In the case of our snowman, we are shifting the snowballs up along the Z-axis using a translation matrix.

The final thing to note is the call to switchBuffer() on snowmanBuffer. When you have placed everything into a buffer than you want rendered, call switch buffer to render the new contents. Everything that was previously rendered from this buffer with no longer be rendered unless it was placed back into the buffer. Thus, if you call switchBuffer() immediately after the first call, nothing will be rendered because your buffer was empty. Notice that we drew our snow body and switched the buffer ourside our rendering loop because we will never move our snowman. Thus, it is pointless to waste cycles re-rendering him.

Try recompiling and running your program. Here's your snowman! Try panning the camera around to get a better view of him.

Step 5: Jazzing up our world

We've got a snowman, but the snowman and the world he lives in aren't very interesting. Let's add some ground now and give our snowman some features:

    class RenderThread extends Thread
    {
        // FPS/snowballs...

        // Nose size
        double noseLength = 0.2;
        double noseRadius = noseLength * 0.1;
        
        // Hat size
        double hatRadius = headRadius*0.7;
        double brimRadius = headRadius;
        double hatHeight = hatRadius*2.0;
        double brimHeight = 0.01;      

        // VisSpheres...

        // Our snow man's nose
        VisCylinder carrotNose = new VisCylinder(noseRadius, 0, 0, noseLength, Color.orange);
 
        // Pieces of our stovepipe hat
        VisCylinder hat = new VisCylinder(hatRadius, hatHeight, Color.black);
        VisCylinder brim = new VisCylinder(brimRadius, brimHeight, Color.black);        

        // Our ground plane 
        VisRectangle groundPlane = new VisRectangle(40, 40, new VisDataFillStyle(Color.gray)); 

        public void run()
        {
            drawTerrain();
            drawSnowBody();
            drawSnowFeatures();
 
            snowmanBuffer.switchBuffer();

            // Update loop....
        }

        // Draw the ground and any other terrain features (trees, boulders...) 
        void drawTerrain()
        {
            snowmanBuffer.addBuffered(groundPlane);
        }

        // Draw snow body...

        // Draw any miscellaneous features we want on our snow man. Carrot nose, hat, charcoal eyes, buttons, arms, etc.
        void drawSnowFeatures()
        {
            double snowmanHeight = 2*(abdomenRadius + thoraxRadius + headRadius);
            double[] nose_xyzrpy = new double[] { headRadius,                                       // x translation
                                                  0,                                                // y translation
                                                  2*abdomenRadius + 2*thoraxRadius + headRadius,    // z translation
                                                  0,                                                // roll (rotation around x)
                                                  Math.PI/2,                                        // pitch (rotation around y)
                                                  0};                                               // yaw (rotation around z)
            snowmanBuffer.addBuffered(new VisChain(nose_xyzrpy, carrotNose)); 

            snowmanBuffer.addBuffered(new VisChain(LinAlg.translate(new double[] {0, 0, snowmanHeight + (brimHeight/2)}), brim));
            snowmanBuffer.addBuffered(new VisChain(LinAlg.translate(new double[] {0, 0, snowmanHeight + brimHeight + (hatHeight/2)}), hat));
        }
    }

Most of this code should look pretty familiar. We're just using a couple of cylinders to max a hat, another to make a nose, and a big rectangle is giving us some ground. You might have noticed that the VisCylinder for that hat were constructed differently than the nose. Different constructors can yield different results, so make sure you understand what they're doing! You also might notice that we just fed in an array of doubles to transform the nose. VisChain also allows you to feed in an xyzrpy array to transform your object. This may be easier to deal with than multiple transformations, in some cases. For example, the nose cone we created started out vertical, so we rotated it 90 degrees (pitch) and then shifted it up to the middle of our snow man's face (x and z translation).

Another new concept has been introduced in the VisRectangle constructor. There are several VisDataStyles that can be used to define how an object is drawn. For example, our VisRectangle is drawn using a VisDataFillStyle in gray, meaning we will draw a rectangle filled with gray. If we had only wanted a gray outline of a rectangle, we would have used a VisDataLineStyle, instead. There is also a VisDataPointStyle for rendering point clouds and similar objects.

Alternatively, we could have used the built in ground plane. By control-right clicking on our VisCanvas and hovering over the ground/grid section, you will see that you can enable the built in ground plane, XY-grid, and origin in any VisCanvas. However, for the sake of learning, we will stick with our VisRectangle for now!

Step 6: Trying some animation

So far we've made a rather dashing looking snow man, but we have yet to actually animate anything. Many visualizations will involve changing worlds, so you will want to be able to update things accordingly. We will making a snowball roll (actually, glide, for simplicity) and accumulate mass around our snow man, occasionally pausing to melt it back down to its original size:

    class RenderThread extends Thread
    {
        // FPS/Snow man stuff...

        // Rolling snowball state
        double rollingSnowballStartRadius = 0.2;
        double rollingSnowballEndRadius = 0.5;
        double rollingSnowballCurrRadius = rollingSnowballStartRadius;
        double growthFactor = 0.02;
        double rollingSnowballTheta = 0;
        double rollSpeed = Math.PI/(4*fps);
        boolean melting = false;

        // VisObjects...

        // Rolling snowball
        VisSphere rollingSnowball = new VisSphere(rollingSnowballStartRadius, Color.white);
     
        public void run()
        {
            // Snow man stuff...
            while (true)
            {
                drawSnowball();

                animatedBuffer.switchBuffer();
                
                TimeUtil.sleep(1000/fps);
            }
        }

        // Snow man drawing functions...

        // Update our rolling snowball
        void drawSnowball()
        {
            // Periodically melt the snowball down to its original size
            if (melting) {
                rollingSnowballCurrRadius *= (1.0 - growthFactor);
                melting = rollingSnowballCurrRadius > rollingSnowballStartRadius;
            } else {
                rollingSnowballCurrRadius *= (1.0 + growthFactor);
                rollingSnowballTheta = MathUtil.mod2pi(rollingSnowballTheta + rollSpeed);
                melting = rollingSnowballCurrRadius > rollingSnowballEndRadius;
            }
            
            double scaleFactor = rollingSnowballCurrRadius/rollingSnowballStartRadius;
       
            animatedBuffer.addBuffered(new VisChain(LinAlg.rotateZ(rollingSnowballTheta),
                                                    LinAlg.translate(new double[] {5.0, 0, rollingSnowballCurrRadius}),
                                                    LinAlg.scale(scaleFactor, scaleFactor, scaleFactor), 
                                                    rollingSnowball));
        }
    }

Our snowball gradually accumulates size as it rolls around our snow man, eventually stopping to melt when it hits peak size. We applied 2 different transformations instead of using an xyzrpy to move the snowball and then scaled it to its proper size. Try compiling and you should see a ball of snow circling around your snow man 5 meters out.

Step 7: Text and draw order

Something I have avoided mentioning thus far is draw order for your buffers. Every buffer has an associated drawing order that effects the order in which buffers are rendered. This is significantly more important in 2D renderings where you may want to overlay the contents of several buffers on top of another. With a rather contrived example, we will use draw order to show and hide a text box. It is better practice to not render the text box at all when we are hiding it, but for the purposes of learning, we will do it the wrong way.

First we need to add a check box to our GUI. We'll use the ParameterGUI from april.util. I won't go into detail about ParameterGUIs here, but the ParameterGUI is a great way to add a bunch of knobs for tweaking your program's settings.

class MyVisDemo
{
    // Vis stuff...

    JFrame jf;
    ParameterGUI pg;
 
    public MyVisDemo()
    {
        // Vis stuff...
        // GUI stuff...
        // JFrame stuff

        pg = new ParameterGUI();
        pg.addCheckBoxes("hideTitle", "Hide Scene Title", false); 
        jf.add(pg, BorderLayout.SOUTH);

        // Rendering stuff
    }

Next, add a VisText to your rendering thread and make a draw text function:

    class RenderThread extends Thread
    {
         // Snow man and scene stuff

         // A text overlay for our image
         VisText sceneTitle = new VisText(new double[] {0, 0}, VisText.ANCHOR.CENTER, "My First Vis Snowman");

         public void run()
         {
             // Snow man stuff

             snowmanBuffer.setDrawOrder(-90);
             snowmanBuffer.switchBuffer();

             // Our update loop...refreshes our animation buffer to show a fresh copy of the world at ~10 Hz
             while (true) {
                  
               drawSnowball();
               drawText();

               animatedBuffer.switchBuffer();
               textBuffer.switchBuffer();

               TimeUtil.sleep(1000/fps);
         }

         // Draw snow man stuff...

         void drawSnowball()
         {
             animatedBuffer.setDrawOrder(-90);
             // Update the rolling snowball...
         }

         // Place our text on screen
         void drawText()
         {
             if (pg.gb("hideTitle"))
                 textBuffer.setDrawOrder(-100);
             else
                 textBuffer.setDrawOrder(0);
             textBuffer.addBuffered(sceneTitle);
         }
     }

Our VisText object will display a text box saying "My First Vis Snowman". The VisText.ANCHOR.CENTER argument alights the text box center with the origin, our specified location. When we check the "Hide scene title" check box, our text will vanish, and it will return when we uncheck the box.

Briefly, what's happening here is an abuse of draw order. The snow man and the rest of the world are drawn at order -90 all of the time. You can think of the world as having many layers, higher numbered layers stacked upon lower numbered layers. We draw layers from the bottom up, laying the most recent layers on top of the previous ones, potentially hiding things that have already been drawn. Normally, our check box is unchecked, so we set the textBuffer's draw order to 0, which places it on top of the rest of our scene. When we check the box to hide our text, we set the draw order to -100 instead, which is behind our scene. Our ground plane is big enough that it generally will cover up the text box, hiding it from view. However, if you zoom out far enough, the plane will no longer cover the entire text box, and it will start to peek our from behind our rectangle.

Important to note: Not all objects are subject to depth buffer testing. For example, objects rendered at different coordinates in the 3D world space will overlap just as one would expect them to. An object that is physically behind another according to our camera view would not be rendered on top of the front-most object even if the back object had a higher draw order. For example, if you were to change the animationBuffer's drawOrder to 100, the rolling snowball would still appear to roll behind the snow man periodically. Draw order only comes into play for image overlays and things such as our text box.

The proper way to hide an object is to either 1) Turn off the buffer by right clicking on your VisCanvas, going to SelectBuffers, and turning that buffer off or by 2) switching in an empty buffer. The code for case 2 looks like:

void drawText()
{
    if (!pg.db("hideTitle"))
        textBuffer.addBuffered(sceneTitle)
}

If we are supposed to hide the title, we will switch in an empty buffer and thus draw no text.

Step 8: Adjusting your camera position

It's probably really annoying having to adjust your camera every time your run your demo. Wouldn't it be great if it was already looking where you wanted it to? This is actually pretty easy to do. We'll just add one line to your constructor:

    public MyVisDemo()
    {
        // Vis stuff ...
         
        vc.getViewManager().viewGoal.lookAt(new double[] {5.0, 5.0, 5.0},               // Camera position in xyz
                                            new double[] {0.0, 0.0, 0.0},               // Point the camera is looking at in xyz
                                            new double[] {-1.0, -1.0, 1.0});            // Up vector to the camera in xyz

        // GUI stuff and everything else...
    }

By retrieving the view manager for your vis canvas, you can set the viewGoal to look at a selected point from wherever you choose. As the comments indicate, you set the camera position in world coordinates (eye), the point you want to look at, and a vector designating up to the camera. This won't lock your camera to a particular position, but it will start it out somewhere more reasonable than the default.

Hopefully this has been enough to get you up and running. If you're not comfortable starting out on your own project yet, try modifying this one. Some interesting things to try:

  • Add falling snowflakes
  • Add trees
  • Add charcoal eyes and buttons to your snowman
  • Give your snowman arms and legs

To make sure you're comfortable with what you've learned, we suggest trying to render one or more of these scenes yourself:

  • A car/truck
  • A bookshelf full of books
  • A desk with a computer on it
  • A robot
  • A house

Miscellaneous Tidbits

While the above tutorial covers a lot of Vis, it misses bits and pieces. We'll talk briefly about these things here.

VisData

Point clouds are fairly common in visualizations of all sorts, from plotting 2D test data to representing 3D LIDAR sweeps. Though our tutorial didn't cover rendering individual points, this can be accomplished fairly easily using the VisData class. VisData one of the true workhorses of Vis, and point clouds are one of its specialties. By feeding in an ArrayList of points represented by 2 or 3 element arrays (eg. {0, 0} or {1, 1, 1}) along with a VisDataPointStyle, a VisData object can be passed into a buffer like any other VisObject, and the result will be a series of points placed at their respective 3D coordinates. Below is a very short example of the use of VisData along with a picture of the results:

import april.vis.*;
import april.util.*;
import java.util.*;
import javax.swing.*;
import java.awt.*;

class MyVisTest implements Runnable
{
    VisWorld vw;
    VisCanvas vc;
    VisWorld.Buffer vb;

    JFrame jf;

    public MyVisTest()
    {
        vw = new VisWorld();
        vc = new VisCanvas(vw);
        vb = vw.getBuffer("vb");

        jf = new JFrame("Test VisData");
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jf.setLayout(new BorderLayout());
        jf.add(vc, BorderLayout.CENTER);

        jf.setSize(800, 600);
        jf.setVisible(true);

        new Thread(this).start();
    }

    public void run()
    {
        while (true)
        {
            ArrayList points = new ArrayList();
            ArrayList shape = new ArrayList();
            ArrayList filledShape = new ArrayList();
            for (int i = 0; i < 10; i++)
            {
                points.add(new double[] {0, 0, i});
            }
            shape.add(new double[] {0,0});
            shape.add(new double[] {0, 4});
            shape.add(new double[] {4, 4});
            shape.add(new double[] {4, 0});

            filledShape.add(new double[] {0,0});
            filledShape.add(new double[] {0, -4});
            filledShape.add(new double[] {-4, -4});
            filledShape.add(new double[] {-4, 0});

            vb.addBuffered(new VisData(points, new VisDataPointStyle(Color.black, 2)));
            vb.addBuffered(new VisData(shape, new VisDataLineStyle(Color.red, 2)));
            vb.addBuffered(new VisData(filledShape, new VisDataFillStyle(Color.blue)));

            vb.switchBuffer();

            TimeUtil.sleep(100);
        }
    }

    static public void main(String args[])
    {
        MyVisTest t = new MyVisTest();
    }
}