屏幕后处理Re(一)

HDR,LDR

首先需要了解Color Grading(颜色分级)又叫调色,是游戏后期处理中常见也必备的一个环节。通过调整它能够改变或者矫正最终图像的颜色和亮度。

Dynamic Range是一种用数学的方式来描述场景亮度层次范围的技术,是图像从暗到亮的亮度/灰度分级,分级越多能够表现的画面层次就越丰富。

HDR和LDR:

  1. Low Dynamic Range(LDR)
    1. LDR作为8位的颜色图片,使用RGB模型,每个颜色有256种亮度等级,总共能够表示256^3种颜色。但是和现实相比仍有局限,所以引入HDR
  2. High Dynamic Range(HDR)
    1. HDR常见有12位和16位,因为显示器限制,显示的值只能在0,1,但是光照计算可以没有这样的限制,这样可以表示更多层次的明暗细节,亮的部分能够很亮,暗的部分能够很暗。
    2. LDR范围确定,但是HDR并不,所以同样是HDR可能范围并不相同。因为范围不同,但是最终需要统一到[0,1]供显示器使用,所以引入了Tone Mapping (色调映射)

色调映射(Tone Mapping)

用于将HDR转换到LDR,Tone mapping有不少算法的更迭,可以参考叛逆者的文章和Krzysztof Narkowicz的文章,目前最常用的是由美国电影艺术与科学学会提出的ACES Tone mapping,这也是基于经验的近似。 使用Tone Mapping的主要目的是获取一个S曲线,这样的话在S的两端,接近0和接近1的地方也能有更多层次的划分(暗的地方更暗,亮的地方更亮)。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
float3 ACESFilm(float3 x, float adapted_lum)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
x *= adapted_lum;
return saturate((x*(a*x+b))/(x*(c*x+d)+e));
}

float3 ACESFilm(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
return saturate((x*(a*x+b))/(x*(c*x+d)+e));
}

基本后处理

亮度

用于表示图片的明暗程度

亮度计算方法:

  • 线性空间:Luminance = color.rgb * float3(0.2125,0.7154,0.0721)

控制方法:系数直接与RGB相乘

1
2
half4 col = tex2D(_MainTex, i.uv);
half3 final_col = col.rgb * _Brightness;

饱和度

用于表示在同亮度下,颜色偏离灰色的程度,偏离越大,颜色越深越鲜艳

控制方法:

  1. 通过lerp函数与同亮度灰色进行插值
1
2
half3 luminance_col = half3(luminance,luminance,luminance);
final_col = lerp(luminance_col, final_col,_Saturation);

对比度

对比度指的是一幅图像中明暗区域最亮的白和最暗的黑之间不同亮度层级的差异,差异范围越大代表对比越大,差异范围越小代表对比越小

控制方法:通过与(0.5, 0.5, 0.5)的灰度插值进行控制

1
2
half3 avg_col = half3(0.5,0.5,0.5);
final_col = lerp(avg_col, final_col, _Contrast);

HSV颜色空间

HSV(Hue, Saturation, Value) 也是一种颜色空间,常用在图像编辑工具中

  • Hue:色调,用角度度量,范围在0°-360°,红色0°,绿色120°,蓝色240°
  • Saturation:饱和度,颜色和对应灰度的混合,0%-100%
  • Value:明度,颜色明亮程度,0%(黑)-100%(白),这个明度和光强度之间没有直接联系

RGB和CMY颜色空间是面向硬件的,HSV颜色空间是面向用户的,更加直观。

转换算法(伪代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
RGB2HSV(){
max=max(R,G,B);
min=min(R,G,B);
V=max(R,G,B);
if (R = max) H =(G-B)/(max-min)* 60;
if (G = max) H = 120+(B-R)/(max-min)* 60;
if (B = max) H = 240 +(R-G)/(max-min)* 60;
if (H < 0) H = H+ 360;
}

HSV2RGB(){
if (s = 0)
R=G=B=V;
else
H /= 60;
i = INTEGER(H);
f = H - i;
a = V * ( 1 - s );
b = V * ( 1 - s * f );
c = V * ( 1 - s * (1 - f ) );
switch(i){
case 0: R = V; G = c; B = a;
case 1: R = b; G = v; B = a;
case 2: R = a; G = v; B = c;
case 3: R = a; G = b; B = v;
case 4: R = c; G = a; B = v;
case 5: R = v; G = a; B = b;
}
}

调整色相

将RGB转成HSV之后,调整对应的H值,再转回RGB

晕影、暗角

根据像素点在屏幕中的位置来设置权重,计算的思路在Unity的PostProcessing v.2的Uber.shader和ExposureHistogram.compute中都有用到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ExposureHistogram.compute
// 这里通过计算像素位置为当前像素对应亮度的权重进行赋值,之后添加刀对应的EV直方图相应的桶下
#if USE_VIGNETTE_WEIGHTING
{
float2 d = abs(sspos - (0.5).xx);
float vfactor = saturate(1.0 - dot(d, d));
vfactor *= vfactor;
weight = (uint)(64.0 * vfactor);
}
#endif


// Uber.shader
// _Vignette_Settings: x: intensity, y: smoothness, z: roundness, w: rounded
{
// 计算距离并调整强度
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);
// 调整暗角边缘平滑度
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);
}

LUT(Look Up Table)调色

Look Up Table校色主要是通过查找表的方式对画面进行风格化的处理,本质上是一张映射表,将采样到的像素经过变换映射到另一个对应的颜色,在摄影软件中很常见,可以很方便的调出预留的风格。

常见的有3D和2D的查找表,3D查找表是将RGB数值作为三维的坐标,把颜色映射到一个立方体中,但是这样做会占用较大存储。

比较常见的是2D查找表,在b对应的数值区域进行了离散化,可以想象成本来3D对应的b值是连续的,但是在2D中我们根据b值将立方体切成了一片一片的,最后再将这些片连接起来。通过R和B来计算uv的x值,通过G通道来计算uv的y值。

下图是UE4文档中显示的LUT效果

可能出现的问题:

在实际的计算过程中,浮点数很有可能会长成这样

所以不能完全的将范围内所有的映射加入,因为很有可能会碰到上图的样子,这样的话就会超出映射范围,采样到范围外

计算方式

计算方式参考这里,通过先缩小一点采样范围,接着将采样范围进行右移,来防止数值的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fixed4 frag(v2f i) : SV_Target
{
float maxColor = COLORS - 1.0;
fixed4 col = saturate(tex2D(_MainTex, i.uv));
// 设置偏移范围
float halfColX = 0.5 / _LUT_TexelSize.z;
float halfColY = 0.5 / _LUT_TexelSize.w;
// 通过格子数量来计算阈值保证质量
float threshold = maxColor / COLORS;
// 新uv坐标生成
float xOffset = halfColX + col.r * threshold / COLORS;
float yOffset = halfColY + col.g * threshold;
float cell = floor(col.b * maxColor);
float2 lutPos = float2(cell / COLORS + xOffset, yOffset);
float4 gradedCol = tex2D(_LUT, lutPos);

return lerp(col, gradedCol, _Contribution);
}

LUT制作方法

  1. 通过Photoshop对目标画面进行调色
  2. 导入需要存储的LUT
  3. 将调好色的图层拖入对应的LUT中

参考

  1. Knarkowicz ACES
  2. Tone mapping进化论
  3. UE4 LookUpTable
  4. Halisavakis LUT
  5. HSV
  6. Unity-Technologies PostProcessing