You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

412 lines
16 KiB

// David Eberly, Geometric Tools, Redmond WA 98052
// Copyright (c) 1998-2021
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
// https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt
// Version: 4.0.2021.04.22
#pragma once
#include <Mathematics/Logger.h>
#include <Mathematics/ETManifoldMesh.h>
#include <Mathematics/LinearSystem.h>
#include <Mathematics/Polynomial1.h>
#include <Mathematics/Vector2.h>
#include <Mathematics/Vector3.h>
// Conformally map a 2-dimensional manifold mesh with the topology of a sphere
// to a sphere. The algorithm is an implementation of the one in the paper
// S.Haker, S.Angenent, A.Tannenbaum, R.Kikinis, G.Sapiro, and M.Halle.
// Conformal surface parameterization for texture mapping,
// IEEE Transactions on Visualization and Computer Graphics,
// Volume 6, Number 2, pages 181–189, 2000
// The paper is available at https://ieeexplore.ieee.org/document/856998 but
// is not freely downloadable.
namespace gte
{
template <typename Real>
class ConformalMapGenus0
{
public:
// The input mesh should be a closed, manifold surface that has the
// topology of a sphere (genus 0 surface).
ConformalMapGenus0()
:
mSphereRadius(0.0f)
{
}
~ConformalMapGenus0()
{
}
// The returned 'bool' value is 'true' whenever the conjugate gradient
// algorithm converged. Even if it did not, the results might still
// be acceptable.
bool operator()(int numPositions, Vector3<Real> const* positions,
int numTriangles, int const* indices, int punctureTriangle)
{
bool converged = true;
mPlaneCoordinates.resize(numPositions);
mSphereCoordinates.resize(numPositions);
// Construct a triangle-edge representation of mesh.
ETManifoldMesh graph;
int const* currentIndex = indices;
int t;
for (t = 0; t < numTriangles; ++t)
{
int v0 = *currentIndex++;
int v1 = *currentIndex++;
int v2 = *currentIndex++;
graph.Insert(v0, v1, v2);
}
auto const& emap = graph.GetEdges();
// Construct the nondiagonal entries of the sparse matrix A.
typename LinearSystem<Real>::SparseMatrix A;
int v0, v1, v2, i;
Vector3<Real> E0, E1;
Real value;
for (auto const& element : emap)
{
v0 = element.first.V[0];
v1 = element.first.V[1];
value = (Real)0;
for (int j = 0; j < 2; ++j)
{
auto triangle = element.second->T[j];
for (i = 0; i < 3; ++i)
{
v2 = triangle->V[i];
if (v2 != v0 && v2 != v1)
{
E0 = positions[v0] - positions[v2];
E1 = positions[v1] - positions[v2];
value += Dot(E0, E1) / Length(Cross(E0, E1));
}
}
}
value *= -(Real)0.5;
std::array<int, 2> lookup = { v0, v1 };
A[lookup] = value;
}
// Construct the diagonal entries of the sparse matrix A.
std::vector<Real> tmp(numPositions, (Real)0);
for (auto const& element : A)
{
tmp[element.first[0]] -= element.second;
tmp[element.first[1]] -= element.second;
}
for (i = 0; i < numPositions; ++i)
{
std::array<int, 2> lookup = { i, i };
A[lookup] = tmp[i];
}
LogAssert(static_cast<size_t>(numPositions) + emap.size() == A.size(), "Mismatched sizes.");
// Construct the sparse column vector B.
currentIndex = &indices[3 * punctureTriangle];
v0 = *currentIndex++;
v1 = *currentIndex++;
v2 = *currentIndex++;
Vector3<Real> V0 = positions[v0];
Vector3<Real> V1 = positions[v1];
Vector3<Real> V2 = positions[v2];
Vector3<Real> E10 = V1 - V0;
Vector3<Real> E20 = V2 - V0;
Vector3<Real> E12 = V1 - V2;
Vector3<Real> normal = Cross(E20, E10);
Real len10 = Length(E10);
Real invLen10 = (Real)1 / len10;
Real twoArea = Length(normal);
Real invLenNormal = (Real)1 / twoArea;
Real invProd = invLen10 * invLenNormal;
Real re0 = -invLen10;
Real im0 = invProd * Dot(E12, E10);
Real re1 = invLen10;
Real im1 = invProd * Dot(E20, E10);
Real re2 = (Real)0;
Real im2 = -len10 * invLenNormal;
// Solve the sparse system for the real parts.
unsigned int const maxIterations = 1024;
Real const tolerance = 1e-06f;
std::fill(tmp.begin(), tmp.end(), (Real)0);
tmp[v0] = re0;
tmp[v1] = re1;
tmp[v2] = re2;
std::vector<Real> result(numPositions);
unsigned int iterations = LinearSystem<Real>().SolveSymmetricCG(
numPositions, A, tmp.data(), result.data(), maxIterations, tolerance);
if (iterations >= maxIterations)
{
LogWarning("Conjugate gradient solver did not converge.");
converged = false;
}
for (i = 0; i < numPositions; ++i)
{
mPlaneCoordinates[i][0] = result[i];
}
// Solve the sparse system for the imaginary parts.
std::fill(tmp.begin(), tmp.end(), (Real)0);
tmp[v0] = -im0;
tmp[v1] = -im1;
tmp[v2] = -im2;
iterations = LinearSystem<Real>().SolveSymmetricCG(numPositions, A,
tmp.data(), result.data(), maxIterations, tolerance);
if (iterations >= maxIterations)
{
LogWarning("Conjugate gradient solver did not converge.");
converged = false;
}
for (i = 0; i < numPositions; ++i)
{
mPlaneCoordinates[i][1] = result[i];
}
// Scale to [-1,1]^2 for numerical conditioning in later steps.
Real fmin = mPlaneCoordinates[0][0], fmax = fmin;
for (i = 0; i < numPositions; i++)
{
if (mPlaneCoordinates[i][0] < fmin)
{
fmin = mPlaneCoordinates[i][0];
}
else if (mPlaneCoordinates[i][0] > fmax)
{
fmax = mPlaneCoordinates[i][0];
}
if (mPlaneCoordinates[i][1] < fmin)
{
fmin = mPlaneCoordinates[i][1];
}
else if (mPlaneCoordinates[i][1] > fmax)
{
fmax = mPlaneCoordinates[i][1];
}
}
Real halfRange = (Real)0.5 * (fmax - fmin);
Real invHalfRange = (Real)1 / halfRange;
for (i = 0; i < numPositions; ++i)
{
mPlaneCoordinates[i][0] = (Real)-1 + invHalfRange * (mPlaneCoordinates[i][0] - fmin);
mPlaneCoordinates[i][1] = (Real)-1 + invHalfRange * (mPlaneCoordinates[i][1] - fmin);
}
// Map the plane coordinates to the sphere using inverse
// stereographic projection. The main issue is selecting a
// translation in (x,y) and a radius of the projection sphere.
// Both factors strongly influence the final result.
// Use the average as the south pole. The points tend to be
// clustered approximately in the middle of the conformally
// mapped punctured triangle, so the average is a good choice
// to place the pole.
Vector2<Real> origin{ (Real)0, (Real)0 };
for (i = 0; i < numPositions; ++i)
{
origin += mPlaneCoordinates[i];
}
origin /= (Real)numPositions;
for (i = 0; i < numPositions; ++i)
{
mPlaneCoordinates[i] -= origin;
}
mMinPlaneCoordinate = mPlaneCoordinates[0];
mMaxPlaneCoordinate = mPlaneCoordinates[0];
for (i = 1; i < numPositions; ++i)
{
if (mPlaneCoordinates[i][0] < mMinPlaneCoordinate[0])
{
mMinPlaneCoordinate[0] = mPlaneCoordinates[i][0];
}
else if (mPlaneCoordinates[i][0] > mMaxPlaneCoordinate[0])
{
mMaxPlaneCoordinate[0] = mPlaneCoordinates[i][0];
}
if (mPlaneCoordinates[i][1] < mMinPlaneCoordinate[1])
{
mMinPlaneCoordinate[1] = mPlaneCoordinates[i][1];
}
else if (mPlaneCoordinates[i][1] > mMaxPlaneCoordinate[1])
{
mMaxPlaneCoordinate[1] = mPlaneCoordinates[i][1];
}
}
// Select the radius of the sphere so that the projected punctured
// triangle has an area whose fraction of total spherical area is
// the same fraction as the area of the punctured triangle to the
// total area of the original triangle mesh.
Real twoTotalArea = (Real)0;
currentIndex = indices;
for (t = 0; t < numTriangles; ++t)
{
V0 = positions[*currentIndex++];
V1 = positions[*currentIndex++];
V2 = positions[*currentIndex++];
E0 = V1 - V0;
E1 = V2 - V0;
twoTotalArea += Length(Cross(E0, E1));
}
ComputeSphereRadius(v0, v1, v2, twoArea / twoTotalArea);
Real sqrSphereRadius = mSphereRadius * mSphereRadius;
// Inverse stereographic projection to obtain sphere coordinates.
// The sphere is centered at the origin and has radius 1.
for (i = 0; i < numPositions; i++)
{
Real rSqr = Dot(mPlaneCoordinates[i], mPlaneCoordinates[i]);
Real mult = (Real)1 / (rSqr + sqrSphereRadius);
Real x = (Real)2 * mult * sqrSphereRadius * mPlaneCoordinates[i][0];
Real y = (Real)2 * mult * sqrSphereRadius * mPlaneCoordinates[i][1];
Real z = mult * mSphereRadius * (rSqr - sqrSphereRadius);
mSphereCoordinates[i] = Vector3<Real>{ x, y, z } / mSphereRadius;
}
return converged;
}
// Conformal mapping of mesh to plane. The array of coordinates has a
// one-to-one correspondence with the input vertex array.
inline std::vector<Vector2<Real>> const& GetPlaneCoordinates() const
{
return mPlaneCoordinates;
}
inline Vector2<Real> const& GetMinPlaneCoordinate() const
{
return mMinPlaneCoordinate;
}
inline Vector2<Real> const& GetMaxPlaneCoordinate() const
{
return mMaxPlaneCoordinate;
}
// Conformal mapping of mesh to sphere (centered at origin). The array
// of coordinates has a one-to-one correspondence with the input vertex
// array.
inline std::vector<Vector3<Real>> const& GetSphereCoordinates() const
{
return mSphereCoordinates;
}
inline Real GetSphereRadius() const
{
return mSphereRadius;
}
private:
void ComputeSphereRadius(int v0, int v1, int v2, Real areaFraction)
{
Vector2<Real> V0 = mPlaneCoordinates[v0];
Vector2<Real> V1 = mPlaneCoordinates[v1];
Vector2<Real> V2 = mPlaneCoordinates[v2];
Real r0Sqr = Dot(V0, V0);
Real r1Sqr = Dot(V1, V1);
Real r2Sqr = Dot(V2, V2);
Real diffR10 = r1Sqr - r0Sqr;
Real diffR20 = r2Sqr - r0Sqr;
Real diffX10 = V1[0] - V0[0];
Real diffY10 = V1[1] - V0[1];
Real diffX20 = V2[0] - V0[0];
Real diffY20 = V2[1] - V0[1];
Real diffRX10 = V1[0] * r0Sqr - V0[0] * r1Sqr;
Real diffRY10 = V1[1] * r0Sqr - V0[1] * r1Sqr;
Real diffRX20 = V2[0] * r0Sqr - V0[0] * r2Sqr;
Real diffRY20 = V2[1] * r0Sqr - V0[1] * r2Sqr;
Real c0 = diffR20 * diffRY10 - diffR10 * diffRY20;
Real c1 = diffR20 * diffY10 - diffR10 * diffY20;
Real d0 = diffR10 * diffRX20 - diffR20 * diffRX10;
Real d1 = diffR10 * diffX20 - diffR20 * diffX10;
Real e0 = diffRX10 * diffRY20 - diffRX20 * diffRY10;
Real e1 = diffRX10 * diffY20 - diffRX20 * diffY10;
Real e2 = diffX10 * diffY20 - diffX20 * diffY10;
Polynomial1<Real> poly0(6);
poly0[0] = (Real)0;
poly0[1] = (Real)0;
poly0[2] = e0 * e0;
poly0[3] = c0 * c0 + d0 * d0 + (Real)2 * e0 * e1;
poly0[4] = (Real)2 * (c0 * c1 + d0 * d1 + e0 * e1) + e1 * e1;
poly0[5] = c1 * c1 + d1 * d1 + (Real)2 * e1 * e2;
poly0[6] = e2 * e2;
Polynomial1<Real> qpoly0(1), qpoly1(1), qpoly2(1);
qpoly0[0] = r0Sqr;
qpoly0[1] = (Real)1;
qpoly1[0] = r1Sqr;
qpoly1[1] = (Real)1;
qpoly2[0] = r2Sqr;
qpoly2[1] = (Real)1;
Real tmp = areaFraction * static_cast<Real>(GTE_C_PI);
Real amp = tmp * tmp;
Polynomial1<Real> poly1 = amp * qpoly0;
poly1 = poly1 * qpoly0;
poly1 = poly1 * qpoly0;
poly1 = poly1 * qpoly0;
poly1 = poly1 * qpoly1;
poly1 = poly1 * qpoly1;
poly1 = poly1 * qpoly2;
poly1 = poly1 * qpoly2;
Polynomial1<Real> poly2 = poly1 - poly0;
LogAssert(poly2.GetDegree() <= 8, "Expecting degree no larger than 8.");
// Bound a root near zero and apply bisection to find t.
Real tmin = (Real)0, fmin = poly2(tmin);
Real tmax = (Real)1, fmax = poly2(tmax);
LogAssert(fmin > (Real)0 && fmax < (Real)0, "Expecting opposite-signed extremes.");
// Determine the number of iterations to get 'digits' of accuracy.
int const digits = 6;
Real tmp0 = std::log(tmax - tmin);
Real tmp1 = (Real)digits * static_cast<Real>(GTE_C_LN_10);
Real arg = (tmp0 + tmp1) * static_cast<Real>(GTE_C_INV_LN_2);
int maxIterations = static_cast<int>(arg + (Real)0.5);
Real tmid = (Real)0, fmid;
for (int i = 0; i < maxIterations; ++i)
{
tmid = (Real)0.5 * (tmin + tmax);
fmid = poly2(tmid);
Real product = fmid * fmin;
if (product < (Real)0)
{
tmax = tmid;
fmax = fmid;
}
else
{
tmin = tmid;
fmin = fmid;
}
}
mSphereRadius = std::sqrt(tmid);
}
// Conformal mapping to a plane. The plane's (px,py) points
// correspond to the mesh's (mx,my,mz) points.
std::vector<Vector2<Real>> mPlaneCoordinates;
Vector2<Real> mMinPlaneCoordinate, mMaxPlaneCoordinate;
// Conformal mapping to a sphere. The sphere's (sx,sy,sz) points
// correspond to the mesh's (mx,my,mz) points.
std::vector<Vector3<Real>> mSphereCoordinates;
Real mSphereRadius;
};
}