阅读视图

发现新文章,点击刷新页面。

让 AI 用自然语言操控三维地球 -- Cesium MCP 开源实践

让 AI 用自然语言操控三维地球 -- Cesium MCP 开源实践

一句"飞到埃菲尔铁塔,加个红色标记",Claude/Copilot/Cursor 就能帮你在 CesiumJS 里完成操作。这是怎么做到的?

演示效果

先看效果,了解 cesium-mcp 能做什么:

demo-full.gif

演示中通过 AI 对话完成了相机飞行、添加标记、样式修改等操作。

背景:当 GIS 遇上 AI Agent

CesiumJS 是 WebGL 三维地球可视化的事实标准。但凡涉及地理信息系统(GIS)的 Web 项目——智慧城市、数字孪生、无人机航线规划——几乎绑定 CesiumJS。

问题是:Cesium API 体量庞大,光 Viewer 就有几十个配置项,Entity 系统更是嵌套层层。新人写个"在地图上加个点"都要翻半天文档。

2024 年底 Anthropic 推出了 MCP(Model Context Protocol),让 AI 智能体能以标准化方式调用外部工具。我们顺着这条路做了一件事:

把 CesiumJS 的能力通过 MCP 协议暴露出来,让任何 AI 智能体都能用自然语言操控三维地球。

这就是 cesium-mcp

它能做什么

整体架构

graph LR
    subgraph AI["AI 智能体"]
        A1["Claude Desktop"]
        A2["VS Code Copilot"]
        A3["Cursor"]
    end

    subgraph MCP_Server["cesium-mcp-runtime<br/>(Node.js MCP Server)"]
        R1["MCP stdio 接口"]
        R2["WebSocket Server"]
    end

    subgraph Browser["浏览器"]
        B1["cesium-mcp-bridge<br/>(SDK)"]
        C1["CesiumJS Viewer<br/>三维地球"]
    end

    A1 -->|"MCP 协议<br/>(stdio)"| R1
    A2 -->|"MCP 协议<br/>(stdio)"| R1
    A3 -->|"MCP 协议<br/>(stdio)"| R1
    R1 <--> R2
    R2 <-->|"WebSocket<br/>JSON-RPC"| B1
    B1 -->|"命令执行"| C1

    style AI fill:#e8f4f8,stroke:#2196F3,stroke-width:2px
    style MCP_Server fill:#fff3e0,stroke:#FF9800,stroke-width:2px
    style Browser fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px

简单说就是三层:

  1. cesium-mcp-bridge(浏览器 SDK):嵌入你的 CesiumJS 应用,通过 WebSocket 接收命令并执行
  2. cesium-mcp-runtime(MCP Server):连接 AI 智能体和浏览器,暴露 19 个标准化工具
  3. cesium-mcp-dev(开发辅助 MCP Server):在 IDE 里让 AI 助手更懂 Cesium API

19 个工具,覆盖 GIS 核心场景

类别 工具 说明
相机 flyTo setView getView zoomToExtent 飞行定位、视角切换
图层 addGeoJsonLayer addHeatmap addMarker addLabel 数据叠加、热力图
图层管理 removeLayer setLayerVisibility listLayers updateLayerStyle 增删改查
三维数据 load3dTiles loadTerrain loadImageryService 3D Tiles、地形、影像服务
底图 setBasemap 天地图、ArcGIS、OSM 一键切换
交互 highlight screenshot 要素高亮、截图
动画 playTrajectory 沿路径播放轨迹动画

你对 AI 说"加载这个 GeoJSON,用渐变色渲染人口密度",它会自动调用 addGeoJsonLayer 并传入样式参数。

三分钟跑起来

第一步:浏览器嵌入 bridge

npm install cesium-mcp-bridge
import { CesiumMcpBridge } from 'cesium-mcp-bridge';

// viewer 是你已有的 Cesium.Viewer 实例
const bridge = new CesiumMcpBridge(viewer, { port: 9100 });
bridge.connect();

第二步:启动 MCP 运行时

npx cesium-mcp-runtime

第三步:接入 AI 智能体

以 Claude Desktop 为例,在配置文件中添加:

{
  "mcpServers": {
    "cesium": {
      "command": "npx",
      "args": ["-y", "cesium-mcp-runtime"]
    }
  }
}

VS Code Copilot 用户在 .vscode/mcp.json 中配置:

{
  "servers": {
    "cesium": {
      "command": "npx",
      "args": ["cesium-mcp-runtime"]
    }
  }
}

然后直接用自然语言下指令:

  • "飞到北京天安门,高度 1000 米"
  • "加载这个 3D Tiles 模型"
  • "画一条从上海到纽约的折线"
  • "截张图发我"

开发时也有 AI 加持

除了运行时操控,我们还做了 cesium-mcp-dev——专为 IDE AI 助手设计的 MCP 服务器:

graph LR
    subgraph IDE["IDE 环境"]
        D1["GitHub Copilot"]
        D2["Cursor AI"]
        D3["Claude Code"]
    end

    subgraph DevServer["cesium-mcp-dev<br/>(MCP Server)"]
        T1["cesium_api_lookup<br/>API 文档查询"]
        T2["cesium_code_gen<br/>代码生成"]
        T3["cesium_entity_builder<br/>Entity 构建器"]
    end

    subgraph Output["输出"]
        O1["API 签名 & 示例"]
        O2["TypeScript 代码片段"]
        O3["Entity 配置 JSON"]
    end

    D1 -->|"MCP stdio"| DevServer
    D2 -->|"MCP stdio"| DevServer
    D3 -->|"MCP stdio"| DevServer
    T1 --> O1
    T2 --> O2
    T3 --> O3

    style IDE fill:#f3e5f5,stroke:#9C27B0,stroke-width:2px
    style DevServer fill:#fff3e0,stroke:#FF9800,stroke-width:2px
    style Output fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px

提供 3 个工具:

工具 功能
cesium_api_lookup 按类名/方法查 Cesium API 文档,覆盖 Viewer、Entity、Camera 等 12 个核心类
cesium_code_gen 自然语言生 Cesium 代码,内置 11 个常见场景模板
cesium_entity_builder 交互式构建 Entity 配置,支持 8 种类型(point/polygon/model 等)

配置方式和 runtime 完全一致:

{
  "servers": {
    "cesium-dev": {
      "command": "npx",
      "args": ["cesium-mcp-dev"]
    }
  }
}

这意味着你在 VS Code 里写 Cesium 代码时,Copilot 可以直接查 API、生成代码片段、构建 Entity 配置——再也不用频繁切到文档网站。

一次操控的完整流程

以"飞到北京天安门,加个红色标记"为例,看看数据是怎么流转的:

sequenceDiagram
    participant User as 用户
    participant AI as AI 智能体
    participant RT as cesium-mcp-runtime
    participant BR as cesium-mcp-bridge
    participant CS as CesiumJS

    User->>AI: "飞到北京天安门,加个红色标记"
    AI->>AI: 理解意图,拆解为两步

    rect rgb(232, 244, 248)
    Note over AI,CS: 第一步:飞行定位
    AI->>RT: MCP tool_call: flyTo({lon:116.39, lat:39.91, h:1000})
    RT->>BR: WebSocket JSON-RPC
    BR->>CS: viewer.camera.flyTo(...)
    CS-->>BR: 飞行完成
    BR-->>RT: result: success
    RT-->>AI: tool_result: "已飞行到目标位置"
    end

    rect rgb(232, 245, 233)
    Note over AI,CS: 第二步:添加标记
    AI->>RT: MCP tool_call: addMarker({lon:116.39, lat:39.91, color:"red"})
    RT->>BR: WebSocket JSON-RPC
    BR->>CS: viewer.entities.add(...)
    CS-->>BR: entity created
    BR-->>RT: result: {id: "marker-1"}
    RT-->>AI: tool_result: "已添加红色标记"
    end

    AI-->>User: "已飞到天安门并添加了红色标记"

AI 自动将自然语言拆解为多个工具调用,每个工具走完 MCP -> WebSocket -> CesiumJS 的完整链路,结果逐级回传。用户只需要说一句话。

技术实现要点

Bridge:命令注册与执行

cesium-mcp-bridge 的核心是一个命令注册表。每个 MCP 工具对应一个命令处理器,通过 CesiumBridge.execute() 分发:

const bridge = new CesiumBridge(viewer);
// 收到 WebSocket 消息后
const result = await bridge.execute({
  action: 'flyTo',
  params: { longitude: 116.4, latitude: 39.9, height: 1000 }
});

Bridge 不关心命令从哪来——WebSocket、HTTP、甚至手动调用都行。这种解耦使得 Bridge SDK 可以独立于 MCP 使用。

Runtime:双向通信

Runtime 同时充当 MCP stdio 服务器和 WebSocket 服务器。AI 智能体通过 MCP 协议发送工具调用,Runtime 把它翻译成 JSON-RPC 命令通过 WebSocket 推给浏览器,等待执行结果后回传给 AI。

支持多会话(session),同一个 Runtime 可以连接多个浏览器页面。

版本策略

主版本号.次版本号跟踪 CesiumJS(1.139.x 对应 Cesium ~1.139.0),补丁版本独立迭代 MCP 功能。这样用户一看版本号就知道兼容哪个 Cesium。

已上架平台

平台 状态
npm Registry cesium-mcp-bridge / cesium-mcp-runtime / cesium-mcp-dev v1.139.2
MCP Official Registry io.github.gaopengbin/cesium-mcp-runtime / cesium-mcp-dev
Smithery runtime(19 tools)/ dev(3 tools)
awesome-mcp-servers PR 已提交

适用场景

  • 快速原型:用自然语言几分钟搭出 GIS 可视化 demo
  • 非开发人员:分析师、项目经理可以直接对 AI 说需求,AI 在 Cesium 上渲染结果
  • 教学演示:课堂上让学生用自然语言探索地理数据
  • 自动化流水线:CI/CD 中自动截图、自动验证地图渲染
  • 智慧城市/数字孪生:AI Agent 作为交互层,终端用户通过对话操控三维场景

参与贡献

项目完全开源(MIT),欢迎参与:

git clone https://github.com/gaopengbin/cesium-mcp.git
cd cesium-mcp
npm install
npm run build
npm test

GitHub: github.com/gaopengbin/… 官方文档: gaopengbin.github.io/cesium-mcp


如果你也在做 GIS + AI 的事情,欢迎交流。有问题直接在 GitHub Issues 提,我们会及时回复。

数字孪生大屏必看:Cesium 3D 模型选中交互,3 种高亮效果拿来就用!

接前文,3D模型加载到页面以后肯定要执行各种各样的操作,模型在大屏上的主要作用是执行相应的建筑物交互。

问题

这里需要注意3D模型的加载仍然存在一些问题。

首先是如果你使用的GLTF文件,在某些特殊情况下可能导致模型的网格加载异常,会出现无法选中的情况。

这一点我目前没发现有什么太好的解决方案,所以这里采用GLB文件规避掉了这个问题。

image.png

如果你手里只有GLTF文件,可以考虑使用 Blender 进行一下转换。

另外模型本身离地高度都是 0 的话可能存在无法选中的问题,所以这里建议背景设置离地高度为 -0.5,普通模型正常为 0

还有就是模型选中以后的显示问题。

解决方案

模型无法选中和选择错误的问题通过两个方案进行规避,一个是height离地高度,一个是pickable拾取

首先设置背景的离地高度为 -0.5,普通可以选中的模型离地高度为 0

image.png

另外设置一下 pickPriority拾取优先级pickable是否可拾取

最后就是设置模型的选中效果,这里我简单写了三种效果给大家选择,可以自行决定。

实际代码

模型添加的时候增加拾取优先级参数:

const buildingEntity = viewer.entities.add({
    id: options.id, // 唯一ID,点击交互时识别核心
    name: options.properties.name || options.id, // 建筑名称(可选)
    position: position,
    orientation: orientation, // 控制模型朝向
    pickPriority: options.pickPriority, // (核心)添加拾取优先级
    pickable: options.pickable,  // (核心)允许拾取
    model: {
        uri: options.modelUrl, // glTF/glb 模型路径
        scale: options.scale || 1.0, // 保证模型真实比例(建模时单位为米)
        minimumPixelSize: 0, // 取消最小像素限制,模型随地图缩放正常变化
        maximumScale: 20000, // 最大缩放限制
        runAnimations: false, // 静态建筑关闭动画(节省性能)
        clampToGround: true, // 贴地(自动适配地形高度,可选)
    },
    properties: options.properties || {}, // 绑定自定义属性(如状态接口)
});

