Unity3D UGUI 源码学习 Raycaster

Raycaster的职责是向场景中的对象投射射线以判断光标是否在其上方,在Unity中,Raycaster系统用于当事件发生时筛选和确定事件的接收对象。对于指定的屏幕坐标点,使用射线投射可以取到所有位于该坐标点之下的对象并得出其中最靠近屏幕的一个。

Raycaster

Unity中有三种内置的Raycaster

  • GraphicRaycaster:用于处理UIelements,依存于一个Canvas并且会在Canvas内搜寻目标
  • Physics2DRaycaster:用于处理2D物理的对象
  • PhysicsRaycaster:用于处理3D物理的对象

各Raycasters继承关系如下:

1
2
3
4
public abstract class BaseRaycaster : UIBehaviour
public class GraphicRaycaster : BaseRaycaster
public class PhysicsRaycaster : BaseRaycaster
public class Physics2DRaycaster : PhysicsRaycaster

本系列主要是阅读分析UGUI代码,所以只讨论GraphicRaycaster相关的内容。

BaseRaycaster

所有的Raycaster的基类,提供了一个抽象方法Raycast(...)交由子类实现:

1
public abstract void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList);

除此之外还有sortOrderPriorityrenderOrderPriority两个get的虚方法,与射线投射的优先级相关,后边会被GraphicRaycaster复写:

1
2
3
4
5
6
7
8
9
public virtual int sortOrderPriority
{
get { return int.MinValue; }
}

public virtual int renderOrderPriority
{
get { return int.MinValue; }
}

OnEnable()OnDisable()中,Raycaster会把自己从RaycasterManager管理的一个列表中添加/移除。

1
2
3
4
5
6
7
8
9
10
11
protected override void OnEnable()
{
base.OnEnable();
RaycasterManager.AddRaycaster(this);
}

protected override void OnDisable()
{
RaycasterManager.RemoveRaycasters(this);
base.OnDisable();
}

GraphicRaycaster

GraphicRaycaster内围绕复写的基类的Raycast方法来展开(注意在GraphicRaycaster内部有另一个名为Raycast的私有静态方法,不要搞混)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
[NonSerialized] private List<Graphic> m_RaycastResults = new List<Graphic>();
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
if (canvas == null)
return;

var eventPosition = Display.RelativeMouseAt(eventData.position);

int displayIndex = canvas.targetDisplay;

// Discard events that are not part of this display so the user does not interact with multiple displays at once.
if (eventPosition.z != displayIndex)
return;

// The multiple display system is not supported on all platforms, when it is not supported the returned position
// will be all zeros so when the returned index is 0 we will default to the event data to be safe.
if (eventPosition.z == 0)
eventPosition = eventData.position;

// Convert to view space
Vector2 pos;
if (eventCamera == null)
{
// Multiple display support only when not the main display. For display 0 the reported
// resolution is always the desktops resolution since its part of the display API,
// so we use the standard none multiple display method. (case 741751)
float w = Screen.width;
float h = Screen.height;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
w = Display.displays[displayIndex].systemWidth;
h = Display.displays[displayIndex].systemHeight;
}
pos = new Vector2(eventPosition.x / w, eventPosition.y / h);
}
else
pos = eventCamera.ScreenToViewportPoint(eventPosition);

// If it's outside the camera's viewport, do nothing
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
return;

float hitDistance = float.MaxValue;

Ray ray = new Ray();

if (eventCamera != null)
ray = eventCamera.ScreenPointToRay(eventPosition);

if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
{
float dist = 100.0f;

if (eventCamera != null)
dist = eventCamera.farClipPlane - eventCamera.nearClipPlane;

if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)
{
if (ReflectionMethodsCache.Singleton.raycast3D != null)
{
RaycastHit hit;
if (ReflectionMethodsCache.Singleton.raycast3D(ray, out hit, dist, m_BlockingMask))
hitDistance = hit.distance;
}
}

if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All)
{
if (ReflectionMethodsCache.Singleton.raycast2D != null)
{
var hit = ReflectionMethodsCache.Singleton.raycast2D((Vector2)ray.origin, (Vector2)ray.direction, dist, (int)m_BlockingMask);
if (hit.collider)
hitDistance = hit.fraction * dist;
}
}
}

m_RaycastResults.Clear();
Raycast(canvas, eventCamera, eventPosition, m_RaycastResults);

for (var index = 0; index < m_RaycastResults.Count; index++)
{
var go = m_RaycastResults[index].gameObject;
bool appendGraphic = true;

if (ignoreReversedGraphics)
{
if (eventCamera == null)
{
// If we dont have a camera we know that we should always be facing forward
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
}
else
{
// If we have a camera compare the direction against the cameras forward.
var cameraFoward = eventCamera.transform.rotation * Vector3.forward;
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
}
}

if (appendGraphic)
{
float distance = 0;

if (eventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
distance = 0;
else
{
Transform trans = go.transform;
Vector3 transForward = trans.forward;
// http://geomalgorithms.com/a06-_intersect-2.html
distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));

// Check to see if the go is behind the camera.
if (distance < 0)
continue;
}

if (distance >= hitDistance)
continue;

var castResult = new RaycastResult
{
gameObject = go,
module = this,
distance = distance,
screenPosition = eventPosition,
index = resultAppendList.Count,
depth = m_RaycastResults[index].depth,
sortingLayer = canvas.sortingLayerID,
sortingOrder = canvas.sortingOrder
};
resultAppendList.Add(castResult);
}
}
}

整个函数代码很多,但是做的事情很简单,对于给定的PointerEventData,射线投射得出一些投射结果RaycastResult,追加到传入的参数List<RaycastResult>之后。函数里边主要做了下边这些工作:

  • 获取eventPositionDisplay.RelativeMouseAt(...)方法用于获取鼠标的相对位置,在PC平台下可能会有多个Display窗口,返回的z表示的是Display的id。如果不支持多个Display则返回值的z为0,此时获取到的就是eventData.position

  • 获取正确的pos:将上一步的屏幕坐标eventPosition转换成ViewPort坐标pos,这里同样也处理了多Display的情况。获取eventCamaera不为null则直接调用相机的坐标转换方法ScreenToViewportPoint,获取eventCamera的逻辑如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public override Camera eventCamera
    {
    get
    {
    if (canvas.renderMode == RenderMode.ScreenSpaceOverlay
    || (canvas.renderMode == RenderMode.ScreenSpaceCamera && canvas.worldCamera == null))
    return null;

    return canvas.worldCamera != null ? canvas.worldCamera : Camera.main;
    }
    }
  • 获取正确的hitDistance:这里分别处理blockingObjects 为不同的枚举值时的情况(默认值为None)。hitDistance表示相机到投射目标点的距离。后边会根据这个距离值筛选掉一部分Graphicm_BlockingMaskLayerMask用于按层筛选投射目标;这里的ReflectionMethodsCache.Singleton.raycast3D这样的方法实际上调用的就是Physics.Raycast(...)方法,后边会有专门的文章讨论这个ReflectionMethodsCache

  • 调用私有的Raycast(...):得到候选的经过排序的Graphic的列表m_RaycastResults,后边细说;

  • 遍历m_RaycastResults:判断对应的GameObject是否有效appendGraphic,即是否继续处理追加该Graphic到射线投射结果的列表。如果ignoreReversedGraphics ,则值追加朝向为正的Graphic,当判断需要追加appendGraphic为正时,会计算目标到摄像机的距离distance并再进行一次筛选,最终通过筛选的对象,会为其new一个RaycastResult并填入数据,最后把它追加到resultAppendList

我们来看静态私有方法Raycast(...)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, List<Graphic> results)
{
// Debug.Log("ttt" + pointerPoision + ":::" + camera);
// Necessary for the event system
var foundGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
for (int i = 0; i < foundGraphics.Count; ++i)
{
Graphic graphic = foundGraphics[i];

// -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
if (graphic.depth == -1 || !graphic.raycastTarget)
continue;

if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
continue;

if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}

s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
// StringBuilder cast = new StringBuilder();
for (int i = 0; i < s_SortedGraphics.Count; ++i)
results.Add(s_SortedGraphics[i]);
// Debug.Log (cast.ToString());

s_SortedGraphics.Clear();
}

主要完成的工作如下:

  • 获取在canvas注册的Graphic的列表foundGraphics
  • 遍历foundGraphics,筛选有效的Graphic(是否已绘制且是raycastTarget且事件坐标点在rectTransform范围内);调用graphic.Raycast(...)方法,符合要求的Graphic放入s_SortedGraphics
  • s_SortedGraphics中的各个Graphic按照depth排序,depth其实是GraphiccanvasRenderer.absoluteDepth
  • 将排序后的Graphic放入results并清空s_SortedGraphics

展开来看Graphic中的Raycast(...)方法,对于给定的屏幕坐标和事件Camera,判断是否是有效的投射结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public virtual bool Raycast(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return false;

var t = transform;
var components = ListPool<Component>.Get();

bool ignoreParentGroups = false;
bool continueTraversal = true;

while (t != null)
{
t.GetComponents(components);
for (var i = 0; i < components.Count; i++)
{
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
continueTraversal = false;

var filter = components[i] as ICanvasRaycastFilter;

if (filter == null)
continue;

var raycastValid = true;

var group = components[i] as CanvasGroup;
if (group != null)
{
if (ignoreParentGroups == false && group.ignoreParentGroups)
{
ignoreParentGroups = true;
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
else if (!ignoreParentGroups)
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
else
{
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}

if (!raycastValid)
{
ListPool<Component>.Release(components);
return false;
}
}
t = continueTraversal ? t.parent : null;
}
ListPool<Component>.Release(components);
return true;
}

首先是做了一些是否有效的判断,然后会在层级结构中自下而上地遍历,直到没有更多的父级节点或者由overrideSortingCanvas为止。遍历过程中,如果遇到有ICanvasRaycastFilterImage实现有此接口)和CanvasGroup则要根据二者的实现及参数做出一些判断,如果都通过了检测,则认为这次投射有效返回true

RaycasterManager

直接是一个静态类,用于管理各Raycaster:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
internal static class RaycasterManager
{
private static readonly List<BaseRaycaster> s_Raycasters = new List<BaseRaycaster>();

public static void AddRaycaster(BaseRaycaster baseRaycaster)
{
if (s_Raycasters.Contains(baseRaycaster))
return;

s_Raycasters.Add(baseRaycaster);
}

public static List<BaseRaycaster> GetRaycasters()
{
return s_Raycasters;
}

public static void RemoveRaycasters(BaseRaycaster baseRaycaster)
{
if (!s_Raycasters.Contains(baseRaycaster))
return;
s_Raycasters.Remove(baseRaycaster);
}
}

内容很少也很简单,添加和移除Raycaster,获取当前激活的Raycaster列表。

RaycastResult

RaycastResult是一个结构体,记录了投射结果相关的信息。

1
public struct RaycastResult

下边是它主要的成员和方法,其实之前在GraphicRaycaster复写的基类的Raycast(...)中最后的部分就有过RaycastResult构造和填入数据的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private GameObject m_GameObject; // Game object hit by the raycast

public GameObject gameObject
{
get { return m_GameObject; }
set { m_GameObject = value; }
}

public BaseRaycaster module; // Event system that hit this object
public float distance; // The distance from the origin this hit was.
public float index; // The index this element is in the raycastList (used for sorting)
public int depth;
public int sortingLayer;
public int sortingOrder;
// World-space position where a ray cast into the screen hits something
public Vector3 worldPosition;
// World-space normal where a ray cast into the screen hits something
public Vector3 worldNormal;

public Vector2 screenPosition;

public bool isValid
{
get { return module != null && gameObject != null; }
}

注释很清楚,而且前边构造(赋值)的过程也可以看到各个成员值的来源。除此之外,RaycastResult可以调用Clear()清除数据。

调用者EventSystem

EventSystem中,获取事件响应的目标对象时,调用RaycastAll()方法,传入参数是一个PointerEventData和一个用于接收射线投射结果的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
raycastResults.Clear();
var modules = RaycasterManager.GetRaycasters();
for (int i = 0; i < modules.Count; ++i)
{
var module = modules[i];
if (module == null || !module.IsActive())
continue;

module.Raycast(eventData, raycastResults);
}

raycastResults.Sort(s_RaycastComparer);
}

各个在RaycasterManager中记录的激活状态的Raycaster调用Raycast方法,在传入的raycastResults后追加射线投射结果。遍历全部的Raycaster之后,对所有的投射结果排序。排序方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private static readonly Comparison<RaycastResult> s_RaycastComparer = RaycastComparer;

private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs)
{
if (lhs.module != rhs.module)
{
if (lhs.module.eventCamera != null && rhs.module.eventCamera != null && lhs.module.eventCamera.depth != rhs.module.eventCamera.depth)
{
// need to reverse the standard compareTo
if (lhs.module.eventCamera.depth < rhs.module.eventCamera.depth)
return 1;
if (lhs.module.eventCamera.depth == rhs.module.eventCamera.depth)
return 0;

return -1;
}

if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);

if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
}

if (lhs.sortingLayer != rhs.sortingLayer)
{
// Uses the layer value to properly compare the relative order of the layers.
var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
return rid.CompareTo(lid);
}


if (lhs.sortingOrder != rhs.sortingOrder)
return rhs.sortingOrder.CompareTo(lhs.sortingOrder);

if (lhs.depth != rhs.depth)
return rhs.depth.CompareTo(lhs.depth);

if (lhs.distance != rhs.distance)
return lhs.distance.CompareTo(rhs.distance);

return lhs.index.CompareTo(rhs.index);
}

优先级依次是:

如果是不同的Raycaster:

  1. module.eventCamera.depth 高者优先
  2. module.sortOrderPriority 高者优先
  3. module.renderOrderPriority 高者优先

如果是相同的Raycaster:

  1. sortingLayer 高者优先
  2. sortingOrder 高者优先
  3. depth 高者优先
  4. distance 小者优先
  5. index 小者优先

对于GraphicRaycaster有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public override int sortOrderPriority
{
get
{
// We need to return the sorting order here as distance will all be 0 for overlay.
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
return canvas.sortingOrder;

return base.sortOrderPriority;
}
}

public override int renderOrderPriority
{
get
{
// We need to return the sorting order here as distance will all be 0 for overlay.
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
return canvas.renderOrder;

return base.renderOrderPriority;
}
}

本系列其它文章详见Unity3D UGUI 源码学习

REFERENCE

https://docs.unity3d.com/ScriptReference