// 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.Generic; using System.Diagnostics; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; namespace UnityFx.Outline { /// /// This asset is used to store references to shaders and other resources needed at runtime without having to use a Resources folder. /// /// [CreateAssetMenu(fileName = "OutlineResources", menuName = "UnityFx/Outline/Outline Resources")] public sealed class OutlineResources : ScriptableObject { #region data [SerializeField] private Shader _renderShader; [SerializeField] private Shader _outlineShader; private Material _renderMaterial; private Material _outlineMaterial; private MaterialPropertyBlock _props; private Mesh _fullscreenTriangleMesh; private float[][] _gaussSamples; private bool _useDrawMesh; #endregion #region interface /// /// Minimum value of outline width parameter. /// /// public const int MinWidth = 1; /// /// Maximum value of outline width parameter. /// /// /// If the value is changed here, it should be adjusted in Outline.shader as well. /// /// public const int MaxWidth = 32; /// /// Minimum value of outline intensity parameter. /// /// /// public const int MinIntensity = 1; /// /// Maximum value of outline intensity parameter. /// /// /// public const int MaxIntensity = 64; /// /// Value of outline intensity parameter that is treated as solid fill. /// /// /// public const int SolidIntensity = 100; /// /// Minimum value of outline alpha cutoff parameter. /// /// public const float MinAlphaCutoff = 0; /// /// Maximum value of outline alpha cutoff parameter. /// /// public const float MaxAlphaCutoff = 1; /// /// Name of _MainTex shader parameter. /// public const string MainTexName = "_MainTex"; /// /// Name of _MaskTex shader parameter. /// public const string MaskTexName = "_MaskTex"; /// /// Name of _TempTex shader parameter. /// public const string TempTexName = "_TempTex"; /// /// Name of _Color shader parameter. /// public const string ColorName = "_Color"; /// /// Name of _Width shader parameter. /// public const string WidthName = "_Width"; /// /// Name of _Intensity shader parameter. /// public const string IntensityName = "_Intensity"; /// /// Name of _Cutoff shader parameter. /// public const string AlphaCutoffName = "_Cutoff"; /// /// Name of _GaussSamples shader parameter. /// public const string GaussSamplesName = "_GaussSamples"; /// /// Name of the _USE_DRAWMESH shader feature. /// public const string UseDrawMeshFeatureName = "_USE_DRAWMESH"; /// /// Name of the outline effect. /// public const string EffectName = "Outline"; /// /// Tooltip text for field. /// public const string OutlineResourcesTooltip = "Outline resources to use (shaders, materials etc). Do not change defaults unless you know what you're doing."; /// /// Tooltip text for field. /// public const string OutlineLayerCollectionTooltip = "Collection of outline layers to use. This can be used to share outline settings between multiple cameras."; /// /// Tooltip text for outline field. /// public const string OutlineLayerMaskTooltip = "Layer mask for outined objects."; /// /// Tooltip text for outline field. /// public const string OutlineRenderingLayerMaskTooltip = "Rendering layer mask for outined objects."; /// /// Index of the default pass in . /// public const int RenderShaderDefaultPassId = 0; /// /// Index of the alpha-test pass in . /// public const int RenderShaderAlphaTestPassId = 1; /// /// Index of the HPass in . /// public const int OutlineShaderHPassId = 0; /// /// Index of the VPass in . /// public const int OutlineShaderVPassId = 1; /// /// SRP not supported message. /// internal const string SrpNotSupported = "{0} works with built-in render pipeline only. It does not support SRP (including URP and HDRP)."; /// /// Post-processing not supported message. /// internal const string PpNotSupported = "{0} does not support Unity Post-processing stack v2. It might not work as expected."; /// /// Hashed name of _MainTex shader parameter. /// public readonly int MainTexId = Shader.PropertyToID(MainTexName); /// /// Texture identifier for _MainTex shader parameter. /// public readonly RenderTargetIdentifier MainTex = new RenderTargetIdentifier(MainTexName); /// /// Hashed name of _MaskTex shader parameter. /// public readonly int MaskTexId = Shader.PropertyToID(MaskTexName); /// /// Texture identifier for _MaskTex shader parameter. /// public readonly RenderTargetIdentifier MaskTex = new RenderTargetIdentifier(MaskTexName); /// /// Hashed name of _TempTex shader parameter. /// public readonly int TempTexId = Shader.PropertyToID(TempTexName); /// /// Texture identifier for _TempTex shader parameter. /// public readonly RenderTargetIdentifier TempTex = new RenderTargetIdentifier(TempTexName); /// /// Hashed name of _Color shader parameter. /// public readonly int ColorId = Shader.PropertyToID(ColorName); /// /// Hashed name of _Width shader parameter. /// public readonly int WidthId = Shader.PropertyToID(WidthName); /// /// Hashed name of _Intensity shader parameter. /// public readonly int IntensityId = Shader.PropertyToID(IntensityName); /// /// Hashed name of _Cutoff shader parameter. /// public readonly int AlphaCutoffId = Shader.PropertyToID(AlphaCutoffName); /// /// Hashed name of _GaussSamples shader parameter. /// public readonly int GaussSamplesId = Shader.PropertyToID(GaussSamplesName); /// /// Temp materials list. Used by to avoid GC allocations. /// internal readonly List TmpMaterials = new List(); /// /// Gets a that renders objects outlined with a solid while color. /// public Shader RenderShader { get { return _renderShader; } } /// /// Gets a that renders outline around the mask, that was generated with . /// public Shader OutlineShader { get { return _outlineShader; } } /// /// Gets a -based material. /// public Material RenderMaterial { get { if (_renderMaterial == null) { UnityEngine.Debug.Assert(_renderShader != null, "No RenderShader is set in outline resources.", this); _renderMaterial = new Material(_renderShader) { name = "Outline - RenderColor", hideFlags = HideFlags.HideAndDontSave }; } return _renderMaterial; } } /// /// Gets a -based material. /// public Material OutlineMaterial { get { if (_outlineMaterial == null) { UnityEngine.Debug.Assert(_outlineShader != null, "No OutlineShader is set in outline resources.", this); _outlineMaterial = new Material(_outlineShader) { name = "Outline - Main", hideFlags = HideFlags.HideAndDontSave }; if (_useDrawMesh) { _outlineMaterial.EnableKeyword(UseDrawMeshFeatureName); } } return _outlineMaterial; } } /// /// Gets a for . /// public MaterialPropertyBlock Properties { get { if (_props is null) { _props = new MaterialPropertyBlock(); } return _props; } } /// /// Gets or sets a fullscreen triangle mesh. The mesh is lazy-initialized on the first access. /// /// /// This is used by to avoid Blit() calls and use DrawMesh() passing /// this mesh as the first argument. When running on a device with Shader Model 3.5 support this /// should not be used at all, as the vertices are generated in vertex shader with DrawProcedural() call. /// /// public Mesh FullscreenTriangleMesh { get { if (_fullscreenTriangleMesh == null) { _fullscreenTriangleMesh = new Mesh() { name = "Outline - FullscreenTriangle", hideFlags = HideFlags.HideAndDontSave, vertices = new Vector3[] { new Vector3(-1, -1, 0), new Vector3(3, -1, 0), new Vector3(-1, 3, 0) }, triangles = new int[] { 0, 1, 2 } }; _fullscreenTriangleMesh.UploadMeshData(true); } return _fullscreenTriangleMesh; } set { _fullscreenTriangleMesh = value; } } /// /// Gets or sets a value indicating whether is used for image effects rendering even when procedural rendering is available. /// public bool UseFullscreenTriangleMesh { get { return _useDrawMesh; } set { if (_useDrawMesh != value) { _useDrawMesh = value; if (_outlineMaterial) { if (_useDrawMesh) { _outlineMaterial.EnableKeyword(UseDrawMeshFeatureName); } else { _outlineMaterial.DisableKeyword(UseDrawMeshFeatureName); } } } } } /// /// Gets a value indicating whether the instance is in valid state. /// public bool IsValid => RenderShader && OutlineShader; /// /// Returns a instance initialized with values from . /// public MaterialPropertyBlock GetProperties(IOutlineSettings settings) { if (_props is null) { _props = new MaterialPropertyBlock(); } _props.SetFloat(WidthId, settings.OutlineWidth); _props.SetColor(ColorId, settings.OutlineColor); if ((settings.OutlineRenderMode & OutlineRenderFlags.Blurred) != 0) { _props.SetFloat(IntensityId, settings.OutlineIntensity); } else { _props.SetFloat(IntensityId, SolidIntensity); } return _props; } /// /// Gets cached gauss samples for the specified outline . /// public float[] GetGaussSamples(int width) { var index = Mathf.Clamp(width, 1, MaxWidth) - 1; if (_gaussSamples is null) { _gaussSamples = new float[MaxWidth][]; } if (_gaussSamples[index] is null) { _gaussSamples[index] = GetGaussSamples(width, null); } return _gaussSamples[index]; } /// /// Resets the resources to defaults. /// public void ResetToDefaults() { _renderShader = Shader.Find("Hidden/UnityFx/OutlineColor"); _outlineShader = Shader.Find("Hidden/UnityFx/Outline"); } /// /// Calculates value of Gauss function for the specified and values. /// /// /// public static float Gauss(float x, float stdDev) { var stdDev2 = stdDev * stdDev * 2; var a = 1 / Mathf.Sqrt(Mathf.PI * stdDev2); var gauss = a * Mathf.Pow((float)Math.E, -x * x / stdDev2); return gauss; } /// /// Samples Gauss function for the specified . /// /// public static float[] GetGaussSamples(int width, float[] samples) { // NOTE: According to '3 sigma' rule there is no reason to have StdDev less then width / 3. // In practice blur looks best when StdDev is within range [width / 3, width / 2]. var stdDev = width * 0.5f; if (samples is null) { samples = new float[MaxWidth]; } for (var i = 0; i < width; i++) { samples[i] = Gauss(i, stdDev); } return samples; } /// /// Writes a console warning if SRP is detected. /// public static void LogSrpNotSupported(UnityEngine.Object obj) { if (GraphicsSettings.renderPipelineAsset) { UnityEngine.Debug.LogWarningFormat(obj, SrpNotSupported, obj.GetType().Name); } } /// /// Writes a console warning if Post Processing Stack v2 is detected. /// [Conditional("UNITY_POST_PROCESSING_STACK_V2")] public static void LogPpNotSupported(UnityEngine.Object obj) { UnityEngine.Debug.LogWarningFormat(obj, PpNotSupported, obj.GetType().Name); } #endregion #region ScriptableObject private void OnValidate() { if (_renderMaterial) { _renderMaterial.shader = _renderShader; } if (_outlineMaterial) { _outlineMaterial.shader = _outlineShader; } } #endregion } }