從像素之間談起:像素游戲的畫面增強


轉自:http://fushigi-hako.site/2017/07/02/from_pixel_to_screen_1/

無所不在的像素畫

分類

隨着分辨率的普遍提高,我們已經告別了依賴於簡陋像素來表現游戲畫面的年代。但還是有不少人像我一樣沉迷於像素美術和游戲。如今到處可以都可以看到的各式像素作品,雖然大多被直接稱呼為像素畫,但實際上已經分化為很多分支,簡單的將其歸類為像素作品未免太含糊。在開始正文之前我先將他們粗粗的分個類。一些比較常見的代表如:

  1. 大顆粒像素,此類像素作品一般細節較少,人物符號化或者抽象化。同時還可能出現非像素元素,如光暈,漸變

  2.粒度較小的像素畫,主要還是色塊化,邊緣並沒有強化。

  3.強化邊緣和高光,細節豐富,但是普遍尺寸較小。

另外,在一些UI圖標的繪制過程中,由於圖標較小,也同樣采用像素點繪的方式。因為它平時也不會被稱為像素畫,所以這里也不討論。

其中第3種是我在本文中將着重討論的。
這類像素圖可能和平時所提到的像素圖差的最遠,因為它並不是為了做出像素化效果而誕生的。相反它是游戲機在分辨率和色板支持加強之后的產物(光是從GB到GBC,支持的色深就從2位變成了15位)。在這方面,任天堂算是是做到了極致(也可能因為任天堂的主機的屏幕天生小的緣故)這類像素畫在抗鋸齒(偽),光照,色彩的調和的方面很有特點(這篇文章中不細說)

再現像素畫

就GBA而言,分辨率為240 *160,但我們現在再制作像素的游戲時,玩家一定不會接受在這么小的一個屏幕上去玩游戲。一個是因為眼睛看的太累(長大后眼睛都變差了…)。另一方面,考慮到像素畫的成本,也不建議針對一個1080p的屏幕進行逐像素繪畫。為了滿足一些玩家想要的像素的效果,一個最簡單直接的方法就是將畫面放大。

雖然這種方法省時省力,但是也會帶來一個問題。在繪制像素畫中的曲線時,由於一般不對線條使用反走樣(會讓畫面變臟)來抗鋸齒。在分辨率較低的時候,像素的邊緣可以幫助人們識別且很難注意到異樣,但當畫面放大后,這些邊緣就會顯得粗糙不堪。這也是像素畫風被一些人所詬病的原因。

為此,包括ppsspp在內的模擬器中,會內置不少shader來對圖像進行后期處理。對於2D圖像來說,具體方法包括xBRZ等濾鏡來平滑放大圖像(xBRZ對2D像素放大會產生平滑而舒適的效果,但是這會損失像素的特征),增加crt, 掃描線等后期特效將像素畫做舊。當然,你也可以利用物理的手段將信號輸出到CRT屏幕上,參考這里
另外,這篇文章 中講述了一些crt效果的來源,也討論了很多細節問題。一個簡單的對比圖:

常見的效果如下

雖然實現方法不同,但總的來說都是在像素之間增加了隔斷,人們的大腦會趨向為這種斷裂解釋理由,自動為圖像進行內部平滑處理。這就和我們湊近屏幕看游戲畫面但是不會覺得畫面模糊的原因類似。另一方面,因為掃描線的存在,畫面的層次感也可以體現出來,使得畫面更加可信。甚至連Her Story中都為了劇情的需要用些crt效果。
這篇文章里介紹了大量的post processing shader,很有借鑒意義。

一個shader的實現思路

