BBM has been designed with BSDF fitting in mind. BSDF fitting is the process where the parameters of a BSDF model is optimized such that some error metric between the fitted model and a reference model are minimized.

Basic Usage

Consider the following example that fits a Cook-Torrance BSDF model to the MERL-MIT measured Alum-Bronze:

#include <iostream>
#include "bbm.h"
using namespace bbm;

#include "optimizer/compass.h"
#include "loss/cosine_weighted_log.h"

int main(int argc, char** argv)
{
  BBM_IMPORT_CONFIG( floatRGB );

  size_t maxItr = 100;

  // create model to fit and reference to fit to.
  auto fitted = aggregate( lambertian<Config>(), cooktorrance<Config>() );
  merl<Config> reference("alum-bronze.binary");

  // Use a standard Log loss
  standardLog loss(fitted, reference, {90, 30}, {1, 9});
  auto param = parameter_values(fitted);

  std::cout << "Initial param: " << param << std::endl;
  std::cout << "Initial loss : " << loss() << std::endl;

  // Use the bsdfmodel defined box constraints on the parameters
  auto low = parameter_lower_bound(fitted);
  auto up = parameter_upper_bound(fitted);

  std::cout << "Lower: " << low << std::endl;
  std::cout << "Upper: " << up << std::endl;

  // Use a compass search for optimization
  compass opt(loss, param, low, up);

  // optimize for 100 steps, or until convergence
  for(size_t t=0; t < maxItr && !bbm::all(opt.is_converged()); ++t)
  {
    auto err = opt.step();
    std::cout << err << ": " << param << ", " << opt.is_converged() << std::endl;
  }

  return 0;
}

This example demonstrates the three key components in BSDF fitting in BBM:

  1. the ability to enumerate the BSDF parameters in a vector-like structure using the parameter_values method, as well as the ability to query the upper and lower limits of the parameters. The enumerated parameter values have been decoupled from the BSDF model making generic interfacing with optimization algorithms easier.

  2. Decoupling of the fitting from the optimization algorithm. Many non-linear optimization implementations assume a L2 error metric. Decoupling the loss from the optimization procedure allows for experimentation.

  3. Separation of the optimization algorithm from the BSDF fitting. The above code employs a compass search. However, future developments can include other algorithms such as Adam.

Parameter Enumeration

Parameters can be enumerated by:

template<typename MODEL>
inline auto bbm::parameter_values(MODEL &&model, bsdf_attr flag = bsdf_attr::All)

Enumerate the parameters of a BSDF model in a vector.

Parameters:
  • model – = bsdf model to extract parameters from

  • flag – = bsdf_attr flag to select a subset of the parameters

Returns:

vector of references to the bsdf parameter values

Note a version of parameter_values exists for both bsdfmodel and bsdf. By changing the flags, a subset of the parameters can be enumerated, for example to separate the fitting of albedo and the non-albedo parameters:

enum class bbm::bsdf_attr

Attribute Property Flags.

Values:

enumerator None
enumerator DiffuseScale
enumerator DiffuseParameter
enumerator SpecularScale
enumerator SpecularParameter
enumerator Dependent
enumerator Diffuse
enumerator Specular
enumerator Scale
enumerator Parameter
enumerator All

The ‘’DiffuseScale’’ and SpecularScale refer to albedo parameters. E.g., the albedo added by wrapping a bsdfmodel in a scaledmodel is a scale. Scalar = DiffuseScale | SpecularScale. The DiffuseParameter and SpecularParameter represent all non-albedo parameters, with Parameter = DiffuseParameter | SpecularParameter. Diffuse and Specular is the union of the corresponding scale and parameter. Finally, a parameter marked as Dependent is not directly optimizable; it is assumed that its value dependents directly or indirectly on the values of other parameters.

A similar interface exists for enumerating parameter_default_values, parameter_lower_bound, and parameter_upper_bound.

Fitting Metric

Similar to the relation between bsdfmodel and bsdf, BBM splits a fitting metric in a lossfunction and a loss (with corresponding loss_base and loss_ptr). The latter wraps a lossfunction and provides an interface with virtual function calls.

A lossfunction must meet bbm::concepts::lossfunction:

template<typename LOSSFUNC>
concept lossfunction
#include <lossfunction.h>

loss function concept

Each loss function requires:

  • concepts::has_config

  • update(void) method that initializes the loss. This method should be called at the beginning of each optimization step()

  • Value_t operator()(Mask=true) const method that returns the loss.

  • The returned loss should support addition.

The loss function constructor should precompute any values that do not change during the whole optimization process. The init method should precompute any values that remain constant within an optimization step. Finally, operator() should return the loss. Note that init is only called once per optimization step, whereas operator() might be called multiple times. A (fictional) example of how this could be used: during construction the loss might precompute cosines of angles that are evaluated as part of the loss; the init function could preselect a batch of directions to compute the loss over; and the operator() might sum the differences over the batch and scale it by the precomputed cosine.

Many fitting metrics compute a loss over a number of sampled (in,out) directions. So simplify creation, BBM introduces a sampledlossfunction (which also meets bbm::concepts::sampledlossfunction):

template<typename BSDF, typename REFERENCE, typename SAMPLELOSSFUNC, typename LINEARIZER, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct sampledlossfunction

sampledlossfunction

Computes the loss over BSDFs sampled by a linearizer.

Satisfied: concepts::sampledlossfunction

Subclassed by bieronL2< BSDF, REFERENCE, COMPONENT, UNIT >, bieronLog< BSDF, REFERENCE, COMPONENT, UNIT >, lowL2< BSDF, REFERENCE, COMPONENT, UNIT >, lowLog< BSDF, REFERENCE, COMPONENT, UNIT >, nganL2< BSDF, REFERENCE, COMPONENT, UNIT >, standardLog< BSDF, REFERENCE, COMPONENT, UNIT >

Public Functions

BBM_IMPORT_CONFIG(BSDF)
inline sampledlossfunction(const BSDF &bsdf, const REFERENCE &reference, const SAMPLELOSSFUNC &samplelossfunc, const LINEARIZER &linearizer)

Constructor.

Parameters:
  • bsdf – = bsdf model to be optimized

  • reference – = goal bsdf model

  • samplelossfunc – = loss over a single loss sample

  • linearizer – = linearizer to sample the bsdf models.

inline void update(void)

Init (does nothing)

inline Size_t samples(void) const

Returns the number of samples.

inline Value operator()(Size_t idx, Mask mask = true) const

Compute the loss over the idx-th sample.

inline Value operator()(Mask mask = true) const

Compute loss over all samples.

Sampled Loss Function

The behavior of a sampledlossfunction is determined by a sample loss function that computes the loss over a single sample and which must meet bbm::concepts::samplelossfunction:

template<typename SAMPLELOSS>
concept samplelossfunction
#include <samplelossfunction.h>

A number of sampled loss functions are predefined, that will be used in conjunction with a linearizer to create different loss functions.

Cosine weighted L2

template<typename CONF>
struct nganL2_error

Standard cosine weighted l2 error.

A cosine weighted l2 error for an (in,out) BSDF sample. Each sample is weighted by sin(theta_in) * sin(theta_out)

Implements: concepts::samplelossfunction

template<typename CONF>
struct lowL2_error

Low et al. cosine weighted l2 error.

Low et al. “BRDF models for accurate and efficient rendering of glossy

surfaces”:

https://doi.org/10.1145/2077341.2077350

A cosine weighted l2 error on (in,out) BSDF sample. Each sample is weighted by sin(theta_in)

Implements: concepts::samplelossfunction

template<typename CONF>
struct bieronL2_error

Bieron and Peers cosine weighted l2 error.

Bieron and Peers “An Adaptive BRDF Fitting Metric”: https://doi.org/10.1111/cgf.14054

A cosine weighted l2 error on an (in,out) BSDF sample. Each sample is weighted by sin(theta_in) * sin(theta_out) * cos(theta_out)

Implements: concepts::samplelossfunction

Cosine weighted Log

template<typename CONF>
struct standardLog_error

Cosine weighted log error weighted by sin theta of in and out.

A cosine weighted log error for an (in,out) BSDF sample. Each sample is weighted by sin(theta_in) * sin(theta_out). While this error is the re-interpretation of Low et al.’s log error with ‘standard’ sin theta in.out weighting of the error.

Implements: concepts::samplelossfunction

template<typename CONF>
struct lowLog_error

Low et al.’s cosine weighted log error.

Low et al. “BRDF models for accurate and efficient rendering of glossy

surfaces”:

https://doi.org/10.1145/2077341.2077350

A cosine weighted log error on (in,out) BSDF sample. Each sample is weighted by sin(theta_in)

Implements: concepts::samplelossfunction

template<typename CONF>
struct bieronLog_error

Bieron and Peers cosine weighted log error.

Bieron and Peers “An Adaptive BRDF Fitting Metric”: https://doi.org/10.1111/cgf.14054

A cosine weighted log error on an (in,out) BSDF sample. Each sample is weighted by sin(theta_in) * sin(theta_out) * cos(theta_out)

Implements: concepts::samplelossfunction

Linearizer

The behavior of a sampledlossfunction is also determined by a linearizer (bbm::concepts::inout_linearizer) that determines how samples are generated:

template<typename T>
concept inout_linearizer
#include <inout_linearizer.h>

inout_linearizer concept

An inout_linearizer enumerates a discrete set of (in,out) directions on the joint incident and outgoing direction sphere.

Each inout_linearizer contains the following:

  • concept::has_config

  • Size_t size(void) const: the number of discrete direction-pairs

  • Vec3dPair opeator()(Size_t index, Mask mask=true) const: returns the index-th direction pair

  • Size_t operator()(const Vec3d& in, const Vec3d& out, Mask mask=true) const: the inverse operation

An example of a linearizer is spherical_linearizer that samples the incident and outgoing hemisphere of directions:

template<typename CONF>
struct spherical_linearizer

Public Functions

inline spherical_linearizer(const vec2d<Size_t> &samplesIn, const vec2d<Size_t> &samplesOut, const Vec2d &startIn = 0, const Vec2d &endIn = Constants::Hemisphere(), const Vec2d &startOut = 0, const Vec2d &endOut = Constants::Hemisphere())

Constructor.

Parameters:
  • samplesIn – = number vec2d<size_t>(phi, theta) samples for incident directions

  • samplesOut – = number vec2d<size_t>(phi, theta) samples for outgoing directions

  • startIn – = start Vec2d(phi,theta) for incident directions (default 0)

  • endIn – = end Vec2d(phi,theta) for incident directions (default HEMISPHERE)

  • startOut – = start Vec2d(phi,theta) for outgoing directions (default 0)

  • endOut – = end Vec2d(phi,theta) for outgoing directions (default HEMISPHERE)

inline spherical_linearizer(const spherical_linearizer &src)

Copy Constructor.

inline Size_t size(void) const

Size of the linearized ‘array’ of direction pairs.

inline Vec3dPair operator()(Size_t idx, Mask mask = true) const

The in-out coordinate of the idx-th sample.

Note: the sample direction is placed at the center of the index’s bin, except for the top and bottom, where it is placed at the center of the top and bottom edge respectively.

Parameters:

