4.17 根据地形正确倾斜模型

问题

当在地形上移动一个汽车模型时,使用教程4-2你可以调整车的高度,使用教程5-9你可以找到位于汽车下面的地形的高度。但是,如果你没有根据车下面的坡度正确使车身发生倾斜,那么在起伏不平的地形上效果看起来不会很好。

你想正确地放置和倾斜汽车模型使之可以匹配地形的起伏。

解决方案

这个问题可以分成四个部分:

  1. 首先,你想找到模型四个轮胎的最低顶点的位置。
  2. 其次,你想获取这四个顶点之下的地形的高度。
  3. 下一步,你想获取模型沿Forward和Side向量的旋转以正确地倾斜模型。
  4. 最后,你需要找到模型和地形之间的高度差并补偿这个差异。

要做到第一步,你可以编写一个自定义模型处理器,这个处理器可以在每个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<Vector3> lowestVertices = new List<Vector3>(); 
    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

图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 获取倾斜角度

计算旋转角度

现在知道了在地形上的高度差,可以使用三角函数计算倾斜角度了。在直角三角形中,如果你知道了一个锐角的对边(图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属性中存储轮子的靠外边的底部,或在方法中手动指定一个。

程序截图


发布时间:2009/9/4 上午7:35:00  阅读次数:6916

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号