本文将从三个方面来记录此次从0到1的过程:
1. 调用影像和地形服务
初始化地图:
php
```js
const viewer = new Cesium.Viewer('map', {
baseLayer: false,// 图层控件显隐控制
timeline: false, // 隐藏时间轴
animation: false, // 隐藏动画控制器
geocoder: false, // 隐藏地名查找控制器
homeButton: false, // 隐藏Home按钮
sceneModePicker: false, // 隐藏投影方式控制器
baseLayerPicker: false, // 隐藏图层选择控制器
navigationHelpButton: false, // 隐藏帮助按钮
fullscreenButton: false, // 隐藏全屏按钮
selectionIndicator: false, // 关闭双击选择entities实例
orderIndependentTranslucency: false,
contextOptions: {
webgl: {
alpha: true
}
},
skyBox: false,
sun: false,
moon: false,
skyAtmosphere: false
})
```
加载wmts影像服务:
php
```js
const osmImageryProvider = new Cesium.WebMapTileServiceImageryProvider({
url: '/geoserver/gwc/rest/wmts/ditu:w(6)/{style}/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png',
layer: 'ditu:w(6)',
format: 'image/png',
style: 'raster',
maximumLevel: 40,
tileMatrixSetID: 'EPSG:900913'
})
viewer.imageryLayers.addImageryProvider(osmImageryProvider)
```
加载地形服务
js
const cesiumTerrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(
'/terrain',
{
requestWaterMask: true, // 请求水体遮罩数据(可选)
requestVertexNormals: true // 请求顶点法线用于光照效果(可选)
}
)
viewer.scene.terrainProvider = cesiumTerrainProvider
加载地形服务的时候要注意,cesium 只支持terrain 数据集
最开始我尝试着使用wmts发布的dem地形服务加载,试过很多方法都不彳亍,后面看到有一个python自动转化的方法,但是由于有学习成本就没使用
转换的方法我这边采用的是cesiumLab,官网直接下载,免费使用,登录之后选择地形处理,输入带dem信息的tif文件后导出即可,terrain 数据集一般内容比较大,建议放在服务器上,放在public里面会导致体积过大
3. 加装水流
这里的思路是加装geojson 之后给高度,这样高度以下的部分就会被dem遮挡住,这样就会有水流的效果,如果需要实现类似水波纹的效果网上可以直接搜索,有很多实例
js
// 核心属性:加载geojson后,用地形遮挡住下面看不见的部分
viewer.scene.globe.depthTestAgainstTerrain = true
// 加载geojson水面效果
function showWather (geojson, height) {
Cesium.GeoJsonDataSource.load(
URL.createObjectURL(new Blob([geojson], { type: 'application/json' }))
).then((dataSource) => {
viewer.dataSources.add(dataSource)
const entitys = dataSource.entities.values
for (const entity of entitys) {
// entity.polygon.material = Cesium.Color.SKYBLUE.withAlpha(0.25);
entity.polygon.material = Cesium.Color.fromRandom({
alpha: 0.5,
blue: 0.8,
green: 0.4,
red: 0.2
})
entity.polygon.outline = false
entity.polygon.extrudedHeight = height
}
})
}
水流逐渐上涨过程
第一步实验
我们有一个水流逐渐上涨的需求,比如从120米涨水到160米,需要有一个缓慢的过渡效果,我最开始的思路是,通过修改geojson的高度来实现
后面发现,每次修改后,它的图层从关闭到重新加载会有一段时间,导致图层会有零点几秒的闪烁时间,展示的效果不好
后续又采用叠加新的geojson 并且给不同的高度来实现
但是最开始测试后发现,图层会重复叠加,由于图层有透明度,图层叠加后会变得不透明 解决方案:每次叠加新图层之前后,清除掉上一个图层
最终虽然这样不会有图层叠加的问题了,但是零点几秒的闪烁时间依然存在,原因就是销毁到加载cesium需要时间
最终解决方案
思路:提前将所有图层全部预加载进入cesium,并且全部存入一个数组中,但是全部不显示,后续根据时间来控制图层的显示隐藏,这样效率比直接加载快很多,并且不会有图层叠加和零点几秒的闪烁时间的问题
代码:
js
dataSource.show = false//隐藏掉图层,dataSource看上面代码
//后续循环村图层的数组进行控制显隐藏就好了,其实这里每次显示时使用一个变量去记住会更好,
//下次直接去销毁就行,不需要循环,性能会更好
data.waterdataTop.forEach(v => {
if (v.tm === tm) {//这里是我需要根据时间判断那个图层需要显示
data.waterdataTop.forEach(res => {
res.dataSource.show = false
})
v.dataSource.show = true
}
}
})
4. 模拟视角飞行
视频上传不了,只能看图了
飞行路径图:
飞行效果图:
这个经过很多很多查找,大部分都是视角跟着一个飞机或者其他物体去运动,或者视角是垂直于地面的,并没有我想要的像是人在走一样的效果
实验过程
思路:由于没有找到对应的飞翔api,所以决定采用将一段路分成多个点,每次定位到下一个点的方式来实现飞行效果,只要定位的速度够快,并且点够多,这样就不会有卡顿
最先想到的是flyto ,因为这个api是有一个过渡效果的,但是经过实验后发现不彳亍,因为它是一个曲线飞行,首先会先将视角跳转到高度后再会来,不能做到平滑过渡,并且它的飞行速度也是曲线,先慢后快,最后决定使用lookAt来实现
最终实现
需要实现的步骤:
- 绘制出飞行路径,第一次点击为起点,后面每一次点击是为一条直线,这条直线就是飞行的路径,根据首尾俩点再在这条直线上插补出很多我需要的经纬度信息
- 将所有的飞行路径放到一个数组里,后续控制飞机飞行就只需要从数组里取到数据就行
- 定义一个全局index,然后使用定时器实现开始和暂停
实现:
- 绘制飞行路径并且生成飞行路径数组
getFlyRoute就是返回飞行数组的函数,需要传入一个多个点的数组,是个二维数组,输出的也是二维数组,数组的第三位是与前一个点的角度
示例:: "routeArr":
[[113.03418811062859,30.96501808361106],[113.03418811062859,30.96501808361106],[113.04394567765505,30.96910239672927],[113.04394567765505,30.96910239672927],[113.05719347194503,30.962402300357656],[113.05719347194503,30.962402300357656],[113.07151377792708,30.95662679673569],[113.07151377792708,30.95662679673569]]
js
import * as Cesium from 'cesium'
import { reactive } from 'vue'
import angle from './mapAngle'
const { Calculate } = angle()
// 视角飞行函数
export default function fly () {
// const routeArr=ref([])
const data = reactive({
// 点的arr
routeArr: [],
// 实例的id,用来销毁
routeID: [],
// 屏幕点击事件变量
handler: null
})
/* const routeArr = [];
const routeID = [];
let handler=null
// 屏幕双击事件变量
let doubleHandler=null */
// 获取飞行的路线
/**
* param:飞行路线数组、
* 返回完整的飞行路线数组
*/
function getFlyRoute (param) {
let arr = []
param.forEach((v, i) => {
if (i === param.length - 1) {
// 如果是最后一个就return 出去
return
}
arr = [...arr, ...interpolateCoordinates(v, param[i + 1])]
})
arr.forEach((v, i) => {
if (i === arr.length - 1) {
// 如果是最后一个,使用前一个的数据的角度
v.push(arr[i - 1][2])
return
}
const angle1 = {
lon: v[0],
lat: v[1],
elv: 200
}
const angle2 = {
lon: arr[i + 1][0],
lat: arr[i + 1][1],
elv: 200
}
v.push(Calculate(angle1, angle2))
})
return arr
}
// 获取点的坐标以及画点
function getPonitCoordinate (_viewer) {
closeEntities(_viewer)
// 注册屏幕点击事件
data.handler = new Cesium.ScreenSpaceEventHandler(_viewer.scene.canvas)
data.handler.setInputAction(function (event) {
// 转换为不包含地形的笛卡尔坐标
const clickPosition = _viewer.scene.camera.pickEllipsoid(event.position)
// 转经纬度(弧度)坐标
const radiansPos = Cesium.Cartographic.fromCartesian(clickPosition)
/* console.log(radiansPos, "radiansPos");
console.log(clickPosition, "clickPosition"); */
// 转角度
// console.log("经度:" + Cesium.Math.toDegrees(radiansPos.longitude) + ", 纬度:" + Cesium.Math.toDegrees(radiansPos.latitude));
// 添加点
const position = [
Cesium.Math.toDegrees(radiansPos.longitude),
Cesium.Math.toDegrees(radiansPos.latitude)
]
addPoint(_viewer, position)
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
// 双击事件关闭画点
data.handler.setInputAction(function (event) {
_viewer.trackedEntity = undefined
closeHandler()
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)
}
// 添加点
function addPoint (_viewer, param = []) {
const id = data.routeArr.length + 1 + 'point'
const point = {
id,
position: Cesium.Cartesian3.fromDegrees(param[0], param[1], 200),
point: {
color: Cesium.Color.YELLOW,
pixelSize: 20
}
}
if (data.routeArr.length === 0) {
// 第一个点为开始点
point.label = {
text: '开始点',
font: '14pt monospace',
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
outlineWidth: 2,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -9)
}
}
_viewer.entities.add(point)
data.routeArr.push(param)
data.routeID.push(id)
drawLine(_viewer, param)
}
// 回显点和线
function addMorePoint (_viewer, params = {}) {
clearRouteArr()
closeEntities(_viewer)
data.routeID = []
// console.log(params);
params.value.forEach((v, i) => {
addPoint(_viewer, v)
})
}
/**
* 俩点之间添加线
* @param {positions:[lastPoint,nextPoint]}
* @returns
*/
function AddPolyline (_viewer, params) {
const id = data.routeArr.length + 'line'
const entity = new Cesium.Entity({
id,
show: true,
polyline: new Cesium.PolylineGraphics({
show: true,
// fromDegrees返回给定的经度和纬度值数组(以度为单位),该数组由Cartesian3位置组成。
// Cesium.Cartesian3.fromDegreesArray([经度1, 纬度1, 经度2, 纬度2,])
// Cesium.Cartesian3.fromDegreesArrayHeights([经度1, 纬度1, 高度1, 经度2, 纬度2, 高度2])
positions: Cesium.Cartesian3.fromDegreesArrayHeights(params.positions),
width: 5,
material: Cesium.Color.RED
})
})
_viewer.entities.add(entity)
data.routeID.push(id)
_viewer.trackedEntity = undefined
}
/**
* 画路径线
*/
function drawLine (_viewer, nextPoint) {
if (data.routeArr.length <= 1) {
return
}
const lastPoint = data.routeArr[data.routeArr.length - 2]
const positions = {
positions: [...lastPoint, 200, ...nextPoint, 200]
}
AddPolyline(_viewer, positions)
}
/**
* 关闭画的点和线显示
*/
function closeEntities (_viewer) {
data.routeID.forEach((v) => {
const entID = _viewer.entities.getById(v)
_viewer.entities.remove(entID)
})
}
/**
* 获取每一次移动的步长
*/
function getStep () {
const lat1 = 113.05459941234724
const lon1 = 30.96423174313923
const lat2 = 113.05454766211562
const lon2 = 30.964100607409584
let coordinates = 0
const latStep = lat2 - lat1 /* / 6 */ // 6个间隔,其中一个是起点
const lonStep = lon2 - lon1 /* / 6 */ // 6个间隔,其中一个是起点
coordinates = latStep * latStep + lonStep * lonStep
return coordinates * 16 /* 数字越大,间隔越大 */
}
/**
* 输入俩点,获取其中的插补
* 插补函数,在俩个坐标点之间插补坐标,输出包含输入第一个点在内的数组
*/
function interpolateCoordinates ([lat1, lon1], [lat2, lon2]) {
// console.log(lat1, lon1, lat2, lon2);
const coordinates = []
const newLat = lat2 - lat1
const newLon = lon2 - lon1
// 俩点连线后的线长
const z = newLat * newLat + newLon * newLon
const step = parseInt(z / getStep())
const latStep = (lat2 - lat1) / (step + 1) // 加一个间隔,其中一个是起点
const lonStep = (lon2 - lon1) / (step + 1) // 加一个间隔,其中一个是起点
for (let i = 0; i <= step; i++) {
const interpolatedLat = lat1 + latStep * i
const interpolatedLon = lon1 + lonStep * i
coordinates.push([interpolatedLat, interpolatedLon])
}
return coordinates
}
// 关闭地图点击和双击事件
function closeHandler () {
data.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
data.handler.removeInputAction(
Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK
)
}
// 清空point数组
function clearRouteArr () {
data.routeArr = []
}
return {
data,
getFlyRoute,
getPonitCoordinate,
closeEntities,
closeHandler,
addMorePoint,
clearRouteArr
}
}
这里会有一个很重要的问题:每次定位的角度怎么计算,毕竟像人一样扭动头需要角度,垂直观看地面则是不需要
思路:将后面的点与前面一个点对比,可以获得经纬度,再根据二点的经纬度来计算角度,这里角度计算踩了很多坑,找chartgpt也写了几个角度也都不对,后面找到了一个在线转换的网站,趴了他的源代码后找到了计算方法,经过改造后就成了我的,很感谢这些旧时代网站,浏览器能直接访问源码,下面是计算角度代码:
这里就是上面引入的计算角度的Calculate方法
js
// 俩个金纬度求与北方的夹角,北方为0度
export default function angle(){
function EarthRadiusInMeters(latitudeRadians) {
var a = 6378137.0;
var b = 6356752.3;
var cos = Math.cos(latitudeRadians);
var sin = Math.sin(latitudeRadians);
var t1 = a * a * cos;
var t2 = b * b * sin;
var t3 = a * cos;
var t4 = b * sin;
return Math.sqrt((t1 * t1 + t2 * t2) / (t3 * t3 + t4 * t4));
}
function LocationToPoint(c) {
var lat = (c.lat * Math.PI) / 180.0;
var lon = (c.lon * Math.PI) / 180.0;
var radius = c.elv + EarthRadiusInMeters(lat);
var cosLon = Math.cos(lon);
var sinLon = Math.sin(lon);
var cosLat = Math.cos(lat);
var sinLat = Math.sin(lat);
var x = cosLon * cosLat * radius;
var y = sinLon * cosLat * radius;
var z = sinLat * radius;
return { x: x, y: y, z: z, radius: radius };
}
function RotateGlobe(b, a, bradius, aradius) {
var br = { lat: b.lat, lon: b.lon - a.lon, elv: b.elv };
var brp = LocationToPoint(br);
brp.x *= bradius / brp.radius;
brp.y *= bradius / brp.radius;
brp.z *= bradius / brp.radius;
brp.radius = bradius;
var alat = (-a.lat * Math.PI) / 180.0;
var acos = Math.cos(alat);
var asin = Math.sin(alat);
var bx = brp.x * acos - brp.z * asin;
var by = brp.y;
var bz = brp.x * asin + brp.z * acos;
return { x: bx, y: by, z: bz };
}
/**
*
* @param {*} a
* {
* lat:30.968046236506876,
lon:113.04772063790635,
elv:200 //高度(米)
}
* @param {*} b 同上
* @returns 俩点与北方(角度为0度)之间的夹角
*/
function Calculate(a,b) {
var ap = LocationToPoint(a);
var bp = LocationToPoint(b);
var br = RotateGlobe(b, a, bp.radius, ap.radius);
var theta = (Math.atan2(br.z, br.y) * 180.0) / Math.PI;
var azimuth = 90.0 - theta;
if (azimuth < 0.0) {
azimuth += 360.0;
}
if (azimuth > 360.0) {
azimuth -= 360.0;
}
return azimuth
}
return {
Calculate
}
}
这样有了一整条路线的数组后,再配合定时器一直定位,就可以实现飞行的效果了,当然,这里转换角度的时候过渡不是很平滑,可以再优化转角度的时候,将角度再做插补
js
flying = setInterval(() => {
flyIndex += 1
if (flyIndex >= flyLength) {
// 如果大于了飞行的数组长度,则暂停
outFly()
return
}
const position = jsonArr[flyIndex]
const destination = Cesium.Cartesian3.fromDegrees(
Number(position[0]),
Number(position[1]),
200
)
const orientation = new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(position[2]),
Cesium.Math.toRadians(-15),
200
)
data.viewer.scene.camera.lookAt(destination, orientation)
}, 80)
暂停或者跑完后,需要解锁相机
data.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY) // 解锁相机
这样一个飞行效果就完美实现了,然后查看浏览器性能,不经感慨,谷歌还是强大,v8NB
![D6$}RUN19KKE{(WQNO{1AK.png
总结
这是一次从0到1的全新尝试,有做得不好的地方,比如,不应该使用原生cesium,应该使用现成的库,毕竟这样能节约很多时间和成本,但是当时并不知道有这么多Mars3D这类库,还是年轻了
淹没的效果其实做得不算太好,这种做法有一个缺点,geojson的高度是固定的,也就是一个范围内的高度都是一样的,会导致有些不该有水的地方有水,有些有水的地方没水,地势高的地方也会没水,不够合理,没能够完美实现想要的效果
还有一些其他的效果也做得不是很好,但是在mars3D里面,使用别人封装好的方法却很好实现,下一步需要去看别人代码看看别人是怎么实现的
结束语:
有时候是真的能感受到说话的艺术,明明有很多话想说,但是的的确确自己很难逻辑清晰的,有条不紊的表达出来,以前偶尔会感慨纸短情长,但是技术的长度,永远不会写尽!
如果看到这里了,说明你是真的闲,欢迎与我交流,私信和留言都会回复,觉得我写的还可以的,或者感兴趣的可以加我vx: zz0254i
一起交流技术