Unity官方示例UnityWaterSurface分析

来源:互联网 发布:社会工程学数据库 编辑:程序博客网 时间:2024/06/14 02:25

Unity官方示例UnityWaterSurface分析

Unity为我们提供了一个使用CustomRenderTexture的简单示例,利用RenderTexture来存储上一此以及上上一次的水面像素点的波幅,来计算下一次该像素点的波幅,用来模拟雨滴掉落水面时的涟漪。示例程序公布在GitHub上:https://github.com/hecomi/UnityWaterSurface

场景总中包含了一个平面和一个Cube。平面用来模拟水面,Cube其实没什么用,仅仅是表现这个平面是半透明的(水面Shader并没有实现折射的效果,如果想实现也不难,使用一个GrabPass抓取平面后面的图像然后做一个偏移就可以模拟出折射的效果,感兴趣的话可以参考一些教程自己实现)

场景使用到的资源也很简单,包括一张用来初始化CustomRenderTexture的纹理(原始程序是中间有一个红圆点的,后面会分析为什么事红圈,我寄几手欠,改成全黑的了)。一张是我们用来保存水面波幅的CustomRenderTexture。两个材质,一个是WaterSurface的材质,应用在Plane上,用来模拟水面涟漪。一个是WaterSimulation材质,用在纹理WaterSimulation上,用来计算并更新水面像素点的波幅保存在一张名为WaterSimulation的CustomRenderTexture里面。名为WaterSurface的Shader正是使用这张CustomRenderTexture作为输入计算出睡眠的顶点偏移和表面法向量,以模拟出水面涟漪的效果。脚本WaterSimulation是用来控制CustomRenderTexture的更新的。

这里写图片描述

水面涟漪如何扩散

首先来讲一讲雨滴掉落水面产生涟漪的理论基础:雨滴坠落水面造成的涟漪是由于对水表面的某一点产生了压力,该店的位置由于力的作用产生了为孩子的偏移,由于水分子之间的张力,带动了其周围的水分子产生位移,四周的水分子的位移同时又会反过来作用于中心点本身,这样循环,产生视觉上的扩散波纹。如果从理论物理的角度进行模拟将会使用大量的流体力学公式,产生庞大的计算量和实现的复杂度,所以我们从数学的角度,参考理论,来使用一种近似的模拟方法,使视觉上达到较为逼真的效果,毕竟从游戏和特效的角度看,如果它看起来很像,那么它就是正确的,很多时候,真正能达到性能和效果双重目标的往往是一些简单的经验公式。
我们假设每一个像素下一次的震动波幅,是受到其自身本时刻的震动方向、以及周围上下左右四个像素的位置的影响:本时刻的运动方向可以用本时刻的位置减去上一时刻的震动位置来表示,上下左右的四个像素对中心像素的影响也可以用他们和中心像素的震动位置的差来表示,那么下一时刻的位置可表示为:
这里写图片描述
为了方便地控制涟漪传播的速度,我们在计算四周的像素度中心的影响时加入了一个权重,作为扩散的速率,这个值越大,那么像素受到四周像素振幅的影响越大,视觉上涟漪传播的速度越快:
这里写图片描述
(官方代码中对speed的注释里写的是PhaseVelocity^2指的也是相位移动速率的平方,是震动波理论中使用的概念,感兴趣的可以自己google:“wave phaseVelocity”关键字,这里我们就理解为波纹扩散的速度)
最后由于各种阻力的影响,应当为结果增加一个能量衰减:
这里写图片描述
这样就得到了下一次该像素的震动位置。

WaterSimulation.cs

先看WaterSimulation的C#脚本:使用脚本根据需求设置CustomRenderTexture的更新次数等相关属性,配置更新区域。
声明一个CustomRenderTexture类型的成员:texture,并在属性面板上,在场景中将新建的 CustomRenderTexture拖进去。
[SerializeField]
CustomRenderTexture texture;

这个整数表示每帧执行更新的次数
int iterationPerFrame = 5;

