将Shader导入到您的引擎
感谢DirectX和Xna,加载fx文件和设置所有必需的参数是很容易的(上一章在绘制2D直线和3D直线时你已经这样做) 。在下一章您会使用一个更一般的类,它可以接受许多不同的shader,并采用了更优化的方式设置所有必需的参数,但对于这一章在引擎中只使用simpleshader.fx文件。
与以往一样,你通过定义单元测试开始。在命名空间Shader中创建一个新文件simpleshader.cs并写入以下代码:
#region Unit Testing public static void TestSimpleShader() { SimpleShader shader = null; Model testModel = null; Texture testTexture = null; TestGame.Start("Test SimpleShader.fx", delegate { shader = new SimpleShader(); testModel = new Model("Apple"); testTexture = new Texture("Marble"); }, delegate { // Render model shader.RenderModel(testModel.XnaModel, testTexture); }); } // TestSimpleShader #endregion
该单元测试还没有Rendermodel的方法,让我们创建它:
public void RenderModel(XnaModel someModel, Texture texture) { } // RenderModel(someModel, texture)
现在,编译后您就可以看到一个空白屏幕。
编译Shader
此单元测试中要在屏幕上显示点什么,你首先必须加载并编译Shader。正如你加载模型和纹理一样,您使用xna的内容管道(content pipeline)。只需将.fx文件拖动到您的项目(同其他材质和模型一样放至正确的目录),这样在项目创建时会自动编译shader,如果Shader并没有编译你还会获取Shader编译的错误信息。同时,你也要确保marble.dds纹理被正确加载,因为您的单元测试中要用到。
加载effect和加载纹理一样简单,只需定义一个effect变量,并在simpleshader构造函数中加载它:
#region Variables Effect effect = null; #endregion #region Constructor public SimpleShader() { effect = BaseGame.Content.Load<Effect>( Path.Combine(Directories.ContentDirectory, "SimpleShader")); } // SimpleShader() #endregion
如果您是在Windows平台上,您也可以动态加载shader,对于在游戏中测试和改变shader是很有用的。我通常使用un-compiled shader文件,我不想改变shader了。下面的代码是用来编译和加载shader的。
请注意,这些类和方法只适用于在Windows平台上。如果您想对Xbox 360使用这些代码,请将#if !XBOX360 #endif围绕这些程序行。
CompiledEffect compiledEffect = Effect.CompileEffectFromFile( Path.Combine("Shaders", shaderContentName + ".fx"), null, null, CompilerOptions.None, TargetPlatform.Windows); effect = new Effect(BaseGame.Device, compiledEffect.GetEffectCode(), CompilerOptions.None, null);
使用参数
在你学习建立Shader的过程中,世界,观察和投影矩阵对转换三维数据,并在屏幕得到正确显示是非常重要的。你应在rendermodel方法设置所有这些Shader的参数,并由您的单元测试调用。
BaseGame.WorldMatrix = Matrix.CreateScale(0.25f, 0.25f, 0.25f); effect.Parameters["worldViewProj"].SetValue( BaseGame.WorldMatrix * BaseGame.ViewMatrix * BaseGame.ProjectionMatrix); effect.Parameters["world"].SetValue( BaseGame.WorldMatrix); effect.Parameters["viewInverse"].SetValue( BaseGame.InverseViewMatrix); effect.Parameters["lightDir"].SetValue( BaseGame.LightDirection); effect.Parameters["diffuseTexture"].SetValue( texture.XnaTexture);
在此代码中首先是设置世界矩阵。这是非常重要的,因为如果世界矩阵没有设置,也许从以往的操作中会产生无法预料的数值,你当然不希望三维模型处于一些随机的位置。因为您的apple.x是相当大的,你应该把它缩放得小一些以适合您上一章建立的simplecamera类。
然后计算worldviewproj矩阵和设置的其他矩阵,lightdir、diffusetexture也很重要,因为FX Composer中会自动加载贴图,而程序中不会,你必须自己设置。如果你从内容管道加载模型,Xna会从模型数据自动加载所有使用的纹理。所以在单元测试中,应加载marble.dds纹理。
顶点格式
在您渲染你的3D苹果模型前必须确保您的程序和shader知道使用哪种顶点格式。在DirectX可以使用一个预定义的固定功能顶点格式,但在xna中不行。你必须用类似于shader中vertexinput结构的方式定义顶点声明。因为你使用内置的vertexpositionnormaltexture结构,所以没无需定义每个值,但在下一章你将学到如何使用您自定义的tangentvertex格式。
// Use the VertexPositionNormalTexture vertex format in SimpleShader.fx BaseGame.Device.VertexDeclaration = new VertexDeclaration(BaseGame.Device, VertexPositionNormalTexture.VertexElements);
您无需在每次调用rendermodel时创建一个新的顶点声明,但为了保持简洁,你每次调用都建立一个新的顶点声明。它以图形设备作为第一个参数,顶点元素的数组vertexpositionnormaltexture结构作为第二个参数。如需详细资讯请参阅第7章。
使用Shader渲染
现在要渲染苹果,您首先应指定您想要使用的technique(默认设置为第一个technique,但最好知道如何设置technique)。你将一直使用effect类的currenttechnique属性。您为每一个technique中的pass(正如我以前说过,通常只要有一个pass)渲染三维数据。渲染苹果并不容易,因为xna只提供一个mesh.draw的方法,可参见model类的代码。
XNA Framework另一个缺少的功能是创造盒,球,或茶壶的mesh辅助类。你也会注意到大部分的direct3dx名字空间的功能在xna中不存在。当您处理自己的content processor时只能使用部分的方法,对您的引擎或测试模型,网格,或shader并没有帮助。因为所有的顶点和索引缓冲区是只写的,所以加载模型时您也不能处理任何的顶点或索引的数据,这虽然有利于快速硬件访问,但不够灵活。在您代码中你只是模拟的mesh.draw方法,只是使用了自己的effect类。
effect.CurrentTechnique = effect.Techniques["SpecularPerPixel"]; effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); // Render all meshes foreach (ModelMesh mesh in someModel.Meshes) { // Render all mesh parts foreach (ModelMeshPart part in mesh.MeshParts) { // Render data our own way BaseGame.Device.Vertices[0].SetSource( mesh.VertexBuffer, part.StreamOffset, part.VertexStride); BaseGame.Device.Indices = mesh.IndexBuffer; // And render BaseGame.Device.DrawIndexedPrimitives( PrimitiveType.TriangleList, part.BaseVertex, 0, part.NumVertices, part.StartIndex, part.PrimitiveCount); } // foreach } // foreach pass.End(); } // for effect.End();
详细地说这意味着你遍历了每一个pass(这里只有一个)渲染所有网格(meshes)(这里也只有一个),然后你制定的所有mesh parts(仍然又只有一个)最后,您调用drawindexedprimitives方法渲染所有shader中的顶点。然后pass和shader关闭,你终于可以看到带有marble.dds纹理的苹果显示在屏幕上 。(见图6-16)
测试Shader
现在程序可以运行了,你可以试着测试一下其他贴图,材质或渲染模式。
比如要实现线框的效果,你可以看一下前面的图6-5,您可以在Shader开始前改变Fillmode:
BaseGame.Device.RenderState.FillMode = FillMode.WireFrame;
或者可以载入的另一个纹理或另一个模型,Shader类允许这样做。最简单的方式是修改环境光,散射光和镜面光的值来渲染一个怪苹果(见图6-17)。
effect.Parameters["ambientColor"].SetValue( Color.Blue.ToVector4()); effect.Parameters["diffuseColor"].SetValue( Color.Orange.ToVector4()); effect.Parameters["specularColor"].SetValue( Color.Orchid.ToVector4());
请注意,您必须将颜色值转换为vector4。当您在effect.Begin()和effect.Ene()之间设定shader参数,你必须调用Effect.Commitchanges()方法,以确保将您所做的更改发送给GPU,但如果象在simpleshader类中的一样,设置参数后才开始effect.Begin()就无需如此。
如果你通过shader参数名去设置参数,当你不当心拼写错参数很容易发生错误,在下一章我们将学习如何更有效率地去设置参数。