第五章:ARCore核心技术与商业级应用开发实践

5.1 ARCore技术架构与开发环境部署

增强现实(AR)技术通过在真实世界视图上叠加数字内容,为用户创造了全新的交互体验。ARCore作为谷歌推出的AR开发平台,其核心在于理解移动设备相对于周围环境的位置与姿态,进而实现虚拟内容的精准放置与稳定追踪。这一过程主要依赖于三个关键技术:运动追踪(Motion Tracking)、环境理解(Environmental Understanding)和光线估计(Light Estimation)。

在商业项目中,例如开发一款用于零售的商品可视化应用,稳定且精准的环境理解是基石。ARCore的运动追踪功能通过读取设备上的惯性测量单元(IMU)数据和分析摄像头捕获的图像特征点,计算出设备在三维空间中的六自由度位姿(包括位置和旋转)。环境理解则允许应用检测水平表面(如地面、桌面)的大小与位置,这是放置虚拟物体的先决条件。光线估计通过分析摄像头画面,推测环境光的方向与强度,使得虚拟物体的阴影与高光能够与真实环境融合,显著提升视觉真实感。

为了开始开发,首先需要在Unity 2021.3.8f1c1中配置开发环境。第一步是通过Package Manager导入ARCore XR Plugin包及其相关的AR Foundation包,后者提供了跨AR平台的统一API。接下来需要进行关键的项目设置。

下面是一个商业项目中常见的初始化脚本,用于检查设备兼容性并启动AR会话。该脚本通常附着在场景中一个核心的管理器游戏对象上。

using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ArSubsystems;
using System.Collections.Generic;

namespace CommercialARProject
{
    /// <summary>
    /// AR会话管理器,负责ARCore初始化、会话生命周期和设备兼容性检查。
    /// 这是一个商业项目中典型的单例模式应用。
    /// </summary>
    public class ARSessionManager : MonoBehaviour
    {
        public static ARSessionManager Instance;

        [Header("AR 基础组件")]
        [SerializeField]
        private ARSessionOrigin arSessionOrigin;
        [SerializeField]
        private ARSession arSession;

        [Header("UI 反馈")]
        [SerializeField]
        private GameObject compatibilityPanel;
        [SerializeField]
        private UnityEngine.UI.Text statusText;

        private List<InputDevice> inputDevices = new List<InputDevice>();

        /// <summary>
        /// 初始化单例并确保场景切换时不销毁。
        /// </summary>
        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
                return;
            }
            Instance = this;
            DontDestroyOnLoad(gameObject);

            if (arSessionOrigin == null)
            {
                arSessionOrigin = FindObjectOfType<ARSessionOrigin>();
            }
            if (arSession == null)
            {
                arSession = FindObjectOfType<ARSession>();
            }
        }

        /// <summary>
        /// 启动AR会话前的准备工作,包括兼容性检查。
        /// </summary>
        private IEnumerator Start()
        {
            // 等待一帧,确保所有组件已加载完毕
            yield return null;

            statusText.text = "正在检查ARCore支持与设备兼容性...";

            // 检查当前设备平台
            if (Application.platform != RuntimePlatform.Android)
            {
                Debug.LogWarning("商业演示:当前为非Android平台,部分ARCore功能将受限。");
                statusText.text = "当前为演示模式(非Android设备)";
                // 在商业项目中,这里可能会进入一个模拟AR模式或提供有限功能
                yield break;
            }

            // 异步检查ARCore可用性及是否需要安装或更新
            AsyncTask<ArAvailability> availabilityTask = ARCoreSession.CheckAvailability();
            yield return availabilityTask.WaitForCompletion();

            if (availabilityTask.Result == ArAvailability.UnsupportedDeviceNotCapable)
            {
                HandleIncompatibleDevice("您的设备不支持ARCore。");
                yield break;
            }

            AsyncTask<SessionInstallStatus> installTask = ARCoreSession.RequestInstall(!availabilityTask.Result.IsSupported());
            yield return installTask.WaitForCompletion();

            if (installTask.Result != SessionInstallStatus.Success)
            {
                HandleIncompatibleDevice("ARCore服务安装或更新失败。");
                yield break;
            }

            // 所有检查通过,启动AR会话
            StartARSession();
        }

        /// <summary>
        /// 启动AR会话并注册相关事件。
        /// </summary>
        private void StartARSession()
        {
            if (arSession == null)
            {
                Debug.LogError("ARSession 组件未找到!");
                return;
            }

            arSession.enabled = true;
            statusText.text = "正在初始化AR会话...";

            // 监听会话状态变化,这在商业应用中对于错误处理和用户引导至关重要
            arSession.onSessionStateChanged += OnSessionStateChanged;
        }

        /// <summary>
        /// 处理AR会话状态变化事件。
        /// </summary>
        private void OnSessionStateChanged(ARSessionStateChangedEventArgs eventArgs)
        {
            Debug.Log($"商业AR会话状态变更为: {eventArgs.state}");

            switch (eventArgs.state)
            {
                case ARSessionState.SessionTracking:
                    statusText.text = "AR会话运行正常。请缓慢移动设备以识别环境。";
                    compatibilityPanel.SetActive(false);
                    // 通知其他系统组件AR已就绪
                    EventSystem.Instance.BroadcastARReady();
                    break;
                case ARSessionState.Unsupported:
                    HandleIncompatibleDevice("当前设备状态不支持AR。");
                    break;
                case ARSessionState.NeedsInstall:
                    statusText.text = "需要安装ARCore服务。";
                    break;
                case ARSessionState.CheckingAvailability:
                case ARSessionState.Ready:
                case ARSessionState.SessionInitializing:
                    // 这些是中间状态,可以更新UI提示用户耐心等待
                    statusText.text = "正在准备AR环境...";
                    break;
                default:
                    break;
            }
        }

        /// <summary>
        /// 处理设备不兼容的情况,引导用户或提供备选方案。
        /// </summary>
        private void HandleIncompatibleDevice(string message)
        {
            Debug.LogError($"设备不兼容: {message}");
            statusText.text = message;
            if (compatibilityPanel != null)
            {
                compatibilityPanel.SetActive(true);
            }
            // 在商业项目中,这里可能会触发一个降级体验,如切换到3D模型查看模式
            arSession.enabled = false;
        }

        /// <summary>
        /// 提供一个公共方法以允许用户手动重试会话初始化。
        /// </summary>
        public void RetrySessionInitialization()
        {
            if (arSession != null)
            {
                arSession.Reset();
                StartARSession();
            }
        }

        private void OnDestroy()
        {
            if (arSession != null)
            {
                arSession.onSessionStateChanged -= OnSessionStateChanged;
            }
        }
    }
}

