FBX转3DTiles带坐标的高效转换工具

Python Tkinter实现FBX转3DTiles程序开发指南

1. 程序架构设计

本程序采用模块化设计,主要包含以下核心模块:

模块名称 功能描述 依赖库
图形界面模块 提供用户交互界面 tkinter
FBX解析模块 读取FBX文件并提取3D数据 fbx-sdk/pyfbx
坐标转换模块 处理地理坐标转换 pyproj
格式转换模块 生成GLB和B3DM文件 pygltflib
压缩优化模块 顶点和文件压缩 draco
文件拆分模块 控制输出文件大小 os, shutil

2. 核心功能实现

2.1 图形界面开发

python 复制代码
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading

class FBXTo3DTilesConverter:
    def __init__(self, root):
        self.root = root
        self.root.title("FBX转3DTiles转换器")
        self.root.geometry("600x400")
        
        self.setup_ui()
        
    def setup_ui(self):
        # 文件选择区域
        file_frame = ttk.LabelFrame(self.root, text="文件选择", padding=10)
        file_frame.pack(fill="x", padx=10, pady=5)
        
        self.fbx_path = tk.StringVar()
        ttk.Entry(file_frame, textvariable=self.fbx_path, width=50).pack(side="left", padx=5)
        ttk.Button(file_frame, text="选择FBX文件", 
                  command=self.select_fbx_file).pack(side="left", padx=5)
        
        # 输出选项区域
        options_frame = ttk.LabelFrame(self.root, text="输出选项", padding=10)
        options_frame.pack(fill="x", padx=10, pady=5)
        
        # 输出格式选择
        self.output_format = tk.StringVar(value="b3dm")
        ttk.Radiobutton(options_frame, text="B3DM", 
                       variable=self.output_format, value="b3dm").pack(side="left")
        ttk.Radiobutton(options_frame, text="GLB", 
                       variable=self.output_format, value="glb").pack(side="left")
        
        # 压缩选项
        self.compression_enabled = tk.BooleanVar(value=True)
        ttk.Checkbutton(options_frame, text="启用顶点压缩", 
                       variable=self.compression_enabled).pack(side="left", padx=20)
        
        # 坐标设置
        coord_frame = ttk.LabelFrame(self.root, text="坐标设置", padding=10)
        coord_frame.pack(fill="x", padx=10, pady=5)
        
        self.coord_x = tk.StringVar(value="116.3974")  # 北京经度
        self.coord_y = tk.StringVar(value="39.9093")   # 北京纬度
        
        ttk.Label(coord_frame, text="经度:").pack(side="left")
        ttk.Entry(coord_frame, textvariable=self.coord_x, width=10).pack(side="left", padx=5)
        ttk.Label(coord_frame, text="纬度:").pack(side="left", padx=10)
        ttk.Entry(coord_frame, textvariable=self.coord_y, width=10).pack(side="left", padx=5)
        
        # 转换按钮
        ttk.Button(self.root, text="开始转换", 
                  command=self.start_conversion).pack(pady=20)
        
        # 进度显示
        self.progress = ttk.Progressbar(self.root, mode='indeterminate')
        self.progress.pack(fill="x", padx=10, pady=5)
        
        self.status_label = ttk.Label(self.root, text="就绪")
        self.status_label.pack()

2.2 FBX文件解析与数据处理

python 复制代码
import fbx
import numpy as np
from pyproj import Transformer

class FBXProcessor:
    def __init__(self):
        self.manager = fbx.FbxManager.Create()
        self.scene = None
        self.transformer = Transformer.from_crs("EPSG:4326", "EPSG:4978", always_xy=True)
        
    def load_fbx(self, file_path):
        """加载FBX文件并提取3D数据"""
        importer = fbx.FbxImporter.Create(self.manager, "")
        
        if not importer.Initialize(file_path):
            raise Exception(f"FBX文件加载失败: {importer.GetStatus().GetErrorString()}")
            
        self.scene = fbx.FbxScene.Create(self.manager, "")
        importer.Import(self.scene)
        importer.Destroy()
        
        return self.extract_mesh_data()
    
    def extract_mesh_data(self):
        """从场景中提取网格数据"""
        mesh_data = {
            'vertices': [],
            'indices': [],
            'normals': [],
            'uvs': [],
            'materials': []
        }
        
        root_node = self.scene.GetRootNode()
        self.process_node(root_node, mesh_data)
        
        return mesh_data
    
    def process_node(self, node, mesh_data):
        """递归处理场景节点"""
        if node.GetNodeAttribute():
            attribute_type = node.GetNodeAttribute().GetAttributeType()
            
            if attribute_type == fbx.FbxNodeAttribute.eMesh:
                self.process_mesh(node, mesh_data)
                
        for i in range(node.GetChildCount()):
            self.process_node(node.GetChild(i), mesh_data)
    
    def process_mesh(self, node, mesh_data):
        """处理网格数据"""
        mesh = node.GetNodeAttribute()
        geometry = mesh.GetGeometry()
        
        # 提取顶点数据
        control_points = geometry.GetControlPoints()
        for i in range(control_points.GetCount()):
            point = control_points.GetAt(i)
            mesh_data['vertices'].extend([point[0], point[1], point[2]])
        
        # 处理多边形数据
        self.process_polygons(geometry, mesh_data)

2.3 坐标转换与地理定位

python 复制代码
class CoordinateConverter:
    def __init__(self, default_lon=116.3974, default_lat=39.9093):
        # 北京默认坐标(天安门)
        self.default_lon = default_lon
        self.default_lat = default_lat
        self.transformer = Transformer.from_crs("EPSG:4326", "EPSG:4978", always_xy=True)
    
    def convert_to_earth_centered(self, vertices, offset_lon=None, offset_lat=None):
        """将局部坐标转换为地心坐标系"""
        lon = offset_lon if offset_lon else self.default_lon
        lat = offset_lat if offset_lat else self.default_lat
        
        # 计算参考点的地心坐标
        ref_x, ref_y, ref_z = self.transformer.transform(lon, lat, 0)
        
        # 应用坐标变换(简化处理)
        earth_centered_vertices = []
        for i in range(0, len(vertices), 3):
            x = vertices[i] + ref_x
            y = vertices[i+1] + ref_y  
            z = vertices[i+2] + ref_z
            earth_centered_vertices.extend([x, y, z])
            
        return earth_centered_vertices
    
    def create_tileset_json(self, bounding_box, file_list):
        """生成3DTiles的tileset.json文件"""
        tileset = {
            "asset": {
                "version": "1.0",
                "tilesetVersion": "1.2.3"
            },
            "properties": {
                "longitude": self.default_lon,
                "latitude": self.default_lat
            },
            "geometricError": 100,
            "root": {
                "boundingVolume": {
                    "region": bounding_box
                },
                "geometricError": 0,
                "refine": "ADD",
                "content": {
                    "uri": file_list[0] if file_list else ""
                },
                "children": []
            }
        }
        return tileset

2.4 格式转换与压缩

python 复制代码
import struct
import zlib
from pygltflib import GLTF2, Buffer, BufferView, Accessor, Mesh, Primitive, Node, Scene

class FormatConverter:
    def __init__(self):
        self.draco_compression = False
        
    def enable_draco_compression(self, enable=True):
        self.draco_compression = enable
    
    def convert_to_glb(self, mesh_data, output_path):
        """将网格数据转换为GLB格式"""
        gltf = GLTF2()
        
        # 创建缓冲区
        vertex_buffer = self.create_vertex_buffer(mesh_data['vertices'])
        index_buffer = self.create_index_buffer(mesh_data['indices'])
        
        # 添加缓冲区视图和访问器
        self.add_buffer_views(gltf, vertex_buffer, index_buffer)
        self.add_accessors(gltf, len(mesh_data['vertices']) // 3, len(mesh_data['indices']))
        
        # 创建网格和场景
        mesh = self.create_mesh(gltf)
        scene = self.create_scene(gltf, mesh)
        
        gltf.scenes = [scene]
        gltf.scene = 0
        
        # 保存GLB文件
        gltf.save_binary(output_path)
        
        return output_path
    
    def convert_to_b3dm(self, glb_path, output_path, feature_table=None):
        """将GLB转换为B3DM格式"""
        with open(glb_path, 'rb') as f:
            glb_data = f.read()
            
        # 创建B3DM头部
        header = self.create_b3dm_header(len(glb_data))
        
        # 创建要素表(可选)
        feature_table_json = self.create_feature_table(feature_table or {})
        feature_table_bin = b''  # 二进制要素数据
        
        # 组合B3DM文件
        b3dm_data = header + feature_table_json + feature_table_bin + glb_data
        
        with open(output_path, 'wb') as f:
            f.write(b3dm_data)
            
        return output_path
    
    def create_b3dm_header(self, glb_length):
        """创建B3DM文件头"""
        magic = b'b3dm'
        version = 1
        byte_length = 28 + glb_length  # 头部长度 + GLB数据长度
        feature_table_json_length = 0
        feature_table_bin_length = 0
        batch_table_json_length = 0
        batch_table_bin_length = 0
        
        return struct.pack('<4sIIIII', magic, version, byte_length, 
                          feature_table_json_length, feature_table_bin_length,
                          batch_table_json_length, batch_table_bin_length)

2.5 文件拆分与大小控制

python 复制代码
import os
import math

class FileSplitter:
    def __init__(self, max_file_size=20*1024*1024):  # 20MB
        self.max_file_size = max_file_size
        
    def split_large_mesh(self, mesh_data, output_dir, base_name):
        """根据文件大小要求拆分大型网格"""
        total_vertices = len(mesh_data['vertices']) // 3
        vertices_per_chunk = self.calculate_optimal_chunk_size(mesh_data)
        
        output_files = []
        
        for i in range(0, total_vertices, vertices_per_chunk):
            chunk_data = self.extract_mesh_chunk(mesh_data, i, vertices_per_chunk)
            
            if self.output_format == 'b3dm':
                output_path = self.create_b3dm_chunk(chunk_data, output_dir, base_name, i)
            else:
                output_path = self.create_glb_chunk(chunk_data, output_dir, base_name, i)
                
            output_files.append(output_path)
            
        return output_files
    
    def calculate_optimal_chunk_size(self, mesh_data):
        """计算最优的块大小以满足文件大小限制"""
        # 估算每个顶点占用的字节数
        bytes_per_vertex = 32  # 保守估计:位置(12) + 法线(12) + UV(8)
        bytes_per_index = 4
        
        total_vertices = len(mesh_data['vertices']) // 3
        total_indices = len(mesh_data['indices'])
        
        # 计算基础大小(不含压缩)
        base_size = (total_vertices * bytes_per_vertex + 
                    total_indices * bytes_per_index)
        
        # 考虑压缩比率(假设压缩比为0.3)
        compression_ratio = 0.3
        estimated_size = base_size * compression_ratio
        
        # 计算每个块的最大顶点数
        max_vertices_per_chunk = int((self.max_file_size / compression_ratio) / bytes_per_vertex)
        
        return min(max_vertices_per_chunk, total_vertices)
    
    def extract_mesh_chunk(self, mesh_data, start_vertex, vertex_count):
        """提取网格数据的指定块"""
        end_vertex = min(start_vertex + vertex_count, len(mesh_data['vertices']) // 3)
        
        chunk_data = {
            'vertices': [],
            'indices': [],
            'normals': [],
            'uvs': []
        }
        
        # 提取顶点数据
        vertex_start = start_vertex * 3
        vertex_end = end_vertex * 3
        chunk_data['vertices'] = mesh_data['vertices'][vertex_start:vertex_end]
        
        # 重新映射索引
        index_remap = {}
        new_index = 0
        
        for i, index in enumerate(mesh_data['indices']):
            if start_vertex <= index < end_vertex:
                if index not in index_remap:
                    index_remap[index] = new_index
                    new_index += 1
                chunk_data['indices'].append(index_remap[index])
                
        return chunk_data

3. 主程序集成

python 复制代码
class MainApplication:
    def __init__(self):
        self.fbx_processor = FBXProcessor()
        self.coord_converter = CoordinateConverter()
        self.format_converter = FormatConverter()
        self.file_splitter = FileSplitter()
        
    def convert_fbx_to_3dtiles(self, fbx_path, output_dir, options):
        """主转换函数"""
        try:
            # 1. 读取FBX文件
            mesh_data = self.fbx_processor.load_fbx(fbx_path)
            
            # 2. 坐标转换
            earth_vertices = self.coord_converter.convert_to_earth_centered(
                mesh_data['vertices'], 
                options.get('longitude'),
                options.get('latitude')
            )
            mesh_data['vertices'] = earth_vertices
            
            # 3. 设置压缩
            if options.get('compression', True):
                self.format_converter.enable_draco_compression(True)
            
            # 4. 文件拆分和格式转换
            output_files = self.file_splitter.split_large_mesh(
                mesh_data, output_dir, os.path.basename(fbx_path)
            )
            
            # 5. 生成tileset.json
            tileset = self.coord_converter.create_tileset_json(
                self.calculate_bounding_box(earth_vertices),
                output_files
            )
            
            with open(os.path.join(output_dir, 'tileset.json'), 'w') as f:
                json.dump(tileset, f, indent=2)
                
            return True, output_files
            
        except Exception as e:
            return False, str(e)
    
    def calculate_bounding_box(self, vertices):
        """计算包围盒"""
        if not vertices:
            return [0, 0, 0, 0, 0, 0]
            
        x_coords = vertices[0::3]
        y_coords = vertices[1::3] 
        z_coords = vertices[2::3]
        
        return [
            min(x_coords), min(y_coords), min(z_coords),
            max(x_coords), max(y_coords), max(z_coords)
        ]

# 启动应用程序
if __name__ == "__main__":
    root = tk.Tk()
    app = FBXTo3DTilesConverter(root)
    root.mainloop()

4. 技术要点说明

4.1 坐标系统处理

本程序采用WGS84地理坐标系(EPSG:4326)到地心地固坐标系(EPSG:4978)的转换,确保3D模型能够正确放置在真实地理位置上。北京默认坐标设置为天安门位置(经度116.3974,纬度39.9093)。

4.2 文件格式选择

  • GLB格式:标准的glTF二进制格式,适用于WebGL和大多数3D引擎
  • B3DM格式:3D Tiles的批处理3D模型格式,包含额外的要素表和批处理表,适合大规模场景

4.3 压缩技术应用

程序支持Draco顶点压缩,可显著减少文件大小而不损失视觉质量。压缩比率根据模型复杂度自动调整,确保在保持质量的同时最大化压缩效果。

4.4 性能优化策略

  • 采用增量式文件处理,避免内存溢出
  • 实现智能网格分割算法,保持模型完整性
  • 多线程处理大型文件转换任务

该解决方案提供了完整的FBX到3DTiles转换工作流,具备良好的用户体验和可靠的技术实现,能够满足大规模3D地理数据处理的需求。


参考来源

相关推荐
2401_857865232 小时前
使用XGBoost赢得Kaggle比赛
jvm·数据库·python
KIKIiiiiiiii2 小时前
微信自动化机器人开发
java·开发语言·人工智能·python·微信·自动化
暮冬-  Gentle°2 小时前
使用Python进行网络设备自动配置
jvm·数据库·python
badhope2 小时前
Python、C、Java 终极对决!谁主沉浮?谁将消亡?
java·c语言·开发语言·javascript·人工智能·python·github
薛不痒2 小时前
模型部署:基于flask和pytorch
人工智能·pytorch·python·深度学习·flask
m0_743297422 小时前
将Python Web应用部署到服务器(Docker + Nginx)
jvm·数据库·python
小邓睡不饱耶2 小时前
实战教程:Python爬取北京新发地农产品价格数据并存储到MySQL
开发语言·python·mysql
一直都在5722 小时前
JSoup:Java 处理 HTML 的实用利器,从基础到实战爬取教程
java·python·html
EnCi Zheng2 小时前
P1B-Python环境配置基础完全指南-Windows系统安装与验证
开发语言·windows·python