实验平台: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);
进一步学习的资源: