MonoGame 範例 NeonShooter 之 5
這是官方範例 NeonShooter 的第五篇,本篇將完成以下部分:
- 理解 BloomComponent
- 加入 Bloom 特效
1. 理解 BloomComponent
範例中使用的 Bloom 特效取自較早的 XNA 教學,連結已經失效了,相關的程式碼雖然還能在 Github 上找到,但是教學過程已經遺失了,我們只能從程式碼中自行研究了。
在開始使用之前必須先來了解程式碼的內容,Bloom 相關的程式碼總共兩個檔案 BloomComponent.cs 和 BloomSettings.cs,BloomComponent.cs 是實作特效主要的檔案,BloomSettings.cs 負責調整一些參數,另外還有 BloomCombine.fx, BloomExtract.fx, GaussianBlur.fx 三個 Shader 在最一開始已經加入 Content Pipeline 了。
BloomComponent.cspublic class BloomComponent : DrawableGameComponent
首先要來看 BloomComponent
,繼承了 DrawableGameComponent
,文件中的說明是這樣的:
A drawable object that, when added to the Game.Components collection of a Game instance, will have it’s Draw(GameTime) method called when Game.Draw(GameTime) is called.
意思就是只要將 BloomComponent
加入 Components
以後,當 base.Draw
被呼叫時就會呼叫 BloomComponent.Draw
,那如果有兩個 DrawableGameComponent
呢?
為了避免理解錯誤,接著去找了 MonoGame 的原始碼來看,在 Game.Draw
中只有遍歷了 _drawables
變數呼叫 Draw
,這個 _drawables
變數是一個 IDrawable
的容器,而 IDrawable
只有用在 DrawableGameComponent
上,也就是說 Game.Draw
中只會遍歷所有 Components
中的 DrawableGameComponent
,順序則是根據 DrawableGameComponent
的 DrawOrder
變數。
BloomComponent.csEffect bloomExtractEffect; Effect bloomCombineEffect; Effect gaussianBlurEffect; RenderTarget2D sceneRenderTarget; RenderTarget2D renderTarget1; RenderTarget2D renderTarget2;
接著往下看,可以看到有三個 Effect
變數,三個 RenderTarget2D
變數, Effect
主要是用來處理渲染過程中的效果,如變形、光照、貼圖、光暈等視覺效果,至少會包含一個 Pixel Shader 和一個 Vertex Shader,RenderTarget2D
是用來儲存渲染後的結果,過程中有多次渲染所以需要多個來儲存,同時還繼承了 Texture2D
因為包含了一個 2D 貼圖資料,可以做為 Texture2D
傳入任何需要的函式。
再往下可以發現兩個函式 LoadContent
和 UnloadContent
並沒有在程式碼內被呼叫過,這是因為在 DrawableGameComponent
中的 Initialize
, Dispose
分別會呼叫到,Initialize
會在兩個時機被呼叫,第一是當 Game.Initialize
之前已將 DrawableGameComponent
加入 Components
,則會在 Game.Initialize
中被呼叫,第二是在 Game.Initialize
之後加入 Components
時呼叫。
到目前為止 BloomComponent
主要架構上已經釐清了,剩下是內容的細節。
BloomComponent.csbloomExtractEffect = this.Game.Content.Load<Effect>("Shaders/BloomExtract"); bloomCombineEffect = this.Game.Content.Load<Effect>("Shaders/BloomCombine"); gaussianBlurEffect = this.Game.Content.Load<Effect>("Shaders/GaussianBlur");
先從 LoadContent
開始,在第一篇中我們直接拿了範例的 mgcb 檔,Shader 檔在 mgcb Build 過後已經轉成了可以被 Content.Load
讀取的格式,具體的過程可以在 MonoGame 原始碼的 EffectProcessor.cs 中看到,在之後只要將 Effect
作為參數傳入 SpriteBatch
就可以了。
BloomComponent.cs// Look up the resolution and format of our main backbuffer. PresentationParameters pp = GraphicsDevice.PresentationParameters; int width = pp.BackBufferWidth; int height = pp.BackBufferHeight; SurfaceFormat format = pp.BackBufferFormat; // Create a texture for rendering the main scene, prior to applying bloom. sceneRenderTarget = new RenderTarget2D(GraphicsDevice, width, height, false, format, pp.DepthStencilFormat, pp.MultiSampleCount, RenderTargetUsage.DiscardContents);
創建 RenderTarget2D
時需要將 GraphicsDevice
作為參數傳入,這是因為 RenderTarget2D
會使用到顯示卡的記憶體,因此如果呼叫了 GraphicsDevice.Reset
那麼 RenderTarget2D
也必須重新創建,後面的參數也都使用與 GraphicsDevice
當前的設定相同,最後一個參數 RenderTargetUsage.DiscardContents
則是說當畫面在渲染時,切換 RenderTarget 之後資料不須保留,因為這裡渲染的結果將作為貼圖來使用,所以不需要保留。
BloomComponent.cs// Create two rendertargets for the bloom processing. These are half the // size of the backbuffer, in order to minimize fillrate costs. Reducing // the resolution in this way doesn't hurt quality, because we are going // to be blurring the bloom images in any case. width /= 2; height /= 2; renderTarget1 = new RenderTarget2D(GraphicsDevice, width, height, false, format, DepthFormat.None); renderTarget2 = new RenderTarget2D(GraphicsDevice, width, height, false, format, DepthFormat.None);
接著的兩個 RenderTarget2D
在寬高只有畫面的一半,將作為模糊效果使用,即使減少畫面的精度也不影響表現,同時也節省一些開銷。
BloomComponent.cspublic void BeginDraw() { if (Visible) { GraphicsDevice.SetRenderTarget(sceneRenderTarget); } }
在 BeginDraw
內只有做一件事,把 GraphicsDevice
的 RenderTarget 換成 sceneRenderTarget
,NeonShooterGame.Draw
的最前面就呼叫了 BeginDraw
,這樣直到呼叫 Draw
之前 spriteBatch.Draw
的貼圖就都會被渲染到 sceneRenderTarget
上。
最後來看 Draw
函式,BloomComponent
中所有的效果都在這個函式內實現。
BloomComponent.csGraphicsDevice.SamplerStates[1] = SamplerState.LinearClamp; //... GraphicsDevice.Textures[1] = sceneRenderTarget;
第一行的 SamplerState
用來描述如何對貼圖採樣,SamplerStates[1]
則代表是對應 s1 Sampler,而在函式的最後還設定了 Textures[1]
為 sceneRenderTarget
,對應的則是 t1 Texture。
實際上運行以後,按 B 開啟 Bloom 效果會發現,畫面變得十分模糊,經過追查以後發現 Textures[1]
並沒有在 Shader 中作用,目前在 Github 上也有提出了 Issue,而根據官方人員在先前的文章回覆的內容上述的用法應該是無誤的,所以只能暫時認為是未修復的 BUG,解法也很簡單,將寫法改成舊式的寫法即可,在 BloomCombine.fx 中手動定義 BaseSampler
,將 sceneRenderTarget
以參數傳入 shader。
BloomCombine.fx//sampler BaseSampler : register(s1); sampler BaseSampler : register(s1) { Texture = (BaseTexture); Filter = Linear; AddressU = clamp; AddressV = clamp; };
BloomComponent.cs//GraphicsDevice.Textures[1] = sceneRenderTarget; bloomCombineEffect.Parameters["BaseTexture"].SetValue(sceneRenderTarget);
加上 Bloom 的效果分為以下四個步驟,在程式碼中分別是 Pass 1~4:
- 將
sceneRenderTarget
使用bloomExtractEffect
取出亮的部分渲染到renderTarget1
上。 - 將
renderTarget1
使用gaussianBlurEffect
加上水平方向的高斯模糊渲染到renderTarget2
上。 - 將
renderTarget2
使用gaussianBlurEffect
加上垂直方向的高斯模糊渲染到renderTarget1
上。 - 將
renderTarget1
和sceneRenderTarget
使用bloomCombineEffect
渲染到預設的 RenderTarget。
Shader 和高斯模糊的具體內容在這個範例裡就先不深究了,以了解使用方式為主。
2. 加入 Bloom 特效
了解 BloomComponent 以後就可以把檔案直接複製過來使用,不再另外更改。
Game1.csprivate GraphicsDeviceManager m_Graphics; private SpriteBatch m_SpriteBatch; private BloomComponent m_BloomComponent; //... public Game1 () { //... m_BloomComponent = new BloomComponent (this); Components.Add (m_BloomComponent); m_BloomComponent.Settings = new BloomSettings (null, 0.25f, 4, 2, 1, 1.5f, 1); m_BloomComponent.Visible = true; Content.RootDirectory = "Content"; IsMouseVisible = true; } //... protected override void Draw (GameTime _gameTime) { m_BloomComponent.BeginDraw (); GraphicsDevice.Clear (Color.CornflowerBlue); //... base.Draw (_gameTime); }
下一篇預計將加入粒子效果。
參考資料