Saturday, April 30, 2016

Writing Shaders for Substance Painter part 1

Introduction


Hi, in this article I’ll try to explain to you how Substance Painter surface-lighting interaction works, what modifications I added to it and why I made them..


Image based lighting


To calculate lighting reflected from a point, you need to sum the incident lighting from all of the directions ωi:
In math language this is integration:
where in а simple form Li is a property of incoming light, such as color and intensity and BRDF is a function that describes surface-light interaction and n ∙ ωi is basically a cosine of the angle between light and surface normal vectors, which describes the distribution of incident light over an area. 
   Usually the source of Li is HDR Environment map and BRDF function is identified. So all we need is to solve the integration, there are several ways to do it but generally there are two most common methods:

 -   Split sum approximation

 used by the most modern game engines. The basic idea is to pre-compute lighting distribution and bake it into a cube map (that would be for light color and intensity), then pre-compute BRDF for all directions , considering Li=1 and view and normal vectors are collinear. Such assumptions lead to several errors in light distribution, specular lobe symmetry around reflection vector,

Jim Blinn would be disappointed

 and that means no stretchy reflections like this:
with Split Sum approach all of the reflections would be ugly isotropic circles under light sources.

Modern games try to avoid this by using analytical(sometimes area) lights for important places, UE4 and Frostbyte using importance sampled SSR.
Still this approach was a major step to photorealism and we got something that tries to be physical instead of nothing. You may read more about that here:[Lazarov2013] and here:[Karis2013]

- Numerical Integration

another way to do this is to look how it works in the real world, to actually sum every light ray and calculate its reflection coefficient for a chosen direction. You may do this brute force, or use a smarter method called importance sampling. What you need is a normal distribution function , which would give you the distribution of reflected rays , Fresnel equation for each pair of rays(for specular) and  albedo multiplied by Lambert's(or whateverYouWant diffuse) law. Also you need to check visibility and shadowing of the calculated ray. 
The drawback of that method is performance, you may need an enormous amount of rays to get good result if the lighting contrast is immense(the sun has 1 million times greater luminance than the sky in zenith for example).



Importance Sampling and Substance

Substance Painter uses importance sampling to simulate specular reflections. That allows it to have quality reflections without pre-compute. So, that can give us a possibility to use any shading we want. But when I tried to write custom shaders I faced some problems:
as we know GGX gives us a uniform hemisphere distribution when roughness = 1. So, it should look close to Lambertian surface. However, it isn’t even close. Moreover, in mid roughness range reflections look blurry, plastic and that is not what GGX is about. When I started to dig deeper into the code, I found that Substance aggressively downscales Env map to get performance, so that led to ugly results.

 By the way, you can get every shading function just by opening "Substance Painter 2.exe" in any text editor, such as notepad++, 
all of the libs are there:
    -lib-alpha.glsl : contains opacity related helpers
    -lib-defines.glsl : contains useful math constants
    -lib-env.glsl : contains environment map related helpers
    -lib-normal.glsl : contains normal map related helpers (and height-map generated normal map)
    -lib-pbr.glsl : contains physically based rendering helpers
    -lib-random.glsl : contains random utilities (hammersley sequence)
    -lib-sampler.glsl : contains channel getters helpers
    -lib-utils.glsl : contains color utility functions (sRGB conversions, tone mapping) 

    When I forced it to give me 0 mip it produced me such a picture:
    Awwww! No per-pixel random for reflection vectors! 

    From that part I understood that it would be easier to re-write from scratch.
    I've added Hammersley 2d point set with per pixel random (it turns out to be faster to compute it realtime than read from the texture like Substance does).
    The whole method is described here:[Karis2013] and here:[Lagarde2014]

    So now it looks like this:
    Yes, that's what i wanted.

    But! It's noisy and the performance is really poor. Reading 4k HDR image 64 times is not the best idea.
    For example, on my GTX970 frame times is:  

    0 mip = 55ms(!) that is 18 fps 
    1 mip = 40ms
    2 mip = 22ms
    3 mip = less than 15ms, that is 60+ fps

    There is no point for reading 4k texture for reflection in 99% percent of cases but you shouldn't be as fanatic as a Substance and read the last mip levels so that you lose all of your material appearance.
    I clamped the min mip level and choosed the max level depending on roughness. As we know the GGX distribution when roughness --> 1 is equal to a uniform diffuse, we can use diffuse irradiance as incoming light when roughness is 1 in the limit. That greatly reduces noise preserving material behaviour:



    2 comments: