Unity Raycasters 剖析
Raycasters 用来检测当前事件发送给哪个对象,检测原理就是 Raycast。当给定一个屏幕坐标系中的位置,Raycasters 就会利用射线检测寻找潜在的对象,并返回一个离当前屏幕最近的对象。
在 Unity Raycasters 中有三种类型的 Raycasters:
Graphic Raycaster - 存在于 Canvas 下,用于检测 Canvas 中所有的物体
Physics 2D Raycaster - 用于检测 2D 物体
Physics Raycaster - 用于检测 3D 物体
接下来,就来分析一下各个类型 Raycaster 的源码来看看其的工作流程。
Raycast 在 Event System 流程中所处的位置大致如下图:
BaseRaycaster 类
Unity Raycasters 中的三个 Raycaster 类都继承自 BaseRaycaster。首先就来看看 BaseRaycaster 类。
BaseRaycaster 类很简单,它包含一个抽象方法 Raycast
,定义如下:
|
|
这个方法供子类覆写以实现对不同类别的物体进行射线检测。BaseRaycaster 类还继承自 UIBehaviour 类,因此它还覆写了 OnEnable
和 OnDisable
方法,在 OnEnable
方法中向 RaycasterManager 类注册了自己,在 OnDisable
方法中从 RaycasterManager 类移除了自己的注册。
另外该类中还包含了 eventCamera、sortOrderPriority、renderOrderPriority 等属性,在射线检测物体时会用到。
Physics Raycaster
Physics Raycaster 用于检测场景中的 3D 物体对象。
PhysicsRaycaster 类继承自 BaseRaycaster,既然是射线检测那么最重要的方法莫过于 Raycast
,接下来就一起看看这个方法。
在 Raycast
方法中,首先使用传入的 PointerEventData 参数调用 ComputeRayAndDistance
方法,计算得到从当前射线检测使用的 Camera 的近裁剪面处出发,穿过屏幕事件发生处位置的一条射线;这个方法还会计算一个射线检测使用的最大距离 distanceToClipPlane
。
ComputeRayAndDistance
内部使用了 Camera 类的 ScreenPointToRay
方法将某点转换成一条射线,根据得到的射线的方向以及 Camera 的 farClipPlane 和 nearClipPlane 求得检测最大距离 distanceToClipPlane
。具体代码如下:
|
|
接下来就是进行射线检测了,代码如下:
|
|
这里的 ReflectionMethodsCache
类里面缓存了一些通过反射得到的射线检测相关的类方法。在上面的代码中使用了 raycast3DAll
这个代理,最终执行的是 Physics 类的 RaycastAll
方法。传入的三个参数就是射线 ray,最大检测距离 distanceToClipPlane 以及需要检测的层 finalEventMask,返回结果就是检测成功得到的 RaycastHit 数组。第三个参数 finalEventMask 定义如下:
|
|
我们知道,射线检测的时候可以设置哪些 layer 可以接收检测碰撞。上面定义的 finalEventMask 就是需要检测的 layer,如果当前 raycaster 所在的对象有 Camera 组件,那么 finalEventMask 就是摄像机设置的渲染的所有层(eventCamera.cullingMask & m_EventMask
),否则就是默认所有的层(int kNoEventMaskSet = -1
)都可以接收射线碰撞检测。
然后对检测得到的 RaycastHit 数组按照 distance 由小到大排序。最后将这些射线检测结果依次拼装成 RaycastResult 并返回给 Event System,这里的 RaycastResult 中的 distance 就是 RaycastHit 的 distance(射线起点到射线碰撞点的距离)。
Physics2D Raycaster
Physics2DRaycaster 类继承自 PhysicsRaycaster,主要就是 Raycast
方法中的一点点细小的区别。
第一,在进行射线检测的时候,Physics2DRaycaster 中最后调用的是 Physics2D 的 GetRayIntersectionAll
方法。
第二处同 PhysicsRaycaster 的不同之处是在返回构造 RaycastResult 时,填充的部分值不一样,包括以下几个:
distance,这个值是摄像机到射线检测碰撞点的距离,而在 PhysicsRaycaster 中是 RaycastHit 的
distance
值(射线起点在近裁剪面发出到碰撞点的距离)。sortingLayer,这个值是当前对象 SpriteRenderer 组件中的
sortingLayerID
值,在 PhysicsRaycaster 为 0。sortingOrder,这个同样为当前对象 SpriteRenderer 组件中的
sortingOrder
值,在 PhysicsRaycaster 为 0。
Graphic Raycaster
Graphic Raycaster 用于射线检测 Canvas 中的 Graphic 对象物体,通常绑定在 Canvas 所在的对象身上。
属性或方法
GraphicRaycaster 类的成员属性很少,除了继承 BaseRaycaster 类的一些属性和方法外,它还拥有以下一些常用的属性或方法:
属性 | 描述 |
---|---|
Ignore Reversed Graphics |
射线检测时是否忽略背向的 Graphics |
Blocked Objects |
哪些类型的对象会阻挡 Graphic raycasts |
Blocking Mask |
哪些 Layer 会阻挡 Graphic raycasts(对 Blocked Objects 指定的对象生效) |
不同于 PhysicsRaycaster 和 Physics2DRaycaster 类中直接使用父类的 sortOrderPriority
方法和 renderOrderPriority
,GraphicRaycaster 覆写了这两个方法,并且当 Canvas 的 render mode 设置为 RenderMode.ScreenSpaceOverlay
时,上面两个方法分别返回 canvas 的 sortingOrder 以及 rootCanvas 的 renderOrder。
对于 eventCamera 的 get 方法,如果 Canvas 的 render mode 设置为 RenderMode.ScreenSpaceOverlay
或者 enderMode.ScreenSpaceCamera
并且 Canvas 的 worldCamera 未设置时,返回 null,否则返回 Canvas 的 worldCamera 或者 Main Camera。
GraphicRaycaster.Raycast
接下来就来到最重要的覆写的 Raycast
方法。
首先调用 GraphicRegistry.GetGraphicsForCanvas
方法获取当前 Canvas 下所有的 Graphic(canvasGraphics,这些 Graphics 在进行射线检测的时候会用到)。
紧接着就是 MultiDisplay 的一些检测,代码如下:
|
|
可以看出,当平台支持 MultiDisplay 时,如果用户操作的不是当前的 Display,那么所有的其他 Display 上产生的事件都会被舍弃。
然后将屏幕坐标转换到 Camera 视窗坐标下。如果 eventCamera 不为空,则使用 Camera.ScreenToViewportPoint
方法转换坐标,否则直接使用当前 Display 的宽高除以 eventPosition 转换为视窗坐标([0,1]之间)。转换后的坐标若超出 Cmera 的范围(0 - 1),则舍弃该事件。
Blocked Objects 和 Blocked Mask 出场
前面讲到 GraphicRaycaster 可以设置 Blocked Objects 和 Blocked Mask 来指定射线检测阻挡,下面一步就到了使用这两个属性来阻断射线检测部分。
|
|
当 Canvas renderMode 不为 RenderMode.ScreenSpaceOverlay
并且设置了 blockingObjects,此时就会 Blocked Objects 和 Blocked Mask 就会生效。
如果 blockingObjects 包含了
BlockingObjects.ThreeD
那么则会使用ReflectionMethodsCache.Singleton.raycast3DAll
方法计算 hitDistance(PhysicsRaycaster 中也使用的该方法进行射线检测)。如果 blockingObjects 也包含了
BlockingObjects.TwoD
,那么会使用ReflectionMethodsCache.Singleton.getRayIntersectionAll
方法(Physics2DRaycaster 射线检测使用)再计算 hitDistance。
具体的计算过程大致是: 这上面的代码中 raycast3DAll 时指定了射线检测层 m_BlockingMask
,这个参数就是自定义设定的 Blocking Mask
,属于 block mask 的对象在这里就会就行射线检测,并得到最小的一个 hitDistance;后面对所有的 Graphics 进行射线检测时,如果检测结果 distance 大于 hitDistance,那么那个结果会被舍弃。如此一来,Blocking Mask
就起到了阻挡的作用,属于这个 layer 的所有对象的一旦被射线检测成功并得到 hitDistance,PhysicsRaycaster 最后的射线检测结果都只会包含这个 hitDistance 距离以内的对象。
GraphicRaycaster 类重载了 “真” Raycast 方法
终于可以进行真真切切的 Graphic Raycast 了。
|
|
在循环中对每一个 Graphic 首先进行了初步的筛选,满足条件的 Graphic 才会调用其 Raycast
方法,这里的条件筛选包括 deth、raycastTarget 设置、位置信息是否满足等。
Graphic.Raycast
对 Canvas 下所有的 graphic 遍历,满足条件则进行射线检测。Graphic 射线检测过程如下:
整个检测过程是在一个循环中实现的,从当前 Graphic 所在节点开始往祖先节点不断递归,直至向上再没有节点或者节点绑定的组件中有被射线检测出不合法而返回。
对于节点对象,首先获取其绑定的所有组件,依次遍历判断组件:
若组件不是
Canvas
或者是 其Canvas
但是其属性 overrideSorting 为false
,此时的检测过程如下: 判断组件是否是ICanvasRaycastFilter
,如不是则继续下一个组件判断;若是则调用ICanvasRaycastFilter
的IsRaycastLocationValid
方法判断事件发生位置相对这个节点对象是否是合法的,如果不合法直接跳出循环和遍历,Raycast
返回false
,表示用于检测的 Graphic 不需要接收此事件;若所有的组件检测都合法且IsRaycastLocationValid
都则返回true
,则继续遍历下一个父节点对象。上一步的检测过程中,当节点遍历完成还没有返回那么就
Raycast
方法返回true
表示用于检测的 Graphic 可以作为事件接收对象。另一种遍历完成的条件为当遍历到某个节点的某个组件,这个组件是
Canvas
并且其 overrideSorting 为true
,在这种情况下会将continueTraversal
局部变量设置为false
表示到这个节点遍历就可以完成了;当前这个节点的判断计算过程同第一步中相同: 在当前节点绑定的一系列实现了ICanvasRaycastFilter
接口的组件上调用IsRaycastLocationValid
方法判断事件发生位置相对这个 Graphic 是否是合法的,如果不合法Raycast
方法直接返回false
,表示当前 Graphic 不需要接收此事件,否则所有的组件检测都合法返回true
,表示当前 Graphic 需要作为事件接收对象。在上面对实现了
ICanvasRaycastFilter
接口的组件判断计算过程中,还会判断组件是否是CanvasGroup
。若是CanvasGroup
且设置了 ignoreParentGroups 为false
,那么会调用IsRaycastLocationValid
计算判断;若是CanvasGroup
但是设置了 ignoreParentGroups 为true
,那么依旧会调用IsRaycastLocationValid
计算判断一次,但是对接下来后面所有的 CanvasGroup 组件将不会调用IsRaycastLocationValid
方法检测(忽略这些父 CanvasGrpup 的判断);如果不是CanvasGroup
,直接调用IsRaycastLocationValid
方法判断事件发生位置相对这个 Graphic 是否是合法的。
从整个 Graphic.Raycast 检测过程可以看出,检测是自当前 graphic 所在节点开始,一旦检测到某个节点添加实现了 ICanvasRaycastFilter
接口且 IsRaycastLocationValid
方法返回 false
则此 graphic 检测失败并结束检测;否则还会继续向上递归检测父节点,当所有节点(绑定了 Canvas 组件并设置了 Canvas.overrideSorting
为 true
的节点会截止此次检测)都射线检测成功,则此次 Graphic.Raycast 成功。
Graphic.Raycast 成功的对象深度排序
对所有射线检测成功的 graphics 按照深度 depth 从大到小排序。
Reversed Graphics 过滤
最后对检测结果再过滤。如果设置了 Ignore Reversed Graphics
为 true,则将背向 Camera 的对象过滤掉,这里面又分为两种情况:
Camera 为空,直接判断当前 Graphic 方向与正方向
Vector3.forward
是否相交,如下:12var dir = go.transform.rotation * Vector3.forward;appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;首先将
Vector3.forward
绕着当前 Graphic 的 rotation 旋转得到 Graphic 的正方向,然后通过点积判断 Graphic 正方向是否与默认正方向(没有 Camera 所以默认正方向为Vector3.forward
)相交。点积大于 0 则相交,说明当前 Graphic 可以加入射线加测结果中。当 Camera 不为空,就使用 Camera 的正方向与 Graphic 的正方向比较是否相交。
distance 检测是最终一道坎
Ignore Reversed Graphics
检测完,对结果进行 distance 计算:
|
|
Render Mode 为 RenderMode.ScreenSpaceOverlay
或者 Camera 为 null,distance 为 0;否则就计算 Graphic 和 Camera 之间的向量在 Graphic 正方向上的投影以及计算射线方向在 Graphic 正方向上的投影,两者相除就得到最终的 distance。
如果 distance 小于 hitDistance(设置的 Blocked Objects 和 Blocked Mask 产生),则结果通过最终的测试可被用作事件的接收者之一。
射线检测前后的一些操作
首先来看看这些 Raycaster 被唤起的部分,也就是最开始的流程图中的第三步。Input Module 中使用 Raycaster 处理射线检测,真正的 Raycaster 实施代码又回到了 EventSystem 类中的 RaycastAll
方法,具体代码如下:
|
|
场景中可以存在一个或多个 Raycaster。当存在多个时,如果需要发起射线检测,那么每个处于 Active 状态的 Raycaster 都会工作,所有 Raycaster 检测得到的结果都会存放在 raycastResults
中(这些 RaycastResult 都是在各自射线检测器中根据 distance 从小到大排过序的)。方法最后使用自定义 Comparer 对所有的 RaycastResult 排序。s_RaycastComparer
有以下几种比较流程:
- 两个 RaycastResult 检测所在的 Raycaster 不同
首先比较两个对象的 Camera 的 depth。在渲染中,Camera depth 越小会越先渲染,越大越往后渲染,因此对于射线检测来说,Camera 的 depth 越大,它对应的物体应该先于 Camera depth 小的物体进行射线检测,检测得到的结果也应排在前面。代码如下:
|
|
当 Camera depth 相等的时候,使用 sortOrderPriority
进行比较。优先级数值越大,越先被射线检测选中,所以这里的 CompareTo
方法使用的是右边的参数去比较左边的参数,最终的结果就是按照从大到小(降序)的顺序排列。
|
|
在 PhysicsRaycaster 和 Physics2DRaycaster 类中没有覆写 sortOrderPriority
方法,因此都返回基类的 int.MinValue
;但在 GraphicRaycaster 类中覆写了此方法,当对应的 Canvas 的 renderMode 设置为 RenderMode.ScreenSpaceOverlay
时,此时的 sortOrderPriority
返回 Canvas 的 sortingOrder(Sort Order越大越在上层),否则同样也是返回基类设置的 int.MinValue
,这是因为在 RenderMode.ScreenSpaceOverlay
模式下,所有的 distance 都将是 0。
当 sortOrderPriority 相同,再使用 renderOrderPriority 比较。
|
|
renderOrderPriority 和 sortOrderPriority 类似,仅在 GraphicRaycaster 类中被覆写,也只有在 Canvas 的 renderMode 设置为 RenderMode.ScreenSpaceOverlay
时才返回 canvas.rootCanvas.renderOrder
,这是因为 Canvas 在其他几种 renderMode 下,渲染的先后顺序都和距离摄像机的距离有关。所以 renderOrderPriority 比较也是按照从大到小的顺序得到最终的结果。
- 同属于一个 Raycaster 检测得到,但是它们的 sortingLayer 不一样
对于 PhysicsRaycaster 检测得到的对象,sortingLayer 都为 0。
对于 Physics2DRaycaster 检测得到的对象,如果对象上挂载有 SpriteRenderer 组件,那么 sortingLayer 对应的 sortingLayerID,否则也为 0。
对于 GraphicRaycaster 检测所得,sortingLayer 就是所在 Canvas 的 sortingLayerID。
|
|
通过 SortingLayer.GetLayerValueFromID
方法计算 sortingLayer 最终的 sorting layer 值,同样是按照降序排列,因此计算得到的 sorting layer 值越大越先排在前面。
- sortingLayer 也相同,使用 sortingOrder 比较
sortingOrder 和 sortingLayer 类似,PhysicsRaycaster 检测得到的对象 sortingOrder 为 0;Physics2DRaycaster 检测得到的对象是 SpriteRenderer 中的 sortingOrder;GraphicRaycaster 检测所得是所在 Canvas 的 sortingOrder。最终 sortingOrder 越大的对象越排前面。代码如下:
|
|
- sortingOrder 相同,使用 depth 比较
PhysicsRaycaster 和 Physics2DRaycaster 中 depth 都被设置为了 0;GraphicRaycaster 检测所得的对象的 depth 就是继承自 Graphic 类的对象所在的 Graphic 的 depth,即 Canvas 下所有 Graphic 深度遍历的顺序。比较同样也是按照降序进行的,因此越嵌套在靠近 Canvas 的对象越排在前面。
- depth 相同,使用 distance 比较
PhysicsRaycaster 中的 distance 就是 RaycastHit 的 distance(射线起点到射线碰撞点的距离)。
Physics2DRaycaster 类中返回的是 Camera 的位置和射线碰撞点之间的距离。
GraphicRaycaster 类中 distance 计算如下:
|
|
距离 distance 越小越靠前。
- 最后如果上述情况都不能满足,使用 index 比较。先被射线检测到的对象排在前面。
Raycaster 后段部分的流程: 取排过序的 RaycastResult 中第一个结果作为响应事件的输入事件的 pointerCurrentRaycast,根据它来在 Messaging System 中分发事件,大致代码如下:
|
|
Raycaster 在 Event System 中的作用和流程基本就是上述的内容。