Light Filters

Light Filters

Light filters change the color of a light after the light has been sampled. Light filters are bound to lights like bxdfs are bound to geometry: they obey attribute scoping and the last one specified wins.

LightFilter "PxrGobo" "gobo1"  "string coordSys" ["Gobo1"] "string map" ["ratGrid.tx"] AreaLightSource "PxrArealight" "light1"

Like lights, light filters are enabled by default. They may be disabled on a particular geometric primitive using:

EnableLightFilter "light1" "gobo1" 0

This turns off the filter gobo1 bound to light light1. Later we'll show how more than one filter can be run on a single light.

Filtering

A light filter plugin implements the following method:

virtual void Filter(RixLightFilterContext const *lfCtx,
                    RtConstPointer instanceData,
                    RtInt const numSamples,
                    RtInt const * shadingCtxIndex,
                    RtVector3 const * toLight,
                    RtFloat const * dist,
                    RtFloat const * lightPdfIllum,
                    RixBXLobeWeights *contribution
                    );

Like pattern plugins, light filters are supplied a context. The RixLightFilterContext is a subset of the RixShadingContext and does not support full pattern generation (there is no EvalParam()). That capability is under consideration for future releases.

Light filtering happens after lighting services has picked which lights will be sampled for each point in a shading context. The RixLightFilterContext member variable numPts is the number of points in the underlying shading context. Light filters are called on a light-by-light basis, and not all of the points in a shading context will be getting a sample from a given light. Some may get zero samples, others may get multiple samples. For this reason, the Filter call is sample-centric: it operates over the samples generated for a given light.

The shadingCtxIndex maps a particular sample back to the point on the shading context for which it was generated.

The three lighting arrays toLight, dist, and lightPdfIllum, are numSamples in length. These are the vector to the light, the distance from the point on the shading context to the sample point on the light, and the pdf of the sample on the light.

The input/output array contribution contains the lighting contribution distributed into some number of diffuse and specular lobes. For each lobe there are numSamples entries. For instance, to modulate the lighting by a filter that passes only the red channel:

for(int j = 0; j < contribution->GetNumSpecularLobes(); ++j) {
    for(int i = 0; i < numSamples; ++i) {
       contribution->GetSpecularLobe(j)[i] *= RtColorRGB(1.0,0.0f,0.0f);
    }
}

for(int j = 0; j < contribution->GetNumDiffuseLobes(); ++j) {
    for(int i = 0; i < numSamples; ++i) {
       contribution->GetDiffuseLobe(j)[i] *= RtColorRGB(1.0,0.0f,0.0f);
    }
}

Instance Data

The optional instance data contains data generated by the plugin when it is first encountered in the RI stream. When the CreateInstanceData routine is called, the plugin has access to the parameter list of the light filter and can create arbitrary data that is stored by the renderer and supplied as the instanceData pointer during filtering. This should be uniform data that can be accessed by multiple threads simultaneously.

As an example, below is the CreateInstanceData method for PxrLightFilterCombiner. Like bxdfs, light filters can be referenced in arguments to other light filters. To run multiple light filters, call EvalParam to get pointers to the upstream light filter and it's instance. These are stored and used later during filtering.

struct myData
{
    int arrayLen;
    RixLightFilter** filters;
    RtConstPointer* instances;
};

static
void releaseInstanceData(RtPointer data)
{
    myData* md = (myData*) data;
    delete[] md->filters;
    delete[] md->instances;
    delete md;
}

int
PxrLightFilterCombiner::CreateInstanceData(RixContext &ctx,
                  char const *handle,
                  RixParameterList const *plist,
                  RixShadingPlugin::InstanceData *idata)
{
    RixSCType typ;
    bool isconnected;
    int arraylen;
    plist->GetParamInfo(k_mult, &typ, &isconnected, &arraylen);

    myData* mydata = new myData;
    mydata->arrayLen = arraylen;
    mydata->filters = new RixLightFilter* [arraylen];
    mydata->instances = new RtConstPointer [arraylen];

    for (int i=0; i<arraylen; ++i)
        plist->EvalParam(k_mult, i, &mydata->filters[i], &mydata->instances[i]);

    idata->data = (void *) mydata;
    idata->freefunc = releaseInstanceData;
    return 0;
}

Running multiple filters

Like bxdfs, light filters can be parameters to other light filters. The last filter given before an AreaLightSource call (the root filter in a tree of filters) is responsible for delegating to the filters in its parameter list. Furthermore, light filters can be disabled on a gprim basis. To respect this, it is the responsibility of the filter to call RixLightFilterContext::IsEnabled(). If the upstream filter is not enabled, don't run it. IsEnabled also returns a pointer to the upstream filter's instanceData, which should be passed to its Filter method.

void combiner::Filter(RixLightFilterContext const *lfCtx,
                  RtConstPointer instanceData,
                  RtInt const numSamples,
                  RtInt const * shadingCtxIndex,
                  RtVector3 const * toLight,
                  RtFloat const * dist,
                  RtFloat const * lightPdfIllum,
                  RixBXLobeWeights *contribution
                 )
{
  myData const * mydata = (myData const *) instanceData;

  for (int i=0; i<mydata->arrayLen; ++i) {
      RtConstPointer nextInstanceData;
      if (lfCtx->IsEnabled(mydata->instances[i], &nextInstanceData))
      {
          mydata->filters[i]->Filter(
              lfCtx, nextInstanceData,
              numSamples, shadingCtxIndex,
              toLight, dist, lightPdfIllum, contribution);
      }
  }
}