B.1 Parameter Sets

A key problem that a rendering API must address is extensibility—as new features are added to the system, how does the user-visible API change and what parts of its implementation change? For pbrt, it’s important that developers be able to easily add new implementations of Shapes, Cameras, Integrators, and so forth. We’ve designed the API with this goal in mind.

To this end, the caller-visible API and its implementation are both as unaware as possible of what particular parameters these objects take and what their semantics are. pbrt uses the ParamSet class to bundle up parameters and their values in a generic way. For example, it might record that there is a single floating-point value named “radius” with a value of 2.5 and an array of four color values named “specular” with various SPDs. The ParamSet provides methods for both setting and retrieving values from these kinds of generic parameter lists. It is defined in core/paramset.h and core/paramset.cpp.

Most of pbrt’s API routines take a ParamSet as one of their parameters; for example, the shape creation routine, pbrtShape(), just takes a string giving the name of the shape to make and a ParamSet with parameters for it. The creation routine of the corresponding shape implementation is called with the ParamSet passed along as a parameter; it extracts values from the ParamSet to get parameters to use in a call to the class’s constructor.

<<ParamSet Declarations>>= 
class ParamSet { public: <<ParamSet Public Methods>> 
ParamSet() { } void AddFloat(const std::string &, const Float *, int nValues = 1); void AddInt(const std::string &, const int *, int nValues); void AddBool(const std::string &, const bool *, int nValues); void AddPoint2f(const std::string &, const Point2f *, int nValues); void AddVector2f(const std::string &, const Vector2f *, int nValues); void AddPoint3f(const std::string &, const Point3f *, int nValues); void AddVector3f(const std::string &, const Vector3f *, int nValues); void AddNormal3f(const std::string &, const Normal3f *, int nValues); void AddString(const std::string &, const std::string *, int nValues); void AddTexture(const std::string &, const std::string &); void AddRGBSpectrum(const std::string &, const Float *, int nValues); void AddXYZSpectrum(const std::string &, const Float *, int nValues); void AddBlackbodySpectrum(const std::string &, const Float *, int nValues); void AddSampledSpectrumFiles(const std::string &, const char **, int nValues); void AddSampledSpectrum(const std::string &, const Float *, int nValues); bool EraseInt(const std::string &); bool EraseBool(const std::string &); bool EraseFloat(const std::string &); bool ErasePoint2f(const std::string &); bool EraseVector2f(const std::string &); bool ErasePoint3f(const std::string &); bool EraseVector3f(const std::string &); bool EraseNormal3f(const std::string &); bool EraseSpectrum(const std::string &); bool EraseString(const std::string &); bool EraseTexture(const std::string &); Float FindOneFloat(const std::string &, Float d) const; int FindOneInt(const std::string &, int d) const; bool FindOneBool(const std::string &, bool d) const; Point2f FindOnePoint2f(const std::string &, const Point2f &d) const; Vector2f FindOneVector2f(const std::string &, const Vector2f &d) const; Point3f FindOnePoint3f(const std::string &, const Point3f &d) const; Vector3f FindOneVector3f(const std::string &, const Vector3f &d) const; Normal3f FindOneNormal3f(const std::string &, const Normal3f &d) const; Spectrum FindOneSpectrum(const std::string &, const Spectrum &d) const; std::string FindOneString(const std::string &, const std::string &d) const; std::string FindOneFilename(const std::string &, const std::string &d) const; std::string FindTexture(const std::string &) const; const Float *FindFloat(const std::string &, int *n) const; const int *FindInt(const std::string &, int *nValues) const; const bool *FindBool(const std::string &, int *nValues) const; const Point2f *FindPoint2f(const std::string &, int *nValues) const; const Vector2f *FindVector2f(const std::string &, int *nValues) const; const Point3f *FindPoint3f(const std::string &, int *nValues) const; const Vector3f *FindVector3f(const std::string &, int *nValues) const; const Normal3f *FindNormal3f(const std::string &, int *nValues) const; const Spectrum *FindSpectrum(const std::string &, int *nValues) const; const std::string *FindString(const std::string &, int *nValues) const; void ReportUnused() const; void Clear(); std::string ToString() const;
private: <<ParamSet Private Data>> 
std::vector<std::shared_ptr<ParamSetItem<bool>>> bools; std::vector<std::shared_ptr<ParamSetItem<int>>> ints; std::vector<std::shared_ptr<ParamSetItem<Float>>> floats; std::vector<std::shared_ptr<ParamSetItem<Point2f>>> point2fs; std::vector<std::shared_ptr<ParamSetItem<Vector2f>>> vector2fs; std::vector<std::shared_ptr<ParamSetItem<Point3f>>> point3fs; std::vector<std::shared_ptr<ParamSetItem<Vector3f>>> vector3fs; std::vector<std::shared_ptr<ParamSetItem<Normal3f>>> normals; std::vector<std::shared_ptr<ParamSetItem<Spectrum>>> spectra; std::vector<std::shared_ptr<ParamSetItem<std::string>>> strings; std::vector<std::shared_ptr<ParamSetItem<std::string>>> textures; static std::map<std::string, Spectrum> cachedSpectra;
};

A ParamSet can hold eleven types of parameters: Booleans, integers, floating-point values, points (2D and 3D), vectors (2D and 3D), normals, spectra, strings, and the names of Textures that are being used as parameters for Materials and other Textures. Internally, it stores a vector of named values for each of the different types that it stores; each parameter is represented by a pointer to a ParamSetItem of the appropriate type. A shared_ptr is used for these pointers; doing so allows a parameter to easily be stored in multiple ParamSets, which we’ll find useful in the following.

Storing parameters unsorted in vectors means that searching for a given parameter takes upper O left-parenthesis n right-parenthesis time, where n is the number of parameters of the parameter’s type. In practice, there are just a handful of parameters to any function, so a more time-efficient representation isn’t necessary.

<<ParamSet Private Data>>= 
std::vector<std::shared_ptr<ParamSetItem<bool>>> bools; std::vector<std::shared_ptr<ParamSetItem<int>>> ints; std::vector<std::shared_ptr<ParamSetItem<Float>>> floats; std::vector<std::shared_ptr<ParamSetItem<Point2f>>> point2fs; std::vector<std::shared_ptr<ParamSetItem<Vector2f>>> vector2fs; std::vector<std::shared_ptr<ParamSetItem<Point3f>>> point3fs; std::vector<std::shared_ptr<ParamSetItem<Vector3f>>> vector3fs; std::vector<std::shared_ptr<ParamSetItem<Normal3f>>> normals; std::vector<std::shared_ptr<ParamSetItem<Spectrum>>> spectra; std::vector<std::shared_ptr<ParamSetItem<std::string>>> strings; std::vector<std::shared_ptr<ParamSetItem<std::string>>> textures;

B.1.1 The ParamSetItem Structure

The ParamSetItem structure stores all of the relevant information about a single parameter, such as its name, its base type, and its value(s). For example (using the syntax from pbrt’s input files), the foo parameter

"float foo" [ 0 1 2 3 4 5 ]

has a base type of float, and six values have been supplied for it. It would be represented by a ParamSetItem<Float>.

<<ParamSet Declarations>>+= 
template <typename T> struct ParamSetItem { <<ParamSetItem Public Methods>> 
ParamSetItem(const std::string &name, const T *val, int nValues = 1);
<<ParamSetItem Data>> 
const std::string name; const std::unique_ptr<T[]> values; const int nValues; mutable bool lookedUp = false;
};

The ParamSetItem directly initializes its members from the arguments and makes a copy of the values.

<<ParamSetItem Methods>>= 
template <typename T> ParamSetItem<T>::ParamSetItem(const std::string &name, const T *v, int nValues) : name(name), values(new T[nValues]), nValues(nValues) { std::copy(v, v + nValues, values.get()); }

The Boolean value lookedUp is set to true after the value has been retrieved from the ParamSet. This makes it possible to print warning messages if any parameters were added to the parameter set but never used, which typically indicates a misspelling in the scene description file or other user error.

<<ParamSetItem Data>>= 
const std::string name; const std::unique_ptr<T[]> values; const int nValues; mutable bool lookedUp = false;

B.1.2 Adding to the Parameter Set

To add an entry to the parameter set, the appropriate ParamSet method should be called with the name of the parameter, a pointer to its data, and the number of data items. These methods first remove previous values for the parameter in the ParamSet, if any.

