Skip navigation

Understanding vertex shaders

Absolutely minimal CG program for good fundamentals understanding

First, make sure you install CG!

// FILE:  AbsoluteMinimalCGProgram.cpp
//////////////////////////////////////////
// ABSOLUTE MINIMAL CG PROGRAM:         //
// BASIC DRAWING IN 2D USING CG         //
//                                      //
// This example discusses the absolute  //
// basics of using CG with OpenGL.      //
//                                      //
// As a prerequisite, you should        //
// already have experience with         //
// OpenGL.  If ya don't. . . I suggest  //
// you get some practice, before        //
// attempting to continue with this     //
// series.                              //
//                                      //
// You found this at bobobobo's weblog, //
// http://bobobobo.wordpress.com        //
//                                      //
// Creation date:  Jan 21/08            //
// Last modified:  Jan 23/08            //
//                                      //
//////////////////////////////////////////

// Loosely based on Chapter 2 of "The Cg Tutorial" by Kilgard and Fernando,
// and the associated code files.

// The original Cg Tutorial code files are freely available for download at
// http://developer.nvidia.com/object/cg_tutorial_home.html

////////////////////////////////////////////////////////////////////
// To use this, get and install the Cg Toolkit and make sure you  //
// get the examples that come with the Cg Toolkit to work first.  //
////////////////////////////////////////////////////////////////////

#include <stdio.h>
#include <stdlib.h>
#include <GL/glut.h>

// If having problems compiling, ('fatal error') see
// http://bobobobo.wordpress.com/2008/01/22/setting-up-cg-environment-variables/
#include <Cg/cg.h>
#include <Cg/cgGL.h>

// These link the Cg libraries.
#pragma comment( lib, "cg.lib" )
#pragma comment( lib, "cgGL.lib" )


// Ok, this package has 2 files:
// AbsoluteMinimalCGProgram.CPP:  The C++ code file.
// vertexShader.CG:               The vertex shader file.
// Take a real quick glance at both, up and down.

//  ________   _____
// |___  ___| |  _  |
//    |  |    | | | |
//    |  |    | |_| |
//    |__|    |_____| do.
//
// 1)  One of the main ideas of this package is to compare
//     OpenGL program behavior when our CG shader is on, and
//     when there is no shader being used at all (classic OpenGL
//     rendering).

//     Go down to the #pragma region part that says
//     "this code has no effect when the shader is on".
//     Verify that claim is true.  What other types of OpenGL
//     function call appears to have no effect when the CG Shader is on?

//     Change the values that are going into gluPerspective and gluLookAt()
//     Change them to completely wild values.  Take the eye
//     really really far away so that the set of triangles is
//     a mere speck. . .  Then turn the shader on.

// Conclusion:  IF YOU HAVE YOUR VERTEX SHADER ON, THEN ALL YOUR
// CODE that uses the OpenGL matrix stakcs __DOES NOT WORK ANYMORE__.

// !!  HMM!!  This is an important point.  This should be very interesting to you.  How
// are we going to make it look 3D without gluPerspective???
// Trust me, we will. . .

// 2)  Play around with the drawing code a bit.  Draw your own shapes.
//     Get a feel for what shows up in the window, and what does not,
//     when the shader is switched on.
//     What are the max/min values of x,y and z that glVertex3f() can take?
//
#pragma region answer to 2
//     ANSWER:  x, y and z can only take values between
//              [-1, +1] each if they are to show up in the final
//              render when the shader is on.
#pragma endregion

//
// 3)  Figure this out:
//     What is the effect of z-value when
//     the shader is on???  When does Z
//     appear to have any affect on the final drawing?
//     (try very large AND very small values of z!)
#pragma region answer to 3
     // At this point, Z-value will appear to have NO EFFECT!
     // To make the red square go in the back, put code like
#pragma endregion

// 4)  Draw a huge red square BEHIND the triangles
//     that appear when the shader is on.
//     How can you make the red square appear behind all
//     the triangles?
#pragma region answer to 4

// Use code like
/// glBegin( GL_QUADS ); glVertex3f(1,1,0);glVertex3f(-1,1,0);
/// glVertex3f(-1,-1,0);glVertex3f(1,-1,0); glEnd();

// _BEFORE_ the glBegin(); for the other triangles.

#pragma endregion


