LoginSignup
38
10
お題は不問!Qiita Engineer Festa 2023で記事投稿!

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

Last updated at Posted at 2023-07-16

2023-07-16_12.51.38.png

はじめに

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

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

まずは基盤地図情報ダウンロードサイトよりDEMのデータをダウンロードします。

今回は岐阜県岐阜市のこちらのメッシュ(533616)を使います。
image.png

ダウンロードしたDEMはそのままでは利用できないので、QGISのQuickDEM4JPプラグインを使用してtiffファイルに変換します。

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

image.png

image.png

QGIS上に表示したDEM(EPSG:6675)
image.png

マイクラのワールドに変換する

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

1.DEMの解像度を上げる

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

インストール

pip install gdal

コード

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
画像出典「Minecraft Wiki 座標」

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

これらを踏まえると、

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

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

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

インストール

pip install rasterio
pip install numpy
pip install pandas

コード

# ライブラリのインポート
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

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

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

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

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

今回使用するDEMをregion(.mca)のファイル名のグリッドで区切ると以下のようになります。
必要なmcaファイルの数は480ファイルです。
image.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のワールドデータを操作し、自分自身でカスタマイズしたワールドを生成するためタスクを簡単に行うことができます。

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

インストール

pip install git+https://github.com/WoutCherlet/anvil-parser.git

ですが、こちらをそのまま使用すると、予期せぬところにブロックが設置されたので、ここのソースコードを32から16に変更したところ、ちゃんと動作しました(この修正が正解なのかはわかりません)。

コード

# ライブラリのインポート
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:
        # 設定するブロックのリスト(草ブロック1、土ブロック1、石ブロック3のレイヤーをつくる)
        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

峠道が見えます。左下には砂防ぽいものがあります。
2023-07-17_02.34.48.png

川の堤防(川の部分は欠損値なので欠けてます)
2023-07-16_13.02.21.png

団地の地形
2023-07-16_13.27.25.png

金華山 標高329m (山頂に岐阜城を建築したいですね)
2023-07-16_12.42.58.png

百々ヶ峰 標高418m (限界の384ブロックの高さに収まらず、山頂が欠けてます)
2023-07-16_12.58.22.png

長良川カントリークラブ(ゴルフ場)
2023-07-17_02.30.06.png

琴塚古墳
2023-07-17_02.23.16.png

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

image.png

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

おまけ

uNmineDは表示したワールドマップを画像としてエクスポートできます。

image.png

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

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

38
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
10