| |||
|
Goal This tutorial presents a simple and efficient algorithm to create a well-known effect, which can be seen in a lot of WinAMP plug-ins such as "Geiss". What you needAll that you need is an understanding of basic C and OpenGL principles. The (very very simple :) interface will be created using GLUT. TheoryThis effect is essentially based on the (very rough) simulation of diffusion (like smoke in a room).
How does it works ?
Texture deformation by a vector field
It is important to note that the grid (geometric quads + texture coordinates) is only computed one time and doesn't change anymore (as long as you want the same movement). The grid is used to do one deformation step. The movement is generated by the fact that the result of this deformation is re-used as the entry texture of the next step (as explained before). You didn't understand all that stuff ? Don't worry, read comments in C code and then come back an read this section again :)) Practice | |||
/* -------------------------------------------------------- */ #include <GL/gl.h> #include <stdio.h> #include <GL/glut.h> #include <time.h> #include <sys/types.h> #include <math.h> #include <stdlib.h> /* -------------------------------------------------------- */ #define GRID_RES 10 // Grid resolution /* -------------------------------------------------------- */ int g_iMainWindow; // Glut Window Id int g_iDeform=2; // Current deformation number int g_iDrawLines=1; // Should we draw random lines ? int g_iDrawCube=1; // Should we draw a nice wireframe 3d cube ? int g_iFrame=0; // Frame counter int g_iClear=1; // Should we clear the texture int g_iShowField=0; // Should we draw the vector field ? int g_iPaintBrush=0; // Should we draw a square under mouse cursor ? double g_dBrushX,g_dBrushY; // Position of mouse cursor GLuint g_uiTexId; // OpenGL texture Id GLuint g_uiGridList; // OpenGL draw list for the grid | |||
|
Glut keyboard Callback.   The keyboard is used to interact with the effect. Key bindings is [SPACE] - draw lines [+] - change deformation [o] - draw object [c] - clear [f] - show vector field [q] - quit | |||
void mainKeyboard(unsigned char key, int x, int y)
{
if (key == '+')
{
g_iDeform=(g_iDeform+1) % 5;
}
else if (key == ' ')
{
g_iDrawLines=1; //!g_iDrawLines;
}
else if (key == 'o')
{
g_iDrawCube=!g_iDrawCube;
}
else if (key == 'c')
{
g_iClear=1;
}
else if (key == 'f')
{
g_iShowField=!g_iShowField;
}
else if (key == 'q')
{
exit (0);
}
}
| |||
|
Glut Callback for mouse click events. The mouse is used to paint colors directly in the effect. Each time the user press down a button, drawing under mouse cursor is started. When the button is released, drawing is stoped. | |||
void mainMouse(int btn, int state, int x, int y)
{
if (state == GLUT_DOWN)
{
g_iPaintBrush=1;
g_dBrushX=((double)x)/((double)glutGet(GLUT_WINDOW_WIDTH));
g_dBrushY=((double)y)/((double)glutGet(GLUT_WINDOW_HEIGHT));
}
else
{
g_iPaintBrush=0;
}
}
| |||
|
Glut Callback for mouse movement when a button is pressed. I use this callback to trace mouse cursor position. | |||
void mainMotion(int x,int y)
{
g_dBrushX=((double)x)/((double)glutGet(GLUT_WINDOW_WIDTH));
g_dBrushY=((double)y)/((double)glutGet(GLUT_WINDOW_HEIGHT));
}
| |||
|
Glut Callback for screen resize. Force window size to be 512x512. Window size should be a power of 2 (this is due to OpenGL texture size limitation). | |||
void mainReshape(int w,int h)
{
glutSetWindow(g_iMainWindow);
glViewport(0,0,w,h);
if (w != 512 || h != 512)
glutReshapeWindow(512,512);
}
| |||
|
Compute texture coordinates in each grid point with distortion. First, the texture coordinates are computed without distortion, next, in the switch statement, these coordinates are perturbated by various funny sin/cos functions. If you want to obtain crazy effects, come here and play !! | |||
void genTexCoords(int time,int res,double ***tbl)
{
double x,y,c,s,d;
int i,j;
for (i=0;i<res;i++)
{
for (j=0;j<res;j++)
{
tbl[i][j][0]=((double)i)/((double)res-1.0);
tbl[i][j][1]=((double)j)/((double)res-1.0);
switch (g_iDeform)
{
case 0:
tbl[i][j][0]+=0.01;
tbl[i][j][1]+=0.01;
break;
case 1:
tbl[i][j][0]+=(i-res/2.0)*0.001;
tbl[i][j][1]+=(j-res/2.0)*0.001;
break;
case 2:
tbl[i][j][0]*=cos((i+j-res)/50.0*M_PI);
tbl[i][j][1]*=cos((i-j)/100.0*M_PI);
break;
case 3:
tbl[i][j][0]+=(i-res/2.0)*0.003;
tbl[i][j][1]+=(j-res/2.0)*0.003;
tbl[i][j][0]-=0.5;
tbl[i][j][1]-=0.5;
x=tbl[i][j][0];
c=cos(M_PI/32.0+(i-res/2)*M_PI/32.0);
s=sin(M_PI/32.0+(j-res/2)*M_PI/32.0);
tbl[i][j][0]=c*x-s*tbl[i][j][1];
tbl[i][j][1]=s*x+c*tbl[i][j][1];
tbl[i][j][0]+=0.5;
tbl[i][j][1]+=0.5;
break;
case 4:
tbl[i][j][0]-=0.5;
tbl[i][j][1]-=0.5;
x=tbl[i][j][0];
d=sqrt((i-res/2.0)*(i-res/2.0)+(j-res/2.0)*(j-res/2.0));
c=cos((d)*M_PI/64.0);
s=sin((d)*M_PI/64.0);
tbl[i][j][0]=c*x-s*tbl[i][j][1];
tbl[i][j][1]=s*x+c*tbl[i][j][1];
tbl[i][j][0]+=0.5;
tbl[i][j][1]+=0.5;
break;
default:
break;
}
}
}
}
| |||
|
Draw something to the screen without putting it in the effect . Draw a 3d wire cube in white. Just add code to draw something else. | |||
void noDiffuseDraw()
{
if (g_iDrawCube)
{
// draw a wire cube
// -> projection
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(90.0,1.0,1.0,100.0);
gluLookAt(7.0,7.0,7.0,
0.0,0.0,0.0,
0.0,0.0,1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// random movement
glRotated(2.0*g_iFrame,0.0,0.0,1.0);
glTranslated(3.0*cos(g_iFrame/1800.0),2.0*sin(g_iFrame/1000.0),0.0);
glRotated(10.0*g_iFrame,0.0,1.0,1.0);
glTranslated(sin(g_iFrame/1500.0),5.0*sin(g_iFrame/2000.0),0.0);
glDisable(GL_TEXTURE_2D);
glLineWidth(3.0);
glColor3d(1.0,1.0,1.0);
glutWireCube(5.0);
}
}
| |||
|
Draw something and put it in the effect. All that is drawn here will be absorbed by the effect. I'm just drawing some random lines, a 3d cube and a square under the mouse cursor. Just add code to draw something else. Note that if you want a color saturation effect, don't use pure RGB colors like (1.0,0.0,0.0). | |||
void diffuseDraw()
{
int i;
double x,y;
if (g_iDrawLines)
{
// draw random lines
glDisable(GL_TEXTURE_2D);
glLineWidth(1.0+(rand()%3));
glEnable(GL_BLEND);
glBlendFunc(GL_ONE,GL_ONE);
glColor3d(1.0,0.6,0.7);
glBegin(GL_LINE_STRIP);
for (i=0;i<10;i++)
{
x=drand48();
y=drand48();
glVertex2d(x,y);
}
glEnd();
glDisable(GL_BLEND);
g_iDrawLines=0;
}
if (g_iPaintBrush)
{
// draw random lines
glDisable(GL_TEXTURE_2D);
glLineWidth(1.0+(rand()%3));
glEnable(GL_BLEND);
glBlendFunc(GL_ONE,GL_ONE_MINUS_SRC_COLOR);
glColor3d(1.0,0.6,0.7);
glTranslated(g_dBrushX,1.0-g_dBrushY,0.0);
glBegin(GL_QUADS);
glVertex2d(-0.04,-0.04);
glVertex2d(0.04,-0.04);
glVertex2d(0.04,0.04);
glVertex2d(-0.04,0.04);
glEnd();
glDisable(GL_BLEND);
g_iDrawLines=0;
}
if (g_iDrawCube)
{
// draw a wire cube
// -> projection
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(90.0,1.0,1.0,100.0);
gluLookAt(7.0,7.0,7.0,
0.0,0.0,0.0,
0.0,0.0,1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// random movement
glRotated(2.0*g_iFrame,0.0,0.0,1.0);
glTranslated(3.0*cos(g_iFrame/1800.0),2.0*sin(g_iFrame/1000.0),0.0);
glRotated(10.0*g_iFrame,0.0,1.0,1.0);
glTranslated(sin(g_iFrame/1500.0),5.0*sin(g_iFrame/2000.0),0.0);
glDisable(GL_TEXTURE_2D);
glLineWidth(1.0+(rand()%3));
glEnable(GL_BLEND);
glBlendFunc(GL_ONE,GL_ONE);
glColor3d(0.1,0.1,0.6);
glutWireCube(5.0);
glDisable(GL_BLEND);
}
}
| |||
|
Draw the grid and the vector field. Nothing special. | |||
void drawVectorField(int res,double ***texCoords)
{
int i,j;
double u,v;
glPushAttrib(GL_ENABLE_BIT);
// projection
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0,1.0,0.0,1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// attributes
glDisable(GL_TEXTURE_2D);
glLineWidth(1.0);
glBegin(GL_LINES);
glColor3d(0.0,1.0,0.0);
for (i=0;i<GRID_RES;i++)
{
for (j=0;j<GRID_RES;j++)
{
u=texCoords[i][j][0]-((double)i)/((double)res-1.0);
v=texCoords[i][j][1]-((double)j)/((double)res-1.0);
glVertex2d(((double)i)/((double)res-1.0),((double)j)/((double)res-1.0));
glVertex2d(((double)i)/((double)res-1.0)+u,((double)j)/((double)res-1.0)+v);
}
}
glEnd();
glColor3d(1.0,1.0,1.0);
glPolygonMode(GL_FRONT,GL_LINE);
glCallList(g_uiGridList);
glPolygonMode(GL_FRONT,GL_FILL);
glPopAttrib();
}
| |||
|
Compute the OpenGL draw list for the grid. The grid is simply made of OpenGL quads. Texture coordinate are read in a texture table generated by genTexCoords. | |||
void computeGrid(int res,double ***texCoords)
{
int i,j;
glNewList(g_uiGridList,GL_COMPILE);
glBegin(GL_QUADS);
for (i=0;i<GRID_RES;i++)
{
for (j=0;j<GRID_RES;j++)
{
glTexCoord2d(texCoords[i][j][0],texCoords[i][j][1]);
glVertex2d(((double)i)/((double)GRID_RES),((double)j)/((double)GRID_RES));
glTexCoord2d(texCoords[i+1][j][0],texCoords[i+1][j][1]);
glVertex2d(((double)i+1)/((double)GRID_RES),((double)j)/((double)GRID_RES));
glTexCoord2d(texCoords[i+1][j+1][0],texCoords[i+1][j+1][1]);
glVertex2d(((double)i+1)/((double)GRID_RES),((double)j+1)/((double)GRID_RES));
glTexCoord2d(texCoords[i][j+1][0],texCoords[i][j+1][1]);
glVertex2d(((double)i)/((double)GRID_RES),((double)j+1)/((double)GRID_RES));
}
}
glEnd();
glEndList();
}
| |||
|
Here it is ! The main rendering function. Calls everything and makes things work. Comments are in the code. | |||
void mainRender()
{
static int deform=-1;
static double ***texCoords=NULL;
static int first=1;
int i,j;
GLint v[4];
| |||
| The code below allocate the texture coordinate table at first call. | |||
if (texCoords == NULL)
{
// first alloc
texCoords=new double **[GRID_RES+1];
for (i=0;i<GRID_RES+1;i++)
texCoords[i]=new double *[GRID_RES+1];
for (i=0;i<GRID_RES+1;i++)
for (j=0;j<GRID_RES+1;j++)
texCoords[i][j]=new double [2];
}
| |||
| If the desired deformation changed, we need to compute texture coordinates again. Note that this is done at first call. | |||
if (first || deform != g_iDeform)
{
genTexCoords(0,GRID_RES+1,(double ***)texCoords);
computeGrid(GRID_RES,(double ***)texCoords);
deform=g_iDeform;
}
// drawing context is in main window
glutSetWindow(g_iMainWindow);
// clear screen
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
| |||
| At first call, we need to put something in the texture before using it. | |||
if (first)
{
// screen -> texture (init)
glGetIntegerv(GL_VIEWPORT,v);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,g_uiTexId);
glCopyTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,0,0,v[2],v[3],0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,GL_CLAMP);
glTexEnvi(GL_TEXTURE_2D, GL_TEXTURE_ENV_MODE ,GL_MODULATE);
first=0;
}
// projection
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0,1.0,0.0,1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// drawing diffusion grid
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,g_uiTexId);
glColor3d(0.98,0.98,0.98);
| |||
| Here we ask OpenGL to draw the grid. The code that will be executed by OpenGL is the code between glNewList and glEndList in . I use an OpenGL draw list because it's faster. | |||
glCallList(g_uiGridList);
// draw for diffusion
diffuseDraw();
// count frames
g_iFrame++;
// flush OpenGL operations
glFlush();
if (g_iClear)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
g_iClear=0;
}
| |||
| Copy the screen to the texture. Next time mainRender will use this as the texture to be drawn by the grid. This create the movement. If there's one important thing to understand in all that page it's what's done below ! | |||
glGetIntegerv(GL_VIEWPORT,v); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D,g_uiTexId); glCopyTexSubImage2D(GL_TEXTURE_2D,0,0,0,0,0,v[2],v[3]); | |||
| After copying the screen into the texture, all that is drawn wouldn't appear in the effect. | |||
// draw without diffusion
noDiffuseDraw();
// if asked, draw the vector field
if (g_iShowField)
drawVectorField(GRID_RES+1,(double ***)texCoords);
// swap
glutSwapBuffers();
}
| |||
|
Glut Idle Callback. This callback is called whenever the system has nothing better to do. It's the perfect moment to ask for redraw ! | |||
void idle( void )
{
glutSetWindow(g_iMainWindow);
glutPostRedisplay();
}
| |||
|
Main proc. Init Glut and OpenGL. No more, no less. | |||
main(int argc, char **argv)
{
int i;
// GLUT
glutInit(&argc, argv);
glutInitWindowSize(512, 512);
glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH);
// main window
g_iMainWindow=glutCreateWindow("Plin");
glutMouseFunc(mainMouse);
glutMotionFunc(mainMotion);
glutKeyboardFunc(mainKeyboard);
glutDisplayFunc(mainRender);
glutReshapeFunc(mainReshape);
glutIdleFunc(idle);
glutSetWindow(g_iMainWindow);
// Init OpenGL
glClearColor(0.0, 0.0, 0.0, 1.0);
glDisable(GL_LIGHTING);
glDisable(GL_DEPTH_TEST);
glEnable(GL_COLOR_MATERIAL);
glEnable(GL_CULL_FACE);
glGenTextures(1,&g_uiTexId);
g_uiGridList=glGenLists(1);
printf("[SPACE] - draw lines\n");
printf("[+] - change deformation\n");
printf("[o] - draw object\n");
printf("[c] - clear\n");
printf("[f] - show vector field\n");
printf("[q] - quit\n");
// let's go
glutMainLoop();
}
| |||
|
| |||