UnityにおけるUGUIソースコード解析のイベントシステム(6)-RayCaster(後編)
前回の記事に引き続きプロジェクターの紹介です。
グラフィックレイキャスター
GraphicRaycaster はBaseRaycasterから継承し、BaseRaycasterの特定の実装クラスであり、オブジェクト上に同時にCanvasコンポーネントが存在する必要がある UGUI 要素のプロジェクターです。
なお、GraphicRaycaster、PhysicsRaycaster、Physics2DRaycaster は別のディレクトリに格納されており、後者 2 つは EventSystem ディレクトリに配置され、GraphicRaycaster は UI ディレクトリに配置されているため、GraphicRaycaster は UI 専用であることを表現したいのかもしれません。
GraphicRaycaster は光線検出を主に RectTranform に関係する長方形フレームに依存しており、基本的にカメラには依存しません。
通常、Canvas を追加すると、このコンポーネントがデフォルトで追加されます。図に示すように:
パネルのプロパティ
- Ignore Reversed Graphics: グラフィックの背面を無視し (デフォルトでチェックされています)、ポイント乗算によって背面から光線が侵入するかどうかを判断します。チェックされている場合、背面はレイ キャスティングを追加しません
- ブロッキング オブジェクト: ブロッキング レイ オブジェクト タイプ。つまり、レイは指定されたタイプのオブジェクトに遭遇するとブロックされ、渡すことはできません。
- なし: デフォルトではブロックしません
GameObject->2D Object
2 つの D: 2D オブジェクトのブロック。ここでの 2D オブジェクトはメニュー内のオブジェクトを指し、オブジェクトには2D コリジョン ボックス(2D コライダー)が必要です。GameObject->3D Object
3 D: 3D オブジェクトのブロック。ここでの 3D オブジェクトはメニュー内のオブジェクトを指し、オブジェクトには3D コリジョン ボックス(3D コライダー)が必要です。- すべて: 2 D + 3 D
- ブロック マスク: ブロック マスク、つまり、特定のレイヤー (レイヤー) のオブジェクトがブロックに参加します。デフォルトはすべてのレイヤーです。
- 上記のブロッキングは、ブロッキング レイヤー (つまり、2D または 3D コライダー) に対応するイベント ハンドラーが存在する必要があることを意味します。
以下は関連するコードです。
[AddComponentMenu("Event/Graphic Raycaster")]
[RequireComponent(typeof(Canvas))]
public class GraphicRaycaster : BaseRaycaster
{
protected const int kNoEventMaskSet = -1;
public enum BlockingObjects
{
None = 0,
TwoD = 1,
ThreeD = 2,
All = 3,
}
[FormerlySerializedAs("ignoreReversedGraphics")]
[SerializeField] private bool m_IgnoreReversedGraphics = true;
[FormerlySerializedAs("blockingObjects")]
[SerializeField] private BlockingObjects m_BlockingObjects = BlockingObjects.None;
public bool ignoreReversedGraphics { get {return m_IgnoreReversedGraphics; } set { m_IgnoreReversedGraphics = value; } }
public BlockingObjects blockingObjects { get {return m_BlockingObjects; } set { m_BlockingObjects = value; } }
[SerializeField]
protected LayerMask m_BlockingMask = kNoEventMaskSet;
private Canvas m_Canvas;
}
プロパティ、フィールド、およびメソッド
//---------------------------------------------------------
// 重写了BaseRaycaster的排序属性
public override int sortOrderPriority
{
get
{
// 如果Canvas的渲染模式为:ScreenSpaceOverlay, 也就是说总是保持在屏幕最上层, 则使用画布的渲染层级在多个画布中排序
// 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.rootCanvas.renderOrder;
return base.renderOrderPriority;
}
}
//-----------------------------------------------------------------
// GraphicRaycaster主要依赖Canvas来进行各种操作
private Canvas m_Canvas;
private Canvas canvas
{
get
{
if (m_Canvas != null)
return m_Canvas;
m_Canvas = GetComponent<Canvas>();
return m_Canvas;
}
}
// 用于发射射线的摄像机
// 如果Canvas的渲染模式为:ScreenSpaceOverlay或者没有指定摄像机则使用屏幕空间
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;
}
}
レイキャスティング
次に、重要で最も複雑な場所が続きます。
[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
// 向给定graphic投射射线, 收集所有被射线穿过的graphic
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
// ----------------------------
// -- graphic相关过滤条件
// depth==-1代表不被这个Canvas处理, 也就是绘制
//
if (graphic.depth == -1 || !graphic.raycastTarget || graphic.canvasRenderer.cull)
continue;
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
continue;
// ----------------------------
// z值超过摄像机范围则忽略, 所以可以通过指定z值来脱离射线投射
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;
// 射线是否穿过graphic
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}
// 深度从大到小排序
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
// StringBuilder cast = new StringBuilder();
totalCount = s_SortedGraphics.Count;
for (int i = 0; i < totalCount; ++i)
results.Add(s_SortedGraphics[i]);
// Debug.Log (cast.ToString());
s_SortedGraphics.Clear();
}
// [public]向给定graphic投射射线, 收集所有被射线穿过的graphic
[NonSerialized] private List<Graphic> m_RaycastResults = new List<Graphic>();
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
if (canvas == null)
return;
// 收集canvas管理的所有graphic
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
if (canvasGraphics == null || canvasGraphics.Count == 0)
return;
int displayIndex;
var currentEventCamera = eventCamera; // Propery can call Camera.main, so cache the reference
// 根据canvas的渲染模式, 选择targetDisplay
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
displayIndex = canvas.targetDisplay;
else
displayIndex = currentEventCamera.targetDisplay;
// 获取屏幕坐标, 支持多屏输出
var eventPosition = Display.RelativeMouseAt(eventData.position);
if (eventPosition != Vector3.zero)
{
// 根据屏幕坐标获取targetDisplay
// We support multiple display and display identification based on event position.
int eventDisplayIndex = (int)eventPosition.z;
// 抛弃非当前targetDisplay
// Discard events that are not part of this display so the user does not interact with multiple displays at once.
if (eventDisplayIndex != displayIndex)
return;
}
else
{
// 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.
eventPosition = eventData.position;
// We dont really know in which display the event occured. We will process the event assuming it occured in our display.
}
// 转换视口坐标
// Convert to view space
Vector2 pos;
if (currentEventCamera == 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 = currentEventCamera.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 (currentEventCamera != null)
ray = currentEventCamera.ScreenPointToRay(eventPosition);
// 2D和3D物体阻挡部分, 收集到投射距离, 代表被阻挡
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
{
float distanceToClipPlane = 100.0f;
if (currentEventCamera != null)
{
float projectionDirection = ray.direction.z;
distanceToClipPlane = Mathf.Approximately(0.0f, projectionDirection)
? Mathf.Infinity
: Mathf.Abs((currentEventCamera.farClipPlane - currentEventCamera.nearClipPlane) / projectionDirection);
}
// 使用反射获取PhysicsRaycaster的投射接口
if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)
{
if (ReflectionMethodsCache.Singleton.raycast3D != null)
{
var hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, (int)m_BlockingMask);
if (hits.Length > 0)
hitDistance = hits[0].distance;
}
}
// 使用反射获取Physics2DRaycaster的投射接口
if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All)
{
if (ReflectionMethodsCache.Singleton.raycast2D != null)
{
var hits = ReflectionMethodsCache.Singleton.getRayIntersectionAll(ray, distanceToClipPlane, (int)m_BlockingMask);
if (hits.Length > 0)
hitDistance = hits[0].distance;
}
}
}
// 收集所有被射线穿过的对象
m_RaycastResults.Clear();
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
int totalCount = m_RaycastResults.Count;
for (var index = 0; index < totalCount; index++)
{
var go = m_RaycastResults[index].gameObject;
bool appendGraphic = true;
// 通过点乘判断背面是否参与投射
if (ignoreReversedGraphics)
{
if (currentEventCamera == 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 = currentEventCamera.transform.rotation * Vector3.forward;
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
}
}
//
if (appendGraphic)
{
float distance = 0;
if (currentEventCamera == 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 - currentEventCamera.transform.position) / 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);
}
}
}
グラフィック関連の内容については、後の記事で説明します。
物理学レイキャスター
3D オブジェクトにイベントを追加する必要がある場合、PhysicsRaycaster コンポーネントがシーンに存在する必要があります。
PhysicsRaycaster は光線検出をカメラに依存する必要がありますが、光線検出部分を除けば基本的に GraphicRaycaster と同じです。
パネルのプロパティ
- イベント マスク: カメラのマスクと同様に、一般的に使用されるマスクは、検出する必要があるレイヤーのオブジェクトを決定するために使用され、カメラとの「ビット AND」演算を実行します。つまり、イベントに参加しているオブジェクトです。検出はカメラで「認識」される必要があります。0 は「何もない」を表し、-1 は「すべて」を表します。
- 最大レイ交差数: レイ ヒットの最大数。つまり、この数値によってレイがヒットすると判断できるオブジェクトの数が決まります。デフォルトは 0 (無制限を意味します) で、このバージョンでは追加のメモリを適用する必要があります (アンマネージド ヒープ、C++ 層、C# 層は事前に数を知ることができないため)、およびその他の値は追加のメモリを適用する必要はなく、マネージド ヒープ内のメモリに適用するだけで済みます。この値は配列に適用するために使用され、負の値はエラーを報告するため、割り当てになります。
以下は属性の関連コードであり、使用部分は次の検出で示されます。
[AddComponentMenu("Event/Physics Raycaster")]
[RequireComponent(typeof(Camera))]
public class PhysicsRaycaster : BaseRaycaster
{
/// EventMask的默认值
protected const int kNoEventMaskSet = -1;
[SerializeField] protected LayerMask m_EventMask = kNoEventMaskSet;
/// 最大射线击中数量, 为0时代表不受限的数量, 会在非托管堆申请内存(c++), 其它正数会在托管堆申请(c#)
[SerializeField] protected int m_MaxRayIntersections = 0;
protected int m_LastMaxRayIntersections = 0;
/// 击中结果
RaycastHit[] m_Hits;
/// EventMask与摄像机按位与之后的结果
public int finalEventMask
{
get { return (eventCamera != null) ? eventCamera.cullingMask & m_EventMask : kNoEventMaskSet; }
}
/// EventMask属性
public LayerMask eventMask
{
get { return m_EventMask; }
set { m_EventMask = value; }
}
/// maxRayIntersections属性
public int maxRayIntersections
{
get { return m_MaxRayIntersections; }
set { m_MaxRayIntersections = value; }
}
/// 摄像机, 主要用于用于Mask确定检测的物体, 还有发射射线和计算起始点距离剪切平面的距离(clipPlane)
protected Camera m_EventCamera;
public override Camera eventCamera
{
get
{
if (m_EventCamera == null)
m_EventCamera = GetComponent<Camera>();
return m_EventCamera ?? Camera.main;
}
}
}
X線検査
一般的な考え方は、カメラから光線を放射し、物理モジュール (Physic) で開始点からクリッピング面までの距離を計算して光線検出を実行することです。
レイ ヒットの最大数に応じて、物理モジュールのさまざまなインターフェイスが呼び出され、ヒット結果が返されます。
関連するコード、C# 部分は次のとおりです。
// 发射射线并计算距离, 注意这个摄像机非常重要, 在不同的摄像机视角下判断击中的结果可能是不一样的
protected void ComputeRayAndDistance(PointerEventData eventData, out Ray ray, out float distanceToClipPlane)
{
ray = eventCamera.ScreenPointToRay(eventData.position);
// compensate far plane distance - see MouseEvents.cs
float projectionDirection = ray.direction.z;
// 如果发射点距离摄像机非常近, 则认为距离平面无限远
distanceToClipPlane = Mathf.Approximately(0.0f, projectionDirection)
? Mathf.Infinity
: Mathf.Abs((eventCamera.farClipPlane - eventCamera.nearClipPlane) / projectionDirection);
}
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
// 抛弃摄像机viewRect之外的部分
// Cull ray casts that are outside of the view rect. (case 636595)
if (eventCamera == null || !eventCamera.pixelRect.Contains(eventData.position))
return;
Ray ray;
float distanceToClipPlane;
ComputeRayAndDistance(eventData, out ray, out distanceToClipPlane);
int hitCount = 0;
// ====================================================
// -- 根据最大射线击中数量调用物理模块不同接口返回击中结果
if (m_MaxRayIntersections == 0)
{ // 等于0, 代表接受不受限的击中物体
// 通过物理模块的检测所有物体的接口
// 底层是PhysicManager.RaycastAll
if (ReflectionMethodsCache.Singleton.raycast3DAll == null)
return;
// 返回击中结果
m_Hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask);
hitCount = m_Hits.Length;
}
else
{ // 非0, 代表接受有限的击中物体
// 通过物理模块的检测所有物体的接口
// 底层是PhysicManager.Raycast
if (ReflectionMethodsCache.Singleton.getRaycastNonAlloc == null)
return;
// 有限击中物体, 预先申请好最大击中结果, 如果两次数量一致则不需重新申请
if (m_LastMaxRayIntersections != m_MaxRayIntersections)
{
m_Hits = new RaycastHit[m_MaxRayIntersections];
m_LastMaxRayIntersections = m_MaxRayIntersections;
}
// 返回实际击中数量
hitCount = ReflectionMethodsCache.Singleton.getRaycastNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
}
// ====================================================
// 根据距离从小到大排序(由近到远)
if (hitCount > 1)
System.Array.Sort(m_Hits, (r1, r2) => r1.distance.CompareTo(r2.distance));
// 返回检测结果
if (hitCount != 0)
{
for (int b = 0, bmax = hitCount; b < bmax; ++b)
{
var result = new RaycastResult
{
gameObject = m_Hits[b].collider.gameObject,
module = this,
distance = m_Hits[b].distance,
worldPosition = m_Hits[b].point,
worldNormal = m_Hits[b].normal,
screenPosition = eventData.position,
index = resultAppendList.Count,
sortingLayer = 0,
sortingOrder = 0
};
resultAppendList.Add(result);
}
}
}
C++ 部分 (著作権上の理由により、コードの一部のみが掲載されています):
// 数量不受限版本
const PhysicsManager::RaycastHits& PhysicsManager::RaycastAll (const Ray& ray, float distance, int mask)
{
// ....
// 会生成静态数组, 处于非托管堆, 无法释放
static vector<RaycastHit> hits;
// ....
RaycastCollector collector;
collector.hits = &hits;
GetDynamicsScene ().raycastAllShapes ((NxRay&)ray, collector, NX_ALL_SHAPES, mask, distance);
return hits;
}
// 数量受限版本(没有找到实现, 我自己猜的)
int PhysicsManager::Raycast (const Ray& ray, RaycastHit[] outHits, float distance, int mask)
{
// ....
vector<RaycastHit> hits;
RaycastCollector collector;
collector.hits = &hits;
GetDynamicsScene().raycastAllShapes((NxRay&)ray, collector, NX_ALL_SHAPES, mask, distance);
int resultCount = hits.size();
const int allowedResultCount = std::min(resultCount, outHitsSize);
for (int index = 0; index < allowedResultCount; ++index)
*(outHits++) = hits[index];
return allowedResultCount;
}
物理2Dレイキャスター
Physics2DRaycaster は PhysicsRaycaster を継承しており、内容は基本的に同じですが、物理モジュールを選択する際には 2D 物理モジュールが使用され、キーコードのみがここに貼り付けられます。
[AddComponentMenu("Event/Physics 2D Raycaster")]
[RequireComponent(typeof(Camera))]
public class Physics2DRaycaster : PhysicsRaycaster
{
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
// ...
if (maxRayIntersections == 0)
{
if (ReflectionMethodsCache.Singleton.getRayIntersectionAll == null)
return;
// 用的接口不一样
m_Hits = ReflectionMethodsCache.Singleton.getRayIntersectionAll(ray, distanceToClipPlane, finalEventMask);
hitCount = m_Hits.Length;
}
else
{
if (ReflectionMethodsCache.Singleton.getRayIntersectionAllNonAlloc == null)
return;
if (m_LastMaxRayIntersections != m_MaxRayIntersections)
{
m_Hits = new RaycastHit2D[maxRayIntersections];
m_LastMaxRayIntersections = m_MaxRayIntersections;
}
// 用的接口不一样
hitCount = ReflectionMethodsCache.Singleton.getRayIntersectionAllNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
}
// ...
}
}
要約する
今日は Unity 用のいくつかのレイキャスター、GraphicRaycaster、PhysicsRaycaster、Physics2DRaycaster を紹介しました。
2 つの物理関連のプロジェクターは、投影されるオブジェクト上に対応するコライダー (Collider と Collider2D) を持つ必要があり、親子関係を必要としないことに注意してください。これらは、カメラで認識されている限り投影できます。 GraphicRaycaster は主にカメラではなく Canvas を投影に使用し、投影されるオブジェクトは Canvas またはその子ノードである必要があります。
解析の際、Unityの3D物理エンジンは「NVIDIA PhysX」、2D物理エンジンは「Box2D」を使用していることも判明し、ずっとUnity自身が書いたものだと思っていました。
最新の記事は比較的内容が深いのか、あまり読みたがらない学生が多く、最初にUnityをプレイしていた学生は最下層を追求する意識が弱いのではないかと思いました。この感覚は正しいようです。
私の個人的な観点から言えば、Unity でゲームを楽しく開発しているときは、時間をかけて最下位レイヤーを勉強することをお勧めします。原理を理解すると、多くの場合、寄り道をせずに済み、同時に次の作業に進むことができるからです。さらに最後には。
次に、イベント システムの最後の部分、つまりコアとなる入力モジュールについて、いくつかの記事を使って詳しく紹介します。
つまり、誰もが自分のニーズに応じてそれを入手できます。今日はここまでです。少しでもお役に立てれば幸いです。