接上次的视频投影,Leader告诉我这个视频投影要用在两个地方,一个是我原先写的轨迹回放那里,另一个在无人机起飞后的地图回显,要实时播放无人机拍摄的视频,还要能转镜头,让我把这个也接一下。
我的天!告诉我的时候人都傻了,这是一个功能嘛?
一个是拿到了全部的轨迹数据进行回显,播放的视频也是完整的资源,视频要求投射在地面上。另一个是接收实时的轨迹数据进行回显,播放的是实时的直播,视频居然还要求跟着镜头一起转。
这是两个完完全全不一样的功能好吧!!!
我拿着标书仔仔细细看过那行"演示无人机画面投影到地图上",一时间陷入了沉默。
无语归无语,但特么还是要写,跟Leader据理力争这不是改改就能换上的功能,然后争取来两周的研发时间。
两周时间看起来很长,其实也就10天,有时候我写个计算方法都要两天,还不一定是最终版,所以只能拜托接下来运气很好,别让我遇到太多阻碍。
开工吧!
Step 0:
思路:最初都是想要拿原来的方法改改看,哪怕不成功,也绝了这个念想。
首先要解决的是视频投放的问题,原本这个投影方法只能投放视频,且只能投影到地面,我要优化成能投影直播并且不接触地面也能投放。
为了解决直播投放问题,我尝试修改了源码,发现困难重重,主要是两点,一个是原方法不依赖dom元素,这意味着我不能暴力篡改成直播通道,另一个是封装层级过多,我想在某几个关键步骤看下效果都不能被满足,只能盲写看最终效果。
卡在第一步我是万万没有想到,经过一下午的尝试,最终让我放弃了继续下去的想法。
Step0.1:当然,我也没有急着完全放弃之前的代码,我还记得我czml的无人机轨迹方法已经很完善了,从静态路径加载改为动态路径回显似乎并不难,如果成功的话只需要给视频材质连四根线,接着解决朝向转动的问题就完成了,大大减轻了工作量。
我将原有轨迹数据做成坐标发射器,通过动态接收点位组成路径,每次接收到数据就更新czml加载的点位集合,成功改成了动态路径回显。
然后我尝试将我之前做好的视椎体视频替换进去,然后切换朝向,发现在dataSource.then中格外难操作矩阵,需要多考虑很多问题,于是暂时搁置该方法,等待重启机会。
旧路已死,新步骤开始从0开始一点点搭建功能。
Step 1:
思路: 选择实体结合时间轴,更为灵活地构建动画,同时代码将更繁琐。
首先写一段简单动画,从一个点到另一个点。
部分代码:
javascript
var startPosition = Cesium.Cartesian3.fromDegrees(-75.0, 40.0, 1000.0);
var endPosition = Cesium.Cartesian3.fromDegrees(-75.0, 42.0, 1000.0);
// 设置时间范围
var startTime = Cesium.JulianDate.now();
var endTime = Cesium.JulianDate.addSeconds(
startTime,
15,
new Cesium.JulianDate()
);
// 创建一个 SampledPositionProperty 来定义路径
var positionProperty = new Cesium.SampledPositionProperty();
// 添加时间和位置样本
positionProperty.addSample(startTime, startPosition);
positionProperty.addSample(endTime, endPosition);
// 创建一个 entity 并设置其 position 为定义的路径
var entity = viewer.entities.add({
position: positionProperty,
point: {
pixelSize: 10,
color: Cesium.Color.RED,
},
});
// 使视图跟踪 entity
viewer.trackedEntity = entity;
// 设置时钟范围
viewer.clock.startTime = startTime.clone();
viewer.clock.stopTime = endTime.clone();
viewer.clock.currentTime = startTime.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; // 当到达结束时间时停止
viewer.clock.multiplier = 1; // 时间加速倍率
viewer.clock.clockRange = Cesium.ClockRange.CLAMPED;
viewer.clock.shouldAnimate = true;
Step 2:
思路:两个点变多个点,写一个点位生成器,模拟实时接收数据,实现多点位连续飞行
Step 3:
思路:模拟点位飞行,点换成模型,视椎体跟随模型一起,并封装成class,效果很好,但视角连贯性有待提高
部分代码:
javascript
class PointMover {
constructor(viewer) {
this.viewer = viewer;
this.pointQueue = [];
this.isAnimating = false;
this.videoEntity = null;
// 添加模型
this.entity = ...
// 添加视椎体
this.frustumPrimitive = viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: geo,
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.RED.withAlpha(0.5)
),
},
}),
appearance: new Cesium.PerInstanceColorAppearance({
translucent: true,
flat: true,
}),
asynchronous: false,
})
);
// 初始化视频材质
this.videoEntity = viewer.entities.add({
name: "uav-tmp-fly-wxsimple",
polygon: {
hierarchy: new Cesium.CallbackProperty((time) => {
// 添加安全检查
if (!this.entity || !this.entity.position) {
return null;
}
try {
const position = this.entity.position.getValue(time);
if (!Cesium.defined(position)) {
return null;
}
// 计算视锥体的远截面四个角点
// 返回新的多边形层次结构
return new Cesium.PolygonHierarchy([
upRightPt,
upLeftPt,
downLeftPt,
downRightPt,
]);
} catch (error) {
console.warn("Error calculating polygon positions:", error);
return null;
}
}, false),
perPositionHeight: true,
material: videoElement,
// 添加其他属性以提高渲染性能
shadows: Cesium.ShadowMode.DISABLED,
classificationType: Cesium.ClassificationType.BOTH,
zIndex: 999,
},
});
// 内部方法:启动动画
startAnimation() {
if (this.pointQueue.length > 2 && !this.isAnimating) {
// 在 onTick 事件处理函数中更新视锥体位置
const onTick = (clock) => {
// 移除旧的视锥体
// 添加新的视锥体
// ...
}
// 添加 onTick 事件监听器
this.viewer.clock.onTick.addEventListener(onTick);
}
}
// 外部调用方法:更新点位并启动动画
updatePositionsAndAnimate(newPosition) {
this.pointQueue.push(newPosition);
if (!this.isAnimating) {
this.startAnimation();
}
}
}
}
Step 4:
思路:加入虚线路径
部分代码:
javascript
class PointMover {
constructor(viewer) {
this.viewer = viewer;
this.pointQueue = [];
this.isAnimating = false;
this.videoEntity = null;
this.pathPoints = []; // 用于记录路径点
this.pathPolyline = null; // 用于绘制路径
// ...
}
// 绘制路径的方法
createPathPolyline() {
this.pathPolyline = this.viewer.entities.add({
polyline: {
positions: new Cesium.CallbackProperty((time) => {
// 取当前记录的路径点
return this.pathPoints;
}, false),
material: new Cesium.PolylineDashMaterialProperty({
dashLength: 16.0, // 虚线的长度
}),
width: 3,
},
});
}
// 更新路径并添加新的位置
updatePath(newPosition) {
this.pathPoints.push(newPosition); // 将新位置加入到路径点
// 更新虚线的路径
if (this.pathPolyline) {
this.pathPolyline.polyline.positions = new Cesium.CallbackProperty(
(time) => {
return this.pathPoints;
},
false
);
}
}
// 内部方法:启动动画
startAnimation() {
if (this.pointQueue.length > 2 && !this.isAnimating) {
// 在 onTick 事件处理函数中更新视锥体位置
const onTick = (clock) => {
// 将当前位置添加到路径数组
this.updatePath(currentPosition);
//...
}
}
}
}
Step 4延伸:
思路:在Step 4基础上尝试扩展,显隐,销毁 show的配合
部分代码:
javascript
class PointMover {
constructor(viewer, show) {
this.viewer = viewer;
this.pointQueue = [];
this.isAnimating = false;
this.videoEntity = null;
this.pathPoints = [];
this.pathPolyline = null;
this.isFrustumShow = show; //用于显隐控制
// ...
if (this.frustumPrimitive) {
if (!this.isFrustumShow) {
this.frustumPrimitive.show = false;
} else {
this.frustumPrimitive.show = true;
}
}
if (this.videoEntity) {
if (!this.isFrustumShow) {
this.videoEntity.show = false;
} else {
this.videoEntity.show = true;
}
}
}
// 内部方法:启动动画
startAnimation() {
if (this.pointQueue.length > 2 && !this.isAnimating) {
// 在 onTick 事件处理函数中更新视锥体位置
const onTick = (clock) => {
// 将当前位置添加到路径数组
//...
this.frustumPrimitive = this.viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: newGeometry,
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.RED.withAlpha(0.5)
),
},
}),
appearance: new Cesium.PerInstanceColorAppearance({
translucent: true,
flat: true,
}),
asynchronous: false,
})
);
if (this.frustumPrimitive) {
if (!this.isFrustumShow) {
this.frustumPrimitive.show = false;
} else {
this.frustumPrimitive.show = true;
}
}
if (this.videoEntity) {
if (!this.isFrustumShow) {
this.videoEntity.show = false;
} else {
this.videoEntity.show = true;
}
}
}
}
}
// ...
// 外部调用方法,更新显隐开关
updateVisibleAndHidden(val) {
this.isFrustumShow = val;
}
dispose() {
if (this.frustumPrimitive) {
viewer.scene.primitives.remove(this.frustumPrimitive);
this.frustumPrimitive = null;
}
if (this.videoEntity) {
viewer.entities.remove(this.videoEntity);
this.videoEntity = null;
}
if (this.entity) {
viewer.entities.remove(this.entity);
this.entity = null;
}
if (this.pathPolyline) {
viewer.entities.remove(this.pathPolyline);
this.pathPolyline = null;
}
}
}
if (this.pointMover) {
clearInterval(this.intervalId);
this.intervalId = null;
this.pointMover.dispose();
this.pointMover = null;
}
this.pointMover = new PointMover(viewer, this.isFrustumShow);
Step 5:
思路:尝试改变视椎体朝向
部分代码:
javascript
class PointMover {
constructor(viewer) {
// ...
this.currentHeading = 0;
this.currentPitch = 0;
this.currentRoll = 0;
// ...
}
// 内部方法:启动动画
startAnimation() {
if (this.pointQueue.length > 2 && !this.isAnimating) {
// 在 onTick 事件处理函数中更新视锥体位置
const onTick = (clock) => {
const heading = Cesium.Math.toRadians(this.currentHeading); // 偏航角
const pitch = Cesium.Math.toRadians(this.currentPitch); // 俯仰角
const roll = Cesium.Math.toRadians(this.currentRoll); // 翻滚角
// 创建一个HeadingPitchRoll对象
const headingPitchRoll = new Cesium.HeadingPitchRoll(
heading,
pitch,
roll
);
// 创建一个旋转矩阵
const rotationMatrix =
Cesium.Transforms.headingPitchRollToFixedFrame(
currentPosition,
headingPitchRoll,
Cesium.Ellipsoid.WGS84
);
// 从旋转矩阵计算四元数
const orientation =
Cesium.Quaternion.fromRotationMatrix(rotationMatrix);
//...
}
}
}
// ...
updateHeadingPitchRoll(heading, patch, roll) {
this.currentHeading = heading;
this.currentPitch = patch;
this.currentRoll = roll;
}
}
Step 5延伸:
思路:在Step 5基础上尝试,同样矩阵转动视频材质
发现视频材质无法贴合视椎体,方案终止
Step 6:
思路:换用另一种视椎体构建方法,采用相机视角重置视椎体视角的方法,消除地理位置的影响
部分代码:
javascript
class PointMover {
constructor(viewer) {
// ...
this.videoEntity = viewer.entities.add({
name: "uav-tmp-fly-wxsimple",
polygon: {
hierarchy: new Cesium.CallbackProperty((time) => {
if (!this.entity || !this.entity.position) {
return null;
}
try {
const position = this.entity.position.getValue(time);
if (!Cesium.defined(position)) {
return null;
}
let frustum = this.camera.frustum;
let Cartesian3 = Cesium.Cartesian3;
let camera = this.camera;
// ...
return new Cesium.PolygonHierarchy([
upRightPt,
upLeftPt,
downLeftPt,
downRightPt,
]);
} catch (error) {
console.warn("Error calculating polygon positions:", error);
return null;
}
}, false),
perPositionHeight: true,
material: new Cesium.ImageMaterialProperty({
image: videoElement, // 这里传入视频元素
transparent: true, // 设置透明
repeat: new Cesium.Cartesian2(1.0, 1.0), // 控制重复
}),
shadows: Cesium.ShadowMode.DISABLED,
classificationType: Cesium.ClassificationType.BOTH,
zIndex: 999,
},
});
}
// 内部方法:启动动画
startAnimation() {
if (this.pointQueue.length > 2 && !this.isAnimating) {
// 在 onTick 事件处理函数中更新视锥体位置
const onTick = (clock) => {
var scene = this.viewer.scene;
this.camera = new Cesium.Camera(scene)
//...
}
}
}
// ...
}
最终
接入实际项目,无人机航拍镜头实时同步反显,完结撒花°˖✧◝(⁰▿⁰)◜✧˖°