Cesium绘图工具Ellipse实战:交互绘制与批量渲染全解析
2026/9/4 4:22:08 网站建设 项目流程

Cesium 的三维场景里,Ellipse(椭圆)不是“画个图形”这么简单,它更多承担的是范围圈选、缓冲区分析、雷达/信号覆盖示意、园区安全区域标绘这类具体业务。很多开发者第一次接触 Cesium 时,会直接用viewer.entities.add把椭圆当成普通对象添加,结果发现交互绘制、参数修改、批量渲染、鼠标拾取这些环节全都要单独处理。那这次就把“Cesium 绘图工具 - Ellipse”完整拆开,讲清楚两种实现方式、交互绘制怎么封装、批量调用怎么做,以及本地验证和排错思路。

先说结论:Cesium 绘制椭圆可以分为 Entity 模式和 Primitive/Geometry 模式。Entity 模式方便调试、支持动态更新,适合中小规模交互业务;Primitive 模式更适合大量静态椭圆批量渲染。如果你只是做一个“鼠标点两点生成圆圈/椭圆圈”的工具,用 Entity + CallbackProperty 已经完全够用;如果后续要加载几千上万个椭圆,就要切到EllipseGeometry+GeometryInstance的批处理方案。

本文不讨论复杂的三维建模,也不会引入额外的大型框架,核心围绕 Cesium 本身的ellipseAPI 和EllipseGeometry展开。适合正在做“Cesium 绘图工具”功能模块、对交互式标绘、批量生成椭圆有需求的 WebGIS 开发者阅读。

1. Cesium 绘图工具 Ellipse 核心能力速览

先把项目能做什么、依赖什么环境列清楚,这样你可以快速判断这个技术方案是否符合手头的业务场景。

能力项说明
技术栈CesiumJS 前端三维地球渲染
主要功能椭圆面、圆面、椭圆边界、椭圆拉伸体、批量椭圆绘制
底层 APIEntity API 的ellipse、Primitive API 的EllipseGeometry
坐标单位半轴长度单位固定为“米”
交互绘制通过ScreenSpaceEventHandler监听点击和鼠标移动实现
平台支持PC 浏览器、移动端浏览器(需具备 WebGL 支持)
部署方式npm 工程 / 静态页面 CDN 引用
是否支持批量任务支持,可通过数组循环,也可通过GeometryInstance合并渲染
是否需要大量显存否,前端 WebGL 渲染,性能瓶颈主要在浏览器 GPU 和实体数量
适合场景地图范围圈选、雷达覆盖区、安全区域标绘、缓冲分析可视化

注意:文中代码均基于 CesiumJS 的常规 API 语法。实际版本之间可能有个别属性名称的差异,建议以你当前安装的 Cesium 版本自带类型定义为准。如果你使用 Vue3 + Vite,直接把核心逻辑放入组件生命周期即可。

2. 适用场景与使用边界

Ellipse 不是只能画“一个扁的圆”那么简单。从工程角度来说,它可以承担的任务主要有:

  • 地图范围圈选:比如在三维场景中框选某个区域,然后交给后端的空间查询接口做分析。
  • 缓冲区示意:以某个设备点为中心,显示覆盖半径圆。
  • 雷达或信号覆盖:用半透明的椭圆表示覆盖方向,配合rotation旋转参数,可以模拟定向天线或雷达波束范围。
  • 区域规划显示:在建筑、园区、管线节点上方标注规划区域。
  • 数据可视化:将后端返回的多个椭圆配置批量渲染成图层。

做一个椭圆绘制工具时,你还要考虑“它不擅长什么”。比如:

  • 椭圆的边界是数学曲线,不是复杂多边形,不能表达不规则的行政区边界。
  • 半长轴和半短轴单位是米,如果需求方给的是经纬度跨度,需要先换算成米。
  • Entity 模式数量过多时,比如超过几百上千个,性能会明显下降;此时要考虑 Primitive 模式。
  • 直接处理海量动态更新时,简单用viewer.entities.removeAll()再重建会产生卡顿。

另外一个容易被忽视的问题是合规边界。地图底图、地理数据、标绘区域都可能涉及第三方版权、测绘资质和隐私问题。技术方案只能在业务自身授权范围内使用,尤其在做人员位置、私人住宅等敏感区域圈选时,要确保数据来源合法,结果不用于侵犯隐私的用途。这篇文章只讨论 Cesium 前端开发方法,不讨论如何获取或绕行任何数据授权限制。

3. 环境准备与 Cesium 接入

一个完整的 Cesium 椭圆绘图功能,最少需要以下环境:

  • Node.js 16 以上,npm 或 pnpm。
  • 一个能运行 WebGL 的现代浏览器,Chrome / Edge 均可。
  • CesiumJS 包,建议用 npm 管理。
  • 可选:Cesium ion 的 Token,或者自己配置在线影像/地形服务作为底图。

如果你不想用 Vue,直接用纯 HTML 页面也可以。这里以 Vite 工程为例:

npm create vite@latest cesium-ellipse-demo -- --template vanilla cd cesium-ellipse-demo npm install npm install cesium npm run dev

工程创建后,在index.html里放一个地图容器:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Cesium Ellipse Drawing Tool</title> <style> html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; } </style> </head> <body> <div id="cesiumContainer"></div> <script type="module" src="/main.js"></script> </body> </html>

main.js中初始化 Cesium Viewer:

import * as Cesium from 'cesium'; import 'cesium/Build/CesiumUnminified/Widgets/widgets.css'; // 如果你有 Cesium ion Token,在这里配置 // Cesium.Ion.defaultAccessToken = 'your token'; const viewer = new Cesium.Viewer('cesiumContainer', { animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, fullscreenButton: false, infoBox: false, selectionIndicator: false }); // 如果要使用地形,可以在这里配置地形 Provider viewer.scene.globe.enableLighting = true;

如果不需要在线底图,也可以给 Viewer 指定一个可访问的地图服务地址。不同版本对imageryProvider的写法有些差异,更稳妥的方式是先把 Viewer 默认初始化和底图加载跑通,再继续后面的业务逻辑。

4. Entity API 绘制静态 Ellipse 参数详解

先看最简单的方式:直接往场景里添加一个椭圆。

const ellipseEntity = viewer.entities.add({ id: 'demo-ellipse', name: '示例椭圆', position: Cesium.Cartesian3.fromDegrees(113.5, 22.7, 0), ellipse: { semiMajorAxis: 5000, semiMinorAxis: 3000, height: 0, material: Cesium.Color.fromCssColorString('#00a8ff').withAlpha(0.35), outline: true, outlineColor: Cesium.Color.WHITE, rotation: Cesium.Math.toRadians(30), stRotation: Cesium.Math.toRadians(0) } }); viewer.flyTo(ellipseEntity);

这段代码的关键参数解释如下:

参数作用注意事项
semiMajorAxis椭圆半长轴单位米,和经纬度坐标本身没有直接换算关系
semiMinorAxis椭圆半短轴单位米,当它等于半长轴时,椭圆退化为圆
height椭圆所处高度相对于椭球体表面的高度,默认 0
extrudedHeight椭圆拉伸高度设置后椭圆会变成圆柱体/椭圆体侧表面
material椭圆面材质支持纯色、图片、渐变等多种方式
outline是否显示边界线需要配合outlineColor使用
rotation椭圆整体旋转弧度制,0 表示不旋转,数值从正北方向开始计算
stRotation纹理旋转通常不设置,默认 0
granularity弧度细粒度数值越小,椭圆弧线段越平滑,同时顶点越多

这里补充一个重要关系:在使用rotation时,角度的含义是绕椭圆的中心点旋转,而不是绕世界坐标轴旋转。也就是说,同一个椭圆放置在不同的经纬度上,同样设置的rotation会让它围绕各自本地坐标系旋转,这一点在地图大范围拼接时特别有用。

如果业务需要“空心椭圆”,可以设置fill: false,只保留边界:

viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(113.5, 22.7), ellipse: { semiMajorAxis: 8000, semiMinorAxis: 4000, fill: false, outline: true, outlineColor: Cesium.Color.YELLOW, outlineWidth: 2 } });

注意:outlineWidth在部分平台和显卡驱动下不一定生效,WebGL 对线宽的限制比较严格,这也是很多开发者遇到“细线设置无效”的原因。

5. 高精度静态渲染:EllipseGeometry + Primitive

Entity 很好用,但它为了维护“动态属性”,内部承载了较多开销。如果你要渲染上千个椭圆,或者这些椭圆基本不再变化,建议改用 Primitive + Geometry。

const instance = new Cesium.GeometryInstance({ geometry: new Cesium.EllipseGeometry({ center: Cesium.Cartesian3.fromDegrees(113.5, 22.7, 0), semiMajorAxis: 5000, semiMinorAxis: 3000, height: 0, vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT }), attributes: { color: Cesium.ColorGeometryInstanceAttribute.fromColor( Cesium.Color.fromCssColorString('#00a8ff').withAlpha(0.5) ) } }); const primitive = new Cesium.Primitive({ geometryInstances: [instance], appearance: new Cesium.PerInstanceColorAppearance({ translucent: true, closed: false }) }); viewer.scene.primitives.add(primitive);

EllipseGeometry相当于底层的几何描述,它生成三角形顶点数据;GeometryInstance负责把多个几何体实例组织在一起;PerInstanceColorAppearance允许每个实例拥有独立的颜色。

这种方式的优点是多实例合并绘制。当你需要一次性添加 1000 个固定椭圆时,不要创建 1000 个Primitive,而是把 1000 个GeometryInstance全部放到一个PrimitivegeometryInstances数组里:

const instances = sites.map((item) => { return new Cesium.GeometryInstance({ geometry: new Cesium.EllipseGeometry({ center: Cesium.Cartesian3.fromDegrees(item.lng, item.lat, item.height || 0), semiMajorAxis: item.semiMajorAxis, semiMinorAxis: item.semiMinorAxis, height: item.height || 0, vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT }), attributes: { color: Cesium.ColorGeometryInstanceAttribute.fromColor( Cesium.Color.fromCssColorString(item.color).withAlpha(item.alpha || 0.4) ) } }); }); const mergedPrimitive = new Cesium.Primitive({ geometryInstances: instances, appearance: new Cesium.PerInstanceColorAppearance({ translucent: true }) }); viewer.scene.primitives.add(mergedPrimitive);

需要说明的是,Primitive 方式添加后,想直接通过修改semiMajorAxis动态更新比较麻烦。适合“数据不变,只是批量展示”的图层;如果要做交互绘制和后续二次编辑,仍然建议使用 Entity。

6. 做一个可交互绘制 Ellipse 的绘图工具

这一节解决“如何在三维地球上用鼠标绘制椭圆”的问题。整体交互流程很简单:

  1. 点击“绘制椭圆”按钮,进入绘制态。
  2. 第一次鼠标左键点击,确认椭圆中心点。
  3. 移动鼠标,动态生成临时半长轴半径,并在场景里做实时预览。
  4. 第二次点击,根据中心点和边缘点确定长半轴,再按比例生成椭圆。

先准备绘制按钮:

<div style="position: absolute; left: 20px; top: 20px; z-index: 100; display: flex; gap: 8px;"> <button id="btnEllipse">绘制椭圆</button> <button id="btnCircle">绘制圆面</button> <button id="btnCancel">取消绘制</button> </div>

核心的绘制状态对象:

const drawState = { status: 'idle', // idle | pickingCenter | pickingRadius ratio: 0.6, // 半短轴 / 半长轴比例,圆面时为 1 center: null, // 中心点 Cartesian3 radius: 0, // 半长轴长度,单位米 previewEntity: null }; const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);

在地球表面拾取点的方式,我建议优先用射线方式获取地形或椭球上的点,而不是直接用scene.pick,这样画线时不容易被已有的实体对象干扰。

function pickGlobePosition(windowPosition) { const ray = viewer.camera.getPickRay(windowPosition); if (!ray) { return undefined; } const picked = viewer.scene.globe.pick(ray, viewer.scene); if (Cesium.defined(picked)) { return picked; } return viewer.camera.pickEllipsoid(windowPosition, viewer.scene.globe.ellipsoid); }

然后绑定鼠标事件:

function startDrawing(mode) { drawState.mode = mode; drawState.ratio = mode === 'circle' ? 1 : 0.6; drawState.status = 'pickingCenter'; drawState.center = null; drawState.radius = 0; drawState.previewEntity = null; } function createPreviewEntity() { drawState.previewEntity = viewer.entities.add({ position: drawState.center, ellipse: { semiMajorAxis: new Cesium.CallbackProperty(() => drawState.radius, false), semiMinorAxis: new Cesium.CallbackProperty( () => drawState.radius * drawState.ratio, false ), material: Cesium.Color.fromCssColorString('#00a8ff').withAlpha(0.3), outline: true, outlineColor: Cesium.Color.CYAN } }); } handler.setInputAction((click) => { if (drawState.status === 'idle') { return; } const position = pickGlobePosition(click.position); if (!position) { return; } if (drawState.status === 'pickingCenter') { drawState.center = position; drawState.radius = 1; createPreviewEntity(); drawState.status = 'pickingRadius'; return; } if (drawState.status === 'pickingRadius') { finalizeEllipse(); drawState.status = 'idle'; } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); handler.setInputAction((movement) => { if (drawState.status !== 'pickingRadius') { return; } const position = pickGlobePosition(movement.endPosition); if (!position || !drawState.center) { return; } const rawDistance = Cesium.Cartesian3.distance(drawState.center, position); drawState.radius = Math.min(rawDistance, 500000); }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

最后确认生成正式的椭圆实体:

function finalizeEllipse() { if (drawState.previewEntity) { viewer.entities.remove(drawState.previewEntity); drawState.previewEntity = null; } const radius = drawState.radius; if (!radius || radius <= 0) { return; } viewer.entities.add({ position: drawState.center, ellipse: { semiMajorAxis: radius, semiMinorAxis: radius * drawState.ratio, material: Cesium.Color.fromCssColorString('#00a8ff').withAlpha(0.4), outline: true, outlineColor: Cesium.Color.WHITE, height: 0 } }); }

对应的按钮绑定代码:

document.getElementById('btnEllipse').addEventListener('click', () => startDrawing('ellipse')); document.getElementById('btnCircle').addEventListener('click', () => startDrawing('circle')); document.getElementById('btnCancel').addEventListener('click', () => { drawState.status = 'idle'; if (drawState.previewEntity) { viewer.entities.remove(drawState.previewEntity); drawState.previewEntity = null; } });

注意,上面的Cartesian3.distance是三维空间直线距离。如果设备覆盖范围较小,比如几公里以内,作为半长轴足够用。如果业务需要严格的球面距离,则需要把中心点和边缘点分别转成Cartographic,再使用测地线相关方法计算地面弧长。实际项目中更稳妥的做法是根据精度要求选择距离算法,并在代码注释里明确说明。

这里实现的绘制逻辑是“中心点 + 一个半径点”,生成的预览形态仍然是由ratio决定的椭圆。如果你希望鼠标点就是椭圆的边缘点,并且让半长轴方向跟随鼠标方向,就还需要在每次移动时根据中心点与鼠标点之间的方位角设置rotation。这个扩展在真实开发中很常见。

7. Ellipse 绘制后的参数修改与旋转角度控制

进入三维场景的椭圆实体,默认情况下不是只能看不能改。因为 Entity 的属性大多是Property,你可以通过.setValue()方法动态修改。

例如捕捉最后生成的实体,然后修改半长轴:

let lastEntity = null; function createEllipse(params) { const entity = viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(params.lng, params.lat, params.height || 0), ellipse: { semiMajorAxis: params.semiMajorAxis, semiMinorAxis: params.semiMinorAxis, rotation: Cesium.Math.toRadians(params.rotation || 0), material: Cesium.Color.fromCssColorString(params.color || '#00a8ff').withAlpha(params.alpha || 0.4), outline: true, outlineColor: Cesium.Color.WHITE } }); lastEntity = entity; return entity; } // 修改半长轴 lastEntity.ellipse.semiMajorAxis.setValue(12000); // 修改旋转角度 lastEntity.ellipse.rotation.setValue(Cesium.Math.toRadians(60)); // 改为动态变化 lastEntity.ellipse.semiMinorAxis = new Cesium.CallbackProperty(() => { const base = 8000; return base + Math.sin(Date.now() / 1000) * 2000; }, false);

这里有几个细节值得实际验证:

  • 如果你把semiMajorAxis传入的是一个固定数值,Cesium 内部会把它包装成ConstantProperty,此时可以调用setValue更新。
  • 如果你希望它随时间或业务状态变化,可以直接把它赋值为CallbackProperty
  • rotation的输入必须是弧度,如果把角度数字直接传进去,椭圆的旋转会远超出预期。
  • 快捷键 Esc 取消绘制、双击删除实体这类交互,都需要自己用ScreenSpaceEventHandler监听对应键盘和鼠标事件,Entity API 本身不提供“绘制工具”级别的封装。

对颜色透明度,建议封装一个统一样式类,这样绘制时和批量加载时的样式保持一致。比如半透明蓝色是很多地图平台默认的“可选区”状态,直接用:

const DEFAULT_ELLIPSE_STYLE = { fillColor: Cesium.Color.fromCssColorString('#00a8ff').withAlpha(0.35), strokeColor: Cesium.Color.WHITE, outline: true };

真正常用的“绘图工具”,不只是画一个面,还包括:选中后高亮、鼠标拖动边缘点改变大小、属性面板编辑、删除按钮。单篇文章无法全部铺开,但架构上建议把“几何参数”和“视图实体”分开:几何参数保存在业务数据层,视图实体只是它的可视化投影。修改时先改数据层,再同步刷新实体属性,这样后期接入后端保存和回显会容易很多。

8. 接口 API 与批量任务调用模板

在 GIS 工程里,前端很少只画一个椭圆就结束。更多场景是后端通过接口返回一批椭圆配置,或者前端把用户绘制的椭圆提交到后端。

以下是一组常用的接口请求参数:

{ "name": "设备覆盖范围", "center": { "lng": 114.06, "lat": 22.55 }, "height": 0, "semiMajorAxis": 1500, "semiMinorAxis": 900, "rotation": 30, "color": "#00a8ff", "alpha": 0.35, "outline": true }

前端请求接口并批量添加:

async function loadEllipseFromApi(url) { const res = await fetch(url); const data = await res.json(); if (!Array.isArray(data)) { console.error('接口返回格式错误,期望数组'); return; } const features = data.map((item) => { return viewer.entities.add({ name: item.name || 'ellipse-' + Math.random().toString(16).slice(2), position: Cesium.Cartesian3.fromDegrees( item.center.lng, item.center.lat, item.height || 0 ), ellipse: { semiMajorAxis: item.semiMajorAxis, semiMinorAxis: item.semiMinorAxis, rotation: Cesium.Math.toRadians(item.rotation || 0), material: Cesium.Color.fromCssColorString(item.color || '#00a8ff').withAlpha(item.alpha || 0.35), outline: !!item.outline, outlineColor: Cesium.Color.WHITE } }); }); return features; }

如果是批量加载上百个固定椭圆,建议不要使用viewer.entities.add循环,而是采用第 5 节提到的GeometryInstance合并方案,这样渲染压力会小很多。

批量删除时,注意不要把viewer.entities当作普通数组操作。正确的清理思路是维护一个图层数组,统一移除:

const ellipseEntityList = []; function clearEllipseLayer() { ellipseEntityList.forEach((entity) => { if (!entity.isDestroyed && viewer.entities.contains(entity)) { viewer.entities.remove(entity); } }); ellipseEntityList.length = 0; }

如果需要把用户画好的椭圆导出到后端,可以使用Cartographic.fromCartesian把中心点转回经纬度:

function getEllipseEntityGeoJson(entity) { const carto = Cesium.Cartographic.fromCartesian(entity.position.getValue()); const lng = Cesium.Math.toDegrees(carto.longitude); const lat = Cesium.Math.toDegrees(carto.latitude); return { type: 'Feature', properties: { semiMajorAxis: entity.ellipse.semiMajorAxis.getValue(), semiMinorAxis: entity.ellipse.semiMinorAxis.getValue(), rotation: entity.ellipse.rotation ? entity.ellipse.rotation.getValue() : 0 },

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询