渲染流水线
主要分为应用阶段、几何阶段、光栅化阶段。
应用阶段:这一阶段最重要的输出是渲染所需的几何信息,即渲染图元。
几何阶段:重要任务就是把顶点坐标变换到屏幕空间。通过对输入的渲染图元进行多次处理后,输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息。
光栅化阶段:决定每个渲染图元中的那些像素应该被绘制在屏幕上。需要对上一个阶段得到的逐顶点数据进行插值,然后再进行逐像素处理。
应用阶段的工作
1.把数据加载到显存中。
硬盘--->内存--->显存
2.设置渲染状态。
定义场景中的网格是怎么被渲染的。例如使用哪个着色器、光源属性、材质等。
3.调用draw call。
draw call就是一个命令,由CPU发起,GPU接收。它的命令仅仅会指向一个需要被渲染的图元列表
几何阶段的工作
顶点着色器:是完全可编程的,通常用于实现顶点的空间变换、顶点着色器等。
曲面细分着色器:是一个可选的着色器,用于细分图元。
几何着色器:可选,可以被用于执行逐图元的着色操作,或者被用于产生更多的图元。
裁剪:可配置。将不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片。
屏幕映射:不可配置和不可编程,负责把每个图元的坐标转换到屏幕坐标系中。
光栅化阶段的工作
三角形设置和三角形遍历:固定函数的阶段。
片元着色器:可编程,用于实现逐片元的着色操作。
逐片元操作:不可编程,可配置。负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等。
顶点着色器
处理单位是顶点。
不创建和销毁任何顶点,无法得到顶点与顶点之间的关系。
GPU可以利用本身的特性并行化处理每个顶点,速度很快。
可以实现坐标变换和逐顶点光照,输出信息。
把顶点坐标从模型空间转换到齐次裁剪空间。
裁剪
裁剪不在摄像机视野范围内的
对于一部分在视野内一部分在视野外的,裁剪掉在视野外的部分,使用新的顶点代替与摄像机的交点。
屏幕映射
把每个图元的x、y坐标转换到屏幕坐标系
不对输入的z坐标做任何处理。
屏幕坐标系和z坐标构成窗口坐标系。
屏幕映射得到的屏幕坐标决定了对这个顶点对应屏幕上那个像素以及距离这个像素有多远。
小心OpenGL和DirectX之间的差异
三角形设置
会计算光栅化一个三角形所需的信息。计算三角网格表示数据的过程。
三角形遍历
检查每个像素是否被一个三角网格所覆盖。
被覆盖就会生成一个片元。
片元不是像素,而是包含很多状态的集合,用于计算每个像素的颜色。
这些状态包含,屏幕坐标、深度、顶点信息、法线、纹理坐标等。
片元着色器
可编程,输出一个或多个颜色值。
可完成很多重要的渲染技术,如纹理采样。
仅可以影响单个片元。
逐片元操作
决定每个片元的可见性,深度测试、模板测试等
对通过测试的片元颜色值和已经存储在颜色缓冲区中的颜色进行合并。
片元--->模板测试--->深度测试--->混合--->颜色缓冲区
模板测试,GPU读取模板缓冲区中改片元的模板值,将改值和读取到的参考值比较,可以指定比较函数。没有通过模板测试就会被舍弃。
不管该片元有没有通过模板测试都可以根据模板测试或深度测试修改模板缓冲区。
模板测试通常用于限制渲染的区域,还能用于渲染阴影、轮廓渲染等。
深度测试,把该片元的深度值和已经存在深度缓冲区中的深度值比较,比较函数可设置,舍弃没有通过测试的片元。
没有通过深度测试的片元没有权利再改变深度缓冲区的值。
通过测试的还可以指定该片元深度值覆盖原来的深度值,方法是深度写入,如实现透明效果。
混合,可以开启或关闭混合功能,没有开启会直接覆盖。
曲面细分着色器
将一个低模的模型,通过渲染管线的曲面细分功能,得到一个高模的结果。
曲面细分着色器包含细分控制着色器 (Tessellation Control Shader / Hull Shader) 和细分计算着色器 (Tessellation Evaluation Shader / Domain Shader) 。
细分控制着色器 (Tessellation Control Shader / Hull Shader) 需要设置:
patch ,比如三角形 UNITY_domain("tri")
如何切割 patch, UNITY_partitioning("fractional_odd")
输出 patch 的拓扑结构,比如三角形朝向为顺时针 UNITY_outputtopology("triangle_cw")
在函数 hsConstFunc 中确定 patch 被细分的因子, UNITY_patchconstantfunc("hsConstFunc")
输出 patch 的顶点数量,UNITY_outputcontrolpoints(3)
细分计算着色器 (Tessellation Evaluation Shader / Domain Shader) ,拿到上一阶段的数据后,对 patch 内的顶点进行插值、置换等一切需要提供给光栅化阶段的操作。
Hull Shader实际上由两部分组成
1.Constant Hull Shader
2.Control Point Hull Shader
constant hull shader
对于每一个patch(原始三角片) 都会执行一次这个constant hull shader,其功能是用来输出所谓的细分因子(tessellation factor).细分因子用于在tessellation 阶段告诉硬件如何对patch进行细分。
constant hull shader 通过InputPatch
control point hull shader
control point hull shader使用多个控制点作为输入(原始模型顶点),并且输出多个控制点。每输出一个控制点都会调用一次 control point hull shader。通常在hull shader阶段输出的控制点数目和输入的控制点数目一致,除非我们要改变模型的几何结构,例如把一个三角面输出为一个三阶贝塞尔曲面。真真正的曲面细分实在下一个tessellation stage完成的。
control point hull shader是每一个输出的control point 都要执行一次,因此这里引入了SV_OutputControlPointID语义修饰的参数i,表示当前hull shader正在处理的那个 control point 的索引。
the tessellation stage
作为程序员我们没法控制 tessellation stage的执行,该阶段的任务都是由硬件完成的,硬件根据constant hull shader输出的细分因子和control point,来决定如何对patch进行细分。
the domain shader
tessellation stage输出我们新创建的所有顶点。 对于每一个tessellation stage输出的顶点都会调用一次domain shader.当开启曲面细分时,vertex shader的功能是处理每一个control point,而domain shader才是实际上的处理细分的patch的顶点着色器。
使用中,我们通常在这里把细分过的顶点坐标,投影到齐次裁减空间,包括顶点法线,切线,UV的处理都在这里执行。在domain shader中,以 constant hull shader输出的细分因子和control point hull shader 输出的control point,以及和细分过的顶点位置相关的参数化的(u,v,w)坐标作为输入,使用这个和实际顶点位置一一对应的参数化的(u,v,w)坐标以及其他输入参数,我们可以计算得到实际的顶点坐标。
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
domain: patch 类型或者叫做模型图元的拓扑结构有效参数为tri,quad,isoline。tri表示三角面
partitioning:指定曲面细分的拆分模式
integer:新的顶点只在细分因子为整数时进行添加或删除,细分因子的小数部分被忽略,这种情况下对于动态曲面细分因子,会出现在某个点(整数点)模型精度突然提高或减低,会有肉眼可见的精度突变
fraction:新的顶点依然在位于整数细分因子时(1.0,2.0,3.0等)进行添加或删除,但是根据细分因子的小数部分进行平滑过度。fraction模式,包括fractional_even和fractional_odd
outputtopology:输出的三角面正面的环绕方式
triangle_cw:顶点顺时针排列代表正面
triangle_ccw:顶点逆时针排列代表正面
line:只针对line的细分
outputcontrolpoints:输出的control point 的数目,同时也是control point hull shader的执行次数,因为每输出一个control point ,都执行一次hull shader.SV_OutputControlPointID语义指定的索引值的总数,对应于这里的control point数目
patchconstantfunc:一个字符串指定的constant hull shader 函数的名字,既前面的constant hull shader
maxtessfactor:告诉硬件最大的细分因子,有了此上限值,硬件可以执行某些优化,例如可以预先知道需要多少资源来执行曲面细分。Direct3D 11支持的最大值为64.其他硬件支持的最大值可能为16,因此unity中使用通常使用16作为最大值上限。
几何着色器
几何着色器有意思的地方在于它在提交到下一个流水线前可以把(一个或多个)顶点转变为完全不同的基本图形,也可以用一个点生成新的几何图元。
首先 [maxvertexcount(num)]这个必须写在geomshader前面,是不可少的,主要是定义输出顶点的最大数量,输出顶点可以每次都不同,但是不超过这个数就行,注意这个数不要太大,影响性能
//point:输入类型,见下表
//VS_OUTPUT:顶点着色器传进来的类型,可以自定义结构体,v2g、v2f...
//IN[1]:IN为输入变量名,可以自定义,里面的1是顶点数量,根据输入类型,见下表
point VS_OUTPUT IN[1]
point 输入图元为点 1
line 输入图元为线 2
triangle 输入图元为三角形 3
lineadj 输入图元为带有邻接信息的直线,由4个顶点构成3条线 4
triangleadj 输入图元为带有邻接信息的三角形,由6个顶点构成 6
//inout:关键词
//TriangleStream:输出类型,见下表及样例
//GS_OUTPUT:几何着色器传出去的类型,可以自定义结构体,g2f、v2f...
//triStream:输出类型变量名
inout TriangleStream
输出类型 描述
PointStream 输出图元为点
LineStream 输出图元为线
TriangleStream 输出图元为三角形
模板测试
stencil与颜色缓冲区和深度缓冲区类似,模板缓冲区可以为屏幕上的每个像素点保存一个无符号整数值(通常的话是个8位整数)。这个值的具体意义视程序的具体应用而定。在渲染的过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值。这个比较的过程被称为模板测试。
stencil完整语法格式如下:
stencil{
Ref referenceValue
ReadMask readMask
WriteMask writeMask
Comp comparisonFunction
Pass stencilOperation
Fail stencilOperation
ZFail stencilOperation
}
Ref用来设定参考值referenceValue,这个值将用来与模板缓冲中的值进行比较。referenceValue是一个取值范围位0-255的整数。
ReadMask 从字面意思的理解就是读遮罩,readMask将和referenceValue以及stencilBufferValue进行按位与(&)操作,readMask取值范围也是0-255的整数,默认值为255,二进制位11111111,即读取的时候不对referenceValue和stencilBufferValue产生效果,读取的还是原始值。
WriteMask是当写入模板缓冲时进行掩码操作(按位与【&】),writeMask取值范围是0-255的整数,默认值也是255,即当修改stencilBufferValue值时,写入的仍然是原始值。
Comp是定义参考值(referenceValue)与缓冲值(stencilBufferValue)比较的操作函数,默认值:always
comparisonFunction比较操作通过Comp命令定义,公式左右两边的结果将通过它进行判断,其取值及其意义如下面列表所示。
Greater 相当于“>”操作,即仅当左边>右边,模板测试通过,渲染像素
GEqual 相当于“>=”操作,即仅当左边>=右边,模板测试通过,渲染像素
Less 相当于“<”操作,即仅当左边<右边,模板测试通过,渲染像素
LEqual 相当于“<=”操作,即仅当左边<=右边,模板测试通过,渲染像素
Equal 相当于“=”操作,即仅当左边=右边,模板测试通过,渲染像素
NotEqual 相当于“!=”操作,即仅当左边!=右边,模板测试通过,渲染像素
Always 不管公式两边为何值,模板测试总是通过,渲染像素
Never 不敢公式两边为何值,模板测试总是失败 ,像素被抛弃
if(referenceValue&readMask comparisonFunction stencilBufferValue&readMask)
Pass 当模版测试和深度测试都通过时,进行处理
Fail 当模版测试和深度测试都失败时,进行处理
ZFail 当模版测试通过而深度测试失败时,进行处理
pass,Fail,ZFail都属于Stencil操作,他们参数统一如下:
Keep 保留当前缓冲中的内容,即stencilBufferValue不变。
Zero 将0写入缓冲,即stencilBufferValue值变为0。
Replace 将参考值写入缓冲,即将referenceValue赋值给stencilBufferValue。
IncrSat stencilBufferValue加1,如果stencilBufferValue超过255了,那么保留为255,即不大于255。
DecrSat stencilBufferValue减1,如果stencilBufferValue超过为0,那么保留为0,即不小于0。
Invert 将当前模板缓冲值(stencilBufferValue)按位取反
IncrWrap 当前缓冲的值加1,如果缓冲值超过255了,那么变成0,(然后继续自增)。
DecrWrap 当前缓冲的值减1,如果缓冲值已经为0,那么变成255,(然后继续自减) 。
使用模板缓冲区最重要的两个值:当前模板缓冲值(stencilBufferValue)和模板参考值(referenceValue)
模板测试主要就是对这个两个值使用特定的比较操作:Never,Always,Less ,LEqual,Greater,Equal等等。
模板测试之后要对模板缓冲区的值(stencilBufferValue)进行更新操作,更新操作包括:Keep,Zero,Replace,IncrSat,DecrSat,Invert等等。
模板测试之后可以根据结果对模板缓冲区做不同的更新操作,比如模板测试成功操作Pass,模板测试失败操作Fail,深度测试失败操作ZFail,还有正对正面和背面精确更新操作PassBack,PassFront,FailBack等等。
深度测试
深度缓存(zwrite) + 颜色缓存(ztest)
系统中存在一个颜色缓冲区和一个深度缓冲区,分别存储颜色值和深度值,来决定画面上应该显示什么颜色。深度值是物体在世界空间中距离摄像机的远近,距离越近深度值越小,反之越大。
Shader深度渲染队列Queue预定义值:Background(1000)、Geometry(2000)、AlphaTest(2450)、Transparent(3000)、Overlay(4000)。
渲染优先顺序: Queue值越小越先渲染,后渲染的物体会覆盖先渲染的物体。深度测试和深度写入可改变渲染顺序。
zwrite 分类
zwrite on / off : 开启深度缓冲/关闭深度缓冲(其实就是记录Z值,把最小的Z存放在缓冲中),一般情况不透明的物体都会使用zwrite on 而半透明的物体都会使用 zwrite off 这样半透明的物体就直接比较ztest了
ztest 分类
LEqua小于等于、Less小于、Greater大于、Equal等于 、NotEqual不等于、always(其实就是拿着当前的像素来对深度缓冲中的z值进行比较的条件)
那么他们的情况就有如下几种:
1. 当ZWrite为On时,ZTest通过时,该像素的深度才能成功写入深度缓存,同时因为ZTest通过了,该像素的颜色值也会写入颜色缓存。
2. 当ZWrite为On时,ZTest不通过时,该像素的深度不能成功写入深度缓存,同时因为ZTest不通过,该像素的颜色值不会写入颜色缓存。
3. 当ZWrite为Off时,ZTest通过时,该像素的深度不能成功写入深度缓存,同时因为ZTest通过了,该像素的颜色值会写入颜色缓存。
4. 当ZWrite为Off时,ZTest不通过时,该像素的深度不能成功写入深度缓存,同时因为ZTest不通过,该像素的颜色值不会写入颜色缓存。
总结:zwrite只是处理深度值,ztest才是决定采用谁的颜色值,zwrite只是为ztest服务的。默认情况下unity采用的是zwrite为on,ztest为lqeual 也就是越靠近相机的显示优先级越高。
混合
在编写shader时我们可以在SubShader或Pass中用Blend与BlendOp指明该对象与下一层色彩如何进行颜色混合,可简单理解为 指上下层图片相互有重叠时如何取色的一种称呼.
Blend Off:关闭混合(这是默认设置)
Blend SrcFactor DstFactor SrcFactor是源系数,DstFactor是目标系数
最终颜色 = (Shader计算出的点颜色值 * 源系数)+(点累积颜色 * 目标系数)
Blend SrcFactor DstFactor:配置并启用混合。生成的颜色乘以SrcFactor。屏幕上已经存在的颜色乘以DstFactor,然后将两者相加。
Blend SrcFactor DstFactor, SrcFactorA DstFactorA:与上述相同,但使用不同的因子来混合Alpha通道。
BlendOp Op:不要将混合的颜色加在一起,而是对它们执行不同的操作。
BlendOp OpColor, OpAlpha:与上述相同,但对颜色(RGB)和alpha(A)通道使用不同的混合操作。
Blend N SrcFactor DstFactor
N:
Add 一起添加源和目标。
Sub 从源中减去目标。
RevSub 从目标减去源。
Min 使用较小的源和目标。
Max 使用较大的源和目标。
......
AlphaToMask On:打开Alpha覆盖率。使用MSAA时,“覆盖率到覆盖率”将与像素着色器结果的alpha值成比例地修改多样本覆盖蒙版。与常规的alpha测试相比,它通常用于更少的锯齿轮廓。对植被和其他经过Alpha测试的着色器有用。
One 值1-使用它使源或目标颜色完全通过。
Zero 零值-使用它可以删除源值或目标值。
SrcColor 此阶段的值乘以源颜色值。
SrcAlpha 此阶段的值乘以源alpha值。
DstColor 此阶段的值乘以帧缓冲区源颜色值。
DstAlpha 此阶段的值乘以帧缓冲区源alpha值。
OneMinusSrcColor 该阶段的值乘以(1-源颜色)。
OneMinusSrcAlpha 此阶段的值乘以(1-源alpha)。
OneMinusDstColor 该阶段的值乘以(1-目标颜色)。
OneMinusDstAlpha 该阶段的值乘以(1-目标alpha)。
下边是一个使用了曲面细分着色器的shader
Shader "Unlit/xifenjihe"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex vert
#pragma hull HS
#pragma domain DS
#pragma fragment frag
#pragma geometry geom
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct app_data
{
float4 positionOS:POSITION;
};
struct VertexOut
{
float3 PosL:TEXCOORD0;
};
VertexOut vert(app_data IN)
{
VertexOut o;
o.PosL = IN.positionOS.xyz;
return o;
}
struct PatchTess
{
float EdgeTess[3]:SV_TessFactor;//在面片的每个边缘上定义镶嵌数量。
float InsideTess : SV_InsideTessFactor;//定义贴片表面内的镶嵌数量。
};
PatchTess ConstantHS(InputPatch patch,uint patchID:SV_PrimitiveID)
{
PatchTess pt;
pt.EdgeTess[0] = 3;
pt.EdgeTess[1] = 3;
pt.EdgeTess[2] = 3;
pt.InsideTess = 3;
return pt;
}
struct HullOut
{
float3 PosL:TEXCOORD0;
};
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
//SV_OutputControlPointID 定义通过调用Hull Shader的主入口进行操作的控制点ID的索引。
HullOut HS(InputPatch p,uint i:SV_OutputControlPointID)
{
HullOut hout;
hout.PosL = p[i].PosL;
return p[i];// hout;
}
struct DomainOut
{
float4 PosH:SV_POSITION;
};
[domain("tri")]
//SV_DomainLocation 定义要评估的当前点在hull上的位置。
DomainOut DS(PatchTess patchTess,float3 baryCoords:SV_DomainLocation,const OutputPatch triangles)
{
DomainOut dout;
float3 p = triangles[0].PosL * baryCoords.x + triangles[1].PosL * baryCoords.y + triangles[2].PosL * baryCoords.z;
dout.PosH = TransformObjectToHClip(p.xyz);
return dout;
}
half4 frag(DomainOut IN) :SV_Target
{
return half4(1,1,1,1);
}
ENDHLSL
}
}
}