上回通过用顶点着色器和片段着色器绘制了一个30像素大小的点,这次主要简单介绍一下各个着色器的功能,介绍检测OpenGL和GLSL错误的模板,以及从文件当中读取GLSL源代码的模板,最后绘制一个简单的二维动画。
一、各着色器功能
顶点着色器:所有的顶点数据都会被传入顶点着色器,顶点们会被一个一个的处理,即顶点着色器会对每个顶点执行一次。对拥有许多顶点的大型复杂规模而言,顶点着色器会执行成百上千甚至上万次,这些执行是并行的。顶点着色器一次只处理一个顶点。
曲面细分着色器:用以生成大量三角形,通常是网格形式,同时也提供了一些可以以各种方法操作这些三角形的工具。当简单形状上需要很多顶点时,曲面细分着色器能很好的发挥作用。
几何着色器:顶点着色器赋予程序员一次操作一个顶点的能力,而几何着色器赋予程序员一次处理一个图元的能力(最常用的图元是三角形)。当到达几何着色阶段时,管线肯定已经完成了将顶点组合为三角形的过程(图元组装),接下来几何着色器会让程序员可以同时访问每个三角形的所有顶点。按图元处理有很多用途,如可以让图元变形,还可以删除图元,同时几何着色器也提供了生成额外图元的方法,几何着色器也能在物体上增加表面纹理。
光栅化:3D模型中的点、三角形、颜色等全部都要展现在一个2D显示器上。这个2D屏幕由光栅(矩阵像素阵列)组成。当3D物体光栅化后,OpenGL将物体中的图元(通常是三角形)转化为片段。片段拥有关于像素的信息。光栅化过程确定了用以显示3个顶点所确定的三角形的所有像素需要绘制的位置。
片段着色器:片段着色器用于为光栅化的像素指定颜色。片段着色器还提供了其他计算颜色的方式,比如可以基于像素位置决定输出颜色等。片段着色器一次操作一个像素。
二、检测OpenGL和GLSL错误
编译和运行GLSL代码与普通代码的过程不同,GLSL编译总发生在C++运行时,且GLSL运行在GPU上,因此操作系统不总是能捕获OpenGL运行时的错误(GLSL错误不会导致C++程序崩溃)。这时,我们通常需要将有关GLSL的日志打印出来,其中glGetShaderiv()和glGetProgramiv()用于提供有关编译过的GLSL着色器和程序信息。以下提供了三个用于捕获和显示GLSL错误的模块。
checkOpenGLError:检查OpenGL错误标志,即是否发生OpenGL错误,既可以用于检测GLSL编译错误,又可以检测OpenGL运行时的错误。
paintShaderLog:当GLSL编译失败时,显示OpenGL日志内容。
paintProgramLog:当GLSL链接失败时,显示OpenGL日志内容。
void printShaderLog(GLuint shader)
{
int len = 0;
int chWrittn = 0;
char *log;
glGetShaderiv(shader,GL_INFO_LOG_LENGTH,&len);
if(len > 0){
log = (char*)malloc(len);
glGetShaderInfoLog(shader,len,&chWrittn,log);
cout<<"shader Info Log:"<<log<<endl;
free(log);
}
}
void printProgramLog(int prog)
{
int len = 0;
int chWrittn = 0;
char *log;
glGetProgramiv(prog,GL_INFO_LOG_LENGTH,&len);
if(len > 0){
log = (char*)malloc(len);
glGetProgramInfoLog(prog,len,&chWrittn,log);
cout<<"Program Info Log:"<<log<<endl;
free(log);
}
}
bool checkOpenGLError()
{
bool foundError = false;
int glErr = glGetError();
while(glErr != GL_NO_ERROR){
cout<<"glError:"<<glErr<<endl;
foundError = true;
glErr = glGetError();
}
return foundError;
}
应用示例如下:
//捕获编译着色器时的错误
glCompileShader(vShader);//编译着色器
checkOpenGLError();
glGetShaderiv(vShader,GL_COMPILE_STATUS,&vertCompiled);//GLint vertCompiled
if(vertComplied != 1){
cout<<"vertex compilation failed:"<<endl;
printShaderLog(vShader);
}
//捕获链接时的着色器错误
glAttachShader(vfProgram,vShader);
glAttachShader(vfProgram,fShader);
glLinkProgram(vfProgram);
checkOpenGLError();
glGetProgramiv(vfProgram,GL_LINK_STATUS,&linked);//GLint linked
if(linked != 1){
cout<<"linking failed"<<endl;
printProgramLog(vfProgram);
}
三、从文件读取GLSL源代码
当程序变得复杂时,将GLSL着色器代码内联存储到字符串中就显得不太实际,应该将GLSL代码放到文件(.glsl) 当中,并用读取文件的方式读取GLSL代码。下面提供了一个读取着色器代码的模块。readShaderSource()读取着色器文本并返回一个字符串数组,其中的每个字符串是文件中的一行文本。根据读入的行数确定数组的大小。
QString MyWidget::readShaderSource(const char*filePath)
{
QString content;
ifstream fileStream(filePath, ios::in);
QString line = "";
while(!fileStream.eof()){
getLine(fileStream,line);
content.append(line+"\n");
}
fileStream.close();
return content;
}
四、简单的三角形动画
与上一回中绘制一个点的程序类似,只需要稍微修改顶点着色器代码便可以绘制三角形,再添加变量控制动画即可。需要注意的是,使用glUniform1f函数需要引入glew库,但是Qt Creator本身不具备这个库,需要添加库之后在.pro文件中将库连接起来,否则编译不成功。
mywidget.h
#ifndef MYWIDGET_H
#define MYWIDGET_H
#define numVAOs 1
#include<GL/glew.h> //该头文件一定要在最前面
#include<QOpenGLWidget>
#include<QOpenGLFunctions>
#include<QOpenGLBuffer>
#include<QOpenGLShader>
#include<QOpenGLShaderProgram>
#include<GL/glfw3.h>
#include<GL/gl.h>
#include<GL/glu.h>
//继承OpenGLWidget,重写initializeGL(),paintGL(),resizeGL()三个函数即可绘制OpenGL图元
class MyWidget : public QOpenGLWidget,protected QOpenGLFunctions
{
public:
MyWidget(QWidget *parent);
GLuint createShaderProgram();//GLuint相当于C++里的unsinged int类型
public slots://定义槽函数
void animate();
protected:
void initializeGL() override;
void paintGL() override;
void resizeGL(int width, int height) override;
private:
GLuint renderingProgram;
GLuint vao[numVAOs];
float x = 0.0f;
float inc = 0.01f;
};
#endif // MYWIDGET_H
mywidget.cpp
#include "mywidget.h"
//#include"checkerror.h"
#include<QOpenGLVertexArrayObject>
#include<QString>
#include<iostream>
#include<fstream>
MyWidget::MyWidget(QWidget *parent)
{
Q_UNUSED(parent);
QSurfaceFormat format = QSurfaceFormat::defaultFormat();
format.setProfile(QSurfaceFormat::CoreProfile);
format.setVersion(4, 3);
QSurfaceFormat::setDefaultFormat(format);
}
void MyWidget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(1.0,0.0,0.0,1.0);//设置背景色为红色
glClear(GL_COLOR_BUFFER_BIT);
renderingProgram = createShaderProgram();
//glGenVertexArrays(numVAOs,vao);
//glBindVertexArray(vao[0]);
}
void MyWidget::paintGL()
{
glClear(GL_DEPTH_BOUNDS_EXT);
glClearColor(1.0,0.0,0.0,1.0);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(renderingProgram);
//每次更新偏移量
x += inc;
if(x > 1.0f) inc = -0.02f;
if(x < -1.0f) inc = 0.02f;
//获取顶点着色器中uniform类全局变量offset的位置
GLuint offsetLoc = glGetUniformLocation(renderingProgram,"offset");
//将x的值赋给uniform类变量offset
glUniform1f(offsetLoc,x);
//将3个顶点绘制为三角形
glDrawArrays(GL_TRIANGLES,0,3);
}
void MyWidget::resizeGL(int width, int height)
{
}
GLuint MyWidget::createShaderProgram()
{
//修改顶点着色器使之变为三角形
//使用unifom类的全局变量offset控制动画
const char *vshaderSource =
"#version 430 \n"
"uniform float offset; \n"
"void main(void) \n"
"{ if(gl_VertexID ==0) gl_Position = vec4(0.25+offset , -0.25, 0.0, 1.0);\n"
"else if(gl_VertexID == 1) gl_Position = vec4(-0.25+offset ,-0.25,0.0,1.0);\n"
"else gl_Position = vec4(0.0 +offset,0.25,0.0,1.0);}";
//顶点沿着管线移动到光栅着色器,它们会在这里被转化为像素(片段)位置,最终这些像素(片段)到达片段着色器
//片段着色器的目的就是将要展示的像素赋予RGB颜色
//"out"标签表明color变量是输出变量
//此处vec4前三个元表示RGB颜色,第四个元表示不透明度
const char *fshaderSource =
"#version 430 \n"
"out vec4 color; \n"
"void main(void) \n"
"{ color = vec4(0.0,0.0,1.0,1.0);}";
//调用glCreateShader(parameter)创建类型为parameter的空着色器
//创建每个着色器对象后会返回一个整数ID作为后面引用它们的序号
GLuint vShader = glCreateShader(GL_VERTEX_SHADER);//GLuint相当于“unsigned int"
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
//glShaderSource将GLSL代码从字符串载入空着色器对象中
//四个参数:1、存放着色器的着色器对象,2、着色器源代码中的字符串数量,3、包含源代码的字符串指针,4、
glShaderSource(vShader,1,&vshaderSource,NULL);
glShaderSource(fShader,1,&fshaderSource,NULL);
//glCompileShader编译着色器
glCompileShader(vShader);
glCompileShader(fShader);
//创建程序对象,并存储指向它的整数ID
//OpenGL的程序对象包含一系列编译过的着色器,使用glAttachShader将着色器加入程序对象
//之后使用glLinkProgram来请求GLSL编译器来确保它们的兼容性。
GLuint vfProgram = glCreateProgram();
glAttachShader(vfProgram,vShader);
glAttachShader(vfProgram,fShader);
glLinkProgram(vfProgram);
return vfProgram;
}
void MyWidget::animate()
{
this->update();//调用update时会自动调用PaintGL函数重绘
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include"mywidget.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
MyWidget *my_widget;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<QTimer>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//创建OpenGLWidget对象
my_widget = new MyWidget(this);
//将mainWindow界面设置为OpenGLWidget
setCentralWidget( my_widget );
//设置界面的大小
resize( 1000, 800 );
//设置计时器并连接槽函数
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, my_widget, &MyWidget::animate);
timer->start(10);//设置重绘的时间间隔
}
MainWindow::~MainWindow()
{
delete ui;
}
main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
运行结果: