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
Related
Managed to get Shadow Mapping to work in my OpenGL rendering engine, but it is producing some weird artifacts that I think are "shadow acne". However, I am using shadow2DProj to get the shadow value from the shadow depth map, which for me has proven to be the only way to get shadows to show up at all. Therefore, looking around at various tutorial at learnopengl, opengl-tutorials and others have yielded no help. Would like some advice as to how I could mitigate this problem.
Here is my shader that I use to draw the shadow map with:
#version 330 core
out vec4 FragColor;
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
vec3 attenuation;
};
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
in vec4 ShadowCoords;
uniform vec3 viewPos;
uniform sampler2D diffuseMap;
uniform sampler2D specularMap;
uniform sampler2DShadow shadowMap;
uniform Light lights[4];
uniform float shininess;
float calculateShadow(vec3 lightDir)
{
float shadowValue = shadow2DProj(shadowMap, ShadowCoords).r;
float shadow = shadowValue;
return shadow;
}
vec3 calculateAmbience(Light light, vec3 textureMap)
{
return light.ambient * textureMap;
}
void main()
{
vec4 tex = texture(diffuseMap, TexCoords);
if (tex.a < 0.5)
{
discard;
}
vec3 ambient = vec3(0.0);
vec3 diffuse = vec3(0.0);
vec3 specular = vec3(0.0);
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
for (int i = 0; i < 4; i++)
{
ambient = ambient + lights[i].ambient * tex.rgb;
vec3 lightDir = normalize(lights[i].position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
diffuse = diffuse + (lights[i].diffuse * diff * tex.rgb);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);
specular = specular + (lights[i].specular * spec * tex.rgb);
float dist = length(lights[i].position - FragPos);
float attenuation = lights[i].attenuation.x + (lights[i].attenuation.y * dist) + (lights[i].attenuation.z * (dist * dist));
if (attenuation > 0.0)
{
ambient *= 1.0 / attenuation;
diffuse *= 1.0 / attenuation;
specular *= 1.0 / attenuation;
}
}
float shadow = calculateShadow(normalize(lights[0].position - FragPos));
vec3 result = (ambient + (shadow) * (diffuse + specular));
FragColor = vec4(result, 1.0);
}
This is the result I get. Notice the weird stripes on top of the cube:
Reading the description about shadow acne, this seems to be the same phenomenon (source: https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping).
According to that article, I need to check if the ShadowCoord depth value, minus a bias constant, is lower then the shadow value read from the shadow map. If so, we have shadow. Now... here comes the problem. Since I am using shadow2DProj and not texture() to get my shadow value from the shadow map (through some intricate sorcery no doubt), I am unable to "port" that article's code into my shader and get it to work. Here is what I have tried:
float calculateShadow(vec3 lightDir)
{
float closestDepth = shadow2DProj(shadowMap, ShadowCoords).r;
float bias = 0.005;
float currentDepth = ShadowCoords.z;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
return shadow;
}
But that produces no shadows at all, since the "shadow" float is always assigned 1.0 from the depth & bias check. I must admit that I do not fully understand what I am getting from using shadow2DProj(...).r as compared to texture(...).r, but it sure is something completely different.
This question has a misunderstanding of what shadow2DProj does. The function does not return a depth value, but a depth comparison result. Therefore, apply the bias before calling it.
Solution 1
Apply the bias prior to running the comparison. ShadowCoords.z is your currentDepth value.
float calculateShadow(vec3 lightDir)
{
const float bias = 0.005;
float shadow = shadow2DProj(shadowMap, vec3(ShadowCoords.uv, ShadowCoords.z - bias)).r;
return shadow;
}
Solution 2
Apply the bias while performing the light-space depth pass.
glPolygonOffset(float factor, float units)
This function offsets Z-axis values by factor * DZ + units where DZ is the z-axis slope of the polygon. Setting this to positive values moves polygons deeper into the scene, which acts like our bias.
During initialization:
glEnable(GL_POLYGON_OFFSET_FILL);
During Light Depth Pass:
// These parameters will need to be tweaked for your scene
// to prevent acne and mitigate peter panning
glPolygonOffset(1.0, 1.0);
// draw potential shadow casters
// return to default settings (no offset)
glPolygonOffset(0, 0);
Shader Code:
// we don't even need the light direction for slope bias
float calculateShadow()
{
float shadow = shadow2DProj(shadowMap, ShadowCoords).r;
return shadow;
}
I am trying to implement the Cook-Torrance lighting mode with four point lights. While I am getting nice results by using just only one point light, I can't understand which is the correct way to sum up the specular term inside my light loop.
I am defining the materials as follows:
struct material {
vec3 ambient; /* ambient color */
vec3 diffuse; /* diffuse color */
vec3 specular; /* speculr color */
float metallic;
float roughness;
};
...whereby my lights only have one color/intensity property,
struct light {
vec3 position;
vec3 color;
bool enabled;
};
Here is the function inside my fragment shader with the fragment color computation:
vec3 lighting() {
vec3 color = vec3(0.0,0.0,0.0);
float r0 = pow(material.metallic - 1.0,2.0)/pow(material.metallic + 1.0,2.0);
vec3 V = normalize(-v_viewpos);
vec3 N = normalize(v_normal);
for (int i = 0; i < 4; i++) {
if (light[i].enabled) {
vec3 L = normalize(light[i].position - v_viewpos);
// Half-way vector
vec3 halfVector = normalize(L + V);
float NdotL = max(dot(N, L),0.0);
float NdotV = max(dot(N, V),0.0);
if (NdotL > 0.001 && NdotV > 0.001) {
float NdotH = max(0.0, dot(N, halfVector));
float HdotV = max(0.0, dot(halfVector, V));
// Beckmann
float tanAlpha = sqrt(1.0-NdotH*NdotH)/NdotH;
float D = exp(-pow(tanAlpha/material.roughness,2.0))/(4.0*pow(material.roughness,2.0)*pow(NdotH,4.0));
// Shadowing-masking term
float G1 = (2.0 * NdotH * NdotV) / HdotV;
float G2 = (2.0 * NdotH * NdotL) / HdotV;
float G = min(1.0, min(G1, G2));
// Fresnel reflection, Schlick approximation
float F = r0 + (1.0 - r0) * pow(1.0 - NdotL, 5.0);
float R = (F*G*D) / (3.14159 * NdotL * NdotV);
color += light[i].color * R * NdotL;
}
color += material.diffuse * light[i].color;
}
}
return color;
}
I believe the key point here is my wrong computation inside the light loop:
color += light[i].color * R * NdotL;
Here is an example of what I mean, the resulting fragment color is either too dark, or too bright. I am not able to sum up each light contribution to get a nice smooth color gradient among the specular term and the material colors.
I am reading here about gamma correction, but I can't understand if this applies to my question or not.
How should I sum up each light.color with the diffuse, ambient and specular colors of the material, to calculate the final fragment color, by correctly including the total amount of specular highlight contribution of each light?
vec3 V should be the normalized vector starting from the fragment position to the camera position.
vec3 L should be the normalized vector starting from the fragment position to the light position.
One of those vectors is wrong in your shader, depending on the actual value of v_viewpos.
The fresnel should be based on HoV not NoL:
pow(1.0 - HoV, 5.0)
For the diffuse part, you consider your light like ambiant light and not point light.
color += material.diffuse * light[i].color;
should be (for simple Lambertian)
color += material.diffuse * light[i].color * NoL;
Most of your computations look good (including the directions of V and L and the Fresnel term). The only thing is that you might have mixed up how to combine the individual lighting components. For specular and diffuse, you have
color += light[i].color * R * NdotL;
R corresponds to the specular part and NdotL to the diffuse part. Both are additive, however. Therefore, the equation should be (plus considering material parameters):
color += light[i].color * (material.specular * R + material.diffuse * NdotL);
For the ambient term you have
color += material.diffuse * light[i].color;
Replace material.diffuse with material.ambient and this should be correct.
And be sure that your lights are not too bright. The screen cannot display anything brighter than white (or fully saturated red).
My quat works perfectly for moving about the worldspace with my camera.
now I took from https://learnopengl.com/#!Lighting/Light-casters a shader program for lighting in which it uses Eucl geo for it's camera and refers to direction a camera.front. I can't figure out how to calculate the same thing with the quat model.
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
// attenuation
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// spotlight intensity
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
// combine results
vec3 ambient = light.ambient * vec3(diffusefinal);
vec3 diffuse = light.diffuse * diff * vec3(diffusefinal);
vec3 specular = light.specular * spec * vec3(specfinal);
ambient *= attenuation * intensity;
diffuse *= attenuation * intensity;
specular *= attenuation * intensity;
return (ambient + diffuse + specular);
}
now it works perfectly when I use it's camera model that uses Eular Angles for moving about, so it comes with an easy conversion for a front facind direction.
However, After much research, i've found what kinda works.
glm::vec3 front = glm::vec3(0.0f, 0.0f, 1.0f) * camera[currentCam].orientation(); -- this is what's suppose to represent a forward direction.
and camera[currentCam].position() is the vec3 location in worldspace of the camera.
lightMgr.SetProperty(Spotlight, spolight, position, -camera[currentCam].position());
lightMgr.SetProperty(Spotlight, spolight, direction, -front);
Why does it require inverting the camera position and direction? It technically works, but I can't move on because I can't wrap my head around why it works when you take negative location and direction, but not positive direction and direction, or any other combination of the two.
Hi i have one question.
I'm trying to understand lighting and material in OpenGL.
but I do not know how the lighting affects the color and reflection of an object.
Here's the light formula I've found:
result Light = ambient + diffuse * (intensity) + specular
and here's an example usage:
ambient = 64,64,64
diffuse = 192,192,192
specular = 32,32,32
intentisy = 0.5
Light = 64 + 192*0.5+ 32 = 192
Result Light = (192,192,192)
Here's how it comes together to form the final output:
Object Color = (Or,Og,Ob)
Material reflect = (Mr,Mg,Mb)
Real Color = (Or * Mr , Og * Mg ,Ob * Mb )
For my question,
I do not know how the "result light" affects the "real color"?
More specifically: How the final pixel output comes to fruition using all the light, material and object color inputs.
A little ambiguous because real can range from physical based rendering techniques to differed shading with various custom lighting shaders.
Depending on the "Material" we'll have a different equation which uses "Result Light".
Here are some basic materials:
Ambient Lighting:
float ambientStrength = 0.1f;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
color = vec4(result, 1.0f);
Diffuse Lighting:
in vec3 FragPos; // input
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * objectColor;
color = vec4(result, 1.0f);
Specular Lighting:
float specularStrength = 0.5f;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
vec3 result = (ambient + diffuse + specular) * objectColor;
color = vec4(result, 1.0f);
Combined Phong:
Recycle code from above.
Source
I implemented a simple shader for the lighting; it kind of works, but the light seems to move when the camera rotates (and only when it rotates).
I'm experimenting with a spotlight, this is how it looks like (it's the spot in the center):
If now I rotate the camera, the spot moves around; for example, here I looked down (I didn't move at all, just looked down) and it seemed at my feet:
I've looked it up and I've seen that it's a common mistake when mixing reference systems in the shader and/or when setting the light's position before moving the camera.
The thing is, I'm pretty sure I'm not doing these two things, but apparently I'm wrong; it's just that I can't find the bug.
Here's the shader:
Vertex Shader
varying vec3 vertexNormal;
varying vec3 lightDirection;
void main()
{
vertexNormal = gl_NormalMatrix * gl_Normal;
lightDirection = vec3(gl_LightSource[0].position.xyz - (gl_ModelViewMatrix * gl_Vertex).xyz);
gl_Position = ftransform();
}
Fragment Shader
uniform vec3 ambient;
uniform vec3 diffuse;
uniform vec3 specular;
uniform float shininess;
varying vec3 vertexNormal;
varying vec3 lightDirection;
void main()
{
vec3 color = vec3(0.0, 0.0, 0.0);
vec3 lightDirNorm;
vec3 eyeVector;
vec3 half_vector;
float diffuseFactor;
float specularFactor;
float attenuation;
float lightDistance;
vec3 normalDirection = normalize(vertexNormal);
lightDirNorm = normalize(lightDirection);
eyeVector = vec3(0.0, 0.0, 1.0);
half_vector = normalize(lightDirNorm + eyeVector);
diffuseFactor = max(0.0, dot(normalDirection, lightDirNorm));
specularFactor = max(0.0, dot(normalDirection, half_vector));
specularFactor = pow(specularFactor, shininess);
color += ambient * gl_LightSource[0].ambient;
color += diffuseFactor * diffuse * gl_LightSource[0].diffuse;
color += specularFactor * specular * gl_LightSource[0].specular;
lightDistance = length(lightDirection[i]);
float constantAttenuation = 1.0;
float linearAttenuation = (0.02 / SCALE_FACTOR) * lightDistance;
float quadraticAttenuation = (0.0 / SCALE_FACTOR) * lightDistance * lightDistance;
attenuation = 1.0 / (constantAttenuation + linearAttenuation + quadraticAttenuation);
// If it's a spotlight
if(gl_LightSource[i].spotCutoff <= 90.0)
{
float spotEffect = dot(normalize(gl_LightSource[0].spotDirection), normalize(-lightDirection));
if (spotEffect > gl_LightSource[0].spotCosCutoff)
{
spotEffect = pow(spotEffect, gl_LightSource[0].spotExponent);
attenuation = spotEffect / (constantAttenuation + linearAttenuation + quadraticAttenuation);
}
else
attenuation = 0.0;
}
color = color * attenuation;
// Moltiplico il colore per il fattore di attenuazione
gl_FragColor = vec4(color, 1.0);
}
Now, I can't show you the code where I render the things, because it's a custom language which integrates opengl and it's designed to create 3D applications (it wouldn't help to show you); but what I do is something like this:
SetupLights();
UpdateCamera();
RenderStuff();
Where:
SetupLights contains actual opengl calls that setup the lights and their positions;
UpdateCamera updates the camera's position using the built-in classes of the language; I don't have much power here;
RenderStuff calls the built-in functions of the language to draw the scene; I don't have much power here either.
So, either I'm doing something wrong in the shader or there's something in the language that "behind the scenes" breaks things.
Can you point me in the right direction?
you wrote
the light's position is already in world coordinates, and that is where I'm doing the computations
however, since you're applying gl_ModelViewMatrix to your vertex and gl_NormalMatrix to your normal, these values are probably in view space, which might cause the moving light.
as an aside, your eye vector looks like it should be in view coordinates, however, view space is a right-handed coordinate system, so "forward" points along the negative z-axis. also, your specular computation will likely be off since you're using the same eye vector for all fragments, but it should probably point towards that fragment's position on the near/far planes.