A Ripple Effect for Android

The code in this article shows you how to apply a wave effect over a bitmap. Unlike many apps in the Google Play Store, this is a genuine wave effect that implements the 2d Wave Equation:

2ut2=c22ux2+2uy2

2d Wave Equation

Here is a sample animation:

The code is presented below with comments.  A full physical model that models specific fluid depths, refractive indices, etc is not required. Instead, practical end-effect constants are defined. E.g. a simple refractive index ratio, a scale factor on the refraction displacement, etc. For speed and battery conservation, the waves are computed on a grid 1/32th the size of the bitmap. The choice of 32 allows quick bit-shift conversion between the wave grid and the pixel grid. 2d linear interpolation is then used to convert the calculated refraction deflections to the bitmap’s pixel grid.

The code includes a ‘Poke‘ function which simulates a finger touching the air/water boundary. This would typically be hooked up to a touch or mouse event. This simulation raises a circular area of the wavefield to the maximum amplitude. Multiple closely-spaced calls to Poke can be used to simulate a dragged finger.

Here is the code:

package com.winwaed.ripples;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;

/**
 * Ripple Data Model
 * Computes ripples and applies them to a base bitmap.
 */

class RippleBox {

    // Amplitude and size of a 'poke' - eg. from a finger touch
    final static double POKE_AMPLITUDE = 200.0;
    final static int POKE_RADIUS = 3;

    final static int POKE_RADIUS_SQ = POKE_RADIUS*POKE_RADIUS;

    // max wave amplitude
    final static double MAX_AMPLITUDE = 255.0;

    // Elasticity<1.0 allows for some absorption - waves slowly die away
    final static double ELASTICITY = 0.99;  // 1.0 = fully elastic : Absorption/delta-t = 1.0 - ELASTICITY
    
    // ratio of refractive indices - controls the amount of refraction
    final static double REFRACTIVE_RATIO = 0.75;

    final static double REFRACTIVE_RATIO_SQ = REFRACTIVE_RATIO*REFRACTIVE_RATIO;

    final static double GRADIENT_SCALE = 0.005; //0.01; // vertical scale on ripples when converting for refraction
    final static double DISPLACEMENT_SCALE = 500.0;  // multiplier for the final pixel displacement

    // speed of wave propagation
    final static double c = 0.3333;

    final static int PIXEL_BITSHIFT = 5; // log2 of PIXEL_SIZE
    final static int PIXEL_BITSHIFT_SQ = 10; // double - for use in squared divisions
    final static int PIXEL_SIZE = 1<<PIXEL_BITSHIFT;  // Actual pixel size in the refraction model

    final static double PIXEL_SIZE_RECIP = 1.0 / (double) PIXEL_SIZE;

    final static double C2 = c*c;
    final static double D1 = 2.0;
    final static double D2 = 1.0;

    // for wave model
    int nWidth, nHeight;

    // wave model
    double [][] displ;
    double [][] displ_1, displ_2;

    // output image
    int[] outpixels;
    Bitmap imgBitmap;

    int [] inpixels;  // stores the input bitmap pixels
    int pxWidth,pxHeight; // display

    /**
    * constructor with a default bitmap
    */
    public RippleBox(Context context, int width, int height) {

        imgBitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
        pxWidth = width;
        pxHeight = height;
        nWidth = width >> PIXEL_BITSHIFT; 
        nHeight = height >> PIXEL_BITSHIFT;
        displ = new double[nWidth][nHeight];
        displ_1 = new double[nWidth][nHeight];
        displ_2 = new double[nWidth][nHeight];

        SetBaseBitmap( BitmapFactory.decodeResource(context.getResources(), R.drawable.base_image) );
        outpixels = new int[pxWidth * pxHeight];

        ClearData();

        AdvanceRipples();
    }

    /**
    * Set the entire wavefield to 0 - ie. flat
    */ 
    public void ClearData()
    {
        for (int i=0; i<nWidth; i++)
            for (int j=0; j<nHeight; j++)
            {
                displ[i][j] = 0.0;
                displ_1[i][j] = 0.0;
                displ_2[i][j] = 0.0;
            }
    }

    /**
    * Apply a 'poke' at pixel position xx,yy
    * Simulates a finger touching the water surface
    */
    public void Poke(int xx, int yy)
    {
        // xx and yy are screen coords: convert to ripple coords
        int x = xx >> PIXEL_BITSHIFT;
        int y = yy >> PIXEL_BITSHIFT;
        if (x<0 || y<0 || x>=nWidth || y>=nHeight)
            return;

        for (int i=-POKE_RADIUS; i<=POKE_RADIUS; i++)
            for (int j=-POKE_RADIUS; j<=POKE_RADIUS; j++)
                if (i*i+j*j < POKE_RADIUS_SQ) {
                    if (x + i > 0 && x + i < nWidth-1)
                        if (y + j > 0 && y + j < nHeight-1) {
                            double this_displ = POKE_AMPLITUDE * 0.5 * (1.0 + Math.cos(Math.sqrt(i * i + j * j) * Math.PI / POKE_RADIUS));
                            displ[x + i][y + j] += this_displ;
                            if (displ[x + i][y + j] > MAX_AMPLITUDE)
                                displ[x + i][y + j] = MAX_AMPLITUDE;

                            displ_1[x + i][y + j] += this_displ;
                            if (displ_1[x + i][y + j] > MAX_AMPLITUDE)
                                displ_1[x + i][y + j] = MAX_AMPLITUDE;

                        }
                }
    }


    /**
    * Update the waves (and hence ripple effect) with one time increment
    */
    public void update()  {
        AdvanceRipples();
        RefractRipples();
    }

    /**
    *  Returns a bitmap of the refracted image
    */
    public Bitmap getBitmap()   {

        imgBitmap.setPixels(outpixels, 0, pxWidth,  0,0, pxWidth,pxHeight);

        return imgBitmap;
    }


    /**
    * Advance the waves (ripples) by one time increment
    */
    public void AdvanceRipples()
    {
        double[][] tempd = displ_2;
        displ_2 = displ_1;
        displ_1 = displ;
        displ = tempd;

        double dd, uu;
        for (int x=1; x<nWidth-1; x++)
            for (int y=1; y<nHeight-1; y++)
            {
              uu = displ_1[x-1][y] - 2 * displ_1[x][y] + displ_1[x+1][y] +
                        displ_1[x][y-1] - 2 * displ_1[x][y] + displ_1[x][y+1];

                dd = 2.0 * displ_1[x][y] - displ_2[x][y] + C2 * uu; //(u_xx + u_yy);

                dd = Math.min(Math.max(dd,-MAX_AMPLITUDE), MAX_AMPLITUDE);

                displ[x][y] = dd * ELASTICITY;
            }
    }

    /** 
    * Apply refractions from the current wavefield to the bitmap image.
    * This creates the visual distortion (aberration) in the image
    */
    public void RefractRipples()
    {
        int w = pxWidth; //cv.getWidth();
        int h = pxHeight; //cv.getHeight();

        double norm[] = new double[3];

        double prev_row_dx[] = new double[nWidth];
        double prev_row_dy[] = new double[nWidth];
        double this_row_dx[] = new double[nWidth];
        double this_row_dy[] = new double[nWidth];
        double trow[];

        for (int y = 1; y < nHeight - 1; y++)
        {
            // calc new row of displacements
            trow = prev_row_dx;
            prev_row_dx = this_row_dx;
            this_row_dx = trow;
            trow = prev_row_dy;
            prev_row_dy = this_row_dy;
            this_row_dy = trow;

            for (int x = 1; x < nWidth - 1; x++)
            {
                // find normal by averaging the four triangles that share this point
                // xn[x][y] = 0.0; yn[x][y] = 0.0; zn[x][y] = 0.0;
                norm[0] = 0.0;
                norm[1] = 0.0;
                norm[2] = 0.0;

                XProductAndSum(displ[x][y], displ[x + 1][y], displ[x][y + 1], 1.0, 1.0, norm);
                XProductAndSum(displ[x][y], displ[x + 1][y], displ[x][y - 1], 1.0, -1.0, norm);
                XProductAndSum(displ[x][y], displ[x - 1][y], displ[x][y + 1], -1.0, 1.0, norm);
                XProductAndSum(displ[x][y], displ[x - 1][y], displ[x][y - 1], -1.0, -1.0, norm);

                // normalise the normal!
                double m = Math.sqrt(norm[0] * norm[0] + norm[1] * norm[1] + norm[2] * norm[2]);
                norm[0] /= m;
                norm[1] /= m;
                norm[2] /= m;

                double cos1 = norm[2];
                double sin1sq = norm[0] * norm[0] + norm[1] * norm[1];
                double sin1 = Math.sqrt(sin1sq);

                double sin3DS = -DISPLACEMENT_SCALE * sin1 * (Math.sqrt(1.0 - REFRACTIVE_RATIO_SQ * sin1sq) - cos1 * REFRACTIVE_RATIO);

                // sin3 is proportional to the sideways aberration
                // direction of this aberration is opposite to norm[0],norm[1]
                this_row_dx[x] = norm[0] * sin3DS;
                this_row_dy[x] = norm[1] * sin3DS;
            }

            // deal with edges
            this_row_dx[0] = this_row_dx[1];
            this_row_dy[0] = this_row_dy[1];
            this_row_dx[nWidth - 1] = this_row_dx[nWidth - 2];
            this_row_dy[nWidth - 1] = this_row_dy[nWidth - 2];

            if (y == 1) {   // make data good for the top row
                System.arraycopy(this_row_dx, 0, prev_row_dx, 0, nWidth);
                System.arraycopy(this_row_dy, 0, prev_row_dy, 0, nWidth);
            }

            // Loop over these squares, filling in with interpolation
            int yps = y << PIXEL_BITSHIFT;
            for (int x = 0; x < nWidth - 1; x++)
            {
                int xps = x << PIXEL_BITSHIFT;

                int dx_y1 = (int)Math.round(prev_row_dx[x]);
                int dx_y2 = (int)Math.round(prev_row_dx[x+1]);
                int dx_y3 = (int)Math.round(this_row_dx[x+1]);
                int dx_y4 = (int)Math.round(this_row_dx[x]);
                int dy_y1 = (int)Math.round(prev_row_dy[x]);
                int dy_y2 = (int)Math.round(prev_row_dy[x+1]);
                int dy_y3 = (int)Math.round(this_row_dy[x+1]);
                int dy_y4 = (int)Math.round(this_row_dy[x]);
                //int t=0;
                int mt = PIXEL_SIZE;
                //double t = 0.0;
                int xx = xps;
                for (int t = 0; t < PIXEL_SIZE; xx+=2, t+=2,mt-=2)
                {
                    int mu = PIXEL_SIZE;
                    int yy = yps;
                    for (int u = 0; u < PIXEL_SIZE; yy+=2, u+=2,mu-=2)
                    {
                        int dx = ( mt*mu*dx_y1 + t*mu*dx_y2 + t*u*dx_y3 + mt*u*dx_y4 ) >> PIXEL_BITSHIFT_SQ;
                        int dy = ( mt*mu*dy_y1 + t*mu*dy_y2 + t*u*dy_y3 + mt*u*dy_y4 ) >> PIXEL_BITSHIFT_SQ;

                        if (dx +xx+1 < w  &&  dx + xx >= 0  &&
                                dy + yy+1 < h   &&  dy +yy >= 0)
                        {
                            outpixels[xx + w * yy] = inpixels[dx + xx + w * (dy + yy)];
                            outpixels[xx + w+ w * yy] = inpixels[dx + xx + w * (dy + yy+1)];
                            outpixels[xx+1 + w * yy] = inpixels[dx + xx+1 + w * (dy + yy)];
                            outpixels[xx+1 + w + w * yy] = inpixels[dx + xx+1 + w * (dy + yy+1)];
                        }
                        else
                        {
                            outpixels[xx + w * yy] = Color.rgb(0,0,0);
                            outpixels[xx + w+ w * yy] = Color.rgb(0,0,0);
                            outpixels[xx+1 + w * yy] = Color.rgb(0,0,0);
                            outpixels[xx+1 + w+ w * yy] = Color.rgb(0,0,0);
                        }
                    }
                }

            }
        }

    }

    /**
    * Draw the wavefield to a bitmap. Useful for debugging
    */
    public void DrawToBitmap( Bitmap bmp)
    {
        Canvas cv = new Canvas(bmp);
        int w = cv.getWidth();
        int h = cv.getHeight();

        for (int x=0; x<nWidth; x++)
            for (int y=0; y<nHeight; y++)
            {
                int d = (int)(displ[x][y]); 
                int c;
                if (d<0)
                     c = Color.rgb( (-d)%256,0,0);
                else c = Color.rgb( 0, d%256,0);
                Paint pp = new Paint();
                pp.setColor(c);
                pp.setStyle(Paint.Style.FILL);
                cv.drawRect(x*32,y*32,x*32+32, y*32+32, pp);
            }
    }


    /**
     * Set the base bitmap - ie. extracts the pixels into an array for future use
     * @param bmp
     */
    public void SetBaseBitmap( Bitmap bmp)
    {
        int w = bmp.getWidth();
        int h = bmp.getHeight();

        float xScale = (float)pxWidth / (float)w;
        float yScale = (float)pxHeight / (float)h;
        float scale = Math.max(xScale, yScale);

        float scaledWidth = scale * w;
        float scaledHeight = scale * h;

        float left = (pxWidth - scaledWidth) / 2;
        float top = (pxHeight - scaledHeight) / 2;

        RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);

        Bitmap bm = Bitmap.createBitmap(pxWidth, pxHeight, bmp.getConfig() );
        Canvas canvas = new Canvas(bm);

        canvas.drawBitmap(bmp, null, targetRect, null);

        inpixels = new int[pxWidth * pxHeight];
        bm.getPixels(inpixels, 0, pxWidth, 0, 0, pxWidth, pxHeight);
    }



    /**
     * Calculates a cross product and ADDS the result to norm
     * vectors are aligned with x and y axes
     * Also applies the gradient scale - converts the wave to a sensible ripple gradient
     * (helper function for refraction)
     *
     * @param aa Central point value
     * @param ax value for x direction
     * @param ay value for y direction
     * @param dx x displacement
     * @param dy y displacement
     * @param norm 3 element normal vector
     */
    private void XProductAndSum( double aa, double ax, double ay, double dx, double dy, double[] norm)
    {
        double u3 = (ax - aa) * GRADIENT_SCALE;
        double v3 = (ay - aa) * GRADIENT_SCALE;

        double n1 =  /*-*/ u3*dy;
        double n2 =  /*-*/ dx*v3;
        double n3=  dx*dy;
        if (n3 >=0.0)
        {
            norm[0] -= n1;
            norm[1] -= n2;
            norm[2] += n3;
        }
        else
        {
            norm[0] += n1;
            norm[1] += n2;
            norm[2] -= n3;
        }
    }

}

 

Leave a Reply