前一篇在学习使用UnReal的时候,了解到了一个非常好的概念 spring arm, 给相机加上这个组件后,能防止相机同步目标物体时,过于频繁的移动导致的抖动。于是乎,想到 天眼3d地图中由于数据频繁的位移抖动导致的视觉效果不佳,遂动手实践一番,写一个PigeonGL的防抖功能。
弹簧 - 拟物
说到防抖,最常见的就是电流按键防抖,简单暴力的去除按键接触电极时频繁的接通断开的电流毛刺。在web领域对应的就是搜索时,延时处理,防止频繁处理搜索请求。而在3d世界中的防抖,则是真真正正的要把抖动的毛刺变成一条平滑曲线而不是去除,所以我就yy了一下,想起来我的破旧小电驴过减震带的颠簸,轮子上的那个巨大的废物减震弹簧应该关系巨大,干脆造一个虚拟弹簧来实现spring arm,固定在相机和目标物体的上面连接起来,计算出由于物体移动位移造成弹簧末端的移动速度,做出类似于css中的ease-out的效果。
计算
弹簧高中物理学过一个 虎克公式 ,既弹簧绳长量和产生的拉力成正比
F = k * X
(F: 拉力 牛 , X: 位移 米)
弹簧的拉力和被拉开的位移成正比;
求出拉力F可以反向通过加速度公式;
F = m * a;
得出加速度 a = k * X / m;
另外又有速度公式
Vt = V0 + a * t;
最终得出
Vt = V0 + k * X / m;
映射速度
经过一系列的计算,得出了速度的最终函数,经过简化系数,可以抽象成下列函数
let v += speedRatio * x
其中x 可以理解为,目标物体从某个位置的到另一个位置间的距离 差 , 这里我设
X = targetCenter[0] - nowCenter[0];
Y = targetCenter[1] - nowCenter[1];
OK,这样我们可以计算出速度了!速度这个概念 对经常做js动画的人来说,可以理解为每一帧的运动距离,我们的速度单位为 m/frame (1frame = 1/fps s)因此可以得出每一帧的距离,接下来只需要设置定时函数,把地图的每一帧加上这个距离
this.eachX += this.speedRatio*(this.targetCenter[0] - this.nowCenter[0])*1/30;//30帧的时间 约1s
this.eachY += this.speedRatio*(this.targetCenter[1] - this.nowCenter[1])*1/30;
this.timeout = setTimeout(()=>{
this.nowCenter[0] += this.eachX;
this.nowCenter[1] += this.eachY;
},30);//每帧30ms
每次判断是否已经移动到目标点
if( (this.eachX>0?-1:1)*(this.nowCenter[0] - this.targetCenter[0])<0){
this.nowCenter = this.targetCenter;
this.eachX = this.eachY = 0;//到终点后,速度归零
this.pigeonMap.cameraControl.setCenter(this.nowCenter);
this.pigeonMap.cameraControl.updateCamera();
return;
}
//没有归零则继续加速
this.pigeonMap.cameraControl.setCenter(this.nowCenter);
this.pigeonMap.cameraControl.updateCamera();
this.toCenter();
然后要支持,弹簧伸缩到一半的时候,目标物体又发生移动,此时要根据当前的位移距离重新计算出加速度,然后累计到当前速度上。
this.nowCenter = this.pigeonMap.cameraControl.map.center;
this.targetCenter = newPosition;
this.eachX += this.speedRatio*(this.targetCenter[0] - this.nowCenter[0])*1/30;//30帧的时间
this.eachY += this.speedRatio*(this.targetCenter[1] - this.nowCenter[1])*1/30;
if(this.timeout)clearTimeout(this.timeout);
现在每当我重新设置目标位置时,就会重新获得拉力,把相机拉向小车,并且ease-out到小车位置
最后
实用主义者认为 有物理意义的 数学知识才是值得学习的!