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:
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:
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.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.