284 lines
8.8 KiB
C#
284 lines
8.8 KiB
C#
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<MeshGenerator>();
|
|
if (_meshGenerator == null)
|
|
Debug.LogError("MeshGenerator is null");
|
|
|
|
var meshRenderer = GetComponent<MeshRenderer>();
|
|
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<GrassData>()),
|
|
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
|