Final Project Paper with appendix

advertisement
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
Download