解释水波特效处理

news2024/11/5 20:35:40

这篇博文译自以下这篇文章——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来获悉这如何影响水波。

figure-a
水平灰线表示水波的平均高度【译者注:这条线作为考察水波高度走势的基准线,而不是x轴。水平方向可以看作为位置,垂直方向为水波高度。水平方向各个点随时间变化上下起伏。】。如果在 (x, y) 的先前值比平均值要小,那么水波将向上升到平均水平,正如图a所示的那样。

figure-b
如果在 (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)。

figure-c
在图c中,红线表示表面法线。穿过水波图的垂直线表示入射光,而连接垂线的箭头是折射光线。正如你所能看见的那样,在折射光与表面法线之间的角度比入射光与表面法线之间的角度要小。


确定入射光的角度

这通过测量在 (x, y) 与 (x-1, y) 之间以及 (x, y) 和 (x, y-1) 的高度差来实现。这给了我们单位为1的三角形。角度为 arctan(高度差 / 1),或 arctan(高度差)。看图d来进行解释:

figure-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

上述代码完全由本人进行注释,若有错误或建议欢迎各位大侠指出~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/508793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ResourceManager HA 原理

简介 为了解决 Yarn 中 ResourceManager 的单点故障问题&#xff0c;在 Hadoop 2.4 中新增了 ResourceManager HA 的能力&#xff0c; 该文章基于 Hadoop 3.1.1 进行讲解。 1.1. 名词定义 全称简称备注ResourceManagerRmZookeeperZK ResourceManager Ha 架构 ResourceMana…

Linux shell编程 数组 ^ 数组排序

数组定义 数组内数据类型可以为数值也可以为字符串。 若字符串类型需要使用 " " 包含以免空格扰乱数组。 方法1 空格分隔直接定义数组 arr(10 20 30 40 50) arr1(zhangsan lisi wangwu) 方法2 指定元素下标定义&#xff0c;若跳过元素不设置会显示为空 arr([0]1…

Python 密码破解指南:10~14

协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【OpenDocCN 饱和式翻译计划】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 收割 SB 的人会被 SB 们封神&#xff0c;试图唤醒 SB 的人是 SB 眼中的 SB。——SB 第三定律 十、加…

震惊!如果患上植物神经紊乱,就会诱发胃肠神经功能紊乱!

植物神经系统和胃肠系统是人体内重要的调节系统&#xff0c;它们分别负责着许多生物过程的调控。当这两个系统出现紊乱时&#xff0c;会对人体健康产生不良影响。本文将从植物神经紊乱与胃肠神经功能紊乱的关系、症状、治疗办法和生活预防方法四个方面进行探讨。 一、植物神经紊…

GoAccess 网站日志分析

GoAccess是一个开源且免费的网站日志分析和交互式WEB日志查看器&#xff0c;可在 Linux 系统的终端中或通过浏览器运行。使用它可让系统管理员视化的查看统计报告&#xff0c;这对于SEO以及运维来说非常有价值。 GoAccess支持几乎所有Web 日志格式&#xff0c;包含&#xff1a;…

数据结构-图的遍历和应用(DAG、AOV、AOE网)

目录 *一、广度优先遍历(BFS) 广度优先生成树 广度优先生成森林 *二、深度优先遍历 深度优先生成树 深度优先生成森林 二、应用 2.1最小生成树 *Prim算法 *Kruskal算法 2.2最短路径 *BFS算法 *Dijkstra算法 *Floyd算法 *2.3有向无环图(DAG网) *2.4拓扑排序(AOV网)…

Java之线程安全

目录 一.上节回顾 1.Thread类常见的属性 2.Thread类中的方法 二.多线程带来的风险 1.观察线程不安全的现象 三.造成线程不安全现象的原因 1.多个线程修改了同一个共享变量 2.线程是抢占式执行的 3.原子性 4.内存可见性 5.有序性 四.解决线程不安全问题 ---synchroni…

【Win32绘图编程,GDI绘图对象】绘图基础,位图处理,绘图消息处理,画笔,画刷,文本绘制

这一篇文章分享本人学习win32绘图编程&#xff0c;其中包括GDI绘图对象&#xff0c;绘图基础&#xff0c;基本图形的绘制&#xff0c;画笔画刷的使用&#xff0c;文本绘制&#xff0c;以及文本字体的更改。 文章目录 一.绘图基础1.BeginPaint函数2.EndPaint函数3.颜色的使用 二…

8 集群管理

8 集群管理 8.1 集群结构 ES通常以集群方式工作&#xff0c;这样做不仅能够提高 ES的搜索能力还可以处理大数据搜索的能力&#xff0c;同时也增加了系统的容错能力及高可用&#xff0c;ES可以实现PB级数据的搜索。 下图是ES集群结构的示意图&#xff1a; 从上图总结以下概念…

SSM整合详细教学(下)

SSM整合详细教学&#xff08;下&#xff09; 五、SSM整合页面开发1 准备工作2 列表查询功能3 添加功能4 修改功能5 删除功能 六、拦截器1 拦截器简介问题导入1.1 拦截器概念和作用1.2 拦截器和过滤器的区别 2 入门案例问题导入2.1 拦截器代码实现【第一步】定义拦截器【第二步】…

从零开始搭建高效的文件服务器:FastDFS与Nginx完美结合,内网穿透实现公网访问

目录 前言 1. 本地搭建FastDFS文件系统 1.1 环境安装 1.2 安装libfastcommon 1.3 安装FastDFS 1.4 配置Tracker 1.5 配置Storage 1.6 测试上传下载 1.7 与Nginx整合 1.8 安装Nginx 1.9 配置Nginx 2. 局域网测试访问FastDFS 3. 安装cpolar内网穿透 4. 配置公网访问…

区间预测 | MATLAB实现QRBiLSTM双向长短期记忆神经网络分位数回归时间序列区间预测

区间预测 | MATLAB实现QRBiLSTM双向长短期记忆神经网络分位数回归时间序列区间预测 目录 区间预测 | MATLAB实现QRBiLSTM双向长短期记忆神经网络分位数回归时间序列区间预测效果一览基本介绍模型描述程序设计参考资料 效果一览 进阶版 基础版 基本介绍 MATLAB实现QRBiLS…

C语言介绍

C语言的简洁 C语言仅有32个关键字、9种控制语句、34种运算符即可实现无数的功能。 关键字 可省略的关键字&#xff1a;auto、extern、signed. 复合类型的关键字&#xff1a;enum、struct、union. include include表示导入&#xff0c;include可以导入任意的文件。 比如#in…

Windows消息,消息循环的处理,消息队列,键盘消息,鼠标消息,定时器消息

上一章节中我们带大家编写了第一个Windows程序&#xff0c;并且带大家学习了注册窗口&#xff0c;创建窗口&#xff0c;这一章中我们来学习Windows消息&#xff0c;学习对消息循环处理的原理&#xff0c;并且带领大家学习一些常见的消息。 文章目录 一.消息基础1.消息概念及其作…

AOF 持久化详解

文章目录 AOF 相关配置AOF 文件的修复AOF 文件格式RESP 协议查看 AOF 文件清单文件 AOF RewriteRewrite 策略手动Rewrite自动Rewrite AOF 持久化过程AOF优缺点AOF与RDB混合持久化 AOF (Append Only File) 是把所有对内存进行修改的指令&#xff08;写操作&#xff09;以独立日志…

《花雕学AI》用AI创造清晨的美好:ChatGPT+DALL-E 2 生成“早上好”的场景图

早晨是一天中最美好的时刻&#xff0c;也是最适合与AI对话的时刻。想象一下&#xff0c;当你醒来&#xff0c;打开手机&#xff0c;就能看到一个AI为你生成的“早上好”的场景图&#xff0c;是不是很温馨&#xff1f;这就是ChatGPTDALL-E 2&#xff08;新Bing&#xff09; 的魅…

Elevate:全世界最小的介入泵,融资五千万美金

近日&#xff0c;以色列医疗器械公司Magenta Medical宣布完成5500万美元的C轮融资&#xff0c;该公司主要产品Elevate是目前全球最小的心脏介入泵&#xff0c;主要用于治疗急性心力衰竭患者和高危PCI的辅助。该泵采用了创新的设计和材料&#xff0c;可以通过桡动脉插入&#xf…

哪款远程控制软件可以远程玩游戏?

远程控制软件可以让你在不同设备之间进行远程控制。许多人可能会想知道&#xff0c;哪款远程控制软件可以在远程玩游戏时享受更好的游戏体验。 首先&#xff0c;在寻找适合远程游戏的远程控制软件之前&#xff0c;我们需要知道什么是远程游戏和远程控制。 远程游戏是一种允许玩…

第十二章_Redis单线程 VS 多线程

Redis为什么选择单线程&#xff1f; 是什么 这种问法其实并不严谨&#xff0c;为啥这么说呢? Redis的版本很多3.x、4.x、6.x&#xff0c;版本不同架构也是不同的&#xff0c;不限定版本问是否单线程也不太严谨。 1 版本3.x &#xff0c;最早版本&#xff0c;也就是大家口口相…

一文读懂 DNS 解析

导读 文章为“一文读懂域名与网站系列”第二篇&#xff0c;上篇文章主要介绍了域名的注册、建站和管理&#xff0c;通过本文你可以了解以下几个问题&#xff1a; 域名的结构、常用解析记录的类型 DNS 解析的过程 DNS 解析拓展知识 众所周知&#xff0c;互联网中的地址其实是…