法線貼圖與切線空間

法線貼圖

法線貼圖是一種技術(shù)扛门,用專業(yè)一點(diǎn)的話來說,叫做采用低面數(shù)模型(低模)實(shí)現(xiàn)對(duì)高面數(shù)模型(高模)模擬的技術(shù)歹河。用通俗的話來說,就是使用二維圖形模擬三維效果的技術(shù)花吟。譬如秸歧,在一面從模型上來看是平滑的墻上看到粗糙的質(zhì)地的三維效果(所謂的凹凸貼圖),這種粗糙質(zhì)地表現(xiàn)為衅澈,隨著光照方向的變化键菱,陰影也會(huì)跟著變化,類似自然界中物體表面不平整的凹凸效果今布。如下圖所示经备,左邊的模型與右邊的模型分別為低模與高模,法線貼圖的作用就是想使用左邊的模型得到右邊模型的效果部默,從而保證了繪制的效率與精度侵蒙。

我們知道,模型表面上像素的光照效果(包括顏色傅蹂、陰影等)只與光照以及表面上的法線相關(guān)纷闺,在光照確定的情況下,則只由法線唯一約束份蝴,而通常所說的法線是沒有長(zhǎng)度的犁功,只由方向唯一標(biāo)識(shí),故而婚夫,法線的方向決定了模型表面的光照效果浸卦。通常平滑表面的各個(gè)像素點(diǎn)的法線自然是完全一致的,導(dǎo)致光照射在其上的表現(xiàn)完全一致案糙,呈現(xiàn)出平滑效果限嫌。而粗糙表面上各點(diǎn)法線各不相同靴庆,導(dǎo)致光照在其上的表現(xiàn)各不相同,從而呈現(xiàn)粗糙質(zhì)地萤皂。

法線貼圖(Normal Mapping)實(shí)質(zhì)上是凹凸貼圖(Bump Mapping)的一種撒穷。凹凸技術(shù)的目的是為了使平滑的表面有粗糙的質(zhì)地感,其最初的手法是通過一張高度圖記錄各個(gè)像素點(diǎn)的高度信息裆熙,從而通過高度差得到各點(diǎn)的法線端礼,而由于平面上各點(diǎn)有著高度差就會(huì)導(dǎo)致法線不同,從而根據(jù)光照表現(xiàn)與法線的關(guān)系得到凹凸效果入录。

凹凸貼圖發(fā)展至今已經(jīng)有了三十余年的歷史蛤奥,其中除了最早的高度圖、目前廣泛使用的法線貼圖僚稿、還有一些其他的技術(shù)被提出并得到了相應(yīng)的驗(yàn)證與推廣凡桥,具體可以參照此文。今天要重點(diǎn)介紹的是法線貼圖技術(shù)蚀同,剛才說到法線貼圖是實(shí)質(zhì)上是低模對(duì)高模的模擬缅刽,那么,這種模擬是怎樣實(shí)現(xiàn)的呢蠢络?下面從原理上做一下簡(jiǎn)要介紹衰猛。

對(duì)于整體形狀近似的低模與高模,例如上面圖示中的兩個(gè)模型刹孔,左邊的低姆仁。看起來平滑,右邊高乃柘迹看起來粗糙卦睹,轉(zhuǎn)換成相關(guān)術(shù)語則是,左邊面數(shù)較少方库,右邊面數(shù)較多结序。如此,對(duì)于左邊的某個(gè)三角面片(以Triangle做陳述范例)纵潦,那么在右邊的模型中笼痹,可能對(duì)應(yīng)多個(gè)三角面片,而右邊模型越復(fù)雜酪穿,對(duì)應(yīng)的面片數(shù)越多。如下圖所示晴裹,將兩個(gè)模型從二維轉(zhuǎn)換成一維被济,那么低模就對(duì)應(yīng)下面一條平滑曲線,而高模則對(duì)應(yīng)上面插滿了標(biāo)槍的曲折曲線涧团,將下方的圖中兩條曲線用折線近似模擬的話只磷,下面曲線的每一段折線都將對(duì)應(yīng)上面曲線的多條折線经磅。而我們現(xiàn)如今想用下面一條曲線與光照配合來模擬出上面一條曲線的豐富細(xì)節(jié),那么我們就應(yīng)該要讓下面曲線上各點(diǎn)的法線與上面曲線上各點(diǎn)法線相對(duì)應(yīng)(至于點(diǎn)與點(diǎn)之間的對(duì)應(yīng)方法钮追,加入我們考慮最簡(jiǎn)單的预厌,從上往下俯視,上下兩條曲線每一對(duì)重合的點(diǎn)形成一個(gè)match)元媚,而之后轧叽,我們將上面一條曲線各個(gè)點(diǎn)的法線(也就是隨地亂插的標(biāo)槍)存儲(chǔ)起來,在渲染下方曲線所表示的低模的時(shí)候刊棕,將法線一一對(duì)應(yīng)到各點(diǎn)上炭晒,那么經(jīng)過光照與法線的作用,在低模上各點(diǎn)就將呈現(xiàn)不同的風(fēng)光甥角,看起來就像那么回事了网严。關(guān)于法線貼圖的一點(diǎn)具體資料可以參見此文

