using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using TerrainTesselationTools; namespace ProceduralGrass { [ExecuteAlways] public class GrassRenderer : ProceduralRenderer { private GraphicsBuffer clumpPoints; [SerializeField][ReadOnly] private List grassOriginPoints = new List(); [SerializeField] private List clumpOriginPoints = new List(); private GraphicsBuffer grassBlades; private GraphicsBuffer grassBladesValid; private GraphicsBuffer defaultColorsBuffer; private GraphicsBuffer secondaryColorsBuffer; private Color[] defaultColors; private Color[] secondaryColors; private float[] uvsValues; private Texture2D heightmapTexture; private bool startExecuted; public static event EventHandler onRefresh; private ProceduralGrassSettingsSO settings; private void Start() { if (grassOriginPoints.Count <= 0) enabled = false; startExecuted = true; onRefresh += Grass_onRefresh; //Get The BirdViewUVs BirdViewUVCalculator birdViewUVs = transform.parent.GetComponent(); uvsValues = birdViewUVs.GetBirdViewUVs(); heightmapTexture = birdViewUVs.GetTexture(); settings = (ProceduralGrassSettingsSO)terrainDecorationSO.proceduralSettings; InitIndirectArgs(); InitCompute(); PopulateInitUpdateCompute(); PopulateShaderMaterial(); } private void Grass_onRefresh(object sender, EventArgs e) { Refresh(); } private void OnEnable() { if (grassOriginPoints.Count <= 0) enabled = false; if (startExecuted && enabled) { InitIndirectArgs(); InitCompute(); PopulateInitUpdateCompute(); PopulateShaderMaterial(); } } private void Update() { UpdateCompute(); } private void OnValidate() { OnEnable(); } public override void Refresh() { if (enabled) OnEnable(); } protected override void PopulateIndirectArgsCompute() { // How many times the Thread Groups need to be dispatched. dispatchSize = settings.grassBladesPerPoint * ((settings.grassSegments - 1) * 3 * 2 + 3); // This must be calculated, so it knows how many threads it needs to dispatch. calculated again in the PopulateInitCompute. //First, create the buffers for the indirect Args, and triangle array for the instance. Used on the render function. instanceTriangles = CreateGraphicsBuffer(GraphicsBuffer.Target.Structured, settings.grassBladesPerPoint * ((settings.grassSegments - 1) * 3 * 2 + 3), sizeof(int)); // This is compulsory in all procedural shaders. Count and stride differ, so you must declare it. //Setting properties computeShaderRenderPrimitives.SetInt("grassOriginPointCount", grassOriginPoints.Count); computeShaderRenderPrimitives.SetInt("grassBladesPerPoint", settings.grassBladesPerPoint); computeShaderRenderPrimitives.SetInt("grassSegments", settings.grassSegments); } protected override void PopulateInitCompute() { // How many times the Thread Groups need to be dispatched. dispatchSize = Mathf.CeilToInt((float)grassOriginPoints.Count / threadGroupSize); // This must be calculated, so it knows how many threads it needs to dispatch. Used In both dispatches, but can be calculated again in the PopulateUpdateCompute if it is different. //Create the buffers for GrassBlades, used on the shader. originPoints = CreateGraphicsBuffer(GraphicsBuffer.Target.Structured, grassOriginPoints.Count, 5 * sizeof(float)); clumpPoints = CreateGraphicsBuffer(GraphicsBuffer.Target.Structured, clumpOriginPoints.Count, 16 * sizeof(float)); grassBlades = CreateGraphicsBuffer(GraphicsBuffer.Target.Append, grassOriginPoints.Count * settings.grassBladesPerPoint, 15 * sizeof(float)); originPoints.SetData(grassOriginPoints); clumpPoints.SetData(clumpOriginPoints.Select(clumpPoint => new ClumpBuffer(clumpPoint.position, clumpPoint.properties)).ToList()); //Setting properties computeShaderInit.SetInt("grassOriginPointCount", grassOriginPoints.Count); computeShaderInit.SetInt("grassBladesPerPoint", settings.grassBladesPerPoint); computeShaderInit.SetFloat("bladeInstanceRadius", settings.bladeInstanceRadius); computeShaderInit.SetTexture(0, "_HeightMap", heightmapTexture); computeShaderInit.SetFloat("_MinX", uvsValues[0]); computeShaderInit.SetFloat("_MinZ", uvsValues[1]); computeShaderInit.SetFloat("_MaxX", uvsValues[2]); computeShaderInit.SetFloat("_MaxZ", uvsValues[3]); computeShaderInit.SetFloat("_TerrainRotation", uvsValues[4]); computeShaderInit.SetFloat("_TerrainForce", uvsValues[5]); computeShaderInit.SetBuffer(0, "_GrassOriginPoints", originPoints); computeShaderInit.SetBuffer(0, "_ClumpPoints", clumpPoints); computeShaderInit.SetBuffer(0, "_GrassBlades", grassBlades); } protected override void PopulateInitUpdateCompute() { //Preparing UpdateCompute grassBladesValid = CreateGraphicsBuffer(GraphicsBuffer.Target.Structured, grassOriginPoints.Count * settings.grassBladesPerPoint, 15 * sizeof(float)); instanceValidCount = CreateGraphicsBuffer(GraphicsBuffer.Target.Structured, 1, sizeof(uint)); computeShader.SetInt("grassOriginPointCount", grassOriginPoints.Count); computeShader.SetInt("grassBladesPerPoint", settings.grassBladesPerPoint); computeShader.SetFloat("frustumTolerance", settings.renderSettings.frustumTolerance); computeShader.SetFloat("distanceCulling", settings.renderSettings.distanceCulling); computeShader.SetFloat("distanceCullingNumber", settings.renderSettings.distanceCullingNumber); computeShader.SetFloat("distanceCullingNone", settings.renderSettings.distanceCullingNone); computeShader.SetBuffer(0, "_GrassOriginPoints", originPoints); computeShader.SetBuffer(0, "_GrassBlades", grassBlades); computeShader.SetBuffer(0, "_GrassBladesValid", grassBladesValid); computeShader.SetBuffer(0, "_IndirectDrawIndexedArgs", indirectBuffer); //Preparing Wind if (GlobalWind.Instance != null) { GlobalWind globalWind = GlobalWind.Instance; computeShader.SetBuffer(0, "_WindValues", globalWind.GetWindBuffer()); computeShader.SetTexture(0, "_WindTexture", globalWind.GetTexture2D()); } else { GraphicsBuffer defaultBuffer = GlobalWind.GetDefaultWindBuffer(); graphicsBuffers.Add(defaultBuffer); computeShader.SetBuffer(0, "_WindValues", defaultBuffer); computeShader.SetTexture(0, "_WindTexture", Texture2D.blackTexture); } } protected override void PopulateShaderMaterial() { //Setting Buffers and variables to shaderMat renderParams = new RenderParams(proceduralMaterial); // I dont think so, but this might give problems with rotationOffset in birdviewUv. If you are using it, and it gives problems: Comment the code bellow, and uncomment the one following it. renderParams.worldBounds = GenerateBounds(uvsValues[0], uvsValues[1], uvsValues[2], uvsValues[3], uvsValues[5], 5); // 5 units offset just in case. //renderParams.worldBounds = transform.parent.gameObject.GetComponent().bounds; //Calculating Color Buffers based on Gradient defaultColorsBuffer = CreateGraphicsBuffer(GraphicsBuffer.Target.Structured, settings.grassSegments + 1, 4 * sizeof(float)); secondaryColorsBuffer = CreateGraphicsBuffer(GraphicsBuffer.Target.Structured, settings.grassSegments + 1, 4 * sizeof(float)); defaultColors = new Color[settings.grassSegments + 1]; secondaryColors = new Color[settings.grassSegments + 1]; for (int i = 0; i < settings.grassSegments + 1; i++) { float linearValue = i / (float)settings.grassSegments; float exponentialValue = Mathf.Pow(linearValue, settings.exponent); defaultColors[i] = settings.defaultColorsGradient.Evaluate(exponentialValue); secondaryColors[i] = settings.secondaryColorsGradient.Evaluate(exponentialValue); } defaultColorsBuffer.SetData(defaultColors); secondaryColorsBuffer.SetData(secondaryColors); //Set Material Properties renderParams.matProps = new MaterialPropertyBlock(); renderParams.matProps.SetBuffer("_DefaultColors", defaultColorsBuffer); renderParams.matProps.SetBuffer("_SecondaryColors", secondaryColorsBuffer); renderParams.matProps.SetBuffer("_GrassBlades", grassBladesValid); renderParams.matProps.SetFloat("_GrassSegments", settings.grassSegments); renderParams.matProps.SetFloat("_GrassBladesPerPoint", settings.grassBladesPerPoint); renderParams.matProps.SetFloat("_VerticesPerBlade", settings.grassSegments * 2 + 1); renderParams.matProps.SetFloat("_Exponent", settings.exponent); renderParams.matProps.SetFloat("_MaxInteractors", GrassInteractorManager.GetMaxInteractors()); //Grass Interactors renderParams.matProps.SetBuffer("_GrassInteractorPoints", GrassInteractorManager.GetBuffer()); } protected override void PopulateUpdateCompute() { if (Camera.main == null) return; //Camera Values for culling. Matrix4x4 projectionMatrix = Camera.main.projectionMatrix; Matrix4x4 worldToCameraMatrix = Camera.main.worldToCameraMatrix; computeShader.SetVector("cameraPosition", Camera.main.gameObject.transform.position); computeShader.SetMatrix("_CameraProjectionMatrix", projectionMatrix); computeShader.SetMatrix("_CameraToWorldMatrix", worldToCameraMatrix); computeShader.SetFloat("frustumTolerance", settings.renderSettings.frustumTolerance); // Rendering renderParams.matProps.SetVector("_WSpaceCameraPos", Camera.main.gameObject.transform.position); } public Bounds GenerateBounds(float minX, float minZ, float maxX, float maxZ, float maxHeight, float offset) { float width = Mathf.Abs(maxX - minX) + offset * 2; float height = maxHeight + offset * 2; float depth = Mathf.Abs(maxZ - minZ) + offset * 2; float centerX = (minX + maxX) / 2; float centerY = maxHeight / 2 + offset; float centerZ = (minZ + maxZ) / 2; Vector3 center = new Vector3(centerX, centerY, centerZ); Vector3 size = new Vector3(width, height, depth); Bounds bounds = new Bounds(center, size); return bounds; } public override void GeneratePoints(BirdViewUVCalculator birdViewUVCalculator, Texture2D grayscaleTexture, List terrainDecorationsExtraData, float minDensityThreshold = 0.05f, float guaranteedDensityThreshold = 0.0f) { grassOriginPoints.Clear(); terrainDecorationSO = transform.parent.GetComponent().GetTerrainDecorationSO(this); uvsValues = birdViewUVCalculator.GetBirdViewUVs(); spacingAtMaxDensity = terrainDecorationSO.spacingAtMaxDensity; float jitterAmount = terrainDecorationSO.jitterAmount; // Use the local variable for jitter if (spacingAtMaxDensity != 0 && grayscaleTexture != null) { for (float x = uvsValues[0]; x <= uvsValues[2]; x += spacingAtMaxDensity) { for (float z = uvsValues[1]; z <= uvsValues[3]; z += spacingAtMaxDensity) { // Sample grayscale value from texture Color density = grayscaleTexture.GetPixelBilinear((x - uvsValues[0]) / (uvsValues[2] - uvsValues[0]), (z - uvsValues[1]) / (uvsValues[3] - uvsValues[1])); // Set a threshold (e.g., 0.1) below which no points are placed if (density.grayscale > minDensityThreshold) { Color ratio = terrainDecorationsExtraData[0].GetPixelBilinear((x - uvsValues[0]) / (uvsValues[2] - uvsValues[0]), (z - uvsValues[1]) / (uvsValues[3] - uvsValues[1])); // Implement rejection sampling float rejectionProbability = 1.0f - density.grayscale - guaranteedDensityThreshold; if (UnityEngine.Random.value > rejectionProbability) { // Apply jitter float xOffset = UnityEngine.Random.Range(-spacingAtMaxDensity * 0.5f * jitterAmount, spacingAtMaxDensity * 0.5f * jitterAmount); float zOffset = UnityEngine.Random.Range(-spacingAtMaxDensity * 0.5f * jitterAmount, spacingAtMaxDensity * 0.5f * jitterAmount); AddPoint(birdViewUVCalculator, x + xOffset, z + zOffset, new float[] { 1, ratio.grayscale }); } } } } } if (grassOriginPoints.Count >= 0) enabled = true; Refresh(); } public override void AddPoint(BirdViewUVCalculator birdViewUVCalculator, float x, float z, float[] arguments) { if (!birdViewUVCalculator.IsXZInsideSquare(x, z)) return; GrassOriginPoint grassStruct = new GrassOriginPoint { position = new Vector3(x, birdViewUVCalculator.GetHeightAtPosition(x, z), z), clumpSecondaryID = (uint)arguments[0], clumpBaseToSecondaryRatio = arguments[1] }; grassOriginPoints.Add(grassStruct); if (grassOriginPoints.Count >= 0) enabled = true; } public override void RemovePoints(BirdViewUVCalculator birdViewUVCalculator, float x, float z, float halfBrushSize) { // Define a predicate for the RemoveAll method bool IsPointInsideCircle(GrassOriginPoint point) { return Vector2.Distance(new Vector2(point.position.x, point.position.z), new Vector2(x, z)) <= halfBrushSize; } // Remove points that satisfy the condition using RemoveAll grassOriginPoints.RemoveAll(IsPointInsideCircle); if (grassOriginPoints.Count <= 0) { Debug.Log("No points in the renderer, disabling script"); enabled = false; } } public static void RefreshStatic(object sender) { onRefresh?.Invoke(sender, EventArgs.Empty); } private void OnDestroy() { onRefresh -= Grass_onRefresh; } public List GetClumps() { return clumpOriginPoints; } } [Serializable] public struct GrassOriginPoint { public Vector3 position; public uint clumpSecondaryID; public float clumpBaseToSecondaryRatio; }; [Serializable] public struct GrassBlade { public Vector3 position; public float height; public float width; public float rotationAngle; public float rotationAngleCurrent; public float hash; public float tilt; public float bend; public float windStrength; public Vector3 surfaceNorm; public float clumpBaseToSecondaryRatio; }; [Serializable] public struct ClumpPoint { public Vector3 position; public ClumpPorpertiesSO properties; }; public struct ClumpBuffer { public Vector3 position; public float moveToCenter; public float pointInSameDirection; public float pointInSameDirectionAngle; public float pointInSameDirectionRelativeCenter; public float pointInSameDirectionAngleRelativeCenter; public float height; public float heightRandom; public float width; public float widthRandom; public float tilt; public float tiltRandom; public float bend; public float bendRandom; public ClumpBuffer(Vector3 position, ClumpPorpertiesSO properties) { this.position = position; moveToCenter = properties.moveToCenter; pointInSameDirection = properties.pointInSameDirection; pointInSameDirectionAngle = properties.pointInSameDirectionAngle; pointInSameDirectionRelativeCenter = properties.pointInSameDirectionRelativeCenter; pointInSameDirectionAngleRelativeCenter = properties.pointInSameDirectionAngleRelativeCenter; height = properties.height; heightRandom = properties.heightRandom; width = properties.width; widthRandom = properties.widthRandom; tilt = properties.tilt; tiltRandom = properties.tiltRandom; bend = properties.bend; bendRandom = properties.bendRandom; } }; }