XNA和HLSL:采样整个纹理的平均亮度

时间:2013-12-22 05:30:10

标签: c# xna hlsl

我正在尝试在我的3D应用中制作HDR渲染通道。

据我所知,为了获得场景上的平均光线,您需要将渲染输出下采样到1x1纹理。这是我现在正在努力的地方。

我已经设置了1x1分辨率渲染目标,我将绘制前一个渲染输出。我尝试使用简单的SpriteBatch Draw调用和使用目标矩形将输出绘制到该渲染目标。然而,希望我发现没有其他人想到的东西太过分了,结果实际上并不是整个场景下采样到1x1纹理,看起来好像只有左上角的像素被绘制,无论我多少玩目标矩形或缩放重载。

现在,我正在尝试进行另一个屏幕四边形渲染过程,使用着色器技术对场景进行采样,并将单个像素渲染到渲染目标中。所以公平地说,标题有点误导,我想要做的是对在表面上均匀分布的像素网格进行采样,然后对其进行平均。但这就是我难倒的地方。

我遇到过这个教程: http://www.xnainfo.com/content.php?content=28

在可以下载的文件中,有几个下采样的例子,我最喜欢的一个是使用经过16个像素的循环,将它们平均并返回。

到目前为止,我所做的一切都无法产生可行的输出。下采样纹理呈现在屏幕的角落以进行调试。

我已将HLSL代码修改为如下所示:

pixelShaderStruct vertShader(vertexShaderStruct input)
{
    pixelShaderStruct output;

    output.position = float4(input.pos, 1);
    output.texCoord = input.texCoord + 0.5f;

    return output;
};

float4 PixelShaderFunction(pixelShaderStruct input) : COLOR0
{
   float4 color = 0;
   float2 position = input.texCoord;

   for(int x = 0; x < 4; x++)
   {
        for (int y = 0; y < 4; y++)
        {
            color += tex2D(getscene, position + float2(offsets[x], offsets[y]));
        }
   }

   color /= 16;

   return color;
}

这条特殊的界限是我认为我犯了错误的地方:

color += tex2D(getscene, position + float2(offsets[x], offsets[y]));

我从未正确理解tex2D采样中使用的texCoord值如何工作。 在制作运动模糊效果时,我不得不转发这样的内部值,我担心它会被舍入到零,以便产生正常的效果,而其他时候,转发大值如30和50是必要的产生占据屏幕三分之一的效果。

所以无论如何,我的问题是:

给定一个屏幕四边形(如此,平面),如何增加或修改texCoord值以使像素网格均匀分布,在其上采样?

我尝试过使用:

color += tex2D(getscene, position + float2(offsets[x] * (1/maxX), offsets[y] * (1/maxY)));

其中maxX和maxY是屏幕分辨率,

color += tex2D(getscene, position + float2(offsets[x] * x, offsets[y] * y));

......和其他“在黑暗中拍摄”,所有结果都是相同的:最终得到的像素看起来与我屏幕正中间的像素相同,好像那是唯一一个被采样。

如何解决?

此外,这些纹理坐标如何工作? (0,0)在哪里?什么是最大值?

提前全部谢谢。

2 个答案:

答案 0 :(得分:1)

我已经解决了这个问题。

我相信使用十几个renderTargets,每一步的分辨率只有一半,但是我错了。

在2013年的中端GPU上,nVidia GTX 560,重新渲染10个渲染目标的成本并不明显,具体数字:从230 FPS下降至220 FPS。

解决方案如下。暗示你已经将整个场景处理并渲染到renderTarget,在我的例子中是“renderOutput”。

首先,我声明一个renderTarget数组:

public RenderTarget2D[] HDRsampling;

接下来,我计算在Load()方法中需要多少个目标,在Menu更新和游戏更新循环之间调用(用于加载菜单中不需要的游戏资源的过渡状态),并正确初始化它们: / p>

    int counter = 0;
    int downX = Game1.maxX;
    int downY = Game1.maxY;

    do
    {
        downX /= 2;
        downY /= 2;
        counter++;

    } while (downX > 1 && downY > 1);

    HDRsampling = new RenderTarget2D[counter];
    downX = Game1.maxX / 2;
    downY = Game1.maxY / 2;

    for (int i = 0; i < counter; i++)
    {
        HDRsampling[i] = new RenderTarget2D(Game1.graphics.GraphicsDevice, downX, downY);
        downX /= 2;
        downY /= 2;
    }

最后,C#渲染代码如下:

        if (settings.HDRpass)
        {   //HDR Rendering passes
            //Uses Hardware bilinear downsampling method to obtain 1x1 texture as scene average

            Game1.graphics.GraphicsDevice.SetRenderTarget(HDRsampling[0]);
            Game1.graphics.GraphicsDevice.Clear(ClearOptions.Target, Color.Black, 0, 0);
            downsampler.Parameters["maxX"].SetValue(HDRsampling[0].Width);
            downsampler.Parameters["maxY"].SetValue(HDRsampling[0].Height);
            downsampler.Parameters["scene"].SetValue(renderOutput);
            downsampler.CurrentTechnique.Passes[0].Apply();
            quad.Render();

            for (int i = 1; i < HDRsampling.Length; i++)
            {   //Downsample the scene texture repeadetly until last HDRSampling target, which should be 1x1 pixel
                Game1.graphics.GraphicsDevice.SetRenderTarget(HDRsampling[i]);
                Game1.graphics.GraphicsDevice.Clear(ClearOptions.Target, Color.Black, 0, 0);
                downsampler.Parameters["maxX"].SetValue(HDRsampling[i].Width);
                downsampler.Parameters["maxY"].SetValue(HDRsampling[i].Height);
                downsampler.Parameters["scene"].SetValue(HDRsampling[i-1]);
                downsampler.CurrentTechnique.Passes[0].Apply();
                quad.Render();

            }
            //assign the 1x1 pixel
            downsample1x1 = HDRsampling[HDRsampling.Length - 1];

            Game1.graphics.GraphicsDevice.SetRenderTarget(extract);
            //switch out rendertarget so we can send the 1x1 sample to the shader.
            bloom.Parameters["downSample1x1"].SetValue(downsample1x1);

        }

这将获得downSample1x1纹理,该纹理稍后在最终着色器的最终传递中使用。

实际下采样的着色器代码很简单:

texture2D scene;

sampler getscene = sampler_state
{
    texture = <scene>;
    MinFilter = linear;
    MagFilter = linear;
    MipFilter = point;
    MaxAnisotropy = 1;
    AddressU = CLAMP;
    AddressV = CLAMP;
};

float maxX, maxY;

struct vertexShaderStruct
{
    float3 pos      : POSITION0;
    float2 texCoord : TEXCOORD0;
};

struct pixelShaderStruct
{
    float4 position : POSITION0;
    float2 texCoord : TEXCOORD0;
};

pixelShaderStruct vertShader(vertexShaderStruct input)
{
    pixelShaderStruct output;

    float2 offset = float2 (0.5 / maxX, 0.5/maxY);
    output.position = float4(input.pos, 1);
    output.texCoord = input.texCoord + offset;

    return output;
};

float4 PixelShaderFunction(pixelShaderStruct input) : COLOR0
{
   return tex2D(getscene, input.texCoord);
}

technique Sample
{
    pass P1
    {
        VertexShader = compile vs_2_0 vertShader();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

你如何实现你的场景平均光度取决于你,我还在尝试所有这些,但我希望这有助于那里的人!

答案 1 :(得分:0)

您链接的教程很好地演示了如何进行Tonemapping,但听起来您还没有执行重复下采样来获取1x1图像。

您不能简单地拍摄高分辨率图像(例如1920x1080),取16个任意像素,将它们相加并除以16并称之为亮度。

您需要反复将源图像下采样为越来越小的纹理,通常每个维度的每个维度减半。所得到的下采样的每个像素是先前纹理上的2×2像素网格的平均值(这由双线性采样处理)。最终,您将获得1x1图像,该图像是整个原始1920x1080图像的平均颜色值,您可以从中计算源图像的平均亮度。

如果没有重复的下采样,您的亮度计算将是一个非常嘈杂的值,因为它的输入仅仅是原始图像中~2M像素中的16个。为了获得正确和平滑的亮度,原始图像中的每个像素都需要贡献。