首先在进行代码编写前,我们要了解渲染的一个机制,就是Tas序列,给Subshader下添加标签可以定义整个Subshader的队列,给Pass添加标签可以定义该Pass的光照模式比如forwardbase,forwardadd类似。 还要我们需要加深一个概念是渲染顺序。在之前的案例由于是单个光照模型不用考虑物体的遮罩情况,所以无所谓渲染顺序。而对于不透明的物体(Rendertype=opaque)不需要考虑渲染顺序也能在屏幕上得到正确的渲染,这是因为摄像机自己做了的个深度检测,在深度缓冲(z-buffer)中会决定那个物体会被渲染在前面,哪些物体会被遮挡。这时候我们不用去关心渲染顺序,因为摄像机会自动判断。
当我们在进行写透明材质的时候有两种解决方案,一种是透明度测试,一种是透明度混合。
透明度测试:一种极端的检测机制,只要有片元不满足这个测试就会被舍弃,满足就保留然后进行深度测试与深度写入。也就是说,其不用关闭ZWrite。
我们先进行透明度测试的shader书写,在此之前,介绍个clip函数
<pre class="wp-block-syntaxhighlighter-code">void clip(float4 x)
{
if (any(x <0))
discard;
}</pre>
意思就是如果给定一个参数的任何一个分量为负数,就会舍弃当前的像素的输出。那么关键函数就很简单了,拿被检测的a分量减去一个自定义值,只有大于0的才能保存。
<pre class="wp-block-syntaxhighlighter-code">Shader "Custom/AlphaTest"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaValue ("AlphaValue", Range(0,1)) = 0.5
}
SubShader
{
Tags{"Queue"="AlphaTest" "IgnoreProjector"="True" "Rendertype"="TransparentCutout" }
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _AlphaValue;
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-_AlphaValue);
fixed3 albedo=texColor.rgb*_Color;
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 "Tranaparent/Cutout/VertexLit"
}
</pre>
第二种方法要稍微圆滑一点,称为透明度混合,这种方法可以真正的得到透明效果。他会先与存储在颜色缓冲中的颜色进行混合,得到新的颜色。但因为,我们关闭了深度写入,所以我们要注意渲染顺序。
为了进行混合,我们需要了解Blend命令,该命令一般在Pass内
Blend Off | 关闭混合(默认) |
Blend SrcFactor DstFactor | 片元产生的颜色乘以SrcFactor,加上屏幕上已有的颜色乘以DstFactor,得到最终的颜色(写入颜色缓存) |
Blend SrcFactor DstFactor,SrcFactorA DstFactorA | 同上,只不过使用单独的银子SrcFactorA与DstFactorA来混合透明度通道 |
BlendOp BlendOperation | 用其他的操作来取代加法混合 |
BlendOp OpColor,OpAlpha | 同上,只不过对于透明度通道的不同操作 |
混合操作(BlendOp)
Add | 加分 FinalColor=SrcFactor*SrcColor+DstFactor*DstColor |
Sub | 减法(源-目标): FinalColor=SrcFactor*SrcColor-DstFactor*DstColor |
RevSub | 减法(目标-源): FinalColor=DstFactor*DstColor-SrcFactor*SrcColor |
Min | 较小值(逐个通道比较) |
Max | 较大值(逐个通道比较) |
混合因子
One | 混合因子1,表示完全的源颜色或目标颜色 |
Zero | 混合因子0,舍弃掉源颜色或目标颜色 |
SrcColor | 源颜色值 |
SrcAlpha | 源透明度 |
DstColor | 目标颜色 |
DstAlpha | 目标透明度 |
OneMinusSrcColor | 1-SrcColor |
OneMinusSrcAlpha | 1-SrcAlpha |
OneMinusDstColor | 1-DstColor |
OneMinusDstAlpha | 1-DstAlpha |
常用的混合命令
<pre class="wp-block-syntaxhighlighter-code">Blend SrcAlpha OneMinusSrcAlpha // 传统透明度
Blend One OneMinusSrcAlpha // 预乘透明度
Blend One One // 叠加
Blend OneMinusDstColor One // 柔和叠加
Blend DstColor Zero // 相乘——正片叠底
Blend DstColor SrcColor // 两倍相乘</pre>
我们在这里运用Blend SrcAlpha OneMinusSrcAlpha,然后再关闭深度写入,其他的就跟我们之前写的贴图材质一样,再复习一遍贴图材质的反射公式:Diffuse=Clight*Mdiffuse*max(0,dot(n,l))所以我们先搭建贴图的shader代码
当然我们想通过一个参数来调节透明度,可以直接在贴图的alpha通道乘以一个参数就行了,然后再使参数暴露。
fixed4 texColor=tex2D(_MainTex,i.uv);
texColor.a*_AlphaScale;</pre>
以下是完整代码
Shader "Custom/AlphaBlendShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Main Texture", 2D) = "white" {}
_AlphaScale ("AlphaScale", Range(0,1)) = 0.5
}
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;
float _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;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*ambient*max(0,dot(worldNormal,worldLightDir));
return fixed4(diffuse+ambient,texColor.a*_AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
代码几乎与之前的漫反射贴图的一样,只是添加了 关闭深度写入 与 混合模式,并定义了渲染队列。
虽然我们完成了这部分的透明度,但信息的你们一定会发觉这部分透明是有问题的,因为我们看不到物体的内部,只是将整体进行透明化的处理。这时候我们就需要思考为什么会发生这种情况,只能看到整个物体的正面,透明物体在现实中应该不是如此。这是因为我们在AlphaBlend中关闭了深度写入,所以摄像机会自动剔除物体的背面,而在unity中如何判断一个面片的正反呢,其实他是根据法线来判断的,举个例子,当三个点进行连线的时候,顺时针为正,反之则为负。
所以在背面的面片由于被摄像机判断为反,则被剔除了。不信的话可以随便新建一个Plane然后看看他的法线方向,当他为背面的时候会摄像机则不会渲染该面片。这也是为何在AlphaTest中为何开启了深度写入,但背部面片仍然不被渲染的原因。所以在AlphaTest中如何渲染背部就非常简单,只需在Pass下CGPROGRAM前添加Cull Off命令,就可以关闭剔除功能,从而达到渲染背面的效果。
但当我们来到AlphaBlend的透明渲染的时候,我们该如何进行背部渲染呢,因为为了达到Blend的效果,我们需要关闭深度写入,但关闭深度写入摄像机则无法检测到背部信息,所以会有第一次AlphaBlend的渲染效果。在这里我们有一个小trick可以做,正如我们在Toon-Shading中所做的一样,我们先进行一次背部渲染,再进行正面渲染就可以达到我们的效果了,但值得注意的是,由于我们关闭了深度检测所以渲染顺序就很重要。以上的渲染思路的伪代码如下
...
Pass{
...
Cull Front
...
}
Pass{
...
Cull Back
...
}
所以我们完整的AlphaBlend渲染代码如下
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Custom/AlphaBlendShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Main Texture", 2D) = "white" {}
_AlphaScale ("AlphaScale", Range(0,1)) = 0.5
}
SubShader
{
Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass{
Tags{"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _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;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*ambient*max(0,dot(worldNormal,worldLightDir));
return fixed4(diffuse+ambient,texColor.a*_AlphaScale);
}
ENDCG
}
Pass{
Tags{"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _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;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*ambient*max(0,dot(worldNormal,worldLightDir));
return fixed4(diffuse+ambient,texColor.a*_AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
以上我们的透明块的渲染告一段落。
接着我们进行阴影的绘制,在绘制阴影的时候我们需要再次回顾一下我们之前提到的三个阴影宏,SHADOW_COORDS、TRANSFER_SHADOW、SHADOW_ATTENUATION。首先SHADOW_COORDS只是一个声明,因为顶点数据中并没有阴影的坐标需要通过顶点着色器进行计算,所以SHADOW_COORDS是在v2f结构体中进行声明并在在顶点着色器中进行计算所以,TRANSFER_SHADOW在顶点着色器中计算上一步的声明的阴影纹理坐标。所以,在写这部分代码的时候要注意带有SHADOW_COORDS的结构体应该在顶点着色器的前部,而不能像之前那样,将V2F结构体放在顶点着色器之后。SHADOW_ATTENUATION则是计算阴影的衰减,所以这部分基本上是在片元着色器之中。而这三个宏是包含在AutoLight.cginc之中,所以在计算阴影的时候需要包含这个头文件。但当SHADOW_COORDS与TRANSFER_SHADOW实际上没有任何作用的时候,SHADOW_ATTENUATION的值会直接等于1。
更需要的注意的是,由于这些宏中会使用上下文变量来进行计算,比如SHADOW_COORDS会用v.vertex或者a.pos来计算,所以为了能够这些宏正常运行,我们需要保证自定义的变量名。所以a2v结构体中的顶点坐标变量名必须是vertex,顶点这所去的输入a2v必须命名为v,v2f中顶点位置必须命名为pos。
在AlphaTest中我可以直接添加相关代码
struct v2f{
...
SHADOW_COORDS(3)
...
};
这是v2f的坐标声名,这里注意下,由于我们在这用的是第四套纹理坐标,所以内部写3,还有就是这个宏不需要添加”;”
v2f vert(a2v v){
...
TRANSFER_SHADOW(o);
...
return o;
}
此处我们将上一步声明的坐标点转换成阴影坐标作为输出。这个宏要加”;”
fixed4 frag(v2f i):SV_Target{
...
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
...
return fixed4 (ambient,diffuse*atten,1.0)
}
在此处我们用了UNITY_LIGHT_ATTENUATION这个宏,输入三个参数,atten我们代码中并没有声明类型,这个宏会自动帮我们声明,参数i是我们整个片元着色器的输入参数,还有与其相关的世界坐标。这个宏的计算过程可以在AutoLight.cginc看到,它会将第二个参数传递给SHADOW_ATTENUATION,来计算阴影值。
接下来我们在我们的混合透明中每一个pass里加入上述代码,就能得到混合透明的阴影。