球面三角網格繪製演算法(附OpenGL程式碼)
技術標籤:筆記
本文給出的程式碼都是在原點處、半徑為1的球面繪製程式碼。
對於以任意點為球心、任意長度為半徑的情況,可以通過使用glTranslated實現球的平移、glScaled實現球的縮放。
編筐法
如圖所示,球的繪製是一層一層完成的,如同編一個竹筐。
將球分為N層,所以共N+1條緯線,除了最頂上和最底下的兩個緯線以外(因為已經退化為一個點),每條緯線均分M等份,相鄰緯線交錯均分,該處的均分指的是角度均分。
如果只是要繪製如圖所示的線框球,將每一層上的兩個點與下一層的對應的點用三角形連線起來即可;如果要繪製完整球面,則還需要反向將下一層的每兩個點與本層連線。
儘管M的取值與N無關,但還是建議取M=2N,這樣球體在緯度和經度方向上均勻程度相同。
下面是N=3和N=20的效果:
N=5 | N=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();
}
}