3D Tiles 格式详解
目录 简介核心概念文件结构tileset.json 格式tile.json 格式B3DM 格式坐标系统LOD 2025-10-21 01:14:41 Author: blog.csdn.net(查看原文) 阅读量:0 收藏

目录


简介

3D Tiles 是 Cesium 开发的用于流式传输和渲染大规模 3D 地理空间数据的开放标准。它通过层次化的瓦片结构(Tile Hierarchy)和多级细节(LOD)机制,实现了海量 3D 模型的高效加载和渲染。

本文档以**德国法兰克福(WEU_FRANKFURT)**数据集为实际案例,提供了 3D Tiles 格式的全面技术指南。配套的演示应用程序基于 Cesium Native 构建,展示了解析、加载和渲染 3D Tiles 数据集的实际实现。

主要特点

  • 层次化结构:树形组织,支持渐进式加载
  • 多种内容格式:B3DM(批量模型)、I3DM(实例模型)、PNTS(点云)、CMPT(组合)
  • 空间索引:边界体积(Bounding Volume)实现快速剔除
  • LOD 控制:几何误差(Geometric Error)驱动的细节层级选择
  • 元数据支持:Batch Table、Feature Table 存储属性数据

应用场景

  • 🌍 城市建筑模型(CityGML、倾斜摄影)
  • 🗺️ 地形数据(高程网格)
  • 🏛️ 地标建筑(精细模型)
  • 🌲 大规模植被(实例化渲染)
  • ☁️ 点云数据(激光雷达)

示例

3D Tile Cesium Viewer


核心概念

1. Tileset(瓦片集)

Tileset 是 3D Tiles 数据集的根对象,定义了整个数据集的元信息和根瓦片。

关键字段

  • asset:资产信息(版本、作者、日期)
  • geometricError:根瓦片的几何误差(米)
  • root:根 Tile 对象
  • properties:元数据属性定义

2. Tile(瓦片)

Tile 是空间数据的基本单元,形成树形层次结构。每个 Tile 包含:

