遊戲場景渲染中的PostProcessing(後處理)
1.基礎部分
PostProcessing,通常在普通的場景渲染結束後對結果進行處理,將一張或數張Texture處理得到一張新的Texture.
PostProcessing的渲染Pipeline普通的模型渲染一樣,不同之處在於在VertexShader中通常只是簡單的拷貝,主要的邏輯寫在Pixel Shader中.
拷貝圖片的圖元是什麼樣的呢?直覺來看就是一個長方形圖元,由兩個三角形組成.但是一般是通過一個大一點單個三角形圖元來實現的,超出長方形框外的部分在光柵化時會被自動過濾掉,這樣寫起來更簡潔,執行速度也稍快一些.
一個三角形的渲染拷貝作用和一個長方形是一樣的
需要注意的是,OpenGL中Texture左下角UV點是(0,0),而DirectX中左上角UV是(0,0).
另外Direct3D 9中有Half-Pixel Offset的問題,取樣畫素時需要將uv點偏移半畫素或者計算頂點座標時時偏移半畫素(遊戲引擎裡一般是直接處理編譯好的shader,這樣不用為不同版本寫不同的shader),來看下微軟文件裡的幾張圖,就能直觀地知道Half-Pixel 是什麼問題:
Direct3D 10 Pixel Coordinate
Direct3D 9 Pixel Coordinate
Direct3D Texel Coordinate
2.UV的使用
根據Texture中點UV的值,施加不同的效果,比如常見的Vignette效果,就是根據到螢幕中心點的距離,混合黑色的顏色,產生邊緣灰暗的效果,非常的簡單.
Vignette效果
根據Texture中畫素點的uv值做偏移,可以得到一些變形,扭曲,波浪,鏡頭變形等效果.比如下面的一個波浪的效果,FS程式碼大致是這樣的:
波浪效果
vec2 v = UV - vec2(0.5, 0.5);
vec2 offset = sin((length(v) * 5 + TIME) * PI * 2) * normalize(v) * 0.1;
COLOR = texture(TEXTURE, UV + offset);
根據到中心點的距離,用正弦函式計算沿著中心方向UV的偏移量,就可以得到波浪的效果.然後在相位上加上時間,可以得到隨時間變化的動態波浪.
3.Noise Texture/Lookup Texture
噪聲圖也是非常常用的一種非常常用的Texture,通常我們見到的都是Perlin Noise Texture,長得是這樣的:
Perlin Noise Texture
比如用Perlin Noise Texture來生成一個Dissolve的效果,就是指定一個閾值,動態clip掉Perlin Noise Texture中相應點的灰度值在閾值下的點:
Dissolve
隨時間改變這個閾值,就可以得到逐漸出現或者消失的Dissolve效果.
還有的Texture儲存一些(u,v)到某些值的對映,叫做Loopup Texture(LUT).很多張Texture組成Array還可以完成(x,y,z)到值的對映,叫做3D LUT.
LUT最常見的應用就是用來做Color Grading.
一個用來ColorGrading的LUT
4.Image Kernels
將在UV當前點周圍數點取樣,然後將多個點處理結果相加,這樣的處理寫成矩陣叫做Image Kernel,這樣的操作也可以叫做Convolution,比如一個邊緣檢測的Kernel是這樣的:
將中心點的畫素值X8,然後減去周圍點的畫素值,就可以做邊緣檢測.
Edge Detection
一些常見的如Sharpen, Blur等各種各樣的效果都是通過這種方式實現的.
5.Gaussian Blur
從訊號和取樣的角度來說,有這樣幾種Filtering:
box filter,tent filter,sinc filter 所有filter的面積和是1
Sinc filtering計算起來最複雜,效果也是最好的.
Filtering來重構訊號的過程大致是這樣:
sinc filter
Guassian Blur可以看作是Sinc Filter的一種近似,用這樣一個距離相關的Kernel 函式:
實際操作時,忽略較遠處的值,用預計算好的權重值,比如一個5X5的Kernel:
5X5 Guassion weight
這樣每次計算時需要取樣25個點才能得到最終的畫素值,因此可以將Guassion blur分成兩次Pass來實現,Kernel是這樣的:
兩次Pass的Guassion weight
這樣每個點需要的取樣次數就降到了10次.Box-filter(周圍N*N個點權重相等)也是可以像這樣分成兩次Pass的,這樣的Kernel是separable的.
當GPU從貼圖中取樣時,使用bilinear方式在某個點取樣時,是根據點的位置從目標點周圍4點中取值加權平均,利用這點,也可以減少需要的取樣次數.比如一個3X3的Box-filter時,所有點的權重相等,將取樣目標點放到4點中心,就可以得到周圍4點的平均值:
9個取樣點降到5個和4個
Guassion blur也可以通過這中方法來減少取樣數,兩種方式結合起來,可以使取樣數達到最小.
6.DownSample/UpSample
一些需要Blur效果的PostProcessing通常將將TextureDownSample到一個較小的尺寸,自帶Blur的效果基本上不會影響最總的效果,較小的Texure還可以加快執行速度.
Bloom的效果,就是使較亮的部分更亮點,並且擴散到附近.就是通過一系列的DownSample和UpSample,最後疊加到原來圖片上來實現的.
Bloom 4次DownSample
Bloom 4次UpSample和最終顏色疊加的效果
7.Depth Buffer
正常地渲染場景後,可以得到Depth Buffer.PostProcessing時,從當前的UV值,和Depth Buffer中讀取到的深度值,可以得到裁剪空間中的座標,再從裁剪空間可以反推出當前畫素點在世界空間中的位置.
需要注意的是,OpenGL中是將-1~1的深度值對映到0~1的Depth Buffer的深度值,從中讀取時需要將得到的值應用d * 2 - 1,得到在裁剪空間中的深度值.
得到世界空間中的座標後,就可以應用一些效果,比如Depth Fog效果,就是計算點到攝影機的距離,根據距離用對數函式計算出一個霧的強度值,將霧的顏色疊加到原始顏色上,就可以得到Depth Fog效果.也可以指定一個地面的高度,根據到地面的距離得到Height Fog的效果.
再看下Depth of Field(DOF 景深)的效果,DOF先設定一個對焦的距離和範圍,超出對焦範圍的部分做blur處理.一個簡單實現的DOF的大致過程如下:
a.將原圖DownSample到1/4大小.
b.確定模糊的強度,模糊強度隨距離變化的曲線大致如下圖所示,根據深度值計算每個點的模糊強度,儲存到一張Texture中.
dof強度和距離的關係
c.對DownSample的Texture進行Blur,每個點使用上面的Texture中的模糊強度計算模糊半徑.用模糊半徑在一個預計算好的Disk中隨機分佈的點位置進行取樣,點的分佈大致是下面的圖片中所示.
d.將得到的Texture用9-tap Tent Filter再次Blur.
e.將得到的Texture和原圖按照計算出的模糊強度混合,得到DOF處理後的圖片.
DOF
8.Motion Blur/Temporal AA
Motion Blur的實現:每個物體記錄上一幀的位置,計算出物體相對上一幀的位置偏移,算出相對鏡頭的速度值,渲染到一個Velocity Buffer上,再根據Velocity Buffer做Blur處理.
TAA:將每個物體的投影矩陣加上一點不同的Jittering,每幀的jittering值不斷變化,將連續兩幀或數幀的結果進行插值,以此來實現快速的AA.
實際的遊戲中有的會將Motion Blur和TAA結合起來實現更好的效果.
這裡僅僅簡單介紹下這兩種處理基本的原理,不再深入探究.
9.Compute Shader
Computer Shader非常適用於PostProcessing,因為不需要Rasterize,OutputMerge等階段,Computer Shader可以執行地比Pixel Shader更快.大部分Post Processing都可以直接切換成CS的寫法來獲得速度提升.不僅如此,CS的GroupShaderdMemory實現執行單元間共享記憶體,讓CS可以用來實現一些PS無法實現的功能.
來看下Auto Exposure的實現,Auto Exposure的原理,就是將所有Texture中的畫素的明亮度統計,計算得到一個合適的曝光度,再應用到Texture上.
使用Pixel Shader來實現的話,將需要計算的Texture上每個點的明亮度,並DownSample到一個合適大小的Texture上,再將該Texture連續按照大小的1/2 DownSample 直到一個1X1的Texture上,該Texture上的值就是所有點的平均明亮度,就可以改變Texture的曝光度.
Pixel Shader的缺點就是隻能得到所有點亮度的平均值或者加權平均值.使用CS則可以統計所有明亮度範圍並進行計數,實現更加強大的Auto Exposure.
來看下一個簡單的AutoExposure CS的實現:
先將Texture DownSample到某個合適的大小,再用CS計算:
- [numthreads(HISTOGRAM_THREAD_X, HISTOGRAM_THREAD_Y, 1)]
- void KEyeHistogram(uint2 dispatchThreadId : SV_DispatchThreadID, uint2 groupThreadId : SV_GroupThreadID)
- {
- const uint localThreadId = groupThreadId.y * HISTOGRAM_THREAD_X + groupThreadId.x;
- // 使用其中部分Thread清零共享記憶體
- if (localThreadId < HISTOGRAM_BINS)
- {
- gs_histogram[localThreadId] = 0u;
- }
- float2 ipos = float2(dispatchThreadId) * 2.0;
- //Thread之間同步等待
- GroupMemoryBarrierWithGroupSync();
- if (ipos.x < _ScaleOffsetRes.z && ipos.y < _ScaleOffsetRes.w)
- {
- uint weight = 1u;
- float2 sspos = ipos / _ScaleOffsetRes.zw;
- // Vignette 權重,中心位置點的權重會更高
- #if USE_VIGNETTE_WEIGHTING
- {
- float2 d = abs(sspos - (0.5).xx);
- float vfactor = saturate(1.0 - dot(d, d));
- vfactor *= vfactor;
- weight = (uint)(64.0 * vfactor);
- }
- #endif
- float3 color = _Source.SampleLevel(sampler_LinearClamp, sspos, 0.0).xyz; // Bilinear downsample 2x
- //計算明亮度
- float luminance = Luminance(color);
- float logLuminance = GetHistogramBinFromLuminance(luminance, _ScaleOffsetRes.xy);
- //根據明亮度計算得到需要累加到的bin位置
- uint idx = (uint)(logLuminance * (HISTOGRAM_BINS - 1u));
- //相應bin位置的計數原子累加
- InterlockedAdd(gs_histogram[idx], weight);
- }
- //同步等待
- GroupMemoryBarrierWithGroupSync();
- //使用其中的部分Thread來將累計值從共享記憶體寫入到RWBuffer
- if (localThreadId < HISTOGRAM_BINS)
- {
- InterlockedAdd(_HistogramBuffer[localThreadId], gs_histogram[localThreadId]);
- }
這樣我們就得到帶有明亮值分佈的Histogram,不僅可以用來計算平均明亮值,還可以根據分佈情況作進一步優化.
UE4中Eye Adaption的Debug
10.Screen-Space Methods
有了Depth Buffer來獲取當前點再世界座標的位置,就可以更進一步,在目標點周圍取點,判斷可見性,進行RayMarching等操作.這類方法叫做Screen-Space Method,常見的有SSAO(Screen Space Abbient Occlusion), SSR(Screen Space Reflecttion)等.
這裡介紹下一個簡單的SSAO的實現.
a.準備Depth Buffer和Normal Buffer,如果是Deferred Rendering,直接在GBUFFER中就能取到,如果是Forward Rendering,可以通過Depth Buffer生成.
b.在一個半球形的區域中隨機取點,並且加上隨機的旋轉值.將隨機到的點通過Normal Buffer計算得到世界空間中的座標,再計算得到在裁剪空間中的值,並和DepthTexture中的深度值進行比較來判斷是否被遮擋,將結果累加,得到一個儲存AO值的buffer.
SSAO Sample Kernel
c.將得到的AO Buffer進行Blur.
d.根據AO值修改Diffuse的光照值,產生AO的效果.
SSAO的效果
SSR的原理和SSAO很相似,需要用到Normal Buffer和Metal Buffer(表示鏡面光反照率,一般Gbuffer中有),根據需要計算反射光照的點的Normal和Metal值,計算得到一條反射光線,沿著這條線進行RayMarch,得到第一個一個與該射線相交的點,將該點的顏色當作反射的顏色進行混合.
步長不斷變化的RayMarching
SSR
因為SSR是Screen Space的,所以無法反射螢幕之外的場景.
11.小結
(1)本文基本上介紹到了大部分比較常見的PostProcessing,其他的效果也都是通過這些方法的擴充套件和延伸.
(2) 各個PostProcessing的順序非常重要,比如SSR SSAO一般在渲染透明物體前執行,DOF Bloom等一般在所有物體渲染結束後,ToneMapping之前,一些自定義的變形效果通常在ToneMapping之後.
(3)各個效果之間可能會互相干擾,比如TAA就會影響很多效果的計算,需要在計算時考慮到這些因素.
(4)各個PostProcessing之間有的可以共用一些Texture,以及在一次Pass中合併執行多個操作,來降低效能消耗.
(5)實際專案中的應用實現一般比文中提到的要複雜得多,需要細心地調節引數,不斷測試改進.本文只是從原理方面簡單描述下,具體的應用還需要參考更加詳細的文件.
作者:TC130
專欄地址:https://zhuanlan.zhihu.com/p/105909416
相關文章
- 遊戲場景中的光影設計遊戲
- 手遊逆向分析: Unity內還原遊戲場景/角色渲染效果Unity遊戲
- 析電子遊戲《空洞騎士》場景切換時的音樂音響處理遊戲
- 乾貨:遊戲中“沙漠”場景的設計手法遊戲
- 遊戲開發中的多語言處理遊戲開發
- 建築師解構遊戲關卡——遊戲研發中的場景需求遊戲
- 關於遊戲中的實時渲染遊戲
- 影片場景下的自然語言處理應用自然語言處理
- Azure OpenAI在遊戲NPC和製作場景中的應用OpenAI遊戲
- 視訊場景下的自然語言處理應用自然語言處理
- 《控制》:從遊戲場景中的“視覺享受”到我們可以享受的“遊戲環境設計”遊戲視覺
- cocos2d-js:遊戲進入後臺和返回遊戲的事件捕獲和處理JS遊戲事件
- SparkSQL中產生笛卡爾積的幾種典型場景以及處理策略SparkSQL
- Java異常處理場景中不同位置的返回值詳細解析Java
- Maxwell 磁場模擬場計算後處理
- 如何分析休閒遊戲的廣告場景?遊戲
- 探討遊戲反饋的處理方法遊戲
- 巧用列舉來處理UI中顯示值與業務值不同的場景UI
- 企業級應用場景中,LLM 的資料特性剖析及處理對策
- Apache Flink 如何正確處理實時計算場景中的亂序資料Apache
- ETL中後設資料處理的方式
- 基於WebGL HTML5 的場景小遊戲WebHTML遊戲
- 複雜場景資料處理的 OLTP 與 OLAP 融合實踐
- steam開始遊戲後自動取消如何處理_steam遊戲開始後立馬自動結束脩複方法遊戲
- 遊戲背後的文化:心流理論與遊戲難度曲線遊戲
- 從中國的山水畫談談遊戲場景設計該有的狀態遊戲
- 實踐場景:解決Spark流處理產生的小檔案Spark
- ORACLE分散式事務鎖各種場景下的處理詳解Oracle分散式
- Android 中的轉場動畫及相容處理Android動畫
- 遊戲場景構成研究:圖形構成遊戲
- 如何完美地處理JavaScript渲染頁面中的非同步載入?JavaScript非同步
- 表單請求 統一欄位不同場景不同處理
- 後臺處理
- 遊戲設計之-排行榜處理遊戲設計
- SAP MM 進口採購業務中供應商多送或者少送場景的處理
- 遊戲基礎知識——碼頭場景的設計手法遊戲
- 遊戲基礎知識——“中心城市”場景的設計遊戲
- 遊戲基礎知識——“隱居住所”場景的設計遊戲