OpenGL渲染三角形

OpenGL渲染三角形

在 OpenGL 中,定义的物体是在 3D 空间中,但屏幕或窗口是一个 2D 像素阵列,因此 OpenGL 的工作是将所有 3D 坐标转换为适合屏幕上的 2D 像素。将 3D 坐标转换为 2D 像素的过程由 OpenGL 的图形管道完成的。图形管道可分为两个大部分:第一部分将3D 坐标转换为 2D 坐标;第二部分将 2D 坐标转换为实际彩色像素。

图形管道将一组 3D 坐标作为输入,并将这些坐标转换为屏幕上的彩色 2D 像素。图形管道可分为几个步骤,其中每个步骤都需要前一步的输出作为输入。所有这些步骤都高度专业化(它们具有一个特定的功能),并且可以很容易地并行执行。由于其平行性质,当今的显卡拥有数千个小型处理内核,可以在图形管道内快速处理数据。处理核心在 GPU 上运行小型程序,这些小程序被称为着色器。

其中一些着色器可由开发人员配置,这使我们能够编写我们自己的着色器来替换现有的默认着色器。这为我们提供了对管道特定部分的更精细的控制,并且由于它们运行在 GPU 上,它们还可以为我们节省宝贵的 CPU 时间。

下面是图形管道所有阶段的抽象表示,请注意,蓝色部分表示我们可以注入我们自己的着色器的部分。
在这里插入图片描述
图形管道包含六大部分,每个部分处理将顶点数据转换为完全渲染的像素的一个特定部分。我们将以简化的方式简要解释管道的每个部分,以便您全面了解管道的运行情况。

作为图形管道的输入,我们传入一个由 3D 坐标组成的列表,这些坐标组成名为Vertex Data的数组用于形成三角形。这个顶点数据是一个顶点的集合。顶点是每个 3D 坐标的数据集合。该顶点的数据表示为顶点属性,同时它可以包含我们想要的任何数据,但为了简单起见,我们假设每个顶点仅由 3D 位置和一些颜色值组成。

管道的第一部分是顶点着色器,将单个顶点用作输入。顶点着色器的主要目的是将 3D 坐标转换为不同的 3D 坐标,顶点着色器允许我们在顶点属性上进行一些基本处理。

顶点解析阶段将来自顶点着色器中的所有顶点(或顶点,如果选择GL_POINTS)作为输入,形成图元并组装给定图元形状中的所有点。

顶点解析阶段的输出传递给几何着色器。几何着色器将形成图元的顶点集合作为输入,并能够通过生成新的顶点来形成新的(或其他)图元来生成其他形状。在此示例中,它会生成给定形状的第二个三角形。

然后,几何着色器的输出被传递到光栅化阶段,在那里它将生成的图元映射到最终屏幕上的相应像素,从而产生供片段着色器使用的片段。在片段着色器运行之前,执行裁剪。裁剪会丢弃了您视图之外的所有片段,提高了性能。

片元着色器的主要目的是计算像素的最终颜色,这通常是所有高级 OpenGL 效果发生的阶段。通常,片元着色器包含有关 3D 场景的数据,可用于计算最终像素颜色(如灯光、阴影、光线颜色等)。

在确定所有相应的颜色值后,对象将再经过一个阶段,我们称之为阿尔法测试混合阶段。此阶段检查片段的相应深度(和模板)值,并使用这些值来检查生成的片段是否位于其他对象的前面或后面,是否进行了相应地丢弃。该阶段还检查阿尔法值(阿尔法值定义对象的不透明性),并相应地混合对象。因此,即使在片元着色器中计算了像素输出颜色,在渲染多个三角形时,最终像素颜色可能仍然完全不同。

图形管道是一个相当复杂的整体,包含许多可配置的部分。然而,对于几乎所有的情况下,我们只需要使用顶点和片元着色器。几何着色器是可选的,通常留给其默认着色器。

在现代 OpenGL 中,我们至少需要定义我们自己的顶点和片元着色器(GPU 上没有默认顶点/片元着色器)。因此,开始学习现代 OpenGL 通常相当困难,因为在能够呈现您的第一个三角形之前需要大量的知识。

一、顶点输入

OpenGL 是一个 3D 图形库,因此我们在 OpenGL 中指定的所有坐标均以 3D(X,Y和Z坐标)进行。OpenGL 不会简单地将所有3D 坐标转换为 2D 像素;OpenGL 仅处理位于所有 3个 坐标轴 -1.0和1.0之间的特定范围内的 3D 坐标。

因为我们想要渲染一个三角形,所以指定三个顶点,每个顶点都有一个 3D 位置。我们在一个float数组中的规范化设备坐标(OpenGL 的可见区域)定义它们:

float vertices[] = {
   
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};  

由于 OpenGL 在 3D 空间中工作,而我们呈现一个 2D 三角形,所以每个顶点Z坐标为0.0。这样,三角形的深度保持不变,使其看起来像是 2D。

定义了顶点数据后,我们希望将其作为输入发送到图形管道的第一个过程:顶点着色器。这是通过在存储顶点数据的GPU 上创建内存来完成的,配置 OpenGL 应如何解释内存并指定如何将数据发送到显卡。然后,顶点着色器处理内存中尽可能多的顶点。

我们通过所谓的顶点缓冲对象(Vertex Buffer Object,简称VBO)可以在GPU的内存中存储大量顶点。使用这个缓冲对象的好处是我们可以将大量数据同时发送到显卡中,如果有足够的内存,并可以保存在显卡中,避免一次发送一个顶点的数据。从 CPU 向显卡发送数据的速度相对较慢,所以我们尽可能尝试同时发送尽可能多的数据。一旦数据进入显卡内存,顶点着色器几乎可以立即访问顶点数据,其速度非常快。

二、创建VBO

OpenGL中有太多的对象,与 OpenGL 中的任何对象一样,顶点缓冲对象具有与该缓冲区相对应的唯一ID,因此我们可以使用glGenBuffers函数生成具有缓冲区功能的 ID:

unsigned int VBO;
glGenBuffers(1, &VBO);  // 生成唯一的ID

OpenGL 具有多种类型的缓冲对象,顶点缓冲对象的缓冲类型为GL_ARRAY_BUFFER。OpenGL 允许我们同时绑定到多个缓冲区,只要它们具有不同的缓冲类型。我们可以将新创建的缓冲区与目标绑定:

glBindBuffer(GL_ARRAY_BUFFER, VBO);  // 指定为顶点缓存对象

此时我们所进行的任何缓冲调用(在GL_ARRAY_BUFFER目标上)将用于配置当前绑定缓冲区,即VBO。然后,我们通过glBufferData函数,将先前定义的顶点数据vertices复制到缓冲区的内存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  //将顶点数据复制到VBO中

glBufferData是一种专门用于将用户定义的数据复制到当前绑定缓冲区中的函数。它的第一个参数是我们要将数据复制到的缓冲区类型:当前绑定到GL_ARRAY_BUFFER目标的顶点缓冲对象。第二个参数指定了我们指定传递给缓冲区的数据大小(以字节为单位),一个简单的sizeof顶点数据。第三个参数是我们要发送的实际数据。

第四个参数指定了我们希望图形显卡如何管理给定数据。这可以采取 3 种形式:

  • GL_STREAM_DRAW:数据仅设置一次,最多由GPU使用几次
  • GL_STATIC_DRAW:数据只设置一次,并多次使用
  • GL_DYNAMIC_DRAW:数据更改了很多,并使用了很多次

三角形的位置数据不会改变,被大量使用,并且每次渲染调用都保持不变,所以它的使用类型最好是GL_STATIC_DRAW。例如,如果一个缓冲器中的数据可能会频繁变化,则使用类型GL_DYNAMIC_DRAW可确保图形显卡将数据放在内存中,从而能够更快地写入。

到目前为止,我们将顶点数据存储在显卡内存中,由名为VBO的顶点缓冲区对象管理。接下来,我们希望创建一个实际处理这些数据的顶点和片元着色器。

三、顶点着色器

顶点着色器是可以进行编程的着色器之一。如果我们想要做一些渲染,现代 OpenGL 要求我们至少设置一个顶点和片元着色器,所以我们将简要地介绍着色器并配置两个非常简单的着色器来绘制我们的第一个三角形。

我们需要做的第一件事是用着色器语言GLSL(OpenGL着色语言)编写顶点着色器,然后编译这个着色器,以便我们可以在应用程序中使用它。下面是非常基本的顶点着色器的源代码:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
   
    gl_Position = vec4(aPos, 1.0);  // gl_Position为顶点着色器的输出
}

GLSL 看起来与 C 类似。每个着色器都以申明其版本开始,我们还明确提到使用的是核心配置文件模式。

使用关键字in声明顶点着色器中的所有输入顶点属性。现在我们只关心位置数据,所以我们只需要一个单一的顶点属性。GLSL 具有矢量数据类型,根据其后缀数字包含 1 到 4 个浮动。由于每个顶点都有一个 3D 坐标,我们创建一个vec3名的输入变量,我们还通过*layout (location = 0)*设置输入变量的位置。

为了设置顶点着色器的输出,我们必须将位置数据分配给预定义变量gl_Position,这是一个vec4的变量。在主函数结束时,无论我们将gl_Position设置什么都将用作顶点着色器的输出。由于我们的输入是大小 3 的矢量,我们必须将此转换为 大小为4的矢量。我们可以通过vec3在构造器中插入值vec4并将其w组件设置为1.0f来实现。

当前顶点着色器可能是我们可以想象到的最简单的顶点着色器,因为我们没有对输入数据进行任何处理,只是将其转发到着色器的输出。在实际应用中,输入数据通常尚未处于规范化设备坐标中,因此我们首先必须将输入数据转换为位于 OpenGL 可见区域内的坐标。

编译着色器

我们获取顶点着色器的源代码存储在代码文件顶部的 const C 字符串中:

const char *vertexShaderSource = "#version 330 core\n"
                                 "layout (location = 0) in vec3 aPos;\n"
                                 "void main()\n"
                                 "{\n"
                                 "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
                                 "}\0";

为了让 OpenGL 使用着色器,它必须在运行时从源代码动态编译它。我们需要做的第一件事是创建一个着色器对象,再次引用ID。因此,我们存储顶点着色器作为一个 unsigned int,并创建着色器:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

我们通过参数 GL_VERTEX_SHADER创建一个顶点着色器,
接下来我们将着色器源代码附加到着色器对象并编译着色器:

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

这着色器源函数将要编译的着色器对象作为其第一个参数。第二个参数指定我们作为源代码传递的字符串数量,只有一个。第三个参数是顶点着色器的实际源代码,我们可以将第四个参数保留为NULL。

您可能想在调用后检查编译着色器是否成功, 如果编译错误,按如下方式完成检查编译:首先,我们定义一个整数来表示成功和一个错误消息(如果有)的存储容器;然后我们调用glGetShaderiv检查编译是否成功; 如果编译失败,我们调用glGetShaderInfoLog 检索错误信息并打印错误信息。

int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success)
{
   
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

四、片元着色器

片元着色器是我们要为渲染三角形而创建的第二个也是最后一个着色器。片元着色器用于计算像素的颜色输出。为简单起见,片元着色器将始终输出橙色。

#version 330 core
out vec4 FragColor;

void main()
{
   
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

片元着色器只需要一个输出变量,它是一个大小为 4 的向量,它定义了我们应该自己计算的最终颜色输出。我们使用out关键字声明输出值,我们将其命名为FragColor。接下来,我们简单地将分配vec4给颜色输出作为具有 alpha 值1.0(1.0完全不透明)的橙色。

编译片元着色器的过程类似于顶点着色器,这次我们使用GL_FRAGMENT_SHADER常量作为着色器类型:

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

两个着色器现在都已编译,剩下要做的就是将两个着色器对象链接到一个着色器程序我们可以用于渲染。确保在这里也编译无错误!

 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
 if (!success)
 {
   
     glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
     std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
 }

五、着色器程序

着色器程序对象是多个着色器组合的最终链接版本。要使用最近编译的着色器,我们必须关联它们到一个着色器程序对象,然后在渲染对象时激活这个着色器程序。当我们发出渲染调用时,将使用激活的着色器程序的着色器。将着色器链接到程序时,它将每个着色器的输出链接到下一个着色器的输入。如果您的输出和输入不匹配,这也是您会收到链接错误的地方。

创建程序对象很容易:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

这创建程序函数创建一个程序并返回对新创建的程序对象的 ID 引用。现在我们需要将之前编译的着色器附加到程序对象,然后将它们链接到链接程序:

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

就像着色器编译一样,我们也可以检查链接着色器程序是否失败并检索相应的日志:

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) 
{
   
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}

一旦我们将它们链接到程序对象中,不要忘记删除着色器对象;我们不再需要它们了:


                
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值