Volsh User's Guide
Volsh is a portable, volume renderer that provides fast, high-fidelity, visualizations of byte-scalar, volumetric datasets. The Volsh user interface is text-based. You may either type commands interactively at the Volsh interpreter, or you may feed the interpreter a file containing a Volsh script. The Volsh interpreter is implemented via an extension to the Tcl scripting language. Hence, all of the language features available to a base Tcl interperter are available in Volsh.
If you are already a Tcl user you should have no trouble learning to use the Volsh extensions. If you are not familiar with Tcl, all is not lost, you should still be able to begin generating images rather quickly. However to construct complicated command scripts you'll want to get better acquainted with the Tcl language. I highly recommend John Ousterhout's excellent book, "Tcl and the Tk Toolkit", for this purpose.
The actual rendering performed by Volsh is carried out by the VolPack rendering library, developed by Phil Lacroute as part of his PhD thesis while at Stanford University. The VolPack library, and consequently Volsh, use no special graphics hardware. All rendering is performed in software by the CPU.
In addition to Tcl and VolPack, Volsh leverages off of at least one other software package: the free-ware volume renderer, Bob. The data formats, and Look Up Table (LUT) based shading and classification model supported by Volsh are identical to those supported by Bob. This was done so that Bob users would have no trouble moving back and forth between Bob and Volsh.
It is not the intent of this document to teach the Tcl scripting language or how to use Bob. Though it is not necessary to be experts with either of these utilities, some familiarity will certainly be helpful.
This document will also not rehash all of the information available in the VolPack User's Guide . It's relatively easy to start rendering voxel data sets with Volsh, but at some point, in order to get best results, you'll want to peruse the VolPack document.
Lastly, this document is not a tutorial. The best way to learn how to use Volsh is probably to experiment with the sample scripts contained in the examples directory. In fact, once you understand Volsh's simple data format, you can probably learn enough to start rendering your own data just by looking at the examples and no other documentation.
Volsh provides a simple, text-based on-line help facility. The on-line help facility serves as the reference manual for Volsh commands and state variables. To invoke help simply type help or help help at the Volsh interpreter prompt.
Much of the operation of Volsh is based on, or was designed in response to, the BoB (Brick of Bytes) volume visualization tool. Volsh was designed to complement, not compete with, the capabilities offered by Bob. It is often the case that it is best to work more or less simultaneously with both Bob and Volsh.
Bob has a graphical user interface, and is therefor generally better suited than Volsh for highly interactive work, such as data browsing. However, Volsh's text-based user interface makes it a better candidate for performing a batch visualization process, such as creating a long, complex animation sequence.
Volume renderers typically make trade-offs between speed and image quality. Bob and Volsh were both designed with an emphasis on speed. However, unlike Bob the Volsh design incorporates a local lighting model, and makes fewer sacrifices with image quality - you can generally produce more visually accurate images with Volsh.
Since Bob and Volsh both support identical data formats. Data sets can be shared between the two packages without needing to perform any data conversion first. Additionally Look Up Tables (LUT's) produced with icol - a stand-alone LUT editor that is part of the Bob package - can be ingested by Volsh.
Volsh is essentially a state machine. You establish a rendering context state through the setting of state variables, and then tell Volsh to render an image based on Volsh's current state. State variables in Volsh are actually Tcl variables. You can perform the same operation on a Volsh state variable as you can any other Tcl variable. The only difference between Volsh state variables and other Tcl variables is that the Volsh interpreter catches any assignments made to state variables and makes sure that the assigned values are valid.
Just about any kind of information you might need to specify to Volsh is done with a state variable. Examples include paths to data files, shading attributes, and rendering optimizations.
All Volsh state variables begin with the prefix v_.
NOTE: The Tcl syntax requires the right-hand side of a variable assignment to be a single argument. Some Volsh state variables expect multiple values. To accomplish this a multi-valued assignment needs to be enclosed in quotes, e.g
In addition to state variables Volsh extends the Tcl interpreter to support a number of commands, the most important of which being the render command. A typical Volsh session involves setting some number of state variables - providing information such as the paths to data files, shading and classification attributes, rendering algorithm types, etc - and then using the render command to tell Volsh to render the scene based on the current rendering context state. This process may than be reiterated over and over again in a single rendering session, while changing the values of various state variable to provide different viewing positions, new data sets, or new transfer functions describing what objects are to be viewed in the volume.
NOTE: Volsh caches information created during the rendering process and tries to reuse this information for subsequent renderings. The more information Volsh can cache, the faster it can work. The setting of certain state variables is one way to invalidate components of the rendering cache. Indiscriminate setting of state variables will defeat optimal performance. More on this later.
Volsh can either be run interactively by typing commands at the keyboard, or Volsh can run in batch mode, processing a script read from a file. An interactive Volsh rendering session is begun by starting up the Volsh interpreter: simply type volsh. Once the Volsh interpreter is running, you may begin entering Volsh (Tcl) commands.
To run in batch mode you can either feed Volsh a script from the standard input, e.g
or, on most UNIX systems you can place a line like the following
as the first line in your script file, and then simply invoke your script by name (assuming you've set permissions correctly, etc.).
There are two types of input files that Volsh understands: data files, and transfer function Look Up Table (LUT) files. The first is always required. The second is optional. Volsh also understands one output file type: image files.
Volsh supports four different types of data formats: bin, raw, octree, and rle. The latter three, raw, raw, and rle, are optionally created by Volsh by using the volume command to preprocess bin files. These preprocessed data files may be used to improve rendering performance. More on this later.
Volsh bin data files are identical to Bob data files. A Volsh (or Bob) data file contains a single data volume with no header. Data volumes contain one byte per voxel. Voxels should be stored contiguously in a single array. The X dimension varies fastest, followed by the Y dimension, followed by Z. I.e. the dataset is made up of Z planes, each plane containing Y rows, each row is X bytes long.
Regularly gridded data is the only data format that Volsh supports. If you have irregular or unstructured data you'll need to resample your data to a regular grid in order to use Volsh.
The v_data_file state variable is used to specify the path to the data file, e.g.
Because Volsh data files contain no header information you'll need to tell Volsh what the resolution of the data is. This is done with the v_dimension state variable. This variable expects three integer parameters - specifying the X, Y, and Z dimensions - thus you'll need to quote them, e.g.
Look Up Table (LUT) Files
LUT files are optionally used to specify RGB, opacity, and gradient transfer functions. LUT file formats are identical to the Indexed Ascii Text format supported by Bob. A LUT file is an ascii file containing up to 256 quadruples. A quadruple is made up of an index and an intensity triple. In the case of an RGB LUT the intensity triples represent red, green, and blue. In the case of an opacity or gradient LUT, the single-valued opacities or gradients are computed as weighted averages from the intensity triples thusly:
value = 0.299*i1 + 0.587*i2 + 0.114*i3
All four elements of the RGB and opacity quadruples are in the range [0,255]. The elements of a gradient quadruple are in the range [0,221].
It is not necessary to specify all 256 (222) quadruples. Missing quadruples are linearly interpolated.
In the current version of Volsh, rendered images are written to a file in the form of a raster image (future versions of Volsh may support sending rendered images directly to a display). Volsh currently supports two image formats: SGI RGB, and raw encodings. Raw encodings are simply runs of rgb triples, one byte per channel, starting at the upper-left corner of the image.
The format of the raster file is determined by the v_image_format state variable. For example,
sets the output image file format to SGI, RGB
The path to the raster file is set by the v_image_file state variable. Lastly, the resolution of the raster image files is controlled by the v_img_res state variable, e.g.
NOTE:setting the image resolution implicitly determines the viewport transformation (see below).
The shading model and the classification method used by Volsh require surface normal information for the data volume. Prior to rendering, Volsh calculates a gradient direction and magnitude for each voxel in the dataset (The gradient estimation method is determined by the value of the v_normal_algo state variable). The gradient magnitude is used during classification, and the gradient direction is used for shading.
Gradient directions are quantized and then stored as encoded 16-bit values. Gradient magnitudes are stored as 8-bit quantities. Hence, a total four bytes of storage is required for each voxel.
Before a data volume can be rendered by Volsh it must first be classified. The classification process assigns an an opacity value to each voxel in the volume. Opacity values are continuous from 0.0 to 1.0. A value of 0.0 indicates a completely transparent voxel, a value of 1.0 indicates a completely opaque voxel. Intermediate values specify semi-transparent voxels.
The purpose of classification is to assign low opacities to regions which are not of interest, and high opacities to regions which should be visible during rendering. Intermediate opacity values are used to produce smooth transitions from transparent to opaque regions, and for effects such as semi-transparent voxels which should not completely occlude objects behind them.
NOTE:In general regions which are fully transparent or fully opaque render faster than those that are semi-transparent.
The classification method used by Volsh is based on LUTs. Each input voxel value is mapped to an opacity via an opacity LUT. The value returned by the opacity LUT is then attenuated by mapping the voxels gradient magnitude into a gradient LUT. Lastly, the resulting opacity is normalized. In other words the opacity for each voxel can be equated:
NOTE:the gradient magnitude is a measure of the strength of a surface passing through a voxel. i.e. the difference in voxel values between neighboring voxels.
Opacity and Gradient LUT's may be specified either indirectly by setting the v_olut_file or v_glut_file state variables to the path to a file containing an opacity or gradient LUT, respectively. Alternatively these LUT's may be specified directly by setting the v_olut or v_glut state variable to an array of opacity/gradient values. Volsh decides which method is to be used by looking at the v_olut_from and v_glut_from state variables. For example,
tells Volsh to look at the v_olut_file state variable for the path to a file containing an opacity LUT.
LUT values are integers in the range 0 to 255.
The shading model used by Volsh to determine the contribution each voxel makes toward the final rendered image can be described as follows: A light ray traveling through the volume towards the eye enters a voxel with an initial intensity and leaves the voxel with a new intensity. The changes in incoming and outgoing light intensity are caused by two factors: 1) incoming light may be attenuated by semi-translucent voxels (voxels with computed opacity greater than zero), and 2) voxels may reflect light from local light sources. The amount of light reflected by a voxel is calculated by evaluating the Phong Illumination model at the voxel and attenuating this result by the voxel's opacity.
NOTE:Light rays from a light source are not attenuated as they pass through the volume.
In order to evaluate Phong's illumination model, a surface normal and ambient, diffuse, and specular material properties must be known for each voxel. Surface normals are discussed above. Material properties are determined in Volsh by combining a color LUT and a global scaling factor for each material component: A voxel's value is mapped into a color LUT containing red, green, and blue color intensities. Each material property for a given voxel is then computed by multiplying the corresponding color intensity found in the color LUT by the global value for that material property. For example, the red component of the diffuse component is computed:
The color LUT may be specified either indirectly by setting the v_clut_file state variable to the path to a file containing a color LUT. Alternatively the LUT may be specified directly by setting the v_clut state variable to an array of r,g,b intensities. Volsh decides which method is to be used by looking at the v_clut_from state variable.
The global ambient, diffuse, and specular material components are set by the v_ambient, v_diffuse, and v_specular state variables. The specular reflection exponent is set by the v_shinyness state variable.
Volsh supports a local lighting model with multiple (up to six) directional (infinite-distance) light sources. For each light that is enabled (turned on), a color, direction and binding are required. The light binding determines how the light direction will be effected by viewing transformations (see below). Lights can either be bound to the camera or the modelview transformation matrix. When bound to the camera, the light moves with the camera. When bound to the combined model and view transformation matrix, the light's direction vector is transformed by post multiplying it by the combined model and viewing transformation matrix.
The light_dir, light_color, and light_binding commands are used to specify a lights direction, color, and binding, respectively. For example,
light_color 0 '256 256 256' light_dir 0 '0.0 0.0 1.0' light_binding 0 camera' sets light 0 to a full intensity white light, oriented along the positive Z axis, facing the origin, and binds it to the camera.
The light_on command is used to turn individual lights on or off. By default light number 0 is a white light (r,g and b are set to maximum intensity), oriented along the positive z axis, facing the coordinate origin, and turned on. All other light sources are turned off.
NOTE:light sources are expensive - the more you use, the slower you go.
Volsh transformations permit you to orient the data volume and light sources in three-dimensional space. Transformations are performed in Volsh by manipulation of four-by-four, homogeneous, coordinate transformation matrices. Volsh maintains four types of matrices: modeling, viewing, projection, and viewport. A stack of matrices is maintained for each transformation type. Initially there is one matrix in each of the four matrix stacks.
These matrices transform the coordinate systems object coordinates, world coordinates, eye coordinates, clip coordinates, and image coordinates, respectively. In the object coordinate system the volume is entirely contained in a unit cube centered at the origin. The modeling transform is an affine transform which converts object coordinates into world coordinates. The view transform is an affine transformation that converts world coordinates into eye coordinates. In eye coordinates the viewer is looking down the -Z axis. The view transform is typically used to specify the position of the viewer in the world coordinate system. The projection transform converts eye coordinates into clip coordinates. This transform may specify a perspective or a parallel projection, although perspective rendering is not yet supported. Finally, the viewport transform converts the clip coordinate system into image coordinates.
Volsh provides a number of commands for manipulating these matrices. Before a matrix can be manipulated it must be made current with the matrix_current command. Once current, all subsequent matrix operations apply to the top-most matrix on the current transformation stack.
Matrix stack's are manipulated with the matrix_push and matrix_pop commands. matrix_push pushes the current matrix stack down by one, duplicating the current matrix. That is, after a matrix_push call, the matrix on the top of the stack is identical to the one below it.
matrix_pop pops the current matrix stack, replacing the current matrix with the one below it on the stack.
Volsh, or more correctly VolPack, supports three rendering algorithms: rle, octree, none. These algorithms differ in their performance characteristics only and should produce visually-equivalent images. The information below regarding the use of these algorithms comes more or less verbatim from the VolPack user's guide.
The rle algorithm is the fastest and allows the user to rapidly render a volume with any view transformation and with any shading parameters while keeping the classification fixed (not changing the opacity LUT). This algorithm relies on a special data structure which contains run-length encoded, classified volume data. Depending on the volume size it can take several minutes to precompute the run-length encoded volume, so this algorithm is most suitable when many renderings will be made from the same volume without changing the classification.
The steps when using this algorithm to render a classified volume are:
The octree algorithm is useful in situations where the classification will be adjusted frequently. It also relies on a special data structure: a min-max octree which contains the minimum and maximum values of each voxel field. This data structure must be computed once when a new volume is acquired. The volume can then be rendered multiple times with any opacity transfer function, any view transformation and any shading parameters.
The steps when using this algorithm to render an unclassified volume are:
Finally, the third algorithm does not use any precomputed data structures. In most cases it is significantly slower than the other two algorithms and is useful only if you wish to make a single rendering from a given volume data set. The steps for using this algorithm are
The rendering algorithm is selected with the v_class state variable.
Maximizing Image Quality
There are two things you can do to ensure superior image quality. First, make sure you have chosen smooth classification functions. Abrupt transitions or discontinuities in either the gradient or opacity transfer functions will often cause aliasing.
Secondly, avoid rendering images whose resolution is greater than the data. This also means that you should avoid upscaling your data using viewing transformations for best results. If, for example, your data is 256x256x256 then your output image should be no greater than 256x256 and you shouldn't specify any transformation scaling factors greater than 1.0. If you need to upscale or render at a resolution greater than your data's, upsample your data prior to rendering with a high-quality filter.
Maximizing Rendering Speed
Choosing A Classification Algorithm
There are a number of things you can do to maximize your rendering performance (minimize rendering time). Be sure to use the appropriate rendering algorithm (see above) for your task. The best algorithm may not always be obvious.
If you are not changing data sets or classification (opacity) functions often relative to rendering, use the rle rendering algorithm. This is the fastest method. If you are not changing datasets but do need to change classification functions use the octree algorithm. If you are constantly changing datasets specify none.
Changes In State
Each time the Volsh render function is invoked, Volsh looks to see what state data has changed since the last invocation of render. Volsh than takes whatever action necessary based on the changes in state. Certain changes are more expensive than others. In general, its best to avoid setting state variables if their value hasn't changed. Any changes that result in volume normals having to be recalculated should be avoided. The most expensive state variables, in no particular order, include the following:
The speed of all of the rendering algorithms described above is highly dependent on the degree of coherence in your classified data (even if your raw data does not exhibit a great degree of coherence, careful choosing of transfer functions can produced a highly-coherent classified volume). Classified volumes with large transparent regions or large opaque regions render fastest. All the algorithms pay a performance penalty if there are a large number of low-opacity voxels.
If you are streaming through a sequence of data files, rendering only a very small number of frames (n <= 2 or 3) for each volume, one of the most effective way's to improve rendering performance may be to take advantage of Volsh's support for preprocessed data. Binary data files may be preprocessed with the volume command to create raw, octree, or rle data volumes that may be rendered more quickly than the original binary data files themselves.
There are a couple of caveats to consider before embarking on this approach. Preprocessed files may be significantly larger than the original ones. Thus, you may trade off improved rendering performance for degraded I/O. Raw and octree volumes are approximately four times as large as binary volumes. Rle volumes vary widely in size depending on the classification function chosen. In addition to the differences in size between binary and preprocessed volume, the preprocessing step fixes some of the parameters (state variables) used to create the preprocessed volume. These fixed parameters won't be changeable later, during rendering. For example, rle volumes are already classified, subsequent changes in opacity won't be possible.
Preprocessed volumes are created, after setting the appropriate state variables required by the desired preprocessed file format, by invoking the volume command. The format of the resultant preprocessed file is determined by the value of the v_class state variable. To create a raw or octree volume, the only information necessary is that required to read in a binary volume, e.g., path to the file, data resolution, etc. To create an rle volume, you additionally need to specify classification information, e.g., opacities and gradients. The volume created by the volume command is written to the path specified by the v_vol_out_file state variable.
Rendering a preprocessed volume is accomplished in much the same manner as rendering a ordinary binary volume; the only difference being that the information captured by preprocessing should not be re-specified. For example, the dimensions of a preprocessed volume are stored with the volume and do not need to be re-specified with the v_dimension state variable. Additionally, you'll need to inform Volsh what format volume file you are using by way of the v_data_format state variable. By default, Volsh expects binary volume files as described at the beginning of this document.
The use of preprocessed volume files may greatly speed rendering of time-evolving data sets. However, the increased size of the preprocessed volumes may negate any improvements in rendering speed. In general, if I/O considerations can be ignored, rle volumes render fastest, followed by octree, followed by raw, and then bin.
Volsh supports parallel execution on shared-memory, symmetric multiprocessors through the use of POSIX threads (pthreads). Since pthreads are not yet widely available, their inclusion in Volsh is a compile time option (see the Volsh installation documentation for details). Assuming your version of Volsh has been compiled with pthread support, there is a single Volsh state variable that effects parallel execution. The v_pthreads variable controls the number of threads created for parallel portions of the code. Setting this variable to the number of processors on the system does not always yield the best parallel performance. Experiment with different values and see what works best for you.
Volsh provides a rudimentary mechanism for performing animation keyframing. Keyframing - the ability to generate a potentially complex animation sequence by specifying only a few key frames and letting Volsh automatically compute the inbetween frames - is possible by taking advantage of Volsh's vector interpolators in conjunction with the matrix_lookat command.
The matrix_lookat command, based on the OpenGL gluLookAt function, is a convenience utility that permits you to specify a viewing matrix derived from an eye point, a reference point indicating the center of the scene, and an up vector.
Volsh's vector interpolators allow you to perform a spline-based interpolation between a sequence of vectors (points in 3-space). Hence, it's possible to combine the matrix_lookat command with the vector interpolators to produce a smooth and complex scene fly through by only specifying a few key parameters and letting Volsh do the rest. For example, you might select a number of key eye (camera) positions, use a vector interpolator to compute the eye points in between, and then pass the results to the matrix_lookat command as eye coordinate parameters.
Volsh's vector interpolators are somewhat difficult to describe, but once you get the hang of them they're very easy to use. To use a vector interpolator you must first define one with the vec_key_bgn and vec_key_end commands. The vec_key_bgn command informs Volsh that you are about to begin defining a vector interpolator. The command expects a single argument, specifying a name for the interpolator you are defining. The name, which will be used for subsequent commands that you want to apply to this particular interpolator, can be anything you want. For example:
After the intepolater definition has begun, you add interpolation knots (key vectors) to the named interpolator with the vec_key_add command. You can add as many knots as you want. Continuing with our example:
When you're done adding knots, you need to terminate the interpolator definition with the vec_key_end command. In addition to the interpolator name argument, the vec_key_end expects an integer specifying the total number of points that you would like in the entire interpolation curve. The number of points needs to be at least the number of knots that you have defined inside the vec_key_end/vec_key_end pair and probably doesn't make any sense unless the total is greater than the number of knots, e.g.
The interpolator is now completely defined. When interrogated, it will yield 100 points along the curve with the knots specified above.
In order to retrieve points from the interpolator, my_name, two commands must be used: vec_key_next and vec_key_get. vec_key_get returns the current point in the named interpolation curve. vec_key_next tells the named interpolator to advance to the next point. vec_key_next returns the value 0 when the current point is the last point (100th point in our example). If the knots we specified above with vec_key_add were key eye positions we could feed the points returned with vec_key_get to the matrix_lookat command as eye parameters and produce a spline-based, animated fly-through.
Tcl (Volsh) provides a mechanism for defining new procedures from within a Tcl (Volsh) script. As you start performing more complex operations with Volsh you'll probably want to take advantage of this feature. However be careful: variables referenced within a Tcl procedure have scope local to the procedure by default. Volsh state variables have global scope. In order to reference a global variable, e.g. a Volsh state variable from within a Tcl procedure, you'll need to use the Tcl global command to declare the global reference. If you forget to do so, the interpreter won't complain - it will just treat the variable as a common Tcl variable, not as a special Volsh state variable.
The raster image created when the render command is invoked is written to the file specified by the v_image_file state variable. If you don't change the value of this variable (or possibly the value of the image file counter variable, v_image_fcount) between successive renderings, raster images will be concatenated to the image file, which may or may not be the desired result.