Unity透明效果原理与实现

    技术2022-07-13  90

    一、前提知识

    (1)深度缓存

    它的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较(如果开启了深度测试),如果它的值距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)。

    (2)颜色缓存

    需要渲染的场景的每一个像素都最终写入该缓冲区,然后由他渲染到屏幕上显示。

    (3)渲染队列

    Unity为了解决渲染顺序的问题提供了渲染顺序这一解决方案。我们可以使用SubShader的Queue标签来决定我们的模型属于哪一个渲染队列。Unity内部使用一系列的整数索引来表示每一个渲染队列。且索引号越小表示越早被渲染。

     

    unity内置的渲染队列.png

    二、透明度测试

    (1)概念

    它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等

    (2)效果

    透明度测试.png

    (3)实现

    Shader "Unity Shaders Book/Chapter 8/Alpha Test" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color ("Main Tint",Color) = (1,1,1,1) //透明度参考值 _Cutoff ("Alpha Cutoff",Range(0,1)) = 0.5 } SubShader { // Queue 设置渲染队列为透明度测试队列 IgnoreProjector 表示是否受投影器(Projectors)的影响 TransparentCutout提前定义的组 Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" } Pass { Tags {"LightMode"="ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float2 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; }; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord,_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); //透明度测试 clip(texColor.a - _Cutoff); //函数实现 //if (texColor.a - _Cutoff < 0.0) //{ // 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" }

    三、透明度混合

    (1)概念

    这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。注意事项:渲染顺序,关闭深度写入

    (2)为什么要关闭深度写入?

    如果不关闭深度写入,一个半透明表面背后的表面本来可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面会被剔除,我们也就无法透过半透明表面看到后面的物体了。

    (2)为什么渲染顺序很重要?

    A为透明物体B为不透明物体.png   AB都为透明物体.png  

    第一种情况:先渲染B,再渲染A。那么由于不透明物体开启了深度测试和深度检验,而此时深度缓冲中没有任何有效数据,因此B首先会写入颜色缓冲和深度缓冲。随后我们渲染A,透明物体仍然会进行深度测试,因此我们发现和B相比A距离摄像机更近,因此,我们会使用A的透明度来和颜色缓冲中的B的颜色进行混合,得到正确的半透明效果。

    第二种情况:先渲染A,再渲染B。渲染A时,深度缓冲区中没有任何有效数据,因此A直接写入颜色缓冲,但由于对半透明物体关闭了深度写入,因此A不会修改深度缓冲。等到渲染B时,B会进行深度测试,它发现“咦,深度缓冲中还没有人来过,那我就放心地写入颜色缓冲了!”,结果就是B会直接覆盖A的颜色。从视觉上来看,B就出现了A的前面,这是错误的。

    (3)渲染顺序结论

    (1)先渲染所有不透明的物体,并开启它们的深度测试和深度写入。 (2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。 (3)以上两种是unity渲染顺序的基本常识,为了解决更多更复杂的渲染顺序问题,我们应该使用unity为我们提供的渲染队列(一、前提知识-(3)渲染队列)。

    (4)透明度混合命令,混合因子,混合操作介绍

    (1)透明度混合命令( Blend ) 在设置混合因子的同时也开启了混合模式。这是因为,只有开启了混合之后,设置片元的透明通道才有意义。

      透明度混合命令.png

    (2)透明度混合因子

      透明度混合因子.png

    (3)透明度混合操作

      透明度混合操作.png

    (4)透明度混合示例 例如: 正常(Normal),即透明度混合 Blend SrcAlpha OneMinusSrcAlpha NewColor = SrcColor ×SrcAlpha + DstColor * (1-SrcAlpha)

      正常(Normal).png

    变亮(Lighten) BlendOp Max Blend One One NewColor =Color( max(SrcColor.r,DstColor.r),max(SrcColor.g,DstColor.g),max(SrcColor.b,DstColor.b),max(SrcColor.a,DstColor.a))

    变亮(Lighten) .png

     

    (5)透明度混合效果

    透明度混合效果.png

    (6)透明度混合实现

    Shader "Unity Shaders Book/Chapter 8/Alpha Blend" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color ("Main Tint",Color) = (1,1,1,1) //透明度参考值 _AlphaScale ("Alpha Cutoff",Range(0,1)) = 1 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } Pass { Tags {"LightMode"="ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _AlphaScale; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float2 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; }; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord,_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,texColor.a * _AlphaScale); } ENDCG } } Fallback "Transparent/Cutout/VertexLit" }

    (7)开启深度写入的透明度混合效果

     

    在一些情况下,由于我们关闭了深度写入,会给我们带来各种复杂的问题。例如:当模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样的因为排序错误而产生的错误的透明效果。如下图:

    错误的透明效果.png  

    这是因为我们模型无法进行像素级别的深度排序。于是我们可以使用两个pass来渲染这个模型:第1个pass开启深度写入,但是不向颜色缓存中输入任何颜色,目的就是为了通过深度缓存的规则,把该模型的深度值写入深度缓存中,这样就可以将模型自身被遮挡住的片元剔除掉。第2个pass就进行正常的透明度混合就行了。

    代码实现:

    Shader "Unity Shaders Book/Chapter 8/Alpha Blending With ZWrite" { 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的作用仅仅是开启深度写入 Pass { ZWrite On //然后我们使用了一个新的渲染命令——ColorMask。在ShaderLab 中,ColorMask用于设置颜色通道的写掩码,为0时表示不写入任何颜色 ColorMask 0 } //普通的透明度混合实现 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; float2 uv : TEXCOORD2; }; //顶点着色器,主要是坐标转换以及UV值 v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _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, texColor.a * _AlphaScale); } ENDCG } } FallBack "Transparent/VertexLit" }

    (8)双面渲染的透明度混合效果

    在正常情况下,我们观察一个透明物体的时候,不仅仅可以看到透明物体的正面,也可以透过他的正面看到物体的背面,在上面的实例中不管是透明度测试还是透明度混合,我们都只能看到物体的一面,看起来物体就像只有半个。这是因为在unity中默认是剔除物体背面的渲染图元的,只渲染物体的正面图元。所以想要得到双面渲染的效果,需要我们在shader中手动的设置哪一面需要被渲染。 指令:Cull Cull Back :背对着摄像机的渲染图元不会被渲染 Cull Front :面朝着摄像机的渲染图元不会被渲染 Cull Off : 关闭剔除功能,所有的图元都将被渲染

    透明度测试的双面渲染代码:

    Shader "Unity Shaders Book/Chapter 8/Alpha Test With Both Side" { 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 vertex vert #pragma fragment frag #include "Lighting.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; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _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); //透明度测试 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)); return fixed4(ambient + diffuse, 1.0); } ENDCG } } FallBack "Transparent/Cutout/VertexLit" }

    透明度混合的双面渲染 对于透明度混合也是直接关闭剔除功能吗?显然是不行的,因为在透明度混合时需要关闭深度写入,所以直接关闭剔除功能,我们不能保证同一个物体正面与背面的图元的渲染顺序,有可能得到错误的半透明效果。为了确保同一个物体的正面与背面的图元以正确的渲染顺序渲染,我们选择把双面渲染的工作分成两个pass,第一个只渲染背面,第二个只渲染正面。因为在SubShader中,pass是顺序执行的。这样可以确保背面在正面之前被渲染。

    代码实现:

    Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" { 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; float2 uv : TEXCOORD2; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _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, texColor.a * _AlphaScale); } ENDCG } 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; float2 uv : TEXCOORD2; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _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, texColor.a * _AlphaScale); } ENDCG } } FallBack "Transparent/VertexLit" }
    Processed: 0.013, SQL: 9