原理已經(jīng)闡述清楚了(嗯嗤无,自認(rèn)為闡述清楚了震束。。)当犯,關(guān)于法線貼圖的具體實(shí)現(xiàn)垢村,還有一些問題需要考慮:

  • 法線的獲取
  • 法線的存儲(chǔ)
  • 法線的應(yīng)用

每個(gè)面法線的獲取比較簡(jiǎn)單,取得這個(gè)面的三個(gè)頂點(diǎn)灶壶,之后構(gòu)造出兩條邊向量肝断,將二者叉乘即可,不過要注意保證方向驰凛,法線的方向總是指向上方胸懈。在這里有一點(diǎn)值得稱道的是坐標(biāo)系:

  • 實(shí)際上不管是左手坐標(biāo)系,還是右手坐標(biāo)系,對(duì)于兩個(gè)代數(shù)表達(dá)式相同的向量,其叉乘的結(jié)果的代數(shù)表達(dá)也必然是相同的:不論左右手攀芯,這個(gè)結(jié)果都是正確的 cross((1,0,0), (0,1,0)) = (0,0,1)
  • 左手坐標(biāo)系(如DX)的頂點(diǎn)環(huán)繞順序是順時(shí)針第焰,與之相對(duì)應(yīng)的是CCW(counter clock wise 逆時(shí)針)背面消隱,即在屏幕上頂點(diǎn)環(huán)繞順序?yàn)槟鏁r(shí)針的將被culling掉
  • 有手坐標(biāo)系(如OpenGL砰左,F(xiàn)BX,3ds Max)的頂點(diǎn)環(huán)繞順序是逆時(shí)針,與之對(duì)應(yīng)的是CW(順時(shí)針)背面消隱井联,在屏幕上環(huán)繞順序?yàn)轫槙r(shí)針的被忽略。

知道這點(diǎn)知識(shí)您旁,就不會(huì)將法線方向搞反了烙常。

法線的存儲(chǔ),這有什么好講的呢鹤盒,直接用三個(gè)浮點(diǎn)數(shù)組成的向量表示不就行了嗎蚕脏?沒錯(cuò)侦副,法線的確是用一個(gè)三維向量就可以表征,但是關(guān)于其具體的處理還有一些細(xì)節(jié)需要完善驼鞭。

三維向量存在的基礎(chǔ)是三維空間坐標(biāo)系秦驯,在游戲開發(fā)中會(huì)涉及到眾多的三維空間,世界空間坐標(biāo)系挣棕,模型空間坐標(biāo)系等译隘,究竟應(yīng)該選擇什么坐標(biāo)系作為法線向量的基礎(chǔ)坐標(biāo)系呢?顯然穴张,模型上各點(diǎn)法線是會(huì)隨著模型的旋轉(zhuǎn)而變化的细燎,假如采用世界坐標(biāo)系作為基礎(chǔ)坐標(biāo)系,那么法線向量將為固定的皂甘,一旦模型發(fā)生旋轉(zhuǎn)變換玻驻,將會(huì)導(dǎo)致法線向量與真實(shí)的法線無法匹配(畢竟世界坐標(biāo)系沒變,原來存儲(chǔ)的法線向量數(shù)值上就沒有發(fā)生變化偿枕,但實(shí)際上的法線已經(jīng)變了)璧瞬。

那么,采用模型空間坐標(biāo)系如何渐夸?這的確是一個(gè)可以實(shí)施的想法嗤锉,將模型上各點(diǎn)相對(duì)于整個(gè)模型坐標(biāo)系的法線向量保存下來,使用的時(shí)候通過矩陣變換將之轉(zhuǎn)換到世界坐標(biāo)系中墓塌,那么就解決了法線的存儲(chǔ)以及模型變換的問題瘟忱。但是,需求是永遠(yuǎn)在變化的苫幢,難纏的策劃們腦洞大開的說访诱,假如模型發(fā)生了變形要怎么辦?很顯然韩肝,如果模型發(fā)生了變形触菜,舉個(gè)例子,某個(gè)平面變成了拱形曲面哀峻,那么即使模型沒有進(jìn)行變換涡相,但這個(gè)面上的法線還是發(fā)生了變化。天剩蟀,世界上怎么有策劃這種生物催蝗!

好在,上有政策育特,下有對(duì)策生逸。考慮到變形的結(jié)果,也就是組成模型的各個(gè)三角面片進(jìn)行了旋轉(zhuǎn)等變換槽袄,雖然這些三角面片的位置以及朝向不同了,但是不變的是三角面片還依然是三角面片锋谐,組成三角面片的各個(gè)頂點(diǎn)也都還沒變(嗯遍尺,頂點(diǎn)的坐標(biāo)當(dāng)然發(fā)生了變化,但是一個(gè)人不論處在北京還是東京涮拗,這個(gè)人還依然是這個(gè)人)乾戏。有人就想到,不如我們就對(duì)最小的單位——三角面片做處理三热,為每個(gè)三角面片設(shè)置一個(gè)坐標(biāo)系鼓择,將法線保存在這個(gè)坐標(biāo)系中怎么樣?這樣行么就漾?當(dāng)然行呐能,而用這種方式實(shí)現(xiàn)的法線的存儲(chǔ)方法就是傳說中的切線空間,至于具體的實(shí)現(xiàn)原理抑堡,將在稍候送上摆出。

