Qt中使用OpenGL(六)

本章中,在三维世界中引入光照

现实中的光照

在 OpenGL 中,光照也可以通过数学手段进行模拟。

冯氏光照模型主要由三个分量构成:

  • 环境光(Ambient):即使在黑暗情况下,也仍有一些光亮(远处的光)
  • 漫反射(Diffuse):模拟光源对物体的方向性影响
  • 镜面光照(Specular):模拟有光泽物体上面出现的亮点

为了确定平面的朝向,还需要法线进行帮助,有了法线,就可以计算这个平面时朝向光源还是背向光源。简单来讲,就是计算一下法线向量和从平面上一个点到光源方向的点乘,也就是 cos 值。cos 值大于 0 ,则说明当前是面向光源。

  1. 在顶点信息中增加法线的概念,以便计算光是否照在这个顶点所在平面
  2. 需要材质的概念,来模拟 环境光、漫反射与镜面反射。
1
2
3
4
5
6
7
8
9
10
11
12
struct Vertex {
QVector3D pos;
QVector2D texture;
QVector3D normal;
};

struct Material {
QVector3D ambient = QVector3D(0.24725, 0.1995, 0.0745);
QVector3D diffuse = QVector3D(0.75164, 0.60648, 0.22648);
QVector3D specular = QVector3D(0.628281, 0.555802, 0.366065);
float shininess = 0.4;
};

normal 就是法线。

然后为 model 添加材质接口与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// model.h
#ifndef MODEL_H
#define MODEL_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions_4_5_Core>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include "Camera.h"
#include "light.h"

struct Vertex {
QVector3D pos;
QVector2D texture;
QVector3D normal;
};

struct Material {
QVector3D ambient = QVector3D(0.24725, 0.1995, 0.0745);
QVector3D diffuse = QVector3D(0.75164, 0.60648, 0.22648);
QVector3D specular = QVector3D(0.628281, 0.555802, 0.366065);
float shininess = 0.4;
};

const static float VertexFloatCount = 8;

class Model : public QOpenGLFunctions_4_5_Core
{
public:
Model();
~Model();

void setScale(float val)
{
m_scale = val;
}
void setRotate(const QVector3D &rotate)
{
m_rotate = rotate;
}
void setPos(const QVector3D &pos)
{
m_pos = pos;
}
float scale()
{
return m_scale;
}
QVector3D rotate()
{
return m_rotate;
}
QVector3D pos()
{
return m_pos;
}

QVector3D getRotate()

{
return m_rotate;
}
void setVertices(const QVector<Vertex> &vertices)
{
m_vertices = vertices;
}
void setTexture(QOpenGLTexture *texture, int index = -1);
void setMaterial(const Material &material, int index = -1);
void setShaderProgram(QOpenGLShaderProgram *program)
{
m_program = program;
}
void setCamera(Camera *camera)
{
m_camera = camera;
}
void setProjection(const QMatrix4x4 &projection)
{
m_projection = projection;
}
void setView(const QMatrix4x4 &view)
{
m_view = view;
}
void setLight(Light *light)
{
m_light = light;
}

QMatrix4x4 model();

virtual void init();
virtual void update();
virtual void paint();
protected:
QVector3D m_pos{ 0, 0, 0 };
QVector3D m_rotate{ 0, 0, 0 };
float m_scale = 1;
QVector<Vertex> m_vertices;
QMap<int, QOpenGLTexture *> m_textures;
QMap<int, Material> m_materials;
QOpenGLVertexArrayObject m_vao;
QOpenGLBuffer m_vbo;
QOpenGLShaderProgram *m_program = nullptr;

QMatrix4x4 m_projection;
QMatrix4x4 m_view;
Camera *m_camera = nullptr;
Light *m_light = nullptr;

};

#endif // MODEL_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// model.cpp
#include "model.h"

Model::Model()
{
}

Model::~Model()
{
}

void Model::setTexture(QOpenGLTexture *texture, int index)
{
if(index == -1) {
if(m_textures.isEmpty()) {
index = 0;
} else {
index = m_textures.keys().last() + 1;
}
}
m_textures.insert(index, texture);
}

void Model::setMaterial(const Material &material, int index)
{
if(index == -1) {
if(m_materials.isEmpty()) {
index = 0;
} else {
index = m_materials.keys().last() + 1;
}
}
m_materials.insert(index, material);
}

