You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决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会自动处理屏幕空间的点击检测,无需手动写射线逻辑,完全规避透视偏差:

  1. 给需要交互的3D物体添加Collider组件(必须)
  2. 添加EventSystem到场景(如果没有)
  3. 给物体挂载实现IPointerClickHandler的脚本:
using UnityEngine;
using UnityEngine.EventSystems;

public class ClickHandler : MonoBehaviour, IPointerClickHandler
{
    public void OnPointerClick(PointerEventData eventData)
    {
        LoadInfo(transform);
    }
}

内容的提问来源于stack exchange,提问作者Digger

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 11:21:27