第一章 绪论
我们通过一系列自包含的示例来介绍 libigl。 每个示例的目的是展示 libigl 的一个特性,同时应用于几何处理中的实际问题。 在本章中,我们将介绍 libigl 的基本概念,并介绍一个简单的网格查看器,它允许可视化表面网格及其属性。 所有教程示例都是跨平台的,可以在 MacOSX、Linux 和 Windows 上编译。
libigl的设计原则
在进入这些实力之前,现总结一下libigl中的一些主要设计原则:
1.没有复杂的数据类型。我们主要使用矩阵和向量。 这极大地有利于代码的可重用性,并迫使函数作者公开算法使用的所有参数。
2.最少依赖项。我们仅在必要时使用外部库,并将它们包装在一小部分函数中。
3.只包含头文件。使用我们的库很简单,因为它只是您项目中的一个附加包含目录。 (如果担心编译速度,也可以将库构建为静态库)
4.函数封装。每个函数(包括其完整实现)都包含在一对具有相同函数名称的 .h/.cpp 文件中。
下载libigl
libigl 可以从 github 存储库下载并使用 git 克隆:
git clone https://github.com/libigl/libigl.git
核心 libigl 功能仅依赖于 C++ 标准库和 Eigen。 可以使用 cmake 下载可选的依赖项,如下所示。
要构建教程(和测试)中的所有示例,您可以使用根文件夹中的 CMakeLists.txt:
cd libigl/
mkdir build
cd build
cmake ../
make
关于CGAL:众所周知,可选的依赖项 CGAL 很难设置(因为它还依赖于 boost/gmp/mpfr)。 默认情况下,只有在系统范围内安装了 GMP 和 MPFR 时,它才会在 Linux/macOS 上启用。 在 Windows 上,它的所有依赖项都将由 CMake 下载,因此您无需进行任何设置。
这些示例也可以使用每个示例文件夹中的 CMakeLists.txt 独立构建。
关于Linux的用户:许多 Linux 发行版的默认安装中不包含 gcc 和基本开发工具。 在 Ubuntu 上,您需要安装以下软件包:
sudo apt-get install git
sudo apt-get install build-essential
sudo apt-get install cmake
sudo apt-get install libx11-dev
sudo apt-get install mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev
sudo apt-get install libxrandr-dev
sudo apt-get install libxi-dev
sudo apt-get install libxmu-dev
sudo apt-get install libblas-dev
sudo apt-get install libxinerama-dev
sudo apt-get install libxcursor-dev
对于Windows的用户:libigl 仅在 64 位模式下支持 Microsoft Visual Studio 2015 编译器及更高版本。 它不适用于 32 位版本,也不适用于旧版本的 Visual Studio。
第 5 章中的一些示例需要 CoMiSo 求解器。 我们提供了一个 CoMISo 镜像,它与 libigl 一起开箱即用。 第一次构建 libigl 根项目时,CMake 会自动下载一个副本。 您可以像往常一样构建教程,libigl 将自动查找并编译 CoMISo。
注意1: CoMISo 是根据 GPL3 许可证分发的,它确实对商业使用施加了限制。
注意2: CoMISo 需要一个 blas 实现。 我们在 macosx 和 linux 中使用内置的 blas,我们为 VS2015 64 位捆绑了一个预编译的二进制文件。 不要在 Windows 上以 32 位编译教程。
libigl的实例教程:
我们提供了一个空白项目示例,展示了如何使用 libigl 和 CMake。 这是在项目中使用 libigl 的推荐方式。 随意并鼓励使用这个存储库作为模板来使用 libigl 开始一个新的个人项目。
网格的表达
libigl中使用Eigen库来编码数组和矩阵。我们建议,在阅读一下的教程的时候可以将稠密以及稀疏的快速索引指南放在手边!
一个三角网格被编码在一堆矩阵之中,矩阵的模式如下:
Eigen::MatrixXd V;
Eigen::MatrixXi F;
其中V是一个N*3的矩阵,存储顶点的坐标。每一行存储着一个顶点的三个位置坐标,x,y,z分别对应着矩阵的第1,2,3列。而矩阵F则表示三角形的位置关系,其中F的每一行表示一个三角形,其中的三个元素分别表示三角形的三个顶点在V中的行索引。
主要注意的是F中顶点索引的存储顺序决定了三角形的死苦想,因此在整个网格的表达的过程中,此顺序应当保持一致。这种简单的表达由以下的一些优点:
- 它在存储上高效且对缓存友好
- 索引而不是指针的使用简化了调试的过程
- 数据可以简单的复制以及序列化
libigl提供一些读取以及存储函数来对常规的网格的形式进行输入输出。这些函数被存储在read*.h/.cpp以及write*.h/.cpp的文件当中。作为一般规则,每个 libigl 函数都包含在一对同名的 .h/.cpp 文件中。 默认情况下,.h 文件包含相应的 cpp 文件,使库仅包含头文件。
从文件中读取一个网格仅仅需要一个单独的libigl的函数:
igl::readOFF(TUTORIAL_SHARED_PATH "/cube.off", V, F);
这个函数从cube.off中读取网格并且降至存储在V和F矩阵当中。类似的,它可以使用如下的函数将网格存储在OBJ格式的文件中:
igl::writeOBJ("cube.obj",V,F);
示例 101 包含一个从 OFF 到 OBJ 格式的简单网格转换器。
网格的可视化
libigl提供了一个基于glfw的OpenGL 3.2的可视化工具来显示网格曲面,它们的特性以及附加的调试信息。
以下代码(示例 102)是本教程中将使用的所有示例的基本框架。 它是一个独立的应用程序,它加载一个网格并使用查看器来渲染它。
#include <igl/readOFF.h>
#include <igl/opengl/glfw/Viewer.h>
Eigen::MatrixXd V;
Eigen::MatrixXi F;
int main(int argc, char *argv[])
{
// Load a mesh in OFF format
igl::readOFF(TUTORIAL_SHARED_PATH "/bunny.off", V, F);
// Plot the mesh
igl::opengl::glfw::Viewer viewer;
viewer.data().set_mesh(V, F);
viewer.launch();
}
set_mesh函数将网格的信息拷贝到viewer中。Viewer.launch()创建了一个窗口,一个OpenGL的上下文并且启动一个绘画循环。相机的默认移动模式是2轴的(ROTATION_TYPE_TWO_AXIS_VALUATOR_FIXED_UP),它也可以通过以下的代码调整为三轴的追踪球的模式。
viewer.core().set_rotation_type(igl::opengl::ViewerCore::ROTATION_TYPE_TRACKBALL);
其他的一些属性同样也可以展示在网格中(之后的内容中会提及),同理这个工具也可以扩展为标准的OpenGL的代码,更多的细节部分参见Viewer.h的代码。
使用键盘以及鼠标进行交互
键盘以及鼠标的触发回调函数也可以在可视化工具中进行调用。它支持以下的回调函数:
bool (*callback_pre_draw)(Viewer& viewer);
bool (*callback_post_draw)(Viewer& viewer);
bool (*callback_mouse_down)(Viewer& viewer, int button, int modifier);
bool (*callback_mouse_up)(Viewer& viewer, int button, int modifier);
bool (*callback_mouse_move)(Viewer& viewer, int mouse_x, int mouse_y);
bool (*callback_mouse_scroll)(Viewer& viewer, float delta_y);
bool (*callback_key_down)(Viewer& viewer, unsigned char key, int modifiers);
bool (*callback_key_up)(Viewer& viewer, unsigned char key, int modifiers);
键盘回调可用于可视化多个网格或算法的不同阶段,如示例 103 所示,其中键盘回调根据按下的键更改可视化网格:
bool key_down(igl::opengl::glfw::Viewer& viewer, unsigned char key, int modifier)
{
if (key == '1')
{
viewer.data().clear();
viewer.data().set_mesh(V1, F1);
viewer.core.align_camera_center(V1,F1);
}
else if (key == '2')
{
viewer.data().clear();
viewer.data().set_mesh(V2, F2);
viewer.core.align_camera_center(V2,F2);
}
return false;
}
回调函数采用如下的方式在工具中进行注册:
viewer.callback_key_down = &key_down;
请注意,网格在使用 set_mesh 之前已被清除。 每次绘制网格的顶点或面数发生变化时,都必须调用它。 每个回调都返回一个布尔值,告诉查看器是否已由插件处理事件,或者查看器是否应该正常处理它。 这很有用,例如,如果您想直接在代码中控制相机,则可以禁用默认的鼠标事件处理。
查看器可以使用插件进行扩展,插件是实现所有查看器回调的类。 有关详细信息,请参阅 Viewer_plugin。
标量场可视化
颜色可以使用set_color函数绑定到面或是顶点上:
viewer.data().set_colors(C);
其中C是一个N*3的矩阵,其中的每一行表示着一个RGB的值。它的行数必须要和待设置的网格的顶点或是面保持一致。查看器会根据C中的大小将不同的颜色应用到顶点和面上去。在例子104中,网格的颜色根据顶点的笛卡尔坐标进行设定。
每个顶点的标量场都可以使用set_data函数来进行设置。
viewer.data().set_data(D);
D 是一个 #V x 1 向量,每个顶点对应一个值。 set_data 将根据线性插值三角形内的数据(在片段着色器中)进行着色,并使用此插值数据在颜色图中查找颜色(存储为纹理)。 颜色图默认为具有 21 个离散间隔的 igl::COLOR_MAP_TYPE_VIRIDIS。 可以使用 set_colormap 设置自定义颜色图。
叠加层
除了绘制表面之外,查看器还支持点、线和文本标签的可视化:这些覆盖在开发几何处理算法以绘制调试信息时非常有用。。
viewer.data().add_points(P,Eigen::RowVector3d(r,g,b));
其中P是一个顶点数组,其中存储了数个顶点,这段代码可以以(r,g,b)的值在查看器中绘制出P中的顶点。其中点P的大小(即点P的像素值)可以通过设置全局变量viewer.data().point_size来进行调整。
viewer.data().add_edges(P1,P2,Eigen::RowVector3d(r,g,b));
同理,P1,P2是两个顶点数组,此段代码可以将p1,p2中每行所对应的顶点连接成线,线的颜色通过(r,g,b)来进行设置。
viewer.data().add_label(p,str);
在位置 p 处绘制一个包含字符串 str 的标签,它是一个长度为 3 的向量。
这些函数在示例 105 中进行了演示,其中使用线和点绘制了网格的边界框。 使用矩阵对网格及其属性进行编码允许为许多操作编写简短而高效的代码,避免编写 for 循环。 例如,网格的边界框可以通过取 V 的 colwise 最大值和最小值来找到:
查看器的菜单
在最新版本中,查看器使用了一个新菜单,并用 Dear ImGui 完全取代了 AntTweakBar 和 nanogui。 要扩展查看器的默认菜单并公开更多用户定义的变量,您必须实现自定义接口,如示例 106 所示:
// Add content to the default menu window
menu.callback_draw_viewer_menu = [&]()
{
// Draw parent menu content
menu.draw_viewer_menu();
// Add new group
if (ImGui::CollapsingHeader("New Group", ImGuiTreeNodeFlags_DefaultOpen))
{
// Expose variable directly ...
ImGui::InputFloat("float", &floatVariable, 0, 0, 3);
// ... or using a custom callback
static bool boolVariable = true;
if (ImGui::Checkbox("bool", &boolVariable))
{
// do something
std::cout << "boolVariable: " << std::boolalpha << boolVariable << std::endl;
}
// Expose an enumeration type
enum Orientation { Up=0, Down, Left, Right };
static Orientation dir = Up;
ImGui::Combo("Direction", (int *)(&dir), "Up\0Down\0Left\0Right\0\0");
// We can also use a std::vector<std::string> defined dynamically
static int num_choices = 3;
static std::vector<std::string> choices;
static int idx_choice = 0;
if (ImGui::InputInt("Num letters", &num_choices))
{
num_choices = std::max(1, std::min(26, num_choices));
}
if (num_choices != (int) choices.size())
{
choices.resize(num_choices);
for (int i = 0; i < num_choices; ++i)
choices[i] = std::string(1, 'A' + i);
if (idx_choice >= num_choices)
idx_choice = num_choices - 1;
}
ImGui::Combo("Letter", &idx_choice, choices);
// Add a button
if (ImGui::Button("Print Hello", ImVec2(-1,0)))
{
std::cout << "Hello\n";
}
}
};
如果您需要一个单独的新菜单窗口实现:
// Draw additional windows
menu.callback_draw_custom_window = [&]()
{
// Define next window position + size
ImGui::SetNextWindowPos(ImVec2(180.f * menu.menu_scaling(), 10), ImGuiSetCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(200, 160), ImGuiSetCond_FirstUseEver);
ImGui::Begin(
"New Window", nullptr,
ImGuiWindowFlags_NoSavedSettings
);
// Expose the same variable directly ...
ImGui::PushItemWidth(-80);
ImGui::DragFloat("float", &floatVariable, 0.0, 0.0, 3.0);
ImGui::PopItemWidth();
static std::string str = "bunny";
ImGui::InputText("Name", str);
ImGui::End();
};
多个网格
Libigl 的 igl::opengl::glfw::Viewer 为渲染多个网格提供了基本支持。
选择哪个网格是通过 viewer.selected_data_index 字段控制的。 默认情况下,索引设置为 0,因此在单个网格的典型情况下,viewer.data() 返回与唯一网格对应的 igl::ViewerData。
多个视图
Libigl 的 igl::opengl::glfw::Viewer 为渲染具有多个视图的网格提供了基本支持。
可以使用 Viewer::append_core() 方法将新的视图核心添加到查看器。 在任何查看器的生命周期中最多可以创建 31 个内核。 每个核心都被分配了一个无符号的 int id,它保证是唯一的。 可以通过调用 Viewer::core(id) 方法的 id 访问核心。
当有多个 view core 时,用户负责通过设置 viewport 属性来指定每个 viewport 的大小和位置。 用户还必须指出当窗口大小改变时如何调整每个视口的大小。 例如:
viewer.callback_post_resize = [&](igl::opengl::glfw::Viewer &v, int w, int h) {
v.core( left_view).viewport = Eigen::Vector4f(0, 0, w / 2, h);
v.core(right_view).viewport = Eigen::Vector4f(w / 2, 0, w - (w / 2), h);
return true;
};
请注意,当前鼠标悬停的视口可以使用 Viewer::selected_core_index() 方法选择,然后可以通过调用 viewer.core_list[viewer.selected_core_index] 访问选定的视图核心。
最后,给定视图核心上的网格的可见性由每个网格的位掩码标志控制。 这个属性可以通过调用方法轻松控制
viewer.data(mesh_id).set_visible(false, view_id);
添加新网格或新视图核心时,可选参数控制现有对象相对于新网格/视图的可见性。 更多细节请参考 Viewer::append_mesh() 和 Viewer::append_core() 的文档。
查看器的Guizmos
目前不可能同时激活多个 ImGui 相关的查看器插件(包括 ImGuiMenu、ImGuizmoPlugin 和 SelectionPlugin)。 请关注#1656 了解更多信息。(目前的一个bug)
查看器与 ImGuizmo 集成以提供用于操作网格的小部件。 网格操作包括平移、旋转和缩放,其中 W,w, E,e 和 R,r 可分别用于在它们之间切换。
首先,在 Viewer 中注册 ImGuizmoPlugin 插件:
#include <igl/opengl/glfw/imgui/ImGuizmoPlugin.h>
// ImGuizmoPlugin replaces the ImGuiMenu plugin entirely
igl::opengl::glfw::imgui::ImGuizmoPlugin plugin;
vr.plugins.push_back(&plugin);
初始化时,必须为 ImGuizmo 提供网格质心,如示例 109 所示:
// Initialize ImGuizmo at mesh centroid
plugin.T.block(0,3,3,1) =
0.5*(V.colwise().maxCoeff() + V.colwise().minCoeff()).transpose().cast<float>();
为了应用由 guizmo 调用的网格操作,计算得到的变换矩阵并通过查看器的 API 显式应用于输入几何数据:
// Update can be applied relative to this remembered initial transform
const Eigen::Matrix4f T0 = plugin.T;
// Attach callback to apply imguizmo's transform to mesh
plugin.callback = [&](const Eigen::Matrix4f & T)
{
const Eigen::Matrix4d TT = (T*T0.inverse()).cast<double>().transpose();
vr.data().set_vertices(
(V.rowwise().homogeneous()*TT).rowwise().hnormalized());
vr.data().compute_normals();
};
Msh查看器
Libigl 可以读取以 Gmsh .msh 版本 2 文件格式存储的混合网格。 这些文件可以包含不同网格的混合,以及在元素级别和顶点级别定义的附加标量和矢量场。
Eigen::MatrixXd X; // Vertex coorinates (Xx3)
Eigen::MatrixXi Tri; // Triangular elements (Yx3)
Eigen::MatrixXi Tet; // Tetrahedral elements (Zx4)
Eigen::VectorXi TriTag; // Integer tags defining triangular submeshes
Eigen::VectorXi TetTag; // Integer tags defining tetrahedral submeshes
std::vector<std::string> XFields; // headers (names) of fields defined on vertex level
std::vector<std::string> EFields; // headers (names) of fields defined on element level
std::vector<Eigen::MatrixXd> XF; // fields defined on vertex
std::vector<Eigen::MatrixXd> TriF; // fields defined on triangular elements
std::vector<Eigen::MatrixXd> TetF; // fields defined on tetrahedral elements
// loading mixed mesh from Gmsh file
igl::readMSH("hand.msh", X, Tri, Tet, TriTag, TetTag, XFields, XF, EFields, TriF, TetF);
但是交互式查看器无法直接绘制四面体。 因此,出于可视化目的,每个四面体都可以转换为四个三角形。
材质获取
MatCaps(材质捕捉),也称为环境贴图,是一种简单的基于图像的渲染技术,无需复杂的着色器程序即可实现复杂的光照。
使用离线渲染甚至绘画程序,创建渲染的单位球体的图像,例如在工作室照明下查看的带有玉石材质的球体图像:
其中球上的每个顶点P都对应着一个法向量
n
^
=
P
\hat{n}=P
n^=P.matcaps 的想法是将此球体图像用作查找表,以输入法线值为键并输出 rgb 颜色:
I
(
n
^
)
→
(
r
,
g
,
b
)
I(\hat{n})\to(r,g,b)
I(n^)→(r,g,b).
当渲染一个非球面形状时,在片段着色器中我们计算法线向量
n
^
\hat{n}
n^,然后使用它的x_和y_组件作为纹理坐标来查找 matcap 图像中的对应点。通过这种方式,片段着色器中没有光照模型或光照计算,它只是一个纹理查找,但不需要模型的 UV 映射(参数化),我们使用每个片段的法线。 通过使用相对于相机坐标系的法线,我们“免费”获得了依赖于视图的复杂光照:
在 libigl 中,如果 matcap 图像的 rgba 数据存储在 R、G、B 和 A 中(作为输出,例如,通过 igl::png::readPNG),则可以将其附加到 igl::opengl:: ViewerData 通过将其设置为纹理数据,然后打开 matcap 渲染:
viewer.data().set_texture(R,G,B,A);
viewer.data().use_matcap = true;