山东大学22届项目实训《基于OpenGL的三维模型渲染引擎》学习记录3——框架搭建思路——渲染器Renderer

我们的框架的基础架构中,首先要有Object类,它代表的是一个个物体,可以平移,旋转,缩放;用它派生出Mesh类(可以把Mesh视为Object的外观),Mesh有Geometry和Matreial两个属性:Geometry代表了它的几何形状(如正方体,球体,猴子头),存储着该Object的顶点数据(VAO);Material决定它如何被渲染出来(像木头,还是钢铁,还是玻璃),可以用Material派生出很多不同类型的Material,比如写实的Material或是卡通的Material,选择不同的Material就可以决定该Mesh的渲染风格。

场景中可能有成千上万个Mesh,而每一帧我们都需要把这些Mesh都渲染出来,我们需要一个渲染列表来存放这些Mesh以便后续渲染操作。所以,我们要定义一个渲染器(Renderer)。

渲染器中需要哪些东西才能渲染出我们想要的画面呢?首先,我们要定义一个渲染列表,里面存放着我们需要在这一帧内要渲染的所有Mesh,然后对于每一个Mesh,我们需要它所在的世界坐标,由Mesh继承的Object提供的模型矩阵而获取;Mesh是什么形状呢?由其Geometry属性提供(VAO);Mesh被如何渲染?由其的Material属性决定(检查材质类型,决定使用的Shader,根据使用的Shader更新顶点着色器和片元着色器中的Uniform变量(常见的Uniform变量有MVP矩阵,光源方向等)。有了以上所有信息,我们就可以通过renderer把我们定义的场景渲染到场景中了,而且管理起来十分方便!

事不宜迟,我们来写自己的Object类。如上文所说,我们需要给Object定义平移,旋转,缩放。所以首先object需要自己当前的位置、旋转和缩放状态。

protected:
	glm::vec3 mPosition{ 0.0f };
	
	//unity旋转标准:pitch yaw roll
	float mAngleX{ 0.0f };
	float mAngleY{ 0.0f };
	float mAngleZ{ 0.0f };

	glm::vec3 mScale{ 1.0f };

而控制Object如何在世界坐标系中改变位置的操作,我们要把这一系列变换做成模型矩阵,在Unity中,旋转顺序是物体先绕自身x轴旋转,再绕自身y轴旋转,最后绕自身z轴旋转,而变换顺序是先缩放,再旋转,最后平移:

public:
	Object();
	~Object();
	
	void setPosition(glm::vec3 pos);

	//增量旋转
	void rotateX(float angle); 
	void rotateY(float angle); 
	void rotateZ(float angle); 

	//设置旋转角度
	void setAngleX(float angle);
	void setAngleY(float angle);
	void setAngleZ(float angle);

	void setScale(glm::vec3 scale);

	glm::vec3 getPosition()const { return mPosition; }

	glm::mat4 getModelMatrix()const;

Object很重要的一点是有父子关系,子物体以父物体的模型坐标为世界坐标,所以子物体如何移动是在父物体的基础之上移动的,其模型矩阵要在最左边乘以父物体的模型矩阵。

综上所述,我们的Object.h最基础的样子便是:

#pragma once 
#include "core.h"


class Object {
public:
	Object();
	~Object();
	
	void setPosition(glm::vec3 pos);

	//增量旋转
	void rotateX(float angle); 
	void rotateY(float angle); 
	void rotateZ(float angle); 

	//设置旋转角度
	void setAngleX(float angle);
	void setAngleY(float angle);
	void setAngleZ(float angle);

	void setScale(glm::vec3 scale);

	glm::vec3 getPosition()const { return mPosition; }

	glm::mat4 getModelMatrix()const;

	//父子关系
	void addChild(Object* obj);
	std::vector<Object*>  getChildren();
	Object* getParent();


protected:
	glm::vec3 mPosition{ 0.0f };
	
	//unity旋转标准:pitch yaw roll
	float mAngleX{ 0.0f };
	float mAngleY{ 0.0f };
	float mAngleZ{ 0.0f };

	glm::vec3 mScale{ 1.0f };

	//父子关系
	std::vector<Object*>	mChildren{};
	Object*					mParent{ nullptr };

};

注意,我们从core.h中获得以下库的引用,#pragma once可以防止头文件被重复包含:

#pragma once

//注意:glad头文件必须在glfw引用之前引用
#include <glad/glad.h>
#include <GLFW/glfw3.h>

//GLM
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>

#include <glm/gtx/matrix_decompose.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <glm/gtx/quaternion.hpp>



#include <vector>
#include <map>
#include <iostream>

在Object.cpp中,我们要获取当前物体的模型矩阵(考虑父子关系),并且能够获取自身的父物体与子物体,逻辑很简单:

#include "object.h"

Object::Object() {
	mType = ObjectType::Object;
}

Object::~Object() {

}

void Object::setPosition(glm::vec3 pos) {
	mPosition = pos;
}

//增量旋转
void Object::rotateX(float angle) {
	mAngleX += angle;
}

void Object::rotateY(float angle) {
	mAngleY += angle;
}

void Object::rotateZ(float angle) {
	mAngleZ += angle;
}

void Object::setAngleX(float angle) {
	mAngleX = angle;
}

void Object::setAngleY(float angle) {
	mAngleY = angle;
}

void Object::setAngleZ(float angle) {
	mAngleZ = angle;
}

void Object::setScale(glm::vec3 scale) {
	mScale = scale;
}



glm::mat4 Object::getModelMatrix()const {
	//首先获取父亲的变换矩阵
	glm::mat4 parentMatrix{ 1.0f };
	if (mParent != nullptr) {
		parentMatrix = mParent->getModelMatrix();
	}


	//unity:缩放 旋转 平移
	glm::mat4 transform{ 1.0f };

	transform = glm::scale(transform, mScale);

	//unity旋转标准:pitch yaw roll
	transform = glm::rotate(transform, glm::radians(mAngleX), glm::vec3(1.0f, 0.0f, 0.0f));
	transform = glm::rotate(transform, glm::radians(mAngleY), glm::vec3(0.0f, 1.0f, 0.0f));
	transform = glm::rotate(transform, glm::radians(mAngleZ), glm::vec3(0.0f, 0.0f, 1.0f));

	transform = parentMatrix * glm::translate(glm::mat4(1.0f),mPosition) * transform;

	return transform;
}


void Object::addChild(Object* obj) {
	//1 检查是否曾经加入过这个孩子--返回迭代器
	auto iter = std::find(mChildren.begin(), mChildren.end(), obj);
	if (iter != mChildren.end()) {
		std::cerr << "Duplicated Child added" << std::endl;
		return;
	}

	//2 加入孩子
	mChildren.push_back(obj);

	//3 告诉新加入的孩子他的爸爸是谁
	obj->mParent = this;
}

std::vector<Object*>  Object::getChildren() {
	return mChildren;
}

Object* Object::getParent() {
	return mParent;
}

我们已经知道,Object要派生出一个Mesh类,Mesh就是物体的外观,它有Geometry(几何形状)和Material(材质)两个属性,所以我们先把Geometry与Material写出来:

对于Geometry,我们要提供物体模型的顶点数据,对于每一个顶点,它都有自己的位置,法线,uv(贴图时用),索引序号(EBO存储的是 顶点数组的索引,用于告诉 OpenGL 如何重用顶点,从而避免重复存储数据,提高渲染效率)等属性。而且在创建一些基础物体(比如立方体,球体)时,我们要在Geometry类的相关函数内部利用OpenGL状态机,创建VBO,VAO,EBO等对象去完成,这样我们在Main函数中创建物体时只需要简单地执行一遍CreateBox(Int size)函数即可;

对于Material,它要派生出很多不同类型的material,我们这里先简单地写一个经典的PhongMaterial,它继承于Material基类。phong光照模型我们需要考虑的是漫反射,镜面反射和环境光,漫反射可以代表物体自身的颜色,也就是物体上的贴图(或者定义顶点颜色插值计算片元颜色);镜面反射由反射强度(Intensity)和反射光斑大小(Shiness)决定,而Intensity一般由光源决定,所以我们的PhongMaterial中只需要保存Shiness属性即可;环境光也用物体贴图表示即可。

有了Geometry和Material类之后,我们可以创建Mesh类了,Mesh中的两个属性便是Geometry和Material。

我们知道,Phong模型需要光照,场景中也得有光源,不然到处都是黑漆漆的一片。而光源很简单,我们这里简单说说。目前我们只需要两种光源:平行光和环境光。我们创建一个Light基类,它的属性有颜色mColor,反射光强度mSpecularIntensity。然后创建DirectionalLight类继承于Light。平行光的属性只需要一个方向即可,不需要位置。之后创建AmbientLight类继承于Light,它更简单,它只需要颜色属性,而父类中已经存在颜色了。

有了Mesh类,但是我们还无法把它们渲染到屏幕上面。我们需要一个Renderer,通过Renderer,我们创建的Mesh就可以与我们写的Shader关联起来,把它的属性送入Shader中从而创建出各种奇妙的效果。

我们创建一个Renderer类,其中最为核心的函数就是render()函数,我们把需要渲染的Mesh列表传入该函数,由该函数负责把它们一个个渲染出来。我们要渲染事物,需要传入哪些参数呢?首先毫无疑问,要传入mesh列表。我们要看到我们渲染的物体,必须得有摄像机;场景要有光源,我们也将其传入:

void render(
	const std::vector<Mesh*>& meshes,
	Camera* camera,
	DirectionalLight* dirLight,
	AmbientLight* ambLight
);

在渲染器类中,我们需要把已经写好的Shader都赋给不同类的Material。比如,我们已经写好了PhongMaterial,那么我们就需要在渲染器中定义属性Shader* phongMaterial,把我们写好的着色器程序传入其中。这些我们在Renderer类的构造函数中完成:

Renderer::Renderer() {
	mPhongShader = new Shader("assets/shaders/phong.vert", "assets/shaders/phong.frag");
}

写一个PickShader函数来选择不同类型的Material,目前我们只有PhongMaterial:

Shader* Renderer::pickShader(MaterialType type) {
	Shader* result = nullptr;

	switch (type) {
	case MaterialType::PhongMaterial:
		result = mPhongShader;
		break;
	default:
		std::cout << "Unknown material type to pick shader" << std::endl;
		break;
	}

	return result;
}

接下来就是具体实现Renderer函数,遍历绘制每一个传入的Mesh了。 首先,我们要设置当前帧绘制的时候,opengl的必要状态机参数;接着再清理画布;然后遍历mesh进行绘制:我们取出当前mesh的Geometry与Material属性,通过Material能够得知该Mesh使用的哪个Shader;将该Shader需要的所有参数传入着色器之后,根据Geometry获取顶点数据,绘制出该面片即可。代码如下:

void Renderer::render(
	const std::vector<Mesh*>& meshes,
	Camera* camera,
	DirectionalLight* dirLight,
	AmbientLight* ambLight
) {
	//1 设置当前帧绘制的时候,opengl的必要状态机参数
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);

	//2 清理画布 
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	//3 遍历mesh进行绘制
	for (int i = 0; i < meshes.size(); i++) {
		auto mesh = meshes[i];
		auto geometry = mesh->mGeometry;
		auto material = mesh->mMaterial;
 
		//1 决定使用哪个Shader 
		Shader* shader = pickShader(material->mType);

		//2 更新shader的uniform
		shader->begin();

		switch (material->mType) {
		case MaterialType::PhongMaterial: {
			PhongMaterial* phongMat = (PhongMaterial*)material;
			//diffuse贴图
			//将纹理采样器与纹理单元进行挂钩
			shader->setInt("sampler", 0);

			//将纹理与纹理单元进行挂钩
			phongMat->mDiffuse->bind();

			//mvp
			shader->setMatrix4x4("modelMatrix", mesh->getModelMatrix());
			shader->setMatrix4x4("viewMatrix", camera->getViewMatrix());
			shader->setMatrix4x4("projectionMatrix", camera->getProjectionMatrix());

			auto normalMatrix = glm::mat3(glm::transpose(glm::inverse(mesh->getModelMatrix())));
			shader->setMatrix3x3("normalMatrix", normalMatrix);

			//光源参数的uniform更新
			shader->setVector3("lightDirection", dirLight->mDirection);
			shader->setVector3("lightColor", dirLight->mColor);
			shader->setFloat("specularIntensity", dirLight->mSpecularIntensity);

			shader->setFloat("shiness", phongMat->mShiness);

			shader->setVector3("ambientColor", ambLight->mColor);

			//相机信息更新
			shader->setVector3("cameraPosition", camera->mPosition);

		}	
			break;
		default:
			continue;
		}
	
		//3 绑定vao
		glBindVertexArray(geometry->getVao());

		//4 执行绘制命令
		glDrawElements(GL_TRIANGLES, geometry->getIndicesCount(), GL_UNSIGNED_INT, 0);
	}
}

经过上序所有步骤之后,我们在main函数中要绘制一个带贴图的立方体的代码如下: 

//首先要定义传入的Mesh列表:
std::vector<Mesh*> meshes{};

DirectionalLight* dirLight = nullptr;
AmbientLight* ambLight = nullptr;

Camera* camera = nullptr;
CameraControl* cameraControl = nullptr;

//定义传入的摄像机
void prepareCamera() {
	float size = 10.0f
	camera = new PerspectiveCamera(
		60.0f, 
		(float)app->getWidth() / (float)app->getHeight(),
		0.1f,
		1000.0f
	);

	cameraControl = new GameCameraControl();
	cameraControl->setCamera(camera);
	cameraControl->setSensitivity(0.4f);
}
//----------准备要绘制的图形--------
void prepare() {
	renderer = new Renderer();

	auto geometry = Geometry::createBox(1.5f);
	
	auto material = new PhongMaterial();
	material->mShiness = 32.0f;
	material->mDiffuse = new Texture("assets/textures/foxgirl.jpg", 0);

	//3 生成mesh
	auto mesh = new Mesh(geometry, material);
	

	meshes.push_back(mesh);

	dirLight = new DirectionalLight();
	ambLight = new AmbientLight();
	ambLight->mColor = glm::vec3(0.1f);
}

int main() {
	if (!app->init(1600, 1200)) {
		return -1;
	}

	//设置opengl视口以及清理颜色
	GL_CALL(glViewport(0, 0, 1600, 1200));
	GL_CALL(glClearColor(0.2f, 0.3f, 0.3f, 1.0f));

	prepareCamera();
	prepare();

	while (app->update()) {
        cameraControl->update();
        //在渲染器中传入需要的事物来绘制----------------
		renderer->render(meshes, camera, dirLight, ambLight);
	}

	app->destroy();

	return 0;
}

 我们可以看到,短短几行代码就可以绘制出一个带贴图的立方体。很多繁琐复杂的步骤已经被我们封装起来了,而且凭借渲染器Renderer,我们在管理要绘制的面片时会更加方便高效直观!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值