Skip to content

Instantly share code, notes, and snippets.

@NoelFB
Last active January 22, 2021 02:13
Show Gist options
  • Save NoelFB/7b6ae9b875be291a848e580e667706ca to your computer and use it in GitHub Desktop.
Save NoelFB/7b6ae9b875be291a848e580e667706ca to your computer and use it in GitHub Desktop.
Terrain Thingy
using UnityEngine;
using System.Collections;
using UnityEditor;
[CustomEditor(typeof(TerrainManager))]
public class TerrainEditor : Editor
{
private TerrainManager terrain { get { return (TerrainManager)target; } }
private Vector2Int? selected;
private Vector2Int? hover;
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (GUILayout.Button("Rebuild Mesh"))
terrain.Generate();
}
protected virtual void OnSceneGUI()
{
var e = Event.current;
var invokeRepaint = false;
Handles.color = Color.blue;
// resize
{
int right = CheckResize(terrain.RightHandle, Vector3.right);
int left = CheckResize(terrain.LeftHandle, Vector3.left);
int forward = CheckResize(terrain.ForwardHandle, Vector3.forward);
int back = CheckResize(terrain.BackHandle, Vector3.back);
if (left != 0 || right != 0 || forward != 0 || back != 0)
terrain.Resize(left, right, forward, back);
}
// drag a surface
var dragging = false;
if (selected != null)
{
EditorGUI.BeginChangeCheck();
var node = terrain.PositionAt(selected.Value.x, selected.Value.y);
var pulled = Handles.Slider(node, Vector2.up);
if (EditorGUI.EndChangeCheck())
{
dragging = true;
hover = null;
var last = terrain.transform.position.y - node.y;
var next = terrain.transform.position.y - pulled.y;
var diff = (next - last) / terrain.Unit;
if (Mathf.Abs(diff) >= 1f)
{
terrain.Map[selected.Value.x + selected.Value.y * terrain.columns].Height -= (int)diff;
terrain.Generate();
}
}
}
// hover
if (!dragging && e.type == EventType.MouseMove)
{
var was = hover;
hover = OverSurfaceTile(e.mousePosition);
if (was.HasValue != hover.HasValue || (was.HasValue && hover.HasValue && was.Value != hover.Value))
invokeRepaint = true;
}
// click
if (!dragging && e.button == 0 && e.type == EventType.MouseDown)
selected = OverSurfaceTile(e.mousePosition);
// draw hovers / selected outlines
if (hover != null)
DrawSurfaceOutline(hover.Value, Color.magenta);
if (selected != null)
DrawSurfaceOutline(selected.Value, Color.blue);
// force repaint
if (invokeRepaint)
Repaint();
}
private void DrawSurfaceOutline(Vector2Int tile, Color color)
{
var center = terrain.PositionAt(tile.x, tile.y);
var a = center + Vector3.forward * terrain.HUnit + Vector3.left * terrain.HUnit;
var b = center + Vector3.forward * terrain.HUnit + Vector3.right * terrain.HUnit;
var c = center + Vector3.back * terrain.HUnit + Vector3.right * terrain.HUnit;
var d = center + Vector3.back * terrain.HUnit + Vector3.left * terrain.HUnit;
Handles.color = color;
Handles.DrawDottedLine(a, b, 2f);
Handles.DrawDottedLine(b, c, 2f);
Handles.DrawDottedLine(c, d, 2f);
Handles.DrawDottedLine(d, a, 2f);
}
private Vector2Int? OverSurfaceTile(Vector2 mousePosition)
{
var ray = HandleUtility.GUIPointToWorldRay(mousePosition);
var hits = Physics.RaycastAll(ray);
foreach (var hit in hits)
{
if (Vector3.Dot(Vector3.up, hit.normal) > 0.8f)
{
var manager = hit.collider.gameObject.GetComponent<TerrainManager>();
if (manager == terrain)
{
var x = (int)((hit.point.x - terrain.transform.position.x) / terrain.Unit);
var y = (int)((hit.point.z - terrain.transform.position.z) / terrain.Unit);
return new Vector2Int(x, y);
}
}
}
return null;
}
private int CheckResize(Vector3 handle, Vector3 direction)
{
EditorGUI.BeginChangeCheck();
Vector3 pulled = Handles.Slider(handle, direction);
if (EditorGUI.EndChangeCheck())
{
var last = (terrain.Center - handle).magnitude;
var next = (terrain.Center - pulled).magnitude;
var diff = (next - last) / terrain.Unit;
if (Mathf.Abs(diff) >= 1f)
return (int)diff;
}
return 0;
}
}
// visual reference: https://twitter.com/NoelFB/status/978706478793080832
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshCollider))]
[ExecuteInEditMode]
public class TerrainManager : MonoBehaviour
{
[System.Serializable]
public struct Tile
{
public static readonly Tile None = new Tile() { Height = 0 };
public int Height;
}
// values to edit
public float Unit = 1.0f;
public float VertexNoise = 0.05f;
public float HUnit { get { return Unit * 0.5f; } }
// map
public Tile[] Map;
[HideInInspector]
public int columns = 8;
[HideInInspector]
public int rows = 8;
// components
[HideInInspector]
public Mesh mesh;
private MeshFilter filter;
private MeshCollider meshCollider;
private MeshRenderer meshRenderer;
// mesh
private Dictionary<Vector3, Vector3> vertexWobble;
private List<Vector3> vertices;
private List<Vector2> uvs;
private List<int> triangles;
private Vector2 textureTile;
public Vector3 Center
{
get { return transform.position + Vector3.forward * rows * HUnit + Vector3.right * columns * HUnit; }
}
public Vector3 RightHandle
{
get { return Center + Vector3.right * columns * HUnit; }
}
public Vector3 LeftHandle
{
get { return Center + Vector3.left * columns * HUnit; }
}
public Vector3 ForwardHandle
{
get { return Center + Vector3.forward * rows * HUnit; }
}
public Vector3 BackHandle
{
get { return Center + Vector3.back * rows * HUnit; }
}
public Vector3 PositionAt(int x, int y)
{
var tile = this[x, y];
return transform.position + Vector3.forward * (y + 0.5f) * Unit + Vector3.right * (x + 0.5f) * Unit + Vector3.up * tile.Height * Unit;
}
void OnEnable()
{
filter = GetComponent<MeshFilter>();
meshCollider = GetComponent<MeshCollider>();
meshRenderer = GetComponent<MeshRenderer>();
Generate();
}
public Tile this[int x, int y]
{
get
{
if (x < 0 || y < 0 || x >= columns || y >= rows)
return Tile.None;
return Map[x + columns * y];
}
set
{
if (x >= 0 && y >= 0 && x < columns && y < rows)
Map[x + columns * y] = value;
}
}
public void Resize(int left, int right, int forward, int back)
{
var lastColumns = columns;
var lastRows = rows;
columns += right + left;
rows += forward + back;
if (Map.GetLength(0) != columns || Map.GetLength(1) != rows)
{
var updated = new Tile[columns * rows];
for (int x = 0, c = Mathf.Min(lastColumns, columns); x < c; x++)
for (int y = 0, r = Mathf.Min(lastRows, rows); y < r; y++)
{
var copy = Tile.None;
var tx = x - left;
var ty = y - back;
if (tx >= 0 && ty >= 0 && tx < c && ty < r)
copy = Map[tx + ty * lastColumns];
updated[x + y * columns] = copy;
}
Map = updated;
}
transform.position += Vector3.left * left + Vector3.back * back;
Generate();
}
public void Generate()
{
// create mesh if it doesn't exist yet
if (mesh == null)
{
mesh = new Mesh();
filter.mesh = mesh;
}
// create map if it doesn't exist yet
if (Map == null)
Map = new Tile[columns * rows];
// get texture tile size
var texture = meshRenderer.sharedMaterial.mainTexture;
textureTile = new Vector2(1f / (texture.width / 16f), 1f / (texture.height / 16f));
// clear arrays
vertexWobble = new Dictionary<Vector3, Vector3>();
vertices = new List<Vector3>();
uvs = new List<Vector2>();
triangles = new List<int>();
// generate sides
for (int x = 0; x < columns; x++)
for (int y = 0; y < rows; y++)
{
var tile = this[x, y];
var ground = new Vector3(x + 0.5f, 0, y + 0.5f) * Unit;
GenerateSide(ground, Vector3.left, tile, this[x - 1, y]);
GenerateSide(ground, Vector3.right, tile, this[x + 1, y]);
GenerateSide(ground, Vector3.back, tile, this[x, y - 1]);
GenerateSide(ground, Vector3.forward, tile, this[x, y + 1]);
GenerateSurface(ground, tile);
}
// update the mesh
mesh.Clear();
mesh.vertices = vertices.ToArray();
mesh.uv = uvs.ToArray();
mesh.triangles = triangles.ToArray();
mesh.RecalculateBounds();
mesh.RecalculateNormals();
meshCollider.sharedMesh = mesh;
}
private void GenerateSide(Vector3 ground, Vector3 forward, Tile current, Tile adjacent)
{
var right = Vector3.Cross(forward.normalized, Vector3.up.normalized) * HUnit;
var left = -right;
var up = Vector3.up * HUnit;
var down = -Vector3.up * HUnit;
for (int i = adjacent.Height; i < current.Height; i++)
{
var origin = ground + Vector3.up * (i + 0.5f) * Unit + forward * HUnit;
GenerateQuad(origin + left + up,
origin + right + up,
origin + right + down,
origin + left + down,
new Vector2Int(1, 0));
}
}
private void GenerateSurface(Vector3 ground, Tile tile)
{
var origin = ground + Vector3.up * tile.Height * Unit;
var left = Vector3.left * HUnit;
var right = Vector3.right * HUnit;
var forward = Vector3.forward * HUnit;
var back = Vector3.back * HUnit;
GenerateQuad(origin + left + forward,
origin + right + forward,
origin + right + back,
origin + left + back,
new Vector2Int(0, 0));
}
private void GenerateQuad(Vector3 a, Vector3 b, Vector3 c, Vector3 d, Vector2Int tile)
{
var start = vertices.Count;
Vector3 wa, wb, wc, wd;
// the wobbly factor could totally be done in a shader but I'm lazy at the moment
float r = VertexNoise;
if (!vertexWobble.TryGetValue(a, out wa))
vertexWobble.Add(a, wa = a + new Vector3(Random.Range(-r, r), Random.Range(-r, r), Random.Range(-r, r)));
if (!vertexWobble.TryGetValue(b, out wb))
vertexWobble.Add(b, wb = b + new Vector3(Random.Range(-r, r), Random.Range(-r, r), Random.Range(-r, r)));
if (!vertexWobble.TryGetValue(c, out wc))
vertexWobble.Add(c, wc = c + new Vector3(Random.Range(-r, r), Random.Range(-r, r), Random.Range(-r, r)));
if (!vertexWobble.TryGetValue(d, out wd))
vertexWobble.Add(d, wd = d + new Vector3(Random.Range(-r, r), Random.Range(-r, r), Random.Range(-r, r)));
vertices.Add(wa);
vertices.Add(wb);
vertices.Add(wc);
vertices.Add(wd);
uvs.Add(new Vector2(tile.x * textureTile.x, tile.y * textureTile.y));
uvs.Add(new Vector2((tile.x + 1) * textureTile.x, tile.y * textureTile.y));
uvs.Add(new Vector2((tile.x + 1) * textureTile.x, (tile.y + 1) * textureTile.y));
uvs.Add(new Vector2(tile.x * textureTile.x, (tile.y + 1) * textureTile.y));
triangles.Add(start + 0);
triangles.Add(start + 1);
triangles.Add(start + 2);
triangles.Add(start + 0);
triangles.Add(start + 2);
triangles.Add(start + 3);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment