Unity 3d曲线编辑器

参考:https://blog.csdn.net/jxw167/article/details/77717012
https://blog.csdn.net/jxw167/article/details/77732605
https://blog.csdn.net/jxw167/article/details/77836509
https://blog.csdn.net/jxw167/article/details/77848742
https://blog.csdn.net/jxw167/article/details/77862110

画线

线的数据类型

using UnityEngine;

public class Line : MonoBehaviour {
    public Vector3 p0, p1;
}

线在编辑器中显示:由于Handles是世界空间,所以,要把point从局部空间转换到世界空间;改变位置后,再从世界空间转换到局部空间,修改point的值;记录改变、提醒保存;可以revert。
该类放在editor目录下

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Line))]
public class LineInspector : Editor
{
    //unity自带的回调函数
    private void OnSceneGUI()
    {
        Line line = target as Line;

        Transform handleTransform = line.transform;
        Quaternion handleRotation = Tools.pivotRotation == PivotRotation.Local ?
            handleTransform.rotation : Quaternion.identity;

        //将p0从局部空间转换到世界空间
        Vector3 p0 = handleTransform.TransformPoint(line.p0); 
        Vector3 p1 = handleTransform.TransformPoint(line.p1);

        Handles.color = Color.yellow;
        Handles.DrawLine(p0, p1);

        EditorGUI.BeginChangeCheck();
        p0 = Handles.DoPositionHandle(p0, handleRotation);
        //当点改变时,才进行点的移动
        if (EditorGUI.EndChangeCheck())
        {
            //记录历史记录,我们就可以执行撤销操作了
            Undo.RecordObject(line, "Move Line");
            //再重新转换回局部空间
            line.p0 = handleTransform.InverseTransformPoint(p0);
            //unity知道线改了,会提醒保存
            EditorUtility.SetDirty(line);
        }

        EditorGUI.BeginChangeCheck();
        p1 = Handles.DoPositionHandle(p1, handleRotation);
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(line, "Move Line");
            line.p1 = handleTransform.InverseTransformPoint(p1);
            EditorUtility.SetDirty(line);
        }
    }
}

Bezier曲线

Bezier曲线来自于线性插值,线性曲线可以写为B(t) = (1 - t) P0 + t P1。二次曲线B(t) = (1 - t) ((1 - t) P0 + t P1) + t ((1 - t) P1 + t P2)更深一步。 这只是线性曲线,P0和P1被两条新的线性曲线取代。它也可以被重写为更紧凑的形式B(t) = (1 - t)2 P0 + 2 (1 - t) t P1 + t2 P2。所以我们也可以直接使用二次公式而不是三次调用Vector3.Lerp。
另外,我们的二次Beziér曲线的一阶导数是B’(t) = 2 (1 - t) (P1 - P0) + 2 t (P2 - P1)。
二阶或三阶Bezier曲线公式

using UnityEngine;

public static class Bezier
{
    public static Vector3 GetSquarePoint(Vector3 p0, Vector3 p1, Vector3 p2, float t)
    {
        t = Mathf.Clamp01(t);
        float oneMinusT = 1f - t;
        return
            oneMinusT * oneMinusT * p0 +
            2f * oneMinusT * t * p1 +
            t * t * p2;
    }

    //B(t) = (1 - t)3 P0 + 3 (1 - t)2 t P1 + 3 (1 - t) t2 P2 + t3 P3
    public static Vector3 GetCubicPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
    {
        t = Mathf.Clamp01(t);
        float oneMinusT = 1f - t;
        return
            oneMinusT * oneMinusT * oneMinusT * p0 +
            3f * oneMinusT * oneMinusT * t * p1 +
            3f * oneMinusT * t * t * p2 +
            t * t * t * p3;
    }

    //B'(t) = 2 (1 - t) (P1 - P0) + 2 t (P2 - P1)。
    public static Vector3 GetSquareFirstDerivative(Vector3 p0, Vector3 p1, Vector3 p2, float t)
    {
        return 2f * (1f - t) * (p1 - p0) +
            2f * t * (p2 - p1);
    }

    public static Vector3 GetCubicFirstDerivative(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
    {
        t = Mathf.Clamp01(t);
        float oneMinusT = 1f - t;
        return
            3f * oneMinusT * oneMinusT * (p1 - p0) +
            6f * oneMinusT * t * (p2 - p1) +
            3f * t * t * (p3 - p2);
    }

    public static Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 p2, float t)
    {
        t = Mathf.Clamp01(t);
        float oneMinusT = 1f - t;
        return
            oneMinusT * oneMinusT * p0 +
            2f * oneMinusT * t * p1 +
            t * t * p2;
    }

