色调映射(Tone Mapping)

普通LDR显示器的亮度范围在0-1之间(普通8bit显示器,每个通道2的八次方256),而Shader计算出来的颜色值很容易超过此范围,属于HDR(10bit,每个通道2的10次方)。

色调映射(Tone-Mapping)就是将高动态范围的图像亮度压缩到低动态的技术。

这里使用ACES色调映射:

ACES

接下来我们在Shader中模拟一下这个色调映射,一般情况下,色调映射技术是在后处理过程中应用的,这里仅作为学习。

先在Shader的顶点函数之前声明一个ACES计算函数

1
2
3
4
5
6
7
8
9
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));
}

然后在片元计算的函数中,先把基础的diffuse贴图从gamma空间转换到线性空间:

1
2
3
half4 base_color = tex2D(_MainTex, i.uv);
//将基础色从gamma空间转换到线性空间,为了在ACES色调映射中得到更好的效果
base_color = pow(base_color,2.2);

在最后计算颜色时,应用ACES色调映射并且将颜色还原到gamma空间

1
2
3
4
5
half3 final_color = (diffuse_color + spec_color + ambient_color) * ao_color;//AO贴图和最终结果乘算
half3 tone_color = ACESFilm(final_color);
//将计算结果转换到gamma空间
tone_color = pow(tone_color,1.0/2.2);
return half4(tone_color, 1.0);

视差偏移(Parallax Mapping)

游戏中凹凸的石板,草地等表面,需要一定的前后遮挡关系,如果只有法线贴图的情况下,从侧面看这些表面并不能反映这种遮挡关系。

我们可以使用置换贴图,使用它之后,模型会先进行表面细分,然后根据贴图指定的高度偏移顶点,从而产生遮挡效果,但是在游戏中很少使用细分。

这里介绍一种叫做视差偏移(视差映射)的方法,它使用一种视差贴图,利用视错觉来让凹凸表面产生前后遮挡的效果。

[视差贴图 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/05 Advanced Lighting/05 Parallax Mapping/)

首先采样一张视差贴图(高度图),从而获得单个像素的高度信息。然后将当前像素到摄像机的向量转化到切线空间中(因为切线空间是一个稳定的空间,不会因为模型的旋转而使xyz轴混乱导致结果不正确),得到这个向量后根据高度对向量进行缩放,从而对uv进行偏移。

在实际应用中,我们会把高度图反相,使用深度图(越白越凹)来进行视差偏移,因为单纯高度贴图不能让模型放大,所以使用深度图更合理。

深度图

首先引用视差贴图,这种贴图只需要单个通道的数据即可,所以我们取half而不是half4

1
half height = tex2D(_ParallaxMap, i.uv);

然后将当前像素到摄像机的向量view_dir转换到切线空间下,使用上一节讲过的TBN矩阵

1
2
3
4
5
6
7
half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
half3 normal_dir = normalize(i.normal_dir);
half3 tangent_dir = normalize(i.tangent_dir);
half3 binormal_dir = normalize(i.binormal_dir);
float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);

half3 view_tangentspace = normalize(mul(TBN, view_dir));

最后将uv进行偏移,然后再将偏移后的uv应用到其他贴图的tex2D函数中。

1
2
3
4
5
6
7
8
half2 uv_parallax = i.uv - (1.0 - height) * view_tangentspace.xy * _Parallax * 0.01;

half4 base_color = tex2D(_MainTex, uv_parallax);

base_color = pow(base_color,2.2);
half4 ao_color = tex2D(_AOMap, uv_parallax);
half4 spec_mask = tex2D(_SpecMask, uv_parallax);
half4 normal_map = tex2D(_NormalMap, uv_parallax);

首先使用1.0 - height来得到深度图,让这个深度图作为缩放因子和切线空间下的view相乘,然后偏移uv,注意用减法。

陡峭视差映射(Steep Parallax Map)

为了让视差偏移更精细,我们每次采样高度图并偏移uv之后,再次用新偏移的uv采样高度图,循环几次得到更精确的结果。

陡峭视差映射

1
2
3
4
5
6
half2 uv_parallax = i.uv;
for(int j = 0; j < 10; j++)
{
half height = tex2D(_ParallaxMap, uv_parallax);
uv_parallax = uv_parallax - (1.0 - height) * view_tangentspace.xy * _Parallax * 0.01;
}

其他优化

如果我们想以高度图亮度的0.5为分界,让图片更分离一些,可以将1.0-height改为0.5-height

我们也可以让view_tangentspace.xy除以view_tangentspace.z + 0.42,这个可以用来校正模型边缘,防止扭曲过大。

1
uv_parallax = uv_parallax - (0.5 - height) * (view_tangentspace.xy / view_tangentspace.z + 0.42) * _Parallax * 0.01;