UV 球体 - 摆脱 join/vertex "dots"

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

我正在创建一个 UV 球体(类似于将地球划分为纬度线)。我通过以下方式执行此操作:

  1. 计算每个平行纬度圆周围的所有顶点(例如每个圆 72 个点)
  2. 使用GL_TRIANGLE_STRIP填充每个纬度圈之间的每个"slice"。

不幸的是,我一直在完美的球体上看到圆点。

什么会导致这种情况,我该如何摆脱它?

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 );
}

我从其他地方复制的其他例子也有同样的问题,所以我坐下来,自己算了一下,但我仍然有同样的问题。

顶点着色器:

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;"
"}"
;

片段着色器:

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);"
"}"                         
;

我是否缺少某种平滑选项?我已经尝试将我的视点移动到球体的两侧,并且点在周围发生 - 所以它不是三角形带 "closes" 的问题 - 它遍布球体。

见下方亮点:

更新:我只是想证明回到零度不是问题。下图是每个圆的四分之一扫过 90 度时的图像。这些点仍然出现在中间区域。

浮点精度不是无限的,在处理超越数时不可避免地会累积误差。

这是一个示例程序,它执行与您的程序相同的循环,只是它只打印出最终角度:

#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;
}

在我的系统上,它打印出 -0.000001。接近于零,但不为零。

如果您希望网格中的两个点对齐,请不要给它们不同的值。否则你会得到像这样的小接缝。

解决这个问题的典型方法是像这样生成一个圆:

#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;
}

圆圈中的每个点 circle[i],下一个点现在只是 circle[(i+1)%N]。这确保 circle[N-1] 之后的点将始终与 circle[0].

完全相同

我发现问题中的顶点计算存在一些问题。由于我每次扫过水平切片时都会计算底部和顶部顶点,因此会产生 rounding/precision 错误。当前切片顶部的一个点应该与下一个切片的底部点相同——但我是在按照 Dietrich Epp 的建议递增后计算这个顶部和底部的。这导致了不同的值。我的解决方案是 re-use 之前的顶部圆顶点作为下一个切片的底部顶点。

我也没有使用相同的扫描角度计算顶部和底部圆的 x/z 位置 - 我增加了我不应该做的角度。

所以从根本上说,问题是由 2 个重叠的顶点引起的,这两个顶点应该具有相同的坐标,但它们却略有不同。

这是可行的解决方案:

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