基于Qt的OpenGL编程(3.x以上GLSL可编程管线版)---(十五)obj模型加载

Vries的原教程里,对于模型载入,使用的是一种非常流行的模型加载库Assimp,可以方便的加载obj,fbx,3ds等常见的模型格式文件,在visual studio2015里,我照原教程进行了Assimp的配置,程序成功运行。在Qt中,把Assimp当作外库进行导入,试了很多种方法也不可以,万般无奈之下,我自写了一个基于Qt平台的简易模型导入程序,仅针对简易obj模型进行解析导入。

https://learnopengl-cn.github.io/03%20Model%20Loading/01%20Assimp/关于Assimp库的内容与常见模型只是请看Vries的原版教程)

 

Qt开发平台:5.8.0

编译器:Desktop Qt 5.8.0 MSVC2015_64bit

 

本程序源代码

百度网盘链接:https://pan.baidu.com/s/1w60NPe69ySSqzkQ6o0BeVA 密码:dmud

csdn下载连接https://download.csdn.net/download/z136411501/10611540

一,程序截图

1.1 开启光照
1.2 关闭光照
1.2 关闭光照
1.3 线性渲染

 

1.4 比武台
1.5 坦克
1.6 基地

二,obj模型解释

  这里借用一下另外一个学习可编程管线OpenGL的网站里,对obj模型的解释,网站原链接如下:

http://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-7-model-loading/

 相对于原链接所介绍的简易OBJ模型,我增添了一些内容,主要是材质文件mtllib的使用,

 

  2.1 OBJ文件示例

  在原作者的示例obj文件中,增添材质属性的使用,大概是这个模样:

# Blender3D v249 OBJ File: untitled.blend
# www.blender3d.org

mtllib cube.mtl

#
# object Arch41_039
#

v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vt 0.748573 0.750412
vt 0.749279 0.501284
vt 0.999110 0.501077
vt 0.999455 0.750380
vt 0.250471 0.500702
vt 0.249682 0.749677
vt 0.001085 0.750380
vt 0.001517 0.499994
vt 0.499422 0.500239
vt 0.500149 0.750166
vt 0.748355 0.998230
vt 0.500193 0.998728
vt 0.498993 0.250415
vt 0.748953 0.250920
vn 0.000000 0.000000 -1.000000
vn -1.000000 -0.000000 -0.000000
vn -0.000000 -0.000000 1.000000
vn -0.000001 0.000000 1.000000
vn 1.000000 -0.000000 0.000000
vn 1.000000 0.000000 0.000001
vn 0.000000 1.000000 -0.000000
vn -0.000000 -1.000000 0.000000
usemtl Material_ray

s off
f 5/1/1 1/2/1 4/3/1
f 5/1/1 4/3/1 8/4/1
f 3/5/2 7/6/2 8/7/2
f 3/5/2 8/7/2 4/8/2
f 2/9/3 6/10/3 3/5/3
f 6/10/4 7/6/4 3/5/4
f 1/2/5 5/1/5 2/9/5
f 5/1/6 6/10/6 2/9/6
f 5/1/7 8/11/7 6/10/7
f 8/11/7 7/12/7 6/10/7
f 1/2/8 2/9/8 3/13/8
f 1/2/8 3/13/8 4/14/8
  • #是注释标记,就像C++中的//
  • object是将一个obj模型分为多个模块,进行储存管理
  • mtllib描述了模型所使用的材质文件所在的路径,材质文件里一般会有多个材质
  • usemtl表示接下来的面f所构成的三维几何结构的材质属性使用该种材质
  • v顶点
  • vt代表顶点的纹理坐标
  • vn代表顶点的法线
  • f代表面

   v vt vn都很好理解。f比较麻烦。例如f 8/11/7 7/12/7 6/10/7:

  • 8/11/7描述了三角形的第一个顶点
  • 7/12/7描述了三角形的第二个顶点
  • 6/10/7描述了三角形的第三个顶点
  • 对于第一个顶点,8指向要用的顶点。此例中是-1.000000 1.000000 -1.000000(索引从1开始,和C++中从0开始不同)
  • 11指向要用的纹理坐标。此例中是0.748355 0.998230。
  • 7指向要用的法线。此例中是0.000000 1.000000 -0.000000。

我们称这些数字为索引。若几个顶点共用同一个坐标,索引就显得很方便,文件中只需保存一个”v”,可以多次引用,节省了存储空间。

以下是原教程一个简易的obj载入函数,仅一个函数即可完成模型载入的关键步骤,不过读取的模型不包含材质文件,只有最基础的顶点位置参数,且读取文件时使用的是最基础的stdio.h标准输入输出流。

百度网盘链接:https://pan.baidu.com/s/15aQ3QbHtzzkt4zQXE6oF3g 密码:22el

 

  2.2 mtl文件示例

   这是一个简单的mtl材质文件:

# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# 创建的文件:01.03.2017 19:24:15

newmtl 01___Default
	Ns 58.0000
	Ni 1.5000
	d 1.0000
	Tr 0.0000
	Tf 1.0000 1.0000 1.0000 
	illum 2
	Ka 1.0000 1.0000 1.0000
	Kd 1.0000 1.0000 1.0000
	Ks 0.1167 0.1167 0.1167
	Ke 0.0000 0.0000 0.0000
	map_Ka white.jpg
	map_Kd white.jpg

newmtl 17___Default
	Ns 1.0000
	Ni 1.5000
	d 1.0000
	Tr 0.0000
	Tf 1.0000 1.0000 1.0000 
	illum 2
	Ka 0.5882 0.5882 0.5882
	Kd 0.5882 0.5882 0.5882
	Ks 0.0000 0.0000 0.0000
	Ke 0.0000 0.0000 0.0000
	map_Ka Arch41_039_bark.jpg
	map_Kd Arch41_039_bark.jpg
  • newmtl代表一种材质,以下皆为该材质的属性参数
  • Ns为Phong式光照模型中镜面光的高光反射系数
  • Ka为Phong式光照模型中环境光的颜色反射系数
  • Kd为Phong式光照模型中漫反射光的颜色反射系数
  • Ks为Phong式光照模型中镜面光的颜色反射系数
  • map_Ka为环境光所采样的纹理贴图路径,在.obj模型文件的根目录下
  • map_Kd为漫反射光所采样的纹理贴图路径
  • 其余参数感兴趣的大家自己查呗,反正我没用上

三,源代码解析

  3.1 项目目录

      

    相对于教程(八)的简单框架,我进行了进一步的精简

  • camera.cpp是摄像机文件,使用WASDEQ按键控制摄像机的前进后退上升下降,鼠标左键拖拽进行视角的移动
  • light.cpp是灯光,存储一个简单的6面,36个顶点的立方体
  • main.cpp主函数调用主程序接口,与使用qss样式文件
  • mainwindow.cpp打开主窗口,相应按键与按钮
  • model.cpp模型文件类,读取指定路径下的obj模型文件
  • oglmanager.cpp继承QOpenGLWidget类,作为附属于窗口类的widget类使用
  • resourcemanager.cpp作为资源管理类,管理shader与texture纹理资源
  • shader.cpp本质对象为QOpenGLShader,做了一些方便管理参数的成员函数
  • texture2d.cpp本质对象为QOpenGLTexture,设置成员函数方便管理

  3.2 模型读取函数解析

这里仅解释model.hmodel.cpp这个类,其余.h与.cpp文件,之前教程有过解析。

model.h

#ifndef MODEL_H
#define MODEL_H

#include <QOpenGLFunctions_3_3_Core>
#include <resourcemanager.h>
#include <QVector>

#include <QDebug>
#include <QVector3D>
#include <QVector2D>
#include <QOpenGLTexture>
#include <QMap>

class Object;
class Material;

class Model
{
public:
  Model();
  bool init(const QString& path); //模型文件的初始化设置,将.obj文件的路径传入参数列表
  void draw(GLboolean isOpenLighting = GL_FALSE);//绘制模型,参数列表为是否打开Phong式光照计算
private:
  bool loadOBJ(const QString& path);//整个程序的最关键函数!!! 参数为obj模型所在的路径
  void bindBufferData();
  QOpenGLFunctions_3_3_Core *core;
  QVector<Object> objects; //存储模型中的各个object模块
  QMap<QString, Material> map_materials;//读取该模型的材质文件,并存储文件中的各个材质属性

};

class Object{
public:
  GLuint positionVBO;
  GLuint uvVBO;
  GLuint normalVBO;

  QVector<QVector3D> positions;
  QVector<QVector2D> uvs;
  QVector<QVector3D> normals;

  QString matName;//材质名称
};

class Material{//一个简易的材质类
public:
  QVector3D Ka;//ambient反射系数
  QVector3D Kd;//diffuse反射系数
  QVector3D Ks;//specular反射系数
  double shininess;
  QString name_map_Ka;
  QString name_map_Kd;

};

#endif // MODEL_H

  我借鉴了Assimp文件库的模型读取思想,将obj模型含有一个或多个object对象,一个object对象含有多个顶点,法向量,纹理坐标与材质信息,一个材质含有Phong式光照模型中的环境光,漫反射光,镜面光反射系数,镜面反射指数,环境纹理贴图与漫反射纹理贴图路径。

解释一下关键函数loadOBJ(const QString &path),流程就是使用QFile打开.obj模型文件所在的路径,while循环,一步一步扫描整个文件,遇到关键字,如object,usemtl,v,或者vn,等分开进行处理,处理完后,绑定数据运行即可。

bool Model::loadOBJ(const QString &path){
  QFile file(path);
  if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    qDebug()<<"OBJLOADER ERROR::FILE CAN NOT OPEN!";
    file.close();
    return false;
  }

  QTextStream in(&file);
  QString line;//文件流

  QVector<int> positionIndices, uvIndices, normalIndices;
  QVector<QVector3D> temp_positions;
  QVector<QVector2D> temp_uvs;
  QVector<QVector3D> temp_normals;
  QString temp_matName;//材质的名称

  while(!in.atEnd()){
    line = in.readLine();//读取一行,还有读取所有readAll();
    QStringList list = line.split(" ", QString::SkipEmptyParts);
    if(list.empty())
      continue;
    //qDebug() << list;
    if(list[0] == "mtllib"){//处理材质文件,即处理图片纹理
      /******* 1.1 处理材质文件路径 *********/
      //":/models/res/models/huapen/penzi.obj"
      QString mtl_path = path;
      int tempIndex = path.lastIndexOf("/")+1;
      mtl_path.remove(tempIndex, path.size()-tempIndex);//":/models/res/models/huapen/" 得到根目录路径,用来和材质文件名结合,生成正确路径
      //mtl_path += list[1];//得到材料路径":/models/res/models/huapen/penzi.mtl"
//      qDebug() << mtl_path;

      /******* 1.2 读取材质文件,导入Material类中 *********/
      QFile mtl_file(mtl_path+list[1]);//正确的材质文件路径
      if(!mtl_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug()<<"OBJLOADER ERROR::MTL_FILE CAN NOT OPEN!";
        mtl_file.close();
        file.close();
        return false;
      }
      QTextStream mtl_in(&mtl_file);
      QString mtl_line;//读取材质文件流的一行

      Material material;
      QString matName;//材质的名称
      while(!mtl_in.atEnd()){
        mtl_line = mtl_in.readLine();//读取一行,还有读取所有readAll();
        QStringList mtl_list = mtl_line.split(QRegExp("\\s+"), QString::SkipEmptyParts); //以“空格”与“\t”为识别符号,分开字符串
        if(mtl_list.empty())
          continue;
        if(mtl_list[0] == "newmtl"){
          matName = mtl_list[1];
          map_materials[matName] = material;
        }else if(mtl_list[0] == "Ns"){
          double shininess = mtl_list[1].toDouble();
          map_materials[matName].shininess = shininess;
        }else if(mtl_list[0] == "Ka"){
          double x = mtl_list[1].toDouble();
          double y = mtl_list[2].toDouble();
          double z = mtl_list[3].toDouble();

          QVector3D Ka(x, y, z);
          map_materials[matName].Ka = Ka;
        }else if(mtl_list[0] == "Kd"){
          double x = mtl_list[1].toDouble();
          double y = mtl_list[2].toDouble();
          double z = mtl_list[3].toDouble();

          QVector3D Kd(x, y, z);
          map_materials[matName].Kd = Kd;
        }else if(mtl_list[0] == "Ks"){
          double x = mtl_list[1].toDouble();
          double y = mtl_list[2].toDouble();
          double z = mtl_list[3].toDouble();

          QVector3D Ks(x, y, z);
          map_materials[matName].Ks = Ks;
        }else if(mtl_list[0] == "map_Ka"){
          ResourceManager::loadTexture(mtl_list[1], mtl_path+mtl_list[1]);
          map_materials[matName].name_map_Ka = mtl_list[1];
        }else if(mtl_list[0] == "map_Kd"){
          ResourceManager::loadTexture(mtl_list[1], mtl_path+mtl_list[1]);
          map_materials[matName].name_map_Kd = mtl_list[1];
        }
      }
     /******* 1.2 读取材质文件,导入Material类中 *********/
    }else if(list.size() > 1 && list[1] == "object"){//扫描寻找object
      if(!objects.empty()){
        for(int i=0; i < positionIndices.size(); i++ ){
          //得到索引
          int posIndex = positionIndices[i];
          int uvIndex = uvIndices[i];
          int norIndex = normalIndices[i];

          //根据索引取值
          QVector3D pos = temp_positions[posIndex-1];
          objects.last().positions.push_back(pos);

          QVector3D nor = temp_normals[norIndex-1];
          objects.last().normals.push_back(nor);

          if(uvIndex != 0){
            QVector2D uv = temp_uvs[uvIndex-1];
            objects.last().uvs.push_back(uv);
          }

        }
        objects.last().matName = temp_matName;
        positionIndices.clear();
        uvIndices.clear();
        normalIndices.clear();
      }

      Object object;
      objects.push_back(object);//obj模型文件中的第一个object对象,因为一个obj模型可能还有多个object对象
    }else if (list[0] == "v"){
      double x = list[1].toDouble();
      double y = list[2].toDouble();
      double z = list[3].toDouble();

      QVector3D pos;
      pos.setX(x);
      pos.setY(y);
      pos.setZ(z);
      temp_positions.push_back(pos);
    }else if (list[0] == "vt"){
      double x = list[1].toDouble();
      double y = list[2].toDouble();

      QVector2D uv;
      uv.setX(x);
      uv.setY(y);
      temp_uvs.push_back(uv);
    }else if (list[0] == "vn"){
      double x = list[1].toDouble();
      double y = list[2].toDouble();
      double z = list[3].toDouble();

      QVector3D nor;
      nor.setX(x);
      nor.setY(y);
      nor.setZ(z);
      temp_normals.push_back(nor);
    }else if (list[0] == "usemtl"){
      temp_matName = list[1];
      //qDebug() << list[1];
    }else if (list[0] == "f"){
      if(list.size() > 4){
        qDebug() << "OBJLOADER ERROR::THE LOADER ONLY SUPPORT THE TRIANGLES MESH!" << endl;
        file.close();
        return false;
      }
      for(int i = 1; i < 4; ++i){//读取处理 f字符后边的 三长串字符,如“f 2396/2442/2376 101/107/111 100/106/110”
        QStringList slist = list[i].split("/");
        int posIndex = slist[0].toInt();
        int uvIndex = slist[1].toInt();
        int norIndex = slist[2].toInt();

        positionIndices.push_back(posIndex);
        uvIndices.push_back(uvIndex);
        normalIndices.push_back(norIndex);
        //qDebug() <<posIndex << " " << uvIndex << " " << norIndex;
      }
    }
  }

  //处理最后一个object

  for(int i=0; i < positionIndices.size(); i++ ){
    //得到索引
    int posIndex = positionIndices[i];
    int uvIndex = uvIndices[i];
    int norIndex = normalIndices[i];

    //根据索引取值
    QVector3D pos = temp_positions[posIndex-1];
    objects.last().positions.push_back(pos);

    QVector3D nor = temp_normals[norIndex-1];
    objects.last().normals.push_back(nor);

    if(uvIndex != 0){
      QVector2D uv = temp_uvs[uvIndex-1];
      objects.last().uvs.push_back(uv);
    }
            //qDebug() <<posIndex << " " << uvIndex << " " << norIndex;
  }
  objects.last().matName = temp_matName;

  file.close();
  return true;
}

四,注意事项

      因为这是一个简易的obj模型读取程序,一些细节处,我懒得处理,所以如果要载入一些新的obj模型,务必修改文件

  的格式,使之与penzi.obj与penzi.mtl的格式对应。

        

 

比如,

  • 该读取格式,读取obj模型必须有与之对应的mtl材质文件存在
  • penzi.obj里材质文件的路径必须为相对路径
  • penzi.obj必须是纯三角形面,不能有四边形等多边形面
  • 每一个面 f 11/2/3,顶点,法线,纹理坐标均需存在,缺一不可
  • penzi.mtl里必须有纹理贴图的路径存在

     另外,该Model类对象,不能拿出来直接单独使用,必须结合指定着色器“model”与ResourceManagersh类使用(资源文件中有,本来想把Model类写成一个独立的类来使用,想了半天,处理起来太麻烦了,没有好的思路,就算了)

看着该读取程序有很多限制,其实改起来也特别简单,学个思想就行。

  • 3
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
要在Qt中使用OpenGL 3.3或更高本来渲染一个OBJ文件,您需要完成以下步骤: 1. 在Qt中创建一个OpenGL窗口: ```c++ class GLWidget : public QOpenGLWidget { public: GLWidget(QWidget *parent = 0); ~GLWidget(); protected: void initializeGL() override; void paintGL() override; void resizeGL(int w, int h) override; }; ``` 2. 在initializeGL函数中初始化OpenGL环境,并编译和链接您的着色器程序: ```c++ void GLWidget::initializeGL() { initializeOpenGLFunctions(); // Create and compile your shader program QOpenGLShaderProgram* shaderProgram = new QOpenGLShaderProgram(this); shaderProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, "vertexShader.glsl"); shaderProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, "fragmentShader.glsl"); shaderProgram->link(); shaderProgram->bind(); } ``` 3. 在paintGL函数中使用您的着色器程序和OBJ文件中的数据绘制场景: ```c++ void GLWidget::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Set up your shader uniforms and attributes shaderProgram->setUniformValue("projection", projection); shaderProgram->setUniformValue("view", view); shaderProgram->setUniformValue("model", model); shaderProgram->setAttributeArray("position", vertices.constData(), 3, sizeof(Vertex)); shaderProgram->setAttributeArray("normal", normals.constData(), 3, sizeof(Vertex)); shaderProgram->setAttributeArray("texCoord", texCoords.constData(), 2, sizeof(Vertex)); shaderProgram->enableAttributeArray("position"); shaderProgram->enableAttributeArray("normal"); shaderProgram->enableAttributeArray("texCoord"); // Draw your OBJ file glDrawArrays(GL_TRIANGLES, 0, vertices.size()); shaderProgram->disableAttributeArray("position"); shaderProgram->disableAttributeArray("normal"); shaderProgram->disableAttributeArray("texCoord"); } ``` 4. 在resizeGL函数中更新OpenGL视口: ```c++ void GLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); } ``` 5. 确保您的OBJ文件中包含顶点位置、法线和纹理坐标数据,并且您可以正确地解析和加载它们。 这只是一个基本的框架,您需要根据您的项目需求进行修改和扩展。您可以使用第三方库,如Assimp,来加载和解析OBJ文件中的数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值