using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace Procedural { [ExecuteAlways] [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class LowPolyTriangleField : MonoBehaviour { [Header("Grid")] [Min(1)] public int columns = 24; [Min(1)] public int rows = 14; public float cellSize = 1.5f; [Range(0f, 0.5f)] public float jitterAmount = 0.35f; [Header("Static Shape")] public float noiseScale = 0.12f; public float heightAmplitude = 1.2f; [Header("Wave Animation")] public bool animate = true; public float waveAmplitude = 0.25f; public float waveFrequency = 0.25f; public float waveSpeed = 0.6f; [Header("Edges")] public float edgeWidth = 0.06f; public float edgeHeightOffset = 0.02f; [Header("Materials")] public Material faceMaterial; public Material edgeMaterial; [Header("Random")] public int seed = 12345; Mesh _mesh; MeshFilter _meshFilter; Vector2[] _gridXZ; float[] _staticHeight; Vector3[] _gridNormalAccum; int[] _gridNormalCount; List _triangles; List<(int a, int b)> _uniqueEdges; Vector3[] _facePositions; Vector3[] _faceNormals; int[] _faceIndices; Vector3[] _edgePositions; int[] _edgeIndices; bool _topologyDirty = true; void OnEnable() { _meshFilter = GetComponent(); RebuildTopology(); UpdateGeometry(0f); #if UNITY_EDITOR EditorApplication.update -= EditorTick; if (!Application.isPlaying) EditorApplication.update += EditorTick; #endif } void OnDisable() { #if UNITY_EDITOR EditorApplication.update -= EditorTick; #endif } void OnValidate() { _topologyDirty = true; } #if UNITY_EDITOR void EditorTick() { if (Application.isPlaying) return; if (_topologyDirty) RebuildTopology(); if (animate) UpdateGeometry((float)EditorApplication.timeSinceStartup); if (this != null) EditorUtility.SetDirty(this); } #endif void Update() { if (!Application.isPlaying) return; if (_topologyDirty) RebuildTopology(); if (animate) UpdateGeometry(Time.time); } void RebuildTopology() { _topologyDirty = false; int vertsX = columns + 1; int vertsZ = rows + 1; int gridCount = vertsX * vertsZ; _gridXZ = new Vector2[gridCount]; _staticHeight = new float[gridCount]; var rng = new System.Random(seed); float originX = -columns * cellSize * 0.5f; float originZ = -rows * cellSize * 0.5f; for (int j = 0; j < vertsZ; j++) { for (int i = 0; i < vertsX; i++) { int idx = j * vertsX + i; bool border = i == 0 || i == columns || j == 0 || j == rows; float jitterX = border ? 0f : ((float)rng.NextDouble() * 2f - 1f) * jitterAmount * cellSize; float jitterZ = border ? 0f : ((float)rng.NextDouble() * 2f - 1f) * jitterAmount * cellSize; float x = originX + i * cellSize + jitterX; float z = originZ + j * cellSize + jitterZ; _gridXZ[idx] = new Vector2(x, z); float n = Mathf.PerlinNoise(x * noiseScale + seed * 0.017f, z * noiseScale + seed * 0.017f); _staticHeight[idx] = (n * 2f - 1f) * heightAmplitude; } } _triangles = new List(columns * rows * 2); var edgeSet = new HashSet<(int, int)>(); _uniqueEdges = new List<(int, int)>(); for (int j = 0; j < rows; j++) { for (int i = 0; i < columns; i++) { int i00 = j * vertsX + i; int i10 = j * vertsX + (i + 1); int i01 = (j + 1) * vertsX + i; int i11 = (j + 1) * vertsX + (i + 1); bool flip = rng.Next(2) == 0; if (flip) { AddTriangle(i00, i11, i01, edgeSet); AddTriangle(i00, i10, i11, edgeSet); } else { AddTriangle(i00, i10, i01, edgeSet); AddTriangle(i10, i11, i01, edgeSet); } } } int faceVertCount = _triangles.Count * 3; _facePositions = new Vector3[faceVertCount]; _faceNormals = new Vector3[faceVertCount]; _faceIndices = new int[faceVertCount]; for (int k = 0; k < faceVertCount; k++) _faceIndices[k] = k; int edgeVertCount = _uniqueEdges.Count * 4; _edgePositions = new Vector3[edgeVertCount]; _edgeIndices = new int[_uniqueEdges.Count * 6]; for (int e = 0; e < _uniqueEdges.Count; e++) { int b = e * 4; int t = e * 6; _edgeIndices[t + 0] = b + 0; _edgeIndices[t + 1] = b + 2; _edgeIndices[t + 2] = b + 1; _edgeIndices[t + 3] = b + 1; _edgeIndices[t + 4] = b + 2; _edgeIndices[t + 5] = b + 3; } _gridNormalAccum = new Vector3[gridCount]; _gridNormalCount = new int[gridCount]; if (_mesh == null) { _mesh = new Mesh { name = "LowPolyTriangleField" }; _mesh.MarkDynamic(); } _mesh.indexFormat = (faceVertCount + edgeVertCount) > 60000 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16; _mesh.Clear(); _meshFilter.sharedMesh = _mesh; var renderer = GetComponent(); renderer.sharedMaterials = new[] { faceMaterial, edgeMaterial }; void AddTriangle(int a, int b2, int c, HashSet<(int, int)> edges) { _triangles.Add(new[] { a, b2, c }); AddEdge(a, b2, edges); AddEdge(b2, c, edges); AddEdge(c, a, edges); } void AddEdge(int a, int b2, HashSet<(int, int)> edges) { var key = a < b2 ? (a, b2) : (b2, a); if (edges.Add(key)) _uniqueEdges.Add(key); } } void UpdateGeometry(float time) { if (_gridXZ == null) return; int gridCount = _gridXZ.Length; var pos = new Vector3[gridCount]; for (int idx = 0; idx < gridCount; idx++) { var xz = _gridXZ[idx]; float wave = 0f; if (animate) { wave = waveAmplitude * 0.5f * ( Mathf.Sin((xz.x + xz.y) * waveFrequency + time * waveSpeed) + Mathf.Sin((xz.x - xz.y) * waveFrequency * 0.7f + time * waveSpeed * 1.3f)); } pos[idx] = new Vector3(xz.x, _staticHeight[idx] + wave, xz.y); _gridNormalAccum[idx] = Vector3.zero; _gridNormalCount[idx] = 0; } for (int t = 0; t < _triangles.Count; t++) { var tri = _triangles[t]; Vector3 v0 = pos[tri[0]]; Vector3 v1 = pos[tri[1]]; Vector3 v2 = pos[tri[2]]; Vector3 normal = Vector3.Cross(v1 - v0, v2 - v0).normalized; int b = t * 3; _facePositions[b + 0] = v0; _facePositions[b + 1] = v1; _facePositions[b + 2] = v2; _faceNormals[b + 0] = normal; _faceNormals[b + 1] = normal; _faceNormals[b + 2] = normal; _gridNormalAccum[tri[0]] += normal; _gridNormalCount[tri[0]]++; _gridNormalAccum[tri[1]] += normal; _gridNormalCount[tri[1]]++; _gridNormalAccum[tri[2]] += normal; _gridNormalCount[tri[2]]++; } for (int e = 0; e < _uniqueEdges.Count; e++) { var (a, bIdx) = _uniqueEdges[e]; Vector3 p0 = pos[a]; Vector3 p1 = pos[bIdx]; Vector3 n0 = _gridNormalCount[a] > 0 ? (_gridNormalAccum[a] / _gridNormalCount[a]).normalized : Vector3.up; Vector3 n1 = _gridNormalCount[bIdx] > 0 ? (_gridNormalAccum[bIdx] / _gridNormalCount[bIdx]).normalized : Vector3.up; Vector3 dir = (p1 - p0).normalized; Vector3 avgNormal = (n0 + n1).normalized; Vector3 side = Vector3.Cross(dir, avgNormal).normalized * (edgeWidth * 0.5f); Vector3 p0Off = p0 + n0 * edgeHeightOffset; Vector3 p1Off = p1 + n1 * edgeHeightOffset; int b = e * 4; _edgePositions[b + 0] = p0Off - side; _edgePositions[b + 1] = p0Off + side; _edgePositions[b + 2] = p1Off - side; _edgePositions[b + 3] = p1Off + side; } _mesh.subMeshCount = 2; _mesh.SetVertices(_facePositions.Length > 0 ? Concat(_facePositions, _edgePositions) : _edgePositions); int faceVertCount = _facePositions.Length; var normals = new Vector3[faceVertCount + _edgePositions.Length]; for (int i = 0; i < faceVertCount; i++) normals[i] = _faceNormals[i]; for (int i = 0; i < _edgePositions.Length; i++) normals[faceVertCount + i] = Vector3.up; _mesh.SetNormals(normals); _mesh.SetIndices(_faceIndices, MeshTopology.Triangles, 0, false); var offsetEdgeIndices = new int[_edgeIndices.Length]; for (int i = 0; i < _edgeIndices.Length; i++) offsetEdgeIndices[i] = _edgeIndices[i] + faceVertCount; _mesh.SetIndices(offsetEdgeIndices, MeshTopology.Triangles, 1, false); _mesh.RecalculateBounds(); } static Vector3[] Concat(Vector3[] a, Vector3[] b) { var result = new Vector3[a.Length + b.Length]; a.CopyTo(result, 0); b.CopyTo(result, a.Length); return result; } } }