Post-Screen Shaders和Rocket Commander游戏

1010 篇文章 11 订阅
786 篇文章 9 订阅
 

§8.0概览

上几章编写游戏引擎挺有趣,那么下一个游戏是什么呢?别担心现在你将遇到一个真正酷的游戏:Rocket Commander。最初版本是在2006年3月发布在www.coding4fun.de网站上,那时Visual Studio 2005也在德国推出,一个月后被试玩了几千次后来英文版的视频教程也出现了,并有了两个MOD版本。半年后,在线试玩达十万次,离线试玩更多,更多的MOD版本被制作出来。游戏引擎被证明很可靠,有两个商业游戏也采用了Rocket Commander游戏引擎,它们是:Pizza Commander 和Rocket Racer。

Rocket Commander游戏和源代码是免费下载的,也鼓励你修改它并由此制作自己的游戏。我收到很多第一次学习C#编程的人们寄来的电子邮件,告诉我他们通过学习coding4fun英文和德文网站上的源代码和视频教程,在很短时间内制作出了自己的引擎和游戏。

可以说Rocket Commander游戏是成功的。我没有因此变得富有但出了名。当2006年12月XNA Game Studio Express发布后,我又开始制作Rocket Commander的XNA版本,让它在Xbox 360上也能玩到。XNA版本性能很好,比老版本运行得更好,即使在HDTV分辨率(1920×1080,希望我也能有这样一个显示器或电视)下渲染几千个高细节的小行星模型也没出现任何问题。新版本的最大改进是支持多核,这对今天的CPU和X360都是很重要的。游戏在另一个CPU内核中执行一个额外的线程去计算物理效果、碰撞检测等,而不占用渲染线程因为这个线程总是忙于将数据传递给GPU。

在进入Rocket Commander之前,先要学习Post-Screen Shader,这也是Rocket Commander引擎和物理效果(关于物理效果的更多知识可参见第13章)。3D物体shader渲染的大部分知识前面已经学习过了,如果你对游戏逻辑感兴趣,可参见源码和Rocket Commander视频教程。

本书的其余部分当然仍要用到XnaGraphicEngine,深入优化渲染系统或物理效果的改进将在以后的项目中用到。在接下来的几个游戏中,包括Rocket Commander,都用到了本章要介绍的Pre-Screen和Post-Screen Shader。

 Pre-Screen Shader在3D场景被渲染前执行,让你能渲染贴图、天空盒、背景效果等,这个shader是很容易的。

Post-Screen Shader复杂得多,它在渲染过程的最后被执行,将整个场景作为一个贴图,在shader中经过几个步骤的调整,渲染到目标,将最终效果显示在屏幕上。获得正确的渲染目标并不容易,但一旦成功你就能容易地插入更多的Post-Screen Shader。不要滥用发光和运动模糊效果,这些效果虽然酷,但过度使用不会让你的游戏变得更好。你应确保Post-Screen Shader运行得尽可能快,因为在高分辨情况下会严重影响性能,特别是当你用5个以上的pass去调整屏幕上的每个像素的时候。

 

§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整合到游戏引擎中去,请看下一章节。

 

 

§8.2实现Post-Screen Shaders

VBScreenHelper和PreScreenSkyCubeMapping类前面已经介绍过了,但实现Post-Screen Shader还需要渲染目标,它由xna中的RenderTarget类处理。这个类的主要问题是你仍要调用很多方法并自己处理很多事情。特别是如果你还想为渲染目标使用深度缓冲并在设置后储存,这对阴影映射shader是很有用的。

RenderToTexture类

一个新的辅助类RenderToTexture提供了重要的方法,使Post-Screen Shader更容易处理(见图8-10)。最重要的方法是构造函数(使用SizeType作为参数),Resolve和SetRenderTarget,通过属性获得XnaTexture和RenderTarget也很有用。请注意,这个类从Texture类继承了所有功能。

图8-10

例如,如果您想为PostScreenDarkenBorder.fx shader创建一个全屏的场景贴图,可以使用以下代码:

sceneMapTexture = new RenderToTexture(RenderToTexture.SizeType.FullScreen);

构造函数使用如下代码:

/// <summary>

/// Creates an offscreen texture with the specified size which

/// can be used for render to texture.

/// </summary> public RenderToTexture(SizeType setSizeType)