5.2 图像识别与交互式产品展示

图像识别(Image Tracking)是ARCore的一项关键功能,它允许应用通过设备摄像头识别特定的二维图片,并在其上方或周围锚定数字内容。在商业领域,此技术被广泛用于营销、教育、工业维护和零售。例如,一家家具零售商可以开发一款应用,让用户扫描产品目录上的图片,即可在真实世界中看到该家具的1:1比例3D模型,并能围绕其走动观察。

实现这一功能首先需要创建“参考图像库”(Reference Image Library)。在Unity中,可以通过XR Reference Image Library资产来管理。添加参考图像时,需提供高质量的.jpg或.png文件,并设置其物理尺寸(以米为单位),这将决定虚拟内容出现的比例。ARCore会根据该图像生成特征数据库并打包进应用。

当应用运行时,ARTrackedImageManager组件负责管理图像追踪。它为每个检测到的参考图像创建ARTrackedImage对象,该对象包含了图像的追踪状态(如Tracking、Limited、None)及其在现实世界中的变换信息(位置、旋转、大小)。

以下是一个商业级交互式产品展示的完整案例。假设我们为一家汽车公司开发应用,用户扫描宣传册上的汽车图片,即可展示该汽车的3D模型,并允许用户通过UI按钮切换颜色或打开车门。

using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using System.Collections.Generic;

namespace CommercialARProject
{
    /// <summary>
    /// 图像识别追踪管理器,负责处理产品图像的识别、内容实例化与交互。
    /// </summary>
    public class ProductImageTracker : MonoBehaviour
    {
        [SerializeField]
        private ARTrackedImageManager trackedImageManager;

        [Header("产品配置")]
        [SerializeField]
        private List<ProductConfiguration> productConfigurations;

        [Header("UI 控制")]
        [SerializeField]
        private ProductUIController productUIController;

        // 字典:以参考图片名称为键,存储对应的产品配置和当前实例化的物体
        private Dictionary<string, TrackedProductInfo> trackedProducts = new Dictionary<string, TrackedProductInfo>();

        private void Awake()
        {
            if (trackedImageManager == null)
            {
                trackedImageManager = FindObjectOfType<ARTrackedImageManager>();
            }

            // 预初始化字典
            foreach (var config in productConfigurations)
            {
                if (!trackedProducts.ContainsKey(config.referenceImageName))
                {
                    trackedProducts[config.referenceImageName] = new TrackedProductInfo(config);
                }
            }
        }

        private void OnEnable()
        {
            if (trackedImageManager != null)
            {
                trackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
            }
        }

        private void OnDisable()
        {
            if (trackedImageManager != null)
            {
                trackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
            }
        }

        /// <summary>
        /// 处理追踪图像变化的核心事件。
        /// </summary>
        private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
        {
            // 处理新识别到的图像
            foreach (var trackedImage in eventArgs.added)
            {
                UpdateTrackedProduct(trackedImage);
            }

            // 处理已更新(状态或位置改变)的图像
            foreach (var trackedImage in eventArgs.updated)
            {
                UpdateTrackedProduct(trackedImage);
            }

            // 处理丢失的图像
            foreach (var trackedImage in eventArgs.removed)
            {
                string imageName = trackedImage.referenceImage.name;
                if (trackedProducts.TryGetValue(imageName, out TrackedProductInfo info))
                {
                    // 在商业项目中,我们可能不希望立即销毁物体,而是可以将其淡出或移至一旁。
                    if (info.spawnedObject != null)
                    {
                        info.spawnedObject.SetActive(false);
                        productUIController.HideUIForProduct(imageName);
                    }
                    info.currentTrackingState = TrackingState.None;
                }
            }
        }

        /// <summary>
        /// 更新或创建被追踪产品对应的3D内容。
        /// </summary>
        private void UpdateTrackedProduct(ARTrackedImage trackedImage)
        {
            string imageName = trackedImage.referenceImage.name;

            if (!trackedProducts.TryGetValue(imageName, out TrackedProductInfo productInfo))
            {
                Debug.LogWarning($"未找到图片 '{imageName}' 的产品配置。");
                return;
            }

            // 根据追踪状态决定行为
            if (trackedImage.trackingState == TrackingState.Tracking && trackedImage.trackingState != productInfo.currentTrackingState)
            {
                // 成功追踪:确保3D模型被实例化并放置在正确位置
                if (productInfo.spawnedObject == null)
                {
                    GameObject productPrefab = productInfo.configuration.productPrefab;
                    if (productPrefab != null)
                    {
                        productInfo.spawnedObject = Instantiate(productPrefab, trackedImage.transform.position, trackedImage.transform.rotation);
                        productInfo.spawnedObject.transform.localScale = Vector3.one * productInfo.configuration.scaleMultiplier;

                        // 将实例化的物体作为追踪图像的子物体,这样它会自动跟随图像移动/旋转
                        productInfo.spawnedObject.transform.SetParent(trackedImage.transform, false);

                        // 初始化产品特定的交互组件
                        IProductInteractable interactable = productInfo.spawnedObject.GetComponent<IProductInteractable>();
                        if (interactable != null)
                        {
                            interactable.Initialize(productInfo.configuration.productId);
                        }

                        // 通知UI控制器显示对应产品的UI面板
                        productUIController.ShowUIForProduct(imageName, productInfo.configuration);
                    }
                }
                else
                {
                    // 如果物体已存在,确保其可见并更新位置(尽管作为子物体通常会自动更新)
                    productInfo.spawnedObject.SetActive(true);
                    productInfo.spawnedObject.transform.SetParent(trackedImage.transform, false);
                }
                productInfo.currentTrackingState = TrackingState.Tracking;
            }
            else if ((trackedImage.trackingState == TrackingState.Limited || trackedImage.trackingState == TrackingState.None) 
                     && productInfo.currentTrackingState == TrackingState.Tracking)
            {
                // 追踪受限或丢失:隐藏物体但保持其父子关系,以便追踪恢复时快速显示
                if (productInfo.spawnedObject != null)
                {
                    productInfo.spawnedObject.SetActive(false);
                    productUIController.HideUIForProduct(imageName);
                }
                productInfo.currentTrackingState = trackedImage.trackingState;
            }
        }

        /// <summary>
        /// 从外部UI调用的方法,用于改变当前展示产品的属性(如颜色)。
        /// </summary>
        public void ChangeProductColor(string productImageName, Color newColor)
        {
            if (trackedProducts.TryGetValue(productImageName, out TrackedProductInfo info) && info.spawnedObject != null)
            {
                IProductInteractable interactable = info.spawnedObject.GetComponent<IProductInteractable>();
                interactable?.ChangeColor(newColor);
            }
        }

        /// <summary>
        /// 从外部UI调用的方法,用于触发产品动画(如打开车门)。
        /// </summary>
        public void TriggerProductAnimation(string productImageName, string animationTrigger)
        {
            if (trackedProducts.TryGetValue(productImageName, out TrackedProductInfo info) && info.spawnedObject != null)
            {
                IProductInteractable interactable = info.spawnedObject.GetComponent<IProductInteractable>();
                interactable?.TriggerAnimation(animationTrigger);
            }
        }
    }

    /// <summary>
    /// 存储被追踪产品的运行时信息。
    /// </summary>
    [System.Serializable]
    public class TrackedProductInfo
    {
        public ProductConfiguration configuration;
        public GameObject spawnedObject;
        public TrackingState currentTrackingState;

        public TrackedProductInfo(ProductConfiguration config)
        {
            configuration = config;
            currentTrackingState = TrackingState.None;
        }
    }

    /// <summary>
    /// 产品配置数据,可在编辑器中配置。
    /// </summary>
    [System.Serializable]
    public class ProductConfiguration
    {
        public string productId;
        public string referenceImageName; // 必须与XR Reference Image Library中的名称一致
        public GameObject productPrefab;
        [Tooltip("相对于识别图片物理尺寸的缩放倍数")]
        public float scaleMultiplier = 1.0f;
        public string displayName;
        public Color[] availableColors;
    }

    /// <summary>
    /// 产品交互接口,确保不同产品的3D预制体有一致的交互方法。
    /// </summary>
    public interface IProductInteractable
    {
        void Initialize(string productId);
        void ChangeColor(Color newColor);
        void TriggerAnimation(string triggerName);
    }
}

5.3 平面检测与空间内容布局

平面检测(Plane Detection)是ARCore环境理解能力的具体体现,它使应用能够识别现实世界中的水平或垂直表面,为虚拟物体提供稳定的放置平台。在商业应用中,此功能是室内设计、虚拟摆放、空间规划和游戏等场景的核心。例如,一家装饰公司可以开发一款应用,让用户将虚拟的沙发、茶几、画作等物品放置到自家客厅的平面上,直观地预览装修效果。

ARCore通过检测特征点和平面区域,生成ARPlane对象,该对象描述了平面的位置(Pose)、尺寸(Extents)、法线方向以及边界多边形(Boundary)。平面有水平和垂直两种对齐方式(PlaneAlignment),并且其状态(如TrackingState)会随着设备对环境的探索而更新。

一个健壮的商业级平面检测与放置系统需要处理以下关键点:平面筛选(如只识别水平面或足够大的平面)、可视化反馈(高亮可放置区域)、用户交互(通过触摸屏幕放置物体)以及虚拟物体与平面的物理适配(如让物体稳稳“坐”在桌面上)。

以下是一个室内设计应用的商业实例代码,演示如何管理平面、处理用户输入以放置家具模型,并确保物体与平面正确贴合。

using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using System.Collections.Generic;

namespace CommercialARProject
{
    /// <summary>
    /// 空间布局管理器:处理平面检测、用户放置交互和已放置物体的管理。
    /// </summary>
    public class SpatialLayoutManager : MonoBehaviour
    {
        [SerializeField]
        private ARPlaneManager arPlaneManager;
        [SerializeField]
        private ARRaycastManager arRaycastManager;
        [SerializeField]
        private ARSessionOrigin arSessionOrigin;

        [Header("放置设置")]
        [SerializeField]
        private GameObject placementIndicatorPrefab;
        [SerializeField]
        private LayerMask planeLayerMask;
        [SerializeField]
        private float maxPlacementDistance = 5.0f;

        [Header("家具数据库")]
        [SerializeField]
        private List<FurnitureItem> furnitureCatalog;
        [SerializeField]
        private FurnitureSelectionUI selectionUI;

        private GameObject currentPlacementIndicator;
        private Pose placementPose;
        private bool isPlacementPoseValid = false;
        private FurnitureItem selectedFurniture;
        private Dictionary<GameObject, FurnitureInstanceData> placedFurniture = new Dictionary<GameObject, FurnitureInstanceData>();

