实现效果
说明
针对上一篇的原地转向版本进行了调整,使转向更加平滑。
思路是第一个点到第二个点的飞行过程中,逐步调整为第二个点到第三个点的转向角度
效果图中的模型不对外公开, 所以示例代码中使用的是大雁塔场景
代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<!-- Use correct character set. -->
<meta charset="utf-8" />
<!-- Tell IE to use the latest, best version. -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Make the application on mobile take up the full browser screen and disable user scaling. -->
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>原地转向版本</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/cesium@1.99.0/Build/Cesium/Widgets/widgets.css"
/>
<script src="https://cdn.jsdelivr.net/npm/cesium@1.99.0/Build/Cesium/Cesium.js"></script>
<style>
html,
body,
#viewerContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.cesium-widget-credits {
display: none;
}
/** 隐藏版权信息 **/
.cesium-viewer .cesium-widget-credits {
display: none;
}
button {
position: fixed;
left: 50%;
top: 0;
padding: 10px;
}
</style>
</head>
<body>
<div id="viewerContainer"></div>
<button id="btn">开始漫游</button>
<script>
// 该token去官网申请 https://ion.cesium.com/tokens?page=1
Cesium.Ion.defaultAccessToken = ''
const viewer = new Cesium.Viewer('viewerContainer', {
geocoder: false, //是否显示geocoder小器件,右上角查询按钮
homeButton: false, //是否显示Home按钮
sceneModePicker: false, //是否显示3D/2D选择器
baseLayerPicker: false, //是否显示图层选择器
navigationHelpButton: false, //是否显示右上角的帮助按钮
animation: false, //是否创建动画小器件,左下角仪表
creditContainer: 'viewerContainer', // 对应上面div的ID
timeline: false, //是否显示时间轴
fullscreenButtion: false, //是否显示全屏按钮
vrButton: false,
selectionIndicator: false //是否显示选取指示器组件
})
viewer.scene.debugShowFramesPerSecond = true // 显示帧率
// 加载地形
viewer.terrainProvider = Cesium.createWorldTerrain()
{
// 监听地图点击事件, 用于获取坐标采点, 可以删除
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
handler.setInputAction(function (movement) {
const position = movement.position
const cartesian = viewer.scene.globe.pick(
viewer.camera.getPickRay(position),
viewer.scene
)
const ellipsoid = viewer.scene.globe.ellipsoid
const cartographic = ellipsoid.cartesianToCartographic(cartesian)
const lng = Cesium.Math.toDegrees(cartographic.longitude)
const lat = Cesium.Math.toDegrees(cartographic.latitude)
const height = cartographic.height
console.log({ lng, lat, height })
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
// 加载大雁塔模型,加载不出来就删除
const tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: 'https://lab.earthsdk.com/model/8c5299e0ce5f11eab7a4adf1d6568ff7/tileset.json'
})
)
viewer.flyTo(tileset)
}
/** 准备工作完成 **/
// 漫游路径
const marks = [
{
lng: 108.95935500781312,
lat: 34.217738490316904,
height: 388.95763998407966
},
{
lng: 108.95780486316032,
lat: 34.22122576397667,
height: 392.1550490863772
},
{
lng: 108.96151737814623,
lat: 34.221218218996675,
height: 391.8944050005566
}
]
const PITCH_VALUE = -50 // 相机角度, 如果大于0那么则是从地底往上看,所以要为负值
const ROAM_SPEED = 30 // 漫游速度 m/s
const HEIGHT = 600 // 飞行高度
const TURN_SPEED = 60 // 转向速度 deg/s
let marksIndex = 1
let remainTime = 0 // 剩余时间
let usedTime = 0 // 已用时
let isRoam = false // 是否是漫游状态
// 为按钮绑定点击事件
const btnEl = document.querySelector('#btn')
btnEl.onclick = function () {
isRoam = !isRoam
if (isRoam) {
// 开始漫游
// 绘制轨迹线, 可删
drawArrowLine()
// 计算距离
calculateFlightDuration()
console.log(marks)
// 飞入第一个点
flyinFirst(3, () => {
flyEvent('play') // 开始漫游
})
this.innerHTML = '停止漫游'
} else {
// 停止漫游
this.innerHTML = '开始漫游'
flyEvent('pause')
}
}
function flyEvent(eventType) {
if (!eventType) return
if (eventType == 'play') {
flyExtent()
remainTime = 0
} else if (eventType == 'pause') {
remainTime = Cesium.JulianDate.secondsDifference(
viewer.clock.stopTime,
viewer.clock.currentTime
)
usedTime += Cesium.JulianDate.secondsDifference(
viewer.clock.currentTime,
viewer.clock.startTime
)
const len = viewer.clock.onTick.numberOfListeners
for (let i = 0; i < len; i++)
viewer.clock.onTick.removeEventListener(viewer.clock.onTick._listeners[i])
} else {
// 停止按钮
marksIndex = 1
remainTime = 0
usedTime = 0
const len = viewer.clock.onTick.numberOfListeners
for (let i = 0; i < len; i++)
viewer.clock.onTick.removeEventListener(viewer.clock.onTick._listeners[i])
}
}
function flyExtent() {
// preIndex 第一个点
// marksIndex 第二个点
// nextIndex 第三个点
let preIndex = marksIndex - 1
let nextIndex = marksIndex + 1
if (marksIndex > marks.length - 1) {
marksIndex = 0
preIndex = marks.length - 1
nextIndex = 1
} else if (marksIndex == marks.length - 1) {
nextIndex = 0
} else if (marksIndex == 0) {
preIndex = marks.length - 1
}
// 第一个点到第二个点的飞行时长
const flyDuration = marks[marksIndex].duration
// 第二个点到第三个点的方向
const heading = bearing(
marks[marksIndex].lat,
marks[marksIndex].lng,
marks[nextIndex].lat,
marks[nextIndex].lng
)
// 需要转向的度数
let angle = heading - Cesium.Math.toDegrees(viewer.camera.heading)
// 调整方向, 取最短角度
angle = angle < -180 ? angle + 360 : angle > 180 ? angle - 360 : angle
// 转向速度 deg/s
const rotateSpeed = Math.abs(angle) / flyDuration
// 每秒转向速度, 需要区分左转和右转, 所以
const turnSpeed = angle > 0 ? rotateSpeed : -rotateSpeed
// 相机的当前heading
const initialHeading = viewer.camera.heading
// 相机看点的角度,如果大于0那么则是从地底往上看,所以要为负值
const pitch = Cesium.Math.toRadians(PITCH_VALUE)
// 时间间隔
setExtentTime(flyDuration)
const Exection = function TimeExecution() {
// 当前已经过去的时间,单位s
const delTime = Cesium.JulianDate.secondsDifference(
viewer.clock.currentTime,
viewer.clock.startTime
)
const originLat =
marksIndex == 0 ? marks[marks.length - 1].lat : marks[marksIndex - 1].lat
const originLng =
marksIndex == 0 ? marks[marks.length - 1].lng : marks[marksIndex - 1].lng
const endPosition = Cesium.Cartesian3.fromDegrees(
originLng +
((marks[marksIndex].lng - originLng) / marks[marksIndex].duration) * delTime,
originLat +
((marks[marksIndex].lat - originLat) / marks[marksIndex].duration) * delTime,
HEIGHT
)
const heading = Cesium.Math.toRadians(delTime * turnSpeed) + initialHeading
// console.log(delTime, heading)
viewer.scene.camera.setView({
destination: endPosition,
orientation: {
heading: heading,
pitch: pitch
}
})
if (Cesium.JulianDate.compare(viewer.clock.currentTime, viewer.clock.stopTime) >= 0) {
viewer.clock.onTick.removeEventListener(Exection)
marksIndex++
flyExtent()
}
}
viewer.clock.onTick.addEventListener(Exection)
}
// 设置飞行的时间到viewer的时钟里
function setExtentTime(time) {
const startTime = Cesium.JulianDate.fromDate(new Date())
const stopTime = Cesium.JulianDate.addSeconds(startTime, time, new Cesium.JulianDate())
viewer.clock.startTime = startTime.clone() // 开始时间
viewer.clock.stopTime = stopTime.clone() // 结速时间
viewer.clock.currentTime = startTime.clone() // 当前时间
viewer.clock.clockRange = Cesium.ClockRange.CLAMPED // 行为方式
viewer.clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK // 时钟设置为当前系统时间; 忽略所有其他设置。
}
/** 相机视角飞行 结束 **/
/** 飞行时 camera的方向调整(heading) 开始 **/
// Converts from degrees to radians.
function toRadians(degrees) {
return (degrees * Math.PI) / 180
}
// Converts from radians to degrees.
function toDegrees(radians) {
return (radians * 180) / Math.PI
}
function bearing(startLat, startLng, destLat, destLng) {
startLat = this.toRadians(startLat)
startLng = this.toRadians(startLng)
destLat = this.toRadians(destLat)
destLng = this.toRadians(destLng)
let y = Math.sin(destLng - startLng) * Math.cos(destLat)
let x =
Math.cos(startLat) * Math.sin(destLat) -
Math.sin(startLat) * Math.cos(destLat) * Math.cos(destLng - startLng)
let brng = Math.atan2(y, x)
let brngDgr = this.toDegrees(brng)
return (brngDgr + 360) % 360
}
/** 飞行时 camera的方向调整(heading) 结束 **/
// 计算飞行时长 两点间的距离 / 漫游速度
function calculateFlightDuration() {
for (let i = 0; i < marks.length - 1; i++) {
const distance = rhumbDistance(
marks[i].lng,
marks[i].lat,
marks[i + 1].lng,
marks[i + 1].lat
)
// 时长/s
const duration = distance / ROAM_SPEED
marks[i + 1].duration = duration
}
// 终点到起点的时长
marks[0].duration =
rhumbDistance(
marks[marks.length - 1].lng,
marks[marks.length - 1].lat,
marks[0].lng,
marks[0].lat
) / ROAM_SPEED
// 计算两个点之间的距离, 单位m
function rhumbDistance(lng1, lat1, lng2, lat2) {
var rad1 = (lat1 * Math.PI) / 180.0
var rad2 = (lat2 * Math.PI) / 180.0
var a = rad1 - rad2
var b = (lng1 * Math.PI) / 180.0 - (lng2 * Math.PI) / 180.0
var r = 6378137
var distance =
r *
2 *
Math.asin(
Math.sqrt(
Math.pow(Math.sin(a / 2), 2) +
Math.cos(rad1) * Math.cos(rad2) * Math.pow(Math.sin(b / 2), 2)
)
)
return distance
}
}
// 绘制箭头线
function drawArrowLine() {
for (let i = 0; i < marks.length - 1; i++) {
// 经纬度转世界坐标
const currentPosition = Cesium.Cartesian3.fromDegrees(marks[i].lng, marks[i].lat)
const nextPosition = Cesium.Cartesian3.fromDegrees(marks[i + 1].lng, marks[i + 1].lat)
viewer.entities.add({
polyline: {
positions: [currentPosition, nextPosition],
// 或者使用
// positions: Cesium.Cartesian3.fromDegreesArray([精度,纬度,经度,纬度...])
// material: Cesium.Color.RED.withAlpha(0.5), // 材质
material: new Cesium.PolylineArrowMaterialProperty(Cesium.Color.RED),
eyeOffset: new Cesium.Cartesian3(0, 0, -100),
clampToGround: true,
width: 20,
heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND
}
})
}
}
/**
* 飞入第一个点, 主要用于调整初始方向
* @param duration 时长 s
* @param callback 飞行完之后的回调
*/
function flyinFirst(duration, callback) {
let heading = bearing(marks[0].lat, marks[0].lng, marks[1].lat, marks[1].lng)
heading = Cesium.Math.toRadians(heading)
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(marks[0].lng, marks[0].lat, HEIGHT),
duration, //定位的时间间隔
orientation: {
heading: heading,
pitch: Cesium.Math.toRadians(PITCH_VALUE)
},
complete: () => {
callback && callback()
}
})
}
</script>
</body>
</html>