识别展开的网格 UV 区域

Identify unwrapped mesh UV regions

假设我们有一个 3D 网格,每个顶点都有纹理坐标,所以如果我渲染它展开,我会得到这样的东西(忽略红色方块):

现在我正在尝试找到合适的算法来使用顶点 UV 唯一标识这些区域并存储具有此唯一 ID 值的属性。这个想法是使用这个值作为颜色的索引 table 并得到这样的东西(手工制作):

我尝试迭代每个顶点并找到 "unconnected" 个比较纹理坐标的三角形,但网格索引顺序似乎与 UV 的放置方式无关,或者我没有应用正确的公式。我毫不怀疑如何存储这个值并将其传递给着色器或其他任何东西,问题是如何知道顶点所属的"region",或者最终,像素。

谢谢。

更新: 用于渲染网格的数据是顶点列表 (GL_VERTEX_BUFFER) 加上索引列表 (GL_ELEMENT_ARRAY)。网格渲染为GL_TRIANGLES,每个顶点都是这样的结构:

struct Vertex
{
    float x, y, z;
    float nx, ny, nz;
    float tcx, tcy;
    float regionId; //the attribute I want to fill
};

struct MPUVRegionVertex
{
    float x, y;
    int faceId, regionId;
};

更新 2:我创建了一个新的 MPUVRegionVertex 顶点数组,每个索引都有一个元素(不是每个唯一顶点)。按照@CsabaBálint 的回复,我得到了这段代码:

MPUVRegionVertex* uvVertexData = new MPUVRegionVertex[indexCount];

for(int ic = 0; ic < indexCount / 3; ic++)
{
    for(int vc = 0; vc < 3; vc++)
    {
        uvVertexData[3*ic+vc].x = vertexData[indexData[3*ic+vc]].tcx;
        uvVertexData[3*ic+vc].y = vertexData[indexData[3*ic+vc]].tcy;
        uvVertexData[3*ic+vc].faceId = ic;
    }
}

std::vector<std::forward_list<int> > graph(indexCount);

for(int t1=0;t1 < indexCount; ++t1)
{
    for(int t2 = t1 + 1; t2 < indexCount; ++t2)
    {
        if (uvVertexData[t1].faceId == uvVertexData[t2].faceId)
        {
            graph[t1].push_front(t2);
            graph[t2].push_front(t1);
        }
    }
}

std::forward_list<int> stack;
std::vector<int> component(indexCount);
std::set<int> notvisited;

for(int nv = 0; nv < indexCount; nv++)
{
    notvisited.insert(nv);
}


int k = 0;
while(notvisited.size() > 0)
{
    stack.push_front(*notvisited.begin());
    notvisited.erase(notvisited.begin());

    while(!stack.empty())
    {
        //SOMETHING WRONG HERE
        int temp = stack.front();
        notvisited.erase(temp);
        stack.pop_front();
        component[temp] = k;
        stack.merge(graph[temp]);
        graph[temp].clear();
    }
    k++;
}

结果是每三个索引都有一个不同的 k,这意味着为每个新三角形调用 k++,所以我在算法中遗漏了一些东西:S。

component[0]=0
component[1]=0
component[2]=0
component[3]=1
component[4]=1
component[5]=1
component[6]=2
component[7]=2
component[8]=2
component[9]=3
...
component[1778]=592
component[1779]=593
component[1780]=593
component[1781]=593

关于网格的一些信息:

Size of shape[0].indices: 1782
shape[0].positions: 1242
shape[0].texcoords: 828
shape[0].normals: 1242

更新 3

有关详细信息,每个顶点只有一个 UV 坐标。

到目前为止的扣减/规则:

如果我没看错的话,这是实现非递归图遍历的正确信息。我需要迭代并保存连接和未连接的顶点,所有连接的顶点都将成为当前区域的一部分,所有不连接的顶点都将再次检查已连接的顶点,第一次迭代存储第一个三角形顶点,第二次存储所有顶点"touching" 第一个三角形的三角形,继续直到迭代没有给出新的连接顶点(如果我们只检查上次迭代中添加的顶点列表,则在此处进行优化),没有添加新顶点意味着是时候增加regionId 并从第一个未连接的顶点重新开始。

我将尝试按照该设计实现搜索。

Now I'm trying to find the proper algorithm to uniquely identify those regions using the vertex UVs and storing an attribute with this unique id value.

创建图表

为顶点和面创建 id(给它们编号)。但要确保相同的顶点获得相同的id-s,通过UV或位置比较它们。

创建向量:std::vector<int> vertexToFace;

vertexToFace[i]==j表示第i个顶点在j面上。

如果两个顶点在同一个面上,则它们是相邻的。

然后创建一个std::vector<std::forward_list<int> > graph;将顶点存储为向量索引,并添加邻居。 (O(n^2) 复杂度)

为此,您必须取第 i 个顶点,并且对于每 j 个顶点,您必须检查它们是否在同一面上。稍微优化的版本:

for(int i=0; i<n; ++i) for(int j=i+1; j <n ++j)
if (vertexToFace[i] == vertexToFace[j])
{
    graph[i].push_front(j);
    graph[j].push_front(i);
}

这是 O(n^2),但很容易实现。一个更难但更快的需要另一个向量:std::vector<std::array<int,3>> faceToVertex;,这样,从第 i 个顶点你可以在常数时间内访问它的邻居。无论哪种方式,我们都建立了一个图表,我们正在寻找 connected components, which is easy with depth-first search.

实现连通分量算法

要实现这一点,您必须制作另一个矢量:std::vector<bool> visited(n,false);,以及另一个 std::vector<int> component(n)。您的问题的解决方案将在最后一个中。

算法简单,从顶点0开始,设visited[0] = true;component[0]=0;。然后对每个未访问的邻居做完全相同的事情,所以对于邻居 i(forward_list 的某个元素)if (!visited[i]) component[i] = 0;,然后做同样的事情。当组件的所有元素都被访问时它停止。因此,您必须寻找一个未访问的元素,然后再次执行上述操作,但要知道您正在执行组件 1,依此类推。示例:

int l, k=0;
while(l!=n)
{
    l=0;
    while(visited[l]) l++;
    fill_from(graph, visited, component, l, k);
    ++k;
}

我想你明白了,所以:(伪代码)

void fill_from(graph, visited, component, l, k)
{
    if(visited[l]) return;
    component[l] = k;
    for(auto &i : graph[l])
            fill_from(graph,visited,component,i,k);
}

然后我们完成了任务,但这还不是最快的解决方案。

更快的算法

为了更快,我们必须摆脱递归,之后我们不需要图形,使用 std::forward_list<int> 作为堆栈。将第一个顶点推入堆栈。然后弹出一个顶点,将其组件设置为 k。将所有它的邻居压入堆栈,然后删除邻居。换句话说,将邻居列表附加到堆栈(非常快的操作)。重复直到堆栈不为空。

这样我们就不会无限循环了,因为如果我们回到同一个顶点,它就没有邻居了,而我们已经访问过它们了。因此不需要访问向量。我们可以多次设置分量向量元素,但总是设置为相同的值,所以为什么要检查它?

但是,如果我们没有访问过的向量,那么就很难找到我们没有访问过的另一个顶点。虽然我们可以在图中寻找一些仍然有邻居的顶点,但有一个更好的解决方案。

为那些还没有访问过的点创建std::set<int> notvisited();。起初它应该包含所有顶点 ID,然后每次我们设置组件 ID 时,我们都会尝试从 notvisited 集合中删除一个顶点。我们重复从集合中获取一个顶点和 运行 fill_from() 算法,直到集合变空,同时,我们拥有所有组件 id-s。

更新:使用有关如何存储网格的更新信息。

如果 "list of vertices" 中没有相等的元素(为什么会这样),那么顶点的索引就是它在数组中的位置。这样,顶点的id-s就完成了。

三角形或面的id-s,在"list of indices"中,让我把这个数组命名为int listOfIndices[];,对于第j个面,连接到它的顶点是listOfIndices[3*j + 0]listOfIndices[3*j + 1]listOfIndices[3*j + 2]。要制作第一个矢量,您必须执行以下操作:

std::vector<int> vertexToFace(num_of_verteces); //previously n
for(int j = 0; j < num_of_faces; ++j)
{
    vertexToFace[listOfIndices[3*j + 0]]=j;
    vertexToFace[listOfIndices[3*j + 1]]=j;
    vertexToFace[listOfIndices[3*j + 2]]=j;
}

(建立逆关系的算法);请注意,在这种情况下,您甚至不需要不同的 faceToVertex 数组,因为您已经拥有它 (listOfIndices),您只需要对其进行不同的索引(每次除以 3)。

好的,根据前面提到的几点,感谢 Csaba Bálint 的初步提示,我找到了解决方案:

MPUVRegionVertex* uvVertexData = new MPUVRegionVertex[indexCount];

for(int ic = 0; ic < indexCount / 3; ic++)
{
    for(int vc = 0; vc < 3; vc++)
    {
        uvVertexData[3*ic+vc].x = vertexData[indexData[3*ic+vc]].tcx;
        uvVertexData[3*ic+vc].y = vertexData[indexData[3*ic+vc]].tcy;
        uvVertexData[3*ic+vc].faceId = ic;
    }
}

std::set<int> notAssigned;

for(int nv = 0; nv < indexCount; nv++)
{
    notAssigned.insert(nv);
}

std::forward_list<int> addedInLastIterationElements;
int currentRegion = 0;

//while there are not assigned vertex 
while (notAssigned.size() > 0)
{
    //the first not assigned element simulate that was "added" to the current region in the last check
    addedInLastIterationElements.push_front(*notAssigned.begin());

    //this first has the new current region as it's regionId
    uvVertexData[*notAssigned.begin()].regionId = currentRegion;

    //and becomes assigned
    notAssigned.erase(notAssigned.begin());

    do
    {
        //will store the elements added in the next iteration
        std::forward_list<int> newRegionElements;

        //iterate not assigned elements
        for(int currentElement : notAssigned)
        {
            //iterate elements added to the current region in the last iteration
            for(int currentRegionElement : addedInLastIterationElements)
            {
                //compare if this vertex belongs to the same region of some of the recently added
                if((uvVertexData[currentElement].x == uvVertexData[currentRegionElement].x && 
                    uvVertexData[currentElement].y == uvVertexData[currentRegionElement].y) ||
                   (uvVertexData[currentElement].faceId == uvVertexData[currentRegionElement].faceId))
                {
                    //store as an element added this iteration
                    newRegionElements.push_front(currentElement);

                    //store the current region
                    uvVertexData[currentElement].regionId = currentRegion;
                }
            }
        }

        //remove new elements from the notAssigned list
        for(int assigned : newRegionElements)
        {
            notAssigned.erase(assigned);
        }

        //replace the elements added the previous iteration for the last iteration
        addedInLastIterationElements = newRegionElements;

        //prepare for new elements
        newRegionElements.clear();
    }
    //if there is no match in the last iteration, means all remaining vertex belongs to another region
    while(!addedInLastIterationElements.empty());

    //next region
    currentRegion++;
}

如果用 table 种颜色渲染生成的 regionId,我会得到想要的结果(请注意,颜色仅在每个通道 0.1 中变化,但这里有 10 种不同的颜色):

当然可以优化算法,但它在合理的时间和内存使用情况下执行,所以,现在还可以。