Sunday, January 25, 2015

Voxel Terrain Generation and Rendering

This time I'll present 3DWorld's voxel terrain system.

I'm generating a 3D array of noise values using different noise functions, computed either on the CPU or on the GPU using a custom fragment shader. The current set of noise functions supported are Perlin noise, Simplex noise, and sum-of-products-of-sines noise (of my own invention). They all seem to produce the same sort of lumpy terrain; in fact, the only real difference is computation time. Naturally, it's faster to generate noise values on the GPU, though reading back all that floating-point data takes around half the time of the CPU noise generation. This inefficiency is partly because I have to generate the volume in 2D slices, to match a 2D frame buffer. If anyone manages to invent a 3D frame buffer then let me know - or I suppose I could give in and use Nvidia-specific compute shaders or CUDA. In the end, it only takes 200ms to generate 16M noise values (64 slices) for a 512x512x64 scene, which comes to 64MB of data.

A big array of noise values is great, but it needs to be rendered. I chose to use the Marching Cubes algorithm because it's fairly simple (compared to the alternatives) and the crazy lookup tables needed for MC can be found online. There are some ambiguous cases in MC, but they tend to not show up often in the scenes I create and I prefer simplicity and efficiency over perfect quality, at least for now. So I can use MC to get triangles, then connect them together and compute the shared vertices so I can render with indexed triangles. This seems to be the most efficient way to render large triangle datasets using OpenGL, and is more compact than storing individual triangles. I divide the scene up into XY tiles (I use Z=up). 16x16 tiles seems to work well, and this division gives some nice benefits:
  • Each block can be generated independently, so I can use openmp to run separate threads on each core. 8 threads (4 core + hyperthreading) gives me about a 5.5x speedup.
  • View frustum culling can be used to drop invisible blocks when rendering
  • Individual blocks can be modified, which is fast enough for realtime voxel editing

Of course I skipped a few steps here, including handling of the mesh boundary, clipping the bottom of the volume with the heightmap floor, removal of disconnected "floaters", etc. This is all pretty standard stuff so I won't go into all of those details. One more important thing is lighting: these voxel scenes just don't look right with simple direct lighting. They need real ambient, and fortunately it's easy to compute ambient occlusion by ray marching through the 3D volume and tracking the visibility of each voxel (again using openmp). I also run path tracing on the triangle mesh like in my previous lighting post with the Sponza scene screenshots. This gives multiple bounce indirect lighting from the sun, moon, etc. for each voxel, which is recomputed (using openmp) when the light sources change.

Here is a screenshot of a 128x128x128 voxel rock/mountain that uses triplanar texturing with different textures + procedural texture selection to give it a variety of rock/grass features. I also placed some dynamic grass blades on the top surfaces of the rock. The shadows and ambient occlusion really bring out the dark crevasses in the rock and make it come to life.

Voxel mountain covered in grass, with indirect lighting, ambient occlusion, and shadows.


Voxel mountain with longer grass blades and denser grass.


Here is another screenshot of a 512x512x64 voxel snowy/icy landscape. The normal maps on the surfaces add nice specular highlights to the snow. The ambient lighting and shadows give depth to the scene, and the indirect lighting is responsible for the blue vs. white coloring. The blue areas are mostly lit by the sky but the white areas include indirect reflections from the sun on the snowy ground and icy walls. I have the indirect term set higher than what is realistic to make the indirect lighting stand out.

If you look at the ridge line on the right, there are some strange cube shapes placed there. These are the results of my voxel editing tests on this scene. I have implemented realtime addition and removal of volumes using different brush shapes (sphere, cube, etc.), sizes, and weights. These brush strokes can be saved to files and re-applied when the scene is loaded after the procedural generation is finished. I decided to store the brushes instead of the raw volume data because it allows for tiny file sizes. It would take a huge amount of time for a user to accumulate 64MB of brush strokes to make the save file exceed the size of the floating-point volume data!

Procedurally generated ice/snow caves + user edits with indirect lighting, ambient occlusion, shadows, and normal mapping.

Here is a zoomed out view of the entire scene - there is a surprising amount of data here! You can easily get lost walking around in this cave system, which is almost entirely connected and walkable. This is 16M voxels, which is converted to around 2M triangles for rendering. I'm getting a nice 300 FPS here with all the effects turned on, even though the camera is too far away to see the details. Oh, and that round hole in the bottom left is a tunnel I made horizontally through the entire scene.

Zoomed out view of voxel ice cave scene.

And finally, that same scene with more interesting lighting: 100 moving, light emitting, colored spheres. This time the sun and moon are both below the horizon for a nice dark night, which makes the colorful light sources stand out. The normal mapping produces some nice specular reflections, though they're a little hard to see at this view distance. I'm still getting a high frame rate of 130 FPS here, even with all the lighting computations for 100 lights.

Ice caves scene with 100 dynamic colored lights.

If I was more of an artist, I would show you some cool screenshots of what I created with the voxel editing tools. However, I was never really into art, and the procedural terrain looks pretty good to me. I'm hoping that I can integrate voxel content into other 3DWorld scene types in the future. 3DWorld already supports the addition of non-voxel polygon objects into voxel-based scenes. For example, I can put the Sponza atrium model on top of these voxels or inside of a large cave. The first person shooter "smiley killer" game can be played in these caves (if you could ever find something to shoot at in the maze). [Yes, I know, I need to post something about that game here since it's not just an engine.]

Well, that's about it for this post. I'll try to put some more screenshots of cool stuff I created with voxels into a later post. I'm sure there's a lot I can do with 3DWorld's voxel framework.

3 comments:

  1. Typo: "can use openmp do run" should be "... to run"
    I like the bumped up indirect lighting intensity! I've experimented with net-positive diffuse, which has some neat results.
    I like the idea of storing "brush strokes" instead of the rasterized results. Minecraft's editing tools always felt tiresome with the single voxel brush.

    ReplyDelete
    Replies
    1. Oh, forgot to mention that a "brush" is basically the way that I programmed my Minecraft feature generation. It wasn't a feature of the engine itself, so I had to add the functionality manually. I haven't looked at your source code to see what you're method is, but you can see the way I did it in the "taperedcylinder()" method here: http://peripheralarbor.com/minecraft/Forester.py

      Delete
    2. Thanks, I fixed the typo.

      Storing and re-applying brush strokes worked pretty well for voxel terrain editing, and I used a similar approach for regular terrain editing later. The save file is very small, and all of my saves were very fast to apply. I imagine if you spent hours applying thousands of brush strokes then it could take a minute or more to re-apply them all.

      I haven't done any programming for Minecraft. I prefer rounded voxels to that flat and pixelated style. I use spherical and cube brushes with different falloff functions at the edges for voxel terrain editing, and vertical cylinders (circles in XY) for 2D heightmap editing. Large brushes can be slow to apply, so I use multiple threads to update chunks when a large brush spans more than one of them.

      Delete