本文的后期特效將主要適用於前面所述的第三種情況,也即通過臨近采樣的方式放大圖像而達到加強像素化的目的。更多的模擬LCD屏幕而不是CRT屏幕,所以一些包括屏幕扭曲,通道分離的效果在本文中將不會涉及。本文會利用psp模擬器,將掃描線效果應用到Tactics Ogres(中文譯為:皇家騎士團)上。
我主要從兩方面完成對像素圖的畫面增強:1.利用微小的分割線來分隔開像素,讓人們產生像素相連的錯局。2.利用低通濾波器稍許的平滑像素邊界(但是不宜平滑太多,不然會失去像素風格的特點)

為了統一,后面的演示代碼都用CG來寫,輸入的紋理尺寸為512 x 384

格子的分割

硬分割

首先,將像素放大了2倍之后,實際看到的一個“像素 pixel”(叫紋素 texel更為貼切)是2 x 2個像素。雖然我們想營造出的效果是讓玩家覺得游戲的像素與像素之間產生了間距,但除了在原先的一個像素上通過勾畫邊緣來實現分割,我們並不能真的將像素之間創造出空格。這步操作之后,最小單位仍然是像素。下圖所示的分別是每2個像素進行一次分割和每4個像素進行一次分割的圖示。

每2格有一個明暗變化周期

每4格有一個明暗變化周期

對於后期特效來說,輸入的紋理為camera input,上圖是1 texel對應 4 pixel,而下面是1 texel對應 16 pixel。
為了找到分割的位置,需要能夠區分一個紋素所對應的像素。方法並不復雜, 若一個紋素拆分為4*4個像素,可以在頂點着色器上輸出如下vec2:

o.pixel_no = float2(o.uv.x * _MainTex_TexelSize.z, o.uv.y * _MainTex_TexelSize.w) * 0.25;

_MainTex_TexelSize 是內置uniform,記錄輸入紋理的相關信息,其中zw分量即為寬和高。對於ppsspp模擬器,可以通過 u_texelDelta 來計算屏幕的resolution,后面會提到。
有了pixel_no的信息,我們就可以在片段着色器里進行插值了:

fixed4 Pass_Scanline(float2 uv) {
        float column = 4;
        float2 pixel_no = 
            frac(float2(uv.x * 1024.0, uv.y * 768) * _ScreenScale / column);
        if(pixel_no.x < 1 / column || pixel_no.y < 1 / column)
            return PREVIOUS_PASS(uv) * 0.5;
        else
            return PREVIOUS_PASS(uv);
}

其中PREVIOUS_PASS是一個宏,用來嵌套偽multi-pass,這里的PREVIOUS_PASS可以簡單的理解為上一個獲取紋理的值的pass。這里當column為4的時候,一個紋素對應的四個像素的pixel_no的x分量分別為1/8, 3/8, 5/8, 7/8,我們可以利用這個信息來判斷究竟哪個像素是這個紋素的邊緣。
硬分割雖然完成了對像素的分割,但是效果比較生硬。玩家感受到的不是從屏幕上反映的圖像,而更像是罩上了網格的圖像。這也和asset store上的這個效果類似。

豐富分割細節

硬分割的效果不理想,於是很自然的想到為這個邊緣添加一些過渡效果是否會好一點呢?答案是肯定的。另外,為了能取得比較好的過渡效果,我們應該適當提高pixel對texel的比例,測試下來發現一般來說3比較合適,2的話太窄,而4的話,圖像放大的過大。
為了理解方便,我們將圖像的邊緣定義為暗,圖像的中央定義為亮,這樣明暗間隔就能產生所謂的掃面線。問題演變為在一個紋素所對應的所有像素中,如何找到一個亮與暗的分布,從而表現出一個熒光格子的效果
如果單純的亮度從中心開始,依照切比雪夫距離向邊緣遞減,效果其實不太理想,紋素與紋素之間割裂的依舊生硬

所以我們想找到一種方式柔滑這一過程,首先可以嘗試用高斯平滑來處理

