關閉
標題:glbtosplitglb_v2.py
內容:
import trimesh
import os
import json
import numpy as np
from pyproj import Transformer, CRS
import math
# ===================== 基本設定 =====================
INPUT_GLB = "28.glb"
COORD_JSON = "28.json"
OUT_DIR = "28"
# ⭐ 旋轉設定
# -90 = 順時針轉 90 度
# 90 = 逆時針轉 90 度
ROTATE_DEGREE = 0
# ⭐ 空間切分設定 (關鍵優化)
# 將地圖切成 50m x 50m 的格子,格子內的物件會合併
# 若模型很精細可設小一點 (e.g. 20),若模型很大可設大一點 (e.g. 100)
GRID_SIZE = 1.0
MAX_DISTANCE = 50000.0 # 過濾閾值
os.makedirs(OUT_DIR, exist_ok=True)
# ===================== 工具函式 =====================
def enu_to_ecef_matrix(lat, lon):
lat = math.radians(lat)
lon = math.radians(lon)
return np.array([
[-math.sin(lon), -math.sin(lat)*math.cos(lon), math.cos(lat)*math.cos(lon), 0],
[ math.cos(lon), -math.sin(lat)*math.sin(lon), math.cos(lat)*math.sin(lon), 0],
[ 0, math.cos(lat), math.sin(lat), 0],
[ 0, 0, 0, 1]
])
def get_y_rotation_matrix(degrees):
# 繞 Y 軸旋轉 (GLB Up-Axis)
rad = math.radians(degrees)
c = math.cos(rad)
s = math.sin(rad)
return np.array([
[c, 0, s, 0],
[0, 1, 0, 0],
[-s, 0, c, 0],
[0, 0, 0, 1]
])
# ===================== 1. 座標系統處理 =====================
print("🔹 1. 讀取座標並建立基準點...")
with open(COORD_JSON, "r", encoding="utf-8") as f:
coord = json.load(f)
# Anchor (TWD97)
anchor_3826 = np.array([
coord["first_x"],
coord["first_y"],
coord.get("first_z", 0.0)
], dtype=float)
print(f"📍 Anchor (TWD97): {anchor_3826}")
# 轉換器
crs_3826 = CRS.from_epsg(3826) # TWD97
crs_4978 = CRS.from_epsg(4978) # ECEF
crs_4326 = CRS.from_epsg(4326) # WGS84
to_ecef = Transformer.from_crs(crs_3826, crs_4978, always_xy=True)
to_geo = Transformer.from_crs(crs_3826, crs_4326, always_xy=True)
# 轉換 Anchor 到世界座標 (ECEF) 和 經緯度
cx, cy, cz = to_ecef.transform(*anchor_3826)
lon, lat, _ = to_geo.transform(*anchor_3826)
print(f"🌍 Anchor lon/lat: {lon:.6f}, {lat:.6f}")
# 建立 Tileset 的變換矩陣 (ENU -> ECEF)
rot = enu_to_ecef_matrix(lat, lon)
transform = rot.copy()
transform[0, 3] = cx
transform[1, 3] = cy
transform[2, 3] = cz
transform_flat = transform.flatten("F").tolist()
# ===================== 2. 載入與預處理 Mesh =====================
print("🔹 2. 載入 GLB 模型...")
# process=False 避免 trimesh 自動合併頂點,保持原始結構
scene = trimesh.load(INPUT_GLB, force="scene", process=False)
geoms = list(scene.dump(concatenate=False))
if not geoms:
raise RuntimeError("❌ GLB 裡沒有 mesh")
print(f"📦 原始 Mesh 數量: {len(geoms)}")
# 準備矩陣
rot_y_matrix = get_y_rotation_matrix(ROTATE_DEGREE)
translation_vector = np.array([
-anchor_3826[0], # East
-anchor_3826[2], # Height (Y in GLB)
anchor_3826[1] # North (Z in GLB needs to be inverted later)
])
# 網格容器:key=(grid_x, grid_z), value=[mesh_list]
grid_batches = {}
skipped_count = 0
global_min = np.array([np.inf, np.inf, np.inf])
global_max = np.array([-np.inf, -np.inf, -np.inf])
print(f"🔄 3. 正在進行空間切分 (Grid Size: {GRID_SIZE}m)...")
for geom in geoms:
# --- A. 過濾檢查 (使用原始座標) ---
bounds_center = geom.bounds.mean(axis=0)
real_x = bounds_center[0]
real_y = -bounds_center[2] # 還原北距
real_z = bounds_center[1] # 高度
dist = np.linalg.norm(np.array([real_x, real_y, real_z]) - anchor_3826)
if dist > MAX_DISTANCE:
skipped_count += 1
continue
# --- B. 變換 (Copy -> Translate -> Rotate) ---
g = geom.copy()
# 1. 歸零 (移到以 Anchor 為原點的局部空間)
# 注意:這裡的 translation_vector Z 分量是 +North,但原始 GLB Z 是 -North
# 所以我們要手動構建平移矩陣比較保險
# Target Local Pos = [InputX - AnchorX, InputY - AnchorZ, InputZ + AnchorY]
# 但 trimesh apply_translation 是直接加法,所以我們用上面的 translation_vector
g.apply_translation(translation_vector)
# 2. 旋轉 (繞 Y 軸)
g.apply_transform(rot_y_matrix)
# --- C. 分配到網格 ---
# 計算變換後的中心點,決定它屬於哪個格子
curr_center = g.bounds.mean(axis=0)
# 計算 Grid Index (用 X 和 Z 平面切分)
gx = int(math.floor(curr_center[0] / GRID_SIZE))
gz = int(math.floor(curr_center[2] / GRID_SIZE))
key = (gx, gz)
if key not in grid_batches:
grid_batches[key] = []
grid_batches[key].append(g)
# 更新全域邊界 (用於 Root Tile)
global_min = np.minimum(global_min, g.bounds[0])
global_max = np.maximum(global_max, g.bounds[1])
print(f"✅ 有效 Mesh: {len(geoms) - skipped_count}, 被過濾: {skipped_count}")
print(f"🧩 切分成了 {len(grid_batches)} 個空間區塊")
# ===================== 3. 匯出 Tile =====================
children = []
tile_counter = 0
print("💾 4. 開始匯出各個區塊...")
for key, batch in grid_batches.items():
if not batch:
continue
# 合併 Mesh
if len(batch) == 1:
merged = batch[0]
else:
merged = trimesh.util.concatenate(batch)
# 檔名包含座標,方便除錯 (tile_X_Z.glb)
fname = f"tile_{key[0]}_{key[1]}.glb"
out_path = os.path.join(OUT_DIR, fname)
merged.export(out_path)
# 計算這個 Tile 的 Bounding Box
bmin, bmax = merged.bounds
center = (bmin + bmax) / 2
dims = bmax - bmin
# ❌ 原本的寫法 (造成拉近消失的原因)
# geo_error = GRID_SIZE
# ✅ 修改後的寫法:
# 因為這是最後一層 (Leaf Node),沒有子節點了
# 必須設為 0.0,告訴 Cesium「這就是最高畫質,不要切換」
geo_error = 0.0
children.append({
"boundingVolume": {"box": [
center[0], center[1], center[2],
dims[0]/2, 0, 0,
0, dims[1]/2, 0,
0, 0, dims[2]/2
]},
"geometricError": geo_error, # 使用 0.0
"content": {"uri": fname}
})
tile_counter += 1
# ===================== 4. 產生 tileset.json =====================
print("📝 5. 寫入 tileset.json...")
# Root 的邊界 (包住所有子節點)
root_center = (global_min + global_max) / 2
root_dims = global_max - global_min
# Root 的 geometricError 要夠大,確保一開始會嘗試讀取子節點
root_error = np.linalg.norm(root_dims)
tileset = {
"asset": {
"version": "1.1",
"gltfUpAxis": "Y"
},
"geometricError": root_error,
"root": {
"transform": transform_flat,
"boundingVolume": {"box": [
root_center[0], root_center[1], root_center[2],
root_dims[0]/2, 0, 0,
0, root_dims[1]/2, 0,
0, 0, root_dims[2]/2
]},
"geometricError": root_error / 2, # 下一層的誤差容許值
"refine": "ADD",
"children": children
}
}
with open(os.path.join(OUT_DIR, "tileset.json"), "w") as f:
json.dump(tileset, f, indent=2)
print(f"\n🚀 全部完成!")
print(f"📂 輸出目錄: {OUT_DIR}/")
print(f"🔢 總共生成 Tile 數: {tile_counter}")
print(f"📐 設定旋轉角度: {ROTATE_DEGREE} 度")