Vx Introduction

From EECS467
Jump to: navigation, search


This is intended to give you a brief introduction to Vx and how to use it. It will cover the basics, including most functionality that you should expect to use.

Layers, Worlds, and Buffers (oh my!)

This section aims to give you an understanding of how Vx handles object rendering. In Vx, objects live in what is called a VxWorld. Typically, you will just have the one VxWorld, but sometimes you may want more. You can create a new VxWorld by making a call to vx_world_create().

In turn, each VxWorld holds one or more VxBuffers, which is where the VxObjects you want to render live. Buffers allow you to control when particular objects are rendered. Objects in the same buffer are rendered at the same time. You can create a new buffer by making a call to vx_world_get_buffer(vxworld, "buffer-name"). If a buffer of that name has already been created in the world in question, this will instead give you a pointer to the existing buffer. VxBuffers implement what is called a double buffer. For those new to computer graphics, the main advantage of double buffering is that it allows us to avoid flickering and tearing in our screen updates since we do not have to update the content in the rendering buffer while simultaneously trying to render it. Instead, a double buffer has two buffers (surprise), a front and back buffer. The content in the front buffer is rendered, while new content to render is written to the back buffer. When the back buffer is ready to be rendered, we swap the two buffers. Now, the old back buffer is our front buffer, meaning that whatever it contained is rendered. Meanwhile, the old front buffer is now the back buffer, and we can start populating it with new content.

This double buffer implementation is reflected in the way we add objects to VxBuffers. The below code adds an object to a back buffer, meaning we would like it to be rendered next swap. After updating our back buffer, we request a buffer swap, which will cause our content to render. Don't worry if you don't understand everything happening in this example. We'll cover VxObjects later.

vx_world_t *world = vx_world_create()
vx_buffer_t *buf = vx_world_get_buffer(world, "example-buffer");
vx_object_t *obj = vxo_sphere(vxo_mesh_style(vx_blue)));

vx_buffer_add_back(buf, obj);

The reason that Vx supports named buffers is that it allows different parts of your code to independently manage different aspects of your display. For example, one part of your code might be responsible for drawing your robot, whereas another might be responsible for drawing targets that you've detected. By using differently named buffers for each of these, you don't need to have a single "draw" function: your two modules can independently update their visualizations. (Also note that the vx_buffer calls are thread-safe and will not block for long periods of time.)

So, now we have VxWorlds full of VxBuffers (which in turn, are full of VxObjects). That still doesn't explain how objects are actually rendered to screen. Enter VxLayers and VxDisplays. A VxLayer holds a pointer to a VxWorld and acts as a viewport into the world. You might have many VxLayers tied to the same VxWorld (you'll need at least one), each providing a separate viewport. Each layer provides its own camera control callbacks that determine how to interact with the layer in question. Layers are then tied to VxDisplays, which handle the actual OpenGL rendering. A VxDisplay is what you, the user, interacts with on your computer. It renders the view through the viewport defined by the associated layer into the layer's associated world (phew, got that all?).

Recap: VxBuffers hold VxObjects (which are the things we want to render). VxWorlds hold VxBuffers. VxLayers attach to particular VxWorlds and act as viewports into them. VxDisplays render what the VxLayers "see".

An Overview of the Objects of Vx

Now that you have an idea about how Vx handles rendering and data management, let's look at some small examples of how to render specific VxObjects! NOTE: These examples assume a lot of setup has already happened, so don't expect them to just work straight out of the box. Consider them a step above pseudocode but a step below compilation. The gist of most VxObjects is that they render a particular primitive shape (e.g. a cube) in a specified style (e.g. as a red wireframe 2 px wide). Objects are rendered at the origin by default, typically at a fixed size. If you want to change their size or positioning, rigid body transformations (RBTs) can be applied to rotate, translate, and scale the object.

There are 3 styles of objects that you can apply to objects: mesh, lines, and points. A points style will try to render the object as points. Typically, this will render all of the vertices of the object. The lines style will typically try to connect these vertices in a meaningful way. For example, it will render a box or tetrahedron as a wireframe version of the shape, while for circle or rectangle, it will render an unfilled version of that shape. Mesh styles color the object, creating filled versions of the 2D primitives and rendering opaque solid versions of the 3D primitives. Color is provided to the style, typically in the form of a float[] = {Red, Green, Blue, Alpha} with values [0.0f, 1.0f]. Additionally, line and point styles take a second float argument that specifies the width of then line/point in pixels.

Let's look at some examples shapes and styles available. Note that the above examples are by no means complete documentation, but are intended to just show various examples. Some shapes will support all styles, and some will only support a few. Do not take the number of styles used in the below examples to be indicative of the styles supported, but rather a reminder that just because styles are supported, does not mean they must always be used.


vxo_rect - a 1x1 rectangle in the XY plane centered at the origin

vx_buffer_add_back(buf, vxo_rect(vxo_mesh_style(vx_red),
                                 vxo_lines_style(vx_yellow, 2.0f),
                                 vxo_points_style(vx_cyan, 5.0f)));

vxo_circle a r=1 circle in the XY plane centered at the origin

vx_buffer_add_back(buf, vxo_circle(vxo_mesh_style(vx_orange),
                                   vxo_lines_style(vx_green, 3.0f)));

vxo_box_t - a 1x1x1 cube centered at the origin

float color[4] = {1.0f, 0.5f, 0.0f, 1.0f};  // RGBA
vx_buffer_add_back(buf, vxo_box(vxo_mesh_style(color),
                                vxo_lines_style(vx_yellow, 2.0f)));


vxo_cylinder - a r=1, h=1 cylinder centered at the origin and along the Z-axis

vx_buffer_add_back(buf, vxo_cylinder(vxo_mesh_style(vx_blue),
                                       vxo_lines_style(vx_cyan, 1.0f)));

vxo_sphere - a r=1 sphere centered at the origin

vx_buffer_add_back(buf, vxo_sphere(vxo_mesh_style(vx_green)));

vxo_square_pyramid - a 1 unit all square pyramid with a 1x1 square base centered at the origin on the XY plane

vx_buffer_add_back(buf, vxo_square_pyramid(vxo_lines_style(vx_purple, 2.0f),
                                           vxo_points_style(vx_cyan, 10.f)));

vxo_tetrahedron - a tetrahedron with 1 unit edges centered at the origin on the XY plane

vx_buffered_add_back(buf, vxo_tetrahedron(vxo_mesh_style(vx_white),
                                          vxo_lines_style(vx_red, 2.0f)));

Text doesn't use styles, instead taking a form of markup language in the text string itself. For example, "<<right,#ff0000,serif>>Example Text" says to render the text "Example Text" as being right aligned, red (#ff0000 specifies a hex color value in a similar manner to that seen in HTML), and in a serifed font. By default, all text is rendered at 128 points. We'll discuss how to deal with that later. We'll also get into more detail about VXO_TEXT_ANCHOR_CENTER.

vxo_text - write text to the screen vxo_text - write text to the screen

vx_buffered_add_back(buf, vxo_text_create(VXO_TEXT_ANCHOR_CENTER,
                                          "<<right,#ff0000,serif>>Example Text"));
vx_buffered_add_back(buf, vxo_text_create(VXO_TEXT_ANCHOR_CENTER,
                                          "<<right,#aa00ff,monospaced>>Example Text"));

Below, we render 100 randomly generated 3D points as red dots in space.

vxo_points - render a set of points to the screen

float points[300] = {x0,y0,z0,x1,y1,z1, ... ,x99,y99,z99};
int npoints = 100;
vx_resc_t *verts = vx_resc_copyf(points, npoints*3);
vx_buffer_add_back(buf, vxo_points(verts, npoints, vxo_points_style(vx_red, 2.0f)));

In this example, we take the same 100 randomly generated points and generate 50 line segments connecting every other point to the point immediately following it in the points array.

vxo_lines - render lines based on a supplied set of points and render style (GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP)

float points[300] = {x0,y0,z0,x1,y1,z1, ... ,x99,y99,z99};
int npoints = 100;
vx_resc_t *verts = vx_resc_copyf(points, npoints*3);
vx_buffer_add_back(buf, vxo_lines(verts, npoints, GL_LINES, vxo_points_style(vx_red, 2.0f)));

This example will show you how to render a vxo_image, assuming you already have an image_u32_t. In this case, we are loading our image_u32_t from a PNM file. Images in Vx are rendered as textures, hence so you'll note that you make calls to vxo_image_tex and vxo_image_texflags to create images (tex == texture). The first function will merely call the second with some default flags, so in our example, we'll show you some possible flags you could supply yourself.

Those of you paying attention may notice that when specifying our image dimensions, we reference the image stride and height instead of the width and height. That is due to the fact that, when reading in our PNM, the image_u32 may automatically have extended its row buffers, to ensure proper memory alignment. Thus, image->width refers to the actual width of the image while image->stride refers to the adjusted width. Also note that it is a good policy to use images that have dimensions which are divisible by 4. This is to support mipmapping, a process often used to allow textures to be rendered more efficiently and at better quality.

We show two examples of how to load an image into a texture below. Both result in the same image also seen below. The first example shows you how to load an image_u32_t from file, create a VxResource (vx_resc_t), which is a type used to pass raw data to GL, and then use that VxResource to create your image texture. The most up-to-date versions of the class repository also have a convenience function that will do the resource construction for you. This is the second example given.

vxo_image - render an image_u32_t, etc. as a rectangular texture centered at the origin in the XY plane

image_u32_t *im = image_u32_create_from_pnm("image.ppm");
vx_resc_t *vr = vx_resc_copyui(im->buf,                          // Source of unsigned integer data to copy
                               im->stride*im->height);           // Number of integers to copy
vxo_object_t *vo = vxo_image_texflags(vr,                        // Image buffer
                                      im->stride,                // Image width
                                      im->height,                // Image height
                                      GL_RGBA,                   // Color format (allows GL to interpret the image buffer's byte data properly)
                                      VXO_IMAGE_FLIPY,           // Flips the Y axis when rendering the image
                                      VX_TEX_MIN_FILTER | VX_TEX_MAG_FILTER); // Texture attribute flags.
vx_buffer_add_back(buf, vo);


image_u32_t *im = image_u32_create_from_pnm("image.ppm");
vxo_object_t *vo = vxo_image_from_u32(im,                        // The source image
                                      VXO_IMAGE_FLIPY,           // Image flags
                                      VX_TEX_MIN_FILTER | VX_TEX_MAG_FILTER); // Texture attribute flags.

vxo_grid - add a 1m grid to your vx world on the XY plane

vx_buffer_add_back(buf, vxo_grid());

Moving and Manipulating Objects

In this section, we'll take a slightly more detailed look at how to use some of the objects shown above. We'll also get into a bit more detail about some of the more complicated objects.

Object Transformations and vxo_chains

All of our objects start at a fixed, unit size at the origin. That's probably not typically what you want, so you're going to have to apply some transformations to fix that. These transformations live in vxo_mat.h/c and are applied by using the vxo_chain. Let's take a look at how they work.

Imagine that we want to render a rectangular prism of size 3x10x5 centered at coordinate (30, 10, 0) and rotated CCW 45 degrees. How would we go about making that happen? Look at the images below.

The corresponding code to the final figure is below:

vx_buffer_add_back(buf, vxo_grid());
vx_object_t *obj = vxo_chain(
                             vxo_mat_translate3(30, 10, 0),             // Translation matrix
                             vxo_mat_rotate_z(M_PI/4.0),                // Rotation matrix
                             vxo_mat_scale3(3, 10, 5),                  // Scale matrix
                                     vxo_lines_style(vx_yellow, 2.0f))
vx_buffer_add_back(buf, obj);

This example introduces a new object: the vxo_chain. Chains are used to apply one or more transformations to one or more objects. The name chain comes from the fact that the transformations are chained together, maintaining state as we go. Our rigid body transformations are actually transforming our coordinate frame. So the above translation action actually translates our origin 30 units down the its X-axis and 10 units down its Y-axis. Then, the rotation rotates the coordinate frame 45 degrees CCW around the coordinate frame's Z-axis. Thus, if the X-axis was pointing horizontally from left to right, now it would be pointing diagonally from the bottom left to the top right. The scale transformation effectively rescales the axes, stretching or shrinking them appropriately. The vxo_chain keeps track of all of these transformations of our coordinate frame. Then, when it encounters a vx_object, it renders it wherever the current origin is. For example, in the below example, notice how the translations are stacked such that each box is evenly spaced 3 units beyond the previous one:

vxo_chain - Chains together rigid body translations and applies them to objects in the chain

vx_buffer_add_back(buf, vxo_grid());
vx_buffer_add_back(buf, vxo_chain(vxo_box(vxo_mesh_style(vx_white),
                                  vxo_mat_translate3(3, 0, 0), 
                                  vxo_mat_translate3(3, 0, 0),
                                  vxo_mat_translate3(3, 0, 0),

NOTE: It is important to remember that the order you apply your transformations is very important. Try changing the order of the operations in the first example to see why.

A More Detailed Look at Text

As you might have realized above, text styling is handled in a totally different way from everything else. Instead of applying one of the various styles to the text, we apply formatting to the string directly using a very simple markup format. Thus, input strings to vxo_text may take the form "<<markup-args>>Text to render". Markup options include:

  • Justification options
    • right
    • left
    • center
  • Color options (hex format #rrggbb or optionally #rrggbbaa)
    • #ff0000 == red
    • #ffaa00 == orange
    • #0000ff == blue
    • etc...
  • Font face options
    • serif
    • sansserif
    • monospaced
    • serif-bold (ditto for the other fonts)
    • serif-italic (ditto for the other fonts)

To give some more concrete examples:

 <<#ff00ff,sansserif-italic>> An italicized, magenta, sans serif font 
 <<center,monospaced>>A black (the default color) monospaced font 
 <<right,#0000ff,serif-bold>> Right justified, bolded, blue, serif font 

You might also have noticed that vxo_text took an argument of the form VXO_TEXT_ANCHOR_CENTER. When you make a text object, you essentially get back a text box. This argument determines how the box is :anchored" with respect to the origin. CENTER will place the center of your text box on the origin. TOP will still be horizontally centered, but will shift your text box down such that the top of the box and the X axis are aligned. BOTTOM_RIGHT will anchor the bottom right corner of your text box to the origin. The list goes on. Common options are:

Pix Coords

Sometimes when you're making a GUI, you want things to stay put relative to your viewport, even if you're changing your view. These are sometimes called overlays. An example overlay you might want is a battery status bar, or perhaps an fps counter. So far, all the vx_objects we've seen render to fixed points in the world, though, meaning that when we move our camera, our view of the objects change. We solve this problem with vxo_pix_coords. These allow us to fix objects to locations in our 2D viewport. For example, if we wanted to put an FPS counter in the bottom left corner of our screen, we could use the following code:

vx_object_t *text = vxo_text_create(VXO_TEXT_ANCHOR_BOTTOM_LEFT, "<<#ffffff>>fps: 30"); // You would probably want the number to actually change ;)
vx_buffer_add_back(buf, vxo_pix_coords(VX_ORIGIN_BOTTOM_LEFT, text));

To Come: Making your own Vx objects

With Vx, you are not just limited to the object primitives we provide you. If you need to make something else, feel free to write a new type of primitive! Being comfortable with computer graphics to some degree will help tremendously. We're not going to get into tremendous detail here, but if you want to write a custom primtive, vxo_box.h/vxo_box.c is one of the most straightforward and understandable examples of a 3D object.

To write any new primitive, you will have to determine what styles you want to support and how you want those styles to render. For vxo_points_style, you will need to supply the style with a set of 3D vertex coordinates. For vxo_lines_style, you will similarly need to provide a series of vertices, but also specify a rule for how they are connected. The vxo_mesh_style is typically the most challenge. To create a mesh, you will need to define polygons (which are typically triangles) as well as corresponding normal vectors for the vertices. These normals will be used to compute lighting properties for the object.