Search code examples
c++openglglfwglm-mathtexture-mapping

In OpenGL, Drawing a spinning globe using glDrawElements has weird artifacts Seems like an edge bug? How can I debug it?


I wrote a demo drawing a textured sphere using an indexed draw using triangle strip. The indices seem correct. Given 30 points around each row:

0, 30, 1, 31, 2, 32, ... 29, 59, 0, 30

Then there are degenerate triangles at the end of each row. The coordinates and indices look right to me. As I count it, if there is lonRes points around each circle around the planet on a line of latitude (in the demo, lonRes= 30) then there should be lonRes+4 per row because each row connects back to the start, and then two degenerate indices to continue to the next row. Somehow that computation is off, indexSize=2503 and the actual size is 2432. I set indexSize to the number of indices actually used and render. Most of the sphere looks good, but there is a missing column where you would expect to connect back to the start. The earth rotates (in the picture shown below, Africa is moving to the right) but that gap at the edge of each row, rotates backward (it's moving to the left and covers it over. Whatever is wrong with my draw command, I would have expected it to all rotate together. I don't understand the mechanism, and the fact that it's a single draw call is what makes it challenging to debug. It's a black box to me.

I would accept an answer, of course, but I would like to learn some strategies for how to go about debugging this. I have printed the coordinates and indices. They seem correct.

I am including the code, the shaders even though I'm fairly sure they aren't the problem, and a screenshot showing the bug.

The code is built using:

g++  -g -std=c++20 -c common/common.cc
g++  -g -std=c++20 06b_sphere3.cc Shape.cc -o bin/06b_sphere3 common.o -lglfw -L/usr/lib64 -lGLEW -lGL -lX11 -lGLU -lwebp

06b_sphere3.cpp:

/*
    Textured Sphere demo
    Load a webp cylindrical projection of earth and map to the sphere
    Tilt earth axis to 23.5 degrees and rotate
*/
#include <GL/glew.h>
#include "common/common.hh"
#include <glm/glm.hpp>
#include <glm/ext.hpp>
#include <numbers>
#include <iostream>
#include <iomanip>
#include <cstdint>
#include <string>
using namespace std;
using namespace glm;
using namespace std::numbers;
constexpr double PI = numbers::pi;

class Sphere {
private:
    uint32_t progid; // handle to the shader code
    uint32_t vao; // array object container for vbo and indices
    uint32_t vbo; // handle to the point data on the graphics card
    uint32_t lbo; // handle to buffer of indices for lines for wireframe sphere
    uint32_t latRes, lonRes;
    uint32_t resolution;
    uint32_t indexSize;
public:
    /**
     * @brief Construct a sphere
     *
     * @param r radius of the sphere
     * @param latRes resolution of the grid in latitude
     * @param lonRes resolution of the grid in latitude
     * @param texturePath path to the texture image
     */
    Sphere(double r, uint32_t latRes, uint32_t lonRes);
    ~Sphere() { cleanup(); }
    void render(mat4& trans, GLuint textureID);
    void cleanup();
};

Sphere::Sphere(double r, uint32_t latRes, uint32_t lonRes) : latRes(latRes), lonRes(lonRes),
    resolution((2*latRes-1)*lonRes + 2) {
    progid = loadShaders("06b_texturepoints.vert", "06b_textures.frag");
//    progid = loadShaders("03gouraud.vert", "03gouraud.frag");
    double dlon = 2.0*PI / lonRes, dlat = PI / (2*latRes);
    double z;
    double lat = -PI/2 + dlat; // latitude in radians
    double rcircle;
    float vert[resolution*5]; // x,y,z,u,v
    uint32_t c = 0;
    for (uint32_t j = 0; j < 2*latRes-1; j++, lat += dlat) {
        //what is the radius of hte circle at that height?
        rcircle = r* cos(lat); // size of the circle at this latitude
        z = r * sin(lat); // height of each circle
    
        cout << "rcircle=" << rcircle << ", z=" << z << endl;
        double t = 0;
        for (uint32_t i = 0; i < lonRes; i++, t += dlon) {
            vert[c++] = rcircle * cos(t),
            vert[c++] = rcircle * sin(t);
                cout << '(' << vert[c-2] << ", " << vert[c-1] << ")  ";
            vert[c++] = z;
            vert[c++] = t / (2.0 * PI); // Correct u mapping
            vert[c++] = (lat + PI / 2.0) / PI; // Correct v mapping
        }
        cout << endl;
    }
    // south pole
    vert[c++] = 0;
    vert[c++] = 0;
    vert[c++] = -r;
    vert[c++] = 0.5;
    vert[c++] = 0;

    // north pole
    vert[c++] = 0;
    vert[c++] = 0;
    vert[c++] = r;
    vert[c++] = 0.5;
    vert[c++] = 1;

    cout << "resolution: " << resolution << endl;
    cout << "predicted num vert components: " << resolution*5 << endl;  
    cout << "actual num vert components: " << c << endl;

    indexSize = resolution * 2 + (2*4*latRes-1); 
    //TODO: North and South Poles aren't used
    uint32_t indices[indexSize]; // connect every point in circles or latitude and longitude
    c = 0;
    for (uint32_t j = 0; j < 2*latRes - 2; j++) {
        uint32_t startrow = j*lonRes;
        for (uint32_t i = 0; i < lonRes; i++) {
            indices[c++] = startrow + i;
            indices[c++] = startrow + lonRes + i;
        }
        indices[c++] = startrow;
        indices[c++] = startrow + lonRes;
        // Add degenerate triangles to connect strips
        indices[c++] = (j + 1) * lonRes;
        indices[c++] = (j + 1) * lonRes;
    }
    cout << "indexSize: " << indexSize << endl;
    cout << "actual grid indices: " << c << endl;

    indexSize = c; // not sure why the computaiton is off...
    // Print index data
    cout << "Index data: " ;
    for (size_t i = 0; i < indexSize; i += 2) {
        cout << '(' << indices[i] << ", " << indices[i+1] << ") ";
    }

    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, resolution*5*sizeof(float), vert, GL_STATIC_DRAW);
    glGenBuffers(1, &lbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, lbo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexSize*sizeof(uint32_t), indices, GL_STATIC_DRAW);
    glBindVertexArray(0);
}


void Sphere::render(mat4& trans, GLuint textureID) {
    glUseProgram(progid);           // Use the shader
    uint32_t matrixID = glGetUniformLocation(progid, "transform");
    glUniformMatrix4fv(matrixID, 1, GL_FALSE, &trans[0][0]);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, textureID);
    glUniform1i(glGetUniformLocation(progid, "textureSampler"), 0);

    glBindVertexArray(vao);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); // Position
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); // Texture coordinates
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);

    glDrawElements(GL_TRIANGLE_STRIP, indexSize, GL_UNSIGNED_INT, 0);

    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
    glBindVertexArray(0);
}

void Sphere::cleanup() {
    glDeleteBuffers(1, &vbo);   // remove vbo memory from graphics card
    glDeleteBuffers(1, &lbo);   // remove lbo (line indices)
    glDeleteVertexArrays(1, &vao); // remove vao from graphics card
    glDeleteProgram(progid);
}

using namespace std;
using namespace numbers;

void glmain() {
    win = createWindow(800, 800, "Sphere demo");

    glClearColor(0.0f, 0.0f, 0.4f, 0.0f);   // Dark blue background
    GLuint textureID = loadWebPTexture("earth.webp"); // Load the texture
    Sphere sphere(1.0, 20, 30);
    float rotAngle = 0, dRotAngle = 0.0052;
    mat4 northup = rotate(mat4(1.0f), float(PI/2), vec3(1, 0, 0));

//    mat4 northup = mat4(1.0f);

    vec3 up(0, 1, 0);    // normal OpenGL coordinates, x positive to right, y is up (z positive out of screen)
    do {
        mat trans = rotate(northup, radians(23.5f), vec3(0, 1, 0)); // tilt axis
        trans = rotate(trans, rotAngle, vec3(0, 0, 1)); // spin on axis
        rotAngle += dRotAngle;

        glClear(GL_COLOR_BUFFER_BIT);   // Clear the screen
        glDisable(GL_DEPTH_TEST);
        sphere.render(trans, textureID);

        glfwSwapBuffers(win);             // double buffer
        glfwPollEvents();
    } while (glfwGetKey(win, GLFW_KEY_ESCAPE) != GLFW_PRESS &&
             glfwWindowShouldClose(win) == 0);
    glDeleteTextures(1, &textureID); // Clean up the texture
}

