366 lines
18 KiB
C#
366 lines
18 KiB
C#
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<GrassOriginPoint> grassOriginPoints = new List<GrassOriginPoint>();
|
|
[SerializeField] private List<ClumpPoint> clumpOriginPoints = new List<ClumpPoint>();
|
|
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<BirdViewUVCalculator>();
|
|
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<MeshRenderer>().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<Texture2D> terrainDecorationsExtraData, float minDensityThreshold = 0.05f, float guaranteedDensityThreshold = 0.0f)
|
|
{
|
|
grassOriginPoints.Clear();
|
|
terrainDecorationSO = transform.parent.GetComponent<TerrainTileDecorated>().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<ClumpPoint> 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;
|
|
}
|
|
};
|
|
}
|