Writing Normal Map Shaders

Normal Map shaders produce Vec3f vector values and can be chained with other normal map shaders. They are very similar to regular Map shaders except rather than having a sample call that produces a color they have a sampleNormal call that produces a vector.

Each Normal Map shader implements two static functions to support MoonRay’s scalar and vector execution modes. The function prototypes are defined in scene_rdl2’s Types.h and they are SampleNormalFunc and SampleNormalFuncv.

Normal Map shaders inherit two protected function pointer members (mSampleNormalFunc and mSampleNormalFuncv) which they must set, typically in the constructor.

There are typically 4 files that make up a shader’s source:

  • <ClassName>.cc
  • <ClassName>.ispc
  • <ClassName>.json
  • CMakeLists.txt

The .cc file is written in C++ and contains the class definition, the constructor/destructor, the update() function, and the static scalar SampleNormalFunc implementation. The .ispc file is written in ISPC and contains the vector SampleNormalFuncv implementation. It is also common for the .ispc source to contain any data structures needed by the shader during rendering. Attributes are declared in the .json file via JSON.

The sampleNormal() function

A Normal Map shader implements two different sampleNormal functions that can be called by the renderer depending on the execution mode. The sampleNormal functions are responsible for generating the vector values that are then provided to the client shaders. The sampleNormal function is called for every shade point and can therefore be executed several millions of times in a single render which should be kept in mind when writing these functions.

  • SampleNormalFunc - (implemented in C++ language)
  • SampleNormalFuncv - (implemented in ISPC language)

The update() function

The update() method is called before rendering begins and anytime the shader’s attributes or bindings are modified. Because the above-mentioned sampleNormal functions are going to be potentially called millions of times, a shader writer should strive to use the update() method whenever possible to do any heap allocations or potentially expensive operations that do not depend on varying values/state. The results of these operations can then be stored as class members and later retrieved during sampling. It is fairly easy to cause major performance issues by doing something which would not be a big concern in “normal” code, but because it is happening millions of times across many threads it causes a bottleneck. Such “expensive operations” that should be kept in the update() function include: memory allocation/deallocation such as declaring a string, something that causes threads to lock such as querying or updating a container, and the construction of non-trivial types.

Here’s a simplified Normal Map shader that produces a normal vector from an Rgb color input.

// RgbToNormalMap.cc

#include "attributes.cc"                  // included at the top of every Displacement shader
#include "RgbToNormalMap_ispc_stubs.h"    // this is generated by the build system from the ispc source

#include <moonray/rendering/shading/MapApi.h>

using namespace scene_rdl2::math;

RDL2_DSO_CLASS_BEGIN(RgbToNormalMap, NormalMap)

public:
    RgbToNormalMap(const SceneClass& sceneClass,
                   const std::string& name);
    ~RgbToNormalMap();            
                   
    // inherited from SceneObject
    virtual void update();

private:
    // scalar implementation (SampleNormalFunc)
    static void sampleNormal(const NormalMap* self,
                             moonray::shading::TLState* tls,
                             const moonray::shading::State& state,
                             Vec3f* sample);

RDL2_DSO_CLASS_END(RgbToNormalMap)


RgbToNormalMap::RgbToNormalMap(const SceneClass& sceneClass,
                               const std::string& name)
    : Parent(sceneClass, name)
{
    // Here, we set the two inherited function pointers to point
    // to our scalar and vector implementations
    mSampleNormalFunc = RgbToNormalMap::sampleNormal;
    mSampleNormalFuncv = (SampleNormalFuncv)ispc::RgbToNormalMap_getSampleFunc();
}

RgbToNormalMap::~RgbToNormalMap()
{
}

void
RgbToNormalMap::update()
{
}

void
RgbToNormalMap::sampleNormal(const NormalMap* self,
                             moonray::shading::TLState* tls,
                             const moonray::shading::State& state,
                             Vec3f* sample)
{
    const RgbToNormalMap* me = static_cast<const RgbToNormalMap*>(self);
    const Color input = evalColor(me, attrInput, tls, state);
    Vec3f result(input.r, input.g, input.b);
    *sample = result;
}
// RgbToNormalMap.ispc

#include "attributes.isph"

#include <moonray/rendering/shading/ispc/MapApi.isph>

static Vec3f
sampleNormal(const uniform NormalMap* uniform map,
             uniform ShadingTLState* uniform tls,
             const varying State& state)
{
    const varying Color input = evalAttrInput(map, tls, state);
    varying Vec3f result = Vec3f_ctor(input.r, input.g, input.b);
    return result;
}

DEFINE_NORMALMAP_SHADER(RgbToNormalMap, sampleNormal)

RgbToNormalMap.json

{
    "name": "RgbToNormalMap",
    "type": "NormalMap",
    "attributes": {
        "attrInput": {
            "name": "input",
            "type": "Rgb",
            "default": "Rgb(1.0f, 1.0f, 1.0f)",
            "comment": "Input color to convert to a normal map",
            "flags": "FLAGS_BINDABLE"
        }
    }
}