问题

你想在场景中同时使用多个光源。

一个方法是使用多个光源绘制场景并将每个光源的影响混合在一起。当添加一个新光源时场景需要整个被重新绘制,这个方法无法拓展,因为帧频率会随着光源的增加按比例下降。

解决方案

本教程中将使用一个完全不同的方法。你将3D场景绘制到一张2D纹理中。然后,为这张纹理中的所有像素计算所有光源的光照。这意味着你要在2D纹理上进行逐像素的处理,但只需绘制3D场景一次。

但在进行光线计算时需要每个像素的初始3D位置,对吗?对。继续看下去如何做,整个过程分为3步,如图6-12所示。

在第一步中,你将整个3D场景绘制到一张纹理中(见教程3-8)—不是一张纹理而是一次三张纹理。下面是你想要的三张纹理:

  • 你要将每个像素的基本颜色绘制到第一张纹理中(图6-12中的左上图)。
  • 你需要将每个像素的法线转换为颜色并将这个颜色存储在第二张纹理中(图6-12中的中上图)。
  • 你要将每个像素的深度(像素与相机间的距离)存储在第三张纹理中(图6-12中的右上图)。

image

图6-12 deferred渲染中的三个步骤

前面已经说过,整个操作过程只需使用一个effect的一个pass进行一次,所以,这个操作在使用没有光照计算的方法绘制场景时开销相同(或者更少,因为effect非常简单)。

现在花点时间理解下面的文字。你需要将场景中的所有像素的深度值存储在一张纹理中。对每个像素,你还需要知道它的2D屏幕坐标,因为这个坐标和它的纹理坐标是相同的。这意味着通过某种方法,从每个像素的2D屏幕坐标中你可以重建它的3D位置。而且,你还存储了每个像素的3D法线。如果你重建了3D位置和法线,就可以计算像素上的光照了。

所以,这就是你接下去要做的事。在生成了三张纹理后,在第二步你要激活一个新的,干净的渲染目标。对这个新目标的每个像素,你将重建它的3D位置和3D法线。这让你可以计算第一光的光照值。最后你会得到一个包含第一个光照的shading贴图。

对每个光源重复上面的步骤,将它们的光照值添加到shading贴图中。最后,你会得到一张包含所有光照的shading贴图。这个过程如图6-12中的step II所示,显示了六个光源。

在第三步中,你将颜色贴图(在第一步中创建)和这个shading贴图(在第二步中创建)组合起来。如图6-12的step III所示。

使用Deferred渲染的优点

如果只是简单地为每个光源绘制场景并将它们组合起来,你必须将3D世界转换到屏幕空间中去。

这样的操作需要使用vertex和pixel shaders。vertex shader必须将每个顶点的3D位置转换到2D屏幕位置。pixel shaders必须计算比屏幕中的像素多得多的像素。例如,如果背景中的物体A首先被绘制,显卡会使用pixel shader 计算像素的颜色。如果接着绘制在物体A之前的物体B,显卡需要再次计算这些像素的颜色。这样,显卡计算的像素会大大增多。

简而言之,vertex shaders需要做大量的工作,pixel shaders需要处理比屏幕上的像素更多的像素。

使用deferred渲染,你只需在第一步中进行这样的操作一次。然后你将为每个光源在纹理上进行一些逐像素的处理,这个处理过程中只需处理像素一次。最后一步将颜色贴图和shading贴图组合在一起包含在了另一个逐像素处理过程中。

简而言之,只需进行一次将3D场景转换为2D空间的操作。对每个光源,你的显卡只需处理屏幕上的像素一次。相对而言,vertex shader做的工作也少很多,当使用多个光源时,使用deferred渲染方法pixel shader也会处理少得多的像素。

工作原理

Deferred渲染需要进行三个步骤,如下所述。

准备工作

每个步骤都要创建一个单独的HLSL文件。创建这些文件(Deferred1Scene. fx, Deferred2L ights . fx, and Deferred3 Final. fx)并在XNA代码中添加变量:

Effect effect1Scene; 
Effect effect2Lights; 
Effect effect3Final; 

别忘了在LoadContent 方法中加载它们:

