(译)波纹效果程序解构

概述:

在计算机图形学的一众效果中,水波效果无疑非常能引起观众眼球。这是一种模拟了水在受到干扰时的行为。

本文由两部分组成。第一部分讲述如何模拟水的行为。第二部分介绍光线到达透明表面时如何折射。通过这两部分知识可以写出效果瞩目的程序。

第 1 部分 - 如何模拟波

这种效果背后的机制很简单,简单到我甚至觉得这是在做区域采样实验时被偶然发现的。不过在研究计算波浪模拟之前,先来聊一下什么是区域采样。

a. 区域采样

区域采样是计算机图形中非常常见的算法。假设有一张二维贴图,点(x, y) 受周围的点影响,如 (x+1,y)、(x-1、y)、(x,y+1) 和 (x,y-1)。波形模拟实际上要使用三个维度,稍后我会进行解释。

b. 区域采样案例:模糊效果

对贴图模糊处理很简单,你需要使用两张贴图,一张包含需要进行模糊的数据,一张保存处理后的结果。算法大致如下(使用五点采样):

ResultMap[x, y] := ( SourceMap[x, y] +
SourceMap[x+1, y] +
SourceMap[x-1, y] +
SourceMap[x, y+1] +
SourceMap[x, y-1] ) DIV 5

(x, y) 值取决于周围值的平均值,当然图片模糊算法没那么简单,不过思路大致如此。

波形模拟也是基于这个原理,但是(x, y)的计算方式略有不同。之前提到波形模拟需要用到三个维度,这第三个就是时间维度。换句话说,在模拟波浪计算时,我们得知道海浪在上一帧的数据,把他作为下一帧的输入。
这是实际的波形模拟的算法:

ResultMap[x, y] := (( CurrentSourceMap[x+1, y] +
CurrentSourceMap[x-1, y] +
CurrentSourceMap[x, y+1] +
CurrentSourceMap[x, y-1] ) DIV 2 ) -
PreviousResultMap[x, y]

当前帧中获取的上下左右四个值除以 2,结果是平均值的两倍,然后减去上一帧点(x, y)的值。图 a 和 b 解释了这段代码的原理

figure_a.gif

水平灰线表示波的平均高度。如果(x, y) 上一帧的值低于平均水平,则波将上升到平均水平,如图 a 所示

figure_b.gif

如果上一帧(x, y)高于平均值,如图 b 所示,则波将向平均值下降。

c. 阻尼

每次波浪上下运动时,其能量都会分布在一个不断增长的区域。这意味着波的幅度会下降,直到波变平。使用阻尼因子可以模拟这一点。从当前幅度中减去一定幅度或一定百分比的因数,以使高幅度快速消失,而低幅度缓慢消失。在下面的示例中,每次移动幅度都被减去十六分之一。

d. 波形模拟示例

下面的代码片段最初包含一些汇编代码,我用Pascal进行了重写,以便移植到其他语言或平台。

.
.
const
MAXX = 320; { 贴图的宽高 }
MAXY = 240;
DAMP = 16; { 阻尼系数 }

{ 定义 WaveMap[frame, x, y] 和 frame-indices }
var
WaveMap: Array[0..1, 0..(MAXX-1), 0..(MAXY-1)] of SmallInt;
CT, NW: SmallInt;

.
.
procedure UpdateWaveMap;
var
x,y,n: Smallint;
begin
{ 跳过边界用以区域采样 }
for y := 1 to MAXY-1 do begin
for x := 1 to MAXX-1 do begin
n := ( WaveMap[CT,x-1,y] +
WaveMap[CT,x+1,y] +
WaveMap[CT,x,y-1] +
WaveMap[CT,x,y+1] ) div 2 -
WaveMap[NW,x,y];

n := n - (n div DAMP);

WaveMap[NW,x,y] := n;
end;
end;
end;
.
.

执行此代码后,结果会被绘制到图像缓冲区。在第2部分会说明如何完成此操作。这里的重点是绘制到缓冲区后,需要将把当前图像数据和绘制结果进行交换用于下一次绘制迭代:

Temporary_Value := CT;
CT := NW;
NW := Temporary_Value;

 

e. 开始运动

上面的过程只是对波浪“模糊”处理。如何才能使整体动起来呢?确切地说,是通过降低波形图中的值来实现的。不受干扰的波形图仅包含零。要创建波浪,只需选择一个随机位置并更改值,如下所示:

WaveMap[x, y] := -100;

值越高,波浪越大。

第2部分-透明表面光线追踪

现在我们有了波形图,我们想对其进行一些乐趣。我们取一束光束,使其垂直穿过表面。因为水的密度比空气高,所以光束会朝着法线折射,因此我们可以计算出光束撞击到其下方的任何位置(例如,图像)。

首先,我们必须找出入射光与表面法线之间的夹角是多少(图c)。

figure_c.gif

在图c中,红线表示表面法线。穿过波形图的垂直线代表入射光,连接到垂直线的箭头是折射光束。如您所见,折射光束与表面法线之间的角度小于入射光束与表面法线之间的角度。

a. 确定入射光的角度

这是通过测量(x,y)与(x-1,y)之间以及(x,y)与(x,y-1)之间的高度差来完成的。这给了我们一个底数为1的三角形。角度等于arctan(高差/ 1)或arctan(高差)。查看图d进行解释:

refract.gif

在我们的情况下,计算表面法线和入射光之间的角度非常简单。如果我们绘制一个虚构的三角形(如此处红色所示),则只需确定alpha即可。当将y(高度差)除以x(1)时,我们得到alpha的切线。换句话说,高度差是alpha的切线,而alpha是ArcTan(height Difference)。

为了使您相信这实际上是表面法线和入射光之间的角度,我将红色三角形逆时针旋转了90度。如您所见,hypothenusa与表面法线平行。

接下来,我们计算折射。如果您还记得高中物理,那您就会知道:

[bquote]折射率= sin(入射光的角度)/ sin(折射光的角度)[/ bquote],
因此可以像这样计算折射光束的角度:

[bquote]折射光的角度= arcsin(sin (入射光的角度)/折射率)[/ bquote]
,其中折射率是水的折射率:2.0。

第三,我们需要计算折射光束击中图像的位置,或它相对于入射光束最初进入的位置的相对位置:

[bquote]位移= tan(折射光束的角度)*高度差[/ bquote]
透明表面光线跟踪示例
下面的代码片段没有经过优化,因为您会错过计算的所有重要细节

for y := 1 to MAXY-1 do begin
for x := 1 to MAXX-1 do begin
xDiff := Trunc(WaveMap[x+1, y] - WaveMap[x, y]);
yDiff := Trunc(WaveMap[x, y+1] - WaveMap[x, y]);

xAngle := arctan( xDiff );
xRefraction := arcsin( sin( xAngle ) / rIndex );
xDisplace := Trunc( tan( xRefraction ) * xDiff );

yAngle := arctan( yDiff );
yRefraction := arcsin( sin( yAngle ) / rIndex );
yDisplace := Trunc( tan( yRefraction ) * yDiff );

if xDiff < 0 then begin
{ Current position is higher - Clockwise rotation }
if yDiff < 0 then
newcolor := BackgroundImage[x-xDisplace, y-yDisplace]
else
newcolor := BackgroundImage[x-xDisplace, y+yDisplace]
end else begin
{ Current position is lower - Counterclockwise rotation }
if yDiff < 0 then
newcolor := BackgroundImage[x+xDisplace, y-yDisplace]
else
newcolor := BackgroundImage[x+xDisplace, y+yDisplace]
end;

TargetImage[x, y] := newcolor;
end;
end;

附件演示了这些效果。(ps:原文并没有附件 ⊙﹏⊙∥)

原文地址:https://www.gamedev.net/tutorials/_/technical/graphics-programming-and-theory/the-water-effect-explained-r915/ ,作者:Myopic Rhino

ps:文章是千禧年,所以别吐槽为啥作者用Pascal编写了,问就是 聪明的程序员 (σ°∀°)σ..:*☆ ,大家就当伪代码看好了~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章