C#, WPF
I have experimented with a random walk, which works better than I had anticipated. I start somewhere on the map, walk to a random adjacent tile and increment its height value, then move to the next and so on. This is repeated thousands of times and eventually leads to a height map like this (100 x 100):
Then, I "discretize" the map, reduce the number of values to the given height levels and assign terrain/color based on that height:
More similar archipelago-like terrains:
Increased number of random steps and height levels to get more mountainous terrain:
Code
Features: Recreate terrain with a button. Show 3D terrain and 2D map. Zooming (mouse wheel) and 3D scrolling (arrow keys). But it's not very performant - after all, this is written purely in WPF, not DirectX or OpenGL.
MainWindow.xaml:
<Window x:Class="VoxelTerrainGenerator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Voxel Terrain Generator" Width="550" Height="280" KeyUp="Window_KeyUp">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Viewport3D x:Name="ViewPort" MouseWheel="ViewPort_MouseWheel">
<Viewport3D.Camera>
<OrthographicCamera x:Name="Camera" Position="-100,-100,150" LookDirection="1,1,-1" UpDirection="0,0,1" Width="150" />
<!--<PerspectiveCamera x:Name="Camera" Position="-100,-100,150" LookDirection="1,1,-1" UpDirection="0,0,1" />-->
</Viewport3D.Camera>
</Viewport3D>
<Grid Grid.Column="1" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Image Grid.Row="0" x:Name="TopViewImage"/>
<Button Grid.Row="1" Margin="0 10 0 0" Click="Button_Click" Content="Generate Terrain" />
</Grid>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Media.Media3D;
namespace VoxelTerrainGenerator
{
public partial class MainWindow : Window
{
const int RandomSteps = 20000;
const int MapLengthX = 100;
const int MapLengthY = 100;
const int MaxX = MapLengthX - 1;
const int MaxY = MapLengthY - 1;
const bool ForceIntoBounds = true;
readonly Random Random = new Random();
readonly List<Color> ColorsByHeight = new List<Color>
{
Color.FromArgb(0, 0, 50),
Color.FromArgb(170, 170, 20),
Color.FromArgb(0, 150, 0),
Color.FromArgb(0, 140, 0),
Color.FromArgb(0, 130, 0),
Color.FromArgb(0, 120, 0),
Color.FromArgb(0, 110, 0),
Color.FromArgb(100, 100, 100),
};
public MainWindow()
{
InitializeComponent();
TopViewImage.Width = MapLengthX;
TopViewImage.Height = MapLengthY;
}
public int[,] CreateRandomHeightMap()
{
var map = new int[MapLengthX, MapLengthY];
int x = MapLengthX/2;
int y = MapLengthY/2;
for (int i = 0; i < RandomSteps; i++)
{
x += Random.Next(-1, 2);
y += Random.Next(-1, 2);
if (ForceIntoBounds)
{
if (x < 0) x = 0;
if (x > MaxX) x = MaxX;
if (y < 0) y = 0;
if (y > MaxY) y = MaxY;
}
if (x >= 0 && x < MapLengthX && y >= 0 && y < MapLengthY)
{
map[x, y]++;
}
}
return map;
}
public int[,] Normalized(int[,] map, int newMax)
{
int max = map.Cast<int>().Max();
float f = (float)newMax / (float)max;
int[,] newMap = new int[MapLengthX, MapLengthY];
for (int x = 0; x < MapLengthX; x++)
{
for (int y = 0; y < MapLengthY; y++)
{
newMap[x, y] = (int)(map[x, y] * f);
}
}
return newMap;
}
public Bitmap ToBitmap(int[,] map)
{
var bitmap = new Bitmap(MapLengthX, MapLengthY);
for (int x = 0; x < MapLengthX; x++)
{
for (int y = 0; y < MapLengthY; y++)
{
int height = map[x, y];
if (height > 255)
{
height = 255;
}
var color = Color.FromArgb(255, height, height, height);
bitmap.SetPixel(x, y, color);
}
}
return bitmap;
}
public Bitmap ToColorcodedBitmap(int[,] map)
{
int maxHeight = ColorsByHeight.Count-1;
var bitmap = new Bitmap(MapLengthX, MapLengthY);
for (int x = 0; x < MapLengthX; x++)
{
for (int y = 0; y < MapLengthY; y++)
{
int height = map[x, y];
if (height > maxHeight)
{
height = maxHeight;
}
bitmap.SetPixel(x, y, ColorsByHeight[height]);
}
}
return bitmap;
}
private void ShowTopView(int[,] map)
{
using (var memory = new System.IO.MemoryStream())
{
ToColorcodedBitmap(map).Save(memory, ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new System.Windows.Media.Imaging.BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
TopViewImage.Source = bitmapImage;
}
}
private void Show3DView(int[,] map)
{
ViewPort.Children.Clear();
var light1 = new AmbientLight(System.Windows.Media.Color.FromArgb(255, 75, 75, 75));
var lightElement1 = new ModelUIElement3D();
lightElement1.Model = light1;
ViewPort.Children.Add(lightElement1);
var light2 = new DirectionalLight(
System.Windows.Media.Color.FromArgb(255, 200, 200, 200),
new Vector3D(0, 1, -0.1));
var lightElement2 = new ModelUIElement3D();
lightElement2.Model = light2;
ViewPort.Children.Add(lightElement2);
for (int x = 0; x < MapLengthX; x++)
{
for (int y = 0; y < MapLengthY; y++)
{
int height = map[x, MapLengthY-y-1];
for (int h = 0; h <= height; h++)
{
Color color = ColorsByHeight[h];
if (height > 0 && h == 0)
{
// No water under sand
color = ColorsByHeight[1];
}
ViewPort.Children.Add(CreateCube(x, y, h, 1,
System.Windows.Media.Color.FromArgb(255, color.R, color.G, color.B)));
}
}
}
}
private ModelVisual3D CreateCube(int x, int y, int z, int length,
System.Windows.Media.Color color)
{
List<Point3D> positions = new List<Point3D>()
{
new Point3D(x, y, z),
new Point3D(x + length, y, z),
new Point3D(x + length, y + length, z),
new Point3D(x, y + length, z),
new Point3D(x, y, z + length),
new Point3D(x + length, y, z + length),
new Point3D(x + length, y + length, z + length),
new Point3D(x, y + length, z + length),
};
List<List<int>> quads = new List<List<int>>
{
new List<int> {3,2,1,0},
new List<int> {0,1,5,4},
new List<int> {2,6,5,1},
new List<int> {3,7,6,2},
new List<int> {3,0,4,7},
new List<int> {4,5,6,7},
};
double halfLength = (double)length / 2.0;
Point3D cubeCenter = new Point3D(x + halfLength, y + halfLength, z + halfLength);
var mesh = new MeshGeometry3D();
foreach (List<int> quad in quads)
{
int indexOffset = mesh.Positions.Count;
mesh.Positions.Add(positions[quad[0]]);
mesh.Positions.Add(positions[quad[1]]);
mesh.Positions.Add(positions[quad[2]]);
mesh.Positions.Add(positions[quad[3]]);
mesh.TriangleIndices.Add(indexOffset);
mesh.TriangleIndices.Add(indexOffset+1);
mesh.TriangleIndices.Add(indexOffset+2);
mesh.TriangleIndices.Add(indexOffset+2);
mesh.TriangleIndices.Add(indexOffset+3);
mesh.TriangleIndices.Add(indexOffset);
double centroidX = quad.Select(v => mesh.Positions[v].X).Sum() / 4.0;
double centroidY = quad.Select(v => mesh.Positions[v].Y).Sum() / 4.0;
double centroidZ = quad.Select(v => mesh.Positions[v].Z).Sum() / 4.0;
Vector3D normal = new Vector3D(
centroidX - cubeCenter.X,
centroidY - cubeCenter.Y,
centroidZ - cubeCenter.Z);
for (int i = 0; i < 4; i++)
{
mesh.Normals.Add(normal);
}
}
Material material = new DiffuseMaterial(new System.Windows.Media.SolidColorBrush(color));
GeometryModel3D model = new GeometryModel3D(mesh, material);
ModelVisual3D visual = new ModelVisual3D();
visual.Content = model;
return visual;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
int[,] map = CreateRandomHeightMap();
int[,] normalizedMap = (Normalized(map, ColorsByHeight.Count-1));
ShowTopView(normalizedMap);
Show3DView(normalizedMap);
ToBitmap(Normalized(map, 255)).Save("heightmap-original.png");
ToBitmap(Normalized(normalizedMap, 255)).Save("heightmap.png");
ToColorcodedBitmap(normalizedMap).Save("terrainmap.png");
}
private void ViewPort_MouseWheel(object sender, MouseWheelEventArgs e)
{
// Zoom in or out
Camera.Width -= (double)e.Delta / 100;
}
private void Window_KeyUp(object sender, KeyEventArgs e)
{
// Scrolling by moving the 3D camera
double x = 0;
double y = 0;
if (e.Key == Key.Left)
{
x = +10;
y = -10;
}
else if (e.Key == Key.Up)
{
x = -10;
y = -10;
}
else if (e.Key == Key.Right)
{
x = -10;
y = +10;
}
else if (e.Key == Key.Down)
{
x = +10;
y = +10;
}
Point3D cameraPosition = new Point3D(
Camera.Position.X + x,
Camera.Position.Y + y,
Camera.Position.Z);
Camera.Position = cameraPosition;
}
}
}
Minecraft esque rendered cubes does not equal voxels. Also is true isometric projection required, or is the the word used loosely as is common in games http://en.wikipedia.org/wiki/Video_games_with_isometric_graphics
– shiona – 2014-03-02T21:34:23.483@shiona: The description of the topic was changed a few days ago to say paralell projected, so anything non-perspective should count. As for voxels: I think minecraftesqe cubes are valid in terms of being voxels: they can be considered huge pixels on a large 3D grid. – SztupY – 2014-03-02T22:45:03.623
No, Minecraftesque cubes are not voxels, because voxels are not cubes, just like how pixels are not squares. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.79.9093
– Pseudonym – 2014-03-03T03:18:26.253I agree with @Pseudonym sort of. I think it is valid though if you want them to be cubes. It does eliminate pretty much every other voxel rasterization technique though. – Tim Seguine – 2014-03-07T10:26:06.490