Ruben Bimmel

Game Development

Unity Spline Tool

Opdracht: Maak een tool voor Unity
Duur: 2 maanden
Team grootte: Solo project
Rol: Development
Vaardigheden:
Links: GitHub

Met deze tool kan je splines maken en bewerken in de editor van Unity. Daarnaast is er ook een tool om objecten te genereren langs deze spline. Op deze manier kan je heel snel complexe scenes bouwen. Mijn idee was om deze tool uiteindelijk te kunnen gebruiken om wegen en steden te genereren.

De wiskunde voor het tekenen van een bezier spline is redelijk eenvoudig, maar dit werd steeds complexer naarmate ik meer dingen ging toevoegen. De rotatie rond de as in de richting van de spline was een van de dingen waar ik veel problemen mee had. Ik heb dit opgelost door voor elk punt ook de up vector op te slaan. Een ander probleem waar ik lang op vast gezeten heb is de spacing tussen objecten langs de spline. Hiervoor maakt de tool gebruik van een lookup table met daarin de lengte van de krommen.

Dit was voor mij de eerste keer dat ik zo een complex project ben begonnen. Ik heb vooral veel tijd gestoken in het testen en toevoegen van nieuwe features. Dit is wel ten koste gegaan van de kwaliteit van de code. Uiteindelijk werden de scripts hierdoor erg lang en onoverzichtelijk. De tool werkt op zich wel, maar om hem nog beter te laten werken en echt alle functionaliteiten er in te krijgen moet er eigenlijk een heel groot gedeelte opnieuw geschreven worden. Ik ben wel van plan om hier nog een keer aan te gaan werken want het resultaat is erg leuk.

Bezier class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//Class that handles all spline calculations. Source: http://catlikecoding.com/unity/tutorials/curves-and-splines/
public static class Bezier {

    //Returns position for a quadratic curve
    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;
    }

    //Returns direction for a quadratic curve
    public static Vector3 GetFirstDerivative(Vector3 p0, Vector3 p1, Vector3 p2, float t) {
        return
            2f * (1f - t) * (p1 - p0) +
            2f * t * (p2 - p1);
    }

    //Returns position for a cubic curve
    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;
    }

    //Returns direction for a cubic curve
    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);
    }
}

Spline class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

[Serializable]
public class Spline {

    public List<ControlPoint> points;
    public string name;
    [SerializeField]
    private SplineSettings settings;
    public bool[] assetIsActive;        //Bools to set assets in the settings on or of for this spline

    private float[] arcLengthTable;     //Contains a list of lengths between parametric values of the bezier curves. It is used to get a point at a distance along the spline.
    private static int tableSize = 100; //Precision value for the table

    //Constructor
    public Spline() {
        points = new List<ControlPoint> {
            new ControlPoint(Vector3.forward, Vector3.forward),
            new ControlPoint(Vector3.forward * 2, Vector3.forward)
        };
        ResetArcLengthTable();
        name = string.Concat("Spline");
        settings = null;
        assetIsActive = null;
    }

    //Constructor using position and index in component
    public Spline(Vector3 position, int index) {
        points = new List<ControlPoint> {
            new ControlPoint(position, Vector3.forward),
            new ControlPoint(position + Vector3.forward, Vector3.forward)
        };
        ResetArcLengthTable();
        name = string.Concat("Spline_", index.ToString("D2"));
        settings = null;
        assetIsActive = null;
    }

    //Add a new controlpoint in front of the spline with the same direction as the last point
    public void AddControlPoint () {
        points.Add(new ControlPoint(
            points[points.Count - 1].GetAnchorPosition() + points[points.Count - 1].GetRelativeHandlePosition(1).normalized, //Position
            .5f * points[points.Count - 1].GetRelativeHandlePosition(1).normalized));                                        //Direction
        ResetArcLengthTable();
    }

    public void RemoveControlPoint (ControlPoint point) {
        points.Remove(point);
        ResetArcLengthTable();
    }

    //Insert a point between two other points, or before the first point
    public void InsertControlPoint (int index) {
        Vector3 newAnchor = new Vector3();
        Vector3 newDirection = new Vector3();
        if (index == 0) {
            newAnchor = points[index].GetAnchorPosition() + points[index].GetRelativeHandlePosition(0).normalized;
            newDirection = 5f * points[index].GetRelativeHandlePosition(1).normalized;
        } else {
            newAnchor = GetPoint(index - 1, .5f);
            newDirection = GetDirection(index - 1, .5f) * points[index].GetRelativeHandlePosition(0).magnitude * .5f;
            points[index - 1].SetMode(BezierControlPointMode.Aligned);
            points[index - 1].SetRelativeHandlePosition(1, points[index - 1].GetRelativeHandlePosition(1) * .5f);
            points[index].SetMode(BezierControlPointMode.Aligned);
            points[index].SetRelativeHandlePosition(0, points[index].GetRelativeHandlePosition(0) * .5f);
        }
        points.Insert(index, new ControlPoint(newAnchor, newDirection));
        ResetArcLengthTable();
    }

    //Get position on spline at the arcdistance of the entire spline
    public Vector3 GetPoint(float t) {
        t = GetArcPos(t);

        int curve = (int)t;
        t = t % 1;
        if (curve == points.Count - 1) {
            curve = points.Count - 2;
            t = 1;
        }

        return GetPoint(curve, t);
    }

    //Get direction on spline at the arcdistance of the entire spline
    public Vector3 GetDirection(float t) {
        t = GetArcPos(t);

        int curve = (int)t;
        t = t % 1;
        if (curve == points.Count - 1) {
            curve = points.Count - 2;
            t = 1;
        }
        return GetDirection(curve, t);
    }

    //Get up vector on spline at the arcdistance of the entire spline
    public Vector3 GetUp(float t) {
        t = GetArcPos(t);

        int curve = (int)t;
        t = t % 1;
        if (curve == points.Count - 1) {
            curve = points.Count - 2;
            t = 1;
        }
        Vector3 direction = GetDirection(curve, t);
        Quaternion rotation = Quaternion.Lerp(points[curve].GetRotation(), points[curve + 1].GetRotation(), t);
        return Vector3.ProjectOnPlane(rotation * Vector3.up, direction);
    }

    //Returns the parametric value for the position at arcdistance t
    private float GetArcPos (float t) {
        for (int i = 0; i < arcLengthTable.Length; i++) {
            if (arcLengthTable[i] > t) {
                float T1 = (float)(i - 1) / (float)tableSize;
                float T2 = (float)(i) / (float)tableSize;
                float dT = (t - arcLengthTable[i - 1]) / (arcLengthTable[i] - arcLengthTable[i - 1]);
                return Mathf.Lerp(T1, T2, dT);
            }
        }
        return points.Count - 1;
    }

    //Get the position for a bezier curve at position t
    private Vector3 GetPoint(int curve, float t) {
        return Bezier.GetPoint(points[curve].GetAnchorPosition(), points[curve].GetHandlePosition(1),
            points[curve + 1].GetHandlePosition(0), points[curve + 1].GetAnchorPosition(), t);
    }

    //Get the direction for a bezier curve at position t
    private Vector3 GetDirection(int curve, float t) {
        return Bezier.GetFirstDerivative(points[curve].GetAnchorPosition(), points[curve].GetHandlePosition(1),
            points[curve + 1].GetHandlePosition(0), points[curve + 1].GetAnchorPosition(), t);
    }

    //Returns the entire length of this spline
    public float GetArcLength() {
        return arcLengthTable[arcLengthTable.Length - 1];
    }

    //Recaclculates the arcLengthTable
    public void ResetArcLengthTable () {
        arcLengthTable = new float[(points.Count - 1) * tableSize + 1];
        arcLengthTable[0] = 0f;
        Vector3 lastPos = points[0].GetAnchorPosition();
        for (int i = 0; i < points.Count - 1; i++) {
            for (int j = 0; j < tableSize; j++) {
                if (i + j != 0) {
                    Vector3 nextPos = Bezier.GetPoint(points[i].GetAnchorPosition(), points[i].GetHandlePosition(1), points[i + 1].GetHandlePosition(0), points[i + 1].GetAnchorPosition(), (float)j / (float)tableSize);
                    arcLengthTable[i * tableSize + j] = arcLengthTable[i * tableSize + j - 1] + (nextPos - lastPos).magnitude;
                    lastPos = nextPos;
                }
            }
        }
        arcLengthTable[arcLengthTable.Length - 1] = arcLengthTable[arcLengthTable.Length - 2] + (points[points.Count - 1].GetAnchorPosition() - lastPos).magnitude;
    }

    public void SetSettings(SplineSettings _settings) {
        settings = _settings;
        assetIsActive = new bool[settings.generated.Count + settings.placers.Count];
        for (int i = 0; i < assetIsActive.Length; i++) {
            assetIsActive[i] = true;
        }
    }

    public SplineSettings GetSettings() {
        return settings;
    }

    /* Static function to calculate the euler angles for a point given its up and forward vector.
     * This is similar to Quaternion.LookRotation. It calculates x, y and z in a different order that makes more sense for a spline*/
    public static Vector3 GetEulerAngles(Vector3 up, Vector3 forward) {
        Vector3 euler = new Vector3();
        euler.y = Mathf.Rad2Deg * Mathf.Atan2(forward.x, forward.z);

        Vector3 xzDirection = forward;
        xzDirection.y = 0;
        euler.x = -Mathf.Rad2Deg * Mathf.Atan2(forward.y, xzDirection.magnitude);

        Vector3 perpendicular = Vector3.Cross(Vector3.up, xzDirection);
        Vector3 normal = Vector3.Cross(forward, perpendicular);
        if (Vector3.Angle(perpendicular, up) < 90)
            euler.z = 360 - Vector3.Angle(normal, up);
        else
            euler.z = Vector3.Angle(normal, up);

        return euler;
    }
}

Control point class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Linq;

public enum BezierControlPointMode {
    Aligned,    //Handles are allowed to have different magnitudes
    Mirrored    //Handles both have the same magnitude
}

[Serializable]
public class ControlPoint {
    [SerializeField]
    private Vector3 anchor;
    [SerializeField]
    private Vector3[] handles;          //handles[0] is the handle before the anchor, handles[1] is the handle after the anchor
    [SerializeField]
    private Vector3 up;                 //Used to store rotations along the splines axis
    [SerializeField]
    private BezierControlPointMode mode;
    public int connectedIndex;          //Used when ControlPoint is part of a junction. Default value = -1

    //Constructor
    public ControlPoint() {
        anchor = Vector3.zero;
        handles = new Vector3[2] {
            -.5f * Vector3.forward,
            .5f * Vector3.forward
        };
        up = Vector3.up;
        mode = BezierControlPointMode.Mirrored;
        connectedIndex = -1;
    }

    //Constructor with position and direction
    public ControlPoint(Vector3 position, Vector3 forward) {
        anchor = position;
        handles = new Vector3[2] {
            -.5f * forward,
            .5f * forward
        };
        up = Vector3.up;
        mode = BezierControlPointMode.Mirrored;
        connectedIndex = -1;
    }

    public Vector3 GetAnchorPosition () {
        return anchor;
    }
    
    public Vector3 GetHandlePosition(int index) {
        return anchor + GetRelativeHandlePosition(index);
    }

    public Vector3 GetRelativeHandlePosition(int index) {
        return handles[index];
    }

    public float GetHandleMagnitude (int index) {
        return handles[index].magnitude;
    }

    public Quaternion GetRotation() {
        return Quaternion.LookRotation(handles[1], up);
    }

    //Euler angles are calculated using the Spline.GetEulerAngles method.
    public Vector3 GetEulerAngles() {
        return Spline.GetEulerAngles(up, handles[1]);
    }

    public BezierControlPointMode GetMode() {
        return mode;
    }

    public void SetAnchorPosition(Vector3 position) {
        anchor = position;
    }

    public void SetHandlePosition(int index, Vector3 position) {
        SetRelativeHandlePosition(index, position - anchor);
    }

    //Updates both handle positions based on the new position of a single handle
    public void SetRelativeHandlePosition (int index, Vector3 position) {
        handles[index] = position;
        switch (mode) {
            case BezierControlPointMode.Aligned:
                Vector3 direction = -position;
                handles[1 - index] = direction.normalized * handles[1 - index].magnitude;
                break;
            case BezierControlPointMode.Mirrored:
                handles[1 - index] = -position;
                break;
        }
    }

    //Set the magnitude of a handle (or both handles when type is mirrored)
    public void SetHandleMagnitude (int index, float magnitude) {
        if (magnitude < .01f)
            magnitude = .01f;

        handles[index] = handles[index].normalized * magnitude;
        if (mode == BezierControlPointMode.Mirrored)
            handles[1 - index] = -handles[index];
    }

    public void Scale (Vector3 scale) {
        handles[0].Scale(scale);
        handles[1].Scale(scale);
    }

    public void SetMode (BezierControlPointMode newMode) {
        mode = newMode;
        SetRelativeHandlePosition(1, GetRelativeHandlePosition(1));
    }

    public void SetRotation (Quaternion rotation) {
        handles[0] = rotation * Vector3.back * handles[0].magnitude;
        handles[1] = rotation* Vector3.forward * handles[1].magnitude;
        up = rotation * Vector3.up;
    }
    
}

« Terug naar het overzicht

Contact