The Brick Map API

The Brick Map API

March, 2008 (Updated March, 2009)

Introduction

As of RenderMan Pro Server version 14.0, brick map files can be read using the brick map API. Reading brick maps has several applications - for example: custom brick map lookup filters, other visualization programs than brickviewer, etc.

Note that there are no API functions for writing to a brick map. The reason is that this would be hard to do efficiently: if you insert data at one level in the brick map all other levels have to be updated accordingly. For now, the only way to generate a brick map is using the brickmake program.

API Definition

From an application program's point of view, a brick map and a brick map octree node are just void pointers:

typedef void *BkmBrickMap;
typedef void *BkmBrickMapNode;

RMAN_BRICKMAP_API_VERSION is #define'd in brickmap.h and can be used to distinguish between different versions of the brick map API. In PRMan 14 it was 1, for PRMan 15 and higher it is 3.

The following procedures are used for reading brick map files: BkmOpenBrickMapFile(), BkmGetBrickMapInfo(), BkmGetOctreeRoots(), BkmGetOctreeChildrenList(), BkmGetVoxelData(), and BkmCloseBrickMapFile().

BkmBrickMap BkmOpenBrickMapFile(char *filename);
BkmOpenBrickMapFile() returns a pointer (handle) to the opened brick map, or NULL if the read failed. filename is the name of the brick map file to be read.
int BkmGetBrickMapInfo(BkmBrickMap brickmap, char *request, void *result);

BkmGetBrickMapInfo() fills in the value of result depending on the value of request. Here is a table of known requests and their result types:

request result
"version" int
"bbox" array of six floats
"nvars" int
"vartypes" array of strings
"varnames" array of strings
"datasize" int
"nbricks" int
"nvoxels" int
"nlevels" int
"nvoxelsatlevels" array of ints
"world2eye" array of sixteen floats
"world2ndc" array of sixteen floats
"format" array of three floats: x res, y res, pixel aspect ratio

The return value is 1 if the request succeeded, and 0 if the request string is unknown.

int BkmGetOctreeRoots(BkmBrickMap brickmap, BkmBrickMapNode roots[7]);
BkmGetOctreeRoots() returns pointers to the seven octree roots for a brick map. The first octree contains data with no normal (i.e. N = (0,0,0)); the second octree contains data with +x the major orientation; the third octree contains data with -x the major orientation; etc. Some of the pointers will be NULL if the brick map contains no data with the corresponding orientation.
int BkmGetOctreeChildren(BkmBrickMapNode node, BkmBrickMapNode children[8]);
BkmGetOctreeChildren() returns pointers to the eight children of a brick map octree node. The first child node is the front lower left child (smallest x, y, z); the second child node is the front lower right child; etc. Some or all of the child nodes may be NULL. We may phase out this function in some future release of PRMan and recommend using BkmGetOctreeChildrenList() instead.
int BkmGetOctreeChildrenList(BkmBrickMap brickmap, BkmBrickMapNode node,
                          BkmBrickMapNode children[8]);
Same as BkmGetOctreeChildren() but with one additional parameter: brickmap.
int BkmGetVoxelData(BkmBrickMap brickmap, BkmBrickMapNode node,
                    int imin, int jmin, int kmin,
                    int imax, int jmax, int kmax,
                    int *nvoxels, int *voxelnumbers,
                    int *voxelhasdata, // (redundant, but useful)
                    float *voxeldata);
BkmGetVoxelData()'s input are a brick map, a brick map octree node, and the i,j,k range of voxels to search through. If imin=jmin=kmin=0 and imax=jmax=kmax=7 all voxels of the brick will be searched. nvoxels is the number of voxels with data - a number between 0 and 8*8*8 = 512. voxelnumbers should be passed an array of 512 ints. The first nvoxels values will be the numbers of the voxels that contain data. The voxel numbers are computed as 64*k + 8*j + i. voxelhasdata should also be passed an array of 512 ints. These values will be 1 if the corresponding voxel contains data, and 0 if the corresponding voxel is empty. The information provided by voxelhasdata is redundant with voxelnumbers, but both representations are convenient for different uses. voxeldata should be passed an array of (at least) datasize*512 floats. The data for non-empty voxels will be copied into the voxeldata array. (The voxeldata values corresponding to empty voxels are not changed.)
void BkmCloseBrickMapFile(BkmBrickMap brickmap);
BkmCloseBrickMapFile() closes the brick map file and free's all associated data. After this call the brickmap pointer is no longer valid.

All these API function prototypes are defined in the header file brickmap.h. Compile and link with the following library (in PRMan 14.0 and higher):

libprman.so/.dylib/.dll

Platform-specific linking instructions can be found in the RenderMan SDK documentation.

Note that this API is targeted at C++, not C.

API Example: brickmaplookup.cpp

The following program illustrates the use of the API for reading brick map data. The program reads a brick map file, determines which octree corresponds to the normal, and traverses the octree to find the voxel that corresponds to the position. If the lookup is successful it then prints the data for that voxel.

//
// brickmaplookup.cpp
// This program demonstrates how to use the brick map API to read
// data from brick map files.
//
// Input: a brick map name, position p, and normal n.
// Output: the brick map data in the voxel corresponding to p and n.
//

#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include "math.h"
#include "assert.h"

#include "brickmap.h"


#define N 8
#define TRUE 1
#define FALSE 0

typedef struct xyz {
    float x, y, z;
} xyz;

typedef struct bbox {
    struct xyz min, max;
} bbox;


//
// Determine which child octant p is in and and that octant's bounding box
//
static void
determineChild(struct bbox *bbox, struct xyz *p,
             int *child, struct bbox *childbbox) // results
{
    struct xyz midpoint;
    int ci, cj, ck;

    // Compute the midpoint of brick bbox
    midpoint.x = 0.5f * (bbox->max.x + bbox->min.x);
    midpoint.y = 0.5f * (bbox->max.y + bbox->min.y);
    midpoint.z = 0.5f * (bbox->max.z + bbox->min.z);

    // Compute which child octant point p is in: ci,cj,ck
    ci = (p->x >= midpoint.x) ? 1 : 0;
    cj = (p->y >= midpoint.y) ? 2 : 0;
    ck = (p->z >= midpoint.z) ? 4 : 0;
    *child = ci + cj + ck;
    assert(0 <= *child && *child <= 7);

    // Compute child bbox
    childbbox->min.x = ci ? midpoint.x : bbox->min.x;
    childbbox->min.y = cj ? midpoint.y : bbox->min.y;
    childbbox->min.z = ck ? midpoint.z : bbox->min.z;
    childbbox->max.x = ci ? bbox->max.x : midpoint.x;
    childbbox->max.y = cj ? bbox->max.y : midpoint.y;
    childbbox->max.z = ck ? bbox->max.z : midpoint.z;
}


//
// Determine which voxel (i,j,k) of bbox that point p is in
//
static void
determineVoxel(struct bbox *bbox, struct xyz *p,
             int *i, int *j, int *k) // results
{
    double fi, fj, fk; // double precision required for tiny voxels
    float edgelen;

    edgelen = bbox->max.x - bbox->min.x; // same for y and z since square bricks

    // Compute which voxel (i,j,k) the point p is in
    fi = (p->x - bbox->min.x) / edgelen * N;
    fj = (p->y - bbox->min.y) / edgelen * N;
    fk = (p->z - bbox->min.z) / edgelen * N;

    *i = (int)fi;
    *j = (int)fj;
    *k = (int)fk;
    assert(-1 <= *i && *i <= N);
    assert(-1 <= *j && *j <= N);
    assert(-1 <= *k && *k <= N);

    // Ugly detail: point p can sometimes be right on or outside the
    // bbox, so i, j, or k gets a value of -1 or N.  Particularly
    // common is that p is right on the bbox max value so one of the
    // indices gets the value N.  We pretend that the point was inside
    // by moving the offending index into the range [0,N-1].
    if (*i == -1) *i = 0;
    if (*i == N) *i = N-1;
    if (*j == -1) *j = 0;
    if (*j == N) *j = N-1;
    if (*k == -1) *k = 0;
    if (*k == N) *k = N-1;
    assert(0 <= *i && *i < N);
    assert(0 <= *j && *j < N);
    assert(0 <= *k && *k < N);
}


//
// Recursively lookup in the brick map
//
int
recLookup(struct xyz *p, struct xyz *n, BkmBrickMap brickmap,
        BkmBrickMapNode node, struct bbox *nodebbox,
        int datasize, float *data)
{
    BkmBrickMapNode children[8], child;
    struct bbox childbbox;
    float *voxeldata;
    int c; // child octant: [0..7]
    int i, j, k; // voxel containing point p
    int v, d;
    int nvoxels, voxelnumbers[512], voxelhasdata[512];
    int success;

    // Get the octree child node that contains p
    //BkmGetOctreeChildren(node, children); // for PRMan 14
    BkmGetOctreeChildrenList(brickmap, node, children); // recommended for PRMan 15
    determineChild(nodebbox, p, &c, &childbbox);
    child = children[c];

    // Call child node if present
    if (child) { // child node exists: recurse
        success = recLookup(p, n, brickmap, child, &childbbox,
                            datasize, data);
        if (success) return TRUE;
    }

    // We get to this point if the node is a leaf or if the child
    // lookup failed (no voxel data).  Lookup voxel in this brick.

    determineVoxel(nodebbox, p, &i, &j, &k);
    voxeldata = (float*) alloca(datasize * 512 * sizeof(float));
    BkmGetVoxelData(brickmap, node, i, j, k, i, j, k,
                    &nvoxels, voxelnumbers, voxelhasdata, voxeldata);

    if (nvoxels == 0)
        return FALSE; // failure

    v = i + j*N + k*N*N;
    assert(voxelhasdata[v]);
    for (d = 0; d < datasize; d++)
        data[d] = voxeldata[v*datasize+d];

    return TRUE; // success
}


//
// Input: a brick map name, position p, and normal n.
// Output: the brick map data in the voxel corresponding to p and n.
//
int
main(int argc, char *argv[]) {
    BkmBrickMap brickmap = NULL; // (a void pointer)
    BkmBrickMapNode roots[7], root; // (void pointers)
    struct bbox bbox;
    struct xyz point, normal;
    float absnx, absny, absnz;
    float *data;
    int nvars, datasize;
    int octree, v, d;
    int success;
    char **vartypes = NULL, **varnames = NULL; // arrays of strings
    char *filename;

    if (argc != 8) {
        fprintf(stderr, "brickmaplookup: needs 7 input: filename px py pz nx ny nz\n");
        exit(1);
    }

    // Parse input
    filename = argv[1];
    point.x = atof(argv[2]);
    point.y = atof(argv[3]);
    point.z = atof(argv[4]);
    normal.x = atof(argv[5]);
    normal.y = atof(argv[6]);
    normal.z = atof(argv[7]);
    printf("filename = '%s'; p = (%f %f %f); n = (%f %f %f)\n",
           filename, point.x, point.y, point.z, normal.x, normal.y, normal.z);

    // Open the brick map file
    brickmap = BkmOpenBrickMapFile(filename);
    if (!brickmap) {
        fprintf(stderr, "brickmaplookup error: unable to open file '%s'\n",
                filename);
        exit(1);
    }

    // Get various info about the brick map contents
    BkmGetBrickMapInfo(brickmap, "bbox", &bbox);
    BkmGetBrickMapInfo(brickmap, "nvars", &nvars);
    BkmGetBrickMapInfo(brickmap, "vartypes", &vartypes);
    BkmGetBrickMapInfo(brickmap, "varnames", &varnames);
    BkmGetBrickMapInfo(brickmap, "datasize", &datasize);

    // Print variable types and names
    printf("Variables in brick map '%s':\n", filename);
    for (v = 0; v < nvars; v++)
        printf("  var %i: %s %s\n", v, vartypes[v], varnames[v]);
    printf("  (datasize = %i)\n", datasize);

    // Get the 7 brick map octree roots and pick the one appropriate for
    // the normal
    BkmGetOctreeRoots(brickmap, roots);
    absnx = fabsf(normal.x);
    absny = fabsf(normal.y);
    absnz = fabsf(normal.z);
    if (absnx*absnx + absny*absny + absnz*absnz < 1e-6f)
        octree = 0;
    else if (absnx >= absny && absnx >= absnz) // x is dominant
        octree = (normal.x > 0.0f) ? 1 : 2;
    else if (absny >= absnx && absny >= absnz) // y is dominant
        octree = (normal.y > 0.0f) ? 3 : 4;
    else // z is dominant
        octree = (normal.z > 0.0f) ? 5 : 6;

    root = roots[octree];
    if (root == NULL) {
        printf("No data for normal (%f %f %f) ~ octree %i\n",
                normal.x, normal.y, normal.z, octree);
        exit(1);
    }

    // Do recursive lookup in brick map octree
    data = (float*) alloca(datasize * sizeof(float));
    success = recLookup(&point, &normal, brickmap, root, &bbox, datasize, data);

    if (success) {
        // Print lookup result
        printf("Data for point (%f %f %f) and normal (%f %f %f):\n",
               point.x, point.y, point.z, normal.x, normal.y, normal.z);
        for (d = 0; d < datasize; d++)
            printf("  data %i: %f\n", d, data[d]);
    } else { // failure
        printf("No data for point (%f %f %f) and normal (%f %f %f)\n",
               point.x, point.y, point.z, normal.x, normal.y, normal.z);
    }
}

