如果你在阅读这篇文章, 大概说明你刚接触OpenGL图形编程, 并且对OpenGL饶有兴趣, 笔者写这篇文章的原因在于, 公开的OpenGL的学习资料十分少并且大多都晦涩难懂, 学习周期长, 并且很少人会讨论本文即将涉及的技术, 即使内容丰富如Games101系列课程也没有提及场景管理这一项, 我认为这项技术能为你的程序带来2~3倍的性能提升而只进行一些"小小的"改动与添加, 尽管仍有许多优化的工作要做, 但对于初学者来说, 本文涉及的技术已经为这些人很大程度上解决了一定规模的模型加载以及渲染流畅度问题, 让他们不必受模型的容量的制约而限制了自己的想象力和创造力, 也不必花太多的精力去搜集有关模型加载和渲染优化的文章, 这篇文章会尽量系统地讲述如何加速模型加载与如何进行场景管理从而更流畅地进行渲染. 我不希望你在学习OpenGL的途中受长时间模型加载的折磨与因为模型容量大而导致低帧率的限制, 如果你也是一名学生并且只有一学期用于学习OpenGL, 我也不希望你花很多时间在模型加载和场景管理的研究上. 我希望你挑选或使用专业软件自己搭建一个十分满意的模型, 不用在乎他的大小如何, 然后用复杂的着色器与光照算法把他美丽地渲染出来. 读完这篇文章, 你就暂时不用担心模型的问题了, 但是如果你想继续深入学习计算机图形学, 你仍然需要继续学习场景管理, LoD等技术来优化渲染时帧率, 仍然需要继续思考为什么已经看似如此优化的情况下加载速率仍不如专业软件, 继续探究, 然后把知识免费分享给后来者.
请注意, 本教程是基于LearnOpenGL教程提供的模型导入框架实现的优化, 并且采用OpenGL3.0以后的新渲染管线, 如果你没有读过相关教程或是采用老渲染管线, 这篇文章也许对你帮助不大.
本文基于八叉树实现了3D场景管理, 所有你需要依赖的外部库: glm, Assimp, OpenGL
你应当在熟悉完LearnOpenGL教程: 模型加载->模型这一节后再来接触这篇博客:
模型 - LearnOpenGL CN (learnopengl-cn.github.io)
在阅读完之后, 你将获得一个比原教程更快(甚至快4-6倍)的模型加载类, 与可以在大容量模型下保持优秀帧数运行的技巧.
下文会先按照逻辑顺序对每个部分进行讲解, 在文章的最后, 我会按头文件的包含顺序贴出每个头文件, 源文件的源代码, 方便读者拷贝.
我习惯把glm常用的库放在一个头文件下, 之后你可以只需要引用这一个头文件就使用其中的工具
Glm.h
#pragma once
#ifndef GLM
#define GLM
#include"glm/glm.hpp"
#include"glm/gtc/matrix_transform.hpp"
#include"glm/gtc/type_ptr.hpp"
#endif // !GLM
Part1: 模型载入
先关注我们模型加载中最主要的成分的这两行代码, 我会称之为导致你模型加载缓慢的毁灭性的罪魁祸首, 如果你熟悉C++对象的构造基本流程, 你可以去思考在这两步里发生了多少次对象即其成员的拷贝用于构造新对象.
而如果你熟悉现代C++(指C++11以后)的编译过程, 你可能会说, 编译器会将这样的代码隐式地调用移动构造函数进行构造, 因此在这里的构造成本其实很少. 确实如此, 但是前提你要有Mesh类的移动构造函数才行呀.
struct Texture {
unsigned int id;
std::string type;
std::string path;
Texture(const Texture& Tex): id(Tex.id), type(Tex.type), path(Tex.path)
{
};
Texture():id(-1) {};
Texture(Texture&& Tex) noexcept : id(Tex.id) {
type = std::move(Tex.type);
path = std::move(Tex.path);
};
Texture& operator=(const Texture& Tex){
if (this == &Tex) return *this;
id = Tex.id;
type = Tex.type;
path = Tex.path;
return *this;
}
Texture& operator=(Texture&& Tex) noexcept {
if (this == &Tex) return *this;
id = Tex.id;
type = std::move(Tex.type);
path = std::move(Tex.path);
return *this;
}
};
class Mesh {
public:
// mesh Data
std::vector<Vertex> vertices;
std::vector<unsigned int> indices;
std::vector<Texture> textures;
unsigned int VAO;
Bounds _bounds;
// constructor
Mesh(std::vector<Vertex>&& vertices, std::vector<unsigned int>&& indices, std::vector<Texture>&& textures);
Mesh(const Mesh& M)noexcept :vertices(M.vertices), indices(M.indices), textures(M.textures), _bounds(M._bounds),
VAO(M.VAO), VBO(M.VBO), EBO(M.EBO){};
Mesh(Mesh&& M)noexcept:VAO(M.VAO), VBO(M.VBO), EBO(M.EBO), _bounds(M._bounds){
vertices = std::move(M.vertices);
indices = std::move(M.indices);
textures = std::move(M.textures);
}
// render the mesh
void Draw(Shader& shader);
private:
// render data
unsigned int VBO, EBO;
// initializes all the buffer objects/arrays
void setupMesh();
};
Mesh::Mesh(vector<Vertex>&& _vertices, vector<unsigned int>&& _indices, vector<Texture>&& _textures)
{
this->vertices = std::move(_vertices);
this->indices = std::move(_indices);
this->textures = std::move(_textures);
float x_min = std::numeric_limits<float>::infinity(), x_max = -std::numeric_limits<float>::infinity();
float y_min = std::numeric_limits<float>::infinity(), y_max = -std::numeric_limits<float>::infinity();
float z_min = std::numeric_limits<float>::infinity(), z_max = -std::numeric_limits<float>::infinity();
std::for_each(vertices.begin(), vertices.end(),
[&x_min, &x_max, &y_min, &y_max, &z_min, &z_max]
(Vertex const& vertex) {
auto const& Pos = vertex.Position;
float x = Pos.x, y = Pos.y, z = Pos.z;
x_min = std::min(x_min, x);
x_max = std::max(x_max, x);
y_min = std::min(y_min, y);
y_max = std::max(y_max, y);
z_min = std::min(z_min, z);
z_max = std::max(z_max, z);
});
_bounds = Bounds( glm::vec3(x_min, y_min, z_min), glm::vec3(x_max, y_max, z_max ) );
// now that we have all the required data, set the vertex buffers and its attribute pointers.
setupMesh();
}
为Mesh类完善构造函数的同时, 也顺手为Texture类完善构造函数, 这是一个好习惯----尽管有时编译器会隐式地为你创造各种构造函数(拷贝构造, 默认构造, 移动构造等等), 但你最好还是显式地将它写出来, 这是有必要且出于安全性考虑的.
你可能会注意到我为Mesh类添加了一个Bounds类型成员, Bounds类型是什么, 有什么用在后面会解释, 现在你可以先忽略他, 因为我们需要先专注于如何加快模型加载.
return new Mesh(std::move(vertices), std::move(indices), std::move(textures));
于是在processMesh()函数的返回语句改成这样, 显式指明调用我们的移动构造函数, 并且将返回值改成Mesh*, 同样地, 你也需要把Model的成员从std::vector<Mesh>类型改为std::vector<Mesh*>
并且稍微调整其他部分对这个成员的访问形式.
还有一个地方是我们可以优化的
就算你不熟悉模型的加载结构,当你看到这段代码的时候也应该有一个强烈的反应: 这不是树的先序遍历吗? 所以我们对树的先序遍历进行一个优化, 用简单的方法就可以从递归转换为迭代的写法, 递归和迭代写法最重要的一点是, 递归容易产生大量的调用栈, 这在加载超大模型时非常容易出现栈溢出的情况, 速度方面两者相差不多, 但编译器对迭代循环的优化是要比对递归的优化要好的.
#include <stack>
// processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any).
void Model::processNode(aiNode* node, const aiScene* scene)
{
// process each mesh located at the current node
stack<aiNode*> stk;
stk.push(node);
while (stk.size()) {
auto top = stk.top(); stk.pop();
unsigned int mNM = top->mNumMeshes;
for (unsigned int i = 0; i < mNM; ++i) {
aiMesh* mesh = scene->mMeshes[top->mMeshes[i]];
Mesh* m = processMesh(mesh, scene);
this->_3DSceneTree->insert(m);
this->meshes.push_back(m);
}
unsigned int mNC = top->mNumChildren;
for (unsigned int i = 0; i < mNC; ++i) {
stk.push(top->mChildren[i]);
}
}
}
思路是很简单的, 维护一个储存节点的栈, 先将当前节点压入栈, 以栈非空为循环条件, 弹出栈顶元素, 然后处理栈顶节点的所有mesh, 之后将栈顶节点的所有子节点压入栈, 熟悉数据结构与算法的你应该对此非常清晰明确.
到此, 去试一试加载一个较大的模型(obj文件100M, gltf文件200M), 并且将它渲染出来.
Par2: 基于八叉树的渲染优化
这才是我们这篇博客的核心内容, 但是如果你在稍微大一点的模型的加载部分就很吃力的话, 你也没有必要进行这样的优化. 所以第一部分非常重要, 你需要先具有加载大模型的能力, 才能谈得上去渲染.
我想先介绍一下我们希望八叉树为我们提供什么样的效果:
就像这张图片一样, 八叉树将模型的基本单元: 网格(Mesh)分割到一个个无交集或互相只有微小交集的方格中, 当然, 面对这样复杂的图形是不好理解八叉树分割的过程原理的, 我们从一个正方体开始, 假设一个正方体是我们所拥有的模型/场景, 其具有数不清的网格:
对于一个大正方形我们可以规则(也可以不规则)地分割成八个小正方形, 对于每一个小正方形又可以分割成更小的八块正方形如此往复, 我们似乎可以得到很多近乎无穷微小的正方形, 但是没有必要这么做, 因为倘若一个正方形内只包含少数物体或不包含物体我们就没有必要继续分割了. 这样分割有什么好处呢? 假设以上是我们分割的最终结果, 我们称第一次分割所得到的八个正方形为第一级正方形, 第二次得到的为第二级, 那么我们目前有8个一级正方形, 其中有一个一级正方形拥有8个二级正方形. 如果我为一级正方形进行编号, 我们将得到:
还有背面的8. 但是在之后的测试中我们不会采用这样的编号顺序, 这里仅仅是为了方便说明.
对于二级正方形编号:
因此, 倘若我有一个有n个八进制数字组成的序列, 第一位表示第一级方块编号, 第二位表示来自第一级方块的第二级方块编号, 第n位就表示来自第n-1级的方块的第n级方块编号, 对于只有两级的情况, 11就表示第一级1号方块中的1号二级方块, 如果每一个一级方块都有自己的二级方块, 那么一共有8*8=64个二级方块, 而我们只需要两个数字就能定位到其中的任何一个方块, 如果每一个二级方块都有八个三级方块, 那将是一共8*8*8 = 512个方块, 但我们只需要3个八进制数字就能定位到其中的任何一个方块, 这有点像Huffman编码不是吗, 所以八叉树可以理解为空间上的编码树.而如果场景被分割了n级, 我们最坏也只需要也就是正比于树的深度的时间去访问到任意一个最小分隔的空间单元:
通过对空间进行八叉树分割我们可以得到快速查找某一块空间的能力, 但是接下来呢?
我们性能提升的关键是如何减少CPU-GPU总线通信的次数, 以及GPU渲染网格的数量. 八叉树空间分割的技术正是牺牲一小部分(非常非常小)的CPU时间去筛选我们所需要渲染的网格并且将其传送至GPU, 不仅减少了总线通信负担, 也减少了GPU渲染负担. 那么如何在不影响观感的情况下, 每次渲染中抛弃我们不需要的元素呢? 对于减少剔除非必要渲染元素我们已经有很多方法了: 面剔除, 深度测试, 模板测试, 视锥体裁剪, 但很可惜, 这些都是在GPU中进行的, 所以我们并不能再额外使用这些技术去达到我们想要的目的. 这里引入一个概念: 包围盒测试和视锥体测试, 和所有测试一样, 包围盒测试也用于丢弃不需要渲染的片段, 其本质在于测试某两个包围盒是否互相包含或者相交, 视锥体测试则用于判断物体是否在视锥体内. 那么聪明的你应该想到了----我们应该只需要渲染视锥体内的元素就行了, 可是对于视锥体的测试要远远比包围盒测试复杂, 所以本文提供的一个方法是: 先计算视锥体的包围盒, 再与场景包围盒进行测试.
不过让我们先忘记视锥体吧, 我们可以先专注于如何从已知包围盒在八叉树中查询在包围盒中的物体. 在这里我们正式从概念到引入有关八叉树的具体实现.
首先是包围盒Bounds类:
struct Bounds {
glm::vec3 min;
glm::vec3 max;
Bounds(glm::vec3 min, glm::vec3 max);
Bounds();
glm::vec3 dimensions() const;
bool contains(const Bounds& other) const;
bool intersects(const Bounds& other) const;
glm::vec3 Center()const;
};
我们在Bounds中储存两个长度为3的向量,并且除了构造函数以外设计了用于进行包含测试于相交测试的函数. 为什么储存两个长度为3的向量呢, 长方体包围盒不是有6个顶点吗, 其实为了描述一个长方体,我们不需要知道6个顶点的坐标, 我们只需要知道6个顶点在每个维度上的最小值和最大值就好了, 我们有3个维度, 因此一共有6个值, 最小值都由glm::vec3 min存储, 最大值由glm::vec3 max存储. 如果你还不理解为什么这样做, 请你打开任一一个可以快速绘制长方体的建模软件, 选择长方体, 你会发现只要鼠标从一个点按下并拖动到另一个点就可以完成长方体的生成, 所以我们实际上只需要特定的两个点就可以完成一个长方体的描述.
contains()函数和intersects()函数主要负责包含测试与交集测试. 具体的包含测试和交集测试比较简单, 这里不多赘述. 函数体会在文章末尾给出.
然后是八叉树类与节点类结构:
class Node {
public:
Bounds bounds;
Node* children[8];
std::vector<Mesh*> objects;
Node(const Bounds& bounds, Node* parent = nullptr);
void insert(Mesh* mesh);
std::vector<Mesh*> get_objects_in_bounds(const Bounds& query_bounds) const;
int get_octant(const glm::vec3& center) const;
bool is_leaf;
~Node();
private:
glm::vec3 center;
Node* parent;
void split_node();
};
class OctantTree {
public:
OctantTree(Bounds bounds);
void insert(Mesh* mesh);
std::vector<Mesh*> get_objects_in_bounds(const Bounds& bounds)const;
private:
std::unique_ptr<Node> _root;
int max_depth_;
};
至于八叉树类中的OctantTree::std::unique_ptr<Node> _root, 这完全可以用Node*来替代, 不过你需要注意的是内存资源的获取与释放, 这里用智能指针动态管理根节点, 如果你使用普通指针, 资源释放就全交给你自由发挥啦. 对于八叉树类我们只有很简单的两个函数:insert()与get_objects_in_bounds(), 第一个用于插入一个网格对象, 第二个用于获取树中被所传入包围框包含或相交片段向量. 对于节点类我们储存一个Bounds类成员, 代表这个节点所占据的空间, 我们的节点就类似于之前所展示的正方体, 节点所在树的层级就是正方体的级数, 而bound就是对该正方体所占据的空间的描述. 我们储存了长度为8的Node*类数组用于保存8个孩子节点. 一个元素为Mesh*类型的向量objects用来存储当前节点所占空间中分布的物体, 除了构造函数以外, 我们也有一个insert()和一个get_objects_in_bounds(), 还有一个bool值变量记录这个节点是否为子叶节点, 保存一个vec3类型的变量center, 还有其父节点指针parent;get_octant()函数可以获取包围盒及其中心属于当前节点的哪个子节点.split_node()用于执行当前节点存放过多物体的情况时的节点下溢分割.我们还有一个glm::vec3类的成员center用于保存当前节点所管理空间的几何中心. 其中关键的是我们的节点插入和下溢处理算法:
void Node::insert(Mesh* mesh) {
if (objects.size() < MaxObjects && is_leaf) {
objects.push_back(mesh);
return;
}
if (is_leaf) {
is_leaf = false;
split_node();
}
int octant = get_octant(mesh->center);
if (octant >= 0 && children[octant] != nullptr) {
children[octant]->insert(mesh);
}
else {
objects.push_back(mesh);
}
}
void Node::split_node() {
using glm::vec3;
// assert(is_leaf && "Node has already been split.");
vec3 half_dims = bounds.dimensions() / 2.f;
children[Oc_FRONT_BOTTOM_LEFT] = new Node(Bounds{
vec3(bounds.min.x, bounds.min.y, bounds.min.z),
vec3(bounds.min.x + half_dims.x, bounds.min.y + half_dims.y, bounds.min.z + half_dims.z)
}, this);
children[Oc_FRONT_BOTTOM_RIGHT] = new Node(Bounds{
vec3(bounds.min.x + half_dims.x, bounds.min.y, bounds.min.z),
vec3(bounds.max.x, bounds.min.y + half_dims.y, bounds.min.z + half_dims.z)
}, this);
children[Oc_FRONT_TOP_LEFT] = new Node(Bounds{
vec3(bounds.min.x, bounds.min.y + half_dims.y, bounds.min.z),
vec3(bounds.min.x + half_dims.x, bounds.max.y, bounds.min.z + half_dims.z)
}, this);
children[Oc_FRONT_TOP_RIGHT] = new Node(Bounds{
vec3(bounds.min.x + half_dims.x, bounds.min.y + half_dims.y, bounds.min.z),
vec3(bounds.max.x, bounds.max.y, bounds.min.z + half_dims.z)
}, this);
children[Oc_BACK_BOTTOM_LEFT] = new Node(Bounds{
vec3(bounds.min.x, bounds.min.y, bounds.min.z + half_dims.z),
vec3(bounds.min.x + half_dims.x, bounds.min.y + half_dims.y, bounds.max.z)
}, this);
children[Oc_BACK_BOTTOM_RIGHT] = new Node(Bounds{
vec3(bounds.min.x + half_dims.x, bounds.min.y, bounds.min.z + half_dims.z),
vec3(bounds.max.x, bounds.min.y + half_dims.y, bounds.max.z)
}, this);
children[Oc_BACK_TOP_LEFT] = new Node(Bounds{
vec3(bounds.min.x, bounds.min.y + half_dims.y, bounds.min.z + half_dims.z),
vec3(bounds.min.x + half_dims.x, bounds.max.y, bounds.max.z)
}, this);
children[Oc_BACK_TOP_RIGHT] = new Node(Bounds{
vec3(bounds.min.x + half_dims.x, bounds.min.y + half_dims.y, bounds.min.z + half_dims.z),
vec3(bounds.max.x, bounds.max.y, bounds.max.z)
}, this);
for (auto obj : objects) {
int octant = get_octant(obj->_bounds->Center());
children[octant]->insert(obj);
}
objects.clear();
}
代码有点长但是原理很简单, 其中多次调用了get_octant()
int Node::get_octant(const glm::vec3& objcenter)const {
int oct = 0; // 0b00000000
glm::vec3 const& center = this->center;
if (center.x < objcenter.x) oct |= 1; // 0b00000001
if (center.y < objcenter.y) oct |= 2; // 0b00000010
if (center.z < objcenter.z) oct |= 4; // 0b00000100
return oct;
}
这个函数的原理也很简单, 前提是你熟悉位运算, 1, 2, 4对于二进制编码分别为0b000001, 0b000010, 0b000100, 所以我们通过三次这样的或运算测试就可以得到当前objcenter位于节点的什么位置, 你应该自己去试着动手算一样, 每种情况对于的oct值分别是多少, 这样你就能并且实际在建树中的编码顺序了.
另一个重要的函数是get_object_in_bounds():
void Node::get_objects_in_bounds(const Bounds& query_bounds, std::vector<Mesh*>& result) const {
std::stack<Node*> node_stack;
// 添加当前节点到栈中,准备开始遍历
node_stack.push(const_cast<Node*>(this));
while (!node_stack.empty()) {
Node* current_node = node_stack.top();
node_stack.pop();
// 节点与查询区域有交集
if (current_node->bounds.intersects(query_bounds)) {
// 查询区域包含整个节点,直接返回节点中的所有物体
if (query_bounds.contains(current_node->bounds)) {
result.insert(result.end(), current_node->objects.begin(), current_node->objects.end());
for (int i = 0; i < 8; ++i) {
if (current_node->children[i] != nullptr) {
// 将子节点入栈,准备遍历
node_stack.push(current_node->children[i]);
}
}
}
else { // 节点部分或与查询区域相交
// 选择节点中包含在查询区域中的物体,将它们添加到结果中
for (const auto& object : current_node->objects) {
if (query_bounds.intersects(object->_bounds)) {
result.push_back(object);
}
}
// 遍历子节点
for (int i = 0; i < 8; ++i) {
if (current_node->children[i] != nullptr) {
node_stack.push(current_node->children[i]);
}
}
}
}
}
}
函数的逻辑很简单, 在结合注释你应该就能看懂, 通过这个函数我们就能获得在query_bounds中的待渲染物品了.
那么, 我们接下来要做的事就是得到包含我们所需要渲染的物品的包围框, 如何得到呢?
既然透视投影矩阵可以将视锥体压成标准正方体, 那么对标准正方体的6个顶点做透视投影的逆变换就可以还原出视锥体的6个顶点, 那么我只需要取这六个顶点各个分量的最小值和最大值组成2个三维向量就可以完成对视锥体的最小外接包围盒的表述.
Bounds CalculateBounds(Camera& camera, glm::mat4& modelMatrix, glm::mat4& V, glm::mat4& P) {
glm::mat4 view_projection_matrix = P * V;
glm::vec3 min_bounds, max_bounds;
// 获取当前视锥体的六个面
calculate_frustum_bounds(view_projection_matrix, min_bounds, max_bounds);
glm::vec4 in_bounds(std::move(min_bounds), 1.0f), ax_bounds(std::move(max_bounds), 1.0f);
glm::mat4 inv_modelMatrix = glm::inverse(modelMatrix);
in_bounds = inv_modelMatrix * in_bounds, ax_bounds = inv_modelMatrix * ax_bounds ;
return Bounds(glm::vec3(in_bounds), glm::vec3(ax_bounds));
}
void calculate_frustum_bounds(const glm::mat4& view_projection_matrix,
glm::vec3& min_bounds, glm::vec3& max_bounds)
{
auto inv_vp = glm::inverse(view_projection_matrix);
const auto near_top_left = get_frustum_corner(-1.0f, 1.0f, -1.0f, inv_vp);
const auto near_top_right = get_frustum_corner(1.0f, 1.0f, -1.0f, inv_vp);
const auto near_bottom_left = get_frustum_corner(-1.0f, -1.0f, -1.0f, inv_vp);
const auto near_bottom_right = get_frustum_corner(1.0f, -1.0f, -1.0f, inv_vp);
const auto far_top_left = get_frustum_corner(-1.0f, 1.0f, 1.0f, inv_vp);
const auto far_top_right = get_frustum_corner(1.0f, 1.0f, 1.0f, inv_vp);
const auto far_bottom_left = get_frustum_corner(-1.0f, -1.0f, 1.0f, inv_vp);
const auto far_bottom_right = get_frustum_corner(1.0f, -1.0f, 1.0f, inv_vp);
std::array<glm::vec3, 8> frustum_vertices{ near_top_left, near_bottom_left,
near_top_right, near_bottom_right,
far_top_left, far_bottom_left,
far_top_right, far_bottom_right };
min_bounds = std::reduce(frustum_vertices.cbegin(), frustum_vertices.cend(),
glm::vec3(std::numeric_limits<float>::max()),
[](auto lhs, auto rhs) { return glm::min(lhs, rhs); });
max_bounds = std::reduce(frustum_vertices.cbegin(), frustum_vertices.cend(),
glm::vec3(-std::numeric_limits<float>::max()),
[](auto lhs, auto rhs) { return glm::max(lhs, rhs); });
}
glm::vec3 get_frustum_corner(float x, float y, float z, const glm::mat4& inv_vp)
{
glm::vec4 clip((x * 2.0f - 1.0f), (y * 2.0f - 1.0f), (z * 2.0f - 1.0f), 1.0f);
auto ndc = inv_vp * clip;
return glm::vec3(ndc) / ndc.w;
}
至此, 我们就完成了所有八叉树与包围盒测试的内容, 还有最后一部分工作就是----修改Model::Draw()中的函数
void Model::Draw(My_Shader& shader, const Bounds& bound)
{
std::vector<Mesh*> vm;
this->_3DSceneTree->get_objects_in_bounds(bound, vm);
std::for_each(vm.begin(), vm.end(), [&shader](Mesh* mesh) {mesh->Draw(shader); });
}
void Model::Draw(My_Shader& shader) {
std::for_each(meshes.begin(), meshes.end(), [&shader](Mesh* mesh) { mesh->Draw(shader); });
}
保留原来的Draw接口, 我们写一个新的接口其多接收一个参数const Bounds&类型的参数, 然后先筛选出我们需要渲染的物品, 再遍历这个物品容器进行渲染.
# 于23/7/24日更新:
我认为执行的代码还不够简洁, 这下我让他足够简洁了:
void Model::Draw(My_Shader& shader, const Bounds& bound)
{
for (auto const& mesh : _3DSceneTree & bound) {
mesh->Draw(shader);
}
}
初看这段代码你可能会对这个范围for的遍历对象有一点疑问, 这是一个什么玩意? 实际上, 我添加了一个这样的友元函数:
friend std::vector<Mesh*> operator & (OctantTree* ,const Bounds& bounds);
我的灵感来源于集合求交的运算, 我希望在场景树中取得与bounds有交集的部分, 所以我打算用与运算来代替这个过程:
std::vector<Mesh*> operator & (OctantTree* Tree, const Bounds& bounds) {
std::vector<Mesh*> res;
Tree->get_objects_in_bounds(bounds, res);
return res;
}
并且你可以把get_objects_in_bounds()函数纳入private成员了. 对于Bounds类也有类似的求交和包含关系的函数, 我们也可以用逻辑运算符将他们替换:
bool operator | (const Bounds& A, const Bounds& B); // 表示A是否被B包含
bool operator & (const Bounds& A, const Bounds& B); // 表示A是否与B有交集
bool operator | (const Bounds& A, const Bounds& B) { //包含测试
return B.min.x <= A.min.x && B.max.x >= A.max.x &&
B.min.y <= A.min.y && B.max.y >= A.max.y &&
B.min.z <= A.min.z && B.max.z >= A.max.z;
}
bool operator & (const Bounds& A, const Bounds& B) { // 交集测试
return A.min.x <= B.max.x && A.max.x >= B.min.x &&
A.min.y <= B.max.y && A.max.y >= B.min.y &&
A.min.z <= B.max.z && A.max.z >= B.min.z;
}
修改一下这个函数中设计交集和包含运算的条件表达式语句
void Node::get_objects_in_bounds(const Bounds& query_bounds, std::vector<Mesh*>& result) const {
std::stack<Node*> node_stack;
// 添加当前节点到栈中,准备开始遍历
node_stack.push(const_cast<Node*>(this));
while (!node_stack.empty()) {
Node* current_node = node_stack.top(); node_stack.pop();
// 节点与查询区域有交集
if (current_node->bounds & query_bounds) {
// 查询区域包含整个节点,直接取节点中的所有物体
if (current_node->bounds | query_bounds) {
result.insert(result.end(), current_node->objects.begin(), current_node->objects.end());
for (int i = 0; i < 8; ++i) {
if (current_node->children[i] != nullptr) {
node_stack.push(current_node->children[i]);
}
}
}
else { // 节点部分或与查询区域相交
// 选择节点中包含在查询区域中的物体,将它们添加到结果中
if(current_node->is_leaf) { //如果是叶节点直接全部加入
result.insert(result.begin(), current_node->objects.begin(), current_node->objects.end());
}
else {
for (const auto& object : current_node->objects) {
if (query_bounds & object->_bounds) {
result.push_back(object);
}
}
// 遍历子节点
for (int i = 0; i < 8; ++i) {
if (current_node->children[i] != nullptr) {
node_stack.push(current_node->children[i]);
}
}
}
}
}
}
}
然后本教程到这里就结束了, 下面是所有的源码.
Bounds.h
#pragma once
#ifndef BOUNDS
#define BOUNDS
#include "Glm.h"
struct Bounds {
glm::vec3 min;
glm::vec3 max;
Bounds(glm::vec3 min, glm::vec3 max);
Bounds();
glm::vec3 dimensions() const;
glm::vec3 Center()const;
};
bool operator | (const Bounds& A, const Bounds& B); // 表示A是否被B包含
bool operator & (const Bounds& A, const Bounds& B); // 表示A是否与B有交集
#endif
Bounds.cpp
#include "Bounds.h"
Bounds::Bounds(glm::vec3 min, glm::vec3 max) : min(min), max(max) {}
Bounds::Bounds() :min(glm::vec3(-2000, -2000, -2000)), max(glm::vec3(2000, 2000, 2000)) {};
glm::vec3 Bounds::dimensions() const { return glm::vec3(max.x - min.x, max.y - min.y, max.z - min.z); }
bool operator | (const Bounds& A, const Bounds& B) { //包含测试
return B.min.x <= A.min.x && B.max.x >= A.max.x &&
B.min.y <= A.min.y && B.max.y >= A.max.y &&
B.min.z <= A.min.z && B.max.z >= A.max.z;
}
bool operator & (const Bounds& A, const Bounds& B) { // 交集测试
return A.min.x <= B.max.x && A.max.x >= B.min.x &&
A.min.y <= B.max.y && A.max.y >= B.min.y &&
A.min.z <= B.max.z && A.max.z >= B.min.z;
}
Mesh.h
#pragma once
#ifndef MESH_H
#define MESH_H
#include <glad/glad.h> // holds all OpenGL type declarations
#include "Shader.h"
#include "Bounds.h"
#include <string>
#include <vector>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "stb_image.h"
#define MAX_BONE_INFLUENCE 4
struct Vertex {
// position
glm::vec3 Position;
// normal
glm::vec3 Normal;
// texCoords
glm::vec2 TexCoords;
// tangent
glm::vec3 Tangent;
// bitangent
glm::vec3 Bitangent;
//bone indexes which will influence this vertex
int m_BoneIDs[MAX_BONE_INFLUENCE];
//weights from each bone
float m_Weights[MAX_BONE_INFLUENCE];
};
struct Texture {
unsigned int id;
std::string type;
std::string path;
Texture(const Texture& Tex): id(Tex.id), type(Tex.type), path(Tex.path)
{
};
Texture():id(-1) {};
Texture(Texture&& Tex) noexcept : id(Tex.id) {
type = std::move(Tex.type);
path = std::move(Tex.path);
};
Texture& operator=(const Texture& Tex){
if (this == &Tex) return *this;
id = Tex.id;
type = Tex.type;
path = Tex.path;
return *this;
}
Texture& operator=(Texture&& Tex) noexcept {
if (this == &Tex) return *this;
id = Tex.id;
type = std::move(Tex.type);
path = std::move(Tex.path);
return *this;
}
};
class Mesh {
public:
// mesh Data
std::vector<Vertex> vertices;
std::vector<unsigned int> indices;
std::vector<Texture> textures;
unsigned int VAO;
Bounds _bounds;
// constructor
Mesh(std::vector<Vertex>&& vertices, std::vector<unsigned int>&& indices, std::vector<Texture>&& textures);
Mesh(const Mesh& M)noexcept :vertices(M.vertices), indices(M.indices), textures(M.textures), _bounds(M._bounds),
VAO(M.VAO), VBO(M.VBO), EBO(M.EBO){};
Mesh(Mesh&& M)noexcept:VAO(M.VAO), VBO(M.VBO), EBO(M.EBO), _bounds(M._bounds){
vertices = std::move(M.vertices);
indices = std::move(M.indices);
textures = std::move(M.textures);
}
// render the mesh
void Draw(Shader& shader);
private:
// render data
unsigned int VBO, EBO;
// initializes all the buffer objects/arrays
void setupMesh();
};
#endif
Mesh.cpp
#include "Mesh.h"
#include "Glm.h"
#include <array>
#include <numeric>
const unsigned int SCR_WIDTH = 1000;
const unsigned int SCR_HEIGHT = 666;
using namespace std;
void Mesh::Draw(My_Shader& shader)
{
// bind appropriate textures
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
unsigned int normalNr = 1;
unsigned int heightNr = 1;
for (unsigned int i = 0; i < textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding
// retrieve texture number (the N in diffuse_textureN)
string number;
string name = textures[i].type;
if (name == "texture_diffuse")
number = std::to_string(diffuseNr++);
else if (name == "texture_specular")
number = std::to_string(specularNr++); // transfer unsigned int to string
else if (name == "texture_normal")
number = std::to_string(normalNr++); // transfer unsigned int to string
else if (name == "texture_height")
number = std::to_string(heightNr++); // transfer unsigned int to string
// now set the sampler to the correct texture unit
glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);
// and finally bind the texture
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
// draw mesh
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// always good practice to set everything back to defaults once configured.
// glActiveTexture(GL_TEXTURE0);
}
Mesh::Mesh(vector<Vertex>&& _vertices, vector<unsigned int>&& _indices, vector<Texture>&& _textures)
{
this->vertices = std::move(_vertices);
this->indices = std::move(_indices);
this->textures = std::move(_textures);
float x_min = std::numeric_limits<float>::infinity();
float x_max = -std::numeric_limits<float>::infinity();
float y_min = std::numeric_limits<float>::infinity();
float y_max = -std::numeric_limits<float>::infinity();
float z_min = std::numeric_limits<float>::infinity();
float z_max = -std::numeric_limits<float>::infinity();
for (size_t i = 0; i < this->vertices.size(); ++ i) {
auto& Pos = vertices[i].Position;
float x = Pos.x;
float y = Pos.y;
float z = Pos.z;
x_min = std::min(x_min, x);
x_max = std::max(x_max, x);
y_min = std::min(y_min, y);
y_max = std::max(y_max, y);
z_min = std::min(z_min, z);
z_max = std::max(z_max, z);
}
_bounds = Bounds( glm::vec3(x_min, y_min, z_min), glm::vec3(x_max, y_max, z_max ) );
// now that we have all the required data, set the vertex buffers and its attribute pointers.
setupMesh();
}
void Mesh::setupMesh()
{
// create buffers/arrays
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
// load data into vertex buffers
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// A great thing about structs is that their memory layout is sequential for all its items.
// The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which
// again translates to 3/2 floats which translates to a byte array.
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
// set the vertex attribute pointers
// vertex Positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// vertex normals
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// vertex texture coords
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
// vertex tangent
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
// vertex bitangent
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));
// ids
glEnableVertexAttribArray(5);
glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));
// weights
glEnableVertexAttribArray(6);
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights));
glBindVertexArray(0);
}
OctantTree.h
#pragma once
#ifndef __OCTANTTREE__
#define __OCTANTTREE__
#include "Camera_Movement.h"
#include <vector>
#include <list>
#include <memory>
#include "Bounds.h"
#include "Mesh.h"
#include <mutex>
enum Octant {
Oc_FRONT_BOTTOM_LEFT = 0,
Oc_FRONT_BOTTOM_RIGHT = 1,
Oc_FRONT_TOP_LEFT = 2,
Oc_FRONT_TOP_RIGHT = 3,
Oc_BACK_BOTTOM_LEFT = 4,
Oc_BACK_BOTTOM_RIGHT = 5,
Oc_BACK_TOP_LEFT = 6,
Oc_BACK_TOP_RIGHT = 7,
};
Bounds CalculateBounds(Camera& camera, glm::mat4&, glm::mat4& V, glm::mat4& P);
unsigned int TextureFromFile(const char* path, const std::string& directory, bool gamma = false);
void calculate_frustum_bounds(const glm::mat4& view_projection_matrix, glm::vec3& min_bounds, glm::vec3& max_bounds);
glm::vec3 get_frustum_corner(float x, float y, float z, const glm::mat4& inv_vp);
class Node {
private:
static const int MaxObjects = 75;
public:
Bounds bounds;
Node* children[8];
std::vector<Mesh*> objects;
Node(const Bounds& bounds, Node* parent = nullptr);
void insert(Mesh* mesh);
void get_objects_in_bounds(const Bounds& query_bounds, std::vector<Mesh*>& result) const;
int get_octant(const glm::vec3& center) const;
bool is_leaf;
~Node();
private:
glm::vec3 center;
Node* parent;
void split_node();
};
class OctantTree {
public:
OctantTree(Bounds bounds);
std::mutex _mutex;
void insert(Mesh* mesh);
friend std::vector<Mesh*> operator & (OctantTree* ,const Bounds& bounds);
private:
std::unique_ptr<Node> _root;
void get_objects_in_bounds(const Bounds& bounds, std::vector<Mesh*>& res)const;
};
#endif
OctantTree.cpp
#include "OctantTree.h"
#include <array>
#include <numeric>
#include <stack>
OctantTree::OctantTree(Bounds bounds) {
_root = std::make_unique<Node>(bounds);
}
void OctantTree::insert(Mesh* mesh) {
_root->insert(mesh);
}
void OctantTree::get_objects_in_bounds(const Bounds& query_bounds, std::vector<Mesh*>& res) const {
return _root->get_objects_in_bounds(query_bounds, res);
}
std::vector<Mesh*> operator & (OctantTree* Tree, const Bounds& bounds) {
std::vector<Mesh*> res;
Tree->get_objects_in_bounds(bounds, res);
return res;
}
Node::Node(const Bounds& bounds, Node* parent) :
bounds(bounds),
center(bounds.Center()),
parent(parent),
is_leaf(true)
{
for (int i = 0; i < 8; ++i) {
children[i] = nullptr;
}
}
Node::~Node() {
for (int i = 0; i < 8; ++i) {
if (children[i]) {
children[i]->parent = nullptr;
delete children[i];
children[i] = nullptr;
}
}
if (parent) {
for (int i = 0; i < 8; ++i) {
if (parent->children[i] == this) {
parent->children[i] = nullptr;
}
}
}
}
void Node::insert(Mesh* mesh) {
if (objects.size() < MaxObjects && is_leaf) {
objects.push_back(mesh);
return;
}
if (is_leaf) {
is_leaf = false;
split_node();
}
int octant = get_octant(mesh->_bounds.Center());
if (octant >= 0 && children[octant] != nullptr) {
children[octant]->insert(mesh);
}
else {
objects.push_back(mesh);
}
}
void Node::split_node() {
using glm::vec3;
vec3 half_dims = bounds.dimensions() / 2.f;
children[Oc_FRONT_BOTTOM_LEFT] = new Node(Bounds{
vec3(bounds.min.x, bounds.min.y, bounds.min.z),
vec3(bounds.min.x + half_dims.x, bounds.min.y + half_dims.y, bounds.min.z + half_dims.z)
}, this);
children[Oc_FRONT_BOTTOM_RIGHT] = new Node(Bounds{
vec3(bounds.min.x + half_dims.x, bounds.min.y, bounds.min.z),
vec3(bounds.max.x, bounds.min.y + half_dims.y, bounds.min.z + half_dims.z)
}, this);
children[Oc_FRONT_TOP_LEFT] = new Node(Bounds{
vec3(bounds.min.x, bounds.min.y + half_dims.y, bounds.min.z),
vec3(bounds.min.x + half_dims.x, bounds.max.y, bounds.min.z + half_dims.z)
}, this);
children[Oc_FRONT_TOP_RIGHT] = new Node(Bounds{
vec3(bounds.min.x + half_dims.x, bounds.min.y + half_dims.y, bounds.min.z),
vec3(bounds.max.x, bounds.max.y, bounds.min.z + half_dims.z)
}, this);
children[Oc_BACK_BOTTOM_LEFT] = new Node(Bounds{
vec3(bounds.min.x, bounds.min.y, bounds.min.z + half_dims.z),
vec3(bounds.min.x + half_dims.x, bounds.min.y + half_dims.y, bounds.max.z)
}, this);
children[Oc_BACK_BOTTOM_RIGHT] = new Node(Bounds{
vec3(bounds.min.x + half_dims.x, bounds.min.y, bounds.min.z + half_dims.z),
vec3(bounds.max.x, bounds.min.y + half_dims.y, bounds.max.z)
}, this);
children[Oc_BACK_TOP_LEFT] = new Node(Bounds{
vec3(bounds.min.x, bounds.min.y + half_dims.y, bounds.min.z + half_dims.z),
vec3(bounds.min.x + half_dims.x, bounds.max.y, bounds.max.z)
}, this);
children[Oc_BACK_TOP_RIGHT] = new Node(Bounds{
vec3(bounds.min.x + half_dims.x, bounds.min.y + half_dims.y, bounds.min.z + half_dims.z),
vec3(bounds.max.x, bounds.max.y, bounds.max.z)
}, this);
for (auto obj : objects) {
int octant = get_octant(obj->_bounds.Center());
children[octant]->insert(obj);
}
objects.clear();
}
int Node::get_octant(const glm::vec3& objcenter)const {
int oct = 0;
glm::vec3 const& center = this->center;
if (center.x < objcenter.x) oct |= 1;
if (center.y < objcenter.y) oct |= 2;
if (center.z < objcenter.z) oct |= 4;
return oct;
}
void Node::get_objects_in_bounds(const Bounds& query_bounds, std::vector<Mesh*>& result) const {
std::stack<Node*> node_stack;
// 添加当前节点到栈中,准备开始遍历
node_stack.push(const_cast<Node*>(this));
while (!node_stack.empty()) {
Node* current_node = node_stack.top(); node_stack.pop();
// 节点与查询区域有交集
if (current_node->bounds & query_bounds) {
// 查询区域包含整个节点,直接取节点中的所有物体
if (current_node->bounds | query_bounds) {
result.insert(result.end(), current_node->objects.begin(), current_node->objects.end());
for (int i = 0; i < 8; ++i) {
if (current_node->children[i] != nullptr) {
node_stack.push(current_node->children[i]);
}
}
}
else { // 节点部分或与查询区域相交
// 选择节点中包含在查询区域中的物体,将它们添加到结果中
if(current_node->is_leaf) { //如果是叶节点直接全部加入
result.insert(result.begin(), current_node->objects.begin(), current_node->objects.end());
}
else {
for (const auto& object : current_node->objects) {
if (query_bounds & object->_bounds) {
result.push_back(object);
}
}
// 遍历子节点
for (int i = 0; i < 8; ++i) {
if (current_node->children[i] != nullptr) {
node_stack.push(current_node->children[i]);
}
}
}
}
}
}
}
Bounds CalculateBounds(Camera& camera, glm::mat4& modelMatrix, glm::mat4& V, glm::mat4& P) {
glm::mat4 view_projection_matrix = P * V;
glm::vec3 min_bounds, max_bounds;
// 获取当前视锥体的六个面
calculate_frustum_bounds(view_projection_matrix, min_bounds, max_bounds);
glm::vec4 in_bounds(min_bounds, 1.0f), ax_bounds(max_bounds, 1.0f);
glm::mat4 inv_modelMatrix = glm::inverse(modelMatrix);
in_bounds = inv_modelMatrix * in_bounds, ax_bounds = inv_modelMatrix * ax_bounds ;
min_bounds = glm::vec3(in_bounds), max_bounds = glm::vec3(ax_bounds);
return Bounds(min_bounds, max_bounds);
}
void calculate_frustum_bounds(const glm::mat4& view_projection_matrix,
glm::vec3& min_bounds, glm::vec3& max_bounds)
{
auto inv_vp = glm::inverse(view_projection_matrix);
const auto near_top_left = get_frustum_corner(-1.0f, 1.0f, -1.0f, inv_vp);
const auto near_top_right = get_frustum_corner(1.0f, 1.0f, -1.0f, inv_vp);
const auto near_bottom_left = get_frustum_corner(-1.0f, -1.0f, -1.0f, inv_vp);
const auto near_bottom_right = get_frustum_corner(1.0f, -1.0f, -1.0f, inv_vp);
const auto far_top_left = get_frustum_corner(-1.0f, 1.0f, 1.0f, inv_vp);
const auto far_top_right = get_frustum_corner(1.0f, 1.0f, 1.0f, inv_vp);
const auto far_bottom_left = get_frustum_corner(-1.0f, -1.0f, 1.0f, inv_vp);
const auto far_bottom_right = get_frustum_corner(1.0f, -1.0f, 1.0f, inv_vp);
std::array<glm::vec3, 8> frustum_vertices{ near_top_left, near_bottom_left,
near_top_right, near_bottom_right,
far_top_left, far_bottom_left,
far_top_right, far_bottom_right };
min_bounds = std::reduce(frustum_vertices.cbegin(), frustum_vertices.cend(),
glm::vec3(std::numeric_limits<float>::max()),
[](auto lhs, auto rhs) { return glm::min(lhs, rhs); });
max_bounds = std::reduce(frustum_vertices.cbegin(), frustum_vertices.cend(),
glm::vec3(-std::numeric_limits<float>::max()),
[](auto lhs, auto rhs) { return glm::max(lhs, rhs); });
}
glm::vec3 get_frustum_corner(float x, float y, float z, const glm::mat4& inv_vp)
{
glm::vec4 clip((x * 2.0f - 1.0f), (y * 2.0f - 1.0f), (z * 2.0f - 1.0f), 1.0f);
auto ndc = inv_vp * clip;
return glm::vec3(ndc) / ndc.w;
}
Model.h
#pragma once
#ifndef __MODEL__
#define __MODEL__
#include <string>
#include <vector>
#include "stb_image.h"
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "Shader.h"
#include "OctantTree.h"
class Model
{
private:
void loadModel(std::string const& path);
private:
// processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any).
void processNode(aiNode* node, const aiScene* scene);
public:
// model data
~Model();
std::vector<Texture> textures_loaded; // stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once.
std::vector<Mesh*> meshes;
std::string directory;
bool gammaCorrection;
Bounds* Bound;
OctantTree* _3DSceneTree;
// constructor, expects a filepath to a 3D model.
Model(std::string const& path, bool gamma = false);
// draws the model, and thus all its meshes
void Draw(My_Shader& shader, const Bounds& bound);
void Draw(My_Shader& shader);
private:
// loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector.
Mesh* processMesh(aiMesh* mesh, const aiScene* scene);
// checks all material textures of a given type and loads the textures if they're not loaded yet.
// the required info is returned as a Texture struct.
std::vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, const std::string& typeName);
};
#endif
Model.cpp
#include "Model.h"
using namespace std;
GLenum const fmt[] = { GL_RED, GL_RG, GL_RGB, GL_RGBA };
Model::~Model() {
if (Bound) delete Bound;
if (_3DSceneTree) delete _3DSceneTree;
unsigned int size = this->meshes.size();
for (int i = 0; i < size; ++i) {
if (meshes[i]) delete meshes[i];
}
}
Model::Model(string const& path, bool gamma) : gammaCorrection(gamma)
{
Bound = new Bounds(glm::vec3(-2000.0f, -2000.0f, -2000.0f), glm::vec3(2000.0f, 2000.0f, 2000.0f));
_3DSceneTree = new OctantTree(*Bound);
loadModel(path);
}
void Model::Draw(My_Shader& shader, const Bounds& bound)
{
for (auto const& mesh : _3DSceneTree & bound) {
mesh->Draw(shader);
}
}
void Model::Draw(My_Shader& shader) {
std::for_each(meshes.begin(), meshes.end(), [&shader](Mesh* mesh) { mesh->Draw(shader); });
}
void Model::loadModel(string const& path)
{
// read file via ASSIMP
Assimp::Importer importer;
/*const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);*/
const aiScene* scene = importer.ReadFile(
path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace
);
// check for errors
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero
{
cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
return;
}
// retrieve the directory path of the filepath
directory = path.substr(0, path.find_last_of('/'));
// process ASSIMP's root node recursively
processNode(scene->mRootNode, scene);
}
#include <stack>
// processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any).
void Model::processNode(aiNode* node, const aiScene* scene)
{
// process each mesh located at the current node
stack<aiNode*> stk;
stk.push(node);
while (stk.size()) {
auto top = stk.top(); stk.pop();
unsigned int mNM = top->mNumMeshes;
for (unsigned int i = 0; i < mNM; ++i) {
aiMesh* mesh = scene->mMeshes[top->mMeshes[i]];
Mesh* m = processMesh(mesh, scene);
this->_3DSceneTree->insert(m);
this->meshes.push_back(m);
}
unsigned int mNC = top->mNumChildren;
for (unsigned int i = 0; i < mNC; ++i) {
stk.push(top->mChildren[i]);
}
}
}
Mesh* Model::processMesh(aiMesh* mesh, const aiScene* scene)
{
// data to fill
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
// walk through each of the mesh's vertices
for (unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
glm::vec3 vector; // we declare a placeholder vector since assimp uses its own vector class that doesn't directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first.
// positions
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
// normals
if (mesh->HasNormals())
{
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
}
// texture coordinates
if (mesh->mTextureCoords[0]) // does the mesh contain texture coordinates?
{
glm::vec2 vec;
// a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't
// use models where a vertex can have multiple texture coordinates so we always take the first set (0).
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
// tangent
vector.x = mesh->mTangents[i].x;
vector.y = mesh->mTangents[i].y;
vector.z = mesh->mTangents[i].z;
vertex.Tangent = vector;
// bitangent
vector.x = mesh->mBitangents[i].x;
vector.y = mesh->mBitangents[i].y;
vector.z = mesh->mBitangents[i].z;
vertex.Bitangent = vector;
}
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
/*try {*/
vertices.push_back(vertex);
/*}
catch (std::exception& e) {
cout << e.what() << endl;
}*/
}
// now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices.
for (unsigned int i = 0; i < mesh->mNumFaces; ++i)
{
aiFace face = mesh->mFaces[i];
// retrieve all indices of the face and store them in the indices vector
for (unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
// process materials
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
// we assume a convention for sampler names in the shaders. Each diffuse texture should be named
// as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER.
// Same applies to other texture as the following list summarizes:
// diffuse: texture_diffuseN
// specular: texture_specularN
// normal: texture_normalN
// 1. diffuse maps
vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
// 2. specular maps
vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
// 3. normal maps
std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
// 4. height maps
std::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
// return a mesh object created from the extracted mesh data
return new Mesh(std::move(vertices), std::move(indices), std::move(textures));
}
vector<Texture> Model::loadMaterialTextures(aiMaterial* mat, aiTextureType type, const string& typeName)
{
vector<Texture> textures;
for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
// check if texture was loaded before and if so, continue to next iteration: skip loading a new texture
bool skip = false;
for (unsigned int j = 0; j < textures_loaded.size(); j++)
{
if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization)
break;
}
}
if (!skip)
{ // if texture hasn't been loaded already, load it
Texture texture;
texture.id = TextureFromFile(str.C_Str(), this->directory);
texture.type = typeName;
texture.path = std::move(str.C_Str());
textures.push_back(texture);
textures_loaded.push_back(texture); // store it as texture loaded for entire model, to ensure we won't unnecessary load duplicate textures.
}
}
return textures;
}
unsigned int TextureFromFile(const char* path, const string& directory, bool gamma)
{
string filename(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char* data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
if (data && nrComponents > 0 && nrComponents <= 4)
{
GLenum format = fmt[nrComponents - 1];
glBindTexture(GL_TEXTURE_2D, textureID);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}