using UnityEditor; using UnityEngine; using static System.Runtime.InteropServices.Marshal; [RequireComponent(typeof(MeshGenerator))] public class GrassInstancer : MonoBehaviour { [SerializeField] LayerMask _grassLayer; [SerializeField] Mesh _grassMesh; [SerializeField] Material _material; [SerializeField] float _density = 10; [SerializeField] int _seed = 42; MeshGenerator _meshGenerator; RenderParams _renderParams; GraphicsBuffer.IndirectDrawIndexedArgs[] commandData; struct GrassData { public Vector3 position; public Vector2 colorTexUV; } struct GrassChunk { public Vector2Int sampleCount; public GraphicsBuffer commandBuffer; public ComputeBuffer grassBuffer; public Bounds bounds; public Material material; public Vector2 uvMin; public Vector2 uvMax; } GrassChunk[] grassChunks; [SerializeField] int chunkCount = 1; void OnEnable() { Setup(); } void OnDisable() { FreeChunks(); } void Setup() { _meshGenerator = GetComponent(); if (_meshGenerator == null) Debug.LogError("MeshGenerator is null"); var meshRenderer = GetComponent(); if (meshRenderer == null) Debug.LogError("MeshRenderer is null"); if (_material == null) Debug.LogError("Material is null"); if (_grassMesh == null) Debug.LogError("Grass mesh is null"); } // Initialize the chunks public void InitChunks() { // Ensure the chunks are freed FreeChunks(); // Create the chunks grassChunks = new GrassChunk[chunkCount * chunkCount]; for (int y = 0; y < chunkCount; y++) { for (int x = 0; x < chunkCount; x++) { var chunkIndex = y * chunkCount + x; var bounds = GetChunkBounds(x, y); var uvMin = new Vector2(x, y) / chunkCount; var uvMax = uvMin + Vector2.one / chunkCount; var size = new Vector2(bounds.size.x, bounds.size.z); var sampleCount = new Vector2Int((int)(_density * size.x), (int)(_density * size.y)); grassChunks[chunkIndex] = new GrassChunk { commandBuffer = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, GraphicsBuffer.IndirectDrawIndexedArgs.size), grassBuffer = new ComputeBuffer(sampleCount.x * sampleCount.y, SizeOf()), bounds = bounds, sampleCount = sampleCount, material = _material, uvMin = uvMin, uvMax = uvMax, }; } } } // Setup a chunk void SetupChunk(GrassChunk chunk) { if (chunk.sampleCount.x <= 0 || chunk.sampleCount.y <= 0) return; // Initialize the matrices var grassData = new GrassData[chunk.sampleCount.x * chunk.sampleCount.y]; var meshSize = new Vector2(_meshGenerator.size.x, _meshGenerator.size.z); // Loop through each grass var index = 0; for (int y = 0; y < chunk.sampleCount.y; y++) { for (int x = 0; x < chunk.sampleCount.x; x++) { // Calculate the position and uvs with random offset var randomOffset = Random.insideUnitCircle / 2; var chunkUV = (new Vector2(x, y) + Vector2.one / 2 + randomOffset) / chunk.sampleCount; var worldUV = new Vector2( Remap(chunkUV.x, 0, 1, chunk.uvMin.x, chunk.uvMax.x), Remap(chunkUV.y, 0, 1, chunk.uvMin.y, chunk.uvMax.y) ); var position2D = (worldUV - Vector2.one / 2) * meshSize; var height = _meshGenerator.GetMeshHeight(worldUV); var position = transform.TransformPoint(new Vector3(position2D.x, height, position2D.y)); // Set the grass data grassData[index] = new GrassData { position = position, colorTexUV = worldUV }; // Increment the index index++; } } chunk.grassBuffer.SetData(grassData); commandData = new GraphicsBuffer.IndirectDrawIndexedArgs[1]; commandData[0] = new GraphicsBuffer.IndirectDrawIndexedArgs { indexCountPerInstance = _grassMesh.GetIndexCount(0), instanceCount = (uint)(chunk.sampleCount.x * chunk.sampleCount.y), startIndex = _grassMesh.GetIndexStart(0), baseVertexIndex = _grassMesh.GetBaseVertex(0), startInstance = 0, }; chunk.commandBuffer.SetData(commandData); } // Free the chunks void FreeChunks() { if (grassChunks == null) return; foreach (var chunk in grassChunks) { chunk.commandBuffer?.Release(); chunk.grassBuffer?.Release(); } grassChunks = null; } void OnDrawGizmos() { if (grassChunks == null) return; foreach (var chunk in grassChunks) { Gizmos.color = Color.red; Gizmos.DrawWireCube(chunk.bounds.center, chunk.bounds.size); } } // Get the bounds of a chunk Bounds GetChunkBounds(int x, int y) { var size = new Vector2(_meshGenerator.size.x, _meshGenerator.size.z); var chunkSize = size / chunkCount; var chunkPosition = new Vector2(x, y) * chunkSize - size / 2; var chunkBounds = new Bounds(); var min = new Vector3(chunkPosition.x, 0, chunkPosition.y); var max = new Vector3(chunkPosition.x + chunkSize.x, _meshGenerator.size.y, chunkPosition.y + chunkSize.y); chunkBounds.SetMinMax( transform.TransformPoint(min), transform.TransformPoint(max) ); return chunkBounds; } // Remap a value from one range to another float Remap(float value, float low1, float high1, float low2, float high2) { return low2 + (value - low1) * (high2 - low2) / (high1 - low1); } public void Generate() { // Set the seed Random.InitState(_seed); // Set the constant render params var block = new MaterialPropertyBlock(); block.SetTexture("_ColorTex", _meshGenerator.colorTexture); block.SetFloat("_MeshHeight", _grassMesh.bounds.size.y); _renderParams = new RenderParams(_material) { layer = (int)Mathf.Log(_grassLayer.value, 2), worldBounds = new Bounds(Vector3.zero, 10000 * Vector3.one), matProps = block, receiveShadows = true, }; // Generate the chunks InitChunks(); foreach (var chunk in grassChunks) SetupChunk(chunk); } // Update the rotation of the grass void UpdateRotation() { var target = Camera.main.transform.position; var rotation = Quaternion.LookRotation(transform.position - target, Vector3.up); var quaternion = new Vector4(rotation.x, rotation.y, rotation.z, rotation.w); _renderParams.matProps.SetVector("_Rotation", quaternion); } void RenderChunks() { // Update the rotation UpdateRotation(); // Render the chunks foreach (var chunk in grassChunks) { // Frustum culling check for the chunk var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(Camera.main); if (!GeometryUtility.TestPlanesAABB(frustumPlanes, chunk.bounds)) continue; // Render the chunk _renderParams.matProps.SetBuffer("_GrassData", chunk.grassBuffer); Graphics.RenderMeshIndirect(_renderParams, _grassMesh, chunk.commandBuffer); } } void Update() { // Validation checks if (_meshGenerator == null) Setup(); if (!_meshGenerator.IsMeshGenerated()) _meshGenerator.GenerateTerrainMesh(); if (transform.lossyScale != Vector3.one) Debug.LogWarning("GrassInstancer does not support scaling"); if (grassChunks == null || grassChunks.Length == 0) Generate(); // Render the chunks RenderChunks(); } } #if UNITY_EDITOR // Custom editor which adds a button to generate the terrain [CustomEditor(typeof(GrassInstancer))] public class GrassInstancerEditor : Editor { public override void OnInspectorGUI() { var script = (GrassInstancer)target; DrawDefaultInspector(); GUILayout.Space(10); // GUILayout.Label("Sample Count: " + script.GetSampleCount()); if (GUILayout.Button("Generate Grass")) script.Generate(); } } #endif