|
文/李炜
专栏地址:https://zhuanlan.zhihu.com/p/53449081
之前在Switch上通关了《空洞骑士》感觉画面非常好看,而且很精致。光影,水纹,灰尘,背景都浑然一体非常好看。
因为之前自己也上线过2D游戏《J-Girl》,所以对好看的2D游戏的风格也很感兴趣,想实现一下类似《空洞骑士》的场景效果。
这篇主要记录一下背景模糊的实现。
我自己搜了一下,提到了2个方法
1、直接给背景的spriteRenderer赋值一个模糊材质
2、增加一个新的摄像机专门渲染背景层级,之后给相机增加一个模糊的后效
因为自己Shader写的也比较少。所以会经常翻冯乐乐的《UnityShader入门精要》,
书中10.2节介绍了一个“玻璃效果”,看完之后又多了两个解决方法
3、用渲染纹理来实现,实际和第二条差不多
4、用GrabPass,抓取屏幕图片当作一张纹理然后进行处理。
作者最后又留了一个介绍使用命令缓冲(Command Buffers)来实现,同时附加了一个官方的链接:
https://docs.unity3d.com/Manual/GraphicsCommandBuffers.html
Graphics Command Buffers
当时也看过LWRP自定义渲染管线的宣传视频,详解Unity轻量级渲染管线LWRP:
Invitation to Join 详解Unity轻量级渲染管线LWRP by Richard Yang 杨栋
LWRP支持增加特定插入点
有多了2种方案
5、使用Command Buffers来定义额外渲染操作
6、将Unity渲染管线设置为LWRP,然后实现接口
然后开始了实验步骤,方案1,直接上模糊材质,这里贴上sprite模糊的shader
- Shader "Unlit/SpriteBlur"
- {
- Properties
- {
- _MainTex ("Base (RGB)", 2D) = "white" {}
- _Color ("_Color", Color) = (1,1,1,1)
- _Distortion ("Distortion", Range(0,3)) = 0
- _Alpha ("Alpha", Range (0,1)) = 1.0
- }
- SubShader
- {
- Tags {"Queue"="Transparent" "IgnoreProjector"="true" "RenderType"="Transparent"}
- ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off
- Pass
- {
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #pragma fragmentoption ARB_precision_hint_fastest
- #pragma target 3.0
- #include "UnityCG.cginc"
- struct appdata_t
- {
- float4 vertex : POSITION;
- float4 color : COLOR;
- float2 texcoord : TEXCOORD0;
- };
- struct v2f
- {
- half2 texcoord : TEXCOORD0;
- float4 vertex : SV_POSITION;
- fixed4 color : COLOR;
- };
- sampler2D _MainTex;
- fixed4 _Color;
- float _Distortion;
- fixed _Alpha;
- v2f vert(appdata_t IN)
- {
- v2f OUT;
- OUT.vertex = UnityObjectToClipPos(IN.vertex);
- OUT.texcoord = IN.texcoord;
- OUT.color = IN.color;
- return OUT;
- }
- float4 frag (v2f i) : COLOR
- {
- float stepU = 0.00390625f * _Distortion;
- float stepV = stepU;
- fixed3x3 gaussian = fixed3x3( 1.0, 2.0, 1.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0);
- float4 result = 0;
- float4 Alpha = tex2D(_MainTex, i.texcoord);
- float2 texCoord;
- texCoord = i.texcoord.xy + float2( -stepU, -stepV ); result += tex2D(_MainTex,texCoord);
- texCoord = i.texcoord.xy + float2( -stepU, 0 ); result += 2.0 * tex2D(_MainTex,texCoord);
- texCoord = i.texcoord.xy + float2( -stepU, stepV ); result += tex2D(_MainTex,texCoord);
- texCoord = i.texcoord.xy + float2( 0, -stepV ); result += 2.0 * tex2D(_MainTex,texCoord);
- texCoord = i.texcoord.xy ; result += 4.0 * tex2D(_MainTex,texCoord);
- texCoord = i.texcoord.xy + float2( 0, stepV ); result += 2.0 * tex2D(_MainTex,texCoord);
- texCoord = i.texcoord.xy + float2( stepU, -stepV ); result += tex2D(_MainTex,texCoord);
- texCoord = i.texcoord.xy + float2( stepU, 0 ); result += 2.0* tex2D(_MainTex,texCoord);
- texCoord = i.texcoord.xy + float2( stepU, -stepV ); result += tex2D(_MainTex,texCoord);
- float4 r;
- r=result*0.0625;
- r.a*=Alpha.a*(1.0-_Alpha);
- r=r*i.color;
- return r;
- }
- ENDCG
- }
- }
- Fallback "Sprites/Default"
- }
复制代码
但是最后效果并不好,模糊的效果有点差
方案2和方案3并没有实践,因为我知道效果肯定能实现,但是多的开销其实没有太大的意义,冯乐乐虽然在书中写了一句:“尽管这种发放需要把部分场景再次渲染一遍,但是我们可以通过调整摄像机的渲染层减少二次渲染的场景大小,或使用其他方法控制摄像机是否需要开启。”因为背景的模糊其实是一直长期存在的,如果别的方法能实现,就不想增加一个相机。
方案4通过GrabPass实现,这个开销更大,书上补充了“高分辨率的设备上可能会造成严重的带宽影响,而且移动设备有的不支持”。我也上网查过,老外的建议是能用Command Buffers实现就不要用GrabPass
方案5和方案6,其实我是先用了方案6 LWRP,但是很可惜在2D游戏里LWRP并没有像宣传的产生高效的渲染效果,当我替换渲染管线之后,场景的FPS降低了一半。所以有就直接放弃了LWRP的方案。(可能是因为LWRP是给3D使用的,2D游戏没有使用灯光,所以显得鸡肋)
没有使用LWRP
使用LWRP
最后方案5 使用Command Buffers的效果,先来一张最后的效果图吧
模糊的很均匀,就是我要的效果。
首先是添加一个Shader,这里是直接用Unity官方示例里的高斯模糊
- Shader "Hidden/SeparableGlassBlur" {
- Properties {
- _MainTex ("Base (RGB)", 2D) = "" {}
- }
- CGINCLUDE
- #include "UnityCG.cginc"
- struct v2f {
- float4 pos : POSITION;
- float2 uv : TEXCOORD0;
- float4 uv01 : TEXCOORD1;
- float4 uv23 : TEXCOORD2;
- float4 uv45 : TEXCOORD3;
- };
- float4 offsets;
- sampler2D _MainTex;
- 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;
- }
- 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;
- }
- ENDCG
- Subshader {
- Pass {
- ZTest Always Cull Off ZWrite Off
- Fog { Mode off }
- CGPROGRAM
- #pragma fragmentoption ARB_precision_hint_fastest
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
- }
- Fallback off
- }
复制代码
然后要给背景的SpriteRenderer替换一个渲染队列为2000的sprite材质。这样子实现分开渲染。这里有一个bug,修改材质之后原本的Order in Layer没有起作用,有的顺序低的还会跑到前面来。我现在的解决办法是用一个渲染队列为1999的sprite材质赋值,强制它变低。
接着就是增加一个Comma Buffer的脚本
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- using UnityEngine.Rendering;
- [ExecuteInEditMode]
- public class CommandBufferBlur : MonoBehaviour
- {
- [Tooltip("模糊程度")]
- public float BufferSize = 0.5f;
- public Shader m_BlurShader;
- private Material m_Material;
- private Camera m_Cam;
- private Dictionary<Camera, CommandBuffer> m_Cameras = new Dictionary<Camera, CommandBuffer>();
- // Remove command buffers from all cameras we added into
- private void Cleanup()
- {
- foreach (var cam in m_Cameras)
- {
- if (cam.Key)
- {
- cam.Key.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, cam.Value);
- }
- }
- m_Cameras.Clear();
- Object.DestroyImmediate(m_Material);
- }
- public void OnEnable()
- {
- Cleanup();
- SetCommandBuffer();
- }
- public void OnDisable()
- {
- Cleanup();
- }
- // Whenever any camera will render us, add a command buffer to do the work on it
- public void SetCommandBuffer()
- {
- var act = gameObject.activeInHierarchy && enabled;
- if (!act)
- {
- Cleanup();
- return;
- }
- var cam = Camera.main;
- if (!cam)
- return;
- CommandBuffer buf = null;
- // Did we already add the command buffer on this camera? Nothing to do then.
- if (m_Cameras.ContainsKey(cam))
- return;
- if (!m_Material)
- {
- m_Material = new Material(m_BlurShader);
- m_Material.hideFlags = HideFlags.HideAndDontSave;
- }
- buf = new CommandBuffer();
- buf.name = "Grab screen and blur";
- m_Cameras[cam] = buf;
- // copy screen into temporary RT
- int screenCopyID = Shader.PropertyToID("_ScreenCopyTexture");
- buf.GetTemporaryRT(screenCopyID, -1, -1, 0, FilterMode.Bilinear);
- buf.Blit(BuiltinRenderTextureType.CurrentActive, screenCopyID);
- // get two smaller RTs
- int blurredID = Shader.PropertyToID("_Temp1");
- int blurredID2 = Shader.PropertyToID("_Temp2");
- buf.GetTemporaryRT(blurredID, -2, -2, 0, FilterMode.Bilinear);
- buf.GetTemporaryRT(blurredID2, -2, -2, 0, FilterMode.Bilinear);
- // downsample screen copy into smaller RT, release screen RT
- buf.Blit(screenCopyID, blurredID);
- buf.ReleaseTemporaryRT(screenCopyID);
- // horizontal blur
- buf.SetGlobalVector("offsets", new Vector4(2.0f* BufferSize / Screen.width, 0, 0, 0));
- buf.Blit(blurredID, blurredID2, m_Material);
- // vertical blur
- buf.SetGlobalVector("offsets", new Vector4(0, 2.0f * BufferSize / Screen.height, 0, 0));
- buf.Blit(blurredID2, blurredID, m_Material);
- // horizontal blur
- buf.SetGlobalVector("offsets", new Vector4(4.0f * BufferSize / Screen.width, 0, 0, 0));
- buf.Blit(blurredID, blurredID2, m_Material);
- // vertical blur
- buf.SetGlobalVector("offsets", new Vector4(0, 4.0f * BufferSize / Screen.height, 0, 0));
- buf.Blit(blurredID2, blurredID, m_Material);
- buf.Blit(blurredID, BuiltinRenderTextureType.CameraTarget);
- cam.AddCommandBuffer(CameraEvent.AfterForwardOpaque, buf);
- }
- }
复制代码
我也是在官方的基础上修改的,所以有一些冗余的代码。
这里最后说一下为什么这种办法模糊效果更好,因为逻辑当中对图案模糊了4次,可以打开Frame Debug来查看
红框框住的就是Command的处理
最后的模糊效果。这个事其实还挺有意思的。
希望大家能喜欢吧
|
|