Drawing a line in modern OpenGL
Asked Answered
H

1

9

I simply want to draw a line to the screen. I'm using OpenGl 4.6. All tutorials I found used a glVertexPointer, which is deprecated as far as I can tell.

I know how you can draw triangles using buffers, so I tried that with a line. It didn't work, merely displaying a black screen. (I'm using GLFW and GLEW, and I am using a vertex+fragment shader I already tested on the triangle)

// 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();
}

Am I going in the right direction, or is a completely different approach the current best practice?

If I am, how do I fix my code?

Hoard answered 27/2, 2020 at 19:43 Comment(7)
If you use a compatibility profile OpenGL Context, then you can still use glVertexAttribPointer. If you have to use a core profile context than you have to use a Shader program and a Vertex Array Object and you'll need a good tutorial (e.g. LearnOpenGL)Trawick
@Rabbid thanks, but I want to learn modern OpenGL straight away, not relying on code that is already outdated. But I am a total beginner, so maybe using this function is the intended way after all?Hoard
As I mentioned, read a tutorial. To explain shaders and vertex array objects is to broad for a single stackoverflow question.Trawick
@Rabbid It's not that I have no idea what this code does. I just asked for help in finding the error.Hoard
You can use the latest OpenGL to do what you want, but if you're concerned about being very up-to-date, which you seem to be, Vulkan is the most modern offering from the Khronos Group, which is also responsible for OpenGL. For instance, Vulkan is ready for multithreaded rendering, whereas this is hard in OpenGL. (OpenGL is still widely supported, but Apple has deprecated support for it, for instance.)Contortion
@Rabbid I am creating a VBO here if I'm not mistaken. I used this to draw a triangle before, simply using 3 instead of 2 vectors. The shaders are some really basic ones I wrote following this tutorial. They worked for the triangle, but I can still put them into the question if you want.Hoard
A Vertex Buffer Object is not a Vertex Array ObjectTrawick
T
31

The issue is the call to glBufferData. The 2nd argument is the size of the buffer in bytes. Since the vertex array consists of 2 coordinates with 2 components, the size of the bufferis 4 * sizeof(float) rather than 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);

But note that is still not "modern" OpenGL. If you want to use core profile OpenGL Context, then you have to use a Shader program and a Vertex Array Object


However, if you are using a core OpenGL context and the forward compatibility bit is set, the width of a line (glLineWidth), cannot be grater than 1.0.
See OpenGL 4.6 API Core Profile Specification - E.2 Deprecated and Removed Features

Wide lines - LineWidth values greater than 1.0 will generate an INVALID_VALUE error.

You have to find a different approach.

I recommend to use a Shader, which generates triangle primitives along a line strip (or even a line loop).
The task is to generate thick line strip, with as little CPU and GPU overhead as possible. That means to avoid computation of polygons on the CPU as well as geometry shaders (or tessellation shaders).

Each segment of the line consist of a quad represented by 2 triangle primitives respectively 6 vertices.

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

Between the line segments the miter hast to be found and the quads have to be cut to the miter.

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

Create an array with the corners points of the line strip. The first and the last point define the start and end tangents of the line strip. So you need to add 1 point before the line and one point after the line. Of course it would be easy, to identify the first and last element of the array by comparing the index to 0 and the length of the array, but we don't want to do any extra checks in the shader.
If a line loop has to be draw, then the last point has to be add to the array head and the first point to its tail.

The array of points is stored to a Shader Storage Buffer Object. We use the benefit, that the last variable of the SSBO can be an array of variable size. In older versions of OpenGL (or OpenGL ES) a Uniform Buffer Object or even a Texture can be used.

The shader doesn't need any vertex coordinates or attributes. All we have to know is the index of the line segment. The coordinates are stored in the buffer. To find the index we make use of the the index of the vertex currently being processed (gl_VertexID).
To draw a line strip with N segments, 6*(N-1) vertices have tpo be processed.

We have to create an "empty" Vertex Array Object (without any vertex attribute specification):

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

And to draw 2*(N-1) triangle (6*(N-1) vertices):

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

For the coordinate array in the SSBO, the data type vec4 is used (Pleas believe me, you don't want to use vec3):

layout(std430, binding = 0) buffer TVertex
{
   vec4 vertex[];
};

Compute the index of the line segment, where the vertex coordinate belongs too and the index of the point in the 2 triangles:

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

Since we are drawing N-1 line segments, but the number of elements in the array is N+2, the elements form vertex[line_t] to vertex[line_t+3] can be accessed for each vertex which is processed in the vertex shader.
vertex[line_t+1] and vertex[line_t+2] are the start respectively end coordinate of the line segment. vertex[line_t] and vertex[line_t+3] are required to compute the miter.

The thickness of the line should be set in pixel unit (uniform float u_thickness). The coordinates have to be transformed from model space to window space. For that the resolution of the viewport has to be known (uniform vec2 u_resolution). Don't forget the perspective divide. The drawing of the line will even work at perspective projection.

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;
}

The miter calculation even works if the predecessor or successor point is equal to the start respectively end point of the line segment. In this case the end of the line is cut normal to its tangent:

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));

In the final vertex shader we just need to calculate either v_miter1 or v_miter2 dependent on the tri_i. With the miter, the normal vector to the line segment and the line thickness (u_thickness), the vertex coordinate can be computed:

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);
}

Finally the window coordinates have to be transformed back to clip space coordinates. Transform from window space to normalized device space. The perspective divide has to be reversed:

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

Polygons created with glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) and glPolygonMode(GL_FRONT_AND_BACK, GL_LINE):

Demo program using GLFW API for creating a window, GLEW for loading OpenGL and GLM -OpenGL Mathematics for the math. (I don't provide the code for the function CreateProgram, which just creates a program object, from the vertex shader and fragment shader source code):

#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);
}
)";


// main

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 )
        throw std::runtime_error( "error initializing glfw" );
    GLFWwindow *window = glfwCreateWindow( 800, 600, "GLFW OGL window", nullptr, nullptr );
    if (window == nullptr)
    {
        glfwTerminate();
        throw std::runtime_error("error initializing window"); 
    }
    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK)
        throw std::runtime_error("error initializing glew");

    OpenGL::CContext::TDebugLevel debug_level = OpenGL::CContext::TDebugLevel::all;
    OpenGL::CContext context;
    context.Init( debug_level );

    GLuint program  = OpenGL::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;

    std::vector<glm::vec4> varray;
    varray.emplace_back(glm::vec4(0.0f, -1.0f, 0.0f, 1.0f));
    varray.emplace_back(glm::vec4(1.0f, -1.0f, 0.0f, 1.0f));
    for (int u=0; u <= 90; u += 10)
    {
        double a = u*M_PI/180.0;
        double c = cos(a), s = sin(a);
        varray.emplace_back(glm::vec4((float)c, (float)s, 0.0f, 1.0f));
    }
    varray.emplace_back(glm::vec4(-1.0f, 1.0f, 0.0f, 1.0f));
    for (int u = 90; u >= 0; u -= 10)
    {
        double a = u * M_PI / 180.0;
        double c = cos(a), s = sin(a);
        varray.emplace_back(glm::vec4((float)c-1.0f, (float)s-1.0f, 0.0f, 1.0f));
    }
    varray.emplace_back(glm::vec4(1.0f, -1.0f, 0.0f, 1.0f));
    varray.emplace_back(glm::vec4(1.0f, 0.0f, 0.0f, 1.0f));
    GLuint ssbo = CreateSSBO(varray);
    
    GLuint vao;
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
    GLsizei N = (GLsizei)varray.size() - 2;

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

    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;
        
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
        glUniformMatrix4fv(loc_mvp, 1, GL_FALSE, glm::value_ptr(mvp1));
        glDrawArrays(GL_TRIANGLES, 0, 6*(N-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;
        
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        glUniformMatrix4fv(loc_mvp, 1, GL_FALSE, glm::value_ptr(mvp2));
        glDrawArrays(GL_TRIANGLES, 0, 6*(N-1));
        
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    glfwTerminate();

    return 0;
}
Trawick answered 27/2, 2020 at 20:3 Comment(3)
The code can be translated to Apple's Metal graphics framework nearly 1:1, if you do this watch out to use the precise::normalize function of the Metal shading language with fast arithmetic since the length can be zero in the miter calculation.Pollen
@Trawick Fantastic example! Could you explain the order of points to draw an open line? This part: "Create an array with the corners points of the line strip. The array has to contain the first and the last point twice..." I tried that but the result was not what I expected. eg. What should the array look like to get line p0: {-100, -100}, p1: {100, 100}, p2: {200, 100}, p3: {300, -50}? I tried [p0, p1, p2, p3, p0, p1] but not all segments were drawn.Cavite
@SinisaDrpa The first and the last point define the start and end tangents of the line. So you need to add 1 point before the line and one point after the line. I changed that part in the answer. e.g. [(-200, -200), (-100, -100), (100, 100), (200, 100), (350, -50), (400, 0)]. In this case (-200, -200) is for the start tangent and (400, 0) for the end tangent.Trawick

© 2022 - 2024 — McMap. All rights reserved.