前言

到目前为止,本教程的完整项目可以在我的资源中得到–OpenGL4.0+VS2019渲染一个模型

本教程将介绍如何使用GLSL在OpenGL 4.0中渲染3D模型。本教程中的代码基于漫反射教程中的代码。

在之前的教程中,我们已经渲染过3D模型,但是它们是由单个三角形组成的,相当没意思。现在已经涵盖了基础知识,我们将继续渲染一个更复杂的对象。在这种情况下,对象将是一个立方体。在介绍如何渲染更复杂的模型之前,我们将首先讨论模型格式。

有许多工具可让用户创建3D模型。Maya和3D Studio Max是最受欢迎的两个3D建模程序。还有许多其他工具,功能较少,但仍然可以满足我们所需的基础知识。

无论选择使用哪种工具,它们都会将其模型导出为多种不同的格式。我的建议是,您创建自己的模型格式并编写解析器以将其导出格式转换为自己的格式。这样做的原因是,您使用的3D建模软件包可能会随时间变化,并且其模型格式也会发生变化。另外,您可能会使用多个3D建模包,因此您将需要处理多种不同的格式。因此,如果您有自己的格式并将其格式转换为自己的格式,则无需更改代码。您只需更改解析器程序即可将这些格式更改为自己的格式。同样,大多数3D建模包会导出大量垃圾,这些垃圾仅对该建模程序有用,并且您不需要模型格式的任何垃圾。

制作自己的格式最重要的部分是它涵盖了您需要做的所有事情,并且使用起来很简单。您还可以考虑为不同的对象制作几种不同的格式,因为有些可能具有动画数据,有些可能是静态的,依此类推。

我要介绍的模型格式非常基本。对于模型中的每个顶点,它将包含一条线。每行都将与代码中使用的顶点格式匹配,这些顶点格式将是位置矢量(x,y,z),纹理坐标(tu,tv)和法线矢量(nx,ny,nz)。该格式的顶点数也位于顶部,因此您可以在读取数据之前读取第一行并构建所需的内存结构。该格式还要求每三行形成一个三角形,并且模型格式中的顶点按顺时针顺序显示。这是我们要渲染的多维数据集的模型文件:

Cube.txt

您可以看到x,y,z,tu,tv,nx,ny,nz数据共有36行。每三行组成一个自己的三角形,使我们有12个三角形,它们将构成一个立方体。该格式非常简单,可以直接读取到我们的顶点缓冲区中,无需进行任何修改即可呈现。

现在需要注意的是,某些3D建模程序会以不同的顺序(例如左手坐标系或右手坐标系)导出数据。我们已经将OpenGL 4.0设置为使用左手坐标系,因此模型数据需要与其匹配。请留意这些差异,并确保您的解析程序可以处理将数据转换为正确的格式/顺序的操作。左手系统中,三角面片的正面的顶点的方向为顺时针。

Vertex Count: 36

Data:

-1.0  1.0 -1.0 0.0 0.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 1.0 0.0  0.0  0.0 -1.0
-1.0 -1.0 -1.0 0.0 1.0  0.0  0.0 -1.0
-1.0 -1.0 -1.0 0.0 1.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 1.0 0.0  0.0  0.0 -1.0
 1.0 -1.0 -1.0 1.0 1.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 0.0 0.0  1.0  0.0  0.0
 1.0  1.0  1.0 1.0 0.0  1.0  0.0  0.0
 1.0 -1.0 -1.0 0.0 1.0  1.0  0.0  0.0
 1.0 -1.0 -1.0 0.0 1.0  1.0  0.0  0.0
 1.0  1.0  1.0 1.0 0.0  1.0  0.0  0.0
 1.0 -1.0  1.0 1.0 1.0  1.0  0.0  0.0
 1.0  1.0  1.0 0.0 0.0  0.0  0.0  1.0
-1.0  1.0  1.0 1.0 0.0  0.0  0.0  1.0
 1.0 -1.0  1.0 0.0 1.0  0.0  0.0  1.0
 1.0 -1.0  1.0 0.0 1.0  0.0  0.0  1.0
-1.0  1.0  1.0 1.0 0.0  0.0  0.0  1.0
-1.0 -1.0  1.0 1.0 1.0  0.0  0.0  1.0
-1.0  1.0  1.0 0.0 0.0 -1.0  0.0  0.0
-1.0  1.0 -1.0 1.0 0.0 -1.0  0.0  0.0
-1.0 -1.0  1.0 0.0 1.0 -1.0  0.0  0.0
-1.0 -1.0  1.0 0.0 1.0 -1.0  0.0  0.0
-1.0  1.0 -1.0 1.0 0.0 -1.0  0.0  0.0
-1.0 -1.0 -1.0 1.0 1.0 -1.0  0.0  0.0
-1.0  1.0  1.0 0.0 0.0  0.0  1.0  0.0
 1.0  1.0  1.0 1.0 0.0  0.0  1.0  0.0
-1.0  1.0 -1.0 0.0 1.0  0.0  1.0  0.0
-1.0  1.0 -1.0 0.0 1.0  0.0  1.0  0.0
 1.0  1.0  1.0 1.0 0.0  0.0  1.0  0.0
 1.0  1.0 -1.0 1.0 1.0  0.0  1.0  0.0
-1.0 -1.0 -1.0 0.0 0.0  0.0 -1.0  0.0
 1.0 -1.0 -1.0 1.0 0.0  0.0 -1.0  0.0
-1.0 -1.0  1.0 0.0 1.0  0.0 -1.0  0.0
-1.0 -1.0  1.0 0.0 1.0  0.0 -1.0  0.0
 1.0 -1.0 -1.0 1.0 0.0  0.0 -1.0  0.0
 1.0 -1.0  1.0 1.0 1.0  0.0 -1.0  0.0

Modelclass.h

在本教程中,我们要做的就是对ModelClass进行一些小的更改,以使其从文本模型文件中渲染3D模型。
现在包括fstream库以处理从模型文本文件读取的操作。

下一个更改是添加了一个新的结构来表示模型格式。它称为ModelType。它包含位置,纹理和法线向量,与我们的文件格式相同。

struct ModelType
	{
		float x, y, z;
		float tu, tv;
		float nx, ny, nz;
	};

现在,Initialize函数将以要加载的模型的字符串文件名作为输入。

bool Initialize(OpenGLClass*, char*, char*, unsigned int, bool);

我们还有两个新功能来处理从文本文件中加载和卸载模型数据。

bool LoadModel(char*);
void ReleaseModel();

最后的更改是一个名为m_model的新私有变量,它将是新私有结构ModelType的数组。该变量将用于在放入顶点缓冲区之前读入并保存模型数据。

ModelType * m_model;

完整代码如下

// Filename: modelclass.h  //如前所述,ModelClass负责封装3D模型的几何图形。

#ifndef _MODELCLASS_H_
#define _MODELCLASS_H_

//
// INCLUDES //
//
#include <fstream>
using namespace std;

///
// MY CLASS INCLUDES //
///
//#include "openglclass.h"
#include "textureclass.h"


// Class name: ModelClass

class ModelClass
{
private:

		struct VertexType
	{
		float x, y, z;
		float tu, tv;
		float nx, ny, nz;//可容纳照明的法线向量
	};

		struct ModelType
		{
			float x, y, z;
			float tu, tv;
			float nx, ny, nz;
		};
public:
	ModelClass();
	ModelClass(const ModelClass&);
	~ModelClass();

	//此处的函数处理模型的顶点和索引缓冲区的初始化和关闭。
	//渲染功能将模型几何图形放置在显卡上,并使用GLSL着色器进行绘制。
	bool Initialize(OpenGLClass*, char*, char*, unsigned int, bool);
	void Shutdown(OpenGLClass*);
	void Render(OpenGLClass*);

private:
	bool InitializeBuffers(OpenGLClass*);
	void ShutdownBuffers(OpenGLClass*);
	void RenderBuffers(OpenGLClass*);
	
	bool LoadTexture(OpenGLClass*, char*, unsigned int, bool);
	void ReleaseTexture();

	bool LoadModel(char*);
	void ReleaseModel();
private:
	//ModelClass中的私有变量是顶点数组对象,顶点缓冲区和索引缓冲区ID。
	//另外,还有两个整数可以跟踪顶点和索引缓冲区的大小。
	int m_vertexCount, m_indexCount;
	unsigned int m_vertexArrayId, m_vertexBufferId, m_indexBufferId;
	TextureClass* m_Texture;
	ModelType* m_model;
};

#endif

modelclass.cpp

在类构造函数中,新的模型结构设置为null。

m_model = 0;

现在,Initialize函数将应加载的模型的文件名作为输入。

bool ModelClass::Initialize(OpenGLClass* OpenGL, char* modelFilename, char* textureFilename, unsigned int textureUnit, bool wrap)

在Initialize函数中,我们现在首先调用新的LoadModel函数。它将把我们提供的文件名中的模型数据加载到新的m_model数组中。填充完模型数组后,我们便可以从中构建顶点和索引缓冲区。由于InitializeBuffers现在依赖于此模型数据,因此必须确保以正确的顺序调用函数。

result = LoadModel(modelFilename);
	if(!result)
	{
		return false;
	}

在Shutdown函数中,我们完成了对ReleaseModel函数的调用,以删除m_model数组数据。

ReleaseModel();

请注意,我们将不再在此处手动设置顶点和索引计数。一旦到达ModelClass :: LoadModel函数,您将看到我们在该点读取了顶点和索引计数。

加载顶点和索引数组的过程有所变化。无需手动设置值,我们循环遍历新m_model数组中的所有元素,然后将数据从那里复制到顶点数组中。索引数组易于构建,因为我们加载的每个顶点都具有与其加载到的数组中的位置相同的索引号。最后要注意的一点是,使用的模型格式确实需要反转OpenGL的视图纹理坐标。

for(i=0; i<m_vertexCount; i++)
	{
		vertices[i].x  = m_model[i].x;
		vertices[i].y  = m_model[i].y;
		vertices[i].z  = m_model[i].z;
		vertices[i].tu = m_model[i].tu;
		vertices[i].tv = 1.0f - m_model[i].tv;
		vertices[i].nx = m_model[i].nx;
		vertices[i].ny = m_model[i].ny;
		vertices[i].nz = m_model[i].nz;

		indices[i] = i;
	}

这是新的LoadModel函数,用于处理将模型数据从文本文件加载到m_model数组变量中的情况。它打开文本文件,然后先读取顶点计数。读取顶点计数后,它将创建ModelType数组,然后将每一行读取到该数组中。现在在此功能中同时设置了顶点数和索引数。

bool ModelClass::LoadModel(char* filename)
{
	ifstream fin;
	char input;
	int i;


	// 打开模型文件
	fin.open(filename);
	
	// 如果无法打开文件,则退出。.
	if(fin.fail())
	{
		return false;
	}

	// 读取顶点计数值。
	fin.get(input);
	while(input != ':')
	{
		fin.get(input);
	}

	// 读取顶点数。
	fin >> m_vertexCount;

	//将索引数设置为与顶点数相同。
	m_indexCount = m_vertexCount;

	// 使用读入的顶点数创建模型。
	m_model = new ModelType[m_vertexCount];
	if(!m_model)
	{
		return false;
	}

	// 读取数据的开头。
	fin.get(input);
	while(input != ':')
	{
		fin.get(input);
	}
	fin.get(input);
	fin.get(input);

	// 读取顶点数据。.
	for(i=0; i<m_vertexCount; i++)
	{
		fin >> m_model[i].x >> m_model[i].y >> m_model[i].z;
		fin >> m_model[i].tu >> m_model[i].tv;
		fin >> m_model[i].nx >> m_model[i].ny >> m_model[i].nz;
	}

	// 关闭模型文件
	fin.close();

	return true;
}


void ModelClass::ReleaseModel()
{
	if(m_model)
	{
		delete [] m_model;
		m_model = 0;
	}

	return;
}

最后,我们绘制三角形时,是使用顶点的索引(索引缓冲区)进行绘制。
glDrawElements()函数的第一个参数,改为GL_TRIANGLES,这表示三角形的顶点缓冲区有重复的点。每个三角形的顶点单独存储,索引数等于三角形的顶点数乘以3.

void ModelClass::RenderBuffers(OpenGLClass* OpenGL)
{
	// 绑定存储有关顶点和索引缓冲区的所有信息的顶点数组对象。
	OpenGL->glBindVertexArray(m_vertexArrayId);

	// 使用索引缓冲区渲染顶点缓冲区
	glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, 0);//GL_TRIANGLE_STRIP,绘制一组相连的三角形,对于奇数n,顶点n、n+1和n+2定义了第n个三角形;对于偶数n,顶点n+1、n和n+2定义了第n个三角形,总共绘制N-2个三角形

	return;
}

Graphicsclass.cpp

现在,模型初始化将使用要加载的模型文件的文件名。在本教程中,我们将使用cube.txt文件,以便将此模型加载到3D立方体对象中以进行渲染。还要注意,我们使用的是称为opengl.tga的新纹理,而不是先前教程中的石头纹理,不过我改写成了加载32位bmp图片,加载tga的函数注释掉了,有需要可以重新改回去。

result = m_Model->Initialize(m_OpenGL,  "../Engine/data/cube.txt", "../Engine/data/opengl.bmp", 0, true);

在本教程中,我将漫反射光的颜色更改为白色。
m_Light-> SetDiffuseColor(1.0f,1.0f,1.0f,1.0f);

总结

通过更改ModelClass,我们现在可以加载3D模型并进行渲染。此处使用的格式仅适用于带照明的基本静态对象,但这是了解模型格式如何工作的一个良好的开端。

练习

1.重新编译代码并运行程序。您应该得到一个带有opengl.tga纹理的旋转立方体。完成后,按Escape键即可退出。

2.找到一个不错的3D建模包(希望是免费的),然后创建自己的简单模型并将其导出。开始查看格式。

3.编写一个简单的解析器程序,该程序将模型导出并转换为此处使用的格式。用您的模型替换cube.txt并运行程序。