In Arnold 5.0, the BSDF API has been redesigned to support more advanced rendering algorithms, more advanced BSDFs, and more optimized implementations.

Diffuse Example

Here is a simple diffuse BSDF example, with a shader that uses the BSDF to integrate direct and indirect light. 

diffuse_bsdf.cpp
#include "diffuse_bsdf.h"


struct DiffuseBSDF
{
   /* parameters */
   AtVector N;
   /* set in bsdf_init */
   AtVector Ng, Ns;
};

AI_BSDF_EXPORT_METHODS(DiffuseBSDFMtd);

bsdf_init
{
   DiffuseBSDF *data = (DiffuseBSDF*)AiBSDFGetData(bsdf);

   // store forward facing smooth normal for bump shadowing
   data->Ns = (sg->Ngf == sg->Ng) ? sg->Ns : -sg->Ns;

   // store geometric normal to clip samples below the surface
   data->Ng = sg->Ngf;

   // initialize the BSDF lobes. in this case we just have a single
   // diffuse lobe with no specific flags or label
   static const AtBSDFLobeInfo lobe_info[1] = {{AI_RAY_DIFFUSE_REFLECT, 0, AtString()}};
   AiBSDFInitLobes(bsdf, lobe_info, 1);

   // specify that we will only reflect light in the hemisphere around N
   AiBSDFInitNormal(bsdf, data->N, true);
}

bsdf_sample
{
   DiffuseBSDF *data = (DiffuseBSDF*)AiBSDFGetData(bsdf);

   // sample cosine weighted incoming light direction
   AtVector U, V;
   AiV3BuildLocalFrame(U, V, data->N);
   float sin_theta = sqrtf(rnd.x);
   float phi = 2 * AI_PI * rnd.y;
   float cosNI = sqrtf(1 - rnd.x);
   AtVector wi = sin_theta * cosf(phi) * U +
                 sin_theta * sinf(phi) * V +
                 cosNI * data->N;

   // discard rays below the hemisphere
   if (!(AiV3Dot(wi, data->Ng) > 0))
      return AI_BSDF_LOBE_MASK_NONE;

   // since we have perfect importance sampling, the weight (BRDF / pdf) is 1
   // except for the bump shadowing, which is used to avoid artifacts when the
   // shading normal differs significantly from the smooth surface normal
   const float weight = AiBSDFBumpShadow(data->Ns, data->N, wi);

   // pdf for cosine weighted importance sampling
   const float pdf = cosNI * AI_ONEOVERPI;

   // return output direction vectors, we don't compute differentials here
   out_wi = AtVectorDv(wi);

   // specify that we sampled the first (and only) lobe
   out_lobe_index = 0;

   // return weight and pdf
   out_lobes[0] = AtBSDFLobeSample(AtRGB(weight), 0.0f, pdf);

   // indicate that we have valid lobe samples for all the requested lobes,
   // which is just one lobe in this case
   return lobe_mask;
}

bsdf_eval
{
   DiffuseBSDF *data = (DiffuseBSDF*)AiBSDFGetData(bsdf);

   // discard rays below the hemisphere
   const float cosNI = AiV3Dot(data->N, wi);
   if (cosNI <= 0.f)
      return AI_BSDF_LOBE_MASK_NONE;

   // return weight and pdf, same as in bsdf_sample
   const float weight = AiBSDFBumpShadow(data->Ns, data->N, wi);
   const float pdf = cosNI * AI_ONEOVERPI;
   out_lobes[0] = AtBSDFLobeSample(AtRGB(weight), 0.0f, pdf);

   return lobe_mask;
}

AtBSDF* DiffuseBSDFCreate(const AtShaderGlobals* sg, const AtRGB& weight, const AtVector& N)
{
   AtBSDF* bsdf = AiBSDF(sg, weight, DiffuseBSDFMtd, sizeof(DiffuseBSDF));
   DiffuseBSDF* data = (DiffuseBSDF*)AiBSDFGetData(bsdf);
   data->N = N;
   return bsdf;
}
diffuse_bsdf.h
#pragma once

#include <ai_shader_bsdf.h>
#include <ai_shaderglobals.h>

AtBSDF* DiffuseBSDFCreate(const AtShaderGlobals* sg, const AtRGB& weight, const AtVector& N);
diffuse_shader.cpp
#include "diffuse_bsdf.h"

#include <ai.h>

AI_SHADER_NODE_EXPORT_METHODS(DiffuseMtd)

enum DiffuseParams {
   p_color,
};

node_parameters
{
   AiParameterRGB("color", 0.8f, 0.8f, 0.8f);
}

node_initialize
{
}

node_update
{
}

node_finish
{
}

shader_evaluate
{
   // early out for shadow rays and black color
   if (sg->Rt & AI_RAY_SHADOW)
      return;

   AtRGB color = AiShaderEvalParamRGB(p_color);
   if (AiColorIsSmall(color))
      return;

   sg->out.CLOSURE() = DiffuseBSDFCreate(sg, color, sg->Nf);
}

