-1

I am creating a UV sphere (similar to an Earth globe divided into lines of latitude). I am doing this by:

  1. Calculating all of the vertices around each each parallel latitude circle (e.g. 72 points per circle)
  2. Using GL_TRIANGLE_STRIP to fill in each "slice" between each of the latitude circles.

Unfortunately I keep seeing dots on my otherwise perfect sphere.

enter image description here

What would cause this and how do I get rid of it?

void CSphere2::AddVertices( void )
{
  #define SPHERE2_RES 72

  // Create sphere using horizontal slices/circles
  int nPointsPerCircle = SPHERE2_RES;
  int nStackedCircles  = SPHERE2_RES;

  GLfloat r          = m_Size;
  GLfloat yAngle     = - (PI / 2.0f);  // Start at -90deg and work up to +90deg (south to north pole)
  GLfloat yAngleStep = PI / nStackedCircles;

  // Sweep angle is zero initially for pointing towards me (-Z direction)
  GLfloat horizSweepAngle = 0;
  GLfloat horizSweepStep  = ( 2 * PI ) / nPointsPerCircle;

  // Each time we have a slice, the top and bottom radii vary..
  GLfloat sweepRadiusTop;
  GLfloat sweepRadiusBottom;

  GLfloat xBottomPoint;
  GLfloat zBottomPoint;

  GLfloat xTopPoint;
  GLfloat zTopPoint;

  for( int c = 0; c < nStackedCircles; c ++ )
  {
    // Draw a circle - note that this always uses two circles - a top and bottom circle.
    GLfloat yBottomCircle;
    GLfloat yTopCircle;

    yTopCircle    = r * sin( yAngle + yAngleStep );
    yBottomCircle = r * sin( yAngle );

    std::vector<GLfloat> vBottom_x;
    std::vector<GLfloat> vBottom_z;

    std::vector<GLfloat> vTop_x;
    std::vector<GLfloat> vTop_z;

    sweepRadiusTop    = r * cos( yAngle + yAngleStep );
    sweepRadiusBottom = r * cos( yAngle );

    // Add 1 face - a triangle strip per slice..
    AddFace();

    m_Faces[ c ].m_DrawType = GL_TRIANGLE_STRIP;

    // Now work out the position of the points around each circle - bottom points will always be the
    //      same as the last top circle points.. but I'm not going to try optimising yet..
    for( int s = 0; s < nPointsPerCircle; s ++ )
    {
      GLfloat xBottomPoint = sweepRadiusBottom * sin( horizSweepAngle );
      GLfloat zBottomPoint = sweepRadiusBottom * cos( horizSweepAngle );

      GLfloat xTopPoint = sweepRadiusTop * sin( horizSweepAngle + horizSweepStep );
      GLfloat zTopPoint = sweepRadiusTop * cos( horizSweepAngle + horizSweepStep );

      vBottom_x.push_back( xBottomPoint );
      vBottom_z.push_back( zBottomPoint );

      vTop_x.push_back( xTopPoint );
      vTop_z.push_back( zTopPoint );

      horizSweepAngle += horizSweepStep;
    }

    // OPTIMISE THIS!!
    for( int s = 1; s <= nPointsPerCircle + 1; s ++ )
    {
      if( s == nPointsPerCircle + 1 )
      {
        // Join the last bottom point with the very first top point - go one more to fully close and leave no vertical gap
        xTopPoint = vTop_x[ 1 ];
        zTopPoint = vTop_z[ 1 ];

        xBottomPoint = vBottom_x[ 0 ];
        zBottomPoint = vBottom_z[ 0 ];
      }
      else
      if( s == nPointsPerCircle )
      {
        // Join the last bottom point with the very first top point
        xTopPoint = vTop_x[ 0 ];
        zTopPoint = vTop_z[ 0 ];

        xBottomPoint = vBottom_x[ s - 1 ];
        zBottomPoint = vBottom_z[ s - 1 ];
      }
      else
      {
        xTopPoint = vTop_x[ s ];
        zTopPoint = vTop_z[ s ];

        xBottomPoint = vBottom_x[ s - 1 ];
        zBottomPoint = vBottom_z[ s - 1 ];
      }

      // Calculate and add the Normal for each vertex.. Normal for a point on surface of a Sphere2 should be the unit vector going from centre
      //      of the Sphere2 to the surface (x,y,z).
      //
      //      If centre of Sphere2 is 0,0,0 then N = | {x,y,z} - {0,0,0} | = | {x,y,z} |
      glm::vec3 vNormalBottom = glm::vec3( xBottomPoint, yBottomCircle, zBottomPoint );
      vNormalBottom = glm::normalize( vNormalBottom );

      glm::vec3 vNormalTop = glm::vec3( xTopPoint, yTopCircle, zTopPoint );
      vNormalTop = glm::normalize( vNormalTop );

      // Add bottom of slice vertex..
      m_Faces[ c ].AddVertexWithNormal( xBottomPoint, yBottomCircle, zBottomPoint, vNormalBottom.x, vNormalBottom.y, vNormalBottom.z );

      // Add top of slice vertex, next step position..
      m_Faces[ c ].AddVertexWithNormal( xTopPoint, yTopCircle, zTopPoint, vNormalTop.x, vNormalTop.y, vNormalTop.z );
    }

    int nVertexCount = m_Faces[ c ].m_Vertices.size();

    m_Faces[ c ].m_SideCount = nVertexCount;

    // Face colouring colours the vertices so they need to be created first..
    m_Faces[ c ].SetRGB( m_RGBA.r, m_RGBA.g, m_RGBA.b );

    yAngle += yAngleStep;
  }
}

