I'm trying to implement physically-based rendering (PBR) in our project (we started a small game engine for academic and learning purposes) and I cannot understand what is the right way to calculate specular and diffuse contribution based on material's metallic and roughness.
We don't use any third party libraries/engines for rendering, everything is hand written in OpenGL 3.3.
Right now I have this (I'll put the full code below):
// Calculate contribution based on metallicity
vec3 diffuseColor = baseColor - baseColor * metallic;
vec3 specularColor = mix(vec3(0.00), baseColor, metallic);
But I'm under the impression that specular term has to be depended by roughness somehow. I was thinking to change it to this:
vec3 specularColor = mix(vec3(0.00), baseColor, roughness);
But again, I'm not sure. What is the right way to do it? Is there even a right way or should I just use the 'trial and error' method until I get a satisfying result?
Here is the full GLSL code:
// Calculates specular intensity according to the Cook - Torrance model
float CalcCookTorSpec(vec3 normal, vec3 lightDir, vec3 viewDir, float roughness, float F0)
{
// Calculate intermediary values
vec3 halfVector = normalize(lightDir + viewDir);
float NdotL = max(dot(normal, lightDir), 0.0);
float NdotH = max(dot(normal, halfVector), 0.0);
float NdotV = max(dot(normal, viewDir), 0.0); // Note: this could also be NdotL, which is the same value
float VdotH = max(dot(viewDir, halfVector), 0.0);
float specular = 0.0;
if(NdotL > 0.0)
{
float G = GeometricalAttenuation(NdotH, NdotV, VdotH, NdotL);
float D = BeckmannDistribution(roughness, NdotH);
float F = Fresnel(F0, VdotH);
specular = (D * F * G) / (NdotV * NdotL * 4);
}
return specular;
}
vec3 CalcLight(vec3 lightColor, vec3 normal, vec3 lightDir, vec3 viewDir, Material material, float shadowFactor)
{
// Helper variables
vec3 baseColor = material.diffuse;
vec3 specColor = material.specular;
vec3 emissive = material.emissive;
float roughness = material.roughness;
float fresnel = material.fresnel;
float metallic = material.metallic;
// Calculate contribution based on metallicity
vec3 diffuseColor = baseColor - baseColor * metallic;
vec3 specularColor = mix(vec3(0.00), baseColor, metallic);
// Lambertian reflectance
float Kd = DiffuseLambert(normal, lightDir);
// Specular shading (Cook-Torrance model)
float Ks = CalcCookTorSpec(normal, lightDir, viewDir, roughness, fresnel);
// Combine results
vec3 diffuse = diffuseColor * Kd;
vec3 specular = specularColor * Ks;
vec3 result = lightColor * (emissive + diffuse + specular);
return result * (1.0 - shadowFactor);
}
What you are looking for is the bidirectional reflectance distribution function (BRDF) for a material. In your code you reference the "Cook - Torrance model" which is a common and effective (but also computationally expensive) BRDF. It seems like you might be getting ideas from both "metallic/roughness" model and the "specular/glossiness" model. This is a huge topic but understanding the two might help.
Anyway, in a physically based shading model the BRDF must conserve energy. Therefore the contribution of diffuse + specular must not exceed 1 or:
Kd = 1 - Ks
The physical accuracy of your shaders are dependent on the computations you perform on the material properties, but in your case you can incorporate the metallic term into the BRDF like this:
BRDF = (1-m)*diffuse + m*specular
From here you can handle the lighting etc.
-- Metalness/Roughness shader origins
Disney came up with a shader method that was more realistic. UnrealEngine4 implemented this model-ish and now there is a lot of confusion around terminology and texture workflow.
UE4 BRDF code - signup required
Disney's BRDF
Basic Background
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With