法線貼圖要考慮的最后一個(gè)問題是法線的應(yīng)用,剛才說到了法線的存儲(chǔ)首妖,那最終我們計(jì)算各點(diǎn)光照的時(shí)候偎漫,要怎么講存儲(chǔ)起來的法線取出來呢?難道要讀文件有缆,用矩陣的方式按照行列索引取值象踊?這種方式自然是可以的,不過為了更加直觀的查看法線棚壁,前輩們采取了將法線存儲(chǔ)為貼圖的方式來解決這個(gè)問題杯矩。這就有趣了,法線怎么可以變成貼圖呢灌曙?我們知道菊碟,法線有x,y,z三個(gè)分量(浮點(diǎn)數(shù)),而我們又知道一張二維圖中的一個(gè)像素點(diǎn)的表現(xiàn)主要由顏色來控制在刺,而顏色由RGB三原色控制逆害,將xyz對(duì)應(yīng)到RGB上,不就能夠?qū)崿F(xiàn)法線到貼圖的轉(zhuǎn)換了嗎蚣驼?也就是說魄幕,將一個(gè)模型上的所有點(diǎn)按照其上的貼圖(texture)坐標(biāo),將法線轉(zhuǎn)變?yōu)镽GB表示的顏色值颖杏,并存儲(chǔ)在貼圖坐標(biāo)上纯陨,形成一張貼圖,稱之為法線貼圖。法線貼圖翼抠,顧名思義咙轩,有法線,有貼圖阴颖,貼圖所指的就是這個(gè)意思了活喊。

在這三點(diǎn)中,其中最為重要的是法線的存儲(chǔ)量愧,下面做一下重點(diǎn)介紹钾菊。

切線空間

法線的存儲(chǔ),不單是用什么樣的格式來存儲(chǔ)數(shù)據(jù)偎肃,更重要的是在存儲(chǔ)之前要對(duì)數(shù)據(jù)進(jìn)行怎樣的處理煞烫。

對(duì)于以模型空間為基準(zhǔn)的法線來說,其前期處理可以直接忽略累颂,只需要面上各點(diǎn)的模型空間坐標(biāo)計(jì)算得到即可滞详。但是對(duì)于切線空間為基準(zhǔn)的法線來說,這一步的處理就很重要了喘落。

我們剛才說到茵宪,切線空間只是保存法線的一種手段。我們采用這種手段的終極目標(biāo)就是要實(shí)現(xiàn)法線的存儲(chǔ)瘦棋,以期望當(dāng)模型發(fā)生形變的時(shí)候稀火,這個(gè)法線還依然能夠用來計(jì)算光照。剛才又說到赌朋,在模型發(fā)生形變的時(shí)候凰狞,三角形面片的內(nèi)部結(jié)構(gòu)是固定的(基本可以假設(shè)固定),也就是各個(gè)像素的相對(duì)位置基本可以認(rèn)為保持不變沛慢,所以我們才想到借用這些固定的數(shù)據(jù)來建立一個(gè)坐標(biāo)系赡若,實(shí)現(xiàn)法線的保存,為什么呢团甲,因?yàn)槟P桶l(fā)生形變逾冬,可以解釋為組成模型的各個(gè)面發(fā)生旋轉(zhuǎn)等變換,對(duì)應(yīng)于一個(gè)變換矩陣躺苦。當(dāng)模型發(fā)生形變的時(shí)候身腻,我們的這個(gè)切線空間坐標(biāo)系乘以這個(gè)面對(duì)應(yīng)的變換矩陣就可以轉(zhuǎn)變?yōu)槟P妥鴺?biāo)系中的數(shù)據(jù),這樣匹厘,再繼續(xù)乘以模型坐標(biāo)系到世界坐標(biāo)系的變換矩陣嘀趟,就能夠變換為世界坐標(biāo)系中的數(shù)據(jù)。

好了愈诚,原理基本上自以為介紹清楚了她按,那么有幾個(gè)問題這里還需要詳細(xì)考慮一下牛隅。

  • 切線空間坐標(biāo)系要怎么選取并計(jì)算,也就是切線空間坐標(biāo)系到模型坐標(biāo)系的變換矩陣要怎么得到
  • 切線空間坐標(biāo)系要怎么保存
  • 法線怎么基于切線空間進(jìn)行保存
  • 一些善后的處理

