testss
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
[VolumeParameterDrawer(typeof(ColorParameter))]
|
||||
sealed class ColorParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Color)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<ColorParameter>();
|
||||
value.colorValue = EditorGUILayout.ColorField(title, value.colorValue, o.showEyeDropper, o.showAlpha, o.hdr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
[VolumeParameterDrawer(typeof(CubemapParameter))]
|
||||
sealed class CubemapParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.ObjectReference)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<CubemapParameter>();
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newTexture = EditorGUILayout.ObjectField(title, value.objectReferenceValue, typeof(Texture), false) as Texture;
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
// Texture field can accept any texture (to allow Cube Render Texture) but we still check the dimension to avoid errors
|
||||
if (newTexture != null && newTexture.dimension == TextureDimension.Cube)
|
||||
value.objectReferenceValue = newTexture;
|
||||
else
|
||||
{
|
||||
if (newTexture != null)
|
||||
Debug.LogError($"{newTexture} is not a Cubemap. Only textures of Cubemap dimension can be assigned to this field.");
|
||||
value.objectReferenceValue = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(NoInterpCubemapParameter))]
|
||||
sealed class NoInterpCubemapParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.ObjectReference)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<NoInterpCubemapParameter>();
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newTexture = EditorGUILayout.ObjectField(title, value.objectReferenceValue, typeof(Texture), false) as Texture;
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
// Texture field can accept any texture (to allow Cube Render Texture) but we still check the dimension to avoid errors
|
||||
if (newTexture.dimension == TextureDimension.Cube)
|
||||
value.objectReferenceValue = newTexture;
|
||||
else
|
||||
{
|
||||
Debug.LogError($"{newTexture} is not a Cubemap. Only textures of Cubemap dimension can be assigned to this field.");
|
||||
value.objectReferenceValue = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,144 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
[VolumeParameterDrawer(typeof(MinFloatParameter))]
|
||||
sealed class MinFloatParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Float)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<MinFloatParameter>();
|
||||
float v = EditorGUILayout.FloatField(title, value.floatValue);
|
||||
value.floatValue = Mathf.Max(v, o.min);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(NoInterpMinFloatParameter))]
|
||||
sealed class NoInterpMinFloatParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Float)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<NoInterpMinFloatParameter>();
|
||||
float v = EditorGUILayout.FloatField(title, value.floatValue);
|
||||
value.floatValue = Mathf.Max(v, o.min);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(MaxFloatParameter))]
|
||||
sealed class MaxFloatParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Float)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<MaxFloatParameter>();
|
||||
float v = EditorGUILayout.FloatField(title, value.floatValue);
|
||||
value.floatValue = Mathf.Min(v, o.max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(NoInterpMaxFloatParameter))]
|
||||
sealed class NoInterpMaxFloatParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Float)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<NoInterpMaxFloatParameter>();
|
||||
float v = EditorGUILayout.FloatField(title, value.floatValue);
|
||||
value.floatValue = Mathf.Min(v, o.max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(ClampedFloatParameter))]
|
||||
sealed class ClampedFloatParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Float)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<ClampedFloatParameter>();
|
||||
EditorGUILayout.Slider(value, o.min, o.max, title);
|
||||
value.floatValue = Mathf.Clamp(value.floatValue, o.min, o.max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(NoInterpClampedFloatParameter))]
|
||||
sealed class NoInterpClampedFloatParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Float)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<NoInterpClampedFloatParameter>();
|
||||
EditorGUILayout.Slider(value, o.min, o.max, title);
|
||||
value.floatValue = Mathf.Clamp(value.floatValue, o.min, o.max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(FloatRangeParameter))]
|
||||
sealed class FloatRangeParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Vector2)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<FloatRangeParameter>();
|
||||
var v = value.vector2Value;
|
||||
|
||||
// The layout system breaks alignement when mixing inspector fields with custom layouted
|
||||
// fields as soon as a scrollbar is needed in the inspector, so we'll do the layout
|
||||
// manually instead
|
||||
const int kFloatFieldWidth = 50;
|
||||
const int kSeparatorWidth = 5;
|
||||
float indentOffset = EditorGUI.indentLevel * 15f;
|
||||
var lineRect = GUILayoutUtility.GetRect(1, EditorGUIUtility.singleLineHeight);
|
||||
lineRect.xMin += 4f;
|
||||
lineRect.y += 2f;
|
||||
var labelRect = new Rect(lineRect.x, lineRect.y, EditorGUIUtility.labelWidth - indentOffset, lineRect.height);
|
||||
var floatFieldLeft = new Rect(labelRect.xMax, lineRect.y, kFloatFieldWidth + indentOffset, lineRect.height);
|
||||
var sliderRect = new Rect(floatFieldLeft.xMax + kSeparatorWidth - indentOffset, lineRect.y, lineRect.width - labelRect.width - kFloatFieldWidth * 2 - kSeparatorWidth * 2, lineRect.height);
|
||||
var floatFieldRight = new Rect(sliderRect.xMax + kSeparatorWidth - indentOffset, lineRect.y, kFloatFieldWidth + indentOffset, lineRect.height);
|
||||
|
||||
EditorGUI.PrefixLabel(labelRect, title);
|
||||
v.x = EditorGUI.FloatField(floatFieldLeft, v.x);
|
||||
EditorGUI.MinMaxSlider(sliderRect, ref v.x, ref v.y, o.min, o.max);
|
||||
v.y = EditorGUI.FloatField(floatFieldRight, v.y);
|
||||
|
||||
value.vector2Value = v;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,123 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEditorInternal;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
[VolumeParameterDrawer(typeof(MinIntParameter))]
|
||||
sealed class MinIntParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Integer)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<MinIntParameter>();
|
||||
int v = EditorGUILayout.IntField(title, value.intValue);
|
||||
value.intValue = Mathf.Max(v, o.min);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(NoInterpMinIntParameter))]
|
||||
sealed class NoInterpMinIntParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Integer)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<NoInterpMinIntParameter>();
|
||||
int v = EditorGUILayout.IntField(title, value.intValue);
|
||||
value.intValue = Mathf.Max(v, o.min);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(MaxIntParameter))]
|
||||
sealed class MaxIntParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Integer)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<MaxIntParameter>();
|
||||
int v = EditorGUILayout.IntField(title, value.intValue);
|
||||
value.intValue = Mathf.Min(v, o.max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(NoInterpMaxIntParameter))]
|
||||
sealed class NoInterpMaxIntParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Integer)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<NoInterpMaxIntParameter>();
|
||||
int v = EditorGUILayout.IntField(title, value.intValue);
|
||||
value.intValue = Mathf.Min(v, o.max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(ClampedIntParameter))]
|
||||
sealed class ClampedIntParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Integer)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<ClampedIntParameter>();
|
||||
EditorGUILayout.IntSlider(value, o.min, o.max, title);
|
||||
value.intValue = Mathf.Clamp(value.intValue, o.min, o.max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(NoInterpClampedIntParameter))]
|
||||
sealed class NoInterpClampedIntParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Integer)
|
||||
return false;
|
||||
|
||||
var o = parameter.GetObjectRef<NoInterpClampedIntParameter>();
|
||||
EditorGUILayout.IntSlider(value, o.min, o.max, title);
|
||||
value.intValue = Mathf.Clamp(value.intValue, o.min, o.max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[VolumeParameterDrawer(typeof(LayerMaskParameter))]
|
||||
sealed class LayerMaskParameterDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.LayerMask)
|
||||
return false;
|
||||
|
||||
value.intValue = EditorGUILayout.MaskField(title, value.intValue, InternalEditorUtility.layers);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
[VolumeParameterDrawer(typeof(Vector4Parameter))]
|
||||
sealed class Vector4ParametrDrawer : VolumeParameterDrawer
|
||||
{
|
||||
public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
{
|
||||
var value = parameter.value;
|
||||
|
||||
if (value.propertyType != SerializedPropertyType.Vector4)
|
||||
return false;
|
||||
|
||||
value.vector4Value = EditorGUILayout.Vector4Field(title, value.vector4Value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine.Assertions;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// A serialization wrapper for <see cref="VolumeParameter{T}"/>.
|
||||
/// </summary>
|
||||
public sealed class SerializedDataParameter
|
||||
{
|
||||
/// <summary>
|
||||
/// The serialized property for <see cref="VolumeParameter.overrideState"/>.
|
||||
/// </summary>
|
||||
public SerializedProperty overrideState { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The serialized property for <see cref="VolumeParameter{T}.value"/>
|
||||
/// </summary>
|
||||
public SerializedProperty value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A pre-fetched list of all the attributes applied on the <see cref="VolumeParameter{T}"/>.
|
||||
/// </summary>
|
||||
public Attribute[] attributes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The actual type of the serialized <see cref="VolumeParameter{T}"/>.
|
||||
/// </summary>
|
||||
public Type referenceType { get; private set; }
|
||||
|
||||
SerializedProperty m_BaseProperty;
|
||||
object m_ReferenceValue;
|
||||
|
||||
/// <summary>
|
||||
/// The generated display name of the <see cref="VolumeParameter{T}"/> for the inspector.
|
||||
/// </summary>
|
||||
public string displayName => m_BaseProperty.displayName;
|
||||
|
||||
internal SerializedDataParameter(SerializedProperty property)
|
||||
{
|
||||
// Find the actual property type, optional attributes & reference
|
||||
var path = property.propertyPath.Split('.');
|
||||
object obj = property.serializedObject.targetObject;
|
||||
FieldInfo field = null;
|
||||
|
||||
foreach (var p in path)
|
||||
{
|
||||
field = obj.GetType().GetField(p, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
obj = field.GetValue(obj);
|
||||
}
|
||||
|
||||
Assert.IsNotNull(field);
|
||||
|
||||
m_BaseProperty = property.Copy();
|
||||
overrideState = m_BaseProperty.FindPropertyRelative("m_OverrideState");
|
||||
value = m_BaseProperty.FindPropertyRelative("m_Value");
|
||||
attributes = field.GetCustomAttributes(false).Cast<Attribute>().ToArray();
|
||||
referenceType = obj.GetType();
|
||||
m_ReferenceValue = obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets and casts an attribute applied on the base <see cref="VolumeParameter{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetAttribute<T>()
|
||||
where T : Attribute
|
||||
{
|
||||
return (T)attributes.FirstOrDefault(x => x is T);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets and casts the underlying reference of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to cast to</typeparam>
|
||||
/// <returns>The reference to the serialized <see cref="VolumeParameter{T}"/> type</returns>
|
||||
public T GetObjectRef<T>()
|
||||
{
|
||||
return (T)m_ReferenceValue;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,529 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor.AnimatedValues;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// This attributes tells a <see cref="VolumeComponentEditor"/> class which type of
|
||||
/// <see cref="VolumeComponent"/> it's an editor for.
|
||||
/// When you make a custom editor for a component, you need put this attribute on the editor
|
||||
/// class.
|
||||
/// </summary>
|
||||
/// <seealso cref="VolumeComponentEditor"/>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public sealed class VolumeComponentEditorAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// A type derived from <see cref="VolumeComponent"/>.
|
||||
/// </summary>
|
||||
public readonly Type componentType;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="VolumeComponentEditorAttribute"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="componentType">A type derived from <see cref="VolumeComponent"/></param>
|
||||
public VolumeComponentEditorAttribute(Type componentType)
|
||||
{
|
||||
this.componentType = componentType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom editor class that draws a <see cref="VolumeComponent"/> in the Inspector. If you do not
|
||||
/// provide a custom editor for a <see cref="VolumeComponent"/>, Unity uses the default one.
|
||||
/// You must use a <see cref="VolumeComponentEditorAttribute"/> to let the editor know which
|
||||
/// component this drawer is for.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// Below is an example of a custom <see cref="VolumeComponent"/>:
|
||||
/// <code>
|
||||
/// using UnityEngine.Rendering;
|
||||
///
|
||||
/// [Serializable, VolumeComponentMenu("Custom/Example Component")]
|
||||
/// public class ExampleComponent : VolumeComponent
|
||||
/// {
|
||||
/// public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);
|
||||
/// }
|
||||
/// </code>
|
||||
/// And its associated editor:
|
||||
/// <code>
|
||||
/// using UnityEditor.Rendering;
|
||||
///
|
||||
/// [VolumeComponentEditor(typeof(ExampleComponent))]
|
||||
/// class ExampleComponentEditor : VolumeComponentEditor
|
||||
/// {
|
||||
/// SerializedDataParameter m_Intensity;
|
||||
///
|
||||
/// public override void OnEnable()
|
||||
/// {
|
||||
/// var o = new PropertyFetcher<ExampleComponent>(serializedObject);
|
||||
/// m_Intensity = Unpack(o.Find(x => x.intensity));
|
||||
/// }
|
||||
///
|
||||
/// public override void OnInspectorGUI()
|
||||
/// {
|
||||
/// PropertyField(m_Intensity);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <seealso cref="VolumeComponentEditorAttribute"/>
|
||||
public class VolumeComponentEditor
|
||||
{
|
||||
class Styles
|
||||
{
|
||||
public static GUIContent overrideSettingText { get; } = EditorGUIUtility.TrTextContent("", "Override this setting for this volume.");
|
||||
public static GUIContent allText { get; } = EditorGUIUtility.TrTextContent("ALL", "Toggle all overrides on. To maximize performances you should only toggle overrides that you actually need.");
|
||||
public static GUIContent noneText { get; } = EditorGUIUtility.TrTextContent("NONE", "Toggle all overrides off.");
|
||||
|
||||
public static string toggleAllText { get; } = L10n.Tr("Toggle All");
|
||||
}
|
||||
|
||||
Vector2? m_OverrideToggleSize;
|
||||
internal Vector2 overrideToggleSize
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!m_OverrideToggleSize.HasValue)
|
||||
m_OverrideToggleSize = CoreEditorStyles.smallTickbox.CalcSize(Styles.overrideSettingText);
|
||||
return m_OverrideToggleSize.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the <see cref="VolumeComponent"/> this editor is drawing.
|
||||
/// </summary>
|
||||
public VolumeComponent target { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A <c>SerializedObject</c> representing the object being inspected.
|
||||
/// </summary>
|
||||
public SerializedObject serializedObject { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The copy of the serialized property of the <see cref="VolumeComponent"/> being
|
||||
/// inspected. Unity uses this to track whether the editor is collapsed in the Inspector or not.
|
||||
/// </summary>
|
||||
public SerializedProperty baseProperty { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The serialized property of <see cref="VolumeComponent.active"/> for the component being
|
||||
/// inspected.
|
||||
/// </summary>
|
||||
public SerializedProperty activeProperty { get; internal set; }
|
||||
|
||||
SerializedProperty m_AdvancedMode;
|
||||
|
||||
List<VolumeParameter> m_VolumeNotAdditionalParameters;
|
||||
/// <summary>
|
||||
/// Override this property if your editor makes use of the "More Options" feature.
|
||||
/// </summary>
|
||||
public virtual bool hasAdvancedMode => target.parameters.Count != m_VolumeNotAdditionalParameters.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the editor currently has the "More Options" feature toggled on.
|
||||
/// </summary>
|
||||
public bool isInAdvancedMode
|
||||
{
|
||||
get => m_AdvancedMode != null && m_AdvancedMode.boolValue;
|
||||
internal set
|
||||
{
|
||||
if (m_AdvancedMode != null)
|
||||
{
|
||||
m_AdvancedMode.boolValue = value;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A reference to the parent editor in the Inspector.
|
||||
/// </summary>
|
||||
protected Editor m_Inspector;
|
||||
|
||||
List<(GUIContent displayName, int displayOrder, SerializedDataParameter param)> m_Parameters;
|
||||
|
||||
static Dictionary<Type, VolumeParameterDrawer> s_ParameterDrawers;
|
||||
|
||||
static VolumeComponentEditor()
|
||||
{
|
||||
s_ParameterDrawers = new Dictionary<Type, VolumeParameterDrawer>();
|
||||
ReloadDecoratorTypes();
|
||||
}
|
||||
|
||||
[Callbacks.DidReloadScripts]
|
||||
static void OnEditorReload()
|
||||
{
|
||||
ReloadDecoratorTypes();
|
||||
}
|
||||
|
||||
static void ReloadDecoratorTypes()
|
||||
{
|
||||
s_ParameterDrawers.Clear();
|
||||
|
||||
// Look for all the valid parameter drawers
|
||||
var types = CoreUtils.GetAllTypesDerivedFrom<VolumeParameterDrawer>()
|
||||
.Where(
|
||||
t => t.IsDefined(typeof(VolumeParameterDrawerAttribute), false)
|
||||
&& !t.IsAbstract
|
||||
);
|
||||
|
||||
// Store them
|
||||
foreach (var type in types)
|
||||
{
|
||||
var attr = (VolumeParameterDrawerAttribute)type.GetCustomAttributes(typeof(VolumeParameterDrawerAttribute), false)[0];
|
||||
var decorator = (VolumeParameterDrawer)Activator.CreateInstance(type);
|
||||
s_ParameterDrawers.Add(attr.parameterType, decorator);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers an Inspector repaint event.
|
||||
/// </summary>
|
||||
public void Repaint()
|
||||
{
|
||||
m_Inspector.Repaint();
|
||||
}
|
||||
|
||||
internal void Init(VolumeComponent target, Editor inspector)
|
||||
{
|
||||
this.target = target;
|
||||
m_Inspector = inspector;
|
||||
serializedObject = new SerializedObject(target);
|
||||
activeProperty = serializedObject.FindProperty("active");
|
||||
m_AdvancedMode = serializedObject.FindProperty("m_AdvancedMode");
|
||||
|
||||
InitParameters();
|
||||
|
||||
OnEnable();
|
||||
}
|
||||
|
||||
void InitParameters()
|
||||
{
|
||||
m_VolumeNotAdditionalParameters = new List<VolumeParameter>();
|
||||
VolumeComponent.FindParameters(target, m_VolumeNotAdditionalParameters, field => field.GetCustomAttribute<AdditionalPropertyAttribute>() == null);
|
||||
}
|
||||
|
||||
void GetFields(object o, List<(FieldInfo, SerializedProperty)> infos, SerializedProperty prop = null)
|
||||
{
|
||||
if (o == null)
|
||||
return;
|
||||
|
||||
var fields = o.GetType()
|
||||
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (field.FieldType.IsSubclassOf(typeof(VolumeParameter)))
|
||||
{
|
||||
if ((field.GetCustomAttributes(typeof(HideInInspector), false).Length == 0) &&
|
||||
((field.GetCustomAttributes(typeof(SerializeField), false).Length > 0) ||
|
||||
(field.IsPublic && field.GetCustomAttributes(typeof(NonSerializedAttribute), false).Length == 0)))
|
||||
infos.Add((field, prop == null ?
|
||||
serializedObject.FindProperty(field.Name) : prop.FindPropertyRelative(field.Name)));
|
||||
}
|
||||
else if (!field.FieldType.IsArray && field.FieldType.IsClass)
|
||||
GetFields(field.GetValue(o), infos, prop == null ?
|
||||
serializedObject.FindProperty(field.Name) : prop.FindPropertyRelative(field.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unity calls this method when the object loads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You can safely override this method and not call <c>base.OnEnable()</c> unless you want
|
||||
/// Unity to display all the properties from the <see cref="VolumeComponent"/> automatically.
|
||||
/// </remarks>
|
||||
public virtual void OnEnable()
|
||||
{
|
||||
// Grab all valid serializable field on the VolumeComponent
|
||||
// TODO: Should only be done when needed / on demand as this can potentially be wasted CPU when a custom editor is in use
|
||||
var fields = new List<(FieldInfo, SerializedProperty)>();
|
||||
GetFields(target, fields);
|
||||
|
||||
m_Parameters = fields
|
||||
.Select(t => {
|
||||
var name = "";
|
||||
var order = 0;
|
||||
var attr = (DisplayInfoAttribute[])t.Item1.GetCustomAttributes(typeof(DisplayInfoAttribute), true);
|
||||
if (attr.Length != 0)
|
||||
{
|
||||
name = attr[0].name;
|
||||
order = attr[0].order;
|
||||
}
|
||||
|
||||
var parameter = new SerializedDataParameter(t.Item2);
|
||||
return (new GUIContent(name), order, parameter);
|
||||
})
|
||||
.OrderBy(t => t.order)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unity calls this method when the object goes out of scope.
|
||||
/// </summary>
|
||||
public virtual void OnDisable()
|
||||
{
|
||||
}
|
||||
|
||||
internal void OnInternalInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
TopRowFields();
|
||||
OnInspectorGUI();
|
||||
EditorGUILayout.Space();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unity calls this method everytime it re-draws the Inspector.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You can safely override this method and not call <c>base.OnInspectorGUI()</c> unless you
|
||||
/// want Unity to display all the properties from the <see cref="VolumeComponent"/>
|
||||
/// automatically.
|
||||
/// </remarks>
|
||||
public virtual void OnInspectorGUI()
|
||||
{
|
||||
// Display every field as-is
|
||||
foreach (var parameter in m_Parameters)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(parameter.displayName.text))
|
||||
PropertyField(parameter.param, parameter.displayName);
|
||||
else
|
||||
PropertyField(parameter.param);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the label for the component header. Override this method to provide
|
||||
/// a custom label. If you don't, Unity automatically inferres one from the class name.
|
||||
/// </summary>
|
||||
/// <returns>A label to display in the component header.</returns>
|
||||
public virtual string GetDisplayTitle()
|
||||
{
|
||||
return target.displayName == "" ? ObjectNames.NicifyVariableName(target.GetType().Name) : target.displayName;
|
||||
}
|
||||
|
||||
void AddToogleState(GUIContent content, bool state)
|
||||
{
|
||||
bool allOverridesSameState = AreOverridesTo(state);
|
||||
if (GUILayout.Toggle(allOverridesSameState, content, CoreEditorStyles.miniLabelButton, GUILayout.ExpandWidth(false)) && !allOverridesSameState)
|
||||
SetOverridesTo(state);
|
||||
}
|
||||
|
||||
void TopRowFields()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
AddToogleState(Styles.allText, true);
|
||||
AddToogleState(Styles.noneText, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all the visible parameters have the given state
|
||||
/// </summary>
|
||||
/// <param name="state">The state to check</param>
|
||||
internal bool AreOverridesTo(bool state)
|
||||
{
|
||||
if (hasAdvancedMode && isInAdvancedMode)
|
||||
return AreAllOverridesTo(state);
|
||||
|
||||
for (int i = 0; i < m_VolumeNotAdditionalParameters.Count; ++i)
|
||||
{
|
||||
if (m_VolumeNotAdditionalParameters[i].overrideState != state)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the given state to all the visible parameters
|
||||
/// </summary>
|
||||
/// <param name="state">The state to check</param>
|
||||
internal void SetOverridesTo(bool state)
|
||||
{
|
||||
if (hasAdvancedMode && isInAdvancedMode)
|
||||
SetAllOverridesTo(state);
|
||||
else
|
||||
{
|
||||
Undo.RecordObject(target, Styles.toggleAllText);
|
||||
target.SetOverridesTo(m_VolumeNotAdditionalParameters, state);
|
||||
serializedObject.Update();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool AreAllOverridesTo(bool state)
|
||||
{
|
||||
for (int i = 0; i < target.parameters.Count; ++i)
|
||||
{
|
||||
if (target.parameters[i].overrideState != state)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void SetAllOverridesTo(bool state)
|
||||
{
|
||||
Undo.RecordObject(target, Styles.toggleAllText);
|
||||
target.SetAllOverridesTo(state);
|
||||
serializedObject.Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates and auto-populates a <see cref="SerializedDataParameter"/> from a serialized
|
||||
/// <see cref="VolumeParameter{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="property">A serialized property holding a <see cref="VolumeParameter{T}"/>
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
protected SerializedDataParameter Unpack(SerializedProperty property)
|
||||
{
|
||||
Assert.IsNotNull(property);
|
||||
return new SerializedDataParameter(property);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a given <see cref="SerializedDataParameter"/> in the editor.
|
||||
/// </summary>
|
||||
/// <param name="property">The property to draw in the editor</param>
|
||||
protected void PropertyField(SerializedDataParameter property)
|
||||
{
|
||||
var title = EditorGUIUtility.TrTextContent(property.displayName, property.GetAttribute<TooltipAttribute>()?.tooltip);
|
||||
PropertyField(property, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles unity built-in decorators (Space, Header, Tooltips, ...) from <see cref="SerializedDataParameter"/> attributes
|
||||
/// </summary>
|
||||
/// <param name="property">The property to obtain the attributes and handle the decorators</param>
|
||||
/// <param name="title">A custom label and/or tooltip that might be updated by <see cref="TooltipAttribute"/> and/or by <see cref="InspectorNameAttribute"/></param>
|
||||
void HandleDecorators(SerializedDataParameter property, GUIContent title)
|
||||
{
|
||||
foreach (var attr in property.attributes)
|
||||
{
|
||||
if (!(attr is PropertyAttribute))
|
||||
continue;
|
||||
|
||||
switch (attr)
|
||||
{
|
||||
case SpaceAttribute spaceAttribute:
|
||||
EditorGUILayout.GetControlRect(false, spaceAttribute.height);
|
||||
break;
|
||||
case HeaderAttribute headerAttribute:
|
||||
{
|
||||
var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight));
|
||||
EditorGUI.LabelField(rect, headerAttribute.header, EditorStyles.miniLabel);
|
||||
break;
|
||||
}
|
||||
case TooltipAttribute tooltipAttribute:
|
||||
{
|
||||
if (string.IsNullOrEmpty(title.tooltip))
|
||||
title.tooltip = tooltipAttribute.tooltip;
|
||||
break;
|
||||
}
|
||||
case InspectorNameAttribute inspectorNameAttribute:
|
||||
title.text = inspectorNameAttribute.displayName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a given <see cref="SerializedDataParameter"/> in the editor using a custom label
|
||||
/// and tooltip.
|
||||
/// </summary>
|
||||
/// <param name="property">The property to draw in the editor.</param>
|
||||
/// <param name="title">A custom label and/or tooltip.</param>
|
||||
protected void PropertyField(SerializedDataParameter property, GUIContent title)
|
||||
{
|
||||
HandleDecorators(property, title);
|
||||
|
||||
// Custom parameter drawer
|
||||
VolumeParameterDrawer drawer;
|
||||
s_ParameterDrawers.TryGetValue(property.referenceType, out drawer);
|
||||
|
||||
bool invalidProp = false;
|
||||
|
||||
if (drawer != null && !drawer.IsAutoProperty())
|
||||
{
|
||||
if (drawer.OnGUI(property, title))
|
||||
return;
|
||||
|
||||
invalidProp = true;
|
||||
}
|
||||
|
||||
// ObjectParameter<T> is a special case
|
||||
if (VolumeParameter.IsObjectParameter(property.referenceType))
|
||||
{
|
||||
bool expanded = property.value.isExpanded;
|
||||
expanded = EditorGUILayout.Foldout(expanded, title, true);
|
||||
|
||||
if (expanded)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
// Not the fastest way to do it but that'll do just fine for now
|
||||
var it = property.value.Copy();
|
||||
var end = it.GetEndProperty();
|
||||
bool first = true;
|
||||
|
||||
while (it.Next(first) && !SerializedProperty.EqualContents(it, end))
|
||||
{
|
||||
PropertyField(Unpack(it));
|
||||
first = false;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
property.value.isExpanded = expanded;
|
||||
return;
|
||||
}
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
// Override checkbox
|
||||
DrawOverrideCheckbox(property);
|
||||
|
||||
// Property
|
||||
using (new EditorGUI.DisabledScope(!property.overrideState.boolValue))
|
||||
{
|
||||
if (drawer != null && !invalidProp)
|
||||
{
|
||||
if (drawer.OnGUI(property, title))
|
||||
return;
|
||||
}
|
||||
|
||||
// Default unity field
|
||||
EditorGUILayout.PropertyField(property.value, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the override checkbox used by a property in the editor.
|
||||
/// </summary>
|
||||
/// <param name="property">The property to draw the override checkbox for</param>
|
||||
protected void DrawOverrideCheckbox(SerializedDataParameter property)
|
||||
{
|
||||
// Create a rect the height + vspacing of the property that is being overriden
|
||||
float height = EditorGUI.GetPropertyHeight(property.value) + EditorGUIUtility.standardVerticalSpacing;
|
||||
var overrideRect = GUILayoutUtility.GetRect(Styles.allText, CoreEditorStyles.miniLabelButton, GUILayout.Height(height), GUILayout.ExpandWidth(false));
|
||||
|
||||
// also center vertically the checkbox
|
||||
overrideRect.yMin += height * 0.5f - overrideToggleSize.y * 0.5f;
|
||||
overrideRect.xMin += overrideToggleSize.x * 0.5f;
|
||||
|
||||
property.overrideState.boolValue = GUI.Toggle(overrideRect, property.overrideState.boolValue, Styles.overrideSettingText, CoreEditorStyles.smallTickbox);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,541 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity uses this class to draw the user interface for all the settings
|
||||
/// contained in a <see cref="VolumeProfile"/> in the Inspector.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// A minimal example of how to write a custom editor that displays the content of a profile
|
||||
/// in the inspector:
|
||||
/// <code>
|
||||
/// using UnityEngine.Rendering;
|
||||
///
|
||||
/// [CustomEditor(typeof(VolumeProfile))]
|
||||
/// public class CustomVolumeProfileEditor : Editor
|
||||
/// {
|
||||
/// VolumeComponentListEditor m_ComponentList;
|
||||
///
|
||||
/// void OnEnable()
|
||||
/// {
|
||||
/// m_ComponentList = new VolumeComponentListEditor(this);
|
||||
/// m_ComponentList.Init(target as VolumeProfile, serializedObject);
|
||||
/// }
|
||||
///
|
||||
/// void OnDisable()
|
||||
/// {
|
||||
/// if (m_ComponentList != null)
|
||||
/// m_ComponentList.Clear();
|
||||
/// }
|
||||
///
|
||||
/// public override void OnInspectorGUI()
|
||||
/// {
|
||||
/// serializedObject.Update();
|
||||
/// m_ComponentList.OnGUI();
|
||||
/// serializedObject.ApplyModifiedProperties();
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public sealed class VolumeComponentListEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// A direct reference to the <see cref="VolumeProfile"/> this editor displays.
|
||||
/// </summary>
|
||||
public VolumeProfile asset { get; private set; }
|
||||
|
||||
Editor m_BaseEditor;
|
||||
|
||||
SerializedObject m_SerializedObject;
|
||||
SerializedProperty m_ComponentsProperty;
|
||||
|
||||
Dictionary<Type, Type> m_EditorTypes; // Component type => Editor type
|
||||
List<VolumeComponentEditor> m_Editors;
|
||||
|
||||
static Dictionary<Type, string> m_EditorDocumentationURLs;
|
||||
|
||||
int m_CurrentHashCode;
|
||||
|
||||
static VolumeComponentListEditor()
|
||||
{
|
||||
ReloadDocumentation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="VolumeComponentListEditor"/> to use in an
|
||||
/// existing editor.
|
||||
/// </summary>
|
||||
/// <param name="editor">A reference to the parent editor instance</param>
|
||||
public VolumeComponentListEditor(Editor editor)
|
||||
{
|
||||
Assert.IsNotNull(editor);
|
||||
m_BaseEditor = editor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the editor.
|
||||
/// </summary>
|
||||
/// <param name="asset">A direct reference to the profile Asset.</param>
|
||||
/// <param name="serializedObject">An instance of the <see cref="SerializedObject"/>
|
||||
/// provided by the parent editor.</param>
|
||||
public void Init(VolumeProfile asset, SerializedObject serializedObject)
|
||||
{
|
||||
Assert.IsNotNull(asset);
|
||||
Assert.IsNotNull(serializedObject);
|
||||
|
||||
this.asset = asset;
|
||||
m_SerializedObject = serializedObject;
|
||||
m_ComponentsProperty = serializedObject.Find((VolumeProfile x) => x.components);
|
||||
Assert.IsNotNull(m_ComponentsProperty);
|
||||
|
||||
m_EditorTypes = new Dictionary<Type, Type>();
|
||||
m_Editors = new List<VolumeComponentEditor>();
|
||||
|
||||
// Gets the list of all available component editors
|
||||
var editorTypes = CoreUtils.GetAllTypesDerivedFrom<VolumeComponentEditor>()
|
||||
.Where(
|
||||
t => t.IsDefined(typeof(VolumeComponentEditorAttribute), false)
|
||||
&& !t.IsAbstract
|
||||
);
|
||||
|
||||
// Map them to their corresponding component type
|
||||
foreach (var editorType in editorTypes)
|
||||
{
|
||||
var attribute = (VolumeComponentEditorAttribute)editorType.GetCustomAttributes(typeof(VolumeComponentEditorAttribute), false)[0];
|
||||
m_EditorTypes.Add(attribute.componentType, editorType);
|
||||
}
|
||||
|
||||
// Create editors for existing components
|
||||
var components = asset.components;
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
CreateEditor(components[i], m_ComponentsProperty.GetArrayElementAtIndex(i));
|
||||
|
||||
// Keep track of undo/redo to redraw the inspector when that happens
|
||||
Undo.undoRedoPerformed += OnUndoRedoPerformed;
|
||||
}
|
||||
|
||||
void OnUndoRedoPerformed()
|
||||
{
|
||||
asset.isDirty = true;
|
||||
|
||||
// Dumb hack to make sure the serialized object is up to date on undo (else there'll be
|
||||
// a state mismatch when this class is used in a GameObject inspector).
|
||||
if (m_SerializedObject != null
|
||||
&& !m_SerializedObject.Equals(null)
|
||||
&& m_SerializedObject.targetObject != null
|
||||
&& !m_SerializedObject.targetObject.Equals(null))
|
||||
{
|
||||
m_SerializedObject.Update();
|
||||
m_SerializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
// Seems like there's an issue with the inspector not repainting after some undo events
|
||||
// This will take care of that
|
||||
m_BaseEditor.Repaint();
|
||||
}
|
||||
|
||||
// index is only used when we need to re-create a component in a specific spot (e.g. reset)
|
||||
void CreateEditor(VolumeComponent component, SerializedProperty property, int index = -1, bool forceOpen = false)
|
||||
{
|
||||
var componentType = component.GetType();
|
||||
Type editorType;
|
||||
|
||||
if (!m_EditorTypes.TryGetValue(componentType, out editorType))
|
||||
editorType = typeof(VolumeComponentEditor);
|
||||
|
||||
var editor = (VolumeComponentEditor)Activator.CreateInstance(editorType);
|
||||
editor.Init(component, m_BaseEditor);
|
||||
editor.baseProperty = property.Copy();
|
||||
|
||||
if (forceOpen)
|
||||
editor.baseProperty.isExpanded = true;
|
||||
|
||||
if (index < 0)
|
||||
m_Editors.Add(editor);
|
||||
else
|
||||
m_Editors[index] = editor;
|
||||
}
|
||||
|
||||
void RefreshEditors()
|
||||
{
|
||||
// Disable all editors first
|
||||
foreach (var editor in m_Editors)
|
||||
editor.OnDisable();
|
||||
|
||||
// Remove them
|
||||
m_Editors.Clear();
|
||||
|
||||
// Refresh the ref to the serialized components in case the asset got swapped or another
|
||||
// script is editing it while it's active in the inspector
|
||||
m_SerializedObject.Update();
|
||||
m_ComponentsProperty = m_SerializedObject.Find((VolumeProfile x) => x.components);
|
||||
Assert.IsNotNull(m_ComponentsProperty);
|
||||
|
||||
// Recreate editors for existing settings, if any
|
||||
var components = asset.components;
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
CreateEditor(components[i], m_ComponentsProperty.GetArrayElementAtIndex(i));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up the editor and individual <see cref="VolumeComponentEditor"/> instances. You
|
||||
/// must call this when the parent editor is disabled or destroyed.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (m_Editors == null)
|
||||
return; // Hasn't been initialized yet
|
||||
|
||||
foreach (var editor in m_Editors)
|
||||
editor.OnDisable();
|
||||
|
||||
m_Editors.Clear();
|
||||
m_EditorTypes.Clear();
|
||||
|
||||
// ReSharper disable once DelegateSubtraction
|
||||
Undo.undoRedoPerformed -= OnUndoRedoPerformed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the editor.
|
||||
/// </summary>
|
||||
public void OnGUI()
|
||||
{
|
||||
if (asset == null)
|
||||
return;
|
||||
|
||||
// Even if the asset is not dirty, the list of component may have been changed by another inspector.
|
||||
// In this case, only the hash will tell us that we need to refresh.
|
||||
if (asset.isDirty || asset.GetComponentListHashCode() != m_CurrentHashCode)
|
||||
{
|
||||
RefreshEditors();
|
||||
m_CurrentHashCode = asset.GetComponentListHashCode();
|
||||
asset.isDirty = false;
|
||||
}
|
||||
|
||||
bool isEditable = !VersionControl.Provider.isActive
|
||||
|| AssetDatabase.IsOpenForEdit(asset, StatusQueryOptions.UseCachedIfPossible);
|
||||
|
||||
using (new EditorGUI.DisabledScope(!isEditable))
|
||||
{
|
||||
// Component list
|
||||
for (int i = 0; i < m_Editors.Count; i++)
|
||||
{
|
||||
var editor = m_Editors[i];
|
||||
string title = editor.GetDisplayTitle();
|
||||
int id = i; // Needed for closure capture below
|
||||
|
||||
m_EditorDocumentationURLs.TryGetValue(editor.target.GetType(), out var documentationURL);
|
||||
|
||||
CoreEditorUtils.DrawSplitter();
|
||||
bool displayContent = CoreEditorUtils.DrawHeaderToggle(
|
||||
title,
|
||||
editor.baseProperty,
|
||||
editor.activeProperty,
|
||||
pos => OnContextClick(pos, editor.target, id),
|
||||
editor.hasAdvancedMode ? () => editor.isInAdvancedMode : (Func<bool>)null,
|
||||
() => editor.isInAdvancedMode ^= true,
|
||||
documentationURL
|
||||
);
|
||||
|
||||
if (displayContent)
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(!editor.activeProperty.boolValue))
|
||||
editor.OnInternalInspectorGUI();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Editors.Count > 0)
|
||||
CoreEditorUtils.DrawSplitter();
|
||||
else
|
||||
EditorGUILayout.HelpBox("This Volume Profile contains no overrides.", MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
using (var hscope = new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button(EditorGUIUtility.TrTextContent("Add Override"), EditorStyles.miniButton))
|
||||
{
|
||||
var r = hscope.rect;
|
||||
var pos = new Vector2(r.x + r.width / 2f, r.yMax + 18f);
|
||||
FilterWindow.Show(pos, new VolumeComponentProvider(asset, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnContextClick(Vector2 position, VolumeComponent targetComponent, int id)
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
menu.AddDisabledItem(EditorGUIUtility.TrTextContent("Move Up"));
|
||||
menu.AddDisabledItem(EditorGUIUtility.TrTextContent("Move to Top"));
|
||||
}
|
||||
else
|
||||
{
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Move to Top"), false, () => MoveComponent(id, -id));
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Move Up"), false, () => MoveComponent(id, -1));
|
||||
}
|
||||
|
||||
if (id == m_Editors.Count - 1)
|
||||
{
|
||||
menu.AddDisabledItem(EditorGUIUtility.TrTextContent("Move to Bottom"));
|
||||
menu.AddDisabledItem(EditorGUIUtility.TrTextContent("Move Down"));
|
||||
}
|
||||
else
|
||||
{
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Move to Bottom"), false, () => MoveComponent(id, (m_Editors.Count - 1) - id));
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Move Down"), false, () => MoveComponent(id, 1));
|
||||
}
|
||||
|
||||
menu.AddSeparator(string.Empty);
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Collapse All"), false, () => CollapseComponents());
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Expand All"), false, () => ExpandComponents());
|
||||
menu.AddSeparator(string.Empty);
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Reset"), false, () => ResetComponent(targetComponent.GetType(), id));
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Remove"), false, () => RemoveComponent(id));
|
||||
menu.AddSeparator(string.Empty);
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Copy Settings"), false, () => CopySettings(targetComponent));
|
||||
|
||||
if (CanPaste(targetComponent))
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Paste Settings"), false, () => PasteSettings(targetComponent));
|
||||
else
|
||||
menu.AddDisabledItem(EditorGUIUtility.TrTextContent("Paste Settings"));
|
||||
|
||||
menu.AddSeparator(string.Empty);
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Toggle All"), false, () => m_Editors[id].SetAllOverridesTo(true));
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Toggle None"), false, () => m_Editors[id].SetAllOverridesTo(false));
|
||||
|
||||
menu.DropDown(new Rect(position, Vector2.zero));
|
||||
}
|
||||
|
||||
VolumeComponent CreateNewComponent(Type type)
|
||||
{
|
||||
var effect = (VolumeComponent)ScriptableObject.CreateInstance(type);
|
||||
effect.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
|
||||
effect.name = type.Name;
|
||||
return effect;
|
||||
}
|
||||
|
||||
internal void AddComponent(Type type)
|
||||
{
|
||||
m_SerializedObject.Update();
|
||||
|
||||
var component = CreateNewComponent(type);
|
||||
Undo.RegisterCreatedObjectUndo(component, "Add Volume Override");
|
||||
|
||||
// Store this new effect as a subasset so we can reference it safely afterwards
|
||||
// Only when we're not dealing with an instantiated asset
|
||||
if (EditorUtility.IsPersistent(asset))
|
||||
AssetDatabase.AddObjectToAsset(component, asset);
|
||||
|
||||
// Grow the list first, then add - that's how serialized lists work in Unity
|
||||
m_ComponentsProperty.arraySize++;
|
||||
var componentProp = m_ComponentsProperty.GetArrayElementAtIndex(m_ComponentsProperty.arraySize - 1);
|
||||
componentProp.objectReferenceValue = component;
|
||||
|
||||
// Create & store the internal editor object for this effect
|
||||
CreateEditor(component, componentProp, forceOpen: true);
|
||||
|
||||
m_SerializedObject.ApplyModifiedProperties();
|
||||
|
||||
// Force save / refresh
|
||||
if (EditorUtility.IsPersistent(asset))
|
||||
{
|
||||
EditorUtility.SetDirty(asset);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveComponent(int id)
|
||||
{
|
||||
// Huh. Hack to keep foldout state on the next element...
|
||||
bool nextFoldoutState = false;
|
||||
if (id < m_Editors.Count - 1)
|
||||
nextFoldoutState = m_Editors[id + 1].baseProperty.isExpanded;
|
||||
|
||||
// Remove from the cached editors list
|
||||
m_Editors[id].OnDisable();
|
||||
m_Editors.RemoveAt(id);
|
||||
|
||||
m_SerializedObject.Update();
|
||||
|
||||
var property = m_ComponentsProperty.GetArrayElementAtIndex(id);
|
||||
var component = property.objectReferenceValue;
|
||||
|
||||
// Unassign it (should be null already but serialization does funky things
|
||||
property.objectReferenceValue = null;
|
||||
|
||||
// ...and remove the array index itself from the list
|
||||
m_ComponentsProperty.DeleteArrayElementAtIndex(id);
|
||||
|
||||
// Finally refresh editor reference to the serialized settings list
|
||||
for (int i = 0; i < m_Editors.Count; i++)
|
||||
m_Editors[i].baseProperty = m_ComponentsProperty.GetArrayElementAtIndex(i).Copy();
|
||||
|
||||
// Set the proper foldout state if needed
|
||||
if (id < m_Editors.Count)
|
||||
m_Editors[id].baseProperty.isExpanded = nextFoldoutState;
|
||||
|
||||
m_SerializedObject.ApplyModifiedProperties();
|
||||
|
||||
// Destroy the setting object after ApplyModifiedProperties(). If we do it before, redo
|
||||
// actions will be in the wrong order and the reference to the setting object in the
|
||||
// list will be lost.
|
||||
Undo.DestroyObjectImmediate(component);
|
||||
|
||||
// Force save / refresh
|
||||
EditorUtility.SetDirty(asset);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
// Reset is done by deleting and removing the object from the list and adding a new one in
|
||||
// the same spot as it was before
|
||||
internal void ResetComponent(Type type, int id)
|
||||
{
|
||||
// Remove from the cached editors list
|
||||
m_Editors[id].OnDisable();
|
||||
m_Editors[id] = null;
|
||||
|
||||
m_SerializedObject.Update();
|
||||
|
||||
var property = m_ComponentsProperty.GetArrayElementAtIndex(id);
|
||||
var prevComponent = property.objectReferenceValue;
|
||||
|
||||
// Unassign it but down remove it from the array to keep the index available
|
||||
property.objectReferenceValue = null;
|
||||
|
||||
// Create a new object
|
||||
var newComponent = CreateNewComponent(type);
|
||||
Undo.RegisterCreatedObjectUndo(newComponent, "Reset Volume Overrides");
|
||||
|
||||
// Store this new effect as a subasset so we can reference it safely afterwards
|
||||
AssetDatabase.AddObjectToAsset(newComponent, asset);
|
||||
|
||||
// Put it in the reserved space
|
||||
property.objectReferenceValue = newComponent;
|
||||
|
||||
// Create & store the internal editor object for this effect
|
||||
CreateEditor(newComponent, property, id);
|
||||
|
||||
m_SerializedObject.ApplyModifiedProperties();
|
||||
|
||||
// Same as RemoveComponent, destroy at the end so it's recreated first on Undo to make
|
||||
// sure the GUID exists before undoing the list state
|
||||
Undo.DestroyObjectImmediate(prevComponent);
|
||||
|
||||
// Force save / refresh
|
||||
EditorUtility.SetDirty(asset);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
internal void MoveComponent(int id, int offset)
|
||||
{
|
||||
// Move components
|
||||
m_SerializedObject.Update();
|
||||
m_ComponentsProperty.MoveArrayElement(id, id + offset);
|
||||
m_SerializedObject.ApplyModifiedProperties();
|
||||
|
||||
// We need to keep track of what was expanded before to set it afterwards.
|
||||
bool targetExpanded = m_Editors[id + offset].baseProperty.isExpanded;
|
||||
bool sourceExpanded = m_Editors[id].baseProperty.isExpanded;
|
||||
|
||||
// Move editors
|
||||
var prev = m_Editors[id + offset];
|
||||
m_Editors[id + offset] = m_Editors[id];
|
||||
m_Editors[id] = prev;
|
||||
|
||||
// Set the expansion values
|
||||
m_Editors[id + offset].baseProperty.isExpanded = targetExpanded;
|
||||
m_Editors[id].baseProperty.isExpanded = sourceExpanded;
|
||||
}
|
||||
|
||||
internal void CollapseComponents()
|
||||
{
|
||||
// Move components
|
||||
m_SerializedObject.Update();
|
||||
int numEditors = m_Editors.Count;
|
||||
for (int i = 0; i < numEditors; ++i)
|
||||
{
|
||||
m_Editors[i].baseProperty.isExpanded = false;
|
||||
}
|
||||
m_SerializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
internal void ExpandComponents()
|
||||
{
|
||||
// Move components
|
||||
m_SerializedObject.Update();
|
||||
int numEditors = m_Editors.Count;
|
||||
for (int i = 0; i < numEditors; ++i)
|
||||
{
|
||||
m_Editors[i].baseProperty.isExpanded = true;
|
||||
}
|
||||
m_SerializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
static bool CanPaste(VolumeComponent targetComponent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EditorGUIUtility.systemCopyBuffer))
|
||||
return false;
|
||||
|
||||
string clipboard = EditorGUIUtility.systemCopyBuffer;
|
||||
int separator = clipboard.IndexOf('|');
|
||||
|
||||
if (separator < 0)
|
||||
return false;
|
||||
|
||||
return targetComponent.GetType().AssemblyQualifiedName == clipboard.Substring(0, separator);
|
||||
}
|
||||
|
||||
static void CopySettings(VolumeComponent targetComponent)
|
||||
{
|
||||
string typeName = targetComponent.GetType().AssemblyQualifiedName;
|
||||
string typeData = JsonUtility.ToJson(targetComponent);
|
||||
EditorGUIUtility.systemCopyBuffer = $"{typeName}|{typeData}";
|
||||
}
|
||||
|
||||
static void PasteSettings(VolumeComponent targetComponent)
|
||||
{
|
||||
string clipboard = EditorGUIUtility.systemCopyBuffer;
|
||||
string typeData = clipboard.Substring(clipboard.IndexOf('|') + 1);
|
||||
Undo.RecordObject(targetComponent, "Paste Settings");
|
||||
JsonUtility.FromJsonOverwrite(typeData, targetComponent);
|
||||
}
|
||||
|
||||
static void ReloadDocumentation()
|
||||
{
|
||||
if (m_EditorDocumentationURLs == null)
|
||||
m_EditorDocumentationURLs = new Dictionary<Type, string>();
|
||||
m_EditorDocumentationURLs.Clear();
|
||||
|
||||
string GetVolumeComponentDocumentation(Type component)
|
||||
{
|
||||
var attrs = component.GetCustomAttributes(false);
|
||||
foreach (var attr in attrs)
|
||||
{
|
||||
if (attr is HelpURLAttribute attrDocumentation)
|
||||
return attrDocumentation.URL;
|
||||
}
|
||||
|
||||
// There is no documentation for this volume component.
|
||||
return null;
|
||||
}
|
||||
|
||||
var componentTypes = CoreUtils.GetAllTypesDerivedFrom<VolumeComponent>();
|
||||
foreach (var componentType in componentTypes)
|
||||
{
|
||||
if (!m_EditorDocumentationURLs.ContainsKey(componentType))
|
||||
m_EditorDocumentationURLs.Add(componentType, GetVolumeComponentDocumentation(componentType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
using IProvider = FilterWindow.IProvider;
|
||||
using Element = FilterWindow.Element;
|
||||
using GroupElement = FilterWindow.GroupElement;
|
||||
|
||||
class VolumeComponentProvider : IProvider
|
||||
{
|
||||
class VolumeComponentElement : Element
|
||||
{
|
||||
public Type type;
|
||||
|
||||
public VolumeComponentElement(int level, string label, Type type)
|
||||
{
|
||||
this.level = level;
|
||||
this.type = type;
|
||||
// TODO: Add support for custom icons
|
||||
content = new GUIContent(label);
|
||||
}
|
||||
}
|
||||
|
||||
class PathNode : IComparable<PathNode>
|
||||
{
|
||||
public List<PathNode> nodes = new List<PathNode>();
|
||||
public string name;
|
||||
public Type type;
|
||||
|
||||
public int CompareTo(PathNode other)
|
||||
{
|
||||
return name.CompareTo(other.name);
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 position { get; set; }
|
||||
|
||||
VolumeProfile m_Target;
|
||||
VolumeComponentListEditor m_TargetEditor;
|
||||
|
||||
public VolumeComponentProvider(VolumeProfile target, VolumeComponentListEditor targetEditor)
|
||||
{
|
||||
m_Target = target;
|
||||
m_TargetEditor = targetEditor;
|
||||
}
|
||||
|
||||
public void CreateComponentTree(List<Element> tree)
|
||||
{
|
||||
tree.Add(new GroupElement(0, "Volume Overrides"));
|
||||
|
||||
var types = VolumeManager.instance.baseComponentTypeArray;
|
||||
var rootNode = new PathNode();
|
||||
|
||||
foreach (var t in types)
|
||||
{
|
||||
// Skip components that have already been added to the volume
|
||||
if (m_Target.Has(t))
|
||||
continue;
|
||||
|
||||
string path = string.Empty;
|
||||
|
||||
// Look for a VolumeComponentMenu attribute
|
||||
var attrs = t.GetCustomAttributes(false);
|
||||
|
||||
bool skipComponent = false;
|
||||
foreach (var attr in attrs)
|
||||
{
|
||||
var attrMenu = attr as VolumeComponentMenu;
|
||||
if (attrMenu != null)
|
||||
path = attrMenu.menu;
|
||||
|
||||
var attrDeprecated = attr as VolumeComponentDeprecated;
|
||||
if (attrDeprecated != null)
|
||||
skipComponent = true;
|
||||
}
|
||||
|
||||
if (skipComponent)
|
||||
continue;
|
||||
|
||||
// If no attribute or in case something went wrong when grabbing it, fallback to a
|
||||
// beautified class name
|
||||
if (string.IsNullOrEmpty(path))
|
||||
path = ObjectNames.NicifyVariableName(t.Name);
|
||||
|
||||
// Prep the categories & types tree
|
||||
AddNode(rootNode, path, t);
|
||||
}
|
||||
|
||||
// Recursively add all elements to the tree
|
||||
Traverse(rootNode, 1, tree);
|
||||
}
|
||||
|
||||
public bool GoToChild(Element element, bool addIfComponent)
|
||||
{
|
||||
if (element is VolumeComponentElement)
|
||||
{
|
||||
var e = (VolumeComponentElement)element;
|
||||
m_TargetEditor.AddComponent(e.type);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AddNode(PathNode root, string path, Type type)
|
||||
{
|
||||
var current = root;
|
||||
var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var child = current.nodes.Find(x => x.name == part);
|
||||
|
||||
if (child == null)
|
||||
{
|
||||
child = new PathNode { name = part, type = type };
|
||||
current.nodes.Add(child);
|
||||
}
|
||||
|
||||
current = child;
|
||||
}
|
||||
}
|
||||
|
||||
void Traverse(PathNode node, int depth, List<Element> tree)
|
||||
{
|
||||
node.nodes.Sort();
|
||||
|
||||
foreach (var n in node.nodes)
|
||||
{
|
||||
if (n.nodes.Count > 0) // Group
|
||||
{
|
||||
tree.Add(new GroupElement(depth, n.name));
|
||||
Traverse(n, depth + 1, tree);
|
||||
}
|
||||
else // Element
|
||||
{
|
||||
tree.Add(new VolumeComponentElement(depth, n.name, n.type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,222 @@
|
||||
using System.IO;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
[CustomEditor(typeof(Volume))]
|
||||
sealed class VolumeEditor : Editor
|
||||
{
|
||||
SerializedProperty m_IsGlobal;
|
||||
SerializedProperty m_BlendRadius;
|
||||
SerializedProperty m_Weight;
|
||||
SerializedProperty m_Priority;
|
||||
SerializedProperty m_Profile;
|
||||
|
||||
VolumeComponentListEditor m_ComponentList;
|
||||
|
||||
Volume actualTarget => target as Volume;
|
||||
|
||||
VolumeProfile profileRef => actualTarget.HasInstantiatedProfile() ? actualTarget.profile : actualTarget.sharedProfile;
|
||||
|
||||
readonly GUIContent[] m_Modes = { new GUIContent("Global"), new GUIContent("Local") };
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
var o = new PropertyFetcher<Volume>(serializedObject);
|
||||
m_IsGlobal = o.Find(x => x.isGlobal);
|
||||
m_BlendRadius = o.Find(x => x.blendDistance);
|
||||
m_Weight = o.Find(x => x.weight);
|
||||
m_Priority = o.Find(x => x.priority);
|
||||
m_Profile = o.Find(x => x.sharedProfile);
|
||||
|
||||
m_ComponentList = new VolumeComponentListEditor(this);
|
||||
RefreshEffectListEditor(actualTarget.sharedProfile);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
m_ComponentList?.Clear();
|
||||
}
|
||||
|
||||
void RefreshEffectListEditor(VolumeProfile asset)
|
||||
{
|
||||
m_ComponentList.Clear();
|
||||
|
||||
if (asset != null)
|
||||
m_ComponentList.Init(asset, new SerializedObject(asset));
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
GUIContent label = EditorGUIUtility.TrTextContent("Mode", "A global volume is applied to the whole scene.");
|
||||
Rect lineRect = EditorGUILayout.GetControlRect();
|
||||
int isGlobal = m_IsGlobal.boolValue ? 0 : 1;
|
||||
EditorGUI.BeginProperty(lineRect, label, m_IsGlobal);
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
isGlobal = EditorGUI.Popup(lineRect, label, isGlobal, m_Modes);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
m_IsGlobal.boolValue = isGlobal == 0;
|
||||
}
|
||||
EditorGUI.EndProperty();
|
||||
|
||||
if (isGlobal != 0) // Blend radius is not needed for global volumes
|
||||
{
|
||||
if (!actualTarget.TryGetComponent<Collider>(out _))
|
||||
{
|
||||
EditorGUILayout.HelpBox("Add a Collider to this GameObject to set boundaries for the local Volume.", MessageType.Info);
|
||||
|
||||
if (GUILayout.Button(EditorGUIUtility.TrTextContent("Add Collider"), EditorStyles.miniButton))
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Box"), false, () => Undo.AddComponent<BoxCollider>(actualTarget.gameObject));
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Sphere"), false, () => Undo.AddComponent<SphereCollider>(actualTarget.gameObject));
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Capsule"), false, () => Undo.AddComponent<CapsuleCollider>(actualTarget.gameObject));
|
||||
menu.AddItem(EditorGUIUtility.TrTextContent("Mesh"), false, () => Undo.AddComponent<MeshCollider>(actualTarget.gameObject));
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.PropertyField(m_BlendRadius);
|
||||
m_BlendRadius.floatValue = Mathf.Max(m_BlendRadius.floatValue, 0f);
|
||||
}
|
||||
|
||||
EditorGUILayout.PropertyField(m_Weight);
|
||||
EditorGUILayout.PropertyField(m_Priority);
|
||||
|
||||
bool assetHasChanged = false;
|
||||
bool showCopy = m_Profile.objectReferenceValue != null;
|
||||
bool multiEdit = m_Profile.hasMultipleDifferentValues;
|
||||
|
||||
// The layout system breaks alignment when mixing inspector fields with custom layout'd
|
||||
// fields, do the layout manually instead
|
||||
int buttonWidth = showCopy ? 45 : 60;
|
||||
float indentOffset = EditorGUI.indentLevel * 15f;
|
||||
lineRect = EditorGUILayout.GetControlRect();
|
||||
var labelRect = new Rect(lineRect.x, lineRect.y, EditorGUIUtility.labelWidth - indentOffset, lineRect.height);
|
||||
var fieldRect = new Rect(labelRect.xMax, lineRect.y, lineRect.width - labelRect.width - buttonWidth * (showCopy ? 2 : 1), lineRect.height);
|
||||
var buttonNewRect = new Rect(fieldRect.xMax, lineRect.y, buttonWidth, lineRect.height);
|
||||
var buttonCopyRect = new Rect(buttonNewRect.xMax, lineRect.y, buttonWidth, lineRect.height);
|
||||
|
||||
GUIContent guiContent;
|
||||
if (actualTarget.HasInstantiatedProfile())
|
||||
guiContent = EditorGUIUtility.TrTextContent("Profile (Instance)", "A copy of a profile asset.");
|
||||
else
|
||||
guiContent = EditorGUIUtility.TrTextContent("Profile", "A reference to a profile asset.");
|
||||
EditorGUI.PrefixLabel(labelRect, guiContent);
|
||||
|
||||
using (var scope = new EditorGUI.ChangeCheckScope())
|
||||
{
|
||||
EditorGUI.BeginProperty(fieldRect, GUIContent.none, m_Profile);
|
||||
|
||||
VolumeProfile profile;
|
||||
|
||||
if (actualTarget.HasInstantiatedProfile())
|
||||
profile = (VolumeProfile)EditorGUI.ObjectField(fieldRect, actualTarget.profile, typeof(VolumeProfile), false);
|
||||
else
|
||||
profile = (VolumeProfile)EditorGUI.ObjectField(fieldRect, m_Profile.objectReferenceValue, typeof(VolumeProfile), false);
|
||||
|
||||
if (scope.changed)
|
||||
{
|
||||
assetHasChanged = true;
|
||||
m_Profile.objectReferenceValue = profile;
|
||||
|
||||
if (actualTarget.HasInstantiatedProfile()) // Clear the instantiated profile, from now on we're using shared again
|
||||
actualTarget.profile = null;
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
using (new EditorGUI.DisabledScope(multiEdit))
|
||||
{
|
||||
if (GUI.Button(buttonNewRect, EditorGUIUtility.TrTextContent("New", "Create a new profile."), showCopy ? EditorStyles.miniButtonLeft : EditorStyles.miniButton))
|
||||
{
|
||||
// By default, try to put assets in a folder next to the currently active
|
||||
// scene file. If the user isn't a scene, put them in root instead.
|
||||
var targetName = actualTarget.name;
|
||||
var scene = actualTarget.gameObject.scene;
|
||||
var asset = VolumeProfileFactory.CreateVolumeProfile(scene, targetName);
|
||||
m_Profile.objectReferenceValue = asset;
|
||||
actualTarget.profile = null; // Make sure we're not using an instantiated profile anymore
|
||||
assetHasChanged = true;
|
||||
}
|
||||
|
||||
if (actualTarget.HasInstantiatedProfile())
|
||||
guiContent = EditorGUIUtility.TrTextContent("Save", "Save the instantiated profile");
|
||||
else
|
||||
guiContent = EditorGUIUtility.TrTextContent("Clone", "Create a new profile and copy the content of the currently assigned profile.");
|
||||
if (showCopy && GUI.Button(buttonCopyRect, guiContent, EditorStyles.miniButtonRight))
|
||||
{
|
||||
// Duplicate the currently assigned profile and save it as a new profile
|
||||
var origin = profileRef;
|
||||
var path = AssetDatabase.GetAssetPath(m_Profile.objectReferenceValue);
|
||||
|
||||
path = IsAssetInReadOnlyPackage(path)
|
||||
// We may be in a read only package, in that case we need to clone the volume profile in an
|
||||
// editable area, such as the root of the project.
|
||||
? AssetDatabase.GenerateUniqueAssetPath(Path.Combine("Assets", Path.GetFileName(path)))
|
||||
// Otherwise, duplicate next to original asset.
|
||||
: AssetDatabase.GenerateUniqueAssetPath(path);
|
||||
|
||||
var asset = Instantiate(origin);
|
||||
asset.components.Clear();
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
|
||||
foreach (var item in origin.components)
|
||||
{
|
||||
var itemCopy = Instantiate(item);
|
||||
itemCopy.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
|
||||
itemCopy.name = item.name;
|
||||
asset.components.Add(itemCopy);
|
||||
AssetDatabase.AddObjectToAsset(itemCopy, asset);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
m_Profile.objectReferenceValue = asset;
|
||||
actualTarget.profile = null; // Make sure we're not using an instantiated profile anymore
|
||||
assetHasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (m_Profile.objectReferenceValue == null && !actualTarget.HasInstantiatedProfile())
|
||||
{
|
||||
if (assetHasChanged)
|
||||
m_ComponentList.Clear(); // Asset wasn't null before, do some cleanup
|
||||
}
|
||||
else
|
||||
{
|
||||
if (assetHasChanged || profileRef != m_ComponentList.asset)
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
serializedObject.Update();
|
||||
RefreshEffectListEditor(profileRef);
|
||||
}
|
||||
|
||||
if (!multiEdit)
|
||||
{
|
||||
m_ComponentList.OnGUI();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
static bool IsAssetInReadOnlyPackage(string path)
|
||||
{
|
||||
Assert.IsNotNull(path);
|
||||
var info = PackageManager.PackageInfo.FindForAssetPath(path);
|
||||
return info != null && (info.source != PackageSource.Local && info.source != PackageSource.Embedded);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
static class VolumeMenuItems
|
||||
{
|
||||
const string k_VolumeRootMenu = "GameObject/Volume/";
|
||||
|
||||
[MenuItem(k_VolumeRootMenu + "Global Volume", priority = CoreUtils.gameObjectMenuPriority)]
|
||||
static void CreateGlobalVolume(MenuCommand menuCommand)
|
||||
{
|
||||
var go = CoreEditorUtils.CreateGameObject("Global Volume", menuCommand.context);
|
||||
var volume = go.AddComponent<Volume>();
|
||||
volume.isGlobal = true;
|
||||
}
|
||||
|
||||
[MenuItem(k_VolumeRootMenu + "Box Volume", priority = CoreUtils.gameObjectMenuPriority)]
|
||||
static void CreateBoxVolume(MenuCommand menuCommand)
|
||||
{
|
||||
var go = CoreEditorUtils.CreateGameObject("Box Volume", menuCommand.context);
|
||||
var collider = go.AddComponent<BoxCollider>();
|
||||
collider.isTrigger = true;
|
||||
var volume = go.AddComponent<Volume>();
|
||||
volume.isGlobal = false;
|
||||
volume.blendDistance = 1f;
|
||||
}
|
||||
|
||||
[MenuItem(k_VolumeRootMenu + "Sphere Volume", priority = CoreUtils.gameObjectMenuPriority)]
|
||||
static void CreateSphereVolume(MenuCommand menuCommand)
|
||||
{
|
||||
var go = CoreEditorUtils.CreateGameObject("Sphere Volume", menuCommand.context);
|
||||
var collider = go.AddComponent<SphereCollider>();
|
||||
collider.isTrigger = true;
|
||||
var volume = go.AddComponent<Volume>();
|
||||
volume.isGlobal = false;
|
||||
volume.blendDistance = 1f;
|
||||
}
|
||||
|
||||
[MenuItem(k_VolumeRootMenu + "Convex Mesh Volume", priority = CoreUtils.gameObjectMenuPriority)]
|
||||
static void CreateConvexMeshVolume(MenuCommand menuCommand)
|
||||
{
|
||||
var go = CoreEditorUtils.CreateGameObject("Convex Mesh Volume", menuCommand.context);
|
||||
var collider = go.AddComponent<MeshCollider>();
|
||||
collider.convex = true;
|
||||
collider.isTrigger = true;
|
||||
var volume = go.AddComponent<Volume>();
|
||||
volume.isGlobal = false;
|
||||
volume.blendDistance = 1f;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// This attributes tells an <see cref="VolumeParameterDrawer"/> class which type of
|
||||
/// <see cref="VolumeParameter"/> it's an editor for.
|
||||
/// When you make a custom drawer for a parameter, you need add this attribute to the drawer
|
||||
/// class.
|
||||
/// </summary>
|
||||
/// <seealso cref="VolumeParameterDrawer"/>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public sealed class VolumeParameterDrawerAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// A type derived from <see cref="VolumeParameter{T}"/>.
|
||||
/// </summary>
|
||||
public readonly Type parameterType;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="VolumeParameterDrawerAttribute"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="parameterType">A type derived from <see cref="VolumeParameter{T}"/>.</param>
|
||||
public VolumeParameterDrawerAttribute(Type parameterType)
|
||||
{
|
||||
this.parameterType = parameterType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A base class to implement to draw custom editors for custom <see cref="VolumeParameter"/>.
|
||||
/// You must use a <see cref="VolumeParameterDrawerAttribute"/> to let the editor know which
|
||||
/// parameter this drawer is for.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you do not provide a custom editor for a <see cref="VolumeParameter"/>, Unity uses the buil-in property drawers to draw the
|
||||
/// property as-is.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// Here's an example about how <see cref="ClampedFloatParameter"/> is implemented:
|
||||
/// <code>
|
||||
/// [VolumeParameterDrawer(typeof(ClampedFloatParameter))]
|
||||
/// class ClampedFloatParameterDrawer : VolumeParameterDrawer
|
||||
/// {
|
||||
/// public override bool OnGUI(SerializedDataParameter parameter, GUIContent title)
|
||||
/// {
|
||||
/// var value = parameter.value;
|
||||
///
|
||||
/// if (value.propertyType != SerializedPropertyType.Float)
|
||||
/// return false;
|
||||
///
|
||||
/// var o = parameter.GetObjectRef<ClampedFloatParameter>();
|
||||
/// EditorGUILayout.Slider(value, o.min, o.max, title);
|
||||
/// value.floatValue = Mathf.Clamp(value.floatValue, o.min, o.max);
|
||||
/// return true;
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <seealso cref="VolumeParameterDrawerAttribute"/>
|
||||
public abstract class VolumeParameterDrawer
|
||||
{
|
||||
// Override this and return false if you want to customize the override checkbox position,
|
||||
// else it'll automatically draw it and put the property content in a horizontal scope.
|
||||
|
||||
/// <summary>
|
||||
/// Override this and return <c>false</c> if you want to customize the position of the override
|
||||
/// checkbox. If you don't, Unity automatically draws the checkbox and puts the property content in a
|
||||
/// horizontal scope.
|
||||
/// </summary>
|
||||
/// <returns><c>false</c> if the override checkbox position is customized, <c>true</c>
|
||||
/// otherwise</returns>
|
||||
public virtual bool IsAutoProperty() => true;
|
||||
|
||||
/// <summary>
|
||||
/// Draws the parameter in the editor. If the input parameter is invalid you should return
|
||||
/// <c>false</c> so that Unity displays the default editor for this parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to draw.</param>
|
||||
/// <param name="title">The label and tooltip of the parameter.</param>
|
||||
/// <returns><c>true</c> if the input parameter is valid, <c>false</c> otherwise in which
|
||||
/// case Unity will revert to the default editor for this parameter</returns>
|
||||
public abstract bool OnGUI(SerializedDataParameter parameter, GUIContent title);
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
[CustomEditor(typeof(VolumeProfile))]
|
||||
sealed class VolumeProfileEditor : Editor
|
||||
{
|
||||
VolumeComponentListEditor m_ComponentList;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
m_ComponentList = new VolumeComponentListEditor(this);
|
||||
m_ComponentList.Init(target as VolumeProfile, serializedObject);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (m_ComponentList != null)
|
||||
m_ComponentList.Clear();
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
m_ComponentList.OnGUI();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,122 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor.ProjectWindowCallback;
|
||||
using System.IO;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace UnityEditor.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// A utility class to create Volume Profiles and components.
|
||||
/// </summary>
|
||||
public static class VolumeProfileFactory
|
||||
{
|
||||
[MenuItem("Assets/Create/Volume Profile", priority = 201)]
|
||||
static void CreateVolumeProfile()
|
||||
{
|
||||
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(
|
||||
0,
|
||||
ScriptableObject.CreateInstance<DoCreatePostProcessProfile>(),
|
||||
"New Volume Profile.asset",
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="VolumeProfile"/> Asset and saves it at the given path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to save the Asset to, relative to the Project folder.</param>
|
||||
/// <returns>The newly created <see cref="VolumeProfile"/>.</returns>
|
||||
public static VolumeProfile CreateVolumeProfileAtPath(string path)
|
||||
{
|
||||
var profile = ScriptableObject.CreateInstance<VolumeProfile>();
|
||||
profile.name = Path.GetFileName(path);
|
||||
AssetDatabase.CreateAsset(profile, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="VolumeProfile"/> Asset and saves it in a folder next to the Scene.
|
||||
/// </summary>
|
||||
/// <param name="scene">The Scene to save the Profile next to.</param>
|
||||
/// <param name="targetName">A name to use for the Asset filename.</param>
|
||||
/// <returns>The newly created <see cref="VolumeProfile"/>.</returns>
|
||||
public static VolumeProfile CreateVolumeProfile(Scene scene, string targetName)
|
||||
{
|
||||
string path;
|
||||
|
||||
if (string.IsNullOrEmpty(scene.path))
|
||||
{
|
||||
path = "Assets/";
|
||||
}
|
||||
else
|
||||
{
|
||||
var scenePath = Path.GetDirectoryName(scene.path);
|
||||
var extPath = scene.name;
|
||||
var profilePath = scenePath + Path.DirectorySeparatorChar + extPath;
|
||||
|
||||
if (!AssetDatabase.IsValidFolder(profilePath))
|
||||
{
|
||||
var directories = profilePath.Split(Path.DirectorySeparatorChar);
|
||||
string rootPath = "";
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
var newPath = rootPath + directory;
|
||||
if (!AssetDatabase.IsValidFolder(newPath))
|
||||
AssetDatabase.CreateFolder(rootPath.TrimEnd(Path.DirectorySeparatorChar), directory);
|
||||
rootPath = newPath + Path.DirectorySeparatorChar;
|
||||
}
|
||||
}
|
||||
|
||||
path = profilePath + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
path += targetName + " Profile.asset";
|
||||
path = AssetDatabase.GenerateUniqueAssetPath(path);
|
||||
|
||||
var profile = ScriptableObject.CreateInstance<VolumeProfile>();
|
||||
AssetDatabase.CreateAsset(profile, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="VolumeComponent"/> in an existing <see cref="VolumeProfile"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">A type of <see cref="VolumeComponent"/>.</typeparam>
|
||||
/// <param name="profile">The profile to store the new component in.</param>
|
||||
/// <param name="overrides">specifies whether to override the parameters in the component or not.</param>
|
||||
/// <param name="saveAsset">Specifies whether to save the Profile Asset or not. This is useful when you need to
|
||||
/// create several components in a row and only want to save the Profile Asset after adding the last one,
|
||||
/// because saving Assets to disk can be slow.</param>
|
||||
/// <returns></returns>
|
||||
public static T CreateVolumeComponent<T>(VolumeProfile profile, bool overrides = false, bool saveAsset = true)
|
||||
where T : VolumeComponent
|
||||
{
|
||||
var comp = profile.Add<T>(overrides);
|
||||
comp.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
|
||||
AssetDatabase.AddObjectToAsset(comp, profile);
|
||||
|
||||
if (saveAsset)
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
|
||||
class DoCreatePostProcessProfile : EndNameEditAction
|
||||
{
|
||||
public override void Action(int instanceId, string pathName, string resourceFile)
|
||||
{
|
||||
var profile = VolumeProfileFactory.CreateVolumeProfileAtPath(pathName);
|
||||
ProjectWindowUtil.ShowCreatedAsset(profile);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user