当前位置: 首页 > backend >正文

Filament引擎(二) ——引擎的调用及接口层核心对象

我们在使用filament这样的开源库,主要还是期望使用它的内核代码,并不期望全部搬到我们自己的项目中,通过分析filamentapp模块中的FilamentApp类的实现,可以看到filament的使用方式。如果我们的目的是去开发一个简单的桌面小工具,那么也可以直接参考filament中的demo,依赖filament提供的filamentapp模块,结合SDL与ImGui进行界面开发。

Filament Demo

filament中包含许多demo,为了方便demo的开发,它构建了一个filamentapp的模块,封装了使用流程。这样在构建demo时,只需要关注具体的渲染元素的构建和处理即可。参考项目中最简单的demo hellotriangle(入口文件hellotriangle.cpp),可以看到,在构建filament的demo时,只需要做以下工作:

  1. 构建一个Config结构体,结构体中主要包含一些窗口信息、渲染后端等。Config结构体也是在filamentapp模块中定义。
  2. 构建setup的回调函数,该函数在filament渲染环境创建成功后被回调。在回调函数中可以进行渲染需要的资源准备相关工作。
  3. 构建cleanup的回调函数,该函数在filament渲染环境销毁前被回调。在回调函数中可以进行渲染资源的清理工作,和setup对应,filament作为一个C++工程,其中的资源创建和销毁需要由使用者自行进行管理。
  4. 如果希望渲染的是动画,通过FilamentApp::get().animate()函数,设置没帧的回调函数,在回调函数中,修改渲染元素的相关状态。
  5. 通过FilamentApp::get().run(app.config, setup, cleanup)启动filament的demo应用。

当然,构建c++工程,需要在cmake中进依赖和入口程序的相关组织,这里并不是对无c++经验的零基础进行分享,便不在此赘述。

Filament引擎调用

filamentapp模块使用imgui+SDL库来支持界面的开发,在FilamentApp类中,对于filament引擎的使用,核心程序就在run方法中。以FilamentApp::run方法为入口进行分析,可以看到filament引擎的调用大概流程大概如下:

// 1. 通过filament::Engine::Builder构建filament::Engine对象。FilamentApp::Window创建中调用。
auto engine = Engine::Builder().build();
// 2. 通过filament::Engine实例,创建filament::SwapChain,用于管理图像缓冲和同步显示。对于无窗口渲染,只需要传入宽高即可,对于窗口渲染需要传入窗口句柄。
// 基于窗口渲染:engine->createSwapChain(nativeWindow, flag);
auto swapChain = engine->createSwapChain(width, height);
// 3. 通过filament::Engine实例,构建filament::Renderer,用于渲染
auto renderer = engine->createRenderer();
// 4. 通过filament::Engine实例,构建filament::View,用于管理渲染场景所需的所有重要对象,包括渲染场景、相机、视口及其他重要参数。
auto view = engine->createView();
// filament默认开启了后处理,后处理中有默认的色调映射,会将输出颜色变成Rec709-sRGB-D65,会导致后面读出来的颜色空间不正确。这里要先禁用掉。也可以通过设置输出颜色为Rec709-Linear-D65来保持输出正确。二选一即可。
view->setPostProcessingEnabled(false);
view->setColorGrading(ColorGrading::Builder().outputColorSpace(Rec709-Linear-D65).build(*engine));
// 4. 通过filament::Engine实例,创建filament::Scene,用于渲染场景的组织。如果期望修改背景色,可以通过向场景中加入skybox来实现
auto scene = engine->createScene();
auto skybox = Skybox::Builder().color({0.1, 0.125, 0.25, 1.0}).build(*engine);
scene->setSkybox(skybox);
// 5. 构建渲染实体,并加入到渲染场景中。具体构建过程,和实际渲染需求紧密相关,参考渲染实体构建部分
auto entity = buildEngity();
scene->addEntity(entity);
// 6. 构建相机实例,用于渲染
auto camEntity = utils::EntityManager::get().create();
auto cam = engine->createCamera(camEntity);
// 7. 将渲染场景、相机、视窗等对象加入到filament::View中,这是渲染必要的对象。
view->setScene(scene);
view->setCamera(cam);
view->setViewport({0, 0, width, height});
// 8. 渲染执行,需要多次渲染,将beginFrame、render、endFrame打包循环调用即可。
renderer->beginFrame(swapChain);
renderer->render(view);
// 为了验证渲染结果的正确性,需要可以在此处读取渲染结果并保存成图片进行验证
// renderer->readPixels(0, 0, 512, 512, std::move(*descriptor));
renderer->endFrame();
// 9. 销毁渲染资源
engine->destroy(camEntity);
engine->destroy(entity);
engine->destroy(ib);
engine->destroy(vb);
engine->destroy(material);
engine->destroy(renderer);
engine->destroy(swapChain);
engine->destroy(skybox);
engine->destroy(scene);
engine->destroy(view);

从上面的调用流程,大概可以看出来,对于filament引擎的使用,可以认为filament::Engine就是主要的入口,相关的filament对象主要就是通过filament::Engine进行创建的。filament在Engine类中,通过ResourceList<T>模板对象,管理各类资源,并提供了各类资源对象对应的destroy函数,用于对象的销毁。在filament的设计中大量使用建造者模式,资源对象中基本都存在一个Builder,来支持对象的构建。采用建造者模式的一个好处,就是使用者不用关注对象构建的构建过程,只需要按需传入构建对象需要的参数即可。

渲染实体构建

filament中采用的ECS的渲染架构,渲染实体(Entity)在libs/utils模块下被定义。Entity下只有一个int32_t的私有变量,在内存中它只是代表一个实体的索引值,实体的具体数据是存储在其他地方。这部分在Filament引擎(一) ——渲染框架设计中的ECS的实现部分已经进行分析过。

渲染实体对应数据的构建,在filament中是通过filament::RenderableManager::Builder来完成。参考其源码,渲染实体构建对应数据结构为RenderableManager::BuilderDetailsfilament::RenderableManager::Builder提供了方法来进行对应属性的设置或者构建。参考hellotriangle.cpp,我们在进行一些简单的渲染时,其实只需要关注主要部分信息即可。比如渲染一些基本的图形或者,不考虑光照、骨骼之类的情况,我们只需要关注Builder中的materialgeometryboundingBox等方法即可。

// 顶点对象构建
auto vb = VertexBuffer::Builder().vertexCount(3).bufferCount(1).attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT2, 0, 12).attribute(VertexAttribute::COLOR, 0, VertexBuffer::AttributeType::UBYTE4, 8, 12).normalized(VertexAttribute::COLOR).build(engine);
vb->setBufferAt(engine, 0, VertexBuffer::BufferDescriptor(TRIANGLE_VERTICES, 36, nullptr));
// 索引对象构建
auto ib = IndexBuffer::Builder().indexCount(3).bufferType(IndexBuffer::IndexType::USHORT).build(engine);
ib->setBuffer(engine, IndexBuffer::BufferDescriptor(TRIANGLE_INDICES, 6, nullptr));
// 材质对象构建
auto material = Material::Builder().package(RESOURCES_BAKEDCOLOR_DATA, RESOURCES_BAKEDCOLOR_SIZE).build(engine);
// 渲染实体
auto entity = utils::EntityManager::get().create();
RenderableManager::Builder(1).boundingBox({{ -1, -1, -1 }, { 1, 1, 1 }}).material(0, material->getDefaultInstance()).geometry(0, RenderableManager::PrimitiveType::TRIANGLES, vb, ib, 0, 3).culling(false).receiveShadows(false).castShadows(false).build(engine, entity);

对象的销毁

在filament中,绝大多数的对象,我们都不需要new,也不应该用delete。一般都是Engine提供构建和销毁的接口,或者由对应的Builder进行构建。由于Engine对于对象基本都有直接或者间接的管理,所有如果有未进行销毁的对象,出现泄露,在Debug模式下,会有对应的日志打出。如上面的示例中,如果camEngity不被释放,则会有如下日志:
泄露日志

上面调用流程的示例中,engine->createCamera(camEntity)构建出来的Camera对象指针,不用处理,其生命周期同camEngity一致。实际上,我们也无法通过delete将其删除,它的析构函数被标记为protected。

另外,在对象的构建和销毁的过程中,对象间有依赖关系,我们一般也是建议按照类似栈的方式进行:先创建的后销毁。如一个实体对象,构建依赖VertexBuffer、Material等,则应当先销毁实体,再销毁其依赖的VertexBuffer、Material等对象。

接口层核心对象

从以上的Filament的调用流程上可以看出,Filament的接口层的整体设计相对还是比较优雅简单的,核心对象的基本都是由Engine提供对应的接口创建和销毁接口。渲染实体相关的顶点、纹理、材质等数据则是由对应数据结构提供Builder进行构建,然后通过负责渲染的Manager提供Builder,将这些数据组合构建成渲染实体的真实数据对象。其他的如相机实体、光照实体等也大同小异。接口层的核心对象主要如下:
接口层核心对象

结合调用的流程示例,在Engine中,我们主要做的就是构建一个视图(View),然后通过渲染器(Renderer),将其绘制到指定的窗口或者离屏Buffer(SwapChain)上去。其中,我们要做的最多的工作就是去构建一个View,View主要包括视窗(Viewport)相机(Camera)场景(Scene)等。通过布置场景(Scene),调整相机(Camera)位置,以及改变最终成像的视窗(Viewport),最终才决定了视图中那些信息会被呈现出来。如果我们期望对最终成像的色调效果进行一些修改,这时就需要用到色调分级(ColorGrading)了。如果我们期望进行离屏渲染,这时就需要用到渲染目标(RenderTarget)了。

构建场景(Scene),需要用到天空盒(Skybox)间接光照(IndirectLight)实体(Entity)。实体包括渲染实体、变换信息实体以及灯光实体等不同的类型,用对应的Manager负责构建。渲染实体的构建,需要用到IndexBuffer、VertexBuffer、Material等等数据,这些就需要一定的渲染基础了。无渲染基础的朋友可以先了解下OpenGL、Vulkan、Metal、DirectX等现代图像渲染API,了解其中一种的渲染管线和大致的渲染流程。很久之前,刚入门图像渲染时,有写过一个系列的笔记Android OpenGLES,可作为参考。

