Search code examples
opengllibgdxglslshaderlwjgl

How to make normal map shader with limited range?


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


Solution

  • 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.