Search Unity

Am I calculating my physically-based shader correctly?

Discussion in 'Shaders' started by PlazmaInteractive, Apr 11, 2017.

  1. PlazmaInteractive

    PlazmaInteractive

    Joined:
    Aug 8, 2016
    Posts:
    114
    I know this is my third forum regarding this but this is gonna be the last one. So I rewrote my code and this time, the result looked better. It doesn't make the material completely black if the roughness is 1 but, I'm still not sure if this is the desired effect.

    Here are some results with different properties. If you see the image with 1 roughness and 1 metallic, the material still does look dark but if you look closely, you can see some texture so it's not completely black.

    The code:
    Code (csharp):
    1. Shader "ShaderChallenge/CustomPBR"
    2. {
    3.     Properties
    4.     {
    5.         _Albedo("Albedo", 2D) = "white" {}
    6.         _Roughness("Roughness", Range(0, 1)) = 1.0
    7.         _Metallic("Metallic", Range(0, 1)) = 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.             sampler2D _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.                 float2 uv : TEXCOORD2;
    32.             };
    33.  
    34.             v2f vert(appdata_base v)
    35.             {
    36.                 v2f o;
    37.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    38.                 // Convert vertex position to world space
    39.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    40.                 // Normal vector
    41.                 o.normal = UnityObjectToWorldNormal(v.normal);
    42.                 o.uv = v.texcoord;
    43.                 return o;
    44.             }
    45.  
    46.             // Oren-Nayar
    47.             float OrenNayar(float3 n, float3 v, float3 l)
    48.             {
    49.                 float nl = dot(n, l);
    50.                 float nv = dot(n, v);
    51.  
    52.                 float anglenl = acos(nl);
    53.                 float anglenv = acos(nv);
    54.  
    55.                 float alpha = max(anglenv, anglenl);
    56.                 float beta = min(anglenv, anglenl);
    57.                 float gamma = dot(v - n * nv, l - n * nl);
    58.  
    59.                 float a2 = pow(_Roughness, 2.0);
    60.  
    61.                 float A = 1.0 - 0.5 * (a2 / (a2 + 0.57));
    62.  
    63.                 float B = 0.45 * (a2 / (a2 + 0.09));
    64.  
    65.                 float C = sin(alpha) * tan(beta);
    66.  
    67.                 float result = max(0.0, nl) * (A + B * max(0.0, gamma) * C);
    68.  
    69.                 return result;
    70.             }
    71.  
    72.             float DistributionGGX(float3 n, float3 h)
    73.             {
    74.                 float a2 = pow(_Roughness, 2.0);
    75.                 float nh = dot(n, h);
    76.                 float nh2 = pow(nh, 2.0);
    77.  
    78.                 float num = a2;
    79.                 float den = (nh2 * (a2 - 1.0) + 1.0);
    80.                 den = pi * pow(den, 2.0);
    81.  
    82.                 return num / den;
    83.             }
    84.          
    85.             float GeometrySchlickGGX(float dot, float k)
    86.             {
    87.             float num = dot;
    88.             float den = dot * (1.0 - k) + k;
    89.  
    90.             return num / den;
    91.             }
    92.  
    93.             float GeometrySmith(float3 n, float3 v, float3 l)
    94.             {
    95.                 float k = pow(_Roughness + 1.0, 2.0) / 8.0;
    96.  
    97.                 float nv = DotClamped(n, v);
    98.                 float nl = DotClamped(n, l);
    99.                 float ggx1 = GeometrySchlickGGX(nv, k);
    100.                 float ggx2 = GeometrySchlickGGX(nl, k);
    101.  
    102.                 return ggx1 * ggx2;
    103.             }
    104.  
    105.             float FresnelSchlick(float3 n, float3 v, float3 albedo)
    106.             {
    107.                 float cosTheta = dot(n, v);
    108.                 float3 F0 = 0.04;
    109.                 F0 = lerp(F0, albedo, _Metallic);
    110.  
    111.                 return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
    112.             }
    113.  
    114.             float Microfacet(float3 n, float3 h, float3 v, float3 l, float3 albedo)
    115.             {
    116.                 float d = DistributionGGX(h, n);
    117.                 float g = GeometrySmith(n, v, l);
    118.                 float f = FresnelSchlick(n, v, albedo);
    119.  
    120.                 return (d * g * f) / 4.0 * DotClamped(n, l) * DotClamped(n, v) + 0.001;
    121.             }
    122.  
    123.             float4 frag(v2f i) : SV_TARGET
    124.             {
    125.                 float3 lightCol = _LightColor0.rgb;
    126.  
    127.                 // Normalize the normal
    128.                 float3 n = normalize(i.normal);
    129.                 // Light vector from mesh's surface
    130.                 float3 l = normalize(_WorldSpaceLightPos0.xyz);
    131.                 // Viewport(camera) vector from mesh's surface
    132.                 float3 v = normalize(_WorldSpaceCameraPos - i.worldPos.xyz);
    133.                 // Halfway vector
    134.                 float3 h = normalize(l + v);
    135.  
    136.                 float3 albedo = tex2D(_Albedo, i.uv).rgb;
    137.                 float3 ambient = unity_AmbientSky;
    138.  
    139.                 float3 specular = Microfacet(n, h, v, l, albedo);
    140.  
    141.                 float3 diffuse = ambient + OrenNayar(n, v, l);
    142.                 diffuse *= 1.0 - _Metallic;
    143.  
    144.                 float3 color = albedo * (diffuse * lightCol) + (specular * lightCol);
    145.  
    146.                 return float4(color, 1.0);
    147.             }
    148.             ENDCG
    149.         }
    150.     }
    151. }
     
  2. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Not really an answer to your question, but I wrote a little piece on how to optimize Oren-Nayar some time ago:
    http://shaderjvo.blogspot.nl/2011/08/van-ouwerkerks-rewrite-of-oren-nayar.html

    It replaces all trigonometric instructions (sin, acos, tan) with a single sqrt. It also removes two max instructions and the one min instruction. It's mathematically the same as the original, but due to rounding there can be small differences. I've measured that to be in the range of 0.01%, so nothing noticeable. But it really is a lot GPU friendlier.

    Applied to your code it would look like:
    Code (csharp):
    1.  
    2. float OrenNayar(float3 n, float3 v, float3 l)
    3. {
    4.    float roughness2 = _Roughness * _Roughness;
    5.    float2 oren_nayar_fraction = roughness2 / (roughness2 + float2(0.33, 0.09));
    6.    float2 oren_nayar = float2(1, 0) + float2(-0.5, 0.45) * oren_nayar_fraction;
    7.    // Theta and phi
    8.    float2 cos_theta = saturate(float2(dot(n, l), dot(n, v)));
    9.    float2 cos_theta2 = cos_theta * cos_theta;
    10.    float sin_theta = sqrt((1-cos_theta2.x) * (1-cos_theta2.y));
    11.    float3 light_plane = normalize(light - cos_theta.x * normal);
    12.    float3 view_plane = normalize(view - cos_theta.y * normal);
    13.    float cos_phi = saturate(dot(light_plane, view_plane));
    14.    // Composition
    15.    float diffuse_oren_nayar = cos_phi * sin_theta / max(cos_theta.x, cos_theta.y);
    16.    float diffuse = cos_theta.x * (oren_nayar.x + oren_nayar.y * diffuse_oren_nayar);
    17.    return diffuse;
    18. }
    19.  
    Edit: The code is a bit old and assumes vector units. For current desktop GPU's it could be optimized a little more.
     
  3. XIV-Studios

    XIV-Studios

    Joined:
    Aug 20, 2012
    Posts:
    8
    Hey, sorry I am not actually able to test and, and this certainly is not physically correct, but you are missing the ambient specular light from a refection probe or the sky. I just through this together so you can see the difference, so optimize and correct the bdrf accordingly.


    Code (CSharp):
    1. // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
    2.  
    3. Shader "ShaderChallenge/CustomPBR"
    4. {
    5.     Properties
    6.     {
    7.         _Albedo("Albedo", 2D) = "white" {}
    8.         _Roughness("Roughness", Range(0, 1)) = 1.0
    9.         _Metallic("Metallic", Range(0, 1)) = 0.0
    10.     }
    11.     SubShader
    12.     {
    13.         Pass
    14.         {
    15.             Tags { "LightMode" = "ForwardBase" }
    16.             CGPROGRAM
    17.             #pragma vertex vert
    18.             #pragma fragment frag
    19.             #include "UnityStandardBRDF.cginc"
    20.             #define pi 3.14159265359
    21.             sampler2D _Albedo;
    22.             float _Roughness;
    23.             float _Metallic;
    24.             struct v2f
    25.             {
    26.                 float4 pos : SV_POSITION;
    27.                 float3 worldPos : TEXCOORD0;
    28.                 float3 normal : TEXCOORD1;
    29.                 float2 uv : TEXCOORD2;
    30.                 half3 worldRefl: TEXCOORD3;
    31.             };
    32.             v2f vert(appdata_base v)
    33.             {
    34.                 v2f o;
    35.                 o.pos = UnityObjectToClipPos(v.vertex);
    36.                 // Convert vertex position to world space
    37.                 float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
    38.                
    39.                 o.worldPos = worldPos;
    40.                 float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
    41.                 // Normal vector
    42.                 float3 worldNormal = UnityObjectToWorldNormal(v.normal);
    43.                 o.normal = worldNormal;
    44.                 o.worldRefl = reflect(-worldViewDir, worldNormal);
    45.                 o.uv = v.texcoord;
    46.                 return o;
    47.             }
    48.             // Oren-Nayar
    49.             float OrenNayar(float3 n, float3 v, float3 l)
    50.             {
    51.                 float nl = dot(n, l);
    52.                 float nv = dot(n, v);
    53.                 float anglenl = acos(nl);
    54.                 float anglenv = acos(nv);
    55.                 float alpha = max(anglenv, anglenl);
    56.                 float beta = min(anglenv, anglenl);
    57.                 float gamma = dot(v - n * nv, l - n * nl);
    58.                 float a2 = pow(_Roughness, 2.0);
    59.                 float A = 1.0 - 0.5 * (a2 / (a2 + 0.57));
    60.                 float B = 0.45 * (a2 / (a2 + 0.09));
    61.                 float C = sin(alpha) * tan(beta);
    62.                 float result = max(0.0, nl) * (A + B * max(0.0, gamma) * C);
    63.                 return result;
    64.             }
    65.             float DistributionGGX(float3 n, float3 h)
    66.             {
    67.                 float a2 = pow(_Roughness, 2.0);
    68.                 float nh = dot(n, h);
    69.                 float nh2 = pow(nh, 2.0);
    70.                 float num = a2;
    71.                 float den = (nh2 * (a2 - 1.0) + 1.0);
    72.                 den = pi * pow(den, 2.0);
    73.                 return num / den;
    74.             }
    75.        
    76.             float GeometrySchlickGGX(float dot, float k)
    77.             {
    78.                 float num = dot;
    79.                 float den = dot * (1.0 - k) + k;
    80.    
    81.                 return num / den;
    82.             }
    83.             float GeometrySmith(float3 n, float3 v, float3 l)
    84.             {
    85.                 float k = pow(_Roughness + 1.0, 2.0) / 8.0;
    86.                 float nv = DotClamped(n, v);
    87.                 float nl = DotClamped(n, l);
    88.                 float ggx1 = GeometrySchlickGGX(nv, k);
    89.                 float ggx2 = GeometrySchlickGGX(nl, k);
    90.                 return ggx1 * ggx2;
    91.             }
    92.             float FresnelSchlick(float3 n, float3 v, float3 albedo)
    93.             {
    94.                 float cosTheta = dot(n, v);
    95.                 float3 F0 = 0.04;
    96.                 F0 = lerp(F0, albedo, _Metallic);
    97.                 return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
    98.             }
    99.             float Microfacet(float3 n, float3 h, float3 v, float3 l, float3 albedo)
    100.             {
    101.                 float d = DistributionGGX(h, n);
    102.                 float g = GeometrySmith(n, v, l);
    103.                 float f = FresnelSchlick(n, v, albedo);
    104.                 return (d * g * f) / 4.0 * DotClamped(n, l) * DotClamped(n, v) + 0.001;
    105.             }
    106.             float4 frag(v2f i) : SV_TARGET
    107.             {
    108.                 float3 lightCol = _LightColor0.rgb;
    109.                 // Normalize the normal
    110.                 float3 n = normalize(i.normal);
    111.                 // Light vector from mesh's surface
    112.                 float3 l = normalize(_WorldSpaceLightPos0.xyz);
    113.                 // Viewport(camera) vector from mesh's surface
    114.                 float3 v = normalize(_WorldSpaceCameraPos - i.worldPos.xyz);
    115.                 // Halfway vector
    116.                 float3 h = normalize(l + v);
    117.                 float3 albedo = tex2D(_Albedo, i.uv).rgb;
    118.                 float3 ambient = unity_AmbientSky;
    119.                 float3 specular = Microfacet(n, h, v, l, albedo);
    120.                
    121.                 float4 probe = UNITY_SAMPLE_TEXCUBE_LOD (unity_SpecCube0, i.worldRefl, _Roughness * 6);
    122.                 specular.xyz += DecodeHDR(probe, unity_SpecCube0_HDR);
    123.                
    124.                 float3 diffuse = ambient + OrenNayar(n, v, l);
    125.                 diffuse *= 1.0 - _Metallic;
    126.                 float3 color = albedo * (diffuse * lightCol) + (specular * lightCol);
    127.                 return float4(color, 1.0);
    128.             }
    129.             ENDCG
    130.         }
    131.     }
    132. }
     
  4. PlazmaInteractive

    PlazmaInteractive

    Joined:
    Aug 8, 2016
    Posts:
    114
    I added in the code quickly just to see the result and you're right that there's not much of a difference in visual appearances. I assume you were the one who came up with this right? If so, I'll be citing your work if I use it after I studied the original BRDF and your rewrite. Thanks for this :)
     
  5. PlazmaInteractive

    PlazmaInteractive

    Joined:
    Aug 8, 2016
    Posts:
    114
    Well, I haven't added in reflection into my code because I wanted to make sure all of the calculations were correct first. But anyways, I added it in and noticed there were some differences between my custom shader and Unity's standard shader. Here are all four results that I compiled together showing the differences and similarities. Mine is the sphere in the left while the standard shader is the one in the right. The one showing a major difference is in the third image which is the one I've been trying to fix for days...
     
  6. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Yes, that page is the result of my take on trying to optimize the Oren-Nayar BRDF. I had a feeling it could be more GPU friendly and after some reformulating it turned out it can be.
     
  7. PlazmaInteractive

    PlazmaInteractive

    Joined:
    Aug 8, 2016
    Posts:
    114
    Alright, thanks again for sharing it!