effect1Scene = Content.Load<Effect>("Deferred1Scene"); 
effect2Lights = ontent.Load<Effect>("Deferred2Lights"); 
effect3Final = Content.Load<Effect>("Deferred3Final"); 

确保在LoadContents方法中加载所需的几何数据。本例中,我将从一个顶点数组绘制一个简单的屋子,在InitSceneVertices方法中进行初始化:

InitSceneVertices();
InitFullscreenVertices(); 

最后一行代码初始化第二个顶点数组,定义了两个大三角形覆盖了整个屏幕。它们被用在了第二步和第三步中,当你使用自己的pixel shaders绘制全屏纹理时,允许你逐像素地处理全屏纹理。InitFullScreenVertices方法来自于教程2-12。

然后为了保持代码清晰,定义了一个RenderScene方法,这个方法以一个effect为参数,使用这个effect绘制整个屏幕。这个简单例子只从顶点数组中绘制三面带纹理的墙和一面地板。如果场景中包含模型,确保也使用这个effect绘制这些模型:

private void RenderScene(Effect effect) 
{
    //Render room 
    effect.Parameters["xWorld"].SetValue(Matrix.Identity); 
    effect.Parameters["xTexture"].SetValue(wallTexture); 
    
    effect.Begin(); 
    foreach (EffectPass pass in effect.CurrentTechnique.Passes) 
    {
        pass.Begin(); 
        device.VertexDeclaration = wallVertexDeclaration; 
        device.DrawUserPrimitives<VertexPositionNormalTexture> (PrimitiveType.TriangleStrip, wallVertices, 0, 14); 
        pass.End(); 
    }
    effect.End(); 
}
第一步:将3D场景绘制到三张纹理中

在第一步中,你将场景绘制到三张纹理中。这些纹理需要包含基本颜色,3D法线和屏幕上每个像素的深度值。深度表示相机和物体上对应像素的距离。

这里只使用一个pixel shader。pixel shader将一次渲染到三张纹理而不是一个。

XNA Code

首先定义渲染目标:

RenderTarget2D colorTarget; 
RenderTarget2D normalTarget; 
RenderTarget2D depthTarget; 

在LoadContent方法中进行初始化:

PresentationParameters pp = device.PresentationParameters; 
int width = pp.BackBufferWidth; 
int height = pp.BackBufferHeight; 
colorTarget = new RenderTarget2D(device, width, height, 1, SurfaceFormat.Color); 
normalTarget = new RenderTarget2D(device, width, height, 1, SurfaceFormat.Color); 
depthTarget = new RenderTarget2D(device, width, height, 1, SurfaceFormat.Single); 

因为法线有三个分量,你将它存储为一个Color。深度值为一个single float值。当pixel shader写入多个渲染目标时,它们的格式必须是相同大小的。Color的每个分量使用8 bits (256 个可能值),所以Color使用32 bits。一个float也使用32 bits,所以不会出错。

创建了渲染目标后就可以进行绘制了。下面的方法处理了整个第一步的过程,所以要在Draw方法的第一行中调用:

private void RenderSceneTo3RenderTargets() 
{
    //bind render targets to outputs of pixel shaders 
    device.SetRenderTarget(0, colorTarget); device.SetRenderTarget(1, normalTarget); 
    device.SetRenderTarget(2, depthTarget); 
    
    //clear all render targets 
    device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1, 0); 
    
    //render the scene using custom effect writing to all targets simultaneously 
    effect1Scene.CurrentTechnique = effect1Scene.Techniques["MultipleTargets"]; 
    effect1Scene.Parameters["xView"].SetValue(fpsCam.ViewMatrix); 
    effect1Scene.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); 
    RenderScene(effect1Scene); 
    
    //deactivate render targets to resolve them 
    device.SetRenderTarget(0, null); 
    device.SetRenderTarget(1, null); 
    device.SetRenderTarget(2, null); 
    
    //copy contents of render targets into texture 
    colorMap = colorTarget.GetTexture(); 
    normalMap = normalTarget.GetTexture(); 
    depthMap = depthTarget.GetTexture(); 
} 

