我想试着实现一下下图中纪念碑谷中的全局渐变的效果。关于如何实现这种渐变效果之前和别人讨论过,一开始我认为是通过从下往上打光来实现的,后来同学说是利用了shader和雾。利用后者实现的效果更为缓和,而利用光照可能会造成底部过亮的问题,不好控制。

  为什么我管这种效果叫“全局渐变”呢?因为当我移动模型的时候,渐变的范围并不是固定在模型身上的,而是在世界空间的“某个位置”(如下图所示)。实际上,我们可以自行规定这个渐变地平线的位置,只需要在shader里加个控制变量就可以了。

实现

变量

首先,我们需要这么几个变量:

  • 顶部的颜色
  • 底部的颜色
  • 渐变混合的程度
  • 光照的强度
  • 渐变地平线
  • 渐变纹理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Properties
{
// 光照的强度
_Contrast("Contrast",Range(0,1))=0.1
// 渐变纹理
_RampTex("渐变纹理", 2D) = "white"{}
// 上下渐变色
_UpColor("UpColor",Color)=(1,1,1,1)
_DownColor("DownColor",Color)=(1,1,1,1)
// 渐变程度
_WorldYDeno("WorldYDeno",float)=0
// 渐变中心位置
_Skyline("地平线", float) = 0
}

顶点着色器

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

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

世界空间的坐标和法线用于光照强度的控制;裁剪空间的坐标用于渐变的渲染

1
2
3
4
5
6
7
8
9
10
11
v2f vert(appdata v){
v2f o;
// 世界空间坐标
o.worldPo=mul(UNITY_MATRIX_M,v.vertex);
// 世界空间法线
o.worldNormal = normalize(mul((float3x3)UNITY_MATRIX_M, v.normal));
// 裁剪空间坐标
o.vertex = UnityObjectToClipPos(v.vertex);
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
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 * _Contrast + (1 - _Contrast), 0, 1);
// 渐变混合
float4 col = fixed4(ambient + diffuse + light * lerp(_DownColor, _UpColor, clamp( (i.worldPos.y - _Skyline) / _WorldYDeno, 0, 1)), 1.0);

return col;
}

  最终,我们可以得到如下的效果(暂未加上渐变纹理贴图):

  可以看出效果不太好,并且在细节上表现的很不自然,画面的颜色很单一。如果加上渐变纹理的话可以得到更好的细节效果:

  好,就是这样了。实际上,我们还可以在较远的物体表面加上一层。有关于雾效的实现,在下一篇demo文章中将会尝试一下。最终我们的材质球面板和面板各属性的控制情况如下所示: