一.在做APP的AR项目中开发“认识地球仪”模块时,要实现在地球仪上显示经线和纬线,与经度和纬度,实现效果如下:
可能有人已经看出来了,旋转地球的时候有些角度会被挡住。对,我就是为了解决它,使其变的更优雅。接着看下图:
这样的话,不管你怎么旋转视角查看地球仪,线上的角度始终跟随摄像机,可见视角区域始终可以看到相应角度,实在不能够被显示的也会渐隐消失来尽量优雅,这样是不是比上面体验更好,更优雅呢。
优雅的同时也确实增加了不少开发工作量,开发起来特别考验开发者对3D几何向量知识的掌握,下面就来介绍一下我的思路和实现:
图1,2实现起来就比较简单了,一般unity开发者都能搞定,就是把3d字体(Unity 3d Text)摆放到固定位置就可以了,就不做细赘述了。
我们来主要看图3,4,角度跟随摄像机是如何实现的? 为便于理解后面的程序实现,还是要介绍一下需求和一些相关的地理知识(若有地理术语使用不当的地方请多包涵,因着重是从产品需求和程序开发方面来介绍的):
经度,经度的话就是标记地球上经线所在位置的角度(就是经线上显示度数),经线之间相隔15°共24条。度数显示在线的中间位置(也是纬度0°的位置)。如图,当摄像机与地球仪平行时正好看到的是经纬度都是0°的位置(通俗的说也就是在球的半腰位置,也就是在摄像机视野中间),问题?这个时候随着摄像机向上看的话,显示经线上的角度变得越来越靠下最后就被球体档住了,如图
纬度,纬度的话就是标记地球仪上纬线位置的角度,纬线之间也是相隔15°共11条(11个环),每条纬线的度数显示在纬线经度0°的位置,如下图,摄像机中心区域显示的正好是经纬0°位置,问题,当左右移动摄像机某个角度,纬线上的度数也会被挡住(描述问题跟经度遇到情况一样所有下面就只截一张图)。
以上这些提到的问题就是需要改善的地方,下面是具体实现:
参考下图1,2,首先是在场景中的地球仪下创建空物体WeftCtrl,在再WeftCtrl下面创建三个子物体分别:
DegreeGroup 用来存放角度位置的空物体,也可以是Unity 3d Text 物体。注意:因为后面用到的是UI, 所以这个下面是只带Transform的空物体;
Center 地球仪中心的位置
Axle 用于计算角度参考的轴位置
1.
2.
3.
图3,生成对应纬度UI,其实第一部分git图看到的效果就是用UI,好处是不管视角怎么反转,纬度文字都是正常的,而用3d字体就会出现旋转字体倾斜,用UI的话会更好。不过仅需要考虑一点,要把3D空间坐标点映射到屏幕UI点上(后面也会贴转换的代码,网上这样的代码也有有很多可以借鉴);
DegreeGroup 下面的物体是通过代码动态生成的,生成纬度代码片段:
//在编辑模式“[ExecuteInEditMode]”下执行一次,生成经纬度对应物体,然后保存场景文件,以后运行场景就不需要再创建了,好处是场景加载时减速cpu计算量;
void CreateWeftDegree(){
UIContainer.RemoveAllChild();
degreeGroup.RemoveAllChild();
degreeFades = new DegreeFade[setDegreeFadeLen];
int _degree_i = 0;
var prefabs = Resources.Load("Prefabs/Degree");
CreateDegreePoint((pos,degree)=>{
//Create Empty object
GameObject newGo = new GameObject();
newGo.name = "degree_"+degree;
newGo.transform.localPosition = pos;
newGo.transform.SetParent(degreeGroup,false);
newGo.transform.localScale = Vector3.one*0.02f;
//create UI object
GameObject UIGo = Instantiate(prefabs) as GameObject;
UIGo.transform.SetParent(UIContainer,false);
UIGo.name = newGo.name;
degree = degree<0?degree*-1:degree;
UIGo.GetComponent().text = degree+"°";
DegreeFade df = UIGo.AddComponent();
df.followerTransform = newGo.transform;
degreeFades[_degree_i++] = df;
ScreenGameObjectFollower follower = UIGo.AddComponent();
follower.UICamera = mainCamera;
follower.followObject = newGo;
UIGo.SetActive(false);
});
}
//计算纬度
protected override void CreateDegreePoint(System.Action fun){
float stepDegree = 15;
setDegreeFadeLen = 11;
Vector3 pos;
var size = gameObject.GetComponent().sharedMesh.bounds.size;
var scale = transform.localScale;
var radius = (size.x * scale.x)/2;
for(int i=0;i
if(i<6){
pos = GetEarthPointBy(radius,0,i*stepDegree)/transform.localScale.x;
fun(pos,i*stepDegree);
}else{
pos = GetEarthPointBy(radius,0,(5-i)*stepDegree)/transform.localScale.x;
fun(pos,(5-i)*stepDegree);
}
}
}
//计算经度
protected override void CreateDegreePoint(System.Action fun){
float stepDegree = 15;
setDegreeFadeLen=24;
Vector3 pos;
var size = gameObject.GetComponent().sharedMesh.bounds.size;
var scale = transform.localScale;
var radius = (size.x * scale.x)/2;
for(int i=0;i
if(i<13){
pos = GetEarthPointBy(radius,i*stepDegree,0)/transform.localScale.x;
fun(pos,i*stepDegree);
}else{
pos = GetEarthPointBy(radius,(12-i)*stepDegree,0)/transform.localScale.x;
fun(pos,(12-i)*stepDegree);
}
}
}
上面这两个方法是用来创建经度和纬度在地球仪上显示的位置和UI显示的度数,下面是经纬度跟随摄像机实现部分:
下图显示初始化效果,坐标系分别是摄像机和地球仪位置,设定“地球仪的坐标系”和“摄像机的坐标系”垂直方向一致。
因为是有AR功能,地球仪旋转被AR依赖,我只能在地球仪物体下创建WeftCtrl和WarpCtrl,分别调整他们和摄像机坐标系垂直方向一致,同样也可以达到目的。
接着调整WeftCtrl坐标系和摄像坐标系朝向相同,中心点调整到地球仪中心位置(0,0,0),在WeftCtrl物体下创建子物体Axle,调整Axle自身到Y轴的(0,0.5,0)位置。有了WeftCtrl中心位置、Axle位置、Camera位置,就可以计算WeftCtrl到Axle差向量“A”和WeftCtrl到Camera差向量“B”, 然后用Vector3f.Angle(A,B)求出两个差向量的角度---当然也可以用几何公式向量叉乘来计算。Unity已经提供现成API使用会更方便。
实现经度跟随摄像机就只需要下面核心的两个方法:
第一个OnUpdate()是摄像机视角有产生变化时调用的,而不是在脚本的Update中一直在调用,避免放update下一直调用,消耗性能。
计算经过地球一圈的所有点位置(计算360个点),每度点与摄像机距离,根据距离排序找出最近距离点的度数,根据最近的度数通过GetEarthPointBy()计算每个经度的位置。
public override void OnUpdate(Vector3 cameraPosition){
var angle = 360;
var stepAngle = 1;
var len = angle/stepAngle;
float[] distances = new float[angle/stepAngle];
float Radius = Vector3.Distance(Axle.position,center.position);
int j=0;
for(int i= 0;i
if(i>=len/2){
j = (angle-i)*stepAngle*-1;
}else{
j = i*stepAngle;
}
distances[i]=Vector3.Distance(cameraPosition,(degreeGroup.position + degreeGroup.rotation*(GetEarthPointBy(Radius,j,0))));
}
距离摄像机最近的纬度
int idx = ArrayUtility.FindSize(distances,(t,min)=>{
return t < min ;
});
if(idx>=len/2){
idx = (angle-idx)*stepAngle*-1;
}else{
idx *=stepAngle;
}
ArrayUtility.Foreach(degreeFades,(df)=>{
df.OnUpdate(degreeGroup.position,cameraPosition);
var parent = df.followerTransform.parent;
if(df.gameObject.name.Equals("S") || df.gameObject.name.Equals("N"))
return ;
df.followerTransform.position = parent.position + parent.rotation*GetEarthPointBy(Radius,idx,df.angle);
});
}
第二个OnUpdate()是判断度数不在摄像机可见范围内时计算渐隐消失,回到可见区再渐显;
public void OnUpdate (Vector3 earthPosition,Vector3 cameraPositon) {
根据当前纬度位置、地球中心位置、摄像机位置求出夹角
var degree = Vector3.Angle(followerTransform.position-earthPosition,cameraPositon-earthPosition);
var distance = Vector3.Distance(followerTransform.position,cameraPositon);
SetTextSize(calculateTextSize(distance));
判断夹角控制纬度文字渐隐、渐显
if(degree >= 70 && !isFade){
isFade = true;
FadeOut();
}else if(degree <= 70 & isFade){
isFade=false;
FadeIn();
}
}
纬度计算跟上面部分类似,也是两个方法:
public override void OnUpdate (Vector3 cameraPosition) {
通过Y轴位置、地球中心位置、摄像机位置,计算他们的夹角
var angle = Vector3.Angle(Axle.position-center.position,cameraPosition-center.position);
if(angle>=90){
angle = (90 - (180 - angle))*-1;
if(angle < -60)
angle = -60;
}else if(angle < 90 && angle >= 0){
angle = 90-angle;
if(angle > 60)
angle = 60;
}
float radius = Vector3.Distance(degreeGroup.GetChild(0).position,degreeGroup.position);
ArrayUtility.Foreach(degreeFades,(df)=>{
df.OnUpdate(degreeGroup.position,cameraPosition);
var parent = df.followerTransform.parent;
df.followerTransform.position = parent.position + parent.rotation*GetEarthPointBy(radius,df.angle,angle);
});
}
public void OnUpdate (Vector3 earthPosition,Vector3 cameraPositon) {
根据当前纬度位置、地球中心位置、摄像机位置求出夹角
var degree = Vector3.Angle(followerTransform.position-earthPosition,cameraPositon-earthPosition);
var distance = Vector3.Distance(followerTransform.position,cameraPositon);
SetTextSize(calculateTextSize(distance));
判断夹角控制纬度文字渐隐、渐显
if(degree >= 70 && !isFade){
isFade = true;
FadeOut();
}else if(degree <= 70 & isFade){
isFade=false;
FadeIn();
}
}
在计算纬度时没有采取通过和摄像机夹角开判断,是因为场景中物体嵌套层级太多,不太方便计算。
判断距离开计算会更简单方便。
这是在简书上的第一篇博客,请大家多支持,描述不清楚点多多指正。