Search code examples
c++openglopengl-es-2.0fragment-shadervertex-shader

UV Sphere - Getting Rid of join/vertex "dots"


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


Solution

  • 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 ++;
      }
    }