第2章 构建AR世界的数学基石:空间计算与交互核心
第2章 构建AR世界的数学基石:空间计算与交互核心
2.1 从现实到虚拟:空间映射的数学原理
增强现实(AR)的本质在于将虚拟信息精准地叠加到现实物理空间,这一过程离不开数学的支撑。在商业AR应用开发中,无论是产品可视化、室内导航还是工业维修指导,都需要开发者深刻理解三维空间中的数学概念。本章将深入探讨AR开发中不可或缺的数学基础,并通过完整的商业项目实例,展示如何将这些理论转化为实用的代码。
任何AR场景都是一个三维坐标系系统。Unity引擎采用左手坐标系,即X轴向右,Y轴向上,Z轴向前。理解这个坐标系是AR开发的第一步。当我们通过手机摄像头观察世界时,设备需要实时计算自身在物理空间中的位置(位置向量)和朝向(旋转),这个计算过程本质上就是求解一个变换矩阵。
在商业AR应用中,比如家具陈列系统,我们需要将虚拟沙发准确地放在用户指定的地板位置。这涉及到多个坐标系的转换:从模型自身的局部坐标系,到场景中的世界坐标系,最终通过摄像头投影到二维的屏幕坐标系。这个转换过程可以用一个4x4的变换矩阵来表示:
M = T * R * S
其中T是平移矩阵,R是旋转矩阵,S是缩放矩阵。在Unity中,每个GameObject的Transform组件都隐含着这个变换矩阵。
让我们通过一个商业实例来理解这个过程。假设我们正在开发一个AR商品展示应用,用户可以在自己的房间里查看不同尺寸的虚拟书架。
using UnityEngine;
namespace ARCommercialDemo.MathFoundation
{
// 商品空间定位管理器 - 处理商品在AR空间中的放置与变换
public class ProductPlacementManager : MonoBehaviour
{
// 商品实例的引用
private GameObject currentProductInstance;
// 原始商品尺寸(从模型获取)
private Vector3 originalProductSize;
// 目标放置位置(从平面检测获取)
private Vector3 targetWorldPosition;
// 目标旋转角度(通常与检测到的平面对齐)
private Quaternion targetWorldRotation;
// 初始化商品实例
public void InitializeProduct(GameObject productPrefab, Vector3 detectedPosition)
{
// 销毁之前的实例
if (currentProductInstance != null)
{
Destroy(currentProductInstance);
}
// 实例化新商品
currentProductInstance = Instantiate(productPrefab);
// 获取原始尺寸
originalProductSize = GetProductOriginalSize(productPrefab);
// 设置初始位置(使用检测到的平面位置)
targetWorldPosition = detectedPosition;
// 默认旋转:与平面对齐,正面朝向相机
Vector3 cameraForward = Camera.main.transform.forward;
cameraForward.y = 0; // 保持水平
targetWorldRotation = Quaternion.LookRotation(cameraForward.normalized);
ApplyTransformation();
}
// 获取商品的原始尺寸(从渲染边界计算)
private Vector3 GetProductOriginalSize(GameObject product)
{
Renderer productRenderer = product.GetComponent<Renderer>();
if (productRenderer != null)
{
return productRenderer.bounds.size;
}
// 如果商品没有Renderer,尝试从子物体获取
Renderer[] childRenderers = product.GetComponentsInChildren<Renderer>();
if (childRenderers.Length > 0)
{
Bounds combinedBounds = childRenderers[0].bounds;
for (int i = 1; i < childRenderers.Length; i++)
{
combinedBounds.Encapsulate(childRenderers[i].bounds);
}
return combinedBounds.size;
}
// 默认尺寸
return Vector3.one;
}
// 应用变换到商品实例
private void ApplyTransformation()
{
if (currentProductInstance == null)
{
return;
}
Transform productTransform = currentProductInstance.transform;
// 应用位置
productTransform.position = targetWorldPosition;
// 应用旋转
productTransform.rotation = targetWorldRotation;
// 保持原始比例
productTransform.localScale = Vector3.one;
}
// 更新商品位置(响应AR平面更新)
public void UpdateProductPosition(Vector3 newPosition, Quaternion newRotation)
{
targetWorldPosition = newPosition;
targetWorldRotation = newRotation;
ApplyTransformation();
}
// 调整商品尺寸(商业应用中常见的功能)
public void ScaleProduct(float scaleFactor)
{
if (currentProductInstance != null)
{
// 限制缩放范围(商业需求:通常不允许无限制缩放)
float minScale = 0.1f;
float maxScale = 5.0f;
scaleFactor = Mathf.Clamp(scaleFactor, minScale, maxScale);
currentProductInstance.transform.localScale = Vector3.one * scaleFactor;
}
}
// 计算商品是否适合目标空间(商业逻辑)
public bool CheckProductFitsSpace(Vector3 availableSpaceSize)
{
if (currentProductInstance == null)
{
return false;
}
// 获取当前商品的实际尺寸(考虑缩放)
Vector3 currentSize = GetCurrentProductSize();
// 检查每个维度是否适合
bool fitsWidth = currentSize.x <= availableSpaceSize.x;
bool fitsHeight = currentSize.y <= availableSpaceSize.y;
bool fitsDepth = currentSize.z <= availableSpaceSize.z;
// 商业逻辑:记录分析数据
LogFittingAnalysis(currentSize, availableSpaceSize, fitsWidth && fitsHeight && fitsDepth);
return fitsWidth && fitsHeight && fitsDepth;
}
// 获取当前商品的实际尺寸
private Vector3 GetCurrentProductSize()
{
Vector3 scaledSize = originalProductSize;
if (currentProductInstance != null)
{
scaledSize = Vector3.Scale(originalProductSize, currentProductInstance.transform.lossyScale);
}
return scaledSize;
}
// 记录商品适配分析数据(用于商业分析)
private void LogFittingAnalysis(Vector3 productSize, Vector3 spaceSize, bool fits)
{
Debug.Log($"商品适配分析 - 产品尺寸: {productSize}, " +
$"空间尺寸: {spaceSize}, " +
$"适配结果: {(fits ? "适合" : "不适合")}");
// 在实际商业项目中,这里通常会发送数据到分析服务器
// SendAnalyticsData("product_fit_check", new { productSize, spaceSize, fits });
}
}
}
2.2 向量数学在AR交互中的应用
向量是AR开发中最基本的数学工具。一个三维向量可以表示位置、方向或速度。在商业AR应用中,向量的运算无处不在。
**点积(Dot Product)**在AR中常用于判断朝向关系。例如,在零售AR应用中,我们需要确保虚拟商品标签始终面向用户,这时就需要用到点积来计算相机与标签之间的角度。
**叉积(Cross Product)**则用于计算法向量和旋转轴。在AR家具布置应用中,当用户旋转虚拟家具时,我们需要计算旋转轴,这就要用到叉积运算。
让我们通过一个完整的商业实例来展示向量数学的实际应用。假设我们正在开发一个AR汽车展示应用,用户可以通过手势旋转查看汽车的不同角度。
using UnityEngine;
namespace ARCommercialDemo.VectorMathematics
{
// AR汽车展示交互控制器
public class ARCarViewerController : MonoBehaviour
{
// 汽车模型引用
public GameObject carModel;
// 旋转速度控制
public float rotationSpeed = 2.0f;
// 自动旋转参数
public bool enableAutoRotation = true;
public float autoRotationSpeed = 0.5f;
// 触摸/鼠标交互参数
private Vector2 previousTouchPosition;
private bool isDragging = false;
// 目标旋转角度(用于平滑旋转)
private Quaternion targetRotation;
// 旋转限制(商业需求:控制旋转范围)
public float minXRotation = -30f;
public float maxXRotation = 30f;
// 初始化
private void Start()
{
if (carModel == null)
{
Debug.LogError("汽车模型未分配!");
return;
}
targetRotation = carModel.transform.rotation;
// 初始化汽车位置,确保在摄像机视野内
InitializeCarPosition();
}
// 初始化汽车位置
private void InitializeCarPosition()
{
// 将汽车放置在摄像机前方适当位置
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
Vector3 cameraForward = mainCamera.transform.forward;
cameraForward.y = 0; // 保持水平
// 计算放置位置(摄像机前方3米)
Vector3 placementPosition = mainCamera.transform.position + cameraForward.normalized * 3f;
placementPosition.y = 0; // 放置在地面高度
carModel.transform.position = placementPosition;
// 使汽车面向摄像机
Vector3 lookDirection = mainCamera.transform.position - placementPosition;
lookDirection.y = 0;
if (lookDirection != Vector3.zero)
{
carModel.transform.rotation = Quaternion.LookRotation(lookDirection.normalized);
targetRotation = carModel.transform.rotation;
}
}
}
// 每帧更新
private void Update()
{
if (carModel == null)
{
return;
}
// 处理用户输入
HandleUserInput();
// 应用自动旋转(如果启用)
if (enableAutoRotation && !isDragging)
{
ApplyAutoRotation();
}
// 平滑旋转到目标角度
ApplySmoothRotation();
}
// 处理用户输入(触摸/鼠标)
private void HandleUserInput()
{
// 鼠标输入(用于编辑器测试)
HandleMouseInput();
// 触摸输入(移动设备)
HandleTouchInput();
}
// 处理鼠标输入
private void HandleMouseInput()
{
if (Input.GetMouseButtonDown(0))
{
// 开始拖拽
isDragging = true;
previousTouchPosition = Input.mousePosition;
// 商业逻辑:记录用户开始交互
LogUserInteraction("mouse_drag_start");
}
else if (Input.GetMouseButtonUp(0))
{
// 结束拖拽
isDragging = false;
// 商业逻辑:记录用户结束交互
LogUserInteraction("mouse_drag_end");
}
if (isDragging && Input.GetMouseButton(0))
{
// 计算鼠标移动量
Vector2 currentMousePosition = Input.mousePosition;
Vector2 deltaPosition = currentMousePosition - previousTouchPosition;
// 应用旋转
ApplyManualRotation(deltaPosition);
previousTouchPosition = currentMousePosition;
}
}
// 处理触摸输入
private void HandleTouchInput()
{
if (Input.touchCount == 1)
{
Touch touch = Input.GetTouch(0);
switch (touch.phase)
{
case TouchPhase.Began:
isDragging = true;
previousTouchPosition = touch.position;
LogUserInteraction("touch_drag_start");
break;
case TouchPhase.Moved:
if (isDragging)
{
Vector2 deltaPosition = touch.position - previousTouchPosition;
ApplyManualRotation(deltaPosition);
previousTouchPosition = touch.position;
}
break;
case TouchPhase.Ended:
case TouchPhase.Canceled:
isDragging = false;
LogUserInteraction("touch_drag_end");
break;
}
}
else if (Input.touchCount == 0)
{
isDragging = false;
}
}
// 应用手动旋转(基于向量运算)
private void ApplyManualRotation(Vector2 deltaPosition)
{
// 计算旋转量
float rotationX = deltaPosition.y * rotationSpeed * Time.deltaTime;
float rotationY = -deltaPosition.x * rotationSpeed * Time.deltaTime;
// 使用叉积计算旋转轴(向量数学的应用)
// 这里我们使用简单的欧拉角,但理解叉积对于更复杂的旋转很重要
Vector3 currentEuler = targetRotation.eulerAngles;
// 应用旋转(限制X轴旋转)
float newXRotation = currentEuler.x + rotationX;
newXRotation = ClampAngle(newXRotation, minXRotation, maxXRotation);
Vector3 newEuler = new Vector3(
newXRotation,
currentEuler.y + rotationY,
currentEuler.z
);
// 使用四元数避免万向节锁
targetRotation = Quaternion.Euler(newEuler);
}
// 应用自动旋转
private void ApplyAutoRotation()
{
// 围绕Y轴缓慢旋转
float autoRotationAmount = autoRotationSpeed * Time.deltaTime;
targetRotation *= Quaternion.Euler(0, autoRotationAmount, 0);
}
// 应用平滑旋转
private void ApplySmoothRotation()
{
// 使用球面线性插值实现平滑旋转
carModel.transform.rotation = Quaternion.Slerp(
carModel.transform.rotation,
targetRotation,
5.0f * Time.deltaTime
);
}
// 角度钳制函数(处理欧拉角超过360度的情况)
private float ClampAngle(float angle, float min, float max)
{
if (angle < -360f)
{
angle += 360f;
}
if (angle > 360f)
{
angle -= 360f;
}
return Mathf.Clamp(angle, min, max);
}
// 重置汽车旋转到初始状态
public void ResetCarRotation()
{
// 商业逻辑:提供重置功能,提升用户体验
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
Vector3 lookDirection = mainCamera.transform.position - carModel.transform.position;
lookDirection.y = 0;
if (lookDirection != Vector3.zero)
{
targetRotation = Quaternion.LookRotation(lookDirection.normalized);
}
}
LogUserInteraction("rotation_reset");
}
// 记录用户交互(商业分析)
private void LogUserInteraction(string interactionType)
{
Debug.Log($"用户交互记录: {interactionType}, 时间: {Time.time}");
// 在实际商业项目中,这里会收集用户交互数据用于分析
// 例如:用户最喜欢查看汽车的哪个角度,平均查看时间等
// SendAnalyticsData("user_interaction", new {
// type = interactionType,
// timestamp = Time.time,
// carModel = carModel.name
// });
}
// 计算汽车与摄像机的相对方向(向量点积的应用)
public float GetCarCameraAlignment()
{
if (carModel == null || Camera.main == null)
{
return 0f;
}
// 获取汽车前向向量
Vector3 carForward = carModel.transform.forward;
// 获取摄像机看向汽车的向量
Vector3 cameraToCar = (carModel.transform.position - Camera.main.transform.position).normalized;
// 使用点积计算对齐程度
// 点积结果范围:-1(完全相反)到1(完全对齐)
float alignment = Vector3.Dot(carForward, cameraToCar);
return alignment;
}
// 获取当前视图的描述(用于语音提示或UI显示)
public string GetCurrentViewDescription()
{
float alignment = GetCarCameraAlignment();
if (alignment > 0.7f)
{
return "您正在查看汽车前部";
}
else if (alignment < -0.7f)
{
return "您正在查看汽车后部";
}
else if (Mathf.Abs(alignment) < 0.3f)
{
// 使用叉积判断是左侧还是右侧
Vector3 carForward = carModel.transform.forward;
Vector3 cameraToCar = (carModel.transform.position - Camera.main.transform.position).normalized;
Vector3 crossResult = Vector3.Cross(carForward, cameraToCar);
if (crossResult.y > 0)
{
return "您正在查看汽车右侧";
}
else
{
return "您正在查看汽车左侧";
}
}
return "您正在查看汽车角度";
}
}
}
2.3 四元数:AR旋转的数学表达
在三维旋转中,欧拉角虽然直观,但存在万向节锁问题。四元数(Quaternion)通过四个数值(x, y, z, w)表示旋转,避免了这个问题,成为Unity中表示旋转的标准方式。
四元数的数学原理较为复杂,但开发者只需要理解其基本性质:
- 单位四元数表示旋转(长度为1)
- 四元数乘法表示旋转的组合
- 球面线性插值(Slerp)可以实现平滑旋转过渡
在商业AR导航应用中,当用户改变方向时,虚拟导航箭头需要平滑地旋转指向新方向,这正是四元数的典型应用场景。
using UnityEngine;
namespace ARCommercialDemo.QuaternionApplications
{
// AR导航箭头控制器
public class ARNavigationArrow : MonoBehaviour
{
// 导航箭头模型
public GameObject arrowModel;
// 当前目标位置
private Vector3 targetPosition;
// 旋转平滑度
public float rotationSmoothness = 5.0f;
// 箭头浮动效果参数
public float floatAmplitude = 0.2f;
public float floatFrequency = 1.0f;
private Vector3 initialArrowPosition;
private float floatTimer = 0f;
// 导航状态
private bool isNavigating = false;
// 初始化
private void Start()
{
if (arrowModel == null)
{
Debug.LogError("导航箭头模型未分配!");
return;
}
initialArrowPosition = arrowModel.transform.localPosition;
// 初始隐藏箭头
SetArrowVisibility(false);
}
// 每帧更新
private void Update()
{
if (!isNavigating || arrowModel == null)
{
return;
}
// 更新箭头旋转(指向目标)
UpdateArrowRotation();
// 应用浮动动画
ApplyFloatingAnimation();
// 更新箭头位置(保持在摄像机视野内)
UpdateArrowPosition();
}
// 开始导航到目标
public void StartNavigation(Vector3 destination)
{
targetPosition = destination;
isNavigating = true;
SetArrowVisibility(true);
Debug.Log($"开始导航到目标位置: {destination}");
// 商业逻辑:记录导航开始
LogNavigationEvent("navigation_started", transform.position, destination);
}
// 停止导航
public void StopNavigation()
{
isNavigating = false;
SetArrowVisibility(false);
Debug.Log("导航已停止");
// 商业逻辑:记录导航结束
LogNavigationEvent("navigation_stopped", transform.position, targetPosition);
}
// 更新箭头旋转(使用四元数)
private void UpdateArrowRotation()
{
if (Camera.main == null)
{
return;
}
// 计算从箭头到目标的方向
Vector3 directionToTarget = targetPosition - arrowModel.transform.position;
// 忽略Y轴差异(假设在同一水平面)
directionToTarget.y = 0;
// 如果方向为零向量,则不旋转
if (directionToTarget == Vector3.zero)
{
return;
}
// 使用四元数创建目标旋转
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized);
// 考虑摄像机旋转,使箭头始终面向用户
Quaternion cameraRotation = Camera.main.transform.rotation;
// 组合旋转:首先指向目标,然后调整以面向摄像机
Quaternion finalRotation = targetRotation * Quaternion.Inverse(cameraRotation) * Quaternion.Euler(0, 180, 0);
// 使用四元数球面线性插值实现平滑旋转
arrowModel.transform.rotation = Quaternion.Slerp(
arrowModel.transform.rotation,
finalRotation,
rotationSmoothness * Time.deltaTime
);
}
// 应用浮动动画
private void ApplyFloatingAnimation()
{
floatTimer += Time.deltaTime;
// 计算浮动偏移
float floatOffset = Mathf.Sin(floatTimer * floatFrequency * Mathf.PI * 2f) * floatAmplitude;
// 应用浮动效果
Vector3 newPosition = initialArrowPosition;
newPosition.y += floatOffset;
arrowModel.transform.localPosition = newPosition;
}
// 更新箭头位置(保持在摄像机视野内)
private void UpdateArrowPosition()
{
if (Camera.main == null)
{
return;
}
// 将箭头放置在摄像机前方固定距离
float distanceFromCamera = 2.0f;
// 获取摄像机的前向方向(水平方向)
Vector3 cameraForward = Camera.main.transform.forward;
cameraForward.y = 0;
cameraForward.Normalize();
// 计算箭头位置
Vector3 arrowWorldPosition = Camera.main.transform.position + cameraForward * distanceFromCamera;
// 设置箭头高度(略低于摄像机)
arrowWorldPosition.y = Camera.main.transform.position.y - 0.5f;
// 更新箭头位置
transform.position = arrowWorldPosition;
}
// 设置箭头可见性
private void SetArrowVisibility(bool isVisible)
{
if (arrowModel != null)
{
arrowModel.SetActive(isVisible);
}
}
// 检查是否到达目标
public bool CheckIfDestinationReached(float arrivalThreshold = 1.0f)
{
if (!isNavigating)
{
return false;
}
// 计算到目标的水平距离
Vector3 currentPosition = transform.position;
currentPosition.y = 0;
Vector3 targetPos = targetPosition;
targetPos.y = 0;
float distanceToTarget = Vector3.Distance(currentPosition, targetPos);
bool reached = distanceToTarget <= arrivalThreshold;
if (reached)
{
Debug.Log($"已到达目的地,距离: {distanceToTarget}");
// 商业逻辑:记录到达事件
LogNavigationEvent("destination_reached", currentPosition, targetPosition);
}
return reached;
}
// 计算导航进度(0到1之间)
public float CalculateNavigationProgress(Vector3 startPosition)
{
if (!isNavigating)
{
return 0f;
}
// 计算总距离
Vector3 startPos = startPosition;
startPos.y = 0;
Vector3 targetPos = targetPosition;
targetPos.y = 0;
float totalDistance = Vector3.Distance(startPos, targetPos);
if (totalDistance <= 0.001f)
{
return 1f;
}
// 计算当前距离
Vector3 currentPos = transform.position;
currentPos.y = 0;
float currentDistance = Vector3.Distance(currentPos, targetPos);
// 计算进度
float progress = 1f - (currentDistance / totalDistance);
// 限制在0到1之间
progress = Mathf.Clamp01(progress);
return progress;
}
// 计算到达时间估计(简单版本)
public float EstimateTimeToDestination(float averageSpeed = 1.4f)
{
if (!isNavigating)
{
return 0f;
}
// 计算剩余距离
Vector3 currentPos = transform.position;
currentPos.y = 0;
Vector3 targetPos = targetPosition;
targetPos.y = 0;
float remainingDistance = Vector3.Distance(currentPos, targetPos);
// 计算估计时间(秒)
float estimatedTime = remainingDistance / averageSpeed;
return estimatedTime;
}
// 获取导航方向提示
public string GetDirectionHint()
{
if (!isNavigating || Camera.main == null)
{
return "未在导航中";
}
// 计算目标方向
Vector3 directionToTarget = targetPosition - transform.position;
directionToTarget.y = 0;
if (directionToTarget == Vector3.zero)
{
return "您已到达目的地";
}
// 获取摄像机前向方向
Vector3 cameraForward = Camera.main.transform.forward;
cameraForward.y = 0;
cameraForward.Normalize();
// 计算方向向量之间的角度
float angle = Vector3.SignedAngle(cameraForward, directionToTarget, Vector3.up);
// 根据角度提供方向提示
if (Mathf.Abs(angle) < 30f)
{
return "目标在您正前方";
}
else if (angle > 30f && angle < 150f)
{
return "目标在您右侧";
}
else if (angle < -30f && angle > -150f)
{
return "目标在您左侧";
}
else
{
return "目标在您后方";
}
}
// 记录导航事件(商业分析)
private void LogNavigationEvent(string eventType, Vector3 currentPosition, Vector3 targetPosition)
{
Debug.Log($"导航事件: {eventType}, " +
$"当前位置: {currentPosition}, " +
$"目标位置: {targetPosition}, " +
$"时间: {Time.time}");
// 在实际商业项目中,这里会收集导航数据用于分析
// 例如:用户平均导航距离,常见目的地,导航成功率等
// SendAnalyticsData("navigation_event", new {
// event_type = eventType,
// current_position = currentPosition,
// target_position = targetPosition,
// timestamp = Time.time
// });
}
}
}
2.4 射线检测:AR交互的数学实现
射线检测是AR交互的核心技术之一。当用户在屏幕上点击时,我们需要从摄像机位置发射一条射线,检测它与虚拟物体或现实平面的交点。这本质上是一个数学上的直线与几何体的相交检测问题。
在商业AR家具布置应用中,用户通过点击屏幕来选择放置家具的位置。这个过程涉及:
- 将屏幕坐标转换为世界空间中的射线
- 检测射线与AR平面的交点
- 在交点位置实例化家具模型
让我们通过一个完整的商业实例来展示射线检测的实现:
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
namespace ARCommercialDemo.RaycastImplementation
{
// AR家具布置管理器
public class ARFurniturePlacer : MonoBehaviour
{
// AR射线管理器
public ARRaycastManager arRaycastManager;
// 家具预制体
public GameObject[] furniturePrefabs;
// 当前选中的家具索引
private int selectedFurnitureIndex = 0;
// 当前放置的家具实例
private GameObject currentFurnitureInstance;
// 放置指示器(显示将要放置的位置)
public GameObject placementIndicator;
// 放置有效性反馈材料
public Material validPlacementMaterial;
public Material invalidPlacementMaterial;
// 当前放置位置是否有效
private bool isPlacementValid = false;
// 当前放置位置
private Pose placementPose;
// 平面检测层
private LayerMask planeLayerMask;
// 初始化
private void Start()
{
if (arRaycastManager == null)
{
arRaycastManager = FindObjectOfType<ARRaycastManager>();
}
if (placementIndicator != null)
{
placementIndicator.SetActive(false);
}
// 设置平面层掩码
planeLayerMask = 1 << LayerMask.NameToLayer("ARPlane");
Debug.Log("AR家具布置系统已初始化");
}
// 每帧更新
private void Update()
{
if (arRaycastManager == null)
{
return;
}
// 更新放置指示器
UpdatePlacementIndicator();
// 处理用户输入
HandleUserInput();
}
// 更新放置指示器
private void UpdatePlacementIndicator()
{
if (placementIndicator == null)
{
return;
}
// 从屏幕中心发射射线
Vector2 screenCenter = new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
// 执行AR射线检测
var hits = new System.Collections.Generic.List<ARRaycastHit>();
if (arRaycastManager.Raycast(screenCenter, hits, TrackableType.PlaneWithinPolygon))
{
// 找到有效的AR平面
isPlacementValid = true;
placementPose = hits[0].pose;
// 获取平面法线
Vector3 planeNormal = hits[0].pose.up;
// 调整放置姿态,使家具与平面对齐
placementPose.rotation = Quaternion.FromToRotation(Vector3.up, planeNormal);
// 显示并更新指示器位置
placementIndicator.SetActive(true);
placementIndicator.transform.SetPositionAndRotation(
placementPose.position,
placementPose.rotation
);
// 更新指示器外观(根据放置有效性)
UpdateIndicatorAppearance(true);
}
else
{
// 没有检测到有效平面
isPlacementValid = false;
placementIndicator.SetActive(false);
}
}
// 更新指示器外观
private void UpdateIndicatorAppearance(bool isValid)
{
if (placementIndicator == null || validPlacementMaterial == null || invalidPlacementMaterial == null)
{
return;
}
Renderer indicatorRenderer = placementIndicator.GetComponent<Renderer>();
if (indicatorRenderer != null)
{
indicatorRenderer.material = isValid ? validPlacementMaterial : invalidPlacementMaterial;
}
}
// 处理用户输入
private void HandleUserInput()
{
// 处理触摸输入
if (Input.touchCount > 0)
{
Touch touch = Input.GetTouch(0);
if (touch.phase == TouchPhase.Began)
{
// 检测是否点击了UI元素
if (IsPointerOverUIObject(touch.position))
{
return;
}
if (isPlacementValid)
{
// 放置家具
PlaceFurniture();
}
else
{
// 尝试从触摸位置发射射线
TryRaycastFromTouch(touch.position);
}
}
}
// 处理鼠标输入(编辑器测试)
if (Input.GetMouseButtonDown(0))
{
// 检测是否点击了UI元素
if (IsPointerOverUIObject(Input.mousePosition))
{
return;
}
if (isPlacementValid)
{
PlaceFurniture();
}
else
{
TryRaycastFromTouch(Input.mousePosition);
}
}
}
// 尝试从触摸位置发射射线
private void TryRaycastFromTouch(Vector2 touchPosition)
{
// 执行射线检测
Ray ray = Camera.main.ScreenPointToRay(touchPosition);
RaycastHit hit;
// 检测与现有家具的交互
if (Physics.Raycast(ray, out hit, Mathf.Infinity))
{
GameObject hitObject = hit.collider.gameObject;
// 检查是否击中了已放置的家具
if (hitObject.CompareTag("Furniture"))
{
// 选中家具进行后续操作(移动、旋转、删除等)
SelectFurniture(hitObject);
}
}
}
// 放置家具
private void PlaceFurniture()
{
if (furniturePrefabs == null || furniturePrefabs.Length == 0)
{
Debug.LogWarning("没有可放置的家具预制体");
return;
}
if (selectedFurnitureIndex < 0 || selectedFurnitureIndex >= furniturePrefabs.Length)
{
selectedFurnitureIndex = 0;
}
// 销毁之前放置的家具(如果存在)
if (currentFurnitureInstance != null)
{
Destroy(currentFurnitureInstance);
}
// 实例化新家具
GameObject furniturePrefab = furniturePrefabs[selectedFurnitureIndex];
currentFurnitureInstance = Instantiate(
furniturePrefab,
placementPose.position,
placementPose.rotation
);
// 设置家具标签(用于后续识别)
currentFurnitureInstance.tag = "Furniture";
// 添加家具交互组件
AddFurnitureInteraction(currentFurnitureInstance);
Debug.Log($"家具已放置: {furniturePrefab.name}, 位置: {placementPose.position}");
// 商业逻辑:记录家具放置事件
LogFurniturePlacement(furniturePrefab.name, placementPose.position);
}
// 添加家具交互组件
private void AddFurnitureInteraction(GameObject furniture)
{
// 添加碰撞器(如果不存在)
if (furniture.GetComponent<Collider>() == null)
{
// 尝试从子物体获取渲染器来计算碰撞器大小
Renderer renderer = furniture.GetComponentInChildren<Renderer>();
if (renderer != null)
{
BoxCollider collider = furniture.AddComponent<BoxCollider>();
collider.size = renderer.bounds.size;
collider.center = renderer.bounds.center - furniture.transform.position;
}
else
{
furniture.AddComponent<BoxCollider>();
}
}
// 添加家具控制器脚本
FurnitureController furnitureController = furniture.AddComponent<FurnitureController>();
furnitureController.Initialize(this);
}
// 选择家具
private void SelectFurniture(GameObject furniture)
{
Debug.Log($"家具被选中: {furniture.name}");
// 高亮显示选中的家具
HighlightFurniture(furniture, true);
// 商业逻辑:记录家具选择事件
LogFurnitureSelection(furniture.name);
}
// 高亮显示家具
private void HighlightFurniture(GameObject furniture, bool highlight)
{
Renderer[] renderers = furniture.GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
Material[] materials = renderer.materials;
for (int i = 0; i < materials.Length; i++)
{
if (highlight)
{
// 创建高亮材质
Material highlightMaterial = new Material(materials[i]);
highlightMaterial.SetFloat("_Metallic", 0.8f);
highlightMaterial.SetFloat("_Glossiness", 1.0f);
materials[i] = highlightMaterial;
}
else
{
// 恢复原始材质(在实际项目中应保存原始材质)
}
}
renderer.materials = materials;
}
}
// 选择家具类型
public void SelectFurnitureType(int index)
{
if (index >= 0 && index < furniturePrefabs.Length)
{
selectedFurnitureIndex = index;
Debug.Log($"已选择家具类型: {furniturePrefabs[index].name}");
}
}
// 旋转当前家具
public void RotateCurrentFurniture(float angle)
{
if (currentFurnitureInstance != null)
{
currentFurnitureInstance.transform.Rotate(Vector3.up, angle, Space.World);
// 商业逻辑:记录家具旋转
LogFurnitureRotation(angle);
}
}
// 调整当前家具大小
public void ScaleCurrentFurniture(float scaleFactor)
{
if (currentFurnitureInstance != null)
{
Vector3 currentScale = currentFurnitureInstance.transform.localScale;
currentFurnitureInstance.transform.localScale = currentScale * scaleFactor;
// 商业逻辑:记录家具缩放
LogFurnitureScaling(scaleFactor);
}
}
// 删除当前家具
public void DeleteCurrentFurniture()
{
if (currentFurnitureInstance != null)
{
string furnitureName = currentFurnitureInstance.name;
Destroy(currentFurnitureInstance);
currentFurnitureInstance = null;
Debug.Log($"家具已删除: {furnitureName}");
// 商业逻辑:记录家具删除
LogFurnitureDeletion(furnitureName);
}
}
// 检查指针是否在UI对象上
private bool IsPointerOverUIObject(Vector2 position)
{
// 在实际项目中,这里会实现具体的UI检测逻辑
// 可以使用EventSystem.current.IsPointerOverGameObject()等方法
return false;
}
// 记录家具放置事件
private void LogFurniturePlacement(string furnitureName, Vector3 position)
{
Debug.Log($"家具放置记录 - 名称: {furnitureName}, 位置: {position}, 时间: {Time.time}");
// 在实际商业项目中,这里会收集用户行为数据
// 例如:哪些家具最受欢迎,用户通常放置的位置等
}
// 记录家具选择事件
private void LogFurnitureSelection(string furnitureName)
{
Debug.Log($"家具选择记录 - 名称: {furnitureName}, 时间: {Time.time}");
}
// 记录家具旋转事件
private void LogFurnitureRotation(float angle)
{
Debug.Log($"家具旋转记录 - 角度: {angle}, 时间: {Time.time}");
}
// 记录家具缩放事件
private void LogFurnitureScaling(float scaleFactor)
{
Debug.Log($"家具缩放记录 - 缩放系数: {scaleFactor}, 时间: {Time.time}");
}
// 记录家具删除事件
private void LogFurnitureDeletion(string furnitureName)
{
Debug.Log($"家具删除记录 - 名称: {furnitureName}, 时间: {Time.time}");
}
}
// 家具控制器(处理单个家具的交互)
public class FurnitureController : MonoBehaviour
{
// 父级布置管理器
private ARFurniturePlacer furniturePlacer;
// 交互状态
private bool isSelected = false;
private bool isDragging = false;
// 拖拽相关变量
private Vector3 dragOffset;
private float dragDistance;
// 初始化
public void Initialize(ARFurniturePlacer placer)
{
furniturePlacer = placer;
}
// 当家具被点击时调用
private void OnMouseDown()
{
if (furniturePlacer == null)
{
return;
}
isSelected = true;
// 计算拖拽偏移
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
Plane plane = new Plane(Vector3.up, transform.position);
if (plane.Raycast(ray, out dragDistance))
{
Vector3 hitPoint = ray.GetPoint(dragDistance);
dragOffset = transform.position - hitPoint;
}
}
// 当拖拽家具时调用
private void OnMouseDrag()
{
if (!isSelected || furniturePlacer == null)
{
return;
}
isDragging = true;
// 计算新的位置
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
Plane plane = new Plane(Vector3.up, transform.position);
if (plane.Raycast(ray, out dragDistance))
{
Vector3 hitPoint = ray.GetPoint(dragDistance);
transform.position = hitPoint + dragOffset;
}
}
// 当释放家具时调用
private void OnMouseUp()
{
if (isDragging)
{
// 拖拽结束,记录最终位置
Debug.Log($"家具拖拽完成,最终位置: {transform.position}");
}
isSelected = false;
isDragging = false;
}
}
}
2.5 数学优化:提升AR性能的商业价值
在商业AR应用中,性能优化直接关系到用户体验和商业成功。数学优化技术可以显著提升AR应用的运行效率,特别是在移动设备上。
空间分区技术如四叉树(2D)和八叉树(3D)可以加速射线检测和碰撞检测。在大型AR零售应用中,当场景中有数百个虚拟商品时,使用空间分区可以将检测复杂度从O(n)降低到O(log n)。
层次包围盒(BVH) 是另一种优化技术,通过为复杂模型创建简化的包围体积,减少精确碰撞检测的计算量。在AR家具应用中,一个复杂的沙发模型可能有上万个三角形,但使用包围盒后,初步的相交检测只需要计算一个简单的长方体。
using UnityEngine;
using System.Collections.Generic;
namespace ARCommercialDemo.MathOptimization
{
// 空间分区管理器(简化版四叉树实现)
public class SpatialPartitionManager : MonoBehaviour
{
// 分区节点类
private class QuadTreeNode
{
public Bounds bounds;
public List<GameObject> objects;
public QuadTreeNode[] children;
public int level;
public int maxLevel;
public int maxObjects;
public QuadTreeNode(Bounds nodeBounds, int nodeLevel, int maxDepth, int maxObjectsPerNode)
{
bounds = nodeBounds;
level = nodeLevel;
maxLevel = maxDepth;
maxObjects = maxObjectsPerNode;
objects = new List<GameObject>();
}
// 分割节点
public void Split()
{
if (children != null || level >= maxLevel)
{
return;
}
children = new QuadTreeNode[4];
Vector3 childSize = bounds.size / 2f;
Vector3 parentCenter = bounds.center;
// 创建四个子节点
for (int i = 0; i < 4; i++)
{
Vector3 childCenter = parentCenter;
switch (i)
{
case 0: // 左下
childCenter.x -= childSize.x / 2f;
childCenter.z -= childSize.z / 2f;
break;
case 1: // 右下
childCenter.x += childSize.x / 2f;
childCenter.z -= childSize.z / 2f;
break;
case 2: // 左上
childCenter.x -= childSize.x / 2f;
childCenter.z += childSize.z / 2f;
break;
case 3: // 右上
childCenter.x += childSize.x / 2f;
childCenter.z += childSize.z / 2f;
break;
}
Bounds childBounds = new Bounds(childCenter, childSize);
children[i] = new QuadTreeNode(childBounds, level + 1, maxLevel, maxObjects);
}
}
// 插入对象
public bool Insert(GameObject obj)
{
// 检查对象是否在节点范围内
Renderer objRenderer = obj.GetComponent<Renderer>();
if (objRenderer == null || !bounds.Intersects(objRenderer.bounds))
{
return false;
}
// 如果还有空间且未分割,直接添加到当前节点
if (objects.Count < maxObjects && children == null)
{
objects.Add(obj);
return true;
}
// 如果达到容量限制但未分割,先分割
if (children == null)
{
Split();
// 将现有对象重新分配到子节点
foreach (GameObject existingObj in objects)
{
Redistribute(existingObj);
}
objects.Clear();
}
// 尝试将对象插入子节点
for (int i = 0; i < 4; i++)
{
if (children[i].Insert(obj))
{
return true;
}
}
// 如果对象跨越多个子节点,留在父节点
objects.Add(obj);
return true;
}
// 重新分配对象到子节点
private void Redistribute(GameObject obj)
{
Renderer objRenderer = obj.GetComponent<Renderer>();
if (objRenderer == null)
{
return;
}
bool inserted = false;
for (int i = 0; i < 4; i++)
{
if (children[i].bounds.Intersects(objRenderer.bounds))
{
if (children[i].Insert(obj))
{
inserted = true;
}
}
}
// 如果对象未插入任何子节点,留在当前节点
if (!inserted)
{
objects.Add(obj);
}
}
// 查询范围内的对象
public void QueryRange(Bounds range, List<GameObject> result)
{
// 如果查询范围与节点不相交,直接返回
if (!bounds.Intersects(range))
{
return;
}
// 添加当前节点的对象
foreach (GameObject obj in objects)
{
Renderer objRenderer = obj.GetComponent<Renderer>();
if (objRenderer != null && range.Intersects(objRenderer.bounds))
{
result.Add(obj);
}
}
// 查询子节点
if (children != null)
{
for (int i = 0; i < 4; i++)
{
children[i].QueryRange(range, result);
}
}
}
}
// 根节点
private QuadTreeNode rootNode;
// 分区参数
public Bounds worldBounds = new Bounds(Vector3.zero, new Vector3(100f, 0f, 100f));
public int maxDepth = 5;
public int maxObjectsPerNode = 8;
// 所有管理的对象
private Dictionary<GameObject, bool> managedObjects = new Dictionary<GameObject, bool>();
// 初始化
private void Start()
{
InitializeQuadtree();
Debug.Log("空间分区管理器已初始化");
}
// 初始化四叉树
private void InitializeQuadtree()
{
rootNode = new QuadTreeNode(worldBounds, 0, maxDepth, maxObjectsPerNode);
}
// 注册对象到空间分区系统
public void RegisterObject(GameObject obj)
{
if (obj == null || managedObjects.ContainsKey(obj))
{
return;
}
// 确保对象有Renderer组件
if (obj.GetComponent<Renderer>() == null)
{
Debug.LogWarning($"对象 {obj.name} 没有Renderer组件,无法注册到空间分区");
return;
}
if (rootNode.Insert(obj))
{
managedObjects[obj] = true;
Debug.Log($"对象 {obj.name} 已注册到空间分区系统");
}
}
// 从空间分区系统移除对象
public void UnregisterObject(GameObject obj)
{
// 注意:简化实现,实际需要从树中递归移除
managedObjects.Remove(obj);
}
// 查询范围内的所有对象
public List<GameObject> QueryObjectsInRange(Bounds range)
{
List<GameObject> result = new List<GameObject>();
if (rootNode != null)
{
rootNode.QueryRange(range, result);
}
return result;
}
// 执行优化的射线检测
public GameObject RaycastOptimized(Ray ray, out RaycastHit hitInfo, float maxDistance = Mathf.Infinity)
{
hitInfo = new RaycastHit();
// 首先使用空间分区缩小检测范围
// 创建沿射线的边界框用于查询
Vector3 rayEnd = ray.origin + ray.direction * maxDistance;
Bounds rayBounds = new Bounds((ray.origin + rayEnd) / 2f, Vector3.zero);
rayBounds.Encapsulate(ray.origin);
rayBounds.Encapsulate(rayEnd);
// 扩展边界框以包含可能的相交对象
rayBounds.Expand(5f);
// 查询可能相交的对象
List<GameObject> potentialHits = QueryObjectsInRange(rayBounds);
// 执行精确的射线检测
GameObject closestHit = null;
float closestDistance = Mathf.Infinity;
foreach (GameObject obj in potentialHits)
{
RaycastHit hit;
if (Physics.Raycast(ray, out hit, maxDistance) && hit.collider.gameObject == obj)
{
if (hit.distance < closestDistance)
{
closestDistance = hit.distance;
closestHit = obj;
hitInfo = hit;
}
}
}
return closestHit;
}
// 性能分析:比较优化前后的检测效率
public void PerformanceAnalysis(int testIterations = 1000)
{
if (managedObjects.Count == 0)
{
Debug.Log("没有已注册的对象,无法进行性能分析");
return;
}
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
// 测试传统射线检测
stopwatch.Start();
for (int i = 0; i < testIterations; i++)
{
Ray randomRay = GenerateRandomRay();
TraditionalRaycast(randomRay, 100f);
}
stopwatch.Stop();
long traditionalTime = stopwatch.ElapsedMilliseconds;
// 测试优化后的射线检测
stopwatch.Restart();
for (int i = 0; i < testIterations; i++)
{
Ray randomRay = GenerateRandomRay();
RaycastHit hitInfo;
RaycastOptimized(randomRay, out hitInfo, 100f);
}
stopwatch.Stop();
long optimizedTime = stopwatch.ElapsedMilliseconds;
// 输出分析结果
Debug.Log($"性能分析结果({testIterations}次迭代):");
Debug.Log($"传统射线检测耗时:{traditionalTime} ms");
Debug.Log($"优化射线检测耗时:{optimizedTime} ms");
Debug.Log($"性能提升:{((traditionalTime - optimizedTime) / (float)traditionalTime * 100f):F1}%");
}
// 生成随机射线(用于测试)
private Ray GenerateRandomRay()
{
Vector3 randomOrigin = new Vector3(
Random.Range(-worldBounds.extents.x, worldBounds.extents.x),
10f,
Random.Range(-worldBounds.extents.z, worldBounds.extents.z)
);
Vector3 randomDirection = new Vector3(
Random.Range(-1f, 1f),
-1f,
Random.Range(-1f, 1f)
).normalized;
return new Ray(randomOrigin, randomDirection);
}
// 传统射线检测(用于对比)
private GameObject TraditionalRaycast(Ray ray, float maxDistance)
{
RaycastHit[] allHits = Physics.RaycastAll(ray, maxDistance);
GameObject closestHit = null;
float closestDistance = Mathf.Infinity;
foreach (RaycastHit hit in allHits)
{
if (hit.distance < closestDistance && managedObjects.ContainsKey(hit.collider.gameObject))
{
closestDistance = hit.distance;
closestHit = hit.collider.gameObject;
}
}
return closestHit;
}
// 可视化调试:绘制四叉树边界
private void OnDrawGizmosSelected()
{
if (rootNode == null)
{
return;
}
DrawNodeGizmos(rootNode);
}
// 递归绘制节点边界
private void DrawNodeGizmos(QuadTreeNode node)
{
if (node == null)
{
return;
}
// 设置颜色基于节点深度
float depthRatio = node.level / (float)maxDepth;
Gizmos.color = new Color(1f - depthRatio, depthRatio, 0f, 0.3f);
// 绘制节点边界
Gizmos.DrawWireCube(node.bounds.center, node.bounds.size);
// 绘制节点中的对象数量
#if UNITY_EDITOR
UnityEditor.Handles.Label(node.bounds.center, $"Objects: {node.objects.Count}");
#endif
// 递归绘制子节点
if (node.children != null)
{
foreach (QuadTreeNode child in node.children)
{
if (child != null)
{
DrawNodeGizmos(child);
}
}
}
}
}
// 层次包围盒管理器
public class BVHManager : MonoBehaviour
{
// 包围盒节点类
private class BVHNode
{
public Bounds bounds;
public GameObject gameObject;
public BVHNode leftChild;
public BVHNode rightChild;
public bool isLeaf;
public BVHNode(GameObject obj)
{
gameObject = obj;
bounds = CalculateObjectBounds(obj);
isLeaf = true;
}
public BVHNode(BVHNode left, BVHNode right)
{
leftChild = left;
rightChild = right;
bounds = Encapsulate(left.bounds, right.bounds);
isLeaf = false;
}
// 计算对象的包围盒
private Bounds CalculateObjectBounds(GameObject obj)
{
Renderer renderer = obj.GetComponent<Renderer>();
if (renderer != null)
{
return renderer.bounds;
}
// 如果没有Renderer,使用所有子物体的包围盒
Renderer[] childRenderers = obj.GetComponentsInChildren<Renderer>();
if (childRenderers.Length > 0)
{
Bounds combinedBounds = childRenderers[0].bounds;
for (int i = 1; i < childRenderers.Length; i++)
{
combinedBounds.Encapsulate(childRenderers[i].bounds);
}
return combinedBounds;
}
// 默认包围盒
return new Bounds(obj.transform.position, Vector3.one);
}
// 合并两个包围盒
private Bounds Encapsulate(Bounds a, Bounds b)
{
Bounds result = a;
result.Encapsulate(b);
return result;
}
}
// BVH根节点
private BVHNode rootNode;
// 所有叶子节点
private List<BVHNode> leafNodes = new List<BVHNode>();
// 构建BVH
public void BuildBVH(List<GameObject> objects)
{
if (objects == null || objects.Count == 0)
{
return;
}
// 创建叶子节点
leafNodes.Clear();
foreach (GameObject obj in objects)
{
leafNodes.Add(new BVHNode(obj));
}
// 递归构建BVH
rootNode = BuildBVHRecursive(leafNodes);
Debug.Log($"BVH构建完成,共{objects.Count}个对象");
}
// 递归构建BVH
private BVHNode BuildBVHRecursive(List<BVHNode> nodes)
{
if (nodes.Count == 1)
{
return nodes[0];
}
// 找到最佳分割轴
int splitAxis = FindBestSplitAxis(nodes);
// 按分割轴排序
nodes.Sort((a, b) => a.bounds.center[splitAxis].CompareTo(b.bounds.center[splitAxis]));
// 分割节点
int midIndex = nodes.Count / 2;
List<BVHNode> leftNodes = nodes.GetRange(0, midIndex);
List<BVHNode> rightNodes = nodes.GetRange(midIndex, nodes.Count - midIndex);
// 递归构建左右子树
BVHNode leftChild = BuildBVHRecursive(leftNodes);
BVHNode rightChild = BuildBVHRecursive(rightNodes);
return new BVHNode(leftChild, rightChild);
}
// 找到最佳分割轴
private int FindBestSplitAxis(List<BVHNode> nodes)
{
if (nodes.Count <= 1)
{
return 0;
}
// 计算所有节点的总包围盒
Bounds totalBounds = nodes[0].bounds;
for (int i = 1; i < nodes.Count; i++)
{
totalBounds.Encapsulate(nodes[i].bounds);
}
// 选择尺寸最大的轴
Vector3 size = totalBounds.size;
if (size.x >= size.y && size.x >= size.z)
{
return 0; // X轴
}
else if (size.y >= size.x && size.y >= size.z)
{
return 1; // Y轴
}
else
{
return 2; // Z轴
}
}
// BVH射线检测
public bool BVHRaycast(Ray ray, out RaycastHit hitInfo, float maxDistance = Mathf.Infinity)
{
hitInfo = new RaycastHit();
return BVHRaycastRecursive(rootNode, ray, ref hitInfo, maxDistance);
}
// 递归BVH射线检测
private bool BVHRaycastRecursive(BVHNode node, Ray ray, ref RaycastHit hitInfo, float maxDistance)
{
if (node == null || !node.bounds.IntersectRay(ray))
{
return false;
}
if (node.isLeaf)
{
// 叶子节点:执行精确检测
return Physics.Raycast(ray, out hitInfo, maxDistance) &&
hitInfo.collider.gameObject == node.gameObject;
}
else
{
// 内部节点:先检测左子树,再检测右子树
RaycastHit leftHit, rightHit;
bool leftResult = BVHRaycastRecursive(node.leftChild, ray, ref leftHit, maxDistance);
bool rightResult = BVHRaycastRecursive(node.rightChild, ray, ref rightHit, maxDistance);
if (leftResult && rightResult)
{
// 两个子树都命中,选择更近的
if (leftHit.distance < rightHit.distance)
{
hitInfo = leftHit;
return true;
}
else
{
hitInfo = rightHit;
return true;
}
}
else if (leftResult)
{
hitInfo = leftHit;
return true;
}
else if (rightResult)
{
hitInfo = rightHit;
return true;
}
return false;
}
}
// 可视化调试:绘制BVH
private void OnDrawGizmosSelected()
{
if (rootNode == null)
{
return;
}
DrawBVHGizmos(rootNode, 0);
}
// 递归绘制BVH
private void DrawBVHGizmos(BVHNode node, int depth)
{
if (node == null)
{
return;
}
// 设置颜色基于深度
float depthRatio = depth / 10f;
Gizmos.color = new Color(depthRatio, 1f - depthRatio, 0f, 0.2f);
// 绘制包围盒
Gizmos.DrawWireCube(node.bounds.center, node.bounds.size);
// 递归绘制子节点
if (!node.isLeaf)
{
DrawBVHGizmos(node.leftChild, depth + 1);
DrawBVHGizmos(node.rightChild, depth + 1);
}
}
}
}
2.6 商业项目实战:AR室内设计系统
基于前面讨论的数学原理和优化技术,我们现在可以构建一个完整的商业AR室内设计系统。这个系统将整合空间计算、向量数学、四元数旋转、射线检测和性能优化技术。
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
namespace ARCommercialDemo.FullProject
{
// AR室内设计系统主控制器
public class ARInteriorDesignSystem : MonoBehaviour
{
[Header("系统组件")]
public ARFurniturePlacer furniturePlacer;
public SpatialPartitionManager spatialManager;
public BVHManager bvhManager;
[Header("UI组件")]
public GameObject furnitureSelectionPanel;
public Button[] furnitureCategoryButtons;
public Button saveDesignButton;
public Button loadDesignButton;
public Button clearAllButton;
[Header("设计管理")]
public Text designInfoText;
public Slider budgetSlider;
public Text budgetText;
// 当前设计状态
private List<GameObject> placedFurniture = new List<GameObject>();
private float currentBudget = 10000f;
private float totalCost = 0f;
// 家具价格表(商业数据)
private Dictionary<string, float> furniturePrices = new Dictionary<string, float>()
{
{"Sofa", 1500f},
{"Chair", 300f},
{"Table", 800f},
{"Bed", 2000f},
{"Cabinet", 1200f},
{"Lamp", 150f},
{"Bookshelf", 600f}
};
// 初始化
private void Start()
{
InitializeSystem();
SetupUI();
LoadInitialData();
}
// 初始化系统
private void InitializeSystem()
{
if (furniturePlacer == null)
{
furniturePlacer = FindObjectOfType<ARFurniturePlacer>();
}
if (spatialManager == null)
{
spatialManager = FindObjectOfType<SpatialPartitionManager>();
}
if (bvhManager == null)
{
bvhManager = FindObjectOfType<BVHManager>();
}
Debug.Log("AR室内设计系统初始化完成");
}
// 设置UI
private void SetupUI()
{
// 设置家具类别按钮
for (int i = 0; i < furnitureCategoryButtons.Length; i++)
{
int categoryIndex = i;
furnitureCategoryButtons[i].onClick.AddListener(() => SelectFurnitureCategory(categoryIndex));
}
// 设置功能按钮
if (saveDesignButton != null)
{
saveDesignButton.onClick.AddListener(SaveCurrentDesign);
}
if (loadDesignButton != null)
{
loadDesignButton.onClick.AddListener(LoadDesign);
}
if (clearAllButton != null)
{
clearAllButton.onClick.AddListener(ClearAllFurniture);
}
// 设置预算滑块
if (budgetSlider != null)
{
budgetSlider.onValueChanged.AddListener(UpdateBudget);
budgetSlider.value = currentBudget / 20000f; // 假设最大预算20000
}
UpdateDesignInfo();
}
// 加载初始数据
private void LoadInitialData()
{
// 在实际商业项目中,这里会从服务器加载家具数据、用户偏好等
Debug.Log("加载初始数据...");
// 模拟数据加载
Invoke("SimulateDataLoading", 1.0f);
}
// 模拟数据加载
private void SimulateDataLoading()
{
Debug.Log("数据加载完成");
// 初始化空间分区系统
if (spatialManager != null)
{
// 注册已存在的家具
GameObject[] existingFurniture = GameObject.FindGameObjectsWithTag("Furniture");
foreach (GameObject furniture in existingFurniture)
{
spatialManager.RegisterObject(furniture);
placedFurniture.Add(furniture);
}
// 构建BVH
if (bvhManager != null && placedFurniture.Count > 0)
{
bvhManager.BuildBVH(placedFurniture);
}
}
}
// 选择家具类别
private void SelectFurnitureCategory(int categoryIndex)
{
if (furniturePlacer == null)
{
Debug.LogError("家具放置器未初始化");
return;
}
furniturePlacer.SelectFurnitureType(categoryIndex);
// 显示类别名称
string[] categoryNames = {"沙发", "椅子", "桌子", "床", "柜子", "台灯", "书架"};
if (categoryIndex < categoryNames.Length)
{
Debug.Log($"已选择家具类别: {categoryNames[categoryIndex]}");
ShowMessage($"已选择: {categoryNames[categoryIndex]}");
}
}
// 放置家具(由ARFurniturePlacer调用)
public void OnFurniturePlaced(GameObject furniture)
{
if (furniture == null)
{
return;
}
// 添加到已放置列表
placedFurniture.Add(furniture);
// 注册到空间分区系统
if (spatialManager != null)
{
spatialManager.RegisterObject(furniture);
}
// 更新BVH
UpdateBVH();
// 计算成本
CalculateFurnitureCost(furniture);
// 更新设计信息
UpdateDesignInfo();
// 检查预算
CheckBudget();
Debug.Log($"家具已添加: {furniture.name}, 总计 {placedFurniture.Count} 件家具");
}
// 计算家具成本
private void CalculateFurnitureCost(GameObject furniture)
{
// 从家具名称提取类型
string furnitureName = furniture.name.ToLower();
float cost = 0f;
// 简单匹配逻辑(实际项目会使用更精确的匹配)
if (furnitureName.Contains("sofa"))
{
cost = furniturePrices["Sofa"];
}
else if (furnitureName.Contains("chair"))
{
cost = furniturePrices["Chair"];
}
else if (furnitureName.Contains("table"))
{
cost = furniturePrices["Table"];
}
else if (furnitureName.Contains("bed"))
{
cost = furniturePrices["Bed"];
}
else if (furnitureName.Contains("cabinet"))
{
cost = furniturePrices["Cabinet"];
}
else if (furnitureName.Contains("lamp"))
{
cost = furniturePrices["Lamp"];
}
else if (furnitureName.Contains("bookshelf"))
{
cost = furniturePrices["Bookshelf"];
}
totalCost += cost;
// 添加成本组件到家具(用于显示)
FurnitureCost furnitureCost = furniture.AddComponent<FurnitureCost>();
furnitureCost.SetCost(cost);
Debug.Log($"家具成本: ${cost}, 总成本: ${totalCost}");
}
// 更新BVH
private void UpdateBVH()
{
if (bvhManager != null && placedFurniture.Count > 0)
{
bvhManager.BuildBVH(placedFurniture);
}
}
// 更新设计信息
private void UpdateDesignInfo()
{
if (designInfoText != null)
{
string info = $"设计信息:\n";
info += $"家具数量: {placedFurniture.Count}\n";
info += $"总成本: ${totalCost:F2}\n";
info += $"预算: ${currentBudget:F2}\n";
info += $"剩余: ${(currentBudget - totalCost):F2}\n";
designInfoText.text = info;
}
if (budgetText != null)
{
budgetText.text = $"预算: ${currentBudget:F2}";
}
}
// 更新预算
private void UpdateBudget(float sliderValue)
{
currentBudget = sliderValue * 20000f; // 映射到0-20000范围
UpdateDesignInfo();
CheckBudget();
}
// 检查预算
private void CheckBudget()
{
if (totalCost > currentBudget)
{
ShowMessage($"警告: 超出预算 ${(totalCost - currentBudget):F2}!");
Debug.LogWarning($"预算超支: 当前成本 ${totalCost}, 预算 ${currentBudget}");
// 在实际商业项目中,这里可能会触发额外的逻辑
// 如:阻止继续添加家具、显示购买提示等
}
}
// 保存当前设计
private void SaveCurrentDesign()
{
if (placedFurniture.Count == 0)
{
ShowMessage("没有可保存的设计");
return;
}
// 创建设计数据
DesignData designData = new DesignData();
designData.designName = $"设计_{System.DateTime.Now:yyyyMMdd_HHmmss}";
designData.creationDate = System.DateTime.Now;
designData.totalCost = totalCost;
designData.budget = currentBudget;
designData.furnitureCount = placedFurniture.Count;
// 收集家具数据
designData.furnitureItems = new List<FurnitureItemData>();
foreach (GameObject furniture in placedFurniture)
{
FurnitureItemData itemData = new FurnitureItemData();
itemData.name = furniture.name;
itemData.position = furniture.transform.position;
itemData.rotation = furniture.transform.rotation;
itemData.scale = furniture.transform.localScale;
FurnitureCost costComponent = furniture.GetComponent<FurnitureCost>();
if (costComponent != null)
{
itemData.cost = costComponent.GetCost();
}
designData.furnitureItems.Add(itemData);
}
// 在实际商业项目中,这里会将designData保存到文件或上传到服务器
string jsonData = JsonUtility.ToJson(designData, true);
Debug.Log($"设计已保存:\n{jsonData}");
ShowMessage($"设计 '{designData.designName}' 已保存");
// 商业逻辑:记录设计保存事件
LogDesignEvent("design_saved", designData);
}
// 加载设计
private void LoadDesign()
{
// 在实际商业项目中,这里会从文件或服务器加载设计数据
Debug.Log("加载设计...");
ShowMessage("加载设计功能开发中...");
// 示例:加载示例设计
LoadExampleDesign();
}
// 加载示例设计(演示用)
private void LoadExampleDesign()
{
ClearAllFurniture();
// 创建示例家具布局
Vector3 roomCenter = Vector3.zero;
// 放置一个沙发
if (furniturePlacer != null && furniturePlacer.furniturePrefabs.Length > 0)
{
GameObject sofaPrefab = furniturePlacer.furniturePrefabs[0];
GameObject sofa = Instantiate(sofaPrefab, roomCenter + new Vector3(0, 0, -1), Quaternion.identity);
OnFurniturePlaced(sofa);
}
// 放置一个桌子
if (furniturePlacer != null && furniturePlacer.furniturePrefabs.Length > 2)
{
GameObject tablePrefab = furniturePlacer.furniturePrefabs[2];
GameObject table = Instantiate(tablePrefab, roomCenter + new Vector3(0, 0, 1), Quaternion.identity);
OnFurniturePlaced(table);
}
// 放置两把椅子
if (furniturePlacer != null && furniturePlacer.furniturePrefabs.Length > 1)
{
GameObject chairPrefab = furniturePlacer.furniturePrefabs[1];
GameObject chair1 = Instantiate(chairPrefab, roomCenter + new Vector3(-1, 0, 1),
Quaternion.Euler(0, -90, 0));
OnFurniturePlaced(chair1);
GameObject chair2 = Instantiate(chairPrefab, roomCenter + new Vector3(1, 0, 1),
Quaternion.Euler(0, 90, 0));
OnFurniturePlaced(chair2);
}
ShowMessage("示例设计已加载");
}
// 清除所有家具
private void ClearAllFurniture()
{
foreach (GameObject furniture in placedFurniture)
{
if (furniture != null)
{
Destroy(furniture);
}
}
placedFurniture.Clear();
totalCost = 0f;
// 清空空间分区
if (spatialManager != null)
{
// 注意:简化实现,实际需要更完整的清理
spatialManager = FindObjectOfType<SpatialPartitionManager>();
}
UpdateDesignInfo();
ShowMessage("所有家具已清除");
Debug.Log("所有家具已清除");
}
// 显示消息
private void ShowMessage(string message)
{
Debug.Log($"系统消息: {message}");
// 在实际商业项目中,这里会显示到UI消息系统
// 例如:messageText.text = message;
// 然后使用协程淡出
}
// 记录设计事件(商业分析)
private void LogDesignEvent(string eventType, DesignData designData = null)
{
Debug.Log($"设计事件: {eventType}, 时间: {System.DateTime.Now}");
// 在实际商业项目中,这里会发送数据到分析服务器
// 例如:用户保存设计的频率、平均家具数量、常用家具类型等
// SendAnalyticsData("design_event", new {
// event_type = eventType,
// furniture_count = placedFurniture.Count,
// total_cost = totalCost,
// timestamp = System.DateTime.Now
// });
}
// 性能优化测试
public void RunPerformanceTest()
{
if (spatialManager != null)
{
spatialManager.PerformanceAnalysis(1000);
}
}
// 导出设计报告(商业功能)
public void ExportDesignReport()
{
if (placedFurniture.Count == 0)
{
ShowMessage("没有设计可导出");
return;
}
// 生成报告
string report = "=== AR室内设计报告 ===\n";
report += $"生成时间: {System.DateTime.Now}\n";
report += $"家具总数: {placedFurniture.Count}\n";
report += $"总成本: ${totalCost:F2}\n";
report += $"预算: ${currentBudget:F2}\n";
report += $"预算状态: {(totalCost <= currentBudget ? "在预算内" : "超预算")}\n\n";
report += "家具清单:\n";
report += "----------------------------------------\n";
// 按类型统计
Dictionary<string, int> typeCount = new Dictionary<string, int>();
Dictionary<string, float> typeCost = new Dictionary<string, float>();
foreach (GameObject furniture in placedFurniture)
{
string type = GetFurnitureType(furniture);
if (!typeCount.ContainsKey(type))
{
typeCount[type] = 0;
typeCost[type] = 0f;
}
typeCount[type]++;
FurnitureCost costComponent = furniture.GetComponent<FurnitureCost>();
if (costComponent != null)
{
typeCost[type] += costComponent.GetCost();
}
}
foreach (var kvp in typeCount)
{
string type = kvp.Key;
int count = kvp.Value;
float cost = typeCost.ContainsKey(type) ? typeCost[type] : 0f;
report += $"{type}: {count} 件, 总价: ${cost:F2}, 均价: ${(cost/count):F2}\n";
}
report += "\n=== 报告结束 ===\n";
Debug.Log(report);
ShowMessage("设计报告已生成(查看控制台)");
// 在实际商业项目中,这里会生成PDF或分享到其他应用
}
// 获取家具类型
private string GetFurnitureType(GameObject furniture)
{
string name = furniture.name.ToLower();
if (name.Contains("sofa")) return "沙发";
if (name.Contains("chair")) return "椅子";
if (name.Contains("table")) return "桌子";
if (name.Contains("bed")) return "床";
if (name.Contains("cabinet")) return "柜子";
if (name.Contains("lamp")) return "台灯";
if (name.Contains("bookshelf")) return "书架";
return "其他";
}
// 设计数据类
[System.Serializable]
public class DesignData
{
public string designName;
public System.DateTime creationDate;
public float totalCost;
public float budget;
public int furnitureCount;
public List<FurnitureItemData> furnitureItems;
}
// 家具数据类
[System.Serializable]
public class FurnitureItemData
{
public string name;
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public float cost;
}
}
// 家具成本组件
public class FurnitureCost : MonoBehaviour
{
private float cost = 0f;
public void SetCost(float furnitureCost)
{
cost = furnitureCost;
}
public float GetCost()
{
return cost;
}
// 显示成本(在Scene视图中)
private void OnDrawGizmosSelected()
{
#if UNITY_EDITOR
UnityEditor.Handles.Label(
transform.position + Vector3.up * 0.5f,
$"${cost:F2}"
);
#endif
}
}
}
本章通过深入讲解AR开发中的数学基础,结合完整的商业项目实例,展示了如何将数学理论应用于实际的AR开发中。从基本的空间坐标变换到高级的性能优化技术,这些数学原理是构建高质量商业AR应用的基石。通过理解和应用这些概念,开发者可以创建出更加精准、高效和用户友好的AR体验,为商业应用带来真正的价值。
更多推荐
所有评论(0)