node_loader
{
   if (i>0)
      return false;

   node->methods      = DiffuseMtd;
   node->output_type  = AI_TYPE_CLOSURE;
   node->name         = "diffuse";
   node->node_type    = AI_NODE_SHADER;
   strcpy(node->version, AI_VERSION);
   return true;
}

Methods

A BSDF consists of a struct to store its parameters, and a number of methods. A new BSDF is created using AiBSDF, which receives a method table and allocates a struct to store parameters. After shader evaluation is finished, the integrator will initialize, evaluate and sample the BSDF.

  • bsdf_init: Called before the BSDF is evaluated or sampled for the first time. BSDF initialization can include: ensuring that the provided parameters are within a valid range, storing local geometry data for later evaluation and sampling (geometric normal, outgoing view direction, ...), and precomputing any data needed for evaluation and sampling. Here the BSDF must also provide information about its lobes, and optionally provide bounds for more efficient light culling.
  • bsdf_eval: Evaluates the BSDF for a given incoming light direction and the current outgoing view direction. If the BSDF consists of multiple lobes, lobe_mask describes which lobes must be evaluated. The result of this evaluation for each lobe is:
    •  RGB weight, defined as BSDF * cos(N.wi) / pdfThe cosine of the angle between the surface normal and the incoming light direction must be included, and the weight is divided by the probability density. For a BSDF that provides perfect importance sampling, this weight would be 1.
    • pdf, the probability density for sampling the the incoming light direction with bsdf_sample.
  • bsdf_sample: Sample an incoming light direction and evaluate the BSDF for this direction. This function returns:
    • Sampled incoming light direction wi.
    • Index of the lobe that was sampled.
    • RGB weight and pdf, matching bsdf_eval for the same incoming light direction.
  • bsdf_interior: Optionally return a list of volume closures to fill the interior of the volume. The typical example would be a glass BSDF returning a volume absorption closure.

Lobes

BSDF can consist of multiple lobes, for example multiple layers in a layered BSDF, or separate reflection and refraction components in glass BSDF. Each lobe has an associated ray type and AOV name. This makes it possible to:

  • Output each lobe to a separate AOV.
  • Independently control the diffuse, glossy and refraction depth and number of samples.
  • Let Arnold do more efficient sampling by separating lobes with distinct shapes.

In BSDF initialization the BSDF specifies the number of lobes it consists of, and provides an array with the ray type and AOV name for each.

The evaluation and sample methods then receive a lobe bitmask to indicate which lobes to evaluate or sample respectively. If the sample method receives a lobe mask with multiple lobes, it is up to the BSDF to pick one of the lobes to sample, ideally importance sampling based on how much each lobe contributes.

These methods output an array of lobe samples, and return a lobe bitmask to indicate which lobe samples were filled in. This may be a subset or superset of the lobes specified with the input lobe mask.

Bounds

If all reflected incoming light on the BSDF is contained in a hemisphere, it is possible to specify the normal of that hemisphere. Providing this information is not strictly required, but can help speed up rendering by letting Arnold more quickly discard light that is outside the bounds of the BSDF.

Bump Mapping

BSDF may use normals different than the smooth surface normal Ns, using bump or normal mapping. If this modified normal is very different than the smooth surface normal, artifacts may appear however. An AiBSDFBumpShadow utility function is provided to hide such artifacts by adding extra shadowing as part of BSDF evaluation.

This function takes as input the forward facing smooth normal, the bump mapped normal, and the incoming light direction, and outputs a factor to multiply with the BSDF weight.

Ray Differentials

Ray directions returned by bsdf_sample include ray differentials. For good texture filtering performance and quality it is important to provide ray differentials. The AiReflectWithDerivs and AiRefractWithDerivs utility functions can be used to compute reflected or refracted vectors with derivatives.

Example for a simple perfectly sharp specular BSDF:

AtVectorDv I = AtVectorDv(sg->Rd, sg->dDdx, sg->dDdy);
AtVectorDv N = AtVectorDv(sg->Nf, sg->dNdx, sg->dNdy);
AtVectorDv R = AiReflectWithDerivs(I, N);

Roughness Clamping

Unidirectional path tracers can't resolve caustics efficiently. Sharp specular bounces seen through a rough specular or diffuse bounce are too noisy without any compensation. Built-in BSDFs automatically increase their roughness to reduce caustic noise at the cost of bias. options.indirect_glossy_blur controls the amount of blurring, with 0 resulting in unbiased renders.

Custom BSDFs can use the same method, by clamping their roughness with the automatically estimated minimum roughness provided by the AiBSDFMinRoughness function.

bsdf_init
{
   ...
   // clamp roughness based on path history
   data->roughness = AiMax(AiBSDFMinRoughness(sg), data->roughness);
   ...
}

Exit Colors

Glass shaders often require many bounces to escape. If the number of bounces is limited, this leads to dark patches. Each BSDF lobe has a flag that can be set to use the background color or a fixed white color when the number of bounces has been exceeded.

Bidirectional Rendering

The API is designed to work with bidirectional rendering algorithms. However as Arnold does not provide such an integrator yet, BSDFs are not yet required to provide implementations compatible with such rendering methods. The reverse_pdf member of lobe samples is a placeholder and ignored currently.

 

  • No labels