300 lines
9.7 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|