最近需要做一个Unity的项目,2D的,需要实现水的效果,这在3D中资料很多,随便找个Shader就能用了,但是对于2D资料却非常少,表示非常伤心。找到个Unity实现2D水纹的package,可见How to do 2d water ripple?
链接里的工程用到了一个前辈用java实现的水纹效果的思想,具体理论来自2D Water
当然,个人对于是否在项目中使用这种算法是有疑虑的,对于并不需要模拟非常真实的水面的需求来说,这种大量的计算很消耗性能,如果用CPU计算,低端安卓手机的帧率连10帧/s都达不到,GPU的话还没测试,但比较传统的实现方式,这种通过大量计算来实现效果的方案应该还是比较耗费性能的。
翻译部分
这个算法是如何实现的?
首先,我们需要2列整形数组(不是字节)(感觉翻译成2维数组更好一点,理解算法应该用2维数组),用来记录水的状态。一个记录当前状态,一个记录上一帧的状态。为什么要用到两个队列呢?因为我们需要知道谁是如何从上一帧变化到这一帧的。
//阻尼:0和1之间的数
damping = some non-integer between 0 and 1
begin loop
//对不是边上的每个元素进行操作
for every non-edge element:
loop
Buffer2(x, y) = (Buffer1(x-1,y) +
Buffer1(x+1,y) +
Buffer1(x,y+1) +
Buffer1(x,y-1)) / 2 - Buffer2(x,y)
Buffer2(x,y) = Buffer2(x,y) * damping
end loop
Display Buffer2
Swap the buffers
end loop
Velocity(x, y) = -Buffer2(x, y)
想象一个播在一个一维表面传播,传播到左边。竖直方向的小箭头表明水位随时间变化的速度。淡的波显示了之前帧的位置,我们怎么才能海阔的每个波(竖直箭头)高度的正确变化呢?
你可以注意到两帧前的波的高度和箭头的大小成正比。因此只要有前两帧的记录,就很容易计算出波的每一部分的高度变化。我们来看下代码,当循环开始的时候,缓冲器1包含前一帧水的状态(wave1),缓冲器2则包含更前一帧的状态,因此缓冲器2有波的垂直速度信息。
波的传播也很重要,所以每一帧都得对缓冲进行处理以使其平滑。
Smoothed(x,y) = (Buffer1(x-1, y) +
Buffer1(x+1, y) +
Buffer1(x, y-1) +
Buffer1(x, y+1)) / 4
现在,用这两个缓冲来计算新产生的水的高度。乘以2减少了速度的效果。
NewHeight(x,y) = Smoothed(x,y)*2 + Velocity(x,y)
最后,水纹是会损耗能量的,所以需要增加阻尼:
NewHeight(x,y) = NewHeight(x,y) * damping
注意:当n是2的幂的时候
优化后得到:
NewHeight(x,y) = NewHeight(x,y) - (NewHeight(x,y)/n)
所以,当需要整合的时候。应该用一些比较快的代码,这在C的内循环中是很重要的:
void ProcessWater(short *source, short *dest)
{
int i;
for (i=320; i<64000-320; i++)
{
dest[i] = (
((source[i-1]+
source[i+1]+
source[i-320]+
source[i+320]) >>1) ) -dest[i];
dest[i] -= (dest[i] >> 5);
}
}
渲染水
缓冲器包含每个像素的水的高度的信息,有很多方法可以用来选人一个高度域,在这个例子中,我们使用简单高效的方法:利用阴影和反射。你需要一张水下的纹理,所以你能看到反射。
for every pixel (x,y) in the buffer
Xoffset = buffer(x-1, y) - buffer(x+1, y)
Yoffset = buffer(x, y-1) - buffer(x, y+1)
Shading = Xoffset
t = texture(x+Xoffset, y+Yoffset)
p = t + Shading
plot pixel at (x,y) with colour p
end loop
**附:**Unity Assets Store中有个利用贴图实现水波效果的插件不错,手机上也能完美运行Easy Water