Wave/Ripple Fragment Shader Working but With Strange Side-Effects

Greetings:

Hopefully this is the right place to post for this problem. I’m not sure if the problem is OpenGL or the shader(s), which is why I’m here.

Background
I want to incorporate a ShaderToy ripple/wave effect into my main C++ windows program:

I slightly modified the fragment shader code to work under C++ with glew/freeglut and also removed mouse and image related code (image not needed for testing the problem). I’m using a basic “gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;” vertex shader.

I borrowed and modified / simplified a multi-pass shader tester program in C++ from here:

I’ve attached all the code I am using. I am using the latest freeglut and glew. I’m developing / running this on Windows 8.1 x64.

Problem
The shader shows up and works under the ShaderTest program but has some strangeness to it. I’m not sure if this is a problem with my OpenGL approach or some kind of problem with running under OpenGL, rather than WebGL in the browser. The browser version runs fine for me.

Strangeness Notes
The shader effect creates a ripple and from that ripple the waves push out. In the WebGL version in the browser, this works perfectly. The ripple and wave generation will go on forever.

Under the C++ test program, ripples are created, but also create ripple waves inside existing ripples (inner ripples), eventually creating an infinite loop of waves, which causes “black holes” to be generated over time. The black holes will consume the screen as time passes.

When the shader runs in a full-screen quad, the “waves” will bounce off of the joined-triangles seem (a quad is two triangles joined).

When the shader runs in an oversized triangle, the waves work correctly, but the inside ripples problem still happens.

What I’ve Tried
I’ve been at this problem for about a week now, so it’s hard to detail everything I’ve tried. ShaderToy describes their buffer with linear 32-bit floating points. It seems that using that format for textures is correct.

If I try using 16-bit float, or RBGA with unsigned byte, the side-effects get lesser and lesser, but never go away fully. The color quality of the effect obviously goes to crap, though, so it doesn’t really help.

With regards to textures and FBOs, I’ve tried many different approaches. Dual-FBOs, dual color-attachments with a single FBO, etc. Nothing seems to work. I have to use at least the one FBO on the buffer channel, it seems.

Summary
The two problems I’m trying to solve are:
1: inner ripple / infinite waves
2: quad seem reflection

My guesses: projection or shader coordinates precision issue (or both).

I’ve attached the two source files and the vert/frag shaders. Screenshots in 2nd post.

Any help much appreciated.

Thanks!

[ATTACH=CONFIG]1576[/ATTACH]
[ATTACH=CONFIG]1577[/ATTACH]
[ATTACH=CONFIG]1578[/ATTACH]
[ATTACH=CONFIG]1579[/ATTACH]
[ATTACH=CONFIG]1580[/ATTACH]

Working from ShaderToy (no texture)
[ATTACH=CONFIG]1581[/ATTACH]

C++ Test: Oversized Triangle (no seem reflection)
[ATTACH=CONFIG]1582[/ATTACH]

C++ Test: Quad (triangles seem reflection)
[ATTACH=CONFIG]1583[/ATTACH]

ShaderTest.cpp

//
//  code based on https://github.com/yasuohasegawa/OpenGL-Multipass-Shader-Sample 
//

#include <GL/glew.h>
#include <GL/glut.h>
#include "ShaderTest.h"

#define TIMER_MS 25
#define CYCLE 1000.0f
#define APP_WIDTH  640
#define APP_HEIGHT 360

static GLuint _s_program1 = 0;
static GLuint _s_program2 = 0;
static GLuint _s_fb = 0;
static GLuint _s_texBuffer = 0;
static GLuint _s_texImage = 0;
static GLuint _s_frame = 0;

void init(){
	glewInit();
	_s_program1 = loadShader("./shaders/shader.vert","./shaders/ripples1_buffer.frag");
	_s_program2 = loadShader("./shaders/shader.vert","./shaders/ripples1_image.frag");

    // buffer texture
    glGenTextures(1, &_s_texBuffer);
    glBindTexture(GL_TEXTURE_2D, _s_texBuffer);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, APP_WIDTH, APP_HEIGHT, 0, GL_RGBA, GL_FLOAT, 0);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    // image buffer
    glGenTextures(1, &_s_texImage);
    glBindTexture(GL_TEXTURE_2D, _s_texImage);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, APP_WIDTH, APP_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
		//glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, APP_WIDTH, APP_HEIGHT, 0, GL_RGBA, GL_FLOAT, 0);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);
    
    // fbo
    glGenFramebuffers(1, &_s_fb);
    glBindFramebuffer(GL_FRAMEBUFFER, _s_fb);
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _s_texBuffer, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void draw_quad(){
	glBegin(GL_QUADS);
		glTexCoord2f(0.0, 0.0); glVertex2i(0,			0);
		glTexCoord2f(1.0, 0.0); glVertex2i(APP_WIDTH,	0);
		glTexCoord2f(1.0, 1.0); glVertex2i(APP_WIDTH,	APP_HEIGHT);
		glTexCoord2f(0.0, 1.0); glVertex2i(0,			APP_HEIGHT);
	glEnd();
}

void draw_triangle(){
	glBegin(GL_TRIANGLES);
		glVertex2i(0,			0);
		glVertex2i(0,			APP_HEIGHT);
		glVertex2i(APP_WIDTH,	APP_HEIGHT);
	glEnd();
}

void update_shader(){
	bool draw_oversized_triangle_instead_of_quad = true;
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glViewport(0, 0, APP_WIDTH, APP_HEIGHT);
    glOrtho(0, APP_WIDTH, 0, APP_HEIGHT, -1, 1);

	if (draw_oversized_triangle_instead_of_quad){
		glTranslatef(0.0, -APP_HEIGHT, 0.0);
		glScalef(2.0, 2.0, 0.0);
	}

	// buffer
    glBindFramebuffer(GL_FRAMEBUFFER, _s_fb);
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, _s_texBuffer);

		glUseProgram(_s_program1);
		glUniform2f(glGetUniformLocation(_s_program1, "iViewportResolution"), (float) APP_WIDTH, (float) APP_HEIGHT);
		glUniform1f(glGetUniformLocation(_s_program1, "iTime"), (float) glutGet(GLUT_ELAPSED_TIME) / CYCLE);
		glUniform1i(glGetUniformLocation(_s_program1, "iFrame"), _s_frame);
		glUniform1i(glGetUniformLocation(_s_program1, "iChannel0"), 0);

		if (draw_oversized_triangle_instead_of_quad)
			 draw_triangle();
		else draw_quad();
    
		glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    
	// image
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, _s_texBuffer);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, _s_texImage);

		glUseProgram(_s_program2);    
		glUniform2f(glGetUniformLocation(_s_program2, "iViewportResolution"), (float) APP_WIDTH, (float) APP_HEIGHT);
		glUniform1i(glGetUniformLocation(_s_program2, "iChannel0"), 0);
		glUniform1i(glGetUniformLocation(_s_program2, "iChannel1"), 1);

		if (draw_oversized_triangle_instead_of_quad)
			 draw_triangle();
		else draw_quad();

		glUseProgram(0);
	glBindTexture(GL_TEXTURE_2D, 0);
	glFlush();

	_s_frame++;
}

void timer(int){
	update_shader();
    glutTimerFunc(TIMER_MS, timer, 0);
}

void display(){}
int main(int argc, char *argv[]){
    glutInit(&argc, argv);
    glutInitWindowPosition(0, 0);
    glutInitWindowSize(APP_WIDTH, APP_HEIGHT);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
    glutCreateWindow(argv[0]);
    glutDisplayFunc(display);
	glutTimerFunc(TIMER_MS, timer, 0);
    init();
    glutMainLoop();
    return 0;
}

ShaderTest.h

//
//  code based on https://github.com/yasuohasegawa/OpenGL-Multipass-Shader-Sample 
//

#include <fstream>
#include <iostream>

int read_shader_file(GLuint shader, const char *name){
    int ret;
    
    std::ifstream file(name, std::ios::binary);
    if (file.fail()) {
        std::cerr << "Can't open file: " << name << std::endl;
        ret = -1;
    } else {
        file.seekg(0L, std::ios::end);
        GLsizei length = file.tellg();
        
        const GLchar *source = new GLchar[length];
        
        file.seekg(0L, std::ios::beg);
        file.read((char *)source, length);
        
        if (file.bad()) {
            std::cerr << "Could not read file: " << name << std::endl;
            ret = -1;
        } else {            
            glShaderSource(shader, 1, &source, &length);
            ret = 0;
        }
        
        delete[] source;
    }
    
    return ret;
}

void print_shader_info(GLuint shader){
    GLsizei bufSize;
    
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH , &bufSize);
    
    if (bufSize > 1) {
        GLchar *infoLog;
        
        infoLog = (GLchar *)malloc(bufSize);
        if (infoLog != NULL) {
            GLsizei length;
            
            glGetShaderInfoLog(shader, bufSize, &length, infoLog);
            fprintf(stderr, "InfoLog:
%s

", infoLog);
            free(infoLog);
        } else fprintf(stderr, "Could not allocate InfoLog buffer.
");
    }
}

void print_program_info(GLuint program){
    GLsizei bufSize;
    
    glGetProgramiv(program, GL_INFO_LOG_LENGTH , &bufSize);
    
    if (bufSize > 1) {
        GLchar *infoLog;
        
        infoLog = (GLchar *)malloc(bufSize);
        if (infoLog != NULL) {
            GLsizei length;
            
            glGetProgramInfoLog(program, bufSize, &length, infoLog);
            fprintf(stderr, "InfoLog:
%s

", infoLog);
            free(infoLog);
        } else fprintf(stderr, "Could not allocate InfoLog buffer.
");
    }
}

GLuint loadShader(const char *vert, const char *frag){
    GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
    GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);

    if (read_shader_file(vertShader, vert)) exit(1);
    if (read_shader_file(fragShader, frag)) exit(1);
    
    GLint compiled, linked;
    
    glCompileShader(vertShader);
    glGetShaderiv(vertShader, GL_COMPILE_STATUS, &compiled);
    print_shader_info(vertShader);
    if (compiled == GL_FALSE) {
        fprintf(stderr, "Compile error in vertex shader.
");
        exit(1);
    }
    
    glCompileShader(fragShader);
    glGetShaderiv(fragShader, GL_COMPILE_STATUS, &compiled);
    print_shader_info(fragShader);
    if (compiled == GL_FALSE) {
        fprintf(stderr, "Compile error in fragment shader.
");
        exit(1);
    }
    
    GLuint gl2Program = glCreateProgram();
    
    glAttachShader(gl2Program, vertShader);
    glAttachShader(gl2Program, fragShader);
    
    glDeleteShader(vertShader);
    glDeleteShader(fragShader);
    
    glLinkProgram(gl2Program);
    glGetProgramiv(gl2Program, GL_LINK_STATUS, &linked);
    print_program_info(gl2Program);
    if (linked == GL_FALSE) {
        fprintf(stderr, "Link error.
");
        exit(1);
    }
    
    return gl2Program;
}

shader.vert

void main(){
  gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

ripples1_buffer.frag

// https://www.shadertoy.com/view/4dK3Ww
#version 130

uniform vec2         iViewportResolution;
uniform float        iTime;               // shader playback time (in seconds)
uniform int          iFrame;              // shader playback frame
//uniform samplerXX  iChannel0..3;        // input channel. XX = 2D/Cube
uniform sampler2D    iChannel0;

void main(){
   vec3 e = vec3(vec2(1.)/iViewportResolution.xy,0.);
   vec2 q = gl_FragCoord.xy/iViewportResolution.xy;

   vec4 c = texture(iChannel0, q);

   float p11 = c.y;

   float p10 = texture(iChannel0, q-e.zy).x;
   float p01 = texture(iChannel0, q-e.xz).x;
   float p21 = texture(iChannel0, q+e.xz).x;
   float p12 = texture(iChannel0, q+e.zy).x;

   float d = 0.;

   // --------------------
   // MOUSE CODE REMOVED
   // --------------------
   //if (iTime >= 2.5 && iTime < 3.0){
      // Simulate rain drops
      float t = iTime*2.;
      vec2 pos = fract(floor(t)*vec2(0.456665,0.708618))*iViewportResolution.xy;
      float amp = 1.-step(.05,fract(t));
      d = -amp*smoothstep(2.5,.5,length(pos - gl_FragCoord.xy));
   //}

   // The actual propagation:
   d += -(p11-.5)*2. + (p10 + p01 + p21 + p12 - 2.);
   d *= .99; // dampening
   d *= float(iFrame>=50); // clear the buffer at iFrame < 2
   d = d*.5 + .5;
   
   // Put previous state as "y":
   gl_FragColor = vec4(d, c.x, 0, 0);
}

ripples1_image.frag

// https://www.shadertoy.com/view/4dK3Ww
#version 130

uniform vec2        iViewportResolution;
uniform vec2        iImageResolution;
//uniform samplerXX iChannel0..3;       // input channel. XX = 2D/Cube
uniform sampler2D   iChannel0;
uniform sampler2D   iChannel1;


void main(){
    vec2 q = gl_FragCoord.xy/iViewportResolution.xy;

    // -----------------------
    // FORCING NON-TEXTURED VERSION
    // -----------------------
    float h = texture(iChannel0, q).x;
    float sh = 1.35 - h*2.;
    vec3 c =
       vec3(exp(pow(sh-.75,2.)*-10.),
            exp(pow(sh-.50,2.)*-20.),
            exp(pow(sh-.25,2.)*-10.));
    gl_FragColor = vec4(c,1.);
}

I’ve further simplified the ShaderTest.cpp code to eliminate the need for the image texture. I draw to the texture within the FBO and then blit it to the screen. The colors are not correct but helps to simplify for testing and diagnosing the problem.

I have tried a quad, triangle fan, oversized triangle, and also a triangle_strip. The code allows for easy switching (see draw enum).

After reading posts and blogs for days, I am leaning towards some kind of precision issue with values within the frag shader. Not really sure to how best test against that.

I found this older thread that describes my problem, although the issue was never resolved, so I don’t know if it was actually the same problem or how it was solved:
https://www.opengl.org/discussion_boards/showthread.php/178720-Fullscreen-quad-invoking-fragment-shader-twice-along-triangle-seam

Thanks.

(NOTE: red diagonal line on image is added by me)
Refined CPP Code Below: Quad Seam / Inner Ripples
[ATTACH=CONFIG]1585[/ATTACH]

ShaderTest.cpp V2

//
//  code based on https://github.com/yasuohasegawa/OpenGL-Multipass-Shader-Sample 
//

#include <GL/glew.h>
#include <GL/glut.h>
#include "ShaderTest.h"

#define TIMER_MS 25
#define CYCLE 1000.0f
#define APP_WIDTH  640
#define APP_HEIGHT 360

static GLuint _s_program1 = 0;
static GLuint _s_fb = 0;
static GLuint _s_texBuffer = 0;
static GLuint _s_frame = 0;

static enum eDrawMethod{
    DRAW_QUAD,
    DRAW_TRIANGLE_FAN,
    DRAW_TRIANGLE_STRIP,
    DRAW_TRIANGLE_OVERSIZED
} _s_drawMethod = DRAW_QUAD;


void draw(){
    if (_s_drawMethod == DRAW_QUAD){
        glBegin(GL_QUADS);
            glTexCoord2f(0.0, 0.0); glVertex2i(0,            0);
            glTexCoord2f(1.0, 0.0); glVertex2i(APP_WIDTH,    0);
            glTexCoord2f(1.0, 1.0); glVertex2i(APP_WIDTH,    APP_HEIGHT);
            glTexCoord2f(0.0, 1.0); glVertex2i(0,            APP_HEIGHT);
        glEnd();

    } else if (_s_drawMethod == DRAW_TRIANGLE_FAN) {
        glBegin(GL_TRIANGLE_FAN);
            glVertex2i(0,            0);
            glVertex2i(0,            APP_HEIGHT);
            glVertex2i(APP_WIDTH,    APP_HEIGHT);
            glVertex2i(APP_WIDTH,    0);
        glEnd();

    } else if (_s_drawMethod == DRAW_TRIANGLE_STRIP) {
        glBegin(GL_TRIANGLE_STRIP);
            glVertex2i(0,            0);
            glVertex2i(APP_WIDTH,    0);
            glVertex2i(0,            APP_HEIGHT);
            glVertex2i(APP_WIDTH,    APP_HEIGHT);
        glEnd();

    } else if (_s_drawMethod == DRAW_TRIANGLE_OVERSIZED) {
        glBegin(GL_TRIANGLES);
            glVertex2i(0,            0);
            glVertex2i(0,            APP_HEIGHT);
            glVertex2i(APP_WIDTH,    APP_HEIGHT);
        glEnd();
    }
}

void init(){
    glewInit();
    _s_program1 = loadShader("./shaders/shader.vert","./shaders/ripples1_buffer.frag");

    // buffer texture
    glGenTextures(1, &_s_texBuffer);
    glBindTexture(GL_TEXTURE_2D, _s_texBuffer);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, APP_WIDTH, APP_HEIGHT, 0, GL_RGBA, GL_FLOAT, 0);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    // fbo
    glGenFramebuffers(1, &_s_fb);
    glBindFramebuffer(GL_FRAMEBUFFER, _s_fb);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _s_texBuffer, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void render(){
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glViewport(0, 0, APP_WIDTH, APP_HEIGHT);
    glOrtho(0, APP_WIDTH, 0, APP_HEIGHT, -1, 1);

    if (_s_drawMethod == DRAW_TRIANGLE_OVERSIZED){
        glTranslatef(0.0, -APP_HEIGHT, 0.0);
        glScalef(2.0, 2.0, 0.0);
    }

    // buffer
    glBindFramebuffer(GL_FRAMEBUFFER, _s_fb);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, _s_texBuffer);
            glUseProgram(_s_program1);
            glUniform2f(glGetUniformLocation(_s_program1, "iViewportResolution"), (float) APP_WIDTH, (float) APP_HEIGHT);
            glUniform1f(glGetUniformLocation(_s_program1, "iTime"), (float) glutGet(GLUT_ELAPSED_TIME) / CYCLE);
            glUniform1i(glGetUniformLocation(_s_program1, "iFrame"), _s_frame++);
            glUniform1i(glGetUniformLocation(_s_program1, "iChannel0"), 0);
                draw();
            glUseProgram(0);
        glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // blit testing...
    glBindFramebuffer(GL_FRAMEBUFFER, _s_fb);
        glReadBuffer(GL_COLOR_ATTACHMENT0);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
            glBlitFramebuffer(0, 0, APP_WIDTH, APP_HEIGHT, 0, 0, APP_WIDTH, APP_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // flush / swap buffers
    glFlush();
}

void timer(int){
    render();
    glutTimerFunc(TIMER_MS, timer, 0);
}

void display(){}
int main(int argc, char *argv[]){
    glutInit(&argc, argv);
    glutInitWindowPosition(0, 0);
    glutInitWindowSize(APP_WIDTH, APP_HEIGHT);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
    glutCreateWindow(argv[0]);
    glutDisplayFunc(display);
    glutTimerFunc(TIMER_MS, timer, 0);
    init();

    glutMainLoop();
    return 0;
}

Figured it out. Needed to ping-pong between two textures for the alternating of read/write during rendering.

ShaderTest.cpp V3 (working)

//
//  code based on https://github.com/yasuohasegawa/OpenGL-Multipass-Shader-Sample 
//

#include <GL/glew.h>
#include <GL/glut.h>
#include "ShaderTest.h"

#define TIMER_MS 25
#define CYCLE 1000.0f
#define APP_WIDTH  640
#define APP_HEIGHT 360
#define BLIT_TESTING_MODE 0

#define TEXTURES_COUNT 2
static GLuint _s_texBuffer[TEXTURES_COUNT] = {0, 0};
static GLuint _s_texBufferIndex = 0;

static GLuint _s_program1 = 0;
static GLuint _s_program2 = 0;
static GLuint _s_fb = 0;
static GLuint _s_texImage = 0;
static GLuint _s_frame = 0;

static enum eDrawMethod{
    DRAW_QUAD,
    DRAW_TRIANGLE_FAN,
    DRAW_TRIANGLE_DUAL,
    DRAW_TRIANGLE_STRIP,
    DRAW_TRIANGLE_OVERSIZED
} _s_drawMethod = DRAW_QUAD;


void draw(){
    if (_s_drawMethod == DRAW_QUAD){
        glBegin(GL_QUADS);
            glTexCoord2f(0.0, 0.0); glVertex2i(0,            0);
            glTexCoord2f(1.0, 0.0); glVertex2i(APP_WIDTH,    0);
            glTexCoord2f(1.0, 1.0); glVertex2i(APP_WIDTH,    APP_HEIGHT);
            glTexCoord2f(0.0, 1.0); glVertex2i(0,            APP_HEIGHT);
        glEnd();

    } else if (_s_drawMethod == DRAW_TRIANGLE_FAN) {
        glBegin(GL_TRIANGLE_FAN);
            glVertex2i(0,            0);
            glVertex2i(0,            APP_HEIGHT);
            glVertex2i(APP_WIDTH,    APP_HEIGHT);
            glVertex2i(APP_WIDTH,    0);
        glEnd();

    // 1---2
    // | / |
    // 3---4
    } else if (_s_drawMethod == DRAW_TRIANGLE_DUAL) {
        glBegin(GL_TRIANGLES);
            glVertex2i(0,            APP_HEIGHT);    // 1
            glVertex2i(APP_WIDTH,    APP_HEIGHT);    // 2
            glVertex2i(0,            0);                // 3
            glVertex2i(APP_WIDTH,    0);                // 4
            glVertex2i(0,            0);                // 3
            glVertex2i(APP_WIDTH,    APP_HEIGHT);    // 2
        glEnd();

    } else if (_s_drawMethod == DRAW_TRIANGLE_STRIP) {
        glBegin(GL_TRIANGLE_STRIP);
            glVertex2i(0,            0);
            glVertex2i(APP_WIDTH,    0);
            glVertex2i(0,            APP_HEIGHT);
            glVertex2i(APP_WIDTH,    APP_HEIGHT);
        glEnd();

    } else if (_s_drawMethod == DRAW_TRIANGLE_OVERSIZED) {
        glBegin(GL_TRIANGLES);
            glVertex2i(0,            0);
            glVertex2i(0,            APP_HEIGHT);
            glVertex2i(APP_WIDTH,    APP_HEIGHT);
        glEnd();
    }
}

void init(){
    glewInit();
    _s_program1 = loadShader("./shaders/shader.vert","./shaders/ripples1_buffer.frag");
    _s_program2 = loadShader("./shaders/shader.vert","./shaders/ripples1_image.frag");

    // buffer textures
    for (int i = 0; i < TEXTURES_COUNT; ++i){
        glGenTextures(1, &_s_texBuffer[i]);
        glBindTexture(GL_TEXTURE_2D, _s_texBuffer[i]);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, APP_WIDTH, APP_HEIGHT, 0, GL_RGBA, GL_FLOAT, 0);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    // image/screen texture
    glGenTextures(1, &_s_texImage);
    glBindTexture(GL_TEXTURE_2D, _s_texImage);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, APP_WIDTH, APP_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);
    
    // fbo
    glGenFramebuffers(1, &_s_fb);
    glBindFramebuffer(GL_FRAMEBUFFER, _s_fb);
        for (int i = 0; i < TEXTURES_COUNT; ++i)
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, _s_texBuffer[i], 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void render(){
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glViewport(0, 0, APP_WIDTH, APP_HEIGHT);
    glOrtho(0, APP_WIDTH, 0, APP_HEIGHT, -1, 1);

    if (_s_drawMethod == DRAW_TRIANGLE_OVERSIZED){
        glTranslatef(0.0, -APP_HEIGHT, 0.0);
        glScalef(2.0, 2.0, 0.0);
    }

    // buffer
    glBindFramebuffer(GL_FRAMEBUFFER, _s_fb);
        glDrawBuffer(GL_COLOR_ATTACHMENT0 + _s_texBufferIndex);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, _s_texBuffer[_s_texBufferIndex ? 0 : 1]);
            glUseProgram(_s_program1);
            glUniform2f(glGetUniformLocation(_s_program1, "iViewportResolution"), (float) APP_WIDTH, (float) APP_HEIGHT);
            glUniform1f(glGetUniformLocation(_s_program1, "iTime"), (float) glutGet(GLUT_ELAPSED_TIME) / CYCLE);
            glUniform1i(glGetUniformLocation(_s_program1, "iFrame"), _s_frame++);
            glUniform1i(glGetUniformLocation(_s_program1, "iChannel0"), 0);
                draw();
            glUseProgram(0);
        glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // image / screen
    // check for blit testing, rather than rendering to the image/screen texture
#if BLIT_TESTING_MODE
    glBindFramebuffer(GL_FRAMEBUFFER, _s_fb);
        glReadBuffer(GL_COLOR_ATTACHMENT0 + _s_texBufferIndex);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
            glBlitFramebuffer(0, 0, APP_WIDTH, APP_HEIGHT, 0, 0, APP_WIDTH, APP_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
#else
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, _s_texBuffer[_s_texBufferIndex]);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, _s_texImage);
        glUseProgram(_s_program2);    
        glUniform2f(glGetUniformLocation(_s_program2, "iViewportResolution"), (float) APP_WIDTH, (float) APP_HEIGHT);
        glUniform1i(glGetUniformLocation(_s_program2, "iChannel0"), 0);
        glUniform1i(glGetUniformLocation(_s_program2, "iChannel1"), 1);
            draw();
        glUseProgram(0);
    glBindTexture(GL_TEXTURE_2D, 0);
#endif

    // update texture index (ping pong)
    _s_texBufferIndex = _s_texBufferIndex ? 0 : 1;

    glFlush();
}

void timer(int){
    render();
    glutTimerFunc(TIMER_MS, timer, 0);
}

void display(){}
int main(int argc, char *argv[]){
    glutInit(&argc, argv);
    glutInitWindowPosition(0, 0);
    glutInitWindowSize(APP_WIDTH, APP_HEIGHT);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
    glutCreateWindow(argv[0]);
    glutDisplayFunc(display);
    glutTimerFunc(TIMER_MS, timer, 0);
    init();
    glutMainLoop();
    return 0;
}