// This file is part of libigl, a simple c++ geometry processing library. // // Copyright (C) 2018 Zhongshi Jiang // // This Source Code Form is subject to the terms of the Mozilla Public License // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. #include "scaf.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mapping_energy_with_jacobians.h" #include #include #include #include #include namespace igl { namespace scaf { void update_scaffold(igl::SCAFData &s) { s.mv_num = s.m_V.rows(); s.mf_num = s.m_T.rows(); s.v_num = s.w_uv.rows(); s.sf_num = s.s_T.rows(); s.sv_num = s.v_num - s.mv_num; s.f_num = s.sf_num + s.mf_num; s.s_M = Eigen::VectorXd::Constant(s.sf_num, s.scaffold_factor); } void adjusted_grad(Eigen::MatrixXd &V, Eigen::MatrixXi &F, double area_threshold, Eigen::SparseMatrix &Dx, Eigen::SparseMatrix &Dy, Eigen::SparseMatrix &Dz) { Eigen::VectorXd M; igl::doublearea(V, F, M); std::vector degen; for (int i = 0; i < M.size(); i++) if (M(i) < area_threshold) degen.push_back(i); Eigen::SparseMatrix G; igl::grad(V, F, G); Dx = G.topRows(F.rows()); Dy = G.block(F.rows(), 0, F.rows(), V.rows()); Dz = G.bottomRows(F.rows()); // handcraft uniform gradient for faces area falling below threshold. double sin60 = std::sin(igl::PI / 3); double cos60 = std::cos(igl::PI / 3); double deno = std::sqrt(sin60 * area_threshold); Eigen::MatrixXd standard_grad(3, 3); standard_grad << -sin60 / deno, sin60 / deno, 0, -cos60 / deno, -cos60 / deno, 1 / deno, 0, 0, 0; for (auto k : degen) for (int j = 0; j < 3; j++) { Dx.coeffRef(k, F(k, j)) = standard_grad(0, j); Dy.coeffRef(k, F(k, j)) = standard_grad(1, j); Dz.coeffRef(k, F(k, j)) = standard_grad(2, j); } } void compute_scaffold_gradient_matrix(SCAFData &s, Eigen::SparseMatrix &D1, Eigen::SparseMatrix &D2) { using namespace Eigen; Eigen::SparseMatrix G; MatrixXi F_s = s.s_T; int vn = s.v_num; MatrixXd V = MatrixXd::Zero(vn, 3); V.leftCols(2) = s.w_uv; double min_bnd_edge_len = INFINITY; int acc_bnd = 0; for (int i = 0; i < s.bnd_sizes.size(); i++) { int current_size = s.bnd_sizes[i]; for (int e = acc_bnd; e < acc_bnd + current_size - 1; e++) { min_bnd_edge_len = (std::min)(min_bnd_edge_len, (s.w_uv.row(s.internal_bnd(e)) - s.w_uv.row(s.internal_bnd(e + 1))) .squaredNorm()); } min_bnd_edge_len = (std::min)(min_bnd_edge_len, (s.w_uv.row(s.internal_bnd(acc_bnd)) - s.w_uv.row(s.internal_bnd(acc_bnd + current_size - 1))) .squaredNorm()); acc_bnd += current_size; } double area_threshold = min_bnd_edge_len / 4.0; Eigen::SparseMatrix Dx, Dy, Dz; adjusted_grad(V, F_s, area_threshold, Dx, Dy, Dz); MatrixXd F1, F2, F3; igl::local_basis(V, F_s, F1, F2, F3); D1 = F1.col(0).asDiagonal() * Dx + F1.col(1).asDiagonal() * Dy + F1.col(2).asDiagonal() * Dz; D2 = F2.col(0).asDiagonal() * Dx + F2.col(1).asDiagonal() * Dy + F2.col(2).asDiagonal() * Dz; } void mesh_improve(igl::SCAFData &s) { using namespace Eigen; MatrixXd m_uv = s.w_uv.topRows(s.mv_num); MatrixXd V_bnd; V_bnd.resize(s.internal_bnd.size(), 2); for (int i = 0; i < s.internal_bnd.size(); i++) // redoing step 1. { V_bnd.row(i) = m_uv.row(s.internal_bnd(i)); } if (s.rect_frame_V.size() == 0) { Matrix2d ob; // = rect_corners; { VectorXd uv_max = m_uv.colwise().maxCoeff(); VectorXd uv_min = m_uv.colwise().minCoeff(); VectorXd uv_mid = (uv_max + uv_min) / 2.; Eigen::Array2d scaf_range(3, 3); ob.row(0) = uv_mid.array() + scaf_range * ((uv_min - uv_mid).array()); ob.row(1) = uv_mid.array() + scaf_range * ((uv_max - uv_mid).array()); } Vector2d rect_len; rect_len << ob(1, 0) - ob(0, 0), ob(1, 1) - ob(0, 1); int frame_points = 5; s.rect_frame_V.resize(4 * frame_points, 2); for (int i = 0; i < frame_points; i++) { // 0,0;0,1 s.rect_frame_V.row(i) << ob(0, 0), ob(0, 1) + i * rect_len(1) / frame_points; // 0,0;1,1 s.rect_frame_V.row(i + frame_points) << ob(0, 0) + i * rect_len(0) / frame_points, ob(1, 1); // 1,0;1,1 s.rect_frame_V.row(i + 2 * frame_points) << ob(1, 0), ob(1, 1) - i * rect_len(1) / frame_points; // 1,0;0,1 s.rect_frame_V.row(i + 3 * frame_points) << ob(1, 0) - i * rect_len(0) / frame_points, ob(0, 1); // 0,0;0,1 } s.frame_ids = Eigen::VectorXi::LinSpaced(s.rect_frame_V.rows(), s.mv_num, s.mv_num + s.rect_frame_V.rows()); } // Concatenate Vert and Edge MatrixXd V; MatrixXi E; igl::cat(1, V_bnd, s.rect_frame_V, V); E.resize(V.rows(), 2); for (int i = 0; i < E.rows(); i++) E.row(i) << i, i + 1; int acc_bs = 0; for (auto bs : s.bnd_sizes) { E(acc_bs + bs - 1, 1) = acc_bs; acc_bs += bs; } E(V.rows() - 1, 1) = acc_bs; assert(acc_bs == s.internal_bnd.size()); MatrixXd H = MatrixXd::Zero(s.component_sizes.size(), 2); { int hole_f = 0; int hole_i = 0; for (auto cs : s.component_sizes) { for (int i = 0; i < 3; i++) H.row(hole_i) += m_uv.row(s.m_T(hole_f, i)); // redoing step 2 hole_f += cs; hole_i++; } } H /= 3.; MatrixXd uv2; igl::triangle::triangulate(V, E, H, std::basic_string("qYYQ"), uv2, s.s_T); auto bnd_n = s.internal_bnd.size(); for (auto i = 0; i < s.s_T.rows(); i++) for (auto j = 0; j < s.s_T.cols(); j++) { auto &x = s.s_T(i, j); if (x < bnd_n) x = s.internal_bnd(x); else x += m_uv.rows() - bnd_n; } igl::cat(1, s.m_T, s.s_T, s.w_T); s.w_uv.conservativeResize(m_uv.rows() - bnd_n + uv2.rows(), 2); s.w_uv.bottomRows(uv2.rows() - bnd_n) = uv2.bottomRows(-bnd_n + uv2.rows()); update_scaffold(s); // after_mesh_improve compute_scaffold_gradient_matrix(s, s.Dx_s, s.Dy_s); s.Dx_s.makeCompressed(); s.Dy_s.makeCompressed(); s.Dz_s.makeCompressed(); s.Ri_s = MatrixXd::Zero(s.Dx_s.rows(), s.dim * s.dim); s.Ji_s.resize(s.Dx_s.rows(), s.dim * s.dim); s.W_s.resize(s.Dx_s.rows(), s.dim * s.dim); } void add_new_patch(igl::SCAFData &s, const Eigen::MatrixXd &V_ref, const Eigen::MatrixXi &F_ref, const Eigen::RowVectorXd ¢er, const Eigen::MatrixXd &uv_init) { using namespace std; using namespace Eigen; assert(uv_init.rows() != 0); Eigen::VectorXd M; igl::doublearea(V_ref, F_ref, M); s.mesh_measure += M.sum() / 2; Eigen::VectorXi bnd; Eigen::MatrixXd bnd_uv; std::vector> all_bnds; igl::boundary_loop(F_ref, all_bnds); int num_holes = all_bnds.size() - 1; s.component_sizes.push_back(F_ref.rows()); MatrixXd m_uv = s.w_uv.topRows(s.mv_num); igl::cat(1, m_uv, uv_init, s.w_uv); s.m_M.conservativeResize(s.mf_num + M.size()); s.m_M.bottomRows(M.size()) = M / 2; for (auto cur_bnd : all_bnds) { s.internal_bnd.conservativeResize(s.internal_bnd.size() + cur_bnd.size()); s.internal_bnd.bottomRows(cur_bnd.size()) = Map(cur_bnd.data(), cur_bnd.size()) + s.mv_num; s.bnd_sizes.push_back(cur_bnd.size()); } s.m_T.conservativeResize(s.mf_num + F_ref.rows(), 3); s.m_T.bottomRows(F_ref.rows()) = F_ref.array() + s.mv_num; s.mf_num += F_ref.rows(); s.m_V.conservativeResize(s.mv_num + V_ref.rows(), 3); s.m_V.bottomRows(V_ref.rows()) = V_ref; s.mv_num += V_ref.rows(); s.rect_frame_V = MatrixXd(); mesh_improve(s); } void compute_jacobians(SCAFData &s, const Eigen::MatrixXd &V_new, bool whole) { auto comp_J2 = [](const Eigen::MatrixXd &uv, const Eigen::SparseMatrix &Dx, const Eigen::SparseMatrix &Dy, Eigen::MatrixXd &Ji) { // Ji=[D1*u,D2*u,D1*v,D2*v]; Ji.resize(Dx.rows(), 4); Ji.col(0) = Dx * uv.col(0); Ji.col(1) = Dy * uv.col(0); Ji.col(2) = Dx * uv.col(1); Ji.col(3) = Dy * uv.col(1); }; Eigen::MatrixXd m_V_new = V_new.topRows(s.mv_num); comp_J2(m_V_new, s.Dx_m, s.Dy_m, s.Ji_m); if (whole) comp_J2(V_new, s.Dx_s, s.Dy_s, s.Ji_s); } double compute_energy_from_jacobians(const Eigen::MatrixXd &Ji, const Eigen::VectorXd &areas, igl::MappingEnergyType energy_type) { double energy = 0; if (energy_type == igl::MappingEnergyType::SYMMETRIC_DIRICHLET) energy = -4; // comply with paper description return energy + igl::mapping_energy_with_jacobians(Ji, areas, energy_type, 0); } double compute_soft_constraint_energy(const SCAFData &s) { double e = 0; for (auto const &x : s.soft_cons) e += s.soft_const_p * (x.second - s.w_uv.row(x.first)).squaredNorm(); return e; } double compute_energy(SCAFData &s, Eigen::MatrixXd &w_uv, bool whole) { if (w_uv.rows() != s.v_num) assert(!whole); compute_jacobians(s, w_uv, whole); double energy = compute_energy_from_jacobians(s.Ji_m, s.m_M, s.slim_energy); if (whole) energy += compute_energy_from_jacobians(s.Ji_s, s.s_M, s.scaf_energy); energy += compute_soft_constraint_energy(s); return energy; } void buildAm(const Eigen::VectorXd &sqrt_M, const Eigen::SparseMatrix &Dx, const Eigen::SparseMatrix &Dy, const Eigen::MatrixXd &W, Eigen::SparseMatrix &Am) { std::vector> IJV; Eigen::SparseMatrix Dz; Eigen::SparseMatrix MDx = sqrt_M.asDiagonal() * Dx; Eigen::SparseMatrix MDy = sqrt_M.asDiagonal() * Dy; igl::slim_buildA(MDx, MDy, Dz, W, IJV); Am.setFromTriplets(IJV.begin(), IJV.end()); Am.makeCompressed(); } void buildRhs(const Eigen::VectorXd &sqrt_M, const Eigen::MatrixXd &W, const Eigen::MatrixXd &Ri, Eigen::VectorXd &f_rhs) { const int dim = (W.cols() == 4) ? 2 : 3; const int f_n = W.rows(); f_rhs.resize(dim * dim * f_n); for (int i = 0; i < f_n; i++) { auto sqrt_area = sqrt_M(i); f_rhs(i + 0 * f_n) = sqrt_area * (W(i, 0) * Ri(i, 0) + W(i, 1) * Ri(i, 1)); f_rhs(i + 1 * f_n) = sqrt_area * (W(i, 0) * Ri(i, 2) + W(i, 1) * Ri(i, 3)); f_rhs(i + 2 * f_n) = sqrt_area * (W(i, 2) * Ri(i, 0) + W(i, 3) * Ri(i, 1)); f_rhs(i + 3 * f_n) = sqrt_area * (W(i, 2) * Ri(i, 2) + W(i, 3) * Ri(i, 3)); } } void get_complement(const Eigen::VectorXi &bnd_ids, int v_n, Eigen::ArrayXi &unknown_ids) { // get the complement of bnd_ids. int assign = 0, i = 0; for (int get = 0; i < v_n && get < bnd_ids.size(); i++) { if (bnd_ids(get) == i) get++; else unknown_ids(assign++) = i; } while (i < v_n) unknown_ids(assign++) = i++; assert(assign + bnd_ids.size() == v_n); } void build_surface_linear_system(const SCAFData &s, Eigen::SparseMatrix &L, Eigen::VectorXd &rhs) { using namespace Eigen; using namespace std; const int v_n = s.v_num - (s.frame_ids.size()); const int dim = s.dim; const int f_n = s.mf_num; // to get the complete A Eigen::VectorXd sqrtM = s.m_M.array().sqrt(); Eigen::SparseMatrix A(dim * dim * f_n, dim * v_n); auto decoy_Dx_m = s.Dx_m; decoy_Dx_m.conservativeResize(s.W_m.rows(), v_n); auto decoy_Dy_m = s.Dy_m; decoy_Dy_m.conservativeResize(s.W_m.rows(), v_n); buildAm(sqrtM, decoy_Dx_m, decoy_Dy_m, s.W_m, A); const VectorXi &bnd_ids = s.fixed_ids; auto bnd_n = bnd_ids.size(); if (bnd_n == 0) { Eigen::SparseMatrix At = A.transpose(); At.makeCompressed(); Eigen::SparseMatrix id_m(At.rows(), At.rows()); id_m.setIdentity(); L = At * A; Eigen::VectorXd frhs; buildRhs(sqrtM, s.W_m, s.Ri_m, frhs); rhs = At * frhs; } else { MatrixXd bnd_pos; igl::slice(s.w_uv, bnd_ids, 1, bnd_pos); ArrayXi known_ids(bnd_ids.size() * dim); ArrayXi unknown_ids((v_n - bnd_ids.rows()) * dim); get_complement(bnd_ids, v_n, unknown_ids); VectorXd known_pos(bnd_ids.size() * dim); for (int d = 0; d < dim; d++) { auto n_b = bnd_ids.rows(); known_ids.segment(d * n_b, n_b) = bnd_ids.array() + d * v_n; known_pos.segment(d * n_b, n_b) = bnd_pos.col(d); unknown_ids.block(d * (v_n - n_b), 0, v_n - n_b, unknown_ids.cols()) = unknown_ids.topRows(v_n - n_b) + d * v_n; } Eigen::SparseMatrix Au, Ae; igl::slice(A, unknown_ids, 2, Au); igl::slice(A, known_ids, 2, Ae); Eigen::SparseMatrix Aut = Au.transpose(); Aut.makeCompressed(); L = Aut * Au; Eigen::VectorXd frhs; buildRhs(sqrtM, s.W_m, s.Ri_m, frhs); rhs = Aut * (frhs - Ae * known_pos); } // add soft constraints. for (auto const &x : s.soft_cons) { int v_idx = x.first; for (int d = 0; d < dim; d++) { rhs(d * (v_n) + v_idx) += s.soft_const_p * x.second(d); // rhs L.coeffRef(d * v_n + v_idx, d * v_n + v_idx) += s.soft_const_p; // diagonal } } } void build_scaffold_linear_system(const SCAFData &s, Eigen::SparseMatrix &L, Eigen::VectorXd &rhs) { using namespace Eigen; const int f_n = s.W_s.rows(); const int v_n = s.Dx_s.cols(); const int dim = s.dim; Eigen::VectorXd sqrtM = s.s_M.array().sqrt(); Eigen::SparseMatrix A(dim * dim * f_n, dim * v_n); buildAm(sqrtM, s.Dx_s, s.Dy_s, s.W_s, A); VectorXi bnd_ids; igl::cat(1, s.fixed_ids, s.frame_ids, bnd_ids); auto bnd_n = bnd_ids.size(); assert(bnd_n > 0); MatrixXd bnd_pos; igl::slice(s.w_uv, bnd_ids, 1, bnd_pos); ArrayXi known_ids(bnd_ids.size() * dim); ArrayXi unknown_ids((v_n - bnd_ids.rows()) * dim); get_complement(bnd_ids, v_n, unknown_ids); VectorXd known_pos(bnd_ids.size() * dim); for (int d = 0; d < dim; d++) { auto n_b = bnd_ids.rows(); known_ids.segment(d * n_b, n_b) = bnd_ids.array() + d * v_n; known_pos.segment(d * n_b, n_b) = bnd_pos.col(d); unknown_ids.block(d * (v_n - n_b), 0, v_n - n_b, unknown_ids.cols()) = unknown_ids.topRows(v_n - n_b) + d * v_n; } Eigen::VectorXd sqrt_M = s.s_M.array().sqrt(); // manual slicing for A(:, unknown/known)' Eigen::SparseMatrix Au, Ae; igl::slice(A, unknown_ids, 2, Au); igl::slice(A, known_ids, 2, Ae); Eigen::SparseMatrix Aut = Au.transpose(); Aut.makeCompressed(); L = Aut * Au; Eigen::VectorXd frhs; buildRhs(sqrtM, s.W_s, s.Ri_s, frhs); rhs = Aut * (frhs - Ae * known_pos); } void solve_weighted_arap(SCAFData &s, Eigen::MatrixXd &uv) { using namespace Eigen; using namespace std; int dim = s.dim; igl::Timer timer; timer.start(); VectorXi bnd_ids; igl::cat(1, s.fixed_ids, s.frame_ids, bnd_ids); const auto v_n = s.v_num; const auto bnd_n = bnd_ids.size(); assert(bnd_n > 0); MatrixXd bnd_pos; igl::slice(s.w_uv, bnd_ids, 1, bnd_pos); ArrayXi known_ids(bnd_n * dim); ArrayXi unknown_ids((v_n - bnd_n) * dim); get_complement(bnd_ids, v_n, unknown_ids); VectorXd known_pos(bnd_ids.size() * dim); for (int d = 0; d < dim; d++) { auto n_b = bnd_ids.rows(); known_ids.segment(d * n_b, n_b) = bnd_ids.array() + d * v_n; known_pos.segment(d * n_b, n_b) = bnd_pos.col(d); unknown_ids.block(d * (v_n - n_b), 0, v_n - n_b, unknown_ids.cols()) = unknown_ids.topRows(v_n - n_b) + d * v_n; } Eigen::SparseMatrix L; Eigen::VectorXd rhs; // fixed frame solving: // x_e as the fixed frame, x_u for unknowns (mesh + unknown scaffold) // min ||(A_u*x_u + A_e*x_e) - b||^2 // => A_u'*A_u*x_u = Au'* (b - A_e*x_e) := Au'* b_u // // separate matrix build: // min ||A_m x_m - b_m||^2 + ||A_s x_all - b_s||^2 + soft + proximal // First change dimension of A_m to fit for x_all // (Not just at the end, since x_all is flattened along dimensions) // L = A_m'*A_m + A_s'*A_s + soft + proximal // rhs = A_m'* b_m + A_s' * b_s + soft + proximal // Eigen::SparseMatrix L_m, L_s; Eigen::VectorXd rhs_m, rhs_s; build_surface_linear_system(s, L_m, rhs_m); // complete Am, with soft build_scaffold_linear_system(s, L_s, rhs_s); // complete As, without proximal L = L_m + L_s; rhs = rhs_m + rhs_s; L.makeCompressed(); Eigen::VectorXd unknown_Uc((v_n - s.frame_ids.size() - s.fixed_ids.size()) * dim), Uc(dim * v_n); SimplicialLDLT> solver; unknown_Uc = solver.compute(L).solve(rhs); igl::slice_into(unknown_Uc, unknown_ids.matrix(), 1, Uc); igl::slice_into(known_pos, known_ids.matrix(), 1, Uc); uv = Map>(Uc.data(), v_n, dim); } double perform_iteration(SCAFData &s) { Eigen::MatrixXd V_out = s.w_uv; compute_jacobians(s, V_out, true); igl::slim_update_weights_and_closest_rotations_with_jacobians(s.Ji_m, s.slim_energy, 0, s.W_m, s.Ri_m); igl::slim_update_weights_and_closest_rotations_with_jacobians(s.Ji_s, s.scaf_energy, 0, s.W_s, s.Ri_s); solve_weighted_arap(s, V_out); auto whole_E = [&s](Eigen::MatrixXd &uv) { return compute_energy(s, uv, true); }; Eigen::MatrixXi w_T; if (s.m_T.cols() == s.s_T.cols()) igl::cat(1, s.m_T, s.s_T, w_T); else w_T = s.s_T; return igl::flip_avoiding_line_search(w_T, s.w_uv, V_out, whole_E, -1) / s.mesh_measure; } } } IGL_INLINE void igl::scaf_precompute( const Eigen::MatrixXd &V, const Eigen::MatrixXi &F, const Eigen::MatrixXd &V_init, igl::SCAFData &data, igl::MappingEnergyType slim_energy, Eigen::VectorXi &b, Eigen::MatrixXd &bc, double soft_p) { Eigen::MatrixXd CN; Eigen::MatrixXi FN; igl::scaf::add_new_patch(data, V, F, Eigen::RowVector2d(0, 0), V_init); data.soft_const_p = soft_p; for (int i = 0; i < b.rows(); i++) data.soft_cons[b(i)] = bc.row(i); data.slim_energy = slim_energy; auto &s = data; if (!data.has_pre_calc) { int v_n = s.mv_num + s.sv_num; int f_n = s.mf_num + s.sf_num; int dim = s.dim; Eigen::MatrixXd F1, F2, F3; igl::local_basis(s.m_V, s.m_T, F1, F2, F3); auto face_proj = [](Eigen::MatrixXd& F){ std::vector >IJV; int f_num = F.rows(); for(int i=0; i(i, i, F(i,0))); IJV.push_back(Eigen::Triplet(i, i+f_num, F(i,1))); IJV.push_back(Eigen::Triplet(i, i+2*f_num, F(i,2))); } Eigen::SparseMatrix P(f_num, 3*f_num); P.setFromTriplets(IJV.begin(), IJV.end()); return P; }; Eigen::SparseMatrix G; igl::grad(s.m_V, s.m_T, G); s.Dx_m = face_proj(F1) * G; s.Dy_m = face_proj(F2) * G; igl::scaf::compute_scaffold_gradient_matrix(s, s.Dx_s, s.Dy_s); s.Dx_m.makeCompressed(); s.Dy_m.makeCompressed(); s.Ri_m = Eigen::MatrixXd::Zero(s.Dx_m.rows(), dim * dim); s.Ji_m.resize(s.Dx_m.rows(), dim * dim); s.W_m.resize(s.Dx_m.rows(), dim * dim); s.Dx_s.makeCompressed(); s.Dy_s.makeCompressed(); s.Ri_s = Eigen::MatrixXd::Zero(s.Dx_s.rows(), dim * dim); s.Ji_s.resize(s.Dx_s.rows(), dim * dim); s.W_s.resize(s.Dx_s.rows(), dim * dim); data.has_pre_calc = true; } } IGL_INLINE Eigen::MatrixXd igl::scaf_solve(SCAFData &s, int iter_num) { using namespace std; using namespace Eigen; s.energy = igl::scaf::compute_energy(s, s.w_uv, false) / s.mesh_measure; for (int it = 0; it < iter_num; it++) { s.total_energy = igl::scaf::compute_energy(s, s.w_uv, true) / s.mesh_measure; s.rect_frame_V = Eigen::MatrixXd(); igl::scaf::mesh_improve(s); double new_weight = s.mesh_measure * s.energy / (s.sf_num * 100); s.scaffold_factor = new_weight; igl::scaf::update_scaffold(s); s.total_energy = igl::scaf::perform_iteration(s); s.energy = igl::scaf::compute_energy(s, s.w_uv, false) / s.mesh_measure; } return s.w_uv.topRows(s.mv_num); } #ifdef IGL_STATIC_LIBRARY #endif