Example 28

Example 28

This shader program uses a simple textured shader to render some shrubbery, but also uses a vertex shader to animate it as if it were blowing in the wind. The vertex shader just varies the location of the vertices along one axis based on their position in space and the time, but with some tweaking this could be turned into a far more convincing shader. Imagine a "gust" of wind blowing through the scene: if the gust is passed to the vertex shader as a point in space (a uniform), the amount of distortion could be proportional to a grass shrub's distance to the center of the gust. In this way you could animate the grass deforming as a character or vehicle moves by, or just more accurately simulate a windy scene. The camera in this scene is a free camera; press 'w' and 's' to move along the viewing direction, and use the mouse to look around.

ex_28/grassy_field.png

Source Code

main.cpp:

// Alexandri Zavodny
// CSE 40166: Computer Graphics, Fall 2010
// Example: Simple texturing example: use 't' to enable textures and rotate 
//              using an arcball camera model.


#include <math.h>
#include <GL/glew.h>
#include <GL/glut.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>

#include <iostream>
#include <fstream>

using namespace std;


// GLOBAL VARIABLES ////////////////////////////////////////////////////////////

static size_t windowWidth = 640;
static size_t windowHeight = 480;
static float aspectRatio;

GLint leftMouseButton, rightMouseButton;    //status of the mouse buttons
int mouseX = 0, mouseY = 0;                 //last known X and Y of the mouse

float cameraTheta, cameraPhi;               //camera direction in spherical coordinates
float cameraX, cameraY, cameraZ;            //camera position in cartesian coordinates
float dirX, dirY, dirZ;                     //camera direction in cartesian coordinates


//some global variables for our textures
unsigned char *grassTexData, *grassMaskData; //the actual image data
GLuint grassTexHandle;                      //a handle to the texture in our OpenGL context
int grassTexWidth, grassTexHeight;          //variables for the image width/height


//contains the X and Z coordinates of grass sprite positions.
//is filled randomly at the start of execution, and then referenced
//during rendering.
vector< pair<float,float> > grassSpritePositions;

char *vertexShaderString;
char *fragmentShaderString;
GLuint vertexShaderHandle, fragmentShaderHandle, shaderProgramHandle;
GLuint timeLocation;

// readPPM() ///////////////////////////////////////////////////////////////////
//
//  This function reads an ASCII PPM, returning true if the function succeeds and
//      false if it fails. If it succeeds, the variables imageWidth and 
//      imageHeight will hold the width and height of the read image, respectively.
//
//  It's not terribly robust.
//
//  Returns the image as an unsigned character array containing 
//      imageWidth*imageHeight*3 entries (for that many bytes of storage).
//
//  NOTE: this function expects imageData to be UNALLOCATED, and will allocate 
//      memory itself. If the function fails (returns false), imageData
//      will be set to NULL and any allocated memory will be automatically deallocated.
//
////////////////////////////////////////////////////////////////////////////////
bool readPPM(string filename, int &imageWidth, int &imageHeight, unsigned char* &imageData)
{
    FILE *fp = fopen(filename.c_str(), "r");
    int temp, maxValue;
    fscanf(fp, "P%d", &temp);
    if(temp != 3)
    {
        fprintf(stderr, "Error: PPM file is not of correct format! (Must be P3, is P%d.)\n", temp);
        fclose(fp);
        return false;
    }

    //got the file header right...
    fscanf(fp, "%d", &imageWidth);
    fscanf(fp, "%d", &imageHeight);
    fscanf(fp, "%d", &maxValue);

    //now that we know how big it is, allocate the buffer...
    imageData = new unsigned char[imageWidth*imageHeight*3];
    if(!imageData)
    {
        fprintf(stderr, "Error: couldn't allocate image memory. Dimensions: %d x %d.\n", imageWidth, imageHeight);
        fclose(fp);
        return false;
    }

    //and read the data in.
    for(int j = 0; j < imageHeight; j++)
    {
        for(int i = 0; i < imageWidth; i++)
        {
            int r, g, b;
            fscanf(fp, "%d", &r);
            fscanf(fp, "%d", &g);
            fscanf(fp, "%d", &b);

            imageData[(j*imageWidth+i)*3+0] = r;
            imageData[(j*imageWidth+i)*3+1] = g;
            imageData[(j*imageWidth+i)*3+2] = b;
        }
    }
    
    fclose(fp);
    return true;
}

