目录
在QT中创建OpenGL工程
1,打开QT,创建QMainWindow工程
2,打开工程的.pro文件,添加以下代码
QT +=opengl
QT += core gui openglwidgets
QT += widgets
3,打开工程界面,创建新的类并继承QOpenGLWidget和QOpenGLFunctions_3_3_Core
MyOpenGLWidget::MyOpenGLWidget(QWidget *parent)
{
setParent(parent);
}
void MyOpenGLWidget::initializeGL()
{
// 1.初始化OpenGL函数,否则OpenGL函数不可调用
initializeOpenGLFunctions();
}
void MyOpenGLWidget::resizeGL(int w, int h)
{
}
void MyOpenGLWidget::paintGL()
{
// 2.initializeOpenGLFunctions();执行后,下面的函数才有执行的意义
// 设置窗口颜色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
QOpenGLWidget提供了显示集成到Qt应用程序中的OpenGL图形的功能,提供三个虚函数:
1)paintGL():渲染OpenGL场景。每当需要更新小部件时调用。
2)resizeGL ():设置OpenGL视区、投影等。每当小部件调整了大小时都会调用该视区(并且当它第一次显示时也会调用,因为所有新创建的小部件都会自动获得一个调整大小的事件)。
3)initializeGL():设置OpenGL呈现上下文,定义显示列表等。在第一次调用resizeGL ()或paintGL ()之前调用一次。
QOpenGLFunctions_3_3_Core封装了OpenGL 3.3 核心框架的方法。
4,打开mainwindowUI,进入设计界面,找到OpenGL Widget,并拖入主窗口中
右键右上角新建的widget组件,选择“提升为”,将改widget提升为步骤3中创建的MyOpenGLWidget
5,编译运行工程得到下图的效果就是正常运行
OpenGL绘制多边形基本流程
1,开辟内存记录多边形的顶点,法线,纹理坐标等信息
2,确定内存的读取方式
3,编写顶点着色器和片元着色器
1)顶点着色器:从内存中读取每一个面的顶点数据并计算处理得到最终的顶点数据;随后OpenGL内部光栅化得到这个面所覆盖的像素集合,并对每一个像素执行片元着色器程序
2)片元着色器:根据像素坐标,纹理坐标等信息确定该像素的颜色
4,调用OpenGL中的Draw方法绘制内存中的多边形
导入OBJ模型
1,OBJ模型文件格式解析:
1)#:表示注释
2)v:表示顶点坐标,后面跟着三个数字分别为xyz的坐标值
3)vn:表示法向量
4)vt:表示纹理坐标
5)f:表示构成一个面的 顶点/法线/纹理(v/vn/vt) 索引的集合,一个面通常由3或4个点组成
2,将模型文件中的数据导入OpenGL
1)新建四个vector容器vexCache, norCache, texCache, faceCache分别记录文件中的顶点,法线,纹理坐标,面的顶点索引信息
2)通过QT的QFile接口打开Obj模型文件,按行读取,根据每行第一个字母确定处理方式
·v:将后三个数字当作一个顶点存入vexCache
·vn:将后三个数字当中一个法向量存入norCache
·vt:将三个数字当作一个纹理坐标存入texCache
·f:将后四组数据(我用的OBJ文件一个面用4个顶点表示)中的每一组数据当作一组索引存 入faceCache(v/vn/vt)
3)创建一个保存所有数据的vector名为mMeshDataBuffer;遍历faceCache,将每一组索引中的顶点,纹理坐标,法向量的具体数据按照(x, y, z, u, v, xn, yn, zn)保存到mMeshDataBuffer中
4)OpenGL的四边形是按照两个三角形绘制的,为了不重复增加顶点数据,创建一个索引vector mMeshElement记录一个四边形面中两个三角形的顶点索引(两个三角形顶点索引顺序保持一致,比如一个四边形的索引是[1,2,3,4]则两个三角形分别为[1,2,3] [1,3,4])
5)创建VAO,VBO,将mMeshDataBuffer数据缓存导VBO中,并通过glVertexAttribPointer方法告诉OpenGL顶点,纹理,法线分别在内存的哪个位置
6)创建EBO,将mMeshElement数据缓存导EBO中,告诉OpenGL每一个面的顶点在VBO的哪个位置
// 读取OBJ文件 ------------------------------------------------------------------
if(vexPath.find_last_of(".obj") == std::string::npos) {
qDebug() << "file is not a obj file";
return;
}
QFile objFile(vexPath.c_str()); // 读取模型文件
if(!objFile.open(QIODevice::ReadOnly)){
qDebug() << "open" << vexPath << "failed";
return;
}
std::vector<std::tuple<float, float, float>> vexCache, norCache;
std::vector<std::tuple<float, float>> texCache;
std::vector<std::tuple<int, int, int>> faceCache;
while(!objFile.atEnd()) {
QByteArray line = objFile.readLine();
line = line.remove(line.length() - 2,2); // remove \r\n
if(line.isEmpty()) {
continue;
}
QList<QByteArray> valInLine = line.split(' ');
valInLine.removeAll(QByteArray());
QString dataType = valInLine.takeFirst();
if(dataType == "v") {
vexCache.push_back(std::make_tuple(valInLine.at(0).toFloat(), valInLine.at(1).toFloat(), valInLine.at(2).toFloat()));
}
else if(dataType == "vt") {
texCache.push_back(std::make_tuple(valInLine.at(0).toFloat(), valInLine.at(1).toFloat()));
}
else if(dataType == "vn") {
norCache.push_back(std::make_tuple(valInLine.at(0).toFloat(), valInLine.at(1).toFloat(), valInLine.at(2).toFloat()));
}
else if(dataType == "f") {
std::transform(valInLine.begin(),valInLine.end(),std::back_inserter(faceCache),[](QByteArray &str){
QList<QByteArray> intStr = str.split('/');
return std::make_tuple(intStr.first().toInt(),intStr.at(1).toInt(),intStr.last().toInt());
});
}
}
objFile.close();
for (auto face : faceCache) { // x y z u v xn yn zn
std::tuple<float, float, float> vex = vexCache[std::get<0>(face)-1];
mMeshDataBuffer.push_back(std::get<0>(vex));
mMeshDataBuffer.push_back(std::get<1>(vex));
mMeshDataBuffer.push_back(std::get<2>(vex));
std::tuple<float, float> tex = texCache[std::get<1>(face)-1];
mMeshDataBuffer.push_back(std::get<0>(tex));
mMeshDataBuffer.push_back(std::get<1>(tex));
std::tuple<float, float, float> nor = norCache[std::get<2>(face)-1];
mMeshDataBuffer.push_back(std::get<0>(nor));
mMeshDataBuffer.push_back(std::get<1>(nor));
mMeshDataBuffer.push_back(std::get<2>(nor));
}
for (int i = 0; i < mMeshDataBuffer.size() / 8; i = i + 4) {
mMeshElement.push_back(i);
mMeshElement.push_back(i + 1);
mMeshElement.push_back(i + 2);
mMeshElement.push_back(i);
mMeshElement.push_back(i + 2);
mMeshElement.push_back(i + 3);
}
// 数据导入OpenGL -----------------------------------------------------------------
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// 具体数据 x y z u v xn yn zn
glBufferData(GL_ARRAY_BUFFER, mMeshDataBuffer.size() * sizeof(float), mMeshDataBuffer.data(), GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(5 * sizeof(float)));
glEnableVertexAttribArray(2);
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, mMeshElement.size() * sizeof(float), mMeshElement.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
3,导入OBJ文件对应的纹理图片
1)创建OpenGL的texture
1)通过QImage类导入图片
2)将图片的数据导入OpenGL的texture中
// 图片纹理数据导入OpenGL
QImage textureSrc(mTexPath.c_str());
if (textureSrc.isNull()) {
qDebug() << "not find picture" << mTexPath;
return;
} else {
qDebug() << textureSrc.format();
}
textureSrc = textureSrc.convertToFormat(QImage::Format_RGB888);
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
textureSrc.width(), textureSrc.height(), 0, GL_RGB, GL_UNSIGNED_BYTE, textureSrc.bits());
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
4,编写着色器
//顶点着色器
#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 texCoord;
layout(location = 2) in vec3 normal;
out vec3 FragPos;
out vec2 TexCoords;
out vec3 Normals;
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
uniform mat4 model;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
TexCoords = texCoord;
FragPos = vec3(model * vec4(position, 1.0f));
Normals = normal;
};
// 片元着色器
#version 330 core
in vec3 FragPos;
in vec2 TexCoords;
in vec3 Normals;
out vec4 FragColor;
uniform sampler2D ourTexture;
uniform vec3 lightPos;
uniform vec3 viewPos;
void main()
{
vec2 modify = vec2(TexCoords.x, 1 - TexCoords.y);
//color = texture(ourTexture, modify);
vec3 color = texture(ourTexture, modify).rgb;
// Ambient
vec3 ambient = 0.5 * color;
// Diffuse
vec3 lightDir = normalize(lightPos - FragPos);
vec3 normal = normalize(Normals);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * color;
// Specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = 0.0;
vec3 halfwayDir = normalize(lightDir + viewDir);
spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
vec3 specular = vec3(0.3) * spec; // assuming bright white light color
FragColor = vec4(ambient + diffuse + specular, 1.0f);
}
OpenGL摄像机
要想在电脑屏幕上显示3D物体,需要将位于世界坐标下的多边形投影到表示屏幕的平面上。可以假设屏幕在世界坐标下是一个摄像机,摄像机的位置就是屏幕的位置,具体投影流程如下:
1,以摄像机朝向的方向向量a和正y轴向量叉乘得到右向量b,再让右向量和朝向的方向向量叉乘得到摄像机朝上的向量c,由a,b,c三个向量坐标转换矩阵lookAt,将多边形从世界坐标映射到以摄像机为中心的观察坐标中,lookAt矩阵可以通过
2,为了显示立体的效果(远小近大),需要使用透视投影project,把在观察坐标中的多边形投影到与摄像机朝向的方向向量垂直的平面上。
namespace CameraParam {
inline QMatrix4x4 project;
inline QMatrix4x4 view;
inline QVector3D cameraPos;
inline QVector3D front;
inline QVector3D up;
}
CameraParam::project.perspective(45, 1.0f, 0.1f, 1000.0f);
CameraParam::cameraPos = QVector3D(0.0f, 0.0f, 3.0f);
CameraParam::front = QVector3D(0.0f, 0.0f, -1.0f);
CameraParam::up = QVector3D(0.0f, 1.0f, 0.0f);
CameraParam::view = QMatrix4x4();
CameraParam::view.lookAt(CameraParam::cameraPos, CameraParam::cameraPos + CameraParam::front, CameraParam::up);
3,为了能够更好观察,可以通过QT监听键盘和鼠标输入调整摄像机的位置,从新的摄像机位置中获取步骤1中的a,b,c向量得到新的lookAt矩阵即可
void GLWidget::keyPressEvent(QKeyEvent *event)
{
GLfloat cameraSpeed = 0.1f;
if(event->key() == Qt::Key_W)
{
CameraParam::cameraPos += cameraSpeed * CameraParam::front;
}
if(event->key() == Qt::Key_S)
{
CameraParam::cameraPos -= cameraSpeed * CameraParam::front;
}
if(event->key() == Qt::Key_A)
{
QVector3D right = QVector3D::crossProduct(CameraParam::front, CameraParam::up).normalized();
CameraParam::cameraPos -= right * cameraSpeed;
}
if(event->key() == Qt::Key_D)
{
QVector3D right = QVector3D::crossProduct(CameraParam::front, CameraParam::up).normalized();
CameraParam::cameraPos += right * cameraSpeed;
}
}
bool isFirst = true;
int lastx = 0;
int lasty = 0;
float yaw = -90.0f;
float pitch = 0.0f;
void GLWidget::mouseMoveEvent(QMouseEvent *event)
{
if (isFirst) {
lastx = event->pos().x();
lasty = event->pos().y();
isFirst = false;
}
int x = event->pos().x();
int y = event->pos().y();
float xoffset = x - lastx;
float yoffset = lasty - y; // reversed since y-coordinates go from bottom to top
lastx = x;
lasty = y;
float sensitivity = 0.1f; // change this value to your liking
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
// make sure that when pitch is out of bounds, screen doesn't get flipped
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
QVector3D front;
front.setX(qCos(qDegreesToRadians(pitch)) * qCos(qDegreesToRadians(yaw)));
front.setY(qSin(qDegreesToRadians(pitch)));
front.setZ(qCos(qDegreesToRadians(pitch)) * qSin(qDegreesToRadians(yaw)));
CameraParam::front = front.normalized();
}
绘制天空盒
为了让背景不那么单调,使用天空盒(立方体贴图)填充背景
1,准备一组六个天空的全景图片
2,利用OpenGL以世界原点为中心,创建边长为1的正方体
3,将六张图片作为立方体贴图导入OpenGL
4,编写着色器
float skyboxVertices[] = {
// positions
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f
};
// 设置多边形
SkyBoxCube::SkyBoxCube()
{
initializeOpenGLFunctions();
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// 具体数据
glBufferData(GL_ARRAY_BUFFER, sizeof(skyboxVertices), skyboxVertices, GL_STATIC_DRAW);
// 解析方法
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// 开启location = 0的属性解析
glEnableVertexAttribArray(0);
// 解绑VBO和VAO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
std::vector<const GLchar*> faces;
faces.push_back("D:\\OpenGLTest\\QtProj\\SkyBox\\right.jpg");
faces.push_back("D:\\OpenGLTest\\QtProj\\SkyBox\\left.jpg");
faces.push_back("D:\\OpenGLTest\\QtProj\\SkyBox\\top.jpg");
faces.push_back("D:\\OpenGLTest\\QtProj\\SkyBox\\bottom.jpg");
faces.push_back("D:\\OpenGLTest\\QtProj\\SkyBox\\back.jpg");
faces.push_back("D:\\OpenGLTest\\QtProj\\SkyBox\\front.jpg");
texture = loadCubemap(faces);
shader.BuildShader("D:\\OpenGLTest\\QtProj\\SkyBox\\skyBox.vs", "D:\\OpenGLTest\\QtProj\\SkyBox\\skyBox.fs");
GLuint uniformBlockIndex = glGetUniformBlockIndex(shader.pgmId, "Matrices");
glUniformBlockBinding(shader.pgmId, uniformBlockIndex, 0);
}
//导入立方体贴图
GLuint SkyBoxCube::loadCubemap(std::vector<const GLchar*> faces)
{
GLuint textureID;
glGenTextures(1, &textureID);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
for(GLuint i = 0; i < faces.size(); i++)
{
//image = SOIL_load_image(faces[i], &width, &height, 0, SOIL_LOAD_RGB);
QImage textureSrc(faces[i]);
if (textureSrc.isNull()) {
qDebug() << "not find picture";
return -1;
}
textureSrc = textureSrc.convertToFormat(QImage::Format_RGB888);
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0,
GL_RGB, textureSrc.width(), textureSrc.height(), 0, GL_RGB, GL_UNSIGNED_BYTE, textureSrc.bits()
);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
return textureID;
}
// 顶点着色器
#version 330 core
layout(location = 0) in vec3 position;
out vec3 TexCoords;
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
mat4 removeTranslate(mat4 hasTranslate)
{
// 消除平移效果,让立方体始终在摄像机周围
for (int i = 0; i < 4; i++) {
hasTranslate[i][3] = 0;
hasTranslate[3][i] = 0;
}
hasTranslate[3][3] = 1;
return hasTranslate;
}
void main()
{
mat4 skyview = removeTranslate(view);
vec4 pos = projection * skyview * vec4(position, 1.0f);
gl_Position = pos.xyww; // 透视除法后得到的深度值为1,可以将天空盒放在最后绘制
TexCoords = position;
};
//片元着色器
#version 330 core
in vec3 TexCoords;
out vec4 color;
uniform samplerCube skybox;
void main()
{
color = texture(skybox, TexCoords);
}