using System; using UnityEngine; namespace StixGames.GrassShader { public static class GrassUtility { /// /// Calculates the tessellation factor, exactly like it is done in the shader, except for some optimization /// public static float CalcTessFactor(Vector3 wpos0, Vector3 wpos1, Vector3 cameraPos, float targetDensity, float falloffStart, float falloffScale, float falloffPower) { //Length of the edge var len = Vector3.Distance(wpos0, wpos1); //Distance to the center of the edge var dist = Vector3.Distance(0.5f * (wpos0 + wpos1), cameraPos); //Limit distance and scale dist = Mathf.Pow(Mathf.Max(dist - falloffStart, 0.0f) * falloffScale, falloffPower); //The target distance between blades of grass var targetLen = targetDensity * Mathf.Max(dist, 1.0f); return len / targetLen; } public static float CalcMaxTessFactor(Vector3 wpos0, Vector3 wpos1, float targetDensity) { //Length of the edge var len = Vector3.Distance(wpos0, wpos1); //The target distance between blades of grass var targetLen = targetDensity; return len / targetLen; } public static float CalcMaxTessFactor(Triangle triangle, float targetDensity) { var tess0 = CalcMaxTessFactor(triangle.V0, triangle.V1, targetDensity); var tess1 = CalcMaxTessFactor(triangle.V1, triangle.V2, targetDensity); var tess2 = CalcMaxTessFactor(triangle.V0, triangle.V2, targetDensity); return (tess0 + tess1 + tess2) * 0.33333f; } public static float CalcTessFactor(Triangle triangle, Vector3 cameraPos, float targetDensity, float falloffStart, float falloffScale, float falloffPower) { var tess0 = CalcTessFactor(triangle.V0, triangle.V1, cameraPos, targetDensity, falloffStart, falloffScale, falloffPower); var tess1 = CalcTessFactor(triangle.V1, triangle.V2, cameraPos, targetDensity, falloffStart, falloffScale, falloffPower); var tess2 = CalcTessFactor(triangle.V0, triangle.V2, cameraPos, targetDensity, falloffStart, falloffScale, falloffPower); return (tess0 + tess1 + tess2) * 0.33333f; } public static float CalcTessLevel(float tessFactor) { return Mathf.Max(Mathf.Log(tessFactor, 2), 0); } private static readonly int[] BillboardCounts = {0, 1, 7, 37, 169, 496, 2977}; public static int TessLevelToBillboardCount(int tessLevel) { //Grass tessellation has the following amounts of blades of grass, per triangle: 1, 7, 37, 169, 496, 2977 //The tessellation on one side of the triangle increases with 2^n-1, if you start counting the amount of blades per row, //starting from the opposing side, they increase in odd numbers (1, 3, 7, ...) until 2^n-1 is reached. //This number is then repeated until the total amount of rows is once again 2^n-1. Sum all rows and you get the total amount of blades of grass. //Here n is the tessellation factor used in the shader, tessfactor is already 2^n //Yes, this was fairly unscientific, but I haven't found any specific resources how tessellation is done and this is not too bad. //If anybody has a better way of doing this, please send man an email... if (tessLevel < 0 || tessLevel >= BillboardCounts.Length) { throw new ArgumentOutOfRangeException("tessLevel"); } return BillboardCounts[tessLevel]; } public static int GetTessLevelFromBillboardCount(int billboardCount) { for (int i = 0; i < BillboardCounts.Length; i++) { if (BillboardCounts[i] >= billboardCount) { return i; } } throw new ArgumentOutOfRangeException("billboardCount"); } private static readonly string[] GrassTypeString = { "SIMPLE_GRASS", "ONE_GRASS_TYPE", "TWO_GRASS_TYPES", "THREE_GRASS_TYPES", "FOUR_GRASS_TYPES" }; public static int GetGrassTypeCount(Material mat) { if (mat.IsKeywordEnabled(GrassTypeString[0])) { return 0; } if (mat.IsKeywordEnabled(GrassTypeString[1])) { return 1; } if (mat.IsKeywordEnabled(GrassTypeString[2])) { return 2; } if (mat.IsKeywordEnabled(GrassTypeString[3])) { return 3; } if (mat.IsKeywordEnabled(GrassTypeString[4])) { return 4; } throw new ArgumentException("No valid grass type"); } public static readonly string[] DensityModes = {"UNIFORM_DENSITY", "VERTEX_DENSITY", ""}; private static readonly int DensityValues = Shader.PropertyToID("_DensityValues"); private static readonly int DensityTexture = Shader.PropertyToID("_DensityTexture"); private static readonly int Disorder = Shader.PropertyToID("_Disorder"); private static readonly int Softness00 = Shader.PropertyToID("_Softness00"); private static readonly int Softness01 = Shader.PropertyToID("_Softness01"); private static readonly int Softness02 = Shader.PropertyToID("_Softness02"); private static readonly int Softness03 = Shader.PropertyToID("_Softness03"); private static readonly int MaxHeight00 = Shader.PropertyToID("_MaxHeight00"); private static readonly int MaxHeight01 = Shader.PropertyToID("_MaxHeight01"); private static readonly int MaxHeight02 = Shader.PropertyToID("_MaxHeight02"); private static readonly int MaxHeight03 = Shader.PropertyToID("_MaxHeight03"); private static readonly int Subsurface00 = Shader.PropertyToID("_Subsurface00"); private static readonly int Subsurface01 = Shader.PropertyToID("_Subsurface01"); private static readonly int Subsurface02 = Shader.PropertyToID("_Subsurface02"); private static readonly int Subsurface03 = Shader.PropertyToID("_Subsurface03"); private static readonly int WindParams = Shader.PropertyToID("_WindParams"); private static readonly int TargetDensity = Shader.PropertyToID("_TargetDensity"); private static readonly int TargetTessellation = Shader.PropertyToID("_TargetTessellation"); private static readonly int CullMode = Shader.PropertyToID("_CullMode"); private static readonly int GrassFadeStart = Shader.PropertyToID("_GrassFadeStart"); private static readonly int GrassFadeEnd = Shader.PropertyToID("_GrassFadeEnd"); private static readonly int GrassFloorColor = Shader.PropertyToID("_GrassFloorColor"); private static readonly int GrassFloorColorTexture = Shader.PropertyToID("_GrassFloorColorTexture"); private static readonly int ColorMap = Shader.PropertyToID("_ColorMap"); public static DensityMode GetDensityMode(Material mat) { if (mat.IsKeywordEnabled(DensityModes[0])) { return DensityMode.Value; } if (mat.IsKeywordEnabled(DensityModes[1])) { return DensityMode.Vertex; } return DensityMode.Texture; } public static void SetDensityMode(Material mat, DensityMode target) { var density = GetDensityMode(mat); switch (density) { case DensityMode.Value: mat.DisableKeyword(DensityModes[0]); break; case DensityMode.Vertex: mat.DisableKeyword(DensityModes[1]); break; } mat.EnableKeyword(DensityModes[(int) target]); } public static Vector4 GetValueDensity(Material mat) { return mat.GetVector(DensityValues); } public static void SetValueDensity(Material mat, Vector4 value) { mat.SetVector(DensityValues, value); } public static void SetValueDensity(MaterialPropertyBlock block, Vector4 value) { block.SetVector(DensityValues, value); } public static Texture2D GetDensityTexture(Material mat) { return mat.GetTexture(DensityTexture) as Texture2D; } public static float GetDisorderValue(Material mat) { return mat.GetFloat(Disorder); } public static float GetMaxSoftnessValue(Material mat) { var count = GetGrassTypeCount(mat); var soft0 = mat.GetFloat(Softness00); var soft1 = mat.GetFloat(Softness01) * (count >= 2 ? 1 : 0); var soft2 = mat.GetFloat(Softness02) * (count >= 3 ? 1 : 0); var soft3 = mat.GetFloat(Softness03) * (count >= 4 ? 1 : 0); return Mathf.Max(soft0, soft1, soft2, soft3); } public static float GetMinSoftnessValue(Material mat) { var count = GetGrassTypeCount(mat); var soft0 = mat.GetFloat(Softness00); var soft1 = mat.GetFloat(Softness01) * (count >= 2 ? 1 : Mathf.Infinity); var soft2 = mat.GetFloat(Softness02) * (count >= 3 ? 1 : Mathf.Infinity); var soft3 = mat.GetFloat(Softness03) * (count >= 4 ? 1 : Mathf.Infinity); return Mathf.Min(soft0, soft1, soft2, soft3); } /// /// Returns the maximum height for one grass type /// /// /// /// public static float GetMaxGrassHeight(Material mat, int grassType) { int maxHeightId; switch (grassType) { case 0: maxHeightId = MaxHeight00; break; case 1: maxHeightId = MaxHeight01; break; case 2: maxHeightId = MaxHeight02; break; case 3: maxHeightId = MaxHeight03; break; default: throw new ArgumentOutOfRangeException(nameof(grassType)); } return mat.GetFloat(maxHeightId); } /// /// Returns the maximum grass height of all types combined. /// /// Expects that mat is a grass material. /// /// /// public static float GetMaxGrassHeight(Material mat) { var count = GetGrassTypeCount(mat); var maxGrassHeight = GetMaxGrassHeight(mat, 0); if (count == 1) { return maxGrassHeight; } maxGrassHeight = Mathf.Max(maxGrassHeight, GetMaxGrassHeight(mat, 1)); if (count == 2) { return maxGrassHeight; } maxGrassHeight = Mathf.Max(maxGrassHeight, GetMaxGrassHeight(mat, 2)); if (count == 3) { return maxGrassHeight; } return Mathf.Max(maxGrassHeight, GetMaxGrassHeight(mat, 3)); } public static void SetWindParams(Material mat, Vector4 value) { mat.SetVector(WindParams, value); } public static void SetEdgeLength(Material mat, float value) { mat.SetFloat(TargetDensity, value); } public static void SetTargetTessellation(Material mat, float value) { mat.SetFloat(TargetTessellation, value); } public static bool IsForwardOnlyGrassMaterial(Material material) { if (material == null) { throw new ArgumentNullException(nameof(material)); } return material.shader.name == "Stix Games/Grass Forward Only"; } public static bool IsHDRPGrassMaterial(Material material) { if (material == null) { throw new ArgumentNullException(nameof(material)); } return material.shader.name == "Stix Games/Grass HDRP"; } public static bool IsURPrassMaterial(Material material) { if (material == null) { throw new ArgumentNullException(nameof(material)); } return material.shader.name == "Stix Games/Grass URP"; } public static bool IsGrassMaterial(Material material) { if (material == null) { throw new ArgumentNullException(nameof(material)); } var shaderName = material.shader.name; return shaderName == "Stix Games/Grass" || shaderName == "Stix Games/Grass Forward Only" || shaderName == "Stix Games/Grass HDRP" || shaderName == "Stix Games/Grass URP" || shaderName == "Hidden/Stix Games/Grass Fallback Renderer"; } public static void SetRandomizedDirectionMode(Material material, bool enable) { if (enable) { material.EnableKeyword("GRASS_RANDOM_DIR"); material.SetInt(CullMode, 2); } else { material.DisableKeyword("GRASS_RANDOM_DIR"); material.SetInt(CullMode, 0); } } public static float GetGrassFadeStart(Material mat) { return mat.GetFloat(GrassFadeStart); } public static float GetGrassFadeEnd(Material mat) { return mat.GetFloat(GrassFadeEnd); } public static void SetFloorColor(Material mat, Color value) { mat.SetColor(GrassFloorColor, value); } public static void SetFloorColorTexture(Material mat, Texture value, Vector2 offset, Vector2 scale) { mat.SetTexture(GrassFloorColorTexture, value); mat.SetTextureOffset(GrassFloorColorTexture, offset); mat.SetTextureScale(GrassFloorColorTexture, scale); } public static void SetColorHeightTexture(Material mat, Texture value) { mat.SetTexture(ColorMap, value); } private static float GetSubsurface(Material mat, int grassType) { if (grassType < 0 || grassType >= 4) { throw new ArgumentOutOfRangeException(nameof(grassType)); } int subsurfaceId; switch (grassType) { case 0: subsurfaceId = Subsurface00; break; case 1: subsurfaceId = Subsurface01; break; case 2: subsurfaceId = Subsurface02; break; case 3: subsurfaceId = Subsurface03; break; default: throw new ArgumentOutOfRangeException(nameof(grassType)); } return mat.GetFloat(subsurfaceId); } public static float GetMaxSubsurface(Material mat) { var count = GetGrassTypeCount(mat); var subsurface0 = GetSubsurface(mat, 0); var subsurface1 = GetSubsurface(mat, 1) * (count >= 2 ? 1 : 0); var subsurface2 = GetSubsurface(mat, 2) * (count >= 3 ? 1 : 0); var subsurface3 = GetSubsurface(mat, 3) * (count >= 4 ? 1 : 0); return Mathf.Max(subsurface0, subsurface1, subsurface2, subsurface3); } public static bool IsFollowingSurfaceNormal(Material material) { return material.IsKeywordEnabled(FollowSurfaceNormalKeyword); } public static void SetFollowSurfaceNormal(Material material, bool enable) { if (enable) { material.EnableKeyword(FollowSurfaceNormalKeyword); } else { material.DisableKeyword(FollowSurfaceNormalKeyword); } } public const string GrassObjectModeKeyword = "GRASS_OBJECT_MODE"; public const string FollowSurfaceNormalKeyword = "GRASS_FOLLOW_SURFACE_NORMAL"; public static bool IsInObjectSpaceMode(Material material) { return material.IsKeywordEnabled(GrassObjectModeKeyword); } public static void SetObjectSpaceMode(Material material, bool enable) { if (enable) { material.EnableKeyword(GrassObjectModeKeyword); } else { material.DisableKeyword(GrassObjectModeKeyword); } } } public enum DensityMode { Value, Vertex, Texture } }