|
|
/**************************************************************************** |
|
|
* VCGLib o o * |
|
|
* Visual and Computer Graphics Library o o * |
|
|
* _ O _ * |
|
|
* Copyright(C) 2004-2016 \/)\/ * |
|
|
* Visual Computing Lab /\/| * |
|
|
* ISTI - Italian National Research Council | * |
|
|
* \ * |
|
|
* All rights reserved. * |
|
|
* * |
|
|
* This program is free software; you can redistribute it and/or modify * |
|
|
* it under the terms of the GNU General Public License as published by * |
|
|
* the Free Software Foundation; either version 2 of the License, or * |
|
|
* (at your option) any later version. * |
|
|
* * |
|
|
* This program is distributed in the hope that it will be useful, * |
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of * |
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
|
|
* GNU General Public License (http://www.gnu.org/licenses/gpl.txt) * |
|
|
* for more details. * |
|
|
* * |
|
|
****************************************************************************/ |
|
|
|
|
|
#ifndef __VCGLIB__SMOOTH |
|
|
#define __VCGLIB__SMOOTH |
|
|
|
|
|
#include <vcg/space/ray3.h> |
|
|
#include <vcg/complex/algorithms/update/normal.h> |
|
|
#include <vcg/complex/algorithms/update/halfedge_topology.h> |
|
|
#include <vcg/complex/algorithms/closest.h> |
|
|
#include <vcg/space/index/kdtree/kdtree.h> |
|
|
|
|
|
namespace vcg |
|
|
{ |
|
|
namespace tri |
|
|
{ |
|
|
/// |
|
|
/** \addtogroup trimesh */ |
|
|
/*@{*/ |
|
|
/// Class of static functions to smooth and fair meshes and their attributes. |
|
|
|
|
|
template <class SmoothMeshType> |
|
|
class Smooth |
|
|
{ |
|
|
|
|
|
public: |
|
|
typedef SmoothMeshType MeshType; |
|
|
typedef typename MeshType::VertexType VertexType; |
|
|
typedef typename MeshType::VertexType::CoordType CoordType; |
|
|
typedef typename MeshType::VertexPointer VertexPointer; |
|
|
typedef typename MeshType::VertexIterator VertexIterator; |
|
|
typedef typename MeshType::ScalarType ScalarType; |
|
|
typedef typename MeshType::FaceType FaceType; |
|
|
typedef typename MeshType::FacePointer FacePointer; |
|
|
typedef typename MeshType::FaceIterator FaceIterator; |
|
|
typedef typename MeshType::FaceContainer FaceContainer; |
|
|
typedef typename MeshType::TetraType TetraType; |
|
|
typedef typename vcg::Box3<ScalarType> Box3Type; |
|
|
typedef typename vcg::face::VFIterator<FaceType> VFLocalIterator; |
|
|
|
|
|
class ScaleLaplacianInfo |
|
|
{ |
|
|
public: |
|
|
CoordType PntSum; |
|
|
ScalarType LenSum; |
|
|
}; |
|
|
|
|
|
// This is precisely what curvature flow does. |
|
|
// Curvature flow smoothes the surface by moving along the surface |
|
|
// normal n with a speed equal to the mean curvature |
|
|
void VertexCoordLaplacianCurvatureFlow(MeshType & /*m*/, int /*step*/, ScalarType /*delta*/) |
|
|
{ |
|
|
} |
|
|
|
|
|
// Another Laplacian smoothing variant, |
|
|
// here we sum the baricenter of the faces incidents on each vertex weighting them with the angle |
|
|
|
|
|
static void VertexCoordLaplacianAngleWeighted(MeshType &m, int step, ScalarType delta) |
|
|
{ |
|
|
ScaleLaplacianInfo lpz; |
|
|
lpz.PntSum = CoordType(0, 0, 0); |
|
|
lpz.LenSum = 0; |
|
|
SimpleTempData<typename MeshType::VertContainer, ScaleLaplacianInfo> TD(m.vert, lpz); |
|
|
FaceIterator fi; |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
TD[*vi] = lpz; |
|
|
ScalarType a[3]; |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
{ |
|
|
CoordType mp = ((*fi).V(0)->P() + (*fi).V(1)->P() + (*fi).V(2)->P()) / 3.0; |
|
|
CoordType e0 = ((*fi).V(0)->P() - (*fi).V(1)->P()).Normalize(); |
|
|
CoordType e1 = ((*fi).V(1)->P() - (*fi).V(2)->P()).Normalize(); |
|
|
CoordType e2 = ((*fi).V(2)->P() - (*fi).V(0)->P()).Normalize(); |
|
|
|
|
|
a[0] = AngleN(-e0, e2); |
|
|
a[1] = AngleN(-e1, e0); |
|
|
a[2] = AngleN(-e2, e1); |
|
|
//assert(fabs(M_PI -a[0] -a[1] -a[2])<0.0000001); |
|
|
|
|
|
for (int j = 0; j < 3; ++j) |
|
|
{ |
|
|
CoordType dir = (mp - (*fi).V(j)->P()).Normalize(); |
|
|
TD[(*fi).V(j)].PntSum += dir * a[j]; |
|
|
TD[(*fi).V(j)].LenSum += a[j]; // well, it should be named angleSum |
|
|
} |
|
|
} |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].LenSum > 0) |
|
|
(*vi).P() = (*vi).P() + (TD[*vi].PntSum / TD[*vi].LenSum) * delta; |
|
|
} |
|
|
}; |
|
|
|
|
|
// Scale dependent laplacian smoothing [Fujiwara 95] |
|
|
// as described in |
|
|
// Implicit Fairing of Irregular Meshes using Diffusion and Curvature Flow |
|
|
// Mathieu Desbrun, Mark Meyer, Peter Schroeder, Alan H. Barr |
|
|
// SIGGRAPH 99 |
|
|
// REQUIREMENTS: Border Flags. |
|
|
// |
|
|
// Note the delta parameter is in a absolute unit |
|
|
// to get stability it should be a small percentage of the shortest edge. |
|
|
|
|
|
static void VertexCoordScaleDependentLaplacian_Fujiwara(MeshType &m, int step, ScalarType delta, bool SmoothSelected = false) |
|
|
{ |
|
|
SimpleTempData<typename MeshType::VertContainer, ScaleLaplacianInfo> TD(m.vert); |
|
|
ScaleLaplacianInfo lpz; |
|
|
lpz.PntSum = CoordType(0, 0, 0); |
|
|
lpz.LenSum = 0; |
|
|
FaceIterator fi; |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
TD[*vi] = lpz; |
|
|
|
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if (!(*fi).IsB(j)) |
|
|
{ |
|
|
CoordType edge = (*fi).V1(j)->P() - (*fi).V(j)->P(); |
|
|
ScalarType len = Norm(edge); |
|
|
edge /= len; |
|
|
TD[(*fi).V(j)].PntSum += edge; |
|
|
TD[(*fi).V1(j)].PntSum -= edge; |
|
|
TD[(*fi).V(j)].LenSum += len; |
|
|
TD[(*fi).V1(j)].LenSum += len; |
|
|
} |
|
|
|
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
// se l'edge j e' di bordo si riazzera tutto e si riparte |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].PntSum = CoordType(0, 0, 0); |
|
|
TD[(*fi).V1(j)].PntSum = CoordType(0, 0, 0); |
|
|
TD[(*fi).V(j)].LenSum = 0; |
|
|
TD[(*fi).V1(j)].LenSum = 0; |
|
|
} |
|
|
|
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
CoordType edge = (*fi).V1(j)->P() - (*fi).V(j)->P(); |
|
|
ScalarType len = Norm(edge); |
|
|
edge /= len; |
|
|
TD[(*fi).V(j)].PntSum += edge; |
|
|
TD[(*fi).V1(j)].PntSum -= edge; |
|
|
TD[(*fi).V(j)].LenSum += len; |
|
|
TD[(*fi).V1(j)].LenSum += len; |
|
|
} |
|
|
// The fundamental part: |
|
|
// We move the new point of a quantity |
|
|
// |
|
|
// L(M) = 1/Sum(edgelen) * Sum(Normalized edges) |
|
|
// |
|
|
|
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].LenSum > 0) |
|
|
{ |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
(*vi).P() = (*vi).P() + (TD[*vi].PntSum / TD[*vi].LenSum) * delta; |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
class LaplacianInfo |
|
|
{ |
|
|
public: |
|
|
LaplacianInfo(const CoordType &_p, const int _n) : sum(_p), cnt(_n) {} |
|
|
LaplacianInfo() {} |
|
|
CoordType sum; |
|
|
ScalarType cnt; |
|
|
}; |
|
|
|
|
|
// Classical Laplacian Smoothing. Each vertex can be moved onto the average of the adjacent vertices. |
|
|
// Can smooth only the selected vertices and weight the smoothing according to the quality |
|
|
// In the latter case 0 means that the vertex is not moved and 1 means that the vertex is moved onto the computed position. |
|
|
// |
|
|
// From the Taubin definition "A signal proc approach to fair surface design" |
|
|
// We define the discrete Laplacian of a discrete surface signal by weighted averages over the neighborhoods |
|
|
// \delta xi = \Sum wij (xj - xi) ; |
|
|
// where xj are the adjacent vertices of xi and wij is usually 1/n_adj |
|
|
// |
|
|
// This function simply accumulate over a TempData all the positions of the ajacent vertices |
|
|
// |
|
|
static void AccumulateLaplacianInfo(MeshType &m, SimpleTempData<typename MeshType::VertContainer, LaplacianInfo> &TD, bool cotangentFlag = false) |
|
|
{ |
|
|
float weight = 1.0f; |
|
|
|
|
|
//if we are applying to a tetrahedral mesh: |
|
|
ForEachTetra(m, [&](TetraType &t) { |
|
|
for (int i = 0; i < 6; ++i) |
|
|
{ |
|
|
VertexPointer v0, v1, vo0, vo1; |
|
|
v0 = t.V(Tetra::VofE(i, 0)); |
|
|
v1 = t.V(Tetra::VofE(i, 1)); |
|
|
|
|
|
if (cotangentFlag) |
|
|
{ |
|
|
vo0 = t.V(Tetra::VofE(5 - i, 0)); |
|
|
vo1 = t.V(Tetra::VofE(5 - i, 1)); |
|
|
|
|
|
ScalarType angle = Tetra::DihedralAngle(t, 5 - i); |
|
|
ScalarType length = vcg::Distance(vo0->P(), vo1->P()); |
|
|
|
|
|
weight = (length / 6.) * (tan(M_PI / 2. - angle)); |
|
|
} |
|
|
|
|
|
TD[v0].sum += v1->cP() * weight; |
|
|
TD[v1].sum += v0->cP() * weight; |
|
|
TD[v0].cnt += weight; |
|
|
TD[v1].cnt += weight; |
|
|
} |
|
|
}); |
|
|
|
|
|
ForEachTetra(m, [&](TetraType &t) { |
|
|
for (int i = 0; i < 4; ++i) |
|
|
if (t.IsB(i)) |
|
|
{ |
|
|
VertexPointer v0, v1, v2; |
|
|
v0 = t.V(Tetra::VofF(i, 0)); |
|
|
v1 = t.V(Tetra::VofF(i, 1)); |
|
|
v2 = t.V(Tetra::VofF(i, 2)); |
|
|
|
|
|
TD[v0].sum = v0->P(); |
|
|
TD[v1].sum = v1->P(); |
|
|
TD[v2].sum = v2->P(); |
|
|
|
|
|
TD[v0].cnt = 1; |
|
|
TD[v1].cnt = 1; |
|
|
TD[v2].cnt = 1; |
|
|
} |
|
|
}); |
|
|
|
|
|
// ForEachTetra(m, [&](TetraType &t) { |
|
|
// for (int i = 0; i < 4; ++i) |
|
|
// if (t.IsB(i)) |
|
|
// { |
|
|
// VertexPointer v0, v1, v2; |
|
|
// v0 = t.V(Tetra::VofF(i, 0)); |
|
|
// v1 = t.V(Tetra::VofF(i, 1)); |
|
|
// v2 = t.V(Tetra::VofF(i, 2)); |
|
|
|
|
|
// TD[v0].sum += v1->P(); |
|
|
// TD[v0].sum += v2->P(); |
|
|
// TD[v0].cnt += 2; |
|
|
|
|
|
// TD[v1].sum += v0->P(); |
|
|
// TD[v1].sum += v2->P(); |
|
|
// TD[v1].cnt += 2; |
|
|
|
|
|
// TD[v2].sum += v0->P(); |
|
|
// TD[v2].sum += v1->P(); |
|
|
// TD[v2].cnt += 2; |
|
|
// } |
|
|
// }); |
|
|
|
|
|
FaceIterator fi; |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
{ |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if (!(*fi).IsB(j)) |
|
|
{ |
|
|
if (cotangentFlag) |
|
|
{ |
|
|
float angle = Angle(fi->P1(j) - fi->P2(j), fi->P0(j) - fi->P2(j)); |
|
|
weight = tan((M_PI * 0.5) - angle); |
|
|
} |
|
|
|
|
|
TD[(*fi).V0(j)].sum += (*fi).P1(j) * weight; |
|
|
TD[(*fi).V1(j)].sum += (*fi).P0(j) * weight; |
|
|
TD[(*fi).V0(j)].cnt += weight; |
|
|
TD[(*fi).V1(j)].cnt += weight; |
|
|
} |
|
|
} |
|
|
// si azzaera i dati per i vertici di bordo |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
{ |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V0(j)].sum = (*fi).P0(j); |
|
|
TD[(*fi).V1(j)].sum = (*fi).P1(j); |
|
|
TD[(*fi).V0(j)].cnt = 1; |
|
|
TD[(*fi).V1(j)].cnt = 1; |
|
|
} |
|
|
} |
|
|
|
|
|
// se l'edge j e' di bordo si deve mediare solo con gli adiacenti |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
{ |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->P(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->P(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
static void VertexCoordLaplacian(MeshType &m, int step, bool SmoothSelected = false, bool cotangentWeight = false, vcg::CallBackPos *cb = 0) |
|
|
{ |
|
|
LaplacianInfo lpz(CoordType(0, 0, 0), 0); |
|
|
SimpleTempData<typename MeshType::VertContainer, LaplacianInfo> TD(m.vert, lpz); |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
if (cb) |
|
|
cb(100 * i / step, "Classic Laplacian Smoothing"); |
|
|
TD.Init(lpz); |
|
|
AccumulateLaplacianInfo(m, TD, cotangentWeight); |
|
|
for (auto vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
{ |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
(*vi).P() = ((*vi).P() + TD[*vi].sum) / (TD[*vi].cnt + 1); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
// Same of above but moves only the vertices that do not change FaceOrientation more that the given threshold |
|
|
static void VertexCoordPlanarLaplacian(MeshType &m, int step, float AngleThrRad = math::ToRad(1.0), bool SmoothSelected = false, vcg::CallBackPos *cb = 0) |
|
|
{ |
|
|
LaplacianInfo lpz(CoordType(0, 0, 0), 0); |
|
|
SimpleTempData<typename MeshType::VertContainer, LaplacianInfo> TD(m.vert, lpz); |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
if (cb) |
|
|
cb(100 * i / step, "Planar Laplacian Smoothing"); |
|
|
TD.Init(lpz); |
|
|
AccumulateLaplacianInfo(m, TD); |
|
|
// First normalize the AccumulateLaplacianInfo |
|
|
for (auto vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
{ |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
TD[*vi].sum = ((*vi).P() + TD[*vi].sum) / (TD[*vi].cnt + 1); |
|
|
} |
|
|
|
|
|
for (auto fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
{ |
|
|
if (!(*fi).IsD()) |
|
|
{ |
|
|
for (int j = 0; j < 3; ++j) |
|
|
{ |
|
|
if (Angle(Normal(TD[(*fi).V0(j)].sum, (*fi).P1(j), (*fi).P2(j)), |
|
|
Normal((*fi).P0(j), (*fi).P1(j), (*fi).P2(j))) > AngleThrRad) |
|
|
TD[(*fi).V0(j)].sum = (*fi).P0(j); |
|
|
} |
|
|
} |
|
|
} |
|
|
for (auto fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
{ |
|
|
if (!(*fi).IsD()) |
|
|
{ |
|
|
for (int j = 0; j < 3; ++j) |
|
|
{ |
|
|
if (Angle(Normal(TD[(*fi).V0(j)].sum, TD[(*fi).V1(j)].sum, (*fi).P2(j)), |
|
|
Normal((*fi).P0(j), (*fi).P1(j), (*fi).P2(j))) > AngleThrRad) |
|
|
{ |
|
|
TD[(*fi).V0(j)].sum = (*fi).P0(j); |
|
|
TD[(*fi).V1(j)].sum = (*fi).P1(j); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
for (auto vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
(*vi).P() = TD[*vi].sum; |
|
|
} // end step |
|
|
} |
|
|
|
|
|
static void VertexCoordLaplacianBlend(MeshType &m, int step, float alpha, bool SmoothSelected = false) |
|
|
{ |
|
|
VertexIterator vi; |
|
|
LaplacianInfo lpz(CoordType(0, 0, 0), 0); |
|
|
assert(alpha <= 1.0); |
|
|
SimpleTempData<typename MeshType::VertContainer, LaplacianInfo> TD(m.vert); |
|
|
|
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
TD.Init(lpz); |
|
|
AccumulateLaplacianInfo(m, TD); |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
{ |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
{ |
|
|
CoordType Delta = TD[*vi].sum / TD[*vi].cnt - (*vi).P(); |
|
|
(*vi).P() = (*vi).P() + Delta * alpha; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
/* a couple of notes about the lambda mu values |
|
|
We assume that 0 < lambda , and mu is a negative scale factor such that mu < - lambda. |
|
|
Holds mu+lambda < 0 (e.g in absolute value mu is greater) |
|
|
|
|
|
let kpb be the pass-band frequency, taubin says that: |
|
|
kpb = 1/lambda + 1/mu >0 |
|
|
|
|
|
Values of kpb from 0.01 to 0.1 produce good results according to the original paper. |
|
|
|
|
|
kpb * mu - mu/lambda = 1 |
|
|
mu = 1/(kpb-1/lambda ) |
|
|
|
|
|
So if |
|
|
* lambda == 0.5, kpb==0.1 -> mu = 1/(0.1 - 2) = -0.526 |
|
|
* lambda == 0.5, kpb==0.01 -> mu = 1/(0.01 - 2) = -0.502 |
|
|
*/ |
|
|
|
|
|
static void VertexCoordTaubin(MeshType &m, int step, float lambda, float mu, bool SmoothSelected = false, vcg::CallBackPos *cb = 0) |
|
|
{ |
|
|
LaplacianInfo lpz(CoordType(0, 0, 0), 0); |
|
|
SimpleTempData<typename MeshType::VertContainer, LaplacianInfo> TD(m.vert, lpz); |
|
|
VertexIterator vi; |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
if (cb) |
|
|
cb(100 * i / step, "Taubin Smoothing"); |
|
|
TD.Init(lpz); |
|
|
AccumulateLaplacianInfo(m, TD); |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
{ |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
{ |
|
|
CoordType Delta = TD[*vi].sum / TD[*vi].cnt - (*vi).P(); |
|
|
(*vi).P() = (*vi).P() + Delta * lambda; |
|
|
} |
|
|
} |
|
|
TD.Init(lpz); |
|
|
AccumulateLaplacianInfo(m, TD); |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
{ |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
{ |
|
|
CoordType Delta = TD[*vi].sum / TD[*vi].cnt - (*vi).P(); |
|
|
(*vi).P() = (*vi).P() + Delta * mu; |
|
|
} |
|
|
} |
|
|
} // end for step |
|
|
} |
|
|
|
|
|
static void VertexQualityTaubin(MeshType &m, int step, float lambda, float mu, bool SmoothSelected = false, vcg::CallBackPos *cb = 0) |
|
|
{ |
|
|
SimpleTempData<typename MeshType::VertContainer, ScalarType> OldQ(m.vert, 0); |
|
|
|
|
|
VertexIterator vi; |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
for (size_t i = 0; i < m.vert.size(); i++) |
|
|
OldQ[i] = m.vert[i].Q(); |
|
|
|
|
|
if (cb) |
|
|
cb(100 * i / step, "Taubin Smoothing"); |
|
|
|
|
|
VertexQualityLaplacian(m, 1, SmoothSelected); |
|
|
|
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
{ |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
{ |
|
|
ScalarType Delta = (*vi).Q() - OldQ[(*vi)]; |
|
|
(*vi).Q() = OldQ[(*vi)] + Delta * lambda; |
|
|
} |
|
|
} |
|
|
|
|
|
for (size_t i = 0; i < m.vert.size(); i++) |
|
|
OldQ[i] = m.vert[i].Q(); |
|
|
VertexQualityLaplacian(m, 1, SmoothSelected); |
|
|
|
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
{ |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
{ |
|
|
ScalarType Delta = m.vert[i].Q() - OldQ[(*vi)]; |
|
|
(*vi).Q() = OldQ[(*vi)] + Delta * mu; |
|
|
} |
|
|
} |
|
|
} // end for step |
|
|
} |
|
|
|
|
|
static void VertexCoordLaplacianQuality(MeshType &m, int step, bool SmoothSelected = false) |
|
|
{ |
|
|
LaplacianInfo lpz; |
|
|
lpz.sum = CoordType(0, 0, 0); |
|
|
lpz.cnt = 1; |
|
|
SimpleTempData<typename MeshType::VertContainer, LaplacianInfo> TD(m.vert, lpz); |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
for (VertexIterator vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
{ |
|
|
float q = (*vi).Q(); |
|
|
(*vi).P() = (*vi).P() * q + (TD[*vi].sum / TD[*vi].cnt) * (1.0 - q); |
|
|
} |
|
|
} // end for |
|
|
}; |
|
|
|
|
|
/* |
|
|
Improved Laplacian Smoothing of Noisy Surface Meshes |
|
|
J. Vollmer, R. Mencl, and H. M<EFBFBD>ller |
|
|
EUROGRAPHICS Volume 18 (1999), Number 3 |
|
|
*/ |
|
|
|
|
|
class HCSmoothInfo |
|
|
{ |
|
|
public: |
|
|
CoordType dif; |
|
|
CoordType sum; |
|
|
int cnt; |
|
|
}; |
|
|
|
|
|
static void VertexCoordLaplacianHC(MeshType &m, int step, bool SmoothSelected = false) |
|
|
{ |
|
|
ScalarType beta = 0.5; |
|
|
HCSmoothInfo lpz; |
|
|
lpz.sum = CoordType(0, 0, 0); |
|
|
lpz.dif = CoordType(0, 0, 0); |
|
|
lpz.cnt = 0; |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
SimpleTempData<typename MeshType::VertContainer, HCSmoothInfo> TD(m.vert, lpz); |
|
|
// First Loop compute the laplacian |
|
|
FaceIterator fi; |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
{ |
|
|
for (int j = 0; j < 3; ++j) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->P(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->P(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
// se l'edge j e' di bordo si deve sommare due volte |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->P(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->P(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
} |
|
|
} |
|
|
VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD()) |
|
|
TD[*vi].sum /= (float)TD[*vi].cnt; |
|
|
|
|
|
// Second Loop compute average difference |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
{ |
|
|
for (int j = 0; j < 3; ++j) |
|
|
{ |
|
|
TD[(*fi).V(j)].dif += TD[(*fi).V1(j)].sum - (*fi).V1(j)->P(); |
|
|
TD[(*fi).V1(j)].dif += TD[(*fi).V(j)].sum - (*fi).V(j)->P(); |
|
|
// se l'edge j e' di bordo si deve sommare due volte |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].dif += TD[(*fi).V1(j)].sum - (*fi).V1(j)->P(); |
|
|
TD[(*fi).V1(j)].dif += TD[(*fi).V(j)].sum - (*fi).V(j)->P(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
{ |
|
|
if (TD[*vi].cnt > 0){ |
|
|
TD[*vi].dif /= (float)TD[*vi].cnt; |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
(*vi).P() = TD[*vi].sum - (TD[*vi].sum - (*vi).P()) * beta + (TD[*vi].dif) * (1.f - beta); |
|
|
} |
|
|
} |
|
|
} // end for step |
|
|
}; |
|
|
|
|
|
// Laplacian smooth of the quality. |
|
|
|
|
|
class ColorSmoothInfo |
|
|
{ |
|
|
public: |
|
|
unsigned int r; |
|
|
unsigned int g; |
|
|
unsigned int b; |
|
|
unsigned int a; |
|
|
int cnt; |
|
|
}; |
|
|
|
|
|
static void VertexColorLaplacian(MeshType &m, int step, bool SmoothSelected = false, vcg::CallBackPos *cb = 0) |
|
|
{ |
|
|
ColorSmoothInfo csi; |
|
|
csi.r = 0; |
|
|
csi.g = 0; |
|
|
csi.b = 0; |
|
|
csi.cnt = 0; |
|
|
SimpleTempData<typename MeshType::VertContainer, ColorSmoothInfo> TD(m.vert, csi); |
|
|
|
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
if (cb) |
|
|
cb(100 * i / step, "Vertex Color Laplacian Smoothing"); |
|
|
VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
TD[*vi] = csi; |
|
|
|
|
|
FaceIterator fi; |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if (!(*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].r += (*fi).V1(j)->C()[0]; |
|
|
TD[(*fi).V(j)].g += (*fi).V1(j)->C()[1]; |
|
|
TD[(*fi).V(j)].b += (*fi).V1(j)->C()[2]; |
|
|
TD[(*fi).V(j)].a += (*fi).V1(j)->C()[3]; |
|
|
|
|
|
TD[(*fi).V1(j)].r += (*fi).V(j)->C()[0]; |
|
|
TD[(*fi).V1(j)].g += (*fi).V(j)->C()[1]; |
|
|
TD[(*fi).V1(j)].b += (*fi).V(j)->C()[2]; |
|
|
TD[(*fi).V1(j)].a += (*fi).V(j)->C()[3]; |
|
|
|
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
|
|
|
// si azzaera i dati per i vertici di bordo |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)] = csi; |
|
|
TD[(*fi).V1(j)] = csi; |
|
|
} |
|
|
|
|
|
// se l'edge j e' di bordo si deve mediare solo con gli adiacenti |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].r += (*fi).V1(j)->C()[0]; |
|
|
TD[(*fi).V(j)].g += (*fi).V1(j)->C()[1]; |
|
|
TD[(*fi).V(j)].b += (*fi).V1(j)->C()[2]; |
|
|
TD[(*fi).V(j)].a += (*fi).V1(j)->C()[3]; |
|
|
|
|
|
TD[(*fi).V1(j)].r += (*fi).V(j)->C()[0]; |
|
|
TD[(*fi).V1(j)].g += (*fi).V(j)->C()[1]; |
|
|
TD[(*fi).V1(j)].b += (*fi).V(j)->C()[2]; |
|
|
TD[(*fi).V1(j)].a += (*fi).V(j)->C()[3]; |
|
|
|
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
|
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
{ |
|
|
(*vi).C()[0] = (unsigned int)ceil((double)(TD[*vi].r / TD[*vi].cnt)); |
|
|
(*vi).C()[1] = (unsigned int)ceil((double)(TD[*vi].g / TD[*vi].cnt)); |
|
|
(*vi).C()[2] = (unsigned int)ceil((double)(TD[*vi].b / TD[*vi].cnt)); |
|
|
(*vi).C()[3] = (unsigned int)ceil((double)(TD[*vi].a / TD[*vi].cnt)); |
|
|
} |
|
|
} // end for step |
|
|
}; |
|
|
|
|
|
static void FaceColorLaplacian(MeshType &m, int step, bool SmoothSelected = false, vcg::CallBackPos *cb = 0) |
|
|
{ |
|
|
ColorSmoothInfo csi; |
|
|
csi.r = 0; |
|
|
csi.g = 0; |
|
|
csi.b = 0; |
|
|
csi.cnt = 0; |
|
|
SimpleTempData<typename MeshType::FaceContainer, ColorSmoothInfo> TD(m.face, csi); |
|
|
|
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
if (cb) |
|
|
cb(100 * i / step, "Face Color Laplacian Smoothing"); |
|
|
FaceIterator fi; |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
TD[*fi] = csi; |
|
|
|
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
{ |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if (!(*fi).IsB(j)) |
|
|
{ |
|
|
TD[*fi].r += (*fi).FFp(j)->C()[0]; |
|
|
TD[*fi].g += (*fi).FFp(j)->C()[1]; |
|
|
TD[*fi].b += (*fi).FFp(j)->C()[2]; |
|
|
TD[*fi].a += (*fi).FFp(j)->C()[3]; |
|
|
++TD[*fi].cnt; |
|
|
} |
|
|
} |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD() && TD[*fi].cnt > 0) |
|
|
if (!SmoothSelected || (*fi).IsS()) |
|
|
{ |
|
|
(*fi).C()[0] = (unsigned int)ceil((float)(TD[*fi].r / TD[*fi].cnt)); |
|
|
(*fi).C()[1] = (unsigned int)ceil((float)(TD[*fi].g / TD[*fi].cnt)); |
|
|
(*fi).C()[2] = (unsigned int)ceil((float)(TD[*fi].b / TD[*fi].cnt)); |
|
|
(*fi).C()[3] = (unsigned int)ceil((float)(TD[*fi].a / TD[*fi].cnt)); |
|
|
} |
|
|
} // end for step |
|
|
}; |
|
|
|
|
|
// Laplacian smooth of the quality. |
|
|
|
|
|
class QualitySmoothInfo |
|
|
{ |
|
|
public: |
|
|
ScalarType sum; |
|
|
int cnt; |
|
|
}; |
|
|
|
|
|
static void PointCloudQualityAverage(MeshType &m, int neighbourSize = 8, int iter = 1) |
|
|
{ |
|
|
tri::RequireCompactness(m); |
|
|
VertexConstDataWrapper<MeshType> ww(m); |
|
|
KdTree<ScalarType> kt(ww); |
|
|
typename KdTree<ScalarType>::PriorityQueue pq; |
|
|
for (int k = 0; k < iter; ++k) |
|
|
{ |
|
|
std::vector<ScalarType> newQVec(m.vn); |
|
|
for (int i = 0; i < m.vn; ++i) |
|
|
{ |
|
|
kt.doQueryK(m.vert[i].P(), neighbourSize, pq); |
|
|
float qAvg = 0; |
|
|
for (int j = 0; j < pq.getNofElements(); ++j) |
|
|
qAvg += m.vert[pq.getIndex(j)].Q(); |
|
|
newQVec[i] = qAvg / float(pq.getNofElements()); |
|
|
} |
|
|
|
|
|
for (int i = 0; i < m.vn; ++i) |
|
|
m.vert[i].Q() = newQVec[i]; |
|
|
} |
|
|
} |
|
|
|
|
|
static void PointCloudQualityMedian(MeshType &m, int medianSize = 8) |
|
|
{ |
|
|
tri::RequireCompactness(m); |
|
|
VertexConstDataWrapper<MeshType> ww(m); |
|
|
KdTree<ScalarType> kt(ww); |
|
|
typename KdTree<ScalarType>::PriorityQueue pq; |
|
|
std::vector<ScalarType> newQVec(m.vn); |
|
|
for (int i = 0; i < m.vn; ++i) |
|
|
{ |
|
|
kt.doQueryK(m.vert[i].P(), medianSize, pq); |
|
|
std::vector<ScalarType> qVec(pq.getNofElements()); |
|
|
for (int j = 0; j < pq.getNofElements(); ++j) |
|
|
qVec[j] = m.vert[pq.getIndex(j)].Q(); |
|
|
std::sort(qVec.begin(), qVec.end()); |
|
|
newQVec[i] = qVec[qVec.size() / 2]; |
|
|
} |
|
|
|
|
|
for (int i = 0; i < m.vn; ++i) |
|
|
m.vert[i].Q() = newQVec[i]; |
|
|
} |
|
|
|
|
|
static void VertexQualityLaplacian(MeshType &m, int step = 1, bool SmoothSelected = false) |
|
|
{ |
|
|
QualitySmoothInfo lpz; |
|
|
lpz.sum = 0; |
|
|
lpz.cnt = 0; |
|
|
SimpleTempData<typename MeshType::VertContainer, QualitySmoothInfo> TD(m.vert, lpz); |
|
|
//TD.Start(lpz); |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
TD[*vi] = lpz; |
|
|
|
|
|
FaceIterator fi; |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < (*fi).VN(); ++j) |
|
|
if (!(*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->Q(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->Q(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
|
|
|
// si azzaera i dati per i vertici di bordo |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < (*fi).VN(); ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)] = lpz; |
|
|
TD[(*fi).V1(j)] = lpz; |
|
|
} |
|
|
|
|
|
// se l'edge j e' di bordo si deve mediare solo con gli adiacenti |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < (*fi).VN(); ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->Q(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->Q(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
|
|
|
//VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
(*vi).Q() = TD[*vi].sum / TD[*vi].cnt; |
|
|
} |
|
|
|
|
|
//TD.Stop(); |
|
|
}; |
|
|
|
|
|
static void VertexNormalLaplacian(MeshType &m, int step, bool SmoothSelected = false) |
|
|
{ |
|
|
LaplacianInfo lpz; |
|
|
lpz.sum = CoordType(0, 0, 0); |
|
|
lpz.cnt = 0; |
|
|
SimpleTempData<typename MeshType::VertContainer, LaplacianInfo> TD(m.vert, lpz); |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
TD[*vi] = lpz; |
|
|
|
|
|
FaceIterator fi; |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if (!(*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->N(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->N(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
|
|
|
// si azzaera i dati per i vertici di bordo |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)] = lpz; |
|
|
TD[(*fi).V1(j)] = lpz; |
|
|
} |
|
|
|
|
|
// se l'edge j e' di bordo si deve mediare solo con gli adiacenti |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->N(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->N(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
|
|
|
//VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
(*vi).N() = TD[*vi].sum / TD[*vi].cnt; |
|
|
} |
|
|
}; |
|
|
|
|
|
// Smooth solo lungo la direzione di vista |
|
|
// alpha e' compreso fra 0(no smoot) e 1 (tutto smoot) |
|
|
// Nota che se smootare il bordo puo far fare bandierine. |
|
|
static void VertexCoordViewDepth(MeshType &m, |
|
|
const CoordType &viewpoint, |
|
|
const ScalarType alpha, |
|
|
int step, bool SmoothSelected, bool SmoothBorder = false) |
|
|
{ |
|
|
LaplacianInfo lpz; |
|
|
lpz.sum = CoordType(0, 0, 0); |
|
|
lpz.cnt = 0; |
|
|
SimpleTempData<typename MeshType::VertContainer, LaplacianInfo> TD(m.vert, lpz); |
|
|
for (int i = 0; i < step; ++i) |
|
|
{ |
|
|
VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
TD[*vi] = lpz; |
|
|
|
|
|
FaceIterator fi; |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if (!(*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->cP(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->cP(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
|
|
|
// si azzaera i dati per i vertici di bordo |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)] = lpz; |
|
|
TD[(*fi).V1(j)] = lpz; |
|
|
} |
|
|
|
|
|
// se l'edge j e' di bordo si deve mediare solo con gli adiacenti |
|
|
if (SmoothBorder) |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
for (int j = 0; j < 3; ++j) |
|
|
if ((*fi).IsB(j)) |
|
|
{ |
|
|
TD[(*fi).V(j)].sum += (*fi).V1(j)->cP(); |
|
|
TD[(*fi).V1(j)].sum += (*fi).V(j)->cP(); |
|
|
++TD[(*fi).V(j)].cnt; |
|
|
++TD[(*fi).V1(j)].cnt; |
|
|
} |
|
|
|
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if (!(*vi).IsD() && TD[*vi].cnt > 0) |
|
|
if (!SmoothSelected || (*vi).IsS()) |
|
|
{ |
|
|
CoordType np = TD[*vi].sum / TD[*vi].cnt; |
|
|
CoordType d = (*vi).cP() - viewpoint; |
|
|
d.Normalize(); |
|
|
ScalarType s = d.dot(np - (*vi).cP()); |
|
|
(*vi).P() += d * (s * alpha); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
/****************************************************************************************************************/ |
|
|
/****************************************************************************************************************/ |
|
|
// Paso Double Smoothing |
|
|
// The proposed |
|
|
// approach is a two step method where in the first step the face normals |
|
|
// are adjusted and then, in a second phase, the vertex positions are updated. |
|
|
// Ref: |
|
|
// A. Belyaev and Y. Ohtake, A Comparison of Mesh Smoothing Methods, Proc. Israel-Korea Bi-Nat"l Conf. Geometric Modeling and Computer Graphics, pp. 83-87, 2003. |
|
|
|
|
|
/****************************************************************************************************************/ |
|
|
/****************************************************************************************************************/ |
|
|
// Classi di info |
|
|
|
|
|
class PDVertInfo |
|
|
{ |
|
|
public: |
|
|
CoordType np; |
|
|
}; |
|
|
|
|
|
class PDFaceInfo |
|
|
{ |
|
|
public: |
|
|
PDFaceInfo() {} |
|
|
PDFaceInfo(const CoordType &_m) : m(_m) {} |
|
|
CoordType m; |
|
|
}; |
|
|
/***************************************************************************/ |
|
|
// Paso Doble Step 1 compute the smoothed normals |
|
|
/***************************************************************************/ |
|
|
// Requirements: |
|
|
// VF Topology |
|
|
// Normalized Face Normals |
|
|
// |
|
|
// This is the Normal Smoothing approach of Shen and Berner |
|
|
// Fuzzy Vector Median-Based Surface Smoothing TVCG 2004 |
|
|
|
|
|
void FaceNormalFuzzyVectorSB(MeshType &m, |
|
|
SimpleTempData<typename MeshType::FaceContainer, PDFaceInfo> &TD, |
|
|
ScalarType sigma) |
|
|
{ |
|
|
int i; |
|
|
|
|
|
FaceIterator fi; |
|
|
|
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
{ |
|
|
CoordType bc = (*fi).Barycenter(); |
|
|
// 1) Clear all the visited flag of faces that are vertex-adjacent to fi |
|
|
for (i = 0; i < 3; ++i) |
|
|
{ |
|
|
vcg::face::VFIterator<FaceType> ep(&*fi, i); |
|
|
while (!ep.End()) |
|
|
{ |
|
|
ep.f->ClearV(); |
|
|
++ep; |
|
|
} |
|
|
} |
|
|
|
|
|
// 1) Effectively average the normals weighting them with |
|
|
(*fi).SetV(); |
|
|
CoordType mm = CoordType(0, 0, 0); |
|
|
for (i = 0; i < 3; ++i) |
|
|
{ |
|
|
vcg::face::VFIterator<FaceType> ep(&*fi, i); |
|
|
while (!ep.End()) |
|
|
{ |
|
|
if (!(*ep.f).IsV()) |
|
|
{ |
|
|
if (sigma > 0) |
|
|
{ |
|
|
ScalarType dd = SquaredDistance(ep.f->Barycenter(), bc); |
|
|
ScalarType ang = AngleN(ep.f->N(), (*fi).N()); |
|
|
mm += ep.f->N() * exp((-sigma) * ang * ang / dd); |
|
|
} |
|
|
else |
|
|
mm += ep.f->N(); |
|
|
(*ep.f).SetV(); |
|
|
} |
|
|
++ep; |
|
|
} |
|
|
} |
|
|
mm.Normalize(); |
|
|
TD[*fi].m = mm; |
|
|
} |
|
|
} |
|
|
|
|
|
// Replace the normal of the face with the average of normals of the vertex adijacent faces. |
|
|
// Normals are weighted with face area. |
|
|
// It assumes that: |
|
|
// VF adjacency is present. |
|
|
|
|
|
static void FaceNormalLaplacianVF(MeshType &m) |
|
|
{ |
|
|
tri::RequireVFAdjacency(m); |
|
|
PDFaceInfo lpzf(CoordType(0, 0, 0)); |
|
|
SimpleTempData<typename MeshType::FaceContainer, PDFaceInfo> TDF(m.face, lpzf); |
|
|
|
|
|
tri::UpdateNormal<MeshType>::NormalizePerFaceByArea(m); |
|
|
|
|
|
for (FaceIterator fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
{ |
|
|
// 1) Clear all the visited flag of faces that are vertex-adjacent to fi |
|
|
for (int i = 0; i < 3; ++i) |
|
|
{ |
|
|
VFLocalIterator ep(&*fi, i); |
|
|
for (; !ep.End(); ++ep) |
|
|
ep.f->ClearV(); |
|
|
} |
|
|
|
|
|
// 2) Effectively average the normals |
|
|
CoordType normalSum = (*fi).N(); |
|
|
|
|
|
for (int i = 0; i < 3; ++i) |
|
|
{ |
|
|
VFLocalIterator ep(&*fi, i); |
|
|
for (; !ep.End(); ++ep) |
|
|
{ |
|
|
if (!(*ep.f).IsV()) |
|
|
{ |
|
|
normalSum += ep.f->N(); |
|
|
(*ep.f).SetV(); |
|
|
} |
|
|
} |
|
|
} |
|
|
normalSum.Normalize(); |
|
|
TDF[*fi].m = normalSum; |
|
|
} |
|
|
for (FaceIterator fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
(*fi).N() = TDF[*fi].m; |
|
|
|
|
|
tri::UpdateNormal<MeshType>::NormalizePerFace(m); |
|
|
} |
|
|
|
|
|
// Replace the normal of the face with the average of normals of the face adijacent faces. |
|
|
// Normals are weighted with face area. |
|
|
// It assumes that: |
|
|
// Normals are normalized: |
|
|
// FF adjacency is present. |
|
|
|
|
|
static void FaceNormalLaplacianFF(MeshType &m, int step = 1, bool SmoothSelected = false) |
|
|
{ |
|
|
PDFaceInfo lpzf(CoordType(0, 0, 0)); |
|
|
SimpleTempData<typename MeshType::FaceContainer, PDFaceInfo> TDF(m.face, lpzf); |
|
|
tri::RequireFFAdjacency(m); |
|
|
|
|
|
FaceIterator fi; |
|
|
tri::UpdateNormal<MeshType>::NormalizePerFaceByArea(m); |
|
|
for (int iStep = 0; iStep < step; ++iStep) |
|
|
{ |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!(*fi).IsD()) |
|
|
{ |
|
|
CoordType normalSum = (*fi).N(); |
|
|
|
|
|
for (int i = 0; i < 3; ++i) |
|
|
normalSum += (*fi).FFp(i)->N(); |
|
|
|
|
|
TDF[*fi].m = normalSum; |
|
|
} |
|
|
for (fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
if (!SmoothSelected || (*fi).IsS()) |
|
|
(*fi).N() = TDF[*fi].m; |
|
|
|
|
|
tri::UpdateNormal<MeshType>::NormalizePerFace(m); |
|
|
} |
|
|
} |
|
|
|
|
|
/***************************************************************************/ |
|
|
// Paso Doble Step 1 compute the smoothed normals |
|
|
/***************************************************************************/ |
|
|
// Requirements: |
|
|
// VF Topology |
|
|
// Normalized Face Normals |
|
|
// |
|
|
// This is the Normal Smoothing approach based on a angle thresholded weighting |
|
|
// sigma is in the 0 .. 1 range, it represent the cosine of a threshold angle. |
|
|
// sigma == 0 All the normals are averaged |
|
|
// sigma == 1 Nothing is averaged. |
|
|
// Only within the specified range are averaged toghether. The averagin is weighted with the |
|
|
|
|
|
static void FaceNormalAngleThreshold(MeshType &m, |
|
|
SimpleTempData<typename MeshType::FaceContainer, PDFaceInfo> &TD, |
|
|
ScalarType sigma) |
|
|
{ |
|
|
for (FaceIterator fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
{ |
|
|
// 1) Clear all the visited flag of faces that are vertex-adjacent to fi |
|
|
for (int i = 0; i < 3; ++i) |
|
|
{ |
|
|
VFLocalIterator ep(&*fi, i); |
|
|
for (; !ep.End(); ++ep) |
|
|
ep.f->ClearV(); |
|
|
} |
|
|
|
|
|
// 1) Effectively average the normals weighting them with the squared difference of the angle similarity |
|
|
// sigma is the cosine of a threshold angle. sigma \in 0..1 |
|
|
// sigma == 0 All the normals are averaged |
|
|
// sigma == 1 Nothing is averaged. |
|
|
// The averaging is weighted with the difference between normals. more similar the normal more important they are. |
|
|
|
|
|
CoordType normalSum = CoordType(0, 0, 0); |
|
|
for (int i = 0; i < 3; ++i) |
|
|
{ |
|
|
VFLocalIterator ep(&*fi, i); |
|
|
for (; !ep.End(); ++ep) |
|
|
{ |
|
|
if (!(*ep.f).IsV()) |
|
|
{ |
|
|
ScalarType cosang = ep.f->N().dot((*fi).N()); |
|
|
// Note that if two faces form an angle larger than 90 deg, their contribution should be very very small. |
|
|
// Without this clamping |
|
|
cosang = math::Clamp(cosang, ScalarType(0.0001), ScalarType(1.f)); |
|
|
if (cosang >= sigma) |
|
|
{ |
|
|
ScalarType w = cosang - sigma; |
|
|
normalSum += ep.f->N() * (w * w); // similar normals have a cosang very close to 1 so cosang - sigma is maximized |
|
|
} |
|
|
(*ep.f).SetV(); |
|
|
} |
|
|
} |
|
|
} |
|
|
normalSum.Normalize(); |
|
|
TD[*fi].m = normalSum; |
|
|
} |
|
|
|
|
|
for (FaceIterator fi = m.face.begin(); fi != m.face.end(); ++fi) |
|
|
(*fi).N() = TD[*fi].m; |
|
|
} |
|
|
|
|
|
/****************************************************************************************************************/ |
|
|
// Restituisce il gradiente dell'area del triangolo nel punto p. |
|
|
// Nota che dovrebbe essere sempre un vettore che giace nel piano del triangolo e perpendicolare al lato opposto al vertice p. |
|
|
// Ottimizzato con Maple e poi pesantemente a mano. |
|
|
|
|
|
static CoordType TriAreaGradient(CoordType &p, CoordType &p0, CoordType &p1) |
|
|
{ |
|
|
CoordType dd = p1 - p0; |
|
|
CoordType d0 = p - p0; |
|
|
CoordType d1 = p - p1; |
|
|
CoordType grad; |
|
|
|
|
|
ScalarType t16 = d0[1] * d1[2] - d0[2] * d1[1]; |
|
|
ScalarType t5 = -d0[2] * d1[0] + d0[0] * d1[2]; |
|
|
ScalarType t4 = -d0[0] * d1[1] + d0[1] * d1[0]; |
|
|
|
|
|
ScalarType delta = sqrtf(t4 * t4 + t5 * t5 + t16 * t16); |
|
|
|
|
|
grad[0] = (t5 * (-dd[2]) + t4 * (dd[1])) / delta; |
|
|
grad[1] = (t16 * (-dd[2]) + t4 * (-dd[0])) / delta; |
|
|
grad[2] = (t16 * (dd[1]) + t5 * (dd[0])) / delta; |
|
|
|
|
|
return grad; |
|
|
} |
|
|
|
|
|
template <class ScalarType> |
|
|
static CoordType CrossProdGradient(CoordType &p, CoordType &p0, CoordType &p1, CoordType &m) |
|
|
{ |
|
|
CoordType grad; |
|
|
CoordType p00 = p0 - p; |
|
|
CoordType p01 = p1 - p; |
|
|
grad[0] = (-p00[2] + p01[2]) * m[1] + (-p01[1] + p00[1]) * m[2]; |
|
|
grad[1] = (-p01[2] + p00[2]) * m[0] + (-p00[0] + p01[0]) * m[2]; |
|
|
grad[2] = (-p00[1] + p01[1]) * m[0] + (-p01[0] + p00[0]) * m[1]; |
|
|
|
|
|
return grad; |
|
|
} |
|
|
|
|
|
/* |
|
|
Deve Calcolare il gradiente di |
|
|
E(p) = A(p,p0,p1) (n - m)^2 = |
|
|
A(...) (2-2nm) = |
|
|
(p0-p)^(p1-p) |
|
|
2A - 2A * ------------- m = |
|
|
2A |
|
|
|
|
|
2A - 2 (p0-p)^(p1-p) * m |
|
|
*/ |
|
|
|
|
|
static CoordType FaceErrorGrad(CoordType &p, CoordType &p0, CoordType &p1, CoordType &m) |
|
|
{ |
|
|
return TriAreaGradient(p, p0, p1) * 2.0f - CrossProdGradient(p, p0, p1, m) * 2.0f; |
|
|
} |
|
|
/***************************************************************************/ |
|
|
// Paso Doble Step 2 Fitta la mesh a un dato insieme di normali |
|
|
/***************************************************************************/ |
|
|
|
|
|
static void FitMesh(MeshType &m, |
|
|
SimpleTempData<typename MeshType::VertContainer, PDVertInfo> &TDV, |
|
|
SimpleTempData<typename MeshType::FaceContainer, PDFaceInfo> &TDF, |
|
|
float lambda) |
|
|
{ |
|
|
vcg::face::VFIterator<FaceType> ep; |
|
|
VertexIterator vi; |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
{ |
|
|
CoordType ErrGrad = CoordType(0, 0, 0); |
|
|
|
|
|
ep.f = (*vi).VFp(); |
|
|
ep.z = (*vi).VFi(); |
|
|
while (!ep.End()) |
|
|
{ |
|
|
ErrGrad += FaceErrorGrad(ep.f->V(ep.z)->P(), ep.f->V1(ep.z)->P(), ep.f->V2(ep.z)->P(), TDF[ep.f].m); |
|
|
++ep; |
|
|
} |
|
|
TDV[*vi].np = (*vi).P() - ErrGrad * (ScalarType)lambda; |
|
|
} |
|
|
|
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
(*vi).P() = TDV[*vi].np; |
|
|
} |
|
|
/****************************************************************************************************************/ |
|
|
|
|
|
static void FastFitMesh(MeshType &m, |
|
|
SimpleTempData<typename MeshType::VertContainer, PDVertInfo> &TDV, |
|
|
//SimpleTempData<typename MeshType::FaceContainer, PDFaceInfo> &TDF, |
|
|
bool OnlySelected = false) |
|
|
{ |
|
|
//vcg::face::Pos<FaceType> ep; |
|
|
vcg::face::VFIterator<FaceType> ep; |
|
|
VertexIterator vi; |
|
|
|
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
{ |
|
|
CoordType Sum(0, 0, 0); |
|
|
ScalarType cnt = 0; |
|
|
VFLocalIterator ep(&*vi); |
|
|
for (; !ep.End(); ++ep) |
|
|
{ |
|
|
CoordType bc = Barycenter<FaceType>(*ep.F()); |
|
|
Sum += ep.F()->N() * (ep.F()->N().dot(bc - (*vi).P())); |
|
|
++cnt; |
|
|
} |
|
|
TDV[*vi].np = (*vi).P() + Sum * (1.0 / cnt); |
|
|
} |
|
|
|
|
|
if (OnlySelected) |
|
|
{ |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
if ((*vi).IsS()) |
|
|
(*vi).P() = TDV[*vi].np; |
|
|
} |
|
|
else |
|
|
{ |
|
|
for (vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
(*vi).P() = TDV[*vi].np; |
|
|
} |
|
|
} |
|
|
|
|
|
// The sigma parameter affect the normal smoothing step |
|
|
|
|
|
static void VertexCoordPasoDoble(MeshType &m, int NormalSmoothStep, typename MeshType::ScalarType Sigma = 0, int FitStep = 50, bool SmoothSelected = false) |
|
|
{ |
|
|
tri::RequireCompactness(m); |
|
|
tri::RequireVFAdjacency(m); |
|
|
PDVertInfo lpzv; |
|
|
lpzv.np = CoordType(0, 0, 0); |
|
|
PDFaceInfo lpzf(CoordType(0, 0, 0)); |
|
|
|
|
|
assert(HasPerVertexVFAdjacency(m) && HasPerFaceVFAdjacency(m)); |
|
|
SimpleTempData<typename MeshType::VertContainer, PDVertInfo> TDV(m.vert, lpzv); |
|
|
SimpleTempData<typename MeshType::FaceContainer, PDFaceInfo> TDF(m.face, lpzf); |
|
|
|
|
|
for (int j = 0; j < NormalSmoothStep; ++j) |
|
|
FaceNormalAngleThreshold(m, TDF, Sigma); |
|
|
|
|
|
for (int j = 0; j < FitStep; ++j) |
|
|
FastFitMesh(m, TDV, SmoothSelected); |
|
|
} |
|
|
|
|
|
static void VertexNormalPointCloud(MeshType &m, int neighborNum, int iterNum, KdTree<ScalarType> *tp = 0) |
|
|
{ |
|
|
SimpleTempData<typename MeshType::VertContainer, CoordType> TD(m.vert, CoordType(0, 0, 0)); |
|
|
VertexConstDataWrapper<MeshType> ww(m); |
|
|
KdTree<ScalarType> *tree = 0; |
|
|
if (tp == 0) |
|
|
tree = new KdTree<ScalarType>(ww); |
|
|
else |
|
|
tree = tp; |
|
|
typename KdTree<ScalarType>::PriorityQueue nq; |
|
|
|
|
|
// tree->setMaxNofNeighbors(neighborNum); |
|
|
for (int ii = 0; ii < iterNum; ++ii) |
|
|
{ |
|
|
for (VertexIterator vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
{ |
|
|
tree->doQueryK(vi->cP(), neighborNum, nq); |
|
|
int neighbours = nq.getNofElements(); |
|
|
for (int i = 0; i < neighbours; i++) |
|
|
{ |
|
|
int neightId = nq.getIndex(i); |
|
|
if (m.vert[neightId].cN() * vi->cN() > 0) |
|
|
TD[vi] += m.vert[neightId].cN(); |
|
|
else |
|
|
TD[vi] -= m.vert[neightId].cN(); |
|
|
} |
|
|
} |
|
|
for (VertexIterator vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
{ |
|
|
vi->N() = TD[vi]; |
|
|
TD[vi] = CoordType(0, 0, 0); |
|
|
} |
|
|
tri::UpdateNormal<MeshType>::NormalizePerVertex(m); |
|
|
} |
|
|
|
|
|
if (tp == 0) |
|
|
delete tree; |
|
|
} |
|
|
|
|
|
//! Laplacian smoothing with a reprojection on a target surface. |
|
|
// grid must be a spatial index that contains all triangular faces of the target mesh gridmesh |
|
|
template <class GRID, class MeshTypeTri> |
|
|
static void VertexCoordLaplacianReproject(MeshType &m, GRID &grid, MeshTypeTri &gridmesh) |
|
|
{ |
|
|
for (VertexIterator vi = m.vert.begin(); vi != m.vert.end(); ++vi) |
|
|
{ |
|
|
if (!(*vi).IsD()) |
|
|
VertexCoordLaplacianReproject(m, grid, gridmesh, &*vi); |
|
|
} |
|
|
} |
|
|
|
|
|
template <class GRID, class MeshTypeTri> |
|
|
static void VertexCoordLaplacianReproject(MeshType &m, GRID &grid, MeshTypeTri &gridmesh, typename MeshType::VertexType *vp) |
|
|
{ |
|
|
assert(MeshType::HEdgeType::HasHVAdjacency()); |
|
|
|
|
|
// compute barycenter |
|
|
typedef std::vector<VertexPointer> VertexSet; |
|
|
VertexSet verts; |
|
|
verts = HalfEdgeTopology<MeshType>::getVertices(vp); |
|
|
|
|
|
typename MeshType::CoordType ct(0, 0, 0); |
|
|
for (typename VertexSet::iterator it = verts.begin(); it != verts.end(); ++it) |
|
|
{ |
|
|
ct += (*it)->P(); |
|
|
} |
|
|
ct /= verts.size(); |
|
|
|
|
|
// move vertex |
|
|
vp->P() = ct; |
|
|
|
|
|
std::vector<FacePointer> faces2 = HalfEdgeTopology<MeshType>::get_incident_faces(vp); |
|
|
|
|
|
// estimate normal |
|
|
typename MeshType::CoordType avgn(0, 0, 0); |
|
|
|
|
|
for (unsigned int i = 0; i < faces2.size(); i++) |
|
|
if (faces2[i]) |
|
|
{ |
|
|
std::vector<VertexPointer> vertices = HalfEdgeTopology<MeshType>::getVertices(faces2[i]); |
|
|
|
|
|
assert(vertices.size() == 4); |
|
|
|
|
|
avgn += vcg::Normal<typename MeshType::CoordType>(vertices[0]->cP(), vertices[1]->cP(), vertices[2]->cP()); |
|
|
avgn += vcg::Normal<typename MeshType::CoordType>(vertices[2]->cP(), vertices[3]->cP(), vertices[0]->cP()); |
|
|
} |
|
|
avgn.Normalize(); |
|
|
|
|
|
// reproject |
|
|
ScalarType diag = m.bbox.Diag(); |
|
|
typename MeshType::CoordType raydir = avgn; |
|
|
Ray3<typename MeshType::ScalarType> ray; |
|
|
|
|
|
ray.SetOrigin(vp->P()); |
|
|
ScalarType t; |
|
|
typename MeshTypeTri::FaceType *f = 0; |
|
|
typename MeshTypeTri::FaceType *fr = 0; |
|
|
|
|
|
std::vector<typename MeshTypeTri::CoordType> closests; |
|
|
std::vector<typename MeshTypeTri::ScalarType> minDists; |
|
|
std::vector<typename MeshTypeTri::FaceType *> faces; |
|
|
ray.SetDirection(-raydir); |
|
|
f = vcg::tri::DoRay<MeshTypeTri, GRID>(gridmesh, grid, ray, diag / 4.0, t); |
|
|
|
|
|
if (f) |
|
|
{ |
|
|
closests.push_back(ray.Origin() + ray.Direction() * t); |
|
|
minDists.push_back(fabs(t)); |
|
|
faces.push_back(f); |
|
|
} |
|
|
ray.SetDirection(raydir); |
|
|
fr = vcg::tri::DoRay<MeshTypeTri, GRID>(gridmesh, grid, ray, diag / 4.0, t); |
|
|
if (fr) |
|
|
{ |
|
|
closests.push_back(ray.Origin() + ray.Direction() * t); |
|
|
minDists.push_back(fabs(t)); |
|
|
faces.push_back(fr); |
|
|
} |
|
|
|
|
|
if (fr) |
|
|
if (fr->N() * raydir < 0) |
|
|
fr = 0; // discard: inverse normal; |
|
|
typename MeshType::CoordType newPos; |
|
|
if (minDists.size() == 0) |
|
|
{ |
|
|
newPos = vp->P(); |
|
|
f = 0; |
|
|
} |
|
|
else |
|
|
{ |
|
|
int minI = std::min_element(minDists.begin(), minDists.end()) - minDists.begin(); |
|
|
newPos = closests[minI]; |
|
|
f = faces[minI]; |
|
|
} |
|
|
|
|
|
if (f) |
|
|
vp->P() = newPos; |
|
|
} |
|
|
|
|
|
}; //end Smooth class |
|
|
|
|
|
} // End namespace tri |
|
|
} // End namespace vcg |
|
|
|
|
|
#endif // VCG_SMOOTH
|
|
|
|