目的
- 有个需求是需要根据现实生活中的2D图片来在游戏里生成3D物体,首先想到的是AR,故把相关的技术和流程走一遍,记录一下,以备参考。
- 本文对基于AR的虚幻引擎2D贴图生成3D物体方案进行实践、总结和记录。
- 本文基于 UE4.27.2,主要在 Android 平台测试,相关功能可能受引擎版本、AR SDK、目标平台影响,但即便在这些差异环境下,也都有一定的参考价值。
效果
2D图片转3D物体https://www.zhihu.com/video/1684575175486541824* 原始图片是彩色的,但是即便是打印出来的黑白图片,也能准确识别,响应速度在毫秒级(这个其实跟图片的复杂度有关,图片越复杂、特征越明显,越好识别)。
- 相机扫描现实场景中的图片后,在游戏里的图片实物位置显示对应的3D物体(这里用简单的模型代替,以示差异)。
- 界面右上角会显示识别出的图片和名字。
步骤
前置条件
- 本文假设你会新建 UE4 工程、正确配置 Android 开发环境、且能正常打出 Android 包。
- 这一步涉及 Android 环境的配置等,如有疑问,可参考:UE5 打包 Android 全图文流程附工具软件下载(虽然是基于 UE5,但是 UE4 基本适用)
启用插件
打开工程(或新建个空工程),在 Plugins 中启用 Google ARCore 。
资源准备
准备如下资源。
- BP_ARTest:一个基于 Actor 创建的 Blueprint ,里边挂了个 StaticMeshComponent ,用以根据追踪到的图片来显示对应的模型。
- Pawn_ARTest:一个基于 Pawn 创建的 Blueprint ,里边挂在了一个 Camera Component ,用以显示扫描相机内容。另外,AR追踪的大部分逻辑也在这里边写了(仅为了方便,不符合工业开发的规范)。
- GM_ARTest:我们用来做AR测试的游戏模式,一个基于 GameModeBase 创建的 Blueprint Class,其 Default Pawn Class 设置为 Pawn_ARTest 。
- L_ARTest:一个基于 Default Level 新建的 Level 地图。为了避免挡住现实场景,把里边的 Sky Sphere、Atmospheric Fog、Floor 删掉了。为了让模型有光照效果,保留了 Light Source 。另外,对应的 GameMode 改成了 GM_ARTest 。
- T_1 - T_5:五张用来识别追踪的贴图,建议用内容复杂的彩色贴图,格式(模式,Format)为 RGBA8 ,以免无法识别(可用 PS 查看和修改格式)。图片的 CompressionSettings 选 UserInterface2D (RGBA) ,TextureGroup 设置为 UI 。
- DA_T_1 - DA_T_5:五个 ARCandidateImage 资源,即”指向场景中要检测的图像并提供现实生活中物体大小的资源“,名字分别为 DA_T_1 、DA_T_2、...、DA_T_5 ,其中属性 CandidateTexture 依次从 T_1 - T_5 里选,FriendlyName 合理设置,这里直接用图片名,Width 和 Height 合理设置,这里设置为 100 。
- DA_ARTest_Session:AR 特定描述配置文件。CandidateImages 里把刚才的 DA_T_1 - DA_T_5 添加进去。MaxNumSimultaneousImagesTracked : 同时跟踪的最大图像数量。 按需设置,这里采用默认值 1。
- WBP_ARTest:一个 UserWidget ,用以显示当前追踪的图片信息,非必须。
逻辑编写
- BP_ARTest:新建一个名为 PicMeshes 的 Map,Key 是字符串,表示图片名字,Value StaticMesh ,表示对应的网格体。新建 SetMesh 函数,调用 StaticMesh 的 Set Static Mesh 方法,去根据识别的图片更新网格体。
- WBP_ARTest:新建 UpdateImageUI 函数,根据传入的图片,调用 Set Brush from Texture 修改显示的图片,根据传入的字符串,修改图片的名字文本。
- Pawn_ARTest
- 概览
- 我们在这里边写 AR 识别的主要逻辑。UI_ARTest 是 WBP_ARTest 类型的参数,主要用以显示当前识别的图片的信息。CurrentSpawnedActor 是 BP_ARTest 类型的参数,用以记录当前已生成的 Mesh Actor,以展示根据当前识别的图片来变化的物体。Camera 是 CameraComponent ,有相机才能看到东西。
- 说明
- Event BeginPlay 的 StartARSession 用以开启 AR 任务,打开相机。参数类型选 DA_ARTest_Session 。Control Screensaver ,控制屏幕是否自动进入休眠模式,非必须。顺便打印一个日志,免得屏幕上啥都没有不知道到底启动了没有。
- Event Tick 调用了几个自定义的函数(等下解释),依次“获取识别出的图片”(Get ARDetected Image)、“更新展示网格体”(Update Static Mesh)、“更新当前识别出的图片信息UI”(Update Image in Screen)。
- GetARDetectedImage 主要是调用 Get All AR Geometries By Class 去获取 ARTrackedImage 类型的 AR 几何体。获取到之后需要通过 Get Tracking State 判断一下状态,只有 Tracking 状态的几何体才是我们的目标(否则追踪过的也会返回)。
- UpdateStaticMesh 主要用于根据当前识别的图片更新用于展示的网格体。如果 CurrentSpawnedActor 不存在,生成一个 BP_ARTest 并设置为 CurrentSpawnedActor,否则(或者生成之后),调用 CurrentSpawnedActor 的 Set Mesh 去根据当前识别的图片更新网格体。最后调用 SetActorTransform 去设置 CurrentSpawnedActor 的 Transform(位置、旋转和缩放等),把它跟识别的图片的信息关联起来。注意它的缩放我们设置为 0.2 了,以免太大遮住了视野。
- UpdateImageInScreen 主要根据当前识别的图片去创建并更新 WBP_ARTest 上的图片信息,如图片和图片名字等。
打包
- 没什么特殊的,直接调用 UE 的原始的打包方式即可打包(UE4和UE5的打包入口略有差别)
- 也可以点击 Launch 里对应的手机,来直接在手机上启动游戏(前提是手机开了开发者模式,并且通过 USB 文件传输连接上了)。
测试
运行后(安装后首次运行)会弹出相机权限,允许之后,相机对着目标图片,会自动生成对应的模型(参见效果)。
答疑
- AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: ".../libarcore_sdk_c.so" is for EM_386 (3) instead of EM_ARM (40)
- 详细报错
09-07 17:45:27.979 1095 1095 D MI-SF : isSceneExist: module = 7 (Unknown), value = 0
09-07 17:45:27.980 26850 26850 E AndroidRuntime: FATAL EXCEPTION: main
09-07 17:45:27.980 26850 26850 E AndroidRuntime: Process: com.YourCompany.ARTest, PID: 26850
09-07 17:45:27.980 26850 26850 E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: "/data/app/~~S-UrOCyyz53B36pDtMp6KA==/com.YourCompany.ARTest-cvTh8jOKgSM9PbCH7q_MEQ==/lib/arm/libarcore_sdk_c.so" is for EM_386 (3) instead of EM_ARM (40)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at java.lang.Runtime.loadLibrary0(Runtime.java:998)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at java.lang.System.loadLibrary(System.java:1656)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at com.epicgames.ue4.GameActivity.<clinit>(GameActivity.java:6687)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at java.lang.Class.newInstance(Native Method)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:43)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.Instrumentation.newActivity(Instrumentation.java:1273)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3660)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3937)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2288)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:106)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:210)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.os.Looper.loop(Looper.java:299)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8293)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:556)
09-07 17:45:27.980 26850 26850 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1045)09-07 17:45:27.982 2795 2836 D HomeWallpaperRenderer: OFFSET x: 1.0 y: 0.0
09-07 17:45:27.982 2795 3357 W OpenGLRenderer: Surface doesn't have any previously queued frames, nothing to readback from
09-07 17:45:27.983 4003 4111 E OOMEventManager: oom event not support this pkg, dismiss this!
09-07 17:45:27.983 1963 3257 W MiuiFreeFormGesturePointerEventListener: setMiuiFreeFormTouchExcludeRegion mTouchExcludeRegion = SkRegion((0,0,1080,90)(0,2296,1080,2340))
09-07 17:45:27.985 2795 2836 D HomeWallpaperRenderer: OFFSET x: 1.0 y: 0.0
- 可能原因
- Android设备不支持 armv7 [aka armeabi-v7a]
- 解决方案
- 修改 Android 打包设置,取消 Support armv7 [aka armeabi-v7a],勾选 Support arm64 [aka arm64-v8a] 。
- java.lang.UnsatisfiedLinkError: dlopen failed: ".../libarcore_sdk_c.so" is 32-bit instead of 64-bit
- 详细报错
09-08 10:32:37.857 14597 14597 D AndroidRuntime: Shutting down VM
09-08 10:32:37.857 14597 14597 E AndroidRuntime: FATAL EXCEPTION: main
09-08 10:32:37.857 14597 14597 E AndroidRuntime: Process: com.YourCompany.ARTest, PID: 14597
09-08 10:32:37.857 14597 14597 E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: "/data/app/~~Bh3coASS6vm3uKzSO2cOJw==/com.YourCompany.ARTest-LCGsCTUB5P3mqup1HwsFIQ==/lib/arm64/libarcore_sdk_c.so" is 32-bit instead of 64-bit
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at java.lang.Runtime.loadLibrary0(Runtime.java:998)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at java.lang.System.loadLibrary(System.java:1656)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at com.epicgames.ue4.GameActivity.<clinit>(GameActivity.java:6687)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at java.lang.Class.newInstance(Native Method)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:43)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.Instrumentation.newActivity(Instrumentation.java:1273)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3660)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3937)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2288)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:106)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:210)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.os.Looper.loop(Looper.java:299)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8293)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:556)
09-08 10:32:37.857 14597 14597 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1045)
09-08 10:32:37.858 2795 2836 D HomeWallpaperRenderer: OFFSET x: 1.0 y: 0.0
09-08 10:32:37.859 4003 4111 E OOMEventManager: oom event not support this pkg, dismiss this!
09-08 10:32:37.860 4003 27583 W MQSEventManager: jeoccur
09-08 10:32:37.861 14600 14600 W android.browse: ClassLoaderContext parent mismatch. (PCL[] | PCL[];PCL[])
- 可能原因
- libarcore_sdk_c.so 估计是拷贝错了,需要 64 位的结果拷贝的 32 位。
- 解决方案
- 可以考虑用 Engine\Source\ThirdParty\GoogleARCore\lib\arm64-v8a 下的 so (如 libarcore_sdk_c.so、libarcore_sdk_jni.so) 替换 Engine\Source\ThirdParty\GoogleARCore\lib\x86 下的。
- 获取几何体失败或识别时间太长。
- 可能原因
- 图片特征不够明显
- 解决方案
- 用复杂点的图片,比如大点的彩色图片。
- PackagingResults: Error: Texture T_3 is not RGBA8 or BGRA8 and cannot be used as a tracking target.
- 可能原因
- 图片格式(模式)不对,必须为 RGBA8 或 BGRA8 。
- 解决方案
- 如果是黑白图片,可能自动就是 gray 的。可以考虑直接用彩色图片,可能默认就是 RGBA8 的。
- 用 PS 打开图片可以查看和修改图片格式
- Failed to get enough keypoints from target image.
- 可能原因
- 图片(里的像素点)太简单了,特征不够,不利于识别。
- 解决方案
- 图片搞复杂点,比如用彩色大图。
- 生成的 Mesh 看不到
- 可能原因
- 可能是太大了。
- 解决方案
- 把 Mesh 缩放搞小点试试
- 注意本示例中,生成的物体的缩放是基于目标的大小的(Get Local to World Transform),直接调整 Actor 资源的缩放不会生效(蓝图里覆盖了),需要在生成时设置缩放(见步骤)。
- 不显示现实场景
- 可能原因
- 现实场景被游戏场景挡住了
- 解决方案
- 把游戏场景中的天空盒、大气等删掉。
- 不过即便挡住了现实场景,但是还是能正常扫描和识别物体的,可以利用这个特性来做特殊需求。
- GetAllARGeometriesByClass 把当前没有关注但之前扫描过的物体也返回了
- 可能原因
- GetAllARGeometriesByClass 这个接口会把之前追踪过的以及当前正在追踪的都返回的。
- 解决方案
- 可对 GetAllARGeometriesByClass 的结果进一步过滤,使用 GetTrackingState 接口判断追踪状态。对于追踪过的,其值为 NotTracking ,对于正在追踪的,其值为 Tracking 。
- 如果原图为彩色,其黑白版本能否被追踪识别?
- 可以
链接
- 增强现实开发
- ARCore SDK for Unreal Engine
- 教程七:使用UE4(4.26)制作安卓AR的图像识别(合集版)
- UE4 GoogleARCore 图片识别生成模型
- AR Tracking State Workaround
- UE4移动平台AR开发快速预览
- ARグレイマンを歩かせてみよう!! UE4-ARCoreのTipsと、AR Shadowについて
- AR Image Tracking for UE4 / Unreal Engine 4
**声明:**本文来自公众号:GameDevLearning,转载请附上原文链接及本声明。