OpenGL C++ 场景管理的最后一块拼图(也许

OpenGL C++, 模型的异步加载

请先移步下方Blog阅读完毕后继续阅读此Blog:
OpenGL场景管理–OcantTree is all you need

Introduction

既然我们已经拥有了良好的加载速度和优化的渲染策略(场景管理), 但是这离理想中的情况仍有一段距离, 我曾经关心有些游戏场景是如何被加载的----在某些游戏中的某个场景中, 场景会随着时间推移而不断发展完全, 并且在短时间内发展为一个完整的场景, 异步技术就在其中发挥作用, 即分多个线程, 一部分线程处理模型的加载, 一部分线程处理渲染的工作. 所以我也想尝试自己进行一个这样的实现.

多线程基础

自从C++11以来标准库增加了多线程部分以支持不同平台的多线程操作. 绝大多数多线程操作都包含在下面的两个头文件中:

...
#include <thread>
#include <mutex>
...

std::thread

std::thread是一个class, 是我们开启新线程的核心, 其构造函数接受一个入口函数(或仿函数, 总之是一个可调用对象), 以及若干入口函数所使用的参数.以类的成员函数作为入口有些特殊

class A{
    A();
    void foo(){cout <<"foo"<< endl;};
};
std::thread t(&A::foo/*,Args..*/);
std::thread::join() / detach()

join() 与 detach()函数的主要区别为, 主进程执行到当前join()或detach()语句时, 是否会停下等待调用join的std::thread对象执行完子线程. join()会让主线程暂停到子线程执行完之后继续运行, detach()会让主线程无视子线程是否执行完毕而继续运行(所以可能发生主线程(进程)结束后, 子线程还在执行的情况(很危险))

互斥量

互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定. 上一个线程的某一处lock住时其他线程执行到lock时会停下, 直到其他线程unlock

lock_guard类模板

lock_guard sbguard(myMutex);取代lock()和unlock();
lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()

用unique_lock取代lock_guard

unique_lock有第二个参数, 分别在以下情况使用:

在这里插入图片描述

以及, thread, lock_guard, unique_lock都只支持移动构造和移动赋值, 即同时只能有一方拥有其所有权.

std::condition_variable

std::condition_variable实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成。
如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行
如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行。
阻塞到什么时候为止呢?阻塞到其他某个线程调用notify_one()成员函数为止.
如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样.
wait()将解锁互斥量,并阻塞到本行,阻塞到其他某个线程调用notify_one()成员函数为止。

std::mutex mymutex1;
std::unique_lock<std::mutex> sbguard1(mymutex1);
std::condition_variable condition;
condition.wait(sbguard1, [this] {if (...)
                                    return true;
                                return false;
                                });   
当其他线程用notify_one()将本线程wait()唤醒后,这个wait恢复后:

1、wait()不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()这里等待获取,如果获取到了,那么wait()就继续执行,获取到了锁
2、如果wait有第二个参数就判断这个lambda表达式。
a)如果表达式为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify_one()唤醒
b)如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住).

notify_one():通知一个线程的wait()
notify_all():通知所有线程的wait()

最后, 在多线程设计中有一个原则----用lock和unlock(或lock_guard, unique_lock)保护的代码段越小, 粒度越细, 执行效率就越高.

回到模型加载

我们希望在主线程中负责渲染, 分几个子线程去从文件中加载模型, 然后我们一边渲染, 一边进行模型的加载. 为了避免脏读脏写, 我们需要在读取操作前, 写入操作后暂停写入操作(上锁)并进行读取, 在写入操作前, 读取操作后暂停读取操作, 并进行写入.经过作者的大量试错, 我总结出了可以在Model类中完成多线程与上锁操作的方法, 其中涉及的一些不重要的逻辑和语法坑本篇就不多讲述了.负责读操作的函数就是Model::Draw(), 其中又委托给了OctantTree::getObjectInBounds()函数获取待渲染向量, 负责写入Model类的函数是Model::loadModel(), 然后具体操作又委托给了Model::processNode();

Model::loadModel()函数原来是在构造函数中调用的, 而我们需要将他作为线程的入口函数, 但是由于变量的作用域/生存期等等的安全因素, 不适合在构造函数中开启一个线程, 因此我们需要在main函数中主动开启一个线程调用loadModel();

