Created
July 5, 2016 15:45
-
-
Save Farfarer/a765cd07920d48a8713a0c1924db6d70 to your computer and use it in GitHub Desktop.
Catenary curves in Unity. Creates a catenary curve between two points with a given amount of slack. http://farfarer.com/temp/unity_cables.png
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine.EventSystems; | |
[System.Serializable] | |
public class CableCurve { | |
[SerializeField] | |
Vector3 m_start; | |
[SerializeField] | |
Vector3 m_end; | |
[SerializeField] | |
float m_slack; | |
[SerializeField] | |
int m_steps; | |
[SerializeField] | |
Color m_color; | |
[SerializeField] | |
bool m_regen; | |
[SerializeField] | |
bool m_handles; | |
static Vector3[] emptyCurve = new Vector3[]{new Vector3 (0.0f, 0.0f, 0.0f), new Vector3 (0.0f, 0.0f, 0.0f)}; | |
[SerializeField] | |
Vector3[] points; | |
public bool drawHandles | |
{ | |
get { return m_handles; } | |
set | |
{ | |
m_handles = value; | |
} | |
} | |
public bool regenPoints | |
{ | |
get { return m_regen; } | |
set { | |
m_regen = value; | |
} | |
} | |
public Vector3 start | |
{ | |
get { return m_start; } | |
set { | |
if (value != m_start) | |
m_regen = true; | |
m_start = value; | |
} | |
} | |
public Vector3 end | |
{ | |
get { return m_end; } | |
set { | |
if (value != m_end) | |
m_regen = true; | |
m_end = value; | |
} | |
} | |
public float slack | |
{ | |
get { return m_slack; } | |
set { | |
if (value != m_slack) | |
m_regen = true; | |
m_slack = Mathf.Max(0.0f, value); | |
} | |
} | |
public int steps | |
{ | |
get { return m_steps; } | |
set { | |
if (value != m_steps) | |
m_regen = true; | |
m_steps = Mathf.Max (2, value); | |
} | |
} | |
public Color color | |
{ | |
get { return m_color; } | |
set { | |
if (value != m_color) | |
m_regen = true; | |
m_color = value; | |
} | |
} | |
public Vector3 midPoint | |
{ | |
get | |
{ | |
Vector3 mid = Vector3.zero; | |
if (m_steps == 2) { | |
return (points[0] + points[1]) * 0.5f; | |
} | |
else if (m_steps > 2) { | |
int m = m_steps / 2; | |
if ((m_steps % 2) == 0) { | |
if (points.InRange (m+1)) | |
mid = (points[m] + points[m + 1]) * 0.5f; | |
} | |
else { | |
if (points.InRange(m)) | |
mid = points[m]; | |
} | |
} | |
return mid; | |
} | |
} | |
public CableCurve () { | |
points = emptyCurve; | |
m_start = Vector3.up; | |
m_end = Vector3.up + Vector3.forward; | |
m_slack = 0.5f; | |
m_steps = 20; | |
m_regen = true; | |
m_color = Color.white; | |
m_handles = true; | |
} | |
public CableCurve (CableCurve v) { | |
points = v.Points (); | |
m_start = v.start; | |
m_end = v.end; | |
m_slack = v.slack; | |
m_steps = v.steps; | |
m_regen = v.regenPoints; | |
m_color = v.color; | |
m_handles = v.drawHandles; | |
} | |
public Color[] Colors () { | |
Color[] cols = new Color[m_steps]; | |
for (int c = 0; c < m_steps; c++) { | |
cols[c] = m_color; | |
} | |
return cols; | |
} | |
public Vector3[] Points () | |
{ | |
if (!m_regen) | |
return points; | |
if (m_steps < 2) | |
return emptyCurve; | |
float lineDist = Vector3.Distance (m_end, m_start); | |
float lineDistH = Vector3.Distance (new Vector3(m_end.x, m_start.y, m_end.z), m_start); | |
float l = lineDist + Mathf.Max(0.0001f, m_slack); | |
float r = 0.0f; | |
float s = m_start.y; | |
float u = lineDistH; | |
float v = end.y; | |
if ((u-r) == 0.0f) | |
return emptyCurve; | |
float ztarget = Mathf.Sqrt(Mathf.Pow(l, 2.0f) - Mathf.Pow(v-s, 2.0f)) / (u-r); | |
int loops = 30; | |
int iterationCount = 0; | |
int maxIterations = loops * 10; // For safety. | |
bool found = false; | |
float z = 0.0f; | |
float ztest = 0.0f; | |
float zstep = 100.0f; | |
float ztesttarget = 0.0f; | |
for (int i = 0; i < loops; i++) { | |
for (int j = 0; j < 10; j++) { | |
iterationCount++; | |
ztest = z + zstep; | |
ztesttarget = (float)Math.Sinh(ztest)/ztest; | |
if (float.IsInfinity (ztesttarget)) | |
continue; | |
if (ztesttarget == ztarget) { | |
found = true; | |
z = ztest; | |
break; | |
} else if (ztesttarget > ztarget) { | |
break; | |
} else { | |
z = ztest; | |
} | |
if (iterationCount > maxIterations) { | |
found = true; | |
break; | |
} | |
} | |
if (found) | |
break; | |
zstep *= 0.1f; | |
} | |
float a = (u-r)/2.0f/z; | |
float p = (r+u-a*Mathf.Log((l+v-s)/(l-v+s)))/2.0f; | |
float q = (v+s-l*(float)Math.Cosh(z)/(float)Math.Sinh(z))/2.0f; | |
points = new Vector3[m_steps]; | |
float stepsf = m_steps-1; | |
float stepf; | |
for (int i = 0; i < m_steps; i++) { | |
stepf = i / stepsf; | |
Vector3 pos = Vector3.zero; | |
pos.x = Mathf.Lerp(start.x, end.x, stepf); | |
pos.z = Mathf.Lerp(start.z, end.z, stepf); | |
pos.y = a * (float)Math.Cosh(((stepf*lineDistH)-p)/a)+q; | |
points[i] = pos; | |
} | |
m_regen = false; | |
return points; | |
} | |
} | |
[RequireComponent(typeof(MeshFilter))] | |
[RequireComponent(typeof(MeshRenderer))] | |
public class Cable : MonoBehaviour { | |
[SerializeField] | |
public List<CableCurve> cables; | |
public Material cableMatr; | |
public Space space; | |
bool firstRun = true; | |
public void GenerateMesh () { | |
Transform tr = transform; | |
MeshFilter meshFilter = GetComponent <MeshFilter>(); | |
MeshRenderer meshRenderer = GetComponent <MeshRenderer>(); | |
Mesh cableMesh = meshFilter.sharedMesh; | |
if (cableMesh == null) | |
cableMesh = new Mesh(); | |
cableMesh.Clear(); | |
int numCables = (cables == null) ? 0 : cables.Count; | |
cableMesh.subMeshCount = numCables; | |
if (firstRun) { | |
for (int c = 0; c < numCables; c++) { | |
cables[c].regenPoints = true; | |
} | |
firstRun = false; | |
} | |
List<Vector3> points = new List<Vector3> (); | |
List<Color> colors = new List<Color> (); | |
for (int c = 0; c < numCables; c++) { | |
points.AddRange (cables[c].Points ()); | |
colors.AddRange (cables[c].Colors ()); | |
} | |
if (space == Space.World) { | |
int count = points.Count; | |
for (int p = 0; p < count; p++) { | |
Matrix4x4 ltw = tr.worldToLocalMatrix; | |
points[p] = ltw.MultiplyPoint (points[p]); | |
} | |
} | |
cableMesh.SetVertices(points); | |
cableMesh.SetColors(colors); | |
int indice = 0; | |
for (int c = 0; c < numCables; c++) { | |
int numIndices = cables[c].steps; | |
int[] indices = new int[numIndices]; | |
for (int i = 0; i < numIndices; i++) { | |
indices[i] = indice; | |
indice++; | |
} | |
cableMesh.SetIndices (indices, MeshTopology.LineStrip, c); | |
} | |
cableMesh.RecalculateBounds (); | |
meshFilter.sharedMesh = cableMesh; | |
AssignMaterials(); | |
} | |
public void AssignMaterials () { | |
MeshRenderer meshRenderer = GetComponent<MeshRenderer>(); | |
int numCables = (cables == null) ? 0 : cables.Count; | |
Material[] mats = new Material[numCables]; | |
for (int c = 0; c < numCables; c++) { | |
mats[c] = cableMatr; | |
} | |
meshRenderer.sharedMaterials = mats; | |
} | |
public void Start () { | |
GenerateMesh (); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Place this into directory called Editor in your Assets directory. | |
using UnityEngine; | |
using System; | |
using System.Collections.Generic; | |
using UnityEditor; | |
[CustomEditor(typeof(Cable))] | |
public class CableEditor : Editor { | |
int highlight = -1; | |
void OnSceneGUI() { | |
Cable cable = (Cable)target; | |
Transform tr = cable.transform; | |
Quaternion rot = Quaternion.identity; | |
Matrix4x4 toWorld = Matrix4x4.identity; | |
Matrix4x4 toLocal = Matrix4x4.identity; | |
if (cable.space == Space.Self) { | |
toWorld = tr.localToWorldMatrix; | |
toLocal = tr.worldToLocalMatrix; | |
rot = tr.rotation; | |
} | |
int count = (cable.cables == null) ? 0 : cable.cables.Count; | |
if (count > 0) { | |
bool setDirty = false; | |
for (int i = 0; i < count; i++) { | |
CableCurve curve = cable.cables[i]; | |
// Draw cable index. | |
Handles.Label(toWorld.MultiplyPoint(curve.midPoint), i.ToString()); | |
// Draw highlighted cable. | |
if (i == highlight) { | |
Handles.color = Color.white; | |
if (cable.space == Space.Self) { | |
Vector3[] points = new Vector3[curve.steps]; | |
Vector3[] curvePoints = curve.Points(); | |
for (int p = 0; p < points.Length; p++) { | |
points[p] = toWorld.MultiplyPoint(curvePoints[p]); | |
} | |
Handles.DrawPolyLine(points); | |
} | |
else { | |
Handles.DrawPolyLine(cable.cables[i].Points()); | |
} | |
} | |
if (curve.drawHandles) { | |
EditorGUI.BeginChangeCheck(); | |
Vector3 start = toLocal.MultiplyPoint(Handles.PositionHandle(toWorld.MultiplyPoint(curve.start), rot)); | |
Vector3 end = toLocal.MultiplyPoint(Handles.PositionHandle(toWorld.MultiplyPoint(curve.end), rot)); | |
if (EditorGUI.EndChangeCheck()) { | |
Undo.RecordObject(cable, "Edited Cable"); | |
curve.start = start; | |
curve.end = end; | |
setDirty = true; | |
} | |
} | |
} | |
if (tr.hasChanged && cable.space == Space.World) { | |
setDirty = true; | |
tr.hasChanged = false; | |
} | |
if (setDirty) { | |
cable.GenerateMesh (); | |
EditorUtility.SetDirty(cable); | |
} | |
} | |
} | |
public override void OnInspectorGUI() { | |
Cable cable = (Cable)target; | |
int count = (cable.cables == null) ? 0 : cable.cables.Count; | |
bool setDirty = false; | |
EditorGUI.BeginChangeCheck(); | |
Space space = (Space)EditorGUILayout.Popup("Space", (int)cable.space, Enum.GetNames(typeof(Space))); | |
if (EditorGUI.EndChangeCheck()) { | |
Undo.RecordObject(cable, "Edited Cable Space"); | |
cable.space = space; | |
setDirty = true; | |
} | |
EditorGUI.BeginChangeCheck(); | |
Material cableMatr = (Material)EditorGUILayout.ObjectField("Material", cable.cableMatr, typeof(Material), true); | |
if (EditorGUI.EndChangeCheck()) { | |
Undo.RecordObject(cable, "Edited Cable Material"); | |
cable.cableMatr = cableMatr; | |
cable.AssignMaterials(); | |
setDirty = true; | |
} | |
EditorGUILayout.Separator(); | |
highlight = -1; | |
if (count > 0) { | |
for (int i = 0; i < count; i++) { | |
Rect r = EditorGUILayout.BeginVertical(); | |
CableCurve curve = cable.cables[i]; | |
EditorGUI.BeginChangeCheck(); | |
curve.drawHandles = EditorGUILayout.Foldout(curve.drawHandles, i.ToString()); | |
if (EditorGUI.EndChangeCheck()) { | |
setDirty = true; | |
} | |
if (curve.drawHandles) { | |
EditorGUI.BeginChangeCheck(); | |
Vector3 start = EditorGUILayout.Vector3Field("Start", curve.start); | |
Vector3 end = EditorGUILayout.Vector3Field("End", curve.end); | |
float slack = EditorGUILayout.FloatField("Slack", curve.slack); | |
int steps = EditorGUILayout.IntField("Points", curve.steps); | |
Color color = EditorGUILayout.ColorField("Color", curve.color); | |
if (EditorGUI.EndChangeCheck()) { | |
Undo.RecordObject(cable, "Edited Cable"); | |
curve.start = start; | |
curve.end = end; | |
curve.slack = slack; | |
curve.steps = steps; | |
curve.color = color; | |
setDirty = true; | |
} | |
if (GUILayout.Button("Clone")) { | |
Undo.RecordObject(cable, "Cloned Cable"); | |
cable.cables.Add(new CableCurve(curve)); | |
count++; | |
setDirty = true; | |
} | |
if (GUILayout.Button("Remove")) { | |
Undo.RecordObject(cable, "Removed Cable"); | |
cable.cables.RemoveAt(i); | |
count--; | |
setDirty = true; | |
} | |
} | |
EditorGUILayout.EndVertical(); | |
if (r.Contains(Event.current.mousePosition)) { | |
highlight = i; | |
setDirty = true; | |
} | |
EditorGUILayout.Space(); | |
} | |
} | |
if (GUILayout.Button("Add Cable")) { | |
Undo.RecordObject(cable, "Added Cable"); | |
if (cable.cables == null) | |
cable.cables = new List<CableCurve>(); | |
cable.cables.Add(new CableCurve()); | |
setDirty = true; | |
} | |
if (setDirty) { | |
EditorUtility.SetDirty(cable); | |
cable.GenerateMesh(); | |
} | |
} | |
} |
just remove those IF statements @ALapidis Works fine without them. Just leave the rest as follows and it works:
if ((m_steps % 2) == 0)
{
mid = (points[m] + points[m + 1]) * 0.5f;
} else
{
mid = points[m];
}
Hope that helps.
Hello, I use your code to creates a catenary curve. I can creates catenary curves when drawing. but when I setted the point position by code, liked this :
the vertical line can not draw. I find your code
`public Vector3[] Points ()
{
if (!m_regen)
return points;
if (m_steps < 2)
return emptyCurve;
float lineDist = Vector3.Distance (m_end, m_start);
float lineDistH = Vector3.Distance (new Vector3(m_end.x, m_start.y, m_end.z), m_start);
float l = lineDist + Mathf.Max(0.0001f, m_slack);
float r = 0.0f;
float s = m_start.y;
float u = lineDistH;
float v = end.y;
if ((u-r) == 0.0f)
return emptyCurve; // when setted the point position ,This return `
when two point position is vertical. has the problem. can you have a solution ?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
When I try and add these scripts to Unity, it complains InRange is not an extension or type of Vector3[]. I'm unclear what InRange is as arrays have no such extension call in Unity. Did I miss something?