第四週 スクリプトによるコマンド入力+モーフィング (2005)
計算機序論2, 
www.kameda-lab.org 
2005/10/24
実習にあたって
    今週が演習については実質最後の週です(最終回は発表会)。
    今週の目的は、スクリプトによりグラフィックスを自由に制御できるように
    することと、モーフィングと呼ばれるグラフィックスのテクニックを扱える
    ようになることです。
スクリプトによる描画の書式
    まずは、スクリプトの概要について考えよう。
    サンプルプログラムでは、以下のような書式を設定している。
    コメント(#),データ入力(L, F, P),描画制御(C, S),
    標準的な描画(T, D),応用的な描画(I, M),繰り返し・終了(E, Z),
    等を用意している。
//  コマンド(外部仕様):
//
//  #: コメント (その行は無視される)
//
//  L: [Line]線分の登録
//     L sx sy sz ex ey ez r g b w
//  F: [Figure]そこまでをひとまとまりの図形として宣言(登録順に基いてfigID付与)
//     F
//  P: [Parameter]幾何変換の登録(登録順に基いてparamID付与)
//     P dx dy dz sx sy sz tx ty tz
//
//  C: [Clear]画面をクリア
//     C
//  S: [Sleep]スリープ (usleep [msec])
//     S usleep
//
//  T: [Translate]指定した幾何変換をセット
//     T paramID
//  D: [Draw]指定された図形を描画
//     D figID
//
//  I: [Interpolation]スムーズに幾何変換パラメータを変更(補間)しながら描画
//     I F P1 P2 times usleep
//     (図形Fを変換P1からP2の間, times刻みで補間しながら描画.
//      途中でusleepずつスリープする)
//  M: [Morphing]モーフィング
//     M F1 P1 F2 P2 times usleep 
//     (図形F1を変換P1したものと図形F2を変換P2したものとのモーフィング)
//
//  E: ここまでの描画順序を無限に繰り返す
//     (ただしこのプログラムではこれを指定するとキーを受け付けなくなる)
//     E
//  Z: これで終了
//     Z
// 
    データの宣言から描画の制御までを一つのファイルに
    書いておくことにより、描画を順番に実行できるようになる。
    今後、CGを変えようとしたときに、プログラムを書き換える
    必要が一切なくなり、格段に扱いやすくなる。
    実際には以下のようにファイルに書く。
    まず,"L","F"等で図形データを登録し、
    次に,"P"で幾何変換パラメータを登録する。
    その後、描画やその制御コマンドを記述する。
    図形データと貴下変換パラメータはどちらも
    登録順にID番号がつくことに注意しよう。
# F0: Y
L       -0.5  0.5 0.0   -0.25  0.0 0.0  1.0 0.5 0.5     3.0
L        0.0  0.5 0.0   -0.25  0.0 0.0  1.0 0.5 0.5     3.0
L       -0.25 0.0 0.0   -0.25 -0.5 0.0  1.0 0.5 0.5     3.0
F
# F1: N
L       0.0  0.5 0.0    0.0 -0.5 0.0    0.5 0.0 1.0     5.0
L       0.0  0.5 0.0    0.5 -0.5 0.0    0.5 0.0 1.0     5.0
L       0.5 -0.5 0.0    0.5  0.5 0.0    0.5 0.0 1.0     5.0
F
(中略)
# 幾何変換 P0〜P3
P       0.3 0.3 -2.0    0.5 0.5 0.5     0.0   0.0   0.0
P       0.0 0.0 -2.0    0.3 0.3 0.5     0.0   0.0 180.0
P       0.2 0.2 -2.0    0.5 0.5 0.5     0.0 180.0   0.0
P       0.0 0.0 -2.0    0.7 0.7 0.5     0.0   0.0   0.0
(中略)
# ここから描画コマンド
C	
T       1
D       1
S       500
(中略)
# スムーズな幾何変換
#       F       P1      P2      times   usleep
I       1       1       2       30      10
(中略)
# モーフィング
#       F1      P1      F2      P2      times   usleep
M	1	1	0	2	30	10
S       500
M       0       2       1       1       30      10
(中略)
# 終了 (Eにすれば,ループとなる)
Z
    余裕のある人は、コマンドを自分で新しく定義し、
    もっと多様なことができるように拡張するのも面白いだろう。
    基本的には,read_commandfile_and_execure 関数の辺りを理解すれば、
    拡張はできるはずである。
    
モーフィングの簡単なサンプル
    まずは、プログラム例を
    見てみよう。LINE_interpolation(), draw_morphed_FIGURE,
    draw_morphing_FIGURE_seq() がモーフィングのための関数である。
    これらの関数は、ある図形から別の図形へ、
    徐々に変わっていく様子を描画するものである。
    今回のプログラムでは、一つの線分から対応する別の線分への変化を
    なめらかに行う。
     まず,図形として与えられたfigure1とfigure2の間で,
     その中間の図形をアニメーションさせながら描画する関数
     draw_morphing_FIGURE_seq()が呼ばれる.
     その関数の中では、アニメーションの1フレーム毎に
     パラメータを変更しながら,2図形のモーフィング図形を描く関数
     draw_eah_morphing_FIGURE()を呼ぶ。
     このdraw_eah_morphing_FIGURE()内部では、
     2つの対応する線分の間の補間をする関数LINE_interpolation()が
     呼び出され、その結果を描画している。
     
//================================================================
// 2つの図形の中間段階を描く
void draw_morphed_FIGURE(struct FIGURE *figure1, 
				struct FIGURE *figure2, 
				struct PARAMETER *parameter1, 
				struct PARAMETER *parameter2, 
				double ratio) {
  struct LINE *line1, *line2;
  struct LINE *transformed_line1, *transformed_line2;
  struct LINE *intermediate_line;
  if (figure1 == NULL || figure2 == NULL ||
      parameter1 == NULL || parameter2 == NULL) 
    return;
  line1 = figure1->first_line;
  line2 = figure2->first_line;
  
  while (line1 != NULL && line2 != NULL) {
    // line1をparameter1で変換したものと, 
    // line2をparameter2で変換したものの補間をする
    transformed_line1 = LINE_transform (line1, parameter1);
    transformed_line2 = LINE_transform (line2, parameter2);
    intermediate_line = LINE_interpolation
      (transformed_line1, transformed_line2, ratio);
    normalized_draw_LINE (intermediate_line);
    line1 = line1->next;
    line2 = line2->next;
    free_LINE(transformed_line1);
    free_LINE(transformed_line2);
    free_LINE(intermediate_line);
  } 
}
//================================================================
// 図形間を順にモーフィングしていく.中間段階を順に描いていく
void draw_morphing_FIGURE_seq(struct FIGURE *figure1, 
			      struct FIGURE *figure2, 
			      struct PARAMETER *parameter1, 
			      struct PARAMETER *parameter2, 
			      int times, float sleep) {
  int i;
  if (figure1 == NULL || figure2 == NULL ||
      parameter1 == NULL || parameter2 == NULL) 
    return;
  
  for (i=0; i <= times; i++){
    glClear (GL_COLOR_BUFFER_BIT);
    draw_morphed_FIGURE(figure1, figure2, parameter1, parameter2,
			(double)i/times);
    glutSwapBuffers();
    glFlush();
    usleep(sleep * 1000);
  }
}
    モーフィングの要点は以下の関数に集約される。
    2つの線分の始点どうし、終点どうしの間の点を各々求めているだけである。
    この関数では、2つの線分を引数として、その中間に位置する線分情報を返す。
    構造体を返す関数であることに注意。
//================================================================
// 2つの線の中間位置を求め, モーフィングされた線分の構造体として返す
struct LINE *LINE_interpolation(struct LINE *line1, 
				       struct LINE *line2, 
				       double ratio) {
  struct LINE *newline;
  double r1, r2;
  if (line1 == NULL || line2 == NULL) return NULL;
  newline = new_LINE();
  if (newline == NULL) return NULL;
  r1 = 1 - ratio;
  r2 = ratio;
  newline->start->x = r1 * line1->start->x + r2 * line2->start->x;
  // (残りは自分で埋めよ。 end pointの他にもrgbと幅を忘れないように)
  return (newline);
}
今週のサンプルプログラムは、先週のサンプルプログラムからまた関数が
増えたので、長さは1000行以上にもなる。
もちろん、このままでも問題はないが、長いソースファイルは、
プログラミングの見通しを非常に悪くする。
そこで、プログラムの分割を行うことを考えよう。
分割を行う場合には、以下のような方針に従うと良い。
- どの関数からみても必須な情報で、実体を伴わないものは、
共通ヘッダファイルにまとめる。
  
  - よく用いる外部ライブラリのインクルードファイル
  
- あちこちで使いそうなマクロの定義
  
- 定数
  
- データ構造体の定義
  
- 関数のプロトタイプ宣言
  
- 大域変数のextern宣言
  
 
- 可能であれば、大域変数は
    一つのファイルにまとめ、一覧しやすいようにする。デバッグ時に特に有効。
- もとの大域変数のうち、他のファイルで参照しないような大域変数がある場合、
    分割後は大域変数とせず、そのファイル内部でのみ相互参照が可能な
    "static"変数にする。
- 似たような機能の関数は同じファイルにまとめる。
- それぞれの分割ファイルにおいて、
    他のファイルに公開する関数と
    公開しない関数(そのファイル内でしか呼び出さない)に分類する。
    公開する関数はプロトタイプ宣言を共通ヘッダファイルに記載する。
    公開しない関数は"staic"宣言を関数定義時に行う。
- それぞれの分割ファイルにおいて、先頭で共通ヘッダファイルを導入する。
    カレントディレクトリに存在する場合は、ファイル名をダブルクォートで
    括って指定することに注意。
このようにして複数のファイルができあがる。ここから実行ファイルを作るには、
まず下記のようにしてそれぞれオブジェクトファイル(*.o)を作成する。
gcc -c -O2 -Wall kj2-3Dscript-animation.c 
gcc -c -O2 -Wall kj2-3Dscript-datastructure.c
gcc -c -O2 -Wall kj2-3Dscript-debug.c
gcc -c -O2 -Wall kj2-3Dscript-drawfigure.c
gcc -c -O2 -Wall kj2-3Dscript-file.c
gcc -c -O2 -Wall kj2-3Dscript-geometry.c
gcc -c -O2 -Wall kj2-3Dscript-global.c
gcc -c -O2 -Wall kj2-3Dscript-glut-events.c
gcc -c -O2 -Wall kj2-3Dscript-matrix.c
gcc -c -O2 -Wall kj2-3Dscript-main.c
続いて、オブジェクトファイルをリンクする。
gcc -o 3Dscript -L/usr/X11R6/lib -lX11 -lXmu -lXi -lGL -lGLU -lglut -lm kj2-3Dscript-animation.o kj2-3Dscript-datastructure.o kj2-3Dscript-debug.o kj2-3Dscript-drawfigure.o kj2-3Dscript-file.o kj2-3Dscript-geometry.o kj2-3Dscript-global.o 	kj2-3Dscript-glut-events.o kj2-3Dscript-matrix.o kj2-3Dscript-main.o
このような作業をコマンドラインから毎回行うのは大変である。
そこで、このような歴史的経緯を経て make コマンドが開発された。
makeコマンドはそのディレクトリに置いてあるMakefileという設定ファイルに従って
様々な作業を代行してくれる。
詳細は今回のMakefileを確認してほしいが、
この設定ファイルを用いれば、
make 3Dscript
とするだけで、上述の作業を全て行ってくれる。
また、プログラムファイルの一部で変更が生じた場合、
そのプログラムファイルについてだけオブジェクトファイルを作成し直し、
リンクし直して最新版の実行ファイルを生成してくれる。
なお、Makefileの中では「行の先頭が文字かどうか」や「空行」「TAB」が
重要な意味を持つので、改変する場合はそのことに注意すること。
興味のある人は、自分でライブラリを作ることもできる。
難しいことではなく、「ライブラリ」=「オブジェクトファイルの集合体」と
いうことさえ理解できればライブラリ化は案外容易である。
いま、"kj2"というライブラリを作ってみよう。arコマンドを使う。
ar r libkj2.a kj2-3Dscript-animation.o kj2-3Dscript-datastructure.o kj2-3Dscript-debug.o kj2-3Dscript-drawfigure.o kj2-3Dscript-file.o kj2-3Dscript-geometry.o kj2-3Dscript-global.o kj2-3Dscript-glut-events.o kj2-3Dscript-matrix.o
このようにすると、libkj2.aができる(含まれるオブジェクトファイルはmain関数さえ含まなければどのような組み合わせでもよい)。
このlibkj2.aがカレントディレクトリにある場合、それを利用すれば
gcc -o 3Dscript -L/usr/X11R6/lib -lX11 -lXmu -lXi -lGL -lGLU -lglut -lm kj2-3Dscript-animation.o kj2-3Dscript-datastructure.o kj2-3Dscript-debug.o kj2-3Dscript-drawfigure.o kj2-3Dscript-file.o kj2-3Dscript-geometry.o kj2-3Dscript-global.o kj2-3Dscript-glut-events.o kj2-3Dscript-matrix.o kj2-3Dscript-main.o
は
gcc -o 3Dscript kj2-3Dscript-main.o -L/usr/X11R6/lib -lX11 -lXmu -lXi -lGL -lGLU -lglut -lm -L. lkj2 
と同じことである("-L. -lkj2" は ./libkj2.a の指定を意味している)。
上手に利用すれば、あまり変更しない完成された関数が入ったプログラムファイルはライブラリにして、必要な部分だけオブジェクトファイルにする、ということができるようになる。
サンプルプログラムは、いつものようにそのままではコンパイルして実行しても
何も表示されない。ソースコードをよく読んで、埋めるべき箇所を埋めよう。
ソースコードが長くなってきた(サンプルプログラムで1000行以上ある)ので、
分割コンパイルを試みてみるのも良い。
以下のファイルはスクリプトの例である。
Yoshinari Kameda: 2004/09/17-
Yuichi Nakamura: Tue Aug 12 00:41:10 JST 2003