在黑暗环境光上应用聚光灯 - HLSL - Monogame
Applying Spotlights Over Dark Ambient Light - HLSL - Monogame
我为我的 Monogame 项目编写了一个 HLSL 着色器,它使用环境照明来创建 day/night 循环。
#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 method 独立于 ambientLight 着色器工作。这是一个使用 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();
}
}
我现在的问题是同时使用这两个着色器。我目前的方法是将我的游戏场景绘制到渲染目标,应用环境光着色器,然后通过在应用聚光灯着色器的同时将游戏场景(现在使用环境光)绘制到客户端屏幕。
这带来了多个问题:
- 在环境光完全遮蔽光周围的任何东西后应用聚光灯着色器,而实际上光周围的区域应该是环境光。
- 在聚光灯着色器中计算的光强度(光有多亮)在"night"时太暗了,因为它是根据环境光着色器的输出计算光色。
我尝试在聚光灯着色器之后应用环境光着色器,但这只会将大部分内容渲染为黑色,因为环境光是针对大部分黑色背景计算的。
我已经尝试向聚光灯着色器添加一些代码,将黑色像素着色为白色,以显示环境光背景,但是仍然根据较暗的环境光计算光强度 - 导致非常暗淡光。
另一个想法是只修改我的环境光着色器以将 lightMask 作为参数,而不是将环境光应用于光罩上标记的灯光。然后我可以只使用聚光灯着色器来应用光的渐变并修改颜色。但我不确定是否应该将这两种看似独立的光效塞进一个像素着色器中。当我尝试这个时,我的着色器也没有编译,因为有太多的算术操作。
所以我想问大家的问题是:
- 我应该避免将多种效果塞进一个像素着色器吗?
- 一般来说,我如何在环境光效果上应用聚光灯 "dark"?
编辑
我的解决方案——最终没有使用聚光灯着色器,但仍然使用文章中给出的纹理绘制光遮罩,然后将该遮光罩传递给此环境光着色器并偏移纹理渐变。
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
}
要随心所欲地链接灯,您需要采用不同的方法。正如您已经遇到的那样,仅在颜色上链接灯光是行不通的,因为一旦颜色变成黑色,就无法再突出显示。处理多个光源有两种典型的方法:前向着色和延迟着色。各有各的优点和缺点,所以你需要看看哪种更适合你的情况。
正向着色
此方法是您通过在单个着色过程中填充所有光照计算进行测试的方法。您将所有光强度加在一起得到最终的光强度,然后将其与颜色相乘。
优点是性能和简单性,缺点是灯光数量的限制和更复杂的着色器代码。
延迟着色
这种方法将单个灯光相互解耦,可用于绘制具有很多灯光的场景。每盏灯都需要原始场景颜色(反照率)来计算其在最终图像中的部分。因此,您首先在没有任何光照的情况下将场景渲染到纹理上(通常称为颜色缓冲区或反照率缓冲区)。然后,您可以通过将每个光与反照率相乘并将其添加到最终图像中来分别渲染每个光。因此,即使在黑暗的部分,原始颜色也会随着光线再次恢复。
优点是结构更简洁,可以使用很多灯,甚至是不同形状的灯。缺点是必须进行额外的缓冲区和绘制调用。
我为我的 Monogame 项目编写了一个 HLSL 着色器,它使用环境照明来创建 day/night 循环。
#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 method 独立于 ambientLight 着色器工作。这是一个使用 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();
}
}
我现在的问题是同时使用这两个着色器。我目前的方法是将我的游戏场景绘制到渲染目标,应用环境光着色器,然后通过在应用聚光灯着色器的同时将游戏场景(现在使用环境光)绘制到客户端屏幕。
这带来了多个问题:
- 在环境光完全遮蔽光周围的任何东西后应用聚光灯着色器,而实际上光周围的区域应该是环境光。
- 在聚光灯着色器中计算的光强度(光有多亮)在"night"时太暗了,因为它是根据环境光着色器的输出计算光色。
我尝试在聚光灯着色器之后应用环境光着色器,但这只会将大部分内容渲染为黑色,因为环境光是针对大部分黑色背景计算的。
我已经尝试向聚光灯着色器添加一些代码,将黑色像素着色为白色,以显示环境光背景,但是仍然根据较暗的环境光计算光强度 - 导致非常暗淡光。
另一个想法是只修改我的环境光着色器以将 lightMask 作为参数,而不是将环境光应用于光罩上标记的灯光。然后我可以只使用聚光灯着色器来应用光的渐变并修改颜色。但我不确定是否应该将这两种看似独立的光效塞进一个像素着色器中。当我尝试这个时,我的着色器也没有编译,因为有太多的算术操作。
所以我想问大家的问题是:
- 我应该避免将多种效果塞进一个像素着色器吗?
- 一般来说,我如何在环境光效果上应用聚光灯 "dark"?
编辑
我的解决方案——最终没有使用聚光灯着色器,但仍然使用文章中给出的纹理绘制光遮罩,然后将该遮光罩传递给此环境光着色器并偏移纹理渐变。
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
}
要随心所欲地链接灯,您需要采用不同的方法。正如您已经遇到的那样,仅在颜色上链接灯光是行不通的,因为一旦颜色变成黑色,就无法再突出显示。处理多个光源有两种典型的方法:前向着色和延迟着色。各有各的优点和缺点,所以你需要看看哪种更适合你的情况。
正向着色
此方法是您通过在单个着色过程中填充所有光照计算进行测试的方法。您将所有光强度加在一起得到最终的光强度,然后将其与颜色相乘。
优点是性能和简单性,缺点是灯光数量的限制和更复杂的着色器代码。
延迟着色
这种方法将单个灯光相互解耦,可用于绘制具有很多灯光的场景。每盏灯都需要原始场景颜色(反照率)来计算其在最终图像中的部分。因此,您首先在没有任何光照的情况下将场景渲染到纹理上(通常称为颜色缓冲区或反照率缓冲区)。然后,您可以通过将每个光与反照率相乘并将其添加到最终图像中来分别渲染每个光。因此,即使在黑暗的部分,原始颜色也会随着光线再次恢复。
优点是结构更简洁,可以使用很多灯,甚至是不同形状的灯。缺点是必须进行额外的缓冲区和绘制调用。