在本篇文章中,我們會具體的討論如何利用紋理映射讓場景擁有豐富的顏色信息,以及紋理貼圖精度大小所帶來的問題,怎麼去解決,最後將會介紹Bump Mapping和Displacement Mapping
1 紋理映射(Texture Mapping)首先讓我們一起來觀察這樣一張圖:
無論是球上的圖案,以及地板的木頭紋理都呈現出了不同的顏色信息,那麼回想在講解Blinn-Phong反射模型的時候曾提到,一個點的顏色是由其漫反射係數決定的,反射什麼顏色的光,人眼就能看見什麼顏色。那麼針對上面這幅圖,難道要去針對每一個點自己去設定一個顏色嗎?還是說有什麼更方便的方法呢?那便是紋理映射了!
我們可以將三維物體上的任意一個點都映射到一個2維平面之上,舉一個簡單例子,地球儀:
倘若擁有從3維World space到2維Texture space的一個映射關係,那麼只需要將每個點的顏色信息即漫反射係數存儲在2維的Texture之上,每次利用光照模型進行計算的時候根據映射關係就能查到這個點的漫反射係數是多少,所有點計算完之後,結果就像最左邊的screen space之中,整個Texture被貼在了模型之上。
有了Texture,有了映射關係,對渲染結果會有一個非常大提升,因為很多fancy的效果都可以通過texture的設計得到(當然這屬於美術的活兒了,咱們用就行了)。可以看這樣一個有點醜的例子。
可以看到在利用texture渲染之後,這個獨眼怪物醜的更有特點了。
好了,相信到這大家都對紋理映射了有了一個大概的了解,那麼有了一張Texture之後,這種映射關係究竟是如何表示的呢?這就要從紋理坐標(UV)說起了。在紋理空間之內任意一個二維坐標都在[0,1]之內。如下圖是一個可視化紋理坐標的結果:
橫軸和縱軸的最大值都為1,為什麼整幅texture圖可視化之後是紅色和綠色呢?可以將(u,v)坐標的兩點想像成red和green就能明白了。一幅Texture上的任意一點都可以用一個(u,v)坐標來表示(0<=u<=1,0<=v<=1),因此只需要在三維world space中每個頂點的信息之中存儲下該頂點在texture space的(u,v)坐標信息,自然而然的就直接的得到了這種映射關係。至於一個頂點所對應在紋理空間的坐標是怎麼得到的,這就並不是程式設計師們關心的了,美術大大們會幫我們最好的(當然讀者有興趣也可以自行搜集資料)。
有一種特殊的紋理稱為tile,這種紋理的特徵是重複拼接之後上下左右都是連續的,因此這種紋理可以複製很多張貼在牆面或地板上。
一個具體的重複利用這種貼圖的例子如下:
最後給出一個紋理坐標使用的偽代碼供參考:
簡而言之就是對每個光柵化的屏幕坐標算出它的uv坐標(利用三角形頂點重心坐標插值),再利用這個uv坐標去查詢texture上的顏色,把這個顏色信息當作漫反射係數Kd。
好了在理解了紋理映射的基礎之後,考慮如果紋理精度特別小(reslution低)或者紋理精度特別大(reslution大)會分別引起什麼問題呢?
2 紋理過小和紋理過大的問題及解決方案2.1 紋理過小引發的問題紋理過小的問題相對容易理解,想想我們把一張100x100的紋理貼圖應用在一500x500的屏幕之上必然會導致走樣失真,因為屏幕空間的幾個像素點對應在紋理貼圖的坐標上都是集中在一個像素大小之內。那麼如果僅僅是使用對應(u,v)坐標在texture貼圖下最近的那個像素點,往往會造成嚴重的走樣。
如圖中紅色點是屏幕空間下一像素所對應在texture空間中的點,會去選擇離他最近的那個橙色框起來的點。
這種方法是不可取的,接下來會介紹利用雙線性插值的方法緩解這種走樣現象。
2.2 雙線性插值(Bilinear Interpolation)我們依然取上圖的點作為例子,解釋雙線性插值。第一步,取出離紅色點最近的4個黑色頂點,分別算出,該紅色點在水平及豎直方向偏移的比率s,t,圖示如下:
接著先利用s,可以線性插值出如下圖所示的u0,u1點的顏色值
那麼下一步相信讀者也能猜到了,利用比例t,顏色值u0,u1插值出紅色點的顏色值
如此這樣利用兩次線性插值,考慮到了所有4個點的顏色值,能夠很好的緩解走樣失真現象,並且計算速度較高。
(tips:還有一種插值方法叫做雙三次插值(Bicubic),是利用三次方程來進行兩次插值,效果可能更好,但是計算速度很低不在這裡具體討論了)
最後以一張閆老師課上的例子看看這3種方法效果的對比
可能會看不太清。理解個意思就好
2.3 紋理過大引發的問題可能對於我們的第一直覺來說,紋理小確實會引發問題,但是紋理大那不是更好嗎,為什麼會引發問題呢?但事實是紋理過大所引發的走樣甚至會更加嚴重。想像一張很大的地板,在上面鋪滿了重複的方格貼圖,我們所期望看到的結果應該是這樣的:
嗯,非常符合透視關係,不錯,當然這只是一個參考。再來看看利用在第一章所提到的計算紋理顏色的偽代碼來計算的結果呢:
近處鋸齒!遠處摩爾紋!非常嚴重的走樣現象,為什麼會導致這樣的一個現象呢?這裡作者嘗試給出自己的兩種解釋:
1 如開頭所說,地板上鋪滿了重複的方格貼圖,根據近大遠小,遠處的一張完整的貼圖可能在屏幕空間中僅僅是幾個像素的大小,那麼必然屏幕空間的一個像素對應了紋理貼圖上的一片範圍的點,這其實就是紋理過大所導致的,直觀來說想用一個點採樣的結果代替紋理空間一片範圍的顏色信息,必然會導致嚴重失真!(從信號的角度來說就是,採樣頻率過低無法還原信號原貌)
2 換一種想法,考慮離相機很遠的一個三角形面,假設該三角形面真正在紋理貼圖上對應的一片區域有10個像素點。但是由於透視的關係,距離很遠的三角形面投影到近平面時可能只有1個或2個像素點的大小(遠遠小於10個像素的原來大小),那麼這1個或2個像素採樣texture的結果就要代表原來這個三角形面10個像素點的顏色信息,自然會導致失真!
(tips:可能有讀者一開始會疑惑(包括我也是)為什麼1個屏幕空間像素點覆蓋多個紋理空間像素就是紋理過大呢,想像一下紋理貼圖大小500x500,屏幕空間100x100,將屏幕空間的像素點均勻分布在紋理空間之中,那麼1個屏幕空間像素點所佔的平均大小就是5x5=25個紋理空間像素,因此這就是紋理過大所導致的結果)
這種現象被形象的成為屏幕像素在texture空間的footprint。如上圖所示一個屏幕空間的藍色像素點離相機越遠,對應在texture空間的範圍也就越大。其實也就是越來越欠採樣,那麼一種直觀的解決方法就是Supersampling,如果一個像素點不足以代表一個區域的顏色信息,那麼便把一個像素細分為更多個小的採樣點不就可以解決這個問題了嗎?對,確實是這樣,可以看看如下圖512x超採樣的結果
效果雖稱不上完美但也極大緩解了走樣現象,但問題是什麼?計算量太大了,一個像素點被分為了512x512個採樣點,計算量幾乎多出了25萬倍!這顯然不是所希望看到的,並且隨著屏幕空間的點離相機距離更遠,更多的texels(紋理空間的像素)會在屏幕像素的一個footprint裡面,會要更高的超採樣頻率。
那麼另外一種想法,如果不去超採樣,僅僅是求出每個屏幕像素所對應的footprint裡所有texels的顏色均值呢?這也就是接下來所要介紹的著名的Mipmap技術了!
2.3 Mipmap回顧一下屏幕像素在Texture空間裡的footprint的這張圖:
正如上文所提,一個採樣點的顏色信息不足以代表 「footprint」裡一個區域的顏色信息,如果可以求出這樣一個區域裡面所有顏色的均值,是不是就是一種可行的方法呢?沒錯我們的目標就是從點查詢Point Query邁向區域查詢Range Query。但依然存在一個問題,從上圖不難看出,不同的屏幕像素所對應的footprint size是不一樣大小的,看下圖這樣一個例子:
遠處圓圈裡的footprint必然比近處的要大,因此必須要準備不同level的區域查詢才可以,而這正是Mipmap。
level 0代表的是原始texture,也是精度最高的紋理,隨著level的提升,每提升一級將4個相鄰像素點求均值合為一個像素點,因此越高的level也就代表了更大的footprint的區域查詢。接下來要做的就是根據屏幕像素的footprint大小選定不同level的texture,再進行點查詢即可,而這其實就相當於在原始texture上進行了區域查詢!
那麼如何去確定使用哪個level的texture呢?利用屏幕像素的相鄰像素點估算footprint大小再確定level D!如下圖:
在屏幕空間中取當前像素點的右方和上方的兩個相鄰像素點(4個全取也可以),分別查詢得到這3個點對應在Texture space的坐標,計算出當前像素點與右方像素點和上方像素點在Texture space的距離,二者取最大值,計算公式如圖中所示,那麼level D就是這個距離的log2值 (D = log2L) ! 這不難理解,讀者可以具體取幾個例子比如L = 1,L = 2,L = 4,看看是否符合這樣的計算即可。
但是這裡D值算出來是一個連續值,並不是一個整數,有兩種對應的方法:
1 四捨五入取得最近的那個level D
2 利用D值在 向下和向上取整的兩個不同level進行3線性插值
第一個方法很容易理解,具體講述一下第二個方法,如圖:
所謂3線性插值,就是在向下取整的D level上進行一次雙線性插值(前文提過),再在D+1 level之上進行一次雙線性插值,這二者數據再根據實際的連續D值在向下和向上取整的兩個不同level之間的比例,再來一次線性插值,而這整體就是一個三線性插值了。
好了!根據上述的方法算出屏幕上每一個像素點所對應的Mipmap level,再進行三線性插值得到顏色值,是否就能很好的解決走樣問題了呢?很遺憾,在本文的那個地板的例子之中,費了這麼大力氣依然不能完美解決,如下圖結果:
雖然和一開始的point sample有了很大的進步,但是有一個嚴重的問題是,遠處的地板產生一種過曝的現象,完全糊在了一起。該如何解決這個最後的問題呢——各向異性過濾。
2.4 各向異性過濾Mipmap好,接著上文的遠處產生過曝的問題繼續來談,產生這種現象的原因是因為,所採用的不同level的Mipmap默認的都是正方形區域的Range Query,然而真實情況並不是如此,見下圖:
可以看出不同screen space的像素點所對應的footprint是不同的,有長方形,甚至是不規則圖形,那麼針對這種情況,有的所需要的是僅僅是水平方向的高level,有的需要的僅僅是豎直方向上的高level,因此這也就啟發了各向異性的過濾:
(個人感覺,應該是要算出水平方向的level D0,再算一個豎直方向的level D1,然後算根據這兩個level去各項異性過濾的texture裡面找一張最合適的)
利用這樣不同的貼圖,更加精細的選擇後結果就會明顯好很多:
可以看出,遠處過曝的現象已經大大減少了。
(Note:其實各向異性過濾並不能解決 diagonal的footprint,因為各向異性只能解決水平或豎直的不同大小的矩形footprint,所以針對diagonal的footprint,一般是去sample更多的點,或者提前算好diagonal 過濾的texture)
總結紋理映射的一些基礎概念和做法個人感覺還是比較容易理解的,難的是後序的紋理過大過小引發的問題,可能相對而言有些抽象,需要自己仔細思考思考,包括解決問題的一些方法Mipmaps,各向異性過濾什麼的,希望能對大家的理解產生一點幫助吧!
最後對大家理解有幫助的話求點讚求收藏求一個大大的關注 :) ,後序會持續更新,感謝閱讀!
Reference[1] GAMES101-現代計算機圖形學入門-閆令琪
聲明:發布此文是出於傳遞更多知識以供交流學習之目的。若有來源標註錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯繫,我們將及時更正、刪除,謝謝。
作者:孫小磊
來源:https://zhuanlan.zhihu.com/p/144332091
More:【微信公眾號】 u3dnotes