赞助广告

 

年份

最新评论

评论 RSS

处理顶点——绘制三角形,线和点

clock 一月 21, 2011 10:45 by author alex
问题 你想定义一个3D几何结构并绘制到屏幕上,这个结构可以是由三角形,线或点组成。 解决方案 当绘制3D几何对象时,你首先需要使用primitives(图元)定义它的形状。Primitives (图元)是可以被XNA绘制的最基本的对象,最常被使用的图元是三角形。任何形状,包括圆,如果圆的数量足够多的话,都能用来表示三角形。XNA Framework可以将点,线、三角形作为图元绘制。 XNA允许你定义这些图元的所有3D坐标。当你调用DrawUserPrimitives方法时,只要你提供正确的观察矩阵和投影矩阵,XNA会自动将这些3D坐标转换为对应的屏幕坐标。 工作原理 要将3D坐标转换为屏幕上的像素位置,XNA需要知道相机的位置(存储在观察矩阵中)和关于相机镜头的某些细节(存储在投影矩阵中)。如果你的程序还没有提供这些矩阵或你未理解这些概念,可以看一下教程2-1和2-3,只需把QuakeCamera . cs文件复制并导入到项目中,通过添加如下变量在项目中添加相机系统: private QuakeCamera fpsCam; 并在Initialize方法中进行初始化: fpsCam = new QuakeCamera(GraphicsDevice.Viewport); 现在你的项目中已经有了一个相机系统!fpsCam对象提供了所需的观察矩阵和投影矩阵。如果你还想移动相机,可以在Update方法中传递用户的输入: protected override void Update(GameTime gameTime) { GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); if (gamePadState.Buttons.Back == ButtonState.Pressed) this.Exit(); MouseState mouseState = Mouse.GetState(); KeyboardState keyState = Keyboard.GetState(); fpsCam.Update(mouseState, keyState, gamePadState); base.Update(gameTime); } 因为三角形使用得最多,所以我从绘制三角形开始。 定义并绘制三角形 使用XNA绘制图元是很简单的,绘制三角形的步骤如下: 定义三角形三个顶点的坐标和颜色,将它们存储在一个数组中。 设置绘制三角形要用到的effect。 声明要传递的数据类型。 在屏幕上绘制数组中的内容。 定义顶点 对应三角形的每个顶点,你不仅存储了3D位置还存储了顶点的颜色。XNA提供了一个叫做VertexPositionColor的结构,这个结构可以存储位置和颜色。所以你需要添加VertexPositionColor结构的数组,告诉显卡每个顶点所包含信息的类型。 VertexPositionColor[] vertices; VertexDeclaration myVertexDeclaration; 定义了变量后,你将创建InitVertices 方法,此方法首先创建VertexDeclaration,然后将三个顶点添加到顶点数组中: private void InitVertices() { myVertexDeclaration = new VertexDeclaration(device, VertexPositionColor.VertexElements); vertices = new VertexPositionColor[3]; vertices[0] = new VertexPositionColor(new Vector3(-5, 1, 1), Color.Red); vertices[1] = new VertexPositionColor(new Vector3(-5, 5, 1), Color.Green); vertices[2] = new VertexPositionColor(new Vector3(-3, 1, 1), Color.Blue); } 在显卡绘制三角形前要用到VertexDeclaration,它告知显卡在顶点中包含何种类型的信息,这个信息通过顶点格式VertexElements显示,本例中是位置和颜色。 注意大多数情况下只在Draw方法中使用myVertexDeclaration变量,所以在调用Draw方法时会创建myVertexDeclaration变量,但这样做会导致每帧都会重新创建myVertexDeclaration!会让程序拖慢一点。更重要的是,因为Xbox 360使用的是.NET Framework的精简版本,这会导致在Xbox 360平台上程序会不定时地发生冲突,而且很难找到原因。所以请确保你只初始化myVertexDeclaration一次。 接下来你指定顶点的位置和颜色,本例中所有顶点的Z坐标都相同,你的三角形是平行于XY平面的。确保在LoadContent方法中调用这个方法。加载BasicEffect 在绘制三角形之前,需要告知显卡如何处理它接收到的信息。本例中,你想让指定的颜色作为三角形顶点的颜色,在更复杂的场景中你可能还要将这些信息用于其他目的。这一步要求调用一个effect。幸运的是,XNA内置了一个effect可以让显卡处理大多数逻辑和基本操作,这个effect叫做BasicEffect,你需要首先创建一个用来绘制三角形。在项目中添加这个变量: private BasicEffect basicEffect; 并在LoadContent方法中进行初始化: protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); InitVertices(); } 将三角形绘制到屏幕 注意因为是演示,所以这里我使用了DrawUserPrimitive方法,虽然这个方法不是最快的。教程5-4会讨论一个更加快且复杂的方法。 定义了顶点和加载了BasicEffect后,你就做好了绘制三角形的准备。显然,绘制三角形应在Draw方法中完成,而Draw方法在XNA Framework中已经提供了,它每帧都会被调用。在清除场景后添加以下代码: device.RenderState.CullMode = CullMode.None; basicEffect.World = Matrix.Identity; basicEffect.View = fpsCam.ViewMatrix; basicEffect.Projection = fpsCam.ProjectionMatrix; basicEffect.VertexColorEnabled = true; basicEffect.Begin(); foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration =myVertexDeclaration; device.DrawUserPrimitives<VertexPositionColor> (PrimitiveType.TriangleList, vertices, 0, 1); pass.End(); } basicEffect.End(); 在绘制三角形前,你需要设置BasicEffect的一些参量。这些参量可能是通过设置世界矩阵(见教程4-2)移动,旋转或缩放三角形。本例中你只是简单地将三角形绘制到指定位置,所以世界矩阵设为单位矩阵。 BasicEffect还需要知道观察矩阵和投影矩阵让显卡可以根据相机的位置将顶点从3D坐标转换为2D屏幕坐标。最后你还需指定BasicEffect effect使用你指定的颜色信息。 注意:如果你没有指定BasicEffect从哪采样颜色,那么它会使用最近指定的那个。 接下来,开始effect。高级ffects可以有多个pass,虽然BasicEffect只有一个pass,但遍历所有可能的pass还是一个好习惯。 一旦开始pass,你首先需要传递VertexDeclaration是显卡知道顶点中包含何种数据。最后调用device.DrawUserPrimitives方法绘制三角形。DrawUserPrimitives方法需要四个参数。第一个参数指定是绘制三角形、线还是点,以及在数组中的存储方式。第二个参数是用来存储顶点的数组。第三个参数指定数组中开始绘制的顶点索引,因为你想从第一个顶点开始绘制,所以这里设置为0。第四个参数表示需要绘制多少个图元,因为在数组中你只存储了三个顶点,所以只需绘制1个三角形。 运行代码后就可以绘制一个三角形了!如你所见,三角形的每个顶点的颜色都是你指定的,顶点之间的颜色是通过线性插值求出的。 注意:代码device.RenderState.CullMode = CullMode.None会关闭剔除(见教程5-6)。剔除会使三角形不会被绘制,所以当开始3D编程前,你应关闭它。 使用TriangleList(三角带)绘制多个三角形TriangleList 会绘制一个三角形,那么绘制多个也不难。首先定义顶点: private void InitVertices() { myVertexDeclaration = new VertexDeclaration(device, VertexPositionColor.VertexElements); vertices = new VertexPositionColor[12]; vertices[0] = new VertexPositionColor(new Vector3(-5, 1, 1), Color.Red); vertices[1] = new VertexPositionColor(new Vector3(-5, 5, 1), Color.Green); vertices[2] = new VertexPositionColor(new Vector3(-3, 1, 1), Color.Blue); vertices[3] = new VertexPositionColor(new Vector3(-3, 5, 1), Color.Gray); vertices[4] = new VertexPositionColor(new Vector3(-1, 1, 1), Color.Purple); vertices[5] = new VertexPositionColor(new Vector3(-1, 5, 1), Color.Orange); vertices[6] = new VertexPositionColor(new Vector3(1, 1, 1), Color.BurlyWood); vertices[7] = new VertexPositionColor(new Vector3(1, 5, 1), Color.Gray); vertices[8] = new VertexPositionColor(new Vector3(3, 1, 1), Color.Green); vertices[9] = new VertexPositionColor(new Vector3(3, 5, 1), Color.Yellow); vertices[10] = new VertexPositionColor(new Vector3(5, 1, 1), Color.Blue); vertices[11] = new VertexPositionColor(new Vector3(5, 5, 1), Color.Red); } 技巧:你也可以使用一个递增的整数节省时间,而不是手动设置所有索引。 定义完12个顶点后就可以绘制4个三角形了: device.DrawUserPrimitives<VertexPositionColor> (PrimitiveType.TriangleList, vertices, 0, 4); 如果你只想绘制最后两个三角形,你可以从第7个顶点开始,因为数组的第一个索引是0,所以这个顶点的索引是6: device.DrawUserPrimitives<VertexPositionColor> (PrimitiveType.TriangleList, vertices, 6, 2); 因为发送到显存的数据变少,这样做可以节省带宽。 使用TriangleStrip绘制多个三角形 如果你要绘制的三角形是相连的,就可以使用TriangleStrip 代替Triangle List,这样可以节省内存和带宽。看一下如图5-1所示的六个三角形。如果使用TriangleList需要6*3 = 18个顶点,当其中只有8个顶点是唯一的,其余10个顶点是重复存储并发送到显卡中的,这样做会浪费内存、带宽和GPU的处理能力! 图5-1 绘制一个TriangleStrip 将要绘制的三角形指定为一个TriangleStrip,XNA会基于前三个顶点绘制第一个三角形,然后为下一个顶点创建一个新三角形,而这个三角形使用了新的顶点和前两个顶点。这样,第一个三角形是由顶点0,1,2定义的;而第二个是由顶点1,2,3定义的;第三个是由顶点1,3,4定义的,以此类推。 要绘制x个三角形,你需要定义x+2个顶点。如果你在数组中存储了12个顶点,可以使用以下代码以TriangleStrip的形式绘制10个三角形: device.DrawUserPrimitives<VertexPositionColor> (PrimitiveType.TriangleStrip, vertices, 0, 10); 注意:当使用TriangleStrip以逆时针方向定义三角形时,不可能遵循剔除规则。(见教程5-6)。要解决这个问题,对于TriangleStrip 来说剔除有些特别,第一个三角形必须以顺时针的顺序定义,而其他三角形的顺序相反。 使用PointList绘制多个点 绘制点和线段的码类似;只是指定的图元类型不同。在数组中只需为每个点存储一个顶点。如果你存储了12个顶点,下面的代码就可以在屏幕上绘制12个点: device.DrawUserPrimitives<VertexPositionColor> (PrimitiveType.PointList, vertices, 0, 12); 注意:无论相机离开点的远近,每个点对应一个像素,你需要一双好眼睛才能看到这些像素。 译者注:要想绘制大于一个像素的点,需要使用PointSprite(点精灵),需要在绘制前添加如下代码: graphics.GraphicsDevice.RenderState.DepthBufferEnable = false; graphics.GraphicsDevice.RenderState.PointSpriteEnable = true; graphics.GraphicsDevice.RenderState.PointSize = 30.0f; 程序截图如下: 使用LineList绘制多条线段 你需要两个点定义一条线段。12个顶点可以绘制12/2 = 6条线段,代码如下: device.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, vertices, 0, 6); 使用LineStrip绘制多条线段如果一组线是相连的,你也可以绘制一个线带,如图5-2所示。 图5-2 使用LineStrip绘制多条线段 绘制x条线段需要x-1个顶点,所以如果你在数组中存储了12个顶点,那么可以使用如下方法画出11条线段: device.DrawUserPrimitives<VertexPositionColor> (PrimitiveType.LineStrip, vertices, 0, 11); 使用TriangleFan绘制多个三角形 如果三角形共享一个顶点,你也可以使用TriangleFan绘制三角形。图5-3显示了这种三角形。注意对应共享的中心点,每个三角形必须与邻近三角形共享一条边。 图5-3 使用TriangleFan绘制多个三角形 使用x–2个顶点绘制x个三角形。这意味着需要12个顶点绘制10个三角形。共享的点必须是数组中的第一个顶点。 注意:TriangleStrip在提供相同性能的前提下更加灵活: device.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleFan, vertices, 0, 10); 代码 因为这个本章的第一个教程,所以下面是使用TriangleStrip绘制三角形的全部代码: using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Net; using Microsoft.Xna.Framework.Storage; namespace BookCode { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; GraphicsDevice device; BasicEffect basicEffect; QuakeCamera fpsCam; CoordCross cCross; VertexPositionColor[] vertices; VertexDeclaration myVertexDeclaration; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void Initialize() { fpsCam = new QuakeCamera(GraphicsDevice.Viewport); base.Initialize(); } protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); InitVertices(); cCross = new CoordCross(device); } private void InitVertices() { myVertexDeclaration = new VertexDeclaration(device, VertexPositionColor.VertexElements); vertices = new VertexPositionColor[12]; vertices[0] = new VertexPositionColor(new Vector3(-5, 1, 1), Color.Red); vertices[1] = new VertexPositionColor(new Vector3(-5, 5, 1), Color.Green); vertices[2] = new VertexPositionColor(new Vector3(-3, 1, 1), Color.Blue); vertices[3] = new VertexPositionColor(new Vector3(-3, 5, 1), Color.Gray); vertices[4] = new VertexPositionColor(new Vector3(-1, 1, 1), Color.Purple); vertices[5] = new VertexPositionColor(new Vector3(-1, 5, 1), Color.Orange); vertices[6] = new VertexPositionColor(new Vector3(1, 1, 1), Color.BurlyWood); vertices[7] = new VertexPositionColor(new Vector3(1, 5, 1), Color.Gray); vertices[8] = new VertexPositionColor(new Vector3(3, 1, 1), Color.Green); vertices[9] = new VertexPositionColor(new Vector3(3, 5, 1), Color.Yellow); vertices[10] = new VertexPositionColor(new Vector3(5, 1, 1),Color.Blue); vertices[11] = new VertexPositionColor(new Vector3(5, 5, 1), Color.Red); } protected override void UnLoadContent() { } protected override void Update(GameTime gameTime) { GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); if (gamePadState.Buttons.Back == ButtonState.Pressed) this.Exit(); MouseState mouseState = Mouse.GetState(); KeyboardState keyState = Keyboard.GetState(); fpsCam.Update(mouseState, keyState, gamePadState); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); cCross.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix); //draw triangles device.RenderState.CullMode = CullMode.None; basicEffect.World = Matrix.Identity; basicEffect.View = fpsCam.ViewMatrix; basicEffect.Projection = fpsCam.ProjectionMatrix; basicEffect.VertexColorEnabled = true; basicEffect.Begin(); foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration; device.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleStrip, vertices, 0, 10); pass.End(); } basicEffect.End(); base.Draw(gameTime); } } }

处理模型——检测光标是否在模型上

clock 一月 21, 2011 10:39 by author alex
问题 你想检测光标是否在模型上。 解决方案 在XNA中,获取光标在屏幕上的2D位置是简单的。屏幕上的这个点对应3D空间中的一条射线Ray,如图4-28所示。   图4-28 2D光标对应3D空间的一条射线 因此,当你想检测光标在哪个模型上,需要检测射线与模型的碰撞,所以,这个教材会用到教程4-18的代码。 很有可能射线与多个模型相交,这个教程还会教你如何获取离屏幕最近的一个模型。 工作原理 你需要创建一个3D射线并将它与模型一起传递到教程4-18创建的ModelRayCollision 方法中。 只要知道了射线上的两个点就可以创建一条射线。你将使用的两个点如图4-28所示。第一个点是射线与近裁屏平面的交点;第二个点是与远裁平面的交点。 如果知道了这两个点的3D位置,你将使用ViewProjection矩阵进行转换获取屏幕上的2D位置。但是,你转换一个Vector3结果仍是一个Vector3,在结果Vector3中,通过使用ViewProjection矩阵进行变换,X和Y分量就是2D屏幕位置,第三个坐标Z也包含有用的信息,即相机与初始点的距离,为0时表示点在近裁平面,为1时表示在远裁平面。在深度缓冲中存储的正是这个距离。所以,每个在2D屏幕上绘制的像素实际上都有3个坐标值。 你要获取的两个点共享相同的像素,相同的2D位置,即它们的X和Y坐标是相同的。因为第一个点位于近裁平面,所以它的Z坐标为0。第二点位于远裁平面,所以Z坐标为1。这两个点在屏幕空间的三个坐标,在光标的情况中如下所示: (mouseX, mouseY, 0) (mouseX, mouseY, 1) 下面是代码: MouseState mouseState = Mouse.GetState(); Vector3 nearScreenPoint = new Vector3(mouseState.X, mouseState.Y, 0); Vector3 farScreenPoint = new Vector3(mouseState.X, mouseState.Y, 1); 如果从3D空间转换到屏幕空间,你要使用ViewProjection矩阵转换3D点。而这里你想讲这些点从屏幕空间转换到3D空间,所以使用的是ViewProjection矩阵的逆矩阵。你还需要将X和Y的光标坐标映射到[-1, 1]范围中,所以需要屏幕的像素大小的高和宽。 幸运的是,XNA提供了UnProject方法实现了这个映射和反向变换: Vector3 near3DWorldPoint = device.Viewport.Unproject(nearScreenPoint, fpsCam.ProjectionMatrix, fpsCam.ViewMatrix, Matrix.Identity); Vector3 far3DWorldPoint = device.Viewport.Unproject(farScreenPoint,fpsCam.ProjectionMatrix, fpsCam.ViewMatrix, Matrix.Identity); 你获得的这两个点如图4-28所示! 注意:你想知道相对于3D初始位置(0,0,0)的位置,所以你将Matrix. Identity作为世界矩阵。ViewProjection矩阵可以通过将View和Projection矩阵相乘获得,这也是你将这两个矩阵作为第二第三个参数的原因。 知道了射线的两个点,就可以创建一个Ray对象: Vector3 pointerRayDirection = far3DWorldPoint - near3DWorldPoint; pointerRayDirection.Normalize(); Ray pointerRay = new Ray(near3DWorldPoint,pointerRayDirection); 创建了Ray之后,你就做好了使用上一个教程中的ModelRayCollision方法检测Ray和模型间碰撞的准备: selected = ModelRayCollision(myModel, modelWorld, pointerRay); 添加一个Crosshair 前面的代码看起来很好,但如果你不能测试,代码再好也看不出来。所以让我们添加一个图像可以显示光标的位置,可见教程3-1学习绘制一个图像的简短介绍。首先将光标的2D位置存储在一个Vector2中: pointerPosition = new Vector2(mouseState.X, mouseState.Y); 在LoadContent方法中,添加一个SpriteBatch对象和一个Texture2D对象保存透明的crosshair图像: spriteBatch = new SpriteBatch(device); crosshair = content.Load("cross"); 然后在Draw方法中将这个图像绘制到屏幕: spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.Deferred, SaveStateMode.SaveState); spriteBatch.Draw(cross, mouseCoords, null, Color.White, 0, new Vector2(7, 7), 1, SpriteEffects.None, 0); spriteBatch.End(); 这可以让你在屏幕上看到光标。图像的中心点(7,7)位于光标位置。 检测多个对象 如果在场景中有多个对象,那么可能有多个对象会与射线发生碰撞。在大多数情况中,你只关心离相机最近的那个对象,因为这个对象才占据屏幕的像素。 要做到这点,你可以稍微调整一下ModelRayCollision方法,让它返回碰撞的距离而不是简单的true或false。类似于Intersect方法,你使用一个可空类型float?变量,这样如果没有碰撞那么返回null: private float? ModelRayCollision(Model model, Matrix modelWorld, Ray ray) { Matrix []modelTransforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(modelTransforms); float? collisionDistance = null; foreach (ModelMesh mesh in model.Meshes) { Matrix absTransform = modelTransforms[mesh.ParentBone.Index]*modelWorld; Triangle[] meshTriangles = (Triangle[])mesh.Tag; foreach (Triangle tri in meshTriangles) { Vector3 transP0 = Vector3.Transform(tri.P0, absTransform); Vector3 transP1 = Vector3.Transform(tri.P1, absTransform); Vector3 transP2 = Vector3.Transform(tri.P2, absTransform); Plane trianglePlane = new Plane(transP0, transP1, transP2); float distanceOnRay = RayPlaneIntersection(ray, trianglePlane); Vector3 intersectionPoint = ray.Position + distanceOnRay * ray.Direction; if (PointInsideTriangle(transP0, transP1, transP2, intersectionPoint)) if ((collisionDistance == null) || (distanceOnRay < collisionDistance)) collisionDistance = distanceOnRay; } } return collisionDistance; } 每次发生碰撞时,你检查collisionDistance是否仍是null。这会显示是第一次检测到碰撞,所以你将这个距离存储到collisionDistance 中。从这时起,你检查这个距离是否小于已知的距离,如果是,则重写这个距离。 从变量collisionDistance返回的结果将包含离相机最近的碰撞点,你可以使用这个结果检测哪个模型离相机最近。 代码 这个代码创建一个3D射线,这个射线描述所有属于通过光标显示的像素的点。这个射线传递到ModelRayCollision方法: Vector3 nearScreenPoint = new Vector3(mouseState.X, mouseState.Y, 0); Vector3 farScreenPoint = new Vector3(mouseState.X, mouseState.Y, 1); Vector3 near3DWorldPoint = device.Viewport.Unproject(nearScreenPoint, fpsCam.ProjectionMatrix, fpsCam.ViewMatrix, Matrix.Identity); Vector3 far3DWorldPoint = device.Viewport.Unproject(farScreenPoint, fpsCam.ProjectionMatrix, fpsCam.ViewMatrix, Matrix.Identity); Vector3 pointerRayDirection = far3DWorldPoint - near3DWorldPoint; pointerRayDirection.Normalize(); Ray pointerRay = new Ray(near3DWorldPoint, pointerRayDirection); selected = ModelRayCollision(myModel, worldMatrix, pointerRay);

处理模型——使用逐三角形检查检测射线-模型碰撞

clock 一月 16, 2011 20:07 by author alex
问题 你想检测一根3D射线是否与模型发生碰撞,如果你想进行一个最高细节的碰撞检测这是必须的,可以作为教程4-11的拓展进行子弹是否与物体发生碰撞的高细节碰撞。如下一个教程所示,这可以用来检测鼠标是否在模型上。 而且,这个方法也可以用来进行模型之间的最高精度的检测,这是因为对应一条射线的每一个三角形的边缘可以用来进行与另一个模型的碰撞检测。 这个教程会处理最复杂的情况,允许模型的不同部分独立地被转换,使这个教程可以适用于模型动画,这些变换作用在模型顶点的最终位置上。例如,如果一个人移动手臂,手臂顶点的最终位置会发生移动。这个教程会考虑到这个变换。 解决方案 这个教程使用教程4-13和4-14中的自定义素材管道的混合,因为在模型的每个ModelMesh的Tag属性中储存了三角形对象的数组,每个三角形对象存储了三个Vector3定义一个三角形。 让每个ModelMesh都存储各自Triangle对象数组的好处是让你可以使用ModelMesh 的Bone矩阵对它们进行转换。无论当前作用在ModelMesh 上的变换是什么,你总能通过使用ModelMesh的绝对变换矩阵找到Triangle对象在3D空间中的绝对位置(见教程3-9和4-17)。这是必须的,因为大多数情况下3D Ray是定义在绝对3D空间中的,所以你需要知道顶点的绝对3D位置。 知道了每个模型三角形的绝对3D位置后,剩下的问题就是对模型每个三角形进行 射线-三角形碰撞检测了。 工作原理 因为内容管道使用了教程4-15中的自定义类(Triangle),最简单的方法是将这个内容管道项目添加到项目中。你可以右击解决方案并选择Add Existing Project,找到内容管道的. Csproj文件并选择它。 下一步是让XNA项目能够访问到这个内容管道项目,可参加教程3-9的步骤1–5。最后两步是让XNA反串行化Triangle对象,这在教程4-15中已经解释过了。 6. 将新创建的引用添加到项目中。 7. 选择新创建的处理器处理模型。 8. 设置项目依赖项。 9. 在XNA项目中,添加内容管道的引用。 10. 添加内容管道命名空间。 别忘了第7步,选择ModelMeshTriangleProcessor处理你导入的模型。 这个处理器将Triangle对象的数组储存在模型的ModelMesh的Tag属性中。 变换 现在你可以处理所有的模型顶点位置,你可以定义一个方法检测射线-模型间的碰撞: private bool ModelRayCollision(Model model, Matrix modelWorld, Ray ray) { Matrix[] modelTransforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(modelTransforms); bool collision = false; foreach (ModelMesh mesh in model.Meshes) { Matrix absTransform = modelTransforms[mesh.ParentBone.Index] *modelWorld; Triangle[] meshTriangles = (Triangle[])mesh.Tag; foreach (Triangle tri in meshTriangles) { Vector3 transP0 = Vector3.Transform(tri.P0, absTransform); Vector3 transP1 = Vector3.Transform(tri.P1, absTransform); Vector3 transP2 = Vector3.Transform(tri.P2, absTransform); // if (Ray-Triangle collision) // return true; } } return collision; } 思路很简单:你首先将collision设置为false。对模型的每个部分,你遍历所有三角形对象,包含三角形位置。对每个三角形,你检查三角形是否和ray碰撞,如果发生碰撞则返回true。 存储在Triangle对象中的位置是相对于ModelMesh的初始位置的,这意味你要首先通过ModelMesh的绝对变换矩阵进行变换获得相对于模型的位置,然后使用模型的世界矩阵获取在3D空间中的位置(换句话说,绝对位置)。 第一个变换看似多余,但事实上它有很大的好处:如果你想旋转模型的一个ModelMesh (例如,人的手臂),这个旋转也会被考虑进去。通过绝对变换矩阵变换了手臂的顶点位置后,你就获得了相对应模型初始位置的位置并考虑了旋转! 在前面的代码中,你首先计算了模型所有Bone的绝对变换矩阵。接下来,将绝对矩阵和世界矩阵组合起来,使用最终的结果矩阵将ModelMesh中的每个三角形变换到绝对3D空间。 最后,你获得了ModelMesh中的每个三角形的确切3D空间位置,可以施加想要的任何动画了。 Ray-Triangle碰撞 有了每个三角形的绝对位置,你将检测三角形和射线之间的碰撞。 这个过程可以分为两部分。首先找到射线与三角形平面的碰撞点。因为三角形和这个相交点是在同一个平面上,所以问题就转化为了一个2D问题,如图4-25所示。接下来,你要检查碰撞点是否在三角形的内部。 图4-25 将3D问题(左图)简化为2D问题(右图) 你将编写两个方法处理问题。第一个方法,RayPlaneIntersection,接受一个Plane和一个Ray为参数并返回射线上的碰撞点的距离。由这个方法可以计算射线和三角形之间的相交点。 第二个方法,PointInsideTriangle,接受三角形的三个坐标和一个额外点(在本教程的情况中即相交点,返回点是否在三角形的内部。 如果在内部,你就检测到了射线和模型的碰撞了。下面的代码要替换掉前面的两行伪代码(译者注:即// if (Ray-Triangle collision) //return true;): Plane trianglePlane = new Plane(transP0, transP1, transP2); float distanceOnRay = RayPlaneIntersection(ray, trianglePlane); Vector3 intersectionPoint = ray.Position + distanceOnRay * ray.Direction; if (PointInsideTriangle(transP0, transP1, transP2, intersectionPoint)) return true; 首先基于三角形的三个顶点创建一个三角形plane。将这个plane和Ray传递到 RayPlaneIntersection方法,获取射线上相交点的位置,这可以用来计算相交点的位置。最后检查相交点是否在三角形内部。 注意:更多Ray对象的使用方法可参加教程4-11和4-19。 获取Ray和Plane之间的碰撞点 你很幸运,第一个方法主要是代数运算。我选择了一个基于矢量的方法让你可以形象地看到这个过程,如图4-26所示。 图4-26 找到射线上的碰撞点的位置 给定一条Ray和一个Plane,这个方法可以找到ray.Direction和collisionPoint两点间的距离,注意这个方法中的变量的所有长度在图中显示在左右两侧。 private float RayPlaneIntersection(Ray ray, Plane plane) { float rayPointDist = -plane.DotNormal(ray.Position); float rayPointToPlaneDist = rayPointDist - plane.D; float directionProjectedLength = Vector3.Dot(plane.Normal, ray.Direction); float factor = rayPointToPlaneDist / directionProjectedLength; return factor; } 首先找到rayPointDist,它是射线投影长度,位于平面法线上(垂直于屏幕的线段)。变量plane. D定义为初始点 (0,0,0)离开平面的最短距离,即沿着平面法线的距离。 有了这两个沿着法线的距离,你可以通过将两者相减找到ray. Position和平面间的距离(即rayPointToPlaneDist)。 这个离开射线的最短距离,从ray. Position指向平面并垂直于平面,即仍是沿着平面法线方向。现在决定射线方向沿平面法线的距离,换句话说,如果你从ray. Position出发发射一条射线,它离开平面有多远? 你可以通过将ray. Direction投影到平面法线上获取这个长度,这可以使用两者的点积。这个长度在图4-26右边显示。 知道了ray. Direction偏离平面的程度和ray. Position偏离平面的距离,你就可以计算需要在ray. Position上乘以ray. Direction的多少倍!这个因子由方法返回用来找到射线和平面相交点的确切位置。首先定义Ray的出发点(=ray. Position),并通过这个因子添加Ray方向上,你就获得了碰撞点。这一步在ModelRayCollision方法中实现: Vector3 intersectionPoint = ray.Position + distanceOnRay * ray.Direction; 检查碰撞点是否在三角形内部 知道了射线在哪儿与三角形平面相交后,你要检查相交点是否在三角形内部。否则,射线和三角形并没有真正相交。 这又一次可以使用一个基于向量的方法。图4-27展示了基本原理。如果你还记得碰撞点位于与三角形相同的平面的内部,这个图不是很复杂。 图4-27 检查碰撞点是否在三角形内部 在图4-27的左边,碰撞点在三角形的外部。首先考虑三角形上的T0,T1边界。 你想知道collisionPoint 在边界的哪一面上。看一下图4-27左边到矢量A = (collisionPoint – T0) 和B = (T1-T0)。如果你从A旋转到B,将是逆时针方向。 但是在图4-27的右边,collisionPoint在三角形内部,即在T0,T1边界的另一边,当从A旋转到B将是顺时针方向! 显然你可以通过这个方法检查collisionPoint在边界的那一边。你可以使用A和B的叉乘, A × B,返回一个向量,当逆时针旋转时这个向量指向平面的外部。如果顺时针旋转,如图4-27右边所示,这个向量指向下方。 下面的代码计算A和B向量和它们的叉乘: Vector3 A = point - p0; Vector3 B = p1 - p0; Vector3 cross = Vector3.Cross(A, B); 让我们再进一步:如果collisionPoint在三角形边界的同一侧说明它在三角形的内部,换句话说,如果三个叉乘矢量同一方向则collisionPoint在三角形的内部。 所以为了检测点是否在三角形的内部,你要计算三角形每个边界的叉乘矢量。当这些矢量的分量的符号相同说明它们指向相同的方向。下面是代码: private bool PointInsideTriangle(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 point) { if (float.IsNaN(point.X)) return false; Vector3 A0 = point - p0; Vector3 B0 = p1 - p0; Vector3 cross0 = Vector3.Cross(A0, B0); Vector3 A1 = point - p1; Vector3 B1 = p2 - p1; Vector3 cross1 = Vector3.Cross(A1, B1); Vector3 A2 = point - p2; Vector3 B2 = p0 - p2; Vector3 cross2 = Vector3.Cross(A2, B2); if (CompareSigns(cross0, cross1) && CompareSigns(cross0, cross2)) return true; else return false; } 上述方法以三角形的三个顶点和collisionPoint位置为参数。如果射线与平面平行,找不到collisionPoint,则point的每个分量都会是NaN(NaN即Not a Number)。如果发生这种情况,方法立即返回,对于一个平行的射线,三角形永远不会和它相交。 如果不平行,这个方法继续运行。对于三角形的每一边,你计算叉乘。如果所有的叉乘矢量都指向相同的方向(这意味着分量的符号是相同的),那么collisionPoint就在三角形的内部! CompareSigns方法接受两个Vector3参数。它检测两个X分量,Y分量,Z分量是否符号相同。简而言之,这个方法检查这个方向的夹角是否小于90度。下面是代码: private bool CompareSigns(Vector3 first, Vector3 second) { if (Vector3.Dot(first, second) > 0) return true; else return false; } 你只是简单地计算两个矢量的点积,将一个矢量投影到另一个矢量上,结果是一个 single类型,如果两个向量夹角大于90度结果为负,垂直时为0,小于90度为正。 性能优化 对于模型的每个三角形都有调用这个检测,所以,让它进行得尽可能快是很重要的。下面,你会学习几个性能优化的建议。 转换射线而不是位置 在ModelMesh中,你使用绝对变换矩阵转换了每个位置,将这些位置转换到绝对3D空间中,这样才可以和Ray比较,因为它同样定义在绝对3D空间中。 更好的做法是将射线转换到ModelMesh的坐标空间中,因为这样做只需对每个 ModelMesh操作一次。这样Ray和三角形位置都在相同的坐标空间中,它们也可以进行比较。 使用以下代码替换ModelRayCollision方法: private bool ModelRayCollision(Model model, Matrix modelWorld, Ray ray) { Matrix[] modelTransforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(modelTransforms); bool collision = false; foreach (ModelMesh mesh in model.Meshes) { Matrix absTransform = modelTransforms[mesh.ParentBone.Index] * modelWorld; Triangle[] meshTriangles = (Triangle[])mesh.Tag; Matrix invMatrix = Matrix.Invert(absTransform); Vector3 transRayStartPoint = Vector3.Transform(ray.Position, invMatrix); Vector3 origRayEndPoint = ray.Position + ray.Direction; Vector3 transRayEndPoint = Vector3.Transform(origRayEndPoint, invMatrix); Ray invRay = new Ray(transRayStartPoint, transRayEndPoint - transRayStartPoint); foreach (Triangle tri in meshTriangles) { Plane trianglePlane = new Plane(tri.P0, tri.P1, tri.P2); float distanceOnRay = RayPlaneIntersection(invRay, trianglePlane); Vector3 intersectionPoint = invRay.Position + distanceOnRay * invRay.Direction; if (PointInsideTriangle(tri.P0, tri.P1, tri.P2, intersectionPoint)) return true; } } return collision; } 你将Ray从绝对3D空间转换到ModelMesh空间而不是将三角形位置转换到绝对3D空间,这是absTransform 变换的反变换,所以你要取逆矩阵。然后,你获取射线的两个点,将它们使用逆矩阵进行变换,并创建一个新的射线。有了在ModelMesh空间中的射线,就可以将这些与未经变换的顶点位置做比较了,因为此时它们在相同的坐标空间! 这样,对每个ModelMesh你只需进行两次变换,而不是对每个三角形进行三次变换! 首先进行一个包围球检测 相对于对射线和所有三角形进行碰撞检测,更好的做法是进行射线和ModelMesh的包围球之间的碰撞检测,如果这个简单快速的检查返回false,那就没必要检查每个三角形了! 如果这个检测返回true,在转到繁重的逐三角形检测之前,更好的方法是检测射线和更小的ModelMesh的包围球之间的碰撞。或者可以做得更好,检测所有ModelMeshPart包围球之间的碰撞(在模型的一个ModelMesh有多个ModelMeshPart的情况下)。 如你所见,一个模型可以分成几个ModelMesh,因为你拥有了更多(也更精确)的包围球,导致更少的三角形检测,所以让更精确的动画和加速碰撞检测成为可能。 代码 这是检测射线和模型间碰撞的最终方法: private bool ModelRayCollision(Model model, Matrix modelWorld, Ray ray) { Matrix[] modelTransforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(modelTransforms); bool collision = false; foreach (ModelMesh mesh in model.Meshes) { Matrix absTransform = modelTransforms[mesh.ParentBone.Index] * modelWorld; Triangle[] meshTriangles = (Triangle[])mesh.Tag; foreach (Triangle tri in meshTriangles) { Vector3 transP0 = Vector3.Transform(tri.P0, absTransform); Vector3 transP1 = Vector3.Transform(tri.P1, absTransform); Vector3 transP2 = Vector3.Transform(tri.P2, absTransform); Plane trianglePlane = new Plane(transP0, transP1, transP2); float distanceOnRay = RayPlaneIntersection(ray, trianglePlane); Vector3 intersectionPoint = ray.Position + distanceOnRay * ray.Direction; if (PointInsideTriangle(transP0,transP1, transP2, intersectionPoint)) return true; } } return collision; } 这个方法调用RayPlaneIntersection方法, 返回一个平面和一条射线之间的碰撞点的位置: private float RayPlaneIntersection(Ray ray, Plane plane) { float rayPointDist = -plane.DotNormal(ray.Position); float rayPointToPlaneDist = rayPointDist - plane.D; float directionProjectedLength = Vector3.Dot(plane.Normal, ray.Direction); float factor = rayPointToPlaneDist / directionProjectedLength; return factor; } PointInsideTriangle方法用来检测碰撞点是否在三角形的内部: private bool PointInsideTriangle(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 point) { if (float.IsNaN(point.X)) return false; Vector3 A0 = point - p0; Vector3 B0 = p1 - p0; Vector3 cross0 = Vector3.Cross(A0, B0); Vector3 A1 = point - p1; Vector3 B1 = p2 - p1; Vector3 cross1 = Vector3.Cross(A1, B1); Vector3 A2 = point - p2; Vector3 B2 = p0 - p2; Vector3 cross2 = Vector3.Cross(A2, B2); if (CompareSigns(cross0, cross1) && CompareSigns(cross0, cross2)) return true; else return false; } 这个方法检测碰撞点是否在三角形的同一侧,这可以通过检测叉乘矢量是否同一方向实现,这个检测是在CompareSigns方法中实现的: private bool CompareSigns(Vector3 first, Vector3 second) { if (Vector3.Dot(first, second) > 0) return true; else return false; }

处理模型——根据地形正确倾斜模型

clock 一月 16, 2011 19:48 by author alex
问题 当在地形上移动一个汽车模型时,使用教程4-2你可以调整车的高度,使用教程5-9你可以找到位于汽车下面的地形的高度。但是,如果你没有根据车下面的坡度正确使车身发生倾斜,那么在起伏不平的地形上效果看起来不会很好。 你想正确地放置和倾斜汽车模型使之可以匹配地形的起伏。 解决方案 这个问题可以分成四个部分: 首先,你想找到模型四个轮胎的最低顶点的位置。 其次,你想获取这四个顶点之下的地形的高度。 下一步,你想获取模型沿Forward和Side向量的旋转以正确地倾斜模型。 最后,你需要找到模型和地形之间的高度差并补偿这个差异。 要做到第一步,你可以编写一个自定义模型处理器,这个处理器可以在每个ModelMesh的Tag属性中存储ModelMesh中最低顶点的位置。因为四个轮子的最低顶点位置在游戏运行时会发生移动,所以每次更新时你都需要基于这些向量的World位置变换这些位置。 要找到由三角形构成的表面上指定位置的高度,你可以使用教程5-9中的 GetExactHeightAt方法。 找到旋转角度的方法基于一个简单的数学原理(这个原理你应该在高中就学过!)。 最后一步需要在模型的世界矩阵上添加一个垂直平移。 工作原理 编写一个自定义模型处理器获取每个ModelMesh最低点的位置 第一步是获取模型轮子最低顶点的位置,这是因为这些顶点与地形接触。你将创建一个模型处理器,这个处理器是教程4-14的简化版本。 对于模型的每个ModelMesh,你将在Tag属性中存储最低点的位置。注意,你想基于ModelMesh的初始位置定义这些位置,这样做可以让本教程的方法也适用于模型的Bone动画(见教程4-9)。 开始的代码在教程4-14中已经解释过了,但在模型处理器的Process方法中有一点小变化: public override ModelContent Process(NodeContent input, ContentProcessorContext context) { List lowestVertices = new List(); lowestVertices = FindLowestVectors(input, lowestVertices); ModelContent usualModel = base.Process(input, context); int i = 0; foreach (ModelMeshContent mesh in usualModel.Meshes) mesh.Tag = lowestVertices[i++]; return usualModel; } FindLowestVertices方法遍历模型的所有节点并将每个ModelMesh的最低点位置存储在lowestVertices集合中。有了这个集合,你再将集合中的每个位置存储到对应ModelMesh的Tag属性中。 基于教程4-14介绍过的AddVertices方法,FindLowestVertices方法将顶点位置添加到集合并将这个集合传递到所有子节点: private List<Vector3>FindLowestVectors(NodeContent node, List<Vector3>lowestVertices) { Vector3? lowestPos = null; MeshContent mesh = node as MeshContent; foreach (NodeContent child in node.Children) lowestVertices = FindLowestVectors(child, lowestVertices); if (mesh != null) foreach (GeometryContent geo in mesh.Geometry) foreach (Vector3 vertexPos in geo.Vertices.Positions) if ((lowestPos == null) || (vertexPos.Y < lowestPos.Value.Y)) lowestPos = vertexPos; lowestVertices.Add(lowestPos.Value); return lowestVertices; } 首先对子节点调用这个方法,这样也在集合中存储了它们最低点的位置。 对于每个节点,你检查节点是否包含几何信息。如果包含,你将遍历所有顶点。如果lowestPos为null (当第一次检查时lowestPos为null)或当前的位置低于存储在lowestPos中的前一个值,那么将当前位置存储在lowestPos中。 最后,有着最低Y坐标的顶点存储在lowestPos中,你将它添加到lowestVertices集合中并将这个集合返回到父节点中。 注意:如教程4-14中所讨论的,一个ModelMesh首先对自己的子节点调用这个方法,然后将最低点位置添加到集合中,更直观的方法是一个节点首先将自己的Vector存储在集合中然后在它的子节点上调用这个方法。你必须按照前面所示的顺序进行这个操作,因为这也是模型处理器中节点转换为ModelMesh的顺序。在Process方法中可以容易地将正确的Vector存储在正确的ModelMesh的Tag属性中。 请确保选择模型处理器去处理导入的模型。 获取轮子最低顶点的绝对3D坐标 在XNA项目中,你已经存储了四个轮子的位置。这些位置基于模型的结构和对应轮子的ModelMesh,你可以使用教程4-8中的代码可视化模型的结构。 对每个轮子来说,你需要知道对应ModelMesh的ID。知道了ID后,你可以访问到每个轮子的最低位置并将它存储在一个变量中。虽然你可以使用下列代码四次或者也可以以一个简单的循环代替,我总是给四个轮子使用四个有直观名称的变量。左前轮简写成fl,右后轮为br等。 int flID = 5; int frID = 1; int blID = 4; int brID = 0; Vector3 frontLeftOrig = (Vector3)myModel.Meshes[flID].Tag; Vector3 frontRightOrig = (Vector3)myModel.Meshes[frID].Tag; Vector3 backLeftOrig = (Vector3)myModel.Meshes[blID].Tag; Vector3 backRightOrig = (Vector3)myModel.Meshes[brID].Tag; 记住,你需要对模型使用正确的ID,可参见教程4-8。 你存储在ModelMesh的Tag属性中的位置是相对于ModelMesh的相对初始位置的,你需要知道它们同地形相同的空间中的位置,即绝对3D空间中的位置。 首先需要知道最低顶点相对于模型初始位置的位置,这可以通过使用ModelMesh的绝对变换矩阵进行转换做到(见教程4-9)。接下来,你可能还要使用一个世界矩阵在3D世界的一个位置绘制模型,你可以将每个ModelMesh的Bone矩阵和模型的世界矩阵组合起来。 myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); Matrix frontLeftMatrix = modelTransforms[myModel.Meshes[flID].ParentBone.Index]; Matrix frontRightMatrix = modelTransforms[myModel.Meshes[frID].ParentBone.Index]; Matrix backLeftMatrix = modelTransforms[myModel.Meshes[blID].ParentBone.Index]; Matrix backRightMatrix = modelTransforms[myModel.Meshes[brID].ParentBone.Index]; Vector3 frontLeft = Vector3.Transform(frontLeftOrig, frontLeftMatrix * modelWorld); Vector3 frontRight = Vector3.Transform(frontRightOrig, frontRightMatrix * modelWorld); Vector3 backLeft = Vector3.Transform(backLeftOrig, backLeftMatrix * modelWorld); Vector3 backRight = Vector3.Transform(backRightOrig, backRightMatrix * modelWorld); 首先,你计算模型的所有Bone的绝对变换矩阵(见教程4-9)。接下来,对每个轮子,你找到存储在对应轮子的ModelMesh的Bone中的绝对矩阵。 知道了每个轮子的绝对变换矩阵之后,将这个矩阵与模型的世界矩阵组合起来,并使用这个结果矩阵变换你的顶点,变换的结果Vector3包含了轮子最低向量的绝对3D坐标。 注意:根据教程4-2中的详细解释,矩阵乘法的顺序是重要的。因为这些顶点是模型的一部分,你首先需要考虑与存储在世界矩阵中的绝对初始位置的偏移,然后你要转换这些顶点使它们变成相对于模型的初始位置。通过这种方式,世界矩阵作用在模型的Bone上,这也是你想要的结果。如果不这样做,那么任何包含在Bone中的旋转将会作用在世界矩阵上,可参见教程4-2获取更对矩阵乘法顺序的知识。 最后,因为顶点的位置和地形坐标的位置都在绝对3D空间中,你就做好了检测四个轮子和地形之间碰撞的准备。 获取模型下面的地形的高度 现在你已经找到了四个轮子的绝对3D位置,就可以找到旋转角度了。首先你想知道应该绕Side向量旋转多少,即汽车的前部应该上升还是下降。你只需要基于两点计算而不是全部四点。第一个点,前面,位于两个前轮之间,第二个点,后面,位于后轮之间,如图4-23所示。你想找到旋转量,这样线段frontToBack (这条线段连接这两个点)会与地形对齐。 图4-23 当倾斜汽车时关注的点 你可以通过计算邻近轮子的平均值获取这两个点的位置,将这两个点相减获取两者之间的backToFront向量: Vector3 front = (frontLeft + frontRight) / 2.0f; Vector3 back = (backLeft + backRight) / 2.0f; Vector3 backToFront = front - back; 记住,你想获取汽车绕Side向量旋转多少角度,所以它的front点会上下移动。理想情况中,你想让frontToBack向量与地形坡度有相同的倾斜角度,如图4-24所示。你想计算的角度在图4-24中表示为fbAngle。 首先需要找到地形上这两个点的高度差,这可以使用教程5-9中的GetExactHeightAt方法: float frontTerHeight = terrain.GetExactHeightAt(front.X, -front.Z); float backTerHeight = terrain.GetExactHeightAt(back.X, -back.Z); float fbTerHeightDiff = frontTerHeight - backTerHeight; 图4-24 获取倾斜角度 计算旋转角度 现在知道了在地形上的高度差,可以使用三角函数计算倾斜角度了。在直角三角形中,如果你知道了一个锐角的对边(图4-24中的A)和邻边(图4-24中的B),皆可以使用反正切函数计算出这个角。对边的长度就是你刚才计算的高度差,第二个长度就是frontToBack向量的长度! 这个方法可以找到旋转角度和构建绕(1,0,0) Side向量的对应旋转。旋转角度存储在一个四元数中(四元数可以存储并组合没有万向节锁的旋转,可参加教程2-4): float fbAngle = (float)Math.Atan2(fbTerHeightDiff, backToFront.Length()); Quaternion bfRot = Quaternion.CreateFromAxisAngle(new Vector3(1, 0, 0), -fbAngle); 如果你使用这个旋转量旋转模型,模型的front和back点将会随着地形倾斜! 显然,现在只完成了50%的工作,因为你还要将模型绕着Forward向量旋转使它的left和right点也能随着地形发生偏转。幸运的是,你可以使用相同的方法和代码计算lrAngle。只需简单地让图4-23中的leftToFront线段对齐之下的地形: Vector3 left = (frontLeft + backLeft) / 2.0f; Vector3 right = (frontRight + backRight) / 2.0f; Vector3 rightToLeft = left - right; float leftTerHeight = terrain.GetExactHeightAt(left.X, -left.Z); float rightTerHeight = terrain.GetExactHeightAt(right.X, -right.Z); float lrTerHeightDiff = leftTerHeight - rightTerHeight; float lrAngle = (float)Math.Atan2(lrTerHeightDiff, rightToLeft.Length()); Quaternion lrRot = Quaternion.CreateFromAxisAngle(new Vector3(0, 0, -1), -lrAngle); 有了两个旋转量,可以很容易地将它们相乘组合起来,并将这个变换与世界变换组合起来: Quaternion combRot = fbRot * lrRot; Matrix rotatedModelWorld = Matrix.CreateFromQuaternion(combRot) * modelWorld; 如果你使用这个rotatedModelWorld矩阵作为渲染模型的矩阵,那么模型将很好地匹配地形旋转!但是,你还需要将模型放置在正确的高度上。 将模型放置在正确地高度上 因为你旋转了模型,一些轮子会低于其他的。现在已经计算好了旋转,你可以很容易地找到轮子的旋转后的位置: Vector3 rotFrontLeft = Vector3.Transform(frontLeftOrig, frontLeftMatrix * rotatedModelWorld); Vector3 rotFrontRight=Vector3.Transform(frontRightOrig,frontRightMatrix * rotatedModelWorld); Vector3 rotBackLeft = Vector3.Transform(backLeftOrig, backLeftMatrix * rotatedModelWorld); Vector3 rotBackRight= Vector3.Transform(backRightOrig, backRightMatrix * rotatedModelWorld); 然后使用这些位置的X和Y分量找到它们应该放置的确切位置: float flTerHeight = terrain.GetExactHeightAt(rotFrontLeft.X, -rotFrontLeft.Z); float frTerHeight = terrain.GetExactHeightAt(rotFrontRight.X, -rotFrontRight.Z); float blTerHeight = terrain.GetExactHeightAt(rotBackLeft.X, -rotBackLeft.Z); float brTerHeight = terrain.GetExactHeightAt(rotBackRight.X, -rotBackRight.Z); 知道了轮子的Y高度坐标和应该放置的位置,就可以很简单地结算应该偏离多少: float flHeightDiff = rotFrontLeft.Y - flTerHeight; float frHeightDiff = rotFrontRight.Y - frTerHeight; float blHeightDiff = rotBackLeft.Y - blTerHeight; float brHeightDiff = rotBackRight.Y - brTerHeight; 你获得了四个不同的值用来将模型放置到正确的高度,但是你调整的是整个模型,所以只有一个值真正有用。 使用哪个值随你喜欢,如果你不想任意一个轮子陷进地面,那就取最大的一个。如果你不想让轮子与地面之间有空隙,则取最小的一个。本教程我取四者的平均值: float finalHeightDiff = (blHeightDiff + brHeightDiff + flHeightDiff + frHeightDiff) / 4.0f; modelWorld = rotatedModelWorld * Matrix.CreateTranslation(new Vector3(0, -finalHeightDiff, 0)); 最后一行代码包含这个垂直变换的矩阵添加到世界矩阵中: 注意:你要将这个新矩阵放置在乘法的右边,这样才能使模型绕着绝对Up轴旋转。如果放在左边,模型将会沿着模型的Up轴旋转。 如果你使用这个矩阵绘制模型,那么模型会很好地匹配地形。代码量很大,但你把它放在一个for循环中,代码量已经除以4了。如果你觉得计算量很大,别忘了这些计算是只作用在那些必须被绘制到屏幕的模型上的! 为动画做准备 如果模型还有动画,你会遇到点麻烦。例如,如果你将一个轮子旋转180度,存储在Tag属性中的向量会变为轮子的最高的而不是最低点!这会让轮子沉到地面之下。要解决这个问题,你需要将轮子的Bone矩阵还原到计算前的初始位置。这不难,因为在进行模型动画时你总有存储这些位置(见教程4-9); 2, 4, 6和8是四个轮子的Bone索引。 myModel.Bones[2].Transform = originalTransforms[2]; myModel.Bones[4].Transform = originalTransforms[4]; myModel.Bones[6].Transform = originalTransforms[6]; myModel.Bones[8].Transform = originalTransforms[8]; float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 1000.0f; Matrix worldMatrix = Matrix.CreateTranslation(new Vector3(10, 0, -12)); // starting position worldMatrix = Matrix.CreateRotationY(MathHelper.PiOver4*3)*worldMatrix; worldMatrix = Matrix.CreateTranslation(0, 0, time)*worldMatrix; //move forward worldMatrix = Matrix.CreateScale(0.001f)*worldMatrix; //scale down a bit worldMatrix = TiltModelAccordingToTerrain(myModel, worldMatrix, 5, 1, 4, 0);//do tilting magic 代码 下面是content pipeline 命名空间下的代码,在每个ModelMesh的Tag属性中保存最低点顶点: namespace ModelVector3Pipeline { [ContentProcessor] public class ModelVector3Processor : ModelProcessor { Public override ModelContent Process(NodeContent input, ContentProcessorContext context) { List lowestVertices = new List(); lowestVertices = FindLowestVectors(input, lowestVertices); ModelContent usualModel = base.Process(input, context); int i = 0; foreach (ModelMeshContent mesh in usualModel.Meshes) mesh.Tag = lowestVertices[i++]; return usualModel; } private List FindLowestVectors(NodeContent node, List lowestVertices) { Vector3? lowestPos = null; MeshContent mesh = node as MeshContent; foreach (NodeContent child in node.Children) lowestVertices = FindLowestVectors(child, lowestVertices); if (mesh != null) foreach (GeometryContent geo in mesh.Geometry) foreach (Vector3 vertexPos in geo.Vertices.Positions) if ((lowestPos == null) || (vertexPos.Y < lowestPos.Value.Y)) lowestPos = vertexPos; lowestVertices.Add(lowestPos.Value); return lowestVertices; } } } 下面的代码可以调整给定世界矩阵让模型很好地匹配地形。你需要传递模型的世界矩阵,模型和对应轮子的ModelMesh的四个索引。 private Matrix TiltModelAccordingToTerrain(Model model, Matrix worldMatrix, int flID, int frID, int blID, int brID) { Vector3 frontLeftOrig = (Vector3)model.Meshes[flID].Tag; Vector3 frontRightOrig = (Vector3)model.Meshes[frID].Tag; Vector3 backLeftOrig = (Vector3)model.Meshes[blID].Tag; Vector3 backRightOrig = (Vector3)model.Meshes[brID].Tag; model.CopyAbsoluteBoneTransformsTo(modelTransforms); Matrix frontLeftMatrix = modelTransforms[model.Meshes[flID].ParentBone.Index]; Matrix frontRightMatrix = modelTransforms[model.Meshes[frID].ParentBone.Index]; Matrix backLeftMatrix = modelTransforms[model.Meshes[blID].ParentBone.Index]; Matrix backRightMatrix = modelTransforms[model.Meshes[brID].ParentBone.Index]; Vector3 frontLeft = Vector3.Transform(frontLeftOrig, frontLeftMatrix * worldMatrix); Vector3 frontRight = Vector3.Transform(frontRightOrig, frontRightMatrix * worldMatrix); Vector3 backLeft = Vector3.Transform(backLeftOrig, backLeftMatrix * worldMatrix); Vector3 backRight = Vector3.Transform(backRightOrig, backRightMatrix * worldMatrix); Vector3 front = (frontLeft + frontRight) / 2.0f; Vector3 back = (backLeft + backRight) / 2.0f; Vector3 backToFront = front - back; float frontTerHeight = terrain.GetExactHeightAt(front.X, -front.Z); float backTerHeight = terrain.GetExactHeightAt(back.X, -back.Z); float fbTerHeightDiff = frontTerHeight - backTerHeight; float fbAngle = (float)Math.Atan2(fbTerHeightDiff, backToFront.Length()); Quaternion fbRot = Quaternion.CreateFromAxisAngle(new Vector3(1, 0, 0), -fbAngle); Vector3 left = (frontLeft + backLeft) / 2.0f; Vector3 right = (frontRight + backRight) / 2.0f; Vector3 rightToLeft = left - right; float leftTerHeight = terrain.GetExactHeightAt(left.X, -left.Z); float rightTerHeight = terrain.GetExactHeightAt(right.X, -right.Z); float lrTerHeightDiff = leftTerHeight - rightTerHeight; float lrAngle = (float)Math.Atan2(lrTerHeightDiff, rightToLeft.Length()); Quaternion lrRot = Quaternion.CreateFromAxisAngle(new Vector3(0, 0, -1), -lrAngle); Quaternion combRot = fbRot * lrRot; Matrix rotatedModelWorld = Matrix.CreateFromQuaternion(combRot) * worldMatrix; Vector3 rotFrontLeft = Vector3.Transform(frontLeftOrig, frontLeftMatrix * rotatedModelWorld); Vector3 rotFrontRight = Vector3.Transform(frontRightOrig, frontRightMatrix * rotatedModelWorld); Vector3 rotBackLeft = Vector3.Transform(backLeftOrig, backLeftMatrix * rotatedModelWorld); Vector3 rotBackRight = Vector3.Transform(backRightOrig, backRightMatrix * rotatedModelWorld); float flTerHeight = terrain.GetExactHeightAt(rotFrontLeft.X, -rotFrontLeft.Z); float frTerHeight = terrain.GetExactHeightAt(rotFrontRight.X, -rotFrontRight.Z); float blTerHeight = terrain.GetExactHeightAt(rotBackLeft.X, -rotBackLeft.Z); float brTerHeight = terrain.GetExactHeightAt(rotBackRight.X, -rotBackRight.Z); float flHeightDiff = rotFrontLeft.Y - flTerHeight; float frHeightDiff = rotFrontRight.Y - frTerHeight; float blHeightDiff = rotBackLeft.Y - blTerHeight; float brHeightDiff = rotBackRight.Y - brTerHeight; float finalHeightDiff = (blHeightDiff + brHeightDiff + flHeightDiff + frHeightDiff) / 4.0f; worldMatrix = rotatedModelWorld * Matrix.CreateTranslation(new Vector3(0, -finalHeightDiff, 0)); return worldMatrix; } 扩展阅读 这个方法要返回正确结果取决于轮子的底部位置是正确的。如果轮子很窄它工作得很好,因为此时轮子底部位置正好在地形上。如果轮子比较宽,那么会有点问题。如果模型处理器获取的是靠内部的底部顶点并进行计算,那么有可能轮子会陷进地面中。如果发生这种情况,要调整Model processor使之在Tag属性中存储轮子的靠外边的底部,或在方法中手动指定一个。

处理模型——通过定义一个自定义的TypeWriter和TypeReader将多个对象存储在Tag属性中

clock 一月 16, 2011 19:44 by author alex
问题 要让模型处理器可以将对象存储在模型中并传递到XNA项目,XNA提供了模型的Tag属性。从教程4-13,4-14和4-15中可以看到Tag属性对于存储一个相同类型的数组是很有用的,例如Vector3的数组或Triangle的数组。但很多情况中,你想传递多个对象,例如同时传递Vector3集合和模型的包围盒。 解决方案 定义一个自定义类,这个类存储所有你想传递的对象。在你的模型处理器中,创建这个类对象,将要传递到XNA项目中的对象存储在这个类对象中。最后在模型的Tag属性中存储这个类对象。 因为你定义了一个自定义类,所以你必须定义一个TypeWriter (见教程4-15)才能让XNA知道如何串行化这个对象,还要定义一个TypeReader,通过这个TypeReader从二进制文件读取这个对象。 工作原理 因为你要扩展内容处理器并传递自定义类对象,所以你要执行教程4-15中的“使用一个自定义类对象扩展内容处理器的步骤清单”中的初始化步骤。在第1步中,我调用了新内容管道项目TagPipeline。在第4步中我调用了处理器ExtendedModelProcessor。 namespace TagPipeline { [ContentProcessor] public class ExtendedModelProcessor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { return base.Process(input, context); } } } 完成初始化后,你就做好了定义一个可以存储你想传递到XNA项目的东西的新类的准备了。本例中,你将传递一个包含模型所有Vector3的数组和全局包围盒。 在内容管道命名空间顶部定义这个新类: public class TagObject { private Vector3[] positions; private BoundingBox boundingBox; public TagObject(Vector3[] positions, BoundingBox boundingBox) { this.positions = positions; this.boundingBox = boundingBox; } public Vector3[] Positions { get { return positions; } } public BoundingBox GlobalBoundingBox { get { return boundingBox; } } } 这个简单的类可以存储一个Vector3数组和包围盒。它的构造函数将这些变量传递到内部变量中,你还定义了两个getter方法,让你可以获取变量的内容。 注意:因为这个变量不包含行为方法,你也可以使用结构数据类型替代类。 然后编写模型处理器代码。它可以获取你想传递的数据:Vector3数组和包围盒。 public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Vector3> vertices = new List<Vector3>(); vertices = AddVerticesToList(input, vertices); BoundingBox bBox = BoundingBox.CreateFromPoints(vertices); TagObject myTagObject = new TagObject(vertices.ToArray(), bBox); usualModel.Tag = myTagObject; return usualModel; } 模型处理器开始的操作已经在前面的教程中做过很多次了。然后,你使用AddVerticesToList方法遍历整个模型结构将所有Vector3添加到一个集合中。 有了这个集合之后,可以使用BoundingBox. CreateFromPoints方法从这个集合生成包围盒。这个方法以Vector3集合为参数,而这个集合具有Ienumerable接口,就好像一个数组或 List。 通过将集合转换到数组,你就拥有了创建TagObject类对象所需的所有东西!最后,将这个类对象存储在模型的Tag属性中。 现在己经完成了这个教程的前半部分,在第二部分,你要编写TypeWriter和TypeReader。 编写TypeWriter和TypeReader 现在如果你运行代码,XNA会报错,这是因为它还不知道如何将TagObject类对象存储到二进制文件,所以需要在内容管道命名空间下添加自定义TypeWriter: [ContentTypeWriter] public class TagObjectTypeWriter : ContentTypeWriter<TagObject> { protected override void Write(ContentWriter output, TagObject value) { output.WriteObject<Vector3[]>(value.Positions); output.WriteObject<BoundingBox>(value.GlobalBoundingBox); } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return typeof(TagObjectTypeReader).AssemblyQualifiedName; } } 如教程4-15中的解释,前两行代码指定这个类是一个可以串行化TagObject对象的ContentTypeWriter。同理,你也要重写两个方法:Write方法指定一个TagObject如何被写入到二进制文件中,GetRuntimeReader方法可以被XNA调用,让程序知道到哪找到对应的TypeReader,可在教程4-15见到更多信息。 默认内容处理器知道如何串行化Vector3数组和包围盒,所以你只需要简单地让XNA为你串行化就可以了。在GetRuntimeReader方法中,你声明在相同的命名空间中编写一个叫做TagObjectReader的对应TypeReader。 注意:如果默认您内容管道不知道如何串行化包围盒你可以自己定义。你可以调整处理TagObjects的TypeWriter,使它将包围盒中的两个Vector3保存到二进制文件中,这样ContentReader可以重新构造这个包围盒。但是,更好的方法是编写一个额外的TypeWriter和TypeReader用来串行化/反串行化一个包围盒对象,如果使用这个方法,后面的ContentWriters会知道如何串行化包围盒对象! 下面编写TypeReader,因为已经在GetRuntimeReader方法中定义了,所以TypeReader类必须被叫做TagObjectTypeReader: public class TagObjectTypeReader : ContentTypeReader<TagObject> { protected override TagObject Read(ContentReader input, TagObject existingInstance) { Vector3[] positions = input.ReadObject<Vector3[]>(); BoundingBox bBox = input.ReadObject<BoundingBox>(); TagObject restoredTagObject = new TagObject(positions, bBox); return restoredTagObject; } } 在程序启动时,每个TagObject类对象都会被串行化为一个二进制文件,而TagObjectTypeReader方法可以重建这些对象。首先你从文件中读取Vector3数组并将它存储在一个变量中;然后对包围盒进行同样的处理。有了这两个对象后,就可以重建TagObject对象并把它传递到XNA程序中。 很简单,但有一点很重要,你必须以写入文件的同样顺序读取这些对象!如果你首先读入的是包围盒,你会基于第一个Vector3数组重建包围盒!幸运的是,XNA team对此进行了保护,如果你颠倒了读取顺序,程序会报错。 注意:如果你的解决方案无法编译,请再看一下教程4-15中的步骤清单。 在XNA项目中访问数据现在运行程序,所有使用ExtendedModelProcessor 的模型都会在Tag属性中包含一个TagObject对象、因为Tag属性可以包含任何东西,所以首先需要将它转换为TagObject对象。现在就可以访问它的属性了: myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; TagObject modelTag = (TagObject)myModel.Tag; BoundingBox modelBBox = modelTag.GlobalBoundingBox; Vector3[] modelVertices = modelTag.Positions; System.Diagnostics.Debugger.Break(); 代码 这个教程中内容管道包含下列对象: ExtendedModelProcessor模型处理器,包含AddVerticesToList辅助类 自定义TagObject类定义 class definition 自定义TypeWriter,可以串行化TagObject类对象 自定义TypeReader,可以从二进制文件读取TagObject类对象 完整代码如下: namespace TagPipeline { public class TagObject { private Vector3[] positions; private BoundingBox boundingBox; public TagObject(Vector3[] positions, BoundingBox boundingBox) { this.positions = positions; this.boundingBox = boundingBox; } public Vector3[] Positions { get { return positions; } } public BoundingBox GlobalBoundingBox { get { return boundingBox; } } } [ContentProcessor] public class ExtendedModelProcessor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Vector3> vertices = new List<Vector3>(); vertices = AddVerticesToList(input, vertices); BoundingBox bBox = BoundingBox.CreateFromPoints(vertices); TagObject myTagObject = new TagObject(vertices.ToArray(), bBox); usualModel.Tag = myTagObject; return usualModel; } private List<Vector3> AddVerticesToList(NodeContent node, List<Vector3> vertList) { MeshContent mesh = node as MeshContent; if (mesh != null) { Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { foreach (int index in geo.Indices) { Vector3 vertex = geo.Vertices.Positions[index]; Vector3 transVertex = Vector3.Transform(vertex, absTransform); vertList.Add(transVertex); } } } foreach (NodeContent child in node.Children) vertList = AddVerticesToList(child, vertList); return vertList; } } [ContentTypeWriter] public class TagObjectTypeWriter : ContentTypeWriter<TagObject> { protected override void Write(ContentWriter output, TagObject value) { output.WriteObject<Vector3[]>(value.Positions); output.WriteObject<BoundingBox>(value.GlobalBoundingBox); } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return typeof(TagObjectTypeReader).AssemblyQualifiedName; } } public class TagObjectTypeReader : ContentTypeReader<TagObject> { protected override TagObject Read(ContentReader input, TagObject existingInstance) { Vector3[] positions = input.ReadObject<Vector3[]>(); BoundingBox bBox = input.ReadObject<BoundingBox>(); TagObject restoredTagObject = new TagObject(positions, bBox); return restoredTagObject; } } }

处理模型——通过定义一个自定义的TypeWriter和TypeReader直接处理顶点位置数据

clock 一月 16, 2011 19:37 by author alex
问题 你想访问模型中每个顶点的位置。但你想要一个三角形对象的数组,每个三角形对象包含三个Vector3向量,而不是想前面的教程中那样将所有Vector3放在一个大数组中。 或者,更普遍的情况,你想将一个自定义内容处理器类对象传递到XNA程序中,但内容管道无法知道应该如何串行化和反串行化这个类对象,所以你必须定义自己的TypeWriter和TypeReader。 解决方案 本教程的代码主要建立在前一个教程的基础上,但这次你将每三个顶点创建一个简单的三角形对象,然后将所有生成的三角形对象添加到一个集合中,并将这个集合存储在模型的Tag属性中。 最主要的区别是你将在自定义内容管道中定义一个Triangle类。默认的XNA内容管道无法将你的自定义类串行化为一个文件或从文件反串行化为一个对象。所以你将自己定义 TypeWriter和TypeReader。 工作原理 这个教程也可以作为编写自定义TypeWriter和TypeReader的教程。看一下图4-20找到 TypeWriter和TypeReader在内容管道中的位置。 图4-20 内容管道中的自定义处理器、TypeWriter和TypeReader 浏览一下教程3-9中的创建自定义内容管道的步骤,将这个类添加到你的新的内容管道项目中: public class Triangle { private Vector3[] points; public Triangle(Vector3 p0, Vector3 p1, Vector3 p2) { points = new Vector3[3]; points[0] = p0; points[1] = p1; points[2] = p2; } public Vector3[] Points { get { return points; } } public Vector3 P0 { get { return points[0]; } } public Vector3 P1 { get { return points[1]; } } public Vector3 P2 { get { return points[2]; } } } 这是个非常简单的类,用来存储三个Vector3。这个类还提供了getter方法,可以一次获取一个Vector3或包含三个Vector3的数组。 对模型的每个三角形来说,你想创建一个自定义类的对象,将这些对象储存在一个数组中,并将这个数组存储在模型的Tag属性中,这样就可以用于XNA的实时代码。 Process类的代码几乎和教程4-13中的一样,但这次你保存的是Triangle对象的集合而不是上一个教程的Vector3的集合。在Triangle类之后添加Processor类: [ContentProcessor] public class ModelTriangleProcessor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Triangle> triangles = new List<Triangle>(); triangles = AddVerticesToList(input, triangles); usualModel.Tag = triangles.ToArray(); return usualModel; } } AddVerticesToList方法会遍历整个模型结构,为每三个Vector3生成一个Triangle对象并将所有的Triangle对象添加到集合中: private List<Triangle> AddVerticesToList(NodeContent node, List<Triangle>triangleList) { MeshContent mesh = node as MeshContent; if (mesh != null) { Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { //generate Triangle objects... } } foreach (NodeContent child in node.Children) triangleList = AddVerticesToList(child, triangleList); return triangleList; } 上面的代码完全来自于教程4-13,下面你将编写生成Triangle对象的代码: int triangles = geo.Indices.Count / 3; for (int currentTriangle = 0; currentTriangle < triangles; currentTriangle++) { int index0 = geo.Indices[currentTriangle * 3 + 0]; int index1 = geo.Indices[currentTriangle * 3 + 1]; int index2 = geo.Indices[currentTriangle * 3 + 2]; Vector3 v0 = geo.Vertices.Positions[index0]; Vector3 v1 = geo.Vertices.Positions[index1]; Vector3 v2 = geo.Vertices.Positions[index2]; Vector3 transv0 = Vector3.Transform(v0, absTransform); Vector3 transv1 = Vector3.Transform(v1, absTransform); Vector3 transv2 = Vector3.Transform(v2, absTransform); Triangle newTriangle = new Triangle(transv0, transv1, transv2); triangleList.Add(newTriangle); } 上述代码很短,但简单的代码看起来更难。 模型是根据索引缓冲中的数据绘制的,每个索引对应顶点缓冲中的一个顶点(可见教程5-3学习更多索引的知识)。首先你需要知道模型中三角形的总数,它等于索引数除以3。 然后,对于每个三角形,你首先找到索引。每个三角形是由三个连续的索引定义的,所以首先你要将这些索引存储在变量中。一旦你有了这些索引值,就可以获取对应的顶点,并通过节点的绝对变换矩阵变换这些顶点(见教程4-9),这样可以使顶点位置变成相对于模型初始位置而不是相对于ModelMesh的初始位置。最后,你基于三个顶点创建一个新的Triangle对象并将这个对象存储在集合中。 在Process方法的最后,这个集合被转换为一个Triangle对象的数组并将这个数组存储在模型的Tag属性中。 这就是上一个教程结束时所做的:你已经将额外的数据存储在了模型的Tag属性中了。但是当你导入模型,选则这个Processor处理这个模型并试图编译时会发生错误: Unsupported type. Cannot find a ContentTypeWriter implementation for ModelTriaPipeline.Triangle 编写自定义内容TypeWriter 之所以会发生这个错误只因为内容管道不知道如何将Triangle对象串行化为一个二进制文件!所以,你必须编写一个简单的TypeWriter处理Triangle对象如何被串行化。当在处理器中生成的一个ModelContent对象处理到Triangle对象时,XNA就会调用这个TypeWriter将 Triangle保存为二进制文件。 在内容管道项目中添加这个新类,这个类需要放置在Processor类的外面: [ContentTypeWriter] public class TriangleTypeWriter : ContentTypeWriter<Triangle> { protected override void Write(ContentWriter output, Triangle value) { output.WriteObject<Vector3>(value.P0); output.WriteObject<Vector3>(value.P1); output.WriteObject<Vector3>(value.P2); } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return typeof(TriangleTypeReader).AssemblyQualifiedName; } } 首先两行代码表示你将定义一个知道如何串行化Triangle 类对象的ContentTypeWriter。 首先你需要重写Write方法,它以你要保存的每个Triangle对象作为参数。这个数组以值参数的形式传递给Write方法。Output变量包含一个ContentWriter对象让你可以保存到二进制文件。 当对一个确定对象编写TypeWriter时,你首先需要考虑存储什么东西,让你可以在加载二进制文件时重建这些对象。然后,你需要将这个对象分解成更多的简单对象,直到内容管道知道如何串行化这些简单对象。 对Triangle来说,存储三个Vector3就可以让你可以重建Triangle了。你很幸运,,因为内容管道知道如何串行化Vector3,所以Write方法只需简单地将Triangle分解成三个Vector3并进行串行化。 技巧:你可以遍历output的不同重载方法,Write方法可以知道哪种数据类型可以被默认内容管道串行化。 注意:你也可以使用output. Write(value. P0); 这是因为Vector3类型是默认被支持的。但是,上述代码中的output. WriteObject方法更普遍,因为这个方法允许写入自定义TypeWriter中的自定义类对象。 当开始XNA程序时,二进制文件还需要被反串行化。默认内容管道也不知道如何反串行化Triangle对象,所以你还要自定义一个TypeReader。 要让XNA程序找到对应的自定义TypeReader,你的TypeWriter需要在GetRuntimeReader方法中指定TypeReader的位置。 这个GetRuntimeReader方法(在前面的代码中也定义了)只是简单地返回一个字符串,表示在哪可以找到用于Triangle对象的TypeReader。 如果运行代码,会产生一个错误:ModelTriaPipeline. TriangleReader class cannot be found,这可能是因为你还没有编写这个类。所以在ModelTriaPipeline命名空间中添加最后一个类: public class TriangleTypeReader : ContentTypeReader<Triangle> { protected override Triangle Read(ContentReader input, Triangle existingInstance) { Vector3 p0 = input.ReadObject<Vector3>(); Vector3 p1 = input.ReadObject<Vector3>(); Vector3 p2 = input.ReadObject<Vector3>(); Triangle newTriangle = new Triangle(p0, p1, p2); return newTriangle; } } 你从ContentTypeReader类继承,让你的自定义TypeReader可以反串行化Triangle类对象。 注意:确保你的自定义reader的名称与你在writer中GetRuntimeReader方法中指定的名称是一致的!否则,XNA仍然无法找到对应的TypeReader。 在XNA项目运行开始时,在二进制文件中每次碰到Triangle对象都会调用这个类的Read方法。你在每个三角形中存储了三个Vector3,所以在TypeReader中,你只需简单地从二进制文件中读出三个Vector3,然后基于这三个Vector3创建三角形对象,最后返回三角形。 在内容管道项目中添加引用 编写完TypeReader后,就做好了运行程序的准备。但是虽然项目可以正确生成,但在运行时仍会遇到一个错误: Cannot find ContentTypeReader ModelTriaPipeline.Triangle, ModelTriaPipeline, Version=1.0.0.0, Culture=neutral 发生这个错误的原因是XNA项目无法访问TypeReader 中的ModelTriaPipeline命名空间。要解决这个问题,你需要在内容管道中添加一个引用。打开XNA主项目,选择Project→Add Reference,在弹出的对话框中选择Projects选项卡,可以看到列表中的内容管道,如图4-21所示,选择并点击OK。 图4-21 添加对自定义内容管道项目的引用 注意:如果你的内容管道不在列表中,请确保你已经生成了内容管道项目(可见教程4-9中步骤列表的第6步)。 这里你需要在XNA项目的Content 目录和XNA项目本身中添加内容管道的引用,如图4-22所示。第一个引用让你可以为模型选择自定义处理器,第二个引用让XNA项目可以实时调用TypeReader。 图4-22 需要两个自定义内容管道的引用 当你选择ModelTriangleProcessor处理模型并运行程序后,你的模型就在Tag属性中包含了Triangle对象的数组。 添加内容管道的命名空间 在XNA项目中,当你想访问存储在Tag属性中的Triangle对象时,你总有将Tag属性中的内容转换为你想要的类型,本例中是Triangle [] (三角形的数组)。但是,因为Triangle类是定义在另一个命名空间中的,你需要在Triangle名称之前加上它的命名空间: ModelTriaPipeline.Triangle[] modelTriangles = (ModelTriaPipeline.Triangle[])myModel.Tag; 这看起来不好。如果你使用using将内容管道命名空间(本例中是ModelTriaPipeline)添加到XNA项目中: using ModelTriaPipeline; 那么自定义内容管道的所有类都能被XNA项目知道,可以让代码变得更短: Triangle[] modelTriangles = (Triangle[])myModel.Tag; 注意:存储在Triangle中的位置信息是相对于模型的初始位置的。当对模型的一部分施加动画时如何使用这些位置信息,可见教程4-14。 使用自定义类对象扩展处理器的步骤清单 这里你可以找到让包含自定义类的内容管道运行的步骤。与教程4-9做的一样,我会总结一个清单,你可以将它作为参考。教程4-9中的初始化列表扩展成两个部分,这在前面已经讨论过了。 1.在解决方案中添加一个内容管道项目 2.在新项目中,添加Microsoft. XNA. Framework. Content. Pipeline的引用。 3.在using代码块添加Pipeline命名空间。 4.表明你将扩展哪个部分(这部分的方法需要重写)。 5.编译新内容管道项目。 6.在主项目中添加新创建的引用 7.选择新创建的处理器处理一个素材。 8.设置项目依赖项。 9.在XNA主程序中,添加自定义内容管道的引用。 10.在XNA主程序中的using代码块中添加内容管道命名空间。 初始化后进行以下步骤: 1.定义自定义类 2.编写第4步中的代码 3.对每个处理器中使用的自定义类,创建一个TypeWriter。 4.对每个处理器中使用的自定义类,创建一个TypeReader。 注意:内容导入器,处理器和typewriters只在编译项目时才会被invoke。所以,这些类无法部署在Xbox 360平台上,因为TypeReader类和你的自定义类无法被Game类找到。要解决这个问题,可将这两个类移至主XNA项目中,并添加引用。可见本教程的示例代码(译者注:是指XboxPipeline文件夹中的示例)。 代码 下面是自定义内容管道命名空间下完整代码。这个命名空间包括一个带有辅助方法的自定义模型处理器,一个自定义类,一个TypeReader (将自定义类存储为一个二进制文件),一个TypeReader (反串行化自定义类对象)。 namespace TrianglePipeline { public class Triangle { private Vector3[] points; public Triangle(Vector3 p0, Vector3 p1, Vector3 p2) { points = new Vector3[3]; points[0] = p0; points[1] = p1; points[2] = p2; } public Vector3[] Points { get { return points; } } public Vector3 P0 { get { return points[0]; } } public Vector3 P1 { get { return points[1]; } } public Vector3 P2 { get { return points[2]; } } } [ContentProcessor] public class ModelTriangleProcessor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Triangle> triangles = new List<Triangle>(); triangles = AddVerticesToList(input, triangles); usualModel.Tag = triangles.ToArray(); return usualModel; } private List<Triangle> AddVerticesToList(NodeContent node, List<Triangle> triangleList) { MeshContent mesh = node as MeshContent; if (mesh != null) { Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { int triangles = geo.Indices.Count / 3; for (int currentTriangle = 0; currentTriangle < triangles; currentTriangle++) { int index0 = geo.Indices[currentTriangle *3+ 0]; int index1 = geo.Indices[currentTriangle *3+ 1]; int index2 = geo.Indices[currentTriangle *3+ 2]; Vector3 v0 = geo.Vertices.Positions[index0]; Vector3 v1 = geo.Vertices.Positions[index1]; Vector3 v2 = geo.Vertices.Positions[index2]; Vector3 transv0 = Vector3.Transform(v0, absTransform); Vector3 transv1 = Vector3.Transform(v1, absTransform); Vector3 transv2 = Vector3.Transform(v2, absTransform); Triangle newTriangle = new Triangle(transv0, transv1, transv2); triangleList.Add(newTriangle); } } } foreach (NodeContent child in node.Children) triangleList = AddVerticesToList(child, triangleList); return triangleList; } } [ContentTypeWriter] public class TriangleTypeWriter : ContentTypeWriter<Triangle> { protected override void Write(ContentWriter output, Triangle value) { output.WriteObject<Vector3>(value.P0); output.WriteObject<Vector3>(value.P1); output.WriteObject<Vector3>(value.P2); } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return typeof(TriangleTypeReader).AssemblyQualifiedName; } } public class TriangleTypeReader : ContentTypeReader<Triangle> { protected override Triangle Read(ContentReader input, Triangle existingInstance) { Vector3 p0 = input.ReadObject<Vector3>(); Vector3 p1 = input.ReadObject<Vector3>(); Vector3 p2 = input.ReadObject<Vector3>(); Triangle newTriangle = new Triangle(p0, p1, p2); return newTriangle; } } } 在主XNA项目中,加载模型,访问它的Triangles,,并放置一个断点让你可以检查modelTriangles数组的内容: myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; Triangle[] modelTriangles = (Triangle[])myModel.Tag; System.Diagnostics.Debugger.Break();

处理模型——通过扩展模型处理器直接处理每个ModelMesh的顶点位置数据

clock 一月 16, 2011 19:33 by author alex
问题 前面的教程中让你可以访问模型的所有顶点的相对于模型初始位置的位置。但是,如果你想对模型的一部分施加动画,例如,旋转一个人的手臂,那么你还想变换手臂,手,手指的位置。使用上一个教程的结果是不可能的,因为Vector3集合不包含Vector3属于模型哪个部分的信息,只包含相对于模型初始位置的信息。 解决方案 模型的独立变换部分是存储在ModelMeshe中的。ModelMesh有一个Tag属性,你可以在Tag中存储有用的信息。在本教程中,你将在Tag属性中存储ModelMesh的所有顶点的位置信息。 你将存储相对于ModelMesh初始位置的顶点位置信息(而不是像前一个教程中的相对于模型的初始位置)。当XNA程序更新Bone矩阵并计算绝对矩阵时(见教程4-9),你可以使用与渲染模型相同的绝对Bone矩阵变换位置,获得相对于模型初始位置的顶点位置。相对于前一个教程,这个方法最大的好处是,这次你将处理独立部分的当前变换。工作原理这个教程在前一个教程的基础上再加以扩展。你仍需要遍历模型的整个结构,但这次,无需将所有顶点存储在一个大数组中,你将为每个节点生成独立的数组。 在遍历了整个模型并存储了数组后,你使用默认模型处理器从节点产生一个ModelContent。最后,你遍历ModelContent中的所有ModelMeshContents,然后在对应的ModelMeshContent的Tag属性中存储每个数组。 注意:ModelContent对象是内容处理器的输出,然后它被写入一个二进制文件。当程序运行时,这个文件从磁盘被加载,这个对象被转换为一个模型。 以下是处理器的开始部分的代码: public override ModelContent Process(NodeContent input,ContentProcessorContext context) { List<Vector3[]> modelVertices = new List<Vector3[]>(); modelVertices = AddModelMeshVertexArrayToList (input, modelVertices); } 你创建了一个集合存储独立的Vector3数组,然后,将这个集合和根节点传递到AddModelMeshVertexArrayToList方法。 这个方法首先调用自己,传递它的子节点,这样可以将子节点的数组添加到集合中。 当所有子节点的数组添加到集合之后,AddModelMeshVertexArrayToList方法检查当前节点是否包含顶点。如果包含,则创建一个包含所有顶点位置的Vector3数组并将它添加到集合中。 private List<Vector3[]> AddModelMeshVertexArrayToList(NodeContent node, List<Vector3[]>modelVertices) { foreach (NodeContent child in node.Children) modelVertices = AddModelMeshVertexArrayToList(child, modelVertices); MeshContent mesh = node as MeshContent; if (mesh != null) { List<Vector3> nodeVertices = new List<Vector3>(); foreach (GeometryContent geo in mesh.Geometry) { foreach (int index in geo.Indices) { Vector3 vertex = geo.Vertices.Positions[index]; nodeVertices.Add(vertex); } } modelVertices.Add(nodeVertices.ToArray()); } return modelVertices; } 注意:与上一个教程相反,这次位置不进行变换。这是因为你想存储相对于ModelMesh初始位置的顶点位置,而不是相对于模型初始位置。你可以在以后通过ModelMesh的绝对矩阵转换它们。 注意:AddModelMeshVertexArrayToList方法与上一个教程的AddVerticesToList方法最主要的不同之处在于:你在添加当前节点的数据之前首先将子节点的数据添加到集合中。在前一个教程中在添加子节点的数据之前首先添加当前节点的数据,这样看起来可能更加直观。但是,以ModelMesh保存到模型的相同顺序将ModelMesh的数组保存是很重要的,这样你可以很容易地在Process方法的最后加载数组。 最后,将下列代码添加到Process方法的最后: ModelContent usualModel = base.Process(input, context); int i = 0; foreach(ModelMeshContent mesh in usualModel.Meshes) mesh.Tag = modelVertices[i++]; return usualModel; 有了数组后,你就可以使用默认模型处理器从节点创建一个默认的ModelContent对象。在每个ModelMeshes中你存储了正确的包含Vector3的数组。 但是,别忘了有些ModelMeshe是由超过一个ModelMeshPart构成的,每个ModelMeshPart 有自己的NodeContent,对于每个ModelMeshPart,AddModelMeshVertexArrayToList方法会将Vector3数组添加到集合。所以要让代码完整,你需要用以下代码替代前面的代码: ModelContent usualModel = base.Process(input, context); int i = 0; foreach (ModelMeshContent mesh in usualModel.Meshes) { List<Vector3> modelMeshVertices = new List<Vector3>(); foreach (ModelMeshPartContent part in mesh.MeshParts) { modelMeshVertices.AddRange(modelVertices[i++]); } mesh.Tag = modelMeshVertices.ToArray(); } return usualModel; 对于属于一个ModelMesh的所有ModelMeshPart,上面的代码将所有Vector3添加到一个集合,这个集合被转换到一个数组并存储在ModelMesh的Tag属性中。 注意:因为一个ModelMesh的所有ModelMeshPart使用相同的绝对变换矩阵,你可以一次性地处理所有Vector3仍能保证正确处理动画(见教程4-9)。你可能还想在ModelMeshPart 的Tag属性中存储每个ModelMeshPart的顶点,例如,创建一个更好的包围球处理快速的碰撞检测。 最后返回ModelContent,做好了串行化为一个二进制文件的准备。 当你使用自定义模型处理器导入一个模型时,你可以在模型的每个ModelMesh的Tag属性中找到一个数组,这个数组包含了ModelMesh的每个三角形的三个Vector3分量。在 LoadContent方法中加载模型: myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; 现在每个模型的ModelMesh的Tag属性中包含了Vector3的集合,你可以通过编译器访问储存在数组中的数据: Vector3[] modelVertices = (Vector3[])myModel.Meshes[0].Tag; System.Diagnostics.Debugger.Break(); 最后一行代码放置了一个断点,你可以观察Vector3的内容。 代码 下面是自定义内容管道: namespace Vector3Pipeline { [ContentProcessor] public class ModelVector3Processor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { List<Vector3[]> modelVertices = new List<Vector3[]>(); modelVertices = AddModelMeshVertexArrayToList(input, modelVertices); ModelContent usualModel = base.Process(input, context); int i = 0; foreach (ModelMeshContent mesh in usualModel.Meshes) { List<Vector3> modelMeshVertices = new List<Vector3>(); foreach (ModelMeshPartContent part in mesh.MeshParts) { modelMeshVertices.AddRange(modelVertices[i++]); } mesh.Tag = modelMeshVertices.ToArray(); } return usualModel; } private List<Vector3[]> AddModelMeshVertexArrayToList(NodeContent node, List<Vector3[]> modelVertices) { foreach (NodeContent child in node.Children) modelVertices = AddModelMeshVertexArrayToList(child, modelVertices); MeshContent mesh = node as MeshContent; if (mesh != null) { List<Vector3> nodeVertices = new List<Vector3>(); foreach (GeometryContent geo in mesh.Geometry) { foreach (int index in geo.Indices) { Vector3 vertex = geo.Vertices.Positions[index]; nodeVertices.Add(vertex); } } modelVertices.Add(nodeVertices.ToArray()); } return modelVertices; } } }

处理模型——通过扩展模型处理器直接访问顶点位置数据

clock 一月 16, 2011 19:30 by author alex
问题 在XNA项目中,你想实时访问每个顶点的位置,如果你需要非常精确的碰撞检测或想找到模型上的特定点时,这一步非常有用。 虽然你可以使用常规代码访问模型的VertexBuffer,但因为顶点数据需要从显卡传递到系统内存,所以这种方法需要笨拙的代码并会拖慢程序。 解决方案 通过提取你想要的数据并把它们存储在模型的Tag属性中扩展默认模型处理器。本教程中,你将编写一个自定义模型处理器创建一个数组,这个数组包含模型中每个三角形的三个Vector3,并将这个数组存储在模型的Tag属性中。因为这个处理器只是默认模型处理器的扩展,所以你可以使用默认的模型导入器和TypeWriter,如图4-18所示。 图4-18 扩展内容管道中的模型处理器 工作原理 首先添加一个内容管道项目,具体步骤可见教程4-15,并起一个名称。 然后从默认ModelProcessor 继承定义你的处理器。因为你想改变模型对象(你需要在Tag属性中存储某些东西),所以需要重写Process方法: [ContentProcessor] public class ModelVector3Processor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Vector3> vertices = new List<Vector3>(); vertices = AddVerticesToList(input, vertices); usualModel.Tag = vertices.ToArray(); return usualModel; } } 这个处理器首先将input传递到基类的Process方法中,这会返回一个默认的ModelContent对象,做好被串行化成一个二进制文件的准备。 但是,在你返回这个对象前,你需要添加一点自定义的东西。所以首先创建一个集合用来存储顶点的位置。在下一段落,你将创建AddVerticesTo List方法,这个方法会遍历整个模型结构并将所有顶点添加到集合中。返回这个集合,并转换为一个数组,然后将这个数组存储在ModelContent对象的Tag属性中。 你的自定义处理的“input”变量包含指向模型根节点(root node)的链接,接下来是几何和材质数据,这个根节点包含指向其子节点的链接等,直到遍历整个模型。如果你想创建一个方法将模型中的所有顶点都保存在一个集合中,那么首先需要保存根节点中的顶点,然后递归调用保存这个根节点的子节点,直至模型中所有顶点都被保存到集合中。 以上就是AddVerticesToList方法所做的工作,将这个方法添加到ModelVector3Processor 类中: private List<Vector3> AddVerticesToList(NodeContent node, List<Vector3> vertList) { MeshContent mesh = node as MeshContent; if (mesh != null) { //This node contains vertices, add them to the list } foreach (NodeContent child in node.Children) vertList = AddVerticesToList(child, vertList); return vertList; } 一个节点不一定包含顶点,有时一个节点只是用来作为二个或二个以上子节点的父节点而不包含顶点数据。这意味着你需要首先判断一个节点是否是一个MeshContent对象,如果是说明它包含顶点。这里的“as”关键字进行了这种检查,如果不成功则说明对象(这里是mesh)为null。所以当程序运行到if代码块时,你可以判断节点一定包含几何数据,就需要把这些顶点存储在集合中。 注意:一个不包含顶点信息的节点只会作为模型对象的一个Bone,而不包含几何信息的节点看作是Bone和链接到Bone的一个ModelMesh(实际上是一个ModelMeshPart而不是ModelMesh)。 在当前节点的所有顶点都存储了之后,你需要调用同样的方法处理子节点,通过这种方法代码可以遍历整个模型的结构。下列代码获取节点中所有位置信息并将它添加到集合中,这个代码要放在if语句块中: Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { foreach (int index in geo.Indices) { Vector3 vertex = geo.Vertices.Positions[index]; Vector3 transVertex = Vector3.Transform(vertex, absTransform); vertList.Add(transVertex); } } 一个节点中的所有顶点的位置被它的父节点引用,要获取相对于根节点的位置(=下一个模型的初始节点),你需要通过这个节点的绝对变换矩阵将位置进行转换(可见教程4-9的相关知识)。每个节点都在它的AbsoluteTransform属性中存储了绝对变换矩阵。 接下来你需要找到实际的位置数据。模型中的所有三角形是从IndexBuffer绘制的,而这个IndexBuffer链接到VertexBuffer (见教程5-3)。每个索引代表一个在VertexBuffer 中的顶点,因此对一个三角形你需要找到三个索引。 注意:模型中的所有三角形都是以索引化的三角形列表的形式绘制的(见教程5-3和 5-4)。 你遍历了节点的索引,从VertexBuffer中查询对应的顶点,将这个顶点的位置进行转换,这样这个位置就是相对于模型的初始位置而不是相对于ModelMesh的初始位置,然后将结果添加到集合中。 在将所有位置添加到集合中之后,这个集合返回到父节点,做好传递到下一个节点的准备,直到将所有节点都添加到集合中。之后,最终的集合返回到Process方法中,将这个集合转换为数组并将这个数组存储在模型的Tag属性中。 好了!当你使用自定义模型处理器将一个模型导入到XNA后,你会发现模型的Tag属性中有一个包含Vector3的数组: myModel = content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; Vector3[] modelVertices = (Vector3[])myModel.Tag; System.Diagnostics.Debugger.Break(); 因为Tag属性可以包含任何东西,你需要使用(Vector3[])指定类型。最后一行代码让程序暂停,这样你可以检查modelVertices的内容,如图4-19所示。 图4-19 模型的所有Vector3在实时都可以访问 代码 你的自定义管道项目包含了自定义模型处理器,这个处理器从默认的ModelProcessor继承但重写了Process方法,可以将所有顶点存储在一个Vector3数组中: namespace Vector3Pipeline { [ContentProcessor] public class ModelVector3Processor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Vector3> vertices = new List<Vector3>(); vertices = AddVerticesToList(input, vertices); usualModel.Tag = vertices.ToArray(); return usualModel; } private List<Vector3> AddVerticesToList(NodeContent node, List<Vector3> vertList) { MeshContent mesh = node as MeshContent; if (mesh != null) { Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { foreach (int index in geo.Indices) { Vector3 vertex = geo.Vertices.Positions[index]; Vector3 transVertex = Vector3.Transform(vertex, absTransform); vertList.Add(transVertex); } } } foreach (NodeContent child in node.Children) vertList = AddVerticesToList(child, vertList); return vertList; } } }

处理模型——扩展模型内容导入器加载自定义Effects

clock 一月 16, 2011 19:26 by author alex
问题 你想使用一个自定义内容处理器改变模型的effect而不是实时处理。通过这种方式,模型可以正确地加载到XNA项目中,而无需在实时保存原始effect中的所有纹理和其他信息。 注意:如果你想学习如何扩展默认模型处理器,最好先看下一个教程。 解决方案 扩展默认模型处理器并重写ConvertMaterial方法,这个方法被模型中的每个MaterialContent调用,把这个方法重写将所有的MaterialContent信息传递到自定义材质处理器。 在这个自定义材质处理器中,你将创建一个空的材质对象,然后将自定义的effect链接到这个对象。你也可以将原始MaterialContent的纹理复制到这个新对象,这个就可以设置effect的所有HLSL变量了。 工作原理 因为这个教程需要扩展默认内容处理器,所以请确保已经读过了教程3-9中内容管道的知识。首先建立一个新的内容管道项目,具体解释可见教程3-9中的“扩展一个已存在的内容处理器”一节。 本教程中,你将扩展默认模型处理器,如图4-16所示。当默认模型导入器从磁盘读取一个模型并创建一个NodeContent对象时,它保存了MaterialContent对象中的effect。你的模型处理器只改变这些MaterialContent对象并存储在NodeContent中,NodeContent对象的其他部分保持不变。 图4-16 内容管道中的自定义模型处理器 注意:如果你比较一下图4-15和图3-6,你会发现你也是扩展了处理器。但是,因为这个处理器处理的是模型而不是纹理,所以它的输入和输出是不同的。 通过扩展Process方法,你可以在模型对象加载到XNA项目之前完全控制它的内容。在本教程中,因为effect是存储在材质信息中的,所以你想改变处理模型中材质的方式。在自定义模型处理器类中,你无需重写Process方法,但要重写ConvertMaterial方法,每次当模型加载材质时默认处理器都会调用这个方法。 所以用以下代码替换Process方法: [ContentProcessor] public class ModelCustomEffectProcessor : ModelProcessor { protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context) { return context.Convert<MaterialContent, MaterialContent>(material, "MaterialCustomEffectProcessor"); } } 以上代码创建一个ModelCustomEffectProcessor类。因为你只重写了ConvertMaterial方法,所以这个处理器会使用与默认模型处理器同样的方式处理几何数据。在ConvertMaterial 方法中,你指定材质需要被MaterialCustomEffectProcessor类处理。 在ModelCustomEffectProcessor类之后,添加这个MaterialCustomEffectProcessor类: [ContentProcessor] public class MaterialCustomEffectProcessor : MaterialProcessor { public override MaterialContent Process(MaterialContent input, ContentProcessorContext context) { return base.Process(input, context); } } 从参数可以看出这个处理器处理的是材质。通过从材质处理器继承,你以一个MaterialContent作为输入,然后进行处理,返回更新过的MaterialContent对象。这次,因为你想改变处理MaterialContent对象的方式,所以需要重写MaterialCustomEffectProcessor的Process方法。 注意:在ModelCustomEffectProcessor类中,你重写了NodeObject 中的所有MaterialContent对象的处理方式。与之类似,因为纹理和effect也是存储在MaterialContent 对象中的,你也可以通过重写MaterialCustomEffectProcessor 类中的BuildEffect或BuildTexture方法改变EffectContent和TextureContent对象的处理方式。 现在Process方法只是调用了它的基类MaterialProcessor。当然,你需要改变这个方法让你可以改变MaterialContent的处理过程。 现在,将Process方法的内容改为以下代码: EffectMaterialContent myMaterial = new EffectMaterialContent(); string map = Path.GetDirectoryName(input.Identity.SourceFilename); string effectFile = Path.Combine(map, "colorchannels.fx"); myMaterial.Effect = new ExternalReference<EffectContent>(effectFile); 上述代码会创建一个空的EffectMaterialContent对象并存储一个指向自定义effect文件的链接。首先找到模型存储的文件夹并附加上effect文件的名称,当编译器运行到这行代码时,会将这个effect添加到素材列表中,这样可以自动加载默认EffectProcessor使用的effect,但你也确保effect文件存储在与模型文件相同的目录中。 大多数模型还包含纹理,你需要将原始MaterialContent对象转换成新的。本例中使用的effect从一个叫做xTexture的纹理中采样颜色,让你可以单独设置红绿篮颜色通道的强度。大多数模型每个effect只有一张纹理,但有些有多张纹理(例如,一个凹凸映射,可见教程5-15)。因为没有和原始effect的纹理序号联系,所以这个代码只能获取xTexture 变量的最后一个纹理,但代码也展示了如何变量多个纹理的方法。 你可以访问原始effect的纹理集合并将这些纹理变量绑定到自定义effect的HLSL纹理变量上。 if (input.Textures != null) foreach (string key in input.Textures.Keys) myMaterial.Textures.Add("xTexture", input.Textures[key]); 如果你想在加载模型时调整某些HLSL参数,下面是方法: myMaterial.OpaqueData.Add("xRedIntensity", 1.1f); myMaterial.OpaqueData.Add("xGreenIntensity", 0.8f); myMaterial.OpaqueData.Add("xBlueIntensity", 0.8f); 注意:在HLSL effect文件中,每个像素的红、绿、篮颜色通道需要乘以这个变量。上面的代码让红色比其他颜色强度更大,模拟出太阳照射的效果。这在简单的post-processing effect也是很有用的! 最后需要返回新建的MaterialContent。你可以直接将它返回,但最好将这个对象传递到默认材质处理器,这样做可以保证错误(例如你没有指定需要的域)会被修正。你可以通过调用base. Process类或使用下列代码做到这点,两者效果一样。 return context.Convert<MaterialContent, MaterialContent>(myMaterial, "MaterialProcessor"); 这行代码搜索叫做MaterialProcessor的处理器并将一个MaterialContent对象作为输入参数并生成另一个MaterialContent对象,这个新创建的Material对象被返回并存储在模型中。 注意:比较一下这行代码和ConvertMaterial方法中的最后一行代码(译者注:即return context.Convert<MaterialContent, MaterialContent>(material, "MaterialCustomEffectProcessor"))。 使用自定义Effect 将模型导入到XNA项目中,然后选择新的ModelCustomEffectProcessor,如图4-17所示 (不要选择MaterialCustomEffectProcessor,因为它也在列表中)。 图4-17 选择自定义模型处理器 在LoadContent method方法中加载这个模型: myModel = content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; 现在你的模型已经包含了指定的effect!当绘制模型时,你可以使用清晰得多的代码设置参数: myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (Effect effect in mesh.Effects) { effect.Parameters["xWorld"]. SetValue(modelTransforms[mesh.ParentBone.Index]); effect.Parameters["xView"].SetValue(fpsCam.GetViewMatrix()); effect.Parameters["xProjection"].SetValue(fpsCam.GetProjectionMatrix()); effect.Parameters["xRedIntensity"].SetValue(1.2f); } mesh.Draw(); } 这个方法比教程4-7更加清晰。 代码 首先你要让MaterialCustomEffectProcessor处理模型中的每个MaterialContent对象,当然,你还要定义这个自定义材质处理器。 namespace ModelCustomEffectPipeline { [ContentProcessor] public class ModelCustomEffectProcessor : ModelProcessor { protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context) { return context.Convert<MaterialContent, MaterialContent>(material, "MaterialCustomEffectProcessor"); } } [ContentProcessor] public class MaterialCustomEffectProcessor : MaterialProcessor { public override MaterialContent Process(MaterialContent input, ContentProcessorContext context) { EffectMaterialContent myMaterial = new EffectMaterialContent(); string map = Path.GetDirectoryName(input.Identity.SourceFilename); string effectFile = Path.Combine(map, "colorchannels.fx"); myMaterial.Effect = new ExternalReference<EffectContent>(effectFile); if (input.Textures != null) foreach (string key in input.Textures.Keys) myMaterial.Textures.Add("xTexture", input.Textures[key]); myMaterial.OpaqueData.Add("xRedIntensity", 1.1f); myMaterial.OpaqueData.Add("xGreenIntensity", 0.8f); myMaterial.OpaqueData.Add("xBlueIntensity", 0.8f); return context.Convert<MaterialContent, MaterialContent>(myMaterial,"MaterialProcessor"); } } } 接下来,将模型导入到XNA项目中,选择自定义模型处理器处理这个模型并加载: protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); cCross = new CoordCross(device); myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; } 当执行这一步时,模型会加载自定义effect,让你可以在绘制前设置参数: protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); cCross.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix); //draw model Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (Effect effect in mesh.Effects) { effect.Parameters["xWorld"]. SetValue(modelTransforms[mesh.ParentBone.Index] * worldMatrix); effect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); effect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); effect.Parameters["xRedIntensity"].SetValue(1.2f); } mesh.Draw(); } base.Draw(gameTime); }

处理模型——对小而快的对象使用Ray-Traced进行碰撞检测

clock 一月 15, 2011 10:52 by author alex
问题 大部分的碰撞检测方法只在两个物体发生物理碰撞时才检测。但是,如果有一个小物体快速地穿过另一个物体,你的程序的更新速度就有可能跟不上而无法检测到碰撞。 举一个具体的例子,比如一枚子弹打穿一个瓶子。子弹以5000km/h 的速度射向一个瓶子,而瓶子的宽度只有15cm。XNA程序每秒更新60次,所以每次更新子弹会前进23米的距离。这样的话,在调用Update方法时几乎没有可能检测到子弹和瓶子的碰撞,即使上一帧子弹的确穿过了瓶子。 解决方案 你可以在子弹的上一个位置和当前位置之间创建一个Ray。然后通过调用Ray或包围球的Intersect方法检测Ray是否和包围球发生碰撞。如果发生碰撞,这个方法返回碰撞点与Ray的终点间的距离(你可以使用子弹的前一个位置)。 你可以使用这个距离检测在子弹的前一个位置和当前位置之间是否真的发生了碰撞。 工作原理 这个方法需要储存在模型Tag属性中的全局包围球,如教程4-5所示: myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "tank", Content); 你将创建一个方法以模型,模型的世界矩阵,快速物体的前一个位置和当前位置为参数。 private bool RayCollision(Model model, Matrix world, Vector3 lastPosition, Vector3 currentPosition) { BoundingSphere modelSpere = (BoundingSphere)model.Tag; BoundingSphere transSphere = TransformBoundingSphere(modelSpere, world); } 首先需要将模型的包围球移动到模型在3D空间中的当前位置,这一步可以通过使用模型的世界矩阵转换包围球做到(在前一个教程中已经解释过了)。 注意:如果你还想事先缩放模型,那么还要把缩放矩阵也组合到世界矩阵中去,这样可以让前面的代码正确地缩放包围球。 接下来找到快速物体的运动方向和自上一帧以来运动的距离: Vector3 direction = currentPosition - lastPosition; float distanceCovered = direction.Length(); direction.Normalize(); 你可以将B减去A获得A至B的方向,调用这个方向的Length 方法获得两者的距离。对于方向来说,通常需要将它归一化使它的长度变为1。因此,请在存储了长度之后再将它归一化,否则距离将一直为1。 现在获得了Ray的一个点和它的方向就可以创建Ray了。你将使用这个Ray的Intersect方法检测与慢模型的包围球之间的碰撞。 Ray ray = new Ray(lastPosition, direction); 现在有了子弹的Ray,你就做好了完成该方法的准备: bool collision = false; float? intersection = ray.Intersects(transSphere); if (intersection != null) if (intersection <= distanceCovered) collision = true; return collision; 首先定义一个collision变量,除非发生碰撞这个变量将保持为false。在方法的最后将返回这个变量。 调用Ray的Intersect方法并传递到模型的变换过的包围球。 Intersection方法有点特别。如果发生碰撞,这个方法返回碰撞点和用来定义Ray的点之间的距离。但是,如果Ray和包围球没有发生碰撞,这个方法返回null。所以你需要一个 float?变量而不是float,因为它需要能够存储null值。 注意:如果碰撞点位于定义ray的点之后,这个方法也会返回null。因为你在创建ray时指定了方向,XNA知道ray的哪一边是否在这个点的前面或后面。 如果ray和包围球之间发生碰撞并且碰撞点在最终位置点之前,intersection就不是null。要验证两帧之间是否发生碰撞,你需要检查碰撞点和快速物体前一个点之间的距离是否小于这个物体在上一帧帧运动的距离。如果是,则发生了碰撞。 更精确的方法 前面的教程中已经解释过了,因为使用的是包围球,这个方法可能在实际上并没有发生碰撞时也会检测到碰撞。所以再次,你可以通过在模型的不同ModelMesh上的小包围球上进行ray和包围球的碰撞检测提高精度。 大而快的物体 前面介绍的碰撞检测的方法要求快速物体非常小,类似于3D空间中的一个点。但在快物体很大不能看成一个点的情况下,你需要将快速物体当作一个包围球。一个解决方法是基于快速物体上的不同点进行多次ray检测,但这样做比较复杂。 一个快速且更简单的方法是将快速物体的半径添加到慢物体的半径上,如图4-15所示。这个图显示了两个物体的碰撞边缘。如果你将小半径添加到大半径上,结果是相同的。使用这个方法,你可以用一个简单的点表示快物体,并使用前面相同的碰撞检测方法。 图4-15 增大包围球可以获得相同的结果。 代码 加载了慢模型后,你需要在Tag属性中存储它的包围球: protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); cCross = new CoordCross(device); myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "tank", Content); } 有了包围球就可以调用下面的方法,显示在上一帧的过程中是否发生碰撞: private bool RayCollision(Model model, Matrix world, Vector3 lastPosition, Vector3 currentPosition) { BoundingSphere modelSpere = (BoundingSphere)model.Tag; BoundingSphere transSphere = XNAUtils.TransformBoundingSphere(modelSpere, world); Vector3 direction = currentPosition - lastPosition; float distanceCovered = direction.Length(); direction.Normalize(); Ray ray = new Ray(lastPosition, direction); bool collision = false; float? intersection = ray.Intersects(transSphere); if (intersection != null) if (intersection <= distanceCovered) collision = true; return collision; }

处理模型——使用包围球进行基本模型碰撞检测

clock 一月 15, 2011 10:49 by author alex
问题 你想检测两个模型是否发生碰撞。如果在场景中有许多模型,你将无法进行一个精确的逐三角形的碰撞检测。你想使用一个快速检测方法并在以后进行一个更好的碰撞检测。 解决方案 当进行碰撞检测时,你会发现常常需要在速度和精确度之间进行衡量。在大多数情况中,你会进行组合检测。在第一轮检测中,使用快速检测遍历所有对象,之后对快速检测中可能发生碰撞的物体进行精确检测。 本教程展示了两个方法处理两个模型间的碰撞检测。快速检测方法是找到模型的全局包围球,并通过调用一个包围球的Intersect 方法检测是否相交。 你可以改进这个方法以增加精确度。由几个ModelMeshes组成的模型存储了模型不同部分的几何信息。每个ModelMeshes都能生成各自的包围球,所以你可以检测第一个模型和第二个模型的每个包围球。显然,这样做提高了精度但计算量也随之加大。 工作原理 快速检测 这个方法使用整个模型的包围球。如果这两个模型的包围球相交,则它们可能发生了碰撞。 在某些情况下,这个方法会导致糟糕的结果,例如在滑板游戏中,当你想检测两个玩家的滑板的碰撞时,滑板的包围球相对于滑板本身大得多,滑板的体积不到包围球体积的百分之一,这个方法将会对全局包围球中的所有物体(可能包含其他滑板!)进行检测,如图4-13所示。 但是,这个方法在进行第一次检测时用得还是很多的,因为它的速度足够快。 图4-13 每个滑板都在另外一个滑板的包围球中 要实现这个快速检测,首先从加载模型和生成它们的的全局包围球开始,这一步已在教程4-5中介绍过了。包围球将存储在模型的Tag属性中: myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "tank",Content); 这个包围球将包含模型的中心位置(中心位置没必要一定在模型的(0,0,0)点上)和半径。 当绘制两个模型时,你需要在每个模型上施加一个不同的世界矩阵让它可以移动(和/或缩放/旋转)至正确的位置。当你移动(和/或缩放/旋转)一个模型时,还需要移动包围球的中心位置(和/或缩放半径)。你可以使用XNAUtils 文件中的TransformBoundingSphere方法做到这一点,这个方法以包围盒和世界矩阵为参数,更加世界矩阵移动中心位置和缩放半径,并返回结果包围球。 要检测两个模型间的碰撞,你需要通过世界矩阵变换每个包围球并检查变换过的包围球是否相交。 private bool CoarseCheck(Model model1, Matrix world1, Model model2, Matrix world2) { BoundingSphere origSphere1 = (BoundingSphere)model1.Tag; BoundingSphere sphere1= XNAUtils.TransformBoundingSphere(origSphere1, world1); BoundingSphere origSphere2= (BoundingSphere)model2.Tag; BoundingSphere sphere2 = XNAUtils.TransformBoundingSphere(origSphere2,world2); bool collision = sphere1.Intersects(sphere2); return collision; } 因为模型的Tag属性可以包含任何东西(这是因为Object类在C#中是根类),你需要通过(BoundingSphere)进行类型转换。如果变换过的包围球发生碰撞,这个方法会返回true。 更加精确,但有点慢 每个模型是由多个成员组成的,几何数据是存储在模型的Meshes集合中的。每个 ModelMesh都能生成自己的包围球。这些小的包围球的总体积比模型的全局包围球小得多,如图4-14所示。图的右边你可以看到模型每个部分的独立包围球。 图4-14 全局包围球和多个小的包围球 在图的左边,两个包围球相交,这样第一次检测会显示两个模型发生碰撞。而在图的右边,没有包围球发生碰撞,所以这次检测会正确地显示两者并没有发生碰撞。第二个方法通常会返回更好的结果;但是,第一种检测只需检查一次,而第二种需要检查(members inside Model1)*(members inside Model2)次。 下面的方法是根据第二种情况的碰撞检测: private bool FinerCheck(Model model1, Matrix world1, Model model2, Matrix world2) { if (CoarseCheck(model1, world1,model2, world2) == false) return false; bool collision = false; Matrix[] model1Transforms= new Matrix[model1.Bones.Count]; Matrix[] model2Transforms = new Matrix[model2.Bones.Count]; model1.CopyAbsoluteBoneTransformsTo(model1Transforms); model2.CopyAbsoluteBoneTransformsTo(model2Transforms); foreach (ModelMesh mesh1 in model1.Meshes) { BoundingSphere origSphere1 = mesh1.BoundingSphere; Matrix trans1 = model1Transforms[mesh1.ParentBone.Index] * world1; BoundingSpheretransSphere1 = XNAUtils.TransformBoundingSphere(origSphere1, trans1); foreach (ModelMesh mesh2 in model2.Meshes) { BoundingSphere origSphere2 = mesh2.BoundingSphere; Matrix trans2 = model2Transforms[mesh2.ParentBone.Index] * world2; BoundingSphere transSphere2 = XNAUtils.TransformBoundingSphere(origSphere2, trans2); if (transSphere1.Intersects(transSphere2)) collision = true; } } return collision; } 你首先进行快速检测,如果这次检测返回false,那就没必要进行精确检测。 如果快速检测显示可能发生碰撞,那么还有进行更加精确的检测。你首先将collision变量设置为false,除非检测到碰撞那么这个变量将保持为false。因为你需要将每个小包围球移动到正确的位置,所以需要绝对Bone矩阵。 要对第一个模型的每个ModelMesh进行这个检测,你需要将包围球移动到绝对位置。要实现这一步,你需要考虑模型包围球的位置和模型在3D世界中的位置。 然后你遍历第二个模型的所有部分并将这些包围球变换到绝对位置。对于第一个模型的每个包围球,你检查是否与第二个模型的任意一个包围球是否发生碰撞,如果是,则将collision变量设置为true。 最后,collision变量显示是否至少有一个模型1的包围球与模型2的包围球发生碰撞并返回这个变量。 优化 因为对模型1的每个部分来说,模型2的所有包围球都是以相同的方式进行变换的,显然这个方法可以进行优化。例如,你可以首先变换模型1和模型2的包围球一次,在方法的开头,将它们存储在一个数组中。之后,你只需简单地对这些变换过的包围球进行碰撞检测。 下面的代码是FinerCheck方法的优化版本: private bool FinerCheck(Model model1, Matrix world1, Model model2, Matrix world2) { if (CoarseCheck(model1, world1, model2, world2) == false) return false; Matrix[] model1Transforms = new Matrix[model1.Bones.Count]; model1.CopyAbsoluteBoneTransformsTo(model1Transforms); BoundingSphere[] model1Spheres = new BoundingSphere[model1.Meshes.Count]; for (int i=0; i<model1.Meshes.Count; i++) { ModelMesh mesh = model1.Meshes[i]; BoundingSphere origSphere = mesh.BoundingSphere; Matrix trans = model1Transforms[mesh.ParentBone.Index]* world1; BoundingSphere transSphere = XNAUtils.TransformBoundingSphere(origSphere,trans); model1Spheres[i] = transSphere; } Matrix[] model2Transforms = new Matrix[model2.Bones.Count]; model2.CopyAbsoluteBoneTransformsTo(model2Transforms); BoundingSphere[] model2Spheres= new BoundingSphere[model2.Meshes.Count]; for (int i = 0; i < model1.Meshes.Count;i++) { ModelMesh mesh = model2.Meshes[i]; BoundingSphere origSphere = mesh.BoundingSphere; Matrix trans = model2Transforms[mesh.ParentBone.Index] * world2; BoundingSphere transSphere = XNAUtils.TransformBoundingSphere(origSphere, trans); model2Spheres[i]= transSphere; } bool collision = false; for (int i=0; i<model1Spheres.Length;i++) for (int j = 0; j < model2Spheres.Length; j++) if (model1Spheres[i].Intersects(model2Spheres[j])) return true; return collision; } 注意:CopyAbsoluteBoneTransformsTo方法只需在模型的Bone矩阵更新后被调用一次。 代码 CoarseCheck和FinerCheck方法的代码前面已经写过了。下面的Draw方法定义了两个世界矩阵将两个模型放置在不同位置。随着时间流逝,一个模型接近另一个,如果两者发生碰撞,背景会变成红色: protected override void Draw(GameTime gameTime) { //define World matrices float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 1000.0f; Matrix worldM1= Matrix.CreateScale(0.01f) * Matrix.CreateRotationY(MathHelper.PiOver2) *Matrix.CreateTranslation(-10,0, 0); Matrix worldM2 = Matrix.CreateScale(0.01f) * Matrix.CreateRotationY(-MathHelper.PiOver2)* Matrix.CreateTranslation(10, 3, 0); worldM2 = worldM2 * Matrix.CreateTranslation(-time* 3.0f, 0, 0); //check for collision if (FinerCheck(myModel, worldM1, myModel, worldM2)) { Window.Title = "Collision!!"; device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer,Color.Red, 1, 0); } else { Window.Title = "No collision ..."; device.Clear(ClearOptions.Target| ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); } //render scene cCross.Draw(fpsCam.ViewMatrix,fpsCam.ProjectionMatrix); DrawModel(myModel, worldM1, modelTransforms); DrawModel(myModel,worldM2, modelTransforms); base.Draw(gameTime); }

处理模型——使Bone独立运动:模型动画

clock 一月 15, 2011 10:45 by author alex
问题 你想独立的移动模型的每一部分。例如,你想摇低一辆车的车窗或让车轮转动。 解决方案 如教程4-1中解释的那样,一个模型是由可单独绘制的ModelMesh组成的。每个ModelMesh都链接到一个Bone,这些Bone互相联系,之间的位置关系是由矩阵显示的。 每个模型都有一个root Bone,所有其他Bone对象直接或间接与它链接,图4-11显示了这种结构的一个例子。 如果你想对图中的root Bone进行变换-例如,对这个root Bone进行缩放,那么它的所有child Bone对象(包括child Bone对象的child Bone对象) 会自动以同样的方式缩放。第二个例子,如果你想对存储在右前门Bone中的矩阵进行旋转,只有门本身,它的车窗和门锁会跟着旋转,这正是你需要的。 工作原理 在你对模型应用一个动画之前,你需要对模型的Bone结构有个概览,前面的教程中已经解释了如何可视化Bone结构,看到哪个ModelMeshes链接到哪个Bone对象上。 让我们看一下在XNA Creators Club网站上找到的坦克模型的Bone结构,这个结构在前一个教程中已经写过了。你有一个root Bone,当你缩放这个Bone的矩阵时,整个坦克都会进行缩放。接下来看一下炮塔的Bone,它是root的子Bone,如果你旋转这个Bone,每个链接到这个Bone的ModelMesh和它的child Bone对象都会旋转。所以当你旋转炮塔Bone的矩阵时,炮塔,炮管,翻盖都会旋转,因为它们都是链接到炮塔上的。 下面是你必须要记住的重点: 模型中所有的可独立绘制的,可变换的部分都存储在不同的ModelMesh对象中。每个ModelMesh对象都链接到一个Bone对象。 每个Bone对象存储这个Bone相对于它的parent Bone的位置,旋转和缩放信息。 当你设置一个Bone的变换时,这个变换还要影响到这个Bone的所有child Bone对象。 CopyAbsoluteBoneTransformsTo方法的必要性(额外的解释) 上面列表中的最后一点不是很容易。在绘制每个ModelMesh时,你需要设置它的世界矩阵,因为你想让ModelMesh放置在正确的3D空间中。问题是这个世界矩阵定义的是ModelMesh在3D空间中的位置,但是,Bone矩阵包含的矩阵存储的相对于它的parent ModelMesh的位置! 以坦克的火炮为例,火炮的Bone矩阵包含一个诸如(0,0,-2)的偏移量:相对于它的父:炮塔向前移动2个单位。 如果你只是简单地将火炮的世界矩阵设置为火炮ModelMesh的矩阵,这会导致火炮ModelMesh被绘制到相对于3D空间的初始位置(0,0,0)偏离(0,0,-2)的地方。 但这不是你想要的结果!你实际是想将火炮放置到相对于它的parent:炮塔偏离(0,0,-2)的位置。 所以在绘制火炮前,你需要将它的Bone和它的parent的Bone (将两者相乘)组合起来。而且还要回溯到根节点,因为这种情况中最终矩阵还要和炮塔的parent Bone(坦克的车身)组合。通过这种方式,你获取了火炮的最终世界矩阵。 因为这个矩阵是相对于坦克初始位置的,所以叫做火炮的绝对变换矩阵(absolute transformation matrix)。 幸运的是,XNA提供了组合这些矩阵的功能。在绘制模型前,你需要调用模型的CopyAbsoluteBoneTransformsTo方法,这个方法会计算所有的组合并将它存储在结果的绝对矩阵数组中。这些绝对矩阵不再包含相对于parent Bone的变换信息;而只包含相对与模型的root的信息。结果是,这些包含在modelTransforms数组中的矩阵包含了坦克模型中所有ModelMesh的绝对变换信息。 你可以使用这些矩阵作为模型中每个ModelMesh的绝对世界矩阵: myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index]; effect.View = fpsCam.GetViewMatrix(); effect.Projection = fpsCam.GetProjectionMatrix(); } mesh.Draw(); } 虽然看起来变得更难了,但这样做会带来巨大的好处 。在这个例子中,只要炮塔的Bone矩阵发生旋转,炮管的(0,0,-2)平移矩阵也会跟着一起旋转。这是因为当CopyAbsoluteBoneTransformsTo方法结束后,火炮的绝对转换矩阵也会包含它的parent Bone 矩阵的旋转信息。而且,所有链接到炮塔的child ModelMesh,诸如炮管和翻盖,也会自动随着炮塔一起旋转。 设置模型中指定的ModelMesh的动画 知道了模型的结构后,现在可以实现模型动画了。 在本例中,你想提升炮管。要做到这一点,使用前一个教程中的方法看一下炮管的Mesh part与哪个Bone相链接,而你将对这个Bone的矩阵施加一个旋转。 但是,这个Bone矩阵中存储了火炮相对于炮塔的原始位置,如果你用旋转矩阵覆盖了这个矩阵,原始位置就丢失(或很难找到)!这样就无法以后对炮管施加动画了,因为你总是想从初始矩阵开始进行旋转,而这个初始矩阵的值已经丢失。 所以在加载了模型后,你需要创建一个原始Bone矩阵的备份,存储相对于parent的位置,这要用到CopyBoneTransformsTo方法: Matrix[] originalTransforms = new Matrix[myModel.Bones.Count]; myModel.CopyBoneTransformsTo(originalTransforms); 注意:对每个ModelMesh,你都需要存储相对于parent ModelMesh的位置信息,所以你使用CopyBoneTransformsTo method。CopyAbsoluteBoneTransformsTo给你相对于模型初始位置的位置信息,如前面在“CopyAbsoluteBoneTransformsTo方法的必要性”一节中解释的。 你需要将这个代码放置在LoadContent方法中。 存储好矩阵后,你可以安全地覆盖存储在Bone对象中的矩阵了,在项目中添加一个canonRot变量: float canonRot = 0; 可以让玩家在Update方法中调整这个变量: if (keyState.IsKeyDown(Keys.U)) canonRot -= 0.05f; if (keyState.IsKeyDown(Keys.D)) canonRot += 0.05f; 现在你可以使用键盘控制这个变量,将以下代码放到Draw方法中,在这行代码之前还要计算模型的绝对世界矩阵: Matrix newCanonMat = Matrix.CreateRotationX(canonRot) * originalTransforms[10]; myModel.Bones[10].Transform = newCanonMat; 在前面的教程中你可以在坦克模型的结构中看到,炮塔的ModelMesh链接到Bone 10。这个代码将炮管的相对于炮塔的初始位置存储在矩阵中,沿着向右向量旋转(使之上下旋转),并在模型中存储组合矩阵。当运行代码后,炮管会根据键盘输入上下旋转。 如果你想旋转整个炮塔,你需要对炮塔的Bone矩阵做同样的事情,首先添加turretRot变量: float turretRot = 0; 然后在Update方法中添加键盘控制代码: if (keyState.IsKeyDown(Keys.L)) turretRot += 0.05f; if (keyState.IsKeyDown(Keys.R)) turretRot -= 0.05f; 在Draw方法中调整对应的Bone矩阵: Matrix newTurretMat = Matrix.CreateRotationY(turretRot) * originalTransforms[9]; myModel.Bones[9].Transform = newTurretMat; 如你在前一个教程中看到的,炮塔的Bone索引是9。首先获取初始矩阵,然后绕着Y轴旋转让炮塔左右旋转。 注意:改变炮管矩阵和改变炮塔矩阵的顺序先后不会影响结果。在绝对矩阵中的Bone对象间的关系只有在调用CopyAbsoluteBoneTransformsTo方法时才会存储。 如前所述,如果旋转炮塔,那么炮塔的children (本例中是炮管和翻盖)也会跟着一起旋转,这是因为炮管的Bone矩阵通过CopyAbsoluteBoneTransformsTo方法和它的child Bone矩阵组合在了一起。 代码 在加载模型后,请确保你存储了原始Bone矩阵: protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); cCross = new CoordCross(device); myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; originalTransforms = new Matrix[myModel.Bones.Count]; myModel.CopyBoneTransformsTo(originalTransforms); } 在update过程中,我们可以改变旋转角度: KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.U)) canonRot -= 0.05f; if (keyState.IsKeyDown(Keys.D)) canonRot += 0.05f; if (keyState.IsKeyDown(Keys.L)) turretRot += 0.05f; if (keyState.IsKeyDown(Keys.R)) turretRot -= 0.05f; 最后绘制模型,你需要用旋转矩阵覆盖原始Bone矩阵并构建绝对Bone矩阵,而这些绝对矩阵必须作为ModelMesh的当前矩阵: protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); cCross.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix); //draw model Matrix newCanonMat = Matrix.CreateRotationX(canonRot) * originalTransforms[10]; MyModel.Bones[10].Transform = newCanonMat; Matrix newTurretMat = Matrix.CreateRotationY(turretRot) * originalTransforms[9]; myModel.Bones[9].Transform = newTurretMat; Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.ViewMatrix; effect.Projection = fpsCam.ProjectionMatrix; } mesh.Draw(); } base.Draw(gameTime); };

处理模型——可视化模型骨骼结构

clock 一月 15, 2011 10:41 by author alex
问题 如在教程4-1中的最后所讨论的,一个模型通常包含许多成员,这些成员叫做ModelMeshes。这些ModelMeshes之间的位置联系是包含在Model对象的Bone结构中的。Bone结构定义了ModelMeshes是如何并在哪儿互相联系,每个ModelMesh 相对于parent ModelMesh旋转和/或缩放了多少。 在你可以让模型动起来前,你需要知道ModelMeshes是连接在哪个Bone上,需要可视化Bone的结构。 解决方案 Model是由ModelMeshe组成的。这些ModelMeshe包含了渲染模型确定部分所需的所有数据,包含所有顶点、索引、纹理、effect等,如教程4-1的第二部分所述。但是,ModelMesh 不包含在模型中的位置的信息。例如,一个car模型的右前门ModelMesh包含所有绘制需要的顶点和纹理信息,但它不包含相对于车身中心绘制在哪儿的信息。 这个信息存储在模型的Bones集合中。每个ModelMesh链接到一个Bone,这样XNA可以找到ModelMesh相对于Model中心的位置。一个Bone只是包含一个变换矩阵,保存这个ModelMesh相对于它的parent ModelMesh的位置。例如,车身的ModelMesh指向一个保存0平移的Bone,这是因为车身是车的主体。右前车门的ModelMesh指向一个保存右车门相对于车身中心位置的转换矩阵的Bone。 你还可以加以扩展。一辆高级的汽车模型还包含一个独立的右前车窗ModelMesh,这个ModelMesh也指向一个Bone,这个Bone包含车窗相对于右前车门的位置信息。图4-11显示了这个Bone结构。我在每个名称的后面加了一个B提醒你这些都是Bone对象。要产生这种结构,每个Bone对象都要链接到它的parent Bone和所有它的child Bone。 图4-11 一个汽车模型的Bone层次 在本教程中,你将遍历这个结构并用类似于图4-11的形式将Bone对象写入一个文件。你还要遍历模型的所有ModelMeshes并显示它们链接到哪个Bone。 工作原理 幸运的是,你可以很容易地通过在加载模型后的代码行中设置断点的方法看到模型中定义了哪些ModelMeshe,这一步可以通过点击代码左侧的侧边栏实现,也可以使用如下的代码: myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; System.Diagnostics.Debugger.Break(); 当运行代码时,程序会在最后一行停止,屏幕的左下角你可以浏览到模型并看到这个模型包含了哪些ModelMeshe,如图4-12所示。 图4-12 浏览实时变量 但你还需要知道每个ModelMesh链接到哪个Bone上,你还想遍历Bone集合获知Bone的层次结构。在本教程中,你将编写一些代码生成一个简单的文本文件,包含Bone结构和ModelMesh-to-Bone之间的联系。 下面的输出清单是DirectX SDK自带的dwarf模型的信息: Model Bone Information ---------------------- - Name : DXCC_ROOT Index: 0 - Name : null Index: 1 - Name : Drawf Index: 2 - Name : Sword_new Index: 3 - Name : Sword_newShape Index: 4 - Name : backpack_new Index: 5 - Name : backpack_newShape Index: 6 - Name : Body1 Index: 7 - Name : Body1Shape Index: 8 - Name : Armor_new Index: 9 - Name : Armor_newShape Index: 10 - Name : Face_gear_new Index: 11 - Name : Face_gear_newShape Index: 12 - Name : Head_new Index: 13 - Name : Head_newShape Index: 14 - Name : side Index: 15 - Name : front Index: 16 - Name : top Index: 17 - Name : persp Index: 18 Model Mesh Information ---------------------- - ID : 0 Name: Sword_newShape Bone: Sword_newShape (4) - ID : 1 Name: backpack_newShape Bone: backpack_newShape (6) - ID : 2 Name: Body1Shape Bone: Body1Shape (8) - ID : 3 Name: Armor_newShape Bone: Armor_newShape (10) - ID : 4 Name: Face_gear_newShape Bone: Face_gear_newShape (12) - ID : 5 Name: Head_newShape Bone: Head_newShape (14) 在代码底部,你知道这个矮人(dwarf)模型包含六个ModelMeshe,每个ModelMeshe链接到各自的Bone。你可以独立地移动剑和背包,但无法移动手臂和腿,因为手臂和腿不是一个独立的ModelMeshes。 这个模型也不支持基本动画。让我们看一下另一个模型的结构,在XNA Creators Club网站上的一个坦克模型: Model Bone Information ---------------------- - Name : tank_geo Index: 0 - Name : r_engine_geo Index: 1 - Name : r_back_wheel_geo Index: 2 - Name : r_steer_geo Index: 3 - Name : r_front_wheel_geo Index: 4 - Name : l_engine_geo Index: 5 - Name : l_back_wheel_geo Index: 6 - Name : l_steer_geo Index: 7 - Name : l_front_wheel_geo Index: 8 - Name : turret_geo Index: 9 - Name : canon_geo Index: 10 - Name : hatch_geo Index: 11 Model Mesh Information ---------------------- - ID : 0 Name: r_back_wheel_geo Bone: r_back_wheel_geo (2) - ID : 1 Name: r_front_wheel_geo Bone: r_front_wheel_geo (4) - ID : 2 Name: r_steer_geo Bone: r_steer_geo (3) - ID : 3 Name: r_engine_geo Bone: r_engine_geo (1) - ID : 4 Name: l_back_wheel_geo Bone: l_back_wheel_geo (6) - ID : 5 Name: l_front_wheel_geo Bone: l_front_wheel_geo (8) - ID : 6 Name: l_steer_geo Bone: l_steer_geo (7) - ID : 7 Name: l_engine_geo Bone: l_engine_geo (5) - ID : 8 Name: canon_geo Bone: canon_geo (10) - ID : 9 Name: hatch_geo Bone: hatch_geo (11) - ID : 10 Name: turret_geo Bone: turret_geo (9) - ID : 11 Name: tank_geo Bone: tank_geo (0) 在这个例子中。你会发现几乎每个独立的部分都有一个ModelMesh!轮子有四个ModelMeshes,车身有两个,炮塔一个,炮管一个,甚至连舱盖也有一个。因为所有的ModelMeshes都有各自的Bone,所以它们都可以独立运动!例如,炮管的 ModelMesh链接到Bone 10,改变Bone 10的矩阵就可以改变炮管的位置和旋转,下一个教程中你会学到更多的知识。 收集模型信息 下面的方法可以将模型信息写到一个文件中: private void WriteModelStructure(Model model) { StreamWriter writer = new StreamWriter("modelStructure.txt"); writer.WriteLine("Model Bone Information"); writer.WriteLine("----------------------"); ModelBone root = model.Root; WriteBone(root, 0, writer); writer.WriteLine(); writer.WriteLine(); writer.WriteLine("Model Mesh Information"); writer.WriteLine("----------------------"); foreach (ModelMesh mesh in model.Meshes) WriteModelMesh(model.Meshes.IndexOf(mesh), mesh, writer); writer.Close(); } 因为你要将文字写到一个文件中,所以需要创建一个StreamWriter,这要求在代码顶部添加System.IO命名空间: using System.IO; 下一步,在文件中先写一个文件头,然后调用WriteBone方法处理模型的root Bone。这个马上就要创建的方法会写下这个Bone的信息并将这个调用传递到所有child Bone对象,将所有子Bone对象的信息也写入文件。 在写入第二个文件头后,你遍历模型的所有ModelMeshe并将它们传递到WriteModelMesh方法,这个方法会将ModelMesh的信息写到文件中。 WriteBone方法 这个方法写入名称(如果没有指定名称则为null)和索引。最后这个方法对当前Bone的所有child Bone调用自己。通过这种方式,你可以只对模型的root Bone 调用此方法一次,所有链接到这个root Bone的Bone对象都会罗列出来。 private void WriteBone(ModelBone bone, int level, StreamWriter writer) { for (int l = 0; l < level; l++) writer.Write("\t"); writer.Write("- Name : "); if ((bone.Name == "") || (bone.Name == "null")) writer.WriteLine("null"); else writer.WriteLine(bone.Name); for (int l = 0; l < level; l++) writer.Write("\t"); writer.WriteLine(" Index: " + bone.Index); foreach (ModelBone childBone in bone.Children) WriteBone(childBone, level + 1, writer); } WriteModelMesh方法这个方法写入每个ModelMesh的ID、名称和链接到的Bone,下一个教程你需要这个信息绘制模型动画: private void WriteModelMesh(int ID, ModelMesh mesh, StreamWriter writer) { writer.WriteLine("- ID : " + ID); writer.WriteLine(" Name: " + mesh.Name); writer.Write(" Bone: " + mesh.ParentBone.Name); writer.WriteLine(" (" + mesh.ParentBone.Index + ")"); } 用法 只需在加载模型时调用WriteModelStructure方法即可: myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; WriteModelStructure(myModel); 当运行这个代码时,模型结构会被写到modelStructure.txt文件中,这个文件可以在生成.exe 文件的映射位置找,默认是在\bin\x86\Debug位置中。 代码 WriteModelStructure,WriteBone和WriteModelMesh方法前面已经全部写过了。

处理模型——使用自定义Effects和纹理绘制模型

clock 一月 15, 2011 10:34 by author alex
问题 当你使用教材4-1中的代码将一个模型加载到XNA项目时,你使用的是一个BasicEffect实例。在简单的情况下BasicEffect可以很好地绘制模型,当我们常常想使用一个不同的,自定义的effect绘制一个模型。 解决方案 通过将模型的effect包含在BasicEffect对象中,你可以获取effect的所有信息,诸如纹理和材质等信息。当你将这些属性复制到某个安全的地方后,就可以用你选择的effect覆盖这个effect。 注意:你还可以使用自定义内容处理器更加清晰地完成这个任务,可见教材4-12。 工作原理 首先你需要加载一个模型和自定义effect,两者都需要使用内容管道进行加载: protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; customEffect = Content.Load<Effect>("bwcolor"); } 本例中的自定义effect会使用灰度值绘制模型(译者注:本例中的灰度实现使用最简单的三个颜色通道求平均值的方法,而不是更好的给绿色更多的权重)。 因为effect是存储在模型的ModelMesh中的,所以你可以简单地使用自定义effect覆盖它们。但是,所有原始effect信息都会丢失!这些信息包括了纹理和材质颜色信息。 在覆盖模型的默认effect前,你想存储这些信息,因为自定义effect需要知道某些信息,例如使用哪个纹理。 你将创建一个方法处理模型和自定义effect,这个方法会使用自定义effect的副本替换模型中的所有effect。最后,存储初始数据,返回初始effect中所有纹理的数组,这样你就可以将它们传递到自定义effect。本例中,所有的effect都会被自定义effect复写,但a small adaptation suffices to override only the effect of a particular ModelMeshPart(译者:?): private Texture2D[] ChangeEffect(ref Model model, Effect newEffect) { } 因为你想永久地改变模型,所以model参数要使用引用(在第一个变量前加上ref)。否则会使用一个本地副本,而对于这个副本所有改变都不会被调用代码获知。 首先,你要创建一个被原始effect使用的所用纹理的副本: List<Texture2D> textureList = new List<Texture2D>(); foreach (ModelMesh mesh in model.Meshes) foreach (ModelMeshPart modmeshpart in mesh.MeshParts) { BasicEffect oldEffect = (BasicEffect)modmeshpart.Effect; textureList.Add(oldEffect.Texture); } Texture2D[] textures = textureList.ToArray(); return textures; 你创建了一个集合存储纹理,然后遍历每个ModelMesh的不同ModelMeshParts,每个ModelMeshPart包含指向effect的链接。将这个effect传递到一个BasicEffect对象,这样你就可以访问到它的属性,例如纹理。对于模型中的每个effect都附加上一个纹理集合中的纹理。 收集完所有的纹理后,你将集合转换为一个数组并将这个数组返回。 现在你所做的操作会使用自定义effect的副本覆盖原始effect,所以在第二个循环中添加以下代码: modmeshpart.Effect = newEffect.Clone(device); 注意:你使用了Clone方法创建了一个effect的副本,你想使用这个副本,否则所有的ModelMeshPart都会共享相同的effect。通过给每个ModelMeshPart施加各自effect的副本,每个ModelMeshPart都能使用不同的纹理。 在LoadContent方法中加载了模型和自定义effect之后调用这个方法: modelTextures = ChangeEffect(ref myModel, customEffect); 当绘制模型时,你就可以单独定义自定义effect的每个参数了: int i = 0; Matrix worldMatrix = Matrix.CreateScale(0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (Effect currentEffect in mesh.Effects) { currentEffect.Parameters["xWorld"]. SetValue(modelTransforms[mesh.ParentBone.Index]*worldMatrix); currentEffect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); currentEffect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); currentEffect.Parameters["xTexture"].SetValue(modelTextures[i++]); } mesh.Draw(); } 请注意你是如何将原始effect信息传递到自定义的effect中的,例如本例中的纹理。 遍历ModelMeshPart而不是Effect 前面的循环中遍历了所有effect,但这样做当你想设置一个特定ModelMeshPart的effect时会出现问题,所以你遍历ModelMesh的ModelMeshPart而不是它的effect: int i = 0; Matrix worldMatrix = Matrix.CreateScale(0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (ModelMeshPart part in mesh.MeshParts) { Effect currentEffect = part.Effect; currentEffect.Parameters["xWorld"]. SetValue(modelTransforms[mesh.ParentBone.Index] * worldMatrix); currentEffect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); currentEffect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); currentEffect.Parameters["xTexture"].SetValue(modelTextures[i++]); } mesh.Draw(); } 存储所有原始信息 如果你还想存储的纹理之外的信息,只需简单地将原始effect添加到集合替代纹理: BasicEffect[] originalEffects; 在ChangeEffect方法中,将原始effect添加到集合中去: BasicEffect oldEffect = (BasicEffect)modmeshpart.Effect; effectList.Add(oldEffect); 当设置自定义effect的参数时,你可以很容易地访问到初始信息: currentEffect.Parameters["xTexture"].SetValue(originalEffects[i++].Texture); 代码 在LoadContent方法中,加载模型和自定义effect: protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); cCross = new CoordCross(device); myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; customEffect = Content.Load<Effect>("bwcolor"); originalEffects = ChangeEffect(ref myModel, customEffect); } 最后一行代码使用自定义effect的副本替换了模型中的effect,是在下列方法中实现的: private BasicEffect[] ChangeEffect(ref Model model, Effect newEffect) { List<BasicEffect> effectList = new List<BasicEffect>(); foreach (ModelMesh mesh in model.Meshes) foreach (ModelMeshPart modmeshpart in mesh.MeshParts) { BasicEffect oldEffect = (BasicEffect)modmeshpart.Effect; effectList.Add(oldEffect); modmeshpart.Effect = newEffect.Clone(device); } BasicEffect[] effects = effectList.ToArray(); return effects; } 当绘制模型时,你可以像这样访问到存储的信息: int i = 0; Matrix worldMatrix = Matrix.CreateScale(0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (ModelMeshPart part in mesh.MeshParts) { Effect currentEffect = part.Effect; currentEffect.Parameters["xWorld"]. SetValue(modelTransforms[mesh.ParentBone.Index] * worldMatrix); currentEffect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); currentEffect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); currentEffect.Parameters["xTexture"].SetValue(originalEffects[i++].Texture); } mesh.Draw(); }

处理模型——缩放模型

clock 一月 15, 2011 10:30 by author alex
问题 当从磁盘载入模型时,往往会太大或太小,你想将模型缩放到定义的大小。 解决方案 首先你需要定义一个模型的全局包围球,前面一个教程已经解释了。知道了这个包围球,你就可以知道模型的当前尺寸了。从这个尺寸,你可以知道需要将模型放大或所小多少。你也可以将这个缩放操作储存在root Bone矩阵中,这样缩放会施加到模型中所有Bone的所有矩阵上(可见教程4-9)。 工作原理 通常,你使用的模型是由不同的工具制作的,或从网上下载的,你无法指定模型的大小。所以,如果你能将模型缩放到你想要的大小那会很棒。 下面的代码计算模型需要缩放到多少,这需要存储在模型Tag属性中的全局包围球(见前一个教程)。 private Matrix[] AutoScale(Model model, float requestedSize) { BoundingSphere bSphere = (BoundingSphere)model.Tag; float originalSize = bSphere.Radius * 2; float scalingFactor = requestedSize / originalSize; model.Root.Transform = model.Root.Transform * Matrix.CreateScale(scalingFactor); Matrix[] modelTransforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(modelTransforms); return modelTransforms; } 你需要将模型传递给这个方法指定最终的模型有多大。这个方法一开始获取一个模型的全局包围球,你需要将求的半径乘2获取球的大小,对应于模型的初始大小。接下来,你将requestedSize除以这个值获取这个模型需要缩放到多大。你可以使用这个缩放因子创建一个矩阵,将这个矩阵作为绘制模型时的世界矩阵,这样你的模型就会缩放到想要的大小了。 在Root Bone上施加缩放 构建良好的模型将所有子网格链接到它们的root bone。这意味着当你缩放了root bone矩阵 (见教程4-9),所有ModelMeshes也会自动缩放。你可以通过将Root矩阵乘以缩放矩阵实现,在前面计算的scalingFactor基础上,将这个结果矩阵存储在模型中。因为你改变了Bone矩阵的内容,所以你必须提取modelTransforms矩阵的新版本。 注意:更好的方法是在自定义模型处理器中使用一个可以进行配置的变量,见教程4-12学习如何扩展默认模型处理器。 代码 在LoadContent方法中,你需要使用两行代码加载并缩放模型: myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "tank", Content); modelTransforms = AutoScale(myModel, 10.0f); 当绘制模型时,你可以使用普通的代码,因为缩放信息已经被储存在模型的Root bone中了。通过这种方式,每次当你调用Model. CopyAbsoluteBoneTransformsTo方法时,所有的结果矩阵都已经包含了缩放操作,这样在将模型的各个部分绘制到屏幕之前它们已经进行了缩放。 Matrix worldMatrix = Matrix.Identity; myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.ViewMatrix; effect.Projection = fpsCam.ProjectionMatrix; } mesh.Draw(); }

处理模型——构建模型的全局包围球

clock 一月 15, 2011 10:01 by author alex
问题 模型的包围球是完整包围模型的最小球体。在很多情况中,例如碰撞检测,判断大小等,使用包围球是非常有用的。 解决方案 你可以访问模型中的每个ModelMesh的包围球。使用CreateMerged方法,你可以将这个包围球组合起来,获取包围整个模型的包围球。 但是,因为每个ModelMesh的包围球是相对于Bone矩阵定义的,所以你需要进行转换。 工作原理 你将创建一个方法将一个素材加载到一个模型变量中,初始化它的Bone矩阵数组,并将全局包围球保存到模型的Tag属性中。 技巧:你可以使用模型的Tag属性存储任何对象。本例中,你使用Tag属性存储包围球。Tag属性在本章中会经常用到。 private Model LoadModelWithBoundingSphere(ref Matrix[] modelTransforms, string asset, ContentManager content) { Model newModel = content.Load<Model>(asset); modelTransforms = new Matrix[newModel.Bones.Count]; newModel.CopyAbsoluteBoneTransformsTo(modelTransforms); return newModel; } 这个方法加载一个模型,已在教程4-1中解释过了。在返回模型前,你需要遍历模型的查看它们的包围球(这些包围球是由默认模型处理器在编译时创建的,可见教程4-13中的解释)。 foreach (ModelMesh mesh in newModel.Meshes) { BoundingSphere origMeshSphere = mesh.BoundingSphere; } 你想将这些包围球组合在一起获取整个模型的全局包围球。你可以通过保存一个新的包围球并把这些小的包围球组合在一起实现这个功能。 BoundingSphere completeBoundingSphere = new BoundingSphere(); foreach (ModelMesh mesh in newModel.Meshes) { BoundingSphere origMeshSphere = mesh.BoundingSphere; completeBoundingSphere = BoundingSphere.CreateMerged(completeBoundingSphere, origMeshSphere); } 这样completeBoundingSphere就是一个包含所有小球的大球。 但是,当你获取ModelMesh的包围球时,这个包围球是定义在ModelMesh的本地空间中的。例如,一个人的躯干的包围球是80cm,而头的包围球只有30cm,如果你只是简单将两者组合在一起,你获得的是80cm的球,这是因为小球是完整包含在大球中的。 要正确的组合两者,你首先要将头的包围球移动到躯干的顶部然后在进行组合,这样才会获得一个包含两者的大的包围球。 在XNA中,这意味着你必须首先使用ModelMesh包含的Bone矩阵转换这个ModelMesh的包围球,就像以下代码所示: BoundingSphere completeBoundingSphere = new BoundingSphere(); foreach (ModelMesh mesh in newModel.Meshes) { BoundingSphere origMeshSphere = mesh.BoundingSphere; BoundingSphere transMeshSphere = XNAUtils.TransformBoundingSphere(origMeshSphere, modelTransforms[mesh.ParentBone.Index]); completeBoundingSphere = BoundingSphere.CreateMerged(completeBoundingSphere, transMeshSphere); } newModel.Tag = completeBoundingSphere; 正确的全局包围球会被存储到模型的Tag属性中。 注意:BoundingSphere.Transform方法是XNA Framework自带的,但它不能处理包含旋转的矩阵,所以我在XNAUtils文件中包含了一个扩展版本,你可以在示例中找到这个方法。 用法 当你想从Tag属性中获取全局包围盒时,你需要首先将它转换为BoundingSphere对象,这是因为Tag属性中可以存储任何类型的对象: BoundingSphere bSphere = (BoundingSphere)myModel.Tag; 代码 下面是完整代码,包括初始化一个模型并将它的包围盒存储到Tag属性中: private Model LoadModelWithBoundingSphere(ref Matrix[] modelTransforms, string asset, ContentManager content) { Model newModel = content.Load<Model>(asset); modelTransforms = new Matrix[newModel.Bones.Count]; newModel.CopyAbsoluteBoneTransformsTo(modelTransforms); BoundingSphere completeBoundingSphere = new BoundingSphere(); foreach (ModelMesh mesh in newModel.Meshes) { BoundingSphere origMeshSphere = mesh.BoundingSphere; BoundingSphere transMeshSphere = XNAUtils.TransformBoundingSphere(origMeshSphere, modelTransforms[mesh.ParentBone.Index]); completeBoundingSphere = BoundingSphere.CreateMerged(completeBoundingSphere, transMeshSphere); } newModel.Tag = completeBoundingSphere; return newModel; }

处理模型——使用加速度控制速度

clock 一月 5, 2011 14:24 by author alex
问题 你想让模型能够漂亮的加速而不是从静止直接提高到最大速度。图4-9显示了你想实现的这种加速过程。 图4-9 模型的速度时间图像 解决方案 通过添加加速度,你可以定义你的模型的速度变化有多快。加速度就是每帧速度的增加量。 工作原理 你需要保存模型的位置和旋转,这是因为你需要知道哪个方向是向前的。在本例中,你只是让模型绕着它的向上的y轴旋转并沿着X和Z轴移动。如果你想让模型只基于X和Z轴沿着一个表面移动可参加教程4-17。 因为模型以后的速度取决于当前速度,所以你需要保存当前速度。你将存储速度矢量而不是标量。这个速度矢量包含模型当前前进的方向,它的长度表示速度的大小。所以在类中添加三个变量: Vector3 modelPosition = new Vector3(); float modelYRot = 0; Vector3 modelVelocity= new Vector3(); 你也可以定义模型的最大加速度和转弯速度: const float modelMaxAcceleration = 30.0f; const float modelMaxTurnSpeed = 0.002f; 在Update方法中接受自上一帧以来经过的时间为参数,检测模型移动的距离: float elapsedSeconds = (float)gameTime.ElapsedGameTime.Milliseconds / 1000.0f; float forwardReq= 0; float angleReq = 0; if (keyState.IsKeyDown(Keys.Up)) forwardReq += 1.0f; if(keyState.IsKeyDown(Keys.Down)) forwardReq -= 1.0f; if (keyState.IsKeyDown(Keys.Left))angleReq += 1.0f; if (keyState.IsKeyDown(Keys.Right)) angleReq -= 1.0f; 当模型向前加速时变量forwardReq为正,减速或向后加速时为负。变量angleReq表示模型左转还是右转。 在光滑表面上加速 下面的代码添加基本加速行为: Matrix rotMatrix = Matrix.CreateRotationY(angle); Vector3 forwardDir = Vector3.Transform(new Vector3(0, 0, -1), rotMatrix); velocity = velocity + elapsedTime * forwardReq *maxAccel *forwardDir; modelPosition += velocity; modelYRot += rotationReq * maxRotSpeed* velocity.Length(); 前两行代码计算模型当前的Forward矢量,这个矢量用来获得模型加速的方向。这个Forward向量是在向上y-轴上附加在默认的(0,0,-1) Forward方向上的旋转获得的。 注意:本教程中的模型只能绕y轴旋转,所以Forward向量只是基于绕向上y轴的旋转。可参加教程2-3和2-4学习计算整个3D空间或四元数的旋转。 接下来的代码计算速度。在前一个速度的基础上基于用户输入添加一个新的矢量。自上一帧经过的时间越长,越需要调整这个速度矢量。另外,加速度越大,越需要调整这个速度矢量。最后你还要考虑模型的最大加速度。将这三个因素相乘获得这一帧需要调整多少速度。因为你需要将一个Vector3添加到速度矢量上,要乘以带有forwardDir的这个值获取想要添加的矢量。 注意:如果没有旋转,速度和moveDirection会指向相同的方向,只是简单使速度矢量变大让模型移动地更快。 最后,这个Vector3添加到模型的位置,模型的旋转被调整。模型运动得越快,转向也越快。 当使用这个代码时,你注意到有两个缺点。首先,模型将一直加速,它没有一个最大速度。你想如图4-9所示增加速度并终止于一个确定的最大速度。使用这个代码,模型将一直以相同的步进加速。 第二,如果模型在一个方向上的速度很大,在旋转模型后,它可能仍在相同的方向。如果模型是在一个冰面上当然不错,但通常这并不是你想要的结果。 添加摩擦力 在真实情况中,模型的速度会因为模型与空气、表面之间的摩擦等慢慢减小。当你停止加速,摩擦力会让速度减小直至停止。当持续加速,摩擦力会导致速度增加到一个特定值不在增加。 你可以通过减去前面的速度获取摩擦力。下面的代码添加了摩擦力: velocity = velocity * (1 - friction * elapsedTime) + elapsedTime * forwardReq *maxAccel *forwardDir; 两帧之间的时间越长,摩擦力的效果越明显,所以你要根据流逝时间乘以friction变量。 让模型保持在向前方向运动 虽然模型现在已经以一个比较自然的方式加速了,但在它旋转时仍有一点瑕疵(除非是在空间游戏或冰面滑行游戏中)。通常,你只想让模型沿着前进方向移动。 你需要知道沿着Forward 方向上的速度矢量是多少。这可以通过点乘得到:它将Velocity (V) 矢量投影到Forward (F)上,如图4-10所示,并返回投影速度的大小。 图4-10 将速度矢量投影到Forward矢量上 所以在更新速度后使用下列代码: float forwardSpeed = Vector3.Dot(velocity, forwardDir); velocity = forwardSpeed * forwardDir; modelPosition += velocity * elapsedTime; modelYRot+= rotationReq * maxRotSpeed * forwardSpeed; forwardSpeed变量表示Velocity矢量在Forward方向上分量的大小,然后乘以Forward方向并将结果存储在一个新的Velocity矢量中。通过这个方法,你可以保证模型将沿着向前方向移动。 注意:使用forwardSpeed变量的一个额外好处是当模型向前运动是它为正,向后运动时它为负,而velocity. Length ()总是正的。 代码 Accelerate方法根据加速度调整模型的位置,速度和旋转,将它们和最大加速度和旋转速度相加。你还要获取用户输入和摩擦力变量。 private float Accelerate(ref Vector3 position, ref float angle, ref Vector3 velocity,float forwardReq, float rotationReq, float elapsedTime, float maxAccel, float maxRotSpeed,float friction) { Matrix rotMatrix = Matrix.CreateRotationY(angle); Vector3 forwardDir= Vector3.Transform(new Vector3(0, 0, -1), rotMatrix); velocity = velocity * (1- friction * elapsedTime) + elapsedTime * forwardReq * maxAccel *forwardDir; loat forwardSpeed = Vector3.Dot(velocity, forwardDir); velocity = forwardSpeed * forwardDir; modelPosition += velocity * elapsedTime; modelYRot += rotationReq * maxRotSpeed* forwardSpeed; return forwardSpeed; } 这个方法返回forwardSpeed变量,若模型向后运动则为负值。 扩展阅读 你可以通过允许乘以加速度施加在模型上扩展这个方法,例如,重力加速度,你可以加上这个加速度将总和作为forwardDir。

处理模型——找到对应一个方向的旋转角

clock 十二月 8, 2010 14:06 by author alex
问题 通过绘制模型前设置世界矩阵(见前一个教程)时,你可以在场景的任意位置放置模型。当你将模型从A点移至B点,你需要确保它转向正确的方向。 这个旋转角通常通过一个自然的方式获取,例如,当处理一个包含旋转的世界矩阵。但是,如果你只是存储模型的前一个位置和当前位置的方法移动模型的话,那么必须手动旋转这个模型。 解决方案 你需要首先计算模型运动的方向。从这个方向的X轴和Z轴开始,很容易找到这个角度。 工作原理 给定一个方向,你想知道需要旋转模型多少角度才能让它指向这个方向。给定如图4-8右边所示的方向,你想找到这个方向与Z轴的夹角。 如果你还记得三角形的基本知识那么这个解决方案是很简单的。给定如图4-8左边所示的三角形,你可以通过计算S除以L的反正切值(atan)得到a角,对于右图的情况,你可以通过计算方向的X分量除以Z分量的反正切值获取旋转角度。 图4-8 计算偏离给定方向的旋转角 atan方法返回的值在+90度和–90度之间(当两个负数相除时,负号会丢失)。 幸运的是,.NET Framework还提供了Atan2方法,这个方法可以让你分别提供X和Z值,返回在–180至180度范围内的值,让旋转角定义在完整的一个圆周内。 下面的简单方法计算对应模型当前位置和前一个位置的旋转角: private float CalculateRotation(Vector3 oldPos, Vector3 newPos, float lastRot) { Vector3 direction = newPos - oldPos; if ((direction.X == 0) &&(direction.Z == 0)) return lastRot; else return (float)Math.Atan2(direction.X,direction.Z); } 首先你找到旧位置指向新位置的方向用于计算对应旋转角。如果模型停止移动或垂直移动,这个方向的X和Z都是0,Atan2方法会返回一个无用的值,这时要返回上次的旋转值,这样不动的模型会保持它的旋转值。 用法 你应该使用这个计算出的旋转角创建一个绕向上的Y轴的旋转,这样模型的前方就会朝向运动的方向。当创建世界矩阵时,你应该首先平移模型然后进行旋转。这意味着你应该在矩阵乘法中将旋转放置在平移的左边(见教材4-2)。看一下下面的例子,模型最后的位置存储在position变量中,而angle变量是由CalculateRotation方法计算得到的: worldMatrix = Matrix.CreateTranslation(position); worldMatrix = Matrix.CreateRotationY(angle) * worldMatrix; worldMatrix = Matrix.CreateScale(0.005f) * worldMatrix; 在本例中,首先模型坐标被移动到模型中心的最终位置,然后施加绕Y轴的旋转,之后使模型变小。 代码 你可以在前面看到CalculateRotation方法的代码。

处理模型——对不同对象设置不同的世界矩阵,组合世界矩阵

clock 十二月 8, 2010 14:04 by author alex
问题 你想在绘制模型之前独立地移动、旋转并/或缩放它们。 解决方案 你可以设置对象的世界矩阵实现这个功能,而矩阵是可以存储任何变换(平移、旋转或缩放等操作)的对象。你可以简单地使用XNA框架提供的基本方法创建一个变换矩阵: Matrix.CreateTranslation Matrix.CreateScale Matrix.CreateRotationX-Y-Z 第一个方法创建一个平移矩阵,你可以定义将模型沿着X、Y和Z方向移动的距离。第二个方法让你可以缩放模型,第三个方法返回绕X、Y和Z轴旋转的矩阵。 你可以乘以这些矩阵组合多个变换,但必须按照正确的顺序,这会在本教程中讨论到。 工作原理 当将模型绘制到屏幕上时,会将它的初始位置放置在3D空间的(0,0,0)点上,而最常见的操作就是让模型在3D场景中移动。 这很简单。在Draw方法中定义一个时间变量代表程序开始以来经过的秒数(精确起见使用毫秒,然后除以1000)。然后定义一个矩阵保存沿X轴方向的平移,而平移量由当前时间决定: float time = (float)gameTime.TotalRealTime.TotalMilliseconds/1000.0f; Matrix worldMatrix = Matrix.CreateTranslation(time, 0, 0); 当将这个矩阵设置为世界矩阵时,可以让模型以每秒一个单位的速度沿x方向移动。下面的代码使用这个矩阵绘制模型: myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.GetViewMatrix(); effect.Projection = fpsCam.GetProjectionMatrix(); } mesh.Draw(); } 要让模型中的所有部分都在正确的位置,需要使Bone变换作为世界矩阵的一部分 (可见教程4-9学习bone变换的更多知识)。但这次,你通过将bone变换矩阵与平移矩阵相乘将两者组合起来,一定要将世界矩阵放置在乘式的右边,因为在矩阵乘法中顺序是很重要的。当你运行这个代码时,模型会缓缓地沿着x轴运动。 对于缩放和旋转方法类似。试着使用下面得世界矩阵: Matrix worldMatrix = Matrix.CreateScale(time); 开始时time = 0,模型看不见,随后慢慢变大。一秒后达到原始大小,之后会继续变大。接下来使用这个矩阵: Matrix worldMatrix = Matrix.CreateRotationY(time); 模型会持续地绕y轴旋转。 组合多个变换 通常情况下,你会组合多个变换作为世界矩阵。例如,你想将一个角色移动到一个新位置,将它旋转到行走的方向并对它进行缩放。 因为这是一个小例子,所以使用下列世界矩阵: Matrix worldMatrix = Matrix.CreateTranslation(time, 0, 0) * Matrix.CreateRotationY(time/10.0f); 这个组合矩阵将作为模型的世界矩阵。但是你可以用另一种方式组合这两个矩阵: Matrix worldMatrix = Matrix.CreateRotationY(time / 10.0f) * Matrix.CreateTranslation(time, 0, 0); 当你运行程序后会发现结果是不同的。前面已经提到,在矩阵数学中,乘法的顺序是很重要的,接下来我将讨论这个问题。 矩阵乘法的顺序 在矩阵数学中,将矩阵M1乘以矩阵M2的结果通常与将M2乘以M1的结果是不同的,在下面的章节中,我会讨论所有可能的组合。 有一个规则(或者说是技巧)你必须记得:在矩阵乘法中,M1*M2就是“M1在M2之后”的意思。 旋转与另一个旋转的组合 旋转矩阵乘法的顺序是很重要的,这是因为当你首先绕A1轴旋转然后绕A2轴旋转,那么绕A1轴的旋转会在绕A2轴旋转前改变A2轴! 例如,M1表示绕向右轴旋转90度,M2表示绕向上轴旋转90度。 首先看一下M1*M2的情况,它的意思是“在绕向上轴之后绕向右轴旋转。”想一下结果会如何。在这种情况中,你的右手臂始终对着向右方向。你首先绕向上轴旋转90度,这样你会面向左方,如图4-2左图所示。当你看一下右手臂,你会发现它跟着你一起旋转!所以当你绕向右轴旋转90度后,你会面朝下躺倒在地,如图4-2的右图所示。 图4-2 先绕Up轴旋转后绕Right轴旋转 下面看一下第二种情况M2*M1,表示“在绕Right轴旋转后绕Up轴旋转。”你首先绕你的向右方向旋转,结果是面朝下,如图4-3的左图所示。你的向上方向变成了水平,即世界坐标系的向前方向。然后当你绕这个向前方向旋转时,结果是你面朝侧面,如图4-3的右图所示! 图4-3 先绕Right轴旋转后绕Up轴旋转 如你所见,M1*M2和M2*M1会导致不同的结果,这是因为当组合两个旋转时,第二个旋转轴会受到第一个旋转地影响。 旋转与平移的组合 在这种情况中,乘法的顺序仍是重要的。在这个例子中,M3表示绕Up轴旋转90度,M4表示沿x轴方向移动10个单位。 M3*M4表示“平移后旋转,”模型会首先移动到新位置。然后绕Up轴旋转,这两步如图4-4所示。 图4-4 先平移后旋转 M4*M3的情况不同。首先整个坐标系(包括模型和x轴)绕Up轴旋转90度,如图4-5左图所示。然后,模型沿着旋转过的x轴移动10个单位。但初始的x轴已经旋转了,本来它位于你的右方,但现在在你的前方!这意味着你实际上是沿世界坐标系向前的z轴在做平移。 图4-5 先旋转后平移 缩放与平移的组合 缩放和平移的乘法顺序还是重要的。本例中。M5表示缩放0.5倍,M6表示沿x轴平移10个单位。 M5*M6表示先平移后缩放。所以首先模型向右沿x轴平移10个单位,然后缩小,如图4-6所示。 图4-6 先平移后缩放 M6*M5的情况中你首先将整个坐标系(包括模型和x轴)缩小,然后缩小的模型沿着缩小的x轴移动10个单位。因为x轴也缩小了一半,所以只移动了相当于5个单位!结果是模型移动的不够远,如图4-7所示。 图4-7 先缩放后平移 安全的组合 幸运的是,有些变换无需考虑乘法的顺序。例如,组合两个平移矩阵是安全的,因为模型只是简单地移动了两次。两个缩放变换的组合也是安全的。 例如,先放大2倍再缩小10倍和先缩小10倍再放大2倍结果是一样的。最后,缩放不影响旋转,反之亦然。这是因为只会缩放坐标轴,但轴之间的角度仍保持90度不变。所以当缩放矩阵和旋转矩阵相乘时,你可以不用考虑乘法的顺序。 译者注:在《Microsoft XNA Game Studio Creator’s Guide》一书的第5章提到一个小技巧:使用I.S.R.O.T.作为矩阵组合的顺序通常就是你想要的结果,此处I.S.R.O.T. 分别代表Identity,Scale,Revolve,Orbit,Translate。 代码 下面的代码组合了旋转和平移矩阵: //draw model float time = (float)gameTime.TotalRealTime.TotalMilliseconds/1000.0f; Matrix worldMatrix = Matrix.CreateScale(0.005f)* Matrix.CreateRotationY(time / 10.0f) * Matrix.CreateTranslation(time, 0, 0); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.ViewMatrix; effect.Projection = fpsCam.ProjectionMatrix; } mesh.Draw(); }

处理模型——使用BasicEffect类载入模型

clock 十二月 8, 2010 14:00 by author alex
问题 因为手动定义一个复杂3D对象的所有点几乎是不可能的,所以这些3D对象应该是由艺术家在3D建模程序中制作,模型可以保存为一个文件。你想从文件加载模型并在场景中绘制这个模型。 解决方案 XNA Framework已经包含了所需方法。XNA提供了一个默认的Model素材管道可以从.x和.fbx文件加载模型。如教程3-1的图片所示,你只需将模型拖动到Solution Explorer的Content文件夹即可,然后将它设置为XNA代码中的一个变量。这个Model变量包含绘制模型的方法。 工作原理 首先将. x或. fbx文件导入到XNA Game Studio中。你可以通过在Windows Explorer中选中这个文件将它拖入到XNA Game Studio的Solution Explorer的Content文件夹中。也可以右击Content文件夹选择Add Existing Item,如图4-1所示,然后浏览到. x或. fbx文件并选择它。 注意:当你点击新添加的模型文件时,在Solution Explorer的右下角Properties框中会显示XNA使用默认的模型导入器和处理器。可见教程4-12至4-16获取如何使用自定义的模型处理器的知识。 添加好模型后,你可以将下面的变量添加到代码中: Model myModel; 图4-1 将一个模型添加到项目中 然后将模型绑定到这个变量。这一步适合在LoadContent方法中进行: myModel = Content.Load("tank"); 注意:这个例子中素材的名称是tank,默认就是模型文件的没有扩展名的名称。你也可以通过选择Solution Explorer中的源文件改变Asset Name属性。 现在你加载了模型,可以通过在Draw 方法中添加以下代码绘制它了,这是将模型绘制到场景所需的所有代码,本教程后面会介绍此代码的背景知识。 //draw model modelTransforms = new Matrix[myModel.Bones.Count]; Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.ViewMatrix; effect.Projection = fpsCam.ProjectionMatrix; } } mesh.Draw(); 模型的结构 在大多数情况下,你可以旋转和移动模型的一部分,例如,移动人的手臂。要能实现这个功能,模型应该要被分成几个成员。对每个成员,你需要知道两件事: 几何数据:你想知道顶点,顶点包含了组成模型成员的所有三角形顶点的信息,这些信息包括位置,颜色,法线等。 成员与它们的父成员如何连接:以人的手臂为例,你要指定手臂是连接在肩膀上的。 每个成员的几何数据以ModelMesh对象的形式存储,而ModelMesh对象是在Model的 ModelMeshCollection中,在这个对象你可以找到它的Meshes属性。成员的位置数据以Bone对象的形式存储,而Bone对象在Model的ModelBoneCollection中,在这个对象中你可以找到模型的Bones集合。每个ModelMesh对象包含指向Bone对象的引用,而一个Bone对象包含指向父Bone的引用,它必须连接到这个父Bone。通过这种方式,你可以将所有Bone对象连接到一起,具体细节可见教程4-8和4-9。在教程4-9中,你可以找到CopyAbsoluteBoneTransformsTo方法的解释。 ModelMeshes和Bones 一个ModelMesh包含模型的一个成员的几何信息,这个成员无法再分成更小的成员。例如,一台笔记本电脑不是一个好的ModelMesh,因为你想翻起/关闭液晶屏,打开/关闭DVD托盘。好的办法是对电脑底座使用一个ModelMesh,液晶屏使用第二个ModelMeshcreen,而DVD托盘使用第三个ModelMesh。 因为所有的ModelMesh都需要他连接到一个Bone上,下一步你的三个ModelMeshes还需要三个Bone。你需要将连接到电脑底座的ModelMesh的Bone作为root Bone,因为电脑底座可以看成电脑的初始位置。连接到液晶屏ModelMesh的表示与底座连接的位置。同理连接DVD托盘的ModelMesh的Bone也要指向root Bone,表示托盘相对于底座的位置。 ModelMeshes和ModelMeshParts 你已经为液晶屏定义了一个ModelMesh和对应的Bone,这样很完美,因为液晶屏不包含可动部分。但是,你可能还想使用一个固定纹理的effect绘制液晶屏的塑料外壳,使用另一个effect绘制LCD,例如,这个效果会从另一张纹理采样颜色或你还想添加一点反光效果。 这就需要用到ModelMeshParts。每个ModelMesh可以包含多个ModelMeshParts,每个ModelMeshParts可以使用不同的纹理,材质或effect进行绘制。这意味着每个ModelMeshPart 都包含各自的几何数据和对应几何数据的effect。 注意:如果你对effect不了解,可以把它们想象成像素的颜色定义,像素是否反光,是否透明,是否从一张图像中获取颜色?可见第6章的例子学习像素如何正确地与光线作用。教程3-13包含了从图像获取颜色的例子。 两个循环嵌套 Model的结构解释了为什么我们需要在绘制模型时使用两个循环。首先,你遍历Model的ModelMeshes,每个ModelMesh包含一个或多个ModelMeshPart,每个ModelMeshPart都有自己的effect。所以,你需要第二个循环遍历当前ModelMesh的所有ModelMeshPart,设置各自effect的参数。但是如果多个ModelMeshParts使用同样的effect,这会导致同样的effect被设置了两次,显然只是浪费时间。要避免这种情况,ModelMesh会通过它的ModelMeshPart保存所有独立的effect,在第二个循环中使用这个effecf集合。 最后,在设置好ModelMesh的所有effect后,你调用ModelMesh对象的Draw方法,这会使用ModelMesh对象的所有ModelMeshParts使用各自的effect进行绘制。 注意:教程4-8的例子中遍历了ModelMesh的ModelMeshParts,而不是effects。 小小的性能提升 前面的代码在每次调用Draw方法时都会重新实例化Bones数组。更好的办法是在加载模型后只实例化并填充数组一次。所以,将以下代码放置到LoadContent方法中: myModel = content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; 代码 首先在LoadContent方法中加载Model并初始化Bone数组: protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; } 然后在Draw 方法中绘制模型: protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); //draw model Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.ViewMatrix; effect.Projection = fpsCam.ProjectionMatrix; } mesh.Draw(); } base.Draw(gameTime); }

友情链接赞助