[index]

10. 曲面の表現

10.1 OpenGL における曲面表現の概要

 グラフィックスライブラリなどを使って複雑な形状を表現する場合, 一番低いレベルでは,グラフィックスハードウェアは, 点,線分,三角形や四角形の多角形を描きます. 滑らかな曲線や曲面は,小さな線分や多角形を非常に多数用いて描きます. このとき, あらかじめ用意した頂点の法線ベクトルの情報と頂点のつなぎ方の情報を用いて, 多数の線分や多角形を描いて複雑な形状を描画するのでは, 精度が固定された近似であることに加え, 保存すべきデータ量も多くなり,あまり得策と言えません.
 一方,Bezier(ベジエ)曲面や NURBS(ナーブス;Non-Uniform Rational B-Spline;非一様有理Bスプライン)曲面のような いくつかの制御点から曲面を定義する表現方法を用いると, 描画するときに,実際に必要な小さな図形が計算されることになり, 正確な曲面を表現できるのに加え,保存しておくデータ量が少なくてすみます.

OpenGL では,制御点をもとに, 曲線や曲面に点を設定するためのエバリュエータを用意しています.
エバリュエータは 基底ベジェ(またはBernstein:ベルンシュタイン) に基づくスプライン曲線や曲面を作成します. 法線ベクトルも自動的に計算され, それらの曲線や曲面は任意の精度でレンダリングできます.
ベジエ,Bスプライン,NURBS の各曲線&曲面など, 現在一般的に利用されているパラメトリック曲面を描くことができます. しかしながら, エバリュエータは曲線や曲面の点の低レベルな描画しか実行しないため, 通常は高レベルなインタフェースを提供するライブラリを介して利用します.

[index]


10.2 Bezier 曲線

ベジュ曲線は1つの変数のベクトル値の関数です。
C(u) = [ X(u), Y(u), Z(u)]
uは領域によって変化します([0. 1]など)。

ベジュ曲面は2つの変数のベクトル値の関数です。
S(u, v) = [ X(u, v), Y(u, v), Z(u,v)]
u, vは領域によって両方変化します。その範囲は必ずしも3次元である必要がありません。

各々のuに対して(曲面の場合はu,とv)、C関数(またはS関数)の式は曲線(または曲面)上の点を計算します。
エバリュエータを使用する場合は、最初に関数C(または関数関数S)を定義し、それを有効化し、glVertex*()の代わりにglEvalCoord1()(またはglEvalCoord2())を使用します。


10.3 1次元エバリュエータ

それでは,OpenGL の1次元エバリュエータを用いて,4つの制御点を使用した3次ベジエ曲線を描いてみましょう. ファイル名を bezcurve.c として,最初から書いて下さい (曲線の様子がよくわかるように,透視投影ではなく,平行投影になっていて, 照明処理も設定していません).

/* bezcurve.c */
/* 1次元エバリュエータを使ってベジエ曲線を描く */
#include <GL/glut.h>
#include <stdlib.h>

GLfloat ctrlpoints[4][3] = {
  {-4.0,-4.0, 0.0},
  {-2.0, 4.0, 0.0},
  { 2.0,-4.0, 0.0},
  { 4.0, 4.0, 0.0} 
};

GLfloat white[] = {1.0, 1.0, 1.0, 1.0};
GLfloat yellow[] = {1.0, 1.0, 0.0, 1.0};

void display(void)
{
  int i;

  glClear(GL_COLOR_BUFFER_BIT);

  glColor4fv(&white[0]);
  glBegin(GL_LINE_STRIP);
  for (i = 0; i <= 30; i++) 
      glEvalCoord1f((GLfloat) i/30.0);
  glEnd();

  /* 制御点を描く */
  glPointSize(5.0);
  glColor4fv(&yellow[0]);
  glBegin(GL_POINTS);
  for (i = 0; i < 4; i++) 
  glVertex3fv(&ctrlpoints[i][0]);
  glEnd();

  glFlush();
}

void resize(int w, int h)
{
  glViewport(0, 0, (GLsizei) w, (GLsizei) h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  if (w <= h)
    glOrtho(-5.0, 5.0, -5.0*(GLfloat)h/(GLfloat)w, 
            5.0*(GLfloat)h/(GLfloat)w, -5.0, 5.0);
  else
    glOrtho(-5.0*(GLfloat)w/(GLfloat)h, 
            5.0*(GLfloat)w/(GLfloat)h, -5.0, 5.0, -5.0, 5.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}

void keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case '\33':
  case 'q':
  case 'Q':
    exit(0);
    break;
  default :
    break;
  }
}

void init(void)
{
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glMap1f(GL_MAP1_VERTEX_3, 0.0, 1.0, 3, 4, &ctrlpoints[0][0]);
  glEnable(GL_MAP1_VERTEX_3);
}

int main(int argc, char** argv)
{
  /* 初期化 */
  glutInit(&argc, argv);

  /* ウィンドウの生成 */
  glutInitDisplayMode(GLUT_RGBA | GLUT_SINGLE | GLUT_DEPTH);
  glutInitWindowPosition(200, 50);
  glutInitWindowSize(300, 300);
  glutCreateWindow(argv[0]);

  /* OpenGL 初期化ルーチンの呼出し */
  init();

  /* 描画ルーチンの設定 */
  glutDisplayFunc(display);
  glutReshapeFunc(resize);

  /* 入力処理ルーチンの設定 */
  glutKeyboardFunc(keyboard);

  /* 無限ループ */
  glutMainLoop();

  return 0;
}

void glMap1f( GLenum target, GLfloat u1, GLfloat u2, GLint stride, GLint order, const GLfloat *points )
1次元エバリュエータを定義します.GLdouble 型の glMap1d もあります.
targetで,エバリュエータで生成する値の種類を指定します. GL_MAP1_VERTEX_3(頂点座標 x, y, z), GL_MAP1_VERTEX_4(頂点座標 x, y, z, w), GL_MAP1_INDEX(カラー指標), GL_MAP1_COLOR_4(R, G, B, A), GL_MAP1_NORMAL(法線座標), GL_MAP1_TEXTURE_COORD_1〜4(テクスチャ座標) というシンボル定数が有効です.値を glEnable や glDisable に指定して, エバリュエータの有効/無効を設定します.
u1, u2では, glEvalCoord1 で使用する変数 u の範囲を設定します. u1 = 0.0, u2 = 1.0 と設定すると,一方の端から他方の端までとなります.
strideでは,points で参照されるデータ構造中において, 制御点の座標をみていく間隔,すなわち, いくつの float ごとに制御点の座標の始点があるかを指定します. これにより,制御点は任意のデータ構造を持つことができます.
orderでは,次数+1,すなわち,制御点の数を指定します.
pointsでは,制御点の配列のポインタを指定します.
void glEvalCoord1f( GLfloat u )
1次元の写像を計算します.GLdouble 型の glEvalCoord1d もあります. glMap1 で指定した基底関数で算出する領域の座標 u を指定します.
void glPointSize( GLfloat size )
描画する点の直径を指定します.初期値は 1.0 です.

glMap1 で1次元エバリュエータを定義し,glEnable で有効にすると, Bernstein 基底関数に基づき,曲線上の点を計算してくれます. 曲線は,glBegin(GL_LINE_STRIP) と glEnd() の間で描かれます. エバリュエータを実行すると,glEvalCoord1f() コマンドは, 入力パラメータ u に対応する曲線の頂点座標で, glVertex() を実行する場合と同様の結果になります. ここでの設定では,1本のベジエ曲線を30本の線分に分けて描いています.

このプログラムでは,N字のような配置で制御点を指定しています. 10.2 節の最後の図にあるようないくつかの配置で制御点を指定し, どのように曲線が描かれるか,確認してみましょう. また,glEvalCoord1f() の引数を変更して描画し, 曲線の精度が変化するのを確認してください. たとえば,1本のベジエ曲線を5本の線分で描く設定にすると, 折れ線として描かれているのがよくわかります. 逆に,50本の線分で描く設定にすると,全画面表示にしても,きれいな曲線に見えますね.

[index]

/************************************************************************************/
1次元エバリュエータの定義と算出

CGでよく用いられるパラメトリック曲線に,3次ベジエ曲線があります. 3次ベジエ曲線は,4つの制御点 (control point) によって定義されます. 制御点は,曲線の両端点と概形を定めます. 通常,パラメータ区間 [0, 1] において定義される多項式曲線で表現されます.

上の図のように, 4つの制御点 , , , (黒い点)から曲線を作る場合, まず, に着目して,t 対 (1-t) の内分点を求めます. このとき,t は 0 ≦ t ≦ 1 の範囲の値をとります. これを とします. 同様に , に対して t 対 (1-t) の内分点 を, に対して t 対 (1-t) の内分点 をそれぞれ求めます. さらに,, を同じ比率で内分して , 同様に , を同じ比率で内分して を求めます. を t 対 (1-t) の内分すると, 点 が1つだけ求められます. この過程を式で表すと,

ですから,点 P は,

となります.この曲線式を変形して,

とすると,重み関数がそれぞれ

と表されます. これは,Bernstein(バーンスタイン)基底関数と呼ばれ,一般形は,

と表されます.添字の i は制御点の番号,n は曲線の次数を示しています. 係数 は n 個の中から i 個を取り出すときの組み合わせの数を示しています.ということで, Bernstein (ベルンシュタイン)基底関数は,

を2項展開したときに得られる各項を示しています. なお,3次の Bernstein (ベルンシュタイン)基底関数グラフは,次のグラフのようになります.

このグラフから,ベジエ曲線の両端点が, 両端の制御点に一致することがよくわかります. また,次の図の3次ベジエ曲線の例からわかるように, ベジエ曲線の両端点の接ベクトルは, 端の制御点とその1つ隣の制御点とを結んだ向きになります.

n 次のベジエ曲線は,n+1 個の制御点に n 次の Bernstein 基底関数 で重みづけをした,次式で表されます.

Bernstein(ベルンシュタイン) 基底関数は,曲線の形状には無関係で, 形状に影響するのは制御点です. 制御点を動かして,曲線の形状を変形していきます.

なお,一般にパラメトリック曲線では, 適当なパラメータ区間に対応する区間曲線(セグメント)を扱い, 複雑な曲線形状を扱う場合には, 複数のセグメントをつなぎ合わせて1本の曲線を表現します.

/************************************************************************************/

[index]


10.4 Bezier 曲面

ベジエ曲面は,(n+1)行 (m+1)列に格子状に並べた制御点 によって定義する曲面です.これらの制御点のうち,, , , の4点が四隅の位置を指定し, その他の点が曲面の概形を定めます. ベジエ曲面は,0≦u≦1,0≦v≦1 のパラメータ領域で定義される多項式曲面で, u に関して n 次,v に関して m 次の式となるので,n×m 次曲面と呼ばれ, 特に n=m のときには双 n 次曲面と呼ばれます.

CGでは,次数が3次までのベジエ曲面が広く用いられています. 双3次ベジエ曲面では, 次の図に示すように16個の制御点, ..., によって定められます. ベジエ曲面の四隅の位置が,制御点 , , , に一致します. 4辺は3次のベジエ曲線となります.

ベジエ曲面は,次式で与えられます.
はそれぞれ n 次と m 次の Bernstein 基底関数です.曲線のときと同様, 曲面の形状に影響を与えるのは制御点です.

[index]


10.5 2次元エバリュエータ

2次元エバリュエータを使って,ベジエ曲面を表現してみましょう. 1次元では u だけだったものが, 2次元では u, v という2つのパラメータをとることになりますが, その他のことは全く同じです. これは,ベジエ曲線の式とベジエ曲面の式の比較からも簡単に予想できますね.

それでは,ワイヤーフレームのベジエ曲面を描いてみましょう. bezcurve.c を bezsurf.c と名前をかえて保存し,太字のところを書換えてください.

/* bezsurf.c: */
/* 2次元エバリュエータを使ってベジエ曲面を描く */
#include <GL/glut.h>
#include <stdlib.h>

GLfloat ctrlpoints[4][4][3] = {
  {{-1.5,-1.5, 4.0}, 
   {-0.5,-1.5, 2.0}, 
   { 0.5,-1.5,-1.0}, 
   { 1.5,-1.5, 2.0}}, 
  {{-1.5,-0.5, 1.0}, 
   {-0.5,-0.5, 3.0}, 
   { 0.5,-0.5, 0.0}, 
   { 1.5,-0.5,-1.0}}, 
  {{-1.5, 0.5, 4.0}, 
   {-0.5, 0.5, 0.0}, 
   { 0.5, 0.5, 3.0}, 
   { 1.5, 0.5, 4.0}}, 
  {{-1.5, 1.5,-2.0}, 
   {-0.5, 1.5,-2.0}, 
   { 0.5, 1.5, 0.0}, 
   { 1.5, 1.5,-1.0}}
};

GLfloat white[] = {1.0, 1.0, 1.0, 1.0};
GLfloat yellow[] = {1.0, 1.0, 0.0, 1.0};

void display(void)
{
  int i, j;

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glColor4fv(&white[0]);
  for (j = 0; j <= 8; j++){
    glBegin(GL_LINE_STRIP);
    for (i = 0; i <= 30; i++)
      glEvalCoord2f((GLfloat)i/30.0, (GLfloat)j/8.0);
    glEnd();
    glBegin(GL_LINE_STRIP);
    for (i = 0; i <= 30; i++)
      glEvalCoord2f((GLfloat)j/8.0, (GLfloat)i/30.0);
    glEnd();
  }

  /* 制御点を描く */
  glPointSize(5.0);
  glColor4fv(&yellow[0]);
  glBegin(GL_POINTS);
  for (j = 0; j < 4; j++) 
    for (i = 0; i < 4; i++) 
      glVertex3fv(&ctrlpoints[j][i][0]);
  glEnd();

  glFlush();
}

void resize(int w, int h)
{
  glViewport(0, 0, (GLsizei) w, (GLsizei) h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  if (w <= h)
    glOrtho(-4.0, 4.0, -4.0*(GLfloat)h/(GLfloat)w, 
	    4.0*(GLfloat)h/(GLfloat)w, -4.0, 4.0);
  else
    glOrtho(-4.0*(GLfloat)w/(GLfloat)h, 
            4.0*(GLfloat)w/(GLfloat)h, -4.0, 4.0, -4.0, 4.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}

void keyboard(unsigned char key, int x, int y)
{
  /* 変更なし */
}

void init(void)
{
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4,
          0, 1, 12, 4, &ctrlpoints[0][0][0]);
  glEnable(GL_MAP2_VERTEX_3);
  glEnable(GL_DEPTH_TEST);
}

int main(int argc, char** argv)
{
  /* 変更なし */
}

void glMap2f( GLenum target, GLfloat u1, GLfloat u2, GLint ustride, GLint uorder, GLfloat v1, GLfloat v2, GLint vstride, GLint vorder, const GLfloat *points )
2次元エバリュエータを定義します.GLdouble 型の glMap2d もあります.
targetで,エバリュエータで生成する値の種類を指定します. GL_MAP2_VERTEX_3(頂点座標 x, y, z), GL_MAP2_VERTEX_4(頂点座標 x, y, z, w), GL_MAP2_INDEX(カラー指標), GL_MAP2_COLOR_4(R, G, B, A), GL_MAP2_NORMAL(法線座標), GL_MAP2_TEXTURE_COORD_1〜4(テクスチャ座標) というシンボル定数が有効です.値を glEnable や glDisable に指定して, エバリュエータの有効/無効を設定します.
u1, u2では,変数 u の範囲を設定します.
ustrideでは,points で参照されるデータ構造中において, 制御点 P_ij の始まりと制御点 P_(i+1)j の始まりとの間にある float の数を指定します.
uorderでは,u 座標軸の制御点配列の大きさを指定します.
v1, v2では,変数 v の範囲を設定します.
vstrideでは,points で参照されるデータ構造中において, 制御点 P_ij の始まりと制御点 P_i(j+1) の始まりとの間にある float の数を指定します.
vorderでは,v 座標軸の制御点配列の大きさを指定します.
pointsでは,制御点の配列のポインタを指定します.

メモリ上では,制御点配列を表すために float の箱がだらだらと並んでいる状態なので, (x, y, z) のまとまりごと,そして,行列の大きさに合うように値を取出すために, 何個の float の箱ごとに値を参照していけばよいか,を指定します. このプログラムでは,ustride を 3,vstride を 12 と指定していますが, それは,下の図のような意味となります.

void glEvalCoord2f( GLfloat u, GLfloat v )
2次元の写像を計算します.GLdouble 型の glEvalCoord2d もあります. glMap2 で指定した基底関数で算出する領域の座標 u, v を指定します.

このプログラムでは,白い正方格子と黄色い点が表示されているだけですね. ベジエ曲面がベジエ曲面らしくみえるように,

glRotatef(85.0, 1.0, 1.0, 1.0);
をプログラムの適当なところへ追加して,別の方向から曲面を眺めてみましょう.
→ 回転 →

平行投影ではなくて,透視投影してみると, 正面からみても正方格子ではなく,歪んでレンダリングされます. 上のプログラムを透視投影に書き換えてみましょう.

平行投影 透視投影
この画像は u 方向と v 方向とがよくわかるよう,制御点の色をかえてあります. u 方向は横方向,v 方向は縦方向です. また,一回転するように,書き換えてみましょう. → 参考プログラム

では次に,ワイヤーフレームではなくて,面をはって,照明処理も施しましょう. bezsurf.c を bezmesh.c と名前をかえて保存し,次の変更を加えてください.

/* bezmesh.c: */
/* 2次元エバリュエータを使ってベジエ曲面を描く*/
#include <GL/glut.h>
#include <stdlib.h>

GLfloat ctrlpoints[4][4][3] = {
/* 変更なし */
}

void initlights(void)
{
  GLfloat ambient[] = {0.2, 0.2, 0.2, 1.0};
  GLfloat position0[] = {4.0, 0.0, 6.0, 1.0};
  GLfloat mat_diffuse[] = {0.6, 0.6, 0.6, 1.0};
  GLfloat mat_specular[] = {1.0, 1.0, 1.0, 1.0};
  GLfloat mat_shininess[] = {50.0};

  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
  glLightfv(GL_LIGHT0, GL_POSITION, position0);

  glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
  glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
  glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
}

void display(void)
{
  static int r = 0;     /* 回転角 */
  static int adjust_r = 5;

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glPushMatrix();
  glRotatef((float)r/adjust_r, 0.2, 1.0, 0.0);
  glEvalMesh2(GL_FILL, 0, 20, 0, 20);
  glPopMatrix();
  glutSwapBuffers();

  /* 回転の制御 */
  if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) {
    if (r++ >= 360*adjust_r) {
      r = 0;
      glutIdleFunc(0);
    }
  }
}

void idle(void)
{
  glutPostRedisplay();
}

void resize(int w, int h)
{
  /* 平行投影のプログラムと変更なし */
}

void keyboard(unsigned char key, int x, int y)
{
  /* 変更なし */
}

void mouse(int button, int state, int x, int y)
{
  switch (button) {
  case GLUT_LEFT_BUTTON:
    if (state == GLUT_UP) glutIdleFunc(idle);
    break;
  case GLUT_MIDDLE_BUTTON:
    if (state == GLUT_UP) glutPostRedisplay();
    break;
  case GLUT_RIGHT_BUTTON:
    if (state == GLUT_UP) glutIdleFunc(NULL);
    break;
  default:
    break;
  }
}

void init(void)
{
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4,
          0, 1, 12, 4, &ctrlpoints[0][0][0]);
  glEnable(GL_MAP2_VERTEX_3);
  glEnable(GL_AUTO_NORMAL);
  glMapGrid2f(30, 0.0, 1.0, 30, 0.0, 1.0);
  initlights();
  glEnable(GL_DEPTH_TEST);
}

int main(int argc, char** argv)
{
  /* 初期化 */
  glutInit(&argc, argv);

  /* ウィンドウの生成 */
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowPosition(200, 50);
  glutInitWindowSize(300, 300);
  glutCreateWindow(argv[0]);

  /* OpenGL 初期化ルーチンの呼出し */
  init();

  /* 描画ルーチンの設定 */
  glutDisplayFunc(display);
  glutReshapeFunc(resize);

  /* 入力処理ルーチンの設定 */
  glutKeyboardFunc(keyboard);
  glutMouseFunc(mouse);

  /* 無限ループ */
  glutMainLoop();

  return 0;
}

void glMapGrid2f( GLint un, GLfloat u1, GLfloat u2, GLint vn, GLfloat v1, GLfloat v2 )
均等な un 間隔で u1 から u2 へ, 均等な vn 間隔で v1 から v2 への2次元メッシュを定義します.
void glEvalMesh2( GLenum mode, GLint i1, GLint i2, GLint j1, GLint j2 )
点,線,多角形の2次元メッシュを計算します. mode には, GL_POINT,GL_LINE,GL_FILL のいずれかを設定し, 何を計算するのか指定します. i1, i2, j1, j2グリッドの領域変数 i または j の先頭と末尾を整数値で指定します.

mode が GL_FILL の場合,glEvalMesh2 の実行は以下と等しくなります.

for (j = j1; j < j2; j++){
  glBegin(GL_QUAD_STRIP);
  for (i = i1; i < i2; i++){
    glEbalCoord2(i*u_d + u1, j*v_d+v1);
    glEbalCoord2(i*u_d + u1, (j+1)*v_d+v1);
  }
  glEnd();     
}
ここで,u_d = (u2-u1)/un,v_d = (v2-v1)/vn です.u1, u2, un, v1, v2, vn は glMapGrid2 の引数です.
void glEnable(GLenum cap)
すでに何度も登場している glEnable ですが, このプログラムで用いているように,引数に GL_AUTO_NORMAL を指定すると, glMap2 で GL_MAP2_VERTEX_3 か GL_MAP2_VERTEX_4 で頂点を生成する際に平面の法線ベクトルを計算します.

