Skip to content

Instantly share code, notes, and snippets.

@N-Carter
Created May 14, 2012 23:22
Show Gist options
  • Save N-Carter/2698046 to your computer and use it in GitHub Desktop.
Save N-Carter/2698046 to your computer and use it in GitHub Desktop.
Custom inspector for SplineMesh
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Linq;
[CustomEditor(typeof(SplineMesh))]
public class SplineMeshEditor : Editor
{
protected SplineMesh m_Target;
protected MeshFilter m_MeshFilter;
protected MeshRenderer m_MeshRenderer;
protected MeshCollider m_MeshCollider;
protected Mesh m_Mesh;
protected int m_CurrentNode = 0;
protected SerializedProperty m_NodesProperty;
protected SerializedProperty m_HeightProperty;
protected SerializedProperty m_DepthProperty;
protected SerializedProperty m_BaselineOffsetProperty;
protected SerializedProperty m_DivisionsProperty;
protected SerializedProperty m_UVTilingProperty;
protected SerializedProperty m_UVOffsetProperty;
protected SerializedProperty m_LengthAffectsUProperty;
protected class MeshData
{
public Vector3[] vertices;
public Vector3[] normals;
public Vector2[] uvs;
public MeshData(int numVertices)
{
vertices = new Vector3[numVertices];
normals = new Vector3[numVertices];
uvs = new Vector2[numVertices];
}
public void PushVertex(int index, Vector3 position, Vector3 normal, Vector2 uv)
{
vertices[index] = position;
normals[index] = normal;
uvs[index] = uv;
// Debug.Log(string.Format("{0}: {1} {2}", index, position, uv));
}
}
#region Events
protected void OnEnable()
{
m_Target = (SplineMesh)target;
m_MeshFilter = m_Target.GetComponent<MeshFilter>();
if(m_MeshFilter == null)
m_MeshFilter = m_Target.gameObject.AddComponent<MeshFilter>();
m_MeshRenderer = m_Target.GetComponent<MeshRenderer>();
if(m_MeshRenderer == null)
m_MeshRenderer = m_Target.gameObject.AddComponent<MeshRenderer>();
m_Mesh = m_MeshFilter.sharedMesh;
if(m_Mesh == null)
CreateMesh();
m_MeshCollider = m_Target.GetComponent<MeshCollider>();
m_NodesProperty = serializedObject.FindProperty("m_Nodes");
m_HeightProperty = serializedObject.FindProperty("m_Height");
m_DepthProperty = serializedObject.FindProperty("m_Depth");
m_BaselineOffsetProperty = serializedObject.FindProperty("m_BaselineOffset");
m_DivisionsProperty = serializedObject.FindProperty("m_Divisions");
m_UVTilingProperty = serializedObject.FindProperty("m_UVTiling");
m_UVOffsetProperty = serializedObject.FindProperty("m_UVOffset");
m_LengthAffectsUProperty = serializedObject.FindProperty("m_LengthAffectsU");
RegenerateMesh();
}
public override void OnInspectorGUI()
{
// base.OnInspectorGUI();
serializedObject.Update();
if(m_Mesh == null)
{
GUILayout.Label(string.Format("This GameObject's MeshFilter already has a Mesh object named {0}.", m_MeshFilter.sharedMesh.name), "Box");
if(GUILayout.Button("Replace This Mesh")) // FIXME: What if we want to update an existing asset on disk?
CreateMesh();
return;
}
if(m_MeshCollider != null)
GUI.enabled = false;
if(GUILayout.Button("Create MeshCollider"))
m_MeshCollider = m_Target.gameObject.AddComponent<MeshCollider>();
if(GUILayout.Button("Create Fill Mesh"))
CreateFillMesh();
GUI.enabled = true;
m_DivisionsProperty.intValue = Mathf.Clamp(EditorGUILayout.IntField("Divisions", m_DivisionsProperty.intValue), 0, 16);
m_HeightProperty.floatValue = EditorGUILayout.FloatField("Height", m_HeightProperty.floatValue);
m_DepthProperty.floatValue = EditorGUILayout.FloatField("Depth", m_DepthProperty.floatValue);
m_BaselineOffsetProperty.floatValue = EditorGUILayout.FloatField("Baseline Offset", m_BaselineOffsetProperty.floatValue);
// FIXME: Bad things happen if you set the tiling to zero.
m_UVTilingProperty.vector2Value = CompactVector2Field("UV Tiling", m_UVTilingProperty.vector2Value);
m_UVOffsetProperty.vector2Value = CompactVector2Field("UV Offset", m_UVOffsetProperty.vector2Value);
m_LengthAffectsUProperty.boolValue = EditorGUILayout.Toggle("Length Affects U", m_LengthAffectsUProperty.boolValue);
// Node controls:
EditorGUILayout.BeginVertical("Box");
int numNodes = m_Target.CountNodes;
if(numNodes > 0)
{
EditorGUILayout.LabelField("Node Index", string.Format("{0} of {1}", m_CurrentNode + 1, numNodes));
EditorGUILayout.LabelField("Vertex Count", string.Format("{0}", m_Mesh.vertexCount));
var node = m_Target.GetNode(m_CurrentNode);
node.position = CompactVector2Field("Position", node.position);
EditorGUILayout.BeginHorizontal();
{
GUI.enabled = (m_CurrentNode > 0);
EditorGUILayout.PrefixLabel("Insert Node");
if(GUILayout.Button("Before"))
InsertNodeBefore(m_CurrentNode);
GUI.enabled = (m_CurrentNode > 0 && m_CurrentNode < m_Target.CountNodes - 1);
if(GUILayout.Button("Divide"))
DivideNode(m_CurrentNode);
GUI.enabled = (m_CurrentNode < m_Target.CountNodes - 1);
if(GUILayout.Button("After"))
InsertNodeAfter(m_CurrentNode);
GUI.enabled = true;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
{
GUI.enabled = (m_Target.CountNodes > 2); // Can't have less than two nodes
EditorGUILayout.PrefixLabel(" ");
if(GUILayout.Button("Delete Node"))
DeleteNode(m_CurrentNode);
GUI.enabled = true;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
if(serializedObject.ApplyModifiedProperties() || GUI.changed ||
(Event.current.type == EventType.ValidateCommand && Event.current.commandName == "UndoRedoPerformed"))
{
EditorUtility.SetDirty(target);
RegenerateMesh();
}
}
protected void OnSceneGUI()
{
serializedObject.Update();
if(m_Target.Nodes == null)
return;
Undo.SetSnapshotTarget(m_Target, "Move Nodes");
Handles.matrix = m_Target.transform.localToWorldMatrix;
// Handles.DrawPolyLine(m_Target.Nodes.Select(node => node.position = Handles.PositionHandle(node.position, m_Target.transform.rotation)).ToArray());
int vertexIndex = 0;
int nodeIndex = 0;
var segments = m_Target.Segments;
var previousSegment = segments.First();
HandleNodeSlider(previousSegment.start, ref nodeIndex);
// Handles.Label(previousSegment.start.position, (vertexIndex++).ToString());
// SplineMesh.Segment offsetSegment = OffsetSegment(previousSegment, previousSegment.Normal * m_Target.Height);
// Vector3 previousIntersection = offsetSegment.start.position;
// Handles.Label(previousIntersection, (vertexIndex++).ToString());
DrawTopLines(previousSegment, ref vertexIndex, ref nodeIndex);
// bool flip = false;
foreach(var segment in segments.Skip(1))
{
// Offset line:
// float difference = SegmentAngle(segment, false) - SegmentAngle(previousSegment, true);
// float lengthOfExtension = LengthOfExtension(difference >= 0.0f ? difference : difference + 360.0f);
// Vector3 direction = previousSegment.Direction;
// Vector3 intersection = previousSegment.start.position - previousSegment.Normal * m_Target.Height + direction + direction.normalized * lengthOfExtension;
// Handles.color = Color.white;
// Handles.DrawSolidDisc(intersection, Vector3.forward, 0.1f);
// Handles.Label(intersection, (vertexIndex++).ToString());
// Handles.color = (flip ? Color.blue : Color.cyan); flip = !flip;
// Handles.DrawLine(previousIntersection, intersection);
DrawTopLines(segment, ref vertexIndex, ref nodeIndex);
previousSegment = segment;
// previousIntersection = intersection;
}
// Vector3 endPoint = previousSegment.end.position - previousSegment.Normal * m_Target.Height;
// Handles.color = (flip ? Color.blue : Color.cyan); flip = !flip;
// Handles.DrawLine(previousIntersection, endPoint);
// Handles.Label(endPoint, (vertexIndex++).ToString());
if(GUI.changed)
{
RegenerateMesh();
EditorUtility.SetDirty(m_Target);
EditorUtility.SetDirty(m_Mesh);
}
}
#endregion
#region Mesh generation
protected void CreateMesh()
{
m_Mesh = new Mesh();
m_Mesh.name = string.Format("SplineMesh {0}", m_Mesh.GetInstanceID());
m_MeshFilter.sharedMesh = m_Mesh;
}
protected void RegenerateMesh()
{
if(m_Target.Nodes == null)
return;
float height = m_HeightProperty.floatValue;
Vector3 depth = Vector3.forward * m_DepthProperty.floatValue;
float baselineOffset = m_BaselineOffsetProperty.floatValue;
Vector2 uvTiling = m_UVTilingProperty.vector2Value;
Vector2 uvOffset = m_UVOffsetProperty.vector2Value;
bool lengthAffectsU = m_LengthAffectsUProperty.boolValue;
// var meshData = new MeshData(m_Target.CountNodes * 2);
var meshData = new MeshData(m_Target.CountSplineDivisions * 2);
int vertexIndex = 0;
float u = uvOffset.x;
float bottomV = uvOffset.y;
float topV = uvOffset.y + uvTiling.y;
// var segments = m_Target.Segments;
var segments = m_Target.SplineSegments;
var previousSegment = segments.First();
Vector3 minIntersection = OffsetSegment(previousSegment, previousSegment.Normal * baselineOffset).start.position;
Vector3 maxIntersection = OffsetSegment(previousSegment, previousSegment.Normal * (height + baselineOffset)).start.position;
meshData.PushVertex(vertexIndex++, minIntersection, -Vector3.forward, new Vector2(u, topV)); // First vertex on baseline
meshData.PushVertex(vertexIndex++, maxIntersection + depth, -Vector3.forward, new Vector2(u, bottomV)); // First offset vertex
u += (lengthAffectsU ? uvTiling.x * previousSegment.Direction.magnitude : uvTiling.x);
if(m_Target.CountNodes > 2)
{
foreach(var segment in segments.Skip(1))
{
float difference = SegmentAngle(segment, false) - SegmentAngle(previousSegment, true);
Vector3 direction = previousSegment.Direction;
Vector3 extendedDirection = direction + direction.normalized * LengthOfExtension(difference, baselineOffset);
minIntersection = previousSegment.start.position - previousSegment.Normal * baselineOffset + extendedDirection;
extendedDirection = direction + direction.normalized * LengthOfExtension(difference, height + baselineOffset);
maxIntersection = previousSegment.start.position - previousSegment.Normal * (height + baselineOffset) + extendedDirection;
meshData.PushVertex(vertexIndex++, minIntersection, -Vector3.forward, new Vector2(u, topV)); // Baseline
meshData.PushVertex(vertexIndex++, maxIntersection + depth, -Vector3.forward, new Vector2(u, bottomV)); // Offset line
u += (lengthAffectsU ? uvTiling.x * segment.Direction.magnitude : uvTiling.x);
previousSegment = segment;
}
}
Vector3 minEndPoint = previousSegment.end.position - previousSegment.Normal * baselineOffset;
Vector3 maxEndPoint = previousSegment.end.position - previousSegment.Normal * (height + baselineOffset);
meshData.PushVertex(vertexIndex++, minEndPoint, -Vector3.forward, new Vector2(u, topV)); // Last baseline vertex
meshData.PushVertex(vertexIndex++, maxEndPoint + depth, -Vector3.forward, new Vector2(u, bottomV)); // Last offset vertex
UpdateMeshObject(meshData);
}
protected void UpdateMeshObject(MeshData meshData)
{
m_Mesh.vertices = meshData.vertices;
m_Mesh.normals = meshData.normals;
m_Mesh.uv = meshData.uvs;
int nodeCount = m_Target.CountSplineDivisions;
int vertexIndex = 0;
var triangles = new int[(nodeCount - 1) * 6]; // FIXME: Prevent cases where there are less than two nodes and thus less than four vertices.
for(int i = 0; i < (nodeCount - 1) * 6; i += 6)
{
// FIXME: Figure out why I apparently have to use clockwise winding order to make things appear from the right side.
triangles[i] = vertexIndex;
triangles[i + 1] = vertexIndex + 2;
triangles[i + 2] = vertexIndex + 1;
triangles[i + 3] = vertexIndex + 1;
triangles[i + 4] = vertexIndex + 2;
triangles[i + 5] = vertexIndex + 3;
vertexIndex += 2;
}
// string dump = "";
// foreach(var index in triangles)
// dump += index + ", ";
// Debug.Log(string.Format("{0}: {1}", triangles.Length, dump));
m_Mesh.triangles = triangles;
}
protected void CreateFillMesh()
{
if(m_Target.Nodes == null)
return;
var vertices = m_Target.SplineSegments.Select((segment, Vector3) => segment.start.position);
var triangulator = new Triangulator(vertices.ToArray());
var indices = triangulator.Triangulate();
var mesh = new Mesh();
mesh.name = string.Format("FillMesh {0}", m_Mesh.GetInstanceID());
mesh.vertices = vertices.ToArray();
mesh.triangles = indices;
mesh.RecalculateBounds();
mesh.RecalculateNormals();
var meshObject = new GameObject("FillMesh");
meshObject.AddComponent<MeshFilter>().sharedMesh = mesh;
meshObject.AddComponent<MeshRenderer>().sharedMaterial = m_MeshRenderer.sharedMaterial;
}
#endregion
#region Tools
protected void InsertNodeBefore(int nodeIndex)
{
var nodes = m_Target.Nodes.ToArray();
Vector3 position = nodes[nodeIndex].position;
Vector3 backwards = nodes[nodeIndex - 1].position - position;
m_NodesProperty.InsertArrayElementAtIndex(nodeIndex);
SetNodePosition(nodeIndex, position + backwards * 0.5f);
ApplyAndRegenerate();
}
protected void InsertNodeAfter(int nodeIndex)
{
var nodes = m_Target.Nodes.ToArray();
Vector3 position = nodes[nodeIndex].position;
Vector3 forwards = nodes[nodeIndex + 1].position - position;
m_NodesProperty.InsertArrayElementAtIndex(nodeIndex);
SetNodePosition(nodeIndex + 1, position + forwards * 0.5f);
ApplyAndRegenerate();
}
protected void DivideNode(int nodeIndex)
{
// Precondition: don't give the index of one of the end nodes.
var nodes = m_Target.Nodes.ToArray();
Vector3 position = nodes[nodeIndex].position;
Vector3 backwards = nodes[nodeIndex - 1].position - position;
Vector3 forwards = nodes[nodeIndex + 1].position - position;
m_NodesProperty.InsertArrayElementAtIndex(nodeIndex);
SetNodePosition(nodeIndex, position + backwards * 0.25f);
SetNodePosition(nodeIndex + 1, position + forwards * 0.25f);
ApplyAndRegenerate();
}
protected void DeleteNode(int nodeIndex)
{
m_NodesProperty.DeleteArrayElementAtIndex(nodeIndex);
ApplyAndRegenerate();
}
protected SerializedProperty GetNodeProperty(int nodeIndex)
{
return m_NodesProperty.GetArrayElementAtIndex(nodeIndex);
}
protected void SetNodePosition(int nodeIndex, Vector3 position)
{
GetNodeProperty(nodeIndex).FindPropertyRelative("position").vector3Value = position;
}
protected void ApplyAndRegenerate()
{
serializedObject.ApplyModifiedProperties();
RegenerateMesh();
Repaint();
}
#endregion
#region GUI elements
protected void DrawTopLines(SplineMesh.Segment segment, ref int vertexIndex, ref int nodeIndex)
{
HandleNodeSlider(segment.end, ref nodeIndex);
// Handles.Label(segment.end.position, (vertexIndex++).ToString());
// Front line:
Handles.color = Color.green;
Handles.DrawLine(segment.start.position, segment.end.position);
// Rear line:
// Vector3 depthOffset = Vector3.forward * m_Target.Depth;
// Handles.color = Color.grey;
// Handles.DrawLine(segment.start.position + depthOffset, segment.end.position + depthOffset);
}
protected void HandleNodeSlider(SplineMesh.Node node, ref int nodeIndex)
{
int hotControl = GUIUtility.hotControl;
float size = HandleUtility.GetHandleSize(node.position);
Handles.color = Color.white;
node.position = Handles.Slider2D(node.position, Vector3.forward, Vector3.right, Vector3.up, size * 0.1f, Handles.RectangleCap, 0.0f);
int newHotControl = GUIUtility.hotControl;
if(hotControl != newHotControl && newHotControl != 0)
{
m_CurrentNode = nodeIndex;
Repaint();
}
if(m_CurrentNode == nodeIndex)
Handles.DrawWireDisc(node.position, Vector3.forward, size * 0.2f);
nodeIndex++;
}
protected Vector2 CompactVector2Field(string label, Vector2 vector)
{
EditorGUILayout.BeginHorizontal();
{
EditorGUILayout.PrefixLabel(label);
vector.x = EditorGUILayout.FloatField(vector.x);
vector.y = EditorGUILayout.FloatField(vector.y);
}
EditorGUILayout.EndHorizontal();
return vector;
}
#endregion
#region Arithmetic
protected SplineMesh.Segment OffsetSegment(SplineMesh.Segment segment, Vector3 direction)
{
// Vector3 offset = Vector3.forward * m_BaselineOffsetProperty.floatValue;
var start = new SplineMesh.Node(segment.start.position - direction);
var end = new SplineMesh.Node(segment.end.position - direction);
return new SplineMesh.Segment(start, end);
}
protected float SegmentAngle(SplineMesh.Segment segment, bool reversed)
{
Vector3 direction = (reversed ? segment.start.position - segment.end.position : segment.end.position - segment.start.position);
return Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
}
protected float LengthOfExtension(float angleBetweenSegments, float height)
{
float halfPerpendicularAngle = (angleBetweenSegments * Mathf.Deg2Rad - Mathf.PI) * 0.5f;
return Mathf.Tan(halfPerpendicularAngle) * height;
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment