using System; using StixGames.NatureCore; using StixGames.NatureCore.Utility; using UnityEngine; using UnityEngine.Rendering; #if UNITY_EDITOR using UnityEditor; #endif namespace StixGames.GrassShader { [ExecuteInEditMode] [RequireComponent(typeof(NatureMeshFilter))] [AddComponentMenu("Stix Games/General/Grass Renderer", 1)] public class GrassRenderer : MonoBehaviour { public Material Material; [Tooltip("If the mesh has multiple materials, this can be used to select the target material index.")] public int MaterialIndex = 0; [LayerField] public int Layer; public ShadowCastingMode ShadowCastingMode = ShadowCastingMode.On; public bool ReceiveShadows = true; [Space] public bool AutoSetFloorColor = false; [Tooltip("If true, the renderer will not render meshes that are not visible by the main camera.")] public bool UseMainCameraFrustumCulling = true; private NatureMeshFilter _natureMeshFilter; private MaterialPropertyBlock propertyBlock = null; private Shader currentGrassShader = null; private Plane[] planes = new Plane[6]; private static bool hasInitializedGlobalVariables; private static readonly int GrassTime = Shader.PropertyToID("_GrassTime"); private static readonly int GlobalTargetDensityMultiplier = Shader.PropertyToID("_GlobalTargetDensityMultiplier"); public NatureMeshFilter NatureMeshFilter { get { return _natureMeshFilter != null ? _natureMeshFilter : (_natureMeshFilter = GetComponent()); } } private void Awake() { SetupGlobalVariables(); } private void SetupGlobalVariables() { if (hasInitializedGlobalVariables) { return; } hasInitializedGlobalVariables = true; if (Shader.GetGlobalFloat(GlobalTargetDensityMultiplier) == 0.0f) { Shader.SetGlobalFloat(GlobalTargetDensityMultiplier, 1); } } private void Start() { if (AutoSetFloorColor) { var renderer = GetComponent(); var terrain = GetComponent(); UpdateFloorColor(renderer, terrain); } } private void OnEnable() { #if UNITY_EDITOR #if UNITY_2019_2_OR_NEWER SceneView.duringSceneGui -= EditorGrassTimeUpdate; SceneView.duringSceneGui += EditorGrassTimeUpdate; #else SceneView.onSceneGUIDelegate -= EditorGrassTimeUpdate; SceneView.onSceneGUIDelegate += EditorGrassTimeUpdate; #endif #endif } private void OnDisable() { #if UNITY_EDITOR #if UNITY_2019_2_OR_NEWER SceneView.duringSceneGui -= EditorGrassTimeUpdate; #else SceneView.onSceneGUIDelegate -= EditorGrassTimeUpdate; #endif #endif } #if UNITY_EDITOR public void EditorGrassTimeUpdate(SceneView view) { UpdateGrassTime(); SceneView.RepaintAll(); #if UNITY_2019_2_OR_NEWER EditorApplication.QueuePlayerLoopUpdate(); #endif } #endif /// /// Set a material property block that will be used for rendering. /// /// public void SetPropertyBlock(MaterialPropertyBlock propertyBlock) { this.propertyBlock = propertyBlock; } private void LateUpdate() { if (Material == null) { return; } if (currentGrassShader != Material.shader) { if (!GrassUtility.IsGrassMaterial(Material)) { throw new InvalidOperationException("GrassMaterial does not use the DX11 Grass Shader"); } currentGrassShader = Material.shader; } #if UNITY_EDITOR SetupGlobalVariables(); #endif var renderer = GetComponent(); var terrain = GetComponent(); if (Application.isEditor && !Application.isPlaying && AutoSetFloorColor) { UpdateFloorColor(renderer, terrain); } if (Material.shader.isSupported) { UpdateGrassTime(); var maxGrassHeight = GrassUtility.GetMaxGrassHeight(Material); Vector3 localBoundingBoxExpansion; if (renderer != null) { // The mesh (and bounding box) will be scaled accoring to the local scale localBoundingBoxExpansion = new Vector3(maxGrassHeight / transform.localScale.x, maxGrassHeight / transform.localScale.y, maxGrassHeight / transform.localScale.z); } else { // Terrain is never scaled localBoundingBoxExpansion = new Vector3(maxGrassHeight, maxGrassHeight, maxGrassHeight); } #if UNITY_EDITOR var mainCamera = Application.isPlaying ? Camera.main : SceneView.lastActiveSceneView?.camera; #else var mainCamera = Camera.main; #endif // Calculate a new frustum that if (UseMainCameraFrustumCulling && mainCamera != null) { var farDistance = Mathf.Min(mainCamera.farClipPlane, GrassUtility.GetGrassFadeEnd(Material) + maxGrassHeight); var modifiedProjectionMatrix = Matrix4x4.Perspective(mainCamera.fieldOfView, mainCamera.aspect, mainCamera.nearClipPlane, farDistance); var modifiedVP = modifiedProjectionMatrix * mainCamera.worldToCameraMatrix; GeometryUtility.CalculateFrustumPlanes(modifiedVP, planes); } var meshes = NatureMeshFilter.GetMeshes(); foreach (var mesh in meshes) { // Update bounds with grass height var original = mesh.bounds; var modified = original; modified.Expand(localBoundingBoxExpansion); mesh.bounds = modified; try { var localToWorldMatrix = renderer != null ? transform.localToWorldMatrix : Matrix4x4.Translate(transform.position); if (UseMainCameraFrustumCulling && mainCamera != null) { var meshWorldBounds = TransformBounds(localToWorldMatrix, modified); // Additional frustum culling to only render meshes where the grass could be visible if (!GeometryUtility.TestPlanesAABB(planes, meshWorldBounds)) { continue; } } // Draw mesh Graphics.DrawMesh(mesh, localToWorldMatrix, Material, Layer, null, MaterialIndex, propertyBlock, ShadowCastingMode, ReceiveShadows); } finally { // Restore bounds mesh.bounds = original; } } } } /// /// Transform a bounding box with a matrix. The new bounds completely encapsulate the new bounding box. /// /// /// /// private static Bounds TransformBounds(Matrix4x4 transformationMatrix, Bounds bounds) { var center = bounds.center; var extent = bounds.extents; var newCenter = transformationMatrix.MultiplyPoint3x4(center); var absRotationMatrix = AbsRotationMatrix(transformationMatrix); var newExtents = absRotationMatrix.MultiplyVector(extent); return new Bounds( newCenter, newExtents * 2 ); } private static Matrix4x4 AbsRotationMatrix(Matrix4x4 m) { return new Matrix4x4( new Vector4(Mathf.Abs(m.m00), Mathf.Abs(m.m01), Mathf.Abs(m.m02), 0), new Vector4(Mathf.Abs(m.m10), Mathf.Abs(m.m11), Mathf.Abs(m.m12), 0), new Vector4(Mathf.Abs(m.m20), Mathf.Abs(m.m21), Mathf.Abs(m.m22), 0), new Vector4(0, 0, 0, 1) ); } private static void UpdateGrassTime() { Shader.SetGlobalVector(GrassTime, new Vector4(Time.time / 20, Time.time, Time.time * 2, Time.time * 3)); } private void UpdateFloorColor(Renderer renderer, Terrain terrain) { if (renderer != null) { var floorMaterial = renderer.sharedMaterial; var baseColor = floorMaterial.color; var colorTexture = floorMaterial.mainTexture; var offset = floorMaterial.mainTextureOffset; var scale = floorMaterial.mainTextureScale; GrassUtility.SetFloorColor(Material, baseColor); GrassUtility.SetFloorColorTexture(Material, colorTexture, offset, scale); return; } if (terrain != null) { //TODO: Add terrain support for this Debug.LogWarning("Auto setting floor color isn't supported for terrain yet."); } } } }