Filament引擎(二) ——引擎的调用及接口层核心对象
我们在使用filament这样的开源库,主要还是期望使用它的内核代码,并不期望全部搬到我们自己的项目中,通过分析filamentapp模块中的FilamentApp类的实现,可以看到filament的使用方式。如果我们的目的是去开发一个简单的桌面小工具,那么也可以直接参考filament中的demo,依赖filament提供的filamentapp模块,结合SDL与ImGui进行界面开发。
Filament Demo
filament中包含许多demo,为了方便demo的开发,它构建了一个filamentapp的模块,封装了使用流程。这样在构建demo时,只需要关注具体的渲染元素的构建和处理即可。参考项目中最简单的demo hellotriangle(入口文件hellotriangle.cpp),可以看到,在构建filament的demo时,只需要做以下工作:
- 构建一个Config结构体,结构体中主要包含一些窗口信息、渲染后端等。Config结构体也是在filamentapp模块中定义。
- 构建setup的回调函数,该函数在filament渲染环境创建成功后被回调。在回调函数中可以进行渲染需要的资源准备相关工作。
- 构建cleanup的回调函数,该函数在filament渲染环境销毁前被回调。在回调函数中可以进行渲染资源的清理工作,和setup对应,filament作为一个C++工程,其中的资源创建和销毁需要由使用者自行进行管理。
- 如果希望渲染的是动画,通过
FilamentApp::get().animate()
函数,设置没帧的回调函数,在回调函数中,修改渲染元素的相关状态。 - 通过
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::BuilderDetails
,filament::RenderableManager::Builder
提供了方法来进行对应属性的设置或者构建。参考hellotriangle.cpp,我们在进行一些简单的渲染时,其实只需要关注主要部分信息即可。比如渲染一些基本的图形或者,不考虑光照、骨骼之类的情况,我们只需要关注Builder中的material
、geometry
、boundingBox
等方法即可。
// 顶点对象构建
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输出 |
显而易见,我们也可以通过ColorSpace,实现自定义的颜色空间的输出。除此之外,ColorGrading还支持曝光(exposure)、白平衡(whiteBalance)、明暗对比度(contrast)、饱和度(saturation)、自然饱和度(vibrance)、RGB颜色通道混合(channelMixer)等等一系列的调整和设置,可用于颜色校正、HDR渲染适配、艺术化风格等等各方面。
欢迎转载,转载请保留文章出处。求闲的博客[https://blog.csdn.net/junzia/article/details/148053163]