cesium加载SEM解决模型抖动问题

Cesium 高精度渲染:转换为局部坐标系的实现方案

在 Cesium 开发中,当需要渲染大量高精度模型或远离地球原点的模型时,经常会遇到浮点精度问题,导致模型抖动、撕裂或位置偏移。这种情况下,将模型转换为局部坐标系(Local Coordinate System)是一种行之有效的解决方案。本文将详细介绍如何在 Cesium 中实现世界坐标到局部坐标的转换,以提高渲染精度和性能。

为什么需要局部坐标系?

Cesium 默认使用 ECEF(Earth-Centered, Earth-Fixed)坐标系,即地心地固坐标系。这是一个以地球中心为原点的笛卡尔坐标系。当模型距离原点很远时,会出现以下问题:

  1. 浮点精度损失:JavaScript 使用 64 位浮点数,有效精度约为 16 位。当坐标值很大时(如地球表面点的 ECEF 坐标通常为百万量级),小数部分的精度会逐渐丢失。
  2. 模型抖动:精度损失导致相机移动时,模型会出现微小的抖动或跳跃。
  3. Z-fighting:深度缓冲区精度不足,导致表面之间出现闪烁。

局部坐标系通过将模型中心设为原点,大幅减小坐标值的绝对大小,从而保留更多小数位精度,解决上述问题。

实现原理

局部坐标系的核心思想是相对于中心点(Relative to Center,简称 RTC)技术。其基本步骤如下:

  1. 选择一个参考点作为局部坐标系的原点(通常是模型的中心点)
  2. 创建从全球坐标到局部坐标的转换矩阵
  3. 将所有顶点从全球坐标转换为相对于该原点的局部坐标
  4. 在渲染时,将整个坐标系一起变换回全球位置

代码实现

1. 创建局部坐标系

首先,我们需要为模型创建一个局部坐标系:

1
2
3
const rtcCartesian3 = Cesium.Cartesian3.fromDegrees(rtcCenter[0], rtcCenter[1], rtcCenter[2]);
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(rtcCartesian3);
const modelMatrixInverse = Cesium.Matrix4.inverse(modelMatrix, new Cesium.Matrix4());

这段代码的工作原理:

  • rtcCenter是局部坐标系的中心点,使用经纬度和高度表示
  • Cesium.Cartesian3.fromDegrees将经纬度坐标转换为 ECEF 坐标
  • eastNorthUpToFixedFrame创建一个变换矩阵,该矩阵定义了一个以指定点为原点、东北天为轴向的局部坐标系
  • 计算矩阵的逆矩阵,用于将全球坐标转换为局部坐标

2. 坐标转换函数

下面是将全球坐标转换为局部坐标的辅助函数:

1
2
3
4
5
6
7
SemModel.prototype.worldToLocal = function (positionWC, result, modelMatrixInverse) {
if (!Cesium.defined(result)) {
result = new Cesium.Matrix4();
}
Cesium.Matrix4.multiplyByPoint(modelMatrixInverse, positionWC, result);
return result;
};

该函数通过将世界坐标点与逆模型矩阵相乘,将其转换为局部坐标系中的点。

3. 创建使用局部坐标的几何体

最后,我们将完整的几何体转换到局部坐标系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
SemModel.prototype.createCityGeometry = function (
coordSystem,
vertices,
vertexIds,
mergeTextureUVs,
modelMatrixInverse
) {
const localVertices = [];
vertices.forEach(vertex => {
// 将4549坐标系下的坐标转成4326下的坐标
const coord = proj4(coordSystem, "EPSG:4326", [vertex[0], vertex[1], vertex[2]]);
const position = Cesium.Cartesian3.fromDegrees(coord[0], coord[1], coord[2]);
this.worldToLocal(position, position, modelMatrixInverse);
localVertices.push(position.x, position.y, position.z);
});
const verticesArr = new Float32Array(localVertices);
const indicesArr = new Uint16Array(vertexIds);
const barycentricCoords = this.generateBarycentricCoords(verticesArr);
const geometry = new Cesium.Geometry({
attributes: {
position: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT,
componentsPerAttribute: 3,
values: verticesArr,
}),
st: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT,
componentsPerAttribute: 2,
values: mergeTextureUVs,
}),
barycentric: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT,
componentsPerAttribute: 3,
values: barycentricCoords,
}),
},
indices: indicesArr,
primitiveType: Cesium.PrimitiveType.TRIANGLES,
boundingSphere: Cesium.BoundingSphere.fromVertices(verticesArr),
});
return geometry;
};

这个函数的工作流程:

  1. 遍历所有顶点
  2. 将原始坐标(此例中为 EPSG:4549)转换为 WGS84 经纬度(EPSG:4326)
  3. 将经纬度转换为 Cesium 的 ECEF 坐标
  4. 将 ECEF 坐标转换为局部坐标
  5. 创建几何体,使用转换后的局部坐标
作者

wangyawei

发布于

2025-03-21

更新于

2026-09-11

许可协议