<<ParamSet Methods>>= 
void ParamSet::AddFloat(const std::string &name, const Float *values, int nValues) { EraseFloat(name); floats.emplace_back(new ParamSetItem<Float>(name, values, nValues)); }

We won’t include the rest of the methods to add data to the ParamSet, but we do include their prototypes here for reference. The erasure methods are also straightforward and won’t be included here.

<<ParamSet Public Methods>>= 
void AddInt(const std::string &, const int *, int nValues); void AddBool(const std::string &, const bool *, int nValues); void AddPoint2f(const std::string &, const Point2f *, int nValues); void AddVector2f(const std::string &, const Vector2f *, int nValues); void AddPoint3f(const std::string &, const Point3f *, int nValues); void AddVector3f(const std::string &, const Vector3f *, int nValues); void AddNormal3f(const std::string &, const Normal3f *, int nValues); void AddString(const std::string &, const std::string *, int nValues); void AddTexture(const std::string &, const std::string &);

A number of different methods for adding spectral data are provided, making it easy for this data to be supplied with a variety of representations. The RGB and XYZ variants take 3 floating-point values for each spectrum. AddBlackbodySpectrum() takes pairs of temperature in Kelvins and a scale factor; it uses BlackbodyNormalized() to compute the SPD, which it scales with the given scale. Finally, AddSampledSpectrumFiles() reads SPDs from files on disk; both it and AddSampledSpectrum() construct a piecewise linear SPD given pairs of wavelengths and SPD values at each wavelength.

<<ParamSet Public Methods>>+=  
void AddRGBSpectrum(const std::string &, const Float *, int nValues); void AddXYZSpectrum(const std::string &, const Float *, int nValues); void AddBlackbodySpectrum(const std::string &, const Float *, int nValues); void AddSampledSpectrumFiles(const std::string &, const char **, int nValues); void AddSampledSpectrum(const std::string &, const Float *, int nValues);

B.1.3 Looking up Values in the Parameter Set

To retrieve a parameter value from a set, it is necessary to loop through the entries of the requested type and return the appropriate value, if any. There are two versions of the lookup method for each parameter type: a simple one for parameters that have a single data value, and a more general one that returns a pointer to the possibly multiple values of array parameter types. The first method mostly serves to reduce the amount of code needed in routines that retrieve parameter values.

The methods that look up a single item (e.g., FindOneFloat()) take the name of the parameter and a default value. If the parameter is not found, the default value is returned. This makes it easy to write initialization code like

Float radius = params.FindOneFloat("radius", 1.f);

In this case, it is not an error if the user didn’t provide a “radius” parameter value; the default value will be used instead. If calling code wants to detect a missing parameter and issue an error, the appropriate second variant of lookup method should be used, since those methods return a nullptr value if the parameter isn’t found.

<<ParamSet Methods>>+=  
Float ParamSet::FindOneFloat(const std::string &name, Float d) const { for (const auto &f : floats) if (f->name == name && f->nValues == 1) { f->lookedUp = true; return f->values[0]; } return d; }

We won’t include the declarations of the analogous methods for the remaining types here (FindOneInt(), FindOnePoint3f(), and so forth); they all follow the same form as FindOneFloat()—each takes a parameter name and a default value and returns a value of the corresponding type.

The second kind of lookup method returns a pointer to the data if the data is present and returns the number of values in n.

<<ParamSet Methods>>+= 
const Float *ParamSet::FindFloat(const std::string &name, int *n) const { for (const auto &f : floats) if (f->name == name) { *n = f->nValues; f->lookedUp = true; return f->values.get(); } return nullptr; }

The general lookup functions for the other types follow the same form and so won’t be included here.

Because the user may misspell parameter names in the scene description file, the ParamSet also provides a ReportUnused() function, not included here, that goes through the parameter set and reports if any of the parameters present were never looked up, checking the ParamSetItem::lookedUp member variable. For any items where this variable is false, it is likely that the user has given an incorrect parameter.

<<ParamSet Public Methods>>+=  
void ReportUnused() const;

The ParamSet::Clear() method clears all of the individual parameter vectors. The corresponding ParamSetItems will in turn be freed if their reference count goes to 0.

<<ParamSet Public Methods>>+= 
void Clear();