common.hh

#include <GL/glew.h>    // OpenGL API
#include <GLFW/glfw3.h> // Window API
#include <glm/glm.hpp>  // Matrix and vector math for OpenGL
#include <glm/ext.hpp>

// all demos use a window, declared globally in common.cc
extern GLFWwindow* win;
GLFWwindow* createWindow(uint32_t w, uint32_t h, const char title[]);
GLuint loadShaders(const char vertexPath[], const char * fragmentPath);

void glmain();

/*
  provide a standardized main, because it's always the same
  It catches exceptions and quits if there is a problem
    you write glmain instead
*/
int main(int argc, char* argv[]);
void dump(glm::mat4& mat);
void transpt(glm::mat4& m, double x, double y, double z);

GLuint loadWebPTexture(const char* filePath);
GLuint build_prog(const char vertex_shader[], const char fragment_shader[]);
const uint32_t INVALID_UNIFORM_LOCATION = 0xFFFFFFFF;

// TODO: eventually move all hardcoded, prebuilt shaders into strings
/*
  generically render a textured object composed of a VAO, containing a vertex buffer, an index buffer,
   a texture, and a texture unit
*/
void render_textured_indexed(GLuint program, GLuint vao, GLuint vert, GLuint index, GLuint texture);

/*
  generically render a textured object composed of a VAO, containing a vertex buffer with a color per vertex,
   an index buffer,
*/
void render_indexed_colored(GLuint program, GLuint vao, GLuint vert, GLuint index);

/*
    generically render a surface composed of a VAO, containing a vertex buffer with a value per vertex looking up in a 1D-texture   
*/
void render_indexed_heatmap(GLuint program, GLuint vao, GLuint vert, GLuint index, GLuint texture);

common.cpp

#include <stdio.h>
#include <string>
#include <vector>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <algorithm>
#include <sstream>
#include <webp/decode.h>
using namespace std;

#include <stdlib.h>
#include <string.h>

#include <GL/glew.h>

#include "common.hh"

GLFWwindow* win = nullptr;


void check_status( GLuint obj, bool isShader ) {
    GLint status = GL_FALSE, log[ 1 << 11 ] = { 0 };
    ( isShader ? glGetShaderiv : glGetProgramiv )( obj, isShader ? GL_COMPILE_STATUS : GL_LINK_STATUS, &status );
    if( status == GL_TRUE ) return;
    ( isShader ? glGetShaderInfoLog : glGetProgramInfoLog )( obj, sizeof( log ), nullptr, (GLchar*)log );
    std::cerr << (GLchar*)log << "\n";
    std::exit( EXIT_FAILURE );
}

void attach_shader( GLuint program, GLenum type, const char* src ) {
    GLuint shader = glCreateShader( type );
    glShaderSource( shader, 1, &src, NULL );
    glCompileShader( shader );
    check_status( shader, true );
    glAttachShader( program, shader );
    glDeleteShader( shader );
}

// build a vertex and fragment shader program from constants in source code
// this hardcoded version is provided for all the standard shaders that you want for common graphics
// you can always load from files but I will build a number of the basic ones in here
GLuint build_prog(const char vertex_shader[], const char fragment_shader[]) {
    GLuint prog = glCreateProgram();
    attach_shader( prog, GL_VERTEX_SHADER, vertex_shader );
    attach_shader( prog, GL_FRAGMENT_SHADER, fragment_shader );
    glLinkProgram( prog );
    check_status( prog, false );
    return prog;
}

GLuint loadShaders(const char vertexPath[], const char * fragmentPath) {
    // Create the shaders
    GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER);
    GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);

    // Read the Vertex Shader code from the file
    std::string VertexShaderCode;
    std::ifstream VertexShaderStream(vertexPath, std::ios::in);
    if(VertexShaderStream.is_open()){
        std::stringstream sstr;
        sstr << VertexShaderStream.rdbuf();
        VertexShaderCode = sstr.str();
        VertexShaderStream.close();
    }else{
        printf("Impossible to open %s. Are you in the right directory ? Don't forget to read the FAQ !\n", vertexPath);
        getchar();
        return 0;
    }

    // Read the Fragment Shader code from the file
    std::string FragmentShaderCode;
    std::ifstream FragmentShaderStream(fragmentPath, std::ios::in);
    if(FragmentShaderStream.is_open()){
        std::stringstream sstr;
        sstr << FragmentShaderStream.rdbuf();
        FragmentShaderCode = sstr.str();
        FragmentShaderStream.close();
    }

    GLint Result = GL_FALSE;
    int InfoLogLength;


    // Compile Vertex Shader
    printf("Compiling shader : %s\n", vertexPath);
    char const * VertexSourcePointer = VertexShaderCode.c_str();
    glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL);
    glCompileShader(VertexShaderID);

    // Check Vertex Shader
    glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result);
    glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
    if ( InfoLogLength > 0 ){
        std::vector<char> VertexShaderErrorMessage(InfoLogLength+1);
        glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
        printf("%s\n", &VertexShaderErrorMessage[0]);
    }

    // Compile Fragment Shader
    printf("Compiling shader : %s\n", fragmentPath);
    char const * FragmentSourcePointer = FragmentShaderCode.c_str();
    glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL);
    glCompileShader(FragmentShaderID);

    // Check Fragment Shader
    glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result);
    glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
    if ( InfoLogLength > 0 ){
        std::vector<char> FragmentShaderErrorMessage(InfoLogLength+1);
        glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
        printf("%s\n", &FragmentShaderErrorMessage[0]);
    }

    // Link the program
    printf("Linking program\n");
    GLuint ProgramID = glCreateProgram();
    glAttachShader(ProgramID, VertexShaderID);
    glAttachShader(ProgramID, FragmentShaderID);
    glLinkProgram(ProgramID);

    // Check the program
    glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result);
    glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength);
    if ( InfoLogLength > 0 ){
        std::vector<char> ProgramErrorMessage(InfoLogLength+1);
        glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]);
        printf("%s\n", &ProgramErrorMessage[0]);
    }

    
    glDetachShader(ProgramID, VertexShaderID);
    glDetachShader(ProgramID, FragmentShaderID);
    
    glDeleteShader(VertexShaderID);
    glDeleteShader(FragmentShaderID);

    return ProgramID;
}

GLFWwindow* createWindow(uint32_t w, uint32_t h, const char title[]) {
    // Initialise GLFW
    if( !glfwInit() )   {
        throw "Failed to initialize GLFW";
    }

    glfwWindowHint(GLFW_SAMPLES, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // To make MacOS happy; should not be needed
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // Open a window and create its OpenGL context
    GLFWwindow* win = glfwCreateWindow(w, h, title, nullptr, nullptr);
    if (win == nullptr) {
        glfwTerminate();
        throw "Failed to open GLFW window";
    }
    glfwMakeContextCurrent(win); // create OpenGL context

    // Initialize GLEW
    glewExperimental = true; // Needed for core profile
    if (glewInit() != GLEW_OK) {
        throw "Failed to initialize GLEW";
    }

    // Ensure we can capture the escape key to quit
    glfwSetInputMode(win, GLFW_STICKY_KEYS, GL_TRUE);

    return win;
}

/*
    standardized main to catch errors.
    In this simplified version each error is just reported as a string
    It would be better to also track which file and line number the error
    happened in, but that would take an exception object.
    For now, keeping it simple
 */
int main(int argc, char* argv[]) {
    try {
        glmain();
        glfwTerminate();        // Close OpenGL window and terminate GLFW
    } catch (const char* msg) {
        cerr << msg << '\n';
        exit(-1);
    }
    return 0;
}


void dump(glm::mat4& mat) {
    // TODO: I suspect we are printing the matrix transposed
    const float* m = &mat[0][0];
    cerr << setprecision(7);
    for (int i = 0, c = 0; i < 4; i++) {
      for (int j = 0; j < 4; j++, c++)
        cerr << setw(14) << m[c];
      cerr << '\n';
    }
  }

void transpt(glm::mat4& m, double x, double y, double z) {
    cerr << "orig=(" << x << "," << y << "," << z << ") transformed: (" <<
     (m[0][0] * x + m[1][0] * y + m[2][0] * z + m[3][0]) << "," <<
     (m[0][1] * x + m[1][1] * y + m[2][1] * z + m[3][1]) << "," <<
     (m[0][2] * x + m[1][2] * y + m[2][2] * z + m[3][2]) << ")\t(";

    cerr <<
         (m[0][0] * x + m[0][1] * y + m[0][2] * z + m[0][3]) << "," <<
     (m[1][0] * x + m[1][1] * y + m[1][2] * z + m[1][3]) << "," <<
     (m[2][0] * x + m[2][1] * y + m[2][2] * z + m[2][3]) << ")\n";

  }

GLuint loadWebPTexture(const char* filePath) {
    // Read the file into a buffer
    std::ifstream file(filePath, std::ios::binary | std::ios::ate);
    if (!file.is_open()) {
        std::cerr << "Failed to open WebP file: " << filePath << std::endl;
        return 0;
    }
    std::streamsize size = file.tellg();
    file.seekg(0, std::ios::beg);
    std::vector<char> buffer(size);
    if (!file.read(buffer.data(), size)) {
        std::cerr << "Failed to read WebP file: " << filePath << std::endl;
        return 0;
    }

    // Decode the WebP image
    int width, height;
    uint8_t* data = WebPDecodeRGBA(reinterpret_cast<uint8_t*>(buffer.data()), size, &width, &height);
    if (!data) {
        std::cerr << "Failed to decode WebP image: " << filePath << std::endl;
        return 0;
    }

    // Generate and bind a texture
    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);

    // Upload the texture data
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

    // Set texture parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // Free the image data
    WebPFree(data);

    return textureID;
}

void transform_dump(glm::mat4& mat, double x, double y, double z) {
    glm::vec4 point = glm::vec4(x, y, z, 1.0f);
    glm::vec4 tp = mat * point;
    cerr << "Transformed point: (" << tp.x << ", " << tp.y << ", " << tp.z << ")\n";
}

06b_texturepoints.vert

#version 330 core

layout(location = 0) in vec3 pos;       // Position (x, y, z)
layout(location = 1) in vec2 texCoord;  // Texture coordinates (u, v)

uniform mat4 transform;

out vec2 TexCoord;

void main() {
    gl_Position = transform * vec4(pos, 1.0);
    TexCoord = texCoord;
}

06b_textures.frag

#version 330 core

in vec2 TexCoord;
out vec4 FragColor;

uniform sampler2D textureSampler;

void main() {
    FragColor = texture(textureSampler, TexCoord);
} 

Here is the screen shot trying to show the issue:

enter image description here


Solution

  • For the sake of argument, let's assume you set lonRes to 10. Given the code above, a single horizontal strip in vert will have u for the left vertex set to 0, 0.1, ..., 0.9.

    In your index calculation code, you connect each vertex with the one after it, and then the final vertex with the first in the row, so the vertices with u=0.9 will be connected to the ones with u=0. This means that final square will contain 90% (1-1/lonRes) of the entire map, flipped horizontally because you go from u=0.9 to u=0.

    Instead, make your loop over longitude one longer so you produce vertices where u=1. Also adjust the loop over indices so it goes up to and including lonRes, and drop the explicit reset to startrow.

    Some unrelated advice:

    • write a struct Vertex { float x,y,z,u,v; } so you can assign values by field or create an entire vertex at once instead of doing vert[c++] = ... all the time.
    • embrace std::vector! Using emplace_back and sensible capacity hints is equally fast as a plain array, with no need to calculate the number of vertices up front.