6.10 使用Deferred Shading在场景中添加多光源

问题

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

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

解决方案

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

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

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

图6-12

图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将计算屏幕上所有像素的光照值。要减少第二步中处理像素的数量,你可以只绘制屏幕中被光照影响的部分而不是整个屏幕。这可以通过调整两个三角形的坐标实现。

程序截图


发布时间:2009/9/30 上午9:03:31  阅读次数:7292

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号