QMatrix4x4 Model::model()
{
QMatrix4x4 _mat;
_mat.setToIdentity();
_mat.translate(m_pos);
_mat.rotate(m_rotate.x(), 1, 0, 0);
_mat.rotate(m_rotate.y(), 0, 1, 0);
_mat.rotate(m_rotate.z(), 0, 0, 1);
_mat.scale(m_scale);
return _mat;
}

void Model::init()
{
}

void Model::update()
{
}

void Model::paint()
{
}

设置法线和材质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
setVertices({
// 1
{{-1, 1, 1,}, {0.50, 0.25}, {0, 0, 1}}, // 左上
{{-1, -1, 1,}, {0.50, 0.50}, {0, 0, 1}}, // 左下
{{ 1, -1, 1,}, {0.75, 0.50}, {0, 0, 1}}, // 右下
{{ 1, 1, 1,}, {0.75, 0.25}, {0, 0, 1}}, // 右上
// 6
{{ 1, 1, -1,}, {0.00, 0.25}, {0, 0, -1}}, // 左上
{{ 1, -1, -1,}, {0.00, 0.50}, {0, 0, -1}}, // 左下
{{-1, -1, -1,}, {0.25, 0.50}, {0, 0, -1}}, // 右下
{{-1, 1, -1,}, {0.25, 0.25}, {0, 0, -1}}, // 右上
// 2
{{ 1, 1, 1,}, {0.75, 0.25}, {1, 0, 0}}, // 左上
{{ 1, -1, 1,}, {0.75, 0.50}, {1, 0, 0}}, // 左下
{{ 1, -1, -1,}, {1.00, 0.50}, {1, 0, 0}}, // 右下
{{ 1, 1, -1,}, {1.00, 0.25}, {1, 0, 0}}, // 右上
// 5
{{-1, 1, -1,}, {0.25, 0.25}, {-1, 0, 0}}, // 左上
{{-1, -1, -1,}, {0.25, 0.50}, {-1, 0, 0}}, // 左下
{{-1, -1, 1,}, {0.50, 0.50}, {-1, 0, 0}}, // 右下
{{-1, 1, 1,}, {0.50, 0.25}, {-1, 0, 0}}, // 右上
// 3
{{-1, 1, -1,}, {0.00, 0.00}, {0, 1, 0}}, // 左上
{{-1, 1, 1,}, {0.00, 0.25}, {0, 1, 0}}, // 左下
{{ 1, 1, 1,}, {0.25, 0.25}, {0, 1, 0}}, // 右下
{{ 1, 1, -1,}, {0.25, 0.00}, {0, 1, 0}}, // 右上
// 4
{{-1, -1, 1,}, {0.00, 0.50}, {0, -1, 0}}, // 左上
{{-1, -1, -1,}, {0.00, 0.75}, {0, -1, 0}}, // 左下
{{ 1, -1, -1,}, {0.25, 0.75}, {0, -1, 0}}, // 右下
{{ 1, -1, 1,}, {0.25, 0.50}, {0, -1, 0}}, // 右上
});

设置材质

1
2
3
4
5
auto _texture = new QOpenGLTexture(QImage("path/to/texture.png"));
_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
_texture->setMagnificationFilter(QOpenGLTexture::Linear);
setTexture(_texture);
setMaterial({QVector3D(0.24725, 0.1995, 0.0745), QVector3D(0.75164, 0.60648, 0.22648), QVector3D(0.628281, 0.555802, 0.366065), 0.4});

使用 shader 计算光照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// vertex.shader
#version 450 core

in vec3 vPos;
in vec2 vTexture;
in vec3 vNormal;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
FragPos = vec3(model * vec4(vPos, 1.0));
Normal = mat3(transpose(inverse(model))) * vNormal;
TexCoords = vTexture;

gl_Position = projection * view * model * vec4(vPos, 1.0);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// fragment.shader
#version 450 core

out vec4 FragColor;

struct Material {
sampler2D texture;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};

struct Light {
vec3 position;
vec3 color;
};

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

uniform vec3 viewPos;
uniform Material material;
uniform Light light;

void main()
{
vec3 materialTexColor = vec3(texture(material.texture, TexCoords));

// ambient
vec3 ambient = materialTexColor * material.ambient;

// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(light.position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * material.diffuse * materialTexColor * light.color;

// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = spec * material.specular * light.color;

vec3 result = ambient + diffuse + specular;

FragColor = vec4(result, 1.0);

}

