Filament引擎(一) ——渲染框架设计

filament是谷歌开源的一个基于物理渲染(PBR)的轻量级、高性能的实时渲染框架,其框架架构设计并不复杂,后端RHI的设计也比较简单。重点其实在于项目中材质、光照模型背后的方程式和理论,以及对它们的实现。相关的信息,可以参考官方给出的文档。filament比较注重运行效率,在实现上也使用了一些抽象和技巧,这些也是比较有意思的代码,可以学习和借鉴。

框架的整体设计

按照github项目中的说明,filament是适用主流平台的实时物理渲染引擎,设计的目的是期望在Android上小而快。

Filament is a real-time physically based rendering engine for Android, iOS, Linux, macOS, Windows, and WebGL. It is designed to be as small as possible and as efficient as possible on Android.

在其设计中,使用Entity、components下各Manager中用来表示Component的数据结构及Manager本身,再加上Renderer,形成了ECS(Entity-Component-System)架构。

另外filament中存在一个backend的子工程,是一套自定义的RHI(Render Hardware Interface),封装了诸如OpenGL、Vulkan、Metal的后端渲染API(PS:没有DirectX,Windows平台,官方默认使用Vulkan)。

在“ECS”和“RHI”之间,filament通过Renderer类,内部使用RenderPassFrameGraph等来组织backend提供的RHI进行渲染,承担最重要的“RenderSystem”的工作。

渲染后端RHI设计

Filament的渲染后端RHI非常轻量,并没有什么复杂的设计,使用起来也相对比较简单。不过它在异步渲染的实现上有一些不太好看但是实用的“奇淫巧技”。

Filament定义了一个RHI空对象基类HwBase,然后派生了一系列的RHI对象,包括HwVertexBufferHwIndexBufferHwProgramHwTextureHwRenderTarget等等。基本上和其他诸多渲染框架采用的是类似的概念,然后不同的后端渲染(OpenGL/Vulkan/Metal)继承这些对象,进行了不同后端的实现,如基于HwProgram派生了OpenGLProgramVulkanProgram以及MetalProgram。其他各类对象也是类似。

另外Filament有一个Driver的基类,基于这个Driver基类,派生了各后端渲染的Driver,包括VulkanDriver/MetalDriver/OpenGLDriver/NoopDriver,没有DirectXDriver,Windows平台,官方使用Vulkan来进行渲染。Driver作为渲染的主要入口,所有RHI对象的创建销毁及更新,都经由Driver来进行调用。
请添加图片描述

RHI的使用流程

Filament的RHI抽象和封装度并不复杂,所以在使用上,如果有用过OpenGL、Vulkan、Metal的API,那么理解Filament的后端渲染也比较简单。主要的使用流程参考以下代码:

// 1. 初始化RHI的Driver
auto backend = filament::Backend::METAL;
auto platform = PlatformFactory::create(&backend);
Platform::DriverConfig const driverConfig;
auto driver = platform->createDriver(nullptr, driverConfig);
auto api = driver;
// 2. 创建SwapChainHandle,作为输出。如果需要输出到Window上,需要利用Window指针来进行创建
// api.createSwapChain(view.ptr, 0)
auto swapChain = api.createSwapChainHeadless(256, 256, 0);
api.makeCurrent(swapChain, swapChain);
// 3. 创建ProgramHandle,后续用来进行渲染,依赖Program对象,注意,Program和ProgramHandle不是同一个东西,Program就是用来创建ProgramHandle的一个参数集合
Program progCfg; // 进行相关配置
progCfg.shaderLanguage(ShaderLanguage::MSL);
projCfg.shader(ShaderStage::VERTEX, mVertexBlob.data(), mVertexBlob.size());
projCfg.shader(ShaderStage::FRAGMENT, mFragmentBlob.data(), mFragmentBlob.size());
projCfg.setSamplerGroup(0, ShaderStageFlags::ALL_SHADER_STAGE_FLAGS, psamplers, sizeof(psamplers) / sizeof(psamplers[0]));
auto program = api.createProgram(progCfg);
// 4. 创建渲染需要的纹理,更新纹理数据
auto tex = api.createTexture(SamplerType::SAMPLER_2D, 1, TextureFormat::RGBA8, 1, 512, 512, 4, usage);
PixelBufferDescriptor descriptor = createImage();
api.update3DImage(tex, 1, 0, 0, 0, 512, 512, 1, std::move(descriptor));
// 5. 设置采样方式
SamplerGroup samplers(1);
SamplerParams sparams = {};
sparams.filterMag = SamplerMagFilter::LINEAR;
sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST;
samplers.setSampler(0, { tex, sparams });
auto sgroup = api.createSamplerGroup(samplers.getSize(), utils::FixedSizeString<32>("Test"));
api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api));
api.bindSamplers(0, sgroup);
// 6. 开始一帧渲染,一个完整的渲染周期,可能由很多次渲染组成的一个渲染帧
api.beginFrame(0, 0, 0);
// 7. 创建RenderTarget,配置RenderPassParams,进而开启RenderPass。RenderPass一般表示的是在一个渲染目标上进行的一系列渲染。
RenderPassParams params = {};
params.flags.clear = TargetBufferFlags::COLOR;
params.clearColor = {0.f, 0.f, 1.f, 1.f};
params.flags.discardStart = TargetBufferFlags::ALL;
params.flags.discardEnd = TargetBufferFlags::NONE;
params.viewport.height = 512;
params.viewport.width = 512;
auto renderTarget = api.createDefaultRenderTarget(0);
api.beginRenderPass(renderTarget, params);
// 8. 配置PipelineState
PipelineState state;
state.program = program;
state.rasterState.colorWrite = true;
state.rasterState.depthWrite = false;
state.rasterState.depthFunc = RasterState::DepthFunc::A;
state.rasterState.culling = CullingMode::NONE;
// 9. 构建Primitive,Primitive主要包括顶点位置、顶点索引等相关数据
auto vertexBufferObj = api.createBufferObject(size, BufferObjectBinding::VERTEX, BufferUsage::STATIC);
auto vertexBufferInfo = api.createVertexBufferInfo(bufferCount, attributeCount, attributes);
auto vertexBuffer = api.createVertexBuffer(vertexBufferObj, vertexBufferInfo);
auto indexBuffer = api.createIndexBuffer(elementType, mIndexCount, BufferUsage::STATIC);
api.updateIndexBuffer(indexBuffer, std::move(indexBufferDesc), 0);
auto primitive = api.createRenderPrimitive( vertexBuffer, indexBuffer, PrimitiveType::TRIANGLES);
// 10. 渲染指令
api.draw(state, primitive, 0, mIndexCount, 1);
// 11. 终止RenderPass。
api.endRenderPass();
// 12. 一帧内要进行的所有渲染指令调用完后,提交并结束一帧渲染
api.commit(swapChain);
api.endFrame()
// 13. 如果当前的渲染任务结束了,不会再执行了,销毁所有资源。
api.destroySamplerGroup(sgroup);
api.destroyProgram(program);
api.destroySwapChain(swapChain);
api.destroyRenderTarget(renderTarget);
// 14. 如果所有渲染结束,要终止渲染了,结束渲染
api.finish();
driver->purge();
driver->terminate();

以上是简化的一个渲染流程中,在实际的渲染过程中,我们一般会进行多帧循环渲染。在一帧渲染过程中,我们也可能会有多个renderpass,甚至有subrenderpass。RenderTargetProgramPipelineStatePrimitive等RHI对象的创建,我们也不是一定要按照上面的顺序来进行,只需要在被使用前创建好既可。

Driver中提供了startCapture来进行CPU和GPU的监测,我们一般也并不需要用到。而且在实现上也只有MetalDriver调用了Metal的API进行了实现。需要使用时,在需要进行监测的区间,调用startCapture/stopCapture即可。

异步渲染的实现

以上的调用,主要是用来说明Filament的使用流程,但是在实际的应用中,考虑到渲染效率和对渲染帧率的要求,我们往往需要进行异步渲染,把CPU和G的操作尽量并行起来。这时候,所有Filament的渲染指令的调用,都需要被发送到另外的线程中进行执行。我们不能再直接使用Driver对象去进行相关渲染指令的调用,而应该用CommandStream

在Filament中有一个DriverAPI.inc的头文件,采用宏定义的方式,定义了一系列的渲染API,宏的具体实现又由引入者来进行。
最终呈现的效果,就是Driver定义了一系列的Driver的API,各后端实现对其进行了继承,并实现。
CommandStream没有继承Driver,但是通过引入DriverAPI.inc,实现了一套和Driver的API一一对应,可以将Driver的各命令提交到队列中的方法。

