Assignment: | Create a tool for Unity |
Duration: | 2 months |
Team size: | Solo project |
Role: | Development |
Skills: | ![]() ![]() |
Links: | ![]() |
For this project I created a spline tool for Unity. The tool allows you to build splines within the editor. It also allows you to generate meshes along the spline. This way you can create complex scenes in a really fast way. My goal was to be able to generate roads and cities with this tool.
The maths behind it is not that hard to begin with, but got a lot more complicated at the end. One of the problems I encountered was to store the rotation around the splines axis. I solved this by storing the up vector for each handle. Another problem that took a lot of time to solve was the spacing of objects along the spline. To do this the tool uses a lookup table with the arc length of all sections.
The tool was a lot harder to build than I expected. It was also my first time working with splines and my first time building a tool. This resulted in some bad decisions in the core functionality of the tool. Because of this the scripts became very long and unclear. I really want to restart building this tool some time because splines are so much fun to create content with.
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);
}
}
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;
}
}
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;
}
}
![]() |
contact@rubenbimmel.nl | ![]() |
![]() |
Artstation | ![]() |
GitHub |