§8.1处理Post-Screen Shader
实施Post-Screen Shader的第一件事是被渲染的场景,并把它传递到渲染目标上(见图8-1 )。下图显示了在FX Composer中的几个例子,在把shader包含在游戏引擎前能用来测试效果。
在进入更复杂的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)。
如果您没有好看的cube map,也可以从网上下载,但大多数分辨率不高,作为测试够用了。你也可以使用本书的cube map,Rocket Commander使用了一个太空背景的cube map,六个面看上去略有不同,让你能傲游在3D空间中。(见图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所示。
编写一个简单的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;
};
提示:这个贴图必须与.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
改进
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),看起来有趣,但处不大用。
好了,回到您最初的任务:使用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;
现在你可以应用亮度公式将图像转换为黑白的,只需告诉哪些组件的权重有多少(绿色始终是最引人注目的颜色):
// 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);
一旦您有了基本设置,编写post-screen shaders可以很好玩,但在实现更酷的post-screen shaders效果前你应将这些Shader整合到游戏引擎中去,请看下一章节。
发布时间:2008/9/21 上午8:37:20 阅读次数:7985