Gamasutra - Feature - "A Real-Time Procedural Universe, Part Four: Dynamic Ground Textures and Objects"
It's free to join Gamasutra!|Have a question? Want to know who runs this site? Here you go.|Targeting the game development market with your product or service? Get info on advertising here.||For altering your contact information or changing email subscription preferences.
Registered members can log in here.Back to the home page.

Search articles, jobs, buyers guide, and more.

By Sean O'Neil
[Author's Bio]

Gamasutra
January 12, 2006

Introduction

Planetary Factories

Volumetric Clouds

Printer Friendly Version
   

Change Login/Pwd
Post A Job
Post A Project
Post Resume
Post An Event
Post A Contractor
Post A Product
Write An Article
Get In Art Gallery
Submit News

 


 


Latest Letters to the Editor:
Perpetual Layoffs by Alexander Brandon [09.21.2007]

Casual friendliness in MMO's by Colby Poulson [09.20.2007]

Scrum deals and 'What is Scrum?' by Tom Plunket [08.29.2007]


[Submit Letter]

[View All...]
  



Upcoming Events:
Video Game Expo (VGXPO)
Philadelphia, United States
11.21.08

DIG London Game Conference
London, Canada
11.27.08

5th Australasian Conference on Interactive Entertainment
Brisbane, Australia
12.03.08

IEEE Symposium on Computational Intelligence and Games
Perth, Australia
12.15.08

2K Bot Prize
Perth, Australia
12.15.08

[Submit Event]
[View All...]

 


[Enter Forums...]

Note: Discussion forums for Gamasutra are hosted by the IGDA, which is free to join.
 


Features

A Real-Time Procedural Universe,
Part Four:
Dynamic Ground Textures and Objects

Planetary Factories

To separate the node class from the code building the planet, I created a class called CPlanetaryFactory. Each class derived from CPlanetaryFactory adds something different to the planet, and factories can be combined in different ways to build different types of planets. The CPlanetaryMap class keeps a list of factories for each planet, and when a node in the quad-tree needs to be built, each factory's BuildNode() method is called. Just in case the factory creates something that needs to be cleaned up, each factory's DestroyNode() method is called when a node is destroyed.

Each factory object is initialized with a CPropertySet object, which is a simple map of named properties. This property set is currently loaded from a configuration file called Planet_Quad.conf. A custom RTTI implementation is used to allow class names to be specified in the configuration file, and to keep CPlanetaryMap from having to know how to create and initialize each factory class. This should make it easy for you to create your own factory class, load its type class, and then change the config file to use it without having to change CPlanetaryMap or CPlanetaryMapNode. This is a very useful separation which can be extended to load factories from other DLLs using LoadLibrary. That would allow new factories to be added dynamically without changing or recompiling the main project.

Here is a list of the factories that currently exist in the Planet_Quad project. All of these are proof-of-concept factories. They are very simple and not very well optimized for now, so there are plenty of things you can do to improve them.

CSimpleHeightMapFactory

This factory loops through each sample point in the node, calculates the direction vector from the center of the planet to that sample point, and passes it to a 3D noise function to determine the height for that sample.

CMixedHeightMapFactory

This factory is just like CSimpleHeightMapFactory, but it pre-generates the first few octaves of noise into buffers. This takes up extra memory and incurs a small performance hit when the demo is launched, but it speeds up the node building process noticeably. It would also be easy to change this so that the pre-generated buffers were created without using noise-based fractals. You could “grow” the planet using plate tectonics, volcanoes, impacts, and erosion. You could also use artist-generated height maps.

CSimpleColorFactory

This factory takes the height of each sample and applies a color to it. This factory is very simple, using height as its only criteria. It would be pretty easy to add latitude or other variables into the calculations, or to build a pre-generated map (i.e. if you “grow” your planet).

CSimpleCraterFactory

This factory adds millions, or even billions, of craters to the planetary surface fairly quickly and without using any memory. It does this by creating a “virtual” quad-tree (i.e. it creates the nodes it needs on the fly). To traverse this non-existent tree, instead of going to a child node, the random seed is altered in such a way that each virtual “child node” has a unique repeatable seed (or as close to unique as we can get it). A simple for loop runs through the levels, adjusting the height of each sample when it finds a “hit”. A very fast RNG (Random Number Generator) is important here. This virtual tree can be used for just about any type of special surface feature.

CGrassFactory

This factory is a very simple test to show that distinct objects can be added to the planet easily. It is just a proof-of-concept. The objects are grass billboards, which are managed in the factory object itself. This helps keep them all together for quick batched rendering, which is important for a large number of small similar objects like grass billboards. For objects that can move around and bump into each other, or for objects where rendering order is important, it would probably be better to manage the objects in the quad-tree.

Dynamically-Generated Ground Textures

Because each node is square and a small height map is generated for each one, it is fairly easy to dynamically generate and apply ground textures. There are a few complications to worry about, and I will explain them here. To start with, here are some constants I have defined in PlanetaryMapNode.h:

#define NODE_WIDTH        8
#define HEIGHT_MAP_SCALE  2

// The surface map will have 9x9 vertices
#define SURFACE_MAP_WIDTH (NODE_WIDTH+1)
#define SURFACE_MAP_COUNT (SURFACE_MAP_WIDTH*SURFACE_MAP_WIDTH)

// The height map will be a multiple of the surface map (i.e. 17X17 or 33x33)
#define HEIGHT_MAP_WIDTH (NODE_WIDTH*HEIGHT_MAP_SCALE+1)
#define HEIGHT_MAP_COUNT (HEIGHT_MAP_WIDTH*HEIGHT_MAP_WIDTH)

// The height map resolution plus a border surrounding it for normal calculations
#define BORDER_MAP_WIDTH (HEIGHT_MAP_WIDTH+2)
#define BORDER_MAP_COUNT (BORDER_MAP_WIDTH*BORDER_MAP_WIDTH)

The surface map size is used for the vertex map, and the height map size is used for the texture and normal maps. The vertex map size is currently fixed at 9x9, and can't be changed without rewriting some of the code used in CPlanetaryNode. The texture/normal map size can be changed by changing HEIGHT_MAP_SCALE. Using a scale of 2, each node gets a 17x17 texture map and a 17x17 normal map. A scale of 4 (33x33) makes the ground textures look very sharp, but it brings the system to a crawl when a lot of nodes need to be split. These are odd sizes for a texture, but keep in mind that the nodes share edges, and the edges of neighboring texture maps must match perfectly to avoid visible seams. The edges of neighboring textures generated from the 17x17 height maps must always match, but re-sampling it down to 16x16 would throw them off.

Fortunately, it is easy to create a 1024x1024 texture (or larger) and treat it like an array of smaller textures. The CTextureArray class handles all of this and makes it relatively painless. It doesn't matter whether the smaller textures inside it are an odd size or not. Odd texture sizes simply cause a little space to be wasted at the edges of the larger parent texture. The CTextureArray class has a method for generating a texture matrix that maps an arbitrary rectangle perfectly to a sub-texture, which makes it easy to set up texture coordinates for each node.

The normal map generation is so similar to the texture map generation that I use the same type of sub-textures for them, and I put them in the same 1024x1024 texture array. The 8-bit RGB values don't allow much precision, but they seem to be sufficient in this case. The only issue we need to worry about is that we need to generate an extra set of height values around the border of the height map to get accurate normals at the edges. It increases the expense of building a node, but without it seams appear at node boundaries.


Figure 3. Visible seam between levels

No matter what you do, seams will show up at the boundary between levels in the quad-tree (i.e. between levels 5 and 6). I'm not certain whether anything can make it go away completely because one node will have more detail and its neighbor will have less. However, there are ways to make this less noticeable. First, keep the texture resolution high. The closer each texel is to one pixel in size, the less visible the problem will be. Second, try to avoid very high-frequency changes in the color or normal maps. If either changes a lot from one level to the next, the seam between levels will be much more noticeable than if the changes are smoother.

Atmospheric Scattering

The ScatterCPU demo was originally published and explained in a GameDev article. The ScatterGPU demo was originally published and explained in a GPU Gems 2 article. Read the articles for an explanation of these algorithms. ScatterCPU is a more accurate approximation of scattering, but it is slower and eats up too much CPU time. Because the Planet_Quad demo uses the version from ScatterGPU, I'll mention some of the problems you may notice with it:

  1. GeForce cards produce very noticeable visual artifacts around the Mie scattering. It looks like lines of color discontinuity. At first glance it appears to be a tesselation artifact, but the discontinuity lines do not line up with triangle edges, and increasing the tesselation level does not improve image quality. The developers I worked with from nVidia on GPU Gems 2 couldn't figure out what was causing this.
  2. The artifacts mentioned above do not show up on Radeon cards. The color gradient looks very smooth on them, but otherwise the colors look a bit off when compared to the GeForce cards.
  3. Some tesselation artifacts are visible when viewing the sky dome from space. Scattering is exponential but interpolation between vertices is linear. Instead of drawing a full sphere for the sky dome all the time, which will never be completely visible, I should be drawing a wedge optimized for the current viewing angle.
  4. The ground is too dark near sunrise/sunset. From space, it actually gets dark before the sky above it does, and this looks bad from certain angles. This is because right after the sun sets, the air above the ground can still see the sun (it is not in the shadow of the planet yet), so it is still lit. Secondary scattering is not calculated or approximated (i.e. skylight is not projected onto the ground), so the ground underneath it is dark when the sky is still lit.
  5. Sunrises and sunsets viewed from space are not as yellow/orange/red as they are in ScatterCPU. I believe this is due to inaccuracies in the scale function for angles greater than 90 degrees. The lookup table in ScatterCPU is fairly accurate. The scale function that attempts to approximate part of it in ScatterGPU diverges sharply when the angle goes above 90 degrees.
 


join | contact us | advertise | write | my profile
news | features | companies | jobs | resumes | education | product guide | projects | store



Copyright © 2005 CMP Media LLC

privacy policy
| terms of service