4.5 Unity引擎中的渲染管线流程
在4.4节中,大家学习了3D数学的计算和材质的制作。那么这些算法和材质是如何在Unity或者其他图形游戏引擎中进行运作的呢?这一节就来学习图形引擎渲染的流程和原理。
如图4-10所示,这是一个3D图形引擎渲染管线的关键流程图。本章把图形绘制管线分为三个主要阶段:应用程序阶段、几何阶段和光栅化阶段。
1.应用程序阶段
这个阶段就是在Unity中通过脚本程序编程的部分。比如,在制作镜面材质时,我们在Unity的脚本程序中,通过相机和镜像相机计算投影矩阵,并且设置纹理图片的部分。应用程序阶段就是使用高级语言C#进行开发的部分,主要和CPU及内存打交道。这个阶段的程序需要处理碰撞检测、场景、图形控制、空间和游戏逻辑等。
2.几何阶段
几何阶段的主要工作是“顶点坐标的三维变换”和“光照计算”。显卡信息中通常会有一个标示为T&L的硬件部分,T&L全称是Transform&Lighting,从字面上理解,是变换和光照的意思。
三维顶点进行坐标空间变换有什么作用呢?图形引擎输入到计算机中的是一系列三维坐标点,但是最终需要看到的是从视点出发观察到的特定点(这句话可以这样理解,要使三维坐标点显示在二维的屏幕上)。一般情况下,GPU帮我们自动完成了这个转换。
使用GPU来开发顶点变换的程序,是开发人员控制坐标空间转换的方法。在开发中永远不要忘了,最终显示给用户的是一个屏幕。屏幕是二维的,但是图形数据却是三维的。GPU所需要做的工作就是把三维的数据绘制到二维屏幕上,并且让人看起来是三维的效果。顶点变换的过程就是为实现这个目的。
根据顶点坐标变换的顺序,本节归纳出下面几个坐标空间,也叫做坐标类型:
·Object space,模型坐标空间;
·World space,世界坐标系空间;
·Eye space,观察坐标空间;
·Clip and Project space,屏幕坐标空间。
3.光栅化阶段
光栅化的过程就是从计算出来的图元中决定哪些像素被覆盖,然后投影到屏幕上的过程。经过坐标变换以后,引擎就得到了每个点在二维屏幕上面的二维坐标,也会获得绘制的图元元素,比如点、线和面。得到了这些数据,还需要解决一些在二维屏幕方面的问题,具体步骤如下所述。
(1)引擎得到的数据中,点是一个浮点数。但是当点投射到屏幕上面时,屏幕上面的像素都是整数点来确定的。那么如何去处理对应的像素问题呢?可以使用接近端点的方法来进行计算。比如一个线上的一个点位置是(9.33,10.55),那么我们使用界定的点来进行计算,根据四舍五入法确定为(9,11)这个像素点。
(2)除了点,在屏幕上还需要绘制线和面等集合元素。那么在算法上,这些线和几何图应该如何来绘制呢?在这方面有一些经常使用的算法,比如在绘制线段时使用到的算法:
DDA算法、Bresenham画线算法;在绘制图元时使用到的算法:区域图元、扫描线多边形填充算法、边界填充算法等。当绘制完毕以后,这些参数就对应到了屏幕上面的像素(pixel)。下面将介绍像素方面的计算:如何给像素计算它的颜色值。
像素编程(Pixel operation)又可以命名为Raster Operation。它是最后一个对像素片段的操作。完成这个操作以后,引擎就可以更新像素的数据到缓存中。操作它的目的是为了计算出最后屏幕显示像素点的颜色值。在这个阶段,被遮挡面会通过一个被称为深度测试的过程而消除。这其中包含多种计算颜色的技术。
在像素编程这个阶段包含以下内容:
(1)消除遮挡面。
(2)Texture operation→纹理操作,根据纹理坐标的位置,在纹理上取值,获得点的颜色。
(3)Blending混色。当计算出当前物体层的颜色,有一些颜色是透明的效果,那么引擎如何绘制透明的部分呢?一般是根据目前已经绘制好的颜色,与正在计算的颜色的透明度(Alpha),然后根据权值混合为两种颜色,作为新的颜色输出。这通常称之为Alpha混合技术。在绘制某个物体的
时候,每个物体的颜色都可以表示为RGB和一个深度缓冲Z。这里可以给定物体的Alpha的值进行计算。一般,不透明的物体Alpha值为1,不显示的物体Alpha值为0。引擎根据物体的透明度进行计算,通过绘制管线得到它的RGBA,然后再根据深度值进行排序计算,这里会使用一个z-buffer的缓冲层进行计算。
(4)Filtering-滤波和滤镜算法。引擎是根据当前的缓冲层再进行一层图像平面处理,然后形成新的颜色值来进行计算。如图4-11所示为像素操作的流程。
4.6 Unity中实现模型的缩放和旋转等程序处理
通过本章的学习读者了解了3D图形数学的基础知识。那么我们该如何在开发Unity应用的时候应用这些3D图形数学的知识呢?这一节应用3D数学知识在Unity中开发一段脚本程序,具体代码示例如下:
//mousemove.cs脚本程序文件using System.Collections;using System.Collections.Generic;using UnityEngine;public class Mousemove : MonoBehaviour {// Use this for initializationvoid Start (){}// Update is called once per frame//平滑旋转的速度float speed = 100.0f;//鼠标x轴方向移动float x;//鼠标y轴方向移动float y;void Update(){if (Input.GetMouseButton(0)){//按着鼠标左键移动y = Input.GetAxis("Mouse X") * Time.deltaTime * speed;x = Input.GetAxis("Mouse Y") * Time.deltaTime * speed;}else{x = y = 0;}//旋转角度(增加)transform.Rotate(new Vector3(x, y, 0));/**---------------其他旋转方式----------------**///transform.Rotate(Vector3.up *Time.deltaTime * speed);//绕y轴旋转//用于平滑旋转至自定义目标smoothrotate();}//平滑旋转至自定义角度//OnGUI是脚本文件中运行UI控制的代码函数片段void OnGUI(){if(GUI.Button(new Rect(Screen.width - 110,10,100,50),"set Rotation")){//自定义角度//通过欧拉角来计算旋转四元数targetRotation = Quaternion.Euler(45.0f,45.0f,45.0f);// 直接设置旋转角度//transform.rotation = targetRotation;// 平滑旋转至目标角度iszhuan = true;}}//标示是否在旋转bool iszhuan= false;//旋转的四元数类Quaternion targetRotation;//旋转的函数void smoothrotate(){if(iszhuan){//根据每一帧运行的片段时间进行旋转transform.rotation = Quaternion.Slerp(transform.rotation,targetRotation,Time.deltaTime * 3);}}}
这里通过运用欧拉角和每一帧运行的时间来平滑旋转物体。在算法上,使用时间和欧拉角进行计算,让物体通过一段时间段的平滑旋转,达到所需要旋转的目标角度,角度是由鼠标拖曳幅度决定的。
编写好这个脚本以后,将其挂载在Unity场景中一个正方体上面,然后运行测试程序,我们就可以看到这一段旋转的Demo展示,如图4-12所示。
下面一段程序将展示在Unity中缩放物体的方法,具体示例代码如下:
using System.Collections;using System.Collections.Generic;using UnityEngine;public class Mousemove : MonoBehaviour {// Use this for initializationvoid Start (){}// Update is called once per frame//缩放的速度float speed = 5.0f;//按x轴缩放物体float x;//按z轴缩放物体float z;void Update(){//键盘输入得到x轴当前的缩放片段x = Input.GetAxis("Horizontal") * Time.deltaTime * speed;//水平//键盘输入得到z轴当前的缩放片段//垂直//Fire1,Fine2,Fine3映射到Ctrl,Alt,Cmd键和鼠标的三键或腰杆按钮//新的输入轴可以在Input Manager中添加z = Input.GetAxis("Vertical") * Time.deltaTime * speed;//对脚本所挂载的物体进行缩放的参数设置transform.localScale += new Vector3(x, 0, z);/**---------------重新设置角度(一步到位)----------------**///transform.localScale = new Vector3(x, 0, z);}}
运行这一段代码,然后使用键盘的方向键,我们就可以控制物体在X轴方向和Z轴方向上的缩放,如图4-13所示。
4.7 Unity中计算射线相关的程序处理
在图形开发和Unity游戏开发中,射线是一个非常重要的概念。在图形应用中,正面和背面的计算、碰撞检测、人物和地面的高度计算等,都大量运用射线进行计算。射线可以看作一个原点、一个方向、无限长度的向量,本节将大量应用向量相关的线性算法进行计算。
在Unity中Ray类和RaycastHit类是两个最常用的射线工具类,一个是射线操作的类,一个是射线检测碰撞的类。创建射线时需要传入两个参数,一个是origin,代表起始点;另一个是direction,决定了射线发射的方向。另外,传入的参数如果没有单位化,Unity会自动对传入的参数做一次归一化。射线Ray的构造函数为:
public Ray(Vector3 origin, Vector3 direction);
下面还有一些和射线碰撞检测相关的类和参数,如下所述。
·RaycastHit:这个类是用来检测射线和物体碰撞以后产生的结果,包括碰撞的位置点和碰撞到的物体等信息。
·distance:发射射线到的最长距离,超过最长距离就不再检测。
·normal:射线摄入平面的法线现象,法线是永远垂直于平面的。
·point:这是一个vector3的数据对象,表示射线检测到碰撞的那个点所在的位置。
·Physics.Raycast:这是一个静态函数,用来计算射线物理碰撞的方法。
下面一段代码是用来计算物体和地面的高度。从平面上很高的高度y点,垂直地向平面发射一条射线,检测射线和物体的碰撞。接着在摄像机的下面放置一个物体,并且给它添加一个碰撞体,然后检测相机下面是否有一个游戏对象。具体代码示例如下:
using UnityEngine;using System.Collections;public class RayDemo : MonoBehaviour{void Update(){// 以摄像机所在位置为起点,创建一条向下发射的射线Ray ray = new Ray(transform.position, -transform.up);RaycastHit hit;if (Physics.Raycast(ray, out hit, Mathf.Infinity)){// 如果射线与平面碰撞,打印碰撞物体信息Debug.Log("碰撞对象: " + hit.collider.name);// 在场景视图中绘制射线Debug.DrawLine(ray.origin, hit.point, Color.red);}}}
我们先在场景的Camera下面建立一个Cube物体,然后给Camera附加4.6节中编写的RayDemo脚本,最后运行Demo。这时摄像机会向下发射一条射线,检测下方是否有物体,如图4-14所示。
接着需要了解如何从屏幕发射射线来检测物体,它能帮助用户通过屏幕单击选择物品。这个射线是屏幕上一点面向3D世界一个方向发射的一条射线。如果是向世界坐标系中的一个矢量方向发射射线,我们已经演示过如何实现碰撞检测。针对向屏幕上的某一点发射射线,从屏幕到世界物体的检测碰撞,Unity提供了两个函数来计算对应的碰撞检测:ScreenPointToRay和
ViewportPointToRay。
public Ray ScreenPointToRay(Vector3 position);
参数说明:position是屏幕坐标系上面的一个点。
返回值说明:从屏幕的上的一点position发射一个射线到3D世界空间,然后通过3D世界空间的摄像机作为原点进行计算,如果计算到碰撞就返回碰撞信息,如果没有计算到碰撞,就返回碰撞点为(0,0,0)。
ScreenPointToRay方法以3D世界环境的摄像机作为原点进行模拟,取屏幕上的一点position,相对原点发射一个射线。position是用屏幕上的像素点作为坐标表示的。。position z等于0,因为屏幕上是一个二维坐标。屏幕分辨率上的值,从0到最大值,在三维世界中对应着它的横坐标X和纵坐标Y。下面用一段程序示例说明,如何利用ScreenPointToRay发射一条射线,指向屏幕上的某点进行定向碰撞检测,具体代码示例如下:
using UnityEngine;using System.Collections;public class ScreenRayDemo : MonoBehaviour{Ray ray;RaycastHit hit;// 创建射线到屏幕上的参考点,像素坐标Vector3 position = newVector3(Screen.width/2.0f,Screen.height/2.0f,0.0f);void Update(){// 射线沿着屏幕x轴从左向右循环扫描position.x = position.x >= Screen.width ? 0.0f : position.x +1.0f;// 生成射线ray = Camera.main.ScreenPointToRay(position);if (Physics.Raycast(ray, out hit, 100.0f)){// 如果与物体发生碰撞,在Scene视图中绘制射线Debug.DrawLine(ray.origin, hit.point, Color.green);// 打印射线检测到的物体的名称Debug.Log("射线检测到的物体名称: " + hit.transform.name);}}}
脚本编写好以后,把这个脚本挂载到Main Camera上,然后运行这个Demo,可以看到屏幕的中心发射了射线,进行物体的扫描。这就是从屏幕向世界坐标体系发射射线检测的方法,如图4-15所示。