在Qt中使用OpenGL(六)

前言

在Qt中使用OpenGL(一)
在Qt中使用OpenGL(二)
在Qt中使用OpenGL(三)
在Qt中使用OpenGL(四)
在Qt中使用OpenGL(五)
在前面的文章中,我们终于实现了自由查看3D世界的功能,现在无论我们之后要创建什么样的3D世界,我们都有能力自由的在其中探索了。
那么,接下来就是需要大家努力思考的问题了:我们要如何构建一个3D世界?
难不成,我们需要手动的为所有我们想要画出来的东西,编写顶点信息和纹理坐标?哪怕我们要画出来1000头牛也要手动进行?
很显然这是不可能的。

模型的基类

一般情况下,构建一个3D世界我们会使用模型这个概念。
一个模型我们就可以理解为一个物体。
比如,一个茶壶,一个人,一个桌子,一张凳子,它们都可以是一个模型。
那么,我们只要指定这些模型的位置和旋转角度,乃至大小,是不是一个简单的3D世界我们就构建出来了?
那么,根据我们之前对于OpenGL的了解,想要画出一个东西出来,我们至少需要两个东西:顶点和纹理。
其中,顶点应该包含位置坐标和纹理坐标,而纹理则是一张图,不,是至少一张图。
之前我们也说过,在OpenGL的世界中,纹理坐标是基于当前激活的纹理来计算的。这也就意味着,我们只需要在绘制前激活不同的纹理,我们就可以在绘制的时候使用不同的纹理来绘制我们的模型。
简单来说,就是茶壶和桌子可以使用不同的纹理。
我们只需要在绘制茶壶的时候激活茶壶纹理,绘制桌子的时候激活桌子纹理就行了。乃至我们可以在绘制茶壶嘴的时候激活一张贴图,绘制茶壶盖的时候激活另一张贴图都是没什么问题的。
好了,那么让我们来总结一下,一个模型中至少应该有哪些东西。

  1. 顶点信息(包括位置坐标和纹理坐标)
  2. 纹理(可以有多个纹理)
  3. 最终在3D世界中的变换(也就是Shader中需要的模型矩阵,可以用来让模型缩放,旋转,平移)

当然,实际情况的模型是会包含更多的内容的,考虑到目前有些东西我们还没有掌握,所以这里就用我们已知的知识来总结。
除了这些数据上的东西,对于要绘制一个模型,我很显然还需要VAO,VBO和Shader。并且,是每一个模型,都需要一个独立的VAO,VBO与Shader。因为我们目前就是这么做的,那么当我们需要画多个物体的时候,重复多次我们之前的行为是一个很正常的操作。
那么,此时你的脑海中是不是就出现了一个类的基本架构了呢?

struct Vertex
{
	QVector3D pos;
	QVector2D texture;
};

class Model : public QOpenGLExtraFunctions
{
public:
	Model();
	~Model();
public:
	void setScale(float val) { m_scale = val; }
	void setRotate(const QVector3D &rotate) { m_rotate = rotate; }
	void setPos(const QVector3D &pos) { m_pos = pos; }
public:
	void setVertices(const QVector<Vertex> &vertices) { m_vertices = vertices; }
	void setTexture(QOpenGLTexture *texture, int index = -1);
	void setShaderProgram(QOpenGLShaderProgram *program) { m_program = program; }
public:
	QMatrix4x4 model();
public:
	virtual void init();
	virtual void update();
	virtual void paint(const QMatrix4x4 &projection, const QMatrix4x4 &view);
protected:
	QVector3D m_pos{ 0,0,0 };
	QVector3D m_rotate{ 0,0,0 };
	float m_scale = 1;
	QVector<Vertex> m_vertices;
	QMap<int, QOpenGLTexture *> m_textures;
	QOpenGLVertexArrayObject m_vao;
	QOpenGLBuffer m_vbo;
	QOpenGLShaderProgram *m_program = nullptr;
};

我们的模型继承QOpenGLExtraFunctions是为了方便使用OpenGL的函数。
成员函数与变量都至少是protected是因为我们实际上目前无法知道一个模型绘制的细节,所以我们首先制定好一个模型的核心内容,形成一个基类,然后将实现的细节部分交给继承的类来做。
很容易理解的原因就是,我们没有办法确定,我们在初始化与绘制的时候,具体要使用什么样的Shader,怎么绑定数据,也不知道具体要怎么去绘制三角形,是画两个三角形形成一个矩形然后重复呢?还是只画一个三角形然后重复呢?
所以,我们先只顶下一个框架。
此时我们可以做的事情不多,也就是如何保存纹理以及如何生成模型矩阵。

void Model::setTexture(QOpenGLTexture *texture, int index)
{
	if (index == -1)
	{
		if (m_textures.isEmpty())
		{
			index = 0;
		}
		else
		{
			index = m_textures.keys().last() + 1;
		}
	}
	m_textures.insert(index, texture);
}

QMatrix4x4 Model::model()
{
	QMatrix4x4 _mat;
	_mat.setToIdentity();
	_mat.translate(m_pos);
	_mat.rotate(m_rotate.x(), 1, 0, 0);
	_mat.rotate(m_rotate.y(), 0, 1, 0);
	_mat.rotate(m_rotate.z(), 0, 0, 1);
	_mat.scale(m_scale);
	return _mat;
}

大概就这这样。
剩下的内容,例如如何初始化,如何绘制,update的时候做什么,我们目前是无法知道的。
但是我们应该有如下的认知:

  1. init函数要在3D窗口的initializeGL()函数中使用,用于创建和填充缓存。
  2. paint函数要在3D窗口的paintGL()函数中使用,用于具体的绘制图像。
  3. 我们在绘制模型的时候是无法知道投影矩阵与摄像机矩阵的,所以必须由外部告诉我们,这就是为什么paint函数设计的时候需要额外的两个参数传递两个矩阵。
  4. update函数具体要干什么,就和模型本身的逻辑有关了。比如,如果我们希望模型每次更新的时候旋转1°,那么就可以在update中修改m_rotate的值。

创建一个色子模型

有了以上的基类,我们就可以开始构建一个具体的模型了。
比如我们的色子模型。
色子模型的类声明非常的简单:

#pragma once
#include "Model.h"
class Dice : public Model
{
public:
	Dice();
	~Dice();
public:
	virtual void init() override;
	virtual void update() override;
	virtual void paint(const QMatrix4x4 &projection, const QMatrix4x4 &view) override;
private:
	float *m_vertexBuffer = nullptr;
	int m_vertexCount = 0;
};

m_vertexBuffer 是一个临时的缓存,用于将顶点信息转化为顶点缓存需要的格式。
m_vertexCount 记录了临时缓存中保存的顶点数量,如果缓存中顶点的数量比较少,就需要重新的初始化缓存,如果缓存中顶点的数量比较多,那么就不需要重新初始化缓存。
当然,第一次的时候肯定需要初始化缓存。
那么,在色子的构造函数中我们要做什么呢?
构造函数中,我们可以初始化色子的顶点信息,加载纹理和Shader(当然这些操作也可以在init做):

