UnityShader透明效果
Posted 夜槿笙歌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UnityShader透明效果相关的知识,希望对你有一定的参考价值。
一、如何实现透明效果
在Unity中实现透明效果的方式有两种,其一是透明度测试,其二是透明度混合。
- 透明度测试:这种方式不需要关闭深度写入,且实现机制非常简单粗暴。只要一个片元的透明度不满足条件(比如小于某个值),则该片元会被直接舍弃,否则就按照不透明物体的处理方式来处理。它产生的效果要么是完全不透明,要么是完全透明,并不是真正的半透明效果。
- 透明度混合:这种方式会使用当前片元的透明度作为混合因子,与颜色缓冲中的颜色进行混合。这就需要关闭深度写入。而关闭深度写入意味着我们需要非常小心物体的渲染顺序,否则可能出现渲染问题。
为什么要关注渲染顺序
在之前的Shader中我们并没有关心过物体渲染顺序的问题。这是因为深度缓冲的存在。当渲染一个片元时,需要把它的深度值与已经存在于深度缓冲中的值进行比较,如果它的深度值更大,说明有物体挡住了它,这个片元就不应该被渲染到屏幕上;反之,这个片元就会覆盖掉颜色缓冲中的像素值,它的深度值也会更新到深度缓冲中。也就是说,有深度缓冲在,即便我们先渲染了前面的物体A,再渲染了后面的物体B,也不用担心B会覆盖掉A。因为已经在深度缓冲中判断出B在A的后面。
那么进行透明度混合时为什么要关闭深度写入呢?对于一个半透明物体来讲,其后面的物体应该是可以透过它看到的。但如果开启了深度写入,距离摄像机更近的透明物体的深度就会覆盖掉后面的物体。从而后面物体的表面将会被剔除,我们也就无法通过透明物体观察到后面的物体了。
一旦关闭了深度写入,事情就会变得复杂起来。
比如上面这两个物体,A是半透明物体,B是不透明物体。
- 如果我们先渲染B,再渲染A。B首先将颜色和深度写入缓冲区。然后渲染A时进行深度测试,发现A离相机更近。因此A的透明度会和缓冲区中的B的颜色进行混合,得到正确的半透明效果。
- 如果我们先渲染A,再渲染B。A会将颜色写入颜色缓冲,但由于透明物体关闭了深度写入,因此A不会将深度值写入深度缓冲。等到渲染B时,由于深度缓冲中没有数据,所以B会直接将颜色和深度写入缓冲中。结果就是B会覆盖A的颜色,结果是错误的。
那么如果是两个半透明物体又会怎样呢?下图中A和B都是半透明物体。
- 先渲染B再渲染A。B会正常写入颜色缓冲,然后A会和颜色缓冲中的B的颜色进行混合,得到正确结果。
- 先渲染A再渲染B。A会先写入颜色缓冲,然后B会与颜色缓冲中A的颜色进行混合。这样混合结果会反过来,看起来像B在A的前面。结果错误。
渲染引擎在渲染前一般会先对物体进行排序。先渲染所有不透明物体,并开启它们的深度测试和深度写入。然后再把半透明物体按从后往前的顺序进行渲染,并开启深度测试,关闭深度写入。尽管如此,仍然会有些不理想的情况:
像上面几种无法得到正确排序顺序的情况,就只能将物体进行拆分或是让透明通道更加柔和,让穿插不是这么明显。
Unity的渲染队列
Unity为了解决渲染顺序的问题,提供了渲染队列这一解决方案。我们可以在SubShader的Tags中决定模型使用哪个渲染队列。
二、透明度测试
接下来我们实现透明度测试效果。首先将之前的纹理采样的Shader复制过来。这里不考虑高光,所以将计算高光的部分删掉
Shader "Unlit/AlphaTest"
Properties
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Gloss("Gloss",Range(1,256)) = 20
_MainTex("MainTex",2D) = "white"
SubShader
Tags
"LightMode"="ForwardBase"
LOD 100
Pass
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
float _Gloss;
sampler2D _MainTex;
float4 _MainTex_ST;
struct v2f
float4 vertex: SV_POSITION;
fixed3 worldNormal: TEXCOORD0;
float2 uv:TEXCOORD1;
;
v2f vert(appdata_base v)
v2f o;
// 将顶点坐标从物体空间转换到裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 将法线从物体空间转换到世界空间
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// 计算uv坐标
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
fixed4 frag(v2f i):SV_Target
// 获取归一化的光源方向
const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
const fixed3 albedo = tex2D(_MainTex,i.uv).rgb;
// 环境光
const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 计算漫反射
const fixed3 diffuse = _LightColor0.rgb * albedo * _Diffuse.rgb * (0.5*dot(i.worldNormal, worldLight)+0.5);
fixed3 color = ambient + diffuse;
return fixed4(color, 1);
ENDCG
首先定义一个属性作为透明度测试时的判断条件,并在CG中定义与之匹配的变量
_Cutoff("Alpha Cutoff",Range(0,1)) = 0.5
在Tags中将渲染队列设置为「AlphaTest」,并忽略投影器的影响
Tags
"Queue"="AlphaTest"
"IgnoreProjector"="True"
最后在片元着色器中根据纹理的alpha通道的值进行剔除
fixed4 frag(v2f i):SV_Target
// 获取归一化的光源方向
const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
const fixed4 texColor = tex2D(_MainTex,i.uv);
// 根据条件剔除
if(texColor.a - _Cutoff < 0)
discard;
// 环境光
const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 计算漫反射
const fixed3 diffuse = _LightColor0.rgb * texColor.rgb * _Diffuse.rgb * (0.5*dot(i.worldNormal, worldLight)+0.5);
fixed3 color = ambient + diffuse;
return fixed4(color, 1);
找一张alpha值不同的图片作为纹理,得到如下效果
三、透明度混合
Unity提供了设置混合模式的命令——Blend。混合时使用的函数就由该指令决定。Unity提供的Blend指令如下
对于「Blend SrcFactor DstFactor」和「Blend SrcFactor DstFactor,SrcFactorA DstFactorA」两个命令,它们实际上表达的是两个混合公式
O
r
g
b
=
S
r
c
F
a
c
t
o
r
×
S
r
g
b
+
D
s
t
F
a
c
t
o
r
×
D
r
g
b
O_rgb=SrcFactor×S_rgb+DstFactor×D_rgb
Orgb=SrcFactor×Srgb+DstFactor×Drgb
O
a
=
S
r
c
F
a
c
t
o
r
A
×
S
a
+
D
s
t
F
a
c
t
o
r
A
×
D
a
O_a=SrcFactorA×S_a+DstFactorA×D_a
Oa=SrcFactorA×Sa+DstFactorA×Da
其中
O
r
g
b
、
O
a
O_rgb、O_a
Orgb、Oa代表混合后的rgb通道和a通道,
S
r
g
b
、
S
a
S_rgb、S_a
Srgb、Sa代表源颜色通道,
D
r
g
b
、
D
a
D_rgb、D_a
Drgb、Da代表目标颜色通道。第一个命令只提供了两个因子,意味着rgb通道和a通道的混合因子都是SrcFactor和DstFactor。
那么这些混合因子可以有哪些值呢?
另外,前面的公式将源颜色和目标颜色与混合因子的乘积相加得出最终结果。我们当然也可以自定义其他的混合操作。「BlendOP BlendOperation」的作用就是如此。Unity提供的混合操作如下
下面列出了常见的混合类型,类似于PS中对应的混合效果
// 正常(Normal),即透明度混合
Blend SrcAlpha OneMinusSrcAlpha
// 柔和相加(Soft Additive)
Blend OneMinusDstColor One
// 正片叠底(Multiply),即相乘
Blend DstColor 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
透明度混合实现
接下来我们尝试实现透明度混合。依然是基于之前的纹理采样的Shader进行修改。
首先定义一个属性用来控制整体的透明度,并在CG中声明对应变量
_AlphaScale("Alpha Scale",Range(0,1)) = 1
在Tags中指定渲染队列,忽略投影器影响,将「RenderType」设置为「Transparent」
Tags
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
在Pass通道中,将光照模式设置为前项渲染路径,关闭深度写入,并设置混合模式
Tags "LightMode"="ForwardBase"
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
最后设置片元着色器返回值中的透明通道
fixed4 frag(v2f i):SV_Target
const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
const fixed4 texColor = tex2D(_MainTex,i.uv);
const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
const fixed3 diffuse = _LightColor0.rgb * texColor.rgb * _Diffuse.rgb * (0.5*dot(i.worldNormal, worldLight)+0.5);
fixed3 color = ambient + diffuse;
// 修改返回值中的透明通道
return fixed4(color, texColor.a * _AlphaScale);
效果如下
但假如我们换一个模型,就会发现这样做有些许问题
很明显,当模型网格相互交叉时,会得到错误的半透明效果。这是因为我们关闭了深度写入,无法对模型进行像素级别的深度排序。解决方案是增加一个Pass通道,在新的Pass通道中开启深度写入,但不输出颜色。这样就会先把模型的深度值写入缓冲中。当第二个Pass再进行透明度混合时,由于上一个Pass已经获得了逐像素的正确深度信息,因而该Pass就可以按照深度排序结果进行透明渲染。当然,这样做的缺点也很明显:多使用了一个Pass会对性能造成影响。
// 新增一个Pass
Pass
ZWrite On
// 不写入任何颜色通道
ColorMask 0
效果如下
UnityShader之遮挡透明
好久没写博客了,最近在学shader,不得不说,shader真的非常美妙,我沉迷其中无法自拔= =
之前做过一个遮挡透明的功能,当物体遮挡住主角时,该物体会变成半透明显示出主角。这次同样是遮挡透明的功能,不过,变透明的刚刚相反,是主角变成半透明,更严谨的说是主角被遮挡的那一部分变成半透明。
先放出结果图:
当被遮挡时,遮挡部分透明处理,那么需要涉及渲染深度的知识。引擎是如何判断哪个物体在前面哪个物体在后面呢?
深度:每个像素有自己的深度值,离摄像机近的深度小,远的深度大
深度缓冲区:存储每个像素的深度
颜色缓冲区:存储每个像素的颜色
过程:首先比较像素的深度与深度缓冲区同一位置的深度,如果前者小于后者,则未通过深度测试;否则,通过深度测试,将前者写入后者,将该像素的颜色写入到颜色缓冲区。将颜色缓冲区像素颜色显示到屏幕上。
通过这个过程即可把深度小的像素剔除掉,将深度大的显示到屏幕上,从而实现物体的前后顺序。
UnityShader提供了ZWrite 和 ZTest对应深度写入和深度测试。
调整ZWrite可以控制是否将深度写入到深度缓冲区,当然,前提是深度测试通过,如果没通过测试,那么肯定是无法写入的
调整ZTest可以定义上述中前者与后者的比较关系,默认为LEqual即小于等于时通过测试
那么可以得到一种实现思路,用两个PASS:
第一个PASS:ZTest 为 Greater,ZWrite 为 Off,当该像素被遮挡即深度大于深度缓冲区对应位置深度时执行该PASS,那么就可以在该PASS中实现被遮挡像素的效果。
第二个PASS:ZTest为LEqual,ZWrite 为 On,这个PASS与上述PASS是互斥的,在这个PASS中实现未被遮挡像素的效果。
设置ZWrite 是为了防止两个PASS都执行,如果第一个PASS的ZWrite为On,某一像素未被遮挡时,执行第一个PASS,将像素深度写入深度缓冲区,然后轮到第二个PASS进行深度测试时也会通过,因为小于等于嘛。
被遮挡像素透明实现用了边缘光使得更炫酷。边缘光公式大概如下:
fixed rim=1-saturate(dot(worldNormalDir,worldViewDir));
fixed3 finalCol=_RimColor.xyz*pow(rim,_RimPower)*_RimIntensity
通过第一个式子可以得到一个参数rim,顶点法线方向与视角方向契合度越高则rim越小,否则rim越大,即越靠近边缘rim越大
第二个式子中pow是为了提高边缘光硬度
代码:
1 // Upgrade NOTE: replaced ‘_Object2World‘ with ‘unity_ObjectToWorld‘ 2 3 Shader "MyShader/Rim/RimShader" { 4 Properties{ 5 _RimColor("Rim Color",Color)=(1.0,1.0,1.0,1.0)//边缘光颜色 6 _RimPower("Rim Power",Range(0.1,10))=3.0//Pow参数 7 _RimIntensity("Rim Intensity",Range(0,100))=10//边缘光强度 8 9 _MainTex("Base (RGB)",2D)="white"{} 10 } 11 SubShader{ 12 //当所有不透明物体渲染后开始渲染此物体 13 Tags{"Queue"="Geometry+50" "RenderType"="Opaque"} 14 15 Pass{ 16 Blend SrcAlpha OneMinusSrcAlpha 17 Cull Off 18 ZWrite Off 19 ZTest Greater 20 21 CGPROGRAM 22 #pragma vertex vert 23 #pragma fragment frag 24 #include "UnityCG.cginc" 25 26 fixed4 _RimColor; 27 float _RimPower; 28 float _RimIntensity; 29 30 struct a2v{ 31 float4 vertex:POSITION; 32 float3 normal:NORMAL; 33 }; 34 35 struct v2f{ 36 float4 pos:SV_POSITION; 37 float4 worldPos:TEXCOORD0; 38 float3 worldNormal:TEXCOORD1; 39 }; 40 41 v2f vert(a2v v){ 42 v2f o; 43 o.pos=mul(UNITY_MATRIX_MVP,v.vertex); 44 o.worldPos=mul(unity_ObjectToWorld,v.vertex); 45 o.worldNormal=UnityObjectToWorldNormal(v.normal); 46 return o; 47 } 48 49 fixed4 frag(v2f i):SV_TARGET{ 50 fixed3 worldNormalDir=normalize(i.worldNormal); 51 fixed3 worldViewDir=normalize(UnityWorldSpaceViewDir(i.worldPos)); 52 fixed rim=1-saturate(dot(worldNormalDir,worldViewDir)); 53 54 fixed3 col=_RimColor.xyz*pow(rim,_RimPower)*_RimIntensity; 55 return fixed4(col,0.3); 56 } 57 ENDCG 58 } 59 60 Pass{ 61 Tags{"LightMode"="ForwardBase"} 62 ZWrite On 63 ZTest LEqual 64 CGPROGRAM 65 #pragma vertex vert 66 #pragma fragment frag 67 #include "UnityCG.cginc" 68 #include "Lighting.cginc" 69 #include "AutoLight.cginc" 70 71 sampler2D _MainTex; 72 float4 _MainTex_ST; 73 74 75 struct a2v{ 76 float4 vertex:POSITION; 77 float2 texcoord:TEXCOORD0; 78 }; 79 80 struct v2f{ 81 float4 pos:SV_POSITION; 82 float2 uv:TEXCOORD0; 83 }; 84 85 v2f vert(a2v v){ 86 v2f o; 87 o.pos=mul(UNITY_MATRIX_MVP,v.vertex); 88 o.uv=v.texcoord*_MainTex_ST.xy+_MainTex_ST.zw; 89 return o; 90 } 91 92 fixed4 frag(v2f i):SV_TARGET{ 93 fixed3 col=tex2D(_MainTex,i.uv).rgb; 94 95 return fixed4(col,1); 96 } 97 98 ENDCG 99 } 100 101 102 } 103 FallBack "Diffuse" 104 }
以上是关于UnityShader透明效果的主要内容,如果未能解决你的问题,请参考以下文章
Unity Shader ------ 纹理之法线纹理单张纹理及遮罩纹理的实现
Unity Shader ------ 高级纹理之渲染纹理及镜子与玻璃效果的实现