{

sizeType = setSizeType; CalcSize();

texFilename = "RenderToTexture instance " + RenderToTextureGlobalInstanceId++;

[...]

SurfaceFormat format = SurfaceFormat.Color;

// Create render target of specified size.

renderTarget = new RenderTarget2D( BaseGame.Device, texWidth, texHeight, 1, format);

} // RenderToTexture(setSizeType)

使用渲染目标,并使渲染所有物体只需调用SetRenderTarget方法,它使用了BaseGame类中的一些新的辅助方法去处理多个堆叠(multiple stacked?)的渲染目标(一些更复杂的Post-Screen Shader会用到):

sceneMapTexture.SetRenderTarget();

所用渲染完成后你调用Resolve方法,把渲染目标复制到贴图中(通过XnaTexture属性)。在XNA中这一步是新的,在DirectX中不需要,因为这样做才能支持Xbox 360硬件,它完全不同于PC的显卡。在PC上,您可以直接访问渲染目标使之用在Post-Screen Shader,但在Xbox 360渲染目标被放至在一个只写的位置,硬件无法访问。你必须复制渲染目标到内部纹理。这一过程需要一些时间,但它仍然非常快,所以不要担心。

在Resolve渲染目标后,你要将渲染目标重置使回到背景缓冲,否则,您仍然渲染到渲染目标,在屏幕上什么也看不到。为了做到这一点,只需调用BaseGame中的ResetRenderTarget方法,清除任何开始的渲染目标。如果你没有任何渲染目标,这个方法仍会工作,但仅return,不采取任何行动。

// Get the sceneMap texture content

sceneMapTexture.Resolve();

// Do a full reset back to the back buffer

BaseGame.ResetRenderTarget(true);

这几乎就是RenderToTexture类的所有内容了。另外的功能现在用不到(直到本书的最后一章)。去看看RenderToTexture的单元测试代码去了解更多东西。

PostScreenDarkenBorder类

在写PostScreenDarkenBorder类之前你就应先定义一个单元测试,并编写想在Post-Screen Shader类中包含的所有功能。请注意,这个类和Pre-Screen Shader一样从ShaderEffect类继承,有一些获取effect类和参数的简化过程,但你仍要在PostScreenDarkenBorder.fx中设置一些新的参数。

看看Post-Screen Shader的单元测试代码:

public static void TestPostScreenDarkenBorder()

{

PreScreenSkyCubeMapping skyCube = null;

Model testModel = null;

PostScreenDarkenBorder postScreenShader = null;

TestGame.Start("TestPostScreenDarkenBorder",

delegate {

skyCube = new PreScreenSkyCubeMapping();

testModel = new Model("Asteroid4");

postScreenShader = new PostScreenDarkenBorder();

},

delegate {

// Start post screen shader to render to our sceneMap

postScreenShader.Start();

// Draw background sky cube

skyCube.RenderSky();

// And our testModel (the asteroid)

testModel.Render(Matrix.CreateScale(10));

// And finally show post screen shader

postScreenShader.Show();

});

} // TestPostScreenDarkenBorder()

请始终用同样的方式命名shader和源代码,这样检查bug更容易。你能做的另一件漂亮的事是:如果以后要编写的新类功能类似,可以从Post-Screen Shader类继承,这个技巧能节省编写相同的效果参数代码量,也避免了重复编写渲染代码。

单元测试中使用了三个变量:

  • skyCube初始化并渲染天空盒纹理。
  • testModel载入asteroid4模型,并缩放10倍显示天空盒背景上。
  • 最后,postScreenShader处理PostScreenDarkenBorder类的一个实例。由于这个shader需要一个有效的场景贴图,你必须调用两次,第一次在Start方法建立场景贴图渲染目标,第二次在渲染结束后,Show方法把一切物体都显示到屏幕上。

看一下PostScreenDarkenBorder.cs中的单元测试,你会发现一些另外的代码,它们的功能是切换Post-Screen Shader的有无、在屏幕上显示帮助信息,这是以后添加的,只是为了改善shader 的可用性,基本布局是相同的。

Post-Screen Shader的布局与PreScreenSkyCube Mapping类似。你只需一个新的Start和Show方法和一些内部变量存储新的effect参数,并检查Post-Screen Shader是否开始(见图8-11)。

