401 lines
14 KiB
C#
401 lines
14 KiB
C#
// @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 };
|
|
}
|
|
}
|