The previous chapter presented all the bidirectional scattering distribution functions (BSDFs) defined by MDL. The BSDF value produced by calling a BSDF constructor is an elemental type in the MDL language—as an author of a material, you do not need to be aware of its internal structure, but only of the parameters that control its behavior during the rendering process.
To create appearance models beyond the simple materials of the previous chapter, BSDFs can be combined using combiner functions. A BSDF combiner function produces a BSDF, just like the value returned by a BSDF constructor. Both BSDF constructors and combiner functions use the same function-like syntax, with a list of arguments contained within parentheses. However, a BSDF combiner function uses arguments that are themselves BSDFs.
Though the combinations provided by combining functions are only the result of arithmetic operations on numerical values, it is useful to compare these combinations to familiar activities in the physical world:
Sometimes the arithmetic relationship between two BSDFs can be as easily expressed by mixing as by layering. When simulating an effect in the physical world, you should attempt to mimic its structure in your design of BSDF combinations. Changes—fixing errors, making improvements—are inevitable for a complex combination of BSDFs. Making these changes is easier when you first consider the real-world effect being modeled, which can then suggest how you should change the material. For example, combining two layers evenly or mixing two BSDFs by the same amount will produce equivalent results, but lacquer painted on wood makes much more intuitive sense as a process of layering, and not as one of mixing.
In the simplest type of BSDF layering, weighted layering, a scaling factor, called the weight, determines the fraction of the layer to be applied over the base.
The other three layering types all similarly define a layer applied over a base based on a weighting factor, but differ in how the fractional amounts of layer and base are calculated.
Layering function | Effect |
weighted_layer | Add BSDF layer based on a weighting factor |
fresnel_layer | Add BSDF layer based on a weighting factor and a Fresnel term expressed by an index of refraction |
custom_curve_layer | Add BSDF layer based on weighting factor and directionally dependent curve function |
measured_curve_layer | Add BSDF layer based on weighting factor and measured reflectance curve |
This chapter first describes layering and mixing two BSDFs. Later sections describe how multiple BSDFs can be combined, as well as suggesting strategies for managing the structural complexity of more elaborate material designs. Later chapters describe the combination of multiple EDFs or VDFs, in which physical principles limit the combination mode to mixing.
The syntax of the layering function when it is used in a material is the same as the syntax of the distribution functions—the name of the function, followed by a parenthesized list of arguments, separated by commas.
df::weighted_layer ( weight: layer-fraction, layer: BSDF, base: BSDF )
For the first example of weighted layering, a df::simple_glossy_bsdf will be layered over a df::diffuse_reflection_bsdf. These BSDFs can be visualized by using each separately in a minimal material.
To visualize the base argument to df::weighted_layer, Figure 6.2 is rendered using df::diffuse_reflection_bsdf:
![]() Figure 6.2
|
material red() = material ( surface: material_surface ( scattering: df::diffuse_reflection_bsdf ( tint: color(0.3, 0.03, 0.05)))); |
To visualize the layer argument, Figure 6.3 is rendered using df::simple_glossy_bsdf:
![]() Figure 6.3
|
material shiny() = material ( surface: material_surface ( scattering: df::simple_glossy_bsdf ( tint: color(0.15), roughness_u: 0.08, mode: df::scatter_reflect))); |
In the same way that the two BSDFs were used for the scattering argument of the material_surface constructor, the BSDFs are now used as arguments to df::weighted_layer:
material shiny_red( float shiny_weight = 0.5) = material ( surface: material_surface (
scattering: df::weighted_layer (Layering function
weight: shiny_weight,Weighting factor
layer: df::simple_glossy_bsdf ( tint: color(.15), roughness_u: .08, mode: df::scatter_reflect),Shiny component
base: df::diffuse_reflection_bsdf ( tint: color(0.3, 0.03, 0.05)))));Diffuse red component
The default value of 0.5 for the shiny_weight parameter of material shiny_red produces an appearance which is composed of equal parts of the two BSDFs in Figure 6.4:
![]() Figure 6.4
|
shiny_red() |
Varying the shiny_weight argument from 0.1 to 0.9 in increments of 0.1 demonstrates the range of possible effects that the shiny_red material can produce.
Note that the simplicity of the arithmetic of the BSDF combination doesn't explain the apparently different substances being rendered—lacquered wood? a metallic finish? It is very hard to predict the perceptual effect of even simple combinations like this example of weighted layering, in which a process that seems to entail matters of degree (the weighting of the layers) become in fact matters of category (the appearance of different substances). More dependable results require materials based on designs that model light interaction in the world—modeling made possible in MDL by the combining functions described in this chapter.
Not all of the language features in MDL are concerned with describing appearance. Some features provide means to structure and organize materials to allow for increasing complexity of expression. One such feature is the let-expression, a means to simplify larger material definitions using intermediate calculations.
In simpler material definitions, field values in the material struct can only be of four types:
In three of these types, the value to be used is expressed directly, either as a constant or a constructor. Only the parameters to the material definition can provide names for values used in the definition of the material.
material shiny_red ( float shiny_weight = 0.5) =Signature with parameters
material (
surface: material_surface ( scattering: df::weighted_layer ( weight: shiny_weight, layer: df::simple_glossy_bsdf ( tint: color(.15), roughness_u: .08, mode: df::scatter_reflect), base: df::diffuse_reflection_bsdf ( tint: color(0.3, 0.03, 0.05)))));Material constructor referring to the parameters contained in the signature
A material designed in this way consists of two fundamental parts: the signature containing the material parameters, and the material definition containing field values that can refer to the parameters.
In traditional programming languages, complex calculations can be broken up into more manageable pieces by using symbolic names, called variables, to store intermediate values. In MDL, the same simplifying procedure exists through an optional third section to the material definition, the let-expression.
The let-expression variables can refer to parameters in the signature, as well as to other variables that have been previously defined in the let-expression. Field values can then refer to both these variables as well as the parameters of the material's signature.
Syntactically, the let-expression follows the equals sign in a material definition, and encloses a series of variable definitions separated by semicolons:
let { data-type variable-name = value-of-type ; ... } in
For example, note that the values of the base and layer fields in the following material, shiny_red_with_let, are BSDF constructors. Rather than constructing the BSDF in the material struct itself, the two BSDFs can be defined as let-expression variables of type bsdf, and then used as the values of the respective fields. Material shiny_red_v1 is the result of using let-expression variables in material shiny_red:
material shiny_red_with_let ( float shiny_weight = 0.5)Signature with a single parameter
=
let { bsdf shiny_bsdf = df::simple_glossy_bsdf ( tint: color(.15), roughness_u: .08, mode: df::scatter_reflect); bsdf red_bsdf = df::diffuse_reflection_bsdf ( tint: color(0.3, .03, 0.05)); } inTemporary variables defined for use in the material definition
material ( surface: material_surface ( scattering: df::weighted_layer ( weight: shiny_weight, layer: shiny_bsdf, base: red_bsdf)));The material definition using the shiny_weight input parameter and the two temporary variables that define BSDFs
These two materials—shiny_red and shiny_red_with_let—produce the same results in a rendered image. But how the material creates that result has been clarified: two BSDFs are constructed and then used as the arguments to df::weighted_layering. Better still, the names of the let-expression variables—shiny_bsdf and red_bsdf—serve to describe the visual intent of those BSDFs. In this simple example, that intent may be easy to see in the version without a let-expression. For more complex materials, however, the naming of intermediate variables can be an important way to find the inevitable errors that complex structures encourage.
The shiny_red and shiny_red_v1 materials use the same BSDF constructors that were shown at the beginning of the chapter, in the materials red and shiny. Rather than copying an existing field value of a struct, MDL provides the dot operator (described in “Accessing struct components with the dot operator”) to refer to the fields within any struct, including the material struct.
The dot operator can be used to extract the BSDFs from materials red and shiny. But unlike the example in Chapter 2, in which field values were extracted from instances saved as variables, the following reimplementation of shiny_red extracts BSDFs from instances constructed directly within the field value:
material shiny_red_extract( float shiny_weight = 0.5) = let {
bsdf shiny_bsdf = shiny().surface.scattering;Scattering BSDF extracted from material shiny
bsdf red_bsdf = red().surface.scattering;Scattering BSDF extracted from material red
} in material ( surface: material_surface ( scattering: df::weighted_layer ( weight: shiny_weight,
layer: shiny_bsdf,Using the BSDF of shiny
base: red_bsdf)));Using the BSDF of red
At first glance, it appears that the shiny and red materials have been created only to be taken apart. However, in developing complex materials, being able to visualize components—because they are renderable as complete materials in themselves—can greatly simplify the design and debugging process. But also note that it is not necessary for the original MDL code to be available for those materials—indeed, for the user of those materials to even know how they are defined—to be reused for the BSDFs that they contain.
The shiny_red material is limited in its design, only allowing the weight of the layering process to be controlled through its shiny_weight parameter. Adding more control over a material's behavior by adding additional parameters is called parameterization.
To create a more flexible material based on shiny_red, the two materials that provide the BSDFs need first to be parameterized. Material diffuse is produced by making the tint argument of df::diffuse_reflection_bsdf a parameter:
material diffuse (
color tint = color(0.5))Parameter tint for the diffuse reflection color
= material ( surface: material_surface ( scattering: df::diffuse_reflection_bsdf (
tint: tint)));Using the tint parameter
Similarly, the tint and roughness arguments to df::simple_glossy_bsdf are defined to be parameters to create material glossy:
material glossy (
color tint = color(0.5),Parameter tint for the glossy reflection color
float roughness = .1)Parameter roughness for the degree of glossy reflection
= material ( surface: material_surface ( scattering: df::simple_glossy_bsdf (
tint: tint,Using the tint parameter
roughness_u: roughness,Using the roughness parameter
mode: df::scatter_reflect)));
Now that the materials used as components to shiny_red have been parameterized, a parameterized version of shiny_red_with_let (using the dot operator to extract BSDFs) can also be created, here called glossy_over_diffuse.
material glossy_over_diffuse(
color glossy_tint = color(0.1), float glossy_roughness = 0.1, float glossy_weight = 0.5,Glossy parameters
color diffuse_tint = color(0.5))Diffuse parameter
= let {
bsdf glossy_bsdf = glossy( tint: glossy_tint, roughness: glossy_roughness ).surface.scattering;Glossy BSDF value extracted from glossy material created here
bsdf diffuse_bsdf = diffuse( tint: diffuse_tint ).surface.scattering;Diffuse BSDF value extracted from diffuse material created here
} in material ( surface: material_surface (
scattering: df::weighted_layer ( weight: glossy_weight, layer: glossy_bsdf, base: diffuse_bsdf)));Layering
Now that shiny_red has been parameterized to produce glossy_over_diffuse, the visual possibilities of this simple layering material can be explored. For example, the glossy_over_diffuse parameter values of Figure 6.8Varying the parameters of material glossy_over_diffuse create an object that looks like blue porcelain (rather than lacquered and painted wood):
![]() Fig. 6.8 – Varying the parameters of material glossy_over_diffuse
|
glossy_over_diffuse( glossy_tint: color(0.1, 0.1, 0.08), glossy_roughness: 0.2, diffuse_tint: color(0.7, 0.8, 0.9)) |
Using material glossy_over_diffuse—generalized through parameterization—material shiny_red can be redefined as a special case of glossy_over_diffuse:
material shiny_red_specialized() = glossy_over_diffuse ( glossy_tint: color(0.15), glossy_roughness: 0.08, diffuse_tint: color(0.3, 0.03, 0.05));
Recall from Chapter 3 that whitespace—the space, tab and newline characters—are ignored in MDL. At first glance, the definition of shiny_red_v3 may appear to be different in structure than the previous material definitions, but this change in format emphasizes that the syntax for all material definitions corresponds to the variable declaration syntax described in “The structure of a material”.
data-type variable-name = variable-value ;
Similarly, the blue ceramic effect can be defined as a new material called “light_blue_ceramic” by also using glossy_over_diffuse.
material light_blue_ceramic() = glossy_over_diffuse ( glossy_tint: color(0.1, 0.1, 0.08), glossy_roughness: 0.2, diffuse_tint: color(0.7, 0.8, 0.9));
The definitions of shiny_red_v3 and light_blue_ceramic do not have any parameters; a user of those definitions cannot change the internal parameter values of glossy_over_diffuse from which these materials are derived. Controlling access to internal state in this manner is called encapsulation—the parameters of an existing material have been hidden (encapsulated) in the creation of a new material.
When creating a new material from an existing material, it may be useful to include a subset of the parameters, a technique called partial encapsulation. For example, this material partially encapsulates glossy_over_diffuse, only allowing the diffuse_tint parameter to be modified in the new material:
material shiny_tint(
color tint = color(1))Parameterizing the diffuse color as tint
= glossy_over_diffuse( glossy_tint: color(0.15), glossy_roughness: 0.08,
diffuse_tint: tint);Using the tint parameter
Partial encapsulations allows complex, highly parameterized materials to serve as the basis of a family of materials, each particularizing the original material for its own purpose. In a production context, a single material may created to implement a complex optical effect—anodized aluminum, car paint with metal flakes and a clear coat, boomerang Formica. Designers are provided with a set of materials that partially encapsulate this complex material, allowing them control over a limited set of the underlying parameters. In an application's graphical user interfaces, a partially encapsulated material presents the user with a simplified interface, allowing modification of only those aspects of the material that the design should allow.
Section 5 of this chapter develops a complex material and then demonstrates this process of partial encapsulation and the way in which it simplifies the user interface to the material.
The df::weighted_layer function combines the base and layer BSDFs in the same way over the surface of the object, controlled by the weighting factor. In contrast, the df::fresnel_layer is directionally dependent; it includes the viewing angle at each point on the rendered surface in its calculation.
The directional dependency of df::fresnel_layer is based on the Fresnel equations, named after Augustin-Jean Fresnel, who derived them in 1823. These equations describe reflection and refraction of light when it strikes a boundary between two transparent media, like air and glass.
When a light strikes such a boundary, the light is typically both reflected and refracted. The proportion of reflection to refraction is determined by two factors: the refractive index of both media and the angle at which the light strikes the boundary (the incident angle). The higher the ratio of the two refractive indices the more light is bent when it crosses the boundary.
Substance | Refractive index |
Vacuum | 1.0 (by definition) |
Air | 1.00029 |
Ice | 1.31 |
Water at 20° Celsius | 1.33 |
Ethyl alcohol | 1.36 |
Fused quartz | 1.46 |
Glycerine | 1.473 |
Acrylic glass | 1.490-1.492 |
Crown (optical) glass | 1.52-1.62 |
Polystyrene | 1.55-1.59 |
Emerald | 1.565-1.602 |
Topaz | 1.609-1.643 |
Sapphire | 1.77 |
Diamond | 2.417 |
Silicon | 3.678 |
Traditionally, computer graphics systems have made the simplifying assumption that objects exist in a vacuum, so that only the refractive index of the object matters (since a number is unchanged by dividing by 1.0). The refractive index of air is close enough to 1.0 so that it, too, can safely be ignored. However, the ratio of the refractive indices of an ice cube in water is very close to 1.0, so that the light direction is changed very little (making ice in water harder to see than glass in air).
The Fresnel equations also define the proportion of light that is reflected and refracted at a boundary. In an idealized object (with no absorption or emission), the sum of the reflected and refracted light will equal the incoming light. Both the ratio of refractive indices and the incident angle determine the amount of light that is reflected and the amount that is refracted. The greater the refractive index ratio, the more light will be reflected from the boundary, rather than refracted through it. In addition, the smaller the incident angle, the more light will be reflected. As the angle between the light direction and the surface (the grazing angle) approaches zero, the surface becomes increasingly reflective. At the theoretical limit—when the grazing angle is zero—the surface is only reflective, with no refraction across the boundary.
For example, in this diagram of the reflective and refractive properties of a sphere of optical glass, the length of the two arrows at each incident point represent the relative proportions of reflection and refraction. Very little light is reflected when the light strikes the surface directly (a grazing angle close to 90°). Much more light is reflected when the grazing angle is close to zero (at the top and bottom of the sphere).
The same increase in reflection at small grazing angles can be seen in this diagram of a sphere made of diamond. But the higher refractive index ratio causes an increase in reflection for all light directions.
The diagram of diamond shows, in comparison to the diagram of glass, why diamonds sparkle—the surface reflects light at any angle. The Fresnel equations quantify this amount: 17% of the light striking a diamond surface is reflected when the light direction is straight on (when it forms a 90° angle with the surface), but only 4% of the incoming light is reflected by glass at that angle. The facets of a cut diamond make use of this property, providing many surfaces at different angles to increase the chance of reflections reaching the eye of the beholder.
The Fresnel equations define light interaction at a boundary: a relationship between refractive indices, angle of light incidence, and the resulting proportion of reflected and transmitted light. But the thin coating of a transparent substance like varnish provides little distance for the change of light direction of the transmitted light—its refraction—to have much of an effect.
MDL models the small influence of refraction in a thin layer with the df::fresnel_layer function. The Fresnel calculation of the fractional amount of reflection is used to scale the contribution of the BSDF value supplied as the value of the layer argument to df::fresnel_layer. To the scaled layer value is added the value of the base BSDF, scaled by the complement of the reflection factor:
(layer * Fresnel-reflection-factor) + (base * (1.0 - Fresnel-reflection-factor))
The change of direction from refraction is ignored, so the same point on the surface is used for the evaluation of both the layer and base BSDFs.
The df::fresnel_layer function is similar in structure to df::weighted_layer, but adds an index of refraction parameter, abbreviated “ior,” to the three parameters in df::weighted_layer:
df::fresnel_layer ( ior: index-of-refraction-of-layer weight: layer-fraction, layer: BSDF, base: BSDF )
Based on the Fresnel equations, the ior parameter controls how much the light the layer parameter's BSDF reflects, with the incident angle defined by the view from the renderer's virtual camera.
The following two BSDFs will be combined by df::fresnel_layer in Figure 6.13Varying the index of refraction of the glossy layer at different ior values:
![]() Figure 6.11
|
material diffuse ( color tint = color(0.5)) = material ( surface: material_surface ( scattering: df::diffuse_reflection_bsdf ( tint: tint))); |
![]() Figure 6.12
|
material glossy ( color tint = color(0.5), float roughness = .1) = material ( surface: material_surface ( scattering: df::simple_glossy_bsdf ( tint: tint, roughness_u: roughness, mode: df::scatter_reflect))); |
Two temporary variables in the let-expression define the glossy BSDF used as a layer and the diffuse BSDF for the base:
material fresnel_glossy_over_diffuse( float glossy_roughness = 0.1, color diffuse_tint = color(0.5),
color ior = color(1.3))Index of refraction parameter
= let {
bsdf glossy_bsdf = glossy( tint: color(0.7), roughness: glossy_roughness).surface.scattering;Glossy layer: the BSDF from the glossy material
bsdf diffuse_bsdf = diffuse( tint: diffuse_tint).surface.scattering;Diffuse layer: the BSDF from the diffuse material
} in material ( surface: material_surface (
scattering: df::fresnel_layer (Fresnel layering
ior: ior,Index of refraction to control glossy layer
weight: 1.0,
layer: glossy_bsdf,Layer affected by the index of refraction layer
base: diffuse_bsdf)));
Rendering a series of images with an increasing ior value shows the increasing amount of reflection of the glossy layer. For an ior value of 1.0, there is no reflection at all. As the ior value increases, more and more of the surface becomes increasingly reflective, showing the influence of the glossy layer.
Though the df::fresnel_layer ignores the refraction component, the reflection values are consistent with the the Fresnel equations. In a later section, the df::fresnel_layer is added as the last layer in a combination of several materials, simulating in a physically based way a clear, protective coating.
The layering functions define how two BSDFs should be combined with a clear spatial relationship—a layer over a base. In MDL's mixing functions, a set of BSDFs are combined in an additive way, like mixing paint. Each component of the mix only defines the contribution of its BSDFs using a weighting factor, a value of type float greater than or equal to 0.0.
Because any number of components can be combined in a mixing function and all components can have an arbitrary weighting factor, the two mixing functions are characterized by how they handle a set of components with a sum of weighting factors that exceeds 1.0.
Mixing function | Modification if weighting factors sum to greater than 1.0 |
normalized_mix | All weighting factors are scaled proportionally so that their sum is 1.0 before mixing begins. |
clamped_mix | Components are summed in order until the sum of the weighting factors is greater than 1.0. The weighting factor of the final component that caused the sum to exceed 1.0 is reduced so that the sum is now 1.0. That final component with its reduced weight factor is then used in the mix. All further components are ignored. |
A picture can clarify the different behavior of the two mixing functions when the sum of their components exceeds 1.0: normalizing retains all the components, but at a different scale; clamping maintains the scale of the components, but does not retain them all.
Each component of a mixing function is defined by a struct of two fields: its weight and BSDF.
df::bsdf_component ( weight: fraction-for-component, component: BSDF-instance )
For example, this df::bsdf_component constructor call creates a diffuse reflection component with a weight of 50%:
df::bsdf_component ( weight: 0.5, component: df::diffuse_reflection_bsdf ( tint: color(0,0,1)))
The components are collected in an MDL array. An array is an ordered sequence of values of the same data type. A pair of square brackets following a data type defines an array of that type. The constructor function for the array lists the components of the array within parentheses, separated by commas.
array-element-type[] ( array-element-1, array-element-2, ... array-element-n )
In the two mixing functions, the value of the components field is an array of type df::bsdf_component[] as in this example of normalized_mix:
normalized_mix ( components: df::bsdf_component[] ( component-1, component-2, ... component-n ))
For example, this function call of normalized mix combines three diffuse reflection BSDF components:
normalized_mix ( components: df::bsdf_component[] (
df::bsdf_component ( weight: 0.3, component: df::diffuse_reflection_bsdf ( tint: color(1,0,0))),Component 1
df::bsdf_component ( weight: 0.2, component: df::diffuse_reflection_bsdf ( tint: color(0,1,0))),Component 2
df::bsdf_component ( weight: 0.5, component: df::diffuse_reflection_bsdf ( tint: color(0,0,1)))))Component 3
Like the layering functions, the normalized_mix and clamped_mix functions return BSDFs and can therefore be used wherever a BSDF value is required—even as a component in a layering function or in another set of BSDFs combined in a mixing function. An illustration of combinatorial possibilities is provided in the last section of this chapter, where five BSDFs are combined.
Chapter 1 described the various types of reflection and transmission at a surface. The shape of the curve that describes relative reflective intensities for glossy reflection from a surface is often described as a “lobe.”
As an example of mixing, the material presented below, two_glossy_lobes, combines two different types of glossy reflections, with color and roughness for both glossy reflections and their relative weight defined by parameters to the material.
material two_glossy_lobes (
float roughness_1 = 0.1, float weight_1 = 0.5, color tint_1 = color(0.7),Parameters for first lobe
float roughness_2 = .5, float weight_2 = 0.5, color tint_2 = color(0.7))Parameters for second lobe
= material ( surface: material_surface ( scattering: df::clamped_mix (
components: df::bsdf_component[] (Array constructor for df::bsdf_component
df::bsdf_component( weight: weight_1, component: df::simple_glossy_bsdf ( tint: tint_1, roughness_u: roughness_1)),Component one
df::bsdf_component( weight: weight_2, component: df::simple_glossy_bsdf ( tint: tint_2, roughness_u: roughness_2))))));Component two
To demonstrate the effect of mixing two glossy reflections, these parameter values will be used in two_glossy_lobes:
Parameter | Component 1 | Component 2 |
tint | color(0.2, 0.3, 0.25) | color(0.6, 0.4, 0.2) |
roughness | 0.3 | 0.6 |
By setting the weight of one component to 1.0 and the other to 0.0, the individual contribution of both components can be displayed. First, the effect of the first component alone:
![]() Figure 6.16
|
two_glossy_lobes( weight_1: 1.0, tint_1: color(0.2, 0.3, 0.25), roughness_1: 0.3, weight_2: 0.0, tint_2: color(0.6, 0.4, 0.2), roughness_2: 0.6) |
Using only the second component in rendering the objects with two_glossy_lobes produces Figure 6.17:
![]() Figure 6.17
|
two_glossy_lobes( weight_1: 0.0, tint_1: color(0.2, 0.3, 0.25), roughness_1: 0.3, weight_2: 1.0, tint_2: color(0.6, 0.4, 0.2), roughness_2: 0.6) |
The effect of the combination of these two glossy reflections can be shown by systematically varying the relative weights between the two glossy reflection components.
Controlling the weights of components in a combining function not only provides greater design control over the effect of the material, it also allows—as in the examples above—a way of checking that the individual components are behaving as expected.
In the fresnel_glossy layering function, the reflective component of the Fresnel equations provides a sense of physical plausibility in the appearance of the resulting image. The design of a successful material may not use actual formulas from physics and yet still base its design on physical principles that provide a higher degree of visual “realism” than are possible in strategies that ignore physics completely. For example, the color of reflection from metals varies based on viewing angle, so that in reality, the reflections from silver aren't simply gray, nor is copper simply brown or gold only yellow.
First, the visually obvious yellow hue of the sharper highlights of the gold reflection, here made visible as in the previous section by setting its component value to 1.0, and the other component to 0.0:
![]() Figure 6.19
|
two_glossy_lobes( weight_1: 0.0, tint_1: color(0.3, 0.15, 0), roughness_1: 0.6, weight_2: 1.0, tint_2: color(0.5, 0.4, 0.1), roughness_2: 0.3) |
The other glossy reflection is a darker, orange color, with a greater degree of roughness so that it is visible outside the areas of greatest reflection of the yellow component.
![]() Figure 6.20
|
two_glossy_lobes( weight_1: 1.0, tint_1: color(0.3, 0.15, 0), roughness_1: 0.6, weight_2: 0.0, tint_2: color(0.5, 0.4, 0.1), roughness_2: 0.3) |
Mixing the yellow and orange components artistically approximates the complex color behavior of gold reflections:
![]() Figure 6.21
|
two_glossy_lobes( weight_1: 0.5, tint_1: color(0.3, 0.15, 0), roughness_1: 0.6, weight_2: 0.5, tint_2: color(0.5, 0.4, 0.1), roughness_2: 0.3) |
By varying the color and roughness of yellow and orange, an artistic simulation of the complex reflective color of gold becomes not just possible, but, with the parameters of two_glossy_lobes, easily fine-tuned for artistic purposes. In this example, the solution to the problem of metallic reflection is not so much physically based, as physically inspired.
In all the previous examples of this chapter, only two BSDFs were combined, either layered or mixed together in various ways. Because the result of the layering and mixing functions is a value of type BSDF, that result can be used as input to yet another combining function. In this way, layer and mixing can be used to create arbitrarily complex aggregations of simple and compound BSDFs.
For example, the output of weighted_layer can serve as the layer or base of another call to the weighted_layer function:
The nesting of the layering function is also clear from the resulting syntactic structure when the result of weighted_layer is used as the value of the layer parameter to another call to weighted_layer (here made more visible by the symbolic representation of the BSDFs as letters A to E):
![]() Fig. 6.23 – The vertical relationship of layers is implied by
the nesting of distribution functions
|
weighted_layer ( weight: w4, weighted_layer ( weight: w3, weighted_layer ( weight: w2, weighted_layer ( weight: w1, layer: A, base: B), base: C), base: D), base: E) |
The syntactic similarity of the combining functions to the constructor of the material struct could obscure other ways of thinking about the relationship of these function calls. Traditional programming languages might favor a more horizontal layout:
function(w4, function(w3, function(w2, function(w1, A, B), C), D), E)
As the number of combinations of BSDFs increases, part of the material designer's task is to manage this complexity in the way that the materials are organized. The next two sections show two of the simpler organizational principles for building more complex materials.
To demonstrate a series of layering functions, the following four materials are defined:
To simplify the material definitions as well as provide consistency when the same color is desired, three const variables of type color are defined:
const color white = color(0.7); const color blue = color(0.2, 0.3, 0.7); const color green = color(0.1, 0.3, 0.05);
No parameters are defined in all four of the following materials; the values to be used in layering are explicitly provided as arguments to the BSDF itself.
![]() Fig. 6.24 – Material 1: Diffuse green reflection as base color
|
material diffuse_green_base() = material ( surface: material_surface ( scattering: df::diffuse_reflection_bsdf ( tint: green))); |
![]() Fig. 6.25 – Material 2: A dull glossy blue (a glossy with a “wide” lobe)
|
material wide_blue_glossy() = material ( surface: material_surface ( scattering: df::simple_glossy_bsdf ( tint: blue, roughness_u: 0.5))); |
![]() Fig. 6.26 – Material 3: A shiny glossy blue (a glossy with a “narrow” lobe)
|
material narrow_blue_glossy() = material ( surface: material_surface ( scattering: df::simple_glossy_bsdf ( tint: blue, roughness_u: 0.2))); |
![]() Fig. 6.27 – Material 4: A mirror reflection for a “clear coat” effect
|
material mirror() = material ( surface: material_surface ( scattering: df::specular_bsdf ( tint: white, mode: df::scatter_reflect))); |
![]() |
In this depiction of BSDF combination, the vertical order of the rendered components suggests different ways of thinking about the process: a thin mirror-like coating over sharp blue highlights, over glossy blue on a base layer of diffuse green. On the other hand, a painter may think in the other direction, with a base layer of matte green, over which blue washes with different degrees of glossiness, finished by a clear-coat layer of varnish. |
The first combination layers the wide blue glossy BSDF over the diffuse green base in material two_layers:
![]() Figure 6.28
|
material two_layers () = combinations::two_glossy_lobes( weight_1: 0.5, tint_1: orange, roughness_1: 0.6, weight_2: 0.5, tint_2: yellow, roughness_2: 0.3); |
The combination of the first two BSDFs in two_layers is used as the base value, extracted by the dot operator to create the temporary variable base in the let expression of material three_layers:
![]() Figure 6.29
|
material three_layers () = let { bsdf layer = two_layers () .surface.scattering; bsdf base = diffuse_green_base() .surface.scattering; } in material ( surface: material_surface( scattering: df::custom_curve_layer( normal_reflectivity: 0, grazing_reflectivity: 1.0, exponent: 0.5, weight: 1, layer: layer, base: base))); |
Finally, a mirror-like layer is combined over the BSDF extracted in the let from material three_layers:
![]() Figure 6.30
|
material four_layers () = let { bsdf layer = narrow_blue_glossy() .surface.scattering; bsdf base = three_layers () .surface.scattering; } in material ( surface: material_surface( scattering: df::weighted_layer( weight: 0.3, layer: layer, base: base))); |
To combine mixing and layering, the result of the mixing function within two_glossy_lobes will be used as a layer argument to df::custom_curve_layer.
As before, the tint parameter values for the components of two_glossy_lobes are defined for clarity as const variables of type color:
const color orange = color(0.3, 0.15, 0.0); const color yellow = color(0.5, 0.4, 0.1);
Adjusting the weights of two_glossy_lobes to display the orange layer alone produces Figure 6.31:
![]() Figure 6.31
|
two_glossy_lobes( weight_1: 1, tint_1: color(orange), roughness_1: 0.6, weight_2: 0, tint_2: color(yellow), roughness_2: 0.3) |
Adjusting the weights to display the yellow layer alone produces Figure 6.32:
![]() Figure 6.32
|
two_glossy_lobes( weight_1: 0, tint_1: color(orange), roughness_1: 0.6, weight_2: 1, tint_2: color(yellow), roughness_2: 0.3) |
![]() |
The orange and yellow components are combined and used for the layer parameter of custom_curve_layer, with the diffuse green component as a base. The other two components from the previous example are layered above as before. |
The orange and yellow glossy reflections are combined in two_glossy_lobes:
![]() Figure 6.33
|
material two_layers () = combinations::two_glossy_lobes( weight_1: 0.5, tint_1: orange, roughness_1: 0.6, weight_2: 0.5, tint_2: yellow, roughness_2: 0.3); |
The BSDF of two_glossy_lobes is used as a layer over the diffuse green BSDF, which is used as a base:
![]() Figure 6.34
|
material three_layers () = let { bsdf layer = two_layers () .surface.scattering; bsdf base = diffuse_green_base() .surface.scattering; } in material ( surface: material_surface( scattering: df::custom_curve_layer( normal_reflectivity: 0, grazing_reflectivity: 1.0, exponent: 0.5, weight: 1, layer: layer, base: base))); |
The BSDF of narrow_blue_glossy is used as a layer over the BSDF of three_layers:
![]() Figure 6.35
|
material four_layers () = let { bsdf layer = narrow_blue_glossy() .surface.scattering; bsdf base = three_layers () .surface.scattering; } in material ( surface: material_surface( scattering: df::weighted_layer( weight: 0.3, layer: layer, base: base))); |
Finally, the combination of the four components is the base for BSDF of mirror used as the uppermost layer in Figure 6.36:
![]() Figure 6.36
|
material five_layers() = let { bsdf layer = mirror() .surface.scattering; bsdf base = four_layers () .surface.scattering; } in material ( surface: material_surface( scattering: df::fresnel_layer( ior: color(1.6), weight: 1.0, layer: layer, base: base))); |
Creating separate materials for each incremental combination of the separate BSDFs is very helpful in designing complex materials. Once the basic structure of a complex material has been designed in this manner, further refinements can be made, including the selection of internal parameter values that should be exposed in the signature of the top-level material.