實驗平臺:Win7,VS2010html
先上結果截圖:算法
本文是我前一篇博客:OpenGL陰影,Shadow Mapping(附源程序)的下篇,描述兩個最經常使用的陰影技術中的第二個,Shadow Volumes 方法。將從基本原理出發,首先講解 Zpass 方法,而後是 Zfail 方法(比較實際的方法),最後對 Shadow Mapping 和 Shadow Volumes 方法作簡要分析對比。編程
Shadow Volumes 須要網格的鏈接信息,本文使用 VCGlib 庫 構造拓撲信息及讀寫網格文件,爲了清晰,將 VCGlib 使用的簡單總結做爲附錄,附於文章的最後。數組
1. 數學原理性能優化
關於陰影的定義,請見個人前一篇博客(文獻[1])。Shadow Mapping 將空間各個方向上離光源最近點的距離編碼成深度紋理。Shadow Volumes 採用一種不一樣的方法,它直接構造光源被物體(投射陰影的物體,Shadow caster)遮擋的空間的邊界,即落在這個邊界內的任何點都處於陰影中,反之被光源照亮,以下圖所示(使用Blender製做,另見文獻[3]PPT第10頁):數據結構
遮擋空間邊界所包圍的空間即爲 Shadow Volume (陰影體積),構造 Shadow Volume 並不困難,對上圖中的三角形(設頂點爲 A,B,C)只須要從光源點到三角形頂點作連線並延伸出去到足夠遠(設 A,B,C 延伸到點 D,E,F),並用這些多邊形構成封閉體積:面ABC、面ADEB、面BEFC、面CFDA、面EDF,共5個面,注意頂點字母的順序已經考慮了頂點環繞方向向外(右手法則)。app
那如何判斷一個點是否位於 Shadow Volume 內部呢? Shadow Volumes 採用一種間接方法:從一個位於全部 Shadow Volume 外的點出發做射線,從 0 開始計數,每穿入一個 Shadow Volume +1,每穿出一個 Shadow Volume -1,這樣到達點 P 時,若是計數爲 0 說明位於陰影體積外,大於 0 說明在一層或多層 Shadow Volume 內部。原理是,每一個 Shadow Volume 都是封閉的,若是點 P 位於全部 Shadow Volume 外,則穿入和穿出必成對出現,有一種極端狀況:射線與一個 Shadow Volume 相切於棱邊上,這時射線與 Shadow Volume 表面只有 1 個交點而不是一般的 2 個交點(Shadow Volume 爲凸時),好在,這裏說的幾何原理的實際實現使用光柵化進行離散化,在離散化空間中,這種極端狀況並不存在(這和光柵化特性有關,如 "watertight" rasterization 見文獻[3])。這個原理以下圖所示(摘自文獻[3]PPT第18頁,二維示意):ide
這個計數的起點其實就是攝像機所在點,計數的任務能夠由圖形硬件的 Stencil Buffer (模板緩衝)機制提供,能夠看到,這裏要求攝像機位於陰影以外。函數
2. Zpass 方法性能
直接實現第1節的數學原理的方法即爲 Zpass 方法。實現 Zpass 須要完成兩方面工做:構造 Shadow Volume 、利用 Stencil Buffer 的功能實現計數。咱們先來看最簡單的狀況,場景中只有兩個三角形和一個地板,以下圖(看到陰影對判斷空間位置的重要性):
場景代碼以下:
// 世界,四邊形地板 void draw_world() { glStaff::xyz_frame(2, 2, 2, false); glBegin(GL_POLYGON); glNormal3f(0, 1, 0); glVertex3f(-5, 0,-5); glVertex3f(-5, 0, 5); glVertex3f(5, 0, 5); glVertex3f(5, 0,-5); glEnd(); } glm::vec3 tri1[3] = { glm::vec3(0, 3, 0), glm::vec3( 0, 3, 2), glm::vec3(2, 3, 0) }; glm::vec3 tri2[3] = { glm::vec3(1, 2,-1), glm::vec3(-1, 2,-1), glm::vec3(1, 2, 1) }; // 模型,兩個三角形 void draw_model() { GLfloat _ca[4], _cd[4]; glGetMaterialfv(GL_FRONT, GL_AMBIENT, _ca); glGetMaterialfv(GL_FRONT, GL_DIFFUSE, _cd); GLfloat c[4]; glBegin(GL_TRIANGLES); c[0]=1; c[1]=0; c[2]=0; c[3]=1; glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, c); glNormal3fv(&glm::normalize(glm::cross(tri1[1]-tri1[0], tri1[2]-tri1[0]))[0]); for(int i=0; i<3; ++i) glVertex3fv(&tri1[i][0]); // tri1,紅色 c[0]=0; c[1]=1; c[2]=0; c[3]=1; glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, c); glNormal3fv(&glm::normalize(glm::cross(tri2[1]-tri2[0], tri2[2]-tri2[0]))[0]); for(int i=0; i<3; ++i) glVertex3fv(&tri2[i][0]); // tri2,綠色 glEnd(); glMaterialfv(GL_FRONT, GL_AMBIENT, _ca); glMaterialfv(GL_FRONT, GL_DIFFUSE, _cd); }
構造 Shadow Volume 代碼以下(light_pos 爲光源位置,位置式光源):
static float d_far = 10; // 構造、繪製 Shadow Volume,僅考慮位置光源 void draw_model_volumes() {for(int t=0; t<2; ++t){ glm::vec3* tri = t==0 ? tri1 : tri2; // tri1 or tri2 glm::vec3 tri_far[3]; for(int i=0; i<3; ++i){ tri_far[i] = tri[i] + glm::normalize(tri[i]-glm::vec3(light_pos))*d_far; } for(int i=0; i<3; ++i){ glBegin(GL_POLYGON); // 三個邊擠出(extrude)的四邊形 glVertex3fv(&tri[i][0]); glVertex3fv(&tri_far[i][0]); glVertex3fv(&tri_far[(i+1)%3][0]); glVertex3fv(&tri[(i+1)%3][0]); glEnd(); } glBegin(GL_TRIANGLES); // 頂部(near cap),原三角形,對 Zpass 來講可選
for(int i=0; i<3; ++i) glVertex3fv(&tri[i][0]); glEnd(); glBegin(GL_TRIANGLES); // 底部(far cap),擠出三角形,對 Zpass 來講可選
for(int i=0; i<3; ++i) glVertex3fv(&tri_far[2-i][0]); glEnd(); } }
構造的 Shadow Volume 以下圖所示:
Stencil Buffer 實現計數代碼:
// ------------------------------------------ 清除緩衝區,包括模板緩衝 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // ------------------------------------------ 第1遍,渲染環境光,深度值 // 關閉光源,打開環境光 GLboolean _li0 = glIsEnabled(GL_LIGHT0); if(_li0) glDisable(GL_LIGHT0); glMatrixMode(GL_MODELVIEW); glLoadMatrixf(&mat_view[0][0]); draw_world(); glMultMatrixf(&mat_model[0][0]); draw_model(); if(_li0) glEnable(GL_LIGHT0); // ------------------------------------------ 第2遍,渲染模板值 // 不須要光照,不更新顏色和深度緩衝 GLboolean _li = glIsEnabled(GL_LIGHTING); if(_li) glDisable(GL_LIGHTING); glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glDepthMask(GL_FALSE); glStencilMask(~0); glEnable(GL_CULL_FACE); glEnable(GL_STENCIL_TEST); glStencilFunc(GL_ALWAYS, 0, ~0); // 剔除背面留下正面,穿入,模板值 加 1 glCullFace(GL_BACK); glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); glMatrixMode(GL_MODELVIEW);glLoadMatrixf(&mat_view[0][0]);glMultMatrixf(&mat_model[0][0]); draw_model_volumes(); // 剔除正面留下背面,穿出,模板值 減 1 glCullFace(GL_FRONT); glStencilOp(GL_KEEP, GL_KEEP, GL_DECR); glMatrixMode(GL_MODELVIEW);glLoadMatrixf(&mat_view[0][0]);glMultMatrixf(&mat_model[0][0]); draw_model_volumes(); // 恢復狀態 if(_li) glEnable(GL_LIGHTING); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDepthMask(GL_TRUE); glStencilMask(~0); glDisable(GL_CULL_FACE); glDisable(GL_STENCIL_TEST); glStencilOp(GL_KEEP,GL_KEEP,GL_KEEP); // ------------------------------------------ 第3遍,渲染光源光照,依據模板值判斷陰影 // 關閉環境光,打開光源 GLfloat _lia[4]; glGetFloatv(GL_LIGHT_MODEL_AMBIENT, _lia); GLfloat ca[4]={0}; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ca); // 模板測試爲,等於0經過, 深度測試爲,相等經過,顏色混合爲直接累加 glEnable(GL_STENCIL_TEST); glStencilFunc(GL_EQUAL, 0, ~0); glDepthFunc(GL_EQUAL); glBlendFunc(GL_ONE, GL_ONE); glMatrixMode(GL_MODELVIEW); glLoadMatrixf(&mat_view[0][0]); glLightfv(GL_LIGHT0, GL_POSITION, &light_pos[0]); // 位置式光源 draw_world(); glMultMatrixf(&mat_model[0][0]); draw_model(); // 恢復狀態 glLightModelfv(GL_LIGHT_MODEL_AMBIENT, _lia); glDisable(GL_STENCIL_TEST); glStencilFunc(GL_ALWAYS, 0, ~0); glDepthFunc(GL_LESS); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // 在光源處繪製一個黃色的球 glMatrixMode(GL_MODELVIEW); glLoadMatrixf(&mat_view[0][0]); dlight(0.05f);
這裏要用到 Stencil Buffer,要在建立窗口時(即建立 OpenGL Context)啓用 Stencil Buffer,GLFW 默認就啓用了(8-bit)。第1遍渲染時,僅開啓環境光,渲染場景後,顏色緩衝是環境光貢獻,深度緩衝是離攝像機最近的片段的深度。第2遍渲染,只更新 Stencil Buffer,由於深度緩衝已經保存了最近片段深度,深度測試 GL_LESS 經過的片段都是未經遮擋的 Shadow Volume 部分,若是看到了正面,模板值+1,背面-1,注意正背面是依據頂點環繞方向肯定的(光柵化的任務),由於是深度測試經過後計數故稱做 Zpass 。第3遍渲染,由於模板值爲0的點爲光照,不然爲陰影,設置模板測試爲和0比較相等時經過,並設置混合函數爲直接累加(和 Shadow Mapping 相似)。
模板緩衝區的值(全黑爲模板值爲0,每一個顏色梯度模板值變化1),以及最終渲染結果以下圖所示:
讀取模板緩衝區使用 glReadPixels(ox,oy, width,height, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, data),上面全部代碼見所附程序中的 volumes_basic0.cpp。
在講輪廓邊以前,先看下上面代碼幾個須要改進的地方:
上面代碼的 「第2遍,渲染模板值」 的繪製部分等價代碼以下:
// 不須要光照,不更新顏色和深度緩衝 // ... // 正面加1,背面減1 glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_INCR_WRAP); // 改進後 glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_DECR_WRAP); glMatrixMode(GL_MODELVIEW); glLoadMatrixf(&mat_view[0][0]); glMultMatrixf(&mat_model[0][0]); draw_model_volumes(glm::affineInverse(mat_model)*light_pos); // 恢復狀態 // ...
將三角形邊擠出到無窮遠的代碼以下(考慮三角形是否背對光源):
// 構造、繪製 Shadow Volume,擠出(extrude)到無窮遠 void draw_model_volumes(glm::vec4& lpos) { for(int t=0; t<2; ++t){ glm::vec3* tri = t==0 ? tri1 : tri2; // tri1 or tri2 glm::vec4 tri_far[3]; for(int i=0; i<3; ++i){ tri_far[i] = glm::vec4( tri[i].x*lpos.w-lpos.x, tri[i].y*lpos.w-lpos.y, tri[i].z*lpos.w-lpos.z, 0); } glm::vec3 n = glm::cross(tri[1]-tri[0], tri[2]-tri[0]); glm::vec3 l0 = lpos.w==0 ? glm::vec3(lpos) : glm::vec3(lpos)/lpos.w-tri[0]; int m = glm::dot(n,l0)>=0 ? 1 : -1; // 是否反轉四邊形環繞方向 for(int i=0; i<3; ++i){ glBegin(GL_POLYGON); // 三個邊擠出(extrude)的四邊形 glVertex3fv(&tri[i][0]); glVertex4fv(&tri_far[i][0]); glVertex4fv(&tri_far[(i+m+3)%3][0]); glVertex3fv(&tri[(i+m+3)%3][0]); glEnd(); } } }
位置光源和平行光源的對好比下:
這部分代碼見所附程序中的 volumes_basic1.cpp。
到目前爲止,咱們的場景過於簡單,如今考慮複雜的網格,這裏僅考慮質量好的三角網格(封閉,任意點爲二維流形,manifold,即每一個邊接兩個面,面之間無交叉)。咱們使用 VCGlib,關於用 VCGlib 讀寫網格文件、構造頂點邊面鏈接信息、法向量計算、平滑等處理請見本文最後的附錄。最簡單的將上述方法擴展到複雜網格的方法是:對每一個三角形都構造 Shadow Volume ,對一個 mesh 的每一個三角形構造 Shadow Volume 的代碼以下(讀入的 PLY 網格文件已經預先用 Blender 和 MeshLab 處理爲 manifold 三角網格,關於 VCGlib 的使用見最後的附錄):
// 構造、繪製 Shadow Volume void draw_model_volumes(GLMesh& mesh, glm::vec4& lpos) { assert(mesh.FN()==mesh.face.size()); // vcg::tri::Allocator<>::CompactFace/Edge/VertexVector() for(int i=0; i<mesh.FN(); ++i){ // for each face (i.e. triangle) GLMesh::FaceType& f = mesh.face[i]; glm::vec4 tri_far[3]; // 擠出的3個點,到無窮遠 for(int i=0; i<3; ++i){ tri_far[i] = glm::vec4( f.V(i)->P().X()*lpos.w-lpos.x, f.V(i)->P().Y()*lpos.w-lpos.y, f.V(i)->P().Z()*lpos.w-lpos.z, 0 ); } glm::vec3 n( vcg_to_glm(f.N()) ); glm::vec3 l0 = lpos.w==0 ? glm::vec3(lpos) : glm::vec3(lpos)/lpos.w - vcg_to_glm(f.V(0)->P()); int m = glm::dot(n,l0)>=0 ? 1 : -1; // 是否反轉四邊形環繞方向 for(int i=0; i<3; ++i){ glBegin(GL_POLYGON); // 三個邊擠出(extrude)的四邊形 glVertex3fv(&f.V(i)->P()[0]); glVertex4fv(&tri_far[i][0]); glVertex4fv(&tri_far[(i+m+3)%3][0]); glVertex3fv(&f.V((i+m+3)%3)->P()[0]); glEnd(); } } }
程序結果以下:左上爲最終結果;右上爲對應 Stencil 值(顏色梯度表示變化 1,能夠想見 Stencil 的更新很是頻繁,但由於都是+1和-1操做,因此累積值並不必定很大);下面是 Shadow Volume 的顯示,能夠看到,由於每一個三角形都構造 Shadow Volume,Shadow Volume 的線條很是密。渲染時間約 180ms:
並不須要對全部邊都進行擠出(extrude),只須要對某些被稱做 「輪廓邊」 的邊(準確的說是 「可能輪廓邊」)進行擠出就能夠構造出合格的 Shadow Volume,「可能輪廓邊」 是指其所鏈接的兩個面(對 manifold 網格每一個邊必鏈接兩個面)一個面對光源另外一個背對光源。面對仍是背對光源能夠用三角形面法向量和光源到三角形上任一點連線向量的內積的正負號判斷,優化後的,只對 「可能輪廓邊」 進行擠出的代碼以下,注意和上面不一樣,此時對邊進行遍歷,而再也不是三角形,注意要保證四邊形環繞方向爲向外:
// 構造、繪製 Shadow Volume void draw_model_volumes(GLMesh& mesh, glm::vec4& lpos) { assert(mesh.EN()==mesh.edge.size()); for(int i=0; i<mesh.EN(); ++i){ GLMesh::EdgeType& e = mesh.edge[i]; GLMesh::FaceType* fa = e.EFp(); // fa,fb 爲邊 e 鄰接的兩個面 GLMesh::FaceType* fb = fa->FFp(e.EFi()); glm::vec3 l0 = lpos.w==0 ? glm::vec3(lpos) : glm::vec3(lpos)/lpos.w-vcg_to_glm(e.V(0)->P()); int sa = glm::dot(l0, vcg_to_glm(fa->N()))>=0 ? 1 : -1; // 面對仍是背對光源 int sb = glm::dot(l0, vcg_to_glm(fb->N()))>=0 ? 1 : -1; if( sa*sb < 0 ){ // 一個面面對,一個面背對光源,「可能輪廓邊」 GLMesh::VertexType* va = fa->V(e.EFi()); GLMesh::VertexType* vb = fa->V((e.EFi()+1)%3); if(sa<0) std::swap(va, vb); // 肯定頂點順序,是最終四邊形環繞方向向外 glm::vec4 e_far[2]; // 擠出的2個點,到無窮遠 e_far[0] = glm::vec4( va->P().X()*lpos.w-lpos.x, va->P().Y()*lpos.w-lpos.y, va->P().Z()*lpos.w-lpos.z, 0 ); e_far[1] = glm::vec4( vb->P().X()*lpos.w-lpos.x, vb->P().Y()*lpos.w-lpos.y, vb->P().Z()*lpos.w-lpos.z, 0 ); glBegin(GL_POLYGON); // 邊擠出(extrude)的四邊形 glVertex3fv(&va->P()[0]); glVertex4fv(&e_far[0][0]); glVertex4fv(&e_far[1][0]); glVertex3fv(&vb->P()[0]); glEnd(); } } }
再看結果,對比上面的圖,如今 Shadow Volume 的邊稀疏多了,且渲染時間減小到了 45ms:
Zpass 方法的第一個問題是:當攝像機位於陰影中時,光照處 Stencil 值將再也不爲0,見下面的例子:
這個問題能夠經過檢測攝像機是否位於陰影中,並在攝像機位於陰影中時對 Stencil 值進行偏移進行解決,但這須要額外開銷,後面用 Zfail 方法避免這一問題。和想象中的不一樣,攝像機並非 「要麼在陰影中,要麼在陰影外」 ,它有可能 「一半位於陰影中,一半位於陰影外」,這實際上是近裁剪面的做用:
近裁剪面問題是 Zpass 方法的第二個問題,詳見文獻[3]。這小節代碼見所附程序中的 volumes_zpass.cpp。
3. Zfail 方法,實際方法
Zpass 失敗的緣由,以及 Zfail 方法的原理以下圖所示(摘自文獻[4],a. Zpass 原理,b. Zpass 失敗例子,c. Zfail 方法原理):
Zpass 從攝像機發出射線到無窮遠並計數,而 Zfail 正好相反,它從攝像機射線的窮遠處到攝像機計數,當 Shadow Volume 封閉時且攝像機位於陰影外時,Zpass 和 Zfail 是等價的,由於:一條射線和封閉的 Shadow Volume 老是交於兩個點(凸時,非凸時老是偶數個交點,前面已經分析了,極端狀況在離散空間並不存在),若點 P 在某 Shadow Volume 中,Zpass 和 Zfail 對該 Shadow Volume 計數結果都爲+1,若 P 在該 Shadow Volume 外,則 Zpass 和 Zfail 計數結果爲 「0 和 +1-1」 或者 「+1-1 和 0」,此兩種狀況都是等價的說明了 Zfail 的正確性。
Zfail 較 Zpass 有更好的特性:
但其也有缺點須要克服:
實現 Zfail 計數是直接的:
對網格構造 Shadow Volume 的代碼和以前稍有區別,須要 cap:
Zfail 代碼見所附程序中的 Volumes_zfail.cpp。程序結果以下圖所示,如今攝像機位於陰影中也不會有問題了,但渲染幀率也從 23fps 降到了 18 fps:
「實際方法」 一詞出自文獻[3],這篇 2002 年的文章經過使用 Zfail 並將攝像機遠裁剪面設置於無窮遠處,改進了 Shadow Volumes 方法,更值得一提的是,它提到的 wrap 方式 Stencil 值更新、Depth Clamping、Two-Sided Stencil Testing 後來都已是 OpenGL 標準了,這使得咱們能夠以更簡潔的方式實現 Shadow Volumes。
多個光源的處理和 Shadow Mapping 相似,下面是結果,代碼見所附程序中的 volumes_multi_lights.cpp,關於平行光,由於已經利用齊次座標特性考慮了光源 w 座標,只需將光源座標 w 份量設爲 0 便可實現平行光:
4. 進一步研究
低質量網格(non-manifold 網格) Shadow Volume 構造見文獻[4],另外文獻[4]給出了用幾何着色器構造 Shadow Volume 的代碼,經過裁剪 Shadow Volume 或交替使用 Zpass/Zfail 減少對像素填充率(須要光柵化的多邊形面積)消耗的性能優化方法見文獻[1]的文獻[1]及文獻[4],基於 Shadow Volumes 的 Soft Shadow 方法見文獻[1]的文獻[1]。
5. Shadow Volumes VS. Shadow Mapping
先來看同一個場景用 Shadow Volumes 和 Shadow Mapping 兩種方法渲染的對比圖(個人機器配置:Pentium Dual-Core 2.6 GHz,4 GB DDR2,GT240 1GB GDDR5 OpenGL 3.3),代碼見所附程序中的 comparison_volumes_mapping.cpp。
第一個場景,2000 個正方體,每一個正方體有 8 個頂點、12 個三角形,下面依次是無陰影、Shadow Volumes、Shadow Mapping 渲染結果,渲染時間和幀率在圖中左上角和左下角(幀率結果包含所有CPU時間和GPU時間,更具綜合性),Shadow Volumes 使用本文最後的 Zfail 方法,Shadow Mapping 使用 2048x2048 陰影圖:
第二個場景,50 個猴頭模型,每一個猴頭模型有 28.9K 個頂點、57.8K 個三角形,程序結果以下:
對上圖做放大觀察,Shadow Volumes 和 Shadow Mapping 方法的結果以下,能夠看到 Shadow Volumes 放大後毫無鋸齒,而 Shadow Mapping 方法已經有輕微鋸齒:
須要指出的是,這裏實現的 Shadow Volumes 和 Shadow Mapping 能夠進一步優化,如使用頂點列表、使用顯示列表、優化幾何數據結構、若是可能重用陰影圖或陰影體積等等,因此上面的性能比較結果並不很準確,這裏只想給出一個參考。
對 Shadow Volumes 和 Shadow Mapping 做以下分析對比:
下載連接:程序集成了上一博客 Shadow Mapping 的源代碼,並支持64位,好多庫是純頭文件,爲了加快編譯速度,使用了預編譯頭,請見代碼中註釋,工程的配置見程序文件夾下 「說明.txt」。
連接: http://pan.baidu.com/s/1i3oXHSL 密碼: agx5
(左Ctrl+鼠標左鍵拖拽改變視角,鼠標滾輪縮放)
參考文獻
*******************************************************************************
附錄:VCGlib 庫 使用說明
先來看看 VCGlib 能作什麼:
VCGlib 的文檔很簡陋,在線文檔並非很全,能夠本身用 Doxygen 從下載的源代碼生成 html API 文檔,爲此只須要(Windows 用戶):
VCGlib 是純頭文件庫,要安裝只需將下載 VCGlib 庫目錄添加到程序的頭文件包含路徑(有些IO函數如讀寫PLY須要包含相應.cpp文件)。
後面按照以下步驟講解:
定義 Mesh 類型的典型代碼以下(API 文檔主頁 Basic Concepts,在線版):
#include "vcg/complex/complex.h" // 類型聲明 class MyVertex;class MyEdge; class MyFace; typedef vcg::UsedTypes< vcg::Use<MyVertex>::AsVertexType, vcg::Use<MyEdge> ::AsEdgeType, vcg::Use<MyFace> ::AsFaceType > MyUsedTypes; // 頂點類型 class MyVertex : public vcg::Vertex<MyUsedTypes, vcg::vertex::Coord3f, vcg::vertex::Normal3f, vcg::vertex::BitFlags > { }; // 邊類型 class MyEdge : public vcg::Edge<MyUsedTypes, vcg::edge::VertexRef, vcg::edge::EFAdj, vcg::edge::BitFlags > { }; // 面類型,三角形 class MyFace : public vcg::Face<MyUsedTypes, vcg::face::VertexRef, vcg::face::Normal3f, vcg::face::FFAdj, vcg::face::BitFlags > { }; // 網格類型 typedef vcg::tri::TriMesh< std::vector<MyVertex>, std::vector<MyEdge>, std::vector<MyFace> > GLMesh;
拋開 MyUseTypes 不看,上面代碼定義的網格類型爲:
VCGlib 使用 Reference 數據結構,對每一個邊、面用指針記錄其頂點、鄰接面等信息,其餘網格數據結構見 wikipedia Polygon Mesh 條目。
爲了作到足夠通用,VCGlib 使用了C++ template metaprogramming(模板元編程)方法。上面代碼中的 MyVertex、MyEdge、MyFace、GLMesh 等類型包含哪些屬性(模板參數)、屬性的順序(模板參數順序)都是能夠根據須要隨意指定的(固然,必須包含足夠的屬性以執行相應網格算法),通常來講,最好使頂點、邊、麪包含標誌位屬性(BitFlags),BitFlags 指示該頂點、邊、面是否可寫、可讀、已刪除(爲了效率,例如,刪除頂點操做可能並不當即刪除頂點數據,而僅僅打個標誌位,待全部操做完成再更新頂點數據)等。不去深刻講解 VCGlib 元編程機理(說實話我還沒弄清楚),可選個數模板參數是經過默認模板參數實現的,vcg::Vertex/Edge/Face<> 將繼承其模板參數。
下面列舉全部可選的模板參數:
訪問 Mesh 數據示例代碼以下:
// load mesh ...
int i=0, j=0; // 見 vcg::tri::TriMesh<> ------------------------------------------------------------- mesh.VN(); mesh.EN(); mesh.FN(); // 頂點、邊、面個數,可能小於 vs/es/fs.size() // 由於有些元素被刪除時僅僅打了標誌位而並未刪除存儲數據 std::vector<GLMesh::VertexType>& vs = mesh.vert; // 頂點數組 std::vector<GLMesh::EdgeType>& es = mesh.edge; // 邊數組 std::vector<GLMesh::FaceType>& fs = mesh.face; // 面數組 // 見 vcg::Vertex<> 及其 模板參數 ------------------------------------------------------- GLMesh::VertexType& v = mesh.vert[i]; // 第 i 個頂點,假設 v.isD()==false,即未標誌爲已刪除 v.P().Z(); v.P().V(j); // 頂點座標,其xyz份量 v.N().X(); // 頂點法向,其x份量 // 見 vcg::Edge<> 及其 模板參數 --------------------------------------------------------- GLMesh::EdgeType& e = mesh.edge[i]; // 第 i 個邊,假設 e.isD()==false GLMesh::VertexType* pve = e.V(j); // j=0,1,邊的兩個端點頂點的指針 GLMesh::FaceType* pfa = e.EFp(); // 邊-面鄰接信息,該邊鏈接的第一個面 // 見 vcg::Face<> 及其 模板參數 --------------------------------------------------------- GLMesh::FaceType& f = mesh.face[i]; // 第 i 個面(三角形),假設 f.isD()==false GLMesh::VertexType* pvf = f.V(j); // j=0,1,2,三角形面的三個頂點的指針 f.N(); // 面的法向量 GLMesh::FaceType* pfb = f.FFp(j); // 面-面鄰接信息,j=0,1,2,面 f 經過其第j個邊鏈接的第一個面 // 能夠經過返回的引用(左值)修改數據,但不要隨便修改,見下文 ------------------------------------ v.P().Y() += 3.2f; e.V(j) = &v; f.V(j) = &v; // 遍歷全部頂點、邊、面須要跳過標記爲已刪除的元素 --------------------------------------------- for(size_t i=0; i<vs.size(); ++i){ if(vs[i].IsD()) continue; // do some thing for each vertex vs[i] ... } // 除非已經刪除了全部標記爲已刪除元素的存儲數據,好比: vcg::tri::Allocator<GLMesh>::CompactVertexVector(mesh); vcg::tri::Allocator<GLMesh>::CompactEdgeVector(mesh); vcg::tri::Allocator<GLMesh>::CompactFaceVector(mesh); for(size_t i=0; i<fs.size(); ++i){ // do some thing for each face fs[i] ... }
填充(Fill)Mesh 數據的示例代碼以下(API 文檔主頁 Creating and destroying elements,在線版,代碼摘自那裏):
// VCGlib Reference 數據結構,依賴於指針,直接操做頂點、邊、面數組 mesh.vert/edge/face 可能 // 產生 std::vector<> 存儲從新分配,此時,相關指針將失效,vcg::tri::Allocator<> 處理這些問題 GLMesh m; GLMesh::VertexIterator vi = vcg::tri::Allocator<GLMesh>::AddVertices(m, 3); GLMesh::FaceIterator fi = vcg::tri::Allocator<GLMesh>::AddFaces(m, 1); GLMesh::VertexPointer ivp[4]; ivp[0]=&*vi; vi->P()=GLMesh::CoordType(0.0f,0.0f,0.0f); ++vi; ivp[1]=&*vi; vi->P()=GLMesh::CoordType(1.0f,0.0f,0.0f); ++vi; ivp[2]=&*vi; vi->P()=GLMesh::CoordType(0.0f,1.0f,0.0f); ++vi; fi->V(0)=ivp[0]; fi->V(1)=ivp[1]; fi->V(2)=ivp[2]; // Alternative, more compact, method for adding a single vertex ivp[3]= &*vcg::tri::Allocator<GLMesh>::AddVertex(m,GLMesh::CoordType(1.0f,1.0f,0.0f)); // Alternative, method for adding a single face (once you have the vertex pointers) vcg::tri::Allocator<GLMesh>::AddFace(m, ivp[1],ivp[0],ivp[3]); // 同理,若是本身保存了頂點等數據指針,須要在修改頂點、邊、面數組後更新該指針 -------------------- // a potentially dangerous pointer to a mesh element GLMesh::FacePointer fp = &m.face[0]; vcg::tri::Allocator<GLMesh>::PointerUpdater<GLMesh::FacePointer> pu; // now the fp pointer could be no more valid due to eventual re-allocation of the m.face vcg::tri::Allocator<GLMesh>::AddVertices(m,3); vcg::tri::Allocator<GLMesh>::AddFaces(m,1,pu); // check if an update of the pointer is needed and do it. if(pu.NeedUpdate()) pu.Update(fp); // 能夠想見,pu 保存了地址偏移信息,只需將 fp 偏移 // 刪除元素的代碼以下 -------------------------------------------------------------------- vcg::tri::Allocator<GLMesh>::DeleteFace(m,m.face[0]); // 拷貝網格(一樣引發地址變化)的代碼以下,GLMesh 沒有拷貝構造函數,也沒有 operator= ------------ GLMesh m2; vcg::tri::Append<GLMesh,GLMesh>::MeshCopy(m2, m, false, true); // m to m2
IO,讀寫網格文件示例代碼以下(API 文檔主頁 Loading and saving meshes,在線版):
// Mesh 文件通常至少包含頂點數組信息,還能夠包含鏈接信息(三角形)、頂點法向量、頂點顏色、面顏色、 // 面法向量、紋理座標等等屬性,用 mask 的二進制位來標記或控制讀取或寫入了 Mesh 文件的哪些屬性 // 見 vcg::tri::io::Mask,讀取 PLY 須要包含文件 "vcglib/wrap/ply/plylib.cpp"(見這裏) // 頭文件包含:#include "wrap/io_trimesh/import.h" #include "wrap/io_trimesh/export.h" GLMesh m; int mask; // 讀取 PLY 文件,並檢查返回值,參數 mask 爲可選,mask 是返回參數:讀入了哪些屬性 if( vcg::tri::io::ImporterPLY<GLMesh>::Open(m, "file_to_open.ply", mask) != vcg::ply::E_NOERROR ) { std::cout << "Load PLY file ERROR\n"; } // some modification to m and mask ... // 保存 PLY 文件,mask 是輸入參數,控制 m 的哪些屬性被寫入到文件 vcg::tri::io::ExporterPLY<GLMesh>::Save(m, "file_to_save.ply", mask); // 讀取或寫入 OBJ 文件的代碼,mask 做用同上 if( vcg::tri::io::ImporterOBJ<GLMesh>::Open(m, "file_to_open.obj", mask) != vcg::tri::io::ImporterOBJ<GLMesh>::E_NOERROR ) { std::cout << "Load OBJ file ERROR\n"; } // some modification to m and mask ... vcg::tri::io::ExporterOBJ<GLMesh>::Save(m, "file_to_save.obj", mask); // 讀取、寫入網格文件,將根據文件擴展名自動匹配文件格式 --------------------------------------- int oerr = vcg::tri::io::Importer<GLMesh>::Open(m, "file_to_open.off", mask); if( oerr != 0 ){ std::cout << "Load mesh file ERROR: " << vcg::tri::io::Importer<GLMesh>::ErrorMsg(oerr) << '\n'; } // some modification to m and mask ... int serr = vcg::tri::io::Exporter<GLMesh>::Save(m, "file_to_save.3ds", mask); if( serr != 0 ){ std::cout << "Save mesh file ERROR: " << vcg::tri::io::Exporter<GLMesh>::ErrorMsg(oerr) << '\n'; }
構造網格拓撲信息示例代碼以下(API 文檔主頁 Adjacency and Topology,在線版):
// load mesh ... vcg::tri::UpdateNormal<GLMesh>::PerFaceNormalized(mesh); // 計算頂點法向量,並單位化 vcg::tri::UpdateNormal<GLMesh>::PerVertexNormalized(mesh); // 計算面法向量,並單位化 vcg::tri::UpdateTopology<GLMesh>::FaceFace(mesh); // 計算面-面鄰接信息 vcg::tri::UpdateTopology<GLMesh>::AllocateEdge(mesh); // 計算邊-面鄰接信息,須要面-面信息 vcg::Matrix44f mat(&glm::translate(glm::vec3(1,2,3))[0][0]); vcg::tri::UpdatePosition<GLMesh>::Matrix(mesh, mat, true); // 更新頂點位置,並更新法向量 // 在調用 UpdateTopology<>::FaceFace() 和 UpdateTopology<>::AllocateEdge() 後就構造了邊到面 // 的信息,對於 manifold 網格,每一個邊必鏈接兩個三角形面,下面代碼對邊 i 查找其鏈接的面 fa 和 fb int i=0; GLMesh::EdgeType& e = mesh.edge[i]; GLMesh::FaceType* fa = e.EFp(); GLMesh::FaceType* fb = fa->FFp(e.EFi());
在準備這篇博客之初,研究 VCGlib 時,發現了 VCGlib 的一個 BUG,已經報告給開發者並獲得確認(見這裏,看看時間,發現這篇博客由於一些緣由拖了20多天...)。
網格處理示例代碼以下:
vcg::tri::Clean<GLMesh>::RemoveDuplicateVertex(mesh); // 去除重合的頂點 vcg::tri::Smooth<GLMesh>::VertexNormalLaplacian(mesh, 5); // 平滑頂點法向量 float maxSizeHole = 2.0f; // fill 全部直徑小於 maxSizeHole 的洞 vcg::tri::Hole<GLMesh>::EarCuttingIntersectionFill <vcg::tri::SelfIntersectionEar<GLMesh>>(mesh, maxSizeHole, false);
進一步學習的資源: