[项目记录]基于AR技术的党史长廊智慧导游APP

0. 项目背景

大二时候立项的校级大学生创新项目,当时受到 Meta 吹嘘的元宇宙影响想到了这么一个项目,总结下来就是给基于 AR 技术给学校的党史长廊做一个导游的 APP 。

项目地址:https://github.com/fengwm64/ARGallerySmartGuide

(暂未开源,2025.09后开源)

APP

1. 项目结构

基于 Unity 平台,使用 AR Foundation 框架进行开发。项目分为 3 个模块,采用一个模块对应一个场景的方法进行开发。模块分别为 导览页、AR扫描页、AR导航页,对应项目中的 Assets/Scenes/ 下的:

  • Assets/Scenes/Home.unity 导览页
  • Assets/Scenes/ARScan.unity AR扫描页
  • Assets/Scenes/ARNav.unity AR导航页

20250325175506

2. UI设计

2.1 场景切换

项目有三个场景,通过UI中的TAB栏进行切换。

所谓TAB栏其实就是一个有三个按钮组成的容器。结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
程序UI界面  
└── xxx 页
├── xxx
└── ...
└── 底部TAB栏
├── 首页按钮
│ ├── Text (TMP)
│ └── MainImage
├── AR扫描按钮
│ ├── Text (TMP)
│ └── MainImage
└── AR导航按钮
├── MainImage
└── Text (TMP)

首先,在 底部TAB栏 挂载一个脚本 Assets/Scripts/UI/TabController.cs,脚本会检测用户点击了第几个按钮,然后加载对应的场景内容,完成场景切换。

其次,需要有一个实体记录当前位于的场景,在TAB栏高亮对应的按钮。建立脚本 Assets/Scripts/UI/TabManager.cs,然后建立一个空的游戏对象挂载此脚本,保存为预制体 Assets/Prefabs/TabManager.prefab,在 底部TAB栏TabController 中引用预制体。

2.2 TAB栏自适应

现在的手机都是全面屏设计,分辨率也是五花八门的,为了可以使得 TAB 栏与屏幕等宽并且位于屏幕最下方且不会遮挡全面屏控制的“小白条”,编写一个脚本 Assets/Scripts/UI/TabBarWidthAdjuster.cs 自动布局 TAB 栏到安全区域内,同样挂载到 底部TAB栏 对象上。

TAB栏自适应

3. 导览页设计

3.1 场景结构

导览页场景主要由 搜索栏、滚动区域两部分组成。其中滚动区域是一个无限滚动列表,用于展示各个景点。详细结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Home  
├── Camera
├── Directional Light
├── EventSystem
└── 程序UI界面
├── 首页
│ ├── 列表面板
│ │ ├── 搜索栏
│ │ │ ├── 搜索内容输入框
│ │ │ │ ├── Text Area
│ │ │ │ │ ├── Placeholder
│ │ │ │ │ └── Text
│ │ │ └── 搜索按钮
│ │ │ └── Image
│ │ ├── 滚动视图
│ │ │ ├── Viewport
│ │ │ │ ├── Content
│ │ │ │ │ ├── ChildItem
│ │ │ │ │ │ ├── Image
│ │ │ │ │ │ ├── TitleText
│ │ │ │ │ │ └── RowText
│ │ │ ├── Scrollbar Vertical
│ │ │ │ ├── Sliding Area
│ │ │ │ │ └── Handle
│ ├── 详细页面板(默认隐藏)
│ │ ├── 返回按钮
│ │ ├── 标题
│ │ ├── 详细介绍
│ │ ├── 场景图片
│ │ └── 评论区
│ │ └── Text (TMP)
├── 底部TAB栏
│ ├── 首页按钮
│ │ ├── Text (TMP)
│ │ └── MainImage
│ ├── AR扫描按钮
│ └── AR导航按钮

3.2 数据库设计

为了存储各个景点的详细信息,利用 SQLite 设计数据库如下:

  • Descriptions 表
字段名 数据类型 长度 非空 唯一 主键 自增 备注
DescriptionID INTEGER - 描述ID(主键,自增)
SceneID INTEGER - 关联的场景ID
DetailedInfo TEXT 1500 详细描述信息
  • Scenes 表
字段名 数据类型 长度 非空 唯一 主键 自增 备注
SceneID INTEGER - 场景ID(主键,自增)
SceneName TEXT 255 场景名称
ScenePhoto TEXT 255 场景照片URL
SceneThumbnail TEXT 255 场景缩略图URL
DescriptionID INTEGER - 关联的描述ID

