1.1 Unity shader入门精要笔记(八)
1.1.1 透明效果
在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道(Alpha Channel)。当开启透明混合后,当一个物体被渲染到屏幕时,每个片元出了颜色值和深度值外,还有另一个属性——透明度。当透明度为1时,表示该像素完全不透明,而为0时,该像素则完全不会显示。
Unity中常用两种方法实现透明效果,[1]透明度测试(Alpha Test)[2]透明度混合(Alpha Blending)
在对于不透明(opaque)物体,不考虑它们的渲染顺序也能得到正确的排序效果,是因为由于强大的深度缓存(depth buffer,也被称为z-buffer)的存在。
透明度测试:只要有一个片元的透明度不满足条件,那么它对应的片元就会被舍弃掉。被舍弃的片元将不会再进行任何处理,也不会对颜色缓存产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理。即透明度测试是不需要关闭深度写入的,他不透明物体最大的不同就是它会更具透明度舍弃一些片元。其要不就不透明,要不就完全透明,两个极端。
透明度混合:可以得到真正的半透明效果。它会使用当前片元的透明度作为因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但其要关闭深度写入。透明度只关闭了深度写入,没有关闭深度测试,意味着使用透明度混合渲染一个片元时,会比较它的深度值与当前深度缓存中的深度值。若其深度值距离摄像机更远,则不会再进行混合操作。该决定当一个透明物体出现在一个透明物体的前面,开发者先渲染不透明物体,但其任然可以正常地遮挡透明物体。从而对于透明度混合来说,深度缓冲是只读。
引擎通常渲染的顺序为:
[1]先渲染所有不透明物体,并开启他们的深度测试和深度写入
[2]把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些透明体悟,并开启深度测试关闭深度写入
而遇到循环重叠时候,萎了减少错误顺序的情况,开发者尽可能让模型是凸面体,并考虑将复杂的模型拆分成可以独立排序的多个子模型。而若不想分割网格可以将透明通道更加柔和,使穿插不那么明显,同时也可以使用开启深度写入的半透明效果来近似模拟物体的半透明。
1.1.2 Unity Shader的渲染顺序
Unity用SubShader的Queue标签来决定模型将归于哪个渲染队列中,从而为解决渲染顺序的问题提供了解决方案。Unity在内部使用了一系列整数索引来表示每个渲染队列,且索引号越小表示越早被渲染。
图 1.50 Unity提前定义的5个渲染队列
代码:
SubShader{ Tags{ "Queue" = "AlphaTest" } Pass{ ... ... } }
若想要通过透明度混合来实现透明效果,则代码包含:
SubShader{ Tags{ "Queue" = "Transparent"; } Pass{ ZWrite Off ... ... } }
ZWrite Off用于关闭深度写入,可以选择写入Pass中,也可以选择写入SubShader中,写入SubShader中则意味着将其下面的所有Pass都关闭深度写入。
1.1.3 透明度测试
透明度测试:只要一个片元的透明度不满足条件(通常小于某个阈值),那么其对应的片元就会被舍弃掉。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它。通常会在片元着色器中使用clip函数来进行透明度测试。
函数:
void clip(float4 x); void clip(float3 x); void clip(float2 x); void clip(float1 x);void clip(float x);
参数:裁剪时使用的标量或矢量条件
描述:如果给定函数的任何一个分量是负数,就会舍弃当前像素的输出颜色,等同于下面代码。
void clip(float4 x){ if(any(x<0)) discard; }
实践步骤:
[1]建场景并去掉天空盒子
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]拉入一个立方体,并把材质赋给该模型。同时在该立方体下面创建一个平面
[4]给该shader附上对应代码
Shader"Unity Shader Book/Chapter8/Alpha Test"{ Properties{ _Color("Color Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white"{} _Cutoff("Alpha CutOff", Range(0,1)) = 0.5 } SubShader{ Tags{ "Queue" = "AlphaTest" //Unity中透明度测试使用的渲染队列是名为Alpha Test的嘟咧,从而把Queue标签设置为Alpha Test "IgnoreProjector" = "True" //IgnoreProject设置为True则意味该Shader不受投影器Projectors的影响 "RenderType" = "TransparentCutout" //RenderType标签可以让Unity把该shader归入到提前定义的组(此为TransparentCutout组),以表明该Shader是一个使用了透明测试的Shader。 //RenderType通常被用于着色器替换功能 } Pass{ Tags{ "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff; //_Cutoff参数用于决定调用clip进行透明度测试时使用的判断条件,其范围为[0,1],纹理像素的透明度在该范围 struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; //留意这里worldpos要的是纹理属性,而不是Position,不一样的东西 float4 texcoord : TEXCOORD2; }; v2f vert(a2v v){ //在顶点着色器中计算出世界空间的法线方向和顶点位置及变换后的纹理坐标 v2f o; o.pos = mul(UNITY_MARTRIX_MVP, v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(_Object2World, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.vertex, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); //Alpha Test clip(texColor.a - _Cutoff); //Equal to if((texColor.a - _Cutoff) < 0.0){ //判断texColor.a - _Cutoff是否为负数,若是则舍弃该片元输出 discard; //剔除该片元 } fixed3 albedo = texColor.rgb * _Color.rgb; //反射率 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; //漫反射 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal, worldLightDir)); return fixed4(ambient + diffuse, 1.0); //计算得到环境光和漫反射光相加 } ENDCG } } FallBack"Transparent/Cutout/VertexLit" //保证该SubShader无法在当前显卡工作时可以有合适的代替shader,保证使用透明度测试的物体可以正确地想其他物体投射阴影 }
图 1.51 随着Alpha Cutoff增大,更多像素被剔除
1.1.4 透明度混合
透明度混合:该方法可以得到真正的半透明效果,它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是透明度混合需要关闭深度写入,这需要十分小心物体渲染顺序。而想要实现半透明的效果需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合,混合时使用的函数就是由该指令决定。
第二种语义在设置混合因子的同时也开启了混合模式。只有开启了混合之后,设置片元的透明通道才有意义,而Unity在开发者使用Blender命令的时候就自动打开。而如果模型没有任何透明效果,往往是因为没有在Pass中使用Blend命令,一方面是没有设置混合因子,另一方面是根本没有打开混合模式。而把原颜色的混合因子CrcFactor设为SrcAlpha,而目标颜色的混合因子DstFactor设为OneMinusSrcAlpha。这意味着混合后新的颜色是:
实践步骤:
[1]建场景并去掉天空盒子
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]拉入一个立方体,并把材质赋给该模型。同时在该立方体下面创建一个平面
[4]给该shader附上对应代码
Shader"Unity Shader Book/Chapter7/Alpha blend"{ Properties{ _Color("Color Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white"{} _AlphaScale("Alpha Scale", Range(0,1)) = 1 //构建新属性_AlphaScale代替_Cutoff属性。_AlphaScale用于在透明纹理的基础上控制整体的透明度 } SubShader{ Tags{ "Queue" = "Transparent" //Unity中透明度混合使用的渲染队列是Transparent的队列 "IgnoreProjector" = "True" //IgnoreProject设置为True则意味该Shader不受投影器Projectors的影响 "RenderType" = "Transparent" } Pass{ Tags{ "LightMode" = "ForwardBase" } ZWrite Off //选择合适的混合状态设置 Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _AlphaScale; struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; //留意这里worldpos要的是纹理属性,而不是Position,不一样的东西 float4 texcoord : TEXCOORD2; }; v2f vert(a2v v){ //在顶点着色器中计算出世界空间的法线方向和顶点位置及变换后的纹理坐标 v2f o; o.pos = mul(UNITY_MARTRIX_MVP, v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(_Object2World, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.vertex, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); fixed3 albedo = texColor.rgb * _Color.rgb; //反射率 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; //漫反射 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal, worldLightDir)); return fixed4(ambient + diffuse, texxColor.a * _AlphaScale); //纹理像素的透明通道和材质参数_AlphaScale乘积 } ENDCG } } FallBack"Transparent/VertexLit" }
图 1.52 随着Alpha Scale的减小,模型变得越发透明
但模型很多时候本身就有复杂的遮挡关系(如一个蜷缩在一块的圆管,其整体有前有后有叠加在一起)或包含了复杂的非凸图网格,此时会产生各种因为排序错误而产生的错误的透明效果。这是由于关闭了深度写入而产生的,对此可以通过分割网格或者重新利用深度写入,从而让模型可以像半透明模型一样淡入淡出。
引:关于深度写入与深度测试的关系:https://blog.csdn.net/qq_36761511/article/details/113616867
——————————————————————————————————
1.1.5 开启深度写入的半透明效果
对于关闭深度写入而造成的错误排序的情况,可以通过使用使用两个Pass来渲染模型,第一个Pass开启深度写入,但不输出颜色,其目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个Pass进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。
缺点在于多一个Pass会对性能造成一定的影响。(个人理解就是说,先是对模型自身单独进行一个深度写入,然后将模型自身如果后面的部分被前面部分遮挡,则放弃后面的片元,只留前面的片元。而之所以不直接都在一个pass里面写是因为它不可以和渲染的部分写在同一个pass里面,如果写在同一个pass里面将会导致如果一个半透明的物体在一个不透明的物体前面,进行深度写入后,后面的不透明物体的片元也会被舍弃掉。而如果分开两个pass写则不会产生这种问题)
实践步骤:
[1]建场景并去掉天空盒子
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]拉入一个立方体,并把材质赋给该模型。同时在该立方体下面创建一个平面
[4]给该shader附上对应代码
Shader"Unity Shader Book/Chapter7/Alpha blend"{ Properties{ _Color("Color Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white"{} _AlphaScale("Alpha Scale", Range(0,1)) = 1 //构建新属性_AlphaScale代替_Cutoff属性。_AlphaScale用于在透明纹理的基础上控制整体的透明度 } SubShader{ Tags{ "Queue" = "Transparent" //Unity中透明度混合使用的渲染队列是Transparent的队列 "IgnoreProjector" = "True" //IgnoreProject设置为True则意味该Shader不受投影器Projectors的影响 "RenderType" = "Transparent" } //额外的pass来进行深度缓冲 //增加的Pass仅是为了把模型的深度信息写入深度缓冲中,从而剔除模型中被资深遮挡的片元 Pass{ ZWrite On ColorMask 0 //ColorMask用于设置颜色通道的写掩码(write mask)。当ColorMask设为0时,意味着该Pass不写入任何颜色通道,即不输出任何颜色 //满足了该Pass只需写入深度缓存即可的需要 } Pass{ Tags{ "LightMode" = "ForwardBase" } ZWrite Off //选择合适的混合状态设置 Blend SrcAlpha OneMinusSrcAlpha } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _AlphaScale; struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float4 texcoord : TEXCOORD2; }; v2f vert(a2v v){ v2f o; o.pos = mul(UNITY_MARTRIX_MVP, v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(_Object2World, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.vertex, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); fixed3 albedo = texColor.rgb * _Color.rgb; //反射率 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; //漫反射 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal, worldLightDir)); return fixed4(ambient + diffuse, texxColor.a * _AlphaScale); //纹理像素的透明通道和材质参数_AlphaScale乘积 } ENDCG } } FallBack"Diffuse" }
1.1.6 shaderlab的混合指令
混合的产生:将源颜色S(指片元着色器产生的颜色值)与目标颜色D(指从颜色缓冲中读取到的颜色值)进行混合后得到的输出颜色O,然后将其重新写入颜色缓冲中。而不管是源颜色还是目标颜色、输出颜色都包含了RGBA四通道(A为Alpha通道)。
想要使用混合,必须开启它。在Unity中使用Blend(Blend Off命令除外),命令时,出了设置混合状态外也开启了混合。而其他图形API需要手动开启,如在OpenGL中用glEnable(GL_BLEND)开启混合。
混合等式和参数:
混合是一个逐片元的操作,它不是可编程的,但是是高度可配置的。开发者可以通过设置混合时使用的运算操作、混合因子等影响混。
进行混合的时候需要两个混合等式,一个用于混合RGB通道,一个用于混合Alpha通道。开发者需要设置混合等式中的操作和因子,默认情况下,混合等式使用的操作都是加操作(也可以有别的操作),开发只需要再设置一下混合因子即可。每个等式有两个因子(一个用于和源颜色相乘,一个用于和目标颜色相乘),两条混合等式,从而总共有四个因子。
图 1.53 ShaderLab设置混合因子的命令
公式:
图 1.54 ShaderLab可用的混合因子类型
在上面指令进行设置时,RGB通道的混合因子和A通道的混合因子都是一样的,而若希望使用不同的参数混合A通道时,可以利用Blend SrcFactor DstFactor, SrcFactorA DstFractorA指令。如:若想混合后,输出颜色的透明度和源颜色的透明度一致,可使用下面命令:
Blend SrcAlpha OneMinusSrcAlpha, One Zero
混合操作:
同时ShaderLab支持多种混合操作命令,开发者可以通过BlendOp BlendOperation命令来实现。
图 1.55 ShaderLab支持的混合操作
常见的混合类型:
通过混合操作和混合因子命令的组合,类似ps的混合效果:
//正常(Normal),即透明度混合 Blend SrcAlpha OneMinusSrcAlpha //柔和相加(Soft Additive) Blend OneMinusDstColor One //正片叠底(Multiply),即相乘 Blend OneMinusDstColor Zero //两倍相乘(2x Multiply) Blend DstColor SrcColor //变暗(Darken) BlendOp Min Blend One One //变亮(Lighten) BlendOp Max Blend One One //滤色(Screen) Blend OneMinusDstColor One //等同于 Blend One OneMinusSrcColor //线性减淡(Linear Dodge) Blend One One
1.1.7 双面渲染的透明效果
渲染引擎在默认情况下将会剔除物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。而如果想要得到双面渲染的效果,可以使用Cull指令来控制需要剔除哪个面的渲染图元。
Cull语法: cull Back | Front | Off
设置成Back则背对摄像机的渲染图元就不会被渲染;如果设置为Front则朝向摄像机的渲染图元不被渲染;若设置为Off则关闭剔除功能,所有的渲染图元都能被渲染。
透明度测试的双面测试:
在透明度测试的基础上添加划横线的一行代码:
Pass{ Tags{ "LightMode" = "ForwardBase" } //在透明度测试代码的基础上,上面的一致不变 Cull Off //下面的一致不变 CGPROGRAM #pragma vertex vert #pragma fragment frag ... ...
图 1.56 双面渲染透明度测试的物体
透明度混合的双面渲染:
在透明度混合代码的基础上,新增一个与原pass一模一样的pass,然后第一个pass增加Cull Front,将朝向摄像头的片元先不渲染,只渲染背面的片元;第二个pass增加Cull Front,将背向摄像头的片元不渲染,从而该顺序执行后,保证了模型的先后都可以被渲染出来。
代码:
Shader"Unity Shader Book/Chapter8-AlphaBlend"{ Properties{ _Color("Color Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white"{} _AlphaScale("Alpha Scale", Range(0,1)) = 1 } SubShader{ Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" } Pass{ Tags{ "LightMode" = "ForwardBase" } Cull Front //不渲染朝向摄像机的渲染图元 ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _AlphaScale; struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; //留意这里worldpos要的是纹理属性,而不是Position,不一样的东西 float4 texcoord : TEXCOORD2; }; v2f vert(a2v v){ //在顶点着色器中计算出世界空间的法线方向和顶点位置及变换后的纹理坐标 v2f o; o.pos = mul(UNITY_MARTRIX_MVP, v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(_Object2World, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.vertex, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); fixed3 albedo = texColor.rgb * _Color.rgb; //反射率 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; //漫反射 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal, worldLightDir)); return fixed4(ambient + diffuse, texxColor.a * _AlphaScale); //纹理像素的透明通道和材质参数_AlphaScale乘积 } ENDCG } Pass{ Tags{ "LightMode" = "ForwardBase" } Cull Back //不渲染背对着摄像机的渲染图元 ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _AlphaScale; struct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; //留意这里worldpos要的是纹理属性,而不是Position,不一样的东西 float4 texcoord : TEXCOORD2; }; v2f vert(a2v v){ //在顶点着色器中计算出世界空间的法线方向和顶点位置及变换后的纹理坐标 v2f o; o.pos = mul(UNITY_MARTRIX_MVP, v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(_Object2World, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.vertex, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); fixed3 albedo = texColor.rgb * _Color.rgb; //反射率 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; //漫反射 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal, worldLightDir)); return fixed4(ambient + diffuse, texxColor.a * _AlphaScale); //纹理像素的透明通道和材质参数_AlphaScale乘积 } ENDCG } } FallBack"Transparent/VertexLit" }
图 1.57 双面渲染的透明度混合物体
1.2 Unity shader入门精要笔记(九)
1.2.1 Unity的渲染路径
在Unity里,渲染路径(Rendering Path)决定了光照时如何应用到Unity Shader中。只有在每个Pass指定它使用的渲染路径,为Shader正确地选择和设置了需要的渲染路径,该Shader的光照计算才能被正确执行。
Unity支持多种类型的渲染路径,Unity5.0版本前主要有3种,前向渲染路径(Forward Rending Path)、延迟渲染路径(Defferred Rendering Path)和顶点照明渲染路径(Vertex Lit Rendering Path)。在Unity5.0后,主要有两变化:[1]顶点照明渲染路径被Unity抛弃(目前仍然可以对之前使用了顶点照明渲染路径的Unity Shader兼容);[2]新的延迟渲染路径代替了原来的延迟渲染路径(目前也提供了对较旧版本的兼容)
大多数情况下,一个项目只使用一种渲染路径,开发者可以为整个项目设置渲染时的渲染路径。可通过Edit -> Project Settings -> Player -> Other Settings -> Rendering Path中选择项目所需的渲染路径。默认情况下选择的是前向渲染路径。
而如果希望可以使用多个渲染路径,可以在每个摄像机的渲染路径中设置该摄像机使用的渲染路径,以覆盖Project Settings中的设置,从而可以做到如:摄像机A渲染的物体使用前向渲染路径,摄像机B渲染的物体使用延迟渲染路径。在这里如果选择了Use Player Settings,则该摄像机会使用Project Settings中的设置,否则将会覆盖掉Project Settings的设置。
完成上述设置后,可以通过每个Pass中使用标签LightMode来制定Pass使用的渲染路径。不同类型路径可能包含多种标签设置。
如:
Pass{ Tags{ "LightMode" = "ForwardBase" }
这表示该Pass使用前向渲染路径中的ForwardBase路径。
图 1.58 LightMode标签支持的渲染路径设置选项
指定渲染路径是和Unity的底层渲染引擎的一次重要沟通,设定好正确的渲染路径可以让底层渲染引擎提前准备好所需要的属性。而如果没有指定任何渲染路径,那么一些光照变量可能不会被正确赋值,计算出的效果也有可能是错误的。
前向渲染路径:
每进行一次完整的前向渲染,开发者需要渲染该对象的渲染图元,并计算两个缓冲区的信息;一个是颜色缓冲区,一个是深度缓冲区。开发者可以利用深度缓冲来决定一个片元是否可见,如更新颜色缓冲区的颜色值,可通过一下伪代码描述:
Pass{ for (each primitive in this model){ //该模式原始的 for (each fragment covered by this primitive){ if(failed in depth test){ //如果没有通过深度测试,说明该片元不可见 discard; } else{ //如果该片元可见 //进行光照计算 float4 color = shading(materialInfo, pos, normal, lightDir, viewDir); //更新帧缓冲 writeFrameBuffer(fragment, color); } } } }
对于每个逐像素光源,开发者都需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的的影响区内,那么该物体需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设场景中有N个物体,每个物体受M个光源的影响,那么渲染整个场景一共需要N * M个Pass。
Unity中的前向渲染:
一个Pass不仅仅可以用来计算逐像素光照,它也可以用来计算逐顶点等其他光照。当渲染一个物体时,Unity会计算哪些光源会照亮它以及用什么方式照亮。
在Unity中,前向渲染路径有3种处理(照亮物体)方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics,SH)处理。决定一个光源使用哪种处理模式则取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否重要。
Unity会按照一定重要度顺序进行渲染,其中一定数目的光源会按照逐像素的方式处理,最多4个光源逐顶点方式处理,剩下光源按照SH方式处理。
Unity评判规则如下:
场景中最亮的平行光宗时按逐像素处理的
渲染模式被设置为Not Important的光源,会按逐顶点或者SH处理。
渲染模式被设置成Important的光源会按逐像素处理
如果按照以上规则得到的逐像素光源数目少于Quality Setting中的逐像素光照数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染。
前向渲染有两种Pass:Base Pass和Additional Pass。
图 1.59 前向渲染的两种Pass
#pragma multi_conpile_fwdbase等类似的编译指令保证Unity可以为响应类型的Pass生成所有需要的shader变种,这些变种会处理不同条件下的渲染逻辑,例如是否使用光照贴图、当前处理哪种光源类型、是否开启了阴影等。只有分别为Base Pass和Additional Pass使用这两个编译指令,开发者才可以在相关的Pass中得到一些正确的光照变量,例如光照衰减值等。
Base Pass中开发者可以访问光照纹理(lightmap)。
Base Pass中渲染的平行光默认是支持阴影的(如果开启了光源的阴影功能),而Additional Pass中渲染的光源在默认情况下没有阴影效果(即使在light组件下开启了有阴影的shadow type)。但实在想要阴影,可以在Additonal Pass中使用#pragma multi_compile_fwdadd_fullshadows代替#pragma multi_compile_fwdadd编译指令。
环境光和自发光也是在Base Pass中计算。(对于一个物体来说,环境光和自发光开发者只希望计算一次即可,而在Additional Pass中计算两种光照将会叠加多次环境光和自发光)
在Additional Pass的渲染设置中,还开启和设置了混合模式。这是因为希望每个Additional Pass可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终的有多个光照的渲染效果。若没有开启和设置混合模式,Additional Pass的渲染结果会覆盖掉之前的渲染结果,从而看起来好像该物体只受该光源的影响。而通常开发者选择的混合模式是Blend One One。
对于前向渲染,一个Unity Shader通常会定义一个Base Pass(Base Pass也可以定义多次,例如双面渲染)以及一个Additional Pass。一个Base Pass仅会执行一次(定义多个Base Pass除外),而一个Additional Pass则会根据该物体的其他逐像素光源的数目被多次调用,每个逐像素光源会执行一次Additional Pass。
内置的光照变量和函数:
根据使用的渲染路径(即Pass标签中的LightMode的值),Unity会把不同的光照变量传递给Shader。对于前向渲染(即LightMode为ForwardBase或ForwardAdd),下图为我们可以在Shader中访问到的光照变量:
图 1.60 前向渲染可以使用的内置光照变量
图 1.61 前向渲染可以使用的内置光照函数
顶点照明渲染路径:
顶点照明渲染路径是对硬件配置要求最少、运算性能最高,同时得到的效果也是最差的一种类型。不支持逐像素才能得到的效果,如:阴影、法线映射、高精度的高光反射等。所有在顶点照明渲染路径中实现的功能都可以在前向渲染路径中完成。而在前向渲染路径中也可以计算一些逐顶点的光源,但如果选择顶点照明渲染路径,那么Unity只会填充那些逐顶点相关的光源变量,不可以使用一下逐像素光照变量。
Unity中的顶点照明渲染:
顶点照明渲染路径通常在一个Pass中就可以完成对物体的渲染。在该Pass中,开发者会计算其所关心的所有光源对该物体的照明,并且这个计算是按逐顶点处理。(这是Unity中最快速的渲染路径,并且具有最广泛的硬件支持,游戏机不支持这种路径)
!顶点照明渲染路径仅仅是前向渲染路径的一个子集。
可访问的内置变量和函数:
在Unity中,一个顶点照明的Pass最多可以访问到8个逐顶点光源。如果影响该物体的光源数目小于8,那么数组剩下的光源颜色会设置成黑色。
图 1.62 顶点照明渲染路径中可以使用的内置变量
图 1.63 顶点照明渲染路径中可以使用的内置函数
延迟渲染路径:
前向渲染的问题在于:当场景总包含了大量实时光源时,前向渲染的性能会急速下降。当一区域有多个光源时,光源影响区域互相叠加,为得到最终光照效果,光源影响区域内的每个物体执行多个Pass来计算不同光源对该物体的光照结果,颜色缓冲中把这些结果混合在一起得到最终的光照。而每执行一个Pass都需要重新渲染一遍物体,从而很多计算是重复的。
而延迟渲染出了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区被统称为G缓冲(G-buffer)。G缓冲区存储了表面(通常指离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性。
延迟渲染的原理:
延迟渲染主要包含了两个Pass。在第一个Pass中,开发者不进行任何光照计算,仅仅是计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,就把它的相关信息存储到G缓冲区中。而第二个Pass中,则利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫反射系数等进行真正的光照计算。
可利用下面伪代码来描述:
Pass 1{ //第一个Pass不进行真正的光照计算 //仅仅把光照计算需要的信息存储到G缓冲中 for(each primitive in this model){ for(each fragment covered by this primitive){ if(failed in depth test){ //如果没有通过深度测试,说明该片元不可见 discard; } else{ //如果该片元可见 //把需要的信息存储到G缓冲中 writeGBuffer(materialInfo, pos, normal); } } } } Pass 2{ // for(each pixel in this screen){ for(the pixel is valid){ //如果该像素有效 //读取对应的G缓冲中的信息 readGBuffer(pixel, materialInfo, pos, normal); //根据读取到的信息进行光照计算 float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir); //更新帧缓冲 writeFrameBfuffer(pixel, color); } } }
Unity中的延迟渲染:
Unity有两种延迟渲染路径,一种是:Unity5之前使用的遗留的延迟渲染路径;另一种是:Unity5.x中使用的延迟渲染路径。新旧延迟渲染路径之间的差别很小,只是使用了不同的技术来权衡不同的需求。
延迟渲染路径中的每个光源都可以按逐像素的方式处理,但是,延迟渲染的缺点为:
[1]不支持真正的抗锯齿(anti-aliasing)功能
[2]不能处理半透明物体
[3]对显卡有一定的要求,如果要使用延迟渲染,显卡必须支持MRT、Shader Mode3.0及以上、深度渲染纹理以及双面的模版缓冲。
在Unity中要求提供的两个Pass,第一个Pass用于渲染G缓冲。物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息将会被渲染到屏幕空间的G缓冲区中。对于每个物体,该Pass只会被执行一次。
第二个Pass将会用于计算真正的光照模型,该Pass会使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。
默认的G缓冲区包含一下几个渲染纹理:RT0, RT1, RT2, RT3,深度缓冲和模版缓冲。
可访问的内置变量和函数
图 1.64 延迟渲染路径中可以使用的内置变量
1.2.2 Unity的光源类型
Unity一共支持4种光源类型:平行光、点光源、聚光灯和面光源(area light)。面光源尽在烘培时才发挥作用(同时如果没记错,其是最损耗性能的,计算量最大的)。
光源类型有什么影响:
最常使用的光源属性有光源的位置、方向(到某点的方向)、颜色、强度以及衰减(到某点的衰减,与该点到光源的距离有关)。
[1]平行光:
平行光可以照亮的范围是没有限制的,它通常是作为太阳这样的角色在场景中出现。
平行光没有一个唯一的位置,它可以放在场景中的任意位置。它的几何属性只有方向,可以通过调整平行光Transform组件中的Rotation属性来改变它的光源方向,而且平行光到场景中所有点的方向都是一样的。而由于平行光没有一个具体的位置,因此也没有衰减的概念,光照强度不会随着距离而发生改变。
[2]点光源:
点光源的照亮空间则是有限的,它是由空间中的一个球体定义。点光源可以由一个点发出的、由所有方向延伸的光。
点光源由位置属性,由Transform中的Position来定义。而对于方向属性,需要用点光源的位置减去某点的位置来得到它到该点的方向。而点光源的颜色和强度可以在Light组件中调整。点光源也是会衰减的,随着物体逐渐远离点光源,它收到的光照强度也会逐渐减小。点光源球心处的光照强度最强,球体边界处的最弱,值为0。中间的衰减值可以用一个函数定义。
[3]聚光灯:
聚光灯是三种光源类型中最复杂的一种。它的照亮空间同样是有限的,其不再是一个球体,而是由空间中的一块锥形区域定义。聚光灯可以用于表示由一个特定位置出发、想特定方向衍生的光。
聚光的位置同样可由Transform中的Position来定义。而对于方向属性,需要用聚光灯的位置减去某点的位置来得到它到该点的方向。聚光灯的衰减也是随着物体逐渐远离聚光灯,它收到的光照强度也会逐渐减小,在锥形的顶点处光照强度最强,在锥形边界处强度为0。其中间的衰减值可以用一个函数定义,但该函数相对于点光源衰减计算公式更加复杂,因为需要判断一个点是否在椎体的范围内。
在前向渲染处理不同的光源类型:
实践步骤:
[1]建场景并去掉天空盒子
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]拉入一个胶囊体,并把材质赋给该模型。再新建一个点光源,并设为绿色,从而和平行光区分
[4]给该shader附上对应代码
Shader "Unity Shaders Book/Chapter 9/Forward Rendering" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) //漫反射 _Specular ("Specular", Color) = (1, 1, 1, 1) //高光反射 _Gloss ("Gloss", Range(8.0, 256)) = 20 } SubShader { Tags { "RenderType"="Opaque" } //如果场景包含多个平行光,Unity将会选择最亮的平行光传递给Base Pass进行逐像素处理,其他平行光将会按照逐顶点或在Additional Pass中按逐像素进行处理 //而如果场景中没有任何平行光,那么Base Pass将会当成全黑的光源处理 //对于Base Pass来说,它处理的逐像素光源类型一定是平行光 Pass { //Base Pass // 关于漫反射和方向光的Pass Tags { "LightMode"="ForwardBase" } CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdbase //该指令可以保证Sahder中使用光照衰减等光照变量可以被正确赋值 #pragma vertex vert #pragma fragment frag #include "Lighting.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); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //通过_WorldSpaceLightPos0来获得平行光的方向 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //计算场景中的环境光,同时该部分只计算一次,从而在Additional Pass部分中将不再计算,同时以后例子中的物体的自发光也是如此 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); //使用 _LightColor0.rgb来得到平行光的颜色和强度(_LightColor0.rgb是颜色和强度相乘后的结果) fixed atten = 1.0; //平行光没有衰减,从而atten衰减值设为1.0 return fixed4(ambient + (diffuse + specular) * atten, 1.0); } ENDCG } Pass { //通常来说Addition Pass的光照处理和Base Pass的处理方式一样,因此只需要将Base Pass的顶点和片元着色器粘贴到Additional Pass中,再稍微修改一下即可 //这些修改往往是为了去掉Base Pass中环境光和自发光、逐顶点光照、SH光照的部分,再添加一些对不同光源类型的支持 //Additional Pass处理的光源类型可能是平行光、点光源或是聚光灯,从而在计算光源的5个属性时,颜色和强度仍然可以用_LightColor0.rgb得到,而位置方向衰减属性则需要根据光源类型分别计算 // Pass for other pixel lights Tags { "LightMode"="ForwardAdd" } Blend One One //开启了Blend命令与混合模式,常见的还有Blend SrcAlpha One CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdadd //保证可以在Addition Pass中访问到正确的光照变量 #pragma vertex vert #pragma fragment frag #include "Lighting.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); #ifdef USING_DIRECTIONAL_LIGHT //判断当前处理的逐像素光源的类型,通过#ifdef指令判断是否定义了USING_DIRECTIONAL_LIGHT来得到 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //如果是平行光,则可以直接通过_WorldSpaceLightPos0.xyz来获得光源方向 #else fixed3 worldLightDir = 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 //如果是平行光,衰减值为1.0 fixed atten = 1.0; #else #if defined (POINT) //其他光源类型处理更复杂,从而Unity选择使用一张纹理作为查找表(Lookup Table,LUT) 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); } ENDCG } } FallBack "Specular" }
1.2.3 Unity的光照衰减
使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减,该好处在于计算衰减不依赖于数学公式的复杂性,只需要一个参数值去纹理中采样即可。
其弊端在于:
[1]需要预处理得到采样纹理,而且纹理的大小也会影响衰减的精度
[2]不直观,同时也不方便,因此一旦把数据存储到查找表中就无法使用其他数字公式来计算衰减
但该方法可以在一定程度上提升性能,得到的效果在大部分情况下都是良好的,因此Unity默认使用这种纹理查找的方法来计算逐像素的点光源和聚光灯的衰减。
用于光照衰减的纹理:
Unity内部使用名为_LightTexture0的纹理来计算光源衰减。但如果对该光源使用了cookie,那么衰减查找纹理是_LightTextureB0。而开发者通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了在光源空间中不同位置的点的衰减值。如:(0,0)表示与光源位置重合的点的衰减值,(1,1)点表明在光源空间中所关心的距离最远的点的衰减。
为了对_LightTexture0纹理采样得到给定点到该光源的衰减值,首先在该点到光源空间中的位置,这是通过_LightMatrix0变换矩阵得到的。只需要把_LightMatrix0和世界空间中的顶点坐标相乘即可得到光源空间中的相应位置:
float3 lightCoord = mul(_LightMatrix0, float(i.worldPosition, 1)).xyz;
使用该坐标的模的平方对衰减纹理进行采样,得到衰减值:
fixed atten = tex2D(_LightTexture, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
使用数学公式计算衰减:
下面的代码可以计算光源的线性衰减:
float distance = length(_worldSpaceLightPos0.xyz - i.worldPosition.xyz); atten = 1.0 / distance; //光源的线性衰减
1.2.4 Unity的阴影
阴影的实现:
在实时渲染中,开发者最常用的是一种名为Shadow Map的技术。该技术首先把摄像机的位置放在与光源重合的位置上,而该场景中光源的阴影区域就是那些摄像机看不到的地方。Unity就是使用这种技术。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理(shadow map)。该阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
阴影映射纹理需要另外写一个pass来进行,并且需要LightMode = ShadowCaster。
在传统阴影映射纹理的实现中,开发者会在正常模式的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后使用xy分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。而如果该深度值小于该顶点的深度值(通常由z分量得到),那么说明该点位于阴影中。而在Unity 5中,Unity使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术(Screen Space Shadow Map)。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法,而并不是所有的Unity平台都是用该技术,因为其需要显卡支持MRT,而有些移动平台不支持这种特性。
一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程:
[1]如果想要一个物体接收来自其他物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
[2]如果想要一个物体向其他物体投射阴影,就必须把该物体加入光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。
不透明物体的阴影:
[1]建场景
[2]构建一个材质与Unity shader,并把该shader赋给材质,给该shader附上上面出现过的ForwardRendering的文件代码
[3]拉入一个正方体、两个平面,并把该材质赋给正方体,但不改变两个平面材质
(1) 物体投射投影
在Light中开启阴影:shadow type选择soft shadows。而在Mesh Renderer组件中设置Cast Shadows和Receive Shadows属性。开启Cast Shadows,Unity会把该物体加入到光源的阴影映射纹理的计算中。该过程是通过为该物体执行LightMode为ShadowCaster的Pass来实现。而Receive Shadows可以选择是否让物体接收来自其他物体的阴影。如果没有开启,当调用Unity的内置宏和变量计算阴影时,这些宏通过判断该物体没有开启接受阴影的功能,就不会再内部计算阴影。
而ForwardRendering文件中,虽然没有LightMode = ShadowCaster,但是在回调FallBack中,通过多次回调,从而在VertexLit.shadow中有该部分的代码,其代码如下:
Pass{ Name"ShadowCaster" Tags{ "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include"UnityCG.cginc" struct a2v{ V2F_SHADOW_CASTER; }; v2f vert(appdata_base v){ v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } float frag(v2f i) : SV_Target{ SHADOW_CASTER_FRAGMENT(i) } ENDCG }
(以上代码为了把深度信息写入渲染目标中。这个Pass的渲染目标可以是光源的阴影映射纹理或事摄像机的深度纹理。)
如果将Cast Shadows设置为Two Sided,则允许对物体的所有面进行计算阴影信息。而正方体的代码没有对阴影进行任何处理,从而不会显示别的平面投射来的阴影。
(2)让物体接收阴影
新建一个shader,起名为shadow后赋给正方体的材质,而删除shadow的代码吧ForwardRendering的代码进行修改并给它,代码如下:(该代码仍然不可用于项目中)
Pass{ Name"ShadowCaster" Tags{ "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include"UnityCG.cginc" struct a2v{ V2F_SHADOW_CASTER; }; v2f vert(appdata_base v){ v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } float frag(v2f i) : SV_Target{ SHADOW_CASTER_FRAGMENT(i) } ENDCG } */ /* Shader "Unity Shaders Book/Chapter 9/Shadow" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) _Specular ("Specular", Color) = (1, 1, 1, 1) _Gloss ("Gloss", Range(8.0, 256)) = 20 } SubShader { Tags { "RenderType"="Opaque" } Pass { // Pass for ambient light & first pixel light (directional light) Tags { "LightMode"="ForwardBase" } CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag // Need these files to get built-in macros #include "Lighting.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; SHADOW_COORDS(2) //内置宏SHADOW_COORDS,其作用是声明一个用于对阴影纹理采样的坐标。这个宏的参数需要下一个可用的插值寄存器的索引值 }; 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; // Pass shadow coordinates to pixel shader TRANSFER_SHADOW(o); //这个宏用于顶点着色器中计算上一步中生命的阴影纹理坐标 return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 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); fixed atten = 1.0; fixed shadow = SHADOW_ATTENUATION(i); //和上面两个内置宏是计算阴影时的“三剑客”,这些内置宏在必要时帮助开发者计算光源的阴影 return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0); } ENDCG } Pass { // Pass for other pixel lights Tags { "LightMode"="ForwardAdd" } Blend One One CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdadd // Use the line below to add shadows for point and spot lights // #pragma multi_compile_fwdadd_fullshadows #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Diffuse; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 position : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; }; v2f vert(a2v v) { v2f o; o.position = 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); #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); #else fixed3 worldLightDir = 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 float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz; fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #endif return fixed4((diffuse + specular) * atten, 1.0); } ENDCG } } FallBack "Specular" }
统一管理光照衰减和阴影:
UNITY_LIGHT_ATTENUATION宏可以实现同时计算光照衰减和阴影。
[1]建场景
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]拉入一个正方体,并把材质赋给该模型。
[4]给该shader附上对应代码
Shader "Unity Shaders Book/Chapter 9/Attenuation And Shadow Use Build-in Functions" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) _Specular ("Specular", Color) = (1, 1, 1, 1) _Gloss ("Gloss", Range(8.0, 256)) = 20 } SubShader { Tags { "RenderType"="Opaque" } Pass { // Pass for ambient light & first pixel light (directional light) Tags { "LightMode"="ForwardBase" } CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag // Need these files to get built-in macros #include "Lighting.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; SHADOW_COORDS(2) //声明阴影坐标 }; 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; // Pass shadow coordinates to pixel shader TRANSFER_SHADOW(o); //向片元着色器传递阴影坐标 return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); // UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); //Unity内置的用于计算光照衰减和阴影的宏,可以在AutoLight.cginc找到相关声明 //其第一个参数atten会在UNITY_LIGHT_ATTENUATION(译:衰减)中帮助声明,第二个参数结构体v2f会传递给SHADOW_ATTENUATION,第三个参数是世界坐标,用于计算光源空间下的坐标,再对光照衰减纹理采样来得到光照衰减 return fixed4(ambient + (diffuse + specular) * atten, 1.0); } ENDCG } Pass { // Pass for other pixel lights Tags { "LightMode"="ForwardAdd" } Blend One One CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdadd // Use the line below to add shadows for point and spot lights // #pragma multi_compile_fwdadd_fullshadows #pragma vertex vert #pragma fragment frag #include "Lighting.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; SHADOW_COORDS(2) }; 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; // Pass shadow coordinates to pixel shader TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); // UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4((diffuse + specular) * atten, 1.0); } ENDCG } } FallBack "Specular" }
透明度物体的阴影:
对于大多数不透明物体来说,把Fallback设置为VertexLit即可得到正确的阴影。而对于透明物体的实现通常会使用透明度测试或透明度混合,从而需要小心设置这些物体的Fallback。
[1]透明度测试+阴影
Shader "Unity Shaders Book/Chapter 9/Alpha Test With Shadow" { Properties { _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Main Tex", 2D) = "white" {} _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 } SubShader { Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"} Pass { Tags { "LightMode"="ForwardBase" } Cull Off CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; SHADOW_COORDS(3) }; 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; o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // Pass shadow coordinates to pixel shader TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); clip (texColor.a - _Cutoff); fixed3 albedo = texColor.rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); // UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4(ambient + diffuse * atten, 1.0); } ENDCG } } FallBack "Transparent/Cutout/VertexLit" }
[2]透明度混合+阴影
Shader "Unity Shaders Book/Chapter 9/Alpha Blend With Shadow" { Properties { _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Main Tex", 2D) = "white" {} _AlphaScale ("Alpha Scale", Range(0, 1)) = 1 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _AlphaScale; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; SHADOW_COORDS(3) }; 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; o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // Pass shadow coordinates to pixel shader TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); fixed3 albedo = texColor.rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); // UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4(ambient + diffuse * atten, texColor.a * _AlphaScale); } ENDCG } } FallBack "Transparent/VertexLit" // Or force to apply shadow // FallBack "VertexLit" }