【问题标题】:How to convert large arrays of quad primitives to triangle primitives?如何将大型四边形基元数组转换为三角形基元?
【发布时间】:2018-08-14 16:52:41
【问题描述】:

我有一个提供 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 上将四边形转换为三角形。

【问题讨论】:

  • "由于我想使用顶点缓冲区和顶点数组对象进行渲染,所以我不能使用 GL_QUAD 原始类型。" 这是放弃 GL_QUADS 的核心配置文件。如果您愿意使用兼容性配置文件,那么您可以随心所欲地使用它们。

标签: opengl glsl shader triangulation


【解决方案1】:

如果必须只执行一次,那么这就是解决方案。但网格数据不是静态的。

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

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

我的建议是让索引缓冲区使用GLushort 作为索引类型。这样,您的索引数据每个四边形仅占用 12 个字节。使用 shorts 可以在单个绘图命令中限制为 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。这允许您指定一个索引来表示“重新启动原语”。这允许您使用GL_TRIANGLE_STRIP 基元并将基元分解为多个部分,同时仍然只有一个绘图调用。因此,不是每个四边形有 6 个索引,而是有 5 个,第 5 个是重新启动索引。所以现在你的 GLushort 索引每个四边形只占用 10 个字节。但是,batchSize 现在必须为 16383,因为索引 0xFFFF 保留用于重新启动。而vertsPerQuad 必须是 5。

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

【讨论】:

  • 每四边形 6 条短裤的绘制限制为 65536/6 个四边形?错误的。每个四边形只有 4 个唯一索引。有 65536 个唯一索引 (65535 PR) 可用于索引。所以 65536/4 四边形。 (65535/4 PR)您不仅限于使用 65536 个元素,因为您使用的是短裤。大声笑,出于某种奇怪的原因,您使用的是四边形计数而不是元素计数。基础顶点也总是在每个四边形上移动 4 个顶点。因此,即使您按元素数量而不是四元数量进行移动,您仍然会移动太多。每个四边形是 5 个还是 6 个元素。
  • @Puddle:“每个四边形只有 4 个唯一索引。有 65536 个唯一索引 (65535 PR) 可用于索引。”很公平。 “似乎当你结束我的问题时,你急于尝试复制我自己的解决方案。”不,它仍然是这个问题的副本。
  • 我同意这是同一问题的副本。但你还是很着急。无论是那个,还是您以前从未理解/测试过它。 (解释所有缺陷)我提到了原始重启,因为我假设他们对每 N 个索引版本都有远见。不需要索引缓冲区。这会非常方便。减少更多的内存。因此为什么我说这是毫无意义的。他们做了一半。
  • 还有 glMultiDrawElementsIndirect 会好很多。减少所有 api 调用的开销,并将所有参数存储在 gpu 上。
  • @Puddle:“glMultiDrawElementsIndirect 也会好很多” 会不会“好很多”? OpenGL 的 API 开销主要围绕着 绘制调用之间的状态变化;两个背靠背绘图调用的执行时间非常短。多重绘制间接可以覆盖循环渲染的四边形,但尾部批处理需要具有可变数量的顶点。而且您不想为每个四批次上传到间接缓冲区。因此,除非要渲染的四边形数量达到数十万,否则上述机制将正常运行。
【解决方案2】:

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

在我的解决方案中,我决定使用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 与 quad 完全不同,但它仍然有效。

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 来捕获包含三角形基元的新缓冲区。

【讨论】:

  • 您有什么证据表明这些解决方案中的任何一个(两者都使用了并不完全以速度着称的工具)比明显的方法更快?
  • 顺便说一句,如果您使用 GL4.x 级别的功能,您也可以使用计算着色器直接生成新的索引缓冲区。由于每个 4 个索引块将生成 6 个(或 5 个用于原始重启的条带)新索引,您可以轻松地将任务拆分为方便数量的线程和工作组,而无需通信或同步线程。
  • @Rabbid76 您对 Nicol Bolas 的回答有何异议?
  • @Makogan 这个想法是天才。它很简单,只需要很少的编程工作,也不需要额外的着色器。我不知道为什么我没有想到这一点。我所有的基本方法都太复杂了。当然,该方法仅在四元图元未由索引定义的情况下才有效。在这种情况下,解决方案是生成一个新的索引缓冲区,可能通​​过计算着色器,以及 derhass 在他的评论中建议。
  • @Rabbid76 那你为什么不接受他的回答呢?我很困惑
猜你喜欢
  • 1970-01-01
  • 2012-06-11
  • 2014-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-11
  • 2013-06-21
  • 2011-03-29
相关资源
最近更新 更多