如何解决SphereCast透视问题,实现相机视图内最近物体精准检测?
问题描述
我正在开发一个通过半径递增的SphereCast检测点击首个触碰物体的功能,预期目标为首个物体=相机视图中距离最近的物体。
- 图1:鼠标指向蓝色物体,检测结果符合预期,命中蓝色物体
- 图2:鼠标靠近蓝色物体,但SphereCast却先检测到红色物体——这是因为3D场景中物体处于不同高度,随着球体半径逐渐增大,会先触碰到红色物体
当前实现代码:
private float maxSphereCastRadius = 0.01f; private void DetectObject(float sphereCastRadius) { while (true) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.SphereCast(ray, sphereCastRadius, out RaycastHit hit)) { LoadInfo(hit.collider.transform); } else { if (sphereCastRadius > maxSphereCastRadius) return; sphereCastRadius += 0.0001f; continue; } break; } }
在鼠标点击位置调用DetectObject(0.001f),球体半径会逐渐增大直到检测到物体。功能整体可行,但希望解决因透视导致的检测偏差问题。
解决方案
方法1:基于屏幕空间距离筛选(推荐)
放弃递增半径找单个命中的逻辑,直接获取所有被球射线命中的物体,计算每个物体命中点与鼠标的屏幕距离,选择距离最近的:
private void DetectObject() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); float maxRadius = 0.01f; // 获取该半径下所有命中的物体 RaycastHit[] hits = Physics.SphereCastAll(ray, maxRadius); if (hits.Length == 0) return; float closestScreenDistSqr = float.MaxValue; Transform target = null; foreach (var hit in hits) { // 将世界空间的命中点转换为屏幕坐标 Vector3 screenPos = Camera.main.WorldToScreenPoint(hit.point); // 计算屏幕距离的平方(避免开根号,提升性能) float distSqr = (Input.mousePosition - new Vector3(screenPos.x, screenPos.y)).sqrMagnitude; if (distSqr < closestScreenDistSqr) { closestScreenDistSqr = distSqr; target = hit.collider.transform; } } if (target != null) { LoadInfo(target); } }
这种方式完全以屏幕视觉距离为判断标准,彻底解决不同高度物体的误判问题。
方法2:保留递增逻辑,增加屏幕距离校验
如果一定要保留半径递增的逻辑,可以在每次命中时,校验命中点的屏幕位置是否足够接近鼠标,否则继续增大半径:
private float maxSphereCastRadius = 0.01f; // 屏幕距离阈值(像素),根据需求调整 private float screenDistanceThreshold = 5f; private void DetectObject(float sphereCastRadius) { while (true) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.SphereCast(ray, sphereCastRadius, out RaycastHit hit)) { Vector3 screenPos = Camera.main.WorldToScreenPoint(hit.point); float screenDistance = Vector2.Distance(Input.mousePosition, new Vector2(screenPos.x, screenPos.y)); // 屏幕距离符合阈值,认定为目标 if (screenDistance <= screenDistanceThreshold) { LoadInfo(hit.collider.transform); break; } // 超过最大半径还没找到符合的,就用当前命中的 else if (sphereCastRadius >= maxSphereCastRadius) { LoadInfo(hit.collider.transform); break; } } if (sphereCastRadius > maxSphereCastRadius) return; sphereCastRadius += 0.0001f; } }
方法3:使用Unity内置交互系统
如果是做3D物体的点击交互,直接用Unity的EventSystem配合IPointerClickHandler接口,Unity会自动处理屏幕空间的点击检测,无需手动写射线逻辑,完全规避透视偏差:
- 给需要交互的3D物体添加
Collider组件(必须) - 添加
EventSystem到场景(如果没有) - 给物体挂载实现
IPointerClickHandler的脚本:
using UnityEngine; using UnityEngine.EventSystems; public class ClickHandler : MonoBehaviour, IPointerClickHandler { public void OnPointerClick(PointerEventData eventData) { LoadInfo(transform); } }
内容的提问来源于stack exchange,提问作者Digger
相关产品推荐
相关产品推荐

