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

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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容