首先酌泰,我們來看一下切線空間(TBN媒佣,Tangent, Binormal宫莱, Normal丈攒,切線空間由這三個(gè)向量組成)的計(jì)算方法,一般呢授霸,我們是選擇面法線作為切線空間的Z軸,而面上貼圖的UV方向作為切線空間的XY軸际插,這里有人就會(huì)有疑問了碘耳,貼圖的UV方向可不一定垂直呀?這樣能作為坐標(biāo)系的坐標(biāo)軸么框弛?沒錯(cuò)辛辨,這個(gè)想法是正確的,UV的確是不一定垂直的瑟枫,心靈手巧的美術(shù)同學(xué)們?cè)诜諹V的時(shí)候斗搞,有時(shí)候就是需要使得貼圖發(fā)生變形,從而導(dǎo)致UV并不垂直慷妙,但是這又如何僻焚?回憶一下課堂上老師對(duì)于代數(shù)幾何的教學(xué),使用標(biāo)準(zhǔn)正交基組成的坐標(biāo)系叫做標(biāo)準(zhǔn)正交坐標(biāo)系膝擂,而使用普通基組成的則是普通坐標(biāo)系(咳咳虑啤,其實(shí)我也是剛剛?cè)ゲ橘Y料才知道的。架馋。)狞山。使用普通坐標(biāo)系,實(shí)際上是可以組成坐標(biāo)系的叉寂,只要其中組成坐標(biāo)系的各個(gè)向量是線性不相關(guān)的即可(當(dāng)保證三個(gè)向量正交萍启,則可以保證在切線空間中的兩個(gè)向量當(dāng)變換到模型坐標(biāo)空間中,這兩個(gè)向量的夾角將保持不變屏鳍;另外勘纯,正交向量組成的正交矩陣,其逆矩陣就是其轉(zhuǎn)置矩陣孕蝉,因此可以簡(jiǎn)化很多計(jì)算屡律。具體可以參見這篇文章)。另外降淮,一般超埋,我們的計(jì)算只需要使得各個(gè)向量為標(biāo)準(zhǔn)向量(即模為1)即可搏讶,對(duì)于方向是否正交,并沒有嚴(yán)格要求霍殴,而通常媒惕,為了計(jì)算的方便,可以通過Gramm-Schmidt正交化方法或者其他正交化方法(如保證N不變来庭,將B與N叉乘妒蔚,得到T,再將N與T叉乘得到B月弛,也能實(shí)現(xiàn)正交)完成正交肴盏。通過這個(gè)方法計(jì)算得到的TBN三個(gè)向量,即可組成一個(gè)3*3矩陣帽衙,這個(gè)矩陣菜皂,實(shí)質(zhì)上就是從切線空間到模型坐標(biāo)空間的變換矩陣,關(guān)于具體的計(jì)算厉萝,可以參考這篇文章還有這篇文章恍飘。

第二個(gè),說到切線空間坐標(biāo)系的保存問題谴垫。我們知道以前的法線數(shù)據(jù)是保存在頂點(diǎn)中的章母,也就是將每個(gè)定點(diǎn)的法線數(shù)據(jù)保存在各自的數(shù)據(jù)中,而不保存面的法線數(shù)據(jù)翩剪,這是因?yàn)閳D形學(xué)中大部分的計(jì)算都是針對(duì)頂點(diǎn)展開的乳怎,而面上其他像素的計(jì)算結(jié)果都是通過對(duì)頂點(diǎn)計(jì)算結(jié)果的插值完成。所以肢专,我們也需要將面上計(jì)算得到的TBN轉(zhuǎn)化為頂點(diǎn)的TBN舞肆,這個(gè)轉(zhuǎn)化就大有學(xué)問,對(duì)于由一個(gè)面獨(dú)享的頂點(diǎn)博杖,那沒什么好說的椿胯,直接將面的TBN數(shù)據(jù)賦值給頂點(diǎn)即可(有的頂點(diǎn)雖然是被多個(gè)面共享,但是共享這個(gè)頂點(diǎn)的多個(gè)面不是屬于同一光滑組剃根,所以不需要對(duì)頂點(diǎn)數(shù)據(jù)進(jìn)行平滑處理哩盲,也就是不需要對(duì)多個(gè)面的數(shù)據(jù)進(jìn)行加權(quán)之后賦給頂點(diǎn),而是將頂點(diǎn)拆分為多個(gè)頂點(diǎn)狈醉,每個(gè)面各抱一個(gè)回家廉油,每個(gè)面抱回家的頂點(diǎn)的數(shù)據(jù)就對(duì)應(yīng)于面的數(shù)據(jù)),其關(guān)鍵的問題在于由多個(gè)面(且這些面都是同一光滑組的)公用的頂點(diǎn)的TBN數(shù)據(jù)要怎么處理苗傅。Crytek引擎的計(jì)算時(shí)通過頂點(diǎn)所對(duì)應(yīng)的夾角來完成平均的(請(qǐng)參見此文)抒线,而FBX(Autodesk的模型保存文件,具有保存切線空間數(shù)據(jù)的功能)計(jì)算很詭異渣慕,對(duì)于共用一條邊的兩個(gè)面來說嘶炭,這條邊對(duì)應(yīng)有兩個(gè)頂點(diǎn)抱慌,對(duì)于這兩個(gè)頂點(diǎn),F(xiàn)BX會(huì)將其拆分為四個(gè)頂點(diǎn)眨猎,每個(gè)頂點(diǎn)一分為二抑进,各占五成,以AB來代替兩面睡陪,對(duì)于A面的這兩頂點(diǎn)來說寺渗,其TBN數(shù)據(jù)是TBN-A * 2 + TBN-B,對(duì)于B面的兩個(gè)頂點(diǎn)來說兰迫,其TBN數(shù)據(jù)為TBN-A + TBN-B * 2信殊,這種二比一的分配方式不知道是因?yàn)楣灿玫氖莾蓚€(gè)頂點(diǎn)還是因?yàn)槠渌颉?duì)于虛幻引擎汁果,其對(duì)于面數(shù)據(jù)到頂點(diǎn)數(shù)據(jù)的處理就更為復(fù)雜鸡号,以下用代碼代替,有興趣的同學(xué)們可以自行發(fā)掘探索须鼎。