Texture

Filament中的纹理,对渲染抽象层的backend::HwTexture进行了封装,支持图像和视频流,视频流通过Stream对象来提供。值得注意的是,它主要是针对Android系统的。做过Android渲染的朋友知道,Android中我们在渲染相机时,一般会用到SurfaceTexture或者AHardwareBuffer,Stream主要就是他们的封装和调用,视频的解码、相机的输入等,并不在Stream中处理,我们需要自行处理视频和相机。

InstanceBuffer

InstanceBuffer主要用于支持同一个实体的重复渲染,即实例化渲染,包含渲染实例所需的额外的矩阵信息。例如我们构建了一个立方体的实体,立方体的实体完全一样,只是期望渲染时的位置、大小、旋转方向等做一些修改,这时候就可以用到InstanceBuffer了。在现代的渲染引擎中,通过InstanceBuffer,来进行多实例渲染,尽量保证数据复用,减少DrawCalls是一个非常常见的操作。Filament中的InstanceBuffer相对比较简单,只支持localTransforms,并不支持其他的属性。

SwapChain

SwapChain是连接渲染系统与显示设备的核心机制,主要用于管理帧缓冲区的提交与呈现。在调用流程示例中有提到,SwapChain支持指定本地渲染窗口以及无窗口的创建方式。指定本地渲染窗口时,需要注意,在不同平台下所需要传入的void* nativeWindow

  • Android: ANativeWindow*
  • macOS(OpenGL): NSView*
  • macOS(Metal): CAMetalLayer*
  • iOS(OpenGL): CAEAGLLayer*
  • iOS(Metal): CAMetalLayer*
  • X11: Window
  • Windows: HWND

ColorGrading

ColorGrading渲染图像的后处理,主要是做色调映射和转换。在上面的Filament调用流程实例中,我们需要禁用view的后处理(view->setPostProcessingEnabled(false)),或者设置ColorGrading的输出颜色空间为Rec709-Linear-D65(view->setColorGrading(ColorGrading::Builder().outputColorSpace(Rec709-Linear-D65).build(*engine)))。这是因为在filament中,view默认使用了后处理,且默认输出颜色空间为Rec709-sRGB-D65。对于通过Skybox将背景色设置为(r:0.1, g:0.125, b:0.25, a:1.0),三顶点分别采用纯色的red、green和blue,这种情况下二者绘制出的三角形对比如下:

sRGB输出Linear输出
sRGB输出Linear输出

显而易见,我们也可以通过ColorSpace,实现自定义的颜色空间的输出。除此之外,ColorGrading还支持曝光(exposure)、白平衡(whiteBalance)、明暗对比度(contrast)、饱和度(saturation)、自然饱和度(vibrance)、RGB颜色通道混合(channelMixer)等等一系列的调整和设置,可用于颜色校正、HDR渲染适配、艺术化风格等等各方面。


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


http://www.xdnf.cn/news/8705.html

相关文章:

  • 在Linux上安装Miniconda
  • leetcode438.找到字符串中所有字母异位词
  • Python之两个爬虫案例实战(澎湃新闻+网易每日简报):附源码+解释
  • 力扣 54 .螺旋矩阵
  • 148. 排序链表
  • 40-智慧医疗服务平台(在线接/问诊/机器学习)
  • 电工杯数学建模竞赛a题完整参考文章
  • C++魔法药水的配方 全国信息素养大赛复赛决赛 C++小学/初中组 算法创意实践挑战赛 内部集训模拟题详细解析
  • 深度学习模型在PDE求解中的实战:详细综述
  • 电磁场与电场、磁场的关系
  • React从基础入门到高级实战:React 基础入门 - React Hooks 入门
  • 状态码··
  • 【go】程序启动时发生了什么?为什么选择go语言开发,优势劣势
  • 5.1/Q1,GBD数据库最新文章解读
  • 创新项目实训开发日志7
  • 【动态规划】简单多状态(一)
  • 77. Combinations
  • Qt实战:自定义QTreeWidget搜索隐藏显示项功能 | 附完整源码
  • 基于音频Transformer与动作单元的多模态情绪识别算法设计与实现(在RAVDESS数据集上的应用)
  • 算法、算力、数据哪个更重要
  • C#核心概念解析:析构函数、readonly与this关键字
  • java 代码查重(五)比较余弦算法、Jaccard相似度、欧式距离、编辑距离等在计算相似度的差异
  • 开发者工具箱-鸿蒙大小写转换开发笔记
  • H3C-WAF-单机部署
  • 【每天一个知识点】“数字人”(Digital Human)
  • Easy Dataset数据集构建使用
  • 解析 Flask 上下文机制:请求上下文、应用上下文
  • AI Agent开发第74课-解构AI伪需求的魔幻现实主义
  • 【c++】成员函数被声明为 `const` 时
  • Oracle 的SHRINK 操作实现原理