diff --git a/Demo/Demo-URP.unitypackage b/Demo/Demo-URP.unitypackage index 4e24d10..4aae923 100644 Binary files a/Demo/Demo-URP.unitypackage and b/Demo/Demo-URP.unitypackage differ diff --git a/Images/Sample_01.PNG b/Images/Sample_01.PNG new file mode 100644 index 0000000..5829dd5 Binary files /dev/null and b/Images/Sample_01.PNG differ diff --git a/Images/Sample_01.PNG.meta b/Images/Sample_01.PNG.meta new file mode 100644 index 0000000..9d6c9a8 --- /dev/null +++ b/Images/Sample_01.PNG.meta @@ -0,0 +1,92 @@ +fileFormatVersion: 2 +guid: c5997898c14b56445b53085a70d1c27e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: -1 + wrapV: -1 + wrapW: -1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 9f964a3..aa6f6da 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Library of utilitities for procedural generation #### Using UnityPackageManager (for Unity 2019.3 or later) Open the package manager window (menu: Window > Package Manager)
Select "Add package from git URL...", fill in the pop-up with the following link:
-https://github.com/coryleach/UnityProcgen.git#0.0.2
+https://github.com/coryleach/UnityProcgen.git#0.0.3
#### Using UnityPackageManager (for Unity 2019.1 or later) @@ -21,7 +21,7 @@ Find the manifest.json file in the Packages folder of your project and edit it t ```js { "dependencies": { - "com.gameframe.procgen": "https://github.com/coryleach/UnityProcgen.git#0.0.2", + "com.gameframe.procgen": "https://github.com/coryleach/UnityProcgen.git#0.0.3", ... }, } @@ -30,6 +30,8 @@ Find the manifest.json file in the Packages folder of your project and edit it t ## Sample Output + + ## Usage diff --git a/Runtime/Utility/HexMeshUtility.cs b/Runtime/Utility/HexMeshUtility.cs new file mode 100644 index 0000000..1c5860f --- /dev/null +++ b/Runtime/Utility/HexMeshUtility.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Gameframe.Procgen +{ + public enum HexDirection + { + NE = 0, + E = 1, + SE = 2, + SW = 3, + W = 4, + NW = 5 + } + + public class HexMeshData + { + public float outerRadius; + public float innerRadius; + public List vertices; + public List triangles; + public List colors; + public List uv; + public Vector3[] corners; + + private float border = 0.25f; + + public float Border => border; + public float Solid => 1 - border; + + public HexMeshData() + { + } + + public HexMeshData(float radius, float border = 0.2f) + { + this.border = Mathf.Clamp01(border); + + outerRadius = radius; + innerRadius = outerRadius * Mathf.Sqrt(3f) * 0.5f; + vertices = new List(); + triangles = new List(); + colors = new List(); + uv = new List(); + corners = new [] { + new Vector3(0f, 0f, outerRadius), + new Vector3(innerRadius, 0f, 0.5f * outerRadius), + new Vector3(innerRadius, 0f, -0.5f * outerRadius), + new Vector3(0f, 0f, -outerRadius), + new Vector3(-innerRadius, 0f, -0.5f * outerRadius), + new Vector3(-innerRadius, 0f, 0.5f * outerRadius), + new Vector3(0f, 0f, outerRadius) + }; + } + + public Vector3 GetBridge(HexDirection direction) + { + return (corners[(int)direction] + corners[(int)direction + 1]) * 0.5f * border; + } + + public Vector3 GetFirstCorner(HexDirection direction) + { + return corners[(int) direction]; + } + + public Vector3 GetSecondCorner(HexDirection direction) + { + return corners[(int) direction + 1]; + } + + public Vector3 GetFirstSolidCorner(HexDirection direction) + { + return corners[(int) direction] * Solid; + } + + public Vector3 GetSecondSolidCorner(HexDirection direction) + { + return corners[(int) direction + 1] * Solid; + } + + public Mesh CreateMesh() + { + Mesh mesh = new Mesh(); + mesh.vertices = vertices.ToArray(); + mesh.triangles = triangles.ToArray(); + mesh.colors = colors.ToArray(); + mesh.uv = uv.ToArray(); + mesh.RecalculateNormals(); + return mesh; + } + + } + + public static class HexMeshUtility + { + public static HexDirection Previous(this HexDirection direction) + { + return direction == HexDirection.NE ? HexDirection.NW : (direction - 1); + } + + public static HexDirection Next(this HexDirection direction) + { + return direction == HexDirection.NW ? HexDirection.NE : (direction + 1); + } + + public static int GetNeighbor(int index, HexDirection hexDirection, int mapWidth, int mapHeight) + { + int y = index / mapWidth; + int x = index - (y * mapWidth); + + switch (hexDirection) + { + case HexDirection.NE: + //Only odd numbered rows shift a column + if ((y & 1) == 1) + { + x++; + } + y++; + break; + case HexDirection.E: + x++; + break; + case HexDirection.SE: + //Only odd numbered rows shift a column + if ((y & 1) == 1) + { + x++; + } + y--; + break; + case HexDirection.SW: + //Only even numbered rows shift a column + if ((y & 1) == 0) + { + x--; + } + y--; + break; + case HexDirection.W: + x--; + break; + case HexDirection.NW: + //Only even numbered rows shift a column + if ((y & 1) == 0) + { + x--; + } + y++; + break; + default: + throw new ArgumentOutOfRangeException(nameof(hexDirection), hexDirection, null); + } + + if (x < 0 || x >= mapWidth) + { + return -1; + } + + if (y < 0 || y >= mapHeight) + { + return -1; + } + + return y * mapWidth + x; + } + + public static Mesh GenerateHexagonMesh(float radius, float border, int startX, int startY, int chunkWidth, int chunkHeight, int mapWidth, int mapHeight, float[] heightMap, Func colorFunction, Func elevationFunction) + { + var meshData = new HexMeshData(radius, border); + + for (int dy = 0; dy < chunkHeight && (startY + dy) < mapHeight; dy++) + { + for (int dx = 0; dx < chunkWidth && (startX + dx) < mapWidth; dx++) + { + int x = startX + dx; + int y = startY + dy; + + var index = (y * mapWidth) + x; + + var xOffset = x + y * 0.5f - (int)(y / 2); + var center = new Vector3(xOffset*meshData.innerRadius*2,0,y*meshData.outerRadius*1.5f); + + for (var direction = 0; direction < 6; direction++) + { + var elevation = elevationFunction?.Invoke(heightMap[index]) ?? 0; + var previousNeighborElevation = elevation; + var neighborElevation = elevation; + var nextNeighborElevation = elevation; + + var neighbor = GetNeighbor(index, (HexDirection)direction, mapWidth, mapHeight); + if (neighbor != -1) + { + neighborElevation = Mathf.Min(elevation,elevationFunction?.Invoke(heightMap[neighbor]) ?? 0); + } + + neighbor = GetNeighbor(index, ((HexDirection)direction).Previous(), mapWidth, mapHeight); + if (neighbor != -1) + { + previousNeighborElevation = Mathf.Min(elevation,elevationFunction?.Invoke(heightMap[neighbor]) ?? 0); + } + + neighbor = GetNeighbor(index, ((HexDirection)direction).Next(), mapWidth, mapHeight); + if (neighbor != -1) + { + nextNeighborElevation = Mathf.Min(elevation,elevationFunction?.Invoke(heightMap[neighbor]) ?? 0); + } + + center.y = elevation; + + var color = colorFunction?.Invoke(heightMap[index]) ?? Color.white; + var uv = new Vector2(x / (float)mapWidth, y / (float)mapHeight); + AddTriangle(meshData, center, uv, neighborElevation, previousNeighborElevation, nextNeighborElevation, (HexDirection)direction, color); + } + + } + } + + return meshData.CreateMesh(); + } + + private static void AddTriangle(HexMeshData meshData, Vector3 center, Vector3 uv, float neighborElevation, float previousElevation, float nextElevation, HexDirection direction, Color color) + { + var v1 = center; + var v2 = center + meshData.GetFirstSolidCorner(direction); + var v3 = center + meshData.GetSecondSolidCorner(direction); + + //Add inner solid triangle + AddTriangle(meshData, v1, v2, v3, color, uv); + + //Add Quad To Fill Border Gap + var v4 = v2 + meshData.GetBridge(direction); + v4.y = neighborElevation; + var v5 = v3 + meshData.GetBridge(direction); + v5.y = neighborElevation; + AddQuad(meshData, v2, v3, v4, v5, color, uv); + + //Add Triangles to fill gap on sides of quad + var v6 = center + meshData.GetFirstCorner(direction); + v6.y = Mathf.Min(neighborElevation,previousElevation); + AddTriangle(meshData,v2, v6, v4, color, uv); + + var v7 = center + meshData.GetSecondCorner(direction); + v7.y = Mathf.Min(neighborElevation,nextElevation); + AddTriangle(meshData, v3, v5, v7, color, uv); + } + + private static void AddTriangle(HexMeshData meshData, Vector3 v1, Vector3 v2, Vector3 v3, Color color, Vector3 uv) + { + var vertexIndex = meshData.vertices.Count; + + meshData.vertices.Add(v1); + meshData.vertices.Add(v2); + meshData.vertices.Add(v3); + + meshData.colors.Add(color); + meshData.colors.Add(color); + meshData.colors.Add(color); + + meshData.uv.Add(uv); + meshData.uv.Add(uv); + meshData.uv.Add(uv); + + meshData.triangles.Add(vertexIndex); + meshData.triangles.Add(vertexIndex + 1); + meshData.triangles.Add(vertexIndex + 2); + } + + private static void AddQuad(HexMeshData meshData, Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Color color, Vector3 uv) + { + var vertexIndex = meshData.vertices.Count; + + meshData.vertices.Add(v1); + meshData.vertices.Add(v2); + meshData.vertices.Add(v3); + meshData.vertices.Add(v4); + + meshData.colors.Add(color); + meshData.colors.Add(color); + meshData.colors.Add(color); + meshData.colors.Add(color); + + meshData.uv.Add(uv); + meshData.uv.Add(uv); + meshData.uv.Add(uv); + meshData.uv.Add(uv); + + meshData.triangles.Add(vertexIndex); + meshData.triangles.Add(vertexIndex + 2); + meshData.triangles.Add(vertexIndex + 1); + + meshData.triangles.Add(vertexIndex + 1); + meshData.triangles.Add(vertexIndex + 2); + meshData.triangles.Add(vertexIndex + 3); + } + + } +} + + diff --git a/Runtime/Utility/HexMeshUtility.cs.meta b/Runtime/Utility/HexMeshUtility.cs.meta new file mode 100644 index 0000000..8ea055e --- /dev/null +++ b/Runtime/Utility/HexMeshUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94e4d100d2a68884f9b173860f59541c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utility/TerrainMeshUtility.cs b/Runtime/Utility/TerrainMeshUtility.cs index 75db20d..ccbdf68 100644 --- a/Runtime/Utility/TerrainMeshUtility.cs +++ b/Runtime/Utility/TerrainMeshUtility.cs @@ -1,4 +1,6 @@ -using UnityEngine; +using System; +using UnityEditor.SceneManagement; +using UnityEngine; namespace Gameframe.Procgen { @@ -7,7 +9,7 @@ public static class TerrainMeshUtility { private const int mapChunkSize = 241; - public static MeshData GenerateMesh(float[] heightMap, int width, int height, int levelOfDetail) + public static MeshData GenerateMesh(float[] heightMap, int width, int height, int levelOfDetail, Func stepFunction, Func colorFunction = null) { float topLeftX = (width - 1) / -2f; float topLeftZ = (height - 1) / 2f; @@ -16,7 +18,7 @@ public static MeshData GenerateMesh(float[] heightMap, int width, int height, in int vertsPerLine = (width - 1) / lodIncrement + 1; int vertsPerColumn = (height - 1) / lodIncrement + 1; - var meshData = new MeshData(vertsPerLine, vertsPerColumn); + var meshData = new MeshData(vertsPerLine, vertsPerColumn, colorFunction != null); int vertIndex = 0; int triangleIndex = 0; for (int i = 0; i < vertsPerColumn; i++) @@ -26,9 +28,14 @@ public static MeshData GenerateMesh(float[] heightMap, int width, int height, in int x = j * lodIncrement; int y = i * lodIncrement; - meshData.vertices[vertIndex] = new Vector3(topLeftX + x, heightMap[y * width + x], topLeftZ - y); + meshData.vertices[vertIndex] = new Vector3(topLeftX + x, stepFunction.Invoke(heightMap[y * width + x]), topLeftZ - y); meshData.uv[vertIndex] = new Vector2(x / (float) width, y / (float) height); + if (colorFunction != null) + { + meshData.colors[vertIndex] = colorFunction.Invoke(heightMap[y * width + x]); + }//*/ + if (j < (vertsPerLine - 1) && i < (vertsPerColumn - 1)) { triangleIndex = meshData.AddTriangle(triangleIndex, vertIndex, vertIndex + vertsPerLine + 1, @@ -39,9 +46,14 @@ public static MeshData GenerateMesh(float[] heightMap, int width, int height, in vertIndex++; } } - + return meshData; } + + public static MeshData GenerateMesh(float[] heightMap, int width, int height, float heightScale, int levelOfDetail) + { + return GenerateMesh(heightMap, width, height, levelOfDetail, x => heightScale * x); + } } public class MeshData @@ -49,6 +61,7 @@ public class MeshData public readonly Vector3[] vertices; public readonly int[] triangles; public readonly Vector2[] uv; + public readonly Color[] colors; public MeshData(Vector3[] vertices, int[] triangles, Vector2[] uv) { @@ -56,13 +69,24 @@ public MeshData(Vector3[] vertices, int[] triangles, Vector2[] uv) this.triangles = triangles; this.uv = uv; } + + public MeshData(Vector3[] vertices, int[] triangles, Vector2[] uv, Color[] colors) + { + this.vertices = vertices; + this.triangles = triangles; + this.uv = uv; + this.colors = colors; + } - public MeshData(int vertsWide, int vertsHigh) + public MeshData(int vertsWide, int vertsHigh, bool vertexColors = false) { vertices = new Vector3[vertsWide * vertsHigh]; uv = new Vector2[vertsWide * vertsHigh]; triangles = new int[(vertsWide - 1) * (vertsHigh - 1) * 6]; - + if (vertexColors) + { + colors = new Color[vertsWide * vertsHigh]; + } } //Add the triangle and return the next index @@ -82,6 +106,12 @@ public Mesh CreateMesh() triangles = triangles, uv = uv }; + + if (colors != null) + { + mesh.colors = colors; + } + mesh.RecalculateNormals(); return mesh; } diff --git a/Runtime/Utility/TextureScale.cs b/Runtime/Utility/TextureScale.cs new file mode 100644 index 0000000..f779fa1 --- /dev/null +++ b/Runtime/Utility/TextureScale.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using UnityEngine; +using System.Threading.Tasks; + +namespace Gameframe.Procgen +{ + // Only works on ARGB32, RGB24 and Alpha8 textures that are marked readable + public static class TextureScale + { + public class TaskData + { + public int start; + public int end; + + public TaskData(int s, int e) + { + start = s; + end = e; + } + } + + public static async Task PointAsync(Texture2D tex, int newWidth, int newHeight) + { + await ThreadedScaleAsync(tex, newWidth, newHeight, false); + } + + public static async Task BilinearAsync(Texture2D tex, int newWidth, int newHeight) + { + await ThreadedScaleAsync(tex, newWidth, newHeight, true); + } + + private static async Task ThreadedScaleAsync(Texture2D tex, int newWidth, int newHeight, bool useBilinear) + { + var texColors = tex.GetPixels(); + var newColors = new Color[newWidth * newHeight]; + + float ratioX; + float ratioY; + if (useBilinear) + { + ratioX = 1.0f / ((float) newWidth / (tex.width - 1)); + ratioY = 1.0f / ((float) newHeight / (tex.height - 1)); + } + else + { + ratioX = ((float) tex.width) / newWidth; + ratioY = ((float) tex.height) / newHeight; + } + + var width = tex.width; + + var cores = Mathf.Min(SystemInfo.processorCount, newHeight); + var slice = newHeight / cores; + + var tasks = new List(); + + if (cores > 1) + { + for (var i = 0; i < cores; i++) + { + var taskData = new TaskData(slice * i, slice * (i + 1)); + var task = Task.Run(() => + { + if (useBilinear) + { + BilinearScale(taskData,texColors,newColors,width,newWidth,ratioX,ratioY); + } + else + { + PointScale(taskData,texColors,newColors,width,newWidth,ratioX,ratioY); + } + }); + tasks.Add(task); + } + await Task.WhenAll(tasks); + } + else + { + var taskData = new TaskData(0, newHeight); + await Task.Run(() => + { + if (useBilinear) + { + BilinearScale(taskData,texColors,newColors,width,newWidth,ratioX,ratioY); + } + else + { + PointScale(taskData,texColors,newColors,width,newWidth,ratioX,ratioY); + } + }); + } + + tex.Resize(newWidth, newHeight); + tex.SetPixels(newColors); + tex.Apply(); + } + + private static void BilinearScale(TaskData taskData, Color[] texColors, Color[] newColors, int width, int newWidth, float ratioX, float ratioY) + { + for (var y = taskData.start; y < taskData.end; y++) + { + int yFloor = (int) Mathf.Floor(y * ratioY); + var y1 = yFloor * width; + var y2 = (yFloor + 1) * width; + var yw = y * newWidth; + + for (var x = 0; x < newWidth; x++) + { + int xFloor = (int) Mathf.Floor(x * ratioX); + var xLerp = x * ratioX - xFloor; + newColors[yw + x] = ColorLerpUnclamped( + ColorLerpUnclamped(texColors[y1 + xFloor], texColors[y1 + xFloor + 1], xLerp), + ColorLerpUnclamped(texColors[y2 + xFloor], texColors[y2 + xFloor + 1], xLerp), + y * ratioY - yFloor); + } + } + } + + private static void PointScale(TaskData taskData, Color[] texColors, Color[] newColors, int width, int newWidth, float ratioX, float ratioY) + { + for (var y = taskData.start; y < taskData.end; y++) + { + var thisY = (int) (ratioY * y) * width; + var yw = y * newWidth; + for (var x = 0; x < newWidth; x++) + { + newColors[yw + x] = texColors[(int) (thisY + ratioX * x)]; + } + } + } + + private static Color ColorLerpUnclamped(Color c1, Color c2, float value) + { + return new Color(c1.r + (c2.r - c1.r) * value, + c1.g + (c2.g - c1.g) * value, + c1.b + (c2.b - c1.b) * value, + c1.a + (c2.a - c1.a) * value); + } + + } +} \ No newline at end of file diff --git a/Runtime/Utility/TextureScale.cs.meta b/Runtime/Utility/TextureScale.cs.meta new file mode 100644 index 0000000..6ccdaab --- /dev/null +++ b/Runtime/Utility/TextureScale.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b537ab97a9076f14ab8b3903b75a6f15 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utility/VoxelMeshUtility.cs b/Runtime/Utility/VoxelMeshUtility.cs index 993e29a..23eb3ee 100644 --- a/Runtime/Utility/VoxelMeshUtility.cs +++ b/Runtime/Utility/VoxelMeshUtility.cs @@ -31,42 +31,42 @@ public static VoxelMeshData CreateMeshData(float[,] noiseMap, int chunkX, int ch var right = mapX + 1 < width ? terrainMap[mapX + 1, mapY] : null; var left = mapX - 1 >= 0 ? terrainMap[mapX - 1, mapY] : null; - meshData.AddUpQuad(x, y, terrain.Elevation); + meshData.AddUpQuad(x, y, terrain.MinElevation); - if (left != null && left.Elevation < terrain.Elevation) + if (left != null && left.MinElevation < terrain.MinElevation) { - meshData.AddLeftQuad(x, y, terrain.Elevation, terrain.Elevation - left.Elevation); + meshData.AddLeftQuad(x, y, terrain.MinElevation, terrain.MinElevation - left.MinElevation); } else if (edges && mapX - 1 < 0) { - meshData.AddLeftQuad(x, y, terrain.Elevation, terrain.Elevation + edgeThickness); + meshData.AddLeftQuad(x, y, terrain.MinElevation, terrain.MinElevation + edgeThickness); } - if (front != null && front.Elevation < terrain.Elevation) + if (front != null && front.MinElevation < terrain.MinElevation) { - meshData.AddFrontQuad(x, y, terrain.Elevation, terrain.Elevation - front.Elevation); + meshData.AddFrontQuad(x, y, terrain.MinElevation, terrain.MinElevation - front.MinElevation); } else if (edges && mapY + 1 >= height) { - meshData.AddFrontQuad(x, y, terrain.Elevation, terrain.Elevation + edgeThickness); + meshData.AddFrontQuad(x, y, terrain.MinElevation, terrain.MinElevation + edgeThickness); } - if (right != null && right.Elevation < terrain.Elevation) + if (right != null && right.MinElevation < terrain.MinElevation) { - meshData.AddRightQuad(x, y, terrain.Elevation, terrain.Elevation - right.Elevation); + meshData.AddRightQuad(x, y, terrain.MinElevation, terrain.MinElevation - right.MinElevation); } else if (edges && mapX + 1 >= width) { - meshData.AddRightQuad(x, y, terrain.Elevation, terrain.Elevation + edgeThickness); + meshData.AddRightQuad(x, y, terrain.MinElevation, terrain.MinElevation + edgeThickness); } - if (back != null && back.Elevation < terrain.Elevation) + if (back != null && back.MinElevation < terrain.MinElevation) { - meshData.AddBackQuad(x, y, terrain.Elevation, terrain.Elevation - back.Elevation); + meshData.AddBackQuad(x, y, terrain.MinElevation, terrain.MinElevation - back.MinElevation); } else if (edges && mapY - 1 < 0) { - meshData.AddBackQuad(x, y, terrain.Elevation, terrain.Elevation + edgeThickness); + meshData.AddBackQuad(x, y, terrain.MinElevation, terrain.MinElevation + edgeThickness); } } } @@ -98,42 +98,42 @@ public static VoxelMeshData CreateMeshData(float[] heightMap, int width, int hei var right = mapX + 1 < width ? terrainMap[mapY * width + mapX + 1] : null; var left = mapX - 1 >= 0 ? terrainMap[mapY * width + mapX - 1] : null; - meshData.AddUpQuad(x, y, terrain.Elevation); + meshData.AddUpQuad(x, y, terrain.MinElevation); - if (left != null && left.Elevation < terrain.Elevation) + if (left != null && left.MinElevation < terrain.MinElevation) { - meshData.AddLeftQuad(x, y, terrain.Elevation, terrain.Elevation - left.Elevation); + meshData.AddLeftQuad(x, y, terrain.MinElevation, terrain.MinElevation - left.MinElevation); } else if (edges && mapX - 1 < 0) { - meshData.AddLeftQuad(x, y, terrain.Elevation, terrain.Elevation + edgeThickness); + meshData.AddLeftQuad(x, y, terrain.MinElevation, terrain.MinElevation + edgeThickness); } - if (front != null && front.Elevation < terrain.Elevation) + if (front != null && front.MinElevation < terrain.MinElevation) { - meshData.AddFrontQuad(x, y, terrain.Elevation, terrain.Elevation - front.Elevation); + meshData.AddFrontQuad(x, y, terrain.MinElevation, terrain.MinElevation - front.MinElevation); } else if (edges && mapY + 1 >= height) { - meshData.AddFrontQuad(x, y, terrain.Elevation, terrain.Elevation + edgeThickness); + meshData.AddFrontQuad(x, y, terrain.MinElevation, terrain.MinElevation + edgeThickness); } - if (right != null && right.Elevation < terrain.Elevation) + if (right != null && right.MinElevation < terrain.MinElevation) { - meshData.AddRightQuad(x, y, terrain.Elevation, terrain.Elevation - right.Elevation); + meshData.AddRightQuad(x, y, terrain.MinElevation, terrain.MinElevation - right.MinElevation); } else if (edges && mapX + 1 >= width) { - meshData.AddRightQuad(x, y, terrain.Elevation, terrain.Elevation + edgeThickness); + meshData.AddRightQuad(x, y, terrain.MinElevation, terrain.MinElevation + edgeThickness); } - if (back != null && back.Elevation < terrain.Elevation) + if (back != null && back.MinElevation < terrain.MinElevation) { - meshData.AddBackQuad(x, y, terrain.Elevation, terrain.Elevation - back.Elevation); + meshData.AddBackQuad(x, y, terrain.MinElevation, terrain.MinElevation - back.MinElevation); } else if (edges && mapY - 1 < 0) { - meshData.AddBackQuad(x, y, terrain.Elevation, terrain.Elevation + edgeThickness); + meshData.AddBackQuad(x, y, terrain.MinElevation, terrain.MinElevation + edgeThickness); } } } diff --git a/Runtime/WorldMap/Layers/HeightMapLayerGenerator.cs b/Runtime/WorldMap/Layers/HeightMapLayerGenerator.cs index f52ac14..fc5d64d 100644 --- a/Runtime/WorldMap/Layers/HeightMapLayerGenerator.cs +++ b/Runtime/WorldMap/Layers/HeightMapLayerGenerator.cs @@ -12,6 +12,10 @@ public enum MapType Falloff, NoiseWithFalloff } + + [SerializeField] private bool step = false; + + [SerializeField] private int stepCount = 10; [SerializeField] private Vector2 offset = Vector2.zero; @@ -56,7 +60,14 @@ public HeightMapLayerData Generate(int width, int height, int seed) { for (int x = 0; x < width; x++) { - heightMap[y * width + x] = noiseMap[x, y]; + var val = noiseMap[x, y]; + + if (step) + { + val = Mathf.RoundToInt(val * stepCount) / (float)stepCount; + } + + heightMap[y * width + x] = val; } } diff --git a/Runtime/WorldMap/Terrain/TerrainTable.cs b/Runtime/WorldMap/Terrain/TerrainTable.cs index f544031..5234f62 100644 --- a/Runtime/WorldMap/Terrain/TerrainTable.cs +++ b/Runtime/WorldMap/Terrain/TerrainTable.cs @@ -16,7 +16,7 @@ public Color GetColor(float value) { if (value <= regions[i].Threshold) { - return regions[i].HighColor; + return regions[i].ColorGradient.Evaluate(1); } } @@ -40,8 +40,8 @@ private Color GetGradiatedColor(float value) public static Color GetGradiatedColor(TerrainType region, float value) { - var intensity = Mathf.InverseLerp(region.Threshold, region.Floor, value); - return Color.Lerp(region.HighColor, region.LowColor, intensity); + var intensity = Mathf.InverseLerp(region.Floor, region.Threshold, value); + return region.ColorGradient.Evaluate(intensity); } public TerrainType GetTerrainType(float value) @@ -86,7 +86,7 @@ public static Color[] GetColorMap(float[,] noiseMap, TerrainType[,] terrainMap, for (int x = 0; x < width; x++) { var index = y * width + x; - colorMap[index] = gradiate ? GetGradiatedColor(terrainMap[x, y], noiseMap[x, y]) : terrainMap[x, y].HighColor; + colorMap[index] = gradiate ? GetGradiatedColor(terrainMap[x, y], noiseMap[x, y]) : terrainMap[x, y].ColorGradient.Evaluate(1); } } @@ -98,7 +98,7 @@ public static Color[] GetColorMap(float[] noiseMap, TerrainType[] terrainMap, bo var colorMap = new Color[noiseMap.Length]; for (int i = 0; i < colorMap.Length; i++) { - colorMap[i] = gradiate ? GetGradiatedColor(terrainMap[i], noiseMap[i]) : terrainMap[i].HighColor; + colorMap[i] = gradiate ? GetGradiatedColor(terrainMap[i], noiseMap[i]) : terrainMap[i].ColorGradient.Evaluate(1); } return colorMap; @@ -116,7 +116,7 @@ public static Color[] GetColorMap(float[,] noiseMap, TerrainType[] terrainMap, b { var index = y * width + x; colorMap[index] = - gradiate ? GetGradiatedColor(terrainMap[index], noiseMap[x, y]) : terrainMap[index].HighColor; + gradiate ? GetGradiatedColor(terrainMap[index], noiseMap[x, y]) : terrainMap[index].ColorGradient.Evaluate(1); } } @@ -196,6 +196,16 @@ private void OnValidate() { regions[i].Floor = regions[i - 1].Threshold; } + + if (i > 0 && regions[i].MinElevation < regions[i - 1].MaxElevation) + { + regions[i].MinElevation = regions[i - 1].MaxElevation; + } + + if (regions[i].MaxElevation < regions[i].MinElevation) + { + regions[i].MaxElevation = regions[i].MinElevation; + } } } diff --git a/Runtime/WorldMap/Terrain/TerrainType.cs b/Runtime/WorldMap/Terrain/TerrainType.cs index f97f0e1..28b66d1 100644 --- a/Runtime/WorldMap/Terrain/TerrainType.cs +++ b/Runtime/WorldMap/Terrain/TerrainType.cs @@ -34,15 +34,24 @@ public float Threshold set => _threshold = value; } - [FormerlySerializedAs("_meshHeight")] [SerializeField] - private float _elevation; - public float Elevation => _elevation; + [FormerlySerializedAs("_elevation")] [SerializeField] + private float _minElevation; + public float MinElevation + { + get => _minElevation; + set => _minElevation = value; + } - [SerializeField] private Color _lowColor = Color.black; - public Color LowColor => _lowColor; + [SerializeField] + private float _maxElevation; + public float MaxElevation + { + get => _maxElevation; + set => _maxElevation = value; + } - [FormerlySerializedAs("_color")] [SerializeField] - private Color _highColor = Color.white; - public Color HighColor => _highColor; + [SerializeField] + private Gradient gradient; + public Gradient ColorGradient => gradient; } } \ No newline at end of file diff --git a/Runtime/WorldMap/Views/WoldMapTerrainMeshView.cs b/Runtime/WorldMap/Views/WoldMapTerrainMeshView.cs index 73c4040..bfb52e3 100644 --- a/Runtime/WorldMap/Views/WoldMapTerrainMeshView.cs +++ b/Runtime/WorldMap/Views/WoldMapTerrainMeshView.cs @@ -10,6 +10,14 @@ public class WoldMapTerrainMeshView : MonoBehaviour, IWorldMapView [SerializeField] private MeshFilter _meshFilter = null; [SerializeField, Range(0,6)] private int levelOfDetail; + + [SerializeField] private float heightScale = 1; + + [SerializeField] private TerrainTable terrainTable = null; + + [SerializeField] private bool smooth = false; + + [SerializeField] private bool useColorGradient = false; //This is here just to get the enabled checkbox in editor private void Start() @@ -23,8 +31,39 @@ public void DisplayMap(WorldMapData worldMapData) return; } var heightMap = worldMapData.GetLayer().heightMap; - var meshData = TerrainMeshUtility.GenerateMesh(heightMap,worldMapData.width,worldMapData.height,levelOfDetail); - _meshFilter.mesh = meshData.CreateMesh(); + + if (terrainTable == null) + { + var meshData = TerrainMeshUtility.GenerateMesh(heightMap,worldMapData.width,worldMapData.height,heightScale,levelOfDetail); + _meshFilter.mesh = meshData.CreateMesh(); + } + else + { + var meshData = TerrainMeshUtility.GenerateMesh(heightMap,worldMapData.width,worldMapData.height,levelOfDetail, + x => + { + var terrainType = terrainTable.GetTerrainType(x); + if (smooth) + { + var t = Mathf.InverseLerp(terrainType.Floor, terrainType.Threshold, x); + return Mathf.Lerp(terrainType.MinElevation, terrainType.MaxElevation, t) * heightScale; + } + return terrainTable.GetTerrainType(x).MinElevation * heightScale; + }, + x => + { + var terrainType = terrainTable.GetTerrainType(x); + if (!useColorGradient) + { + return terrainType.ColorGradient.Evaluate(0); + } + var t = Mathf.InverseLerp(terrainType.Floor, terrainType.Threshold, x); + return terrainType.ColorGradient.Evaluate(t); + }); + _meshFilter.mesh = meshData.CreateMesh(); + } + } + } } diff --git a/Runtime/WorldMap/Views/WorldMapBorderView.cs b/Runtime/WorldMap/Views/WorldMapBorderView.cs new file mode 100644 index 0000000..4e88e7d --- /dev/null +++ b/Runtime/WorldMap/Views/WorldMapBorderView.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using ICSharpCode.NRefactory.Ast; +using UnityEngine; +using UnityEngine.Serialization; + +namespace Gameframe.Procgen +{ + public class WorldMapBorderView : MonoBehaviour, IWorldMapView + { + [SerializeField] + private LineRenderer prefab = null; + + [SerializeField] + private Vector3 offset; + + [SerializeField] + private Color[] regionColors = new Color[0]; + + [FormerlySerializedAs("lines")] [SerializeField] + private List lineRenderers = new List(); + + [SerializeField] private int minLineLength = 2; + + private void Start() + { + if (prefab != null) + { + prefab.gameObject.SetActive(false); + } + } + + [ContextMenu("Clear Lines")] + public void ClearLines() + { + foreach (var line in lineRenderers) + { + if (Application.isPlaying) + { + Destroy(line.gameObject); + } + else + { + DestroyImmediate(line.gameObject); + + } + } + lineRenderers.Clear(); + } + + public void DisplayMap(WorldMapData mapData) + { + if (!enabled || prefab == null) + { + return; + } + + prefab.gameObject.SetActive(false); + + ClearLines(); + + var positionOffset = new Vector3(mapData.width*-0.5f,0, mapData.height*0.5f) + offset; + + var regionLayer = mapData.GetLayer(); + foreach (var region in regionLayer.regions) + { + //Each region could have more than one border line to draw + //Border point list is unordered so we need to also figure out what the lines are + + //Convert border points to 3D space + var points = new List(region.borderPoints.Select((pt) => new Vector3(pt.x,0,-pt.y) + positionOffset)); + + //Extract lines from list of points by getting closest points + var lines = new List>(); + var currentLine = new List(); + lines.Add(currentLine); + var currentPt = points[0]; + points.RemoveAt(0); + while (points.Count > 0) + { + float minDistance = float.MaxValue; + int minIndex = -1; + + for (int i = 0; i < points.Count; i++) + { + var distance = (currentPt - points[i]).sqrMagnitude; + if (distance < minDistance) + { + minDistance = distance; + minIndex = i; + } + } + + + if (minDistance > 2) + { + //Start a new line if the closest points was more than a certain distance away + lines.Add(currentLine); + currentLine = new List(); + } + + currentPt = points[minIndex]; + currentLine.Add(points[minIndex]); + points.RemoveAt(minIndex); + } + + var regionColor = regionColors.Length > 0 ? regionColors[(region.id - 1) % regionColors.Length] : Color.white; + + foreach (var line in lines) + { + //Don't draw anything with less than 2 points + if (line.Count < minLineLength) + { + continue; + } + + var lineRenderer = Instantiate(prefab, transform); + lineRenderer.transform.localEulerAngles = new Vector3(90,0,0); + lineRenderer.startColor = regionColor; + lineRenderer.endColor = regionColor; + + //lineRenderer.positionCount = region.borderPoints.Count; + lineRenderer.positionCount = line.Count; + lineRenderer.SetPositions(line.ToArray()); + + var startPt = line[0]; + var endPt = line[line.Count - 1]; + //If the end is close enough to the start then complete the loop + if ((startPt - endPt).sqrMagnitude <= 2) + { + lineRenderer.loop = true; + } + + lineRenderer.gameObject.SetActive(true); + lineRenderers.Add(lineRenderer); + } + + } + } + } +} + diff --git a/Runtime/WorldMap/Views/WorldMapBorderView.cs.meta b/Runtime/WorldMap/Views/WorldMapBorderView.cs.meta new file mode 100644 index 0000000..e22306f --- /dev/null +++ b/Runtime/WorldMap/Views/WorldMapBorderView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e5068605380f54a4392fe9a94c0c8f7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/WorldMap/Views/WorldMapHexMeshView.cs b/Runtime/WorldMap/Views/WorldMapHexMeshView.cs new file mode 100644 index 0000000..76acb55 --- /dev/null +++ b/Runtime/WorldMap/Views/WorldMapHexMeshView.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Gameframe.Procgen +{ + + public class WorldMapHexMeshView : MonoBehaviour, IWorldMapView + { + [SerializeField] private WorldMapViewChunk _prefab = null; + [SerializeField] private float radius = 1f; + [SerializeField] private TerrainTable terrainTable = null; + [SerializeField] private bool useColorGradient = false; + [SerializeField] private int chunkWidth = 16; + [SerializeField] private int chunkHeight = 16; + [SerializeField] private float heightScale = 1; + [SerializeField] private bool smooth = false; + + [SerializeField, Range(0,1f)] private float border = 0.25f; + + [SerializeField] private List chunkList = new List(); + private readonly Dictionary _chunks = new Dictionary(); + + private void Start() + { + //This is here just to show the enable checkbox in editor + } + + private void OnDisable() + { + ClearChunks(); + } + + public void DisplayMap(WorldMapData mapData) + { + if (!enabled) + { + return; + } + + var heightMap = mapData.GetLayer().heightMap; + + ClearChunks(); + + //Chunks + var chunksWide = Mathf.CeilToInt(mapData.width / (float)chunkWidth); + var chunksHigh = Mathf.CeilToInt(mapData.height / (float)chunkHeight); + + for (var chunkY = 0; chunkY < chunksHigh; chunkY++) + { + for (var chunkX = 0; chunkX < chunksWide; chunkX++) + { + //Create the mesh + var startX = chunkX * chunkWidth; + var startY = chunkY * chunkHeight; + var chunkView = GetChunk(new Vector2Int(chunkX,chunkY)); + chunkView.transform.localPosition = new Vector3(0,0,0); + var mesh = HexMeshUtility.GenerateHexagonMesh(radius, border, startX, startY, chunkWidth, chunkHeight, mapData.width, mapData.height, heightMap, GetColor, GetElevation); + chunkView.SetMesh(mesh); + } + } + } + + private Color GetColor(float height) + { + if (terrainTable == null) + { + return Color.white; + } + + var terrainType = terrainTable.GetTerrainType(height); + if (!useColorGradient) + { + return terrainType.ColorGradient.Evaluate(0); + } + + var t = Mathf.InverseLerp(terrainType.Floor, terrainType.Threshold, height); + return terrainType.ColorGradient.Evaluate(t); + } + + private float GetElevation(float height) + { + var terrainType = terrainTable.GetTerrainType(height); + if (smooth) + { + var t = Mathf.InverseLerp(terrainType.Floor, terrainType.Threshold, height); + return Mathf.Lerp(terrainType.MinElevation, terrainType.MaxElevation, t) * heightScale; + } + return terrainTable.GetTerrainType(height).MinElevation * heightScale; + } + + private WorldMapViewChunk GetChunk(Vector2Int chunkPt) + { + WorldMapViewChunk chunk = null; + if (_chunks.TryGetValue(chunkPt, out chunk)) + { + return chunk; + } + + //Create a new chunk + chunk = Instantiate(_prefab,transform); + _chunks.Add(chunkPt, chunk); + chunkList.Add(chunk); + return chunk; + } + + [ContextMenu("Clear Chunks")] + public void ClearChunks() + { + foreach (var chunk in chunkList) + { + if (chunk == null) + { + continue; + } + + if (Application.isEditor) + { + DestroyImmediate(chunk.gameObject); + } + else + { + Destroy(chunk.gameObject); + } + } + + chunkList.Clear(); + _chunks.Clear(); + } + + } + +} + diff --git a/Runtime/WorldMap/Views/WorldMapHexMeshView.cs.meta b/Runtime/WorldMap/Views/WorldMapHexMeshView.cs.meta new file mode 100644 index 0000000..bcd47d2 --- /dev/null +++ b/Runtime/WorldMap/Views/WorldMapHexMeshView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 048b34f00c228f94fbd7154e0145d6f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/WorldMap/Views/WorldMapTextureView.cs b/Runtime/WorldMap/Views/WorldMapTextureView.cs index 9b9b53c..92a7a5f 100644 --- a/Runtime/WorldMap/Views/WorldMapTextureView.cs +++ b/Runtime/WorldMap/Views/WorldMapTextureView.cs @@ -4,14 +4,12 @@ namespace Gameframe.Procgen { public class WorldMapTextureView : MonoBehaviour, IWorldMapView { - [SerializeField] private Renderer _renderer = null; + [SerializeField] private Material _material = null; [SerializeField] private bool mainTexture = false; [SerializeField] private string texturePropertyName = "_BaseMap"; [SerializeField] private TerrainTable _terrainTable = null; - - [SerializeField] private bool scaleRenderer = false; - + [SerializeField] private bool gradiate = false; [SerializeField] private bool fillRegions = false; @@ -24,8 +22,21 @@ public class WorldMapTextureView : MonoBehaviour, IWorldMapView [SerializeField] private Color[] regionColors = new Color[0]; [SerializeField] private FilterMode filterMode = FilterMode.Point; - public void DisplayMap(WorldMapData worldMapData) + [SerializeField] private bool scaleTexture = false; + [SerializeField] private float textureScale = 2f; + + private void Start() { + //This is here just to show the enabled checkbox in the unity inspector + } + + public async void DisplayMap(WorldMapData worldMapData) + { + if (!enabled) + { + return; + } + var heightMapLayer = worldMapData.GetLayer(); var regionMapLayer = worldMapData.GetLayer(); @@ -98,9 +109,9 @@ public void DisplayMap(WorldMapData worldMapData) texture.filterMode = filterMode; SetTexture(texture); - if (scaleRenderer) + if (scaleTexture) { - _renderer.transform.localScale = new Vector3(width, height, 1); + await TextureScale.BilinearAsync(texture, Mathf.Min(2048,Mathf.RoundToInt(width * textureScale)), Mathf.Min(2048,Mathf.RoundToInt(height * textureScale))); } } @@ -108,11 +119,11 @@ private void SetTexture(Texture2D texture) { if (!mainTexture) { - _renderer.sharedMaterial.SetTexture(texturePropertyName, texture); + _material.SetTexture(texturePropertyName, texture); } else { - _renderer.sharedMaterial.mainTexture = texture; + _material.mainTexture = texture; } } diff --git a/Runtime/WorldMap/WorldMapGenController.cs b/Runtime/WorldMap/WorldMapGenController.cs index 0a9dea0..241cac3 100644 --- a/Runtime/WorldMap/WorldMapGenController.cs +++ b/Runtime/WorldMap/WorldMapGenController.cs @@ -1,7 +1,9 @@ //Ignore those dumb 'never assigned to' warnings cuz this is Unity and our fields are serialized #pragma warning disable CS0649 +using System; using UnityEngine; +using Random = UnityEngine.Random; namespace Gameframe.Procgen { @@ -11,6 +13,8 @@ public class WorldMapGenController : MonoBehaviour [SerializeField] private int seed = 100; + [SerializeField] private bool randomizeSeed = false; + [SerializeField, HideInInspector] private WorldMapData _mapData; private void Start() @@ -21,6 +25,10 @@ private void Start() [ContextMenu("GenerateMap")] public void GenerateMap() { + if (randomizeSeed) + { + seed = Random.Range(int.MinValue, Int32.MaxValue); + } _mapData = _generator.GenerateMap(seed); DisplayMap(_mapData); } diff --git a/package.json b/package.json index 0a9e1cb..07494c4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "name": "com.gameframe.procgen", "displayName": "Gameframe.Procgen", "repositoryName": "UnityProcgen", - "version": "0.0.2", + "version": "0.0.3", "description": "Library of utilitities for procedural generation", "type": "library", "keywords": [],