        private void Start()
        {
            if (arPlaneManager == null) arPlaneManager = FindObjectOfType<ARPlaneManager>();
            if (arRaycastManager == null) arRaycastManager = FindObjectOfType<ARRaycastManager>();
            if (arSessionOrigin == null) arSessionOrigin = FindObjectOfType<ARSessionOrigin>();

            // 初始化放置指示器
            if (placementIndicatorPrefab != null)
            {
                currentPlacementIndicator = Instantiate(placementIndicatorPrefab);
                currentPlacementIndicator.SetActive(false);
            }

            // 订阅UI事件
            if (selectionUI != null)
            {
                selectionUI.OnFurnitureSelected += HandleFurnitureSelected;
            }

            // 初始可选第一件家具
            if (furnitureCatalog.Count > 0)
            {
                selectedFurniture = furnitureCatalog[0];
            }
        }

        private void Update()
        {
            // 如果没有选中家具,则不更新放置逻辑
            if (selectedFurniture == null)
            {
                if (currentPlacementIndicator != null && currentPlacementIndicator.activeSelf)
                {
                    currentPlacementIndicator.SetActive(false);
                }
                return;
            }

            UpdatePlacementPoseAndIndicator();
            HandlePlacementInput();
        }

        /// <summary>
        /// 使用射线检测更新有效的放置位置和姿态。
        /// </summary>
        private void UpdatePlacementPoseAndIndicator()
        {
            // 从屏幕中心发射射线,模拟用户将要放置的位置
            Vector2 screenCenter = new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
            List<ARRaycastHit> hitResults = new List<ARRaycastHit>();

            if (arRaycastManager.Raycast(screenCenter, hitResults, TrackableType.PlaneWithinPolygon, planeLayerMask))
            {
                // 筛选出水平面且距离合适的命中点
                ARRaycastHit validHit = default;
                float minDistance = float.MaxValue;

                foreach (var hit in hitResults)
                {
                    var plane = arPlaneManager.GetPlane(hit.trackableId);
                    if (plane != null && plane.alignment == PlaneAlignment.HorizontalUp && hit.distance < maxPlacementDistance && hit.distance < minDistance)
                    {
                        validHit = hit;
                        minDistance = hit.distance;
                    }
                }

                if (validHit.trackableId != TrackableId.invalidId)
                {
                    placementPose = validHit.pose;

                    // 调整姿态:让物体垂直于平面(Y轴朝上),并根据家具的预设朝向前方(Z轴)
                    Vector3 forward = Vector3.ProjectOnPlane(arSessionOrigin.camera.transform.forward, Vector3.up).normalized;
                    if (forward.magnitude > 0.01f)
                    {
                        placementPose.rotation = Quaternion.LookRotation(forward, Vector3.up);
                    }
                    else
                    {
                        placementPose.rotation = Quaternion.identity;
                    }

                    isPlacementPoseValid = true;

                    if (currentPlacementIndicator != null)
                    {
                        currentPlacementIndicator.SetActive(true);
                        currentPlacementIndicator.transform.SetPositionAndRotation(placementPose.position, placementPose.rotation);
                        // 可以根据选中的家具缩放指示器
                        currentPlacementIndicator.transform.localScale = Vector3.one * selectedFurniture.indicatorScale;
                    }
                    return;
                }
            }

            // 没有找到有效平面
            isPlacementPoseValid = false;
            if (currentPlacementIndicator != null && currentPlacementIndicator.activeSelf)
            {
                currentPlacementIndicator.SetActive(false);
            }
        }

        /// <summary>
        /// 处理用户触摸输入以放置家具。
        /// </summary>
        private void HandlePlacementInput()
        {
            // 在商业应用中,可能使用单指触摸放置,双指操作已放置物体
            if (Input.touchCount > 0 && isPlacementPoseValid)
            {
                Touch touch = Input.GetTouch(0);
                if (touch.phase == TouchPhase.Began)
                {
                    PlaceFurniture();
                }
            }

            // 为编辑器内测试提供支持
            if (Input.GetMouseButtonDown(0) && isPlacementPoseValid && Application.isEditor)
            {
                PlaceFurniture();
            }
        }

        /// <summary>
        /// 实例化并放置选中的家具。
        /// </summary>
        private void PlaceFurniture()
        {
            if (selectedFurniture == null || selectedFurniture.prefab == null)
            {
                Debug.LogWarning("未选择有效的家具进行放置。");
                return;
            }

            GameObject furnitureInstance = Instantiate(selectedFurniture.prefab, placementPose.position, placementPose.rotation);

            // 确保家具底部接触平面:通过获取渲染器的边界并调整Y轴位置
            Renderer rend = furnitureInstance.GetComponentInChildren<Renderer>();
            if (rend != null)
            {
                Bounds bounds = rend.bounds;
                float offsetY = bounds.extents.y;
                furnitureInstance.transform.position += Vector3.up * offsetY;
            }

            // 为实例添加交互组件
            FurnitureInteractable interactable = furnitureInstance.AddComponent<FurnitureInteractable>();
            interactable.Initialize(selectedFurniture.id);

            // 记录已放置的家具
            placedFurniture.Add(furnitureInstance, new FurnitureInstanceData(selectedFurniture.id, placementPose));

            Debug.Log($"放置家具: {selectedFurniture.displayName}{placementPose.position}");

            // 放置后,可以选择清空当前选中,或者保持选中以连续放置同一物品
            // selectedFurniture = null;
        }

        /// <summary>
        /// 处理从UI传来的家具选择事件。
        /// </summary>
        private void HandleFurnitureSelected(string furnitureId)
        {
            selectedFurniture = furnitureCatalog.Find(item => item.id == furnitureId);
            if (selectedFurniture == null)
            {
                Debug.LogError($"未找到ID为 {furnitureId} 的家具配置。");
            }
        }

        /// <summary>
        /// 控制平面可视化的开关,在商业应用中,识别成功后通常隐藏平面以保持界面整洁。
        /// </summary>
        public void TogglePlaneVisualization(bool isVisible)
        {
            if (arPlaneManager != null)
            {
                foreach (var plane in arPlaneManager.trackables)
                {
                    plane.gameObject.SetActive(isVisible);
                }
            }
        }

        /// <summary>
        /// 获取当前所有已放置家具的数据,可用于保存场景布局。
        /// </summary>
        public List<FurnitureInstanceData> GetAllPlacedFurnitureData()
        {
            return new List<FurnitureInstanceData>(placedFurniture.Values);
        }

        private void OnDestroy()
        {
            if (selectionUI != null)
            {
                selectionUI.OnFurnitureSelected -= HandleFurnitureSelected;
            }
        }
    }

    /// <summary>
    /// 家具数据定义。
    /// </summary>
    [System.Serializable]
    public class FurnitureItem
    {
        public string id;
        public string displayName;
        public GameObject prefab;
        [Tooltip("放置指示器的缩放比例")]
        public float indicatorScale = 1.0f;
    }

    /// <summary>
    /// 已放置家具的实例数据。
    /// </summary>
    public class FurnitureInstanceData
    {
        public string furnitureId;
        public Pose placementPose;
        public Quaternion customRotation; // 用户可能后续调整的旋转

        public FurnitureInstanceData(string id, Pose pose)
        {
            furnitureId = id;
            placementPose = pose;
            customRotation = pose.rotation;
        }
    }

    /// <summary>
    /// 附加到已放置家具上的交互组件,处理后续的选择、移动、旋转、删除等操作。
    /// </summary>
    public class FurnitureInteractable : MonoBehaviour, IInteractable
    {
        public string FurnitureId { get; private set; }
        private bool isSelected = false;
        private Material originalMaterial;
        [SerializeField]
        private Material selectedMaterial;

        public void Initialize(string furnitureId)
        {
            FurnitureId = furnitureId;
            Renderer rend = GetComponentInChildren<Renderer>();
            if (rend != null)
            {
                originalMaterial = rend.material;
            }
        }

        public void OnSelect()
        {
            isSelected = true;
            HighlightObject(true);
            // 通知UI管理器显示针对此家具的操作面板
            FurnitureUIManager.Instance.ShowControlsForFurniture(this);
        }

        public void OnDeselect()
        {
            isSelected = false;
            HighlightObject(false);
        }

        public void MoveToPosition(Vector3 newPosition)
        {
            transform.position = newPosition;
            // 可以在这里添加一个平滑移动的动画
        }

        public void Rotate(float degrees)
        {
            transform.Rotate(Vector3.up, degrees);
        }

        public void Delete()
        {
            // 通知空间布局管理器移除记录
            SpatialLayoutManager manager = FindObjectOfType<SpatialLayoutManager>();
            // 这里需要扩展管理器以提供移除方法
            Destroy(gameObject);
        }

        private void HighlightObject(bool highlight)
        {
            Renderer rend = GetComponentInChildren<Renderer>();
            if (rend != null && selectedMaterial != null)
            {
                rend.material = highlight ? selectedMaterial : originalMaterial;
            }
        }
    }

    public interface IInteractable
    {
        void OnSelect();
        void OnDeselect();
        void MoveToPosition(Vector3 newPosition);
        void Rotate(float degrees);
        void Delete();
    }
}

5.4 持久化AR体验与多用户协作

云锚点(Cloud Anchors)是ARCore提供的一项高级服务,它解决了AR体验的两个核心限制:持久化(Persistence)和共享(Sharing)。云锚点允许将设备本地识别的特定空间点(一个锚点)上传至云端,并生成一个唯一的ID。其他设备可以在同一物理位置下载并解析这个ID,从而将虚拟内容准确地锚定在相同的真实世界位置上。这对于多用户协作应用(如联合设计评审、多人AR游戏)或需要跨会话保存AR布局的应用(如虚拟家具的永久性摆放)至关重要。

其工作流程通常涉及“主机”设备(Host)和“客户端”设备(Client)。主机负责创建和上传云锚点,客户端通过云锚点ID进行解析和解析。ARCore云锚点服务会处理设备间的空间坐标差异,确保虚拟内容在所有用户视野中对齐。

在商业项目中,例如开发一个用于房地产的协作看房应用,不同地点的设计师和客户可以同时进入一个虚拟的毛坯房场景,共同讨论并放置虚拟的家具和装饰品。云锚点保证了所有人看到的虚拟物品都在房间的同一个角落。

以下是一个简化的多用户协作展厅布置的商业实例代码框架,展示了云锚点的创建、解析和基于此的内容同步逻辑。

