Unity PostProcess簡要分析與總結
整體流程
後處理主要內容列表
- Ambient Occlusion
- Anti-aliasing
- Auto-exposure
- Bloom
- Chromatic Aberration
- Color Grading
- Deferred Fog
- Depth of Field
- Grain
- Lens Distortion
- Motion Blur
- Screen-space reflections
- Vignette
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次