内容会持续更新,有错误的地方欢迎指正,谢谢!
1.渲染流程(又叫渲染管线、渲染流水线)
渲染流程的任务:从一个三维的场景出发,渲染出一张二维的图像。
《Real-Time Rendering》中将渲染流程分成三个阶段:应用阶段、几何阶段、光栅化阶段。
应用阶段
- 主要是CPU与内存打交道,输出渲染图元(渲染所需的几何信息:点、线、三角面等),将图元加载到显存中,如此,GPU可快速访问这些数据。
- 设置渲染状态:也就是使用什么材质、纹理、着色器等。
- 调用Draw Call:DrawCall的发起方是CPU,接收方是GPU,它告诉GPU按照设置的渲染状态来渲染,一个Draw Call指向本次需要渲染的图元列表。
如下图所示,发送渲染命令就是发送Draw Call。
几何阶段
主要负责顶点坐标变换、屏幕映射。
顶点坐标变换:模型坐标—>世界坐标—>观察坐标—>裁剪坐标—>屏幕坐标
MVP矩阵能将顶点从模型空间转换到裁剪空间。顶点着色器(有多少个顶点就会运行多少次)的最基本的任务就是通过MVP矩阵(模型-视图-投影矩阵)把顶点坐标从模型空间转换到裁剪空间中。从裁剪空间到屏幕空间是Unity自动完成的。
投影矩阵:投影矩阵并没有进行真正的投影,而是为后面真正的投影做准备。投影是个降维的过程,从三维降到二维,但真正的投影发生在后面的屏幕映射中,通过齐次除法获得二维坐标。
光栅化阶段
通过三角形设置和遍历计算每个图元覆盖了哪些像素:这时三维模型已经投影到了二维平面上,但屏幕是一个个小的像素格子,光栅化就是在选格子嘛。
三角形设置:计算光栅化一个三角网格所需的信息,它的输出是为下一步骤做准备。
三角形遍历:找到哪些像素被三角网格覆盖的过程。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。
为这些像素计算它们的颜色:光栅化后得到了格子,要搞上颜色,且没法将一个格子一半涂成白色一半涂成黑色,所以我们追求高像素屏幕,像素越高,效果越好。这时候会用到片元着色器(有多少个像素就会运行多少次)负责给每个像素上色。
各种测试用于决定每个片元的可见性,涉及了很多测试工作。比如:模板测试(限制渲染区域)、深度测试(不小于深度缓冲区中该片元的深度值就舍弃该片元)等测试。如果一个片元通过了所有测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行混合,最后再写入颜色缓冲区。也就是将片元的颜色值一个个地堆叠起来。
2.MipMap和LOD
MipMap这个东西在二维比较常见,三维通常会用到LOD。
MipMap:给一张图通过图像处理自动生成多张低分辨率的图。为了解决图像无限缩小(960w平方公里的图片相对于你手机来说,那就是无限缩小)引起的失真问题。
LOD:层次细节,对同一个物体,就是它根据离摄像机的远近来使用不同的模型。不过,LOD分几个级别,美术就得做几套资源给你,不像MipMap一样电脑帮你自动生成。
3.优秀的高光反射光照模型Blinn-Phone
虽然是一个经验模型,但已经能满足大多数的需求了,很常用。
公式:
C light是入射光线的颜色和强度
M specular是材质的高光反射系数
n是法线方向
h是由视角方向v和光照方向l 共同决定的,是一个半角向量
n和h做点积。
4.法线贴图
法线贴图呈现为蓝紫色,美术可以用软件生成。法线贴图的每个像素的RGB存的是模型法线的XYZ。若要实现外表凹凸不平的效果,常用的方法是使用一张法线贴图来修改模型表面的法线,以便为模型提供更多的细节,这种方法又被称为法线映射。
另外,渐变纹理(又叫坡度图)可用于渲染卡通风格;遮罩纹理可给模型增加更多的细节,让其某些地方看起来更亮一些,有些地方更暗一些。
5.基本shader的编写
Unity Shader的基本结构:根目录、Property属性、 SubShader子着色器、Fallback回滚。
其中,一个SubShader中可以有多个Pass,但Pass数目过多,会造成渲染性能降低,因为一个Pass定义了一次完整的渲染流程。
SubShader可以理解成由Tags(标签:告诉Unity怎样渲染这个对象)和RenderSetup(渲染状态设置:是否开启深度测试等)和Pass三部分组成。
在材质面板上显示属性的Shader代码:(注意a2v和v2f这两个结构体的使用)
Shader"Unity Book/Chapter5/Simple Shader"
{
Properties
{
_Color("Color Tint",Color) = (1.0,1.0,1.0,1.0)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//在Cg代码中,我们需要定义一个与属性名称和类型都匹配的变量
uniform fixed4 _Color;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return fixed4(i.color,1.0);
}
ENDCG
}
}
}
6.Unity引擎中前向、延迟渲染路径的区别
前言:
G-buffer:所有物体在不进行光照运算的情况下被绘制,然后对每个像素生成一组数据,这些数据包括位置、法线、高光颜色等。
正题:
所有的渲染路径的核心都是光照的处理。相比较逐顶点光照,游戏开发者更喜欢逐像素光照,但逐像素光照有一个显著的缺点:每个光源会让每个在它光照范围的物体增加一个渲染批次,所以才会有一个物体最多受4个光源影响的限制。
所以,延迟渲染是光照问题的救星!
- 延迟渲染路径:使用任意数量的光源,也不会增加额外的渲染批次。因为大部分模型在渲染的时候并不需要光照计算,而是在屏幕空间中才使用光照信息,就如同渲染一张2D图片,这时使用的数据是在上一个批次中生成的G-buffer。
- 前向渲染路径: 把空间的点进行各种剪裁后,再进行处理,所处理量远远大于我们最终看到的,并且是对每个光源进行计算产生最终结果。
那到底采用哪个?
- 延迟渲染的确好,但是使用有限制。延迟渲染不允许我们渲染半透明物体。因为如果场景中存在半透明的物体,只能记录一个物体的信息。缺乏抗锯齿的支持,但你可以使用屏幕空间抗锯齿算法。
- 移动设备上延迟渲染的性能会比前向渲染的性能要差一些,如果你的场景中只有一个光源,那么使用延迟渲染是不划算的。