void CSphere2::Create( GLfloat fSize )
{
  m_Size = fSize;

  // Must add vertices first..
  AddVertices();

  glGenBuffers( 1, &m_VBO );
  glBindBuffer( GL_ARRAY_BUFFER, m_VBO );

  int nFaces = m_Faces.size();
  int nVertexCount = 0;

  for( int f = 0; f < nFaces; f ++ )
  {
    nVertexCount += m_Faces[ f ].m_Vertices.size();
    m_Faces[ f ].m_SideCount = nVertexCount;
  }

  // Define the size of the buffer.. 
  glBufferData( GL_ARRAY_BUFFER, sizeof( COLVERTEX ) * nVertexCount, NULL, GL_STATIC_DRAW );

  int nOffset = 0;

  for( int f = 0; f < nFaces; f ++ )
  {
    // Copy in each vertice's data..
    for( int v = 0; v < (int) m_Faces[ f ].m_Vertices.size(); v ++ )
    {
      glBufferSubData( GL_ARRAY_BUFFER, nOffset, sizeof( COLVERTEX ), &m_Faces[ f ].m_Vertices[ v ].m_VertexData );

      nOffset += sizeof( COLVERTEX );
    }
  }

  glBindBuffer( GL_ARRAY_BUFFER, 0 );
}

I had the same problem with other examples that I'd copied from elsewhere so I sat down, did the math myself and I still have the same problem.

Vertex shader:

char *vs3DShader  = 

"#version 140\n"

"#extension GL_ARB_explicit_attrib_location : enable\n"

"layout (location = 0) in vec3 Position;"
"layout (location = 1) in vec4 color;"
"layout (location = 2) in vec3 aNormal;"

"out vec4 frag_color;"
"out vec3 Normal;"
"out vec3 FragPos;"

"uniform mat4 model;"
"uniform mat4 view;"
"uniform mat4 projection;"

"void main()"
"{"
"  FragPos = vec3(model * vec4(Position, 1.0));"

"  gl_Position = projection * view * vec4(FragPos, 1.0);"

//  Rotate normals with respect to current Model matrix (object rotation).
"  Normal = mat3( transpose( inverse( model ) ) ) * aNormal; "

"  // Pass vertex color to fragment shader.. \n"
"  frag_color = color;"
"}"
;

Fragment shader:

char *fs3DShader  = 

"#version 140\n"
"in  vec4 frag_color;"
"in  vec3 Normal;"
"in  vec3 FragPos;"

"out vec4 FragColor;"

"uniform vec3 lightPos; "
"uniform vec3 lightColor; "

"void main()"
"{"
"  // ambient\n"
"  float ambientStrength = 0.1;"
"  vec3 ambient = ambientStrength * lightColor;"

"  // diffuse \n"
"  vec3 norm = normalize(Normal);"
"  vec3 lightDir = normalize(lightPos - FragPos);"
"  float diff = max(dot(norm, lightDir), 0.0);"
"  vec3 diffuse = diff * lightColor;"

"  vec3 result = (ambient + diffuse) * frag_color;"

"  FragColor = vec4(result, 1.0);"
"}"                         
;

Am I missing some sort of smoothing option? I have tried moving my viewpoint to both sides of the sphere and the dots are happening all around - so it isn't where the triangle strip band "closes" that's the problem - its all over the sphere.

See bright dots below:

enter image description here

Update: I just wanted to prove that the wrapping back to zero degrees isn't the problem. Below is an image when only a quarter of each circle is swept through 90 degrees. The dots are still appear in the mid regions.

enter image description here

SparkyNZ
  • 6,266
  • 7
  • 39
  • 80

2 Answers2

3

Floating point accuracy is not infinite, when working with transcendental numbers you will inevitably accumulate errors.

Here is an example program that does the same loop that your program does, except it just prints out the final angle:

#include <cmath>
#include <cstdio>

int main() {
    const int N = 72;
    const float step = std::atan(1.0f) * 8 / N;
    float x = 0.0f;
    for (int i = 0; i < N; i++) {
        x += step;
    }
    std::printf("x - 2pi = %f\n", x - 8 * std::atan(1.0f));
    return 0;
}

On my system, it prints out -0.000001. Close to zero, but not zero.

If you want two points in your mesh to line up, don't give them different values. Otherwise you get small seams like this.

A typical approach to this problem is to just generate a circle like this:

#include <cmath>
#include <cstdio>
#include <vector>

struct vec2 { float x, y; };

int main() {
    const int N = 72;
    const float step = std::atan(1.0f) * 8 / N;
    std::vector<vec2> circle;
    for (int i = 0; i < N; i++) {
        float a = i * step;
        circle.push_back({ std::cos(a), std::sin(a) });
    }
    return 0;
}

At every point in the circle, circle[i], the next point is now just circle[(i+1)%N]. This ensures that the point after circle[N-1] will always be exactly the same as circle[0].

Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
  • Thanks for your answer. I don't think its the problem though. Wouldn't what you describe only affect where the circle "joins" itself - ie - when a 360degree sweep is complete? If you have a look in my updated image, I have swept only 90degrees of the sphere and I still have dots appearing in the 'middle' of the bands. – SparkyNZ Mar 28 '18 at 18:45
  • I'm also reusing the same vertex coordinates when the top circle of a slice becomes the bottom circle of the slice above. This is what made me think it may be more of a problem with smoothing or something. In the case where the dots are brighter than the surrounding areas, wouldn't this suggest that perhaps the normals may be wrong at certain positions, or some vertices are "sticking out" too much? – SparkyNZ Mar 28 '18 at 18:46
  • I'm not convinced that `sin(x + y)` is the same as `x += y;` then calculating `sin(x)` afterwards. You can check for yourself. – Dietrich Epp Mar 28 '18 at 23:11
  • Oh I see what you mean. That makes sense. The vertical increment of angle. I thought you were referring to the horizontal sweep. Shame people downvote when asking for help. Must make them feel good putting somebody down. – SparkyNZ Mar 29 '18 at 05:16
0

I found a couple of problems with the vertex calculation in the question. Since I was calculating both bottom and top vertices every time I was sweeping around a horizontal slice there was rounding/precision error produced. A point on the top of the current slice should be the same as the bottom point on the next slice up - but I was calculating this top and bottom after incrementing as Dietrich Epp suggested. This resulted in different values. My solution was to re-use the previous top circle vertices as the bottom vertices of the next slice up.

I also hadn't calculated the x/z positions for top and bottom circles using the same sweep angle - I'd incremented the angle which I shouldn't have done.

So fundamentally, problem was caused by 2 overlapping vertices that should have had identical coordinates but were ever so slightly different.

Here's the working solution:

void CSphere2::AddVertices( void )
{
  #define SPHERE2_RES 72

  // Create sphere using horizontal slices/circles
  int nPointsPerCircle = SPHERE2_RES;
  int nStackedCircles  = SPHERE2_RES;

  GLfloat r          = m_Size;
  GLfloat yAngle     = - (PI / 2.0f);  // Start at -90deg and work up to +90deg (south to north pole)
  GLfloat yAngleStep = PI / nStackedCircles;

  // Sweep angle is zero initially for pointing towards me (-Z direction)
  GLfloat horizSweepAngle = 0;
  GLfloat horizSweepStep  = ( 2 * PI ) / nPointsPerCircle;

  // Each time we have a slice, the top and bottom radii vary..
  GLfloat sweepRadiusTop;
  GLfloat sweepRadiusBottom;

  GLfloat xBottomPoint;
  GLfloat zBottomPoint;

  GLfloat xTopPoint;
  GLfloat zTopPoint;

  std::vector<GLfloat> vCircle_x;
  std::vector<GLfloat> vCircle_z;

  std::vector<GLfloat> vLastCircle_x;
  std::vector<GLfloat> vLastCircle_z;

  int nFace = 0;

  for( int c = 0; c <= nStackedCircles + 1; c ++ )
  {
    // Draw a circle - note that this always uses two circles - a top and bottom circle.
    GLfloat yBottomCircle;
    GLfloat yTopCircle;

    yTopCircle    = r * sin( yAngle + yAngleStep );
    yBottomCircle = r * sin( yAngle );

    sweepRadiusTop = r * cos( yAngle );

    GLfloat xCirclePoint;
    GLfloat zCirclePoint;

    horizSweepAngle = 0;

    vCircle_x.clear();
    vCircle_z.clear();

    // Now work out the position of the points around each circle - bottom points will always be the
    //      same as the last top circle points.. 
    for( int s = 0; s < nPointsPerCircle; s ++ )
    {
      zCirclePoint = sweepRadiusTop * sin( horizSweepAngle );
      xCirclePoint = sweepRadiusTop * cos( horizSweepAngle );

      vCircle_x.push_back( xCirclePoint );
      vCircle_z.push_back( zCirclePoint );

      horizSweepAngle += horizSweepStep;
    }

    if( c == 0 )
    {
      // First time around there is no last circle, so just use the same points..
      vLastCircle_x = vCircle_x;
      vLastCircle_z = vCircle_z;

      // And don't add vertices until next time..
      continue;
    }

    // Add 1 face - a triangle strip per slice..
    AddFace();

    m_Faces[ nFace ].m_DrawType = GL_TRIANGLE_STRIP;

    for( int s = 1; s <= nPointsPerCircle + 1; s ++ )
    {
      if( s == nPointsPerCircle + 1 )
      {
        // Join the last bottom point with the very first top point
        xTopPoint = vCircle_x[ 1 ];
        zTopPoint = vCircle_z[ 1 ];

        xBottomPoint = vLastCircle_x[ 0 ];
        zBottomPoint = vLastCircle_z[ 0 ];
      }
      else
      if( s == nPointsPerCircle )
      {
        // Join the last bottom point with the very first top point
        xTopPoint = vCircle_x[ 0 ];
        zTopPoint = vCircle_z[ 0 ];

        xBottomPoint = vLastCircle_x[ s - 1 ];
        zBottomPoint = vLastCircle_z[ s - 1 ];
      }
      else
      {
        xTopPoint = vCircle_x[ s ];
        zTopPoint = vCircle_z[ s ];

        xBottomPoint = vLastCircle_x[ s - 1 ];
        zBottomPoint = vLastCircle_z[ s - 1 ];
      }

      // Calculate and add the Normal for each vertex.. Normal for a point on surface of a Sphere2 should be the unit vector going from centre
      //      of the Sphere2 to the surface (x,y,z).
      //
      //      If centre of Sphere2 is 0,0,0 then N = | {x,y,z} - {0,0,0} | = | {x,y,z} |
      glm::vec3 vNormalBottom = glm::vec3( xBottomPoint, yBottomCircle, zBottomPoint );
      vNormalBottom = glm::normalize( vNormalBottom );

      glm::vec3 vNormalTop = glm::vec3( xTopPoint, yTopCircle, zTopPoint );
      vNormalTop = glm::normalize( vNormalTop );

      // Add bottom of slice vertex..
      m_Faces[ nFace ].AddVertexWithNormal( xBottomPoint, yBottomCircle, zBottomPoint, vNormalBottom.x, vNormalBottom.y, vNormalBottom.z );

      // Add top of slice vertex, next step position..
      m_Faces[ nFace ].AddVertexWithNormal( xTopPoint, yTopCircle, zTopPoint, vNormalTop.x, vNormalTop.y, vNormalTop.z );
    }

    // Now copy the current circle x/y positions as the last circle positions (bottom circle)..
    vLastCircle_x = vCircle_x;
    vLastCircle_z = vCircle_z;

    int nVertexCount = m_Faces[ nFace ].m_Vertices.size();

    m_Faces[ nFace ].m_SideCount = nVertexCount;

    // Face colouring colours the vertices so they need to be created first..
    m_Faces[ nFace ].SetRGB( m_RGBA.r, m_RGBA.g, m_RGBA.b );

    yAngle += yAngleStep;

    nFace ++;
  }
}
SparkyNZ
  • 6,266
  • 7
  • 39
  • 80