Zephus 发表于 2022-8-8 17:23

UE/C++编程方略(9)显示摄像头画面

相关代码仓库:https://gitee.com/xarray/unreal-cpp-examples
在继续本节的内容之前,我们需要先做一些预备的工作,首先是考虑如何通过第三方库来实现摄像头画面的加载。我们选择OpenCV这个久负盛名的图像处理库来完成我们的需求。它的下载地址为:
https://opencv.org/releases/
然后笔者必须要提及一个问题,目前看来应该是Unreal 5.0.X版本的一个BUG,即Windows下为自己的工程导入第三方的动态链接库(DLL)时,有可能造成编辑器在执行到75%的阶段时卡死。该问题在Github上有相关的讨论和解决方案链接,但是整个过程比较复杂,并非本文所能涵盖:
https://github.com/wongfei/ue4-mediapipe-plugin/issues/42
因此,我们选择暂时降低版本到Unreal 4.27,并在此编辑器的基础上实现本节内容的开发(后续章节如果有类似问题,则也会专门说明)。基本的工作流程和之前5.0.3版本并没有区别,如果后续UE5修正了这个问题(或者有更好的解决方案),那么相关的内容都可以直接平移到UE5编辑器中使用。
UE4中建立C++工程的过程与UE5类似,我们仍需要建立一个自己的Actor对象,命名它为WebcamUpdater,然后我们首先需要修改Build.cs文件的内容,以便将第三方库的头文件路径和.lib文件导入进来,供C++工程引用和链接。


首先我们需要加入一些额外的Unreal内部依赖(RHI和RenderCore),它们用于后续代码中动态纹理数据的更新,然后我们建立一个新的函数BuildWithOpenCV(),因为工程的编译脚本是使用C#脚本编写的,因此C++开发者可以很快地了解和学会使用。


在脚本函数中,我们需要设置头文件路径并添加到PublicIncludePaths中,然后将必要的.lib文件添加到PublicAdditionalLibraries中;如果还有宏定义信息,则可以添加到PublicDefinitions中。这与以往C++工程的依赖库设置方法基本一致。
之后我们需要手动将动态库,例如本文中的opencv_world460.dll文件拷贝到当前工程的Binaries/Win64目录中,否则Unreal编辑器在启动过程中,会因为找不到对应的动态库模块而自动退出。更多有关工程编译脚本的内容可以参考Unreal官方的文档:
https://docs.unrealengine.com/5.0/en-US/integrating-third-party-libraries-into-unreal-engine/
尝试编译一下当前的工程,看是否存在问题。然后我们可以开始具体C++代码的编写工作了,首先是WebcamUpdater.h文件中,加入必要的OpenCV头文件:
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
我们这一次将不会直接创建一个场景中可见的对象,我们选择将WebcamUpdater做成一个Unreal编辑器中可以继续使用的Actor工具,也就是说,它本身会输出摄像头画面的结果数据(保存成UTexture2D纹理的形式),然后开发者自己或者其它同事可以在Unreal蓝图中将它与其它对象建立关联,将动态纹理设置给场景中的物体材质。因此,我们需要暴露一些变量,供蓝图中进行调用。


这几个变量的可读性,对应的分类名称,以及具体的含义都列举在头文件当中,并通过UPROPERTY宏进行了设置。它们分别对应了相机设备的ID,是否开启,视频的尺寸,以及当前视频纹理对象。如果后续我们在蓝图编辑器中右键点击并输入分类名Webcam,则会看到这几个变量均可以在蓝图中被载入和使用。


我们在头文件中还定义了一些成员函数和变量,这些变量不需要显示到蓝图当中,它们是摄像头更新时内部使用的。包括更新RHI(即Unreal的渲染层接口)纹理的范围定义_region,存储图像数据的数组_data,以及OpenCV的视频捕捉器对象和当前帧矩阵。


在BeginPlay()函数中,我们首先创建OpenCV捕捉器,获取第一帧的图像数据,然后创建UTexture2D纹理,并且第一次更新纹理的尺寸和内容。


创建纹理的过程在上一节中已经进行了介绍,而UpdateTextureData()函数则负责具体的底层纹理数据填充和更新。我们稍后再介绍这个函数的内容,首先看Tick()函数中需要完成什么工作:


实际上依然是读取每一帧的图像数据,然后调用UpdateTextureData()函数来执行数据的更新。我们首先看这个函数实现内容的第一部分,它仅仅是将OpenCV的BGR图像逐像素地传递给数据_data,以确保最终传递给Unreal纹理的数据符合内存排列需求。


第二部分是定义要传递给RHI层面的纹理数据,我们用一个自定义的结构体来描述所有相关的参数,并且将具体值传递给结构体的变量。


最后,通过Unreal内部的渲染指令执行2D纹理数据的更新。注意这里只考虑了不设置Mipmaps的情况,并且我们始终认为纹理数据需要全部更新,不考虑多次分区域更新的情形。


现在我们可以编译Unreal工程代码并启动编辑器了。目前我们只能在内容菜单的C++类中发现自己之前定义的WebcamUpdater对象,并且将它手动拖曳添加到当前场景中。如果现在运行该场景的话,也许可以看到自己的摄像头已经进入工作状态(例如通过笔记本电脑的摄像头指示灯),但是除此之外什么也没有发生。这是因为WebcamUpdater目前并没有任何可显示的内容,系统也不知道如何将WebcamUpdater中的动态纹理数据传递给哪个对象。为此,我们需要首先建立一个自己的材质对象,这一过程可以参考前面的内容,这一次我们选择建立一个无光照的材质,命名为WebcamMtl,并且添加一个新的2D纹理参数对象,命名为Webcam,并设置到自发光颜色中。


我们选择将一个Plane对象添加到WebcamUpdater的子组件中(当然也可以放在世界中的其它位置上)。这样我们手里就有了一个可以选择的Static Mesh,稍后只需要在蓝图中找到这个对象,然后将之前在C++头文件中定义的VideoTexture设置给它的纹理参数即可。


打开WebcamUpdater的蓝图编辑器,建立一个内部的动态材质实例对象DynamicMtl。在系统开始执行的时候,创建一个新的动态材质实例并设置给DynamicMtl,该动态材质源自于之前我们所创建的WebcamMtl。


然后我们设法找到刚才创建的Plane对象,例如下图中我们从当前Actor的根节点组件直接获取子对象,并转换到Static Mesh,然后将刚才赋值的DynamicMtl对象设置为它的新材质。这些工作都是在场景开始运行时即完成的,它们只会执行一次。


而在Tick事件阶段,我们需要获取刚才定义的DynamicMtl对象,以及本文开始时定义的VideoTexture对象,然后设置材质内的纹理参数。因为DynamicMtl是源自于WebcamMtl材质类型,因此它必然有一个纹理参数名为Webcam,我们直接将这个纹理参数的内容设置为当前的VideoTexture结果值即可。




最终运行的结果如上图所示,通过这个简单的代码编写和场景逻辑实现过程,我们已经实现了OpenCV这种相对复杂的第三方库合并到Unreal引擎中使用,并且实现了动态纹理图像的生成、更新和渲染显示。那么在下一个小节中,我们会考虑用OpenCV库来做更多的一些事情。
页: [1]
查看完整版本: UE/C++编程方略(9)显示摄像头画面