Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to apply a sinewave distortion effect purely in shaders?

Tags:

opengl

shader

Sinewave distortion of a 2D image is a classic visual effect: taking a 2D image and warping it along either the X or the Y axis by shifting pixels according to a sine wave. It ends up looking something like this: Y-axis sinewave distortion example

I've seen a few examples of code for it, and the standard way to do this with OpenGL seems to be, for a an image of dimensions (x, y):

for each column from 0 to X
  draw a single quad one pixel wide and y pixels high, offset by a sine wave value

Of course, this involves a lot of work on the client-side. Is there any way to draw a single quad and offload the distortion work to the GPU with shaders? Only vertex and fragment shaders; I'm using OpenGL 2, so there are no geometry shaders available.

I know I could use a fragment shader to sample texture coordinates that are offset by a sine wave, but getting them to place at locations outside the original box defined by the quad would be tricky, and I'd prefer not to have the output be clipped like in the sample picture. Is there any way around this problem?

like image 510
Mason Wheeler Avatar asked Nov 01 '11 18:11

Mason Wheeler


2 Answers

Yes, this can be done using shaders. Using a vertex shader you can apply a sine distortion on a grid. A fragment shader can modulate the texture coordinate, but not the target pixel location; fragment shaders are gatherers and can not do data scattering.

Update

Working example for texture coordinate modulation:

#include <stdlib.h>
#include <stdio.h>
#include <GL/glew.h>
#include <GL/glfw.h>

static void pushModelview()
{
    GLenum prev_matrix_mode;
    glGetIntegerv(GL_MATRIX_MODE, &prev_matrix_mode);
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glMatrixMode(prev_matrix_mode);
}

static void popModelview()
{
    GLenum prev_matrix_mode;
    glGetIntegerv(GL_MATRIX_MODE, &prev_matrix_mode);
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
    glMatrixMode(prev_matrix_mode);
}

static const GLchar *vertex_shader_source =
"#version 130\n"
"void main()"
"{"
"   gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;"
"   gl_TexCoord[0] = gl_MultiTexCoord0;"
"   gl_FrontColor = gl_Color;"
"   gl_BackColor = gl_Color;"
"}\0";
GLuint shaderVertex = 0;

static const GLchar *fragment_shader_source = 
"#version 130\n"
"uniform sampler2D texCMYK;\n"
"uniform sampler2D texRGB;\n"
"uniform float T;\n"
"const float pi = 3.14159265;\n"
"void main()\n"
"{\n"
"   float ts = gl_TexCoord[0].s;\n"
"   vec2 mod_texcoord = gl_TexCoord[0].st + vec2(0, 0.5*sin(T + 1.5*ts*pi));\n"
"   gl_FragColor = -texture2D(texCMYK, mod_texcoord) + texture2D(texRGB, gl_TexCoord[0].st);\n"
"}\n\0";
GLuint shaderFragment = 0;

GLuint shaderProgram = 0;

#define TEX_CMYK_WIDTH 2
#define TEX_CMYK_HEIGHT 2
GLubyte textureDataCMYK[TEX_CMYK_WIDTH * TEX_CMYK_HEIGHT][3] = {
    {0x00, 0xff, 0xff}, {0xff, 0x00, 0xff},
    {0xff, 0xff, 0x00}, {0x00, 0x00, 0x00}
};
GLuint texCMYK = 0;

#define TEX_RGB_WIDTH 2
#define TEX_RGB_HEIGHT 2
GLubyte textureDataRGB[TEX_RGB_WIDTH * TEX_RGB_HEIGHT][3] = {
    {0x00, 0x00, 0xff}, {0xff, 0xff, 0xff},
    {0xff, 0x00, 0x00}, {0x00, 0xff, 0x00}
};
GLuint texRGB = 0;

GLfloat cube_vertices[][8] =  {
    /*  X     Y     Z   Nx   Ny   Nz    S    T */
    {-1.0, -1.0,  1.0, 0.0, 0.0, 1.0, 0.0, 0.0}, // 0
    { 1.0, -1.0,  1.0, 0.0, 0.0, 1.0, 1.0, 0.0}, // 1
    { 1.0,  1.0,  1.0, 0.0, 0.0, 1.0, 1.0, 1.0}, // 2
    {-1.0,  1.0,  1.0, 0.0, 0.0, 1.0, 0.0, 1.0}, // 3

    { 1.0, -1.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0},
    {-1.0, -1.0, -1.0, 0.0, 0.0, -1.0, 1.0, 0.0},
    {-1.0,  1.0, -1.0, 0.0, 0.0, -1.0, 1.0, 1.0},
    { 1.0,  1.0, -1.0, 0.0, 0.0, -1.0, 0.0, 1.0},

    {-1.0, -1.0,  1.0, -1.0, 0.0, 0.0, 0.0, 0.0},
    {-1.0, -1.0, -1.0, -1.0, 0.0, 0.0, 1.0, 0.0},
    {-1.0,  1.0, -1.0, -1.0, 0.0, 0.0, 1.0, 1.0},
    {-1.0,  1.0,  1.0, -1.0, 0.0, 0.0, 0.0, 1.0},

    { 1.0, -1.0, -1.0,  1.0, 0.0, 0.0, 0.0, 0.0},
    { 1.0, -1.0,  1.0,  1.0, 0.0, 0.0, 1.0, 0.0},
    { 1.0,  1.0,  1.0,  1.0, 0.0, 0.0, 1.0, 1.0},
    { 1.0,  1.0, -1.0,  1.0, 0.0, 0.0, 0.0, 1.0},

    { 1.0, -1.0, -1.0,  0.0, -1.0, 0.0, 0.0, 0.0},
    {-1.0, -1.0, -1.0,  0.0, -1.0, 0.0, 1.0, 0.0},
    {-1.0, -1.0,  1.0,  0.0, -1.0, 0.0, 1.0, 1.0},
    { 1.0, -1.0,  1.0,  0.0, -1.0, 0.0, 0.0, 1.0},

    {-1.0, 1.0,  1.0,  0.0,  1.0, 0.0, 0.0, 0.0},
    { 1.0, 1.0,  1.0,  0.0,  1.0, 0.0, 1.0, 0.0},
    { 1.0, 1.0, -1.0,  0.0,  1.0, 0.0, 1.0, 1.0},
    {-1.0, 1.0, -1.0,  0.0,  1.0, 0.0, 0.0, 1.0},
};

static void draw_cube(void)
{
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glVertexPointer(3, GL_FLOAT, sizeof(GLfloat) * 8, &cube_vertices[0][0]);
    glNormalPointer(GL_FLOAT, sizeof(GLfloat) * 8, &cube_vertices[0][3]);
    glTexCoordPointer(2, GL_FLOAT, sizeof(GLfloat) * 8, &cube_vertices[0][6]);

    glDrawArrays(GL_QUADS, 0, 24);
}

static void bind_sampler_to_unit_with_texture(GLchar const * const sampler_name, GLuint texture_unit, GLuint texture)
{
        glActiveTexture(GL_TEXTURE0 + texture_unit); 
        glBindTexture(GL_TEXTURE_2D, texture);
        GLuint loc_sampler = glGetUniformLocation(shaderProgram, sampler_name);
        glUniform1i(loc_sampler, texture_unit);
}

static void display(double T)
{
    int window_width, window_height;

    glfwGetWindowSize(&window_width, &window_height);
    if( !window_width || !window_height )
        return;

    const float window_aspect = (float)window_width / (float)window_height;

    glDisable(GL_SCISSOR_TEST);

    glClearColor(0.5, 0.5, 0.7, 1.0);
    glClearDepth(1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    glEnable(GL_DEPTH_TEST);
    glViewport(0, 0, window_width, window_height);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glFrustum(-window_aspect, window_aspect, -1, 1, 1, 100);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0, 0, -5);

    pushModelview();
    glRotatef(T * 0.1 * 180, 0., 1., 0.);
    glRotatef(T * 0.1 *  60, 1., 0., 0.);
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

    glUseProgram(shaderProgram);
    glUniform1f(glGetUniformLocation(shaderProgram, "T"), T);
    bind_sampler_to_unit_with_texture("texCMYK", 0, texCMYK);
    bind_sampler_to_unit_with_texture("texRGB", 1, texRGB);

    draw_cube();
    popModelview();

    glfwSwapBuffers();
}

static int open_window(void)
{
#if 0
    glfwWindowHint(GLFW_OPENGL_VERSION_MAJOR, 2);
    glfwWindowHint(GLFW_OPENGL_VERSION_MINOR, 0);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
#endif

    if( glfwOpenWindow(0, 0,     /* default size */
                       8,  8, 8, /* 8 bits per channel */
                       8, 24, 8, /* 8 alpha, 24 depth, 8 stencil */
                       GLFW_WINDOW) != GL_TRUE ) {
        fputs("Could not open window.\n", stderr);
        return 0;
    }

    if( glewInit() != GLEW_OK ) {
        fputs("Could not initialize extensions.\n", stderr);
        return 0;
    }
    return 1;
}

static int check_extensions(void)
{
    if( !GLEW_ARB_vertex_shader ||
        !GLEW_ARB_fragment_shader ) {
        fputs("Required OpenGL functionality not supported by system.\n", stderr);
        return 0;
    }

    return 1;
}

static int check_shader_compilation(GLuint shader)
{
    GLint n;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &n);
    if( n == GL_FALSE ) {
        GLchar *info_log;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &n);
        info_log = malloc(n);
        glGetShaderInfoLog(shader, n, &n, info_log);
        fprintf(stderr, "Shader compilation failed: %*s\n", n, info_log);
        free(info_log);
        return 0;
    }
    return 1;
}

static int init_resources(void)
{
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
    glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);

    glGenTextures(1, &texCMYK);
    glBindTexture(GL_TEXTURE_2D, texCMYK);
    glTexImage2D(GL_TEXTURE_2D, 0,  GL_RGB8, TEX_CMYK_WIDTH, TEX_CMYK_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, textureDataCMYK);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    glGenTextures(1, &texRGB);
    glBindTexture(GL_TEXTURE_2D, texRGB);
    glTexImage2D(GL_TEXTURE_2D, 0,  GL_RGB8, TEX_RGB_WIDTH, TEX_RGB_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, textureDataRGB);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    shaderVertex = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(shaderVertex, 1, (const GLchar**)&vertex_shader_source, NULL);
    glCompileShader(shaderVertex);
    if( !check_shader_compilation(shaderVertex) )
        return 0;

    shaderFragment = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(shaderFragment, 1, (const GLchar**)&fragment_shader_source, NULL);
    glCompileShader(shaderFragment);
    if( !check_shader_compilation(shaderFragment) )
        return 0;

    shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, shaderVertex);
    glAttachShader(shaderProgram, shaderFragment);
    glLinkProgram(shaderProgram);

    return 1;
}

static void main_loop(void)
{
    glfwSetTime(0);
    while( glfwGetWindowParam(GLFW_OPENED) == GL_TRUE ) {
        display(glfwGetTime());
    }
}

int main(int argc, char *argv[])
{
    if( glfwInit() != GL_TRUE ) {
        fputs("Could not initialize framework.\n", stderr);
        return -1;
    }

    if( !open_window() )
        return -1;

    if( !check_extensions() )
        return -1;

    if( !init_resources() )
        return -1;

    main_loop();

    glfwTerminate();
    return 0;
}

The fragment shader part is this:

#version 130
uniform sampler2D texCMYK;
uniform sampler2D texRGB;
uniform float T;
const float pi = 3.14159265;
void main()
{
    float ts = gl_TexCoord[0].s;
    vec2 mod_texcoord = gl_TexCoord[0].st + vec2(0, 0.5*sin(T + 1.5*ts*pi));
    gl_FragColor = -texture2D(texCMYK, mod_texcoord) + texture2D(texRGB, gl_TexCoord[0].st);
};

Update – a shader that "expands":

uniform sampler2D texCMYK;
uniform sampler2D texRGB;
uniform float T;
const float pi = 3.14159265;
void main()
{
   float ts = gl_TexCoord[0].s;
   vec2 mod_texcoord = gl_TexCoord[0].st*vec2(1., 2.) + vec2(0, -0.5 + 0.5*sin(T + 1.5*ts*pi));
   if( mod_texcoord.t < 0. || mod_texcoord.t > 1. ) { discard; }
   gl_FragColor = -texture2D(texCMYK, mod_texcoord) + texture2D(texRGB, gl_TexCoord[0].st);
};
like image 83
datenwolf Avatar answered Oct 15 '22 09:10

datenwolf


For a given input quad render a quad 2 * max_amplitude taller (maybe with a vertex shader?) and in your pixel shader discard pixels that aren't currently being sin()'d onto.

That way you can reach "outside" your original quad.

like image 29
genpfault Avatar answered Oct 15 '22 08:10

genpfault