From 3bde9fc53f39d85a7c23d1956a89268635bf242a Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 20 Oct 2026 22:46:57 +0200 Subject: [PATCH] Add URP upscalers and independent effect toggles --- Editor/UrpUpscalerBuildResources.cs | 64 +++++++ Editor/UrpUpscalerBuildResources.cs.meta | 2 + ...sualPinball.Engine.Unity.Urp.Editor.asmdef | 7 +- Runtime/UrpGraphicsApplier.cs | 171 +++++++++++++++++- 4 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 Editor/UrpUpscalerBuildResources.cs create mode 100644 Editor/UrpUpscalerBuildResources.cs.meta diff --git a/Editor/UrpUpscalerBuildResources.cs b/Editor/UrpUpscalerBuildResources.cs new file mode 100644 index 0000000..f3f4ab1 --- /dev/null +++ b/Editor/UrpUpscalerBuildResources.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEditor.Rendering; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; + +namespace VisualPinball.Unity.Urp +{ + /// Keeps STP's runtime resources when users can select it after the build. + public sealed class UrpUpscalerBuildResources : IPreprocessBuildWithReport, IPostprocessBuildWithReport + { + // After CorePreprocessBuild (+50), before URPPreprocessBuild (+100) snapshots the list. + public int callbackOrder => int.MinValue + 75; + private static UniversalRenderPipelineAsset _retentionAsset; + private static List _buildAssets; + + public void OnPreprocessBuild(BuildReport report) + { + Cleanup(); + var assets = CoreBuildData.instance.renderPipelineAssets; + UniversalRenderPipelineAsset source = null; + foreach (var asset in assets) { + if (asset is UniversalRenderPipelineAsset urp) { + if (urp.isStpUsed) return; + source ??= urp; + } + } + if (source == null) return; + // Only the transient build-data list changes. Project assets and quality tiers stay untouched. + _retentionAsset = Object.Instantiate(source); + _retentionAsset.name = "URP runtime STP resource retention"; + _retentionAsset.hideFlags = HideFlags.HideAndDontSave; + _retentionAsset.upscalingFilter = UpscalingFilterSelection.STP; +#if ENABLE_UPSCALER_FRAMEWORK + _retentionAsset.upscalerName = "Spatial-Temporal Post-Processing"; +#endif + _buildAssets = assets; + assets.Add(_retentionAsset); + EditorApplication.update += CleanupAfterBuild; + AssemblyReloadEvents.beforeAssemblyReload += Cleanup; + } + + public void OnPostprocessBuild(BuildReport report) => Cleanup(); + + private static void CleanupAfterBuild() + { + // Postprocess callbacks may be skipped on failure/cancellation. + if (!BuildPipeline.isBuildingPlayer) Cleanup(); + } + + private static void Cleanup() + { + EditorApplication.update -= CleanupAfterBuild; + AssemblyReloadEvents.beforeAssemblyReload -= Cleanup; + if (_buildAssets != null && _retentionAsset != null) _buildAssets.Remove(_retentionAsset); + _buildAssets = null; + if (_retentionAsset != null) Object.DestroyImmediate(_retentionAsset); + _retentionAsset = null; + } + } +} diff --git a/Editor/UrpUpscalerBuildResources.cs.meta b/Editor/UrpUpscalerBuildResources.cs.meta new file mode 100644 index 0000000..d1085f6 --- /dev/null +++ b/Editor/UrpUpscalerBuildResources.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 80b11aaff51b483ca88f7515167417e6 \ No newline at end of file diff --git a/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef b/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef index 603ada1..fa13283 100644 --- a/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef +++ b/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef @@ -8,9 +8,12 @@ "VisualPinball.Unity.Editor", "VisualPinball.Engine.Unity.Urp", "Unity.RenderPipelines.Core.Runtime", - "Unity.RenderPipelines.Universal.Runtime" + "Unity.RenderPipelines.Universal.Runtime", + "Unity.RenderPipelines.Core.Editor" + ], + "includePlatforms": [ + "Editor" ], - "includePlatforms": [ "Editor" ], "excludePlatforms": [], "allowUnsafeCode": true, "overrideReferences": false, diff --git a/Runtime/UrpGraphicsApplier.cs b/Runtime/UrpGraphicsApplier.cs index d37295b..3046179 100644 --- a/Runtime/UrpGraphicsApplier.cs +++ b/Runtime/UrpGraphicsApplier.cs @@ -25,22 +25,183 @@ namespace VisualPinball.Unity.Urp { /// /// Applies the host app's to URP at runtime: the game camera's - /// anti-aliasing and the global Volume's bloom. The other effects are URP renderer features, which - /// the renderer assets configure. + /// anti-aliasing, global Volume bloom and the active pipeline's SSR/SSAO renderer features. /// public sealed class UrpGraphicsApplier : IVpeGraphicsApplier { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - private static void Register() => VpeGraphics.Register(new UrpGraphicsApplier()); + private static void Register() + { + RestoreUpscalerState(); + _upscaler = VpeUpscaler.Off; + _antiAliasing = -1; + VpeGraphics.Register(new UrpGraphicsApplier()); + VpeGraphics.UpscalerSupport = IsUpscalerSupported; + VpeGraphics.RenderScaleSource = () => GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset p ? p.renderScale : 1f; +#if UNITY_EDITOR + UnityEditor.EditorApplication.playModeStateChanged -= OnEditorPlayModeChanged; + UnityEditor.EditorApplication.playModeStateChanged += OnEditorPlayModeChanged; +#endif + } + +#if UNITY_EDITOR + private static readonly System.Collections.Generic.Dictionary OriginalFeatureStates = new(); + + private static void OnEditorPlayModeChanged(UnityEditor.PlayModeStateChange state) + { + if (state != UnityEditor.PlayModeStateChange.ExitingPlayMode) return; + foreach (var item in OriginalFeatureStates) { + if (item.Key != null) item.Key.SetActive(item.Value); + } + OriginalFeatureStates.Clear(); + RestoreUpscalerState(); + } +#endif // the user's choice (-1 = none yet, the camera keeps its own) private static int _antiAliasing = -1; + private static VpeUpscaler _upscaler; + private static int _upscalerQuality; + + private struct PipelineState + { + public float Scale; + public int Msaa; + public UpscalingFilterSelection Filter; +#if ENABLE_UPSCALER_FRAMEWORK + public string Name; +#endif + } + private struct CameraState { public bool PostProcessing, DynamicResolution; public VpeUpscaler LastUpscaler; public float LastScale; } + private static readonly System.Collections.Generic.Dictionary PipelineStates = new(); + private static readonly System.Collections.Generic.Dictionary CameraStates = new(); + + private static bool IsUpscalerSupported(VpeUpscaler upscaler) => upscaler switch { + VpeUpscaler.Off => true, + VpeUpscaler.Fsr1 => FSRUtils.IsSupported(), + VpeUpscaler.Stp => STP.IsSupported(), + _ => false, + }; + + // Ratios are absolute output-resolution fractions, not multiplied by the preset's render scale. + private static float UpscalerScale(int quality) => Mathf.Clamp(quality, 0, 3) switch { + 0 => 1f / 1.5f, + 1 => 1f / 1.7f, + 2 => 0.5f, + _ => 1f / 3f, + }; + + private static void RestoreUpscalerState() + { + foreach (var item in PipelineStates) { + if (item.Key == null) continue; + RestorePipeline(item.Key, item.Value); + } + PipelineStates.Clear(); + foreach (var item in CameraStates) { + if (item.Key == null) continue; + item.Key.allowDynamicResolution = item.Value.DynamicResolution; + if (item.Key.TryGetComponent(out var data)) data.renderPostProcessing = item.Value.PostProcessing; + } + CameraStates.Clear(); + } + + private static void RestorePipeline(UniversalRenderPipelineAsset pipeline, PipelineState original) + { + pipeline.renderScale = original.Scale; + pipeline.msaaSampleCount = original.Msaa; + pipeline.upscalingFilter = original.Filter; +#if ENABLE_UPSCALER_FRAMEWORK + pipeline.upscalerName = original.Name; +#endif + } + + private static VpeUpscaler ApplyUpscaler(Camera camera, UniversalAdditionalCameraData data) + { + if (GraphicsSettings.currentRenderPipeline is not UniversalRenderPipelineAsset pipeline) return VpeUpscaler.Off; + if (!PipelineStates.TryGetValue(pipeline, out var original)) { + original = new PipelineState { Scale = pipeline.renderScale, Msaa = pipeline.msaaSampleCount, Filter = pipeline.upscalingFilter }; +#if ENABLE_UPSCALER_FRAMEWORK + original.Name = pipeline.upscalerName; +#endif + PipelineStates.Add(pipeline, original); + } + if (!CameraStates.TryGetValue(camera, out var cameraOriginal)) { + cameraOriginal = new CameraState { PostProcessing = data.renderPostProcessing, DynamicResolution = camera.allowDynamicResolution, LastScale = original.Scale }; + CameraStates.Add(camera, cameraOriginal); + } + var effective = IsUpscalerSupported(_upscaler) ? _upscaler : VpeUpscaler.Off; + // STP shares URP TAA's restriction against camera stacks and overlay cameras. + if (effective == VpeUpscaler.Stp && (data.renderType == CameraRenderType.Overlay || data.cameraStack?.Count > 0 || CabinetProjection.EffectiveAntiAliasing(camera, VpeAntiAliasing.Taa) != VpeAntiAliasing.Taa)) effective = VpeUpscaler.Off; + if (effective == VpeUpscaler.Off) { + RestorePipeline(pipeline, original); + if (cameraOriginal.LastUpscaler != VpeUpscaler.Off || !Mathf.Approximately(cameraOriginal.LastScale, original.Scale)) data.resetHistory = true; + cameraOriginal.LastUpscaler = VpeUpscaler.Off; + cameraOriginal.LastScale = original.Scale; + CameraStates[camera] = cameraOriginal; + data.renderPostProcessing = cameraOriginal.PostProcessing; + camera.allowDynamicResolution = cameraOriginal.DynamicResolution; + return effective; + } + pipeline.renderScale = UpscalerScale(_upscalerQuality); + pipeline.msaaSampleCount = effective == VpeUpscaler.Stp ? 1 : original.Msaa; + pipeline.upscalingFilter = effective == VpeUpscaler.Stp ? UpscalingFilterSelection.STP : UpscalingFilterSelection.FSR; +#if ENABLE_UPSCALER_FRAMEWORK + pipeline.upscalerName = effective == VpeUpscaler.Stp ? "Spatial-Temporal Post-Processing" : "FidelityFX Super Resolution 1.0"; +#endif + data.renderPostProcessing = true; + camera.allowDynamicResolution = false; + if (cameraOriginal.LastUpscaler != effective || !Mathf.Approximately(cameraOriginal.LastScale, pipeline.renderScale)) data.resetHistory = true; + cameraOriginal.LastUpscaler = effective; + cameraOriginal.LastScale = pipeline.renderScale; + CameraStates[camera] = cameraOriginal; + return effective; + } public void Apply(VpeGraphicsSettings settings) { _antiAliasing = Mathf.Clamp(settings.AntiAliasing, 0, 3); + _upscaler = (VpeUpscaler)settings.Upscaler; + _upscalerQuality = Mathf.Clamp(settings.UpscalerQuality, 0, 3); ApplyCameraAntialiasing(Camera.main); ApplyVolume(settings); + ApplyRendererFeatures(settings); + } + + private static void ApplyRendererFeatures(VpeGraphicsSettings settings) + { + if (GraphicsSettings.currentRenderPipeline is not UniversalRenderPipelineAsset pipeline) { + return; + } + foreach (var renderer in pipeline.rendererDataList) { + if (renderer == null) { + continue; + } + foreach (var feature in renderer.rendererFeatures) { + if (feature == null) { + continue; + } + // SSR is an optional third-party assembly; match its type rather than its editable asset name. + switch (feature.GetType().FullName) { + case "UniversalScreenSpaceReflection.ScreenSpaceReflection": + SetFeatureActive(feature, settings.ScreenSpaceReflections); + break; + case "UnityEngine.Rendering.Universal.ScreenSpaceAmbientOcclusion": + SetFeatureActive(feature, settings.AmbientOcclusion); + break; + } + } + } + } + + private static void SetFeatureActive(ScriptableRendererFeature feature, bool enabled) + { +#if UNITY_EDITOR + if (Application.isPlaying && !OriginalFeatureStates.ContainsKey(feature)) { + OriginalFeatureStates.Add(feature, feature.isActive); + } +#endif + feature.SetActive(enabled); } /// The chosen anti-aliasing, or what the camera's cabinet projection allows instead. @@ -49,8 +210,10 @@ internal static void ApplyCameraAntialiasing(Camera camera) if (camera == null || !camera.TryGetComponent(out var data)) { return; } + var upscaler = ApplyUpscaler(camera, data); if (_antiAliasing >= 0) { - var aa = CabinetProjection.EffectiveAntiAliasing(camera, (VpeAntiAliasing)_antiAliasing); + var requested = upscaler == VpeUpscaler.Stp ? VpeAntiAliasing.Taa : (VpeAntiAliasing)_antiAliasing; + var aa = CabinetProjection.EffectiveAntiAliasing(camera, requested); data.antialiasing = aa switch { VpeAntiAliasing.Fxaa => AntialiasingMode.FastApproximateAntialiasing, VpeAntiAliasing.Smaa => AntialiasingMode.SubpixelMorphologicalAntiAliasing,