You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用GL_TRIANGLES绘制复杂2D多边形的错误及通用方案问询

解决OpenGL中用GL_TRIANGLES绘制凹多边形的问题

嘿,这个问题我之前做OpenGL 2D渲染的时候也踩过坑!你一开始用的那种“以第一个顶点为中心,依次连接后续顶点构建三角形”的方法,本质是扇形三角剖分,这种方法只对凸多边形有效——碰到L形这种凹多边形,就会出现你说的“在空白区域错误绘制三角形”的情况,因为凹多边形的内部会被这种方法“穿”过去。

针对你这种顺时针排序的简单多边形(边不自交),最通用的解决方案是用耳切法(Ear Clipping),它可以完美生成GL_TRIANGLES需要的顶点索引,完全不需要切换到其他渲染模式。

耳切法的核心思路

简单来说,耳切法就是不断在多边形上找到“耳”——一个凸的顶点,且这个顶点和相邻两个顶点组成的三角形内部不包含多边形的其他顶点。每次找到一个耳,就把这个三角形的索引加入列表,然后移除这个顶点,重复这个过程直到多边形只剩3个顶点(最后一个三角形)。

具体实现步骤(结合OpenGL需求)

1. 准备工作

确保你的多边形是简单多边形(边之间除了相邻顶点外没有交点),耳切法只处理这种情况。同时保留好原始顶点的索引,因为后续移除顶点时需要跟踪原始的顶点位置。

2. 判断凸顶点

因为你的点是顺时针排序的,我们可以通过叉积来判断一个顶点是否是凸顶点:
对于三个连续的顶点A(prev)、B(curr)、C(next),计算叉积:

float cross = (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x);

如果cross >= 0(假设OpenGL的坐标系是y轴向上),说明B是凸顶点(顺时针排序下,凸顶点的转向是顺时针,叉积非负)。如果你的坐标系是y轴向下,判断条件改成cross <= 0即可。

3. 判断是否是“耳”

找到凸顶点后,需要验证这个顶点对应的三角形ABC内部是否包含多边形的其他顶点。如果没有其他顶点在三角形内部,那这个顶点就是一个“耳”。

判断点是否在三角形内部,可以用叉积法:对于待判断的点P,计算P与三角形三条边的叉积,确保所有叉积的符号都和三角形的顺时针方向一致(即都非负,对应y轴向上的情况)。

4. 生成GL_TRIANGLES的索引

每次找到一个耳,就把对应的三个原始顶点索引加入你的索引数组(比如prev_idx, curr_idx, next_idx),然后从顶点列表和原始索引列表中移除当前顶点。重复这个过程,直到顶点列表只剩3个顶点,把这三个的索引也加入数组。

伪代码示例

// 假设我们有:
// std::vector<glm::vec2> vertices; // 顺时针排序的顶点列表
// std::vector<unsigned int> original_indices; // 原始顶点的索引,初始是0,1,2,...n-1
// std::vector<unsigned int> triangles_indices; // 最终给GL_TRIANGLES的索引

while (vertices.size() > 3) {
    bool found_ear = false;
    for (int i = 0; i < vertices.size(); ++i) {
        int prev_i = (i - 1 + vertices.size()) % vertices.size();
        int curr_i = i;
        int next_i = (i + 1) % vertices.size();
        
        glm::vec2 A = vertices[prev_i];
        glm::vec2 B = vertices[curr_i];
        glm::vec2 C = vertices[next_i];
        
        // 判断是否是凸顶点(y轴向上,顺时针排序)
        float cross = (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x);
        if (cross < 0) continue; // 凹顶点,跳过
        
        // 判断三角形ABC内部是否有其他顶点
        bool is_ear = true;
        for (int j = 0; j < vertices.size(); ++j) {
            if (j == prev_i || j == curr_i || j == next_i) continue;
            glm::vec2 P = vertices[j];
            // 计算P与三条边的叉积
            float c1 = (B.x - A.x) * (P.y - A.y) - (B.y - A.y) * (P.x - A.x);
            float c2 = (C.x - B.x) * (P.y - B.y) - (C.y - B.y) * (P.x - B.x);
            float c3 = (A.x - C.x) * (P.y - C.y) - (A.y - C.y) * (P.x - C.x);
            // 顺时针三角形,内部点的叉积都应该>=0(包含在边上的情况)
            if (c1 < 0 || c2 < 0 || c3 < 0) {
                is_ear = false;
                break;
            }
        }
        
        if (is_ear) {
            // 加入GL_TRIANGLES的索引
            triangles_indices.push_back(original_indices[prev_i]);
            triangles_indices.push_back(original_indices[curr_i]);
            triangles_indices.push_back(original_indices[next_i]);
            // 移除当前顶点和对应的原始索引
            vertices.erase(vertices.begin() + curr_i);
            original_indices.erase(original_indices.begin() + curr_i);
            found_ear = true;
            break;
        }
    }
    if (!found_ear) {
        // 理论上简单多边形一定会有耳,这里处理异常情况
        break;
    }
}

// 处理最后三个顶点
triangles_indices.push_back(original_indices[0]);
triangles_indices.push_back(original_indices[1]);
triangles_indices.push_back(original_indices[2]);

额外提示

  • 如果你的多边形顶点数量很多,建议预计算三角化索引,而不是实时计算,这样能提升渲染性能。
  • 如果你需要处理自交的复杂多边形,耳切法就不适用了,需要先对多边形进行分割,不过大部分2D场景中遇到的都是简单凹多边形,耳切法足够用。
  • 注意坐标系的y轴方向,叉积的符号判断要对应你的坐标系设置,不然会把凹顶点当成凸顶点,导致错误。

内容的提问来源于stack exchange,提问作者Sibh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 11:17:51