遊戲場景渲染中的PostProcessing(後處理)

遊資網發表於2020-03-13
PostProcessing是現代遊戲中必不可少的技術之一,本文簡單來總結下PostProcessing的實現原理和應用.因為詳細寫起來需要很大篇幅且很費時間,這裡只簡單介紹下原理.

1.基礎部分

PostProcessing,通常在普通的場景渲染結束後對結果進行處理,將一張或數張Texture處理得到一張新的Texture.

PostProcessing的渲染Pipeline普通的模型渲染一樣,不同之處在於在VertexShader中通常只是簡單的拷貝,主要的邏輯寫在Pixel Shader中.

拷貝圖片的圖元是什麼樣的呢?直覺來看就是一個長方形圖元,由兩個三角形組成.但是一般是通過一個大一點單個三角形圖元來實現的,超出長方形框外的部分在光柵化時會被自動過濾掉,這樣寫起來更簡潔,執行速度也稍快一些.

遊戲場景渲染中的PostProcessing(後處理)
一個三角形的渲染拷貝作用和一個長方形是一樣的

需要注意的是,OpenGL中Texture左下角UV點是(0,0),而DirectX中左上角UV是(0,0).

另外Direct3D 9中有Half-Pixel Offset的問題,取樣畫素時需要將uv點偏移半畫素或者計算頂點座標時時偏移半畫素(遊戲引擎裡一般是直接處理編譯好的shader,這樣不用為不同版本寫不同的shader),來看下微軟文件裡的幾張圖,就能直觀地知道Half-Pixel 是什麼問題:

遊戲場景渲染中的PostProcessing(後處理)
Direct3D 10 Pixel Coordinate

遊戲場景渲染中的PostProcessing(後處理)
Direct3D 9 Pixel Coordinate

遊戲場景渲染中的PostProcessing(後處理)
Direct3D Texel Coordinate

2.UV的使用

根據Texture中點UV的值,施加不同的效果,比如常見的Vignette效果,就是根據到螢幕中心點的距離,混合黑色的顏色,產生邊緣灰暗的效果,非常的簡單.

遊戲場景渲染中的PostProcessing(後處理)
Vignette效果

根據Texture中畫素點的uv值做偏移,可以得到一些變形,扭曲,波浪,鏡頭變形等效果.比如下面的一個波浪的效果,FS程式碼大致是這樣的:

遊戲場景渲染中的PostProcessing(後處理)
波浪效果

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,長得是這樣的:

遊戲場景渲染中的PostProcessing(後處理)
Perlin Noise Texture

比如用Perlin Noise Texture來生成一個Dissolve的效果,就是指定一個閾值,動態clip掉Perlin Noise Texture中相應點的灰度值在閾值下的點:

遊戲場景渲染中的PostProcessing(後處理)
Dissolve

隨時間改變這個閾值,就可以得到逐漸出現或者消失的Dissolve效果.

還有的Texture儲存一些(u,v)到某些值的對映,叫做Loopup Texture(LUT).很多張Texture組成Array還可以完成(x,y,z)到值的對映,叫做3D LUT.

LUT最常見的應用就是用來做Color Grading.

遊戲場景渲染中的PostProcessing(後處理)
一個用來ColorGrading的LUT

4.Image Kernels

將在UV當前點周圍數點取樣,然後將多個點處理結果相加,這樣的處理寫成矩陣叫做Image Kernel,這樣的操作也可以叫做Convolution,比如一個邊緣檢測的Kernel是這樣的:

遊戲場景渲染中的PostProcessing(後處理)

將中心點的畫素值X8,然後減去周圍點的畫素值,就可以做邊緣檢測.

遊戲場景渲染中的PostProcessing(後處理)
Edge Detection

一些常見的如Sharpen, Blur等各種各樣的效果都是通過這種方式實現的.

5.Gaussian Blur

從訊號和取樣的角度來說,有這樣幾種Filtering:

遊戲場景渲染中的PostProcessing(後處理)
box filter,tent filter,sinc filter 所有filter的面積和是1

Sinc filtering計算起來最複雜,效果也是最好的.

Filtering來重構訊號的過程大致是這樣:

遊戲場景渲染中的PostProcessing(後處理)
sinc filter

Guassian Blur可以看作是Sinc Filter的一種近似,用這樣一個距離相關的Kernel 函式:

遊戲場景渲染中的PostProcessing(後處理)

實際操作時,忽略較遠處的值,用預計算好的權重值,比如一個5X5的Kernel:

遊戲場景渲染中的PostProcessing(後處理)
5X5 Guassion weight

這樣每次計算時需要取樣25個點才能得到最終的畫素值,因此可以將Guassion blur分成兩次Pass來實現,Kernel是這樣的:

遊戲場景渲染中的PostProcessing(後處理)
兩次Pass的Guassion weight

這樣每個點需要的取樣次數就降到了10次.Box-filter(周圍N*N個點權重相等)也是可以像這樣分成兩次Pass的,這樣的Kernel是separable的.

當GPU從貼圖中取樣時,使用bilinear方式在某個點取樣時,是根據點的位置從目標點周圍4點中取值加權平均,利用這點,也可以減少需要的取樣次數.比如一個3X3的Box-filter時,所有點的權重相等,將取樣目標點放到4點中心,就可以得到周圍4點的平均值:

遊戲場景渲染中的PostProcessing(後處理)
9個取樣點降到5個和4個

Guassion blur也可以通過這中方法來減少取樣數,兩種方式結合起來,可以使取樣數達到最小.

6.DownSample/UpSample

一些需要Blur效果的PostProcessing通常將將TextureDownSample到一個較小的尺寸,自帶Blur的效果基本上不會影響最總的效果,較小的Texure還可以加快執行速度.

Bloom的效果,就是使較亮的部分更亮點,並且擴散到附近.就是通過一系列的DownSample和UpSample,最後疊加到原來圖片上來實現的.

遊戲場景渲染中的PostProcessing(後處理)
Bloom 4次DownSample

遊戲場景渲染中的PostProcessing(後處理)
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的效果.

遊戲場景渲染中的PostProcessing(後處理)

再看下Depth of Field(DOF 景深)的效果,DOF先設定一個對焦的距離和範圍,超出對焦範圍的部分做blur處理.一個簡單實現的DOF的大致過程如下:

a.將原圖DownSample到1/4大小.

b.確定模糊的強度,模糊強度隨距離變化的曲線大致如下圖所示,根據深度值計算每個點的模糊強度,儲存到一張Texture中.

遊戲場景渲染中的PostProcessing(後處理)
dof強度和距離的關係

c.對DownSample的Texture進行Blur,每個點使用上面的Texture中的模糊強度計算模糊半徑.用模糊半徑在一個預計算好的Disk中隨機分佈的點位置進行取樣,點的分佈大致是下面的圖片中所示.

遊戲場景渲染中的PostProcessing(後處理)

d.將得到的Texture用9-tap Tent Filter再次Blur.

e.將得到的Texture和原圖按照計算出的模糊強度混合,得到DOF處理後的圖片.

遊戲場景渲染中的PostProcessing(後處理)
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計算:

  1. [numthreads(HISTOGRAM_THREAD_X, HISTOGRAM_THREAD_Y, 1)]
  2. void KEyeHistogram(uint2 dispatchThreadId : SV_DispatchThreadID, uint2 groupThreadId : SV_GroupThreadID)
  3. {
  4.     const uint localThreadId = groupThreadId.y * HISTOGRAM_THREAD_X + groupThreadId.x;

  5.     // 使用其中部分Thread清零共享記憶體
  6.     if (localThreadId < HISTOGRAM_BINS)
  7.     {
  8.         gs_histogram[localThreadId] = 0u;
  9.     }

  10.     float2 ipos = float2(dispatchThreadId) * 2.0;

  11.     //Thread之間同步等待
  12.     GroupMemoryBarrierWithGroupSync();
  13.     if (ipos.x < _ScaleOffsetRes.z && ipos.y < _ScaleOffsetRes.w)
  14.     {
  15.         uint weight = 1u;
  16.         float2 sspos = ipos / _ScaleOffsetRes.zw;
  17.         // Vignette 權重,中心位置點的權重會更高
  18.         #if USE_VIGNETTE_WEIGHTING
  19.         {
  20.             float2 d = abs(sspos - (0.5).xx);
  21.             float vfactor = saturate(1.0 - dot(d, d));
  22.             vfactor *= vfactor;
  23.             weight = (uint)(64.0 * vfactor);
  24.         }
  25.         #endif

  26.         float3 color = _Source.SampleLevel(sampler_LinearClamp, sspos, 0.0).xyz; // Bilinear downsample 2x
  27.         //計算明亮度
  28.         float luminance = Luminance(color);
  29.         float logLuminance = GetHistogramBinFromLuminance(luminance, _ScaleOffsetRes.xy);
  30.         //根據明亮度計算得到需要累加到的bin位置
  31.         uint idx = (uint)(logLuminance * (HISTOGRAM_BINS - 1u));
  32.         //相應bin位置的計數原子累加
  33.         InterlockedAdd(gs_histogram[idx], weight);
  34.     }
  35.     //同步等待
  36.     GroupMemoryBarrierWithGroupSync();
  37.     //使用其中的部分Thread來將累計值從共享記憶體寫入到RWBuffer
  38.     if (localThreadId < HISTOGRAM_BINS)
  39.     {
  40.         InterlockedAdd(_HistogramBuffer[localThreadId], gs_histogram[localThreadId]);
  41.     }
複製程式碼


這樣我們就得到帶有明亮值分佈的Histogram,不僅可以用來計算平均明亮值,還可以根據分佈情況作進一步優化.

遊戲場景渲染中的PostProcessing(後處理)
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.

遊戲場景渲染中的PostProcessing(後處理)
SSAO Sample Kernel

c.將得到的AO Buffer進行Blur.

d.根據AO值修改Diffuse的光照值,產生AO的效果.

遊戲場景渲染中的PostProcessing(後處理)
SSAO的效果

SSR的原理和SSAO很相似,需要用到Normal Buffer和Metal Buffer(表示鏡面光反照率,一般Gbuffer中有),根據需要計算反射光照的點的Normal和Metal值,計算得到一條反射光線,沿著這條線進行RayMarch,得到第一個一個與該射線相交的點,將該點的顏色當作反射的顏色進行混合.

遊戲場景渲染中的PostProcessing(後處理)
步長不斷變化的RayMarching

遊戲場景渲染中的PostProcessing(後處理)
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

相關文章