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)
実装
おおまかにこんな感じ。
- ローカル実行
- LasデータをCSV変換
- CSVデータに対してグリッド化、原点に平行移動、スケール
- マインクラフトで実行
- 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}")
以上。