關閉      標題:glb 轉 3dtiles
內容:

#!/usr/bin/python3

'''
  glb to 3dtiles 
  - Author: Feather Mountain(https://3wa.tw)
  - Require:
        pip install trimesh pyglet numpy
        pip install py3dtiles
  - usage:
    Usage: python glbto3dtiles.py <input.glb> <out_dir> [divisions] [--loc lon lat height]
    Example: python glbto3dtiles.py 4.glb out 3 --loc 121 23 0
'''

import sys
import json
import struct
import numpy as np
import trimesh
import math
from pathlib import Path
from trimesh import transformations

# ---------------------
# WGS84 & Matrix Math
# ---------------------
def compute_transform_matrix(lat, lon, height):
    a = 6378137.0
    e2 = 6.69437999014e-3
    lat_rad = math.radians(lat)
    lon_rad = math.radians(lon)
    sin_lat = math.sin(lat_rad)
    cos_lat = math.cos(lat_rad)
    sin_lon = math.sin(lon_rad)
    cos_lon = math.cos(lon_rad)
    
    N = a / math.sqrt(1 - e2 * sin_lat**2)
    x = (N + height) * cos_lat * cos_lon
    y = (N + height) * cos_lat * sin_lon
    z = (N * (1 - e2) + height) * sin_lat
    
    # ENU Matrix
    r = [
        -sin_lon,           cos_lon,            0,
        -sin_lat * cos_lon, -sin_lat * sin_lon, cos_lat,
         cos_lat * cos_lon,  cos_lat * sin_lon, sin_lat
    ]
    
    transform = [
        r[0], r[1], r[2], 0,
        r[3], r[4], r[5], 0,
        r[6], r[7], r[8], 0,
        x,    y,    z,    1
    ]
    return transform

# ---------------------
# Helper: Write B3DM
# ---------------------
def write_b3dm(mesh, output_path):
    glb_content = mesh.export(file_type='glb')
    padding = len(glb_content) % 4
    if padding != 0: glb_content += b'\x00' * (4 - padding)

    ft_json = b'{"BATCH_LENGTH":0}  '
    while len(ft_json) % 4 != 0: ft_json += b' '

    MAGIC = b'b3dm'
    VERSION = 1
    header_len = 28
    total_len = header_len + len(ft_json) + len(glb_content)

    with open(output_path, 'wb') as f:
        f.write(MAGIC)
        f.write(struct.pack('<I', VERSION))
        f.write(struct.pack('<I', total_len))
        f.write(struct.pack('<I', len(ft_json)))
        f.write(struct.pack('<I', 0))
        f.write(struct.pack('<I', 0))
        f.write(struct.pack('<I', 0))
        f.write(ft_json)
        f.write(glb_content)

# ---------------------
# Usage & Parsing
# ---------------------
args = sys.argv[1:]
if len(args) < 2:
    print("Usage: python glbto3dtiles.py <input.glb> <out_dir> [divisions] [--loc lon lat height]")
    sys.exit(1)

input_glb = Path(args[0])
output_dir = Path(args[1])
divisions = 3
target_loc = None 

idx = 2
while idx < len(args):
    if args[idx] == "--loc":
        if idx + 3 < len(args):
            lon = float(args[idx+1])
            lat = float(args[idx+2])
            height = float(args[idx+3])
            target_loc = (lat, lon, height)
            idx += 4
            continue
    elif args[idx].isdigit():
        divisions = int(args[idx])
        idx += 1
    else:
        idx += 1

output_dir.mkdir(exist_ok=True, parents=True)

# ---------------------
# Main Logic
# ---------------------
print(f"Loading {input_glb}...")
scene = trimesh.load(input_glb, force='scene')
scene = scene.copy()
scene.apply_transform(np.eye(4))
mesh = trimesh.util.concatenate(scene.dump())

# 注意:如果不需要旋轉,請確保 gltfUpAxis 設定正確
# 如果模型看起來是躺著的,解開下面兩行的註解:
matrix_rot = transformations.rotation_matrix(math.radians(90), [1, 0, 0])
mesh.apply_transform(matrix_rot)

# 建議:除非模型法線全錯,否則不要執行 fix_normals,它會導致邊緣銳利化或破面
# mesh.fix_normals() 

