using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using UnityEngine.SceneManagement; namespace UnityEditor.Rendering.Universal { [CustomEditor(typeof(ScreenSpaceAmbientOcclusion))] internal class ScreenSpaceAmbientOcclusionEditor : Editor, IOwningRendererDataConsumer { #region Serialized Properties private SerializedProperty m_AOMethod; private SerializedProperty m_Downsample; private SerializedProperty m_AfterOpaque; private SerializedProperty m_Source; private SerializedProperty m_NormalQuality; private SerializedProperty m_Intensity; private SerializedProperty m_DirectLightingStrength; private SerializedProperty m_Radius; private SerializedProperty m_Falloff; private SerializedProperty m_Samples; private SerializedProperty m_BlurQuality; #endregion private bool m_IsInitialized = false; private HeaderBool m_ShowQualitySettings; private HeaderBool m_ShowDeprecatedSettings; private bool m_ShowAfterOpaqueTileOnlyError; private static readonly string k_AfterOpaqueIncompatibleWithTileOnlyMode = L10n.Tr("'After Opaque' is incompatible with the enabled 'Tile-Only Mode'. Disable After Opaque."); /// /// The renderer data that owns the feature when the inspector is drawn. /// public ScriptableRendererData owningRendererData { get; set; } class HeaderBool { private string key; public bool value; internal HeaderBool(string _key, bool _default = false) { key = _key; if (EditorPrefs.HasKey(key)) value = EditorPrefs.GetBool(key); else value = _default; EditorPrefs.SetBool(key, value); } internal void SetValue(bool newValue) { value = newValue; EditorPrefs.SetBool(key, value); } } // Structs private struct Styles { public static GUIContent AOMethod = EditorGUIUtility.TrTextContent("Method", "The noise method to use when calculating the Ambient Occlusion value."); public static GUIContent Intensity = EditorGUIUtility.TrTextContent("Intensity", "The degree of darkness that Ambient Occlusion adds."); public static GUIContent Radius = EditorGUIUtility.TrTextContent("Radius", "The radius around a given point, where Unity calculates and applies the effect."); public static GUIContent Falloff = EditorGUIUtility.TrTextContent("Falloff Distance", "The distance from the camera where Ambient Occlusion should be visible."); public static GUIContent DirectLightingStrength = EditorGUIUtility.TrTextContent("Direct Lighting Strength", "Controls how much the ambient occlusion affects direct lighting."); public static GUIContent Quality = EditorGUIUtility.TrTextContent("Quality", ""); public static GUIContent Source = EditorGUIUtility.TrTextContent("Source", "The source of the normal vector values.\nDepth Normals: the feature uses the values generated in the Depth Normal prepass.\nDepth: the feature reconstructs the normal values using the depth buffer.\nIn the Deferred rendering path, the feature uses the G-buffer normals texture."); public static GUIContent NormalQuality = new GUIContent("Normal Quality", "The number of depth texture samples that Unity takes when computing the normals. Low:1 sample, Medium: 5 samples, High: 9 samples."); public static GUIContent Downsample = EditorGUIUtility.TrTextContent("Downsample", "With this option enabled, Unity downsamples the SSAO effect texture to improve performance. Each dimension of the texture is reduced by a factor of 2."); public static GUIContent AfterOpaque = EditorGUIUtility.TrTextContent("After Opaque", "With this option enabled, Unity calculates and apply SSAO after the opaque pass to improve performance on mobile platforms with tiled-based GPU architectures. This is not physically correct."); public static GUIContent BlurQuality = EditorGUIUtility.TrTextContent("Blur Quality", "High: Bilateral, Medium: Gaussian. Low: Kawase (Single Pass)."); public static GUIContent Samples = EditorGUIUtility.TrTextContent("Samples", "The number of samples that Unity takes when calculating the obscurance value. Low:4 samples, Medium: 8 samples, High: 12 samples."); } private void Init() { m_ShowQualitySettings = new HeaderBool($"SSAO.QualityFoldout", false); m_ShowDeprecatedSettings = new HeaderBool("SSAO.DeprecatedFoldout", false); SerializedProperty settings = serializedObject.FindProperty("m_Settings"); m_AOMethod = settings.FindPropertyRelative("AOMethod"); m_Intensity = settings.FindPropertyRelative("Intensity"); m_Radius = settings.FindPropertyRelative("Radius"); m_Falloff = settings.FindPropertyRelative("Falloff"); m_DirectLightingStrength = settings.FindPropertyRelative("DirectLightingStrength"); m_Source = settings.FindPropertyRelative("Source"); m_NormalQuality = settings.FindPropertyRelative("NormalSamples"); m_Downsample = settings.FindPropertyRelative("Downsample"); m_AfterOpaque = settings.FindPropertyRelative("AfterOpaque"); m_BlurQuality = settings.FindPropertyRelative("BlurQuality"); m_Samples = settings.FindPropertyRelative("Samples"); m_IsInitialized = true; } public override void OnInspectorGUI() { if (!m_IsInitialized) Init(); #if MODERN_SSAO bool volumeActive = SceneHasActiveSSAOVolumeOverride(); if (volumeActive) { EditorGUILayout.HelpBox( "A Screen Space Ambient Occlusion Volume Override is active in the scene and controls all SSAO settings. " + "The settings below are kept as fallback only and will not take effect while the Volume Override is present.", MessageType.Info); } else { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField( new GUIContent( "Configuring SSAO via the SSAO Renderer Feature is deprecated. Use the new Screen Space Ambient Occlusion Volume Override instead.", EditorGUIUtility.GetHelpIcon(MessageType.Info)), EditorStyles.wordWrappedLabel); using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); if (GUILayout.Button("Add Volume Override")) MigrateSettingsToVolume(); } } } EditorGUILayout.Space(5); m_ShowDeprecatedSettings.SetValue(EditorGUILayout.Foldout(m_ShowDeprecatedSettings.value, "Settings (Deprecated)")); EditorGUI.BeginDisabledGroup(volumeActive); if (m_ShowDeprecatedSettings.value) { EditorGUI.indentLevel++; DrawSsaoSettingsGUI(); EditorGUI.indentLevel--; } EditorGUI.EndDisabledGroup(); #else DrawSsaoSettingsGUI(); #endif } void DrawSsaoSettingsGUI() { EditorGUILayout.PropertyField(m_AOMethod, Styles.AOMethod); EditorGUILayout.PropertyField(m_Intensity, Styles.Intensity); EditorGUILayout.PropertyField(m_Radius, Styles.Radius); EditorGUILayout.PropertyField(m_Falloff, Styles.Falloff); m_DirectLightingStrength.floatValue = EditorGUILayout.Slider(Styles.DirectLightingStrength, m_DirectLightingStrength.floatValue, 0f, 1f); // Make sure these fields are never below 0.0... m_Intensity.floatValue = Mathf.Max(m_Intensity.floatValue, 0f); m_Radius.floatValue = Mathf.Max(m_Radius.floatValue, 0f); m_Falloff.floatValue = Mathf.Max(m_Falloff.floatValue, 0f); m_ShowQualitySettings.SetValue(EditorGUILayout.Foldout(m_ShowQualitySettings.value, Styles.Quality)); if (m_ShowQualitySettings.value) { bool isDeferredRenderingMode = RendererIsDeferred(); EditorGUI.indentLevel++; // Selecting source is not available for Deferred Rendering... using (new EditorGUI.DisabledScope(isDeferredRenderingMode)) EditorGUILayout.PropertyField(m_Source, Styles.Source); // We only enable this field when depth source is selected... using (new EditorGUI.DisabledScope(isDeferredRenderingMode || m_Source.enumValueIndex != (int)ScreenSpaceAmbientOcclusionSettings.DepthSource.Depth)) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(m_NormalQuality, Styles.NormalQuality); EditorGUI.indentLevel--; } EditorGUILayout.PropertyField(m_Downsample, Styles.Downsample); EditorGUILayout.PropertyField(m_AfterOpaque, Styles.AfterOpaque); if (Event.current.type == EventType.Layout) { var rendererData = (this as IOwningRendererDataConsumer).owningRendererData as UniversalRendererData; bool tileOnlyMode = rendererData != null && rendererData.tileOnlyMode; bool afterOpaque = m_AfterOpaque.boolValue; m_ShowAfterOpaqueTileOnlyError = tileOnlyMode && afterOpaque; } if (m_ShowAfterOpaqueTileOnlyError) EditorGUILayout.HelpBox(k_AfterOpaqueIncompatibleWithTileOnlyMode, MessageType.Error, true); EditorGUILayout.PropertyField(m_BlurQuality, Styles.BlurQuality); EditorGUILayout.PropertyField(m_Samples, Styles.Samples); EditorGUI.indentLevel--; } } #if MODERN_SSAO static bool SceneHasActiveSSAOVolumeOverride() { var stack = VolumeManager.instance.stack; if (stack == null) return false; var ssaoVolume = stack.GetComponent(); return ssaoVolume != null && ssaoVolume.AnyPropertiesIsOverridden(); } static void CreateSSAOGlobalVolume(out Volume volume, out ScreenSpaceAmbientOcclusionVolumeOverride ssaoOverride) { var go = new GameObject("SSAO Global Volume"); Undo.RegisterCreatedObjectUndo(go, "Create SSAO Global Volume"); volume = go.AddComponent(); volume.isGlobal = true; Scene scene = go.scene; var profile = VolumeProfileFactory.CreateVolumeProfile(scene, "SSAO Volume Profile"); volume.sharedProfile = profile; // AnyPropertiesIsOverridden() must return true for the runtime volume takeover path to activate. ssaoOverride = profile.Add(overrides: true); AssetDatabase.AddObjectToAsset(ssaoOverride, profile); } void MigrateSettingsToVolume() { if (SceneHasActiveSSAOVolumeOverride()) { bool proceed = EditorUtility.DisplayDialog( "SSAO Volume Already Exists", "An SSAO Volume Override already exists in the scene. " + "A new global volume will be created which may conflict with the existing one. Continue?", "Create Anyway", "Cancel"); if (!proceed) return; } CreateSSAOGlobalVolume(out var volume, out var ssaoOverride); Undo.RecordObject(ssaoOverride, "Migrate SSAO Settings to Volume"); var feature = (ScreenSpaceAmbientOcclusion)target; #pragma warning disable CS0618 ref var settings = ref feature.settings; #pragma warning restore CS0618 ssaoOverride.mode = ScreenSpaceAmbientOcclusionMode.Standard; ssaoOverride.quality = ScreenSpaceAmbientOcclusionQuality.Custom; ssaoOverride.intensity = settings.Intensity; ssaoOverride.radius = settings.Radius; ssaoOverride.falloffDistance = settings.Falloff; ssaoOverride.directLightingStrength = settings.DirectLightingStrength; ssaoOverride.downsample = settings.Downsample; ssaoOverride.afterOpaque = settings.AfterOpaque; ssaoOverride.method = settings.AOMethod == ScreenSpaceAmbientOcclusionSettings.AOMethodOptions.BlueNoise ? ScreenSpaceAmbientOcclusionNoiseMethod.BlueNoise : ScreenSpaceAmbientOcclusionNoiseMethod.InterleavedGradient; ssaoOverride.depthSource = (ScreenSpaceAmbientOcclusionDepthSource)(int)settings.Source; ssaoOverride.normalQuality = (ScreenSpaceAmbientOcclusionNormalQuality)(int)settings.NormalSamples; ssaoOverride.blurQuality = settings.BlurQuality switch { ScreenSpaceAmbientOcclusionSettings.BlurQualityOptions.High => ScreenSpaceAmbientOcclusionBlurQuality.High, ScreenSpaceAmbientOcclusionSettings.BlurQualityOptions.Medium => ScreenSpaceAmbientOcclusionBlurQuality.Medium, _ => ScreenSpaceAmbientOcclusionBlurQuality.Low }; ssaoOverride.sampleCount = settings.Samples switch { ScreenSpaceAmbientOcclusionSettings.AOSampleOption.High => ScreenSpaceAmbientOcclusionSampleCount.High, ScreenSpaceAmbientOcclusionSettings.AOSampleOption.Medium => ScreenSpaceAmbientOcclusionSampleCount.Medium, _ => ScreenSpaceAmbientOcclusionSampleCount.Low }; EditorUtility.SetDirty(ssaoOverride); EditorUtility.SetDirty(volume.sharedProfile); AssetDatabase.SaveAssets(); string profilePath = AssetDatabase.GetAssetPath(volume.sharedProfile); var volumeGo = volume.gameObject; EditorApplication.delayCall += () => { if (volumeGo == null) return; Selection.activeGameObject = volumeGo; EditorGUIUtility.PingObject(volumeGo); }; } #endif private bool RendererIsDeferred() { ScreenSpaceAmbientOcclusion ssaoFeature = (ScreenSpaceAmbientOcclusion) target; UniversalRenderPipelineAsset pipelineAsset = (UniversalRenderPipelineAsset) GraphicsSettings.currentRenderPipeline; if (ssaoFeature == null || pipelineAsset == null) return false; // We have to find the renderer related to the SSAO feature, then test if it is in deferred mode. var rendererDataList = pipelineAsset.m_RendererDataList; for (int rendererIndex = 0; rendererIndex < rendererDataList.Length; ++rendererIndex) { var rendererData = rendererDataList[rendererIndex] as UniversalRendererData; if (rendererData == null) continue; if (!rendererData.usesDeferredLighting) continue; var rendererFeatures = rendererData.rendererFeatures; foreach (var feature in rendererFeatures) if (feature is ScreenSpaceAmbientOcclusion occlusion && occlusion == ssaoFeature) return true; } return false; } } }