401 lines
14 KiB
C#
Raw Normal View History

2025-09-06 17:17:39 +04:00
// @Minionsart version
// credits to forkercat https://gist.github.com/junhaowww/fb6c030c17fe1e109a34f1c92571943f
// and NedMakesGames https://gist.github.com/NedMakesGames/3e67fabe49e2e3363a657ef8a6a09838
// for the base setup for compute shaders
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor;
using System.Collections.Generic;
[ExecuteInEditMode]
public class GrassComputeScript : MonoBehaviour
{
[Header("Components")]
[SerializeField] private GrassPainter grassPainter = default;
[SerializeField] private Mesh sourceMesh = default;
[SerializeField] private Material material = default;
[SerializeField] private ComputeShader computeShader = default;
// Blade
[Header("Blade")]
public float grassHeight = 1;
public float grassWidth = 0.06f;
public float grassRandomHeight = 0.25f;
[Range(0, 1)] public float bladeRadius = 0.6f;
[Range(0, 1)] public float bladeForwardAmount = 0.38f;
[Range(1, 5)] public float bladeCurveAmount = 2;
[SerializeField] private ShaderInteractor playerInteractor;
// Wind
[Header("Wind")]
public float windSpeed = 10;
public float windStrength = 0.05f;
// Interactor
[Header("Interactor")]
public float affectRadius = 0.3f;
public float affectStrength = 5;
// LOD
[Header("LOD")]
public float minFadeDistance = 40;
public float maxFadeDistance = 60;
// Material
[Header("Material")]
public Color topTint = new Color(1, 1, 1);
public Color bottomTint = new Color(0, 0, 1);
public float ambientStrength = 0.1f;
// Other
[Header("Other")]
public UnityEngine.Rendering.ShadowCastingMode castShadow;
private Camera m_MainCamera;
private readonly int m_AllowedBladesPerVertex = 6;
private readonly int m_AllowedSegmentsPerBlade = 7;
// The structure to send to the compute shader
// This layout kind assures that the data is laid out sequentially
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind
.Sequential)]
private struct SourceVertex
{
public Vector3 position;
public Vector3 normal;
public Vector2 uv;
public Vector3 color;
}
// A state variable to help keep track of whether compute buffers have been set up
private bool m_Initialized;
// A compute buffer to hold vertex data of the source mesh
private ComputeBuffer m_SourceVertBuffer;
// A compute buffer to hold vertex data of the generated mesh
private ComputeBuffer m_DrawBuffer;
// A compute buffer to hold indirect draw arguments
private ComputeBuffer m_ArgsBuffer;
// Instantiate the shaders so data belong to their unique compute buffers
private ComputeShader m_InstantiatedComputeShader;
private Material m_InstantiatedMaterial;
// The id of the kernel in the grass compute shader
private int m_IdGrassKernel;
// The x dispatch size for the grass compute shader
private int m_DispatchSize;
// The local bounds of the generated mesh
private Bounds m_LocalBounds;
private Camera sceneCam;
// The size of one entry in the various compute buffers
private const int SOURCE_VERT_STRIDE = sizeof(float) * (3 + 3 + 2 + 3);
private const int DRAW_STRIDE = sizeof(float) * (3 + (3 + 2 + 3) * 3);
private const int INDIRECT_ARGS_STRIDE = sizeof(int) * 4;
// The data to reset the args buffer with every frame
// 0: vertex count per draw instance. We will only use one instance
// 1: instance count. One
// 2: start vertex location if using a Graphics Buffer
// 3: and start instance location if using a Graphics Buffer
private int[] argsBufferReset = new int[] { 0, 1, 0, 0 };
#if UNITY_EDITOR
SceneView view;
// get the scene camera in case of no maincam
void OnFocus()
{
// Remove delegate listener if it has previously
// been assigned.
SceneView.duringSceneGui -= this.OnScene;
// Add (or re-add) the delegate.
SceneView.duringSceneGui += this.OnScene;
}
void OnDestroy()
{
// When the window is destroyed, remove the delegate
// so that it will no longer do any drawing.
SceneView.duringSceneGui -= this.OnScene;
}
void OnScene(SceneView scene)
{
view = scene;
}
#endif
private void OnValidate()
{
// Set up components
grassPainter = GetComponent<GrassPainter>();
sourceMesh = GetComponent<MeshFilter>().sharedMesh; // generated by GeometryGrassPainter
}
public void AssignCamera(Camera camToAssign)
{
m_MainCamera = camToAssign;
}
public void AssignInteractionAffector(ShaderInteractor playerInteractor)
{
this.playerInteractor = playerInteractor;
}
private void OnEnable()
{
// If initialized, call on disable to clean things up
if (m_Initialized)
{
OnDisable();
}
#if UNITY_EDITOR
SceneView.duringSceneGui += this.OnScene;
#endif
// Setup compute shader and material manually
// Don't do anything if resources are not found,
// or no vertex is put on the mesh.
if (grassPainter == null || sourceMesh == null || computeShader == null || material == null)
{
return;
}
sourceMesh = GetComponent<MeshFilter>().sharedMesh;
if (sourceMesh.vertexCount == 0)
{
return;
}
//force assign the camera to the players if he is in the scene.
if (Player.Instance != null)
{
//Debug.Log($"Main Cam Before Assignment: {m_MainCamera}");
if (m_MainCamera == null) { m_MainCamera = Player.Instance.cameraController.playerCam; }
//Debug.Log($"Main Cam After Assignment: {m_MainCamera}");
}
m_Initialized = true;
// Instantiate the shaders so they can point to their own buffers
m_InstantiatedComputeShader = Instantiate(computeShader);
m_InstantiatedMaterial = Instantiate(material);
// Grab data from the source mesh
Vector3[] positions = sourceMesh.vertices;
Vector3[] normals = sourceMesh.normals;
Vector2[] uvs = sourceMesh.uv;
Color[] colors = sourceMesh.colors;
// Create the data to upload to the source vert buffer
SourceVertex[] vertices = new SourceVertex[positions.Length];
for (int i = 0; i < vertices.Length; i++)
{
Color color = colors[i];
vertices[i] = new SourceVertex()
{
position = positions[i],
normal = normals[i],
uv = uvs[i],
color = new Vector3(color.r, color.g, color.b) // Color --> Vector3
};
}
int numSourceVertices = vertices.Length;
// Each segment has two points
int maxBladesPerVertex = Mathf.Max(1, m_AllowedBladesPerVertex);
int maxSegmentsPerBlade = Mathf.Max(1, m_AllowedSegmentsPerBlade);
int maxBladeTriangles = maxBladesPerVertex * ((maxSegmentsPerBlade - 1) * 2 + 1);
// Create compute buffers
// The stride is the size, in bytes, each object in the buffer takes up
m_SourceVertBuffer = new ComputeBuffer(vertices.Length, SOURCE_VERT_STRIDE,
ComputeBufferType.Structured, ComputeBufferMode.Immutable);
m_SourceVertBuffer.SetData(vertices);
m_DrawBuffer = new ComputeBuffer(numSourceVertices * maxBladeTriangles, DRAW_STRIDE,
ComputeBufferType.Append);
m_DrawBuffer.SetCounterValue(0);
m_ArgsBuffer =
new ComputeBuffer(1, INDIRECT_ARGS_STRIDE, ComputeBufferType.IndirectArguments);
// Cache the kernel IDs we will be dispatching
m_IdGrassKernel = m_InstantiatedComputeShader.FindKernel("Main");
// Set buffer data
m_InstantiatedComputeShader.SetBuffer(m_IdGrassKernel, "_SourceVertices",
m_SourceVertBuffer);
m_InstantiatedComputeShader.SetBuffer(m_IdGrassKernel, "_DrawTriangles", m_DrawBuffer);
m_InstantiatedComputeShader.SetBuffer(m_IdGrassKernel, "_IndirectArgsBuffer",
m_ArgsBuffer);
// Set vertex data
m_InstantiatedComputeShader.SetInt("_NumSourceVertices", numSourceVertices);
m_InstantiatedComputeShader.SetInt("_MaxBladesPerVertex", maxBladesPerVertex);
m_InstantiatedComputeShader.SetInt("_MaxSegmentsPerBlade", maxSegmentsPerBlade);
m_InstantiatedMaterial.SetBuffer("_DrawTriangles", m_DrawBuffer);
m_InstantiatedMaterial.SetColor("_TopTint", topTint);
m_InstantiatedMaterial.SetColor("_BottomTint", bottomTint);
m_InstantiatedMaterial.SetFloat("_AmbientStrength", ambientStrength);
// Calculate the number of threads to use. Get the thread size from the kernel
// Then, divide the number of triangles by that size
m_InstantiatedComputeShader.GetKernelThreadGroupSizes(m_IdGrassKernel,
out uint threadGroupSize, out _, out _);
m_DispatchSize = Mathf.CeilToInt((float)numSourceVertices / threadGroupSize);
// Get the bounds of the source mesh and then expand by the maximum blade width and height
m_LocalBounds = sourceMesh.bounds;
m_LocalBounds.Expand(Mathf.Max(grassHeight + grassRandomHeight, grassWidth));
SetGrassDataBase();
}
private void OnDisable()
{
// Dispose of buffers and copied shaders here
if (m_Initialized)
{
// If the application is not in play mode, we have to call DestroyImmediate
if (Application.isPlaying)
{
Destroy(m_InstantiatedComputeShader);
Destroy(m_InstantiatedMaterial);
}
else
{
DestroyImmediate(m_InstantiatedComputeShader);
DestroyImmediate(m_InstantiatedMaterial);
}
// Release each buffer
m_SourceVertBuffer?.Release();
m_DrawBuffer?.Release();
m_ArgsBuffer?.Release();
}
m_Initialized = false;
}
// LateUpdate is called after all Update calls
private void LateUpdate()
{
// If in edit mode, we need to update the shaders each Update to make sure settings changes are applied
// Don't worry, in edit mode, Update isn't called each frame
if (Application.isPlaying == false)
{
OnDisable();
OnEnable();
}
// If not initialized, do nothing (creating zero-length buffer will crash)
if (!m_Initialized)
{
// Initialization is not done, please check if there are null components
// or just because there is not vertex being painted.
return;
}
// Clear the draw and indirect args buffers of last frame's data
m_DrawBuffer.SetCounterValue(0);
m_ArgsBuffer.SetData(argsBufferReset);
// Transform the bounds to world space
Bounds bounds = TransformBounds(m_LocalBounds);
// Update the shader with frame specific data
SetGrassDataUpdate();
// Dispatch the grass shader. It will run on the GPU
m_InstantiatedComputeShader.Dispatch(m_IdGrassKernel, m_DispatchSize, 1, 1);
// DrawProceduralIndirect queues a draw call up for our generated mesh
Graphics.DrawProceduralIndirect(m_InstantiatedMaterial, bounds, MeshTopology.Triangles,
m_ArgsBuffer, 0, null, null, castShadow, true, gameObject.layer);
}
private void SetGrassDataBase()
{
// Send things to compute shader that dont need to be set every frame
m_InstantiatedComputeShader.SetMatrix("_LocalToWorld", transform.localToWorldMatrix);
m_InstantiatedComputeShader.SetFloat("_Time", Time.time);
m_InstantiatedComputeShader.SetFloat("_GrassHeight", grassHeight);
m_InstantiatedComputeShader.SetFloat("_GrassWidth", grassWidth);
m_InstantiatedComputeShader.SetFloat("_GrassRandomHeight", grassRandomHeight);
m_InstantiatedComputeShader.SetFloat("_WindSpeed", windSpeed);
m_InstantiatedComputeShader.SetFloat("_WindStrength", windStrength);
m_InstantiatedComputeShader.SetFloat("_InteractorRadius", affectRadius);
m_InstantiatedComputeShader.SetFloat("_InteractorStrength", affectStrength);
m_InstantiatedComputeShader.SetFloat("_BladeRadius", bladeRadius);
m_InstantiatedComputeShader.SetFloat("_BladeForward", bladeForwardAmount);
m_InstantiatedComputeShader.SetFloat("_BladeCurve", Mathf.Max(0, bladeCurveAmount));
m_InstantiatedComputeShader.SetFloat("_MinFadeDist", minFadeDistance);
m_InstantiatedComputeShader.SetFloat("_MaxFadeDist", maxFadeDistance);
}
private void SetGrassDataUpdate()
{
// Compute Shader
// m_InstantiatedComputeShader.SetMatrix("_LocalToWorld", transform.localToWorldMatrix);
m_InstantiatedComputeShader.SetFloat("_Time", Time.time);
if (playerInteractor != null)
{
m_InstantiatedComputeShader.SetVector("_PositionMoving", playerInteractor.transform.position);
}
else
{
m_InstantiatedComputeShader.SetVector("_PositionMoving", Vector3.zero);
}
if (m_MainCamera != null)
{
m_InstantiatedComputeShader.SetVector("_CameraPositionWS", m_MainCamera.transform.position);
}
#if UNITY_EDITOR
// if we dont have a main camera (it gets added during gameplay), use the scene camera
else if (view != null)
{
m_InstantiatedComputeShader.SetVector("_CameraPositionWS", view.camera.transform.position);
}
#endif
}
// This applies the game object's transform to the local bounds
// Code by benblo from https://answers.unity.com/questions/361275/cant-convert-bounds-from-world-coordinates-to-loca.html
private Bounds TransformBounds(Bounds boundsOS)
{
var center = transform.TransformPoint(boundsOS.center);
// transform the local extents' axes
var extents = boundsOS.extents;
var axisX = transform.TransformVector(extents.x, 0, 0);
var axisY = transform.TransformVector(0, extents.y, 0);
var axisZ = transform.TransformVector(0, 0, extents.z);
// sum their absolute value to get the world extents
extents.x = Mathf.Abs(axisX.x) + Mathf.Abs(axisY.x) + Mathf.Abs(axisZ.x);
extents.y = Mathf.Abs(axisX.y) + Mathf.Abs(axisY.y) + Mathf.Abs(axisZ.y);
extents.z = Mathf.Abs(axisX.z) + Mathf.Abs(axisY.z) + Mathf.Abs(axisZ.z);
return new Bounds { center = center, extents = extents };
}
}