这篇博文译自以下这篇文章——The Water Effect Explained
由于这篇文章主要用Pascal语言进行描述的。因此我后面会添加一些注释,并结合Apple提供的ripple相关的Demo给出一些额外的遵守GNU11规范的C代码。
介绍
在计算机图形中的许多特效中,水特效是一种完全抓取观众注意的效果。它模拟了水在被外界干扰时的行为。
这篇文章由两部分组成。第一部分介绍了水的行为如何被模拟。第二部分描述了当光照射到透明的表面时,你可以如何计算光的折射。它们一起为你提供了对一个抓取视线模拟程序的知识。
第1部分-水波如何被模拟
隐藏在这种特效后的机制非常简单。它太简单了,以至于我相信它是在对区域采样的实验中偶然被发明的。但在我深入水波模拟背后的计算之前,我将告诉你一些关于区域采样的知识。
区域采样
区域采样在计算机图形学中是一种非常普遍的算法。考虑一个二维图,在 (x, y) 处的值受 (x, y) 位置的周围值的影响,诸如 (x+1, y),(x-1, y),(x, y+1),以及 (x, y-1)。我们的水波模拟实际上在三个维度上工作,但我们将在后面谈到这点。
区域采样例子:一个简单的模糊
将一个图进行模糊非常简单。你将需要两个图:一个含有你想要模糊的数据,一个用于生成结果图。算法(使用五个样本值)看上去像以下形式:
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) 的像素值由其周围5个点的像素值的算术平均数计算得到。】当然,当你想要模糊图像时事情会变得有一点复杂,不过你获得了这种想法。
创建一个水波模拟基本上是相同的,但是 (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来获悉这如何影响水波。
水平灰线表示水波的平均高度【译者注:这条线作为考察水波高度走势的基准线,而不是x轴。水平方向可以看作为位置,垂直方向为水波高度。水平方向各个点随时间变化上下起伏。】。如果在 (x, y) 的先前值比平均值要小,那么水波将向上升到平均水平,正如图a所示的那样。
如果在 (x, y) 处的先前的值比平均值高,那么正如图b所示的那样,水波将下降到平均水平。
阻尼
一个水波每次上下移动时,其能量会分布在一个扩展区域上。这意味着水波的振幅一直下降直到水波达到平衡【译者注:即水面恢复平静】。我们可以使用一个阻尼系数来模拟这种情况。该因子,振幅的某个百分量,从当前的振幅减去以让高振幅快速消失,并且低振幅缓慢消失。在以下例子中,当每次水波移动时,振幅的十六分之一被减去。
水波模拟例子
下列代码片段一开始包含了某个内联汇编器,但我用本地的Pascal代码代替它了,这样它可以更容易地被移植到任一语言以及任一平台。
const
MAXX = 320; { 水波图的宽度和高度 }
MAXY = 240;
DAMP = 16; { 阻尼系数 }
{ 定义水波图WaveMap[frame, x, y]以及帧索引 }
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;
不过 CT 和 NW 意思是什么呢?CT 和 NW 是指向不同水波图的变量。CT 是当前水波图,它含有我们需要生成新的水波图的数据,被NW所指。CT 和 NW 可以持有两个值,0和1,并且可以一直不能相同。因为我们在每次迭代后交换这两个图,新的水波图含有在当前水波图之前所生成的水波图的数据。我意识到这可能听上去复杂,但这并不是那样。
使它移动
上述过程简单地让水波平静下来。那么,我们如何能让整个水波移动呢?确切地说,是通过削减水波位图中的值。一个未受外界干扰的水波图仅包含零值。要创建一个水波,只要挑选一个随即位置并改变这个值,就像下面那样:
WaveMap[x, y] := -100;
值越大,水波越大。
第2部分——透明表面光照追踪
现在,我们有自己的水波图,我们想对它玩一些把戏。我们取一束光,让它垂直地照射穿过水表面。因为水比空气具有更高的密度,所以光线向表面发现进行折射,并且我们可以计算光束照射到哪儿,不管那底下是啥(比如一个图像)。
首先,我们需要知道在入射光与表面法线之间的角度是啥(图c)。
在图c中,红线表示表面法线。穿过水波图的垂直线表示入射光,而连接垂线的箭头是折射光线。正如你所能看见的那样,在折射光与表面法线之间的角度比入射光与表面法线之间的角度要小。
确定入射光的角度
这通过测量在 (x, y) 与 (x-1, y) 之间以及 (x, y) 和 (x, y-1) 的高度差来实现。这给了我们单位为1的三角形。角度为 arctan(高度差 / 1)
,或 arctan(高度差)
。看图d来进行解释:
计算表面法线与入射光之间的角度在我们的实例中非常简单。如果我们画一个假象的三角形,这里用红色表示,那么我们需要做的就是确定alpha。当我们用 x(为1) 去除 y(为高度差)时,我们就得到了 alpha 的正切。换句话说,高度差是 alpha 的高度差,并且 alpha 是 arctan(高度差)
。
为了要为你鉴证这个事实——这个实际上是表面法线与入射光之间的角度——我将红色三角形按逆时针旋转90度。正如你所看到的,斜边与表面法线平行。【译者注:这里其实也采用了微分方法。图d中斜边为图c中的正弦曲线上的一小段,水平方向取1个单位,相应获得水波图中两个相邻位置的水波高度差,即为图d中的直角边。这就非常容易证明入射光与法线的夹角与 alpha 是相等的——含有一个公共角的两个直角的邻角相等。】
下一步,我们计算折射角。如果你记得大学里的物理,那么你知道:
折射率 = sin(入射光的角度) / sin(折射光的角度)
这样,被折射光线的角度可以这么被计算出:
折射光的角度 = arcsin(sin(入射光的角度) / 折射率)
这里,折射率是水的折射率:2.0。
第三,我们需要计算折射光照射到图像哪里,或者它与入射光原始进入的地方的相对位置:
位移 = tan(折射光的角度) * 高度差
透明表面的光线追踪的例子
下列代码片段没有被优化,因为这样,你不会错过计算上的很多细节。
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
{ 当前位置为更高值 - 顺时针方向旋转 }
if yDiff < 0 then
newColor := BackgroundImage[x-xDisplace, y-yDisplace]
else
newColor := BackgroundImage[x+xDisplace, y+yDisplace]
end;
TargetImage[x, y] := newColor;
end;
end;
以下是Apple提供的一个水波纹理的Demo,里面有我写的一些注释,应该已经比较清楚了~🙂:
/*
File: RippleModel.m
Abstract: Ripple model class that simulates the ripple effect.
Version: 1.0
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms. If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple. Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Copyright (C) 2013 Apple Inc. All Rights Reserved.
*/
#import "RippleModel.h"
@interface RippleModel () {
unsigned int screenWidth;
unsigned int screenHeight;
unsigned int poolWidth; // 水平方向所要绘制的网格数
unsigned int poolHeight; // 垂直方向所要绘制的网格数
unsigned int touchRadius; // 手指触摸屏幕后初始的水波半径
unsigned int meshFactor; // 网格宽度(iPhone上默认设置为4;iPad上默认设置为8)
float texCoordFactorS; // 用于将纹理坐标规格化的水平方向上的单位宽度
float texCoordOffsetS; // 纹理水平方向的偏移;此偏移由于可能要针对高度做规格化而产生的位置偏差
float texCoordFactorT; // 用于将纹理坐标规格化的垂直方向上的单位高度
float texCoordOffsetT; // 纹理垂直方向的偏移;此偏移由于可能要针对宽度做规格化而产生的位置偏差
// ripple coefficients
float *rippleCoeff; // 水波系数表,实际长度为float[2*touchRadius+1][2*touchRadius+1]
// ripple simulation buffers
float *rippleSource; // 源水波
float *rippleDest; // 目的水波
// data passed to GL
GLfloat *rippleVertices; // 水波顶点坐标;每个元素为struct {float x, y;};类型
GLfloat *rippleTexCoords; // 水波纹理坐标;每个元素为struct {float s, t;};类型
GLushort *rippleIndicies;
}
@end
@implementation RippleModel
- (void)initRippleMap
{
// +2 for padding the border
memset(rippleSource, 0, (poolWidth+2)*(poolHeight+2)*sizeof(float));
memset(rippleDest, 0, (poolWidth+2)*(poolHeight+2)*sizeof(float));
}
// 在以(2 * touchRadius + 1)为边长的正方形的内切圆内计算各个像素点所对应的水波振幅系数
- (void)initRippleCoeff
{
// 一共(2 * touchRadius + 1)行
for (int y=0; y <= 2*touchRadius; y++)
{
// 每行有(2 * touchRadius + 1)个点
for (int x=0; x <= 2*touchRadius; x++)
{
// 当前点到圆心(touchRadius, touchRadius)的距离。
// 若当前点正好在圆心上,则distance为0。
float distance = sqrt((x-touchRadius)*(x-touchRadius)+(y-touchRadius)*(y-touchRadius));
if (distance <= touchRadius)
{
// 若当前点在内切圆的范围内,则计算该点的系数。
float factor = distance / touchRadius; // 该因子的取值范围是[0, 1]
// goes from -512 -> 0
// 赋值给当前点的系数。系数的确定是通过由中心点(touchRadius, touchRadius)作为起始点,在正方形内切圆范围内作cos波形扩散。
// 使用余弦是因为它是偶函数,正好与y轴(这里表示水波的振幅)对称。这里的余弦函数的取值范围是[-1, 1],并且正好是半个周期,由于distance的范围是[0, 1]。
// 这里可以看到使用-cos(factor * π)因为在起始点处(也就是手指点下去的那一点),初始波的振幅是向下(负方向)绝对值最大的。
// 然后获得的振幅加1,再乘以256,使得最终值定格在[-512, 0],用于量化。
rippleCoeff[y*(touchRadius*2+1)+x] = -(cos(factor*M_PI)+1.f) * 256.f;
}
else
{
// 内切圆边界外的系数设为0
rippleCoeff[y*(touchRadius*2+1)+x] = 0.f;
}
}
}
}
// 初始化网格
- (void)initMesh
{
// 先针对网格初始化顶点坐标以及纹理坐标
for (int i=0; i<poolHeight; i++)
{
for (int j=0; j<poolWidth; j++)
{
// v[i, j].x = j * (2 / (w - 1)) - 1; 将屏幕横坐标规格化到[-1, 1],第0列时为-1
rippleVertices[(i*poolWidth+j)*2+0] = -1.f + j*(2.f/(poolWidth-1));
// v[i, j].y = 1 - i * (2 / (h - 1)); 将屏幕纵坐标规格化到[-1, 1],第h-1行时为-1
rippleVertices[(i*poolWidth+j)*2+1] = 1.f - i*(2.f/(poolHeight-1));
// 这里的纹理宽高为640x480,而显示的时候以屏幕宽高(竖屏)方式展示,因此这里需要将纹理坐标做一个转置
// 使得s为垂直方向,t为水平方向。以下分别为水波网格中各个顶点设置相应的纹理坐标
rippleTexCoords[(i*poolWidth+j)*2+0] = (float)i/(poolHeight-1) * texCoordFactorS + texCoordOffsetS;
rippleTexCoords[(i*poolWidth+j)*2+1] = (1.f - (float)j/(poolWidth-1)) * texCoordFactorT + texCoordFactorT;
}
}
// 设置水波顶点索引;这里采用GL_TRIANGLE_STRIP方式渲染
// 由于iOS系统所支持的GPU支持前一条带的最后一点重复一次,后一条带第一个点重复一次能形成新的一个三角条带,所以以下的emit extra index就是做这个操作
unsigned int index = 0;
for (int i=0; i<poolHeight-1; i++)
{
for (int j=0; j<poolWidth; j++)
{
// 对于偶数行
if (i%2 == 0)
{
// emit extra index to create degenerate triangle
if (j == 0)
{
// 发射额外的索引来创建退化的三角形(多取一次(i, j)这一点)
rippleIndicies[index] = i*poolWidth+j;
index++;
}
// 取(i, j)点的位置
rippleIndicies[index] = i*poolWidth+j;
index++;
// 取(i+1, j)点的位置
rippleIndicies[index] = (i+1)*poolWidth+j;
index++;
// emit extra index to create degenerate triangle
if (j == (poolWidth-1))
{
// 发射额外的索引来创建退化的三角形(多取一次(i+1, j)这一点)
rippleIndicies[index] = (i+1)*poolWidth+j;
index++;
}
}
else // 对于奇数行
{
// emit extra index to create degenerate triangle
if (j == 0)
{
// 发射额外的索引来创建退化的三角形(多取一次(i+1, j)这一点)
rippleIndicies[index] = (i+1)*poolWidth+j;
index++;
}
// 取(i+1, j)点的位置
rippleIndicies[index] = (i+1)*poolWidth+j;
index++;
// 取(i, j)点的位置
rippleIndicies[index] = i*poolWidth+j;
index++;
// emit extra index to create degenerate triangle
if (j == (poolWidth-1))
{
// 发射额外的索引来创建退化的三角形(多取一次(i, j)这一点)
rippleIndicies[index] = i*poolWidth+j;
index++;
}
}
}
}
}
- (GLfloat *)getVertices
{
return rippleVertices;
}
- (GLfloat *)getTexCoords
{
return rippleTexCoords;
}
- (GLushort *)getIndices
{
return rippleIndicies;
}
- (unsigned int)getVertexSize
{
return poolWidth*poolHeight*2*sizeof(GLfloat);
}
- (unsigned int)getIndexSize
{
return (poolHeight-1)*(poolWidth*2+2)*sizeof(GLushort);
}
- (unsigned int)getIndexCount
{
return [self getIndexSize]/sizeof(*rippleIndicies);
}
- (void)freeBuffers
{
free(rippleCoeff);
free(rippleSource);
free(rippleDest);
free(rippleVertices);
free(rippleTexCoords);
free(rippleIndicies);
}
- (id)initWithScreenWidth:(unsigned int)width
screenHeight:(unsigned int)height
meshFactor:(unsigned int)factor
touchRadius:(unsigned int)radius
textureWidth:(unsigned int)texWidth
textureHeight:(unsigned int)texHeight
{
self = [super init];
if (self)
{
screenWidth = width;
screenHeight = height;
meshFactor = factor;
poolWidth = width/meshFactor;
poolHeight = height/meshFactor;
touchRadius = radius;
// 将纹理坐标规格化
// 这里的纹理宽高为640x480,而显示的时候以屏幕宽高(竖屏)方式展示,因此后期处理需要将纹理坐标做一个转置
if ((float)screenHeight/screenWidth < (float)texWidth/texHeight)
{
texCoordFactorS = (float)(texHeight*screenHeight)/(screenWidth*texWidth);
texCoordOffsetS = (1.f - texCoordFactorS)/2.f;
texCoordFactorT = 1.f;
texCoordOffsetT = 0.f;
}
else
{
texCoordFactorS = 1.f;
texCoordOffsetS = 0.f;
texCoordFactorT = (float)(screenWidth*texWidth)/(texHeight*screenHeight);
texCoordOffsetT = (1.f - texCoordFactorT)/2.f;
}
rippleCoeff = (float *)malloc((touchRadius*2+1)*(touchRadius*2+1)*sizeof(float));
// +2 for padding the border
rippleSource = (float*)malloc((poolWidth+2)*(poolHeight+2)*sizeof(float));
rippleDest = (float*)malloc((poolWidth+2)*(poolHeight+2)*sizeof(float));
rippleVertices = (GLfloat*)malloc(poolWidth*poolHeight*2*sizeof(GLfloat));
rippleTexCoords = (GLfloat*)malloc(poolWidth*poolHeight*2*sizeof(GLfloat));
rippleIndicies = (GLushort*)malloc((poolHeight-1)*(poolWidth*2+2)*sizeof(GLushort));
if (!rippleCoeff || !rippleSource || !rippleDest ||
!rippleVertices || !rippleTexCoords || !rippleIndicies)
{
[self freeBuffers];
return nil;
}
[self initRippleMap];
[self initRippleCoeff];
[self initMesh];
}
return self;
}
// 每次刷新视图时调用此方法
- (void)runSimulation
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// first pass for simulation buffers...
// 第一遍,用于计算水波模拟的目标值。以下操作一共执行poolHeight行
dispatch_apply(poolHeight, queue, ^(size_t y) {
// y从0到poolHeight-1
for (int x=0; x<poolWidth; x++)
{
// * - denotes current pixel
//
// a
// c * d
// b
// +1 to both x/y values because the border is padded
// 这里,当前点的坐标为(x+1, y+1)
float a = rippleSource[(y)*(poolWidth+2) + x+1];
float b = rippleSource[(y+2)*(poolWidth+2) + x+1];
float c = rippleSource[(y+1)*(poolWidth+2) + x];
float d = rippleSource[(y+1)*(poolWidth+2) + x+2];
// 这里的(a + b + c + d) / 2 - rippleDest其实是指:
// avg = (a + b + c + d) / 4; result = avg + (avg - rippleDest)
// 如果当前水波系数值比均值小,那么水波将从平均位置上升
// 如果当前水波系数值比均值大,那么水波将从平均位置下降
float result = (a + b + c + d)/2.f - rippleDest[(y+1)*(poolWidth+2) + x+1];
result -= result/32.f;
rippleDest[(y+1)*(poolWidth+2) + x+1] = result;
}
});
// second pass for modifying texture coord
// 第二遍,用于计算纹理坐标进行采样。以下操作一共执行poolHeight行
dispatch_apply(poolHeight, queue, ^(size_t y) {
// y从0到poolHeight-1
for (int x=0; x<poolWidth; x++)
{
// * - denotes current pixel
//
// a
// c * d
// b
// +1 to both x/y values because the border is padded
// 这里,当前点的坐标为(x+1, y+1)
float a = rippleDest[(y)*(poolWidth+2) + x+1];
float b = rippleDest[(y+2)*(poolWidth+2) + x+1];
float c = rippleDest[(y+1)*(poolWidth+2) + x];
float d = rippleDest[(y+1)*(poolWidth+2) + x+2];
// 所以这里除以2048再做一次针对纹理坐标偏移的规格化(512 * 4)
// 这里纹理是被转置90度的。b-a表征了横向水波的起伏趋势;
// c-d表征了纵向水波的起伏趋势;这里a与b以及c与d可以相互交换,即符号相反也没问题
float s_offset = ((b - a) / 2048.f);
float t_offset = ((c - d) / 2048.f);
// clamp
// 将纹理水平与垂直方向的偏移都确保在[-0.5, 0.5]范围内
s_offset = (s_offset < -0.5f) ? -0.5f : s_offset;
t_offset = (t_offset < -0.5f) ? -0.5f : t_offset;
s_offset = (s_offset > 0.5f) ? 0.5f : s_offset;
t_offset = (t_offset > 0.5f) ? 0.5f : t_offset;
// 获取当前正常的纹理坐标
float s_tc = (float)y/(poolHeight-1) * texCoordFactorS + texCoordOffsetS;
float t_tc = (1.f - (float)x/(poolWidth-1)) * texCoordFactorT + texCoordOffsetT;
// 真正获取所要采样的纹理坐标
rippleTexCoords[(y*poolWidth+x)*2+0] = s_tc + s_offset;
rippleTexCoords[(y*poolWidth+x)*2+1] = t_tc + t_offset;
}
});
// 这一步用来交换源水波与目的水波,使得当前的目的水波将作为后一帧的源水波
float *pTmp = rippleDest;
rippleDest = rippleSource;
rippleSource = pTmp;
}
// 在手指点的位置处设置rippleSource
- (void)initiateRippleAtLocation:(CGPoint)location
{
// 当前位置所对应的网格索引
unsigned int xIndex = (unsigned int)((location.x / screenWidth) * poolWidth);
unsigned int yIndex = (unsigned int)((location.y / screenHeight) * poolHeight);
// 以当前位置为圆心,touchRadius为半径,根据水波系数设置水波源
for (int y=(int)yIndex-(int)touchRadius; y<=(int)yIndex+(int)touchRadius; y++)
{
for (int x=(int)xIndex-(int)touchRadius; x<=(int)xIndex+(int)touchRadius; x++)
{
// 仅对在网格区域范围内的水波系数和水波源进行操作
if (x>=0 && x<poolWidth &&
y>=0 && y<poolHeight)
{
// +1 to both x/y values because the border is padded
// 以(xIndex - touchRadius, yIndex - touchRadius)作为起始点,依次获取这个圆范围内的每个点相应的水波系数
// 这个获取顺序与初始化水波的位置顺序一致
// 随后,将这些系数依次映射到水波源相应的网格位置中,并与原来的水波系数相加
rippleSource[(poolWidth+2)*(y+1)+x+1] += rippleCoeff[(y-(yIndex-touchRadius))*(touchRadius*2+1)+x-(xIndex-touchRadius)];
}
}
}
}
- (void)dealloc
{
[self freeBuffers];
}
@end
上述代码完全由本人进行注释,若有错误或建议欢迎各位大侠指出~