美文网首页
Unity自定义SRP(十一):后处理

Unity自定义SRP(十一):后处理

作者: Dragon_boy | 来源:发表于2021-01-20 01:46 被阅读0次

    https://catlikecoding.com/unity/tutorials/custom-srp/post-processing/

    1 后处理效果栈

    ​ 一个渲染好的图像需要经过一些特殊的处理,使用许多不同的效果。通常的效果包括发光,颜色分级,景深,运动模糊和色调映射等,这些效果以栈的形式排列。

    1.1 设置

    ​ 为一个栈创建一个类用于存储相关设置:

    using UnityEngine;
    
    [CreateAssetMenu(menuName = "Rendering/Custom Post FX Settings")]
    public class PostFXSettings : ScriptableObject { }
    

    ​ 在CustomRenderPipelineAsset中添加配置选项:

        [SerializeField]
        PostFXSettings postFXSettings = default;
    
        protected override RenderPipeline CreatePipeline () 
        {
            return new CustomRenderPipeline(
                useDynamicBatching, useGPUInstancing, useSRPBatcher,
                useLightsPerObject, shadows, postFXSettings
            );
        }
    

    ​ 然后在CustomRenderPipeline中添加对应的参数和变量。

    1.2 栈对象

    ​ 创建一个栈类,追踪缓冲、渲染内容、摄像机和后处理效果设置等属性,并添加一个Setup方法初始化:

    using UnityEngine;
    using UnityEngine.Rendering;
    
    public class PostFXStack {
    
        const string bufferName = "Post FX";
    
        CommandBuffer buffer = new CommandBuffer {
            name = bufferName
        };
    
        ScriptableRenderContext context;
        
        Camera camera;
    
        PostFXSettings settings;
    
        public void Setup (
            ScriptableRenderContext context, Camera camera, PostFXSettings settings
        ) 
        {
            this.context = context;
            this.camera = camera;
            this.settings = settings;
        }
    }
    

    ​ 然后,添加一个全局属性来指示栈是否启用,存在对应的设置就启用:

        public bool IsActive => settings != null;
    

    ​ 添加一个Render方法来渲染栈。添加效果的方法是渲染一个四边形覆盖原始的图像。目前我们还没编写相应的shader,因此现在先暂时进行复制图像的操作,可以通过调用命令缓冲上的Blit方法来完成,参数为源图像id和目标图像id:

        public void Render (int sourceId) 
        {
            buffer.Blit(sourceId, BuiltinRenderTextureType.CameraTarget);
            context.ExecuteCommandBuffer(buffer);
            buffer.Clear();
        }
    

    1.3 使用栈

    CameraRenderer现在需要一个栈实例,并在Render中调用其Setup方法:

        Lighting lighting = new Lighting();
    
        PostFXStack postFXStack = new PostFXStack();
    
        public void Render (…) 
        {
            …
            lighting.Setup(
                context, cullingResults, shadowSettings, useLightsPerObject
            );
            postFXStack.Setup(context, camera, postFXSettings);
            buffer.EndSample(SampleName);
            Setup();
            …
        }
    

    ​ 我们需要一个中间纹理来让栈进行操作:

        static int frameBufferId = Shader.PropertyToID("_CameraFrameBuffer");
        
        …
        
        void Setup () 
        {
            context.SetupCameraProperties(camera);
            CameraClearFlags flags = camera.clearFlags;
    
            if (postFXStack.IsActive) 
            {
                buffer.GetTemporaryRT(
                    frameBufferId, camera.pixelWidth, camera.pixelHeight,
                    32, FilterMode.Bilinear, RenderTextureFormat.Default
                );
                buffer.SetRenderTarget(
                    frameBufferId,
                    RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store
                );
            }
    
            buffer.ClearRenderTarget(…);
            buffer.BeginSample(SampleName);
            ExecuteBuffer();
        }
    

    ​ 添加一个Cleanup方法来释放对应的纹理:

        void Cleanup () 
        {
            lighting.Cleanup();
            if (postFXStack.IsActive) 
            {
                buffer.ReleaseTemporaryRT(frameBufferId);
            }
        }
    

    1.4 强制清除

    ​ 当绘制到一个中间帧缓冲时,我们会将内容渲染到一个用任意数据填充的纹理,我们可以在帧调试器中看到。Unity确保在每一帧的开始帧缓冲器会清除帧缓冲,但我们因为渲染到了自己的纹理,回避了这一点,结果通常是会覆盖上一帧的结果,但这不能保证,但如果摄像机的清除标识设置为天空盒或某一颜色的话就能保证了。为了避免随机的结果,我么需要总是清除深度和颜色,除非使用天空盒:

            CameraClearFlags flags = camera.clearFlags;
    
            if (postFXStack.IsActive) 
            {
                if (flags > CameraClearFlags.Color) 
                {
                    flags = CameraClearFlags.Color;
                }
                …
            }
    
            buffer.ClearRenderTarget(…);
    

    1.5 Gizmos

    ​ 目前我们会同时绘制gizmo,不过它们本身有些会在应用后处理前绘制,有些在之后绘制,我们修改:

        partial void DrawGizmosBeforeFX ();
    
        partial void DrawGizmosAfterFX ();
        
        …
        
    #if UNITY_EDITOR
        
        …
                            
        partial void DrawGizmosBeforeFX () 
        {
            if (Handles.ShouldRenderGizmos()) 
            {
                context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
                //context.DrawGizmos(camera, GizmoSubset.PostImageEffects);
            }
        }
    
        partial void DrawGizmosAfterFX () 
        {
            if (Handles.ShouldRenderGizmos()) 
            {
                context.DrawGizmos(camera, GizmoSubset.PostImageEffects);
            }
        }
    

    ​ 在Render中绘制:

            //DrawGizmos();
            DrawGizmosBeforeFX();
            if (postFXStack.IsActive) 
            {
                postFXStack.Render(frameBufferId);
            }
            DrawGizmosAfterFX();
    

    1.6 自定义绘制

    Blit方法会绘制覆盖整个屏幕空间的四边形,即两个三角形,不过我们其实可以只绘制一个三角形来得到相同的效果,最大的好处是可以避免对角线上的片元重复渲染。

    ​ 添加PostFXStackPasses.hlsl,定义Varyings结构体,包含裁剪空间坐标和屏幕空间UV:

    #ifndef CUSTOM_POST_FX_PASSES_INCLUDED
    #define CUSTOM_POST_FX_PASSES_INCLUDED
    
    struct Varyings 
    {
        float4 positionCS : SV_POSITION;
        float2 screenUV : VAR_SCREEN_UV;
    };
    
    #endif
    

    ​ 然后,创建一个默认的顶点pass,使用顶点id作为参数,SV_VertexID为语义。X坐标为-1,-1,3,Y坐标为-1,3,-1,UV坐标的U为0,0,2,V为0,2,0:

    image
    Varyings DefaultPassVertex (uint vertexID : SV_VertexID) 
    {
        Varyings output;
        output.positionCS = float4(
            vertexID <= 1 ? -1.0 : 3.0,
            vertexID == 1 ? 3.0 : -1.0,
            0.0, 1.0
        );
        output.screenUV = float2(
            vertexID <= 1 ? 0.0 : 2.0,
            vertexID == 1 ? 2.0 : 0.0,
        );
        return output;
    }
    

    ​ 添加一个进行复制操作的片元pass:

    float4 CopyPassFragment (Varyings input) : SV_TARGET 
    {
        return float4(input.screenUV, 0.0, 1.0);
    }
    

    ​ 创建一个shader,关闭剔除,忽略深度写入,pass命名为Copy:

    Shader "Hidden/Custom RP/Post FX Stack" 
    {
        
        SubShader 
        {
            Cull Off
            ZTest Always
            ZWrite Off
            
            HLSLINCLUDE
            #include "../ShaderLibrary/Common.hlsl"
            #include "PostFXStackPasses.hlsl"
            ENDHLSL
    
            Pass 
            {
                Name "Copy"
                
                HLSLPROGRAM
                    #pragma target 3.5
                    #pragma vertex DefaultPassVertex
                    #pragma fragment CopyPassFragment
                ENDHLSL
            }
        }
    }
    

    ​ 在设置中手动连接shader:

    public class PostFXSettings : ScriptableObject 
    {
    
        [SerializeField]
        Shader shader = default;
    }
    

    ​ 我们需要创建对应的材质:

        Material material;
    
        public Material Material 
        {
            get 
            {
                if (material == null && shader != null) 
                {
                    material = new Material(shader);
                    material.hideFlags = HideFlags.HideAndDontSave;
                }
                return material;
            }
        }
    

    ​ 在PassFXStack中创建一个枚举,管理pass:

        enum Pass 
        {
            Copy
        }
    

    ​ 现在定义我们自己的绘制方法Draw,两个RenderTargetIdentifier变量作为参数,指示源和目标,以及一个pass参数。首先获取源纹理,然后将渲染目标设置为目标纹理,最后绘制三角形,调用DrawProcedural

        int fxSourceId = Shader.PropertyToID("_PostFXSource");
        
        …
        
        void Draw (
            RenderTargetIdentifier from, RenderTargetIdentifier to, Pass pass
        ) 
        {
            buffer.SetGlobalTexture(fxSourceId, from);
            buffer.SetRenderTarget(
                to, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store
            );
            buffer.DrawProcedural(
                Matrix4x4.identity, settings.Material, (int)pass,
                MeshTopology.Triangles, 3
            );
        }
    

    ​ 替换Blit方法:

            Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
    

    1.7 不总是应用后处理

    ​ 此时我们会发现几乎所有与材质相关的窗口都显示了后处理效果,甚至是反射探针。我们需要将后处理应用到合适的摄像机中,我们可以检测摄像机类型是否是game或scene,如果不是后处理设置为空:

            this.settings =
                camera.cameraType <= CameraType.SceneView ? settings : null;
    

    ​ 除此之外,我们也可以在scene窗口中有选择地开启后处理,我们针对PostFXStack创建一个partial类,创建一个ApplySceneViewState方法,在编辑器模式下,检测scene窗口是否关闭了后处理设置:

    using UnityEditor;
    using UnityEngine;
    
    partial class PostFXStack 
    {
    
        partial void ApplySceneViewState ();
    
    #if UNITY_EDITOR
    
        partial void ApplySceneViewState () 
        {
            if (
                camera.cameraType == CameraType.SceneView &&
                !SceneView.currentDrawingSceneView.sceneViewState.showImageEffects
            ) 
            {
                settings = null;
            }
        }
    
    #endif
    }
    

    PostFXStack.Setup末尾调用:

    public partial class PostFXStack 
    {
    
        …
    
        public void Setup (…) 
        {
            …
            ApplySceneViewState();
        }
    

    1.8 复制

    ​ 在shader中,我们往往需要获得源图像采样,使用线性限制采样器,我们添加一个GetSource方法:

    TEXTURE2D(_PostFXSource);
    SAMPLER(sampler_linear_clamp);
    
    float4 GetSource(float2 screenUV) 
    {
        return SAMPLE_TEXTURE2D(_PostFXSource, sampler_linear_clamp, screenUV);
    }
    
    float4 CopyPassFragment (Varyings input) : SV_TARGET 
    {
        return GetSource(input.screenUV);
    }
    

    ​ 注意有些API的UV原点的位置不同,我们可以通过_ProjectionParams的x组件获取:

    Varyings DefaultPassVertex (uint vertexID : SV_VertexID) 
    {
        …
        if (_ProjectionParams.x < 0.0)
        {
            output.screenUV.y = 1.0 - output.screenUV.y;
        }
        return output;
    }
    

    2 发光

    ​ 这里制作LDR发光。

    2.1 发光锥体

    ​ 我们可以模糊图像亮部来模拟发光。最简单的方法是复制一个尺寸为一半的纹理然后模糊,复制pass的每个样本在4个源像素间采样,重复此过程即可:

    image

    ​ 我们定义最大层级,16层:

        const int maxBloomPyramidLevels = 16;
    

    ​ 为追踪锥体中的纹理,我们定义纹理Id,以_BloomPyramid0_BloomPyramid1的顺序命名:

        int bloomPyramidId;
        
        …
        
        public PostFXStack () 
        {
            bloomPyramidId = Shader.PropertyToID("_BloomPyramid0");
            for (int i = 1; i < maxBloomPyramidLevels; i++) 
            {
                Shader.PropertyToID("_BloomPyramid" + i);
            }
        }
    

    ​ 然后创建一个DoBloom方法,一开始将纹理尺寸减半,并使用默认的渲染纹理格式:

        void DoBloom (int sourceId) 
        {
            buffer.BeginSample("Bloom");
            int width = camera.pixelWidth / 2, height = camera.pixelHeight / 2;
            RenderTextureFormat format = RenderTextureFormat.Default;
            int fromId = sourceId, toId = bloomPyramidId;
            buffer.EndSample("Bloom");
        }
    

    ​ 然后遍历所有的锥体级别,每次,检测是否还可生成子级别,如果可以创建一个新渲染纹理,复制到其上,设置新源,增加目标数,纹理尺寸减半:

            int fromId = sourceId, toId = bloomPyramidId;
    
            int i;
            for (i = 0; i < maxBloomPyramidLevels; i++) 
            {
                if (height < 1 || width < 1) 
                {
                    break;
                }
                buffer.GetTemporaryRT(
                    toId, width, height, 0, FilterMode.Bilinear, format
                );
                Draw(fromId, toId, Pass.Copy);
                fromId = toId;
                toId += 1;
                width /= 2;
                height /= 2;
            }
    

    ​ 遍历结束后,将结果复制到摄像机目标,然后反迭代释放资源:

            for (i = 0; i < maxBloomPyramidLevels; i++) { … }
    
            Draw(fromId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
    
            for (i -= 1; i >= 0; i--) 
            {
                buffer.ReleaseTemporaryRT(bloomPyramidId + i);
            }
            buffer.EndSample("Bloom");
    

    ​ 在Render中使用:

        public void Render (int sourceId) 
        {
            //Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
            DoBloom(sourceId);
            context.ExecuteCommandBuffer(buffer);
            buffer.Clear();
        }
    

    2.2 可配置发光

    ​ 我们可以提早结束锥体循环,可以限制迭代次数,也可以将迭代在某处终止。在PostFXSettings中添加BloomSettings结构体:

        [System.Serializable]
        public struct BloomSettings 
        {
    
            [Range(0f, 16f)]
            public int maxIterations;
    
            [Min(1f)]
            public int downscaleLimit;
        }
    
        [SerializeField]
        BloomSettings bloom = default;
    
        public BloomSettings Bloom => bloom;
    

    ​ 在DoBloom中使用:

            PostFXSettings.BloomSettings bloom = settings.Bloom;
            int width = camera.pixelWidth, height = camera.pixelHeight;
            RenderTextureFormat format = RenderTextureFormat.Default;
            int fromId = sourceId, toId = bloomPyramidId;
    
            int i;
            for (i = 0; i < bloom.maxIterations; i++, toId++) 
            {
                if (height < bloom.downscaleLimit || width < bloom.downscaleLimit) 
                {
                    break;
                }
                buffer.GetTemporaryRT(
                    toId, width, height, 0, FilterMode.Bilinear, format
                );
                Draw(fromId, toId, Pass.Copy);
                fromId = toId;
                width /= 2;
                height /= 2;
            }
    

    2.2 高斯滤波

    ​ 我们可以使用高斯滤波来改善上述模糊效果,应用9\times 9滤波器,将其分为水平pass和垂直pass。

    ​ 首先是水平pass,在PostFXStackPasses中创建BloomHorizontalPassFragment方法,它处理当前UV坐标的一行9个样本,因为我们进行了降采样操作,因此每次迭代的偏移即源纹素宽度的两倍:

    float4 _PostFXSource_TexelSize;
    
    float4 GetSourceTexelSize () 
    {
        return _PostFXSource_TexelSize;
    }
    
    …
    
    float4 BloomHorizontalPassFragment (Varyings input) : SV_TARGET 
    {
        float3 color = 0.0;
        float offsets[] = {
            -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0
        };
        float weights[] = {
            0.01621622, 0.05405405, 0.12162162, 0.19459459, 0.22702703,
            0.19459459, 0.12162162, 0.05405405, 0.01621622
        };
        for (int i = 0; i < 9; i++) 
        {
            float offset = offsets[i] * 2.0 * GetSourceTexelSize().x;
            color += GetSource(input.screenUV + float2(offset, 0.0)).rgb * weights[i];
        }
        return float4(color, 1.0);
    }
    

    ​ 权重值来源:对于一个9\times 9高斯滤波器,我们从杨辉三角获得第9行,得到1 8 28 56 70 56 28 8 1,不过这会让边缘的样本的贡献过低,因此我们获得第13行,裁剪边,得到66 220 495 792 924 792 495 220 66,和为4070,让权重和为1后就得到上述权重。

    ​ 在shader中添加Bloom HorizontalPass:

            Pass 
            {
                Name "Bloom Horizontal"
                
                HLSLPROGRAM
                    #pragma target 3.5
                    #pragma vertex DefaultPassVertex
                    #pragma fragment BloomHorizontalPassFragment
                ENDHLSL
            }
    

    ​ 添加到Pass枚举中。

    ​ 然后在DoBloom中应用:

                Draw(fromId, toId, Pass.BloomHorizontal);
    

    ​ 然后创建垂直passBloomVerticalPassFragment,修改一下水平Pass的内容即可:

    float4 BloomVerticalPassFragment (Varyings input) : SV_TARGET 
    {
        float3 color = 0.0;
        float offsets[] = {
            -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0
        };
        float weights[] = {
            0.01621622, 0.05405405, 0.12162162, 0.19459459, 0.22702703,
            0.19459459, 0.12162162, 0.05405405, 0.01621622
        };
        for (int i = 0; i < 9; i++) 
        {
            float offset = offsets[i] * GetSourceTexelSize().y;
            color += GetSource(input.screenUV + float2(0.0, offset)).rgb * weights[i];
        }
        return float4(color, 1.0);
    }
    

    ​ 我们需要额外的锥体级别来进行中间计算,每两个级别间加上一个中间级别:

        public PostFXStack () 
        {
            bloomPyramidId = Shader.PropertyToID("_BloomPyramid0");
            for (int i = 1; i < maxBloomPyramidLevels * 2; i++) 
            {
                Shader.PropertyToID("_BloomPyramid" + i);
            }
        }
    

    ​ 在DoBloom中,每次迭代时,首先为中间级别创建渲染纹理,然后对目标级别创建渲染纹理,接着将水平模糊的结果应用到中间级别渲染纹理,然后对中间的渲染纹理应用垂直模糊,将结果应用到目标纹理中:

    image
        void DoBloom (int sourceId) 
        {
            …
            int fromId = sourceId, toId = bloomPyramidId + 1;
            
            for (i = 0; i < bloom.maxIterations; i++) 
            {
                …
                int midId = toId - 1;
                buffer.GetTemporaryRT(
                    midId, width, height, 0, FilterMode.Bilinear, format
                );
                buffer.GetTemporaryRT(
                    toId, width, height, 0, FilterMode.Bilinear, format
                );
                Draw(fromId, midId, Pass.BloomHorizontal);
                Draw(midId, toId, Pass.BloomVertical);
                fromId = toId;
                toId += 2;
                …
            }
    
            Draw(fromId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
    
            for (i -= 1; i >= 0; i--) 
            {
                buffer.ReleaseTemporaryRT(fromId);
                buffer.ReleaseTemporaryRT(fromId - 1);
                fromId -= 2;
            }
            buffer.EndSample("Bloom");
        }
    

    ​ 我们可以使用二次线性滤波来减少每次高斯采样时采样点的次数,在垂直pass使用,因为水平Pass中我们已经使用降采样进行了一次二次线性滤波:

        float offsets[] = {
            -3.23076923, -1.38461538, 0.0, 1.38461538, 3.23076923
        };
        float weights[] = {
            0.07027027, 0.31621622, 0.22702703, 0.31621622, 0.07027027
        };
        for (int i = 0; i < 5; i++) 
        {
            float offset = offsets[i] * GetSourceTexelSize().y;
            color += GetSource(input.screenUV + float2(0.0, offset)).rgb * weights[i];
        }
    

    2.4 额外模糊

    ​ 使用发光锥体顶部的图片作为最终的图片并不能看出有什么东西在发光。我们可以反向增采样回去,得到一张纹理,以此得到想要的结果。

    image

    ​ 我们可以添加新的源纹理:

        int
            fxSourceId = Shader.PropertyToID("_PostFXSource"),
            fxSource2Id = Shader.PropertyToID("_PostFXSource2");
    

    ​ 在DoBloom中,最后不显示最终的图片,而是释放水平绘制最后一次迭代使用的纹理,并将目标设置为倒数第二张。

            buffer.ReleaseTemporaryRT(fromId - 1);
            toId -= 5;
    

    ​ 反向迭代,每次将每一届别的结果作为源,迭代结束后,使用源图像作为最后的目标:

            for (i -= 1; i > 0; i--) 
            {
                buffer.SetGlobalTexture(fxSource2Id, toId + 1);
                Draw(fromId, toId, Pass.Copy);
                buffer.ReleaseTemporaryRT(fromId);
                buffer.ReleaseTemporaryRT(toId + 1);
                fromId = toId;
                toId -= 2;
            }
    
            buffer.SetGlobalTexture(fxSource2Id, sourceId);
            Draw(fromId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
            buffer.ReleaseTemporaryRT(fromId);
            buffer.EndSample("Bloom");
    

    ​ 在shader中获得第二个源图像:

    TEXTURE2D(_PostFXSource);
    TEXTURE2D(_PostFXSource2);
    SAMPLER(sampler_linear_clamp);
    
    …
    
    float4 GetSource2(float2 screenUV) 
    {
        return SAMPLE_TEXTURE2D(_PostFXSource2, sampler_linear_clamp, screenUV);
    }
    

    ​ 添加一个BloomCombinePassFragmentpass,采样两张源图像,结合在一起:

    float4 BloomCombinePassFragment (Varyings input) : SV_TARGET 
    {
        float3 lowRes = GetSource(input.screenUV).rgb;
        float3 highRes = GetSource2(input.screenUV).rgb;
        return float4(lowRes + highRes, 1.0);
    }
    

    ​ 在增采样时应用:

            for (i -= 1; i > 0; i--) 
            {
                buffer.SetGlobalTexture(fxSource2Id, toId + 1);
                Draw(fromId, toId, Pass.BloomCombine);
                …
            }
    
            buffer.SetGlobalTexture(fxSource2Id, sourceId);
            Draw(
                bloomPyramidId, BuiltinRenderTextureType.CameraTarget,
                Pass.BloomCombine
            );
    

    ​ 注意,没有迭代的话,我们需要跳过增采样的代码,释放第一个水平pass使用的纹理:

            if (i > 1) 
            {
                buffer.ReleaseTemporaryRT(fromId - 1);
                toId -= 5;
                for (i -= 1; i > 0; i--) 
                {
                    …
                }
            }
            else 
            {
                buffer.ReleaseTemporaryRT(bloomPyramidId);
            }
    

    ​ 如果跳过发光操作的话,只执行复制操作:

            if (
                bloom.maxIterations == 0 ||
                height < bloom.downscaleLimit || width < bloom.downscaleLimit
            ) 
            {
                Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
                buffer.EndSample("Bloom");
                return;
            }
    

    2.5 双三次增采样

    ​ 尽管高斯滤波得到平滑的结果,但我们在增采样时仍使用了二次线性滤波,这样结果不大行,我们可以切换为双三次滤波,使用Filtering.hlsl中的SampleTexture2DBicubic得到对应的采样结果:

    float4 GetSourceBicubic (float2 screenUV) 
    {
        return SampleTexture2DBicubic(
            TEXTURE2D_ARGS(_PostFXSource, sampler_linear_clamp), screenUV,
            _PostFXSource_TexelSize.zwxy, 1.0, 0.0
        );
    }
    

    ​ 我们可为该方式增添选项:

    bool _BloomBicubicUpsampling;
    
    float4 BloomCombinePassFragment (Varyings input) : SV_TARGET 
    {
        float3 lowRes;
        if (_BloomBicubicUpsampling) 
        {
            lowRes = GetSourceBicubic(input.screenUV).rgb;
        }
        else 
        {
            lowRes = GetSource(input.screenUV).rgb;
        }
        float3 highRes = GetSource2(input.screenUV).rgb;
        return float4(lowRes + highRes, 1.0);
    }
    

    ​ 在PostFXSettings.BloomSettings中添加:

            public bool bicubicUpsampling;
    

    ​ 在DoBloom增采样前应用:

        int
            bloomBucibicUpsamplingId = Shader.PropertyToID("_BloomBicubicUpsampling"),
            fxSourceId = Shader.PropertyToID("_PostFXSource"),
            fxSource2Id = Shader.PropertyToID("_PostFXSource2");
        
        …
        
        void DoBloom (int sourceId) 
        {
            …
            
            buffer.SetGlobalFloat(
                bloomBucibicUpsamplingId, bloom.bicubicUpsampling ? 1f : 0f
            );
            if (i > 1) { … }
            …
        }
    

    2.6 分辨率减半

    ​ 生成发光想过耗费时间挺多的,我们可以减半分辨率。

    ​ 首先,将降采样限制翻倍:

            if (
                bloom.maxIterations == 0 ||
                height < bloom.downscaleLimit * 2 || width < bloom.downscaleLimit * 2
            ) 
            {
                Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
                buffer.EndSample("Bloom");
                return;
            }
    

    ​ 然后,声明一张半尺寸的纹理,在一开始使用:

        int
            bloomPrefilterId = Shader.PropertyToID("_BloomPrefilter"),
    

    ​ 在DoBloom中,将源纹理内容复制到这张半尺寸纹理中,同样宽高减半:

            RenderTextureFormat format = RenderTextureFormat.Default;
            buffer.GetTemporaryRT(
                bloomPrefilterId, width, height, 0, FilterMode.Bilinear, format
            );
            Draw(sourceId, bloomPrefilterId, Pass.Copy);
            width /= 2;
            height /= 2;
    
            int fromId = bloomPrefilterId, toId = bloomPyramidId + 1;
            int i;
            for (i = 0; i < bloom.maxIterations; i++) 
            {
                …
            }
    
            buffer.ReleaseTemporaryRT(bloomPrefilterId);
    

    2.7 阈值

    ​ 我们可用阈值控制发光区域,我们将颜色乘以权重w = \frac {max(0,b-t)} {max(b,0.00001)},其中b是亮度,t是可配置的阈值,我们将颜色的通道中最大值赋予b,不过权重会在某处到达0,我们进行改进,w = \frac {max(s,b-t)} {max(b,0.00001)}s=\frac {min(max(0,b-t+tk),2tk)^2} {4tk+0.00001},其中k在0-1之间。

    ​ 在BloomSettings中添加属性:

            [Min(0f)]
            public float threshold;
    
            [Range(0f, 1f)]
            public float thresholdKnee;
    

    ​ 在DoBloom中预计算:

            Vector4 threshold;
            threshold.x = Mathf.GammaToLinearSpace(bloom.threshold);
            threshold.y = threshold.x * bloom.thresholdKnee;
            threshold.z = 2f * threshold.y;
            threshold.w = 0.25f / (threshold.y + 0.00001f);
            threshold.y -= threshold.x;
            buffer.SetGlobalVector(bloomThresholdId, threshold);
    
            RenderTextureFormat format = RenderTextureFormat.Default;
            buffer.GetTemporaryRT(
                bloomPrefilterId, width, height, 0, FilterMode.Bilinear, format
            );
            Draw(sourceId, bloomPrefilterId, Pass.BloomPrefilter);
    

    ​ shader中添加对应的函数:

    float4 _BloomThreshold;
    
    float3 ApplyBloomThreshold (float3 color) {
        float brightness = Max3(color.r, color.g, color.b);
        float soft = brightness + _BloomThreshold.y;
        soft = clamp(soft, 0.0, _BloomThreshold.z);
        soft = soft * soft * _BloomThreshold.w;
        float contribution = max(soft, brightness - _BloomThreshold.x);
        contribution /= max(brightness, 0.00001);
        return color * contribution;
    }
    
    float4 BloomPrefilterPassFragment (Varyings input) : SV_TARGET {
        float3 color = ApplyBloomThreshold(GetSource(input.screenUV).rgb);
        return float4(color, 1.0);
    }
    

    2.8 强度

    ​ 我们还可以控制发光的强度:

            [Min(0f)]
            public float intensity;
    

    DoBloom中,如为0则跳过:

            if (
                bloom.maxIterations == 0 || bloom.intensity <= 0f ||
                height < bloom.downscaleLimit * 2 || width < bloom.downscaleLimit * 2
            )
            {
                Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
                buffer.EndSample("Bloom");
                return;
            }
    

    ​ 不是0则传到GPU中:

            buffer.SetGlobalFloat(bloomIntensityId, 1f);
            if (i > 1) 
            {
                …
            }
            else 
            {
                buffer.ReleaseTemporaryRT(bloomPyramidId);
            }
            buffer.SetGlobalFloat(bloomIntensityId, bloom.intensity);
            buffer.SetGlobalTexture(fxSource2Id, sourceId);
            Draw(fromId, BuiltinRenderTextureType.CameraTarget, Pass.BloomCombine);
    

    ​ 然后在shader中应用即可:

    bool _BloomBicubicUpsampling;
    float _BloomIntensity;
    
    float4 BloomCombinePassFragment (Varyings input) : SV_TARGET 
    {
        …
        return float4(lowRes * _BloomIntensity + highRes, 1.0);
    }
    

    相关文章

      网友评论

          本文标题:Unity自定义SRP(十一):后处理

          本文链接:https://www.haomeiwen.com/subject/nqehzktx.html