原因

在使用 Unity 编辑器开发一些功能辅助的时候,想要在地面等其他网格上进行踩点,但是这些网格并没有碰撞体组件,所以只能寻找其他方式来达到鼠标所在即是网格上的点。

刚好看到一个开源项目达到了这个要求,项目地址 https://github.com/slipster216/VertexPaint,里面的视频效果如下:
Unity SceneView 鼠标所在网格位置_unity

实现

首先,先实现鼠标所在位置有个球进行跟随,这样才好进行下一步的踩点操作。创建一个编辑器类,代码如下:

using UnityEditor;
using UnityEngine;

public class SceneMouseWindow : EditorWindow
{
    [MenuItem("Tool/Window/Scene Mouse")]
    public static void ShowWindow()
    {
        var window = GetWindow<SceneMouseWindow>();
        window.Show();
    }

    void OnFocus()
    {
        SceneView.onSceneGUIDelegate -= OnSceneGUI;
        SceneView.onSceneGUIDelegate += OnSceneGUI;
        Repaint();
    }

    void OnDestroy()
    {
        SceneView.onSceneGUIDelegate -= OnSceneGUI;
    }

    private void OnSceneGUI(SceneView sceneView)
    {
        // 当前屏幕坐标,左上角是(0,0)右下角(camera.pixelWidth,camera.pixelHeight)
        Vector2 mousePosition = Event.current.mousePosition;

        // Retina 屏幕需要拉伸值
        float mult = 1;
#if UNITY_5_4_OR_NEWER
        mult = EditorGUIUtility.pixelsPerPoint;
#endif

        // 转换成摄像机可接受的屏幕坐标,左下角是(0,0,0)右上角是(camera.pixelWidth,camera.pixelHeight,0)
        mousePosition.y = sceneView.camera.pixelHeight - mousePosition.y * mult;
        mousePosition.x *= mult;

        // 近平面往里一些,才能看得到摄像机里的位置
        Vector3 fakePoint = mousePosition;
        fakePoint.z = 20;
        Vector3 point = sceneView.camera.ScreenToWorldPoint(fakePoint);

        Handles.SphereCap(0, point, Quaternion.identity, 2);

        // 刷新界面,才能让球一直跟随
        sceneView.Repaint();
        HandleUtility.Repaint();
    }
}

运行效果如下所示:
Unity SceneView 鼠标所在网格位置_编辑器_02

网格

通过鼠标所在位置与网格进行射线检测,需要编辑器的隐藏接口HandleUtility.IntersectRayMesh,这个接口没有公开,所以只能反射获取,代码参照 https://gist.github.com/MattRix/9205bc62d558fef98045,另外,使用Handles.SphereCap进行绘制球体的话,没法跟网格进行交叉,无法很好的确定交点的位置,所以这里改成创建一个球体物体,代码如下:

    private void OnSceneGUI(SceneView sceneView)
    {
        // 省略...

        Ray ray = sceneView.camera.ScreenPointToRay(mousePosition);
        MeshFilter[] componentsInChildren = GameObject.FindObjectsOfType<MeshFilter>();
        float num = float.PositiveInfinity;
        foreach (MeshFilter meshFilter in componentsInChildren)
        {
            Mesh sharedMesh = meshFilter.sharedMesh;
            RaycastHit hit;
            if (sharedMesh
                && RXLookingGlass.IntersectRayMesh(ray, sharedMesh, meshFilter.transform.localToWorldMatrix, out hit)
                && hit.distance < num)
            {
                point = hit.point;
                num = hit.distance;
            }
        }

        //Handles.SphereCap(0, point, Quaternion.identity, 2);
        SphereCapPos(point);

        // 省略...
    }

    private static Transform capSphere;

    private void SphereCapPos(Vector3 point)
    {
        if (capSphere == null)
        {
            GameObject go = GameObject.Find("[SphereCapPos]");
            if (go == null)
            {
                go = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                go.name = "[SphereCapPos]";

                Collider collider = go.GetComponent<Collider>();
                DestroyImmediate(collider);

                Material mat = new Material(Shader.Find("Unlit/Color"));
                mat.SetColor("_Color", Color.cyan);
                mat.hideFlags = HideFlags.HideAndDontSave;

                Renderer renderer = go.GetComponent<Renderer>();
                renderer.sharedMaterial = mat;
            }

            go.hideFlags = HideFlags.HideAndDontSave;
            capSphere = go.transform;
            capSphere.rotation = Quaternion.identity;
            capSphere.localScale = Vector3.one * 0.5f;
        }
        capSphere.position = point;
    }

运行效果如下所示:
Unity SceneView 鼠标所在网格位置_鼠标_03

代码

代码地址:
https://github.com/akof1314/UnityEditorLearn/blob/master/Demo/Assets/Scripts/Editor/SceneMouseWindow.cs