如何将大型四边形基元数组转换为三角形基元?

How to convert large arrays of quad primitives to triangle primitives?

我有一个现有的系统,它提供 3D 网格。提供的数据是一个顶点坐标数组,包含 3 个分量(x、y、z)和一个索引列表。 问题是索引列表是一个连续的四边形基元数组。
系统必须首先使用核心配置文件 OpenGL Context 运行,然后使用 OpenGL ES 3.x。

我知道所有四边形都有相同的绕组顺序(逆时针方向),但我没有关于四边形的更多信息。我对他们的关系或邻接关系一无所知。

因为我想使用核心配置文件 Context 进行渲染,所以我不能使用 GL_QUAD 原始类型。我必须将四边形转换为三角形。

当然,四边形索引数组可以轻松转换为三角形索引数组:

std::vector<unsigned int> triangles;
triangles.reserve( no_of_indices * 6 / 4 );
for ( int i = 0; i < no_of_indices; i += 4 )
{
    int tri[] = { quad[i], quad[i+1], quad[i+2], quad[i], quad[i+2], quad[i+3] };
    triangles.insert(triangles.end(), tri, tri+6 );
}

如果只需要做一次,那就是解决方案。但是网格数据不是静态的。数据可以动态变化。 数据不是每次都连续变化的,而是不可预知的随机变化的。

另一个简单的解决方案是创建一个顶点数组对象,它直接引用带有四边形的元素数组缓冲区,并使用 GL_TRIANGLE_FAN 原始类型在循环中绘制它们:

for ( int i = 0; i < no_of_indices; i += 4 )
    glDrawElements( GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, (void*)(sizeof(unsigned int) * 4) );

但我希望有更好的解决方案。我正在寻找通过一次绘制调用绘制四边形或在 GPU 上将四边形转换为三角形的可能性。

首先我想指出,这不是一个我想自己回答的问题,但我想提供我目前对此问题的解决方案。 这意味着,我仍在寻找 "the" 解决方案,这是完全可以接受的解决方案。

在我的解决方案中,我决定使用Tessellation。我绘制大小为 4 的补丁:

glPatchParameteri( GL_PATCH_VERTICES, self.__patch_vertices )
glDrawElements( GL_PATCHES, no_of_indices, GL_UNSIGNED_INT, 0 )

Tessellation Control Shader 具有默认行为。补丁数据直接从顶点着色器调用传递到曲面细分图元生成。因此,它可以完全省略。

Tessellation Evaluation Shader 使用四边形补丁 (quads) 创建 2 个三角形:

#version 450

layout(quads, ccw) in;

in TInOut
{
    vec3 pos;
} inData[];

out TInOut
{
    vec3 pos;
} outData;

uniform mat4 u_projectionMat44;

void main()
{
    const int inx_map[4] = int[4](0, 1, 3, 2);

    float i_quad = dot( vec2(1.0, 2.0), gl_TessCoord.xy );
    int   inx    = inx_map[int(round(i_quad))];

    outData.pos = inData[inx].pos;
    gl_Position = u_projectionMat44 * vec4( outData.pos, 1.0 );
}

另一种解决方案是使用 Geometry Shader。输入图元类型lines_adjacency提供了4个顶点,可以映射到2个三角形(triangle_strip)。当然,这似乎是一个 hack,因为 lines adjacency 与四边形完全不同,但它仍然有效。

glDrawElements( GL_LINES_ADJACENCY, no_of_indices, GL_UNSIGNED_INT, 0 );

几何着色器:

#version 450

layout( lines_adjacency ) in;
layout( triangle_strip, max_vertices = 4 ) out;

in TInOut
{
    vec3 pos;
} inData[];

out TInOut
{
    vec3 col;
} outData;

uniform mat4 u_projectionMat44;

void main()
{
    const int inx_map[4] = int[4](0, 1, 3, 2);
    for ( int i=0; i < 4; ++i )
    {
        outData.pos = inData[inx_map[i]].pos;
        gl_Position = u_projectionMat44 * vec4( outData.pos, 1.0 );
        EmitVertex();
    }
    EndPrimitive();
}

一个改进是使用 Transform Feedback 来捕获包含三角形基元的新缓冲区。

If that has to be done only once, then that would be the solution. But the mesh data are not static.

网格数据可能是动态的,但该列表的拓扑是相同的。每4个顶点是一个四边形,所以每4个顶点代表三角形(0, 1, 2)和(0, 2, 3)。

所以你可以构建一个任意大的 static 索引缓冲区,其中包含这些数字的不断增加的序列 (0, 1, 2, 0, 2, 3, 4, 5, 6、4、6、7 等)。您甚至可以 use baseVertex rendering 偏移它们以使用相同的索引缓冲区渲染不同系列的四边形。

我的建议是使索引缓冲区使用 GLushort 作为索引类型。这样,您的索引数据每个四边形仅占用 12 个字节。使用短裤可以在单个绘图命令中限制 16384 个四边形,但您可以重复使用相同的索引缓冲区来绘制多个系列的四边形 baseVertex 渲染:

constexpr GLushort batchSize = 16384;
constexpr unsigned int vertsPerQuad = 6;
void drawQuads(GLuint quadCount)
{
  //Assume VAO is set up.
  int baseVertex = 0;
  while(quadCount > batchSize)
  {
    glDrawElementsBaseVertex(GL_TRIANGLES​, batchSize * vertsPerQuad, GL_UNSIGNED_SHORT, 0, baseVertex​ * 4);
    baseVertex += batchSize;
    quadCount -= batchSize;
  }
  glDrawElementsBaseVertex(GL_TRIANGLES​, quadCount * vertsPerQuad, GL_UNSIGNED_SHORT, 0, baseVertex​ * 4);
}

如果希望索引数据稍微少一些,可以使用primitive restart indices。这允许您指定一个索引来表示 "restart the primitive"。这允许您使用 GL_TRIANGLE_STRIP 图元并将图元分解成多个部分,同时仍然只有一个绘制调用。因此,每个四边形不是 6 个索引,而是 5 个,第 5 个是重新启动索引。所以现在你的 GLushort 索引每个四边形只占用 10 个字节。但是,batchSize 现在必须是 16383,因为索引 0xFFFF 是为重新启动保留的。 vertsPerQuad 必须是 5.

当然,baseVertex 渲染在图元重启时工作得很好,所以上面的代码也能工作。