**Notes on the implementation of the unified material model ** ![
A knob using the unified material showing the blending between diffuse reflection and specular refraction.
](images/gallery/unified-material/pngs/spectrans-07.png width="400px") I have been working on a material model that can handle various common effects reasonably well, which I called the unified material. It is based on the popular Disney BSDF [#Bur15], which is an extension of the previous Disney BRDF [#Bur12]. Two most important changes of the Disney BSDF are the integration of (specular) transmission/refraction and subsurface scattering. The unified material in Narumi offers similar capabilities except subsurface scattering support, which would be another chunk of future work. I came across several implementation details and ideas which I think are worthwhile to take some notes. The core idea of the Disney BSDF, or any other similar "uber" model, is to (linearly) blend between several specified components in order to support a variety of materials. It is important to understand the limitation of this strategy: even if each component is physically-based, blending between them may make less physical sense. For example, the "metallic" parameter introduced by the previous Disney BRDF is not quite "physically-based", because there is nothing in real world that is "50% metallic". Nonetheless, it's simple to use and expressive enough, especially so when considering texture blending. The point is that a general-purpose model does need to sacrifice accuracy in favor of usability and artistic demands very often, even if it's a "PBR" model. Back to the Disney BSDF. It adds a third component ("_Specular BSDF_") and a second blending parameter ("`specTrans`"). _Specular BSDF_ models the specular reflection and tranmission based on Walter et al.'s model [#WMLT07]. It only applies to the dielectric part because light can (almost) never pass through metallic objects. The course note is somewhat vague on the exact meaning of `specTrans`. I think a more precise definition is that `specTrans` desribes how light behaves after the initial refraction at the interface by splitting the behavior into two cases (the reflection part is the same): 1. The first part of energy is never scattered, and the ray direction never changes after the refraction (until next hit). This part contriutes to (`specTrans`) of the entire refracted energy. 2. The second part of energy is scattered one or more times, and the ray direction may change after each scattering. This part contriutes to (`1.0 - specTrans`) of the entire refracted energy. This strategy is a bit ad-hoc because the probability of scattering also depends on the scattesring properties ($\sigma_s$ and $\sigma_t$) of the material and the travel distance of light. But it would be too complicated for a general-purpose material to accurately model this because then it is not only coupled with other material inputs, but also geometry. It is a compromise just like the `metallic` parameter. A problem with calculating Fresnel coefficients occurs after the integration of refraction. The old Disney BRDF uses Schlick's approximation, and lerp between the metallic $R_0$ and dielectric $R_0$ based on the `metallic` parameter. This now doesn't quite work because caclulating metallic Fresnel when the incident ray is inside the material doesn't make sense. It also interferes with the explicit index of refraction input for the dieletric part. The [pbrt implementation](https://github.com/mmp/pbrt-v3/blob/master/src/materials/disney.cpp) can be summarized as the snippet below with some simplification: ~~~~~~ C++ // Material inputs Spectrum baseColor; float metallic; float e; // The (relative) IOR of the material. Spectrum metallicR0 = baseColor; float dielectricR0 = SchlickR0FromEta(e); // Dielectric Fresnel coefficients are monochromatic. Spectrum R0 = lerp(dielectricR0, metallicR0, metallic); Spectrum Fr = lerp(fresnelDielectric(e), fresnelSchlick(R0), metallic); // Other inputs (wi, wt, etc) not shown. return Fr; ~~~~~~ It can be seen here that `metallic` is used twice to blend not only $R_0$, but also the final calulation. The final calculation is a blend between the accurate dielectric part `fresnelDielectric(e)` and the approximated part `fresnelSchlick(R0)` which accounts for both dielectric and metallic part. This looks quite suspicious to me as I couldn't find arguments to support it. It seems to be more broken when the incident ray comes beneath the material. In that case, $e$ becomes $\frac{1}{e}$, and $R_0$ becomes ill-defined. Another problem with this special Fresnel calculation is that for the dielectric part, we can no longer perform importance sampling between the reflection and refraction lobe based on the Fresnel. This sampling is very useful, especially at grazing angles where one would almost never want to sample the refraction lobe, and I don't want to lose it. To solve the problem, I made several modifications: To represent the above changes in a small code snippet: ~~~~~~ C++ // Material inputs Spectrum baseColor; float metallic; float specTrans; float e; // The (relative) IOR of the material. // Other inputs and components omitted. Spectrum metalRefl = metallic * cookTorrance(fresnelSchlick(baseColor)); Spectrum dielectricRefl = (1.0 - metallic) * cookTorrance(fresnelDielectric(e)); Spectrum refr = (1.0 - metallic) * (specTrans) * cookTorrance(fresnelDielectric(e)); Spectrum diffuse = (1.0 - metallic) * (specTrans) * diffuse(); ~~~~~~ A good thing about it is that we can do importance sampling based on Fresnel again: ~~~~~~ C++ // Suppose we've already chosen to sample the dielectric specular bsdf. // Material inputs float metallic; float specTrans; float e; // The (relative) IOR of the material. float3 wo; // The outgoing direction. float3 u; // 3D random number. float3 wm = sampleVNDF(wo, u.xy); // Use first two dimensions to sample the microfacet normal. float fr = fresnelDielectric(e, wo, wm); // Other inputs omitted. float t1 = fr; float t2 = (1.0 - fr) * specTrans; // Don't forget weighting by specTrans. if (u < t1 / (t1 + t2)) { // Sample reflection. } else { // Sample refraction. } ~~~~~~ Since the metallic lobe is now "decoupled", it is easier to switch to the accurate Fresnel calculation that takes complex IOR. We can also use the parameterization proposed in [#Gul14] which is more intuitive, but still maintains the one-to-one mapping to complex IOR. Another decision I made is to **not** evaluate the metallic lobe and the diffuse lobe when the incident ray is inside the surface. This seems to break reciprocity, but I think it fits the context better. In real world light cannot travel in a pure metallic object. The case happens here only because we "blend" between a metallic model and a dielectric model, meaning we may first sample a ray from the dielectric refraction lobe so that the ray travels inside, and then sample from the metallic lobe when it hits another boundary. The conflict is that during the first sample we "think" the material is dielectric, but during the second sample we "think" it's metallic. We shouldn't just flip the normal and sample the metallic/diffuse lobe because 1. it doesn't make physical sense because both metallic reflection and subsurface scattering are only defined on one side, and 2. it violates the previous assumption we made when selecting the first sample (the ray is refracted into the dielectric media, and it won't scatter). Instead, we should keep the assumption and only consider the dielectric specular BSDF. In addition, in this case we no longer need to apply the `(1.0 - metallic)` or `(1.0 - metallic) * specTrans` weight because the metallic lobe and the diffuse lobe have been disabled. The downside with this strategy, as mentioned earlier, is that reciprocity no longer holds when $wo \cdot wi < 0$. I need some further investigation to see how it affects bidirectional methods. **Results**
Varying `metallic`, with `specTrans = 0`, `roughness = 0`, `baseColor = {0.95, 0.63, 0.53}`
![
0.0
](images/gallery/unified-material/pngs/metallic-00.png)
![
0.25
](images/gallery/unified-material/pngs/metallic-025.png)
![
0.5
](images/gallery/unified-material/pngs/metallic-05.png)
![
0.75
](images/gallery/unified-material/pngs/metallic-075.png)
![
1.0
](images/gallery/unified-material/pngs/metallic-10.png)
Varying `roughness`, with `specTrans = 0`, `metallic = 1`, `baseColor = {0.55, 0.55, 0.55}`
![
0.01
](images/gallery/unified-material/pngs/roughness-00.png)
![
0.3
](images/gallery/unified-material/pngs/roughness-03.png)
![
0.5
](images/gallery/unified-material/pngs/roughness-05.png)
![
0.7
](images/gallery/unified-material/pngs/roughness-07.png)
![
1.0
](images/gallery/unified-material/pngs/roughness-10.png)
Varying `roughness`, with `specTrans = 1`, `metallic = 0`, `baseColor = {1, 1, 1}`
![
0.01
](images/gallery/unified-material/pngs/roughness-refr-00.png)
![
0.3
](images/gallery/unified-material/pngs/roughness-refr-03.png)
![
0.5
](images/gallery/unified-material/pngs/roughness-refr-05.png)
![
0.7
](images/gallery/unified-material/pngs/roughness-refr-07.png)
![
1.0
](images/gallery/unified-material/pngs/roughness-refr-10.png)
Varying `specTrans`, with `roughness = 0.1`, `metallic = 0`, `baseColor = {1, 1, 1}`
![
0.0
](images/gallery/unified-material/pngs/spectrans-00.png)
![
0.3
](images/gallery/unified-material/pngs/spectrans-03.png)
![
0.5
](images/gallery/unified-material/pngs/spectrans-05.png)
![
0.7
](images/gallery/unified-material/pngs/spectrans-07.png)
![
1.0
](images/gallery/unified-material/pngs/spectrans-10.png)
Varying `ior`, with `roughness = 0.1`, `metallic = 0`, `specTrans = 1`, `baseColor = {1, 1, 1}`
![
1.1
](images/gallery/unified-material/pngs/ior-11.png)
![
1.3
](images/gallery/unified-material/pngs/ior-13.png)
![
1.5
](images/gallery/unified-material/pngs/ior-15.png)
![
1.7
](images/gallery/unified-material/pngs/ior-17.png)
![
1.9
](images/gallery/unified-material/pngs/ior-19.png)
Here are some images rendered using the unified material that show how different parameters alter the look. The first two rows are reflection-only. They shouldn't look too different from the results of any "PBR" models. The third row shows the support of rough refraction. Note that how the object becomes darker with the increasing roughness, which is due to the fact that the vanilla microfacet model does not handle multiple-scattering between microfacets. The energy loss also happens for the reflection case, except that it is much more visible for the refraction case. The fourth row shows how `specTrans` controls the blending between diffuse reflection and specular transmission. It works reasonably well and the change feels "linear". The object becomes more opaque as `specTrans` decreases. The "blending" nature of the model is especially obvious in the `specTrans = 0.7` image of the fourth row (also shown at the top). Finally, the fifth row shows the results with varying IOR. To summarize, the unified material in Narumi is based on the Disney BSDF but with several important changes. Some of the future works include integrating subsurface scattering, investigating multiple scattering support, and investigating bidirectional method compatibility. References:
[ Bur15] Brent Burley. "Extending the Disney BRDF to a BSDF with integrated subsurface scattering." Physically Based Shading in Theory and Practice'SIGGRAPH Course (2015).
[ Bur12] Brent Burley. Physically-based shading at Disney, course notes, revised 2014. In ACM SIGGRAPH, Practical physically-based shading in film and game production, 2012
[ Gul14] Gulbrandsen, Ole. "Artist friendly metallic fresnel." Journal of Computer Graphics Techniques 3.4 (2014).
[ WMLT07] Bruce Walter, Stephen R. Marschner, Hongsong Li, and Kenneth E. Torrance. Microfacet models for refraction through rough surfaces. In Proceedings of the Eurographics Symposium on Rendering,2007.