idx – = index of the sample (in [0, samples()-1]

Returns:

a Ve3dPair of the in and out direction of the sample. Will return {0,0} if the index is not valid.

inline Size_t operator()(const Vec3d &in, const Vec3d &out, Mask mask = true) const

Map a coordinate to an linear index.

Invalid querries are set to ‘size()’.

Parameters:
  • in – = incident direction

  • out – = outgoing direction

Returns:

linearized index

Note

A inout_linearizer is also a useful tool for creating a staticmodel. The merl static BSDF model utilizes a merl_linearizer.

Loss functions

The spherical_linearizer is used to predefine a number of sampled loss functions (stored in include/loss.

Sampled Squared Loss

template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct nganL2 : public bbm::sampledlossfunction<BSDF, REFERENCE, nganL2_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct lowL2 : public bbm::sampledlossfunction<BSDF, REFERENCE, lowL2_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct bieronL2 : public bbm::sampledlossfunction<BSDF, REFERENCE, bieronL2_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>

Sampled Log Loss

template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct standardLog : public bbm::sampledlossfunction<BSDF, REFERENCE, standardLog_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct lowLog : public bbm::sampledlossfunction<BSDF, REFERENCE, lowLog_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct bieronLog : public bbm::sampledlossfunction<BSDF, REFERENCE, bieronLog_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>

Optimizer

The optimization algorithms interface is defined by bbm::concepts::optimization_algorithm

template<typename OPT>
concept optimization_algorithm
#include <optimization_algorithm.h>

optimization_algorithm concept

Each optimization algorithm has the following:

  • concepts::has_config

  • Value step(void) : take one optimization step

  • void reset(void) : reset the internal state of the algorithm

  • Mask is_converged(void) const : true if the algorithm has converged

Similar to bsdfmodel and bsdf, a virtual interface is provided through optimizer (and similarly optimizer_base and optimizer_ptr).

An example of an optimization algorithm is bbm::compass:

template<typename LOSSFUNC, typename PARAM, typename BOX = PARAM>
struct compass

Compass Search.

Performs a pattern search by probing each cardinal direction in the parameter space.

When the Value type is a packet, this compass search will perform N parallel searches (where N == size of the packet).

Satisfies: concepts::optimization algorithm

Template Parameters:
  • LOSSFUNC – = loss function

  • PARAM – = parameters to optimize

  • BOX – = box constraint type (default=PARAM)

Public Functions

inline compass(LOSSFUNC &lossfunc, PARAM &param, const BOX &lower = BOX(), const BOX &upper = BOX(), Scalar tolerance = Constants::Epsilon(), Scalar stepSize = 1.0, Scalar contraction = 0.5, Scalar expansion = 1.0, Mask mask = true)

Constructor: compass search.

Parameters:
  • lossfunc – = loss function to minimize

  • param – = set of parameters to optimize

  • lower – = lower bound of box constraint

  • upper – = upper bound of box constraint

  • tolerance – = threshold on step size to determine convergence (default=Epsilon)

  • stepSize – = initial step size (default=1.0)

  • contraction – = reduction factor for step size if no probe in the cardinal directions improves the loss (default=0.5)

  • expansion – = expansion factor for step size if a good candinal direction was found (default=1.0)

inline Value step(void)

probe each cardinal direction, and update the parameters to the one with the lowest loss.

Returns:

loss after update

inline void reset(void)

reset the step size

inline Mask is_converged(void) const

is_converged when _step < _tolerance

Note

optimization algorithms are defined directly in the bbm namespace and stored in include/optimizer

When implementing a new optimization algorithm, care must be taken to ensure that the implementation is compatible with packet types.

Fit files

To store and exhange BSDF fits, BBM uses a simple text based format. In this format lines with comments start with #. Each material fit is stored as: name = model(parameters, where name is a unique identifier (e.g., the material name), model(parameters) is what you would see when the str method in Python is called on the model or the output of bbm::toString. We refer the fits subdirectory for examples. To import the BSDF fits in python:

import bbm_floatRGB as bbm
from import_fits import *
fits = import_fits('fits/ngan_ward.fit', bbm)
str(fits['alum-bronze'])

The 3rd line, reads the fits/ngan_ward.fit file, and imports the fits into a dictionary (called fits). This dictionary contains the BSDF fits with the key the name of the MERL material, and the value the corresponding BSDF (defined in the Python bbm namespace).

In C++ we can use the bbm::io::importFIT method:

template<typename CONF>
void bbm::io::importFIT(const std::string &filename, std::map<std::string, bsdf_ptr<CONF>> &data)

Import a ‘fit’ file.

Parameters:
  • filename – = FIT filename to read from

  • data – = std::map<std::string, bsdf_ptr> to store the fit data to

For example:

#include "io/fit.h"

// ....

std::map<std::string, bsdf_ptr<Config>> fits;
bbm::io::importFIT("filename.fit", fits);
std::cout << fits["alum-bronze"] << std::endl;

Similarly, one can export an std::map of fits using the bbm::exportFIT method:

template<typename CONF>
void bbm::io::exportFIT(const std::string &filename, const std::map<std::string, bsdf_ptr<CONF>> &data, const std::string &comment)

Export a ‘fit’ file.

Parameters:
  • filename – = FIT filename to write to

  • data – = std::map<std::string, bsdf_ptr> of fitted data

  • comment – = string of comment to add to the start of the fit file (ignored if empty)