其中

  • 环境光 = 环境光参数 * 纹理采样(vec3(texture(material.texture, TexCoords))
  • 漫反射 = 法线(normalize(Normal))和朝向光源向量(light.position - FragPos)的点乘 乘以 漫反射参数 乘以 纹理采样 乘以 光(light.color
  • 镜面反射 : 先用光的方向和法线方向计算出反射光线方向(vec3 lightDir = reflect(-lightDir, norm)),然后计算视角朝向方向(vec3 viewDir = normalize(viewPos - FragPos)),然后使用 pow 函数计算光斑(float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess)),最终镜面反射 = 光斑 乘以 镜面反射参数 乘以 光
  • 最终结果 = 环境光 + 漫反射 + 镜面反射

然后加载 shader

1
2
3
4
auto _program = new QOpenGLShaderProgram();
_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vertex.shader");
_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fragment.shader");
setShaderProgram(_program);

初始化带法线的顶点缓存

修改 init() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void Dice::init()
{
initializeOpenGLFunctions();
if(!m_vao.isCreated()) {
m_vao.create();
}
if(!m_vbo.isCreated()) {
m_vbo.create();
}
if(!m_program->isLinked()) {
m_program->link();
}
if(m_vertexCount < m_vertices.count()) {
if(m_vertexBuffer) {
delete[] m_vertexBuffer;
}
m_vertexBuffer = new float[m_vertices.count() * VertexFloatCount];
m_vertexCount = m_vertices.count();
int _offset = 0;
for(auto &vertex : m_vertices) {
m_vertexBuffer[_offset] = vertex.pos.x();
_offset++;
m_vertexBuffer[_offset] = vertex.pos.y();
_offset++;
m_vertexBuffer[_offset] = vertex.pos.z();
_offset++;
m_vertexBuffer[_offset] = vertex.texture.x();
_offset++;
m_vertexBuffer[_offset] = vertex.texture.y();
_offset++;
m_vertexBuffer[_offset] = vertex.normal.x();
_offset++;
m_vertexBuffer[_offset] = vertex.normal.y();
_offset++;
m_vertexBuffer[_offset] = vertex.normal.z();
_offset++;
}
}
m_vao.bind();
m_vbo.bind();
m_vbo.allocate(m_vertexBuffer, sizeof (float)*m_vertices.count() * VertexFloatCount);
m_program->bind();
// 绑定顶点坐标信息 从 0*sizeof(flaot)开始读取 3 个 float, 一个顶点有 VertexFLoatCount 个 float 数据,所以下一个数据要偏移 VertexFloatCount * sizeof(float) 个字节
m_program->setAttributeBuffer("vPos", GL_FLOAT, 0 * sizeof (float), 3, VertexFloatCount * sizeof (float));
m_program->enableAttributeArray("vPos");
// 绑定纹理坐标信息 从 3*sizeof(float)开始读取 2 个 float,一个顶点有 VertexFLoatCount 个 float 数据,所以下一个数据要偏移 VertexFloatCount * sizeof(float) 个字节
m_program->setAttributeBuffer("vTexture", GL_FLOAT, 3 * sizeof (float), 2, VertexFloatCount * sizeof (float));
m_program->enableAttributeArray("vTexture");
m_program->setAttributeBuffer("vNormal", GL_FLOAT, 5 * sizeof (float), 3, VertexFloatCount * sizeof (float));
m_program->enableAttributeArray("vNormal");
m_program->release();
m_vbo.release();
m_vao.release();
}

修改 paint() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void Dice::paint()
{
for(auto index : m_textures.keys()) {
m_textures[index]->bind(index);
}
m_vao.bind();
m_program->bind();
// 绑定变换矩阵
m_program->setUniformValue("projection", m_projection);
m_program->setUniformValue("view", m_camera->GetViewMatrix());
m_program->setUniformValue("viewPos", m_camera->Position);
m_program->setUniformValue("model", model());
// 设置灯光位置和颜色
m_program->setUniformValue("light.position", m_light->pos());
m_program->setUniformValue("light.color", m_light->color().redF(), m_light->color().greenF(), m_light->color().blueF());
auto &_material = m_materials.value(0);
// 设置材质
m_program->setUniformValue("material.ambient", _material.ambient);
m_program->setUniformValue("material.diffuse", _material.diffuse);
m_program->setUniformValue("material.specular", _material.specular);
m_program->setUniformValue("material.shininess", _material.shininess);
// 绘制
for(int i = 0; i < 6; i++) {
glDrawArrays(GL_TRIANGLE_FAN, i * 4, 4);
}
m_program->release();
m_vao.release();
for(auto texture : m_textures) {
texture->release();
}
}

修改 OpenGLWidget 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class TreeOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
Q_OBJECT
public:
explicit TreeOpenGLWidget(QWidget *parent = nullptr);
~TreeOpenGLWidget();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
void wheelEvent(QWheelEvent *event);
void keyPressEvent(QKeyEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
signals:
public slots:
void on_timeout();
private:
QTimer m_timer;
QTime m_time;
Camera m_camera;
Light m_light;
QMatrix4x4 m_projection;

QMatrix4x4 m_view;
QVector<Model *> m_models;
Model *m_lightModel;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
TreeOpenGLWidget::TreeOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
connect(&m_timer, SIGNAL(timeout()), this, SLOT(on_timeout()));
m_timer.start(timeOutmSec);
m_time.start();
m_camera.Position = viewInitPos;
m_light.setPos({10, 3, 0});
m_light.setColor(QColor(255, 255, 255));
setFocusPolicy(Qt::StrongFocus);
}

TreeOpenGLWidget::~TreeOpenGLWidget()
{
if(!isValid()) {
return;
}
makeCurrent();
// 删除必要的
// glDeleteBuffers(1, &VBO);
// glDeleteVertexArrays(1, &VAO);
// glDeleteVertexArrays(1, &lightVAO);
doneCurrent();
}

void TreeOpenGLWidget::initializeGL()
{
initializeOpenGLFunctions();
glEnable(GL_DEPTH_TEST);
for (int i = 0; i < 3 ; i++ ) {
auto _dice = new Dice();
_dice->init();
_dice->setPos({0, i * 3.0f, 0});
_dice->setLight(&m_light);
_dice->setCamera(&m_camera);
m_models << _dice;
}
}

void TreeOpenGLWidget::resizeGL(int w, int h)
{
Q_UNUSED(w);
Q_UNUSED(h);
}

void TreeOpenGLWidget::paintGL()
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
m_projection.setToIdentity();
m_view.setToIdentity();
m_projection.perspective(m_camera.Zoom, (float)width() / height(), 0.1, 100);
m_view = m_camera.GetViewMatrix();
for(auto dice : m_models) {
dice->setProjection(m_projection);
dice->paint();
}
}

void TreeOpenGLWidget::wheelEvent(QWheelEvent *event)
{
m_camera.ProcessMouseScroll(event->angleDelta().y() / 120);
}

void TreeOpenGLWidget::keyPressEvent(QKeyEvent *event)
{
float deltaTime = timeOutmSec / 50.0f;
switch (event->key()) {
case Qt::Key_W:
m_camera.ProcessKeyboard(FORWARD, deltaTime);
break;
case Qt::Key_S:
m_camera.ProcessKeyboard(BACKWARD, deltaTime);
break;
case Qt::Key_A:
m_camera.ProcessKeyboard(LEFT, deltaTime);
break;
case Qt::Key_D:
m_camera.ProcessKeyboard(RIGHT, deltaTime);
break;
case Qt::Key_Q:
m_camera.ProcessKeyboard(UP, deltaTime);
break;
case Qt::Key_E:
m_camera.ProcessKeyboard(DOWN, deltaTime);
break;
case Qt::Key_Space:
m_camera.Position = viewInitPos;
break;
default:
break;
}
}

void TreeOpenGLWidget::mouseMoveEvent(QMouseEvent *event)
{
makeCurrent();
if(event->buttons() & Qt::RightButton) {
auto currentPos = event->pos();
QPoint deltaPos = currentPos - lastPos;
lastPos = currentPos;
m_camera.ProcessMouseMovement(deltaPos.x(), -deltaPos.y());
}
doneCurrent();
}

void TreeOpenGLWidget::mousePressEvent(QMouseEvent *event)
{
makeCurrent();
lastPos = event->pos();
doneCurrent();
}

void TreeOpenGLWidget::on_timeout()
{
float _speed = 0.1;
for(auto dice : m_models) {
float _y = dice->getRotate().y() + _speed;
if(_y >= 360) {
_y -= 360;
}
dice->setRotate({0, _y, 0});
_speed += 0.01;
}
// auto _h = m_light.color().hslHue() + 1;
// if(_h >= 360) {
// _h -= 360;
// }
// m_light.setColor(QColor::fromHsv(_h, 255, 255));
update();
}