Created
May 14, 2012 23:22
-
-
Save N-Carter/2698046 to your computer and use it in GitHub Desktop.
Custom inspector for SplineMesh
This file contains hidden or 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 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