基于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

一,程序截图

基于Qt的OpenGL编程(3.x以上GLSL可编程管线版)---(十五)obj模型加载_第1张图片 1.1 开启光照 基于Qt的OpenGL编程(3.x以上GLSL可编程管线版)---(十五)obj模型加载_第2张图片 1.2 关闭光照 基于Qt的OpenGL编程(3.x以上GLSL可编程管线版)---(十五)obj模型加载_第3张图片 1.3 线性渲染

 

基于Qt的OpenGL编程(3.x以上GLSL可编程管线版)---(十五)obj模型加载_第4张图片 1.4 比武台 基于Qt的OpenGL编程(3.x以上GLSL可编程管线版)---(十五)obj模型加载_第5张图片 1.5 坦克 基于Qt的OpenGL编程(3.x以上GLSL可编程管线版)---(十五)obj模型加载_第6张图片 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 项目目录

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

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

  • 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 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

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 objects; //存储模型中的各个object模块
  QMap map_materials;//读取该模型的材质文件,并存储文件中的各个材质属性

};

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

  QVector positions;
  QVector uvs;
  QVector 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 positionIndices, uvIndices, normalIndices;
  QVector temp_positions;
  QVector temp_uvs;
  QVector 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() <

四,注意事项

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

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

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

 

比如,

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

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

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

你可能感兴趣的:(现代OpenGL学习教程)