GENERATING TERRAINS VIA MARCHING CUBE ALGORITHM FOR GAMES Kethan Tellis Adam Smith Chris Barakian Ravi Vaishnav University of Southern California CSCI 580 Los Angeles, CA Introduction Terrains are very useful and essential components of modern games. Games have featured terrains since the earliest 3D graphics engines. Terrains are widely used in games to represent both Earth’s surface and imaginary worlds. Environment plays a huge role in games therefore the realism of terrain and attention to detail is very important. Terrain also plays an important role in gameplay and offers advantages to the player on higher ground and can be seen in various games like Call of Duty®, Skyrim® etc. Above all else, terrain makes the scenes look more realistic enhancing the beauty of the game itself. They are also used in other computer graphics fields such as satellite maps, ground representations for aircrafts, etc. but their most recent foray is in video games. There are various ways in which interesting game terrains can be created. Height maps are one of the most frequently used techniques. Basically they are raster images used to store height values. Typically the images are stored as grayscale images with black representing lower points and white for the higher points. Height maps are computationally fast and relatively inexpensive to create, but they only allow variation of height. This means complex structures like caves or tunnels cannot be created. Procedurally generated Terrains are also used quite often. As suggested by the name they involve generation of terrain using procedures based on some mathematical function, with some form of randomness introduced by the use of noise. Various complex and beautiful terrains can be generated using this technique but as they are generated on-the-fly they are usually not reused or saved for later use. Geometry shaders are fairly new to the graphics scene. Direct3D 10 and OpenGL 3.2 were the first libraries to introduce this technology. They were available in OpenGL 2.0+, but only with the use of extensions1. Geometry shaders coexist in the graphics pipeline nestled between vertex shaders and fragment shaders. Vertex data is processed and manipulated in the vertex shaders and then (optionally) passed into a geometry shader where new primitives (lines, triangles, etc.) can be generated. The new primitives are then passed into the fragment shader for rendering and shading. Geometry shaders can add an astounding amount of new detail, but they cannot update physical models. This creates issues for collision detection and other physical simulations. 1 www.opengl.org/wiki Model scanning scans real world objects or environments to collect data on shape and possibly color. This data can then be used to generate a three dimensional model. Scanned models can contain lots of detailed information producing high resolution models, but they are difficult to generate and can be expensive in time and resources. Marching cubes is an algorithm published by Lorensen and Cline in a 1987 SIGGRAPH paper which talks about a method for building a polygonal mesh of a solid surface from three dimensional density fields2. The algorithm scans through the data analyzing eight locations near each other in a 2x2x2 “cube” area. The algorithm then analyzes the density values and generates triangle meshes based on these values and how they intersect with the 2x2x2 cubes. Perlin Noise In order to generate the 3D terrain meshes, a method of building the three dimensional data set is needed. A value 0.0 – 1.0 would be needed for each point. If the value is greater than 0.5, then it is considered be a covered point, and values less than 0.5 mean the point is not covered. To do this the team chose to use traditional Perlin Noise and modify it to give results that better fit the project. Perlin noise can be used in n-dimensional space. In this project a 3-dimensional noise function was used to create noise values for any position of x, y, and z. One of advantage of Perlin noise is its ability to generate relatively smooth results by summing over increasingly higher octaves (figure 1). 2 Lorensen &Cline Figure 1: Multiple octaves of noise3 For this projects implementation of Perlin noise a set of parameters were used to help modify and manipulate the generated noise. The first parameter in the noise function is alpha. This parameter inversely affects the amplitude as octaves are summed together. The next parameter is beta, and this parameter directly affects the frequency and weight of each increasing octave. Therefore the equation for a given octave N of noise is ππππ π(πα΄Ίπ₯, πα΄Ίπ¦, πα΄Ίπ§)/πα΄Ί The next parameter in the noise function is N which represents the number of octaves to sum over. This gives the equation where the values are summed over i to N – 1. n ο1 noise (bi x, bi y, bi z ) ο₯ ai i ο½1 As values are summing over higher and higher octaves the increase in the frequency by the 3 Bourke increasing b value is lessened by the increasing a. Higher values of b and small values of a, a much nosier signal will be generated as the higher octaves will have a greater weight as the sum is computed. Inversely if smaller values are used for b and greater values for a then the noise will be dictated more by the lower octaves than the higher ones. The last parameter added to the noise function is frequency(F). Its main responsibility is to magnify the returned noise values by the given factor. One challenge faced with the projects implementation of Perlin Noise is the constraint of the input positions needing to correspond to integer positions in a three dimensional array. This meant that all of the values of x,y,z were integers, but Perlin Noise works best for continuous input. To solve this, a bit of randomness was generated for each input value of x, y, and z. This value was a random number less than 1 and was added to the input coordinates in order to jitter them in to noninteger numbers. With the input position no longer an integer, the noise function is now able to generate a noise value between -1.0 and 1.0. That value is taken and using the equation (1.0 + π₯)/2.0 3. Sorting vertices in correct winding order 4. Determining the correct vertex normal In this section vertices on the cube will be referred to as nodes and will be considered as “on” or “off,” and vertices that are part of the triangles as vertices. The algorithm decided on was one that would work consistently for any possible case. For simplicity’s sake triangle fans (figure 2) were used as they are represented as a starting vertex v1 and followed by a list of n connected vertices. Figure 2: Triangle fan4 Two key features were observed. First, that of the fifteen possible cases, there are at most four triangle fans, as in the case shown in figure 3. a value between 0.0 – 1.0 is returned. This is now a scaled value that can be used within the implemented Marching Cube algorithm. Marching Cubes Implementation For this project it was decided to implement the entire Marching Cubes algorithmically. This was broken into three steps: 1. Determining an algorithm 2. Sorting and storing all 256 cases in a lookup table Figure 3: Four Fan Case5 4 5 MSDN Geiss7 The second observation was that there were at most six triangles per fan, as shown in the case in figure 4. opposite facing vectors, then the normal was reversed. Figure 4: Six Triangle Case6 Figure 5: Face Normal Reversal7 Using this, a structure composed of seventy-two vertices (four fans by six triangles by three vertices per triangle) for each case is built. The algorithm determined would march around the edge of the cube for each case sorting each vertex against a start vertex of the corresponding fan. If the normal needed to be reversed it indicated that the winding order needed to be reversed as well, and would therefore reverse the ordering of the vertices. After determining this algorithm and a method for storing all the cases, a fix was needed for the triangle winding order. This was needed so vertices could be sent in the proper format to ensure the correct face normal. (It was quickly realized that having the correct vertex normal but incorrect winding order was insufficient as 3D modeling programs such as Maya would determine that triangle as back-facing). First, the face normal of each triangle was calculated via the cross product of two edges. This face normal could either be pointing inside or outside of the surface that it enclosed. To determine that the proper normal was facing outward, the dot product was compared to the unit normal N calculated against a unit vector from the bounded node to any vertex on the triangle (vector formed by b to a in figure 5). If the dot product is less than zero, and therefore 6 Geiss7 Another case needed to be considered is when more than four vertices are “on” the signs are flipped (e.g. seven nodes on and one off becomes one on and seven off). This would adversely reverse the normal as well. A simple fix was to reverse the winding order for these situations to ensure proper facing of the face normal. The last part of the algorithm is ensuring that the vertex normals are properly oriented and averaged for smooth shading. Since each vertex normal is calculated independently, a method is needed for comparing them to all other faces that share that vertex. There are two ways in which the vertex can be shared either at the triangle-fan level, or at the inter-cube level. For each vertex on the fan, a vertex normal is created at the time the fans were initialized. The triangle normal was added to the vertex normal for each triangle that vertex was connected to as demonstrated in figure 6. Vertex normal V1 is 7 Geiss7 the sum of N0, N1, N2; Position of V1 is irrelevant. Figure 6: Face Normal Reversal8 It was chosen not to normalize the vector at this point as it would not be correctly weighted between cubes. (e.g. if V0 is in another cube but only used once, normalizing V1 prior to averaging it would result in a normal biased towards the second cube). Now, look up tables for all 256 cases of cubes can be created for both the temporary unnormalized vertex normals and triangle vertices. The values for the edges are passed and used for the lookup for any grid size needed (a 100x100x100 grid is used in the demo). Neighboring cubes are compared to each other to average the normals. Each of the six neighboring cubes (top, bottom, left, right, front, back) and the four possible vertices on each touching cube-face are checked to see if the vertex exists. If it does, the normals are summed and then normalized to get the proper averaged vertex normal for the surface. Compiling the Cubes 8 Geiss7 Once the cubes have all been calculated and compiled they need to be prepared for rendering. There are a couple of different options for rendering the cubes. As of this project the primary two choices are the latest graphics libraries OpenGL 4 and DirectX 11. Both libraries would work perfectly okay for this project, but OpenGL was chosen. The next choice was to determine an easy and efficient method to store and render the meshes. Two methods were chosen to test out and ultimately use. The first choice was geometry instancing. Geometry instancing is a rendering method that involves loading a model one time and then rendering multiple copies of it around the scene. It allows for a more efficient use of memory as there is no reason to load the exact same model multiple times. In this project there were only 256 models needed. When drawing the model all that was needed was the instance of the model and the position it needed to be rendered at. Unfortunately, geometry instancing proved to have a few problems. The first problem encountered was that OpenGL does not natively support geometry instancing unlike DirectX 11. The closest that can be achieved is somewhat hacky. The second problem was that there were 256 models. Sorting and rendering such a large number of models had too much overhead because each case was only used a small number of times. While prototyping with geometry instancing only a simple cube model was used and the frame rates were always over 60 frames per second (fps). After switching to rendering all 256 cases the frame rate dropped to an estimated average of 50 fps. This could be due to all of the switching in the larger set of models. Another problem with this method was the support for vertex normals. The nature of vertex normals and how they are calculated breaks the efficiency of instancing. Vertex normals are calculated by averaging the normals of each face connected to that particular vertex. Since each case was surrounded by different cube cases this makes storing vertex normals per cube case very complex and inefficient. The next rendering choice was to render individual triangles. This method proved to be much simpler to implement. The cube cases were calculated just as they were previously and then stored in a look up table. As the cubes cases were calculated they were stored as triangle fans, but when building the models for rendering they were deconstructed into individual triangles. Using individual triangles was also beneficial because it allowed for unique vertex normals, and even more so, custom vertex formats. During the development process adding features at the vertex level was as easy as adding a new vertex attribute and attaching it to the vertex array. It was nontrivial to add vertex colors and UV coordinates using this method. Another added benefit was found while building the final model. All that was needed to add a cluster of triangles to the final model was to use the triangles from the look up table and add an offset to each vertex. Even though this was inefficient with memory it did yield a higher frame rate. The frame rate was always over sixty fps and rarely dipped below sixty. Compiling the entire mesh into a single vertex array yielded much better results. This was most likely due to using only one mesh versus switching between 256 different cases. In the end using individual triangles compiled into a vertex array was the best option. Rendering A specific version of OpenGL needed to be targeted since it was the library of choice. The version that was chosen was 4.2 and it was the highest version supported by the team’s graphics cards. Pre-4.0 versions were not considered because of the removal of fixedfunction pipelines in favor of fully programmable rendering pipelines9. Speed and compatibility could have become a factor since the older functions were deprecated. Fortunately the programmable pipeline allows for complete customizability using OpenGL’s shading language called GLSL. This allows for custom shading calculations and custom vertex formats. Conclusion and future enhancements Just like any other program there is plenty of room for expanding and optimizing. One area that could be optimized is how the triangles are handled. When building the vertex array each triangle was stored as three vertices. A better solution could be to save space and build the mesh using triangle strips. The figure below will give a better idea of what they are. Figure 7: Triangle strip10 Another enhancement would be to use OpenGL’s geometry shader feature to add more 9 www.opengl.org/wiki MSDN 10 detail. This would allow for the same main memory usage, but have more triangles. Geometry shaders work off of sending a simple primitive to the video card and have a geometry shader generate more primitives. There are two down sides to this method. The first is that the extra geometry cannot be used in any calculations done on the CPU. Collision detection becomes a much more difficult, but not an impossible task. Procedural texturing was a feature that was desired, but there was not enough time for it. In the vertex and fragment shaders uv coordinates could be generated based on the per-fragment normals. One simple solution would be to shade the fragment based on a look up into a cube map based on the interpolated vertex normal. If one wanted to generate a scene similar to the floating islands scene in Avatar, a grass texture could be applied to the +Y map, and dirt textures to every other face of the cube map. Figure 8: Floating islands from the movie Avatar11 In conclusion, this project was a great experiment for the team. It yielded a lot of technical hurdles for the group to overcome. Everyone gained a good amount of experience with implementing multiple algorithms and 11 Avatar wikia technologies experience together for in an improved games. References 1. http://www.opengl.org/wiki/Geometry_Shader. 2. William E. Lorensen, and Harvey E. Cline. Marching Cubes: A High Resolution 3D Surface Reconstruction Algorithm. Computer Graphics, Volume 21, Number 4, July 1987. 3. Paul Bourke. Perlin Noise and Turbulence, 2000. http://paulbourke.net/texture_colour/perlin/. 4. http://msdn.microsoft.com/en-us/library/windows/desktop/bb206271(v=vs.85).aspx 5. Ken Perlin. Coherent Noise Function. http://mrl.nyu.edu/~perlin/ 6. Ken Perlin. Making Noise. http://www.noisemachine.com/talk1/index.html 7. Ryan Geiss. GPU Gems 3. http://http.developer.nvidia.com/GPUGems3/gpugems3_ch01.html. NVIDIA Corporation 2007. 8. Ryan Geiss, and Michael Thompson. "NVIDIA Demo Team Secrets—Cascades." Presentation at Game Developers Conference 2007. (Accessed Online at http://developer.download.nvidia.com) 9. http://www.opengl.org/wiki/Fixed_Function_Pipeline 10. http://msdn.microsoft.com/en-us/library/windows/desktop/bb206274(v=vs.85).aspx 11. http://james-camerons-avatar.wikia.com/wiki/Hallelujah_Mountains Appendix Appendix Figure 1: Perlin Noise points in 3D Appendix Figure 2: Perlin Noise points in 3D Appendix Figure 3: Perlin Noise points in 3D Appendix Figure 4: Parametric equation of a sphere Appendix Figure 5: Parametric equation of a sphere (outlining triangles formed) Appendix Figure 6: Perlin Noise with Marching Cube Algorithm Appendix Figure 7: Perlin Noise with Marching Cube Algorithm Appendix Figure 8: Parametric Equation of a Torus viewed in GamePipe Game Engine Appendix Figure 9: Perlin Noise generation viewed in GamePipe Game Engine