This commit is contained in:
Gerard Gascón 2025-02-02 23:44:44 +01:00
commit f5c1616018
679 changed files with 188502 additions and 0 deletions

View file

@ -0,0 +1,284 @@
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