Unity正交相机智能包围物体(组)方案

一、技术背景

今晚是双十一,祝大家剁手愉快啊明天还得做个快乐的打工人,哈哈_~

进入正题,最近要做个小地图显示,网上也有许多相关文章或技术实现,主要是通过一个额外的相机渲染出一张Textrue投送到UI上实现,但是在我这里的需求有点不一样,需要选择到地图上的实际物体。因此,我就想直接使用相机渲染输出,一般小地图都是用正交相机,由此引发出如何自动改变改变正交相机的参数,从而使得想要被渲染的物体刚好在相机中的问题。

本篇文章主要就是解决上述问题,如何将Unity中正交相机的视野自动包裹住想要看到的物体。下面我们先对相关概念进行介绍。

二、相关概念

2.1 正交摄像机

Unity中的相机大家肯定都十分熟悉了,主要有两种摄像机,即透视摄像机Perspective)和正交摄像机Orthographic)。

透视摄像机是我们一般默认的相机类型,它的视野窗口是一个四锥体,相机会根据离物体的远近而改变物体大小,就如同我们的眼睛一样,如下图:

正交摄像机的视野窗口则是一个长方体,它所看到的东西则是物体的投影,不会因为相机距离物体的远近而改变视野,还需要注意,若相机超过物体,那么相机还是会不渲染物体,后面会讲到正交相机的高度设置问题,如下图:

正交相机由于以上特性,因此也比较适用于做2D游戏、制作小地图等用途。弄清楚上面简单的概念,我们下面讲一下正交相机比较重要的参数,这些参数都是我们要用到的。

2.2 正交相机的Size

提起正交相机,就不得不讲一下它的Size属性了,这个属性也是我们要在后面自动修改的值。首先看一下这个值是什么含义,一般默认的正交相机的Size为5,如下图:

那么这个5代表什么意思呢?我们在场景(0,0,0)点处放一个Cube,然后在(0,0,-10)处放正交相机,我们先来看一下其完整渲染画面如何:

观察上述截图,我们知道Unity中标准的一个Cube长宽高都为1,那么在这个正交相机渲染的画面中,怎么得出Size为5的呢?下面我们再来看一张图:

我在场景中又加了10个Cube,这样我们就可以明显看出来,原来Size=5的意思是正交摄像机显示高度的一半尺寸为5。那么将相机的Size改为10看一下效果:

可以看到,现在在视野中,Cube组的上下各空出5个单位的距离。至此,关于正交摄像机的Size属性相信你已经很了解了,这个属性如何设置是我们解决开头问题的一个关键。

2.3 相机的Aspect

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

2.4 包围盒

关于包围盒算法,网上有许多介绍,例如包围盒算法是一种求离散点集最优包围空间的方法。基本思想就是用体积稍大且特性简单的几何体(包围盒)来近似地代替复杂的集合对象。如下图,给三个物体生成了一个AABB包围盒的碰撞体(AABB包围盒定义为包含该对象,且边平行于坐标轴的最小六面体。还有其他几种包围盒的形式,我们这里主要使用AABB包围盒)。

在这里,我们只需要了解包围盒的概念就好,因为需要用包围盒来计算需要包围物体的范围是多少,从而计算正交相机的Size。Unity中的包围盒用结构体——Bounds来表示。再者注意上图为了示意包围盒,我将其做成了碰撞体显示出来。

三、解决方案

解决我们开头的问题,首先要分析一下需要解决什么问题:

  • 求得物体(组)的正交投影范围;
  • 移动正交相机到物体组上方的中心位置,并自动调整Size。

针对第一个问题,问题的本质其实是求物体(组)的包围盒,进而算得物体的正交投影大小。

3.1 求物体的包围盒

求包围盒的算法我们可以利用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的包围盒,这个立方体包围盒肯定是可以将这个物体组以最小六面体包围的。

3.2 正交相机参数设置——位置、Size

3.2.1 正交相机位置计算

由上一节中,我们计算出来了物体组的包围盒,如果想使得正交相机的视野都包含该物体组,那么正交相机的位置肯定为包围盒的中心点,或者说将该物体组放到正交相机的视野中心,如下图:

注意,由上图,我们的这里的正交相机是对准x-y平面的,相机的深度方向在z轴上,因此在x-y平面上,相机若要在该物体组的中心点处,则:

camera.position.x = new Vector3(bound.center.x, bound.center.y, bound.center.z+k);

还观察到相机的z坐标加了一个数k,这个k是需要根据自己的情况来给定的,例如我这个例子中,相机在物体组的后面,因此k需要给定一个足够小的负值,否则相机跑到物体组的前面或里面的话,就不能完全包围物体组了:

3.2.2 正交相机Size计算

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.对于不遵守此声明或者其他违法使用本文内容者,本人依法保留追究权等。

Unity正交相机智能包围物体(组)方案的更多相关文章

  1. Unity 3D 正交相机(Orthographic)

    1. Camera.aspect 表示摄像机显示区域的纵横比.宽高比,摄像机初始化的时候会默认设置成当前屏幕的宽高比,可以更改,也可以通过 Camera.ResetAspect 来重置. 2. Cam ...

  2. 一款新型的智能家居WiFi选择方案——SimpleWiFi在无线智能家居中的应用

    一款新型的智能家居WiFi选择方案——SimpleWiFi在无线智能家居中的应用 先上图:     随着科学技术的不断发展,局域网也正逐渐向无线化,多网合一的方向发展,在这个多网合一快速发展过程中,带 ...

  3. unity平行光太亮?物体发白?可能你使用了2个或多个平行光

    unity平行光太亮?物体发白?可能你使用了2个或多个平行光 今天做项目时就遇到了这个问题,光亮得让物体发白 发现加载的场景 里面有个 平行光,删了就好了 要是感觉还是太亮,就把主平行光的Intens ...

  4. 【转】UNITY中相机空间,投影空间的正向问题

    原文链接1:https://www.cnblogs.com/wantnon/p/4570188.html 原文链接2:https://www.cnblogs.com/hefee/p/3820610.h ...

  5. Unity设置相机正交相机和透视相机的动态切换

    Camera.main.orthographic = true;    Camera.main.orthographicSize = 4;    Camera.main.orthographic = ...

  6. 介绍Unity中相机的投影矩阵与剪切图像、投影概念

    这篇作为上一篇的补充介绍,主要讲Unity里面的投影矩阵的问题: 上篇的链接写给VR手游开发小白的教程:(三)UnityVR插件CardboardSDKForUnity解析(二) 关于Unity中的C ...

  7. 超低功耗、无需网关,CSR智能家居蓝牙控制照明方案

    本文转载至 http://blog.csdn.net/justinjing0612/article/details/39250997 [导读] iOS 8 Beta2终于让智能家居HomeKit功能露 ...

  8. Unity 个人用过的地面检测方案总结

    Unity 个人用过的地面检测方案总结 1.普通射线 在角色坐标(一般是脚底),发射一根向下的射线,长度大约为0.2, 只适用于简单地形,实际使用中常常遇到以下问题 用的collider去碰撞地面时, ...

  9. Unity 场景中看不到物体或者OnDrawGizmos画的线看不到

    有时候,Unity中的场景里面,物体突然看不见了,可以这样做:     首先,在 Hierarchy 面板选择看不见的物体,按下快捷键 f.如果物体还是看不见,见下图: 看看图中圈红的地方.如果,如果 ...

随机推荐

  1. ==38254==Sanitizer CHECK failed报错解决

    跑代码时发现有如下报错: LeakSanitizer: bad pointer 0x7ffd00735130==38254==Sanitizer CHECK failed: ../../../../l ...

  2. shell脚本中,关于if,以及条件判断

    #!/bin/sh SYSTEM=`uname -s` #获取操作系统类型 if [ $SYSTEM = "Linux" ] ; then #如果是linux的话打印linux字符 ...

  3. 关于Elasticsearch版本升级,Kibana报index迁移与需要x-pack插件问题

    关于Elasticsearch版本升级,Kibana报index迁移与需要x-pack插件问题 这个问题是由于elasticsearch旧版残留文件导致,使用下述指令删除即可 查看所有elastics ...

  4. DE2资源集锦

    1.The School of Electrical and Computer Engineering (ECE) at the Georgia Institute of Technology:htt ...

  5. 多测师讲解selenium_运行报告相出错归纳_高级讲师肖sir

    <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'> EETraceback (most recent c ...

  6. git的一些操作命令

    一,如何修改一个commit的注释? root@kubuntu:/data/git/clog# git commit --amend 说明:架构森林是一个专注架构的博客,地址:https://www. ...

  7. centos8平台编译安装nginx1.18.0

    一,nginx的官网: http://nginx.org/ 说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest 对应的源码 ...

  8. git学习(十) idea git reset 操作

    git reset 是回滚操作,在 idea 中使用如下: Reset Type 有三种: Mixed 默认方式,只保留源码,回退 commit 和 index 信息 Soft 回退到某个版本,只回退 ...

  9. spring cloud gateway整合sentinel作网关限流

    说明: sentinel可以作为各微服务的限流,也可以作为gateway网关的限流组件. spring cloud gateway有限流功能,但此处用sentinel来作为替待. 说明:sentine ...

  10. CTF-pwn:老板,来几道简单pwn

    wdb_2018_3rd_soEasy 保护全关 在栈上写入shellcode,然后ret2shellcode from pwn import * local = 0pa binary = " ...