ThomagicRenderer/TerrainDetailRenderer.cs

300 lines
9.7 KiB
C#

using System;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.Rendering;
namespace Assets.ThoMagic.Renderer
{
/// <summary>
/// Adds a tree renderer and grass renderer to a terrain object.
/// </summary>
[AddComponentMenu("ThoMagic/Detail Renderer")]
[ExecuteAlways]
[DisallowMultipleComponent]
public class TerrainDetailRenderer : MonoBehaviour
{
/// <summary>
/// This event is called after this detail renderer is initialized.
/// </summary>
public event EventHandler<TerrainDetailRenderer> Initialized;
/// <summary>
/// The terrain object.
/// </summary>
public Terrain terrain;
/// <summary>
/// Cached terrain data.
/// </summary>
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;
/// <summary>
/// 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().
/// </summary>
public bool AutoRefreshTerrainAtRuntime
{
get => _autoRefreshTerrainAtRuntime;
set => _autoRefreshTerrainAtRuntime = value;
}
/// <summary>
/// Enable the default unity renderer for trees and grass if this component gets disabled?
/// </summary>
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);
}
/// <summary>
/// Used to refresh the renderers.
/// </summary>
/// <param name="flags">The terrain changes</param>
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);
}
/// <summary>
/// Replace unsupported grass details with placeholders.
/// </summary>
/// <returns>True if a placeholder was created, otherwise false.</returns>
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<Camera> 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<Terrain>();
terrainData = terrain != null ? terrain.terrainData : null;
return terrainData;
}
private Bounds CalculateBounds(GameObject obj)
{
var meshRenderer = obj.GetComponentsInChildren<MeshRenderer>();
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;
}
}
}