From GPWiki
Jump to: navigation, search

Cook-Torrance

A common problem with the Phong and Blinn-Phong models discussed in the previous chapter is that they look too artificial. Whilst they are general purpose in design a number of simplifications means that a lot of the subtleties of materials cannot be represented. The model discussed throughout this chapter was published by Robert Cook and Kenneth Torrance in 1982 ([Cook&Torrance82]) – a few years after James Blinn published his modifications to Phong’s original model.

The Cook-Torrance lighting model is a general model for rough surfaces and is targeted at metals and plastics – although it is still possible to represent many other materials. The model is still split into two distinct terms – diffuse and specular – as was true of the previous model (and for those discussed in later chapters); the key difference is the computation of the specular term.

Phong’s model uses simple mathematical principles of reflection to determine how much, if any, of a specular highlight is visible. Whilst the colour of this highlight can be varied as a per-pixel input into the equation it is a constant colour from all angles regardless of the viewing direction. The Cook-Torrance model details a more complex and accurate way of computing the colour of the specular term – an approach based more on physics than pure mathematics.


The Cook-Torrance Equation

This equation is much more involved than the simpler Phong-based equations of the previous chapter. Despite this the equation can be neatly summarised as follows:

R = I \times (Normal \bullet Light) \times (Specular \times R_s + Diffuse)

R_s = \frac{Fresnel \times Roughness \times Geometric}{(Normal \bullet View) \times (Normal \bullet Light)}

As previously discussed the key differentiator in the Cook-Torrance equation is the specular term, Rs. The remainder of this section will focus on this term and specifically work through the three main components making up its numerator – Fresnel, roughness and geometric.

Modelling a rough surface might seem possible by using a high resolution per-pixel normal map (as discussed in chapter 3) but this doesn’t tend to yield the desired results. Cook-Torrance adopted the “Microfacet” method for describing the roughness of a given surface:

Diagram 5.1.png
Diagram 5.1

The microfacets are intended to be smaller than can be displayed on the screen; rather it is the combined effect across the entire surface that shows the roughness. Constants can be used to control the depth and size of these facets and thus control the perceived roughness of the surface. The implementation in this chapter sets these constants on a per-material basis but it is perfectly possible to store per-pixel roughness coefficients so as to allow for a much higher degree of variation.

Half vectors introduced as part of the Blinn-Phong model make an appearance in the Cook-Torrance model. In much the same way as with Blinn-Phong, the reflected specular energy is defined about this vector rather than the reflection vector as in the original Phong equation.

The geometric term

In the original paper ‘Geometric Attenuation’ is a product of two effects – shadowing and masking. Fundamentally they are the same effect, but one caters for incoming energy and the other for outgoing energy.

Shadowing can be referred to as ‘self shadowing’ – a term quite commonly used for several currently popular algorithms. Diagram 5.2 shows how incoming light energy can be blocked by its own, or a neighbouring, facet:

Diagram 5.2.png
Diagram 5.2

The other factor, masking, is very similar but covers the possibility that the reflected light is blocked by the facet. Diagram 5.3 illustrates this:

Diagram 5.3.png
Diagram 5.3

Given that these microfacets are supposed to be extremely small it might seem irrelevant that they block light energy, but it is a very important factor in the final image. Shadowing and masking increases with the depth of the microfacets leading to very rough surfaces being quite dull or matt in appearance.

The following equation can be used to compute the geometric term:

Geometric = min \begin{pmatrix} {1,} \\ {\frac{2 \times (Normal \bullet Half) \times (Normal \bullet View)}{View \bullet Half},} \\ {\frac{2 \times (Normal \bullet Half) \times (Normal \bullet View)}{View \bullet Half}}  \end{pmatrix}

The Roughness Term

This factor is a distribution function to determine the proportion of facets that are oriented in the same direction as the half vector. Only facets facing in this direction will contribute to reflected energy and a simple observation of modelling a rough surface is that not all of the surface will be facing in the same direction. As the surface becomes rougher it is more likely that parts of the surface will be oriented away from the half vector.

Various distributions have been experimented with for this component of the Cook-Torrance model. Many models aim to reproduce the wavelength scattering measured from specific real-world rough materials and tend to be very complicated albeit much more physically accurate.

When graphed, these distribution functions appear to be similar to the specular distribution used in the Phong model; light energy is scattered within a volume about the reflected vector. For a very smooth surface this distribution is concentrated about the reflected vector and yields a very sharp highlight, for a rough surface the distribution is much wider due to the roughness scattering light energy in many different directions. The smooth-surface case is very similar to that modelled by Phong.

Diagram 5.4.png
Diagram 5.4

Beckmann’s distribution ([Beckmann&Spizzichino63]) covers a wide range of materials of varying roughness with considerable accuracy; the main trade-off is that it is a complex distribution to evaluate and thus not so suitable for performance-intensive usage.

Roughness = \frac{1}{m^2 \times \cos^4 \alpha} \times e^{-(\frac{\tan \alpha}{m})^2}

The above equation can be converted to vector form so as to eliminate the two trigonometric functions. α is defined as being the angle between the normal vector and the half vector. This makes the equation much more suitable for implementation in a pixel shader:

\tan^2 \alpha \equiv \frac{\sin^2 \alpha}{\cos^2 \alpha}

1 \equiv \sin^2 \alpha + \cos^2 \alpha

\sin^2 \alpha \equiv 1 - \cos^2 \alpha

\tan^2 \alpha \equiv \frac{1 - \cos^2 \alpha}{\cos^2 \alpha}

-(\frac{\tan^2 \alpha}{m^2}) \equiv -(\frac{\frac{1 - \cos^2 \alpha}{\cos^2 \alpha}}{m^2}) \equiv -(\frac{1 - \cos^2 \alpha}{m^2 \times \cos^2 \alpha}) \equiv \frac{\cos^2 \alpha - 1}{m^2 \times \cos^2 \alpha}

\equiv \frac{(Normal \bullet Half)^2 - 1}{m^2 \times (Normal \bullet Half)^2}

Roughness = \frac{1}{m^2 \times (Normal \bullet Half)^4} \times e^{\frac{(Normal \bullet Half)^2 - 1}{m^2 \times (Normal \bullet Half)^2}}

It is also worth considering that this distribution could be stored in a texture as a look-up table (a method suggested in the first chapter of this section). Using <Normal•Half, m2> as a 2D texture coordinate will allow a single texture sample to replace the entire evaluation of this distribution. The following code can be used to create the look-up texture:

HRESULT CreateRoughnessLookupTexture( ID3D10Device* pDevice )
{
    HRESULT hr = S_OK;
 
    // The dimension value becomes a trade-off between
    // quality and storage requirements
    const UINT LOOKUP_DIMENSION = 512;
 
    // Describe the texture
    D3D10_TEXTURE2D_DESC texDesc;
    texDesc.ArraySize           = 1;
    texDesc.BindFlags           = D3D10_BIND_SHADER_RESOURCE;
    texDesc.CPUAccessFlags      = 0;
    texDesc.Format              = DXGI_FORMAT_R32_FLOAT;
    texDesc.Height              = LOOKUP_DIMENSION;
    texDesc.Width               = LOOKUP_DIMENSION;
    texDesc.MipLevels           = 1;
    texDesc.MiscFlags           = 0;
    texDesc.SampleDesc.Count    = 1;
    texDesc.SampleDesc.Quality  = 0;
    texDesc.Usage               = D3D10_USAGE_IMMUTABLE;
 
    // Generate the initial data
    float* fLookup = new float[ LOOKUP_DIMENSION*LOOKUP_DIMENSION ];
 
    for( UINT x = 0; x < LOOKUP_DIMENSION; ++x )
    {
        for( UINT y = 0; y < LOOKUP_DIMENSION; ++y )
        {
            // This following fragment is a direct conversion of
            // the code that appears in the HLSL shader
            float NdotH = static_cast< float >( x ) 
                        / static_cast< float >( LOOKUP_DIMENSION );
            float Roughness = static_cast< float >( y ) 
                        / static_cast< float >( LOOKUP_DIMENSION );
 
            // Convert the 0.0..1.0 ranges to be -1.0..+1.0
            NdotH *= 2.0f;
            NdotH -= 1.0f;
 
            // Evaluate a Beckmann distribution for this element
            // of the look-up table:
            float r_sq = Roughness * Roughness;
            float r_a = 1.0f / ( 4.0f * r_sq * pow( NdotH, 4 ) );
            float r_b = NdotH * NdotH - 1.0f;
            float r_c = r_sq * NdotH * NdotH;
 
            fLookup[ x + y * LOOKUP_DIMENSION ] 
                    = r_a * expf( r_b / r_c );
        }
    }
 
    D3D10_SUBRESOURCE_DATA initialData;
    initialData.pSysMem          = fLookup;
    initialData.SysMemPitch      = sizeof(float) * LOOKUP_DIMENSION;
    initialData.SysMemSlicePitch = 0;
 
    // Create the actual texture
    hr = pDevice->CreateTexture2D
                        ( 
                            &texDesc, 
                            &initialData, 
                            &g_pRoughnessLookUpTex 
                        );
    if( FAILED( hr ) )
    {
        ERR_OUT( L"Failed to create look-up texture" );
 
        SAFE_DELETE_ARRAY( fLookup );
 
        return hr;
    }
 
    // Create a view onto the texture
    ID3D10ShaderResourceView* pLookupRV = NULL;
 
    hr = pDevice->CreateShaderResourceView
                                    ( 
                                        g_pRoughnessLookUpTex, 
                                        NULL, 
                                        &pLookupRV 
                                    );
    if( FAILED( hr ) )
    {
        SAFE_RELEASE( pLookupRV );
        SAFE_RELEASE( g_pRoughnessLookUpTex );
        SAFE_DELETE_ARRAY( fLookup );
 
        return hr;
    }
 
    // Bind it to the effect variable
    ID3D10EffectShaderResourceVariable *pFXVar 
        = g_pEffect->GetVariableByName("texRoughness")->AsShaderResource( );
    if( !pFXVar->IsValid() )
    {
        SAFE_RELEASE( pLookupRV );
        SAFE_RELEASE( g_pRoughnessLookUpTex );
        SAFE_DELETE_ARRAY( fLookup );
 
        return hr;
    }
 
    pFXVar->SetResource( pLookupRV );
 
    // Clear up any intermediary resources
    SAFE_RELEASE( pLookupRV );
    SAFE_DELETE_ARRAY( fLookup );
 
    return hr;
}

