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