解釋水波特效處理


這篇博文譯自以下這篇文章——http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/the-water-effect-explained-r915

由於這篇文章主要用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

上述代碼完全由本人進行注釋,若有錯誤或建議歡迎各位大俠指出~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM