上一节中我们了解了引擎的整体运行流程以及反射机制的实现,这一节开始探究 Piccolo 的渲染系统是如何实现的,首先来了解渲染系统的整个流程。
回顾上一节中,引擎运行时最核心的函数 tickOneFrame()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bool PilotEngine::tickOneFrame (float delta_time) { logicalTick (delta_time); calculateFPS (delta_time); g_runtime_global_context.m_render_system->swapLogicRenderData (); rendererTick (); g_runtime_global_context.m_window_system->pollEvents (); g_runtime_global_context.m_window_system->setTile ( std::string ("Pilot - " + std::to_string (getFPS ()) + " FPS" ).c_str ()); const bool should_window_close = g_runtime_global_context.m_window_system->shouldClose (); return !should_window_close; }
可以看到引擎每一帧的流程就是先进行 logicalTick
,然后通过 swapLogicRenderData
进行渲染系统的数据更新,最后进行渲染 rendererTick
,之后还有一些窗口的事件响应。接下来我们从 logicalTick
开始看看整个系统是如何运行的。
1 logicalTick logicalTick
的定义如下:
1 2 3 4 5 void PilotEngine::logicalTick (float delta_time) { g_runtime_global_context.m_world_manager->tick (delta_time); g_runtime_global_context.m_input_system->tick (); }
logicalTick
内部调用了世界管理系统的 tick 和输入管理系统的 tick。
1.1 世界管理系统 世界管理系统 m_world_manager
属于 WorldManager
类,该类用于管理整个游戏世界,包括编辑世界和运行世界,世界中包含各种关卡(Level),于是 WorldManager
的 tick 实际上就是调用关卡的 tick:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void WorldManager::tick (float delta_time) { if (!m_is_world_loaded) { loadWorld (m_current_world_url); } std::shared_ptr<Level> active_level = m_current_active_level.lock (); if (active_level) { active_level->tick (delta_time); } }
而关卡类 Level 就是用于管理游戏对象(GO)的类,包含多个 Game Objects,于是 Level 的 tick 函数自然就是调用每个 GO 的 tick:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void Level::tick (float delta_time) { if (!m_is_loaded) { return ; } for (const auto & id_object_pair : m_gobjects) { assert (id_object_pair.second); if (id_object_pair.second) { id_object_pair.second->tick (delta_time); } } if (m_current_active_character && g_is_editor_mode == false ) { m_current_active_character->tick (delta_time); } }
可以看到除了调用每个 GO 的 tick ,最后还判断了是否有当前激活的角色和是否在编辑模式,如果不在编辑模式且存在激活的角色(也就是可被控制的角色),就调用该角色的 tick。角色 Character 类可以通过游戏对象 GObject 类来创建,因此它们都属于游戏对象,而 GObject 类的成员如下:
1 2 3 4 5 6 7 8 protected : GObjectID m_id {k_invalid_gobject_id}; std::string m_name; std::string m_definition_url; std::vector<Reflection::ReflectionPtr<Component>> m_components;
每一个 GO 包含唯一的 GID 标识,名字,读取的链接以及各种组件,这些组件需要使用反射,因为组件有许多不同的类型,比如动画组件、mesh 组件、运动组件、相机组件等等,并且这些组件还要显示在组件面板,所以需要用到反射机制。于是每一个 GO 的 tick 就是调用它所有组件的 tick:
1 2 3 4 5 6 7 8 9 10 void GObject::tick (float delta_time) { for (auto & component : m_components) { if (shouldComponentTick (component.getTypeName ())) { component->tick (delta_time); } } }
而不同组件的 tick 自然就是对应于动画、物理、相机运动、物体形变等各个逻辑系统的运算。
1.2 输入管理系统 输入管理系统 InputSystem
类自然是用来管理鼠标、键盘的各种输入并作出响应,这里不多赘述。
2 swapLogicRenderData 逻辑系统运算完毕后,这一帧的世界就构建完成了,但是逻辑运算是在当前世界上进行的,如果世界的资源发生了改变,那么逻辑系统告诉渲染系统更新了哪些资源,渲染系统要获取这些资源加入到渲染资源中,这是通过渲染系统中的 swapLogicRenderData
函数完成的:
1 void RenderSystem::swapLogicRenderData () { m_swap_context.swapLogicRenderData (); }
可以看到该函数调用了渲染系统的成员 m_swap_context
的 swapLogicRenderData
函数, m_swap_context
属于 RenderSwapContext
类,其中包含要交换的数据 RenderSwapData
,RenderSwapData
的定义如下:
1 2 3 4 5 6 7 8 9 10 struct RenderSwapData { std::optional<LevelResourceDesc> level_resource_desc; std::optional<GameObjectResourceDesc> game_object_resource_desc; std::optional<GameObjectResourceDesc> game_object_to_delete; std::optional<CameraSwapData> camera_swap_data; void addDirtyGameObject (GameObjectDesc desc) ; void addDeleteGameObject (GameObjectDesc desc) ; };
其中包含关卡资源、游戏对象、相机数据,这些都是在逻辑系统计算的。这里使用了 C++ 17 的新特性 optional
,为了之后可以通过 has_value
函数快速判断是否需要在渲染系统中更新这些资源的数据。
m_swap_context
的 swapLogicRenderData
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void RenderSwapContext::swapLogicRenderData () { if (isReadyToSwap ()) { swap (); } } bool RenderSwapContext::isReadyToSwap () const { return !(m_swap_data[m_render_swap_data_index].level_resource_desc.has_value () || m_swap_data[m_render_swap_data_index].game_object_resource_desc.has_value () || m_swap_data[m_render_swap_data_index].game_object_to_delete.has_value () || m_swap_data[m_render_swap_data_index].camera_swap_data.has_value ()); } void RenderSwapContext::swap () { resetLevelRsourceSwapData (); resetGameObjectResourceSwapData (); resetGameObjectToDelete (); resetCameraSwapData (); std::swap (m_logic_swap_data_index, m_render_swap_data_index); }
其功能就是当需要在渲染系统中更新这些资源的数据时就进行数据交换操作。
3 rendererTick 接下来就是最重要的 rendererTick 了,rendererTick 调用了渲染系统的 tick 函数:
1 2 3 4 5 bool PilotEngine::rendererTick () { g_runtime_global_context.m_render_system->tick (); return true ; }
渲染系统的 tick 函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void RenderSystem::tick () { processSwapData (); m_rhi->prepareContext (); m_render_resource->updatePerFrameBuffer (m_render_scene, m_render_camera); m_render_scene->updateVisibleObjects (std::static_pointer_cast <RenderResource>(m_render_resource), m_render_camera); m_render_pipeline->preparePassData (m_render_resource); 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" ); } }
首先是处理从逻辑系统交换来的数据,也就是根据场景或者 GO 是否有变化从硬盘加载对应的资源或者卸载部分资源,然后准备渲染的各种数据。这些数据包括:
变换矩阵、场景的光源数据,这通过 updatePerFrameBuffer
函数来设定,该函数根据场景和相机来获取矩阵和场景的光照信息,而整个渲染场景的光源在系统初始化的时候会进行构建,之后不会改变,相机会在逻辑 tick 中改变;
可见的物体数据,这通过 updateVisibleObjects
函数进行,实际上就是在进入渲染之前,把完全不可见的物体剔除掉。
数据准备完毕后准备渲染的 Pass,最后根据指定的渲染模式进行前向渲染或者延迟渲染。
下一节开始我们将学习这些流程的具体实现。