Cesium 裁剪平面(Clipping Plane)详解
概述
裁剪平面(Clipping Plane)是Cesium中用于裁剪3D几何体的重要功能。本文档详细介绍了在虚拟驾驶舱(Virtual Cockpit)中使用裁剪平面来裁剪视锥体(Frustum)在地表以下部分的实现原理。
应用场景
在虚拟驾驶舱中,我们需要显示相机的视锥体,但不希望视锥体穿透地表显示在地下。使用裁剪平面可以实现:
- 只显示地表以上的视锥体部分
- 提升视觉真实感
- 避免地下几何体的渲染
视锥体(完整)
/|\
/ | \
/ | \
/ | \
━━━━━━━━━━━━━━━━━ ← 地表(裁剪平面)
✂️裁剪掉这部分代码实现
完整代码
// 位置:src/common/composables/useVirtualCockpit/frustum-layer.ts
// 1. 计算相机位置投影到地表的点
const cartographic = Cartographic.fromCartesian(options.cameraPosition);
cartographic.height = 0; // 地表
const surfacePoint = Ellipsoid.WGS84.cartographicToCartesian(cartographic);
// 2. 计算地表法向量
const normal = Ellipsoid.WGS84.geodeticSurfaceNormal(
surfacePoint,
new Cartesian3(),
);
// 3. 计算平面到原点的距离
const distance = -Cartesian3.dot(normal, surfacePoint);
// 4. 创建裁剪面
const clippingPlane = new ClippingPlane(normal, distance);
const clippingPlanes = new ClippingPlaneCollection({
planes: [clippingPlane],
unionClippingRegions: true,
});
// 5. 应用到视锥体primitive
this.frustumPrimitive = new Primitive({
geometryInstances: [instanceGeo],
appearance: new PerInstanceColorAppearance({
closed: true,
flat: true,
}),
asynchronous: false,
});
(this.frustumPrimitive as any).clippingPlanes = clippingPlanes;分步详解
步骤1:计算地表投影点
const cartographic = Cartographic.fromCartesian(options.cameraPosition);
cartographic.height = 0; // 地表
const surfacePoint = Ellipsoid.WGS84.cartographicToCartesian(cartographic);目的: 获取相机位置在地表上的投影点
过程:
- 将相机位置从笛卡尔坐标(世界坐标系)转换为地理坐标(经度、纬度、高度)
- 将高度设置为0,表示地表高度
- 再将地理坐标转回笛卡尔坐标,得到地表投影点
示例:
// 假设相机在北京上空1000米
// 输入:cameraPosition = {x: -2175033, y: 4392098, z: 4071623}
// 输出:surfacePoint = {x: -2169511, y: 4385890, z: 4066234} (地表)步骤2:计算地表法向量
const normal = Ellipsoid.WGS84.geodeticSurfaceNormal(
surfacePoint,
new Cartesian3(),
);目的: 获取地表在该点的法向量(垂直于地表向上的方向)
关键点:
- 法向量是相对于**地心(世界坐标原点)**的向量
- 法向量从地心指向地表该点,延伸到天空
- 法向量是归一化的(长度为1)
坐标系说明:
Cesium使用ECEF坐标系(Earth-Centered, Earth-Fixed):
Z轴 (北极)
↑
|
|
------+------→ X轴 (本初子午线)
/|
/ |
/
↙
Y轴 (东经90°)法向量的几何意义:
天空
↑ normal (世界坐标向量)
|
|
━━━━━━━━━━━━●━━━━━━━━━━━ 地表(椭球面)
surfacePoint
|
|
○ 地心(原点)
(0, 0, 0)为什么法向量相对于地心?
- Cesium的所有计算在世界坐标系中进行
- 地球是椭球体,不同位置的"向上"方向在世界坐标系中指向不同
- 统一的坐标系便于不同对象之间的几何计算
示例:
// 北京地表点的法向量
const surfacePoint = { x: -2169511, y: 4385890, z: 4066234 };
const normal = { x: -0.337, y: 0.681, z: 0.649 }; // 归一化向量
// 验证归一化:√(x² + y² + z²) = √(0.337² + 0.681² + 0.649²) ≈ 1步骤3:计算平面方程的距离参数
const distance = -Cartesian3.dot(normal, surfacePoint);目的: 计算平面方程中的常数项 d
数学原理:
3D空间中的平面方程:
n·x + d = 0其中:
n是平面的法向量(normal)x是平面上的任意点d是常数项·表示点乘(dot product)
变换得到:
d = -n·x点乘计算:
// 点乘公式
dot(n, p) = n.x * p.x + n.y * p.y + n.z * p.z
// 具体示例
const surfacePoint = {x: -2169511, y: 4385890, z: 4066234};
const normal = {x: -0.337, y: 0.681, z: 0.649};
const dotProduct = (-0.337) * (-2169511) +
0.681 * 4385890 +
0.649 * 4066234
= 731145 + 2986791 + 2639006
= 6356942
const distance = -dotProduct = -6356942;几何意义:
distance 表示平面到坐标原点(地心)的有向距离,约为地球半径(6371km)
为什么要加负号?
在Cesium的 ClippingPlane 定义中:
- 平面方程:
n·x + d = 0 - 保留区域:
n·x + d ≥ 0的点会被保留 - 裁剪区域:
n·x + d < 0的点会被裁剪掉
加负号确保裁剪方向正确:
法向量 n (指向天空)
↑
|
━━━━━━━━━━━━━━━━━━ ← 地表平面
保留上方 (n·x + d ≥ 0)
━━━━━━━━━━━━━━━━━━
裁剪下方 (n·x + d < 0)
↓
地心 (原点)验证示例:
// 对于地表点:
n·surfacePoint + d = 6356942 + (-6356942) = 0 ✓ (在平面上)
// 对于天空中的点 (距地表1000米):
const skyPoint = {x: -2175033, y: 4392098, z: 4071623};
n·skyPoint + d = 6357942 + (-6356942) = 1000 > 0 ✓ (保留)
// 对于地下点:
const undergroundPoint = {x: -2164000, y: 4379000, z: 4061000};
n·undergroundPoint + d = 6355942 + (-6356942) = -1000 < 0 ✓ (裁剪)步骤4:创建裁剪平面
const clippingPlane = new ClippingPlane(normal, distance);
const clippingPlanes = new ClippingPlaneCollection({
planes: [clippingPlane],
unionClippingRegions: true,
});参数说明:
ClippingPlane 构造函数
typescriptnew ClippingPlane(normal: Cartesian3, distance: number)normal: 平面法向量(世界坐标系)distance: 平面方程的常数项d
ClippingPlaneCollection 配置
planes: 裁剪平面数组(可以有多个)unionClippingRegions:true: 使用并集模式(保留所有平面的"保留区域"的并集)false: 使用交集模式(保留所有平面的"保留区域"的交集)
unionClippingRegions 示例:
并集模式 (true): 交集模式 (false):
保留A ∪ 保留B 保留A ∩ 保留B
平面A 平面B 平面A 平面B
| | | |
████| ████| ████| ████|
████| ████| ████| ████|
━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━
保留|保留|保留 |保留|裁剪|裁剪
| | | |步骤5:应用裁剪平面
this.frustumPrimitive = new Primitive({
geometryInstances: [instanceGeo],
appearance: new PerInstanceColorAppearance({
closed: true,
flat: true,
}),
asynchronous: false,
});
(this.frustumPrimitive as any).clippingPlanes = clippingPlanes;关键点:
- 将裁剪平面集合应用到 Primitive 上
- 使用类型断言
as any是因为 TypeScript 定义可能不完整 - 裁剪在GPU中自动执行,性能高效
数学公式总结
平面方程
n·x + d = 0展开形式:
n.x * x + n.y * y + n.z * z + d = 0距离计算
d = -n·p其中 p 是平面上的已知点(本例中是 surfacePoint)
点到平面的有向距离
对于空间中任意点 q,其到平面的有向距离为:
dist = (n·q + d) / |n|由于 n 已归一化(|n| = 1),简化为:
dist = n·q + ddist > 0: 点在平面法向量指向的一侧(保留)dist = 0: 点在平面上dist < 0: 点在平面法向量相反的一侧(裁剪)
实际应用效果
裁剪前后对比
裁剪前:
视锥体穿透地表
/|\
/ | \
/ | \
/ | \
/ | \
/ | \
━━━━━━━━━━●━━━━━━━━ 地表
/ | \
/ | \ ← 不真实的地下部分
/________|________\裁剪后:
视锥体止于地表
/|\
/ | \
/ | \
/ | \
━━━━━━━━━━●━━━━━━━━ 地表(裁剪平面)
地下部分已被裁剪 ✂️常见问题
Q1: 为什么不使用局部坐标系?
A: Cesium的所有几何计算在统一的世界坐标系(ECEF)中进行,这样:
- 不同位置的对象可以正确交互
- 避免坐标系转换的性能开销
- 简化多对象场景的计算
Q2: 法向量为什么要归一化?
A: 归一化后的法向量(长度为1)使得:
- 点到平面的距离计算更简单:
dist = n·p + d - 避免浮点运算误差累积
- 符合数学标准定义
Q3: 可以使用多个裁剪平面吗?
A: 可以!例如同时裁剪地表和天空:
const groundPlane = new ClippingPlane(groundNormal, groundDistance);
const skyPlane = new ClippingPlane(skyNormal, skyDistance);
const clippingPlanes = new ClippingPlaneCollection({
planes: [groundPlane, skyPlane],
unionClippingRegions: false, // 交集:同时满足两个条件
});Q4: distance 的单位是什么?
A: 米(Meter),与Cesium的世界坐标系单位一致。例如 -6356942 约等于地球半径(6371km)。
性能优化建议
静态裁剪平面缓存
typescript// 如果地表裁剪平面不变,只创建一次 private static groundClippingPlanes: ClippingPlaneCollection;减少裁剪平面数量
- 每个裁剪平面都有计算开销
- 通常1-4个裁剪平面性能良好
按需启用裁剪
typescript// 只在需要时应用裁剪 if (needClipping) { primitive.clippingPlanes = clippingPlanes; }
参考资料
总结
裁剪平面是Cesium中强大的几何裁剪工具,通过理解其数学原理和坐标系统,我们可以:
- 正确创建裁剪平面
- 理解法向量和距离参数的含义
- 应用到实际项目中(如虚拟驾驶舱)
- 优化性能和视觉效果
关键要点:
- 法向量相对于地心(世界坐标系)
- 距离参数
d = -n·p定义平面位置 - 保留区域:
n·x + d ≥ 0 - 裁剪区域:
n·x + d < 0

