RSL
Color By Collision


return to main index

Links:
    RfM: HyperShade ColorByCollision Node tutorial



Introduction

This tutorial demonstrates how to change the color of an object based on its proximity to another surface - figure 1. It also presents a solution to the more challenging problem of how to ensure the color change, caused by a collision with another object, "sticks" to an object after it has moved away from the surface that caused the collision - figure 2.



Figure 1 - coloration by proximity to ground plane



Figure 2 - "sticky" coloration


Coloration by Proximity

The ColorByProximity shader, listing 1, uses the gather() function to trace rays, more or less, in the direction of the surface normal. If any ray hits a surface and the distance is less than "collision_dist" the surface is tinted red. For simplicity the shader does not perform any lighting calculations.


Listing 1 - ColorByProximity.sl


surface
ColorByProximity(float  collision_dist = 0.25;
                 float  sample_angle = 2;
                 float  samples = 4)
{
color    surfcolor = Cs;
float    blur = radians(sample_angle);
float    len = 1000, collision = 0;
  
gather("illuminance", P, N, blur, samples, "ray:length", len) {
    if(len <= collision_dist)
        collision = 1;
    }
if(collision)
    surfcolor = color(1,0,0);
Oi = Os;
Ci = Oi * surfcolor;
}


If the intention is to only test distances from, say, the sphere to the floor plane then using gather() in this very simple way does not avoid the spheres effecting and colorizing each other. For example, note the interaction of the two spheres on the right of figure 3.



Figure 3


In the case of concave objects the rays traced from one part of an object may hit another part of the same object - figure 4.



Figure 4
Sphere effected by the floor plane AND by itself.


Figure 5
After checking the "ids" the sphere does not effect itself.


In rib files generated by Pixar's RenderMan Studio each surface in a scene is "tagged" with a custom "id" attribute. For example,

    Attribute "identifier" "float id" [4]
    ReadArchive "dented_sphere.rib"

To avoid erroneous self ray-hits (figure 5) a shader can compare the "id" of the micro-polygon being shaded with the surface "id" hit by a ray traced by the gather() function - listing 2.


Listing 2 - ColorByProximity.sl (version 2)


surface
ColorByProximity(float  collision_dist = 0.25;
                 float  sample_angle = 2;
                 float  samples = 4)
{
color    surfcolor = Cs;
float    blur = radians(sample_angle);
float    len = 1000, collision = 0;
float    selfID = 0;
  
// Query the "id" of the current surface
attribute("identifier:id", selfID);
  
float hitID = 0;
gather("illuminance", P, N, blur, samples, "ray:length", len,
                      // query the "id" of the hit surface
                      "attribute:identifier:id", hitID) {
                    
    // Compare the ids and ignore collision if necessary. 
    if(len <= collision_dist && selfID != hitID)
        collision = 1;
    }
if(collision)
    surfcolor = color(1,0,0);
Oi = Os;
Ci = Oi * surfcolor;
}


Collisions & Retained Color

To shade objects so that they uniformly, and persistently, change color as a result of collisions, figure 6, a shader must retain ("remember") the results of proximity tests that it previously performed during an animation. Another requirement of a collision shader is that it must be able, as a single instance, to shade an arbitrary number of objects according to each ones retained collision "status".



Figure 6


Thanks for the Memory

The technique used by the collision shader in listing 3 relies on the use of a pointcloud as an accumulation buffer. Refer to the tutorial,
    RSL: Point Clouds as Accumulation Buffers
for background information about the writing and reading of data to a pointcloud. In effect, a pointcloud is used as a simple memory device that stores information that indicates whether an object has been in close proximity to another object aka. collision.


All for One and One for All

Ideally, each of the mirco-polygons of an object should be able to read and write a collision value to a single shared location in a pointcloud. Unfortunately, micro-polygona must write data to different locations. To overcome this limitation the ColorByCollision shader writes a micro-polygons collision data to a location within a cluster of spatially close randomized points. Each object to which the shader is assigned has its own specific cluster of locations.

With the exception of frame 0 the shader only writes data (a value of 1.0) when a micro-polygon detects another surface is within a proximity threshold. As each micro-polygon is shaded it looks up a collision value within the cluster of pointcloud locations. If the look-up yields a value of 1.0 the micro-polygon is immediately assigned a color that signifies "collision". Otherwise, the gather() function is used to determine if the object is colliding with another surface, in which case the micro-polygon receives the collision color AND the bake3d() function writes a value of 1.0, within the cluster of locations in the pointcloud. If no collision is detected the pointcloud does not receive a value.

Each object is given a unique central location for its very small cluster of pointcloud locations based on the "id" attribute it has been assigned by RenderMan Studio. By keeping the cluster of locations extremely close to each other the texture3d() function is tricked into treating them as a single location. The technique is probably skating on thin RSL ice but it appears to work quite well. However, errors can occur if an object moves "out-of-frame" and then subsequently moves "back-in-frame" during an animation. This defect is shown in - figure 7.



Figure 7
Notice the color error of the sphere on the right.


Listing 3 - ColorByCollision.sl


surface
ColorByCollision(float  collisionDist = 1;
                 float  sampleAngle = 5;
                 float  samples = 8;
                string  ptcName = "";
                string  dataChannel = "collisions")
{
uniform float    frameNum = 0;
uniform float    id = 0;
option("Frame", frameNum);
attribute("identifier:id", id);
  
// Create a point based on the "id" attribute.
point    fakeP = point(id + random() * 0.00001, 0, 0);
normal   fakeN = normal(0,0,0);
float    collision = 0;
color    surfcolor = color(0.7,0.5,0.3);
    
// Initialize the data to 0.
if(ptcName != "" && frameNum == 0) {
    bake3d(ptcName, dataChannel, fakeP, fakeN, dataChannel, 0.0);
    }
// Check the pointcloud.
else if(ptcName != "") {
    if(texture3d(ptcName, fakeP, fakeN, dataChannel, collision) == 0) {
        collision = 0;
        }
    }
// No need to use gather() when "collision" is 1.
if(collision != 1) {
    float    len = 1000000;
    gather("illuminance", P, N, radians(sampleAngle), samples, "ray:length", len) {
        if(len <= collisionDist)
            collision = 1;
        }
    }
// Assign the collision color and write the value to the pointcloud.
if(collision == 1) {
    surfcolor = color(1,0,0);
    if(ptcName != "")
        bake3d(ptcName, dataChannel, fakeP, fakeN, dataChannel, 1);
    }
Oi = Os;
Ci = Oi * Cs * surfcolor;
}


Preparation and Use of a Header File

Having tested the shader its "core" code can be wrapped in a RSL function and saved to a header (.h) file. For example, listing 4 implementes a function named ColorByCollision saved in a header file named Collision.h. The header file is referenced by the simplied version of the original shader - listing 5. The principle difference between the shader code and the function code is that the inputs (parameters) of a shader always have default values. Some extra code, shown in bold, has been added to the function. The extra code ensures the function only performs calculations for "camera" rays.

The comments by the side of each function argument are there so that Cutter can be used to convert the function into a custom hypershade node description. For more information on the topic of function-to-node conversion refer to Cutter: HyperShade Node Scripts. Another tutorial, RfM: HyperShade ColorByCollision Node uses listing 4 to generate a custom hypershade node that can be used with Pixar's sophisticated physically plausible materials.


Listing 4 - Collision.h


void ColorByCollision(float   dist;          /* [label "Collision Distance" default 0.25] */
                      float   samples;       /* [label "Samples" default 5] */
                      float   samplingAngle; /* [label "Sample Angle" default 2] */
                      string  ptcName;       /* [label "Ptc Path"] */
                      string  dataChannel;   /* [label "Data Channel Name" default "collisions"] */
                      color   preCollision;  /* [label "Pre Collision Color" default ".7 .5 .3"] */
                      color   postCollision; /* [label "Post Collision Color" default "1 .0  0"] */
        output varying color resultC;
        output varying float resultF;)
{
resultC = preCollision;
resultF = 0;
 
// We only shade when hit by a "camera" ray
float    raydepth = 2;
rayinfo("depth", raydepth);
if(raydepth > 0)
    return;
  
uniform float   frameNum = 0;
uniform float   id = 0;
option("Frame", frameNum);
attribute("identifier:id", id);
  
point    fakeP = point(id + random() * 0.00001, 0, 0);
normal   fakeN = normal(0,0,0);
float   collision = 0;
  
if(ptcName != "" && frameNum == 0) {
    bake3d(ptcName, dataChannel, fakeP, fakeN, dataChannel, 0.0);
    }
else if(ptcName != "") {
    if(texture3d(ptcName, fakeP, fakeN, dataChannel, collision) == 0) {
        collision = 0;
        }
    }
if(collision != 1) {
    float    len = 1000000;
    gather("illuminance", P, N, radians(samplingAngle), samples, "ray:length", len) {
        if(len <= dist)
            collision = 1;
        }
    }
if(collision == 1) {
    resultC = postCollision;
    resultF = 1;
    if(ptcName != "")
        bake3d(ptcName, dataChannel, fakeP, fakeN, dataChannel, 1);
    }
}


Listing 5 - ColorByCollision.sl (version 2)


#include "Collision.h"
  
surface
ColorByCollision(float   collisionDist = 1;
                 float   samples = 8;
                 float   sampleAngle = 5;
                 string  ptcName = "";
                 string  dataChannel = "collisions";
                 color   preCollision = color(1,0.7,0.4);
                 color   postCollision = color(1,0,0))
{
color surfcolor;
float mask; // not used
  
ColorByCollision(collisionDist,samples,sampleAngle,
                ptcName,dataChannel,
                preCollision,postCollision,surfcolor,mask);
 
Oi = Os;
Ci = Oi * Cs * surfcolor;
}






© 2002- Malcolm Kesson. All rights reserved.