问题
当在地形上移动一个汽车模型时,使用教程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 ListFindLowestVectors(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; }
首先对子节点调用这个方法,这样也在集合中存储了它们最低点的位置。
对于每个节点,你检查节点是否包含几何信息。如果包含,你将遍历所有顶点。如果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属性中存储轮子的靠外边的底部,或在方法中手动指定一个。