在现代 OpenGL 中画一条线

Posted

技术标签:

【中文标题】在现代 OpenGL 中画一条线【英文标题】:Drawing a line in modern OpenGL 【发布时间】:2020-02-27 19:43:49 【问题描述】:

我只是想在屏幕上画一条线。我正在使用 OpenGL 4.6。我发现的所有教程都使用了glVertexPointer,据我所知,它已被弃用。

我知道如何使用缓冲区绘制三角形,所以我尝试了用一条线。它不起作用,只是显示黑屏。 (我使用的是 GLFW 和 GLEW,我使用的是我已经在三角形上测试过的顶点+片段着色器)

// Make line
float line[] = 
    0.0, 0.0,
    1.0, 1.0
;

unsigned int buffer; // The ID, kind of a pointer for VRAM
glGenBuffers(1, &buffer); // Allocate memory for the triangle
glBindBuffer(GL_ARRAY_BUFFER, buffer); // Set the buffer as the active array
glBufferData(GL_ARRAY_BUFFER, 2 * sizeof(float), line, GL_STATIC_DRAW); // Fill the buffer with data
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0); // Specify how the buffer is converted to vertices
glEnableVertexAttribArray(0); // Enable the vertex array

// Loop until the user closes the window
while (!glfwWindowShouldClose(window))

    // Clear previous
    glClear(GL_COLOR_BUFFER_BIT);

    // Draw the line
    glDrawArrays(GL_LINES, 0, 2);

    // Swap front and back buffers
    glfwSwapBuffers(window);

    // Poll for and process events
    glfwPollEvents();

我的方向是否正确,或者当前的最佳实践是完全不同的方法?

如果是,我该如何修复我的代码?

【问题讨论】:

如果您使用兼容性配置文件OpenGL Context,那么您仍然可以使用glVertexAttribPointer。如果您必须使用核心配置文件上下文,则必须使用 Shader 程序和 Vertex Array Object 并且您需要一个好的教程(例如 LearnOpenGL) @Rabbid 谢谢,但我想立即学习现代 OpenGL,而不是依赖已经过时的代码。但我是一个完全的初学者,所以也许使用这个功能毕竟是预期的方式? 正如我提到的,阅读教程。解释着色器和顶点数组对象对于单个 *** 问题来说是广泛的。 @Rabbid 并不是我不知道这段代码的作用。我只是寻求帮助以找到错误。 您可以使用最新的 OpenGL 来做您想做的事,但如果您担心要保持最新状态(您似乎是这样),Vulkan 是 Khronos 提供的最现代的产品Group,它也负责 OpenGL。例如,Vulkan 已准备好进行多线程渲染,而这在 OpenGL 中很难。 (OpenGL 仍然得到广泛支持,但 Apple 已经弃用了对它的支持。) 【参考方案1】:

问题是对glBufferData 的调用。第二个参数是以字节为单位的缓冲区大小。由于顶点数组由 2 个坐标和 2 个分量组成,因此缓冲区的大小是 4 * sizeof(float) 而不是 2 * sizeof(float)

glBufferData(GL_ARRAY_BUFFER, 2 * sizeof(float), line, GL_STATIC_DRAW);

glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(float), line, GL_STATIC_DRAW);

但请注意,它仍然不是“现代”OpenGL。如果要使用核心配置文件OpenGL Context,则必须使用Shader 程序和Vertex Array Object


但是,如果您使用的是核心 OpenGL context 并且设置了前向兼容性位,则行的宽度 (glLineWidth) 不能再大了大于 1.0。 见OpenGL 4.6 API Core Profile Specification - E.2 Deprecated and Removed Features

宽线 - LineWidth 值大于 1.0 将生成 INVALID_VALUE 错误。

你必须找到不同的方法。

我建议使用Shader,它会沿着线条(甚至是线条循环)生成triangle primitives。 任务是生成粗线带,尽可能少的 CPU 和 GPU 开销。这意味着要避免在 CPU 以及几何着色器(或曲面细分着色器)上计算多边形。

直线的每一段由一个四边形组成,由 2 个三角形图元分别代表 6 个顶点。

0        2   5
 +-------+  +
 |     /  / |
 |   /  /   |
 | /  /     |
 +  +-------+
1   3        4

在必须找到斜接的线段和必须将四边形切割到斜接的线段之间。

+----------------+
|              / |
| segment 1  /   |
|          /     |
+--------+       |
         | segment 2
         |       |
         |       |
         +-------+

用线条的角点创建一个数组。第一个点和最后一个点定义线带的起点和终点切线。所以需要在行前加1点,行后加1点。当然,通过比较索引为 0 和数组的长度来识别数组的第一个和最后一个元素是很容易的,但是我们不想在着色器中进行任何额外的检查。 如果必须画一个线循环,则必须将最后一个点添加到数组头部,并将第一个点添加到它的尾部。

点数组存储到Shader Storage Buffer Object。我们利用的好处是,SSBO 的最后一个变量可以是一个可变大小的数组。在旧版本的 OpenGL(或 OpenGL ES)中,可以使用 Uniform Buffer Object 甚至 Texture。

着色器不需要任何顶点坐标或属性。我们只需要知道线段的索引。坐标存储在缓冲区中。为了找到索引,我们使用当前正在处理的顶点的索引 (gl_VertexID)。 要绘制带有N 线段的线带,需要处理6*(N-1) 顶点。

我们必须创建一个“空”Vertex Array Object(没有任何顶点属性规范):

glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

并绘制2*(N-1)三角形(6*(N-1)顶点):

glDrawArrays(GL_TRIANGLES, 0, 6*(N-1));

对于SSBO中的坐标数组,使用数据类型vec4(请相信我,你不想使用vec3):

layout(std430, binding = 0) buffer TVertex

   vec4 vertex[];
;

计算顶点坐标所属的线段的索引以及2个三角形中点的索引:

int line_i = gl_VertexID / 6;
int tri_i  = gl_VertexID % 6;

由于我们正在绘制N-1 线段,但数组中的元素数量为N+2,因此在顶点着色器中处理的每个顶点都可以访问从vertex[line_t]vertex[line_t+3] 的元素。 vertex[line_t+1]vertex[line_t+2] 分别是线段的起点和终点坐标。计算斜接需要vertex[line_t]vertex[line_t+3]

线的粗细应该以像素为单位设置(uniform float u_thickness)。坐标必须从模型空间转换到窗口空间。为此,必须知道视口的分辨率 (uniform vec2 u_resolution)。不要忘记perspective divide。线条的绘制甚至可以在透视投影中使用。

vec4 va[4];
for (int i=0; i<4; ++i)

    va[i] = u_mvp * vertex[line_i+i];
    va[i].xyz /= va[i].w;
    va[i].xy = (va[i].xy + 1.0) * 0.5 * u_resolution;

即使前导点或后继点等于线段的起点和终点,斜接计算也有效。在这种情况下,线的末端垂直于其切线:

vec2 v_line   = normalize(va[2].xy - va[1].xy);
vec2 nv_line  = vec2(-v_line.y, v_line.x);
vec2 v_pred   = normalize(va[1].xy - va[0].xy);
vec2 v_succ   = normalize(va[3].xy - va[2].xy);
vec2 v_miter1 = normalize(nv_line + vec2(-v_pred.y, v_pred.x));
vec2 v_miter2 = normalize(nv_line + vec2(-v_succ.y, v_succ.x));

在最终的顶点着色器中,我们只需要根据tri_i 计算v_miter1v_miter2。通过斜接、线段的法向量和线的粗细(u_thickness),可以计算出顶点坐标:

vec4 pos;
if (tri_i == 0 || tri_i == 1 || tri_i == 3)

    vec2 v_pred  = normalize(va[1].xy - va[0].xy);
    vec2 v_miter = normalize(nv_line + vec2(-v_pred.y, v_pred.x));

    pos = va[1];
    pos.xy += v_miter * u_thickness * (tri_i == 1 ? -0.5 : 0.5) / dot(v_miter, nv_line);

else

    vec2 v_succ  = normalize(va[3].xy - va[2].xy);
    vec2 v_miter = normalize(nv_line + vec2(-v_succ.y, v_succ.x));

    pos = va[2];
    pos.xy += v_miter * u_thickness * (tri_i == 5 ? 0.5 : -0.5) / dot(v_miter, nv_line);

最后必须将窗口坐标转换回剪辑空间坐标。从窗口空间转换为标准化设备空间。必须扭转视角鸿沟:

pos.xy = pos.xy / u_resolution * 2.0 - 1.0;
pos.xyz *= pos.w;

着色器可以生成以下多边形(用glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)渲染)

(使用默认模式 - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

对于以下简单的演示程序,我使用GLFW API 创建一个窗口,GLEW 用于加载 OpenGL,GLM -OpenGL Mathematics 用于数学运算。我不提供函数CreateProgram的代码,它只是从顶点着色器和片段着色器源代码中创建一个程序对象:

#include <vector>
#include <string>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <gl/gl_glew.h>
#include <GLFW/glfw3.h>

std::string vertShader = R"(
#version 460

layout(std430, binding = 0) buffer TVertex

   vec4 vertex[]; 
;

uniform mat4  u_mvp;
uniform vec2  u_resolution;
uniform float u_thickness;

void main()

    int line_i = gl_VertexID / 6;
    int tri_i  = gl_VertexID % 6;

    vec4 va[4];
    for (int i=0; i<4; ++i)
    
        va[i] = u_mvp * vertex[line_i+i];
        va[i].xyz /= va[i].w;
        va[i].xy = (va[i].xy + 1.0) * 0.5 * u_resolution;
    

    vec2 v_line  = normalize(va[2].xy - va[1].xy);
    vec2 nv_line = vec2(-v_line.y, v_line.x);

    vec4 pos;
    if (tri_i == 0 || tri_i == 1 || tri_i == 3)
    
        vec2 v_pred  = normalize(va[1].xy - va[0].xy);
        vec2 v_miter = normalize(nv_line + vec2(-v_pred.y, v_pred.x));

        pos = va[1];
        pos.xy += v_miter * u_thickness * (tri_i == 1 ? -0.5 : 0.5) / dot(v_miter, nv_line);
    
    else
    
        vec2 v_succ  = normalize(va[3].xy - va[2].xy);
        vec2 v_miter = normalize(nv_line + vec2(-v_succ.y, v_succ.x));

        pos = va[2];
        pos.xy += v_miter * u_thickness * (tri_i == 5 ? 0.5 : -0.5) / dot(v_miter, nv_line);
    

    pos.xy = pos.xy / u_resolution * 2.0 - 1.0;
    pos.xyz *= pos.w;
    gl_Position = pos;

)";

std::string fragShader = R"(
#version 460

out vec4 fragColor;

void main()

    fragColor = vec4(1.0);

)";

GLuint CreateSSBO(std::vector<glm::vec4> &varray)

    GLuint ssbo;
    glGenBuffers(1, &ssbo);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo );
    glBufferData(GL_SHADER_STORAGE_BUFFER, varray.size()*sizeof(*varray.data()), varray.data(), GL_STATIC_DRAW); 
    return ssbo;


int main(void)

    if ( glfwInit() == 0 )
        return 0;
    GLFWwindow *window = glfwCreateWindow( 800, 600, "GLFW OGL window", nullptr, nullptr );
    if ( window == nullptr )
    
        glfwTerminate();
        retturn 0;
    
    glfwMakeContextCurrent(window);
    if ( glewInit() != GLEW_OK )
        return 0;

    GLuint program  = CreateProgram(vertShader, fragShader);
    GLint  loc_mvp  = glGetUniformLocation(program, "u_mvp");
    GLint  loc_res  = glGetUniformLocation(program, "u_resolution");
    GLint  loc_thi  = glGetUniformLocation(program, "u_thickness");

    glUseProgram(program);
    glUniform1f(loc_thi, 20.0);

    GLushort pattern = 0x18ff;
    GLfloat  factor  = 2.0f;

    glm::vec4 p0(-1.0f, -1.0f, 0.0f, 1.0f);
    glm::vec4 p1(1.0f, -1.0f, 0.0f, 1.0f);
    glm::vec4 p2(1.0f, 1.0f, 0.0f, 1.0f);
    glm::vec4 p3(-1.0f, 1.0f, 0.0f, 1.0f);
    std::vector<glm::vec4> varray1 p3, p0, p1, p2, p3, p0, p1 ;
    GLuint ssbo1 = CreateSSBO(varray1);

    std::vector<glm::vec4> varray2;
    for (int u=-8; u <= 368; u += 8)
    
        double a = u*M_PI/180.0;
        double c = cos(a), s = sin(a);
        varray2.emplace_back(glm::vec4((float)c, (float)s, 0.0f, 1.0f));
    
    GLuint ssbo2 = CreateSSBO(varray2);

    GLuint vao;
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    glm::mat4(project);
    int vpSize[2]0, 0;
    while (!glfwWindowShouldClose(window))
    
        int w, h;
        glfwGetFramebufferSize(window, &w, &h);
        if (w != vpSize[0] ||  h != vpSize[1])
        
            vpSize[0] = w; vpSize[1] = h;
            glViewport(0, 0, vpSize[0], vpSize[1]);
            float aspect = (float)w/(float)h;
            project = glm::ortho(-aspect, aspect, -1.0f, 1.0f, -10.0f, 10.0f);
            glUniform2f(loc_res, (float)w, (float)h);
        

        glClear(GL_COLOR_BUFFER_BIT);

        glm::mat4 modelview1( 1.0f );
        modelview1 = glm::translate(modelview1, glm::vec3(-0.6f, 0.0f, 0.0f) );
        modelview1 = glm::scale(modelview1, glm::vec3(0.5f, 0.5f, 1.0f) );
        glm::mat4 mvp1 = project * modelview1;

        glUniformMatrix4fv(loc_mvp, 1, GL_FALSE, glm::value_ptr(mvp1));
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo1);
        GLsizei N1 = (GLsizei)varray1.size()-2;
        glDrawArrays(GL_TRIANGLES, 0, 6*(N1-1));

        glm::mat4 modelview2( 1.0f );
        modelview2 = glm::translate(modelview2, glm::vec3(0.6f, 0.0f, 0.0f) );
        modelview2 = glm::scale(modelview2, glm::vec3(0.5f, 0.5f, 1.0f) );
        glm::mat4 mvp2 = project * modelview2;

        glUniformMatrix4fv(loc_mvp, 1, GL_FALSE, glm::value_ptr(mvp2));
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo2);
        GLsizei N2 = (GLsizei)varray2.size()-2;
        glDrawArrays(GL_TRIANGLES, 0, 6*(N2-1));

        glfwSwapBuffers(window);
        glfwPollEvents();
    
    glfwTerminate();

    return 0;

【讨论】:

代码几乎可以 1:1 转换为 Apple 的 Metal 图形框架,如果您这样做,请注意使用具有快速算术的 Metal 着色语言的精确::normalize 函数,因为长度可以在斜接计算中为零。 @Rabbid76 很棒的例子!你能解释一下画一条开放线的点顺序吗?这部分:“用线条的角点创建一个数组。数组必须包含第一个和最后一个点两次......”我试过了,但结果不是我所期望的。例如。得到线 p0: -100, -100, p1: 100, 100, p2: 200, 100, p3: 300, -50 的数组应该是什么样子?我尝试了 [p0, p1, p2, p3, p0, p1] 但并非所有线段都被绘制出来。 @SinisaDrpa 第一个点和最后一个点定义了直线的起点和终点。所以需要在行前加1点,行后加1点。我改变了答案中的那部分。例如[(-200, -200), (-100, -100), (100, 100), (200, 100), (350, -50), (400, 0)]。在这种情况下,(-200, -200) 用于起始切线,(400, 0) 用于结束切线。

以上是关于在现代 OpenGL 中画一条线的主要内容,如果未能解决你的问题,请参考以下文章

在 UIViewController 上画一条线

如何在HTML中画一条线

在 QML 的圆形路径中画一条线?

如何在 Sprite-kit 中画一条线

在jetpack compose中画一条线

在自定义视图中画一条线