地球仪经线纬线上的经度纬度始终跟随摄像机

一.在做APP的AR项目中开发“认识地球仪”模块时,要实现在地球仪上显示经线和纬线,与经度和纬度,实现效果如下:

1.点击“认识经度”按钮,会在地球仪上依次显示经线和经度,手指在屏幕上左右滑动来查看不同视角。
2.点击“认识纬度”按钮,会在地球仪上依次显示纬线和纬度,手指在屏幕上左右滑动来查看不同视角。

可能有人已经看出来了,旋转地球的时候有些角度会被挡住。对,我就是为了解决它,使其变的更优雅。接着看下图:

3
4


这样的话,不管你怎么旋转视角查看地球仪,线上的角度始终跟随摄像机,可见视角区域始终可以看到相应角度,实在不能够被显示的也会渐隐消失来尽量优雅,这样是不是比上面体验更好,更优雅呢。

优雅的同时也确实增加了不少开发工作量,开发起来特别考验开发者对3D几何向量知识的掌握,下面就来介绍一下我的思路和实现:

图1,2实现起来就比较简单了,一般unity开发者都能搞定,就是把3d字体(Unity 3d Text)摆放到固定位置就可以了,就不做细赘述了。

我们来主要看图3,4,角度跟随摄像机是如何实现的? 为便于理解后面的程序实现,还是要介绍一下需求和一些相关的地理知识(若有地理术语使用不当的地方请多包涵,因着重是从产品需求和程序开发方面来介绍的):

经度,经度的话就是标记地球上经线所在位置的角度(就是经线上显示度数),经线之间相隔15°共24条。度数显示在线的中间位置(也是纬度0°的位置)。如图,当摄像机与地球仪平行时正好看到的是经纬度都是0°的位置(通俗的说也就是在球的半腰位置,也就是在摄像机视野中间),问题?这个时候随着摄像机向上看的话,显示经线上的角度变得越来越靠下最后就被球体档住了,如图

地球仪经线纬线上的经度纬度始终跟随摄像机_第1张图片
地球仪经线纬线上的经度纬度始终跟随摄像机_第2张图片
地球仪经线纬线上的经度纬度始终跟随摄像机_第3张图片
地球仪经线纬线上的经度纬度始终跟随摄像机_第4张图片

纬度,纬度的话就是标记地球仪上纬线位置的角度,纬线之间也是相隔15°共11条(11个环),每条纬线的度数显示在纬线经度0°的位置,如下图,摄像机中心区域显示的正好是经纬0°位置,问题,当左右移动摄像机某个角度,纬线上的度数也会被挡住(描述问题跟经度遇到情况一样所有下面就只截一张图)。

地球仪经线纬线上的经度纬度始终跟随摄像机_第5张图片


以上这些提到的问题就是需要改善的地方,下面是具体实现:

参考下图1,2,首先是在场景中的地球仪下创建空物体WeftCtrl,在再WeftCtrl下面创建三个子物体分别:

DegreeGroup 用来存放角度位置的空物体,也可以是Unity 3d Text 物体。注意:因为后面用到的是UI, 所以这个下面是只带Transform的空物体;

Center 地球仪中心的位置

Axle 用于计算角度参考的轴位置

1.

地球仪经线纬线上的经度纬度始终跟随摄像机_第6张图片

2.

地球仪经线纬线上的经度纬度始终跟随摄像机_第7张图片

3.

地球仪经线纬线上的经度纬度始终跟随摄像机_第8张图片

图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显示的度数,下面是经纬度跟随摄像机实现部分:

下图显示初始化效果,坐标系分别是摄像机和地球仪位置,设定“地球仪的坐标系”和“摄像机的坐标系”垂直方向一致。

地球仪经线纬线上的经度纬度始终跟随摄像机_第9张图片

因为是有AR功能,地球仪旋转被AR依赖,我只能在地球仪物体下创建WeftCtrl和WarpCtrl,分别调整他们和摄像机坐标系垂直方向一致,同样也可以达到目的。

地球仪经线纬线上的经度纬度始终跟随摄像机_第10张图片

接着调整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();

}

}

在计算纬度时没有采取通过和摄像机夹角开判断,是因为场景中物体嵌套层级太多,不太方便计算。

判断距离开计算会更简单方便。

这是在上的第一篇博客,请大家多支持,描述不清楚点多多指正。

你可能感兴趣的:(地球仪经线纬线上的经度纬度始终跟随摄像机)