You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
329 lines
16 KiB
329 lines
16 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using UnityEditor.Rendering.Analytics;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace UnityEditor.Rendering
|
|
{
|
|
[CustomEditor(typeof(Volume))]
|
|
[CanEditMultipleObjects]
|
|
sealed class VolumeEditor : Editor
|
|
{
|
|
const string k_TemplatePath = "Packages/com.unity.render-pipelines.core/Editor/UXML/VolumeEditor.uxml";
|
|
|
|
static class Styles
|
|
{
|
|
public static readonly string isGlobalDropdownTooltip = L10n.Tr("Global Volumes affect the Camera wherever the Camera is in the Scene. Local Volumes affect the Camera if they encapsulate the Camera within the bounds of their Collider.");
|
|
public static readonly GUIContent addBoxCollider = EditorGUIUtility.TrTextContent("Add a Box Collider");
|
|
public static readonly GUIContent sphereBoxCollider = EditorGUIUtility.TrTextContent("Add a Sphere Collider");
|
|
public static readonly GUIContent capsuleBoxCollider = EditorGUIUtility.TrTextContent("Add a Capsule Collider");
|
|
public static readonly GUIContent meshBoxCollider = EditorGUIUtility.TrTextContent("Add a Mesh Collider");
|
|
public static readonly GUIContent addColliderFixMessage = EditorGUIUtility.TrTextContentWithIcon("Add a Collider to this GameObject to set boundaries for the local Volume.", CoreEditorStyles.iconWarn);
|
|
public static readonly GUIContent disableColliderFixMessage = EditorGUIUtility.TrTextContentWithIcon("Global Volumes do not need a collider. Disable or remove the collider.", CoreEditorStyles.iconWarn);
|
|
public static readonly GUIContent enableColliderFixMessage = EditorGUIUtility.TrTextContentWithIcon("Local Volumes need a collider enabled. Enable the collider.", CoreEditorStyles.iconWarn);
|
|
public static readonly GUIContent newLabel = EditorGUIUtility.TrTextContent("New", "Create a new profile.");
|
|
public static readonly GUIContent saveLabel = EditorGUIUtility.TrTextContent("Save", "Save the instantiated profile");
|
|
public static readonly GUIContent cloneLabel = EditorGUIUtility.TrTextContent("Clone", "Create a new profile and copy the content of the currently assigned profile.");
|
|
public static readonly GUIContent enableAll = EditorGUIUtility.TrTextContent("Enable All");
|
|
public static readonly GUIContent disableAll = EditorGUIUtility.TrTextContent("Disable All");
|
|
public static readonly GUIContent removeAll = EditorGUIUtility.TrTextContent("Remove All");
|
|
}
|
|
|
|
SerializedProperty m_IsGlobal;
|
|
SerializedProperty m_Profile;
|
|
VolumeComponentListEditor m_ComponentList;
|
|
|
|
Volume targetVolume => target as Volume;
|
|
VolumeProfile profileRef => targetVolume.HasInstantiatedProfile() ? targetVolume.profile : targetVolume.sharedProfile;
|
|
|
|
void OnEnable()
|
|
{
|
|
var o = new PropertyFetcher<Volume>(serializedObject);
|
|
m_IsGlobal = o.Find(x => x.isGlobal);
|
|
m_Profile = o.Find(x => x.sharedProfile);
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
m_ComponentList?.Clear();
|
|
}
|
|
|
|
public override VisualElement CreateInspectorGUI()
|
|
{
|
|
var template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_TemplatePath);
|
|
var root = template.Instantiate();
|
|
|
|
string ModeIntToString(int modeInt) => modeInt == 1 ? "Global" : "Local";
|
|
var modeList = new List<int> { 0, 1 };
|
|
var isGlobalDropdown = new PopupField<int>("Mode", modeList, m_IsGlobal.boolValue ? 1 : 0, ModeIntToString, ModeIntToString);
|
|
isGlobalDropdown.tooltip = Styles.isGlobalDropdownTooltip;
|
|
isGlobalDropdown.AddToClassList("unity-base-field__aligned");
|
|
isGlobalDropdown.RegisterValueChangedCallback(evt =>
|
|
{
|
|
m_IsGlobal.boolValue = evt.newValue != 0;
|
|
serializedObject.ApplyModifiedProperties();
|
|
});
|
|
|
|
// This is required to get notified when the property changes through Undo/Redo
|
|
isGlobalDropdown.TrackPropertyValue(m_IsGlobal, property =>
|
|
{
|
|
isGlobalDropdown.SetValueWithoutNotify(property.boolValue ? 1 : 0);
|
|
});
|
|
|
|
// Note: Must insert directly into root at the appropriate index, using a container VisualElement breaks
|
|
// alignment to other PropertyFields (achieved via the "unity-base-field__aligned" class).
|
|
root.Insert(0, isGlobalDropdown);
|
|
|
|
// Fix me boxes
|
|
var blendDistancePropertyField = root.Q<PropertyField>("volume-profile-blend-distance");
|
|
root.Q("collider-fixme-box__container").Add(new IMGUIContainer(() =>
|
|
{
|
|
bool hasCollider = targetVolume.TryGetComponent<Collider>(out var collider);
|
|
if (m_IsGlobal.boolValue) // Blend radius is not needed for global volumes
|
|
{
|
|
if (hasCollider && collider.enabled)
|
|
CoreEditorUtils.DrawFixMeBox(Styles.disableColliderFixMessage, () => SetColliderEnabledWithUndo(collider, false));
|
|
|
|
blendDistancePropertyField.style.display = DisplayStyle.None;
|
|
}
|
|
else
|
|
{
|
|
if (hasCollider)
|
|
{
|
|
if (!collider.enabled)
|
|
CoreEditorUtils.DrawFixMeBox(Styles.enableColliderFixMessage, () => SetColliderEnabledWithUndo(collider, true));
|
|
}
|
|
else
|
|
{
|
|
CoreEditorUtils.DrawFixMeBox(Styles.addColliderFixMessage, AddColliderWithUndo);
|
|
}
|
|
blendDistancePropertyField.style.display = DisplayStyle.Flex;
|
|
}
|
|
}));
|
|
|
|
var assetIcon = AssetDatabase.LoadAssetAtPath<Texture2D>(
|
|
"Packages/com.unity.render-pipelines.core/Editor/Icons/Processed/d_VolumeProfile Icon.asset");
|
|
root.Q<Image>("volume-profile-header__asset-icon").image = assetIcon;
|
|
root.Q<Image>("volume-profile-objectfield__contextmenu-image").image = CoreEditorStyles.paneOptionsIcon;
|
|
root.Q<Button>(classes: "volume-profile-objectfield__contextmenu").clicked += OnVolumeProfileContextClick;
|
|
root.Q<Button>("volume-profile-new-button").clicked += CreateNewProfile;
|
|
|
|
var volumeProfileObjectField = root.Q<ObjectField>(classes: "volume-profile-objectfield");
|
|
volumeProfileObjectField.objectType = typeof(VolumeProfile);
|
|
volumeProfileObjectField.TrackPropertyValue(m_Profile, _ =>
|
|
{
|
|
ClearInstantiatedProfile();
|
|
UpdateSelectedProfile();
|
|
UpdateElementVisibility(root);
|
|
});
|
|
|
|
// Analytics events
|
|
// NOTES:
|
|
// - schedule.Execute is needed to defer the registration until attached to panel. Otherwise the event fires during initialization.
|
|
// - PropertyField.RegisterValueChangeCallback doesn't work as expected even with schedule.Execute, so use ChangeEvent<float> instead.
|
|
volumeProfileObjectField.schedule.Execute(() => volumeProfileObjectField.RegisterValueChangedCallback(evt =>
|
|
{
|
|
VolumeProfileUsageAnalytic.Send(targetVolume, profileRef);
|
|
}));
|
|
var priorityPropertyField = root.Q<PropertyField>("volume-profile-priority");
|
|
priorityPropertyField.schedule.Execute(() => priorityPropertyField.RegisterCallback<ChangeEvent<float>>(evt =>
|
|
{
|
|
VolumePriorityUsageAnalytic.Send(targetVolume);
|
|
}));
|
|
|
|
root.Q("volume-profile-component-container").Add(new IMGUIContainer(() =>
|
|
{
|
|
// Needs updating every frame because the profile can be instantiated from user scripts
|
|
if (profileRef != m_ComponentList.asset)
|
|
{
|
|
UpdateSelectedProfile();
|
|
UpdateElementVisibility(root);
|
|
}
|
|
|
|
// UITK-IMGUI interaction hacks:
|
|
// - Set & restore labelWidth (global state), otherwise you get jumpy sliders on the componentList.
|
|
// - To match IMGUI PropertyField labelWidth with UITK one, we query a UITK label width and use that.
|
|
var oldLabelWidth = EditorGUIUtility.labelWidth;
|
|
var uitkPropertyFieldLabel = priorityPropertyField.Q<Label>();
|
|
if (uitkPropertyFieldLabel != null)
|
|
EditorGUIUtility.labelWidth = uitkPropertyFieldLabel.layout.width + 2;
|
|
|
|
if (!m_Profile.hasMultipleDifferentValues)
|
|
{
|
|
GUILayout.BeginHorizontal();
|
|
GUILayout.Space(16);
|
|
GUILayout.BeginVertical();
|
|
m_ComponentList.OnGUI();
|
|
GUILayout.EndVertical();
|
|
GUILayout.EndHorizontal();
|
|
}
|
|
|
|
EditorGUIUtility.labelWidth = oldLabelWidth;
|
|
}));
|
|
|
|
m_ComponentList = new VolumeComponentListEditor(this);
|
|
UpdateSelectedProfile();
|
|
UpdateElementVisibility(root);
|
|
|
|
Utilities.LocalizationHelper.LocalizeVisualTree(root);
|
|
|
|
return root;
|
|
}
|
|
|
|
void UpdateElementVisibility(VisualElement root)
|
|
{
|
|
root.Q<HelpBox>("volume-profile-empty-helpbox").style.display = profileRef != null ? DisplayStyle.None : DisplayStyle.Flex;
|
|
root.Q<Button>("volume-profile-new-button").style.display = profileRef != null ? DisplayStyle.None : DisplayStyle.Flex;
|
|
root.Q<Label>("volume-profile-instance-profile-label").style.display = targetVolume.HasInstantiatedProfile() ? DisplayStyle.Flex : DisplayStyle.None;
|
|
}
|
|
|
|
void RefreshEffectListEditor(VolumeProfile asset)
|
|
{
|
|
m_ComponentList.Clear();
|
|
|
|
if (asset != null)
|
|
{
|
|
asset.Sanitize();
|
|
m_ComponentList.Init(asset, new SerializedObject(asset));
|
|
}
|
|
}
|
|
|
|
void UpdateSelectedProfile()
|
|
{
|
|
serializedObject.ApplyModifiedProperties();
|
|
serializedObject.Update();
|
|
RefreshEffectListEditor(profileRef);
|
|
}
|
|
|
|
void ClearInstantiatedProfile()
|
|
{
|
|
if (targetVolume.HasInstantiatedProfile())
|
|
targetVolume.profile = null;
|
|
}
|
|
|
|
string VolumeProfileNameFromGameObject() => targetVolume.name + " Profile";
|
|
|
|
void CreateNewProfile()
|
|
{
|
|
// 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 = VolumeProfileNameFromGameObject();
|
|
var scene = targetVolume.gameObject.scene;
|
|
var asset = VolumeProfileFactory.CreateVolumeProfile(scene, targetName);
|
|
m_Profile.objectReferenceValue = asset;
|
|
|
|
UpdateSelectedProfile();
|
|
ClearInstantiatedProfile();
|
|
}
|
|
|
|
void CloneProfile()
|
|
{
|
|
// Duplicate the currently assigned profile and save it as a new profile
|
|
var origin = profileRef;
|
|
var path = AssetDatabase.GetAssetPath(m_Profile.objectReferenceValue);
|
|
var directory = path.Length == 0 ? "Assets" : Path.GetDirectoryName(path);
|
|
path = $"{directory}/{ VolumeProfileNameFromGameObject() }.asset";
|
|
|
|
path = CoreEditorUtils.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;
|
|
|
|
UpdateSelectedProfile();
|
|
ClearInstantiatedProfile();
|
|
}
|
|
|
|
void OnVolumeProfileContextClick()
|
|
{
|
|
var menu = new GenericMenu();
|
|
menu.AddItem(Styles.newLabel, false, CreateNewProfile);
|
|
|
|
if (profileRef != null)
|
|
{
|
|
var cloneLabel = targetVolume.HasInstantiatedProfile() ? Styles.saveLabel : Styles.cloneLabel;
|
|
menu.AddItem(cloneLabel, false, CloneProfile);
|
|
menu.AddSeparator(string.Empty);
|
|
menu.AddItem(VolumeProfileUtils.Styles.collapseAll, false, () =>
|
|
{
|
|
VolumeProfileUtils.SetComponentEditorsExpanded(m_ComponentList.editors, false);
|
|
});
|
|
menu.AddItem(VolumeProfileUtils.Styles.expandAll, false, () =>
|
|
{
|
|
VolumeProfileUtils.SetComponentEditorsExpanded(m_ComponentList.editors, true);
|
|
});
|
|
menu.AddSeparator(string.Empty);
|
|
menu.AddItem(Styles.enableAll, false, () => SetComponentsActive(true));
|
|
menu.AddItem(Styles.disableAll, false, () => SetComponentsActive(false));
|
|
menu.AddItem(Styles.removeAll, false, () => m_ComponentList.RemoveAllComponents());
|
|
menu.AddItem(VolumeProfileUtils.Styles.resetAll, false, () => m_ComponentList.ResetAllComponents());
|
|
menu.AddSeparator(string.Empty);
|
|
menu.AddItem(VolumeProfileUtils.Styles.copyAllSettings, false,
|
|
() => VolumeComponentCopyPaste.CopySettings(profileRef.components));
|
|
if (VolumeComponentCopyPaste.CanPaste(profileRef.components))
|
|
menu.AddItem(VolumeProfileUtils.Styles.pasteSettings, false, () =>
|
|
{
|
|
VolumeComponentCopyPaste.PasteSettings(profileRef.components);
|
|
VolumeManager.instance.OnVolumeProfileChanged(profileRef);
|
|
});
|
|
else
|
|
menu.AddDisabledItem(VolumeProfileUtils.Styles.pasteSettings);
|
|
}
|
|
|
|
menu.ShowAsContext();
|
|
}
|
|
|
|
void SetComponentsActive(bool active)
|
|
{
|
|
foreach (var c in profileRef.components)
|
|
c.active = active;
|
|
}
|
|
|
|
void SetColliderEnabledWithUndo(Collider collider, bool enabled)
|
|
{
|
|
Undo.RecordObject(collider, $"Set collider enabled to {enabled.ToString()}");
|
|
collider.enabled = enabled;
|
|
}
|
|
|
|
void AddColliderWithUndo()
|
|
{
|
|
var menu = new GenericMenu();
|
|
menu.AddItem(Styles.addBoxCollider, false, () => Undo.AddComponent<BoxCollider>(targetVolume.gameObject));
|
|
menu.AddItem(Styles.sphereBoxCollider, false,
|
|
() => Undo.AddComponent<SphereCollider>(targetVolume.gameObject));
|
|
menu.AddItem(Styles.capsuleBoxCollider, false,
|
|
() => Undo.AddComponent<CapsuleCollider>(targetVolume.gameObject));
|
|
menu.AddItem(Styles.meshBoxCollider, false, () =>
|
|
{
|
|
var meshCollider = Undo.AddComponent<MeshCollider>(targetVolume.gameObject);
|
|
meshCollider.convex = true;
|
|
});
|
|
menu.ShowAsContext();
|
|
}
|
|
}
|
|
}
|