数据库文件放置在 Assets/StreamingAssets/ScenesData.db 中,因为移动端Android和iOS都不允许直接访问 StreamingAssets 文件夹中的文件,故在 首页 组件挂载脚本 Assets/Scripts/Database/DbInit.cs,该脚本功能是将启动时候将 StreamingAssets 下的数据库文件拷贝到APP下的持久化访问路径。

数据库相关的动态链接文件为 libsqlite3.soMono.Data.Sqlite.dll,具体放置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Assets/Plugins
├── Android
│   ├── libs
│   │   ├── arm64-v8a
│   │   │   └── libsqlite3.so
│   │   ├── armeabi-v7a
│   │   │   └── libsqlite3.so
│   │   └── x86
│   │      └── libsqlite3.so
├── IOS
│   └── libsqlite3.so
├── x86
│   └── sqlite3.dll
│── x86_64
│   └── sqlite3.dll
└── SQLite
   ├── Mono.Data.Sqlite.dll
    ├── System.Data.dll
   └── sqlite3.dll

数据库增删改查的所有操作封装在脚本 Assets/Scripts/Database/DbAccess.cs 中。

3.3 无限滚动列表设计

无限滚动列表的原理其实就是,列表向上滑动时,当item超过显示区域的上边界时,把item移动到列表底部,重复使用item并更新item的ui显示,向下滑动同理,把超过显示区域底部的item复用到顶部。

无限滚动列表原理

这部分我是参考【游戏开发实战】Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码,在此感谢@林新发

项目中,涉及无限循环列表的组件和脚本如下:

  • 滚动视图 挂载 Assets/Scripts/List/RecyclingListViewItem.cs
  • ChildItem 挂载 Assets/Scripts/List/ListItem.cs
  • 主页 挂载 Assets/Scripts/List/CreateList.cs 启动时候查询数据库,然后创建滚动列表

3.4 搜索功能实现

设计时候考虑到列表中的景点会很多,设计了一个搜索栏,由输入框和输入按钮组成。

在搜索按钮上挂载脚本 Assets/Scripts/List/SearchList.cs

SearchList.cs 脚本中,字符串相似度计算使用的是 Levenshtein 距离算法。该算法通过计算两个字符串之间的编辑距离来确定它们的相似度。编辑距离是指将一个字符串转换为另一个字符串所需的最少编辑操作次数(插入、删除或替换一个字符)。

当搜索到结果后会自动滚动到与搜索关键词最相似的行进行黄色效果的高亮。

黄色高亮效果

4. AR扫描页设计

4.1 场景结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ARScan
├── AR Session
├── XR Origin
│ ├── EventSystem
│ └── Directional Light
├── 程序UI界面
│ ├── AR扫描页
│ │ ├── 标题
│ │ └── 引导动画
│ │ ├── RawImage
│ │ ├── 引导动画播放控制器
│ │ └── Text (TMP)
│ └── 底部TAB栏
│ ├── 主页按钮
│ ├── AR扫描按钮
│ └── AR导航按钮

4.2 引导动画

参考网络上的类似 APP 都有做引导的动画,我也做了一个。没有触发 AR 扫描效果时候显示引导动画,这里所谓动画其实一段视频,从谷歌 ARCore 的 Demo 里偷的。这部分的代码在 Assets/Scripts/Animation/PlayVideoOnStart.cs 中实现,其实就是做了个视频播放器,循环播放 Assets/Resources/video/hand_oem.webm 的引导的视频。

4.3 AR扫描效果管理器

一般来说直接都是直接将图片到ReferenceImageLibrary,然后直接在XR OriginARTrackedImageManager脚本上挂载AR效果的预制体,但是这样做只能挂载一个预制体,存在难以管理不同AR效果预制体的问题。为了更好地管理不同的AR效果预制体,我们可以通过脚本动态管理AR效果的生成和销毁。

创建脚本Assets/Scripts/ARScan/ImageTracker.cs统一管理AR扫描效果目标,挂载到XR Origin上,支持3种目标,分别为AR 3D模型、AR视频、AR文字介绍。

AR效果

4.4 AR扫描效果制作

4.4.1 收集识别图进行预处理

  • 收集:拿手机去长廊上面拍照,尽量在光照良好的情况下正对展墙上的识别图进行拍照
  • 抠图:去除背景,将识别图扣出来
  • 矫正:利用图像处理工具对畸形的图像进行透视矫正

这里我用的是手机上的夸克扫描王工具,一条龙做完,导出时候使用导出为图片不用充值会员。

4.4.2 扫描图导入Unity

图片导入到Assets/Resources/img/AR扫描识别图内,导入后点击图片在右边检查器窗口设置纹理类型Sprite(2D和UI),然后点击空白处等弹出窗口,然后点击保存

然后,添加图片到ReferenceImageLibrary,位置Assets/ARScanImgLib/ReferenceImageLibrary.asset。点击ReferenceImageLibrary.asset后在右边检查器窗口添加图片,注意Name唯一;勾选Specify SizePhysical Size设置X0.2Y会自动计算无需输入。

4.4.3 建立AR效果的预制体

AR效果都是在预制体里设计的,做好后放在Assets/Prefabs/AR扫描效果预制件文件夹下,保证在预制体中做的所有效果放在PanelParent/Panel下。

如一个3D模型的预制体结构如下:

1
2
3
4
5
第一辆汽车
└── PanelParent
└── Panel
└── 第一辆汽车
└── Obj3d66-1 195112-2
  • 第一辆汽车Transform位置(0,0,0)旋转(0,0,0)缩放(1,1,1)
  • PanelParentTransform位置(0,0,0)旋转(0,0,0)缩放(1,1,1)
  • PanelTransform位置(0,0,0)旋转(0,0,0)缩放(0.1,0.1,0.1)
  • Panel的子对象就是设计AR效果内容,设置位置(0,0,0),旋转与缩放按实际需要填写(一开始填000和111就可以,不行再调整)

⚠️注意:AR视频直接使用Assets/Prefabs/AR扫描效果预制件/视频播放预制件.prefab即可;AR文字不需要做预制体,代码自己生成。

4.4.4 挂载

ARScan场景左侧的层级面板中点击XR Origin,然后在右边的检查器面板展开Image Tracker脚本,然后在arTargets中加一个对象,然后填对应ReferenceImageLibrary图片的名字,挂载预制件,设置内容类型等等参数

  • 注意AR视频才需要设置videoPath,AR文字才需要设置introductionTexttextColortextMargin等参数

4.5 AR 3D模型交互

当扫描显示AR 3D模型效果后,用户自然会想到与模型进行一些交互,如旋转、放缩等。

创建一个脚本Assets/Scripts/ARInteraction/ARModelInteraction.cs,挂载预制体的AR 3D模型上,具体来说挂载在Panel的第一个子对象上,既可实现与模型的交互。

5. AR导航页设计

5.1 场景结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ARNav
├── XR Origin
├── AR Session
├── EventSystem
├── Directional Light
└── 程序UI界面
│ ├── AR导航页
│ │ ├── LineOptionsButton
│ │ ├── DebugOptionsButton
│ │ ├── 楼层切换按钮
│ │ ├── 导线类型切换按钮
│ │ ├── DebugOptionsPanel
│ │ ├── FloorOptionsPanel
│ │ ├── QrCodeScannerPanel
│ │ └── MiniMapRawImage
│ ├── 底部TAB栏
│ └── 二维码扫描
├── AR导航地图
│ ├── 一层地图
│ └── 二层地图
├── AR导航控制器
├── AR导航可视化指示器
│ ├── IndicatorSphere
│ ├── 小地图摄像机
│ ├── 线型导航路径可视化
│ └── 方向箭头导航路径可视化
└── 导航路线计算器

5.2 长廊地图建模

层级 面板中建立 AR导航地图 对象,然后建立导航网格 FirstFloor-NavigationArea,使用长廊的介绍的小地图图片制作一个 FloorCubeMaterial 材质作为底图,使用 cube 将可以行走的区域围出来。

长廊地图建模

5.3 导航目标管理

创建一个 AR导航控制器 对象,挂载 Assets/Scripts/Core/TargetHandler.cs,该脚本实现对AR导航目标的管理,AR导航的目标存储在一个JSON文件 Assets/Resources/TargetData.json中,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"TargetList": [
{
"Name": "主题雕塑:地球上的红飘带",
"FloorNumber": 0,
"Position": {
"x": 2.0,
"y": 0.00,
"z": 0.00
},
"Rotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0
}
}
]
}

在JSON中写明目标在Unity中的位置和旋转的信息(旋转一般为(0,0,0)),然后在 检查器 面板的 TargetHandler 中引用这个JSON文件。

其次,在 TargetHandler 中,Start 方法中,调用了 GenerateTargetItemsFillDropdownWithTargetItems 方法,分别用于生成目标项和填充下拉菜单。GenerateTargetItems 方法从数据源读取目标数据,并通过调用 CreateTargetFacade 方法创建目标点的可视化对象。CreateTargetFacade 方法实例化目标对象预制体,并根据目标数据设置其位置、旋转和属性。

GenerateTargetDataFromSource 方法从 JSON 文件解析目标数据,并返回目标数据集合。FillDropdownWithTargetItems 方法将所有目标点信息转换为下拉菜单选项数据,并清空下拉菜单后添加新的选项。

此外,类中还包含两个公共方法:SetSelectedTargetPositionWithDropdownGetCurrentTargetByTargetTextSetSelectedTargetPositionWithDropdown 方法根据下拉菜单选择设置导航目标,GetCurrentTargetByTargetText 方法根据目标名称查找目标对象。GetCurrentlySelectedTarget 方法用于获取指定索引的目标位置,如果选择值超出范围,则返回零向量。

5.4 导航路线可视化

导航路线可视化部分由 Assets/Scripts/Utilities/PathVisualisation/ 负责,主要为 PathLineVisualisation.cs 负责切换两种不同的导航路线可视化方法;PathArrowVisualisation.cs 负责实现箭头类型的可视化方法;PathLineVisualisation.cs 负责实现线段类型的可视化方法。

创建 AR导航可视化指示器 对象,分别对应两种可视化lineGameObject对象,挂载对应的脚本。

可视化部分PathArrowVisualisation逻辑为:
首先从 navigationController 获取当前计算的导航路径,然后调用 AddOffsetToPath 方法为路径点添加高度偏移。接着调用 SelectNextNavigationPoint 方法选择下一个导航点,并调用 AddArrowOffset 方法根据滑块值调整箭头的 Y 轴位置。最后,将箭头对象朝向下一个导航点。

PathLineVisualisation.cs部分的逻辑与上面类似,不再赘述。

箭头型可视化

线型可视化

5.5 小地图设计

同样在 AR导航可视化指示器 对象下创建子对象,IndicatorSphere标识用户目前处在的位置,小地图摄像机对象(从上俯拍地图)。

在UI区域添加一个 MiniMapRawImage 用于显示摄像机画面。

小地图设计

5.6 利用二维码实现重定位

5.6.1 介绍

创建一个组件 二维码扫描,挂载脚本 Assets/Scripts/Core/QrCodeRecenter.cs,然后在UI中设置对应激活的按钮。

QrCodeRecenter.cs用于在Unity中处理二维码扫描和场景重定位,实现了从相机帧中检测二维码并根据二维码内容重置AR会话的位置和旋转。

类中定义了一些序列化字段,这些字段可以在Unity Inspector中进行设置,包括 ARSessionXROriginARCameraManagerTargetHandlerGameObject 类型的 qrCodeScanningPanel。这些字段分别用于管理AR会话的生命周期、重定位AR会话的原点、获取相机帧、管理目标对象以及显示或隐藏二维码扫描面板。

类中还定义了一些私有字段,包括用于存储相机帧纹理的 Texture2D、用于创建二维码读取器实例的 IBarcodeReader 以及控制扫描启用状态的布尔变量 scanningEnabled

OnEnable 方法中,注册了相机帧事件,当组件启用时会调用 OnCameraFrameReceived 方法处理相机帧数据。相应地,在 OnDisable 方法中,注销了相机帧事件,以确保在组件禁用时不再处理相机帧数据。

OnCameraFrameReceived 方法是处理相机帧数据的核心部分。首先,它检查扫描是否启用,如果未启用则直接返回。然后尝试获取最新的CPU图像,如果获取失败也会返回。接下来,设置图像转换参数,包括输入矩形、输出尺寸、输出格式和转换方式。计算存储最终图像所需的字节数,并分配缓冲区来存储图像数据。提取图像数据后,将其转换为RGBA32格式并写入缓冲区,随后释放 XRCpuImage 以避免资源泄漏。将数据放入纹理中以便可视化,并在完成临时数据处理后释放缓冲区。最后,检测并解码纹理中的二维码,如果解码成功则调用 SetQrCodeRecenterTarget 方法设置重新定位目标,并调用 ToggleScanning 方法切换扫描状态。

SetQrCodeRecenterTarget 方法根据二维码内容设置场景重定位目标。它通过 targetHandler 查找对应目标,并重置AR会话的位置和旋转,将 sessionOrigin 的位置和旋转设置为目标对象的位置和旋转。

ChangeActiveFloor 方法用于切换到指定楼层,它调用 SetQrCodeRecenterTarget 方法并传入楼层入口标识符。

最后,ToggleScanning 方法用于切换二维码扫描状态,并根据扫描状态显示或隐藏扫描面板。

5.6.2 二维码制作

我使用的是草料二维码,二维码信息为文本类型,内容为对应的景点名字,如“主题雕塑:地球上的红飘带”。

示例二维码如下:

主题雕塑地球上的红飘带 红色文化演艺广场