将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)

1
图 6-16

测试Shader

现在程序可以运行了,你可以试着测试一下其他贴图,材质或渲染模式。

比如要实现线框的效果,你可以看一下前面的图6-5,您可以在Shader开始前改变Fillmode:

BaseGame.Device.RenderState.FillMode = FillMode.WireFrame;

或者可以载入的另一个纹理或另一个模型,Shader类允许这样做。最简单的方式是修改环境光,散射光和镜面光的值来渲染一个怪苹果(见图6-17)。

2
图 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参数名去设置参数,当你不当心拼写错参数很容易发生错误,在下一章我们将学习如何更有效率地去设置参数。