背景
结合位置和颜色数据, 如何通过着色器渲染更加丰富的颜色。
顶点着色器
在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:
vertices={
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部
};
由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把aColor属性的位置值设置为1:
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
由于我们不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,我们必须再修改一下片段着色器:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
知道了现在使用的布局,我们就可以使用shaderProgram.setAttributeBuffer函数更新顶点格式。
之前我们是使用数据id来更新,为了方便理解,后面我们会使用变量名来进行更新。
我们更新initializeGL函数为:
void Widget::initializeGL()
{
this->initializeOpenGLFunctions(); //初始化opengl函数
if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){ //添加并编译顶点着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果编译出错,打印报错信息
}
if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){ //添加并编译片段着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果编译出错,打印报错信息
}
if(!shaderProgram.link()){ //链接着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果链接出错,打印报错信息
}
// VAO.create(); //生成VAO对象
// VAO.bind(); //绑定VAO,之后所以的顶点缓冲对象的操作都会存储到VAO中
QOpenGLVertexArrayObject::Binder{&VAO};
VBO.create(); //生成VBO对象
VBO.bind(); //将VBO绑定到当前的顶点缓冲对象(QOpenGLBuffer::VertexBuffer)中
//将顶点数据分配到VBO中,第一个参数为数据指针,第二个参数为数据的字节长度
VBO.allocate(vertices.data(),sizeof(float)*vertices.size());
shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 6);
shaderProgram.enableAttributeArray("aPos");
shaderProgram.setAttributeBuffer("aColor", GL_FLOAT,sizeof(GLfloat) * 3, 3, sizeof(GLfloat) * 6);
shaderProgram.enableAttributeArray("aColor");
// VAO.release(); //解绑VAO,安全使用
}
由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。
同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是 sizeof(float) * 3,用字节来计算就是12字节。
运行程序你应该会看到如下结果:
这个图片可能不是你所期望的那种,因为我们只提供了3个颜色,而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
这正是在这个三角形中发生了什么。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50000左右的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。
完整代码实践
** widget.h **
#ifndef WIDGET_H
#define WIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QTimer>
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
protected:
virtual void initializeGL() override;
virtual void resizeGL(int w,int h) override;
virtual void paintGL() override;
private:
QVector<float> vertices;
QOpenGLShaderProgram shaderProgram;
QOpenGLBuffer VBO;
QOpenGLVertexArrayObject VAO;
};
#endif // WIDGET_H
** widget.cpp **
#include "widget.h"
#include <QtMath>
#include <QTime>
Widget::Widget(QWidget *parent)
: QOpenGLWidget(parent)
, VBO(QOpenGLBuffer::VertexBuffer)
{
vertices={
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部
};
}
Widget::~Widget()
{
}
void Widget::initializeGL()
{
this->initializeOpenGLFunctions(); //初始化opengl函数
if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){ //添加并编译顶点着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果编译出错,打印报错信息
}
if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){ //添加并编译片段着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果编译出错,打印报错信息
}
if(!shaderProgram.link()){ //链接着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果链接出错,打印报错信息
}
// VAO.create(); //生成VAO对象
// VAO.bind(); //绑定VAO,之后所以的顶点缓冲对象的操作都会存储到VAO中
QOpenGLVertexArrayObject::Binder{&VAO};
VBO.create(); //生成VBO对象
VBO.bind(); //将VBO绑定到当前的顶点缓冲对象(QOpenGLBuffer::VertexBuffer)中
//将顶点数据分配到VBO中,第一个参数为数据指针,第二个参数为数据的字节长度
VBO.allocate(vertices.data(),sizeof(float)*vertices.size());
shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 6);
shaderProgram.enableAttributeArray("aPos");
shaderProgram.setAttributeBuffer("aColor", GL_FLOAT,sizeof(GLfloat) * 3, 3, sizeof(GLfloat) * 6);
shaderProgram.enableAttributeArray("aColor");
// VAO.release(); //解绑VAO,安全使用
}
void Widget::resizeGL(int w, int h)
{
this->glViewport(0,0,w,h); //定义视口区域
}
void Widget::paintGL()
{
this->glClearColor(0.1f,0.5f,0.7f,1.0f); //设置清屏颜色
this->glClear(GL_COLOR_BUFFER_BIT); //清除颜色缓存
shaderProgram.bind(); //使用shaderProgram着色程序
{
QOpenGLVertexArrayObject::Binder{&VAO};
this->glDrawArrays(GL_TRIANGLES,0,3); //使用以0开始,长度为3的顶点数据来绘制三角形
}
}
triangle.vert
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
triangle.frag
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}