本篇总结Unity中PostProcessing组件的使用

参考资料
Unity-PostProcessing官方Wiki
了解MSAA处理的阶段:深入剖析MSAA

普通-安装/升级:包管理器中的post-processing;安装某个SRP版本后将自动包含此项;
Github安装:卸载已有的post-processing,克隆项目到项目的"Assets"目录;
使用:为相机添加Post Process Layer组件;为指定layer添加Post Process Volume组件;
各种可能的需求:运行时开启某个效果一段时间/设置效果参数;自定义新效果;
为什么用PostProcessing V2:避免重复造轮子,官方已经实现了一些有算法要求的效果;我们自己写自定义效果的话可以考虑写灰度图(特殊滤镜)、老电影(特殊噪音)、雾效/小星星(特殊遮罩)
学习后处理的意义:补完渲染流水线;这部分就算我们不刻意使用,Unity也会使用抗锯齿、HDR等效果

案例1:灰屏

情景:游戏过程中人物死亡时触发,通过1秒的过渡时间完成全屏灰屏效果;
当人物死亡时,调用人物的OnDead()方法,其中开启GrayScale效果,在1秒钟内将混合值从0渐变至1;
如果玩家点击了复活按钮,则取消GrayScale效果,在1秒钟内将混合值从1渐变至0;
实现:动画逻辑部分不是本篇讨论的重点,要实现动画效果可以在Update中lerp、协程、使用动画插件DOTween;
此类实现依赖于基于时间线的任务逻辑,需要实现添加动画任务、获取任务完成回执等功能;

效果部分和built in一样也是用command buffer,使用HLSL语法和LWRP提供的类来操作;
built in的代码经过简单修改也能在SRP中使用;
Shader:

Shader "Hidden/Custom/Grayscale"
{
    HLSLINCLUDE
        #include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"
        TEXTURE2D_SAMPLER2D(_MainTex, sampler_MainTex);
        float _Blend;
        float4 Frag(VaryingsDefault i) : SV_Target
        {
            float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.texcoord);
            float luminance = dot(color.rgb, float3(0.2126729, 0.7151522, 0.0721750));
            color.rgb = lerp(color.rgb, luminance.xxx, _Blend.xxx);
            return color;
        }
    ENDHLSL

    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Pass
        {
            HLSLPROGRAM
                #pragma vertex VertDefault
                #pragma fragment Frag
            ENDHLSL
        }
    }
}

这是官方给的案例;
RenderState:由于后处理shader的特殊性,剔除/深度写入/各种测试等都需要避免;
vertex函数的逻辑隐藏在StdLib.hlsl文件中,挖出来不算困难:
vertex/POSITION:输入为float3类型,实际上只会利用到xy两个值;参考下方对BlitFullscreenTriangle的解释;
vertex/SV_POSITION:这里也是只会利用到xy值;w分量为1,保证了NDC坐标值保持不变;z值固定为0,相当于取消了深度映射关系;
texcoord/TEXCOORD0:将值域为-1~1的NDC坐标转换到值域0~1,作为贴图采样用的uv;
texcoordStereo/TEXCOORD1:VR模式用uv;
SAMPLE_TEXTURE2D:和tex2D应该本质上是一样的,就是sample;
luminance:dot(颜色,灰度vect),得到像素的灰度值;luminance.xxx构成灰度图;
_Blend.xxx:混合度,使最终颜色在原图和灰度图之间进行差值;
alpha:选择性保留context.source的alpha值,这个值对FXAA有用;

C#部分:
LWRP中使用PostProcess插件进行后处理,C#部分中新建的效果需要继承PostProcessEffectRenderer<\T>和覆盖Render方法;

using System;
using UnityEngine;
using UnityEngine.Rendering.PostProcessing;

[Serializable]
[PostProcess(typeof(GrayscaleRenderer), PostProcessEvent.AfterStack, "Custom/Grayscale")]
public sealed class Grayscale : PostProcessEffectSettings
{
    [Range(0f, 1f), Tooltip("Grayscale effect intensity.")]
    public FloatParameter blend = new FloatParameter { value = 0.5F };

    public override bool IsEnabledAndSupported(PostProcessRenderContext context)
    {
        return enabled.value && blend.value > 0f;
    }
}

public sealed class GrayscaleRenderer : PostProcessEffectRenderer<Grayscale>
{
    public override void Render(PostProcessRenderContext context)
    {
        var sheet = context.propertySheets.Get(Shader.Find("Hidden/Custom/Grayscale"));
        sheet.properties.SetFloat("_Blend", settings.blend);
        context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
    }
}

注:这里内部封装的东西很多,我们需要知道有哪些函数是可以重写的;
参考:
关于可用的属性覆盖:/PostProcessing/Runtime/ParameterOverride.cs
注:后处理用的shader需要在Edit -> Project Settings -> Graphics中添加为Always Included Shaders

sheet:获取shader的PropertySheet,可用于管理材质属性和关键字;
SetFloat:将blend绑定到材质属性_Blend,这里blend是FloatParameter类型(引用类型),区别于float;对blend.value进行赋值时会更新值的地址,使值改动能实时更新;
settings:每个PostProcessEffectRenderer<\T>实例都绑定了一个T实例,可用于重载参数
T:用于绑定材质属性、设置介入时间点、设置效果处理顺序、编辑模式菜单地址;

PostProcessEvent
BeforeTransparent:在执行完Queue为不透明分类的渲染任务后执行
BeforeStack:在Bloom、Vignette等built-in流程前操作
AfterStack:在built-in流程后操作,在FXAA、Dithering前操作;
参考资料
/PostProcessing/Runtime/PostProcessLayer.cs 第962行附近

IsEnabledAndSupported:重写此方法可用于在特定条件下自动关闭效果

BlitFullscreenTriangle:提交Darwcall的关键函数,有很多重载;context.source和context.destination指定上下文中的输入和输出;pass为0表示使用shader中的index为0的pass;sheet表示了shader和相关的属性、关键字;

总结:在本例中,提交Drawcall之前唯一需要做的就是绑定材质的_Blend属性,便于实时调整渐变效果。

案例2:Bloom

Bloom效果是现代游戏最常用的后处理效果之一,人称光污染;
相对案例1:灰屏来说,直接学习Bloom有非常大的难度跨越,但是对快速掌握PostProcess好处很多;
Bloom是Unity自带的效果,所以我们需要在源代码中将Bloom相关的逻辑都抛出来;
参考资料
Unity中的PostFX v2
Bloom 辉光 后期处理系列2
Shader:/PostProcessing/Shaders/Builtins/Bloom.shader
关联库:StdLib.hlsl Colors.hlsl Sampling.hlsl
Bloom面板:
C#执行入口:/PostProcessing/Runtime/PostProcessLayer.cs
Effect初始化:/PostProcessing/Runtime/PostProcessBundle.cs
C#效果逻辑:/PostProcessing/Runtime/Effects/Bloom.cs
编辑器逻辑:/PostProcessing/Editor/Effects/BloomEditor.cs

C#执行入口:

if (hasBeforeStackEffects) lastTarget = RenderInjectionPoint(PostProcessEvent.BeforeStack, context, "BeforeStack", lastTarget);
lastTarget = RenderBuiltins(context, !needsFinalPass, lastTarget, eye);
if (hasAfterStackEffects) lastTarget = RenderInjectionPoint(PostProcessEvent.AfterStack, context, "AfterStack", lastTarget);
if (needsFinalPass) RenderFinalPass(context, lastTarget, eye);

先判断是否满足条件,再执行对应的渲染队列任务;
对于用户自定义的后处理效果,会使用RenderInjectionPoint方法;
对于内置的后处理效果,会使用RenderBuiltins方法;
两个方法都会判断IsEnabledAndSupported和Scene视图显示条件,所以重写IsEnabledAndSupported可用于动态关闭效果;
两种模式中,单个效果都会被序列化为PostProcessBundle实例,在PostProcessBundle初始化时绑定一个PostProcessAttribute实例、一个PostProcessEffectSettings实例、一个PostProcessEffectRenderer实例;
而这3个实例,正是我们自定义效果时需要继承或覆盖的内容,可以参考这3个C#文件内容;

C#效果逻辑:
Init

public override void Init()
{
    m_Pyramid = new Level[k_MaxPyramidSize];
    for (int i = 0; i < k_MaxPyramidSize; i++)
   {
       m_Pyramid[i] = new Level
       {
           down = Shader.PropertyToID("_BloomMipDown" + i),
           up = Shader.PropertyToID("_BloomMipUp" + i)
       };
    }
}

Init方法在PostProcessBundle.renderer首次被访问时执行,可用于初始化实例;
internal关键词限制方法只能在程序集内使用,这里指的是PostProcessing程序集;

生成长度为16的Level数组;Shader.PropertyToID方法用于保存属性ID,用于set时代替字符串;
额外补充下MaterialPropertyBlock的用法,此方法是后处理系统唯一的材质赋值方法:

MaterialPropertyBlock props = new MaterialPropertyBlock();
int colorID = Shader.PropertyToID("_Color");
props.SetColor(colorID, new Color(r, g, b, 1));
GetComponent<Renderer>().SetPropertyBlock(props);

使用MaterialPropertyBlock实例代替Material实例重写材质属性;
这里的属性ID的数值在每次运行时不是确定的,但是是唯一的,不可序列化存储、网络传输;
down和up保存了Shader中声明的属性的ID,这部分是性能方面的优化,不是核心逻辑;

Render逻辑:
这部分的代码很长,130多行,就不贴出来了;

var cmd = context.command;
cmd.BeginSample("BloomPyramid");

cmd的定义需要参考PostProcessRenderContext.cs,command为CommandBuffer实例;
PostProcessRenderContext的域名为UnityEngine.Rendering.PostProcessing,是UnityEngine.Rendering的子域名,所以自动包含了UnityEngine.Rendering程序集,这部分的代码是不开源的;

BeginSample:Begin profiling a piece of code with a custom label.
字符串BloomPyramid用于标记目录节点,可在Profiler窗口查看(CPU-Hierarchy模式搜索名字即可);
BloomPyramid的上级有BuiltinStack、Render PostProcessing Effects等
Profiler窗口:
排序模式:Hierarchy视图上方有黑色箭头的那一列;
Self ms/Self:函数自己的耗时(时间占比);
Time ms/Total:函数自己和它调用的其他函数的耗时总和(时间占比),可作为性能参考依据;
Calls:函数被调用的次数/帧;
层级关系:cmd.BeginSample/cmd.EndSample定义层级目录;

context.resources.shaders.bloom
这里resources是一个反序列化对象:/PostProcessing/PostProcessResources.asset
可以在其面板中观察赋值情况,这里shaders.bloom对应的文件在/PostProcessing/Shaders/Builtins下
这样做的好处是保存了Shader实例的地址,实际上我们自己定义一个shader实例也是可以的:
Shader bloom = Shader.Find("Sekia/PostProcess/CustomBloom");

ShaderIDs.AutoExposureTex:参考ShaderIDs.cs,这里表示"_AutoExposureTex";
context.autoExposureTexture:默认为白色贴图,参考PostProcessLayer.cs 1090行;
这样做的目的是实现曝光强度调节,白色贴图表示默认曝光,我们可以在shader中设置属性默认值代替这个操作:
Properties
{
[NoScaleOffset]_AutoExposureTex ("_AutoExposureTex", 2D) = "white" {}
}

anamorphicRatio
Mathf.Clamp(settings.anamorphicRatio, -1, 1);
Mathf.Clamp(x,a,b):当x<a时,返回a;当x>b时,返回b;else,返回x;
anamorphicRatio:通过垂直(范围[-1,0])或水平(范围[0,1])缩放bloom来改变Bloom在屏幕上的扩散方向。

tw/th
Prefilter/DownSample贴图的像素宽度和像素高度,这个贴图的具体效果需要阅读bloom shader的第1、第2个Pass;
tw_stereo:带stereo关键词的都是与VR有关的,可以选择性忽略;

iterations/sampleScale
iterations是迭代次数,每有一次迭代就有一张尺寸更小的Prefilter/DownSample贴图;

int s = Mathf.Max(tw, th);
float logs = Mathf.Log(s, 2f) + Mathf.Min(settings.diffusion.value, 10f) - 10f;
int logs_i = Mathf.FloorToInt(logs);
int iterations = Mathf.Clamp(logs_i, 1, k_MaxPyramidSize);

迭代次数与tw、th、diffusion有直接关系,当tw或th大于1024时,Mathf.Log(s, 2f)等于10;
这样实现了根据屏幕的分辨率控制迭代次数;sampleScale的作用需要阅读bloom shader的第5、6个Pass;

threshold:过滤,低于此亮度的像素没有bloom效果;
softKnee:柔和,指定threshold边界线的柔和区域范围;
clamp:参考bloom shader的第1、2个Pass;

fastMode
在bloom shader中,准备了6个pass,Prefilter/Downsample/Upsample类型的Pass各两个;
设置fastMode为true时,qualityOffset返回1,改变blit命令使用的pass;
同类型的Pass中,index+1的Pass计算量更小,用于移动版本优化;

GetTemporaryRT
类似于RenderTexture.GetTemporary;
从池中生成RenderTexture并绑定一个RenderTargetIdentifier对象,不会自动清理已有图像内容;

CommandBuffer.BlitFullscreenTriangle
这个是后处理组件的扩展方法,可以查看源代码,RuntimeUtilities.cs;

public static void BlitFullscreenTriangle(this CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, bool clear = false, Rect? viewport = null)
{
    cmd.SetGlobalTexture(ShaderIDs.MainTex, source);
    LoadAction loadAction = viewport == null ? LoadAction.DontCare : LoadAction.Load;
    if (clear)
    {
        cmd.SetRenderTargetWithLoadStoreAction(destination, loadAction, StoreAction.Store, depth, loadAction, StoreAction.Store);
        cmd.ClearRenderTarget(true, true, Color.clear);
    }
    else
    {
        cmd.SetRenderTargetWithLoadStoreAction(destination, loadAction, StoreAction.Store, depth, LoadAction.Load, StoreAction.Store);
    }
    if (viewport != null)
        cmd.SetViewport(viewport.Value);
    cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, propertySheet.material, 0, pass, propertySheet.properties);
}

最终调用的是DrawMesh,mesh是fullscreenTriangle,其定义为:

static Mesh s_FullscreenTriangle;
public static Mesh fullscreenTriangle
{
    get
    {
        if (s_FullscreenTriangle != null)
        return s_FullscreenTriangle;
        s_FullscreenTriangle = new Mesh { name = "Fullscreen Triangle" };
        s_FullscreenTriangle.SetVertices(new List<Vector3>
        {
            new Vector3(-1f, -1f, 0f),
            new Vector3(-1f,  3f, 0f),
            new Vector3( 3f, -1f, 0f)
        });
        s_FullscreenTriangle.SetIndices(new [] { 0, 1, 2 }, MeshTopology.Triangles, 0, false);
        s_FullscreenTriangle.UploadMeshData(false);
        return s_FullscreenTriangle;
    }
}

用代码创建了一个mesh,这个mesh有3个顶点,SetIndices设置mesh的Triangles面列表,3个顶点序列表示1个面;
所以这个mesh的3个顶点刚好能覆盖NDC坐标系上[-1,1]的值域,其转换到世界空间用的是单位矩阵Matrix4x4.identity;
mesh的顶点位置会被投影到屏幕平面上,参考StdLib.hlsl中的VertDefault函数,vertex/SV_POSITION仅使用顶点的xy值;

RenderTexture/RenderTargetIdentifier/int
RenderTexture就是一张特殊的图片,可以将其转换为png:

RenderTexture renderTexture = RenderTexture.GetTemporary(context.screenWidth, context.screenHeight, 0, context.sourceFormat);
cmd.CopyTexture(context.destination, renderTexture);
renderTexture.Sekia_SaveRenderTextureToPNG();
renderTexture.Release();

Sekia_SaveRenderTextureToPNG的处理代码:

public static bool Sekia_SaveRenderTextureToPNG(this RenderTexture rt, string contents = "D:\\", string pngName = "test")
{
    RenderTexture prev = RenderTexture.active;
    RenderTexture.active = rt;
    Texture2D png = new Texture2D(rt.width, rt.height, TextureFormat.ARGB32, false);
    png.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
    if (!Directory.Exists(contents))
        Directory.CreateDirectory(contents);
    System.IO.File.WriteAllBytes(contents + "/" + pngName + ".png", png.EncodeToPNG());
    Texture2D.DestroyImmediate(png);
    png = null;
    RenderTexture.active = prev;
    return true;
}