bbox_min, bbox_max = mesh.bounds
total_extent = bbox_max - bbox_min
print(f"Mesh Bounds: {bbox_min} to {bbox_max}")

# 計算 Grid
step = total_extent / divisions
tri_centers = mesh.triangles_center
indices = ((tri_centers - bbox_min) / step).astype(int)
indices = np.clip(indices, 0, divisions - 1)
hash_keys = indices[:, 0] * (divisions * divisions) + indices[:, 1] * divisions + indices[:, 2]
unique_keys = np.unique(hash_keys)

root_children = []

print(f"Processing {len(unique_keys)} tiles...")

for key in unique_keys:
    iz = key % divisions
    iy = (key // divisions) % divisions
    ix = key // (divisions * divisions)
    
    face_indices = np.where(hash_keys == key)[0]
    
    # 取出子 mesh
    # 注意:這裡 copy 一份,避免修改到原始 mesh
    sub_mesh = mesh.submesh([face_indices], append=True).copy()
    
    if len(sub_mesh.faces) == 0: continue

    # --- [關鍵修正] RTC (Relative To Center) ---
    # 1. 計算該區塊的中心點
    s_min, s_max = sub_mesh.bounds
    s_center = (s_min + s_max) / 2.0
    
    # 2. 將子模型的頂點座標「歸零」到中心點 (消除抖動)
    translation_matrix = transformations.translation_matrix(-s_center)
    sub_mesh.apply_transform(translation_matrix)
    
    # 3. 寫入檔案 (這時內部的座標很小,精度很高)
    tile_filename = f"tile_{ix}_{iy}_{iz}.b3dm"
    tile_path = output_dir / tile_filename
    write_b3dm(sub_mesh, tile_path)
    
    # 4. 在 tileset.json 中補回偏移量
    # 這樣 Cesium 載入時會把模型移回正確位置
    
    # Bounding Box 保持原樣 (在父座標系中的位置)
    s_extent = (s_max - s_min) * 1.1 # 縮小一點 padding,1.3 可能太大導致重疊嚴重
    
    child_box = [
        float(s_center[0]), float(s_center[1]), float(s_center[2]),
        float(s_extent[0]/2), 0, 0,
        0, float(s_extent[1]/2), 0,
        0, 0, float(s_extent[2]/2)
    ]
    
    # 建立 Transform 矩陣 (將歸零的模型移回 s_center)
    # [1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1] (Column-major in concept, but list is row-major order in JSON? No, flat array)
    # 3D Tiles 規範的 transform 是 Column-major。
    # [1,0,0,0, 0,1,0,0, 0,0,1,0, tx, ty, tz, 1] -> [0,4,8,12] is x axis... 
    # 平移量在最後一行 (index 12, 13, 14)
    tile_transform = [
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        float(s_center[0]), float(s_center[1]), float(s_center[2]), 1
    ]

    root_children.append({
        "transform": tile_transform, # 這是關鍵,把模型移回去
        "boundingVolume": {"box": child_box},
        "geometricError": 0,
        "refine": "REPLACE",
        "content": {"uri": tile_filename}
    })
    print(f"  -> {tile_filename}: {len(sub_mesh.faces)} faces")

# Root 
root_center = (bbox_min + bbox_max) / 2.0
root_extent = (bbox_max - bbox_min) * 1.05
root_box = [
    float(root_center[0]), float(root_center[1]), float(root_center[2]),
    float(root_extent[0]/2), 0, 0,
    0, float(root_extent[1]/2), 0,
    0, 0, float(root_extent[2]/2)
]

root_obj = {
    "boundingVolume": {"box": root_box},
    "geometricError": 200, 
    "refine": "ADD", # 修正:空間切分必須用 ADD
    "children": root_children
}

if target_loc:
    lat, lon, height = target_loc
    print(f"Applying Geolocation: Lon {lon}, Lat {lat}, Height {height}")
    root_obj["transform"] = compute_transform_matrix(lat, lon, height)

# 依你的需求設定 Up Axis
tileset_json = {
    "asset": {
        "version": "1.0", 
        "gltfUpAxis": "Z" 
    },
    "geometricError": 2000,
    "root": root_obj
}

with open(output_dir / "tileset.json", "w") as f:
    json.dump(tileset_json, f, indent=2)

print(f"Done! Generated {len(root_children)} tiles.")