Struggling with GPGPU

I am having difficulty writing an application to simulate some fluid dynamics. Forgetting the context specific stuff, my problem right now seems to be with the general setup of using the GPU for general purpose calculations.

My Fluid class has a 2D grid of vertices, each containing its integral ordinal (X,Y). I try to store this info in a vertex buffer object (m_VBO_GridIndices).

I want to pass the vertex coordinate to the shader, have it do some calculations, and store the result somewhere. As I understand it, it can’t write to a texture, but it can write to a frame buffer object which can then be converted to a 2D texture, which is what I need. Each texel in the texture will contain a calculation result.

When I call Fluid::EvaluateWaves(), the glDrawElements raises an “invalid framebuffer operation”. I am fairly certain I am not setting up the state correctly.


// Constructor...
Fluid::Fluid(
    GLuint const Width, 
    GLuint const Height, 
    GLfloat const DistanceBetweenVertices, 
    GLfloat const EvaluationInterval, 
    GLfloat const WaveSpeed, 
    GLfloat const Viscosity)
    : m_Width(Width),
      m_Height(Height),
      m_ProgramObject(0),
      m_GVA_VertexIndices(0),
      m_FrameBufferObject(0),
      m_RenderBufferObject(0),
      m_BufferSwitch(GL_FALSE),
      m_VBO_GridIndices(0)
{
    // Reserve texture names...
    
        // Z-displacement...
        glGenTextures(2, m_TextureID_Z);
        
        // Normals...
        glGenTextures(2, m_TextureID_Normals);
        
        // Tangents...
        glGenTextures(2, m_TextureID_Tangents);

    // Check for OpenGL errors...
    PrintOpenGLErrors();

    // Install fluid vertex and fragment shader calculators...
    
        // The vertex shaders...
        GLchar const   *ppVertexShaders[]   = { "fluid.glslv" };
        
        // The fragment shaders...
        GLchar const   *ppFragmentShaders[] = { "fluid.glslf" };

        // Generic vertex attribute names...
        GLchar const   *ppGVA[]             = { "VertexIndex" };

        // Generic vertex attribute ID space...
        GLuint          GVA_Indices[sizeof(ppGVA)];

        // Compile and link...
        m_ProgramObject = InstallProgram(
            ppVertexShaders, 
            sizeof(ppVertexShaders) / sizeof(ppVertexShaders[0]), 
            ppFragmentShaders, 
            sizeof(ppFragmentShaders) / sizeof(ppFragmentShaders[0]),
            ppGVA,
            sizeof(ppGVA) / sizeof(ppGVA[0]),
            GVA_Indices);

        // Store the GVA indices...
        m_GVA_VertexIndices = GVA_Indices[0];

    // Initialize vertex IDs in grid...

        // Calculate required storage space for all vertex IDs...
        GLuint const Storage = m_Width * m_Height * sizeof(GLuint) * 2;

        // Allocate on stack...
        GLuint GridIDs[m_Height][m_Width][2];
        
        // Initialize vertex numbers across the grid...
        for(GLuint CurrentY = 0; CurrentY < m_Height; ++CurrentY)
            for(GLuint CurrentX = 0; CurrentX < m_Width; ++CurrentX)
            {
                // Store grid index...
                GridIDs[CurrentY][CurrentX][0] = CurrentX;
                GridIDs[CurrentY][CurrentX][1] = CurrentY;
            }

    // Initialize vertex buffer object with vertex IDs...
    
        // Allocate...
        glGenBuffers(1, &m_VBO_GridIndices);
        
        // Select...
        glBindBuffer(GL_ARRAY_BUFFER, m_VBO_GridIndices);
        
        // Commit grid indices to card...
        glBufferData(GL_ARRAY_BUFFER, Storage, &GridIDs, GL_STATIC_DRAW);
        glVertexAttribPointer(m_GVA_VertexIndices, 2, GL_UNSIGNED_INT, GL_FALSE, 0, NULL);

    // Initialize frame and render buffers objects...
    
        // Generate frame and render buffer objects...
        glGenFramebuffers(1, &m_FrameBufferObject);
        glGenRenderbuffers(1, &m_RenderBufferObject);

        // Initialize the render buffer object...
        
            // Select...
            glBindRenderbuffer(GL_RENDERBUFFER, m_RenderBufferObject);
            
            // Allocate storage...
            glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16,
                m_Width, m_Height);

        // Initalize the frame buffer object...

            // Select frame buffer object...
            glBindFramebuffer(GL_FRAMEBUFFER, m_FrameBufferObject);

            // Attach render buffer object to frame buffer object...
            glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, 
                GL_RENDERBUFFER, m_RenderBufferObject);

            // Select the Z-displacement texture map...
            glBindTexture(GL_TEXTURE_2D, m_TextureID_Z[m_BufferSwitch]);

            // Specify texture location...
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Width, m_Height, 0,
                GL_RGBA, GL_FLOAT, NULL);

            // Attach the Z-displacement texture map to the frame buffer object...
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
                GL_TEXTURE_2D, m_TextureID_Z[m_BufferSwitch], 0);

    // Go back to regular frame buffer rendering
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // Check for OpenGL errors...
    PrintOpenGLErrors();
}

