Qt 可以通过一组基于 QOpenGLShaderProgram 的类来使用着色器。 此类允许编译、链接和执行用 GLSL 编写的着色器程序。您可以通过检查接受指向 OpenGL 上下文的指针的静态 QOpenGLShaderProgram::hasOpenGLShaderPrograms() 调用的结果来检查您的 OpenGL 实现是否支持着色器。所有现代硬件和所有不错的图形驱动程序都应该对着色器有一定的支持。
Qt 支持各种着色器,最常见的是顶点着色器和片段着色器。 这些都是经典 OpenGL 管道的一部分。 您可以在下图中看到管道的图示:
单个着色器由 QOpenGLShader 类的实例表示。您需要在此类的构造函数中指定着色器的类型。然后,您可以通过调用 QOpenGLShader::compileSourceCode() 或 QOpenGLShader::compileSourceFile() 来编译着色器的源代码,它具有许多用于处理不同输入格式的重载。QOpenGLShader 对象存储已编译着色器的 ID 以备将来使用。
当您定义了一组着色器时,您可以使用 QOpenGLShaderProgram::addShader() 组装一个完整的程序。添加完所有着色器后,您可以 link() 程序并将其 bind() 到当前的 OpenGL 上下文。程序类有许多方法来设置不同输入参数的值——单一和数组版本的统一和属性。
shaderglscene.h
#ifndef SHADERGLSCENE_H
#define SHADERGLSCENE_H
#include <QObject>
#include <QOpenGLShaderProgram>
#include "abstractglscene.h"
#include "sceneglwindow.h"
class ShaderGLScene : public QObject, public AbstractGLScene
{
Q_OBJECT
Q_PROPERTY(qreal angle READ angle WRITE setAngle)
public:
ShaderGLScene(SceneGLWindow *window);
void initialize();
void paint();
qreal angle() const;
void paintObject(const QMatrix4x4& mvMatrix);
public slots:
void setAngle(qreal a);
protected:
void initializeCubeData();
private:
bool m_initialized;
qreal m_angle = 0;
struct ScenePoint {
QVector3D coords;
QVector3D normal;
ScenePoint(const QVector3D &c = QVector3D(),
const QVector3D &n = QVector3D()) :
coords(c), normal(n)
{
}
};
QVector<ScenePoint> m_data;
QOpenGLShaderProgram m_shader;
//视图转换的三个矩阵
QMatrix4x4 m_modelMatrix;
QMatrix4x4 m_viewMatrix;
QMatrix4x4 m_projectionMatrix;
};
#endif // SHADERGLSCENE_H
shaderglscene.cpp
#include "shaderglscene.h"
#include <QFile>
#include "plyreader.h"
ShaderGLScene::ShaderGLScene(SceneGLWindow *window) : AbstractGLScene(window) {
window->setScene(this);
m_initialized = false;
initializeCubeData();
}
void ShaderGLScene::initialize() {
//将场景的背景颜色设置为黑色
AbstractGLScene::initialize();
m_initialized = true;
glClearColor(0, 0, 0, 0);
//从这些文件中读取着色器并链接着色器程序
m_shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/phong.vert");
m_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/phong.frag");
//link() 函数返回一个布尔值,这里跳过错误检查
m_shader.link();
//为着色器准备所有输入数据
//首先,着色器程序绑定到当前上下文
//以便我们可以对其进行操作
m_shader.bind();
//然后,我们启用两个属性数组的设置
//一个用于顶点坐标,另一个用于它们的法线
//数据存储在 QVector<ScenePoint> 中
//其中每个 ScenePoint 都有坐标和法线字段,因此坐标和法线没有单独的 C++ 数组
m_shader.setAttributeArray("Vertex", GL_FLOAT,
&m_data[0].coords, 3, sizeof(ScenePoint));
m_shader.enableAttributeArray("Vertex");
m_shader.setAttributeArray("Normal", GL_FLOAT,
&m_data[0].normal, 3, sizeof(ScenePoint));
m_shader.enableAttributeArray("Normal");
m_shader.setUniformValue("mat.ka", QVector3D(0.1, 0, 0.0));
m_shader.setUniformValue("mat.kd", QVector3D(0.7, 0.0, 0.0));
m_shader.setUniformValue("mat.ks", QVector3D(1.0, 1.0, 1.0));
m_shader.setUniformValue("mat.shininess", 128.0f);
m_shader.setUniformValue("light.position", QVector3D(2, 1, 1));
m_shader.setUniformValue("light.intensity", QVector3D(1, 1, 1));
}
void ShaderGLScene::paint() {
//我们重置投影矩阵
m_projectionMatrix.setToIdentity();
float aspectRatio = qreal(window()->width()) / window()->height();
//使用perspective()方法根据当前窗口大小给它一个透视变换
m_projectionMatrix.perspective(90, aspectRatio, 0.5, 40);
//视图矩阵也被重置,并使用lookAt()方法为相机准备变换
//中心值表示眼睛正在看的视图的中心。 向上矢量指示相机的垂直方向
m_viewMatrix.setToIdentity();
QVector3D eye(0, 0, 2);
QVector3D center(0, 0, 0);
QVector3D up(0, 1, 0);
m_viewMatrix.lookAt(eye, center, up);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, window()->width(), window()->height());
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
//我们首先设置模型矩阵,它决定了渲染对象相对于世界中心的位置
//(在这种情况下,我们说它绕 y 轴旋转了 45 度)
m_modelMatrix.setToIdentity();
m_modelMatrix.scale(2);
m_modelMatrix.rotate(m_angle, 0, 1, 0);
m_modelMatrix.rotate(-90, 1, 0, 0);
//然后我们构建模型-视图矩阵(表示对象相对于相机的位置)并将其传递给paintObject()方法
QMatrix4x4 modelViewMatrix = m_viewMatrix * m_modelMatrix;
paintObject(modelViewMatrix);
}
qreal ShaderGLScene::angle() const {
return m_angle;
}
//激活着色器程序,然后将所有需要的矩阵设置为着色器的Uniform
void ShaderGLScene::paintObject(const QMatrix4x4 &mvMatrix) {
m_shader.bind();
m_shader.setUniformValue("projectionMatrix", m_projectionMatrix);
m_shader.setUniformValue("modelViewMatrix", mvMatrix);
m_shader.setUniformValue("mvpMatrix", m_projectionMatrix*mvMatrix);
m_shader.setUniformValue("normalMatrix", mvMatrix.normalMatrix());
//glDrawArrays() 的调用,告诉它使用活动数组以 GL_TRIANGLES 模式渲染
glDrawArrays(GL_TRIANGLES, 0, m_data.size());
}
void ShaderGLScene::setAngle(qreal a) {
m_angle = a;
window()->update();
}
void ShaderGLScene::initializeCubeData() {
PlyReader reader(":/monkey.ply");
if(reader.read()) {
// reconstruct faces
const QVector<PlyReader::ScenePoint> vertices = reader.vertices();
const QVector<QVector<int>> faces = reader.faces();
for(const QVector<int>& face: faces) {
ScenePoint pt;
for(int vertexId: face) {
const PlyReader::ScenePoint &scenePoint = vertices.at(vertexId);
pt.coords = scenePoint.coord;
pt.normal = scenePoint.normal;
m_data << pt;
}
}
}
}
phong.vert
//我们声明了四个矩阵,代表场景坐标映射的不同阶段
uniform highp mat4 modelViewMatrix;
uniform highp mat3 normalMatrix;
uniform highp mat4 projectionMatrix;
uniform highp mat4 mvpMatrix;
//我们还定义了两个输入属性——顶点和法线
//它们包含顶点数据
attribute highp vec4 Vertex;
attribute mediump vec3 Normal;
//着色器将输出两条数据
//归一化顶点法线和相机看到的变换顶点坐标
varying mediump vec3 N;
varying highp vec3 v;
void main(void)
{
N = normalize(normalMatrix * Normal);
v = vec3(modelViewMatrix * Vertex);
//我们将 gl_Position 设置为最终的顶点坐标。
gl_Position = mvpMatrix * Vertex;
}
phong.frag
//着色器还声明了两个结构来保存光和材质信息
struct Material {
lowp vec3 ka;
lowp vec3 kd;
lowp vec3 ks;
lowp float shininess;
};
struct Light {
lowp vec4 position;
lowp vec3 intensity;
};
uniform Material mat;
uniform Light light;
//插值法线 (N) 和片段 (v) 位置
varying mediump vec3 N;
varying highp vec3 v;
//由于每个顶点的所有输入数据都是针对每个片段进行插值的,因此会为每个像素单独计算最终颜色。
void main(void) {
vec3 n = normalize(N);
vec3 L = normalize(light.position.xyz - v);
vec3 E = normalize(-v);
vec3 R = normalize(reflect(-L, n));
float LdotN = dot(L, n);
float diffuse = max(LdotN, 0.0);
vec3 spec = vec3(0, 0, 0);
if(LdotN > 0.0) {
float RdotE = max(dot(R, E), 0.0);
spec = light.intensity * pow(RdotE, mat.shininess);
}
//它会计算三个分量——环境光、漫射光和镜面反射——将它们加在一起
//并将其设置为gl_FragColor
vec3 color = light.intensity * (mat.ka + mat.kd * diffuse + mat.ks * spec);
gl_FragColor = vec4(color, 1.0);
}