OpenVDB  6.2.0
Transforms and Maps

Contents

Transforms in OpenVDB

The OpenVDB Tree is a sparse representation of a three-dimensional array of voxels, each element of which is addressed via discrete, three-dimensional index space coordinates, typically in the form of a Coord. For example, the following code retrieves the floating-point value of a voxel with index coordinates (1, 2, 3):

openvdb::FloatGrid grid = ...;
openvdb::Coord ijk(1,2,3);
float value = accessor.getValue(ijk);

A Transform relates index space coordinates to world space coordinates that give a spatial context for the discretized data. Translation from index coordinates (i,&nbspj,&nbspk) to world space coordinates (x,&nbspy,&nbspz) is done with a call to the indexToWorld method, and from world space coordinates to index space coordinates with a call to worldToIndex:

// Create a linear transform that scales i, j and k by 0.1
openvdb::math::Transform::Ptr linearTransform =
openvdb::math::Transform::createLinearTransform(0.1);
// Compute the location in world space that is the image of (1,2,3).
// The result will be (0.1, 0.2, 0.3).
openvdb::Coord ijk(1,2,3);
openvdb::Vec3d worldSpacePoint = linearTransform->indexToWorld(ijk);
// Compute the location in index space that is the pre-image of (0.1, 0.2, 0.3).
// The result will be (1.0, 2.0, 3.0).
openvdb::Vec3d indexSpacePoint = linearTransform->worldToIndex(worldSpacePoint);

In the above example, there are two things to notice. First, world space locations are specified as three-dimensional, double-precision, floating-point vectors, and second, worldToIndex does not return discrete coordinates, but rather a floating-point vector. This is a reflection of the fact that not every location in a continuous world space, i.e., not every (x,&nbspy,&nbspz), is the image of discrete integral coordinates (i,&nbspj,&nbspk).

Linear Transforms

Currently two different types of transforms are supported: linear and frustum transforms. A linear transform can be composed of scale, translation, rotation, and shear; essentially those things that can be mathematically represented by an invertible, constant-coefficient, 3×3 matrix and a translation (mathematically, an affine map). An essential feature of a linear transformation is that it treats all regions of index space equally: a small box in index space about origin (i,&nbspj,&nbspk)=(0,0,0) is mapped (sheared, scaled, rotated, etc.) in just the same way that a small box about any other index point is mapped.

// Create a linear transform from a 4x4 matrix (identity in this example).
openvdb::math::Mat4d mat = openvdb::math::Mat4d::identity();
openvdb::math::Transform::Ptr linearTransform =
openvdb::math::Transform::createLinearTransform(mat);
// Rotate the transform by 90 degrees about the X axis.
// As a result the j-index will now map into the -z physical direction,
// and the k-index will map to the +y physical direction.
linearTransform->preRotate(M_PI/2, openvdb::math::X_AXIS);

Frustum Transforms

The frustum transform is a nonlinear transform that treats different index points differently. And while the linear transform can be applied to any point in index or world space, the frustum transform is designed to operate on a subset of space. Specifically, it transforms a given box in index space to a tapered box in world space that is a frustum of a rectangular pyramid.

// Create the bounding box that will be mapped by the transform into
// the shape of a frustum.
// The points (0,0,0), (100,0,0), (0,50,0) and (100,50,0) will map to
// the corners of the near plane of the frustum, while the corners
// of the far plane will be the images of (0,0,120), (100,0,120),
// (0,50,120) and (100,50,120).
const openvdb::math::BBoxd bbox(/*min=*/openvdb::math::Vec3d(0,0,0),
/*max=*/openvdb::math::Vec3d(100,50,120));
// The far plane of the frustum will be twice as big as the near plane.
const double taper = 2;
// The depth of the frustum will be 10 times the x-width of the near plane.
cosnt double depth = 10;
// The x-width of the frustum in world space units
const double xWidth = 100;
// Construct a frustum transform that results in a frustum whose
// near plane is centered on the origin in world space.
openvdb::math::Transform::Ptr frustumTransform =
openvdb::math:::Transform::createFrustumTransform(
bbox, taper, depth, xWidth);
// The frustum shape can be rotated, scaled, translated and even
// sheared if desired. For example, the following call translates
// the frustum by 10,15,0 in world space:
frustumTransform->postTranslate(openvdb::math::Vec3d(10,15,0));
// Compute the world space image of a given point within
// the index space bounding box that defines the frustum.
openvdb::Coord ijk(20,10,18);
openvdb::Vec3d worldLocation = frustumTransform->indexToWorld(ijk);

Cell-Centered vs. Vertex-Centered Transforms

When partitioning world space with a regular grid, two popular configurations are cell-centered and vertex-centered grids. This is really a question of interpretation and transforms.

The cell-centered interpretation imagines world space as divided into discrete cells (e.g., cubes) centered on the image of the index-space lattice points. So the physical location (x,&nbspy,&nbspz) that is the image (result of indexToWorld) of a lattice point (i,&nbspj,&nbspk) is the center of a cube. In the vertex-centered approach, the images of the lattice points form the vertices of cells, so the location (x,&nbspy,&nbspz) would be a corner, not the center, of a cube.

The link between transforms and cell-centered or vertex-centered partitioning of world space is tenuous. It boils down to this: the “first” cell vertex often is aligned with the origin. In the cell-centered case, when the cells are cubes of length Δ on a side, the transform (x,&nbspy,&nbspz) = (Δi + Δ/2, Δj + Δ/2, Δk + Δ /2) will place the center of the first cube (i.e., the image of (0,0,0)) at the world space location of (Δ/2, Δ/2, Δ/2), and the cube will have walls coincident with the x=0, y=0 and z=0 planes. Using the OpenVDB transforms to create a so-called cell-centered transform could be done like this:

// -- Constructing a uniform, cell-centered transform --
// The grid spacing
const double delta = 0.1;
// The offset to cell-center points
const openvdb::math::Vec3d offset(delta/2., delta/2., delta/2.);
// A linear transform with the correct spacing
openvdb::math::Transform::Ptr transform =
openvdb::math:::Transform::createLinearTransform(delta);
// Add the offset.
transform->postTranslate(offset);

In contrast, for the vertex-centered partitions of space the first vertex is just the image of the first lattice point (i,&nbspj,&nbspk) = (0,0,0), and the transform would lack the offset used in the cell-centered case; so it would simply be (x,&nbspy,&nbspz) = (Δi, Δj, Δk).

// -- Constructing a uniform, vertex-centered transform --
// The grid spacing
const double delta = 0.1;
// A linear transform with the correct spacing
openvdb::math::Transform::Ptr transform =
openvdb::math:::Transform::createLinearTransform(delta);

Voxel Interpretations

A similar and often related concept to cell- and vertex-centered partitioning of world space is the idea of a voxel. A voxel historically refers to the volumetric equivalent of a pixel and as such implies a small region of world space. A voxel could, for instance, be the image under transform of a vertex-centered (or cell-centered) box in index space. In this way the voxel can be seen as a generalization of regular grid cells. The interpretation of data stored in a grid can be related to the concept of a voxel but need not be. An application might interpret the grid value indexed by (i,&nbspj,&nbspk) as the spatial average of a quantity in a defined world-space voxel centered on the image of that lattice point. But in other situations the value stored at (i,&nbspj,&nbspk) might be a discrete sample taken from a single point in world space.

The Transform class does include methods such as voxelSize and voxelVolume that suppose a particular interpretation of a voxel. They assume a voxel that is the image of a vertex-centered cube in index space, so the voxelSize methods return the lengths of line segments in world space that connect vertices:

openvdb::Coord ijk(0,0,0);
openvdb::Coord tmp0(1,0,0), tmp1(0,1,0), tmp2(0,0,1);
size.x() = (xform.indexToWorld(ijk + tmp0) - xform.indexToWorld(ijk)).length();
size.y() = (xform.indexToWorld(ijk + tmp1) - xform.indexToWorld(ijk)).length();
size.z() = (xform.indexToWorld(ijk + tmp2) - xform.indexToWorld(ijk)).length();
// The voxelSize() for the voxel at (0,0,0) is consistent with
// the computation above.
assert(xform.voxelSize(ijk) == size);

In the case where the transform is linear, the result of voxelSize will be independent of the actual location (i,&nbspj,&nbspk), but the voxel size for a nonlinear transform such as a frustum will be spatially varying. The related voxelVolume can not in general be computed from the voxelSize, because there is no guarantee that a general transform will convert a cube-shaped voxel into another cube. As a result, voxelVolume actually returns the determinant of the transform, which will be a correct measure of volume even if the voxel is sheared into a general parallelepiped.

Staggered Velocity Grids

Staggered velocity data is often used in fluid simulations, and the relationship between data interpretation and transforms is somewhat peculiar when using a vector grid to hold staggered velocity data. In this case the lattice point (i,&nbspj,&nbspk) identifies a cell in world space by mapping to the cell’s center, but each element of the velocity vector is identified with a different face of the cell. The first element of the vector is identified with the image of the (i−1/2, jk) face, the second element with (ij−1/2, k), and the third element with (ijk−1/2).

Maps in OpenVDB Transforms

The actual work of a Transform is performed by an implementation object called a Map. The Map in turn is a polymorphic object whose derived types are designed to optimally represent the most common transformations. Specifically, the Transform holds a MapBase pointer to a derived type that has been simplified to minimize calculations. When the transform is updated by prepending or appending a linear operation (e.g., with preRotate), the implementation map is recomputed and simplified if possible. For example, in many level-set-oriented applications the transform between index space and world space is simply a uniform scaling of the index points, i.e., (x,&nbspy,&nbspz) = (Δi, Δj, Δk), where Δ has some world space units. For transforms such as this, the implementation is a UniformScaleMap.

// Create a linear transform that scales i, j and k by 0.1
openvdb::math::Transform::Ptr linearTransform =
openvdb::math::Transform::createLinearTransform(0.1);
// Create an equivalent map.
openvdb::math::UniformScaleMap uniformScale(0.1);
// At this time the map holds a openvdb::math::UniformScaleMap.
assert(linearTransform->mapType() == openvdb::math::UniformScaleMap::type());
openvdb::Coord ijk(1,2,3);
// Applying the transform...
openvdb::math::Vec3d transformResult = linearTransform->indexToWorld(ijk);
// ...is equivalent to applying the map.
openvdb::math::Vec3d mapResult = uniformScale.applyMap(ijk);
assert(mapResult == transformResult);

There are specialized maps for a variety of common linear transforms: pure translation (TranslationMap), uniform scale (UniformScaleMap), uniform scale and translation (UniformScaleTranslateMap), non-uniform scale (ScaleMap), and non-uniform scale and translation (ScaleTranslateMap). A general affine map (AffineMap) is used for all other cases (those that include non-degenerate rotation or shear).

An Equivalent Matrix Representation

The matrix representation used within OpenVDB adheres to the minority convention of right-multiplication of the matrix against a vector:

// Example matrix transform that scales, next translates,
// and finally rotates an incoming vector
openvdb::math::Mat4d transform = openvdb::math::Mat4d::identity();
transform.preScale(openvdb::math::Vec3d(2,3,2));
transform.postTranslate(openvdb::math::Vec3d(1,0,0));
transform.postRotate(openvdb::math::X_AXIS, M_PI/3.0);
// Location of a point in index space
openvdb::math::Vec3d indexSpace(1,2,3);
// Apply the transform by right-multiplying the matrix.
openvdb::math::Vec3d worldSpace = indexSpace * transform;

Any linear map can produce an equivalent AffineMap, which in turn can produce an equivalent 4×4 matrix. Starting with a linear transform one can produce a consistent matrix as follows:

if (transform->isLinear()) {
// Get the equivalent 4x4 matrix.
matrix = transform->getBaseMap()->getAffineMap()->getMat4();
}

This could be used as an intermediate form when constructing new linear transforms by combining old ones.

// Get the matrix equivalent to linearTransformA.
linearTransformA->getBaseMap()->getAffineMap()->getMat4();
// Invert the matrix equivalent to linearTransformB.
(linearTransformB->getBaseMap()->getAffineMap()->getMat4()).inverse();
// Create a new transform that maps the index space of linearTransformA
// to the index space of linearTransformB.
openvdb::math::Transform::Ptr linearTransformAtoB =
openvdb::math::Trasform::createLinearTransform(matrixA * matrixBinv);

Notice that in the above example, the internal representation used by the transform will be simplified if possible to use one of the various map types.

Working Directly with Maps

Accessing a transform’s map through virtual function calls introduces some overhead and precludes compile-time optimizations such as inlining. For this reason, the more computationally intensive OpenVDB tools are templated to work directly with any underlying map. This allows the tools direct access to the map’s simplified representation and gives the compiler a free hand to inline.

Maps themselves know nothing of index space and world space, but are simply functions xrange = f(xdomain) that map 3-vectors from one space (the domain) to another (the range), or from the range back to the domain (xdomain = f−1(xrange)), by means of the methods applyMap and applyInverseMap.

// Create a simple uniform scale map that scales by 10.
openvdb::math::UniformScaleMap usm(10.0);
// A point in the domain
openvdb::math::Vec3d domainPoint(0,1,3);
// The resulting range point
openvdb::math::Vec3d rangePoint = usm.applyMap(domainPoint);
// The map is inverted thus:
assert(domainPoint == usm.applyInverseMap(rangePoint));

In many tools, the actual map type is not known a priori and must be deduced at runtime prior to calling the appropriate map-specific or map-templated code. The type of map currently being used by a transform can be determined using the mapType method:

// Test for a uniform scale map.
if (transform->mapType() == openvdb::math::UniformScaleMap::type()) {
// This call would return a null pointer in the case of a map type mismatch.
openvdb::math::UniformScaleMap::ConstPtr usm =
transform->map<openvdb::math::UniformScaleMap>();
// Call a function that accepts UniformScaleMaps.
dofoo(*usm)
}

To simplify this process, the function processTypedMap has been provided. It accepts a transform and a functor templated on the map type.

Maps and Mathematical Operations

In addition to supporting the mapping of a point from one space to another, maps also support mapping of local gradients. This results from use of the chain rule of calculus in computing the Jacobian of the map. Essentially, the gradient calculated in the domain of a map can be converted to the gradient in the range of the map, allowing one to compute a gradient in index space and then transform it to a gradient in world space.

// Compute the gradient at a point in index space in a
// floating-point grid using the second-order central difference.
openvdb::FloatGrid grid = ...;
openvdb::Coord ijk(2,3,5)
openvdb::math::Vec3d isGrad =
openvdb::math::ISGradient<CD_2D>::result(grid, ijk);
// Apply the inverse Jacobian transform to convert the result to the
// gradient in the world space defined by a map that scales index space
// to create voxels of size 0.1 x 0.2 x 0.1
openvdb::math::ScaleMap scalemap(0.1, 0.2, 0.1);
openvdb::math::Vec3d wsGrad = scalemap.applyIJT(isGrad);