Last active
January 22, 2021 02:13
-
-
Save NoelFB/7b6ae9b875be291a848e580e667706ca to your computer and use it in GitHub Desktop.
Terrain Thingy
This file contains 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 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; | |
} | |
} |
This file contains 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
// 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