using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor.AnimatedValues; using UnityEditor.Callbacks; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Rendering; namespace UnityEditor.Rendering { /// /// A custom editor class that draws a in the Inspector. If you do not /// provide a custom editor for a , Unity uses the default one. /// You must use a to let the editor know which /// component this drawer is for. /// /// /// Below is an example of a custom : /// /// using UnityEngine.Rendering; /// /// [Serializable, VolumeComponentMenu("Custom/Example Component")] /// public class ExampleComponent : VolumeComponent /// { /// public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f); /// } /// /// And its associated editor: /// /// using UnityEditor.Rendering; /// /// [CustomEditor(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); /// } /// } /// /// /// [CustomEditor(typeof(VolumeComponent), true)] public class VolumeComponentEditor : Editor { const string k_KeyPrefix = "CoreRP:VolumeComponent:UI_State:"; EditorPrefBool m_EditorPrefBool; internal string categoryTitle { get; set; } /// /// If the editor for this is expanded or not in the inspector /// public bool expanded { get => m_EditorPrefBool.value; set => m_EditorPrefBool.value = value; } internal bool visible { get; private set; } static class Styles { public static readonly GUIContent k_OverrideSettingText = EditorGUIUtility.TrTextContent("", "Override this setting for this volume."); public static readonly GUIContent k_AllText = EditorGUIUtility.TrTextContent("ALL", "Toggle all overrides on. To maximize performances you should only toggle overrides that you actually need."); public static readonly GUIContent k_NoneText = EditorGUIUtility.TrTextContent("NONE", "Toggle all overrides off."); public static string toggleAllText { get; } = L10n.Tr("Toggle All"); public const int overrideCheckboxWidth = 14; public const int overrideCheckboxOffset = 9; } Vector2? m_OverrideToggleSize; internal Vector2 overrideToggleSize { get { if (!m_OverrideToggleSize.HasValue) m_OverrideToggleSize = CoreEditorStyles.smallTickbox.CalcSize(Styles.k_OverrideSettingText); return m_OverrideToggleSize.Value; } } /// /// Specifies the this editor is drawing. /// public VolumeComponent volumeComponent => target as VolumeComponent; /// /// The copy of the serialized property of the being /// inspected. Unity uses this to track whether the editor is collapsed in the Inspector or not. /// [Obsolete("Please use expanded property instead. #from(2022.2)", false)] public SerializedProperty baseProperty { get; internal set; } /// /// The serialized property of for the component being /// inspected. /// public SerializedProperty activeProperty { get; internal set; } #region Additional Properties List m_VolumeNotAdditionalParameters = new List(); /// /// Override this property if your editor makes use of the "Additional Properties" feature. /// public virtual bool hasAdditionalProperties => volumeComponent.parameterList.Count != m_VolumeNotAdditionalParameters.Count; /// /// Set to true to show additional properties. /// public bool showAdditionalProperties { get => AdvancedProperties.enabled; set => AdvancedProperties.enabled = value; } /// /// Start a scope for additional properties. /// This will handle the highlight of the background when toggled on and off. /// /// True if the additional content should be drawn. protected bool BeginAdditionalPropertiesScope() { if (!showAdditionalProperties || !hasAdditionalProperties) return false; AdvancedProperties.BeginGroup(); return true; } /// /// End a scope for additional properties. /// protected void EndAdditionalPropertiesScope() { if (hasAdditionalProperties && showAdditionalProperties) AdvancedProperties.EndGroup(); } #endregion /// /// A reference to the parent editor in the Inspector. /// protected Editor m_Inspector; /// /// A reference to the parent editor in the Inspector. /// internal Editor inspector { get => m_Inspector; set => m_Inspector = value; } internal void SetVolume(Volume v) { volume = v; } /// /// Obtains the that is being edited if editing a scene volume, otherwise null. /// protected Volume volume { get; private set; } internal void SetVolumeProfile(VolumeProfile p) { volumeProfile = p; } /// /// Obtains the that is being edited. /// VolumeProfile volumeProfile { get; set; } List<(GUIContent displayName, int displayOrder, SerializedDataParameter param)> m_Parameters; static Dictionary s_ParameterDrawers; SupportedOnRenderPipelineAttribute m_SupportedOnRenderPipelineAttribute; Type[] m_LegacyPipelineTypes; static VolumeComponentEditor() { s_ParameterDrawers = new Dictionary(); ReloadDecoratorTypes(); } [DidReloadScripts] static void OnEditorReload() { ReloadDecoratorTypes(); } static void ReloadDecoratorTypes() { s_ParameterDrawers.Clear(); foreach (var type in TypeCache.GetTypesDerivedFrom()) { if (type.IsAbstract) continue; var attr = type.GetCustomAttribute(false); if (attr == null) { Debug.LogWarning($"{type} is missing the attribute {nameof(VolumeParameterDrawerAttribute)}"); continue; } s_ParameterDrawers.Add(attr.parameterType, Activator.CreateInstance(type) as VolumeParameterDrawer); } } /// /// Triggers an Inspector repaint event. /// public new void Repaint() { // Volume Component Editors can be shown in the Graphics Settings window (default volume profile) // This will force a repaint of the whole window, otherwise, additional properties highlight animation does not work properly. SettingsService.RepaintAllSettingsWindow(); base.Repaint(); } internal static string GetAdditionalPropertiesPreferenceKey(Type type) { return $"UI_Show_Additional_Properties_{type}"; } internal void InitAdditionalPropertiesPreference() { string key = GetAdditionalPropertiesPreferenceKey(GetType()); AdvancedProperties.UpdateShowAdvancedProperties(key, EditorPrefs.HasKey(key) && EditorPrefs.GetBool(key)); } internal void Init() { activeProperty = serializedObject.FindProperty("active"); string inspectorKey = string.Empty; bool expandedByDefault = true; if (!enableOverrides) { inspectorKey += "default"; // Ensures the default VolumeProfile editor doesn't share expander state with other editors expandedByDefault = false; } m_EditorPrefBool = new EditorPrefBool(k_KeyPrefix + inspectorKey + volumeComponent.GetType().Name, expandedByDefault); InitAdditionalPropertiesPreference(); InitParameters(); OnEnable(); var volumeComponentType = volumeComponent.GetType(); m_SupportedOnRenderPipelineAttribute = volumeComponentType.GetCustomAttribute(); #pragma warning disable CS0618 var supportedOn = volumeComponentType.GetCustomAttribute(); m_LegacyPipelineTypes = supportedOn != null ? supportedOn.pipelineTypes : Array.Empty(); #pragma warning restore CS0618 EditorApplication.contextualPropertyMenu += OnPropertyContextMenu; } void OnDestroy() { EditorApplication.contextualPropertyMenu -= OnPropertyContextMenu; } internal void DetermineVisibility(Type renderPipelineAssetType, Type renderPipelineType) { if (renderPipelineAssetType == null) { visible = false; return; } if (m_SupportedOnRenderPipelineAttribute != null) { visible = m_SupportedOnRenderPipelineAttribute.GetSupportedMode(renderPipelineAssetType) != SupportedOnRenderPipelineAttribute.SupportedMode.Unsupported; return; } if (renderPipelineType != null && m_LegacyPipelineTypes.Length > 0) { visible = m_LegacyPipelineTypes.Contains(renderPipelineType); return; } visible = true; } void InitParameters() { VolumeComponent.FindParameters(target, m_VolumeNotAdditionalParameters, field => field.GetCustomAttribute() == 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)); } } /// /// Unity calls this method when the object loads. /// /// /// You can safely override this method and not call base.OnEnable() unless you want /// Unity to display all the properties from the automatically. /// 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 (fieldInfo, serializedProperty) = t; var attr = (DisplayInfoAttribute[])fieldInfo.GetCustomAttributes(typeof(DisplayInfoAttribute), true); if (attr.Length != 0) { name = attr[0].name; order = attr[0].order; } var parameter = new SerializedDataParameter(t.Item2); return (EditorGUIUtility.TrTextContent(name), order, parameter); }) .OrderBy(t => t.order) .ToList(); } /// /// Unity calls this method when the object goes out of scope. /// public virtual void OnDisable() { } internal void AddDefaultProfileContextMenuEntries( GenericMenu menu, VolumeProfile defaultProfile, GenericMenu.MenuFunction copyAction) { // Host can be either VolumeProfileEditor or VolumeEditor var profile = volume ? volume.HasInstantiatedProfile() ? volume.profile : volume.sharedProfile : volumeProfile; if (defaultProfile != null && profile != null && defaultProfile != profile) { menu.AddItem(EditorGUIUtility.TrTextContent($"Show Default Volume Profile"), false, () => Selection.activeObject = defaultProfile); menu.AddItem(EditorGUIUtility.TrTextContent($"Apply Values to Default Volume Profile"), false, copyAction); } } void OnPropertyContextMenu(GenericMenu menu, SerializedProperty property) { if (property.serializedObject.targetObject != target) return; var targetComponent = property.serializedObject.targetObject as VolumeComponent; AddDefaultProfileContextMenuEntries(menu, VolumeManager.instance.globalDefaultProfile, () => VolumeProfileUtils.AssignValuesToProfile(VolumeManager.instance.globalDefaultProfile, targetComponent, property)); } /// /// Unity calls this method after drawing the header for each VolumeComponentEditor /// protected virtual void OnBeforeInspectorGUI() { } internal bool OnInternalInspectorGUI() { if (serializedObject == null || serializedObject.targetObject == null) return false; serializedObject.Update(); using (new EditorGUILayout.VerticalScope()) { OnBeforeInspectorGUI(); if (enableOverrides) TopRowFields(); else GUILayout.Space(4); OnInspectorGUI(); EditorGUILayout.Space(); } return serializedObject.ApplyModifiedProperties(); } /// /// Unity calls this method each time it re-draws the Inspector. /// /// /// You can safely override this method and not call base.OnInspectorGUI() unless you /// want Unity to display all the properties from the /// automatically. /// public override 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); } } /// /// Sets the label for the component header. Override this method to provide /// a custom label. If you don't, Unity automatically obtains one from the class name. /// /// A label to display in the component header. public virtual GUIContent GetDisplayTitle() { var title = string.IsNullOrEmpty(volumeComponent.displayName) ? ObjectNames.NicifyVariableName(volumeComponent.GetType().Name) : volumeComponent.displayName; return EditorGUIUtility.TrTextContent(title, string.Empty); } void AddToggleState(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()) { AddToggleState(Styles.k_AllText, true); AddToggleState(Styles.k_NoneText, false); } } /// /// Checks if all the visible parameters have the given state /// /// The state to check internal bool AreOverridesTo(bool state) { if (hasAdditionalProperties && showAdditionalProperties) return AreAllOverridesTo(state); for (int i = 0; i < m_VolumeNotAdditionalParameters.Count; ++i) { if (m_VolumeNotAdditionalParameters[i].overrideState != state) return false; } return true; } /// /// Sets the given state to all the visible parameters /// /// The state to check internal void SetOverridesTo(bool state) { if (hasAdditionalProperties && showAdditionalProperties) SetAllOverridesTo(state); else { Undo.RecordObject(target, Styles.toggleAllText); volumeComponent.SetOverridesTo(m_VolumeNotAdditionalParameters, state); serializedObject.Update(); } } internal bool AreAllOverridesTo(bool state) { for (int i = 0; i < volumeComponent.parameterList.Count; ++i) { if (volumeComponent.parameterList[i].overrideState != state) return false; } return true; } internal void SetAllOverridesTo(bool state) { Undo.RecordObject(target, Styles.toggleAllText); volumeComponent.SetAllOverridesTo(state); serializedObject.Update(); } /// /// Generates and auto-populates a from a serialized /// . /// /// A serialized property holding a /// /// A that encapsulates the provided serialized property. protected SerializedDataParameter Unpack(SerializedProperty property) { Assert.IsNotNull(property); return new SerializedDataParameter(property); } /// /// Draws a given in the editor. /// /// The property to draw in the editor /// true if the property field has been rendered protected bool PropertyField(SerializedDataParameter property) { var title = EditorGUIUtility.TrTextContent(property.displayName, property.GetAttribute()?.tooltip); // avoid property from getting the tooltip of another one with the same name return PropertyField(property, title); } static readonly Dictionary s_HeadersGuiContents = new Dictionary(); /// /// Draws a header into the inspector with the given title /// /// The title for the header protected void DrawHeader(string header) { if (!s_HeadersGuiContents.TryGetValue(header, out GUIContent content)) { content = EditorGUIUtility.TrTextContent(header); s_HeadersGuiContents.Add(header, content); } EditorGUILayout.Space(4); var rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight); EditorGUI.LabelField(rect, content, EditorStyles.boldLabel); } /// /// Handles unity built-in decorators (Space, Header, Tooltips, ...) from attributes /// /// The property to obtain the attributes and handle the decorators /// A custom label and/or tooltip that might be updated by and/or by internal 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: DrawHeader(headerAttribute.header); break; case TooltipAttribute tooltipAttribute: if (string.IsNullOrEmpty(title.tooltip)) title.tooltip = tooltipAttribute.tooltip; break; case InspectorNameAttribute inspectorNameAttribute: title.text = inspectorNameAttribute.displayName; break; } } } /// /// Get indentation from Indent attribute /// /// The property to obtain the attributes /// The relative indent level change int HandleRelativeIndentation(SerializedDataParameter property) { foreach (var attr in property.attributes) { if (attr is VolumeComponent.Indent indent) return indent.relativeAmount; } return 0; } /// /// Draws a given in the editor using a custom label /// and tooltip. /// /// The property to draw in the editor. /// A custom label and/or tooltip. /// true if the property field has been rendered protected bool PropertyField(SerializedDataParameter property, GUIContent title) { if (VolumeParameter.IsObjectParameter(property.referenceType)) return DrawEmbeddedField(property, title); else return DrawPropertyField(property, title); } /// /// Draws a given in the editor using a custom label /// and tooltip. /// /// The property to draw in the editor. /// A custom label and/or tooltip. private bool DrawPropertyField(SerializedDataParameter property, GUIContent title) { using (var scope = new OverridablePropertyScope(property, title, this)) { if (!scope.displayed) return false; // Custom drawer if (scope.drawer?.OnGUI(property, title) ?? false) return true; // Standard Unity drawer EditorGUILayout.PropertyField(property.value, title); } return true; } /// /// Draws a given in the editor using a custom label /// and tooltip. This variant is only for embedded class / struct /// /// The property to draw in the editor. /// A custom label and/or tooltip. private bool DrawEmbeddedField(SerializedDataParameter property, GUIContent title) { bool isAdditionalProperty = property.GetAttribute() != null; bool displayed = !isAdditionalProperty || BeginAdditionalPropertiesScope(); if (!displayed) return false; // Custom parameter drawer s_ParameterDrawers.TryGetValue(property.referenceType, out VolumeParameterDrawer drawer); if (drawer != null && !drawer.IsAutoProperty()) if (drawer.OnGUI(property, title)) { if (isAdditionalProperty) EndAdditionalPropertiesScope(); return true; } // Standard Unity drawer using (new IndentLevelScope()) { bool expanded = property?.value?.isExpanded ?? true; expanded = EditorGUILayout.Foldout(expanded, title, true); if (expanded) { // 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; } } property.value.isExpanded = expanded; } if (isAdditionalProperty) EndAdditionalPropertiesScope(); return true; } /// /// Draw a Color Field but convert the color to gamma space before displaying it in the shader. /// Using SetColor on a material does the conversion, but setting the color as vector3 in a constant buffer doesn't /// So we have to do it manually, doing it in the UI avoids having to do a migration step for existing fields /// /// The color property protected void ColorFieldLinear(SerializedDataParameter property) { var title = EditorGUIUtility.TrTextContent(property.displayName, property.GetAttribute()?.tooltip); using (var scope = new OverridablePropertyScope(property, title, this)) { if (!scope.displayed) return; // Standard Unity drawer CoreEditorUtils.ColorFieldLinear(property.value, title); } } /// /// Draws the override checkbox used by a property in the editor. /// /// The property to draw the override checkbox for 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.k_AllText, CoreEditorStyles.miniLabelButton, GUILayout.Height(height), GUILayout.Width(Styles.overrideCheckboxWidth + Styles.overrideCheckboxOffset), GUILayout.ExpandWidth(false)); // also center vertically the checkbox overrideRect.yMin += height * 0.5f - overrideToggleSize.y * 0.5f; overrideRect.xMin += Styles.overrideCheckboxOffset; property.overrideState.boolValue = GUI.Toggle(overrideRect, property.overrideState.boolValue, Styles.k_OverrideSettingText, CoreEditorStyles.smallTickbox); } /// /// Scope for property that handle: /// - Layout decorator (Space, Header) /// - Naming decorator (Tooltips, InspectorName) /// - Overridable checkbox if parameter IsAutoProperty /// - disabled GUI if Overridable checkbox (case above) is unchecked /// - additional property scope /// This is automatically used inside PropertyField method /// protected struct OverridablePropertyScope : IDisposable { bool isAdditionalProperty; VolumeComponentEditor editor; IDisposable disabledScope; IDisposable indentScope; internal bool haveCustomOverrideCheckbox { get; private set; } internal VolumeParameterDrawer drawer { get; private set; } /// /// Either the content property will be displayed or not (can varry with additional property settings) /// public bool displayed { get; private set; } /// /// The title modified regarding attribute used on the field /// public GUIContent label { get; private set; } /// /// Constructor /// /// The property that will be drawn /// The label of this property /// The editor that will draw it public OverridablePropertyScope(SerializedDataParameter property, GUIContent label, VolumeComponentEditor editor) { disabledScope = null; indentScope = null; haveCustomOverrideCheckbox = false; drawer = null; displayed = false; isAdditionalProperty = false; this.label = label; this.editor = editor; Init(property, label, editor); } /// /// Constructor /// /// The property that will be drawn /// The label of this property /// The editor that will draw it public OverridablePropertyScope(SerializedDataParameter property, string label, VolumeComponentEditor editor) { disabledScope = null; indentScope = null; haveCustomOverrideCheckbox = false; drawer = null; displayed = false; isAdditionalProperty = false; this.label = EditorGUIUtility.TrTextContent(label); this.editor = editor; Init(property, this.label, editor); } void Init(SerializedDataParameter property, GUIContent label, VolumeComponentEditor editor) { // Below, 3 is horizontal spacing and there is one between label and field and another between override checkbox and label EditorGUIUtility.labelWidth -= Styles.overrideCheckboxWidth + Styles.overrideCheckboxOffset + 3 + 3; isAdditionalProperty = property.GetAttribute() != null; displayed = !isAdditionalProperty || editor.BeginAdditionalPropertiesScope(); s_ParameterDrawers.TryGetValue(property.referenceType, out VolumeParameterDrawer vpd); drawer = vpd; //never draw override for embedded class/struct haveCustomOverrideCheckbox = (displayed && !(drawer?.IsAutoProperty() ?? true)) || VolumeParameter.IsObjectParameter(property.referenceType); if (displayed) { editor.HandleDecorators(property, label); int relativeIndentation = editor.HandleRelativeIndentation(property); int indent = relativeIndentation * 15; if (haveCustomOverrideCheckbox) indent += 15; if (indent != 0) indentScope = new IndentLevelScope(indent); if (!haveCustomOverrideCheckbox) { EditorGUILayout.BeginHorizontal(); if (editor.enableOverrides) editor.DrawOverrideCheckbox(property); disabledScope = new EditorGUI.DisabledScope(!property.overrideState.boolValue); } } } /// /// Dispose of the class /// void IDisposable.Dispose() { disabledScope?.Dispose(); indentScope?.Dispose(); if (!haveCustomOverrideCheckbox && displayed) EditorGUILayout.EndHorizontal(); if (isAdditionalProperty) editor.EndAdditionalPropertiesScope(); EditorGUIUtility.labelWidth += Styles.overrideCheckboxWidth + Styles.overrideCheckboxOffset + 3 + 3; } } /// /// Like EditorGUI.IndentLevelScope but this one will also indent the override checkboxes. /// protected class IndentLevelScope : GUI.Scope { int m_Offset; /// /// Constructor /// /// [optional] Change the indentation offset public IndentLevelScope(int offset = 15) { m_Offset = offset; // When using EditorGUI.indentLevel++, the clicking on the checkboxes does not work properly due to some issues on the C++ side. // This scope is a work-around for this issue. GUILayout.BeginHorizontal(); EditorGUILayout.Space(offset, false); GUIStyle style = new GUIStyle(); GUILayout.BeginVertical(style); EditorGUIUtility.labelWidth -= m_Offset; } /// /// Closes the scope /// protected override void CloseScope() { EditorGUIUtility.labelWidth += m_Offset; GUILayout.EndVertical(); GUILayout.EndHorizontal(); } } /// /// Whether to draw the UI elements related to overrides. /// public bool enableOverrides { get; set; } = true; } }