如何在OpenGL 3.2中绘制全屏四边形的最佳方法?

32

我正在片段着色器中进行光线投射。我可以想到几种方法来绘制全屏四边形。一种是使用投影矩阵设置为单位矩阵在裁剪空间中绘制四边形,另一种是使用几何着色器将点转换为三角形带。前者使用立即模式,在OpenGL 3.2中已弃用。后者我使用新颖性,但仍然使用立即模式绘制一个点。


3
如果你只需要一个矩形,使用几何着色器从一个点生成一个矩形似乎过于复杂了。可以绘制两个三角形或者一个三角带即可。这四个顶点不会对你造成损失,至少不会像为了实现这么简单的功能写一个特殊的几何着色器那样困难。 - Christian Rau
6个回答

36

我认为最有效的方法是绘制一个“全屏”三角形。为了使三角形覆盖整个屏幕,它需要比实际视口更大。在NDC(以及剪切空间中,如果我们设置 w=1 ),视口将始终为[-1,1]正方形。为了使三角形完全覆盖这个区域,我们需要让两边的长度是视口矩形的两倍,这样第三边就会穿过视口的边缘,因此我们可以例如使用以下坐标(按逆时针顺序):(-1,-1)(3,-1)(-1,3)

我们也不需要担心纹理坐标。为了在可见视口中获得通常的归一化[0,1]范围,我们只需要使顶点对应的纹理坐标增加一倍,而重心插值将为任何视口像素产生与使用四边形时完全相同的结果。

当然,我们也可以像demanze的答案建议的那样与无属性渲染结合使用。

out vec2 texcoords; // texcoords are in the normalized [0,1] range for the viewport-filling quad part of the triangle
void main() {
        vec2 vertices[3]=vec2[3](vec2(-1,-1), vec2(3,-1), vec2(-1, 3));
        gl_Position = vec4(vertices[gl_VertexID],0,1);
        texcoords = 0.5 * gl_Position.xy + vec2(0.5);
}

为什么单个三角形更有效率?

这不仅仅是因为可以省略一个顶点着色器调用和一个前端处理的三角形。使用单个三角形最显著的效果是可以减少片段着色器调用量

真正的GPU总是在2x2像素大小的块(“四边形”)中调用片段着色器,只要基元的任何一个像素落入该块中就会触发调用。这是计算窗口空间导数函数所必需的(这些函数对于纹理采样也是隐含需要的,请参见此问题)。

如果基元没有覆盖该块中的所有4个像素,则其余的片段着色器调用将不会执行任何有用的工作(除了提供用于导数计算的数据),并且被称为帮助着色器调用(甚至可以通过gl_HelperInvocation GLSL函数查询)。请参见Fabian“ryg”Giesen的博客文章以获取更多详细信息。

如果您使用两个三角形渲染四边形,则两个三角形都将在视口对角线上生成大量无用的帮助程序调用。对于完美的正方形视口(宽高比1),效果最差。如果您只画一个三角形,则没有这样的对角线边缘(它位于视口之外,根本不会影响光栅化器),因此不会产生额外的帮助调用。

等等,如果三角形跨越视口边界,那么它不会被裁剪,并且实际上会在GPU上产生更多的工作吗?

如果您阅读有关图形管道(甚至GL规范)的教材材料,您可能会有这样的印象。但是,现实世界中的GPU使用一些不同的方法,例如查障剪裁(Guard-band clipping)。我不会在这里详细介绍(这将是一个单独的话题,请查看Fabian“ryg”Giesen的精彩博客文章以获取详细信息),但是一般思路是,无论基元是否完全在视口内部,光栅化器都只会为像素内部(或剪刀矩形)的像素产生片段,因此如果以下两种情况均成立:

  • a)三角形仅延伸到2D顶部/底部/左侧/右侧剪裁平面(与更棘手的z维度近/远截面相反,这更难处理,特别是因为顶点也可以位于相机后面


1
我喜欢这个答案,不仅因为它的性能好;而且还很容易实现! - tuket
这个回答应该得到更多的尊重和认可...由于我的着色器非常复杂,我获得了约15帧的提升。顺便说一下,在模糊方法中,我也能够看到四边形的对角线,但我仍然不知道为什么,但在那之后,当我开始使用三角形时,我就不用再担心它了。非常感谢您花费时间回答我的问题。 - Mudit Bhatia
1
“那为什么不用它呢?”因为它会使你试图做的事情变得模糊不清。如果你正在使用一些非常复杂的着色器进行绘制,那当然可以使用它,但否则你正在过度优化,并且使下一个需要理解你所谓的绘制四边形函数的人难以阅读代码,因为它只使用了3个顶点,而纹理坐标计算似乎没有意义。 - gman
2
如果有人在绘制此答案的代码上遇到困难:GLuint emptyVAO; glGenVertexArrays(1, &emptyVAO); glBindVertexArray(emptyVAO); glDrawArrays(GL_TRIANGLES, 0, 3);(感谢geenux提供的空VAO创建)。 - Van der Deken

21
您可以发送两个三角形来创建一个四边形,它们的顶点属性分别设置为-1/1。
在顶点/片段着色器中,您无需将它们与任何矩阵相乘。
以下是一些简单的代码示例:
顶点着色器:
const vec2 madd=vec2(0.5,0.5);
attribute vec2 vertexIn;
varying vec2 textureCoord;
void main() {
   textureCoord = vertexIn.xy*madd+madd; // scale vertex attribute to [0-1] range
   gl_Position = vec4(vertexIn.xy,0.0,1.0);
}

片元着色器:

varying vec2 textureCoord;
void main() {
   vec4 color1 = texture2D(t,textureCoord);
   gl_FragColor = color1;
}

3
顺便说一下,这个想法跟你以前的想法一致,但在OpenGL 3.2中绝对没有被弃用。你仍然可以通过顶点数组/缓冲区等方式发送顶点属性。OpenGL仍然是一种直接模式渲染API。 - AdilYalcin

16

不需要使用几何着色器、VBO或任何内存。

顶点着色器可以生成四边形。

layout(location = 0) out vec2 uv;

void main() 
{
    float x = float(((uint(gl_VertexID) + 2u) / 3u)%2u); 
    float y = float(((uint(gl_VertexID) + 1u) / 3u)%2u); 

    gl_Position = vec4(-1.0f + x*2.0f, -1.0f+y*2.0f, 0.0f, 1.0f);
    uv = vec2(x, y);
}

绑定一个空的VAO,发送一个包含6个顶点的绘制调用。


2
无用的计算,直接硬编码顶点位置。 - jeyko
绘制调用会是什么样子?你能写一个例子吗? - neoexpert

15

可以使用几何着色器输出全屏四边形:

#version 330 core

layout(points) in;
layout(triangle_strip, max_vertices = 4) out;

out vec2 texcoord;

void main() 
{
    gl_Position = vec4( 1.0, 1.0, 0.5, 1.0 );
    texcoord = vec2( 1.0, 1.0 );
    EmitVertex();

    gl_Position = vec4(-1.0, 1.0, 0.5, 1.0 );
    texcoord = vec2( 0.0, 1.0 ); 
    EmitVertex();

    gl_Position = vec4( 1.0,-1.0, 0.5, 1.0 );
    texcoord = vec2( 1.0, 0.0 ); 
    EmitVertex();

    gl_Position = vec4(-1.0,-1.0, 0.5, 1.0 );
    texcoord = vec2( 0.0, 0.0 ); 
    EmitVertex();

    EndPrimitive(); 
}

顶点着色器是空的:

#version 330 core

void main()
{
}

使用这个着色器,您可以使用带有空VBO的虚拟绘制命令:

glDrawArrays(GL_POINTS, 0, 1);

1
为什么剪裁空间的Z值是0.5而不是0.0?当绘制实际的四边形(或者正如其他人指出的,一个大三角形)更容易且更便宜时,为什么要使用它? - Nicol Bolas
Z坐标可以是0.0,你是对的。至于第二个问题,它对CPU来说确实更便宜,并且更灵活,因为可以在不重新构建程序的情况下更改着色器。 - Dimitry Leonov
多便宜?这是实际上可以感知和衡量的吗? - Nicol Bolas
如果您正在作为延迟渲染的一部分绘制与屏幕对齐的四边形,那么您需要找到在该三角形上放置UV坐标的位置吗?而对于四边形、正方形三角带,顶点位置和相应的UV{0.0,0.0...1.0,1.0}映射到顶点位置。 - ste3e
你如何创建VBO?到目前为止,我尝试的一切都没有成功:( - geenux
无需在意,它是这样的:GLint vao; glGenVertexArrays(1, &vao); glBindVertexArray(vao); glDrawArrays(GL_POINTS, 0, 1); - geenux

7

这与 demanze 的答案类似,但我认为更容易理解。同时,这只使用了 TRIANGLE_STRIP 来绘制 4 个顶点。

#version 300 es
out vec2 textureCoords;

void main() {
    const vec2 positions[4] = vec2[](
        vec2(-1, -1),
        vec2(+1, -1),
        vec2(-1, +1),
        vec2(+1, +1)
    );
    const vec2 coords[4] = vec2[](
        vec2(0, 0),
        vec2(1, 0),
        vec2(0, 1),
        vec2(1, 1)
    );

    textureCoords = coords[gl_VertexID];
    gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}

2
以下内容来自绘制FBO纹理到屏幕对齐四边形的类的绘制函数。
Gl.glUseProgram(shad);      

Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, vbo);           
Gl.glEnableVertexAttribArray(0);
Gl.glEnableVertexAttribArray(1);
Gl.glVertexAttribPointer(0, 3, Gl.GL_FLOAT, Gl.GL_FALSE, 0, voff);
Gl.glVertexAttribPointer(1, 2, Gl.GL_FLOAT, Gl.GL_FALSE, 0, coff);  

Gl.glActiveTexture(Gl.GL_TEXTURE0);
Gl.glBindTexture(Gl.GL_TEXTURE_2D, fboc);
Gl.glUniform1i(tileLoc, 0);

Gl.glDrawArrays(Gl.GL_QUADS, 0, 4);

Gl.glBindTexture(Gl.GL_TEXTURE_2D, 0);
Gl.glBindBuffer(Gl.GL_ARRAY_BUFFER, 0); 

Gl.glUseProgram(0); 

实际的四边形本身和坐标值从以下获得:

private float[] v=new float[]{  -1.0f, -1.0f, 0.0f,
                                1.0f, -1.0f, 0.0f,
                                1.0f, 1.0f, 0.0f,
                                -1.0f, 1.0f, 0.0f,

                                0.0f, 0.0f,
                                1.0f, 0.0f,
                                1.0f, 1.0f,
                                0.0f, 1.0f
};

关于vbo的绑定和设置,我交给你处理。

顶点着色器:

#version 330

layout(location = 0) in vec3 pos;
layout(location = 1) in vec2 coord;

out vec2 coords;

void main() {
    coords=coord.st;
    gl_Position=vec4(pos, 1.0);
}

由于该位置是原始的,即未乘以任何矩阵,四边形拟合的-1、-1::1、1适合于视口。请查看Alfonse在openGL.org上发布的任何帖子中链接的教程。


这是哪种编程语言?Java?Python? - BЈовић
3
D编程语言。太好了!使用Java/C#语法的真正指针。 - ste3e

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接