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 );
}

上面的代码虽然浅显易懂,但是计算挺复杂,研究了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;
}

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

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;
}
Spline类

公共成员:

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

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

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

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

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}"));
        }
    };
}

然后在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();
}

重载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;
    }
}

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();
    }
}

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();
    }
}

通过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;
}

源码下载

点击此处下载源码

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

示申○言舌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值