Search code examples
c++opengltexture-mapping

Create contour plot in opengl using grid data and a 1D texture map


I have a set of X,Y,Z values on a regular spaced grid from which I need to create a color-filled contour plot using C++. I've been googling on this for days and the consensus appears to be that this is achievable using a 1D texture map in openGL. However I have not found a single example of how to actually do this and I'm not getting anywhere just reading the openGL documentation. My confusion comes down to one core question:

My data does not contain an X,Y value for every pixel - it's a regularly spaced grid with data every 4 units on the X and Y axis, with a positive integer Z value.

For example: (0, 0, 1), (4, 0, 1), (8, 0, 2), (0, 4, 2), (0, 8, 4), (4, 4, 3), etc.

Since the contours would be based on the Z value and there are gaps between data points, how does applying a 1D texture achieve contouring this data (i.e. how does applying a 1D texture interpolate between grid points?)

The closest I've come to finding an example of this is in the online version of the Redbook (http://fly.cc.fer.hr/~unreal/theredbook/chapter09.html) in the teapot example but I'm assuming that teapot model has data for every pixel and therefore no interpolation between data points is needed.

If anyone can shed light on my question or better yet point to a concrete example of working with a 1D texture map in this way I'd be forever grateful as I've burned 2 days on this project with little to show for it.

EDIT:

The following code is what I'm using and while it does display the points in the correct location there is no interpolation or contouring happening - the points are just displayed as, well, points.

//Create a 1D image - for this example it's just a red line
int stripeImageWidth = 32;
GLubyte   stripeImage[3*stripeImageWidth];
for (int j = 0; j < stripeImageWidth; j++) {
    stripeImage[3*j] = j < 2 ? 0 : 255;
    stripeImage[3*j+1] = 255;
    stripeImage[3*j+2] = 255;
}
glDisable(GL_TEXTURE_2D);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage1D(GL_TEXTURE_1D, 0, 3, stripeImageWidth, 0, GL_RGB, GL_UNSIGNED_BYTE, stripeImage);
glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

glTexGeni( GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR );
float s[4] = { 0,1,0,0 };
glTexGenfv( GL_S, GL_OBJECT_PLANE, s );
glEnable( GL_TEXTURE_GEN_S );
glEnable( GL_TEXTURE_1D );
glBegin(GL_POINTS);

//_coords contains X,Y,Z data - Z is the value that I'm trying to contour
for (int x = 0; x < _coords.size(); ++x)
{
    glTexCoord1f(static_cast<ValueCoord*>(_coords[x])->GetValue());
    glVertex3f(_coords[x]->GetX(), _coords[x]->GetY(), zIndex);
}
glEnd();

Solution

  • The idea is using the Z coordinate as S coordinate into the texture. The linear interpolation over the texture coordinate then creates the contour. Note that by using a shader you can put the XY->Z data into a 2D texture and use a shader to do a indirection of the value of the 2D sampler in the color ramp of the 1D texture.

    Update: Code example

    First we need to change the way you use textures a bit.

    To this to prepare the texture:

    //Create a 1D image - for this example it's just a red line
    int stripeImageWidth = 32;
    GLubyte   stripeImage[3*stripeImageWidth];
    for (int j = 0; j < stripeImageWidth; j++) {
        stripeImage[3*j] = j*255/32; // use a gradient instead of a line
        stripeImage[3*j+1] = 255;
        stripeImage[3*j+2] = 255;
    }
    
    GLuint texID;
    glGenTextures(1, &texID);
    glBindTexture(GL_TEXTURE_1D, texID);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexImage1D(GL_TEXTURE_1D, 0, 3, stripeImageWidth, 0, GL_RGB, GL_UNSIGNED_BYTE, stripeImage);
    
    // We want the texture to wrap, so that values outside the range [0, 1] 
    // are mapped into a gradient sawtooth
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
    

    And this to bind it for usage.

    // The texture coordinate comes from the data, it it not
    // generated from the vertex position!!!
    glDisable( GL_TEXTURE_GEN_S ); 
    glDisable(GL_TEXTURE_2D);
    glEnable( GL_TEXTURE_1D );
    glBindTexture(GL_TEXTURE_1D, texID);
    

    Now to your conceptual problem: You cannot directly make a contour plot from XYZ data. XYZ are just sparse sampling points. You need to fill the gaps, for example by putting it into a 2D histogram first. For this create a grid with a certain amount of bins in each direction, initialized to all NaN (pseudocode)

    float hist2D[bins_x][bins_y] = {NaN, NaN, ...}
    

    then for each XYZ, add the Z value to the bins of the grid if not a NaN, otherwise replace NaN with the Z value. Afterwards use a Laplace filter on the histogram to smooth out the bins still containing a NaN. Finally you can render the grid as contour plot using

    glBegin(GL_QUADS);
    for(int y=0; y<grid_height; y+=2) for(int x=0; x<grid_width; x+=2) {
        glTexCoord1f(hist2D[x  ][y  ]]); glVertex2i(x  ,y);
        glTexCoord1f(hist2D[x+1][y  ]]); glVertex2i(x+1,y);
        glTexCoord1f(hist2D[x+1][y+1]]); glVertex2i(x+1,y+1);
        glTexCoord1f(hist2D[x  ][y+1]]); glVertex2i(x  ,y+1);
    }
    glEnd();
    

    or you could upload the grid as a 2D texture and use a fragment shader to indirect into the color ramp.

    Another way to fill the gaps in sparse XYZ data is to find the 2D Voronoi diagram of the XY set and use this to create the sampling geometry. The Z values for the vertices would be the distance weighted average of the XYZs contributing to the Voronoi cells intersecting.