using UnityEngine;
using UnityEngine.XR.ARFoundation;
using Google.XR.ARCoreExtensions;
using TMPro;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CommercialARProject
{
    /// <summary>
    /// 云锚点协作管理器:处理云锚点的生命周期和多用户间虚拟内容的同步。
    /// </summary>
    public class CloudAnchorCollaborationManager : MonoBehaviour
    {
        [SerializeField]
        private ARAnchorManager arAnchorManager;
        [SerializeField]
        private ARSessionOrigin arSessionOrigin;

        [Header("UI 组件")]
        [SerializeField]
        private TMP_Text instructionText;
        [SerializeField]
        private GameObject hostButton;
        [SerializeField]
        private GameObject clientPanel;
        [SerializeField]
        private TMP_InputField anchorIdInputField;

        [Header("共享内容")]
        [SerializeField]
        private GameObject sharedContentPrefab; // 所有用户都能看到的共享物体,如一个展台

        private ARCloudAnchor currentCloudAnchor;
        private string resolvedAnchorId;
        private ResolveState cloudAnchorResolveState = ResolveState.None;
        private List<GameObject> sharedContentInstances = new List<GameObject>();

        // 模拟的网络管理器,在真实项目中会替换为Photon、Mirror或自定义的Socket方案
        private MockNetworkManager networkManager;

        private enum ResolveState
        {
            None,
            Hosting,
            Resolving,
            Success,
            Failed
        }

        private void Start()
        {
            if (arAnchorManager == null) arAnchorManager = FindObjectOfType<ARAnchorManager>();
            if (arSessionOrigin == null) arSessionOrigin = FindObjectOfType<ARSessionOrigin>();

            networkManager = new MockNetworkManager();
            networkManager.OnAnchorIdReceived += HandleReceivedAnchorId;

            instructionText.text = "请先识别一个平面,然后选择‘创建共享点’或‘加入共享会话’。";
        }

        /// <summary>
        /// 由UI按钮调用:主机模式,在当前位置创建云锚点。
        /// </summary>
        public async void HostCloudAnchor()
        {
            // 首先,在屏幕中心检测一个平面,并创建一个本地锚点
            Vector2 screenCenter = new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
            List<ARRaycastHit> hits = new List<ARRaycastHit>();
            if (arAnchorManager.Raycast(screenCenter, hits, TrackableType.PlaneWithinPolygon))
            {
                Pose hitPose = hits[0].pose;
                ARAnchor localAnchor = arAnchorManager.AddAnchor(hitPose);

                if (localAnchor == null)
                {
                    Debug.LogError("创建本地锚点失败。");
                    instructionText.text = "创建本地锚点失败,请重试。";
                    return;
                }

                instructionText.text = "正在创建云锚点...";
                hostButton.SetActive(false);
                cloudAnchorResolveState = ResolveState.Hosting;

                // 将本地锚点转换为云锚点并上传
                currentCloudAnchor = arAnchorManager.HostCloudAnchor(localAnchor);
                if (currentCloudAnchor == null)
                {
                    instructionText.text = "云锚点创建失败。";
                    cloudAnchorResolveState = ResolveState.Failed;
                    hostButton.SetActive(true);
                    return;
                }

                // 等待云锚点完成上传(异步轮询状态)
                await WaitForCloudAnchorAsync(currentCloudAnchor);

                if (currentCloudAnchor.cloudAnchorState == CloudAnchorState.Success)
                {
                    resolvedAnchorId = currentCloudAnchor.cloudAnchorId;
                    instructionText.text = $"云锚点创建成功!ID: {resolvedAnchorId.Substring(0, 16)}...\n已将此ID分享给其他用户。";
                    cloudAnchorResolveState = ResolveState.Success;

                    // 1. 在主机本地实例化共享内容
                    SpawnSharedContentAtAnchor(currentCloudAnchor.transform);
                    // 2. 通过网络将云锚点ID广播给其他客户端
                    networkManager.BroadcastAnchorId(resolvedAnchorId);

                    // 主机也进入监听模式,以便接收其他用户放置的内容(扩展功能)
                }
                else
                {
                    instructionText.text = $"云锚点创建失败: {currentCloudAnchor.cloudAnchorState}";
                    cloudAnchorResolveState = ResolveState.Failed;
                    hostButton.SetActive(true);
                }
            }
            else
            {
                instructionText.text = "未检测到平面,请将设备对准地面或桌面。";
            }
        }

        /// <summary>
        /// 由UI按钮调用:客户端模式,通过输入的ID解析云锚点。
        /// </summary>
        public void ResolveCloudAnchor()
        {
            string inputId = anchorIdInputField.text.Trim();
            if (string.IsNullOrEmpty(inputId))
            {
                instructionText.text = "请输入有效的云锚点ID。";
                return;
            }

            instructionText.text = "正在解析云锚点...";
            cloudAnchorResolveState = ResolveState.Resolving;
            clientPanel.SetActive(false);

            // 开始解析云锚点
            currentCloudAnchor = arAnchorManager.ResolveCloudAnchorId(inputId);
            if (currentCloudAnchor == null)
            {
                instructionText.text = "云锚点解析请求失败。";
                cloudAnchorResolveState = ResolveState.Failed;
                clientPanel.SetActive(true);
                return;
            }

            // 异步等待解析完成
            WaitForResolveAsync(currentCloudAnchor);
        }

        /// <summary>
        /// 异步等待云锚点上传完成。
        /// </summary>
        private async Task WaitForCloudAnchorAsync(ARCloudAnchor anchor)
        {
            while (anchor.cloudAnchorState == CloudAnchorState.TaskInProgress)
            {
                await Task.Delay(100); // 每100毫秒检查一次状态
            }
        }

        /// <summary>
        /// 异步等待云锚点解析完成。
        /// </summary>
        private async void WaitForResolveAsync(ARCloudAnchor anchor)
        {
            while (anchor.cloudAnchorState == CloudAnchorState.TaskInProgress)
            {
                await Task.Delay(100);
            }

            if (anchor.cloudAnchorState == CloudAnchorState.Success)
            {
                resolvedAnchorId = anchor.cloudAnchorId;
                instructionText.text = "云锚点解析成功!共享内容已加载。";
                cloudAnchorResolveState = ResolveState.Success;

                // 在解析到的位置实例化共享内容
                SpawnSharedContentAtAnchor(anchor.transform);

                // 客户端可以开始放置自己的内容,并同步给主机(扩展功能)
            }
            else
            {
                instructionText.text = $"云锚点解析失败: {anchor.cloudAnchorState}";
                cloudAnchorResolveState = ResolveState.Failed;
                clientPanel.SetActive(true);
            }
        }

        /// <summary>
        /// 在指定的锚点位置生成共享的虚拟内容。
        /// </summary>
        private void SpawnSharedContentAtAnchor(Transform anchorTransform)
        {
            if (sharedContentPrefab != null)
            {
                GameObject content = Instantiate(sharedContentPrefab, anchorTransform.position, anchorTransform.rotation);
                sharedContentInstances.Add(content);
                // 将内容设为锚点的子物体,确保其位置关系固定
                content.transform.SetParent(anchorTransform, false);

                // 为共享内容添加协作交互组件
                CollaborativeInteractable collabInteractable = content.AddComponent<CollaborativeInteractable>();
                collabInteractable.Initialize(resolvedAnchorId, networkManager);
            }
        }

        /// <summary>
        /// 处理从网络接收到的云锚点ID(客户端自动加入场景)。
        /// </summary>
        private void HandleReceivedAnchorId(string anchorId)
        {
            // 避免重复解析同一个ID
            if (cloudAnchorResolveState == ResolveState.None || cloudAnchorResolveState == ResolveState.Failed)
            {
                anchorIdInputField.text = anchorId;
                instructionText.text = $"收到共享邀请,正在自动加入...";
                // 可以在此自动调用 ResolveCloudAnchor,或提示用户确认
                // 为简化,这里直接解析
                StartCoroutine(AutoResolveAfterDelay(anchorId, 1.0f));
            }
        }

        private System.Collections.IEnumerator AutoResolveAfterDelay(string anchorId, float delay)
        {
            yield return new WaitForSeconds(delay);
            if (cloudAnchorResolveState != ResolveState.Success)
            {
                anchorIdInputField.text = anchorId;
                ResolveCloudAnchor();
            }
        }

        /// <summary>
        /// 清理资源。
        /// </summary>
        private void OnDestroy()
        {
            if (networkManager != null)
            {
                networkManager.OnAnchorIdReceived -= HandleReceivedAnchorId;
                networkManager.Dispose();
            }
        }
    }

    /// <summary>
    /// 可协作交互的物体,其状态变化会通过网络同步给所有用户。
    /// </summary>
    public class CollaborativeInteractable : MonoBehaviour
    {
        public string AssociatedAnchorId { get; private set; }
        private MockNetworkManager networkManager;
        private string objectInstanceId;
        private bool isSelectedByMe = false;

        public void Initialize(string anchorId, MockNetworkManager netManager)
        {
            AssociatedAnchorId = anchorId;
            networkManager = netManager;
            objectInstanceId = System.Guid.NewGuid().ToString(); // 生成唯一实例ID

            // 监听网络管理器关于此物体的状态更新消息
            networkManager.OnObjectStateUpdated += HandleObjectStateUpdate;
        }

        /// <summary>
        /// 当本地用户与此物体交互时调用(如改变颜色)。
        /// </summary>
        public void OnLocalInteraction(string interactionType, string interactionValue)
        {
            // 1. 在本地立即应用交互效果(如改变颜色)
            ApplyInteractionLocally(interactionType, interactionValue);

            // 2. 创建状态更新消息并广播给其他所有用户
            ObjectStateUpdate update = new ObjectStateUpdate
            {
                AnchorId = AssociatedAnchorId,
                ObjectInstanceId = objectInstanceId,
                InteractionType = interactionType,
                InteractionValue = interactionValue,
                Timestamp = System.DateTime.UtcNow.Ticks
            };

            networkManager.BroadcastObjectState(update);
        }

        private void ApplyInteractionLocally(string type, string value)
        {
            // 根据交互类型应用效果
            switch (type)
            {
                case "COLOR_CHANGE":
                    if (ColorUtility.TryParseHtmlString(value, out Color newColor))
                    {
                        GetComponentInChildren<Renderer>().material.color = newColor;
                    }
                    break;
                case "TOGGLE_VISIBILITY":
                    bool isVisible = bool.Parse(value);
                    gameObject.SetActive(isVisible);
                    break;
                // 可以扩展更多交互类型
            }
        }

        /// <summary>
        /// 处理从网络接收到的其他用户对此物体的状态更新。
        /// </summary>
        private void HandleObjectStateUpdate(ObjectStateUpdate update)
        {
            // 确保这个更新是针对此物体实例的
            if (update.ObjectInstanceId == objectInstanceId && !isSelectedByMe) // 避免应用自己发出的更新
            {
                // 在商业项目中,这里可能需要更复杂的状态同步和冲突解决逻辑
                ApplyInteractionLocally(update.InteractionType, update.InteractionValue);
            }
        }

        private void OnDestroy()
        {
            if (networkManager != null)
            {
                networkManager.OnObjectStateUpdated -= HandleObjectStateUpdate;
            }
        }
    }

    /// <summary>
    /// 模拟的网络管理器,用于演示消息传递。真实项目需替换。
    /// </summary>
    public class MockNetworkManager
    {
        public delegate void AnchorIdReceivedHandler(string anchorId);
        public event AnchorIdReceivedHandler OnAnchorIdReceived;

        public delegate void ObjectStateUpdatedHandler(ObjectStateUpdate update);
        public event ObjectStateUpdatedHandler OnObjectStateUpdated;

        public void BroadcastAnchorId(string anchorId)
        {
            // 模拟网络延迟后广播
            Task.Delay(300).ContinueWith(t =>
            {
                OnAnchorIdReceived?.Invoke(anchorId);
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

        public void BroadcastObjectState(ObjectStateUpdate update)
        {
            Task.Delay(150).ContinueWith(t =>
            {
                OnObjectStateUpdated?.Invoke(update);
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

        public void Dispose()
        {
            // 清理资源
        }
    }

    public struct ObjectStateUpdate
    {
        public string AnchorId;
        public string ObjectInstanceId;
        public string InteractionType;
        public string InteractionValue;
        public long Timestamp;
    }
}

5.5 高级优化策略与性能考量

在商业级AR应用开发中,实现功能仅是第一步,确保应用在各种设备上运行流畅、稳定且耗电可控,是项目成功并赢得用户的关键。AR应用同时处理摄像头输入、传感器数据、环境计算、3D图形渲染和可能的网络通信,对性能有极高要求。

1. 渲染优化:
AR场景的渲染需兼顾虚拟物体的逼真度和对真实世界视图的实时叠加。首要策略是控制绘制调用(Draw Calls)。对于识别后生成的多个相同模型(如多个同款椅子),应使用GPU Instancing技术。在Unity中,这需要着色器支持,并确保材质的“Enable GPU Instancing”选项被勾选。

// 在实例化物体时,如果材质支持,Instancing会自动生效
// 但需确保着色器编写正确。以下是商业项目中一个简单物品的材质配置代码示例。
public class InstancedObjectSpawner : MonoBehaviour
{
    public GameObject instancedPrefab;
    public int spawnCount = 10;
    private List<GameObject> instances = new List<GameObject>();

    void Start()
    {
        Material prefabMaterial = instancedPrefab.GetComponent<Renderer>().sharedMaterial;
        if (prefabMaterial.enableInstancing)
        {
            for (int i = 0; i < spawnCount; i++)
            {
                Vector3 position = new Vector3(Random.Range(-2f, 2f), 0, Random.Range(-2f, 2f));
                GameObject obj = Instantiate(instancedPrefab, position, Quaternion.identity);
                instances.Add(obj);
                // 由于启用Instancing,即使材质相同,这些物体的渲染合批效率也会很高。
            }
            Debug.Log($"已生成 {spawnCount} 个支持GPU Instancing的物体。");
        }
        else
        {
            Debug.LogWarning("预制体材质未启用GPU Instancing,大量实例将严重影响性能。");
        }
    }
}

此外,应使用遮挡剔除(Occlusion Culling),虽然AR中虚拟物体常位于前景,但当虚拟场景复杂或包含大型虚拟结构时,此技术能避免渲染被真实或虚拟物体遮挡的部分。同时,必须设置合理的LOD(Level of Detail)系统,根据物体与摄像机的距离切换不同精度的模型。

2. 内存与资源管理:
AR应用常需动态加载和卸载3D模型、纹理等资源。不当管理会导致内存峰值和卡顿。商业项目应实现一个资源池(Object Pooling)系统,特别是对于频繁实例化/销毁的物体(如点击放置产生的临时预览框、交互特效)。

using System.Collections.Generic;

namespace CommercialARProject
{
    public class ObjectPool : MonoBehaviour
    {
        public static ObjectPool Instance;

        [System.Serializable]
        public class Pool
        {
            public string tag;
            public GameObject prefab;
            public int initialSize;
        }

        public List<Pool> pools;
        private Dictionary<string, Queue<GameObject>> poolDictionary;

        private void Awake()
        {
            Instance = this;
            InitializePools();
        }

        private void InitializePools()
        {
            poolDictionary = new Dictionary<string, Queue<GameObject>>();

            foreach (Pool pool in pools)
            {
                Queue<GameObject> objectPool = new Queue<GameObject>();
                for (int i = 0; i < pool.initialSize; i++)
                {
                    GameObject obj = Instantiate(pool.prefab);
                    obj.SetActive(false);
                    obj.transform.SetParent(this.transform); // 集中管理
                    objectPool.Enqueue(obj);
                }
                poolDictionary.Add(pool.tag, objectPool);
            }
        }

        public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation)
        {
            if (!poolDictionary.ContainsKey(tag))
            {
                Debug.LogWarning($"对象池中没有标签为 '{tag}' 的预设。");
                return null;
            }

            // 如果池空了,动态扩展(商业项目中应设置上限)
            if (poolDictionary[tag].Count == 0)
            {
                Debug.LogWarning($"对象池 '{tag}' 已空,正在动态创建新实例。");
                GameObject newObj = Instantiate(pools.Find(p => p.tag == tag).prefab);
                newObj.transform.SetParent(this.transform);
                newObj.SetActive(false);
                poolDictionary[tag].Enqueue(newObj);
            }

            GameObject objectToSpawn = poolDictionary[tag].Dequeue();
            objectToSpawn.transform.SetPositionAndRotation(position, rotation);
            objectToSpawn.SetActive(true);

            // 调用物体上的初始化方法
            IPooledObject pooledObj = objectToSpawn.GetComponent<IPooledObject>();
            pooledObj?.OnObjectSpawn();

            return objectToSpawn;
        }

        public void ReturnToPool(string tag, GameObject obj)
        {
            if (!poolDictionary.ContainsKey(tag))
            {
                Debug.LogWarning($"无法回收到不存在的标签 '{tag}' 的对象池。");
                Destroy(obj);
                return;
            }

            obj.SetActive(false);
            poolDictionary[tag].Enqueue(obj);
        }
    }

    public interface IPooledObject
    {
        void OnObjectSpawn();
        void OnObjectReturn();
    }
}

对于通过网络下载的3D模型(如从云端产品库加载),必须实现异步加载和缓存机制,并附带加载进度提示和失败处理。

3. AR会话与多线程处理:
ARCore的计算密集型任务(如图像特征提取、平面检测)主要在后台线程运行。Unity的主线程需要及时消费这些结果。确保在Update循环中处理AR Foundation管理器的事件(如ARTrackedImagesChanged)时逻辑高效,避免阻塞。对于复杂的、基于识别结果的业务逻辑(如识别图片后下载并加载对应的复杂模型),应使用C#的async/awaitTask机制,防止主线程卡顿。

4. 网络优化(针对云锚点等在线功能):
云锚点的上传和解析涉及网络延迟。商业应用必须设计健壮的重试机制和超时处理,并提供清晰的用户反馈(如“正在上传…”、“网络不佳,正在重试”)。在弱网环境下,可以考虑降级方案,例如将云锚点数据临时保存在本地,待网络恢复后再同步。对于多用户同步应用,应采用差分状态同步而非全量同步,并使用UDP等低延迟协议(通过专门的网络库如LiteNetLib或Photon的UDP模块实现)。

通过系统性地实施上述优化策略,商业AR应用能够在提供沉浸式体验的同时,保持应用的响应速度、稳定性和电池续航能力,从而满足终端用户和专业客户的高标准要求。

Logo

这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

更多推荐