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:
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:
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
}
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.