• Unity编辑器扩展——实现样条线编辑器


    Unity编辑器扩展——实现样条线编辑器

    视频效果

    Unity编辑器扩展样条线编辑

    引言

    一直以来有一个想法,想实现在unity中程序化摆放物体,比如,沿着公路自动摆放路灯,在一个范围内自动摆放建筑物,生成自动化城市场景等。于是,就开始了编辑器扩展的研究,遗憾的是,网络上虽然有很多的编辑器扩展方面的文章,但极少有在场景中实现样条线编辑方面的内容,只好自己研究啦,幸好,我记得Cinemachine中自带一个路径摄像机的功能,于是深入的研究了一下,发现完全可以拿来学习。

    实现视频中的效果,所需要的几个要点:

    • 贝塞尔曲线的算法
    • 曲线功能描述
      • 可以实现闭环曲线
      • 可以给定0-1之间的值,获得曲线上位置的点坐标,以及任意位置的切线方向。
    • 可视化的编辑(本文重点介绍)
      • ReorderableList类的用法,重载OnInspectorGUI,绘制Inspector面板。
      • 重载OnSceneGUI,实现坐标点的绘制,以及坐标点的在Scene窗口中的位置编辑。
      • 通过DrawGizmos在Scene窗口中绘制编辑的结果,完成曲线可视化。

    原理

    贝塞尔曲线

    贝塞尔曲线的原理网络上很多文章,这里不再赘述,但是这里值得一提的是,本来我以为贝塞尔计算起来也就是这样:

    public static Vector3 BezierLerp(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
    {
    	t = Mathf.Clamp01(t);
    	Vector3 w0 = Vector3.Lerp( p0, p1, t );
    	Vector3 w1 = Vector3.Lerp( p1, p2, t );
    	Vector3 w2 = Vector3.Lerp( p2, p3, t );
    	Vector3 r0 = Vector3.Lerp( w0, w1, t );
    	Vector3 r1 = Vector3.Lerp( w1, w2, t );
    	return Vector3.Lerp( r0, r1, t );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上面的代码虽然浅显易懂,但是计算挺复杂,研究了Cinemachine中的代码,发现人家的代码效率很高,研究了好一会儿,没怎么理解背后的几何原理,哎,学好数学是多么重要啊。算球,公式记下来,以后就这么用吧:

    public static Vector3 Bezier3(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
    {
        t = Mathf.Clamp01(t);
        float d = 1f - t;
        return d * d * d * p0 + 3f * d * d * t * p1
            + 3f * d * t * t * p2 + t * t * t * p3;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    另附一个贝塞尔切线算法:

    public static Vector3 BezierTangent3(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
    {
        t = Mathf.Clamp01(t);
        return (-3f * p0 + 9f * p1 - 9f * p2 + 3f * p3) * (t * t)
            +  (6f * p0 - 12f * p1 + 6f * p2) * t
            -  3f * p0 + 3f * p1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    Spline类

    公共成员:

    // 是否循环
    public bool Looped;
    // 两点之间曲线细节(分辨率)
    public int Resolution = 20;
    // 曲线所经过的位置点
    public Vector3 []  positions = Array.Empty<Vector3>();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个类最重要的是两个方法:

    // 给定0-1的值,获得曲线上任意点位置
    public Vector3 EvaluatePosition(float pos);
    // 给定0-1的值,获得曲线上任意点切线
    public Vector3 EvaluateTangent(float pos);
    
    • 1
    • 2
    • 3
    • 4

    这个类其实并不复杂,也比较容易理解,这里不展开介绍了。

    SplineEditor类(重点)

    编辑器扩展,用它来实现Spline的编辑。这里面有几个要点:

    ReorderableList类的用法,重载OnInspectorGUI,绘制Inspector面板。

    ReorderableList用来在Inspector面板展示可以调整顺序的列表,如下图:
    ReorderableList
    使用方法也很简单,只要在OnEnable里初始化一下:

    private void OnEnable()
    {
        positions = new ReorderableList(serializedObject, serializedObject.FindProperty("positions"))
        {
            elementHeight = EditorGUIUtility.singleLineHeight + 8,
            drawHeaderCallback = rect => { EditorGUI.LabelField(rect, $"坐标点列表 [{positions.count}]"); },
            drawElementCallback = (rect, index, _, _) =>
            {
                var item = positions.serializedProperty.GetArrayElementAtIndex(index);
                rect.y += 4;
                rect.height -= 8;
                EditorGUI.PropertyField(rect, item, new GUIContent($"坐标{index}"));
            }
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    然后在OnInspectorGUI中调用下DoLayoutList()方法。

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        
        EditorGUILayout.PropertyField(serializedObject.FindProperty("Looped"), new GUIContent("循环"));
        EditorGUILayout.IntSlider(serializedObject.FindProperty("Resolution"), 1, 100, new GUIContent("细节"));
        EditorGUILayout.Space();
        positions.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    重载OnSceneGUI,实现坐标点的绘制,以及坐标点的在Scene窗口中的位置编辑。

    private void OnSceneGUI()
    {
        if (Tools.current == Tool.Move)
        {
            Color oldColor = Handles.color;
            var localToWorld = Target.transform.localToWorldMatrix;
            for (int i = 0; i < Target.positions.Length; ++i)
            {
                DrawSelectionHandle(i, localToWorld);
                if (positions.index == i) // Selected
                {
                    DrawPositionControl(i, localToWorld, Target.transform.rotation);
                }
            }
    
            Handles.color = oldColor;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Scene窗口中,有三种基本操作:移动位置,旋转,缩放,对应快捷点:W、E、R,Tools.current==Tool.Move,其实就是判断一下是不是处于位置移动模式。因为贝塞尔曲线的编辑,只需要移动位置点的位置,控制点和位置点的朝向信息是不需要的,所以,只有在移动模式下,才去绘制位置点并允许编辑。
    DrawSelectionHandle是绘制位置点上的小圆球。

    private void DrawSelectionHandle(int i, Matrix4x4 localToWorld)
    {
        if (Event.current.button != 1)
        {
            Vector3 pos = localToWorld.MultiplyPoint(Target.positions[i]);
            float size = HandleUtility.GetHandleSize(pos) * 0.2f;
            Handles.color = Color.white;
            if (Handles.Button(pos, Quaternion.identity, size, size, Handles.SphereHandleCap)
                && positions.index != i)
            {
                positions.index = i;
                EditorApplication.QueuePlayerLoopUpdate();
                InternalEditorUtility.RepaintAllViews();
            }
    
            // Label it
            Handles.BeginGUI();
            Vector2 labelSize = new Vector2(
                EditorGUIUtility.singleLineHeight * 2, EditorGUIUtility.singleLineHeight);
            Vector2 labelPos = HandleUtility.WorldToGUIPoint(pos);
            labelPos.y -= labelSize.y / 2;
            labelPos.x -= labelSize.x / 2;
            GUILayout.BeginArea(new Rect(labelPos, labelSize));
            GUIStyle style = new GUIStyle
            {
                normal =
                {
                    textColor = Color.black
                },
                alignment = TextAnchor.MiddleCenter
            };
            GUILayout.Label(new GUIContent(i.ToString(), "坐标点" + i), style);
            GUILayout.EndArea();
            Handles.EndGUI();
        }
    }
    
    • 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

    DrawPositionControl则是绘制被选中的位置点的坐标轴,允许用户进行位置移动。

    private void DrawPositionControl(int i, Matrix4x4 localToWorld, Quaternion localRotation)
    {
        Vector3 wp = Target.positions[i];
        Vector3 pos = localToWorld.MultiplyPoint(wp);
        EditorGUI.BeginChangeCheck();
        Handles.color = Color.red;
        Quaternion rotation = (Tools.pivotRotation == PivotRotation.Local)
            ? localRotation
            : Quaternion.identity;
        float size = HandleUtility.GetHandleSize(pos) * 0.1f;
        Handles.SphereHandleCap(0, pos, rotation, size, EventType.Repaint);
        pos = Handles.PositionHandle(pos, rotation);
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(target, "Move Waypoint");
            wp = Matrix4x4.Inverse(localToWorld).MultiplyPoint(pos);
            Target.positions[i] = wp;
            Target.Invalidate();
            EditorApplication.QueuePlayerLoopUpdate();
            InternalEditorUtility.RepaintAllViews();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    通过DrawGizmos在Scene窗口中绘制编辑的结果,完成曲线可视化。

    [DrawGizmo(GizmoType.Active | GizmoType.NotInSelectionHierarchy |
               GizmoType.InSelectionHierarchy | GizmoType.Pickable, typeof(Spline))]
    private static void DrawGizmos(Spline path, GizmoType selectionType)
    {
        DrawPathGizmo(path, Selection.activeGameObject == path.gameObject);
    }
    
    private static void DrawPathGizmo(Spline path, bool isActive)
    {
        // Draw the path
        Color colorOld = Gizmos.color;
        Gizmos.color = Color.green;
        float step = 1f / path.Resolution;
    
        Vector3 lastPos = path.EvaluatePosition(path.MinPos);
        float tEnd = path.MaxPos + step / 2;
        for (float t = path.MinPos + step; t <= tEnd; t += step)
        {
            Vector3 p = path.EvaluatePosition(t);
            Gizmos.DrawLine(p, lastPos);
            lastPos = p;
        }
        Gizmos.color = colorOld;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    源码下载

    点击此处下载源码

  • 相关阅读:
    【网络协议】Http-下
    python中,reshape 用法
    从零开始:使用 Kubernetes 部署 Nginx 应用
    Flutter的Platform介绍-跨平台开发,如何根据不同平台创建不同UI和行为
    vuex基础用法1.0
    【电源专题】CCM (ContinuousConduction Mode)和DCM(Discontinuous Conduction Mode)有什么区别?
    Windows安装MySQL8.0完整教程
    融合模型权限管理设计方案
    iOS15.4来袭:新增“男妈妈”表情及口罩面容解锁、AirTags反跟踪等新功能
    眼科动态图像处理系统使用说明(2023-8-11 ccc)
  • 原文地址:https://blog.csdn.net/sdhexu/article/details/127642062