这里的参数其实在模型选择那里可以再增加一层判断。

模型选中方法主要有三种:

// 1. 轮廓线方案
/**
 * 选中指定模型
 * @param {Cesium.Entity} entity 要选中的模型实体
 * @param {Function} onSelect 选中回调(如展示状态弹窗)
 * @param {Function} onUnselect 取消选中回调(用于先取消之前的选中)
 */
const selectModel = (entity, onSelect, onUnselect) => {
    // 取消之前的选中状态(包括回调执行)
    if (selectedEntity) {
        unselectModel(onUnselect);
    }

    // 校验是否为模型实体
    if (!entity || !entity.model) {
        console.warn('❌ 选中的不是模型实体');
        return;
    }

    // 标记为当前选中实体
    selectedEntity = entity;

    // 轮廓线高亮(更醒目,性能略高,需 Cesium 1.90+)
    entity.model.outlineColor = Cesium.Color.RED;
    entity.model.outlineWidth = 2;
    entity.model.outline = true;

    // 执行选中回调(绑定业务逻辑)
    if (typeof onSelect === 'function') {
        onSelect(entity);
    }
};

如果没有特殊要求,轮廓线方案其实非常简单实用。

// 2. 颜色高亮,修改模型材质
originalModelMaterial = entity.model.color || Cesium.Color.WHITE.clone();
entity.model.color = Cesium.Color.fromCssColorString('#fb0528').withAlpha(0.8);
// 强制刷新
viewer.scene.requestRender();

这里需要注意,修改模型材质一定要执行强制刷新

// 3. 模型遮罩效果
viewer.entities.add({
    position: entity.position,
    orientation: entity.orientation,
    model: {
        uri: entity.model.uri, // 复用同一个模型文件
        scale: 0.35, // 稍微大一点
        color: Cesium.Color.fromCssColorString('#409EFF').withAlpha(0.5), // 半透明蓝
        silhouetteColor: Cesium.Color.BLUE, // 可选:配合轮廓
        silhouetteSize: 2.0
    }
});

这种效果也非常不错,复用模型文件稍大一号,让他完美的遮住原始模型,给出一个透明色作为材质,显得很有科技感。

总结

模型设计的时候推荐大家优先使用 GLB 格式替代 GLTF 规避网格加载异常问题,

另外通过pickPriority(拾取优先级)和pickable(是否可拾取)参数,从逻辑层面控制模型的交互规则,彻底解决 点错模型、点不到模型 的问题。

后续增加相关图标的单击和操作,实现小型设备的交互。

解决 Cesium 网络卡顿!5 分钟加载天地图,内网也能流畅用,附完整代码

接上文,之前使用 Cesium.Ion 已经成功将地球效果展示出来了,飞入效果也非常不错。详细可以参考这篇文章:# 拿来就用!Vue3+Cesium 飞入效果封装,3D大屏多场景直接复用

但是仍然存在一个问题没解决, Cesium.Ion 的服务部署在外面,但是我们这边因为众所周知的原因网络受到一些限制。

image.png

虽然Cesium的服务是不被禁止访问的,但是访问速度和丢包率也是异常"喜人",所以之前还是打算在这个地方做一下优化。

解决思路

其实想要解决这个问题也非常的简单,将卫星地图(瓦片地图)换成我们自己的服务即可,访问咱们这边的服务是没啥问题的。

目前基本上是两个思路

  • 使用在线服务,主要是天地图、腾讯、高德等等几家
  • 使用离线服务,自己下载瓦片地图,自己搭建服务

这两种路线我都用了,可以说如果你有资源的话,那么我强烈建议你自己搭建离线地图服务,效果非常好。

最关键的是这套系统就能够实现离线部署了,在某些私有化场景下非常契合。

image.png

但是两个问题需要解决,资源存储空间

目前资源问题勉强能凑合解决一下,但是存储空间确实没有。毕竟地图下载下来也是真不小,另外目前没有离线部署的需求,所以考虑使用在线服务。

在线服务不推荐腾讯、高德几家,一来配置起来并不好整,我之前尝试腾讯的,鼓捣了半天仅仅弄好了个矢量图,卫星图花了一下午时间也没弄好。

切换到天地图,只用了5分钟就齐活了。

解决方案

使用天地图服务首先去天地图官网注册个账号,地址给大家放一下:www.tianditu.gov.cn/

首先进入控制台,选择开发管理下的开发者认证,认证一下个人开发者

只有这样才能够创建应用,生成tk

image.png

然后进入开发管理 > 应用管理 > 我的应用 > 创建新应用,简单填写一下必要信息,就能够创建一个新应用了。

复制一下应用密钥(tk)

实际代码

初始化加载 Cesium图层 的地方设置为 false

// 初始化 Cesium 地球
const initCesium = async () => {
    // 创建 Cesium 视图实例
    viewer.value = new Cesium.Viewer('cesiumContainer', {
        // 隐藏默认控件,简化界面
        timeline: false,
        animation: false,
        baseLayerPicker: false,
        geocoder: false,
        homeButton: false,
        infoBox: false,
        sceneModePicker: false,
        navigationHelpButton: false,
        // 开启深度检测,避免地形闪烁
        scene3DOnly: true,
        requestRenderMode: true,
        // 不加载默认的 Cesium Ion 影像图层
        baseLayer: false
    });

    // 隐藏 Cesium 版权信息(可选)
    viewer.value._cesiumWidget._creditContainer.style.display = 'none';

    // 等待 Cesium 完全加载完成
    await waitForCesiumFullyLoaded();
    
    // 添加天地图地图影像图层
    addTianDituImageryLayer();

    // 触发 cesiumReady 事件
    emit('cesiumReady', viewer.value);
}

天地图图层主要有两部分,一个是卫星影像底图,另一个是注记图层,当然如果不考虑名称,注记图层可以不添加。

/**
 * 添加天地图地图影像图层(卫星图 + 注记)
 */