glMapGrid と glEvalMesh を組み合わせて使用すると, 一連の等間隔写像領域の生成,算出を効果的に実行できます.

[index]


10.6 NURBS 曲面の描画

この演習では,NURBS 曲面について,詳しく学ぶことは目的としません. NURBS 曲面は,有理ベジエ曲面とBスプライン曲面の性質を併せもっていて, 工業製品の設計に広く使われている曲面であることをふまえつつ, OpenGL における表現方法を学びましょう.


[参考]

有理ベジエ曲面は,制御点 P_ij と,各制御点に対する重み w_ij によって定義される曲面で,次の式で与えられます. 基底関数は Bernstein 基底関数です.

また, Bスプライン曲面は,制御点 P_ij と,u 方向とv 方向のノットベクトル u_i,v_i によって定義される曲面です.ノットベクトルは, 混ぜ合わせ関数(基底関数)の有効範囲を設定するのに使われます. 基底関数は,ノットベクトルを用いて定義されるBスプライン基底関数です. L×K 個のパッチからなる n×m 次のBスプライン曲面は, , において定義され,次の式で与えられます.

NURBS(非一様有理Bスプライン)曲面は,制御点 P_ij と各制御点に対する重み w_ij,u 方向と v 方向のノットベクトル u_i,v_i によって定義される曲面です. 全ての重みが等しいと,Bスプライン曲面と等しくなります. L×K 個のパッチからなる n×m 次の NURBS 曲面は,パラメータ区間 , において定義され,次の式で与えられます.


GLU (OpenGL Utility Library の略,コンパイル時に -lGLU として利用しています)では,OpenGL エバリュエータ上に構築した NURBS インタフェースを提供しています.

次のプログラムは,-3.0 から 3.0 の制御点で,左右対称な丘のような形状の NURBS 曲面を描画するものです.

/* nurbs_surf.c */
#include <GL/glut.h>
#include <stdlib.h>
#include <math.h>

GLfloat ctlpoints[4][4][3];
int showPoints = 0;
GLfloat knots[8] = {0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0};

GLUnurbsObj *theNurb;

/* x, y, z において -3 から 3 の範囲で,制御点の初期化 */
void init_surface(void)
{
  int u, v;
  for (u = 0; u < 4; u++) {
    for (v = 0; v < 4; v++) {
      ctlpoints[u][v][0] = 2.0*((GLfloat)u - 1.5);
      ctlpoints[u][v][1] = 2.0*((GLfloat)v - 1.5);

      if ((u == 1 || u == 2) && (v == 1 || v == 2))
        ctlpoints[u][v][2] = 3.0;
      else
        ctlpoints[u][v][2] = -3.0;
    }
  }
}

void init(void)
{
  GLfloat mat_diffuse[] = { 0.7, 0.7, 0.7, 1.0 };
  GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
  GLfloat mat_shininess[] = { 100.0 };

  glClearColor(0.0, 0.0, 0.0, 1.0);
  glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
  glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
  glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);

  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glDepthFunc(GL_LESS);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_AUTO_NORMAL);  /* 法線の計算 */
  glEnable(GL_NORMALIZE);    /* 正規化 */

  init_surface();

  theNurb = gluNewNurbsRenderer();  /* NURBS オブジェクトを作成 */
  gluNurbsProperty(theNurb, GLU_SAMPLING_TOLERANCE, 25.0);
  gluNurbsProperty(theNurb, GLU_DISPLAY_MODE, GLU_FILL);
}

void display(void)
{
  int i, j;

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix();
  glRotatef(330.0, 1.0, 0.0, 0.0);
  glScalef(0.25, 0.25, 0.25);

  gluBeginSurface(theNurb);
  gluNurbsSurface(theNurb, 8, knots, 8, knots, 4 * 3, 3, 
                  &ctlpoints[0][0][0], 4, 4, GL_MAP2_VERTEX_3);
  gluEndSurface(theNurb);

  if(showPoints) {
    glPointSize(5.0);
    glDisable(GL_LIGHTING);
    glColor3f(1.0, 1.0, 0.0);
    glBegin(GL_POINTS);
    for(i = 0; i < 4 ; i++)
      for(j = 0; j < 4; j++)
        glVertex3f(ctlpoints[i][j][0],
                   ctlpoints[i][j][1],
                   ctlpoints[i][j][2]);
    glEnd();
    glEnable(GL_LIGHTING);
  }
  glPopMatrix();
  glFlush();
}

void resize(int w, int h)
{
  glViewport(0, 0, w, h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(45.0, (GLdouble)w/(GLdouble)h, 3.0, 8.0);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glTranslatef(0.0, 0.0, -5.0);
}

void keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case 'c':
  case 'C':
    showPoints = !showPoints;
    glutPostRedisplay();
    break;
  case '\33':
  case 'q':
  case 'Q':
    exit(0);
    break;
  default:
    break;
  }
}

int main(int argc, char** argv)
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
  glutCreateWindow(argv[0]);
  init();
  glutReshapeFunc(resize);
  glutDisplayFunc(display);
  glutKeyboardFunc(keyboard);
  glutMainLoop();
  return 0;
}

GLUnurbs* gluNewNurbsRenderer( void )
新規の NURBS オブジェクトのポインタを作成し,返します. このオブジェクトは,NURBS のレンダリングと制御関数をコールする場合に参照します.
void gluNurbsProperty( GLUnurbs* nurb, GLenum property, GLfloat value )
NRUBS オブジェクト nobj の属性を制御します.
property で設定する属性を指定します. GLU_SAMPLING_TOLERANCE, GLU_DISPLAY_MODE, GLU_CULLING, GLU_AUTO_LOAD_MATRIX, GLU_PARAMETRIC_TOLERANCE, GLU_SAMPLING_METHOD, GLU_U_STEP, GLU_V_STEP, GLU_NURBS_MODE_EXT を指定可能です.
value には, 指示した属性として設定する値を指定します.
property に GLU_SAMPLING_TOLERANCE を指定した場合,value は,NURBS 曲面や曲線の描画に使用される多角形の線分や辺の最大長を ピクセルで定義します.初期値は 50.0 ピクセルです.
property に GLU_DISPLAY_MODE を指定した場合,value は NURBS 曲面の描画方法を定義します. 初期値は GLU_FILL で,多角形の集合として描画されます. ほかに,多角形の輪郭のみを描く GLU_OUTLINE_PORYGON や, ユーザが設定したパッチの輪郭やトリミング曲線を作成する GLU_OUTLINE_PATCH を設定できます.
その他の設定については,man gluNurbsProperty を参照してください.
void gluNurbsSurface( GLUnurbs* nurb, GLint sKnotCount, GLfloat* sKnots, GLint tKnotCount, GLfloat* tKnots, GLint sStride, GLint tStride, GLfloat* control, GLint sOrder, GLint tOrder, GLenum type )
NURBS 曲面の形を決定します.
nurb には NURBS オブジェクトを指定します.
sKnotCount / tKnotCount には u / v 方向のノットの数を指定します.
sKnots / tKnots には u / v 方向のノットの配列を指定します.
sStride / tStride には u / v 方向の座標をみていく間隔を指定します.
control では NURBS 曲面の制御点を含む配列を指定します.sStride と tStride で与えられる間隔で値を参照します.
sOrder / tOrder では,u / v 方向の NURBS 曲面の次数を指定します.
type では,曲面の型式を指定します. 有効な2次元のエバリュエータ(GL_MAP2_VERTEX_3 や GL_MAP2_COLOR_4 など)であれば,どの型式でも指定可能です.
void gluBeginSurface( GLUnurbs* nurb ), void gluEndSurface( GLUnurbs* nurb )
NURBS 曲面の定義の範囲を設定します.gluBeginSurface を使用して,NURBS 曲面の定義の始点をマークします.その後,gluNurbsSurface を1回以上呼んで, その面の属性を定義します.そのコールの中に GL_MAP2_VERTEX_3 か GL_MAP2_VERTEX_4 の面型式が含まれている必要があります. NURBS 曲面の定義の終点をマークするのに gluEndSurface を呼びます.
void glDepthFunc( GLenum func )
デプスバッファの比較に使用する値の設定をします. GL_NEVER, GL_LESS, GL_EQUAL, GL_LEQUAL, GL_GREATER, GL_NOTEQUAL, GL_GEQUAL, GL_ALWAYS を指定可能です. 初期値は GL_LESS で,この場合, 入力 z 値が保存されている z 値よりも小さいときにパスします. このプログラムでは,初期設定の値を指定しているので, コメントアウトしても表示結果に変化はありませんが, 指定可能であることを知っておきましょう. その他の値の意味は man glDepthFunc を参照してください.

GLU を利用して NURBS 曲線&曲面を描く場合,まず最初に,その NURBS に関する情報を格納するための GLUnurbsObj 型の構造体を割り当てる必要があります. そして,実際に NURBS 曲面の描画を始めるときは,gluBeginSurface() と gluEndSurface() とで gluNurbsSurface() をはさんで使用します. gluNurbsSurface() で指定する u と v の次数や配列は,NURBS 曲面の式にも変数として登場しているものであることを確認してください. なお,ノット配列 sKnots,tKnots には,実用上は,Bスプラインの場合,0,1,2…と単調に等間隔で増加, ベジエの場合,配列の前半分が 0,後ろ半分が 1,という, いずれかの配列を用いる場合が多いようです.

[index]


10.7 ユタのティーポット

ここまで,あまりおもしろみのない曲面データばかりだったかと思います. そこで,形に意味のある曲面データの例をみてみましょう. CGの分野で,新しいレンダリングアルゴリズムを提案したり, アルゴリズムの速度を比較したりするときに, 標準の物体として使われたことで有名なティーポットのデータがあります. これは,ユタ大学に実際にあったティーポットから座標を算出し, ベジエ曲面の制御点が定められたものです. 左の画像において,太線はベジエ曲面の境界をあらわします.
双3次ベジエ曲面で表されたティーポット

このティーポットを描く関数が,GLUT の関数として用意されています. それは,void glutSolidTeapot(GLdouble size) や, void glutWireTeapot(GLdouble size) という, 引数に大きさを入れるだけでティーポットを作成するというものです. その関数をつかっているプログラム例が, GLUT のサンプルプログラムにあります. このファイルを自分のところに保存して, コンパイル&実行してみましょう.このプログラムは, いろいろな材質感を表現しているところも参考になりますので, そこらへんにもちょっと注意してみましょう.

さて,ティーポットデータがレンダリングされた様子を観察したところで, この glutSolidTeapot や glutWireTeapot がどのようにプログラミングされているか,見てみましょう.

ティーポットを描く関数は,GLUT ライブラリのソースコードのうち,glut_teapot.c というそのものズバリな名前のファイルにかかれています.

/* Copyright (c) Mark J. Kilgard, 1994. */
#include <GL/gl.h>
#include <GL/glut.h>

/* Rim, body, lid, and bottom data must be reflected in x and y;
   handle and spout data across the y axis only.  */

static int patchdata[][16] =
{
  {102,103,104,105,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15},/* rim */
  { 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27},/* body */
  { 24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40},
  { 96, 96, 96, 96, 97, 98, 99,100,101,101,101,101,  0,  1,  2, 3,},/* lid */
  {  0,  1,  2,  3,106,107,108,109,110,111,112,113,114,115,116,117},
  {118,118,118,118,124,122,119,121,123,126,125,120, 40, 39, 38, 37},/* bottom */
  { 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56},/* handle */
  { 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 28, 65, 66, 67},
  { 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83},/* spout */
  { 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95}
};

static float cpdata[][3] =
{
  {0.2, 0, 2.7},                /*  0 */
  {0.2, -0.112, 2.7}, 
  {0.112, -0.2, 2.7}, 
  {0, -0.2, 2.7}, 
  {1.3375, 0, 2.53125}, 
  {1.3375, -0.749, 2.53125},
  {0.749, -1.3375, 2.53125}, 
  {0, -1.3375, 2.53125}, 
  {1.4375, 0, 2.53125}, 
  {1.4375, -0.805, 2.53125}, 
  {0.805, -1.4375, 2.53125},    /* 10 */
  {0, -1.4375, 2.53125}, 
  {1.5, 0, 2.4}, 
  {1.5, -0.84, 2.4}, 
  {0.84, -1.5, 2.4}, 
  {0, -1.5, 2.4}, 
  {1.75, 0, 1.875},
  {1.75, -0.98, 1.875}, 
  {0.98, -1.75, 1.875}, 
  {0, -1.75, 1.875}, 
  {2, 0, 1.35},                 /* 20 */
  {2, -1.12, 1.35}, 
  {1.12, -2, 1.35},
  {0, -2, 1.35}, 
  {2, 0, 0.9}, 
  {2, -1.12, 0.9}, 
  {1.12, -2, 0.9}, 
  {0, -2, 0.9}, 
  {-2, 0, 0.9}, 
  {2, 0, 0.45}, 
  {2, -1.12, 0.45},             /* 30 */
  {1.12, -2, 0.45}, 
  {0, -2, 0.45}, 
  {1.5, 0, 0.225},
  {1.5, -0.84, 0.225}, 
  {0.84, -1.5, 0.225}, 
  {0, -1.5, 0.225},
  {1.5, 0, 0.15}, 
  {1.5, -0.84, 0.15}, 
  {0.84, -1.5, 0.15}, 
  {0,-1.5, 0.15},               /* 40 */
  {-1.6, 0, 2.025}, 
  {-1.6, -0.3, 2.025}, 
  {-1.5, -0.3, 2.25}, 
  {-1.5, 0, 2.25}, 
  {-2.3, 0, 2.025}, 
  {-2.3, -0.3, 2.025}, 
  {-2.5, -0.3, 2.25}, 
  {-2.5, 0, 2.25}, 
  {-2.7, 0, 2.025}, 
  {-2.7, -0.3, 2.025},          /* 50 */
  {-3, -0.3, 2.25}, 
  {-3, 0, 2.25}, 
  {-2.7, 0, 1.8}, 
  {-2.7, -0.3, 1.8}, 
  {-3, -0.3, 1.8},
  {-3, 0, 1.8}, 
  {-2.7, 0, 1.575}, 
  {-2.7, -0.3, 1.575}, 
  {-3, -0.3, 1.35}, 
  {-3, 0, 1.35},                /* 60 */
  {-2.5, 0, 1.125}, 
  {-2.5, -0.3, 1.125}, 
  {-2.65, -0.3, 0.9375}, 
  {-2.65, 0, 0.9375}, 
  {-2, -0.3, 0.9}, 
  {-1.9, -0.3, 0.6}, 
  {-1.9, 0, 0.6}, 
  {1.7, 0, 1.425}, 
  {1.7, -0.66, 1.425}, 
  {1.7, -0.66, 0.6},            /* 70 */
  {1.7, 0, 0.6}, 
  {2.6, 0, 1.425}, 
  {2.6, -0.66, 1.425}, 
  {3.1, -0.66, 0.825}, 
  {3.1, 0, 0.825}, 
  {2.3, 0, 2.1}, 
  {2.3, -0.25, 2.1},
  {2.4, -0.25, 2.025}, 
  {2.4, 0, 2.025}, 
  {2.7, 0, 2.4},                /* 80 */
  {2.7, -0.25, 2.4}, 
  {3.3, -0.25, 2.4}, 
  {3.3, 0, 2.4}, 
  {2.8, 0, 2.475}, 
  {2.8, -0.25, 2.475}, 
  {3.525, -0.25, 2.49375},
  {3.525, 0, 2.49375}, 
  {2.9, 0, 2.475}, 
  {2.9, -0.15, 2.475},
  {3.45, -0.15, 2.5125},        /* 90 */
  {3.45, 0, 2.5125}, 
  {2.8, 0, 2.4},
  {2.8, -0.15, 2.4}, 
  {3.2, -0.15, 2.4}, 
  {3.2, 0, 2.4}, 
  {0, 0, 3.15}, 
  {0.8, 0, 3.15}, 
  {0.8, -0.45, 3.15}, 
  {0.45, -0.8, 3.15}, 
  {0, -0.8, 3.15},              /* 100 */
  {0, 0, 2.85}, 
  {1.4, 0, 2.4}, 
  {1.4, -0.784, 2.4}, 
  {0.784, -1.4, 2.4}, 
  {0, -1.4, 2.4}, 
  {0.4, 0, 2.55}, 
  {0.4, -0.224, 2.55}, 
  {0.224, -0.4, 2.55}, 
  {0, -0.4, 2.55}, 
  {1.3, 0, 2.55},               /* 110 */
  {1.3, -0.728, 2.55}, 
  {0.728, -1.3, 2.55}, 
  {0, -1.3, 2.55}, 
  {1.3, 0, 2.4}, 
  {1.3, -0.728, 2.4},
  {0.728, -1.3, 2.4}, 
  {0, -1.3, 2.4}, 
  {0, 0, 0}, 
  {1.425, -0.798, 0}, 
  {1.5, 0, 0.075},              /* 120 */
  {1.425, 0, 0}, 
  {0.798, -1.425, 0}, 
  {0, -1.5, 0.075}, 
  {0, -1.425, 0}, 
  {1.5, -0.84, 0.075},
  {0.84, -1.5, 0.075}           /* 126 */
};

static float tex[2][2][2] =
{
  { {0, 0},
    {1, 0}},
  { {0, 1},
    {1, 1}}
};


static void 
teapot(GLint grid, GLdouble scale, GLenum type)
{
  float p[4][4][3], q[4][4][3], r[4][4][3], s[4][4][3];
  long i, j, k, l;

  glPushAttrib(GL_ENABLE_BIT | GL_EVAL_BIT);
  glEnable(GL_AUTO_NORMAL);
  glEnable(GL_NORMALIZE);
  glEnable(GL_MAP2_VERTEX_3);
  glEnable(GL_MAP2_TEXTURE_COORD_2);

  glPushMatrix();
  glRotatef(270.0, 1.0, 0.0, 0.0);
  glScalef(0.5 * scale, 0.5 * scale, 0.5 * scale);
  glTranslatef(0.0, 0.0, -1.5);
  for (i = 0; i < 10; i++) {
    for (j = 0; j < 4; j++) {
      for (k = 0; k < 4; k++) {
          p[j][k][l] = cpdata[patchdata[i][j*4 + k]][l];

          /* 座標を逆順に呼び、さらに y 座標を反転させて、反対側を作る */
          q[j][k][l] = cpdata[patchdata[i][j*4 + (3-k)]][l];
          if (l == 1)   q[j][k][l] *= -1.0;

          if (i < 6) {  
            /* 取手 と 注ぎ口 でないならば 
               x 座標 または x, y 座標 を反転させて、反対側も作る */
            r[j][k][l] = cpdata[patchdata[i][j*4 + (3-k)]][l];
            if (l == 0) r[j][k][l] *= -1.0;

            s[j][k][l] = cpdata[patchdata[i][j*4 + k]][l];
            if (l == 0) s[j][k][l] *= -1.0;
            if (l == 1) s[j][k][l] *= -1.0;
          }
        }
      }
    }
    glMap2f(GL_MAP2_TEXTURE_COORD_2, 0, 1, 2, 2, 0, 1, 4, 2, &tex[0][0][0]);
    glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &p[0][0][0]);
    glMapGrid2f(grid, 0.0, 1.0, grid, 0.0, 1.0);
    glEvalMesh2(type, 0, grid, 0, grid);
    glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &q[0][0][0]);
    glEvalMesh2(type, 0, grid, 0, grid);

    if (i < 6) {        /* 取手 と 注ぎ口 でないならば */
      glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &r[0][0][0]);
      glEvalMesh2(type, 0, grid, 0, grid);
      glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &s[0][0][0]);
      glEvalMesh2(type, 0, grid, 0, grid);
    }
  }
  glPopMatrix();
  glPopAttrib();
}

void
glutSolidTeapot(GLdouble scale)
{
  teapot(14, scale, GL_FILL);
}

void
glutWireTeapot(GLdouble scale)
{
  teapot(10, scale, GL_LINE);
}

まず最初に,127 の制御点(cpdata)と, どの制御点を16個ずつ使って一つの面を表現するのかを示す配列(patchdata)とを宣言 しています.形状の対称性を反映し, 座標を反転させれば作ることのできる面, 例えば,胴体やフタに関しては, 全体の 1/4 分だけ制御点が用意してあって,残りの 3/4 分は, 座標値を反転させて制御点データを生成します. 生成した16個ずつの制御点データは, p, q, r, s という配列に格納され, それが glMap2f に渡されます. その後は,10.5節でベジエ曲面をはったのと同様の手順で 面をはっています. テクスチャ座標も計算していますが,それに関しては特に触れないことにします.

実際にティーポットを描く記述は,glut_teapot.c 内で定義している teapot 関数内に書かれていて,この teapot 関数を さらに glutSolidTeapot や glutWireTeapot から呼び出して使っています. こうすることで,GLUT ユーザには,ややこしい処理を見せることなく, とりあえず,大きさだけ与えればティーポットを描けるという, お手軽な関数を用意しています.

さて,先ほどのteapots.cのことですが, いろいろな材質感を表現しているところを見てみましょう,などと言っても, これまでの経験上,もともと勉強熱心な人は見てくれますが, そうでない人は見てくれないようなので,簡単な課題を出します.

[index]