找回密码
 立即注册
查看: 154|回复: 0

Unity 射线检测的原理分析

[复制链接]
发表于 2022-11-28 18:34 | 显示全部楼层 |阅读模式
首先让我们带着问题来分析
1、什么是射线?
2、当我们点击屏幕的时候为什么能点击到物体呢?
3、2d物体和3d物体的检测原理一样吗?
带着这几个问题我们开始进入我们的正文。

一、什么是射线?


要搞明白这个问题,让我们直接看源代码分析下

public struct Ray
{
    private Vector3 m_Origin;
    private Vector3 m_Direction;

    public Ray(Vector3 origin, Vector3 direction)
    {
      this.m_Origin = origin;
      this.m_Direction = direction.normalized;
    }

    public Vector3 origin
    {
      get => this.m_Origin;
      set => this.m_Origin = value;
    }

    public Vector3 direction
    {
      get => this.m_Direction;
      set => this.m_Direction = value.normalized;
    }

    public Vector3 GetPoint(float distance) => this.m_Origin + this.m_Direction * distance;

}

这段代码很简单,只包含两个属性,m_Origin 代表原点,m_Direction代表方向,且是归一化的(归一化就是将数据统一映射到[0,1]区间上)。所以射线就是有原点有方向的一条线。

那射线怎么用呢?我们看下射线在Unity中的具体应用

二、射线在Unity中的应用


1、在触摸事件源码分析中,我们提到在检测触摸事件时用到了eventSystem.RaycastAll(pointerData, m_RaycastResultCache) 接口,其中就调用了RectTransformUtility.RectangleContainsScreenPoint接口,用于判断点击屏幕的点是否处于某个图片范围内。
2、在3d场景中,我们要拖动3d物体移动到某个位置,也是用到了射线。

还有我们经常用到的 RectTransformUtility.ScreenPointToWorldPointInRectangle 、RectTransformUtility.ScreenPointToLocalPointInRectangle 等都用到了射线。

那射线检测的原理是怎样的呢?

我们拿 RectTransformUtility.ScreenPointToWorldPointInRectangle 来举例说明。让我们看下源码
public static bool ScreenPointToWorldPointInRectangle(RectTransform rect, Vector2 screenPoint, Camera cam, out Vector3 worldPoint)
{
    worldPoint = Vector2.zero;
    Ray ray = ScreenPointToRay(cam, screenPoint);
    var plane = new Plane(rect.rotation * Vector3.back, rect.position);

    float dist;
    if (!plane.Raycast(ray, out dist))
        return false;

    worldPoint = ray.GetPoint(dist);
    return true;
}

public static Ray ScreenPointToRay(Camera cam, Vector2 screenPos)
{
    if (cam != null)
        return cam.ScreenPointToRay(screenPos);

    Vector3 pos = screenPos;
    pos.z -= 100f;
    return new Ray(pos, Vector3.forward);
}
ScreenPointToRay的作用是将屏幕坐标转换成 Ray 对象,Ray对象是根据摄像机近裁剪面、远裁剪面和屏幕坐标经过视口矩阵、投影矩阵的相关矩阵变化得来的一条以近裁剪面上的一个点为原点射向远裁剪面的某一点的射线。

得到射线后,根据射线以及Plane 对象就能得出Plane平面上的一个点,这个点就是要得到的坐标点。
如果plane.Raycast 为false,就说明触摸点不在这个rect范围内。

也许有的人看到这里还是一脸懵逼,Ray还没弄懂呢,Plane又是什么东西?我们一起来看看

三、Plane是什么?

让我们还是看源码,源码之下无秘密
public partial struct Plane
{
    // sizeof(Plane) is not const in C# and so cannot be used in fixed arrays, so we define it here
    internal const int size = 16;

    Vector3 m_Normal;
    float m_Distance;

    // Normal vector of the plane.
    public Vector3 normal
    {
        get { return m_Normal; }
        set { m_Normal = value; }
    }
    // Distance from the origin to the plane.
    public float distance
    {
        get { return m_Distance; }
        set { m_Distance = value; }
    }

    // Creates a plane.
    public Plane(Vector3 inNormal, Vector3 inPoint)
    {
        m_Normal = Vector3.Normalize(inNormal);
        m_Distance = -Vector3.Dot(m_Normal, inPoint);
    }

    // Creates a plane.
    public Plane(Vector3 inNormal, float d)
    {
        m_Normal = Vector3.Normalize(inNormal);
        m_Distance = d;
    }

    // Creates a plane.
    public Plane(Vector3 a, Vector3 b, Vector3 c)
    {
        m_Normal = Vector3.Normalize(Vector3.Cross(b - a, c - a));
        m_Distance = -Vector3.Dot(m_Normal, a);
    }

    // Sets a plane using a point that lies within it plus a normal to orient it (note that the normal must be a normalized vector).
    public void SetNormalAndPosition(Vector3 inNormal, Vector3 inPoint)
    {
        m_Normal = Vector3.Normalize(inNormal);
        m_Distance = -Vector3.Dot(inNormal, inPoint);
    }

    // Sets a plane using three points that lie within it.  The points go around clockwise as you look down on the top surface of the plane.
    public void Set3Points(Vector3 a, Vector3 b, Vector3 c)
    {
        m_Normal = Vector3.Normalize(Vector3.Cross(b - a, c - a));
        m_Distance = -Vector3.Dot(m_Normal, a);
    }


    // Translates the plane into a given direction
    public void Translate(Vector3 translation) { m_Distance += Vector3.Dot(m_Normal, translation); }

    // Creates a plane that's translated into a given direction
    public static Plane Translate(Plane plane, Vector3 translation) { return new Plane(plane.m_Normal, plane.m_Distance += Vector3.Dot(plane.m_Normal, translation)); }

    // Calculates the closest point on the plane.
    public Vector3 ClosestPointOnPlane(Vector3 point)
    {
        var pointToPlaneDistance = Vector3.Dot(m_Normal, point) + m_Distance;
        return point - (m_Normal * pointToPlaneDistance);
    }

    // Returns a signed distance from plane to point.
    public float GetDistanceToPoint(Vector3 point) { return Vector3.Dot(m_Normal, point) + m_Distance; }

    // Is a point on the positive side of the plane?
    public bool GetSide(Vector3 point) { return Vector3.Dot(m_Normal, point) + m_Distance > 0.0F; }

    // Are two points on the same side of the plane?
    public bool SameSide(Vector3 inPt0, Vector3 inPt1)
    {
        float d0 = GetDistanceToPoint(inPt0);
        float d1 = GetDistanceToPoint(inPt1);
        return (d0 >  0.0f && d1 >  0.0f) ||
            (d0 <= 0.0f && d1 <= 0.0f);
    }

    // Intersects a ray with the plane.
    public bool Raycast(Ray ray, out float enter)
    {
        float vdot = Vector3.Dot(ray.direction, m_Normal);
        float ndot = -Vector3.Dot(ray.origin, m_Normal) - m_Distance;

        if (Mathf.Approximately(vdot, 0.0f))
        {
            enter = 0.0F;
            return false;
        }

        enter = ndot / vdot;

        return enter > 0.0F;
    }

}

我们发现这个类很简单,就包含两个属性 m_Normal 法线和 m_Distance 距离(表示从原点到这个平面的距离),那有了这两个属性为什么就可以获取到该平面上的某个点呢?

我们着重分析下面这个接口:
public bool Raycast(Ray ray, out float enter)
{
    float vdot = Vector3.Dot(ray.direction, m_Normal);
    float ndot = -Vector3.Dot(ray.origin, m_Normal) - m_Distance;

    if (Mathf.Approximately(vdot, 0.0f))
    {
        enter = 0.0F;
        return false;
    }

    enter = ndot / vdot;

    return enter > 0.0F;
}
这个接口是求射线和平面的相交的。

这里面用到了点乘。有很多同学不知道点乘的几何意义,让我们先复习下,然后回过头再来分析这个接口
四、点乘和叉乘的几何意义

在空间中有两个向量: \vec a=(x_1,y_1,z_1) ,\vec b = (x_2,y_2,z_2), \vec a  与 \vec b 之间的夹角为 \theta ,
从代数角度看,点积是对两个向量对应位置上的值相乘再相加的操作,其结果即为点积。
\vec a . \vec b = x_1.x_2 + y_1.y_2 + z_1 .z_2  
从几何角度看,点积是两个向量的长度与它们夹角余弦的积。
\vec a.\vec b = |\vec a||\vec b|cos\theta   



几何意义:

点乘的结果表示 \vec a 在 \vec b 方向的投影与 |\vec b| 的乘积,反映了两个向量在方向上的相似度,结果越大越相似。基于结果可以判断这两个向量是否是同一方向,是否正交垂直,具体对应关系为:
1、 \vec a\cdot \vec b>0  则方向基本相同,夹角在0°到90°之间
2、 \vec a\cdot \vec b=0 则正交,相互垂直
3、 \vec a\cdot \vec b<0 则方向基本相反,夹角在90°到180°之间

点乘代数定义推导几何定义:(常用来求向量夹角)

设 \vec a 终点为 A(x_1,y_1,z_1) , \vec b 终点为 B(x2,y2,z2),原点为 O ,则
在  △OAB  中,由余弦定理得:
\left |\vec {AB}\right |^2=\left |\vec a  \right |^2+\left |\vec b  \right |^2-2\left |\vec a  \right |\left |\vec b  \right |\cos\theta
使用距离公式进行处理,可得:
\left |\vec a  \right |\left |\vec b  \right |\cos\theta=\frac {x_1^2+y_1^2+z_1^2+x_2^2+y_2^2+z_2^2-[(x_2-x_1)^2+(y_2-y_1)^2+(z_2-z_1)^2]}{2}
去括号后合并,可得:
\left |\vec a  \right |\left |\vec b  \right |\cos\theta=x_1x_2+y_1y_2+z_1z_2=\vec a\cdot \vec b
根据上面的工式可计算 \vec a 与 \vec b 之间的夹角:\theta=\arccos (\frac {\vec a\cdot\vec b} {\left |\vec a \right |\left |\vec b \right |})

五、理解了点乘的概念,我们再来看下上面那个接口

public bool Raycast(Ray ray, out float enter)
{
    float vdot = Vector3.Dot(ray.direction, m_Normal);   // 求射线和平面法线的夹角, 设为 cosθ
    float ndot = -Vector3.Dot(ray.origin, m_Normal) - m_Distance;  // 求射线原点到平面的距离 d

    if (Mathf.Approximately(vdot, 0.0f))
    {
        enter = 0.0F;
        return false;
    }

    enter = ndot / vdot;  // 射线原点与平面相交点 L = d/cosθ

    return enter > 0.0F;
}

通过注释相信你已经理解了这个接口,通过L 代入到 ray.GetPoint(L);就可以求得我们点击到平面上某个点的世界坐标。

以上就是射线的用法,其中有很多数学和几何方面的知识,如果对这方面有所理解的话,那么理解射线检测就没什么难的了。

其中矩阵变换我没有讲,因为那不是本文的重点,本文重点是带大家理解射线的原理和应用,让大家在开发中用起来能够得心应手,我们的目的就达到了。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-6-27 06:25 , Processed in 0.185415 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表