Libigl学习笔记——二、第五章——参数化
第五章
- 在计算机图形学中,我们将从曲面到 R^2 .它通常由网格每个顶点的一组新的 2D 坐标编码(也可能由一组与原始表面的面一对一对应的新面编码)。请注意,此定义与经典微分几何定义相反。
- 参数化有许多应用,从纹理映射到表面网格重新划分。已经提出了许多算法,它们可以大致分为四个系列:
- 单个斑块,固定边界:这些算法可以在给定其边界的固定 2D 位置的情况下参数化曲面的圆盘状部分。这些算法高效且简单,但由于边界固定,它们通常会产生高失真图。
- 单块,自由边界:这些算法让边界自由变形,大大降低了地图失真。应注意防止边界自相交。
- 全局参数化:这些算法适用于具有任意属的网格。他们最初将网格切割成多个可以单独参数化的补丁。生成的贴图在切口上是不连续的(通常称为接缝)。
- 全局无缝参数化:这些是全局参数化算法,它隐藏接缝,使参数化“连续”,根据我们将在后面讨论的特定假设。
5.1 谐波参数化 Harmonic Parametrization
- 谐波参数化是一种单块、固定边界参数化算法,它将扁平网格的二维坐标计算为两个谐波函数。
- 该算法分为 3 个步骤:
- 检测边界顶点
- 将边界折点映射到圆
- 计算两个谐波函数(一个用于您,一个用于 v 坐标)。调和函数使用圆上的固定顶点作为边界约束。
- 可以使用 libigl 对算法进行编码,如下所示:
Eigen::VectorXi bnd;
igl::boundary_loop(V,F,bnd);
Eigen::MatrixXd bnd_uv;
igl::map_vertices_to_circle(V,bnd,bnd_uv);
igl::harmonic(V,F,bnd,bnd_uv,1,V_uv);
- 其中 bnd 包含边界顶点的索引,bnd_uv它们在UV平面上的位置,“1”表示我们要计算谐波函数(2表示双谐波,3表示三谐波等)。请注意,这三个函数中的每一个都设计为可在其他参数化算法中重用。
- UV参数化可以通过以下方式在查看器中可视化:
viewer.data().set_uv(V_uv);
- 然后使用UV坐标将程序棋盘纹理应用于网格(示例501)
- 501案例完整展示:
#include <igl/boundary_loop.h>
#include <igl/harmonic.h>
#include <igl/map_vertices_to_circle.h>
#include <igl/read_triangle_mesh.h>
#include <igl/opengl/glfw/Viewer.h>
int main(int argc, char *argv[])
{
Eigen::MatrixXd V, V_uv;
Eigen::MatrixXi F;
// Load a mesh in OFF format
igl::read_triangle_mesh(TUTORIAL_SHARED_PATH "/camelhead.off", V, F);
// Find the open boundary
Eigen::VectorXi bnd;
igl::boundary_loop(F,bnd);
// Map the boundary to a circle, preserving edge proportions
Eigen::MatrixXd bnd_uv;
igl::map_vertices_to_circle(V,bnd,bnd_uv);
// Harmonic parametrization for the internal vertices
igl::harmonic(V,F,bnd,bnd_uv,1,V_uv);
// Scale UV to make the texture more clear
V_uv *= 5;
// Plot the mesh
igl::opengl::glfw::Viewer viewer;
viewer.data().set_mesh(V, F);
viewer.data().set_uv(V_uv);
// Attach callback to allow toggling between 3D and 2D view
viewer.callback_key_pressed =
[&V,&V_uv,&F](igl::opengl::glfw::Viewer& viewer, unsigned int key, int /*mod*/)
{
if(key == '3' || key == '2')
{
// Plot the 3D mesh or 2D UV coordinates
viewer.data().set_vertices(key=='3'?V:V_uv);
viewer.data().compute_normals();
viewer.core().align_camera_center(key=='3'?V:V_uv,F);
// key press was used
return true;
}
// key press not used
return false;
};
// Disable wireframe
viewer.data().show_lines = false;
// Draw default checkerboard texture
viewer.data().show_texture = true;
std::cout<<R"(
3 Show 3D model
2 Show 2D parametrization
)";
// Launch the viewer
viewer.launch();
}
例 501)谐波参数化。(左)带纹理的网格,(右)带纹理的 UV 参数化
5.2 最小二乘共形映射 Least Squares Conformal Maps
-
最小二乘共形映射参数化可最大程度地减少参数化的共形(角度)变形。与谐波参数化不同,它不需要有固定的边界。
-
LSCM可减少以下能量:
-
可以以矩阵形式重写为:
- 其中 Lc 是余切拉普拉斯矩阵, A 是一个等于网格向量面积的 [u,v]^t A[u,v] 矩阵。
- 使用libigl,这个矩阵能量可以用几行代码编写。余切矩阵可以用以下公式 igl::cotmatrix 计算:
SparseMatrix<double> L;
igl::cotmatrix(V,F,L);
- 请注意,我们希望同时将拉普拉斯矩阵应用于 u 和 v 坐标,因此我们需要用 2x2 单位矩阵扩展它,取左克罗内克积:
SparseMatrix<double> L_flat;
igl::repdiag(L,2,L_flat);
- 面积矩阵的计算公式为 igl::vector_area_matrix :
SparseMatrix<double> A;
igl::vector_area_matrix(F,A);
- 最终的能量矩阵是 Lflat−2A 。请注意,在这种情况下,我们不需要固定边界。要消除能量的零空间并使最小值唯一,将两个任意顶点固定到两个任意位置就足够了。完整的源代码在例 502 中提供。
- 502案例完整展示:
#include <igl/boundary_loop.h>
#include <igl/readOFF.h>
#include <igl/opengl/glfw/Viewer.h>
#include <igl/lscm.h>
Eigen::MatrixXd V;
Eigen::MatrixXi F;
Eigen::MatrixXd V_uv;
bool key_down(igl::opengl::glfw::Viewer& viewer, unsigned char key, int modifier)
{
if (key == '1')
{
// Plot the 3D mesh
viewer.data().set_mesh(V,F);
viewer.core().align_camera_center(V,F);
}
else if (key == '2')
{
// Plot the mesh in 2D using the UV coordinates as vertex coordinates
viewer.data().set_mesh(V_uv,F);
viewer.core().align_camera_center(V_uv,F);
}
viewer.data().compute_normals();
return false;
}
int main(int argc, char *argv[])
{
using namespace Eigen;
using namespace std;
// Load a mesh in OFF format
igl::readOFF(TUTORIAL_SHARED_PATH "/camelhead.off", V, F);
// Fix two points on the boundary
VectorXi bnd,b(2,1);
igl::boundary_loop(F,bnd);
b(0) = bnd(0);
b(1) = bnd(bnd.size()/2);
MatrixXd bc(2,2);
bc<<0,0,1,0;
// LSCM parametrization
igl::lscm(V,F,b,bc,V_uv);
// Scale the uv
V_uv *= 5;
// Plot the mesh
igl::opengl::glfw::Viewer viewer;
viewer.data().set_mesh(V, F);
viewer.data().set_uv(V_uv);
viewer.callback_key_down = &key_down;
// Disable wireframe
viewer.data().show_lines = false;
// Draw checkerboard texture
viewer.data().show_texture = true;
// Launch the viewer
viewer.launch();
}
(例502)LSCM参数化。(左)带纹理的网格,(右)UV 参数化
5.3 尽可能刚性参数化 Least Squares Conformal Maps
- 尽可能刚性参数化是一种强大的单面体非线性算法,用于计算努力保持距离(以及角度)的参数化。这个想法与ARAP表面变形非常相似:每个三角形都映射到平面上,试图保持其原始形状,直到刚性旋转。
- 该算法可以重用变形一章中讨论的函数来实现: igl::arap_precomputation 和 igl::arap_solve 。唯一的区别是优化必须在 2D 而不是 3D 中完成,并且我们需要计算起点。虽然对于 3D 变形,优化是使用原始网格引导的,但 ARAP 参数化并非如此,因为起点必须是 2D 网格。在示例 503 中,我们使用谐波参数化初始化优化。与LSCM类似,边界可以自由变形,以尽量减少失真。
- 503案例完整展示:
#include <igl/arap.h>
#include <igl/boundary_loop.h>
#include <igl/harmonic.h>
#include <igl/map_vertices_to_circle.h>
#include <igl/readOFF.h>
#include <igl/opengl/glfw/Viewer.h>
Eigen::MatrixXd V;
Eigen::MatrixXi F;
Eigen::MatrixXd V_uv;
Eigen::MatrixXd initial_guess;
bool show_uv = false;
bool key_down(igl::opengl::glfw::Viewer& viewer, unsigned char key, int modifier)
{
if (key == '1')
show_uv = false;
else if (key == '2')
show_uv = true;
if (key == 'q')
V_uv = initial_guess;
if (show_uv)
{
viewer.data().set_mesh(V_uv,F);
viewer.core().align_camera_center(V_uv,F);
}
else
{
viewer.data().set_mesh(V,F);
viewer.core().align_camera_center(V,F);
}
viewer.data().compute_normals();
return false;
}
int main(int argc, char *argv[])
{
using namespace std;
// Load a mesh in OFF format
igl::readOFF(TUTORIAL_SHARED_PATH "/camelhead.off", V, F);
// Compute the initial solution for ARAP (harmonic parametrization)
Eigen::VectorXi bnd;
igl::boundary_loop(F,bnd);
Eigen::MatrixXd bnd_uv;
igl::map_vertices_to_circle(V,bnd,bnd_uv);
igl::harmonic(V,F,bnd,bnd_uv,1,initial_guess);
// Add dynamic regularization to avoid to specify boundary conditions
igl::ARAPData arap_data;
arap_data.with_dynamics = true;
Eigen::VectorXi b = Eigen::VectorXi::Zero(0);
Eigen::MatrixXd bc = Eigen::MatrixXd::Zero(0,0);
// Initialize ARAP
arap_data.max_iter = 100;
// 2 means that we're going to *solve* in 2d
arap_precomputation(V,F,2,b,arap_data);
// Solve arap using the harmonic map as initial guess
V_uv = initial_guess;
arap_solve(bc,arap_data,V_uv);
// Scale UV to make the texture more clear
V_uv *= 20;
// Plot the mesh
igl::opengl::glfw::Viewer viewer;
viewer.data().set_mesh(V, F);
viewer.data().set_uv(V_uv);
viewer.callback_key_down = &key_down;
// Disable wireframe
viewer.data().show_lines = false;
// Draw checkerboard texture
viewer.data().show_texture = true;
// Launch the viewer
viewer.launch();
}
(例503)尽可能刚性参数化。(左)带纹理的网格,(右)带纹理的 UV 参数化
5.4 N 旋转对称切场 N-rotationally Symmetric Tangent Fields
- 切线场设计是用于设计均匀四边形和六面体重新网格划分的引导场的基本工具。Libigl 包含所有最先进的算法的实现,用于设计 N-RoSy 场及其泛化。
- 在 libigl 中,切线单位长度向量场在三角形网格的面上是分段常数,并且它们由每个面的一个或多个向量描述。函数
igl::nrosy(V,F,b,bc,b_soft,b_soft_weight,bc_soft,N,0.5,
output_field,output_singularities);
-
创建一个平滑的单位长度向量场 (N=1),从一组稀疏的约束面开始,其索引列在 b 中,其约束值以 bc 指定。这些函数支持soft_constraints(b_soft、b_soft_weight、bc_soft),并返回三角形网格每个面的插值场 (output_field) 以及场的奇异点 (output_singularities)。
单位长度向量场的设计
-
奇点是场消失的顶点(在上图中以红色突出显示)。 igl::nrosy 还可以生成N-RoSy场,这是向量场 30 的推广,其中在每个面中向量被定义为恒定旋转 2π/N 。如下图所示,用不同N生成的字段的奇点是不同类型的,它们出现在不同的位置。
2、4和9RoSy油田的设计
-
我们在示例 504 中演示了如何调用和绘制 N-RoSy 字段,其中可以通过按数字键更改字段的度数。 igl::nrosy 实现了 中 26 提出的算法。N-RoSy 字段也可以与许多其他算法进行插值,请参阅库 libdirectional 以获取最流行算法的参考实现。有关各种应用程序中使用的字段的完整分类,请参阅 Vaxman 等人 2016。
-
504案例完整展示:(案例504~506直接运行跑不通需要解决对应报错)
#include <igl/avg_edge_length.h>
#include <igl/barycenter.h>
#include <igl/local_basis.h>
#include <igl/readOFF.h>
#include <igl/copyleft/comiso/nrosy.h>
#include <igl/opengl/glfw/Viewer.h>
#include <igl/PI.h>
// Mesh
Eigen::MatrixXd V;
Eigen::MatrixXi F;
// Constrained faces id
Eigen::VectorXi b;
// Cosntrained faces representative vector
Eigen::MatrixXd bc;
// Degree of the N-RoSy field
int N = 4;
// Converts a representative vector per face in the full set of vectors that describe
// an N-RoSy field
void representative_to_nrosy(
const Eigen::MatrixXd& V,
const Eigen::MatrixXi& F,
const Eigen::MatrixXd& R,
const int N,
Eigen::MatrixXd& Y)
{
using namespace Eigen;
using namespace std;
MatrixXd B1, B2, B3;
igl::local_basis(V,F,B1,B2,B3);
Y.resize(F.rows()*N,3);
for (unsigned i=0;i<F.rows();++i)
{
double x = R.row(i) * B1.row(i).transpose();
double y = R.row(i) * B2.row(i).transpose();
double angle = atan2(y,x);
for (unsigned j=0; j<N;++j)
{
double anglej = angle + 2*igl::PI*double(j)/double(N);
double xj = cos(anglej);
double yj = sin(anglej);
Y.row(i*N+j) = xj * B1.row(i) + yj * B2.row(i);
}
}
}
// Plots the mesh with an N-RoSy field and its singularities on top
// The constrained faces (b) are colored in red.
void plot_mesh_nrosy(
igl::opengl::glfw::Viewer& viewer,
Eigen::MatrixXd& V,
Eigen::MatrixXi& F,
int N,
Eigen::MatrixXd& PD1,
Eigen::VectorXd& S,
Eigen::VectorXi& b)
{
using namespace Eigen;
using namespace std;
// Clear the mesh
viewer.data().clear();
viewer.data().set_mesh(V,F);
// Expand the representative vectors in the full vector set and plot them as lines
double avg = igl::avg_edge_length(V, F);
MatrixXd Y;
representative_to_nrosy(V, F, PD1, N, Y);
MatrixXd B;
igl::barycenter(V,F,B);
MatrixXd Be(B.rows()*N,3);
for(unsigned i=0; i<B.rows();++i)
for(unsigned j=0; j<N; ++j)
Be.row(i*N+j) = B.row(i);
viewer.data().add_edges(Be,Be+Y*(avg/2),RowVector3d(0,0,1));
// Plot the singularities as colored dots (red for positive, blue for negative)
for (unsigned i=0; i<S.size();++i)
{
if (S(i) < -0.001)
viewer.data().add_points(V.row(i),RowVector3d(0,0,1));
else if (S(i) > 0.001)
viewer.data().add_points(V.row(i),RowVector3d(1,0,0));
}
// Highlight in red the constrained faces
MatrixXd C = MatrixXd::Constant(F.rows(),3,1);
for (unsigned i=0; i<b.size();++i)
C.row(b(i)) << 1, 0, 0;
viewer.data().set_colors(C);
}
// It allows to change the degree of the field when a number is pressed
bool key_down(igl::opengl::glfw::Viewer& viewer, unsigned char key, int modifier)
{
using namespace Eigen;
using namespace std;
if (key >= '1' && key <= '9')
N = key - '0';
MatrixXd R;
VectorXd S;
igl::copyleft::comiso::nrosy(V,F,b,bc,VectorXi(),VectorXd(),MatrixXd(),N,0.5,R,S);
plot_mesh_nrosy(viewer,V,F,N,R,S,b);
return false;
}
int main(int argc, char *argv[])
{
using namespace std;
using namespace Eigen;
// Load a mesh in OFF format
igl::readOFF(TUTORIAL_SHARED_PATH "/bumpy.off", V, F);
// Threshold faces with high anisotropy
b.resize(1);
b << 0;
bc.resize(1,3);
bc << 1,1,1;
igl::opengl::glfw::Viewer viewer;
// Interpolate the field and plot
key_down(viewer, '4', 0);
// Plot the mesh
viewer.data().set_mesh(V, F);
viewer.callback_key_down = &key_down;
// Disable wireframe
viewer.data().show_lines = false;
// Launch the viewer
viewer.launch();
}
2、4和9RoSy油田的设计
5.5 全局无缝整数网格参数化 Global, Seamless Integer-grid Parametrization
- 以前的参数化方法侧重于创建表面补丁的参数化,旨在纹理映射或烘焙其他表面属性,例如法线和高频细节。全局无缝参数化旨在使用与一组给定方向对齐的参数化对复杂形状进行参数化,以实现表面网格重新划分。在 libigl 中,我们提供了混合整数四边形论文 26 中提出的管道的参考实现。
- 第一步涉及设计一个 4-RoSy 场(有时称为交叉场),该场描述了所需四边形网格重新划分的边缘对齐方式。字段约束通常是手动指定或从主曲率方向中提取的。在示例 505 中,我们只是在随机方向上固定一个面。
初始交叉场规定边缘对齐
5.5.1 梳理和切割 Combing And Cutting
- 给定交叉场,我们现在想要切割表面,使其与圆盘同胚。虽然这可以直接在跨场上完成,但我们选择在其平分场(场旋转 45 度的副本)上执行此操作,因为它更稳定和通用。在平分线上的工作允许我们将广义、非正交和非单位长度的交叉场作为输入。
- 因此,我们旋转字段,505案例
Bisector field. 平分线字段
- 我们通过为每个面分配一个 U 和一个 V 方向来消除旋转歧义。作业是通过从随机面开始的广度优先搜索完成的。
Combed bisector field. 梳理平分线场
- 你可以把这个过程想象成梳理一个毛茸茸的表面:你将能够梳理它的一部分,但在某些时候你将无法始终如一地梳理整个表面(毛球定理)。梳理中的不连续性定义了切割图:
Cut graph. 剪切图
- 最后,我们将梳理的场旋转 45 度以撤消初始度数旋转:
Combed cross field. 梳理的横场
- 梳理的交叉场可以看作是参数化的理想雅可比量,将在下一节中计算。
5.5.2 泊松参数化 Poisson Parametrization
- 沿接缝切割网格并计算参数化,试图找到两个标量函数,其梯度与梳理的交叉场方向匹配。这是一个经典的泊松问题,可以最小化以下二次能量:
- 其中 Xu 和 表示 Xu 梳理的交叉字段。解决此问题会生成一个参数化,其 u 和 v 等值线与输入交叉场对齐
Poisson parametrization.泊松参数化
- 我们通过向泊松问题添加整数约束来隐藏接缝,以对齐每个接缝 26 两侧的等值线。
Seamless Poisson parametrization.无缝泊松参数化
- 请注意,此参数化只能用于重新划分网格,因为它包含许多重叠。
- 可以使用libQEx(不包括在libigl中)从该参数化中提取四边形网格。完整管道在示例 505 中实现。
- 505完整案例展示:
#include <igl/avg_edge_length.h>
#include <igl/barycenter.h>
#include <igl/comb_cross_field.h>
#include <igl/comb_frame_field.h>
#include <igl/compute_frame_field_bisectors.h>
#include <igl/cross_field_mismatch.h>
#include <igl/cut_mesh_from_singularities.h>
#include <igl/find_cross_field_singularities.h>
#include <igl/local_basis.h>
#include <igl/readOFF.h>
#include <igl/rotate_vectors.h>
#include <igl/copyleft/comiso/miq.h>
#include <igl/copyleft/comiso/nrosy.h>
#include <igl/opengl/glfw/Viewer.h>
#include <igl/PI.h>
#include <sstream>
#include <igl/serialize.h>
// Input mesh
Eigen::MatrixXd V;
Eigen::MatrixXi F;
// Face barycenters
Eigen::MatrixXd B;
// Scale for visualizing the fields
double global_scale;
bool extend_arrows = false;
// Cross field
Eigen::MatrixXd X1,X2;
// Bisector field
Eigen::MatrixXd BIS1, BIS2;
// Combed bisector
Eigen::MatrixXd BIS1_combed, BIS2_combed;
// Per-corner, integer mismatches
Eigen::Matrix<int, Eigen::Dynamic, 3> MMatch;
// Field singularities
Eigen::Matrix<int, Eigen::Dynamic, 1> isSingularity, singularityIndex;
// Per corner seams
Eigen::Matrix<int, Eigen::Dynamic, 3> Seams;
// Combed field
Eigen::MatrixXd X1_combed, X2_combed;
// Global parametrization (with seams)
Eigen::MatrixXd UV_seams;
Eigen::MatrixXi FUV_seams;
// Global parametrization
Eigen::MatrixXd UV;
Eigen::MatrixXi FUV;
// Create a texture that hides the integer translation in the parametrization
void line_texture(Eigen::Matrix<unsigned char,Eigen::Dynamic,Eigen::Dynamic> &texture_R,
Eigen::Matrix<unsigned char,Eigen::Dynamic,Eigen::Dynamic> &texture_G,
Eigen::Matrix<unsigned char,Eigen::Dynamic,Eigen::Dynamic> &texture_B)
{
unsigned size = 128;
unsigned size2 = size/2;
unsigned lineWidth = 3;
texture_R.setConstant(size, size, 255);
for (unsigned i=0; i<size; ++i)
for (unsigned j=size2-lineWidth; j<=size2+lineWidth; ++j)
texture_R(i,j) = 0;
for (unsigned i=size2-lineWidth; i<=size2+lineWidth; ++i)
for (unsigned j=0; j<size; ++j)
texture_R(i,j) = 0;
texture_G = texture_R;
texture_B = texture_R;
}
bool key_down(igl::opengl::glfw::Viewer& viewer, unsigned char key, int modifier)
{
if (key == 'E')
{
extend_arrows = !extend_arrows;
}
if (key <'1' || key >'8')
return false;
viewer.data().clear();
viewer.data().show_lines = false;
viewer.data().show_texture = false;
if (key == '1')
{
// Cross field
viewer.data().set_mesh(V, F);
viewer.data().add_edges(extend_arrows ? B - global_scale*X1 : B, B + global_scale*X1 ,Eigen::RowVector3d(1,0,0));
viewer.data().add_edges(extend_arrows ? B - global_scale*X2 : B, B + global_scale*X2 ,Eigen::RowVector3d(0,0,1));
}
if (key == '2')
{
// Bisector field
viewer.data().set_mesh(V, F);
viewer.data().add_edges(extend_arrows ? B - global_scale*BIS1 : B, B + global_scale*BIS1 ,Eigen::RowVector3d(1,0,0));
viewer.data().add_edges(extend_arrows ? B - global_scale*BIS2 : B, B + global_scale*BIS2 ,Eigen::RowVector3d(0,0,1));
}
if (key == '3')
{
// Bisector field combed
viewer.data().set_mesh(V, F);
viewer.data().add_edges(extend_arrows ? B - global_scale*BIS1_combed : B, B + global_scale*BIS1_combed ,Eigen::RowVector3d(1,0,0));
viewer.data().add_edges(extend_arrows ? B - global_scale*BIS2_combed : B, B + global_scale*BIS2_combed ,Eigen::RowVector3d(0,0,1));
}
if (key == '4')
{
// Singularities and cuts
viewer.data().set_mesh(V, F);
// Plot cuts
int l_count = Seams.sum();
Eigen::MatrixXd P1(l_count,3);
Eigen::MatrixXd P2(l_count,3);
for (unsigned i=0; i<Seams.rows(); ++i)
{
for (unsigned j=0; j<Seams.cols(); ++j)
{
if (Seams(i,j) != 0)
{
P1.row(l_count-1) = V.row(F(i,j));
P2.row(l_count-1) = V.row(F(i,(j+1)%3));
l_count--;
}
}
}
viewer.data().add_edges(P1, P2, Eigen::RowVector3d(1, 0, 0));
// Plot the singularities as colored dots (red for negative, blue for positive)
for (unsigned i=0; i<singularityIndex.size();++i)
{
if (singularityIndex(i) < 2 && singularityIndex(i) > 0)
viewer.data().add_points(V.row(i),Eigen::RowVector3d(1,0,0));
else if (singularityIndex(i) > 2)
viewer.data().add_points(V.row(i),Eigen::RowVector3d(0,1,0));
}
}
if (key == '5')
{
// Singularities and cuts, original field
// Singularities and cuts
viewer.data().set_mesh(V, F);
viewer.data().add_edges(extend_arrows ? B - global_scale*X1_combed : B, B + global_scale*X1_combed ,Eigen::RowVector3d(1,0,0));
viewer.data().add_edges(extend_arrows ? B - global_scale*X2_combed : B, B + global_scale*X2_combed ,Eigen::RowVector3d(0,0,1));
// Plot cuts
int l_count = Seams.sum();
Eigen::MatrixXd P1(l_count,3);
Eigen::MatrixXd P2(l_count,3);
for (unsigned i=0; i<Seams.rows(); ++i)
{
for (unsigned j=0; j<Seams.cols(); ++j)
{
if (Seams(i,j) != 0)
{
P1.row(l_count-1) = V.row(F(i,j));
P2.row(l_count-1) = V.row(F(i,(j+1)%3));
l_count--;
}
}
}
viewer.data().add_edges(P1, P2, Eigen::RowVector3d(1, 0, 0));
// Plot the singularities as colored dots (red for negative, blue for positive)
for (unsigned i=0; i<singularityIndex.size();++i)
{
if (singularityIndex(i) < 2 && singularityIndex(i) > 0)
viewer.data().add_points(V.row(i),Eigen::RowVector3d(1,0,0));
else if (singularityIndex(i) > 2)
viewer.data().add_points(V.row(i),Eigen::RowVector3d(0,1,0));
}
}
if (key == '6')
{
// Global parametrization UV
viewer.data().set_mesh(UV, FUV);
viewer.data().set_uv(UV);
viewer.data().show_lines = true;
}
if (key == '7')
{
// Global parametrization in 3D
viewer.data().set_mesh(V, F);
viewer.data().set_uv(UV,FUV);
viewer.data().show_texture = true;
}
if (key == '8')
{
// Global parametrization in 3D with seams
viewer.data().set_mesh(V, F);
viewer.data().set_uv(UV_seams,FUV_seams);
viewer.data().show_texture = true;
}
viewer.data().set_colors(Eigen::RowVector3d(1,1,1));
// Replace the standard texture with an integer shift invariant texture
Eigen::Matrix<unsigned char,Eigen::Dynamic,Eigen::Dynamic> texture_R, texture_G, texture_B;
line_texture(texture_R, texture_G, texture_B);
viewer.data().set_texture(texture_R, texture_B, texture_G);
viewer.core().align_camera_center(viewer.data().V,viewer.data().F);
return false;
}
int main(int argc, char *argv[])
{
using namespace Eigen;
// Load a mesh in OFF format
igl::readOFF(TUTORIAL_SHARED_PATH "/3holes.off", V, F);
double gradient_size = 50;
double iter = 0;
double stiffness = 5.0;
bool direct_round = 0;
// Compute face barycenters
igl::barycenter(V, F, B);
// Compute scale for visualizing fields
global_scale = .5*igl::avg_edge_length(V, F);
// Contrain one face
VectorXi b(1);
b << 0;
MatrixXd bc(1, 3);
bc << 1, 0, 0;
// Create a smooth 4-RoSy field
VectorXd S;
igl::copyleft::comiso::nrosy(V, F, b, bc, VectorXi(), VectorXd(), MatrixXd(), 4, 0.5, X1, S);
// Find the orthogonal vector
MatrixXd B1, B2, B3;
igl::local_basis(V, F, B1, B2, B3);
X2 = igl::rotate_vectors(X1, VectorXd::Constant(1, igl::PI / 2), B1, B2);
// Always work on the bisectors, it is more general
igl::compute_frame_field_bisectors(V, F, X1, X2, BIS1, BIS2);
// Comb the field, implicitly defining the seams
igl::comb_cross_field(V, F, BIS1, BIS2, BIS1_combed, BIS2_combed);
// Find the integer mismatches
igl::cross_field_mismatch(V, F, BIS1_combed, BIS2_combed, true, MMatch);
// Find the singularities
igl::find_cross_field_singularities(V, F, MMatch, isSingularity, singularityIndex);
// Cut the mesh, duplicating all vertices on the seams
igl::cut_mesh_from_singularities(V, F, MMatch, Seams);
// Comb the frame-field accordingly
igl::comb_frame_field(V, F, X1, X2, BIS1_combed, BIS2_combed, X1_combed, X2_combed);
// Global parametrization
igl::copyleft::comiso::miq(V,
F,
X1_combed,
X2_combed,
MMatch,
isSingularity,
Seams,
UV,
FUV,
gradient_size,
stiffness,
direct_round,
iter,
5,
true);
// Global parametrization (with seams, only for demonstration)
igl::copyleft::comiso::miq(V,
F,
X1_combed,
X2_combed,
MMatch,
isSingularity,
Seams,
UV_seams,
FUV_seams,
gradient_size,
stiffness,
direct_round,
iter,
5,
false);
// Plot the mesh
igl::opengl::glfw::Viewer viewer;
// Plot the original mesh with a texture parametrization
key_down(viewer,'7',0);
// Launch the viewer
viewer.callback_key_down = &key_down;
viewer.launch();
}
5.6 各向异性网格重新划分 Anisotropic Remeshing
- 各向异性和非均匀四边形重新划分网格对于将单元集中在具有更多细节的区域非常重要。可以使用网格变形方法 33 扩展MIQ四边形网格框架以生成各向异性四边形网格。
- 各向异性网格重新网格划分算法的输入是一组稀疏约束,用于定义所需四边形的形状和比例。这可以编码为帧字段,它是一对非正交和非单位长度向量。框架场可以通过在 4-RoSy 场和唯一的仿射变换中分解来插值。然后可以分别对两个部分进行插值,用于交叉场, igl::nrosy 谐波插值用于仿射部分。
帧场的插值。矢量上的颜色表示所需的比例。红色面包含帧场约束
- 插值后,对表面进行扭曲,将每个帧转换为正交和单位长度交叉(即从帧中删除缩放和偏斜)。此变形为曲面定义了新的嵌入(和新度量)。
曲面变形以变换交叉场中的框架场
- 变形的表面可以使用上一节中介绍的MIQ算法进行各向同性重新划分网格。
变形的表面进行各向同性重新划分网格
- 然后,可以使用变形表面的UV坐标将参数化传输到原始表面,其中等值线将跟踪四边形网格,其元素类似于输入帧字段中规定的形状。
全局参数化被提升到原始表面以创建各向异性四边形网格划分
- 我们的实现(示例506)使用MIQ生成UV参数化,但可以应用其他算法:唯一的缺点是生成的四边形网格应尽可能各向同性。
- 506案例完整展示:
#include <igl/avg_edge_length.h>
#include <igl/barycenter.h>
#include <igl/frame_field_deformer.h>
#include <igl/frame_to_cross_field.h>
#include <igl/jet.h>
#include <igl/local_basis.h>
#include <igl/readDMAT.h>
#include <igl/readOBJ.h>
#include <igl/rotate_vectors.h>
#include <igl/copyleft/comiso/nrosy.h>
#include <igl/copyleft/comiso/miq.h>
#include <igl/copyleft/comiso/frame_field.h>
#include <igl/opengl/glfw/Viewer.h>
#include <igl/PI.h>
// Input mesh
Eigen::MatrixXd V;
Eigen::MatrixXi F;
// Face barycenters
Eigen::MatrixXd B;
// Scale for visualizing the fields
double global_scale;
// Input frame field constraints
Eigen::VectorXi b;
Eigen::MatrixXd bc1;
Eigen::MatrixXd bc2;
// Interpolated frame field
Eigen::MatrixXd FF1, FF2;
// Deformed mesh
Eigen::MatrixXd V_deformed;
Eigen::MatrixXd B_deformed;
// Frame field on deformed
Eigen::MatrixXd FF1_deformed;
Eigen::MatrixXd FF2_deformed;
// Cross field on deformed
Eigen::MatrixXd X1_deformed;
Eigen::MatrixXd X2_deformed;
// Global parametrization
Eigen::MatrixXd V_uv;
Eigen::MatrixXi F_uv;
// Create a texture that hides the integer translation in the parametrization
void line_texture(Eigen::Matrix<unsigned char,Eigen::Dynamic,Eigen::Dynamic> &texture_R,
Eigen::Matrix<unsigned char,Eigen::Dynamic,Eigen::Dynamic> &texture_G,
Eigen::Matrix<unsigned char,Eigen::Dynamic,Eigen::Dynamic> &texture_B)
{
unsigned size = 128;
unsigned size2 = size/2;
unsigned lineWidth = 3;
texture_R.setConstant(size, size, 255);
for (unsigned i=0; i<size; ++i)
for (unsigned j=size2-lineWidth; j<=size2+lineWidth; ++j)
texture_R(i,j) = 0;
for (unsigned i=size2-lineWidth; i<=size2+lineWidth; ++i)
for (unsigned j=0; j<size; ++j)
texture_R(i,j) = 0;
texture_G = texture_R;
texture_B = texture_R;
}
bool key_down(igl::opengl::glfw::Viewer& viewer, unsigned char key, int modifier)
{
using namespace std;
using namespace Eigen;
if (key <'1' || key >'6')
return false;
viewer.data().clear();
viewer.data().show_lines = false;
viewer.data().show_texture = false;
if (key == '1')
{
// Frame field constraints
viewer.data().set_mesh(V, F);
MatrixXd F1_t = MatrixXd::Zero(FF1.rows(),FF1.cols());
MatrixXd F2_t = MatrixXd::Zero(FF2.rows(),FF2.cols());
// Highlight in red the constrained faces
MatrixXd C = MatrixXd::Constant(F.rows(),3,1);
for (unsigned i=0; i<b.size();++i)
{
C.row(b(i)) << 1, 0, 0;
F1_t.row(b(i)) = bc1.row(i);
F2_t.row(b(i)) = bc2.row(i);
}
viewer.data().set_colors(C);
MatrixXd C1,C2;
VectorXd K1 = F1_t.rowwise().norm();
VectorXd K2 = F2_t.rowwise().norm();
igl::jet(K1,true,C1);
igl::jet(K2,true,C2);
viewer.data().add_edges(B - global_scale*F1_t, B + global_scale*F1_t ,C1);
viewer.data().add_edges(B - global_scale*F2_t, B + global_scale*F2_t ,C2);
}
if (key == '2')
{
// Frame field
viewer.data().set_mesh(V, F);
MatrixXd C1,C2;
VectorXd K1 = FF1.rowwise().norm();
VectorXd K2 = FF2.rowwise().norm();
igl::jet(K1,true,C1);
igl::jet(K2,true,C2);
viewer.data().add_edges(B - global_scale*FF1, B + global_scale*FF1 ,C1);
viewer.data().add_edges(B - global_scale*FF2, B + global_scale*FF2 ,C2);
// Highlight in red the constrained faces
MatrixXd C = MatrixXd::Constant(F.rows(),3,1);
for (unsigned i=0; i<b.size();++i)
C.row(b(i)) << 1, 0, 0;
viewer.data().set_colors(C);
}
if (key == '3')
{
// Deformed with frame field
viewer.data().set_mesh(V_deformed, F);
viewer.data().add_edges(B_deformed - global_scale*FF1_deformed, B_deformed + global_scale*FF1_deformed ,Eigen::RowVector3d(1,0,0));
viewer.data().add_edges(B_deformed - global_scale*FF2_deformed, B_deformed + global_scale*FF2_deformed ,Eigen::RowVector3d(0,0,1));
viewer.data().set_colors(RowVector3d(1,1,1));
}
if (key == '4')
{
// Deformed with cross field
viewer.data().set_mesh(V_deformed, F);
viewer.data().add_edges(B_deformed - global_scale*X1_deformed, B_deformed + global_scale*X1_deformed ,Eigen::RowVector3d(0,0,1));
viewer.data().add_edges(B_deformed - global_scale*X2_deformed, B_deformed + global_scale*X2_deformed ,Eigen::RowVector3d(0,0,1));
viewer.data().set_colors(RowVector3d(1,1,1));
}
if (key == '5')
{
// Deformed with quad texture
viewer.data().set_mesh(V_deformed, F);
viewer.data().set_uv(V_uv,F_uv);
viewer.data().set_colors(RowVector3d(1,1,1));
viewer.data().show_texture = true;
}
if (key == '6')
{
// Deformed with quad texture
viewer.data().set_mesh(V, F);
viewer.data().set_uv(V_uv,F_uv);
viewer.data().set_colors(RowVector3d(1,1,1));
viewer.data().show_texture = true;
}
// Replace the standard texture with an integer shift invariant texture
Eigen::Matrix<unsigned char,Eigen::Dynamic,Eigen::Dynamic> texture_R, texture_G, texture_B;
line_texture(texture_R, texture_G, texture_B);
viewer.data().set_texture(texture_R, texture_B, texture_G);
viewer.core().align_camera_center(viewer.data().V,viewer.data().F);
return false;
}
int main(int argc, char *argv[])
{
using namespace Eigen;
// Load a mesh in OBJ format
igl::readOBJ(TUTORIAL_SHARED_PATH "/bumpy-cube.obj", V, F);
// Compute face barycenters
igl::barycenter(V, F, B);
// Compute scale for visualizing fields
global_scale = .2*igl::avg_edge_length(V, F);
// Load constraints
MatrixXd temp;
igl::readDMAT(TUTORIAL_SHARED_PATH "/bumpy-cube.dmat",temp);
b = temp.block(0,0,temp.rows(),1).cast<int>();
bc1 = temp.block(0,1,temp.rows(),3);
bc2 = temp.block(0,4,temp.rows(),3);
// Interpolate the frame field
igl::copyleft::comiso::frame_field(V, F, b, bc1, bc2, FF1, FF2);
// Deform the mesh to transform the frame field in a cross field
igl::frame_field_deformer(
V,F,FF1,FF2,V_deformed,FF1_deformed,FF2_deformed);
// Compute face barycenters deformed mesh
igl::barycenter(V_deformed, F, B_deformed);
// Find the closest crossfield to the deformed frame field
igl::frame_to_cross_field(V_deformed,F,FF1_deformed,FF2_deformed,X1_deformed);
// Find a smooth crossfield that interpolates the deformed constraints
MatrixXd bc_x(b.size(),3);
for (unsigned i=0; i<b.size();++i)
bc_x.row(i) = X1_deformed.row(b(i));
VectorXd S;
igl::copyleft::comiso::nrosy(
V,
F,
b,
bc_x,
VectorXi(),
VectorXd(),
MatrixXd(),
4,
0.5,
X1_deformed,
S);
// The other representative of the cross field is simply rotated by 90 degrees
MatrixXd B1,B2,B3;
igl::local_basis(V_deformed,F,B1,B2,B3);
X2_deformed =
igl::rotate_vectors(X1_deformed, VectorXd::Constant(1,igl::PI/2), B1, B2);
// Global seamless parametrization
igl::copyleft::comiso::miq(V_deformed,
F,
X1_deformed,
X2_deformed,
V_uv,
F_uv,
60.0,
5.0,
false,
2);
igl::opengl::glfw::Viewer viewer;
// Plot the original mesh with a texture parametrization
key_down(viewer,'6',0);
// Launch the viewer
viewer.callback_key_down = &key_down;
viewer.launch();
}
5.7 平坦化 Planarization
- 四边形网格可以通过 Shape-Up 27 转换为平面四边形网格,这是一种局部/全局方法,它使用全局步骤来强制表面连续性,使用局部步骤来强制平面性。
- 示例507对四边形网格进行平面化,直到它满足用户给定的平面度阈值。
- 507示例完整展示:
#include <igl/avg_edge_length.h>
#include <igl/barycenter.h>
#include <igl/jet.h>
#include <igl/planarize_quad_mesh.h>
#include <igl/quad_planarity.h>
#include <igl/readDMAT.h>
#include <igl/readOFF.h>
#include <igl/slice.h>
#include <igl/opengl/glfw/Viewer.h>
#include <vector>
#include <cstdlib>
// Quad mesh generated from conjugate field
Eigen::MatrixXd VQC;
Eigen::MatrixXi FQC;
Eigen::MatrixXi FQCtri;
Eigen::MatrixXd PQC0, PQC1, PQC2, PQC3;
// Planarized quad mesh
Eigen::MatrixXd VQCplan;
Eigen::MatrixXi FQCtriplan;
Eigen::MatrixXd PQC0plan, PQC1plan, PQC2plan, PQC3plan;
// Scale for visualizing the fields
double global_scale; //TODO: not used
bool key_down(igl::opengl::glfw::Viewer& viewer, unsigned char key, int modifier)
{
using namespace std;
using namespace Eigen;
// Plot the original quad mesh
if (key == '1')
{
// Draw the triangulated quad mesh
viewer.data().set_mesh(VQC, FQCtri);
// Assign a color to each quad that corresponds to its planarity
VectorXd planarity;
igl::quad_planarity( VQC, FQC, planarity);
MatrixXd Ct;
igl::jet(planarity, 0, 0.01, Ct);
MatrixXd C(FQCtri.rows(),3);
C << Ct, Ct;
viewer.data().set_colors(C);
// Plot a line for each edge of the quad mesh
viewer.data().add_edges(PQC0, PQC1, Eigen::RowVector3d(0,0,0));
viewer.data().add_edges(PQC1, PQC2, Eigen::RowVector3d(0,0,0));
viewer.data().add_edges(PQC2, PQC3, Eigen::RowVector3d(0,0,0));
viewer.data().add_edges(PQC3, PQC0, Eigen::RowVector3d(0,0,0));
}
// Plot the planarized quad mesh
if (key == '2')
{
// Draw the triangulated quad mesh
viewer.data().set_mesh(VQCplan, FQCtri);
// Assign a color to each quad that corresponds to its planarity
VectorXd planarity;
igl::quad_planarity( VQCplan, FQC, planarity);
MatrixXd Ct;
igl::jet(planarity, 0, 0.01, Ct);
MatrixXd C(FQCtri.rows(),3);
C << Ct, Ct;
viewer.data().set_colors(C);
// Plot a line for each edge of the quad mesh
viewer.data().add_edges(PQC0plan, PQC1plan, Eigen::RowVector3d(0,0,0));
viewer.data().add_edges(PQC1plan, PQC2plan, Eigen::RowVector3d(0,0,0));
viewer.data().add_edges(PQC2plan, PQC3plan, Eigen::RowVector3d(0,0,0));
viewer.data().add_edges(PQC3plan, PQC0plan, Eigen::RowVector3d(0,0,0));
}
return false;
}
int main(int argc, char *argv[])
{
using namespace Eigen;
using namespace std;
// Load a quad mesh generated by a conjugate field
igl::readOFF(TUTORIAL_SHARED_PATH "/inspired_mesh_quads_Conjugate.off", VQC, FQC);
// Convert it in a triangle mesh
FQCtri.resize(2*FQC.rows(), 3);
FQCtri << FQC.col(0),FQC.col(1),FQC.col(2),
FQC.col(2),FQC.col(3),FQC.col(0);
igl::slice( VQC, FQC.col(0).eval(), 1, PQC0);
igl::slice( VQC, FQC.col(1).eval(), 1, PQC1);
igl::slice( VQC, FQC.col(2).eval(), 1, PQC2);
igl::slice( VQC, FQC.col(3).eval(), 1, PQC3);
// Planarize it
igl::planarize_quad_mesh(VQC, FQC, 100, 0.005, VQCplan);
// Convert the planarized mesh to triangles
igl::slice( VQCplan, FQC.col(0).eval(), 1, PQC0plan);
igl::slice( VQCplan, FQC.col(1).eval(), 1, PQC1plan);
igl::slice( VQCplan, FQC.col(2).eval(), 1, PQC2plan);
igl::slice( VQCplan, FQC.col(3).eval(), 1, PQC3plan);
// Launch the viewer
igl::opengl::glfw::Viewer viewer;
key_down(viewer,'2',0);
viewer.data().invert_normals = true;
viewer.data().show_lines = false;
viewer.callback_key_down = &key_down;
viewer.launch();
}
非平面四边形网格(左)使用 libigl 函数 igl::p lanarize(右)平面化。颜色表示四边形的平面度