前言

本文章演示Demo已上传Github:CameraProjectionMatix

3D渲染流水线中,物体某一个点从三维空间中映射到二维的屏幕上,通常使用MVP变换矩阵,而这三个字母分别代指不同坐标空间转换的三个矩阵,即:

  • M(Model):从本地空间转换到世界空间
  • V(View):世界空间转换到相机空间
  • P(Projection):相机空间转换到规则观察体

在之前的文章中,有对相机不同坐标空间的转换矩阵做过具体的描述,其中关于物体本地坐标转换到世界坐标的介绍,同样适用于世界空间转换为相机空间,如果感兴趣,可以查看该链接:

  • Unity 不同空间坐标转换中的矩阵应用

在前篇文章的基础上,本文章会介绍到渲染过程中最重要的P变换,即相机空间转换到规则观察体的过程(也可以理解为将相机的透视空间转换到正交空间),过程如图(图片来源于网络)所示:

关于投影矩阵推导有很多大佬在理论上做过的详细描述,不过通常是面向图形学的公式解析,比较难懂的同时对于工程项目上的应用提及较少。为了可以简单的理解投影的变换过程并可以实际应用,本文章会基于Unity引擎做一个详细的变换可视化,尽可能简单的拆解整个过程

相机投影矩阵的意义

1、相机透视投影概念:

在绘画理论中,用透视来表示平面或曲面上描绘物体的空间关系的方法或技术,通常来讲,在平面上在线空间感、立体感会通过三个属性来表示:

  • 物体的透视形(轮廓线),即上、下、左、右、前、后不同距离形的变化和缩小的原因
  • 距离造成的色彩变化,即色彩透视和空气透视的科学化
  • 物体在不同距离上的模糊程度,即隐形透视

以上信息来自百度百科对于透视的解释。简单的理解就是,在物体的透视形上,由于眼睛张角的存在,使得我们观察物体是总会有近大远小的感受,长此以往,由于经验的逐渐积累,这种现象间接的帮助人类完成的立体空间的构造

回到游戏引擎,没有什么是比模拟人眼成像更好的方式的,为了可以让计算机正确模拟人类感官的渲染出透视画面,相机在透视模式下,通过具有张角的锥形标识其取景范围,而且通常该锥形的顶部的尖会被消除,被称为视锥

虽然使用视锥的概念可以很好的解决透视的问题,但是对其后续计算带来了一定的问题。简单的来说,一个矩形可以通过中心点与其各边的长度非常方便的划分其空间范围,同时压缩某一轴达到三维到二维投影的目的。但是视锥是一个不同轴方向的长度不等的锥体,很难对范围做出界定

既然难以直接对视锥做空间判定,优秀的程序员或数学家就想到了将视锥规则化的数学变换公式,并以矩阵的形式参与运算,这就是相机的透视投影矩阵

2、通过Gizmos可视化相机视锥空间:

与相机的正交投影模式不同,透视投影为了得到近大远小的画面,会根据深度信息做平切面的画面缩放,而这些平切面连续组合起来就组成了相机的视锥空间,可以通过Unity提供的绘制工具Gizmos来表示出相机的渲染空间范围,绘制代码为:

 public void OnDrawGizmos(){//相机投影矩阵绘制Matrix4x4 start = Gizmos.matrix;Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);Gizmos.color = Color.yellow;Gizmos.DrawFrustum(Vector3.zero, cam.fieldOfView, cam.farClipPlane, 0, cam.aspect);Gizmos.color = Color.red;Gizmos.DrawFrustum(Vector3.zero, cam.fieldOfView, cam.farClipPlane, cam.nearClipPlane, cam.aspect);      Gizmos.matrix = start;//坐标辅助线绘制Gizmos.color = Color.red;Gizmos.DrawLine(cam.transform.position, cam.transform.position + cam.transform.right * 10);Gizmos.color = Color.green;Gizmos.DrawLine(cam.transform.position, cam.transform.position + cam.transform.up * 10);Gizmos.color = Color.blue;Gizmos.DrawLine(cam.transform.position, cam.transform.position + cam.transform.forward * 10);}

在编辑完成上面的代码后,可以在Unity引擎的Scene窗口可以看到下图中的辅助线,其中红框划定的立体图形即为相机投影视锥:

投影矩阵变换过程

通过视锥可以划定出相机的渲染空间,但是如何通过数学方式表示以便于机器理解与运算呢,要想得到结论,需要先明确条件。在开始推到前需要得到所拥有的信息条件,查阅Unity官方文档,可以得到与相机视角有关的参数信息:

  • Near Clip Planes:近裁剪平面,代表物体将要渲染的最近位置
  • Far Clip Planes:远裁剪平面,代表物体将要渲染的最远位置
  • 相机FOV:相机张角代表视野的宽高比例
  • 屏幕的宽高比:根据该比例可以得到另一方向相机的FOV

1、标识投影视锥八个顶点:

要标识一个空间的范围,通常会利用线段组合成对应形状的线框,例用模型的网格。而要决定线段的长度位置,就需要先得到顶点。所以我们第一步会基于上面的相机的一些基本参数来计算处相机视锥对应的八个顶点,来为后续的CVV空间划定获取做基础

为了简化推导过程,将三维空间问题映射到二维来考虑。以Y轴为法线,相机视锥的形状如下图所示,以X轴与Z轴构成的二维视角下的相机视锥空间范围标识线,并标识一些关键点的代名词,根据前面的相机关键参数,可以得到以下的已知信息:

  • AB的长度:为相机的近裁剪平面nearClipPlane
  • AC的长度:为相机的远裁剪平面farClipPlane
  • 角BAD的角度:为相机FOVhoriziontalvertical方向)的一半对应弧度

以D点的坐标获取为例,通过上面的图中所构造的三角形,已知AB的长度为近裁剪平面距离相机的长度,角BAD的度数为相机FOV的一半,利用三角函数可以计算出BD的长度,这样就得到了D点在Z轴与X轴的坐标长度AB与BD

至于Z轴的长度,对应相机在同样可以通过近裁剪平面的长度与相机在另外一个轴方向的FOV(可以使用Camera的静态方法VerticalToHorizontalFieldOfView求得)的数值

循环上面的求值过程,得到相机视锥八个顶点的坐标,并在对应位置分别放置一个物体来实例化这些坐标点,代码如下:

     List<Vector3> GetPosLocation(){List<Vector3> backList=new List<Vector3>();for (int z = 0; z < 2; z++){for (int i = -1; i < 2; i += 2){for (int j = -1; j < 2; j += 2){Vector3 pos = GetPos(z, new Vector2Int(i, j) , cam);backList.Add(pos);}}}return backList;}Vector3 GetPos(int lenType,Vector2Int dirType,Camera cam){Vector3 cPos = cam.transform.position;//根据FOV得到水平与垂直角度一般对应的弧度float vecAngle = (cam.fieldOfView*Mathf.PI)/360;float horAngle = Camera.VerticalToHorizontalFieldOfView(cam.fieldOfView, cam.aspect) * Mathf.PI/360;float zoffset  = lenType == 0 ? cam.nearClipPlane : cam.farClipPlane;float vecOffset = zoffset * Mathf.Tan(vecAngle);float horOffset = zoffset * Mathf.Tan(horAngle);Debug.Log(Mathf.Tan(horAngle));Vector3 offsetV3 = new Vector3(horOffset * dirType.x, vecOffset * dirType.y, zoffset);return cPos + offsetV3;}

通过三角函数得到八个顶点的坐标位置后,并实例化Sphere来标定这些点,具体效果如图所示:

2、通过投影矩阵转换相机空间至CVV:

为了标定经过投影矩阵变换后的视锥范围,利用上面得到相机视锥的八个顶点做参照,对其做MVP计算,由于本文章直接基于世界坐标的点做空间转换,并没有本地坐标的概念,所以可以忽略物体从本地空间转换到世界空间的过程

跳过M计算,直接将上面通过三角函数的八个点的坐标与相机的空间转换坐标计算得到其在相机空间下的坐标,如图所示:

通过上图可以看出,当相机处于原点时,得到的计算后的八个顶点与计算之前的八个顶点坐标X轴与Y轴坐标相同,但是Z轴是反的。这是因为Unity的世界空间与本地空间坐标都是采用左手坐标系,而相机空间是使用相反的右手坐标系

在完成顶点从世界空间到相机空间的转换后,就可以通过相机的投影矩阵视线从相机空间到CVV的转换,但是注意与投影矩阵计算时实在齐次坐标下做计算,所以在得到CVV空间的坐标时需要除以Vector4返回值中的W分量,计算完成后在显示为:
如图所示,经过投影矩阵的乘法计算后,相机视锥内的空间坐标点会被映射到一个空间范围为1的规则观察体中,当然在三维空间内的长度表示不明显,将其映射到二维空间内,其长度如图:


通过投影变换得到CVV内对应坐标后,只需要抛弃某一个轴就可以将三维空间降维到二维平面内,就可以得到了相机的取景画面。同时为了获取正确遮挡关系的画面,通常以Z轴为基准做画面的绘制处理,即深度缓冲的概念

相机投影矩阵:

通过前面的可视化过程可以看到,将相机空间的视锥转换到CVV就是相机投影矩阵的意义。反过来说,支撑这一过程的投影矩阵就是这一变换过程中数学公式的集合

根据Unity文档,可以得到相机的投影矩阵,不过要注意矩阵中舍弃了相机FOV参数,转换为相机近裁剪平面的中心点到四边的距离,这样矩阵的表达稍微清晰明确一些,矩阵表示为:

{2∗near/(right−left)0(right+left)/(right−left)002∗near/(top−bottom)(top+bottom)/(top−bottom)000−(far+near)/(far−near)−(2∗far∗near)/(far−near)00−10}\left\{ \begin{matrix} 2*near/(right-left) & 0 & (right+left)/(right-left) & 0\\ 0 & 2*near/(top-bottom) & (top + bottom) / (top - bottom) & 0\\ 0 & 0 & -(far + near) / (far - near) & -(2 * far * near) / (far - near) \\ 0 &0 &-1 &0 \end{matrix} \right\}⎩⎨⎧​2∗near/(right−left)000​02∗near/(top−bottom)00​(right+left)/(right−left)(top+bottom)/(top−bottom)−(far+near)/(far−near)−1​00−(2∗far∗near)/(far−near)0​⎭⎬⎫​

其中各参数意义:

  • near:相机近裁剪平面到相机的距离
  • far:相机远裁剪平面到相机的距离
  • right: 相机近裁剪平面右边框到中点的距离
  • left:相机近裁剪平面左边框到中点的距离
  • top:相机近裁剪平面上边框到中点的距离
  • bottom:相机近裁剪平面底边框到中点的距离

虽然原理至于其具体的推导过程比较容易理解,但是其中的数学公式的表达体现比较复杂,如果非常有兴趣可以阅读该文章:

深入探索透视投影变换

投影矩阵扩展

1、利用相机投影矩阵判断某点是否在相机视野内

由于相机视锥的范围不规则性,如果直接使用世界坐标来判断某点是否在相机视野内是比较复杂的。而在前面的投影矩阵理解中,当相机的视锥从世界坐标转换到CVV后,其边界范围一个已知大小的规则立方体,非常容易的就可以判断出某点是否存在于该空间范围内:

 public static bool CheckPointIsInCamera(Vector3 worldPoint, Camera camera){Vector4 projectionPos = camera.projectionMatrix * camera.worldToCameraMatrix * new Vector4(worldPoint.x, worldPoint.y, worldPoint.z, 1);if (projectionPos.x < -projectionPos.w) return false;if (projectionPos.x > projectionPos.w) return false;if (projectionPos.y < -projectionPos.w) return false;if (projectionPos.y > projectionPos.w) return false;if (projectionPos.z < -projectionPos.w) return false;if (projectionPos.z > projectionPos.w) return false;return true;}

不过通常对于一个点的判断场景还是比较少的,更多的是对于场景中某个物体做处理。同时为了尽量的减少计算量,会物体的边界范围Bound执行判断,这里就粘贴出一个大佬写的判断物体的Bound是否与相机视锥有交点的方法:

    public static bool CheckBoundIsInCamera(this Bounds bound, Camera camera){System.Func<Vector4, int> ComputeOutCode = (projectionPos) =>{int _code = 0;if (projectionPos.x < -projectionPos.w) _code |= 1;if (projectionPos.x > projectionPos.w) _code |= 2;if (projectionPos.y < -projectionPos.w) _code |= 4;if (projectionPos.y > projectionPos.w) _code |= 8;if (projectionPos.z < -projectionPos.w) _code |= 16;if (projectionPos.z > projectionPos.w) _code |= 32;return _code;};Vector4 worldPos = Vector4.one;int code = 63;for (int i = -1; i <= 1; i += 2){for (int j = -1; j <= 1; j += 2){for (int k = -1; k <= 1; k += 2){worldPos.x = bound.center.x + i * bound.extents.x;worldPos.y = bound.center.y + j * bound.extents.y;worldPos.z = bound.center.z + k * bound.extents.z;code &= ComputeOutCode(camera.projectionMatrix * camera.worldToCameraMatrix * worldPos);}}}return code == 0 ? true : false;}

2、深度缓冲小知识

深度缓冲用于记录每个像素的深度值,,通过深度缓冲区,可以进行深度测试,从而确定像素的遮挡关系,保证渲染正确。当空间中的物体经过MVP计算到CVV空间时,会以Z轴为方向记录距离相机的距离,即为某以像素点的深度

由于空间投影的关系,深度缓冲的精度是非线性的,通常距离相机越近时,深度缓冲的精度越高,可以从下图的示例中看到,世界空间中的某点(以右边的黄球标识)匀速远离相机时,其对应的CVV空间的Z轴变化会越来越来越小:

Unity 透视投影矩阵变换的可视化相关推荐

  1. Unity 中的音乐可视化

    Unity 中的音乐可视化 本帖最后由 204有个大坑 于 2017-5-31 17:33 编辑 1738music-visulization-in-unity.jpg (32.33 KB, 下载次数 ...

  2. Unity 语音识别以及音频可视化

    代码很简单没有难度,自己看一下应该就能明白. OK 老规矩,直接上代码: 语音识别以及音频可视化 怎么说呢,就是这个语音识别的模块现在Unity只能识别关键字,并不能完整的识别语句以及语气,只能做一些 ...

  3. 在unity中读取并可视化dicom图像(fo-dicom / C# / unity)

    第一步:在unity中安装fo-dicom插件 方法:点击window,选择Asset Store,然后搜索fo-dicom,导入即可. 第二步:读取dicom文件中的图像信息并转换为Texture2 ...

  4. 【转载+补充】“最简单的” 相机透视投影矩阵推导与解析

    原文链接 作者:大其心宏其量扩其识 链接:https://www.jianshu.com/p/09fef48e7b0f 来源:简书 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. ...

  5. unity 可视化渲染管线_如何为高端可视化设置Unity的高清渲染管道

    unity 可视化渲染管线 Prior to Unite Copenhagen in September 2019, Unity collaborated with Lexus and its age ...

  6. Unity 3D学习视觉脚本无需编码即可创建高级游戏

    在本课程中,您将学习如何在Unity中使用可视化脚本(以前称为Bolt)以及如何在不编写一行代码的情况下创建自己的高级游戏所需的一切.本课程将教你如何掌握可视化脚本,即使你以前没有任何关于unity或 ...

  7. Unity创造没有代码的游戏学习教程

    流派:电子学习| MP4 |视频:h264,1280×720 |音频:AAC,48.0 KHz 语言:英语+中英文字幕(根据原英文字幕机译更准确)|大小:17.4 GB |时长:17h 18m 你会学 ...

  8. Unity发布四款新产品,加速本土化技术研发

    近日,在中国Unity线上技术大会上,Unity正式宣布MARS.ArtEngine.Plastic SCM以及即时游戏(Instant Game)四款全新技术产品落地中国.围绕创建和运营两大核心业务 ...

  9. unity 3d开发的大型网络游戏

    unity3D是如今绝大多数游戏开发团队的首选3D引擎,并且它在2D上的表现也及为优秀.它可以轻松解决很多其它引擎不能解决的问题,哪些游戏是用unity3d做的? 有的网友说unity3d开发的游戏, ...

最新文章

  1. C# 自定义事件和委托
  2. SQLite中的SELECT子句使用通配符
  3. Windows系统下使用protobuf:protobuf的简介、安装、使用方法之详细攻略
  4. 图床失效了?也许你应该试试这个工具
  5. 【原】页面跳转以及表单提交中有中文的解决办法
  6. java实用教程——组件及事件处理——对话框(消息对话框,输入对话框,确认对话框)
  7. 前端学习(2971):前一天回顾
  8. linux创建文件后会自动删除,linux会自动删除目录和文件的吗
  9. python变量 数据类型 列表 元组 字典
  10. activity 变成后台进程后被杀死_Android 后台运行白名单,优雅实现保活
  11. 信息学奥赛一本通 1848:【07NOIP提高组】字符串的展开 | OpenJudge NOI 1.7 35:字符串的展开 | 洛谷 P1098 [NOIP2007 提高组] 字符串的展开
  12. 测试hudi-0.7.0对接spark structure streaming
  13. 不要版面费的期刊名称
  14. GoEasy实现简单聊天室
  15. vpython_vpython初探
  16. C++语言for循环实现从1加到100:1+2+3+...+100=
  17. C语言自定义类型——枚举类型讲解
  18. windows 2003 系统优化参考
  19. 棋牌游戏服务器开发心得
  20. C++编程基础(1)-C中的malloc/free和C++中的new/delete

热门文章

  1. U盘上的文件删除了可以恢复吗 U盘上的文件怎么在电脑上恢复
  2. 商务网站建设与维护【17】
  3. Eigen:矩阵计算简单用法(一)
  4. 移动硬盘连接不到电脑上?
  5. 03-Jmeter参数化取值策略
  6. 【面试】前端路由hash和history的区别
  7. python开发sdk模块
  8. 【lvcreate】创建lv需要在vg上创建
  9. 你我皆凡人,小事里修行
  10. 楼盘vr实景线上虚拟展示功能及特点