Unity 是一款跨平台的 3D 引擎,有着强大的渲染功能,并主要用于游戏开发。
谈到 Unity 的渲染功能,我们不得不提及到着色器(Shader)——3D 游戏引擎中最重要的一个因素,它在游戏效果以及画面显示方面起到了决定性的作用。Shader 编程也属于计算机图形学中一个重要的部分。
接下来让我们从可编程渲染管线来了解 Shader 编程。
渲染管线模型
3D 游戏以及 3D 模型通过渲染管线来渲染到 2D 的屏幕上。渲染管线的流程是在 GPU 中进行的,它主要占有计算机的显存部分。渲染管线在这个过程中进行了顶点处理、面处理、光栅化、像素处理。
1)顶点处理
大多数接触过 3D 图形的人都知道3D 模型是通过众多点构成的面而展现出来的。
顶点处理,是通过一系列坐标系的变换,让各个顶点通过一定的规律在摄像机前位移,最终在屏幕上对应这些顶点的过程。
首先,物体的各个顶点从自身坐标系,通过世界变换矩阵处理转换到世界坐标系,再通过取景变换矩阵变换到观察者坐标系,最后通过投影变换,将顶点转移到屏幕坐标系。
有一点大家会经常忘记,在观察者坐标系转换到投影坐标系的过程中,GPU(图形处理单元)还对材质属性和光照属性进行了处理。
2)面处理
三点成一面。面处理有三个部分:面的组装、面的截取、面的剔除。
面的组装:模型中的三个点会组成一个三角形的面(非任意点,因为每个点都有自己的编号)。这些面,面面相接,组成了我们能看到的模型。
面的截取:由于摄像机和人眼一样,可视的区域是一个锥形,模型在摄像机可视范围内可能并不是全覆盖,也就是在摄像机外,这些在摄像机之内的部分就会被截取。
面的剔除:为了模拟肉眼,摄像机前的物体会出现近大远小的现象,那么物体和物体之间会有遮挡,被遮挡的面会被剔除不处理;每个面都有法向量,所以只有在面的法向量和摄像机散射向量夹角大于90度的才会被摄像机捕捉到。
3)光栅化
光栅化,又称之合并阶段。它的主要功能是将面转换成一帧中的像素集合。
这一阶段是不可以编程的,它负责执行多个片段测试,包括:深度测试、alpha 测试和模板测试,程序员可以通过高度配置来实现想要的效果。如果通过了所有的测试,这部分颜色就会与帧缓冲存储的颜色通过 alpha 混合函数进行合并。
4)像素处理
这个阶段将像素区域着色,然后赋予贴图。
(左上为3D网格模型,左下为赋予贴图后的3D模型,右图为贴图)
Shader 详解
介绍完GPU 渲染管线之后,我们再来简单了解一下可编程着色器 Shader —— 图形渲染里最有趣的部分。Shader 能让渲染的图形展示出水面效果、火焰的热流效果、角色的虚化效果等视觉效果。
着色器可分为顶点着色器(VertexShader)、几何着色器(Geometry Shader)和像素着色器(Pixel Shader)。它们从输入的数据中取得一个元素,通过程序计算,变换为输出数据的一个或多个元素。
顶点着色器输入源为顶点,顶点包含其在自身坐标系或世界坐标系的位置和矢量信息,而输出源为已通过变换和照明处理的顶点,包含其投影坐标系的信息。
几何着色器的输入源为一个有 n 个顶点的几何图元以及最多 n 个作为控制点的额外顶点,输出源则变成 0 或多个图元。根据效果需要,这些图元的可能和输入的时候不同。
像素着色器输入为顶点间的片段,这些片段所包含的信息来自于对三角形顶点信息的插值,输出成将要写到帧缓冲里的颜色。
渲染管线概述
Unity渲染管线流程:
多相机渲染
CPU渲染管线
层级剔除
遮挡剔除
发送数据(将所有数据打包,发送给GPU)
GPU渲染管线
图元装配及光栅化
执行裁剪
裁剪空间转换到NDC
计算NDC
背面剔除
图元装配
光栅化
片元着色器Shader
纹理寻址模式
纹理压缩格式
光照计算
光照模型
经验光照模型
环境光
输出合并
Alpha测试
模板测试
深度测试
混合Blending
后处理
Unity渲染管线流程:
CPU打包数据,材质,法线
a. 剔除物体(摄像机外的物体将被剔除)
b. 渲染排序(安装给定顺序和深度对物体进行排序)
c. 发送数据(将所有数据打包,发送给GPU)
d. 调用Shader:SetPassCall,DrawCall
GPU渲染管线
a. 顶点着色器,将逐顶点执行。将模型空间转换到裁剪空间。可以处理顶点偏移和顶点光照。
b. 顶点着色器后:光栅化阶段
裁剪片元: 将裁剪空间外的模型裁剪掉
转换为NDC(标准化设备坐标,使用透视除法)
剔除(背面剔除,正面剔除等),将特定方向的顶点进行剔除
视口转换(转换到屏幕坐标)
图元装配
光栅化(会产生锯齿)
c. 片元着色器,逐像素执行。
d. 片元着色器之后:输出合并阶段
Alpha测试
模板测试
深度测试
颜色混合
e. 帧缓冲区
f. 后处理阶段(CPU调用GPU渲染管线)
g. GPU渲染到屏幕
多相机渲染
每个摄像机都会渲染一遍该相机内的物体的顶点。
每个相机都会跑一次完整的渲染管线流程
默认会清除除天空盒之外的其他物体,通过ClearFlags和Depth设置清楚条件和渲染顺序。
如果不清楚其他相机的物体将会出现渲染叠加。
通过一个相机清除,一个相机保留好像可以实现3D视频的左右眼效果。(虽然需要两倍的渲染时间)
CPU渲染管线
剔除物体(摄像机外的物体将被剔除)
视锥体剔除
即:使用摄像机可视范围的视锥体构成的区域与物体进行碰撞检测。
优化:但是复杂的物体碰撞检测很耗性能,因此会使用一个简单的碰撞体Box Colider进行包裹,称为AABB包围盒。
层级剔除
通过摄像机进行特定物体的剔除。剔除特定层级的物体(未被勾选即被剔除物体)。
感觉这个可以实现物体的快速隐藏。(通过将物体设置到隐藏层来减少渲染,而又无需摧毁)。
遮挡剔除
通过判断物体位置和遮挡关系对被不透明物体完全遮挡的物体进行剔除。
渲染排序(安装给定顺序和深度对物体进行排序)
通过Render Queue数值进行排序,相等时使用深度进行排序。
不透明物体默认2000
透明物体默认3000
大于2500理解为半透明队列,将按深度进行从后向前排序
小于2500理解为不透明物体,按深度从前向后排序
不透明物体从前向后排序有助于优化,被遮挡物体将不会被渲染。
半透明物体从后向前渲染才能保证渲染颜色正确,半透明颜色混合顺序不同,结果颜色不同。
半透明物体内部不能保证完全从后向前进行渲染。导致的显示效果:1.向面片一样没有厚度,
发送数据(将所有数据打包,发送给GPU)
模型数据
格式:obj格式,fbx格式(推荐)
obj数据:
顶点坐标(三维)
法线信息(法线向量)
UV坐标(二维数据,最后一位无用)
索引列表,一行数据表示一个三角面(通过索引找到每个顶点的顶点、法线、UV)。通过索引记录有助于压缩数据,
调用Shader:SetPassCall,DrawCall
GPU渲染管线
顶点着色器Shader
Unity中的顶点Shader
最重要的任务:将顶点坐标从模型空间变换到裁剪空间:
通过MVP矩阵对顶点的坐标进行变换。
模型空间变化到世界空间:
参考系由物体自身变化到世界统一坐标
模型空间时建模软件的坐标空间,就是模型生成的坐标。
世界空间变化到相机空间
参考系由世界坐标变化到以相机为中心
相机空间变化到裁剪空间
将相机的视锥体进行压扁标准化成2x2x1的(CVV矩阵)
图元装配及光栅化
执行裁剪
在裁剪坐标空间中裁剪多余的像素和图像。
超过CVV之外的形状将会被裁剪。
空间范围:[w, w, 0] ~ [-w, w, w]
NDC - 标准化设备坐标
为转换到屏幕坐标做准备
空间范围:[-1, -1, 1]~[1, 1, 0]
空间中的z值为深度值,将会在下面几个操作中保留,然后再深度剔除中排上用场。
裁剪空间转换到NDC
P.xyz / P.w = NDC
同时要注意不同的设备NDC不同
DX平台:左上角[0, 0], 右下角[1, 1]
OpenGL:左下角[0, 0],右上角[1, 1]
// 适应不同平台下的ndc坐标
o.screen_pos.y = o.screen_pos.y * _ProjectionParams.x;
计算NDC
我们能直接获得的最远只有裁剪空间下的坐标,想要获取NDC坐标需要自己计算。但是很简单。
裁剪空间下坐标空间为:[-w,-w,w] -> [w, w, 0]
NDC坐标为:[-1, -1,1] -> [1, 1, 0]
裁剪空间坐标除以w可以得到=>[-1, -1, 0] -> [1, 1, 0]
方法一:
// 顶点着色器
o.screen_pos = o.vertex;
// 适应不同平台下的ndc坐标
o.screen_pos.y = o.screen_pos.y * _ProjectionParams.x;
// 片段着色器
// 计算屏幕空间NDC坐标
// 透视除法 xyz范围 [-w,-w,w],[w,w,0] => [-1, -1,1]=>[1,1,0]
half2 screen_ndc = i.screen_pos.xy / (i.screen_pos.w + 0.000001);
half2 screen_uv = (screen_ndc + 1.) * .5; // [x, y]范围: [-1, 1] => [0, 1]
方法二:
// 顶点着色器
o.screen_pos = ComputeScreenPos(o.vertex);
// 片段着色器
// 计算屏幕空间NDC坐标
// [x, y]范围: [-1, 1] => [0, 1]
half2 screen_uv = i.screen_pos.xy / (i.screen_pos.w + 0.000001);
背面剔除
将背对着摄像机的物体进行剔除。
通过三角面的索引列表进行排列,如果排列顺序为顺时针则为正面,为逆时针则为背面。
屏幕坐标空间- 视口转换
将xy坐标转换到屏幕空间的宽高。
将NDC空间下转换到屏幕空间下的坐标
图元装配
在平面中将顶点连线,形成一个一个封闭的三角形。
光栅化
在平面中对每一个图元中每一个片元生成像素。
深度值Z
法线
顶点色
切线
位置
所有自定义数据
片元着色器Shader
纹理技术
纹理采样
在纹理坐标UV中进行采样[0, 0] ~ [1, 1]。通过UV映射到纹理的像素上,取出那个点的像素。
纹理过滤机制
小图像映射到大块区域:相当于图片放大显示。
四舍五入 - 会产生比较尖锐的边缘
双线性差值
大图像映射到小块区域引起的失真:
Mipmap,通过给定一组大小不同的图像进行映射。显示大区域时用大图像,小区域小图像。
Mipamp只会增加1/3的存储占用。每个图像大小依次递减1/2。
纹理寻址模式
当索引超过UV时应该怎么取值。
Repeat重复模式 - 取小数(将形成循环取UV的效果)
Clamp截取 - 超过将会取最大、最小值。
纹理压缩格式
RGBA 32bit
ASTC 4x4 block, ASTC 6x6 - 纹理细节保留较好,压缩明显
ETC2 8bits
PVRTC 4bits
光照计算
光照组成:
直接光照
间接光照
光照模型
BRDF基于物理的渲染模型,过于复杂。
经验光照模型
光照模型基本框架 = 直接光漫反射 + 直接光镜面反射 + 间接光漫反射 + 间接光镜面反射 + …(次表面散射,PDR等)
Lambert漫反射光照模型
Phong光照模型
通过入射光方向和法向量计算反射光方向。知道了反射光就可以对比视线和反射光的夹角来得出其看到的光线多少。
Blinn光照模型
通过入射光方向和法向量夹角,计算出其半角向量。以半角方向对反射光方向进行近似(半角计算更简单)。
Ground光照模型
Flat光照模型
环境光
可以等同于间接光进行模拟。
间接光照实现:
Lightmap
Reflection Probe
Light Probe
输出合并
最重要任务:处理遮挡关系,处理半透明混合。
帧缓冲区FrameBuffer:
颜色缓冲区ColorBuffer
深度缓冲区DepthBuffer,Z-buffer
模板缓冲区StencilBuffer
Alpha测试
完全透明的,或者不满足给定透明度的片元将被丢弃。
模板测试
选定一个区域进行对比,不在区域中会被丢弃。
深度测试
使用深度进行测试,不通过将被丢弃。
通过测试的片元的深度将被存在Z-buffer中。(也可控制不写入)。
在Shader中可以通过ZWrite和ZTest进行控制。
ZWrite:是否写入Z-buffer
ZTest:深度测试规则
提前深度测试技术:
Early-Z,发生在顶点Shader之后,图元装配之前。
是一种提前优化技术,通过提前测试避免不必要的图元装配和片元计算。
混合Blending
对半透明像素进行颜色混合。
从后到前
一般情况下会关闭ZWrite
写在Shader中如:
Blend SrcAlpha OneMinusSrcAlpha
Blend SrcAlpha One
后处理
后处理可以在着色器工作完成后由CPU调用GPU对整体屏幕画面进行调整。
渲染管线可以看成3大步骤组成:
CPU处理要渲染的数据,将数据发送给GPU
GPU根据CPU发来的数据,将画面渲染到帧缓冲区
一个画面在帧缓冲区渲染完了,进行后处理后输出
一、CPU阶段
剔除
将不在视锥范围内的物体剔除掉
渲染顺序
将根据视锥范围内的物体位置决定渲染顺序
不透明队列(RenderQueue<2500),按照从前到后的顺序排序
透明队列(RenderQueue>2500),按照从后到前的顺序排序
先透明队列后不透明队列
打包发送数据
将要渲染的数据发送给GPU
一个模型的数据,包含以下内容:
顶点坐标(Position)
法线向量(Normal)
贴图采样位置(UV)
索引列表
每一行表示一个面的数据,而一个面是由三个点构成
每个点的数据,都包含上述abc三个数据
因此每一行的数据可以看成:
点1.Position/点1.UV/点1.Normal
点2.Position/点2.UV/点2.Normal
点3.Position/点3.UV/点3.Normal
调用SetPass Call与Draw call
SetPass Call:设置好各种渲染状态、背面剔除、要使用的Shader与混合模式
Draw Call:通知GPU去渲染模型
二、GPU阶段
GPU阶段可以视为:
点:将拿到的顶点坐标,转化为屏幕屏幕坐标
线:将各个顶点连接起来,形成一个个三角面(即图元)
面:给三角面上色
最后输出合并。
点
顶点Shader:将模型空间顶点转化为裁剪空间(投影成像)
裁剪操作:将不在裁剪空间里的顶点给裁剪掉
NDC标准化设备坐标:将裁剪空间转化为NDC标准化设备坐标
背面剔除:将背面的三角面剔除掉
视口变换:将裁剪空间转换为屏幕空间坐标
线
图元装配:将顶点连接,形成三角面,称为图元
面
光栅化:通过插值,将图元内部形成一个个片元(片元可以理解成一个像素点)
片元Shader:通过纹理着色、光照着色来给片元上色
输出合并
通过alpha测试、模板测试、深度测试、颜色混合,
处理遮挡关系、处理半透明混合,将结果绘制到帧缓冲区