🌏

第3回 Minecraft × PLY点群:Minecraft世界をOBJ形式で保存する方法を探る

に公開

前回までで、PLY形式で現実世界のものをMinecraft内で再現することに成功しました。
では、その逆はどうでしょうか。

「Minecraftの世界をOBJ形式に変換して保存する」 方法を試してみます。
今回は、MinecraftのブロックデータをPLYに書き出し、そのPLYから立方体ブロックで再現するOBJ形式へ変換する方法を紹介します。

前回の記事

https://zenn.dev/kento_1938/articles/0ba1a34a01cc1e

今回保存する世界

1️⃣ Minecraftの世界をPLYとして保存する

まず、Minecraftの中心位置を基準に指定半径の範囲を取得し、ブロックごとの色付き点群としてPLYに保存します。

from plyfile import PlyData, PlyElement
from mcpi.minecraft import Minecraft
from mcpi import block
import numpy as np

# 簡易ブロックID→RGB対応
BLOCK_COLOR_MAP = {
    0: None,  # AIR
    1: (125, 125, 125),  # STONE
    2: (95, 159, 53),    # GRASS
    3: (134, 96, 67),    # DIRT
    35: (255, 255, 255), # WOOL
    # ...必要なブロックだけ省略
}

def block_to_rgb(block_id, block_data):
    return BLOCK_COLOR_MAP.get(block_id, (255, 255, 255))  # デフォルト白

def save_world_as_colored_ply(output_ply_path, center_pos, radius, scale=1.0):
    mc = Minecraft.create("127.0.0.1")
    points = []

    min_x = center_pos.x - radius
    max_x = center_pos.x + radius
    min_y = center_pos.y
    max_y = center_pos.y + radius
    min_z = center_pos.z - radius
    max_z = center_pos.z + radius

    print(f"ワールド取得範囲: X[{min_x},{max_x}] Y[{min_y},{max_y}] Z[{min_z},{max_z}]")

    for x in range(min_x, max_x + 1):
        for y in range(min_y, max_y + 1):
            for z in range(min_z, max_z + 1):
                block_obj = mc.getBlockWithData(x, y, z)
                b_id = block_obj.id
                b_data = block_obj.data

                if b_id == block.AIR.id:
                    continue

                px = float(x - center_pos.x) / scale
                py = float(y - center_pos.y) / scale
                pz = float(z - center_pos.z) / scale

                r, g, b = block_to_rgb(b_id, b_data)
                points.append((px, py, pz, r, g, b))

    if not points:
        print("範囲内にブロックがありませんでした。")
        return

    vertex = np.array(points, dtype=[
        ('x', 'f4'), ('y', 'f4'), ('z', 'f4'),
        ('red', 'u1'), ('green', 'u1'), ('blue', 'u1')
    ])
    ply_element = PlyElement.describe(vertex, 'vertex')
    PlyData([ply_element], text=True).write(output_ply_path)

    print(f"PLYファイル保存完了: {output_ply_path}, 総点数: {len(points)}")

if __name__ == "__main__":
    mc = Minecraft.create("127.0.0.1")
    player_pos = mc.player.getTilePos()
    save_world_as_colored_ply("my_world_radius50.ply", player_pos, radius=50, scale=80)

🔹 説明

  • BLOCK_COLOR_MAP でブロックIDからRGB値を割り当て(今回は決め打ち)
  • 空気はスキップして効率化
  • PLYの座標は中心位置を原点としてスケーリング
  • 出力されるPLYは、ブロック1個=1点の色付き点群として保存されます

2️⃣ PLYからOBJに変換(立方体ブロックで再現)

PLYの各点を1x1x1の立方体ブロックとして展開し、OBJ形式で保存します。
MTLファイルも作成して、色付きで表示可能です。

from plyfile import PlyData
import numpy as np

# 基準立方体の頂点と面
cube_vertices = np.array([
    [-0.5, -0.5, -0.5],[ 0.5, -0.5, -0.5],[ 0.5,  0.5, -0.5],[-0.5,  0.5, -0.5],
    [-0.5, -0.5,  0.5],[ 0.5, -0.5,  0.5],[ 0.5,  0.5,  0.5],[-0.5,  0.5,  0.5]
])
cube_faces = [
    (0,2,1),(0,3,2),(4,5,6),(4,6,7),(0,5,1),(0,4,5),
    (2,3,7),(2,7,6),(1,6,2),(1,5,6),(0,7,3),(0,4,7)
]

def rgb_to_material_name(rgb):
    r,g,b = rgb
    return f"mat_{r}_{g}_{b}"

def ply_to_obj_with_mtl(input_ply, output_obj, output_mtl, block_size=1.0):
    plydata = PlyData.read(input_ply)
    vertices_out = []
    faces_out = []
    materials_for_faces = []
    vertex_count = 0
    material_dict = {}

    for v in plydata['vertex']:
        x, y, z = v['x'], v['y'], v['z']
        r, g, b = int(v['red']), int(v['green']), int(v['blue'])
        mat_name = rgb_to_material_name((r,g,b))
        if mat_name not in material_dict:
            material_dict[mat_name] = (r,g,b)

        cube_offset = cube_vertices * block_size + np.array([x, y, z])
        vertices_out.extend(cube_offset.tolist())
        new_faces = [(a+vertex_count,b+vertex_count,c+vertex_count) for (a,b,c) in cube_faces]
        faces_out.extend(new_faces)
        materials_for_faces.extend([mat_name]*len(new_faces))
        vertex_count += len(cube_vertices)

    # MTL書き込み
    with open(output_mtl,"w") as f_mtl:
        for mat_name,(r,g,b) in material_dict.items():
            f_mtl.write(f"newmtl {mat_name}\n")
            f_mtl.write(f"Kd {r/255:.4f} {g/255:.4f} {b/255:.4f}\n")
            f_mtl.write("Ka 0.0 0.0 0.0\nKs 0.0 0.0 0.0\nd 1.0\n\n")

    # OBJ書き込み
    with open(output_obj,"w") as f_obj:
        f_obj.write(f"mtllib {output_mtl}\n")
        for vx,vy,vz in vertices_out:
            f_obj.write(f"v {vx} {vy} {vz}\n")
        current_mat = None
        for idx,(a,b,c) in enumerate(faces_out):
            mat = materials_for_faces[idx]
            if mat != current_mat:
                f_obj.write(f"usemtl {mat}\n")
                current_mat = mat
            f_obj.write(f"f {a+1} {b+1} {c+1}\n")

    print(f"OBJファイル: {output_obj}")
    print(f"MTLファイル: {output_mtl}")

if __name__ == "__main__":
    ply_to_obj_with_mtl("my_world_radius50.ply", "my_world_blocks.obj", "my_world_blocks.mtl", block_size=0.01)

🔹 説明

  • PLYの点を基準立方体に展開して立方体ブロックとしてOBJ化
  • 各立方体に色付きマテリアルを割り当て、MTLファイルも生成
  • block_sizeで1ブロックあたりのOBJ上のサイズを調整可能
  • OBJファイルをBlenderなどで読み込むとMinecraftワールドをそのまま3Dモデル化できます

Blenderで読み込んだOBJモデル

💡 コメント

  • PLY→OBJ変換により、Minecraftの世界を3Dモデルとして他アプリで活用可能
  • 色付きブロックの形状を忠実に再現できるため、Minecraftワールドの可視化・編集・レンダリングに便利

この手順を応用すれば、Minecraftワールドを他の3Dソフトで自由に活用できるようになります。

3️⃣ PLYからMinecraftへ色付きブロックを再構築する

前節で紹介したPLY→OBJ変換に加え、Minecraftの他のワールドに建築物を移設方法としてPLYデータを直接Minecraftのブロックとして再現する方法もあります。

イメージ
Minecraft → PLY → Minecraft

今回は簡易的に、RGB値に最も近いMinecraftブロックを選択して配置する例です。

from plyfile import PlyData
from mcpi.minecraft import Minecraft
from mcpi import block
import math

# 簡易色マッピング
BLOCK_COLORS = {
    block.STONE.id: (125, 125, 125),
    block.GRASS.id: (95, 159, 53),
    block.DIRT.id: (134, 96, 67),
    block.SAND.id: (217, 212, 130),
    block.WATER.id: (64, 64, 255),
    block.WOOL.id: (255, 255, 255),  # 白羊毛
}

def rgb_to_block(r, g, b):
    best_id = block.STONE.id
    best_dist = float("inf")
    for b_id, (cr, cg, cb) in BLOCK_COLORS.items():
        dist = math.sqrt((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2)
        if dist < best_dist:
            best_id = b_id
            best_dist = dist
    return best_id

def ply_to_minecraft(input_ply_path, center_pos, scale=1.0):
    mc = Minecraft.create("127.0.0.1")
    plydata = PlyData.read(input_ply_path)

    for vertex in plydata['vertex']:
        x = int(vertex['x'] * scale + center_pos.x)
        y = int(vertex['y'] * scale + center_pos.y)
        z = int(vertex['z'] * scale + center_pos.z)
        r, g, b = int(vertex['red']), int(vertex['green']), int(vertex['blue'])

        block_id = rgb_to_block(r, g, b)
        mc.setBlock(x, y, z, block_id)

    print("PLYからMinecraftへの生成が完了しました。")

if __name__ == "__main__":
    mc = Minecraft.create("127.0.0.1")
    player_pos = mc.player.getTilePos()
    ply_to_minecraft("my_world_radius50.ply", player_pos, scale=80)

🔹 説明

  1. RGB値に最も近いブロックを選択

    • BLOCK_COLORSで色付きブロックを定義
    • ユークリッド距離で最も近いブロックIDを選ぶ
  2. スケール調整

    • scaleでPLY座標をMinecraftのブロック座標に変換
  3. 中心位置の調整

    • プレイヤー位置を原点にして配置することで、任意の場所に再構築可能
  4. 結果

    • PLYの点群がMinecraft内で色付きブロックとして再現される
    • 写真例では白羊毛や草ブロックで簡易色再現

💡 コメント

  • OBJ化とは異なり、Minecraft内で直接遊べる形で再現できるのが利点(ワールド間ブロック転送)
  • RGB値をもっと細かくマッピングすれば、より忠実な色再現も可能
  • PLY→Minecraft→OBJと組み合わせれば、ワールドの双方向変換が可能

これで第3回では、MinecraftとPLY点群の双方向ワールド変換の全体像を一通り体験できました。

  1. Minecraft → PLY
    ブロックワールドを点群データとして抽出し、現実世界や他の3Dツールで活用できる形式に変換。
  2. PLY → OBJ
    点群を立方体の3DモデルとしてOBJ化。これにより、Minecraftの世界を3Dモデリングソフトで表示・編集可能に。
  3. PLY → Minecraft
    RGB値に基づいて色付きブロックを再配置し、点群をMinecraft上で再現。オリジナルワールドを忠実に再構築できます。

この一連の流れを通して、Minecraftの世界をデータ化して自由に加工し、またMinecraftに戻すことができることが確認できました。

3回を通じて、Minecraftの世界は単なるゲームの枠を超え、3Dデータの遊べるキャンバスとして活用できる可能性を模索してきました。

PLY形式の扱いや点群データの性質、色や形状の再現方法についても理解が深まり、Minecraftと3Dデータの双方向活用の面白さを体験できたと思います。

参考

https://zenn.dev/kento_1938/articles/1884ccc4fe231d

https://zenn.dev/kento_1938/articles/793d803b73dfc0

https://zenn.dev/kento_1938/articles/0ba1a34a01cc1e

Discussion