Search code examples
javaopengllibgdxshaderblending

Ambient light is getting brighter when a light is added (OpenGL, Normal mapping)


In the last few days I played around with lightning in Java (Libgdx). I'm new to OpenGL or Shaders and I've stumbled across a nice Tutorial how to implement lighting with normal mapping (https://github.com/mattdesl/lwjgl-basics/wiki/ShaderLesson6). So far I managed to do this with one light and now I'm trying to do the same effect with multiple lights. I tried to perform one draw call for each light with additive blending. The shadows are drawing correctly, but every time I add a light the ambient color is getting brighter. I tried a few things but nothing worked and I am stuck.

My render methods:

@Override
public void render () {
    renderToFbo(Gdx.input.getX(), Gdx.graphics.getHeight() - Gdx.input.getY());
    renderToScreen(Gdx.input.getX(), Gdx.graphics.getHeight() - Gdx.input.getY());

    renderToFbo(200, 200);
    batch.setBlendFunction(GL_ONE,GL_ONE_MINUS_SRC_COLOR);
    renderToScreen(200,200);

    renderToFbo(500, 500);
    batch.setBlendFunction(GL_ONE, GL_ONE_MINUS_SRC_COLOR);
    renderToScreen(500,500);
}


private void renderToFbo(float posX, float posY){
    fbo.begin();
    batch.setBlendFunction(GL_ONE, GL_ZERO);
    batch.setShader(defaultShader);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    batch.begin();
    batch.draw(lightMap, posX - lightSize / 2, posY - lightSize  / 2, lightSize,lightSize);
    batch.end();
    fbo.end();
}

private void renderToScreen(float posX, float posY){
    batch.setShader(lightningShader);
    batch.begin();

    float x = posX / (float) Gdx.graphics.getWidth();
    float y = posY / (float) Gdx.graphics.getHeight();
    LIGHT_POS.x = x;
    LIGHT_POS.y = y;

    lightningShader.setUniformf("lightPos", LIGHT_POS.x, LIGHT_POS.y, LIGHT_POS.z);

    fbo.getColorBufferTexture().bind(2);
    normalMap.bind(1);
    texture.bind(0);

    batch.draw(texture, 0,0);
    batch.end();
}

And here my fragment shader:

varying vec4 vColor;
varying vec2 vTexCoord;
uniform sampler2D u_texture;   //diffuse map
uniform sampler2D u_normals;   //normal map
uniform sampler2D u_light;     //light map

uniform vec2 resolution;      //resolution of screen
uniform vec3 lightPos;        //light position, normalized
uniform vec4 lightColor;      //light RGBA -- alpha is intensity
uniform vec4 ambientColor;    //ambient RGBA -- alpha is intensity


void main() {
//RGBA of our diffuse color
vec4 diffuseColor = texture2D(u_texture, vTexCoord);

//RGB of our normal map
vec3 normalMap = texture2D(u_normals, vTexCoord).rgb;
//NormalMap.g = 1.0 - NormalMap.g;

//The delta position of light
vec3 lightDir = vec3(lightPos.xy - (gl_FragCoord.xy / resolution.xy), lightPos.z);

lightDir.x *= resolution.x / resolution.y;


//normalize our vectors
vec3 N = normalize(normalMap * 2.0 - 1.0);
vec3 L = normalize(lightDir);

//Pre-multiply light color with intensity
//Then perform "N dot L" to determine our diffuse term
vec3 diffuse = (lightColor.rgb * lightColor.a) * max(dot(N, L), 0.0);

//pre-multiply ambient color with intensity
vec3 ambient = ambientColor.rgb * ambientColor.a;

//calculate attenuation from lightmap
vec2 lighCoord = (gl_FragCoord.xy / resolution.xy);
vec3 attenuation = texture2D(u_light, lighCoord).rgb;

//the calculation which brings it all together
vec3 intensity = ambient + diffuse * attenuation;
vec3 finalColor = diffuseColor.rgb * intensity;
gl_FragColor = vColor * vec4(finalColor, diffuseColor.a);
}

Solution

  • To do this with a single render call, your fragment shader must accept an array of light positions to process. Shaders must know array sizes at compile time, so you should set the array big enough for as many lights as you will need (and when you need fewer you can set the remaining lights to black).

    I adapted your shader below, just assuming it works correctly above in your code. I don't know what you're doing with the lightmap stuff, so I replaced your attenuation calculation with something more traditional.

    varying vec4 vColor;
    varying vec2 vTexCoord;
    uniform sampler2D u_texture;   //diffuse map
    uniform sampler2D u_normals;   //normal map
    
    const int LIGHT_COUNT = 4;
    
    uniform vec2 resolution;      //resolution of screen
    uniform vec3[LIGHT_COUNT] lightPos;      //light position, normalized
    uniform vec4[LIGHT_COUNT] lightColor;      //light RGBA -- alpha is intensity
    uniform vec4 ambientColor;    //ambient RGBA -- alpha is intensity
    
    void main() {
    
        vec4 diffuseColor = texture2D(u_texture, vTexCoord);
        vec3 normalMap = texture2D(u_normals, vTexCoord).rgb;
        vec3 N = normalize(normalMap * 2.0 - 1.0);
    
        float resolutionFactor = resolution.x / resolution.y;
    
        vec3 diffuse = new vec3(0.0);
        for (int i=0; i<LIGHT_COUNT; i++){
            vec3 lightDir = vec3(lightPos[i].xy - (gl_FragCoord.xy / resolution.xy), lightPos[i].z);
            lightDir.x *= resolutionFactor;
            vec3 L = normalize(lightDir);
            float distance = length(lightDir);
            vec3 attenuation = 1.0 / ( 0.4 + 3.0*distance + (20.0*distance*distance ) );
            diffuse += attenuation * (lightColor[i].rgb * lightColor[i].a) * max(dot(N, L), 0.0);
        }
    
        //pre-multiply ambient color with intensity
        vec3 ambient = ambientColor.rgb * ambientColor.a;
    
    
        //the calculation which brings it all together
        vec3 intensity = min(vec3(1.0), ambient + diffuse); // 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 = diffuseColor.rgb * intensity;
        gl_FragColor = vColor * vec4(finalColor, diffuseColor.a);
    
    }
    

    To pass your light parameters to your shader:

    static final int LIGHT_COUNT = 4;
    final float[] tmpLightPositions = new float[3 * LIGHT_COUNT];
    final float[] tmpLightColors = new float[4 * LIGHT_COUNT];
    
    //...
    
    int i = 0;
    for (Vector3 pos : myLightPositions) {// should be LIGHT_COUNT of them
        tmpLightPositions[i++] = pos.x;
        tmpLightPositions[i++] = pos.y;
        tmpLightPositions[i++] = pos.z;
    }
    i = 0;
    for (Color col : myLightColors) {
        tmpLightColors[i++] = color.r;
        tmpLightColors[i++] = color.g;
        tmpLightColors[i++] = color.b;
        tmpLightColors[i++] = color.a;
    }
    shader.setUniform3fv("lightPos", tmpLightPositions, 0, tmpLightPositions.length);
    shader.setUniform4fv("lightColor", tmpLightColors, 0, tmpLightColors.length);