appleseedhq / appleseed

A modern open source rendering engine for animation and visual effects
https://appleseedhq.net/
MIT License
2.19k stars 329 forks source link

Implement microfacet normal mapping #2886

Closed lorentzo closed 3 years ago

lorentzo commented 4 years ago

Intro

This PR contains the implementation of microfacet based normal mapping for more robust normal mapping. It is connected to the issue "Investigate more robust normal mapping" #2427. It is implemented during Google Summer of Code 2020.

Implementation and further discussion is based on paper Microfacet-based Normal Mapping for Robust Monte Carlo path Tracing V. Schussler, E. Heitz, J. Hanika, C. Dachsbacher.

PR contains complete code and test scenes:

  1. Main source code in located in src/appleseed/renderer/modeling/bsdf/microacetbrdfwrapper.h.
  2. Test scenes are located in sandbox/tests/test scenes/basis modifiers/.

Problem description

Regular normal mapping causes several problems for Monte Carlo path tracing:

  1. Non-Symmetric BRDF due to basis construced using shading normals sampled from normal map.
  2. Tilting of the positive hemisphere of outgoing directions due to shading normals (ws) sampled from normal map which cause inconsistencies with geometric (wg) hemisphere. This effect is visible as black fringes on final render.
  3. Violation of energy conservation. hemisphere_tilt

Solution introduction

Authors introduced microfacet based surface model. Profile of this surface model contains two facets per shading point. Instead of just replacing geometric normal, the sampled shading normal (perturbed normal) from normal map determines the orientation of one of the facets (perturbed facet, wp). Other facet is used so that the average microfacet normal in shading point equals the geometric normal (tangent facet, wt).

profile

Other properties of this model are similar to microfacet models: distribution of normals, projected areas and intersection probabilities, masking and shadowing function. Using this properties macrosurface single and multiple scattering BRDF is derived with assumption of arbitrary perturbed and tangent facet BRDF.

Multiple scattering BRDF model evaluation and sapling is solved using random walk with arbitrary amount of scatterings. In shading point, perturbed facet has BRDF specified by the user. For example, if user decides on glossy BRDF with particular normal map, then perturbed facet will contain glossy BRDF and the direction of perturbed normal map will ne equal to sampled normal from that normal map. Tangent facet, on the other hand, can be more arbitrary. One choice is that tangent facet can contain same BRDF as perturbed facet. Other choice is that tangent facet has the specular BRDF (note that tangent facet has the little influence as possible, it is only used so that average normal in shading point equals to geometrical normal).

The case where tangent facet has specular BRDF is chosen in this implementation. There are several reasons for that:

  1. Authors noted that tangent facet with specular BRDF removes most artefacts and produces results close to classical normal mapping.
  2. Random walk algorithm can be simplified to the analytical model with 2nd order scattering which can be evaluated fast.

Analytical model with 2nd order scattering has three distinct cases due to two facets per shading normal:

  1. Outgoing direction hitting perturbed facet and reflecting in incoming direction (IPO case).
  2. Outgoing direction hitting perturbed facet, reflecting in tangent facet and reflecting in incoming direction (IPTO case).
  3. Outgoing direction hitting tangent facet, reflecting in perturbed facet and reflecting in incoming direction (ITPO case).

Implementation

As discussed this PR contributes with analytical 2nd order scattering model for microfacet based normal mapping for more robust normal mapping. Implementation is based on:

  1. Analytical single and double scattering given in Equation 23
  2. Random walk on the microsurface with specular tangent facet given in Algorithm 2

Idea was that microfacet based normal mapping can be applied to arbitrary BRDF (note R for reflective materials). Therefore it is implemented as wrapper agnostic of particular BRDF. Main implementation is in microfacetbrdfwrapper.h as MicrofacetBRDFWRapper class located with other BRDFs in src/appleseed/renderer/modeling/bsdf.

MicrofacetBRDFWRapper is a template class which can be specified with arbitrary BRDF. This class has access to BRDF::sample(), BRDF::evaluate() and BRDF::evaluate_pdf(). Using these BRDF functions it is reimplementing them in MicrofacetBRDFWRapper::sample(), MicrofacetBRDFWRapper::evaluate() and MicrofacetBRDFWRapper::evaluate_pdf() so that microfacet based normal mapping is applied.

Microfacet normal mapping wrapper is currently (this PR) added to metal, glossy, plastic, lambertian, blinn and sheen BRDFs. To add microfacet normal mapping to new BRDF it is recommended to see how it is done in this PR. General steps are as follow (with metalBRDF as an example):

  1. Add microfacet normal mapping wrapper to new BRDF: typedef MicrofacetBRDFWrapper<MetalBRDFImpl> MicrofacetMetalBRDF; (metalbrdf.cpp)
  2. Add MicrofacetMetalBRDFFactory which is constructing MicrofacetMetalBRDF (metalbrdf.h and metalbrdf.cpp)
  3. Register MicrofacetMetalBRDFFactory in bsdffactoryregistar.cpp (steps, for now, enable usage of microfacet based normal mapping for metal BRDF in appleseed built-in system -- effectively new BRDF is added, but this BRDF is actually using metalBRDF with microfacet normal mapping wrapper. See test scenes for usage)
  4. Register MicrofacetMetalID in closures.h
  5. Add MicrofacetMetalClosure in closures.cpp
  6. Add as_microfacet_metal closure in as_osl_extensions.h.in
  7. Register MicrofacetMetalBRDF in oslbsdf.cpp
  8. Create as_microfacet_metal.osl to use it in scene construction (These steps allow using microfacet normal mapping wrapped metalBRDF as new closure: as_microfacet_metal. See test scenes for usage)

Results

With this contribution, black areas due to regular normal mapping are fixed. Authors have provided code in Mitsuba which was used to compare the results.

Appleseed, original normal mapping: appleseed_circle2_original_area_metal

Appleseed, microfacet based normal mapping: appleseed_circle2_microfacet_area_metal

Mitsuba, microfacet based normal mapping: mitsuba_metal_point_mirofacet_circl2

Important note:

Issue: Double wrapping of child BSDFs in BSDFBlend, BSDFMix and OSLBSDF #1243 is also affecting the results when microfacet normal mapping OSL closure is used. This PR: Fixing double wrapping for OSLBRDF children #2889 is fixing the problem of double wrapping for OSLBSDF children. Therefore, PR #2889 should be merged after the current PR.

LZaw commented 4 years ago

When loading the test scenes as they are, currently the model is set to the respective "standard" model, even though the microfacet version is set in the appleseed file. When loading the 26 - normal mapping - metal brdf - point light.appleseed, I get the following BSDF setting: 26 - normal mapping - metal brdf - point light

lorentzo commented 4 years ago

When loading the test scenes as they are, currently the model is set to the respective "standard" model, even though the microfacet version is set in the appleseed file. When loading the 26 - normal mapping - metal brdf - point light.appleseed, I get the following BSDF setting: 26 - normal mapping - metal brdf - point light

OLD:

OK. I have to investigate this bit more. It seems that only the label is wrong. Regardless of that microfacet normal mapping is applied. So this is just a minor GUI-related bug.

NEW:

Intro

I will use metal_brdf and its microfacet normal mapping alternative microfacet_normal_mapping_metal_brdf for description of the problem but the same idea applies to other BRDFs and its microfacet based normal mapping alternatives. The problem is following:

I think I have found the source of the problem. First, I will describe how the problem occurs. Then, I will propose (a simple) solution (and maybe not the best one).

Problem occurance description

  1. appleseed project file contains microfacet_normal_mapping_metal_brdf as BRDF in assembly. Example:

    ...
    <bsdf name="my_metal_BRDF" model="microfacet_normal_mapping_metal_brdf">
    <parameter name="anisotropy" value="0.0" />
    <parameter name="edge_tint" value="0.0" />
    <parameter name="normal_reflectance" value="0.92" />
    <parameter name="reflectance_multiplier" value="1.0" />
    <parameter name="roughness" value="0.3" />
    </bsdf>
    ...
  2. Opening project file in GUI calls XML project file reader, which (among others) reads "microfacet_normal_mapping_metal_brdf" tag and calls the corresponding factory which is MicrofacetMetalBRDFFactory.

  3. MicrofacetMetalBRDFFactory creates metalBRDF (my_metal_BRDF) wrapped with microfacetBRDFwrapper (MicrofacetBRDFWrapper<MetalBRDFImpl>). Note that created my_metal_BRDF has "metal_brdf" model name (my_metal_BRDF.get_model()). my_metal_BRDF is added to assembly.

  4. GUI then displays assembly elements using get_model() call. In particular: my_metal_BRDF.get_model() call. Which results in "metal_brdf" and not "microfacet_normal_mapping_metal_brdf"! (see step 3).

  5. When GUI rendering is performed, we will get a microfacet based normal mapping result because my_metal_BRDF is wrapped with MicrofacetBRDFWrapper (see step (3)).

  6. When we use GUI to save this file, XML project writer is called, which writes assembly elements to the file. Saved file will contain "metal_brdf" and not "microfacet_normal_mapping_metal_brdf". This is because my_metal_BRDF.get_model() is called which returns "metal_brdf" and not "microfacet_normal_mapping_metal_brdf"!

Solution proposal:

The main problem is that MicrofacetMetalBRDFFactory has different model name than the model name of BRDF it creates: MicrofacetMetalBRDFFactory.get_name() = "microfacet_normal_mapping_metal_brdf" vs MetalBRDFImpl.get_name() = "metal_brdf".

Idea is to have enumeration: {metal_brdf, microfacet_normal_mapping_metal_brdf}. And in MetalBRDFFactory::create() as well as MicrofacetMetalBRDFFactory::create() set Model variable to metal_brdf or microfacet_normal_mapping_metal_brdf respectively.

This still has to be tested! Also, a better solution might be always possible.

UPDATE

I can confirm that the above problem description is correct. Simply replacing Model variable containing "metal_brdf" with MicrofacetModel containing "microfacet_normal_mapping_metal_brdf" in MetalBRDFImpl::get_model() results in correct reading and writing of microfacet based normal mapping brdf. Of course, this is not a solution, just a test. Therefore, I am proposing new solution which seems quite elegant.

New solution proposal

(1) Create MetalBRDFImplMicrofacet class which is inheriting MetalBRDFImpl class. MetalBRDFImplMicrofacet has all the same functions except MetalBRDFImplMicrofacet::get_model(). Function MetalBRDFImplMicrofacet::get_model() must return MicrofacetModel variable instead of Model variable. (2) Use MetalBRDFImplMicrofacet class as a child class for MicrofacetBRDFWrapper.

Example in code. Only change is required in metalbrdf.cpp:

  1. Create MetalBRDFImplMicrofacet as described above:
class MetalBRDFImplMicrofacet : public MetalBRDFImpl
{
    using MetalBRDFImpl::MetalBRDFImpl;

    const char* get_model() const override
    {
        return MicrofacetModel;
    }
};
  1. Use MetalBRDFImplMicrofacet class as a child class for MicrofacetBRDFWrapper.

typedef MicrofacetBRDFWrapper<MetalBRDFImplMicrofacet> MicrofacetMetalBRDF;