原文:http://www.flipcode.com/archives/Frustum_Culling.shtml html
時至今日,許多剛剛下海的3D引擎程序員仍不瞭解視錐剔除(Frustum Culling)的重要性和益處,這讓我和個人小夥伴們感到很震♂驚.我在Flipcode的論壇中發現儘管網絡上有海量的相關資料,仍有許多人提出對視錐剔除實現的問題.所以我決定撰寫這篇文檔,簡單描繪出我如今所使用的四叉樹剔除引擎(Quad-tree Culled Engine)的工做方式.誠然,市面上有許多種成熟且高效的視錐剔除算法,但我認爲這個算法足以用來學習視錐剔除的理論基礎.在正式開始前我還想說明一件事,之前我一直把Frustum(平截頭體)打成Frustrum(截頭錐),爲此我沒少被論壇上的人噴.在這裏我認可Frustum是正確的拼寫.對那些之前被我冒犯的人我表示抱歉...大家這羣吹毛求疵的傻[嗶-]... 程序員
大多數人已經知道什麼是視錐剔除了(譯者:若是你是手滑誤點進來的...視錐剔除是一個圖形渲染前的步驟,用於剔除掉不須要繪製的部分).視錐(準確說是平截頭體Frustum)的形狀酷似一個塔尖被削平了的金字塔,更準確地說,是一個四棱錐的頂點偏下位置被一個裁面(Clipping Plane,見圖1)裁斷.事實上,視錐自己就是由6個面所組成.這6個面被稱爲近裁面,遠裁面,上裁面,下裁面,左裁面,右裁面.視錐剪裁僅僅是一個用來判斷物體是否須要被繪製的過程.儘管從本質上講視錐剔除應該是三維層面的,但事實上大多數時候它僅僅須要以純代數的方法便能解決.這也是爲何我如此推崇視錐剔除的緣由,它很是的快(若是算法好的話),並且是在渲染管線(Rendering Pipeline)以前進行的,不像背面剔除(Backface Culling)那樣須要在渲染管線以後一個頂點一個頂點地計算.對於被剪裁掉的物體繪圖引擎都不會將其送入顯卡(譯者:那是...被剔除掉的壓根都不用渲染),所以視錐剔除對渲染速度有巨大的改善,畢竟什麼都不渲染是最快的渲染. 算法
然而,視錐剔除的高效還得益於它背後的算法,分層剔除法(Hierarchy Culling Method)是一種很好的算法.該算法將一個三維世界分割爲一個樹型結構,一旦咱們剔除掉一個節點,那麼那個節點往下的子節點就也被一併剔除了.這樣咱們就沒必要一個個剔除三維世界中每個物體.咱們只需簡單地分層處理便能成倍地剔除物體,省下了大量的剔除計算時間.若不使用分層法,視錐剔除就必須以最少O(n)的時間複雜度一個個判斷物體是否須要繪製,換句話說,100個物體須要100次計算,1000000個物體就須要1000000次!儘管進行視錐剔除總比不進行好,但糟糕的算法卻會使咱們的努力變得不那麼明顯.若要製做出一個優化良好的遊戲,能使用一個O(lgN)甚至O(c)的算法就不要使用一個O(n)的算法,我是毫不接受後者的,所以我選擇了分層剔除法.想象一下,這裏有100個物體,但只有1個位於咱們的視野內,若是咱們使用二分法來剔除的話,本來須要100次計算的線性算法,如今只須要7步(100->50->25->13->7->4->2->1,做者原文寫的6步...).O(n)與O(lgN)的區別,很容易就能看出來. 網絡
個人分層剔除法使用四叉樹分層.一個二叉樹或八叉樹或隨便什麼結構其實均可以勝任這個工做.事實上,代碼很容易移植.我選擇四叉樹是由於它很是直觀.本質上四叉樹是一個二維平面上有4個節點的樹(見圖2).所以,每個子節點都恰好是父節點的平面中的一個象限.所以咱們認爲它是"分層的",每個子節點都在父節點以內,若是咱們認定一個父節點是不可見的,咱們便也能夠判定子節點一樣是不可見的.所以咱們能夠以飛快的速度處理海量的物體數據,特別是對於擁有數十萬頂點數據的地形渲染引擎.並且它具備擴展性,地形上的裝飾物能夠被加進最小的四叉樹節點當中. 數據結構
要規劃一個視錐剔除系統的第一步是確保你已經有合適的剔除算法,這意味着咱們須要先知道如何根據視角/投影矩陣(View/Projection Matrices)經過6個平面構建出一個平截頭體,同時還要讓它既能檢測球體也能檢測立方體.說的更簡單一點,咱們得知道判斷一個球體或立方體與平截頭體是包含、相交仍是外離的方法.這可讓咱們待會將要製做分層剔除法更加完善. ide
首先讓咱們來定義咱們的平截頭體.平截頭體的6個面能夠根據咱們使用的渲染API(Direct3D/OpenGL)的鏡頭系統的視角/投影矩陣來定義.在不一樣的渲染API中定義起來有些差異.我嘗試爲Direct3D和OpenGL各寫一份,但我最後發現這樣只能讓讀者愈來愈困惑.幸運的是個人一位朋友已經寫了一份很好的教程.回到1996年,我參觀Raven公司時碰見了吉爾,那時我幾乎已是鐵定被簽約下來,到Raven和吉爾一塊兒工做,但NAFTA(北美自由貿易協定)卻阻止我這樣沒有大學文憑的人跨國工做.NAFTA我去年買了個爬山包!超耐磨!無論怎麼說,這是題外話.最重要的是那份教程清晰地描述瞭如何從矩陣中提取出6個裁面來.我建議你仔細讀讀並理解它: 函數
http://www2.ravensoft.com/users/ggribb/plane%20extraction.pdf
(譯註:這個網址已經訪問不了了,能夠試試這個http://www.cs.otago.ac.nz/postgrads/alexis/planeExtraction.pdf) post
如今我假定你已經編寫好了一個經過6個裁面定義的平截頭體類,爲了提升效率,請將其設計爲僅在視角/投影矩陣發生改變時(換句話說,鏡頭髮生移動時)才進行重構,而不是每一幀都重構一次.接下來咱們要設計判斷一個球體與平截頭體是內含、相交仍是外離的算法(譯註:內含和相交有什麼區別?等看到章節"優化1"時你就明白了).這其實很是簡單:遍歷6個裁面,判斷球體是在該裁面的正面,背面,仍是相交,實現起來確實不難,咱們須要計算從球體中心到該裁面的距離.若是距離的絕對值小於球體半徑,那麼就是相交.若是距離大於0那就是在裁面正面(有可能在平截頭體內).若是小於0那麼就是在裁面背面.計算方法是: 性能
設C(Center)爲球中心座標
設N(Normal)爲平面法線向量
設D(Distance)爲座標系原點到裁面的距離
則計算公式爲:距離=(C向量點乘N)+D,即C·N+D
(譯註:因爲C和N同維數,因此C與N能夠直接點乘.我剛開始看到這個公式時很暈乎,後來百度出一篇高中教案,其中寫道"利用法向量求點到平面的距離...把點A到平面的距離當作點A與平面內任意一點B所構成的向量在法向量方向上的投影的長度...",這即是C·N的出處,因爲座標系原點不必定在咱們的裁面上,因此最後還要+D來修正一下.順便一提那教案結尾還加了一句"此方法無需技巧,人人都會",我去年買了個表.) 學習
正如我以前說的那樣,再往下咱們所須要作的就僅僅只是判斷距離與球體半徑的大小關係.這裏是我編寫的C++代碼:
///判斷某球體是否在平截頭體內 int Frustrum::ContainsSphere(const Sphere& refSphere) const { //球體中心到某裁面的距離 float fDistance; //遍歷全部裁面並計算 for(int i = 0; i < 6; ++i) { //計算距離 fDistance = m_plane[i].Normal().dotProduct(refSphere.Center())+m_plane[i].Distance(); //若是距離小於負的球體半徑,那麼就是外離 if(fDistance < -refSphere.Radius()) return(OUT); //若是距離的絕對值小於球體半徑,那麼就是相交 if((float)fabs(fDistance) < refSphere.Radius()) return(INTERSECT); } //不然,就是內含 return(IN); }
接下來就是(在四叉樹中)判斷一個軸對齊包圍盒(Axis-aligned Bounding Box,簡稱AABB,不是BBA哦...說白了就是一個邊平行於座標軸的立方體)與平截頭體的關係.對於這步操做有數種方法.首先是包圍盒(Bounding Box,指一個多用於碰撞檢測的有邊界的立方體,AABB包圍盒是最簡單的一種)的定義,個人包圍盒是由2個座標組成,分別表明"最小(靠近座標軸負方向)"頂點和"最大"頂點,爲了壓縮體積,我乾脆將它的數據結構設計爲連續的6個數,接下來即是將包圍盒的8個頂點依次與平截頭體做對比,儘管這種作法不是最快的一種,但它倒是最容易實現,最容易被學會的一種.咱們只須要測試每個頂點(立方體的角)與平截頭體的關係便可.若是全部的點都在平截頭體內,那這個立方體就是被包含;若是有1~7個點在裏面,就是相切;若是全部的點都在某一個裁面的背後,那就是外離;不然,就仍是相切,爲何?很好,由於有可能立方體沒有一個點在平截頭體內,但依然和平截頭體相切(譯註:好比平截頭體的一個"尖"剛好插入立方體內).事實上,咱們仍沒有考慮一種極端狀況:有一個超大的包圍盒將平截頭體整個包裹起來了,按咱們如今的算法它是相交,事實上它真的是相交嗎?這種狀況咱們會在章節"優化1"中討論.
檢測一個點是否在平截頭體內也依舊簡單到不須要媽媽擔憂.咱們只須要將它和6個裁面做對比,判斷它是在每個裁面的正面就行(做者:別忘了,咱們的裁面都是面向平截頭體內部的. 譯者:我去年買了個表,你以前有說嗎?).判斷點和裁面的關係其實和判斷球體和裁面的關係同樣...依然是萬金油公式:C·N+D.若是結果大於0,就是在正面.若是小於0,就是在背面.若是是0就說明是在面內.除非你設計的算法有特殊的要求,否則我建議你僅需簡單地將其等價視爲在正面.(另外,若是你真的須要判斷點是不是在面上的話,請記住絕對不要直接用等於號判斷,別忘了浮點精度啊...).儘管聽起來它和球體判斷是同樣的,但我仍不介意再重寫一遍,下面是代碼:
///判斷AABB盒是否在平截頭體內 int Frustrum::ContainsAaBox(const AaBox& refBox) const { Vector3f vCorner[8]; int iTotalIn = 0; //得到全部頂點 refBox.GetVertices(vCorner); //測試6個面的8個頂點 //若是全部點都在一個的背後,那就是外離 //若是全部點都在每個面的正面,就是內含 for(int p = 0; p < 6; ++p) { int iInCount = 8; int iPtIn = 1; for(int i = 0; i < 8; ++i) { //測試這個點 if(m_plane[p].SideOfPlane(vCorner[i]) == BEHIND) { iPtIn = 0; --iInCount; } } //全部點都在p面背後嗎? if(iInCount == 0) return(OUT); //外離 iTotalIn += iPtIn; } //若是iTotalIn是6,那麼就都是在正面 if(iTotalIn == 6) return(IN); //內含 return(INTERSECT); //相交 }
好了,這就是視錐剔除的主要部分,你還好吧?
第一個對上面的函數的優化的方式很是簡單.若是你仔細研究了那兩個函數,你會發現球體檢測比立方體檢測要快不少.這意味着咱們最好儘可能使用球體檢測而不是立方體檢測,或者用球體檢測進行預預判,判斷經過後再使用立方體檢測進行詳細判斷,有時咱們使用立方體做爲物體的渲染範圍是由於它更匹配物體的形態,但我仍推薦先使用球體進行預判斷,而後再用立方體判斷.事實上,我如今使用的遊戲引擎爲每個渲染目標同時分配了球體和立方體兩個渲染範圍.它們都在同一個四叉樹節點上,在判斷時前者優先於後者,這樣我就能夠用球體判斷來快速剔除掉大量的節點/物體,而後再使用立方體判斷仔細篩選.這一種簡單的優化僅須要犧牲一點點內存空間便能提高性能.你所須要的僅僅只是爲每個物體分配兩個渲染範圍斷定.
下一個優化是選擇一種最優的分層結構遍歷方式.以咱們使用的四叉樹爲例,傳統方法是檢測一個父節點是否爲可見的.若是是,那麼就去檢測每個子節點.若是不是,那麼就中止處理.說白了這就是對每個節點進行遞歸處理.一旦咱們遍歷完成並決定出了須要繪製的節點,咱們便將那些節點的內容送往顯卡進行渲染.但想象一下,對於一個還沒有完結的節點(一個有子節點的節點),若是這個節點是所有可見的,那麼咱們還須要檢查它的子節點嗎?既然它已是所有可見的了,那麼它的子節點也必定是可見的,再對它們進行判斷就只是浪費你的CPU了!這就比如你不會去處理那些絕對不可見的節點同樣.明白爲什麼咱們以前要判斷"內含,相交,外離"而不是簡單的"包含,不包含"了吧?對於徹底不可見的節點,咱們只需簡單地剔除掉便可,對於所有可見的節點,咱們也只需簡單地放行便可,真正須要進一步遞歸處理的是部分可見而部分又不可見的節點,也就是那些"相交"的節點.這一步優化又能提高很多性能.
第三項優化則是若是鏡頭位於某個節點的渲染範圍內部的話,就直接視爲檢測經過並開始檢測它的子節點.這種狀況能夠被視爲相交,咱們只須要開始檢測子節點就好了.個人實現方法是若是判斷鏡頭位於一個立方檢測盒內部的話,就直接按照相交來處理.若是採用了這個優化,那麼你的四叉樹遞歸函數應該相似於這樣:
///對節點的遞歸處理 void QuadTree::RecurseProcess(Camera* pPovCamera, QuadNode* pNode, bool bTestChildren) { //在裁剪前是否須要檢測? if(bTestChildren) { //首先檢測是不是在檢測盒內 if(pNode->m_bbox.ContainsPoint(pPovCamera->Position()) == NOT_INSIDE) { //先進行球體檢測 switch(pPovCamera->Frustrum().ContainsSphere(pNode->m_sphere)) { case OUT: return; case IN: bTestChildren = false; break; case INTERSECT: //檢測立方體是否在視角內 switch(pPovCamera->Frustrum().ContainsAaBox(pNode->m_bbox)) { case IN: bTestChildren = false; break; case OUT: return; } break; } } } //[在這裏填入判斷代碼] }
若是你已經實現了上述全部的內容,而且注意觀察你的程序的性能的話,你應該會注意到相交判斷代碼依然有些冗雜,它們佔了CPU運算的大頭.固然具體程度取決於你的程序中的物體數,以及你的四叉樹的深度,和各類各樣的因素.就算不進行性能分析,光憑眼睛看也會發現相交判斷代碼十分的複雜.就算是相對最快的球體檢測也依舊須要檢測球心和數個面的關係,最好狀況下也至少得有1個,最糟狀況下須要和所有6個面進行檢測.不過這還有改善空間.先讓我簡單闡述一下接下來咱們要作的.
第一個是球-球相交檢測.這個檢測又快又簡單.只須要計算兩個球體的球心距離而後和二者的半徑和做對比,若距離小於半徑和則是相交(在這裏咱們將內含也視爲相交,由於已經沒有必要分的那麼清楚了),不然就是不想交.下面是個人算法,你應該會注意到我是使用距離的平方進行判斷,由於開根操做很慢!
///測試兩個球體是否相交 bool Sphere::Intersects(const Sphere& refSphere) const { //得到一個從目標球心指向本球心的向量 Vector3f vSepAxis = this->Center() - refSphere.Center(); //計算半徑和 float fRadiiSum = this->Radius() + refSphere.Radius(); //簡單地說,若是向量的模長小於半徑的話,就是相交 //但直接求向量模長的話,須要一次開根操做 //而乘法(平方)操做遠比開跟操做要快 //因此我使用摸的平方與半徑和的平方做對比 if(vSepAxis.getSqLength() < (fRadiiSum * fRadiiSum)) return(true); //不然就是不相交 return(false); }
下一個是判斷球體和圓錐是否相交的算法.這個要比球-球相交斷定複雜得多.我能夠給你完整解釋一遍如何實現,但幸運的是,已經有人替我這麼作了.Dave Eberly(譯註:《3D Game Engine Architecture》一書的做者)已經在他的網站Magic-Software.com上撰寫了一篇完善的文檔,用以解釋球-圓錐相交斷定的算法.
譯者:事實上,Magic-Software.com彷佛已經易主了...原文中的連接也天然不在了,你能夠在百度文庫中找到一個副本: http://wenku.baidu.com/view/6388b482e53a580216fcfe9c
我認爲這個網站是一個超棒的資料庫,裏面有大量的代碼和文檔(譯者:但是TMD已經不在了啊...).他的文檔即詳細解釋了原理,又給出了完整的代碼.事實上,若是你沒有雄厚的數學背景的話,你也能夠簡單地把結尾的代碼複製而後直接使用.雖然我不推薦這樣作,但有些人就是不能徒手將天然底數算到小數點後第⑨位,對於像譯者那樣的數死早,我只能說呵♂呵.那麼如今咱們已經有了兩個高貴上檔次的新算法了...可它們能用來幹啥?很好,這就是下一章的內容...
Yep,你猜到了.那兩個算法是用來進一步優化咱們的視錐剔除算法的.正如我在上一章的開頭所說,平截頭體檢測實際上是很慢的,能避免的話就不要進行與它相關的操做.你應該也注意到球-球檢測和球-錐檢測要比平截頭體檢測要快一些.因此咱們能夠把它們做爲預檢測算法,利用它們的高速,提早剔除掉那些根本不可能相交的物體,僅讓可能相交的物體進行平截頭體檢測.接下來咱們要作的是爲平截頭體構建一個球體和圓錐.以後在檢測時,咱們讓物體的渲染斷定球和平截頭體的球進行檢測,經過後再與圓錐進行檢測,依然經過後則再與平截頭體進行檢測.這種算法看上去很麻煩,其實對計算機而言計算起來遠比直接檢測平截頭體要快得多.
建立一個包裹着平截頭體的球體並不難(譯者:尼瑪我怎麼就不會...),但建立一個合適的圓錐就不容易了.建立球體的方法是讓球的球心位於平截頭體的中央,讓它的半徑可以恰好到平截頭體的遠角(遠裁面的任意一個角).爲何是遠角?好吧,平截頭體其實不就是一個向外擴張的被砍掉頭的棱柱嗎?不管你的視野再怎麼遠,也遠不過平截頭體的遠角,換句話說,若是半徑可以到它,那麼球體就能完整地包裹下整個平截頭體.你也許會問爲什麼不是近角(近裁面的角),這給就是純幾何問題了,請看圖3.定位球體中心位置的過程絕不費力.將近裁面與遠裁面之間中間的那個點定爲中點就足夠了.而計算球體半徑有一點小難.一般平截頭體是由FOV(視野範圍,一個角度,一般爲75)決定.利用FOV,咱們能經過幾何方法計算出從球心到遠角的距離.咱們將原點定爲鏡頭位置,X軸Y軸定爲與近/遠裁面平行,Z軸定爲從原點指向遠裁面中點的方向.將P點定爲球心,Q定爲一個遠點,則P能夠被表示爲(0,0,nearClip + ((farClip + nearClip) / 2)),其中nearClip和farClip分別爲原點到近裁面和遠裁面的距離.下面是個人代碼:
//計算平截頭體球的半徑 //首先計算遠裁面與近裁面的距離 float fViewLen = m_fFarPlane - m_fNearPlane; //計算遠裁面的高,這裏有個問題,FOV一般是從視角原點開始算的,這裏應該用m_fFarPlane而不是fViewLen纔對,做者的筆誤? float fHeight = fViewLen * tan(m_fFovRadians * 0.5f); //在橫縱比爲1時,寬和高同樣,不然還要再計算寬... float fWidth = fHeight; //肯定P點 Vector3f P(0.0f, 0.0f, m_fNearPlane + fViewLen * 0.5f); //肯定Q點 Vector3f Q(fWidth, fHeight, fViewLen); //得到一個半徑向量 Vector3f vDiff(P - Q); //因而球體半徑就是模長了 m_frusSphere.Radius() = vDiff.getLength(); //而後咱們要肯定這個球在全局座標系中的位置 Vector3f vLookVector; m_mxView.LookVector(&vLookVector); //計算球體中心在全局座標中的位置 m_frusSphere.Center() = m_vCameraPosition + (vLookVector * (fViewLen * 0.5f) + m_fNearPlane);
構建包圍平截頭體的圓錐則相對簡單一些.若是你讀了那篇介紹球-錐相交斷定的文章的話,你應該知道一個圓錐能夠經過一個頂點(圓錐的原點),一個軸射線(指向圓錐的方向)以及一個錐角(母線和軸射線的夾角)組成.圓錐的頂點就是鏡頭的原點.軸射線就是鏡頭面向的方向.惟一的難度在於錐角的計算.若是咱們只是簡單地用FOV做爲錐角的話,那麼咱們建立出的圓錐沒法包裹住平截頭體的四個遠角.因此仍是要經過計算來算出一個合適的錐角.利用一些幾何學技巧,咱們能夠算出新的FOV.既然咱們已經有平截頭體的FOV,咱們就能利用勾股定理,使用舊FOV(以及屏幕的尺寸)計算出FOV三角形的臨邊,而後再用臨邊與屏幕中點到邊角的線段組成一個新的三角形,進而計算出新的FOV.聽起來有些略繁瑣,能夠看下面的代碼:
// vLookVector是鏡頭的方向向量 // Position()爲獲取鏡頭的位置 // fWidth爲屏幕寬度的一半(單位:像素). // fHeight爲屏幕高度的一半(單位:像素). // m_fFovRadians是平截頭體的FOV // 計算FOV三角形的臨邊 float fDepth = fHeight / tan(m_fFovRadians * 0.5f); // 計算從屏幕中點到邊角的距離 float fCorner = sqrt(fWidth * fWidth + fHeight * fHeight); // 計算新的FOV float fFov = atan(fCorner / fDepth); // 初始化圓錐 m_frusCone.Axis() = vLookVector; m_frusCone.Vertex() = Position(); m_frusCone.SetConeAngle(fFov);[/code]
///對節點的遞歸處理 void QuadTree::RecurseProcess(Camera* pPovCamera, QuadNode* pNode, bool bTestChildren) { //在裁剪前是否須要檢測? if(bTestChildren) { //首先檢測是不是在檢測盒內 if(pNode->m_bbox.ContainsPoint(pPovCamera->Position()) == NOT_INSIDE) { //檢測咱們是否和平截頭體的球體相交 if(!pPovCamera->FrustrumSphere().Intersects(pNode->m_sphere)) return; //檢測咱們是否和平截頭體的圓錐相交 if(!TestConeSphereIntersect(pPovCamera->FrustumCone(), pNode->m_sphere)) return; //先進行球體檢測 switch(pPovCamera->Frustrum().ContainsSphere(pNode->m_sphere)) { case OUT: return; case IN: bTestChildren = false; break; case INTERSECT: //檢測立方體是否在視角內 switch(pPovCamera->Frustrum().ContainsAaBox(pNode->m_bbox)) { case IN: bTestChildren = false; break; case OUT: return; } break; } } } //[在這裏填入額外的判斷和渲染代碼] }
我自認爲這是一篇挺不錯的視錐剔除入門文章,我但願它能對新手圖像引擎程序員有幫助.若是你仔細讀下來的話,你也會發現本文除了譯者糟糕的翻譯之外,沒有什麼過於複雜的概念和大筆大筆的數學公式(事實上,數學公式都在引用的兩篇文章裏...).誠然,我介紹的算法並不是最佳的視錐剔除算法,更況且對於這種算法而言,仍有很多進一步優化的方法,但對於入門而言,它已經足夠了.若是你發現文中有含糊不清的地方的話,若是確保不是譯者犯二的話就儘管找我抗議吧!
我很感謝個人好基友Gil Gribb(譯註:Raven公司員工,參與過包括《戰爭機器》、《命運戰士》等多款遊戲的製做)和Klaus Hartmann(譯者:《哈德曼的妖怪少女》...歪樓了(づ ̄3 ̄)づ),感謝他們在裁面提取方面的優秀的文章.我也很感謝Dave Eberly在圓錐算法方面的幫助,以及他那個一應俱全的網站(儘管已關閉)!另外,感謝Charles Bloom在網上撰寫的一篇文章,它啓發了我使用球體和圓錐來進行快速剔除.人類的最大美德莫過於知識的分享,以致於我除了發自心底地感謝那些願意將本身的知識分享給他人的人之外,別無他言.
另外記住...練習纔是進步的最佳捷徑 - 百試百靈,無效退款.我(口頭)保證!
譯者:既然做者寫完了那麼就輪到我來蛋逼了!(~o ̄▽ ̄)~o 好久沒作翻譯了...上一次翻技術文章仍是去年暑假...老實說在計算機圖形學我仍是個猹啊,專業名詞方面我儘可能作到規整,不過仍是翻得有些213...好吧很少說了,撤了...ԅ(¯﹃¯ԅ)