• 创建场景和赛道——赛道


    赛道

    除了有HUD这个游戏并没有真正看起来像一个赛车游戏,在发光和颜色修正post-screen shaders作用下更像是一个幻想角色扮演,没有赛道和赛车使它看起来不像一个赛车游戏。单单把车放在场景中看起来挺有趣,但你不想在地面上驾驶,尤其是场景看起来不那么好(1场景纹纹理像素2×2米,使整个车放置在2个纹理像素上)。

    这个游戏的构思是制作一些像赛道狂飙游戏类似的赛道,但经过研究赛道狂飙和游戏中的编辑器后,你会看到游戏中的绘制过程是如此的复杂。我把赛道作为一个整体渲染,而赛道狂飙的关卡是由许多不同的赛道块构成的,相互间配合完美,通过这种方式,你可以把三个轨道环相互连接而不需要自己绘制或创造它。这种方法的缺点是,可用的赛道块有限,从开发者的角度来看,创造数以百计的这些赛道块要做大量的工作,更别说要在所有关卡中测试它们。

    因此,这一构思被放弃。我回到我原来的做法,只是创造一个简单的二维赛道并通过场景高程图增加了一点高度。我想使用一张绘制有赛道的位图,在位图上以一条白线作为赛道,然后从位图上提取位置信息和创建三维顶点并将其导入到游戏中。但尝试后发现这个方法使赛道紧贴在场景地面上,要实现轨道环,坡道,疯狂的曲线是不可能的,或者至少很难实现。

    因此,这个构思再次被放弃。为了更好地了解赛道看起来如何我使用了3D Studio Max,通过spline函数建立一个仅有4个点的简单循环轨道(见图12 -12)。旋转90度到左侧,这看起来很像赛道,比位图的方法更吸引人。

    1
    图 12-12

    我得把这些spline数据从3D Studio Max中导出并插到我的引擎中,这样我就可以在3D Studio Max中创建赛道并将它导入到赛车游戏引擎中。困难的部分是从spline数据中产生一条可用的赛道,因为每个spline点只是一个点,而不是一片有方向有宽度的道路。

    在花更多的时间试图找出最好的方式来产生赛道,将spline数据导入到你的游戏前,你应该确保这一想法可行。

    单元测试

    这个游戏再次进行了一些繁重的单元测试。由新的TrackLine类中的第一个叫做TestRenderingTrack的单元测试开始,它只是创建了一个简单的如3D Studio Max中类似的曲线,并把它显示在屏幕上:

    public static void TestRenderingTrack()
    {
       TrackLine testTrack = new TrackLine(
        new Vector3[]
        {
          new Vector3(20, -20, 0),
          new Vector3(20, 20, 0),
          new Vector3(-20, 20, 0),
          new Vector3(-20, -20, 0),
        });
    
      TestGame.Start(
        delegate
        {
          ShowGroundGrid();
          ShowTrackLines(testTrack);
          ShowUpVectors(testTrack);
        });
    } // TestRenderingTrack()

    ShowGroundGrid方法只是在xy平面上显示一些网格线以帮助你知道地面在哪。之后我在model类中写了这个方法,能够被重复使用。ShowTrackLines是最重要的方法,因为它显示了所有的线条和插值点,这些线条和插值点已在TrackLine类的构造函数中生成。最后ShowUpVectors方法告诉向量的向上方向供赛道每个点使用。没有向上向量,你将无法正确地生成道路的左右两侧。例如,在曲线赛道上应该倾斜、在循环赛道上你需要向上向量指向圆轨道圆心,而不只是向上。

    ShowTrackLines辅助方法显示赛道的每个点,它们之间通过白色直线连接。当你执行TestRenderingTrack单元测试后就可以看到如图12-13的画面。

    public static void ShowTrackLines(TrackLine track)
    {
      // Draw the line for each line part
      for (int num = 0; num < track.points.Count; num++)
        BaseGame.DrawLine(
          track.points[num].pos,
          track.points[(num + 1)%track.points.Count].pos,
          Color.White);
    } // ShowTrackLines(track)

    2
    图 12-13

    借助于红色的向上向量和绿色的切线向量,这条赛道看起来有点像公路了。你现在要做的是调整赛道生成代码,测试更多的放样线条。在TrackLine类中你可以看到我的几个测试赛道,这些赛道是通过手工添加一些3D点创建的,更多的赛道可通过使用Collada文件实现,这时要将3D Studio Max中的赛道数据导入到你的引擎,这在接下去会讨论到。

    在你查看构造函数中的放样线条插值代码前,你也可以创建一个简单的循环赛道,只需转换赛道顶点的x和z值(见图12-14 )。为了使样条看起来更圆我还补充四个新的点。新的TestRenderingTrack单元测试如下所示:

    public static void TestRenderingTrack()
    {
      TrackLine testTrack = new TrackLine(
        new Vector3[]
        {
          new Vector3(0, 0, 0),
          new Vector3(0, 7, 3),
          new Vector3(0, 10, 10),
          new Vector3(0, 7, 17),
          new Vector3(0, 0, 20),
          new Vector3(0, -7, 17),
          new Vector3(0, -10, 10),
          new Vector3(0, -7, 3),
        });
      // [Rest stays the same]
    } // TestRenderingTrack()

    3
    图 12-14

    插值样条

    你可能会问如何只通过在4个或8个输入点获得所有这些点和这些点如何才能插值得更好。这一切都发生在TrackLine的构造函数中,或更准确地说,应该是在Load方法中,它可以让你在需要重新生成时重新载入数据。第一次看到Load方法时,你会觉得不很容易,它是加载所有赛道数据、验证赛道数据、插值和生成向上和切线向量的主要方法。隧道和场景物体也在这里生成。

    Load方法做了以下事情:

    • 它允许重新载入,这对载入和重新开始关卡是非常重要的。如果你再次调用Load方法,以往的数据会自动被清除。

    • 验证所有数据以确保你可以产生赛道并使用所有辅助类。

    • 检查赛道上的每一点看看它是否在场景之上。如果没有,该点会被纠正,而且周围的点也会稍微上升少许以使赛道看起来更光滑。通过这种方式,你可以轻松地在Max中生成一个三维赛道,而当将赛道放置在场景之上时,就无需担心场景的实际高度。

    • 圆轨道被简化为上下两个取样点。加载代码会自动检测这两个点并用完整循环的九个点取代它们,这样插入更多的点以产生非常光滑和正确的圆轨道。

    • 然后,所有赛道上的点通过Catmull-Rom插值方法被插值。你马上就会看到这种方法。

    • 向上和切线向量会生成并插值好几次以使道路尽可能平滑。切线向量尤其不应该突然改变方向或翻转到另一边,这将使得在这条道路上开车变得非常困难。这个代码我花费了最长的时间才使之能工作正常。
    • 然后,所有分析所有辅助类和对应赛道上的每一个点的道路宽度被储存,以便接下去使用,实际渲染发生在Track类,它是基于TrackLine类的。

    • 道路纹理的纹理坐标也在这里生成,因为你将所有赛道点的信息存储在TrackVertex数组中,这样可以使接下去的渲染更容易。只有u纹理坐标是储存在这里的,而v纹理坐标在后来只是被设置为0或1,这取决于你是在道路的左边还是右边。

    • 然后,分析隧道辅助类并生成隧道数据。这里的代码只是构建了一些新的点供以后使用。它们被用来在Track类中绘制带有隧道纹理的隧道盒。
    • 最后所有的场景模型被添加。他们和赛道数据一起被保存为一个完整的关卡。附加的场景物体也在Track类中自动生成,例如,路边的路灯等。

    当我开始编写TrackLine类时,构造函数只能通过Catmull-rom spline辅助方法从输入点中生成新的插值点。该代码看上去如下,在Load方法中也能找到类似代码:

    // Generate all points with help of catmull rom splines
    for (int num = 0; num < inputPoints.Length; num++)
    {
      // Get the 4 required points for the catmull rom spline
      Vector3 p1 = inputPoints[num-1 < 0 ? inputPoints.Length-1 : num-1];
      Vector3 p2 = inputPoints[num];
      Vector3 p3 = inputPoints[(num + 1) % inputPoints.Length];
      Vector3 p4 = inputPoints[(num + 2) % inputPoints.Length];
    
      // Calculate number of iterations we use here based
      // on the distance of the 2 points we generate new points from.
      float distance = Vector3.Distance(p2, p3);
      int numberOfIterations =
        (int)(NumberOfIterationsPer100Meters * (distance / 100.0f));
      if (numberOfIterations <= 0)
        numberOfIterations = 1;
    
      Vector3 lastPos = p1;
      for (int iter = 0; iter < numberOfIterations; iter++)
      {
        TrackVertex newVertex = new TrackVertex(
          Vector3.CatmullRom(p1, p2, p3, p4,
          iter / (float)numberOfIterations));
    
        points.Add(newVertex);
      } // for (iter)
    } // for (num)
    更复杂的赛道

    单元测试已经能让一切都启动和运行了,但赛道越复杂,通过输入每个样点的3D位置产生赛道就更难。要让事情变得简单点,可以使用从3D Max Studio中导出的spline,这可以让创建和修改spline变得更容易。

    看看如图12-15所示的XNA Racing游戏中专家关卡的赛道。这条赛道仅包含约85个点,插值到2000个点使赛道约有24000个多边形。赛道围栏和额外的赛道物体在以后生成。构建这样的一条赛道并调整它,如果没有一个好的编辑器几乎是不可能的,不过幸好有3D Max。也许将来我会为这个游戏制作一个赛道编辑器,至少能让你在游戏中直接创建简单的赛道。

    4
    图 12-15

    我最初认为导出这种数据并不容易。.x档案不支持spline,.fbx文件也不行。即使他们能导出spline,你仍需要做大量的工作从赛道中提取数据,因为在XNA中从导入的模型中获得顶点数据是不可能的。我决定使用目前非常流行的Collada格式,这种格式允许在不同的应用程序间互相导入导出3D数据。相比其他格式,Collada的主要优势是一切都存储为XML格式,从导出的文件上你可以很容易看出哪个数据对应哪个功能。你甚至不需要寻找任何文件,只需寻找你需要的数据并提取它(在这里,你只需寻找spline和辅助数据,其余的对你并不重要)。

    对游戏来说Collada不是一个真正优秀的导出格式,因为它通常储存了太多的信息,而且XML数据仅仅是一堆文字,所以比起二进制文件,Collada文件的尺寸也大得多,出于这个理由,而且我也不能在XNA Starter Kit中使用任何外部数据格式,所有的Collada数据在TrackImporter类中被转换为内部数据格式。使用自己的数据格式加快了加载过程,并确保没有人能创建自己的赛道。嘿,等一下,你不希望别人创建自己的赛道吗?我的确希望这变得更容易,你需要3D Studio Max才能创建或改建赛道并不好。我必须在以后实现某种方法可以导入和创建赛道。

    导入赛道数据

    为了使装载Collada文件变得容易些使用了一些辅助类。首先,XmlHelper类(见图12-16 )能帮你加载和管理XML文件。

    5
    图 12-16

    ColladaLoader类只是一个很短的类,它加载Collada文件(只是一个xml文件),让使用XmlHelper方法的派生类更容易。

    • ColladaTrack是用来加载赛道本身(trackPoints),其他辅助对象如widthHelpers可使赛道变宽和变窄,roadHelpers用于隧道,棕榈树,路灯等路边的物体。最后所有的场景物体在你接近他们时被显示(因为在场景中有大量的物体)。
    • ColladaCombiModels是一个小的辅助类,它用于一次加载并显示多个模型,只需设置一个包含多达10个模型的组合模型,这十个模型有不同的位置和旋转值。例如,如果你想放置一个具有建筑物的城市区域,只需使用Buildings.CombiModel文件,如果你需要一些棕榈树外加几块石头可使用Palms.CombiModel文件。

    想对加载过程了解得更多,可以使用TrackLine和Track类中的单元测试,但更重要的查看ColladaTrack构造函数本身:

    public ColladaTrack(string setFilename)
      : base(setFilename)
    {
      // Get spline first (only use one line)
      XmlNode geometry =
        XmlHelper.GetChildNode(colladaFile, "geometry");
      XmlNode visualScene =
        XmlHelper.GetChildNode(colladaFile, "visual_scene");
    
      string splineId = XmlHelper.GetXmlAttribute(geometry, "id");
      // Make sure this is a spline, everything else is not supported.
      if (splineId.EndsWith("-spline") == false)
        throw new Exception("The ColladaTrack file " + Filename +
          " does not have a spline geometry in it. Unable to load " +
          "track!");
    
      // Get spline points
      XmlNode pointsArray =
        XmlHelper.GetChildNode(geometry, "float_array");
      // Convert the points to a float array
      float[] pointsValues =
        StringHelper.ConvertStringToFloatArray(
        pointsArray.FirstChild.Value);
    
    // Skip first and third of each input point (MAX tangent data)
    trackPoints.Clear();
    int pointNum = 0;
    while (pointNum < pointsValues.Length)
    {
      // Skip first point (first 3 floating point values)
      pointNum += 3;
      // Take second vector
      trackPoints.Add(MaxScalingFactor * new Vector3(
        pointsValues[pointNum++],
        pointsValues[pointNum++],
        pointsValues[pointNum++]));
      // And skip thrid
      pointNum += 3;
    } // while (pointNum)
    
    // Check if we can find translation or scaling values for our
    // spline
    XmlNode splineInstance = XmlHelper.GetChildNode(
      visualScene, "url", "#" + splineId);
    XmlNode splineMatrixNode = XmlHelper.GetChildNode(
      splineInstance.ParentNode, "matrix");
    if (splineMatrixNode != null)
      throw new Exception("The ColladaTrack file " + Filename +
        " should not use baked matrices. Please export again " +
        "without baking matrices. Unable to load track!");
      XmlNode splineTranslateNode = XmlHelper.GetChildNode(
        splineInstance.ParentNode, "translate");
      XmlNode splineScaleNode = XmlHelper.GetChildNode(
        splineInstance.ParentNode, "scale");
      Vector3 splineTranslate = Vector3.Zero;
      if (splineTranslateNode != null)
      {
        float[] translateValues =
          StringHelper.ConvertStringToFloatArray(
          splineTranslateNode.FirstChild.Value);
        splineTranslate = MaxScalingFactor * new Vector3(
          translateValues[0], translateValues[1], translateValues[2]);
      } // if (splineTranslateNode)
      Vector3 splineScale = new Vector3(1, 1, 1);
      if (splineScaleNode != null)
      {
        float[] scaleValues = StringHelper.ConvertStringToFloatArray(
          splineScaleNode.FirstChild.Value);
        splineScale = new Vector3(
          scaleValues[0], scaleValues[1], scaleValues[2]);
      } // if (splineTranslateNode)
    
      // Convert all points with our translation and scaling values
      for (int num = 0; num < trackPoints.Count; num++)
      {
        trackPoints[num] = Vector3.Transform(trackPoints[num],
          Matrix.CreateScale(splineScale) *
          Matrix.CreateTranslation(splineTranslate));
      } // for (num)
    
      // [Now Helpers are loaded here, the loading code is similar]
    } // ColladaTrack(setFilename)

    获取spline数据本身并不是很难,但获取移动,缩放,旋转值要多费些功夫(辅助类也更复杂),但在你编写和测试了此代码后(有几个单元测试和测试文件被用来实现这一构造函数),创建新的赛道并将它们导入到游戏中是很容易的。

    从赛道数据生成顶点

    获取赛道数据和导入辅助数据只完成了一半工作。你已经看到TrackLine类的构造函数是多么复杂了,它帮你产生插值点,并建立向上和切线向量。纹理坐标和所有辅助和场景模型也在这里处理。但是你现在仍然只有一大堆点,并没有一条真正的道路让你的车可以行使其上。为绘制一条具有纹理的真正的道路(见图12-17),你需要首先为所有的三维数据创建顶点,并最终生成道路,还要包括其他动态创建的对象,如护栏。最重要的纹理是道路本身,但没有法线贴图游戏看起来有点枯燥。法线贴图给道路添加了一个闪闪发光的结构,使道路在面向太阳时有光泽。道路两旁的纹理、背景(RoadBack.dds)和隧道(RoadTunnel.dds)也很重要,但你不会经常看到它们。

    6
    图 12-17

    TrackLine类处理所有这些纹理,有道路材质,道路水泥柱、护栏、检查站等,它是基于,它从Track类继承而来的。Landscape类用来绘制赛道和所有场景物体以及场景本身,最后才能使汽车在道路上行驶。你还需要物理学处理在赛道上的运动、与护栏的碰撞,这在下一章会说到。

    Track类负责所有道路材质,生成所有顶点以及索引缓冲,并最终在shader的帮助下渲染所有的赛道顶点。大多数材质使用NormalMapping中的Specular20技术产生一个有光泽的道路,但对隧道和其他非光泽道路材质,应使用Diffuse20技术。

    绘制赛道的单元测试很简单,所有你想做的事就是绘制赛道。

    public static void TestRenderTrack()
    {
      Track track = null;
      TestGame.Start(
        delegate
        {
          track = new Track("TrackBeginner", null);
        },
        delegate
        {
          ShowUpVectors(track);
          track.Render();
        });
    } // TestRenderingTrack()

    如你所见你仍然使用TrackLine类中的ShowUpVectors辅助方法,因为你是从Track类中继承而来的。Render方法也类似于前一章Mission类中的场景渲染的方法。

    public void Render()
    {
      // We use tangent vertices for everything here
      BaseGame.Device.VertexDeclaration = TangentVertex.VertexDeclaration;
      // Restore the world matrix
      BaseGame.WorldMatrix = Matrix.Identity;
    
      // Render the road itself
      ShaderEffect.normalMapping.Render(
        roadMaterial, "Specular20",
        delegate
        {
          BaseGame.Device.Vertices[0].SetSource(roadVb, 0,
            TangentVertex.SizeInBytes);
          BaseGame.Device.Indices = roadIb;
          BaseGame.Device.DrawIndexedPrimitives(
            PrimitiveType.TriangleList,
            0, 0, points.Count * 5,
            0, (points.Count - 1) * 8);
        });
    
      // [etc. Render rest of road materials]
    } // Render()

    嗯,看来并不十分复杂。看一下生成的道路顶点和索引缓冲的代码。私有辅助类GenerateVerticesAndObjects执行上述操作:

    private void GenerateVerticesAndObjects(Landscape landscape)
    {
      #region Generate the road vertices
      // Each road segment gets 5 points:
      // left, left middle, middle, right middle, right.
      // The reason for this is that we would have bad triangle errors if the
      // road gets wider and wider. This happens because we need to render
      // quads, but we can only render triangles, which often have different
      // orientations, which makes the road very bumpy. This still happens
      // with 8 polygons instead of 2, but it is much better this way.
    // Another trick is to not do so many iterations in TrackLine, which
    // causes this problem. Better to have a not so round track, but at
    // least the road up/down itself is smooth.
    // The last point is duplicated (see TrackLine) because we have 2 sets
    // of texture coordinates for it (begin block, end block).
    // So for the index buffer we only use points.Count-1 blocks.
    roadVertices = new TangentVertex[points.Count * 5];
    
    // Current texture coordinate for the roadway (in direction of
    // movement)
    for (int num = 0; num < points.Count; num++)
    {
      // Get vertices with the help of the properties in the TrackVertex
      // class. For the road itself we only need vertices for the left
      // and right side, which are vertex number 0 and 1.
      roadVertices[num * 5 + 0] = points[num].RightTangentVertex;
      roadVertices[num * 5 + 1] = points[num].MiddleRightTangentVertex;
      roadVertices[num * 5 + 2] = points[num].MiddleTangentVertex;
      roadVertices[num * 5 + 3] = points[num].MiddleLeftTangentVertex;
      roadVertices[num * 5 + 4] = points[num].LeftTangentVertex;
    } // for (num)
    
    roadVb = new VertexBuffer(
      BaseGame.Device,
      typeof(TangentVertex),
      roadVertices.Length,
      ResourceUsage.WriteOnly,
      ResourceManagementMode.Automatic);
      roadVb.SetData(roadVertices);
    
    // Also calculate all indices, we have 8 polygons for each segment
    // with 3 vertices each. We have 1 segment less than points because
    // the last point is duplicated (different tex coords).
    int[] indices = new int[(points.Count - 1) * 8 * 3];
    int vertexIndex = 0;
    for (int num = 0; num < points.Count - 1; num++)
    {
      // We only use 3 vertices (and the next 3 vertices),
      // but we have to construct all 24 indices for our 4 polygons.
      for (int sideNum = 0; sideNum < 4; sideNum++)
      {
        // Each side needs 2 polygons.
        // 1. Polygon
        indices[num * 24 + 6 * sideNum + 0] = vertexIndex + sideNum;
        indices[num * 24 + 6 * sideNum + 1] =
          vertexIndex + 5 + 1 + sideNum;
        indices[num * 24 + 6 * sideNum + 2] = vertexIndex + 5 + sideNum;
    
        // 2. Polygon
        indices[num * 24 + 6 * sideNum + 3] =
          vertexIndex + 5 + 1 + sideNum;
        indices[num * 24 + 6 * sideNum + 4] = vertexIndex + sideNum;
        indices[num * 24 + 6 * sideNum + 5] = vertexIndex + 1 + sideNum;
      } // for (num)
        // Go to the next 5 vertices
        vertexIndex += 5;
      } // for (num)
    
      // Set road back index buffer
      roadIb = new IndexBuffer(
        BaseGame.Device,
        typeof(int),
        indices.Length,
        ResourceUsage.WriteOnly,
        ResourceManagementMode.Automatic);
        roadIb.SetData(indices);
      #endregion
    
      // [Then the rest of the road back, tunnel, etc. vertices are
      // generated here and all the landscape objects, checkpoints, palms,
      // etc. are generated at the end of this method]
    } // GenerateVerticesAndObjects(landscape)

    在编写这个代码时我写了很多注释。第一部分生成一个很大的切线数组,数组大小是TrackLine类中的赛道顶点的5倍。此数据直接传递到顶点缓冲区,然后被用于构造多边形的索引缓冲区。每个道路片有8个多边形(由四部分组成,每部分两个多边形),因此该索引缓冲区大小是赛道顶点索引的24倍。为了确保仍然能够正确使用所有这些索引,必须使用int类型替代short类型,以前我使用short类型是因为这样做能节省一半内存。但在这种情况下有超过32000个索引( 专家关卡的赛道有2000个道路片,它的24倍已达到48000个索引)。因为赛道是自动生成而不是手工产生,所以你需要许多迭代点,如果你没有足够的迭代点会导致重叠错误,这样就没法使赛道足够圆滑(见图12-18)。

    7
    图 12-18

    你可能会问,为什么是四个部分产生每个道路片,原因不是因为我喜欢让低档的GPU处理很多多边形。这项技术是用来改善赛道的视觉效果的,特别是在曲线的情况下。

    图12-19能更好地解释这个问题。如你所见,构成不平行的方块的两个多边形并不总是大小相同的,但它们仍然使用同样数量的纹理像素。在右边你可以看到一个极端的情况下,道路的右下角部分严重扭曲,不看好了。

    8
    图 12-19

    这个问题可通过将道路分成多个部分加以解决。你可以将道路片分成四个部分,这样做道路看起来好多了。

    最后结果

    让场景和道路正确显示要做大量的工作,但现在你已做得不错了,至少图形部分不错。你可以在Track命名空间下的类中看到许多小窍门和技巧。请查看单元测试以了解更多关于如何绘制道路的两旁、圆轨道和隧道的知识。

    图12-20显示了Track类中的TestRenderTrack单元测试的最终结果。

    9
    图 12-20

    和本章第一部分的场景渲染整合在一起,你就有了一个相当不错渲染引擎。加上背景的post-screen shader天空盒,场景和道路渲染看起来相当不错(见图12-21)。Post-screen glow shader也使一切都配合得更好,尤其是在场景中有很多物体的情况下。

    10
    图 12-21

  • 相关阅读:
    linux strace 命令详解
    Redis执行Lua脚本示例
    getconf
    rc.sysinit 解析
    Linux系统启动内幕
    syslinux 和 grub
    isolinux.cfg 文件是干什么的
    C++中构造函数调用构造函数
    static和extern的作用域--题目
    构造函数与析构函数不能被继承
  • 原文地址:https://www.cnblogs.com/AlexCheng/p/2120199.html
Copyright © 2020-2023  润新知