// readAlphaPPM() //////////////////////////////////////////////////////////////
//
//  Very simple PPM reader, except only stores one channel of information, and
//      the image is presumed to correspond to an alpha map. Yes, this functionality
//      could (and probably should) be combined with the regular PPM function;
//      there's a lot of functional overlap.
//
////////////////////////////////////////////////////////////////////////////////
bool readAlphaPPM(string filename, int &imageWidth, int &imageHeight, unsigned char* &charMask)
{
    FILE *fp = fopen(filename.c_str(), "r");
    int temp, maxValue;
    fscanf(fp, "P%d", &temp);
    if(temp != 3)
    {
        fprintf(stderr, "Error: PPM file is not of correct format! (Must be P3, is P%d.)\n", temp);
        fclose(fp);
        return false;
    }

    //got the file header right...
    fscanf(fp, "%d", &imageWidth);
    fscanf(fp, "%d", &imageHeight);
    fscanf(fp, "%d", &maxValue);

    //now that we know how big it is, allocate the buffer...
    charMask = new unsigned char[imageWidth*imageHeight];
    if(!charMask)
    {
        fprintf(stderr, "Error: couldn't allocate image memory. Dimensions: %d x %d.\n", imageWidth, imageHeight);
        fclose(fp);
        return false;
    }

    //and read the data in.
    for(int j = 0; j < imageHeight; j++)
    {
        for(int i = 0; i < imageWidth; i++)
        {
            int t;
            fscanf(fp, "%d", &t);
            charMask[j*imageWidth+i] = (unsigned char)t;
            fscanf(fp, "%d", &t);
            fscanf(fp, "%d", &t);
        }
    }
    
    fclose(fp);
    return true;
}

// registerOpenGLTexture() /////////////////////////////////////////////////////
//
//  Attempts to register an image buffer with OpenGL. Sets the texture handle
//      appropriately upon success, and uses a number of default texturing
//      values. This function only provides the basics: just sets up this image
//      as an unrepeated 2D texture.
//
////////////////////////////////////////////////////////////////////////////////
bool registerTransparentOpenGLTexture(unsigned char *imageData, unsigned char *imageMask, int texWidth, int texHeight, GLuint &texHandle)
{
    unsigned char *fullData = new unsigned char[texWidth*texHeight*4];
    for(int j = 0; j < texHeight; j++)
    {
        for(int i = 0; i < texWidth; i++)
        {
            fullData[(j*texWidth+i)*4+0] = imageData[(j*texWidth+i)*3+0];
            fullData[(j*texWidth+i)*4+1] = imageData[(j*texWidth+i)*3+1];
            fullData[(j*texWidth+i)*4+2] = imageData[(j*texWidth+i)*3+2];
            fullData[(j*texWidth+i)*4+3] = imageMask[(j*texWidth+i)];
        }
    }


    //first off, get a real texture handle.
    glGenTextures(1, &texHandle);

    //make sure that future texture functions affect this handle
    glBindTexture(GL_TEXTURE_2D, texHandle);

    //set this texture's color to be multiplied by the surface colors -- 
    //  GL_MODULATE instead of GL_REPLACE allows us to take advantage of OpenGL lighting
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

    //set the minification ang magnification functions to be linear; not perfect
    //  but much better than nearest-texel (GL_NEAREST).
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    //Set the texture to repeat in S and T -- though it doesn't matter here
    //  because our texture coordinates are always in [0,0] to [1,1].
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    //actually transfer the texture to the GPU!
    glTexImage2D(GL_TEXTURE_2D,                 //still working with 2D textures, obv.
                    0,                          //not using mipmapping, so this is the highest level.
                    GL_RGBA,                     //we're going to provide OpenGL with R, G, and B components...
                    texWidth,                   //...of this width...
                    texHeight,                  //...and this height.
                    0,                          //give it a border of 0 pixels (none)
                    GL_RGBA,                     //and store it, internally, as RGB (OpenGL will convert to floats between 0.0 and 1.0f)
                    GL_UNSIGNED_BYTE,           //this is the format our data is in, and finally,
                    fullData);                 //there's the data!


    delete fullData;

    //whoops! i guess this function can't fail. in an ideal world, there would
    //be checks with glGetError() that we could use to determine if this failed.
    return true;
}


// recomputeOrientation() //////////////////////////////////////////////////////
//
// This function updates the camera's position in cartesian coordinates based 
//  on its position in spherical coordinates. Should be called every time 
//  cameraTheta, cameraPhi, or cameraRadius is updated. 
//
////////////////////////////////////////////////////////////////////////////////
void recomputeOrientation()
{
    dirX = sinf(cameraTheta)*sinf(cameraPhi);
    dirZ = -cosf(cameraTheta)*sinf(cameraPhi);
    dirY = cosf(cameraPhi);

    glutPostRedisplay();
}

// resizeWindow() //////////////////////////////////////////////////////////////
//
//  GLUT callback for window resizing. Resets GL_PROJECTION matrix and viewport.
//
////////////////////////////////////////////////////////////////////////////////
void resizeWindow(int w, int h)
{
    aspectRatio = w / (float)h;

    windowWidth = w;
    windowHeight = h;

    //update the viewport to fill the window
    glViewport(0, 0, w, h);

    //update the projection matrix with the new window properties
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0,aspectRatio,0.1,100000);
}



// mouseCallback() /////////////////////////////////////////////////////////////
//
//  GLUT callback for mouse clicks. We save the state of the mouse button
//      when this is called so that we can check the status of the mouse
//      buttons inside the motion callback (whether they are up or down).
//
////////////////////////////////////////////////////////////////////////////////
void mouseCallback(int button, int state, int thisX, int thisY)
{
    //update the left and right mouse button states, if applicable
    if(button == GLUT_LEFT_BUTTON)
        leftMouseButton = state;
    else if(button == GLUT_RIGHT_BUTTON)
        rightMouseButton = state;
    
    //and update the last seen X and Y coordinates of the mouse
    mouseX = thisX;
    mouseY = thisY;
}

// mouseMotion() ///////////////////////////////////////////////////////////////
//
//  GLUT callback for mouse movement. We update cameraPhi, cameraTheta, and/or
//      cameraRadius based on how much the user has moved the mouse in the
//      X or Y directions (in screen space) and whether they have held down
//      the left or right mouse buttons. If the user hasn't held down any
//      buttons, the function just updates the last seen mouse X and Y coords.
//
////////////////////////////////////////////////////////////////////////////////
void mouseMotion(int x, int y)
{
    if(leftMouseButton == GLUT_DOWN)
    {
        cameraTheta += (x - mouseX)*0.005;
        cameraPhi   += (y - mouseY)*0.005;

        // make sure that phi stays within the range (0, M_PI)
        if(cameraPhi <= 0)
            cameraPhi = 0+0.001;
        if(cameraPhi >= M_PI)
            cameraPhi = M_PI-0.001;
        
        
        recomputeOrientation();     //update camera (x,y,z) based on (radius,theta,phi)
    }
    mouseX = x;
    mouseY = y;
}



