§7.3Shadereffect类
你使用两章前介绍的shadereffect类来渲染shader。上一章已涵盖了基础知识,现在你唯一要做的就是增加更多的参数,而其他功能保持不变(见图7-13)。你还使用了类似于第五章LineManage类中Render方法去渲染3D数据。
渲染3D数据你不光需要3D几何数据和渲染代码,还需要材质参数,包括材质颜色和贴图。为了更好地管理这些数据,这里使用一个新的辅助类Material.cs(见图7-14)储存所有的材质数据。
Shadereffect的setparameters方法将Material类作为参数,并把它传递给Render方法。通过这种方式你能方便地设置材质,而无需自己设置所有的effect参数。
通过新的类你能容易地编写单元测试,让上一章的苹果支持Normal mapping.fx Shader。另外你也能通过加载小行星的diffuse和Normal贴图来改变小行星的材质,以测试您的新Material类。
TangentVertex格式
在单元测试之前,你还需要VertexInput结构。不像第6章的VertexPositionNormalTexture结构,XNA没有包含切线数据的预定义结构,你必须自己定义。以下TangentVertex类(见图7-15)用来定义normalmapping.fx和所有Shader中要用到的VertexInput结构。
定义结构中的字段并没有什么特别,只需定义你需要的四种数据类型:pos(位置),normal(法线),tangent(切线)和UV texture coordinates(UV纹理坐标):
/// <summary>
/// Position
/// </summary>
public Vector3 pos;
/// <summary>
/// Texture coordinates
/// </summary>
public Vector2 uv;
/// <summary>
/// Normal
/// </summary>
public Vector3 normal;
/// <summary>
/// Tangent
/// </summary>
public Vector3 tangent;
有些顶点缓冲器需要您指定结构大小。在MDX中你可以通过使用Direct3dx或unsafe代码的sizeof方法实现,但在xna中没这么简单,你必须自己定义大小。
/// <summary>
/// Stride size, in XNA called SizeInBytes.
/// </summary>
public static int SizeInBytes
{
get
{
// 4 bytes per float: // 3 floats pos, 2 floats uv, 3 floats normal and 3 float tangent.
return 4 * (3 + 2 + 3 + 3);
}
// get
} // StrideSize
结构的其余部分是相当简单的,唯一一个从外部得到的字段是Vertex Declaration,它由自定义代码生成:
#region Generate vertex declaration
/// <summary>
/// Vertex elements for Mesh.Clone
/// </summary>
public static readonly VertexElement[] VertexElements = GenerateVertexElements(); /// <summary> /// Vertex declaration for vertex buffers.
/// </summary>
public static VertexDeclaration VertexDeclaration = new VertexDeclaration(BaseGame.Device, VertexElements);
/// <summary>
/// Generate vertex declaration
/// </summary>
private static VertexElement[] GenerateVertexElements()
{
VertexElement[] decl = new VertexElement[]
{
// Construct new vertex declaration with tangent info
// First the normal stuff (we should already have that)
new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0), new VertexElement(0, 12, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0), new VertexElement(0, 20, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0),
// And now the tangent
new VertexElement(0, 32, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Tangent, 0),
};
return decl;
} // GenerateVertexElements()
VertexElement使用下列参数定义了3D数据的声明顺序:
- Stream(第一个参数):通常设为0,如果您有多个顶点缓冲流才需另外设置,这挺复杂。
- Offset(第二个参数):从流开始算起的偏移量(以字节为单位)。计算离结构起点有多少字节,浮点数、整数和dwords占用4个字节。
- 顶点元素格式(第三个参数):定义使用哪种类型的数据,和Shader中的VertexInput使用相同的名称。通常使用vector2,vector3,vector4。整数或浮点数类型,通常只用于蒙皮和骨骼动画。
- 顶点单元方法:使用默认值。也允许UV和Lookup,但不是经常用到。
- 顶点元素usage:数据类型如何使用?与你定义的Shader语义类似。你应该始终设置使用的数据类型,因为如果和Shader中的顺序不一样,数据将根据usage类型重新排序,如果您不定义切线,VertexInput数据可能会一团糟,格式不匹配的话XNA会报错。
- 最后,您指定usage索引,不过你可能不需要使用这些数据,只需把它设置为0。
Normal Mapping单元测试
通常你会先写单元测试,你已经在上一章中有了Shader的单元测试。现在你需要首先定义您的新的Shader如何工作并弄清楚哪些类是必需的。现在写Shader单元测试更容易。请注意,你无需在shadereffect中编写任何测试代码,因为在device初始化前shadereffect无法得到所有初始化的Shader!看一下Normal Mapping Shader的单元测试:
public static void TestNormalMappingShader()
{
Model testModel = null;
Material testMaterial = null;
TestGame.Start("TestNormalMappingShader", delegate {
testModel = new Model("apple");
testMaterial = new Material( Material.DefaultAmbientColor, Material.DefaultDiffuseColor, Material.DefaultSpecularColor, "asteroid4~0", "asteroid4Normal~0", "", "");
},
delegate {
// Render model
BaseGame.WorldMatrix = Matrix.CreateScale(0.25f, 0.25f, 0.25f);
BaseGame.Device.VertexDeclaration = TangentVertex.VertexDeclaration;
ShaderEffect.normalMapping.Render(testMaterial, "Specular20",
delegate {
// Render all meshes
foreach (ModelMesh mesh in testModel.XnaModel.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
});
});
} // TestNormalMappingShader()
与前一章的单元测试一样,首先加载Model,但使用了材质(包括小行星模型的颜色、diffuse和Normal贴图)代替加载贴图。渲染过程中世界矩阵像以前一样被设置,然后设置TangentVertex结构。渲染代码与simple shader rendermodel一样:遍历所有网格和小行星的材质,图7-16显示了单元测试的结果。
苹果仍在,但非常暗,Normal贴图看起来也不太好,与FX Composer中看到的不一样。这是因为切线数据有问题。您告诉XNA如何使用输入数据,但切线数据仍然丢失,你必须在模型导入内容管道前生成切线数据。
通过自定义处理器(Custom Processor)加入切线数据
解决切线问题并不容易,写Custom Processor也不是一件简单的工作,以下是基本步骤。如果你想对Custom Processor了解得更多或自己写一个完整的content importer类,你可参考XNA的帮助文件或网上的例子。
首先,你必须创建一个新的DLL项目,只需选择添加现有解决方案,并选择XNA的DLL或C#的DLL类型。现在确认XNA框架和XNA管道DLL被加到了reference部分(见图7-17)。
将以下代码写入DLL文件的main类中,这是从Model Processor继承的,扩展xna Model的默认行为。代码为模型中的每个网格调用了CalculateTangentFrames方法。此项目中的代码更为复杂,但修正了若干问题,为复杂模型的高度优化渲染做准备。
/// <summary>
/// XnaGraphicEngine model processor for x files. Loads models the same
/// way as the ModelProcessor class, but generates tangents and some
/// additional data too.
/// </summary>
[ContentProcessor(DisplayName = "XnaGraphicEngine Model (Tangent support)")]
public class XnaGraphicEngineModelProcessor : ModelProcessor
{
#region Process
/// <summary>
/// Process the model
/// </summary>
/// <param name="iContext for logging
/// <returns>
Model content
</returns>
public override ModelContent Process( NodeContent input, ContentProcessorContext {
// First generate tangent data because x files don't store them
GenerateTangents(input, context);
// And let the rest be processed by the default model processor
return base.Process(input, context);
}
// Process
#endregion
#region Generate tangents
/// <summary>
/// Generate tangents helper method, x files do not have tangents
/// exported, we have to generate them ourselfs.
/// </summary>
/// <param name="input" />
Input data
/// <param name="context" />
Context for logging
private void GenerateTangents( NodeContent input, ContentProcessorContext context) { MeshContent mesh = input as MeshContent;
if (mesh != null) {
// Generate trangents for the mesh. We don't want binormals,
// so null is passed in for the last parameter.
MeshHelper.CalculateTangentFrames(mesh, VertexChannelNames.TextureCoordinate(0), VertexChannelNames.Tangent(0), null);
} // if
// Go through all childs
foreach (NodeContent child in input.Children)
{
GenerateTangents(child, context);
} // foreach
} // GenerateTangents(input, context) #endregion
} // class
该MeshHelper类是仅在内容管道中有效,有几个方法可以帮助您调整输入的数据。GenerateTangents方法从第一套纹理坐标产生切线框架,忽略了binormal值。虽然也能获得一些不错的切线,但往往看上去是错误的,特别是一些较早时候建立的模型。较新版本的3D Studio Max(8和9 )利用所谓Rocket模式建立Normal贴图,但旧版本和其他工具可能会产生四舍五入Normal贴图,这将弄乱xna中自动创建的切线。在xna早期版本我使用自己的切线处理和生成器,但在xna中并不那么容易。你不直接处理模型的顶点数据,虽然也有一些小辅助方法,但提取所有数据并把它还原到一起仍要做大量的工作。
如果你真的需要对你的3D数据做大量的改动或添加额外的功能,那么写一个全新的内容导入器要比将现有的格式的混在一起简单一点。例如,在Quake/Doom中md3或MD5格式更常见,人们经常编写自己的内容导入器把其他游戏中一些很酷的模型显示在自己的游戏引擎中。请参考XNA帮助文件了解如何编写自己的内容导入器。此外,希望将来在网上出现更多自己编写的内容导入器。如果你不想把.X或.fbx文件搞得一团糟,我推荐最简单的格式collada格式。它需要一些自定义代码,但一旦你用了这个格式,那么扩展你的内容导入器、添加新功能、随心所欲地改变行为是很容易的。
现在您的自定义模型处理器完成了,它所产生了Normal Mapping Shader所需要的切线数据,要使自定义处理器工作起来,你还必须通过打开每个内容的文件属性选择它。在选择自定义模型处理器之前,你必须确保内容管道知道它存在,现在您你只需打开磁盘某处的刚刚创建的DLL文件。打开您的项目属性(鼠标右击解决方案资源管理中XnagraphicEngine.csproj文件的顶端,并选择属性)。最后一个选项是所谓的内容管道,这里您可以选择其他内容导入器及处理器(见图7-18)。如果您单击添加…您可以选择上两个文件夹层次的xnagraphicenginecontentprocessors.dll文件,然后选择其bin\Debug目录,若是Release模式,则选择bin\Release\目录。
之后,你能为每个加载的模型选择“XnagraphicEngine Model(切线支持)”。您也可以选择多个.x文件,然后改变模型的内容处理器(见图7-19)。
做了大量工作后,现在你就可以开始你的最后一个单元测试了,看一下有什么变化。请注意,新的内容处理器支持并没有改动任何XnaGraphicEngine中的代码,这些都在处理器DLL中完成。如果在内容处理器中有代码错误或内容文件不包含有效数据,那么项目启动前时将获得一个警告或错误,因为内容管道建立的所有内容是在项目启动前。一旦通过您的新的处理器建立了内容,它就不会再改变,也无需重新建立了,XNA将自动为您忽略它。
如你在图7-20看到的那样,您的带有小行星材质的苹果模型现在看起来好得多,你也可以为了好玩指定不同的材质。请注意,苹果模型的内部结构与你的TangentVertex格式是不同的。如果您直接设置了苹果的材质,你应确保正确的顶点声明,否则纹理坐标将不匹配。(但这对您的测试并无大碍)。
您可能会注意到,无论是rocket还是Marble纹理似乎被镜像了。这是正常的,因为苹果模型是为DirectX(使用左手坐标系)生成的。你可以将所有模型转化为右手坐标系(3D Studio的Panda DirectX导出插件支持此项功能),您也可以忽略这个问题,因为在Rocket Commander游戏中它并没有那么重要。但如果纹理上有文字或不想镜像纹理,那么请你确保一开始就使用正确的坐标系。它始终是重要的是利用相同的格式,在艺术家建模阶段就使用相同的格式是很重要的,如果它不匹配,要么调整您的引擎,要么将其转换成需要的格式。
最后的小行星单元测试
显示一个简单的小行星模型并不需要所有这方面的知识,因为下面的单元测试表明:只加载模型,并使用3D Studio 中自动选择的Shader是可行的。但当您尝试每帧渲染1000个小行星时,此种方式就会出现问题,为每个小行星都使用一个新的Shader,重复设置参数,使用未经优化的渲染方法不光慢而且不切实际。
作为替代,你应使用自己的Shader类,然后只初始化材质和Shader设置,尽可能快地渲染所有模型。通过这种方式您才能在Rocket Commander游戏中获得很好的性能。
public static void TestAsteroidModel()
{
Model testModel = null;
TestGame.Start("TestNormalMappingShader",
delegate {
testModel = new Model("asteroid4");
},
delegate {
// Render model
testModel.Render(Matrix.CreateScale(10.25f, 10.25f, 10.25f));
}
);
} // TestAsteroidModel()
如果小行星模型(您也可以使用低精度模型进行测试)使用了支持切线的模型处理器,您应该可以看到带有视差映射(parallax mapping,与Normal Mapping类似,只是利用一个额外的高度贴图)效小行星模型的另一个问题是边缘有锯齿,看起来贴图不匹配,但如果关闭Normal Mapping又正常了。因此,问题来自于切线数据,而Normal Mapping贴图是正确的,因为DirectX版本的Rocket Commander游戏是正常的(归功于正确的切线数据生成方式,但在XNA中不能用在.x文件上)。另一些像火箭之类的模型看起来不错,但旧模型仍有一些不可修复的问题。
重新导出小行星模型或重新生成Normal Mapping贴图可能在某种程度上能解决小行星模型的错误,不过,工作量很大而且小行星看起来已足够好了。原始版本的Rocket Commander游戏仍然存在,你可以看看它是如何做的,对于XNA,你要忍受一些轻微的故障。
对现在的这个游戏这个问题不会发生,但在本书接下来的游戏中您应确保所有模型正确显示。请注意,3D建模、正确输入输出模型,编写Shader显示出艺术家心目中的效果等等需要花费大量的时间。你可以使用一个现成游戏引擎(如本书中写的)一段时间后再开始编写自己的3D代码,但不要在引擎上用时过多而不写自己的游戏。一个常见的错误是:所有人都编写自己的游戏引擎,但极少有人由引擎做出自己的游戏,Be smarter than them。
发布时间:2008/9/17 下午4:25:37 阅读次数:7129