關閉      標題: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} 度")