C#、WPF
ランダムウォークを試しましたが、予想よりもうまく機能します。マップのどこかから開始し、ランダムに隣接するタイルまで歩き、その高さの値を増やしてから、次へと移動します。これは数千回繰り返され、最終的に次のような高さマップになります(100 x 100)。
次に、マップを「離散化」し、値の数を指定された高さレベルに減らし、その高さに基づいて地形/色を割り当てます。
より類似した群島のような地形:
より多くの山岳地形を得るために、ランダムなステップと高さレベルの数を増やしました:
コード
機能:ボタンで地形を再作成します。3D地形と2Dマップを表示します。ズーム(マウスホイール)および3Dスクロール(矢印キー)。ただし、パフォーマンスはあまりよくありません。結局のところ、これはDirectXやOpenGLではなく、純粋にWPFで記述されています。
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;
}
}
}