前言
这个并不是技术分享的教程类博客,只是我记录的笔记而已。不,不要误会了!(虽然溜一遍下来发现好像也能看
透明效果概念和其它
概念与渲染过程
当一个物体被渲染到屏幕上时,每个片元除了颜色和深度外还有透明度这个属性。透明度为1表示完全不透明,为0表示完全不显示。
深度缓冲:根据深度缓存中的值来判断一个片元距离摄像机的距离。当渲染另一片元时,会将它的深度值和意见存在于深度缓冲的值比较(深度测试),如果他的值距离摄像机更远,就不会被渲染(因为前面有物体挡住了它);如果他的值距离摄像机更近,就会覆盖掉此时颜色缓冲中的像素值,并把深度值更新到深度缓冲中(深度写入)。
有两种方法实现透明效果,分别是透明度测试和透明度混合。
透明度测试
采用极端的机制,只要一个片元透明度不满足条件(小于一个阈值)就舍弃不渲染,如果满足就按照普通的不透明物体处理(进行深度测试和写入)。虽然简单但是效果极端(不是完全透明就是完全不透明),做不到半透明的效果。需要打开深度写入。
透明度混合
使用当前片元的透明度作为混合因子,与已经储存在颜色缓冲中的颜色缓冲值混合。但是执行该操作需要关闭深度写入,未关闭深度测试(如果不关闭,一个半透明表面背后表面本来可以被我们看到,但是由于深度测试判断结果时半透明表面更近,这样背后表面会被剔除)。对于透明度混合,深度缓冲是可读的。
渲染顺序
渲染顺序对于透明效果十分重要,特别是关闭了深度写入时。当先渲染近处的片元再渲染远处时,因为深度缓冲区没有任何有效数据,因此直接写入颜色缓冲,但是不会修改深度缓冲(未开启深度写入)。这样当渲染远处的时候进行深度测试,由于深度缓存啥也没有,所以会直接覆盖近处的颜色,从而导致错误。所以无论是半透明还是透明物体之间都需要符合一定渲染顺序。
目前的常用渲染方法为:
1.先渲染所有不透明物体并开启它们的深度测试和深度写入;
2.把半透明物体按距离排序,从后往前渲染透明物体,并开启深度测试,关闭深度写入。
但是问题依然没有解决,当几个物体交错放时,A物体一半在B物体前,一半在后面,就很难使用”从后往前“这个宽泛的说法。
针对上面的问题,Unity是这样解决的:
Unity解决方案
Unity提供了渲染队列这一说法,采用SubShader中的Queue标签决定模型归于哪个渲染队列,队列索引号越小越早被渲染。
Background:索引号1000,用来渲染那些需要绘制再背景上的物体
Geometry:索引号2000,默认渲染队列,不透明物体使用该序列
AlphaTest:索引号2450,需要透明度测试的物体选用该队列
Transparent:索引号3000,使用透明度混合的物体选用该队列
Overlay:索引号4000,用于实现一些叠加效果,任何需要在最后渲染的物体使用该队列
标签具体实现方法:
SubShader{
Tags{"Queue"="里面写标签"}
Pass{
......
}
}
透明度测试实践
Shader"Custom/Alpha Test"{
Properties{
_Color("Main 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"}
//"TransparentCutout"标签指这是个使用了透明度的Shader
//True表明Shader不会受到投影器(Projector)的影响
Pass{
Tags{"LightMode"="ForwardBase"}
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"
//换了个回调,保证使用透明度测试的物体可以正确向其它物体投射投影
}
**clip(float4/3/2/1 x)**:如果x内任何一个分量是负数则舍弃当前像素输出颜色
结果很极端:要么完全透明要么完全不透明,而且边缘处存在锯齿,参差不齐(由于透明度的变化精度问题)
透明度混合实践
Unity提供混合命令Blend:
Blend Off:关闭混合
Blend SrcFactor DstFactor:开启混合并设置混合因子,计算公式如下:
$$DstColor_{new}=SrcAlpha*SrcColor+(1-SrcAlpha)*DstColor_{old}$$
Blend SrcFactor DstFactor,SrcFactorA DstFactorA:和上面一样,但计算公式变成:
$$ O_{rgb}=SrcFactorS_{rgb}+DstFactorD_{rgb} $$
$$ O_a=SrcFactorAS_a+DstFactorAD_a $$
通过两个公式分别计算新颜色的rgb和a值
BlendOp BlendOperation:使用BlendOperation对它们进行其它操作
(还有很多ShaderLab混合命令,这里就不说了)
Shader "Custom/AlphaBlend"
{
Properties
{
_Color ("Main 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"}
//因为是透明度混合,所以是Transparent
Pass{
Tags{"LightMode"="ForwardBase"}
ZWrite Off
//关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha
//原颜色和1-原颜色
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);
//返回的不是1,而是透明通道乘以材质参数
}
ENDCG
}
}
Fallback "Transparent/VertexLit"
}
但由于关闭了深度写入,在相互交叉时往往会得到错误的效果(后面的部分也能看见)
开启深度写入的半透明效果
为了针对上述错误,我们可以使用两个Pass:第一个开启深度写入而不输出颜色,目的是将该模型的深度值写入深度缓冲;第二个才是正常透明度融合。
只要在上面代码加上:
Pass{
ZWrite On
ColorMask 0
//用于设置颜色通道的写掩码,0代表不输出任何颜色
//这样就只需写入深度缓存即可
}
双面渲染的透明效果
可以通过Cull指令控制剔除哪个面的渲染图元:
Cull Back:剔除背对摄像机的图元
Cull Front:剔除正对摄像机的图元
Cull Off:关闭剔除功能,全会被渲染
透明度测试的双面渲染
因为图元被直接舍弃,所以最好的方法就是关闭剔除:
Pass{
Tags{"LightMode"="ForwardBase"}
Cull Off
//后面一样
}
透明度混合的双面渲染
因为透明度混合方法注重渲染的顺序,直接关闭剔除功能又不打开深度写入就会无法保证正面和背面的渲染顺序。所以最好的办法是分成两个Pass,第一个剔除正面,第二个剔除背面。由于Unity顺序执行,所以先渲染背面再渲染正面,这就保证了渲染顺序的正确性。
代码如下:
Pass{
Tags={"LightMode"="ForwardBase"}
Cull Front
......
//和之前一样的代码
}
Pass{
Tags={"LightMode"="ForwardBase"}
Cull Back
......
//和之前一样的代码
}
//其它都一样
- 本文链接:https://karmotrine.fun/2022/01/13/Unity-Shader03%E2%80%94%E2%80%94%E9%80%8F%E6%98%8E%E6%95%88%E6%9E%9C/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。
若没有本文 Issue,您可以使用 Comment 模版新建。