
简介面向 Unity 开发者的 Rokid AR 眼镜扫码识别解决方案基于 Unity 2022.3.56f1c1 与 UXR3.0.3 SDK适配 Rokid Max Pro 与 Station Pro 设备适合正在为工业巡检、资产追踪、仓储物流等场景构建无接触信息采集功能的开发者。核心实现思路是先用 CameraPreview 类抓取眼镜端摄像头预览画面再通过 ZXing 插件解析二维码或条码内容识别到指定信息后即可对接后续业务逻辑整体代码清晰、可改造空间大。资源包采用 7z 压缩共 728 个文件、约 15.87MB包含 C# 脚本、Unity 场景、预制体、材质、着色器、字体、贴图等工程必备资源另有 PDF 说明文档、动画控制器、输入配置与 DLL 插件可帮助快速理清扫码功能的工程结构并直接集成到自己的 AR 应用。已有 196 人浏览学习资源提供了从摄像头预览、解码到事件处理的完整链路适合具备一定 Unity 基础、需要参考真实设备联调思路的开发者。1. Rokid AR 眼镜上的扫码识别这份 Unity 源码解决的不只是扫出来第一次在 Rokid AR 眼镜上做扫码识别我原以为调个 ZXing 库就完事了。实际上真正的难点全在相机取流、视频帧处理和坐标转换这三段路上——画面能不能出来、识别率高不高、识别到的二维码能不能钉在现实空间里每一步都有坑。这份 Unity3D C# 源码的价值在于把 Rokid AR 眼镜从视频流采集、二维码识别到空间定位的完整链路都打通了可以直接改、直接跑。适合两类人刚拿到 Rokid 设备想快速跑通扫码 Demo 的 Unity 开发者以及在做 AR 眼镜应用但被相机接口和像素坐标转换卡住的进阶开发者。最值得读的不是 ZXing 封装而是视频帧处理与坐标映射那部分逻辑。2. 扫码识别前置工作Rokid AR 的相机数据通路与坐标系2.1 相机取流的三种方式和选型理由先说结论在 Rokid AR 眼镜上拿摄像头画面有WebCamTexture、Rokid SDK 相机接口、Android Camera2 原生三套路子。这份源码采用的是WebCamTexture方案理由很实际——它在 Unity 里上手最快能直接把相机画面绑定到RawImage上做预览和调试省掉一版原生桥接代码。但WebCamTexture不是万能的。它本质上是 Android 暴露给 Unity 的虚拟摄像头封装帧数据的到达时间不受你控制而且分辨率、帧率都是请求式的设备不一定按你说的给。以 Rokid 眼镜为例请求 1280x72030fps实际到手的可能是 640x48025fps画面偏暗。但对扫码来说 640 宽已经够用了——QR 码只要在画面里占到 80 到 100 像素宽ZXing 就能稳定解码。如果你后续要处理更复杂的视觉任务比如同时做人脸检测和扫码建议早点切到 Rokid SDK 的相机接口。SDK 暴露的不是 Texture 而是原始帧回调YUV 数据拿到手再按需转 RGBA这样能省一次纹理上传还能拿到摄像头内参。内参在后续做坐标定位时是刚需WebCamTexture拿不到。到时候你会发现这个选择直接决定你能不能算准二维码在空间里的位置而不是只把识别结果当个文本用。提示第一版先用WebCamTexture把识别链路跑通。等你在第 5 章做坐标定位时发现内参不够用再换 SDK 接口也来得及接口切换对识别逻辑的影响很小主要改的是取帧那一层。2.2 为什么不能直接截屏帧格式与同步问题有朋友问过既然眼镜是 AR 设备屏幕上已经有相机画面了直接ScreenCapture截屏再送去识别不行吗我试过结论是能用但别用。ScreenCapture拿到的画面走的是 GPU 回读延迟和帧率都不稳定截十次有三四次拿到的其实是上一帧或上上一帧的画面这会让扫码框出现明显的迟滞感眼睛看到二维码已经移开识别框还停在原地。更关键的是截屏拿到的是屏幕坐标系下的成品画面已经叠了 Unity 渲染的 UI 和 3D 内容。如果二维码恰好被 UI 控件挡住或者识别框叠在别的组件上识别结果就不可靠了。而相机直接出的帧是干净的只有镜头看到的真实世界识别结果不会受 UI 污染。帧格式上WebCamTexture给你的是 RGB24 或 RGBA32 的Texture2D可以直接喂给 ZXing 的 Color32 解码接口。SDK 原始帧一般是 NV21 或 YUV420需要先转一次格式。转换耗时在高分辨率下很可观2048x1536 的一帧 YUV 转 RGBA 单线程要 10ms 上下直接拖帧。这也是为什么不建议一味追求高分辨率取流——扫码不是拍照画面够清楚就行帧率稳定比分辨率重要得多。2.3 一套代码要面对的坐标系像素、相机与世界扫码识别的输出是图像里某个矩形的四个顶点像素坐标但 AR 场景里你要的是现实空间中二维码在哪、朝向哪。这两者中间隔着三次坐标变换。第一次是像素坐标到归一化坐标。像素坐标以图像左上角为原点向右为 x、向下为 y归一化坐标把 x、y 都映射到 [0,1] 区间方便统一处理横竖屏和不同分辨率。第二次是归一化坐标到相机坐标这一步需要镜头内参也就是焦距和主点。第三次是相机坐标到 Unity 世界坐标取决于相机在虚拟场景里的位姿。坐标系原点与方向典型用途数据来源像素坐标 (x, y)图像左上角向右向下ZXing 识别输出相机帧归一化坐标 (u, v)左上角 (0,0)右下角 (1,1)跨分辨率统一像素坐标除以宽高相机坐标 (Xc, Yc, Zc)光心为原点Z 朝前深度估计、PnP 求解内参矩阵Unity 世界坐标 (Xw, Yw, Zw)场景原点Y 向上放置 AR 锚点相机外参这三步每一步都可能有偏差内参猜错、图像翻转没处理、相机外参没对齐都会让识别出的二维码位置偏出十几厘米。对扫码定位来说十几厘米意味着锚点可能直接贴到墙里或悬在半空这在体验上是不可接受的。源码里的坐标转换脚本核心价值就是把这三步写得可配置、可调参而不是写死一套内参算到底。后面我会在第 5 章把这条路完整走一遍。3. 源码工程拆解视频流采集、二维码识别与事件回调3.1 工程目录与脚本职责划分这份源码的工程结构不算复杂核心脚本集中在Scripts目录下。理解每个脚本的职责边界比直接上手改代码更重要——因为扫码识别的链路是线性的任何一个环节改动都会影响下游输出。目录 / 脚本职责关键依赖Scripts/Camera/CameraFrameGrabber.cs相机启动、帧拉取、预览绑定WebCamTexture / Rokid SDKScripts/QR/QRCodeDecoder.csZXing 初始化、解码、结果输出ZXing.unityScripts/QR/QRScanController.cs调度识别流程、事件广播、扫码节流上面两个脚本Scripts/AR/CoordConverter.cs像素坐标到世界坐标的换算相机内参、TransformScripts/AR/AnchorPlacer.cs在识别位置生成锚点与提示 UIUGUI、CoordConverterScripts/UI/ScanPreviewUI.cs相机预览、扫码框、结果显示RawImage、Text这套划分的逻辑是相机层只负责把帧交出去不关心帧里是什么识别层只负责从一段字节或纹理里解出二维码文本和顶点不关心帧从哪来定位层只负责把识别结果映射到三维空间不关心文本内容。我见过不少人把所有逻辑写进一个MonoBehaviour结果换 SDK、调内参、加 UI 全靠改同一个文件改一处炸三处。源码这种分层方式改起来会舒服很多。3.2 视频帧采集CameraFrameGrabber 的实现相机取帧这一步源码用的是WebCamTexture加RawImage预览。典型实现如下// CameraFrameGrabber.cs — 从 Rokid AR 相机获取视频帧 using UnityEngine; using UnityEngine.UI; public class CameraFrameGrabber : MonoBehaviour { public RawImage preview; // 调试预览目标 public QRCodeDecoder decoder; // 下游识别器 private WebCamTexture _webCam; private bool _isReady; void Start() { // Rokid 眼镜的相机在 Android 上会作为 WebCamTexture 设备暴露 // 拿到的设备名可能是 0 或 Camera 0按实际设备调整 _webCam new WebCamTexture(); _webCam.requestedWidth 1280; _webCam.requestedHeight 720; _webCam.requestedFPS 30; if (preview ! null) preview.texture _webCam; _webCam.Play(); _isReady true; } void Update() { if (!_isReady) return; if (_webCam null || !_webCam.isPlaying) return; if (!_webCam.didUpdateThisFrame) return; // 只处理新帧 // 把当前帧交给识别器内部会自行判断是否需要执行解码 if (decoder ! null) decoder.FeedFrame(_webCam); } void OnDestroy() { if (_webCam ! null) { _webCam.Stop(); Destroy(_webCam); } } }这段代码里有几个值得注意的细节。didUpdateThisFrame是WebCamTexture提供的帧更新标记必须判断它否则同一帧会在连续几个Update里被重复处理识别器每次都跑一遍完整解码性能直接崩。requestedWidth和requestedHeight是请求值而不是保证值设备会按自身能力就近匹配。如果你的项目走 Rokid SDK 原始帧回调替代写法是注册OnFrameReceived(byte[] yuv, int width, int height)在回调里先把 YUV 按NV21ToRGBA转成Color32[]再构造Texture2D。注意 SDK 回调可能不在主线程转完纹理后要通过UnityMainThreadDispatcher或队列把数据切回主线程再喂给识别器这个坑我在第 4 章详细讲。3.3 ZXing 识别器的接入与参数设置二维码解码部分源码用的是 ZXing 的 Unity 封装。这里有一个常见的认知误区ZXing 不是越快的模式越好TryHarder虽然是提高识别率的万能开关但它的代价是单帧解码耗时翻两三倍在 30fps 的视频流里会把帧率直接压垮。// QRCodeDecoder.cs — ZXing 解码器封装 using ZXing; using ZXing.Common; using ZXing.QrCode; using UnityEngine; public class QRCodeDecoder : MonoBehaviour { private BarcodeReader _reader; private Texture2D _texture; private Color32[] _pixels; [Header(识别参数)] public bool tryHarder false; // 高识别率模式耗时会显著增加 public float scanInterval 0.15f; // 两次识别之间的最小间隔秒 private float _lastScanTime; void Awake() { _reader new BarcodeReader { Options new DecodingOptions { TryHarder tryHarder, PossibleFormats new System.Collections.Generic.ListBarcodeFormat( new[] { BarcodeFormat.QR_CODE } // 只识别 QR 码提速明显 ), PureBarcode false, CharacterSet UTF-8 }, AutoRotate true }; } public void FeedFrame(WebCamTexture camTex) { if (Time.time - _lastScanTime scanInterval) return; // 节流 if (_texture null) { _texture new Texture2D(camTex.width, camTex.height, TextureFormat.RGBA32, false); _pixels new Color32[camTex.width * camTex.height]; } // 从 WebCamTexture 拷贝像素到 Color32 数组 _pixels camTex.GetPixels32(); var result _reader.Decode(_pixels, camTex.width, camTex.height); if (result ! null !string.IsNullOrEmpty(result.Text)) { Debug.Log($[QR] 识别成功: {result.Text}); QRScanController.Instance.BroadcastResult(result, camTex); } _lastScanTime Time.time; } }关键参数有三个。PossibleFormats限定为QR_CODE这一项能过滤掉大量无关的条形码检测分支解码速度提升非常明显我实测同一台设备从 40ms 降到 12ms 左右。scanInterval是识别节流阈值设为 0.15 秒意味着每秒最多跑六七次完整解码既能保证连续扫码体验又不会让 CPU 一直满负荷。AutoRotate应对扫码时可能把手机横过来用的情况但 AR 眼镜的场景里用户头部转动是常态建议保持开启。注意GetPixels32()每次调用都会从 GPU 侧拷贝数据这是解码管线里最贵的操作之一。如果发现帧率吃紧优先瓶颈排查这里而不是去调 ZXing 参数。3.4 识别结果的回调与 UI 联动源码里用了一个简单的单例QRScanController做结果分发。识别器不直接操纵 UI而是把结果抛给控制器由控制器决定是显示文本、生成锚点还是触发某个业务事件。// QRScanController.cs — 扫码事件分发 using UnityEngine; using ZXing; public class QRScanController : MonoBehaviour { public static QRScanController Instance { get; private set; } public System.ActionResult, Texture OnQRDetected; // 外部订阅 private string _lastCode ; private float _lastCodeTime 0f; void Awake() { if (Instance ! null Instance ! this) Destroy(gameObject); Instance this; } public void BroadcastResult(Result result, Texture source) { // 3 秒内识别到相同文本则忽略避免重复触发 if (result.Text _lastCode Time.time - _lastCodeTime 3f) return; _lastCode result.Text; _lastCodeTime Time.time; OnQRDetected?.Invoke(result, source); } }这个控制器的职责很纯粹去重、广播。_lastCode加时间戳的去重逻辑是我后来补上的——刚开始没加同一个二维码停下来盯两秒日志里刷了十几条相同结果业务还被重复触发了。具体的 UI 订阅可以在任意脚本里注册QRScanController.Instance.OnQRDetected OnScanResultHandler做到识别层与 UI 层完全解耦。4. 扫码识别常见问题与避坑黑屏、识别率低、坐标偏移4.1 现象相机画面全黑但应用没有报错原因Android 相机权限没有在运行时申请或者WebCamTexture.Play()在权限对话框弹出前就被调用了。Rokid 眼镜的 Android 系统对相机权限卡得比较严即使Manifest里声明了CAMERA权限运行时还是要过一遍动态权限流程。解决在启动相机流程前用AndroidPermissionsManager安卓原生权限封装或Permission.HasUserAuthorizedPermission(Permission.Camera)显式申请权限。权限回调确认后再调用_webCam.Play()。顺带检查一下Manifest里是否同时声明了RECORD_AUDIO部分 SDK 版本在拿相机流时会连带要求麦克风权限缺了也会黑屏。4.2 现象同一个二维码要扫五六次才识别成功原因最常见的有三个。一是画面太暗Rokid 眼镜的摄像头在室内光线不足时噪点很大ZXing 对噪点的容忍度有限。二是二维码在画面里占比太小识别器要求码的尺寸达到画面宽度的一定比例才稳定工作。三是TryHarder关闭后对小码的补全逻辑弱了很多。解决先看预览画面确认码的像素宽度是不是小于 100 像素如果是靠近一点或把取流分辨率调低分辨率调低后同样的物理码在画面里占比反而变大。其次检查曝光WebCamTexture没有曝光接口但可以等GetPixels32()之后先算平均亮度低于阈值就在 UI 上提示环境过暗。最后再把TryHarder打开做个对照测试——如果从识别不出来变成稳定识别说明确实是解码难度问题可以考虑开启但把scanInterval调大来折算耗时。4.3 现象识别成功但定位锚点偏了十几厘米原因这是坐标转换的问题追根溯源的第 4.3 条几乎全部出在图像翻转和分辨率不匹配上。WebCamTexture在眼镜这类设备上经常以竖屏物理方向出帧但 Unity 的世界坐标系是固定的图像 y 轴方向和世界 y 轴方向不一致直接拿像素坐标做映射当然偏。解决在CoordConverter里加一个可配置的镜像和翻转标志位分别对应WebCamTexture.videoMirrored和videoRotationAngle。每帧判断这两个值把像素坐标先做旋转和镜像再进归一化流程。另外确认转换脚本用的是实际取流分辨率而不是请求分辨率这个错我犯过两次——requestedWidth 1280实际拿到 640坐标直接翻了一倍。4.4 现象帧率掉到 15 FPS 以下UI 卡顿明显原因GetPixels32()和解码都在主线程跑一次完整流程 30ms 到 50ms加上渲染开销帧率自然上不去。我见过把 ZXing 解码直接塞在Update里不设任何节流的写法画面直接变成幻灯片。解决两个手段配合用。第一解码放进独立线程主线程只做结果回调。ZXing 的BarcodeReader.Decode本身是线程安全的喂进去Color32[]数组它不碰 Unity API跑在Task.Run或后台Thread里没任何问题回调到主线程时再处理 UI。第二严格设置scanInterval把识别频率控制在 6 到 8 次每秒就足够扫码不是需要实时 30fps 识别的工作。4.5 现象同一个码识别成功后移开再回来又被当成新码触发原因第 3.4 节的去重逻辑只在 3 秒内有效。用户把码移出视野再移回来如果超过 3 秒控制器会当成新识别事件重新广播。解决把去重窗口从时间间隔改成上次识别后是否发生了目标移除——也就是维护一个_isTargetVisible状态。识别到码时置为 true连续若干帧都没识别到任何码时置为 false只有当_isTargetVisible从 false 变为 true 时才广播事件。这样用户长时间举着同一个码不会重复触发移开再拿回来才会触发重新识别。5. 把二维码坐标变成 AR 锚点像素到世界坐标的完整链路5.1 相机内参与像素归一化拿到 ZXing 输出的四个顶点像素坐标之后第一步是归一化。假设图像宽为W、高为H顶点(x, y)的归一化坐标是u x / W、v y / H。这一步看起来简单但有一个容易忽略的细节WebCamTexture的像素数组是按横屏还是竖屏排布的直接决定归一化的宽高取哪个值。推荐的做法是只认GetPixels32()返回数组的真实尺寸不依赖任何请求参数。相机内参这块源码给了一个可配置项fov水平视场角。如果没有拿到 SDK 的真实内参可以用 FOV 近似估算焦距已知水平 FOV 为fov度图像宽W像素则焦距fx (W / 2) / tan(fov / 2)。Rokid 眼镜的摄像头水平 FOV 通常在 60 到 90 度之间具体数值在设备规格书里能查到。焦距是后续从像素尺寸推算真实距离的关键参数能拿到精确内参就不要用估算值。5.2 深度估计与射线投射二维码坐标转成 AR 锚点核心问题是深度未知像素坐标只告诉你方向没告诉你距离。有两个方案复杂度差别很大。方案一是纯射线方案从相机位置朝二维码像素对应的方向打一条射线在射线与某个平面比如地面或桌面的交点处放锚点。这个方案只适合码贴在固定平面上且平面已经用平面检测或手动标记的方式确定好的场景。方案二是单目测距方案已知二维码的物理尺寸L比如 10cm检测到它在画面里占的像素宽度w结合焦距fx深度Z fx * L / w。拿到深度之后二维码的三维位置就是相机坐标系下沿该方向距离Z处的点// CoordConverter.cs — 从像素坐标和码宽推算世界坐标位置 using UnityEngine; public static class CoordConverter { // imageW/imageH: 实际帧尺寸fov: 水平视场角度 public static bool PixelToWorld( Vector2 qrCenter, float qrPixelWidth, float imageW, float imageH, float fov, float qrRealWidthMeters, // 二维码物理宽度单位米 Transform cameraTransform, out Vector3 worldPos) { worldPos Vector3.zero; // 1. 归一化到 [-1, 1] float ndcX (qrCenter.x / imageW) * 2f - 1f; float ndcY (qrCenter.y / imageH) * 2f - 1f; // 2. 估算焦距像素单位 float fx (imageW / 2f) / Mathf.Tan(fov * Mathf.Deg2Rad / 2f); // 3. 单目测距 float depth fx * qrRealWidthMeters / qrPixelWidth; // 4. 从相机出发构造射线方向 Vector3 cameraForward cameraTransform.forward; Vector3 cameraRight cameraTransform.right; Vector3 cameraUp cameraTransform.up; Vector3 rayDir cameraForward cameraRight * ndcX * Mathf.Tan(fov * Mathf.Deg2Rad / 2f) cameraUp * ndcY * Mathf.Tan(fov * Mathf.Deg2Rad / 2f) * (imageH / imageW); rayDir.Normalize(); worldPos cameraTransform.position rayDir * depth; return true; } }这段代码的逻辑按四步走先算归一化坐标再估算焦距然后用已知码宽和像素宽度推算深度最后从摄像头位姿出发构造一条指向该方向且距离为depth的点。注意第 4 步里ndcY乘了imageH / imageW这一项是因为像素长宽比不是 1:1如果不修正垂直方向的角度会算偏。这是整套坐标转换里最容易翻车的地方也是源码里为什么要保留fov、imageW、imageH三个独立可配置参数的原因。5.3 锚点生成与 UGUI 提示坐标算出来后在worldPos处实例化一个锚点GameObject挂上简单的标识模型或粒子效果即可。这一步建议注意锚点的朝向——二维码不是对称的四个顶点给了你码的旋转信息。用result.ResultPoints取出四个顶点就能算出码的平面法线让锚点的 forward 方向对齐法线。这样二维码在任意倾斜角度被识别时锚点都会贴在码面上而不是永远朝一个固定方向。// AnchorPlacer.cs — 放置锚点并显示识别结果 public void PlaceAnchor(Vector3 worldPos, Vector3 normal, string payload) { var go Instantiate(_anchorPrefab, worldPos, Quaternion.identity); go.transform.forward normal; // 在锚点上方生成一个 UGUI 标签显示码内容 var label Instantiate(_labelPrefab, _canvas.transform); label.GetComponentText().text payload; // 把 UI 位置绑定到锚点的屏幕投影上 _trackingList.Add(new TrackingItem { Anchor go, Label label }); }TrackingItem在Update里把锚点的世界坐标转成屏幕坐标再赋值给RectTransform的position实现标签跟手的效果。到这里从相机画面到空间锚点的完整链路就通了。6. 进阶玩法连续扫码去重与性能验证清单6.1 扫码节流与结果去重基本去重用第 3.4 节的时间窗口就够了但真实业务里一个很常见的问题是一条产线上的设备连续贴了十几个内容相同但批次不同的二维码用户希望每个码都被当成独立事件不能因为内容相同就被吞掉。这时候只靠内容去重就不够了要把码的区域位置也加进去——如果新识别到的码虽然文本相同但中心坐标和上一次识别相差超过画面宽度的 30%就认为它是一个新的码。另一个实际场景是混合码。有些业务要在同一个画面里同时放一个 QR 码和一张一维码ZXing 在PossibleFormats限制为QR_CODE时会漏掉一维码。如果你的业务有混合码需求把BarcodeFormat.EAN_13、CODE_128加回去但别忘了同时把scanInterval调大一点因为解码分支多了单帧耗时必然上升。6.2 四步性能验证法我每次改完取流或识别逻辑都强制走一遍这四个验证步骤这几乎成了我的习惯第一步用Profiler记录主线程耗时占比。取流加解码总耗时超过 25ms 就必须优化要么降分辨率要么把解码移线程。第二步跑一个 30 秒连续扫码压力测试确认没有内存泄漏——Texture2D每次新建不释放是这个项目的经典泄漏源GetPixels32的数组如果频繁重新分配也会造成 GC 抖动。第三步在不同室内光照下各测十次识别成功率低于 90% 就优先处理光线问题。第四步把锚点放在已知物理距离的固定位置量一下 Unity 坐标里的距离误差超过 5% 就检查 FOV 参数和深度公式。从那以后我每次接手类似的 AR 扫码项目都会先跑完这套验证再动业务逻辑——因为定位误差和识别成功率是扫码 AR 的两条生命线数据不立住后面所有功能都是空中楼阁。希望这份源码和这些踩坑记录能帮你在 Rokid AR 眼镜上少走几个弯路。本文还有配套的精品资源点击获取