Files
Outline-Toolkit/Runtime/Scripts/OutlineBehaviour.cs
2025-07-20 21:01:09 +08:00

444 lines
8.8 KiB
C#

// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved.
// See the LICENSE.md file in the project root for more information.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityFx.Outline
{
/// <summary>
/// Attach this script to a <see cref="GameObject"/> to add outline effect. It can be configured in edit-time or in runtime via scripts.
/// </summary>
/// <seealso cref="OutlineEffect"/>
[ExecuteInEditMode]
[DisallowMultipleComponent]
public sealed class OutlineBehaviour : MonoBehaviour, IOutlineSettings
{
#region data
#pragma warning disable 0649
[SerializeField, Tooltip(OutlineResources.OutlineResourcesTooltip)]
private OutlineResources _outlineResources;
[SerializeField, HideInInspector]
private OutlineSettingsInstance _outlineSettings;
[SerializeField, HideInInspector]
private int _ignoreLayerMask;
[SerializeField, HideInInspector]
private CameraEvent _cameraEvent = OutlineRenderer.RenderEvent;
[SerializeField, HideInInspector]
private Camera _targetCamera;
[SerializeField, Tooltip("If set, list of object renderers is updated on each frame. Enable if the object has child renderers which are enabled/disabled frequently.")]
private bool _updateRenderers;
#pragma warning restore 0649
private Dictionary<Camera, CommandBuffer> _cameraMap = new Dictionary<Camera, CommandBuffer>();
private List<Camera> _camerasToRemove = new List<Camera>();
private OutlineRendererCollection _renderers;
#endregion
#region interface
/// <summary>
/// Gets or sets resources used by the effect implementation.
/// </summary>
/// <exception cref="ArgumentNullException">Thrown if setter argument is <see langword="null"/>.</exception>
/// <seealso cref="OutlineSettings"/>
public OutlineResources OutlineResources
{
get
{
return _outlineResources;
}
set
{
if (value is null)
{
throw new ArgumentNullException(nameof(OutlineResources));
}
_outlineResources = value;
}
}
/// <summary>
/// Gets or sets outline settings. Set this to non-<see langword="null"/> value to share settings with other components.
/// </summary>
/// <seealso cref="OutlineResources"/>
public OutlineSettings OutlineSettings
{
get
{
if (_outlineSettings == null)
{
_outlineSettings = new OutlineSettingsInstance();
}
return _outlineSettings.OutlineSettings;
}
set
{
if (_outlineSettings == null)
{
_outlineSettings = new OutlineSettingsInstance();
}
_outlineSettings.OutlineSettings = value;
}
}
/// <summary>
/// Gets or sets layer mask to use for ignored <see cref="Renderer"/> components in this game object.
/// </summary>
public int IgnoreLayerMask
{
get
{
return _ignoreLayerMask;
}
set
{
if (_ignoreLayerMask != value)
{
_ignoreLayerMask = value;
_renderers?.Reset(false, value);
}
}
}
/// <summary>
/// Gets or sets <see cref="CameraEvent"/> used to render the outlines.
/// </summary>
public CameraEvent RenderEvent
{
get
{
return _cameraEvent;
}
set
{
if (_cameraEvent != value)
{
foreach (var kvp in _cameraMap)
{
if (kvp.Key)
{
kvp.Key.RemoveCommandBuffer(_cameraEvent, kvp.Value);
kvp.Key.AddCommandBuffer(value, kvp.Value);
}
}
_cameraEvent = value;
}
}
}
/// <summary>
/// Gets outline renderers. By default all child <see cref="Renderer"/> components are used for outlining.
/// </summary>
/// <seealso cref="UpdateRenderers"/>
public ICollection<Renderer> OutlineRenderers
{
get
{
CreateRenderersIfNeeded();
return _renderers;
}
}
/// <summary>
/// Gets or sets camera to render outlines to. If not set, outlines are rendered to all active cameras.
/// </summary>
/// <seealso cref="Cameras"/>
public Camera Camera
{
get
{
return _targetCamera;
}
set
{
if (_targetCamera != value)
{
if (value)
{
_camerasToRemove.Clear();
foreach (var kvp in _cameraMap)
{
if (kvp.Key && kvp.Key != value)
{
kvp.Key.RemoveCommandBuffer(_cameraEvent, kvp.Value);
kvp.Value.Dispose();
_camerasToRemove.Add(kvp.Key);
}
}
foreach (var camera in _camerasToRemove)
{
_cameraMap.Remove(camera);
}
}
_targetCamera = value;
}
}
}
/// <summary>
/// Gets all cameras outline data is rendered to.
/// </summary>
/// <seealso cref="Camera"/>
public ICollection<Camera> Cameras => _cameraMap.Keys;
/// <summary>
/// Updates renderer list.
/// </summary>
/// <seealso cref="OutlineRenderers"/>
public void UpdateRenderers()
{
_renderers?.Reset(false, _ignoreLayerMask);
}
#endregion
#region MonoBehaviour
private void Awake()
{
OutlineResources.LogSrpNotSupported(this);
OutlineResources.LogPpNotSupported(this);
CreateRenderersIfNeeded();
CreateSettingsIfNeeded();
}
private void OnEnable()
{
Camera.onPreRender += OnCameraPreRender;
}
private void OnDisable()
{
Camera.onPreRender -= OnCameraPreRender;
foreach (var kvp in _cameraMap)
{
if (kvp.Key)
{
kvp.Key.RemoveCommandBuffer(_cameraEvent, kvp.Value);
}
kvp.Value.Dispose();
}
_cameraMap.Clear();
}
private void Update()
{
if (_outlineResources != null && _renderers != null)
{
_camerasToRemove.Clear();
if (_updateRenderers)
{
_renderers.Reset(false, _ignoreLayerMask);
}
foreach (var kvp in _cameraMap)
{
var camera = kvp.Key;
var cmdBuffer = kvp.Value;
if (camera)
{
cmdBuffer.Clear();
FillCommandBuffer(camera, cmdBuffer);
}
else
{
cmdBuffer.Dispose();
_camerasToRemove.Add(camera);
}
}
foreach (var camera in _camerasToRemove)
{
_cameraMap.Remove(camera);
}
}
}
#if UNITY_EDITOR
private void OnValidate()
{
CreateRenderersIfNeeded();
CreateSettingsIfNeeded();
}
private void Reset()
{
if (_renderers != null)
{
_renderers.Reset(false, _ignoreLayerMask);
}
}
#endif
#endregion
#region IOutlineSettings
/// <inheritdoc/>
public Color OutlineColor
{
get
{
CreateSettingsIfNeeded();
return _outlineSettings.OutlineColor;
}
set
{
CreateSettingsIfNeeded();
_outlineSettings.OutlineColor = value;
}
}
/// <inheritdoc/>
public int OutlineWidth
{
get
{
CreateSettingsIfNeeded();
return _outlineSettings.OutlineWidth;
}
set
{
CreateSettingsIfNeeded();
_outlineSettings.OutlineWidth = value;
}
}
/// <inheritdoc/>
public float OutlineIntensity
{
get
{
CreateSettingsIfNeeded();
return _outlineSettings.OutlineIntensity;
}
set
{
CreateSettingsIfNeeded();
_outlineSettings.OutlineIntensity = value;
}
}
/// <inheritdoc/>
public float OutlineAlphaCutoff
{
get
{
CreateSettingsIfNeeded();
return _outlineSettings.OutlineAlphaCutoff;
}
set
{
CreateSettingsIfNeeded();
_outlineSettings.OutlineAlphaCutoff = value;
}
}
/// <inheritdoc/>
public OutlineRenderFlags OutlineRenderMode
{
get
{
CreateSettingsIfNeeded();
return _outlineSettings.OutlineRenderMode;
}
set
{
CreateSettingsIfNeeded();
_outlineSettings.OutlineRenderMode = value;
}
}
#endregion
#region IEquatable
/// <inheritdoc/>
public bool Equals(IOutlineSettings other)
{
return OutlineSettings.Equals(_outlineSettings, other);
}
#endregion
#region implementation
private void OnCameraPreRender(Camera camera)
{
if (camera && (!_targetCamera || _targetCamera == camera))
{
if (_outlineSettings.RequiresCameraDepth)
{
camera.depthTextureMode |= DepthTextureMode.Depth;
}
if (!_cameraMap.ContainsKey(camera))
{
var cmdBuf = new CommandBuffer();
cmdBuf.name = string.Format("{0} - {1}", GetType().Name, name);
camera.AddCommandBuffer(_cameraEvent, cmdBuf);
_cameraMap.Add(camera, cmdBuf);
#if UNITY_EDITOR
FillCommandBuffer(camera, cmdBuf);
#endif
}
}
}
private void FillCommandBuffer(Camera camera, CommandBuffer cmdBuffer)
{
if (_renderers.Count > 0)
{
using (var renderer = new OutlineRenderer(cmdBuffer, _outlineResources, camera.actualRenderingPath))
{
renderer.Render(_renderers.GetList(), _outlineSettings, name);
}
}
}
private void CreateSettingsIfNeeded()
{
if (_outlineSettings == null)
{
_outlineSettings = new OutlineSettingsInstance();
}
}
private void CreateRenderersIfNeeded()
{
if (_renderers == null)
{
_renderers = new OutlineRendererCollection(gameObject);
_renderers.Reset(false, _ignoreLayerMask);
}
}
#endregion
}
}