整體流程
後處理主要內容列表
- 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次