Conference Room: How to Create Radiance Patterns for Other Renderers

25 Jun 2014

The Conference Room is a very famous scene, originally created for and rendered by Radiance. The room does (or at least did) exist in reality and was painstakingly measured and re-created in a text editor (vi). See README file and credits in the repository, where I keep my test scenes in various formats:

scene rendered by Radiance

What's missing in the current state of my multi-exporter is the ability to re-create the 3D textures or patterns being used in the scene. If you compare with the Luxrender rendering below, you will notice that the ceiling, the red chairs, the table, the podium, and other objects in the scene use patterns to modify the color of a material based on the position of the geometry in space.

scene rendered by Luxrender

So, let's have a look at one of those patterns by using the reference manual of Radiance and trying to understand how those patterns modify existing materials.

First of all we have to patch the checked in versions of *.rad files to end up with something Radiance can render, we provide a Makefile for that:

cd ~/git/github/export_multi/04_conference_room/rad/
make apply_patches

After that we can open the file table.rad and see how the materials and patterns are applied:

# description of a small table of the conference room
# measures are in inches

void brightfunc xwood_pat
4 xgrain -s .4
1                0.6

xwood_pat plastic table_mat
5 .29 .15 .13 .005 .1

!genprism table_mat table ...

The call to genprism (see list of all Radiance programs) generates the geometry for the table. The next string (table_mat) defines the material name, and the table string names the geometry itself. In the reference manual we find the description of the plastic materials being used:

mod plastic id
5 red green blue spec rough

Plastic is a material with uncolored highlights. It is given by its RGB reflectance, its fraction of specularity, and its roughness value. The basic format of the scene description starts with a modifier (in this case xwood_pat), the type (of e.g. a material), and an identifier. Basically the plastic material color gets modified by another element, called brightfunc, which is a pre-defined pattern:

mod brightfunc id
2+ refl funcfile transform
n A1 A2 .. An

All the magic happens in the funcfile called

    Wood grain pattern:

    A1 - magnitude (0 to 1)

xgrain = woodgrain(Ring(Py,Pz));    { along x axis }
ygrain = woodgrain(Ring(Px,Pz));    { along y axis }
zgrain = woodgrain(Ring(Px,Py));    { along z axis }

woodgrain(r) = hermite(.6-A1/2,.6+A1/2,2,.5,2*tri(r,.5));

Ring(a,b) = sqrt( 25 + sq(mod(a,50)-25) + sq(mod(b,50)-25)) +
          7 * fnoise3(Px/40,Py/40,Pz/40) ;

This is like writing a shader for other renderers. There are predefined names for the position a ray has hit on the geometry we try to shade, like Px, Py, and Pz, and functions which are implemented in C, like hermite, tri, sqrt, sq, and fnoise3. A1 is a shader parameter handed in by the scene description, in this case the value is 0.6. Ring and woodgrain are entirely defined within the file, and xgrain, ygrain, and zgrain are variations, which can be chosen from via the scene description format, in our case we use xgrain.