图8-11

Start方法为场景贴图调用SetRenderTarget,重要代码如下:

/// <summary>

/// Execute shaders and show result on screen, Start(..) must have been

/// called before and the scene should be rendered to sceneMapTexture.

/// </summary>

public virtual void Show()

{

// Only apply post screen glow if texture and effect are valid

if (sceneMapTexture == null || Valid == false || started == false)

return;

started = false;

// Resolve sceneMapTexture render target for Xbox360 support

sceneMapTexture.Resolve();

// Don't use or write to the z buffer

BaseGame.Device.RenderState.DepthBufferEnable = false;

BaseGame.Device.RenderState.DepthBufferWriteEnable = false;

// Also don't use any kind of blending.

BaseGame.Device.RenderState.AlphaBlendEnable = false;

if (windowSize != null)

windowSize.SetValue(new float[] { sceneMapTexture.Width, sceneMapTexture.Height });

if (sceneMap != null)

sceneMap.SetValue(sceneMapTexture.XnaTexture);

effect.CurrentTechnique = effect.Techniques["ScreenDarkenBorder"];

// We must have exactly 1 pass!

if (effect.CurrentTechnique.Passes.Count != 1)

throw new Exception("This shader should have exactly 1 pass!");

effect.Begin();

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

{

if (pass == 0)

// Do a full reset back to the back buffer

BaseGame.ResetRenderTarget(true);

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

effectPass.Begin();

VBScreenHelper.Render();

effectPass.End();

} // for (pass, <, ++)

effect.End();

// Restore z buffer state

BaseGame.Device.RenderState.DepthBufferEnable = true;

BaseGame.Device.RenderState.DepthBufferWriteEnable = true;

} // Show()

如果仍然有shader运行,首先检查Start是否被正确调用,否则,内容管道中的场景贴图渲染目标将不包含任何有用的数据。然后Resolv渲染目标以支持Xbox 360,设置所有的参数和technique。你可能会问,为什么这里使用technique名调用technique,通过reference不是更快吗,这样可以在构造函数中加以初始化?你是对的!但这并不重要,你每帧调用shader一次,在分析工具中看不出有多少性能差异(几千行代码中的一行并不会影响太多性能)。如果更频繁地调用shader,你最好定义technique reference缓存,这样就用不着每一帧重复调用了。可参见ShaderEffect类详细了解这种优化。

现在开始shader,像以前一样你使用VBScreenHelper类渲染屏幕,但你必须确保为每一个pass提供正确的渲染目标。这里只有一个pass,你只需把它重置到后备缓冲(你应该在post-screen shader中最后一个pass执行这个操作,否则你将不会在屏幕上看到任何东西)。对于更复杂的例子您就可以参考PostScreenGlow类。

单元测试结果

在渲染到设备后,渲染状态被储存(也许你还想在显示shader后渲染3D数据)。如果看一下代码,你还可以看到一些额外的try和catch代码块,只是为了确保渲染引擎在shader发生错误时运行正常。在这种情况下,shader被设置成不正常状态而不再被使用,错误被写到日志,shader效果看不到,但游戏其余部分仍能工作。

如果一些代码不工作或崩溃你应该总能提供选择。大部分情况下代码只提供视觉效果。不使用shader游戏仍能运行,只是看起来不漂亮。本书中的游戏shader永远不会崩溃,你不必担心,但如果在自己的开发过程中您很容易搞砸的effect参数或在像素着色尝试新事物,你不想让您的整个游戏或单元测试崩溃,它仍然应该继续运行。当运行TestPostScreenDarkenBorder单元测试后,您应该看到如图8-12的结果。您也可以调整一个shader参数或代码,例如,尝试渲染一个较小的目标只提供四分之一的画面,看看最终输出效果。

图8-12

如果你有Xbox 360,你也应尽早在X360上测试所有的shader,这是非常重要的,因为shader有时表现得并不一样,你必须确保所有渲染目标也能适应Xbox 360的内存并性能良好。PostScreenDarkenBorder和RenderToTexture类完全兼容Xbox 360,在Xbox 360游戏机工作得很好。

 

 

§8.3更多Post-Screen Shaders

改变屏幕边界,添加天空,将场景纹理变成黑白很漂亮,但还有更多更好的Post-Screen Shaders。Rocket Commande通过模拟alpha通道的HDR光线使用了相当复杂的发光效果,通过几个pass,混合上一帧的场景纹理和发光效果,实现了运动模糊。

这个shadre被称为PostScreenGlow,它可以在Rocket Commander(包括了DirectX和xna版本)。图8-13显示了基本功能,shader 比这更复杂,因为它支持多种shader模型(1.1和2.0),并对慢的计算机做了一些优化。在Rocket Commander游戏中的Shader Model可以选择,在速度慢的电脑上也可关闭Post-Screen Shaders,但没有这些效果游戏乐趣会降低。

图8-13

这看起来有点复杂,要做大量工作,但也有许多很好的shader书籍可供参考,你可以借鉴别人的东西。

Shader的有无效果是相当不同的(见图8-14)。

图8-14

运动模糊

你已经在PostScreenGlow例子中看到使用Post-Screen Shaders实现运动模糊的一个简单方法,但对你来说可能并不简单,但运动模糊效果是glow中最简单的一部分,它可以扩展成所谓的“Per Pixel Motion Blur”的效果。要实现Per Pixel Motion Blur你必须在一个特殊shader的帮助下,保存场景中每个像素的速度;无需渲染场景中的每个物体,只渲染运动的物体。

接着在shader中你根据玩家的运动方向、每个像素的速度和方向,对每个像素决定采用多大的模糊程度。一些较新的赛车游戏采用了这种技术,但渲染这种特殊纹理很占资源和shader指令。

因为要用重用上一帧的屏幕,所以本章以前使用的运动模糊在一个较大的模糊值下工作。这意味着你只需要增加10%的新的运动模糊而再利用90%上个画面的运动模糊。Per Pixel Motion Blur也能这样做,但如果每个物体都在运动时就很难跟踪,如果这样不如计算整个屏幕的运动模糊。

一个相对简单的诀窍是把你要渲染的屏幕分割成10×10的网格,把它们当成一个格子渲染,但允许允许顶点shader根据运动模糊的强度使用不同的权重,见图8-15比较只有一个格子和5×5格子的区别,采用10×10网格看上去甚至更好一点。

图8-15

颜色校正

我经常在Post-Screen Shaders中使用颜色校正,因为用起来比较简单而且对最后的画面影响很大。就像室外的光照,颜色修正影响所有的像素,所以它能使纹理更加真实。例如:如果你在一个洞穴里会很暗,只有一点光照进来,但如果你放置一个明亮的石头材质可能看起来会比周围显得更亮,使用颜色修正shader无法修正这个问题(because you should really put better objects in your cave that fit together in a better way),但改变一点颜色能使场景看起来更好一点。例如:如果整个场景很暗,那么增加一点蓝色会使各种材质融合得更好(见图8-16)。此图来自于我一年前写的一个游戏,它本来没有颜色修正,但有一天我想:为什么不能像电影画面处理的那样去实现这个效果呢?几乎每部电影都使用了颜色校正和对比度匹配,效果很好。现在我的每个新游戏都有色彩校正shader。

图8-16

亮度效果的基本代码只是简单地把颜色值乘以一个常数(如果你想动态地改变亮度也可使用)。例如:乘以0.5使得整个图像的变暗一半,乘以2.0使得亮度加倍,乘以0.0使完全黑色等等。只使用一条shader指令,你还能把它整合到颜色修正中(本章接下去要用到),它仍然只需一个shader指令。图8-17显示了效果。

图8-17

return brightness * originalColorValue;

改变对比度有点麻烦。使用1.0的值使图像保持不变,0.0使图像完全变灰(即没对比度)和更高的值使图像锐利。要实现这个效果,首先必须从每个颜色通道减去0.5,然后再把结果乘以对比度值,最后再在每种颜色通道加0.5。

当你在颜色通道中加或减超过1.0时会使图像丢失很多颜色而0.0使图像完全变灰(因为只有您最终使用的0.5有效,其余乘0不起效果)。高的对比度让暗的更暗亮的更亮(见图8-18)。

return (originalColorValue-float3(0.5, 0.5, 0.5)) * contrast + float3(0.5, 0.5, 0.5);

图8-18

为了实现图8-16的效果只需使用接近1的亮度值,我使用了0.92和1.2的对比度值。另外,您还可以改变每个像素的颜色。