    public static Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
    {
        t = Mathf.Clamp01(t);
        float oneMinusT = 1f - t;
        return
            oneMinusT * oneMinusT * oneMinusT * p0 +
            3f * oneMinusT * oneMinusT * t * p1 +
            3f * oneMinusT * t * t * p2 +
            t * t * t * p3;
    }

    public static Vector3 GetFirstDerivative(Vector3 p0, Vector3 p1, Vector3 p2, float t)
    {
        return 2f * (1f - t) * (p1 - p0) +
            2f * t * (p2 - p1);
    }

    public static Vector3 GetFirstDerivative(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
    {
        t = Mathf.Clamp01(t);
        float oneMinusT = 1f - t;
        return
            3f * oneMinusT * oneMinusT * (p1 - p0) +
            6f * oneMinusT * t * (p2 - p1) +
            3f * t * t * (p3 - p2);
    }
}

立方Bezier曲线类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubicBezierCurve : MonoBehaviour {

    public Vector3[] points;

    //它用三点进行初始化, 该方法也可用作特殊的Unity方法,该方法在第一次创建组件或在编辑器模式点击重置组件时由编辑器调用。该方法只在编辑模式被调用,经常被用来设置初始值
    public void Reset()
    {
        points = new Vector3[] {
            new Vector3(1f, 0f, 0f),
            new Vector3(2f, 0f, 0f),
            new Vector3(3f, 0f, 0f),
            new Vector3(4f, 0f, 0f)
        };
    }

    public Vector3 GetPoint(float t)
    {
        //转换到世界空间
        return transform.TransformPoint(Bezier.GetCubicPoint(points[0], points[1], points[2], points[3], t));
    }

    //GetFirstDerivative函数产生与曲线相切的线,可以将其解释为沿着曲线移动的速度。 所以现在我们可以添加一个GetVelocity方法到Bezier曲线。因为它产生一个速度矢量而不是一个点,它不应该受到曲线的位置的影响,所以我们在变换后减去它。
    public Vector3 GetVelocity(float t)
    {
        return transform.TransformPoint(Bezier.GetCubicFirstDerivative(points[0], points[1], points[2], points[3], t)) -
            transform.position;
    }

    //将速度向量归一化,表示方向
    public Vector3 GetDirection(float t)
    {
        return GetVelocity(t).normalized;
    }
}

立方Bezier曲线编辑器类

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(CubicBezierCurve))]
public class CubicBezierCurveInspector : Editor
{
    private CubicBezierCurve curve;
    private Transform handleTransform;
    private Quaternion handleRotation;

    //step越大,越圆滑
    private const int lineSteps = 10;

    private void OnSceneGUI()
    {
        curve = target as CubicBezierCurve;
        handleTransform = curve.transform;
        handleRotation = Tools.pivotRotation == PivotRotation.Local ?
        handleTransform.rotation : Quaternion.identity;

        //将点的显示提取到函数,增加复用性
        Vector3 p0 = ShowPoint(0);
        Vector3 p1 = ShowPoint(1);
        Vector3 p2 = ShowPoint(2);
        Vector3 p3 = ShowPoint(3);

        Handles.color = Color.gray;
        //控制点之间的线段
        Handles.DrawLine(p0, p1);
        Handles.DrawLine(p1, p2);
        Handles.DrawLine(p2, p3);

        Vector3 lineStart = curve.GetPoint(0f);
        Handles.color = Color.green;
        Handles.DrawLine(lineStart, lineStart + curve.GetDirection(0f));
        //绘制曲线,调用Bezier根据t获取曲线上的点的公式。bezier曲线的思想是参数化。通过在曲线上的连续步长之间绘制直线来近似。
        //也可以用unity自带的函数:Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);绘制漂亮的立方曲线
        for (int i = 1; i <= lineSteps; i++)
        {
            //getPoint:获取点并转换到世界空间
            Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);
            Handles.color = Color.white;
            Handles.DrawLine(lineStart, lineEnd);
            Handles.color = Color.green;
            Handles.DrawLine(lineEnd, lineEnd + curve.GetDirection(i / (float)lineSteps));
            lineStart = lineEnd;
        }
    }

    private Vector3 ShowPoint(int index)
    {
        Vector3 point = handleTransform.TransformPoint(curve.points[index]);
        EditorGUI.BeginChangeCheck();
        point = Handles.DoPositionHandle(point, handleRotation);
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(curve, "Move Point");
            EditorUtility.SetDirty(curve);
            curve.points[index] = handleTransform.InverseTransformPoint(point);
        }
        return point;
    }

}

Spine B样条曲线

如果要创建复杂的曲线,需要连接多个曲线,这样的构造称为样条。
先复制上面的bezierCurve及其编辑器的代码,将类型名改为BezierSpline。然后给BezierSpline添加一个方法,以向样条添加另一条曲线,因为样条曲线需要连续,所以上一条曲线的最后一个点与下一条曲线的第一个点是一样的。

using System;
using UnityEngine;

public class BezierSplineCurve : MonoBehaviour {
    [SerializeField]
    private bool loop;

    public bool Loop
    {
        get
        {
            return loop;
        }
        set
        {
            loop = value;
            if (value == true)
            {
                modes[modes.Length - 1] = modes[0];
                SetControlPoint(0, points[0]);
            }
        }
    }


    //在曲线之间存储模式,重置和创建新曲线时,需要改变模式数组的大小。模式数数组为曲线数+1的数组
    [SerializeField]
    private BezierControlPointMode[] modes;

    //虽然样条曲线是连续的,但在曲线段之间会急剧变化,这种突变导致了点的方向和速度变化。因为这两个曲线之间的共享控制点会产生两个不同的速度。如果我们想让曲线速度相等,必须确保定义他们的两个控制点——前一条曲线和后一条曲线在共享控制点周围镜像,这就确保了第一阶和二阶导数是连续的。最灵活的办法是决定每条曲线的边界,当然,一旦有了这些限制,就不能让任何人直接编辑BezierSpline的点。所以,数组要私有并提供对他们的间接访问,确保让unity知道我们仍然想要序列化我们的点,否则不会被保存。
    [SerializeField]
    private Vector3[] points;


    public int ControlPointCount
    {
        get
        {
            return points.Length;
        }
    }

    public Vector3 GetControlPoint(int index)
    {
        return points[index];
    }

    public void SetControlPoint(int index, Vector3 point)
    {
        if (index % 3 == 0)
        {
            Vector3 delta = point - points[index];
            if(loop)
            {
                if (index == 0)
                {
                    points[1] += delta;
                    points[points.Length - 2] += delta;
                    points[points.Length - 1] = point;
                }
                else if (index == points.Length - 1)
                {
                    points[0] = point;
                    points[1] += delta;
                    points[index - 1] += delta;
                }
                else
                {
                    points[index - 1] += delta;
                    points[index + 1] += delta;
                }
            }
            else
            {
                //中间节点移动时,前后两个节点一起动
                if (index > 0)
                {
                    points[index - 1] += delta;
                }
                if (index + 1 < points.Length)
                {
                    points[index + 1] += delta;
                }
            }
        }
        points[index] = point;
        EnforceMode(index);
    }

    private void EnforceMode(int index)
    {
        int modeIndex = (index + 1) / 3;
        BezierControlPointMode mode = modes[modeIndex];
        //模式设为自由或在曲线的端点时,不做任何事就返回
        if (mode == BezierControlPointMode.Free || (!loop && (modeIndex == 0 || modeIndex == modes.Length - 1)))
        {
            return;
        }
        int middleIndex = modeIndex * 3;
        int fixedIndex, enforcedIndex;
        if (index <= middleIndex)
        {
            fixedIndex = middleIndex - 1;
            //考虑循环
            if (fixedIndex < 0)  
            {
                fixedIndex = points.Length - 2;
            }
            enforcedIndex = middleIndex + 1;
            //考虑循环
            if (enforcedIndex >= points.Length)  
            {
                enforcedIndex = 1;
            }
        }
        else
        {
            fixedIndex = middleIndex + 1;
            //考虑循环
            if (fixedIndex >= points.Length)
            {
                fixedIndex = 1;
            }
            enforcedIndex = middleIndex - 1;
            //考虑循环
            if (enforcedIndex < 0)   
            {
                enforcedIndex = points.Length - 2;
            }
        }

        Vector3 middle = points[middleIndex];
        Vector3 enforcedTangent = middle - points[fixedIndex];
        if (mode == BezierControlPointMode.Aligned)
        {
            enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
        }
        points[enforcedIndex] = middle + enforcedTangent;
    }

    //每个控制点的模式。点索引0、1、2、3、4、5、6,对应模式索引为0、0、1、1、1、2、2。就是+1除以3的关系
    public void SetControlPointMode(int index, BezierControlPointMode mode)
    {
        int modeIndex = (index + 1) / 3;
        modes[modeIndex] = mode;
        if (loop)
        {
            if (modeIndex == 0)
            {
                modes[modes.Length - 1] = mode;
            }
            else if (modeIndex == modes.Length - 1)
            {
                modes[0] = mode;
            }
        }
        EnforceMode(index);
    }

    public void Reset()
    {
        points = new Vector3[] {
            new Vector3(1f, 0f, 0f),
            new Vector3(2f, 0f, 0f),
            new Vector3(3f, 0f, 0f),
            new Vector3(4f, 0f, 0f)
        };
        modes = new BezierControlPointMode[] {
            BezierControlPointMode.Free,
            BezierControlPointMode.Free
        };
        //Debug.LogError("I am Reset");
    }

    //public Vector3 GetPoint(float t)
    //{
    //    return transform.TransformPoint(Bezier.GetCubicPoint(points[0], points[1], points[2], points[3], t));
    //}

    //乘以曲线数,得到点的index;t乘以曲线数,减去index取小数得到曲线的内插值
    public Vector3 GetPoint(float t)
    {
        int i;
        if (t >= 1f)
        {
            t = 1f;
            i = points.Length - 4;
        }
        else
        {
            t = Mathf.Clamp01(t) * CurveCount;
            i = (int)t;
            t -= i;
            i *= 3;
        }
        return transform.TransformPoint(Bezier.GetCubicPoint(
            points[i], points[i + 1], points[i + 2], points[i + 3], t));
    }

    public Vector3 GetVelocity(float t)
    {
        int i;
        if (t >= 1f)
        {
            t = 1f;
            i = points.Length - 4;
        }
        else
        {
            t = Mathf.Clamp01(t) * CurveCount;
            i = (int)t;
            t -= i;
            i *= 3;
        }
        return transform.TransformPoint(Bezier.GetCubicFirstDerivative(points[i], points[i + 1], points[i + 2], points[i + 3], t)) -
            transform.position;
    }

    public Vector3 GetDirection(float t)
    {
        return GetVelocity(t).normalized;
    }
    //样条曲线由多条bezier曲线构成,在bezier曲线基础上添加曲线,每次增加3个点。上一曲线的最后一个点和下一曲线的第一个点是一样的
    public void AddCurve()
    {
        Vector3 point = points[points.Length - 1];
        //扩大points数组
        Array.Resize(ref points, points.Length + 3);
        point.x += 1f;
        points[points.Length - 3] = point;
        point.x += 1f;
        points[points.Length - 2] = point;
        point.x += 1f;
        points[points.Length - 1] = point;

        Array.Resize(ref modes, modes.Length + 1);
        modes[modes.Length - 1] = modes[modes.Length - 2];
        //添加曲线时,约束被执行
        EnforceMode(points.Length - 4);

        //循环
        if (loop)
        {
            points[points.Length - 1] = points[0];
            modes[modes.Length - 1] = modes[0];
            EnforceMode(0);
        }
    }

    public int CurveCount
    {
        get
        {
            return (points.Length - 1) / 3;
        }
    }

    public BezierControlPointMode GetControlPointMode(int index)
    {
        //Debug.LogError(modes.Length + "這是总长度,index:" + index);
        return modes[(index + 1) / 3];
    }

}

控制点模式

public enum BezierControlPointMode
{
    Free,
    Aligned,
    Mirrored
}

编辑器

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(BezierSplineCurve))]
public class BezierSplineCurveInspector : Editor
{
    private BezierSplineCurve spline;
    private Transform handleTransform;
    private Quaternion handleRotation;

    private const int lineSteps = 50;
    private const float directionScale = 3f;

    private const int stepsPerCurve = 10;
    private const float handleSize = 0.04f;
    private const float pickSize = 0.06f;

    private int selectedIndex = -1;

    private static Color[] modeColors = {
        Color.white,
        Color.yellow,
        Color.cyan
    };

