2D游戏实现背景模糊

作者:李炜 2019-01-04

专栏地址: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

  1. Shader "Unlit/SpriteBlur"
  2. {
  3. Properties
  4. {
  5. _MainTex ("Base (RGB)", 2D) = "white" {}
  6. _Color ("_Color", Color) = (1,1,1,1)
  7. _Distortion ("Distortion", Range(0,3)) = 0
  8. _Alpha ("Alpha", Range (0,1)) = 1.0
  9. }

  10. SubShader
  11. {

  12. Tags {"Queue"="Transparent" "IgnoreProjector"="true" "RenderType"="Transparent"}
  13. ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off

  14. Pass
  15. {

  16. CGPROGRAM
  17. #pragma vertex vert
  18. #pragma fragment frag
  19. #pragma fragmentoption ARB_precision_hint_fastest
  20. #pragma target 3.0
  21. #include "UnityCG.cginc"

  22. struct appdata_t
  23. {
  24. float4 vertex : POSITION;
  25. float4 color : COLOR;
  26. float2 texcoord : TEXCOORD0;
  27. };

  28. struct v2f
  29. {
  30. half2 texcoord : TEXCOORD0;
  31. float4 vertex : SV_POSITION;
  32. fixed4 color : COLOR;
  33. };


  34. sampler2D _MainTex;
  35. fixed4 _Color;
  36. float _Distortion;
  37. fixed _Alpha;

  38. v2f vert(appdata_t IN)
  39. {
  40. v2f OUT;
  41. OUT.vertex = UnityObjectToClipPos(IN.vertex);
  42. OUT.texcoord = IN.texcoord;
  43. OUT.color = IN.color;
  44. return OUT;
  45. }

  46. float4 frag (v2f i) : COLOR
  47. {
  48. float stepU = 0.00390625f * _Distortion;
  49. float stepV = stepU;

  50. fixed3x3 gaussian = fixed3x3( 1.0,        2.0,        1.0, 2.0,        4.0,        2.0, 1.0,        2.0,        1.0);

  51. float4 result = 0;
  52. float4 Alpha = tex2D(_MainTex, i.texcoord);

  53. float2 texCoord;

  54. texCoord = i.texcoord.xy + float2( -stepU, -stepV ); result += tex2D(_MainTex,texCoord);
  55. texCoord = i.texcoord.xy + float2( -stepU, 0 ); result += 2.0 * tex2D(_MainTex,texCoord);
  56. texCoord = i.texcoord.xy + float2( -stepU, stepV ); result += tex2D(_MainTex,texCoord);
  57. texCoord = i.texcoord.xy + float2( 0, -stepV ); result += 2.0 * tex2D(_MainTex,texCoord);
  58. texCoord = i.texcoord.xy ; result += 4.0 * tex2D(_MainTex,texCoord);
  59. texCoord = i.texcoord.xy + float2( 0, stepV ); result += 2.0 * tex2D(_MainTex,texCoord);
  60. texCoord = i.texcoord.xy + float2( stepU, -stepV ); result += tex2D(_MainTex,texCoord);
  61. texCoord = i.texcoord.xy + float2( stepU, 0 ); result += 2.0* tex2D(_MainTex,texCoord);
  62. texCoord = i.texcoord.xy + float2( stepU, -stepV ); result += tex2D(_MainTex,texCoord);

  63. float4 r;
  64. r=result*0.0625;
  65. r.a*=Alpha.a*(1.0-_Alpha);
  66. r=r*i.color;        
  67. return r;        
  68. }
  69. ENDCG
  70. }
  71. }
  72. Fallback "Sprites/Default"
  73. }
复制代码


但是最后效果并不好,模糊的效果有点差


方案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官方示例里的高斯模糊

  1. Shader "Hidden/SeparableGlassBlur" {
  2. Properties {
  3. _MainTex ("Base (RGB)", 2D) = "" {}
  4. }

  5. CGINCLUDE

  6. #include "UnityCG.cginc"

  7. struct v2f {
  8. float4 pos : POSITION;
  9. float2 uv : TEXCOORD0;

  10. float4 uv01 : TEXCOORD1;
  11. float4 uv23 : TEXCOORD2;
  12. float4 uv45 : TEXCOORD3;
  13. };

  14. float4 offsets;

  15. sampler2D _MainTex;

  16. v2f vert (appdata_img v) {
  17. v2f o;
  18. o.pos = UnityObjectToClipPos(v.vertex);

  19. o.uv.xy = v.texcoord.xy;

  20. o.uv01 = v.texcoord.xyxy + offsets.xyxy * float4(1,1, -1,-1);
  21. o.uv23 = v.texcoord.xyxy + offsets.xyxy * float4(1,1, -1,-1) * 2.0;
  22. o.uv45 = v.texcoord.xyxy + offsets.xyxy * float4(1,1, -1,-1) * 3.0;

  23. return o;
  24. }

  25. half4 frag (v2f i) : COLOR {
  26. half4 color = float4 (0,0,0,0);

  27. color += 0.40 * tex2D (_MainTex, i.uv);
  28. color += 0.15 * tex2D (_MainTex, i.uv01.xy);
  29. color += 0.15 * tex2D (_MainTex, i.uv01.zw);
  30. color += 0.10 * tex2D (_MainTex, i.uv23.xy);
  31. color += 0.10 * tex2D (_MainTex, i.uv23.zw);
  32. color += 0.05 * tex2D (_MainTex, i.uv45.xy);
  33. color += 0.05 * tex2D (_MainTex, i.uv45.zw);

  34. return color;
  35. }
  36. ENDCG

  37. Subshader {
  38. Pass {
  39. ZTest Always Cull Off ZWrite Off
  40. Fog { Mode off }

  41. CGPROGRAM
  42. #pragma fragmentoption ARB_precision_hint_fastest
  43. #pragma vertex vert
  44. #pragma fragment frag
  45. ENDCG
  46. }
  47. }
  48. Fallback off
  49. }
复制代码

然后要给背景的SpriteRenderer替换一个渲染队列为2000的sprite材质。这样子实现分开渲染。这里有一个bug,修改材质之后原本的Order in Layer没有起作用,有的顺序低的还会跑到前面来。我现在的解决办法是用一个渲染队列为1999的sprite材质赋值,强制它变低。

接着就是增加一个Comma Buffer的脚本

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.Rendering;

  5. [ExecuteInEditMode]
  6. public class CommandBufferBlur : MonoBehaviour
  7. {
  8. [Tooltip("模糊程度")]
  9. public float BufferSize = 0.5f;
  10. public Shader m_BlurShader;
  11. private Material m_Material;

  12. private Camera m_Cam;

  13. private Dictionary<Camera, CommandBuffer> m_Cameras = new Dictionary<Camera, CommandBuffer>();

  14. // Remove command buffers from all cameras we added into
  15. private void Cleanup()
  16. {
  17. foreach (var cam in m_Cameras)
  18. {
  19. if (cam.Key)
  20. {
  21. cam.Key.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, cam.Value);
  22. }
  23. }
  24. m_Cameras.Clear();
  25. Object.DestroyImmediate(m_Material);
  26. }

  27. public void OnEnable()
  28. {
  29. Cleanup();
  30. SetCommandBuffer();
  31. }

  32. public void OnDisable()
  33. {
  34. Cleanup();
  35. }

  36. // Whenever any camera will render us, add a command buffer to do the work on it
  37. public void SetCommandBuffer()
  38. {
  39. var act = gameObject.activeInHierarchy && enabled;
  40. if (!act)
  41. {
  42. Cleanup();
  43. return;
  44. }

  45. var cam = Camera.main;
  46. if (!cam)
  47. return;

  48. CommandBuffer buf = null;
  49. // Did we already add the command buffer on this camera? Nothing to do then.
  50. if (m_Cameras.ContainsKey(cam))
  51. return;

  52. if (!m_Material)
  53. {
  54. m_Material = new Material(m_BlurShader);
  55. m_Material.hideFlags = HideFlags.HideAndDontSave;
  56. }

  57. buf = new CommandBuffer();
  58. buf.name = "Grab screen and blur";
  59. m_Cameras[cam] = buf;

  60. // copy screen into temporary RT
  61. int screenCopyID = Shader.PropertyToID("_ScreenCopyTexture");
  62. buf.GetTemporaryRT(screenCopyID, -1, -1, 0, FilterMode.Bilinear);
  63. buf.Blit(BuiltinRenderTextureType.CurrentActive, screenCopyID);

  64. // get two smaller RTs
  65. int blurredID = Shader.PropertyToID("_Temp1");
  66. int blurredID2 = Shader.PropertyToID("_Temp2");
  67. buf.GetTemporaryRT(blurredID, -2, -2, 0, FilterMode.Bilinear);
  68. buf.GetTemporaryRT(blurredID2, -2, -2, 0, FilterMode.Bilinear);

  69. // downsample screen copy into smaller RT, release screen RT
  70. buf.Blit(screenCopyID, blurredID);
  71. buf.ReleaseTemporaryRT(screenCopyID);

  72. // horizontal blur
  73. buf.SetGlobalVector("offsets", new Vector4(2.0f* BufferSize / Screen.width, 0, 0, 0));
  74. buf.Blit(blurredID, blurredID2, m_Material);
  75. // vertical blur
  76. buf.SetGlobalVector("offsets", new Vector4(0, 2.0f * BufferSize / Screen.height, 0, 0));
  77. buf.Blit(blurredID2, blurredID, m_Material);
  78. // horizontal blur
  79. buf.SetGlobalVector("offsets", new Vector4(4.0f * BufferSize / Screen.width, 0, 0, 0));
  80. buf.Blit(blurredID, blurredID2, m_Material);
  81. // vertical blur
  82. buf.SetGlobalVector("offsets", new Vector4(0, 4.0f * BufferSize / Screen.height, 0, 0));
  83. buf.Blit(blurredID2, blurredID, m_Material);

  84. buf.Blit(blurredID, BuiltinRenderTextureType.CameraTarget);

  85. cam.AddCommandBuffer(CameraEvent.AfterForwardOpaque, buf);
  86. }
  87. }
复制代码

我也是在官方的基础上修改的,所以有一些冗余的代码。

这里最后说一下为什么这种办法模糊效果更好,因为逻辑当中对图案模糊了4次,可以打开Frame Debug来查看


红框框住的就是Command的处理


最后的模糊效果。这个事其实还挺有意思的。

希望大家能喜欢吧

最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多