我们的框架的基础架构中,首先要有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,我们在管理要绘制的面片时会更加方便高效直观!