UV-Free Anisotropic Highlights

Anisotropic Reflections and Specular Highlights are widely used in visual effects and animations to simulate surfaces with directional reflectivity like brushed metals or holograms. There are some algorithms for calculating anisotropic distribution models such as Ward or Heidrich–Seide models.The limitation with Renderman shaders is that they need a parametric surface such as NURBS or Subdivisions to calculate the anisotropy of surfaces. Here I have tried to find a solution to use anisotropic shaders for polygon surfaces.


Definition

Most of surfaces in real world are not perfect circles and they have a degree of anisotropy; the reflectivity of the surface varies in different directions. Greg Ward Larson defines the anisotropic specular highlights as:

where X and Y are two perpendicular tangent vectors and αx and αy are the deviation of surface in X and Y directions respectively or simply the roughness of surface on those directions. So if we find a pair of tangent vectors, we can calculate the specular highlights. In NURBS surfaces dPdu and dPdv are suitable to be used as tangent vectors, because they are the direction of U and V respectively. But what if our surface does not have defined U and V directions, like polygon surfaces?



Every surface has a single normal vector on every point, but infinity numbers of tangent vectors. So we need to prioritize only one pair of perpendicular vectors as tangent vectors. The best solution is to define a rational "up vector" for our calculations, then define the U vector with the Right Hand Rule: if the up vector is in the direction of our thumb, U vectors are the ones turning around the up vector, like the other fingers. The up vector that I chose was the Y direction of the shaded object.
Now we can define the U tangent by calculating the cross product of the up-vector and the normal. Defining the V direction will be also easy: the cross product of normal and the U vector. Having the U and V tangent vectors we can calculate the result of the Ward’s formula by substituting them with αx and αy.

Now what if we need to rotate the specular highlights? In order to find new U and V directions, we multiply the U and V vectors (which are of course normalized vectors with unit lengths) by cosine and sine of and arbitrary angle and then add these two vectors. One of benefits of this feature is that the angle could vary from point to point, i.e. a texture map could be used to manipulate the rotation of the specular highlights.
There is a small glitch in the shader: when the normal are parallel to the up vector, the cross product becomes zero; therefore no tangent vector could be defined. This problem leads to artifacts, for instance on a simple poly plane. To solve this problem I made I made an exception for this case and defined the tangent vector exactly toward X direction of the object.


The Shader Code:

color wardAniso(normal    N;
                vector    V;
                vector    xdir;
                float    xroughness, yroughness;)
{
    float sqr (float x) { return x * x; }
    
    float xrough = xroughness, yrough = yroughness;
    
    if (xroughness < 0.0001) { xrough = 0.0001;}
    if (yroughness < 0.0001) { yrough = 0.0001;}
    
  
    float cos_theta_r = clamp(N . V, 0.0001, 1);
    vector X = xdir / xrough;
    vector Y = (N ^ xdir) / yrough;
    
    color C = 0;
    extern point P;
    illuminance(P, N, PI/2)
    {
        extern vector L;
        extern color Cl;
        float nonspec = 0;
        lightsource("__nonspecular", nonspec);
        if (nonspec < 1)
        {
            vector LN = normalize(L);
            float cos_theta_i = LN . N;
            if (cos_theta_i > 0.0)
            {
                vector H = normalize(V + LN);
                float rho = exp (-2 * (sqr(X . H) + sqr(Y . H)) / (1 + H . N))
                                / sqrt (cos_theta_i * cos_theta_r);
                C += Cl * ((1 - nonspec) * cos_theta_i * rho);
            }
        }
        
    }
    return C / (4 * xrough * yrough);
}
  
/////////////////////////////////////////////////////////////////////////////////////////////
include "/home/aseiff20/maya/projects/RMS_slim/wardAniso.h"
surface ward(    color    surfColor = color(0.5,0.5,0.5);
                color    specColor = 1;
                float    Ka = 0.2, Ks = 0.5;
                float    angle = 360;
                string    angleMap = "/home/aseiff20/mount/stuhome/vsfx755/cutter/man.tex";
                float    xrough = 1;
                string    xroughMap = ""; 
                float    yrough = 0.5;
                string    yroughMap = "";)
{
    
    normal    Nf = faceforward(normalize(N), I);
    vector    V = -normalize(I);
    vector    upvec = vector "object" (0, 1, 0);
    vector    xdir;
    float    xroughness = xrough;
    float    yroughness = yrough;
    
    if (normalize(N).upvec > 0.999)    
        {xdir = vector "object" (1, 0, 0);}
    else
        {xdir = normalize(upvec ^ N);}
    vector    ydir = normalize(N ^ xdir);
    float    ang = angle;
  
    if (angleMap != "")
    {
        float r = texture(angleMap[0]);
        float g = texture(angleMap[1]);
        float b = texture(angleMap[2]);        
        ang = (r + g + b) / 3;
    }
    
    if (xroughMap != "")
    {
        float r = texture(xroughMap[0]);
        float g = texture(xroughMap[1]);
        float b = texture(xroughMap[2]);        
        xroughness = (r + g + b) / 3;
    }
    
    if (yroughMap != "")
    {
        float r = texture(yroughMap[0]);
        float g = texture(yroughMap[1]);
        float b = texture(yroughMap[2]);        
        yroughness = (r + g + b) / 3;
    }
        
    
    vector    direction = normalize (xdir * cos(radians(ang)) + ydir * sin(radians(ang)));
    
    Oi = Os;
    Ci = Cs * ((Ka * ambient()) + (diffuse(Nf) * surfColor) + (Ks * specColor * wardAniso(Nf, V, direction, xroughness, yroughness)));
    Ci *= Oi;
}