卷積核
簡單的過渡不夠,所以需要找到一個卷積核(kernel)來將像素周圍的情況考慮進去,最常見的低通濾波器就是高斯濾波器(Gaussian Filter)但直接使用的話,會造成畫面均勻平滑。Themaister提供了一個很好的思路(雖然由於git目錄失效,原始的代碼已經不可考,但是我還是在網上找到了一個GLSL版本 ),效果如下圖所示:

他的思路簡單概述起來就是,一組像素(如4x4)向所在紋素的相鄰8個紋素取樣,權重為該像素到紋素距離倒數的負相關。本質上是一個非對稱的低通濾波器。它的優勢在於,針對每個紋素內的像素,所采樣的紋素是一致的(保留了像素的質感)而在紋素內部,利用非對稱的卷積核實現亮度的變化。

一個紋素被分為9個像素

取左上角的像素演示,紅色的線條的長度與權重成負關系

我們知道越靠近中間,加權值越高,對於一個靠左下角的像素來說,將其卷積核畫出來可能會像這樣:

權重為Exp(-2.05 * 平方歐氏距離)

權重為Exp(-2.05 * 平方歐氏距離)

之所以不選擇平方歐氏距離,是因為這會造成加權之后,中間亮度區分不開來,而周圍的亮度又太低,會有種硬分割的感覺。
在對周圍的采樣做了積分之后可以得到下圖。雖然和前面的圖很像,這張圖的意義和剛才的並不一樣,它代表的是一個紋素內的亮度分布(假設亮度的原始分布均勻)。

考慮到以上的操作局限在一個很小的范圍內,所以我們可以將其離散化后觀察

頂部看更直觀

一些細節

濾波器的構成

Themaister的方法中,考慮了亮度對像素最終顏色的影響,這個濾波器由兩個函數構成,一個是空間域上的濾波器系數,另一個是值域(亮度)上的系數。如果采樣點上的亮度越亮,意味着它將會更多的侵蝕着其他的像素。有關Glow效果,可以參考這篇文章

float color_bloom(float3 color)
{
    // const float3 gray_coeff = float3(0.30, 0.59, 0.11);
    const float shine = 0.25;
    float bright = Luminance(color);//dot(color, gray_coeff);
    return lerp(1.0 + shine * 0.5, 1.0 - shine, bright);
}

這里我們除了可以自己定義gray_coeff以外,我們也可以使用unity中的內置函數,它對應的 gray_coeff為fixed3(0.22, 0.707, 0.071)
另外,通過在lerp的時候增加一個系數,我將暗部的亮度稍微提高了下,彌補曝光不足的情況。

No.的偏移

剛才的卷積核只是一個理想狀態的演示,實際上,由於任意兩個紋素是相鄰的,所以只能在一個紋素的兩邊(看成一個正方形)上進行邊的繪制。否則,兩個相鄰紋素在交界處都繪上黑邊會導致掃描線過粗。另外,如果直接采樣,將會出現平頂的情況,也即是當邊上為偶數個像素的時候,中間會出現高度一樣的狀況。於是需要對之前的pixel_no進行偏移,偏移之后將會打破原有的平衡,找到一個新的中心。這里的偏移值應該小於1/(column * 2),否則循環周期將會出問題。

float delta = dist(frac(pixel_no + float2(-0.125, 0.125)), offset + float2(0.5, 0.5));

通過對比可以看出,偏移之后,左側和上側的亮度明顯變暗,亮度會表現的更集中在中間的一個點。

圖為不同顆粒下的表現

采樣的偏移

為了給物體增加一些投影,特別是文字,會對當前像素點的周圍采樣。我們並不是直接用相鄰像素采樣(相鄰像素很有可能來自於同一紋素,所以采樣沒有意義),而是偏移一段距離,這和ps中的投影是一個原理。只是這里需要特別注意一個問題,也即是之前看到的一張圖中出現的黑邊問題。

注意人物輪廓周圍的黑邊

這個問題的起因是:如果采樣點之間始終距離為一個紋素的時,雖然能保證取到的都是周圍的紋素,但當圖像中文本的邊界正好是處於格子的邊緣(也就是亮度最低的位置)在經歷一個周期后,亮度是最低的地方(周期性所致)就會對之前還在暗色邊界范圍內的像素采樣,這樣就會出現在一個白的背景上出現了一條黑邊。
解決方法就是將采樣偏移限制在紋素所包含的像素個數之內,雖然這意味着我們的投影無法超過一個紋素,但是起碼會避免一些比較糟糕的情況。

歐氏距離與曼哈頓距離的選擇

前面在談到權重的時候,我們的圖示標注出來的是歐幾里得距離,那么如果為了將指令減少幾條,變成曼哈頓距離如何呢?結果是:並不好

可以看出,形成了一個明顯的十字亮斑,並且高度差異並區分度不高

另外值得一提的是,由於編譯器和顯卡的優化,使用曼哈頓距離並不能節省什么開銷。

增加bloom

Bloom能起到加光暈的效果,能進一步降低粗糙感。通常來說,bloom只是作為HDR的一環,過程還可以包括Tone Mapping、Bright Pass Filter以及Blur。但由於我們這里只考慮2D的情況,更多時候HDR可以由美術手工實現,所以我們先不討論ToneMapping而簡單實現Bright和Blur。

1 混合橫向的bloom和縱向的bloom
比較常見的bloom中的blur過程分為兩次,一次橫向像素上的模糊,一次縱向像素上的模糊,兩次疊加。但是我們為了省力,也可以在一個pass中進行,畢竟我們只是為了虛化邊緣,制造投影的效果。

fixed4 Pass_SimpleBloom(float2 uv)
{
    float4 sum = float4(0, 0, 0, 0);
    float4 bum = float4(0, 0, 0, 0);
    
    float2 glareSize = float2(1.0 / 512, 1.0 / 384) * 0.65;
    int height = 3;
    int width = 1;
    for(int i = -width; i < width; i++)
    {
        for(int j = -height; j < height; ++j)
        {
            sum += tex2D(_MainTex, uv + float2(i, j) * glareSize);
            bum += tex2D(_MainTex, uv + float2(j, i) * glareSize);
        }
    }
    fixed4 color = PREVIOUS_PASS(uv);
    color = (sum*sum*0.001 + bum*bum*0.0080) * _Amount / ((2* height +1) *(2* width +1)) + color*_Power;
    return color;
}

renderTexture與multipass

Bloom的操作我並沒有在ppsspp模擬器中實施,主要原因是我不知道如何在ppsspp中實現真正的multi-pass shader,如果只是通過宏將pass折疊起來,由於bloom需要對周圍采樣,將會導致計算量指數式上漲。
但是這一切在unity中就很容易解決了,只需要在第一遍的pass中將bloom后的輸出輸出到render texture就可以被后面的shader所利用,兩者加起來的時間測試下來大概只有single-pass的1/5,優化效果還是非常明顯的。

RenderTexture rtTemp = RenderTexture.GetTemporary(src.width, src.height);
Graphics.Blit(src, rtTemp, _Material_1);
Graphics.Blit(rtTemp, dst, _Material_2);
RenderTexture.ReleaseTemporary(rtTemp);

優化之前幾乎所有消耗都耗在最后一個DrawIndexed上

可以看出分割出兩個pass之后開銷一下平衡很多。另外,unity中在利用RenderTexture.GetTemporary時,內部會調用DiscardContents ,因而對CPU的效率也有所提升。詳情可以參考官方文檔
增加了bloom之后的效果圖。

其他可能的改進

投影增強

前面我們在進行擴散投影模擬的時候,是同時對周圍八個點進行采樣,但是事實上,有時為了控制投影的方向,可以只對一側的點進行采樣

如圖所示,只需要對右下角的五個格子采樣,就可以模擬出左上角的光照。

這樣造成的效果是亮的部分會凸起,暗的部分會產生凹陷的效果

