Cesium有很多很强大的功能,可以在地球上实现很多炫酷的3D效果。今天给大家分享一个第一人称漫游功能。
1.话不多说,先展示
视频:第一人称漫游管理
2.设计思路
点击绘制开始在地图上绘制漫游的路径点位,双击结束后可编辑漫游路径名称。点击飞行,以第一视角飞行,可暂停、继续和退出飞行,删除时清除地图上的漫游路径。
3.具体代码
<template>
<div class="page">
<el-button @click="drawLineRoad">绘制</el-button>
<el-table :data="dataList" border v-loading="loading">
<el-table-column prop="name" label="名称" align="center" />
<el-table-column prop="action" label="操作" align="center">
<template #default="scope">
<el-button type="primary" style="width: 30px"
@click="startFly(scope.row, scope.$index)">飞行</el-button>
<el-button type="primary" style="width: 30px" @click="stopFly()">暂停</el-button>
<el-button type="primary" style="width: 30px" @click="continueFly()">继续</el-button>
<el-button type="primary" style="width: 30px"
@click="finishFly(scope.row, scope.$index)">退出</el-button>
<el-button link type="primary" size="small" @click="delEntity(scope.row, scope.$index)"><el-icon
:size="16"><ele-Delete /> </el-icon></el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="dialogFormVisible" title="配置" width="500" :close-on-press-escape="false"
:close-on-click-modal="false" :show-close="false">
<el-form ref="formRef" :model="form" label-width="auto" :rules="rules">
<el-form-item label="漫游路径名称" prop="title">
<el-input v-model="form.title" placeholder="请输入" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm(formRef)"> 确定 </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import * as Cesium from 'cesium';
import { ElMessage, ElMessageBox } from "element-plus";
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import * as api from "/@/api/main/manYGL";
import { initClock } from '/@/utils/cesium-utils';
const loading = ref(false);
const props = defineProps(['viewer']);
const dialogFormVisible = ref(false);
var handler: any = null;
const formRef = ref();
const rules = {
title: { required: true, message: '请输入漫游路径名称', trigger: 'blur' },
};
// 视点名称
const form = reactive({
title: '',
height: 100,
});
//是否开始绘制
const drawing = ref(false);
//列表数据
const dataList: any = reactive([]);
var entities: any = [];
//point实体列表
var pointEntities: any = [];
//线实体列表
var linesEntities: any = [];
var activeShapePoints: any = [];
var customMarks: any = [];
var Exection: any = null;
const pitchValue = -20;
var marksIndex = 1;
var flytime = 10;
var changeCameraTime = 7;
var floatingPoint: any = undefined;
var activeShape: any = undefined;
const startFly = (item: any, index: number) => {
if (Exection) {
props.viewer.clock.onTick.removeEventListener(Exection);
}
initClock(props.viewer);
flyExtent(item.positions);
};
//开始飞行
const flyExtent = (marks: any) => {
// 相机看点的角度,如果大于0那么则是从地底往上看,所以要为负值
const pitch = Cesium.Math.toRadians(pitchValue);
// 时间间隔2秒钟
setExtentTime(10);
Exection = function TimeExecution() {
let preIndex = marksIndex - 1;
if (marksIndex == 0) {
preIndex = marks.length - 1;
}
//计算俯仰角
let heading = bearing(marks[preIndex].lat, marks[preIndex].lng, marks[marksIndex].lat, marks[marksIndex].lng);
heading = Cesium.Math.toRadians(heading);
// 当前已经过去的时间,单位s
const delTime = Cesium.JulianDate.secondsDifference(props.viewer.clock.currentTime, props.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) / flytime) * delTime,
originLat + ((marks[marksIndex].lat - originLat) / flytime) * delTime,
marks[marksIndex].height
);
props.viewer.scene.camera.setView({
destination: endPosition,
orientation: {
heading: heading,
pitch: pitch,
},
});
if (Cesium.JulianDate.compare(props.viewer.clock.currentTime, props.viewer.clock.stopTime) >= 0) {
props.viewer.clock.onTick.removeEventListener(Exection);
//有个转向的功能
changeCameraHeading(marks);
}
};
props.viewer.clock.onTick.addEventListener(Exection);
};
//停止飞行
const stopFly = () => {
props.viewer.clock.shouldAnimate = false;
};
//继续飞行
const continueFly = () => {
props.viewer.clock.shouldAnimate = true;
};
const finishFly = (item: any, index: number) => {
if (Exection) {
props.viewer.clock.onTick.removeEventListener(Exection);
}
initClock(props.viewer);
};
// 设置飞行的时间到viewer的时钟里
const setExtentTime = (time: any) => {
const startTime = Cesium.JulianDate.fromDate(new Date());
const stopTime = Cesium.JulianDate.addSeconds(startTime, time, new Cesium.JulianDate());
props.viewer.clock.startTime = startTime.clone(); // 开始时间
props.viewer.clock.stopTime = stopTime.clone(); // 结速时间
props.viewer.clock.currentTime = startTime.clone(); // 当前时间
props.viewer.clock.clockRange = Cesium.ClockRange.CLAMPED; // 行为方式-达到终止时间后停止
props.viewer.clock.clockStep = Cesium.ClockStep.SYSTEM_CLOCK; // 时钟设置为当前系统时间; 忽略所有其他设置。
};
//计算俯仰角
const bearing = (startLat: any, startLng: any, destLat: any, destLng: any) => {
startLat = toRadians(startLat);
startLng = toRadians(startLng);
destLat = toRadians(destLat);
destLng = 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 = toDegrees(brng);
return (brngDgr + 360) % 360;
};
// 相机原地定点转向
const changeCameraHeading = (marks: any) => {
let nextIndex = marksIndex + 1;
if (marksIndex == marks.length - 1) {
nextIndex = 0;
}
// 计算两点之间的方向
const heading = bearing(marks[marksIndex].lat, marks[marksIndex].lng, marks[nextIndex].lat, marks[nextIndex].lng);
// 相机看点的角度,如果大于0那么则是从地底往上看,所以要为负值
const pitch = Cesium.Math.toRadians(pitchValue);
// 给定飞行一周所需时间,比如10s, 那么每秒转动度数
const angle = (heading - Cesium.Math.toDegrees(props.viewer.camera.heading)) / changeCameraTime;
// 时间间隔2秒钟
setExtentTime(changeCameraTime);
// 相机的当前heading
const initialHeading = props.viewer.camera.heading;
Exection = function TimeExecution() {
// 当前已经过去的时间,单位s
const delTime = Cesium.JulianDate.secondsDifference(props.viewer.clock.currentTime, props.viewer.clock.startTime);
const heading = Cesium.Math.toRadians(delTime * angle) + initialHeading;
props.viewer.scene.camera.setView({
orientation: {
heading: heading,
pitch: pitch,
},
});
if (Cesium.JulianDate.compare(props.viewer.clock.currentTime, props.viewer.clock.stopTime) >= 0) {
props.viewer.clock.onTick.removeEventListener(Exection);
marksIndex = ++marksIndex >= marks.length ? 0 : marksIndex;
if (marksIndex != 0) {
flyExtent(marks);
}
}
};
props.viewer.clock.onTick.addEventListener(Exection);
};
/**
* 删除已绘制的图形
*/
const delEntity = async (item: any, index: number) => {
await Delete(item, index);
};
/**
* 点击确定
*/
const submitForm = async (formEl: any) => {
const valid = await formEl.validate();
if (valid) {
Save(form.title, customMarks);
dialogFormVisible.value = false;
formEl.resetFields();
}
};
//绘制线路
const drawLineRoad = () => {
drawing.value = true;
handler = new Cesium.ScreenSpaceEventHandler(props.viewer.scene.canvas);
//鼠标左键
handler.setInputAction(function (event: any) {
if (drawing.value) {
var earthPosition = props.viewer.scene.pickPosition(event.position);
if (Cesium.defined(earthPosition)) {
if (activeShapePoints.length === 0) {
floatingPoint = createPoint(earthPosition);
activeShapePoints.push(earthPosition);
var dynamicPositions = new Cesium.CallbackProperty(function () {
return activeShapePoints;
}, false);
activeShape = drawShape(dynamicPositions); //绘制动态图
//线实体集合
linesEntities.push(activeShape);
}
activeShapePoints.push(earthPosition);
//点实体集合
pointEntities.push(createPoint(earthPosition));
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
//鼠标移动
handler.setInputAction(function (event: any) {
if (Cesium.defined(floatingPoint)) {
var newPosition = props.viewer.scene.pickPosition(event.endPosition);
if (Cesium.defined(newPosition)) {
floatingPoint.position.setValue(newPosition);
activeShapePoints.pop();
activeShapePoints.push(newPosition);
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
handler.setInputAction(function () {
if (drawing.value) {
drawing.value = false;
terminateShape();
}
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
};
//绘制点
const createPoint = (worldPosition: any) => {
var point = props.viewer.entities.add({
position: worldPosition,
point: {
color: Cesium.Color.RED,
pixelSize: 10,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
});
return point;
};
//绘制线
const drawShape = (positionData: any) => {
var shape = props.viewer.entities.add({
polyline: {
with: 10,
color: Cesium.Color.RED,
positions: positionData,
clampToGround: true,
},
});
return shape;
};
const terminateShape = () => {
activeShapePoints.pop(); //去除最后一个动态点
if (activeShapePoints.length) {
customMarks = [];
for (const position of activeShapePoints) {
const latitude = toDegrees(Cesium.Cartographic.fromCartesian(position).latitude);
const longitude = toDegrees(Cesium.Cartographic.fromCartesian(position).longitude);
customMarks.push({ lat: latitude, lng: longitude, height: 300 });
}
linesEntities.push(drawShape(activeShapePoints)); //绘制最终图
}
dialogFormVisible.value = true; //弹出对话框
props.viewer.entities.remove(floatingPoint); //去除动态点图形(当前鼠标点)
props.viewer.entities.remove(activeShape); //去除动态图形
props.viewer.trackedEntity = null;//为了去除双击后锁定无法移动视角
floatingPoint = undefined;
activeShape = undefined;
activeShapePoints = [];
};
// 弧度转角度
const toDegrees = (radians: any) => {
return (radians * 180) / Math.PI;
};
// 角度转弧度
const toRadians = (degrees: any) => {
return (degrees * Math.PI) / 180;
};
onMounted(async () => {
await handleQuery();
});
onUnmounted(() => {
if (Exection) {
props.viewer.clock.onTick.removeEventListener(Exection);
}
//清除绘制的内容
props.viewer.entities.removeAll();
if (handler != null) {
handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
}
});
/**
* 删除信息
*/
const Delete = async (item: any, index: any) => {
ElMessageBox.confirm(`确定要删除吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
loading.value = true;
var res = await api.deleteManYGL({ id: item.id });
if (res.data.type == "success") {
//退出飞行
if (Exection) {
props.viewer.clock.onTick.removeEventListener(Exection);
}
for (const obj of entities[index].pointEntities) {
props.viewer.entities.remove(obj);
}
for (const obj of entities[index].linesEntities) {
props.viewer.entities.remove(obj);
}
entities.splice(index, 1);
dataList.splice(index, 1);
ElMessage.success("删除成功");
}
loading.value = false;
}).catch(() => { });
}
/**
* 查询
*/
const handleQuery = async () => {
loading.value = true;
var res = await api.listManYGL();
console.log(res);
if (res.data.code == 200 && res.data.result) {
for (const item of res.data.result) {
showResult(item.manYMCh, item._CoordinateInfoList, item.id);
}
}
loading.value = false;
}
/**
* 保存漫游
*/
const Save = async (name: string, positions: any,) => {
var pointList: any = [];
for (const item of positions) {
pointList.push({
x: item.lng,
y: item.lat,
z: item.height,
});
}
var param = {
'manYMCh': name,
'_CoordinateInfoList': pointList
};
console.log(param);
var res = await api.addManYGL(param);
console.log(res);
if (res.data.code == 200 && res.data.result) {
ElMessage.success("漫游添加成功");
addElectronicFence(res.data.result.manYMCh, res.data.result._CoordinateInfoList, res.data.result.id);
}
}
/**
* 添加
*/
var addElectronicFence = (name: string, positions: any, id: any) => {
var customMarks = [];
for (const position of positions) {
const latitude = position.y;
const longitude = position.x;
const height = position.z;
customMarks.push({ lat: latitude, lng: longitude, height: height });
}
//点实体和线实体的集合
entities.push({
pointEntities: pointEntities,
linesEntities: linesEntities,
});
dataList.push({
id: id,
name: name,
positions: customMarks,
});
pointEntities = [];
linesEntities = [];
};
/**
* 显示查询结果
*/
var showResult = (name: string, positions: any, id: any) => {
var pointEntities: any = [];
var linesEntities: any = [];
var points: any = [];
for (const iterator of positions) {
var point = Cesium.Cartesian3.fromDegrees(iterator.x, iterator.y, iterator.z);
points.push(point);
pointEntities.push(createPoint(point));
}
console.log(pointEntities);
linesEntities.push(drawShape(points));
var customMarks = [];
for (const position of positions) {
const latitude = position.y;
const longitude = position.x;
const height = position.z;
customMarks.push({ lat: latitude, lng: longitude, height: height });
}
//点实体和线实体的集合
entities.push({
pointEntities: pointEntities,
linesEntities: linesEntities,
});
dataList.push({
id: id,
name: name,
positions: customMarks,
});
};
</script>
<style scoped>
.page {
position: absolute;
right: 10px;
top: 10px;
color: #fff;
background: #fff;
padding: 10px;
border-radius: 5px;
width: 400px;
}
</style>
4.问题
第一人称角度偏转旋转时还存在计算问题,需继续优化。