Examples of the output:

>brickmaplookup irmateapot.bkm 1.5 0.5 0 0 0 0
filename = 'irmateapot.bkm'; p = (1.500000 0.500000 0.000000); n = (0.000000 0.000000 0.000000)
Variables in brick map 'irmateapot.bkm':
  var 0: point P
  var 1: normal N
  var 2: color Cs
  var 3: color Os
  (datasize = 12)
No data for normal (0.000000 0.000000 0.000000) ~ octree 0
>brickmaplookup irmateapot.bkm 1.5 0.5 0 1 0 0
filename = 'irmateapot.bkm'; p = (1.500000 0.500000 0.000000); n = (1.000000 0.000000 0.000000)
Variables in brick map 'irmateapot.bkm':
  var 0: point P
  var 1: normal N
  var 2: color Cs
  var 3: color Os
  (datasize = 12)
Data corresponding to point (1.500000 0.500000 0.000000) and normal (1.000000 0.000000 0.000000):
  data 0: 1.862042
  data 1: 0.196729
  data 2: -0.417597
  data 3: 0.935172
  data 4: 0.242909
  data 5: -0.203269
  data 6: 0.777650
  data 7: 0.704896
  data 8: 0.203238
  data 9: 1.000000
  data 10: 1.000000
  data 11: 1.000000

Note that this sample program only point samples the brick map voxels, and therefore is prone to aliasing. For smoother results, trilinear or quadrilinear lookups are recommended.

Trilinear filtering: given a filtering radius, we can simply construct a filter box centered at the lookup point and with edge length twice the filter radius. From the filter radius, we can also determine the appropriate level in the brick map: the level where the voxels are closest to the size of the filter box. When doing brick map lookups, the voxels that overlap the filter box are found, and their data weighted by the relative overlap of the filter box and the respective voxels. (This is a simple box filter; it would also be possible to improve/extend the filtering to e.g. a tent filter or Gaussian filter.)

For quadrilinear filtering, one can simply do trilinear lookups in two levels of the brick map (the two levels where the voxel sizes are most similar to the filter box size), and do a linear interpolation between the two sets of results. (This is in fact how PRMan's texture3d() function does brick map lookups.)