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).
Related
I have simple normal map shader for 7 lights and it work on entire screen. How the hell to make it work only on limited distance? I tried calculate distance between light and pixel, and simple 'if' if distance is to big but this don't work for me.
varying vec4 v_color;
varying vec2 v_texCoords;
uniform vec3 lightColor[7];
uniform vec3 light[7];
uniform sampler2D u_texture;
uniform sampler2D u_normals;
uniform vec2 resolution;
uniform bool useNormals;
uniform bool useShadow;
uniform float strength;
uniform bool yInvert;
uniform bool xInvert;
uniform vec4 ambientColor;
void main() {
// sample color & normals from our textures
vec4 color = texture2D(u_texture, v_texCoords.st);
vec3 nColor = texture2D(u_normals, v_texCoords.st).rgb;
// some bump map programs will need the Y value flipped..
nColor.g = yInvert ? 1.0 - nColor.g : nColor.g;
nColor.r = xInvert ? 1.0 - nColor.r : nColor.r;
// this is for debugging purposes, allowing us to lower the intensity of our bump map
vec3 nBase = vec3(0.5, 0.5, 1.0);
nColor = mix(nBase, nColor, strength);
// normals need to be converted to [-1.0, 1.0] range and normalized
vec3 normal = normalize(nColor * 2.0 - 1.0);
vec3 sum = vec3(0.0);
for ( int i = 0; i < 7; ++i ){
vec3 currentLight = light[i];
vec3 currentLightColor = lightColor[i];
// here we do a simple distance calculation
vec3 deltaPos = vec3( (currentLight.xy - gl_FragCoord.xy) / resolution.xy, currentLight.z );
vec3 lightDir = normalize(deltaPos * 1);
float lambert = clamp(dot(normal, lightDir), 0.0, 1.0);
vec3 result = color.rgb;
result = (currentLightColor.rgb * lambert);
result *= color.rgb;
sum += result;
}
vec3 ambient = ambientColor.rgb * ambientColor.a;
vec3 intensity = min(vec3(1.0), ambient + sum); // don't remember if min is critical, but I think it might be to avoid shifting the hue when multiple lights add up to something very bright.
vec3 finalColor = color.rgb * intensity;
//finalColor *= (ambientColor.rgb * ambientColor.a);
gl_FragColor = v_color * vec4(finalColor, color.a);
}
edit:
my map editor screen
close-up of details
You need to measure the length of the light delta vector and use that to attenuate.
Right after the lightDir line, you can put something like this, but you'll have to adjust the FALLOFF constant to get the distance you want. FALLOFF must be greater than 0. As a starting point, a value of 0.1 will give you a light radius of about 4 units. Smaller values enlarge the radius. You might even want to define it as a parameter of each light (make them vec4s).
float distance = length(deltaPos);
float attenuation = 1.0 / (1.0 + FALLOFF * distance * distance);
float lambert = attenuation * clamp(dot(normal, lightDir), 0.0, 1.0);
This attenuation formula has a bell curve. If you want the curve to have a pointy tip, which is maybe more realistic (though probably pointless for 2D lighting), you can add a second parameter (which you can initially give a value of 0.1 and increase from there):
float attenuation = 1.0 / (1.0 + SHARPNESS * distance + FALLOFF * distance * distance);
Someone on this question posted this helpful chart you can play with to visually see how the parameters change the curve.
Also, don't multiply by an integer. This will cause the shader to fail to compile on some devices:
vec3 lightDir = normalize(deltaPos * 1); // The integer 1 is unsupported.
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'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
I have a scene that works perfectly with one light. However, when I add two more - each new addition becomes dimmer until it is almost unseen. Is the attenuation factors wrong or could it be something else?
int i = 0;
for(i=0; i<3; i++){
if (lights[i].enabled == 1.0){
//Lighting Attributes
vec4 light_position = vec4(lights[i].position,1.0);
vec4 light_ambient = lights[i].ambient;
vec4 light_diffuse = lights[i].diffuse;
vec4 light_specular = lights[i].specular;
float light_att_constant = 1.0;
float light_att_linear = 0.0;
float light_att_quadratic = 0.01;
float light_shine = 1.0;
//Object Attributes
vec3 obj_position = n_vertex;
vec3 obj_normals = n_normal;
vec4 obj_color = n_colors;
//Calc Distance
vec3 distance_LO = (obj_position - light_position.xyz);
float distance = length(distance_LO);
//Normalize some attributes
vec3 n_light_position = normalize(distance_LO);
//Apply ambience
finalColor *= light_ambient * global_ambient;
//Calc Cosine of Normal and Light
float NdotL = max(dot(obj_normals, n_light_position),0.0);
//Calc Eye Vector (negated position)
vec3 eye_view = -obj_position;
//Check if Surface is facing the Light
if (NdotL > 0){
//Apply lambertian reflection
finalColor += obj_color * light_diffuse * NdotL;
//Calc the half-vector
vec3 half_vector = normalize(light_position.xyz + eye_view);
//Calc angle between normal and half-vector
//See the engine notebook for a diagram.
float NdotHV = max(dot(obj_normals, half_vector), 0.0);
//Apply Specularity
finalColor += obj_color * light_specular * pow(NdotHV, light_shine);
}
//Calc Attenuation
float attenuation = light_att_constant / ((1 + light_att_linear * distance) *
1 + light_att_quadratic * distance * distance);
//Apply Attenuation
finalColor = finalColor * attenuation;
}
}
color = vec4(finalColor.rgb, 1.0);
You multiply in your colours. This means that shadows will get darker.
If you have an area around some relative brightness 1/2, then you multiply it by 1/2 (contribution from that light), you'll get 1/4.
If you have Photoshop or Gimp you can test this yourself with the Multiply blending mode, and three circles, pure red, pure green and pure blue and overlap them. Compare Multiply to Linear Dodge (the plus operation in Photoshop.)
Here's an example.
You'll most certainly want an additive effect, that is, add the terms together.
I tried to incorporate attentuation, but it failed does nothing.
I have diffuse, ambient, and specular lighting working. I just need to dim the light as the fragments get further away from the light.
Also, i have the attenuation parameter for my light:
glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.0004f);
This is the floor lighting, the light is positioned just behind the cube:
http://oi43.tinypic.com/i39fuo.jpg
.vert
varying vec3 N;
varying vec3 v;
varying vec3 c;
varying float dist;
void main(void)
{
vec4 ecPos;
vec3 aux;
ecPos = gl_ModelViewMatrix * gl_Vertex;
aux = vec3(gl_LightSource[0].position-ecPos);
dist = length(aux);
c = vec3(gl_Color);
v = vec3(gl_ModelViewMatrix * gl_Vertex);
N = normalize(gl_NormalMatrix * gl_Normal);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
.frag
varying vec3 N;
varying vec3 v;
varying vec3 c;
varying float dist;
void main (void)
{
float att;
att = 1.0 / (gl_LightSource[0].constantAttenuation +
gl_LightSource[0].linearAttenuation * dist +
gl_LightSource[0].quadraticAttenuation * dist * dist);
vec3 L = normalize(gl_LightSource[0].position.xyz - v);
vec3 E = normalize(-v); // we are in Eye Coordinates, so EyePos is (0,0,0)
vec3 R = normalize(-reflect(L,N));
float nDotL = max(dot(N,L), 0.0);
float rDotE = max(dot(R,E),0.0);
float power = pow(rDotE, gl_FrontMaterial.shininess);
//calculate Ambient Term:
vec4 Iamb = gl_FrontLightProduct[0].ambient * att;
//calculate Diffuse Term:
vec4 Idiff = gl_FrontLightProduct[0].diffuse * nDotL * att;
Idiff = clamp(Idiff, 0.0, 1.0);
// calculate Specular Term:
vec4 Ispec = gl_FrontLightProduct[0].specular * power * att;
Ispec = clamp(Ispec, 0.0, 1.0);
// write Total Color:
gl_FragColor = Iamb + Idiff + Ispec + c;
}
From this image i cant' really see anything. How about setting a small object as lightsource.
Which object's use this shader?
Some things that come to my mind:
You normalized your normal in your vertex shader, which is an unnecessary step.
Passed vectors from vertex to fragment shader must be normalized inside fragment shader since they will be interpolated.
Aslong you don't do any length based calculations in your vertex shader, which you aren't no normalization is necessary in vertex shader.
You should normalize the normal in fragment shader, then you don't need to normalize your reflect vector.
Attenuation is not based on anything "complex" calculated in shader. So output it and then check the rest. How does your diffuse term looks like?
Further hints:
You could place the light vector and attenuation calculation inside vertex shader and pass it as to fragment shader (pack it in a 4 vec component) to save interpolators.
the final specular clamp is unecessary, the values should be within [0, 1] range automatically. If not you have a problem.