65fb961b8b
-Added unwrap functionality to Mesh -Ability to display and debug mesh UVs -Added multiline draw, so it's easier and faster to draw UVs -Many fixes to SurfaceTool -Fixes to Thekla Unwrap, but it's a piece of ass and it keeps crashing. Will have to go away
1520 lines
44 KiB
C++
1520 lines
44 KiB
C++
// Copyright NVIDIA Corporation 2006 -- Ignacio Castano <icastano@nvidia.com>
|
|
|
|
#include "nvmesh.h" // pch
|
|
|
|
#include "Atlas.h"
|
|
#include "Util.h"
|
|
#include "AtlasBuilder.h"
|
|
#include "AtlasPacker.h"
|
|
#include "SingleFaceMap.h"
|
|
#include "OrthogonalProjectionMap.h"
|
|
#include "LeastSquaresConformalMap.h"
|
|
#include "ParameterizationQuality.h"
|
|
|
|
//#include "nvmesh/export/MeshExportOBJ.h"
|
|
|
|
#include "nvmesh/halfedge/Mesh.h"
|
|
#include "nvmesh/halfedge/Face.h"
|
|
#include "nvmesh/halfedge/Vertex.h"
|
|
|
|
#include "nvmesh/MeshBuilder.h"
|
|
#include "nvmesh/MeshTopology.h"
|
|
#include "nvmesh/param/Util.h"
|
|
#include "nvmesh/geometry/Measurements.h"
|
|
|
|
#include "nvmath/Vector.inl"
|
|
#include "nvmath/Fitting.h"
|
|
#include "nvmath/Box.inl"
|
|
#include "nvmath/ProximityGrid.h"
|
|
#include "nvmath/Morton.h"
|
|
|
|
#include "nvcore/StrLib.h"
|
|
#include "nvcore/Array.inl"
|
|
#include "nvcore/HashMap.inl"
|
|
|
|
using namespace nv;
|
|
|
|
|
|
/// Ctor.
|
|
Atlas::Atlas()
|
|
{
|
|
failed=false;
|
|
}
|
|
|
|
// Dtor.
|
|
Atlas::~Atlas()
|
|
{
|
|
deleteAll(m_meshChartsArray);
|
|
}
|
|
|
|
uint Atlas::chartCount() const
|
|
{
|
|
uint count = 0;
|
|
foreach(c, m_meshChartsArray) {
|
|
count += m_meshChartsArray[c]->chartCount();
|
|
}
|
|
return count;
|
|
}
|
|
|
|
const Chart * Atlas::chartAt(uint i) const
|
|
{
|
|
foreach(c, m_meshChartsArray) {
|
|
uint count = m_meshChartsArray[c]->chartCount();
|
|
|
|
if (i < count) {
|
|
return m_meshChartsArray[c]->chartAt(i);
|
|
}
|
|
|
|
i -= count;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
Chart * Atlas::chartAt(uint i)
|
|
{
|
|
foreach(c, m_meshChartsArray) {
|
|
uint count = m_meshChartsArray[c]->chartCount();
|
|
|
|
if (i < count) {
|
|
return m_meshChartsArray[c]->chartAt(i);
|
|
}
|
|
|
|
i -= count;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// Extract the charts and add to this atlas.
|
|
void Atlas::addMeshCharts(MeshCharts * meshCharts)
|
|
{
|
|
m_meshChartsArray.append(meshCharts);
|
|
}
|
|
|
|
void Atlas::extractCharts(const HalfEdge::Mesh * mesh)
|
|
{
|
|
MeshCharts * meshCharts = new MeshCharts(mesh);
|
|
meshCharts->extractCharts();
|
|
addMeshCharts(meshCharts);
|
|
}
|
|
|
|
void Atlas::computeCharts(const HalfEdge::Mesh * mesh, const SegmentationSettings & settings, const Array<uint> & unchartedMaterialArray)
|
|
{
|
|
failed=false;
|
|
MeshCharts * meshCharts = new MeshCharts(mesh);
|
|
meshCharts->computeCharts(settings, unchartedMaterialArray);
|
|
addMeshCharts(meshCharts);
|
|
}
|
|
|
|
|
|
|
|
|
|
#if 0
|
|
|
|
/// Compute a seamless texture atlas.
|
|
bool Atlas::computeSeamlessTextureAtlas(bool groupFaces/*= true*/, bool scaleTiles/*= false*/, uint w/*= 1024*/, uint h/* = 1024*/)
|
|
{
|
|
// Implement seamless texture atlas similar to what ZBrush does. See also:
|
|
// "Meshed Atlases for Real-Time Procedural Solid Texturing"
|
|
// http://graphics.cs.uiuc.edu/~jch/papers/rtpst.pdf
|
|
|
|
// Other methods that we should experiment with:
|
|
//
|
|
// Seamless Texture Atlases:
|
|
// http://www.cs.jhu.edu/~bpurnomo/STA/index.html
|
|
//
|
|
// Rectangular Multi-Chart Geometry Images:
|
|
// http://graphics.cs.uiuc.edu/~jch/papers/rmcgi.pdf
|
|
//
|
|
// Discrete differential geometry also provide a way of constructing
|
|
// seamless quadrangulations as shown in:
|
|
// http://www.geometry.caltech.edu/pubs/TACD06.pdf
|
|
//
|
|
|
|
#pragma message(NV_FILE_LINE "TODO: Implement seamless texture atlas.")
|
|
|
|
if (groupFaces)
|
|
{
|
|
// @@ TODO.
|
|
}
|
|
else
|
|
{
|
|
// @@ Create one atlas per face.
|
|
}
|
|
|
|
if (scaleTiles)
|
|
{
|
|
// @@ TODO
|
|
}
|
|
|
|
/*
|
|
if (!isQuadMesh(m_mesh)) {
|
|
// Only handle quads for now.
|
|
return false;
|
|
}
|
|
|
|
// Each face is a chart.
|
|
const uint faceCount = m_mesh->faceCount();
|
|
m_chartArray.resize(faceCount);
|
|
|
|
for(uint f = 0; f < faceCount; f++) {
|
|
m_chartArray[f].faceArray.clear();
|
|
m_chartArray[f].faceArray.append(f);
|
|
}
|
|
|
|
// Map each face to a separate square.
|
|
|
|
// Determine face layout according to width and height.
|
|
float aspect = float(m_width) / float(m_height);
|
|
|
|
uint i = 2;
|
|
uint total = (m_width / (i+1)) * (m_height / (i+1));
|
|
while(total > faceCount) {
|
|
i *= 2;
|
|
total = (m_width / (i+1)) * (m_height / (i+1));
|
|
}
|
|
|
|
uint tileSize = i / 2;
|
|
|
|
int x = 0;
|
|
int y = 0;
|
|
|
|
m_result = new HalfEdge::Mesh();
|
|
|
|
// Once you have that it's just matter of traversing the faces.
|
|
for(uint f = 0; f < faceCount; f++) {
|
|
// Compute texture coordinates.
|
|
Vector2 tex[4];
|
|
tex[0] = Vector2(float(x), float(y));
|
|
tex[1] = Vector2(float(x+tileSize), float(y));
|
|
tex[2] = Vector2(float(x+tileSize), float(y+tileSize));
|
|
tex[3] = Vector2(float(x), float(y+tileSize));
|
|
|
|
Array<uint> indexArray(4);
|
|
|
|
const HalfEdge::Face * face = m_mesh->faceAt(f);
|
|
|
|
int i = 0;
|
|
for(HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance(), i++) {
|
|
const HalfEdge::Edge * edge = it.current();
|
|
const HalfEdge::Vertex * vertex = edge->from();
|
|
|
|
HalfEdge::Vertex * newVertex = m_result->addVertex(vertex->id(), vertex->pos());
|
|
|
|
newVertex->setTex(Vector3(tex[i], 0));
|
|
newVertex->setNor(vertex->nor());
|
|
|
|
indexArray.append(m_result->vertexCount() + 1);
|
|
}
|
|
|
|
m_result->addFace(indexArray);
|
|
|
|
// Move to the next tile.
|
|
x += tileSize + 1;
|
|
if (x + tileSize > m_width) {
|
|
x = 0;
|
|
y += tileSize + 1;
|
|
}
|
|
}
|
|
*/
|
|
|
|
return false;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
void Atlas::parameterizeCharts()
|
|
{
|
|
foreach(i, m_meshChartsArray) {
|
|
m_meshChartsArray[i]->parameterizeCharts();
|
|
}
|
|
}
|
|
|
|
|
|
float Atlas::packCharts(int quality, float texelsPerUnit, bool blockAlign, bool conservative)
|
|
{
|
|
AtlasPacker packer(this);
|
|
packer.packCharts(quality, texelsPerUnit, blockAlign, conservative);
|
|
if (hasFailed())
|
|
return 0;
|
|
return packer.computeAtlasUtilization();
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ctor.
|
|
MeshCharts::MeshCharts(const HalfEdge::Mesh * mesh) : m_mesh(mesh)
|
|
{
|
|
}
|
|
|
|
// Dtor.
|
|
MeshCharts::~MeshCharts()
|
|
{
|
|
deleteAll(m_chartArray);
|
|
}
|
|
|
|
|
|
void MeshCharts::extractCharts()
|
|
{
|
|
const uint faceCount = m_mesh->faceCount();
|
|
|
|
int first = 0;
|
|
Array<uint> queue(faceCount);
|
|
|
|
BitArray bitFlags(faceCount);
|
|
bitFlags.clearAll();
|
|
|
|
for (uint f = 0; f < faceCount; f++)
|
|
{
|
|
if (bitFlags.bitAt(f) == false)
|
|
{
|
|
// Start new patch. Reset queue.
|
|
first = 0;
|
|
queue.clear();
|
|
queue.append(f);
|
|
bitFlags.setBitAt(f);
|
|
|
|
while (first != queue.count())
|
|
{
|
|
const HalfEdge::Face * face = m_mesh->faceAt(queue[first]);
|
|
|
|
// Visit face neighbors of queue[first]
|
|
for (HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
|
{
|
|
const HalfEdge::Edge * edge = it.current();
|
|
nvDebugCheck(edge->pair != NULL);
|
|
|
|
if (!edge->isBoundary() && /*!edge->isSeam()*/
|
|
//!(edge->from()->tex() != edge->pair()->to()->tex() || edge->to()->tex() != edge->pair()->from()->tex()))
|
|
!(edge->from() != edge->pair->to() || edge->to() != edge->pair->from())) // Preserve existing seams (not just texture seams).
|
|
{
|
|
const HalfEdge::Face * neighborFace = edge->pair->face;
|
|
nvDebugCheck(neighborFace != NULL);
|
|
|
|
if (bitFlags.bitAt(neighborFace->id) == false)
|
|
{
|
|
queue.append(neighborFace->id);
|
|
bitFlags.setBitAt(neighborFace->id);
|
|
}
|
|
}
|
|
}
|
|
|
|
first++;
|
|
}
|
|
|
|
Chart * chart = new Chart();
|
|
chart->build(m_mesh, queue);
|
|
|
|
m_chartArray.append(chart);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
LSCM:
|
|
- identify sharp features using local dihedral angles.
|
|
- identify seed faces farthest from sharp features.
|
|
- grow charts from these seeds.
|
|
|
|
MCGIM:
|
|
- phase 1: chart growth
|
|
- grow all charts simultaneously using dijkstra search on the dual graph of the mesh.
|
|
- graph edges are weighted based on planarity metric.
|
|
- metric uses distance to global chart normal.
|
|
- terminate when all faces have been assigned.
|
|
- phase 2: seed computation:
|
|
- place new seed of the chart at the most interior face.
|
|
- most interior is evaluated using distance metric only.
|
|
|
|
- method repeates the two phases, until the location of the seeds does not change.
|
|
- cycles are detected by recording all the previous seeds and chartification terminates.
|
|
|
|
D-Charts:
|
|
|
|
- Uniaxial conic metric:
|
|
- N_c = axis of the generalized cone that best fits the chart. (cone can a be cylinder or a plane).
|
|
- omega_c = angle between the face normals and the axis.
|
|
- Fitting error between chart C and tringle t: F(c,t) = (N_c*n_t - cos(omega_c))^2
|
|
|
|
- Compactness metrics:
|
|
- Roundness:
|
|
- C(c,t) = pi * D(S_c,t)^2 / A_c
|
|
- S_c = chart seed.
|
|
- D(S_c,t) = length of the shortest path inside the chart betwen S_c and t.
|
|
- A_c = chart area.
|
|
- Straightness:
|
|
- P(c,t) = l_out(c,t) / l_in(c,t)
|
|
- l_out(c,t) = lenght of the edges not shared between C and t.
|
|
- l_in(c,t) = lenght of the edges shared between C and t.
|
|
|
|
- Combined metric:
|
|
- Cost(c,t) = F(c,t)^alpha + C(c,t)^beta + P(c,t)^gamma
|
|
- alpha = 1, beta = 0.7, gamma = 0.5
|
|
|
|
|
|
|
|
|
|
Our basic approach:
|
|
- Just one iteration of k-means?
|
|
- Avoid dijkstra by greedily growing charts until a threshold is met. Increase threshold and repeat until no faces left.
|
|
- If distortion metric is too high, split chart, add two seeds.
|
|
- If chart size is low, try removing chart.
|
|
|
|
|
|
Postprocess:
|
|
- If topology is not disk:
|
|
- Fill holes, if new faces fit proxy.
|
|
- Find best cut, otherwise.
|
|
- After parameterization:
|
|
- If boundary self-intersects:
|
|
- cut chart along the closest two diametral boundary vertices, repeat parametrization.
|
|
- what if the overlap is on an appendix? How do we find that out and cut appropiately?
|
|
- emphasize roundness metrics to prevent those cases.
|
|
- If interior self-overlaps: preserve boundary parameterization and use mean-value map.
|
|
|
|
*/
|
|
|
|
|
|
SegmentationSettings::SegmentationSettings()
|
|
{
|
|
// Charts have no area or boundary limits right now.
|
|
maxChartArea = NV_FLOAT_MAX;
|
|
maxBoundaryLength = NV_FLOAT_MAX;
|
|
|
|
proxyFitMetricWeight = 1.0f;
|
|
roundnessMetricWeight = 0.1f;
|
|
straightnessMetricWeight = 0.25f;
|
|
normalSeamMetricWeight = 1.0f;
|
|
textureSeamMetricWeight = 0.1f;
|
|
}
|
|
|
|
|
|
|
|
void MeshCharts::computeCharts(const SegmentationSettings & settings, const Array<uint> & unchartedMaterialArray)
|
|
{
|
|
Chart * vertexMap = NULL;
|
|
|
|
if (unchartedMaterialArray.count() != 0) {
|
|
vertexMap = new Chart();
|
|
vertexMap->buildVertexMap(m_mesh, unchartedMaterialArray);
|
|
|
|
if (vertexMap->faceCount() == 0) {
|
|
delete vertexMap;
|
|
vertexMap = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
AtlasBuilder builder(m_mesh);
|
|
|
|
if (vertexMap != NULL) {
|
|
// Mark faces that do not need to be charted.
|
|
builder.markUnchartedFaces(vertexMap->faceArray());
|
|
|
|
m_chartArray.append(vertexMap);
|
|
}
|
|
|
|
if (builder.facesLeft != 0) {
|
|
|
|
// Tweak these values:
|
|
const float maxThreshold = 2;
|
|
const uint growFaceCount = 32;
|
|
const uint maxIterations = 4;
|
|
|
|
builder.settings = settings;
|
|
|
|
//builder.settings.proxyFitMetricWeight *= 0.75; // relax proxy fit weight during initial seed placement.
|
|
//builder.settings.roundnessMetricWeight = 0;
|
|
//builder.settings.straightnessMetricWeight = 0;
|
|
|
|
// This seems a reasonable estimate.
|
|
uint maxSeedCount = max(6U, builder.facesLeft);
|
|
|
|
// Create initial charts greedely.
|
|
nvDebug("### Placing seeds\n");
|
|
builder.placeSeeds(maxThreshold, maxSeedCount);
|
|
nvDebug("### Placed %d seeds (max = %d)\n", builder.chartCount(), maxSeedCount);
|
|
|
|
builder.updateProxies();
|
|
|
|
builder.mergeCharts();
|
|
|
|
#if 1
|
|
nvDebug("### Relocating seeds\n");
|
|
builder.relocateSeeds();
|
|
|
|
nvDebug("### Reset charts\n");
|
|
builder.resetCharts();
|
|
|
|
if (vertexMap != NULL) {
|
|
builder.markUnchartedFaces(vertexMap->faceArray());
|
|
}
|
|
|
|
builder.settings = settings;
|
|
|
|
nvDebug("### Growing charts\n");
|
|
|
|
// Restart process growing charts in parallel.
|
|
uint iteration = 0;
|
|
while (true)
|
|
{
|
|
if (!builder.growCharts(maxThreshold, growFaceCount))
|
|
{
|
|
nvDebug("### Can't grow anymore\n");
|
|
|
|
// If charts cannot grow more: fill holes, merge charts, relocate seeds and start new iteration.
|
|
|
|
nvDebug("### Filling holes\n");
|
|
builder.fillHoles(maxThreshold);
|
|
nvDebug("### Using %d charts now\n", builder.chartCount());
|
|
|
|
builder.updateProxies();
|
|
|
|
nvDebug("### Merging charts\n");
|
|
builder.mergeCharts();
|
|
nvDebug("### Using %d charts now\n", builder.chartCount());
|
|
|
|
nvDebug("### Reseeding\n");
|
|
if (!builder.relocateSeeds())
|
|
{
|
|
nvDebug("### Cannot relocate seeds anymore\n");
|
|
|
|
// Done!
|
|
break;
|
|
}
|
|
|
|
if (iteration == maxIterations)
|
|
{
|
|
nvDebug("### Reached iteration limit\n");
|
|
break;
|
|
}
|
|
iteration++;
|
|
|
|
nvDebug("### Reset charts\n");
|
|
builder.resetCharts();
|
|
|
|
if (vertexMap != NULL) {
|
|
builder.markUnchartedFaces(vertexMap->faceArray());
|
|
}
|
|
|
|
nvDebug("### Growing charts\n");
|
|
}
|
|
};
|
|
#endif
|
|
|
|
// Make sure no holes are left!
|
|
nvDebugCheck(builder.facesLeft == 0);
|
|
|
|
const uint chartCount = builder.chartArray.count();
|
|
for (uint i = 0; i < chartCount; i++)
|
|
{
|
|
Chart * chart = new Chart();
|
|
m_chartArray.append(chart);
|
|
|
|
chart->build(m_mesh, builder.chartFaces(i));
|
|
}
|
|
}
|
|
|
|
|
|
const uint chartCount = m_chartArray.count();
|
|
|
|
// Build face indices.
|
|
m_faceChart.resize(m_mesh->faceCount());
|
|
m_faceIndex.resize(m_mesh->faceCount());
|
|
|
|
for (uint i = 0; i < chartCount; i++)
|
|
{
|
|
const Chart * chart = m_chartArray[i];
|
|
|
|
const uint faceCount = chart->faceCount();
|
|
for (uint f = 0; f < faceCount; f++)
|
|
{
|
|
uint idx = chart->faceAt(f);
|
|
m_faceChart[idx] = i;
|
|
m_faceIndex[idx] = f;
|
|
}
|
|
}
|
|
|
|
// Build an exclusive prefix sum of the chart vertex counts.
|
|
m_chartVertexCountPrefixSum.resize(chartCount);
|
|
|
|
if (chartCount > 0)
|
|
{
|
|
m_chartVertexCountPrefixSum[0] = 0;
|
|
|
|
for (uint i = 1; i < chartCount; i++)
|
|
{
|
|
const Chart * chart = m_chartArray[i-1];
|
|
m_chartVertexCountPrefixSum[i] = m_chartVertexCountPrefixSum[i-1] + chart->vertexCount();
|
|
}
|
|
|
|
m_totalVertexCount = m_chartVertexCountPrefixSum[chartCount - 1] + m_chartArray[chartCount-1]->vertexCount();
|
|
}
|
|
else
|
|
{
|
|
m_totalVertexCount = 0;
|
|
}
|
|
}
|
|
|
|
|
|
void MeshCharts::parameterizeCharts()
|
|
{
|
|
ParameterizationQuality globalParameterizationQuality;
|
|
|
|
// Parameterize the charts.
|
|
uint diskCount = 0;
|
|
const uint chartCount = m_chartArray.count();
|
|
for (uint i = 0; i < chartCount; i++)\
|
|
{
|
|
Chart * chart = m_chartArray[i];
|
|
|
|
bool isValid = false;
|
|
|
|
if (chart->isVertexMapped()) {
|
|
continue;
|
|
}
|
|
|
|
if (chart->isDisk())
|
|
{
|
|
diskCount++;
|
|
|
|
ParameterizationQuality chartParameterizationQuality;
|
|
|
|
if (chart->faceCount() == 1) {
|
|
computeSingleFaceMap(chart->unifiedMesh());
|
|
|
|
chartParameterizationQuality = ParameterizationQuality(chart->unifiedMesh());
|
|
}
|
|
else {
|
|
computeOrthogonalProjectionMap(chart->unifiedMesh());
|
|
ParameterizationQuality orthogonalQuality(chart->unifiedMesh());
|
|
|
|
computeLeastSquaresConformalMap(chart->unifiedMesh());
|
|
ParameterizationQuality lscmQuality(chart->unifiedMesh());
|
|
|
|
// If the orthogonal projection produces better results, just use that.
|
|
// @@ It may be dangerous to do this, because isValid() does not detect self-overlaps.
|
|
// @@ Another problem is that with very thin patches with nearly zero parametric area, the results of our metric are not accurate.
|
|
/*if (orthogonalQuality.isValid() && orthogonalQuality.rmsStretchMetric() < lscmQuality.rmsStretchMetric()) {
|
|
computeOrthogonalProjectionMap(chart->unifiedMesh());
|
|
chartParameterizationQuality = orthogonalQuality;
|
|
}
|
|
else*/ {
|
|
chartParameterizationQuality = lscmQuality;
|
|
}
|
|
|
|
// If conformal map failed,
|
|
|
|
// @@ Experiment with other parameterization methods.
|
|
//computeCircularBoundaryMap(chart->unifiedMesh());
|
|
//computeConformalMap(chart->unifiedMesh());
|
|
//computeNaturalConformalMap(chart->unifiedMesh());
|
|
//computeGuidanceGradientMap(chart->unifiedMesh());
|
|
}
|
|
|
|
//ParameterizationQuality chartParameterizationQuality(chart->unifiedMesh());
|
|
|
|
isValid = chartParameterizationQuality.isValid();
|
|
|
|
if (!isValid)
|
|
{
|
|
nvDebug("*** Invalid parameterization.\n");
|
|
#if 0
|
|
// Dump mesh to inspect problem:
|
|
static int pieceCount = 0;
|
|
|
|
StringBuilder fileName;
|
|
fileName.format("invalid_chart_%d.obj", pieceCount++);
|
|
exportMesh(chart->unifiedMesh(), fileName.str());
|
|
#endif
|
|
}
|
|
|
|
// @@ Check that parameterization quality is above a certain threshold.
|
|
|
|
// @@ Detect boundary self-intersections.
|
|
|
|
globalParameterizationQuality += chartParameterizationQuality;
|
|
}
|
|
|
|
if (!isValid)
|
|
{
|
|
//nvDebugBreak();
|
|
// @@ Run the builder again, but only on this chart.
|
|
//AtlasBuilder builder(chart->chartMesh());
|
|
}
|
|
|
|
// Transfer parameterization from unified mesh to chart mesh.
|
|
chart->transferParameterization();
|
|
|
|
}
|
|
|
|
nvDebug(" Parameterized %d/%d charts.\n", diskCount, chartCount);
|
|
nvDebug(" RMS stretch metric: %f\n", globalParameterizationQuality.rmsStretchMetric());
|
|
nvDebug(" MAX stretch metric: %f\n", globalParameterizationQuality.maxStretchMetric());
|
|
nvDebug(" RMS conformal metric: %f\n", globalParameterizationQuality.rmsConformalMetric());
|
|
nvDebug(" RMS authalic metric: %f\n", globalParameterizationQuality.maxAuthalicMetric());
|
|
}
|
|
|
|
|
|
|
|
Chart::Chart() : m_chartMesh(NULL), m_unifiedMesh(NULL), m_isDisk(false), m_isVertexMapped(false)
|
|
{
|
|
}
|
|
|
|
void Chart::build(const HalfEdge::Mesh * originalMesh, const Array<uint> & faceArray)
|
|
{
|
|
// Copy face indices.
|
|
m_faceArray = faceArray;
|
|
|
|
const uint meshVertexCount = originalMesh->vertexCount();
|
|
|
|
m_chartMesh = new HalfEdge::Mesh();
|
|
m_unifiedMesh = new HalfEdge::Mesh();
|
|
|
|
Array<uint> chartMeshIndices;
|
|
chartMeshIndices.resize(meshVertexCount, ~0);
|
|
|
|
Array<uint> unifiedMeshIndices;
|
|
unifiedMeshIndices.resize(meshVertexCount, ~0);
|
|
|
|
// Add vertices.
|
|
const uint faceCount = faceArray.count();
|
|
for (uint f = 0; f < faceCount; f++)
|
|
{
|
|
const HalfEdge::Face * face = originalMesh->faceAt(faceArray[f]);
|
|
nvDebugCheck(face != NULL);
|
|
|
|
for(HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
|
{
|
|
const HalfEdge::Vertex * vertex = it.current()->vertex;
|
|
const HalfEdge::Vertex * unifiedVertex = vertex->firstColocal();
|
|
|
|
if (unifiedMeshIndices[unifiedVertex->id] == ~0)
|
|
{
|
|
unifiedMeshIndices[unifiedVertex->id] = m_unifiedMesh->vertexCount();
|
|
|
|
nvDebugCheck(vertex->pos == unifiedVertex->pos);
|
|
m_unifiedMesh->addVertex(vertex->pos);
|
|
}
|
|
|
|
if (chartMeshIndices[vertex->id] == ~0)
|
|
{
|
|
chartMeshIndices[vertex->id] = m_chartMesh->vertexCount();
|
|
m_chartToOriginalMap.append(vertex->id);
|
|
m_chartToUnifiedMap.append(unifiedMeshIndices[unifiedVertex->id]);
|
|
|
|
HalfEdge::Vertex * v = m_chartMesh->addVertex(vertex->pos);
|
|
v->nor = vertex->nor;
|
|
v->tex = vertex->tex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is ignoring the canonical map:
|
|
// - Is it really necessary to link colocals?
|
|
|
|
m_chartMesh->linkColocals();
|
|
//m_unifiedMesh->linkColocals(); // Not strictly necessary, no colocals in the unified mesh. # Wrong.
|
|
|
|
// This check is not valid anymore, if the original mesh vertices were linked with a canonical map, then it might have
|
|
// some colocal vertices that were unlinked. So, the unified mesh might have some duplicate vertices, because firstColocal()
|
|
// is not guaranteed to return the same vertex for two colocal vertices.
|
|
//nvCheck(m_chartMesh->colocalVertexCount() == m_unifiedMesh->vertexCount());
|
|
|
|
// Is that OK? What happens in meshes were that happens? Does anything break? Apparently not...
|
|
|
|
|
|
|
|
Array<uint> faceIndices(7);
|
|
|
|
// Add faces.
|
|
for (uint f = 0; f < faceCount; f++)
|
|
{
|
|
const HalfEdge::Face * face = originalMesh->faceAt(faceArray[f]);
|
|
nvDebugCheck(face != NULL);
|
|
|
|
faceIndices.clear();
|
|
|
|
for(HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
|
{
|
|
const HalfEdge::Vertex * vertex = it.current()->vertex;
|
|
nvDebugCheck(vertex != NULL);
|
|
|
|
faceIndices.append(chartMeshIndices[vertex->id]);
|
|
}
|
|
|
|
m_chartMesh->addFace(faceIndices);
|
|
|
|
faceIndices.clear();
|
|
|
|
for(HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
|
{
|
|
const HalfEdge::Vertex * vertex = it.current()->vertex;
|
|
nvDebugCheck(vertex != NULL);
|
|
|
|
vertex = vertex->firstColocal();
|
|
|
|
faceIndices.append(unifiedMeshIndices[vertex->id]);
|
|
}
|
|
|
|
m_unifiedMesh->addFace(faceIndices);
|
|
}
|
|
|
|
m_chartMesh->linkBoundary();
|
|
m_unifiedMesh->linkBoundary();
|
|
|
|
//exportMesh(m_unifiedMesh.ptr(), "debug_input.obj");
|
|
|
|
if (m_unifiedMesh->splitBoundaryEdges()) {
|
|
m_unifiedMesh = unifyVertices(m_unifiedMesh.ptr());
|
|
}
|
|
|
|
//exportMesh(m_unifiedMesh.ptr(), "debug_split.obj");
|
|
|
|
// Closing the holes is not always the best solution and does not fix all the problems.
|
|
// We need to do some analysis of the holes and the genus to:
|
|
// - Find cuts that reduce genus.
|
|
// - Find cuts to connect holes.
|
|
// - Use minimal spanning trees or seamster.
|
|
if (!closeHoles()) {
|
|
/*static int pieceCount = 0;
|
|
StringBuilder fileName;
|
|
fileName.format("debug_hole_%d.obj", pieceCount++);
|
|
exportMesh(m_unifiedMesh.ptr(), fileName.str());*/
|
|
}
|
|
|
|
m_unifiedMesh = triangulate(m_unifiedMesh.ptr());
|
|
|
|
//exportMesh(m_unifiedMesh.ptr(), "debug_triangulated.obj");
|
|
|
|
|
|
// Analyze chart topology.
|
|
MeshTopology topology(m_unifiedMesh.ptr());
|
|
m_isDisk = topology.isDisk();
|
|
|
|
// This is sometimes failing, when triangulate fails to add a triangle, it generates a hole in the mesh.
|
|
//nvDebugCheck(m_isDisk);
|
|
|
|
/*if (!m_isDisk) {
|
|
static int pieceCount = 0;
|
|
StringBuilder fileName;
|
|
fileName.format("debug_hole_%d.obj", pieceCount++);
|
|
exportMesh(m_unifiedMesh.ptr(), fileName.str());
|
|
}*/
|
|
|
|
|
|
#if 0
|
|
if (!m_isDisk) {
|
|
nvDebugBreak();
|
|
|
|
static int pieceCount = 0;
|
|
|
|
StringBuilder fileName;
|
|
fileName.format("debug_nodisk_%d.obj", pieceCount++);
|
|
exportMesh(m_chartMesh.ptr(), fileName.str());
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
void Chart::buildVertexMap(const HalfEdge::Mesh * originalMesh, const Array<uint> & unchartedMaterialArray)
|
|
{
|
|
nvCheck(m_chartMesh == NULL && m_unifiedMesh == NULL);
|
|
|
|
m_isVertexMapped = true;
|
|
|
|
// Build face indices.
|
|
m_faceArray.clear();
|
|
|
|
const uint meshFaceCount = originalMesh->faceCount();
|
|
for (uint f = 0; f < meshFaceCount; f++) {
|
|
const HalfEdge::Face * face = originalMesh->faceAt(f);
|
|
|
|
if (unchartedMaterialArray.contains(face->material)) {
|
|
m_faceArray.append(f);
|
|
}
|
|
}
|
|
|
|
const uint faceCount = m_faceArray.count();
|
|
|
|
if (faceCount == 0) {
|
|
return;
|
|
}
|
|
|
|
|
|
// @@ The chartMesh construction is basically the same as with regular charts, don't duplicate!
|
|
|
|
const uint meshVertexCount = originalMesh->vertexCount();
|
|
|
|
m_chartMesh = new HalfEdge::Mesh();
|
|
|
|
Array<uint> chartMeshIndices;
|
|
chartMeshIndices.resize(meshVertexCount, ~0);
|
|
|
|
// Vertex map mesh only has disconnected vertices.
|
|
for (uint f = 0; f < faceCount; f++)
|
|
{
|
|
const HalfEdge::Face * face = originalMesh->faceAt(m_faceArray[f]);
|
|
nvDebugCheck(face != NULL);
|
|
|
|
for (HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
|
{
|
|
const HalfEdge::Vertex * vertex = it.current()->vertex;
|
|
|
|
if (chartMeshIndices[vertex->id] == ~0)
|
|
{
|
|
chartMeshIndices[vertex->id] = m_chartMesh->vertexCount();
|
|
m_chartToOriginalMap.append(vertex->id);
|
|
|
|
HalfEdge::Vertex * v = m_chartMesh->addVertex(vertex->pos);
|
|
v->nor = vertex->nor;
|
|
v->tex = vertex->tex; // @@ Not necessary.
|
|
}
|
|
}
|
|
}
|
|
|
|
// @@ Link colocals using the original mesh canonical map? Build canonical map on the fly? Do we need to link colocals at all for this?
|
|
//m_chartMesh->linkColocals();
|
|
|
|
Array<uint> faceIndices(7);
|
|
|
|
// Add faces.
|
|
for (uint f = 0; f < faceCount; f++)
|
|
{
|
|
const HalfEdge::Face * face = originalMesh->faceAt(m_faceArray[f]);
|
|
nvDebugCheck(face != NULL);
|
|
|
|
faceIndices.clear();
|
|
|
|
for(HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
|
{
|
|
const HalfEdge::Vertex * vertex = it.current()->vertex;
|
|
nvDebugCheck(vertex != NULL);
|
|
nvDebugCheck(chartMeshIndices[vertex->id] != ~0);
|
|
|
|
faceIndices.append(chartMeshIndices[vertex->id]);
|
|
}
|
|
|
|
HalfEdge::Face * new_face = m_chartMesh->addFace(faceIndices);
|
|
nvDebugCheck(new_face != NULL);
|
|
}
|
|
|
|
m_chartMesh->linkBoundary();
|
|
|
|
|
|
const uint chartVertexCount = m_chartMesh->vertexCount();
|
|
|
|
Box bounds;
|
|
bounds.clearBounds();
|
|
|
|
for (uint i = 0; i < chartVertexCount; i++) {
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(i);
|
|
bounds.addPointToBounds(vertex->pos);
|
|
}
|
|
|
|
ProximityGrid grid;
|
|
grid.init(bounds, chartVertexCount);
|
|
|
|
for (uint i = 0; i < chartVertexCount; i++) {
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(i);
|
|
grid.add(vertex->pos, i);
|
|
}
|
|
|
|
|
|
#if 0
|
|
// Arrange vertices in a rectangle.
|
|
vertexMapWidth = ftoi_ceil(sqrtf(float(chartVertexCount)));
|
|
vertexMapHeight = (chartVertexCount + vertexMapWidth - 1) / vertexMapWidth;
|
|
nvDebugCheck(vertexMapWidth >= vertexMapHeight);
|
|
|
|
int x = 0, y = 0;
|
|
for (uint i = 0; i < chartVertexCount; i++) {
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(i);
|
|
|
|
vertex->tex.x = float(x);
|
|
vertex->tex.y = float(y);
|
|
|
|
x++;
|
|
if (x == vertexMapWidth) {
|
|
x = 0;
|
|
y++;
|
|
nvCheck(y < vertexMapHeight);
|
|
}
|
|
}
|
|
|
|
#elif 0
|
|
// Arrange vertices in a rectangle, traversing grid in 3D morton order and laying them down in 2D morton order.
|
|
vertexMapWidth = ftoi_ceil(sqrtf(float(chartVertexCount)));
|
|
vertexMapHeight = (chartVertexCount + vertexMapWidth - 1) / vertexMapWidth;
|
|
nvDebugCheck(vertexMapWidth >= vertexMapHeight);
|
|
|
|
int n = 0;
|
|
uint32 texelCode = 0;
|
|
|
|
uint cellsVisited = 0;
|
|
|
|
const uint32 cellCodeCount = grid.mortonCount();
|
|
for (uint32 cellCode = 0; cellCode < cellCodeCount; cellCode++) {
|
|
int cell = grid.mortonIndex(cellCode);
|
|
if (cell < 0) continue;
|
|
|
|
cellsVisited++;
|
|
|
|
const Array<uint> & indexArray = grid.cellArray[cell].indexArray;
|
|
|
|
foreach(i, indexArray) {
|
|
uint idx = indexArray[i];
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(idx);
|
|
|
|
//vertex->tex.x = float(n % rectangleWidth) + 0.5f;
|
|
//vertex->tex.y = float(n / rectangleWidth) + 0.5f;
|
|
|
|
// Lay down the points in z order too.
|
|
uint x, y;
|
|
do {
|
|
x = decodeMorton2X(texelCode);
|
|
y = decodeMorton2Y(texelCode);
|
|
texelCode++;
|
|
} while (x >= U32(vertexMapWidth) || y >= U32(vertexMapHeight));
|
|
|
|
vertex->tex.x = float(x);
|
|
vertex->tex.y = float(y);
|
|
|
|
n++;
|
|
}
|
|
}
|
|
|
|
nvDebugCheck(cellsVisited == grid.cellArray.count());
|
|
nvDebugCheck(n == chartVertexCount);
|
|
|
|
#else
|
|
|
|
uint texelCount = 0;
|
|
|
|
const float positionThreshold = 0.01f;
|
|
const float normalThreshold = 0.01f;
|
|
|
|
uint verticesVisited = 0;
|
|
uint cellsVisited = 0;
|
|
|
|
Array<int> vertexIndexArray;
|
|
vertexIndexArray.resize(chartVertexCount, -1); // Init all indices to -1.
|
|
|
|
// Traverse vertices in morton order. @@ It may be more interesting to sort them based on orientation.
|
|
const uint cellCodeCount = grid.mortonCount();
|
|
for (uint cellCode = 0; cellCode < cellCodeCount; cellCode++) {
|
|
int cell = grid.mortonIndex(cellCode);
|
|
if (cell < 0) continue;
|
|
|
|
cellsVisited++;
|
|
|
|
const Array<uint> & indexArray = grid.cellArray[cell].indexArray;
|
|
|
|
foreach(i, indexArray) {
|
|
uint idx = indexArray[i];
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(idx);
|
|
|
|
nvDebugCheck(vertexIndexArray[idx] == -1);
|
|
|
|
Array<uint> neighbors;
|
|
grid.gather(vertex->pos, positionThreshold, /*ref*/neighbors);
|
|
|
|
// Compare against all nearby vertices, cluster greedily.
|
|
foreach(j, neighbors) {
|
|
uint otherIdx = neighbors[j];
|
|
|
|
if (vertexIndexArray[otherIdx] != -1) {
|
|
HalfEdge::Vertex * otherVertex = m_chartMesh->vertexAt(otherIdx);
|
|
|
|
if (distance(vertex->pos, otherVertex->pos) < positionThreshold &&
|
|
distance(vertex->nor, otherVertex->nor) < normalThreshold)
|
|
{
|
|
vertexIndexArray[idx] = vertexIndexArray[otherIdx];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If index not assigned, assign new one.
|
|
if (vertexIndexArray[idx] == -1) {
|
|
vertexIndexArray[idx] = texelCount++;
|
|
}
|
|
|
|
verticesVisited++;
|
|
}
|
|
}
|
|
|
|
nvDebugCheck(cellsVisited == grid.cellArray.count());
|
|
nvDebugCheck(verticesVisited == chartVertexCount);
|
|
|
|
vertexMapWidth = ftoi_ceil(sqrtf(float(texelCount)));
|
|
vertexMapWidth = (vertexMapWidth + 3) & ~3; // Width aligned to 4.
|
|
vertexMapHeight = vertexMapWidth == 0 ? 0 : (texelCount + vertexMapWidth - 1) / vertexMapWidth;
|
|
//vertexMapHeight = (vertexMapHeight + 3) & ~3; // Height aligned to 4.
|
|
nvDebugCheck(vertexMapWidth >= vertexMapHeight);
|
|
|
|
nvDebug("Reduced vertex count from %d to %d.\n", chartVertexCount, texelCount);
|
|
|
|
#if 0
|
|
// This lays down the clustered vertices linearly.
|
|
for (uint i = 0; i < chartVertexCount; i++) {
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(i);
|
|
|
|
int idx = vertexIndexArray[i];
|
|
|
|
vertex->tex.x = float(idx % vertexMapWidth);
|
|
vertex->tex.y = float(idx / vertexMapWidth);
|
|
}
|
|
#else
|
|
// Lay down the clustered vertices in morton order.
|
|
|
|
Array<uint> texelCodes;
|
|
texelCodes.resize(texelCount);
|
|
|
|
// For each texel, assign one morton code.
|
|
uint texelCode = 0;
|
|
for (uint i = 0; i < texelCount; i++) {
|
|
uint x, y;
|
|
do {
|
|
x = decodeMorton2X(texelCode);
|
|
y = decodeMorton2Y(texelCode);
|
|
texelCode++;
|
|
} while (x >= U32(vertexMapWidth) || y >= U32(vertexMapHeight));
|
|
|
|
texelCodes[i] = texelCode - 1;
|
|
}
|
|
|
|
for (uint i = 0; i < chartVertexCount; i++) {
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(i);
|
|
|
|
int idx = vertexIndexArray[i];
|
|
if (idx != -1) {
|
|
uint texelCode = texelCodes[idx];
|
|
uint x = decodeMorton2X(texelCode);
|
|
uint y = decodeMorton2Y(texelCode);
|
|
|
|
vertex->tex.x = float(x);
|
|
vertex->tex.y = float(y);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
static void getBoundaryEdges(HalfEdge::Mesh * mesh, Array<HalfEdge::Edge *> & boundaryEdges)
|
|
{
|
|
nvDebugCheck(mesh != NULL);
|
|
|
|
const uint edgeCount = mesh->edgeCount();
|
|
|
|
BitArray bitFlags(edgeCount);
|
|
bitFlags.clearAll();
|
|
|
|
boundaryEdges.clear();
|
|
|
|
// Search for boundary edges. Mark all the edges that belong to the same boundary.
|
|
for (uint e = 0; e < edgeCount; e++)
|
|
{
|
|
HalfEdge::Edge * startEdge = mesh->edgeAt(e);
|
|
|
|
if (startEdge != NULL && startEdge->isBoundary() && bitFlags.bitAt(e) == false)
|
|
{
|
|
nvDebugCheck(startEdge->face != NULL);
|
|
nvDebugCheck(startEdge->pair->face == NULL);
|
|
|
|
startEdge = startEdge->pair;
|
|
|
|
const HalfEdge::Edge * edge = startEdge;
|
|
do {
|
|
nvDebugCheck(edge->face == NULL);
|
|
nvDebugCheck(bitFlags.bitAt(edge->id/2) == false);
|
|
|
|
bitFlags.setBitAt(edge->id / 2);
|
|
edge = edge->next;
|
|
} while(startEdge != edge);
|
|
|
|
boundaryEdges.append(startEdge);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool Chart::closeLoop(uint start, const Array<HalfEdge::Edge *> & loop)
|
|
{
|
|
const uint vertexCount = loop.count() - start;
|
|
|
|
nvDebugCheck(vertexCount >= 3);
|
|
if (vertexCount < 3) return false;
|
|
|
|
nvDebugCheck(loop[start]->vertex->isColocal(loop[start+vertexCount-1]->to()));
|
|
|
|
// If the hole is planar, then we add a single face that will be properly triangulated later.
|
|
// If the hole is not planar, we add a triangle fan with a vertex at the hole centroid.
|
|
// This is still a bit of a hack. There surely are better hole filling algorithms out there.
|
|
|
|
Array<Vector3> points;
|
|
points.resize(vertexCount);
|
|
for (uint i = 0; i < vertexCount; i++) {
|
|
points[i] = loop[start+i]->vertex->pos;
|
|
}
|
|
|
|
bool isPlanar = Fit::isPlanar(vertexCount, points.buffer());
|
|
|
|
if (isPlanar) {
|
|
// Add face and connect edges.
|
|
HalfEdge::Face * face = m_unifiedMesh->addFace();
|
|
for (uint i = 0; i < vertexCount; i++) {
|
|
HalfEdge::Edge * edge = loop[start + i];
|
|
|
|
edge->face = face;
|
|
edge->setNext(loop[start + (i + 1) % vertexCount]);
|
|
}
|
|
face->edge = loop[start];
|
|
|
|
nvDebugCheck(face->isValid());
|
|
}
|
|
else {
|
|
// If the polygon is not planar, we just cross our fingers, and hope this will work:
|
|
|
|
// Compute boundary centroid:
|
|
Vector3 centroidPos(0);
|
|
|
|
for (uint i = 0; i < vertexCount; i++) {
|
|
centroidPos += points[i];
|
|
}
|
|
|
|
centroidPos *= (1.0f / vertexCount);
|
|
|
|
HalfEdge::Vertex * centroid = m_unifiedMesh->addVertex(centroidPos);
|
|
|
|
// Add one pair of edges for each boundary vertex.
|
|
for (uint j = vertexCount-1, i = 0; i < vertexCount; j = i++) {
|
|
HalfEdge::Face * face = m_unifiedMesh->addFace(centroid->id, loop[start+j]->vertex->id, loop[start+i]->vertex->id);
|
|
nvDebugCheck(face != NULL);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool Chart::closeHoles()
|
|
{
|
|
nvDebugCheck(!m_isVertexMapped);
|
|
|
|
Array<HalfEdge::Edge *> boundaryEdges;
|
|
getBoundaryEdges(m_unifiedMesh.ptr(), boundaryEdges);
|
|
|
|
uint boundaryCount = boundaryEdges.count();
|
|
if (boundaryCount <= 1)
|
|
{
|
|
// Nothing to close.
|
|
return true;
|
|
}
|
|
|
|
// Compute lengths and areas.
|
|
Array<float> boundaryLengths;
|
|
//Array<Vector3> boundaryCentroids;
|
|
|
|
for (uint i = 0; i < boundaryCount; i++)
|
|
{
|
|
const HalfEdge::Edge * startEdge = boundaryEdges[i];
|
|
nvCheck(startEdge->face == NULL);
|
|
|
|
//float boundaryEdgeCount = 0;
|
|
float boundaryLength = 0.0f;
|
|
//Vector3 boundaryCentroid(zero);
|
|
|
|
const HalfEdge::Edge * edge = startEdge;
|
|
do {
|
|
Vector3 t0 = edge->from()->pos;
|
|
Vector3 t1 = edge->to()->pos;
|
|
|
|
//boundaryEdgeCount++;
|
|
boundaryLength += length(t1 - t0);
|
|
//boundaryCentroid += edge->vertex()->pos;
|
|
|
|
edge = edge->next;
|
|
} while(edge != startEdge);
|
|
|
|
boundaryLengths.append(boundaryLength);
|
|
//boundaryCentroids.append(boundaryCentroid / boundaryEdgeCount);
|
|
}
|
|
|
|
|
|
// Find disk boundary.
|
|
uint diskBoundary = 0;
|
|
float maxLength = boundaryLengths[0];
|
|
|
|
for (uint i = 1; i < boundaryCount; i++)
|
|
{
|
|
if (boundaryLengths[i] > maxLength)
|
|
{
|
|
maxLength = boundaryLengths[i];
|
|
diskBoundary = i;
|
|
}
|
|
}
|
|
|
|
|
|
// Sew holes.
|
|
/*for (uint i = 0; i < boundaryCount; i++)
|
|
{
|
|
if (diskBoundary == i)
|
|
{
|
|
// Skip disk boundary.
|
|
continue;
|
|
}
|
|
|
|
HalfEdge::Edge * startEdge = boundaryEdges[i];
|
|
nvCheck(startEdge->face() == NULL);
|
|
|
|
boundaryEdges[i] = m_unifiedMesh->sewBoundary(startEdge);
|
|
}
|
|
|
|
exportMesh(m_unifiedMesh.ptr(), "debug_sewn.obj");*/
|
|
|
|
//bool hasNewHoles = false;
|
|
|
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
// @@ Close loop is wrong, after closing a loop, we do not only have to add the face, but make sure that every edge in he loop is pointing to the right place.
|
|
|
|
// Close holes.
|
|
for (uint i = 0; i < boundaryCount; i++)
|
|
{
|
|
if (diskBoundary == i)
|
|
{
|
|
// Skip disk boundary.
|
|
continue;
|
|
}
|
|
|
|
HalfEdge::Edge * startEdge = boundaryEdges[i];
|
|
nvDebugCheck(startEdge != NULL);
|
|
nvDebugCheck(startEdge->face == NULL);
|
|
|
|
#if 1
|
|
Array<HalfEdge::Vertex *> vertexLoop;
|
|
Array<HalfEdge::Edge *> edgeLoop;
|
|
|
|
HalfEdge::Edge * edge = startEdge;
|
|
do {
|
|
HalfEdge::Vertex * vertex = edge->next->vertex; // edge->to()
|
|
|
|
uint i;
|
|
for (i = 0; i < vertexLoop.count(); i++) {
|
|
if (vertex->isColocal(vertexLoop[i])) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool isCrossing = (i != vertexLoop.count());
|
|
|
|
if (isCrossing) {
|
|
|
|
HalfEdge::Edge * prev = edgeLoop[i]; // Previous edge before the loop.
|
|
HalfEdge::Edge * next = edge->next; // Next edge after the loop.
|
|
|
|
nvDebugCheck(prev->to()->isColocal(next->from()));
|
|
|
|
// Close loop.
|
|
edgeLoop.append(edge);
|
|
closeLoop(i+1, edgeLoop);
|
|
|
|
// Link boundary loop.
|
|
prev->setNext(next);
|
|
vertex->setEdge(next);
|
|
|
|
// Start over again.
|
|
vertexLoop.clear();
|
|
edgeLoop.clear();
|
|
|
|
edge = startEdge;
|
|
vertex = edge->to();
|
|
}
|
|
|
|
vertexLoop.append(vertex);
|
|
edgeLoop.append(edge);
|
|
|
|
edge = edge->next;
|
|
} while(edge != startEdge);
|
|
|
|
closeLoop(0, edgeLoop);
|
|
#endif
|
|
|
|
/*
|
|
|
|
// Add face and connect boundary edges.
|
|
HalfEdge::Face * face = m_unifiedMesh->addFace();
|
|
face->setEdge(startEdge);
|
|
|
|
HalfEdge::Edge * edge = startEdge;
|
|
do {
|
|
edge->setFace(face);
|
|
|
|
edge = edge->next();
|
|
} while(edge != startEdge);
|
|
|
|
*/
|
|
|
|
|
|
/*
|
|
uint edgeCount = 0;
|
|
HalfEdge::Edge * edge = startEdge;
|
|
do {
|
|
edgeCount++;
|
|
edge = edge->next();
|
|
} while(edge != startEdge);
|
|
|
|
|
|
|
|
// Count edges in this boundary.
|
|
uint edgeCount = 0;
|
|
HalfEdge::Edge * edge = startEdge;
|
|
do {
|
|
edgeCount++;
|
|
edge = edge->next();
|
|
} while(edge != startEdge);
|
|
|
|
// Trivial hole, fill with one triangle. This actually works for all convex boundaries with non colinear vertices.
|
|
if (edgeCount == 3) {
|
|
// Add face and connect boundary edges.
|
|
HalfEdge::Face * face = m_unifiedMesh->addFace();
|
|
face->setEdge(startEdge);
|
|
|
|
edge = startEdge;
|
|
do {
|
|
edge->setFace(face);
|
|
|
|
edge = edge->next();
|
|
} while(edge != startEdge);
|
|
|
|
// @@ Implement the above using addFace, it should now work with existing edges, as long as their face pointers is zero.
|
|
|
|
}
|
|
else {
|
|
// Ideally we should:
|
|
// - compute best fit plane of boundary vertices.
|
|
// - project boundary polygon onto plane.
|
|
// - triangulate boundary polygon.
|
|
// - add faces of the resulting triangulation.
|
|
|
|
// I don't have a good triangulator available. A more simple solution that works in more (but not all) cases:
|
|
// - compute boundary centroid.
|
|
// - add vertex centroid.
|
|
// - connect centroid vertex with boundary vertices.
|
|
// - connect radial edges with boundary edges.
|
|
|
|
// This should work for non-convex boundaries with colinear vertices as long as the kernel of the polygon is not empty.
|
|
|
|
// Compute boundary centroid:
|
|
Vector3 centroid_pos(0);
|
|
Vector2 centroid_tex(0);
|
|
|
|
HalfEdge::Edge * edge = startEdge;
|
|
do {
|
|
centroid_pos += edge->vertex()->pos;
|
|
centroid_tex += edge->vertex()->tex;
|
|
edge = edge->next();
|
|
} while(edge != startEdge);
|
|
|
|
centroid_pos *= (1.0f / edgeCount);
|
|
centroid_tex *= (1.0f / edgeCount);
|
|
|
|
HalfEdge::Vertex * centroid = m_unifiedMesh->addVertex(centroid_pos);
|
|
centroid->tex = centroid_tex;
|
|
|
|
// Add one pair of edges for each boundary vertex.
|
|
edge = startEdge;
|
|
do {
|
|
HalfEdge::Edge * next = edge->next();
|
|
|
|
nvCheck(edge->face() == NULL);
|
|
HalfEdge::Face * face = m_unifiedMesh->addFace(centroid->id(), edge->from()->id(), edge->to()->id());
|
|
|
|
if (face != NULL) {
|
|
nvCheck(edge->face() == face);
|
|
}
|
|
else {
|
|
hasNewHoles = true;
|
|
}
|
|
|
|
edge = next;
|
|
} while(edge != startEdge);
|
|
}
|
|
*/
|
|
}
|
|
|
|
/*nvDebugCheck(!hasNewHoles);
|
|
|
|
if (hasNewHoles) {
|
|
// Link boundary again, in case closeHoles created new holes!
|
|
m_unifiedMesh->linkBoundary();
|
|
}*/
|
|
|
|
// Because some algorithms do not expect sparse edge buffers.
|
|
//m_unifiedMesh->compactEdges();
|
|
|
|
// In case we messed up:
|
|
//m_unifiedMesh->linkBoundary();
|
|
|
|
getBoundaryEdges(m_unifiedMesh.ptr(), boundaryEdges);
|
|
|
|
boundaryCount = boundaryEdges.count();
|
|
nvDebugCheck(boundaryCount == 1);
|
|
|
|
//exportMesh(m_unifiedMesh.ptr(), "debug_hole_filled.obj");
|
|
|
|
return boundaryCount == 1;
|
|
}
|
|
|
|
|
|
// Transfer parameterization from unified mesh to chart mesh.
|
|
void Chart::transferParameterization() {
|
|
nvDebugCheck(!m_isVertexMapped);
|
|
|
|
uint vertexCount = m_chartMesh->vertexCount();
|
|
for (uint v = 0; v < vertexCount; v++) {
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(v);
|
|
HalfEdge::Vertex * unifiedVertex = m_unifiedMesh->vertexAt(mapChartVertexToUnifiedVertex(v));
|
|
vertex->tex = unifiedVertex->tex;
|
|
}
|
|
}
|
|
|
|
float Chart::computeSurfaceArea() const {
|
|
return nv::computeSurfaceArea(m_chartMesh.ptr()) * scale;
|
|
}
|
|
|
|
float Chart::computeParametricArea() const {
|
|
// This only makes sense in parameterized meshes.
|
|
nvDebugCheck(m_isDisk);
|
|
nvDebugCheck(!m_isVertexMapped);
|
|
|
|
return nv::computeParametricArea(m_chartMesh.ptr());
|
|
}
|
|
|
|
Vector2 Chart::computeParametricBounds() const {
|
|
// This only makes sense in parameterized meshes.
|
|
nvDebugCheck(m_isDisk);
|
|
nvDebugCheck(!m_isVertexMapped);
|
|
|
|
Box bounds;
|
|
bounds.clearBounds();
|
|
|
|
uint vertexCount = m_chartMesh->vertexCount();
|
|
for (uint v = 0; v < vertexCount; v++) {
|
|
HalfEdge::Vertex * vertex = m_chartMesh->vertexAt(v);
|
|
bounds.addPointToBounds(Vector3(vertex->tex, 0));
|
|
}
|
|
|
|
return bounds.extents().xy();
|
|
}
|