Welcome to rednebula.com
This is the personal site of Bjarne Fich.
Site formerly known as "www.dragonslayer.dk"
Optimized for
Get Firefox!

How to build a flight simulator!
This article goes through the concepts of building a flight simulator. The focus has been on the feeling and illusion rather than building a reality like engine.

This article describes some of the basic methods and ideas. It is not the only way to do it, and the article doesn’t describe every detail. I have released the source code for the flight simulator and there might be parts that are not explained in this article. Fell free to contact me if you want me to elaborate on one or more parts.

You can also read the article to get ideas for methods to be used in entirely different programs. Here you have a complete terrain generator that can be used for adventure games and other stuff if you want to.

Checkout and download the source code here.

Enjoy.
Bjarne


Terrain
The terrain is the key to the flight simulator. Basically besides a plane model and some fancy movement, everything goes into the terrain.

The class contain the data and two main procedures: MakeTerrain() and Draw(). All other procedures and functions a secondary support for these two. The MakeTerrain() generates a new terrain, based on the world.xml config file. The Draw() draws the terrain, based on the user (camera) position, to openGL.

MakeTerrain()

The Data
Most terrain data are kept in unsigned char arrays. I use a one-dimensional array and manage the two-dimensional indexing my self. The world is wrap-around, and this is one of the reasons the sizes have to be a power of two. I simple mask the bits to create wrap around with the & (and) function. unsigned char *Height; #define HEIGHT(x,z) Height[((x)&SizeXMask)+((z)&SizeZMask)*SizeX] …where SizeXMask and SizeZMask are the sizes minus 1. A size of 128 looks like this in binary: 10000000 and the mask become 01111111. When x becomes larger than 128 it wraps around and lookup in x-128 etc…

The Generator
I use one basic method to generate the landscape. This method is used with small changes allowing me to do heights, forests, clouds etc… /* First fill up the world with random numbers. */ for (x=0; x<SizeX; x++) for (z=0; z<SizeZ; z++) HEIGHT(x,z) = RANDOM_INT(256); /* Now level everything out */ for (i=0; i<10; i++) for (x=0; x<SizeX; x++) for(z=0; z<SizeZ; z++) { sum = 0; for (xx=x-1; xx<=x+1; xx++) for (zz=z-1; zz<=z+1; zz++) sum += HEIGHT(xx,zz); HEIGHT(x,z) = sum / 9; } First I fill the array with random numbers and then I run through the array and make each point (x,z) equal to the average of a 3x3 set of points around it. This is done 10 times in the above example. This concept can be modified to accommodate the need. The leveling routine can use a 5x5 area (or a round area for more advanced use). It can be run more or less than the 10 times in the above example. Feel free to experiment you self.

It is easy to understand in regards to heights, but how do I use it for forests? Basically I make everything below a certain threshold be forests. Then I later remove the forests in mountain or water sectors.

What about clouds? Even more easy. Like water, but may be above water and mountains.

Some times I run a check during the leveling routine in order to find the maximum and minimum and then go through the array expanding it to allow values from 0-255. Otherwise everything will become more and more average and end up with 128 if the i-loop is run many times enough.

The min/max loop looks like this: for (x=0;x<SizeX;x++) for (z=0;z<SizeZ;z++) HEIGHT(x,z) = (unsigned char)(RANDOM_FLOAT*255); for (i=0;i<40;i++) { max = 0; min = 255; for (x=0;x<SizeX;x++) { for (z=0;z<SizeZ;z++) { s = 0; for (xx=x-2;xx<=x+2;xx++) for (zz=z-2;zz<=z+2;zz++) s += HEIGHT(xx,zz); HEIGHT(x,z) = CHECK_RANGE(s/25,0,255); if (HEIGHT(x,z)<min) min = HEIGHT(x,z); if (HEIGHT(x,z)>max) max = HEIGHT(x,z); } } if (max>min) for (x=0;x<SizeX;x++) for (z=0;z<SizeZ;z++) { s = HEIGHT(x,z); s = (s-min)*255/(max-min); HEIGHT(x,z) = CHECK_RANGE(s,0,255); } } HEIGHT(x,z), CHECK_RANGE and RANDOM_FLOAT are macros. I hope you understand these without additional explanation. Otherwise checkout the source code Macros.hpp.

The land types.
I use 7 types of land.

  • 1 Deep sea
  • 2 Sea
  • 3 Shores
  • 4 Fields
  • 5 Hills
  • 6 Mountains
  • 7 Peeks

    These are kept in the lowest 3 bits of the Land variable. Based upon the config file they may have different percentages of the world. Basically I build a Height map with the above described methods. Then I start to make Deep Sea land types with all the lowest parts of the map until I have enough Seep Sea sectors to mach the requirement by the config file. Then I start picking Sea sectors. These will be adject to the Deep Sea sectors as I still fill up the map from the lowest values and up. Like this I continue until I reach the Peeks.

    The routine looks like this:
    (The HEIGHT(x,z) map is build like described above). /* t[] contain the amount of sector we wish for each type */ /* p[] contain the requirements from the config file */ for (s=0;s<8;s++) t[s] = (p[s]*SizeX*SizeZ) / 100; /* Now pick the Land types from the height map. */ for (i=1, s=0; s<256; s++) { for (; ((t[i]<=0) && (i<7)); i++); for (x=0;x<SizeX;x++) { for (z=0;z<SizeZ;z++) { if (HEIGHT(x,z)==s) { LAND(x,z) = i; t[i]--; } else if ((i==7) && (LAND(x,z)==0)) { LAND(x,z) = 7; } } } } (Now this routine can be optimized, but that’s an other story. My optimization has been focused on the Drawing routines, not the generation.)

    I typical has p[3]=0 as this is the shores. The shore is later picked from the Sea and the Fields based on the water level.

    LOD and Detail map.
    When I have build the landscape I setup a detail map. The detail map is used to determine the quality of each sector square drawed. I merge all sectors into groups of 2x2.

    Now if I require high detail I draw a triangle fan, starting with the center point and them move clock wise around the points.
    If the points 2,4,6 or 8 are very close to the line between the corners I don’t really need to draw these. If point no. 4 is close the triangle fan is drawn like this. (Point no. 4 is skipped.)


    To determine if a point is close to the centerline I simple take the average of the two corners; (p1+p2) / 2. for (x=0;x<=SizeX/2;x++) { for (z=0;z<=SizeZ/2;z++) { bits = 0; h0 = HEIGHT(x*2,z*2)*2; h1 = HEIGHT(x*2+1,z*2)*2; h2 = HEIGHT(x*2+2,z*2)*2; d = ((h0+h2)/2)-h1; if (ABS(d) <= DETAIL_LEVEL) bits |= 1; ...etc... Because I bundle the sectors in 2x2, the x and z loops only run to half the sizes, and the x and z are multiplied by 2 when retrieving the Height information.

    The detail bits for the four points are stored for later use in the drawing routine. If all the 4 edge points can be removed, it might be possible to remove the center point as well. This gives two possibilities: or

    I determine if one of these is useful and store this information in the 5th and 6th bit in the detail map.



    The detail map is a neat way to improve performance. In the FlightSim you can turn the detail on/off by pressing the ”r” key. Press the ”e” key to enter wire frame first, then it’s easier to see the effect. The large flat plains are typically drawn with low detail while the jaggy mountains are drawn with high detail.

    Ground Textures
    This was a little tricky. I would like to use different textures depending on the terrain type, but when I did this the areas where two different terrain types meet would look awful. Now what I did was to build a very special texture:



    A normal sector only uses the center of each of the four squares, witch is made to wrap around, while sectors abject to a different land type would stretch the sector toward the edge of the 4 squares. This would allow for the edges to become ”dusty” while large areas of identical sectors keep a fine texture.



    The method is not perfect, but in a fast moving plane the illusion works just fine. And basically it is more important to keep up a good illusion, rather than a perfect world. (We should use a real-time ray-tracer if we wanted perfection – but then it would be goodbye performance. At least for the next couple of years.)



    Airfields
    Finally I place some airfields. Basically I just look for fairly flat areas and place a predefined map of the airfield. I use the 7th detail bit (see above) to indicate an airfield. In this case the drawing routines will reallocate new textures etc. so a world filled with airfields would run rather slow as I would be switching textures all the time. (This is the same reason the 4 ground texture types are placed in one texture rather than 4)

    I use a small static array of strings ascii characters like this: … " "," ", " "," ", " "," / < ", " V "," #< < ", " | ##H "," / # ## ", " #+-##H "," ##### ", " | #PH "," < # ## ", " | T "," # / ", " | "," # ", " | "," # ", " V "," # ", " "," < / " … The main reason is that it is easier to create new airfield layouts, rather than hard code the routines. Later I might move these definitions outside into a file for nerds to mess around with…

    Basically it is often a good idea to do things like this as you greatly increase your flexibility of your programming by holding the data or layouts in specific data-containers (in this case an array), and then building a routine to handle the data. Like that you have divided the data and logic into two parts inside the program. For those who are familiar with client server systems this should sound familiar, as it is the same when you divide into presentation, business logic and data.


    Terrain drawing.
    This is the most critical part of the flight simulator. I have focused on performance, performance and performance. The goal has been to give an illusion that mainly provides the feeling of you flying around in a plane, rather than a real world engine. This is not because real world engines are bad; it has just not been my goal.

    The most important features (beside the terrain) have been to provide sea, forests and clouds. These have been chosen as they provide a good sensation of a landscape. I have later added models and might add other features, but I still believe the three above is the key to breath some life into the terrain.

    In terms of performance the key is to draw as little as possible. To do this I use the frustum. The frustum is one of the best things to understand if you want to work with 3D.

    Frustum
    I first heard about it some time ago as I was browsing the Internet looking for something interesting. Words like 3D and better performance quickly catched my eyes and soon I was downloading some examples using the frustum. (Sorry, I can't link you to it as the article has been removed.)

    But what is the frustum?

    Basically it’s the box containing everything you can see on the screen. Everything outside the screen or behind the rear plane (or front plane) is outside the frustrum. The trick is that it is fast to check if an object is inside the frustum, at least faster than drawing the object.



    Imagine a space game where you have a 2000 polygon destroyer flying around. If you quickly could determine if it would be inside the viewing area, you might save a lot of drawing time. Imagine there were 10 destroyers flying around, and maybe only 3 were in the view (the others was behind you firing at you engines – oh… that was an other story).

    As every polygon uses several vertexes, you should check if any of the vertexes is inside the frustrum. Well that kind of destroys the idea, as we now have to run through all of the 2000 polygons anyway to see if some part of them is inside. Well you don’t have to do that. You can check if a point is close to the frusturm (actually you can determine how far outside it is. If the destroyer is 100 units long, you can simply check the center point to see if it is further than 50 units outside the frusturm. You can even then check each part (engines, gun tower, front plating etc…) of the destroyer to see if they may be inside.

    Now I do the same with the terrain. Every sector is checked to see if it is inside the frusturm. I check the center point (see detail map in the MakeTerrain function) of each sector to see if it is close to the edge, before drawing it.

    But the frusturm is not static. What happen every time we move the camera (change projection). Then you have to extract the frustrum again and recalculate it. It takes a small amount of time, that you need to invest every frame (or at least when you move the view point), but compared to the amount of polygons you save, is it often worth it.

    You can find all the code for the frustum in my SDK, in the glWindow file.

    In the terrain I use the frustrum for more than the basic frustum functionality described above.

    Drawing from back to front
    I need to draw everything from back to front. This is because I use blending. The trees behind other trees can bee seen between the leaves. If I used DephtMask or if I did draw the ones in front first thing would be overwritten and look stupid.

    I could not put all the trees into an array and sort it based on depth (distance to the view point) as it would take too much time. I typically draw thousand of trees in every frame.

    I look at the perspective to see where sectors would be behind other sectors and choose a drawing order that would ensure the rear ones to be drawn first. In order to do this I need to determine the direction I look in. I therefore divided my viewing directions up into octets. (8 basic directions) and made routine fore these. They are paired 2 octets at a time, so basically I have 4 almost identical drawing routines. The only differences are if I draw from east to west, north to south or the other way around… (In programming terms if x and z are incremented or decremented).

    I will describe the solution I use for one of the octet pairs, the method are identical for the others.

    In terms of the perspective the sectors lying precisely east (north, south or west) of me are the ones that always will be closer to me than those lying in the same column (or row) to the sides. Therefore I should draw the sectors like this:





    To support the main Drawing procedure I have a lot of sub procedure to draw individual sectors, on to draw trees in a sector etc… so basically I have a huge loop running like the fig. above, calling the supporting procedures.

    To determine the area to draw i start by determine the frustrum boundary box (the minimum and maximum of the frustum). This is determining how far to the east (north, south or west) my drawing should begin. // Determine the maximum view ranges if (glWindow->Frustum->GetBoundryBox(&p1, &p2)) { xmin = (int)(-p2.x/20.0); xmax = (int)(-p1.x/20.0); } Then for each sector (from the far east to my position) I determine how far to the north and south I should draw. This is done by determine the intersection point between a line (going north or south) and the right and left frusturm planes. This intersection point determines the maximum and minimum for the column (north-south). I determine this for all the sectors generating an array with the boundaries.



    The code:
    px, pz is the sector beneath the viewpoint.
    xmax, xmin is the frustrun boundry points
    /* Build viewrange array */ for (c=0, cx=xmax; cx>=xmin; cx--, c++) { /* Set the vector to intersect the plane. The vector is based on 2 points */ p1.Set(cx*20+60, view_level, 10000); p2.Set(cx*20+60, view_level, -10000); /* Determine where the line intersect the right plane. Result in p3 */ if (glWindow->Frustum->LineIntersectPlane(&p1, &p2, &p3, FRUSTUM_RIGHT_PLANE)) { max_array[c] = (int)(p3.z/20.0); } else { max_array[c] = pz; } /* Determine where the line intersect the left plane. Result in p3 */ if (glWindow->Frustum->LineIntersectPlane(&p1, &p2, &p3, FRUSTUM_LEFT_PLANE)) { min_array[c] = (int)(p3.z/20.0); } else { min_array[c] = pz; } /* Check and see if we hit the rear plane. */ p1.Set(cx*20-30, view_level, 10000); p2.Set(cx*20-30, view_level, -10000); if (glWindow->Frustum->LineIntersectPlane(&p1, &p2, &p3, FRUSTUM_REAR_PLANE)) { zz = (int)(p3.z/20.0); if (vec.z>0) { if (zz<max_array[c]) max_array[c] = zz; } else { if (zz>min_array[c]) min_array[c] = zz; } } else { max_array[c] = pz; min_array[c] = pz; } max_array[c] = CHECK_RANGE(max_array[c],pzsubview,pzaddview) + 2; min_array[c] = CHECK_RANGE(min_array[c],pzsubview,pzaddview) - 2; } When the arrays are build the loops can be run and the landscape drawn. First the ground, then the water and finally the clouds, trees and models. /* Draw from far east towards us. */ for (c=0, cx=xmax; cx>=xmin; cx--, c++) { /* Draw from south towards the east bound center line */ ppz = pz; i = MIN(min_array[c],min_array_water[c]); if (i>ppz) ppz = i; z = MAX(max_array[c],max_array_water[c]); for (; z>ppz; z--) DrawLandSector(cx,z); /* Draw from north towards the east bound center line */ ppz = pz; i = MAX(max_array[c],max_array_water[c]); if (i<ppz) ppz = i; z = MIN(min_array[c],min_array_water[c]); for (; z<=ppz; z++) DrawLandSector(cx,z); } The support procedures like DrawLandSector() above uses the Detail maps, Textures etc. as described in the MakeTerrain() documentation. Check the code to see how it works.


    Bugs
    The methods above have some errors. Some I haven’t got the time to fix yet, others have been ignored as there is no quick-way to fix them and they don’t show up.

    One of the bugs can be seen if you fly high up into the sky and look down. Clouds and sometimes the ground vanish at the edges. This is due to an error in the frustrum calculations. The reason for the bug is the use of a line to determine how far north and south the areas should be drawn. The line is drawn at a y-height called viewlevel. This is wrong. If the frustrum (view area) is tilted or rolled (if the plane tilt or roll) the line will hit at a wrong place in the right and left plane. The solution is to use a vertical plane parallel to the north-south axis, and determine the minimum and maximum rather than use a line to determine minimum and maximum.

    The other bug I choose to ignore. Sectors placed on the eastbound centerline may have errors when drawing trees. There may be several trees in each sector. I do draw them from the far corner of the sector. For sectors north of the eastbound centerline I draw from the northeast corner, while I on the south side draw from the southeast corner. But for sectors on the centerline there will be trees south of the view position and others on the north side. I choose to use the same routine and draw them from the northeast corner. Some trees right to the east may therefore be drawn wrongly in front of others. This effect can be seen in the clouds as well, but in both cases you need to look careful to see it. The only solution is to make a special routine to draw sectors right on the centerline.


    Comment...
    Look through the code to see how the generation and drawing works. The above description should provide you with an idea about how it is build, the details you must figure out on your own. You can also use some of the above methods and ideas to your own project – then I hope this article have been to inspiration for you.

  • Contact