Hello, Lumpy World!

Overview: how to generate 3D terrain for iOS. This assumes basic knowledge of iOS programming (how to create a project and add files to it.)

I’ve been working on an iOS app that uses cocos3d and Robert’s hill-generating algorithm, which make nice hills that remind me of early Mario levels. For example:

Nice starter world
My lovely verdant lumps.

You can download the hill generation library from Github. I’ve converted Rob’s code from C++ to Objective-C and added support rectangular terrain (instead of just square terrain).

To generate your own terrain with it, download and install cocos3d and create a new cocos3d2 project called “TerrainGenerator”. It’ll create a 3D “Hello, world!” application. Hit the “Play” button to make sure it runs:

hello, world
Absolutely gorgeous.

To add the hill generation code, download the library from Github and add HillTerrain.m and HillTerrain.h to your project.

Replace the contents of TerrainGeneratorScene.m with:

#import "TerrainGeneratorScene.h"
#import "CC3PODResourceNode.h"
#import "CC3ActionInterval.h"
#import "CC3MeshNode.h"
#import "CC3Camera.h"
#import "CC3Light.h"
#import "CC3ParametricMeshNodes.h"
#import "HillTerrain.h"


@implementation TerrainGeneratorScene

-(void) dealloc {
  [super dealloc];
}

-(void) initializeScene {
  // Lights
  CC3Light* lamp = [CC3Light nodeWithName: @"Lamp"];
  lamp.location = cc3v(-2.0, 0.0, 0.0);
  lamp.isDirectionalOnly = NO;

  // Camera
  CC3Camera* cam = [CC3Camera nodeWithName: @"Camera"];
  cam.location = cc3v(128.0, 0.0, 300.0);
  [cam rotateByAngle:15 aroundAxis:cc3v(1.0, 0.0, 0.0)];
  [cam addChild: lamp];
  [self addChild: cam];

  // Action! (Well, okay, just terrain.)
  [self createTerrain];

  // OpenGL fun.
  [self createGLBuffers];
  [self releaseRedundantContent];
  [self selectShaderPrograms];
}

-(void) createTerrain {
  HillTerrain *terrain = [[HillTerrain alloc] init];
  [terrain generate];

  CC3MeshNode *mesh = [[[CC3MeshNode alloc] init] retain];
  mesh.vertexContentTypes =
    kCC3VertexContentLocation | kCC3VertexContentColor | kCC3VertexContentNormal;
  [mesh populateAsRectangleWithSize:CGSizeMake(terrain.size.width, terrain.size.height)
                  andRelativeOrigin:CGPointZero
                    andTessellation:CC3TessellationMake(terrain.size.width-1,
                                                        terrain.size.height-1)];

  int count = 0;
  for (int j = 0; j < terrain.size.height; ++j) {
    for (int i = 0; i < terrain.size.width; ++i) {
      float height = [terrain getCell:cc3p(i,j)];
      [mesh setVertexLocation:CC3VectorMake(i, j, height * 128) at:count];
      [mesh setVertexColor4F:ccc4f(0.0, height, 1.0-height, 1.0) at:count];
      ++count;
    }
  }

  [mesh setShouldUseLighting:YES];
  [self addChild:mesh];
  [terrain release];
}

@end

Save and run, and you should see something like this:

Default terrain
Hello, lumpy world!

The createTerrain method is where all of the magic happens, so let’s take it one section at a time:

Generating the terrain

Here’s the part that actually generates the terrain:

  HillTerrain *terrain = [[HillTerrain alloc] init];
  [terrain generate];

You can change any of the parameters you want here, for example, let’s try a different seed:

  HillTerrain *terrain = [[HillTerrain alloc] init];
  [terrain setSeed:123]; // Because I'm so creative.
  [terrain generate];

This generates a nice range of mountains:

A blobby range of mountains
The majestic Blob Range

Creating something OpenGL can use

The next part is mapping this terrain onto an array of vertexes. First, we create a mesh, aka the surface we want to display. You can picture it like a fishing net you’ll be draping over the landscape.

  CC3MeshNode *mesh = [[[CC3MeshNode alloc] init] retain];

Specifying the types of storage we’ll need

Next, we have to let Cocos3d know what type of info we want to store about this mesh. For us, this includes:

  1. The location of each vertex, because that’s the point of all this.
  2. The color at each vertex, because it’ll vary based on height.
  3. The normal of each vertex, which is the direction light will bounce off of it. This is automatically populated by Cocos3d, so you don’t have to worry about it for now. However, it’s interesting to try removing this one and seeing what the terrain looks like.
  mesh.vertexContentTypes =
    kCC3VertexContentLocation | kCC3VertexContentColor | kCC3VertexContentNormal;

Allocating the mesh

Now that we’ve told Cocos3d the types of storage we need, we tell it the shape and size to allocate: a rectangular grid the same size as our terrain.

  [mesh populateAsRectangleWithSize:CGSizeMake(terrain.size.width, terrain.size.height)
                  andRelativeOrigin:CGPointZero
                    andTessellation:CC3TessellationMake(terrain.size.width-1,
                                                        terrain.size.height-1)];

andTessellation specifies how many squares across the terrain will be, so you want that number to be one fewer than the number of vertexes you have. For example, if you had a tessellation of 1 square by 1 square, you’d need 4 vertexes (one for each corner of the square). Thus, the -1s.

Mapping the terrain onto the mesh

First, we get the height at one of the map coordinates. This will be a number between 0 and 1, so we’ll scale it up to something reasonable, given our scale (height*128 in this case).

      float height = [terrain getCell:cc3p(i,j)];
      [mesh setVertexLocation:CC3VectorMake(i, j, height * 128) at:count];

Then we set the color, making the high locations greenest and the lowest locations bluest:

      [mesh setVertexColor4F:ccc4f(0.0, height, 1.0-height, 1.0) at:count];

The at:count indicates the index of the vertex we’re setting. Cocos3d keeps all of the vertexes in a big array and this is the index into it.

Turning on the lights

Finally, we tell OpenGL to actually apply light to the mesh. (Try removing this line and see what it looks like.)

  [mesh setShouldUseLighting:YES];

And we add the mesh as a child to our scene and free the terrain memory:

  [self addChild:mesh];
  [terrain release];

That’s it! If anyone has any suggestions or improvements, please feel free to file a pull request. I’m not too happy with island generation yet: I haven’t figured out the right combination of options so my islands aren’t just lumps.

If you’re interested, Rob’s algorithm is pretty interesting and easy-to-follow. I recommend reading through his description if you plan on using it in a project.

Achievement Unlocked: Found bug in LLVM debugger (maybe)

Also, on an unrelated note, I seem to have found a bug in LLDB (the LLVM debugger) in the making of this blog post, which makes me inordinately proud.

2 thoughts on “Hello, Lumpy World!

    1. Unfortunately, no. I feel like a jerk, but I couldn’t figure out how to make a reproducible case that was less than about a MB so I gave up 😦

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: