目录
- 功能分析(需求分析)
- 技术点分析
- OpenGL
- 立即渲染模式
- 可编程渲染管线模式
- QOpenGLWidget
- 派生类 glwidget逻辑
- glwidget.h
- glwidget.cpp
- 鼠标交互功能
- obj格式介绍
- 效果
- bunny
- Cayman_GT
功能分析(需求分析)
- 基于QT平台,使用OpenGL进行obj文件加载显示;
- 使用鼠标对场景进行缩放、移动、旋转交互;
技术点分析
OpenGL
OpenGL是基于C的,学习曲线比较抖,但是总的来说就是下面一幅图,
用语言简单的描述(个人理解,可能不太准确)是把cpu里内存里的3D数据,传输到显卡的内存里,以及如何转换成2D平面上像素点显示(也就是矩阵变换,在好多开源的框架里都进行了进一步封装,形成了渲染器、场景、相机等)。这个数据是顶点坐标、颜色等,传输跟送快递一样,除了要有数据本身之外,还要有其他信息,也就是要有个约定,到显卡拿到数据之后怎么解析。
有个网站比较有名learnopengl,可以对着学一遍,在网上找资料的时候,需要注意是立即渲染模式,还是可编程渲染管线模式,现在官方推荐是使用可编程渲染管线模式开发。
立即渲染模式
glBegin(GL_TRIANGLES)
glTranslatef(1,2,3);
// 其他操作
// glVertex*() 设置顶点坐标
// glColor*() 设置当前颜色
// glIndex*() 设置当前颜色表
// glNormal*() 设置法向坐标
// glEvalCoord*() 产生坐标
// glTexCoord*() 设置纹理坐标
// glEdgeFlag*() 控制边界绘制
// glMaterial*() 设置材质
glEnd()
可编程渲染管线模式
1、创建VBO顶点数据对象
GLuint VBO;
glGenBuffers(1,&VBO);
2、VBO与显卡缓存绑定
glBindBuffer(GL_ARRAY_BUFFER, VBO)
3、绑定数据缓存对象
glBufferData(GL_ARRAY_BUFFER,sizeof(vertexs),vertex,)
4、数据格式
glVertexAttribPoint(0,3,GL_FLOAT,)
5、启用/绘制
glEnableVertexAttribArray(0)
glDrawArrays(GL_TRIANGLES, 0, 10);
6、最后关闭
glBindBuffer(GL_ARRAY_BUFFER,0)
glBindVertexArray(0);
以上两段代码只是展示两种模式区别,很明显立即渲染模式容易理解,但是性能有限制,可编程渲染管线模式理解和使用门槛高,但更能接触底层。
QOpenGLWidget
QT对OpenGL进行了封装,提供了QOpenGLWidget类,只需要对其继承,在initialzeGL(),resizeGL(),paintGL()逻辑下进行业务实现,包括VBO、VAO的创建与绑定,着色器程序的编译与链接等。
派生类 glwidget逻辑
先简单介绍派生类的写法,当然为了方便扩展和使用,抽象出了相机类、渲染器类、物体类,完整的工程代码资源可以下载参考,具体的可以根据自己要实现的功能进行编写。
glwidget.h
#ifndef GLWIDGET_H
#define GLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QMouseEvent>
#include <QTimer>
#include <QQuaternion>
#include <vector>
#include <QKeyEvent>
#include "utils/Common.h"
#include "genericRender.h"
#include "grid.h"
#include "coorsystem.h"
#include "camera.h"
class glwidget :public QOpenGLWidget, QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
glwidget(QWidget * parent = nullptr);
//鼠标交互事件重写
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
//键盘交互
void keyReleaseEvent(QKeyEvent *event) override; //按键释放事件
void keyPressEvent(QKeyEvent *event) override; //按键按下事件
void slotTimeOut();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
public:
GenericRender m_render;//渲染器
Camera m_camera;//相机
grid m_grid;//网格平面xy
coorSystem m_coordsys;//坐标系
QTimer tm_;
public:
QList<int> keys;
QTimer* keyRespondTimer; //头文件中添加成员
static int mouse_button;
static int modifier_key;
public:
//鼠标变量
static double mouse_pos_x_old;
static double mouse_pos_y_old;
bool changeview_ = false; //改变视角的标志位
float fov = 2.0f; //视野范围
};
#endif // GLWIDGET_H
glwidget.cpp
#include "glwidget.h"
#include "box.h"
int glwidget::mouse_button = -1;
int glwidget::modifier_key = 0;
double glwidget::mouse_pos_x_old = 0;
double glwidget::mouse_pos_y_old = 0;
glwidget::glwidget(QWidget * parent):QOpenGLWidget(parent)
{
keyRespondTimer = new QTimer(this); //构造函数中创建定时器对象,并连接信号槽
connect(keyRespondTimer, &QTimer::timeout, this, &glwidget::slotTimeOut);
setFocusPolicy(Qt::StrongFocus);//否则进入不了键盘事件监听
}
void glwidget::initializeGL()
{
initializeOpenGLFunctions();
glEnable(GL_DEPTH_TEST);
m_render.init();
m_camera.setMatPro(fov);
QStringList renderfiles={":/obj/data/Cayman_GT.obj", ":/obj/data/bunny_10k.obj"};
for(QString file : renderfiles){
Box* b1 = new Box();
b1->load(file);
m_render.addBox(b1);
}
m_grid.initize();
m_coordsys.initize();
}
void glwidget::resizeGL(int w, int h)
{
m_camera.setRatio((float)width() / (float)height());
}
void glwidget::paintGL()
{
glClearColor(1.0f, 1.0f, 1.0f, 0.1f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPolygonMode(GL_FRONT, GL_LINE);
QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();
QMatrix4x4 mMatrix;
m_camera.setMatPro(fov);
m_grid.paint(f,m_camera.getMatPro(),m_camera.getMatView(),mMatrix);
m_coordsys.paint(f,m_camera.getMatPro(),m_camera.getMatView(),mMatrix);
m_render.render(f,m_camera,mMatrix);
}
void glwidget::mousePressEvent(QMouseEvent *event){
mouse_pos_x_old = event->pos().x();
mouse_pos_y_old = event->pos().y();
mouse_button = event->button();
}
void glwidget::mouseMoveEvent(QMouseEvent *event){
int x = event->pos().x();
int y = event->pos().y();
double w = width(), h = height();
double d_x = (mouse_pos_x_old - x)/w;
double d_y = (mouse_pos_y_old - y)/h;
if (mouse_button == Qt::LeftButton)
{
foreach (int key, keys) {
switch (key) {
case Qt::Key_Control:
m_camera.rotateY(-d_x*180*5);
m_camera.rotateX(-d_y*180*5);
break;
case Qt::Key_Shift:
m_camera.move(5*d_x, 5*d_y, 0);
break;
default:
break;
}
}
}
mouse_pos_x_old = x;
mouse_pos_y_old = y;
this->repaint();
}
void glwidget::mouseReleaseEvent(QMouseEvent *event){ //鼠标左键松开禁止改变相机视角
changeview_ = false;
}
void glwidget::wheelEvent(QWheelEvent *event){
if (event->delta() > 0)
fov-=2.0f;
else
fov+=2.0f;
if (fov<0.50f)
fov = 0.5f;
if (fov>=200.f)
fov = 200.0;
this->repaint();
}
void glwidget::keyReleaseEvent(QKeyEvent *event)
{
if(!event->isAutoRepeat()) //判断如果不是长按时自动触发的释放,就将key值从容器中删除
keys.removeAll(event->key());
if(keys.isEmpty()) //容器空了,关闭定时器
keyRespondTimer->stop();
}
void glwidget::keyPressEvent(QKeyEvent *event)
{
if(!event->isAutoRepeat()) //判断如果不是长按时自动触发的按下,就将key值加入容器
keys.append(event->key());
if(!keyRespondTimer->isActive()) //如果定时器不在运行,就启动一下
keyRespondTimer->start(4);
}
void glwidget::slotTimeOut(){
foreach (int key, keys) {
switch (key) {
case Qt::Key_D:
break;
case Qt::Key_Shift:
modifier_key = Qt::Key_Shift;
break;
default:
break;
}
}
}
鼠标交互功能
交互的功能实现是重载QOpenGLWidget的鼠标事件函数,修改相应的矩阵,也就对应最上面的相机变化、灯光变化等;
obj格式介绍
OBJ文件(.obj)包含有关3D对象的几何体的信息,下面是一个长方体的obj格式文件;
#
# Object file
#
mtllib Cube3x3x10.mtl
# Cube3x3x10\实体
v 0 0 0
v 3.00000002607703 0 0
v 3.00000002607703 0 9.99999977648258
v 0 0 9.99999977648258
v 3.00000002607703 3.00000002607703 9.99999977648258
v 0 3.00000002607703 9.99999977648258
v 0 0 9.99999977648258
v 3.00000002607703 0 9.99999977648258
v 0 3.00000002607703 9.99999977648258
v 0 3.00000002607703 0
v 0 0 0
v 0 0 9.99999977648258
v 0 3.00000002607703 0
v 3.00000002607703 3.00000002607703 0
v 3.00000002607703 0 0
v 0 0 0
v 3.00000002607703 3.00000002607703 0
v 3.00000002607703 3.00000002607703 9.99999977648258
v 3.00000002607703 0 9.99999977648258
v 3.00000002607703 0 0
v 3.00000002607703 3.00000002607703 9.99999977648258
v 3.00000002607703 3.00000002607703 0
v 0 3.00000002607703 0
v 0 3.00000002607703 9.99999977648258
vn 0 -1 0
vn 0 -1 0
vn 0 -1 0
vn 0 -1 0
vn 0 0 1
vn 0 0 1
vn 0 0 1
vn 0 0 1
vn -1 0 0
vn -1 0 0
vn -1 0 0
vn -1 0 0
vn 0 0 -1
vn 0 0 -1
vn 0 0 -1
vn 0 0 -1
vn 1 0 0
vn 1 0 0
vn 1 0 0
vn 1 0 0
vn 0 1 0
vn 0 1 0
vn 0 1 0
vn 0 1 0
vt 0 0
vt 0 0.00300000002607703
vt 0.00999999977648258 0.00300000002607703
vt 0.00999999977648258 0
vt 0.00150000001303852 0.00300000002607703
vt -0.00150000001303852 0.00300000002607703
vt -0.00150000001303852 0
vt 0.00150000001303852 0
vt 0.00499999988824129 0.00300000002607703
vt -0.00499999988824129 0.00300000002607703
vt -0.00499999988824129 0
vt 0.00499999988824129 0
vt 0.00150000001303852 0.00300000002607703
vt -0.00150000001303852 0.00300000002607703
vt -0.00150000001303852 0
vt 0.00150000001303852 0
vt 0.00499999988824129 0.00300000002607703
vt -0.00499999988824129 0.00300000002607703
vt -0.00499999988824129 0
vt 0.00499999988824129 0
vt 0.00999999977648258 0.00300000002607703
vt 0 0.00300000002607703
vt 0 0
vt 0.00999999977648258 0
o Cube3x3x10\实体
s off
# face 0
f 2/2/2 3/3/3 1/1/1
f 1/1/1 3/3/3 4/4/4
# face 1
f 5/5/5 6/6/6 8/8/8
f 8/8/8 6/6/6 7/7/7
# face 2
f 9/9/9 10/10/10 12/12/12
f 12/12/12 10/10/10 11/11/11
# face 3
f 13/13/13 14/14/14 16/16/16
f 16/16/16 14/14/14 15/15/15
# face 4
f 17/17/17 18/18/18 20/20/20
f 20/20/20 18/18/18 19/19/19
# face 5
f 21/21/21 22/22/22 24/24/24
f 24/24/24 22/22/22 23/23/23
其中
- mtllib Cube3x3x10.mtl 表示引用的材质文件的文件名
- v 0 0 0 表示一个点的xyz坐标,使用空格隔开
- vn 0 -1 0 表示一个点的法向量,使用空格隔开
- vt 0.09 0 表示uv纹理坐标,使用空格隔开
- f 2/2/2 3/3/3 1/1/1 表示一个面, 2/2/2依次为顶点索引,纹理坐标索引,法向索引,因为是三角面片,所以是三组,也有超多三个点的,自己造轮子的时候要注意
- o Cube 表示指定了模型名称为Cube
- s off 表示关闭光滑组
void ObjLoader::load(QString filename)
{
// 打开文件
QFile file(filename);
if(!file.open(QIODevice::ReadOnly)){
qDebug()<<"[Error] fail to open file: "<< filename;
return;
}
// 读取文件
QTextStream ts(&file);
// 临时存储
QVector<QVector3D> v;
QVector<QVector3D> vn;
QVector<QVector3D> vt;
QVector<QStringList> str_faces;
QVector<Face> faces;
while(!ts.atEnd()){
QStringList list = ts.readLine().split(QRegExp("(\\s+)"));
list.removeAll(" ");
if(list.size() == 0 )
break;
if(list[0] == "v")
v.push_back(QVector3D(list[1].toFloat(),list[2].toFloat(),list[3].toFloat()));
if(list[0] == "vn")
vn.push_back(QVector3D(list[1].toFloat(),list[2].toFloat(),list[3].toFloat()));
if(list[0] == "vt")
vt.push_back(QVector3D(list[1].toFloat(),list[2].toFloat(),0));
if(list[0] == "f")
str_faces.push_back(list);
}
for( int i =0; i< str_faces.size(); i++)
{
Face face;
for( int j = 1; j<=3; j++) //obj中顶点索引是从1开始
{
QStringList list = str_faces[i][j].split("/");
face.v[j-1] = list[0].toInt() -1;
face.t[j-1] = list[1].toInt() -1;
face.n[j-1] = list[2].toInt() -1;
}
faces.push_back(face);
}
for( int i = 0; i<faces.size();i++)
{
QVector3D a,b,c,na,nb,nc;
a = v[faces[i].v[0]];
b = v[faces[i].v[1]];
c = v[faces[i].v[2]];
na = vn[faces[i].n[0]];
nb = vn[faces[i].n[1]];
nc = vn[faces[i].n[2]];
mv.push_back(a.x());
mv.push_back(a.y());
mv.push_back(a.z());
mv.push_back(b.x());
mv.push_back(b.y());
mv.push_back(b.z());
mv.push_back(c.x());
mv.push_back(c.y());
mv.push_back(c.z());
mn.push_back(na.x());
mn.push_back(na.y());
mn.push_back(na.z());
mn.push_back(nb.x());
mn.push_back(nb.y());
mn.push_back(nb.z());
mn.push_back(nc.x());
mn.push_back(nc.y());
mn.push_back(nc.z());
}
file.close();
std::cout<< filename.toStdString() <<" id succeded!\t model_size:"<<mv.size()/3<<std::endl;
}
效果
暂时还不会弄视频或者动态图,先看个静态效果吧