3D Tiles 是 Cesium 开发的用于流式传输和渲染大规模 3D 地理空间数据的开放标准。它通过层次化的瓦片结构(Tile Hierarchy)和多级细节(LOD)机制,实现了海量 3D 模型的高效加载和渲染。
本文档以**德国法兰克福(WEU_FRANKFURT)**数据集为实际案例,提供了 3D Tiles 格式的全面技术指南。配套的演示应用程序基于 Cesium Native 构建,展示了解析、加载和渲染 3D Tiles 数据集的实际实现。

Tileset 是 3D Tiles 数据集的根对象,定义了整个数据集的元信息和根瓦片。
关键字段:
asset:资产信息(版本、作者、日期)geometricError:根瓦片的几何误差(米)root:根 Tile 对象properties:元数据属性定义Tile 是空间数据的基本单元,形成树形层次结构。每个 Tile 包含:
核心属性:
boundingVolume:空间边界(region/box/sphere)geometricError:屏幕空间误差阈值(SSE)refine:细化策略(ADD/REPLACE)content:内容引用(URI)transform:变换矩阵(4×4,列主序)children:子瓦片数组用于视锥剔除和 LOD 选择,支持三种类型:
| 类型 | 数组长度 | 说明 |
|---|---|---|
| region | 6 | [west, south, east, north, minHeight, maxHeight](弧度和米) |
| box | 12 | 中心点(3)+ X轴(3)+ Y轴(3)+ Z轴(3) |
| sphere | 4 | [centerX, centerY, centerZ, radius](笛卡尔坐标) |
表示当前 Tile 的近似误差(米)。LOD 选择基于屏幕空间误差(SSE):
SSE = (geometricError × height) / (distance × 2 × tan(FOV / 2))
| 策略 | 说明 | 应用场景 |
|---|---|---|
| ADD | 叠加渲染:父瓦片 + 子瓦片 | 地形、逐级增加细节 |
| REPLACE | 替换渲染:仅渲染子瓦片 | 建筑物、完全替换父级 |
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.json(根文件)<id>.<type>.tile.json
<id>:唯一标识符(如时间戳、编号)<type>:图层类型(buildings、terrain、landmark)<id>.<type>.b3dm(对应的二进制模型文件){
"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": {
"version": "1.0", // 必需:3D Tiles 规范版本
"tilesetVersion": "2.0", // 可选:数据集版本
"gltfUpAxis": "Y" // 可选:glTF 上轴(Y 或 Z)
}
根 Tile 对象,包含整个数据集的空间范围和初始瓦片:
"root": {
"boundingVolume": { ... }, // 空间边界
"geometricError": 482.34, // 几何误差
"refine": "ADD", // 细化策略
"children": [ ... ] // 子瓦片(可选)
}
{
"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 模型文件
}
}
}
表示该瓦片是叶子节点,不再有子瓦片。SSE 检查会直接渲染此瓦片内容。
4×4 变换矩阵(列主序,右乘):
| R00 R10 R20 Tx |
| R01 R11 R21 Ty |
| R02 R12 R22 Tz |
| 0 0 0 1 |
应用顺序:
世界坐标 = 根瓦片 transform × 子瓦片 transform × ... × 顶点坐标
引用的内容文件:
.b3dm、.i3dm、.pnts、.cmpt、.gltf、.glb、.json(嵌套 Tileset)B3DM(Batched 3D Model) 是一种二进制格式,用于存储批量 3D 模型数据,支持高效的 GPU 渲染。
┌─────────────────────────────────────────────────────────┐
│ Header (28 bytes) │
├─────────────────────────────────────────────────────────┤
│ Feature Table JSON (可变长度) │
├─────────────────────────────────────────────────────────┤
│ Feature Table Binary (可变长度) │
├─────────────────────────────────────────────────────────┤
│ Batch Table JSON (可变长度) │
├─────────────────────────────────────────────────────────┤
│ Batch Table Binary (可变长度) │
├─────────────────────────────────────────────────────────┤
│ glTF/GLB Binary (可变长度) │
└─────────────────────────────────────────────────────────┘
| 偏移 | 类型 | 字段 | 说明 |
|---|---|---|---|
| 0 | char[4] | magic | 魔法字节:b3dm(0x62 0x33 0x64 0x6D) |
| 4 | uint32 | version | 版本号(通常为 1) |
| 8 | uint32 | byteLength | 整个文件的字节长度 |
| 12 | uint32 | featureTableJsonByteLength | Feature Table JSON 长度 |
| 16 | uint32 | featureTableBinaryByteLength | Feature Table Binary 长度 |
| 20 | uint32 | batchTableJsonByteLength | Batch Table JSON 长度 |
| 24 | uint32 | batchTableBinaryByteLength | Batch Table Binary 长度 |
使用 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-0x03:62 33 64 6d → "b3dm"(魔法字节)0x04-0x07:01 00 00 00 → 版本号 = 10x08-0x0B:e8 b3 00 00 → 文件总长度 = 46,056 字节(0xB3E8)0x0C-0x0F:14 00 00 00 → Feature Table JSON 长度 = 20 字节0x10-0x13:00 00 00 00 → Feature Table Binary 长度 = 00x14-0x17:00 00 00 00 → Batch Table JSON 长度 = 00x18-0x1B:00 00 00 00 → Batch Table Binary 长度 = 0Feature Table JSON(0x1C 起):
{"BATCH_LENGTH":0}
glTF 数据(0x30 起):
67 6c 54 46 → "glTF"存储每个 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]
}
存储每个 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 2.0 模型数据(GLB 二进制格式),包含:
3D Tiles 使用 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 使用 大地坐标(经纬度 + 高程):
"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
]
}
转换关系:
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;
}
bool shouldRefine(const Tile& tile, double sse, double sseThreshold) {
// SSE > 阈值:需要加载更精细的子瓦片
if (sse > sseThreshold && tile.children.size() > 0) {
return true; // 递归加载子瓦片
}
// SSE ≤ 阈值:当前瓦片足够精确
return false; // 渲染当前瓦片
}
渲染:父瓦片 + 子瓦片(共存)
应用场景:
- 地形数据(逐级增加细节)
- 植被(逐级增加密度)
示例:
Level 0: ████████ (粗略地形)
Level 1: ████████ + ▓▓▓▓▓▓▓▓ (增加细节)
渲染:仅子瓦片(父瓦片隐藏)
应用场景:
- 建筑物(完全替换低精度模型)
- 精细模型(避免重复渲染)
示例:
Level 0: ████████ (低精度)
Level 1: ▓▓▓▓▓▓▓▓ (高精度,完全替代)
位置:德国法兰克福(Frankfurt am Main)
范围:约 8.62°E - 8.71°E,50.09°N - 50.13°N
高程:134.7m - 513.3m
图层:
总瓦片数:158
{
"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)
]
}
}
特点:
ADD示例: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"
}
}
}
特点:
ADD示例: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"
}
}
}
特点:
ADD示例: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 高)
└─ ...
| 图层 | 最小 | 最大 | 平均 | 说明 |
|---|---|---|---|---|
| Landmark | 5.39m | 111.78m | ~30m | 精细模型,低误差 |
| Buildings | 19.15m | 482.34m | ~200m | 中等细节 |
| Terrain | 43.54m | 363.24m | ~180m | 地形网格 |
// 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;
}
// 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);
}
}
// 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;
}
// 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; // 保存变换矩阵
}
}
}
// 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
);
}
// 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;
}
// 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);
}
}
✅ 推荐:
❌ 避免:
.tile.json| 级别 | 几何误差 | 应用场景 |
|---|---|---|
| Level 0 | 500-1000m | 全球/区域根瓦片 |
| Level 1 | 100-500m | 城市级别 |
| Level 2 | 20-100m | 街区级别 |
| Level 3 | 5-20m | 建筑物级别 |
| Level 4 | 0-5m | 精细模型(叶子节点) |
| 内容类型 | 推荐策略 | 理由 |
|---|---|---|
| 地形 | ADD | 逐级增加分辨率,保留低精度数据 |
| 建筑物 | REPLACE | 完全替换低精度模型,避免重叠 |
| 植被 | ADD | 逐级增加密度 |
| 地标 | REPLACE | 精细模型完全替代简化版 |
// 三级缓存:
// 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;
}
// 渲染 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); // 绿色:低误差
}
A: 可能的原因:
调试:
spdlog::info("Tile SSE: {}, Threshold: {}", sse, threshold);
spdlog::info("Frustum culled: {}", !frustum.intersects(bv));
A: 优化策略:
A: 矩阵累乘(从根到叶):
世界坐标 = 根 transform × 子 transform × ... × 顶点坐标
// 代码实现
glm::dmat4 computedTransform = parentTransform;
if (!tile.transform.empty()) {
glm::dmat4 tileTransform = tileTransformToMatrix(tile.transform);
computedTransform = parentTransform * tileTransform;
}
A:
| 类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| region | 大范围地理数据 | 直观(经纬度) | 极点附近失真 |
| box | 局部模型、倾斜数据 | 任意方向、紧凑 | 需要变换矩阵 |
| sphere | 球形对象、快速剔除 | 计算简单 | 空间浪费 |