Search code examples
xnashadermonogamehlsllighting

Applying Spotlights Over Dark Ambient Light - HLSL - Monogame


I wrote an HLSL shader for my Monogame project that uses ambient lighting to create a day/night cycle.

#if OPENGL
    #define SV_POSITION POSITION
    #define VS_SHADERMODEL vs_3_0
    #define PS_SHADERMODEL ps_3_0
#else
    #define VS_SHADERMODEL vs_4_0_level_9_1
    #define PS_SHADERMODEL ps_4_0_level_9_1
#endif

sampler s0;

struct VertexShaderOutput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR0;
    float2 TextureCoordinates : TEXCOORD0;
};


float ambient = 1.0f;
float percentThroughDay = 0.0f;

float4 MainPS(VertexShaderOutput input) : COLOR
{
    float4 pixelColor = tex2D(s0, input.TextureCoordinates);
    float4 outputColor = pixelColor;

    // lighting intensity is gradient of pixel position
    float Intensity = 1 + (1  - input.TextureCoordinates.y) * 1.3;
    outputColor.r = outputColor.r / ambient * Intensity;
    outputColor.g = outputColor.g / ambient * Intensity;
    outputColor.b = outputColor.b / ambient * Intensity;

    // sun set/rise blending 
    float exposeRed = (1 + (.39 - input.TextureCoordinates.y) * 8); // overexpose red
    float exposeGreen = (1 + (.39 - input.TextureCoordinates.y) * 2); // some extra green for the blue pixels
    float exposeBlue = (1 + (.39 - input.TextureCoordinates.y) * 6); // some extra blue 

    // happens over full screen
    if (input.TextureCoordinates.y < 1.0f) {

        float redAdder = max(1, (exposeRed * (percentThroughDay/0.25f))); // be at full exposure at 25% of day gone
        float greenAdder = max(1, (exposeGreen * (percentThroughDay/0.25f))); // be at full exposure at 25% of day gone
        float blueAdder = max(1, (exposeBlue * (percentThroughDay/0.25f))); // be at full exposure at 25% of day gone

        // begin reducing adders
        if (percentThroughDay >= 0.25f && percentThroughDay < 0.50f) {
            redAdder = max(1, (exposeRed * (1-(percentThroughDay - 0.25f)/0.25f)));
            greenAdder = max(1, (exposeGreen * (1-(percentThroughDay - 0.25f)/0.25f)));
            blueAdder = max(1, (exposeGreen * (1-(percentThroughDay - 0.25f)/0.25f)));
        }

        //mid day
        else if (percentThroughDay >= 0.50f && percentThroughDay < 0.75f) {
            redAdder = 1;
            greenAdder = 1;
            blueAdder = 1;
        }

        // add adders back for sunset
        else if (percentThroughDay >= 0.75f && percentThroughDay < 0.85f) {
            redAdder = max(1, (exposeRed * ((percentThroughDay - 0.75f)/0.10f)));
            greenAdder = max(1, (exposeGreen * ((percentThroughDay - 0.75f)/0.10f)));
            blueAdder = max(1, (exposeBlue * ((percentThroughDay - 0.75f)/0.10f)));
        }

        // begin reducing adders
        else if (percentThroughDay >= 0.85f) {
            redAdder = max(1, (exposeRed * (1-(percentThroughDay - 0.85f)/0.15f)));
            greenAdder = max(1, (exposeGreen * (1-(percentThroughDay - 0.85f)/0.15f)));
            blueAdder = max(1, (exposeBlue * (1-(percentThroughDay - 0.85f)/0.15f)));
        }

        outputColor.r = outputColor.r * redAdder;
        outputColor.g = outputColor.g * greenAdder;
        outputColor.b = outputColor.b * blueAdder;
    }

    return outputColor;
}

technique ambientLightDayNight
{
    pass P0
    {
        PixelShader = compile ps_2_0 MainPS();
    }
};

This works how I want it to for the most part (it could definitely use some calculation optimization though).

However, I am now looking at adding spotlights in my game for the player to use. I followed along with this method which I got working independently of the ambientLight shader. It is a pretty simple shader that uses a lightMask.

sampler s0;  

texture lightMask;  
sampler lightSampler = sampler_state{Texture = lightMask;};  

float4 PixelShaderLight(float2 coords: TEXCOORD0) : COLOR0  
{  
    float4 color = tex2D(s0, coords);  
    float4 lightColor = tex2D(lightSampler, coords);  
    return color * lightColor;  
}  


technique Technique1  
{  
    pass Pass1  
    {  
        PixelShader = compile ps_2_0 PixelShaderLight();  
    }  
}  

My problem is now using both of these shaders together. My current method is to draw my game scene to a render target, apply the ambient light shader, and then finish by drawing the gamescene (with the ambient light now) to the client screen while applying the spotlight shader.

This bring up multiple issues:

  • Applying the spotlight shader after the ambient light completely blacks out anything around the light, when in reality the area surrounding the light should be the ambient light.
  • The light intensity (how bright the light is) calculated in the spotlight shader is too dull when it is "night" because it is calculating the light color based on the ambient light shader's output.

I've tried to apply the ambient light shader after the spotlight shader instead, but this just renders most of everything black because the ambient light calculates against a mostly black background.

I've tried adding some code to the spotlight shader to color black pixels to white in order to reveal the ambient light background, however the light intensity is still being calculated against the darker ambient light - resulting in a very dull light.

Another thought was to just modify my ambient light shader to take the lightMask as a param and just not apply the ambient light to lights marked on the light mask. Then I could just use the spotlight shader to apply the graident of the light and modify the color. But I was unsure if I should be cramming these two seemingly separate light effects into one pixel shader. When I tried this, my shader also didn't compile because there were too many arithmetic ops.

So my questions for everyone are:

  • Should I avoid cramming multiple effects into one pixel shader?
  • Generally, how would I apply spot lighting over an ambient light effect that can be "dark"?

EDIT

my solution - Did not end up using the spot light shader, but still draw the light mask with the texture given in the article, then pass that light mask to this ambient light shader and offset the texture gradient.

float4 MainPS(VertexShaderOutput input) : COLOR
{

    float4 constant = 1.5f;
    float4 pixelColor = tex2D(s0, input.TextureCoordinates);
    float4 outputColor = pixelColor;

    // lighting intensity is gradient of pixel position
    float Intensity = 1 + (1  - input.TextureCoordinates.y) * 1.05;
    outputColor.r = outputColor.r / ambient * Intensity;
    outputColor.g = outputColor.g / ambient * Intensity;
    outputColor.b = outputColor.b / ambient * Intensity;

    // sun set/rise blending  
    float gval = (1 - input.TextureCoordinates.y); // replace 1 with .39 to lock to 39 percent of screen (this is how it was before)
    float exposeRed = (1 + gval * 8); // overexpose red
    float exposeGreen = (1 + gval * 2); // some extra green
    float exposeBlue = (1 + gval * 4); // some extra blue 

    float quarterDayPercent = (percentThroughDay/0.25f);
    float redAdder = max(1, (exposeRed * quarterDayPercent)); // be at full exposure at 25% of day gone
    float greenAdder = max(1, (exposeGreen * quarterDayPercent)); // be at full exposure at 25% of day gone
    float blueAdder = max(1, (exposeBlue * quarterDayPercent)); // be at full exposure at 25% of day gone

    // begin reducing adders
    if (percentThroughDay >= 0.25f ) {
        float gradientVal1 = (1-(percentThroughDay - 0.25f)/0.25f);
        redAdder = max(1, (exposeRed * gradientVal1));
        greenAdder = max(1, (exposeGreen * gradientVal1));
        blueAdder = max(1, (exposeGreen * gradientVal1));
    }

    //mid day
    if (percentThroughDay >= 0.50f) {
        redAdder = 1;
        greenAdder = 1;
        blueAdder = 1;
    }

    // add adders back for sunset
    if (percentThroughDay >= 0.75f) {
        float gradientVal2 = ((percentThroughDay - 0.75f)/0.10f);
        redAdder = max(1, (exposeRed * gradientVal2));
        greenAdder = max(1, (exposeGreen * gradientVal2));
        blueAdder = max(1, (exposeBlue * gradientVal2));
    }

    // begin reducing adders
    if (percentThroughDay >= 0.85f) {

        float gradientVal3 = (1-(percentThroughDay - 0.85f)/0.15f);
        redAdder = max(1, (exposeRed * gradientVal3));
        greenAdder = max(1, (exposeGreen * gradientVal3));
        blueAdder = max(1, (exposeBlue * gradientVal3));
    }

    outputColor.r = outputColor.r * redAdder;
    outputColor.g = outputColor.g * greenAdder;
    outputColor.b = outputColor.b * blueAdder;

    // first check if we are in a lightMask light
    float4 lightMaskColor = tex2D(lightSampler, input.TextureCoordinates);
    if (lightMaskColor.r != 0.0f || lightMaskColor.g != 0.0f || lightMaskColor.b != 0.0f) 
    {
        // we are in the light so don't apply ambient light
        return pixelColor * (lightMaskColor + outputColor) * constant; // have to offset by outputColor here because the lightMask is pure black
    }

    return outputColor * pixelColor * constant; // must multiply by pixelColor here to offset the lightMask bounds. TODO: could try to restore original color by removing this multiplaction and factoring in more of an offset on ln 91
}

Solution

  • To chain lights as you want, you need a different approach. As you already encountered, chaining lights solely on the color won't work, as once the color has become black it can't be highlighted anymore. To deal with multiple lights there are two typical approaches: forward shading and deferred shading. Each has its advantages and disadvantages, so you need to look which fits better your situation.


    Forward Shading

    This approach is the one you tested with stuffing all lighting computations in a single shading pass. You are adding all light intensities together to a final light intensity and then multiply it with the color.

    Pros are the performance and simplicity, Cons are the limitation in the amount of lights and more complex shader code.


    Deferred Shading

    This approach decouples single lights from each other and can be used to draw scenes with very many lights. Each light needs the original scene color (albedo) to compute its part of the final image. Therefore you first render your scene without any lighting onto a texture (usually called color buffer or albedo buffer). Then you can render each light separately with multiplying it with the albedo and adding it to the final image. So even in the dark parts the original color comes back again with a light.

    Pros are the cleaner structure and possibility to use a lot of lights, even with different shapes. Cons are the extra buffers and draw calls which have to be made.