using System; using System.Collections.Generic; using System.Reflection; using System.Text; using UnityEditor.Inspector.GraphicsSettingsInspectors; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; namespace UnityEditor.Rendering.Universal { using CED = CoreEditorDrawer; //The internal one is private static class StringBuilderPool { internal static readonly UnityEngine.Pool.ObjectPool s_Pool = new ( () => new StringBuilder(), null, sb => sb.Clear() //clear on release ); public static StringBuilder Get() => s_Pool.Get(); public static UnityEngine.Pool.PooledObject Get(out StringBuilder value) => s_Pool.Get(out value); public static void Release(StringBuilder toRelease) => s_Pool.Release(toRelease); } internal partial class UniversalRenderPipelineAssetUI { private static class GraphicsSettingPanelButton { private const int k_OpenGraphicsSettingsPanelLeftMargin = 18; private static GUIStyle ButtonStylingWithInspectorIndent() { var style = GUI.skin.button; style.margin.left = k_OpenGraphicsSettingsPanelLeftMargin; return style; } public const string k_OpenGraphicsSettingsPanelButtonText = "Open Project Settings > Graphics ..."; public const string k_OpenGraphicsSettingsPanelButtonPath = "Project/Graphics"; public readonly static Lazy s_OpenGraphicsSettingsPanelButtonStyle = new(() => ButtonStylingWithInspectorIndent()); } internal enum Expandable { Rendering = 1 << 1, Quality = 1 << 2, Lighting = 1 << 3, Shadows = 1 << 4, PostProcessing = 1 << 5, #if ENABLE_ADAPTIVE_PERFORMANCE AdaptivePerformance = 1 << 6, #endif Volumes = 1 << 7, } enum ExpandableAdditional { Rendering = 1 << 1, Lighting = 1 << 2, PostProcessing = 1 << 3, Shadows = 1 << 4, Quality = 1 << 5, } internal static void RegisterEditor(UniversalRenderPipelineAssetEditor editor) { k_AdditionalPropertiesState.RegisterEditor(editor); } internal static void UnregisterEditor(UniversalRenderPipelineAssetEditor editor) { k_AdditionalPropertiesState.UnregisterEditor(editor); } static bool ValidateRendererGraphicsAPIsForLightLayers(UniversalRenderPipelineAsset pipelineAsset, out string unsupportedGraphicsApisMessage) { unsupportedGraphicsApisMessage = null; BuildTarget platform = EditorUserBuildSettings.activeBuildTarget; GraphicsDeviceType[] graphicsAPIs = PlayerSettings.GetGraphicsAPIs(platform); for (int apiIndex = 0; apiIndex < graphicsAPIs.Length; apiIndex++) { if (!RenderingUtils.SupportsLightLayers(graphicsAPIs[apiIndex])) { if (unsupportedGraphicsApisMessage != null) unsupportedGraphicsApisMessage += ", "; unsupportedGraphicsApisMessage += System.String.Format("{0}", graphicsAPIs[apiIndex]); } } if (unsupportedGraphicsApisMessage != null) unsupportedGraphicsApisMessage += "."; return unsupportedGraphicsApisMessage == null; } static bool ValidateRendererGraphicsAPIs(UniversalRenderPipelineAsset pipelineAsset, out string unsupportedGraphicsApisMessage) { // Check the list of Renderers against all Graphics APIs the player is built with. unsupportedGraphicsApisMessage = null; BuildTarget platform = EditorUserBuildSettings.activeBuildTarget; GraphicsDeviceType[] graphicsAPIs = PlayerSettings.GetGraphicsAPIs(platform); int rendererCount = pipelineAsset.m_RendererDataList.Length; for (int i = 0; i < rendererCount; i++) { ScriptableRenderer renderer = pipelineAsset.GetRenderer(i); if (renderer == null) continue; GraphicsDeviceType[] unsupportedAPIs = renderer.unsupportedGraphicsDeviceTypes; for (int apiIndex = 0; apiIndex < unsupportedAPIs.Length; apiIndex++) { if (Array.FindIndex(graphicsAPIs, element => element == unsupportedAPIs[apiIndex]) >= 0) unsupportedGraphicsApisMessage += $"{renderer} at index {i} does not support {unsupportedAPIs[apiIndex]}.\n"; } } return unsupportedGraphicsApisMessage == null; } static readonly ExpandedState k_ExpandedState = new(Expandable.Rendering, "URP"); readonly static AdditionalPropertiesState k_AdditionalPropertiesState = new(0, "URP"); internal static void Expand(Expandable expandable, bool state) { k_ExpandedState[expandable] = state; } public static readonly CED.IDrawer Inspector = CED.Group( CED.Group(PrepareTileOnlyModeWarning), CED.AdditionalPropertiesFoldoutGroup(Styles.renderingSettingsText, Expandable.Rendering, k_ExpandedState, ExpandableAdditional.Rendering, k_AdditionalPropertiesState, DrawRendering, DrawRenderingAdditional), CED.FoldoutGroup(Styles.qualitySettingsText, Expandable.Quality, k_ExpandedState, DrawQuality), CED.AdditionalPropertiesFoldoutGroup(Styles.lightingSettingsText, Expandable.Lighting, k_ExpandedState, ExpandableAdditional.Lighting, k_AdditionalPropertiesState, DrawLighting, DrawLightingAdditional), CED.AdditionalPropertiesFoldoutGroup(Styles.shadowSettingsText, Expandable.Shadows, k_ExpandedState, ExpandableAdditional.Shadows, k_AdditionalPropertiesState, DrawShadows, DrawShadowsAdditional), CED.FoldoutGroup(Styles.postProcessingSettingsText, Expandable.PostProcessing, k_ExpandedState, DrawPostProcessing), CED.FoldoutGroup(Styles.volumeSettingsText, Expandable.Volumes, k_ExpandedState, DrawVolumes) #if ENABLE_ADAPTIVE_PERFORMANCE , CED.FoldoutGroup(Styles.adaptivePerformanceText, Expandable.AdaptivePerformance, k_ExpandedState, CED.Group(DrawAdaptivePerformance)) #endif ); static void DrawRendering(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { if (ownerEditor is not UniversalRenderPipelineAssetEditor urpAssetEditor) return; EditorGUILayout.Space(); urpAssetEditor.rendererList.DoLayoutList(); if (!serialized.asset.ValidateRendererData(-1)) EditorGUILayout.HelpBox(Styles.rendererMissingDefaultMessage.text, MessageType.Error, true); else if (!serialized.asset.ValidateRendererDataList(true)) EditorGUILayout.HelpBox(Styles.rendererMissingMessage.text, MessageType.Warning, true); else if (!ValidateRendererGraphicsAPIs(serialized.asset, out var unsupportedGraphicsApisMessage)) EditorGUILayout.HelpBox(Styles.rendererUnsupportedAPIMessage.text + unsupportedGraphicsApisMessage, MessageType.Warning, true); EditorGUILayout.PropertyField(serialized.requireDepthTextureProp, Styles.requireDepthTextureText); DisplayTileOnlyHelpBox(serialized.requireDepthTextureProp, p => p.boolValue, Styles.requireDepthTextureText); EditorGUILayout.PropertyField(serialized.requireOpaqueTextureProp, Styles.requireOpaqueTextureText); DisplayTileOnlyHelpBox(serialized.requireOpaqueTextureProp, p => p.boolValue, Styles.requireOpaqueTextureText); EditorGUI.BeginDisabledGroup(!serialized.requireOpaqueTextureProp.boolValue); EditorGUILayout.PropertyField(serialized.opaqueDownsamplingProp, Styles.opaqueDownsamplingText); EditorGUI.EndDisabledGroup(); EditorGUILayout.PropertyField(serialized.supportsTerrainHolesProp, Styles.supportsTerrainHolesText); EditorGUILayout.PropertyField(serialized.gpuResidentDrawerMode, Styles.gpuResidentDrawerMode); var brgStrippingError = EditorGraphicsSettings.batchRendererGroupShaderStrippingMode != BatchRendererGroupStrippingMode.KeepAll; var lightingModeError = !HasCorrectLightingModes(serialized.asset); var staticBatchingWarning = PlayerSettings.GetStaticBatchingForPlatform(EditorUserBuildSettings.activeBuildTarget); var graphicsApis = PlayerSettings.GetGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget); var gles3Index = Array.IndexOf(graphicsApis, GraphicsDeviceType.OpenGLES3); var webGpuIndex = Array.IndexOf(graphicsApis, GraphicsDeviceType.WebGPU); var webGL2TargetError = gles3Index >= 0 && (webGpuIndex == -1 || gles3Index < webGpuIndex); if ((GPUResidentDrawerMode)serialized.gpuResidentDrawerMode.intValue != GPUResidentDrawerMode.Disabled) { ++EditorGUI.indentLevel; using (new EditorGUI.MixedValueScope(serialized.smallMeshScreenPercentage.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); float newSmallMeshScreenPercentage = Mathf.Clamp(EditorGUILayout.FloatField(Styles.smallMeshScreenPercentage, serialized.smallMeshScreenPercentage.floatValue), 0.0f, 20.0f); if (EditorGUI.EndChangeCheck()) serialized.smallMeshScreenPercentage.floatValue = newSmallMeshScreenPercentage; } EditorGUILayout.PropertyField(serialized.gpuResidentDrawerEnableOcclusionCullingInCameras, Styles.gpuResidentDrawerEnableOcclusionCullingInCameras); DisplayTileOnlyHelpBox(serialized.gpuResidentDrawerEnableOcclusionCullingInCameras, p => p.boolValue, Styles.gpuResidentDrawerEnableOcclusionCullingInCameras); --EditorGUI.indentLevel; if (webGL2TargetError) EditorGUILayout.HelpBox(Styles.webGL2GpuResidentDrawerErrorMessage.text, MessageType.Warning, true); if (brgStrippingError) { EditorGUILayout.HelpBox(Styles.brgShaderStrippingErrorMessage.text, MessageType.Warning, true); if (GUILayout.Button(GraphicsSettingPanelButton.k_OpenGraphicsSettingsPanelButtonText, GraphicsSettingPanelButton.s_OpenGraphicsSettingsPanelButtonStyle.Value)) { GraphicsSettingsInspectorUtility.OpenAndScrollTo("m_BrgStripping"); } } if (lightingModeError) { var renderersToChange = GetRendererNamesWithoutCorrectLightingMode(serialized.asset); EditorGUILayout.HelpBox(string.Format(Styles.lightModeErrorFormatter, renderersToChange), MessageType.Error, true); } if (staticBatchingWarning) EditorGUILayout.HelpBox(Styles.staticBatchingInfoMessage.text, MessageType.Info, true); } } private static bool HasCorrectLightingModes(UniversalRenderPipelineAsset asset) { // Only the URP rendering paths using the cluster light loop (F+ lights & probes) can be used with GRD, // since BiRP-style per-object lights and reflection probes are incompatible with DOTS instancing. foreach (var rendererData in asset.m_RendererDataList) { if (rendererData is not UniversalRendererData universalRendererData) return false; if (!universalRendererData.usesClusterLightLoop) return false; } return true; } /// Formats renderer names as "'Name1', 'Name2'" using pooled StringBuilder. Shared by Tile-Only Mode and lighting mode messages. static string FormatRendererNames(IEnumerable collection, string suffix = "") { if (collection == null) return string.Empty; using var _ = StringBuilderPool.Get(out var sb); var first = true; foreach (var r in collection) { if (r == null) continue; if (!first) sb.Append(", "); sb.Append("'").Append(r.name).Append("'").Append(suffix); first = false; } return sb.ToString(); } private static IEnumerable GetRenderersWithoutCorrectLightingMode(UniversalRenderPipelineAsset asset) { if (asset?.m_RendererDataList == null) yield break; foreach (var rendererData in asset.m_RendererDataList) { if (rendererData == null) continue; if (rendererData is not UniversalRendererData universalRendererData || !universalRendererData.usesClusterLightLoop) yield return rendererData; } } private static string GetRendererNamesWithoutCorrectLightingMode(UniversalRenderPipelineAsset asset) => FormatRendererNames(GetRenderersWithoutCorrectLightingMode(asset)); static void DrawRenderingAdditional(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { EditorGUILayout.PropertyField(serialized.srpBatcher, Styles.srpBatcher); if (serialized.supportsDynamicBatching.boolValue) { EditorGUILayout.PropertyField(serialized.supportsDynamicBatching, Styles.dynamicBatching); EditorGUILayout.HelpBox(Styles.warningDynamicBatching.text, MessageType.Warning); } EditorGUILayout.PropertyField(serialized.storeActionsOptimizationProperty, Styles.storeActionsOptimizationText); } static bool IsAndroidXRTargetted() //Include Quest platform { #if XR_MANAGEMENT_4_0_1_OR_NEWER var buildTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget); if (buildTargetGroup != BuildTargetGroup.Android) return false; var buildTargetSettings = XR.Management.XRGeneralSettingsPerBuildTarget.XRGeneralSettingsForBuildTarget(buildTargetGroup); return buildTargetSettings != null && buildTargetSettings.Manager != null && buildTargetSettings.Manager.activeLoaders.Count > 0; #else return false; #endif } static void DrawQuality(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { DrawHDR(serialized, ownerEditor); EditorGUILayout.PropertyField(serialized.msaa, Styles.msaaText); DisplayTileOnlyHelpBox( serialized.msaa, p => p.intValue != (int)MsaaQuality.Disabled // This operation is actually ok on Quest && !IsAndroidXRTargetted(), Styles.msaaText, MessageType.Info, Styles.msaaTileOnlyInfo); #if ENABLE_UPSCALER_FRAMEWORK // A quality-mode upscaler dictates the render resolution, so Render Scale is ignored for it. bool renderScaleDrivenByUpscaler = SelectedUpscalerDictatesResolution(serialized); #else const bool renderScaleDrivenByUpscaler = false; #endif using (new EditorGUI.DisabledScope(renderScaleDrivenByUpscaler)) using (new EditorGUI.MixedValueScope(serialized.renderScale.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); float newRenderScale = EditorGUILayout.Slider(Styles.renderScaleText, serialized.renderScale.floatValue, UniversalRenderPipeline.minRenderScale, UniversalRenderPipeline.maxRenderScale); if (EditorGUI.EndChangeCheck()) serialized.renderScale.floatValue = newRenderScale; } #if ENABLE_UPSCALER_FRAMEWORK if (renderScaleDrivenByUpscaler) { // Quality mode dictates the render resolution; Render Scale is ignored (the upscaler still runs). EditorGUILayout.HelpBox(Styles.renderScaleQualityModeInfo, MessageType.Info, true); } else if (serialized.renderScale.floatValue > 1.0f) { // Above 1.0 the image is rendered larger than the display and downsampled (supersampling), so no // upscaler runs — show this regardless of the selected upscaler/filter. EditorGUILayout.HelpBox(Styles.renderScaleSupersamplingInfo, MessageType.Info, true); } else if (TryGetCustomScalingClampInfo(serialized, out float upscalerMinScale, out float upscalerMaxScale, out bool upscalerPinsResolution, out bool upscalerHardRange)) { float renderScale = serialized.renderScale.floatValue; if (upscalerPinsResolution) EditorGUILayout.HelpBox(Styles.renderScaleUpscalerNoRangeInfo, MessageType.Info, true); else if (renderScale < upscalerMinScale || renderScale > upscalerMaxScale) { if (upscalerHardRange) // enforced limits → warn that it will be clamped at runtime EditorGUILayout.HelpBox(string.Format(Styles.renderScaleUpscalerClampWarning, upscalerMinScale, upscalerMaxScale), MessageType.Warning, true); else // advisory floor → info that quality degrades, no clamp EditorGUILayout.HelpBox(string.Format(Styles.renderScaleUpscalerRecommendedInfo, upscalerMinScale), MessageType.Info, true); } } #endif DisplayTileOnlyHelpBox( serialized.renderScale, p => { // Usual threshold is 0.05f, but display warning to users even when RenderScale not actually applied since users are not aware of threshold bool userAttemptingRenderScale = !Mathf.Approximately(p.floatValue, 1.0f); if (!userAttemptingRenderScale) return false; // This operation is actually ok on Quest return !IsAndroidXRTargetted(); }, Styles.renderScaleText); DrawUpscalingFilterDropdownAndOptions(serialized, ownerEditor); #if ENABLE_UPSCALER_FRAMEWORK bool stpUpscalingSelected = serialized.asset.upscalerName == UniversalRenderPipeline.k_UpscalerName_STP; bool fsr1UpscalingSelected = serialized.asset.upscalerName == UniversalRenderPipeline.k_UpscalerName_FSR1; #else bool stpUpscalingSelected = serialized.asset.upscalingFilter == UpscalingFilterSelection.STP; bool fsr1UpscalingSelected = serialized.asset.upscalingFilter == UpscalingFilterSelection.FSR; #endif if (serialized.renderScale.floatValue < 1.0f || stpUpscalingSelected || fsr1UpscalingSelected) { EditorGUILayout.HelpBox("Camera depth isn't supported when Upscaling is turned on in the game view. We will automatically fall back to not doing depth-testing for this pass when upscaling is applied.", MessageType.Warning, true); } EditorGUILayout.PropertyField(serialized.enableLODCrossFadeProp, Styles.enableLODCrossFadeText); EditorGUI.BeginDisabledGroup(!serialized.enableLODCrossFadeProp.boolValue); EditorGUILayout.PropertyField(serialized.lodCrossFadeDitheringTypeProp, Styles.lodCrossFadeDitheringTypeText); if (serialized.asset.enableLODCrossFade && serialized.asset.lodCrossFadeDitheringType == LODCrossFadeDitheringType.Stencil) { var rendererData = serialized.asset.m_RendererDataList[serialized.asset.m_DefaultRendererIndex]; if (rendererData is UniversalRendererData && ((UniversalRendererData)rendererData).defaultStencilState.overrideStencilState) { EditorGUILayout.HelpBox(Styles.stencilLodCrossFadeWarningMessage.text, MessageType.Warning, true); } } EditorGUI.EndDisabledGroup(); } static void DrawUpscalingFilterDropdownAndOptions(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { #if ENABLE_UPSCALER_FRAMEWORK // --- 1. Get the available upscaler names --- string[] namesArray = null; if (UniversalRenderPipeline.upscaling != null) { // names come in sorted defined by UniversalRenderPipeline.k_UpscalerSortOrder namesArray = UniversalRenderPipeline.upscaling.upscalerNames as string[]; } else { namesArray = Array.Empty(); } // --- 2. Get selected index or fall-back to a safe default --- string currentName = serialized.selectedUpscalerName.stringValue; int selectedIndex = Array.IndexOf(namesArray, currentName); if (selectedIndex == -1) { selectedIndex = 0; // Default to "Automatic" or "Bilinear" } // --- 3. Draw the Single Dropdown --- EditorGUI.BeginChangeCheck(); selectedIndex = EditorGUILayout.Popup(Styles.upscalingFilterText, selectedIndex, namesArray); if (EditorGUI.EndChangeCheck()) { // --- 4. Save to the serialzied asset if we change value --- serialized.selectedUpscalerName.stringValue = namesArray[selectedIndex]; } DisplayTileOnlyHelpBox(serialized.upscalingFilter, p => serialized.selectedUpscalerName.stringValue != UniversalRenderPipeline.k_UpscalerName_Auto, Styles.upscalingFilterText); // --- 5. Draw Options per upscaler --- string selectedName = namesArray[selectedIndex]; switch (selectedName) { // Special-case for FSR1. // FSR1 has two passes: Upscaling + Sharpening. // IUpscaler framework handles the upscaling part. // The sharpening is done in a separate pass from the upscaling (final post), // hence we keep the fsrOverrideSharpness & fsrSharpness properties within the // URPAsset and render them here under FSR1 upscaler options. // This way the user will see it as a single solution for Upscaling+Sharpening, as AMD intended. // Typically, the upscaler options are captured by the UpscalerOptions object, excluding this case. case UniversalRenderPipeline.k_UpscalerName_FSR1: { ++EditorGUI.indentLevel; EditorGUILayout.PropertyField(serialized.fsrOverrideSharpness, Styles.fsrOverrideSharpness); // We put the FSR sharpness override value behind an override checkbox so we can tell when the user intends to use a custom value rather than the default. if (serialized.fsrOverrideSharpness.boolValue) { using (new EditorGUI.MixedValueScope(serialized.fsrSharpness.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); float newFsrSharpness = EditorGUILayout.Slider(Styles.fsrSharpnessText, serialized.fsrSharpness.floatValue, 0.0f, 1.0f); if (EditorGUI.EndChangeCheck()) serialized.fsrSharpness.floatValue = newFsrSharpness; } } --EditorGUI.indentLevel; break; } default: // Use options editor of the particular IUpscaler. UpscalerOptions options = serialized.asset.GetUpscalerOptions(selectedName); UniversalRenderPipelineAssetEditor urpEditor = ownerEditor as UniversalRenderPipelineAssetEditor; Editor upscalerOptionsEditor = urpEditor.upscalerOptionsEditorCache.GetOrCreateEditor(options); if (upscalerOptionsEditor != null) { ++EditorGUI.indentLevel; // Delegate to the upscaler's own options editor so each draws its own fields and conditional logic. upscalerOptionsEditor.OnInspectorGUI(); --EditorGUI.indentLevel; } // Warn users about performance expectations if they attempt to enable STP on a mobile platform if (selectedName == UniversalRenderPipeline.k_UpscalerName_STP && PlatformAutoDetect.isShaderAPIMobileDefined) { EditorGUILayout.HelpBox(Styles.stpMobilePlatformWarning, MessageType.Warning, true); } break; } #else // Count builtin upscalers int numBuiltInUpscalers = (int)UpscalingFilterSelection.STP + 1; int numTotalUpscalers = numBuiltInUpscalers; // Create arrays for options and enum values string[] names = new string[numTotalUpscalers]; // Get names and values for builtin upscalers { var bnames = Enum.GetNames(typeof(UpscalingFilterSelection)); for (int i = 0; i < numBuiltInUpscalers; i++) { // Get the display name from the InspectorName attribute if it exists var field = typeof(UpscalingFilterSelection).GetField(bnames[i]); var inspectorNameAttribute = field.GetCustomAttribute(); names[i] = inspectorNameAttribute != null ? inspectorNameAttribute.displayName : bnames[i]; } } // Get the current enum value UpscalingFilterSelection curUpscaler = (UpscalingFilterSelection)serialized.upscalingFilter.enumValueIndex; // Find the current selected index int selectedIndex = 0; // [0, iUpscalerCount + BuiltinUpscalerCount) { selectedIndex = serialized.upscalingFilter.enumValueIndex; } // --------------------------------------------------- GUI --------------------------------------------------- // Show the dropdown EditorGUI.BeginChangeCheck(); selectedIndex = EditorGUILayout.Popup(Styles.upscalingFilterText, selectedIndex, names); if (EditorGUI.EndChangeCheck()) { serialized.upscalingFilter.enumValueIndex = Math.Min(selectedIndex, (int)UpscalingFilterSelection.STP); } DisplayTileOnlyHelpBox(serialized.upscalingFilter, p => p.intValue != (int)UpscalingFilterSelection.Auto, Styles.upscalingFilterText); // draw upscaler options, if any switch (serialized.asset.upscalingFilter) { case UpscalingFilterSelection.FSR: { ++EditorGUI.indentLevel; EditorGUILayout.PropertyField(serialized.fsrOverrideSharpness, Styles.fsrOverrideSharpness); // We put the FSR sharpness override value behind an override checkbox so we can tell when the user intends to use a custom value rather than the default. if (serialized.fsrOverrideSharpness.boolValue) { using (new EditorGUI.MixedValueScope(serialized.fsrSharpness.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); float newFsrSharpness = EditorGUILayout.Slider(Styles.fsrSharpnessText, serialized.fsrSharpness.floatValue, 0.0f, 1.0f); if (EditorGUI.EndChangeCheck()) serialized.fsrSharpness.floatValue = newFsrSharpness; } } --EditorGUI.indentLevel; } break; case UpscalingFilterSelection.STP: { // Warn users about performance expectations if they attempt to enable STP on a mobile platform if (PlatformAutoDetect.isShaderAPIMobileDefined) { EditorGUILayout.HelpBox(Styles.stpMobilePlatformWarning, MessageType.Warning, true); } } break; } #endif } #if ENABLE_UPSCALER_FRAMEWORK // True if the named upscaler exposes a quality mode. static bool SelectedUpscalerHasQualityMode(string upscalerName) { Upscaling upscaling = UniversalRenderPipeline.upscaling; if (upscaling == null || string.IsNullOrEmpty(upscalerName)) return false; IUpscaler upscaler = upscaling.GetIUpscalerByName(upscalerName); return upscaler != null && upscaler.hasQualityMode; } // True when the selected upscaler's quality mode dictates the render resolution (Render Scale / DRS ignored). static bool SelectedUpscalerDictatesResolution(SerializedUniversalRenderPipelineAsset serialized) { string selectedName = serialized.selectedUpscalerName.stringValue; if (!SelectedUpscalerHasQualityMode(selectedName)) return false; UpscalerOptions options = serialized.asset.GetUpscalerOptions(selectedName); return options != null && options.resolutionMode != UpscalerResolutionMode.CustomScaling; } // For an upscaler in Custom Scaling, reports how it constrains Render Scale at runtime: // - returns false when this doesn't apply (no quality-mode upscaler, not Custom Scaling, or it leaves Render // Scale fully free), so the caller draws nothing; // - driven == true: the quality mode has no range (e.g. UltraPerformance), so Render Scale is ignored and the // render resolution is pinned to the recommended size; // - hardRange == true: minScale/maxScale are the hard range Render Scale is clamped into at runtime (DLSS); // - hardRange == false: minScale is an advisory floor (FSR2's recommended minimum) — not clamped, info only. static bool TryGetCustomScalingClampInfo(SerializedUniversalRenderPipelineAsset serialized, out float minScale, out float maxScale, out bool driven, out bool hardRange) { minScale = 0f; maxScale = 1f; driven = false; hardRange = false; string selectedName = serialized.selectedUpscalerName.stringValue; if (!SelectedUpscalerHasQualityMode(selectedName)) return false; UpscalerOptions options = serialized.asset.GetUpscalerOptions(selectedName); if (options == null || options.resolutionMode != UpscalerResolutionMode.CustomScaling) return false; // Evaluate the upscaler's pixel range at the actual display resolution and convert to a Render Scale ratio. Vector2Int referenceResolution = GetUpscalerReferenceResolution(); if (referenceResolution.x <= 0 || !TryGetSelectedUpscalerResolutionInfo(serialized, referenceResolution, out UpscalerResolutionInfo info)) return false; driven = info.constraint == UpscalerResolutionConstraint.Fixed; if (driven) return true; // pinned to optimal; Render Scale ignored if (info.constraint == UpscalerResolutionConstraint.Range) { // Hard limits: the pipeline clamps Render Scale into this range at runtime (e.g. DLSS). hardRange = true; minScale = (float)info.minResolution.x / referenceResolution.x; maxScale = (float)info.maxResolution.x / referenceResolution.x; return true; } if (info.recommendedMinScale > 0f) { // Advisory floor only: the upscaler runs below it but degrades in quality; not clamped (e.g. FSR2). It's // already a render-scale ratio, so no reference-resolution conversion is needed. minScale = info.recommendedMinScale; maxScale = 1f; // no upper recommendation; Render Scale > 1.0 is handled by the supersampling notice return true; } return false; // upscaler leaves Render Scale fully free (e.g. STP) } // The display resolution to evaluate the upscaler's [min,max] against. Uses the actual main Game View size so the // resulting Render Scale ratios match what will render (handles any resolution, including above 4K); falls back // to 4K if the Game View size isn't available (e.g. no Game View open). static Vector2Int GetUpscalerReferenceResolution() { Vector2 gameViewSize = UnityEditor.Handles.GetMainGameViewSize(); int width = Mathf.RoundToInt(gameViewSize.x); int height = Mathf.RoundToInt(gameViewSize.y); if (width <= 0 || height <= 0) return new Vector2Int(3840, 2160); return new Vector2Int(width, height); } static bool TryGetSelectedUpscalerResolutionInfo(SerializedUniversalRenderPipelineAsset serialized, Vector2Int referenceResolution, out UpscalerResolutionInfo info) { info = default; Upscaling upscaling = UniversalRenderPipeline.upscaling; string selectedName = serialized.selectedUpscalerName.stringValue; if (upscaling == null || string.IsNullOrEmpty(selectedName)) return false; IUpscaler upscaler = upscaling.GetIUpscalerByName(selectedName); if (upscaler == null || !upscaler.hasQualityMode) return false; info = upscaler.GetResolutionInfo(referenceResolution, upscaling.GetGlobalOptions(upscaler)); return true; } #endif public static readonly string disabledPostprocessing = L10n.Tr("HDR is not supported by one of the Universal Render Pipeline renderers."); static void DrawHDR(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { EditorGUILayout.PropertyField(serialized.hdr, Styles.hdrText); // A ScriptableRenderFeature can add HDR, for example the OnTilePostProcessing extension. DisplayTileOnlyHelpBox(serialized.hdr, p => p.boolValue, Styles.hdrText, customMessage: Styles.tileOnlyModeMaybeMessage); // Nested and in-between additional property bool additionalProperties = k_ExpandedState[Expandable.Quality] && k_AdditionalPropertiesState[ExpandableAdditional.Quality]; if (serialized.hdr.boolValue && additionalProperties) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(serialized.hdrColorBufferPrecisionProp, Styles.hdrColorBufferPrecisionText); EditorGUI.indentLevel--; } } static void DrawLighting(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { // Main Light bool disableGroup = false; EditorGUI.BeginDisabledGroup(disableGroup); CoreEditorUtils.DrawPopup(Styles.mainLightRenderingModeText, serialized.mainLightRenderingModeProp, Styles.mainLightOptions); EditorGUI.EndDisabledGroup(); EditorGUI.indentLevel++; disableGroup |= serialized.mainLightRenderingModeProp.hasMultipleDifferentValues || !serialized.mainLightRenderingModeProp.boolValue; EditorGUI.BeginDisabledGroup(disableGroup); EditorGUILayout.PropertyField(serialized.mainLightShadowsSupportedProp, Styles.supportsMainLightShadowsText); EditorGUI.EndDisabledGroup(); disableGroup |= serialized.mainLightShadowsSupportedProp.hasMultipleDifferentValues || !serialized.mainLightShadowsSupportedProp.boolValue; EditorGUI.BeginDisabledGroup(disableGroup); EditorGUILayout.PropertyField(serialized.mainLightShadowmapResolutionProp, Styles.mainLightShadowmapResolutionText); EditorGUI.EndDisabledGroup(); EditorGUI.indentLevel--; EditorGUILayout.Space(); // Probe volumes EditorGUILayout.PropertyField(serialized.lightProbeSystem, Styles.lightProbeSystemContent); if (serialized.lightProbeSystem.intValue == (int)LightProbeSystem.ProbeVolumes) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(serialized.probeVolumeTextureSize, Styles.probeVolumeMemoryBudget); EditorGUILayout.PropertyField(serialized.probeVolumeSHBands, Styles.probeVolumeSHBands); EditorGUILayout.PropertyField(serialized.supportProbeVolumeGPUStreaming, Styles.supportProbeVolumeGPUStreaming); EditorGUI.BeginDisabledGroup(serialized.supportProbeVolumeGPUStreaming.hasMultipleDifferentValues || !serialized.supportProbeVolumeGPUStreaming.boolValue); EditorGUI.indentLevel++; EditorGUILayout.PropertyField(serialized.supportProbeVolumeDiskStreaming, Styles.supportProbeVolumeDiskStreaming); EditorGUI.indentLevel--; EditorGUI.EndDisabledGroup(); EditorGUILayout.PropertyField(serialized.supportProbeVolumeScenarios, Styles.supportProbeVolumeScenarios); if (serialized.supportProbeVolumeScenarios.boolValue) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(serialized.supportProbeVolumeScenarioBlending, Styles.supportProbeVolumeScenarioBlending); if (serialized.supportProbeVolumeScenarioBlending.boolValue) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(serialized.probeVolumeBlendingTextureSize, Styles.probeVolumeBlendingMemoryBudget); EditorGUI.indentLevel--; } EditorGUI.indentLevel--; } int estimatedVMemCost = ProbeReferenceVolume.instance.GetVideoMemoryCost(); if (estimatedVMemCost == 0) { EditorGUILayout.HelpBox($"Estimated GPU Memory cost: 0.\nProbe reference volume is not used in the scene and resources haven't been allocated yet.", MessageType.Info, wide: true); } else { EditorGUILayout.HelpBox($"Estimated GPU Memory cost: {estimatedVMemCost / (1000 * 1000)} MB.", MessageType.Info, wide: true); } EditorGUI.indentLevel--; EditorGUILayout.Space(); } // Additional light EditorGUILayout.PropertyField(serialized.additionalLightsRenderingModeProp, Styles.addditionalLightsRenderingModeText); EditorGUI.indentLevel++; disableGroup = serialized.additionalLightsRenderingModeProp.hasMultipleDifferentValues || serialized.additionalLightsRenderingModeProp.intValue == (int)LightRenderingMode.Disabled; EditorGUI.BeginDisabledGroup(disableGroup); using (new EditorGUI.MixedValueScope(serialized.additionalLightsPerObjectLimitProp.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); int newPerObjectLimit = EditorGUILayout.IntSlider(Styles.perObjectLimit, serialized.additionalLightsPerObjectLimitProp.intValue, 0, UniversalRenderPipeline.maxPerObjectLights); if (EditorGUI.EndChangeCheck()) serialized.additionalLightsPerObjectLimitProp.intValue = newPerObjectLimit; } #if UNITY_META_QUEST if (serialized.additionalLightsPerObjectLimitProp.intValue > 1) { EditorGUILayout.HelpBox("When targeting Meta Quest, setting the Per Object Limit to 1 will improve shader performance.", MessageType.Info); } #endif EditorGUI.EndDisabledGroup(); disableGroup |= serialized.additionalLightsPerObjectLimitProp.hasMultipleDifferentValues || serialized.additionalLightsRenderingModeProp.hasMultipleDifferentValues || serialized.additionalLightsPerObjectLimitProp.intValue == 0 || serialized.additionalLightsRenderingModeProp.intValue != (int)LightRenderingMode.PerPixel; EditorGUI.BeginDisabledGroup(disableGroup); EditorGUILayout.PropertyField(serialized.additionalLightShadowsSupportedProp, Styles.supportsAdditionalShadowsText); EditorGUI.EndDisabledGroup(); disableGroup |= serialized.additionalLightShadowsSupportedProp.hasMultipleDifferentValues || !serialized.additionalLightShadowsSupportedProp.boolValue; EditorGUI.BeginDisabledGroup(disableGroup); EditorGUILayout.PropertyField(serialized.additionalLightShadowmapResolutionProp, Styles.additionalLightsShadowmapResolution); DrawShadowResolutionTierSettings(serialized, ownerEditor); EditorGUI.EndDisabledGroup(); EditorGUILayout.Space(); disableGroup = serialized.additionalLightsRenderingModeProp.hasMultipleDifferentValues || serialized.additionalLightsRenderingModeProp.intValue == (int)LightRenderingMode.Disabled || serialized.supportsLightCookies.hasMultipleDifferentValues || !serialized.supportsLightCookies.boolValue; EditorGUI.BeginDisabledGroup(disableGroup); EditorGUILayout.PropertyField(serialized.additionalLightCookieResolutionProp, Styles.additionalLightsCookieResolution); EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(disableGroup); EditorGUILayout.PropertyField(serialized.additionalLightCookieFormatProp, Styles.additionalLightsCookieFormat); EditorGUI.EndDisabledGroup(); EditorGUI.indentLevel--; EditorGUILayout.Space(); // Reflection Probes EditorGUILayout.LabelField(Styles.reflectionProbesSettingsText); EditorGUI.indentLevel++; EditorGUILayout.PropertyField(serialized.reflectionProbeBlendingProp, Styles.reflectionProbeBlendingText); EditorGUI.indentLevel++; EditorGUI.BeginDisabledGroup(!serialized.reflectionProbeBlendingProp.boolValue); EditorGUILayout.PropertyField(serialized.reflectionProbeAtlasProp, Styles.reflectionProbeAtlasText); EditorGUI.EndDisabledGroup(); // Disable probeAtlas when probeBlending is off. if (!serialized.reflectionProbeBlendingProp.boolValue) serialized.reflectionProbeAtlasProp.boolValue = false; if ((GPUResidentDrawerMode)serialized.gpuResidentDrawerMode.intValue != GPUResidentDrawerMode.Disabled) { if (!serialized.reflectionProbeBlendingProp.boolValue || !serialized.reflectionProbeAtlasProp.boolValue) EditorGUILayout.HelpBox(Styles.reflectionProbeBlendingGpuResidentDrawerWarningText.text, MessageType.Warning, true); } EditorGUI.indentLevel--; EditorGUILayout.PropertyField(serialized.reflectionProbeBoxProjectionProp, Styles.reflectionProbeBoxProjectionText); EditorGUI.indentLevel--; } static void DrawLightingAdditional(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { EditorGUILayout.PropertyField(serialized.mixedLightingSupportedProp, Styles.mixedLightingSupportLabel); EditorGUILayout.PropertyField(serialized.useRenderingLayers, Styles.useRenderingLayers); EditorGUILayout.PropertyField(serialized.supportsLightCookies, Styles.supportsLightCookies); EditorGUILayout.PropertyField(serialized.shEvalModeProp, Styles.shEvalModeText); if (serialized.useRenderingLayers.boolValue && !ValidateRendererGraphicsAPIsForLightLayers(serialized.asset, out var unsupportedGraphicsApisMessage)) EditorGUILayout.HelpBox(Styles.lightlayersUnsupportedMessage.text + unsupportedGraphicsApisMessage, MessageType.Warning, true); } static void DrawShadowResolutionTierSettings(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { // UI code adapted from HDRP U.I logic implemented in com.unity.render-pipelines.high-definition/Editor/RenderPipeline/Settings/SerializedScalableSetting.cs ) var rect = EditorGUILayout.GetControlRect(); var contentRect = EditorGUI.PrefixLabel(rect, Styles.additionalLightsShadowResolutionTiers); const int k_ShadowResolutionTiersCount = 3; var values = new[] { serialized.additionalLightsShadowResolutionTierLowProp, serialized.additionalLightsShadowResolutionTierMediumProp, serialized.additionalLightsShadowResolutionTierHighProp }; var num = contentRect.width / (float)k_ShadowResolutionTiersCount; // space allocated for every field including the label var indentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; // Reset the indentation float pixelShift = 0; // Variable to keep track of the current pixel shift in the rectangle we were assigned for this whole section. for (var index = 0; index < k_ShadowResolutionTiersCount; ++index) { var labelWidth = Mathf.Clamp(EditorStyles.label.CalcSize(Styles.additionalLightsShadowResolutionTierNames[index]).x, 0, num); EditorGUI.LabelField(new Rect(contentRect.x + pixelShift, contentRect.y, labelWidth, contentRect.height), Styles.additionalLightsShadowResolutionTierNames[index]); pixelShift += labelWidth; // We need to remove from the position the label size that we've just drawn and shift by it's length float spaceLeft = num - labelWidth; // The amount of space left for the field if (spaceLeft > 2) // If at least two pixels are left to draw this field, draw it, otherwise, skip { var fieldSlot = new Rect(contentRect.x + pixelShift, contentRect.y, num - labelWidth, contentRect.height); // Define the rectangle for the field using (new EditorGUI.MixedValueScope(values[index].hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); int value = EditorGUI.DelayedIntField(fieldSlot, values[index].intValue); if (EditorGUI.EndChangeCheck()) values[index].intValue = Mathf.Max(UniversalAdditionalLightData.AdditionalLightsShadowMinimumResolution, Mathf.NextPowerOfTwo(value)); } } pixelShift += spaceLeft; // Shift by the slot that was left for the field } EditorGUI.indentLevel = indentLevel; } static void DrawShadows(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { using (new EditorGUI.MixedValueScope(serialized.shadowDistanceProp.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); float newShadowDistance = Mathf.Max(0.0f, EditorGUILayout.FloatField(Styles.shadowDistanceText, serialized.shadowDistanceProp.floatValue)); if (EditorGUI.EndChangeCheck()) serialized.shadowDistanceProp.floatValue = newShadowDistance; } EditorUtils.Unit unit = EditorUtils.Unit.Metric; if (serialized.shadowCascadeCountProp.intValue != 0) { EditorGUI.BeginChangeCheck(); unit = (EditorUtils.Unit)EditorGUILayout.EnumPopup(Styles.shadowWorkingUnitText, serialized.state.value); if (EditorGUI.EndChangeCheck()) { serialized.state.value = unit; } } EditorGUILayout.IntSlider(serialized.shadowCascadeCountProp, UniversalRenderPipelineAsset.k_ShadowCascadeMinCount, UniversalRenderPipelineAsset.k_ShadowCascadeMaxCount, Styles.shadowCascadesText); int cascadeCount = serialized.shadowCascadeCountProp.intValue; EditorGUI.indentLevel++; bool useMetric = unit == EditorUtils.Unit.Metric; float baseMetric = serialized.shadowDistanceProp.floatValue; int cascadeSplitCount = cascadeCount - 1; DrawCascadeSliders(serialized, cascadeSplitCount, useMetric, baseMetric); EditorGUI.indentLevel--; DrawCascades(serialized, cascadeCount, useMetric, baseMetric); EditorGUI.indentLevel++; if ((GPUResidentDrawerMode)serialized.gpuResidentDrawerMode.intValue != GPUResidentDrawerMode.Disabled) { var shadowSmallMeshScreenPctStyles = new [] { Styles.shadowSmallMeshPct1, Styles.shadowSmallMeshPct2, Styles.shadowSmallMeshPct3, Styles.shadowSmallMeshPct4}; using (new EditorGUI.MixedValueScope(serialized.shadowSmallMeshScreenPercentages.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); Vector4 newShadowSmallMeshScreenPercentages = new Vector4(); for (int i = 0; i < 4; i++) { newShadowSmallMeshScreenPercentages[i] = Mathf.Clamp( EditorGUILayout.FloatField(shadowSmallMeshScreenPctStyles[i], serialized.shadowSmallMeshScreenPercentages.vector4Value[i]), 0.0f, 50.0f); } if (EditorGUI.EndChangeCheck()) serialized.shadowSmallMeshScreenPercentages.vector4Value = newShadowSmallMeshScreenPercentages; } } using (new EditorGUI.MixedValueScope(serialized.shadowDepthBiasProp.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); float newDepthBias = EditorGUILayout.Slider(Styles.shadowDepthBias, serialized.shadowDepthBiasProp.floatValue, 0.0f, UniversalRenderPipeline.maxShadowBias); if (EditorGUI.EndChangeCheck()) serialized.shadowDepthBiasProp.floatValue = newDepthBias; } using (new EditorGUI.MixedValueScope(serialized.shadowNormalBiasProp.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); float newNormalBias = EditorGUILayout.Slider(Styles.shadowNormalBias, serialized.shadowNormalBiasProp.floatValue, 0.0f, UniversalRenderPipeline.maxShadowBias); if (EditorGUI.EndChangeCheck()) serialized.shadowNormalBiasProp.floatValue = newNormalBias; } EditorGUILayout.PropertyField(serialized.softShadowsSupportedProp, Styles.supportsSoftShadows); if (serialized.softShadowsSupportedProp.boolValue) { EditorGUI.indentLevel++; DrawShadowsSoftShadowQuality(serialized, ownerEditor); EditorGUI.indentLevel--; } EditorGUI.indentLevel--; } static void DrawShadowsSoftShadowQuality(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { int selectedAssetSoftShadowQuality = serialized.softShadowQualityProp.intValue; Rect r = EditorGUILayout.GetControlRect(true); EditorGUI.BeginProperty(r, Styles.softShadowsQuality, serialized.softShadowQualityProp); { using (var checkScope = new EditorGUI.ChangeCheckScope()) { selectedAssetSoftShadowQuality = EditorGUI.IntPopup(r, Styles.softShadowsQuality, selectedAssetSoftShadowQuality, Styles.softShadowsQualityAssetOptions, Styles.softShadowsQualityAssetValues); if (checkScope.changed) { serialized.softShadowQualityProp.intValue = Math.Clamp(selectedAssetSoftShadowQuality, (int)SoftShadowQuality.Low, (int)SoftShadowQuality.High); } } } EditorGUI.EndProperty(); } static void DrawShadowsAdditional(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { EditorGUILayout.PropertyField(serialized.conservativeEnclosingSphereProp, Styles.conservativeEnclosingSphere); } static void DrawCascadeSliders(SerializedUniversalRenderPipelineAsset serialized, int splitCount, bool useMetric, float baseMetric) { Vector4 shadowCascadeSplit = Vector4.one; if (splitCount == 3) shadowCascadeSplit = new Vector4(serialized.shadowCascade4SplitProp.vector3Value.x, serialized.shadowCascade4SplitProp.vector3Value.y, serialized.shadowCascade4SplitProp.vector3Value.z, 1); else if (splitCount == 2) shadowCascadeSplit = new Vector4(serialized.shadowCascade3SplitProp.vector2Value.x, serialized.shadowCascade3SplitProp.vector2Value.y, 1, 0); else if (splitCount == 1) shadowCascadeSplit = new Vector4(serialized.shadowCascade2SplitProp.floatValue, 1, 0, 0); float splitBias = 0.001f; float invBaseMetric = baseMetric == 0 ? 0 : 1f / baseMetric; // Ensure correct split order shadowCascadeSplit[0] = Mathf.Clamp(shadowCascadeSplit[0], 0f, shadowCascadeSplit[1] - splitBias); shadowCascadeSplit[1] = Mathf.Clamp(shadowCascadeSplit[1], shadowCascadeSplit[0] + splitBias, shadowCascadeSplit[2] - splitBias); shadowCascadeSplit[2] = Mathf.Clamp(shadowCascadeSplit[2], shadowCascadeSplit[1] + splitBias, shadowCascadeSplit[3] - splitBias); EditorGUI.BeginChangeCheck(); for (int i = 0; i < splitCount; ++i) { float value = shadowCascadeSplit[i]; float minimum = i == 0 ? 0 : shadowCascadeSplit[i - 1] + splitBias; float maximum = i == splitCount - 1 ? 1 : shadowCascadeSplit[i + 1] - splitBias; if (useMetric) { float valueMetric = value * baseMetric; valueMetric = EditorGUILayout.Slider(EditorGUIUtility.TrTextContent($"Split {i + 1}", "The distance where this cascade ends and the next one starts."), valueMetric, 0f, baseMetric, null); shadowCascadeSplit[i] = Mathf.Clamp(valueMetric * invBaseMetric, minimum, maximum); } else { float valueProcentage = value * 100f; valueProcentage = EditorGUILayout.Slider(EditorGUIUtility.TrTextContent($"Split {i + 1}", "The distance where this cascade ends and the next one starts."), valueProcentage, 0f, 100f, null); shadowCascadeSplit[i] = Mathf.Clamp(valueProcentage * 0.01f, minimum, maximum); } } if (EditorGUI.EndChangeCheck()) { switch (splitCount) { case 3: serialized.shadowCascade4SplitProp.vector3Value = shadowCascadeSplit; break; case 2: serialized.shadowCascade3SplitProp.vector2Value = shadowCascadeSplit; break; case 1: serialized.shadowCascade2SplitProp.floatValue = shadowCascadeSplit.x; break; } } var borderValue = serialized.shadowCascadeBorderProp.floatValue; EditorGUI.BeginChangeCheck(); if (useMetric) { var lastCascadeSplitSize = splitCount == 0 ? baseMetric : (1.0f - shadowCascadeSplit[splitCount - 1]) * baseMetric; var invLastCascadeSplitSize = lastCascadeSplitSize == 0 ? 0 : 1f / lastCascadeSplitSize; float valueMetric = borderValue * lastCascadeSplitSize; valueMetric = EditorGUILayout.Slider(EditorGUIUtility.TrTextContent("Last Border", "The distance of the last cascade."), valueMetric, 0f, lastCascadeSplitSize, null); borderValue = valueMetric * invLastCascadeSplitSize; } else { float valueProcentage = borderValue * 100f; valueProcentage = EditorGUILayout.Slider(EditorGUIUtility.TrTextContent("Last Border", "The distance of the last cascade."), valueProcentage, 0f, 100f, null); borderValue = valueProcentage * 0.01f; } if (EditorGUI.EndChangeCheck()) { serialized.shadowCascadeBorderProp.floatValue = borderValue; } } static void DrawCascades(SerializedUniversalRenderPipelineAsset serialized, int cascadeCount, bool useMetric, float baseMetric) { var cascades = new ShadowCascadeGUI.Cascade[cascadeCount]; Vector3 shadowCascadeSplit = Vector3.zero; if (cascadeCount == 4) shadowCascadeSplit = serialized.shadowCascade4SplitProp.vector3Value; else if (cascadeCount == 3) shadowCascadeSplit = serialized.shadowCascade3SplitProp.vector2Value; else if (cascadeCount == 2) shadowCascadeSplit.x = serialized.shadowCascade2SplitProp.floatValue; else shadowCascadeSplit.x = serialized.shadowCascade2SplitProp.floatValue; float lastCascadePartitionSplit = 0; for (int i = 0; i < cascadeCount - 1; ++i) { cascades[i] = new ShadowCascadeGUI.Cascade() { size = i == 0 ? shadowCascadeSplit[i] : shadowCascadeSplit[i] - lastCascadePartitionSplit, // Calculate the size of cascade borderSize = 0, cascadeHandleState = ShadowCascadeGUI.HandleState.Enabled, borderHandleState = ShadowCascadeGUI.HandleState.Hidden, }; lastCascadePartitionSplit = shadowCascadeSplit[i]; } // Last cascade is special var lastCascade = cascadeCount - 1; cascades[lastCascade] = new ShadowCascadeGUI.Cascade() { size = lastCascade == 0 ? 1.0f : 1 - shadowCascadeSplit[lastCascade - 1], // Calculate the size of cascade borderSize = serialized.shadowCascadeBorderProp.floatValue, cascadeHandleState = ShadowCascadeGUI.HandleState.Hidden, borderHandleState = ShadowCascadeGUI.HandleState.Enabled, }; EditorGUI.BeginChangeCheck(); ShadowCascadeGUI.DrawCascades(ref cascades, useMetric, baseMetric); if (EditorGUI.EndChangeCheck()) { if (cascadeCount == 4) serialized.shadowCascade4SplitProp.vector3Value = new Vector3( cascades[0].size, cascades[0].size + cascades[1].size, cascades[0].size + cascades[1].size + cascades[2].size ); else if (cascadeCount == 3) serialized.shadowCascade3SplitProp.vector2Value = new Vector2( cascades[0].size, cascades[0].size + cascades[1].size ); else if (cascadeCount == 2) serialized.shadowCascade2SplitProp.floatValue = cascades[0].size; serialized.shadowCascadeBorderProp.floatValue = cascades[lastCascade].borderSize; } } static void DrawPostProcessing(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { EditorGUILayout.PropertyField(serialized.colorGradingMode, Styles.colorGradingMode); bool isHdrOn = serialized.hdr.boolValue; if (!isHdrOn && serialized.colorGradingMode.intValue == (int)ColorGradingMode.HighDynamicRange) EditorGUILayout.HelpBox(Styles.colorGradingModeWarning, MessageType.Warning); else if (isHdrOn && serialized.colorGradingMode.intValue == (int)ColorGradingMode.HighDynamicRange) EditorGUILayout.HelpBox(Styles.colorGradingModeSpecInfo, MessageType.Info); else if (isHdrOn && PlayerSettings.allowHDRDisplaySupport && serialized.colorGradingMode.intValue == (int)ColorGradingMode.LowDynamicRange) EditorGUILayout.HelpBox(Styles.colorGradingModeWithHDROutput, MessageType.Warning); EditorGUI.BeginChangeCheck(); EditorGUILayout.DelayedIntField(serialized.colorGradingLutSize, Styles.colorGradingLutSize); if (EditorGUI.EndChangeCheck()) serialized.colorGradingLutSize.intValue = Mathf.Clamp(serialized.colorGradingLutSize.intValue, UniversalRenderPipelineAsset.k_MinLutSize, UniversalRenderPipelineAsset.k_MaxLutSize); if (isHdrOn && serialized.colorGradingMode.intValue == (int)ColorGradingMode.HighDynamicRange && serialized.colorGradingLutSize.intValue < 32) EditorGUILayout.HelpBox(Styles.colorGradingLutSizeWarning, MessageType.Warning); HDRColorBufferPrecision hdrPrecision = (HDRColorBufferPrecision)serialized.hdrColorBufferPrecisionProp.intValue; bool alphaEnabled = !isHdrOn /*RGBA8*/ || (isHdrOn && hdrPrecision == HDRColorBufferPrecision._64Bits); /*RGBA16Float*/ EditorGUILayout.PropertyField(serialized.allowPostProcessAlphaOutput, Styles.allowPostProcessAlphaOutput); if(!alphaEnabled && serialized.allowPostProcessAlphaOutput.boolValue) EditorGUILayout.HelpBox(Styles.alphaOutputWarning, MessageType.Warning); EditorGUILayout.PropertyField(serialized.useFastSRGBLinearConversion, Styles.useFastSRGBLinearConversion); EditorGUILayout.PropertyField(serialized.supportDataDrivenLensFlare, Styles.supportDataDrivenLensFlare); EditorGUILayout.PropertyField(serialized.supportScreenSpaceLensFlare, Styles.supportScreenSpaceLensFlare); } static Editor s_VolumeProfileEditor; static void DrawVolumes(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { CoreEditorUtils.DrawPopup(Styles.volumeFrameworkUpdateMode, serialized.volumeFrameworkUpdateModeProp, Styles.volumeFrameworkUpdateOptions); EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.PropertyField(serialized.volumeProfileProp, Styles.volumeProfileLabel); var profile = serialized.volumeProfileProp.objectReferenceValue as VolumeProfile; if (EditorGUI.EndChangeCheck() && UniversalRenderPipeline.asset == serialized.serializedObject.targetObject && RenderPipelineManager.currentPipeline is UniversalRenderPipeline) VolumeManager.instance.SetQualityDefaultProfile(serialized.volumeProfileProp.objectReferenceValue as VolumeProfile); var contextMenuButtonRect = GUILayoutUtility.GetRect(CoreEditorStyles.contextMenuIcon, Styles.volumeProfileContextMenuStyle.Value); if (GUI.Button(contextMenuButtonRect, CoreEditorStyles.contextMenuIcon, Styles.volumeProfileContextMenuStyle.Value)) { var profileEditor = s_VolumeProfileEditor as VolumeProfileEditor; var componentEditors = profileEditor != null ? profileEditor.componentList.editors : null; var srpAsset = serialized.serializedObject.targetObject as UniversalRenderPipelineAsset; var pos = new Vector2(contextMenuButtonRect.x, contextMenuButtonRect.yMax); VolumeProfileUtils.OnVolumeProfileContextClick(pos, srpAsset.volumeProfile, componentEditors, overrideStateOnReset: false, defaultVolumeProfilePath: $"Assets/{srpAsset.name}_VolumeProfile.asset", onNewVolumeProfileCreated: volumeProfile => { Undo.RecordObject(srpAsset, "Set UniversalRenderPipelineAsset Volume Profile"); srpAsset.volumeProfile = volumeProfile; if (UniversalRenderPipeline.asset == srpAsset) VolumeManager.instance.SetQualityDefaultProfile(volumeProfile); EditorUtility.SetDirty(srpAsset); }); } EditorGUILayout.EndHorizontal(); GUILayout.Space(2); if (profile != null) { Editor.CreateCachedEditor(profile, typeof(VolumeProfileEditor), ref s_VolumeProfileEditor); bool oldEnabled = GUI.enabled; GUI.enabled = AssetDatabase.IsOpenForEdit(profile); s_VolumeProfileEditor.OnInspectorGUI(); GUI.enabled = oldEnabled; } else { CoreUtils.Destroy(s_VolumeProfileEditor); } } #if ENABLE_ADAPTIVE_PERFORMANCE static void DrawAdaptivePerformance(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { EditorGUILayout.PropertyField(serialized.useAdaptivePerformance, Styles.useAdaptivePerformance); } #endif struct TileOnlyModeInfos { public bool enabled => !string.IsNullOrEmpty(rendererNames); public readonly bool isSingleRendererCase; public readonly string rendererNames; public TileOnlyModeInfos(bool isSingleRendererCase, string rendererNames) { this.isSingleRendererCase = isSingleRendererCase; this.rendererNames = rendererNames; } } static TileOnlyModeInfos lastTileOnlyModeInfos; //prevent computing this multiple time for this ImGUI frame static void PrepareTileOnlyModeWarning(SerializedUniversalRenderPipelineAsset serialized, Editor ownerEditor) { // Rules: // - mono selection: // - only 1 Renderer in the list: Display warning if RendererData's Tile-Only Mode is enabled // - many Renderers in the list: Display warning with list of RendererData's where Tile-Only Mode is enabled // - multi selection: // - compute the list interection of RendererDatas where Tile-Only Mode is enabled amongst all URPAsset in selection // - If list is not empty, display warning with this list. Additionaly specify for item iden due to being at different position // - Additionally for both, for Post Processing section: only show names where Post Processing is enabled lastTileOnlyModeInfos = default; // If impacted section are not opened, early exit if (!(k_ExpandedState[Expandable.Rendering] || k_ExpandedState[Expandable.Quality] || k_ExpandedState[Expandable.PostProcessing])) return; // Helper to iterate property of an array quickly (without GetArrayElementAtIndex) IEnumerable ArrayElementPropertyEnumerator(SerializedProperty property) { if (!property.hasVisibleChildren) yield break; var iterator = property.Copy(); var end = iterator.GetEndProperty(); // Move to the first child property iterator.NextVisible(enterChildren: true); iterator.NextVisible(enterChildren: false); //skip size while (!SerializedProperty.EqualContents(iterator, end)) { yield return iterator; iterator.NextVisible(enterChildren: false); } } // Helper to filter the list and get only unique result of UniversalRendererData that have Tile-Only Mode. // The returned IDisposable is for being able to return the HashSet to the pool when Dispose is call like at end of Using. IDisposable SelectUniqueAndCast(IEnumerable properties, out HashSet uniques) { var e = properties.GetEnumerator(); var disposer = HashSetPool.Get(out uniques); while (e.MoveNext()) if (!e.Current.hasMultipleDifferentValues && e.Current.boxedValue is UniversalRendererData universalData && universalData.tileOnlyMode) uniques.Add(universalData); return disposer; } // Helper for multiple selection to distinguish element that remain at stable position (in the selection) from others string ConcatCollectionInName(IEnumerable rightlyPositioned, IEnumerable wronglyPositioned) { var firstPart = FormatRendererNames(rightlyPositioned); var secondPart = FormatRendererNames(wronglyPositioned, Styles.suffixWhenDifferentPositionTileOnlyMode); if (string.IsNullOrEmpty(firstPart)) return secondPart; if (string.IsNullOrEmpty(secondPart)) return firstPart; return $"{firstPart}, {secondPart}"; } string names = null; if (!serialized.rendererDatas.hasMultipleDifferentValues) { // Simple case: all element selected share the same list. // Minimize the operation alongs foldout opened using (SelectUniqueAndCast(ArrayElementPropertyEnumerator(serialized.rendererDatas), out var renderers)) { if (renderers.Count == 0) return; if (k_ExpandedState[Expandable.Rendering] || k_ExpandedState[Expandable.Quality]) names = FormatRendererNames(renderers); bool isSingleRendererCase = serialized.rendererDatas.arraySize == 1; lastTileOnlyModeInfos = new TileOnlyModeInfos(isSingleRendererCase, names); return; } } // Complex case: the renderer list is different in some elements of the selection // Let's compute the intersection of each RendererList where it is a UniversalRenderer with Tile-Only Mode enabled. // If the intersection is empty, it would means no RendererData validate the criteria so we early exit. // We can retrieve element at stable position by directly checking the serialization of the selection. // Elements in the intersection that are not in the stable positio list are elements shared in all list but with moving index. // Helper to build the HashSet of UniversalRenderer that have Tile-Only Mode on one targeted asset. // The returned IDisposable is for being able to return the HashSet to the pool when Dispose is call like at end of Using. IDisposable GetUniversalRendererWithTileOnlyModeEnabled(UniversalRenderPipelineAsset asset, out HashSet set) { IDisposable disposer = HashSetPool.Get(out set); for (int rendererIndex = 0; rendererIndex < asset.rendererDataList.Length; ++rendererIndex) if (asset.rendererDataList[rendererIndex] is UniversalRendererData universalData && universalData.tileOnlyMode) set.Add(universalData); return disposer; } using (GetUniversalRendererWithTileOnlyModeEnabled((UniversalRenderPipelineAsset)serialized.serializedObject.targetObjects[0], out var movingPositions)) { if (movingPositions.Count == 0) return; for (int i = 1; i < serialized.serializedObject.targetObjects.Length; ++i) using (GetUniversalRendererWithTileOnlyModeEnabled((UniversalRenderPipelineAsset)serialized.serializedObject.targetObjects[i], out var otherIntersection)) { if (otherIntersection.Count == 0) return; movingPositions.IntersectWith(otherIntersection); if (movingPositions.Count == 0) return; } using (SelectUniqueAndCast(ArrayElementPropertyEnumerator(serialized.rendererDatas), out var stablePositions)) { foreach (var stablePositionElement in stablePositions) movingPositions.Remove(stablePositionElement); if (k_ExpandedState[Expandable.Rendering] || k_ExpandedState[Expandable.Quality]) names = ConcatCollectionInName(stablePositions, movingPositions); } lastTileOnlyModeInfos = new TileOnlyModeInfos( isSingleRendererCase: false, rendererNames: names ); } } static void DisplayTileOnlyHelpBox(SerializedProperty prop, Func shouldDisplay, GUIContent label, MessageType messageType = MessageType.Warning, string customMessage = null) { if (prop == null || shouldDisplay == null || !lastTileOnlyModeInfos.enabled || !shouldDisplay(prop)) return; var fmt = !string.IsNullOrEmpty(customMessage) ? customMessage : Styles.formatterTileOnlyMode; var labelText = label == null ? prop.displayName : label.text; string message = string.Format(fmt, labelText, lastTileOnlyModeInfos.rendererNames); EditorGUILayout.HelpBox(message, messageType); } } }