dasg
dasg

Reputation: 321

How can I draw 2D water in Android using OpenGL ES 2.0?

I am developing Android application in Java. I want to draw dynamical images like in attached files (print screens of very old DOS program). I think it is water waves.

Can any one explain me how can I do this job? I have no ideas how this pictures were drawn.

Thanks!

p.s. May be it is a traveling wave in a compressible fluid?

EDITED: screen record with required animation: http://www.youtube.com/watch?v=_zeSQX_8grY

print screen

second print screen

third print screen

EDITED2: I have found sources of this video effect here. There is a compiled program for DOS (can be run in DOS box) and sourses in ASM. Folder "PART3" contains sources of required video effect (file WPLASMA.ASM). Unfortunately I don't know Turbo Assembler. Can somebody help me to understand how this program draws this video effect? I published content of WPLASMA.ASM here.

EDITED3: I have ported most parts of the code to C. But I don't know how VGA mode works. I have difficulties with PutBmp function.

#include <cmath>
#include <ctime>
#include <cstring>
#include <cstdlib>
#include <cassert>

#include <opencv2/highgui/highgui.hpp>

struct RGB {
    char red, green, blue;
};

#define MAXH 60           // horiz wave length.
#define MAXVW 64          // vert wave length.
#define MAXHW 32          // max horiz wave amount.
#define MAXV (80 + MAXHW) // vert wave length.

static void UpdHWaves( char* HWave1, char* HWave2,
                       int& HWavPos1, int& HWavPos2,
                       int HWavInc1 ) // Updates the Horiz Waves.
{
    for( int i = 0; i < MAXH - 1; ++i ) {
        HWave1[ i ] = HWave1[ i + 1 ];
    }

    int8_t val = 127 * std::sin( HWavPos1 * M_PI / 180.0 );
    HWave1[ MAXH - 1 ] =  val >> 1;

    HWavPos1 += HWavInc1;
    if( HWavPos1 >= 360 ) {
        HWavPos1 -= 360;
    }

    for( int i = 0; i < MAXH; ++i ) {
        val = 127 * std::sin( ( HWavPos2 + i * 4 ) * M_PI / 180.0 );
        val = ( val >> 1 ) + HWave1[ i ];
        HWave2[ i ] = ( val >> 3 ) + 16;
    }

    HWavPos2 += 4;
    if( HWavPos2 >= 360 ) {
        HWavPos2 -= 360;
    }
}

static void UpdVWaves( char *VWave1, char* VWave2,
                       int& VWavPos1, int& VWavPos2,
                       int VWavInc1 )
{
    for( int i = 0; i < MAXV - 1; ++i ) {
        VWave1[ i ] = VWave1[ i + 1 ];
    }

    int8_t val = 127 * std::sin( VWavPos1 * M_PI / 180.0 );
    VWave1[ MAXV - 1 ] = val >> 1;

    VWavPos1 += VWavInc1;
    if( VWavPos1 >= 360 ) {
        VWavPos1 -= 360;
    }

    for( int i = 0; i < MAXV; ++i ) {
        val = 127 * std::sin( ( VWavPos2 + i * 3 ) * M_PI / 180.0 );
        val = ( val >> 1 ) + VWave1[ i ];
        VWave2[ i ] = ( val >> 2 ) + 32;
    }

    ++VWavPos2;
    if( VWavPos2 >= 360 ) {
        VWavPos2 -= 360;
    }
}

static void UpdBmp( char *Bitmap, const char *VWave2 ) // Updates the Plasma bitmap.
{
    for( int k = 0; k < MAXV; ++k ) {
        char al = VWave2[ k ];
        int i = 0;
        for( int l = 0; l < MAXH; ++l ) {
            ++al;
            Bitmap[ i ] = al;
            i += 256;
        }
        ++Bitmap;
    }
}

static void PutBmp( const RGB* palete,
                    const char* BitMap,
                    const char* HWave2 ) // Puts into the screen the Plasma bitmap.
{
    RGB screen[320*200];
    memset( screen, 0, sizeof( screen ) );
    RGB *screenPtr = screen;

    const char *dx = BitMap;
    const char *si = HWave2;

    for( int i = 0; i < MAXH; ++i ) {
        char ax = *si;
        ++si;

        const char *si2 = ax + dx;
        for( int j = 0; j < 40; ++j ) {
            assert( *si2 < MAXH + MAXVW );
            *screenPtr = palete[ *si2 ];
            ++screenPtr;
            ++si2;

            assert( *si2 < MAXH + MAXVW );
            *screenPtr = palete[ *si2 ];
            ++screenPtr;
            ++si2;
        }
        dx += 256;
    }

    static cv::VideoWriter writer( "test.avi", CV_FOURCC('M','J','P','G'), 15, cv::Size( 320, 200 ) );

    cv::Mat image( 200, 320, CV_8UC3 );
    for( int i = 0; i < 200; ++i ) {
        for( int j = 0; j < 320; ++j ) {
            image.at<cv::Vec3b>(i, j )[0] = screen[ 320 * i + j ].blue;
            image.at<cv::Vec3b>(i, j )[1] = screen[ 320 * i + j ].green;
            image.at<cv::Vec3b>(i, j )[2] = screen[ 320 * i + j ].red;
        }
    }

    writer.write( image );
}

int main( )
{
    RGB palete[256];
    // generation of the plasma palette.
    palete[ 0 ].red = 0;
    palete[ 0 ].green = 0;
    palete[ 0 ].blue = 0;
    RGB *ptr = palete + 1;
    int ah = 0;
    int bl = 2;
    for( int i = 0; i < MAXH + MAXVW; ++i ) {
        ptr->red = 32 - ( ah >> 1 );
        ptr->green = 16 - ( ah >> 2 );
        ptr->blue = 63 - ( ah >> 2 );
        ah += bl;
        if( ah >= 64 ) {
            bl = - bl;
            ah += 2 * bl;
        }
        ptr += 1;
    }

    //setup wave parameters.
    int HWavPos1 = 0; // horiz waves pos.
    int HWavPos2 = 0;
    int VWavPos1 = 0; // vert waves pos.
    int VWavPos2 = 0;
    int HWavInc1 = 1; // horiz wave speed.
    int VWavInc1 = 7; // vert wave speed.

    char HWave1[ MAXH ]; // horiz waves.
    char HWave2[ MAXH ];
    char VWave1[ MAXV ]; // vert waves.
    char VWave2[ MAXV ];

    char Bitmap[ 256 * MAXH + MAXV ];
    memset( Bitmap, 0, sizeof( Bitmap ) );

    //use enough steps to update all the waves entries.
    for( int i = 0; i < MAXV; ++i ) {
        UpdHWaves( HWave1, HWave2, HWavPos1, HWavPos2, HWavInc1 );
        UpdVWaves( VWave1, VWave2, VWavPos1, VWavPos2, VWavInc1 );
    }

    std::srand(std::time(0));
    for( int i = 0; i < 200; ++i ) {
        UpdHWaves( HWave1, HWave2, HWavPos1, HWavPos2, HWavInc1 );
        UpdVWaves( VWave1, VWave2, VWavPos1, VWavPos2, VWavInc1 );
        UpdBmp( Bitmap, VWave2 );
        PutBmp( palete, Bitmap, HWave2 );

        //change wave's speed.
        HWavInc1 = ( std::rand( ) & 7 ) + 3;
        VWavInc1 = ( std::rand( ) & 3 ) + 5;
    }

    return 0;
}

Upvotes: 0

Views: 1938

Answers (2)

Tommy
Tommy

Reputation: 100632

Can you name the DOS program? Or find a similar effect on YouTube?

At a guess, it's a "plasma" effect, which used to be very common on the demo scene. You can see one in the background of the menu of the PC version of Tempest 2000, including very briefly in this YouTube video. Does that look right?

If so then as with all demo effects, it's smoke and mirrors. To recreate one in OpenGL you'd need to produce a texture with a spherical sine pattern. So for each pixel, work out its distance from the centre. Get the sine of that distance multiplied by whatever number you think is aesthetically pleasing. Store that value to the texture. Make sure you're scaling to fill a full byte. You should end up with an image that looks like ripples on the surface of a pond.

To produce the final output, you're going to additively composite at least three of those. If you're going to do three then multiply each by 1/3rd so that the values in your framebuffer end up in the range 0–255. You're going to move the three things independently to produce the animation, also by functions of sine — e.g. one might follow the path centre + (0.3 * sin(1.8 + time * 1.5), 0.8 * sin(0.2 + time * 9.2)), and the others will also obey functions of that form. Adjust the time multiplier, angle offset and axis multiplier as you see fit.