RenderTexture中还可能包含了深度缓冲、模板缓冲等值,具体怎么取出来欢迎留言;
RenderTargetIdentifier:这是一个关联用实例,可以由Texture、int等生成,但是不能直接取出里面的RenderTexture;
所以,当我们要为shader的一个贴图属性赋值时,就不能Sheet.properties.SetTexture,而应该用cmd.SetGlobalTexture;
因为:Unity封装了这部分方法,在外部的dll不能访问这些属性,可以理解成cmd里面有一个图片词典,可以按名称查找;
代码解读到这里,就完成了Prefilter和Downsample的步骤,需要仔细阅读bloom shader才能搞懂贴图里具体操作了什么;

Bloom Shader
经过以上代码层面的分析,我们对提交DrawCall有了更为细节的理解,我们再回头看VertDefault;

VaryingsDefault VertDefault(AttributesDefault v)
{
    VaryingsDefault o;
    o.vertex = float4(v.vertex.xy, 0.0, 1.0);
    o.texcoord = TransformTriangleVertexToUV(v.vertex.xy);

#if UNITY_UV_STARTS_AT_TOP
    o.texcoord = o.texcoord * float2(1.0, -1.0) + float2(0.0, 1.0);
#endif

    o.texcoordStereo = TransformStereoScreenSpaceTex(o.texcoord, 1.0);

    return o;
}

因为mesh只有3个顶点,所以这个逐顶点的vertex函数会被执行3次;
计算texcoordStereo会有点浪费差值寄存器空间,不需要的话建议手动重写此函数;
因为vertex函数中没有使用任何空间转换矩阵,相当于其使用的是模型空间;

UV坐标方面,UNITY_UV_STARTS_AT_TOP实现了根据平台改变UV坐标系,因为这里的UV不是来自于顶点数据v.texcoord,而是根据模型空间坐标转换的,需要手动设置坐标系;
在uber shader中这个转换公式在C#中指定:
uberSheet.properties.SetVector(UVTransform, SystemInfo.graphicsUVStartsAtTop ? new Vector4(1.0f, -1.0f, 0.0f, 1.0f) : new Vector4(1.0f, 1.0f, 0.0f, 0.0f));
这里_UVTransform是StdLib.hsls中,另一个vertex函数模板VertUVTransform中的参数;
可以自己重写vertex函数;

UnityStereoAdjustedTexelSize
参数为_MainTex_TexelSize,这个参数由Unity暗中操作,xyzw分别是1/width、1/height、width、height;
定义于xRLib.hlsl,定义UNITY_SINGLE_PASS_STEREO后缩放纹理宽度和高度,没有定义时不处理;

TEXTURE2D_PARAM
这个宏每个平台都定义了一次,拿D3D11来说,参考D3D11.hlsl:
#define TEXTURE2D_PARAM(textureName, samplerName) textureName, samplerName
对于D3D9、OpenGL、PSP2等平台,结果里只有textureName;

UnityStereoTransformScreenSpaceTex
定义了UNITY_SINGLE_PASS_STEREO时对参数uv的xy值进行缩放和平移,反之不变;

DownsampleBox13Tap
对于Win10电脑,非VR项目来说,相当于;
DownsampleBox13Tap(_MainTex, sampler_MainTex, i.uv, _MainTex_TexelSize.xy);
DownsampleBox13Tap定义于Sampling.hlsl,TEXTURE2D_ARGS宏作用与TEXTURE2D_PARAM一致;
对uv周围一像素宽的范围内,总计采样了13次:
1像素距离的田字型范围采样8个点,0.5像素距离的田字型范围采样4个点,中心1个点;
1距离/0.5距离/0距离的点占最终输出权重分别为1/8、1/2、3/8;权重合计为1;

SafeHDR:把颜色控制在HDR范围内;
half3 SafeHDR(half3 c) { return min(c, 65504); }
half4 SafeHDR(half4 c) { return min(c, 65504); }

