This commit is contained in:
2021-06-13 10:28:03 +02:00
parent eb70603c85
commit df2d24cbd3
7487 changed files with 943244 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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&lt;ExampleComponent&gt;(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);
}
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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));
}
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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&lt;ClampedFloatParameter&gt;();
/// 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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}