For fun I implemented an Arnold shader which creates the same pattern, the C functions mentioned above were directly taken from the Radiance source code and are omitted here, but some code fragmets for Arnold are shown (to see how it translates from the file to Arnold's C/C++ API):

#include <ai.h>
#include <cstring>

#define XGRAIN 0
#define YGRAIN 1
#define ZGRAIN 2

static const char* enum_direction[] = {
  "xgrain", "ygrain", "zgrain", NULL


enum RadWoodpatParams

  AiParameterRGB("Kd_color", 1.0f, 1.0f, 1.0f);
  AiParameterFLT("magnitude", 1.0f);
  AiParameterEnum("direction", XGRAIN, enum_direction);
  AiParameterPNT("scale", 1.0f, 1.0f, 1.0f);
  AiParameterPNT("rotation", 0.0f, 0.0f, 0.0f);


// code from Radiance /////////////////////////////////////////////////////////


// Ring(a,b) = (sqrt(25 + sq(mod(a,50)-25) + sq(mod(b,50)-25)) + 7 *
// fnoise3(Px/40,Py/40,Pz/40));
Ring(double a, double b, double Px, double Py, double Pz) {

  double a2 = (a - floor(a/50.0) * 50.0) - 25.0;
  double b2 = (b - floor(b/50.0) * 50.0) - 25.0;
  double p[3] = { Px/40.0, Py/40.0, Pz/40.0 };
  return (sqrt(25.0 + (a2*a2) + (b2*b2)) +
          7.0 * fnoise3(p));

// woodgrain(r) = hermite(.6-A1/2,.6+A1/2,2,.5,2*tri(r,.5));
woodgrain(double r, float magnitude) {
  double A1 = (double) magnitude;
  // from
  // mod(n,d) : n - floor(n/d)*d;
  double m = (r - 0.5) - floor((r - 0.5) / 1.0)*1.0;
  // tri(n,d) : abs( d - mod(n-d,2*d) );
  double tri = (0.5 - m);
  // abs(x) : if( x, x, -x );
  if (tri < 0.0) tri = -tri;
  return hermite(0.6 - A1 / 2.0, 0.6 + A1 / 2.0, 2.0, 0.5, 2.0 * tri);

// code from Radiance /////////////////////////////////////////////////////////

  AtColor Kd_color = AiShaderEvalParamRGB(p_Kd_color);
  float magnitude = AiShaderEvalParamFlt(p_magnitude);
  const int direction = AiShaderEvalParamEnum(p_direction);
  AtPoint scale = AiShaderEvalParamPnt(p_scale);
  AtPoint rotation = AiShaderEvalParamPnt(p_rotation);
  AtPoint P = sg->P;
  AtPoint Ps = P;
  Ps.x = Ps.x / scale.x;
  Ps.y = Ps.y / scale.y;
  Ps.z = Ps.z / scale.z;
  // see (e.g. export_multi/04_conference_room/rad/
  double grain = 1.0;
  double ring = 0.0;
  switch (direction) {
  case XGRAIN:
    ring = Ring(Ps.y, Ps.z, Ps.x, Ps.y, Ps.z);
    grain = woodgrain(ring, magnitude); // magnitude = A1
  case YGRAIN:
    ring = Ring(Ps.x, Ps.z, Ps.x, Ps.y, Ps.z);
    grain = woodgrain(ring, magnitude); // magnitude = A1
  case ZGRAIN:
    ring = Ring(Ps.x, Ps.y, Ps.x, Ps.y, Ps.z);
    grain = woodgrain(ring, magnitude); // magnitude = A1
    AiMsgError("[rad_woodpat] direction has to be in [\"%s\", \"%s\", \"%s\"].",
               enum_direction[0], enum_direction[1], enum_direction[2]);
  sg->out.RGB = Kd_color * grain;

Here is the table top rendered in Radiance's interactive viewer rvu on the left and the Arnold shader on the right:

Pattern rendered in Radiance
vs. Arnold

After compiling the shader you can use kick (the Arnold executable) to get infos about the shader parameters and types, or which strings an ENUM parameters accepts:

% kick -info rad_woodpat
node:         rad_woodpat
type:         shader
output:       RGB
parameters:   6
filename:     ./

Type          Name                              Default
------------  --------------------------------  --------------------------------
RGB           Kd_color                          1, 1, 1
FLOAT         magnitude                         1
ENUM          direction                         xgrain
POINT         scale                             1, 1, 1
POINT         rotation                          0, 0, 0
STRING        name                              

% kick -info rad_woodpat.direction
node: rad_woodpat
param: direction
type: ENUM
default: xgrain
enum values: xgrain ygrain zgrain 

But how does that pattern (or the Arnold shader) get applied in an .ass (Arnold's scene description) file? Here is a snippet of an example file:

 name rad_woodpat
 Kd_color 0.289999992 0.150000006 0.129999995
 magnitude 0.6
 direction "xgrain"
 scale .4 .4 .4
 rotation 0 0 0

 name MAtable_mat
 Kd 0.995000005
 Kd_color rad_woodpat
 ##Kd_color 0.289999992 0.150000006 0.129999995
 Ks 0.00499999989
 Ks_color 1 1 1
 specular_roughness 0.100000001
 Kr 0.00499999989
 Kr_color 1 1 1

Basically, instead of specifying three RGB values for the standard shader parameter Kd_color, we link to the new shader called rad_woodpat which uses the original Kd_color values as one of the input parameters and modifies the color based on the calculated (monochromatic) pattern.

Here is another example of a Radiance pattern, based on, parts of a chair rendered in Radiance's interactive viewer rvu on the left and the Arnold shader on the right:

Chair pattern rendered in
Radiance vs. Arnold