接上篇文章画的饼,这回我打算试着实现一下雾效,我们最终的目标就是实现类似下图的效果。当然,纪念碑谷中的不怎么被雾影响的方块和浸泡在雾气中的方块使用的应该是两种shader,他们不会混在一起。但如果我们要实现的是一个可以移动的场景呢?使用我想在纪念碑谷的静态贴地雾效上加点东西,即:当摄像机移动时,雾的范围会随着摄像机的移动而移动,始终做到远处模糊、近处清晰的效果。

思路

其实我们实现纪念碑谷中的这种雾效的思路和全局渐变非常类似。总结一下,主要有以下两点需求:

  • 离摄像机越远,雾气越浓,具体表现在物体整体与雾气的颜色相融
  • 离摄像机越近,雾气越轻,具体表现在雾气很浅,几乎消失

  即,离相机较远的物体如下左所示;离相机较近的物体如下右所示:

  我们可以调整地平线(渐变中心位置)的值来实现这个效果。当一个物体离摄像机较近时,它的渐变中心也较低;反之则较高

实现

变量

我们需要这么几个变量:

  • 渐变纹理
  • 主色调
  • 雾的颜色
  • 雾的浓度
1
2
3
4
5
6
7
8
Properties{
_RampTex("渐变纹理", 2D) = "white"{}
// 颜色设置
_UpColor("Main Color",Color)=(1,1,1,1)
_DownColor("Fog Color",Color)=(1,1,1,1)
// 雾气浓度
_Concentration("Fog Concentration", float) = 0
}

顶点着色器

在顶点着色器中我们需要获取几组坐标:

  1. 世界空间的顶点坐标
  2. 世界空间的顶点法线
  3. 裁剪空间的顶点坐标
  4. 观察空间的顶点坐标

  在顶点着色器中,我们最关键的步骤是求出顶点和相机之间的距离。实现这个操作的方法是利用观察空间的坐标深度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v2f vert(appdata v){
v2f o;
// 获取观察空间坐标
float3 cameraPos = mul(UNITY_MATRIX_MV,v.vertex).xyz;
// 计算与相机距离
o.distance = length(cameraPos);

// 获取世界空间坐标
o.worldPos=mul(UNITY_MATRIX_M,v.vertex);
// 获取裁剪空间坐标
o.vertex = UnityObjectToClipPos(v.vertex);

o.worldNormal = normalize(mul((float3x3)UNITY_MATRIX_M, v.normal));
o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
return o;
}

片元着色器

在片元着色器中,我们进行三个主要步骤:

  1. 计算渐变纹理的映射
  2. 计算光照
  3. 雾气的渐变混合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
float4 frag(v2f i) : SV_Target{
// 渐变纹理
fixed3 worldN = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed halfLambert = 0.5 * dot(worldN, worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb;
fixed3 diffuse = _LightColor0.rgb * diffuseColor;

// 计算光照
float NdotL = dot(i.worldNormal, _WorldSpaceLightPos0);
float light = clamp(NdotL * 0.237 + 0.763, 0, 1);

// 利用相机距离远近作为控制
_Concentration = _Concentration + i.distance;
// 混合
float4 fogCol = light * lerp(_DownColor, _UpColor, clamp( (i.worldPos.y - _Concentration) / 7.5, 0, 1));
float4 col = fixed4(ambient + diffuse + fogCol, 1.0);

return col;
}

  最终效果和材质球控制演示如下所示:

优化

  完成之后感觉好像看上去哪里不对。对比了一下纪念碑谷中的图之后,我意识到我的目标效果应该是贴地雾效,而不是全局雾效。即使物体距离摄像机比较远,他应该也只是底部的一部分被云雾覆盖住,而不是整个变成云雾的颜色。所以,我们应该在变量里加上一个“云雾地平线”:

1
2
// 最高雾气位置
_Skyline("Skyline",float) = 0

  之后,在片元着色器中对雾气浓度加上限制。由于测试过的数值都在0以下,所以选用了min函数:

1
_Concentration = min(_Concentration + i.distance, _Test);

  这样,即使在很远的位置,较高的部分依然展现出原有的色彩:

再画个饼,下一篇文章打算研究一下几何形状的low poly风格海洋的实现。