Unity AR室内导航Demo:AR Foundation平面识别与NavMesh路径渲染

发布时间:2026/10/18 14:33:18
Unity AR室内导航Demo:AR Foundation平面识别与NavMesh路径渲染 简介这是一款基于Unity3D与Vuforia SDK开发的AR室内导航Demo源码包面向Unity开发者、AR/VR方向学习者及室内定位技术爱好者。项目将增强现实与LBS位置服务相结合通过图像识别目标作为锚点在真实环境上叠加虚拟路径指引适用于商场、展馆、园区等室内场景的原型验证与教学实践。压缩包共856个文件整体约62.46MB内容涵盖60个C#脚本、18个Prefab预制体、16个Asset配置、材质着色器、纹理模型资源以及Android/iOS原生工程相关文件目录结构清晰可作为二次开发基础。资源兼顾Vuforia识别逻辑、导航算法、UI交互与跨平台工程配置便于读者分析场景搭建、目标识别、路径计算等实现细节。已有5716人学习或下载适合希望快速上手AR室内导航项目、拓展增强现实应用能力的开发者参考学习。1. 先看这项目解决什么问题拿着手机在商场里找一家店GPS信号一进大厅就跳水定位点可能在隔壁楼层乱跳。这个Demo的思路很直接既然卫星信号靠不住就用摄像头平面识别把手机固定到真实空间里再把导航路径直接画在地板上让你跟着地上的箭头拐弯。它是一个基于Unity3d AR LBS的室内导航示例工程核心是AR Foundation平面识别 虚拟导航网格 路径渲染。如果你想快速跑通AR室内导航或者正在做展厅、商场、地下车库的导视原型这个工程值得下载下来跑一遍。它不需要服务端不依赖昂贵的定位硬件只用一台支持ARCore的安卓机就能看到地板上的箭头。下了工程我第一件事没急着跑是先看目录结构。搞清每一个脚本挂在哪、数据文件在哪后面调参才不会像瞎猫碰死耗子。下面从设计拆到实现再落到“怎么跑、怎么改”。2. 整体架构与技术选型AR Foundation LBS 定位融合思路2.1 为什么选 AR Foundation 而不是直接调 ARCore/ARKitUnity3d 里写AR有两种路线直接引用ARCore或ARKit的SDK或者用AR Foundation这套统一接口。这个Demo选了后者理由很实际AR Foundation把会话管理、平面检测、Raycast全部封装成通用组件切换底层平台时不用重写业务代码。你在Build Settings里切到Android它会自动走ARCore的底层实现将来丢到iOS上换成ARKit的底层实现核心脚本几乎不动。对Demo来说这套抽象能省不少调试时间至少不用维护两套SDK的差异。这里说的LBS不是狭义的GPS定位而是整个位置服务链路——从真实地理坐标到室内导航网格的映射。Demo把LBS数据拆成两层一层是外部真实世界的位置经纬度或点位编号另一层是Unity世界坐标下的导航点。两者通过一个可配置的JSON文件关联这样换场地时不需要改代码只改数据和导航网格二次接手的开发就能把精力放在业务层上。我拿到工程后最先看的是Assets目录里有没有可以运行的场景结果发现它把AR生命周期、寻路、渲染分得很清楚。ARAnchorController只管“把虚拟根物体锚到现实空间”NavigationService只管“算路径”PathRenderer只管“画路径”。这种单一职责的写法让我在Debug时能直接定位到出问题的脚本而不是在一个几百行的MonoBehaviour里翻找逻辑。2.2 工程目录结构与模块划分整个工程的结构很标准适合拿来做老项目重构的参照。我列一下关键路径Assets/ Scenes/ ARNavigationDemo.unity Scripts/ AR/ARAnchorController.cs LBS/LbsDataProvider.cs Nav/NavigationService.cs Render/PathRenderer.cs Utils/CoordinateUtils.cs Prefabs/ ARSession.prefab NavMeshRoot.prefab ArrowIndicator.prefab Resources/ Data/ lbs_poi.json Materials/ NavPathMaterial.matARAnchorController负责AR会话里的锚点绑定是整个空间对齐的入口。LbsDataProvider负责解析JSON里的信息点把地理坐标转成Unity坐标。NavigationService负责计算从起点到目标点的路径PathRenderer负责把路径画到地板上。Utils里的坐标换算函数是独立工具不依赖AR运行时后面写单测或者接外部数据都能复用。Prefabs里的NavMeshRoot尤其重要它内部放了一个已经烘焙好的NavMesh测试房间模型。运行时ARAnchorController把它实例化到识别平面上导航计算全部发生在它内部。这里有个设计亮点现实世界的地板和虚拟世界的NavMesh共用同一个锚点只要初始对齐没问题后面就不会出现路径悬空的尴尬。2.3 LBS 数据怎么组织从经纬度到虚拟坐标Resources/Data/lbs_poi.json 一开始会让人以为要写一堆真实GPS数据实际上它是为室内导航缩小的坐标系。我摘了一段{ building: { name: demo_floor_1, baseLat: 30.123456, baseLon: 120.123456, scale: 100, floorHeight: 3.0 }, pois: [ { id: 101, floor: 1, geo: { lat: 30.123471, lon: 120.123489 }, virtual: { x: -4.2, z: 2.8 } }, { id: 205, floor: 1, geo: { lat: 30.123520, lon: 120.123590 }, virtual: { x: 3.1, z: -1.6 } } ] }building里的baseLat和baseLon是整个导航区域的基准点scale是真实世界米数与Unity单位之间的缩放系数示例工程里取100也就是说真实世界1米对应0.01个Unity单位适合室内精度。geo字段保留经纬度用于以后接外部LBS服务virtual字段才是直接在Unity场景里定位用的坐标。用virtual坐标的好处是离线也能跑通不依赖GPS和基站。真正接手的时候只需要用卷尺量出几个参考点把经纬度和virtual坐标一一对应再跑一遍校准整个导航区域就贴合现实了。如果点位特别多这个JSON文件还可以拆成按楼层分组的多个文件LbsDataProvider加载时按floor字段过滤后面扩展多楼层会顺手很多。3. 核心模块拆解从命中平面到画出路径的完整链路3.1 先让 ARAnchorController 把导航根节点锚定到现实空间室内导航最怕打开应用后一片空白。ARAnchorController要做的事很明确等待用户点击屏幕用ARRaycastManager在现实平面上射一根线命中后把NavMeshRoot这个虚拟导航网格挂到现实空间的锚点上。这样地面上多出来的“虚拟地板”才能跟着手机视角一起动路径线也就有了一个稳定的父级坐标。using System.Collections.Generic; using UnityEngine; using UnityEngine.XR.ARFoundation; using UnityEngine.XR.ARSubsystems; public class ARAnchorController : MonoBehaviour { [Header(AR Dependencies)] public ARSessionOrigin sessionOrigin; public ARPlaneManager planeManager; public ARRaycastManager raycastManager; [Header(UI)] public GameObject navMeshRootPrefab; private GameObject activeNavRoot; private Pose anchorPose; void Update() { if (Input.touchCount 0) return; Touch touch Input.GetTouch(0); if (touch.phase ! TouchPhase.Began) return; var hits new ListARRaycastHit(); if (raycastManager.Raycast(touch.position, hits, TrackableType.PlaneWithinPolygon)) { anchorPose hits[0].pose; // 用识别到的平面中心来对齐避免点击点偏离地板导致整体旋转 var plane planeManager.GetPlane(hits[0].trackableId); if (plane ! null) { anchorPose.position plane.centerPose.position; anchorPose.rotation plane.centerPose.rotation; } CreateOrUpdateNavRoot(anchorPose); } } private void CreateOrUpdateNavRoot(Pose pose) { if (activeNavRoot null) { activeNavRoot Instantiate(navMeshRootPrefab, pose.position, pose.rotation, sessionOrigin.transform); } else { activeNavRoot.transform.SetPositionAndRotation( pose.position, pose.rotation); } } }这段代码里最需要注意的是Raycast返回的hits[0].trackableId和ARPlaneManager.GetPlane的配合。Raycast命中点通常是人眼点击的位置这个点可能落在平面边缘直接拿它当锚点会让NavMeshRoot歪斜。我一般会立刻用GetPlane把这个hit对应的平面找出来再把锚点修正为平面的centerPose这样activeNavRoot才能水平贴地。参数说明TrackableType.PlaneWithinPolygon表示只接受平面多边形内命中比PlaneBoundary更严格命中距离由AR内部按平面大小和相机距离过滤不需要额外设置。如果你的场景出现“点击没反应”先看两件事ARSessionOrigin是不是挂在了Camera父级上以及Main Camera标签是否被其他相机抢走。3.2 LBS坐标换算把 JSON 里的虚拟坐标变成导航点NavMeshRoot锚定后要把JSON里的POI转换成导航网格上的目标点。CoordinateUtils负责这段换算它不依赖AR库可以单独跑单元测试using UnityEngine; public static class CoordinateUtils { // 每度纬度约对应110540米精度在室内范围内足够 private const float MetersPerLatDegree 110540f; // 每度经度需要乘cos(lat)这里取近似常数 private const float MetersPerLonDegree 111319.9f; public static Vector3 GeoToLocal(float lat, float lon, float baseLat, float baseLon, float scale) { float x (lon - baseLon) * MetersPerLonDegree * Mathf.Cos(lat * Mathf.Deg2Rad) / scale; float z (lat - baseLat) * MetersPerLatDegree / scale; return new Vector3(x, 0f, z); } public static Vector3 AddFloorHeight(Vector3 localPoint, int floor, float floorHeight) { float y floor 0 ? floor * floorHeight : 0f; return new Vector3(localPoint.x, y, localPoint.z); } }这里没有必要用完整的UTM投影室内范围最多几百米直接用经纬度差乘以米/度系数就能满足0.5米的定位精度。scale是Unity单位与真实米的比值如果你想在现实场景中让1米等于1个Unity单位把scale设成1就行想让模型小一点、方便观察就按100或者1000来缩放。调用时先拿到building里的baseLat/baseLon再用POI的geo字段算local坐标。如果POI是虚拟坐标那GeoToLocal根本不用调直接把virtual字段填进寻路。将来接入室外GPS时这套换算还能当“最后一公里”的入口把GPS坐标换算成楼宇坐标系再交给室内模块做引导。3.3 NavigationService从当前点到目标点算一条可走的路导航计算用的是Unity自带的NavMesh。NavigationService挂在NavMeshRoot上它负责采样位置、计算路径并把路径点交给渲染模块。这样做的好处是寻路算法和画面表现彻底解耦后面想换成A*或者Dijkstra都不需要碰渲染代码。using UnityEngine; using UnityEngine.AI; public class NavigationService : MonoBehaviour { [Header(Path Settings)] public float sampleRadius 1.0f; private NavMeshPath currentPath new NavMeshPath(); private PathRenderer pathRenderer; private void Awake() { pathRenderer GetComponentPathRenderer(); } public bool BuildPath(Vector3 startPoint, Vector3 endPoint) { NavMeshHit startHit, endHit; // 先在地面网格上采样防止起点跑到墙体或悬空 if (!NavMesh.SamplePosition(startPoint, out startHit, sampleRadius, NavMesh.AllAreas)) { Debug.LogError(起点不在可导航区域内); return false; } if (!NavMesh.SamplePosition(endPoint, out endHit, sampleRadius, NavMesh.AllAreas)) { Debug.LogError(终点不在可导航区域内); return false; } if (NavMesh.CalculatePath(startHit.position, endHit.position, NavMesh.AllAreas, currentPath)) { pathRenderer.DrawPath(currentPath.corners); return true; } Debug.LogWarning(导航失败检查NavMesh是否烘焙完整); return false; } }sampleRadius默认取1.0米太大容易把墙体内部的点也采样进来路径会穿墙太小会导致起点稍微偏移一点就找不到落脚点所以宁可以当前点为中心多试几次。NavMesh.SamplePosition是Unity提供的“把任意点吸附到最近NavMesh面”的函数它能保证算出来的路径起点和终点都在可走区域上。计算成功后会拿到currentPath.corners这是一组从起点到终点的折线拐点。pathRenderer.DrawPath拿到拐点后自己画线NavigationService不关心渲染细节。这样如果某个角落路径计算失败日志会直接打出“导航失败检查NavMesh是否烘焙完整”而不是让你去翻Mesh代码。3.4 PathRenderer把折线画成地板上的箭头PathRenderer的代码不长但有几个坑值得单独说using System.Collections.Generic; using UnityEngine; public class PathRenderer : MonoBehaviour { public LineRenderer lineRenderer; public GameObject arrowPrefab; [Range(0.05f, 0.3f)] public float lineWidth 0.12f; private readonly ListGameObject spawnedArrows new ListGameObject(); public void DrawPath(Vector3[] corners) { ClearArrows(); if (lineRenderer null) { Debug.LogError(没有配置 LineRenderer); return; } lineRenderer.positionCount corners.Length; lineRenderer.SetPositions(corners); for (int i 0; i corners.Length - 1; i) { Vector3 midPoint (corners[i] corners[i 1]) * 0.5f; Vector3 direction corners[i 1] - corners[i]; direction.y 0f; Quaternion rot Quaternion.LookRotation(direction); GameObject arrow Instantiate(arrowPrefab, midPoint, rot, transform); spawnedArrows.Add(arrow); } } private void ClearArrows() { foreach (var arrow in spawnedArrows) { if (arrow ! null) Destroy(arrow); } spawnedArrows.Clear(); } }LineRenderer渲染路径时记得把Alignment设为View这样线条始终面向相机而不是固定朝世界坐标Z轴。如果不设置AR相机一偏线就会变成一条看不见的细线。箭头方向用Quaternion.LookRotation计算direction.y要归零否则在坡道或者楼梯上箭头会竖立起来看着像悬浮物。每次导航重新开始时要先把上一轮的箭头清掉不然路径一直在更新AR画面上箭头叠得越来越多。ClearArrows里用Destroy释放GameObject如果箭头数量少没问题数量大了可以考虑用对象池。不过Demo阶段够用对象池属于性能优化不用一上来就上。4. 从克隆到跑通环境配置、场景搭建与参数调试4.1 Unity 版本与 Android 环境配置这个Demo基于Unity 2021.3 LTS和AR Foundation 4.2。用更高的版本打开一般能直接迁移但建议别用Unity 6 betaAR Foundation打包规则有些变化真机上容易出现黑屏。新建项目时记得勾选Android Build Support随后进入Package Manager添加这几个包AR Foundation、ARCore XR Plugin、XR Plugin Management。Android端还需要在Player Settings里配置权限Common Settings里勾选Camera和Internet权限XR Plug-in Management里把ARCore勾上。很多人会漏掉Internet权限后面一旦对接网络LBS定位就会莫名失败。Minimum API Level建议设到Android 7.0以上否则ARCore服务可能不兼容。Unity编辑器里运行AR时会提示不支持模拟器这是正常的。我一般先在编辑器里跑场景流程确认UI交互没问题再上真机测AR。真机上需要提前开启USB调试装好ARCore服务如果设备不支持ARCore应用会黑屏或者提示no tracking遇到这种情况别怀疑代码先换一台支持列表里的测试机。4.2 从空场景搭出可运行的 ARNavigationDemo打开Scenes/ARNavigationDemo.unity以后如果不想直接用也可以从空场景自己搭建步骤比较固定创建AR Session和AR Session Origin把Main Camera拖到AR Session Origin下挂上ARCameraManager和ARCameraBackground。创建EventSystem保证UI的Image和Button能接收点击事件。在AR Session Origin下创建一个空物体挂上NavigationService和PathRenderer。把NavMeshRoot.prefab拖给ARAnchorController的navMeshRootPrefab字段。确认场景里有一个已烘焙NavMesh的虚拟房间模型放在NavMeshRoot下。运行后屏幕会出现可识别的平面网格。点击地面任意位置先让NavMeshRoot对齐到该平面再点击导航UI里的目标POI就会调用BuildPath在地板上画出路径。这个过程不需要物理键盘触摸就能完成和最终用户的体验保持一致。实际操作时我建议站在光照充足、地面纹理清晰的地方把手机倾斜45度俯拍地面左右缓慢移动5秒左右再点击屏幕。整个识别期间不要猛烈晃动手机尤其是点击完成后的前两秒ARCore需要稳定特征点来维持锚点。这一步听起来像玄学但确实是平面检测成功率最高的姿势。4.3 关键参数与日志输出调试AR项目不能只靠肉眼关键参数我先列成表参数位置默认值作用sampleRadiusNavigationService1.0NavMesh采样半径越大越容易吸附到最近面lineWidthPathRenderer0.12路径线宽单位是Unity世界单位arrowPrefabPathRendererArrowIndicator箭头预制体放在每一个转折点floorHeightLbsDataProvider3.0默认层高多楼层时Y轴抬升scalelbs_poi.json100米转Unity单位的缩放系数日志是定位问题最快的抓手。ARAnchorController和NavigationService都已经打了关键日志点击命中后打印锚点位置寻路成功返回时打印路径点数。跑真机前我会在Logcat里过滤Unity标签然后额外加一句Debug.Log($Anchor Pose: {anchorPose.position} rotation: {anchorPose.rotation});这行日志能在平面检测失败时告诉我到底是“没识别到平面”还是“识别了但锚点绑定失败”。如果发现pose.position全是初始值0那就要回头检查ARSessionOrigin的Transform有没有被意外重置。真机上打印一次锚点坐标再用卷尺量一下实际位置基本就能判断坐标换算准不准。5. 避坑记录室内AR导航最常踩的五个坑5.1 平面检测一直不触发画面里没有网格现象真机上打开应用后屏幕中央一直显示“正在识别平面”三分钟过去依然没有网格出现。原因往往不是代码逻辑而是三个方向的问题相机权限没开AR Session根本拿不到图像环境太暗或地面纹理太少ARCore找不到特征点Main Camera标签被场景里其他相机抢走导致AR相机没有被正确识别。解决先在Player Settings里确认Camera权限已勾选再把场景里的主动相机名字改成Main Camera并打上MainCamera标签。最后站到有纹理的地砖前缓慢移动手机别站在纯白墙壁前。如果还不行Logcat里搜“ARCore”看有没有明显的初始化失败记录。5.2 路径整体偏移锚点贴到了天花板上现象点击地面后NavMeshRoot出现在空中半米路径线悬空箭头和地面完全不贴合。原因我在ARAnchorController最初版里直接拿hits[0].pose当作锚点屏幕点击处的射线会命中倾斜平面或远处地面相机视角稍微倾斜锚点位置就被抬高了。解决改成先拿trackableId找到对应ARPlane再把锚点换成plane.centerPose.position并同步旋转。改完以后即使从侧面点击地面路径也会水平贴地。我当时改完发现之前白调整半天的模型缩放其实没有意义问题就出在锚点没有用平面中心。5.3 路径从墙角或柱子内部穿过去现象路径明明计算成功但折线明显切过墙角或柱子内部看起来像直接从障碍物里穿过去了。原因烘焙NavMesh时墙体下方没有完全被导航网格覆盖或者墙体模型没有加NavMeshObstacle且没有勾选Carve。这种情况下NavMesh认为障碍物占用的区域依然可以走路径自然就穿墙了。解决在编辑器中给墙和柱子模型添加NavMeshObstacle并勾选Carve把Project Settings里NavMesh的Voxel Size调到0.1米左右重新烘焙。同时把NavigationService的sampleRadius调小到0.5-1.0米避免采样点吸附到障碍物内部。改完以后墙角切线的现象基本消失。5.4 打包后真机上一直黑屏编辑器里却正常现象编辑器里通过AR Remote跑得好好的打包到安卓机上直接黑屏只显示Unity默认启动画面。原因最常见的是ARCore XR Plugin没有打进Android构建或者是权限声明不完整。还有一次我遇到Minimum API Level选成了Android 6.0ARCore服务直接拒绝初始化黑屏自然就发生了。解决打开XR Plug-in Management确认Android列表下的ARCore勾选是开启的Player Settings里把Minimum API Level提到Android 7.0以上Camera和Internet权限都勾上。如果还黑屏利用Logcat过滤ARCore相关内容搜“unsupported”或“initialize”关键字判断是设备不支持还是初始化顺序问题。5.5 从强光走进室内AR画面立刻泛白闪烁现象拿着手机从窗边走到室内阴影区画面瞬间过曝变白然后恢复反复闪烁路径线也跟着忽明忽暗。原因AR背景材质默认会参与相机的自适应曝光而AR相机在运动中频繁调整亮度所以画面会出现短暂过曝或欠曝特别是光照突变明显的时候。解决把ARCameraBackground使用的材质里的Exposure Adjustment参数关掉或者禁用ARCameraManager的Adaptive Exposure开关。还有一个办法是把AR相机挂上一个后处理组件把曝光范围限制在一个固定值。改完后画面不再跳变路径线也就保持稳定可见了。6. 二次开发把 Demo 扩展成可切换楼层的导航框架6.1 把 LBS 数据源从本地 JSON 换成网络接口实际落地时POI和楼层图不可能一直随安装包发布。最顺手的改造是给LbsDataProvider增加一个加载模式从Resources.Load改成UnityWebRequest拉取服务器上的JSON。数据结构不用变仍然是building pois两段网络请求只需要替换加载函数后面所有寻路逻辑照旧。真机验证时记得检查Android的Internet权限这就是第5章黑屏问题的另一个非显性版本。我在另一个模拟项目X里用了完全相同的JSON结构团队后端只需要按这个Schema输出数据前端一行业务代码都没改。那一次改造让我体会到数据结构和逻辑分离写后期接真实服务时血泪会少很多。6.2 给导航算法增加回退不用 NavMesh 时走 A*如果是大型商场维护NavMesh烘焙数据会有点费劲。我一般会在NavigationService里做一个策略分支当NavMesh.CalculatePath返回false时回退到“路径点图 A*”。路径点图从JSON里的virtual坐标自动生成每个POI连到附近的2-3个邻居起点和终点再插入最近路径点。这样就算某个局部区域没有烘焙网格导航也还有一条可提的方案。验证方法很朴素在测试会议室里放两张A4纸一张标记起点、一张标记终点用卷尺量水平距离再靠近观察AR路径是否贴合。我会让Debug菜单显示当前使用的寻路算法名和路径点数确保回退逻辑确实被触发。从那以后我每次改动AR导航都会先跑一遍“固定纸张、记录坐标、量误差”的流程误差控制在20厘米以内才敢交给下一轮测试。希望这个Demo也能帮你快速验证自己的室内导航方案少走我踩过的那些弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询