// Re-evaluate the waves, but must be called at constant interval...
void Fluid::EvaluateWaves()
{
    // Variables...
    GLint   Viewport[4] = { 0, 0, 0, 0 };

    /* Variables...
    GLint   ActiveTextureUnit   = 0;

    // Remember the current texture unit...
    glGetIntegerv(GL_ACTIVE_TEXTURE, &ActiveTextureUnit);*/

    // Remember the original viewport dimensions...
    glGetIntegerv(GL_VIEWPORT, Viewport);

    // Make sure no texture selected...
    glBindTexture(GL_TEXTURE_2D, 0);

    // Redirect render output to our framebuffer object...
    glBindFramebuffer(GL_FRAMEBUFFER, m_FrameBufferObject);

    // Resize to size of grid...
    glViewport(0, 0, m_Width, m_Height);

    // Enable required vertex arrays...
    glEnableVertexAttribArray(m_GVA_VertexIndices);

    // Install shader...
    glUseProgram(m_ProgramObject);

    // Select the grid indices...
    glBindBuffer(GL_ARRAY_BUFFER, m_VBO_GridIndices);

    // Send vertex grid indices to shader...
    glDrawElements(GL_POINTS, m_Width * m_Height, GL_UNSIGNED_INT, NULL);

    // Disable required vertex arrays...
    glDisableVertexAttribArray(m_GVA_VertexIndices);
    
    // Restore normal framebuffer rendering...
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    
    // Restore viewport to original dimensions...
    glViewport(Viewport[0], Viewport[1], Viewport[2], Viewport[3]);
}

// Deconstructor...
Fluid::~Fluid()
{

}


Any help is appreciated.

Kip

Each texel in the texture will contain a calculation result

when doing old GPGPU with glsl, ur pixel shader should be the kernel and not the vertex shader ,because in vertex shader u have only access to ur vertices while in pixel shader and after rasterization each pixel will trigger a shader that can access ur all texture.

it can’t write to a texture

u can simply attach a texture to ur fbo! and ur shader will write on. that is it!
did u mean that ur vertex shader cant fetch texture?

In classic GPGPU model , u need an fbo( + ping pong if needed), pixel shader and finally a simple quad vertices where ur texture is mapped on it.

now for ur code , can u plz post only the important part of ur fbo initialization and verify that it is generating the same error on ur machine before posting it .

Thanks Abdallah. I am going through the official specifications now for core profile 3.2 and things are starting to click. I think I was just rushing and jumping in too fast without really understanding and feeling confident in how the FBO work.

One thing I am still unclear on is, say I would like to render to a 2D texture, I can’t figure out the difference between glFramebufferTexture and glFramebufferTexture2D, other than the textarget parameter in the latter (which I also can’t figure its purpose).

I will pass in vertex number pairs (position is irrelevant) as GVA to represent points on a grid, then based on constants precomputed and passed in via uniforms, calculate wave position on the surface at each point in the grid. This just means determining each position on the grid’s displacement in the vertical direction. So I will have each texel in the 2D texture outputted from the shader as the displacement value (only need a single channel for this). Since I don’t need per-pixel calculation for this (assume its purpose is not even graphical at this point), I can do these calculations in the vertex shader? The fragment shader I would like to just use to pass on the calculated displacement for each grid position just in the texture.

I can’t figure out the difference between glFramebufferTexture and glFramebufferTexture2D, other than the textarget parameter in the latter (which I also can’t figure its purpose).

The texture to framebuffer binding functions are kind of… weird. As the FBO extension grew and added new functionality, the new binding functions have replaced some of the uses of the old. But the old ones still have value.

glFramebufferTexture is a prime example.

The main purpose for adding glFramebufferTexture was to add a binding function for layered rendering. Layered rendering (see [url=Framebuffer Object - OpenGL Wiki]the wiki article for details) is geometry shader functionality, where primitives can be emitted to a specific layer of an FBO. This basically lets you render to all 6 faces of a cubemap simultaneously, and so forth.

glFramebufferTexture can be used to bind unlayered textures (regular 1D and 2D). However, if you have a cubemap, and you don’t want to use layered rendering (you really only want to render to one face), glFramebufferTexture can’t help you.

It can’t help because it doesn’t have the textarget parameter. This parameter is used to select what face of the cubemap you want to bind to the FBO. So you have to use glFramebufferTexture2D when binding cubemaps for non-layered rendering.

Thanks Alfonse. Very helpful.

Ok, I believe I have the FBO <–> texture logic setup ok, but I am getting a segfault when I try to send my grid geometry to the vertex shader via glDrawElements() in Fluid::EvaluateWaves().

My guess is this is either the wrong function to use, or something is wrong with the VBO that contains my grid pairs. The grid is just 2D array (allocated and initialized as a flat 1D array) where each element contains a pair of integers designating its location on a grid. I don’t care about points, lines, quads, etc., because this is not even graphical data yet. I just want the grid location pairs to be sent to my shader where I receive them as…


in uvec2 GridLocation;

Any help is appreciated. Here is my code.


// Constructor...
Fluid::Fluid(
    GLuint const Width, 
    GLuint const Height, 
    GLfloat const DistanceBetweenVertices, 
    GLfloat const EvaluationInterval, 
    GLfloat const WaveSpeed, 
    GLfloat const Viscosity)
    : m_Width(Width),
      m_Height(Height),
      m_FrameBufferObject(0),
      m_PO_EvaluateWaves(0),
      m_GVA_GridLocations(0),
      m_VBO_GridLocations(0),
      m_BufferSwitch(0)
{
    // Variables...
    GLint   MaxColourAttachments    = 0;
    GLint   PreviousProgramObject   = 0;

    // Initialize framebuffer object...

        // Reserve a name and allocate...
        glGenFramebuffers(1, &m_FrameBufferObject);
        
        // Select framebuffer into current context...
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_FrameBufferObject);

        // Check for OpenGL errors...
        PrintOpenGLErrors();

    // Reserve texture names for both buffers and identify attachment points...
    
        // Z-displacement...

            // Reserve names...
            glGenTextures(2, m_ZMaps_TextureID);
            
            // Allocate and configure first buffer...
            glBindTexture(GL_TEXTURE_2D, m_ZMaps_TextureID[0]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexImage2D(GL_TEXTURE_2D, 0, 1, m_Width, m_Height, 0, GL_RED, GL_FLOAT, NULL);

            // Allocate and configure second buffer...
            glBindTexture(GL_TEXTURE_2D, m_ZMaps_TextureID[1]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexImage2D(GL_TEXTURE_2D, 0, 1, m_Width, m_Height, 0, GL_RED, GL_FLOAT, NULL);

            // Remember logical buffer attachment points to framebuffer object...
            m_ZMaps_AttachmentPoint[0]          = GL_COLOR_ATTACHMENT0;
            m_ZMaps_AttachmentPoint[1]          = GL_COLOR_ATTACHMENT1;

        // Normals...

            // Reserve names...
            glGenTextures(2, m_NormalMaps_TextureID);
            
            // Allocate and configure first buffer...
            glBindTexture(GL_TEXTURE_2D, m_NormalMaps_TextureID[0]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexImage2D(GL_TEXTURE_2D, 0, 3, m_Width, m_Height, 0, GL_RGB, GL_FLOAT, NULL);
            
            // Allocate and configure second buffer...
            glBindTexture(GL_TEXTURE_2D, m_NormalMaps_TextureID[1]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexImage2D(GL_TEXTURE_2D, 0, 3, m_Width, m_Height, 0, GL_RGB, GL_FLOAT, NULL);

            // Remember logical buffer attachment points to framebuffer object...
            m_NormalMaps_AttachmentPoint[0]     = GL_COLOR_ATTACHMENT2;
            m_NormalMaps_AttachmentPoint[1]     = GL_COLOR_ATTACHMENT3;

        // Tangents...

            // Reserve names...
            glGenTextures(2, m_TangentMaps_TextureID);
            
            // Allocate and configure first buffer...
            glBindTexture(GL_TEXTURE_2D, m_TangentMaps_TextureID[0]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexImage2D(GL_TEXTURE_2D, 0, 3, m_Width, m_Height, 0, GL_RGB, GL_FLOAT, NULL);
            
            // Allocate and configure second buffer...
            glBindTexture(GL_TEXTURE_2D, m_TangentMaps_TextureID[1]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexImage2D(GL_TEXTURE_2D, 0, 3, m_Width, m_Height, 0, GL_RGB, GL_FLOAT, NULL);

            // Remember logical buffer attachment points to framebuffer object...
            m_TangentMaps_AttachmentPoint[0]    = GL_COLOR_ATTACHMENT4;
            m_TangentMaps_AttachmentPoint[1]    = GL_COLOR_ATTACHMENT5;

            // Check for OpenGL errors...
            PrintOpenGLErrors();

    // Attach maps to framebuffer object...

        // Verify sufficient colour logical buffer attachment points for the
        //  Z, normal, and tangent maps primary and secondary buffers 
        //  respectively...
        glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &MaxColourAttachments);

            // Not enough, since we need three...
            if(MaxColourAttachments < 6)
                FatalError("We need 6 colour logical buffer attachment points,"
                           " but %d were available.", MaxColourAttachments);

        // Attach the z-maps...
        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, m_ZMaps_AttachmentPoint[0], GL_TEXTURE_2D, m_ZMaps_TextureID[0], 0);
        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, m_ZMaps_AttachmentPoint[1], GL_TEXTURE_2D, m_ZMaps_TextureID[1], 0);

        // Attach the normal maps...
        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, m_NormalMaps_AttachmentPoint[0], GL_TEXTURE_2D, m_NormalMaps_TextureID[0], 0);
        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, m_NormalMaps_AttachmentPoint[1], GL_TEXTURE_2D, m_NormalMaps_TextureID[1], 0);
        
        // Attach the tangent maps...
        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, m_TangentMaps_AttachmentPoint[0], GL_TEXTURE_2D, m_TangentMaps_TextureID[0], 0);
        glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, m_TangentMaps_AttachmentPoint[1], GL_TEXTURE_2D, m_TangentMaps_TextureID[1], 0);

        // Check for OpenGL errors...
        PrintOpenGLErrors();

    // Verify framebuffer completeness...
    GLenum const FramebufferStatus = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);

        // Problem...
        if(FramebufferStatus != GL_FRAMEBUFFER_COMPLETE)
        {
            // What happened?
            switch(FramebufferStatus)
            {
                // Bad attachment...
                case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
                    FatalError("framebuffer incomplete (check attachments)");

                // Mising attachment...
                case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
                    FatalError("framebuffer incomplete (missing attachment)");

                // Unsupported...
                case GL_FRAMEBUFFER_UNSUPPORTED:
                    FatalError("framebuffer unsupported");

                // Other...
                default:
                    FatalError("framebuffer incomplete (%u)", FramebufferStatus);
            }
        }

        // Check for OpenGL errors...
        PrintOpenGLErrors();

    // Clearing the z-map clears it to default calm state...
    glClear(GL_COLOR_BUFFER_BIT);

    // Remember the currently installed program object...
    glGetIntegerv(GL_CURRENT_PROGRAM, &PreviousProgramObject);

    // Compile and link wave evaluator program...

        // The vertex shaders...
        GLchar const   *ppVertexShaders[]   = { "evaluate_waves.glslv" };

        // The fragment shaders...
        GLchar const   *ppFragmentShaders[] = { "evaluate_waves.glslf" };

        // Generic vertex attribute names...
        GLchar const   *ppGVA[]             = { "GridLocation" };

        // Generic vertex attribute ID space...
        GLuint          GVA_Indices[sizeof(ppGVA)];
        
        // Fragment data names...
        GLchar const   *ppFragmentData[]    = 
        { 
            "StoredZ_0", 
            "StoredZ_1", 
            "StoredNormal_0", 
            "StoredNormal_1", 
            "StoredTangent_0", 
            "StoredTangent_1" 
        };

        // Fragment data requested colour buffer indices...
        GLuint const    FragmentDataBuffers[] = 
        { 
            EvaluateWaves_FragData_Z_0,
            EvaluateWaves_FragData_Z_1,
            EvaluateWaves_FragData_Normal_0,
            EvaluateWaves_FragData_Normal_1,
            EvaluateWaves_FragData_Tangent_0,
            EvaluateWaves_FragData_Tangent_1 
        };

        // Compile and link...
        m_PO_EvaluateWaves = InstallProgram(
            ppVertexShaders, 
            sizeof(ppVertexShaders) / sizeof(ppVertexShaders[0]), 
            ppFragmentShaders, 
            sizeof(ppFragmentShaders) / sizeof(ppFragmentShaders[0]),
            ppGVA,
            sizeof(ppGVA) / sizeof(ppGVA[0]),
            GVA_Indices,
            ppFragmentData,
            sizeof(ppFragmentData) / sizeof(ppFragmentData[0]),
            FragmentDataBuffers);

        // Store the generic vertex attribute indices...
        m_GVA_GridLocations = GVA_Indices[0];

        // Initialize uniforms...

            // Buffer switch...
            glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "BufferSwitch"), 0);

            /* Samplers...

                // Z-maps...
                glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "ZMap_0"), EvaluateWaves_FragData_Z_0);
                glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "ZMap_1"), EvaluateWaves_FragData_Z_1);

                // Normal maps...
                glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "NormalMap_0"), EvaluateWaves_FragData_Normal_0);
                glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "NormalMap_1"), EvaluateWaves_FragData_Normal_1);

                // Tangent maps...
                glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "TangentMap_0"), EvaluateWaves_FragData_Tangent_0);
                glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "TangentMap_1"), EvaluateWaves_FragData_Tangent_1);*/

        // Bind fragment shader outputs to respective data buffers...

            // Create list of attachment points for each fragment output ID...
            GLuint const Buffers[6] = { 
                m_ZMaps_AttachmentPoint[0],
                m_ZMaps_AttachmentPoint[1],
                m_NormalMaps_AttachmentPoint[0],
                m_NormalMaps_AttachmentPoint[1],
                m_TangentMaps_AttachmentPoint[0],
                m_TangentMaps_AttachmentPoint[1]
            };
        
            // Load the draw buffers...    
            glDrawBuffers(6, Buffers);

    // Restore previously installed program object...
    glUseProgram(PreviousProgramObject);

    // Initialize grid location pairs...

        // Calculate required storage space for all location pairs...
        GLuint const Storage = m_Width * m_Height * sizeof(GLuint) * 2;

        // Allocate...
        GLuint *pGridLocations = (GLuint *) malloc(Storage);
        
        // Initialize vertex ordinals across the grid...
        for(GLuint CurrentY = 0; CurrentY < m_Height; ++CurrentY)
            for(GLuint CurrentX = 0; CurrentX < m_Width; ++CurrentX)
            {
                // Calculate current location...
                GLuint *pCurrentLocation = 
                    pGridLocations + (2 * ((CurrentY * m_Width) + CurrentX));

                // Store grid index...
                pCurrentLocation[0] = CurrentX;
                pCurrentLocation[1] = CurrentY;
            }

        // Initialize vertex buffer object with vertex IDs...
        
            // Allocate...
            glGenBuffers(1, &m_VBO_GridLocations);
            
            // Select...
            glBindBuffer(GL_ARRAY_BUFFER, m_VBO_GridLocations);
            
            // Commit grid indices to card...
            glBufferData(GL_ARRAY_BUFFER, Storage, pGridLocations, GL_STATIC_DRAW);
            glVertexAttribPointer(m_GVA_GridLocations, 2, GL_UNSIGNED_INT, GL_FALSE, 0, BufferOffset(0));

    // Restore default framebuffer...
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

    // Check for OpenGL errors...
    PrintOpenGLErrors();
}

// Re-evaluate the waves, but must be called at constant interval...
void Fluid::EvaluateWaves()
{
    // Variables...
    GLint   Viewport[4] = { 0, 0, 0, 0 };

    // Backup viewport size, and now resize to size of grid maps...
    glGetIntegerv(GL_VIEWPORT, Viewport);
    glViewport(0, 0, m_Width, m_Height);

    // Redirect rendering through our framebuffer object...
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_FrameBufferObject);

        // Check for OpenGL errors...
        PrintOpenGLErrors();

    // Load texture units...

        // Z-maps...
        glActiveTexture(GL_TEXTURE0 + EvaluateWaves_FragData_Z_0);
        glBindTexture(GL_TEXTURE_2D, m_ZMaps_TextureID[0]);
        glActiveTexture(GL_TEXTURE0 + EvaluateWaves_FragData_Z_1);
        glBindTexture(GL_TEXTURE_2D, m_ZMaps_TextureID[1]);

        // Normal maps...
        glActiveTexture(GL_TEXTURE0 + EvaluateWaves_FragData_Normal_0);
        glBindTexture(GL_TEXTURE_2D, m_NormalMaps_TextureID[0]);
        glActiveTexture(GL_TEXTURE0 + EvaluateWaves_FragData_Normal_1);
        glBindTexture(GL_TEXTURE_2D, m_NormalMaps_TextureID[1]);
        
        // Tangent maps...
        glActiveTexture(GL_TEXTURE0 + EvaluateWaves_FragData_Tangent_0);
        glBindTexture(GL_TEXTURE_2D, m_TangentMaps_TextureID[0]);
        glActiveTexture(GL_TEXTURE0 + EvaluateWaves_FragData_Tangent_1);
        glBindTexture(GL_TEXTURE_2D, m_TangentMaps_TextureID[1]);

            // Check for OpenGL errors...
            PrintOpenGLErrors();

    // Send grid location data to program...

        // Install program...
        glUseProgram(m_PO_EvaluateWaves);

        // Enable required vertex arrays...
        glEnableVertexAttribArray(m_GVA_GridLocations);

        // Select the grid indices...
        glBindBuffer(GL_ARRAY_BUFFER, m_VBO_GridLocations);

        // Commit data to shader...
        glDrawElements(GL_POINTS, m_Width * m_Height, GL_UNSIGNED_INT, BufferOffset(0));

        // Disable required vertex arrays...
        glDisableVertexAttribArray(m_GVA_GridLocations);

        // Check for OpenGL errors...
        PrintOpenGLErrors();

    // Restore state...
    
        // Default framebuffer...
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    
        // Viewport back to original dimensions...
        glViewport(Viewport[0], Viewport[1], Viewport[2], Viewport[3]);
        
        // Check for OpenGL errors...
        PrintOpenGLErrors();
}

Kip