Category "Object graphics"


I spent some time improving the POV-Ray destination for object graphics I talked about in a previous article. I can now produce visualizations of actual data (like the one on the right) using this destination and its related library of classes.

Halfcell

My goals for this destination are two-fold:

  1. Create better renderings of pure IDL object graphics scenes (with no POV-Ray knowledge necessary).
  2. Ability to use POV-Ray specific features using custom object graphics classes. These classes render in some way in IDL, but produce effects in POV-Ray that can’t be completely replicated in IDL.

I think the current VISgrPOVRay class supports objective 1 common 3-dimensional object graphics scenes, but lacks support for every property provided by the IDL library. Support for most properties can be easily done as needed.

The second objective requires creating new subclasses of classes in the IDL library with additional properties. For example, there is a VISgrPOVRayLight class which inherits from IDLgrLight class, but also provides support for the POV-Ray area light type. There is a VISgrPOVRayView which provides for features like focal blur in addition to the properties of the IDLgrView. There are also subclasses for grid planes, streamlines, and POV-Ray polygon graphics atoms as well as a finish attribute class. More classes can be added to support additional POV-Ray features as necessary.

POV-Ray is an open-source 3D ray-tracing tool (check out the Hall of Fame for examples of its use). After listening to Peter Messmer’s excellent introduction to POV-Ray scene creation and seeing VisIt’s export to POV-Ray capability, I thought it wouldn’t be that hard to make an object graphics destination. To use it, create the scene as usual and then have the POV-Ray destination send its output to files for input into POV-Ray (the same as using IDLgrWindow).

POV-Ray cow

The image shown is a direct export of a simple object graphics scene with an IDLgrPolygon (cow10.sav) with VERT_COLORS and SHADING=1, a directional light source, and a green IDLgrPolygon. Right now the POV-Ray destination supports ambient and directional lights, polygons, and surfaces. Only a few properties for polygons are supported: vertex colors, shading, and color. I’ll add more property and atom support as needed. The only problem right now is that POV-Ray uses a left-handed coordinate system, so I’m trying to think of the most elegant way to turn things around so that its scene matches the IDL scene.

I’m not sure what I’m going to do with the source code right now. I’ve added it to a ā€œvisualization libraryā€ that I’ve been using. Let me know if you are interested in trying it out.

UPDATE: it’s not hard to turn the POV-Ray output into a movie.

Revolution IDL is a nifty new tool created by Eduardo Iturrate at ITT VIS to create object graphics scenes. It can place text, images, various kinds of plots and polygons, and object graphics entities like clipping planes and lights into a scene. You can be translate, scale, and rotate all the objects in the scene as well as change their properties.

Revolution IDL

What makes this a great tool for learning object graphics is that it reverses the normal workflow from code to visualization. Revolution IDL can support a workflow where you:

  1. use a visual interface to create the scene
  2. display and edit the object graphics tree and the properties of the selected object, and
  3. export the scene to IDL code that makes the scene.

The code exported is then an automated script to make the same visualization. From there it can then be modified to get the real data, produce a different kind of ouput, and/or be inserted into your system where it is needed. It would be nice if the iTools could do things like this, but until then Revolution IDL can get you started.

I was motivated to make a diagram of IDL’s operators by Mark Lentczner’s Periodic Table of the Operators in Perl. IDL doesn’t have nearly as many operators as Perl, so the IDL Periodic Table is much sparser. I added some operators not listed in the table of precedence in the online help. Order of precedence was determined empirically.

Periodic Table of IDL Operators

Here’s the table along with the code and data to make it: mg_make_op_table.pro (docs), mgffxmlsaloperators__define.pro (docs), and operators.xml. MGffXMLSAXOperators is an example of using IDL’s SAX parser (and object-graphics).

The main problem that programmers new to object graphics have is getting anything to appear on the screen (at least anything they recognize). Once you have your graphics appearing in a window, it’s pretty simple to change their properties, rotate them, make them dance the rhumba, etc.

Simple object graphics example

There are various ways to scale your data into the view volume. This article will show one scaling technique I like for displaying 3D objects: using the [XYZ]COORD_CONV properties of a graphics atom object.

The code for the example is MG_CC_DEMO (doc) and a helper routine MG_LINEAR_FUNCION (doc).

The simplest possible object graphics hierarchy is an IDLgrView containing an IDLgrModel which contains a graphics atom object.

oview = obj_new('IDLgrView')
<omodel = obj_new('IDLgrModel')
oview->add, omodel

The surface object has many properties effecting its display. Here, style=2 is a shaded surface, while COLOR and BOTTOM set the color of the top and bottom of the surface.

osurface = obj_new('IDLgrSurface', hanning(20, 20), style=2, $
                   color=[255, 0, 0], bottom=[100, 0, 0])
omodel->add, osurface

The default lighting model is an ambient light that will cause 3D objects to look flat like a silhouette. I put the light in its own model so that when I rotate the model containing the surface the light will stay fixed. Here, type=2 specifies a directional light coming from [1, 1, 1] and heading towards the origin.

olightmodel = obj_new('IDLgrModel')
oview->add, olightmodel
olight = obj_new('IDLgrLight', type=2, location=[1, 1, 1])
olightmodel->add, olight

The hierarchy is complete, but we need to change some properties to get a more reasonable display. The most important task is to make sure the data space is scaled into the coordinates of the view. The default coordinates of the view vary from -1 to 1 in every dimension. First, we need to know the extent of our data in each dimension.

osurface->getProperty, xrange=xr, yrange=yr, zrange=zr

The two-element arrays xr, yr, and zr contain the minimum and maximum value of their respective variables. A handy routine MG_LINEAR_FUNCTION produces a linear function which scales its first argument into its second argument. (A lot of people use the IDL library routine NORM_COORD to do this. NORM_COORD is like MG_LINEAR_FUNCTION with a second argument that is always [0, 1], so it must be shifted to use the range [-0.5, 0.5] and other ranges require some algebra.)

xc = mg_linear_function(xr, [-0.6, 0.6])
yc = mg_linear_function(yr, [-0.6, 0.6])
zc = mg_linear_function(zr, [-0.6, 0.6])

The two-element arrays xc, yc, and zc are the coefficients of linear functions. These are the appropriate values for the `[XYZ]COORD_CONV properties of the surface.

osurface->setProperty, xcoord_conv=xc, ycoord_conv=yc, zcoord_conv=zc

The default orientation will be looking straight down on the surface. A better orientation will make the shape of the surface more apparent. Remember: the model is responsible for holding the transformation matrix, so use methods on the model to rotate, scale, or translate.

omodel->rotate, [1, 0, 0], -90
omodel->rotate, [0, 1, 0], 30
omodel->rotate, [1, 0, 0], 30

Finally, create a destination, here an IDLgrWindow, and use its draw method.

owindow = obj_new('IDLgrWindow', dimensions=[400, 400])
owindow->draw, oview

Don’t forget to destroy the view object when you’re done with the object graphics hierarchy. It will in turn destroy the objects that are contained in it. The owindow object will be destroyed when the user closes the window.

obj_destroy, oview

This program is an example of using timer events to do a task in the background while allowing the the user interface to still respond to events. This technique requires the background task to be split into parts which are short enough for each part to complete quickly enough for a user to not notice a delay if he begins to interact with the interface. In other words, if each part of the task takes 0.1 seconds, then there is a potential delay of 0.1 seconds before the user interface will respond.

mg_timer_demo screenshot

Here are the source and docs for the demo.

I’m going to use the normal ā€œpstateā€ technique for passing data to my event handlers and standard techniques for manipulating object graphics interactively without explaining them. I’m only going to talk about the ā€œnewā€ part: timer events to handle a background task.

First, we need a widget that won’t generate events normally. We could use one we already have, but I usually create one specifically to do the timer events. Since bases don’t appear in the interface, I often use something like:

timer = widget_base(toolbar, uname='timer')

Next, we’ll need a few fields in our state structure to store information related to the timer events.

state = { oview: oview, $
          owindow: owindow, $
          otrack: otrack, $
          time: 0.1, $
          t: 0L, $
          stop: 1B $
        }

The time field holds the time value between timer events. Make sure this is long enough to accomplish a step in your task. The t field is a counter. We’ll rotate the surface one degree each time the timer goes off and stop after 360 rotations; using the counter to mark our position. The stop field indicates if the ā€œstopā€ button has been hit (the initial condition is as if the ā€œstopā€ button was just hit).

The event handler code for the timer event is fairly straightforward. Here is the case for timer events:

'timer' : begin
    if ((*pstate).stop) then return
    if (++(*pstate).t gt 360) then return
    omodel = (*pstate).oview->getByName('model')
    omodel->rotate, [0, 1, 0], 1
    (*pstate).owindow->draw, (*pstate).oview
    widget_control, event.id, timer=(*pstate).time
  end

If the stop field is set, then we don’t do anything. If the counter (which we increment) has gone past 360 we also don’t do anything. Otherwise, rotate the model by a degree around the y-axis. Finally, set the timer to go off again in (*pstate).time seconds.

Another choice for handling background tasks is to use the new IDL_IDLBridge class. With the IDL_IDLBridge class technique, the task would not have to be broken down into small subtasks, but there are more difficulties with overhead and passing data. This will be the subject of a future article.

When rendering object graphics atoms with transparency, the order the atoms are rendered (i.e. the order the atoms are created and added to the hierarchy) determines what can be seen. You want to draw the atoms from back to front when the front items are transparent. The same principle holds true for iTools since they use object graphics.

Rendering order

If you add atoms to a model in the wrong order, you can always change the order with the IDL_Container::move method. With the iTools, you can use the BringToFront, SendToBack, BringForward, and SendBackward operations. These are available from the ā€œEditā€ menu or the context menu for a visualization. You can use these operations with code as well. First create a surface iTool and get its object reference (we’ll assume data has been read in already).

isurface, d
id = itGetCurrent(tool=otool)

To make the surface transparent, we’ll get its identifier and use the IDLitTool::doSetProperty method.

surfID = otool->findIdentifiers('*surface', /visualization)
result = otool->doSetProperty(surfID[0], 'Transparency', 50)
otool->commitActions

The IDLitTool::commitActions method tells the iTool that it should put any actions (like our property change) into the undo/redo buffer and refresh the window. Now, we’re ready to add a second data set.

iplot, x, y, z, /overplot

Finally, we find the identifier for the SendToBack operation and do it.

sendToBackID = otool->findIdentifiers('*sendtoback', /operations)
result = otool->doAction(sendToBackID)

Unlike doSetProperty, the IDLitTool::doAction does not need commitActions for its action to be added to the undo/redo buffer.

Here’s what the results look like:

Polylines in front (added last); you can’t see through the supposedly transparent surface. Polylines sent to back; now you can partially see the polyline behind the surface.
Polyline in front Polyline in back

Here are the source code and docs for the demo.

Anaglyphs are 3D images made from stereographic image pairs and viewed with red-blue glasses. I have a destination class, MGgrWindow3D, for the object graphics system that will automatically produce anaglyphs from regular 3D object graphics hierarchies (but not 2D plots and images). I’ve seen anaglyphs which are able to maintain color information; I might try to update the code to allow for that, but right now everything ends up greyscale.

Example anaglyph produced by MGgrWindow3d

It’s easy to use MGgrWindow3D to make anaglyphs. Simply make the object graphics hierarchy and then use a MGgrWindow3D object as the destination:

owindow = obj_new('MGgrWindow3d')
owindow->draw, oview

The only special code for making an anaglyph in a widget program is the use of the classname keyword:

draw = widget_draw(tlb, xsize=400, ysize=400, graphics_level=2, $
                   /motion_events, /button_events, $
                   classname='MGgrWindow3D', uname='draw')

The CLASSNAME keyword was undocumented for a while, but has been officially documented since IDL 6.2.

Code for making anaglyphs:

MGgr3DConverter__define.pro (doc)

This code does the heavy lifting of producing left and right eye images and combining them.

MGgrWindow3D__define.pro (doc)

A drop-in replacement for IDLgrWindow that produces anaglyphs instead of regular output.

mg_3d_demo.pro (doc)

A demo of producing an anaglyph.

mg_3dwidget_demo.pro (doc)

A demo of producing an anaglyph in a draw widget.

A couple weeks ago, I wrote a demo program to view JPEG 2000 images as a ā€œregularā€ widget program. Now I want to rewrite the same program as an ā€œobject widget,ā€ in other words write methods of a class instead of normal functions and procedures. You need to already understand the basics of object-oriented and widget programming in IDL to follow along with this example. The following files are needed for this program: mgtilejp2__define.pro (doc), mgobjectwidget__define.pro (doc), mg_object_event_handler.pro , and mg_object_cleanup.pro.

JPEG 2000 tile viewer as an object widget

This new program will have exactly the same functionality as the old program. So what are the advantages of writing the program as an object? Of course it’s a matter of preference and some will already have a preference to use or avoid objects. Here are my thoughts about the advantages of this approach:

  1. Cleaner: state/pstate doesn’t need to be passed around.
  2. Better encapsulation when multiple programs are interacting with each other and passing messages to each other. In other words, one program doesn’t need to know internal details about the other program in order to pass a message to it.

New architecture

There are several techniques to make ā€œobject widgets.ā€ I found the following technique simple yet still has the advantages of object-oriented programming. The steps to making a simple object widget:

  1. The member variables of the object hold what used to be in the state or pstate.
  2. The init method is the widget creation part of the program. One important item: place self in the UVALUE of the top-level base. This will make our scheme for event handling work.
  3. XMANAGER can’t call a method for an event handler or cleanup routine, so we will trick it. Instead write a simple two line event handler which pulls out the UVALUE of the top-level base (which is our object widget reference) and calls the real event handler method of the object. This will mean that our object widget will only have a single event handler that handles all events (but possibly dispatches events to other routines). The same must be done for the cleanup routine.

Once you’ve done this, it’s fairly easy to modify the structure a bit for your own purposes.

Code changes

Here’s what I had to do to make MG_TILEJP2 into an object widget:

  1. Change the names of the routines from mg_tilejp2_refresh to mgtilejp2::refresh. mgtilejp2 becomes mgtilejp2::init. Make sure to change it to a function and return 1.
  2. Add MGTILEJP2__DEFINE that simply names the class, inherits from MGObjectWidget, and creates the member variables with the same name and type as the fields of the old state variable.
  3. Convert creating pstate and putting it into the tlb’s UVALUE into putting self into the tlb’s UVALUE. Get rid of
widget_control, event.top, get_uvalue=pstate

in the event handler.

  1. Fix up passing around pstate since it’s no longer needed. Fix (*pstate).owindow to self.owindow.
  2. Added a cleanupWidgets method, fixed up cleanup method, and MG_TILEJP2_EVENT to mgtilejp2::handleEvents.

See previous post about tiling JPEG 2000 images with IDLgrImage.

UPDATED 10/25/13: Updated links of library source code.

I’m finding myself using object graphics over direct graphics more and more in my projects. My rule used to be to use direct graphics unless there was a compelling reason to use object graphics. This rule has now reversed itself, making object graphics the new default for me.

Here are the reasons which have lead to this:

  1. Visualization properties are persistent. So after you create a visualization, it is easy to change one property of your visualization and redraw. This is particularly nice in widget programs.
  2. Anti-aliased fonts.
  3. IDLgrBuffer beats the True Color deficient Z buffer, hands down.
  4. You want to make a 3D visualization with multiple items? In direct graphics, you can use the Z buffer to do hidden line removal, but then you are stuck in 8-bit color. Object graphics handles 3D graphics by default (of course, there are some minor snags with the interaction of rendering order and transparency, but nothings perfect).
  5. Slice through anything with the CLIP_PLANES keyword.
  6. Make anything transparent with the ALPHA_CHANNEL keyword.
  7. Volume rendering.
  8. It’s easier to create a visualization for one destination (like a graphics window) and then send it to another (like the printer or a Postscript file).
  9. No system variables!
  10. With the iTools using object graphics, in most situations you can create a quick visualization as fast as in direct graphics (or should I say, in as much typing). (See some of the advantages of direct graphics below for a situation you can’t do this.)
  11. Since the MAP_PROJ_* routines were introduced in IDL 5.6, it’s possible to do mapping in object graphics.
  12. With IDL 6.2 and a decent graphics card, displaying images is faster in object graphics.

Of course, there are some reasons still to use direct graphics.

  1. While debugging a program, if you’re stopped at a line in the source code and want to make a quick visualization of one of the local variables, your options are limited. ā€œQuick visualizationā€ doesn’t really fit with hand coding object graphics, nor are the iTools an option in this situation since the event handlers for the widget program will not work. So I use direct graphics.
  2. There are still some tricks with pixmaps in widget programs that I think look better in direct graphics.

Feel free to comment below on your own findings about object graphics versus direct graphics. Does anyone have another great reason to use direct graphics (besides the learning curve)?

« newer postsolder posts »