ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Cesium绘图工具Ellipse实战:交互绘制与批量渲染全解析

Cesium绘图工具Ellipse实战:交互绘制与批量渲染全解析 Cesium 的三维场景里Ellipse椭圆不是“画个图形”这么简单它更多承担的是范围圈选、缓冲区分析、雷达/信号覆盖示意、园区安全区域标绘这类具体业务。很多开发者第一次接触 Cesium 时会直接用viewer.entities.add把椭圆当成普通对象添加结果发现交互绘制、参数修改、批量渲染、鼠标拾取这些环节全都要单独处理。那这次就把“Cesium 绘图工具 - Ellipse”完整拆开讲清楚两种实现方式、交互绘制怎么封装、批量调用怎么做以及本地验证和排错思路。先说结论Cesium 绘制椭圆可以分为 Entity 模式和 Primitive/Geometry 模式。Entity 模式方便调试、支持动态更新适合中小规模交互业务Primitive 模式更适合大量静态椭圆批量渲染。如果你只是做一个“鼠标点两点生成圆圈/椭圆圈”的工具用 Entity CallbackProperty 已经完全够用如果后续要加载几千上万个椭圆就要切到EllipseGeometryGeometryInstance的批处理方案。本文不讨论复杂的三维建模也不会引入额外的大型框架核心围绕 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 vitelatest cesium-ellipse-demo -- --template vanilla cd cesium-ellipse-demo npm install npm install cesium npm run dev工程创建后在index.html里放一个地图容器!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleCesium Ellipse Drawing Tool/title style html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; } /style /head body div idcesiumContainer/div script typemodule src/main.js/script /body /html在main.js中初始化 Cesium Viewerimport * 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椭圆所处高度相对于椭球体表面的高度默认 0extrudedHeight椭圆拉伸高度设置后椭圆会变成圆柱体/椭圆体侧表面material椭圆面材质支持纯色、图片、渐变等多种方式outline是否显示边界线需要配合outlineColor使用rotation椭圆整体旋转弧度制0 表示不旋转数值从正北方向开始计算stRotation纹理旋转通常不设置默认 0granularity弧度细粒度数值越小椭圆弧线段越平滑同时顶点越多这里补充一个重要关系在使用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 PrimitiveEntity 很好用但它为了维护“动态属性”内部承载了较多开销。如果你要渲染上千个椭圆或者这些椭圆基本不再变化建议改用 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全部放到一个Primitive的geometryInstances数组里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 的绘图工具这一节解决“如何在三维地球上用鼠标绘制椭圆”的问题。整体交互流程很简单点击“绘制椭圆”按钮进入绘制态。第一次鼠标左键点击确认椭圆中心点。移动鼠标动态生成临时半长轴半径并在场景里做实时预览。第二次点击根据中心点和边缘点确定长半轴再按比例生成椭圆。先准备绘制按钮div styleposition: absolute; left: 20px; top: 20px; z-index: 100; display: flex; gap: 8px; button idbtnEllipse绘制椭圆/button button idbtnCircle绘制圆面/button button idbtnCancel取消绘制/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 },
返回列表