当你在颜色通道中加减超过0.1时,你会看到绿色比红蓝色更加明显,看起来会太鲜艳;建议使用非常小的值如0.01和0.05。下面的代码使输出图像更蓝,去掉了点红颜色,这可以用在极地的环境,在沙漠中你可能需要橙色调,而火山环境可以使用更多的红颜色,等等。

inputColor = float3(inputColor.r-0.04f,inputColor.g,inputColor.b+0.05f);

有一个非常简单的方法可以改变颜色:如果你想进行更复杂的操作或在shader中优化色彩校正,您可以使用矩阵预先计算所有组合矩阵的结果,这样只需要修改像素一次就可以了。以下是一些矩阵例子:

brightnessMatrix = float4x4( brightness, 0, 0, 0,

0, brightness, 0, 0,

0, 0, brightness, 0,

0, 0, 0, 1);

contrastMatrix = float4x4( 1, 0, 0, 0,

0, 1, 0, 0,

0, 0, 1, 0,

-0.5, -0.5, -0.5, 1);

contrastMatrix *= float4x4( contrast, 0, 0, 0,

0, contrast, 0, 0,

0, 0, contrast, 0,

0, 0, 0, 1);

contrastMatrix *= float4x4( 1, 0, 0, 0,

0, 1, 0, 0,

0, 0, 1, 0,

+0.5, +0.5, +0.5, 1);

addColorMatrix = float4x4( 1, 0, 0, 0,

0, 1, 0, 0,

0, 0, 1, 0,

-0.04, 0.0, +0.05, 1);

你可以通过矩阵做很多事情,比如旋转或缩放等等。旋转对完全改变颜色、切换颜色通道很有用;而缩放矩阵中的一行可以实现你以前看到过的发光效果。你可以试着自己发挥一下。

菜单效果

本书的最后一个游戏你将会用到菜单的Post-Screen效果,它与PostScreenGlow shader很相似。但它的发光效果不强,但仍清晰可见。更重要的是增加了像老电影一样的效果,看起来挺有趣的。它可能不适合每个游戏,但对赛车游戏来说很酷,并和菜单音乐合拍。基本的思路是采用噪声纹理并非常缓慢的遍历它,数值增加地如此之大导致只有最亮的点才有用。效果从顶部扩散到底部,并在顶部增加了点其他效果(见图8-19 )。

图8-19

Post-Screen shader能实现很多其他效果,有时你并不想在post-screen space中做任何事。储存渲染过程中一些像素的数据,然后用在后面的Post-Screen shader中能让事情变得简单些。这种方法不仅比对每个像素进行运算快得多,而且也能更容易地将临近像素混合在一起、移动像素,改变颜色等。例如,要实现阴影映射shader,你首先要渲染一个根据灯光的角度而生成的阴影贴图,然后再次渲染场景并与阴影贴图进行比较。它工作得很好,但是如果你有很多不同的shader,你需要修改所有这些shader,而shader(尤其是Pixel Shader 1.1)存在指令限制,你没法增加更多的指令。然后,你需再增加另一个pass再次渲染几何数据。

另一种办法再次渲染场景阴影,但是这一次只是渲染一个场景的阴影贴图,这个贴图在后面的Post-Screen Shaders使用,将没有阴影效果的场景贴图与阴影贴图混合在一起。归功于屏幕空间,你也可以容易地实现模糊阴影或改变影子的强度。

关于今天硬件的shader、Post-Screen shader,阴影映射,渲染技术的更多知识,我建议你阅读关于shader的图书(如果你全看过的话,你就是一个shader高手了),如Shader X,GPU Gems, Game Programming Gems等(如果你全看过的话,你就是一个shader高手了),你可以很容易地在诸如亚马逊之类的网站上找到。我还建议下载NVIDIA SDK,学习其中的shader例子。

§8.4Rocket Commander游戏

关于post-screen shaders我们已经说了很多了,现在没剩下多少篇幅留给Rocket Commander游戏。这没关系,因为Rocket Commander已经推出一年有余,有很多教程,其中包括非常受欢迎的Coding4Fun网上的视频教程系列。

没必要再次重复了,Rocket Commander也在Managed DirectX NET 1.1上被编写,游戏本身运行在NET 2.0。Managed DirectX与XNA有些不同——很多事情可以容易移植,但所有字体渲染,sprite处理等等是完全不同的,一些功能,如动画模型仍不能被XNA支持(至少不容易导入)。

因此,XNA版本的Rocket Commander只展示基本引擎,但它仍很棒很有趣,即使在Xbox 360上用Xbox 360手柄控制。本节仅讨论从MDX迁移到XNA。如果你想了解更多请访问网站www.RocketCommander.com,查看视频教程、阅读源代码和文件、并玩其中一个Mod游戏。

XNA的性能

没有人相信XNA的性能有多好,主要的原因是大部分XNA游戏只是简单的2D游戏,不关心性能、3D硬件和shader效果。另外一个原因是,很多人认为将DirectX包装成MDX或XNA,最终还是重定向调用的DirectX基础框架,这会导致性能下降。但你能在XNA中少些很多代码,而且写起来更简单,这一点相当重要。特别是如果你想要做跨Windows和Xbox 360平台的游戏时,只使用C++实现所有功能工作量很大。相比一个经验丰富的XNA团队,您可能需要两倍的程序员才能完成同样的工作,而且更有可能会遇到诸如内存问题、指针管理、非托管环境中的全面管理等问题。

以前我已证明,大多数程序员错误认为MDX比DirectX慢得多并不对。如果您的代码占用了100%的GPU和100%的CPU,您可能根本就没法编出游戏,这只是一个压力测试。即使在这种情况下托管的DirectX也能达到几乎相同的性能(98%~99%),但这对于真正的游戏并不重要,因为你没有时间去优化每一个行代码。相反你把重点放在重要部分,并努力使最常被使用的10%~20%代码工作得尽可能快。给其余80%~90%进行优化花费5倍时间性能却只提升百分之几。

你应把关注游戏本身。玩家不关心游戏运行在59或58帧,他只想要一个有趣的游戏,如果你允许不同的设置,他甚至能让分辨率最小、关闭效果去获得更高的帧率。今天太多的游戏只侧重于画面(像我在这本书中做的那样,但至少我也展示了许多酷游戏的想法,希望您能使用基本代码写出自己的游戏,而不是长时间编写游戏引擎而不制作自己的游戏)。

Rocket Commander的代码被高度优化,当我开发的第一个版本时我做了大量的性能测试后。整个游戏编码占用了四个星期,如果你只有这点时间,你很难实现最佳的shader效果。但我仍然设法在配有Shader Model 2.0显卡的中端计算机实现了超过每秒100帧的帧频,高分辨率下能同时显示5000的小行星(换句话说,每秒超过3000万多边形),同时包括碰撞测试和其他一些效果。低端PC,即便完全没有shader支持,仍然能够运行游戏。它看起来不够漂亮,但很不错了。

今天的电脑变得更快,CPU有多个内核,可以被用来在同一时间执行不同任务。Xbox 360有三个核心和6个线程,它们执行所有您需要的的游戏代码。我不知道今天的任何一个Xbox 360游戏是否确实需要这一强大的CPU能力,GPU也很快,但在高分辨率下仍然是GPU将阻止您获得更好的帧速率。

Rocket Commander游戏是一个很好的例子,展示了如何将工作分为两个内核。物理计算和预测试要占用一半的CPU时间(有时甚至更多),渲染效果占用另一半CPU时间。物理可以并行处理,因为所有物理计算检查两个小行星受否相交,如果相交则反弹(见图8月20日),并确保他们不再相交。

图8-20

下一帧将更新新的物理位置、运动矢量等等,有点坏处是,因为物理计算的是下一帧而不是当前帧(因为大多数小行星将根据物理计算被渲染了,再渲染新的位置已为时过晚)。正如您所看到图8-20,你不必检查每一个小行星,只需检查临近的,还可以把它们放在区域中进一步,在原始版本的Rocket Commander中能提高1/10的性能。

Rocket Commander XNA版本比原来的更好。在Xbox(或PC)上能达到HDTV 1080p的分辨率,并仍使用所有图形效果,使用全屏反锯齿,并在高帧率中同时显示数千颗小行星。

从MDX迁移到XNA

Rocket Commander基本引擎与XnaGraphicEngine类似,在一些地方有更多的功能,如动画模型、渲染大量小行星的优化处理等。例如小行星都使用相同的shader和材质以一个非常有效率的方式被渲染。但其他一些功能,XnaGraphicEngine比Rocket Commander更容易和更好,如字体和sprite管理等。你还可以插入新的shader去改变Rocket Commander的视觉效果。

请注意,一些原版本有的功能,如动画模型支持或镜头眩光occlusion测试在XNA中无法实现。occlusion测试在XNA框架丢失了。我猜想这是因为很难以相同的方式工作在PC和Xbox 360,镜头眩光效果有破绽:当一个小行星挡住前面的阳光是,眩光也不会消失和淡入淡出。动画模型可以通过custom model processing(自定义模型处理过程)或为模型采用一种新的格式实现。但为了移植简单,你当然不希望再导出每个模型并一一加以测试。

网络代码在Xbox 360版本中也被剔除,因为在Xbox 360没有System.Net命名空间。Windows版本仍能通过网络服务从互联网服务器上发送和接收最高分数。各关卡和其他游戏数据是100%兼容的,MOD版本也能被移植到XNA上。欢迎更多的Rocket Commander MOD版本,特别是对XNA框架。我总是很高兴地接收到电子邮件告诉我成功事例。

在2006年底,我在网站上发表了一篇很长的文章,比较了MDX与XNA并讨论了使用XNA的优缺点。如果你以前学习过MDX,想知道XNA是否值得学习,去看看那篇文章。在Rocket Commander XNA版本中又增加了点新功能,特别是更多的单元测试和多线程,我马上就要谈到,但源代码减少了10%以上,这表明你可以移植了一个项目使它更简单。如果你刚开始新的XNA项目,效果是显而易见的。

使用线程来改进性能

Rocket Commander XNA版本在Windows和Xbox 360上性能都很好,尤其是当只是做一些基准测试时是绝对完美的。GPU是用到了极限,没有任何理害怕托管代码。Windows的性能尤其好。我所有的游戏和程序即使在低分辨率,一个线程时也完全是要使用GPU的。

在Xbox 360性能有所损失,你必须考虑很多事情,这是因为文档不多。举例来说,在Xbox 360最糟糕的事是每一帧都产生新的数据,即使你只是建立一个迭代器执行foreach循环,它也会影响性能。但Xbox 360的优点是您手头有三个核心(和6个硬件线程)让您来优化性能,对于Rocket Commander游戏来说能极大地优化游戏循环,因为物理和更新线程占用了近50%的CPU时间,这在电脑上并不重要,因为是GPU拖慢了性能(见图8-21),但在Xbox 360上使用多个线程上帧速率几何能增加一倍。

图8-21

结果与截图

Rocket Commander XNA版本是比原始版本运行得更好,支持Xbox 360和XNA框架。归功于多线程,你能容易地将物理和渲染引擎分开处理,游戏比以前性能好很多,物理效果更好,渲染引擎有更多时间渲染更多小行星,如果你喜欢还可以添加其他很酷的效果。

XNA版本和原始版本看上去类似,您只有通过缺失的功能(如动画模型和镜头眩光occlusion检查),或只在看游戏中的新文字加以区别。我在主菜单添加了XNA的图标显示区别(见图8-22)。

图8-22

希望你玩得愉快。Rocket Commander原始版本是免费提供的,你可以自己改造。图8-23是Rocket Commander截图。

图8-23

 

§8.5挑战:编写Edge Detection Shader(边缘检测着色)

本章对post-screen shade已经谈论了很多了,涵盖几乎所有的基本post-screen shade。你学习了通过基本组件编写类似的shader。举例来说,色彩校正shader可用于实现如图8-1中的灰度效果。Edge Detection Shader我故意留着没写。它可能不是每个游戏都用到,但有许多其他shader使用了边缘检测shader的基本功能。下面是一些例子:

  • 边缘检测shader可以表现出一种异形的视觉效果,或类似高科技特殊操作的视觉模式,使您可以在夜间看得更清晰。
  • 漫画和卡通渲染,只是将输入的颜色减少到非常少的颜色,例如通过颜色盘或简单的图像,把贴图上的颜色映射到某些预定的颜色,接着为每个三维表面绘制粗线边缘,通过边缘检测shader很容易做到。您还可以通过画三维线条,然后在顶部加上几何体,只留下每一个三维物体的轮廓去改善效果,使用边缘检测shader也很容易实现。
  • 边缘检测shader可以用来增加场景的对比度或找到线条或轮廓。边缘检测往往与模糊效果相反,模糊效果将一切都混合在一起,有时会使最后的结果失去太多细节。如果您想要开始前做更多的辉光和模糊的效果,边缘检测shader可以帮你获得一个比较锐利的图像。
  • 边缘检测shader也可以被用来创造看起来像画出来的图片。您只需使用非常高的边缘检测值并添加一个如图8-1一样的half-tone shader。

您要做的就是创建一个简单的边缘检测过滤器。基本思路是从原来的场景贴图中提取若干像素,并与邻近的像素比较。在Shader Model 1.1中,您只能提取四个纹理像素,在Shader Model 2.0可以有8个,因此,你能使用的像素是有限的。在Pixel Shader 1.1中与邻近像素比较不容易效果也不好。你可以只取一个中心点,把它乘以2,然后减去两个周围的点(顶部和底部,或左边和右边),然后在Luminance函数结果的帮助下采用灰度值,去实现边缘效果。

以下是基本的Pixel Shader代码,如果是Shader Model 2.0,您可以做更多的纹理提取。如果您不能确定如何扩展shader请在Nvidia的FX Composer中检查边缘shader。你应该在FX Composer中完成整个shader,然后就可以通过C#类将其导入到您的引擎中。变量col1,col2,col3和col4从场景贴图的四个纹理坐标中提取的,然后将它们传递给Pixel Shader。

return Luminance(

// Horizontal

(col1*2.0f-col2-col3)+

// Vertical

(col2*2.0f-col1-col4));

如果shader正常应该可以看类似图8-24的效果。

图8-24

§8.6总结

在这一章中你学习了所有的post-screen shader并完成了您的图形引擎。图形效果很好,调整效果也很有趣。但本书针对的是游戏开发,图形只是工作的一部分。在后面几章,你将把重点放在声音、输入和游戏引擎之类的其他方面。有些代码已经被应用在以前的游戏中了,特别是Rocket Commander游戏使用了声音,先进的输入类和很多游戏逻辑。

下面是post-screen shader使用的一个概括:

  • 天空立方体映射(这是一个pre-screen shader,但工作原理类似)。
  • 在场景上绘制纹理(混合覆盖)。
  • 翻转场景贴图(很容易,只要使用1-color)。
  • 将场景贴图转换成黑白。
  • 新增屏幕边界效果,本章做的是变暗屏幕边界模拟电视的效果。
  • Glowing技技术,以及如何让他们通过down-sampling执行得更快。
  • 使用glow shader中的额外alpha通道模仿HDR效果。你还可以使用支持超过每通道8bit的渲染目标,在场景贴图中保存HDR颜色信息,在后面可以用来实现HDR效果。
  • 边缘检测(当然,这是你要自己完成的部分)。
  • 菜单效果,噪声纹理,half-tone效果等等。
  • 色彩校正,包括亮度变化,对比度调整,影响整体外观的增加色彩。通过颜色校正的post-screen shader.还能实现灰度效果。
  • 运动模糊,对每个像素或是对整个屏幕。更多的技巧,使用10×10网格代替整个画面渲染以改善性能的技巧也被提及。
  • 还有更多shader。

归功于每一章节的例子,您可以在很短时间内创建您的XNA图形引擎。在这一章您可以通过XnaGraphicEngine的源代码测试许多post-screen shader,这个引擎现在已经相当庞大大和强壮了,您可以处理shader,三维数据,精灵,post-screen shader等许多事情而不用担心任何执行的细节,因为引擎会为您处理这一切。

使用多线程还使你的Rocket Commander游戏性能更好,但多线程编写起来很难。

依靠图形引擎,现在可以很容易地编写游戏了,但不要停止写单元测试,当您尝试找出为什么某菜单没有正常工作时单元测试能帮助你找到原因。通过菜单单元测试,您可以迅速找到问题所在,通过游戏中部分单元测试去除错也比遍历整个游戏等待错误发生快得多。更多的图形效果,物理,以及赛车游戏将在本书的最后一部分讨论,现在可以进入第三部分了,里面有另一个很棒的游戏(XNA Shooter),你将更深入地了解声音,输入和游戏逻辑的细节。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值