(本文是LearnOpenGL的学习笔记, 教程中文翻译地址https://learnopengl-cn.github.io/(备用地址https://learnopengl-cn.readthedocs.io/zh/latest/),写于 2020-5-8 ,并在 2021-9-16 进行了更新)
0.前言
上一节学习了光照颜色(https://blog.csdn.net/gongjianbo1992/article/details/104176837),教程在本节主要讲了冯氏光照模型,代码在上一节的基础上改进。
1.知识点
现实世界的光照是极其复杂的,OpenGL的光照使用的是简化的模型,其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。如图所示(图片来自LearnOpenGL):
环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
(这一章的知识和Camera那章一样比较多,详情参照教程)
2.如何实现
环境光照。现实世界很多分散的光源,这里为了方便,使用单一的光源来计算。我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
漫反射光照。漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。为了能够更好的理解漫反射光照,请看下图(图片来自LearnOpenGL):
图左上方有一个光源,它所发出的光线落在物体的一个片段上。如果光线垂直于物体表面,这束光对物体的影响会最大化(更亮)。为了得到这个角度,我们需要两个东西:
- 法向量:一个垂直于顶点表面的向量。
- 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
法向量。法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。我们能够使用一个小技巧,使用叉乘对立方体所有的顶点计算法向量,但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中。目前片段着色器里的计算都是在世界空间坐标中进行的,所以应该把法向量也转换为世界空间坐标:
Normal = mat3(transpose(inverse(model))) * aNormal; //normal是我们传入的法线向量
传入法线数据和光源位置后(主要是修改物体着色器和传入相关数据),计算得到如下结果:
镜面光照。和漫反射光照一样,镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的。镜面光照是基于光的反射特性。如图(图片来自LearnOpenGL):
我们通过反射法向量周围光的方向来计算反射向量。然后我们计算反射向量和视线方向的角度差,如果夹角越小,那么镜面光的影响就会越大。我们是在世界空间进行光照计算的,为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替。
3.实现代码
(项目git链接:https://github.com/gongjianbo/OpenGLwithQtWidgets.git)
我的GLBasicLighting类实现效果:
GLBasicLighting类代码(和教程有区别的是,我用的四元数进行旋转):
#pragma once
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLTexture>
#include <QVector3D>
#include <QMatrix4x4>
#include <QQuaternion>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QTimer>
//光照基础
//QOpenGLWidget窗口上下文
//QOpenGLFunctions访问OpenGL接口,可以不继承作为成员变量使用
class GLBasicLighting
: public QOpenGLWidget
, protected QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
explicit GLBasicLighting(QWidget *parent = nullptr);
~GLBasicLighting();
protected:
//【】继承QOpenGLWidget后重写这三个虚函数
//设置OpenGL资源和状态。在第一次调用resizeGL或paintGL之前被调用一次
void initializeGL() override;
//渲染OpenGL场景,每当需要更新小部件时使用
void paintGL() override;
//设置OpenGL视口、投影等,每当尺寸大小改变时调用
void resizeGL(int width, int height) override;
//鼠标操作,重载Qt的事件处理
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
private:
void initShader();
private:
//着色器程序
QOpenGLShaderProgram lightingShader,lampShader;
//顶点数组对象
QOpenGLVertexArrayObject lightingVao,lampVao;
//顶点缓冲
QOpenGLBuffer vbo;
//
QVector3D rotationAxis;
QQuaternion rotationQuat;
//透视投影的fovy参数,视野范围
float projectionFovy{ 45.0f };
//鼠标位置
QPoint mousePos;
//旋转
QTimer timer;
int rotate{ 0 };
};
#include "GLBasicLighting.h"
#include <cmath>
#include <QtMath>
#include <QDebug>
GLBasicLighting::GLBasicLighting(QWidget *parent)
: QOpenGLWidget(parent)
{
connect(&timer,&QTimer::timeout,this,[this](){
rotate+=2;
if(isVisible()){
update();
}
});
timer.setInterval(50);
}
GLBasicLighting::~GLBasicLighting()
{
//initializeGL在显示时才调用,释放未初始化的会异常
if(!isValid())
return;
//QOpenGLWidget
//三个虚函数不需要makeCurrent,对应的操作已由框架完成
//但是释放时需要设置当前上下文
makeCurrent();
vbo.destroy();
lightingVao.destroy();
lampVao.destroy();
doneCurrent();
}
void GLBasicLighting::initializeGL()
{
//为当前上下文初始化OpenGL函数解析
initializeOpenGLFunctions();
initShader();
//方块的顶点和法向量
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
vbo=QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
vbo.create();
//light vao
lightingVao.create();
lightingVao.bind();
vbo.bind();
vbo.allocate(vertices,sizeof(vertices));
//setAttributeBuffer(int location, GLenum type, int offset, int tupleSize, int stride = 0)
lightingShader.setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(GLfloat) * 6);
lightingShader.enableAttributeArray(0);
lightingShader.setAttributeBuffer(1, GL_FLOAT, sizeof(GLfloat) * 3, 3, sizeof(GLfloat) * 6);
lightingShader.enableAttributeArray(1);
vbo.release();
lightingVao.release();
//lamp vao
lampVao.create();
lampVao.bind();
vbo.bind();
//setAttributeBuffer(int location, GLenum type, int offset, int tupleSize, int stride = 0)
lampShader.setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(GLfloat) * 6);
lampShader.enableAttributeArray(0);
vbo.release();
lampVao.release();
timer.start();
}
void GLBasicLighting::paintGL()
{
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
//清除深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//Z缓冲(Z-buffer),深度缓冲(Depth Buffer)。
glEnable(GL_DEPTH_TEST);
//draw lighting
lightingShader.bind();
lightingShader.setUniformValue("objectColor", QVector3D(1.0f,0.5f,0.31f));
lightingShader.setUniformValue("lightColor", QVector3D(1.0f,1.0f,1.0f));
QMatrix4x4 view; //观察矩阵
float radius = 10.0f;
view.translate(0.0f, 0.0f, -radius);
view.rotate(rotationQuat);
lightingShader.setUniformValue("view", view);
QMatrix4x4 projection; //透视投影
projection.perspective(projectionFovy, 1.0f * width() / height(), 0.1f, 100.0f);
lightingShader.setUniformValue("projection", projection);
QMatrix4x4 model;//模型矩阵
//model.rotate(30, QVector3D(1.0f, 1.0f, 0.0f)); //先不考虑旋转
lightingShader.setUniformValue("model", model);
//因为要获取灯的位置,所以提前算灯的model矩阵
model = QMatrix4x4();
float tx = std::sin(rotate*0.05) * 2.0f;
float tz = std::cos(rotate*0.05) * 2.0f;
model.translate(QVector3D(tx, 1.0f, tz));
//model.rotate(30, QVector3D(1.0f, 1.0f, 0.0f));
model.scale(0.3f);
QVector3D light_pos = model.map(QVector3D(0.0f, 0.0f, 0.0f));
QMatrix4x4 vv = view.inverted(); //逆矩阵求观察点位置
QVector3D view_pos = vv.map(QVector3D(0.0f, 0.0f, 0.0f));
lightingShader.setUniformValue("lightPos", light_pos);
lightingShader.setUniformValue("viewPos", view_pos);
lightingVao.bind();
glDrawArrays(GL_TRIANGLES, 0, 36);
lightingVao.release();
lightingShader.release();
//draw lamp
lampShader.bind();
lampShader.setUniformValue("view", view);
lampShader.setUniformValue("projection", projection);
//model = QMatrix4x4();
//model.translate(QVector3D(1.0f, 1.0f, 0.0f));
//model.rotate(30, QVector3D(1.0f, 1.0f, 0.0f));
//model.scale(0.3f);
lampShader.setUniformValue("model", model);
lampVao.bind();
glDrawArrays(GL_TRIANGLES, 0, 36);
lampVao.release();
lampShader.release();
}
void GLBasicLighting::resizeGL(int width, int height)
{
glViewport(0, 0, width, height);
}
void GLBasicLighting::mousePressEvent(QMouseEvent *event)
{
event->accept();
mousePos = event->pos();
}
void GLBasicLighting::mouseReleaseEvent(QMouseEvent *event)
{
event->accept();
}
void GLBasicLighting::mouseMoveEvent(QMouseEvent *event)
{
event->accept();
//参照示例cube
QVector2D diff = QVector2D(event->pos()) - QVector2D(mousePos);
mousePos = event->pos();
QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized();
rotationAxis = (rotationAxis + n).normalized();
//不能对换乘的顺序
rotationQuat = QQuaternion::fromAxisAndAngle(rotationAxis, 2.0f) * rotationQuat;
update();
}
void GLBasicLighting::wheelEvent(QWheelEvent *event)
{
event->accept();
//fovy越小,模型看起来越大
if(event->delta() < 0){
//鼠标向下滑动为-,这里作为zoom out
projectionFovy += 0.5f;
if(projectionFovy > 90)
projectionFovy = 90;
}else{
//鼠标向上滑动为+,这里作为zoom in
projectionFovy -= 0.5f;
if(projectionFovy < 1)
projectionFovy = 1;
}
update();
}
void GLBasicLighting::initShader()
{
//lingting shader
//in输入,out输出,uniform从cpu向gpu发送
const char *lighting_vertex=R"(#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
out vec3 Normal;
out vec3 FragPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
FragPos = vec3(model * vec4(position, 1.0f));
Normal = mat3(transpose(inverse(model))) * normal;
})";
const char *lighting_fragment=R"(#version 330 core
in vec3 Normal;
in vec3 FragPos;
uniform vec3 objectColor;
uniform vec3 lightColor;
uniform vec3 lightPos;
uniform vec3 viewPos;
out vec4 FragColor;
void main()
{
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0f);
})";
//将source编译为指定类型的着色器,并添加到此着色器程序
if(!lightingShader.addCacheableShaderFromSourceCode(
QOpenGLShader::Vertex,lighting_vertex)){
qDebug()<<"compiler vertex error"<<lightingShader.log();
}
if(!lightingShader.addCacheableShaderFromSourceCode(
QOpenGLShader::Fragment,lighting_fragment)){
qDebug()<<"compiler fragment error"<<lightingShader.log();
}
//使用addShader()将添加到该程序的着色器链接在一起。
if(!lightingShader.link()){
qDebug()<<"link shaderprogram error"<<lightingShader.log();
}
//lamp shader
const char *lamp_vertex=R"(#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0f);
})";
const char *lamp_fragment=R"(#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0);
})"; // set alle 4 vector values to 1.0
if(!lampShader.addCacheableShaderFromSourceCode(
QOpenGLShader::Vertex,lamp_vertex)){
qDebug()<<"compiler vertex error"<<lampShader.log();
}
if(!lampShader.addCacheableShaderFromSourceCode(
QOpenGLShader::Fragment,lamp_fragment)){
qDebug()<<"compiler fragment error"<<lampShader.log();
}
if(!lampShader.link()){
qDebug()<<"link shaderprogram error"<<lampShader.log();
}
}
4.参考
LearnOpenGL:https://learnopengl-cn.github.io/02%20Lighting/02%20Basic%20Lighting/
博客(Qt+OpenGL):https://www.jianshu.com/p/bc40f5bd60f3
博客(Qt+OpenGL):https://blog.csdn.net/z136411501/article/details/80112880