第一章
我们通过一系列独立的示例介绍libigl。每个示例的目的是展示libigl的一个特性,同时应用于几何处理中的实际问题。在本章中,我们将介绍libigl的基本概念,并介绍一个简单的网格查看器,可以用于可视化表面网格及其属性。所有教程示例都是跨平台的,可以在MacOSX、Linux和Windows上编译。
Libigl设计原则在进入示例之前,我们总结一下libigl的主要设计原则:
没有复杂的数据类型。我们主要使用矩阵和向量。这极大地促进了代码的可重用性,并迫使函数作者暴露算法中使用的所有参数。
最小依赖性。我们只在必要时使用外部库,并将它们封装在一小组函数中。
头文件。使用我们的库非常简单,因为只需在项目中添加一个额外的包含目录即可。(如果您担心编译速度,也可以将库构建为静态库)
函数封装。每个函数(包括其完整实现)都包含在一对具有相同名称的.h/.cpp文件中。
许多Linux发行版在默认安装中不包括gcc和基本的开发工具。在Ubuntu上,您需要安装以下软件包:
sudo apt-get install \
git \
build-essential \
cmake \
libx11-dev \
mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev \
libxrandr-dev \
libxi-dev \
libxmu-dev \
libblas-dev \
libxinerama-dev \
libxcursor-dev
下载Libigl您可以从我们的GitHub存储库下载libigl,也可以使用git进行克隆:
git clone https://github.com/libigl/libigl.git
核心libigl功能仅依赖于C++标准库和Eigen。在下面执行cmake时,可选依赖项将被下载。
要构建教程中的所有示例(和测试),您可以使用根文件夹中的CMakeLists.txt:
cd libigl/
mkdir build
cd build
cmake ../
make
一、FileIO
# 包含必要的头文件
#include <igl/readOFF.h> // 包含用于读取OFF文件格式的函数
#include <igl/writeOBJ.h> // 包含用于写入OBJ文件格式的函数
#include <iostream> // 包含标准输入输出流功能
// 定义Eigen库中的矩阵类型来存储顶点和面
Eigen::MatrixXd V; // V是存储网格顶点坐标的矩阵,MatrixXd表示动态大小的矩阵,其中元素类型为double
Eigen::MatrixXi F; // F是存储网格面顶点索引的矩阵,MatrixXi表示动态大小的矩阵,其中元素类型为int
// 主函数
int main(int argc, char *argv[])
{
// 加载一个OFF格式的网格
igl::readOFF(TUTORIAL_SHARED_PATH "/cube.off", V, F); // 调用igl::readOFF函数读取文件
// 打印顶点矩阵和面矩阵
std::cout << "Vertices: " << std::endl << V << std::endl; // 输出"Vertices: ",然后输出顶点矩阵V
std::cout << "Faces: " << std::endl << F << std::endl; // 输出"Faces: ",然后输出面矩阵F
// 将网格保存为OBJ格式
igl::writeOBJ("cube.obj",V,F); // 调用igl::writeOBJ函数写入OBJ文件,文件名为"cube.obj"
}
以上代码的功能是加载一个OFF格式的三维网格模型,打印出该模型的顶点和面信息,然后将这个模型保存为OBJ格式的文件。这个过程通过使用Eigen库来处理矩阵运算,以及利用libigl库中的函数来实现对OFF和OBJ文件格式的读写操作。
二、DrawMesh
# 包含必要的头文件
#include <igl/readOFF.h> // 包含用于读取OFF文件格式的函数
#include <igl/opengl/glfw/Viewer.h> // 包含用于渲染和显示三维网格的查看器类
// 定义Eigen库中的矩阵类型来存储顶点和面
Eigen::MatrixXd V; // V是存储网格顶点坐标的矩阵,MatrixXd表示动态大小的矩阵,其中元素类型为double
Eigen::MatrixXi F; // F是存储网格面顶点索引的矩阵,MatrixXi表示动态大小的矩阵,其中元素类型为int
// 主函数
int main(int argc, char *argv[])
{
// 加载一个OFF格式的网格
igl::readOFF(TUTORIAL_SHARED_PATH "/bunny.off", V, F); // 调用igl::readOFF函数读取文件
// 绘制网格
igl::opengl::glfw::Viewer viewer; // 创建一个查看器实例
viewer.data().set_mesh(V, F); // 使用set_mesh函数设置查看器中的网格数据
viewer.launch(); // 启动查看器窗口,并渲染显示网格
}
以上代码的功能是加载一个OFF格式的三维网格模型(具体为“bunny.off”),然后创建一个查看器窗口来渲染并展示这个模型。代码中使用了Eigen库来存储模型的顶点和面数据,并利用libigl的查看器功能来进行三维网格的可视化展示。
三、Events
该代码扩展了之前的功能,现在加载了两个OFF格式网格。每个网格通过Eigen库的MatrixXd和MatrixXi来存储顶点和面数据。此外,代码提供了一个键盘回调函数key_down,允许在运行时通过按键 '1' 和 '2' 来在两个不同的网格模型之间切换显示。
回调函数key_down中使用了viewer.data().clear()来清除当前查看器中的网格数据,然后根据按下的键来选择加载不同的网格数据。函数viewer.data().set_mesh()用于设置新的顶点和面数据,函数viewer.core().align_camera_center()用于根据新的网格数据自动居中并对齐相机视角。
在main()函数中,使用igl::readOFF函数读取了两个网格文件,并在命令行中打印了用户指南。然后在启动查看器窗口之前,将键盘回调函数绑定到查看器,并初始化默认显示第一个网格模型。运行程序后,用户可以按键 '1' 和 '2' 来在“bumpy”(凹凸不平)网格和“fertility”(生育)网格之间切换。
// 包含必要的头文件
#include <igl/readOFF.h> // 包含用于读取OFF文件格式的函数
#include <igl/opengl/glfw/Viewer.h> // 包含用于渲染和显示三维网格的查看器类
#include <iostream> // 包含标准输入输出流功能
// 定义Eigen库中的矩阵类型来存储顶点和面
Eigen::MatrixXd V1,V2; // V1和V2分别存储两个网格顶点坐标的矩阵
Eigen::MatrixXi F1,F2; // F1和F2分别存储两个网格面顶点索引的矩阵
// 该函数在每次按键时被调用
bool key_down(igl::opengl::glfw::Viewer& viewer, unsigned char key, int modifier)
{
// 打印按下的键和对应的ASCII值
std::cout<<"Key: "<<key<<" "<<(unsigned int)key<<std::endl;
if (key == '1') // 检查是否按下了键'1'
{
// 在绘制网格前应调用clear函数
viewer.data().clear();
// 根据提供的顶点V和面F来创建或更新显示的网格
// 如果已经显示了一个网格,则draw_mesh在V和F的大小与当前不同时返回错误
viewer.data().set_mesh(V1, F1);
// 使相机居中对齐于V1和F1定义的网格
viewer.core().align_camera_center(V1,F1);
}
else if (key == '2') // 检查是否按下了键'2'
{
viewer.data().clear();
// 设置第二个网格的顶点和面
viewer.data().set_mesh(V2, F2);
// 使相机居中对齐于V2和F2定义的网格
viewer.core().align_camera_center(V2,F2);
}
return false;
}
// 主函数
int main(int argc, char *argv[])
{
// 加载两个网格模型
igl::readOFF(TUTORIAL_SHARED_PATH "/bumpy.off", V1, F1);
igl::readOFF(TUTORIAL_SHARED_PATH "/fertility.off", V2, F2);
// 打印使用说明
std::cout << R"(
1 Switch to bump mesh
2 Switch to fertility mesh
)";
igl::opengl::glfw::Viewer viewer; // 创建查看器实例
// 注册一个键盘回调函数,允许在两个加载的网格之间切换
viewer.callback_key_down = &key_down;
// 初始化查看器窗口显示的是第一个网格
viewer.data().set_mesh(V1, F1);
// 启动查看器窗口,并渲染显示网格
viewer.launch();
}
四、Colors
以上代码的功能是加载一个OFF格式的三维网格模型(在本例中为“screwdriver.off”即螺丝刀模型),然后创建一个查看器窗口来渲染并展示这个模型。此外,该代码还使用顶点的位置信息来为每个顶点生成一个归一化的颜色,以根据其位置在模型上显示不同的颜色。
具体来说:
调用
igl::readOFF
函数加载了名为 "screwdriver.off" 的螺丝刀模型。初始化
igl::opengl::glfw::Viewer
对象,并通过set_mesh
方法设置查看器中渲染的网格数据。使用顶点坐标数据(V)经过归一化处理计算出每个顶点的颜色值(C)。
调用
set_colors
方法将归一化后的顶点颜色(C)设置到查看器中,使渲染的三维网格拥有了顶点色彩。最后,调用
viewer.launch()
方法启动查看器窗口,用户可以在其中观察带有色彩的模型。
// 包含必要的头文件
#include <igl/readOFF.h> // 包含用于读取OFF文件格式的函数
#include <igl/opengl/glfw/Viewer.h> // 包含用于渲染和显示三维网格的查看器类
// 定义Eigen库中的矩阵类型来存储顶点、面和颜色
Eigen::MatrixXd V; // V是存储网格顶点坐标的矩阵
Eigen::MatrixXi F; // F是存储网格面顶点索引的矩阵
Eigen::MatrixXd C; // C是存储顶点颜色的矩阵
// 主函数
int main(int argc, char *argv[])
{
// 加载一个OFF格式的网格
igl::readOFF(TUTORIAL_SHARED_PATH "/screwdriver.off", V, F); // 使用igl::readOFF函数读取螺丝刀网格模型
// 绘制网格
igl::opengl::glfw::Viewer viewer; // 创建一个查看器实例
viewer.data().set_mesh(V, F); // 设置查看器中的网格数据
// 使用归一化后的顶点位置作为颜色
C =
(V.rowwise() - V.colwise().minCoeff()).array().rowwise()/
(V.colwise().maxCoeff() - V.colwise().minCoeff()).array();
// 添加每个顶点的颜色
viewer.data().set_colors(C); // 使用set_colors函数为网格顶点设置颜色
// 启动查看器
viewer.launch(); // 启动查看器窗口,并渲染显示网格
}
五、Overlays(windows系统配置有问题,linux OK)
此代码主要实现了加载并可视化一个OFF格式的三维网格模型(本例中为“bunny.off”即兔子模型),同时找到并绘制了模型的包围盒。这包括计算包围盒的角点和边,以及将这些几何数据添加到查看器中以便可视化。
具体步骤:
使用 igl::readOFF 函数从指定路径加载了兔子模型的顶点坐标和面。
计算出了模型包围盒的两个对角顶点(m 和 M),并据此生成了包围盒的其他角点(V_box)以及边(E_box)。
利用 igl::opengl::glfw::Viewer 实例化查看器,并使用 set_mesh 方法来渲染模型。
通过 add_points 和 add_edges 方法添加了包围盒的角点和边到查看器,并分别用红色标记。
添加了两个顶点坐标作为标签,显示在包围盒的对角顶点处,并启用了自定义标签渲染。
使用 igl::opengl::glfw::imgui::ImGuiPlugin 实例化 ImGui 插件以使用 ImGui 功能来渲染和显示文本标签。
最后,调用 viewer.launch() 方法启动查看器窗口,渲染并展示了包含模型、包围盒及其标签的三维场景。
//包含必要的头文件
#include <igl/readOFF.h> // 包含用于读取OFF文件格式的函数
#include <igl/opengl/glfw/Viewer.h> // 包含用于渲染和显示三维网格的查看器类
#include <igl/opengl/glfw/imgui/ImGuiPlugin.h> // 包含libigl集成的ImGui插件相关功能,用于GUI渲染
#include <igl/opengl/glfw/imgui/ImGuiMenu.h> // 包含用于操作ImGui菜单的函数
#include <sstream> // 包含标准字符串流的功能
// 定义Eigen库中的矩阵类型来存储顶点和面
Eigen::MatrixXd V; // V是存储网格顶点坐标的矩阵
Eigen::MatrixXi F; // F是存储网格面顶点索引的矩阵
// 主函数
int main(int argc, char *argv[])
{
// 加载一个OFF格式的网格
igl::readOFF(TUTORIAL_SHARED_PATH "/bunny.off", V, F); // 使用igl::readOFF函数读取“bunny.off”模型的顶点和面数据
// 找到网格模型的包围盒(bounding box)
Eigen::Vector3d m = V.colwise().minCoeff(); // 计算每个维度的最小值,得到包围盒的一个角点m
Eigen::Vector3d M = V.colwise().maxCoeff(); // 计算每个维度的最大值,得到包围盒的对角点M
// 包围盒的角点
Eigen::MatrixXd V_box(8,3); // 声明V_box存储包围盒的8个角点
V_box <<
m(0), m(1), m(2),
M(0), m(1), m(2),
M(0), M(1), m(2),
m(0), M(1), m(2),
m(0), m(1), M(2),
M(0), m(1), M(2),
M(0), M(1), M(2),
m(0), M(1), M(2);
// 包围盒的边
Eigen::MatrixXi E_box(12,2); // 声明E_box存储包围盒的12条边的顶点索引
E_box <<
0, 1,
1, 2,
2, 3,
3, 0,
4, 5,
5, 6,
6, 7,
7, 4,
0, 4,
1, 5,
2, 6,
7 ,3;
// 绘制网格
igl::opengl::glfw::Viewer viewer; // 创建一个查看器实例
viewer.data().set_mesh(V, F); // 设置查看器中的网格数据
// 绘制包围盒角点作为点
viewer.data().add_points(V_box,Eigen::RowVector3d(1,0,0)); // 在查看器中添加包围盒角点,并设置颜色为红色
// 绘制包围盒的边
for (unsigned i=0;i<E_box.rows(); ++i)
viewer.data().add_edges
(
V_box.row(E_box(i,0)), // 边的一个顶点
V_box.row(E_box(i,1)), // 边的另一个顶点
Eigen::RowVector3d(1,0,0) // 边的颜色(红色)
);
// 绘制包围盒顶点坐标的标签
std::stringstream l1;
l1 << m(0) << ", " << m(1) << ", " << m(2);
viewer.data().add_label(m+Eigen::Vector3d(-0.007, 0, 0),l1.str()); // 添加标签显示m点坐标
std::stringstream l2;
l2 << M(0) << ", " << M(1) << ", " << M(2);
viewer.data().add_label(M+Eigen::Vector3d(0.007, 0, 0),l2.str()); // 添加标签显示M点坐标
// 激活标签渲染
viewer.data().show_custom_labels = true; // 设置查看器显示自定义标签
// ImGui插件处理文本标签渲染,因此我们需要启用ImGui插件以显示文本标签。
igl::opengl::glfw::imgui::ImGuiPlugin plugin; // 创建ImGui插件实例
viewer.plugins.push_back(&plugin); // 将ImGui插件添加到查看器插件列表中
igl::opengl::glfw::imgui::ImGuiMenu menu; // 创建ImGui菜单实例
plugin.widgets.push_back(&menu); // 将ImGui菜单添加到插件的小部件列表中
menu.callback_draw_viewer_window = [](){}; // 设置菜单的回调函数,此处为空
// 启动查看器
viewer.launch(); // 启动查看器窗口,并渲染显示网格模型及其包围盒和标签
}
六、ViewerMenu
以上示例代码演示了如何使用libigl库和ImGui进行三维网格可视化以及创建一个简单的用户界面(UI)。主要内容包括:
利用igl::readOFF函数从指定路径加载OFF格式的三维网格模型文件(如"bunny.off",兔子模型)。
初始化libigl的OpenGL查看器igl::opengl::glfw::Viewer,用于显示三维网格。
附加ImGui插件igl::opengl::glfw::imgui::ImGuiPlugin到查看器,并创建了一个菜单igl::opengl::glfw::imgui::ImGuiMenu,将该菜单添加到插件的小部件列表中。ImGui是一个即时模式图形用户界面库,用于创建交互且简洁的用户界面。
自定义ImGui菜单,包括添加一个新的折叠头部"New Group",并在其中创建不同类型的控件,例如输入框(用于输入双精度浮点数和字符串)、复选框、下拉框和按钮。同时,展示了如何响应用户的输入与互动(如打印文本到控制台)。
绘制一个额外的ImGui窗口,并在其中显示并允许用户交互修改双精度变量。
在查看器中设置网格数据,并添加一个标签"Hello World!"显示在第一个顶点的临近位置。
启动查看器,显示网格和用户界面,用户可以与之互动。
总的来说,这段代码不仅展示了三维模型的加载与呈现,还涉及了用户界面的交互和可视化,通过ImGui为用户提供了一套操作界面,用以实时交互显示参数等信息,并可以根据用户操作进行相应的动作或显示效果。
// 包含必要的头文件
#include <igl/readOFF.h> // 包含用于读取OFF文件格式的函数
#include <igl/opengl/glfw/Viewer.h> // 包含用于渲染和显示三维网格的查看器类
#include <igl/opengl/glfw/imgui/ImGuiPlugin.h> // 包含libigl集成的ImGui插件相关功能
#include <igl/opengl/glfw/imgui/ImGuiMenu.h> // 包含用于操作ImGui菜单的函数
#include <igl/opengl/glfw/imgui/ImGuiHelpers.h> // 包含ImGui辅助函数
#include <iostream> // 包含标准输入输出流功能
// 主函数
int main(int argc, char *argv[])
{
Eigen::MatrixXd V; // 定义用于存储网格顶点的矩阵
Eigen::MatrixXi F; // 定义用于存储网格面索引的矩阵
// 加载一个OFF格式的网格
igl::readOFF(TUTORIAL_SHARED_PATH "/bunny.off", V, F); // 使用igl::readOFF函数读取“bunny.off”模型的顶点和面数据
// 初始化查看器
igl::opengl::glfw::Viewer viewer; // 创建查看器实例
// 附加一个菜单插件
igl::opengl::glfw::imgui::ImGuiPlugin plugin; // 创建ImGui插件实例
viewer.plugins.push_back(&plugin); // 将插件添加到查看器的插件列表中
igl::opengl::glfw::imgui::ImGuiMenu menu; // 创建ImGui菜单实例
plugin.widgets.push_back(&menu); // 将菜单添加到插件的小部件列表中
// 自定义菜单
double doubleVariable = 0.1f; // 在两个菜单之间共享的变量
// 向默认菜单窗口添加内容
menu.callback_draw_viewer_menu = [&]()
{
// 绘制父菜单内容
menu.draw_viewer_menu();
// 添加新分组
if (ImGui::CollapsingHeader("New Group", ImGuiTreeNodeFlags_DefaultOpen))
{
// 直接暴露变量...
ImGui::InputDouble("double", &doubleVariable, 0, 0, "%.4f");
// ...或使用自定义回调
static bool boolVariable = true;
if (ImGui::Checkbox("bool", &boolVariable))
{
// 做些事情
std::cout << "boolVariable: " << std::boolalpha << boolVariable << std::endl;
}
// 暴露枚举类型变量
enum Orientation { Up=0, Down, Left, Right };
static Orientation dir = Up;
ImGui::Combo("Direction", (int *)(&dir), "Up\0Down\0Left\0Right\0\0");
// 我们也可以动态定义一个std::vector<std::string>
static int num_choices = 3; // 初始化一个静态整数变量num_choices来指定选择项的数量,默认是3
static std::vector<std::string> choices; // 初始化一个静态的字符串向量choices来存放下拉列表中的选项
static int idx_choice = 0; // 初始化一个静态整数变量idx_choice来存储当前选择的下拉列表项的索引,默认选择第一个选项
// 创建一个输入框,用户可以输入字母的数量,并对用户的输入做出响应
if (ImGui::InputInt("Num letters", &num_choices))
{
// 对用户输入的数量进行限制,确保它在1到26之间
num_choices = std::max(1, std::min(26, num_choices));
}
// 如果用户输入的字母数量与当前选项数量不同,更新下拉列表的选项
if (num_choices != (int) choices.size())
{
// 根据用户指定的数量调整选项列表的大小
choices.resize(num_choices);
// 填充选项列表,从"A"开始,按字母顺序添加
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);
// 添加按钮
if (ImGui::Button("Print Hello", ImVec2(-1,0)))
{
std::cout << "Hello\n";
}
}
};
// 绘制额外窗口
menu.callback_draw_custom_window = [&]()
{
// 定义下一个窗口的位置和大小
// 设置下一个ImGui窗口的位置
ImGui::SetNextWindowPos(ImVec2(180.f * menu.menu_scaling(), 10), ImGuiCond_FirstUseEver);
// 设置下一个ImGui窗口的大小
ImGui::SetNextWindowSize(ImVec2(200, 160), ImGuiCond_FirstUseEver);
// 创建一个新的ImGui窗口 "New Window"
ImGui::Begin(
"New Window", nullptr, // 窗口标题为 "New Window", 不使用额外的窗口标志位
ImGuiWindowFlags_NoSavedSettings // 设置ImGui窗口标志,指示不保存设置
);
// 开始定义窗口内的控件内容
// 设置拖动操作的宽度
ImGui::PushItemWidth(-80);
// 创建一个拖动控件,用于修改double类型的变量doubleVariable,步长为0.1
ImGui::DragScalar("double", ImGuiDataType_Double, &doubleVariable, 0.1, 0, 0, "%.4f");
// 恢复之前设置的项目宽度
ImGui::PopItemWidth();
// 定义一个静态的字符串str并初始化为 "bunny"
static std::string str = "bunny";
// 创建一个文本输入框,用于编辑字符串str的内容,并标记为 "Name"
ImGui::InputText("Name", str);
// 结束定义当前窗口,渲染在屏幕上
ImGui::End();
};
// 绘制网格
viewer.data().set_mesh(V, F); // 在查看器中设置网格
// 添加“Hello World!”标签在第一个顶点上,并且偏移一个顶点法线的长度
viewer.data().add_label(viewer.data().V.row(0) + viewer.data().V_normals.row(0).normalized()*0.005, "Hello World!");
viewer.launch(); // 启动查看器
}
七、MultipleMeshes