§8.1处理Post-Screen Shader

实施Post-Screen Shader的第一件事是被渲染的场景,并把它传递到渲染目标上(见图8-1 )。下图显示了在FX Composer中的几个例子,在把shader包含在游戏引擎前能用来测试效果。

图8-1

在进入更复杂的Post-Screen Shader并通过新RenderToTexture类的帮助获得渲染目标之前,你应首先测试Pre-Screen Sky Cube Mapping shade,使它在你的引擎中有与在FX Composer中一样的效果。

Pre-Screen Sky Cube Mapping(立方映射)

首先你需要一个Sky Cube贴图,它由六个面组成,因为你只能看到其中一个方向,所以每个面的分辨率要高。如果你使用很多Post-Screen Shader,每个面分辨率512 × 512就可以了,1024×1024更好些,这样在高分辨率的屏幕上效果更好。

创建sky cube贴图不容易。您可以使用DirectX dxtexture工具把六个独立的面拼在一起,也可以用代码实现,但载入一个单独包含六个面的Cube Map文件要比载入六个分离贴图再自己把它们拼起来容易得多。还可以选择Photoshop导入6 * 512×512的纹理,并通过NVIDIA的DDS Exporter插件导出cube map dds(见图8-2)。

图8-2

如果您没有好看的cube map,也可以从网上下载,但大多数分辨率不高,作为测试够用了。你也可以使用本书的cube map,Rocket Commander使用了一个太空背景的cube map,六个面看上去略有不同,让你能傲游在3D空间中。(见图8-3)。

图8-3

看看将cube贴图显示在屏幕上的Shader代码。在渲染场景前调用此shader,因为它填充所有背景缓冲,所以你无需清除背景颜色(但仍可能要清楚z缓冲区)。要渲染此shader你应关闭depth comparing,因为不关心天空有多远,所以它应该总是被渲染。

下面的代码来自PresSreenSkyCubeMapping.fx:

struct VertexInput

{

// We only need 2d screen position, that's all.

float2 pos : POSITION;

};

struct VB_OutputPos3DTexCoord

{

float4 pos : POSITION;

float3 texCoord : TEXCOORD0;

};

VB_OutputPos3DTexCoord VS_SkyCubeMap(VertexInput In)

{

VB_OutputPos3DTexCoord Out;

Out.pos = float4(In.pos.xy, 0, 1);

// Also negate xy because the cube map is for MDX (left handed)

// and is upside down

Out.texCoord = mul(float4(-In.pos, scale, 0), viewInverse).xyz;

// And fix rotation too (we use better x, y ground plane system)

Out.texCoord = float3( -Out.texCoord.x*1.0f, -Out.texCoord.z*0.815f, -Out.texCoord.y*1.0f);

return Out;

} // VS_SkyCubeMap(..)

float4 PS_SkyCubeMap(VB_OutputPos3DTexCoord In) : COLOR

{

float4 texCol = ambientColor * texCUBE(diffuseTextureSampler, In.texCoord);

return texCol;

} // PS_SkyCubeMap(.)

technique SkyCubeMap < string Script = "Pass=P0;"; > {

pass P0 < string Script = "Draw=Buffer;"; >

{

ZEnable = false;

VertexShader = compile vs_1_1 VS_SkyCubeMap();

PixelShader = compile ps_1_1 PS_SkyCubeMap();

}

// pass P0

} // technique

SkyCubeMap 渲染shader你知需要Vector2的屏幕位置,它通过乘以反视矩阵被转换到sky Cube 的位置。Scale值被用来微调视野,但通常设为1。获得sky cube map的纹理坐标后,需要从左手坐标系转换到右手坐标系,应交换Y和Z坐标,改变纹理坐标指向,最后将Y值乘以0.815,使天空在4:3的屏幕上显示1:1的比例。所有这些量的调整都要通过单元测试检验直至显示正常。

如果您想要建立自己的sky cube贴图,您可以使用DirectX SDK中的DirectX Texture Tool,加载六个二维图像去生成。另外,许多工具都可以直接把三维场景渲染成cube map,如Bryce, 3D Studio Max等。因为这个sky cube贴图是为MDX左手坐标系设计的,所以在xna中要翻转一下。而渲染sky cube是简单的,只需抓取贴图颜色值并乘以环境光颜色(通常是白色的)。这个shader没用到Shader Model 2.0的高级功能,所以也能用于Shader Model 1.1。

使用以下单元测试:

public static void TestSkyCubeMapping()

{

PreScreenSkyCubeMapping skyCube = null;

TestGame.Start("TestSkyCubeMapping", delegate

{

skyCube = new PreScreenSkyCubeMapping();

},

delegate

{

skyCube.RenderSky();

});

} // TestSkyCubeMapping()

PreScreenSkyCubeMapping类继承自ShaderEffect类,在构造函数中添加了PreScreenSkyCubeMapping.fx,其他参数都已定义,重要的新方法是RenderSky,以下是基本代码:

AmbientColor = setSkyColor;

InverseViewMatrix = BaseGame.InverseViewMatrix;

// Start shader

// Remember old state because we will use clamp texturing here

effect.Begin(SaveStateMode.SaveState);

for (int num = 0; num < effect.CurrentTechnique.Passes.Count; num++)

{

EffectPass pass = effect.CurrentTechnique.Passes[num];

// Render each pass

pass.Begin();

VBScreenHelper.Render();

pass.End();

} // foreach (pass)

// End shader

effect.End();

首先设定参数,此shader 只用到环境光颜色和逆视矩阵两个参数。然后开始执行shader,通过VBScreenHelper的Render方法渲染所有pass(这里只有一个pass),post-screen shaders也使用这个方法。

VBScreenHelper生成一个很简单的屏幕,初始化所有顶点缓冲和处理渲染。以下代码初始化VertexPositionTexture格式的顶点缓冲区。你只能有一个此类的静态实例,且只用在pre-screen shaders和post-screen shaders中。

public VBScreen()

{

VertexPositionTexture[] vertices = new VertexPositionTexture[] {

new VertexPositionTexture( new Vector3(-1.0f, -1.0f, 0.5f), new Vector2(0, 1)), new VertexPositionTexture( new Vector3(-1.0f, 1.0f, 0.5f), new Vector2(0, 0)), new VertexPositionTexture( new Vector3(1.0f, -1.0f, 0.5f), new Vector2(1, 1)), new VertexPositionTexture( new Vector3(1.0f, 1.0f, 0.5f), new Vector2(1, 0)), };

vbScreen = new VertexBuffer( BaseGame.Device, typeof(VertexPositionTexture), vertices.Length, ResourceUsage.WriteOnly, ResourceManagementMode.Automatic);

vbScreen.SetData(vertices);

decl = new VertexDeclaration(BaseGame.Device, VertexPositionTexture.VertexElements);

} // VBScreen()

第5章和第6章已经讨论过,使用-1至1表示边界的最大和最小值,超过这些值将不会显示在屏幕上。投影矩阵会把像素放在真正的屏幕位置。这里使用简单的VertexPositionTexture格式的顶点。对于sky cube mapping shader,不需要纹理坐标,但后面的post-screen shaders需要。请注意,屏幕位置坐标+1指左下角,-1是右上角。如果您想把左上屏幕的纹理显示在左上角, the texture coordinates of 0, 0 should be in the –1, +1 corner。

创建VertexBuffers时应始终指定WriteOnly和Automatic,这样硬件能以最快的速度执行而无需担心从CPU过来的读过程,而Automatic使清除顶点缓冲变得容易,使xna能在顶点缓冲丢失必须重建时,自动重建顶点缓冲(通常发生在按下Alt +tab切换出全屏又切换回来时)。

有了这些新类,现在你能执行TestSkyCubeMapping单元测试,结果如图8-4所示。

图8-4

编写一个简单的Post-Screen Shader

Post-Screen Shader不容易,它包含了大量的测试、正确处理渲染目标,而在FX Composer和引擎中都能正常工作也挺难。为了让你的第一个Post-Screen Shader简单点,你可以添加一个最简单的效果。你只需将屏幕边界弄得暗一些,模拟电视机的效果。没有Post-Screen Shader要改变某些东西是不可能的,接下去你还要把整个屏幕变成黑白的。

在FX Composer中打开PostScreenDarkenBorder.fx文件在开头注释和描述之后定义一个脚本,脚本只被用在FX Composer中,表示这是Post-Screen Shader需通过特定方式渲染。以大写字母开头的量为常量。

游戏引擎和FX Composer不关心大小写,但通过上面这种方式你能容易地分辨哪些是不可改变的常量,哪些能被改变。ClearColor和ClearDepth只在FX Composer中使用,在程序中无需关心,因为你自己处理清理过程。测试中你可以将ClearColor设置成其他颜色,但通常这些值总是一样的,在Post-Screen Shader中默认采用相同值。

// This script is only used for FX Composer, most values here

// are treated as constants by the application anyway.

// Values starting with an upper letter are constants.

float Script : STANDARDSGLOBAL

<

string ScriptClass = "scene";

string ScriptOrder = "postprocess";

string ScriptOutput = "color";

// We just call a script in the main technique.

string Script = "Technique=ScreenDarkenBorder;";

> = 0.5;

const float4 ClearColor : DIFFUSE = { 0.0f, 0.0f, 0.0f, 1.0f};

const float ClearDepth = 1.0f;

Post-Screen Shader需要知道分辨率和渲染目标,接下来的代码用来处理窗口尺寸和场景贴图。窗口大小就是渲染的分辨率,而场景贴图渲染目标将整个场景作为一张贴图。请注意这里使用的VIEWPORTPIXELSIZE语义,它帮助FX Composer自动预览效果设置正确的值。如果你不使用VIEWPORTPIXELSIZE,将导致FX Composer无法使用正确的窗口大小,你只能在属性面板中自己设置。

// Render-to-Texture stuff

float2 windowSize : VIEWPORTPIXELSIZE;

texture sceneMap : RENDERCOLORTARGET

<

float2 ViewportRatio = { 1.0, 1.0 };

int MIPLEVELS = 1;

>;

sampler sceneMapSampler = sampler_state

{

texture = <sceneMap>;

AddressU = CLAMP;

AddressV = CLAMP;

AddressW = CLAMP;

MIPFILTER = NONE;

MINFILTER = LINEAR;

MAGFILTER = LINEAR;

};

为使屏幕边界变暗你还使用了一个名为ScreenBorderFadeout.dds的贴图(见图8-5):

// For the last pass we add this screen border fadeout map to darken the borders

texture screenBorderFadeoutMap : Diffuse

<

string UIName = "Screen border texture";

string ResourceName = "ScreenBorderFadeout.dds";

>;

sampler screenBorderFadeoutMapSampler = sampler_state

{

texture = <screenBorderFadeoutMap> ;

AddressU = CLAMP;

AddressV = CLAMP;

AddressW = CLAMP;

MIPFILTER = NONE;

MINFILTER = LINEAR;

MAGFILTER = LINEAR;

};

图8-5

提示:这个贴图必须与.fx文件一样被加入到内容管道,xna不会自动载入贴图,也同样不会自动载入包含材质与贴图的模型。

要实现将场景中的像素变暗的效果只需简单地乘以ScreenBorderFadeout贴图的颜色值。通过这种方式大多数像素保持不变(中间的白色部分),而边界上的像素越来越暗,但不是完全黑色,因为你只想让它变得暗一些。

看一下非常简单的vertex shader,它只将纹理坐标传递给Pixel Shader。为了能兼容Pixel Shader 1.1,你必须复制纹理坐标,因为你需要访问他们两次,一次是为场景贴图和一次是为ScreenBorderFadeout贴图。请注意,您增加了半个像素到纹理坐标中,以以解决一个非常普遍的问题:在DirectX (XNA)中所有像素有一个0.5像素的偏移,你把像素移动到正确位置去修复此问题。去修复此问题。

有关这个问题的讨论网上有很多资料。因为所以辅助类(例如:SpriteBatch)已经帮你处理了此问题,所以不用担心,但如果渲染自己的Post-Screen Shader不要忘了修复它。对这个Shader来说问题不大,但如果你有一个非常精确的shader that shadows certain pixels you want to make sure to completely hit the pixel position and not draw somewhere else。

struct VB_OutputPos2TexCoords

{

float4 pos : POSITION;

float2 texCoord[2] : TEXCOORD0;

};

VB_OutputPos2TexCoords VS_ScreenQuad( float4 pos : POSITION, float2 texCoord : TEXCOORD0)

{

VB_OutputPos2TexCoords Out;

float2 texelSize = 1.0 / windowSize;

Out.pos = pos;

// Don't use bilinear filtering

Out.texCoord[0] = texCoord + texelSize*0.5; Out.texCoord[1] = texCoord + texelSize*0.5;

return Out;

} // VS_ScreenQuad(..)

现在位置已经处在正确的空间中,只需把它psss over。接着Pixel Shader处理场景贴图,如果所有量都被正确将返回场景贴图的颜色值:

float4 PS_ComposeFinalImage(VB_OutputPos2TexCoords In) : COLOR

{

float4 orig = tex2D(sceneMapSampler, In.texCoord[0]);

return orig;

}

// PS_ComposeFinalImage(...)

在添加了ScreenDarkenBorder之后,你就能看到与FX Composer (见图8-6)中相同的结果。请注意:这里所有的语义只用在FX Composer 中! 在FX Composer 中使用标准的球模型去渲染pre-screen sky cube mapping shader。

// ScreenDarkenBorder technique for ps_1_1

technique ScreenDarkenBorder <

// Script stuff is just for FX Composer

string Script = "RenderColorTarget=sceneMap;"

"ClearSetColor=ClearColor; Clear=Color;"

"ClearSetDepth=ClearDepth; Clear=Depth;"

"ScriptSignature=color; ScriptExternal=;"

"Pass=DarkenBorder;";

>

{

pass DarkenBorder <

string Script = "RenderColorTarget0=; Draw=Buffer;";

>

{

VertexShader = compile vs_1_1 VS_ScreenQuad();

PixelShader = compile ps_1_1 PS_ComposeFinalImage(sceneMapSampler);

} // pass DarkenBorder

} // technique ScreenDarkenBorder

图8-6

改进

shader运行后就可测试(在FX Composer中,单击材质面板中的shader,然后选择“Apply to Scene”),你现在可轻易地修改输出。只需改变Pixel Shader中的最后一行代码:

return 1.0f - orig;

这将从每个组件的颜色值减去1.0(如果您没有指定float4(1,1,1,1),shader根据需要会自动将float转换到float3或float4)。这个公式将反转整个图象(见图8-7),看起来有趣,但处不大用。

图8-7

好了,回到您最初的任务:使用screen border纹理。要使边界变暗,您首先载入screen border纹理,然后再乘以初始场景纹理的颜色值。你只需返回结果,差不多快完成了(见图8-8):

float4 orig = tex2D(sceneSampler, In.texCoord[0]);

float4 screenBorderFadeout =tex2D(screenBorderFadeoutMapSampler, In.texCoord[1]);

float4 ret = orig;

ret.rgb *= screenBorderFadeout;

return ret;

图8-8

现在你可以应用亮度公式将图像转换为黑白的,只需告诉哪些组件的权重有多少(绿色始终是最引人注目的颜色):

// Returns luminance value of col to convert color to grayscale

float Luminance(float3 col)

{

return dot(col, float3(0.3, 0.59, 0.11));

} // Luminance(.)

这种方法也适用于场景纹理,只需修改Pixel Shader中的一条代码,就可实现想要的效果:使边界变暗并呈黑白显示(见图8-9):

float4 ret = Luminance(orig);

图8-9

一旦您有了基本设置,编写post-screen shaders可以很好玩,但在实现更酷的post-screen shaders效果前你应将这些Shader整合到游戏引擎中去,请看下一章节。


发布时间:2008/9/21 上午8:37:20  阅读次数:7985

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

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号