//////////////////////////////////////////////////////////
// 功能:仿照虛幻引擎的切線空間處理方式,根據(jù)平滑組對(duì)切線空間數(shù)據(jù)進(jìn)行平滑處理
// 參數(shù):Mesh &mesh    網(wǎng)格數(shù)據(jù)
// 參數(shù):vector<D3DXVECTOR3> &Tan    切線數(shù)據(jù)
// 參數(shù):vector<D3DXVECTOR3> &Binor 副法線數(shù)據(jù)
// 參數(shù):vector<D3DXVECTOR3> Nor        法線數(shù)據(jù)
// 參數(shù):Index   對(duì)max的頂點(diǎn)環(huán)繞方向進(jìn)行修正的索引數(shù)組
// 返回:無
//////////////////////////////////////////////////////////
void TangentSpaceSmooth4VE (Mesh & mesh, vector <D3DXVECTOR3> & Tan, vector <D3DXVECTOR3> &Binor, vector <D3DXVECTOR3> Nor, const int * const Index, BOOL vertexOrthogonal )
{
                 int numTris = mesh.getNumFaces();  //面數(shù)
                 Face *geomFaces = mesh.faces;                        //面數(shù)據(jù)
                 TVFace *mapFaces = mesh.tvFace;                  //貼圖數(shù)據(jù)
                 Point3 *geomVerts = mesh.verts;                      //頂點(diǎn)數(shù)據(jù)
                 D3DXVECTOR3 zeroVec (0, 0, 0);                      
                 vector<int > adjacentFaces;
                 vector<float > determinant( numTris, 0.f);
                
                 //計(jì)算各個(gè)面的切線空間的行列式
                 for (int i = 0; i < numTris ; i++)
                {
                                 determinant[i ] = computeHybridProd( Tan[i * 3], Binor[i * 3], Nor[i * 3]);
                }

                 //用于存儲(chǔ)與某個(gè)面的共用某個(gè)頂點(diǎn)的鄰面的相關(guān)信息
                 struct FFanFace
                {
                                 int faceIndex ;                         //鄰面索引
                                 int linkedVertexIndex ;           //共用頂點(diǎn)在鄰面的位置(未經(jīng)過環(huán)繞方向Index處理)
                                 bool bFilled ;           //判斷是否處于同一光滑組府蔗,且可用于對(duì)切線空間向量進(jìn)行光滑
                                 bool bBlendNormals ;           //法線是否滿足光滑條件
                                 bool bBlendTangents ; //切線是否滿足光滑條件
                };

                 //暫存中間數(shù)據(jù)
                 vector<D3DXVECTOR3 > norCopy( Nor);
                 vector<D3DXVECTOR3 > tanCopy( Tan);
                 vector<D3DXVECTOR3 > binorCopy( Binor);
                
                 //計(jì)算需要使用到的存儲(chǔ)列表
                 vector<FFanFace > relateFace4Ver[3];               //存儲(chǔ)與某個(gè)頂點(diǎn)相關(guān)的面索引
                 vector<int >            dupVerts;                               //存儲(chǔ)與某個(gè)頂點(diǎn)索引相同的頂點(diǎn)位序


                 for (int fInd = 0; fInd < numTris ; fInd++)
                {
                                 int faceOffset = fInd * 3;
                                 Face& geomFace = geomFaces[ fInd];
                                 Point3 vPos [3];
                                 D3DXVECTOR3 tanArr [3], binorArr[3], norArr[3];

                                 for (int vInd = 0; vInd < 3; vInd ++)
                                {
                                                 vPos[vInd ] = geomVerts[ geomFaces[fInd ].v[vInd]];

                                                 tanArr[vInd ] = zeroVec;
                                                 binorArr[vInd ] = zeroVec;
                                                 norArr[vInd ] = zeroVec;

                                                 relateFace4Ver[vInd ].clear();
                                }

                                 //對(duì)于至少兩頂點(diǎn)重合的退化三角形晋控,將其數(shù)據(jù)設(shè)置為0
                                 if (vPos [0] == vPos[1] || vPos[1] == vPos [2] || vPos[0] == vPos[2])
                                {
                                                 for (int vInd = 0; vInd < 3; vInd ++)
                                                {
                                                                 Tan[faceOffset + vInd] = zeroVec;
                                                                 Binor[faceOffset + vInd] = zeroVec;
                                                                 Nor[faceOffset + vInd] = zeroVec;
                                                }
                                                 continue;
                                }

                                 //查找與當(dāng)前面具有共同頂點(diǎn)的面,將其索引存入AdjacentFaces中
                                 adjacentFaces.clear ();
                                 for (int vInd = 0; vInd < 3; vInd ++)
                                {
                                                 int vertexIndex = geomFace. v[vInd ];
                                                 dupVerts.clear ();
                                                 for (int i = 0; i < numTris ; i++)
                                                                 for (int j = 0; j < 3; j ++)
                                                                {
                                                                                 //查找具有相同索引的頂點(diǎn)姓赤,其中自身也被當(dāng)成一份子算入
                                                                                 if (geomFaces [i].v[j] == vertexIndex)
                                                                                                 dupVerts.push_back (i * 3 + j);
                                                                }

                                                 int dSize = dupVerts. size();
                                                 for (int i = 0; i < dSize ; i++)
                                                                 adjacentFaces.push_back (dupVerts[ i] / 3);//應(yīng)該要保持unique
                                }

                                 //去除重復(fù)的面
                                 sort(adjacentFaces .begin(), adjacentFaces.end ());
                                 adjacentFaces.erase (unique( adjacentFaces.begin (), adjacentFaces. end()), adjacentFaces.end ());

                                 //對(duì)與當(dāng)前面相鄰的面進(jìn)行處理赡译,根據(jù)共用頂點(diǎn)不同,將信息存入不同的vector中
                                 for (int ajctFace = 0; ajctFace < adjacentFaces .size(); ajctFace++)
                                {
                                                 int adjFaceIndex = adjacentFaces[ ajctFace];

                                                 //對(duì)此面的每個(gè)頂點(diǎn)不铆,找到與之相關(guān)的每個(gè)鄰面蝌焚,并進(jìn)行信息搜集處理(共用了幾個(gè)頂點(diǎn),是否可以參與向量光滑)
                                                 for (int vInd = 0; vInd < 3; vInd ++)
                                                {
                                                                 FFanFace newFanFace ;
                                                                 int commonIndexCount = 0;

                                                                 //將相鄰面的公共頂點(diǎn)存入一個(gè)FFanFace結(jié)構(gòu)中l(wèi)inkedVertexIndex中誓斥,對(duì)于兩個(gè)面中的每一對(duì)公共頂點(diǎn)只洒,都對(duì)應(yīng)一個(gè)FFanFace結(jié)構(gòu)
                                                                 if (fInd == adjFaceIndex)
                                                                {
                                                                                 commonIndexCount = 3;
                                                                                 newFanFace.linkedVertexIndex = vInd;
                                                                }
                                                                 else
                                                                {
                                                                                 for (int adjVerIndex = 0; adjVerIndex < 3; adjVerIndex ++)
                                                                                {
                                                                                                 if (geomVerts [geomFaces[ fInd].v [vInd]] == geomVerts[geomFaces [adjFaceIndex]. v[adjVerIndex ]])
                                                                                                {
                                                                                                                 commonIndexCount++;
                                                                                                                 newFanFace.linkedVertexIndex = adjVerIndex;
                                                                                                                 break;//對(duì)于某個(gè)頂點(diǎn)而言,不可能出現(xiàn)與兩個(gè)頂點(diǎn)重合的情況(如果出現(xiàn)劳坑,則為退化情況毕谴,之前已經(jīng)處理過)
                                                                                                }
                                                                                }

                                                                }

                                                                 //如果有共用頂點(diǎn),則將相關(guān)信息填充后距芬,存入與頂點(diǎn)對(duì)應(yīng)的vector中
                                                                 if (commonIndexCount > 0)
                                                                {
                                                                                 newFanFace.faceIndex = adjFaceIndex;
                                                                                 newFanFace.bFilled = (adjFaceIndex == fInd);                //最開始涝开,只有此三角面自身可以參與自身三個(gè)頂點(diǎn)的向量光滑
                                                                                 newFanFace.bBlendNormals = newFanFace. bFilled;
                                                                                 newFanFace.bBlendTangents = newFanFace. bFilled;
                                                                                 relateFace4Ver[vInd ].push_back( newFanFace);
                                                                }
                                                }
                                }

                                 //再進(jìn)行一輪處理,在此處理中框仔,光滑組信息會(huì)被考慮舀武,處理完成后,法線與切線的平滑條件被修改离斩,得到能夠參與到平滑過程中的所有面的相關(guān)信息
                                 for (int vInd = 0; vInd < 3; vInd ++)
                                {
                                                 int relVecSize = relateFace4Ver[ vInd].size ();
                                                 int newConnections ;//循環(huán)條件银舱,大于0瘪匿,則繼續(xù)循環(huán)。 其意義為纵朋,還存在著從bFilled的false陣營(yíng)向true陣營(yíng)的滲透
                                                 do
                                                {
                                                                 newConnections = 0;
                                                                 for (int curFaceIndex = 0; curFaceIndex < relVecSize ; curFaceIndex++)
                                                                {
                                                                                 FFanFace &curFace = relateFace4Ver[vInd ][curFaceIndex]; //取出當(dāng)前待考量三角面的鄰接信息
                                                                                 if (curFace .bFilled) //最初柿顶,只有最原始的三角面本身滿足此條件
                                                                                {
                                                                                                 for (int nextFaceIndex = 0; nextFaceIndex < relVecSize ; nextFaceIndex++)
                                                                                                {
                                                                                                                 FFanFace &nextFace = relateFace4Ver[vInd ][nextFaceIndex];
                                                                                                                 if (nextFace .bFilled) //將所有的相關(guān)面片分割成兩個(gè)部分,以bFilled(是否同陣營(yíng))標(biāo)記操软,以true陣營(yíng)為基礎(chǔ)嘁锯,不斷腐蝕false陣營(yíng)
                                                                                                                                 continue;

                                                                                                                 //如果不屬于一個(gè)光滑組,則不需處理聂薪,即不必要參與到光滑中
                                                                                                                 if (mesh.faces [curFaceIndex]. smGroup&mesh .faces[nextFaceIndex]. smGroup) // || curFaceIndex == nextFaceIndex
                                                                                                                                 continue;

                                                                                                                 int commonVertices = 0;
                                                                                                                 int commonNormals = 0;
                                                                                                                 int commonTangents = 0;

                                                                                                                 //根據(jù)相應(yīng)的判斷條件家乘,對(duì)是否需要進(jìn)行法線、切線光滑進(jìn)行設(shè)置
                                                                                                                 for (int curVertIndex = 0; curVertIndex < 3; curVertIndex ++)
                                                                                                                {
                                                                                                                                 for (int nextVertIndex = 0; nextVertIndex < 3; nextVertIndex ++)
                                                                                                                                {
                                                                                                                                                 if (geomVerts[geomFaces [curFaceIndex]. v[curVertIndex ]] == geomVerts[geomFaces [nextFaceIndex]. v[nextVertIndex ]])
                                                                                                                                                {
                                                                                                                                                                commonVertices ++;
                                                                                                                                                                                
                                                                                                                                                                //在頂點(diǎn)坐標(biāo)相同的情況下藏澳,判斷UV坐標(biāo)是否相等仁锯,來確定是否進(jìn)行切線空間平滑
                                                                                                                                                                if (mesh.getTVert (mapFaces[ curFaceIndex].t [curVertIndex]) == mesh.getTVert (mapFaces[ nextFaceIndex].t [nextVertIndex]))
                                                                                                                                                                {
                                                                                                                                                                                 commonTangents++;
                                                                                                                                                                }

                                                                                                                                                                //根據(jù)索引是否相同,來確定是否需要進(jìn)行法線光滑
                                                                                                                                                                if (geomFaces[curFaceIndex ].v[curVertIndex] == geomFaces[nextFaceIndex ].v[nextVertIndex]) // || bBlendOverlappingNormals
                                                                                                                                                                {
                                                                                                                                                                                 commonNormals++;
                                                                                                                                                                }
                                                                                                                                                }
                                                                                                                                }
                                                                                                                }

                                                                                                                 //當(dāng)兩個(gè)面至少共用一條邊的時(shí)候翔悠,可以考慮對(duì)數(shù)據(jù)進(jìn)行平滑處理
                                                                                                                 if (commonVertices > 1)
                                                                                                                {
                                                                                                                                 newConnections++;
                                                                                                                                 nextFace.bFilled = true;        //加入true陣營(yíng):同一光滑組业崖,且至少與true陣營(yíng)中某面共邊
                                                                                                                                 nextFace.bBlendNormals = (commonNormals > 1);        //平滑法線的條件是,至少有兩個(gè)共同頂點(diǎn)滿足法線平滑的條件蓄愁,即具有相同索引
                                                                                                                                 if (curFace .bBlendTangents && commonTangents > 1) //平滑此面切線的基本條件是双炕,此面的入黨介紹人滿足切線平滑條件,且此面至少有兩個(gè)公共頂點(diǎn)具有相同的UV坐標(biāo)
                                                                                                                                {
                                                                                                                                                 //切線平滑升級(jí)條件:此面切線空間與最原始面的切線空間具有一致的方向(即滿足相同的手系撮抓?)
                                                                                                                                                 if (determinant [fInd] * determinant[nextFaceIndex ] > 0.0f)
                                                                                                                                                                nextFace .bBlendTangents = true;
                                                                                                                                }
                                                                                                                }
                                                                                                }
                                                                                }
                                                                }
                                                } while (newConnections > 0);
                                }

                                 //頂點(diǎn)數(shù)據(jù)平滑處理
                                 for (int vInd = 0; vInd < 3; vInd ++)
                                {
                                                 int relVecSize = relateFace4Ver[ vInd].size ();
                                                 for (int relFaceIndex = 0; relFaceIndex < relVecSize ; relFaceIndex++)
                                                {
                                                                 FFanFace const & relateFace = relateFace4Ver[vInd ][relFaceIndex];

                                                                 //不屬于同一光滑組妇斤,或者沒有與原始面片共邊
                                                                 if (!relateFace .bFilled)
                                                                                 continue;

                                                                
                                                                 int relateFaceIndex = relateFace. faceIndex;
                                                                 if (relateFace .bBlendNormals)
                                                                {
                                                                                 norArr[vInd ] += norCopy[ relateFaceIndex * 3];
                                                                }
                                                                 if (relateFace .bBlendTangents)
                                                                {
                                                                                 tanArr[vInd ] += tanCopy[ relateFaceIndex * 3];
                                                                                 binorArr[vInd ] += binorCopy[ relateFaceIndex * 3];
                                                                }
                                                }
                                }

                                 //對(duì)頂點(diǎn)切線空間數(shù)據(jù)進(jìn)行正交化處理,然后將之存入相應(yīng)的vector中
                                 for (int vInd = 0; vInd < 3; vInd ++)
                                {
                                                 if (vertexOrthogonal )
                                                {
                                                                 //先歸一化
                                                                 D3DXVec3Normalize(&tanArr [vInd], & tanArr[vInd ]);
                                                                 D3DXVec3Normalize(&binorArr [vInd], & binorArr[vInd ]);
                                                                 D3DXVec3Normalize(&norArr [vInd], & norArr[vInd ]);
                
                                                                 //斯密特正交(與虛幻完全一致)
                                                                 binorArr[vInd ] = binorArr[ vInd] - D3DXVec3Dot (&tanArr[ vInd], &binorArr[vInd ]) * tanArr[ vInd];
                                                                 D3DXVec3Normalize(&binorArr [vInd], & binorArr[vInd ]);
                                                                 tanArr[vInd ] = tanArr[ vInd] - D3DXVec3Dot (&norArr[ vInd], &tanArr[vInd ]) * norArr[ vInd];
                                                                 D3DXVec3Normalize(&tanArr [vInd], & tanArr[vInd ]);
                                                                 binorArr[vInd ] = binorArr[ vInd] - D3DXVec3Dot (&norArr[ vInd], &binorArr[vInd ]) * norArr[ vInd];
                                                                 D3DXVec3Normalize(&binorArr [vInd], & binorArr[vInd ]);
                                                }

                                                 //需要加上Index進(jìn)行左右手坐標(biāo)系的交換
                                                 Tan[faceOffset + Index[vInd]] = tanArr[vInd ];
                                                 Binor[faceOffset + Index[vInd]] = binorArr[vInd ];
                                                 Nor[faceOffset + Index[vInd]] = norArr[vInd ];
                                }
                }

}

第三個(gè)丹拯,說到了頂點(diǎn)法線的在切線空間中的保存站超。我們知道,我們要保存的實(shí)際上是高模上各點(diǎn)的法線乖酬,而法線的存儲(chǔ)是借助低模的面上的切線空間完成的死相。也就是,我們計(jì)算低模上各個(gè)面的切線空間剑刑,并將高模上各個(gè)點(diǎn)的法線(模型空間)經(jīng)過空間變換媳纬,轉(zhuǎn)換為切線空間中的法線數(shù)據(jù),并以貼圖形式保存下來(由于對(duì)于低模的一個(gè)面上各點(diǎn)對(duì)應(yīng)的高模上各點(diǎn)的法線與低模上面法線基本平行施掏,導(dǎo)致轉(zhuǎn)變?yōu)榍芯€空間中各個(gè)法線基本上與Z軸平行钮惠,而Z軸對(duì)應(yīng)于貼圖中的B分量,所以就導(dǎo)致貼圖整體呈現(xiàn)藍(lán)色)七芭。在最后進(jìn)行圖形渲染的時(shí)候素挽,將從法線貼圖上取出的切線空間法線與模型上各面的切線空間相乘,就可以得到模型空間上的法線狸驳,而當(dāng)物體發(fā)生形變预明,就會(huì)導(dǎo)致模型空間的面發(fā)生變換缩赛,繼而導(dǎo)致切線空間的變換矩陣發(fā)生變化,這時(shí)候撰糠,只需要乘上變化后的切線空間矩陣酥馍,就可以保證變形后的光照效果接近自然界中真實(shí)變形的情況,而如果不重新計(jì)算各個(gè)面的切線空間矩陣阅酪,而是沿用最開始的切線空間矩陣旨袒,所得到的效果就跟使用普通法線貼圖(而非切線空間法線貼圖)一致。而為了保證顯示效果术辐,一般需要保證計(jì)算法線貼圖的切線空間坐標(biāo)系與最終渲染時(shí)候用的切線空間坐標(biāo)系一致砚尽。

最后,關(guān)于切線空間的實(shí)現(xiàn)還有一些善后處理辉词,比如:

  • 貼圖翻轉(zhuǎn)必孤,導(dǎo)致法線反向
  • 鏡像貼圖,導(dǎo)致法線反向
  • 柱面貼圖瑞躺,導(dǎo)致邊緣重合敷搪,進(jìn)而使得貼圖坐標(biāo)發(fā)現(xiàn)錯(cuò)誤
  • 球面貼圖等

關(guān)于這些問題的處理,在這篇文章中有詳細(xì)介紹幢哨,有興趣的同學(xué)們請(qǐng)自行翻閱购啄。

總結(jié)

關(guān)于法線貼圖與切線空間就講到這里,下面看下采用這種方式得到的模型在光照變化下的凹凸效果嘱么。圖像右邊的為光照方向的改變,光源起點(diǎn)在圓心顽悼。

關(guān)于切線空間以及法線貼圖曼振,上面文章中介紹的也都只是很少的一部分,還有很多的知識(shí)等待發(fā)掘蔚龙,如果發(fā)現(xiàn)文中有不恰當(dāng)或者不正確的介紹冰评,請(qǐng)大家不吝指正。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末木羹,一起剝皮案震驚了整個(gè)濱河市甲雅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坑填,老刑警劉巖抛人,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異脐瑰,居然都是意外死亡妖枚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門苍在,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绝页,“玉大人荠商,你說我怎么就攤上這事⌒” “怎么了莱没?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)酷鸦。 經(jīng)常有香客問我饰躲,道長(zhǎng),這世上最難降的妖魔是什么井佑? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任属铁,我火速辦了婚禮,結(jié)果婚禮上躬翁,老公的妹妹穿的比我還像新娘焦蘑。我一直安慰自己,他們只是感情好盒发,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布例嘱。 她就那樣靜靜地躺著,像睡著了一般宁舰。 火紅的嫁衣襯著肌膚如雪拼卵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天蛮艰,我揣著相機(jī)與錄音腋腮,去河邊找鬼。 笑死壤蚜,一個(gè)胖子當(dāng)著我的面吹牛即寡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播袜刷,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼聪富,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了著蟹?” 一聲冷哼從身側(cè)響起墩蔓,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎萧豆,沒想到半個(gè)月后奸披,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涮雷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年源内,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡膜钓,死狀恐怖嗽交,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颂斜,我是刑警寧澤夫壁,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站沃疮,受9級(jí)特大地震影響盒让,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜司蔬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一邑茄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧俊啼,春花似錦肺缕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跛十,卻和暖如春彤路,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芥映。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工洲尊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奈偏。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓颊郎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親霎苗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容