HDK
|
Volumes are 3d boxes that are uniformly divided into small cubes, or voxels, with a value stored in said cubes. They are analogous to a stack of images with the voxels representing pixels.
Volumes can be used in many different areas of Houdini, ranging from SOPs, to DOPs, to Mantra. The use to which they are put can likewise vary, from specifying a density cloud to specifying wind velocities or temperature, or the surface of a fluid.
The generic structure to hold an array of voxels is the UT_VoxelArray. This is a templated class, meaning you can use it to build a 3d array out of any number-like data structure you desire. The most common specialization is to store a float at each voxel, and this is typedefed to UT_VoxelArrayF. Another option is to store an int64, a 64-bit integer, at each coordinate.
When you build a UT_VoxelArray with the UT_VoxelArray::size method you specify a resolution. The internal representation is not a flat array of that size, however. Instead, it is broken into 16x16x16 tiles. These tiles can be compressed and accessed independently of each other. The simplest type of compression is constant tiles - tiles that have a uniform value throughout. Because of these, creating a new UT_VoxelArray is fast, even if it is of large resolution, since all the tiles will be constant. Another form of compression of note is with int64 tiles - these will compress down to 8-bit integers if the range of numbers within a tile are restricted, making it efficient to store index-like values in the voxel.
The simplest approach to query the array is the UT_VoxelArray::getValue which takes an integer voxel index and looks it up. Note that this does clamping, so out of bound reads will act according to the UT_VoxelBorderType which the array has been set to. A slightly faster approach is the UT_VoxelArray::operator() which skips the bounds checking if you know your index is within range.
An alternative way to query the array is by position. Imagine taking a cube of unit size with one corner at (0,0,0) and the other at (1,1,1). Divide this up into smaller cubes according to the resolution of the voxel array. Then, at the center of each of the smaller cubes, put a point with the corresponding voxel array value. When you use the UT_VoxelArray::operator() with a UT_Vector3, you will find the 8 closest points to your query point and trilinearly interpolate the value.
Writing to the UT_VoxelArray is performed with UT_VoxelArray::setValue. This must be given integer coordinates within the voxel array's resolution.
Quite often in a volume algorithm you need to loop over all the voxels in an array. One way to do this is with:
While this allows you to control the loop order, it isn't necessarily the best order because of the internal division into 16x16x16 tiles. The UT_VoxelArrayIterator can be used to run over all the voxels in a tile-by-tile order. Within the tile, a z-y-x ordering of loops will be done.
There are some convenience methods with UT_VoxelArrayIterator to fetch or write to the current pointed at voxel.
Also of note are the UT_VoxelArrayIterator::isStartOfTile which can be used to do per-tile tests. For example, you can use UT_VoxelArrayIterator::isTileConstant to test if the tile has one constant value and thus do some short-circuit evaluation.
When writing to an array one can invoke UT_VoxelArrayIterator::setCompressOnExit. When this flag is set (it defaults to not set) whenever the UT_VoxelArrayIterator::advance will move to a new tile the old tile will be tested for compressibility. This can ensure that when filling in constant data that memory does not need to expand beyond a single tile.
Querying the UT_VoxelArray::getValue has a fair bit of overhead. Each query has to determine which tile the index refers to, and then what sort of compression that tile has. If you are accessing the voxel array in a sequential order with the x-step in the inner most loop (such as happens with UT_VoxelArrayIterator) the UT_VoxelProbe can cache most of these lookups so the tile queries are only done every 16 queries.
Volumes, due to their 3d nature, tend to consume a lot of memory. Doubling the resolution of a volume increases the memory needed by eight times. In order to ensure that a chain of SOPs that contain volumes do not require too much memory, the underlying UT_VoxelArray are shared between different GEO_PrimVolume. This is achieved using a Copy On Write paradigm.
The UT_COWHandle, UT_COWReadHandle, and UT_COWWriteHandle set of templates define a copy on write paradigm. The UT_COWHandle holds onto a copy of the data you wish to track. If you want to read from it, you cast it into a UT_COWReadHandle and treat the result as a pointer. If you want to write, you do the same but to a UT_COWWriteHandle. The trick is that when you copy UT_COWHandle it just shares the data - until a UT_COWWriteHandle is performed. Then the UT_COWHandle that was the source is made unique.
In the case of volumes, the UT_VoxelArrayHandleF, UT_VoxelArrayReadHandleF, and UT_VoxelArrayWriteHandleF are the typedefs for the relevant classes.
You should only ever have one UT_VoxelArrayWriteHandle created for a volume at a time!
In particular, if you have mulithreaded code in which you are writing to the same volume from multiple threads, you cannot have the UT_VoxelArrayWriteHandle be created for each thread. You have to create only one of them outside of the threaded code, and pass in a reference to its UT_VoxelArray to the worker threads. (Each worker thread will still have to write to independent tiles.)
In SOPs there is one type of volume, the Volume primitive, which is stored as a GEO_PrimVolume. GEO_PrimVolume have a vertex which references a point. This point represents the center of the volume. They also have a UT_Matrix3 for a general 3x3 transform, thus allowing them to be rotated or scaled.
You can create a new GEO_PrimVolume using GU_PrimVolume::build. GEO_PrimVolume::setTransform can be used to then set the size/orientation of the volume. To set the data, you can either use GEO_PrimVolume::setVoxels if you already have a UT_VoxelArrayHandle, or GEO_PrimVolume::getVoxelWriteHandle to get a handle to write to.
The transform specified is to convert between the voxel's space and the GU_Detail's space. The voxel space is defined to be the 2-radius cube from (-1,-1,-1) to (1,1,1) centered at (0,0,0). The voxel values are taking to be the centers of the voxels of said cube. Note that this is a different positioned default cube than UT_VoxelArray.
The common convention in Houdini is to use the index geometry primitive attribute "name" to store semantic information about the meaning of the volume. The convention is to use .x, .y, and .z to identify the respective components of vector volumes such as velocity fields. Thus the three primitive volumes vel.x, vel.y, and vel.z will be grouped by Mantra and Houdini into a logical vector-valued volume named vel. SOP group fields can use @name=vel
.* to select only the velocity primitive volumes.
GEO_PrimVolume has a few extra parameters in addition to those of UT_VoxelArray. In boundary conditions it supports an SDF boundary condition. This condition will extend the values by selecting the nearest point in the volume and then adding the distance to that point. This simulates the proper expansion of distance field. It also stores the various parameters used for visualization of volumes.
Several examples read and write from GEO_PrimVolume:
One use for volumes is to store signed distance fields. Signed distance fields are a way to represent the surface of a closed piece of geometry using a voxel field. This is done by storing in each voxel the distance to the closest point of the surface. The sign convention is then added that inside the object is negative distances and outside is positive distance. The zero crossing thus becomes an approximation of the original surface.
The GU_SDF class is both a method to build these distance fields as well as having some accessors to interface with the built field. Usually the field will be converted to a GEO_PrimVolume before use, however, as the latter is more general. One exception is the SIM_SDF, a SIM_Data used heavily in DOPs for collision resolution. This will return a GU_SDF structure.
In DOPs the fluid simulations are axis aligned. Instead of a general 3x3 matrix and center point, there is a size and center point. The basic SIM_Data that corresponds to the GEO_PrimVolume is the SIM_RawField. This is then collated into the SIM_ScalarFiled, SIM_VectorField, and SIM_MatrixField to match with the higher level fields in DOPs. The SIM_RawIndexField and SIM_IndexField pair do not have an analogue in SOPs and store an int64 based UT_VoxelArray underneath.
Most manipulation of SIM_ScalarField, etc, are done either at the SIM_RawField or down to UT_VoxelArray using SIM_RawField::field.
As you are iterating over voxels of a SIM_RawField, you may need to read values from other fields. To this end, you can use the SIM_ScalarFieldSampler and SIM_VectorFieldSampler classes to efficiently query the input values.
An example of a micro solver that manipulates simulation fields can be found at SIM/SIM_GasAdd.C and SIM/SIM_GasAdd.h.