BSDFs - Arnold Developer Guide
In Arnold 5.0, the BSDF API has been redesigned to support more advanced rendering algorithms, more advanced BSDFs, and more optimized implementations.
Methods
A BSDF consists of a struct to store its parameters and several methods. A new BSDF is created using AiBSDF
, which receives a method table and allocates a struct to store parameters. After the 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 asBSDF * cos(N.wi) / pdf
. The 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 be1
. pdf
, the probability density for sampling the incoming light direction withbsdf_sample
.
- RGB
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
andpdf
, matchingbsdf_eval
for the same incoming light direction.
- Sampled 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, various layers in a layered BSDF, or separate reflection and refraction components in a 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 it 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, since Arnold does not yet provide such an integrator yet, BSDFs are not yet required to provide implementations compatible with these rendering methods. The reverse_pdf
member of lobe samples is a placeholder and is ignored currently.
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", 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;
}