在 WebGL 三维可视化领域,CesiumJS 是当之无愧的王者,但其庞大的 API 和复杂的坐标系让许多开发者望而却步。EarthSDK 地球可视化二次开发框架,一套代码,实现 Cesium、UnrealEngine、OpenLayers 多引擎可视化。本文记录了基于 EarthSDK 3版本的二次开发实战过程,涵盖环境搭建、常用功能实现。
一、 搭建一个地球
1.访问官方网站下载模板
2.个人从0搭建 (vue+vite项目)
Node.js ≥ 20.x(推荐 LTS)Vue 3 + Vite 已不再支持 Node 18 及以下
包管理器 pnpm / npm / yarn / bun
推荐 pnpm(速度快、磁盘占用小)
IDE VS Code 必装插件:Vue - Official(原 Volar)
#环境检查 node -v # 输出 v20.x 或更高 pnpm -v # 或 npm -v- 项目初始化
- 安装earthsdk基础包
pnpm add earthsdk3 --save # pnpm add cesium pnpm add earthsdk3-ue --save # pnpm add cesium pnpm add earthsdk3-cesium --save #静态资源提取库 pnpm add earthsdk3-assets --save #配置一个资源copy pnpm add vite-plugin-static-copy #配置插件 pnpm add vite-plugin-cesium- 配置
main.ts
import { createApp } from 'vue' import App from './App.vue' import { ESObjectsManager } from 'earthsdk3'; import { ESCesiumViewer } from 'earthsdk3-cesium'; import { ESUeViewer } from 'earthsdk3-ue'; const objm = new ESObjectsManager(ESUeViewer, ESCesiumViewer); createApp(App,{ objm }).mount('#app') objm.sceneTree.createSceneObjectTreeItemFromJson({ "id": "ae103185-08c7-4ed0-b6d4-15ad77bbbf66", "type": "ESImageryLayer", "url": "https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", "maximumLevel": 18, "name": "全球影像", "allowPicking": true })vite.config.ts
import vue from '@vitejs/plugin-vue'; import { defineConfig, normalizePath } from 'vite'; import cesium from 'vite-plugin-cesium'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import path from 'path'; export default defineConfig({ // Use relative asset paths so dist can be hosted under subdirectories. base: './', server: { proxy: { '/pfsc-api': { target: 'https://pfsc.agri.cn', changeOrigin: true, rewrite: (p) => p.replace(/^\/pfsc-api/, '') } } }, resolve: { alias: { '@': path.resolve(__dirname, 'src'), // 配置Cesium的访问 cesium: path.resolve(__dirname, 'node_modules/cesium/Source/Cesium') } }, plugins: [ vue(), cesium(), // 运行和构建时copy viteStaticCopy({ targets: [ { src: normalizePath(path.resolve(__dirname, './node_modules/earthsdk3-assets')), dest: './js' } ] }) ] })getobjm.ts
import { ESObjectsManager } from "earthsdk3"; import { inject } from "vue"; /** * 获取ESObjectsManager * @returns {ESObjectsManager} */ export function getobjm() { const objm = inject<ESObjectsManager>('objm'); if (!objm) throw new Error('ESObjectsManager not found'); return objm; }MapContainer.vue
<script setup lang="ts"> import {getobjm} from "@/scripts/getobjm.ts"; import {nextTick, ref} from "vue"; import type { ESObjectsManager } from "earthsdk3"; const container = ref<HTMLDivElement | null>(null); const objm: ESObjectsManager = getobjm() as ESObjectsManager; console.log("objm",objm); objm.viewerCreatedEvent.don(async (viewer: any) => { viewer.clickEvent.don((e: any) => { viewer.pick(e.screenPosition, "customPick").then((res: unknown) => { console.log("全局点击事件"); }); }); }); nextTick(() => { if (!container.value) return; objm.createCesiumViewer(container.value); }) </script> <template> <div class="container" ref="container"></div> </template> <style scoped lang="scss"> </style>app.vue
<script setup lang="ts"> import MapContainer from "@/views/MapContainer.vue"; import {provide} from "vue"; const props = defineProps(['objm']); provide('objm', props.objm); </script> <template> <MapContainer /> </template> <style scoped></style>成功界面
二、 实战核心功能模块
1、引擎切换
#创建Cesium视口 const viewer = objm.createCesiumViewer(container.value); #创建UE视口 const options = { type: "ESUeViewer", container.value, options: { "https://earth3d.alink.link:30002", "earthsdk3"}, } const viewer = objm.createUeViewer(options); #引擎切换 const switchEngine = (engine) => { if (engine === 'cesium') { objm.switchToCesiumViewer({ "container": container.value }) } else { objm.switchToUEViewer({ "container": container.value, "uri": "https://earth3d.alink.link:30002", "app": "earthsdk3" }) } }2、获取ESS里场景的数据
# 获取ess登陆凭证 async function fetchEssLoginAuth() { let authorization = localStorage.getItem("essAuthorization"); if (authorization) return authorization; const url="ESS登陆地址"; //例如https://earth3d:1111/login try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: '登陆账号', password: '登陆密码' }) }); const data = await response.json(); if (data.status !== "ok") { throw new Error(`登录失败,原因:${data.status}`); } localStorage.setItem("essAuthorization", data.data); return data.data; } catch (error) { console.error('ESS认证失败:', error); throw error; } } #获取内容 async function fetchEssContent(id) { const authorization = await fetchEssLoginAuth(); const url=`https://earth3d:1111/staticscene/get?id=${id}` try { const response = await fetch(url, { method: 'GET', headers: { 'Authorization': `${authorization}` } }); const data = await response.json(); if (data.status !== "ok") { throw new Error(`获取场景内容失败,原因:${data.status}`); } const rootChildren = data?.data?.content?.sceneTree?.root?.children; if (rootChildren && Array.isArray(rootChildren)) { return rootChildren; } } catch (error) { console.error('获取场景内容失败:', error); throw error; } } #返回数据结构 const response=[ { "children": [], "name": "全球影像", "sceneObj": { "maximumLevel": 18, "name": "全球影像", "id": "08838491-acba-451e-bf1f-ed4d458f7e38", "type": "ESImageryLayer", "url": "https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" } }, { "children": [], "collapsed": true, "name": "boundary", "sceneObj": { "strokeStyle": { "color": [ 0.21568627450980393, 0.9294117647058824, 0.03529411764705882, 1 ], "material": "", "width": 5, "ground": false, "materialParams": {}, "widthType": "screen" }, "flyInParam": { "flyDuration": 1, "rotation": [ 359.99999999999886, -37.20170635557474, 359.99999999999903 ], "position": [ 101.18400820868538, 30.28480013892119, 137990.32151619304 ] }, "name": "boundary", "filled": false, "id": "7a0ca06f-6054-44c6-848f-51855b2b8cb4", "type": "ESGeoJson", "url": "https://gismap.alink.link/3dmapfiles/rangtang/rangtangxian.json" } }, { "children": [], "name": "road", "sceneObj": { "name": "road", "id": "5c3cc102-a8bc-4bf6-a1c2-fdc9c77e19c6", "type": "ESImageryLayer", "url": "https://mapdata.alink.link:6001/indexCia/tms/tilemapresource.xml", "zIndex": 1 } }, { "children": [], "collapsed": true, "name": "terrain", "sceneObj": { "name": "terrain", "id": "d11ddc26-7a25-4351-a100-ee0d22283276", "type": "ESImageryLayer", "url": "https://mapdata.alink.link:6001/pak/layer.json", "zIndex": 2 } }, { "children": [ { "children": [], "collapsed": true, "name": "蒲西乡", "sceneObj": { "name": "蒲西乡", "fontSize": 18, "id": "36c9a5b1-ba60-427c-9efd-8ad394967c1b", "position": [ 101.31096988518799, 31.758069533041372, -0.3554368269705526 ], "text": "蒲西乡", "type": "ESTextLabel" } }, { "children": [], "name": "宗科乡", "sceneObj": { "name": "宗科乡", "fontSize": 18, "id": "6bdf556e-ec0b-4d53-9dca-8e514b6a764b", "position": [ 101.06397423616474, 31.735457402324926, 0.07002222182983407 ], "text": "宗科乡", "type": "ESTextLabel" } }, { "children": [], "name": "石里乡", "sceneObj": { "name": "石里乡", "fontSize": 18, "id": "69e75356-3a34-4b20-9175-6d00ba6cf9e3", "position": [ 101.11188855211684, 31.894306648044548, -0.1276350955737183 ], "text": "石里乡", "type": "ESTextLabel" } }, { "children": [], "collapsed": true, "name": "吾伊乡", "sceneObj": { "name": "吾伊乡", "fontSize": 18, "id": "50e3f588-de57-4b40-b341-3e9d3af1a067", "position": [ 101.00075510206388, 32.049643388354454, -0.395072220384877 ], "text": "吾伊乡", "type": "ESTextLabel" } }, { "children": [], "collapsed": true, "name": "中壤塘镇", "sceneObj": { "name": "中壤塘镇", "fontSize": 18, "id": "65bfbfbe-53bc-47e7-8ab1-0ea63e26fa03", "position": [ 101.22475720742355, 32.18666584333736, 0.04699996893255173 ], "text": "中壤塘镇", "type": "ESTextLabel" } }, { "children": [], "name": "上壤塘乡", "sceneObj": { "name": "上壤塘乡", "fontSize": 18, "id": "f96054b5-40ae-4f06-810a-a484bbd2a468", "position": [ 101.3462915036391, 32.24639772361728, 0.14415805983716212 ], "text": "上壤塘乡", "type": "ESTextLabel" } }, { "children": [], "collapsed": true, "name": "尕多乡", "sceneObj": { "name": "尕多乡", "fontSize": 18, "id": "4fd1ba95-41f9-4c64-add2-0f63e9f9a3ac", "position": [ 101.14077716155053, 32.39338163805662, -0.15134801435379808 ], "text": "尕多乡", "type": "ESTextLabel" } }, { "children": [], "name": "南木达镇", "sceneObj": { "name": "南木达镇", "fontSize": 18, "id": "facf3d08-f1bb-4d0c-a75b-6be49850cfa2", "position": [ 101.02613744401968, 32.410023040030836, -0.22438337926771446 ], "text": "南木达镇", "type": "ESTextLabel" } }, { "children": [], "name": "茸木达乡", "sceneObj": { "name": "茸木达乡", "fontSize": 18, "id": "b523be1e-be6d-4417-a505-efcdbf155825", "position": [ 101.04638705887756, 32.51416480265879, 0.045464148752334864 ], "text": "茸木达乡", "type": "ESTextLabel" } }, { "children": [], "collapsed": true, "name": "上杜柯乡", "sceneObj": { "name": "上杜柯乡", "fontSize": 18, "id": "d074f6cd-cb95-4b22-a95f-95fb0823ad63", "position": [ 100.74553605818096, 32.453110335930056, -0.2785384109580931 ], "text": "上杜柯乡", "type": "ESTextLabel" } }, { "children": [], "collapsed": true, "name": "岗木达镇", "sceneObj": { "name": "岗木达镇", "fontSize": 18, "id": "b538b18b-d8aa-4632-bc78-496b9e3ce4a6", "position": [ 100.88045514041491, 32.23970992447721, -0.10215226380875761 ], "text": "岗木达镇", "type": "ESTextLabel" } } ], "collapsed": true, "name": "boundaryName" }, { "children": [ { "children": [], "collapsed": true, "name": "测试", "sceneObj": { "mode": "SquareV03", "socketName": "站点1", "name": "测试", "style": { "textBoxAlign": "start", "textOffset": [ 10, 2 ], "textBoxShow": true, "fontSize": 20, "textBoxMode": "default", "imageSize": [ 60, 60 ], "textBackgroundSize": [ 120, 30 ], "textBackgroundColor": [ 1, 1, 1, 0 ], "textColor": [ 1, 1, 1, 1 ], "textBoxOffset": [ 40, 0 ] }, "id": "e79ece31-eebb-4eff-877f-211e3a7e7070", "allowPicking": true, "position": [ 100.78260332965166, 32.1923681024918, -0.0021763699037819872 ], "actorTag": "61f09fa0-2820-11f1-a1d0-a736ecedf777", "type": "ESPoi2D" } } ], "name": "sites" } ]3、场景对象创建与销毁
他们有官方示例,大家可自行参考,我是根据实际开发需求总结的,无论是添加影像、地形、3DTileset以及各种类型的标记等等
#创建方式一、拿到数据里的sceneObj根据createSceneObjectFromJson创建对象 const createSceneObjectFromJson = (name, data) => { if (data.length === 0) return; if (!objm.savedSceenObjects[name]) { objm.savedSceenObjects[name] = []; } for (let datum of data) { const sceneObject = objm.createSceneObjectFromJson(datum.sceneObj); sceneObject.allowPicking = true; if (!objm.savedSceenObjects[name]) { objm.savedSceenObjects[name] = []; } objm.savedSceenObjects[name].push(sceneObject); sceneObject.pickedEvent.don((e) => { console.log("FromJson", e?.sceneObject.json); }) } } 创建方式二、拿到的数据转为GeoJson,使用矢量的方式创建 const formatGeoJsonData = (type, data) => { let result = { "type": "FeatureCollection", "features": [] } data.forEach(item => { const sceneObj = item?.sceneObj; if (!sceneObj) return; const {position, flyToParam} = sceneObj; sceneObj.type = type; if (position && flyToParam) { sceneObj.flyInParam = objm.activeViewer.transformFlyParam(position, flyToParam) } result.features.push({ "type": "Feature", "geometry": { "type": "Point", "coordinates": position }, "properties": { "sceneObj": sceneObj, "name": sceneObj.name, "type": type } }) }); return result; } const createSceneObjectFromGeoJson = (name, data) => { const sceneObject1 = objm.createSceneObject('ESGeoJson'); sceneObject1.url = data; sceneObject1.textProperty = "name"; sceneObject1.allowPicking = true; sceneObject1.textFontSize = 26; sceneObject1.imageSize = [53, 57]; sceneObject1.textOffset = [50, -70] sceneObject1.textAnchor = [0.5, 1] if (name === "boundaryName") { sceneObject1.url = "https://earth3d.alink.link:30002/defaultIcon.png" } if (!objm.savedSceenObjects[name]) { objm.savedSceenObjects[name] = []; } objm.savedSceenObjects[name].push(sceneObject1); sceneObject1.pickedEvent.don((e) => { console.log('FromGeo', e.geojsonPickInfo) }) } #销毁对象 objm.destroySceneObject(sceneObject) #对象显示隐藏 const visible=true/false; sceneObject.show = visible;