Prefilter函数
采样了自动曝光贴图,这里设置为白色,只能采样到1;
color = min(_Params.x, color);
这里的_Params也就是Bloom面板上的Clamp值,作用类似于SafeHDR,保留一个即可;
color = QuadraticThreshold(color, _Threshold.x, _Threshold.yzw);
xyzw值举例:1,1-(1x0.5+0.007),(1x0.5+0.007)x2,1 / 4 / (1x0.5+0.007)
也就是:1,0.493,1.014,0.493;第一个参数用于限制效果,后三个参数用于柔和;

QuadraticThreshold定义于Colors.hlsl,用于控制最低有效bloom亮度和其柔和效果
half br = Max3(color.r, color.g, color.b); //返回最亮的通道
half rq = clamp(br - curve.x, 0.0, curve.y); //获取柔和区域的范围 x为下限 x+y为上限
rq = curve.z * rq * rq; //rq越接近于最大柔和半径时,返回的值越接近1/4;这是一个正值;
color *= max(rq, br - threshold) / max(br, EPSILON);
EPSILON定义域StdLib.hlsl:#define EPSILON 1.0e-4 //等于0.18,可避免分母值过低;
可以理解成在水平轴x上,有点A、B、C、D,分别A和C关于B点对称,分别表示柔和下限、阀值、上限,D表示亮度;
当D在B以上时,直接返回(br - threshold)/br;正常裁剪
当D在B以下时,直接返回rq/br; 有微弱的亮度,随着br的变小结果也变小;

Downsample
到此Prefilter类型的两个Pass作用已经很明显,像素范围采样+过滤;
后续的Downsample类型的两个Pass和Prefilter的区别是没有过滤这个步骤;
那么,在1个像素周围采样多次的目的究竟是什么呢,参考C#中的代码:
cmd.BlitFullscreenTriangle(lastDown, mipDown, sheet, pass);
提交的DrawCall中,前后RenderTexture的宽高不一样;
context.source会作为_MainTex被赋值,而contex.destination才是真正的"屏幕尺寸";
渲染流水线的屏幕映射阶段,需要将上面说的3个顶点,映射到屏幕空间,生成像素坐标和深度信息;
而像素坐标则是根据指定的RenderTexture来决定宽度和高度;
在回来看DownsampleBox13Tap函数,这里采样用的uv是逐像素进行的,使用的是mipDown的高宽;
此时两个像素之间的间隔相当于原来的两倍,差值后传递给fragment函数的uv值也是如此;
降采样的过程实际上是:Blit提交DrawCall,目标RenderTexture尺寸减半时的处理方法;

如果RenderTexture尺寸不是严格减半呢?
anamorphicRatio属性可以控制目标mipDown的高宽,其默认为0,使rw/rh为0;
当anamorphicRatio不为0时,mipDown就不是严格减半缩小的,有一边会变宽;
变宽的那一边在屏幕映射阶段有更多的像素,像素间隔变短了,像素范围采样用UV:
uv + texelSize * float2(-1.0, 1.0))
举例:mipDown相对source宽高为(1/2,1),或者(1/3,1/2);
在不同缩放比下可考虑用对应的降采样UV算法;
anamorphicRatio为1时,mipDown的高度缩放为1,使垂直方向上1个点的颜色混合上下2个像素;
到目前为止mipDown缩放对bloom方向的影响还看不出来,后续继续讨论;

Upsample
在C#代码中可以看到mipDown被赋值给BloomTex,lastUp作为MainTex;
mipDown和mipUp属于同一个迭代,lastUp属于下一级迭代;

UpsampleTent
在win10环境下非VR开发环境,这里的代码相当于:
UpsampleTent(_MainTex, sampler_MainTex, i.uv, _MainTex_TexelSize.xy, _SampleScale);
这个方法声明在Sampling.hlsl,对应目标RenderTexture比MainTex高宽更大的情况;
如果是严格的双倍放大,大么目标RenderTexture中的1个像素宽度相当于MainTex中的1/2;
SampleScale参数参考C#代码;其值与tw/th/diffusion等参数有关;
假设s是1024,diffusion为7,那么logs等于7,logs_i等于7,sampleScale等于0.5;
如果log(s,2f)值为7.99999,那么logs等于7.99999,logs_i等于7,sampleScale等于1.49999;
以上两个假设代表了sampleScale值的两种极端情况,sampleScale可以看作s离下一个2的整数幂的程度;
UpsampleTent的采样思路是:以UV坐标周围1个单位距离的位置采样9次,这个距离受到sampleScale缩放;
9次采样中,中心1个点/次周围4个点/外围4个点分别占权重1/4、1/2、1/4;
这里对sampleScale的控制我没有理解;
对于升采样操作(Upsample)我觉得普通的采样操作即可,不存在丢失图片信息的担忧,修改代码后也是如此;

Combine
合并MainTex(last)和BloomTex(mipDown)的颜色到目标RenderTexture(mipUp);
对于最后一次迭代的lastUp来说,其高宽可能只有8像素左右;最后一次迭代的up类型的贴图没有使用过;
Combline操作相加的两个对象为本次迭代的mipDown和下一级的mipDown;
比如,将8像素宽度的Maintex和16像素宽度的BloomTex的采样值相加;
也就是双倍亮度了;也加入了低迭代级别贴图的模糊效果;
在shader代码中,尝试修改返回结果,只返回一种颜色,会有奇怪的bloom效果;
到这里blooom shader的代码解读完毕,快速版本(移动版本)的Pass减少了采样次数,逻辑一致;
随着迭代次数的增加合并操作积累的亮度变高;屏幕的分辨率越高,迭代次数越高,导致平均亮度变高;

uber shader
这部分的代码并不复杂,C#中开启了BLOOM关键词,则计算对应部分;
处理方式是:在原贴图的基础上叠加bloom光照强度;
bloom光照强度 = lastUp采样结果 * bloom强度 * bloom颜色;
原贴图 = context.source采样结果 这个贴图并没有被修改过;
两者相加让亮出更亮,到此uber shader解读完毕;

总结
1.bloom效果的原理,如何控制1个点能bloom的范围和强度
假设contex.source为1024X1024,只有正中心4个像素颜色为0.9,其他地方颜色都是0;
假设贴图的迭代次数为4次,anamorphicRatio为0;阀值为1,柔和为0.5;
降采样、迭代0:512X512:正中心4个像素合并为新的像素,颜色值为0.9;
过滤一次:
rq = clamp(0.9 - 0.5, 0, 1);
rq = 1 / 4 / 0.5 * rq * rq
rq等于0.08;EPSILON等于0.18;
color *= 0.08/0.18 颜色值等于0.4;
降采样、迭代1:256X256:正中心4个像素合并为新的像素,颜色值为0.1;
降采样、迭代2:128X128:正中心4个像素合并为新的像素,颜色值为0.025;
降采样、迭代3:64X64:正中心4个像素合并为新的像素,颜色值为0.00625;
升采样、迭代2:128X128:0.025+0.00625=0.03125
升采样、迭代1:256X256:0.1+0.03125=0.13125
升采样、迭代0:512X512:0.4+0.13125=0.53125
bloom处理:0.9+0.53125Xbloom强度Xbloom颜色 ⇒溢出为1
迭代0中1个像素覆盖了2X2个像素;
迭代1中1个像素覆盖了4X4个像素;
迭代2中1个像素覆盖了8X8个像素;
迭代3中1个像素覆盖了16X16个像素;
每多一次迭代,一个像素的bloom范围就扩大一倍,强度则是最终结算时的系数;

2.anamorphicRatio不为0时,是如何控制bloom方向的
anamorphicRatio改变了迭代0的贴图尺寸,后续迭代贴图也跟着高宽/2;
其他条件和上一例不变,anamorphicRatio为1时,讨论为何bloom水平扩张;
降采样、迭代0:512X1024:正中心2个像素合并为新的像素,颜色值为0.9;
过滤一次:颜色值依旧等于0.4,且本次有2个竖排相连的像素通过了过滤;
迭代0中1个像素覆盖了2X1个像素;
迭代1中1个像素覆盖了4X2个像素;
迭代2中1个像素覆盖了8X4个像素;
迭代3中1个像素覆盖了16X8个像素;
仅缩小宽度时,迭代贴图中单个像素对横向像素的覆盖范围更广,使bloom范围变得不均;
本例中横向的bloom范围没有变化,而竖向的bloom范围缩小了;
到此Bloom效果解析完毕,有疑问可以留言;


关注成长,注重因果。