函數的擬合

前面在計算相鄰點的加權顏色值時,用到了一個指數函數。指數函數的效果的確很好,考慮到在某些平台上exp的消耗可能有點大。另外,任意兩個像素之間的歐幾里得距離不會超過2.3個像素,所以我們嘗試對函數進行一個擬合,如0.926+1.441x + 0.6578x^2 + 0.0417x^4

其實我們還可以將把它化成1/(7x^2 +1),效果也還可以,只是無論是哪種情況,在PC上測試差距並不明顯(也有可能適得其反)

掃描線

考慮到有些游戲中,會出現一些因為曝光過度而無法顯示掃描線的情況。於是,我們就需要對掃描線進行加強:

float limit = 1 - step(257.0, min(frac(i.pixel_no.x), 1- frac(i.pixel_no.y)) * _MainTex_TexelSize.z);
float bright = Luminance(out_color);
return fixed4(out_color *(1.8 - limit * bright * bright * 0.89), 1.0);

但是對於某些偏暗的游戲,如果為了提高整體亮度,而掃描線同時也強化的很厲害,那么就會導致“碳化”

中間的白色由於亮度過高,在補償的時候會顯得非常暗

雖然這樣的掃描線加強在其他場合正是我們需要的,但在這里只會讓畫面變得很臟。關於這個問題並沒有很好的解決方案,這需要根據不同游戲對參數做出調整。作為游戲開發,如果美術風格及早的確定,顏色的選擇有所參照,將會對程序的優化有極大的幫助。而2D像素游戲由於很少受光照影響,再加上像素畫本身也極其依賴於palette,所以如果palette控制的好,是可以根據其調試出一個很好的狀態的。

由於目前只用針對一款游戲,所以上面的手工調整可以接受。如果我們需要大量的調整,我在想,可能還有一種思路是像tone mapping一樣,將亮度映射在一個合理的區域內,這樣既保留了細節又處理了邊界狀況。

Tactics Ogre的特殊處理

剛開始我為Taactic Ogre(中文譯為:皇家騎士團)寫shader的時候,出現了一個問題。由於很容易知道psp的分辨率是480*272,我就將其硬編碼到shader中。但是卻出現了一些意想不到的狀況。在橫坐標方向上,掃描線的分布不均勻。由於是周期性的,並且隨着窗口的擴大問題更為嚴重,我最開始猜測是模擬器的精度出現了問題,我查了下changelog也的確提到了這個問題,只是我使用的版本應該已經修復了這個問題。另外,我測試了其他的游戲,發現一切都很正常,如果真的是精度問題,不該只出現在這一款游戲上。查看了整個render過程后發現Tactics Ogre中有些地方與其他游戲做的不同,比如

注意紋理右側的黑邊

Tactics Ogre在draw頂部的滾動文本時,並不會對其裁剪,而是放到了第二個color pass里才進行裁剪

不過ppsspp模擬器提供了u_texelDelta這樣一個uniform,我們可以利用它得知當前輸入紋理的resolution:

vec2 c_resolution = 1.0f / u_texelDelta;

這樣,即使在某些場景中,屏幕的分辨率發生變化,我們也能夠保證顯示正確的掃描線。

最終PSP模擬器效果圖

在這里給出自己制作的在PSP模擬器上的最終效果,請放大后觀察

結語

本文主要討論了針對細像素游戲的畫質增強,但是這並不意味着像素游戲的增強方式只有一種,相反,光是這里 就提到多種后期特效。我們也無法說哪種效果比另一種更好。更多的時候,還是需要根據對游戲的定位來定制自己的后期特效,從而讓畫面為游戲核心服務。程序和美術之間的溝通是否充分也是能否有效的構建出成功的游戲畫面中很重要的一個因素。

最后,你們覺得這是一篇討論像素游戲中畫面增強的文章嗎?
不,不是的,我只是在安利Tactics Ogre :P


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM