【问题标题】:Identify unwrapped mesh UV regions识别展开的网格 UV 区域
【发布时间】:2016-01-28 22:09:39
【问题描述】:

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

现在我正在尝试找到合适的算法来使用顶点 UV 来唯一地识别这些区域,并使用这个唯一的 id 值存储一个属性。我们的想法是将此值用作颜色表的索引并获得类似的东西(手工制作):

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

谢谢。

更新: 用于渲染网格的数据是一个顶点列表 (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 坐标。

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

  • 一个顶点可以位于多个面上(多个三角形的一部分)。
  • 顶点将在 vertexToFace 数组中出现 n 次,它所属的每个面一次。
  • vertexToFace 数组中的第一个顶点将任意具有 regionId = 0。
  • 如果一个顶点共享相同的 x 和 y 坐标或该区域中另一个顶点的相同面,则该顶点属于该区域。

如果我理解得很好,这是实现非递归图遍历的正确信息。我需要迭代并保存已连接和未连接的顶点,所有连接的顶点都将成为当前区域的一部分,所有未连接的顶点都将再次检查,第一次迭代存储第一个三角形顶点,第二次存储所有顶点“接触”第一个三角形的三角形,继续直到迭代没有给出新的连接顶点(如果我们只检查最后一次迭代中添加的顶点列表,则优化这里),没有添加新顶点意味着是时候增加 regionId 和从第一个未连接的顶点重新开始。

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

【问题讨论】:

    标签: c++ opengl shader vertex uv-mapping


    【解决方案1】:

    现在我正在尝试找到合适的算法来使用顶点 UV 唯一标识这些区域并使用此唯一 id 值存储属性。

    创建图表

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

    创建一个向量:std::vector&lt;int&gt; vertexToFace;

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

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

    然后创建一个std::vector&lt;std::forward_list&lt;int&gt; &gt; 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&lt;std::array&lt;int,3&gt;&gt; faceToVertex;,这样,你可以从第 i 个顶点以恒定的时间访问它的邻居。无论哪种方式,我们都建立了一个图表,我们正在其中寻找connected components,使用depth-first search 很容易。

    实现连通分量算法

    要实现这一点,您必须创建另一个向量:std::vector&lt;bool&gt; visited(n,false);,以及另一个 std::vector&lt;int&gt; 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&lt;int&gt; 作为堆栈。将第一个顶点压入堆栈。然后弹出一个顶点,设置它的分量为k。将其所有邻居推入堆栈,然后删除邻居。换句话说,将邻居列表附加到堆栈(非常快的操作)。重复直到堆栈不为空。

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

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

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

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

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

    三角形或面的 id-s 在“索引列表”中,让我将此数组命名为 int listOfIndices[];,对于第 j 个面,连接到它的顶点是 listOfIndices[3*j + 0],@987654340 @ 和 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)。

    【讨论】:

    • 这并不能帮助他找到给定人脸的像素值。
    • @cmbasnett 您指的是“我对如何存储并将此值传递给着色器或其他任何东西毫无疑问,疑问是如何知道顶点所属的“区域”,或者最终,像素。”在我对给定顶点 id 的回答中,有一个组件 id,他已经知道如何将其传递给着色器。
    • 这似乎是我需要的,但是,我怎样才能获得第一个“面”值“vertexToFace[i]==j 表示第 i 个顶点在面 j 上。”?我认为,在第一次尝试中,我做错了获得脸部的紫外线比较。
    • @Gallo 我不知道你如何显示几何图形。例如,如果您使用 GL_TRIANGLES,并且没有索引缓冲区,那么每 3 个顶点形成(完全)一个三角形。在这种情况下,您有一个所有顶点Vertex *vertarr; 的数组,它进入您的顶点数组缓冲区(简称 VBO),并且在该数组中,第 k 个顶点位于面层(k/3)上。 (Vertex 是一些包含位置、法线、紫外线等的结构。)因此,第 m 个三角形由 arr[3*m]arr[3*m+1]arr[3*m+2] 顶点组成。
    • @Gallo 如果您使用有关网格的存储和显示模式的信息更新您的问题,我将相应地更新我的答案。 (如果你这样做,请告诉我:))
    【解决方案2】:

    好的,按照前面提到的几点并感谢 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++;
    }
    

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

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

    【讨论】:

      猜你喜欢
      • 2011-08-05
      • 2021-11-07
      • 2017-05-27
      • 1970-01-01
      • 2021-10-10
      • 1970-01-01
      • 1970-01-01
      • 2017-02-20
      • 1970-01-01
      相关资源
      最近更新 更多