canvas實現(xiàn)水波紋效果

本文將會從水波的基本原理開始,詳細講解在canvas中模擬水波擴散,分析并計算水波的能量分布,并通過振幅模擬水波對圖像的折射效果,最后實現(xiàn)水波特效。

水波基本原理

首先復習一波高中物理知識。

波是指振動的傳播。波的傳播方向與質(zhì)點振動方向垂直的為橫波,相同則為縱波,水波是橫波和縱波的疊加。

對于水波這種波,我們在實現(xiàn)這個特效的時候,需要考慮到下面的特性:

  • 圓形波:當你投一塊石頭到水池中時,你會看到一個以石頭入水點為圓心所形成的一圈圈的水波
  • 反射:水波碰到墻壁后會反射
  • 衰減:因為水是有阻尼的,所以你會看到水波越往外擴散,越弱,最后消失,水面回復平靜
  • 水波使得圖像發(fā)生折射,由于水波,使得水面凹凸不平,會折射和反射水池中的圖像
  • 衍射,波在傳播中遇到有很大障礙物或遇到大障礙物中的孔隙時,會繞過障礙物的邊緣或孔隙的邊緣,呈現(xiàn)路徑彎曲,在障礙物或孔隙邊緣的背后展衍。

水波紋效果反映到圖像上,其本質(zhì)就是像素的偏移,相當于很多縮放的結合。因此對圖像的處理就轉化為如何移動圖像上的像素點,從而模擬和表現(xiàn)出水波紋的效果。下面是本文將會實現(xiàn)的水波紋特效:更好的效果頁面

http://asset.uusama.com/example/water_ripple.html

波幅計算

波幅表示方法

波的本質(zhì)是振動,然后傳遞能量,波的表現(xiàn)形式就是能量的分布情況,我們使用波幅(振動幅度)來描述每一點攜帶的能量。

假設一開始水面是平靜的,整個水面的能量均勻分布。我們知道在canvas中,我們可以使用ctx.getImageData(0, 0, width, height)方法將一幅寬為width,高為height的圖像像素信息存入一個數(shù)組中,這個數(shù)組大小為 width × height × 4 bytes(RGBA信息)。

我們可以建立兩個和圖像一樣大小 width × height的數(shù)組,用來保存水面上每一個點的前一時刻和后一時刻波幅數(shù)據(jù)。或者直接使用一個 2 × width × height的數(shù)組,分為前半部分和后半部分來保存前后時刻的波幅數(shù)據(jù)。

水面在初始狀態(tài)時是平靜的平面,各點的波幅都為0,所以,數(shù)組的所有初始值都等于0。

var width = settings.width,  // canvas寬度
      height = settings.height, // canvas高度
      amplitude_size = width * (height + 2) * 2, // 振幅數(shù)組大小
      ripple_map = [],  // 產(chǎn)生水波下一時刻振幅
      last_map = [];  // 初始時刻振幅
// 波幅數(shù)組初始化為0
for (var i = 0; i < amplitude_size; i++) {
    ripple_map[i] = last_map[i] = 0;
}

忽略阻尼計算振幅

由上面一小節(jié),我們可以用X_i來表示圖像中的任意一個像素點,其中i的值在0到 width × height之間,我們把寬度width簡記為W,將高度height簡記為H,則可以用下面的集合表示圖像上的像素點集合

如果你發(fā)現(xiàn)下面的公式顯示不正常,那么是解析器插件罷工了,請移步到這兒

\\{ X_i|0\le i \le WH \\}

  • 其中坐標為(x,y)的點為X_{yW+x}

由于波的傳播特性,某一點下一時刻的振動情況,受到周圍質(zhì)點的振動以及自身振動情況的聯(lián)合影響。為了使問題簡化,我們假設X_i點的振幅A_i除了受到自身的影響外,還受到來自它周圍前、后、左、右四個點(X_{i-W},X_{i+W},X_{i-1},X_{i+1})的影響,并且假設這四個點對X_i點的影響力機會均等并且線性疊加的。那么可以得到X_i點的振幅公式:

A_i^{\prime} = a(A_{i-W}+A_{i+W}+A_{i-1}+A_{i+1})+bA_i

  • A_i分別為點X_i當前時刻的振幅
  • a、b為待定系數(shù),A_0^{\prime}X_0點下一時刻的振幅
  • 對于圖像邊界上的點,需要進行特殊處理,可以適當增大振幅數(shù)組:(W+2)x(H+2)

假設水的阻尼為0。在這種理想條件下,水的總勢能將保持不變。也就是說在任何時刻,所有點的振幅的和保持不變。那么可以得到下面能量守恒公式:

\sum_{i=0}^n{A_i^{\prime}}=\sum_{i=0}^n{A_i}

將上面的X_i點的振幅公式帶入可得:

\sum_{i=0}^n{[a(A_{i-W}+A_{i+W}+A_{i-1}+A_{i+1})+bA_i]}=\sum_{i=0}^n{A_i}

拆開可得:

a \sum_{i=0}^n{A_{i-W}}+a \sum_{i=0}^n{A_{i+W}}+a \sum_{i=0}^n{A_{i-1}}+a \sum_{i=0}^n{A_{i+1}}+b \sum_{i=0}^n{A_i}=\sum_{i=0}^n{A_i}

其中可以近似的認為:

\sum_{i=0}^n{A_{i-W}}= \sum_{i=0}^n{A_{i+W}}= \sum_{i=0}^n{A_{i-1}}= \sum_{i=0}^n{A_{i+1}}= \sum_{i=0}^n{A_i}

等式兩邊消去可得:

4a+b=1

找出一個最簡解:a = \frac{1}{2}, b = -1

因為\frac{1}{2}可以用移位運算符“>>”來進行,不用進行乘除法,所以,這組解是最適用的而且是最快的。那么最后得到的下一時刻的振幅公式就是:

A_i^{\prime} =\frac{1}{2}(A_{i-W}+A_{i+W}+A_{i-1}+A_{i+1})-A_i

得到上面這個近似公式后,如果已知某一時刻水面上任意一點的波幅,就可以求出下一時刻水面上任意一點的波幅。

考慮阻尼

然而,在實際中是存在阻尼的,否則,用上面這個公式,一旦你在水中增加一個波源,水面將永不停止的震蕩下去。

所以,還需要對波幅數(shù)據(jù)進行衰減處理,讓每一個點在經(jīng)過一次計算后,波幅都比理想值按一定的比例降低。這個衰減率經(jīng)過測試,用\frac{1}{32}比較合適,也就是\frac{1}{2^5},可以通過移位運算很快的獲得。

最后的振幅計算算法如下:

// 計算下一時刻波幅,index為像素點位置,old_amplitude為上一時刻該點波幅
function calculAmplitude(index, old_amplitude) {
    var x_boundary = 0, judge = map_index % width;
    // 由于波幅數(shù)據(jù)順序存儲,加上左右邊界檢查,避免左邊水波傳遞到右邊
    if (judge == 0) {
        x_boundary = 1; // 左邊邊界
    }else if (judge == width - 1) {
        x_boundary = 2; // 右邊邊界
    }
    var top = ripple_map[index - width],// 上邊的相鄰點
        bottom = ripple_map[index + width],// 下邊的相鄰點
        left = x_boundary != 1 ? ripple_map[index - 1] : 0,// 左邊的相鄰點
        right = x_boundary != 2 ? ripple_map[index + 1] : 0;// 右邊的相鄰點
    // 計算當前像素點下一時刻的振幅
    var amplitude = top + bottom + left + right;
    amplitude >>= 1;
    amplitude -= old_amplitude;
    amplitude -= amplitude >> 5;  // 計算衰減
    return amplitude;
}

頁面渲染

因為水的折射,當水面不與我們的視線相垂直的時候,我們所看到的水下的景物并不是在觀察點的正下方,而存在一定的偏移。

偏移的程度與水波的斜率,水的折射率和水的深度都有關系,如果要進行精確的計算的話,顯然是很不現(xiàn)實的。同樣,我們只需要做線形的近似處理就行了。

因為水面越傾斜,所看到的水下景物偏移量就越大,最簡單的做法可以近似的用水面上某點的前后、左右兩點的波幅之差來代表所看到水底景物的偏移量。

這里我們選用畫面的中點作為參考點來計算視覺的偏移。

我們將原始圖像的像素信息保存在兩個數(shù)組中,一個用于保存原始圖像數(shù)據(jù),一個用于實時保存實時渲染數(shù)據(jù)。這里需要注意更新圖像的時候,圖像的恢復問題,這里我們用一個反相器來進行恢復,一個點偏移了,我們給它一個反方向的偏移來抵消就可以恢復。

根據(jù)偏移量將原始圖象上的每一個象素復制到渲染頁面上,將渲染數(shù)據(jù)繪制到canvas中即可。

// 渲染下一幀
function renderRipple() {
    var i = old_index,
        deviation_x,  // x水平方向偏移
        deviation_y,  // y豎直方向偏移
        pixel_deviation, // 偏移后的ImageData對象像素索引
        pixel_source;  // 原始ImageData對象像素索引

    // 交互索引 old_index, new_index
    old_index = new_index;
    new_index = i;

    // 設置像素索引和振幅索引
    i = 0;
    map_index = old_index;

    // 渲染所有像素點
    for (var y = 0; y < height; y++) {
        for (var x = 0; x < width; x++) {
            // 計算當前像素點下一時刻的振幅
            var amplitude = calculAmplitude(map_index, ripple_map[new_index + i]);

            // 更新振幅數(shù)組
            ripple_map[new_index + i] = amplitude;

            amplitude = 1024 - amplitude;
            var old_amplitude = last_map[i];
            last_map[i] = amplitude;

            if (old_amplitude != amplitude) {
                 // 計算偏移
                deviation_x = (((x - half_width) * amplitude / 1024) << 0) + half_width;
                deviation_y = (((y - half_height) * amplitude / 1024) << 0) + half_height;

                // 檢查邊界
                if (deviation_x > width) {
                    deviation_x = width - 1;
                }
                if (deviation_x < 0) {
                    deviation_x = 0;
                }
                if (deviation_y > height) {
                    deviation_y = height - 1;
                }
                if (deviation_y < 0) {
                    deviation_y = 0;
                }
                
                // 計算imageData中對應的像素RGBA偏移位置
                pixel_source = i * 4;
                pixel_deviation = (deviation_x + (deviation_y * width)) * 4;

                // 移動像素的RGBA信息,ripple和texture為背景圖的ImageData對象
                ripple.data[pixel_source] = texture.data[pixel_deviation];
                ripple.data[pixel_source + 1] = texture.data[pixel_deviation + 1];
                ripple.data[pixel_source + 2] = texture.data[pixel_deviation + 2];
            }
            ++i;
            ++map_index;
        }
    }
    // 渲染處理之后的圖像
    ctx.putImageData(ripple, 0, 0);
}

波源

為了形成波,我們必須在平靜的水面上加入波源,就像向水池中投入一個石頭一樣,形成的波源的大小和能量與石頭的半徑和你扔石頭的力量都有關系。

為了模擬波源,我們只需要修改一開始初始化的波幅分布數(shù)組即可。需要注意投入石頭的地方的波幅不易過小和過大。

另外,這個波源的半徑也很好控制,只要以波源為圓心,畫一個圓,讓這個圓內(nèi)的所有點都來一個脈沖即可。

波源生成方法如下:

// 在指定地點產(chǎn)生波源
function disturb(circleX, circleY) {
    // 下面的移位運算可以將值向下取整
    circleX <<= 0;
    circleY <<= 0;
    var maxDistanceX = circleX + dropRadius,
        maxDistanceY = circleY + dropRadius;
    for (var y = circleY - dropRadius; y < maxDistanceY; y++) {
        for (var x = circleX - dropRadius; x < maxDistanceX; x++) {
            ripple_map[old_index + y * width + x] += 512;
        }
    }
}

待處理事宜

還有很多要完善的地方,以后會更新到github,本文所有的效果代碼也可以在Git上面找到,歡迎大家star。

最后,簡單列一下接下來需要優(yōu)化的點:

  • 添加衍射
  • 兼容跨域圖片
  • 圖片自動縮放處理
  • JQuery插件化封裝
  • 適配優(yōu)化,速度優(yōu)化,效果優(yōu)化
  • 普通HTML元素支持,局部特效

衍射

在水波擴散的過程中,如果遇到障礙物,水波會繞過障礙物的邊緣或孔隙的邊緣,呈現(xiàn)路徑彎曲,在障礙物或孔隙邊緣的背后展衍。

其實實現(xiàn)起來很簡單,我們只要始終保持障礙物的振幅一直為0即可。

原文鏈接:http://uusama.com/643.html
canvas系列教程:http://uusama.com/tag/canvas

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容