Pythonを使用して数値標高モデル（DEM）からMinecraftの地形を作成する


![2023-07-16_12.51.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/8b639d8a-e3b4-42e5-8670-9e9db8e7850f.png)


# はじめに
この記事では、Pythonのライブラリであるanvil-parserを使用して、数値標高モデル（DEM）からMinecraft（Java版）のワールドデータを作成する方法を紹介します。この方法により、実世界の地形をMinecraftで再現することが可能になります。

# DEMデータをダウンロードする

まずは[基盤地図情報ダウンロードサイト](https://fgd.gsi.go.jp/download/menu.php)よりDEMのデータをダウンロードします。

今回は岐阜県岐阜市のこちらのメッシュ（533616）を使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/328620b4-6949-7893-3767-feb218ab6534.png)

ダウンロードしたDEMはそのままでは利用できないので、QGISの[QuickDEM4JP](https://plugins.qgis.org/plugins/QuickDEM4JP/)プラグインを使用してtiffファイルに変換します。

詳しい使い方はこちらの記事を参照してください。
[「国土地理院の標高データ（DEM）をQGIS上でサクッとGeoTIFFを作って可視化するプラグインを公開しました！（Terrain RGBもあるよ）」](https://qiita.com/nokonoko_1203/items/b99aa733cb215305f8aa)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/5455f245-52da-30b4-f10a-fe1e0ab638a0.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/62c20273-4b76-fc0c-d793-4e5dd114e175.png)

QGIS上に表示したDEM（EPSG：6675）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/295e1a36-27f5-d008-6b11-ccb6517b305e.png)

# マイクラのワールドに変換する
データの用意ができたので、ここからはPythonでワイクラのワールドに変換していきます。



# 1.DEMの解像度を上げる

マインクラフトの世界は設定上1ブロック＝1mとなっています。これに合わせるため、gdalを使用して、DEMのラスター解像度を1m x 1mに変換します。今回使用するDEMは日本測地系なので、1ピクセル＝1mとなります。

インストール
```bash
pip install gdal
```
コード

```python
from osgeo import gdal

# 入力ファイルを開く
tiff = gdal.Open("553616_dem.tiff", gdal.GA_ReadOnly)

# 変換パラメーターを設定
dst_size_x = 1
dst_size_y = 1

# 立方体補間(Cubic Convolution) で解像度を変更する
warp_options = gdal.WarpOptions(xRes=dst_size_x, yRes=dst_size_y, resampleAlg=gdal.GRIORA_Cubic)

# ラスターの変換を実行
dst = gdal.Warp("553616_dem_1m.tiff", tiff, options=warp_options)

# ファイルをクローズ
src = None
dst = None
```

# 2.DEMをMinecraftのワールド座標系に変換する
次に、このDEMがもつ地理座標系の三次元表記をマイクラのワールド座標系の三次元表記に合わせていきます。

地理座標系のX、Y、Zの三次元表記は以下になります

X座標: 東西の位置を表します。東に行くほど数値が増え、西に行くほど数値が減ります。
Y座標: 南北の位置を表します。北に行くほど数値が増え、南に行くほど数値が減ります。
Z座標（ある場合）: 高さ（標高）を表します。地上に行くほど数値が増え、地下に行くほど数値が減ります。

一方、マイクラのX、Y、Zの三次元表記は以下になります。

X座標: 東西の位置。東に行くほど数値が増え、西に行くほど数値が減ります。
Y座標: 高さ（標高）。地上に行くほど数値が増え、地下に行くほど数値が減ります。
Z座標: 南北の位置。南に行くほど数値が増え、北に行くほど数値が減ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/c4315573-7433-66f0-3e33-d76313144d09.png)
[画像出典「Minecraft Wiki　座標」](https://minecraft.fandom.com/ja/wiki/%E5%BA%A7%E6%A8%99)




また、マインクラフトの中心座標(ブロック座標[X,Z])は[０,０]になるので、その位置が今回のDEMの中心座標に来るようにする必要があります（そのままの座標位置でも問題ないが、最初のリスポーン地点からだいぶ離れた位置にできてしまうため）。

これらを踏まえると、
- Y座標の情報とZ座標の情報を入れ替える。
- X軸を反転させる。
- 中心の座標が[0,0]になるように全体をずらす。

といった処理が必要になります

下記のコードは、DEMが持つ地理空間データをMinecraftのワールド座標系に変換し、それを各ピクセルの中心座標と一緒にデータフレームに保存することを目的としています。

インストール
```bash
pip install rasterio
pip install numpy
pip install pandas
```
コード
```python
# ライブラリのインポート
import rasterio
import numpy as np
import pandas as pd

# 変換したラスターファイルを開く
with rasterio.open("553616_dem_1m.tiff") as src:

    # 幅と高さを取得
    width, height = src.width, src.height

    # 中心点のオフセットを計算
    # 偶数の場合は0.5、奇数の場合は0
    offset_x = 0.5 if width % 2 == 0 else 0
    offset_y = -0.5 if height % 2 == 0 else 0

    # トランスフォーメーション行列から中心座標を計算
    transform = src.transform
    center_x = transform.c + width / 2.0 * transform.a + offset_x
    center_y = transform.f + height / 2.0 * transform.e + offset_y

    # 数値標高データを取得
    data = src.read(1)

    # 各ピクセルの中心座標を取得
    y_indices, x_indices = np.indices(data.shape)
    x_coords = x_indices * transform.a + transform.c + transform.a / 2.0
    y_coords = y_indices * transform.e + transform.f + transform.e / 2.0

    # ピクセル座標と標高値を一次元化
    x_coords = x_coords.ravel()
    y_coords = y_coords.ravel()
    data = data.ravel()

    # マイクラの基準にする中心の座標を引く
    x_coords = x_coords - center_x
    y_coords = y_coords - center_y

    # y軸(北南)を反転。マイクラ座標に合わせるため
    y_coords = y_coords * -1

    # 小数点以下を排除
    x_coords = np.trunc(x_coords).astype(int)
    y_coords = np.trunc(y_coords).astype(int)
    data = np.trunc(data).astype(int)

    # マイクラのブロック座標からregion(.mca)のファイル名を取得
    region_x = (x_coords // 512).astype(int)
    region_z = (y_coords // 512).astype(int)

    # 文字列に変換
    region_x_str = np.char.mod("%d", region_x)
    region_z_str = np.char.mod("%d", region_z)

    # ベクトル化した文字列フォーマット操作を適用
    region = np.vectorize("r.{}.{}.mca".format)(region_x, region_z)

    # データフレームを作成
    df = pd.DataFrame({
        "x": x_coords,
        "z": y_coords,
        "y": data,
        "region": region
    })
```


まず、変換したラスターファイルを開き、その幅と高さを取得します。これらの情報を使用して、ラスター画像の中心座標を計算します。中心座標の計算には、特定のオフセット値を設定します。このオフセットは、ラスターの幅と高さが偶数か奇数かによって異なります。

具体的には、画像の幅と高さが偶数の場合、中心座標がピクセルの角に位置してしまいます。これを避けるため、中心座標を0.5ずつずらすオフセットを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/17270e6d-1a91-475f-6410-e69c1ca9d644.png)

このオフセットを用いて、ラスターのトランスフォーメーション（変換行列）を使用して中心座標を計算します。
今回使用したDEMの中心座標は`[-32150.744183319344 -60035.32409617323]`です。
この計算された中心座標は、マイクラのブロック座標系（x, z）の[0,0]点に対応します。この操作により、マイクラのブロック座標が地理座標系のどの座標点に対応するかが決定されます。


次にラスターの各ピクセルの中心座標を計算します。これらの座標は、後の計算のために一次元の配列に変換（平坦化）されます。
そして、各座標をMinecraftの基準[0,0]に合わせるために、中心座標を引きます。北と南の座標（y軸）は、Minecraftの座標系に合わせるために−１を乗算して反転します。その後、座標と標高データから小数点以下を切り捨て、整数型に変換します。

次に、Minecraftのワールド座標からregion(.mca)のファイル名を取得します。ここで注目すべきは、Minecraftのワールドは、512x512ブロックの領域、いわゆるregionに区切られているという点です。このregionは、それぞれが個別の.mcaファイルとして保存されています。



https://docs.fileformat.com/ja/game/mca/

したがって、各座標を512で割ることで、対応するregionの番号を取得します。この値は整数化され、文字列に変換されます。そして、フォーマット文字列操作が適用され、それぞれの座標が対応するregionファイル名に変換されます。


今回使用するDEMをregion(.mca)のファイル名のグリッドで区切ると以下のようになります。
必要なmcaファイルの数は480ファイルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/8a200da5-159e-376e-b41b-60d75bf150d0.png)




最後に、これらすべてのデータ（座標、標高、regionファイル名）をPandasのデータフレームに保存します。このデータフレームは、Minecraftのワールドデータの作成や他の後処理に利用します。


|           | x    | z    | y    | region     |
|-----------|------|------|------|------------|
| 0         | -5688| -4643| -9999| r.-12.-10.mca |
| 1         | -5687| -4643| -9999| r.-12.-10.mca |
| 2         | -5686| -4643| -9999| r.-12.-10.mca |
| 3         | -5685| -4643| -9999| r.-12.-10.mca |
| 4         | -5684| -4643| -9999| r.-12.-10.mca |
| ...       | ...  | ...  | ...  | ...        |
| 105658194 | 5684 | 4643 | -9999| r.11.9.mca |
| 105658195 | 5685 | 4643 | -9999| r.11.9.mca |
| 105658196 | 5686 | 4643 | -9999| r.11.9.mca |
| 105658197 | 5687 | 4643 | -9999| r.11.9.mca |
| 105658198 | 5688 | 4643 | -9999| r.11.9.mca |

（最初と最後だけ表示したテーブルなので欠損値ばっかですね）




# 3.ブロックを設置する
作成したPandasデータフレームを使用して、Minecraftのワールドにブロックを配置します。

## anvil-parserについて
anvil-parserはPythonで書かれたライブラリで、MinecraftのAnvilファイルフォーマットの読み書きを可能にします。Anvilファイルフォーマットは、Minecraftのワールドデータを表現するために使用されています。
anvil-parserをつかえば、プレイヤーがワールドに入らなくても、Pythonで直接Minecraftのワールドデータを操作し、自分自身でカスタマイズしたワールドを生成するためタスクを簡単に行うことができます。


https://anvil-parser.readthedocs.io/en/latest/

https://github.com/matcool/anvil-parser


しかし、anvil-parserは256ブロックの高さまでしか設置できません（2023年7月現在）
マイクラは2021年のv1.18から384ブロックの高さまで置けるようようになりました。できれば、最大限の高さまでブロックを設置したいので、本家ではなくフォークしたものは384ブロックの高さまで設置できるようなので、今回はこちらをインストールして使いました。

https://github.com/WoutCherlet/anvil-parser

インストール
```bash
pip install git+https://github.com/WoutCherlet/anvil-parser.git
```

ですが、こちらをそのまま使用すると、予期せぬところにブロックが設置されたので、[ここのソースコード](https://github.com/WoutCherlet/anvil-parser/blob/master/anvil/empty_region.py#L136-L137)を32から16に変更したところ、ちゃんと動作しました（この修正が正解なのかはわかりません）。



コード
```Python
# ライブラリのインポート
import anvil
import random

# -9999のデータを削除
df = df.loc[df["y"] != -9999]

# 最小値を取得
min_value = df["y"].min()

# regionごとにグループ分け
grouped = df.groupby(["region"])

# 3種類のブロックを定義
grass = anvil.Block("minecraft", "grass_block")
dirt = anvil.Block("minecraft", "dirt")
stone = anvil.Block("minecraft", "stone")

# 3種類の草を定義
grass_plant = anvil.Block("minecraft", "grass")
tall_grass_l = anvil.Block("minecraft", "tall_grass", properties={"half": "lower"})
tall_grass_u = anvil.Block("minecraft", "tall_grass", properties={"half": "upper"})

# ブロックを設置する処理
def set_blocks(region, x, y, z):

    # 319以下の場合にブロック設置を開始
    if y <= 319:
        # 設定するブロックのリスト(草ブロック１、土ブロック１、石ブロック３のレイヤーをつくる)
        blocks = [grass, dirt, stone, stone, stone]

        # 5%の確率で草を生やす
        if random.random() > 0.95 and y < 319:
            region.set_block(random.choice([grass_plant, tall_grass_l, tall_grass_u]), x, y + 1, z)

        # ブロックのレイヤーを設置する。ブロック設置ができない範囲はbreakで抜ける
        for i, block in enumerate(blocks):
            if -64 <= y - i <= 319:
                region.set_block(block, x, y - i, z)
            else:
                break

for _name, group in grouped:
    # 先頭行をスキップ
    group = group.iloc[1:]

    # regionを作成
    region = anvil.EmptyRegion(0, 0)
    x = group["x"] % 512
    y = group["y"] - min_value - 64
    z = group["z"] % 512

    for xi, yi, zi in zip(x, y, z):
        set_blocks(region, xi, yi, zi)

    # regionを保存
    region.save(group.iloc[1]["region"])

```

まず、不要なデータを削除します。この場合、データの中で標高値が-9999（欠損値）となっているものを除外します。次に、標高の最小値を取得し、regionに基づいてデータをグループ化します。

次に、Minecraftで使用するいくつかのブロックを定義します。ここでは、「草ブロック」、「土ブロック」、「石ブロック」の3種類のブロックと、3種類の草を定義しています。

次に、関数 `set_blocks` を定義します。この関数は、指定されたx, y, z座標にブロックを配置します。特定の標高以下の場合（ここでは319以下）、ブロックを配置するように指定されています。ブロックのレイヤーを設置する際、マイクラで配置可能な高さの範囲外（-64 ~ 319）の場合は処理を中断します。また、より自然な地形にしたかったので、5%の確率で、地面に草を生やすようにしています。

その後、データフレームの各regionごとに、指定された座標にブロックを配置します。これは、各regionに対応するグループのデータを順に処理することで実現しています。xとz座標は、Minecraftのregionの大きさ（512）でモジュロ演算を行うことで0から511の範囲に正規化されます。これは、設置する時の値がワールドの絶対座標ではなく、各regionの相対座標でないとブロックの設置処理ができないからです。y座標は、全体の最小値とMinecraftの最小高度（-64）を基準に調整されます。


最後に、配置が完了したregionを.mcaファイルとして保存します。これにより、Minecraftのワールドデータとして読み込むことができます。

作成した.mcaファイルはマインクラフトのセーブデータの`region`フォルダの中に突っ込めばOKです。

# 実際にできた地形
![2023-07-16_14.48.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/2adaefd1-9857-e9cf-02fc-0107efd6b9a9.png)

峠道が見えます。左下には砂防ぽいものがあります。
![2023-07-17_02.34.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/1a8cddd3-78a8-9658-d900-451b10315ff9.png)


川の堤防（川の部分は欠損値なので欠けてます）
![2023-07-16_13.02.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/0dc15f37-d766-7fd0-8024-022ae53a24bb.png)

団地の地形
![2023-07-16_13.27.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/b3478cbe-bb2b-a29c-ca31-9c56df5d6c59.png)

[金華山](https://ja.wikipedia.org/wiki/金華山_(岐阜県))　標高329m (山頂に岐阜城を建築したいですね)
![2023-07-16_12.42.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/d9c9e4ee-c154-2a0a-8e86-e0c365c96ba4.png)


[百々ヶ峰](https://ja.wikipedia.org/wiki/%E7%99%BE%E3%80%85%E3%83%B6%E5%B3%B0)　標高418m　（限界の384ブロックの高さに収まらず、山頂が欠けてます）
![2023-07-16_12.58.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/fdadff69-4140-4202-d4fa-3b99ae8c8af7.png)

[長良川カントリークラブ](https://ja.wikipedia.org/wiki/長良川カントリー倶楽部)（ゴルフ場）
![2023-07-17_02.30.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/54939d8b-9e21-fbd4-ef4c-91323eedeb73.png)

[琴塚古墳](https://ja.wikipedia.org/wiki/琴塚古墳)
![2023-07-17_02.23.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/3a54c937-6a1b-5466-df94-aaba10c1fa32.png)



作成した地形をマイクラのマップビュワーソフトで確認します。

https://unmined.net/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/0b0aa7f5-7510-7e48-774b-010ab373142a.png)

ほぼ陰影図と変わらないです。




# おまけ


[uNmineD](https://unmined.net/)は表示したワールドマップを画像としてエクスポートできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/4a928f21-b057-ea5c-ae8c-58ec6759385a.png)

エクスポートしたマップ画像をgdalのシェルコマンドでtiffに変換します。`EPSG:6677`で四隅の座標は、先ほどマイクラの起点とした地理座標`[-32150.744183319344 -60035.32409617323]`から480ファイル分のregionグリッドに分けた時の四隅の座標を指定します。
```bash
gdal_translate -a_srs "EPSG:6675" -a_ullr -38294.9648437500000000 -54915.5390625000000000 -26006.9628906250000000 -65155.5468750000000000 minecraft_dem.png minecraft_dem.tif
```

そして、QGISにドラッグ&ドロップすれば、作成したマイクラのワールドマップがピッタリ地図に重なります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/074a9652-ff5c-aa80-3561-8e5dbfcef398.png)