简介AR-Android 是一套面向 Android 平台的增强现实开发框架示例工程专为希望快速构建 AR 应用的移动开发者设计。项目围绕增强现实基础、追踪技术、图像处理、3D 渲染与用户交互等核心模块展开涵盖特征点追踪、标记识别与 SLAM 定位等实现思路适用于教育虚拟实验、AR 导航、零售试穿等场景。压缩包共包含 49 个文件其中 27 个 asset 文件保存 3D 模型、材质与场景配置14 个 meta 文件用于 Unity 资源元数据2 个 unity 场景文件定义 AR 交互流程另有项目级 asset 配置和 manifest.json 等工程文件并附带可直接安装的 Test.apk 演示包整体大小 17.68MB。目前已有 189 人浏览学习。解压即可获得完整 Unity 工程目录包括 Assets、ProjectSettings、Packages 等便于开发者对照场景结构、脚本与项目设置理解 AR 应用从素材导入到打包发布的完整链路也可直接基于该项目二次开发快速验证 AR 功能。1. AR-Android 是什么一句话定位再决定要不要投入AR-Android 这个关键词指向的是在 Android 设备上把虚拟内容叠加到真实画面的整套工程方案——涉及相机采集、传感器融合、3D 渲染和机型适配不是加一个依赖包就能出效果的小功能。它解决的实际问题是用户举起手机摄像头画面里能稳定地出现一个随手机移动而保持空间位置的虚拟物体。这篇文章从 ARCore 入手把环境搭建、最小可运行实现、关键参数和排查路径全部过一遍。适合正打算启动 AR 功能但没定技术方案的团队也适合已经在开发中被稳定性问题磨到怀疑人生的开发者。2. 搭建 AR-Android 工程先把设备、依赖和调试条件定下来2.1 设备能力支持清单是第一道门槛不是一道能绕过的墙AR-Android 开发的第一步是确定运行设备。增强现实依赖摄像头画面与惯性传感器数据的实时融合这个能力不是所有 Android 设备都具备。交付某个模拟项目X 时一开始只写了业务逻辑、没做设备检测测试人员拿一台旧机型安装后应用启动直接闪退后来补上这段检查整个链路才走通。fun isArSupported(context: Context): Boolean { return try { val session Session(context) val supported session.isSupported session.close() supported } catch (e: UnavailableException) { false } }这段代码做的事是创建会话、查询支持状态、主动释放会话。容易漏掉的是后半句session.close()在应用启动路径里多次创建会话而不关闭会累积底层资源严重点会让后续启动变慢。还有一个小陷阱isSupported在部分设备上要等摄像头真正打开后才返回可靠结果所以更稳的做法是先用 PackageManager 查FEATURE_CAMERA_AR特性再用会话结果兜底。两者结果不一致的场景并不少见最终判断以真机表现为准。提示Session 的所有调用必须在主线程执行在子线程创建会直接抛 RuntimeException。设备检查通过后下一个决定影响整个节奏的是渲染方案选型。常见做法有三类一是直接用底层图形 API 写渲染循环控制力最强但工作量最大模型加载、着色器、相机纹理都要自己处理二是用封装好的 Android AR 渲染库几分钟能跑通官方示例适合产品原型和中小型项目三是接入通用 3D 引擎的移动端版本适合项目后期要做复杂交互或跨平台。我的建议是团队没有专职图形程序员时不要尝试第一种。封装库固然有黑匣子的成分但它的示例代码和排错手段都更成熟出了问题至少搜得到同类场景。2.2 依赖与 manifest 配置五个不能漏的声明AR-Android 工程的 manifest 有一组固定配置项。漏掉任何一个行为都是「安装正常、权限都给了但一开相机就崩」。manifest xmlns:androidhttp://schemas.android.com/apk/res/android uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-feature android:nameandroid.hardware.camera.ar android:requiredtrue / application meta-data android:namecom.ar.core.VERSION android:value1.0 / /application /manifestCAMERA 权限不用解释没有它整个应用都不成立。INTERNET 和 ACCESS_NETWORK_STATE 是为远程模型资源和会话调试准备的就算第一版全部用本地模型我也建议先加上否则后期要排查网络问题时还得回来加权限、重新出包。uses-feature的requiredtrue表示不支持 AR 的设备在应用市场上直接搜不到这个应用属于硬性过滤对工具类 AR 应用是合适的如果做的是普通应用里的一个辅助 AR 功能required要改成 false并在代码里做降级处理。meta-data 那一项是 ARCore 运行时读取版本声明的地方漏掉它的典型报错五花八门大部分指向「启动会话失败」。依赖方面工程需要两个基础模块运行时库和渲染辅助库。运行时库提供平面检测、锚点、光照估计这些能力渲染辅助库负责把 3D 模型加载进场景并绘制到屏幕上。选型时不要只看 star 数要看最后提交时间和示例的完整性。一个维护停顿的项目遇到编译问题基本只能自己啃源码。2.3 模拟器和真机的差距两个只在真机复现的 bug模拟器上跑通 AR 场景不代表真机能跑。摄像头数据、陀螺仪频率、加速度计时序在模拟器里全是仿真出来的最典型的问题有两个。第一个是平面检测在模拟器里秒成功真机上却迟迟检测不到平面。模拟器的平面数据是预设好的环境光照也恒定而真机在弱光、重复纹理的地面上几乎无法收敛。第二个是旋转姿态在模拟器里平滑得可怕真机上手一抖模型就开始跳。日常开发中模拟器只适合验证权限流程和 UI 布局算法表现和渲染效果必须用真机调试。血泪经验是至少备两部不同品牌的中端机型最好一部高通平台一部非高通平台能覆盖绝大多数用户的实际硬件。单价高的旗舰机型性能好、传感器豪华但恰恰掩盖了代码里很多性能问题。3. 让 AR 物体出现在摄像头画面里最小可运行实现3.1 初始化会话顺序不对画面就是不亮AR-Android 应用启动后的第一个动作是初始化 AR 会话。这一步的代码量不大但调用顺序直接影响摄像头是否能点亮。class ArActivity : AppCompatActivity() { private lateinit var session: Session private lateinit var arSurfaceView: GLSurfaceView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ar) arSurfaceView findViewById(R.id.ar_surface_view) session Session(this) val config Config(session) config.planeFindingMode Config.PlaneFindingMode.HORIZONTAL session.configure(config) // GLSurfaceView 的渲染器要在会话 resume 之前绑定 arSurfaceView.onResume() session.resume() } override fun onPause() { session.pause() arSurfaceView.onPause() super.onPause() } }这里有两个细节值得展开。第一是session.resume()的时机必须在渲染表面已经做好准备之后调用。GLSurfaceView 的onResume()会创建或恢复渲染线程如果先恢复会话再恢复渲染表面首帧到来时没有可绘制的表面画面就会卡在黑屏而且没有任何异常日志。第二是 Config 的planeFindingMode这个参数决定系统去识别哪种平面。只检测水平面时系统会把更多算力集中在找地面或桌面适合放家具、摆装饰物的场景需要竖直平面时再加 HORIZONTAL_AND_VERTICAL代价是检测耗时更长首次完成平面识别的速度会慢一截。这组生命周期还有一个特点是顺序敏感性。Activity 走到onPause时必须先暂停会话否则相机资源被占住下一个 Activity 打开摄像头就会失败。反过来onResume时也要处理一种情况应用退到后台再回来会话状态可能已经重置要对 Session 做一次重新配置或重启而不是直接继续用旧会话。3.2 点按放置锚点把屏幕坐标换算成空间坐标锚点是 AR 场景里「把虚拟物体固定在现实空间某个位置」的机制。用户的触摸坐标落在屏幕的二维平面上需要把它映射成一条从相机出发的空间射线再与检测到的平面求交点。override fun onTouchEvent(event: MotionEvent): Boolean { if (event.action ! MotionEvent.ACTION_UP) { return super.onTouchEvent(event) } val frame session.update() val hitResults frame.hitTest(event.x, event.y) if (hitResults.isEmpty()) { Toast.makeText(this, 这里没有检测到平面换个位置试试, Toast.LENGTH_SHORT).show() return true } val hit hitResults[0] val anchor try { hit.createAnchor() } catch (e: Exception) { return true } placeModel(anchor) return true }hitTest返回一个按深度排序的结果列表取第一个通常是最靠近用户的交点。这里必须处理两个高频问题一是createAnchor()可能在会话状态异常时抛异常裸奔的话会直接闪退需要包一层 try-catch二是用户快速连点时每次点击都创建锚点会让场景堆满模型正确的商业产品里要做 300 到 500 毫秒的点击节流。还有一个更深层的边界frame必须是当前相机帧的有效状态如果session.update()还没跑过或离上一帧太近hitTest拿到的可能是过期帧碰撞点会明显偏离手指位置。这个问题不容易察觉表现在用户点按到一个物体旁边模型却出现在远处或歪了。我习惯在onDrawFrame里保存最新帧引用触摸事件直接读缓存帧的hitTest而不是在触摸回调里再调一次session.update()。3.3 模型加载与帧循环让物体「钉」在现实里模型加载是异步操作本地一个 5MB 的 GLB 模型在低端机上可能要几百毫秒。从点击到模型出现的这段空窗期最好给用户一个可感知的反馈。private fun placeModel(anchor: Anchor) { val node AnchorNode(anchor) ModelBuilder.builder() .setSource(ContextCompat.getDrawable(this, R.drawable.model_asset)) .build() .thenAccept { renderable - node.renderable renderable arSceneView.addChild(node) isLoading false } .exceptionally { e - Log.e(ARPlacement, 模型加载失败, e) null } }thenAccept回调里的renderable一旦挂到节点上节点就会跟随锚点的空间位置变化。AnchorNode 的位姿由 ARCore 每帧更新所以模型看起来才像是被钉在现实世界里。帧循环里的活通常很轻更新会话、获取相机纹理、把模型摆放信息同步给渲染器。override fun onDrawFrame() { val frame session.update() val camera frame.camera // 对每个已放置的锚点节点把模型朝向修正为面向相机 for (node in placedNodes) { if (node.parent ! null node.renderable ! null) { val direction node.worldPosition - camera.position node.worldRotation Quaternion.lookRotation(direction, Vector3.up()) } } }这段代码解决的是一个真实问题模型默认的方向常常和用户期待的不一致。比如摆一张桌子用户希望桌面法线朝上放一个箭头指示牌用户希望箭头面朝自己。做一次lookRotation把模型朝向相机能省去美术反复调模型旋转轴的扯皮。真正需要单独优化的是当锚点丢失时模型会「跳走」的场景处理方式比较多后面避坑章节会细说。4. AR-Android 的四个必调参数前三个影响观感第四个影响开发排期4.1 平面检测模式水平、垂直还是都要平面检测是 AR 体验的地基。模式选错用户要么等不到平面提示要么识别出来的地面歪得离谱。ARCore 的模式有三档只检测水平面、只检测竖直面、两者都检测。从数据看实际差距水平面检测在普通室内环境下通常两三秒内出结果竖直面由于墙面纹理信息少往往需要用户慢慢扫过墙面才能锁定。所以产品设计上如果主交互是「把虚拟柜子放地上」就只开 HORIZONTAL并在界面上提示用户「对着地面慢慢移动手机」。如果开了竖直面检测就要给用户更长的等待预期不能拿水平面的速度标准来衡量。val config Config(session) config.planeFindingMode Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL config.updateMode Config.UpdateMode.LATEST_CAMERA_IMAGE session.configure(config)UpdateMode.LATEST_CAMERA_IMAGE是一个常被忽略的选项。默认模式下会话更新可能会等待相机图像就绪导致画面流畅度受影响设置为 LATEST_CAMERA_IMAGE 后系统会用最新图像更新跟踪状态视觉上拖影更少。代价是传感器数据融合的延迟略高但对大多数展示类 AR 场景来说视觉优先是对的。4.2 光照估计虚拟物体能不能融入现实靠它不调光照参数模型就像悬浮在场景上——亮度对不上阴影方向也对不上。ARCore 的光照估计分两种模式一种是简单的环境光强度估计只给一个整体亮度值另一种是环境 HDR会捕捉场景的球面光照信息让模型表面的明暗变化和现实环境一致。val config Config(session) config.lightEstimationMode Config.LightEstimationMode.ENVIRONMENTAL_HDR session.configure(config) // 渲染时把估计的光照应用到模型 val lightEstimate frame.lightEstimate modelNode.renderable?.setShadowCaster(true)有一点需要注意光照估计模式必须和渲染器配合。封装库如果本身不支持 PBR 光照开了 HDR 也不会有明显效果反而增加计算开销。打开光照估计后要做的验证是把手机从窗边移到室内暗处观察模型亮度变化是否平滑——如果出现亮度跳动多半是 HDR 参数没跟上实时估计需要手动把变化频率做一下平滑。4.3 渲染分辨率与功耗清晰度和发热怎么取平衡一台中端手机跑 1080p 渲染和 720p 渲染发热差异肉眼可见。项目早期可以用一个全局配置来控制渲染目标分辨率而不是写死。val renderConfig RenderConfig.builder() .setResolution(1920, 1080) .setCameraStreamMode(CameraStreamMode.FULL_FRAME) .build()真机上测过的经验是这样的720p 渲染加全帧相机流视觉清晰度已经能满足大部分商品展示场景1080p 只是放大看纹理时更锐利一些但持续运行十分钟后手机背部温度能差 3 到 5 度。这个差异在夏天会被用户直接感知。产品验收时如果 AR 是核心功能建议默认 1080p如果 AR 只是辅助手段720p 更稳妥。以后遇到低端机型再按运行内存动态降级。4.4 遮挡虚拟物体为什么应该被墙挡住AR 场景里一个违背直觉的现象是虚拟物体永远画在真实物体上层。桌球放到桌面边缘本该被桌子边缘挡住的部分会直接叠在桌子上。这就是遮挡处理。val depthMode if (session.isDepthModeSupported(Config.DepthMode.AUTOMATIC)) { Config.DepthMode.AUTOMATIC } else { Config.DepthMode.DISABLED } config.depthMode depthMode开启 Depth 模式后系统会生成场景深度图渲染器可以据此判断虚拟物体哪些像素应该被真实物体覆盖。这个参数的坑在兼容性支持深度模式的设备占比有上限低端机基本不支持。所以代码里必须先isDepthModeSupported判断不支持就降级为无遮挡版本。实际上卖相最好的 AR 商品展示几乎都开了深度遮挡没开之前用户很容易用一个手势在虚拟物体前比划一下然后立刻指出「它是假的手穿过去了」。5. AR-Android 避坑记录四个高频翻车现场与排查顺序5.1 现象摄像头黑屏没有任何报错黑屏是最磨人的问题因为它不崩溃、日志也少。常见原因有三类权限弹窗还没点完就开始会话、摄像头被其他应用占用、会话恢复顺序错误。排查顺序先看 Logcat 有没有 Camera 相关的异常再确认session.resume()是否在渲染表面 ready 之后执行。解决方法也直接权限回调成功后再初始化会话onPause 时释放会话onResume 时重建。5.2 现象模型原地抖动或者随手机转动漂移这个现象在所有 AR 应用里都可能出现。原因分两层一是光线弱相机帧质量差导致姿态解算抖动二是当前设备的陀螺仪没有校准系统融合出来的位姿噪声大。解决时先判断是全局抖动还是局部抖动。全局抖动且出现时间固定去检查光线和纹理只有某个锚点位置的模型抖去检查这个锚点创建时是否碰到移动中的物体上。锚点必须创建在静态平面上人走过去挡住平面后再创建锚点大概率飘。注意地面是反光瓷砖时平面检测会反复失败因为反光面的特征点在每一帧变化很大。遇到这种场景引导用户换一个粗糙纹理的地面。5.3 现象模型被真实物体穿透或者被地面吃掉一半穿透多半是深度遮挡没开前面第 4.4 节已经覆盖。这里补充一个更隐蔽的原因模型本身的碰撞体和包围盒没设置。某些渲染库加载 GLB 模型后默认没有碰撞体系统不知道模型的真实空间范围遮挡计算只能用锚点近似。解决是在模型加载后设置碰撞体尺寸通常用归一化包围盒自动生成。模型被地面吃掉的常见原因是模型锚定在了平面上、但模型基准点的原点和锚点重合美术建模时原点不在底部模型就会陷进地面。检查一下模型原点必要时在节点上加一段垂直偏移。5.4 现象应用切后台再回来AR 画面恢复但模型全没了这个问题出在 Activity 重建时没有保存锚点状态。ARCore 的锚点绑定的是会话生命周期会话销毁后锚点随之失效。切后台导致 Activity 重建、会话被销毁再回到前台时旧锚点已经全部不可用。解决思路是在onSaveInstanceState里记录每个锚点的空间 pose 和语义信息Activity 重建后按 pose 重新创建锚点。对于要长期保存的锚点把 pose 数据同步到云端下次启动直接恢复——这也是 AR 应用完成「记住了摆放位置」的基建做法。需要说明的是锚点恢复本身有位置误差实际偏移通常在厘米级产品设计上要给用户一个微调手势。6. 交付前要做的三项验证把 AR 场景从「能跑」带到「敢上线」进入提测阶段前我会固定走三个验证流程每个都能撞出隐藏问题。第一项是光线验证。分别在明亮窗边、普通室内灯、傍晚昏暗走廊三个环境各跑十分钟记录平面检测成功率和模型抖动次数。弱光下如果平面检测失败率高要在 UI 上提示「增加光线」而不是让用户自己干等。这个提示字段在需求文档里经常被忘记但在用户反馈里排名前三。第二项是动线验证。握着手机走一条包含转弯和上下楼梯的路径连续放置多个锚点并来回绕行。AR 系统对位置的估计误差会随运动路径累积绕一圈回到起点时模型相对初始位置通常会有偏差。偏差数值在 5 厘米内可以接受超过就要检查是不是初始化时手机还在移动。验证的最终目的是找到一个「建议用户开始的姿势」一般要求用户先保持静止两秒再开始。第三项是长时间会话内存观察。开启内存监控持续运行三十分钟观察原生内存和 Java 堆内存曲线。AR 应用最常见的隐性 bug 是每帧创建新对象导致 GC 频率不断升高表现就是运行越久画面越卡。抓取堆转储后用内存分析工具看大对象持有链重点排查模型纹理是否重复加载和帧对象是否被外部引用住。最后说一个形成习惯的做法每次调整配置参数都记录当时的机型、系统版本、光线条件和观察结果。AR 的参数改动没有全局最优解只有「在特定环境、特定机型下最适合」的解。这些记录攒到一定量后你就能回答那个项目评审会上必被问到的问题「这个功能在所有用户手机上效果有保障吗」——虽然还是不能保证但至少你能说出哪些机型验证过、哪些参数怎么调出来的。希望这篇笔记能帮你少踩一些已经有人踩过的坑。本文还有配套的精品资源点击获取