using System.Collections; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.Rendering; namespace FidelityFX { /// /// This class is responsible for hooking into various Unity events and translating them to the FSR2 subsystem. /// This includes creation and destruction of the FSR2 context, as well as dispatching commands at the right time. /// This component also exposes various FSR2 parameters to the Unity inspector. /// [RequireComponent(typeof(Camera))] public class Fsr2Controller : MonoBehaviour { public Fsr2.QualityMode qualityMode = Fsr2.QualityMode.Quality; public bool performSharpenPass = true; [Range(0, 1)] public float sharpness = 0.8f; [Tooltip("Allow the use of half precision compute operations, potentially improving performance")] public bool enableFP16 = false; [Header("Reactivity, Transparency & Composition")] public Texture reactiveMask = null; public Texture transparencyAndCompositionMask = null; public bool autoGenerateReactiveMask = true; [SerializeField] private GenerateReactiveParameters generateReactiveParameters = new GenerateReactiveParameters(); public GenerateReactiveParameters GenerateReactiveParams => generateReactiveParameters; [System.Serializable] public class GenerateReactiveParameters { [Range(0, 2)] public float scale = 1.0f; [Range(0, 1)] public float cutoffThreshold = 0.2f; [Range(0, 1)] public float binaryValue = 0.9f; public Fsr2.GenerateReactiveFlags flags = Fsr2.GenerateReactiveFlags.ApplyTonemap | Fsr2.GenerateReactiveFlags.ApplyThreshold | Fsr2.GenerateReactiveFlags.UseComponentsMax; } private Fsr2Context _context; private Vector2Int _renderSize; private Vector2Int _displaySize; private bool _reset; private Camera _renderCamera; private RenderTexture _originalRenderTarget; private DepthTextureMode _originalDepthTextureMode; private Rect _originalRect; private readonly Fsr2.DispatchDescription _dispatchDescription = new Fsr2.DispatchDescription(); private readonly Fsr2.GenerateReactiveDescription _genReactiveDescription = new Fsr2.GenerateReactiveDescription(); private Fsr2.QualityMode _prevQualityMode; private Vector2Int _prevDisplaySize; private bool _prevGenReactiveMask; private CommandBuffer _dispatchCommandBuffer; private CommandBuffer _opaqueInputCommandBuffer; private CommandBuffer _inputsCommandBuffer; private void OnEnable() { Fsr2.InitializationFlags flags = 0; if (enableFP16) flags |= Fsr2.InitializationFlags.EnableFP16Usage; _displaySize = new Vector2Int(Screen.width, Screen.height); Fsr2.GetRenderResolutionFromQualityMode(out var renderWidth, out var renderHeight, _displaySize.x, _displaySize.y, qualityMode); _renderSize = new Vector2Int(renderWidth, renderHeight); _context = Fsr2.CreateContext(_displaySize, _renderSize, flags); // Set up the original camera to output all of the required FSR2 input resources at the desired resolution _renderCamera = GetComponent(); _originalDepthTextureMode = _renderCamera.depthTextureMode; _renderCamera.depthTextureMode = _originalDepthTextureMode | DepthTextureMode.Depth | DepthTextureMode.MotionVectors; _dispatchCommandBuffer = new CommandBuffer { name = "FSR2 Dispatch" }; // Create command buffers to bind the camera's output at the right moments in the render loop _opaqueInputCommandBuffer = new CommandBuffer { name = "FSR2 Opaque Input" }; _opaqueInputCommandBuffer.GetTemporaryRT(Fsr2Pipeline.SrvOpaqueOnly, _renderSize.x, _renderSize.y, 0, default, RenderTextureFormat.ARGBHalf); _opaqueInputCommandBuffer.Blit(BuiltinRenderTextureType.CameraTarget, Fsr2Pipeline.SrvOpaqueOnly); _inputsCommandBuffer = new CommandBuffer { name = "FSR2 Inputs" }; _inputsCommandBuffer.SetGlobalTexture(Fsr2Pipeline.SrvInputColor, BuiltinRenderTextureType.CameraTarget, RenderTextureSubElement.Color); _inputsCommandBuffer.SetGlobalTexture(Fsr2Pipeline.SrvInputDepth, BuiltinRenderTextureType.CameraTarget, RenderTextureSubElement.Depth); _inputsCommandBuffer.SetGlobalTexture(Fsr2Pipeline.SrvInputMotionVectors, BuiltinRenderTextureType.MotionVectors); _renderCamera.AddCommandBuffer(CameraEvent.BeforeImageEffects, _inputsCommandBuffer); if (autoGenerateReactiveMask) { _renderCamera.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, _opaqueInputCommandBuffer); } // Apply a mipmap bias so that textures retain their original sharpness float biasOffset = Fsr2.GetMipmapBiasOffset(_renderSize.x, _displaySize.x); Fsr2.GlobalCallbacks.ApplyMipmapBias(biasOffset); _prevDisplaySize = _displaySize; _prevQualityMode = qualityMode; _prevGenReactiveMask = autoGenerateReactiveMask; } private void OnDisable() { float biasOffset = Fsr2.GetMipmapBiasOffset(_renderSize.x, _prevDisplaySize.x); Fsr2.GlobalCallbacks.ApplyMipmapBias(-biasOffset); if (_opaqueInputCommandBuffer != null) { _renderCamera.RemoveCommandBuffer(CameraEvent.BeforeForwardAlpha, _opaqueInputCommandBuffer); _opaqueInputCommandBuffer.Release(); _opaqueInputCommandBuffer = null; } if (_inputsCommandBuffer != null) { _renderCamera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, _inputsCommandBuffer); _inputsCommandBuffer.Release(); _inputsCommandBuffer = null; } if (_dispatchCommandBuffer != null) { _dispatchCommandBuffer.Release(); _dispatchCommandBuffer = null; } _renderCamera.depthTextureMode = _originalDepthTextureMode; if (_context != null) { _context.Destroy(); _context = null; } } private void Update() { if (Screen.width != _prevDisplaySize.x || Screen.height != _prevDisplaySize.y || qualityMode != _prevQualityMode) { // Force all resources to be destroyed and recreated with the new settings OnDisable(); OnEnable(); } if (autoGenerateReactiveMask != _prevGenReactiveMask) { if (autoGenerateReactiveMask) _renderCamera.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, _opaqueInputCommandBuffer); else _renderCamera.RemoveCommandBuffer(CameraEvent.BeforeForwardAlpha, _opaqueInputCommandBuffer); _prevGenReactiveMask = autoGenerateReactiveMask; } } public void Reset() { _reset = true; } private void OnPreRender() { // Render to a smaller portion of the screen by manipulating the camera's viewport rect _originalRect = _renderCamera.rect; _renderCamera.aspect = (Screen.width * _originalRect.width) / (Screen.height * _originalRect.height); _renderCamera.rect = new Rect(0, 0, _originalRect.width * _renderSize.x / _displaySize.x, _originalRect.height * _renderSize.y / _displaySize.y); // Set up the parameters to auto-generate a reactive mask if (autoGenerateReactiveMask) { _genReactiveDescription.ColorOpaqueOnly = null; _genReactiveDescription.ColorPreUpscale = null; _genReactiveDescription.OutReactive = null; _genReactiveDescription.RenderSize = _renderSize; _genReactiveDescription.Scale = generateReactiveParameters.scale; _genReactiveDescription.CutoffThreshold = generateReactiveParameters.cutoffThreshold; _genReactiveDescription.BinaryValue = generateReactiveParameters.binaryValue; _genReactiveDescription.Flags = generateReactiveParameters.flags; } // Set up the main FSR2 dispatch parameters // The input and output textures are left blank here, as they are already being bound elsewhere in this source file _dispatchDescription.Color = null; _dispatchDescription.Depth = null; _dispatchDescription.MotionVectors = null; _dispatchDescription.Exposure = null; _dispatchDescription.Reactive = null; _dispatchDescription.TransparencyAndComposition = null; _dispatchDescription.Output = null; _dispatchDescription.PreExposure = 0; _dispatchDescription.EnableSharpening = performSharpenPass; _dispatchDescription.Sharpness = sharpness; _dispatchDescription.MotionVectorScale.x = -_renderCamera.pixelWidth; _dispatchDescription.MotionVectorScale.y = -_renderCamera.pixelHeight; _dispatchDescription.RenderSize = _renderSize; _dispatchDescription.FrameTimeDelta = Time.unscaledDeltaTime; _dispatchDescription.CameraNear = _renderCamera.nearClipPlane; _dispatchDescription.CameraFar = _renderCamera.farClipPlane; _dispatchDescription.CameraFovAngleVertical = _renderCamera.fieldOfView * Mathf.Deg2Rad; _dispatchDescription.ViewSpaceToMetersFactor = 1.0f; // 1 unit is 1 meter in Unity _dispatchDescription.Reset = _reset; _reset = false; // Perform custom jittering of the camera's projection matrix according to FSR2's recipe int jitterPhaseCount = Fsr2.GetJitterPhaseCount(_renderSize.x, _displaySize.x); Fsr2.GetJitterOffset(out float jitterX, out float jitterY, Time.frameCount, jitterPhaseCount); _dispatchDescription.JitterOffset = new Vector2(jitterX, jitterY); jitterX = 2.0f * jitterX / _renderSize.x; jitterY = 2.0f * jitterY / _renderSize.y; var jitterTranslationMatrix = Matrix4x4.Translate(new Vector3(jitterX, jitterY, 0)); _renderCamera.nonJitteredProjectionMatrix = _renderCamera.projectionMatrix; _renderCamera.projectionMatrix = jitterTranslationMatrix * _renderCamera.nonJitteredProjectionMatrix; _renderCamera.useJitteredProjectionMatrixForTransparentRendering = true; } private void OnRenderImage(RenderTexture src, RenderTexture dest) { // Restore the camera's viewport rect so we can output at full resolution _renderCamera.rect = _originalRect; _renderCamera.ResetProjectionMatrix(); _dispatchCommandBuffer.Clear(); if (autoGenerateReactiveMask) { _dispatchCommandBuffer.GetTemporaryRT(Fsr2Pipeline.UavAutoReactive, _renderSize.x, _renderSize.y, 0, default, GraphicsFormat.R8_UNorm, 1, true); _context.GenerateReactiveMask(_genReactiveDescription, _dispatchCommandBuffer); _dispatchCommandBuffer.ReleaseTemporaryRT(Fsr2Pipeline.SrvOpaqueOnly); _dispatchDescription.Reactive = Fsr2Pipeline.UavAutoReactive; } else if (reactiveMask != null) { _dispatchDescription.Reactive = reactiveMask; } if (transparencyAndCompositionMask != null) { _dispatchDescription.TransparencyAndComposition = transparencyAndCompositionMask; } _dispatchDescription.InputResourceSize = new Vector2Int(src.width, src.height); if (dest != null) { Debug.Log($"src = {src.width}x{src.height}, dest = {dest.width}x{dest.height}"); // We have more image effects lined up after this, so FSR2 can output straight to the intermediate render texture // TODO: we should probably use a shader to include depth & motion vectors into the output, making sure they match the expected output resolution _dispatchCommandBuffer.SetGlobalTexture(Fsr2Pipeline.UavUpscaledOutput, dest); } else { // We are rendering to the backbuffer, so we need a temporary render texture for FSR2 to output to _dispatchCommandBuffer.GetTemporaryRT(Fsr2Pipeline.UavUpscaledOutput, Screen.width, Screen.height, 0, default, GraphicsFormat.R16G16B16A16_SFloat, 1, true); } _context.Dispatch(_dispatchDescription, _dispatchCommandBuffer); // Output upscaled image to screen // TODO: if `dest` is null, we likely don't care about the depth & motion vectors anymore if (dest == null) { _dispatchCommandBuffer.Blit(Fsr2Pipeline.UavUpscaledOutput, dest); _dispatchCommandBuffer.ReleaseTemporaryRT(Fsr2Pipeline.UavUpscaledOutput); } if (autoGenerateReactiveMask) { _dispatchCommandBuffer.ReleaseTemporaryRT(Fsr2Pipeline.UavAutoReactive); } Graphics.ExecuteCommandBuffer(_dispatchCommandBuffer); // Shut up the Unity warning about not writing to the destination texture Graphics.SetRenderTarget(dest); } } }