Unity Shader之磨砂玻璃与水雾玻璃效果

2020年01月30日 11:14 2 点赞 1 评论 更新于 2025-11-21 21:31
Unity Shader之磨砂玻璃与水雾玻璃效果

导读

玻璃效果在游戏场景中十分常见,除了普通的透明玻璃,磨砂玻璃效果也较为常用。此外,玻璃与场景中的其他物体存在交互,例如浴室玻璃、雨天窗户在水汽作用下会呈现出不同的雾效。本文将以Unity Frosted Glass项目及开源库中的相关项目为例,详细介绍磨砂玻璃效果的实现方法,并分析其在移动端的运行性能。

开源库链接https://lab.uwa4d.com/lab/5b5613a3d7f10a201fd80bbb

模糊效果

磨砂玻璃具有模糊和半透明的特点,本项目通过自定义的卷积实现来达成模糊效果,具体代码实现在FrostedGlass.shader中。

顶点着色器及相关数据结构(SeparableGlassBlur.shader

// vertex to fragment
struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float4 uv01 : TEXCOORD1;
float4 uv23 : TEXCOORD2;
float4 uv45 : TEXCOORD3;
};

v2f vert (appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy;
o.uv01 = v.texcoord.xyxy + offsets.xyxy * float4(1, 1, -1, -1);
o.uv23 = v.texcoord.xyxy + offsets.xyxy * float4(1, 1, -1, -1) * 2.0;
o.uv45 = v.texcoord.xyxy + offsets.xyxy * float4(1, 1, -1, -1) * 3.0;
return o;
}

offsets是一个float4类型的定值,表示偏移量。经过顶点着色器的运算,输出的v2f结构体中,pos存储了该顶点从物体空间转换到相机裁剪空间后的齐次坐标;uvuv01uv23uv45分别存储了该顶点、偏移量为offsets的两个顶点、偏移量为2 * offsets的两个顶点、偏移量为3 * offsets的两个顶点的UV坐标。

片元着色器(SeparableGlassBlur.shader

half4 frag (v2f i) : COLOR {
half4 color = float4(0, 0, 0, 0);
color += 0.40 * tex2D(_MainTex, i.uv);
color += 0.15 * tex2D(_MainTex, i.uv01.xy);
color += 0.15 * tex2D(_MainTex, i.uv01.zw);
color += 0.10 * tex2D(_MainTex, i.uv23.xy);
color += 0.10 * tex2D(_MainTex, i.uv23.zw);
color += 0.05 * tex2D(_MainTex, i.uv45.xy);
color += 0.05 * tex2D(_MainTex, i.uv45.zw);
return color;
}

该卷积核的一维权重分布如下: | 偏移量 | 权重 | | ---- | ---- | | 0 | 0.40 | | ±1 | 0.15 | | ±2 | 0.10 | | ±3 | 0.05 |

滤波操作(CommandBufferBlur.cs

_CommandBuffer.SetGlobalVector("offsets", new Vector4(2.0f / sizes[i].x, 0, 0, 0));
_CommandBuffer.Blit(blurredID, blurredID2, _Material);
_CommandBuffer.SetGlobalVector("offsets", new Vector4(0, 2.0f / sizes[i].y, 0, 0));
_CommandBuffer.Blit(blurredID2, blurredID, _Material);

对图像使用水平方向一维卷积核与竖直方向一维卷积核进行两次滤波,即可得到最终的图像,这等同于使用一个二维卷积核进行滤波。

卷积核的选择有多种,较为常用的有高斯模糊、kawase Blur等,开源库中有相关项目实现了这些效果,例如:Blur for UnityGaussian BlurSuper Blur。不同的模糊方式采用的卷积核各不相同,有兴趣的读者可以进行相关试验。

捕捉屏幕纹理

实现模糊效果后,需要捕捉玻璃后方的屏幕图像,并将其交给模糊效果着色器进行处理。在Unity Shader中,GrabPass可以方便地获取屏幕图像,但这种方式开销较大,不适合在移动端运行。本项目采用CommandBuffer来实现这一目的,以节省开销、提高性能。

获取屏幕图像(CommandBufferBlur.cs

// 创建名为“Blur screen”的CommandBuffer
_CommandBuffer = new CommandBuffer();
_CommandBuffer.name = "Blur screen";

int screenCopyID = Shader.PropertyToID("_ScreenCopyTexture");
// 新建一个临时RenderTexture
_CommandBuffer.GetTemporaryRT(screenCopyID, -1, -1, 0, FilterMode.Bilinear, _TextureFormat);
// 获取当前屏幕图像
_CommandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, screenCopyID);
int blurredID = Shader.PropertyToID("_Grab" + i + "_Temp1");
int blurredID2 = Shader.PropertyToID("_Grab" + i + "_Temp2");
_CommandBuffer.GetTemporaryRT(blurredID, (int)sizes[i].x, (int)sizes[i].y, 0, FilterMode.Bilinear, _TextureFormat);
_CommandBuffer.GetTemporaryRT(blurredID2, (int)sizes[i].x, (int)sizes[i].y, 0, FilterMode.Bilinear, _TextureFormat);
_CommandBuffer.Blit(screenCopyID, blurredID);
_CommandBuffer.ReleaseTemporaryRT(screenCopyID);

此时获取到的是一整张屏幕图像,而我们需要处理的只是玻璃模型背后的图像,因此需要在顶点着色器中对顶点进行处理。

顶点着色器处理(FrostedGlass.shader

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 确保材质球中的缩放和偏移位置正确
o.uvfrost = TRANSFORM_TEX(v.uv, _FrostTex);
// 获得该顶点在屏幕图象中正确的纹理坐标
o.uvgrab = ComputeGrabScreenPos(o.vertex);
return o;
}

Unity内置的ComputeGrabScreenPos函数帮助我们完成了坐标转换,根据o.uvgrab在屏幕图像中采集到的图像信息,就是玻璃模型背后的屏幕图像:

half4 ref00 = tex2Dproj(_GrabBlurTexture_0, i.uvgrab);

CommandBuffer挂载到相机上,即可实现实时更新渲染:

_Camera.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, _CommandBuffer);

水雾玻璃效果

在磨砂玻璃的基础上,可以进一步拓展其他特殊效果。浴室玻璃、雨天窗户在水汽作用下的雾效,除了模糊和半透明外,还存在模糊程度的差别。

定义不同大小的偏移量(CommandBufferBlur.cs

Vector2[] sizes = {
new Vector2(Screen.width, Screen.height),
new Vector2(Screen.width / 2, Screen.height / 2),
new Vector2(Screen.width / 4, Screen.height / 4),
new Vector2(Screen.width / 8, Screen.height / 8)
};

通过SeparableGlassBlur.shader的运算,得到四张模糊程度不同的屏幕图像,模糊程度从0到3依次加深:

sampler2D _GrabBlurTexture_0;
sampler2D _GrabBlurTexture_1;
sampler2D _GrabBlurTexture_2;
sampler2D _GrabBlurTexture_3;

片元着色器(FrostedGlass.shader

fixed4 frag (v2f i) : SV_Target
{
float surfSmooth = 1 - tex2D(_FrostTex, i.uvfrost) * _FrostIntensity;
// 如果x 值小于 a,则返回a;如果 x 值大于 b,返回b;否则,返回 x
surfSmooth = clamp(0, 1, surfSmooth);
half4 refraction;
// 二维纹理投影映射
half4 ref00 = tex2Dproj(_GrabBlurTexture_0, i.uvgrab);
half4 ref01 = tex2Dproj(_GrabBlurTexture_1, i.uvgrab);
half4 ref02 = tex2Dproj(_GrabBlurTexture_2, i.uvgrab);
half4 ref03 = tex2Dproj(_GrabBlurTexture_3, i.uvgrab);
// 进行平滑过渡
float step00 = smoothstep(0.75, 1.00, surfSmooth);
float step01 = smoothstep(0.5, 0.75, surfSmooth);
float step02 = smoothstep(0.05, 0.5, surfSmooth);
refraction = lerp(lerp(lerp(ref03, ref02, step02), ref01, step01), ref00, step00);
return refraction;
}

项目以_FrostTex图像的r值作为采样依据,根据1 - r值 * _FrostIntensity得到的数值(surfSmooth)作为权重,从四张模糊程度不同的屏幕图像中进行采集,最终得到该顶点的颜色。通过不同的surfSmooth值,可以渲染出不同的模糊程度,从而模拟出不同的水雾玻璃效果。

性能测试(使用UWA GOT Online工具测评)

选择低端机型红米4x进行测试(不开启多线程渲染):

  • 使用水雾玻璃效果
  • FPS均值为26帧。
  • Camera.Render函数耗时约11ms,开销不大。
  • CPU等待GPU渲染完成的时间较长,当前渲染压力在GPU端。可见,实时抓取屏幕图片并进行渲染操作在移动端的开销较大。该效果生成四张模糊效果图片,每张图片的生成需要进行两次SeparableGlassBlur.shader的计算,最终每个顶点还需要在FrostedGlass.shader中进行运算,导致GPU计算量过大。
  • 只使用磨砂玻璃效果:红米4x的FPS均值约为42帧。此时只生成一张模糊效果图片,该图片的生成同样需要进行两次SeparableGlassBlur.shader的计算。相比之下,GPU端的计算量大幅减少,CPU等待时间缩短,性能提升明显。

因此,开发者在实现此效果时,需要在性能与效果之间进行平衡,尽可能减少计算量。例如,可以使用3x3的卷积核代替5x5的卷积核,或者在采样之前进行判断,减少采样次数。

今天的推荐就到这里,这个项目既可以直接使用,也可以根据需求进行优化,希望能给您带来一些启发。