24.绘制平面的QuadSceneNode类
你应先去看5.1 绘制三角形,线和点、5.2 在三角形上添加纹理、5.3 使用索引移除冗余顶点、5.4 使用顶点缓冲和索引缓冲将顶点和索引保存在显存中弄懂如何绘制基本的图元,比绘制三角形再难一点的就是绘制四边形了。XNA官网上的示例Primitives3D(http://creators.xna.com/en-US/sample/primitives3D)也很有参考价值,不过对初学者有点难(源代码Primitives3DSample.zip下载)。
QuadSceneNode类从GenericMaterialSceneNode类继承,而GenericMaterialSceneNode类从Renderable3DsceneNode继承。首先是GenericMaterialSceneNode的代码,它的代码很简单,主要就是添加了GenericMaterial类型的材质变量并实现了Imaterial接口:
namespace StunEngine.SceneNodes { ////// 使用GenericMaterial材质的节点都是从这个类继承的,包括平面、立方体、球体、简单地形等。 /// public class GenericMaterialSceneNode: Renderable3DSceneNode { #region 构造函数和成员变量 ////// 材质 /// protected GenericMaterial material; ////// 创建一个新GenericMaterialSceneNode对象 /// /// 引擎 /// 所属场景 /// 漫反射纹理名称 /// 漫反射纹理平铺次数 /// 细节纹理名称 /// 细节纹理平铺次数 public GenericMaterialSceneNode(StunXnaGE engine, Scene setScene, string setDiffuseTextureName, Vector2 setDiffuseTiles, string setDetailTextureName, Vector2 setDetailTiles) : base(engine, setScene) { //创建并设置材质 material = new GenericMaterial(engine, engine.BaseEffect); this.material.DiffuseTextureName = setDiffuseTextureName; this.material.DetailTextureName = setDetailTextureName; if (string.IsNullOrEmpty(setDetailTextureName)) setDetailTiles = Vector2.Zero; this.material.DiffuseUVTile = setDiffuseTiles; this.material.DetailUVTile = setDetailTiles; //设置IMaterial接口 Imaterial = (IMaterial)material; } #endregion ////// 获取材质 /// public GenericMaterial Material { get { return material; } } } }
QuadSceneNode类的代码如下,代码很短,倒是单元测试的代码比类本身的代码还长:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using StunEngine.Controllers; using StunEngine.Rendering; using StunEngine.SceneManagement; namespace StunEngine.SceneNodes { ////// QuadSceneNode是一个由两个三角形构成的简单平面。 /// public class QuadSceneNode : GenericMaterialSceneNode { #region 构造函数和成员变量 ////// 创建一个默认平面,不使用细节纹理。 /// /// 引擎 /// 所属场景 /// 漫反射纹理名称 public QuadSceneNode(StunXnaGE engine,Scene setScene, string diffuseTextureName) : this(engine,setScene,diffuseTextureName, Vector2 .One ,null,Vector2 .Zero) { } ////// 创建一个拥有颜色纹理和细节纹理的平面。 /// /// 引擎 /// 所属场景 /// 漫反射纹理名称 /// 漫反射纹理UV重复次数 /// 细节纹理名称 /// 细节纹理UV重复次数 public QuadSceneNode(StunXnaGE engine,Scene setScene, string setDiffuseTextureName, Vector2 setDiffuseTiles,string setDetailTextureName,Vector2 setDetailTiles): base(engine,setScene,setDiffuseTextureName ,setDiffuseTiles ,setDetailTextureName ,setDetailTiles ) { } #endregion ////// 初始化QuadSceneNode对象。注意不要使用Initialize方法,它是由引擎调用的。 /// public override void Initialize() { this.UpdateOrder = SceneNodeOrdering.SceneNode.GetValue(); //创建平面顶点和索引数据 this.mesh = MeshBuilder.CreateQuad(engine.GraphicsDevice); base.Initialize(); } #region 单元测试 #if DEBUG ////// 测试QuadSceneNode类 /// public static void TestQuadSceneNode() { QuadSceneNode square1 = null; QuadSceneNode square2 = null; TestGame.Start("测试QuadSceneNode类", delegate { //添加一个覆盖有草地纹理和细节纹理的平面 square1 = new QuadSceneNode(TestGame.engine, TestGame.scene, "Textures\\Grass",new Vector2 (1.0f,1.0f),"Textures\\dirty", new Vector2 (1.0f,1.0f)); TestGame.scene.AddNode(square1); //将平面放置在左侧 Vector3 position=new Vector3 (-4,2,0); square1.pose.SetPosition(ref position); //将平面放大为4倍 Vector3 scale = new Vector3(4.0f, 4.0f, 1.0f); square1.Pose.SetScale(ref scale); //添加一个只有顶点绿色的的平面,开启镜面高光 square2 = new QuadSceneNode(TestGame.engine, TestGame.scene, null); TestGame.scene.AddNode(square2); square2.Material.DiffuseColor = new Vector3(0.0f, 1.0f, 0.0f); square2.Material.SpecularColor = new Vector3(1.0f, 1.0f, 1.0f); square2.Material.SpecularPower = 50; //将平面放置在右侧 position = new Vector3(4, 2, 0); square2.pose.SetPosition(ref position); //将平面放大为4倍 scale = new Vector3(4.0f, 4.0f, 1.0f); square2.Pose.SetScale(ref scale); //不显示光标 TestGame.scene.IsShowMouse = false; }, delegate { //按数字1键则切换使用的technique if (Input.KeyboardKeyJustPressed(Keys.D1)) { if (square1.Material.CurrentTechniqueName == "SimpleTextured") square1.Material.CurrentTechniqueName = "TexturedLights"; else square1.Material.CurrentTechniqueName = "SimpleTextured"; } //按数字2键则切换平面上的纹理 if (Input.KeyboardKeyJustPressed(Keys.D2)) { if (square1.Material.DiffuseTextureName == "Textures\\Grass") square1.Material.DiffuseTextureName = "Textures\\Rock"; else square1.Material.DiffuseTextureName = "Textures\\Grass"; } //按数字3键切换漫反射纹理平铺值 if (Input.KeyboardKeyJustPressed(Keys.D3)) { if (square1.Material.DiffuseUVTile == Vector2.One ) square1.Material.DiffuseUVTile = new Vector2(2.0f,2.0f); else square1.Material.DiffuseUVTile = Vector2.One; } }); } #endif #endregion } }
代码中的核心是创建平面顶点和索引的方法,为了代码清晰,我将这个方法放在了MeshBuilder类的静态方法CreateQuad中,代码如下:
////// 创建一个平面。 /// /// 图形设备 ///public static Mesh CreateQuad(GraphicsDevice g) { VertexPositionNormalTexture[] vertices; // 创建4个顶点。 vertices = new VertexPositionNormalTexture[4]; // 左下角顶点 vertices[0].Position = new Vector3(-HALF, -HALF, 0); vertices[0].TextureCoordinate = new Vector2(0, 1); vertices[0].Normal = Vector3.Backward ; // 左上角顶点 vertices[1].Position = new Vector3(-HALF, HALF, 0); vertices[1].TextureCoordinate = new Vector2(0, 0); vertices[1].Normal = Vector3.Backward ; // 右下角顶点 vertices[2].Position = new Vector3(HALF, -HALF, 0); vertices[2].TextureCoordinate = new Vector2(1, 1); vertices[2].Normal = Vector3.Backward ; // 右上角顶点 vertices[3].Position = new Vector3(HALF, HALF, 0); vertices[3].TextureCoordinate = new Vector2(1, 0); vertices[3].Normal = Vector3.Backward ; VertexBuffer vb = new VertexBuffer(g, typeof(VertexPositionNormalTexture), 4, BufferUsage.None); vb.SetData(vertices, 0, 4); VertexDeclaration vxDeclaration = new VertexDeclaration(g, VertexPositionNormalTexture.VertexElements); return new Mesh(PrimitiveType.TriangleStrip, vb, vxDeclaration, 4, 2); }
创建的平面如下图所示,四个顶点的第一个数字表示顶点序号,括号中的数字表示坐标。
首先,因为需要进行光照运算,所以顶点需要包含法线信息,所以它的数据类型是VertexPositionNormalTexture。
其次,因为XNA使用的是右手坐标系,所以顶点的创建顺序是顺时针方向,否则会因为背面剔除不被显示。如果不使用索引且使用TriangleList方式,你可以按0、1、2和1、3、2顺序创建顶点,当然按1、2、0和2、1、3顺序也行,但这样做需要发送给显卡六个顶点的数据,即6*(3+3+2)*4=216个字节(这是因为VertexPositionNormalTexture结构中Positon为Vector3类型,3个浮点数,Positon为Vector3类型,3个浮点数,Texture为Vector2类型,2个浮点数,而在32bit系统上一个浮点数大小为4个字节),数据有冗余。如果使用索引,只需发送四个顶点的数据共128个字节,但还要包括索引的大小,但平面只使用6个索引,不超过65536个,所以无需使用int类型,用ushort就可以了,数据量大小为6*2=12个字节,一共发送了140个字节。
但是还有更好的方法,就是使用TriangleStrip方式,使用TriangleStrips时你只需每个顶点定义1次。所以对第一个三角形,你需要定义顶点0,1和2(顺时针)。现在,对下一个三角形,你只需添加顶点3!XNA总是会使用最后三个顶点绘制三角形,所以第二个三角形使用顶点1, 2和3(逆时针)。通式是:第n个三角形由第(n-1),第n和第(n+1)顶点定义,n从1开始。通过这种方式,顶点的数量大大减少,只需发送128字节的数据!而且在大多数情况中,显卡绘制TriangleStrip的效率最高。你还可以参考一下这篇文章:3D系列3.8 使用Triangle Strips提高性能。虽然clayman在他的博客中Optimize Triangle Mesh Vertex(http://www.cnblogs.com/clayman/archive/2009/05/29/1491530.html)提到不见得如此,但我实验下来通常是使用TriangleStrip和TriangleStrip两者速度相差不多。所以我的原则是能使用索引就使用索引,能使用TriangleStrip就使用TriangleStrip。至于TriangleFan类型还是免了吧,它使用的场合极少,而且DirectX10已经不再支持这种类型,所以XGS4.0中也取消了对它的支持。
单元测试截图如下:
文件下载(已下载 1561 次)发布时间:2010/4/23 下午1:21:06 阅读次数:6409