using Nova; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; namespace NovaSamples.Effects { internal struct BlurFilter : IEquatable { public const int MinDownscale = 0; public const int MaxDownscale = 6; public float BlurRadius; public float Contrast; public float Saturation; public float Brightness; public float Grain; public BlurMode BlurMode; public int BlurDownscaleFactor; public bool UseComputeShader; public int GetBlurableSize(Texture texture) { if (texture == null) { return 1; } int downscale = Mathf.Clamp(BlurDownscaleFactor, MinDownscale, MaxDownscale); int max = Mathf.Max(texture.width, texture.height); return Mathf.Max(Mathf.RoundToInt(max / Mathf.Pow(2, downscale)), 1); } public bool Equals(BlurFilter other) { return BlurRadius == other.BlurRadius && Contrast == other.Contrast && Saturation == other.Saturation && Brightness == other.Brightness && Grain == other.Grain && BlurMode == other.BlurMode && UseComputeShader == other.UseComputeShader && BlurDownscaleFactor == other.BlurDownscaleFactor; } } internal class TextureBlur : IDisposable { #region Constants private const string BlurShaderName = "BlurSinglePassShader"; private const string BlurComputeName = "BlurSinglePassCompute"; private const int MinPasses = 1; private const int MaxPasses = 6; private const float BlurPixelSize = 0.25f; private const float BlurLogBase = 2; private const float TexelPassDownScale = 1 / BlurLogBase; private const int TempTextureDepth = 0; private const int BlurPass = 0; private const int ColorFiltersPass = 1; private const string COLOR_ADJUSTMENT_ON = "COLOR_ADJUSTMENT_ON"; private const string LINEAR_SPACE = "LINEAR_SPACE"; private const string GRAIN_ON = "GRAIN_ON"; private const string BlurKernel = "Blur"; private const float Epsilon = 1e-5f; /// An arbitrary scalar, but grain values over ~0.2 are very noisy and seem less useful private const float GrainScalar = 1 / 5f; private static readonly Vector4 DefaultColors = new Vector4(0, 1, 1, 1); [NonSerialized] private static uint NextInstanceID = 0; [NonSerialized] private static int BlurID = Shader.PropertyToID("_BlurBox"); [NonSerialized] private static int UVScaleID = Shader.PropertyToID("_UVScale"); [NonSerialized] private static int InputTextureID = Shader.PropertyToID("_Input"); [NonSerialized] private static int OutputTextureID = Shader.PropertyToID("_Output"); [NonSerialized] private static int ColorAdjustmentID = Shader.PropertyToID("_ColorAdjustments"); [NonSerialized] private static int GrainID = Shader.PropertyToID("_Grain"); [NonSerialized] private static int OffsetID = Shader.PropertyToID("_Offset"); [NonSerialized] private static int ClipUVsID = Shader.PropertyToID("_ClipUVs"); [NonSerialized] private static int GrainUVsID = Shader.PropertyToID("_GrainUVs"); #endregion #region Properties public RenderTexture OutputTexture => finalBlurredTexture; private Material[] blurMaterials = new Material[MaxPasses]; [NonSerialized] private static Shader blurShader = null; private static Shader BlurShader { get { if (blurShader == null) { blurShader = Resources.Load(BlurShaderName) as Shader; } return blurShader; } } [NonSerialized] private static ComputeShader sharedComputeShader = null; private static ComputeShader SharedComputeShader { get { if (sharedComputeShader == null) { sharedComputeShader = Resources.Load(BlurComputeName) as ComputeShader; } return sharedComputeShader; } } #region Materials /// /// Need multiple materials because material.SetFloat() doesn't /// have a CommandBuffer equivalent and therefore can't be queued /// without preconfiguring multiple materials for the different steps. /// private Material[] BlurMaterials { get { if (blurMaterials[0] == null) { for (int i = 0; i < MaxPasses; ++i) { blurMaterials[i] = new Material(BlurShader); blurMaterials[i].hideFlags = HideFlags.HideAndDontSave; } } return blurMaterials; } } private Material colorAdjustmentMaterial = null; private Material ColorAdjustmentMaterial { get { if (colorAdjustmentMaterial == null) { colorAdjustmentMaterial = new Material(BlurShader); colorAdjustmentMaterial.hideFlags = HideFlags.HideAndDontSave; colorAdjustmentMaterial.EnableKeyword(COLOR_ADJUSTMENT_ON); } return colorAdjustmentMaterial; } } private Material grainMaterial = null; private Material GrainMaterial { get { if (grainMaterial == null) { grainMaterial = new Material(BlurShader); grainMaterial.hideFlags = HideFlags.HideAndDontSave; grainMaterial.EnableKeyword(GRAIN_ON); } return grainMaterial; } } #endregion #region Compute Shaders private ComputeShader computeShader = null; private ComputeShader ComputeShader { get { if (computeShader == null && SharedComputeShader != null) { computeShader = ComputeShader.Instantiate(SharedComputeShader); computeShader.hideFlags = HideFlags.HideAndDontSave; if (UseLinearSpaceComputeShader) { computeShader.EnableKeyword(LINEAR_SPACE); } } return computeShader; } } private ComputeShader colorAdjustmentComputeShader = null; private ComputeShader ColorAdjustmentComputeShader { get { if (colorAdjustmentComputeShader == null && SharedComputeShader != null) { colorAdjustmentComputeShader = ComputeShader.Instantiate(SharedComputeShader); colorAdjustmentComputeShader.hideFlags = HideFlags.HideAndDontSave; colorAdjustmentComputeShader.EnableKeyword(COLOR_ADJUSTMENT_ON); if (UseLinearSpaceComputeShader) { colorAdjustmentComputeShader.EnableKeyword(LINEAR_SPACE); } } return colorAdjustmentComputeShader; } } private int computeKernel = -1; private int ComputeKernel { get { if (computeKernel < 0) { computeKernel = ComputeShader.FindKernel(BlurKernel); } return computeKernel; } } private Vector3Int computeThreads = Vector3Int.zero; private Vector3Int ComputeThreads { get { if (computeThreads == Vector3Int.zero) { ComputeShader.GetKernelThreadGroupSizes(ComputeKernel, out uint x, out uint y, out uint z); computeThreads = new Vector3Int((int)x, (int)y, (int)z); } return computeThreads; } } #endregion private CommandBuffer commandBuffer = null; private CommandBuffer CommandBuffer { get { if (commandBuffer == null) { commandBuffer = new CommandBuffer() { name = $"Blur Effect {NextInstanceID++}" }; } return commandBuffer; } } private int prevMaxBlurableSize = 0; private BlurFilter prevParams = default; private Vector2 prevUVScale = Vector2.zero; private Vector2 prevUIBlockSize = Vector2.zero; private Rect prevViewportRect; private List computeBlurs = new List(); private int[] tempTextureIDs = new int[] { Shader.PropertyToID("_TextureBlurTemp1"), Shader.PropertyToID("_TextureBlurTemp2"), }; private RenderTexture finalBlurredTexture = null; #endregion public void ApplyFilter(UIBlock2D uiBlock, Rect viewportRect, BlurFilter filter, Texture texture, out bool blurTextureChanged) { Vector2 uiBlockSize = uiBlock.CalculatedSize.XY.Value; Vector2 uvScale = GetUVScale(uiBlock, texture); int blurableSize = filter.GetBlurableSize(texture); blurTextureChanged = TryUpdateOutputTexture(uiBlock, texture); bool propertyChanged = !prevParams.Equals(filter); propertyChanged |= prevUIBlockSize != uiBlockSize; propertyChanged |= prevUVScale != uvScale; propertyChanged |= prevViewportRect != viewportRect; propertyChanged |= prevMaxBlurableSize != blurableSize; if (blurTextureChanged || propertyChanged || CommandBuffer.sizeInBytes == 0) { prevParams = filter; prevUIBlockSize = uiBlockSize; prevUVScale = uvScale; prevMaxBlurableSize = blurableSize; prevViewportRect = viewportRect; UpdateBlurCommands(uiBlock, filter, uvScale, viewportRect, texture); } Graphics.ExecuteCommandBuffer(CommandBuffer); } private bool TryUpdateOutputTexture(UIBlock2D uiBlock, Texture texture) { Vector2 inputTextureSize = texture.Size(); bool needFinalTexture = finalBlurredTexture == null; bool finalTextureDimensionsChanged = !needFinalTexture && finalBlurredTexture.Size() != inputTextureSize; if (!needFinalTexture && !finalTextureDimensionsChanged) { return false; } ReleaseOutputTexture(uiBlock); finalBlurredTexture = RenderTexture.GetTemporary((int)inputTextureSize.x, (int)inputTextureSize.y, TempTextureDepth); finalBlurredTexture.name = "Blurred Texture"; return true; } private void ReleaseOutputTexture(UIBlock2D uiBlock) { if (finalBlurredTexture != null) { RenderTexture.ReleaseTemporary(finalBlurredTexture); finalBlurredTexture = null; uiBlock.ClearImage(); } } private void UpdateBlurCommands(UIBlock2D uiBlock, BlurFilter filter, Vector2 uvScale, Rect viewportRect, Texture inputTexture) { CommandBuffer.Clear(); float blur = Mathf.Max(filter.BlurRadius, 0); // input is in range -1 to 1, so we shift by default colors to put into range 0 to 2 Vector4 csb = new Vector4(filter.Contrast, filter.Saturation, filter.Brightness, 0) + DefaultColors; float grainFilter = filter.Grain * GrainScalar; float baseWidth = finalBlurredTexture.width; float baseHeight = finalBlurredTexture.height; float basis = Mathf.Min(filter.GetBlurableSize(inputTexture), Mathf.Max(baseWidth, baseHeight)); int blurableWidth = Mathf.RoundToInt(baseWidth > baseHeight ? basis : basis * baseWidth / baseHeight); int blurableHeight = Mathf.RoundToInt(baseHeight > baseWidth ? basis : basis * baseHeight / baseWidth); Vector2 blurTextureSize = new Vector2(blurableWidth, blurableHeight); Vector2 inputTextureSize = inputTexture.Size(); Vector2 pixelsPerUnit = (inputTextureSize / uvScale) / uiBlock.CalculatedSize.XY.Value; float pixelSize = BlurPixelSize * Mathf.Max(pixelsPerUnit.x, pixelsPerUnit.y); Vector2 texelBlur = blur * new Vector2(pixelSize / inputTextureSize.x, pixelSize / inputTextureSize.y); // Divide by BlurPixelSize because, while texelBlur is technically the right offset, // the algorithm will sample the pixels adjacent to the exterior edges in the first pass // those external samples will bleed into the actual UIBlock rect over sequential passes. // So we clip extra pixels to have the final interior rectangle only include samples from // the input texture. Vector2 blurUVs = texelBlur / BlurPixelSize; Rect scissor = GetScissorRect(blurTextureSize, viewportRect, blurUVs); Vector2 centerUV = viewportRect.center; Vector2 sizeUV = viewportRect.size; if (blur == 0) { bool colorAdjustments = !IsDefaultColors(csb); bool grain = grainFilter > 0; if (colorAdjustments || grain) { ColorAdjustmentMaterial.SetKeyword(COLOR_ADJUSTMENT_ON, colorAdjustments); ColorAdjustmentMaterial.SetKeyword(GRAIN_ON, grain); ColorAdjustmentMaterial.SetVector(ClipUVsID, GetClipUVs(scissor, blurTextureSize)); ColorAdjustmentMaterial.SetVector(GrainUVsID, new Vector4(centerUV.x, centerUV.y, sizeUV.x, sizeUV.y)); ColorAdjustmentMaterial.SetVector(ColorAdjustmentID, csb); ColorAdjustmentMaterial.SetFloat(GrainID, grainFilter); ColorAdjustmentMaterial.SetVector(UVScaleID, GetShaderUVScale(finalBlurredTexture)); CommandBuffer.Blit(inputTexture, finalBlurredTexture, ColorAdjustmentMaterial, ColorFiltersPass); } else { CommandBuffer.Blit(inputTexture, finalBlurredTexture); } return; } float blurMax = Mathf.Max(blurTextureSize.x, blurTextureSize.y); float passBasis = Mathf.Min(blurMax, blur); int logPass = Mathf.FloorToInt(Mathf.Log(passBasis, BlurLogBase)) + 1; int passes = Mathf.Clamp(logPass, MinPasses, MaxPasses); bool useComputeShader = filter.UseComputeShader && ComputeShader != null && ComputeShaderSupported; UnityEngine.Experimental.Rendering.GraphicsFormat format = QualitySettings.activeColorSpace == ColorSpace.Linear ? UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8B8A8_SRGB : UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8B8A8_UNorm; CommandBuffer.GetTemporaryRT(tempTextureIDs[0], (int)blurTextureSize.x, (int)blurTextureSize.y, TempTextureDepth, FilterMode.Bilinear, format, antiAliasing: 1, enableRandomWrite: useComputeShader); CommandBuffer.GetTemporaryRT(tempTextureIDs[1], (int)blurTextureSize.x, (int)blurTextureSize.y, TempTextureDepth, FilterMode.Bilinear, format, antiAliasing: 1, enableRandomWrite: useComputeShader); if (useComputeShader) { UpdateComputeCommands(passes, texelBlur, csb, blurTextureSize, scissor, inputTexture); } else { UpdateBlitCommands(passes, texelBlur, csb, blurTextureSize, scissor, inputTexture); } GrainMaterial.SetKeyword(GRAIN_ON, grainFilter > 0); GrainMaterial.SetFloat(GrainID, grainFilter); GrainMaterial.SetVector(ClipUVsID, GetClipUVs(scissor, blurTextureSize)); GrainMaterial.SetVector(GrainUVsID, new Vector4(centerUV.x, centerUV.y, sizeUV.x, sizeUV.y)); GrainMaterial.SetVector(UVScaleID, GetShaderUVScale(finalBlurredTexture)); CommandBuffer.Blit(tempTextureIDs[0], finalBlurredTexture, GrainMaterial, ColorFiltersPass); CommandBuffer.ReleaseTemporaryRT(tempTextureIDs[0]); CommandBuffer.ReleaseTemporaryRT(tempTextureIDs[1]); } private void UpdateComputeCommands(int passes, Vector2 texelBlur, Vector4 csb, Vector2 blurableSize, Rect scissor, Texture inputTexture) { Vector3Int threads = ComputeThreads; int blurGroupsX = Mathf.Max(Mathf.CeilToInt((float)scissor.width / threads.x), 1); int blurGroupsY = Mathf.Max(Mathf.CeilToInt((float)scissor.height / threads.y), 1); int blurGroupsZ = Mathf.Max(threads.z, 1); int kernel = ComputeKernel; computeBlurs.Clear(); computeBlurs.Add(new Vector4(-texelBlur.x, -texelBlur.y, texelBlur.x, texelBlur.y)); ComputeShader firstShader = !IsDefaultColors(csb) ? ColorAdjustmentComputeShader : ComputeShader; CommandBuffer.SetComputeVectorParam(firstShader, OffsetID, scissor.position); CommandBuffer.SetComputeVectorParam(firstShader, ColorAdjustmentID, csb); CommandBuffer.SetComputeVectorParam(firstShader, BlurID, computeBlurs[0]); CommandBuffer.SetComputeVectorParam(firstShader, UVScaleID, GetShaderUVScale((int)blurableSize.x, (int)blurableSize.y)); CommandBuffer.SetComputeTextureParam(firstShader, kernel, InputTextureID, inputTexture); CommandBuffer.SetComputeTextureParam(firstShader, kernel, OutputTextureID, tempTextureIDs[0]); CommandBuffer.DispatchCompute(firstShader, kernel, blurGroupsX, blurGroupsY, blurGroupsZ); CommandBuffer.SetComputeVectorParam(ComputeShader, OffsetID, scissor.position); CommandBuffer.SetComputeVectorParam(ComputeShader, UVScaleID, GetShaderUVScale((int)blurableSize.x, (int)blurableSize.y)); texelBlur *= TexelPassDownScale; for (int pass = 1; pass < passes; ++pass) { computeBlurs.Add(new Vector4(-texelBlur.x, -texelBlur.y, texelBlur.x, texelBlur.y)); CommandBuffer.SetComputeVectorParam(ComputeShader, BlurID, computeBlurs[pass]); CommandBuffer.SetComputeTextureParam(ComputeShader, kernel, InputTextureID, tempTextureIDs[0]); CommandBuffer.SetComputeTextureParam(ComputeShader, kernel, OutputTextureID, tempTextureIDs[1]); CommandBuffer.DispatchCompute(ComputeShader, kernel, blurGroupsX, blurGroupsY, blurGroupsZ); Swap01(tempTextureIDs); texelBlur *= TexelPassDownScale; } for (int pass = passes - 2; pass >= 0; --pass) { CommandBuffer.SetComputeVectorParam(ComputeShader, BlurID, computeBlurs[pass]); CommandBuffer.SetComputeTextureParam(ComputeShader, kernel, InputTextureID, tempTextureIDs[0]); CommandBuffer.SetComputeTextureParam(ComputeShader, kernel, OutputTextureID, tempTextureIDs[1]); CommandBuffer.DispatchCompute(ComputeShader, kernel, blurGroupsX, blurGroupsY, blurGroupsZ); Swap01(tempTextureIDs); } } private void UpdateBlitCommands(int passes, Vector2 texelBlur, Vector4 csb, Vector2 blurableSize, Rect scissor, Texture inputTexture) { ColorAdjustmentMaterial.EnableKeyword(COLOR_ADJUSTMENT_ON); ColorAdjustmentMaterial.DisableKeyword(GRAIN_ON); Material blurMaterial = !IsDefaultColors(csb) ? ColorAdjustmentMaterial : BlurMaterials[0]; Vector4 clipUVs = GetClipUVs(scissor, blurableSize); blurMaterial.SetVector(BlurID, new Vector4(-texelBlur.x, -texelBlur.y, texelBlur.x, texelBlur.y)); BlurMaterials[0].SetVector(BlurID, new Vector4(-texelBlur.x, -texelBlur.y, texelBlur.x, texelBlur.y)); blurMaterial.SetVector(ColorAdjustmentID, csb); BlurMaterials[0].SetVector(ClipUVsID, clipUVs); blurMaterial.SetVector(ClipUVsID, clipUVs); CommandBuffer.Blit(inputTexture, tempTextureIDs[0], blurMaterial, BlurPass); texelBlur *= TexelPassDownScale; for (int pass = 1; pass < passes; ++pass) { blurMaterial = BlurMaterials[pass]; blurMaterial.SetVector(BlurID, new Vector4(-texelBlur.x, -texelBlur.y, texelBlur.x, texelBlur.y)); blurMaterial.SetVector(ClipUVsID, clipUVs); CommandBuffer.Blit(tempTextureIDs[0], tempTextureIDs[1], blurMaterial, BlurPass); Swap01(tempTextureIDs); texelBlur *= TexelPassDownScale; } for (int pass = passes - 2; pass >= 0; --pass) { CommandBuffer.Blit(tempTextureIDs[0], tempTextureIDs[1], BlurMaterials[pass], BlurPass); Swap01(tempTextureIDs); } } public void Unset(UIBlock2D uiBlock) { ReleaseOutputTexture(uiBlock); CommandBuffer.Clear(); prevParams = default; } public void Dispose() { DisposeCommandBuffer(); for (int i = 0; i < MaxPasses; ++i) { Destroy(blurMaterials[i]); blurMaterials[i] = null; } Destroy(colorAdjustmentMaterial); colorAdjustmentMaterial = null; Destroy(computeShader); computeShader = null; Destroy(colorAdjustmentComputeShader); colorAdjustmentComputeShader = null; } public void DisposeCommandBuffer() { if (commandBuffer == null) { return; } commandBuffer.Dispose(); commandBuffer = null; } private static void Swap01(T[] textures) { T temp = textures[0]; textures[0] = textures[1]; textures[1] = temp; } public static Vector2 GetUVScale(UIBlock2D uiBlock, Texture image) { ImageAdjustment imageAdjustment = uiBlock.ImageAdjustment; if (imageAdjustment.ScaleMode == ImageScaleMode.Manual) { return imageAdjustment.UVScale; } Vector2 imageSize = new Vector2(image.width, image.height); Vector2 uiBlockSize = uiBlock.CalculatedSize.XY.Value; float imageAspectRatio = imageSize.x / imageSize.y; float uiBlockAspectRatio = uiBlockSize.x / uiBlockSize.y; float relativeAspectRatio = imageAspectRatio / uiBlockAspectRatio; switch (imageAdjustment.ScaleMode) { case ImageScaleMode.Fit: return relativeAspectRatio > 1 ? new Vector2(1, 1 / relativeAspectRatio) : new Vector2(relativeAspectRatio, 1); case ImageScaleMode.Envelope: return relativeAspectRatio > 1 ? new Vector2(relativeAspectRatio, 1) : new Vector2(1, 1 / relativeAspectRatio); default: return Vector2.one; } } private static Vector4 GetShaderUVScale(Texture image) => GetShaderUVScale(image.width, image.height); private static Vector4 GetShaderUVScale(int width, int height) => new Vector4(1f / width, 1f / height, width, height); private static Rect GetScissorRect(Vector2 blurableSize, Rect viewportRect, Vector2 blurUVs) { float x = Mathf.Max(Mathf.FloorToInt((viewportRect.x - blurUVs.x) * blurableSize.x), 0); float y = Mathf.Max(Mathf.FloorToInt((viewportRect.y - blurUVs.y) * blurableSize.y), 0); float width = Mathf.Min(Mathf.CeilToInt((viewportRect.width + 2 * blurUVs.x) * blurableSize.x), blurableSize.x); float height = Mathf.Min(Mathf.CeilToInt((viewportRect.height + 2 * blurUVs.y) * blurableSize.y), blurableSize.y); return new Rect(x, y, width, height); } private Vector4 GetClipUVs(Rect scissor, Vector2 full) { Vector2 minUV = Vector2.Max(scissor.min, Vector2.zero) / full; Vector2 maxUV = Vector2.Min(scissor.max, full) / full; return new Vector4(minUV.x, minUV.y, maxUV.x, maxUV.y); } private static void Destroy(T obj) where T : UnityEngine.Object { if (obj == null) { return; } if (Application.isPlaying) { UnityEngine.Object.Destroy(obj); } else { UnityEngine.Object.DestroyImmediate(obj); } } private static bool IsDefaultColors(Vector4 csb) { return Mathf.Abs(csb.x - DefaultColors.x) < Epsilon && Mathf.Abs(csb.y - DefaultColors.y) < Epsilon && Mathf.Abs(csb.z - DefaultColors.z) < Epsilon && Mathf.Abs(csb.w - DefaultColors.w) < Epsilon; } private static bool ComputeShaderSupported => SystemInfo.supportsComputeShaders && !IsMobileOpenGLLinear; /// /// https://issuetracker.unity3d.com/issues/android-compute-shader-is-not-working-with-linear-color-space-when-it-is-built-to-android-device /// https://forum.unity.com/threads/compute-shaders-dont-work-on-android-with-linear-lighting.611314/ /// private static bool IsMobileOpenGLLinear => QualitySettings.activeColorSpace == ColorSpace.Linear && IsOpenGL(SystemInfo.graphicsDeviceType) && Application.isMobilePlatform; private static bool IsOpenGL(GraphicsDeviceType deviceType) => deviceType == GraphicsDeviceType.OpenGLES3 || deviceType == GraphicsDeviceType.OpenGLCore; private static bool UseLinearSpaceComputeShader => QualitySettings.activeColorSpace == ColorSpace.Linear && SystemInfo.graphicsDeviceType != GraphicsDeviceType.Metal; } internal static class TextureExtensions { public static Vector2 Size(this Texture texture) => new Vector2(texture.width, texture.height); } internal static class MaterialExtensions { public static void SetKeyword(this Material material, string keyword, bool enable) { if (enable) { material.EnableKeyword(keyword); } else { material.DisableKeyword(keyword); } } } }