之前在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?

docs.unity3d.com
圖標
Graphics Command BuffersGraphics Command Buffers?

docs.unity3d.com
圖標

當時也看過LWRP自定義渲染管線的宣傳視頻,詳解Unity輕量級渲染管線LWRP:

Invitation to Join 詳解Unity輕量級渲染管線LWRP by Richard Yang 楊棟?

connect.unity.com
圖標

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的處理

最後的模糊效果。這個事其實還挺有意思的。

希望大家能喜歡吧


推薦閱讀:
相关文章