今晚是双十一,祝你们剁手愉快啊明天还得作个快乐的打工人,哈哈_~算法
进入正题,最近要作个小地图显示,网上也有许多相关文章或技术实现,主要是经过一个额外的相机渲染出一张Textrue投送到UI上实现,可是在我这里的需求有点不同,须要选择到地图上的实际物体。所以,我就想直接使用相机渲染输出,通常小地图都是用正交相机,由此引起出如何自动改变改变正交相机的参数,从而使得想要被渲染的物体恰好在相机中的问题。c#
本篇文章主要就是解决上述问题,如何将Unity中正交相机的视野自动包裹住想要看到的物体。下面咱们先对相关概念进行介绍。函数
Unity中的相机你们确定都十分熟悉了,主要有两种摄像机,即透视摄像机(Perspective)和正交摄像机(Orthographic)。3d
透视摄像机是咱们通常默认的相机类型,它的视野窗口是一个四锥体,相机会根据离物体的远近而改变物体大小,就如同咱们的眼睛同样,以下图:code
正交摄像机的视野窗口则是一个长方体,它所看到的东西则是物体的投影,不会由于相机距离物体的远近而改变视野,还须要注意,若相机超过物体,那么相机仍是会不渲染物体,后面会讲到正交相机的高度设置问题,以下图:orm
正交相机因为以上特性,所以也比较适用于作2D游戏、制做小地图等用途。弄清楚上面简单的概念,咱们下面讲一下正交相机比较重要的参数,这些参数都是咱们要用到的。对象
提起正交相机,就不得不讲一下它的Size属性了,这个属性也是咱们要在后面自动修改的值。首先看一下这个值是什么含义,通常默认的正交相机的Size为5,以下图:blog
那么这个5表明什么意思呢?咱们在场景(0,0,0)点处放一个Cube,而后在(0,0,-10)处放正交相机,咱们先来看一下其完整渲染画面如何:游戏
观察上述截图,咱们知道Unity中标准的一个Cube长宽高都为1,那么在这个正交相机渲染的画面中,怎么得出Size为5的呢?下面咱们再来看一张图:源码
我在场景中又加了10个Cube,这样咱们就能够明显看出来,原来Size=5的意思是正交摄像机显示高度的一半尺寸为5。那么将相机的Size改成10看一下效果:
能够看到,如今在视野中,Cube组的上下各空出5个单位的距离。至此,关于正交摄像机的Size属性相信你已经很了解了,这个属性如何设置是咱们解决开头问题的一个关键。
Unity相机有一个通用属性aspect,这个属性摄像机显示区域的宽、高比,在其初始化的时候会默认设置成当前屏幕的宽高比,也能够经过改变相机的Rect来改变该值。
aspect值再结合2.2中正交相机的size含义,咱们就能够推算出正交相机渲染画面的大小,即画面高、宽分别为:
camera.height=camera.orthographicSize*2f
camera.width=height*camera.aspect
例如咱们刚才的例子,屏幕为1920*1080,相机的Viewport Rect为(0,0,1,1),则:
camera.aspect=(1920*1)/(1080*1)=1.77778
camera.height=5*2=10
camera.width=10*1.77778=17.7778
关于包围盒算法,网上有许多介绍,例如包围盒算法是一种求离散点集最优包围空间的方法。基本思想就是用体积稍大且特性简单的几何体(包围盒)来近似地代替复杂的集合对象。以下图,给三个物体生成了一个AABB包围盒的碰撞体(AABB包围盒定义为包含该对象,且边平行于坐标轴的最小六面体。还有其余几种包围盒的形式,咱们这里主要使用AABB包围盒)。
在这里,咱们只须要了解包围盒的概念就好,由于须要用包围盒来计算须要包围物体的范围是多少,从而计算正交相机的Size。Unity中的包围盒用结构体——Bounds来表示。再者注意上图为了示意包围盒,我将其作成了碰撞体显示出来。
解决咱们开头的问题,首先要分析一下须要解决什么问题:
针对第一个问题,问题的本质实际上是求物体(组)的包围盒,进而算得物体的正交投影大小。
求包围盒的算法咱们能够利用Unity中的API快速算出,思路就是利用物体(组)的Render组件来求出包围盒的中心点及边界信息,具体作法以下:
先将要计算包围盒的物体(组)放到统一个父物体下,例如上面的例子,包括Sphere、Cube和Capsule,以下图:
而后利用一下代码进行计算:
/// <summary> /// 获取物体包围盒 /// </summary> /// <param name="obj">父物体</param> /// <returns>返回该物体(组)的包围盒</returns> private Bounds GetBoundPointsByObj(GameObject obj) { var bounds = new Bounds(); if (obj != null) {//得到全部子物体的Render var renders = obj.GetComponentsInChildren<Renderer>(); if (renders != null) { //计算包围盒的中心点 var boundscenter = Vector3.zero; foreach (var item in renders) { boundscenter += item.bounds.center; } if (obj.transform.childCount > 0) boundscenter /= obj.transform.childCount; //新建一个包围盒 bounds = new Bounds(boundscenter, Vector3.zero); foreach (var item in renders) {//构建包围盒 bounds.Encapsulate(item.bounds); } } } return bounds; }
以上代码不难理解,就是先求最终包围盒的中心点,而后再从中心点开始逐步向外计算包围盒,bounds.Encapsulate(Bounds bounds)即为扩大包围盒函数。
根据以上方法,咱们就能够获得一个包围着Sphere、Cube和Capsule的包围盒,这个立方体包围盒确定是能够将这个物体组以最小六面体包围的。
由上一节中,咱们计算出来了物体组的包围盒,若是想使得正交相机的视野都包含该物体组,那么正交相机的位置确定为包围盒的中心点,或者说将该物体组放到正交相机的视野中心,以下图:
注意,由上图,咱们的这里的正交相机是对准x-y平面的,相机的深度方向在z轴上,所以在x-y平面上,相机若要在该物体组的中心点处,则:
camera.position.x = new Vector3(bound.center.x, bound.center.y, bound.center.z+k);
还观察到相机的z坐标加了一个数k,这个k是须要根据本身的状况来给定的,例如我这个例子中,相机在物体组的后面,所以k须要给定一个足够小的负值,不然相机跑到物体组的前面或里面的话,就不能彻底包围物体组了:
OK,咱们来看一下这个方案中关键的一点,如何设置正交相机的Size,先直接上代码来看一下:
public float ScreenScaleFactor;//占屏比例系数 /// <summary> /// 设置正交相机的Size /// </summary> /// <param name="xmin">包围盒x方向最小值</param> /// <param name="xmax">包围盒x方向最大值</param> /// <param name="ymin">包围盒y方向最小值</param> /// <param name="ymax">包围盒y方向最大值</param> private void SetOrthCameraSize(float xmin, float xmax, float ymin, float ymax) { float xDis = xmax - xmin;//x方向包围盒尺寸 float yDis = ymax - ymin;//y方向包围盒尺寸 float sizeX = xDis / ScreenScaleFactor / 2 / SetCamera.aspect; float sizeY = yDis / ScreenScaleFactor / 2; if (sizeX >= sizeY)//从X或Y方向选择一个合适的相机Size SetCamera.orthographicSize = sizeX; else SetCamera.orthographicSize = sizeY; }
这段代码量较少,可是要搞透仍是须要一些理解,简单来说,就是经过包围盒的平面尺寸来反推相机的Size是多少。
咱们先将上述式子中的ScreenScaleFactor=1。首先咱们回忆一下正交相机的Size是什么意思:Size为视野高度的一半。则若是想把物体组的Y方向尺寸所有包含到视野中,那么就有:
sizeY=yDis/2
那么为何又要算一个sizeX呢?由于sizeY实际上只适用于要包含物体组的高宽比大于1的状况(即高大于宽),而当物体组宽大于高的话,再利用sizeY来当作正交相机的Size就有可能显示不全。这也很好理解,要让相机包围物体组,那确定是选一个较大的边来处理。这样,由camera.aspect,sizeX就为:
sizeX=xDis/2/camera.aspect
咱们来作一个实验,新建一个Cube,当Cube的高为10,宽为1时,此时使用的是sizeY,显示以下:
当Cube的高为1,宽为10时,此时使用的是sizeX,显示以下:
OK,上面的内容理解的话,咱们再来看一下ScreenScaleFactor
参数,这个参数如今应该就很好理解了,其实它就是屏占比的意思,例如咱们在后一个例子上,将ScreenScaleFactor=0.8f,则有:
或者令ScreenScaleFactor=0.5f,则有:
根据上述例子,相信你们对ScreenScaleFactor这个比例系数的含义也明白了。
以上就是我对于正交相机只能包围物体(组)的解决方案,主要仍是理解其中的原理,下面附上完整源码:
public class Test : MonoBehaviour { public GameObject Obj;//要包围的物体 public Camera SetCamera;//正交相机 public float ScreenScaleFactor;//占屏比例系数 private void Start() { var bound = GetBoundPointsByObj(Obj); var center = bound.center; var extents = bound.extents; SetCamera.transform.position = new Vector3(center.x, center.y, center.z - 10); SetOrthCameraSize(center.x - extents.x, center.x + extents.x, center.y - extents.y, center.y + extents.y); } /// <summary> /// 获取物体包围盒 /// </summary> /// <param name="obj">父物体</param> /// <returns>物体包围盒</returns> private Bounds GetBoundPointsByObj(GameObject obj) { var bounds = new Bounds(); if (obj != null) { var renders = obj.GetComponentsInChildren<Renderer>(); if (renders != null) { var boundscenter = Vector3.zero; foreach (var item in renders) { boundscenter += item.bounds.center; } if (obj.transform.childCount > 0) boundscenter /= obj.transform.childCount; bounds = new Bounds(boundscenter, Vector3.zero); foreach (var item in renders) { bounds.Encapsulate(item.bounds); } } } return bounds; } /// <summary> /// 设置正交相机的Size /// </summary> /// <param name="xmin">包围盒x方向最小值</param> /// <param name="xmax">包围盒x方向最大值</param> /// <param name="ymin">包围盒y方向最小值</param> /// <param name="ymax">包围盒y方向最大值</param> private void SetOrthCameraSize(float xmin, float xmax, float ymin, float ymax) { float xDis = xmax - xmin; float yDis = ymax - ymin; float sizeX = xDis / ScreenScaleFactor / 2 / SetCamera.aspect; float sizeY = yDis / ScreenScaleFactor / 2; if (sizeX >= sizeY) SetCamera.orthographicSize = sizeX; else SetCamera.orthographicSize = sizeY; } }
写文不易~所以作如下申明:
1.博客中标注原创的文章,版权归原做者 煦阳(本博博主) 全部;
2.未经原做者容许不得转载本文内容,不然将视为侵权;
3.转载或者引用本文内容请注明来源及原做者;
4.对于不遵照此声明或者其余违法使用本文内容者,本人依法保留追究权等。