Diffusion effect tutorial
(c) Sylvain Lefebvre 2001 - http://www.aracknea.net

View source code


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 need

All that you need is an understanding of basic C and OpenGL principles. The (very very simple :) interface will be created using GLUT.

Theory
This effect is essentially based on the (very rough) simulation of diffusion (like smoke in a room).

How does it works ?
The rendering process begins by drawing a texture deformed by a vector field (I will explain how later). Now, imagine that you take this drawing and re-used it as the texture for the next rendering pass. The deformation, applied in an iterative way, will generate a movement. Let me do a little example. On the line that follows, the deformation take the line and decal it to the left (an 'O' represents a white pixel, an '|' a black pixel).
|0||||| -> ||0||||
Now, if we apply this process in an itterative way, re-using the result of the previous pass for the next, a movement will appear:
|0||||| -> ||0|||| -> |||0||| -> ||||0|| -> |||||0|
We can easily add an attenuation effect: before re-using the previous result, we make it darker so as it tends to disapear.
|0||||| -> ||o|||| -> |||.||| -> |||||||

Texture deformation by a vector field
The rendering is done in a regular grid. Each cell of the grid is an OpenGL textured quad. The distortion is obtained by defining a vector at each grid point. This vector is added to the texture coordinates during texture mapping. The deformation at each point is automatically interpolated by OpenGL, this is why a blured effect is obtained.
These pictures show the vector field (in green) used to distored the texture (click to enlarge).
This picture shows the regular grid and vectors used to perturb texture coordinates.
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();
}


 


 
Tutorial generated by