void Start()
{
texture.Initialize();
}
在Start()函数中初始化纹理

void Update()
{
//每帧开始前先将上次的更新区域列表清空(不是不更新了,会按照默认情况,使用属性面板中配置的pass对整张纹理更新,这里是名为Update的pass0,这也是能抽产生持续扩散的波纹的原因)
texture.ClearUpdateZones();
UpdateZones();//这个函数指定纹理的更新区域列表
texture.Update(iterationPerFrame);//每帧更新5次
}

//这个函数用来控制每一次更新时的更新区域,鼠标右键和左键不同带来的不同结果仅仅是该像素的初始振幅是向上还是向下(CustomRenderTexture中该像素的值是正是负),这在更新shader中可以看到。
void UpdateZones()
{
bool leftClick = Input.GetMouseButton(0);
bool rightClick = Input.GetMouseButton(1);
//如果什么都没有点直接返回,不对CustomRenderTexture做任何更新
if (!leftClick && !rightClick) return;
//如果点了,找到点击的位置对应的像素
RaycastHit hit;
var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
//用于渲染模拟水面纹理CustomRenderTexture的shader一共有3个Pass可选:0(纹理中的波纹扩散),1(左键),2(右键)
var defaultZone = new CustomRenderTextureUpdateZone();
defaultZone.needSwap = true;
defaultZone.passIndex = 0;
defaultZone.rotation = 0f;
defaultZone.updateZoneCenter = new Vector2(0.5f, 0.5f);
defaultZone.updateZoneSize = new Vector2(1f, 1f);
var clickZone = new CustomRenderTextureUpdateZone();
clickZone.needSwap = true;
clickZone.passIndex = leftClick ? 1 : 2;
clickZone.rotation = 0f;
clickZone.updateZoneCenter = new Vector2(hit.textureCoord.x, 1f - hit.textureCoord.y);
clickZone.updateZoneSize = new Vector2(0.01f, 0.01f);
texture.SetUpdateZones(new CustomRenderTextureUpdateZone[]{ defaultZone, clickZone});
}
}
上面模拟更新涟漪扩散的是pass0,其updateZone区域是整个纹理,pass1和pass2用来模拟点击位置的振幅更新,仅仅是给CustomRenderTexture的当前点击位置像素的r值赋值为1或者-1,其updateZones设置为点击的位置附近的0.01大小的正方形区域(在结果中看还是很明显的)。后面的模拟涟漪扩散的工作是由pass0(名为Update)完成的。

WaterSimulation.shader

下面我们来分析一下更新CustomRenderTexture使用的Shader:WaterSimulation.shader
CustomRenderTexture中存储了像素当前的振幅信息,用r通道保存,g通道保存了上一次的振幅结果。这就是为什么初始化纹理会有红色原点的原因。
从结构上看,我么可以轻易的发现这个shader遵守了我们官方文档手册中的一些规定,使用了v2f_customrendertexture结构体和内置的顶点着色器函数:CustomRenderTextureVertexShader。
他有三个Pass,Pass0、Pass1和Pass2分别命名为:Update,LeftClick和RightClick。他们的区别在于片元着色器函数:
正如我们上面分析的那样,Pass0就根据四周的像素振幅以及自身本时刻的震动方向来计算下一次该像素的振幅值,从而模拟涟漪扩散的:
关键代码及注释如下:
float4 frag(v2f_customrendertexture i) : SV_Target
{
float2 uv = i.globalTexcoord;//获取CustomRenderTexture纹理采样坐标

    float du = 1.0 / _CustomRenderTextureWidth;    float dv = 1.0 / _CustomRenderTextureHeight;    float3 duv = float3(du, dv, 0) * _DeltaUV;//纹理坐标偏移    float2 c = tex2D(_SelfTexture2D, uv);//对上一次double buffer内交换前的CustomRenderTexture进行采样    //这个公式计算得到当前像素的上下波动值(根据上一次其四周的像素波动和上上次(c.g)的波动值进行计算)_S2就是扩散速度,_Atten就是能量衰减,导致振幅逐渐减小    float p = (2 * c.r - c.g +         _S2 * (tex2D(_SelfTexture2D, uv - duv.zy).r +                tex2D(_SelfTexture2D, uv + duv.zy).r +                tex2D(_SelfTexture2D, uv - duv.xz).r +                tex2D(_SelfTexture2D, uv + duv.xz).r -                 4 * c.r))                  * _Atten;    return float4(p, c.r, 0, 0);//用p值更新r通道,g通道保存上一次该像素的波动高度,}

在Pass1和Pass2中的偏远函数就简单多了,都只有一句,让点击位置的振幅为1或者-1,保存在CustomRenderTexture的r通道中:
float4 frag_left_click(v2f_customrendertexture i) : SV_Target
{
return float4(-1, 0, 0, 0);
}

float4 frag_right_click(v2f_customrendertexture i) : SV_Target
{
return float4(1, 0, 0, 0);
}

WaterSurface.shader

下面我们来看看如何根基存储振幅的CustomRenderTexture来渲染出波纹扩散的效果。实际上非常简单:
先看编译指令:
pragma surface surf Standard alpha addshadow fullforwardshadows vertex:disp tessellate:tessDistance
在WaterSurface.shader中,通过编译指令指明了这是一个表面着色器,使用标准光照模型,添加阴影,指定了自定义的顶点阶段函数disp,指定了曲面细分函数:tessDistance
sampler2D类型的_DispTex变量就是我们实时更新的CustomRenderTexture。
对于输入,我们只需要得到_DispTex的纹理坐标即可:
struct Input
{
float2 uv_DispTex;//位移纹理(CustomRenderTexture)坐标
};
下面定义了编译指令中指定的自定义顶点函数,根据CustomRenderTexture进行顶点偏移,模拟水面顶点震动的顶点动画
void disp(inout appdata v)
{
float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;//采样值乘以偏移系数
v.vertex.xyz += v.normal * d;//沿法线方向偏移d
}

表面函数很简单,代码和相关注释如下:
void surf(Input IN, inout SurfaceOutputStandard o)
{
o.Albedo = _Color.rgb;//水面主色调
o.Metallic = _Metallic;//金属质感(原理参考Unity手册PBR光照相关理论)
o.Smoothness = _Glossiness;//高光光滑度

//将CustomRenderTexture的r值截断在(0,1)之间,再映射到(0.5,1)的范围内,再与_Color的透明度相乘混合,CustomRenderTexture中的r值越大,alpha越大,越不透明。
o.Alpha = _Color.a * (0.5 + 0.5 * clamp(tex2D(_DispTex, IN.uv_DispTex).r, 0, 1));

//对CustomRenderTexture像素上下左右的点的y值分别采样,上减下得到法线的x方向,左减右得到法线的y方向,并归一化
float3 duv = float3(_DispTex_TexelSize.xy, 0);
half v1 = tex2D(_DispTex, IN.uv_DispTex - duv.xz).y;
half v2 = tex2D(_DispTex, IN.uv_DispTex + duv.xz).y;
half v3 = tex2D(_DispTex, IN.uv_DispTex - duv.zy).y;
half v4 = tex2D(_DispTex, IN.uv_DispTex + duv.zy).y;
o.Normal = normalize(float3(v1 - v2, v3 - v4, 0.3));
}
最终的结果如官方视频:
https://www.youtube.com/watch?v=jclxfdS3a3w

使用CustomRenderTexture可以模拟出很多复杂的特效,比如粘稠液体在物体表面的流动等等。有时间我会使用CustomRenderTexture将英伟达开发者论坛上一个HLSL实现的BloodShader(使用纹理保存表面每个像素的流量和重力图来计算下一时刻该像素流进的流量和流出的流量),移植到Unity上看看效果。

由于本人水平有限,外加时间紧张,文中难免有理解错误和解释不到位的地方,欢迎大家留言发问或批评指正。

原创粉丝点击