现实生活中,地面上经常会有云彩的影子在移动,它其实就是通过太阳光将其投射到地面上的,这个应用在游戏中跟人的感觉场景比较真实,在FoxHole散兵坑游戏中就实现了这个效果,如下图所示:
实现类似的效果方法有两种:一种是常用的方法,就是使用一个天空盒,再实现一个飘动的云层,然后通过平行光将其投射到地面上,虽然比较好理解,这种方法不推荐,运行效率也会受影响。另一种方法就是我们本篇博客需要实现的,通过改变纹理的UV,实现移动效果。下面具体介绍如何实现
首先我们要整一个平面将阴影渲染到上面去,这里不是指RenderTexture,这个平面设置的时候需要一点小技巧就是它的位置和大小跟摄像机的远裁剪面是一样的,效果如下所示:
上图是我们需要将云层绘制的平面区域,下面是摄像机的远裁剪面,如下所示:
下面是通过代码实现二者的一致性,先实现四边形与摄像机远裁剪面位置一致:
Camera cam = Camera.main;
float dist = cam.farClipPlane - 0.1f; // 0.1 is some magic value to avoid culling
Vector3 campos = cam.transform.position;
Vector3 camray = cam.transform.forward * dist;
Vector3 quadpos = campos + camray;
transform.position = quadpos;
还有四边形平面与摄像机远裁减面的大小要一致:
Vector3 scale = transform.parent ? transform.parent.localScale : Vector3.one;
float h = cam.orthographic ? cam.orthographicSize * 2f : Mathf.Tan(cam.fieldOfView * Mathf.Deg2Rad * 0.5f) * dist * 2f;
transform.localScale = new Vector3(h * cam.aspect / scale.x, h / scale.y, 0f);
上面基本的操作已经完成,接下来就要考虑我们的代码放在哪个函数里面?Update,FixedUpdate还是自定义函数?
在回答这个问题之前,先给读者看一下Unity官方提供的函数执行顺序图:
这个图的执行顺序大家一定要牢记,关键时候可以派上用场,我们可以看到在Scene rendering中有个函数OnWillRenderObject,官方给出的功能解释是:如果对象可见,则为每个相机调用一次此函数,我们的渲染选择的就是该函数,我们还看到一个函数是OnRenderImage,可能感觉它比较合适,官方给出的解释是:OnRenderImage(仅限专业版): 在完成场景渲染后调用此函数,以便对屏幕图像进行后处理,所以不适合我们需要的。
继续我们的云层渲染讲解,第一步的工作已经完成,下面是考虑实现投影到地面上的云层有哪些需要考虑的点:
其中最终要的是深度纹理图的实现,在Unity引擎中的前向渲染路径(Forward Rendering)下,我们需要手动设置相机,让它提供场景的深度信息,代码如下所示:
Camera.main.depthTextureMode |= DepthTextureMode.Depth;
如果在延迟渲染路径(Deferred Lighting)下,由于延迟渲染需要场景的深度信息和法线信息来做光照计算,所以并不需要我们手动设置相机。
这样我们就可以在shader中访问_CameraDepthTexture来获取保存的场景的深度信息,之后再利用UNITY_SAMPLE_DEPTH这个宏来处理_CameraDepthTexture的值,这样我们就获取了某个像素的深度值。
此时的深度值并非是线性的,因此我们常常需要利用另一个内建的方法Linear01Depth将结果转化为线性的。这样,我们就能将场景的深度信息渲染为一张灰度图,Shader代码如下所示:
float depth = Linear01Depth( SAMPLE_DEPTH_TEXTURE( _CameraDepthTexture, i.uv ) );
我们就能将场景的深度信息渲染为一张灰度图。如下所示:
我们利用深度图可以做很多事情,渲染后处理效果比如景深,SSAO,Blur,Bloom等等都与深度图技术相关,其实很多游戏使用该功能渲染场景,在此只是跟读者再回顾一下,比如下面的游戏画面:
我们实现的地面动态实时阴影也使用了该技术,接下来我们再介绍云层移动,云层移动是与uv相关的,我们先要获取到uv,Shader代码如下所示:
o.pos = UnityObjectToClipPos (v.vertex);
o.uv = v.texcoord.xy;
接下来计算在世界空间中从相机到远裁剪的平面四边形的射线,Shader代码如下所示:
float3 pos_world = mul (unity_ObjectToWorld, v.vertex);
o.ray = pos_world - _WorldSpaceCameraPos;
以上代码都是在顶点着色器中计算得到的,下面在片段着色器中实现云层的uv变化,Shader代码如下所示:
float depth = Linear01Depth( SAMPLE_DEPTH_TEXTURE( _CameraDepthTexture, i.uv ) );
float3 world = i.ray * depth + _WorldSpaceCameraPos;
// Convert world position to cloud uv
#if SSCS_TOPDOWN
float2 world_uv = world.xz;
#else
float2 world_uv = world.xy;
#endif
float2 cloud_uv = world_uv * 0.005f;
float2 cloud_wind = _CloudFactor.xy;
float2 cloud_tiling = _CloudFactor.zw;
cloud_uv = (cloud_uv + cloud_wind) * cloud_tiling;
最后云层还需要有淡入淡出效果,Shader代码如下所示:
float cloud = tex2D(_MainTex, cloud_uv).r;
float cloud_faded = cloud * (1.0 - depth);
阴影渲染是在相机空间中实现的,我们需要将一些值传给我们的Shader,下面的逻辑代码完成了传值操作:
transform.rotation = Quaternion.LookRotation(quadpos - campos, cam.transform.up); // align uv with _depthtexture
float t = Time.time;
_ren.sharedMaterial.SetVector("_CloudFactor", new Vector4(
cloudWindSpeed.x * t
, cloudWindSpeed.y * t
, cloudTiling.x
, cloudTiling.y));
if(cam.orthographic)
{
_ren.sharedMaterial.EnableKeyword("SSCS_ORTHO");
_ren.sharedMaterial.SetVector("_WorldSpaceCameraRay", camray);
}
else
{
_ren.sharedMaterial.DisableKeyword("SSCS_ORTHO");
}
另外,云层的颜色,形状都可以改变的,实现逻辑代码如下所示:
_ren.sharedMaterial.SetTexture ("_MainTex", cloudTexture);
// Pre calculate color multiplier at script (not shader)
// 1 minus for darken blending
_ren.sharedMaterial.SetVector("_ShadowFactor", new Vector4(
(1f - shadowColor.r) * shadowIntensity
,(1f - shadowColor.g) * shadowIntensity
,(1f - shadowColor.b) * shadowIntensity
,1f));
// If cloud is topdown way, world position.xz converted to cloud uv
if(isTopDown)
{
_ren.sharedMaterial.EnableKeyword("SSCS_TOPDOWN");
}
else
{
_ren.sharedMaterial.DisableKeyword("SSCS_TOPDOWN");
}
下面我们把脚本和Shader挂到我们的四边形平面的实例化对象上面,如下所示:
对 CloudShadow.cs脚本中的参数我们可以设置,改变云层的形状,颜色,移动速度等等,实现效果如下所示:
另外我们可通过改变纹理对应云层形状改变,如下图所示:
代码下载地址:链接:https://pan.baidu.com/s/1DYwieVaBgrUANeoopPueJQ 密码:a6hy