什么是Shader?
Shader用来控制渲染流水线中的某些特定阶段。
渲染流水线
工作任务:由一个三维场景出发,生成(渲染)一张二维图像。
应用阶段(Application Stage)
由CPU负责,开发者拥有绝对的控制权。
步骤:
- 准备场景数据,做粗粒度剔除,将不可见的物体剔除出去。
- 加载渲染数据。(硬盘HDD->内存RAM->显存VRAM)
- 设置渲染状态,包括不限于材质、纹理、shader。
- 调用Draw Call,输出渲染图元。
几何阶段(Geometry Stage)
由GPU负责,接收顶点数据作为输入,和每个渲染图元打交道,进行逐顶点、逐多边形的操作。
任务:
- 顶点坐标 –> 屏幕空间
- 输出屏幕空间的二维顶点坐标,及对应的深度值,着色等相关信息。
顶点着色器(Vertex Shader)
完全可编程,通常用于实现顶点的空间变换,顶点着色等功能。
单位是顶点,从CPU传递过来的顶点都会调用一次顶点着色器。
着色器不能创建/销毁任何顶点,也无法得到顶点与顶点之间的关系。
任务:
- 坐标系转换:将顶点坐标从模型空间转换到齐次裁剪空间(MVP),再由硬件做透视除法,得到归一化的设备坐标(NDC)
- 逐顶点光照
- 坐标变换:模拟水面、布料等
曲面细分着色器
可选的着色器,用于细分图元
几何着色器
可选的着色器,逐图元的着色操作,或者被用于产生更多的图元。
裁剪
剔除不在摄像机视野范围内的顶点,包括完全在视野内,部分在视野内(会生成新的顶点),完全在视野外
屏幕映射
将每个图元的坐标转换到屏幕坐标系(视口变换)
光栅化阶段(Rasterizer Stage)
处理上一阶段的信息:屏幕坐标系下的顶点位置以及相关信息。
由GPU负责,生成屏幕上的像素,渲染出最终的图像。
任务:
- 计算图元覆盖了哪些像素,为这些像素计算它们的颜色
- 对上一阶段得到的逐顶点数据进行插值运算,在进行逐像素处理
三角形设置
固定函数阶段。
计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。
三角形遍历(扫描变换)
固定函数阶段。
检查每个像素是否被三角形网格覆盖,如果被覆盖则生成片元。输出一个片元序列。
片元:包含了很多状态的集合,如屏幕坐标,深度信息,顶点信息
片元着色器
实现逐片元的着色操作,是非常重要的可编程着色器阶段。
输入是上一个阶段对顶点信息插值得到的结果,根据片元信息,计算该片元的输出颜色。
在这个阶段,可以做纹理采样等很多重要的渲染技术。
逐片元操作
决定每个片元的可见性,涉及到各种测试工作,如深度测试、模板测试。
通过测试后,将片元的颜色和存储在颜色缓冲区的颜色进行合并,或者说混合。
模板测试(即颜色缓冲,深度缓冲):
读取模板缓冲区中该片元位置的模板值,任何进行比较。
通常用于限制渲染的区域,高级用法则是渲染阴影、轮廓渲染等深度测试:
将该片元的深度值和深度缓冲区的深度值进行比较,进行遮挡剔除,透明效果。
可以决定是否进行深度写入,将该片元的深度值覆盖原有的深度值
在Unity中,深度测试是在片元着色器之前,减少性能损耗,但有时也会出现操作上的冲突。合并
对颜色信息进行处理,因为颜色缓冲往往已经有了上次渲染之后的结果,是要覆盖还是其它效果?
不透明物体:关闭混合
半透明:开启混合
需要一个混合函数,
显示到屏幕
屏幕显示的时颜色缓冲区中的颜色值,GPU会用双重缓冲的策略,对场景的渲染发送在后置缓冲中,GPU会交换前置缓冲区和后置缓冲区中的内容。
OpenGL/DirectX
图像应用编程接口,是上层应用程序和底层GPU的沟通桥梁。
HSLS、GLSL、CG则是着色器编程语言。
ShaderLab
概述
Unity中,需要材质(Material)和Shader配合。
ShaderLab:对普通的Shader进行抽象,再封装一层。是Unity为开发者提供的高层级的渲染抽象层。
各模块解释
Properties:材质属性的声明。
类型:Int,Float,Ranage,Color,Vector,2D,Cube,3D
SubShader:因为不同的显卡具有不同的能力,所以需要用不同的SubShader去匹配不同目标平台。
Pass:定义了一次完整的渲染流程,Pass过多会导致性能下降。
状态设置
标签
顶点着色器的输入参数
可以定义一个结构体,用来定义顶点着色器的输入,注意语义。
这些输入的数据,由使用该材质的Mesh Render组件提供。
内置文件与变量
可以用#include指令将内置的包含文件包含进来。
在Unity应用程序中找到CGIncludes文件夹,里面包含了所有的内置包含文件。
这里列举几个常用的:
- UnityCGcginc:包含了最常使用的帮助函数、宏和结构体等
- UnityShaderVariables.cginc:会被自动包含进来,包括各种内置的全局变量,如MVP矩阵
- Lighting.cginc:包含各种内置的光照模型,如果用的是Surface Shader,则会自动包含进来
- HLSLSupport.cginc:在编译Unity Shader时,会被自动包含进来,声明了许多跨平台的宏和定义
Unity支持的语义
应用阶段–>顶点着色器:
- POSITION:模型空间中的顶点位置,通常是float4类型
- NORMAL:顶点法线,通常是float3类型
- TANGENT:顶点切线,通常是float4类型
- TEXCOORDn:顶点的纹理坐标,0表示第一组,以此类推,通常是float2或float4类型
- COLOR:顶点颜色,通常是fixed4或float4类型
顶点着色器–>片元着色器:
- SV_POSITION:裁剪空间中的顶点坐标
- COLOR0/1:通常用于输出第一/二组顶点颜色
- TEXCOORD0~TEXCOORD7:通常用于输出纹理坐标
片元着色器–>Unity
- SV_Target:输出值将会存储到渲染目标
Debug
- 使用假彩色图像
- 用Visual Studio进行调试
其它
不同的Shader Target,不同的着色器阶段,可使用的临时寄存器和指令数目都是不同的。
基础光照
标准光照模型
标准光照模型只关心直接光照
环境光
物体可以被间接光照照亮,环境光用来模拟间接光照,通常是一个全局变量
自发光
用材质本身的自发光颜色
漫反射
兰伯特定律:反射光线的强度与表面的法线和光源之间的夹角的余弦值成正比。
高光反射
Phong模型,Blinn-Phong模型(无法表示涅尔反射和各向异性)
代码
1 | Shader "Unlit/MyShader6_4" |
基础纹理
单张纹理
我们通常会用一张纹理来代替物体的漫反射颜色。
除了在Properties语义块添加一个纹理属性以外,还需要为纹理类型的属性声明一个float4类型的变量,来表示缩放和平移值。
凹凸映射
原理是用一张纹理来修改模型表面的法线。
方法有二:
- 高度纹理:存储高度值,表示模型表面局部的海拔高度,需要由像素的灰度值计算而得,消耗性能
- 法线纹理:
- 法线方向的分量范围在[-1,1],而像素的分量范围为[0,1],所以需要做一个映射
- 一般采用模型顶点的切线空间在存储法线,对于模型的每个顶点,都有属于它的切线空间,
渐变纹理
用渐变纹理来控制漫反射光照
遮罩纹理
遮罩纹理允许我们保护某些范围,让它们免于修改。
流程:通过采样得到遮罩纹理的纹素值,然后用其中某个通道的值来与某种表面属性进行相乘,这样,当该通道的值为0时,可以保护表面不受该属性的影响。
透明效果
要在实时渲染中实现透明效果,通常会在渲染模型时控制它的透明通道(Alpha Channel)
当开启透明混合后,当一个物体被渲染到屏幕上时,每个片元除了颜色值和深度值以外,还有另一个属性–透明度。(1为完全不透明)
两种方法来实现透明效果:
- 透明度测试:无法得到真正的半透明效果,有深度写入,要么完全透明,要么完全不透明。
- 透明度混合:当前片元的透明度作为混合因子,与已经存储在颜色缓冲的颜色值进行混合,得到新的颜色。
渲染顺序
在使用深度缓冲的情况下,可以不用担心不透明物体的渲染顺序,但是要实现透明效果,就需要使用透明度混合,同时关闭深度写入。
渲染引擎一般会先对物体进行排序,再渲染,常用的方法是:
- 先渲染所以不透明物体,并开启它们的深度测试和深度写入
- 再将半透明物体按它们的远近进行排序,然后按顺序依次渲染,开启深度测试,关闭深度写入
UnityShader的渲染顺序
Queue标签决定模型归属于哪个渲染队列。
透明度测试
简单粗暴,透明度低于某个值就直接剔除掉。
无法得到真正的半透明效果,有深度写入,要么完全透明,要么完全不透明。
透明度混合
真正的实现半透明效果
当前片元的透明度作为混合因子,与已经存储在颜色缓冲的颜色值进行混合,得到新的颜色。
双面渲染
一个物体如果透明能透过它看见其他物体,那也能透过它看到自身的结构,但是由于性能优化, 看不见的一面默认不渲染。
需要看见自己需要开启双面渲染,使用Cull命令。
Cull Back | Front | Off
分别表示提出背面,正面,关闭剔除。
Reference
[1] [Unity-shader入门精要-冯乐乐]