using UnityEngine; using UnityEditor; using System; using System.Collections.Generic; using UnityEngine.UIElements; using UnityEditor.UIElements; namespace Unity.Cinemachine.Editor { [CustomPropertyDrawer(typeof(LensSettingsHideModeOverridePropertyAttribute))] class LensSettingsHideModeOverridePropertyDrawer : LensSettingsPropertyDrawer { public LensSettingsHideModeOverridePropertyDrawer() => HideModeOverride = true; } [CustomPropertyDrawer(typeof(LensSettings))] class LensSettingsPropertyDrawer : PropertyDrawer { static LensSettings s_Def = new (); // to access name strings static bool IsOrtho(SerializedProperty property) => AccessProperty( typeof(LensSettings), SerializedPropertyHelper.GetPropertyValue(property), "Orthographic"); static bool IsPhysical(SerializedProperty property) => AccessProperty( typeof(LensSettings), SerializedPropertyHelper.GetPropertyValue(property), "IsPhysicalCamera"); static float Aspect(SerializedProperty property) => AccessProperty( typeof(LensSettings), SerializedPropertyHelper.GetPropertyValue(property), "Aspect"); static bool UseHorizontalFOV(SerializedProperty property) => AccessProperty( typeof(LensSettings), SerializedPropertyHelper.GetPropertyValue(property), "UseHorizontalFOV"); static T AccessProperty(Type type, object obj, string memberName) { if (string.IsNullOrEmpty(memberName) || (type == null)) return default; System.Reflection.BindingFlags bindingFlags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic; if (obj != null) bindingFlags |= System.Reflection.BindingFlags.Instance; else bindingFlags |= System.Reflection.BindingFlags.Static; System.Reflection.PropertyInfo pi = type.GetProperty(memberName, bindingFlags); if ((pi != null) && (pi.PropertyType == typeof(T))) return (T)pi.GetValue(obj, null); else return default; } static List m_PresetOptions; static List m_PhysicalPresetOptions; const string k_AddPresetsLabel = "New Palette entry with these Settings..."; const string k_EditPresetsLabel = "Edit Palette..."; const string k_PaletteLabel = "Palette..."; float m_PreviousAspect; protected bool HideModeOverride { get; set; } void InitPresetOptions() { m_PresetOptions ??= new List(); m_PresetOptions.Clear(); var palette = CinemachineLensPalette.InstanceIfExists; for (int i = 0; palette != null && i < palette.Presets.Count; ++i) m_PresetOptions.Add(palette.Presets[i].Name); m_PresetOptions.Add(""); m_PresetOptions.Add(k_AddPresetsLabel); m_PresetOptions.Add(k_EditPresetsLabel); var physicalPresets = CinemachinePhysicalLensPalette.InstanceIfExists; m_PhysicalPresetOptions ??= new List(); m_PhysicalPresetOptions.Clear(); for (int i = 0; physicalPresets != null && i < physicalPresets.Presets.Count; ++i) m_PhysicalPresetOptions.Add(physicalPresets.Presets[i].Name); m_PhysicalPresetOptions.Add(""); m_PhysicalPresetOptions.Add(k_AddPresetsLabel); m_PhysicalPresetOptions.Add(k_EditPresetsLabel); } public override VisualElement CreatePropertyGUI(SerializedProperty property) { InitPresetOptions(); var ux = new VisualElement(); // When foldout is closed, we display the FOV on the same line, for convenience var foldout = new Foldout { text = property.displayName, tooltip = property.tooltip, value = property.isExpanded }; foldout.BindProperty(property); var outerFovControl = new FovPropertyControl(property, true) { style = { flexGrow = 1 }}; ux.Add(new InspectorUtility.FoldoutWithOverlay( foldout, outerFovControl, outerFovControl.ShortLabel) { style = { flexGrow = 1 }}); //outerFovControl.OnInitialGeometry(() => outerFovControl.SafeSetIsDelayed()); // Populate the foldout var innerFovControl = foldout.AddChild(new FovPropertyControl(property, false) { style = { flexGrow = 1 }}); //innerFovControl.OnInitialGeometry(() => innerFovControl.SafeSetIsDelayed()); var nearClip = property.FindPropertyRelative(() => s_Def.NearClipPlane); foldout.AddChild(new PropertyField(nearClip)).RegisterValueChangeCallback((evt) => { if (!IsOrtho(property) && nearClip.floatValue < 0.01f) { nearClip.floatValue = 0.01f; property.serializedObject.ApplyModifiedPropertiesWithoutUndo(); } }); foldout.Add(new PropertyField(property.FindPropertyRelative(() => s_Def.FarClipPlane))); foldout.Add(new PropertyField(property.FindPropertyRelative(() => s_Def.Dutch))); var physical = foldout.AddChild(new PropertyField(property.FindPropertyRelative(() => s_Def.PhysicalProperties))); SerializedProperty modeOverrideProperty = null; VisualElement modeHelp = null; if (!HideModeOverride) { modeOverrideProperty = property.FindPropertyRelative(() => s_Def.ModeOverride); modeHelp = foldout.AddChild( new HelpBox("Lens Mode Override must be enabled in the Cinemachine Brain for Mode Override to take effect", HelpBoxMessageType.Warning)); foldout.AddChild(new PropertyField(modeOverrideProperty)).TrackPropertyValue( modeOverrideProperty, (p) => InspectorUtility.RepaintGameView()); } // GML: This is rather evil. Is there a better (event-driven) way? DoUpdate(); ux.schedule.Execute(DoUpdate).Every(250); void DoUpdate() { if (property.serializedObject.targetObject == null) return; // target deleted // We need to track the aspect ratio because HFOV display is dependent on it var isHorizontal = UseHorizontalFOV(property); var aspect = Aspect(property); bool aspectChanged = isHorizontal && m_PreviousAspect != aspect; m_PreviousAspect = aspect; bool isPhysical = IsPhysical(property); physical.SetVisible(isPhysical); outerFovControl.TimedUpdate(aspectChanged); innerFovControl.TimedUpdate(aspectChanged); if (!HideModeOverride) { var brainHasModeOverride = CinemachineBrain.ActiveBrainCount > 0 && CinemachineBrain.GetActiveBrain(0).LensModeOverride.Enabled; modeHelp.SetVisible(!brainHasModeOverride && modeOverrideProperty.intValue != (int)LensSettings.OverrideModes.None); } }; // In case presets asset gets deleted or modified externally ux.TrackAnyUserActivity(InitPresetOptions); return ux; } /// /// Make the complicated FOV widget which works in 4 modes, with preset popups /// and optional weird small label display /// class FovPropertyControl : InspectorUtility.LabeledRow { public readonly Label ShortLabel; readonly SerializedProperty m_LensProperty; readonly SerializedProperty m_SensorSizeProperty; readonly FloatField m_Control; readonly PopupField m_Presets; enum Modes { Ortho, Physical, VFOV, HFOV }; Modes GetLensMode() { if (IsOrtho(m_LensProperty)) return Modes.Ortho; if (IsPhysical(m_LensProperty)) return Modes.Physical; return UseHorizontalFOV(m_LensProperty) ? Modes.HFOV : Modes.VFOV; } public FovPropertyControl(SerializedProperty property, bool hideLabel) : base(hideLabel ? "" : "(fov)") { style.flexDirection = FlexDirection.Row; m_LensProperty = property; var physicalProp = property.FindPropertyRelative(() => s_Def.PhysicalProperties); m_SensorSizeProperty = physicalProp.FindPropertyRelative(() => s_Def.PhysicalProperties.SensorSize); m_Control = Contents.AddChild(new FloatField("") { style = {flexBasis = 20, flexGrow = 2, marginLeft = 0}}); m_Control.RegisterValueChangedCallback(OnControlValueChanged); Label.SetVisible(!hideLabel); Label.AddToClassList("unity-base-field__label--with-dragger"); new FieldMouseDragger(m_Control).SetDragZone(Label); m_Presets = Contents.AddChild(new PopupField { tooltip = "Customizable Lens Palette", style = {flexBasis = 20, flexGrow = 1}}); m_Presets.RegisterValueChangedCallback(OnPresetValueChanged); ShortLabel = new Label("X") { style = { alignSelf = Align.Center, opacity = 0.5f }}; ShortLabel.AddToClassList("unity-base-field__label--with-dragger"); new FieldMouseDragger(m_Control).SetDragZone(ShortLabel); this.TrackPropertyWithInitialCallback(property, OnLensPropertyChanged); } void OnControlValueChanged(ChangeEvent evt) { var mode = GetLensMode(); switch (mode) { case Modes.Ortho: { var orthoProp = m_LensProperty.FindPropertyRelative(() => s_Def.OrthographicSize); orthoProp.floatValue = evt.newValue; orthoProp.serializedObject.ApplyModifiedProperties(); break; } case Modes.Physical: { // Convert and clamp var vfov = FocalLengthToFov(evt.newValue); vfov = Mathf.Clamp(vfov, 1, 179); m_Control.SetValueWithoutNotify(FovToFocalLength(vfov)); // Push to property var fovProp = m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView); fovProp.floatValue = vfov; fovProp.serializedObject.ApplyModifiedProperties(); break; } case Modes.VFOV: case Modes.HFOV: { var newValue = Mathf.Clamp(evt.newValue, 1, 179); m_Control.SetValueWithoutNotify(newValue); // Convert from display units if (mode == Modes.HFOV) newValue = Camera.HorizontalToVerticalFieldOfView(newValue, Aspect(m_LensProperty)); // Push to property var fovProp = m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView); fovProp.floatValue = newValue; fovProp.serializedObject.ApplyModifiedProperties(); break; } } } void OnLensPropertyChanged(SerializedProperty p) { var mode = GetLensMode(); switch (mode) { case Modes.Ortho: { var orthoProp = m_LensProperty.FindPropertyRelative(() => s_Def.OrthographicSize); m_Control.SetValueWithoutNotify(orthoProp.floatValue); break; } case Modes.Physical: { // Convert to display FolcalLength units var fovProp = m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView); var v = FovToFocalLength(fovProp.floatValue); m_Control.SetValueWithoutNotify(v); // Sync the presets var presets = CinemachinePhysicalLensPalette.InstanceIfExists; var index = presets == null ? -1 : presets.GetMatchingPreset(new () { FocalLength = v, PhysicalProperties = ReadPhysicalSettings() }); m_Presets.SetValueWithoutNotify(index < 0 ? k_PaletteLabel : presets.Presets[index].Name); break; } case Modes.VFOV: case Modes.HFOV: { // Convert to display FOV units var fovProp = m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView); var v = fovProp.floatValue; if (mode == Modes.HFOV) v = Camera.VerticalToHorizontalFieldOfView(v, Aspect(m_LensProperty)); m_Control.SetValueWithoutNotify(v); // Sync the presets var presets = CinemachineLensPalette.InstanceIfExists; var index = presets == null ? -1 : presets.GetMatchingPreset(fovProp.floatValue); m_Presets.SetValueWithoutNotify(index < 0 ? k_PaletteLabel : presets.Presets[index].Name); break; } } TimedUpdate(false); } public void TimedUpdate(bool aspectChanged) { switch (GetLensMode()) { case Modes.Ortho: { var text = "O"; if (ShortLabel.text != text) { var orthoProp = m_LensProperty.FindPropertyRelative(() => s_Def.OrthographicSize); ShortLabel.text = text; Label.text = orthoProp.displayName; Label.tooltip = m_Control.tooltip = ShortLabel.tooltip = orthoProp.tooltip; m_Presets.SetVisible(false); } break; } case Modes.Physical: { var text = "F"; if (ShortLabel.text != text) { var fovProp = m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView); ShortLabel.text = text; Label.text = ShortLabel.tooltip = "Focal Length"; Label.tooltip = m_Control.tooltip = fovProp.tooltip; m_Presets.choices = m_PhysicalPresetOptions; m_Presets.SetVisible(true); } break; } case Modes.HFOV: { var text = "H"; if (ShortLabel.text != text) { var fovProp = m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView); ShortLabel.text = text; Label.text = ShortLabel.tooltip = "Horizontal FOV"; Label.tooltip = m_Control.tooltip = fovProp.tooltip; m_Presets.choices = m_PresetOptions; m_Presets.SetVisible(true); } // No event-driven way to detect this. Our display depends on aspect if (aspectChanged) OnLensPropertyChanged(m_LensProperty); break; } case Modes.VFOV: { var text = "V"; if (ShortLabel.text != text) { var fovProp = m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView); ShortLabel.text = text; Label.text = ShortLabel.tooltip = "Vertical FOV"; Label.tooltip = m_Control.tooltip = fovProp.tooltip; m_Presets.choices = m_PresetOptions; m_Presets.SetVisible(true); } break; } } } float FocalLengthToFov(float focal) => Camera.FocalLengthToFieldOfView(Mathf.Max(0.01f, focal), m_SensorSizeProperty.vector2Value.y); float FovToFocalLength(float vfov) => Camera.FieldOfViewToFocalLength(vfov, m_SensorSizeProperty.vector2Value.y); void OnPresetValueChanged(ChangeEvent evt) { if (GetLensMode() == Modes.Physical) { // Physical presets var palette = CinemachinePhysicalLensPalette.Instance; if (palette != null) { // Edit the presets assets if desired if (evt.newValue == k_EditPresetsLabel) Selection.activeObject = palette; else if (evt.newValue == k_AddPresetsLabel) { Selection.activeObject = palette; Undo.RecordObject(palette, "add palette entry"); palette.Presets.Add(new () { Name = $"{m_Control.value}mm preset {palette.Presets.Count + 1}", FocalLength = m_Control.value, PhysicalProperties = ReadPhysicalSettings() }); } else { // Apply the preset var index = palette.GetPresetIndex(evt.newValue); if (index >= 0) { var v = palette.Presets[index]; m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView).floatValue = FocalLengthToFov(v.FocalLength); WritePhysicalSettings(v.PhysicalProperties); m_LensProperty.serializedObject.ApplyModifiedProperties(); return; } } } } else { // Nonphysical Presets var palette = CinemachineLensPalette.Instance; if (palette != null) { var fovProp = m_LensProperty.FindPropertyRelative(() => s_Def.FieldOfView); // Edit the presets assets if desired if (evt.newValue == k_EditPresetsLabel) Selection.activeObject = palette; else if (evt.newValue == k_AddPresetsLabel) { Selection.activeObject = palette; Undo.RecordObject(palette, "add palette entry"); palette.Presets.Add(new () { Name = $"{fovProp.floatValue} preset {palette.Presets.Count + 1}", VerticalFOV = fovProp.floatValue, }); } else { // Apply the preset var index = palette.GetPresetIndex(evt.newValue); if (index >= 0) fovProp.floatValue = palette.Presets[index].VerticalFOV; m_LensProperty.serializedObject.ApplyModifiedProperties(); } } } m_Presets.SetValueWithoutNotify(string.Empty); } LensSettings.PhysicalSettings ReadPhysicalSettings() { var p = m_LensProperty.FindPropertyRelative(() => s_Def.PhysicalProperties); return new () { GateFit = (Camera.GateFitMode)p.FindPropertyRelative(() => s_Def.PhysicalProperties.GateFit).intValue, SensorSize = p.FindPropertyRelative(() => s_Def.PhysicalProperties.SensorSize).vector2Value, LensShift = p.FindPropertyRelative(() => s_Def.PhysicalProperties.LensShift).vector2Value, Iso = p.FindPropertyRelative(() => s_Def.PhysicalProperties.Iso).intValue, ShutterSpeed = p.FindPropertyRelative(() => s_Def.PhysicalProperties.ShutterSpeed).floatValue, Aperture = p.FindPropertyRelative(() => s_Def.PhysicalProperties.Aperture).floatValue, BladeCount = p.FindPropertyRelative(() => s_Def.PhysicalProperties.BladeCount).intValue, Curvature = p.FindPropertyRelative(() => s_Def.PhysicalProperties.Curvature).vector2Value, BarrelClipping = p.FindPropertyRelative(() => s_Def.PhysicalProperties.BarrelClipping).floatValue, Anamorphism =p.FindPropertyRelative(() => s_Def.PhysicalProperties.Anamorphism).floatValue }; } void WritePhysicalSettings(in LensSettings.PhysicalSettings s) { var p = m_LensProperty.FindPropertyRelative(() => s_Def.PhysicalProperties); p.FindPropertyRelative(() => s_Def.PhysicalProperties.GateFit).intValue = (int)s.GateFit; p.FindPropertyRelative(() => s_Def.PhysicalProperties.SensorSize).vector2Value = s.SensorSize; p.FindPropertyRelative(() => s_Def.PhysicalProperties.LensShift).vector2Value = s.LensShift; p.FindPropertyRelative(() => s_Def.PhysicalProperties.Iso).intValue = s.Iso; p.FindPropertyRelative(() => s_Def.PhysicalProperties.ShutterSpeed).floatValue = s.ShutterSpeed; p.FindPropertyRelative(() => s_Def.PhysicalProperties.Aperture).floatValue = s.Aperture; p.FindPropertyRelative(() => s_Def.PhysicalProperties.BladeCount).intValue = s.BladeCount; p.FindPropertyRelative(() => s_Def.PhysicalProperties.Curvature).vector2Value = s.Curvature; p.FindPropertyRelative(() => s_Def.PhysicalProperties.BarrelClipping).floatValue = s.BarrelClipping; p.FindPropertyRelative(() => s_Def.PhysicalProperties.Anamorphism).floatValue = s.Anamorphism; } } } }