Rendezvous-Tokyo

Minescriptを使って点群データをマイクラに反映

地道に建築するのに飽きたので。

完成物

とある23区の町の一部

近寄るとこんな感じ。

建物内はスッカスカ(データ的にしょうがない!)

事前準備

点群データGet

東京の23区の点群データがあったので使わせてもらった。

https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024 (opens new window)

MinescriptのSetup

環境: forge1.21.5

こちらのYoutube動画を参考にさせていただいた。
https://youtu.be/O8HXiBRPcuU?si=BHFgiLL0UgSRwEYd (opens new window)

実装

おおまかにこんな感じ。

  • ローカル実行
    1. LasデータをCSV変換
    2. CSVデータに対してグリッド化、原点に平行移動、スケール
  • マインクラフトで実行
    1. setBlock

LasデータをCSV変換

ローカル実行。

import laspy
import csv

filename = "input.las"
las = laspy.read(filename)

with open("exported_points.csv", "w", newline="") as csvfile:
    writer = csv.writer(csvfile)
    writer.writerow(["X", "Y", "Z", "R", "G", "B"])
    for x, y, z, r, g, b in zip(las.X, las.Y, las.Z, las.red, las.green, las.blue):
        writer.writerow([x, y, z, r, g, b])

CSVデータに対してグリッド化、原点に平行移動、スケール

ローカル実行。

import csv
import pandas as pd


# CSV読み込み
csv_path = "exported_points.csv"
with open(csv_path, newline="") as csvfile:
    reader = csv.DictReader(csvfile)
    points = list(reader)

GRID_SIZE = 0.01
EPSILON = 1e-6
TARGET_SPAN = 500.0

df = pd.DataFrame(points)
df[['X', 'Y', 'Z', 'R', 'G', 'B']] = df[['X', 'Y', 'Z', 'R', 'G', 'B']].astype(float)
df[['X', 'Y', 'Z']] = (df[['X', 'Y', 'Z']] / GRID_SIZE + EPSILON).round(0) * GRID_SIZE
df_group = df.groupby(['X', 'Y', 'Z']).median().reset_index()
df_group[['X', 'Y', 'Z']] -= df_group[['X', 'Y', 'Z']].min()
original_max = df_group[["X", "Y", "Z"]].max()
scale = TARGET_SPAN / original_max.max()
df_group[['X', 'Y', 'Z']] *= scale
df_group = df_group.round().astype(int)

output_path = "processed_points.csv"
df_group.to_csv(output_path, index=False)

setBlock

このコードはマインクラフト側で実行。
スケールしてるからチャンクの読み込みは不要かもしれぬ。その辺は今後改善。

import minescript
import csv
import math
from collections import defaultdict


# --- Minecraft ワールドの高さ制限(ver 1.18以降) ---
MC_MIN_Y = 0       # 最低設置可能高度(地下含めるなら -64)
MC_MAX_Y = 319     # 最高設置可能高度

POINT_SKIP_INTERVAL = 1 # 間引きしたい場合は50とか
CHUNK_SIZE = 16
MAX_CHUNKS_PER_BATCH = 5  # 一度に読み込む最大チャンク数


minecraft_blocks = {
    "white_concrete": (207, 213, 214),
    "orange_concrete": (224, 97, 0),
    "magenta_concrete": (169, 48, 159),
    "light_blue_concrete": (36, 137, 199),
    "yellow_concrete": (241, 175, 21),
    "lime_concrete": (94, 168, 24),
    "pink_concrete": (214, 101, 143),
    "gray_concrete": (54, 57, 61),
    "light_gray_concrete": (125, 125, 115),
    "cyan_concrete": (21, 137, 145),
    "purple_concrete": (121, 42, 172),
    "blue_concrete": (44, 46, 143),
    "brown_concrete": (96, 59, 31),
    "green_concrete": (73, 91, 36),
    "red_concrete": (142, 32, 32),
    "black_concrete": (8, 10, 15),
}

def closest_block_color(rgb):
    r1, g1, b1 = rgb
    min_dist = float("inf")
    closest = None
    for name, (r2, g2, b2) in minecraft_blocks.items():
        dist = math.sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2)
        if dist < min_dist:
            min_dist = dist
            closest = name
    return closest

def convert_point_to_minecraft(px, py, pz, cx, cy, cz, X, Y, Z):
    """
    実世界座標 (X, Y, Z) を Minecraft 座標に変換
    - X: 東西 → Minecraft X
    - Y: 南北 → Minecraft Z
    - Z: 標高 → Minecraft Y
    """
    dx = round(float(X) - cx)
    dz = round(float(Y) - cy)
    dy = round(float(Z) - cz)  # Zは高さ

    mx = int(px + dx)
    my = int(py + dy)
    mz = int(pz + dz)

    return mx, my, mz

# CSV読み込み。完全パス
csv_path = "C:/Users/ユーザ名/AppData/Roaming/.minecraft/.minecraft-forge1.21.5/minescript/processed_points.csv"
with open(csv_path, newline="") as csvfile:
    reader = csv.DictReader(csvfile)
    points = list(reader)

# 点群中心を計算
cx = sum(float(p["X"]) for p in points) / len(points)
cy = sum(float(p["Y"]) for p in points) / len(points)
cz = sum(float(p["Z"]) for p in points) / len(points)

chunk_to_points = defaultdict(list)

# プレイヤー位置取得
px, py, pz = minescript.player().position
px, py, pz = int(px), int(py), int(pz)


# 点群をブロック化
for p in points[::POINT_SKIP_INTERVAL]:
    x, y, z = convert_point_to_minecraft(px, py, pz, cx, cy, cz, p["X"], p["Y"], p["Z"])
    chunk_x = x // CHUNK_SIZE
    chunk_z = z // CHUNK_SIZE
    chunk_to_points[(chunk_x, chunk_z)].append((x, y, z, p))

chunk_keys = list(chunk_to_points.keys())
for i in range(0, len(chunk_keys), MAX_CHUNKS_PER_BATCH):
    batch = chunk_keys[i:i + MAX_CHUNKS_PER_BATCH]
    for cx, cz in batch:
        minescript.execute(f"forceload add {cx * CHUNK_SIZE} {cz * CHUNK_SIZE}")
    for cx, cz in batch:
        for x, y, z, p in chunk_to_points[(cx, cz)]:
            if not (MC_MIN_Y  <= y <= MC_MAX_Y):
                continue
            r = int(p["R"]) >> 8
            g = int(p["G"]) >> 8
            b = int(p["B"]) >> 8
            block = closest_block_color((r, g, b))
            minescript.echo(f"setblock {x} {y} {z} {block}")
            minescript.execute(f"setblock {x} {y} {z} {block}")
    for cx_, cz_ in batch:
        minescript.execute(f"forceload remove {cx * CHUNK_SIZE} {cz * CHUNK_SIZE}")

以上。