AR Foundation图像追踪与坐标转换实战:从原理到精准交互实现
1. 项目概述与核心价值在AR应用开发中图像追踪Image Tracking是实现虚实结合最基础也最核心的能力之一。想象一下你打开手机摄像头对准一张海报海报上立刻“活”过来一个3D动画角色或者一个产品模型从说明书上旋转升起——这种魔法般的体验其基石就是图像追踪。而要让这个虚拟角色稳稳地“站”在现实世界的图像上或者让用户能精准地“点击”到它就离不开一套精确的坐标转换Coordinate Conversion体系。这个项目标题“AR Foundation实战图像追踪与坐标转换技巧上”直接点出了AR开发中两个紧密耦合、且新手最容易“卡壳”的硬核主题。很多开发者跟着教程跑通了Demo把模型显示在了图像上但一旦涉及到交互比如点击图像触发事件、或者需要将虚拟物体的位置与其他系统如地图、后端服务对接时就会遇到各种坐标错乱、位置飘移的问题。其根源往往在于对AR Foundation提供的坐标系统理解不透彻对不同空间之间的转换关系模糊不清。本文将从一线实战的角度彻底拆解AR Foundation中图像追踪的工作流并聚焦于最关键的“坐标转换”技巧。我会带你理解从2D屏幕、到3D相机、再到真实世界坐标系World Space和图像局部坐标系Local Space之间是如何层层转换的。这不仅关乎一个功能能否实现更决定了你的AR应用交互是否精准、体验是否稳定。无论你是想实现点击图像弹出信息还是根据图像位置动态布局UI或是为更复杂的空间计算打下基础这里的技巧都是你必须掌握的“内功”。2. 图像追踪的核心原理与ARTrackedImageManager解析2.1 AR图像追踪是如何工作的在深入代码之前我们必须先搞清楚AR Foundation以及其背后的ARKit/ARCore是如何“看见”并“记住”一张图片的。这个过程不是简单的图像识别而是一个实时的、基于特征点的空间定位过程。当你通过ARTrackedImageManager将一张参考图片Reference Image添加到资源库时SDK会提前对这张图片进行离线分析。它并不存储整张图片而是提取出一系列高区分度的特征点Feature Points并生成一个独特的“特征描述符”。在运行时设备摄像头捕捉到的每一帧画面也会被实时提取特征点。系统通过高效的算法将当前帧的特征点与预存的特征描述符进行匹配。当匹配度足够高且匹配的特征点分布能计算出合理的透视变换时系统就判定“追踪到了”。此时最关键的一步发生了系统会估算出被识别图像在真实3D空间中的位置一个三维坐标点、朝向一个旋转四元数和物理尺寸。这个“物理尺寸”就是你之前在ARTrackedImageManager中为参考图设置的宽度或高度单位通常是米。所有后续的虚拟内容放置都基于这个计算出的空间姿态Pose。注意图像追踪的稳定性和精度极度依赖参考图的质量。高对比度、丰富纹理、非对称的图片效果最好。纯色、大面积重复图案、或完全对称的Logo追踪效果会很差甚至失败。2.2 ARTrackedImageManager 深度配置指南ARTrackedImageManager是你的总指挥中心。在Unity Inspector中配置它时有几个参数至关重要参考图像库Reference Image Library这是你所有待追踪图片的集合。创建时务必为每张图片设置一个合理的物理尺寸。这个尺寸不是图片的像素大小而是它在现实世界中你期望的宽度或高度例如一张A4纸上的图片宽可能设为0.21米。这个值将直接决定ARTrackedImage.size属性是后续所有坐标计算的基础。最大移动图像数Max Number Of Moving Images对于静态图像识别保持默认值0即可。除非你在追踪快速移动的图片否则不要修改。支持的运动类型Supported Motion Types对于大多数贴在墙面或桌面的图片选择TrackingOnly仅追踪即可性能最优。如果你的图片可能被用户拿在手里移动则选择TrackingAndTracking。在代码中你需要订阅关键事件来响应追踪状态的变化public class ImageTracker : MonoBehaviour { public ARTrackedImageManager trackedImageManager; void OnEnable() { trackedImageManager.trackedImagesChanged OnTrackedImagesChanged; } void OnDisable() { trackedImageManager.trackedImagesChanged - OnTrackedImagesChanged; } void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs) { // 新检测到的图像 foreach (var newImage in eventArgs.added) { Debug.Log($新图像被添加: {newImage.referenceImage.name}); // 通常在这里实例化并放置与该图像关联的虚拟物体 SetupARContentForImage(newImage); } // 状态发生更新的图像包括位置更新、丢失后又找回等 foreach (var updatedImage in eventArgs.updated) { // 核心根据追踪状态决定如何处理虚拟内容 if (updatedImage.trackingState TrackingState.Tracking) { // 图像被稳定追踪更新虚拟物体的位置和旋转 UpdateARContent(updatedImage); } else if (updatedImage.trackingState TrackingState.Limited) { // 追踪受限例如图像部分移出视野可以隐藏或淡化虚拟内容 LimitARContent(updatedImage); } else // TrackingState.None { // 图像丢失隐藏虚拟内容 HideARContent(updatedImage); } } // 被移除的图像通常发生在会话重置或管理器禁用时 foreach (var removedImage in eventArgs.removed) { Debug.Log($图像被移除: {removedImage.referenceImage.name}); // 清理和销毁与该图像关联的虚拟物体 CleanupARContent(removedImage); } } }实操心得永远不要假设图像会一直被稳定追踪。环境光线变化、摄像头遮挡、快速移动都可能导致追踪状态在Tracking、Limited和None之间切换。一个健壮的AR应用必须根据trackingState来优雅地显示、隐藏或调整虚拟内容避免出现物体“鬼畜”闪烁或悬空的情况。3. 坐标系统深度解析从2D屏幕到3D世界坐标转换之所以令人困惑是因为在AR场景中我们同时在与多个不同的坐标系统打交道。理解它们之间的关系是进行任何精准操作的前提。3.1 四大核心坐标系统屏幕坐标系Screen Space原点设备屏幕的左下角。单位像素Pixels。坐标值Input.mousePosition或Input.GetTouch(0).position返回的就是这个坐标系下的值。例如在1080x1920的手机上右下角坐标大约是(1080, 1920)。视口坐标系Viewport Space原点设备屏幕的左下角。单位归一化的比例值范围在[0, 1]之间。坐标值将屏幕坐标除以屏幕分辨率得到。右下角永远是(1, 1)。这个坐标系与屏幕分辨率无关常用于跨设备适配的UI计算。世界坐标系World Space原点AR会话启动时相机所在的位置即设备在真实世界中的初始位置。单位Unity单位通常1单位1米。坐标值所有AR Trackable如图像、平面、人脸的transform.position以及你放置的3D模型的位置都基于这个坐标系。它是描述虚拟物体在真实空间中位置的绝对参考系。图像局部坐标系Local Space of Tracked Image原点被追踪图像的中心点。轴向通常X轴向右Y轴向上Z轴指向图像平面的法线方向即垂直于纸面向外。单位Unity单位米。坐标值以图像中心为(0,0,0)图像范围在X轴上是[-size.x/2, size.x/2]在Y轴上是[-size.y/2, size.y/2]。这是判断一个点是否“落在图像上”最自然的坐标系。3.2 关键转换函数与底层原理Unity提供了在屏幕、视口、世界坐标系之间转换的核心函数理解其输入输出至关重要Camera.ScreenPointToRay(Vector3 screenPos)输入屏幕坐标系下的一个点z值通常被忽略或用于指定深度在AR中一般用0。输出一条从相机镜头近裁剪面出发穿过屏幕该点射向世界远方的射线Ray。这条射线包含了起点origin和方向direction。原理它基于相机的投影矩阵Projection Matrix和变换矩阵View Matrix将2D屏幕点反投影Unproject到3D世界形成一条无限延伸的线。这是实现“点击选中3D物体”的基石。Camera.ScreenToWorldPoint(Vector3 screenPos)注意这个函数在传统3D项目中常用但在AR中需极其谨慎使用。因为它需要一个具体的深度值screenPos.z这个深度值代表距离相机多远的世界点。在AR中我们通常不知道一个虚拟点离相机具体多少米所以直接使用此函数往往得不到正确结果。更通用的做法是先用ScreenPointToRay获得射线再通过与其他几何体如平面求交来得到具体的世界坐标。transform.InverseTransformPoint(Vector3 worldPosition)功能将一个世界坐标系下的点转换到某个特定物体如ARTrackedImage的局部坐标系下。这是坐标转换的“万能钥匙”。当你得到一个世界空间中的点例如射线与图像平面的交点想判断它相对于图像的位置时就必须使用这个函数将其转换到以图像为中心的局部坐标系中。4. 实战实现精准的图像点击检测现在我们将把所有理论串联起来解决一个最常见的需求用户点击屏幕判断是否点中了某个正在被追踪的图像并触发相应操作。这正是网络资料中提到的核心问题。4.1 完整实现步骤拆解我们将编写一个名为TrackedImageInteractionController的完整脚本来处理这个逻辑。步骤一组件与变量准备using UnityEngine; using UnityEngine.XR.ARFoundation; using UnityEngine.XR.ARSubsystems; [RequireComponent(typeof(ARRaycastManager))] // 可选用于备用方案 public class TrackedImageInteractionController : MonoBehaviour { [SerializeField] private ARTrackedImageManager _trackedImageManager; [SerializeField] private Camera _arCamera; // 必须赋值场景中的AR相机 // 用于存储上一次被点击的图像避免同一帧内重复触发 private ARTrackedImage _lastClickedImage null; private float _lastClickTime 0f; public float clickCooldown 0.5f; // 点击冷却时间 void Start() { // 安全校验 if (_arCamera null) { _arCamera Camera.main; Debug.LogWarning(AR Camera未手动赋值已自动使用Main Camera。); } } }步骤二在Update中捕获输入并发射射线void Update() { // 处理触摸输入移动端 if (Input.touchCount 0 Input.GetTouch(0).phase TouchPhase.Began) { ProcessTouch(Input.GetTouch(0).position); } // 同时支持鼠标点击便于在Editor中测试 else if (Input.GetMouseButtonDown(0)) { ProcessTouch(Input.mousePosition); } } void ProcessTouch(Vector2 screenPosition) { // 冷却时间检查防止连点误触发 if (Time.time - _lastClickTime clickCooldown) return; // 核心将屏幕点击位置转换为一条3D射线 Ray touchRay _arCamera.ScreenPointToRay(screenPosition); // 遍历所有被追踪的图像检测点击 ARTrackedImage hitImage GetImageHitByRay(touchRay); if (hitImage ! null hitImage ! _lastClickedImage) { _lastClickedImage hitImage; _lastClickTime Time.time; OnImageClicked(hitImage); } }步骤三射线与图像平面求交的核心算法这是整个功能最核心的部分我们将其独立成一个方法。private ARTrackedImage GetImageHitByRay(Ray ray) { // 如果管理器不存在或没有追踪图像直接返回 if (_trackedImageManager null) return null; ARTrackedImage firstHitImage null; float closestHitDistance float.MaxValue; // 遍历所有可追踪的图像包括正在追踪和受限的 foreach (var trackedImage in _trackedImageManager.trackables) { // 只处理正在被稳定追踪的图像。对于Limited状态的可以根据需求决定是否交互。 if (trackedImage.trackingState ! TrackingState.Tracking) continue; // 1. 获取图像的法线方向平面朝向和中心位置 Vector3 planeNormal trackedImage.transform.forward; // 图像平面的法线垂直于平面 Vector3 planeCenter trackedImage.transform.position; // 2. 使用Unity的Plane类构造一个无限大的平面 // Plane的构造函数需要法线和平面上任意一点 Plane imagePlane new Plane(planeNormal, planeCenter); // 3. 计算射线与该平面的交点 // Raycast方法返回是否相交并通过out参数返回从射线原点到交点的距离 if (imagePlane.Raycast(ray, out float enterDistance)) { // 4. 如果相交计算交点在世界坐标系中的具体位置 Vector3 worldHitPoint ray.GetPoint(enterDistance); // 5. 将世界坐标点转换到图像的局部坐标系 Vector3 localHitPoint trackedImage.transform.InverseTransformPoint(worldHitPoint); // 6. 获取图像的实际半宽半高单位米 // trackedImage.size 是在Reference Image Library中设置的物理尺寸 float halfWidth trackedImage.size.x / 2f; float halfHeight trackedImage.size.y / 2f; // 7. 判断局部坐标点是否在图像矩形范围内忽略Z轴只考虑XY平面 // 使用Mathf.Abs取绝对值判断是否在中心对称的矩形内 bool isWithinWidth Mathf.Abs(localHitPoint.x) halfWidth; bool isWithinHeight Mathf.Abs(localHitPoint.y) halfHeight; if (isWithinWidth isWithinHeight) { // 找到命中的图像但我们还需要找到距离相机最近的那个因为射线可能穿过多个重叠图像 if (enterDistance closestHitDistance) { closestHitDistance enterDistance; firstHitImage trackedImage; } } } } return firstHitImage; }步骤四处理点击事件private void OnImageClicked(ARTrackedImage clickedImage) { string imageName clickedImage.referenceImage.name; Debug.Log($colorgreen成功点击图像{imageName}/color); // 这里可以触发任何自定义逻辑例如 // 1. 显示/隐藏与该图像关联的3D模型 // 2. 播放一个粒子特效或动画 // 3. 弹出与该图像内容相关的UI面板 // 4. 发送网络请求获取更多信息 // 示例在图像上方实例化一个提示特效 SpawnClickEffect(clickedImage.transform.position); // 示例触发特定图像的行为 switch (imageName) { case ProductPoster: ShowProduct3DModel(clickedImage); break; case MapMarker: NavigateToLocation(clickedImage); break; default: ToggleGenericARContent(clickedImage); break; } } // 一个简单的点击反馈效果 void SpawnClickEffect(Vector3 worldPosition) { // 可以是一个预制体包含粒子系统和声音 GameObject effectPrefab Resources.LoadGameObject(ClickEffect); if (effectPrefab ! null) { Instantiate(effectPrefab, worldPosition, Quaternion.identity); } }4.2 关键细节与避坑指南trackingState的判断至关重要在GetImageHitByRay方法中我们只处理TrackingState.Tracking状态的图像。这是因为只有处于稳定追踪状态时图像的transform位置和旋转以及size才是可靠且连续的。如果图像状态是Limited例如图像只有一部分在视野内其transform可能会抖动或跳跃此时进行点击检测会导致体验不佳。你可以根据应用场景决定是否放宽这个条件。图像尺寸trackedImage.size的来源这个尺寸直接来源于你在Unity Editor中创建Reference Image Library时为每张图片设置的Physical Size。如果设置错误例如把一张名片大小的图设成了1米宽那么你的点击检测范围也会同比放大或缩小导致交互区域严重错位。务必确保物理尺寸设置准确。处理多个重叠图像我们的代码通过closestHitDistance找到了射线最先击中的即距离相机最近的图像。这是符合直觉的行为用户点击时他们想交互的应该是视觉上最前面的那个物体。如果你的应用需要同时选中多个图像可以修改逻辑将命中的所有图像添加到一个列表中一并处理。性能优化考虑在Update中遍历所有trackables在图像数量不多时10没有问题。如果同时追踪的图像非常多可以考虑进行空间划分优化例如只对视野中心一定范围内的图像进行检测或者使用ARRaycastManager配合RaycastFilter进行初步筛选。在Unity Editor中测试由于AR设备依赖在Editor中直接运行可能无法获得ARTrackedImage。你可以编写模拟代码在按下特定键时在场景中某个测试物体的位置模拟一个图像被点击的事件从而测试你的点击反馈逻辑是否正确。5. 坐标转换的进阶应用在图像指定局部位置放置物体点击检测只是坐标转换的一个应用。另一个更常见的需求是在追踪到的图像上相对于图像本身的某个固定位置例如右上角、左下角的一个图标处放置一个虚拟物体并且当图像移动时这个物体要随之移动保持相对位置不变。5.1 计算图像局部坐标对应的世界坐标假设我们想在图像的局部坐标(localX, localY, 0)处放置一个物体其中localX和localY的取值范围在[-size.x/2, size.x/2]和[-size.y/2, size.y/2]之间。public GameObject contentPrefab; // 要放置的虚拟物体预制体 private DictionaryARTrackedImage, GameObject _spawnedObjects new DictionaryARTrackedImage, GameObject(); void UpdateARContent(ARTrackedImage trackedImage) { // 检查是否已经为该图像创建了物体 if (!_spawnedObjects.TryGetValue(trackedImage.trackableId, out GameObject contentObject)) { // 如果没有则实例化一个新的 contentObject Instantiate(contentPrefab, Vector3.zero, Quaternion.identity); _spawnedObjects.Add(trackedImage.trackableId, contentObject); } // 关键步骤将局部坐标转换为世界坐标 // 假设我们想把物体放在图像的右上角距离右边缘10%上边缘10%的位置 float offsetRatioX 0.4f; // 半宽为0.50.4表示距离中心点80%半宽的位置即非常靠近右边 float offsetRatioY 0.4f; // 同上靠近上边 float localTargetX trackedImage.size.x / 2f * offsetRatioX; float localTargetY trackedImage.size.y / 2f * offsetRatioY; Vector3 localPosition new Vector3(localTargetX, localTargetY, 0); // Z0表示在图像平面上 // 使用Transform.TransformPoint将局部坐标转换为世界坐标 Vector3 worldPosition trackedImage.transform.TransformPoint(localPosition); // 设置物体的位置 contentObject.transform.position worldPosition; // 通常我们也希望物体的旋转与图像对齐。 // 如果你希望物体“站立”在图像上如一个3D模型可能需要调整旋转。 // 这里让物体与图像保持相同的朝向。 contentObject.transform.rotation trackedImage.transform.rotation; // 让物体始终面向相机Billboard效果如果需要的话 // contentObject.transform.LookAt(_arCamera.transform); // contentObject.transform.rotation Quaternion.Euler(0, contentObject.transform.rotation.eulerAngles.y 180, 0); }原理剖析Transform.TransformPoint(Vector3 localPosition)是InverseTransformPoint的逆操作。它接收一个在自身局部坐标系下的点然后根据自身的旋转、缩放和平移计算出这个点在父级世界坐标系下的位置。这正是将“图像上的一个固定点”映射到“不断变化的真实世界空间”的数学桥梁。5.2 处理图像旋转与物体朝向上面的例子让虚拟物体完全平行于图像平面。但很多时候我们希望物体垂直于图像“站立”起来比如一个从海报里跳出来的角色。这时就需要额外的旋转计算。void PlaceObjectUprightOnImage(ARTrackedImage trackedImage, GameObject obj, Vector2 localOffset) { // 1. 计算世界位置同上 Vector3 localPos new Vector3(localOffset.x, localOffset.y, 0); obj.transform.position trackedImage.transform.TransformPoint(localPos); // 2. 计算朝向我们希望物体“向上”是世界Y轴或图像平面的法线方向的反方向并看向相机。 // 图像平面的法线是 trackedImage.transform.forward // 我们想要物体“站立”即它的向上方向应该垂直于图像平面但通常我们定义模型的“上”是Y轴。 // 一个简单的方法是让物体与图像有相同的旋转然后绕X轴旋转90度假设模型初始是“站立”姿态。 // 但这取决于你的预制体的初始朝向。 // 更通用的方法使用LookRotation来构建一个旋转。 // 假设我们希望物体的Z轴前向与图像的法线forward相反即面向图像外物体的Y轴上与世界Y轴对齐。 Vector3 desiredForward -trackedImage.transform.forward; // 面向图像外部 Vector3 desiredUp Vector3.up; // 使用世界朝上 // 但如果图像是倾斜的比如贴在墙上你可能希望物体沿着图像平面“向上”即图像的Y轴。 // desiredUp trackedImage.transform.up; // 使用Quaternion.LookRotation来创建这个旋转 obj.transform.rotation Quaternion.LookRotation(desiredForward, desiredUp); }实操心得虚拟物体的朝向是AR体验中“真实感”的关键。一个物体是“飘”在图像上还是“粘”在图像上区别往往就在这几行旋转计算的代码里。务必根据你的3D模型自身的轴向和期望的视觉效果来调整LookRotation的参数。在开发过程中频繁地在真机上测试不同角度的图像是调试朝向问题最有效的方法。6. 常见问题排查与性能优化技巧即使代码逻辑正确在实际开发中你仍会遇到各种棘手问题。以下是我从多个项目中总结出的“避坑清单”。6.1 点击无响应或位置不准问题现象可能原因排查步骤与解决方案完全点击不到任何图像1._arCamera变量未正确赋值。2.ARTrackedImageManager未赋值或未启用。3. 图像未被成功追踪trackingState不是Tracking。4. 射线检测代码逻辑错误如平面法线方向弄反。1. 在Inspector中确认相机和管理器已拖拽赋值。2. 在OnTrackedImagesChanged事件中添加日志确认图像是否进入Tracking状态。3. 在GetImageHitByRay方法开始处添加Debug.DrawRay在Scene视图中可视化射线看它是否穿过图像。4. 检查Plane构造函数的法线方向确保是transform.forward。点击位置有偏移感觉“点不准”1.参考图像的物理尺寸Physical Size设置错误。这是最常见的原因。2. 图像局部坐标系的原点理解有误以为是角上其实是中心。3. 屏幕点击坐标获取有误触摸相位处理不对。1.用尺子量一下现实中图片的尺寸在Unity中精确设置。这是根治方法。2. 在图像中心实例化一个Cube作为参考看点击逻辑判断的中心点是否与Cube重合。3. 确保只在TouchPhase.Began或GetMouseButtonDown时处理点击避免一帧内多次触发。只有图像中心很小区域能点击到计算局部坐标时判断条件写成了localPoint.x halfWidth localPoint.y halfHeight缺少绝对值Mathf.Abs导致只检测了第一象限右上角。修正判断条件为Mathf.Abs(localPoint.x) halfWidth Mathf.Abs(localPoint.y) halfHeight。当图像旋转后点击区域错乱使用了错误的轴向来计算平面。图像旋转后其局部坐标系的X和Y轴也随之旋转但我们的点击检测逻辑基于局部XY平面依然有效因为InverseTransformPoint已经考虑了旋转。如果错乱可能是用世界坐标的某个平面进行了错误计算。坚持使用trackedImage.transform.InverseTransformPoint方法它能正确处理所有旋转。避免尝试自己用点积等复杂数学去计算。6.2 虚拟物体位置抖动或漂移问题现象可能原因排查步骤与解决方案放置在图像上的物体轻微但持续抖动这是AR追踪本身的正常现象。视觉惯性里程计VIO会有毫米级的噪声。1.对位置进行平滑滤波。不要直接将虚拟物体的位置设置为trackedImage.transform.position而是使用插值Lerp或低通滤波器。csharpbr// 在Update中br_smoothedPosition Vector3.Lerp(_smoothedPosition, targetWorldPosition, Time.deltaTime * smoothSpeed);brcontentObject.transform.position _smoothedPosition;br2. 适当降低ARTrackedImageManager的Update Frequency但这会影响追踪响应速度。物体会突然“跳”到另一个位置1. 图像追踪丢失后重新找回姿态估计发生了“重定位”Relocalization。2. 环境光线剧烈变化导致特征点匹配出现歧义。1. 在OnTrackedImagesChanged的updated事件中当状态从None或Limited变为Tracking时不要立刻显示物体可以加入一个淡入动画或延迟避免视觉跳跃。2. 优化参考图像增加其独特性和抗光照变化能力。在光照条件差的环境下提示用户。多个关联物体之间的相对位置不对每个物体都独立地根据图像姿态计算自己的世界坐标浮点数误差累积可能导致相对位置轻微偏移。如果多个物体需要保持固定的相对布局不要为每个物体单独计算世界坐标。应该只以一个物体或图像本身为父节点其他物体作为其子物体在局部坐标系内设置相对位置。这样它们的相对关系由Unity的变换层级保证绝对位置随父节点一起更新。6.3 性能优化与高级技巧减少不必要的遍历在Update中遍历所有trackables是O(n)操作。如果场景中可追踪图像很多可以在OnTrackedImagesChanged事件中维护两个列表一个ListARTrackedImage用于存储当前处于Tracking状态的图像另一个存储Limited状态。在点击检测时只遍历Tracking列表。使用空间数据结构对于需要频繁进行点击检测的复杂场景可以考虑将图像的位置信息同步到一个空间数据结构中如四叉树2D或八叉树3D用于快速筛选出点击点附近的图像而不是遍历全部。合并射线检测如果你的场景中既有图像需要点击也有AR平面或其他可交互物体可以考虑使用Unity的物理射线检测Physics.Raycast或ARRaycastManager进行一次检测然后通过碰撞体或返回的ARRaycastHit来判断命中类型避免多套检测逻辑。处理图像比例不一致有时参考图的长宽比与实际打印的图片可能不完全一致例如图片被拉伸打印。这会导致trackedImage.size.x / size.y与预期不符。一个容错性更强的点击判断是使用归一化的局部坐标Vector3 normalizedLocalPoint new Vector3(localPoint.x / halfWidth, localPoint.y / halfHeight, 0); if (Mathf.Abs(normalizedLocalPoint.x) 1 Mathf.Abs(normalizedLocalPoint.y) 1) { ... }但这依然要求宽高比例误差不能太大否则交互区域形状会失真。在图像丢失时保留内容对于一些应用如将虚拟物体“放置”在图像曾经所在的位置即使图像移出视野也希望虚拟物体留在原地。你可以这样做当图像状态变为Limited或None时不再更新虚拟物体的位置和旋转而是让其保留在最后一帧的已知位置。同时可以提供一个手动重置的按钮。

相关新闻

3个颠覆性功能重塑你的免费音乐体验:MusicFree插件生态完全指南

3个颠覆性功能重塑你的免费音乐体验:MusicFree插件生态完全指南

3个颠覆性功能重塑你的免费音乐体验:MusicFree插件生态完全指南 【免费下载链接】MusicFreePlugins MusicFree播放插件 项目地址: https://gitcode.com/gh_mirrors/mu/MusicFreePlugins 还在为不同音乐平台的会员费而烦恼吗?厌倦了在多个应用间切…

2026/7/16 4:21:27阅读更多 →
2026勰码金牌夏令营济南A-营模拟赛二补题报告

2026勰码金牌夏令营济南A-营模拟赛二补题报告

2026勰码金牌夏令营济南A-营模拟赛二补题报告S12316 明宗亮一、比赛过程1、赛前估分赛前估分T1 双数组构造T2 染色T3 动态规划T4 分形图总分10 pts10 pts10 pts16pts46 pts2、实际分数实际分数T1 双数组构造T2 染色T3 动态规划T4 分形图总分10 pts10 pts10 pts0 pts30 pts3、赛…

2026/7/16 4:21:27阅读更多 →
从零到一:揭秘开源H5编辑器的技术架构与创作哲学

从零到一:揭秘开源H5编辑器的技术架构与创作哲学

从零到一:揭秘开源H5编辑器的技术架构与创作哲学 【免费下载链接】h5maker h5编辑器类似maka、易企秀 账号/密码:admin 项目地址: https://gitcode.com/gh_mirrors/h5/h5maker 在移动互联网内容创作领域,H5页面已成为连接品牌与用户的…

2026/7/16 4:16:26阅读更多 →
为了监控国产数据库,我干碎了3台服务器!直到我用C#零拷贝+IMemoryOwner逆天改命 [特殊字符]

为了监控国产数据库,我干碎了3台服务器!直到我用C#零拷贝+IMemoryOwner逆天改命 [特殊字符]

🩸 一、 翻车现场:为什么 byte[] 是可观测性的“万恶之源”? 很多新手老铁写网络数据接收,上来就是 new byte[8192]。 兄弟,在低并发场景下,这没毛病。但在国产数据库这种每秒几十万条SQL、几万个并发连接的…

2026/7/16 5:31:32阅读更多 →
C++实现MusicXML解析与乐谱可视化引擎:从数据到图形

C++实现MusicXML解析与乐谱可视化引擎:从数据到图形

1. 项目概述:当C遇见乐谱如果你是一个既写代码又玩音乐的程序员,或者你正在开发一个音乐教育、打谱、或者音乐分析相关的软件,那么“解析MusicXML”这个需求迟早会找上你。MusicXML作为数字乐谱交换的事实标准,其地位就像图像领域…

2026/7/16 5:31:32阅读更多 →
C++ HTTP客户端实战:libcurl与cpp-httplib选型、集成与性能优化指南

C++ HTTP客户端实战:libcurl与cpp-httplib选型、集成与性能优化指南

1. 项目概述:为什么要在C里折腾HTTP请求?在C的世界里,我们习惯了和内存、指针、模板这些底层玩意儿打交道,处理网络请求听起来像是Python、Java这些“高级语言”的活儿。但现实是,越来越多的C项目,无论是游…

2026/7/16 5:31:32阅读更多 →
C++线程调度优化:深入解析std::this_thread::yield()的正确使用场景与性能调优

C++线程调度优化:深入解析std::this_thread::yield()的正确使用场景与性能调优

1. 项目概述:为什么我们需要主动让出CPU? 在C多线程编程的世界里,我们常常关注如何让线程高效地“跑起来”——创建线程、加锁、同步。但一个同样重要却容易被忽视的问题是: 如何让线程在合适的时机“停下来”或“让一让” 。 …

2026/7/16 5:31:32阅读更多 →
思科路由器实战配置:从基础到高级的完整命令手册

思科路由器实战配置:从基础到高级的完整命令手册

1. 思科路由器基础配置入门刚拿到一台全新的思科路由器时,就像面对一台未组装的电脑,需要先完成基础配置才能让它正常工作。这些基础配置包括设置主机名、管理密码、接口IP等,相当于给设备"上户口"和"配钥匙"。设备初始化…

2026/7/16 5:31:32阅读更多 →
【HarmonyOS 5 入门系列】鸿蒙 HarmonyOS 示例项目讲解

【HarmonyOS 5 入门系列】鸿蒙 HarmonyOS 示例项目讲解

前言对于刚开始学习 HarmonyOS 的开发者来说,官方 Sample(示例项目) 是最好的学习资料。很多开发者都有这样的困惑:学了 ArkTS 语法,不知道如何写完整项目会写页面,不知道如何组织工程DataPreferences、Rou…

2026/7/16 5:26:32阅读更多 →
VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异当你在VSCode中启动一个新的TypeScript项目时,第一个技术决策往往从安装方式开始。这个看似简单的选择——全局安装还是项目本地安装——实际上会深刻影响你的开发流程、团队协作和…

2026/7/15 6:42:19阅读更多 →
智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 智慧树刷课插件是一款专为智慧树在线教育平台设计的Chrome浏…

2026/7/15 6:12:45阅读更多 →
Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 你是否在GOG或Epic Games Store购买了心仪的游戏…

2026/7/15 10:54:00阅读更多 →
A--10 Codex Review与GitHub PR工作流实战指南:从代码审查到安全合并

A--10 Codex Review与GitHub PR工作流实战指南:从代码审查到安全合并

摘要:本文系统讲解如何利用Codex App的Review功能与GitHub PR工作流,实现从代码修改到安全合并的完整流程。涵盖Review面板深度使用、/review命令实战、GitHub Connector配置、PR描述撰写技巧,以及常见问题排查方法。通过多个实战案例和流程图,帮助开发者建立高效的AI辅助代…

2026/7/16 0:00:38阅读更多 →
遗传算法解5皇后问题:从Hello World到工业优化的进化实验室

遗传算法解5皇后问题:从Hello World到工业优化的进化实验室

1. 项目概述:为什么用遗传算法解5皇后问题,而不是直接回溯?我带过十几届算法课,也给不少初创团队做过AI架构咨询。每次讲到组合优化问题,学生和工程师的第一反应永远是“写个回溯试试”。这没错——55棋盘上找所有合法…

2026/7/16 0:00:38阅读更多 →
5.1V稳压管输出为何只有4.7V?工作电流与负载影响分析

5.1V稳压管输出为何只有4.7V?工作电流与负载影响分析

前几天调试一个简单的电源模块,用到了5.1V稳压管。电路接好,上电测试,万用表一量——输出居然只有4.7V。第一反应是稳压管坏了,换了一个新的,结果还是4.7V。这让我想起很多初学者都会遇到的困惑:明明标称5.…

2026/7/16 0:00:38阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/15 15:50:47阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/15 8:52:38阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/15 14:06:23阅读更多 →