Cesium有很多很强大的功能,可以在地球上实现很多炫酷的3D效果。今天给大家分享一个第一人称漫游功能。

1.话不多说,先展示

【Cesium开发实战】第一视角漫游功能的实现,可设置漫游路径,飞行,暂停,继续,退出,删除路径_漫游

视频:第一人称漫游管理

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.问题

第一人称角度偏转旋转时还存在计算问题,需继续优化。