Dice::Dice()
	: Model()
{
	setVertices({
		// 顶点				纹理
		// 1
			{{-1,  1,  1,}, {0.50, 0.25}},	// 左上
			{{-1, -1,  1,}, {0.50, 0.50}},	// 左下
			{{ 1, -1,  1,}, {0.75, 0.50}},	// 右下
			{{ 1,  1,  1,}, {0.75, 0.25}},	// 右上
		// 6
			{{ 1,  1, -1,}, {0.00, 0.25}},	// 左上
			{{ 1, -1, -1,}, {0.00, 0.50}},	// 左下
			{{-1, -1, -1,}, {0.25, 0.50}},	// 右下
			{{-1,  1, -1,}, {0.25, 0.25}},	// 右上
		// 2
			{{ 1,  1,  1,}, {0.75, 0.25}},	// 左上
			{{ 1, -1,  1,}, {0.75, 0.50}},	// 左下
			{{ 1, -1, -1,}, {1.00, 0.50}},	// 右下
			{{ 1,  1, -1,}, {1.00, 0.25}},	// 右上
		// 5
			{{-1,  1, -1,}, {0.25, 0.25}},	// 左上
			{{-1, -1, -1,}, {0.25, 0.50}},	// 左下
			{{-1, -1,  1,}, {0.50, 0.50}},	// 右下
			{{-1,  1,  1,}, {0.50, 0.25}},	// 右上
		// 3
			{{-1,  1, -1,}, {0.00, 0.00}},	// 左上
			{{-1,  1,  1,}, {0.00, 0.25}},	// 左下
			{{ 1,  1,  1,}, {0.25, 0.25}},	// 右下
			{{ 1,  1, -1,}, {0.25, 0.00}},	// 右上
		// 4
			{{ 1, -1,  1,}, {0.00, 0.50}},	// 左上
			{{ 1, -1, -1,}, {0.00, 0.75}},	// 左下
			{{-1, -1, -1,}, {0.25, 0.75}},	// 右下
			{{-1, -1,  1,}, {0.25, 0.50}},	// 右上
		});

	setTexture(new QOpenGLTexture(QImage("D:/dice.png")));

	auto _program = new QOpenGLShaderProgram();
	_program->addShaderFromSourceCode(QOpenGLShader::Vertex, u8R"(
#version 330 core
in vec3 vPos;
in vec2 vTexture;
out vec2 oTexture;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main()
{
    gl_Position = projection * view * model * vec4(vPos, 1.0);
    oTexture = vTexture;
}
)");
	_program->addShaderFromSourceCode(QOpenGLShader::Fragment, u8R"(
#version 330 core
in vec2 oTexture;
uniform sampler2D uTexture;
void main()
{
    gl_FragColor = texture(uTexture, oTexture);
} 
)");
	setShaderProgram(_program);
}

然后,init()函数中,我们就可以创建各种缓存了:

void Dice::init()
{
	initializeOpenGLFunctions();
	if (!m_vao.isCreated())
		m_vao.create();
	if (!m_vbo.isCreated())
		m_vbo.create();
	if (!m_program->isLinked())
		m_program->link();

	if (m_vertexCount < m_vertices.count())
	{
		if (m_vertexBuffer)
			delete[] m_vertexBuffer;
		m_vertexBuffer = new float[m_vertices.count() * VertexFloatCount];
		m_vertexCount = m_vertices.count();
		int _offset = 0;
		for (auto &vertex : m_vertices)
		{
			m_vertexBuffer[_offset] = vertex.pos.x(); _offset++;
			m_vertexBuffer[_offset] = vertex.pos.y(); _offset++;
			m_vertexBuffer[_offset] = vertex.pos.z(); _offset++;
			m_vertexBuffer[_offset] = vertex.texture.x(); _offset++;
			m_vertexBuffer[_offset] = vertex.texture.y(); _offset++;
		}
	}

	m_vao.bind();
	m_vbo.bind();
	m_vbo.allocate(m_vertexBuffer, sizeof(float) * m_vertices.count() * VertexFloatCount);

	m_program->bind();
	// 绑定顶点坐标信息, 从0 * sizeof(float)字节开始读取3个float, 因为一个顶点有5个float数据, 所以下一个数据需要偏移5 * sizeof(float)个字节
	m_program->setAttributeBuffer("vPos", GL_FLOAT, 0 * sizeof(float), 3, 5 * sizeof(float));
	m_program->enableAttributeArray("vPos");
	// 绑定纹理坐标信息, 从3 * sizeof(float)字节开始读取2个float, 因为一个顶点有5个float数据, 所以下一个数据需要偏移5 * sizeof(float)个字节
	m_program->setAttributeBuffer("vTexture", GL_FLOAT, 3 * sizeof(float), 2, 5 * sizeof(float));
	m_program->enableAttributeArray("vTexture");
	m_program->release();

	m_vbo.release();
	m_vao.release();
}

我们在这个函数中将类中保存的顶点信息转化为了OpenGL需要的顶点缓存的格式,当然,方法有很多,这只是一种方式。可能还有其他更好的转换办法或者保存顶点信息的办法,大家可以多找找资料。

最终,我们就可以在paint函数中绘制我们的色子了:

void Dice::paint(const QMatrix4x4 &projection, const QMatrix4x4 &view)
{
	for (auto index : m_textures.keys())
	{
		m_textures[index]->bind(index);
	}
	m_vao.bind();
	m_program->bind();
	// 绑定变换矩阵
	m_program->setUniformValue("projection", projection);
	m_program->setUniformValue("view", view);
	m_program->setUniformValue("model", model());
	// 绘制
	for (int i = 0; i < 6; ++i)
	{
		glDrawArrays(GL_TRIANGLE_FAN, i * 4, 4);
	}
	m_program->release();
	m_vao.release();
	for (auto texture : m_textures)
	{
		texture->release();
	}
}

代码都是之前的代码,也就是绑定纹理那里有些不一样,但同样没什么好说的。

使用色子模型

既然我们已经成功的创建了色子模型,那么就让我们试着同时画出多个色子,然后让它们以不同的速度旋转吧!
首先,由于我们已经成功的创建了摄像机类和色子类,3D窗口中需要管理的内容开始直线下降,这就是最新的3D窗口的声明:

#pragma once

#include 
#include 
#include "Camera.h"
#include "Dice.h"

class OpenGLWidget : public QOpenGLWidget, public QOpenGLExtraFunctions
{
	Q_OBJECT

public:
	OpenGLWidget(QWidget *parent = nullptr);
	~OpenGLWidget();
protected:
	virtual void initializeGL() override;
	virtual void resizeGL(int w, int h) override;
	virtual void paintGL() override;

	virtual void timerEvent(QTimerEvent *event);

private:
	QMatrix4x4 m_projection;

	Camera m_camera;
	QVector<Model *> m_models;
};

几乎没什么东西了。
注意我们使用了Model而不是Dice来保存我们的色子模型,使用父类指针是因为我们未来可以并不总是要使用色子模型,为了通用性,所以直接使用父类。要不然我们辛苦编写父类然后继承是要干嘛?

初始化函数中我们要做的工作大幅度减少:

void OpenGLWidget::initializeGL()
{
	initializeOpenGLFunctions();
	glClearColor(0, 0.5, 0.7, 1);

	for (int i = 0; i < 3; ++i)
	{
		auto _dice = new Dice();
		_dice->init();
		_dice->setPos({ 0, i * 3.f, 0 });
		m_models << _dice;
	}
}

最复杂的逻辑竟然是给每个色子设定一个坐标。
而绘制方面简直没有什么可以说的了:

void OpenGLWidget::paintGL()
{
	glEnable(GL_DEPTH_TEST);
	for (auto dice : m_models)
	{
		dice->paint(m_projection, m_camera.view());
	}
}

循环遍历,然后传入投影矩阵和摄像机的视图矩阵进行绘制,完事。
整个类中最复杂的逻辑来了,每个色子都会根据时间进行旋转,并且旋转速度都不一样:

void OpenGLWidget::timerEvent(QTimerEvent *event)
{
	m_camera.update();
	float _speed = 1;
	for (auto dice : m_models)
	{
		float _y = dice->rotate().y() + _speed;
		if (_y >= 360)
			_y -= 360;
		dice->setRotate({ 0, _y, 0 });
		++_speed;
	}
	repaint();
}

我们没有使用色子模型的update函数,因为我们希望每个色子模型都有它自己的旋转逻辑而不是相同的旋转逻辑,并且我们不想提供额外的控制函数来控制旋转速度。
至此,一切工作都完成了,让我们看看效果吧:

下一篇:
在Qt中使用OpenGL(七)

你可能感兴趣的:(OpenGL,qt,opengl)