James Blinn proposed the use of a standard Gaussian distribution:

Roughness = c \times e^{-(\frac{\alpha}{m^2})}

This distribution is not as physically accurate as Beckmann’s distribution but in a lot of scenarios the approximation will be perfectly acceptable. Evaluation of the Gaussian distribution is much easier than Beckmann’s thus making the approximation a compelling option for real-time use. The only potential problem of the Gaussian distribution is that it relies on an arbitrary constant that needs to be empirically determined for best results.

The Fresnel Term

An important part of the Cook-Torrance model is that the reflected specular light is not a constant colour; rather it is view-dependent. The Fresnel term, introduced in the first chapter of this section, can be used to model this characteristic.

The full Fresnel equation is overly complicated for this purpose and relies on expensive and complex evaluation. The original publication contains the full equations along with useful derivations regarding source data. However it is worth noting that any measurement used in Fresnel evaluations is dependent on numerous physical properties such as the wavelength of light and temperature. It depends on the intended usage whether modelling these characteristics is worth the additional complexity. For real-time computer graphics it is generally more suitable to use an approximation presented by Christophe Schlick ([Schlick94]).

Fresnel = F_0 + (1 - (Half \bullet View))^5 \times (1 - F_0)

In the above approximation F0 is the Fresnel reflectance as measured at normal incidence. Available Fresnel information is typically expressed in this form which makes it a convenient parameter to work with.

Schlick’s approximation is simply a nonlinear interpolation between the refraction at normal incidence and total reflection at grazing angles.

Implementation

The previous section covered the components of the Cook-Torrance lighting model. The following list summarises each of these parts in the order of which they must be evaluated:


1. Compute geometric term
Geometric = min \begin{pmatrix} {1,} \\ {\frac{2 \times (Normal \bullet Half) \times (Normal \bullet View)}{View \bullet Half},} \\ {\frac{2 \times (Normal \bullet Half) \times (Normal \bullet View)}{View \bullet Half}}  \end{pmatrix}
2. Compute roughness term using one of the following options:
2a Sample from a texture generated by the application – distribution becomes unimportant. [Texture2D look-up and roughness value required]
2b Evaluate with the Beckmann Distribution [roughness value required]
Roughness = \frac{1}{m^2 \times (Normal \bullet Half)^4} \times e^{\frac{(Normal \bullet Half)^2 - 1}{m^2 \times (Normal \bullet Half)^2}}
2c Evaluate with a Gaussian Distribution [roughness value and arbitrary constant required]
Roughness = c \times e^{-(\frac{Normal \bullet Half}{m^2})}
3 Compute the Fresnel term
3a Evaluate Christophe Schlick’s approximation [Requires index of refraction]
Fresnel = F_0 + (1 - (Half \bullet View))^5 \times (1 - F_0)
4 Evaluate Rs term of the full equation:
R_s = \frac{Fresnel \times Roughness \times Geometric}{(Normal \bullet View) \times (Normal \bullet Light)}
5 Evaluate complete equation
R = I \times (Normal \bullet Light) \times (Specular \times R_s + Diffuse)

The above series of steps when implemented in HLSL is as follows, note that this is a single HLSL function that relies on conditional compilation to split into different techniques; this is a valuable technique for code re-use and maintenance but arguably it can make the code less intuitive to read:

float4 cook_torrance
        (
            in float3 normal,
            in float3 viewer,
            in float3 light,
            uniform int roughness_mode
        )
{    
    // Compute any aliases and intermediary values
    // -------------------------------------------
    float3 half_vector = normalize( light + viewer );
    float NdotL        = saturate( dot( normal, light ) );
    float NdotH        = saturate( dot( normal, half_vector ) );
    float NdotV        = saturate( dot( normal, viewer ) );
    float VdotH        = saturate( dot( viewer, half_vector ) );
    float r_sq         = roughness_value * roughness_value;
 
 
 
    // Evaluate the geometric term
    // --------------------------------
    float geo_numerator   = 2.0f * NdotH;
    float geo_denominator = VdotH;
 
    float geo_b = (geo_numerator * NdotV ) / geo_denominator;
    float geo_c = (geo_numerator * NdotL ) / geo_denominator;
    float geo   = min( 1.0f, min( geo_b, geo_c ) );
 
 
 
    // Now evaluate the roughness term
    // -------------------------------
    float roughness;
 
    if( ROUGHNESS_LOOK_UP == roughness_mode )
    {
        // texture coordinate is:
        float2 tc = { NdotH, roughness_value };
 
        // Remap the NdotH value to be 0.0-1.0
        // instead of -1.0..+1.0
        tc.x += 1.0f;
        tc.x /= 2.0f;
 
        // look up the coefficient from the texture:
        roughness = texRoughness.Sample( sampRoughness, tc );
    }
    if( ROUGHNESS_BECKMANN == roughness_mode )
    {        
        float roughness_a = 1.0f / ( 4.0f * r_sq * pow( NdotH, 4 ) );
        float roughness_b = NdotH * NdotH - 1.0f;
        float roughness_c = r_sq * NdotH * NdotH;
 
        roughness = roughness_a * exp( roughness_b / roughness_c );
    }
    if( ROUGHNESS_GAUSSIAN == roughness_mode )
    {
        // This variable could be exposed as a variable
        // for the application to control:
        float c = 1.0f;
        float alpha = acos( dot( normal, half_vector ) );
        roughness = c * exp( -( alpha / r_sq ) );
    }
 
 
 
    // Next evaluate the Fresnel value
    // -------------------------------
    float fresnel = pow( 1.0f - VdotH, 5.0f );
    fresnel *= ( 1.0f - ref_at_norm_incidence );
    fresnel += ref_at_norm_incidence;
 
 
 
    // Put all the terms together to compute
    // the specular term in the equation
    // -------------------------------------
    float3 Rs_numerator   = ( fresnel * geo * roughness );
    float Rs_denominator  = NdotV * NdotL;
    float3 Rs             = Rs_numerator/ Rs_denominator;
 
 
 
    // Put all the parts together to generate
    // the final colour
    // --------------------------------------
    float3 final = max(0.0f, NdotL) * (cSpecular * Rs + cDiffuse);
 
 
 
    // Return the result
    // -----------------
    return float4( final, 1.0f );
}

Results

Image 5.5.png
Image 5.5

The above image shows the Beckmann distribution (top) and Gaussian distribution (bottom) with roughness values of 0.2 (left), 0.4, 0.6, 0.8 and 1.0 (right). Whilst the Gaussian form is noticeably different to the Beckmann form it is worth noting that there is an arbitrary constant controlling the Gaussian distribution. Experimentation with this coefficient can generate closer or more visually acceptable results.

Image 5.6.png
Image 5.6

The two key inputs into the Cook-Torrance model are reflectance at normal incidence and roughness. Image 5.6 serves to show their relationship and the types of results that can be achieved.

It should come as no surprise that low roughness values, implying a smooth surface, generate the shiniest results with specular highlights similar to that seen with the Phong model. Experimentation for achieving other materials is necessary, but from Image 5.6 it should be apparent that the Cook-Torrance model spans a large number of possibilities; the range shown in the image is representative but does not display the extreme values – lower or higher inputs are allowed.

The assembly code emitted by the HLSL compiler for the Cook-Torrance model yields some surprising results. In particular the Gaussian roughness distribution compiles to more instructions than the Beckmann distribution – given that the motivation behind the Gaussian distribution was its simple implementation!

Using a look-up texture shaves around 16% off the instruction count, which is a worthwhile saving provided there aren’t any problems with using an extra 1-2mb of video memory.


References

[Cook&Torrance82] “A Reflectance Model for Computer Graphics” ACM Transactions on Graphics, volume 1, number 1, January 1982 pages 7-24

[Schlick94] “An inexpensive BRDF model for physically-based rendering.” Christophe Schlick, Computer Graphics Forum, 13(3):233—246, 1994.

[Beckmann&Spizzichino63] “The scattering of electromagnetic waves from rough surfaces.” MacMillan, New York, 1963, pages 1-33 and 70-98.

Navigate to other chapters in this section:

Foundation & Theory Direct Light Sources Techniques For Dynamic Per-Pixel Lighting Phong and Blinn-Phong Cook-Torrance Oren-Nayar Strauss Ward Ashikhmin-Shirley Comparison and Summary