////////////////////////
// GLOBALS:  Here we declare 3 global variables
//           for use by our CG program.  More detail
//           when they're actually used.
CGcontext   myCgContext;        // Like a huge container for all of our CG stuff
CGprofile   myCgVertexProfile;  // A CGprofile contains a summary of the
                                // capabilities of the hardware that this
                                // CG program is going to run on.
CGprogram   myCgVertexProgram;  // And finally this global variable will be
                                // a reference to the program itself, once
                                // it has been compiled.

bool shaderOn = false;          // This is a global var that we use to
                                // switch on and off the shader, so
                                // we can see the diff.  You press spacebar.

// Below is a diagram that kind of shows in a big picture
// sort of way how the CGcontext, CGprofile and CGprogram exist.
/*
           /----------------------------------\
          /                                    \
         /                                      \
        /                                        \
       /                                          \
       \   CGprogram  ----- USES ----> CGprofile  /
        \                                        /
         \                                      /
          \                                    /
           \----------------------------------/
                         CGcontext
   
   CGprogram USES CGprofile, when CGprogram is compiled.

   CGcontext contains both CGprogram AND CGprofile.

*/

// Function that checks to see if CG is crying
// about some error.  We run this function
// after every Cg command we execute from
// our C++ code.
void checkForCgError(const char *situation)
{
  CGerror error;
  const char *string = cgGetLastErrorString(&error);

  if (error != CG_NO_ERROR) {
    printf("%s: %s: %s\n",
           "ERROR", situation, string);
    if (error == CG_COMPILER_ERROR) {
        printf("%s\n", cgGetLastListing(myCgContext));
    }
    exit(1);
  }
}


////////////////////////
// display() function
// Once the line of code that says "glutMainLoop()" executes
// we'll be trapped cycling in this "display" function
// forever until someone hits the ESC key.
void display()
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  if( shaderOn == true )
  {
    // !! Next 2 lines actually TURN ON THE VERTEX SHADER!!
    cgGLBindProgram(myCgVertexProgram);
    cgGLEnableProfile(myCgVertexProfile);
  }
  
  
  #pragma region this code has no effect when shader is on
  glMatrixMode( GL_PROJECTION );
  glLoadIdentity();
  gluPerspective( 45, 1.0, 0.4, 1000 );

  glMatrixMode( GL_MODELVIEW );
  glLoadIdentity();
  gluLookAt( 6.5, 0.0, 5.0,
             0.0, 0.0, 0.0,
             0.0, 1.0, 0.0 );
  #pragma endregion
  

  // Notice how we're always picking
  // values between -1 and +1.
  // That's important!  Try drawing your own stuff.
  glBegin(GL_TRIANGLES);
  
    // First, a red triangle that goes from the middle to
    // the top right corner.
    glColor3d ( 1,     0,     0 );
    glVertex3d( 0,     0,     0 );
    glVertex3d( 0,     0.98,  0 );
    glVertex3d( 0.98,  0.98,  0 );

    // Second, a green triangle that goes from the middle to
    // the top left corner
    glColor3d ( 0,     1,     0 );
    glVertex3d( 0,     0,     0 );
    glVertex3d( 0,     0.98,  0 );
    glVertex3d(-0.98,  0.98,  0 );

    // Third, a blue triangle that goes from the middle to
    // the bottom left corner
    glColor3d ( 0,     0,     1 );
    glVertex3d( 0,     0,     0 );
    glVertex3d( 0,    -0.98,  0 );
    glVertex3d(-0.98, -0.98,  0 );

    // Finally, a white triangle in the bottom right corner
    glColor3d ( 1,     1,     1 );
    glVertex3d( 0,     0,     0 );
    glVertex3d( 0,    -0.98,  0 );
    glVertex3d( 0.98, -0.98,  0 );

  glEnd();

  if( shaderOn == true )
  {
    // Now that we are totally done drawing, we want to
    // actually TURN OFF the shader.
    cgGLDisableProfile(myCgVertexProfile);      // !! SHADER OFF
  }

  glutSwapBuffers();
}

void keyboard(unsigned char c, int x, int y)
{
  switch (c) {
  case 27:  /* Esc key */
    /* Demonstrate proper deallocation of Cg runtime data structures.
       Not strictly necessary if we are simply going to exit. */
    cgDestroyProgram(myCgVertexProgram);
    cgDestroyContext(myCgContext);
    exit(0);
    break;
  case ' ': // spacebar
    {
        shaderOn = !shaderOn;   // flip

        char t[100];
        sprintf( t, "First Cg program!  Shader is %s", shaderOn ? "ON!" : "off." );
        glutSetWindowTitle( t );
        
        glutPostRedisplay();    // redraw screen
    }
    break;
  }
} 

int main(int argc, char **argv)
{
  // 0.  Initialize GLUT.
  // Thanks Kilgard!  You da bomb.  :D.
  #pragma region GLUT INIT
  // If we weren't using GLUT, we'd have much 
  // more than 6 lines of code to write to get
  // a window up in Windows.

  glutInitWindowSize(600, 600);
  glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH);
  glutInit(&argc, argv);

  glutCreateWindow("First Cg program!  Press spacebar to turn shader ON!");
  glutDisplayFunc(display);
  glutKeyboardFunc(keyboard);
  #pragma endregion

  glClearColor(0.1, 0.12, 0.3, 0.0);  // dark background

  #pragma region CG INIT
  //////////////////////////////////////////////
  //
  // 1.  CREATE the Cg CONTEXT.
  //////////////////////////////////////////////
  #pragma region create Cg context
  // What's a "context"?
  
  // When you're programming, the word comes up a lot.
  // In Windows programming, you hear a lot about "device context."
  // With OpenGL, you talk about the "rendering context."

  // In plain English, a "context" is like a "circumstance."
  // You usually need to know the "context" of a sentence
  // to totally understand what it means.

  // "Sorry, I should have held that."

  // You'd need a context for that sentence for it
  // to make any sense.

  // In computing though, a context is a little more than
  // just a situation.

  // A "CONTEXT" in computing specifically is
  // a COLLECTION of variables and ALL the data that represents
  // the overall STATE of a program.

  // One of the main jobs of your operating system is to
  // handle "CONTEXT SWITCHES" -- ie to swap out
  // a program and all of its associated variables from
  // the CPU's registers, so another program can have
  // a chance to run for microsecond or two.

  // Read this:  Context switch on Wikipedia

  // So for Cg, a context will be a COLLECTION OF ALL
  // THE ASSOCIATED INFORMATION THAT HAS TO DO WITH
  // THIS CG SHADER.
  #pragma endregion

  // And so, now we shall create the context.
  myCgContext = cgCreateContext();
  checkForCgError("creating context");

  ////////////////////////////////////////
  // 
  // 2.  PICK A CG PROFILE TO USE
  ////////////////////////////////////////
  #pragma region select a CG profile
  // What's a CG PROFILE?
  
  // So what's a PROFILE in CG?  Its where


  // CG programs go to show off the number
  // of friends they have . . . :)

  // But really, a CG PROFILE is kind of like
  // a personality profile, only it tells the
  // CG Compiler what assembly level instructions
  // the present graphics hardware is capable of
  // processing.

  // The information about the PROFILE is used
  // WHEN THE CG PROGRAM IS COMPILED.

  // 'case ya didn't know, CG Programs are COMPILED
  // AT RUN TIME.  The CG CODE is normally __NOT__ compiled
  // along with the rest of your C program
  // when you first press F5 in Visual Studio.

  // That's called Dynamic Compilation and it is
  // a Good Thing.

  // You CAN, however, statically compile your
  // Cg program if you really want to.
  
  // Different people running your CG program
  // will be using different graphics cards to
  // run it, each with different capabilities.

  // My good ol' FX 5200, for example, has a much
  // different set of capabilities than Nathan's
  // 8800 card that he got for Christmas.

  // Should the instructions generated by the Cg
  // program to my old FX 5200 be exactly the same
  // as the instructions generated to Nathan's 8800?
  
  // No way!!

  // 

  // As such, since Nathan's 8800 card CAN DO MORE
  // THINGS than my FX 5200 can, the CG compiler
  // should have a much different _attitude_ when its 
  // generating assembly code for my old FX 5200
  // than it does for Nathan's 8800.

  // THE PROFILE YOU USE DEPENDS ON:
     // 1)  The hardware you have available
     // 2)  The graphics API you are using (OpenGL or Direct3D).

  #pragma endregion

  myCgVertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX);
  cgGLSetOptimalOptions(myCgVertexProfile);
  checkForCgError("selecting vertex profile");


  ////////////////////////////////////////
  // 
  // 3.  CREATE THE VERTEX PROGRAM, ASSOCIATING IT
  //     WITH YOUR CGcontext _and_ YOUR VERTEX PROFILE
  ////////////////////////////////////////
  myCgVertexProgram =
    cgCreateProgramFromFile(
      myCgContext,              /* Cg runtime context */
      CG_SOURCE,                /* Program in human-readable form */
      "vertexShader.cg",        /* Name of file containing program */
      myCgVertexProfile,        /* Profile: OpenGL ARB vertex program */
      "vertexShaderFunction",   /* Entry function name of vertex shader program */
      NULL);                    /* No extra compiler options */

  checkForCgError("creating vertex program from file");

  ////////////////////////////////////////
  // 
  // 4.  LOAD THE VERTEX PROGRAM IN TO THE GPU MEMORY
  ////////////////////////////////////////
  cgGLLoadProgram(myCgVertexProgram);
  checkForCgError("loading vertex program");
  #pragma endregion

  glutMainLoop();  // GOTO the display() function next
  return 0;
}


/* 
     ____   __   __      __   __  ___
    / _  \ /  / /  /    /  /  \ \/  /
   / _/ / /  / /  /    /  /    \   /
  / _/ \ /  / /  /__  /  /__   /  /
 /_____//__/ /______//______/ /__/

*/








// FILE:  vertexShader.cg
// This is our vertex shader program.  It is a single function.

// vertexShaderFunction takes in vertex position and color,
// whose values originate in our OpenGL code.

// It outputs a new vertex position and color, which will be 
// used to draw the final shapes to the screen.

/*

------------    -----------------------------    -------------
| INCOMING | => | vertexShaderFunction      | => | OUTGOING  |
|  VERTEX  |    |                           |    |  VERTEX   |
| with own | => | Like processing plant     | => | final     |
| position |    | takes incoming position   |    | position  |
| and color| => | and color values for each | => | and color |
------------    | vertex and does some      |    | of vertex |
                | math on them.             |    -------------
                -----------------------------

*/


void vertexShaderFunction (
            float3 incomingPosition : POSITION,  // : POSITION is a SEMANTIC (see note below)
            float3 incomingColor    : COLOR,

        out float4 outgoingPosition : POSITION,
        out float4 outgoingColor    : COLOR      // : COLOR is a SEMANTIC (see note below)
   )
{
    // compute outgoingPosition using the incomingPosition
    // In this example, we'll just pass it through unmodified.
    outgoingPosition.x = incomingPosition.x;
    outgoingPosition.y = incomingPosition.y;
    outgoingPosition.z = incomingPosition.z;
    outgoingPosition.w = 1; // don't worry what w is YET, we'll get to that later.

    
    // Here's a more advanced operation, to make color look cool.
    float3 transformedColor = saturate( ( cosh( incomingColor ) ) * cos( incomingPosition.yxy ) );


    outgoingColor = float4( transformedColor, 1 );
}

//////////////// WHAT IS THIS??? ////////////////////
// vertexShaderFunction is like a PROCESSING PLANT:
  // This vertex shader takes IN two things:
    // 1.  It TAKES IN the raw vertex xyz position in space,
    //        as handed to it by OUR OpenGL code (incomingPosition)
    //
    // 2.  It TAKES IN the raw vertex color, as handed to it
    //        by OUR OpenGL code (incomingColor)

  // This vertex shader SPEWS OUT two things:
    // 1.  It SPITS OUT a NEW, FINAL POSITION for the
    //        vertex (variable outgoingPosition)
    //
    // 2.  It SPITS OUT a NEW, FINAL COLOR for the
    //        vertex (in variable outgoingColor)

// This vertex shader will process EVERY SINGLE VERTEX
// IN OUR PROGRAM as it travels down the graphics pipeline.

// Something very important to realize about writing your
// own vertex shaders is, when you activate your vertex
// shader, YOU COMPLETELY BYPASS the regular OpenGL
// transformation and project matrices.

// So if you go to the C++ code and glRotatef, glTranslatef,
// and gluLookAt() to your heart's content, IT WILL HAVE
// ABSOLUTELY NO EFFECT on the final result you see here.

// That is because by writing and enabling your vertex 
// shader program, you have effectively __TAKEN OVER__
// the graphics pipeline.

// It is now YOUR responsibility to take in x, y and z
// coordinates that the C++ program will feed in here,
// and TRANSFORM THEM TO GENERATE THE 3D LOOKING IMAGE.

// So if you're used to programming with OpenGL purely
// without using shaders, and you're used to using
// gluLookAt() and such things to get your scene to look
// the way you want, and gluPerspective() to get that
// sweet perspective view, you'll need to kiss those glMatrix
// functions goodbye for now.

// K-I-S-S ;).

// Ok, now that seems very complicated!!!  How will we ever
// do that???

// Don't worry, we will.

// In order to make this digestable, we'll first start
// by totally ignoring the Z-component.  We'll pretend
// that z doesn't exist and we'll totally draw in 2D.


// You're used to programming so that you write all this code
// to get the vertices into correct position BEFORE calling
// glVertex3f.  Then the reality of it was, gluPerspective()
// and gluLookAt() took care of all the other stuff for you.

// Now we're doing things differently.

// The vertex shader we're writing makes changes to
// vertex position _AFTER_ the call of glVertex3f.

// is that the vertex shader will make changes to the
// vertices that you specified __AFTER__ the glVertex3f calls.

//
// This is totally NOT what you're used to, if you've never
// programmed shaders before.
// Also other points (that we'll explain in much more detail
// later) are that
//    - when the shader is on, you TOTALLY BYPASS the regular
//      OpenGL transformation matrices (glRotatef, glTranslatef
//      HAVE NO EFFECT WHEN YOUR SHADER IS ON).
//    - The reason the glRotatef and glTranslatef functions have
//      no effect is, once you've activated your shader, YOU
//      ARE IN FULL CONTROL OF THE GPU.  When your shader is on,
//      you become responsible to feed the rasterizer a set of 
//      points that have x values between [-1, +1] and
//      y-values between [-1, +1].  Seems strange, but get used to the idea.
//


///////////////////

// COVERED HERE:
// 1.  vector data types: float3 and float4
// 2.  SEMANTICS
// 3.  Keywords IN and OUT
// 

// BEFORE YOU READ THIS
// Make sure you know EXACTLY WHAT A "function parameter" is.
// If you don't, you need to brush up on your C++ programming,
// specifically "functions, parameters and arguments"
// before you attempt to understand this, or it won't make
// any sense.

///////////////////
// 1.  Vector data types: float3 and float4
//
// One of the absolute coolest things about working
// with CG is its treatment of vector values as
// first class, primitive types.

// The next tutorial will deal with vector data
// types and the operations that are defined for
// them in much more detail, but for now, just
// recognize that its just like working with
// real vectors.

///////////////////
// 2.  SEMANTICS:
// 
// Look at the first parameter to the vertexShaderFunction.

//             float3 incomingPosition : POSITION

// The last word there in all caps ("POSITION") is what
// we call a SEMANTIC.
//
// What's a semantic?  In plain English,
// 'semantic' just means the 'meaning' of something.
//
// We attach a SEMANTIC ("meaning") to each one
// of vertexShaderFunction()'s parameters 
// precisely so that CG KNOWS WHAT EACH FUNCTION PARAMETER IS
// __TO BE USED FOR__.
//
// FIRST PARAM:  incomingPosition
// The first parameter to "vertexShaderFunction"
// (the "incomingPosition" parameter) is given the
// POSITION semantic.  This lets CG know that CG should
// pass in the vertex position as the first argument
// to this function.

// CG WILL AUTOMATICALLY FEED IN POSITION ___FOR YOU___.  You don't
// have to do anything to pass the vertex position to the
// vertex shader OTHER THAN SPECIFY THE VERTICES IN THE OPENGL CODE.
// This takes a tad bit of getting used to, but its really natural.

// SECOND PARAM:  incomingColor
// The second parameter ("incomingColor") to vertexShaderFunction() is given the
// COLOR semantic.  This lets CG know that CG should
// pass in the vertex's COLOR as the second argument
// to this function.  Again, this happens AUTOMATICALLY
// and there's nothing special you have to do to get it
// to happen other than specify a color in OpenGL (using
// glColor3f or something).

// So attaching the POSITION semantic to a function parameter
// identifies it to CG.  Variable name doesn't matter.

///////////////////////////////
// 3.  KEYWORD OUT:
//
// Take a look at the third parameter "outgoingPosition."
// In front, it has the special word 'OUT'.
// At the end, it also uses the semantic "POSITION."
// Combining that information, that tells CG that the
// variable "outgoingPosition" will be the FINAL OUTPUTTED
// POSITION of the vertex.

// Same goes for outgoingColor, except of course,
// outgoingColor will contain the final outputted color.

// Keyword OUT tells CG that those two parameters
// ARE the output values of this function.

// You can only have ONE PARAMETER that has the combination
// of OUT and POSITION applied to it, and ONE PARAMETER
// that has the combination of OUT and COLOR applied to it,
// since each vertex can only have ONE FINAL position and
// ONE FINAL color.

// Also it might take some getting used to the fact that
// this shader program acts as a processing plant that
// sort of takes in two FULL containers (incomingPosition
// and incomingColor) and it ALSO TAKES +IN+ two EMPTY
// containers (outgoingPosition and outgoingColor).
// The JOB of the vertexShaderFunction is to use the
// stuff provided in the incomingPosition and incomingColor
// containers and to FILL the outgoingPosition and outgoingColor
// containers appropriately.

// Programmatically, you've seen interfaces like this before
// if you've ever used the strcpy() function in C
//    strcpy( char * dest, char * src );
// if we were to write this like a shader function is written,
// it would be:

//    strcpy( out char * dest,
//                char * src );

// ALSO as a final note, there is a keyword IN that you CAN use
//void vertexShaderFunction (
//         IN float3 incomingPosition : POSITION,  // keyword IN optional,
//         IN float3 incomingColor    : COLOR,     // designate as inputs
//
//        out float4 outgoingPosition : POSITION,
//        out float4 outgoingColor    : COLOR
//   )

// AND finally, note that YOU COULD ALSO do this:
//void vertexShaderFunction (
//        inout float4 pos  : POSITION,   // this var is both in and output
//        inout float4 color : COLOR,     // both in and output
//   )
// { /* code */ }

// And that's all there is to the basics of a vertex shader.
// If you got most of that, you're ready for the next one! :).



////////////////////
// REVIEW POINTS:
// Its not the NAMING of the parameters to the function
// "incomingColor" and "outgoingColor" that tell CG what
// each variable does.  Its the SEMANTIC and whether or
// not the variable uses the word OUT in front that tells
// CG what the variable is for.  FOr all Cg cares, you could
// write

//void vertexShaderFunction(
//                float3 hamburger   : POSITION,
//                float3 Fries       : COLOR,

//            out float4 milkshake   : POSITION,
//            out float4 coke        : COLOR
//          )

// and still the program would work the same, provided you
// make the appropriate changes to the function body as well.


// ONLY the ENTRY FUNCTION can use SEMANTICS.
// So if you write an additional helper function
// in the CG code file here, that's fine and dandy,
// but if the helper function has use of the POSITION
// or COLOR semantic, it will be to no effect.  You'd
// have to pass those values manually yourself, if
// you wanted access to the in your helper function.

// Final note:  The strangest thing you have to get
// used to is even though "vertexShaderFunction" is 
// just a function, YOU never get a chance to CALL
// IT YOURSELF.

// Semantics are the ONLY WAY you tell CG what
// variables do what.  YOU DO NOT get a chance
// to write a line of code like:
//
//     vertexShaderFunction( v1, c1, v2, c2 );

// As such, you must provide instructions to CG:
// "What parameter does what?" before you even
// compile and run the program.


// Additional:  What's an ENTRY FUNCTION?

// vertexShaderFunction (name of the shader function
// waaaaaaaaaay up at the top of this file) IS an entry function.
// Because vertexShaderFunction is where the program starts,
// vertexShaderFunction is called the ENTRY FUNCTION.

// Ok I'm done.  Send any questions, comments,
// +/- fdback, or appreciations! to billy.baloop@gmail.com.

/* 
     ____   __   __      __   __  ___
    / _  \ /  / /  /    /  /  \ \/  /
   / _/ / /  / /  /    /  /    \   /
  / _/ \ /  / /  /__  /  /__   /  /
 /_____//__/ /______//______/ /__/

*/


As always, code package available on esnips (thanks esnips!)

About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 43 other followers

%d bloggers like this: