Filament引擎(三) ——引擎渲染流程

通过Filament引擎(二) ——引擎的调用及接口层核心对象的介绍我们知道,要在项目中使用filament,首先我们需要构建出filament的Engine的对象,然后通过filament::Engine对象实例,来构建其他对象,组装渲染场景,执行渲染操作等。那么filament::Engine的构建过程具体发生了什么事情呢?我们组装的渲染场景,又是如何被渲染渲染出来的?在本篇博客中,我们进一步去了解下Filament引擎的内部是如何工作的。

一、Engine构建

我们通过Engine::Builder().build()来创建filament::Engine对象,会调用到Engine* FEngine::create(Builder const& builder)函数来创建FEngine对象。

在编译时,如果开启FILAMENT_SINGLE_THREADED宏,则filament不会开启单独的渲染线程,否则Engine.create的时候,会构建单独的DriverThread,渲染工作在此线程中执行。出于渲染效率的考虑,我们在使用filament时,一般都是会采用异步渲染的方式。

在异步渲染的模式下,filament渲染的核心对象Driver,会在渲染线程中根据调用者配置和当前运行环境进行构建。构建成功后,通知工作线程Driver构建成功,然后渲染线程会进入命令处理的循环中。

二、工作线程

我们将调用Filament命令的线程称为工作线程。在工作线程中,我们调用filament的API,创建IndexBuffer、VertexBufer、Texture、Material之类的渲染对象,构建渲染场景,然后调用渲染器进行渲染,实际上并不会真正的调用OpenGL、Metal、Vulkan、DirectX这样的底层渲染API。
在构建渲染对象时,实际上会调用我们封装的渲染驱动(OpenGLDriver、MetalDriver、VulkanDriver等,具体调用取决于平台及构建Engine时的配置),会先创建出filament抽象出的对应的RHI对象,如果需要调到底层渲染API,引擎会通过CommandStream在CircularBuffer实例中构建出对应的Command,由渲染线程进行执行。
在Filament引擎(一) ——渲染框架设计中异步渲染的实现这部分,对此部分实现,也进行了分析和说明。它用了一些C++开发中的小技巧,在此也不再赘述。

渲染驱动指令的构建

IndexBuffer::setBuffer的调用为例,其堆栈如下:
在这里插入图片描述
CommandStream::updateIndexBuffer函数是通过DriverAPI.inc中的宏DECL_DRIVER_API_N声明,DECL_DRIVER_API_N会展开为DECL_DRIVER_API,在CommandStream.h中引入DriverAPI.inc前对DECL_DRIVER_API进行了定义:

#define DECL_DRIVER_API(methodName, paramsDecl, params)                                         \inline void methodName(paramsDecl) {                                                        \DEBUG_COMMAND_BEGIN(methodName, false, params);                                         \using Cmd = COMMAND_TYPE(methodName);                                                   \void* const p = allocateCommand(CommandBase::align(sizeof(Cmd)));                       \new(p) Cmd(mDispatcher.methodName##_, APPLY(std::move, params));                        \DEBUG_COMMAND_END(methodName, false);                                                   \}

所以,实际上CommandStream::updateIndexBuffer的实现,宏展开后如下:

inline void updateIndexBuffer(filament::backend::Handle<filament::backend::HwIndexBuffer> ibh, filament::backend::BufferDescriptor && data, unsigned int byteOffset){                                                       mDriver.debugCommandBegin(this, false, "updateIndexBuffer");using Cmd = CommandType<decltype(&Driver::updateIndexBuffer)>::Command<&Driver::updateIndexBuffer>;void* const p = allocateCommand(CommandBase::align(sizeof(Cmd)));new(p) Cmd(mDispatcher.updateIndexBuffer_, std::move(ibh), std::move(data), std::move(byteOffset));mDriver.debugCommandEnd(this, false, "updateIndexBuffer");
}

filament以DriverAPI中的函数作为模板参数,通过CommandType模板类及其内部模板类Command,将DriverAPI函数及调用传入的参数封装成CommandBase的子类对象,对象存储在CircularBuffer中。这样,渲染线程就能不关注渲染的具体指令,而是按照统一的调用方式进行执行。

三、渲染线程

在这里插入图片描述
渲染线程需要做的工作,只是不断的从CircularBuffer中取出需要执行命令,让Driver进行执行。

FEngine::execute:通过调用mCommandBufferQueue.waitForCommands,每次循环从引擎实例中的CommandBufferQueue实例内,取出当前的待执行的Commands(std::vector<CommandBufferQueue::Range>), 然后遍历的将Range所指向的内存Buffer,传递给CommandStream.execute进行处理。

  • CommandBufferQueue::Range记录的只有begin和end两个void*指针,指向的是一组Commands的起止地址。CircularBuffer、Command及Range的关系示意如下:
    在这里插入图片描述

  • mCommandBufferQueue.waitForCommands内部在命令队列为空或者在暂停渲染时,进入等待状态,阻塞渲染线程的工作。

CommandStream.execute: CommandStream封装了代表渲染驱动的Driver,在execute方法中,会将FEngine.execute传递进来的buffer(CommandBufferQueue::Range) 转换成Command(CommandBase*)进行执行。Command执行(CommandBase.execute)会返回下一个CommandBase对象的指针。

四、帧渲染

通过上面的分析,我们大致可以知道,在工作线程中,我们调用的filament的API,会直接或间接的转换成渲染驱动命令,存储在CircularBuffer中,由渲染线程进行消费。

对于VertexBuffer、IndexBuffer、Texture等对象的创建,filament会构建出对应渲染驱动命令。对于Material对象,则会在其实例化的时候(createInstance),构建对应的渲染驱动命令。渲染Entity的构建,会构建“创建渲染图元”的驱动命令,这些调用路径都相对比较简单且直观。

这种工作线程+渲染线程的方式,是现代渲染引擎比较通用的实现方案。其实对于一个渲染引擎来说,更关键的是,渲染场景的组织如何去设计,以及在工作线程中如何将组织好的渲染场景转换成渲染线程可以“无脑”执行的渲染指令。

在filament中,当我们组织好场景后,会调用Renderer.beginFrameRenderer.render以及Renderer.endFrame进行场景的“渲染”。这个过程,实际上就是将我们组织的渲染场景,变成一系列的渲染驱动命令,发送到渲染线程执行。

beginFrame

在beginFrame中,会利用FrameSkipper去动态控制渲染帧的跳过策略,在渲染负载过高时返回false。我们在使用时,当beginFrame返回false时,就不在调用Renderer.render,避免因渲染延迟导致画面卡顿。
除此之外,beginFrame中主要做的工作包括:

  1. SwapChain::makeCurrent: 交换链和渲染环境的上下文绑定,让当前渲染线程知道要把图画到哪里,确保后续的渲染操作能正确显示在指定的面上。
  2. CommandStream::tick: 发个指令让渲染线程执行需要在渲染线程执行的周期性的任务。这些任务一般是因为渲染后端实现时需要控制某些渲染指令的调用时机而发出的任务。
  3. CommandStream::beginFrame: 发送beginFrame的渲染驱动命令。不同驱动(MetalDriver、OpenGLDriver、VulkanDriver等)处理不同。
  4. FrameInfoManager::beginFrame: 帧信息收集,FrameInfoManager用于管理帧级别的渲染元数据和性能监控信息。
  5. FEngine.prepare: 主要工作都是针对材质,包括将材质实例中的参数的修改同步到渲染管线,保证运行时修改的材质参数能被GPU使用。以及检查着色器程序与材质参数一致性。

render

filament中每帧的渲染由View进行组织,场景(Scene)和相机(Camera)是其进行内容呈现的必要元素。render的核心调用栈为:FRenderer::render->FRenderer::renderInternal->FRenderer::renderJob

在render中,主要是对场景、相机的存在进行判断,以及保证渲染前的flush操作。

在renderInternal中,会基于Engine中RenderPassArena构建一个RootArenaScope对象,在渲染构建帧图的过程中,RenderPass会被存储到RenderPassArean中。在renderInternal执行完成后,RootAreanScope自动析构,析构时会把新加入到RenderPassArean中的RenderPass使用的内存进行回收。此外,renderInternal中,会为JobSystem创建rootJob,并在函数执行完成前runAndWait(rootJob),以保证在此间所有的Job都在管控中切执行完成。

在renderJob中,最为核心的工作包括帧图(FrameGraph)的构建、编译和执行 以及 后处理(PostProcessManager)的设置,主要的流程大致为:

  1. engine.getPostProcessManager().setFrameUniforms(driver, view.getFrameUniforms()) 将后处理过程中需要用到的渲染统一变量进行同步。
  2. 获取view中的各种配置项和功能状态,包括抗锯齿、防抖动、后处理等等各种配置和状态。
  3. view.prepare进行View的准备工作,其入参中的cameraInfo由view.computeCameraInfo返回,并根据后处理、fass等设置,在必要时将渲染视口的尺寸调整为16的倍数,帮助优化内存分配和四边形渲染。
    1. scene->prepare收集渲染此场景所需的所有信息。工作流程:
      1. 遍历Entities,按照光源实体和渲染实体进行分类,存储到不同的容器中。
      2. 根据光源实体和渲染实体的容器中实体个数,调整mLightDatamRenderableData的容器大小,他们分别是光照和渲染的SoA(Structure of Arrays)数据。
      3. 发起任务到JobSystem中执行,填充光照和渲染的SoA。定向光源(directional ligth)需要单独处理。
    2. 当场景中设置了POINT、FOCUSED_SPOT及SPOT类型的光源,会在任务系统(JobSystem)中运行FView::prepareVisibleLights,对设置进来的这些光源进行准备工作。
      1. 进行必要的光照剔除,确定场景中那些光源是可见的。未被标记参与光照投影计算的(lightCaster)、与视椎不相交等情况下的光源,会比标记为不可见。可见光源会被排到SoA的前面来,便于渲染处理。
      2. 计算光源和相机之间的距离,按照离相机由近到远对光源进行排序。按照源码中注释的解释,这么做是未来方便后续构建光源树。如果光源数量超过GPU缓冲区所能容纳的数量,距离相机较远的光源会被舍弃掉。
    3. 开启了视椎裁切的时候,将不在视椎中的渲染对象进行剔除,标记为VISIBLE_RENDERABLE_BIT
    4. prepareVisibleLights执行完成后,判断如果存在动态光源,就进行Froxel化。Froxel是filament中的光源在视椎空间下的体素化,结合Frustum和Voxel造的词。光照效果的渲染,filament采用的是分簇前向渲染的方式,来平衡渲染效果和渲染效率。关于filament具体的光照实现,在另外一篇博客中再进一步分析。
    5. 进行阴影的准备工作,主要是根据光照信息进行ShadowMap的构建和更新。
    6. 按照渲染实体的可见性,将渲染的SoA进行分组,然后进行渲染对象的准备工作,更新渲染的SoA。分组包括:
      1. 主摄像机可见: 被主摄像机捕获且需要直接渲染到屏幕的对象,会参与主渲染通道。
      2. 主摄像机可见且进行定向光阴影投射:主摄像机可见且需要为定向光投射阴影,在生成平行光阴影贴图时渲染,同时也会参与主渲染通道。
      3. 进行定向光阴影投射:不可见于主摄像机,但需要进行定向光阴影投射,仅在生成平行光阴影贴图时渲染。
      4. 潜在的点光源阴影投射:可能被点光源或聚光灯照射,但未被主摄像机直接看到,在生成点光源阴影贴图时渲染。
      5. 明确不可见:完全不可见且无需参与任何渲染或阴影计算,会在渲染时被剔除。
    7. 进行光照信息的准备工作,更新光照的UBO,以及设置IBL(Indirect Light)等等。
  4. view.prepareUpscaler进行上采样的设置。
  5. FrameGraph的构建和设置,这部分在renderJob中占据最大的篇幅。其主要流程如下:
    1. 以Renderer中的mResourceAllocator作为入参,构建FrameGraph实例。
    2. 获取FrameGraph中的Blackboard,它主要是作为FrameGraph中的全局资源管理器,用于存储和传递渲染过程中需要共享的虚拟资源(如纹理、渲染目标等)。在有阴影效果时,设置其阴影资源,阴影资源会用到view.prepare时构建的ShadowMap。
    3. 构建FrameGraph的RenderTarget资源,作为FrameGraph的渲染目标。
    4. 根据渲染对象的可见性进行图元更新。
    5. 在此过程中,会解析各种设置和状态,构建RenderPass,作为FrameGraph中的渲染单元。
  6. FrameGraph的“编译”和执行。编译阶段,会分析渲染通道依赖关系,进行无效节点的剔除,并标记资源生命周期,以保证在资源不再被使用时及时销毁或回收。执行阶段就是根据编译结果动态实例化GPU资源并将渲染指令提交到真正的渲染线程中。

关于renderJob中关键源码的具体分析,后续进行进一步的展开,此处仅做简单的流程性分析。

endFrame

一帧渲染完结,由endFrame中来进行必要的指令提交,资源回收等工作,主要包括:

  1. SwapChain::commit: 交换链提交,一般会刷新缓冲区,可以看做是将渲染管线的后缓冲区提交到渲染链前段,使渲染结果能从GPU到显示设备。
  2. FrameInfoManager::endFrame: 帧信息收集完结,同beginFrame中的FrameInfoManager::beginFrame对应。
  3. CommandStream::tick: 发个指令让渲染线程执行需要在渲染线程执行的周期性的任务。
  4. mResourceAllocator.gc帧资源回收

欢迎转载,转载请保留文章出处。求闲的博客[https://blog.csdn.net/junzia/article/details/149294146]


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88603.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88603.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Oracle存储过程导出数据到Excel:全面实现方案详解

技术背景与需求分析 数据导出是企业级应用的核心功能,Oracle存储过程因其高性能执行(减少网络传输)、代码复用性(封装业务逻辑)和事务安全性(ACID保障)成为理想载体。Excel作为使用率$ \geq 95% $的办公工具,其兼容性需求尤为突出。典型场景包括: 财务报表自动生成物…

解决el-table右下角被挡住部分

一部分展示不全&#xff0c;被遮挡&#xff0c;因为 最右边加了fixed"right"<el-table-column fixed"right" label"操作" width"120">解决&#xff1a;1、去除fixed"right"或2、设置样式单页面<style lang"sc…

Waiting for server response 和 Content Download

在浏览器网络调试&#xff08;如 Chrome DevTools 的 Network 面板&#xff09;中&#xff0c;Timing 选项卡下的 Waiting for server response 和 Content Download 是两个关键性能指标&#xff0c;它们分别代表了 HTTP 请求生命周期的不同阶段。以下是详细解释和优化方案&…

《Java Web程序设计》实验报告五 Java Script学习汇报

目 录 一、实验目的 二、实验环境 三、实验步骤和内容 1、小组成员分工&#xff08;共计4人&#xff09; 2、实验方案 3、实验结果与分析 Ⅰ、简述JavaScript的产生过程与Java的关系 Ⅱ、简述JavaScript的特点有哪些 Ⅲ、简述ECMAScript的历史 Ⅳ、简述ECMAScript与J…

C#与FX5U进行Socket通信

实现效果实现步骤&#xff1a;注意&#xff1a;详细的参数这里就不说明了&#xff0c;自己网上搜即可&#xff1b;打开GX Works3 创建FX5U项目系统参数设置PLC的具体型号&#xff08;我有实物PLC&#xff09;设置IP及组态参数添加通讯设备&#xff08;这里PLC做客户端&#xff…

ubuntu20.04基于tensorRT和c++跑yolo11

设备 系统&#xff1a;Ubuntu 20.04 显卡&#xff1a;NVIDIA GeForce RTX 3050 显卡驱动&#xff1a; Driver Version: 535.183.01 CUDA Version: 12.2 关键软件版本总结 Cmake: 3.28.6 Cuda&#xff1a; 12.2.2 Cudnn: 8.9.7 TensorRT: 10.8.0.43 Python&#xff1a;3.10.1…

玖玖NFT数字藏品源码(源码下载)

玖玖NFT数字藏品源码 这套还是很不错的&#xff0c;前端uniapp&#xff0c;后端FastAdmin&#xff0c;对接汇元支付&#xff0c;富友支付&#xff0c;对接avata链&#xff0c;感兴趣的自行下载研究 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/9133…

【Redis-05】高可用方案-主从哨兵

1 概述 高可用&#xff08;High Availability&#xff09;指系统在部分节点故障时仍能持续提供服务的能力。Redis 作为核心缓存组件&#xff0c;主流的高可用方案有主从复制、哨兵模式、集群模式三种。本文介绍主从复制、哨兵模式两种高可用方案。 2 主从复制 通过 “一主多从”…

焊接机器人智能节气装置

工业焊接作为现代制造业的重要组成部分&#xff0c;广泛应用于汽车、航空航天、建筑、船舶等多个领域。随着自动化技术的快速发展&#xff0c;焊接机器人已成为提升焊接效率和质量的关键装备。在传统焊接及部分自动化焊接过程中&#xff0c;气体流失问题仍然普遍存在&#xff0…

【6.1.0 漫画数据库技术选型】

漫画数据库技术选型 &#x1f3af; 学习目标&#xff1a;掌握架构师核心技能——数据库技术选型&#xff0c;针对不同业务场景选择最合适的数据库方案 &#x1f3db;️ 第一章&#xff1a;关系型数据库对比选型 &#x1f914; MySQL vs PostgreSQL vs TiDB 想象数据库就像不同…

CVE-2022-4262/CVE-2022-3038

CVE-2022-4262&#xff08;Linux内核UAF漏洞&#xff09;漏洞原理CVE-2022-4262是Linux内核中RDS&#xff08;Reliable Datagram Sockets&#xff09;协议实现的一个UAF&#xff08;Use-After-Free&#xff0c;释放后使用&#xff09;漏洞。具体来说&#xff1a;在rds_rdma_ext…

[Token]Token merging for Vision Generation

Token Compression for Vision Domain_Generation 文章目录Image GenerationToken Merging for Fast Stable Diffusion, CVPRW 2023.Token Fusion: Bridging the Gap between Token Pruning and Token Merging, WACV 2024ToDo: Token Downsampling for Efficient Generation of…

React封装过哪些组件-下拉选择器和弹窗表单

背景&#xff08;S - Situation&#xff09;&#xff1a;在某活动管理系统中&#xff0c;前端页面需要支持用户选择“要配置的当前活动”&#xff0c;并提供「新增」「编辑」功能&#xff0c;操作内容包括填写活动名称、ID、版本号等字段。原始实现逻辑分散、复用性差&#xff…

多租户架构下的多线程处理实践指南

在现代 SaaS 系统中&#xff0c;多租户架构&#xff08;Multi-Tenant Architecture&#xff09;已成为主流。然而&#xff0c;随着系统性能要求的提升和业务复杂度的增加&#xff0c;多线程成为不可避免的技术手段。但在多租户环境下使用多线程&#xff0c;容易引发数据错乱、租…

MyBatis插件机制揭秘:从拦截器开发到分页插件实战

一、拦截器体系架构解析 1.1 责任链模式在MyBatis中的实现 MyBatis通过动态代理技术构建拦截器链&#xff0c;每个插件相当于一个切面&#xff1a; // 拦截器链构建过程 public class InterceptorChain {private final List<Interceptor> interceptors new ArrayList<…

百度文心一言开源ERNIE-4.5深度测评报告:技术架构解读与性能对比

目录一、技术架构解读1.1、ERNIE 4.5 系列模型概览1.2、模型架构解读1.2.1、异构MoE&#xff08;Heterogeneous MoE&#xff09;1.2.2、视觉编码器&#xff08;Vision Encoder&#xff09;1.2.3、适配器&#xff08;Adapter&#xff09;1.2.4、多模态位置嵌入&#xff08;Multi…

Matplotlib 模块入门

Python 中有个非常实用的可视化库 ——Matplotlib。数据可视化是数据分析中不可或缺的环节&#xff0c;而 Matplotlib 作为 Python 的 2D 绘图库&#xff0c;能帮助我们生成高质量的图表&#xff0c;让数据更直观、更有说服力。接下来&#xff0c;我们将从 Matplotlib 的概述、…

LeetCode 3169.无需开会的工作日:排序+一次遍历——不需要正难则反,因为正着根本不难

【LetMeFly】3169.无需开会的工作日&#xff1a;排序一次遍历——不需要正难则反&#xff0c;因为正着根本不难 力扣题目链接&#xff1a;https://leetcode.cn/problems/count-days-without-meetings/ 给你一个正整数 days&#xff0c;表示员工可工作的总天数&#xff08;从第…

VUE3 el-table 主子表 显示

在Vue 3中&#xff0c;实现主子表&#xff08;主从表&#xff09;的显示通常涉及到两个组件&#xff1a;一个是主表&#xff08;Master Table&#xff09;&#xff0c;另一个是子表&#xff08;Detail Table&#xff09;。我们可以使用el-table组件来实现这一功能。这里&#x…

张量数值计算

一.前言前面我们介绍了一下pytorch还有张量的创建&#xff0c;而本章节我们就来介绍一下张量的计算&#xff0c;类型转换以及操作&#xff0c;这个是十分重要的&#xff0c;我们的学习目标是&#xff1a;掌握张量基本运算、掌握阿达玛积、点积运算 掌握PyTorch指定运算设备。Py…