Skip to content

Instantly share code, notes, and snippets.

@ElementalSpork
Forked from phosphoer/SimpleGeo.cs
Created July 14, 2016 03:51
Show Gist options
  • Save ElementalSpork/cc77b3ae94d8e201ddd7da505f5b5022 to your computer and use it in GitHub Desktop.
Save ElementalSpork/cc77b3ae94d8e201ddd7da505f5b5022 to your computer and use it in GitHub Desktop.
Simple Geometry Painter for Unity
// David Evans @phosphoer
// Feel free to use this in your commercial projects
// Let me know if you do cause it will make me feel good and stuff
using UnityEngine;
using System.Collections.Generic;
using System;
#if UNITY_EDITOR
using UnityEditor;
#endif
[ExecuteInEditMode]
public class SimpleGeo : MonoBehaviour
{
public float Height = 0.25f;
public float Resolution = 10.0f;
public float BevelRadius = 0.1f;
public List<TilePos> TileMap = new List<TilePos>();
private Mesh mesh;
private List<Vector3> hull;
[Serializable]
public struct TilePos
{
public int x;
public int y;
}
public void AddPoint(Vector3 point)
{
point = transform.InverseTransformPoint(point);
var x = Mathf.RoundToInt(point.x * Resolution);
var y = Mathf.RoundToInt(point.z * Resolution);
TilePos pos;
pos.x = x;
pos.y = y;
if (!TileMap.Contains(pos))
TileMap.Add(pos);
RebuildMesh();
}
public void RemovePoint(Vector3 point)
{
point = transform.InverseTransformPoint(point);
for (var i = 0; i < TileMap.Count; ++i)
{
var tilePos = TileMap[i];
var p = new Vector3(tilePos.x / Resolution, 0, tilePos.y / Resolution);
if (Vector3.Distance(p, point) < 0.5f)
TileMap.RemoveAt(i);
}
RebuildMesh();
if (hull.Count < 3)
{
DestroyImmediate(gameObject);
}
}
private void SideLoop(List<Vector3> verts, Vector3 center, float bottom, float top, float bottomInset, float topInset)
{
center.y = 0;
Vector3 a, b, aToCenter, bToCenter;
// Loop sides
for (var i = 0; i < hull.Count - 1; ++i)
{
a = hull[i];
b = hull[i + 1];
aToCenter = (center - a).normalized;
bToCenter = (center - b).normalized;
verts.Add(a + Vector3.up * bottom + aToCenter * bottomInset);
verts.Add(a + Vector3.up * top + aToCenter * topInset);
verts.Add(b + Vector3.up * bottom + bToCenter * bottomInset);
verts.Add(b + Vector3.up * bottom + bToCenter * bottomInset);
verts.Add(a + Vector3.up * top + aToCenter * topInset);
verts.Add(b + Vector3.up * top + bToCenter * topInset);
}
// Connect end
a = hull[hull.Count - 1];
b = hull[0];
aToCenter = (center - a).normalized;
bToCenter = (center - b).normalized;
verts.Add(a + Vector3.up * bottom + aToCenter * bottomInset);
verts.Add(a + Vector3.up * top + aToCenter * topInset);
verts.Add(b + Vector3.up * bottom + bToCenter * bottomInset);
verts.Add(b + Vector3.up * bottom + bToCenter * bottomInset);
verts.Add(a + Vector3.up * top + aToCenter * topInset);
verts.Add(b + Vector3.up * top + bToCenter * topInset);
}
public void RebuildMesh()
{
if (mesh == null)
mesh = new Mesh();
mesh.Clear();
// Convex hull the points
//
var points = new List<Vector3>();
foreach (var tilePos in TileMap)
points.Add(new Vector3(tilePos.x / Resolution, 0, tilePos.y / Resolution));
hull = ConvexHull(points);
// Build mesh to extrusion height
//
var verts = new List<Vector3>();
var triangles = new List<int>();
if (hull.Count >= 3)
{
// Find center of hull
var center = Vector3.zero;
for (var i = 0; i < hull.Count; ++i)
center += hull[i];
center /= hull.Count;
center.x = Mathf.Round(center.x * Resolution) / Resolution;
center.z = Mathf.Round(center.z * Resolution) / Resolution;
// Loop sides
SideLoop(verts, center, 0, Height - BevelRadius, 0, 0);
if (BevelRadius != 0)
SideLoop(verts, center, Height - BevelRadius, Height, 0, BevelRadius);
// Fan top
Vector3 a, b, aToCenter, bToCenter;
for (var i = 0; i < hull.Count - 1; ++i)
{
a = hull[i];
b = hull[i + 1];
aToCenter = (center - a).normalized;
bToCenter = (center - b).normalized;
verts.Add(center + Vector3.up * Height);
verts.Add(b + Vector3.up * Height + bToCenter * BevelRadius);
verts.Add(a + Vector3.up * Height + aToCenter * BevelRadius);
}
// Connect end
a = hull[0];
b = hull[hull.Count - 1];
aToCenter = (center - a).normalized;
bToCenter = (center - b).normalized;
verts.Add(center + Vector3.up * Height);
verts.Add(a + Vector3.up * Height + aToCenter * BevelRadius);
verts.Add(b + Vector3.up * Height + bToCenter * BevelRadius);
}
// Triangles for each 3 vertices
for (var i = 0; i < verts.Count; ++i)
triangles.Add(i);
// Update mesh
mesh.SetVertices(verts);
mesh.SetTriangles(triangles, 0);
mesh.RecalculateNormals();
mesh.RecalculateBounds();
mesh.name = gameObject.name;
var meshFilter = GetComponent<MeshFilter>();
if (meshFilter == null)
meshFilter = gameObject.AddComponent<MeshFilter>();
meshFilter.mesh = mesh;
var meshCollider = GetComponent<MeshCollider>();
if (meshCollider == null)
meshCollider = gameObject.AddComponent<MeshCollider>();
meshCollider.sharedMesh = mesh;
meshCollider.convex = true;
var r = gameObject.GetComponent<MeshRenderer>();
if (r == null)
{
r = gameObject.AddComponent<MeshRenderer>();
}
}
private void Start()
{
RebuildMesh();
}
private void OnDrawGizmos()
{
#if UNITY_EDITOR
if (!SimpleGeoEditor.EditorActive || Selection.activeGameObject == null)
return;
foreach (var tilePos in TileMap)
{
var pos = new Vector3(tilePos.x / Resolution, 0, tilePos.y / Resolution);
Gizmos.DrawSphere(transform.TransformPoint(pos), 1.0f / (Resolution * 5));
}
if (hull != null && hull.Count > 1)
{
for (var i = 0; i < hull.Count; ++i)
{
Gizmos.color = Color.cyan;
if (i > 0)
Gizmos.DrawLine(transform.TransformPoint(hull[i - 1]), transform.TransformPoint(hull[i]));
Gizmos.DrawSphere(transform.TransformPoint(hull[i]), 1.0f / (Resolution * 5));
}
Gizmos.DrawLine(transform.TransformPoint(hull[hull.Count - 1]), transform.TransformPoint(hull[0]));
}
#endif
}
private static int ConvexTurn(Vector3 p, Vector3 q, Vector3 r)
{
var val = (q.x - p.x)*(r.z - p.z) - (r.x - p.x)*(q.z - p.z);
if (val < 0)
return -1;
if (val == 0)
return 0;
return 1;
}
private static float ConvexDistance(Vector3 p, Vector3 q)
{
return Vector3.SqrMagnitude(p - q);
}
private static Vector3 ConvexNextPoint(List<Vector3> points, Vector3 p)
{
var q = p;
foreach (var r in points)
{
var t = ConvexTurn(p, q, r);
if (t == -1 || t == 0 && ConvexDistance(p, r) > ConvexDistance(p, q))
q = r;
}
return q;
}
private static List<Vector3> ConvexHull(List<Vector3> points)
{
var hull = new List<Vector3>();
if (points.Count < 3)
return hull;
var minPoint = points[0];
foreach (var p in points)
{
if (p.x < minPoint.x)
minPoint = p;
else if (Mathf.Abs(p.x - minPoint.x) < Mathf.Epsilon && p.z < minPoint.z)
minPoint = p;
}
var nextHullPoint = minPoint;
hull.Add(nextHullPoint);
do
{
nextHullPoint = ConvexNextPoint(points, nextHullPoint);
if (Vector3.Distance(nextHullPoint, hull[0]) > Mathf.Epsilon)
hull.Add(nextHullPoint);
} while (Vector3.Distance(nextHullPoint, hull[0]) > Mathf.Epsilon && hull.Count < 50);
return hull;
}
}
// David Evans @phosphoer
// Feel free to use this in your commercial projects
// Let me know if you do cause it will make me feel good and stuff
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
[CustomEditor(typeof(SimpleGeo))]
public class SimpleGeoEditor : Editor
{
static public bool EditorActive;
private float lastHeight;
private float lastResolution;
private float lastBevelRadius;
private void Awake()
{
Undo.undoRedoPerformed += OnUndo;
}
private void OnUndo()
{
var terrains = FindObjectsOfType<SimpleGeo>();
for (var i = 0; i < terrains.Length; ++i)
{
terrains[i].RebuildMesh();
}
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (!EditorActive && GUILayout.Button("Start editing"))
{
EditorActive = true;
SceneView.RepaintAll();
}
if (EditorActive && GUILayout.Button("Stop editing"))
{
EditorActive = false;
SceneView.RepaintAll();
}
if (EditorActive)
{
GUILayout.Label("LMB + Drag - Paint geometry");
GUILayout.Label("RMB + Drag - Erase geometry");
GUILayout.Label("LMB + Shift + Drag - Raise/lower geometry");
GUILayout.Label("Control - Select geometry under mouse");
}
}
private void OnSceneGUI()
{
var geoItem = target as SimpleGeo;
// Update terrain when properties change
if (lastResolution != geoItem.Resolution || lastBevelRadius != geoItem.BevelRadius || lastHeight != geoItem.Height)
{
geoItem.RebuildMesh();
lastResolution = geoItem.Resolution;
lastHeight = geoItem.Height;
lastBevelRadius = geoItem.BevelRadius;
}
// Don't do anything if editor isn't active
if (!EditorActive)
return;
// Boilerplate for preventing default events
var controlID = GUIUtility.GetControlID(FocusType.Passive);
if (Event.current.type == EventType.Layout)
{
HandleUtility.AddDefaultControl(controlID);
}
// Raycast to edit plane
var mouseRay = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
var plane = new Plane(geoItem.transform.up, geoItem.transform.position);
var hitEnter = 0.0f;
Vector3 hitPoint = Vector3.zero;
var raycastHitPlane = plane.Raycast(mouseRay, out hitEnter);
if (raycastHitPlane)
{
hitPoint = mouseRay.origin + mouseRay.direction * hitEnter;
}
// Draw 3D GUI
if (raycastHitPlane)
{
var c = Color.white;
c.a = 0.25f;
Handles.color = c;
var scale = Mathf.Max(geoItem.transform.localScale.x, geoItem.transform.localScale.z) / geoItem.Resolution;
Handles.DrawSolidDisc(hitPoint, Vector3.up, scale);
}
// Referesh view on mouse move
if (Event.current.type == EventType.MouseMove)
{
SceneView.RepaintAll();
}
// Press escape to stop editing
if (Event.current.keyCode == KeyCode.Escape)
{
EditorActive = false;
SceneView.RepaintAll();
Repaint();
return;
}
// Press control to select existing terrain
if (Event.current.type == EventType.KeyDown && Event.current.control)
{
// Raycast to select an existing terrain
RaycastHit hitInfo;
if (Physics.Raycast(mouseRay, out hitInfo))
{
var t = hitInfo.collider.GetComponent<SimpleGeo>();
if (t != null)
{
Selection.activeGameObject = t.gameObject;
}
}
}
// Control click to start new terrain
if (Event.current.type == EventType.MouseDown && Event.current.control && Event.current.button == 0)
{
// Raycast to start new geo at hit point
RaycastHit hitInfo;
if (Physics.Raycast(mouseRay, out hitInfo))
{
var geoPrefab = PrefabUtility.GetPrefabParent(Selection.activeGameObject);
var newGeo = geoPrefab != null ? PrefabUtility.InstantiatePrefab(geoPrefab) as GameObject : Instantiate(geoItem.gameObject);
newGeo.transform.position = hitInfo.point;
newGeo.transform.localRotation = Quaternion.identity;
newGeo.transform.up = hitInfo.normal;
newGeo.GetComponent<SimpleGeo>().TileMap.Clear();
newGeo.GetComponent<SimpleGeo>().RebuildMesh();
Selection.activeGameObject = newGeo.gameObject;
Undo.RegisterCreatedObjectUndo(newGeo, "New SimpleGeo");
}
}
// Click and drag handler
if (Event.current.type == EventType.MouseDrag && !Event.current.control)
{
// Do nothing on camera orbit
if (Event.current.alt)
return;
// Consume the event so the rest of the editor ignores it
Event.current.Use();
if (geoItem != null)
Undo.RecordObject(geoItem, "SimpleGeo Edit");
// Raycast the mouse drag
if (raycastHitPlane)
{
// Manipulation during terrain creation
if (Event.current.button == 0)
{
// Hold shift to resize terrain height
if (Event.current.shift)
{
geoItem.Height += Event.current.delta.y * -0.03f;
geoItem.RebuildMesh();
}
// Draw on the terrain
else
{
geoItem.AddPoint(hitPoint);
}
}
// Right click to erase terrain
else if (Event.current.button == 1)
{
geoItem.RemovePoint(hitPoint);
}
}
}
// End the terrain on mouse release
if (Event.current.type == EventType.MouseUp)
{
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment