Skip to content

Instantly share code, notes, and snippets.

@Farfarer
Created July 5, 2016 15:45
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Farfarer/a765cd07920d48a8713a0c1924db6d70 to your computer and use it in GitHub Desktop.
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
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 ();
}
}
// 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();
}
}
}
@ALapidis
Copy link

ALapidis commented Jul 9, 2016

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?

@SimonDarksideJ
Copy link

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.

@chenyi555
Copy link

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 :
problem
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