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.
我只是想在屏幕上画一条线。我使用的是OpenGL 4.6。我找到的所有教程都使用了glVertex指针,据我所知,它是不受欢迎的。
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)
我知道如何使用缓冲区绘制三角形,所以我尝试了一条线。它不起作用,只是显示了一个黑屏。(我使用的是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();
}
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?
如果是,我该如何修复我的代码?
更多回答
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)
如果您使用兼容性配置文件OpenGL上下文,则仍然可以使用glVertex AttribPointer.如果你必须使用核心配置文件上下文,那么你必须使用Shader程序和顶点数组对象,并且你需要一个好的教程(例如LearnOpenGL)
@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?
@rabid谢谢,但我想直接学习现代OpenGL,而不是依赖已经过时的代码。但我完全是一个初学者,所以也许使用这个功能终究是我想要的方式?
As I mentioned, read a tutorial. To explain shaders and vertex array objects is to broad for a single stackoverflow question.
正如我所提到的,请阅读教程。对于单个堆栈溢出问题来说,解释着色器和顶点数组对象过于宽泛。
@Rabbid It's not that I have no idea what this code does. I just asked for help in finding the error.
@rabbit这不是我不知道这段代码是做什么的。我只是请求帮助找出错误。
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.)
你可以使用最新的OpenGL来做你想做的事情,但如果你担心非常及时,你似乎就是这样,Vulkan是Khronos Group提供的最现代的产品,该集团也负责OpenGL。例如,Vulkan已经为多线程渲染做好了准备,而这在OpenGL中是很难做到的。(例如,OpenGL仍然受到广泛支持,但苹果已经不再支持它。)
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的调用。第二个参数是以字节为单位的缓冲区大小。由于顶点数组由2个坐标和2个分量组成,因此缓冲区的大小是4*sizeof(浮点数)而不是2*sizeof(浮点数):
glBufferData(GL_ARRAY_BUFFER, 2 * sizeof(float), line, GL_STATIC_DRAW);
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
但请注意,这仍然不是“现代”的OpenGL。如果要使用核心配置文件OpenGL上下文,则必须使用Shader程序和Vertex数组对象
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
但是,如果您使用的是核心OpenGL上下文并且设置了向前兼容位,则线的宽度(GlLineWidth)不能大于1.0。请参阅OpenGL 4.6 API核心配置文件规范-E.2已弃用和已删除的功能
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).
我推荐使用Shader,它可以沿着线条(甚至是线环)生成三角形基元。任务是用尽可能少的CPU和GPU开销生成粗线条。这意味着避免在CPU和几何体着色器(或细分着色器)上计算多边形。
Each segment of the line consist of a quad represented by 2 triangle primitives respectively 6 vertices.
这条线的每一段都由一个四边形组成,分别由两个三角形基元和6个顶点表示。
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.
使用线条的角点创建阵列。第一个点和最后一个点定义线条的起点和终点切线。所以你需要在这条线之前加1点,在这条线后面加1点。当然,通过比较索引与0和数组的长度来识别数组的第一个和最后一个元素是很容易的,但我们不想在着色器中进行任何额外的检查。如果必须绘制直线循环,则必须将最后一个点添加到数组头部,并将第一个点添加到其尾部。
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.
点数组存储到着色器存储缓冲区对象。我们利用的好处是,SSBO的最后一个变量可以是大小可变的数组。在旧版本的OpenGL(或OpenGL ES)中,可以使用统一缓冲区对象甚至纹理。
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.
着色器不需要任何顶点坐标或属性。我们所要知道的就是线段的索引。坐标存储在缓冲区中。为了找到索引,我们使用当前正在处理的顶点的索引(Gl_Vertex ID)。要绘制具有N个线段的线条,需要处理6*(N-1)个顶点。
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):
并绘制2*(N-1)个三角形(6*(N-1)个顶点):
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
):
对于SSBO中的坐标数组,使用了数据类型ve4(请相信我,您不会想要使用ve3):
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.
由于我们绘制的是N-1个线段,但数组中的元素数量为N+2,因此在顶点着色器中处理的每个顶点都可以访问从顶点[line_t]到顶点[line_t+3]的元素。顶点[line_t+1]和顶点[line_t+2]分别是直线段的起点和终点坐标。计算斜接需要顶点[line_t]和顶点[line_t+3]。
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.
线条的粗细应以像素为单位设置(均匀浮动u_thickness)。坐标必须从模型空间转换到窗口空间。为此,必须知道该视口中的分辨率(均匀向量u_分辨率)。别忘了视角上的分歧。这条线的绘制甚至可以在透视投影下工作。
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:
在最终的顶点着色器中,我们只需要根据tri_i计算v_miter1或v_miter2。使用斜接、线段的法线向量和线厚度(U_Ickness),可以计算顶点坐标:
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)
:
使用glPolygon模式(GL_FORWARE_AND_BACK,GL_FILL)和glPolygon模式(GL_FORWARD_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):
演示程序使用GLFW API创建一个窗口,GLEW加载OpenGL和GLM -OpenGL数学的数学。(我不提供的代码的功能,它只是创建一个程序对象,从顶点着色器和片段着色器的源代码):
#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;
}
更多回答
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.
该代码可以转换为苹果的金属图形框架,几乎1:1,如果你做这个小心使用金属着色语言的函数与快速算法,因为长度可以是零的斜接计算。
@Rabbid76 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.
@Rabbid76奇妙的例子!你能解释一下画一条开口线的点的顺序吗?此部分:“使用线条的角点创建一个数组。该数组必须包含第一个点和最后一个点两次...”我试过了,但结果并不像我预期的那样。例如。数组应该是什么样子才能得到行p0:{-100,-100}、p1:{100,100}、p2:{200,100}、p3:{300,-50}?我试了[p0,p1,p2,p3,p0,p1],但没有画出所有的线段。
@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.
@SinisaDrpa第一点和最后一点定义直线的起点和终点。所以你需要在这条线之前加1点,在这条线后面加1点。我更改了答案中的这一部分。例如[(-200,-200),(-100,-100),(100,100),(200,100),(350,-50),(400,0)]。在本例中,(-200,-200)表示起点切线,(400,0)表示终点切线。
我是一名优秀的程序员,十分优秀!