GLSL Two sided Lighting

Hi,

I am currently trying to achieve Two sided per pixel lighting in GLSL as described here:
http://www.gamedev.net/reference/articles/article2428.asp

Even though it generally works, the two sided aspect is wrong while the fixed functionality version works fine. I use exactly the same functions as written in the article above.

Here is a little image that shows the problem. Shader Lighting on the left, Fixed Function on the right:

The first thing I don’t really get is why the materials are so much brighter in the shader version. But more important is obviously the fact that part of the two sided lighting in my shader are lit which obviously should be dark.

Any Ideas what could be the problem?
Thanks in advance!

could be the normals. if you copy n pasted the shader code then it should work correct (hopefully). look for errors coming from your application, especially how you calculate and pass normals but on the other hand as you say fixed function works alright. if you use modified shader code you should post it here to have a look.

well the normals are the same for the fixed functionality version so I doubt that it has anything to do with that. I simply pass the normal from the vertex to my Fragmentshader the exact way as it is done in the example code at gamedev. I will check my source code again though. Any other ideas?

shader code would help. could be also a harmless driver bug somewhere. hard to tell what the problem is just from provided information.

okay, I will check my shaders again and post it if I still can’t find the bug :slight_smile:

Well I had to change some things to the shader for testing. I am on a First generation Mac Book Pro with an ATI Radeon x1600. I put all the lighting functions into on fragment shader and had to make some changes to the order because otherwise I had some errors telling me that the functions were not found. Another thing is that I replaced all the gl_LightSource[i]…; with gl_LightSource[0]…; because otherwise it didn’t work. For some reason the is were only accepted if I changed them to const int but that didn’t work for passing them from function to function. so my final fragment shader code looks like this:


varying vec3 normal;
varying vec3 vertex;

const vec4 AMBIENT_BLACK = vec4(0.0, 0.0, 0.0, 1.0);
const vec4 DEFAULT_BLACK = vec4(0.0, 0.0, 0.0, 0.0);

bool isLightEnabled(in int i)
{
    // A separate variable is used to get
    // rid of a linker error.
    bool enabled = true;
	
    // If all the colors of the Light are set
    // to BLACK then we know we don't need to bother
    // doing a lighting calculation on it.
    if ((gl_LightSource[i].ambient  == AMBIENT_BLACK) &&
        (gl_LightSource[i].diffuse  == DEFAULT_BLACK) &&
        (gl_LightSource[i].specular == DEFAULT_BLACK))
	enabled = false;
	
    return(enabled);
}

float calculateAttenuation(in int i, in float dist)
{
    return(1.0 / (gl_LightSource[0].constantAttenuation +
                  gl_LightSource[0].linearAttenuation * dist +
                  gl_LightSource[0].quadraticAttenuation * dist * dist));
}

void directionalLight(in int i, in vec3 N, in float shininess,
                      inout vec4 ambient, inout vec4 diffuse, inout vec4 specular)
{
    const int b = 0;
    vec3 L = normalize(gl_LightSource[b].position.xyz);
	
    float nDotL = dot(N, L);
	
    if (nDotL > 0.0)
    {   
        vec3 H = gl_LightSource[b].halfVector.xyz;
		
        float pf = pow(max(dot(N,H), 0.0), shininess);
		
        diffuse  += gl_LightSource[b].diffuse  * nDotL;
        specular += gl_LightSource[b].specular * pf;
    }
	
    ambient  += gl_LightSource[b].ambient;
}

void pointLight(in int i, in vec3 N, in vec3 V, in float shininess,
                inout vec4 ambient, inout vec4 diffuse, inout vec4 specular)
{
    vec3 D = gl_LightSource[0].position.xyz - V;
    vec3 L = normalize(D);
	
    float dist = length(D);
    float attenuation = calculateAttenuation(0, dist);
	
    float nDotL = dot(N,L);
	
    if (nDotL > 0.0)
    {   
        vec3 E = normalize(-V);
        vec3 R = reflect(-L, N);
		
        float pf = pow(max(dot(R,E), 0.0), shininess);
		
        diffuse  += gl_LightSource[0].diffuse  * attenuation * nDotL;
        specular += gl_LightSource[0].specular * attenuation * pf;
    }
	
    ambient  += gl_LightSource[0].ambient * attenuation;
}

void spotLight(in int i, in vec3 N, in vec3 V, in float shininess,
               inout vec4 ambient, inout vec4 diffuse, inout vec4 specular)
{
    vec3 D = gl_LightSource[0].position.xyz - V;
    vec3 L = normalize(D);
	
    float dist = length(D);
    float attenuation = calculateAttenuation(0, dist);
	
    float nDotL = dot(N,L);
	
    if (nDotL > 0.0)
    {   
        float spotEffect = dot(normalize(gl_LightSource[0].spotDirection), -L);
		
        if (spotEffect > gl_LightSource[0].spotCosCutoff)
        {
            attenuation *=  pow(spotEffect, gl_LightSource[0].spotExponent);
			
            vec3 E = normalize(-V);
            vec3 R = reflect(-L, N);
			
            float pf = pow(max(dot(R,E), 0.0), shininess);
			
            diffuse  += gl_LightSource[0].diffuse  * attenuation * nDotL;
            specular += gl_LightSource[0].specular * attenuation * pf;
        }
    }
	
    ambient  += gl_LightSource[0].ambient * attenuation;
}

void calculateLighting(in int numLights, in vec3 N, in vec3 V, in float shininess,
                       inout vec4 ambient, inout vec4 diffuse, inout vec4 specular)
{ 
    // Just loop through each light, and if its enabled add
    // its contributions to the color of the pixel.
    for (int i = 0; i < numLights; i++)
    {	
        if (isLightEnabled(i))
        {
            if (gl_LightSource[0].position.w == 0.0)
			directionalLight(i, N, shininess, ambient, diffuse, specular);
            else if (gl_LightSource[0].spotCutoff == 180.0)
			pointLight(i, N, V, shininess, ambient, diffuse, specular);
			else
			spotLight(i, N, V, shininess, ambient, diffuse, specular);
        }
    }
}


vec3 findDiffuse(in vec3 n, in vec3 lVec, in vec3 lCol){
	vec3 ccol = vec3(0.0);
	float diffuse = max(dot(n, lVec), 0.0);
	if(diffuse < 0.125)
	diffuse = 0.125;
	
	ccol = lCol * diffuse;
	
	return (ccol);
}

void main()
{   
    // Normalize the normal. A varying variable CANNOT
    // be modified by a fragment shader. So a new variable
    // needs to be created.
    vec3 n = normalize(normal);
   
    vec4 ambient, diffuse, specular, color;

    // Initialize the contributions.
    ambient  = vec4(0.0);
    diffuse  = vec4(0.0);
    specular = vec4(0.0);
   
    // In this case the built in uniform gl_MaxLights is used
    // to denote the number of lights. A better option may be passing
    // in the number of lights as a uniform or replacing the current
    // value with a smaller value.
    calculateLighting(1, n, vertex, gl_FrontMaterial.shininess,
                      ambient, diffuse, specular);
   
    color  = gl_FrontLightModelProduct.sceneColor  +
             (ambient  * gl_FrontMaterial.ambient) +
             (diffuse  * gl_FrontMaterial.diffuse) +
             (specular * gl_FrontMaterial.specular);

    // Re-initialize the contributions for the back
    // pass over the lights
    ambient  = vec4(0.0);
    diffuse  = vec4(0.0);
    specular = vec4(0.0);
          
    // Now caculate the back contribution. All that needs to be
    // done is to flip the normal.
    calculateLighting(1, -n, vertex, gl_BackMaterial.shininess,
                      ambient, diffuse, specular);

    color += gl_BackLightModelProduct.sceneColor  +
             (ambient  * gl_BackMaterial.ambient) +
             (diffuse  * gl_BackMaterial.diffuse) +
             (specular * gl_BackMaterial.specular);

    color = clamp(color, 0.0, 1.0);
	
    gl_FragColor = color;

}

vertex shader:


varying vec3 normal;
varying vec3 vertex;

void main(void)
{   	 
	normal = normalize(gl_NormalMatrix * gl_Normal);
	vertex = vec3(gl_ModelViewMatrix * gl_Vertex);

	gl_Position = ftransform();

}

ATI drivers suck. as I said I can only access the lightsources using a const int. But since the test only uses light0 the above example should work correctly without showing the errors from the first image.

Thanks!

ATI drivers suck. as I said I can only access the lightsources using a const int

No, this is not ATI drivers, it is glslang 1.2 and also AFAIK with 1.3, you simply can’t index an array with a non constant index (like a uniform integer) and it is a shame.
However you can sometimes do it in loops when the compiler is able to unroll it.
I will look into your shader when I would have time.

Okay, thank you very much!

Can I use a newer Version of GLSLang on a mac? or is that bound to the GFX card which is almost three years old now as far as I can tell.

thats quite some code. what lighting function do you actually use to make the problem appear? point, directional, spot?

I tried them all. It appears to be the same problem with them all. For the screenshot its directional.

Okay, I made another screenshot that should illustrate the problem a little more. This time I simply disabled all materials related to the backface. As you can see the shader results look as if it does not really know which part belongs to wich face (front or back). I am starting to ask myself if this is possible at all, or maybe a driver specific bug?

Maybe proper two-sided lighting is only possible per vertex?

I have checked your code. There is not bug, this is the way the you are doing it in the fragment shader:

You are computing first the contribution from front faces:


calculateLighting(1, n, vertex, gl_FrontMaterial.shininess,
                      ambient, diffuse, specular);
   
    color  = gl_FrontLightModelProduct.sceneColor  +
             (ambient  * gl_FrontMaterial.ambient) +
             (diffuse  * gl_FrontMaterial.diffuse) +
             (specular * gl_FrontMaterial.specular);

Then the contribution from back:


calculateLighting(1, -n, vertex, gl_BackMaterial.shininess,
                      ambient, diffuse, specular);

    color += gl_BackLightModelProduct.sceneColor  +
             (ambient  * gl_BackMaterial.ambient) +
             (diffuse  * gl_BackMaterial.diffuse) +
             (specular * gl_BackMaterial.specular);

And you add all together to compute the final fragment color! There is a problem here, you are suming twice the ambient and specular contributions thus you get an overbright result.

About getting a higher version of shading language this is not that simple, it depends only on hardware. With your X1600 only glslang 1.2 is supported. Anyway I cheched it out in the spec and it seems that indexing with variables is still not possible in the latest glslang version (1.3), maybe an hardware limitation.

IMO, this is not the most efficient way to do 2-sided shading. You should test the sign of n.z (since after modelview transformation the eye is looking in the -z direction). If n.z is negative, then it is a fragment from back and you compute lighting with -n. Otherwise you use n.

This way, you compute light only one time and it is faster.

but if my code is correct, what causes the problems illustrated in the first image? it does not make sense.

and also about only summing diffuse and ambient once, the result looks still quite much brighter than the fixed functionality version.
Thanks fo your help!

I have made a little mistake, it is just the ambient term that is added twice to the final fragment color contribution.

So, you mean that even if you material ambient term to zero, you get the same brightness or at least something more brighter? A screenshot of the new result would be helpful.

Do not expect to obtain exactly the same result, you are computing color per pixel and opengl do it per vertex then interpolate colors in each fragment.

I looked into the orange book and the two sided per vertex lighting example contributes ambient, diffuse and specular for both faces to the final color.

The main problem still is the same, no matter what contributes to the final color. It seems that the fragment colours are somehow mixed between front and back face.

Thank you!

Ok, is it possible to provide a simple glut opengl code so that I can run and modify it by myself?

Maybe I have not expressed myself clearly. I agree with you, you have to use ambient term for front and back faces. What I meant is that for, say, front face you compute diffuse and specular contributrion and you finally add the ambient term. But secondly you compute exactly the same thing with the normal flipped. Because it is a front face the diffuse and specular term will be zero with the flipped normal but the ambient term won’t be.

Finally you have:

front_color = front_diffuse + front_specular + front_ambient + back_ambient

and you should have:

front_color = front_diffuse + front_specular + front_ambient

front_ambient (resp. back_ambient) is not a correct designation, it just means ambient when computing front contribution (resp. back contribution)

The correct way is to remove the computation of ambient term in every lighting function (spot, directional, omni) and add it at the end when you are about to output the fragment color.

I will try to make a glut sample for you. Could take some time though.

I understand what you are saying but as far as I can tell that would change the overall brightness and not remove the “bug” i pointed out in the screenshots right?

Because right now, things look as if I just applied one sided lighting twice with flipped normals and added it together. If the outside of the half shpere is bright, the inside is too, just as in one sided lighting. but as far as my understanding goes the inside should be dark when I use two sided lighting just as it is in my screens with fixed functionality.

Thanks!

it actually looks to me that you have multiple lights in your shader and only one in the FF pipe, probably just forgot to glEnable other lights. now, in GLSL it is ignored in FF not. please check again because i have no more ideas on this one.

no, I only use one light for sure, also visible in the source of the shader. Furthermore the shader ignores if I enabled lights or not as far as I know. (even though I did it in this case)