1. 程式人生 > 其它 >球面三角網格繪製演算法(附OpenGL程式碼)

球面三角網格繪製演算法(附OpenGL程式碼)

技術標籤:筆記

本文給出的程式碼都是在原點處、半徑為1的球面繪製程式碼。

對於以任意點為球心、任意長度為半徑的情況,可以通過使用glTranslated實現球的平移、glScaled實現球的縮放。

編筐法


如圖所示,球的繪製是一層一層完成的,如同編一個竹筐。

將球分為N層,所以共N+1條緯線,除了最頂上和最底下的兩個緯線以外(因為已經退化為一個點),每條緯線均分M等份,相鄰緯線交錯均分,該處的均分指的是角度均分。

如果只是要繪製如圖所示的線框球,將每一層上的兩個點與下一層的對應的點用三角形連線起來即可;如果要繪製完整球面,則還需要反向將下一層的每兩個點與本層連線。

儘管M的取值與N無關,但還是建議取M=2N,這樣球體在緯度和經度方向上均勻程度相同。

下面是N=3和N=20的效果:

N=5N=20

該方法的優點是簡單,缺點是圖元的分佈不均勻,兩極的圖元密度比赤道的密度大很多。

下面是完整程式碼:

/**
 * @brief 扎籃子法
 * @param resolution 解析度
 */
void
DrawSphere_1(GLuint resolution)
{
  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

  GLdouble dTheta = kPi / resolution;

  struct XY
  {
    GLdouble x, y;
  };
  auto a = new XY[
2 * resolution + 1]; auto b = new XY[2 * resolution + 1]; GLdouble thetaZ = dTheta; // 緯轉角 GLdouble thetaXY = 0; // 經轉角 GLdouble az = cos(thetaZ); // a緯面高 GLdouble bz; // b緯面高 GLdouble rr = sin(thetaZ); // 緯半徑 // 繪製頂蓋 glBegin(GL_TRIANGLE_FAN); glVertex3d(0, 0, 1); for (auto
i = 0U; i < 2 * resolution; ++i) { a[i].x = cos(thetaXY) * rr; a[i].y = sin(thetaXY) * rr; thetaXY += dTheta; glVertex3d(a[i].x, a[i].y, az); } a[2 * resolution] = a[0]; glVertex3d(a[2 * resolution].x, a[2 * resolution].y, az); // 封閉緯面 glEnd(); // 繪製中間層 for (auto i = 1U; i < resolution - 1; ++i) { thetaZ += dTheta; bz = cos(thetaZ) * 1; rr = sin(thetaZ) * 1; thetaXY = dTheta / 2 * i; glBegin(GL_TRIANGLES); for (auto j = 0U; j < 2 * resolution; ++j) { b[j].x = cos(thetaXY) * rr; b[j].y = sin(thetaXY) * rr; thetaXY += dTheta; glVertex3d(a[j].x, a[j].y, az); glVertex3d(a[j + 1].x, a[j + 1].y, az); glVertex3d(b[j].x, b[j].y, bz); } b[2 * resolution] = b[0]; glEnd(); auto tmp = a; a = b; b = tmp; az = bz; } // 繪製底蓋 glBegin(GL_TRIANGLE_FAN); glVertex3d(0, 0, -1); for (auto i = 0U; i < 2 * resolution; ++i) { glVertex3d(a[i].x, a[i].y, az); } glVertex3d(a[2 * resolution].x, a[2 * resolution].y, az); // 封閉緯面 glEnd(); delete[] a; delete[] b; }

面細分法


如上圖所示,通過將原來的三角面劃分為更細小的面即可實現對球面的逼近。

該演算法的核心是細分操作,一次細分操作將一個三角面劃分為四個更小的三角面。給定一個頂點都在球面上的三個點a、b、c,依次求出角aob、boc、coa的平分線與球面的交點d、e、f,則新的四個三角形為:afd、bde、cef、def。

細分的起點可以是四面體、八面體、十二面體等等。從理論上說,任何一個封閉的、由三角形構成的凸多面體都可以收斂為一個球,但是建議採用八面體作為細分的起點,因為八面體的頂點座標非常簡單,且由於其對稱性,只需計算第一象限的點,然後做映象變換即可得到整個球。示例圖片就是第一象限的細分過程。

細分的終點可以是多種多樣的,例如,可以當三角面的面積小於某個給定值時就不再細分,也可以當三角形的邊長不長於某個給定值時就不再細分,還可以直接指定細分的次數。

該方法的一個好處就是可以得到較均勻的球面近似結果,例如,如果約束條件為三角形的邊長不大於X,那麼最終結果繪製的線端之間的長度之差不會超過X/2,使用面積也同理。

一個較高精細度下的效果,這種方法繪製出的球體極性不明顯,看著有點暈:
在這裡插入圖片描述
下面的函式midArcPoint計算角平分線與球面的交點(也即球面上弧的中點):

struct XYZ
{
  GLdouble x, y, z;
};

XYZ
midArcPoint(const XYZ& a, const XYZ& b)
{
  XYZ c{ a.x + b.x, a.y + b.y, a.z + b.z };
  GLdouble mod = sqrt(c.x * c.x + c.y * c.y + c.z * c.z);
  c.x /= mod, c.y /= mod, c.z /= mod;
  return c;
}

下面是一種繪製程式碼:

#include <queue>

GLdouble
distSquare(const XYZ& a, const XYZ& b)
{
  GLdouble dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
  return dx * dx + dy * dy + dz * dz;
}

/**
 * @brief 面細分法
 * @param resolution 解析度
 */
void
DrawSphere_2(GLdouble resolution)
{
  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

  struct Triangle
  {
    XYZ a, b, c;
  };

  std::queue<Triangle> triangles;
  triangles.push({ 1, 0, 0, 0, 1, 0, 0, 0, 1 });

  resolution *= resolution;
  while (!triangles.empty()) {
    auto& t = triangles.front();

    // 當三角形各邊長度都不大於resolution時就不再進一步細分
    if (distSquare(t.a, t.b) > resolution ||
        distSquare(t.b, t.c) > resolution ||
        distSquare(t.c, t.a) > resolution) {
      auto d = midArcPoint(t.a, t.b), e = midArcPoint(t.b, t.c),
           f = midArcPoint(t.c, t.a);
      triangles.push({ t.a, f, d });
      triangles.push({ t.b, d, e });
      triangles.push({ t.c, e, f });
      triangles.push({ d, e, f });
    } else {
      glBegin(GL_TRIANGLES);

      // 第一象限
      glVertex3dv((GLdouble*)&t.a);
      glVertex3dv((GLdouble*)&t.b);
      glVertex3dv((GLdouble*)&t.c);

      // 第二象限
      t.a.x = -t.a.x, t.b.x = -t.b.x, t.c.x = -t.c.x;
      glVertex3dv((GLdouble*)&t.a);
      glVertex3dv((GLdouble*)&t.b);
      glVertex3dv((GLdouble*)&t.c);

      // 第三象限
      t.a.y = -t.a.y, t.b.y = -t.b.y, t.c.y = -t.c.y;
      glVertex3dv((GLdouble*)&t.a);
      glVertex3dv((GLdouble*)&t.b);
      glVertex3dv((GLdouble*)&t.c);

      // 第四象限
      t.a.x = -t.a.x, t.b.x = -t.b.x, t.c.x = -t.c.x;
      glVertex3dv((GLdouble*)&t.a);
      glVertex3dv((GLdouble*)&t.b);
      glVertex3dv((GLdouble*)&t.c);

      // 第五象限
      t.a.z = -t.a.z, t.b.z = -t.b.z, t.c.z = -t.c.z;
      glVertex3dv((GLdouble*)&t.a);
      glVertex3dv((GLdouble*)&t.b);
      glVertex3dv((GLdouble*)&t.c);

      // 第六象限
      t.a.x = -t.a.x, t.b.x = -t.b.x, t.c.x = -t.c.x;
      glVertex3dv((GLdouble*)&t.a);
      glVertex3dv((GLdouble*)&t.b);
      glVertex3dv((GLdouble*)&t.c);

      // 第七象限
      t.a.y = -t.a.y, t.b.y = -t.b.y, t.c.y = -t.c.y;
      glVertex3dv((GLdouble*)&t.a);
      glVertex3dv((GLdouble*)&t.b);
      glVertex3dv((GLdouble*)&t.c);

      // 第八象限
      t.a.x = -t.a.x, t.b.x = -t.b.x, t.c.x = -t.c.x;
      glVertex3dv((GLdouble*)&t.a);
      glVertex3dv((GLdouble*)&t.b);
      glVertex3dv((GLdouble*)&t.c);
      glEnd();
    }
    triangles.pop();
  }
}