首先,你将三个渲染目标绑定到pixel shaders中的COLOR0, COLOR1和COLOR2。请确保将它们的内容清空为黑色,(更重要)z-buffer设置为1 (见教程2-1)。

初始化结束后,就可以绘制场景了。使用MultipleTargets technique,这个technique将在下面定义。设置World, View和Projection矩阵(World矩阵必须在RenderScene方法中设置,因为场景中每个对象的世界矩阵是不同的)。通过将MultipleTargets technique传递到RenderScene绘制场景。

RenderScene方法完成后,三个渲染目标就会包含屏幕上每个像素的颜色,法线和深度值。在将它们保存到纹理之前,需要关闭它们(见教程3-8)。

HLSL代码

你仍需定义MultipleTargets technique,这个technique一次将场景绘制到三张纹理中。首先定义XNA-to-HLSL变量:

float4x4 xWorld; 
float4x4 xView; 
float4x4 xProjection; 

Texture xTexture; 
sampler TextureSampler = sampler_state
{
    texture = <xTexture> ; 
    agfilter = LINEAR; 
    minfilter = LINEAR; 
    mipfilter=LINEAR; 
    AddressU = wrap; 
    AddressV = wrap; 
};

像以往一样,你需要定义World,View和Projection矩阵。因为房间的墙和地板都带有纹理,还需要一个texture用来采样颜色。这些颜色会被保存到第一个渲染目标中。

下面是vertex 和pixel shaders的output结构:

struct VertexToPixel 
{
    float4 Position : POSITION; 
    float3 Normal : TEXCOORD0; 
    float4 ScreenPos : TEXCOORD1; 
    float2 TexCoords: TEXCOORD2; 
}; 

struct PixelToFrame 
{
    float4 Color: COLOR0; 
    float4 Normal: COLOR1; 
    float4 Depth: COLOR2; 
}; 

在必须的Position之后,vertex shader还将法线传递到pixel shader以使它可以存储到第二个渲染目标。另外,因为pixel shader需要将深度值保存到第三个渲染目标中,你还需要将屏幕坐标传递到pixel shader。屏幕坐标的X和Y分量包含了当前像素的屏幕坐标,Z分量包含深度。

最后,pixel shader需要纹理坐标从纹理中对应的位置采样颜色。

非常重要的是pixel shader的output结构。不像本书的其他任何一个部分,本例中的pixel shader会生成多个output。你的pixel shader不仅会写入COLOR0,还会写入COLOR1和COLOR2 .显然这些output对应三个渲染目标。

先讨论简单的vertex shader:

VertexToPixel MyVertexShader(float4 inPos: POSITION0, float3 inNormal: NORMAL0, float2 inTexCoords: TEXCOORD0)
{
    VertexToPixel Output = (VertexToPixel)0; 
    
    float4x4 preViewProjection = mul(xView, xProjection); 
    float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); 
    Output.Position = mul(inPos, preWorldViewProjection); 
    float3x3 rotMatrix = (float3x3)xWorld; 
    float3 rotNormal = mul(inNormal, rotMatrix); 
    Output.Normal = rotNormal; 
    Output.ScreenPos = Output.Position; 
    Output.TexCoords = inTexCoords; 
    
    return Output; 
} 

3D位置转换为2D屏幕位置很简单。法线通过世界矩阵中的旋转部分进行旋转(见教程6-5)。纹理坐标直接输出到output,2D屏幕坐标复制到ScreenPos变量中。

下面是pixel shader:

PixelToFrame MyPixelShader(VertexToPixel PSIn) 
{
    PixelToFrame Output = (PixelToFrame)0; 
    Output.Color.rgb = tex2D(TextureSampler, PSIn.TexCoords); 
    Output.Normal.xyz = PSIn.Normal/2.0f+0.5f; 
    Output.Depth = PSIn.ScreenPos.z/PSIn.ScreenPos.w; 
    
    return Output; 
} 

这很简单。颜色从纹理中采样(本例中是墙上的砖块纹理)并存储在第一个渲染目标中。然后是法线,因为3D法线的每个分量定义在[–1,1]区间,你需要将它们转换到[0,1]区间,这样它才可以存储为一个颜色分量。你可以将这个值除以2然后加0.5实现上述目的。

最后,需要在第三个渲染目标中存储深度值。深度值存储在ScreenPos变量的Z分量中。因为ScreenPos是4 × 4矩阵乘法额结果,所以它是一个4 × 1向量。在可以使用前三个分量前,你需要将它们除以第四个分量,这就是pixel shader中最后一行代码进行的操作。

下面是technique定义:

technique MultipleTargets 
{
    pass Pass0
    {
        VertexShader = compile vs_2_0 MyVertexShader(); 
        PixelShader = compile ps_2_0 MyPixelShader(); 
    }
}
第一步的总结

第一步结束后,你生成并存储了三个纹理:第一个纹理包含基本颜色,第二个包含法线,第三个包含深度。

第二步:生成Shading贴图

知道了每个像素的颜色,法线和深度后,就可以进行光照计算了。这需要让显卡绘制两个覆盖整个屏幕的三角形,让你可以创建一个pixel shader用来被屏幕上的每个像素调用。在这个pixel shader中,你将计算一个光源施加在一个像素上的光照值。

这个过程对场景中的每个光源进行重复,这些重复过程对应图6-12中step II 的六张图像,因为这个例子使用了六个光源。本例中展示的是如何计算聚光灯的光照。

注意:如果你想添加一个不同的光源,需要调整光照计算。这只是pixel shader中的一小部分代码,其他部分保持不变。

HLSL代码

简而言之,这个effect将从深度贴图中采样每个像素的深度以重建像素的3D位置。知道了3D位置,就可以进行光照计算了。

要重新创建3D位置,你需要反转ViewProjection矩阵和深度贴图。而且还需要法线贴图和一些变量设置聚光灯(见教程6-8):

float4x4 xViewProjectionInv; 
float xLightStrength; 
float3 xLightPosition; 
float3 xConeDirection; 
float xConeAngle; 
float xConeDecay; 

Texture xNormalMap; 
sampler NormalMapSampler = sampler_state 
{
    texture = <xNormalMap> ; 
    magfilter = LINEAR;
    minfilter = LINEAR; 
    mipfilter=LINEAR; 
    AddressU = mirror; 
    AddressV = mirror; 
}; 

Texture xDepthMap; 
sampler DepthMapSampler = sampler_state 
{
    texture = <xDepthMap> ; 
    magfilter = LINEAR; 
    minfilter = LINEAR; 
    mipfilter=LINEAR; 
    AddressU = mirror; 
    AddressV = mirror; 
}; 

然后是vertex和pixel shader的output结构。vertex shader生成2D屏幕坐标。还需要纹理坐标,这样每个像素才能从正确的位置采样法线贴图和深度贴图。

这次,pixel shader只需生成一个output值:当前光源对当前像素的光照值。

struct VertexToPixel 
{
    float4 Position : POSITION; 
    float2 TexCoord : TEXCOORD0; 
}; 

struct PixelToFrame 
{
    float4 Color : COLOR0; 
}; 

因为在InitFullscreenVertices方法中定义的六个顶点已经定义在屏幕坐标中(位于[(–1,–1),(1,1)]区间)了,vertex shader只需简单地将位置和纹理坐标传递到output:

VertexToPixel MyVertexShader(float4 inPos: POSITION0, float2 texCoord: TEXCOORD0) 
{
    VertexToPixel Output = (VertexToPixel)0; 
    Output.Position = inPos; 
    Output.TexCoord = texCoord; 
    
    return Output; 
} 

颜色处理都在pixel shader中。首先从法线贴图和深度贴图中采样法线和深度值:

PixelToFrame MyPixelShader(VertexToPixel PSIn) : COLOR0 
{
    PixelToFrame Output = (PixelToFrame)0; 
    
    float3 normal = tex2D(NormalMapSampler, PSIn.TexCoord).rgb; 
    normal = normal*2.0f-1.0f; 
    normal = normalize(normal); 
    float depth = tex2D(DepthMapSampler, PSIn.TexCoord); 
}

深度值可以立即从深度贴图中采样。而法线必须首先将[0,1]区间重新映射到[–1,–1]区间,这是第一步中的逆操作。

下一步是重新构建像素的3D位置。要获取这个位置,首先需要当前像素的屏幕位置。 当前像素的纹理坐标适合做这件事,但它需要从[0,1]纹理坐标映射到[–1,1]屏幕坐标。屏幕坐标的Y需要取负值:

float4 screenPos; 
screenPos.x = PSIn.TexCoord.x*2.0f-1.0f; 
screenPos.y = -(PSIn.TexCoord.y*2.0f-1.0f); 

但是,屏幕位置还有第三个分量:相机和像素的距离。这就是你为什么生成第二个渲染目标的原因。因为知道了深度,就知道了第三个分量:

screenPos.z = depth; 
screenPos.w = 1.0f; 

第四个分量是需要的,因为接下来你要将这个矢量与一个4 × 4矩阵相乘。你可以通过把第四个分量设置为1将一个Vector3变成一个Vector4。

现在有了像素的屏幕坐标,但你想获取3D位置。还记得你可以通过把3D位置乘以ViewProjection矩阵(教程2-1)将一个3D位置转换成2D屏幕位置吗?所以,如何进行相反的操作——将2D屏幕位置转换为3D位置?这很简单,只需乘以ViewProjection的逆矩阵:

float4 worldPos = mul(screenPos, xViewProjectionInv); 
worldPos /= worldPos.w;

矩阵的逆矩阵由XNA代码设置并计算,这很容易做到。

向量与4 × 4矩阵的计算结果返回一个同源(homogenous)向量,在使用前你需要将它前三个分量除以第四个分量。

最后获得了像素的3D位置。你还知道了像素的3D法线。有了这两者,就可以进行任何光照计算了。pixel shader的其余部分计算了一个聚光灯的光照值(来自于教程6-8),下面是完整的pixel shader代码:

PixelToFrame MyPixelShader(VertexToPixel PSIn) : COLOR0 
{
    PixelToFrame Output = (PixelToFrame)0; 
    float3 normal = tex2D(NormalMapSampler, PSIn.TexCoord).rgb; 
    normal = normal*2.0f-1.0f; normal = normalize(normal); 
    float depth = tex2D(DepthMapSampler, PSIn.TexCoord).r; 
    float4 screenPos; 
    screenPos.x = PSIn.TexCoord.x*2.0f-1.0f; 
    screenPos.y = -(PSIn.TexCoord.y*2.0f-1.0f); 
    screenPos.z = depth; screenPos.w = 1.0f; 
    float4 worldPos = mul(screenPos, xViewProjectionInv); 
    worldPos /= worldPos.w; 
    float3 lightDirection = normalize(worldPos - xLightPosition); 
    float coneDot = dot(lightDirection, normalize(xConeDirection)); 
    
    bool coneCondition = coneDot >= xConeAngle; 
    float shading = 0; 
    if (coneCondition) 
    {
        float coneAttenuation = pow(coneDot, xConeDecay); 
        shading = dot(normal, -lightDirection); 
        shading *= xLightStrength; 
        shading *= coneAttenuation; 
    }
    Output.Color.rgb = shading; 
    
    return Output; 
} 

下面是technique定义:

technique DeferredSpotLight 
{
    pass Pass0
    {
        VertexShader = compile vs_2_0 MyVertexShader(); 
        PixelShader = compile ps_2_0 MyPixelShader(); 
    }
} 

你创建了一个effect,这个effect从一张深度贴图,一张法线贴图和一个聚光灯开始,创建了一个包含聚光灯光照可见范围的shading贴图。

XNA代码

在XNA代码中,你将对场景中的每个光源调用这个effect。要管理光源,应该创建一个结构,保存聚光灯的所有细节:

public struct SpotLight 
{
    public Vector3 Position; 
    public float Strength; 
    public Vector3 Direction; 
    public float ConeAngle; 
    public float ConeDecay; 
} 

在项目中添加这些对象的数组:

SpotLight[] spotLights; 

对它进行初始化以存储一些光源:

spotLights = new SpotLight[NumberOfLights]; 

现在你就可以定义每个聚光灯了。你可以在Update方法中改变它们的设置,让你可以让这些光源绕着场景旋转!

然后,你将创建一个可以以SpotLight对象为参数的方法,这个方法将这个聚光灯的光照值绘制到渲染目标中:

private void AddLight(SpotLight spotLight) 
{
    effect2Lights.CurrentTechnique = effect2Lights.Techniques["DeferredSpotLight"]; 
    effect2Lights.Parameters["xNormalMap"].SetValue(normalMap); 
    effect2Lights.Parameters["xDepthMap"].SetValue(depthMap); 
    effect2Lights.Parameters["xLightPosition"].SetValue(spotLight.Position); 
    effect2Lights.Parameters["xLightStrength"].SetValue(spotLight.Strength); 
    effect2Lights.Parameters["xConeDirection"].SetValue(spotLight.Direction); 
    effect2Lights.Parameters["xConeAngle"].SetValue(spotLight.ConeAngle); 
    effect2Lights.Parameters["xConeDecay"].SetValue(spotLight.ConeDecay); 
    Matrix viewProjInv = Matrix.Invert(fpsCam.ViewMatrix * fpsCam.ProjectionMatrix); 
    effect2Lights.Parameters["xViewProjectionInv"].SetValue(viewProjInv); 
    
    effect2Lights.Begin(); 
    foreach (EffectPass pass in effect2Lights.CurrentTechnique.Passes) 
    {
        pass.Begin(); 
        device.VertexDeclaration = fsVertexDeclaration; 
        device.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleStrip, fsVertices, 0, 2); 
        pass.End(); 
    }
    effect2Lights.End(); 
} 

首先选择你刚才定义的HLSL technique。然后,传递法线贴图和深度贴图,这两个贴图都是在第一步中创建的。接下来的代码传递聚光灯的设置。最后一个变量设置ViewProjection矩阵的逆矩阵,这个逆矩阵可以简单地使用Matrix. Invert方法得到。

定义完所有变量后,显卡绘制两个覆盖整个屏幕的三角形。这样显卡就可以对屏幕上的每个像素计算当前聚光灯的光照值。

AddLight方法绘制一个光源的光照值。你可以为每个聚光灯调用这个方法并将它们的光照值加在一起!这可以通过使用additive alpha混合做到。使用additive alpha混合,每个光照值都会被添加到相同的渲染目标中。

private Texture2D GenerateShadingMap() 
{
    device.SetRenderTarget(0, shadingTarget); 
    device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1, 0); 
    
    device.RenderState.AlphaBlendEnable = true; 
    device.RenderState.SourceBlend = Blend.One; 
    device.RenderState.DestinationBlend = Blend.One; 
    
    for (int i = 0; i < NumberOfLights; i++) 
        AddLight(spotLights[i]); 
    device.RenderState.AlphaBlendEnable = false; 
    device.SetRenderTarget(0, null); 
    
    return shadingTarget.GetTexture(); 
} 

GenerateShadingMap方法首先开启一个新的叫做shadingTarget的渲染目标。首先要清除前面的内容。然后,打开additive alpha混合并将所有的光照值添加到渲染目标。然后关闭alpha混合 blending防止与后面的渲染混在一起。最后,渲染目标的内容被保存到一张纹理,并返回这个纹理。

这个方法应该在Draw方法中的第二行中调用:

shadingMap = GenerateShadingMap(); 

还需要在项目中添加shadingTarget和shadingMap变量:

RenderTarget2D shadingTarget; Texture2D shadingMap; 

在LoadContent方法中初始化渲染目标:

shadingTarget = new RenderTarget2D(device, width, height, 1, SurfaceFormat.Color); 

因为这个渲染目标包含屏幕上每个像素的光照值,所以必须拥有与屏幕相同的大小。

第二步的总结

现在,你有了一张包含屏幕的每个像素光照值的shading贴图。

第三步:组合颜色贴图和Shading贴图

最后一步很简单。在第一步中每个像素的基本颜色存储在colorMap中。第二步中每个像素的光照值存储在shadingMap中。在第三步中,你只需简单地将两者相乘获取最终的颜色。

HLSL代码

effect接受colorMap和shadingMap纹理。要照亮场景中没有被聚光灯照到的部分,你需要添加一个小小的环境光:

float xAmbient; 

Texture xColorMap; 
sampler ColorMapSampler = sampler_state 
{
    texture = <xColorMap> ; 
    magfilter = LINEAR; 
    minfilter = LINEAR; 
    mipfilter=LINEAR; 
    AddressU = mirror; 
    AddressV = mirror; 
}; 

Texture xShadingMap; 
sampler ShadingMapSampler = sampler_state 
{
    texture = <ShadingMap>; 
    magfilter = LINEAR; 
    minfilter = LINEAR; 
    mipfilter=LINEAR; 
    AddressU = mirror; 
    AddressV = mirror; 
}; 

vertex shader和pixel shader的output结构,vertex shader本身与前面的教程中完全一样。这是因为vertex shader将接受六个顶点定义两个覆盖整个屏幕的三角形。

struct VertexToPixel 
{
    float4 Position : POSITION; 
    float2 TexCoord : TEXCOORD0; 
}; 

struct PixelToFrame 
{
    float4 Color : COLOR0; 
}; 

// Technique: 
CombineColorAndShading VertexToPixel MyVertexShader(float4 inPos: POSITION0, float2 texCoord: TEXCOORD0) 
{
    VertexToPixel Output = (VertexToPixel)0; 
    Output.Position = inPos; 
    Output.TexCoord = texCoord; 
    
    return Output; 
} 

pixel shader很简单:

PixelToFrame MyPixelShader(VertexToPixel PSIn) : COLOR0 
{
    PixelToFrame Output = (PixelToFrame)0; 
    float4 color = tex2D(ColorMapSampler, PSIn.TexCoord); 
    float shading = tex2D(ShadingMapSampler, PSIn.TexCoord); 
    Output.Color = color*(xAmbient + shading); 
    
    return Output; 
} 

你采样color和shading值,添加环境光并将它们相乘。最终的颜色传递到渲染目标。

下面是technique定义:

technique CombineColorAndShading 
{
    pass Pass0 
    {
        VertexShader = compile vs_2_0 MyVertexShader(); 
        PixelShader = compile ps_2_0 MyPixelShader(); 
    }
}
XNA代码

effect需要在Draw方法的最后被调用。CombineColorAndShading方法选择technique,传递color和shading贴图,设置环境光。最后,使用刚才定义的technique绘制两个三角形:

private void CombineColorAndShading() 
{
    effect3Final.CurrentTechnique= effect3Final.Techniques["CombineColorAndShading"]; 
    effect3Final.Parameters["xColorMap"].SetValue(colorMap); 
    effect3Final.Parameters["xShadingMap"].SetValue(shadingMap); 
    effect3Final.Parameters["xAmbient"].SetValue(0.3f); 
    
    effect3Final.Begin(); 
    foreach (EffectPass pass in effect3Final.CurrentTechnique.Passes) 
    {
        pass.Begin(); 
        device.VertexDeclaration = fsVertexDeclaration; 
        device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleStrip, fsVertices, 0, 2); 
        pass.End(); 
    }
    effect3Final.End(); 
} 

这两个三角形需要被绘制到屏幕而不是渲目标。

第三步的总结

对屏幕上的每个像素,你组合了基本颜色和光照强度。

代码

所有的effect文件、对应deferred shading的主要方法前面已经写过了。因为你将代码分解成几个方法,所以Draw方法非常清晰:

protected override void Draw(GameTime gameTime) 
{
    //render color, normal and depth into 3 render targets 
    RenderSceneTo3RenderTargets(); 
    
    //Add lighting contribution of each light onto shadingMap 
    shadingMap = GenerateShadingMap(); 
    
    //Combine base color map and shading map 
    CombineColorAndShading(); 
    
    base.Draw(gameTime); 
} 
性能技巧

对每个光源,你的pixel shader将计算屏幕上所有像素的光照值。要减少第二步中处理像素的数量,你可以只绘制屏幕中被光照影响的部分而不是整个屏幕。这可以通过调整两个三角形的坐标实现。

image