Model model;
std::thread tmodel(&Model::loadModel, &model, path); 
......
tmodel.join();
// 必须得传递model的地址才能够在子线程中对model进行写入 否则子线程中进行的一切操作对于这个变量都是没效果的!
接下来就得专注于内部的实现了, 首先为Model类添加几个成员变量
#include <mutex>

class Model {
...
private:
    bool is_reading, is_writing;
    std::mutex m_mutex;
    std::condition_variable m_condition;
...
};

然后我们先修改Draw()函数, 就像之前所说的那样, 我们要先等待本次写入(读取模型)结束, 再进行读取, 所以我们使用两个标记变量, 利用m_condition.wait()来控制等待
我们在写入开始时都会标记is_writing为true, 结束时都会标记is_writing为false, 并且提醒主线程可以开始读入, 在读取开始时都会标记is_reading为true, 结束时标记为false, 并且提醒子线程可以继续读取.

void Model::Draw(My_Shader& shader, const Bounds& bound)
{
    vector<Mesh*>* vec;
    {
        std::unique_lock<mutex> mtxguard(m_mutex);
        m_condition.wait(mtxguard, [this] {
            return !this->is_writing;
            });
        is_reading = true;
        vec = new vector<Mesh*>(std::move(_3DSceneTree & bound));
        is_reading = false;
        m_condition.notify_one();
    }
    shader.use();
    for (auto const& mesh : *vec) {
        mesh->Draw(shader);
    }
}

void Model::Draw(My_Shader& shader) {
    std::unique_lock<mutex> mtxguard(m_mutex);
    m_condition.wait(mtxguard, [this] {
        return !this->is_writing;
        });
    is_reading = true;
    shader.use();
    std::for_each(meshes.begin(), meshes.end(), [&shader](Mesh* mesh) { mesh->Draw(shader); });
    is_reading = false;
    m_condition.notify_one();
}
在写入时也是如此
// 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; 
        {
            std::unique_lock<mutex> mtxguard(m_mutex);
            m_condition.wait(mtxguard, [this] {
                return !this->is_reading;
                });
            is_writing = true;
            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);
            }
            is_writing = false;
            m_condition.notify_one(); 
        }
        unsigned int mNC = top->mNumChildren;
        for (unsigned int i = 0; i < mNC; ++i) {
            stk.push(top->mChildren[i]);
        }
    }
}

注意这里加上了一个看似无意义的中括号, 实际上是为了控制mtxguard的生存期, 最大限度减小加锁的范围

调整

理论上来说这样就可以实现异步模型加载和渲染了, 但是在编译通过开始运行时程序崩溃于glDrawElements().
崩溃时IDE抛出如下异常:
在这里插入图片描述

发现崩溃时我的心情也是如此崩溃, 但在搜寻资料时我找到了答案:
在这里插入图片描述

出处
不仅是纹理id是不正确的, 而是所有绑定缓冲区时返回的id都是不正确的, 因此我们需要把绑定缓冲区的操作从子线程中抽取出来放到主线程来运行. 我们需要操作的有两大缓冲区: 顶点缓冲区(VAO, VBO, EBO)与纹理缓冲, 遗憾的是, 因为纹理缓冲的函数调用深度太深, 将其抽出过于困难, 所以接下来仅介绍将绑定顶点缓冲的过程抽出到主线程执行的办法

Mesh::setupMesh()

Mesh::setupMesh()负责将顶点数组及索引创建缓冲区并发送到GPU, 在执行Mesh的构造函数时被调用. Mesh的构造完全在子线程中执行, 但是Mesh的渲染是在主线程中执行的, 所以我为Mesh添加一个bool类型的成员变量Meshseted, 默认值为false, 并且在构造函数中取消对setupMesh()的调用, 当这个mesh被第一次渲染时, 调用setupMesh(), 并把Meshseted置为true, 此后渲染便不会重复调用这个函数.

void Mesh::Draw(const 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 const& 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
    if (!Meshseted) { // If unseted up, set
        setupMesh(); Meshseted = true;
    }
    glBindVertexArray(VAO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    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);

这样以来就实实在在地实现了模型的异步加载了----除了纹理无法被正确显示, 我希望有其他人能够尝试去将纹理绑定的操作抽出, 让纹理也能正确显示, 这样以来场景管理的最后一块拼图就算完成了.

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值