OpenGL(QT平台)学习与实战(四)

编译链接着色器

顶点着色器

顶点着色器(Vertex Shader)是几个可编程着色器中的一个。如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。

我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它了。下面你会看到一个非常基础的GLSL顶点着色器的源代码:

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

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

可以看到,GLSL看起来很像C语言。每个着色器都起始于一个版本声明。OpenGL 3.3以及和更高版本中,GLSL版本号和OpenGL的版本是匹配的(比如说GLSL 420版本对应于OpenGL 4.2)。我们同样明确表示我们会使用核心模式。

下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量aPos。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。

向量(Vector)
在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.xvec.yvec.zvec.w来获取。注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。

为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数的最后,我们将gl_Position设置的值会成为该顶点着色器的输出。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。

当前这个顶点着色器可能是我们能想到的最简单的顶点着色器了,因为我们对输入数据什么都没有处理就把它传到着色器的输出了。在真实的程序里输入数据通常都不是标准化设备坐标,所以我们首先必须先把它们转换至OpenGL的可视区域内。

编译着色器

现在,我们暂时将顶点着色器的源代码硬编码在代码文件顶部的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,然后用glCreateShader创建这个着色器:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

我们把需要创建的着色器类型以参数形式提供给glCreateShader。由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER

下一步我们把这个着色器源码附加到着色器对象上,然后编译它:

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

glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。

你可能会希望检测在调用glCompileShader后编译是否成功了,如果没成功的话,你还会希望知道错误是什么,这样你才能修复它们。检测编译时错误可以通过以下代码来实现:

int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

首先我们定义一个整型变量来表示是否成功编译,还定义了一个储存错误消息(如果有的话)的容器。然后我们用glGetShaderiv检查是否编译成功。如果编译失败,我们会用glGetShaderInfoLog获取错误消息,然后打印它。

if(!success)
{
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

如果编译的时候没有检测到任何错误,顶点着色器就被编译成功了。

片段着色器

片段着色器(Fragment Shader)是第二个也是最后一个我们打算创建的用于渲染三角形的着色器。片段着色器所做的是计算像素最后的颜色输出。为了让事情更简单,我们的片段着色器将会一直输出橘黄色。

在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。这三种颜色分量的不同调配可以生成超过1600万种不同的颜色!

#version 330 core
out vec4 FragColor;

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

片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。声明输出变量可以使用out关键字,这里我们命名为FragColor。下面,我们将一个Alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。

编译片段着色器的过程与顶点着色器类似,只不过我们使用GL_FRAGMENT_SHADER常量作为着色器类型:

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

两个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。

着色器程序

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。

当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。

创建一个程序对象很简单:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

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

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

代码应该很清楚,我们把着色器附加到了程序上,然后用glLinkProgram链接。

就像着色器的编译一样,我们也可以检测链接着色器程序是否失败,并获取相应的日志。与上面不同,我们不会调用glGetShaderiv和glGetShaderInfoLog,现在我们使用:

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    ...
}

得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:

glUseProgram(shaderProgram);

在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。

对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。

范例程序:

#include "zjjopenglwidget.h"

//    创建VBO和VAO对象,并赋予ID
GLuint VBO, VAO;
//以数组的形式传递3个3D坐标作为图形 pipeline的输入
//OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的
//范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)
//范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。
GLfloat vertices[] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f, 0.5f, 0.0f
};

//着色器程序
unsigned int shaderProgram;
//顶点着色器
unsigned int vertexShader;
//片段着色器
unsigned int fragmentShader;

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";
const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
    "}\0";

//由于OpenGL是在3D空间中工作的,而我们渲染的是一个2D三角形,我们将它顶点的z坐标设置为0.0。
//这样子的话三角形每一点的深度(Depth,译注2)都是一样的,从而使它看上去像是2D的。
ZjjOpenGLWidget::ZjjOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{

}

void ZjjOpenGLWidget::initializeGL()
{
    initializeOpenGLFunctions();

//    创建一个VertexArrays并且给VAO一个编号(改变VAO的值)
    glGenVertexArrays(1, &VAO);
//    创建一个Buffers并且给VBO一个编号(改变VBO的值)
    glGenBuffers(1, &VBO);

//    绑定VBO和VAO对象
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);

//    为当前绑定到target的缓冲区对象创建一个新的数据存储
//    如果data不是null,则使用来自此指针的数据初始化数据存储
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//    告知显卡如何解析缓冲里的属性值
//    void glVertexAttribPointer(GLuint index, GLint size, GLenum type,
//        GLboolean normalized, GLsizei stride, const GLvoid* pointer);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
//    开启VAO管理的第一个属性值
    glEnableVertexAttribArray(0);

//    将上下文的GL_ARRAY_BUFFER对象设回默认的ID-0,相当于与所创建的VAO和VBO解绑
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

//    一旦我们重新绑定这个VAO和VBO到GL_ARRAY_BUFFER位置,这些选项就会重新生效,不需要重新设置

//    我们首先要做的是创建一个着色器对象,注意还是用ID来引用的。所以我们储存这个顶点着色器为unsigned int,
//    然后用glCreateShader创建这个着色器
    vertexShader = glCreateShader(GL_VERTEX_SHADER);

//    把这个着色器源码附加到着色器对象上
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//    编译着色器代码
    glCompileShader(vertexShader);

//    检测在调用glCompileShader后编译是否成功了
    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;
    }

//    片段着色器配置过程,与定点着色器类似
    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;
    }

    //    创建一个着色器程序对象,并返回新创建程序对象的ID引用
    shaderProgram = glCreateProgram();
//    把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

//    就像着色器的编译一样,我们也可以检测链接着色器程序是否失败,并获取相应的日志。
//    与上面不同,我们不会调用glGetShaderiv和glGetShaderInfoLog,现在我们使用:
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if(!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
//    对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
}

void ZjjOpenGLWidget::resizeGL(int w, int h)
{

}

void ZjjOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

//    调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象
    glUseProgram(shaderProgram);

    glBindVertexArray(VAO);
//    为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,OpenGL需要你去指定这些数据所表示的
//    渲染类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?
//    做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给OpenGL。
//    这是其中的几个:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

运行结果:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值