Track
Except for the HUD game, it does not really look like a racing game. It is more like a fantasy role playing under the light and color correction post-screen shaders, no tracks and racing cars make it look like a racing game. It looks interesting to put a car on a single scene, but you don't want to drive on the ground, especially when the scene looks not so good (1 scene texture pixel 2 × 2 meters, place the entire vehicle on two texture pixels ).
The idea of this game is to make a track like a racing game, but after studying the racing track and the editor in the game, you will see that the rendering process in the game is so complicated. I use the track as a whole rendering, and the racing level of the track is composed of many different track blocks. They work perfectly together. In this way, you can connect the three orbital rings without drawing or creating them by yourself. The disadvantage of this method is that the available track blocks are limited. From the developer's perspective, creating hundreds of these track blocks requires a lot of work, let alone testing them in all levels.
Therefore, this idea is abandoned. Back to my original practice, I just created a simple two-dimensional track and added a little height through the scene elevation map. I want to use a bitmap that draws a track. A white line is used as the track on the in-place chart. Then, the location information is extracted from the bitmap and 3D vertices are created and imported into the game. However, it was discovered that this method made the track close to the scene ground. It was impossible or at least difficult to achieve the track ring, ramp, and crazy curve.
Therefore, this idea is abandoned again. To better understand how the track looks like I used 3D Studio Max, I used the spline function to create a simple Loop Track with only four points (see figure 12-12 ). Rotate 90 degrees to the left, which looks like a track and is more attractive than bitmap.
Figure 12-12
I have to export the spline data from 3D Studio MAX and plug it into my engine so that I can create a track in 3D Studio MAX and import it into the racing game engine. The difficult part is to generate an available track from the spline data, because each spline point is just a point, rather than a road with a wide direction.
Before you spend more time trying to find the best way to generate a track and import spline data to your game, make sure this idea works.
Unit Test
The game once again conducted some heavy unit tests. Starting from the first unit test in the new trackline class called testrenderingtrack, it just creates a simple curve like this in 3D Studio MAX and displays it on the screen:
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 ()
The showgroundgrid method only displays some gridlines on the XY plane to help you know where the ground is. Then I wrote this method in the model class, which can be reused. Showtracklines is the most important method because it displays all the lines and interpolation points that have been generated in the constructor of the trackline class. The showupvectors method tells the vector's upward direction for each point on the track. Without an upward vector, you cannot correctly generate the left and right sides of the road. For example, the curve track should be tilted, and you need to point the vector up to the center of the circle track, not just up.
The showtracklines auxiliary method displays each point of the track, which is connected in a white line. After you perform the testrenderingtrack unit test, you can see the 12-13 screen.
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)
Figure 12-13
The track looks a little like a road with the help of the red up vector and the green tangent vector. What you need to do now is to adjust the track generationCodeTo test more lofting lines. In the trackline class, you can see several of my test tracks. These tracks are created by manually adding some 3D points. More tracks can be achieved by using the Collada file, at this time, we will import the track data in 3D Studio Max to your engine, which will be discussed later.
Before you view the loose line interpolation code in the constructor, you can create a simple Loop Track by converting the X and Z values of the track vertices (see figure 12-14 ). In order to make the spline look more circular, I also add four new points. The new testrenderingtrack unit test is as follows:
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 ()
Figure 12-14
Interpolation spline
You may ask how to get all these points by getting them at four or eight input points and how to insert the points worth better. All of this happens in the trackline constructor, or more accurately, it should be in the load method, which allows you to reload data when you need to regenerate it. The first time you see the load method, you will find it difficult. It is the main method for loading all track data, verifying track data, interpolation, and generating up and tangent vectors. Tunnel and scene object are also generated here.
The load method does the following:
It allows re-loading, which is very important for loading and re-starting the level. If you call the load method again, the previous data will be cleared automatically.
Verify all data to ensure that you can generate tracks and use all auxiliary classes.
Check every point on the track to see if it is above the scene. If not, the point will be corrected, and the surrounding points will also rise slightly to make the track look smoother. In this way, you can easily generate a 3D track in Max. When you place the track above the scene, you don't have to worry about the actual height of the scene.
The circular orbit is simplified to the upper and lower sampling points. The loaded code will automatically detect these two points and replace them with the nine points of the complete loop, so that more points are inserted to produce a smooth and correct circle orbit.
Then, points on all tracks are interpolated using the Catmull-Rom interpolation method. You will immediately see this method.
- The up and tangent vectors are generated and interpolated several times to make the road as smooth as possible. In particular, the tangent vector should not suddenly change the direction or flip to the other side, which makes driving on this road very difficult. This Code took me the longest time to make it work normally.
Then, all the auxiliary classes for analysis and the road width of each point on the corresponding track are stored for subsequent use. The actual rendering occurs in the track class, which is based on the trackline class.
Texture coordinates of road textures are also generated here, because you store the information of all track points in the trackvertex array, which makes subsequent rendering easier. Only U texture coordinates are stored here, while V texture coordinates are only set to 0 or 1 later, depending on whether you are on the left or right side of the road.
- Then, analyze the tunnel auxiliary class and generate the tunnel data. The code here only creates some new points for future use. They are used to draw tunnel boxes with tunnel textures in the track class.
Finally, all the scenario models are added. They are stored together with track data as a complete level. Additional scene objects are also automatically generated in the track class, such as street lights on the side of the road.
when I started to write the trackline class, the constructor could only generate new interpolation points from the input points using the Catmull-Rom spline helper method. The code looks as follows, and similar code can be found in the load method:
// Generate all points with help of Catmull Rom splinesfor (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/1001_f); 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)
More complex tracks
Unit Tests can start and run everything, but the more complex the tracks are, the more difficult it is to create tracks by entering 3d positions for each sample. To make things easier, you can use the spline exported from 3D Max Studio, which makes it easier to create and modify splines.
Take a look at the xNa racing game's exclusive track from 12 to 15. This track contains only about 85 points and is interpolated to 2000 points so that the track has about 24000 polygon. Track fences and additional track objects are generated later. Building such a track and adjusting it is almost impossible without a good editor, but fortunately there is 3D Max. Maybe I will create a track editor for this game in the future, at least so that you can directly create a simple track in the game.
Figure 12-15
I initially thought it was not easy to export such data .. Xfiles do not support spline, And. fbx files do not. Even if they can export spline, you still need to do a lot of work to extract data from the track, because it is impossible to obtain vertex data from the imported model in xNa. I decided to use the currently popular Collada format, which allows different applicationsProgramImport and Export 3D data. Compared with other formats, Collada stores everything in XML format. You can easily see which data corresponds to which function from the exported file. You don't even need to find any files. You just need to find the data you need and extract it (here, you only need to look for spline and auxiliary data, and the rest is not important to you ).
For games, Collada is not a really good export format, because it usually stores too much information, and XML data is just a bunch of text, so compared with binary files, the size of the Collada file is also much larger. For this reason, and I cannot use any external data format in xNa starter kit, all Collada data is converted into internal data formats in the trackimporter class. Use your own data format to speed up the loading process and ensure that no one can create their own tracks. Hey, wait, don't you want others to create your own tracks? I do want this to be easier. You need 3D Studio Max to create or rebuild a track. I must implement some way to import and create tracks in the future.
Import track data
To make it easier to load the Collada file, Some helper classes are used. First, the xmlhelper class (see figure 12-16) can help you load and manage XML files.
Figure 12-16
The colladaloader class is just a very short class. It loads the Collada file (just an XML file), making it easier to use the xmlhelper Method for derived classes.
- Colladatrack is used to load trackpoints. Other auxiliary objects, such as widthhelpers, can narrow the track width and width. roadhelpers is used for tunnels, palm trees, street lights, and other roadside objects. Finally, all scene objects are displayed when you approach them (because there are a large number of objects in the scene ).
Colladacombimodels is a small helper class used to load and display multiple models at a time. You only need to set a composite model containing up to 10 models, these ten models have different positions and rotation values. For example, if you want to place a city area with buildings, you only need to use the buildings. combimodel file. If you need some palm trees and several stones, you can use the Palms. combimodel file.
To learn more about the loading process, you can use unit tests in the trackline and track classes, but more importantly, you can refer to the colladatrack constructor itself:
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 float Ing 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 // splinexmlnode splineinstance = xmlhelper. getchildnode (visualscene, "url", "#" + splineid); xmlnode s Plinematrixnode = xmlhelper. getchildnode (splineinstance. parentnode, "matrix"); If (splinematrixnode! = NULL) throw new exception ("The colladatrack file" + filename + "shocould 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)
It is not difficult to obtain the spline data, but it takes more effort to obtain the movement, scaling, and rotation values (the auxiliary class is also more complex ), but after you write and test this code (several unit tests and test files are used to implement this constructor ), it is easy to create new tracks and import them into the game.
Generate vertex from track data
Obtaining track data and importing auxiliary data is only half done. You have seen how complicated the constructor of the trackline class is. It helps you generate interpolation points and create up and tangent vectors. Texture coordinates and all auxiliary and scene models are also processed here. But you still have a lot of points, and there is no real road for your car to exercise on it. To draw a true road with textures (see figure 12-17), you need to first create a vertex for all the 3D data and finally generate a path, and also include other dynamically created objects, for example, guardrail. The most important texture is the road itself, but the game without normal textures looks a bit boring. The normal map adds a glittering structure to the road to shine in the sun. The textures, backgrounds (roadback. DDS), and tunnels (roadtunnel. DDS) on both sides of the road are also important, but you won't see them frequently.
Figure 12-17
The trackline class handles all these textures, including road materials, road cement columns, guardrails, checkpoints, etc. It is based on and inherits from the track class. Landscape is used to draw a track, all scene objects, and the scene itself, so that the vehicle can be driven on the road. You also need physics to deal with the movement on the track and the collision with the guardrail, which will be discussed in the next chapter.
The track class is responsible for all road materials, generating all vertices and index buffering, and finally rendering all track vertices with the help of shader. Most materials use the specular20 Technology in normalmapping to generate a glossy Road, but diffuse20 technology should be used for tunnels and other non-glossy road materials.
Unit Testing is easy to draw tracks. All you want to do is to draw tracks.
Public static void testrendertrack () {track = NULL; testgame. start (delegate {track = new track ("trackbeginner", null) ;}, delegate {showupvectors (track); track. render () ;}) ;}// testrenderingtrack ()
As you can see, you still use the showupvectors auxiliary method in the trackline class, because you inherit from the track class. The render method is similar to the scenario Rendering Method in the Mission class of the previous chapter.
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 ()
Well, it doesn't seem very complicated. Take a look at the generated path vertex and index buffer code. The private auxiliary class generateverticesandobjects performs the preceding operations:
Private void generateverticesandobjects (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 wowould 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 except 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)
I wrote a lot of comments when writing this code. The first part generates a large tangent array, which is five times the size of the track vertices in the trackline class. This data is directly transmitted to the vertex buffer and then used to construct the index buffer of a polygon. Each road slice has eight polygon (composed of four parts, each of which has two polygon). Therefore, the index buffer size is 24 times that of the track vertex index. To ensure that all these indexes can still be correctly used, the short type must be replaced by the int type. Previously, I used the short type because it saved half of the memory. However, in this case, there are more than 32000 indexes (the track at the expert level has 2000 Road slices, and it has reached 48000 indexes 24 times ). Because the track is automatically generated rather than manually generated, you need many iteration points. If you do not have enough iteration points, it will lead to overlap errors, this won't make the track smooth enough (see figure 12-18 ).
Figure 12-18
You may ask why each road slice is generated in four parts, not because I like low-end GPUs to process a lot of polygon. This technology is used to improve the visual effect of the track, especially in the case of curves.
Figure 12-19 can better explain this problem. As you can see, the two polygon that make up non-parallel blocks do not always have the same size, but they still use the same number of texture pixels. On the right side, you can see that in extreme cases, the bottom right corner of the road is severely distorted.
Figure 12-19
This problem can be solved by dividing the road into multiple parts. You can divide the road slice into four parts, so the road looks much better.
Final Result
You need to do a lot of work to display the scene and road correctly, but now you have done a good job, at least the graphic part is good. You can see many tips and tips in the class in the track namespace. Please refer to unit tests for more information on how to draw sides, circles, and tunnels of a road.
Figure 12-20 shows the final result of testrendertrack unit test in the track class.
Figure 12-20
Combined with scene rendering in the first part of this chapter, you have a pretty good rendering engine. With the post-screen shader sky box in the background, scenes and road rendering look pretty good (see figure 12-21 ). The post-screen glow shader also makes everything better, especially when there are many objects in the scenario.
Figure 12-21