2121 字
11 分钟
透明效果

知识点#

整体思路

  1. 先渲染所有不透明物体,开启深度测试和深度写入
  2. 把所有半透明物体按他们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染。开启深度测试,但关闭深度写入

仍然存在的问题

  1. 深度测试是逐像素比较,而排序是逐物体的,即要么A排在B前面,要么B排在A前面。如果AB存在循环重叠/互相遮挡,则永远得不到正确的排序结果。
  2. 物体可能涵盖多个像素,每个像素的深度可能都不一致,选用哪一个像素的深度代表物体的深度,可能得到不同的深度排序结果。

解决思路 将物体分割,对分割后的物体再进行深度排序。尽可能让模型是凸面体,并且考虑将复杂模型拆分成可以独立排序的多个子模型。如果不想分割,可以让透明通道更为柔和,使穿插看起来不那么明显。也可使用开启了深度写入的半透明效果。

快速开始#

透明度测试#

片元的透明度大于阈值则保留,完全显示;否则舍弃,完全不显示。

  1. 顶点着色器:顶点变换、UV变换、计算世界法线和世界顶点坐标。
  2. 片元着色器:获取顶点着色器传入的世界法线和世界顶点坐标,根据UV采样透明度纹理,使用纹理采样结果的A通道值确定是否保留该片元。综合各项信息计算光照。
Shader "Unlit/Chapter8-AlphaTest"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color Tint",Color) = (1,1,1,1)
_Cutoff("Alpha Cutoff",Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="TransparentCutout" "Queue" = "AlphaTest" "IgnoreProjector" = "True" }//透明度测试的三个标签
Pass
{
Tags {"LightMode" = "ForwardBase"}//必须声明前向渲染模式才能得到一些Unity内置的光照变量
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"//必须声明这个头文件以获取灯光的信息
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed _Cutoff;
struct a2v
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal:NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);//注意这两个变换法线和变换坐标的方法
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal=normalize(i.worldNormal);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));//注意这个获取世界坐标系下光的方向的方法
fixed4 texColor=tex2D(_MainTex,i.uv);
fixed3 albedo=texColor.rgb*_Color.rgb;
clip(texColor.a-_Cutoff);//进行透明度测试的核心操作
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*albedo*saturate(dot(worldNormal,worldLightDir));
return fixed4(ambient+diffuse,1.0);
}
ENDCG
}
}
}

透明度混合#

将当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合(使用混合命令完成)

  1. 顶点着色器:顶点变换、UV变换、计算世界法线和世界顶点坐标。
  2. 片元着色器:接收顶点着色器的世界法线和世界顶点坐标,使用UV采样透明度纹理,使用采样结果的颜色值计算光照,使用采样结果的透明度值作为片元的透明度值。GPU 按 Blend 指令自动叠色。
Shader "Unlit/Chapter8-AlphaTest"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color Tint",Color) = (1,1,1,1)
_AlphaScale ("Alpha Scale",range(0,1)) = 0.5//控制透明度混合的程度
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" }//透明度混合的三个标签
Pass
{
Tags {"LightMode" = "ForwardBase"}
ZWrite Off//记得关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha//声明混合模式,没有这一行BLEND命令,即使代码写正确,也不会看到混合效果。因为默认情况下是BLEND OFF即没有混合,新片元颜色值会覆盖旧片元颜色值,效果就是完全不透明。
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal:NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal=normalize(i.worldNormal);
float3 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*saturate(dot(worldNormal,worldLightDir));
return fixed4(ambient+diffuse,texColor.a*_AlphaScale);//片元着色器只输出带透明度值的片元颜色值即可,GPU自动读取并进行混合操作
}
ENDCG
}
}
}

开启深度写入的半透明效果#

解决模型跨度大、相互遮挡、自遮挡时逐物体的深度排序失效,且不想进行模型分割。分两步渲染,第一步只写入深度,记录整个场景中半透明物体的深度情况;第二步正常渲染半透明颜色。由于第一步已经得到了正确的、完整的逐像素的深度信息,所以这一步的颜色渲染的结果也是正确的。

Shader "Unlit/Chapter8-AlphaTest"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color Tint",Color) = (1,1,1,1)
_AlphaScale ("Alpha Scale",range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" }//透明度测试的三个标签
Pass{
Zwrite On//写入深度缓冲
ColorMask 0//不输出任何颜色
}
Pass
{
Tags {"LightMode" = "ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal:NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal=normalize(i.worldNormal);
float3 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*saturate(dot(worldNormal,worldLightDir));
return fixed4(ambient+diffuse,texColor.a*_AlphaScale);
}
ENDCG
}
}
}

双面渲染#

使用Cull指令来控制需要剔除哪个面的渲染图元:Cull Back | Front | Off,分别对应剔除背面(默认)、剔除正面和不剔除(双面渲染)。

透明度测试的双面渲染#

在透明度测试的代码中,在Pass内加上Cull Off,关闭剔除。

Shader "Unlit/Chapter8-AlphaTest"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color Tint",Color) = (1,1,1,1)
_Cutoff("Alpha Cutoff",Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="TransparentCutout" "Queue" = "AlphaTest" "IgnoreProjector" = "True" }//透明度测试的三个标签
Pass
{
Tags {"LightMode" = "ForwardBase"}//必须声明前向渲染模式才能得到一些Unity内置的光照变量
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"//必须声明这个头文件以获取灯光的信息
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed _Cutoff;
struct a2v
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal:NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);//注意这两个变换法线和变换坐标的方法
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal=normalize(i.worldNormal);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));//注意这个获取世界坐标系下光的方向的方法
fixed4 texColor=tex2D(_MainTex,i.uv);
fixed3 albedo=texColor.rgb*_Color.rgb;
clip(texColor.a-_Cutoff);//进行透明度测试的核心操作
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*albedo*saturate(dot(worldNormal,worldLightDir));
return fixed4(ambient+diffuse,1.0);
}
ENDCG
}
}
}

透明度混合的双面渲染#

由于透明度混合关闭了深度写入,因此如果仅开启双面渲染,背面和正面的渲染顺序是未知的、不可控的,可能出现背面遮挡正面的不合理情况。所以需要两个Pass,第一个pass渲染背面,第二个pass渲染正面。两个pass的透明度混合的代码完全相同,只是第一个pass内剔除指令为Cull Front,第二个pass内剔除指令为Cull Back。

Shader "Unlit/Chapter8-AlphaTest"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color Tint",Color) = (1,1,1,1)
_AlphaScale ("Alpha Scale",range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" }//透明度测试的三个标签
Pass
{
Tags {"LightMode" = "ForwardBase"}
Cull Front//剔除正面,渲染背面
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal:NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal=normalize(i.worldNormal);
float3 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*saturate(dot(worldNormal,worldLightDir));
return fixed4(ambient+diffuse,texColor.a*_AlphaScale);
}
ENDCG
}
Pass
{
Tags {"LightMode" = "ForwardBase"}
Cull Back//剔除背面,渲染正面
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal:NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 worldNormal:TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldNormal=normalize(i.worldNormal);
float3 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*saturate(dot(worldNormal,worldLightDir));
return fixed4(ambient+diffuse,texColor.a*_AlphaScale);
}
ENDCG
}
}
}

透明效果
https://fuwari.vercel.app/posts/notes/unity_shader/transparent/
作者
Ruby
发布于
2026-05-14
许可协议
CC BY-NC-SA 4.0