const addTianDituImageryLayer = () => {
    if (!viewer.value) return;

    // 使用天地图卫星影像,tk (密钥)
    const webKey = '你的tk';

    // 天地图卫星影像底图
    const imgProvider = new Cesium.WebMapTileServiceImageryProvider({
        url: 'https://t{s}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&FORMAT=tiles&tk=' + webKey,
        layer: 'tdtImgBasicLayer',
        style: 'default',
        format: 'image/jpeg',
        tileMatrixSetID: 'GoogleMapsCompatible',
        maximumLevel: 18,
        minimumLevel: 1,
        subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
        credit: new Cesium.Credit('天地图'),
        // 启用 CORS
        enablePickFeatures: false
    });

    // 添加卫星影像图层
    viewer.value.imageryLayers.addImageryProvider(imgProvider);

    // 天地图注记图层(地名标注)
    const ciaProvider = new Cesium.WebMapTileServiceImageryProvider({
        url: 'https://t{s}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&FORMAT=tiles&tk=' + webKey,
        layer: 'tdtAnnoLayer',
        style: 'default',
        format: 'image/jpeg',
        tileMatrixSetID: 'GoogleMapsCompatible',
        maximumLevel: 18,
        minimumLevel: 1,
        subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
        credit: new Cesium.Credit('天地图注记'),
        // 启用 CORS
        enablePickFeatures: false
    });

    // 添加注记图层(叠加在影像之上)
    viewer.value.imageryLayers.addImageryProvider(ciaProvider);

    console.log('卫星影像加载完成!');
};

这里需要注意:记得将 enablePickFeatures 设为false,避免出现跨域问题。

总结

后续看是否有合适的项目,我会将离线地图的资源和创建方式分享给大家。

如果你的资源足够强,甚至能看到非常精细的卫星图像。

离线地图的玩法也远比在线地图要多得多,你甚至可以DIY某个地方的卫星图像,做出现实版的我的世界

另外需要注意,天地图的API调用是有限制的,详情可以参考下图。

20260309-限额.png

拿来就用!Vue3+Cesium 飞入效果封装,3D大屏多场景直接复用

最近有点儿事儿,之前的大屏项目拖了一段时间,现在打算继续开发。原本以为用熟悉的Cesium能快速搞定,没想到还是踩了几个坑,整理出来和大家分享,避免后续有人走同样的弯路。

页面地球飞入效果采用 Cesium 进行开发,一来 Cesium 作为开源的3D地理信息可视化框架,API 封装完善,开发效率高。

二来个人长期使用该框架,对其核心逻辑比较熟悉,本以为能快速落地,实际开发中却遇到了加载、时机监听、多场景复用等多个问题,逐一排查解决后,才实现了流畅的飞入效果。

实现效果

Video Project 2.gif

Cesium 地球初始化完成后,自动触发指定地点的飞入动画,相机从初始视角平滑过渡到目标经纬度对应的视角,过程流畅无卡顿、无图层闪烁。

核心问题

简单的飞入效果之前都是使用的现成的方法,从零开始遇到点问题。

实际开发中需要兼顾加载性能、时机准确性、多场景复用等,具体问题如下:

  • Cesium 加载速度问题:在线地图服务加载延迟高,弱网环境下易报错,影响用户体验;
  • Cesium 加载完成时机的监听:若监听时机不准确,会导致飞入动画触发时,地图图层、影像未加载完成,出现“空地球”或“图层闪烁”问题;
  • 多地点复用问题:大屏项目中可能需要切换多个目标地点,需对飞入逻辑进行封装,实现灵活调用。

解决方案(附完整代码+细节说明)

加载速度优化:解决在线地图加载慢的问题

我最初采用的是 Cesium 官方的 Ion 在线地图服务,毕竟无需额外配置,直接调用即可,但实际测试后发现两个致命问题:

  1. 加载延迟高:官方服务器位于海外,国内网络环境下,地图影像加载速度极慢,甚至需要10秒以上才能完全渲染,远超用户6~8秒的等待极限;

  2. 稳定性差:弱网环境下,地图会直接加载失败,控制台报错“影像图层加载超时”,导致页面无法正常展示。

在线地图服务受网络环境影响极大,若要实现生产环境的稳定运行,优先替换为本地地图服务(如天地图本地部署、GeoServer 发布的本地影像),从根源上解决加载慢、报错的问题。

由于手头暂无本地地图部署工具,本次开发暂用在线地图过渡,后续会替换为本地服务。

以下是优化后的初始化代码,增加了加载超时处理,提升弱网环境下的容错性:

// 导入 Cesium 核心模块
import * as Cesium from 'cesium'
// 引入 Cesium 样式(必须,否则控件和地球样式异常)
import 'cesium/Build/Cesium/Widgets/widgets.css'

// 初始化 Cesium 地球(增加超时处理,优化加载体验)
const initCesium = async () => {
    try {
        // 配置 Cesium Token(可从 Cesium 官网免费申请,需注册账号)
        Cesium.Ion.defaultAccessToken = '你的官网Token';

        // 创建 Cesium 视图实例,精简界面控件,提升加载速度
        viewer.value = new Cesium.Viewer('cesiumContainer', {
            // 隐藏默认控件,适配大屏简洁风格
            timeline: false, // 时间轴控件
            animation: false, // 动画控件
            baseLayerPicker: false, // 底图切换控件
            geocoder: false, // 地理编码控件(搜索地点)
            homeButton: false, // 首页按钮
            infoBox: false, // 信息弹窗(点击要素时显示)
            sceneModePicker: false, // 场景模式切换(2D/3D/哥伦布视图)
            navigationHelpButton: false, // 导航帮助按钮
            // 性能优化配置
            scene3DOnly: true, // 仅开启3D模式,减少2D渲染开销
            requestRenderMode: true, // 开启请求渲染模式,降低CPU占用
            maximumRenderTimeChange: 1 / 60, // 控制渲染帧率,避免卡顿
            // 开启地形(如果不需要地形展示,可注释,进一步提升加载速度)
            // terrainProvider: Cesium.createWorldTerrain()
        });

        // 隐藏 Cesium 底部版权信息(可选,根据项目需求调整)
        viewer.value._cesiumWidget._creditContainer.style.display = 'none';

        // 等待 Cesium 完全加载完成(包括影像图层、场景渲染)
        await waitForCesiumFullyLoaded();
        
        // 触发 cesiumReady 事件,通知外部执行飞入等后续操作
        emit('cesiumReady', viewer.value);
    } catch (error) {
        console.error('Cesium 初始化失败:', error);
        // 加载失败提示,提升用户体验
        ElMessage.error('地球加载失败,请检查网络或刷新页面重试');
    }
}

加载时机监听

这是本次开发最容易踩坑的点,在创建 viewer 实例后,直接触发飞入动画,导致动画执行时,地图影像还未加载完成,出现“相机飞向空地球”的尴尬场景。

Cesium 初始化是异步过程,创建 viewer 实例只是第一步,后续还需要加载影像图层、渲染场景、初始化相机等操作,这些操作完成后,才能确保飞入动画的流畅性。

封装两个异步方法,分别监听场景渲染就绪影像图层加载完成,只有两个条件都满足,才触发后续的飞入操作,确保时机精准。

代码如下:

/**
 * 等待 Cesium 完全加载完成(包括场景渲染和影像图层)
 * 核心逻辑:先确保场景渲染就绪,再等待影像图层加载完成,双重校验
 * @returns {Promise}
 */
const waitForCesiumFullyLoaded = () => {
    return new Promise((resolve) => {
        const checkSceneReady = () => {
            // 先检查 viewer 和 scene 是否存在(避免初始化未完成时调用)
            if (!viewer.value || !viewer.value.scene) {
                // 每50ms检查一次,避免频繁占用资源
                setTimeout(checkSceneReady, 50);
                return;
            }
            
            // 使用 postRender 事件,确保场景至少完成一帧渲染
            viewer.value.scene.postRender.addEventListener(() => {
                // 场景就绪后,再等待影像图层加载完成
                waitForImageryLoaded().then(resolve);
            }, viewer.value.scene);
        };
        checkSceneReady();
    });
}

/**
 * 等待影像图层加载完成(单独封装,便于后续扩展)
 * 核心逻辑:遍历所有影像图层,检查是否有正在加载的图块
 * @returns {Promise}
 */
const waitForImageryLoaded = () => {
    return new Promise((resolve) => {
        // 若 viewer 或 scene 不存在,直接resolve(容错处理)
        if (!viewer.value || !viewer.value.scene) {
            resolve();
            return;
        }

        const imageryLayers = viewer.value.imageryLayers;
        // 若没有影像图层,直接resolve
        if (!imageryLayers || imageryLayers.length === 0) {
            resolve();
            return;
        }

        // 循环检查所有影像图层是否加载完成
        const checkLoaded = () => {
            let allLoaded = true;
            
            for (let i = 0; i < imageryLayers.length; i++) {
                const layer = imageryLayers.get(i);
                if (layer && layer.imageryProvider) {
                    // 检查当前图层是否有正在加载的图块(_loading 为Cesium内部属性)
                    if (layer._loading) {
                        allLoaded = false;
                        break;
                    }
                }
            }

            if (allLoaded) {
                // 确保影像加载完成后,场景再渲染一帧,避免闪烁
                viewer.value.scene.postRender.addEventListener(() => {
                    resolve();
                }, viewer.value.scene);
            } else {
                // 每100ms检查一次,平衡性能和准确性
                setTimeout(checkLoaded, 100);
            }
        };

        checkLoaded();
    });
}

关键注意点:将两个方法拆分开写,是为了后续扩展——比如项目中需要添加3D模型、矢量数据加载。

可直接在 waitForCesiumFullyLoaded 方法中添加对应的等待逻辑,无需大幅修改代码,提升可维护性。

封装飞入方法

大屏项目中,往往需要切换多个目标地点(如从全国视角飞入各省、从省视角飞入各市),若每次切换都重复编写代码冗余。

因此,简单封装一个通用的飞入方法。

/**
 * 控制 Cesium 相机飞往指定目标地点(通用封装,支持多场景复用)
 * @param {Object} options - 飞行配置项(必传参数标注,可选参数有默认值)
 * @param {Number} options.longitude - 目标经度(必传,如北京:116.4074)
 * @param {Number} options.latitude - 目标纬度(必传,如北京:39.9042)
 * @param {Number} options.height - 目标高度 (米,必传,根据场景调整,如大屏常用5000米)
 * @param {Number} [options.duration=3] - 飞行时长 (秒,可选,默认3秒,兼顾流畅度和效率)
 * @param {Number} [options.heading=0] - 相机朝向 (角度,可选,0 为正北,可根据需求调整)
 * @param {Number} [options.pitch=-60] - 俯仰角 (角度,可选,-90 为垂直向下,-60为常用视角)
 * @param {Function} [options.onComplete] - 飞行完成回调(可选,如飞行结束后加载区域数据)
 * @param {Function} [options.onCancel] - 飞行取消回调(可选,如用户手动中断飞行时的处理)
 */
const flyToLocation = async (options) => {
    // 校验 viewer 实例是否存在,避免报错
    if (!viewer.value) {
        console.warn('Viewer 实例不存在,无法执行飞行操作');
        ElMessage.warning('地球未加载完成,无法执行飞入操作');
        return;
    }

    // 解构配置项,设置默认值
    const {
        longitude,
        latitude,
        height,
        duration = 3,
        heading = 0,
        pitch = -60,
        onComplete,
        onCancel
    } = options

    // 由于 cesiumReady 触发时已确保影像加载完成,这里直接执行飞行
    viewer.value.camera.flyTo({
        // 将经纬度、高度转换为 Cesium 支持的笛卡尔坐标系
        destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
        // 相机朝向配置(heading:方位角,pitch:俯仰角,roll:翻滚角)
        orientation: {
            heading: Cesium.Math.toRadians(heading), // 角度转弧度(Cesium 内部使用弧度)
            pitch: Cesium.Math.toRadians(pitch),
            roll: 0.0 // 翻滚角,默认0,无需调整
        },
        duration: duration, // 飞行时长
        complete: () => {
            console.log('已飞到目标地点!');
            // 执行完成回调(若有)
            if (onComplete) onComplete();
        },
        cancel: () => {
            console.log('飞行被取消!');
            // 执行取消回调(若有)
            if (onCancel) onCancel();
        },
        canInterrupt: true // 允许用户手动中断飞行(如鼠标拖拽相机)
    })
}

注意:项目使用 Vue3 + setup 语法,需通过 defineExposeflyToLocation 方法导出,外部组件才能调用。

总结

Cesium 作为成熟的3D地理可视化框架,本身的 API 封装已经非常完善,实现飞入效果的核心逻辑并不复杂。

但实际开发中,往往是细节问题导致踩坑,总结几点关键经验,供大家参考:

  1. 加载优化优先选本地地图:生产环境中,务必替换掉官方在线地图,改用本地部署的地图服务(天地图、高德地图本地切片等),彻底解决加载慢、报错的问题;

  2. 加载时机监听不能省:不要省略 waitForCesiumFullyLoaded 方法,否则会出现图层闪烁、空地球等问题,拆分方法便于后续扩展;

  3. 封装逻辑提升复用性:多地点切换场景,一定要封装通用的飞入方法,明确配置项的必传/可选,增加容错处理,减少代码冗余;

  4. 内存管理要注意:页面卸载时,务必销毁 Cesium 实例(包括 viewer事件监听等),避免内存泄漏,导致页面卡顿、崩溃,销毁代码示例如下:

// 页面卸载时销毁 Cesium 实例(Vue3 onUnmounted 中调用)
onUnmounted(() => {
    if (viewer.value) {
        // 销毁 viewer 实例,释放内存
        viewer.value.destroy();
        viewer.value = null;
    }
});

最后,Cesium 的坑大多集中在“加载时机”和“性能优化”上,只要理清初始化流程、做好细节校验,就能快速实现流畅的交互效果。

后续我会继续更新这个大屏项目中 Cesium 的其他坑点,欢迎大家留言交流,共勉!

Cesium-气象要素PNG色斑图叠加

1、PNG色斑图渲染原理

  • PNG的R通道存储要素的像素值0-255
  • 构建256色查找表,将颜色查找表转换为cesium可用纹理
  • ceaium渲染中纹理采样选择最邻近采样,边界清晰

2、实现步骤

(1)生成256色查找表,使用反归一化,将像素值转为实际值

反归一化公式: 实际值 = 归一化值 * (最大值 - 最小值)+ 最小值

映射过程 (i / 255)将像素索引归一化到[0, 1]区间 如i= 128 -> 128/255 ≈ 0.5

代码的实现

/**
 * 生成256色查找表
 * @param {Object} params - 参数
 * @param {string} params.colorInfo - 色卡信息,如 "[[255,0,0],[0,255,0],[0,0,255]]"
 * @param {string} params.colorLevel - 色阶,如 "-10,0,10,20,30,40"
 * @param {number} params.minValue - 数据最小值
 * @param {number} params.maxValue - 数据最大值
 * @returns {Uint8Array} 颜色查找表 (256x4 RGBA)
 */
generateColorLUT({colorInfo, colorLevel, minValue, maxValue}) {

    // 解析色卡
    let colors = colorInfo.split('],').map(ele => ele.trim()).map(ele => ele.replace('[', '').replace(']', ''));
    colors = colors.map(ele => ele.split(',').map(Number));

    // 解析色阶
    let levels = colorLevel.split(',').map(Number);

    // 构建颜色区间
    const colorRange = []
    for (let i = 0; i < levels.length - 1; i++) {
        colorRange.push({
            min: levels[i],
            max: levels[i + 1],
            color: colors[i] || colors[i - 1],
        });
    }

    // 生成256色查询表(RGBA格式)
    let lut = new Uint8Array(256 * 4); // 256个颜色,每个RGBA 4个字节

    for (let i = 0; i < 256; i++) {
        const idx = i * 4

        // 索引0为透明背景
        if (i === 0) {
            lut[idx] = 0;      // R
            lut[idx + 1] = 0;  // G
            lut[idx + 2] = 0;  // B
            lut[idx + 3] = 0;  // A (透明)
            continue;
        }

        // 将像素索引(0-255)反归一化为实际值
        const tempValue = (i / 255) * (maxValue - minValue) + minValue;

        // 查找对应的颜色区间
        let selectedColor = colors[colors.length -1]; // 默认最大值颜色

        for (let j = 0; j < colorRange.length; j++) {
            const range = colorRange[j];

            if (tempValue >= range.min && tempValue < range.max) {
                selectedColor = range.color
                break;
            }

        }

        // 设置颜色 (完全不透明)
        lut[idx] = selectedColor[0] // R
        lut[idx + 1] = selectedColor[1] // G
        lut[idx + 2] = selectedColor[2] // B
        lut[idx + 3] = 255 // A (不透明)

    }

    return lut;

}
(2)将颜色查找表转换为cesium可用纹理 Uint8Array -> HTMLCanvasElement
/**
 * 从数组创建纹理
 * @param {Uint8Array} data - RGBA数据
 * @param {number} width - 宽度
 * @param {number} height - 高度
 * @returns {HTMLCanvasElement} Canvas纹理
 */
createTextureFromArray(data, width, height) {
    // 创建canvas
    const canvas = document.createElement('canvas');
    canvas.width = width; // 通常是256
    canvas.height = height; // 通常是1

    // 获取2D绘图上下文
    const ctx = canvas.getContext('2d');

    // 创建空的ImageData对象--ImageData是canvas原生支持的像素格式数据
    const imageData = ctx.createImageData(width, height);
    // imageData.data 是一个Unit8ClampedArray,长度为width * height * 4

    // 复制数据--将Uint8Array数据复制到ImageData中
    for (let i = 0; i < data.length; i++) {
        imageData.data[i] = data[i];
    }

    // 将ImageData绘制到Canvas上
    ctx.putImageData(imageData, 0, 0);

    return canvas;
}

输出为

image.png

(3)加载图片数据
/**
 * 加载图片
 * @param {string} url - 图片URL或者
 * @returns {Promise<HTMLImageElement>}
 */
loadImage(url) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.crossOrigin = 'anonymous'; // 设置跨域属性
        img.onload = () => resolve(img);
        img.onerror = reject;
        img.src = url;
    })
}
(4)创建材质

最近邻采样 (Nearest Neighbor):边界锐利,使用四舍五入

// GLSL实现
float index = pixel.r * 255.0;
float roundedIndex = floor(index + 0.5);  // 四舍五入到最近整数
float texCoord = (roundedIndex + 0.5) / 256.0;
vec4 color = texture2D(colorLUT, vec2(texCoord, 0.5));

image.png

在着色器中使用最邻近采样(另一篇文章有解释最邻近采样和线性插值,有兴趣的可以看一下)

/**
 * 获取着色器源码
 * @returns {string} GLSL着色器代码
 */
getShaderSource() {
    return `
  uniform sampler2D dataTexture;    // 数据纹理 (PNG, R通道存储温度值)
  uniform sampler2D colorLUT;        // 颜色查找表 (256x1)
  uniform float minTemp;             // 温度最小值
  uniform float maxTemp;             // 温度最大值
  uniform float opacity;             // 透明度
  uniform float visible;              // 可见性
  
  czm_material czm_getMaterial(czm_materialInput materialInput) {
    czm_material material = czm_getDefaultMaterial(materialInput);
    
    // 如果不可见,返回透明
    if (visible < 0.5) {
      material.alpha = 0.0;
      return material;
    }
    
    // 采样数据纹理
    vec4 pixel = texture(dataTexture, materialInput.st);
    
    // 透明像素直接返回
    if (pixel.a < 0.01) {
      material.alpha = 0.0;
      return material;
    }
    
    // 【关键】最近邻采样 - 避免颜色混合
    // pixel.r 范围 0.0-1.0,乘以255得到0-255的索引
    float index = pixel.r * 255.0;
    // 四舍五入到最近的整数索引
    float roundedIndex = floor(index + 0.5);
    // 转换为纹理坐标 (0-1),偏移0.5到像素中心
    float texCoord = (roundedIndex + 0.5) / 256.0;
    
    // 从查找表获取颜色
    vec4 color = texture(colorLUT, vec2(texCoord, 0.5));
    
    // 计算实际温度值 (用于调试,但不影响渲染)
    // float temperature = roundedIndex / 255.0 * (maxTemp - minTemp) + minTemp;
    
    material.diffuse = czm_gammaCorrect(color.rgb);
    material.alpha = color.a * opacity;
    
    return material;
  }
`;
}

创建材质

//创建材质
this.material = new Cesium.Material({
    fabric: {
        type: 'material',
        uniforms: {
            dataTexture: dataImage,
            colorLUT: lutTexture,
            minValue: minValue,
            maxValue: maxValue,
            opacity: opacity / 100,
            visible: 1.0
        },
        source: this.getShaderSource()
    }
});
(5)Cesium中创建一个矩形几何体并将其添加到3D场景中
// 创建矩形范围-   extend数组包含4个值:[西经, 南纬, 东经, 北纬]
const rectangle = Cesium.Rectangle.fromDegrees(
    extend[0], extend[1], extend[2], extend[3]
);

// 创建几何体
const geometry = new Cesium.RectangleGeometry({
    rectangle: rectangle,
    vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
    height: 0
});

// 创建几何体实例
const instance = new Cesium.GeometryInstance({
    id: id,
    geometry: geometry
});

// 创建外观
const appearance = new Cesium.EllipsoidSurfaceAppearance({
    material: this.material,
    translucent: true,
    flat: true,
    aboveGround: true
});

// 创建Primitive(使用普通Primitive替代GroundPrimitive,避免地形细分产生的接缝,我页面上用了地形,会出现接缝)
this.layer = new Cesium.Primitive({
    geometryInstances: instance,
    appearance: appearance,
    asynchronous: false
});

// 添加到场景
this.viewer.scene.primitives.add(this.layer);

Primitive与GroundPrimitive的区别 image.png

3、最终实现效果:

image.png

4、所有代码

StaticLayer.js

import * as Cesium from "cesium";

class StaticLayers {

    constructor(viewer) {
        this.viewer = viewer;
        this.imageLayers = [];
        this.layer = null;
        this.isVisible = false;

    }

    /**
     * 生成256色查找表
     * @param {Object} params - 参数
     * @param {string} params.colorInfo - 色卡信息,如 "[[255,0,0],[0,255,0],[0,0,255]]"
     * @param {string} params.colorLevel - 色阶,如 "-10,0,10,20,30,40"
     * @param {number} params.minValue - 数据最小值
     * @param {number} params.maxValue - 数据最大值
     * @returns {Uint8Array} 颜色查找表 (256x4 RGBA)
     */
    generateColorLUT({colorInfo, colorLevel, minValue, maxValue}) {

        // 解析色卡
        let colors = colorInfo.split('],').map(ele => ele.trim()).map(ele => ele.replace('[', '').replace(']', ''));
        colors = colors.map(ele => ele.split(',').map(Number));

        // 解析色阶
        let levels = colorLevel.split(',').map(Number);

        // 构建颜色区间
        const colorRange = []
        for (let i = 0; i < levels.length - 1; i++) {
            colorRange.push({
                min: levels[i],
                max: levels[i + 1],
                color: colors[i] || colors[i - 1],
            });
        }

        console.log('colorRange--', colorRange)

        // 生成256色查询表(RGBA格式)
        let lut = new Uint8Array(256 * 4); // 256个颜色,每个RGBA 4个字节

        for (let i = 0; i < 256; i++) {
            const idx = i * 4

            // 索引0为透明背景
            if (i === 0) {
                lut[idx] = 0;      // R
                lut[idx + 1] = 0;  // G
                lut[idx + 2] = 0;  // B
                lut[idx + 3] = 0;  // A (透明)
                continue;
            }

            // 将像素索引(0-255)反归一化为实际值
            const tempValue = (i / 255) * (maxValue - minValue) + minValue;

            // 查找对应的颜色区间
            let selectedColor = colors[colors.length -1]; // 默认最大值颜色

            for (let j = 0; j < colorRange.length; j++) {
                const range = colorRange[j];

                if (tempValue >= range.min && tempValue < range.max) {
                    selectedColor = range.color
                    break;
                }

            }

            // 设置颜色 (完全不透明)
            lut[idx] = selectedColor[0] // R
            lut[idx + 1] = selectedColor[1] // G
            lut[idx + 2] = selectedColor[2] // B
            lut[idx + 3] = 255 // A (不透明)

        }

        return lut;

    }

    /**
     * 从数组创建纹理
     * @param {Uint8Array} data - RGBA数据
     * @param {number} width - 宽度
     * @param {number} height - 高度
     * @returns {HTMLCanvasElement} Canvas纹理
     */
    createTextureFromArray(data, width, height) {
        // 创建canvas
        const canvas = document.createElement('canvas');
        canvas.width = width; // 通常是256
        canvas.height = height; // 通常是1

        // 获取2D绘图上下文
        const ctx = canvas.getContext('2d');

        // 创建空的ImageData对象--ImageData是canvas原生支持的像素格式数据
        const imageData = ctx.createImageData(width, height);
        // imageData.data 是一个Unit8ClampedArray,长度为width * height * 4

        // 复制数据--将Uint8Array数据复制到ImageData中
        for (let i = 0; i < data.length; i++) {
            imageData.data[i] = data[i];
        }

        // 将ImageData绘制到Canvas上
        ctx.putImageData(imageData, 0, 0);

        return canvas;
    }

    /**
     * 加载图片
     * @param {string} url - 图片URL或者
     * @returns {Promise<HTMLImageElement>}
     */
    loadImage(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'anonymous'; // 设置跨域属性
            img.onload = () => resolve(img);
            img.onerror = reject;
            img.src = url;
        })
    }

    /**
     * 获取着色器源码
     * @returns {string} GLSL着色器代码
     */
    getShaderSource() {
        return `
      uniform sampler2D dataTexture;    // 数据纹理 (PNG, R通道存储温度值)
      uniform sampler2D colorLUT;        // 颜色查找表 (256x1)
      uniform float minTemp;             // 温度最小值
      uniform float maxTemp;             // 温度最大值
      uniform float opacity;             // 透明度
      uniform float visible;              // 可见性
      
      czm_material czm_getMaterial(czm_materialInput materialInput) {
        czm_material material = czm_getDefaultMaterial(materialInput);
        
        // 如果不可见,返回透明
        if (visible < 0.5) {
          material.alpha = 0.0;
          return material;
        }
        
        // 采样数据纹理
        vec4 pixel = texture(dataTexture, materialInput.st);
        
        // 透明像素直接返回
        if (pixel.a < 0.01) {
          material.alpha = 0.0;
          return material;
        }
        
        // 【关键】最近邻采样 - 避免颜色混合
        // pixel.r 范围 0.0-1.0,乘以255得到0-255的索引
        float index = pixel.r * 255.0;
        // 四舍五入到最近的整数索引
        float roundedIndex = floor(index + 0.5);
        // 转换为纹理坐标 (0-1),偏移0.5到像素中心
        float texCoord = (roundedIndex + 0.5) / 256.0;
        
        // 从查找表获取颜色
        vec4 color = texture(colorLUT, vec2(texCoord, 0.5));
        
        // 计算实际温度值 (用于调试,但不影响渲染)
        // float temperature = roundedIndex / 255.0 * (maxTemp - minTemp) + minTemp;
        
        material.diffuse = czm_gammaCorrect(color.rgb);
        material.alpha = color.a * opacity;
        
        return material;
      }
    `;
    }


    /**
     * 添加色斑图
     * @param {Object} options - 配置选项
     * @param {string} options.data - base64 (R通道存储温度值)
     * @param {Array<number>} options.extend - 地理范围 [西经, 南纬, 东经, 北纬]
     * @param {number} options.min_value - 最小值
     * @param {number} options.max_value - 最大值
     * @param {string} options.colorParams - 色卡信息
     * @param {number} options.opacity - 透明度 0-100,默认100
     * @param {string} options.id - 图层ID(可选)
     * @returns {Promise<Cesium.GroundPrimitive>} 返回创建的Primitive
     */
    async addLayer(options) {
        const {
            data,
            extend,
            min_value,
            max_value,
            colorParams,
            opacity = 100,
            id = 'temperature-layer'
        } = options;

        const minValue =  Number(min_value);
        const maxValue =  Number(max_value);

        const {
            colorInfo,
            colorLevel,
            dataType,
            code,
            elementName
        } = colorParams

        // 生成颜色查找表
        const lutData = this.generateColorLUT({colorInfo, colorLevel, minValue, maxValue});

        // 创建查找表纹理(256 * 1)--目的将颜色查找表数据转换为cesium可用纹理 unit8Array --> HTMLCanvasElement
        const lutTexture = this.createTextureFromArray(lutData, 256, 1)

        // 加载图片数据
        const dataImage = await this.loadImage(data);

        // 4. 创建材质
        this.material = new Cesium.Material({
            fabric: {
                type: 'material',
                uniforms: {
                    dataTexture: dataImage,
                    colorLUT: lutTexture,
                    minValue: minValue,
                    maxValue: maxValue,
                    opacity: opacity / 100,
                    visible: 1.0
                },
                source: this.getShaderSource()
            }
        });

        // 5. 创建几何体
        const rectangle = Cesium.Rectangle.fromDegrees(
            extend[0], extend[1], extend[2], extend[3]
        );

        const geometry = new Cesium.RectangleGeometry({
            rectangle: rectangle,
            vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
            height: 0
        });

        const instance = new Cesium.GeometryInstance({
            id: id,
            geometry: geometry
        });

        const appearance = new Cesium.EllipsoidSurfaceAppearance({
            material: this.material,
            translucent: true,
            flat: true,
            aboveGround: true
        });

        // 6. 创建Primitive(使用普通Primitive替代GroundPrimitive,避免地形细分产生的接缝)
        this.layer = new Cesium.Primitive({
            geometryInstances: instance,
            appearance: appearance,
            asynchronous: false
        });

        // 7. 添加到场景
        this.viewer.scene.primitives.add(this.layer);
        this.isVisible = true;

        return this.layer;



    }

    /**
     * 显示图层
     */
    showLayer() {
        if (this.layer) {
            this.layer.show = true
            if (this.material?.uniforms) {
                this.material.uniforms.visible = 1.0;
            }
            this.isVisible = true;
        }
    }

    /**
     * 隐藏图层
     */
    hideLayer() {
        if (this.layer) {
            this.layer.show = false;
            if (this.material?.uniforms) {
                this.material.uniforms.visible = 0.0;
            }
            this.isVisible = false;
        }
    }




    
}

export default StaticLayers;

使用方法:

let baseMap;
let baseViewer;
let staticLayers;
onMounted(() => {
  baseMap= new BaseMap('cesiumContainer');
  baseViewer = baseMap.getViewer()
  staticLayers = new StaticLayers(baseViewer);

  // 隐藏logo
  baseViewer.cesiumWidget.creditContainer.style.display = 'none';

});

const createLayer = () => {
  const extend = [spotTempData.min_lon, spotTempData.min_lat, spotTempData.max_lon, spotTempData.max_lat];

  const staticOptions = {
    ...spotTempData,
    extend,
    colorParams: {...colorList}
  }

  staticLayers.addLayer(staticOptions)
}
❌