摘自冯乐乐《shader入门精要》
在效果图中如果使用repeat模式在高光区域中有一些黑点,这是由于浮点精度造成的,当使用float2(halfLambert,halfLambert)对渐变纹理进行采样,虽然理论在[0,1]之间,但是有时会出现1.0001的值,使用repeat模式会只保留小数部分,得到的值为0.0001因此把Wrap Mode改成Clamp就可解决该问题
遮罩纹理:
流程:
采样得到遮罩纹理的纹素值,然后使用几个pass通道和表面的属性相乘
步骤:
声明一个贴图2D纹理和一个2D法线纹理和一个2D遮罩纹理,顶点函数不变,在片元函数中多加了这几行
这里的遮罩纹理的rgb都一样所以取了一个来控制高光反射的强度,
透明效果:
透明度为1表示完全不透明,0表示完全透明
对于不透明的物体,不考虑渲染顺序也能得到正确的排序效果,这是由于深度缓冲,z-buffer的存在。
深度缓冲:
解决了物体可见性的问题,如果开启了深度测试,那么渲染一个片元时把该片元的深度值和深度缓冲中的值进行比较,如果值距离相机更远,就不应该渲染它,否则,覆盖,并把该深度值保存到深度缓冲当中
所以绘制一个实心物体,要判断哪个像素渲染哪个放弃,就要开启深度写入,绘制半透明物体,就要关闭
对于实现透明效果有以下两种方式:
透明度测试:
类似于深度测试,片元透明度满足条件,开启深度测试和深度写入,否则舍弃该片元,通常,我们会在片元着色器中使用clip 函数来进行透明度测试。clip 是CG中的一个函数
使用了透明度测试的Shader 都应该在SubShader 中设置这三个标签
在片元函数中加上
clip(texColor.a - _Cutoff); //透明度测试
透明度混合:
用当前片元的透明度和已经存储在颜色缓冲中的颜色混合得到新的颜色,但是透明度混合需要关闭深度写入,因为深度写入是按照实心物体和透明物体来处理,注意的是,透明度混合只关闭了深度写入,但没有关闭深度测试,因为,混合片元颜色时,还是会比较片元深度和深度缓冲中的深度,如果它的深度比缓冲中更远,就不再混合了。因此,渲染顺序非常重要了。
Blend混合:
把源颜色的混合因子SrcFactor 设置为SrcAlpha ,而目标颜色的混合因子DstFactor 设为 OneMinusSrcAlpha。这意味着,经过混合后新的颜色是:
如果blend有四个值,前两个是源和目标的rgb的混合,后两个为源和目标的alpha的混合
如果是三个值,比如这样:Blend SrcAlpha OneMinusSrcAlpha, One Zero //混合后输出颜色遵从混合,透明度值就是源颜色的透明度(alpha的源为one目标zero)
blend命令是把两个混合值相加,如果是相减用BlendOp
代码:
SubShader{
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass{
Tags {"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
然后设置一个变量控制透明度,返回的alpha乘以这个变量
fixed4 texColor = tex2D(_MainTex,i.uv);
return fixed4(ambient + diffuse,texColor.a * _AlphaScale);
对于透明度测试和透明度混合都可以实现这样的效果:
渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是:
1)先渲染所有不透明的物体,并开启它们的深度测试和深度写入。
2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
引入的问题:
写入半透明效果成了左边图片我们希望是右边的那样,原因是我们关闭了深度写入造成的,因为这样我们就无法对模型进行像素级别的深度排序
解决方法:
使用两个Pass 来渲染模型:第一个Pass 开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个Pass 进行正常的透明度混合,由于上一个Pass 已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。但这种方法的缺点在于,多使用一个Pass 会对性能造成一定的影响。我们使用这种方法,我们仍然可以实现模型与它后面的 背景混合的效果,但模型内部之间不会有任何真正的半透明效果。
做法:
Pass{
ZWrite On
ColorMask 0
}
Pass{
ags {"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
双面渲染的透明效果
无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,我们使用Cull 指令来控制需要剔除哪个面的渲染图元。
Cull Back | Front | Off
如果设置为Back,那么那些背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态:如果设置为Front,那么那些朝向摄像机的渲染图元就不会被渲染;如果设置为Off,就会关闭剔除功能,那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是用于特殊效果,例如这里的双面渲染的透明效果,通常情况下是不会关闭剔除功能的。
让使用了透明度测试的物体实现双面渲染效果。在Pass 中添加一行代码Cull Off
让透明度混合实现双面渲染会更复杂一些,把双面渲染的工作分成两个Pass——第一个Pass只渲染背面,第二个Pass只渲染正面,保证背面总是在正面被渲染之前渲染,从而可以保证正确的深度渲染关系。
SubShader{
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass{
Tags{"LightMode"="ForwardBase"}
Cull Front
}
Pass{
Tags{"LightMode"="ForwardBase"}
Cull Back
渲染路径:
如果要和光源打交道,我们需要为每个Pass指定它使用的渲染路径,Unity5.0版本之前,主要有3种:前向渲染路径(Forward Rendering Path)、延迟渲染路径(Deferred Rendering Path)和顶点照明渲染路径(Vertex Lit Rendering Path)。
前向渲染路径是传统的渲染方式,也是我们最常用的一种渲染路径。
前向渲染路径原理:每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值。
前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。例如,如果我们在场景的某一块区域放置了多个光源,这些光源影像的区域互相重叠,那么为了得到最终的光照效果,我们就需要为该区域内的每个物体执行多个Pass来计算不同光源对物体的光照结果,然后再颜色缓存中把这些结果混合起来得到最终的光照。然而,每执行一个Pass我们都需要重新渲染一遍物体,但很多计算实际上是重复的。
延迟渲染是一张更古老的渲染办法,但由于上述前向渲染可能造成的瓶颈问题,近几年又流行起来.
延迟渲染的原理:延迟渲染主要包含了两个Pass。在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中。然后,在第二个Pass中,我们利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫发射系数等,进行真正的光照计算。
前向渲染有两种Pass:Base Pass 和 Addition Pass,
环境光和自发光也是在Base Pass 中计算的。这是因为,对于一个物体来说,环境光和自发光我们只希望计算一次即可,而如果我们在Addtional Pass 中计算这两种光照,就会造成多次叠加环境光和自发光,这不是我们想要的。
对于前向渲染来说,一个Unity Shader通常会定义一个Base Pass以及一次Addtional Pass。一个Base Pass 仅会执行一次,而一个Addtional Pass 会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次Addtional Pass。
在前向渲染中,当我们渲染一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如,距离该物体的远近、光源强度等)对这些光源进行一个重要度排序。判断规则如下:
场景中最亮的平行光总是按逐像素处理的。
渲染模式被设置成Not Important的光源,会按逐顶点或者SH处理。
如果根据以上规则得到的逐像素光源数量小于Quality Setting 中的逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染。
对于ForwardBase的代码和平常的一样,
SubShader{
Tags{"RenderType"="Opaque"}
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
//#pragma multi_compile_fwdbase可以保证我们在Shader中使用光照衰减等
//光照变量可以被正确赋值。这是不可缺少的
#pragma multi_compile_fwdbase
但是Additional 代码不太一样:
//通常来说,Addtional Pass的光照处理和Base Pass的处理方式是一样的,因此我们只需要把
//Base Pass的顶点和片元着色器代码复制到Additional Pass中,稍微修改一下即可。
Pass{
Tags{"LightMode"="ForwardAdd"}
//开启和设置了混合模式
//希望Additional Pass 计算得到的光照结果与之前的光照结果进行叠加。
//没有使用Blend命令的话,Additional Pass会直接覆盖掉之前的光照结果
//我们也可以选择其他Blend命令,如Blend SrcAlpha One
Blend One One
CGPROGRAM
//这个指令保证我们再Additional Pass中访问正确的光照变量
#pragma multi_compile_fwdadd
#pragma vertex vert
#pragma fragment frag
#include "Lightinh.cginc"
#include "AutoLight.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v){
v2f o;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World,v.vertex).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
//如果是平行光的话
#if def USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDor = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb*max(0,dot(worldNormal,worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir+viewDir);
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
//如果是平行光的话
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
//如果是点光源
#if defined(POINT)
float3 lightCoord = mul(_LightMatrix0,float4(i.worldPos,1)).xyz;
fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
//如果是聚光灯
#elif defined(SPOT)
float4 lightCoord = mul(_LightMatrix0,float4(i.worldPos,1));
fixed atten = (lightCoord.z > 0)*tex2D(_LightTexture0,lightCoord.xy/lightCoord.w+0.5).w*
tex2D(_LightTextureB0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
#else
fixed atten = 1.0
#endif
#endif
return fixed4((diffuse+specular)*atten,1.0);
}
Unity 的光照衰减
使用纹理查找计算衰减的方式来计算逐像素的点光源和聚光灯的衰减的。
Unity在内部使用一张名为_LightTexture0的纹理来计算光源衰减。需要注意的是,如果我们对该光源使用了cookie,那么衰减查找纹理是_LightTextureB0,但这里不讨论这种情况。我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了再光源空间中不同位置的点的衰减值。例如(0,0)点表明了与光源位置重合的点的衰减值,而(1,1)点表明了再光源空间中所关心的距离最远的点的衰减。
为了对_LightTexture0纹理采样得到给定点到该光源的衰减值,我们首先需要得到该点在光源空间中的位置,这是通过LightMatrix0变换矩阵得到。在之前,我们已经知道LightMatrix0可以把顶点从世界空间变换到光源空间。因此,我们只需要把_LightMatrix0和世界空间中的顶点坐标相乘即可得到光源空间中的相应位置:
float3 lightCoord = mul(_LightMatrix0,float4(i.worldPos,1)).xyz;
然后,我们可以使用这个坐标的模的平方对衰减纹理进行采样,得到衰减值:
fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
使用了光源空间中顶点距离的平方(通过dot函数来得到)来对纹理采样
尽管纹理采样的方法可以减少计算衰减的复杂度,但有时我们希望可以在代码中利用公式来计算光源的衰减。例如,下面的代码可以计算光源的线性衰减。
float distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
atten = 1.0/distance;
在实时渲染中,我们最常用的是一种名为Shadow Map 的技术。这种技术很简单,它会首先把摄像机位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是摄像机看不到的地方。而Unity就是使用这种技术。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
Unity选择使用一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode标签被设置为ShadowCaster的Pass。这个Pass的渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)。Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。
总结一下,一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。
如果我们想要一个物体接收来自其他物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity 中,这个过程通过为该物体执行LightMode 为ShadowCaster 的 Pass 来实现的。如果使用了屏幕空间的投射映射技术,Unity还会使用这个Pass 产生一张摄像机的深度纹理。
们在Unity中,我们可以渲染是否让一个物体投射或者接收阴影。这是通过设置Mesh Renderer组件的Cast Shadows和Receive Shadows 属性来实现的,如下图所示:
Cast Shadows 可以被设置为开启或关闭,如果开启了Cast Shadows属性,那么Unity就会把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。正如之前所说,这个过程是通过为物体执行LightMode 为ShadowCaster的Pass来实现的。Receive Shadows则可以选择是否让物体接收来自其他物体的阴影。如果没有开启Receive Shadows,那么当我们调用Unity的内置宏和变量计算阴影时,这些宏通过判断该物体没有开启接收阴影的功能,就不会再内部为我们计算阴影。
实际上,光照衰减和阴影对物体最终的渲染结果的影响本质上是相同的——我们都是把光照衰减因子和阴影值即光照结果相乘得到最终的渲染结果。那么,是不是可以有一个方法可以同时计算两个信息呢?好消息是,Unity在Shader里提供了这样的功能,主要是通过内置的UNITY_LIGHT_ATTENUATION宏来实现的。
在片元函数中加上:
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
透明度物体的阴影:我们从一开始就强调,想要在Unity里让物体能够向其他物体投射阴影,一定要在它使用的Unity Shader 中提供一个LightMode 为 ShadowCaster的Pass。在前面的例子中,我们使用内置的VertexLit中提供的ShadowCaster来投射阴影。VertexLit中的ShadowCaster实现很简单,它会正常渲染整个物体,然后把深度结果输出到一张深度图或阴影映射纹理中。
对于大多数不透明物体来说,把Fallback设为VertexLit就可以直接得到正确的阴影。但对于透明物体来说,我们就需要小心处理它的阴影。透明物体的实现通车会使用透明度测试或透明度混合,我们需要小心设置这些物体的Fallback。
透明度测试的处理比较简单,但如果我们仍然直接使用VertexLit、Diffuse、Specular等作为回调,往往无法得到正确的阴影。这是因为透明度测试需要再片元着色器中舍弃某些片元,而VertexLit中的阴影投射纹理并没有做这样从操作。
效果如下:
镂空区域出现了不正常的阴影,看起来就像这个正方体是一个普通的正方体一样。而这样并不是我们想要得到的,我们希望有些光应该是可以通过这些镂空区域偷过来的,这些区域不应该有阴影。出现这样的情况是因为,我们使用的是内置的VertexLit中提供的ShadowCaster来投射阴影,而这个Pass中并没有进行任何透明度测试的计算,因此,它会把整个物体的深度信息渲染到深度图和阴影映射纹理中。因此,如果我们想要得到经过透明度测试后的阴影效果,就需要提供一个有透明度测试功能的ShadowCaster Pass。当然,我们可以自行编写这样的Pass。
为了让使用透明度测试的物体得到正确的阴影效果,我们只需要在Unity Shader 中更改一行代码,即把Fallback 设置为"Transparent/Cutout/VertexLit",它的ShadowCaster Pass也计算了透明度而是,因此会把裁剪后的物体深度信息写入深度图和阴影映射纹理中。但需要注意的是,由于"Transparent/Cutout/VertexLit"中计算透明度测试,使用了名为_Cutoff的属性来进行透明度测试,因此,这要求我们的Shader中也必须提供名为_Cutoff的属性。否则,同样无法得到正确的阴影结果。
更改了Fallback之后,我们可以得到下图中的结果。
但是,这样的结果仍然有一些问题,例如出现了一些不应该透过光的部分。出现这种情况的原因是,默认情况下把物体渲染到深度图和阴影映射纹理中仅考虑物体的正面。但对于本例的正方体来说,由于一些面完全背对光源,因此这些面的深度信息没有加入到阴影映射纹理的计算中。为了得到正确的结果,我们可以将正方体的Mesh Renderer组件的Cast Shadows属性设置为Two Sided,强制Unity在计算阴影映射纹理时计算所有面的深度信息。下图给出了正确设置后的渲染结果。
与透明度测试的物体相比,想要为使用透明度混合的物体添加阴影是一件比较复杂的事情。事实上,所有内置的透明度混合的Unity Shader,如Transparent/VertexLit等,都没有包含阴影投射的Pass。这意味着,这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,它们不会向其他物体投射阴影,同样它们也不会接收来自其他物体的阴影