一、AMD FSR簡介
AMD FidelityFX Super Resolution,簡稱FSR,中文名稱是“AMD超級分辨率銳畫技術”。就是使用超分辨率技術實作高分辨率,高品質遊戲畫面,并顯著提高遊戲運作效率的一套實作方法和程式庫。它免費開源(https://github.com/GPUOpen-Effects/FidelityFX-FSR),跨平台,針對硬體進行了優化,內建到程式也非常簡單(實際上隻有兩個檔案),最神奇的是,運作它并不需要特殊的硬體,甚至如前幾代的Intel CPU内內建的核顯,都可以使用該技術。如果現在你在開發一個新的次時代畫面遊戲,真是沒有理由不使用它。
如果要用一句話說明白FSR是如何工作的,總結了如下公式:
是的,就是如此簡單。在放大的步驟,FSR實際上通過算法實作了對圖形邊緣的檢測,在放大圖像的同時盡量保持原有的邊緣,進而實作超分辨率。往往超分辨率算法會産生類似模糊或虛影的錯誤,FSR使用了一個銳化步驟來消除超分辨率的這個副作用。對AMD技術熟悉的朋友大概還記得AMD曾經推出過一個銳化技術:AMD FidelityFX Contrast Adaptive Sharpening, 簡稱CAS,沒錯,這裡複用了該技術,并對其進行了針對性優化,使得最終的圖像品質得以保證
而FSR在渲染流水線中的位置,大概位于實際渲染完成以後,部分後處理(Post-processing)之前,你可以把FSR看做是後處理的一部分,隻不過要十分小心FSR在後處理棧中所處的位置,避免一些後處理導緻FSR處理錯誤,影響最終的圖像品質。
比如膠片噪點(Film grain)效果,在很多追求電影式畫面的遊戲中廣泛使用,該效果産生的噪點就會影響FSR的發揮,(FSR會加強這些噪點的存在感)使得最終畫面出現錯誤。
二、超分辨率的算法
超分辨率算法大緻可以看做是一種插值算法,簡單來說就是“無中生有”的算法。提高圖像的分辨率意味着要向其中添加像素,那麼新添加的像素是什麼顔色呢?最簡單最自然的想法,就是在已知的像素中間插值。
目前的超分辨率算法可以大緻分為兩種:基于數值插值的方法,和基于AI的方法。許多算法可能要求使用多幀的資料,為簡單起見,我們在這裡僅讨論使用單幀圖像的情況。
2.1 最近點采樣(Nearest neighbor sample)和雙線性插值(Bilinear interpolation)
這兩種方法通常用在普通的貼圖過濾采樣中,被叫做最近點過濾和雙線性過濾。所謂的過濾,是指模型渲染到螢幕上時可能被縮放,是以螢幕上的一個點可能對應貼圖上的多個像素,也可能是螢幕上的多個點隻能對應貼圖上一個點。也就是說這兩種方法不但可以處理圖像放大,還可以在圖像縮小的時候發揮作用。
現如今很少使用這兩種方法來做超分辨率了,但是這兩種方法是硬體能夠直接支援的方法,也是其他數值方法的基礎,是以在這裡做一下簡單介紹。
最近點采樣方法十分直接,插值出的像素,直接複用它距離它最近像素的顔色。缺點也很明顯,圖形邊緣會出現鋸齒狀走樣,俗稱“狗牙”。
雙線性插值更進一步,使用周圍4個像素來計算一個“平均顔色”,可以有效減少鋸齒,但缺點同樣明顯,會使得圖形邊緣變得模糊不清。
2.2 兩次立方插值(Bicubic interpolation)
兩次立方方法廣泛應用在各種圖像算法中,兩次立方插值實際上是采樣了更多周圍像素的結果,通過對周圍像素使用不同的權重,得到的“平均顔色”更加接近應有的結果。
在很多照片處理軟體,數位相機以及手機攝像元件中,對數字圖像的旋轉縮放等處理都使用了該方法。該方法的缺點是要采樣更多的像素,運算量比較大,通常不會用在需要實時處理的領域。
2.3 Lanczos插值(Lanczos interpolation)
Lanczos方法也是應用廣泛的數值方法,在實體、機械、工程、圖像各種領域都有應用。FSR正是使用了Lanczos的簡化方法來進行插值,具體的應用方法詳見下文。
Lanczos插值的結果與兩次立方插值是接近的,但是由于其可以用較少的采樣點達到近似的效果,是以在實時程式中應用更廣泛一些。
下圖對比了各種算法在平面直角坐标系下的差別。
2.4 基于人工神經網絡的方法
基于AI深度學習的超分辨率是如今的大熱門話題,通過選擇合适的神經網絡模型和大量的訓練,深度學習方法可以産生非常準确的圖像,尤其是在分辨率放大倍數特别大的情況下要明顯優于純數值方法。除去超分辨率之外,基于AI深度學習的方法還可以提供包括降噪和銳化等額外的效果,而數值方法則需要更多處理步驟來實作。NVIDIA的DLSS就是屬于這一類方法。
NVIDIA DLSS(深度學習超采樣Deep Learning Super Sampling)是NVIDIA為其RTX系列顯示卡開發的一個程式庫,可以将低分辨率的遊戲畫面轉換為高分辨率,使用時下流行的AI計算方法去除錯誤增加品質,實際上也是跟FSR一樣的功效:既可以超分辨率處理,也可以負擔抗鋸齒的任務,DLSS具體的模型算法目前并未公開。
神經網絡計算雖然看起來高大上,但需要大規模的并行計算能力,在超分辨率實時應用中往往會擠占本來就非常緊張的計算資源。比如NVIDIA的DLSS就是利用圖形渲染時可能會閑置的AI計算核心(Tensor Core)來計算超分辨率。而且NVIDIA隻允許擁有TensorCore的RTX系列開啟DLSS功能,因為直接使用通用計算單元的話可能就會适得其反,不但效率得不到提升,反而會拖慢整個渲染。這也是為什麼10系的N卡無法使用DLSS。
神經網絡計算的另一個弊端就是前期需要大量的資料進行訓練。 比如DLSS原始版本和DLSS 2.0就需要使用NVIDIA NGX超級計算機預先進行訓練,并且需要開發團隊提供數千幀超大分辨率的遊戲畫面作為訓練資料輸入。DLSS 2.1及以後的版本神經網絡已經穩定,所有遊戲就統一使用同樣的模型進行計算了。
除以上所列出的以外,還有很多其他算法,比如基于時序的數值算法(用于視訊處理),棋盤格渲染(SONY PlayStation4曾經有使用過)等等,限于篇幅此處不作展開。
三、FSR的原理
上文提到一個公式,FSR= 放大+銳化,實際上FSR由兩個分别負責放大和銳化的元件組成:
- 邊緣自适應空間上采樣EASU(Edge Adaptive Spatial Upsampling)
- 穩定對比度自适應銳化RCAS(Robust Contrast Adaptive Sharpening)
3.1 EASU的工作原理
EASU是超分辨率的核心。EASU通過優化的采樣政策從原始圖像上取得附近的像素,對其進行插值計算得到目标像素。
其算法有可以大緻分為三個階段:
-
一階段:像素采樣
它使用一個圓形的采樣區域來盡量減少采樣的像素,通過特别計算的采樣點,直接利用硬體支援的雙線性采樣函數進行采樣,最大限度降低采樣次數。
-
二階段:插值計算
這部分是整個算法中最複雜的一部分,首先是積累計算線性插值的方向和長度,然後在所有方向上計算Lanczos插值。此處FSR對Laczos-2算法進行了數值近似,去掉了原有的三角函數和開方運算以提高效率。
-
三階段:限制輸出
由于Lanczos-2函數會産生值小于0的部分(見圖 各種算法的核函數),在某些情況下回出現一個環形的失真,是以在得到最終結果後,将結果限制在臨近4個像素的最大和最小值之間。
另外,對于支援16bit半精度計算的硬體,FSR将使用打包的16bit模式(Packed 16bit),可以使得2個16bit資料并行計算以提高性能;對于不支援的硬體,将回退到32bit模式,這将造成一定的性能損失。
3.2 RCAS的工作原理
AMD的FidelityFX CAS技術,是使用像素點附近的局部對比度(Local Contrast)對顔色進行調整,以消除因為抗鋸齒,圖像拉伸等操作造成的細節模糊。
RCAS在此基礎上進行了進一步的優化,去掉了CAS對圖像拉伸的支援(該功能已經由EASU實作了),并且直接使用最大局部銳度進行解算。
由于FSR對局部變化比較大(高頻)的區域敏感,是以在FSR處理之前圖像不可以有任何添加噪點的後處理操作,如果有必要還應添加抗鋸齒(反走樣)流程。此外FSA還提供了一些額外的功能,如下:
- 線性膠片顆粒 LFGA (Linear Film Grain Applicator) 用于在縮放圖像後添加膠片顆粒的算法
- 簡單可逆調和映射 SRTM (Simple Reversible Tone-Mapper)線性的動态高範圍(HDR)可逆映射器
- 時序能量儲存抖動 TEPD (Temporal Energy Preserving Dither) 一個将圖像從線性空間轉到Gamma 2.0空間的映射器
四、FSR的實作
4.1 EASU (Edge Adaptive Spatial Upsampling)
在這一步的進行中,将低分辨率的紋理通過自适應上采樣的方式放大到目标分辨率。
4.1.1 上采樣(upsampling),下采樣(downsampling)
上采樣和下采樣都是對紋理進行重新采樣(Resampling)的兩種方式
從藍色到1個點是下采樣,從一個點到紅色是上采樣
- 上采樣:就是把原來的紋理放大,然後空的部分通過比如線性插值之類的進行填充。
- 下采樣:就是把原來的紋理縮小,縮小的方式很多比如mipmap就是把四個像素取平均值算做一個。
4.1.2 邊緣與非邊緣的上采樣
當采用EASU上采樣将紋理放大之後,空出來的像素點需要填充,此時就有兩種情況:
-
非邊緣: 如果是非邊緣的情況,對于采樣的像素點 P 周圍像素的灰階值應該與像素點 P 非常接近,那麼像素點 P
隻需要進行權重平均即可:
放大之後的紋理上的像素點 P 對應原始紋理的像素點為 Q , Qi 為像素點 Q 周圍的像素點, f(x) 為采樣點 x 的灰階值,wi 為權重(為正)。
任何顔色都有紅、綠、藍三原色組成,假如原來某點的顔色為RGB (R, G, B),那麼,我們可以通過下面幾種方法,将其轉換為灰階:
1.浮點算法: Gray = R*0.3+G*0.59+B*0.11
2.整數方法: Gray = (R*30+G*59+B*11)/100
3.移位方法: Gray = (R*28+G*151+B*77)>>8
4.平均值法: Gray = (R+G+B) /3
5.僅取綠色: Gray = G;
通過上述任一種方法求得Gray後,将原來的RGB (R, G, B)中的R, G, B統一用Gray替換,形新的顔色RGB (Gray, Gray, Gray),用它替換原來的RGB (R, G, B)就是灰階圖了。
- 邊緣: 此時如果采樣的像素點 P 應該為邊緣,再按照式非邊緣處理那勢必會把邊緣處理的很模糊。根據邊緣銳化的思路,對邊緣進行上采樣為:
其中 F(Q) 為一個高頻濾波器, λ 用來縮放 F(Q) 。
舉個例子,拉普拉斯算子就是一個常用的高頻濾波器:
如上圖所示,采用拉普拉斯算子得到的值就為:
如果 Qx,y 周圍的像素灰階值變化越小(低頻)則 F(Q) 越小,灰階值變化越大(高頻)則 F(Q) 越大。
所謂高頻濾波,關鍵是求某一像素點與周圍像素點的內插補點,本質上還是權重法,隻不過這裡的權重有為負(為負是為了計算內插補點)。
那既然對于邊緣也可以用權重來做上采樣,EASU是以提出了一種統一的表達式:
其中 H(Qi) 為權重計算公式,并且應該滿足,當 Q 點不為邊緣的時候應該為正;當 Q 點為邊緣的時候應該包括為負的權重用于計算高頻濾波器,是以最關鍵的就是針對是否為邊緣确定負權重。
當然可以直接采用公式 f ( P ) =f(Q) + λF(Q) 的方式來上采樣,但是如此就做不到針對每個像素做出不同的上采樣政策,很容易導緻圖檔噪點。
4.1.3 Lanczos2 函數
是以EASU引入了Lanczos函數:
當 α 取2的時候通常将其稱作Lanczos2 函數,EASU都是以Lanczos2 函數作為基礎處理的。先來看看Lanczos2 函數圖像:
Lanczos2的圖像看起來平平無奇,不過大家注意 x∈[0,1] 部分函數值大于0, x∈[1,2] 部分,函數值小于0.
EASU提出可以通過多項式來拟合下公式
同時映入一個變量來控制 w 的函數值:
其中 w 是用于控制函數值的變量。再來看看函數圖像:
是不是感覺有點希望了! 區間 x∈[1,2] 的負半軸明顯發生變化,那接下來還有個問題就是如何确定某個像素是否為邊緣。
4.1.4 邊緣特征
以已經轉為灰階值的下圖為例:
邊緣大概分為上面三種,但EASU主要解決第1,2種邊緣類型。是以特征越像第1,2種類型則 w 越小,同時對于像素點 Q 隻計算上下左右方向的像素點,如圖所示。
EASU定義的邊緣特征 F 計算公式為:
注意,上面的計算都需要先轉為灰階值,并且 FX,FY∈[0,1] 。需要注意的是由于是直接加上XY方向是以 F∈[0,2] ,是以EASU提出将其映射到 [0,1] :
如此一來就針對第1,2種邊緣類型計算出了邊緣特征 Feature (根據本小節上述兩公式,越趨近1、2種邊緣類型, Feature 越大)。
那麼接下來的問題就是如何建立邊緣特征 Feature 與 w 的聯系?
4.1.5 邊緣特征 Feature 與 變量 w
還是從拟合的公式函數 *** L(x),x∈[-2,2]*** 入手(由于 *** L(x)*** 關于 *** y*** 軸對稱,下面的分析隻針對正半軸):
如上圖所示,在正半軸 *** L(x)*** 的根:
情況一:
分析可知:
是以,隻需要改變 1/√w 的大小(當然必須屬于 [1,2] 區間),就可以很容易操控區間 [1,1/√w] 負值大小(用來作為上采樣統一表達式的負權重);
是以
可以解得:
根據上述分析确定了 w 與區間 [1,1/√w] 中負值(負權重)的關系。是以可以很容易的建立一個 邊緣特征 Feature 與 變量 w 的線性關系( Feature=1 時***w=1/4*** , Feature=0 時***w=1*** ):
但在實際使用過程中由于 1/√w 趨于1時負權重不夠,進而導緻部分區域邊緣資訊識别不夠。是以EASU 限定 1/√w 範圍為 [√2,2] ,分析同上可以解出 *** w∈[1/4,1/2]*** ,同時可以得出一個新的邊緣特征 Feature 與 變量 w 的線性關系:
至此就建立了邊緣資訊與Lanczos2 函數的關系。:對于區間 [1,1/√w] ,對于區間 [1/√w,2] 直接裁剪,即:
4.1.6 雙線性插值
如果對于像素 Q 單單隻計算上下左右四個像素可能會導緻畢竟大的偏差,是以為了更加準确EASU還提出了一種雙線性插值。如上圖所示,對于像素 Q 采樣他周圍12個像素,然後按照上面不同顔分成四組,根據公式計算出四組 w 然後采用雙線性插值。接下來的問題就是如何計算雙線性插值的權重。
Q 點通常不是整數, O=floor(Q) 。假設 Q 點在以 O 點為原點的坐标系中坐标為 [公式] ,則雙線性插值之後的結果為:
4.1.7 旋轉和縮放
為了更加适宜各種角度的邊緣,EASU還提出可以進行旋轉。首先計算出 xy 的梯度:
注意:梯度的計算仍然需要采用4.1.6提出的雙線性插值計算。
對于梯度向量 Dir=(Dx,Dy) 的意義就在于,沿着該向量方向是灰階值變換最快的方向,也就是說邊緣垂直于梯度向量 Dir 。是以可以直接采用梯度旋轉采樣點 (x,y) ,首先将 Dir 進行歸一化(需要注意***Dx=Dy=0*** 的情況),此時 Dir=(cosθ,sinθ) ,旋轉之後的坐标為:
為了減少鋸齒,EASU還提出可以根據梯度和邊緣資訊進行縮放,EASU定義的縮放比例為(注意,這裡是直接定義的公式并沒有數學邏輯):
至此,EASU結束,成功的對圖像進行了自适應上采樣!!!
4.1.8 計算着色器代碼
#version 430 core
#pragma optionNV (unroll all)
#define LOCAL_GROUP_SIZE 32
layout(local_size_x = LOCAL_GROUP_SIZE, local_size_y = LOCAL_GROUP_SIZE) in;
layout(rgba32f, binding = 0) uniform writeonly image2D u_OutputEASUTexture;
uniform sampler2D u_InputTexture;
uniform int u_DisplayWidth;
uniform int u_DisplayHeight;
uniform vec4 u_Con0;
uniform vec4 u_Con1;
uniform vec4 u_Con2;
uniform vec4 u_Con3;
vec4 fsrEasuRF(vec2 p) { vec4 res = vec4(textureGather(u_InputTexture, p, 0)); return res; }
vec4 fsrEasuGF(vec2 p) { vec4 res = vec4(textureGather(u_InputTexture, p, 1)); return res; }
vec4 fsrEasuBF(vec2 p) { vec4 res = vec4(textureGather(u_InputTexture, p, 2)); return res; }
vec3 min3F3(vec3 x, vec3 y, vec3 z) { return min(x, min(y, z)); }
vec3 max3F3(vec3 x, vec3 y, vec3 z) { return max(x, max(y, z)); }
void fsrEasuTapF(
inout vec3 aC, // Accumulated color, with negative lobe.
inout float aW, // Accumulated weight.
vec2 Off, // Pixel offset from resolve position to tap.
vec2 Dir, // Gradient direction.
vec2 Len, // Length.
float w, // Negative lobe strength.
float Clp, // Clipping point.
vec3 Color) { // Tap color.
//公式15
vec2 v;
v.x = (Off.x * (Dir.x)) + (Off.y * Dir.y);
v.y = (Off.x * (-Dir.y)) + (Off.y * Dir.x);
//公式16
v *= Len;
float x2 = v.x * v.x + v.y * v.y;
//根據w裁剪
x2 = min(x2, Clp);
//公式6
// (25/16 * (2/5 * x^2 - 1)^2 - (25/16 - 1)) * (1/4 * x^2 - 1)^2
// |_______________________________________| |_______________|
// base window
float WindowB = float(2.0 / 5.0) * x2 + float(-1.0);
float WindowA = w * x2 + float(-1.0);
WindowB *= WindowB;
WindowA *= WindowA;
WindowB = float(25.0 / 16.0) * WindowB + float(-(25.0 / 16.0 - 1.0));
float Window = (WindowB * WindowA);
aC += Color * Window; aW += Window;
}
//根據公式7計算Feature,根據公式14計算梯度Dir
void fsrEasuSetF(
inout vec2 Dir,
inout float Feature,
vec2 P,
bool BoolS, bool BoolT, bool BoolU, bool BoolV,
float LumaA, float LumaB, float LumaC, float LumaD, float LumaE) {
// s t
// u v
float Weight = 0.0f;
if (BoolS) Weight = (1.0f - P.x) * (1.0f - P.y);
if (BoolT) Weight = P.x * (1.0f - P.y);
if (BoolU) Weight = (1.0f - P.x) * P.y;
if (BoolV) Weight = P.x * P.y;
float DC = LumaD - LumaC;
float CB = LumaC - LumaB;
float FeatureX = max(abs(DC), abs(CB));
float DirX = LumaD - LumaB;
Dir.x += DirX * Weight;
FeatureX = clamp(abs(DirX) / FeatureX, 0.0f, 1.0f);
FeatureX *= FeatureX;
Feature += FeatureX * Weight;
// Repeat for the y axis.
float EC = LumaE - LumaC;
float CA = LumaC - LumaA;
float FeatureY = max(abs(EC), abs(CA));
float DirY = LumaE - LumaA;
Dir.y += DirY * Weight;
FeatureY = clamp(abs(DirY) / FeatureY, 0.0f, 1.0f);
FeatureY *= FeatureY;
Feature += FeatureY * Weight;
}
vec3 fsrEasuF(ivec2 ip)
{
// +---+---+
// | | |
// +--(0)--+
// | b | c |
// +---F---+---+---+
// | e | f | g | h |
// +--(1)--+--(2)--+
// | i | j | k | l |
// +---+---+---+---+
// | n | o |
// +--(3)--+
// | | |
// +---+---+
//------------------------------------------------------------------------------------------------------------------------------
vec2 P = vec2(ip) * u_Con0.xy + u_Con0.zw;
vec2 F = floor(P);
P -= F;
vec2 P0 = F * u_Con1.xy + u_Con1.zw;
vec2 P1 = P0 + u_Con2.xy;
vec2 P2 = P0 + u_Con2.zw;
vec2 P3 = P0 + u_Con3.xy;
vec4 bczzR = fsrEasuRF(P0);
vec4 bczzG = fsrEasuGF(P0);
vec4 bczzB = fsrEasuBF(P0);
vec4 ijfeR = fsrEasuRF(P1);
vec4 ijfeG = fsrEasuGF(P1);
vec4 ijfeB = fsrEasuBF(P1);
vec4 klhgR = fsrEasuRF(P2);
vec4 klhgG = fsrEasuGF(P2);
vec4 klhgB = fsrEasuBF(P2);
vec4 zzonR = fsrEasuRF(P3);
vec4 zzonG = fsrEasuGF(P3);
vec4 zzonB = fsrEasuBF(P3);
//------------------------------------------------------------------------------------------------------------------------------
vec4 bczzL = bczzB * 0.5f + (bczzR * 0.5f + bczzG);
vec4 ijfeL = ijfeB * 0.5f + (ijfeR * 0.5f + ijfeG);
vec4 klhgL = klhgB * 0.5f + (klhgR * 0.5f + klhgG);
vec4 zzonL = zzonB * 0.5f + (zzonR * 0.5f + zzonG);
// Rename.
float bL = bczzL.x;
float cL = bczzL.y;
float iL = ijfeL.x;
float jL = ijfeL.y;
float fL = ijfeL.z;
float eL = ijfeL.w;
float kL = klhgL.x;
float lL = klhgL.y;
float hL = klhgL.z;
float gL = klhgL.w;
float oL = zzonL.z;
float nL = zzonL.w;
vec2 Dir = vec2(0.0);
float Feature = 0.0;
// 雙線性插值
fsrEasuSetF(Dir, Feature, P, true, false, false, false, bL, eL, fL, gL, jL);
fsrEasuSetF(Dir, Feature, P, false, true, false, false, cL, fL, gL, hL, kL);
fsrEasuSetF(Dir, Feature, P, false, false, true, false, fL, iL, jL, kL, nL);
fsrEasuSetF(Dir, Feature, P, false, false, false, true, gL, jL, kL, lL, oL);
//------------------------------------------------------------------------------------------------------------------------------
//
{//歸一化梯度向量Dir
vec2 Dir2 = Dir * Dir;
float DirR = Dir2.x + Dir2.y;
bool Zero = DirR < (1.0 / 32768.0);
DirR = 1.0f / sqrt(DirR);
DirR = Zero ? 1.0f : DirR;
Dir.x = Zero ? 1.0f : Dir.x;
Dir *= vec2(DirR);
}
{//公式8
Feature = Feature * 0.5f;
Feature *= Feature;
}
//公式16
float Stretch = (1.0f / (max(abs(Dir.x), abs(Dir.y))));
vec2 Len = vec2(1.0f + (Stretch - 1.0f) * Feature, 1.0f + -0.5f * Feature);
//公式11
float w = 0.5f - 0.25f * Feature;
//公式12
float Clp = 1.0f / w;
vec3 Min4 = min(min3F3(vec3(ijfeR.z, ijfeG.z, ijfeB.z), vec3(klhgR.w, klhgG.w, klhgB.w), vec3(ijfeR.y, ijfeG.y, ijfeB.y)),
vec3(klhgR.x, klhgG.x, klhgB.x));
vec3 Max4 = max(max3F3(vec3(ijfeR.z, ijfeG.z, ijfeB.z), vec3(klhgR.w, klhgG.w, klhgB.w), vec3(ijfeR.y, ijfeG.y, ijfeB.y)),
vec3(klhgR.x, klhgG.x, klhgB.x));
// Accumulation.
vec3 aC = vec3(0.0);
float aW = (0.0);
fsrEasuTapF(aC, aW, vec2(0.0, -1.0) - P, Dir, Len, w, Clp, vec3(bczzR.x, bczzG.x, bczzB.x)); // b
fsrEasuTapF(aC, aW, vec2(1.0, -1.0) - P, Dir, Len, w, Clp, vec3(bczzR.y, bczzG.y, bczzB.y)); // c
fsrEasuTapF(aC, aW, vec2(-1.0, 1.0) - P, Dir, Len, w, Clp, vec3(ijfeR.x, ijfeG.x, ijfeB.x)); // i
fsrEasuTapF(aC, aW, vec2(0.0, 1.0) - P, Dir, Len, w, Clp, vec3(ijfeR.y, ijfeG.y, ijfeB.y)); // j
fsrEasuTapF(aC, aW, vec2(0.0, 0.0) - P, Dir, Len, w, Clp, vec3(ijfeR.z, ijfeG.z, ijfeB.z)); // f
fsrEasuTapF(aC, aW, vec2(-1.0, 0.0) - P, Dir, Len, w, Clp, vec3(ijfeR.w, ijfeG.w, ijfeB.w)); // e
fsrEasuTapF(aC, aW, vec2(1.0, 1.0) - P, Dir, Len, w, Clp, vec3(klhgR.x, klhgG.x, klhgB.x)); // k
fsrEasuTapF(aC, aW, vec2(2.0, 1.0) - P, Dir, Len, w, Clp, vec3(klhgR.y, klhgG.y, klhgB.y)); // l
fsrEasuTapF(aC, aW, vec2(2.0, 0.0) - P, Dir, Len, w, Clp, vec3(klhgR.z, klhgG.z, klhgB.z)); // h
fsrEasuTapF(aC, aW, vec2(1.0, 0.0) - P, Dir, Len, w, Clp, vec3(klhgR.w, klhgG.w, klhgB.w)); // g
fsrEasuTapF(aC, aW, vec2(1.0, 2.0) - P, Dir, Len, w, Clp, vec3(zzonR.z, zzonG.z, zzonB.z)); // o
fsrEasuTapF(aC, aW, vec2(0.0, 2.0) - P, Dir, Len, w, Clp, vec3(zzonR.w, zzonG.w, zzonB.w)); // n
//------------------------------------------------------------------------------------------------------------------------------
// Normalize and dering.
return min(Max4, max(Min4, aC / aW));
}
void main()
{
ivec2 FragPos = ivec2(gl_GlobalInvocationID.xy);
imageStore(u_OutputEASUTexture, FragPos, vec4(fsrEasuF(FragPos), 1));
}
4.2 RCAS (Robust Contrast Adaptive Sharpening)
在成功用EASU進行上采樣之後,FSR算法還進行了自适應銳化進一步強化邊緣資訊,不過相比于EASU,RCAS就十分的簡單了。
前面我們在介紹過拉普拉斯算子,RCAS就是一個拉普拉斯算子的變種,其模闆為:
對于像素 Q 也隻需要按照上面的權重計算即可:
RCAS的做法是根據像素周圍的對比度來計算 w
如上圖所示,首先計算出最大值 Max ,以及最小值 Min :
其中 Scale 為上采樣之後的分辨率與原分辨率的比值。
4.2.1 計算着色器代碼
#version 430 core
#pragma optionNV (unroll all)
#define LOCAL_GROUP_SIZE 32
// This is set at the limit of providing unnatural results for sharpening.
#define FSR_RCAS_LIMIT (0.25 - (1.0 / 16.0))
layout (local_size_x = LOCAL_GROUP_SIZE, local_size_y = LOCAL_GROUP_SIZE) in;
layout (rgba32f, binding = 1) uniform writeonly image2D u_OutputRCASImage;
uniform sampler2D u_RASUTexture;
uniform vec4 u_Con0;
vec4 fsrRcasLoadF(ivec2 p) { return texelFetch(u_RASUTexture, ivec2(p), 0); }
float max3F1(float x,float y,float z){return max(x,max(y,z));}
float min3F1(float x,float y,float z){return min(x,min(y,z));}
void fsrRcasF(out float pixR,out float pixG,out float pixB,ivec2 ip) { // Constant generated by RcasSetup().
// Algorithm uses minimal 3x3 pixel neighborhood.
// b
// d e f
// h
ivec2 P = ivec2(ip);
vec3 b = fsrRcasLoadF(P + ivec2(0, -1)).rgb;
vec3 d = fsrRcasLoadF(P + ivec2(-1, 0)).rgb;
vec3 e = fsrRcasLoadF(P).rgb;
vec3 f = fsrRcasLoadF(P + ivec2(1, 0)).rgb;
vec3 h = fsrRcasLoadF(P + ivec2(0, 1)).rgb;
// Rename (32-bit) or regroup (16-bit).
float bR = b.r;
float bG = b.g;
float bB = b.b;
float dR = d.r;
float dG = d.g;
float dB = d.b;
float eR = e.r;
float eG = e.g;
float eB = e.b;
float fR = f.r;
float fG = f.g;
float fB = f.b;
float hR = h.r;
float hG = h.g;
float hB = h.b;
// Luma times 2.
float bL = bB * 0.5f + (bR * 0.5f + bG);
float dL = dB * 0.5f + (dR * 0.5f + dG);
float eL = eB * 0.5f + (eR * 0.5f + eG);
float fL = fB * 0.5f + (fR * 0.5f + fG);
float hL = hB * 0.5f + (hR * 0.5f + hG);
// Min and max of ring.
float Min4R = min(min3F1(bR, dR, fR), hR);
float Min4G = min(min3F1(bG, dG, fG), hG);
float Min4B = min(min3F1(bB, dB, fB), hB);
float Max4R = max(max3F1(bR, dR, fR), hR);
float Max4G = max(max3F1(bG, dG, fG), hG);
float Max4B = max(max3F1(bB, dB, fB), hB);
// Immediate constants for peak range.
vec2 PeakC = vec2(1.0, -1.0 * 4.0);
// Limiters, these need to be high precision RCPs.
float HitMinR = Min4R * (1.0f / (4.0f * Max4R));
float HitMinG = Min4G * (1.0f / (4.0f * Max4G));
float HitMinB = Min4B * (1.0f / (4.0f * Max4B));
float HitMaxR = (PeakC.x - Max4R) * (1.0f / (4.0f * Min4R + PeakC.y));
float HitMaxG = (PeakC.x - Max4G) * (1.0f / (4.0f * Min4G + PeakC.y));
float HitMaxB = (PeakC.x - Max4B) * (1.0f / (4.0f * Min4B + PeakC.y));
float LobeR = max(-HitMinR, HitMaxR);
float LobeG = max(-HitMinG, HitMaxG);
float LobeB = max(-HitMinB, HitMaxB);
float Lobe = max(float(-FSR_RCAS_LIMIT), min(max3F1(LobeR, LobeG, LobeB), 0.0)) * (u_Con0.x);
// Resolve, which needs the medium precision rcp approximation to avoid visible tonality changes.
float RcpL = 1.0f / (4.0f * Lobe + 1.0f);
pixR = (Lobe * bR + Lobe * dR + Lobe * hR + Lobe * fR + eR) * RcpL;
pixG = (Lobe * bG + Lobe * dG + Lobe * hG + Lobe * fG + eG) * RcpL;
pixB = (Lobe * bB + Lobe * dB + Lobe * hB + Lobe * fB + eB) * RcpL;
}
void main()
{
ivec2 FragPos = ivec2(gl_GlobalInvocationID.xy);
vec3 Color = vec3(0);
fsrRcasF(Color.r,Color.g,Color.b,FragPos);
imageStore(u_OutputRCASImage, FragPos, vec4(Color, 1));
}
低分辨率:
FSR處理後(提升一些,但效果不太好,邊緣噪點也被放大,但不依賴于硬體,還行):