DriverAPI.inc使用宏调用的方式定义了一系列的函数,宏的定义又并不是在DriverAPI.inc中,几个宏DECL_DRIVER_APIDECL_DRIVER_API_SYNCHRONOUS以及DECL_DRIVER_API_RETURN都是留给引用者定义,并在文件后undef这三个宏避免污染。每次引用DriverAPI.inc前,必须定义这三个宏,否则会编译报错。
Vulkan/Metal/OpenGL等各后端对于DECL_DRIVER_API的定义并没什么差别,就是直接声明对应名称的函数。只有NoopDriver,在定义DECL_DRIVER_API_RETURN进行了空实现。

CommandStream/Dispatcher也都引入了DriverAPI.inc。
Dispatcher中DECL_DRIVER_API的定义就是一个function,那么引入DriverAPI.inc实际上,就是定义了一堆的Function,这么做主要是为了帮助CommandStream实现DriverAPI.inc中的方法时,来进行Command的构建。
CommandStream中的DECL_DRIVER_API的定义,是根据方法名(methodName),利用Dispatcher,构建出一个调用Driver.methodName的Command。这种实现方式虽然阅读起来稍微麻烦一点,但是需要进行Driver函数的扩展会可以减少很多工作。

CommandStream中需要的Command是通过模板的方式进行定义的,参考CommandType和Command模板,由Command自己存储所有指令执行时所需要的参数信息。CommandStream中有一个CircularBuffer,用来存储所有的Command,Cammand一般都是在CommandStream调用DriverAPI.inc声明的相关API时进行创建。创建过程是先根据Command对象需要的大小来申请内存,然后在这块内存上构建(也可以说是初始化)Command对象。

在使用上,CommandStreamDriver具有基本一致的函数,利用Filament的backend去实现异步渲染时,相对同步渲染,只需要以下几步:

  1. 创建CommandBufferQueue,然后利用Driver实例和CommandBufferQueue中的CircularBuffer去构造一个CommandStream
  2. 创建一个渲染线程,在渲染线程中循环等待渲染指令,然后执行渲染指令。在必要的时候进行退出。
  3. 在另外的线程里面像使用Driver一样,使用CommandStream去执行相关命令接口。
    当然,这是简化的说明,在实际使用中肯定还需要做一些额外的处理工作。Filament在使用其backend时,基于backend的api进行了进一步的封装,以简化调用。

渲染系统的实现

在filament中,实际上是由Renderer类承担了渲染系统的职责,其执行渲染工作的核心代码比较简单:

if (renderer->beginFrame(window->getSwapChain())) {for (filament::View* offscreenView: mOffscreenViews) {renderer->render(offscreenView);}for (auto const& view: window->mViews) {renderer->render(view->getView());}if (postRender) {postRender(mEngine, window->mViews[0]->getView(), mScene, renderer);}renderer->endFrame();
}

其主要使用流程为:

  1. beginFrame开启一帧渲染。beginFrame的时候,需要指定一个SwapChain,决定了最终渲染输出的位置。
  2. render执行一帧渲染。render执行一帧渲染的时候,不是直接就进行了渲染,而是构建FrameGraph,并进行compile,然后execute。
  3. endFrame结束一帧渲染

Renderer的每一次渲染,都是执行一次renderJob(这个函数比较复杂,理论上应该拆一下)。它进行的工作,主要就是构建一个FrameGraph,通过对其进行“编译”,再执行,以达到优化渲染的目的。FrameGraph的构建看起来非常复杂,主要进行的工作是将外部设置的信息,转换成FrameGraph中的ResourceNodePassNodeVirtualResource等列表,并在此过程中,构建DependencyGraph,以明确PassNodeResourceNode的依赖关系。

FrameGraph的“编译”(compile),主要做了以下事情:

  1. 遍历它所包含的所有的PassNode。对于每个PassNode, 会通过DependencyGraph,去获取它所依赖的资源,并把这些资源信息注册到PassNode当中。RendererTarget的数据的更新在资源注册后,通过调用PassNode.resolve进行。在资源信息注册到PassNode时,每个资源会记录并更新最早使用它的节点和最晚使用它的节点, 以便后续根据这个信息,来进行真实资源的创建和销毁。
  2. 而后,资源列表会被遍历,然后借助资源中记录的最早和最晚使用它的节点信息,来把资源直接挂载到相应节点的资源构造(Resource.devirtualize)和资源析构列表(Resource.destroy),这样就后续就可以方便合理的进行资源的按需创建和销毁。
  3. 遍历所有的资源节点,解析它们的用途(Resource.usage),后续资源进行真实资源创建时(Resource.devirtualize),需要用到。

FrameGraph的“执行”(execute),也是遍历它所包含的所有PassNode,针对每个PassNode进行一下工作:

  1. 资源准备,VirtualResource.devirtualize
  2. RenderPassNode执行
    • 根据RenderPassData列表,进行必要的RenderTarget的构造
    • mPassBase->execute,将指令进行部分解析,转换成RHI指令数据,加入到Engine下的CommandStream中,由CircularBuffer进行存储和管理。
    • 析构前面构造的RenderTarget
  3. 资源析构,VirtualResource.destroy
    需要注意的是,其中的资源构造和资源析构,并不一定是在一次循环里对同一个资源进行。而是根据需要,在资源不再被后续的PassNode使用时,才会被析构。

在异步模式下,filament的FEngine构建时候,会启动一个渲染线程,用来通过CommandStream对应的CommandBuffferQueue,来循环从CircularBuffer中获取指令列表,让真正的Driver来进行执行.执行完成后CircularBuffer中相应的指令空间就会被回收,用来接受CommandStream给的新指令。

ECS的实现

前面说到,Filament上层是一个ECS的设计。我们将其拆开来理解。引擎中的Entity结构非常简单,它在内存中实际上就是一个unit32_t,代表的Entity的索引,它本身并不持有Component的数据,相对来说非常简单,重点在于ComponentSystem

Filament中的Component更多的是一个概念,每类Component都有一个对应的Manager,Manager包含了:

  1. Component的数据结构
  2. 向Entity中增删此Component。实际上Component的数据实例,是由Manager持有和管理,并记录和Entity的关联。
  3. 查询某个Entity,是否具备Component。
  4. 更新Entity中对应Component的数据

Filament中的ComponentManager包括CameraManagerLightManagerRenderableManagerTransformManager。这些Manager的实现大同小异,都是依赖utils::SingleInstanceComponentManagerutils::EntityInstance这些模板类来做的实现。

以RenderableManager为例,其内部存在一个mManager成员,对象类型继承自utils::SingleInstanceComponentManager模板类,如下代码所示。

using Base = utils::SingleInstanceComponentManager<Box,                             // AABBuint8_t,                         // LAYERSMorphWeights,                    // MORPH_WEIGHTSuint8_t,                         // CHANNELSInstancesInfo,                   // INSTANCESVisibility,                      // VISIBILITYutils::Slice<FRenderPrimitive>,  // PRIMITIVESBones,                           // BONESFMorphTargetBuffer*              // MORPHTARGET_BUFFER
>;struct Sim : public Base {using Base::gc;using Base::swap;struct Proxy {// all of this gets inlinedUTILS_ALWAYS_INLINEProxy(Base& sim, utils::EntityInstanceBase::Type i) noexcept: aabb{ sim, i } { }union {// this specific usage of union is permitted. All fields are identicalField<AABB>                 aabb;Field<LAYERS>               layers;Field<MORPH_WEIGHTS>        morphWeights;Field<CHANNELS>             channels;Field<INSTANCES>            instances;Field<VISIBILITY>           visibility;Field<PRIMITIVES>           primitives;Field<BONES>                bones;Field<MORPHTARGET_BUFFER>   morphTargetBuffer;};};UTILS_ALWAYS_INLINE Proxy operator[](Instance i) noexcept {return { *this, i };}UTILS_ALWAYS_INLINE const Proxy operator[](Instance i) const noexcept {return { const_cast<Sim&>(*this), i };}
};Sim mManager;

RenderableManager对外的函数,最后基本上是对mManager的包装。mManager用一个StructureOfArrays的模板类对象实例mData,来管理着当前Manager对应类别的所有Component数据集。上面SingleInstanceComponentManager模板传入的参数,实际就构成了Component的数据结构。Sim定义了下标运算符,使mManager可以像数组一样通过索引取得Component的代理对象,访问Component数据。

但是,**在内存中实际存储时,并不是按照Component的数据结构来存储的,而是把相同的属性的数据放到了一起。**StructureOfArrays中,mArray是一个std::tuple,存储了所有属性数据的起始地址。以FRenderableManager中mManager的数据存储为例,图示如下:
请添加图片描述

在Filament的ECS中,S其实主要就一个,渲染系统Renderer,上面已对其渲染执行的过程进行了简单的分析。其构建FrameGraph的过程比较复杂,涉及到诸多信息的处理,另外还包含一些View的操作,后面有时间再对其构建过程进行解读。


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


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

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

相关文章

洛谷B3876—— [信息与未来 2015] 中间值

见&#xff1a;B3876 [信息与未来 2015] 中间值 - 洛谷 题目描述 给出一个正整数 n&#xff0c;生成长度为 n 的数列 a&#xff0c;其中 ai​i(1≤i≤n)。 若 n 为奇数&#xff0c;则输出 a 的中间数&#xff08;位于 a 正中位置的数&#xff09;&#xff1b;若 n 为偶数&am…

Java 后端基础 Maven

Maven 1.什么是Maven 2.Maven的作用 Maven核心 Maven概述 IDEA集成Maven 1.创建Maven项目 点击设置里的 Project Structure 将jdk和编译语言进行设置 随后点击apply点击ok 2.Maven坐标 3.导入Maven项目 将文件夹复制到当前项目的目录下 在这个目录下&#xff0c;在磁盘中…

qtcreater配置opencv

我配置opencv不管是按照网上的教程还是deep seek发现都有些问题&#xff0c;下面是我的配置方法以及实践成功的心得 电脑环境 windows平台qt6 下载 我这里直接提供官网下载地址&#xff1a;https://opencv.org/releases/ 我下载的是最新版&#xff0c;下载后是一个.exe文件…

单片机-STM32部分:15、直流电机与步进电机 PWM/IO

飞书文档https://x509p6c8to.feishu.cn/wiki/InUfwEeJNimqctkyW1mcImianLh 一、步进电机与直流电机&#xff1a; 1-1、什么是直流电机&#xff1f; 直流电机是最常见的电机类型。直流电动机通常只有两个引线&#xff0c;一个正极和一个负极。直流电机的转速控制主要依靠改变输…

「佰傲再生医学」携手企企通,解锁企业采购供应链数字化新体验

健康&#xff0c;是人类美好生活的基石。随着“健康中国2030”规划的深入推进&#xff0c;生物医药和再生医学等前沿技术快速崛起&#xff0c;已成为促进全民健康、提升生命质量的重要支撑&#xff0c;为健康事业注入了新的希望和动力。 一、佰傲再生医学&#xff0c;让每个人…

PyTorch Geometric(PyG):基于PyTorch的图神经网络(GNN)开发框架

PyTorch Geometric&#xff08;PyG&#xff09;&#xff1a;基于PyTorch的图神经网络&#xff08;GNN&#xff09;开发框架 一、PyG核心功能全景图 PyTorch Geometric&#xff08;PyG&#xff09;是基于PyTorch的图神经网络&#xff08;GNN&#xff09;开发框架&#xff0c;专…

亮相戛纳电影节、北京电影节的影星

​17日&#xff0c;由高圆圆、古天乐主演的《风林火山》剧组&#xff0c;在第78届戛纳影展上走红毯亮相&#xff0c;记者争相拍照&#xff0c;风光无限。 值得关注的是&#xff0c;导演麦浚龙以一身黑色晚礼服踏上红毯&#xff0c;微笑间显得踌躇滿志&#xff1b;古天乐则以白色…

Django框架的前端部分使用Ajax请求一

Ajax请求 目录 1.ajax请求使用 2.增加任务列表功能(只有查看和新增) 3.代码展示集合 这篇文章, 要开始讲关于ajax请求的内容了。这个和以前文章中写道的Vue框架里面的axios请求, 很相似。后端代码, 会有一些细节点, 跟前几节文章写的有些区别。 一、ajax请求使用 我们先…

IP地址代理公司:服务模式与行业应用探析

随着数据驱动型经济的快速发展和互联网应用的普及&#xff0c;IP地址代理服务逐渐成为支持多种网络业务的重要组成部分。近年来&#xff0c;提供代理IP服务的公司遍地开花&#xff0c;这一市场强调供给的技术深度和服务灵活性&#xff0c;而代理IP公司本身也逐步从单一的技术供…

C语言练手磨时间

167. 两数之和 II - 输入有序数组 给你一个下标从 1 开始的整数数组 numbers &#xff0c;该数组已按 非递减顺序排列 &#xff0c;请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] &#xff0c;则 1 <…

本地部署Firecrawl+Dify调用踩坑记录

最近自己研究Dify&#xff0c;使用到Firecrawl这个比较好用的工具。用Firecrawl官网的不知道为什么总是卡住得不到结果&#xff0c;于是我打算自己去本地部署一个。好家伙真给我人搞麻了&#xff0c;太多问题了。 我是在京东云上面租的一台服务器。 首先就是docker的安装&…

iOS SwiftUI的具体运用实例(SwiftUI库的运用)

最近接触到一个 SwiftUI的第三方框架&#xff0c;它非常的好用。以下是 具体运用实例&#xff0c;结合其核心功能与开发场景&#xff0c;分多个维度进行详细解析&#xff1a; 一、基础 UI 组件开发 登录界面 SwiftUI 的 VStack、TextField 和 Button 可快速构建用户登录表单。例…

【C++】模板上(泛型编程) —— 函数模板与类模板

文章目录 一、啥是泛型编程二、函数模板2.1、函数模板的概念2.2、函数模板的格式2.3、函数模板的原理2.4、函数模板的实例化2.4.1、隐式实例化&#xff1a;让编译器根据实参推演模板参数的实际类型2.4.2、显示实例化&#xff1a;在函数名后的<>中指定模板参数的实际类型 …

语音识别-2

目录 1.蓝牙优化 1.打开sco 2.外放时的蓝牙的不同版本适配 2.微软文本转语音优化 1.异步文本转语音 2.语音的个性化 上一篇关于语音识别, 虽然能用,但在系统适配,机器适配方面,速度,性能等还是有优化的地方.所以这篇是关于这些的. 1.蓝牙优化 A2DP:是一种单向的高品质音…

【springcloud学习(dalston.sr1)】服务消费者通过restTemplate来访问服务提供者(含源代码)(五)

该系列项目整体介绍及源代码请参照前面写的一篇文章​​​​​​【springcloud学习(dalston.sr1)】项目整体介绍&#xff08;含源代码&#xff09;&#xff08;一&#xff09; springcloud学习&#xff08;dalston.sr1&#xff09;系统文章汇总如下&#xff1a; 【springcloud…

小白学编程之——数据库如何性能优化

小白学编程之——数据库性能优化指南 数据库如同一个大型仓库&#xff0c;性能优化就是帮助仓库管理员&#xff08;数据库&#xff09;更高效地存取货物&#xff08;数据&#xff09;。本文将以通俗易懂的方式&#xff0c;带你避开常见误区&#xff0c;让数据库运行得更快更稳…

SQLMesh信号机制详解:如何精准控制模型评估时机

SQLMesh的信号机制为数据工程师提供了更精细的模型评估控制能力。本文深入解析信号机制的工作原理&#xff0c;通过简单和高级示例展示如何自定义信号&#xff0c;并提供实用的使用技巧和测试方法&#xff0c;帮助读者优化数据管道的调度效率。 一、为什么需要信号机制&#xf…

FreeSWITCH 简单图形化界面43 - 使用百度的unimrcp搞个智能话务台,用的在线的ASR和TTS

FreeSWITCH 简单图形化界面43 - 使用百度的unimrcp搞个智能话务台 0、一个fs的web配置界面预览1、安装unimrcp模块2、安装完成后&#xff0c;配置FreeSWITCH。2.1 有界面的配置2.1.1 mod_unimrcp模块配置2.1.2 mod_unimrcp客户端配置 2.2 无界面的配置 3、呼叫规则4、编写流程4…

【架构】RUP统一软件过程:企业级软件开发的全面指南

一、RUP概述 RUP(Rational Unified Process&#xff0c;统一软件过程)是由Rational Software公司(后被IBM收购)开发的一种迭代式软件开发过程框架。它结合了传统瀑布模型的系统性和敏捷方法的灵活性&#xff0c;为中大型软件项目提供了全面的开发方法论。 RUP不仅仅是一种过程…

DeepSeek赋能电商,智能客服机器人破解大型活动人力困境

1. DeepSeek 与电商客服结合的背景 1.1 电商行业客服需求特点 电商行业具有独特的客服需求特点&#xff0c;这些特点决定了智能客服机器人在该行业的必要性和重要性。 高并发性&#xff1a;电商平台的用户数量庞大&#xff0c;尤其是在促销活动期间&#xff0c;用户咨询量会…