Removed PlanarCharts and ClusteredCharts from xatlas.

This commit is contained in:
Relintai 2021-09-28 16:35:49 +02:00
parent a3c9009996
commit 62f5f8f269

View File

@ -4642,931 +4642,6 @@ static uint32_t s_planarRegionsCurrentRegion;
static uint32_t s_planarRegionsCurrentVertex; static uint32_t s_planarRegionsCurrentVertex;
#endif #endif
struct PlanarCharts {
PlanarCharts(AtlasData &data) :
m_data(data), m_nextRegionFace(MemTag::SegmentAtlasPlanarRegions), m_faceToRegionId(MemTag::SegmentAtlasPlanarRegions) {}
const Basis &chartBasis(uint32_t chartIndex) const { return m_chartBasis[chartIndex]; }
uint32_t chartCount() const { return m_charts.size(); }
ConstArrayView<uint32_t> chartFaces(uint32_t chartIndex) const {
const Chart &chart = m_charts[chartIndex];
return ConstArrayView<uint32_t>(&m_chartFaces[chart.firstFace], chart.faceCount);
}
uint32_t regionIdFromFace(uint32_t face) const { return m_faceToRegionId[face]; }
uint32_t nextRegionFace(uint32_t face) const { return m_nextRegionFace[face]; }
float regionArea(uint32_t region) const { return m_regionAreas[region]; }
void compute() {
const uint32_t faceCount = m_data.mesh->faceCount();
// Precompute regions of coplanar incident faces.
m_regionFirstFace.clear();
m_nextRegionFace.resize(faceCount);
m_faceToRegionId.resize(faceCount);
for (uint32_t f = 0; f < faceCount; f++) {
m_nextRegionFace[f] = f;
m_faceToRegionId[f] = UINT32_MAX;
}
Array<uint32_t> faceStack;
faceStack.reserve(min(faceCount, 16u));
uint32_t regionCount = 0;
for (uint32_t f = 0; f < faceCount; f++) {
if (m_nextRegionFace[f] != f)
continue; // Already assigned.
if (m_data.isFaceInChart.get(f))
continue; // Already in a chart.
faceStack.clear();
faceStack.push_back(f);
for (;;) {
if (faceStack.isEmpty())
break;
const uint32_t face = faceStack.back();
m_faceToRegionId[face] = regionCount;
faceStack.pop_back();
for (Mesh::FaceEdgeIterator it(m_data.mesh, face); !it.isDone(); it.advance()) {
const uint32_t oface = it.oppositeFace();
if (it.isBoundary())
continue;
if (m_nextRegionFace[oface] != oface)
continue; // Already assigned.
if (m_data.isFaceInChart.get(oface))
continue; // Already in a chart.
if (!equal(dot(m_data.faceNormals[face], m_data.faceNormals[oface]), 1.0f, kEpsilon))
continue; // Not coplanar.
const uint32_t next = m_nextRegionFace[face];
m_nextRegionFace[face] = oface;
m_nextRegionFace[oface] = next;
m_faceToRegionId[oface] = regionCount;
faceStack.push_back(oface);
}
}
m_regionFirstFace.push_back(f);
regionCount++;
}
#if XA_DEBUG_EXPORT_OBJ_PLANAR_REGIONS
static std::mutex s_mutex;
{
std::lock_guard<std::mutex> lock(s_mutex);
FILE *file;
XA_FOPEN(file, "debug_mesh_planar_regions.obj", s_planarRegionsCurrentRegion == 0 ? "w" : "a");
if (file) {
m_data.mesh->writeObjVertices(file);
fprintf(file, "s off\n");
for (uint32_t i = 0; i < regionCount; i++) {
fprintf(file, "o region%u\n", s_planarRegionsCurrentRegion);
for (uint32_t j = 0; j < faceCount; j++) {
if (m_faceToRegionId[j] == i)
m_data.mesh->writeObjFace(file, j, s_planarRegionsCurrentVertex);
}
s_planarRegionsCurrentRegion++;
}
s_planarRegionsCurrentVertex += m_data.mesh->vertexCount();
fclose(file);
}
}
#endif
// Precompute planar region areas.
m_regionAreas.resize(regionCount);
m_regionAreas.zeroOutMemory();
for (uint32_t f = 0; f < faceCount; f++) {
if (m_faceToRegionId[f] == UINT32_MAX)
continue;
m_regionAreas[m_faceToRegionId[f]] += m_data.faceAreas[f];
}
// Create charts from suitable planar regions.
// The dihedral angle of all boundary edges must be >= 90 degrees.
m_charts.clear();
m_chartFaces.clear();
for (uint32_t region = 0; region < regionCount; region++) {
const uint32_t firstRegionFace = m_regionFirstFace[region];
uint32_t face = firstRegionFace;
bool createChart = true;
do {
for (Mesh::FaceEdgeIterator it(m_data.mesh, face); !it.isDone(); it.advance()) {
if (it.isBoundary())
continue; // Ignore mesh boundary edges.
const uint32_t oface = it.oppositeFace();
if (m_faceToRegionId[oface] == region)
continue; // Ignore internal edges.
const float angle = m_data.edgeDihedralAngles[it.edge()];
if (angle > 0.0f && angle < FLT_MAX) { // FLT_MAX on boundaries.
createChart = false;
break;
}
}
if (!createChart)
break;
face = m_nextRegionFace[face];
} while (face != firstRegionFace);
// Create a chart.
if (createChart) {
Chart chart;
chart.firstFace = m_chartFaces.size();
chart.faceCount = 0;
face = firstRegionFace;
do {
m_data.isFaceInChart.set(face);
m_chartFaces.push_back(face);
chart.faceCount++;
face = m_nextRegionFace[face];
} while (face != firstRegionFace);
m_charts.push_back(chart);
}
}
// Compute basis for each chart using the first face normal (all faces have the same normal).
m_chartBasis.resize(m_charts.size());
for (uint32_t c = 0; c < m_charts.size(); c++) {
const uint32_t face = m_chartFaces[m_charts[c].firstFace];
Basis &basis = m_chartBasis[c];
basis.normal = m_data.faceNormals[face];
basis.tangent = Basis::computeTangent(basis.normal);
basis.bitangent = Basis::computeBitangent(basis.normal, basis.tangent);
}
}
private:
struct Chart {
uint32_t firstFace, faceCount;
};
AtlasData &m_data;
Array<uint32_t> m_regionFirstFace;
Array<uint32_t> m_nextRegionFace;
Array<uint32_t> m_faceToRegionId;
Array<float> m_regionAreas;
Array<Chart> m_charts;
Array<uint32_t> m_chartFaces;
Array<Basis> m_chartBasis;
};
struct ClusteredCharts {
ClusteredCharts(AtlasData &data, const PlanarCharts &planarCharts) :
m_data(data), m_planarCharts(planarCharts), m_texcoords(MemTag::SegmentAtlasMeshData), m_bestTriangles(10), m_placingSeeds(false) {}
~ClusteredCharts() {
const uint32_t chartCount = m_charts.size();
for (uint32_t i = 0; i < chartCount; i++) {
m_charts[i]->~Chart();
XA_FREE(m_charts[i]);
}
}
uint32_t chartCount() const { return m_charts.size(); }
ConstArrayView<uint32_t> chartFaces(uint32_t chartIndex) const { return m_charts[chartIndex]->faces; }
const Basis &chartBasis(uint32_t chartIndex) const { return m_charts[chartIndex]->basis; }
void compute() {
const uint32_t faceCount = m_data.mesh->faceCount();
m_facesLeft = 0;
for (uint32_t i = 0; i < faceCount; i++) {
if (!m_data.isFaceInChart.get(i))
m_facesLeft++;
}
const uint32_t chartCount = m_charts.size();
for (uint32_t i = 0; i < chartCount; i++) {
m_charts[i]->~Chart();
XA_FREE(m_charts[i]);
}
m_charts.clear();
m_faceCharts.resize(faceCount);
m_faceCharts.fill(-1);
m_texcoords.resize(faceCount * 3);
if (m_facesLeft == 0)
return;
// Create initial charts greedely.
placeSeeds(m_data.options.maxCost * 0.5f);
if (m_data.options.maxIterations == 0) {
XA_DEBUG_ASSERT(m_facesLeft == 0);
return;
}
relocateSeeds();
resetCharts();
// Restart process growing charts in parallel.
uint32_t iteration = 0;
for (;;) {
growCharts(m_data.options.maxCost);
// When charts cannot grow more: fill holes, merge charts, relocate seeds and start new iteration.
fillHoles(m_data.options.maxCost * 0.5f);
#if XA_MERGE_CHARTS
mergeCharts();
#endif
if (++iteration == m_data.options.maxIterations)
break;
if (!relocateSeeds())
break;
resetCharts();
}
// Make sure no holes are left!
XA_DEBUG_ASSERT(m_facesLeft == 0);
}
private:
struct Chart {
Chart() :
faces(MemTag::SegmentAtlasChartFaces) {}
int id = -1;
Basis basis; // Best fit normal.
float area = 0.0f;
float boundaryLength = 0.0f;
Vector3 centroidSum = Vector3(0.0f); // Sum of chart face centroids.
Vector3 centroid = Vector3(0.0f); // Average centroid of chart faces.
Array<uint32_t> faces;
Array<uint32_t> failedPlanarRegions;
CostQueue candidates;
uint32_t seed;
};
void placeSeeds(float threshold) {
XA_PROFILE_START(clusteredChartsPlaceSeeds)
m_placingSeeds = true;
// Instead of using a predefiened number of seeds:
// - Add seeds one by one, growing chart until a certain treshold.
// - Undo charts and restart growing process.
// @@ How can we give preference to faces far from sharp features as in the LSCM paper?
// - those points can be found using a simple flood filling algorithm.
// - how do we weight the probabilities?
while (m_facesLeft > 0)
createChart(threshold);
m_placingSeeds = false;
XA_PROFILE_END(clusteredChartsPlaceSeeds)
}
// Returns true if any of the charts can grow more.
void growCharts(float threshold) {
XA_PROFILE_START(clusteredChartsGrow)
for (;;) {
if (m_facesLeft == 0)
break;
// Get the single best candidate out of the chart best candidates.
uint32_t bestFace = UINT32_MAX, bestChart = UINT32_MAX;
float lowestCost = FLT_MAX;
for (uint32_t i = 0; i < m_charts.size(); i++) {
Chart *chart = m_charts[i];
// Get the best candidate from the chart.
// Cleanup any best candidates that have been claimed by another chart.
uint32_t face = UINT32_MAX;
float cost = FLT_MAX;
for (;;) {
if (chart->candidates.count() == 0)
break;
cost = chart->candidates.peekCost();
face = chart->candidates.peekFace();
if (!m_data.isFaceInChart.get(face))
break;
else {
// Face belongs to another chart. Pop from queue so the next best candidate can be retrieved.
chart->candidates.pop();
face = UINT32_MAX;
}
}
if (face == UINT32_MAX)
continue; // No candidates for this chart.
// See if best candidate overall.
if (cost < lowestCost) {
lowestCost = cost;
bestFace = face;
bestChart = i;
}
}
if (bestFace == UINT32_MAX || lowestCost > threshold)
break;
Chart *chart = m_charts[bestChart];
chart->candidates.pop(); // Pop the selected candidate from the queue.
if (!addFaceToChart(chart, bestFace))
chart->failedPlanarRegions.push_back(m_planarCharts.regionIdFromFace(bestFace));
}
XA_PROFILE_END(clusteredChartsGrow)
}
void resetCharts() {
XA_PROFILE_START(clusteredChartsReset)
const uint32_t faceCount = m_data.mesh->faceCount();
for (uint32_t i = 0; i < faceCount; i++) {
if (m_faceCharts[i] != -1)
m_data.isFaceInChart.unset(i);
m_faceCharts[i] = -1;
}
m_facesLeft = 0;
for (uint32_t i = 0; i < faceCount; i++) {
if (!m_data.isFaceInChart.get(i))
m_facesLeft++;
}
const uint32_t chartCount = m_charts.size();
for (uint32_t i = 0; i < chartCount; i++) {
Chart *chart = m_charts[i];
chart->area = 0.0f;
chart->boundaryLength = 0.0f;
chart->basis.normal = Vector3(0.0f);
chart->basis.tangent = Vector3(0.0f);
chart->basis.bitangent = Vector3(0.0f);
chart->centroidSum = Vector3(0.0f);
chart->centroid = Vector3(0.0f);
chart->faces.clear();
chart->candidates.clear();
chart->failedPlanarRegions.clear();
addFaceToChart(chart, chart->seed);
}
XA_PROFILE_END(clusteredChartsReset)
}
bool relocateSeeds() {
XA_PROFILE_START(clusteredChartsRelocateSeeds)
bool anySeedChanged = false;
const uint32_t chartCount = m_charts.size();
for (uint32_t i = 0; i < chartCount; i++) {
if (relocateSeed(m_charts[i])) {
anySeedChanged = true;
}
}
XA_PROFILE_END(clusteredChartsRelocateSeeds)
return anySeedChanged;
}
void fillHoles(float threshold) {
XA_PROFILE_START(clusteredChartsFillHoles)
while (m_facesLeft > 0)
createChart(threshold);
XA_PROFILE_END(clusteredChartsFillHoles)
}
#if XA_MERGE_CHARTS
void mergeCharts() {
XA_PROFILE_START(clusteredChartsMerge)
const uint32_t chartCount = m_charts.size();
// Merge charts progressively until there's none left to merge.
for (;;) {
bool merged = false;
for (int c = chartCount - 1; c >= 0; c--) {
Chart *chart = m_charts[c];
if (chart == nullptr)
continue;
float externalBoundaryLength = 0.0f;
m_sharedBoundaryLengths.resize(chartCount);
m_sharedBoundaryLengths.zeroOutMemory();
m_sharedBoundaryLengthsNoSeams.resize(chartCount);
m_sharedBoundaryLengthsNoSeams.zeroOutMemory();
m_sharedBoundaryEdgeCountNoSeams.resize(chartCount);
m_sharedBoundaryEdgeCountNoSeams.zeroOutMemory();
const uint32_t faceCount = chart->faces.size();
for (uint32_t i = 0; i < faceCount; i++) {
const uint32_t f = chart->faces[i];
for (Mesh::FaceEdgeIterator it(m_data.mesh, f); !it.isDone(); it.advance()) {
const float l = m_data.edgeLengths[it.edge()];
if (it.isBoundary()) {
externalBoundaryLength += l;
} else {
const int neighborChart = m_faceCharts[it.oppositeFace()];
if (neighborChart == -1)
externalBoundaryLength += l;
else if (m_charts[neighborChart] != chart) {
if ((it.isSeam() && (isNormalSeam(it.edge()) || it.isTextureSeam()))) {
externalBoundaryLength += l;
} else {
m_sharedBoundaryLengths[neighborChart] += l;
}
m_sharedBoundaryLengthsNoSeams[neighborChart] += l;
m_sharedBoundaryEdgeCountNoSeams[neighborChart]++;
}
}
}
}
for (int cc = chartCount - 1; cc >= 0; cc--) {
if (cc == c)
continue;
Chart *chart2 = m_charts[cc];
if (chart2 == nullptr)
continue;
// Must share a boundary.
if (m_sharedBoundaryLengths[cc] <= 0.0f)
continue;
// Compare proxies.
if (dot(chart2->basis.normal, chart->basis.normal) < XA_MERGE_CHARTS_MIN_NORMAL_DEVIATION)
continue;
// Obey max chart area and boundary length.
if (m_data.options.maxChartArea > 0.0f && chart->area + chart2->area > m_data.options.maxChartArea)
continue;
if (m_data.options.maxBoundaryLength > 0.0f && chart->boundaryLength + chart2->boundaryLength - m_sharedBoundaryLengthsNoSeams[cc] > m_data.options.maxBoundaryLength)
continue;
// Merge if chart2 has a single face.
// chart1 must have more than 1 face.
// chart2 area must be <= 10% of chart1 area.
if (m_sharedBoundaryLengthsNoSeams[cc] > 0.0f && chart->faces.size() > 1 && chart2->faces.size() == 1 && chart2->area <= chart->area * 0.1f)
goto merge;
// Merge if chart2 has two faces (probably a quad), and chart1 bounds at least 2 of its edges.
if (chart2->faces.size() == 2 && m_sharedBoundaryEdgeCountNoSeams[cc] >= 2)
goto merge;
// Merge if chart2 is wholely inside chart1, ignoring seams.
if (m_sharedBoundaryLengthsNoSeams[cc] > 0.0f && equal(m_sharedBoundaryLengthsNoSeams[cc], chart2->boundaryLength, kEpsilon))
goto merge;
if (m_sharedBoundaryLengths[cc] > 0.2f * max(0.0f, chart->boundaryLength - externalBoundaryLength) ||
m_sharedBoundaryLengths[cc] > 0.75f * chart2->boundaryLength)
goto merge;
continue;
merge:
if (!mergeChart(chart, chart2, m_sharedBoundaryLengthsNoSeams[cc]))
continue;
merged = true;
break;
}
if (merged)
break;
}
if (!merged)
break;
}
// Remove deleted charts.
for (int c = 0; c < int32_t(m_charts.size()); /*do not increment if removed*/) {
if (m_charts[c] == nullptr) {
m_charts.removeAt(c);
// Update m_faceCharts.
const uint32_t faceCount = m_faceCharts.size();
for (uint32_t i = 0; i < faceCount; i++) {
XA_DEBUG_ASSERT(m_faceCharts[i] != c);
XA_DEBUG_ASSERT(m_faceCharts[i] <= int32_t(m_charts.size()));
if (m_faceCharts[i] > c) {
m_faceCharts[i]--;
}
}
} else {
m_charts[c]->id = c;
c++;
}
}
XA_PROFILE_END(clusteredChartsMerge)
}
#endif
private:
void createChart(float threshold) {
Chart *chart = XA_NEW(MemTag::Default, Chart);
chart->id = (int)m_charts.size();
m_charts.push_back(chart);
// Pick a face not used by any chart yet, belonging to the largest planar region.
chart->seed = 0;
float largestArea = 0.0f;
for (uint32_t f = 0; f < m_data.mesh->faceCount(); f++) {
if (m_data.isFaceInChart.get(f))
continue;
const float area = m_planarCharts.regionArea(m_planarCharts.regionIdFromFace(f));
if (area > largestArea) {
largestArea = area;
chart->seed = f;
}
}
addFaceToChart(chart, chart->seed);
// Grow the chart as much as possible within the given threshold.
for (;;) {
if (chart->candidates.count() == 0 || chart->candidates.peekCost() > threshold)
break;
const uint32_t f = chart->candidates.pop();
if (m_data.isFaceInChart.get(f))
continue;
if (!addFaceToChart(chart, f)) {
chart->failedPlanarRegions.push_back(m_planarCharts.regionIdFromFace(f));
continue;
}
}
}
bool isChartBoundaryEdge(const Chart *chart, uint32_t edge) const {
const uint32_t oppositeEdge = m_data.mesh->oppositeEdge(edge);
const uint32_t oppositeFace = meshEdgeFace(oppositeEdge);
return oppositeEdge == UINT32_MAX || m_faceCharts[oppositeFace] != chart->id;
}
bool computeChartBasis(Chart *chart, Basis *basis) {
const uint32_t faceCount = chart->faces.size();
m_tempPoints.resize(chart->faces.size() * 3);
for (uint32_t i = 0; i < faceCount; i++) {
const uint32_t f = chart->faces[i];
for (uint32_t j = 0; j < 3; j++)
m_tempPoints[i * 3 + j] = m_data.mesh->position(m_data.mesh->vertexAt(f * 3 + j));
}
return Fit::computeBasis(m_tempPoints, basis);
}
bool isFaceFlipped(uint32_t face) const {
const Vector2 &v1 = m_texcoords[face * 3 + 0];
const Vector2 &v2 = m_texcoords[face * 3 + 1];
const Vector2 &v3 = m_texcoords[face * 3 + 2];
const float parametricArea = ((v2.x - v1.x) * (v3.y - v1.y) - (v3.x - v1.x) * (v2.y - v1.y)) * 0.5f;
return parametricArea < 0.0f;
}
void parameterizeChart(const Chart *chart) {
const uint32_t faceCount = chart->faces.size();
for (uint32_t i = 0; i < faceCount; i++) {
const uint32_t face = chart->faces[i];
for (uint32_t j = 0; j < 3; j++) {
const uint32_t offset = face * 3 + j;
const Vector3 &pos = m_data.mesh->position(m_data.mesh->vertexAt(offset));
m_texcoords[offset] = Vector2(dot(chart->basis.tangent, pos), dot(chart->basis.bitangent, pos));
}
}
}
// m_faceCharts for the chart faces must be set to the chart ID. Needed to compute boundary edges.
bool isChartParameterizationValid(const Chart *chart) {
const uint32_t faceCount = chart->faces.size();
// Check for flipped faces in the parameterization. OK if all are flipped.
uint32_t flippedFaceCount = 0;
for (uint32_t i = 0; i < faceCount; i++) {
if (isFaceFlipped(chart->faces[i]))
flippedFaceCount++;
}
if (flippedFaceCount != 0 && flippedFaceCount != faceCount)
return false;
// Check for boundary intersection in the parameterization.
XA_PROFILE_START(clusteredChartsPlaceSeedsBoundaryIntersection)
XA_PROFILE_START(clusteredChartsGrowBoundaryIntersection)
m_boundaryGrid.reset(m_texcoords);
for (uint32_t i = 0; i < faceCount; i++) {
const uint32_t f = chart->faces[i];
for (uint32_t j = 0; j < 3; j++) {
const uint32_t edge = f * 3 + j;
if (isChartBoundaryEdge(chart, edge))
m_boundaryGrid.append(edge);
}
}
const bool intersection = m_boundaryGrid.intersect(m_data.mesh->epsilon());
#if XA_PROFILE
if (m_placingSeeds)
XA_PROFILE_END(clusteredChartsPlaceSeedsBoundaryIntersection)
else
XA_PROFILE_END(clusteredChartsGrowBoundaryIntersection)
#endif
if (intersection)
return false;
return true;
}
bool addFaceToChart(Chart *chart, uint32_t face) {
XA_DEBUG_ASSERT(!m_data.isFaceInChart.get(face));
const uint32_t oldFaceCount = chart->faces.size();
const bool firstFace = oldFaceCount == 0;
// Append the face and any coplanar connected faces to the chart faces array.
chart->faces.push_back(face);
uint32_t coplanarFace = m_planarCharts.nextRegionFace(face);
while (coplanarFace != face) {
XA_DEBUG_ASSERT(!m_data.isFaceInChart.get(coplanarFace));
chart->faces.push_back(coplanarFace);
coplanarFace = m_planarCharts.nextRegionFace(coplanarFace);
}
const uint32_t faceCount = chart->faces.size();
// Compute basis.
Basis basis;
if (firstFace) {
// Use the first face normal.
// Use any edge as the tangent vector.
basis.normal = m_data.faceNormals[face];
basis.tangent = normalize(m_data.mesh->position(m_data.mesh->vertexAt(face * 3 + 0)) - m_data.mesh->position(m_data.mesh->vertexAt(face * 3 + 1)));
basis.bitangent = cross(basis.normal, basis.tangent);
} else {
// Use best fit normal.
if (!computeChartBasis(chart, &basis)) {
chart->faces.resize(oldFaceCount);
return false;
}
if (dot(basis.normal, m_data.faceNormals[face]) < 0.0f) // Flip normal if oriented in the wrong direction.
basis.normal = -basis.normal;
}
if (!firstFace) {
// Compute orthogonal parameterization and check that it is valid.
parameterizeChart(chart);
for (uint32_t i = oldFaceCount; i < faceCount; i++)
m_faceCharts[chart->faces[i]] = chart->id;
if (!isChartParameterizationValid(chart)) {
for (uint32_t i = oldFaceCount; i < faceCount; i++)
m_faceCharts[chart->faces[i]] = -1;
chart->faces.resize(oldFaceCount);
return false;
}
}
// Add face(s) to chart.
chart->basis = basis;
chart->area = computeArea(chart, face);
chart->boundaryLength = computeBoundaryLength(chart, face);
for (uint32_t i = oldFaceCount; i < faceCount; i++) {
const uint32_t f = chart->faces[i];
m_faceCharts[f] = chart->id;
m_facesLeft--;
m_data.isFaceInChart.set(f);
chart->centroidSum += m_data.mesh->computeFaceCenter(f);
}
chart->centroid = chart->centroidSum / float(chart->faces.size());
// Refresh candidates.
chart->candidates.clear();
for (uint32_t i = 0; i < faceCount; i++) {
// Traverse neighboring faces, add the ones that do not belong to any chart yet.
const uint32_t f = chart->faces[i];
for (uint32_t j = 0; j < 3; j++) {
const uint32_t edge = f * 3 + j;
const uint32_t oedge = m_data.mesh->oppositeEdge(edge);
if (oedge == UINT32_MAX)
continue; // Boundary edge.
const uint32_t oface = meshEdgeFace(oedge);
if (m_data.isFaceInChart.get(oface))
continue; // Face belongs to another chart.
if (chart->failedPlanarRegions.contains(m_planarCharts.regionIdFromFace(oface)))
continue; // Failed to add this faces planar region to the chart before.
const float cost = computeCost(chart, oface);
if (cost < FLT_MAX)
chart->candidates.push(cost, oface);
}
}
return true;
}
// Returns true if the seed has changed.
bool relocateSeed(Chart *chart) {
// Find the first N triangles that fit the proxy best.
const uint32_t faceCount = chart->faces.size();
m_bestTriangles.clear();
for (uint32_t i = 0; i < faceCount; i++) {
const float cost = computeNormalDeviationMetric(chart, chart->faces[i]);
m_bestTriangles.push(cost, chart->faces[i]);
}
// Of those, choose the most central triangle.
uint32_t mostCentral = 0;
float minDistance = FLT_MAX;
for (;;) {
if (m_bestTriangles.count() == 0)
break;
const uint32_t face = m_bestTriangles.pop();
Vector3 faceCentroid = m_data.mesh->computeFaceCenter(face);
const float distance = length(chart->centroid - faceCentroid);
if (distance < minDistance) {
minDistance = distance;
mostCentral = face;
}
}
XA_DEBUG_ASSERT(minDistance < FLT_MAX);
if (mostCentral == chart->seed)
return false;
chart->seed = mostCentral;
return true;
}
// Cost is combined metrics * weights.
float computeCost(Chart *chart, uint32_t face) const {
// Estimate boundary length and area:
const float newChartArea = computeArea(chart, face);
const float newBoundaryLength = computeBoundaryLength(chart, face);
// Enforce limits strictly:
if (m_data.options.maxChartArea > 0.0f && newChartArea > m_data.options.maxChartArea)
return FLT_MAX;
if (m_data.options.maxBoundaryLength > 0.0f && newBoundaryLength > m_data.options.maxBoundaryLength)
return FLT_MAX;
// Compute metrics.
float cost = 0.0f;
const float normalDeviation = computeNormalDeviationMetric(chart, face);
if (normalDeviation >= 0.707f) // ~75 degrees
return FLT_MAX;
cost += m_data.options.normalDeviationWeight * normalDeviation;
// Penalize faces that cross seams, reward faces that close seams or reach boundaries.
// Make sure normal seams are fully respected:
const float normalSeam = computeNormalSeamMetric(chart, face);
if (m_data.options.normalSeamWeight >= 1000.0f && normalSeam > 0.0f)
return FLT_MAX;
cost += m_data.options.normalSeamWeight * normalSeam;
cost += m_data.options.roundnessWeight * computeRoundnessMetric(chart, newBoundaryLength, newChartArea);
cost += m_data.options.straightnessWeight * computeStraightnessMetric(chart, face);
cost += m_data.options.textureSeamWeight * computeTextureSeamMetric(chart, face);
//float R = evaluateCompletenessMetric(chart, face);
//float D = evaluateDihedralAngleMetric(chart, face);
// @@ Add a metric based on local dihedral angle.
// @@ Tweaking the normal and texture seam metrics.
// - Cause more impedance. Never cross 90 degree edges.
XA_DEBUG_ASSERT(isFinite(cost));
return cost;
}
// Returns a value in [0-1].
// 0 if face normal is coplanar to the chart's best fit normal.
// 1 if face normal is perpendicular.
float computeNormalDeviationMetric(Chart *chart, uint32_t face) const {
// All faces in coplanar regions have the same normal, can use any face.
const Vector3 faceNormal = m_data.faceNormals[face];
// Use plane fitting metric for now:
return min(1.0f - dot(faceNormal, chart->basis.normal), 1.0f); // @@ normal deviations should be weighted by face area
}
float computeRoundnessMetric(Chart *chart, float newBoundaryLength, float newChartArea) const {
const float oldRoundness = square(chart->boundaryLength) / chart->area;
const float newRoundness = square(newBoundaryLength) / newChartArea;
return 1.0f - oldRoundness / newRoundness;
}
float computeStraightnessMetric(Chart *chart, uint32_t firstFace) const {
float l_out = 0.0f; // Length of firstFace planar region boundary that doesn't border the chart.
float l_in = 0.0f; // Length that does border the chart.
const uint32_t planarRegionId = m_planarCharts.regionIdFromFace(firstFace);
uint32_t face = firstFace;
for (;;) {
for (Mesh::FaceEdgeIterator it(m_data.mesh, face); !it.isDone(); it.advance()) {
const float l = m_data.edgeLengths[it.edge()];
if (it.isBoundary()) {
l_out += l;
} else if (m_planarCharts.regionIdFromFace(it.oppositeFace()) != planarRegionId) {
if (m_faceCharts[it.oppositeFace()] != chart->id)
l_out += l;
else
l_in += l;
}
}
face = m_planarCharts.nextRegionFace(face);
if (face == firstFace)
break;
}
#if 1
float ratio = (l_out - l_in) / (l_out + l_in);
return min(ratio, 0.0f); // Only use the straightness metric to close gaps.
#else
return 1.0f - l_in / l_out;
#endif
}
bool isNormalSeam(uint32_t edge) const {
const uint32_t oppositeEdge = m_data.mesh->oppositeEdge(edge);
if (oppositeEdge == UINT32_MAX)
return false; // boundary edge
if (m_data.mesh->flags() & MeshFlags::HasNormals) {
const uint32_t v0 = m_data.mesh->vertexAt(meshEdgeIndex0(edge));
const uint32_t v1 = m_data.mesh->vertexAt(meshEdgeIndex1(edge));
const uint32_t ov0 = m_data.mesh->vertexAt(meshEdgeIndex0(oppositeEdge));
const uint32_t ov1 = m_data.mesh->vertexAt(meshEdgeIndex1(oppositeEdge));
if (v0 == ov1 && v1 == ov0)
return false;
return !equal(m_data.mesh->normal(v0), m_data.mesh->normal(ov1), kNormalEpsilon) || !equal(m_data.mesh->normal(v1), m_data.mesh->normal(ov0), kNormalEpsilon);
}
const uint32_t f0 = meshEdgeFace(edge);
const uint32_t f1 = meshEdgeFace(oppositeEdge);
if (m_planarCharts.regionIdFromFace(f0) == m_planarCharts.regionIdFromFace(f1))
return false;
return !equal(m_data.faceNormals[f0], m_data.faceNormals[f1], kNormalEpsilon);
}
float computeNormalSeamMetric(Chart *chart, uint32_t firstFace) const {
float seamFactor = 0.0f, totalLength = 0.0f;
uint32_t face = firstFace;
for (;;) {
for (Mesh::FaceEdgeIterator it(m_data.mesh, face); !it.isDone(); it.advance()) {
if (it.isBoundary())
continue;
if (m_faceCharts[it.oppositeFace()] != chart->id)
continue;
float l = m_data.edgeLengths[it.edge()];
totalLength += l;
if (!it.isSeam())
continue;
// Make sure it's a normal seam.
if (isNormalSeam(it.edge())) {
float d;
if (m_data.mesh->flags() & MeshFlags::HasNormals) {
const Vector3 &n0 = m_data.mesh->normal(it.vertex0());
const Vector3 &n1 = m_data.mesh->normal(it.vertex1());
const Vector3 &on0 = m_data.mesh->normal(m_data.mesh->vertexAt(meshEdgeIndex0(it.oppositeEdge())));
const Vector3 &on1 = m_data.mesh->normal(m_data.mesh->vertexAt(meshEdgeIndex1(it.oppositeEdge())));
const float d0 = clamp(dot(n0, on1), 0.0f, 1.0f);
const float d1 = clamp(dot(n1, on0), 0.0f, 1.0f);
d = (d0 + d1) * 0.5f;
} else {
d = clamp(dot(m_data.faceNormals[face], m_data.faceNormals[meshEdgeFace(it.oppositeEdge())]), 0.0f, 1.0f);
}
l *= 1 - d;
seamFactor += l;
}
}
face = m_planarCharts.nextRegionFace(face);
if (face == firstFace)
break;
}
if (seamFactor <= 0.0f)
return 0.0f;
return seamFactor / totalLength;
}
float computeTextureSeamMetric(Chart *chart, uint32_t firstFace) const {
float seamLength = 0.0f, totalLength = 0.0f;
uint32_t face = firstFace;
for (;;) {
for (Mesh::FaceEdgeIterator it(m_data.mesh, face); !it.isDone(); it.advance()) {
if (it.isBoundary())
continue;
if (m_faceCharts[it.oppositeFace()] != chart->id)
continue;
float l = m_data.edgeLengths[it.edge()];
totalLength += l;
if (!it.isSeam())
continue;
// Make sure it's a texture seam.
if (it.isTextureSeam())
seamLength += l;
}
face = m_planarCharts.nextRegionFace(face);
if (face == firstFace)
break;
}
if (seamLength <= 0.0f)
return 0.0f; // Avoid division by zero.
return seamLength / totalLength;
}
float computeArea(Chart *chart, uint32_t firstFace) const {
float area = chart->area;
uint32_t face = firstFace;
for (;;) {
area += m_data.faceAreas[face];
face = m_planarCharts.nextRegionFace(face);
if (face == firstFace)
break;
}
return area;
}
float computeBoundaryLength(Chart *chart, uint32_t firstFace) const {
float boundaryLength = chart->boundaryLength;
// Add new edges, subtract edges shared with the chart.
const uint32_t planarRegionId = m_planarCharts.regionIdFromFace(firstFace);
uint32_t face = firstFace;
for (;;) {
for (Mesh::FaceEdgeIterator it(m_data.mesh, face); !it.isDone(); it.advance()) {
const float edgeLength = m_data.edgeLengths[it.edge()];
if (it.isBoundary()) {
boundaryLength += edgeLength;
} else if (m_planarCharts.regionIdFromFace(it.oppositeFace()) != planarRegionId) {
if (m_faceCharts[it.oppositeFace()] != chart->id)
boundaryLength += edgeLength;
else
boundaryLength -= edgeLength;
}
}
face = m_planarCharts.nextRegionFace(face);
if (face == firstFace)
break;
}
return max(0.0f, boundaryLength); // @@ Hack!
}
bool mergeChart(Chart *owner, Chart *chart, float sharedBoundaryLength) {
const uint32_t oldOwnerFaceCount = owner->faces.size();
const uint32_t chartFaceCount = chart->faces.size();
owner->faces.push_back(chart->faces);
for (uint32_t i = 0; i < chartFaceCount; i++) {
XA_DEBUG_ASSERT(m_faceCharts[chart->faces[i]] == chart->id);
m_faceCharts[chart->faces[i]] = owner->id;
}
// Compute basis using best fit normal.
Basis basis;
if (!computeChartBasis(owner, &basis)) {
owner->faces.resize(oldOwnerFaceCount);
for (uint32_t i = 0; i < chartFaceCount; i++)
m_faceCharts[chart->faces[i]] = chart->id;
return false;
}
if (dot(basis.normal, m_data.faceNormals[owner->faces[0]]) < 0.0f) // Flip normal if oriented in the wrong direction.
basis.normal = -basis.normal;
// Compute orthogonal parameterization and check that it is valid.
parameterizeChart(owner);
if (!isChartParameterizationValid(owner)) {
owner->faces.resize(oldOwnerFaceCount);
for (uint32_t i = 0; i < chartFaceCount; i++)
m_faceCharts[chart->faces[i]] = chart->id;
return false;
}
// Merge chart.
owner->basis = basis;
owner->failedPlanarRegions.push_back(chart->failedPlanarRegions);
// Update adjacencies?
owner->area += chart->area;
owner->boundaryLength += chart->boundaryLength - sharedBoundaryLength;
// Delete chart.
m_charts[chart->id] = nullptr;
chart->~Chart();
XA_FREE(chart);
return true;
}
private:
AtlasData &m_data;
const PlanarCharts &m_planarCharts;
Array<Vector2> m_texcoords;
uint32_t m_facesLeft;
Array<int> m_faceCharts;
Array<Chart *> m_charts;
CostQueue m_bestTriangles;
Array<Vector3> m_tempPoints;
UniformGrid2 m_boundaryGrid;
#if XA_MERGE_CHARTS
// mergeCharts
Array<float> m_sharedBoundaryLengths;
Array<float> m_sharedBoundaryLengthsNoSeams;
Array<uint32_t> m_sharedBoundaryEdgeCountNoSeams;
#endif
bool m_placingSeeds;
};
struct ChartGeneratorType { struct ChartGeneratorType {
enum Enum { enum Enum {
OriginalUv, OriginalUv,
@ -5578,39 +4653,22 @@ struct ChartGeneratorType {
struct Atlas { struct Atlas {
Atlas() : Atlas() :
m_originalUvCharts(m_data), m_planarCharts(m_data), m_clusteredCharts(m_data, m_planarCharts) {} m_originalUvCharts(m_data) {}
uint32_t chartCount() const { uint32_t chartCount() const {
return m_originalUvCharts.chartCount() + m_planarCharts.chartCount() + m_clusteredCharts.chartCount(); return m_originalUvCharts.chartCount();
} }
ConstArrayView<uint32_t> chartFaces(uint32_t chartIndex) const { ConstArrayView<uint32_t> chartFaces(uint32_t chartIndex) const {
if (chartIndex < m_originalUvCharts.chartCount()) return m_originalUvCharts.chartFaces(chartIndex);
return m_originalUvCharts.chartFaces(chartIndex);
chartIndex -= m_originalUvCharts.chartCount();
if (chartIndex < m_planarCharts.chartCount())
return m_planarCharts.chartFaces(chartIndex);
chartIndex -= m_planarCharts.chartCount();
return m_clusteredCharts.chartFaces(chartIndex);
} }
const Basis &chartBasis(uint32_t chartIndex) const { const Basis &chartBasis(uint32_t chartIndex) const {
if (chartIndex < m_originalUvCharts.chartCount()) return m_originalUvCharts.chartBasis(chartIndex);
return m_originalUvCharts.chartBasis(chartIndex);
chartIndex -= m_originalUvCharts.chartCount();
if (chartIndex < m_planarCharts.chartCount())
return m_planarCharts.chartBasis(chartIndex);
chartIndex -= m_planarCharts.chartCount();
return m_clusteredCharts.chartBasis(chartIndex);
} }
ChartGeneratorType::Enum chartGeneratorType(uint32_t chartIndex) const { ChartGeneratorType::Enum chartGeneratorType(uint32_t chartIndex) const {
if (chartIndex < m_originalUvCharts.chartCount())
return ChartGeneratorType::OriginalUv; return ChartGeneratorType::OriginalUv;
chartIndex -= m_originalUvCharts.chartCount();
if (chartIndex < m_planarCharts.chartCount())
return ChartGeneratorType::Planar;
return ChartGeneratorType::Clustered;
} }
void reset(const Mesh *mesh, const ChartOptions &options) { void reset(const Mesh *mesh, const ChartOptions &options) {
@ -5622,24 +4680,14 @@ struct Atlas {
} }
void compute() { void compute() {
if (m_data.options.useInputMeshUvs) {
XA_PROFILE_START(originalUvCharts) XA_PROFILE_START(originalUvCharts)
m_originalUvCharts.compute(); m_originalUvCharts.compute();
XA_PROFILE_END(originalUvCharts) XA_PROFILE_END(originalUvCharts)
}
XA_PROFILE_START(planarCharts)
m_planarCharts.compute();
XA_PROFILE_END(planarCharts)
XA_PROFILE_START(clusteredCharts)
m_clusteredCharts.compute();
XA_PROFILE_END(clusteredCharts)
} }
private: private:
AtlasData m_data; AtlasData m_data;
OriginalUvCharts m_originalUvCharts; OriginalUvCharts m_originalUvCharts;
PlanarCharts m_planarCharts;
ClusteredCharts m_clusteredCharts;
}; };
struct ComputeUvMeshChartsTaskArgs { struct ComputeUvMeshChartsTaskArgs {