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地理数据处理的需求。


参考来源

相关推荐
kcuwu.1 分钟前
Python进阶:生成器与协程,高效并发编程的核心实践
windows·python·php
XiaoQiao6669992 分钟前
python 简单题目练手【详解版】【1】
开发语言·python
ZC跨境爬虫7 分钟前
极验滑动验证码自动化实战:背景提取、缺口定位与Playwright滑动模拟
前端·爬虫·python·自动化
智算菩萨9 分钟前
【Python图像处理】2 数字图像基础与Python图像表示
开发语言·图像处理·python
xiaoshuaishuai81 小时前
Git二分法定位Bug
开发语言·python
2401_835792541 小时前
FastAPI 速通
windows·python·fastapi
YMWM_2 小时前
export MPLBACKEND=Agg命令使用
linux·python
派大星~课堂2 小时前
【力扣-148. 排序链表】Python笔记
python·leetcode·链表
微涼5302 小时前
【Python】在使用联网工具时需要的问题
服务器·python·php
小白菜又菜2 小时前
Leetcode 657. Robot Return to Origin
python·leetcode·职场和发展