游戏引擎的渲染部分的核心我觉得可以分为6个部分:1.生命周期; 2. SceneGraph; 3. SceneManager; 4.材质系统; 5.Pipeline; 6.GFX。所以要学习游戏引擎渲染的底层原理,掌握这几个核心模块即可,一个引擎的架构也都是围绕这几个核心进行调整,这篇主要讲解的是SceneGraph的原理与实现。
如果我们只是为了绘制个位数的物体,那么自个儿自嗨下完全够用,但是游戏引擎往往需要绘制庞大数量的物体,那么选择合适的场景组织方式就迫在眉睫,我们一般把这种复杂的多物体通过某种方式组织一起的方式称为SceneGraph(场景图)。SceneGraph是一种通用的数据结构,通常用于组织场景节点的逻辑和空间表示。
SceneGraph
如果我们尝试过在图形应用程序中绘制多个物体,那么应该已经使用了一些简单的东西,比如数组或向量,来保存要绘制的网格。这种方式只适合于简单的场景,但当我们的场景变得更复杂,这种线性方法的可渲染存储就变的有点累赘。比如车的底盘和车轮都需要有单独的网格,那么如何组织车轮与底盘的关系?如果汽车还有驾驶员网格数据呢?一旦我们开始添加许多对象,组织所有物体的相对位置就变得很困难了!这就是SceneGraph派上用场的地方。SceneGraph是由许多场景节点组成的,它们以树状结构组织在一起——每个节点都有一个父节点和一些子节点。刚说到的汽车的例子可以用下面的SceneGraph来表示。
上图中底盘节点管理着4个车轮节点,而底盘节点有可能依赖于车身节点(这里没有画出),而4个车轮节点用的网格数据是一样的,这里的“管理”或“依赖”在场景图中是通过存储父节点与子节点的关系来实现。也就是4个车轮是底盘节点的子节点,而底盘节点又作为4个车轮节点的父节点存在。
Local Transformations
游戏中SceneGraph的每个节点都包含了表示其局部变换的数据——其相对于父节点的位置和方向。一般来说,这只是一个简单的变换矩阵,就像我们在场景中设置物体的模型矩阵一样,它可以包含平移、旋转和缩放信息。场景节点的变换矩阵不是将顶点从局部空间转换到世界空间,而是相对于父节点的局部位置进行变换。这意味着所有的转换信息都会级联到SceneGraph中——包括缩放变换。比如一个节点的缩放值发生变化,那么与其关联的所有子节点都将相应变化。更具体点就是当底盘缩放至5时,关联的4个轮子也都将同步缩放至原来的5倍。
Graph Traversal
如果我们想在屏幕上渲染SceneGraph中节点对应的顶点信息,我们需要每个节点的世界变换矩阵,作为顶点着色器中的模型矩阵传递。为了计算各节点的世界矩阵,我们必须遍历SceneGraph。从SceneGraph的根节点开始,通过将节点的局部变换矩阵与其父节点的世界变换矩阵相乘,就可以计算出每个节点的世界变换矩阵。那么该节点的任何子节点就自然可以计算出它们在世界空间中的位置,以此类推,直到到达叶节点(即没有子节点的节点)。这个操作通常使用迭代循环或递归函数来完成,在子节点上调用自身,直到它已经遍历到了叶节点为止。
通常来讲,会把局部变换和世界变换存储到节点中,作为节点信息的一部分。从根节点顺延计算各个节点世界矩阵的方式就可以通过当前节点的局部变换矩阵与父节点的世界变换矩阵相乘,求取的结果就是该节点的世界变换矩阵。如果我们不存储世界变换矩阵,那么就需要重复遍历SceneGraph,这对庞大的场景的性能消耗是巨大的。
Transition and State Nodes
场景节点并不一定要包含网格等顶点数据,可能只是纯粹的作为过渡节点。也就是说,它们只是作为管理节点,并通过这些管理节点转换子节点,但它们自己不带任何网格或模型信息,也就是经常说到的空节点。例如,也许我们汽车的车轴是一个过渡节点,车轮就是它的子节点。通过这种组织关系,只需要转动车轴来转动两个车轮。或者可能节点只包含一些状态信息,比如是否激活还是屏蔽节点(或该状态信息对应着色器中的某些状态开启),一旦节点被屏蔽,那么它的所有级联的子节点都将被屏蔽,包括对应级联子节点的网格或顶点都不会被渲染。
World Scene Graph
SceneGraph不只是一个可分解的节点对象,如上面的汽车例子,可以表示为一个简单的ScenGraph例子。游戏世界中的一切都可以是一个大的SceneGraph的一部分。在游戏中,图形渲染器通常只有一个场景根节点,将当前场景的所有内容都包含在子节点中。以汽车生产线来讲,也许我们的汽车在一个汽车传送器上(所以汽车节点是汽车传送器节点的子节点),而汽车传送器节点是汽车生产车间节点的子节点,而汽车生产车间就是整个游戏世界节点的子节点。
Example Program
为了演示SceneGraph的工作原理,我们将创建一个简单的SceneNode类。这个类允许我们执行两种流行的节点遍历,高效地为场景图中的每个节点生成正确的世界转换,并绘制整个节点树。案例中我们会制作一个简单的立方体机器人,它的四肢和头部是身体节点的子节点,各子节点可以独立变换,互不影响。此外,为了演示SceneGraph中的每个节点如何附带着色器的状态信息,我们将编写两个着色器——SceneVertex和SceneFragment,它们将通过SceneNode类的颜色变量设置顶点的颜色。
SceneNode类非常简单。为了创建SceneGraph的树状结构,每个SceneNode都有一个父节点和子节点列表。每个SceneNode保存着局部变换与世界变换、颜色和模型缩放以及指向Mesh的指针。Update函数将遍历SceneGraph,并计算世界变换矩阵且以独立于帧率的方式更新成员变量,而Draw将实际绘制SceneNode,并接受当前正在绘制的Renderer的const引用。
#pragma once
#include "Mesh.h"
#include "Vector4.h"
#include "Matrix4.h"
class SceneNode
{
public:
SceneNode(Mesh* mesh, Vector4 colour = { 1, 0, 0, 1 });
~SceneNode();
// 设置或获取局部变换矩阵
void SetTransform(const Matrix4 &);
const Matrix4& GetTransform() const;
// 获取父节点
SceneNode* getParent() const;
// 获取世界变换矩阵
const Matrix4& GetWorldTransform() const;
// 设置或获取颜色值
const Vector4& GetColour() const;
void SetColour(const Vector4 &);
// 设置或获取模型的缩放值
const Vector3& GetModelScale() const;
void SetModelScale(const Vector3&);
// 设置或获取Mesh对象
Mesh* GetMesh() const;
void SetMesh(Mesh* mesh);
// 添加子节点
void AddChild(SceneNode* node);
// 更新世界矩阵
virtual void Update(float msec);
// 绘制mesh
virtual void Draw(const OGLRenderer &);
inline auto GetChildBegin() {
return _children.begin();
}
inline auto GetChildEnd() {
return _children.end();
}
protected:
SceneNode* _parent;
Mesh* _mesh;
Matrix4 _worldTransform;
Matrix4 _transform;
Vector3 _modelScale;
Vector4 _colour;
std::vector<SceneNode*> _children;
};
这里单独定义modelScale变量是为了单独改变网格大小,这样我们就可以缩放构成机器人肢体的立方体,而不把缩放结果注入到变换矩阵中,因为注入到变换矩阵后。计算后的世界变换矩阵会附带缩放信息,从而影响子节点世界变换矩阵的计算(实际开发过程会同时定义localScale, localPosition, localRotation, worldScale, worldPosition, worldRotation!并从localScale, localPosition, localRotation推导出localTransform的值,且通过localTransform计算worldTransform,并从worldTransform反向获取worldScale, worldPosition, worldRotation的值)。
#include "SceneNode.h"
SceneNode::SceneNode(Mesh* mesh, Vector4 colour) : _mesh(mesh), _colour(colour),
_parent(nullptr), _modelScale({1,1,1})
{}
SceneNode::~SceneNode()
{
for (uint32_t i = 0; i < _children.size(); i++) {
delete _children[i];
}
}
void SceneNode::AddChild(SceneNode* node)
{
_children.emplace_back(node);
node->_parent = this;
}
构造函数初始化类的变量,而析构函数删除节点的所有子节点。AddChild函数将SceneNode添加到它的子节点列表中,并将新的子节点的父节点设置为自身,代码还是很简单直接的。但是SceneNode不会删除mesh变量——因为我们有可能在多个节点中使用同一个mesh对象,这也意味着我们必须在其他地方处理mesh对象的删除。
接下来,我们实现Draw函数,它会绘制当前SceneNode的mesh信息。
void SceneNode::Draw(const Renderer& renderer)
{
if (_mesh) _mesh->Draw();
}
继续实现SceneNode类中的Update函数。函数内部只需将节点的局部变换矩阵乘以其父节点的世界矩阵,就可以计算出该节点正确的世界变换矩阵。由于SceneGraph是从根节点级联向下遍历的方式进行的,所以执行到当前节点的Update函数时,其父节点已经计算出了正确的世界变换矩阵。
void SceneNode::Update(float msec)
{
if (_parent) {
// 如果其有父节点,它的世界变换矩阵就是局部变换矩阵与父节点的世界变换矩阵相乘的结果
_worldTransform = _parent->_worldTransform * _transform;
}
else {
// 如果是根节点,那么世界变换矩阵就是其局部变换矩阵
_worldTransform = _transform;
}
// 继续向下遍历,计算出所有子节点的世界变换矩阵
for (auto* node: _children) {
node->Update(msec);
}
}
CubeRobot Class
上面实现了场景节点的基本逻辑,但是为了展示SceneGraph的灵活性,我们要对其进行业务功能拓展,首先继续定义CubeRobot类,并继承SceneNode。CubeRobot类将定义机器人的身体与四肢。
CubeRobot Scene Nodes
CubeRobot会定义一个节点作为机器人所有节点的根节点,根节点在机器人两脚间,根节点有个子节点是其身体节点,而身体节点有5个子节点,分别为1头节点,2腿节点,2胳膊节点。它的SceneGraph组织关系图如下:
CubeRobot Mesh
立方体机器人完全由立方体构成,这些立方体会按不同的比例缩放,形成了不同的肢体形状。所以我们要使用的立方体网格数据,它的坐标原点不能在立方体中心位置,而应该位于立方体边缘中心位置上。
根据上图很容易看出坐标中心在立方体网格边缘中心位置能够更容易的控制躯干的变换。
CubeRobot类没有太多新的内容,只是重载了update函数,该函数会处理立方体机器人一些简单的动画,另外不管我们有多少的CubeRobot,我们只需要一个cube mesh即可。所以我们将cube mesh作为类的静态变量,以及需要一个显式创建和删除该mesh的函数。
#include "CubeRobot.h"
Mesh* CubeRobot::cube{ nullptr };
CubeRobot::CubeRobot(): SceneNode(nullptr) {
// 设置身体的颜色为红色
SceneNode* body = new SceneNode(cube, {1, 0, 0, 1});
body->SetModelScale({ 10, 15, 5 });
body->SetTransform(Matrix4::Translation({ 0, 35, 0 }));
AddChild(body);
head = new SceneNode(cube, { 0, 1, 0, 1 }); // Green
head->SetModelScale({ 5, 5, 5 });
head->SetTransform(Matrix4::Translation({ 0, 30, 0 }));
body->AddChild(head);
leftArm = new SceneNode(cube, {0, 0, 1, 1}); // Blue
leftArm->SetModelScale({3, -18, 3});
leftArm->SetTransform(Matrix4::Translation({ -12, 30, -1 }));
body->AddChild(leftArm);
rightArm = new SceneNode(cube, {0, 0, 1, 1});
rightArm -> SetModelScale(Vector3(3, -18, 3));
rightArm -> SetTransform(Matrix4::Translation(Vector3(12, 30, -1)));
body -> AddChild(rightArm);
SceneNode * leftLeg = new SceneNode(cube, Vector4(0, 0, 1, 1)); // Blue !
leftLeg -> SetModelScale(Vector3(3, -17.5, 3));
leftLeg -> SetTransform(Matrix4::Translation(Vector3(-8, 0, 0)));
body -> AddChild(leftLeg);
SceneNode * rightLeg = new SceneNode(cube, Vector4(0, 0, 1, 1)); // Blue !
rightLeg -> SetModelScale(Vector3(3, -17.5, 3));
rightLeg -> SetTransform(Matrix4::Translation(Vector3(8, 0, 0)));
body -> AddChild(rightLeg);
}
void CubeRobot::Update(float msec) {
// 整个机器人自转
_transform = _transform *
Matrix4::Rotation(msec / 10.0F, Vector3(0, 1, 0));
// 头部Y轴自转
head -> SetTransform(head -> GetTransform() *
Matrix4::Rotation(-msec / 10.0F, Vector3(0, 1, 0)));
// 左部胳膊X轴自转
leftArm -> SetTransform(leftArm -> GetTransform() *
Matrix4::Rotation(-msec / 10.0F, Vector3(1, 0, 0)));
// 右部胳膊X轴自转
rightArm -> SetTransform(rightArm -> GetTransform() *
Matrix4::Rotation(msec / 10.0F, Vector3(1, 0, 0)));
SceneNode::Update(msec);
}
void CubeRobot::CreateCube() {
OBJMesh* m = new OBJMesh();
m->LoadOBJMesh(MESHDIR"cube.obj");
cube = m;
}
void CubeRobot::DeleteCube() {
delete cube;
}
最后,我们需要能够处理所有场景节点的Renderer类。
#include "Renderer.h"
Renderer::Renderer(Window& parent) {
// 创建cube mesh
CubeRobot::CreateCube();
camera = new Camera();
// 加载shader
currentShader = new Shader(SHADERDIR"SceneVertex.glsl",
SHADERDIR"SceneFragment.glsl");
if (!currentShader -> LinkProgram()) {
return;
}
projMatrix = Matrix4::Perspective(1.0f, 10000.0f,
(float)width / (float)height, 45.0f);
camera -> SetPosition(Vector3(0, 30, 175));
// 创建根节点
root = new SceneNode();
// 添加立方体机器人
root->AddChild(new CubeRobot());
// 默认开启深度测试
glEnable(GL_DEPTH_TEST);
init = true;
}
Renderer::~Renderer() {
// 清理场景节点
delete root;
// 清理cube mesh
CubeRobot::DeleteCube();
}
void Renderer::UpdateScene(float msec) {
camera->UpdateCamera(msec);
viewMatrix = camera->BuildViewMatrix();
// 更新所有节点的worldTransform
root->Update(msec);
}
void Renderer::RenderScene() {
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
glUseProgram(currentShader->GetProgram());
UpdateShaderMatrices();
glUniform1i(glGetUniformLocation(currentShader->GetProgram(),
"diffuseTex"), 1);
// 绘制画面
DrawNode(root);
glUseProgram(0);
SwapBuffers();
}
void Renderer::DrawNode(SceneNode * n) {
if (n->GetMesh()) {
Matrix4 transform = n->GetWorldTransform() *
Matrix4::Scale(n->GetModelScale());
// 根据计算完成的worldTransform作为顶点着色器的modelMatrix传递
glUniformMatrix4fv(
glGetUniformLocation(currentShader -> GetProgram(),
"modelMatrix"), 1, false, (float*)&transform);
glUniform4fv(glGetUniformLocation(currentShader -> GetProgram(),
"nodeColour"), 1, (float*)&n -> GetColour());
glUniform1i(glGetUniformLocation(currentShader -> GetProgram(),
"useTexture"), (int)n -> GetMesh() -> GetTexture());
// 绘制mesh
n->Draw(*this);
}
// 遍历节点,如果有mesh就进行绘制
for (auto* node: n->GetChildren()) {
DrawNode(node);
}
}
接下来是我们的顶点着色器。
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform vec4 nodeColour;
in vec3 position;
in vec2 texCoord;
out Vertex {
vec2 texCoord ;
vec4 colour ;
} OUT;
void main (void) {
gl_Position = (projMatrix * viewMatrix * modelMatrix ) * vec4 (position, 1.0);
OUT.texCoord = texCoord;
OUT.colour = nodeColour;
}
还有片元着色器。
uniform sampler2D diffuseTex;
uniform int useTexture;
in Vertex {
vec2 texCoord;
vec4 colour;
} IN;
out vec4 fragColour;
void main (void) {
fragColour = IN.colour;
if(useTexture > 0) {
fragColour *= texture(diffuseTex, IN.texCoord);
}
}
最后通过main函数调用整体逻辑:
int main() {
Window w("Scene Graphs!", 800,600,false);
Renderer renderer(w);
while(w.UpdateWindow() && !Window::GetKeyboard()->KeyDown(KEYBOARD_ESCAPE)){
// 先更新SceneGraph所有节点的worldTransform
renderer.UpdateScene(w.GetTimer()->GetTimedMS());
// 渲染实际网格数据
renderer.RenderScene();
}
return 0;
}
最后运行的效果如下: