1. 程式人生 > 實用技巧 >Unity PostProcess簡要分析與總結

Unity PostProcess簡要分析與總結

整體流程

後處理主要內容列表

PostProcess Effect處理順序

  • Anti-aliasing
  • Blur

Builtins:
DepthOfField
Uber Effects:

  • AutoExposure
  • LensDistortion
  • CHromaticAberration
  • Bloom
  • Vignette
  • Grain
  • ColorGrading(tonemapping)

關鍵類

PostProcessResource 繫結shader資源

PostProcessRenderContext 重要引數快取、

PostProcessLayer 後處理渲染控制類

PostProcessEffectRenderer 所有後處理Effect繼承它,並實現其render介面

PostProcessEffectSettings Effect的面板、屬性描述類

一個Effect一般包括

  • 一個它自己的shader
  • 一個UI描述類(CustomEffect,繼承PostProcessEffectSettings)
  • 一個渲染介面類(CustomEffectRender,繼承PostProcessEffectRender)

自定義後處理

可以新增這幾類後處理:BeforeTransparent,BeforeStack,AfterStack,這類後處理可以不修改原PostProcessing下的程式碼進行新增。

如果想新增Builtin階段的後處理,那麼一般在PostProcessing/Runtime/Effects下進行新增,這類後處理可能會修改PostProcessing下的程式碼或shader

核心函式

後處理的入口函式為PostProcessLayer::OnPreRender

核心渲染控制邏輯

1- BuildCommandBuffers

PostProcessLayer::BuildCommandBuffers
{
    int tempRt = m_TargetPool.Get();
    context.GetScreenSpaceTemporaryRT(m_LegacyCmdBuffer, tempRt, 0, sourceFormat, RenderTextureReadWrite.sRGB);
    m_LegacyCmdBuffer.BuiltinBlit(cameraTarget, tempRt, RuntimeUtilities.copyStdMaterial, stopNaNPropagation ? 1 : 0);
    context.command = m_LegacyCmdBuffer;
    context.source = tempRt;
    context.destination = cameraTarget;
    Render(context);
    ...
}

該函式從攝像機拷貝了一份臨時RT作為source,接著進入渲染階段Render(context)

2- Render(context)

if (hasBeforeStackEffects)
lastTarget = RenderInjectionPoint(PostProcessEvent.BeforeStack, context, "BeforeStack", lastTarget);

// Builtin stack,Effects
lastTarget = RenderBuiltins(context, !needsFinalPass, lastTarget);

// After the builtin stack but before the final pass (before FXAA & Dithering)
if (hasAfterStackEffects)
    lastTarget = RenderInjectionPoint(PostProcessEvent.AfterStack, context, "AfterStack", lastTarget);

// And close with the final pass
if (needsFinalPass)
    RenderFinalPass(context, lastTarget);

這裡最關鍵的是RenderBuiltins和RenderFinalPass

3- RenderBuiltins

這是後處理的關鍵邏輯

中間的大量Effect都是計算引數、獲得各種Texture

最終使用Uber將各種效果混合到context.destination

int RenderBuiltins(PostProcessRenderContext context, bool isFinalPass, int releaseTargetAfterUse)
{
    ...
    context.uberSheet = uberSheet;//context.resources.shaders.uber
    ....
    cmd.BeginSample("BuiltinStack");
    int tempTarget = -1;
    var finalDestination = context.destination;//暫存 finalRT
    if (!isFinalPass)
    {
        // Render to an intermediate target as this won't be the final pass
        tempTarget = m_TargetPool.Get();
        context.GetScreenSpaceTemporaryRT(cmd, tempTarget, 0, context.sourceFormat);
        context.destination = tempTarget;//臨時RT,臨時destination
    ...
    }
    ....
    int depthOfFieldTarget = RenderEffect<DepthOfField>(context, true);
    ..
    int motionBlurTarget = RenderEffect<MotionBlur>(context, true);
    ..
    if (ShouldGenerateLogHistogram(context))
        m_LogHistogram.Generate(context);

    // Uber effects
    RenderEffect<AutoExposure>(context);
    uberSheet.properties.SetTexture(ShaderIDs.AutoExposureTex, context.autoExposureTexture);

    RenderEffect<LensDistortion>(context);
    RenderEffect<ChromaticAberration>(context);
    RenderEffect<Bloom>(context);
    RenderEffect<Vignette>(context);
    RenderEffect<Grain>(context);

    if (!breakBeforeColorGrading)
        RenderEffect<ColorGrading>(context);

    if (isFinalPass)
    {
        uberSheet.EnableKeyword("FINALPASS");
        dithering.Render(context);
        ApplyFlip(context, uberSheet.properties);
    }
    else
    {
        ApplyDefaultFlip(uberSheet.properties);
    }
    //使用uber shader混合前面的Effects的結果
    cmd.BlitFullscreenTriangle(context.source, context.destination, uberSheet, 0);

    context.source = context.destination;
    context.destination = finalDestination;

    ...releaseRTs
    cmd.EndSample("BuiltinStack");

    return tempTarget;
}

RT

PostProcess中RT總覽

可以看到儲存渲染圖象的RT都的RW都為sRGB

RT與sRGB

在客戶端配置為linear-space,開啟HDR的情況下 如果建立RT時,RenderTextureReadWrite為sRGB,也就是RT儲存的是sRGB值,讀值時,會自動進行sRGB->linear轉化,寫值時自動進行linear->sRGB轉化

初始時,只有一個cameraTarget,它的RenderTextureReadWrite為sRGB 在進入Effects前,會拷貝一個臨時RT作為context.source,他的RenderTextureReadWrite為sRGB

經過Anti-aliasing、Blur、Beforestack後,會將context.destination改為一個空白的臨時RT(RW為sRGB)

在Uber最後的混合階段,會將所有效果混合

RT的尺寸

通過PostProcessRenderContext.GetScreenSpaceTemporaryRT獲取RT,其預設尺寸儲存在PostProcessRenderContext.width,PostProcessRenderContext.height

設定PostProcessRenderContext.m_Camera會呼叫Camera.set,這裡會處理預設RT尺寸

if (m_Camera.stereoEnabled)
{
#if UNITY_2017_2_OR_NEWER
    var xrDesc = XRSettings.eyeTextureDesc;
    width = xrDesc.width;
    height = xrDesc.height;

    screenWidth = XRSettings.eyeTextureWidth;
    screenHeight = XRSettings.eyeTextureHeight;
#endif
}
else
{
    width = m_Camera.pixelWidth;
    height = m_Camera.pixelHeight;
    ...
}

除了幾個主要RT,中間作為Uber的紋理引數的RT的尺寸都不一定與cameraRT尺寸一樣。

典型的幾個都是通過new RenderTexture建立的,他們的大小和格式都需要注意

Effects

Effects處理順序

這裡再次列出來

  • Anti-aliasing
  • Blur

Builtins:
DepthOfField
Uber Effects:

  • AutoExposure
  • LensDistortion
  • CHromaticAberration
  • Bloom
  • Vignette
  • Grain
  • ColorGrading(tonemapping)

Uber Effects

說明

Uber Effects包括

  • AutoExposure
  • LensDistortion
  • CHromaticAberration
  • Bloom
  • Vignette
  • Grain
  • ColorGrading(tonemapping)
  • Uber混合階段

其中Uber混合階段之前的每個階段會根據配置生成對應的引數和臨時紋理,作為引數傳遞給Uber

同時如果某個階段啟用了,其render介面還會啟用Uber中對應的關鍵字(比如Vignette的render極端會啟用"VIGENETTE"),使用此方法來控制是Uber階段是否執行某個階段

Bloom

說明

Bloom 通過Downsample和Upsample得到一張BloomTex,這個過程需要確定迭代次數,每次使用的RT的尺寸

Bloom面板上有個關鍵引數Anamorphic Ratio([-1,1])能決定這些臨時RT的尺寸

最終得到的BloomTex的尺寸就是第0次Downsample的尺寸,也就是(tw,th)

面板上的重要引數說明

  • threshold :亮度分離閾值,比如大於改值的進行Bloom效果
  • intensity :強度
  • Anamorphic Ratio:決定bloomtex的尺寸,間接決定模糊迭代次數

Bloom 分為三步

  • 分離原圖中亮度較大的畫素,進行降解析度處理
  • 把分離的亮度圖進行高斯模糊
  • 將模糊後的亮度圖和原圖進行疊加,這一步在uber階段完成

Bloom臨時RT尺寸、迭代次數確定

公式

程式碼

float ratio = Mathf.Clamp(settings.anamorphicRatio, -1, 1);
float rw = ratio < 0 ? -ratio : 0f;
float rh = ratio > 0 ?  ratio : 0f;
int tw = Mathf.FloorToInt(context.screenWidth / (2f - rw));
int th = Mathf.FloorToInt(context.screenHeight / (2f - rh));

在DownSample迭代時,對應的RT的邊長每次會減少一半

迭代次數iterations確定

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);
float sampleScale = 0.5f + logs - logs_i;
sheet.properties.SetFloat(ShaderIDs.SampleScale, sampleScale);

Downsample和Upsample

shader

  • Bloom.shader 核心shader
  • Sampling.hlsl 若干取樣函式

不同階段使用的pass列表

Downsample

每次Downsample,RT邊長會縮短一倍,同時建立了一對引數一樣的臨時RT,這些RT的(RW都為sRGB)

shader使用上

  • 第0次使用的是 FragPrefilter13或FragPrefilter4
  • 其他迴圈使用 FragDownsample13或Downsample4
var lastDown = context.source;
for (int i = 0; i < iterations; i++)
{
    int mipDown = m_Pyramid[i].down;
    int mipUp = m_Pyramid[i].up;
    int pass = i == 0? (int)Pass.Prefilter13 + qualityOffset
        : (int)Pass.Downsample13 + qualityOffset;
    context.GetScreenSpaceTemporaryRT(cmd, mipDown, 0, context.sourceFormat, RenderTextureReadWrite.Default, FilterMode.Bilinear, tw, th);
    context.GetScreenSpaceTemporaryRT(cmd, mipUp, 0, context.sourceFormat, RenderTextureReadWrite.Default, FilterMode.Bilinear, tw, th);
    cmd.BlitFullscreenTriangle(lastDown, mipDown, sheet, pass);
    lastDown = mipDown;
    tw = Mathf.Max(tw / 2, 1);
    th = Mathf.Max(th / 2, 1);
}

Upsample

int lastUp = m_Pyramid[iterations - 1].down;
for (int i = iterations - 2; i >= 0; i--)
{
    int mipDown = m_Pyramid[i].down;
    int mipUp = m_Pyramid[i].up;
    cmd.SetGlobalTexture(ShaderIDs.BloomTex, mipDown);
    cmd.BlitFullscreenTriangle(lastUp, mipUp, sheet, (int)Pass.UpsampleTent + qualityOffset);
    lastUp = mipUp;
}

Uber混合階段

這個階段在Uber Effects都執行完之後才執行,進行效果混合

這段邏輯在Uber.shader中

half4 bloom = UpsampleTent(TEXTURE2D_PARAM(_BloomTex, sampler_BloomTex), uvDistorted, _BloomTex_TexelSize.xy, _Bloom_Settings.x);
bloom *= _Bloom_Settings.y;
dirt *= _Bloom_Settings.z;
color += bloom * half4(_Bloom_Color, 1.0);
color += dirt * bloom;

這裡將bloom顏色疊加到color上

Vignette

說明

聚焦,邊緣darkening

效果略

有兩個模式

  • Classic 邊緣黑化
  • Masked 使用一張自定義圖片覆蓋在螢幕上,以實現特俗效果

Vignette重要工作都在Uber中執行,其Render部分只根據設定進行引數設定

var sheet = context.uberSheet;

sheet.EnableKeyword("VIGNETTE");

sheet.properties.SetColor(ShaderIDs.Vignette_Color, settings.color.value);

if (settings.mode == VignetteMode.Classic)
{
    sheet.properties.SetFloat(ShaderIDs.Vignette_Mode, 0f);
    sheet.properties.SetVector(ShaderIDs.Vignette_Center, settings.center.value);
    float roundness = (1f - settings.roundness.value) * 6f + settings.roundness.value;
    sheet.properties.SetVector(ShaderIDs.Vignette_Settings, new Vector4(settings.intensity.value * 3f, settings.smoothness.value * 5f, roundness, settings.rounded.value ? 1f : 0f));
}
else // Masked
{
    sheet.properties.SetFloat(ShaderIDs.Vignette_Mode, 1f);
    sheet.properties.SetTexture(ShaderIDs.Vignette_Mask, settings.mask.value);
    sheet.properties.SetFloat(ShaderIDs.Vignette_Opacity, Mathf.Clamp01(settings.opacity.value));
}

Uber混合階段

if (_Vignette_Mode < 0.5)
{
    half2 d = abs(uvDistorted - _Vignette_Center) * _Vignette_Settings.x;
    d.x *= lerp(1.0, _ScreenParams.x / _ScreenParams.y, _Vignette_Settings.w);
    d = pow(saturate(d), _Vignette_Settings.z); // Roundness
    half vfactor = pow(saturate(1.0 - dot(d, d)), _Vignette_Settings.y);
    color.rgb *= lerp(_Vignette_Color, (1.0).xxx, vfactor);
    color.a = lerp(1.0, color.a, vfactor);
}
else
{
    half vfactor = SAMPLE_TEXTURE2D(_Vignette_Mask, sampler_Vignette_Mask, uvDistorted).a;
    #if !UNITY_COLORSPACE_GAMMA
    {
        vfactor = SRGBToLinear(vfactor);
    }
    #endif
    half3 new_color = color.rgb * lerp(_Vignette_Color, (1.0).xxx, vfactor);
    color.rgb = lerp(color.rgb, new_color, _Vignette_Opacity);
    color.a = lerp(1.0, color.a, vfactor);
}

Grain

效果略

grain是基於噪聲模擬老式電影膠片的顆粒感,恐怖遊戲中常用這中效果

它的Render部分是使用GrainBaker.shader中的演算法生成一張128x128的GrainTex,Uber階段將之混合到最終效果

Uber混合階段

half3 grain = SAMPLE_TEXTURE2D(_GrainTex, sampler_GrainTex, i.texcoordStereo * _Grain_Params2.xy + _Grain_Params2.zw).rgb;

// Noisiness response curve based on scene luminance
float lum = 1.0 - sqrt(Luminance(saturate(color)));
lum = lerp(1.0, lum, _Grain_Params1.x);

color.rgb += color.rgb * grain * _Grain_Params1.y * lum;

ColorGrading

說明

ColorGrading有三種模式:

HighDefinitionRange

LowDefinitionRange

External :要求支援compute shader 與3D RT

有三條管線,分別是

RenderExternalPipeline3D :要求支援compute shader 與3D RT

RenderHDRPipeline3D :要求支援compute shader 與3D RT

RenderHDRPipeline2D 當不支援compute shader和3D RT時,使用這個進行HDR color pipeline

RenderLDRPipeline2D

這裡只考慮RenderHDRPipeline2D

RenderHDRPipeline2D

該階段分為5個部分

  • Tonemapping
  • Basic
  • Channel Mixer
  • Trackballs
  • Grading Curves

使用的shader為lut2DBaker,核心的檔案還有Colors.hlsl、ACES.hlsl

目的是生成一張顏色查詢表Lut2D,然後在Uber階段,根據該表查詢對映顏色,作為新的顏色值

Lut2D的RenderTextureReadWrite為Linear,也就是儲存的是Linear資料

可選的,有3種Tonemapping方式:Neutral、ACES、Custom

這裡只考慮ACES

根據配置設定好lut2DBaker的各個階段的引數和特性後,就進入計算階段

context.command.BeginSample("HdrColorGradingLut2D");
context.command.BlitFullscreenTriangle(BuiltinRenderTextureType.None, m_InternalLdrLut, lutSheet, (int)Pass.LutGenHDR2D);
context.command.EndSample("HdrColorGradingLut2D");

當計算結束,會把計算結果Lut2D作為引數出傳遞給Uber,同時還會設定對應引數,最後的顏色替換階段在Uber中完成

uberSheet.EnableKeyword("COLOR_GRADING_HDR_2D");
uberSheet.properties.SetVector(ShaderIDs.Lut2D_Params, new Vector3(1f / lut.width, 1f / lut.height, lut.height - 1f));
uberSheet.properties.SetTexture(ShaderIDs.Lut2D, lut);
uberSheet.properties.SetFloat(ShaderIDs.PostExposure, RuntimeUtilities.Exp2(settings.postExposure.value));

Lut2DBaker

入口

float4 FragHDR(VaryingsDefault i) : SV_Target
{
    float3 colorLutSpace = GetLutStripValue(i.texcoord, _Lut2D_Params);
    float3 graded = ColorGradeHDR(colorLutSpace);
    return float4(graded, 1.0);
}
float3 GetLutStripValue(float2 uv, float4 params)
{
    uv -= params.yz;
    float3 color;
    color.r = frac(uv.x * params.x);
    color.b = uv.x - color.r / params.x;
    color.g = uv.y;
    return color * params.w;
}

_Lut2D_Params是寫死的,值為:

(lut_height, 0.5 / lut_width, 0.5 / lut_height, lut_height / lut_height - 1)
其中lut_height = 32;lut_width = 32*32

也就是說,該表的大小為(32*32,32)

下圖是一個例子(這裡觀察到的結果與實際的儲存值是不一致的)

ColorGradeHDR主要將HDR顏色轉到ACES顏色空間,並進行tonemapping

//相關函式在Colors.hlsl中
float3 ColorGradeHDR(float3 colorLutSpace)
{
    //得到HDR顏色
    float3 colorLinear = LUT_SPACE_DECODE(colorLutSpace);
    //
    float3 aces = unity_to_ACES(colorLinear);
    // ACEScc (log) space
    float3 acescc = ACES_to_ACEScc(aces);
    acescc = LogGradeHDR(acescc);
    aces = ACEScc_to_ACES(acescc);
    // ACEScg (linear) space
    float3 acescg = ACES_to_ACEScg(aces);
    acescg = LinearGradeHDR(acescg);    
    // Tonemap ODT(RRT(aces))  
    aces = ACEScg_to_ACES(acescg);  

    //tonemap
    colorLinear = AcesTonemap(aces);

    return colorLinear;
}

兩個重要的對映函式

#define LUT_SPACE_ENCODE(x) LinearToLogC(x) 
#define LUT_SPACE_DECODE(x) LogCToLinear(x) //在Uber中進行顏色對映時使用該介面
float3 LinearToLogC(float3 x)
{
    return LogC.c * log10(LogC.a * x + LogC.b) + LogC.d;   
}
float3 LogCToLinear(float3 x)
{
    return (pow(10.0, (x - LogC.d) / LogC.c) - LogC.b) / LogC.a;    
}
static const ParamsLogC LogC =
{
    0.011361, // cut
    5.555556, // a
    0.047996, // b
    0.244161, // c
    0.386036, // d
    5.301883, // e
    0.092819  // f
};

LinearToLogC

公式

影象

LogCToLinear

公式

影象

Uber階段

這裡根據lut進行顏色對映

color *= _PostExposure;
float3 colorLutSpace = saturate(LUT_SPACE_ENCODE(color.rgb));
color.rgb = ApplyLut2D(TEXTURE2D_PARAM(_Lut2D, sampler_Lut2D), colorLutSpace, _Lut2D_Params);

這裡使用LUT_SPACE_ENCODE將顏色對映到HDR空間,然後通過這個值在lut中查詢到對應的顏色值,作為新的color

// 2D LUT grading
// scaleOffset = (1 / lut_width, 1 / lut_height, lut_height - 1)
//
half3 ApplyLut2D(TEXTURE2D_ARGS(tex, samplerTex), float3 uvw, float3 scaleOffset)
{
    // Strip format where `height = sqrt(width)`
    uvw.z *= scaleOffset.z;
    float shift = floor(uvw.z);
    uvw.xy = uvw.xy * scaleOffset.z * scaleOffset.xy + scaleOffset.xy * 0.5;
    uvw.x += shift * scaleOffset.y;
    uvw.xyz = lerp(
        SAMPLE_TEXTURE2D(tex, samplerTex, uvw.xy).rgb,
        SAMPLE_TEXTURE2D(tex, samplerTex, uvw.xy + float2(scaleOffset.y, 0.0)).rgb,
        uvw.z - shift
    );
    return uvw;
}

Uber整合階段

原始顏色假設為color0

color.rgb *= autoExposure;
//Bloom
color += dirt * bloom;
//Vignette
color.rgb = lerp(color.rgb, new_color, _Vignette_Opacity); color.a = lerp(1.0, color.a, vfactor);
//Grain
color.rgb += color.rgb * grain * _Grain_Params1.y * lum;
//COLOR_GRADING_HDR_2D
color.rgb = ApplyLut2D(TEXTURE2D_PARAM(_Lut2D, sampler_Lut2D), colorLutSpace, _Lut2D_Params);

之後,還會根據是否是final pass執行如下邏輯

非final pass

UNITY_BRANCH
if (_LumaInAlpha > 0.5)
{
    // Put saturated luma in alpha for FXAA - higher quality than "green as luma" and
    // necessary as RGB values will potentially still be HDR for the FXAA pass
    half luma = Luminance(saturate(output));
    output.a = luma;
}
#if UNITY_COLORSPACE_GAMMA
{
    output = LinearToSRGB(output);
}
#endif

如果是sRGB工作空間,還會將結果進行gamma矯正

final pass

如果定義了UNITY_COLORSPACE_GAMMA

還需要將linear 轉到 sRGB

#if UNITY_COLORSPACE_GAMMA
{
    output = LinearToSRGB(output);
}
#endif

注意

Bloom一般開啟fastmode,此模式下只採樣4次,預設模式會取樣13次