Search Unity

Weird visual glitch with custom Cook-Torrance shader

Discussion in 'Shaders' started by PlazmaInteractive, Aug 1, 2017.

  1. PlazmaInteractive

    PlazmaInteractive

    Joined:
    Aug 8, 2016
    Posts:
    114
    I've been working with PBR for a good amount of time and have kind of successfully implemented a version based on Unity's own standard shader, that is, using a Visibility term (as opposed to Geometric) and a Torrance-Sparrow microfacet model. I decided to go with that approach because I was having trouble implementing a Cook-Torrance version.

    But now, I decided to try again being more experienced with shaders. After I looked through various codes in GitHub and other sources, I wrote the shader in Unity and the result comes out looking weird again like before. I can't really pinpoint where the error is coming from but if I were to guess, it's probably a dot product calculation doing it but I don't really know. I'm using:
    1. Trowbridge-Reitz for Distribution
    2. Schlick-GGX for Geometric
    3. Schlick for Fresnel

    I'm pretty sure everything was implemented correctly because the components above looked correct when I output them individually (proofs). Only when I tried to multiply them together using the Cook-Torrance formula it comes out looking weird.
    Cook-Torrance.png
    Let me know if you find what's causing this error. Here's the code:
    Code (CSharp):
    1. Shader "ShaderChallenge/Cook-Torrance"
    2. {
    3.     Properties
    4.     {
    5.         _Albedo ("Albedo", Color) = (1.0, 1.0, 1.0, 1.0)
    6.         _Roughness ("Roughness", Range(0.0, 1.0)) = 1.0
    7.         _Metallic ("Metallic", Range(0.0, 1.0)) = 0.0
    8.     }
    9.     SubShader
    10.     {
    11.         Pass
    12.         {
    13.             Tags { "LightMode" = "ForwardBase" }
    14.  
    15.             CGPROGRAM
    16.             #pragma vertex vert
    17.             #pragma fragment frag
    18.             #include "UnityStandardBRDF.cginc"
    19.  
    20.             #define pi 3.14159265359
    21.  
    22.             float4 _Albedo;
    23.             float _Roughness;
    24.             float _Metallic;
    25.  
    26.             struct v2f
    27.             {
    28.                 float4 pos : SV_POSITION;
    29.                 float3 worldPos : TEXCOORD0;
    30.                 float3 normal : TEXCOORD1;
    31.             };
    32.  
    33.             v2f vert(appdata_base v)
    34.             {
    35.                 v2f o;
    36.                 o.pos = UnityObjectToClipPos(v.vertex);
    37.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    38.                 o.normal = v.normal;
    39.                 return o;
    40.             }
    41.  
    42.             float DistributionGGX(float3 n, float3 h)
    43.             {
    44.                 float a = pow(_Roughness, 2.0);
    45.                 float a2 = pow(a, 2.0);
    46.                 float nh = max(dot(n, h), 0.0);
    47.                 float nh2 = pow(nh, 2.0);
    48.  
    49.                 float nom = a2;
    50.                 float denom = (nh2 * (a2 - 1.0) + 1.0);
    51.                 denom = pi * pow(denom, 2.0);
    52.  
    53.                 return nom / denom;
    54.             }
    55.  
    56.             float GeometrySchlickGGX(float cosTheta)
    57.             {
    58.                 float r = (_Roughness + 1.0);
    59.                 float k = pow(r, 2.0) / 8.0;
    60.  
    61.                 float nom = cosTheta;
    62.                 float denom = cosTheta * (1.0 - k) + k;
    63.  
    64.                 return nom / denom;
    65.             }
    66.  
    67.             float GeometrySmith(float3 n, float3 v, float3 l)
    68.             {
    69.                 float nv = max(dot(n, v), 0.0);
    70.                 float nl = max(dot(n, l), 0.0);
    71.                 float ggx1 = GeometrySchlickGGX(nv);
    72.                 float ggx2 = GeometrySchlickGGX(nl);
    73.  
    74.                 return ggx1 * ggx2;
    75.             }
    76.  
    77.             float3 FresnelSchlick(float cosTheta, float3 F0)
    78.             {
    79.                 return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
    80.             }
    81.            
    82.             float4 frag(v2f i) : SV_TARGET
    83.             {
    84.                 float3 n = normalize(i.normal);
    85.                 float3 l = normalize(_WorldSpaceLightPos0.xyz);
    86.                 float3 v = normalize(_WorldSpaceCameraPos - i.worldPos.xyz);
    87.                 float3 h = normalize(l + v);
    88.  
    89.                 float3 F0 = 0.04;
    90.                 F0 = lerp(F0, _Albedo.rgb, _Metallic);
    91.  
    92.                 float D = DistributionGGX(n, h);
    93.                 float G = GeometrySmith(n, v, l);
    94.                 float F = FresnelSchlick(max(dot(n, v), 0.0), F0);
    95.  
    96.                 float3 specular = D * G * F / (4.0 * max(dot(n, v), 0.0) * max(dot(n, l), 0.0));
    97.                 return float4(specular, 1.0);
    98.             }
    99.             ENDCG
    100.         }
    101.     }
    102. }
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Seems about right for a high roughness Torrance-Sparrow, honestly.
     
  3. PlazmaInteractive

    PlazmaInteractive

    Joined:
    Aug 8, 2016
    Posts:
    114
    What do you mean by that? You mean the result it's showing is actually correct? The result is showing a weird split in the sphere, where the first half of it looks correct but the second half is showing completely black. I'm pretty sure that's not how it should look like.

    EDIT: I found an example of what I'm expecting Cook-Torrance to look like from this website.
    Specular.png
     
    Last edited: Aug 2, 2017
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Several distribution functions for Cook-Torrance or Torrance-Sparrow have a very bright concave shape at high roughness values, I assumed that's what you were getting ... but I hadn't actually looked at the shader code.
    I realize it's just because you're applying the cook-torrance specular model's denominator which is unnecessary with the GGX distribution function you're using. Notice your "DistributionGGX" function has a "denom" variable, that's an half angle version of the Cook-Torrance denominator, using Pi instead of 4 (which is apparently more correct).

    TLDR; use:
    specular = D * G * F;

    The / (4 * max(dot(n, v), 0.0) * max(dot(n, l), 0.0)) isn't correct here.
     
    neoshaman likes this.
  5. PlazmaInteractive

    PlazmaInteractive

    Joined:
    Aug 8, 2016
    Posts:
    114
    Ahh yeah, that does fix the issue I'm having. Do you know which distribution function would need the Cook-Torrance's denominator if I use it? Also I'm wondering, if that's the case here, why does this tutorial still calculate with the denominator? My shader is based off their code which you can find if you scroll down.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Cook-Torrance is traditionally either Beckman or Blinn Phong based for the distribution function.
     
  7. MarcAllozaAyxendri

    MarcAllozaAyxendri

    Joined:
    Sep 9, 2022
    Posts:
    2
    If I'm not wrong, you missed the N·L component of the formula: (numerator/denominator)*(N·L)*LightRGB;