Weird Waves

So I have a 2D grid of vertices, each with a Z value representing the displacement at that point. Collectively, these form waves on the surface.

I initialize this Z-map to all zeros (state of rest), with a disturbance in the middle (-0.5, and -0.3 to surrounding 8 vertices).

A 9x9 grid:


0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	
0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	
0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	
0.00	0.00	0.00	-0.30	-0.30	-0.30	0.00	0.00	0.00	
0.00	0.00	0.00	-0.30	-0.50	-0.30	0.00	0.00	0.00	
0.00	0.00	0.00	-0.30	-0.30	-0.30	0.00	0.00	0.00	
0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	
0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	
0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00

I call a method EvaluateWaves() at regular intervals to update the Z values. It takes into account preset constants, such as desired wave speed, fluid viscosity, and so on.

Since a disturbance in the middle should result in the same thing happening outward radially in all directions, I was surprised to see the result after an EvaluateWaves() call resulting in some non-symmetrical results:


0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	
0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	
0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	
0.00	0.00	0.00	0.00	-0.11	-0.11	-0.11	0.00	0.00	
0.00	0.00	0.00	-0.11	-0.08	-0.26	-0.08	-0.11	0.00	
0.00	0.00	0.00	-0.11	-0.26	-0.21	-0.26	-0.11	0.00	
0.00	0.00	0.00	-0.11	-0.08	-0.26	-0.08	-0.11	0.00	
0.00	0.00	0.00	0.00	-0.11	-0.11	-0.11	0.00	0.00	
0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00	0.00

I am sending the grid to my fragment shader which performs the calculations and writes it out to a texture bound to the framebuffer object. The texture is ping-ponged back and forth at each evaluation.

The EvaluateWaves() call:


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

    // Setup viewport for one to one pixel = texel = geometry mapping...

        // Projection should not transform...
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        gluOrtho2D(0.0, m_Width, 0.0, m_Height);

        // There should be no modelview transformations either...
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();

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

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

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

    // Prepare shader state and dispatch grid location data to program...

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

        // Prepare texture units...

            // First texture unit always contains first z-map...
            glActiveTexture(GL_TEXTURE0 + m_ZMaps_TextureUnit[0]);
            glBindTexture(GL_TEXTURE_2D, m_ZMaps_TextureID[0]);

            // Second texture unit always contains second z-map...
            glActiveTexture(GL_TEXTURE0 + m_ZMaps_TextureUnit[1]);
            glBindTexture(GL_TEXTURE_2D, m_ZMaps_TextureID[1]);

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

        // Update uniforms...

            // Z-maps...
            
                // Previous...
                glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "ZMapPrevious"), 
                    m_ZMaps_TextureUnit[!m_SourceBufferSwitch]);

                // Current...
                glUniform1i(GetUniformIndex(m_PO_EvaluateWaves, "ZMapCurrent"), 
                    m_ZMaps_TextureUnit[m_SourceBufferSwitch]);

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

            // Create list of attachment points for each fragment output ID...
            GLuint const DrawBuffers[] = {
                m_ZMaps_AttachmentPoint[!m_SourceBufferSwitch],
                m_NormalMap_AttachmentPoint,
                m_TangentMap_AttachmentPoint
            };

            // Load the draw buffers...
            glDrawBuffers(sizeof(DrawBuffers) / sizeof(DrawBuffers[0]), DrawBuffers);

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

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

        // Commit data to shader...
        glDrawArrays(GL_POINTS, 0, m_Width * m_Height);

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

        // Swap the source buffer for next evaluation...
        m_SourceBufferSwitch = !m_SourceBufferSwitch;

        // 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();
}

Vertex shader:


// Input variables...

    // Grid location...
    in vec2         GridLocation;

// Uniform variables...

    // Dimensions...
    uniform uint    Width;
    uniform uint    Height;

// Variables for the fragment shader, none of which need be interpolated...

    // Normalized location cooresponding to grid location...
    out vec2        NormalizedLocation;
    
    // Texture location cooresponding to grid location...
    out vec2        TextureLocation;

// Entry point...
void main()
{
    // Calculate texture location which has components in [0, 1] range...
    TextureLocation = GridLocation / vec2(Width, Height); 

    // Calculate normalized location which has components in [-1, 1] range...
    NormalizedLocation = TextureLocation * vec2(2.0, 2.0) - vec2(1.0, 1.0);

    // Generate a fragment...
    gl_Position = vec4(NormalizedLocation, 0.0, 1.0);
}

Fragment shader:


// Input variables...

    // Normalized location cooresponding to grid location...
    in vec2             NormalizedLocation;
    
    // Texture location cooresponding to grid location...
    in vec2             TextureLocation;

// Uniform variables...

    // Dimensions...
    uniform uint        Width;
    uniform uint        Height;

    // Grid spacing between vertices...
    uniform float       DistanceBetweenVertices;
    
    // Pre-computed equation constants...
    uniform float       CachedConstant1;
    uniform float       CachedConstant2;
    uniform float       CachedConstant3;
    
    // Z-displacement maps...
    uniform sampler2D   ZMapPrevious;
    uniform sampler2D   ZMapCurrent;

// Output variables...

    // Z-displacement output buffer...
    out float           ZPreviousOut;
    
    // Normal...
    out vec3            NormalOut;
    
    // Tangent...
    out vec3            TangentOut;

// Take a grid location and transform to a normalized texture coordinate...
vec2 GridToTexture(const uvec2 Location)
{
    // Transform to [0..1] and return it...
    return (Location / vec2(Width, Height));
}

// Take coordinate normalized in [-1..1] and transform to [0..Width or Height]...
uvec2 NormalizedPointToGrid(const vec2 Normalized)
{
    // Transform and return it... p = (d/2)(p'+1)
    return uvec2((vec2(Width, Height) * vec2(0.5, 0.5)) * (Normalized + vec2(1.0, 1.0)));
}

// Transform coordinate in [-1..1] to [0..1]
vec2 NormalizedPointToTexture(const vec2 Normalized)
{
    // Transform normalized form to texture coordinate... p_t = (p' + <1,1>) / 2
    return (Normalized + vec2(1.0, 1.0)) * vec2(0.5, 0.5);
}

// Entry point...
void main()
{
    // Get location on grid...
    uvec2 GridLocation = NormalizedPointToGrid(NormalizedLocation);
    
        // We assume the outer edge of fluid surface is fixed. This also makes
        //  querying neighbours easier since we can assume there always will be
        //  exactly eight all around it after this, though we don't affect the
        //  diagonal neighbours...

        if(GridLocation.x == 0u || GridLocation.x == (Width - 1u) ||
           GridLocation.y == 0u || GridLocation.y == (Height - 1u))
            discard;

    // Lookup displacement of current location...
    float CurrentZ      = texture(ZMapCurrent, TextureLocation).r;
    
    // Lookup displacements of current location's neighbours...
    float AboveCurrentZ = texture(ZMapCurrent, GridToTexture(GridLocation + uvec2( 0,  1))).r;
    float BelowCurrentZ = texture(ZMapCurrent, GridToTexture(GridLocation + uvec2( 0, -1))).r;
    float RightCurrentZ = texture(ZMapCurrent, GridToTexture(GridLocation + uvec2( 1,  0))).r;
    float LeftCurrentZ  = texture(ZMapCurrent, GridToTexture(GridLocation + uvec2(-1,  0))).r;
    
    // Lookup displacement of last passes displacement at current location...
    float PreviousZ     = texture(ZMapPrevious, TextureLocation).r;

    // Update previous buffer's displacement here using equation 12.25, p.335, 
    //  of Mathematics for 3D Game Programming and Computer Graphics. */
    ZPreviousOut    = CachedConstant1 * CurrentZ +                        /* first term */
                      CachedConstant2 * PreviousZ +                       /* second term */
                      CachedConstant3 * (RightCurrentZ + LeftCurrentZ +   /* third term */
                                         AboveCurrentZ + BelowCurrentZ);

    // Calculate normal...
    NormalOut       = vec3(LeftCurrentZ  - RightCurrentZ, 
                           BelowCurrentZ - AboveCurrentZ,
                           2.0 * DistanceBetweenVertices);

    // Calculate tangent...
    TangentOut      = vec3(2.0 * DistanceBetweenVertices, 
                           0.0, 
                           RightCurrentZ - LeftCurrentZ);
}

I know this isn’t a trivial algorithm, but any light one can shed on this is appreciated.

Kip

That looks pretty symmetrical to me. What were you expecting?

No because the -0.08 marks where the disturbance happened originally, but you’ll note the values it is surrounded by vary non-symmetrically (-0.11 on the top, -0.26 on the bottom).

Figured it out. Everywhere where I was using Width or Height variable in the vertex and fragment shader, I changed to Width or Height - 1u.

Still getting used to this shader [censored]. It’s a brave new world and I am still thinking in the CPU way of things.

Kip