Games104 源码解析笔记

Games104 源码解析笔记

学习网站

学习 Git 的网站

https://learngitbranching.js.org/?locale=zh_CN

学习 cmake

cmake 官网和

https://github.com/SFUMECJF/cmake-examples-Chinese

sourcetrail 使用

一般拿到手都是先编译吗,得到他要的那个 json 文件

但是也可以用空项目

如果出现有些文件没有识别出来,看一看报错信息,大概率是有些头文件找不到

那就是头文件的路径不对

我看别人还有出现 C++ 版本不对,或者自己是 mac 系统但是包含了 win 或 linux 的文件

源文件就看 source 就行了

在这里插入图片描述

头文件这里,直接用 auto-detect

在这里插入图片描述

选择要找的文件夹的时候记得要多选择到 engine 文件夹

在这里插入图片描述

因为调用的第三方库没有放在 source 而是放在 source 外和 source 平级,所以干脆选择 engine 文件夹,一定不会出错

之后再生成的 sourcetrail 项目就没有报错了,没有报错就不会缺少文件信息

代码结构

engine 的主要作用:启动和关闭各系统

engine\source\runtime\engine.h

    class PiccoloEngine
    {
...
    public:
        void startEngine(const std::string& config_file_path);
        void shutdownEngine();

        void initialize();
        void clear();

        bool isQuit() const { return m_is_quit; }
        void run();
        bool tickOneFrame(float delta_time);

startEngine 里面注册反射,使用 g_runtime_global_context 启动各个系统

shutdownEngine() 里面使用 g_runtime_global_context 结束各个系统,然后注销反射

run() 在 while 中判断时都要关闭窗口,如果不关闭,那么执行一个 tickOneFrame(delta_time);

一个 tick 里面有逻辑帧的 tick 和渲染帧的 tick

所以 engine 最主要的是使用 g_runtime_global_context 启动和关闭各个系统

g_runtime_global_context

void RuntimeGlobalContext::startSystems(const std::string& config_file_path) 中初始化了各个 system 和 manager

system 偏向于使用者的物理支持,例如窗口,输入,渲染

manager 偏向于游戏逻辑,例如物理,粒子

tickOneFrame

bool PiccoloEngine::tickOneFrame(float delta_time) 里面分为逻辑帧和渲染帧

先算逻辑帧,然后在逻辑帧和渲染帧之间交换数据,然后算渲染帧

然后还有一些不方便划分到逻辑或者渲染中,但是又需要更新的东西,例如物理,事件,应用窗口标题

void PiccoloEngine::logicalTick(float delta_time) 中首先 tick 了 world 然后 tick 了 input

world 是逻辑实体的载体,world 由一个个 level 组成

void WorldManager::tick(float delta_time) 如果不存在 world,那么根据路径去加载 world;tick 关卡的 level

一个 world 的 tick 中只会执行一个 active_level 的 tick

void Level::tick(float delta_time) 中,tick 物体,角色,物理

GameObject 的 tick void GObject::tick(float delta_time) 只是遍历这个 GameObject 的所有 Component 的 tick

执行 Lua 脚本

下载 lua 和 sol2

在 3rdparty 中的 CMakeList 中添加 lua 和 sol2 的编译语句

参考 Jolt 的写法

if(NOT TARGET Jolt)
    option(TARGET_HELLO_WORLD "" OFF)
    option(TARGET_PERFORMANCE_TEST "" OFF)
    option(TARGET_SAMPLES "" OFF)
    option(TARGET_UNIT_TESTS "" OFF)
    option(TARGET_VIEWER "" OFF)

    if(ENABLE_PHYSICS_DEBUG_RENDERER)
        option(TARGET_TEST_FRAMEWORK "" ON)
    else()
        option(TARGET_TEST_FRAMEWORK "" OFF)
    endif()

    add_subdirectory(JoltPhysics/Build)

    if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
        include(CheckCXXCompilerFlag)
        check_cxx_compiler_flag("-Wno-unqualified-std-cast-call" COMPILER_CHECK_UNQUALIFIED)
        if(COMPILER_CHECK_UNQUALIFIED)
            target_compile_options(Jolt PRIVATE "-Wno-unqualified-std-cast-call")
        endif()
    endif()

    if(ENABLE_PHYSICS_DEBUG_RENDERER)
        set_target_properties(Jolt TestFramework
            PROPERTIES 
            FOLDER ${third_party_folder}/JoltPhysics
            MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
    else()
        set_target_properties(Jolt
            PROPERTIES 
            FOLDER ${third_party_folder}/JoltPhysics
            MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
    endif()

endif()

只需要写成

if(NOT TARGET sol2)
    add_subdirectory(sol2-3.3.0)
endif()


if(NOT TARGET lua)
    include(lua.cmake)
endif()

这些拉取的最新版的 piccolo 都有

然后这个 lua.cmake 也是有的……他这里好像是把 lua 文件夹下的 .c 大部分包括了

然后要在 Piccolo-main\engine\source\runtime 的 CMakeList.txt 中链接

target_link_libraries(${TARGET_NAME} PUBLIC lua_static sol2)

好高级……我都不懂

然后他在 engine\source\runtime\function\framework\component\lua\lua_component.cpp 新建了 lua 的 component

给 component 新建了一个 string,设置了这个 string 字段的反射类型位 Enabled,配合这个类的反射类型为白名单,表示这个字段可以被反射,然后现在我们要配置这个 component 到场景中

默认的场景是 engine\asset\world\hello.world.json

其中设置了关卡 “default_level_url”: “asset/level/1-1.level.json”

关卡的 json 中开头是一些琐碎的设置,例如重力,角色,然后是 objects 列表

看里面的名字为 Player 的物体,定义了一个transform component 和一个 definition

“definition”: “asset/objects/character/player/player.object.json”

definition 也是一个 json

查看这个 player 的 json,可以添加一个 component

component 的 json 定义由两个部分组成,context 和 typeName

typeName 是组建的名字

context 里面是序列化的字段

之前我们定义了 m_lua_script 字段,这里就写 lua_script

直接把 lua_script 赋为 lua 脚本

反射的时候会自动去掉 m_ 前缀

之后在 lua component 的头文件中添加 sol 的头文件,添加一个 sol::state m_lua_state;,通过这个 state 就可以载入函数库,运行 lua 脚本

问题

小引擎使用全局变量 g_runtime_global_context 可以在任何地方方便访问各个系统,这种方式有什么优缺点?有没有其他方式能实现同样目的?

优点是方便在任何地方访问这个全局变量,缺点是使用到这个全局变量的函数的可读性可移植性可能不好,与这个全局变量相关的逻辑可能很复杂,写新逻辑的人需要知道这个全局变量的所有使用情况,才能保证自己写的逻辑不会和别人的冲突

GObject 上不同类型的 Component 调用顺序是不确定的,比如一个 Player 可能先跑脚本再跑动画,另一个可能先动画后脚本,这会给游戏的运行带来什么问题?如何才能让 Component 有确定的顺序去执行?

这样使得游戏的运行结果是不确定的,可能在物理模拟,渲染,网络同步等需要确定性的系统上出现问题

在 level 的 tick 中,对每一个物体的 tick,而物体的 tick 是对这个物体的所有 component 进行 tick

物体的 tick 之间的顺序应该由物体之间的父子关系决定,而每一个物体中的 component 的 tick 之间的顺序可以通过排序来保证,例如在初始化的时候排一次序,在物体的 component 变动的时候排一次序

我感觉是可以人为设定一个字典,设置每一种 component 对应的 key,根据这个 key 进行排序

这次视频我们只实现了 lua 简单脚本的执行。目前 lua 脚本每帧执行一次,而且还不能调用任何引擎的接口,你会怎么设计 lua 脚本的生命周期,以及与引擎各系统的交互方式?

lua 脚本的生命周期我觉得就参考 Unity 脚本的生命周期吧,在 GObject 创建的时候调用 Lua 脚本中的 Awake 函数,在关卡第一次遍历的时候调用 Lua 脚本的 Start 函数,在 LuaComponent tick 的时候调用 Lua 脚本的 Tick 函数,在 Component Disable Enable 的时候……

lua 脚本与引擎各系统的交互方式:可以 C++ 定义一个函数,这个函数通过自定义的反射系统来访问那些允许反射的实例,然后将这个函数绑定到 lua 函数上

最新版的 piccolo 给出了使用反射的示例

在这里插入图片描述

反射系统

反射系统结构

在这里插入图片描述
首先根据 parser 源码构建 piccolo parser

然后收集 runtime 所有头文件的路径,然后生成一个 parser_header.h

cmake 构建 picoolo runtime 工程之前会构建 piccolo precompile 工程

在构建 precompile 工程时,会调用 piccolo parser 去解析 parser_header.h

解析方式是使用 clang 库,得到所有类的抽象语法树,根据在类上打的标签,筛选出我们感兴趣的信息,例如类需要反射的类型和变量名

根据得到的信息生成反射代码,然后将这些代码与 runtime 源码一起构建 piccolo runtime

反射生成的代码

第一部分是 json 相关

第二部分是取基类反射示例

第三部分是对每一个反射的字段生成的代码

取这个字段的类型名,取变量名,get set,判断是否为数组(如果是 std::vector 那么返回 true)

在这里插入图片描述

显然,如果这个类有多个需要反射的字段,那么在第三部分,就会对每一个字段都生成相应的函数

例如现在有一个方法,两个属性需要反射,那么在第三部分生成的代码就会是:

在这里插入图片描述

接下来是一个 Register 函数,以反射的类的类名为后缀,这里是 TypeWrapperRegister_MotorComponent

对于每一个要反射的字段、方法、基类,把生成的函数合成一个 tuple,放进 map 中

在这里插入图片描述
属性放属性的 map,方法放方法的 map,数组,基类也是

在这里插入图片描述

这些宏定义只是单纯的在 map 中增删

在这里插入图片描述

从整体来看比较简单的反射代码:

在这里插入图片描述

可以看更复杂的反射代码的样子:

在这里插入图片描述

这里看上去不知道为什么会多了这些东西,于是去看这份反射代码对应的源码头文件

在这里插入图片描述

可见,这是因为一份头文件里面塞下了多个要反射的类

那么按照常理来说,之前那个复杂的反射生成代码的结构应该只是简单反射生成代码的重复

实际上确实是这样,只是默认的折叠遮盖了结构而已

在这里插入图片描述

最终在注册的时候包含了三个要注册的函数

再看 reflection 的目录,除了 engine\source_generated\reflection\all_reflection.h 其他反射生成的代码都是以 reflection.gen.h 结尾,而且里面的结构都是一样的

all_reflection.h 里面把所有反射生成的代码的头文件 include 进来,然后创建了一个 metaRegister 函数,装了所有反射生成的那个要把 tuple 注册到 map 的函数

在这里插入图片描述

这个 metaRegister 就是在 startEngine 里面调用的

Lua 组件通过反射访问组件

根据字符串可以找到某一个组件的某一个成员

一开始这里是找到某个组件

    bool find_component_field(std::weak_ptr<GObject>     game_object,
                              const char*                field_name,
                              Reflection::FieldAccessor& field_accessor,
                              void*&                     target_instance)
    {
        auto components = game_object.lock()->getComponents();

        std::istringstream iss(field_name);
        std::string        current_name;
        std::getline(iss, current_name, '.');
        auto component_iter = std::find_if(
            components.begin(), components.end(), [current_name](auto c) { return c.getTypeName() == current_name; });

这个 current_name 其实是根据 '.'field_name 导出的 string 流的第一个 string

看官方的用例

set_float(GameObject, \"MotorComponent.m_motor_res.m_jump_height\", 10)

可见,第一个得到的是 MotorComponent

这就很神奇,因为其实我们一直都是想着对一个类得到这个类的变量,但是他这里开头是一个类型名

实际上是因为一开始从一个 Component 出发,我们只知道这个 Component 所在的 GObject

之前在 void LuaComponent::postLoadResource(std::weak_ptr<GObject> parent_object) 给 state 的 “GameObject” 键设为 m_parent_object 的值

m_lua_state["GameObject"] = m_parent_object;

就是在 Lua 脚本中设置了一些内置变量,很方便

        if (component_iter != components.end())
        {
            auto  meta           = Reflection::TypeMeta::newMetaFromName(current_name);
            void* field_instance = component_iter->getPtr();

            // find target field
            while (std::getline(iss, current_name, '.'))
            {
                Reflection::FieldAccessor* fields;
                int                        fields_count = meta.getFieldsList(fields);
                auto                       field_iter   = std::find_if(
                    fields, fields + fields_count, [current_name](auto f) { return f.getFieldName() == current_name; });
                if (field_iter == fields + fields_count) // not found
                {
                    delete[] fields;
                    return false;
                }

                field_accessor = *field_iter;
                delete[] fields;

                target_instance = field_instance;

                // for next iteration
                field_instance = field_accessor.get(target_instance);
                field_accessor.getTypeMeta(meta);
            }
            return true;
        }
        return false;
    }

他这里每一次循环都是用一个 TypeMeta 类的实例,得到这个实例的 Reflection::FieldAccessor 数组,在这个数组中寻找字段名与待查找的字段名相同的 Reflection::FieldAccessor 成员

找到之后,用这个 Reflection::FieldAccessor 成员从实例中获得目标字段的实例

如此循环,直到字符串读取完成,如果仍然能够读到字段实例,说明这个 GObject 中有这个字段

例如 "MotorComponent.m_motor_res.m_jump_height"

一开始是从 GObject 获得 Component 的指针,然后获得这个 Component 的 void* 类型的指针,获得这个 Component 的 name,加入循环:根据 name 创建 TypeMeta 类的一个变量 meta,meta 可以根据 name 获得相应的字段访问器,例如我输入 "MotorComponent" 创建的 TypeMeta 类的变量可以获得 MotorComponent 类里面可反射字段的字段访问器,然后在这些字段访问器中查找,如果找到名称为 "m_motor_res" 的,就可以使用根据 void* 类型的指向实例的指针,配合这个字段访问器,得到字段的实例,类型为 void* 如此循环

这里 void* 是方便泛型编程

void* 的指针使用的时候必须强转为显式类型的指针,也就是说这个反射要用到 field_instance 的时候……?

然后 lua 组件的自定义 get set 就会使用这个 find_component_field 来找到字段

之后有价值的是,得到一个 void* 的字段实例和一个字段访问器之后,就用这个字段访问器去访问字段实例,得到实际的值

    template<typename T>
    void LuaComponent::set(std::weak_ptr<GObject> game_object, const char* name, T value)
    {
        LOG_INFO(name);
        Reflection::FieldAccessor field_accessor;
        void*                     target_instance;
        if (find_component_field(game_object, name, field_accessor, target_instance))
        {
            field_accessor.set(target_instance, &value);
        }
        else
        {
            LOG_ERROR("Can't find target field.");
        }
    }

    template<typename T>
    T LuaComponent::get(std::weak_ptr<GObject> game_object, const char* name)
    {

        LOG_INFO(name);

        Reflection::FieldAccessor field_accessor;
        void*                     target_instance;
        if (find_component_field(game_object, name, field_accessor, target_instance))
        {
            return *(T*)field_accessor.get(target_instance);
        }
        else
        {
            LOG_ERROR("Can't find target field.");
        }
    }

然后 FieldAccessor 的 get set 的实现,具体是怎么实现访问字段实例的?

        void* FieldAccessor::get(void* instance)
        {
            // todo: should check validation
            return static_cast<void*>((std::get<1>(*m_functions))(instance));
        }

        void FieldAccessor::set(void* instance, void* value)
        {
            // todo: should check validation
            (std::get<0>(*m_functions))(instance, value);
        }

他这里是使用了之前在反射生成代码里面捆绑的字段函数 tuple 中的第二个函数实现 get,字段函数 tuple 中的第一个函数实现 get

但是我这是已经看了即便所以才这么想的,具体到这个代码里面,还需要知道 m_functions 这个 tuple 是从哪来的

直接查找引用是只能找到 FieldAccessor 的构造函数中对这个 m_functions 赋值了

那就只能去看这个 FieldAccessor 类型的变量是怎么来的了,还是回到 find_component_field 函数

这里是用 TypeMeta 的 meta.getFieldsList(fields); 才找到的 FieldAccessor 类型的变量

而 TypeMeta 的这个获取字段列表也只是拷贝自己的变量 m_fields 出去

        int TypeMeta::getFieldsList(FieldAccessor*& out_list)
        {
            int count = m_fields.size();
            out_list  = new FieldAccessor[count];
            for (int i = 0; i < count; ++i)
            {
                out_list[i] = m_fields[i];
            }
            return count;
        }

而这个变量在 TypeMeta 的构造函数中被赋值

        TypeMeta::TypeMeta(std::string type_name) : m_type_name(type_name)
        {
            m_is_valid = false;
            m_fields.clear();
            m_methods.clear();

            auto fileds_iter = m_field_map.equal_range(type_name);
            while (fileds_iter.first != fileds_iter.second)
            {
                FieldAccessor f_field(fileds_iter.first->second);
                m_fields.emplace_back(f_field);
                m_is_valid = true;

                ++fileds_iter.first;
            }

            auto methods_iter = m_method_map.equal_range(type_name);
            while (methods_iter.first != methods_iter.second)
            {
                MethodAccessor f_method(methods_iter.first->second);
                m_methods.emplace_back(f_method);
                m_is_valid = true;

                ++methods_iter.first;
            }
        }

这里用的是 FieldAccessor 的构造函数 FieldAccessor(FieldFunctionTuple* functions);

equal_range 返回的是 key 等于给定值的 pair<迭代器表示的下界,迭代器表示的上界>

fileds_iter.first 一开始表示的是下界,它也可以自增,用于迭代,直到等于上界

那么其实这个函数就是从 m_field_map 取出键为字段名的所有字段函数 tuple,构造 FieldAccessor,装入 TypeMeta 实例的 m_fields 成员

方法也是同理

m_field_map 就是反射代码最终将自己的 tuple 放到的容器中

这样,反射代码和 TypeMeta 之间的数据流动就串起来了

这里还有一点是,TypeMeta 自己有一个名字,FieldAccessor 其实可以通过 tuple 中的函数也能得到自己的名字,所以就能够通过字符串去创建 TypeMeta,也可以根据字符串去查找某一个 TypeMeta 类的实例所含有的 FieldAccessor 数组中的某一个元素

反射解析器

首先看解析器的 main 函数

engine\source\meta_parser\parser\main.cpp

        MetaParser::prepare();

        result = parse(argv[1], argv[2], argv[3], argv[4], argv[5], argv[6]);

首先使用了 Parser 的预处理,然后选六个参数传入 parse 函数

parse 函数用这六个参数创建一个 MetaParser 类的实例,调用这个对象的 parse 函数,获得结果,如果结果编号不为 0,也就是如果出错了就返回这个编码,没有出错就生成反射文件

int parse(std::string project_input_file_name,
          std::string source_include_file_name,
          std::string include_path,
          std::string sys_include,
          std::string module_name,
          std::string show_errors)
{
    std::cout << std::endl;
    std::cout << "Parsing meta data for target \"" << module_name << "\"" << std::endl;
    std::fstream input_file;

    bool is_show_errors = "0" != show_errors;

    MetaParser parser(
        project_input_file_name, source_include_file_name, include_path, sys_include, module_name, is_show_errors);

    std::cout << "Parsing in " << include_path << std::endl;
    int result = parser.parse();
    if (0 != result)
    {
        return result;
    }

    parser.generateFiles();

    return 0;
}

先跳过构造函数,看 int MetaParser::parse(void)

其中主要在

    m_translation_unit = clang_createTranslationUnitFromSourceFile(
        m_index, m_source_include_file_name.c_str(), static_cast<int>(arguments.size()), arguments.data(), 0, nullptr);
    auto cursor = clang_getTranslationUnitCursor(m_translation_unit);

    Namespace temp_namespace;

    buildClassAST(cursor, temp_namespace);

调用 clang 然后生成类的抽象语法树

void MetaParser::buildClassAST(const Cursor& cursor, Namespace& current_namespace)
{
    for (auto& child : cursor.getChildren())
    {
        auto kind = child.getKind();

        // actual definition and a class or struct
        if (child.isDefinition() && (kind == CXCursor_ClassDecl || kind == CXCursor_StructDecl))
        {
            auto class_ptr = std::make_shared<Class>(child, current_namespace);

            TRY_ADD_LANGUAGE_TYPE(class_ptr, classes);
        }
        else
        {
            RECURSE_NAMESPACES(kind, child, buildClassAST, current_namespace);
        }
    }
}

它在所有的根节点中找到类声明和结构体的声明,然后构造了 Class 类的实例,传递到了 m_schema_modules

但是那个 classes 变量,好神奇,我一开始去找一个名字叫作 classes 的变量,没找到,给我整晕了

之后才想到这是在宏定义里面,他其实只是宏定义中的一个字符串而已

#define TRY_ADD_LANGUAGE_TYPE(handle, container) \
    { \
        if (handle->shouldCompile()) \
        { \
            auto file = handle->getSourceFile(); \
            m_schema_modules[file].container.emplace_back(handle); \
            m_type_table[handle->m_display_name] = file; \
        } \
    }
std::unordered_map<std::string, SchemaMoudle> m_schema_modules;
struct SchemaMoudle
{
    std::string name;

    std::vector<std::shared_ptr<Class>> classes;
};

然后看 Class 类是怎么样的

Class 类的构造函数里面,对基类、字段、方法做特殊处理

Class::Class(const Cursor& cursor, const Namespace& current_namespace) :
    TypeInfo(cursor, current_namespace), m_name(cursor.getDisplayName()),
    m_qualified_name(Utils::getTypeNameWithoutNamespace(cursor.getType())),
    m_display_name(Utils::getNameWithoutFirstM(m_qualified_name))
{
    Utils::replaceAll(m_name, " ", "");
    Utils::replaceAll(m_name, "Piccolo::", "");

    for (auto& child : cursor.getChildren())
    {
        switch (child.getKind())
        {
            case CXCursor_CXXBaseSpecifier: {
                auto base_class = new BaseClass(child);

                m_base_classes.emplace_back(base_class);
            }
            break;
            // field
            case CXCursor_FieldDecl:
                m_fields.emplace_back(new Field(child, current_namespace, this));
                break;
            // method
            case CXCursor_CXXMethod:
                m_methods.emplace_back(new Method(child, current_namespace, this));
            default:
                break;
        }
    }
}

参考 Field 类可以实现 Method 类,大部分是复制的 Field 类,有点看不懂

然后回到 main,看 parser.generateFiles(); 怎么生成反射代码的

看到

int ReflectionGenerator::generate(std::string path, SchemaMoudle schema)

其中关键代码是 genClassRenderData(class_temp, class_def);

使用 engine\template 中的代码模板生成反射代码

之后他又根据 field 的反射生成逻辑做了 method 的反射生成逻辑

呃……全程没看懂

渲染系统

各个系统在 startEngine 的时候初始化

void RenderSystem::initialize(RenderSystemInitInfo init_info)

渲染系统的初始化,首先设置渲染内容,然后设置相机,场景,渲染管线

将渲染的功能抽象成一个个 pass

在 pipeline 的初始化中会调用各个 pass 的初始化

在渲染系统的 tick 中,首先调用 void RenderSystem::processSwapData()

查看 RenderSwapData 这个结构

    struct RenderSwapData
    {
        std::optional<LevelResourceDesc>       m_level_resource_desc;
        std::optional<GameObjectResourceDesc>  m_game_object_resource_desc;
        std::optional<GameObjectResourceDesc>  m_game_object_to_delete;
        std::optional<CameraSwapData>          m_camera_swap_data;
        std::optional<ParticleSubmitRequest>   m_particle_submit_request;
        std::optional<EmitterTickRequest>      m_emitter_tick_request;
        std::optional<EmitterTransformRequest> m_emitter_transform_request;

        void addDirtyGameObject(GameObjectDesc&& desc);
        void addDeleteGameObject(GameObjectDesc&& desc);

        void addNewParticleEmitter(ParticleEmitterDesc& desc);
        void addTickParticleEmitter(ParticleEmitterID id);
        void updateParticleTransform(ParticleEmitterTransformDesc& desc);
    };

m_level_resource_desc 用于指定 colorgrading 和 天空盒 的贴图

m_game_object_resource_descm_game_object_to_delete 分别指定要添加和删除的 GameObject

m_camera_swap_data 更新相机参数

m_particle_submit_request, m_emitter_tick_request, m_emitter_transform_request 指定要创建的粒子发射器,要 tick 的粒子发射器,更新粒子发射器的位置

void RenderSystem::processSwapData() 这个函数之后的部分就是根据 RenderSwapData 这个结构体中的内容进行处理

这里的 swap 不是特别准确,因为现在只有渲染从逻辑拿数据

void RenderSystem::tick(float delta_time) 中,首先调用 processSwapData 从逻辑拿数据

    void RenderSystem::tick(float delta_time)
    {
        // process swap data between logic and render contexts
        processSwapData();

        // prepare render command context
        m_rhi->prepareContext();

        // update per-frame buffer
        m_render_resource->updatePerFrameBuffer(m_render_scene, m_render_camera);

        // update per-frame visible objects
        m_render_scene->updateVisibleObjects(std::static_pointer_cast<RenderResource>(m_render_resource),
                                             m_render_camera);

        // prepare pipeline's render passes data
        m_render_pipeline->preparePassData(m_render_resource);

        g_runtime_global_context.m_debugdraw_manager->tick(delta_time);

        // render one frame
        if (m_render_pipeline_type == RENDER_PIPELINE_TYPE::FORWARD_PIPELINE)
        {
            m_render_pipeline->forwardRender(m_rhi, m_render_resource);
        }
        else if (m_render_pipeline_type == RENDER_PIPELINE_TYPE::DEFERRED_PIPELINE)
        {
            m_render_pipeline->deferredRender(m_rhi, m_render_resource);
        }
        else
        {
            LOG_ERROR(__FUNCTION__, "unsupported render pipeline type");
        }
    }

RHI(Render Hardware Interface) 图形学 api 的函数抽象(VulkanRHI 与 RHI 的关系就是继承,外界代码调用 RHI 接口,不需要考虑内部实现是 DirectX 或者 Vulkan)

然后调用 RHI 的 prepareContext 函数

这里 RHI 是基类,VulkanRHI 是子类,m_rhi 是基类的指针,这样我们就可以不用考虑内部实现是 Vulkan 还是 DirectX

接下来的 RenderResource::updatePerFrameBuffer 是准备每帧 pass 共用的数据

RenderScene::updateVisibleObjects 是为每个 pass 筛选每个 pass 具体所需的数据

RenderPipelineBase::preparePassData 会调用各个 pass 的 preparePassData 进行数据准备

最后判断是使用前向渲染还是延迟渲染

对于前向渲染:

engine\source\runtime\function\render\render_pipeline.cpp

RenderPipeline::forwardRender 中,先准备渲染对象,然后调用相机 Pass 的 drawForward 函数

在相机 Pass 的 drawForward 函数中,会执行各个 subPass 的 draw 函数

如果是延迟渲染,那么调用 MainCameraPass::draw 函数

这个函数中,先会进行一次 MainCameraPass::drawMeshGbuffer() 的绘制,将物体的几何信息存到 G-Buffer 中,然后利用 G-Buffer 里缓存的光照信息进行光照计算

color grading 的条纹问题

然后它就用 renderdoc 一看就发现了 color grading 的条纹问题是 mipmap 的精度问题

应该只生成一级 mipmap

在生成纹理的地方指定生成一级 mipmap

engine\source\runtime\function\render\render_resource.cpp

        // create color grading texture
        rhi->createGlobalImage(
            m_global_render_resource._color_grading_resource._color_grading_LUT_texture_image,
            m_global_render_resource._color_grading_resource._color_grading_LUT_texture_image_view,
            m_global_render_resource._color_grading_resource._color_grading_LUT_texture_image_allocation,
            color_grading_map->m_width,
            color_grading_map->m_height,
            color_grading_map->m_pixels,
            color_grading_map->m_format, 1);
shader 应用流程

ColorGradingPassRenderPass 的一个子类

RenderPass 类,其中的

GlobalRenderResource* m_global_render_resource {nullptr}; 定义了可能用到的子类

std::vector<Descriptor> m_descriptor_infos; 定义了输入和输出的资源在 shader 中的布局信息

std::vector<RenderPipelineBase> m_render_pipelines; 定义了渲染管线的信息

static VisiableNodes m_visiable_nodes; 记录了这个 pass 可见的物体,例如场景中可能有多个物体,该 pass 只会用到其中一部分

这里 color grading pass 被设计为 main camera pass 的一个 sub pass

    enum
    {
        _main_camera_subpass_basepass = 0,
        _main_camera_subpass_deferred_lighting,
        _main_camera_subpass_forward_lighting,
        _main_camera_subpass_tone_mapping,
        _main_camera_subpass_color_grading,
        _main_camera_subpass_fxaa,
        _main_camera_subpass_ui,
        _main_camera_subpass_combine_ui,
        _main_camera_subpass_count
    };

sub pass 是 vulkan api 的一个设计,作为 vulkan render pass 对象的一个部分,一个 vulkan render pass 可以包含多个串行的 subpass,前一个 subpass 的输出作为后一个 subpass 的输入

在这里添加 color_grading pass

engine\source\runtime\function\render\passes\main_camera_pass.cpp

RHISubpassDescription& color_grading_pass   = subpasses[_main_camera_subpass_color_grading];

对于 color grading pass 的初始化函数

一开始拿到了一个 InitInfo 也就是拿到了 lookuptable 这张贴图

    void ColorGradingPass::initialize(const RenderPassInitInfo* init_info)
    {
        RenderPass::initialize(nullptr);

        const ColorGradingPassInitInfo* _init_info = static_cast<const ColorGradingPassInitInfo*>(init_info);
        m_framebuffer.render_pass                  = _init_info->render_pass;

        setupDescriptorSetLayout();
        setupPipelines();
        setupDescriptorSet();
        updateAfterFramebufferRecreate(_init_info->input_attachment);
    }

setupDescriptorSetLayout(); 描述了输入输出的资源排布

这里用到两个资源,一个是 tone mapping pass 的输出,一个是调色纹理

setupPipelines(); 描述管线,color grading pass 使用的是 graphics 管线,主体部分是顶点着色器和片元着色器

        RHIShader* vert_shader_module = m_rhi->createShaderModule(POST_PROCESS_VERT);
        RHIShader* frag_shader_module = m_rhi->createShaderModule(COLOR_GRADING_FRAG);

视口裁剪,朝向剔除,混合模式,深度模板

setupDescriptorSet(); 描述实际使用到的资源

DescriptorSet 可以理解为资源 handle 的集合

updateAfterFramebufferRecreate 视口变化时进行一些更新操作

color grading pass 的 draw() 会在 main camera pass 的 draw() 中被调用

draw 函数就是提交了一些 vulkan 的命令

仿照 color grading 添加 vignette pass

复制粘贴 color grading 的源文件和头文件,重命名

在文件夹中搜索 color_grading,根据搜索结果,复制粘贴 color_grading 相关的代码并修改:

在 engine\source\runtime\function\render\render_pass.h 中添加 _main_camera_subpass_vignette 的 enum

在 engine\source\runtime\function\render\render_pipeline_base.h 中添加

std::shared_ptr<RenderPassBase> m_vignette_pass;

在 engine\source\runtime\function\render\render_pipeline.cpp 中添加

#include "runtime/function/render/passes/vignette_pass.h"

void RenderPipeline::initialize(RenderPipelineInitInfo init_info) 中添加

m_vignette_pass      = std::make_shared<VignettePass>();
m_vignette_pass->setCommonInfo(pass_common_info);
        VignettePassInitInfo vignette_init_info;
        vignette_init_info.render_pass = _main_camera_pass->getRenderPass();
        vignette_init_info.input_attachment =
            _main_camera_pass->getFramebufferImageViews()[_main_camera_pass_backup_buffer_even];
        m_vignette_pass->initialize(&vignette_init_info);

void RenderPipeline::forwardRender(std::shared_ptr<RHI> rhi, std::shared_ptr<RenderResourceBase> render_resource) 中添加:

VignettePass&     vignette_pass      = *(static_cast<VignettePass*>(m_vignette_pass.get()));

修改:

        static_cast<MainCameraPass*>(m_main_camera_pass.get())
            ->drawForward(color_grading_pass,
                          vignette_pass,
                          fxaa_pass,
                          tone_mapping_pass,
                          ui_pass,
                          combine_ui_pass,
                          particle_pass,
                          vulkan_rhi->m_current_swapchain_image_index);

void RenderPipeline::deferredRender(std::shared_ptr<RHI> rhi, std::shared_ptr<RenderResourceBase> render_resource)

添加

VignettePass&     vignette_pass      = *(static_cast<VignettePass*>(m_vignette_pass.get()));

修改

        static_cast<MainCameraPass*>(m_main_camera_pass.get())
            ->draw(color_grading_pass,
                   vigntee_pass,
                   fxaa_pass,
                   tone_mapping_pass,
                   ui_pass,
                   combine_ui_pass,
                   particle_pass,
                   vulkan_rhi->m_current_swapchain_image_index);

void RenderPipeline::passUpdateAfterRecreateSwapchain()

添加

VignettePass&     vignette_pass      = *(static_cast<VignettePass*>(m_vignette_pass.get()));

修改

        tone_mapping_pass.updateAfterFramebufferRecreate(
            main_camera_pass.getFramebufferImageViews()[_main_camera_pass_backup_buffer_odd]);
        color_grading_pass.updateAfterFramebufferRecreate(
            main_camera_pass.getFramebufferImageViews()[_main_camera_pass_backup_buffer_even]);
        vignette_pass.updateAfterFramebufferRecreate(
            main_camera_pass.getFramebufferImageViews()[_main_camera_pass_backup_buffer_odd]);
        fxaa_pass.updateAfterFramebufferRecreate(
            main_camera_pass.getFramebufferImageViews()[_main_camera_pass_post_process_buffer_even]);
        combine_ui_pass.updateAfterFramebufferRecreate(
            main_camera_pass.getFramebufferImageViews()[_main_camera_pass_backup_buffer_even],
            main_camera_pass.getFramebufferImageViews()[_main_camera_pass_backup_buffer_odd]);

这里的 buffer_odd buffer_even 是 subpass 之间的缓冲区,所以新加了一个 vignette_pass 之后对后面的 sub pass 的奇偶做一下替换

在 engine\source\runtime\function\render\passes\main_camera_pass.cpp

void MainCameraPass::setupRenderPass()

添加

        RHIAttachmentReference vignette_pass_input_attachment_reference {};
        vignette_pass_input_attachment_reference.attachment =
            &backup_even_color_attachment_description - attachments;
        vignette_pass_input_attachment_reference.layout = RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

        RHIAttachmentReference vignette_pass_color_attachment_reference {};
        if (m_enable_fxaa)
        {
            vignette_pass_color_attachment_reference.attachment =
                &post_process_odd_color_attachment_description - attachments;
        }
        else
        {
            vignette_pass_color_attachment_reference.attachment =
                &backup_odd_color_attachment_description - attachments;
        }
        vignette_pass_color_attachment_reference.layout = RHI_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

        RHISubpassDescription& vignette_pass   = subpasses[_main_camera_subpass_vignette];
        vignette_pass.pipelineBindPoint       = RHI_PIPELINE_BIND_POINT_GRAPHICS;
        vignette_pass.inputAttachmentCount    = 1;
        vignette_pass.pInputAttachments       = &vignette_pass_input_attachment_reference;
        vignette_pass.colorAttachmentCount    = 1;
        vignette_pass.pColorAttachments       = &vignette_pass_color_attachment_reference;
        vignette_pass.pDepthStencilAttachment = NULL;
        vignette_pass.preserveAttachmentCount = 0;
        vignette_pass.pPreserveAttachments    = NULL;

还要管理依赖关系

添加一个 dependencies,8 改为 9

RHISubpassDependency dependencies[9] = {};

添加并修改

        RHISubpassDependency& vignette_pass_depend_on_color_grading_pass = dependencies[5];
        vignette_pass_depend_on_color_grading_pass.srcSubpass           = _main_camera_subpass_color_grading;
        vignette_pass_depend_on_color_grading_pass.dstSubpass           = _main_camera_subpass_vignette;
        vignette_pass_depend_on_color_grading_pass.srcStageMask =
            RHI_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        vignette_pass_depend_on_color_grading_pass.dstStageMask =
            RHI_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        vignette_pass_depend_on_color_grading_pass.srcAccessMask =
            RHI_ACCESS_SHADER_WRITE_BIT | RHI_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
        vignette_pass_depend_on_color_grading_pass.dstAccessMask =
            RHI_ACCESS_SHADER_READ_BIT | RHI_ACCESS_COLOR_ATTACHMENT_READ_BIT;
        vignette_pass_depend_on_color_grading_pass.dependencyFlags = RHI_DEPENDENCY_BY_REGION_BIT;

        RHISubpassDependency& fxaa_pass_depend_on_vignette_pass = dependencies[6];
        fxaa_pass_depend_on_vignette_pass.srcSubpass           = _main_camera_subpass_vignette;
        fxaa_pass_depend_on_vignette_pass.dstSubpass           = _main_camera_subpass_fxaa;
        fxaa_pass_depend_on_vignette_pass.srcStageMask =
            RHI_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        fxaa_pass_depend_on_vignette_pass.dstStageMask =
            RHI_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        fxaa_pass_depend_on_vignette_pass.srcAccessMask =
            RHI_ACCESS_SHADER_WRITE_BIT | RHI_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
        fxaa_pass_depend_on_vignette_pass.dstAccessMask =
            RHI_ACCESS_SHADER_READ_BIT | RHI_ACCESS_COLOR_ATTACHMENT_READ_BIT;

之后的序号也要变

修改函数定义:

    void MainCameraPass::draw(ColorGradingPass& color_grading_pass,
                              VignettePass&     vignette_pass,
                              FXAAPass&         fxaa_pass,
                              ToneMappingPass&  tone_mapping_pass,
                              UIPass&           ui_pass,
                              CombineUIPass&    combine_ui_pass,
                              ParticlePass&     particle_pass,
                              uint32_t          current_swapchain_image_index)

添加:

        m_rhi->cmdNextSubpassPFN(m_rhi->getCurrentCommandBuffer(), RHI_SUBPASS_CONTENTS_INLINE);

        vigneete_pass.draw();

修改函数定义:

    void MainCameraPass::drawForward(ColorGradingPass& color_grading_pass,
                                     VignettePass&     vignette_pass,
                                     FXAAPass&         fxaa_pass,
                                     ToneMappingPass&  tone_mapping_pass,
                                     UIPass&           ui_pass,
                                     CombineUIPass&    combine_ui_pass,
                                     ParticlePass&     particle_pass,
                                     uint32_t          current_swapchain_image_index)

添加:

        m_rhi->cmdNextSubpassPFN(m_rhi->getCurrentCommandBuffer(), RHI_SUBPASS_CONTENTS_INLINE);

        vignette_pass.draw();

在 engine\source\runtime\function\render\passes\main_camera_pass.h

添加

#include "runtime/function/render/passes/vignette_pass.h"

修改函数声明

        void draw(ColorGradingPass& color_grading_pass,
            VignettePass& vignette_pass,
            FXAAPass& fxaa_pass,
            ToneMappingPass& tone_mapping_pass,
            UIPass& ui_pass,
            CombineUIPass& combine_ui_pass,
            ParticlePass& particle_pass,
            uint32_t          current_swapchain_image_index);

        void drawForward(ColorGradingPass& color_grading_pass,
            VignettePass& vignette_pass,
            FXAAPass& fxaa_pass,
            ToneMappingPass& tone_mapping_pass,
            UIPass& ui_pass,
            CombineUIPass& combine_ui_pass,
            ParticlePass& particle_pass,
            uint32_t          current_swapchain_image_index);

全都是照着视频做的,但是我在编译的时候出了问题

[  1%] Building CXX object engine/source/meta_parser/CMakeFiles/PiccoloParser.dir/parser/cursor/cursor.cpp.obj
g++.exe: error: /O2: No such file or directory
g++.exe: error: /Ob2: No such file or directory
mingw32-make.exe[2]: *** [engine\source\meta_parser\CMakeFiles\PiccoloParser.dir\build.make:76: engine/source/meta_parser/CMakeFiles/PiccoloParser.dir/parser/cursor/cursor.cpp.obj] Error 1
mingw32-make.exe[1]: *** [CMakeFiles\Makefile2:1434: engine/source/meta_parser/CMakeFiles/PiccoloParser.dir/all] Error 2
mingw32-make.exe: *** [Makefile:135: all] Error 2

我一看,那个源文件确实在那里,也没报错,不知道为什么没找到 obj

之后删除了 build 文件夹,然后重新编译,就出现了 vignette pass 相关的报错

之后查询了一下 cmake 指令

cmake -S . -B build

在 help 中的信息:

  -S <path-to-source>          = Explicitly specify a source directory.
  -B <path-to-build>           = Explicitly specify a build directory.

那么其实这个命令就是以当前文件夹(.)为源码根目录,以 build 文件夹为构建根目录

之后就是缺少 vignette_frag.h

这个是在构建过程中由 vignette.frag 转化成的

在 engine\source\runtime\function\render\passes\vignette_pass.cpp

将小写改成大写

RHIShader* frag_shader_module = m_rhi->createShaderModule(VIGNETTE_FRAG);

将 vignette 的贴图资源改回 color_grading 的贴图

因为暂时没有为 vignette 提供贴图

        vignette_LUT_image_info.imageView =
            m_global_render_resource->_color_grading_resource._vignette_LUT_texture_image_view;

其实 vignette 暗角效果是不需要贴图的

然后我不知道为什么教学视频里面 render doc 能够显示 render pass 的名字,而我自己捕捉的没有

实际测试小引擎,加了一个 vignette pass,shader 是复制 color_grading 的,所以理论上我们走了两个 color grading,得到的颜色应该比一次 color grading 更明显,但是实际运行出来,两次跟一次的效果是一样的

所以我们打开 render doc 来查找原因,可以看到,vkCmdNextSubpass() => 4 和 vkCmdNextSubpass() => 5 之间的 vkCmdDraw(3, 1) 就是 color_grading shader,而 vkCmdNextSubpass() => 5 和 vkCmdNextSubpass() => 6 之间的 vkCmdDraw(3, 1) 就是 vignette shader

然后可以发现这两次 draw 的材质输入和输出都是一样的,这就导致运行的结果变得一样

这应该是 subpass 传递贴图的 buffer 之间没有设置对

之前在 void RenderPipeline::passUpdateAfterRecreateSwapchain() 中,我们修改了 sub pass 的 buffer

然后用到这个 buffer 的不止一处,还有在别的地方有用

之后他这里在源码控制这里创建了一个新的分支,这样就可以方便比较某一个分支和当前修改的区别

这就很方便,尤其是在有一些琐碎的修改的时候,比如像现在这种修改 even 和 odd 的,很容易看少

像这样,一开始少了一些

在这里插入图片描述

根据对比,就知道哪些代码自己改过,哪些没改过

在这里插入图片描述

在 engine\source\runtime\function\render\passes\main_camera_pass.cpp

vignette_pass 之后那一大片都要改

	RHIAttachmentReference vignette_pass_input_attachment_reference {};
        vignette_pass_input_attachment_reference.attachment =
            &backup_odd_color_attachment_description - attachments;
        vignette_pass_input_attachment_reference.layout = RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

        RHIAttachmentReference vignette_pass_color_attachment_reference {};
        if (m_enable_fxaa)
        {
            vignette_pass_color_attachment_reference.attachment =
                &post_process_even_color_attachment_description - attachments;
        }
        else
        {
            vignette_pass_color_attachment_reference.attachment =
                &backup_even_color_attachment_description - attachments;
        }
        vignette_pass_color_attachment_reference.layout = RHI_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

        RHISubpassDescription& vignette_pass   = subpasses[_main_camera_subpass_vignette];
        vignette_pass.pipelineBindPoint       = RHI_PIPELINE_BIND_POINT_GRAPHICS;
        vignette_pass.inputAttachmentCount    = 1;
        vignette_pass.pInputAttachments       = &vignette_pass_input_attachment_reference;
        vignette_pass.colorAttachmentCount    = 1;
        vignette_pass.pColorAttachments       = &vignette_pass_color_attachment_reference;
        vignette_pass.pDepthStencilAttachment = NULL;
        vignette_pass.preserveAttachmentCount = 0;
        vignette_pass.pPreserveAttachments    = NULL;

        RHIAttachmentReference fxaa_pass_input_attachment_reference {};
        if (m_enable_fxaa)
        {
            fxaa_pass_input_attachment_reference.attachment =
                &post_process_even_color_attachment_description - attachments;
        }
        else
        {
            fxaa_pass_input_attachment_reference.attachment = &backup_odd_color_attachment_description - attachments;
        }
        fxaa_pass_input_attachment_reference.layout = RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

        RHIAttachmentReference fxaa_pass_color_attachment_reference {};
        fxaa_pass_color_attachment_reference.attachment = &backup_even_color_attachment_description - attachments;
        fxaa_pass_color_attachment_reference.layout     = RHI_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

        RHISubpassDescription& fxaa_pass   = subpasses[_main_camera_subpass_fxaa];
        fxaa_pass.pipelineBindPoint       = RHI_PIPELINE_BIND_POINT_GRAPHICS;
        fxaa_pass.inputAttachmentCount    = 1;
        fxaa_pass.pInputAttachments       = &fxaa_pass_input_attachment_reference;
        fxaa_pass.colorAttachmentCount    = 1;
        fxaa_pass.pColorAttachments       = &fxaa_pass_color_attachment_reference;
        fxaa_pass.pDepthStencilAttachment = NULL;
        fxaa_pass.preserveAttachmentCount = 0;
        fxaa_pass.pPreserveAttachments    = NULL;

        RHIAttachmentReference ui_pass_color_attachment_reference {};
        ui_pass_color_attachment_reference.attachment = &backup_odd_color_attachment_description - attachments;
        ui_pass_color_attachment_reference.layout     = RHI_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

        uint32_t ui_pass_preserve_attachment = &backup_even_color_attachment_description - attachments;

        RHISubpassDescription& ui_pass  = subpasses[_main_camera_subpass_ui];
        ui_pass.pipelineBindPoint       = RHI_PIPELINE_BIND_POINT_GRAPHICS;
        ui_pass.inputAttachmentCount    = 0;
        ui_pass.pInputAttachments       = NULL;
        ui_pass.colorAttachmentCount    = 1;
        ui_pass.pColorAttachments       = &ui_pass_color_attachment_reference;
        ui_pass.pDepthStencilAttachment = NULL;
        ui_pass.preserveAttachmentCount = 1;
        ui_pass.pPreserveAttachments    = &ui_pass_preserve_attachment;

        RHIAttachmentReference combine_ui_pass_input_attachments_reference[2] = {};
        combine_ui_pass_input_attachments_reference[0].attachment =
            &backup_even_color_attachment_description - attachments;
        combine_ui_pass_input_attachments_reference[0].layout = RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
        combine_ui_pass_input_attachments_reference[1].attachment =
            &backup_odd_color_attachment_description - attachments;
        combine_ui_pass_input_attachments_reference[1].layout = RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

engine\source\runtime\function\render\render_pipeline.cpp

中也是一大片要改

        VignettePassInitInfo vignette_init_info;
        vignette_init_info.render_pass = _main_camera_pass->getRenderPass();
        vignette_init_info.input_attachment =
            _main_camera_pass->getFramebufferImageViews()[_main_camera_pass_backup_buffer_odd];
        m_vignette_pass->initialize(&vignette_init_info);

        UIPassInitInfo ui_init_info;
        ui_init_info.render_pass = _main_camera_pass->getRenderPass();
        m_ui_pass->initialize(&ui_init_info);

        CombineUIPassInitInfo combine_ui_init_info;
        combine_ui_init_info.render_pass = _main_camera_pass->getRenderPass();
        combine_ui_init_info.scene_input_attachment =
            _main_camera_pass->getFramebufferImageViews()[_main_camera_pass_backup_buffer_even];
        combine_ui_init_info.ui_input_attachment =
            _main_camera_pass->getFramebufferImageViews()[_main_camera_pass_backup_buffer_odd];
        m_combine_ui_pass->initialize(&combine_ui_init_info);

        PickPassInitInfo pick_init_info;
        pick_init_info.per_mesh_layout = descriptor_layouts[MainCameraPass::LayoutType::_per_mesh];
        m_pick_pass->initialize(&pick_init_info);

        FXAAPassInitInfo fxaa_init_info;
        fxaa_init_info.render_pass = _main_camera_pass->getRenderPass();
        fxaa_init_info.input_attachment =
            _main_camera_pass->getFramebufferImageViews()[_main_camera_pass_post_process_buffer_even];
        m_fxaa_pass->initialize(&fxaa_init_info);

vignette.frag 写为

这里新传入了一个 uv 参数,然后使用了一个距离场

#version 310 es

#extension GL_GOOGLE_include_directive : enable

#include "constants.h"

layout(input_attachment_index = 0, set = 0, binding = 0) uniform highp subpassInput in_color;

layout(location = 0) in highp vec2 in_uv;

layout(location = 0) out highp vec4 out_color;

void main()
{
    highp vec4 color = subpassLoad(in_color).rgba;

    highp float cutoff = 0.4f;
    highp float exponent = 1.5f;
    highp float len = length(in_uv - 0.5f);
    highp float ratio = pow(cutoff/len, exponent);

    out_color = color * min(ratio, 1.0f);
}

复制 post_process.vert 改为

engine\shader\glsl\vignette.vert

传出 uv 参数

#version 310 es

#extension GL_GOOGLE_include_directive : enable

#include "constants.h"

layout(location = 0) out vec2 out_uv;

void main()
{
    const vec3 fullscreen_triangle_positions[3] =
        vec3[3](vec3(3.0, 1.0, 0.5), vec3(-1.0, 1.0, 0.5), vec3(-1.0, -3.0, 0.5));
    gl_Position = vec4(fullscreen_triangle_positions[gl_VertexIndex], 1.0);
    out_uv = (fullscreen_triangle_positions[gl_VertexIndex].xy + 1.0f)/2.0f;
}

在 engine\source\runtime\function\render\passes\vignette_pass.cpp 中使用 vignette 顶点着色器

RHIShader* vert_shader_module = m_rhi->createShaderModule(VIGNETTE_VERT);

void VignettePass::setupDescriptorSetLayout()

        RHIDescriptorSetLayoutBinding post_process_global_layout_bindings[2] = {};

        RHIDescriptorSetLayoutBinding& post_process_global_layout_input_attachment_binding =
            post_process_global_layout_bindings[0];
        post_process_global_layout_input_attachment_binding.binding         = 0;
        post_process_global_layout_input_attachment_binding.descriptorType  = RHI_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
        post_process_global_layout_input_attachment_binding.descriptorCount = 1;
        post_process_global_layout_input_attachment_binding.stageFlags      = RHI_SHADER_STAGE_FRAGMENT_BIT;

        RHIDescriptorSetLayoutBinding& post_process_global_layout_LUT_binding = post_process_global_layout_bindings[1];
        post_process_global_layout_LUT_binding.binding                       = 1;
        post_process_global_layout_LUT_binding.descriptorType  = RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
        post_process_global_layout_LUT_binding.descriptorCount = 1;
        post_process_global_layout_LUT_binding.stageFlags      = RHI_SHADER_STAGE_FRAGMENT_BIT;

改为

        RHIDescriptorSetLayoutBinding post_process_global_layout_bindings[1] = {};

        RHIDescriptorSetLayoutBinding& post_process_global_layout_input_attachment_binding =
            post_process_global_layout_bindings[0];
        post_process_global_layout_input_attachment_binding.binding         = 0;
        post_process_global_layout_input_attachment_binding.descriptorType  = RHI_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
        post_process_global_layout_input_attachment_binding.descriptorCount = 1;
        post_process_global_layout_input_attachment_binding.stageFlags      = RHI_SHADER_STAGE_FRAGMENT_BIT;

删去贴图的输入

void VignettePass::updateAfterFramebufferRecreate(RHIImageView* input_attachment)

        RHIDescriptorImageInfo post_process_per_frame_input_attachment_info = {};
        post_process_per_frame_input_attachment_info.sampler =
            m_rhi->getOrCreateDefaultSampler(Default_Sampler_Nearest);
        post_process_per_frame_input_attachment_info.imageView   = input_attachment;
        post_process_per_frame_input_attachment_info.imageLayout = RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

        RHIDescriptorImageInfo vignette_LUT_image_info = {};
        vignette_LUT_image_info.sampler = m_rhi->getOrCreateDefaultSampler(Default_Sampler_Linear);
        vignette_LUT_image_info.imageView =
            m_global_render_resource->_color_grading_resource._color_grading_LUT_texture_image_view;
        vignette_LUT_image_info.imageLayout = RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

        RHIWriteDescriptorSet post_process_descriptor_writes_info[2];

        RHIWriteDescriptorSet& post_process_descriptor_input_attachment_write_info =
            post_process_descriptor_writes_info[0];
        post_process_descriptor_input_attachment_write_info.sType           = RHI_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
        post_process_descriptor_input_attachment_write_info.pNext           = NULL;
        post_process_descriptor_input_attachment_write_info.dstSet          = m_descriptor_infos[0].descriptor_set;
        post_process_descriptor_input_attachment_write_info.dstBinding      = 0;
        post_process_descriptor_input_attachment_write_info.dstArrayElement = 0;
        post_process_descriptor_input_attachment_write_info.descriptorType  = RHI_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
        post_process_descriptor_input_attachment_write_info.descriptorCount = 1;
        post_process_descriptor_input_attachment_write_info.pImageInfo = &post_process_per_frame_input_attachment_info;

        RHIWriteDescriptorSet& post_process_descriptor_LUT_write_info = post_process_descriptor_writes_info[1];
        post_process_descriptor_LUT_write_info.sType                 = RHI_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
        post_process_descriptor_LUT_write_info.pNext                 = NULL;
        post_process_descriptor_LUT_write_info.dstSet                = m_descriptor_infos[0].descriptor_set;
        post_process_descriptor_LUT_write_info.dstBinding            = 1;
        post_process_descriptor_LUT_write_info.dstArrayElement       = 0;
        post_process_descriptor_LUT_write_info.descriptorType        = RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
        post_process_descriptor_LUT_write_info.descriptorCount       = 1;
        post_process_descriptor_LUT_write_info.pImageInfo            = &vignette_LUT_image_info;

改为

        RHIDescriptorImageInfo post_process_per_frame_input_attachment_info = {};
        post_process_per_frame_input_attachment_info.sampler =
            m_rhi->getOrCreateDefaultSampler(Default_Sampler_Nearest);
        post_process_per_frame_input_attachment_info.imageView   = input_attachment;
        post_process_per_frame_input_attachment_info.imageLayout = RHI_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

        RHIWriteDescriptorSet post_process_descriptor_writes_info[1];

        RHIWriteDescriptorSet& post_process_descriptor_input_attachment_write_info =
            post_process_descriptor_writes_info[0];
        post_process_descriptor_input_attachment_write_info.sType           = RHI_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
        post_process_descriptor_input_attachment_write_info.pNext           = NULL;
        post_process_descriptor_input_attachment_write_info.dstSet          = m_descriptor_infos[0].descriptor_set;
        post_process_descriptor_input_attachment_write_info.dstBinding      = 0;
        post_process_descriptor_input_attachment_write_info.dstArrayElement = 0;
        post_process_descriptor_input_attachment_write_info.descriptorType  = RHI_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
        post_process_descriptor_input_attachment_write_info.descriptorCount = 1;
        post_process_descriptor_input_attachment_write_info.pImageInfo = &post_process_per_frame_input_attachment_info;

删去贴图的输入

顶部的头文件改成

#include <vignette_vert.h>

然后就做完了

总之还是有很多不懂……

可见,

QA

各个 sub pass 的设置是硬编码的,比较繁琐,怎么设计 UI
shader 是硬编码解析成头文件的,该怎么运行时加载?

别人的答案:

哈西法
感觉是不是可以考虑render graph的方式,每个render pass 定义shader、输入、输出,根据这些信息可以排序render pass,然后每个输入输出都定义一个资源,使用一个resource pool进行这些资源的管理。为了简化代码,需要保证每个renderpass的输入输出名字不同,虽然会增加内存占用,但是可以减少很多问题

哎哟小Z
第一个问题参考UE的RDG,将Pass进行封装处理
第二个问题参考UE的材质,提供几种shader框架,然后在材质中记录好的蓝图图表编译为shader,插入到固定的几个计算位置编译成一个完整的shader(即着色器编译)

完全不懂……

真的要学 opengl 和 vulkan 了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值