using System; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using UnityEngine.Rendering; namespace Assets.ThoMagic.Renderer { /// /// Adds a tree renderer and grass renderer to a terrain object. /// [AddComponentMenu("ThoMagic/Detail Renderer")] [ExecuteAlways] [DisallowMultipleComponent] public class TerrainDetailRenderer : MonoBehaviour { /// /// This event is called after this detail renderer is initialized. /// public event EventHandler Initialized; /// /// The terrain object. /// public Terrain terrain; /// /// Cached terrain data. /// public TerrainData terrainData; [NonSerialized] private bool isInitialized = false; [NonSerialized] private TreeRenderer treeRenderer; [NonSerialized] private GrassRenderer grassRenderer; [NonSerialized] private Vector3 lastPosition; [NonSerialized] private Quaternion lastOrientation; [NonSerialized] private Vector3 lastScale; [Tooltip("Should ThoMagic Renderer automatically refresh the terrain data if the terrain was modified at runtime? If disabled, use Refresh() from a custom script to refresh the terrain data.")] [SerializeField] private bool _autoRefreshTerrainAtRuntime = true; [Tooltip("Should the default grass and tree rendering of Unity be enabled when ThoMagic Renderer is disabled?")] [SerializeField] private bool _enableUnityRendererWhenDisabled = true; [Tooltip("Delay the initialization of ThoMagic Renderer until the first LateUpdate event")] [SerializeField] private bool _delayInitialize = true; /// /// If true and the terrain is changed at runtime the renderers are updated automaticly. /// If needed you can also refresh them manually by calling Refresh(). /// public bool AutoRefreshTerrainAtRuntime { get => _autoRefreshTerrainAtRuntime; set => _autoRefreshTerrainAtRuntime = value; } /// /// Enable the default unity renderer for trees and grass if this component gets disabled? /// public bool EnableUnityRendererWhenDisabled { get => _enableUnityRendererWhenDisabled; set => _enableUnityRendererWhenDisabled = value; } private bool HasValidTerrainData() { TerrainData terrainData = GetTerrainData(); return terrainData != null && terrainData.detailResolution > 0 && terrainData.alphamapResolution > 0 && terrainData.heightmapResolution > 0 && terrainData.size != Vector3.zero; } private bool CanInitialize() => isActiveAndEnabled && HasValidTerrainData(); private void Awake() { //Load resources } private void OnEnable() { if (_delayInitialize && Application.isPlaying || (isInitialized || !CanInitialize())) return; Initialize(); } private void Start() { if (_delayInitialize && Application.isPlaying || (isInitialized || !CanInitialize())) return; Initialize(); } private void OnDisable() { if (!isInitialized) return; Destroy(); } private void OnDestroy() { if (!isInitialized) return; Destroy(); } //Called by unity on terrain changes public void OnTerrainChanged(TerrainChangedFlags flags) { if (!isInitialized || !AutoRefreshTerrainAtRuntime) return; Refresh(flags); } /// /// Used to refresh the renderers. /// /// The terrain changes public void Refresh(TerrainChangedFlags flags) { //if a detail object has to be replaced by a placeholder return immediatly, as a second OnTerrainChanged will be called //cause the detail protoypes where changed on the terrain. if(flags.HasFlag(TerrainChangedFlags.FlushEverythingImmediately) && ReplaceUnsupportedDetails() && AutoRefreshTerrainAtRuntime) return; treeRenderer?.OnTerrainChanged(flags); grassRenderer?.OnTerrainChanged(flags); } /// /// Replace unsupported grass details with placeholders. /// /// True if a placeholder was created, otherwise false. private bool ReplaceUnsupportedDetails() { bool flag = false; DetailPrototype[] detailPrototypes = terrainData.detailPrototypes; for (int index = 0; index < detailPrototypes.Length; ++index) { if (!RendererUtility.IsSupportedByUnity(detailPrototypes[index])) { GameObject supportedPlaceholder = RendererUtility.GetSupportedPlaceholder(detailPrototypes[index]); if (supportedPlaceholder != detailPrototypes[index].prototype) { detailPrototypes[index].prototype = supportedPlaceholder; flag = true; } } } if (flag) terrainData.detailPrototypes = detailPrototypes; return flag; } private void DisableUnityRenderer() { if (terrain == null) return; terrain.drawTreesAndFoliage = false; } private void RestoreUnityRenderer() { if (terrain == null || !EnableUnityRendererWhenDisabled) return; terrain.drawTreesAndFoliage = true; } private void Initialize() { if (isInitialized) return; DisableUnityRenderer(); GetTerrainData(); ReplaceUnsupportedDetails(); if (treeRenderer == null) treeRenderer = new TreeRenderer(terrain); treeRenderer?.Load(); if (grassRenderer == null) grassRenderer = new GrassRenderer(terrain); grassRenderer?.Load(); if (Initialized != null) Initialized(this, this); isInitialized = true; lastPosition = terrain.transform.position; lastOrientation = terrain.transform.rotation; lastScale = terrain.transform.localScale; #if UNITY_EDITOR SceneView.lastActiveSceneView?.Repaint(); RenderPipelineManager.endContextRendering -= OnBeginContextRendering; RenderPipelineManager.endContextRendering += OnBeginContextRendering; #endif } #if UNITY_EDITOR private void OnBeginContextRendering( ScriptableRenderContext context, List cameras) { LateUpdate(); } #endif private void LateUpdate() { if (!isInitialized && CanInitialize()) Initialize(); if (!isInitialized) return; //TODO: We are currently not supporting a floating offset, //so if the terrain moves we refresh/rebuild everything, //which is expenisve. If possible we should switch to a //floating offset. if (terrain.transform.position != lastPosition || terrain.transform.localScale != lastScale || terrain.transform.rotation != lastOrientation) { lastPosition = terrain.transform.position; lastOrientation = terrain.transform.rotation; lastScale = terrain.transform.localScale; Refresh(TerrainChangedFlags.FlushEverythingImmediately); } treeRenderer?.LateUpdate(); grassRenderer?.LateUpdate(); } private void Destroy() { if (!isInitialized) return; treeRenderer?.Destroy(); treeRenderer = null; grassRenderer?.Destroy(); grassRenderer = null; RestoreUnityRenderer(); isInitialized = false; #if UNITY_EDITOR RenderPipelineManager.endContextRendering -= OnBeginContextRendering; #endif } private TerrainData GetTerrainData() { if (terrain == null) terrain = GetComponent(); terrainData = terrain != null ? terrain.terrainData : null; return terrainData; } private Bounds CalculateBounds(GameObject obj) { var meshRenderer = obj.GetComponentsInChildren(); Bounds b = new Bounds(); if (meshRenderer.Length > 0) { b = new Bounds(meshRenderer[0].bounds.center, meshRenderer[0].bounds.size); for (int r = 1; r < meshRenderer.Length; r++) { b.Encapsulate(meshRenderer[r].bounds); } } return b; } } }