// initScene() /////////////////////////////////////////////////////////////////
//
//  A basic scene initialization function; should be called once after the
//      OpenGL context has been created. Doesn't need to be called further.
//
////////////////////////////////////////////////////////////////////////////////
void initScene() 
{
    glEnable(GL_DEPTH_TEST);

    float lightCol[4] = { 1, 1, 1, 1};
    float ambientCol[4] = {0.3, 0.3, 0.3, 1.0};
    glLightfv(GL_LIGHT0,GL_DIFFUSE,lightCol);
    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientCol);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);


    float rangeX = 50.0f;
    float rangeZ = 50.0f;
    int numSprites = 100;
    for(int i = 0; i < numSprites; i++)
    {
        grassSpritePositions.push_back(pair<float,float>(
                                        rand()/(float)RAND_MAX * rangeX - rangeX/2.0f,
                                        //rand()/(float)RAND_MAX * rangeZ - rangeZ/2.0f),
                                        (rangeZ * (i/(float)numSprites)) - rangeZ / 2.0f));
    }


    //glShadeModel(GL_SMOOTH);
    glShadeModel(GL_FLAT);
}

// renderScene() ///////////////////////////////////////////////////////////////
//
//  GLUT callback for scene rendering. Sets up the modelview matrix, renders
//      a teapot to the back buffer, and switches the back buffer with the
//      front buffer (what the user sees).
//
////////////////////////////////////////////////////////////////////////////////
void renderScene(void) 
{
    //update the modelview matrix based on the camera's position
    glMatrixMode(GL_MODELVIEW);         //make sure we aren't changing the projection matrix!
    glLoadIdentity();
    gluLookAt(cameraX, cameraY, cameraZ,//camera is located at (x,y,z)
                cameraX+dirX,
                cameraY+dirY,
                cameraZ+dirZ,
                0.0f,1.0f,0.0f);        //up vector is (0,1,0) (positive Y)

    //update the light position after we set the modelview matrix;
    //lights get transformed by the modelview matrix just like every other point!
    float lPosition[4] = { 1000, 1000, 1000, 1 };
    float lAmbient[4] = { 1.0f,1.0f,1.0f,1.0f};
    glLightfv(GL_LIGHT0,GL_POSITION,lPosition);
    glLightfv(GL_LIGHT0, GL_AMBIENT, lAmbient);

    //enable OpenGL blending!
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    //clear the render buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //enable textures.
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, grassTexHandle);

    glUseProgram(shaderProgramHandle);
    glUniform1f(timeLocation, glutGet(GLUT_ELAPSED_TIME)/1000.0f);


    //somewhat hacky note: the grass sprites are all sorted by their X position so we don't run into
    //any transparent rendering problems. In the real world, they would have to be sorted dynamically.
    float spriteXWidth = 5.0f;
    float spriteYWidth = 3.25;
    glColor4f(1.0f,1.0f,1.0f,1.0f);
    for(int i = 0; i < grassSpritePositions.size(); i++)
    {
        glBegin(GL_QUADS);
            glNormal3f(0,0,1);
            glTexCoord2f(0,0);
            glVertex3f(grassSpritePositions[i].first-(spriteXWidth/2.0f), spriteYWidth, grassSpritePositions[i].second);

            glNormal3f(0,0,1);
            glTexCoord2f(1,0);
            glVertex3f(grassSpritePositions[i].first+(spriteXWidth/2.0f), spriteYWidth, grassSpritePositions[i].second);

            glNormal3f(0,0,1);
            glTexCoord2f(1,1);
            glVertex3f(grassSpritePositions[i].first+(spriteXWidth/2.0f), 0.0f, grassSpritePositions[i].second);

            glNormal3f(0,0,1);
            glTexCoord2f(0,1);
            glVertex3f(grassSpritePositions[i].first-(spriteXWidth/2.0f), 0.0f, grassSpritePositions[i].second);
        glEnd();
    }


    //push the back buffer to the screen
    glutSwapBuffers();
}


// readTextFile() //////////////////////////////////////////////////////////////
//
//  Reads in a text file as a single string. Used to aid in shader loading.
//
////////////////////////////////////////////////////////////////////////////////
void readTextFile(string filename, char* &output)
{
    string buf = string("");
    string line;

    ifstream in(filename.c_str());
    while(getline(in, line))
        buf += line + "\n";
    output = new char[buf.length()+1];
    strncpy(output, buf.c_str(), buf.length());
    output[buf.length()] = '\0';

    in.close();
}

// setupShaders() //////////////////////////////////////////////////////////////
//
//  A simple helper subrouting that performs the necessary steps to enable shaders
//      and bind them appropriately. Note because we're using global variables,
//      everything is relatively hard-coded, including filenames, and none of
//      the information are passed to the function as parameters.
//
////////////////////////////////////////////////////////////////////////////////
void setupShaders()
{
    readTextFile("texture.vert", vertexShaderString);
    readTextFile("texture.frag", fragmentShaderString);

    vertexShaderHandle = glCreateShader(GL_VERTEX_SHADER);
    fragmentShaderHandle = glCreateShader(GL_FRAGMENT_SHADER);

    glShaderSource(vertexShaderHandle, 1, (const char**)&vertexShaderString, NULL);
    glShaderSource(fragmentShaderHandle, 1, (const char**)&fragmentShaderString, NULL);

    //free up that memory cause we're awesome programmers.
    delete [] vertexShaderString;
    delete [] fragmentShaderString;

    glCompileShader(vertexShaderHandle);
    glCompileShader(fragmentShaderHandle);

    shaderProgramHandle = glCreateProgram();

    glAttachShader(shaderProgramHandle, vertexShaderHandle);
    glAttachShader(shaderProgramHandle, fragmentShaderHandle);

    glLinkProgram(shaderProgramHandle);
    glUseProgram(shaderProgramHandle);

    timeLocation = glGetUniformLocation(shaderProgramHandle, "time");
}

// normalKeys() ////////////////////////////////////////////////////////////////
//
//  GLUT keyboard callback.
//
////////////////////////////////////////////////////////////////////////////////
void normalKeys(unsigned char key, int x, int y) 
{
    if(key == 'q' || key == 'Q')
        exit(0);


    //basic controls for a free-view camera.
    if(key == 'w' || key == 'W')
    {
        cameraX += dirX;
        cameraY += dirY;
        cameraZ += dirZ;
    }

    if(key == 's' || key == 'S')
    {
        cameraX -= dirX;
        cameraY -= dirY;
        cameraZ -= dirZ;
    }
}




// main() //////////////////////////////////////////////////////////////////////
//
//  Program entry point. Does not process command line arguments.
//
////////////////////////////////////////////////////////////////////////////////
int main(int argc, char **argv)
{
    //create a double-buffered GLUT window at (50,50) with predefined windowsize
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(50,50);
    glutInitWindowSize(windowWidth,windowHeight);
    glutCreateWindow("how soothing");


    //give the camera a 'pretty' starting point!
    //cameraRadius = 7.0f;
    cameraX = cameraY = cameraZ = 10.0f;
    cameraTheta = -1.00;
    cameraPhi = 2.0;
    recomputeOrientation();

    //register callback functions...
    glutKeyboardFunc(normalKeys);
    glutDisplayFunc(renderScene);
    glutIdleFunc(renderScene);
    glutReshapeFunc(resizeWindow);
    glutMouseFunc(mouseCallback);
    glutMotionFunc(mouseMotion);

    //do some basic OpenGL setup
    initScene();

    glewInit();
    setupShaders();

    //read in our file and register our texture
    bool success = readPPM("blades.ppm", grassTexWidth, grassTexHeight, grassTexData);
    readAlphaPPM("blades_mask.ppm", grassTexWidth, grassTexHeight, grassMaskData);

    //NOTE THAT THIS NEEDS TO HAPPEN AFTER THE OPENGL CONTEXT HAS BEEN INITIALIZED.
    //  which it has, thankfully.
    if(success)
        registerTransparentOpenGLTexture(grassTexData, grassMaskData, grassTexWidth, grassTexHeight, grassTexHandle);

    //and enter the GLUT loop, never to exit.
    glutMainLoop();

    return(0);
}

Files