using System; using System.Collections.Generic; using System.Reflection; using System.Text.RegularExpressions; using UnityEngine; using UnityEngine.Rendering.Universal; using Object = UnityEngine.Object; namespace UnityEditor.Rendering.Universal { [CustomEditor(typeof(ScriptableRendererData), true)] public class ScriptableRendererDataEditor : Editor { class Styles { public static readonly GUIContent RenderFeatures = new GUIContent("Renderer Features", "A Renderer Feature is an asset that lets you add extra Render passes to a URP Renderer and configure their behavior."); public static readonly GUIContent PassNameField = new GUIContent("Name", "Render pass name. This name is the name displayed in Frame Debugger."); public static readonly GUIContent MissingFeature = new GUIContent("Missing RendererFeature", "Missing reference, due to compilation issues or missing files. you can attempt auto fix or choose to remove the feature."); public static GUIStyle BoldLabelSimple; static Styles() { BoldLabelSimple = new GUIStyle(EditorStyles.label); BoldLabelSimple.fontStyle = FontStyle.Bold; } } private SerializedProperty m_RendererFeatures; private SerializedProperty m_RendererFeaturesMap; private SerializedProperty m_FalseBool; [SerializeField] private bool falseBool = false; List m_Editors = new List(); private void OnEnable() { m_RendererFeatures = serializedObject.FindProperty(nameof(ScriptableRendererData.m_RendererFeatures)); m_RendererFeaturesMap = serializedObject.FindProperty(nameof(ScriptableRendererData.m_RendererFeatureMap)); var editorObj = new SerializedObject(this); m_FalseBool = editorObj.FindProperty(nameof(falseBool)); UpdateEditorList(); } private void OnDisable() { ClearEditorsList(); } public override void OnInspectorGUI() { if (m_RendererFeatures == null) OnEnable(); else if (m_RendererFeatures.arraySize != m_Editors.Count) UpdateEditorList(); serializedObject.Update(); DrawRendererFeatureList(); } private void DrawRendererFeatureList() { EditorGUILayout.LabelField(Styles.RenderFeatures, EditorStyles.boldLabel); EditorGUILayout.Space(); if (m_RendererFeatures.arraySize == 0) { EditorGUILayout.HelpBox("No Renderer Features added", MessageType.Info); } else { //Draw List CoreEditorUtils.DrawSplitter(); for (int i = 0; i < m_RendererFeatures.arraySize; i++) { SerializedProperty renderFeaturesProperty = m_RendererFeatures.GetArrayElementAtIndex(i); DrawRendererFeature(i, ref renderFeaturesProperty); CoreEditorUtils.DrawSplitter(); } } EditorGUILayout.Space(); //Add renderer if (GUILayout.Button("Add Renderer Feature", EditorStyles.miniButton)) { AddPassMenu(); } } private bool GetCustomTitle(Type type, out string title) { var isSingleFeature = type.GetCustomAttribute(); if (isSingleFeature != null) { title = isSingleFeature.customTitle; return title != null; } title = null; return false; } private bool GetTooltip(Type type, out string tooltip) { var attribute = type.GetCustomAttribute(); if (attribute != null) { tooltip = attribute.tooltip; return true; } tooltip = string.Empty; return false; } private void DrawRendererFeature(int index, ref SerializedProperty renderFeatureProperty) { Object rendererFeatureObjRef = renderFeatureProperty.objectReferenceValue; if (rendererFeatureObjRef != null) { bool hasChangedProperties = false; string title; bool hasCustomTitle = GetCustomTitle(rendererFeatureObjRef.GetType(), out title); if (!hasCustomTitle) { title = ObjectNames.GetInspectorTitle(rendererFeatureObjRef); } string tooltip; GetTooltip(rendererFeatureObjRef.GetType(), out tooltip); // Get the serialized object for the editor script & update it Editor rendererFeatureEditor = m_Editors[index]; SerializedObject serializedRendererFeaturesEditor = rendererFeatureEditor.serializedObject; serializedRendererFeaturesEditor.Update(); // Foldout header EditorGUI.BeginChangeCheck(); SerializedProperty activeProperty = serializedRendererFeaturesEditor.FindProperty("m_Active"); bool displayContent = CoreEditorUtils.DrawHeaderToggle(EditorGUIUtility.TrTextContent(title, tooltip), renderFeatureProperty, activeProperty, pos => OnContextClick(pos, index)); hasChangedProperties |= EditorGUI.EndChangeCheck(); // ObjectEditor if (displayContent) { if (!hasCustomTitle) { EditorGUI.BeginChangeCheck(); SerializedProperty nameProperty = serializedRendererFeaturesEditor.FindProperty("m_Name"); nameProperty.stringValue = ValidateName(EditorGUILayout.DelayedTextField(Styles.PassNameField, nameProperty.stringValue)); if (EditorGUI.EndChangeCheck()) { hasChangedProperties = true; // We need to update sub-asset name rendererFeatureObjRef.name = nameProperty.stringValue; AssetDatabase.SaveAssets(); // Triggers update for sub-asset name change ProjectWindowUtil.ShowCreatedAsset(target); } } EditorGUI.BeginChangeCheck(); rendererFeatureEditor.OnInspectorGUI(); hasChangedProperties |= EditorGUI.EndChangeCheck(); EditorGUILayout.Space(EditorGUIUtility.singleLineHeight); } // Apply changes and save if the user has modified any settings if (hasChangedProperties) { serializedRendererFeaturesEditor.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties(); ForceSave(); } } else { CoreEditorUtils.DrawHeaderToggle(Styles.MissingFeature, renderFeatureProperty, m_FalseBool, pos => OnContextClick(pos, index)); m_FalseBool.boolValue = false; // always make sure false bool is false EditorGUILayout.HelpBox(Styles.MissingFeature.tooltip, MessageType.Error); if (GUILayout.Button("Attempt Fix", EditorStyles.miniButton)) { ScriptableRendererData data = target as ScriptableRendererData; data.ValidateRendererFeatures(); } } } private void OnContextClick(Vector2 position, int id) { var menu = new GenericMenu(); if (id == 0) menu.AddDisabledItem(EditorGUIUtility.TrTextContent("Move Up")); else menu.AddItem(EditorGUIUtility.TrTextContent("Move Up"), false, () => MoveComponent(id, -1)); if (id == m_RendererFeatures.arraySize - 1) menu.AddDisabledItem(EditorGUIUtility.TrTextContent("Move Down")); else menu.AddItem(EditorGUIUtility.TrTextContent("Move Down"), false, () => MoveComponent(id, 1)); menu.AddSeparator(string.Empty); menu.AddItem(EditorGUIUtility.TrTextContent("Remove"), false, () => RemoveComponent(id)); menu.DropDown(new Rect(position, Vector2.zero)); } private void AddPassMenu() { GenericMenu menu = new GenericMenu(); TypeCache.TypeCollection types = TypeCache.GetTypesDerivedFrom(); foreach (Type type in types) { var data = target as ScriptableRendererData; if (data.DuplicateFeatureCheck(type)) { continue; } string path = GetMenuNameFromType(type); menu.AddItem(new GUIContent(path), false, AddComponent, type.Name); } menu.ShowAsContext(); } private void AddComponent(object type) { serializedObject.Update(); ScriptableObject component = CreateInstance((string)type); component.name = $"{(string)type}"; Undo.RegisterCreatedObjectUndo(component, "Add Renderer Feature"); // Store this new effect as a sub-asset so we can reference it safely afterwards // Only when we're not dealing with an instantiated asset if (EditorUtility.IsPersistent(target)) { AssetDatabase.AddObjectToAsset(component, target); } AssetDatabase.TryGetGUIDAndLocalFileIdentifier(component, out var guid, out long localId); // Grow the list first, then add - that's how serialized lists work in Unity m_RendererFeatures.arraySize++; SerializedProperty componentProp = m_RendererFeatures.GetArrayElementAtIndex(m_RendererFeatures.arraySize - 1); componentProp.objectReferenceValue = component; // Update GUID Map m_RendererFeaturesMap.arraySize++; SerializedProperty guidProp = m_RendererFeaturesMap.GetArrayElementAtIndex(m_RendererFeaturesMap.arraySize - 1); guidProp.longValue = localId; UpdateEditorList(); serializedObject.ApplyModifiedProperties(); // Force save / refresh if (EditorUtility.IsPersistent(target)) { ForceSave(); } serializedObject.ApplyModifiedProperties(); } private void RemoveComponent(int id) { SerializedProperty property = m_RendererFeatures.GetArrayElementAtIndex(id); Object component = property.objectReferenceValue; property.objectReferenceValue = null; Undo.SetCurrentGroupName(component == null ? "Remove Renderer Feature" : $"Remove {component.name}"); // remove the array index itself from the list m_RendererFeatures.DeleteArrayElementAtIndex(id); m_RendererFeaturesMap.DeleteArrayElementAtIndex(id); UpdateEditorList(); 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. if (component != null) { Undo.DestroyObjectImmediate(component); ScriptableRendererFeature feature = component as ScriptableRendererFeature; feature?.Dispose(); } // Force save / refresh ForceSave(); } private void MoveComponent(int id, int offset) { Undo.SetCurrentGroupName("Move Render Feature"); serializedObject.Update(); m_RendererFeatures.MoveArrayElement(id, id + offset); m_RendererFeaturesMap.MoveArrayElement(id, id + offset); UpdateEditorList(); serializedObject.ApplyModifiedProperties(); // Force save / refresh ForceSave(); } private string GetMenuNameFromType(Type type) { string path; if (!GetCustomTitle(type, out path)) { path = ObjectNames.NicifyVariableName(type.Name); } if (type.Namespace != null) { if (type.Namespace.Contains("Experimental")) path += " (Experimental)"; } return path; } private string ValidateName(string name) { name = Regex.Replace(name, @"[^a-zA-Z0-9 ]", ""); return name; } private void UpdateEditorList() { ClearEditorsList(); for (int i = 0; i < m_RendererFeatures.arraySize; i++) { m_Editors.Add(CreateEditor(m_RendererFeatures.GetArrayElementAtIndex(i).objectReferenceValue)); } } //To avoid leaking memory we destroy editors when we clear editors list private void ClearEditorsList() { for (int i = m_Editors.Count - 1; i >= 0; --i) { DestroyImmediate(m_Editors[i]); } m_Editors.Clear(); } private void ForceSave() { EditorUtility.SetDirty(target); } } }