Unity3D-移动平台简单水效果的实现

发表于2017-02-23
评论4 1.45w浏览
?

在游戏场景中我们可以看到各种各样的河流、湖泊等水效果,因为有了这些水效果才让使游戏的场景更美、更真实。那么在移动平台上如何实现简单的水效果呢?考虑到有些项目开发人员还不知,下面就来简单的介绍下Unity3D移动平台中的水效果实现。

一、前言

      本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。

二、概述

之前我有专门开了一个系列来讲水效的实现方法,可以看到渲染的结果还是比较不错的。但是,如果将之用于移动平台,那么你会发现,帧率掉的只剩下十几帧。所以,我们需要做一些事来优化之前的方案,以便于在移动平台上实现水效果。

来回顾一下水效的渲染方案。首先是一个等式,水效=波的模拟+折射+散射+反射+菲涅尔效应+高光。对此有疑惑的同学请移步Unity3D-实现水面渲染》

在之前,我们使用Gerstner+法线贴图来模拟水波,里面用了一堆的公式来计算每个顶点,而且还需要叠加8次,这是一笔非常昂贵的开销,我们考虑在移动平台去掉Gerstner波的计算,而仅仅只留下法线贴图来模拟水波。这样做了之后,你会发现实际上水面已经不再起伏了。

至于折射和散射,之前我们使用了额外的一个相机来渲染折射图和散射图,这会导致额外的渲染批次,而且即使我们计算散射时是用的指数雾的公式,这也会带来比较高的性能消耗,毕竟散射是在像素着色器上进行计算的。所以,在移动平台上,我准备把折射和散射全部都砍掉,而仅仅使用一张预先渲染好的折射+散射图来替代。

然后是反射,镜面反射也是需要额外的相机来渲染反射图的,而且反射的透视投影还需要经过斜平面裁剪。这里就需要做一点平衡,如果项目中并不需要实时的反射,那么这一部分其实也是可以删掉的,我们可以简单的用一个cubemap来替代,具体细节在后面进行说明。

接着是菲涅尔效应,《3D游戏与计算机图形学》中有一个计算菲涅尔效应的5次多项式:

理所当然的,它对移动平台很不友好,所幸的是F值的计算结果是一个[0,1]之间的数,我们可以预先计算出F值,然后将结果存储在一张贴图上,仅仅只需要损失一点精度,我们就可以通过VH来获取到F值(实际中使用VN效果比较好)。

最后就是高光了,高光还是继续使用Blinn-phong光照模型,它的开销算是比较小的了。如果不想用Blinn-phong或者觉得它效果不好,也可以考虑使用MatCap方法来计算高光。当然,在这里我就直接保留了Blinn-phong高光公式来计算高光。

先来一张最后的效果图:

效果还是过的去的,而且更为重要的是,它对硬件的要求非常的低,你尽可以在七八百块钱的低端安卓机上毫无压力地渲染水面。

三、实现

首先是让美术制作一张结合了折射和散射的水面图,比如下面这张:

得到这张底图后,使用一个法线贴图来对它进行扰动,以模拟水波的效果:(下面代码是在像素着色器内的)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
half3 PerPixelNormal(sampler2D bumpMap, half4 coords, half bumpStrength)
{
    float2 bump = (UnpackNormal(tex2D(bumpMap, coords.xy)) + UnpackNormal(tex2D(bumpMap, coords.zw))) * 0.5;
    //bump += (UnpackNormal(tex2D(bumpMap, coords.xy*2))*0.5 +   UnpackNormal(tex2D(bumpMap, coords.zw*2))*0.5) * 0.5;
    //bump += (UnpackNormal(tex2D(bumpMap, coords.xy*8))*0.5 + UnpackNormal(tex2D(bumpMap, coords.zw*8))*0.5) * 0.5;
    float3 worldNormal = float3(0,0,0);
    worldNormal.xz = bump.xy * bumpStrength;
    worldNormal.y = 1;
    return worldNormal;
}
  
half3 worldNormal = normalize(PerPixelNormal(_BumpTex, i.bumpCoords, _BumpStrength));
  
half2 offsets = worldNormal.xz*viewVector.y;
half4 refractColor = tex2D(_RefractTex, i.uv.xy+offsets*_Params.y)*_Color;

   其中_BumpTex就是法线贴图,一般来说它长下面这个样子:

这是一张高度图,导入U3D后,需要将它的Texture Type设置为Normal,并且勾上Create from grayscale,这样U3D就会将高度图转换为法线贴图:

bumpCoords是法线贴图的uv,我们在PerPixelNormal中进行了两次纹理查询,第一次使用bumpCoords.xy,第二次使用bumpCoords.zw,这样两个不同方向,不同大小的波叠加起来,会使我们的水波看起来比较真实。计算bumpCoords的代码是在顶点着色器中进行的:

?
1
2
3
4
half3 worldPos = mul(_Object2World, v.vertex);
 
o.bumpCoords.xyzw = (worldPos.xzxz + _Time.yyyy * _BumpDirection.xyzw) * _BumpTiling.xyzw;
o.viewVector = (worldPos - _WorldSpaceCameraPos.xyz);

然后使用offsets来对底图_RefractTex进行扰动,就得到下图:

 

(未扰动的原图)

(扰动后的底图)

 

计算反射时,就不再做实时的镜面反射了。首先由美术制作一张skybox

将之导入U3DTextureType设置为Cubemap,这样它就成了下图:

然后,我们通过上面已经得到的worldNormal来计算出反射向量,并且用反射向量在Cubemap上做纹理查询:

?
1
2
half3 reflUV = reflect( viewVector, worldNormal);
half3 reflectColor = texCUBE(_Skybox, reflUV);

接着计算VNviewVector是顶点到相机的单位向量,N是法线向量,用VN_FresnelTex上查询fresnel值。

?
1
2
half2 fresnelUV = half2( saturate(dot(-viewVector, worldNormal)), 0.5);
half fresnel = tex2D(_FresnelTex, fresnelUV).r;

fresnel值来混合折射和反射:

?
1
result.xyz = lerp(refractColor.xyz, reflectColor.xyz, fresnel);

这样我们就得到一张效果不错的水效果:

最后,别忘了高光:

?
1
2
half3 specularColor = _Specular.w*pow(max(0, dot(worldNormal, halfVector)), _Params.x);
result.xyz += _Specular.xyz*specularColor;

其中半角向量halfVector是光照方向和视线方向之和:

?
1
half3 halfVector = normalize((normalize(_SunDir.xyz)-viewVector));

如此就得到我们的最终水效果:

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引