Microfacet bsdfmodel

A common type of bsdfmodel are microfacet models. To ease the development of microfacet models, and to maximize code reuse, BBM offers a microfacet bsdfmodel specialization:

template<typename NDF, typename MaskingShadowing, typename Fresnel, auto NormalizationFactor = microfacet_n::Unnormalized, string_literal NAME = "microfacet">
struct microfacet

General microfacet BRDF model.

NDF, MaskingShadowing, and Fresnel must also meet config::matching_config

Implements: concepts::bsdfmodel

Template Parameters:
  • NDF – = microfacet normal distribution (concepts::ndf && std::default_initializable)

  • MaskingShadowing – = masking and shadowing function (concepts::maskingshadowing)

  • Fresnel – = fresnel implementation (requires concepts::fresnel)

  • NormalizationFactor – = see microfacet_n

  • NAME – = model name

Subclassed by lowmicrofacet< CONF, NormalizationFactor, NAME >

This bsdfmodel will be defined a microfacet normal distribution, a masking and shadowing implementation, a Fresnel implementation, a normalization factor, and a name. The normalization factor is provided by:

struct microfacet_n

Public Static Attributes

static constexpr literal Unnormalized = 1.0
static constexpr literal Walter = 4.0
static constexpr literal Cook = constants<double>::Pi()

The default is microfacet_n::Unnormalized. This microfacet_n::Cook corresponds to the normalization in the Cook-Torrance microfacet BRDF model, i.e., a division by PI. microfacet_n::Walter corresponds to the additional normalization by 4 as proposed by Walter et al. in Microfacet Models for Refraction through Rough Surfaces.

Fresnel

‘Fresnel` implements Fresnel surface reflectance. Each Fresnel implementation must meet bbm::concepts::fresnel:

template<typename Fresnel>
concept fresnel
#include <fresnel.h>

fresnel concept

Each fresnel implementation has:

  • concepts::has_config

  • typename parameter_type

  • static Value|Spectrum eval(const parameter_type& eta, const Value& cosTheta, Mask Mask=true)

parameter_type are the fresnel parameter types from the bbm::ior namespace (i.e., bbm::ior::ior, bbm::ior::reflectance, etc…).

All Fresnel implementations reside in the bbm::fresnel namespace. Two common Fresnel implementations are predefined:

template<typename CONF, typename PARAM = ior::ior<Value_t<CONF>>>
struct cook

Implements the Fresnel reflectance equation as proposed by Cook and Torrance [SIGGRAPH’82]: https://doi.org/10.1145/357290.357293.

Implements: concepts::fresnel

Template Parameters:
  • CONF – = bbm configuration

  • PARAM – = parameter type (ior)

Public Types

using parameter_type = PARAM

Fresnel Parameter Type.

Public Static Functions

static inline constexpr std::decay_t<parameter_type>::type eval(const parameter_type &param, const Value &cosTheta, Mask mask = true)

Evaluate Fresnel reflectance.

This method follows Cook and Torrance’s [SIGGRAPH’ 82] suggested evaluation of the Fresnel reflectance (https://doi.org/10.1145/357290.357293). The method takes either ior or reflectance (relies on auto-conversion to ior), as well as the dot product between view and halfway vector (cosTheta) as input.

Parameters:
  • param – = index of refraction

  • cosTheta – = dot product between view and halfway.

  • mask – = mask compute lines

Returns:

Fresnel reflectance with the type determined from the underlying type of the parameter.

template<typename CONF, typename PARAM = ior::reflectance<Value_t<CONF>>>
struct schlick

Implements the Fresnel reflectance equation as proposed by Schlick [Comp. Graph. Forum ‘94]: https://doi.org/10.1111%2F1467-8659.1330233.

Implements: concepts::fresnel

Template Parameters:

CONF – = bbm configuration

Public Types

using parameter_type = PARAM

Fresnel Parameter Type.

Public Static Functions

static inline constexpr std::decay_t<parameter_type>::type eval(const parameter_type &param, const Value &cosTheta, Mask mask = true)

Evaluate Fresnel reflectance.

This method follows Schlick’s approximation [Comp. Graph. Forum’ 94] of the Fresnel reflectance (https://doi.org/10.1111%2F1467-8659.1330233). The method takes either ior or reflectance (relies on auto-conversion to reflectance), as well as the dot product between view and halfway vector (cosTheta) as input.

Parameters:
  • param – = reflectance at normal incidence

  • cosTheta – = dot product between view and halfway.

  • mask – = mask compute lines

Returns:

Approximate Fresnel reflectance

However, any implementation that follows bbm::`concepts::fresnel` is valid. For example, Bagher et al. use a modified Schlick Fresnel approximation that uses a 2D reflectance type. Please refer to include/bsdfmodel/bagher.h for the implementation of the custom Fresnel method.

Microfacet Normal distribution

The microfacet normal distribution, or NDF, describes the distribution of the microfacets’ normals which in the end determine the reflectance behavior of the materials. BBM provides an interface for NDF implementations. Each NDF must meet concepts::ndf:

template<typename NDF>
concept ndf
#include <ndf.h>

ndf concept

Each ndf requires:

  • concepts::has_config

  • concepts::named

  • Value/Spectrum eval(const Vec3d& halfway, Mask mask=true) const

  • Vec3d sample(const Vec3d& view, const Vec2d& xi, Mask mask=true) const

  • Value pdf(const Vec3d& view, const Vec3d& m, Mask mask=true) const

  • Value/Spectrum G1(const Vec3d& v, const Vec3d& m, Mask mask=true) const

Note

NDFs are defined in the bbm::ndf namespace and stored in the include/ndf subdirectory.

We will demonstrate how to implement a new NDF by recreating the phong microfacet distribution (from Walter et al. in Microfacet Models for Refraction through Rough Surfaces). We start by defining the basic structure similar to a bsdfmodel:

#ifndef _BBM_PHONG_NDF_H_
#define _BBM_PHONG_NDF_H_

#include "bbm/ndf.h"

namespace bbm {
  namespace ndf {

    template<typename CONF, string_literal NAME="Phong"> requires concepts::config<CONF>
      struct phong
    {
      BBM_IMPORT_CONFIG( CONF );
      static constexpr string_literal name = NAME;

      specular_sharpness<Value> sharpness;
      BBM_ATTRIBUTES(sharpness);

      BBM_DEFAULT_CONSTRUCTOR(phong) {}
    };

  } // end ndf namespace
} // end bbm namespace

In this case, the ndf::phong implementation will feature one attribute: sharpness that we again expose via attribute reflection. Similar as with a bsdfmodel, we let BBM automatically generate a constructor.

Next we add the four required functions:

template<typename CONF, string_literal NAME="Phong"> requires concepts::config<CONF>
  struct phong
{
  BBM_IMPORT_CONFIG( CONF );
  static constexpr string_literal name = NAME;

  Value eval(const Vec3d& halfway, Mask mask=true) const;
  Vec3d sample(const Vec3d& view, const Vec2d& xi, Mask mask=true) const;
  Value pdf(const Vec3d& view, const Vec3d& m, Mask mask=true) const;
  Value G1(const Vec3d& v, const Vec3d& m, Mask mask=true) const;

  specular_sharpness<Value> sharpness;
  BBM_ATTRIBUTES(sharpness);

  BBM_DEFAULT_CONSTRUCTOR(phong) {}
};

BBM_CHECK_CONCEPT(concepts::ndf, phong<config>);

In contrast to a bsdfmodel, an ndf we opted not to support named arguments for the four methods as the signatures of the methods are short and they do not include many optional parameters. However, BBM does require named arguments for the constructor.

The eval method evaluates the NDF given a halfway vector:

Value eval(const Vec3d& halfway, Mask mask=true) const
{
  // above surface?
  mask &= (vec::z(halfway) > 0);

  // Quick exit
  if(bbm::none(mask)) return 0;

  // eval NDF
  Value normalization = (sharpness + 2) / Constants::Pi(2);
  Value D = bbm::pow( spherical::cosTheta(halfway), sharpness ) * normalization;

  // Done.
  return bbm::select(mask, D, 0);
}

The implementation is similar to that of a bsdfmodel, except that we do not need to check the light transport unit_t or component. Care must be taken, to ensure that the implementation is compatible with both packet and scalar types.

The sample method samples a new halfway vector based on two random values (passed as a Vec2d). Additionally, a view vector is also passed to support sampling methods that only consider visible microfacets. This is ignored in ndf::phong:

Vec3d sample(const Vec3d& /*view*/, const Vec2d& xi, Mask mask=true) const
{
  // check valid xi
  mask &= (xi[0] >= 0) && (xi[1] >= 0) && (xi[0] <= 1) && (xi[1] <= 1);

  // quick exit
  if(bbm::none(mask)) return 0;

  // sample microfacet normal
  Value cosTheta = bbm::pow( xi[0], 1.0 / (sharpness + 2) );
  Value sinTheta = bbm::safe_sqrt(1.0 - cosTheta*cosTheta);
  Vec2d csp = bbm::cossin( xi[1] * Constants::Pi(2) );

  // Done.
  return bbm::select(mask, vec::expand(csp*sinTheta, cosTheta), 0);
}

In the sample method, we first check if the random values xi are valid (i.e., between 0 and 1). Next we compute the sin and cos of the theta and phi angle of the sampled microfacet normal. Finally, we return the sampled vector if the mask (including validity of the random variable) is true. Note we abuse the joint computation of sin and cos with bbm::cossin which produces a Vec2d, which we subsequently expand to a Vec3d with vec::expand.

The pdf method returns the PDF corresponding to the sample method given a microfacet normal m (and the view direction). Unlike a bsdfmodel, the sample method of an ndf only returns the sampled microfacet normal, not the PDF.

Finally, the G1 method is the mono-directional shadowing and masking term parameterized by the incident/outgoing vector v and the microfacet normal m.

Masking and shadowing

The ndf’s G1 function is only models the mono-directional shadowing and masking term. Computing the bi-directional shadowing and masking implementation. Each maskingshadowing must meet bbm::concepts::maskingshadowing:

template<typename MS>
concept maskingshadowing
#include <maskingshadowing.h>

maskingshadowing concept

Each Masking-Shadowing requires:

  • concepts::has_config

  • static Value/Spectrum eval(const NDF&, const Vec3d&, const Vec3d& in, const Vec3d& out, const Vec3d& m, Mask mask=true)

Note

Maskingshadowing implementations are defined in the bbm::maskingshadowing namespace and the implementations are stored in include/maskingshadowing.

A masking and shadowing implementation is a structure with a single static method that takes the ndf, in and out directions, and microfacet normal to compute the shadowing and masking. Four masking and shadowing methods have been predefined:

template<typename CONF>
struct vgroove

Vgroove shadowing and masking.

Based on Torrance and Sparrow, 19967, “Theory for off-specular

reflection from roughened surfaces”:

https://dl.acm.org/doi/10.5555/136913.136924

template<typename CONF>
struct uncorrelated

Uncorrelated joint masking and shadowing.

Follows Eq. 98 from “Understanding the Masking-Shadowing Function in

Microfacet-Based BRDFs” [Heitz 2014]:

https://jcgt.org/published/0003/02/03/

template<typename CONF>
struct heightcorrelated

Height correlated joint masking and shadowing.

Follows Eq. 99 from “Understanding the Masking-Shadowing Function in

Microfacet-Based BRDFs” [Heitz 2014]:

https://jcgt.org/published/0003/02/03/

template<typename CONF>
struct vanginneken

Heigh correlated joint masking and shadowing following Vanginneken et al.

See also Eq. 101 from “Understanding the Masking-Shadowing Function in

Microfacet-Based BRDFs” [Heitz 2014]:

https://jcgt.org/published/0003/02/03/

Example: Cook-Torrance

As an example of a microfacet bsdfmodel, consider the Cook-Torrance microfacet BRDF model:

template<typename CONF, string_literal NAME = "CookTorrance"> requires concepts::config<CONF>
using cooktorrance = scaledmodel<microfacet<ndf::beckmann<CONF,  symmetry_v::Isotropic, false>,
                                            maskingshadowing::vgroove<CONF>,
                                            fresnel::cook<CONF>,
                                            microfacet_n::Cook,
                                            NAME>,
                                 bsdf_attr::SpecularScale>;

BBM_CHECK_CONCEPT(concepts::bsdfmodel, cooktorrance<config>);

In this case the model consists of an unnormalized (false) isotropic (symmetry_v::Isotropic) ndf::beckmann distribution, a maskingshadowing::vgroove and the fresnel::cook functions. Because an ndf is typically normalized, and thus does not contain an ‘albedo’ factor, we wrap the microfacet bsdfmodel in a scaledmodel which is a bsdfmodel by itself. scaledmodel passes through sample and pdf to the underlying models, and scales the results of eval and reflectance by an additional albedo attribute:

template<typename BSDFMODEL, bsdf_attr FLAG = bsdf_attr::Scale, string_literal NAME = BSDFMODEL::name>
struct scaledmodel : public BSDFMODEL

Scaled BSDF model.

Implements: concepts::bsdfmodel

Template Parameters:
  • BSDFMODEL – = bsdf model to scale; must be default constructible.

  • FLAG – = bsdf_attr flag of the albedo attribute

  • NAME – = model name (default copy name from BSDFMODEL)

Unnamed Group

bsdf_scale<Spectrum, FLAG> albedo

Class Attributes.

Public Functions

inline Spectrum eval(const Vec3d &in, const Vec3d &out, BsdfFlag component = bsdf_flag::All, unit_t unit = unit_t::Radiance, Mask mask = true) const

Evaluate the albedo*BSDFMODELeval.

Parameters:
  • in – = incident direction

  • out – = outgoing direction

  • component – = which reflectance component to eval

  • unit – = unit of computation (ignored)

  • mask – = masking of lanes (e.g., for Packet eval)

Returns:

Evaluation of the BSDF per spectrum.

inline Spectrum reflectance(const Vec3d &out, BsdfFlag component = bsdf_flag::All, unit_t unit = unit_t::Radiance, Mask mask = true) const

Return albedo*BSSFMODELreflectance.

Parameters:
  • out – = the outgoing direction

  • component – = which reflectance component to eval

  • unit – = unit of computation (ignored)

  • mask – = masking of lanes

Returns:

the approximate hemispherical reflectance of the BSDF for a given direction

inline BBM_DEFAULT_CONSTRUCTOR(scaledmodel)

Default constructor.

ndf::sampler

Not all NDF models have a published importance sampling formula. BBM provides a convenient numerical ndf::sampler for isotropic NDFs that constructs a cumulative distribution function of the NDF, and numerically samples this:

template<typename NDF, size_t samplesTheta = 90, size_t samplesPhi = 1, string_literal NAME = NDF::name + string_literal("_sampler")>
class sampler : public NDF

Replace an NDFs sample and pdf method with a data-driven numerical approximation.

Template Parameters:
  • NDF – = NDF for which to replace sample and pdf.

  • samplesTheta – = number of samples to take along the theta angle of the halfway vector (detault = 90).

  • samplesPhi – = number of phi angles to average theta samples over (default = 1).

  • NAME – = name of the NDF (default is NDF::name + ‘_sampler’).

For non-microfacet models, a similar numerical sampling approximation exists. bbm::ndf_sampler wraps around an existing bsdfmodel and constructs a numerical NDF by sampling the underlying bsdfmodel as if it was a microfacet model (i.e., it samples halfway vectors (in == out) over the hemisphere). For example, the He et al. bsdf model has no known importance sampling method. BBM resolves this by wrapping the bsdfmodel implementation (he_base with a placeholder diffuse importance sampler) in an ndf_sampler:

template<typename CONF, string_literal NAME="He"> requires concepts::config<CONF>
 using he = ndf_sampler<he_base<CONF, ...>, 90, 1, NAME>;

Note for clarity we omit the various template arguments passed to he_base.