核心属性

  • boundingVolume:空间边界(region/box/sphere)
  • geometricError:屏幕空间误差阈值(SSE)
  • refine:细化策略(ADD/REPLACE
  • content:内容引用(URI)
  • transform:变换矩阵(4×4,列主序)
  • children:子瓦片数组

3. Bounding Volume(边界体积)

用于视锥剔除和 LOD 选择,支持三种类型:

类型数组长度说明
region6[west, south, east, north, minHeight, maxHeight](弧度和米)
box12中心点(3)+ X轴(3)+ Y轴(3)+ Z轴(3)
sphere4[centerX, centerY, centerZ, radius](笛卡尔坐标)

4. Geometric Error(几何误差)

表示当前 Tile 的近似误差(米)。LOD 选择基于屏幕空间误差(SSE):

SSE = (geometricError × height) / (distance × 2 × tan(FOV / 2))
  • SSE > 阈值:需要细化,加载子瓦片
  • SSE ≤ 阈值:当前瓦片足够精确

5. Refine 策略

策略说明应用场景
ADD叠加渲染:父瓦片 + 子瓦片地形、逐级增加细节
REPLACE替换渲染:仅渲染子瓦片建筑物、完全替换父级

文件结构

典型的 3D Tiles 数据集目录

WEU_FRANKFURT/
├── tileset.json                        # 根 Tileset 文件
├── 1510449583.buildings.tile.json      # 建筑物子瓦片集
├── 1510449583.buildings.b3dm           # 建筑物模型数据
├── 1510449583.terrain.tile.json        # 地形子瓦片集
├── 1510449583.terrain.b3dm             # 地形模型数据
├── DEU_057_FRANKFURT_PAULSKI.landmark.tile.json   # 地标子瓦片集
├── DEU_057_FRANKFURT_PAULSKI.landmark.b3dm        # 地标模型数据
└── ...                                 # 更多瓦片文件

文件命名规范

  • Tileset: tileset.json(根文件)
  • Tile: <id>.<type>.tile.json
    • <id>:唯一标识符(如时间戳、编号)
    • <type>:图层类型(buildingsterrainlandmark
  • Content: <id>.<type>.b3dm(对应的二进制模型文件)

tileset.json 格式

完整结构

{
  "asset": {
    "version": "1.0",           // 3D Tiles 规范版本
    "tilesetVersion": "1.0.0",  // 数据集版本(可选)
    "gltfUpAxis": "Y"           // glTF 上轴(可选)
  },
  "geometricError": 482.34,     // 根瓦片几何误差(米)
  "root": {                     // 根 Tile 对象
    "boundingVolume": {
      "region": [
        0.1504742724916318,     // west(弧度)
        0.8742841244217815,     // south(弧度)
        0.15203862091564313,    // east(弧度)
        0.8749724238967941,     // north(弧度)
        134.65468889328199,     // minHeight(米)
        513.2841617178684       // maxHeight(米)
      ]
    },
    "geometricError": 482.34,   // 当前 Tile 的几何误差
    "refine": "ADD",            // 细化策略(ADD 或 REPLACE)
    "children": [               // 子瓦片数组
      {
        "boundingVolume": { "region": [...] },
        "geometricError": 135.22,
        "content": {
          "uri": "1510449583.buildings.tile.json"  // 子瓦片集引用
        }
      },
      {
        "boundingVolume": { "region": [...] },
        "geometricError": 151.33,
        "content": {
          "uri": "1510449583.terrain.tile.json"    // 另一个子瓦片集
        }
      }
    ]
  },
  "properties": {               // 元数据属性定义(可选)
    "Height": {
      "minimum": 1.0,
      "maximum": 241.6
    }
  }
}

关键字段解析

asset

定义数据集的元信息:

"asset": {
  "version": "1.0",           // 必需:3D Tiles 规范版本
  "tilesetVersion": "2.0",    // 可选:数据集版本
  "gltfUpAxis": "Y"           // 可选:glTF 上轴(Y 或 Z)
}
root

根 Tile 对象,包含整个数据集的空间范围和初始瓦片:

"root": {
  "boundingVolume": { ... },  // 空间边界
  "geometricError": 482.34,   // 几何误差
  "refine": "ADD",            // 细化策略
  "children": [ ... ]         // 子瓦片(可选)
}

tile.json 格式

示例:buildings.tile.json

{
  "asset": {
    "version": "1.0"
  },
  "geometricError": 135.22,   // 当前瓦片集的几何误差
  "root": {
    "boundingVolume": {
      "region": [
        0.15102648717863973,  // west: 约 8.65°E
        0.8743073700305852,   // south: 约 50.1°N
        0.1511008554861323,   // east
        0.8743774694361734,   // north
        136.32472193284124,   // minHeight: 136.3m
        166.8066269783693     // maxHeight: 166.8m
      ]
    },
    "geometricError": 0.0,    // ⚠️ 叶子节点,不再细化
    "refine": "ADD",
    "transform": [            // 4×4 变换矩阵(列主序)
      -0.15042804843574237, -0.7583488025695948, 0.6342542833005352, 0,
       0.9886209598444765,  -0.11538995736249825, 0.09650780018250797, 0,
       6.657811999962993e-18, 0.6415545583823271, 0.7670774071883864, 0,
       4053351.715689529,     616755.8781181051,   4869372.124682956, 1
    ],
    "content": {
      "uri": "1510449583.buildings.b3dm"  // B3DM 模型文件
    }
  }
}

关键字段解析

geometricError: 0.0

表示该瓦片是叶子节点,不再有子瓦片。SSE 检查会直接渲染此瓦片内容。

transform

4×4 变换矩阵(列主序,右乘):

| R00  R10  R20  Tx |
| R01  R11  R21  Ty |
| R02  R12  R22  Tz |
|  0    0    0    1 |
  • 前 3×3:旋转矩阵(将模型从本地坐标系转换到 ECEF 坐标系)
  • 第 4 列:平移向量(ECEF 坐标,单位:米)

应用顺序

世界坐标 = 根瓦片 transform × 子瓦片 transform × ... × 顶点坐标
content.uri

引用的内容文件:

  • 相对路径:相对于当前 JSON 文件的路径
  • 绝对路径:完整 URL(用于远程数据)
  • 支持格式.b3dm.i3dm.pnts.cmpt.gltf.glb.json(嵌套 Tileset)

B3DM 格式

B3DM(Batched 3D Model) 是一种二进制格式,用于存储批量 3D 模型数据,支持高效的 GPU 渲染。

文件结构

┌─────────────────────────────────────────────────────────┐
│ Header (28 bytes)                                       │
├─────────────────────────────────────────────────────────┤
│ Feature Table JSON (可变长度)                           │
├─────────────────────────────────────────────────────────┤
│ Feature Table Binary (可变长度)                         │
├─────────────────────────────────────────────────────────┤
│ Batch Table JSON (可变长度)                             │
├─────────────────────────────────────────────────────────┤
│ Batch Table Binary (可变长度)                           │
├─────────────────────────────────────────────────────────┤
│ glTF/GLB Binary (可变长度)                               │
└─────────────────────────────────────────────────────────┘

Header 结构(28 字节)

偏移类型字段说明
0char[4]magic魔法字节:b3dm(0x62 0x33 0x64 0x6D)
4uint32version版本号(通常为 1)
8uint32byteLength整个文件的字节长度
12uint32featureTableJsonByteLengthFeature Table JSON 长度
16uint32featureTableBinaryByteLengthFeature Table Binary 长度
20uint32batchTableJsonByteLengthBatch Table JSON 长度
24uint32batchTableBinaryByteLengthBatch Table Binary 长度

实际示例:1510449583.buildings.b3dm

使用 hexdump 查看文件头部:

00000000  62 33 64 6d 01 00 00 00  e8 b3 00 00 14 00 00 00  |b3dm............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 7b 22 42 41  |............{"BA|
00000020  54 43 48 5f 4c 45 4e 47  54 48 22 3a 30 7d 20 20  |TCH_LENGTH":0}  |
00000030  67 6c 54 46 02 00 00 00  b8 b3 00 00 14 23 00 00  |glTF.........#..|

解析

  • 0x00-0x0362 33 64 6d"b3dm"(魔法字节)
  • 0x04-0x0701 00 00 00 → 版本号 = 1
  • 0x08-0x0Be8 b3 00 00 → 文件总长度 = 46,056 字节(0xB3E8)
  • 0x0C-0x0F14 00 00 00 → Feature Table JSON 长度 = 20 字节
  • 0x10-0x1300 00 00 00 → Feature Table Binary 长度 = 0
  • 0x14-0x1700 00 00 00 → Batch Table JSON 长度 = 0
  • 0x18-0x1B00 00 00 00 → Batch Table Binary 长度 = 0

Feature Table JSON(0x1C 起)

{"BATCH_LENGTH":0}

glTF 数据(0x30 起)

  • 魔法字节:67 6c 54 46"glTF"

Feature Table

存储每个 Feature(特征)的几何信息

常见属性

  • BATCH_LENGTH:批次数量
  • RTC_CENTER:相对坐标中心(Relative to Center)
  • QUANTIZED_VOLUME_OFFSET:量化体积偏移
  • QUANTIZED_VOLUME_SCALE:量化体积缩放

示例

{
  "BATCH_LENGTH": 100,
  "RTC_CENTER": [4053351.7, 616755.9, 4869372.1]
}

Batch Table

存储每个 Feature 的属性数据(元数据):

示例

{
  "buildingId": [1, 2, 3, ...],
  "buildingName": ["Building A", "Building B", ...],
  "height": [30.5, 45.2, 60.0, ...],
  "buildDate": ["2020-01-01", "2019-05-15", ...]
}

glTF/GLB 数据

嵌入的 glTF 2.0 模型数据(GLB 二进制格式),包含:

  • Meshes:网格几何
  • Materials:材质(PBR)
  • Textures:纹理图像
  • Animations:动画(可选)

坐标系统

ECEF(Earth-Centered, Earth-Fixed)

3D Tiles 使用 ECEF 笛卡尔坐标系

  • 原点:地球质心
  • X 轴:指向赤道与本初子午线交点
  • Y 轴:指向赤道与东经 90° 交点
  • Z 轴:指向北极
  • 单位:米

经纬度 → ECEF 转换

// WGS84 椭球参数
const double a = 6378137.0;                    // 长半轴(米)
const double e2 = 0.00669437999014;            // 第一偏心率平方

// 输入:经纬度(弧度)、高程(米)
double lon, lat, height;

// 计算卯酉圈曲率半径
double N = a / sqrt(1.0 - e2 * sin(lat) * sin(lat));

// ECEF 坐标
double x = (N + height) * cos(lat) * cos(lon);
double y = (N + height) * cos(lat) * sin(lon);
double z = (N * (1.0 - e2) + height) * sin(lat);

Region 边界体积

region 使用 大地坐标(经纬度 + 高程):

"boundingVolume": {
  "region": [
    0.1504742724916318,   // west: 约 8.62°E
    0.8742841244217815,   // south: 约 50.09°N
    0.15203862091564313,  // east: 约 8.71°E
    0.8749724238967941,   // north: 约 50.13°N
    134.65468889328199,   // minHeight: 134.7m
    513.2841617178684     // maxHeight: 513.3m
  ]
}

转换关系

  • 经度(弧度)= 角度 × π / 180
  • 纬度(弧度)= 角度 × π / 180

LOD 机制

屏幕空间误差(SSE)计算

double calculateSSE(
    const Tile& tile,
    const glm::dvec3& cameraPosition,
    double fovY,
    int screenHeight)
{
    // 1. 计算相机到 Tile 边界的距离
    double distance = calculateDistanceToBoundingVolume(
        tile.boundingVolume,
        cameraPosition
    );
    
    // 2. 计算屏幕空间误差
    double sse = (tile.geometricError * screenHeight) /
                 (distance * 2.0 * tan(fovY / 2.0));
    
    return sse;
}

LOD 选择策略

bool shouldRefine(const Tile& tile, double sse, double sseThreshold) {
    // SSE > 阈值:需要加载更精细的子瓦片
    if (sse > sseThreshold && tile.children.size() > 0) {
        return true;  // 递归加载子瓦片
    }
    
    // SSE ≤ 阈值:当前瓦片足够精确
    return false;  // 渲染当前瓦片
}

Refine 策略对比

ADD(叠加)
渲染:父瓦片 + 子瓦片(共存)

应用场景:
- 地形数据(逐级增加细节)
- 植被(逐级增加密度)

示例:
Level 0: ████████ (粗略地形)
Level 1: ████████ + ▓▓▓▓▓▓▓▓ (增加细节)
REPLACE(替换)
渲染:仅子瓦片(父瓦片隐藏)

应用场景:
- 建筑物(完全替换低精度模型)
- 精细模型(避免重复渲染)

示例:
Level 0: ████████ (低精度)
Level 1:           ▓▓▓▓▓▓▓▓ (高精度,完全替代)

实例解析:WEU_FRANKFURT

数据集概览

位置:德国法兰克福(Frankfurt am Main)
范围:约 8.62°E - 8.71°E,50.09°N - 50.13°N
高程:134.7m - 513.3m
图层

  • terrain(地形):40 个瓦片
  • buildings(建筑物):40 个瓦片
  • landmark(地标):78 个精细模型

总瓦片数:158

tileset.json 结构

{
  "asset": { "version": "1.0" },
  "geometricError": 482.34,    // 根瓦片:约 482 米误差
  "root": {
    "boundingVolume": {
      "region": [
        0.1504742724916318,   // 8.62°E
        0.8742841244217815,   // 50.09°N
        0.15203862091564313,  // 8.71°E
        0.8749724238967941,   // 50.13°N
        134.65468889328199,   // 135m
        513.2841617178684     // 513m
      ]
    },
    "geometricError": 482.34,
    "refine": "ADD",
    "children": [
      // 158 个子瓦片(buildings, terrain, landmark)
    ]
  }
}

图层分类

1. Buildings(建筑物)

特点

  • 数量:40 个瓦片
  • 几何误差:19.15 - 482.34 米
  • RefineADD
  • 应用:城市建筑群,逐级增加细节

示例1510449583.buildings.tile.json

{
  "geometricError": 135.22,
  "root": {
    "boundingVolume": {
      "region": [0.151026, 0.874307, 0.151101, 0.874377, 136.3, 166.8]
    },
    "geometricError": 0.0,      // 叶子节点
    "refine": "ADD",
    "transform": [ ... ],        // ECEF 变换矩阵
    "content": {
      "uri": "1510449583.buildings.b3dm"
    }
  }
}
2. Terrain(地形)

特点

  • 数量:40 个瓦片
  • 几何误差:43.54 - 363.24 米
  • RefineADD
  • 应用:地形网格,逐级增加分辨率

示例1510449583.terrain.tile.json

{
  "geometricError": 151.33,
  "root": {
    "boundingVolume": {
      "region": [0.150001, 0.874297, 0.151097, 0.874369, 136.0, 150.5]
    },
    "geometricError": 0.0,
    "refine": "ADD",
    "transform": [ ... ],
    "content": {
      "uri": "1510449583.terrain.b3dm"
    }
  }
}
3. Landmark(地标)

特点

  • 数量:78 个瓦片
  • 几何误差:5.39 - 111.78 米
  • RefineADD
  • 应用:精细地标建筑(塔楼、教堂、纪念碑)

示例DEU_057_FRANKFURT_PAULSKI.landmark.tile.json

{
  "geometricError": 19.15,
  "root": {
    "boundingVolume": {
      "region": [0.151501, 0.874600, 0.151513, 0.874610, 139.5, 206.4]
    },
    "geometricError": 0.0,
    "refine": "ADD",
    "transform": [
      -0.15093, -0.75850,  0.63396, 0,
       0.98854, -0.11581,  0.09679, 0,
       0.00000,  0.64130,  0.76729, 0,
       4051548.17, 618584.27, 4870821.97, 1
    ],
    "content": {
      "uri": "DEU_057_FRANKFURT_PAULSKI.landmark.b3dm"
    }
  }
}

空间组织

tileset.json (root)
├─ geometricError: 482.34m
│
├─ buildings (40 瓦片)
│  ├─ 1510449583.buildings (geometricError: 135.22m)
│  ├─ 1510449594.buildings (geometricError: 239.23m)
│  └─ ...
│
├─ terrain (40 瓦片)
│  ├─ 1510449583.terrain (geometricError: 151.33m)
│  ├─ 1510449594.terrain (geometricError: 238.60m)
│  └─ ...
│
└─ landmark (78 瓦片)
   ├─ DEU_057_FRANKFURT_PAULSKI (geometricError: 19.15m)
   ├─ DEU_074_FRANKFURT_ALTEOPE (geometricError: 32.60m)
   ├─ DEU_136_FRANKFURT_MAINTOW (geometricError: 29.46m, 361m 高)
   └─ ...

几何误差分布

图层最小最大平均说明
Landmark5.39m111.78m~30m精细模型,低误差
Buildings19.15m482.34m~200m中等细节
Terrain43.54m363.24m~180m地形网格

代码实现

1. tileset.json 解析(TileJsonManager)

// TileJsonManager.cpp
std::optional<Cesium3DTiles::Tileset> 
TileJsonManager::parseTilesetJsonFile(const std::string& filePath) {
    // 1. 读取 JSON 文件
    std::ifstream file(filePath);
    std::string jsonContent(
        (std::istreambuf_iterator<char>(file)),
        std::istreambuf_iterator<char>()
    );
    
    // 2. 解析 JSON
    rapidjson::Document document;
    document.Parse(jsonContent.c_str());
    
    if (document.HasParseError()) {
        spdlog::error("JSON 解析失败: {}", filePath);
        return std::nullopt;
    }
    
    // 3. 创建 Tileset 对象
    Cesium3DTiles::Tileset tileset;
    
    // 解析 asset
    if (document.HasMember("asset")) {
        const auto& asset = document["asset"];
        if (asset.HasMember("version")) {
            tileset.asset.version = asset["version"].GetString();
        }
    }
    
    // 解析 geometricError
    if (document.HasMember("geometricError")) {
        tileset.geometricError = document["geometricError"].GetDouble();
    }
    
    // 解析 root Tile
    if (document.HasMember("root")) {
        tileset.root = parseTileObject(document["root"]);
    }
    
    return tileset;
}

2. tile.json 解析

// TileJsonManager.cpp
std::optional<Cesium3DTiles::Tile> 
TileJsonManager::parseTileJsonFile(const std::string& filePath) {
    // 检测 JSON 格式(Tileset 或 Tile)
    bool isTilesetFormat = document.HasMember("root");
    
    if (isTilesetFormat) {
        // Tileset 格式:提取 root Tile
        return parseTileObject(document["root"], filePath);
    } else {
        // Tile 格式:直接解析
        return parseTileObject(document, filePath);
    }
}

3. B3DM 加载

// ModelLoader.cpp
std::shared_ptr<CesiumGltf::Model> 
ModelLoader::loadB3DMFileInternal(const std::string& filePath) {
    // 1. 读取二进制文件
    std::ifstream file(filePath, std::ios::binary);
    std::vector<std::byte> fileData(fileSize);
    file.read(reinterpret_cast<char*>(fileData.data()), fileSize);
    
    // 2. 创建 AssetFetcher
    Cesium3DTilesContent::AssetFetcher assetFetcher(
        _asyncSystem,
        nullptr,
        "",
        glm::dmat4(1.0),  // identity transform
        {},
        CesiumGeometry::Axis::Y
    );
    
    // 3. 使用 Cesium 的 B3DM 转换器
    auto future = Cesium3DTilesContent::B3dmToGltfConverter::convert(
        std::span<const std::byte>(fileData),
        CesiumGltfReader::GltfReaderOptions(),
        assetFetcher
    );
    
    // 4. 等待转换完成
    auto result = future.wait();
    if (result.model.has_value()) {
        return std::make_shared<CesiumGltf::Model>(
            std::move(result.model.value())
        );
    }
    
    return nullptr;
}

4. LOD 选择

// TilesetViewer.cpp
void TilesetViewer::selectTilesRecursively(
    const Cesium3DTiles::Tile& tile,
    const glm::dmat4& parentTransform,
    std::vector<const Cesium3DTiles::Tile*>& selectedTiles)
{
    // 1. 计算累积变换矩阵
    glm::dmat4 computedTransform = parentTransform;
    if (!tile.transform.empty()) {
        glm::dmat4 tileTransform = MathUtil::tileTransformToMatrix(tile.transform);
        computedTransform = parentTransform * tileTransform;
    }
    
    // 2. 计算屏幕空间误差(SSE)
    double sse = _lodCalculator.calculateScreenSpaceError(
        tile.boundingVolume,
        tile.geometricError,
        _viewState.position,
        _viewState.viewportHeight,
        _viewState.fovy
    );
    
    // 3. 视锥剔除
    if (!_frustum.intersects(tile.boundingVolume, computedTransform)) {
        return;  // 不在视野内,跳过
    }
    
    // 4. 图层过滤
    if (_filterEnabled && shouldFilterLayer(tile.content.uri)) {
        return;  // 图层被过滤,跳过
    }
    
    // 5. LOD 选择
    if (sse > _lodCalculator.getSseThreshold() && !tile.children.empty()) {
        // SSE > 阈值:递归加载子瓦片
        for (const auto& child : tile.children) {
            selectTilesRecursively(child, computedTransform, selectedTiles);
        }
    } else {
        // SSE ≤ 阈值:选择当前瓦片渲染
        if (tile.content.has_value()) {
            selectedTiles.push_back(&tile);
            _tileTransforms[&tile] = computedTransform;  // 保存变换矩阵
        }
    }
}

5. 渲染管线(ModelRender)

// ModelRender.cpp
void ModelRender::renderTiles(
    const std::vector<const Cesium3DTiles::Tile*>& tiles)
{
    // 1. 遍历选中的瓦片
    for (const auto* tile : tiles) {
        // 2. 获取瓦片的变换矩阵
        glm::dmat4 computedTransform = getTileTransform(tile);
        
        // 3. 加载内容
        if (tile->content.uri.ends_with(".tile.json")) {
            // 嵌套 Tileset:递归加载
            loadNestedTileset(tile->content.uri);
        } else if (tile->content.uri.ends_with(".b3dm")) {
            // B3DM 模型
            renderB3DMContent(*tile, tile->content, computedTransform);
        } else if (tile->content.uri.ends_with(".gltf") || 
                   tile->content.uri.ends_with(".glb")) {
            // GLTF 模型
            renderGLTFContent(*tile, tile->content, computedTransform);
        }
    }
}

void ModelRender::renderB3DMContent(
    const Cesium3DTiles::Tile& tile,
    const Cesium3DTiles::Content& content,
    const glm::dmat4& computedTransform)
{
    // 1. 构建文件路径
    std::string filePath = buildFilePath(_tilesetDir, content.uri);
    
    // 2. 加载模型(带缓存)
    auto modelPtr = _modelLoader->loadModelPtr(filePath);
    if (!modelPtr) return;
    
    // 3. 渲染 glTF 模型
    renderGLTFModel(
        tile,
        *modelPtr,
        computedTransform,  // 传递瓦片变换矩阵
        _viewMatrix,
        _projectionMatrix,
        _cameraPosition,
        _lightPosition
    );
}

6. 性能优化

模型缓存
// ModelLoader.cpp
std::shared_ptr<CesiumGltf::Model> 
ModelLoader::loadModelPtr(const std::string& filePath) {
    std::lock_guard<std::mutex> lock(_cacheMutex);
    
    // 检查缓存
    auto it = _modelCache.find(filePath);
    if (it != _modelCache.end()) {
        _cacheHits++;
        return it->second;  // 返回共享指针,避免复制
    }
    
    // 缓存未命中:加载模型
    _cacheMisses++;
    auto model = loadB3DMFileInternal(filePath);
    _modelCache[filePath] = model;
    
    return model;
}
Tile JSON 缓存
// TileJsonManager.cpp
std::optional<Cesium3DTiles::Tile> 
TileJsonManager::getOrParseTileJson(const std::string& filePath) {
    std::lock_guard<std::mutex> lock(_cacheMutex);
    
    // 检查缓存
    auto it = _tileCache.find(filePath);
    if (it != _tileCache.end()) {
        _cacheHits++;
        return it->second;
    }
    
    // 解析并缓存
    _cacheMisses++;
    auto tile = parseTileJsonFile(filePath);
    _tileCache[filePath] = tile;
    
    return tile;
}
扁平化遍历
// ShaderRender.cpp
void ShaderRender::flattenModelPrimitives(const CesiumGltf::Model& model) {
    _flatPrimitives.clear();
    
    // 遍历场景图,扁平化到线性数组
    for (const auto& scene : model.scenes) {
        for (int nodeIndex : scene.nodes) {
            flattenNode(model, nodeIndex, glm::dmat4(1.0));
        }
    }
}

void ShaderRender::flattenNode(
    const CesiumGltf::Model& model,
    int nodeIndex,
    const glm::dmat4& parentTransform)
{
    const auto& node = model.nodes[nodeIndex];
    
    // 计算节点变换
    glm::dmat4 nodeTransform = parentTransform * getNodeTransform(node);
    
    // 处理网格
    if (node.mesh >= 0) {
        const auto& mesh = model.meshes[node.mesh];
        for (size_t i = 0; i < mesh.primitives.size(); ++i) {
            _flatPrimitives.push_back({
                node.mesh,
                static_cast<int>(i),
                nodeTransform
            });
        }
    }
    
    // 递归处理子节点
    for (int childIndex : node.children) {
        flattenNode(model, childIndex, nodeTransform);
    }
}

最佳实践

1. 数据组织

推荐

  • 图层类型分离(terrain、buildings、landmark)
  • 使用有意义的命名(包含 ID、类型、区域)
  • 合理设置 geometricError(根据模型复杂度)

避免

  • 单个 Tileset 包含所有数据(过大)
  • 过深的树结构(超过 10 层)
  • 不必要的嵌套 .tile.json

2. 几何误差设置

级别几何误差应用场景
Level 0500-1000m全球/区域根瓦片
Level 1100-500m城市级别
Level 220-100m街区级别
Level 35-20m建筑物级别
Level 40-5m精细模型(叶子节点)

3. Refine 策略选择

内容类型推荐策略理由
地形ADD逐级增加分辨率,保留低精度数据
建筑物REPLACE完全替换低精度模型,避免重叠
植被ADD逐级增加密度
地标REPLACE精细模型完全替代简化版

4. 性能优化

缓存策略
// 三级缓存:
// 1. Tile JSON 缓存(TileJsonManager)
// 2. B3DM 模型缓存(ModelLoader)
// 3. OpenGL VAO/VBO 缓存(ShaderRender)
批量渲染
// 按材质/纹理分组,减少状态切换
std::unordered_map<MaterialKey, std::vector<Primitive>> batchedPrimitives;

for (const auto& primitive : allPrimitives) {
    MaterialKey key = getMaterialKey(primitive);
    batchedPrimitives[key].push_back(primitive);
}

for (const auto& [key, primitives] : batchedPrimitives) {
    bindMaterial(key);
    for (const auto& primitive : primitives) {
        drawPrimitive(primitive);
    }
}
增量更新
// 仅在相机移动超过阈值时更新 LOD
if (glm::distance(camera.position, _lastCameraPosition) > _updateThreshold) {
    updateLODSelection();
    _lastCameraPosition = camera.position;
}

5. 调试技巧

可视化边界体积
// 渲染 Bounding Volume 边框
void renderBoundingVolume(const BoundingVolume& bv) {
    if (!bv.region.empty()) {
        renderRegion(bv.region);
    } else if (!bv.box.empty()) {
        renderBox(bv.box);
    } else if (!bv.sphere.empty()) {
        renderSphere(bv.sphere);
    }
}
几何误差热力图
// 按 geometricError 着色
glm::vec3 getErrorColor(double geometricError) {
    if (geometricError > 200) return glm::vec3(1, 0, 0);  // 红色:高误差
    if (geometricError > 50)  return glm::vec3(1, 1, 0);  // 黄色:中误差
    return glm::vec3(0, 1, 0);  // 绿色:低误差
}

6. 常见问题

Q: 为什么某些瓦片不显示?

A: 可能的原因:

  1. 视锥剔除:瓦片不在视野内
  2. LOD 选择:SSE 太小,未达到渲染阈值
  3. 图层过滤:图层被关闭(检查键盘 1/2/3)
  4. 文件路径错误:URI 指向不存在的文件

调试

spdlog::info("Tile SSE: {}, Threshold: {}", sse, threshold);
spdlog::info("Frustum culled: {}", !frustum.intersects(bv));
Q: 如何提高渲染帧率?

A: 优化策略:

  1. 启用 LOD 优化(按 O 键)
  2. 关闭不需要的图层(按 1/2/3 键)
  3. 降低 SSE 阈值(减少瓦片数量)
  4. 启用批量渲染
  5. 使用增量更新
Q: transform 矩阵如何应用?

A: 矩阵累乘(从根到叶):

世界坐标 = 根 transform × 子 transform × ... × 顶点坐标

// 代码实现
glm::dmat4 computedTransform = parentTransform;
if (!tile.transform.empty()) {
    glm::dmat4 tileTransform = tileTransformToMatrix(tile.transform);
    computedTransform = parentTransform * tileTransform;
}
Q: Region 和 Box 有什么区别?

A:

类型适用场景优点缺点
region大范围地理数据直观(经纬度)极点附近失真
box局部模型、倾斜数据任意方向、紧凑需要变换矩阵
sphere球形对象、快速剔除计算简单空间浪费

参考资源

官方文档

  • 3D Tiles 规范: https://github.com/CesiumGS/3d-tiles/tree/main/specification
  • B3DM 格式: https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel
  • glTF 2.0: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html

工具

  • Cesium Native: https://github.com/CesiumGS/cesium-native
  • CesiumJS: https://cesium.com/platform/cesiumjs/
  • 3D Tiles Validator: https://github.com/CesiumGS/3d-tiles-validator

文章来源: https://blog.csdn.net/kesalin/article/details/153677066
如有侵权请联系:admin#unsafe.sh