There's one more sine pattern to apply: if this were a DOS program, you'd further have set up your palette so that brightness comes and goes in a sine wave — e.g. colours 0–31 would be one complete cycle, 32–63 would be a repeat of the cycle, etc. You can't set a palette on modern devices and OpenGL ES doesn't do paletted textures so you're going to have to write a shader. On the plus side, the trigonometric functions are built into GLSL so it'll be a fairly simple one.

EDIT: I threw together a quick test project and wrote the following vertex shader:

attribute vec4 position;
attribute vec2 texCoord;

uniform mediump float time;

varying highp vec2 texCoordVarying1, texCoordVarying2, texCoordVarying3;

void main()
{
    mediump float radiansTime = time * 3.141592654 * 2.0;

    /*
        So, coordinates here are of the form:

            texCoord + vec2(something, variant of same thing)

        Where something is:

            <linear offset> + sin(<angular offset> + radiansTime * <multiplier>)


        What we're looking to do is to act as though moving three separate sheets across
        the surface. Each has its own texCoordVarying. Each moves according to a
        sinusoidal pattern. Note that the multiplier is always a whole number so
        that all patterns repeat properly as time goes from 0 to 1 and then back to 0,
        hence radiansTime goes from 0 to 2pi and then back to 0.

        The various constants aren't sourced from anything. Just play around with them.

    */

    texCoordVarying1 = texCoord + vec2(0.0 + sin(0.0 + radiansTime * 1.0) * 0.2, 0.0 + sin(1.9 + radiansTime * 8.0) * 0.4);
    texCoordVarying2 = texCoord - vec2(0.2 - sin(0.8 + radiansTime * 2.0) * 0.2, 0.6 - sin(1.3 + radiansTime * 3.0) * 0.8);
    texCoordVarying3 = texCoord + vec2(0.4 + sin(0.7 + radiansTime * 5.0) * 0.2, 0.5 + sin(0.2 + radiansTime * 9.0) * 0.1);

    gl_Position = position;
}

... and fragment shader:

varying highp vec2 texCoordVarying1, texCoordVarying2, texCoordVarying3;

void main()
{
    /*
        Each sheet is coloured individually to look like ripples on
        the surface of a pond after a stone has been thrown in. So it's
        a sine function on distance from the centre. We adjust the ripple
        size with a quick multiplier.

        Rule of thumb: bigger multiplier = smaller details on screen.

    */
    mediump vec3 distances =
        vec3(
            sin(length(texCoordVarying1) * 18.0),
            sin(length(texCoordVarying2) * 14.2),
            sin(length(texCoordVarying3) * 11.9)
        );

    /*
        We work out outputColour in the range 0.0 to 1.0 by adding them,
        and using the sine of that.
    */
    mediump float outputColour = 0.5 + sin(dot(distances, vec3(1.0, 1.0, 1.0)))*0.5;

    /*
        Finally the fragment colour is created by linearly interpolating
        in the range of the selected start and end colours 48 36 208
    */
    gl_FragColor =
        mix( vec4(0.37, 0.5, 1.0, 1.0), vec4(0.17, 0.1, 0.8, 1.0), outputColour);
}

/*
    Implementation notes:

        it'd be smarter to adjust the two vectors passed to mix so as not
        to have to scale the outputColour, leaving it in the range -1.0 to 1.0
        but this way makes it clearer overall what's going on with the colours
*/

Putting that into a project that draws a quad to display the texCoord range [0, 1] in both dimensions (aspect ratio be damned) and setting time so that it runs from 0 to 1 once every minute gave me this:

YouTube link

It's not identical, clearly, but it's the same effect. You just need to tweak the various magic constants until you get something you're happy with.

EDIT 2: it's not going to help you all that much but I've put this GL ES code into a suitable iOS wrapper and uploaded to GitHub.

Upvotes: 2

ClayMontgomery
ClayMontgomery

Reputation: 2832

There is a sample program in the Android NDK called "bitmap-plasma" whch produces very similar patterns to this. It is in C, but could probably be converted into GLSL code.

Upvotes: 1

Related Questions