    private void OnSceneGUI()
    {
        spline = target as BezierSplineCurve;
        handleTransform = spline.transform;
        handleRotation = Tools.pivotRotation == PivotRotation.Local ?
            handleTransform.rotation : Quaternion.identity;

        Vector3 p0 = ShowPoint(0);
        //所有曲线都要绘制,循环每条曲线
        for(int i = 1; i < spline.ControlPointCount; i += 3)
        {
            Vector3 p1 = ShowPoint(i);
            Vector3 p2 = ShowPoint(i + 1);
            Vector3 p3 = ShowPoint(i + 2);

            Handles.color = Color.gray;
            Handles.DrawLine(p0, p1);
            Handles.DrawLine(p1, p2);
            Handles.DrawLine(p2, p3);

            //用unity自带的可以,自己写也可以;自己写的可以控制步长
            //Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);
            for (int j = 1; j <= lineSteps; j++)
            {
                Vector3 lineEnd = spline.GetPoint(j / (float)lineSteps);
                Handles.color = Color.white;
                Handles.DrawLine(p0, lineEnd);
                Handles.color = Color.green;
                Handles.DrawLine(lineEnd, lineEnd + spline.GetDirection(i / (float)lineSteps));
                p0 = lineEnd;
            }
            p0 = p3;
        }
        ShowDirections();

        //Vector3 p1 = ShowPoint(1);
        //Vector3 p2 = ShowPoint(2);
        //Vector3 p3 = ShowPoint(3);

        //Handles.color = Color.gray;
        //Handles.DrawLine(p0, p1);
        //Handles.DrawLine(p1, p2);
        //Handles.DrawLine(p2, p3);

        //Vector3 lineStart = spline.GetPoint(0f);
        //Handles.color = Color.green;
        //Handles.DrawLine(lineStart, lineStart + spline.GetDirection(0f));
        //for (int i = 1; i <= lineSteps; i++)
        //{
        //    Vector3 lineEnd = spline.GetPoint(i / (float)lineSteps);
        //    Handles.color = Color.white;
        //    Handles.DrawLine(lineStart, lineEnd);
        //    Handles.color = Color.green;
        //    Handles.DrawLine(lineEnd, lineEnd + spline.GetDirection(i / (float)lineSteps));
        //    lineStart = lineEnd;
        //}
    }

    //只显示点击的控制点处的Handles;默认情况下没有选择
    private Vector3 ShowPoint(int index)
    {
        Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
        //固定的屏幕大小
        float size = HandleUtility.GetHandleSize(point);
        //第一个点2倍大
        if (index == 0)
        {
            size *= 2f;
        }
        Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
        if(Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap))
        {
            selectedIndex = index;
            //当我们在场景视图中选择一个点时,检查器不会刷新自己。我们可以通过调用spline的SetDirty来解决这个问题,但这并不正确,因为样条没有改变。幸运的是,我们可以发出重新绘制的请求
            Repaint();
        }
        if(selectedIndex == index)
        {
            EditorGUI.BeginChangeCheck();
            point = Handles.DoPositionHandle(point, handleRotation);
            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(spline, "Move Point");
                EditorUtility.SetDirty(spline);
                spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
            }
        }
        return point;
    }

    private void ShowDirections()
    {
        Handles.color = Color.green;
        Vector3 point = spline.GetPoint(0f);
        Handles.DrawLine(point, point + spline.GetDirection(0f) * directionScale);
        int steps = stepsPerCurve * spline.CurveCount;
        for (int i = 1; i <= steps; i++)
        {
            point = spline.GetPoint(i / (float)steps);
            Handles.DrawLine(point, point + spline.GetDirection(i / (float)steps) * directionScale);
        }
    }

    public override void OnInspectorGUI()
    {
    //当然    ,我们也不再希望允许直接访问检查器中的数组,因此取消对DrawDefaultInspector的调用,为了仍然允许通过输入进行更改,让我们为选定的点显示一个向量场。
        spline = target as BezierSplineCurve;
        EditorGUI.BeginChangeCheck();
        bool loop = EditorGUILayout.Toggle("Loop", spline.Loop);
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(spline, "Toggle Loop");
            EditorUtility.SetDirty(spline);
            spline.Loop = loop;
        }
        if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount)
        {
            DrawSelectedPointInspector();
        }
        //在检查器添加“增加曲线”按钮
        if (GUILayout.Button("Add Curve"))
        {
            Undo.RecordObject(spline, "Add Curve");
            spline.AddCurve();
            EditorUtility.SetDirty(spline);
        }
    }

    private void DrawSelectedPointInspector()
    {
        GUILayout.Label("Selected Point");
        EditorGUI.BeginChangeCheck();
        Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(spline, "Move Point");
            EditorUtility.SetDirty(spline);
            spline.SetControlPoint(selectedIndex, point);
        }
        EditorGUI.BeginChangeCheck();
        BezierControlPointMode mode = (BezierControlPointMode)
            EditorGUILayout.EnumPopup("Mode", spline.GetControlPointMode(selectedIndex));
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(spline, "Change Point Mode");
            //允许我们改变所选点的模式,改变一个点的模式也会改变相关的点的模式
            spline.SetControlPointMode(selectedIndex, mode);
            EditorUtility.SetDirty(spline);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_18229381/article/details/80785172